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-25910" # MODEL_UIE_PATH = R"../uie/output/checkpoint-32750" MODEL_ERNIE_PATH = R"../ernie/output_temp/checkpoint-34340" MODEL_UIE_PATH = R"../uie/output_temp/checkpoint-34050" # 类别名称列表 labels = [ "天气查询", "互联网查询", "页面切换", "日计划数量查询", "周计划数量查询", "日计划作业内容", "周计划作业内容", "施工人数", "作业考勤人数", "知识问答", "通用对话", "作业面查询", "班组人数查询", "班组数查询", "作业面内容", "班组详情", "工程进度查询", "人员查询", "分公司查询","工程数量查询","工程详情查询","项目部数量查询", "建管单位数量查询","建管单位详情","分包单位数量查询","分包单位详情" ] # 标签映射 label_map = { 0: 'O', # 非实体 1: 'B-date', 18: 'I-date', 2: 'B-projectName', 19: 'I-projectName', 3: 'B-projectType', 20: 'I-projectType', 4: 'B-constructionUnit', 21: 'I-constructionUnit', 5: 'B-implementationOrganization', 22: 'I-implementationOrganization', 6: 'B-projectDepartment', 23: 'I-projectDepartment', 7: 'B-projectManager', 24: 'I-projectManager', 8: 'B-subcontractor', 25: 'I-subcontractor', 9: 'B-teamLeader', 26: 'I-teamLeader', 10: 'B-riskLevel', 27: 'I-riskLevel', 11: 'B-page', 28: 'I-page', 12: 'B-operating', 29: 'I-operating', 13: 'B-teamName', 30: 'I-teamName', 14: 'B-constructionArea', 31: 'I-constructionArea', 15: 'B-personName', 32: 'I-personName', 16: 'B-personQueryType', 33: 'I-personQueryType', 17: 'B-projectStatus', 34: 'I-projectStatus', } 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) # 每天凌晨1点执行 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 prefix != latest_user_question 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 prefix != msg.content 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]]) logger.info(f"last_chat_history:{last_chat_history}") logger.info(f"oldest_chat_history):{oldest_chat_history}") prompt = f''' 你是一个意图识别与补全助手,你的任务是根据用户的最新问题判断是否需要补全,如果不需要补全,则原样返回用户的最新问题,否则需要结合最新对话历史和最老对话历史补全用户的最新问题,并只返回最终的完整问题。请严格按照如下逻辑判断并执行: --- 【规则判断与补全流程】 第一步:用户最新问题是否以“公司”为主语?→ 原样返回,无需补全 - 若用户最新问题主语是“公司”,直接返回原句,无需补全。 - 主语为“公司”的典型句式: - 以“公司”开头; - 以“今天”“昨天”“本周”“下周”等时间词开头,紧跟“公司”作为主语; - 示例: - 用户的最新问题:“今天公司有多少四级风险作业计划?” - 用户的最新问题:“今天公司有多少作业计划” - 用户的最新问题:“公司今天有多少4级风险的作业面?” - 最终提问均为: 原句不变。 第二步:用户最新问题是否是完整的问题?→ 原样返回,无需补全 - 若用户最新问题中包含下列之一:具体的项目部名、工程名、分公司名、班组名、地区名等信息,且同时出现作业计划、作业面、班组等查询对象,视为完整问题,直接返回原句,无需补全。 - 示例: - 用户最新问题:“今天张三班组有多少作业计划?” - 用户最新问题:“今天绿雪莲塘工程有多少作业计划” - 最终提问均为: 原句不变。 第三步:用户最新问题是否存在指代词?→ 结合用户最新问题和最新对话历史进行补全 - 若用户最新问题问题中出现模糊表达,如“具体是哪些项”、“是哪两个”、“作业计划分别是什么”、“合肥中心变工程呢”、“具体是哪20项”等,请只使用紧邻最新问题之前的用户问题和AI回复补全问题信息。 - 示例1: - 用户最新问题:“具体的作业计划分别是什么” - 紧邻最新问题的对话历史的用户问题:“今天公司有多少项作业计划” - 紧邻最新问题的对话历史的AI回答:“2025-04-25公司一共有421项作业计划,分别如下:风险等级为2级的有15项,3级的有144项,4级的有262项,5级的有0项” - 则最终提问应为: “今天公司的421项作业计划分别是什么” - 示例2: - 用户最新问题:“具体的作业内容是什么” - 最新对话历史的用户问题:今天送一分公司第一项目部有多少项作业计划 - 最新对话历史的AI回答:今天送电一分公司第一项目管理部有21项作业计划 - 则最终提问应为: “今天送电一分公司第一项目管理部的21项作业计划分别是什么” 第四步:用户最新问题是否为序号指代(第一个/第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个:安徽宏源电力建设有限公司(变电),请选择哪一个 - 则最终提问应为: "安徽宏源电力建设有限公司(变电)第三项目部今天有多少项作业计划" 第五步:输出最终问题 - 直接输出最终问题(无解释、无多余前缀或后缀) - 保持句式自然清晰 --- 最老对话历史: {oldest_chat_history} 最新对话历史: {last_chat_history} 用户最新问题: {latest_user_question} 请输出最终问题:''' message = [ {"role": "user", "content": 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=18074, debug=False)