Ver Fonte

新增名片信息提取后,更新知识图谱的操作。
新增自然语言查询知识图谱的API接口。

maxiaolong há 1 semana atrás
pai
commit
f3e07e4aad

+ 237 - 0
api-query-kg.md

@@ -0,0 +1,237 @@
+# 知识图谱查询 API 文档
+
+## API 接口: `/query-kg`
+
+该 API 接口允许您使用自然语言查询知识图谱数据库。它利用 Deepseek API 将您的查询需求转换为 Cypher 脚本,并在 Neo4j 数据库上执行。
+
+### 请求详情
+
+- **请求方法**: POST
+- **URL**: `/api/data_parse/query-kg`
+- **Content-Type**: application/json
+
+### 请求参数
+
+| 参数名 | 类型 | 是否必需 | 描述 |
+|-------|------|----------|------|
+| `query_requirement` | 字符串 | 是 | 对要从知识图谱中查询信息的自然语言描述 |
+
+#### 请求体示例
+```json
+{
+  "query_requirement": "查找所有在上海的五星级酒店担任总经理职位的人才"
+}
+```
+
+### 响应格式
+
+API 返回具有以下结构的 JSON 对象:
+
+| 字段 | 类型 | 描述 |
+|-----|------|------|
+| `code` | 整数 | HTTP 状态码(200 表示成功,4xx/5xx 表示错误)|
+| `success` | 布尔值 | 操作是否成功 |
+| `message` | 字符串 | 结果描述或错误信息 |
+| `query` | 字符串 | 执行的 Cypher 查询语句(供参考)|
+| `data` | 数组 | 包含查询结果的对象数组 |
+
+#### 成功响应示例
+```json
+{
+  "code": 200,
+  "success": true,
+  "message": "查询成功执行",
+  "query": "MATCH (t:talent)-[r:WORKS_FOR]->(h:hotel) WHERE r.title_zh CONTAINS '总经理' AND h.hotel_zh CONTAINS '上海' RETURN t.name_zh as 姓名, t.email as 邮箱, r.title_zh as 职位, h.hotel_zh as 酒店名称 ORDER BY h.hotel_zh",
+  "data": [
+    {
+      "姓名": "张三",
+      "邮箱": "zhang@example.com",
+      "职位": "总经理",
+      "酒店名称": "上海四季酒店"
+    },
+    {
+      "姓名": "李四",
+      "邮箱": "li@example.com",
+      "职位": "执行总经理",
+      "酒店名称": "上海浦东丽思卡尔顿酒店"
+    }
+  ]
+}
+```
+
+#### 错误响应示例
+```json
+{
+  "code": 400,
+  "success": false,
+  "message": "请求数据为空或缺少query_requirement字段",
+  "data": []
+}
+```
+
+### 响应状态码
+
+| 状态码 | 描述 |
+|-------|------|
+| 200 | 请求成功 |
+| 400 | 错误请求(缺少或无效参数)|
+| 500 | 服务器错误(处理错误或数据库连接失败)|
+
+### 错误处理
+
+如果发生错误,响应将包括:
+- 非 200 状态码
+- `success` 设置为 `false`
+- 描述性错误信息
+- 空的 `data` 数组
+
+## 知识图谱结构
+
+该 API 与包含以下节点和关系的知识图谱交互:
+
+### 节点
+1. **talent** - 人才节点
+   - 属性: `pg_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)` - 酒店属于某品牌集团
+
+## 前端集成示例
+
+```javascript
+// 使用 Axios 的 Vue.js 示例
+import axios from 'axios';
+
+export default {
+  data() {
+    return {
+      queryInput: '',
+      queryResults: [],
+      isLoading: false,
+      error: null
+    }
+  },
+  methods: {
+    async queryKnowledgeGraph() {
+      this.isLoading = true;
+      this.error = null;
+      
+      try {
+        const response = await axios.post('/api/data_parse/query-kg', {
+          query_requirement: this.queryInput
+        });
+        
+        if (response.data.success) {
+          this.queryResults = response.data.data;
+        } else {
+          this.error = response.data.message;
+        }
+      } catch (error) {
+        this.error = error.response?.data?.message || '查询失败,请稍后重试';
+      } finally {
+        this.isLoading = false;
+      }
+    }
+  }
+}
+```
+
+```html
+<!-- Vue.js 模板示例 -->
+<template>
+  <div class="knowledge-graph-query">
+    <h2>知识图谱查询</h2>
+    
+    <div class="query-input">
+      <textarea 
+        v-model="queryInput"
+        placeholder="请输入查询需求,例如:查找所有在上海的五星级酒店担任总经理职位的人才"
+        rows="3"
+      ></textarea>
+      <button 
+        @click="queryKnowledgeGraph" 
+        :disabled="isLoading || !queryInput"
+      >
+        {{ isLoading ? '查询中...' : '查询' }}
+      </button>
+    </div>
+    
+    <div v-if="error" class="error-message">
+      {{ error }}
+    </div>
+    
+    <div v-if="queryResults.length > 0" class="results-table">
+      <table>
+        <thead>
+          <tr>
+            <th v-for="(value, key) in queryResults[0]" :key="key">{{ key }}</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="(item, index) in queryResults" :key="index">
+            <td v-for="(value, key) in item" :key="`${index}-${key}`">{{ value }}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    
+    <div v-else-if="!isLoading && !error" class="no-results">
+      还没有查询结果,请输入查询需求并点击查询按钮
+    </div>
+  </div>
+</template>
+```
+
+## 查询示例
+
+以下是一些可以使用的查询示例:
+
+1. 查找特定酒店的所有员工:
+   ```
+   查找在上海四季酒店工作的所有人才及其职位
+   ```
+
+2. 查找特定职位的人才:
+   ```
+   查找所有担任总经理或执行总经理职位的人才及其所在酒店
+   ```
+
+3. 按标签查找人才:
+   ```
+   查找拥有"市场营销"标签的所有人才
+   ```
+
+4. 查找特定品牌集团的酒店:
+   ```
+   查找属于希尔顿集团的所有酒店
+   ```
+
+5. 复杂关系查询:
+   ```
+   查找在上海的酒店工作并且同时拥有"领导力"和"酒店管理"标签的人才
+   ```
+
+## 注意事项
+
+- 查询结果格式取决于生成的Cypher脚本,列名将根据查询内容动态生成
+- 复杂查询可能需要更详细的描述以生成准确的Cypher脚本
+- 查询结果默认没有分页,如需处理大量数据,请在查询中指明限制条件 

+ 43 - 1
app/api/data_parse/routes.py

@@ -1,6 +1,6 @@
 from flask import jsonify, request, make_response, Blueprint, current_app, send_file
 from app.api.data_parse import bp
-from app.core.data_parse.parse import parse_data, process_business_card, update_business_card, get_business_cards, update_business_card_status, create_talent_tag, get_talent_tag_list, update_talent_tag, delete_talent_tag
+from app.core.data_parse.parse import parse_data, process_business_card, update_business_card, get_business_cards, update_business_card_status, create_talent_tag, get_talent_tag_list, update_talent_tag, delete_talent_tag, query_neo4j_graph
 from app.config.config import DevelopmentConfig, ProductionConfig
 import logging
 import boto3
@@ -418,3 +418,45 @@ def delete_talent_tag_route(tag_id):
             'data': None
         }), 500
 
+@bp.route('/query-kg', methods=['POST'])
+def query_kg():
+    """
+    查询知识图谱API接口
+    
+    请求参数:
+        - query_requirement: 查询需求描述(JSON格式)
+        
+    返回:
+        - JSON: 包含查询结果和处理状态
+    """
+    try:
+        # 获取请求数据
+        data = request.json
+        
+        if not data or 'query_requirement' not in data:
+            return jsonify({
+                'code': 400,
+                'success': False,
+                'message': '请求数据为空或缺少query_requirement字段',
+                'data': []
+            }), 400
+        
+        query_requirement = data['query_requirement']
+        
+        # 调用业务逻辑函数执行查询
+        result = query_neo4j_graph(query_requirement)
+        
+        # 根据处理结果设置HTTP状态码
+        status_code = 200 if result['success'] else 500
+        
+        return jsonify(result), status_code
+        
+    except Exception as e:
+        logger.error(f"查询知识图谱失败: {str(e)}")
+        return jsonify({
+            'code': 500,
+            'success': False,
+            'message': f"查询知识图谱失败: {str(e)}",
+            'data': []
+        }), 500
+

+ 205 - 6
app/core/data_parse/parse.py

@@ -40,7 +40,7 @@ def parse_data(data: Dict[str, Any]) -> Dict[str, Any]:
 class BusinessCard(db.Model):
     __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_en = 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)
 2. 英文姓名 (name_en)
@@ -520,8 +520,8 @@ def parse_text_with_qwen25VLplus(image_data):
 12. 中文邮政编码 (postal_code_zh)
 13. 英文邮政编码 (postal_code_en)
 14. 品牌组合 (brand_group) - 如有多个品牌,使用逗号分隔
-15. 职业轨迹 (career_path) - 如能从名片中推断,以JSON数组格式返回
-
+15. 职业轨迹 (career_path) - 如能从名片中推断,以JSON数组格式返回,包含当前日期,公司名称和职位
+16. 隶属关系 (affiliation) - 如能从名片中推断,以JSON数组格式返回,包含公司名称和隶属集团名称
 ## 输出格式
 请以严格的JSON格式返回结果,不要添加任何额外解释文字。JSON格式如下:
 ```json
@@ -540,7 +540,8 @@ def parse_text_with_qwen25VLplus(image_data):
   "postal_code_zh": "",
   "postal_code_en": "",
   "brand_group": "",
-  "career_path": []
+  "career_path": [],
+  "affiliation": []
 }
 ```"""
         
@@ -797,6 +798,66 @@ def update_business_card(card_id, data):
         # 保存更新
         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 {
             'code': 200,
             'success': True,
@@ -1233,4 +1294,142 @@ def delete_talent_tag(tag_id):
             'success': False,
             'message': error_msg,
             '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': []
         }

BIN
人才地图-字典20250421.xlsx