|
@@ -40,7 +40,7 @@ def parse_data(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
class BusinessCard(db.Model):
|
|
class BusinessCard(db.Model):
|
|
__tablename__ = 'business_cards'
|
|
__tablename__ = 'business_cards'
|
|
|
|
|
|
- id = db.Column(db.Integer, primary_key=True)
|
|
|
|
|
|
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
name_zh = db.Column(db.String(100), nullable=False)
|
|
name_zh = db.Column(db.String(100), nullable=False)
|
|
name_en = db.Column(db.String(100))
|
|
name_en = db.Column(db.String(100))
|
|
title_zh = db.Column(db.String(100))
|
|
title_zh = db.Column(db.String(100))
|
|
@@ -498,13 +498,13 @@ def parse_text_with_qwen25VLplus(image_data):
|
|
)
|
|
)
|
|
|
|
|
|
# 构建优化后的提示语
|
|
# 构建优化后的提示语
|
|
- prompt = """你是专业的名片信息提取助手。请仔细分析图片中的名片,精确提取以下信息:
|
|
|
|
|
|
+ prompt = """你是企业名片的信息提取专家。请仔细分析提供的名片,精确提取以下信息:
|
|
|
|
|
|
## 提取要求
|
|
## 提取要求
|
|
- 区分中英文内容,分别提取
|
|
- 区分中英文内容,分别提取
|
|
- 保持提取信息的原始格式(如大小写、标点)
|
|
- 保持提取信息的原始格式(如大小写、标点)
|
|
- 对于无法识别或名片中不存在的信息,返回空字符串
|
|
- 对于无法识别或名片中不存在的信息,返回空字符串
|
|
-
|
|
|
|
|
|
+- 名片中没有的信息,请不要猜测
|
|
## 需提取的字段
|
|
## 需提取的字段
|
|
1. 中文姓名 (name_zh)
|
|
1. 中文姓名 (name_zh)
|
|
2. 英文姓名 (name_en)
|
|
2. 英文姓名 (name_en)
|
|
@@ -520,8 +520,8 @@ def parse_text_with_qwen25VLplus(image_data):
|
|
12. 中文邮政编码 (postal_code_zh)
|
|
12. 中文邮政编码 (postal_code_zh)
|
|
13. 英文邮政编码 (postal_code_en)
|
|
13. 英文邮政编码 (postal_code_en)
|
|
14. 品牌组合 (brand_group) - 如有多个品牌,使用逗号分隔
|
|
14. 品牌组合 (brand_group) - 如有多个品牌,使用逗号分隔
|
|
-15. 职业轨迹 (career_path) - 如能从名片中推断,以JSON数组格式返回
|
|
|
|
-
|
|
|
|
|
|
+15. 职业轨迹 (career_path) - 如能从名片中推断,以JSON数组格式返回,包含当前日期,公司名称和职位
|
|
|
|
+16. 隶属关系 (affiliation) - 如能从名片中推断,以JSON数组格式返回,包含公司名称和隶属集团名称
|
|
## 输出格式
|
|
## 输出格式
|
|
请以严格的JSON格式返回结果,不要添加任何额外解释文字。JSON格式如下:
|
|
请以严格的JSON格式返回结果,不要添加任何额外解释文字。JSON格式如下:
|
|
```json
|
|
```json
|
|
@@ -540,7 +540,8 @@ def parse_text_with_qwen25VLplus(image_data):
|
|
"postal_code_zh": "",
|
|
"postal_code_zh": "",
|
|
"postal_code_en": "",
|
|
"postal_code_en": "",
|
|
"brand_group": "",
|
|
"brand_group": "",
|
|
- "career_path": []
|
|
|
|
|
|
+ "career_path": [],
|
|
|
|
+ "affiliation": []
|
|
}
|
|
}
|
|
```"""
|
|
```"""
|
|
|
|
|
|
@@ -797,6 +798,66 @@ def update_business_card(card_id, data):
|
|
# 保存更新
|
|
# 保存更新
|
|
db.session.commit()
|
|
db.session.commit()
|
|
|
|
|
|
|
|
+ # 更新成功后,更新Neo4j图数据库中的人才-酒店关系
|
|
|
|
+ try:
|
|
|
|
+ from app.services.neo4j_driver import neo4j_driver
|
|
|
|
+ from app.core.graph.graph_operations import create_or_get_node
|
|
|
|
+
|
|
|
|
+ # 获取当前时间
|
|
|
|
+ current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
+
|
|
|
|
+ # 创建或更新人才节点
|
|
|
|
+ talent_properties = {
|
|
|
|
+ 'pg_id': card_id, # PostgreSQL数据库中的ID
|
|
|
|
+ 'name_zh': card.name_zh, # 中文姓名
|
|
|
|
+ 'name_en': card.name_en, # 英文姓名
|
|
|
|
+ 'mobile': card.mobile, # 手机号码
|
|
|
|
+ 'email': card.email, # 电子邮箱
|
|
|
|
+ 'updated_at': current_time # 更新时间
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ talent_node_id = create_or_get_node('talent', **talent_properties)
|
|
|
|
+
|
|
|
|
+ # 如果有酒店信息,创建或更新酒店节点
|
|
|
|
+ if card.hotel_zh or card.hotel_en:
|
|
|
|
+ hotel_properties = {
|
|
|
|
+ 'hotel_zh': card.hotel_zh, # 酒店中文名称
|
|
|
|
+ 'hotel_en': card.hotel_en, # 酒店英文名称
|
|
|
|
+ 'updated_at': current_time # 更新时间
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ hotel_node_id = create_or_get_node('hotel', **hotel_properties)
|
|
|
|
+
|
|
|
|
+ # 创建或更新人才与酒店之间的WORK_FOR关系
|
|
|
|
+ if talent_node_id and hotel_node_id:
|
|
|
|
+ # 构建Cypher查询以创建或更新关系
|
|
|
|
+ cypher_query = """
|
|
|
|
+ MATCH (t:talent), (h:hotel)
|
|
|
|
+ WHERE id(t) = $talent_id AND id(h) = $hotel_id
|
|
|
|
+ MERGE (t)-[r:WORKS_FOR]->(h)
|
|
|
|
+ SET r.title_zh = $title_zh,
|
|
|
|
+ r.title_en = $title_en,
|
|
|
|
+ r.updated_at = $updated_at
|
|
|
|
+ RETURN r
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ with neo4j_driver.get_session() as session:
|
|
|
|
+ session.run(
|
|
|
|
+ cypher_query,
|
|
|
|
+ talent_id=talent_node_id,
|
|
|
|
+ hotel_id=hotel_node_id,
|
|
|
|
+ title_zh=card.title_zh,
|
|
|
|
+ title_en=card.title_en,
|
|
|
|
+ updated_at=current_time
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ logging.info(f"已成功更新人才(ID:{talent_node_id})与酒店(ID:{hotel_node_id})的WORK_FOR关系")
|
|
|
|
+
|
|
|
|
+ logging.info(f"Neo4j图数据库关系更新成功")
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logging.error(f"更新Neo4j图数据库关系失败: {str(e)}", exc_info=True)
|
|
|
|
+ # 不因为图数据库更新失败而影响PostgreSQL数据库的更新结果
|
|
|
|
+
|
|
return {
|
|
return {
|
|
'code': 200,
|
|
'code': 200,
|
|
'success': True,
|
|
'success': True,
|
|
@@ -1233,4 +1294,142 @@ def delete_talent_tag(tag_id):
|
|
'success': False,
|
|
'success': False,
|
|
'message': error_msg,
|
|
'message': error_msg,
|
|
'data': None
|
|
'data': None
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+def query_neo4j_graph(query_requirement):
|
|
|
|
+ """
|
|
|
|
+ 查询Neo4j图数据库,通过Deepseek API生成Cypher脚本
|
|
|
|
+
|
|
|
|
+ Args:
|
|
|
|
+ query_requirement (str): 查询需求描述
|
|
|
|
+
|
|
|
|
+ Returns:
|
|
|
|
+ dict: 包含查询结果的字典,JSON格式
|
|
|
|
+ """
|
|
|
|
+ try:
|
|
|
|
+ # 导入必要的模块
|
|
|
|
+ from app.services.neo4j_driver import neo4j_driver
|
|
|
|
+ import requests
|
|
|
|
+ import json
|
|
|
|
+
|
|
|
|
+ # Deepseek API配置
|
|
|
|
+ api_key = DEEPSEEK_API_KEY
|
|
|
|
+ api_url = DEEPSEEK_API_URL
|
|
|
|
+
|
|
|
|
+ # 构建提示文本,描述图数据库结构和查询需求
|
|
|
|
+ prompt = f"""
|
|
|
|
+ 请根据以下Neo4j图数据库结构和查询需求,生成一个Cypher查询脚本。
|
|
|
|
+
|
|
|
|
+ ## 图数据库结构
|
|
|
|
+
|
|
|
|
+ ### 节点
|
|
|
|
+ 1. talent - 人才节点
|
|
|
|
+ 属性: pg_id(PostgreSQL数据库ID), name_zh(中文姓名), name_en(英文姓名),
|
|
|
|
+ mobile(手机号码), email(电子邮箱), updated_at(更新时间)
|
|
|
|
+
|
|
|
|
+ 2. hotel - 酒店节点
|
|
|
|
+ 属性: hotel_zh(酒店中文名称), hotel_en(酒店英文名称), updated_at(更新时间)
|
|
|
|
+
|
|
|
|
+ 3. talent_tag - 人才标签节点
|
|
|
|
+ 属性: name(标签名称), category(标签分类), en_name(英文名称)
|
|
|
|
+
|
|
|
|
+ 4. hotel_tag - 酒店标签节点
|
|
|
|
+ 属性: name(标签名称), category(标签分类), en_name(英文名称)
|
|
|
|
+
|
|
|
|
+ 5. brand_group - 品牌集团节点
|
|
|
|
+ 属性: name(集团名称), en_name(英文名称)
|
|
|
|
+
|
|
|
|
+ ### 关系
|
|
|
|
+ 1. WORKS_FOR - 工作关系,人才在酒店工作
|
|
|
|
+ (talent)-[WORKS_FOR]->(hotel)
|
|
|
|
+ 属性: title_zh(中文职位), title_en(英文职位), updated_at(更新时间)
|
|
|
|
+
|
|
|
|
+ 2. BELONGS_TO - 从属关系
|
|
|
|
+ (talent)-[BELONGS_TO]->(talent_tag) - 人才属于某标签
|
|
|
|
+ (hotel)-[BELONGS_TO]->(hotel_tag) - 酒店属于某标签
|
|
|
|
+ (hotel)-[BELONGS_TO]->(brand_group) - 酒店属于某品牌集团
|
|
|
|
+
|
|
|
|
+ ## 查询需求
|
|
|
|
+ {query_requirement}
|
|
|
|
+
|
|
|
|
+ ## 输出要求
|
|
|
|
+ 1. 只输出有效的Cypher查询语句,不要包含任何解释或注释
|
|
|
|
+ 2. 确保查询结果包含有意义的列名
|
|
|
|
+ 3. 根据需要使用适当的过滤、排序、聚合和限制
|
|
|
|
+ 4. 尽量利用图数据库的特性来优化查询效率
|
|
|
|
+
|
|
|
|
+ 注意:请直接返回Cypher查询语句,无需任何其他文本。
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ # 调用Deepseek API生成Cypher脚本
|
|
|
|
+ headers = {
|
|
|
|
+ "Authorization": f"Bearer {api_key}",
|
|
|
|
+ "Content-Type": "application/json"
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ payload = {
|
|
|
|
+ "model": "deepseek-chat",
|
|
|
|
+ "messages": [
|
|
|
|
+ {"role": "system", "content": "你是一个专业的Neo4j Cypher查询专家。"},
|
|
|
|
+ {"role": "user", "content": prompt}
|
|
|
|
+ ],
|
|
|
|
+ "temperature": 0.1
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ logging.info("发送请求到Deepseek API生成Cypher脚本")
|
|
|
|
+ response = requests.post(api_url, headers=headers, json=payload, timeout=30)
|
|
|
|
+ response.raise_for_status()
|
|
|
|
+
|
|
|
|
+ # 解析API响应
|
|
|
|
+ result = response.json()
|
|
|
|
+ cypher_script = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
|
|
+
|
|
|
|
+ # 清理Cypher脚本,移除不必要的markdown格式或注释
|
|
|
|
+ cypher_script = cypher_script.strip()
|
|
|
|
+ if cypher_script.startswith("```cypher"):
|
|
|
|
+ cypher_script = cypher_script[9:]
|
|
|
|
+ if cypher_script.endswith("```"):
|
|
|
|
+ cypher_script = cypher_script[:-3]
|
|
|
|
+ cypher_script = cypher_script.strip()
|
|
|
|
+
|
|
|
|
+ logging.info(f"生成的Cypher脚本: {cypher_script}")
|
|
|
|
+
|
|
|
|
+ # 执行Cypher脚本
|
|
|
|
+ with neo4j_driver.get_session() as session:
|
|
|
|
+ result = session.run(cypher_script)
|
|
|
|
+ records = [record.data() for record in result]
|
|
|
|
+
|
|
|
|
+ # 构建查询结果
|
|
|
|
+ response_data = {
|
|
|
|
+ 'code': 200,
|
|
|
|
+ 'success': True,
|
|
|
|
+ 'message': '查询成功执行',
|
|
|
|
+ 'query': cypher_script,
|
|
|
|
+ 'data': records
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return response_data
|
|
|
|
+
|
|
|
|
+ except requests.exceptions.HTTPError as e:
|
|
|
|
+ error_msg = f"调用Deepseek API失败: {str(e)}"
|
|
|
|
+ logging.error(error_msg)
|
|
|
|
+ if hasattr(e, 'response') and e.response:
|
|
|
|
+ logging.error(f"错误状态码: {e.response.status_code}")
|
|
|
|
+ logging.error(f"错误内容: {e.response.text}")
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ 'code': 500,
|
|
|
|
+ 'success': False,
|
|
|
|
+ 'message': error_msg,
|
|
|
|
+ 'data': []
|
|
|
|
+ }
|
|
|
|
+ except Exception as e:
|
|
|
|
+ error_msg = f"查询Neo4j图数据库失败: {str(e)}"
|
|
|
|
+ logging.error(error_msg, exc_info=True)
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ 'code': 500,
|
|
|
|
+ 'success': False,
|
|
|
|
+ 'message': error_msg,
|
|
|
|
+ 'data': []
|
|
}
|
|
}
|