优化代码

This commit is contained in:
weiweiw 2025-05-13 17:11:59 +08:00
parent cf768bcc67
commit 722c9cc7d8
1 changed files with 414 additions and 0 deletions

414
api/main_5.py Normal file
View File

@ -0,0 +1,414 @@
import logging
import time
from flask import Flask, jsonify, request
from pydantic import BaseModel, Field
from werkzeug.exceptions import HTTPException
from typing import List
from pydantic import ValidationError
from logger_util import setup_logger
from intentRecognition import IntentRecognition
from slotRecognition import SlotRecognition
from utils import CheckResult, check_standard_name_slot_probability, check_lost
from config import *
from globalData import GlobalData
from apscheduler.schedulers.background import BackgroundScheduler
MODEL_ERNIE_PATH = R"../ernie/output/checkpoint-14672"
MODEL_UIE_PATH = R"../uie/output/checkpoint-16380"
# 类别名称列表
labels = [
"天气查询", "互联网查询", "页面切换", "日计划数量查询", "周计划数量查询",
"日计划作业内容", "周计划作业内容", "施工人数", "作业考勤人数", "知识问答",
"通用对话", "作业面查询", "班组人数查询", "班组数查询", "作业面内容", "班组详情",
"工程进度查询", "人员查询", "分公司查询","工程数量查询","工程详情查询","项目部数量查询",
"建管单位数量查询","建管单位详情","分包单位数量查询","分包单位详情"
]
# 标签映射
label_map = {
0: 'O', # 非实体
1: 'B-date', 20: 'I-date',
2: 'B-projectName', 21: 'I-projectName',
3: 'B-projectType', 22: 'I-projectType',
4: 'B-constructionUnit', 23: 'I-constructionUnit',
5: 'B-implementationOrganization', 24: 'I-implementationOrganization',
6: 'B-projectDepartment', 25: 'I-projectDepartment',
7: 'B-projectManager', 26: 'I-projectManager',
8: 'B-subcontractor', 27: 'I-subcontractor',
9: 'B-teamLeader', 28: 'I-teamLeader',
10: 'B-riskLevel', 29: 'I-riskLevel',
11: 'B-page', 30: 'I-page',
12: 'B-operating', 31: 'I-operating',
13: 'B-teamName', 32: 'I-teamName',
14: 'B-constructionArea', 33: 'I-constructionArea',
15: 'B-personName', 34: 'I-personName',
16: 'B-personQueryType', 35: 'I-personQueryType',
17: 'B-projectStatus', 36: 'I-projectStatus',
18: 'B-skyNet', 37: 'I-skyNet',
19: 'B-programNavigation', 38: 'I-programNavigation'
}
logger = setup_logger("main", level=logging.DEBUG)
# 初始化工具类
intent_recognizer = IntentRecognition(MODEL_ERNIE_PATH, labels)
# 初始化槽位识别工具类
slot_recognizer = SlotRecognition(MODEL_UIE_PATH, label_map)
# 设置Flask应用
app = Flask(__name__)
def job():
logger.info(f"✅ [Info] Executing update_from_redis...at {time.strftime('%Y-%m-%d %H:%M:%S')}")
GlobalData.update_from_redis()
job()
# 创建后台调度器
scheduler = BackgroundScheduler()
scheduler.add_job(job, 'cron', hour=3, minute=0) # 每天凌晨3点执行
scheduler.start()
# 统一的异常处理函数
@app.errorhandler(Exception)
def handle_exception(e):
"""统一异常处理"""
if isinstance(e, HTTPException):
return jsonify({
"error": {
"type": e.name,
"message": e.description,
"status_code": e.code
}
}), e.code
return jsonify({
"error": {
"type": "InternalServerError",
"message": str(e)
}
}), 500
def validate_user(data):
"""验证用户ID"""
if data.get("user_id") != '3bb66776-1722-4c36-b14a-73dd210fe750':
return jsonify(
code=401,
msg='权限验证失败,请联系接口开发人员',
label=-1,
probability=-1
), 401
return None
class LabelMessage(BaseModel):
text: str = Field(..., description="消息内容")
user_id: str = Field(..., description="消息内容")
# 每条消息的结构
class Message(BaseModel):
role: str = Field(..., description="消息内容")
content: str = Field(..., description="消息内容")
# 请求数据的结构
class RequestData(BaseModel):
messages: List[Message] = Field(..., description="消息列表")
user_id: str = Field(..., description="用户ID")
# 意图识别
@app.route('/intent_reco', methods=['POST'])
def intent_reco():
"""意图识别"""
try:
# 获取请求中的 JSON 数据
data = request.get_json()
request_data = LabelMessage(**data) # Pydantic 会验证数据结构
text = request_data.text
user_id = request_data.user_id
# 检查必需字段
if not text:
return jsonify({"error": "text is required"}), 400
if not user_id:
return jsonify({"error": "user_id is required"}), 400
# 验证用户ID
user_validation_error = validate_user(data)
if user_validation_error:
return user_validation_error
# 调用predict方法进行意图识别
predicted_label, predicted_probability, predicted_id = intent_recognizer.predict(text)
return jsonify(
code=200,
msg="成功",
int=predicted_id,
label=predicted_label,
probability=float(predicted_probability)
)
except Exception as e:
logger.error(f"error:{e}")
return jsonify({"error": str(e)}), 500
# 槽位抽取
@app.route('/slot_reco', methods=['POST'])
def slot_reco():
"""槽位识别"""
try:
# 获取请求中的 JSON 数据
data = request.get_json()
request_data = LabelMessage(**data) # Pydantic 会验证数据结构
text = request_data.text
user_id = request_data.user_id
# 检查必需字段
if not text:
return jsonify({"error": "text is required"}), 400
if not user_id:
return jsonify({"error": "user_id is required"}), 400
# 验证用户ID
user_validation_error = validate_user(data)
if user_validation_error:
return user_validation_error
# 调用 recognize 方法进行槽位识别
# entities = slot_recognizer.recognize(text)
entities, slot_probability = slot_recognizer.recognize_probability(text)
logger.info(f"槽位抽取后的实体:{entities},实体后的可能值:{slot_probability}")
return jsonify(
code=200,
msg="成功",
slot=entities)
except Exception as e:
logger.error(f"error:{e}")
return jsonify({"error": str(e)}), 500
@app.route('/agent', methods=['POST'])
def agent():
try:
data = request.get_json()
except Exception as e:
logger.error(f"body不是一个有效的json")
return jsonify({"error": str(e)}), 500 # 捕捉其他错误并返回
try:
# 使用 Pydantic 来验证数据结构
request_data = RequestData(**data) # Pydantic 会验证数据结构
messages = request_data.messages
user_id = request_data.user_id
# 检查必需字段是否存在
if not messages:
return jsonify({"error": "messages is required"}), 400
if not user_id:
return jsonify({"error": "user_id is required"}), 400
# 验证用户ID假设这个函数已经定义
user_validation_error = validate_user(data)
if user_validation_error:
return user_validation_error
if len(messages) == 1: # 首轮
query = messages[0].content # 使用 Message 对象的 .content 属性
# 先进行意图识别
predicted_label, predicted_probability, predicted_id = intent_recognizer.predict(query)
# 再进行槽位抽取
entities, slot_probability = slot_recognizer.recognize_probability(query)
logger.info(
f"第一轮意图识别后的label:{predicted_label}, id:{predicted_id},槽位抽取后的实体:{entities},,slot_probability:{slot_probability},message:{messages}",
)
# 多轮
else:
res = extract_multi_chat(messages)
predicted_label, predicted_probability, predicted_id = intent_recognizer.predict(res)
#0:天气1互联网查询9知识问答10通用对话
if predicted_id in [0, 1, 9, 10]:
logger.info(f"多轮意图识别后的label:{predicted_label}, id:{predicted_id},message:{messages}")
return jsonify({
"code": 200, "msg": "成功",
"answer": {"int": predicted_id, "label": predicted_label, "probability": predicted_probability},
"finalQuery": res
})
# entities = slot_recognizer.recognize(res)
entities, slot_probability = slot_recognizer.recognize_probability(res)
logger.info(
f"多轮意图识别后的label:{predicted_label}, id:{predicted_id},槽位抽取后的实体:{entities},slot_probability:{slot_probability},message:{messages}")
#必须槽位缺失检查
status, sk = check_lost(predicted_id, entities)
if status == CheckResult.NEEDS_MORE_ROUNDS:
return jsonify({"code": 10001, "msg": "成功",
"answer": {"miss": sk},
})
#工程名、分公司名和项目名标准化
result, information = check_standard_name_slot_probability(predicted_id, entities)
if result == CheckResult.NEEDS_MORE_ROUNDS:
return jsonify({
"code": 10001, "msg": "成功",
"answer": {"miss": information},
})
return jsonify({
"code": 200, "msg": "成功",
"answer": {"int": predicted_id, "label": predicted_label, "probability": predicted_probability,
"slot": entities},
})
except ValidationError as e:
return jsonify({"error": e.errors()}), 400 # 捕捉 Pydantic 错误并返回
except Exception as e:
return jsonify({"error": str(e)}), 500 # 捕捉其他错误并返回
def extract_multi_chat(messages):
from openai import OpenAI
client = OpenAI(base_url=api_base_url, api_key=api_key)
latest_message = messages[-1]
latest_user_question = latest_message.content if latest_message.role == "user" else ""
time_prefixes = ["当前","今天", "昨天", "本周", "下周", "明天", "今日","打开"]
history_messages = [] if any(prefix in latest_user_question and len(latest_user_question) > 3 for prefix in time_prefixes) else messages[:-1]
logger.info(f"len(history_messages):{len(history_messages)}")
#最新问题的上一个问题里如果含有时间,则清空最老的历史对话
last_two_messages = history_messages[-2:]
has_time_prefix = any(
msg.role == "user" and any(prefix in msg.content and len(msg.content) > 3 for prefix in time_prefixes)
for msg in last_two_messages
)
last_chat_history = "\n".join([f"{msg.role}: {msg.content}" for msg in last_two_messages])
oldest_chat_history = "" if has_time_prefix else "\n".join([f"{msg.role}: {msg.content}" for msg in history_messages[:2]])
# if len(history_messages) == 6:
# middle_messages = history_messages[2:4]
# if not any(msg.role == "user" and any(prefix in msg.content and len(msg.content) > 3 for prefix in time_prefixes) for msg in middle_messages):
# middle_chat_history = "\n".join([f"{msg.role}: {msg.content}" for msg in middle_messages[:2]])
logger.info(f"last_chat_history:{last_chat_history}")
logger.info(f"oldest_chat_history:{oldest_chat_history}")
prompt = f'''
你是一个意图识别与补全助手你的任务是根据用户的最新问题判断是否需要补全如果不需要补全则原样返回用户的最新问题否则需要结合最新对话历史和最老对话历史补全用户的最新问题并只返回最终的完整问题
请严格按照如下逻辑判断并执行
---
规则判断与补全流程
第一步用户最新问题是否存在指代词 结合用户最新问题和历史用户问题进行补全
- 若用户最新问题中出现模糊表达具体是哪些项是哪两个作业计划分别是什么合肥中心变工程呢具体是哪20项考勤人数呢请根据需要结合最新对话历史和最老对话历史的补全问题信息
- 需要补全的信息包括时间主语查询对象等
- **特别地若最新用户问题中未提及时间但最新对话历史中的用户问题中出现今天昨日本周等时间词则需要将其补全进最终问题中**
- **特别地若最新用户问题中未提及时间但最老对话历史中的用户问题中出现今天昨日本周等时间词则需要将其补全进最终问题中**
- 示例1
- 用户最新问题具体的作业内容是什么
- 最新对话历史的用户问题送一分公司第一项目部今天有多少项作业计划
- 最新对话历史的AI回答送电一分公司第一项目管理部有21项作业计划
- 则最终提问应为
今天送电一分公司第一项目管理部的21项作业计划分别是什么
- 示例2
- 用户最新问题2项作业计划是什么
- 最老对话历史的用户问题宋1分公司第一项目部今天有多少作业计划
- 最老对话历史的AI回答未匹配到您说的分公司名宋1分公司请提供更准确的分公司名
- 最新对话历史的用户问题送一分公司
- 最新对话历史的AI回答送电一分公司第一项目管理部有2项作业计划
- 则最终提问应为
今天送电一分公司第一项目管理部的2项作业计划是什么
- 示例3
- 用户最新问题具体的班组详情
- 最新对话历史的用户问题送一分公司第一项目部今天有多少班组
- 最新对话历史的AI回答送电一分公司第一项目部金上有20个班组
- 则最终提问应为
送电一分公司第一项目部金上今天具体的20个班组详情
第二步用户最新问题是否为序号指代第一个/第2个 用完整工程/变电站/换流站/项目/公司名替换补全
- 精确提取用户所指的序号第3个指第3个工程变电站换流站公司名或项目部名并将完整名称包括括号中的编号提取出来
- 用完整工程变电站换流站公司名或项目部名的名称替换掉最新对话历史的用户问题中出现的简称或模糊表达,但必须保留用户问题中的其余所有内容不得遗漏
- 必须在进行项目部替换时需要从最老历史对话中补充分公司名称
- 禁止添加用户原始问题中未出现的动作性动词例如打开查看查询显示展示
- 最终问题必须贴近原始提问语气和意图只替换模糊对象的全称不改变句式和动作行为表达
- 示例1
- 用户最新问题:"第二个" "第2个"
- 最新对话历史的用户问题"2025年南苑调相机检修(PROJ-2023-0179)今天有多少作业计划""
- 最新对话历史的AI回答你说的工程名可能是第一个检修公司调相机一二次设备检修维护和改造服务框架-2025年南苑调相机检修(PROJ-2023-0179)第二个:黄阳-仙河110kV线路工程(PROJ-2024-0047)请确认您要选择哪一个
- 则最终提问应为
"黄阳-仙河110kV线路工程(PROJ-2024-0047))今天有多少作业计划"
- 示例2
- 用户最新问题:"第2个" "第二个"
- 最新对话历史的用户问题"请帮我查一下今天芦集变电站的进度情况"
- 最新对话历史的AI回答你说的工程名可能是第1个芦集-古沟π入潘集变电站220kV线路工程(PROJ-2024-0189)第二个淮南芦集220千伏变电站220千伏配电装置改造工程(PROJ-2024-0265)请确认您要选择哪一个
- 则最终提问应为
"请帮我查一下今天淮南芦集220千伏变电站220千伏配电装置改造工程(PROJ-2024-0265)的进度情况"
- 示例3
- 用户最新问题:"第2个"
- 最新对话历史的用户问题"宏源电力公司第三项目部今天有多少项作业计划"
- 最新对话历史的AI回答您说的实施组织名可能是,第1个安徽宏源电力建设有限公司(线路)第2个安徽宏源电力建设有限公司(变电),请选择哪一个
- 则最终提问应为
"安徽宏源电力建设有限公司(变电)第三项目部今天有多少项作业计划"
- 示例4
- 用户最新问题:"第2个"
- 最新对话历史的用户问题"打开中心变摄像头"
- 最新对话历史的AI回答您说的工程名可能是,第1个锦绣-常青π入中心变电站220kV架空线路工程(PROJ-2024-1206)第2个合肥中心变B包(PROJ-2024-0176),请选择哪一个
- 则最终提问应为
"打开合肥中心变B包(PROJ-2024-0176)摄像头"
- 示例5
- 最新对话历史的用户问题今天循环园工程有多少项作业计划
- 最新对话历史的AI回答您说的工程名可能是第1个... 第2个... 第3个黄山巷联-水西220kV线路工程(PROJ-2024-0220)请确认您要选择哪一个
- 用户最新问题第3个
- 错误输出不要这样打开黄山巷联-水西220kV线路工程(PROJ-2024-0220)今天有多少项作业计划
- 正确输出你应该输出黄山巷联-水西220kV线路工程(PROJ-2024-0220)今天有多少项作业计划
第三步输出最终问题
- 直接输出最终问题无解释无多余前缀或后缀
- 保持句式自然清晰
---
最老对话历史:
{oldest_chat_history}
最新对话历史
{last_chat_history}
用户最新问题
{latest_user_question}
请输出最终问题'''
message = [
{"role": "user", "content": prompt}
]
# logger.info(f"*********messages:{prompt}")
response = client.chat.completions.create(
messages=message,
model=model_name,
max_tokens=100,
temperature=0.1, # 降低随机性,提高确定性
stream=False
)
res = response.choices[0].message.content.strip()
logger.info(f"多轮意图后用户想要的问题是:{res}")
return res
if __name__ == '__main__':
# 启动时立即执行一次
app.run(host='0.0.0.0', port=18073, debug=False)