From bf987f18aa31fc9b32459d9af0c6012c6a28cc07 Mon Sep 17 00:00:00 2001 From: zhangkunxiang Date: Fri, 24 Jul 2026 09:59:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=BA=E5=88=B6=E5=90=8C=E6=AD=A59092?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E7=BA=BF=E4=B8=8A=E8=BF=90=E8=A1=8C=E7=89=88?= =?UTF-8?q?=E6=9C=AC=EF=BC=8C=E8=A6=86=E7=9B=96=E8=BF=9C=E7=AB=AF=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/app.cpython-310.pyc | Bin 0 -> 38020 bytes .../checkpointer_config.cpython-310.pyc | Bin 0 -> 15891 bytes __pycache__/chunk_text.cpython-310.pyc | Bin 0 -> 36567 bytes __pycache__/config.cpython-310.pyc | Bin 0 -> 6118 bytes __pycache__/doc2pdf.cpython-310.pyc | Bin 0 -> 6540 bytes __pycache__/documents_prompt.cpython-310.pyc | Bin 0 -> 96527 bytes ...ename_proceess_and_kgquery.cpython-310.pyc | Bin 0 -> 13783 bytes __pycache__/fileparse_util.cpython-310.pyc | Bin 0 -> 5770 bytes __pycache__/gw_write.cpython-310.pyc | Bin 0 -> 11524 bytes __pycache__/main_agent.cpython-310.pyc | Bin 0 -> 6126 bytes __pycache__/prompts.cpython-310.pyc | Bin 0 -> 37118 bytes __pycache__/resetlevel.cpython-310.pyc | Bin 0 -> 4297 bytes __pycache__/workflow_registry.cpython-310.pyc | Bin 0 -> 7024 bytes app.py | 95 +- config.py | 17 +- main_agent.py.bak | 249 +++ main_agent.py~ | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 119 -> 119 bytes .../__pycache__/model_api.cpython-310.pyc | Bin 8930 -> 10580 bytes modelsAPI/model_api.py | 62 +- modelsAPI/model_api.py.bak | 368 ++++ prompt.pt_0721 | 1232 ++++++++++++ prompts.py | 26 +- tools/__pycache__/__init__.cpython-310.pyc | Bin 419 -> 705 bytes .../agent_usage_statistics.cpython-310.pyc | Bin 0 -> 7005 bytes .../fault_record_db.cpython-310.pyc | Bin 0 -> 9764 bytes .../fault_statistics.cpython-310.pyc | Bin 0 -> 20855 bytes .../__pycache__/function_tool.cpython-310.pyc | Bin 44589 -> 5446 bytes tools/__pycache__/graph_tools.cpython-310.pyc | Bin 0 -> 32586 bytes tools/__pycache__/rag_tools.cpython-310.pyc | Bin 0 -> 6456 bytes .../__pycache__/ship_model_db.cpython-310.pyc | Bin 0 -> 6373 bytes tools/__pycache__/vlm_tools.cpython-310.pyc | Bin 0 -> 8936 bytes utils/__pycache__/__init__.cpython-310.pyc | Bin 144 -> 144 bytes .../function_tracker.cpython-310.pyc | Bin 9228 -> 9228 bytes utils/__pycache__/image_utils.cpython-310.pyc | Bin 0 -> 5456 bytes .../intent_matcher.cpython-310.pyc | Bin 0 -> 2517 bytes .../word_generator.cpython-310.pyc | Bin 17508 -> 22084 bytes workflows/.workflow_baike.py.swp | Bin 0 -> 16384 bytes workflows/__init__.py | 5 +- .../__pycache__/__init__.cpython-310.pyc | Bin 284 -> 284 bytes .../history_manager.cpython-310.pyc | Bin 0 -> 6474 bytes .../workflow_baike.cpython-310.pyc | Bin 17260 -> 21645 bytes .../workflow_fault_diagnosis.cpython-310.pyc | Bin 46091 -> 45960 bytes .../workflow_utils.cpython-310.pyc | Bin 0 -> 29256 bytes workflows/history_manager.py | 32 +- workflows/history_manager.py.bak | 217 +++ workflows/history_manager.py_0723 | 233 +++ workflows/workflow_baike.py | 872 +++++---- workflows/workflow_baike.py_0721 | 597 ++++++ workflows/workflow_fault_diagnosis.py | 1651 ----------------- workflows/workflow_operate.py | 1457 --------------- workflows/workflow_utils.py | 1557 +++++++++++++++- workflows/workflow_utils.py_0723 | 1332 +++++++++++++ 53 files changed, 6356 insertions(+), 3646 deletions(-) create mode 100644 __pycache__/app.cpython-310.pyc create mode 100644 __pycache__/checkpointer_config.cpython-310.pyc create mode 100644 __pycache__/chunk_text.cpython-310.pyc create mode 100644 __pycache__/config.cpython-310.pyc create mode 100644 __pycache__/doc2pdf.cpython-310.pyc create mode 100644 __pycache__/documents_prompt.cpython-310.pyc create mode 100644 __pycache__/filename_proceess_and_kgquery.cpython-310.pyc create mode 100644 __pycache__/fileparse_util.cpython-310.pyc create mode 100644 __pycache__/gw_write.cpython-310.pyc create mode 100644 __pycache__/main_agent.cpython-310.pyc create mode 100644 __pycache__/prompts.cpython-310.pyc create mode 100644 __pycache__/resetlevel.cpython-310.pyc create mode 100644 __pycache__/workflow_registry.cpython-310.pyc create mode 100644 main_agent.py.bak create mode 100644 main_agent.py~ create mode 100644 modelsAPI/model_api.py.bak create mode 100644 prompt.pt_0721 create mode 100644 tools/__pycache__/agent_usage_statistics.cpython-310.pyc create mode 100644 tools/__pycache__/fault_record_db.cpython-310.pyc create mode 100644 tools/__pycache__/fault_statistics.cpython-310.pyc create mode 100644 tools/__pycache__/graph_tools.cpython-310.pyc create mode 100644 tools/__pycache__/rag_tools.cpython-310.pyc create mode 100644 tools/__pycache__/ship_model_db.cpython-310.pyc create mode 100644 tools/__pycache__/vlm_tools.cpython-310.pyc create mode 100644 utils/__pycache__/image_utils.cpython-310.pyc create mode 100644 utils/__pycache__/intent_matcher.cpython-310.pyc create mode 100644 workflows/.workflow_baike.py.swp create mode 100644 workflows/__pycache__/history_manager.cpython-310.pyc create mode 100644 workflows/__pycache__/workflow_utils.cpython-310.pyc create mode 100644 workflows/history_manager.py.bak create mode 100644 workflows/history_manager.py_0723 create mode 100644 workflows/workflow_baike.py_0721 delete mode 100644 workflows/workflow_fault_diagnosis.py delete mode 100644 workflows/workflow_operate.py create mode 100644 workflows/workflow_utils.py_0723 diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02147f4c2e5e78fdc00e97330650241eac8a7fca GIT binary patch literal 38020 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 literal 0 HcmV?d00001 diff --git a/__pycache__/checkpointer_config.cpython-310.pyc b/__pycache__/checkpointer_config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..086a03f2fa7efa148b5f782f18885368b86342ce GIT binary patch literal 15891 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 R^YBZ~7#ozX*56vw{$C3fi>v?u literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-310.pyc b/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d81aa13b4c310c8562ac271f5e4f0b0a5c6b823 GIT binary patch literal 6118 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 literal 0 HcmV?d00001 diff --git a/__pycache__/filename_proceess_and_kgquery.cpython-310.pyc b/__pycache__/filename_proceess_and_kgquery.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..514a7bafc35aac92388ebe894625dff3e2280fe1 GIT binary patch literal 13783 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 literal 0 HcmV?d00001 diff --git a/__pycache__/fileparse_util.cpython-310.pyc b/__pycache__/fileparse_util.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70c93d242a179bc10390c739e2be864cbe696186 GIT binary patch literal 5770 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 O|C|S$-ZTtvef9sCgp+Il literal 0 HcmV?d00001 diff --git a/__pycache__/gw_write.cpython-310.pyc b/__pycache__/gw_write.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cd09900574f0e381d9c05785e1f15ecd73d9168 GIT binary patch literal 11524 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 F{a^P(8|wf7 literal 0 HcmV?d00001 diff --git a/__pycache__/workflow_registry.cpython-310.pyc b/__pycache__/workflow_registry.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e13b0b42a98e76ec35be62e315a002301d05b11 GIT binary patch literal 7024 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 literal 0 HcmV?d00001 diff --git a/app.py b/app.py index 666c85f..a2c4176 100644 --- a/app.py +++ b/app.py @@ -629,6 +629,76 @@ class FeedbackClassifyRequest(BaseModel): +def _citation_display_filename( + value: Any, + file_id: Any = "", +) -> Any: + """仅转换文本溯源展示名,不修改 file_id 或实际文件路径。""" + if not isinstance(value, str): + return value + if not re.search(r"(?i)\.(?:pptx|ppt)$", value.strip()): + return value + normalized_file_id = str(file_id or "").strip() + if not normalized_file_id: + return value + return f"{normalized_file_id}.pdf" + + +def normalize_source_citation_filenames( + source_citation: Dict[str, Any], +) -> Dict[str, Any]: + """将最终文本溯源中的 PPT/PPTX 展示后缀统一转换为 PDF。""" + if not isinstance(source_citation, dict): + return source_citation + + normalized: Dict[str, Any] = {} + for resource_name, items in source_citation.items(): + group_file_id = "" + if isinstance(items, list): + group_file_id = next( + ( + item.get("file_id") + for item in items + if isinstance(item, dict) and item.get("file_id") + ), + "", + ) + display_resource = _citation_display_filename( + resource_name, + group_file_id, + ) + + if isinstance(items, list): + normalized_items = [] + for item in items: + if not isinstance(item, dict): + normalized_items.append(item) + continue + normalized_item = dict(item) + item_file_id = normalized_item.get("file_id") or group_file_id + for field in ("filename", "resource", "title"): + if field in normalized_item: + normalized_item[field] = _citation_display_filename( + normalized_item[field], + item_file_id, + ) + normalized_items.append(normalized_item) + else: + normalized_items = items + + # 极少数同名 .ppt/.pptx 会映射到同一 .pdf,保留全部切片。 + if ( + display_resource in normalized + and isinstance(normalized[display_resource], list) + and isinstance(normalized_items, list) + ): + normalized[display_resource].extend(normalized_items) + else: + normalized[display_resource] = normalized_items + + return normalized + + def format_stream_event(event_type: str, data: Dict[str, Any], chat_id: str = "", @@ -690,7 +760,9 @@ def format_stream_event(event_type: str, if has_rag or has_graph: source_citation = {} if has_rag: - source_citation.update(rag_result) + source_citation.update( + normalize_source_citation_filenames(rag_result) + ) if has_graph: source_citation["xxxx.graph"] = graph_result @@ -936,6 +1008,7 @@ async def stream_main_agent_execution( agent_task = asyncio.create_task(run_agent()) execution_complete_received = False + has_streamed_content = False rag_result = [] graph_result = [] while True: @@ -953,6 +1026,11 @@ async def stream_main_agent_execution( if event_type == "function_execution": logger.info(f"function_execution event - title: {title}, has_result: {result is not None}, result_type: {type(result)}") + # 装饰器产生的无标题事件属于内部入参/返回值,不应在前端展示。 + # 前端会把它们自动编号成“步骤 2”“步骤 5”,从而泄露检索原文 + # 和未整理的回答草稿。 + if not title.strip(): + continue if title == "知识库搜索工具" and result and isinstance(result, dict): print("===============================", result) rag_result_raw = result.get("sourceCitation", {}) @@ -985,9 +1063,11 @@ async def stream_main_agent_execution( elif event_type == "stream_content": content = event.get("content", "") - yield format_stream_event("stream_content", { - "content": content - }, chat_id=chat_id, message_id=message_id) + if content: + has_streamed_content = True + yield format_stream_event("stream_content", { + "content": content + }, chat_id=chat_id, message_id=message_id) except asyncio.TimeoutError: @@ -1005,8 +1085,13 @@ async def stream_main_agent_execution( }, chat_id=chat_id, message_id=message_id) else: title = await extract_conversation_title(final_result.get("content", "") if final_result else "") + frontend_final_result = final_result + if has_streamed_content and final_result: + # 正文已按增量流式发送,完成事件只补充 actions 等元数据。 + # 再携带完整 content 会被前端追加为第二份重复正文。 + frontend_final_result = {**final_result, "content": ""} yield format_stream_event("execution_complete", { - "final_result": final_result + "final_result": frontend_final_result }, chat_id=chat_id, message_id=message_id) yield format_stream_event("execution_complete", { diff --git a/config.py b/config.py index a838438..e8122b9 100644 --- a/config.py +++ b/config.py @@ -37,7 +37,7 @@ _request_user_config: ContextVar[Optional[Dict[str, str]]] = ContextVar('request # RAG配置的默认值 _RAG_CONFIG_DEFAULT = { - "search_endpoint": "http://192.168.0.46:11000/api/v1/knowledge/search/", + "search_endpoint": "http://192.168.0.46:11100/api/v1/knowledge/search/", "x-user-id": "1", "x-user-name": "testuser", "x-role": "admin", @@ -180,7 +180,7 @@ NEO4J_CONFIG = { # ==================== 知识库匹配配置 ==================== KB_TREE_CONFIG = { - "url": "http://192.168.0.46:11000/api/v1/knowledge/knowledge_base/tree" + "url": "http://192.168.0.46:11100/api/v1/knowledge/knowledge_base/tree" } # ==================== 维修反馈统计配置 ==================== @@ -214,6 +214,17 @@ MAP_INS={ "舷号" : "ship_number", } +# ==================== Rerank 服务配置 ==================== +RERANK_CONFIG = { + "enabled": "true".lower() == "true", + "endpoint": "http://192.168.0.46:59600/v1/rerank", + "model": "bge-rerank", + "timeout": 30.0, + "max_candidates": 24, + "top_k": 10, +} + + # ==================== neo4j配置 ==================== NEO4J_CONFIG = { "uri": "neo4j://192.168.0.46:57687", @@ -242,4 +253,4 @@ SHIP_MODEL_NAME = "/app/ship_xinghao.json" TREE_JSON_PATH = "/app/tree_data.json" # ==================== mineru和kgrag目录映射地址 ==================== SEARCH_DIR ="/app/mineru_output" -IMAGE_DIR = "/app/mineru_output/images" \ No newline at end of file +IMAGE_DIR = "/app/mineru_output/images" diff --git a/main_agent.py.bak b/main_agent.py.bak new file mode 100644 index 0000000..3ba9199 --- /dev/null +++ b/main_agent.py.bak @@ -0,0 +1,249 @@ +""" +主脑Agent - 简化版(无意图分类) + +职责: +1. 输入处理:图片、音频、文本提取 +2. 直接路由:根据外部传入的 route_flag 决定调用哪个工作流 + +工作流列表(来自 workflow_registry): +- 故障排查与修理 +- 舰船百科 +- 操作使用 +""" + +from typing import TypedDict, Literal, Optional, Dict, Any, List +from langgraph.graph import StateGraph, START, END +from tools.function_tool import ( + vlm_image_to_text, + asr_audio_to_text, + detect_input_type +) +from modelsAPI.model_api import OpenaiAPI +from utils.function_tracker import track_function_calls +from workflow_registry import VALID_ROUTE_FLAGS +import json +import re + + +class MainAgentState(TypedDict): + """主脑Agent状态""" + raw_input: Dict[str, Any] + has_image: bool + has_audio: bool + has_query: bool + has_file_text: bool + input_type: str + + history_message: str + extracted_text: str + image_description: str + asr_text: str + file_text: str + combined_query: str + + workflow_result: Optional[Dict[str, Any]] + final_response: str + error_message: str + + route_flag: str + route_params: Dict[str, Any] + + +def detect_input_type_node(state: MainAgentState) -> Dict[str, Any]: + """检测输入类型""" + raw = state["raw_input"] + print(raw) + print(22222222222222222222222222) + result = detect_input_type.invoke({"raw_input": raw}) + + has_image = result.get("has_image", False) + has_audio = result.get("has_audio", False) + has_query = result.get("has_query", False) + + has_file_text = False + file_text_v = raw.get("file_text") + if isinstance(file_text_v, str) and file_text_v.strip(): + has_file_text = True + + input_type = "mixed" + count = sum([has_image, has_audio, has_query, has_file_text]) + if count == 1: + if has_image: + input_type = "image" + elif has_audio: + input_type = "audio" + elif has_query: + input_type = "text" + elif has_file_text: + input_type = "file" + + return { + "has_image": has_image, + "has_audio": has_audio, + "has_query": has_query, + "has_file_text": has_file_text, + "input_type": input_type, + } + + +async def process_multimodal_input(state: MainAgentState) -> Dict[str, Any]: + """处理多模态输入""" + has_image = state.get("has_image", False) + has_audio = state.get("has_audio", False) + has_query = state.get("has_query", False) + has_file_text = state.get("has_file_text", False) + raw = state["raw_input"] + + image_description = "" + asr_text = "" + extracted_text = "" + + try: + if has_image: + image_path = raw.get("image", "") + if image_path: + try: + image_description = await vlm_image_to_text.ainvoke({ + "image_path": image_path + }) + if not image_description or image_description.startswith("VLM 调用失败"): + image_description = "" + except Exception: + image_description = "" + + if has_audio: + audio_path = raw.get("audio", "") + if audio_path: + try: + asr_text = await asr_audio_to_text.ainvoke({ + "audio_path": audio_path + }) + if not asr_text or asr_text.startswith("ASR 调用失败"): + asr_text = "" + except Exception: + asr_text = "" + + text_parts = [] + if raw.get("query"): + text_parts.append(raw["query"]) + if image_description: + text_parts.append(f"[图片描述] {image_description}") + if asr_text: + text_parts.append(f"[语音转文字] {asr_text}") + if raw.get("file_text"): + text_parts.append(f"[文件内容] {raw['file_text']}") + + extracted_text = "\n".join(text_parts) if text_parts else "" + combined_query = raw.get("query", "") or extracted_text + + return { + "extracted_text": extracted_text, + "image_description": image_description, + "asr_text": asr_text, + "combined_query": combined_query, + "file_text": raw.get("file_text", ""), + } + + except Exception as e: + return { + "error_message": f"输入处理失败: {str(e)}", + "extracted_text": "", + } + +# @track_function_calls +def route_to_workflow(state: MainAgentState) -> Dict[str, Any]: + """ + 🤖分析意图,调用相关智能体 + """ + route_flag = state.get("route_flag", "问题排查") + + if route_flag not in VALID_ROUTE_FLAGS: + route_flag = "问题排查" + + query_text = state.get("extracted_text", "") + combined_query = state.get("combined_query", "") or query_text + + raw_input = state.get("raw_input", {}) + history_message = raw_input.get("history_message", "") + + route_params = { + "extracted_text": query_text, + "combined_query": combined_query, + "history_message": history_message, + } + + if raw_input.get("file_text"): + route_params["file_text"] = raw_input["file_text"] + + print(f"[主脑Agent] 路由到: {route_flag}") + print(f"[主脑Agent] 参数: combined_query={combined_query[:100]}...") + + if route_flag == "问题排查": + detail = "正在调用问题排查智能体" + elif route_flag == "百科问答": + detail = "正在调用百科问答智能体" + elif route_flag == "操作使用": + detail = "正在调用操作使用智能体" + elif route_flag == "统计": + detail = "正在调用统计智能体" + else: + detail = "正在调用通用智能体" + + return { + "route_flag": route_flag, + "route_params": route_params, + "__detail__": detail, + } + + +def create_main_agent(): + """创建主脑Agent""" + workflow = StateGraph(MainAgentState) + + workflow.add_node("detect_input_type", detect_input_type_node) + workflow.add_node("process_input", process_multimodal_input) + workflow.add_node("route", route_to_workflow) + + workflow.add_edge(START, "detect_input_type") + workflow.add_edge("detect_input_type", "process_input") + workflow.add_edge("process_input", "route") + workflow.add_edge("route", END) + + return workflow.compile() + + +from workflows.history_manager import filter_image_urls + + +async def extract_conversation_title(content: str) -> str: + """提取对话标题""" + try: + prompt = f"请从以下对话内容中提取一个简短的标题(不超过20字):\n\n{content[:500]}" + response = await OpenaiAPI.open_api_chat_without_thinking( + query=prompt, + model=None, + system_prompt="你是一个标题提取助手,请从对话内容中提取一个简短的标题。", + messages=[] + ) + title = response.strip() if response else "新对话" + if len(title) > 30: + title = title[:30] + "..." + return title + except Exception: + return "新对话" + + +if __name__ == "__main__": + import asyncio + + async def test(): + print("测试主脑Agent") + agent = create_main_agent() + result = await agent.ainvoke({ + "raw_input": {"query": "101舰主机发生异响"}, + "route_flag": "故障排查与修理" + }) + print(f"结果: {result}") + + asyncio.run(test()) + diff --git a/main_agent.py~ b/main_agent.py~ new file mode 100644 index 0000000..e69de29 diff --git a/modelsAPI/__pycache__/__init__.cpython-310.pyc b/modelsAPI/__pycache__/__init__.cpython-310.pyc index 7ce8c18e9b8d8dccc357c3afc05b6adc8d606ce1..b1007b9f19a480f6e63a0c150320d85db8adefa9 100644 GIT binary patch delta 18 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 ZN*&?YBB3f&wR<7InfPfpBC7o5e*p_YnAZRR delta 3253 zcmZuzO>A6A5$=9J^M2;f|MvK296Pp?$;L^X4IwNhO7%8Kvsf777nb;X%7evT=+X74lVWoE0B=5qg^ctRc{i<3e2d# zuCA`CuI{R?et$aix0hxc>9kAWcf0yK|I%JUKET2DpACa!(1Py~^4X? z1My4>W(I+4aZj}>kl#Rt|-Jq8L zAGL8X^OUJa3!_^Jwu4Q?+C5x%CY_zmlrs%&h8O;KCe;9fa0#?pbyyHa~zzUHGKDXAO=h^Q$c`)|FXnFkV&3 zO3Z)$BjqLCFP7_Q<1 zFL}poUvOB8rJ3VXpGFSL%#%nH3oOgJ;&Jyp2@Td0LFU0ycEzkDDi-VWN#y*Pew&_z z#a|(9H*zaBgR`tS6&H@8e?cT+Te3)hIu@)4A+O!nlpz{buB=j{9`bsqj8?#fysGuS zdS4TRAT5RfPAIqLtrf$@3as7?okH8jC=EK$`TMP6H>y8G-lt?@PR1~hvtnQ@**HY<^~9r44nZib z$btapAe45+X^$=0IVJ`&GJ13OhE>XTDBpa`XM{l_F_8{~k z3;-x2glHBtF^-&r2y@8RTcY}yiwxlaoQShTjNo_-A&z8TF81Eoz`zwUodnUBc_=It zm`0dE*oCkgp@6Ul0TV?zFRhh$GxQsE<%Gg3@g=XcqI5`0<(3t3~pSD@GwjaoAl@DLC&i?9zBlEPapKE5lIizCQ%xDX9d ztOd)WlX@t=U%t~fG4W|2gU-Y7aG6duTYCj#&sfXUTu`eLRL&?4R=h_o=c% zzs8|5#a_Z4Ov;D-!*oy%54?O54HXP=96Hs*YfGFlzrI{t@&aCjv+g*Hes1>zg)?xN z#Vv*4hJ0t>?+eePHB%%|egUPD$Ak5H>4mBfg+P1`+3|vAyrB3zGUDTiS2&Hs?c05S zFh{??`R~CrEyVj-LxC3&u;_>r2(KW-xpWe#Q*t(UXbO$M#_8I2bHG#feATNhG4JTl ziF_m1{>;{r1x;!D4k*KiD)Y3GJCAjs1i?G{E*J?kfIf2hi2P3OfNTtRUBIo1MTD&_ zLIG=tkh6j%UdM^2wt5P9D%lY!GB5O$`{XRQuP@c`IQ_}yN5hn+l+fc+bCD}Q2R8Rgvnr!A^5C29alukyN>!eJeu0Z?#63+YP; zE`V}NUY+@H96V5N{JIu#Ln^i9vJW%PmQ+j*vW}1rLff5`f60$(zb5jd{31Oq7sj8V z>zkLy-=zukR~(Xa6SH>gx+x-Ao;X0i0I4&2Se%9Fg0>*uM6oy<&uvlha6b+&0Kh{@ z#20~pRQT1-8JV3NceExPU7U0z^azv9+r}!LISEPrL>I*Sy}UaRD0^hh=dx zKNjK?9+NUKIg}1>WMM#DNA|k>!Q@1b%61NBD_xZQ!{kt3yvVmu>;}S3G$B8noT5tR zcaH2(>6%|Jh8G17E;g!c`#GWHsQm5DQSHxUb9LtmrOWbzsmYB`;e-kG&TlHS+F0cR zsvi_kyo5^kmICn>P_UgJLMMLIk%pS!ff95}%Jkv=7s_nav4~m~uT=5`Q#NEmk@@Aa zN+JbMwO0+-pYllDGQeO~eslV48cTd3A+A6tPRoBy4?*cNXP)giXe>1vRb}7>?PS&g z6g!4+1_AGCeAa>?r6W8^@%XqmfTs-RmqKNRt!9;ryC`)FArANxh(nLzOA(@z;4S&f znIq|0jl%z-rllTeugbYyi`15H?0WBcOlbjvkG?UoKo$Cw List[dict]: + """ + 调用 59600 rerank 服务。 + + 返回格式: + [ + {"index": 0, "relevance_score": 0.98}, + ... + ] + """ + if not RERANK_CONFIG.get("enabled", True) or not query or not documents: + return [] + + max_candidates = int(RERANK_CONFIG.get("max_candidates") or 24) + selected_documents = [str(x or "")[:4000] for x in documents[:max_candidates]] + + try: + async with httpx.AsyncClient( + timeout=float(RERANK_CONFIG.get("timeout") or 30), + verify=False, + trust_env=False, + ) as client: + response = await client.post( + str(RERANK_CONFIG["endpoint"]), + json={ + "model": model or RERANK_CONFIG.get("model") or "bge-rerank", + "query": query, + "documents": selected_documents, + }, + ) + response.raise_for_status() + payload = response.json() + except Exception as exc: + print(f"[rerank] rerank service failed: {exc}") + return [] + + rows: List[dict] = [] + for row in payload.get("results", []): + try: + idx = int(row.get("index")) + except Exception: + continue + if idx < 0 or idx >= len(selected_documents): + continue + rows.append({ + "index": idx, + "relevance_score": float(row.get("relevance_score") or 0.0), + }) + + rows.sort(key=lambda x: float(x.get("relevance_score") or 0), reverse=True) + if top_k is not None: + rows = rows[:top_k] + return rows + @staticmethod def get_embeddings(embedding_text: str): embedding_base_url = EMBEDDING_CONFIG["base_url"] diff --git a/modelsAPI/model_api.py.bak b/modelsAPI/model_api.py.bak new file mode 100644 index 0000000..4473b6f --- /dev/null +++ b/modelsAPI/model_api.py.bak @@ -0,0 +1,368 @@ +import asyncio +import httpx +from typing import List, Union +import os +import numpy as np +from openai import OpenAI +from config import LLM_CONFIG, EMBEDDING_CONFIG + +from openai import AsyncOpenAI + +# 模块级单例客户端,避免每次调用都创建新连接 +_async_client: AsyncOpenAI = None + + +def _get_async_client() -> AsyncOpenAI: + """获取或创建 AsyncOpenAI 单例客户端""" + global _async_client + if _async_client is None: + _async_client = AsyncOpenAI( + api_key=LLM_CONFIG["api_key"], + base_url=LLM_CONFIG["base_url"], + ) + return _async_client + + +class OpenaiAPI: + @staticmethod + async def open_api_chat_without_thinking( + query: str = None, + model: str = None, + json_output: bool = False, + system_prompt: str = None, + messages: list = None, + enable_thinking: bool = True, + temperature: float = 0.1 + ) -> str: + if model is None: + model = LLM_CONFIG["model"] + if system_prompt is None: + system_prompt = "我是一个专业的AI助手,请用中文回答问题。请直接给出最终答案,不要包含任何推理步骤、思考过程、解释性文字或前缀。" + + + # 构建消息 + message_list = [{"role": "system", "content": system_prompt}] + + if messages is not None: + message_list.extend(messages) + if query is not None: + message_list.append({"role": "user", "content": query}) + + client = _get_async_client() + + kwargs = { + "model": model, + "messages": message_list, + "temperature": temperature, + "stream": False, + "max_tokens":LLM_CONFIG["max_tokens"] + } + + kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}} + + if json_output: + kwargs["response_format"] = {"type": "json_object"} + + print("model API. history message", message_list) + response = await client.chat.completions.create(**kwargs) + return response.choices[0].message.content + + @staticmethod + async def open_api_chat_stream( + query: str = None, + model: str = None, + json_output: bool = False, + system_prompt: str = None, + messages: list = None, + enable_thinking: bool = True, + temperature: float = 0.1 + ): + """ + 流式输出大模型响应 + + Yields: + str: 流式内容片段 + """ + if model is None: + model = LLM_CONFIG["model"] + if system_prompt is None: + system_prompt = "我是一个专业的AI助手,请用中文回答问题。请直接给出最终答案,不要包含任何推理步骤、思考过程、解释性文字或前缀。" + + # 构建消息 + message_list = [{"role": "system", "content": system_prompt}] + + if messages is not None: + message_list.extend(messages) + if query is not None: + message_list.append({"role": "user", "content": query}) + + client = _get_async_client() + + kwargs = { + "model": model, + "messages": message_list, + "temperature": temperature, + "stream": True, + "max_tokens":LLM_CONFIG["max_tokens"] + } + + kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}} + + if json_output: + kwargs["response_format"] = {"type": "json_object"} + + print("model API stream. history message", message_list) + + stream = await client.chat.completions.create(**kwargs) + async for chunk in stream: + if chunk.choices and len(chunk.choices) > 0: + delta = chunk.choices[0].delta + if delta.content: + yield delta.content + + @staticmethod + async def open_api_vl_without_thinking( + image_url: str + ) -> str: + """ + 专门用于从图片中提取【设备名称】和【故障现象】。 + 自动过滤思考过程,仅返回核心结果。 + + 参数: + image_url: Base64 格式的图片数据 (data:image/...;base64,...) + model: 模型名称,默认为配置中的模型 + custom_instruction: 额外的特定指令 (可选) + """ + + # 1. 确定模型名称 + model = LLM_CONFIG.get("model") + + # 2. 构建强约束的 System Prompt + # 核心目标:禁止思考标签,禁止废话,只给结果 + system_prompt = ( + "你是一个工业视觉分析专家。你的任务是从图片中识别设备并诊断故障。\n" + "【严格约束】\n" + "1. 绝对禁止输出 , , 等任何思考过程标签。\n" + "2. 绝对禁止输出'好的'、'根据图片'、'分析如下'等开场白或结束语。\n" + "3. 直接输出最终结论,格式必须严格遵守下面的模板。" + ) + + # 3. 构建针对性的 User Prompt + default_task = ( + "请分析这张图片,描述图片内容,重点关注以下信息\n" + "1. 设备识别:图中是什么设备?型号或标签是否可见?\n" + "2. 状态评估:仪表盘数值是否在正常区间?指示灯颜色是什么?\n" + "3. 异常检测:是否存在漏油、生锈、电缆破损、螺丝松动或烟雾?\n" + "4. 环境风险:周围是否有杂物堆放或安全隐患?\n" + "请用专业、客观、简短的语言描述。" + ) + + final_user_prompt = default_task + + # 4. 初始化客户端 + client = _get_async_client() + + # 5. 构建请求参数 + kwargs = { + "model": model, + "messages": [ + {"role": "system", "content": system_prompt}, + { + "role": "user", + "content": [ + {"type": "text", "text": final_user_prompt}, + {"type": "image_url", "image_url": {"url": image_url}} + ] + } + ], + "temperature": 0.1, # 低温度以保证事实准确性 + "stream": False, + "max_tokens": LLM_CONFIG.get("max_tokens"), + } + + # 尝试通过参数关闭思考 (取决于后端支持情况) + kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}} + + try: + print(f"🚀 正在调用 {model} 进行设备故障分析...") + response = await client.chat.completions.create(**kwargs) + raw_content = response.choices[0].message.content or "" + + print(f"✅ 分析完成:\n{raw_content}") + return raw_content + + except Exception as e: + import traceback + error_msg = f"❌ 设备故障分析失败:{str(e)}\n{traceback.format_exc()}" + print(error_msg) + return f"Error: {str(e)}" + + @staticmethod + def get_embeddings(embedding_text: str): + embedding_base_url = EMBEDDING_CONFIG["base_url"] + + with httpx.Client(timeout=60) as client: + response = client.post( + embedding_base_url, + json={"model": EMBEDDING_CONFIG["model"], "input": embedding_text}, + headers={"Authorization": EMBEDDING_CONFIG["api_key"]}, + ) + embedding_text = response.json()["data"][0]["embedding"] + return embedding_text + + @staticmethod + async def get_embeddings_async(embedding_text: str) -> List[float]: + embedding_base_url = EMBEDDING_CONFIG["base_url"] + + async with httpx.AsyncClient(timeout=60) as client: + response = await client.post( + embedding_base_url, + json={"model": EMBEDDING_CONFIG["model"], "input": embedding_text}, + headers={"Authorization": EMBEDDING_CONFIG["api_key"]}, + ) + embedding = response.json()["data"][0]["embedding"] + return embedding + + @staticmethod + async def get_embeddings_batch_async(texts: List[str]) -> List[List[float]]: + embedding_base_url = EMBEDDING_CONFIG["base_url"] + + async with httpx.AsyncClient(timeout=120) as client: + response = await client.post( + embedding_base_url, + json={"model": EMBEDDING_CONFIG["model"], "input": texts}, + headers={"Authorization": EMBEDDING_CONFIG["api_key"]}, + ) + data = response.json()["data"] + sorted_data = sorted(data, key=lambda x: x["index"]) + embeddings = [item["embedding"] for item in sorted_data] + return embeddings + + @staticmethod + def cosine_similarity(vec1: Union[List[float], np.ndarray], vec2: Union[List[float], np.ndarray]) -> float: + v1 = np.array(vec1) if not isinstance(vec1, np.ndarray) else vec1 + v2 = np.array(vec2) if not isinstance(vec2, np.ndarray) else vec2 + + norm1 = np.linalg.norm(v1) + norm2 = np.linalg.norm(v2) + + if norm1 == 0 or norm2 == 0: + return 0.0 + + return float(np.dot(v1, v2) / (norm1 * norm2)) + + @staticmethod + def cosine_similarity_batch(query_vec: Union[List[float], np.ndarray], + candidates_vecs: List[Union[List[float], np.ndarray]]) -> List[float]: + query = np.array(query_vec) if not isinstance(query_vec, np.ndarray) else query_vec + candidates = [np.array(v) if not isinstance(v, np.ndarray) else v for v in candidates_vecs] + + query_norm = np.linalg.norm(query) + if query_norm == 0: + return [0.0] * len(candidates) + + similarities = [] + for candidate in candidates: + cand_norm = np.linalg.norm(candidate) + if cand_norm == 0: + similarities.append(0.0) + else: + sim = float(np.dot(query, candidate) / (query_norm * cand_norm)) + similarities.append(sim) + + return similarities + + @staticmethod + async def hybrid_match_with_embeddings( + query_text: str, + candidates: List[dict], + name_key: str = "display_name", + embedding_key: str = "embedding", + text_weight: float = 0.3, + semantic_weight: float = 0.7, + text_threshold: float = 0.3, + semantic_threshold: float = 0.5 + ) -> tuple: + if not candidates: + return None, 0.0 + + query_embedding = await OpenaiAPI.get_embeddings_async(query_text) + + scored_candidates = [] + + for idx, candidate in enumerate(candidates): + name = candidate.get(name_key, "") + if not name: + props = candidate.get("props", {}) + name = props.get("name") or props.get("名称") or props.get("设备名称") or "" + + text_score = OpenaiAPI._compute_text_similarity(query_text, name) + + props = candidate.get("props", {}) + candidate_embedding = props.get(embedding_key) or candidate.get(embedding_key) + + if candidate_embedding: + semantic_score = OpenaiAPI.cosine_similarity(query_embedding, candidate_embedding) + else: + semantic_score = 0.0 + + combined_score = text_weight * text_score + semantic_weight * semantic_score + + scored_candidates.append({ + "candidate": candidate, + "text_score": text_score, + "semantic_score": semantic_score, + "combined_score": combined_score, + "index": idx + }) + + scored_candidates.sort(key=lambda x: x["combined_score"], reverse=True) + + best = scored_candidates[0] + best_candidate = best["candidate"] + best_score = best["combined_score"] + + min_threshold = max(text_threshold * text_weight + semantic_threshold * semantic_weight, 0.3) + + if best_score < min_threshold: + return None, best_score + + return best_candidate, best_score + + @staticmethod + def _compute_text_similarity(a: str, b: str) -> float: + if not a or not b: + return 0.0 + a = str(a).strip().lower() + b = str(b).strip().lower() + if not a or not b: + return 0.0 + if a == b: + return 1.0 + if a in b or b in a: + return 0.9 + + set_a, set_b = set(a), set(b) + inter = len(set_a & set_b) + union = len(set_a | set_b) or 1 + jaccard = inter / union + + len_diff = abs(len(a) - len(b)) + len_penalty = max(0.0, 1.0 - len_diff / max(len(a), len(b), 1)) + + return 0.5 * jaccard + 0.5 * len_penalty + + + + +if __name__ == "__main__": + answer = asyncio.run(OpenaiAPI.open_api_chat_without_thinking( + query="你好", + json_output=True, + system_prompt="你是一个助手,请用中文回答问题。请直接给出最终答案,不要包含任何推理步骤、思考过程、解释性文字或前缀。", + enable_thinking=True + )) + print(answer) + + + diff --git a/prompt.pt_0721 b/prompt.pt_0721 new file mode 100644 index 0000000..0311e91 --- /dev/null +++ b/prompt.pt_0721 @@ -0,0 +1,1232 @@ +""" +提示词集中管理模块 +将各工作流中的提示词统一提取到此处,方便调整和维护。 +每个提示词都附有详细注释说明其用途和所属工作流节点。 + +模块结构: +- COMMON_PROMPTS: 多个工作流共享的提示词(格式规范、友好回复、无检索建议等) +- BAIKE_PROMPTS: 百科问答工作流专用提示词 +- STATISTICS_PROMPTS: 统计工作流专用提示词 +- FAULT_DIAGNOSIS_PROMPTS: 故障诊断工作流专用提示词(仅含与操作指导不同的部分) +- OPERATE_PROMPTS: 操作指导工作流专用提示词(仅含与故障诊断不同的部分) +""" + + +# ============================================================ +# 一、共享提示词(多个工作流通用) +# ============================================================ + +COMMON_PROMPTS = { + # ------------------------------------------------------------------ + # 格式规范系统提示词 + # 用于: workflow_baike.py (generate_answer)、workflow_utils.py (stream_format_and_postprocess) + # ------------------------------------------------------------------ + "format_system": ( + "你是一个专业的格式规范助手,负责将给定的文本内容规范成美观、易读的格式。" + ), + + # ------------------------------------------------------------------ + # 格式规范用户提示词(带图片示例,用于百科问答和通用格式化) + # 输入变量: raw_content + # 用于: workflow_baike.py (generate_answer)、workflow_utils.py (stream_format_and_postprocess 无content_label时) + # ------------------------------------------------------------------ + "format_user_with_image_examples": ( + "请将以下内容规范成美观、易读的Markdown格式:\n" + "\n" + "【原始内容】\n" + "{raw_content}\n" + "\n" + "【格式规范要求】\n" + "1. 保持原始内容的完整性和准确性,**不要修改任何内容**,只调整格式\n" + "2. 适当使用标题、段落分隔等Markdown格式,使内容更易读\n" + "3. 确保图片链接在相关位置自然展示使用,并且只输出准确格式,`![图片](/api/v1/knowledge/files/images/xxx.jpg)` \n" + "4. 如果参考资料中没有图片,**不要输出任何关于图片的说明**\n" + "5. 确保LaTeX公式格式正确:$$...$$,并在 $$...$$ 前后加空格\n" + "6. 如果有代码片段,使用适当的代码块格式\n" + "7. 不要添加任何额外的解释或说明\n" + "\n" + "【图片输出示例】\n" + "示例一: 原始内容:`![图片](images/xxx.jpg)`\n" + " 规范后输出:`![图片](/api/v1/knowledge/files/images/xxx.jpg)`\n" + "示例二: 原始内容:`![图片](/api/v1/knowledge/files/images/xxx.jpg)`\n" + " 规范后输出:`![图片](/api/v1/knowledge/files/images/xxx.jpg)`\n" + "示例三: 原始内容:`images/xxx.jpg`\n" + " 规范后输出:`![图片](/api/v1/knowledge/files/images/xxx.jpg)`\n" + "示例四: 原始内容:`xxx.jpg`\n" + " 规范后输出:`![图片](/api/v1/knowledge/files/images/xxx.jpg)`\n" + "示例五: 原始内容:`![油污清理](/api/v1/knowledge/files/images/ce9650912c24fa0ed693228469322ec2a735f14d9ac729a1dc87427441bad0b.jpg)`\n" + " 规范后输出:`![图片](/api/v1/knowledge/files/images/ce9650912c24fa0ed693228469322ec2a735f14d9ac729a1dc87427441bad0b.jpg)`\n" + "示例六: 原始内容:`![部分断裂的螺栓及垫片](![图片](/api/v1/knowledge/files/images/e34eb1ddbe26730c84409e02365db416e74ce9e3bf2793a2605db5a085548415.jpg)`\n" + " 规范后输出:`![图片](/api/v1/knowledge/files/images/e34eb1ddbe26730c84409e02365db416e74ce9e3bf2793a2605db5a085548415.jpg)`\n" + "\n" + "请直接输出规范后的内容:" + ), + + # ------------------------------------------------------------------ + # 格式规范用户提示词(简化版,用于方案重新格式化,图片链接保持原样) + # 输入变量: content_label, raw_content + # 用于: workflow_utils.py (stream_format_and_postprocess 有content_label时) + # ------------------------------------------------------------------ + "format_user_simple": ( + "请将以下{content_label}内容规范成美观、易读的Markdown格式:\n" + "\n" + "【原始内容】\n" + "{raw_content}\n" + "\n" + "【格式规范要求】\n" + "1. 保持原始内容的完整性和准确性,**不要修改任何内容**,只调整格式\n" + "2. 适当使用标题、列表、段落分隔等Markdown格式,使内容更易读\n" + "3. 确保图片链接保持原样:`![图片](URL)`\n" + "4. 确保LaTeX公式格式正确:$$...$$,并在 $$...$$ 前后加空格\n" + "5. 如果有代码片段,使用适当的代码块格式\n" + "6. 不要添加任何额外的解释或说明\n" + "\n" + "请直接输出规范后的内容:" + ), + + # ------------------------------------------------------------------ + # 意图分类用户提示词(故障诊断和操作指导共用) + # 输入变量: combined_query + # 用于: workflow_fault_diagnosis.py、workflow_operate.py 的 classify_intent 节点 + # ------------------------------------------------------------------ + "classify_intent_user": ( + "\n" + "### 用户输入\n" + "\"{combined_query}\"\n" + "\n" + "请根据上述标准进行意图分类:" + ), + + # ------------------------------------------------------------------ + # 信息提取用户提示词(故障诊断和操作指导共用) + # 输入变量: existing_info_str, history_str, combined_query + # 用于: workflow_fault_diagnosis.py、workflow_operate.py 的 extract_info 节点 + # ------------------------------------------------------------------ + "extract_info_user": ( + "请从以下内容中提取{biz_label}相关信息:\n" + "\n" + "【已提取的信息】\n" + "{existing_info_str}\n" + "\n" + "【历史对话】\n" + "{history_str}\n" + "\n" + "【当前用户输入】\n" + "{combined_query}\n" + "\n" + "请提取信息(JSON格式):" + ), + + # ------------------------------------------------------------------ + # 询问用户系统提示词(故障诊断和操作指导共用) + # 用于: workflow_fault_diagnosis.py、workflow_operate.py 的 ask_user 节点 + # ------------------------------------------------------------------ + "ask_user_system": ( + "你是一个专业的{biz_label}助手,需要友好地引导用户提供缺失信息。\n" + "\n" + "要求:\n" + "1. 语气友好、专业,不要生硬\n" + "2. 说明为什么需要这些信息(更准确的{biz_label_short})\n" + "3. 鼓励用户提供更多细节" + ), + + # ------------------------------------------------------------------ + # 询问用户提示词(故障诊断和操作指导共用) + # 输入变量: info_completeness, existing_str, missing_str, combined_query + # ------------------------------------------------------------------ + "ask_user_prompt": ( + "请生成一段友好的回复,引导用户补充缺失信息。\n" + "\n" + "【已获取的信息】(完整性评分: {info_completeness})\n" + "{existing_str}\n" + "\n" + "【缺失的信息】\n" + "{missing_str}\n" + "\n" + "【用户当前输入】\n" + "{combined_query}\n" + "\n" + "请生成回复:" + ), + + # ------------------------------------------------------------------ + # 无检索结果且无设备信息时的系统提示词(故障诊断和操作指导共用) + # ------------------------------------------------------------------ + "generate_response_friendly_system": ( + "你是一个友好的{biz_label}助手。用户可能只是在打招呼或询问一般性问题。\n" + "请友好地回复,并简要介绍你能提供的帮助。" + ), + + # ------------------------------------------------------------------ + # 无检索结果且无设备信息时的用户提示词(故障诊断和操作指导共用) + # 输入变量: combined_query + # ------------------------------------------------------------------ + "generate_response_friendly_user": ( + "用户说:{combined_query}\n" + "\n" + "请友好回复:" + ), + + # ------------------------------------------------------------------ + # 无检索结果但有设备信息时的系统提示词(故障诊断和操作指导共用) + # ------------------------------------------------------------------ + "generate_response_no_rag_system": ( + "你是一个专业的{biz_label}助手。\n" + "虽然没有检索到相关资料,但可以根据用户提供的信息给出一般性建议。\n" + "注意:明确告知用户这是基于一般经验的建议。" + ), + + # ------------------------------------------------------------------ + # 无检索结果(简化版)系统提示词(故障诊断和操作指导共用) + # ------------------------------------------------------------------ + "generate_response_no_rag_simple_system": ( + "你是一个专业的{biz_label}助手。\n" + "虽然没有检索到相关资料,但可以根据用户提供的信息给出一般性建议。" + ), + + # ------------------------------------------------------------------ + # 后续交互提示词(故障诊断和操作指导共用) + # 输入变量: biz_label, ship_number, device_name, item_name, detail_info, + # rag_answer, combined_query, image_guidance + # 用于: workflow_fault_diagnosis.py、workflow_operate.py 的 generate_response 节点 + # ------------------------------------------------------------------ + "generate_response_follow_up": ( + "你是一名资深工业设备{biz_label}工程师。用户正在与你进行后续交互。\n" + "\n" + "【设备信息】\n" + "- 舷号:{ship_number}\n" + "- 设备名称:{device_name}\n" + "- {item_label}:{item_name}\n" + "{detail_info}" + "【参考资料】\n" + "{rag_answer}\n" + "\n" + "【用户当前问题】\n" + "{combined_query}\n" + "\n" + "【输出要求】\n" + "1. {image_guidance}\n" + "2. 如果参考资料包含表格,请转化成不用表格的形式输出内容\n" + "3. 如果参考资料包含 LaTeX 公式,请转为 $$...$$ 公式格式输出,并在 $$...$$ 前后加空格\n" + "4. 所有链接必须与参考资料保持一致,不得裁剪、转义或改写\n" + "5. 不能虚构参考资料中未提及的图片、表格、零件号或步骤\n" + "\n" + "请给出回复:" + ), + + # ------------------------------------------------------------------ + # 正常生成方案提示词(故障诊断和操作指导共用) + # 输入变量: biz_label, ship_number, device_name, item_name, item_label, + # detail_info, item_action, rag_answer + # ------------------------------------------------------------------ + "generate_response_normal": ( + "你是一名资深工业设备{biz_label}工程师,请根据以下信息生成{biz_label}分析与{biz_label}方案。\n" + "\n" + "【设备信息】\n" + "- 舷号:{ship_number}\n" + "- 设备名称:{device_name}\n" + "- {item_label}:{item_name}\n" + "{detail_info}" + "当前的{item_label}要以{item_name}为准,{item_action}。\n" + "\n" + "【参考资料】\n" + "{rag_answer}\n" + "\n" + "【输出要求】\n" + "1. **重要**:如果参考资料中包含图片链接(格式为 `![图片](URL)` 或 `images/xxx.jpg`),请务必在相关位置自然展示使用,如果参考资料中没有图片,**不要输出任何关于图片的说明**\n" + "2. 如果参考资料包含 LaTeX 公式、连接等,请在对应位置正常使用,不得修改或删除\n" + "3. 请专注于内容的准确性、完整性和逻辑性,不要担心格式的美观性\n" + "4. 可以适当使用简单的段落分隔,但不要使用复杂的Markdown格式\n" + "5. 如果参考资料包含表格,请不要输出表格,而要直接描述表格的内容\n" + "\n" + "请给出回复:" + ), + + # ------------------------------------------------------------------ + # 基于反馈重新生成方案提示词(故障诊断和操作指导共用) + # 输入变量: biz_label, ship_number, device_name, item_name, item_label, + # detail_info, item_action, user_feedback_for_regenerate, + # rag_answer, image_guidance + # ------------------------------------------------------------------ + "generate_response_regenerating": ( + "你是一名资深工业设备{biz_label}工程师。用户对之前的方案有反馈,请根据反馈重新生成{biz_label}分析与{biz_label}方案。\n" + "\n" + "【设备信息】\n" + "- 舷号:{ship_number}\n" + "- 设备名称:{device_name}\n" + "- {item_label}:{item_name}\n" + "{detail_info}" + "【用户反馈】\n" + "{user_feedback_for_regenerate}\n" + "\n" + "【参考资料】\n" + "{rag_answer}\n" + "\n" + "【输出要求】\n" + "1. 先用一句话判断{item_label}原因(包含舷号、设备、{item_label},不超过40字)\n" + "2. 然后给出{biz_label}方案,步骤清晰、可直接执行\n" + "3. 根据用户的反馈调整方案\n" + "4. 如果参考资料信息不足,明确指出缺少什么\n" + "5. {image_guidance},如果参考资料中没有图片,**不要输出任何关于图片的说明**\n" + "6. 如果参考资料包含表格,请转化成不用表格的形式输出内容\n" + "7. 如果参考资料包含 LaTeX 公式,请转为 $$...$$ 公式格式输出,并在 $$...$$ 前后加空格\n" + "8. 所有链接必须与参考资料保持一致,不得裁剪、转义或改写\n" + "9. 不能虚构参考资料中未提及的图片、表格、零件号或步骤\n" + "\n" + "请直接输出调整后的方案:" + ), + + # ------------------------------------------------------------------ + # 生成内容系统提示词(故障诊断和操作指导共用) + # ------------------------------------------------------------------ + "generate_response_content_system": ( + "你是一名资深工业设备{biz_label}工程师,严格依据检索信息生成{biz_label}方案。请专注于内容的准确性、完整性和逻辑性,如果参考资料中包含图片,请务必在回答中展示出来。" + ), + + # ------------------------------------------------------------------ + # 深度分析点选择系统提示词(故障诊断和操作指导共用) + # ------------------------------------------------------------------ + "deep_rag_analysis_system": ( + "你是一名资深工业设备{biz_label}专家。用户反馈之前的方案没有解决问题,请分析:\n" + "1. 之前方案可能存在的不足\n" + "2. 用户当前问题的关键点\n" + "3. 选择3-5个可以深入检索的分析点(如特定部件、原理、{biz_label}方法等)\n" + "\n" + "输出格式(JSON):\n" + "{\n" + " \"analysis_points\": [\"分析点1\", \"分析点2\", \"分析点3\"],\n" + " \"reason\": \"分析理由\"\n" + "}" + ), + + # ------------------------------------------------------------------ + # 深度分析点选择用户提示词(故障诊断和操作指导共用) + # 输入变量: ship_number, device_name, item_name, item_label, detail_info, + # history_str, last_generated_scheme, combined_query + # ------------------------------------------------------------------ + "deep_rag_analysis_user": ( + "【设备信息】\n" + "- 舷号:{ship_number}\n" + "- 设备名称:{device_name}\n" + "- {item_label}:{item_name}\n" + "{detail_info}" + "\n" + "【对话历史】\n" + "{history_str}\n" + "\n" + "【之前的方案】\n" + "{last_generated_scheme}\n" + "\n" + "【用户当前反馈】\n" + "{combined_query}\n" + "\n" + "请分析并选择深度分析点(JSON格式):" + ), + + # ------------------------------------------------------------------ + # 深度响应生成提示词(故障诊断和操作指导共用) + # 输入变量: biz_label, ship_number, device_name, item_name, item_label, + # detail_info, combined_query, analysis_points_prompt, + # analysis_points_text, original_rag_text, graph_results + # ------------------------------------------------------------------ + "generate_deep_response": ( + "你是一名资深工业设备{biz_label}专家。用户反馈之前的方案没有解决问题,请根据多维度深度检索的资料,进行深入的{item_label}原因分析和解决方案规划。\n" + "\n" + "【设备信息】\n" + "- 舷号:{ship_number}\n" + "- 设备名称:{device_name}\n" + "- {item_label}:{item_name}\n" + "{detail_info}" + "【用户反馈】\n" + "{combined_query}\n" + "{analysis_points_prompt}" + "【针对分析点的深度检索资料】\n" + "{analysis_points_text}\n" + "{original_rag_text}\n" + "\n" + "【图谱检索结果】\n" + "{graph_results}\n" + "\n" + "【输出要求】\n" + "1. 首先重新深入分析{item_label}原因,结合所有检索资料进行推理\n" + "2. 给出更详细、更精准的{biz_label}方案,步骤清晰、可直接执行\n" + "3. 重点关注用户反馈的问题,根据用户反馈和检索资料,进行{item_label}原因分析和解决方案规划。\n" + "4. **重要**:如果参考资料中包含图片链接(格式为 `![图片](URL)` 或 `images/xxx.jpg`),请务必在相关位置自然展示使用,如果参考资料中没有图片,**不要输出任何关于图片的说明**\n" + "5. 如果参考资料包含 LaTeX 公式、连接等,请在对应位置正常使用,不得修改或删除\n" + "6. 请专注于内容的准确性、完整性和逻辑性,不要担心格式的美观性\n" + "7. 可以适当使用简单的段落分隔,但不要使用复杂的Markdown格式\n" + "8. 如果参考资料包含表格,请不要输出表格,而要直接描述表格的内容\n" + "\n" + "请直接输出深度分析和方案:" + ), + + # ------------------------------------------------------------------ + # 深度响应内容系统提示词(故障诊断和操作指导共用) + # ------------------------------------------------------------------ + "generate_deep_response_content_system": ( + "你是一名资深工业设备{biz_label}专家,严格依据检索信息进行深度分析和方案生成。请专注于内容的准确性、完整性和逻辑性,如果参考资料中包含图片,请务必在回答中展示出来。" + ), + + # ------------------------------------------------------------------ + # 基于反馈修改方案提示词(故障诊断和操作指导共用) + # 输入变量: biz_label, ship_number, device_name, item_name, item_label, + # detail_info, item_action, last_scheme, user_feedback, image_guidance + # ------------------------------------------------------------------ + "regenerate_scheme_from_feedback": ( + "你是一名资深工业设备{biz_label}专家。用户对之前的{biz_label}方案有反馈,请根据反馈直接修改方案。\n" + "\n" + "【设备信息】\n" + "- 舷号:{ship_number}\n" + "- 设备名称:{device_name}\n" + "- {item_label}:{item_name}\n" + "{detail_info}" + "当前的{item_label}要以{item_name}为准,{item_action}。\n" + "\n" + "【之前的方案】\n" + "{last_scheme}\n" + "\n" + "【用户反馈】\n" + "{user_feedback}\n" + "\n" + "【输出要求】\n" + "1. 首先根据用户反馈,分析之前方案的问题所在\n" + "2. 然后基于之前的方案进行修改和优化,给出调整后的方案\n" + "3. 重点解决用户反馈的问题\n" + "4. {image_guidance}\n" + "5. 如果之前方案包含表格,请转化成不用表格的形式输出内容\n" + "6. 如果之前方案包含 LaTeX 公式,请转为 $$...$$ 公式格式输出,并在 $$...$$ 前后加空格\n" + "7. 所有链接必须与原方案保持一致,不得裁剪、转义或改写\n" + "8. 不能虚构原方案中未提及的图片、表格、零件号或步骤\n" + "\n" + "请直接输出修改后的完整方案:" + ), + + # ------------------------------------------------------------------ + # 基于反馈修改方案系统提示词(故障诊断和操作指导共用) + # ------------------------------------------------------------------ + "regenerate_scheme_from_feedback_system": ( + "你是一名资深工业设备{biz_label}专家,根据用户反馈直接修改和优化{biz_label}方案。请专注于内容的准确性、完整性和逻辑性。" + ), +} + + +# ============================================================ +# 二、百科问答工作流 (workflow_baike.py) 专用提示词 +# ============================================================ + +BAIKE_PROMPTS = { + # ------------------------------------------------------------------ + # judge_need_retrieval 节点:判断用户问题是否需要检索知识库 + # 输入变量: history_messages, original_query + # ------------------------------------------------------------------ + "judge_need_retrieval": ( + "You are an intent classifier for a knowledge-base QA assistant. Decide whether the user question needs retrieval.\n" + "\n" + "Return true when the question asks about documents, files, knowledge-base content, equipment, systems, faults, operations, standards, procedures, meeting notes, database-related materials, or any factual/domain-specific information.\n" + "Return false only for pure greetings, thanks, cancellation/confirmation with no substantive question, or a follow-up that can be fully answered from the conversation history alone.\n" + "Default rule: when unsure, return true.\n" + "\n" + "Conversation history:\n" + "{history_messages}\n" + "\n" + "User question:\n" + "{original_query}\n" + "\n" + "Output only true or false." + ), + + # ------------------------------------------------------------------ + # rewrite_query_with_history 节点:使用历史上下文重写检索query + # 输入变量: last_user_query, history_str, original_query + # ------------------------------------------------------------------ + "rewrite_query_with_history": ( + "你是一个专业的检索query重写助手,负责根据对话历史和当前用户问题,生成**适合向量检索/RAG 的单条查询语句**。\n" + "\n" + "请严格遵守以下要求:\n" + "1. 只融合与当前问题语义强相关的**最近几轮对话信息**,不要机械拼接全部历史。\n" + "2. 消除指代和省略(例如:这个、刚才那个问题、上面说的故障等),补全成自洽、完整的描述。\n" + "3. 不要加入与用户问题无关的背景信息,不要过度扩展检索范围。\n" + "4. 输出必须是一句自然的中文问句或陈述句,便于检索;不要包含条目符号、编号或解释性文字。\n" + "5. 只输出改写后的query本身:\n" + " - 不要输出说明文字\n" + " - 不要包含「改写结果:」「检索query:」等前缀\n" + " - 不要添加引号\n" + "\n" + "【特别提示】\n" + "- 请优先参考**上一轮用户的问题**来重写本轮query。\n" + "- 上一轮用户的问题是:{last_user_query}\n" + "\n" + "【对话历史(JSON 格式,已按时间排序)】\n" + "{history_str}\n" + "\n" + "【当前用户问题】\n" + "{original_query}\n" + "\n" + "请给出最终用于检索的单条中文query。" + ), + + # ------------------------------------------------------------------ + # generate_suggested_replies_baike 函数:生成百科问答的建议回复问题 + # 输入变量: question, answer + # ------------------------------------------------------------------ + "generate_suggested_replies": ( + "你是一个专业的公文问答助手。根据以下问答内容,生成两个用户可能关心的后续问题。\n" + "\n" + "【用户问题】\n" + "{question}\n" + "\n" + "【回答内容】\n" + "{answer}\n" + "\n" + "请生成两个简洁、实用的问题,这些问题应该:\n" + "1. 不得出现需要归档和下载等操作相关的问题\n" + "2. 与当前问答内容相关,能够帮助用户进一步了解相关信息\n" + "3. 问题要具体、可操作\n" + "4. 每个问题不超过20个字\n" + "\n" + "请以JSON格式输出,格式如下:\n" + "[\n" + " {{\"title\": \"问题1的标题\", \"content\": \"问题1的完整内容\"}},\n" + " {{\"title\": \"问题2的标题\", \"content\": \"问题2的完整内容\"}}\n" + "]\n" + "\n" + "只输出JSON,不要有其他文字说明。" + ), + + # ------------------------------------------------------------------ + # generate_answer 节点 - 有检索结果时的系统提示词 + # 输入变量: domain_desc + # ------------------------------------------------------------------ + "generate_answer_with_retrieval_system": ( + + "我是一个专业的{domain_desc}知识问答助手,根据用户的问题,综合所有检索结果,提供准确、专业、完整的回答内容。" + ), + + # ------------------------------------------------------------------ + # generate_answer 节点 - 有检索结果时的用户提示词 + # 输入变量: extracted_text, rag_count, all_source_text + # ------------------------------------------------------------------ + "generate_answer_with_retrieval_user": ( + "{extracted_text}\n" + "\n" + "# 检索到的相关信息(共 {rag_count} 条):\n" + "\n" + "{all_source_text}\n" + "\n" + "【输出要求】\n" + "1. **重要**:如果参考资料中包含图片链接(格式为 `![图片](URL)` 或 `images/xxx.jpg`),请务必在相关位置自然展示使用,如果参考资料中没有图片,**不要输出任何关于图片的说明**\n" + "2. 如果参考资料包含 LaTeX 公式、连接等,请在对应位置正常使用,不得修改或删除\n" + "3. 请专注于内容的准确性、完整性和逻辑性,不要担心格式的美观性\n" + "4. 可以适当使用简单的段落分隔,但不要使用复杂的Markdown格式\n" + "5. 如果参考资料包含表格,请不要输出表格,而要直接描述表格的内容\n" + "\n" + "请给出回复:" + ), + + # ------------------------------------------------------------------ + # generate_answer 节点 - 无检索结果时的系统提示词 + # 输入变量: domain_desc + # ------------------------------------------------------------------ + "generate_answer_without_retrieval_system": ( + "我是一个专业的{domain_desc}知识问答助手,根据用户的问题,结合我的专业知识,提供准确、专业、完整的回答内容。" + ), + + # ------------------------------------------------------------------ + # generate_answer 节点 - 无检索结果时的用户提示词 + # 输入变量: extracted_text + # ------------------------------------------------------------------ + "generate_answer_without_retrieval_user": ( + "{extracted_text}\n" + "\n" + "【输出要求】\n" + "1. 请根据您的专业知识提供准确、完整的回答\n" + "2. 可以适当使用简单的段落分隔,但不要使用复杂的Markdown格式\n" + "3. 请专注于内容的准确性和专业性\n" + "\n" + "请给出回复:" + ), +} + + +# ============================================================ +# 三、统计工作流 (workflow_statistics.py) 专用提示词 +# ============================================================ + +STATISTICS_PROMPTS = { + # ------------------------------------------------------------------ + # _extract_info_with_llm 函数 - 故障频次统计信息提取 + # 输入变量: text + # ------------------------------------------------------------------ + "extract_fault_frequency_info": ( + "从以下文本中提取统计所需的信息,返回JSON格式。\n" + "提取内容:\n" + "- system: 系统名称(如:动力系统、电气系统等),如果没有则为空字符串\n" + "- time: 时间描述(如:近一个月、上周等),如果没有则为空字符串\n" + "\n" + "文本:\n" + "{text}\n" + "\n" + "输出格式示例:\n" + "{{\"system\": \"动力系统\", \"time\": \"近一个月\"}}\n" + "{{\"system\": \"\", \"time\": \"上周\"}}\n" + "{{\"system\": \"电气系统\", \"time\": \"\"}}\n" + "\n" + "现在请提取:" + ), + + # ------------------------------------------------------------------ + # _extract_info_with_llm 函数 - 备件消耗统计信息提取 + # 输入变量: text + # ------------------------------------------------------------------ + "extract_spare_parts_info": ( + "从以下文本中提取统计所需的信息,返回JSON格式。\n" + "提取内容:\n" + "- ship_number: 舷号数字(如:101、163等),如果没有则为空字符串\n" + "- time: 时间描述(如:近一个月、上周等),如果没有则为空字符串\n" + "\n" + "文本:\n" + "{text}\n" + "\n" + "输出格式示例:\n" + "{{\"ship_number\": \"101\", \"time\": \"近一个月\"}}\n" + "{{\"ship_number\": \"\", \"time\": \"上周\"}}\n" + "{{\"ship_number\": \"163\", \"time\": \"\"}}\n" + "\n" + "现在请提取:" + ), + + # ------------------------------------------------------------------ + # _extract_info_with_llm 函数 - 维修反馈统计信息提取 + # 输入变量: text + # ------------------------------------------------------------------ + "extract_repair_feedback_info": ( + "从以下文本中提取统计所需的信息,返回JSON格式。\n" + "提取内容:\n" + "- source: 反馈来源(如:好评、差评、反馈),如果没有则为空字符串\n" + "\n" + "文本:\n" + "{text}\n" + "\n" + "输出格式示例:\n" + "{{\"source\": \"差评\"}}\n" + "{{\"source\": \"好评\"}}\n" + "{{\"source\": \"\"}}\n" + "\n" + "现在请提取:" + ), + + # ------------------------------------------------------------------ + # extract_tool_parameters 节点:统计工具参数提取 + # 输入变量: current_date, tool_name, tool_description, params_desc_str, + # history_str, extracted_text, extra_instructions + # ------------------------------------------------------------------ + "extract_tool_parameters": ( + "你是一个参数提取助手。请从用户问题中提取统计工具所需的参数。\n" + "\n" + "当前日期:{current_date}\n" + "\n" + "工具名称:{tool_name}\n" + "工具描述:{tool_description}\n" + "\n" + "所需参数:\n" + "{params_desc_str}\n" + "\n" + "对话历史:\n" + "{history_str}\n" + "\n" + "用户问题:\n" + "{extracted_text}\n" + "\n" + "请特别注意:\n" + "1. 时间参数映射:\n" + " - \"最近一周\" -> 开始日期为当前日期前7天,结束日期为当前日期\n" + " - \"最近一个月\" -> 开始日期为当前日期前30天,结束日期为当前日期\n" + " - \"最近三个月\" -> 开始日期为当前日期前90天,结束日期为当前日期\n" + " - \"上个月\" -> 开始日期为上个月1号,结束日期为上个月最后一天\n" + " - \"今年\" -> 开始日期为今年1月1日,结束日期为当前日期\n" + " - 如果用户提到具体日期,请按原样提取\n" + "{extra_instructions}\n" + "3. top_n参数:\n" + " - 如果用户提到\"前N个\"、\"Top N\"等,提取N的值\n" + " - 否则使用默认值\n" + "\n" + "请输出JSON格式:\n" + "{{\n" + " \"parameters\": {{\n" + " \"参数名\": \"参数值\"\n" + " }},\n" + " \"missing_required\": [\"缺少的必填参数列表\"],\n" + " \"confidence\": 0.0-1.0之间的置信度\n" + "}}\n" + "\n" + "如果某个参数在用户问题中没有提到且不是必填,则不要包含在parameters中。\n" + "只输出JSON,不要有其他文字。" + ), +} + + +# ============================================================ +# 三-B、受控 Text2SQL 工作流 (workflow_statistics.py) 专用提示词 +# ============================================================ + +TEXT2SQL_PROMPTS = { + # ------------------------------------------------------------------ + # router_node: 路由判断 - 判断用户问题是统计分析还是普通问答 + # 输入变量: question + # ------------------------------------------------------------------ + "router": ( + "你是一个路由判断专家。请判断用户的问题是否需要查询数据库进行统计分析。\n" + "\n" + "判断标准:\n" + "- 如果问题涉及统计、排名、频次、数量、趋势、对比、分布等,需要查询数据库 → 回答 \"statistics\"\n" + "- 如果问题涉及维修反馈、好评、差评等,需要查询外部系统 → 回答 \"feedback\"\n" + "- 如果是一般性知识问答、维修方法、操作指导等,不需要查询数据库 → 回答 \"normal\"\n" + "\n" + "用户问题:{question}\n" + "\n" + "请输出JSON格式:\n" + "{{\"route\": \"statistics\" 或 \"feedback\" 或 \"normal\", \"reason\": \"判断理由\"}}\n" + "只输出JSON,不要有其他文字。" + ), + + # ------------------------------------------------------------------ + # SQL Generation: 根据用户问题生成SQL + # 输入变量: schema, question, current_date + # ------------------------------------------------------------------ + "sql_generation": ( + "你是舰船维修数据分析助手。请根据用户问题生成SQL查询。\n" + "\n" + "【数据库Schema】\n" + "{schema}\n" + "\n" + "【当前日期】{current_date}\n" + "\n" + "【用户问题】\n" + "{question}\n" + "\n" + "【其他有效信息】\n" + "{extra_info}\n" + "【生成规则】\n" + "1. 只能生成 SELECT 语句,禁止 INSERT/UPDATE/DELETE\n" + "2. 只能查询 fault_records 表\n" + "3. 默认 LIMIT 100(如果未指定)\n" + "4. 不允许使用 UNION、子查询嵌套超过2层\n" + "5. 不允许使用任何危险SQL(如 pg_sleep、注释注入等)\n" + "6. 时间条件使用 created_at 字段\n" + "7. 备件统计需要使用 jsonb_array_elements_text(spare_parts) 展开JSONB数组\n" + "8. 系统名称使用 system_name 字段,必须使用 LIKE 模糊匹配(如 system_name LIKE '%动力%'),不要用等号精确匹配\n" + "9. 舷号使用 ship_number 字段\n" + "\n" + "【常见统计模式】\n" + "- 故障频次排名:SELECT device_name || fault AS name, system_name, COUNT(*) AS count FROM fault_records [WHERE ...] GROUP BY name, system_name ORDER BY count DESC LIMIT N\n" + "- 备件消耗排名:SELECT sp AS name, system_name, COUNT(*) AS count FROM fault_records, jsonb_array_elements_text(spare_parts) AS sp [WHERE ...] GROUP BY sp, system_name ORDER BY count DESC LIMIT N\n" + "- 故障类型分布:SELECT system_name, COUNT(*) AS count FROM fault_records [WHERE ...] GROUP BY system_name ORDER BY count DESC\n" + "- 按舷号统计:在WHERE中添加 ship_number = 'xxx'\n" + "- 按系统筛选:在WHERE中添加 system_name LIKE '%动力%'\n" + "- 按时间范围:在WHERE中添加 created_at >= 'xxx' AND created_at <= 'xxx'\n" + "\n" + "【时间映射规则】\n" + "- \"最近一周\" → created_at >= 当前日期前7天\n" + "- \"最近一个月\" → created_at >= 当前日期前30天\n" + "- \"最近三个月\" → created_at >= 当前日期前90天\n" + "- \"今年\" → created_at >= 当年1月1日\n" + "\n" + "请输出JSON格式:\n" + "{{\"sql\": \"SELECT ...\", \"explanation\": \"SQL逻辑说明\"}}\n" + "只输出JSON,不要有其他文字。" + ), + + # ------------------------------------------------------------------ + # SQL Regeneration: 基于反馈重新生成SQL + # 输入变量: schema, question, current_date, previous_sql, feedback + # ------------------------------------------------------------------ + "sql_regeneration": ( + "你是舰船维修数据分析助手。之前的SQL存在问题,请根据反馈重新生成。\n" + "\n" + "【数据库Schema】\n" + "{schema}\n" + "\n" + "【当前日期】{current_date}\n" + "\n" + "【用户问题】\n" + "{question}\n" + "\n" + "【之前的SQL】\n" + "{previous_sql}\n" + "\n" + "【问题反馈】\n" + "{feedback}\n" + "\n" + "请根据反馈修正SQL,输出JSON格式:\n" + "{{\"sql\": \"SELECT ...\", \"explanation\": \"修正说明\"}}\n" + "只输出JSON,不要有其他文字。" + ), + + # ------------------------------------------------------------------ + # SQL Review: 审查SQL是否正确回答了用户问题 + # 输入变量: schema, question, sql + # ------------------------------------------------------------------ + "sql_review": ( + "你是SQL审查专家。请检查以下SQL是否真正回答了用户问题。\n" + "\n" + "【数据库Schema】\n" + "{schema}\n" + "\n" + "【用户问题】\n" + "{question}\n" + "\n" + "【待审查SQL】\n" + "{sql}\n" + "\n" + "【审查要点】\n" + "1. 时间条件是否正确(用户说\"近一个月\",SQL是否有对应的时间范围?)\n" + "2. 聚合逻辑是否正确(COUNT、GROUP BY是否与问题匹配?)\n" + "3. 排序是否正确(排名类问题是否ORDER BY DESC?)\n" + "4. 字段是否存在(所有字段都在Schema中?)\n" + "5. 是否符合业务语义(故障频次应统计device_name+fault,备件应展开spare_parts)\n" + "6. WHERE条件是否遗漏(用户指定了舷号/系统,SQL是否包含?)\n" + "7. 备件统计是否正确使用了 jsonb_array_elements_text\n" + "\n" + "请输出JSON格式:\n" + "{{\n" + " \"approved\": true 或 false,\n" + " \"issues\": [\"问题1\", \"问题2\"],\n" + " \"suggested_sql\": \"修正后的SQL(如果approved为false,提供修正版本)\",\n" + " \"review_comment\": \"审查意见\"\n" + "}}\n" + "只输出JSON,不要有其他文字。" + ), + + # ------------------------------------------------------------------ + # Result Reflection: 检查查询结果是否合理 + # 输入变量: question, sql, row_count, sample_rows + # ------------------------------------------------------------------ + "result_reflection": ( + "你是数据分析审查专家。请检查SQL查询结果是否合理地回答了用户问题。\n" + "\n" + "【用户问题】\n" + "{question}\n" + "\n" + "【执行的SQL】\n" + "{sql}\n" + "\n" + "【结果概况】\n" + "- 返回行数:{row_count}\n" + "- 前5行数据:\n" + "{sample_rows}\n" + "\n" + "【判断标准】\n" + "1. 如果结果为空(0行),可能SQL有问题 → 需要重试\n" + "2. 如果结果数量异常少(如统计排名只有1条),可能条件过严 → 需要重试\n" + "3. 如果结果明显不符合业务逻辑 → 需要重试\n" + "4. 如果结果合理 → 通过\n" + "\n" + "请输出JSON格式:\n" + "{{\n" + " \"reasonable\": true 或 false,\n" + " \"reason\": \"判断理由\",\n" + " \"suggestion\": \"如果不合理,给出改进建议\"\n" + "}}\n" + "只输出JSON,不要有其他文字。" + ), + + # ------------------------------------------------------------------ + # Final Answer: 将SQL结果转为自然语言 + # 输入变量: question, sql, row_count, result_data + # ------------------------------------------------------------------ + "final_answer": ( + "你是舰船维修数据分析助手。请将SQL查询结果转换为清晰的自然语言回答。\n" + "\n" + "【用户问题】\n" + "{question}\n" + "\n" + "【执行的SQL】\n" + "{sql}\n" + "\n" + "【查询结果】(共{row_count}条)\n" + "{result_data}\n" + "\n" + "【回答要求】\n" + "1. 用自然语言总结核心结论\n" + "2. 如果是排名类结果,用Markdown表格展示(列名:排名、名称、次数/数量、系统等)\n" + "3. 如果是分布类结果,说明各部分的占比\n" + "4. 数字要准确,不要编造数据\n" + "5. 语言简洁专业\n" + "6. 如果结果为空,说明可能没有相关数据\n" + ), +} + + +# ============================================================ +# 四、故障诊断工作流 (workflow_fault_diagnosis.py) 专用提示词 +# 仅包含与操作指导不同的部分,共享提示词使用 COMMON_PROMPTS +# ============================================================ + +FAULT_DIAGNOSIS_PROMPTS = { + # ------------------------------------------------------------------ + # classify_intent 节点:故障诊断意图分类系统提示词 + # 用于判断用户意图是"维修"还是"百科" + # ------------------------------------------------------------------ + "classify_intent_system": ( + "你是一个船舶设备维修诊断系统的意图识别专家。你的任务是精准识别用户的核心诉求,将其分类为\"维修\"或\"百科\"。\n" + "\n" + "### 📚 分类定义与判别标准\n" + "\n" + "#### 1. 维修\n" + "- **核心特征**:用户描述了一个具体的**故障现象**、**异常状态**、**报警代码**,或者明确询问**解决方案**、**排查步骤**。\n" + "- **关键判定规则**:\n" + " - **故障即维修**:只要用户陈述了一个设备故障现象(如\"主机排烟温度高\"),即使没有说\"怎么修\",也默认归类为维修(隐含求助意图)。\n" + " - **排查导向**:涉及\"原因分析\"、\"处理措施\"、\"应急操作\"的内容。\n" + "- **示例**:\n" + " - \"主机滑油压力低\"(隐含:怎么办?)\n" + " - \"发电机启动失败,有报警E05\"\n" + " - \"分油机震动大怎么处理\"\n" + " - \"锅炉点火失败的原因\"(针对具体故障的原因分析属于维修)\n" + "\n" + "#### 2. 百科\n" + "- **核心特征**:用户询问**理论知识**、**设备原理**、**参数定义**、**结构组成**或**常规保养规范**,且不涉及当前的具体故障。\n" + "- **关键判定规则**:\n" + " - **概念导向**:包含\"什么是\"、\"原理\"、\"作用\"、\"结构\"、\"定义\"等词汇。\n" + " - **通用性**:问题适用于所有同类设备,而非针对某一台坏掉的设备。\n" + "- **示例**:\n" + " - \"柴油机的工作原理是什么\"\n" + " - \"滑油分油机的结构图\"\n" + " - \"什么是扫气箱着火\"(询问定义)\n" + " - \"主机日常保养项目有哪些\"\n" + "\n" + "### ⚠️ 冲突处理原则\n" + "- 如果用户输入既像原理又像故障(例如\"涡轮增压器喘振\"),**优先归类为\"维修\"**,因为用户更可能是在遇到故障。\n" + "- 如果用户明确使用了\"什么是\"、\"怎么定义\",归类为\"百科\"。\n" + "\n" + "### 📤 输出格式\n" + "请仅输出一个标准的 JSON 对象,不要包含 markdown 标记或其他文本:\n" + "{\n" + " \"intent\": \"维修\" 或 \"百科\",\n" + " \"reason\": \"简短的判断理由,指出触发分类的关键特征词或逻辑\"\n" + "}" + ), + + # ------------------------------------------------------------------ + # extract_info 节点:故障诊断信息提取系统提示词 + # 用于从用户输入中提取维修相关信息(舷号、设备名称、故障现象等) + # ------------------------------------------------------------------ + "extract_info_system": ( + "你是一个信息提取专家,负责从用户输入中提取维修相关信息。\n" + "\n" + "你需要提取以下信息:\n" + "1. 舷号:船舰编号,如\"101\",\"111\",\"122\"等\n" + "2. 设备名称:故障设备的名称\n" + "3. 故障现象:故障的具体表现\n" + "4. 故障码:报警代码或故障代码\n" + "5. 故障时间:故障发生的时间\n" + "6. 故障频率:故障发生的频率\n" + "7. 已尝试措施:用户已经尝试过的维修方法\n" + "8. 运行工况:设备运行时的参数\n" + "9. 其他信息:其他补充说明\n" + "\n" + "提取规则:\n" + "- 如果某项信息没有提及,返回空字符串\n" + "- 优先使用当前输入中的信息\n" + "- 注意保留已有的信息,不要丢失\n" + "\n" + "输出格式(JSON):\n" + "{\n" + " \"ship_number\": \"舷号\",\n" + " \"device_name\": \"设备名称\",\n" + " \"fault\": \"故障现象\",\n" + " \"fault_code\": \"故障码\",\n" + " \"fault_time\": \"故障时间\",\n" + " \"fault_frequency\": \"故障频率\",\n" + " \"tried_measures\": \"已尝试措施\",\n" + " \"running_condition\": \"运行工况\",\n" + " \"additional_info\": \"其他信息\"\n" + "}" + ), + + # ------------------------------------------------------------------ + # agent_think 节点(方案已生成时):故障诊断后续意图分析系统提示词 + # 用于判断用户在方案已生成后的后续意图 + # ------------------------------------------------------------------ + "agent_think_intent_system": ( + "你是一个专业的意图分析专家,用于船舶故障诊断系统。\n" + "\n" + "根据对话历史和用户当前输入,判断用户的意图。\n" + "\n" + "【意图分类说明】\n" + "1. generate_report: 用户明确要求生成、导出、下载、保存维修报告\n" + " - 示例:\"生成报告\"、\"写报告\"、\"导出报告\"、\"下载报告\"、\"保存报告\"\n" + "\n" + "2. modify_info: 用户需要修改或补充之前提供的故障信息(舷号、设备名称、故障现象等)\n" + " - 示例:\"其实是...\"、\"不对,应该是...\"、\"我补充一下...\"、\"修改设备名称是...\"、\"更正一下,故障是...\"\n" + "\n" + "3. has_error: 用户指出之前生成的维修方案有错误、需要修改或调整方案内容\n" + " - 示例:\"方案有误\"、\"这个步骤不对\"、\"需要修改方案\"、\"这个方案有问题\"\n" + "\n" + "4. not_solved: 用户反映按照之前的方案执行后,故障问题依然没有解决,需要更深入的分析\n" + " - 示例:\"没有解决\"、\"还是不行\"、\"问题依旧\"、\"还是没解决\"、\"按照方案做了但还是有问题\"、\"需要更深入的分析故障\"、\"需要详细的解决方案\"\n" + "\n" + "5. related_question: 用户询问与当前故障、设备、原理等相关的问题(非方案调整、非信息修改)\n" + " - 示例:\"为什么会出现这个故障?\"、\"这个设备的工作原理是什么?\"、\"这个部件怎么更换?\"、\"执行方案时遇到问题:...\"\n" + "\n" + "6. other: 其他无法归类到上述类别的意图\n" + " - 示例:打招呼、闲聊、与当前故障诊断无关的问题\n" + "\n" + "输出格式(JSON):\n" + "{\"intent\": \"意图类型\", \"reason\": \"判断理由\"}" + ), + + # ------------------------------------------------------------------ + # agent_think 节点:故障诊断后续意图分析用户提示词 + # 输入变量: ship_number, device_name, fault, history_str, combined_query + # ------------------------------------------------------------------ + "agent_think_intent_user": ( + "【当前故障诊断信息】\n" + "- 舷号: {ship_number}\n" + "- 设备名称: {device_name}\n" + "- 故障现象: {fault}\n" + "\n" + "【对话历史】\n" + "{history_str}\n" + "\n" + "【用户当前输入】\n" + "{combined_query}\n" + "\n" + "请分析用户意图(JSON格式):" + ), + + # ------------------------------------------------------------------ + # ask_user 节点:用户补充信息提取提示词 + # 输入变量: missing_str, user_query + # ------------------------------------------------------------------ + "ask_user_extract": ( + "从用户输入中提取缺失的信息。\n" + "\n" + "缺失的信息类型: {missing_str}\n" + "\n" + "用户输入: {user_query}\n" + "\n" + "请提取用户补充的信息,输出JSON格式:\n" + "{{\"ship_number\": \"舷号\", \"device_name\": \"设备名称\", \"fault\": \"故障现象\"}}\n" + "如果某项信息没有提及,返回空字符串。" + ), + + # ------------------------------------------------------------------ + # generate_response 节点 - 无检索结果但有设备信息时的用户提示词 + # 输入变量: ship_number, device_name, fault, fault_code, fault_time, + # fault_frequency, tried_measures, running_condition, additional_info + # ------------------------------------------------------------------ + "generate_response_no_rag_user": ( + "用户描述:\n" + "- 舷号: {ship_number}\n" + "- 设备名称: {device_name}\n" + "- 故障现象: {fault}\n" + "- 故障码: {fault_code}\n" + "- 故障时间: {fault_time}\n" + "- 故障频率: {fault_frequency}\n" + "- 已尝试措施: {tried_measures}\n" + "- 运行工况: {running_condition}\n" + "- 其他信息: {additional_info}\n" + "\n" + "请给出一般性的故障诊断建议:" + ), + + # ------------------------------------------------------------------ + # generate_response 节点 - 无检索结果(简化版)用户提示词 + # 输入变量: ship_number, device_name, fault + # ------------------------------------------------------------------ + "generate_response_no_rag_simple_user": ( + "用户描述:\n" + "- 舷号: {ship_number}\n" + "- 设备名称: {device_name}\n" + "- 故障现象: {fault}\n" + "\n" + "请给出一般性的故障诊断建议:" + ), +} + + +# ============================================================ +# 五、操作指导工作流 (workflow_operate.py) 专用提示词 +# 仅包含与故障诊断不同的部分,共享提示词使用 COMMON_PROMPTS +# ============================================================ + +OPERATE_PROMPTS = { + # ------------------------------------------------------------------ + # classify_intent 节点:操作指导意图分类系统提示词 + # 用于判断用户意图是"操作"还是"百科" + # ------------------------------------------------------------------ + "classify_intent_system": ( + "你是一个船舶设备操作指导系统的意图识别专家。你的任务是精准识别用户的核心诉求,将其分类为\"操作\"或\"百科\"。\n" + "\n" + "### 📚 分类定义与判别标准\n" + "\n" + "#### 1. 操作\n" + "- **核心特征**:用户描述了一个具体的**操作需求**、**操作步骤**、**操作代码**,或者明确询问**操作方法**、**操作流程**。\n" + "- **关键判定规则**:\n" + " - **操作即指导**:只要用户陈述了一个设备操作需求(如\"主机启动操作\"),即使没有说\"怎么操作\",也默认归类为操作(隐含求助意图)。\n" + " - **流程导向**:涉及\"操作步骤\"、\"操作方法\"、\"操作规程\"的内容。\n" + "- **示例**:\n" + " - \"主机启动操作步骤\"(隐含:怎么做?)\n" + " - \"发电机并车操作流程\"\n" + " - \"分油机操作方法\"\n" + " - \"锅炉点火操作规程\"\n" + "\n" + "#### 2. 百科\n" + "- **核心特征**:用户询问**理论知识**、**设备原理**、**参数定义**、**结构组成**或**常规保养规范**,且不涉及当前的具体操作。\n" + "- **关键判定规则**:\n" + " - **概念导向**:包含\"什么是\"、\"原理\"、\"作用\"、\"结构\"、\"定义\"等词汇。\n" + " - **通用性**:问题适用于所有同类设备,而非针对某一项具体操作。\n" + "- **示例**:\n" + " - \"柴油机的工作原理是什么\"\n" + " - \"滑油分油机的结构图\"\n" + " - \"什么是扫气箱着火\"(询问定义)\n" + " - \"主机日常保养项目有哪些\"\n" + "\n" + "### ⚠️ 冲突处理原则\n" + "- 如果用户输入既像原理又像操作(例如\"涡轮增压器操作\"),**优先归类为\"操作\"**,因为用户更可能是需要操作指导。\n" + "- 如果用户明确使用了\"什么是\"、\"怎么定义\",归类为\"百科\"。\n" + "\n" + "### 📤 输出格式\n" + "请仅输出一个标准的 JSON 对象,不要包含 markdown 标记或其他文本:\n" + "{\n" + " \"intent\": \"操作\" 或 \"百科\",\n" + " \"reason\": \"简短的判断理由,指出触发分类的关键特征词或逻辑\"\n" + "}" + ), + + # ------------------------------------------------------------------ + # extract_info 节点:操作指导信息提取系统提示词 + # 用于从用户输入中提取操作相关信息(舷号、设备名称、操作项目等) + # ------------------------------------------------------------------ + "extract_info_system": ( + "你是一个信息提取专家,负责从用户输入中提取操作相关信息。\n" + "\n" + "你需要提取以下信息:\n" + "1. 舷号:船舰编号,如\"101\",\"111\",\"122\"等\n" + "2. 设备名称:操作设备的名称\n" + "3. 操作项目:操作的具体内容\n" + "4. 操作代码:报警代码或操作代码\n" + "5. 操作时间:操作发生的时间\n" + "6. 操作频率:操作发生的频率\n" + "7. 已尝试措施:用户已经尝试过的操作方法\n" + "8. 运行工况:设备运行时的参数\n" + "9. 其他信息:其他补充说明\n" + "\n" + "提取规则:\n" + "- 如果某项信息没有提及,返回空字符串\n" + "- 优先使用当前输入中的信息\n" + "- 注意保留已有的信息,不要丢失\n" + "\n" + "输出格式(JSON):\n" + "{\n" + " \"ship_number\": \"舷号\",\n" + " \"device_name\": \"设备名称\",\n" + " \"operation_item\": \"操作项目\",\n" + " \"operation_code\": \"操作代码\",\n" + " \"operation_time\": \"操作时间\",\n" + " \"operation_frequency\": \"操作频率\",\n" + " \"tried_measures\": \"已尝试措施\",\n" + " \"running_condition\": \"运行工况\",\n" + " \"additional_info\": \"其他信息\"\n" + "}" + ), + + # ------------------------------------------------------------------ + # agent_think 节点(方案已生成时):操作指导后续意图分析系统提示词 + # 用于判断用户在方案已生成后的后续意图 + # ------------------------------------------------------------------ + "agent_think_intent_system": ( + "你是一个专业的意图分析专家,用于船舶操作指导系统。\n" + "\n" + "根据对话历史和用户当前输入,判断用户的意图。\n" + "\n" + "【意图分类说明】\n" + "1. modify_info: 用户需要修改或补充之前提供的操作信息(舷号、设备名称、操作项目等)\n" + " - 示例:\"其实是...\"、\"不对,应该是...\"、\"我补充一下...\"、\"修改设备名称是...\"、\"更正一下,操作是...\"\n" + "\n" + "2. has_error: 用户指出之前生成的操作方案有错误、需要修改或调整方案内容\n" + " - 示例:\"方案有误\"、\"这个步骤不对\"、\"需要修改方案\"、\"这个方案有问题\"\n" + "\n" + "3. not_solved: 用户反映按照之前的方案执行后,操作问题依然没有解决,需要更深入的分析\n" + " - 示例:\"没有解决\"、\"还是不行\"、\"问题依旧\"、\"还是没解决\"、\"按照方案做了但还是有问题\"、\"需要更深入的分析\"、\"需要详细的解决方案\"\n" + "\n" + "4. related_question: 用户询问与当前操作、设备、原理等相关的问题(非方案调整、非信息修改)\n" + " - 示例:\"为什么会出现这个操作问题?\"、\"这个设备的工作原理是什么?\"、\"这个部件怎么更换?\"、\"执行方案时遇到问题:...\"\n" + "\n" + "5. other: 其他无法归类到上述类别的意图\n" + " - 示例:打招呼、闲聊、与当前操作指导无关的问题\n" + "\n" + "输出格式(JSON):\n" + "{\"intent\": \"意图类型\", \"reason\": \"判断理由\"}" + ), + + # ------------------------------------------------------------------ + # agent_think 节点:操作指导后续意图分析用户提示词 + # 输入变量: ship_number, device_name, operation_item, history_str, combined_query + # ------------------------------------------------------------------ + "agent_think_intent_user": ( + "【当前操作指导信息】\n" + "- 舷号: {ship_number}\n" + "- 设备名称: {device_name}\n" + "- 操作项目: {operation_item}\n" + "\n" + "【对话历史】\n" + "{history_str}\n" + "\n" + "【用户当前输入】\n" + "{combined_query}\n" + "\n" + "请分析用户意图(JSON格式):" + ), + + # ------------------------------------------------------------------ + # ask_user 节点:用户补充信息提取提示词 + # 输入变量: missing_str, user_query + # ------------------------------------------------------------------ + "ask_user_extract": ( + "从用户输入中提取缺失的信息。\n" + "\n" + "缺失的信息类型: {missing_str}\n" + "\n" + "用户输入: {user_query}\n" + "\n" + "请提取用户补充的信息,输出JSON格式:\n" + "{{\"ship_number\": \"舷号\", \"device_name\": \"设备名称\", \"operation_item\": \"操作项目\"}}\n" + "如果某项信息没有提及,返回空字符串。" + ), + + # ------------------------------------------------------------------ + # generate_response 节点 - 无检索结果但有设备信息时的用户提示词 + # 输入变量: ship_number, device_name, operation_item, operation_code, + # operation_time, operation_frequency, tried_measures, + # running_condition, additional_info + # ------------------------------------------------------------------ + "generate_response_no_rag_user": ( + "用户描述:\n" + "- 舷号: {ship_number}\n" + "- 设备名称: {device_name}\n" + "- 操作项目: {operation_item}\n" + "- 操作代码: {operation_code}\n" + "- 操作时间: {operation_time}\n" + "- 操作频率: {operation_frequency}\n" + "- 已尝试措施: {tried_measures}\n" + "- 运行工况: {running_condition}\n" + "- 其他信息: {additional_info}\n" + "\n" + "请给出一般性的操作指导建议:" + ), + + # ------------------------------------------------------------------ + # generate_response 节点 - 无检索结果(简化版)用户提示词 + # 输入变量: ship_number, device_name, operation_item + # ------------------------------------------------------------------ + "generate_response_no_rag_simple_user": ( + "用户描述:\n" + "- 舷号: {ship_number}\n" + "- 设备名称: {device_name}\n" + "- 操作项目: {operation_item}\n" + "\n" + "请给出一般性的操作指导建议:" + ), +} + diff --git a/prompts.py b/prompts.py index e056a7b..962f533 100644 --- a/prompts.py +++ b/prompts.py @@ -470,7 +470,7 @@ BAIKE_PROMPTS = { # 输入变量: question, answer # ------------------------------------------------------------------ "generate_suggested_replies": ( - "你是一个专业的公文问答助手。根据以下问答内容,生成两个用户可能关心的后续问题。\n" + "你是一个专业的工业设备问答助手。根据以下问答内容,生成两个用户可能关心的后续问题。\n" "\n" "【用户问题】\n" "{question}\n" @@ -498,7 +498,13 @@ BAIKE_PROMPTS = { # 输入变量: domain_desc # ------------------------------------------------------------------ "generate_answer_with_retrieval_system": ( - "我是一个专业的{domain_desc}知识问答助手,根据用户的问题,综合所有检索结果,提供准确、专业、完整的回答内容。" + "You are a professional knowledge-base assistant for {domain_desc}.\n" + "Answer ONLY from the retrieved evidence provided by the user message. Do not use prior knowledge as factual evidence.\n" + "If the evidence is insufficient, say clearly what is missing and what can be confirmed from the evidence.\n" + "Prefer concrete facts, names, parameters, steps, conclusions, and source relationships from the evidence.\n" + "When sources conflict, explain the conflict instead of inventing a merged fact.\n" + "Use images only when their URLs appear in the retrieved evidence; never fabricate or repair image URLs.\n" + "Always respond in Chinese. Do not expose internal tool names or implementation details." ), # ------------------------------------------------------------------ @@ -506,18 +512,20 @@ BAIKE_PROMPTS = { # 输入变量: extracted_text, rag_count, all_source_text # ------------------------------------------------------------------ "generate_answer_with_retrieval_user": ( + "用户问题:\n" "{extracted_text}\n" "\n" - "# 检索到的相关信息(共 {rag_count} 条):\n" + "# Retrieved evidence ({rag_count} items)\n" "\n" "{all_source_text}\n" "\n" "【输出要求】\n" - "1. **重要**:如果参考资料中包含图片链接(格式为 `![图片](URL)` 或 `images/xxx.jpg`),请务必在相关位置自然展示使用,如果参考资料中没有图片,**不要输出任何关于图片的说明**\n" - "2. 如果参考资料包含 LaTeX 公式、连接等,请在对应位置正常使用,不得修改或删除\n" - "3. 请专注于内容的准确性、完整性和逻辑性,不要担心格式的美观性\n" - "4. 可以适当使用简单的段落分隔,但不要使用复杂的Markdown格式\n" - "5. 如果参考资料包含表格,请不要输出表格,而要直接描述表格的内容\n" + "1. 先直接回答用户问题,再补充必要依据。\n" + "2. 每个关键事实都必须能在 Retrieved evidence 中找到依据;不要补充资料外结论。\n" + "3. 如果资料只能说明部分内容,请明确说“资料中只能确认...”和“资料中未提供...”。\n" + "4. LaTeX 格式必须严格区分:变量或很短的表达式使用 $...$;包含等号、求和、积分、分式或较长的完整方程必须使用独立段落,且 $$ 必须各自单独占一行。两条独立公式必须分别使用两个公式块,两个公式块之间保留一个空行,不能写进同一个公式块或紧挨在一起。不要把完整方程嵌在列表正文中。\n" + "5. 参考资料中真实存在的图片可在对应段落附近展示;URL 必须逐字来自资料。\n" + "6. 表格内容请转成自然语言描述,不要输出复杂表格。\n" "\n" "请给出回复:" ), @@ -541,6 +549,7 @@ BAIKE_PROMPTS = { "1. 请根据您的专业知识提供准确、完整的回答\n" "2. 可以适当使用简单的段落分隔,但不要使用复杂的Markdown格式\n" "3. 请专注于内容的准确性和专业性\n" + "4. LaTeX 格式必须严格区分:变量或很短的表达式使用 $...$;包含等号、求和、积分、分式或较长的完整方程必须使用独立段落,且 $$ 必须各自单独占一行。两条独立公式必须分别使用两个公式块,两个公式块之间保留一个空行,不能写进同一个公式块或紧挨在一起。不要把完整方程嵌在列表正文中。\n" "\n" "请给出回复:" ), @@ -1228,4 +1237,3 @@ OPERATE_PROMPTS = { "请给出一般性的操作指导建议:" ), } - diff --git a/tools/__pycache__/__init__.cpython-310.pyc b/tools/__pycache__/__init__.cpython-310.pyc index 73c380d6123fc73194b5b4fe4c8f4a013f998b0b..9ba6da5c6a1e9ac7c9f253a9d9e805909f3d367a 100644 GIT binary patch delta 440 zcmZXQzfQw25XNI#MAPO^Q+0rX#Do|sAtqLY#8{~VUGQ=;iJKb5NwA%YPJM=`13OIc z7Q6vs^wVHB2!?w2+4p_tyVJe3KKFyd_q&ccU*GqSFReN_L9=>%cELNi!(Ht1F7EOk z?r{%$+{Zo-aKJ+x@;>e(Cv}p&SA?S)Plut75~^sdfaPVSK&?tze~j0oqNIW-R^*Th zQZ7J{6eJ}gUg+@u4XP+I9W65sjFXg_7!-Y0IwDd4sS;-5U+K{3f|yPTU9y-0$rG59 zDpPvU{Iz8hDr3PO)>KDxmi^Uh`i!>e+ZjPBGo4f{mW_AXcymFYDw@ZuorGgq5<$U0 z$sN~Mr+(7$^xF`2A8L%j@@NfbEI}9pC diff --git a/tools/__pycache__/agent_usage_statistics.cpython-310.pyc b/tools/__pycache__/agent_usage_statistics.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5bdf858eb32cd5218c49f2f80e42a44193498ef4 GIT binary patch literal 7005 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 literal 0 HcmV?d00001 diff --git a/tools/__pycache__/fault_record_db.cpython-310.pyc b/tools/__pycache__/fault_record_db.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf23432890c7d559249e6f058506df17ef07992d GIT binary patch literal 9764 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 NF*@N^t*7??{vUK2^OXPq literal 0 HcmV?d00001 diff --git a/tools/__pycache__/fault_statistics.cpython-310.pyc b/tools/__pycache__/fault_statistics.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..917fd28fd1ffc13780160480792ae3b2e8cee146 GIT binary patch literal 20855 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 EpOE0hFaQ7m literal 0 HcmV?d00001 diff --git a/tools/__pycache__/function_tool.cpython-310.pyc b/tools/__pycache__/function_tool.cpython-310.pyc index 12068244deb763423e18b054dbb24e00d8169391..d4fd75bac288b0e9cb2540aec17aadc1326dda16 100644 GIT binary patch literal 5446 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 literal 44589 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

S6K*P+a$O4D>lCeq`J7_#ErbMcN!zv%Pbj$O& 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

=~VRea(>*8!JV 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 literal 0 HcmV?d00001 diff --git a/tools/__pycache__/rag_tools.cpython-310.pyc b/tools/__pycache__/rag_tools.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c854b838540751d4db4137ec4ea91f7b885bd09 GIT binary patch literal 6456 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 literal 0 HcmV?d00001 diff --git a/tools/__pycache__/ship_model_db.cpython-310.pyc b/tools/__pycache__/ship_model_db.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..074ca5c4ec8916132d3d8d380d83189dbde2d05c GIT binary patch literal 6373 zcmcIoYj7LY72cPm)mnC(M@a&u?08DC+~9-|6Pi8-V=Kf{BO6O@h$z)4+O-krVOFb2 z>`IwHoQxA<@BnEH!NG2!aY_^Npd=)v`O)crra$^)|Ke5TnNB;M4l^Al#69QU^?D^s zCY_rnM*FDew>)QWb78DM|#TQCSW44GI@x{A+XPpfgfw?27r? zl?Tn!ufSh%a@xEwT0C)~IDX!|_C|4b#+*7S-F$O)?%Jz2&(GYPI8qwBP#XJ$T7Q1< zC0d(2TrelbZ@xWBU6_Y2-@JTXvOJxeG+&-I-1z?6hl!u_gHem*=M44#_VHe4d~7DB=uDYq1P@_g1`1OYhKn9kx#F zd`oh87iX-t6?P+25b}b;zQAe61{tOP*KEO+cXcw)uKqb+a2GrUk<@RplUb$%cLWt%S1SIYYMx9oq?ynb_y+o!tO*9%6_>gSY@7@m!_vmFOGZzy>QIt z3f*v6E$>IJt;$+;EL{f(+e_3uf4n$0G4ent&<#*3cl7iJ!_6&zd1qhGlU3%yzuM1# zu?m{m($)qsHGlTv{J{yp@d*FWmaRF2+~O-oOB1so^7EPx-cj=~(afJ7H!of+*KDJ8 zPEOCiIa97_BO3F)Go>4+%QcTs&GnBl?8@5NPTP6u4|AVXXdb0?UcWj&d70GtqLB<@ zYiGqMwp*i^JW`x|m3ptIqu6eZ;>3;OrMJp8bQI>qSn1{W%QbWq^KX1?UYRP>JVL#h z<0p%g$BD-3(YAKzQSsP&=J*F7j__?;e?aKb}h{#bS6qOGmA5!S(@$C+x+!W+Rn&7s;+8`FTA>ffG%P93-?r6C$b}8 zHpz}vyNMcOv@Ku2)H5xA*52J82+7U&XE(|8UfCYP7Wlx#TK+`gs;TqBsXV7qz(^W$pQ1$q8nY3!~Nj2^FQeOh@F@}H@4IZi(u6QPw)+4?| zI-w_G$*2<3V}`T@Pez?%@P^lh+Ov%I9q7ZC_iP3#wpjqd4YxUd4ZDVIWE;gsUgR6OHJ}&ShP49A zv7*cO; z#0{t$5I46)+&n2-li|JMRY41(d4* zq-xrWge&hIiYQ)1dWWB3S?0y=ylXeZF^b2gSj6L<%PiUiwX#Q?vcv=sDnTTakdptK{_CXtp9!NKF06M z{T;p_iL<%uuUh%RwKvRT)1^;8H4o3uAHuBxQVeG*A`$yOvKq2G>>#bm=pe5!$4&yV zvdx~cpAa~H*|Zc2O*`bKihZhSlicKp543=d3#E3Tpv>OHnhNDCrnR-zZthv~M98D& zfl=okg`yHboaj(S6-zhXG*6wrdG;Ee2@Utou!ebNaO6pAH(nY_Eex%-Ps=@hoq<02 z@jXBT4FvgQ0SW8ztt=b-dWK^jW>b8^z_5l z*$9CeJPxUaTazD}6ThV$0&Z;{d$$rOs`2!iKtXd#>TahKCStg{?A{^xIj{dGKl@*} z*^0cZzqb=}-B2K0a~wFPrOzh+7;@o`?*7g|XX}!*9HX7HfY0`Kb5^#6H5>i{5vmd+ z>EGWT`4$000(Ju|Iq4#>1_Y-zeDyg&X+_Jy zJGRU_EeX&u2z2%d+akoB0f=*15a(MwiKCRtf;iU!mj!X~L?Es{mxMVci_{#Oo_`DS z!j&Pe8iJAP%lQ?-DeCnb!zt>n45vogI^l2u!o0?rg1mcIpfBJ|5+P9U&uW`s;5j5j z7-ZCM)gYs)@*vY%p`PDO84qsBS~=gS8Y^{hQmyJLVHC4aXE9u)mBU3~eYa`2G)iUR zvIYCV62{TE4dMB0CT+O&VLTDnIP4q}2u;Ak1qI}Wne;$nkP<8z#Rvj~ ze;C4ENZmAyL=F8r;vLgFjSwU8M}UpFr1}$#)hsd=ff3ysSRW_ycXx0P3SR-&S>QOx O-FWURPLOWh)%ssAWehj~ literal 0 HcmV?d00001 diff --git a/tools/__pycache__/vlm_tools.cpython-310.pyc b/tools/__pycache__/vlm_tools.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e25b979cba2a024d7cfe949bc98ad12b06a8b9f GIT binary patch literal 8936 zcmaJ`Yj70TmG188d9_9(F%l9F88^1EtptRziL+L`Nw6&(B_IeQ_R`tZWSDM=5i=vX zyN8Fi$14yL;vs|s1{;JKVF)+@i8wY0Bw(wyYJcqRkFEXLwY9Yst7k^FKb+qVHv661 zGqQlx&Q#yN_x8E3bI(2BIp=m)R!SUx|M8#Oy5#3M?h7i+|5c*!V?4%Hp5s(b;Z$A~ zlDxvRx1b2@?NL1JEh-{=dlfHx`xGDEo}@qJSNuG+6_bHfPzk0aMM_mD6|Byi45ccS zN|yH}!>KBzisgl5wNi~X{^WvGM2WDvK(Z!PtJJ0zDhpXTn5;|HEA=cdB^RX@D~owf zt5qu&bIKCDLwGM$E0qQ{tURGsDa+JqRU~c}lI*o>uGe z)M!t4_|!$}V$?pPE@5;}gKnwXfSMKB(^`Y}L@n3PYs<7JJ3Q(Wpjd|INwk&mEN6Wz z?+B?+sZSs9D$fR=39e9IVsy`f?m0Zq|r0M&1snF;g=WDXr0C2R8MXiSBePX^U?p;->9cm);8! zZ)+;nsUhQkZLg^rt(%Y)&F=1GqhQnYqOWgiTi^PoUD@8U<*k-2r6P!en|ic#EU|8L zD+;xy9*ggcc4X3V8YCKzC6k67i1j3*+hc}idsE%2mP~Mc99KB=A^m25H2Xi8oZdr;)we(fBMFk(`TeLO>*JnmHg1n{K)Z{yJu%^4}$H5LkEjDC!D(nzMLL% ze!E|8>PdI5l$*MGkgksP^sEN2W2>8*(DvG)FQ*UDKrfa%Do$LOJw2WuJ>(4hv9y|t zr;3;NV+&_5j1~`lipc5!^1n%a`Pw4Ru`!}jT#nbFg6D80EKB!~;(c^!}O8WNMU zpwjw9<^E^lYZFp1<4yl-V z79Ba)2lJy>ozd$hA-HojKYU?k^0xdMB>UyHa$)*ZA-B&K^zC1HGiJxCziDgq+G00O zhwUTbe|fDP-2U=fEgi=huti;qskTqmsMILv5sYB-9iMP|19D&0Gf|@RXRM|TWx1xF zy>`fL@9ywQkQzQDAtAy)^FUulop^UARASB!Ez!Bdj1xo3KQA&O9>$y(kTJO&ZviEi zf+1OfmB1`k@>qi9spV9mp391ss3Ol;F^XoAjGrqn^3Qi`=~ZuSkV#}*6*e>owN2-WV3sxmNsY?skt+7)qxzHyOh6u9FkS;#u@)0f9h8La9@6O zB7f>&e(YliUt27(OOtiYNc1Mso$`0(HLtu_>Q3&_bUBkwn2`lupo*g?6wERM_I5S-knLCOsAAq{kI@BXf=`$=JoGV z!;mZMu7KJVuBKsa?CMl(Pc)J4$)FD=3w9utNJp`&njPxu(K@5=fT0OZ_a1>Eq^~+ zlDsec%W~Caif@e7u^++WvgtWq{L>jWf=t=75dCm%3!cHC+Y57R_jZ73rsg z`bba4JCF5H^H>kHn9%~OhRwegX~e3=oPBJLfn2p!W7T#DrnhTBS7c1cE;Rl9T(-_y z*i~cIsWngxwZqVy;CHQ6ms`j*Yn{3fT&`E^ta{=jM7cf_!w$M?)yd*dEHkOdx7S+M*e;6oyH6D*g@Y+Fn;SwWFgLIwT!q z2cQ%oF&Rx?f@0$$o#0hpMM)DSt0^JWBy5eTnr;Vx=TTZ~*(_msPa@T+_`3B(XLNVz zy~E};yMnNwr1_M}htR_gxyVyei(UhUv!+x+JV{ZzwTd^b?M9F61<>k9z=%h=l-Z$F zxa(A*dOKlqWI8%DT?xlw>EVN+sZq@KQI)>fNZ|-X-Ifd|up)L+n7VxftCgexF9_AV z#Mkm+_7C(44+0(uAor{g5CXi&i-O2kqa49M%vax!_#*rgp;qvrwiYe*jl{MJ*0GBK zUoD$gSncL@7(=>hG<6x>X}dzzjJTd)yK76`8Ixc`P3Sh3No3Iu9LhLhPx%){hWunU z^Os@w^nNo36(NI5!)Glgy)vNj#ScqA#>bJyQoY4R^j<^1#8p!(6^-$+jdR7gh zR1LLGrB$kgQV{B$N^8{$lmwzTg)Uf3YDldd7N8EroOg`t@~L69Y5<-i)M~Zq&jrS~ zTu@yw#xphRgL3o&poA_dR{?9w5A_-_L%B+eKq?4D9Z+jvxK?H*OUeNT#sDL412oF! zYB~UO)tqmxT7D~F@uiWf$2hfiRLoY;+}J1!EYBEUo?|BmFr%>}RIC46?+Stjb)j0P z)(?9|YO|7A1EnruE+KOvV5Y8LFs@Ro$L4Tj{u}_Qn4wwIRc|ip5cdA7YfMwSZCn~a zYK3x(tqNKT8*fo*yahB~*AjrLguPmJ9pj(YB{%V6vwY4Um`d@T)2GQ`OVS%$Vl zM%n&l{jbwIjb@gU@tbcsHJ1l<-3r&bhVMyE;qW-jy?H8j&z?PH4NHD!J~s`^x@-?q z$McR-Gl4DoL5J@2Tr`@S!?y>mOj(L5u*J@i%{zn5vb*^Ci*!9?M$M8f%u$=SR5z$m z>Y90gw8Cqroq1OM=pk%<$EJ_RkMa z2A!-^@({@`=XcMN_l60=>oYRC7L6J4M8ftZ(yEp=Gvx05vXxx@c7T`)lY?!obMsS| zvCVR$sJDYik5WSBhrSg_<1+pGlz#<@Ep#J5lZx%spj!=F%w!U(?WJ_B?b)I2(P;*@ zC*8f<4uI_)G-F$2?zh<0ok%mp)|;t2f@yf4dcxG~V8ZAwn+E8Z)(sDXxP>v8eixJ7 zaDb(Rkw}9B={R~#;#zjc$F@gcgRrBz#0Gk~-o(vakT*dENQG3=r> zu~q*$C0i&V{L)*Hz@OP;&SS%R`mhv<5-N!@7^?48s@WXDd_W=tBmE~tNj~{JU$B9U zhKJBH8cVCCCc8ju=tzW!1W&M9s0IuM_-`Lk2n#FWFG)fO-y0qXqQDCPN#FfCzlb^k zpef-m@%isKXw?L-Yb4xL@rT1j_mN$bAB3{ap-e`&^&vA{ zd@HAlKjE@o)2DhZZ(0As$>J=as_It*!(!HF`PATI2pp&+PZb-exz0BfXbGav^ZK9C^8`LUmbA;ax{?0jkrEqy__QQ+$LkM3Sl3z7`YjtCQ1ybRd7^s&nxSdM|72Rr1W#K=Iy5=j=!M(Z0gekQ*YKr>IxS`I8g*qx(BwyaD7XZef1pf=;@uMn zv>|>v0~ot=0RVz&f)|gj0g=8}5xRnfhC}khS66?Vml3RV25!tuox*8t&K3Lm zi}wy91PeCNn%EenH6d8bqPwNgEW-feCPi{f0pL5s#m^7qZ~lQ8#sa|eL%*OcPTZWC zI$4-PbZNgcalUx(LuWcy(t)>b}PK85ZC;<{fIR?xxxI&yR?<#2vT2}J##$2A0fE&5}1uR zl)Gmb#~Hui#;ptEeTDPautmj*YsD*lu2F%_$yC#EDjd3n3yb_8uON_&Asao9I8|-T z`(<7%6LnWankB{GgUbm(Uhk4p=CAk&KsV;1J)-mp@iGKPe?EWW=Hr;iPyntQLbB0} zp6m^-E{~8QH&Bw6|I!VD(~&C6)32WYif_yiVEoB-nq4t-an`x`hvH|Knq>yAI+QEN zRz(2;SCy0rY+h=tWWJF8V^Gx--tTi+|2yA*XN_7# zV*&eshv%3-V|hwmjdyRl6s$zx4#-wPcy|t-Ekibe0+v8F_GZWy$=s(E6S6^Saml!4 z-J2!Xgxxc`Uj~LC^$=NCAG?GUkvnh$cQg1zjXv%xe0HI5b`rN&1$I3nv9=F;F|II> zg~&Vzj5l{*<~lf!`zLIxnq^!q;eu=)&e4?F{x!>U!$Y&;Zpj_^aks*~=VM?pub)O< zCpm9pHa$#iNso0#4K1d}5txw?@*c)l{TL;rJl5M`qdTL=wbv77jP5aQE<*wY>@2L6VrFak(`g$)M{brUWEuKEGHs#kl4+qQvf!25Iy&3ATeYL3Zb(kANC(hi2Md z7}hC+oosfT8$<39o}0WP(aCZTrQ41zv~)_V+%-kxr9~+LHVEuuI{*^4VEug>O{9VT zs^J@PHRlDQSs7_H#T%;k;M|4k9LQK;WlrV?HXYTDGtw-CUQxHgJhu*Wf zbpwkT+Crk6g$(uQ@X@YK#?qbf9WmTCz^d1pT-MTcjqO87u_Mu`2dNWM3w8~=q&9G) z-NbUFa$OBvmoXDbqiODXo85Lp&yhw^LO488G>X2quDwMGQ^{5BZR<9+=>63F0ip|U zY;B9SZd~_fOSHXJX;DHHN{#;f7wa~(Zr!FVXy4YpwPj;;^Oj8;H*bxi7q-{zYL9^i z3v#;QP710qlb;C<o!~y4)AM!e0cR^p)^3zTSWItK^mbBD^Jd X#Bb<}kF^VZExaO#;@1z;R^R_W>5z_f literal 0 HcmV?d00001 diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc index b4cb04d1151e3eb8bbcc18e21f3f66cd5290f9b2..8cf1caaa4597201a3554c51e6124c1e56346599c 100644 GIT binary patch delta 21 bcmbQhIDwHjpO=@50SGQ!ip&z5$lDA6Gm!;f delta 21 bcmbQhIDwHjpO=@50SIR2oX(V*$lDA6G1dh+ diff --git a/utils/__pycache__/function_tracker.cpython-310.pyc b/utils/__pycache__/function_tracker.cpython-310.pyc index 9846d1097dd6438fbbbcb7ee765c2844a7c1ba83..fa01e63d88e562c2fa498c6937face3afbca2d39 100644 GIT binary patch delta 1801 zcmZuyOKcle6wRBD9ow<$ zo+1;C-=m)^M~ex=DAB;Q9p5mDolj*ReWF}|g7D0>fYt&SV-Mn=X3~U@t^>%m24~Ex*gE6&vnP!pujc9_JcwM zrjd1m)so}mQ^-vk%eGrGtX12kK5a%%Bj;uIAnC3i7h)j}CsA_>K}N}=bQ<9W0bE=1 zJO@-1Roe|f7g8Y#hma6McoC0?&&e7mRUX|F>~`uhnQ1&s{i;0I>TF$w!+3fY z;dO-8k+68w`%whj0EgIe-(1gIC@vsKu2yY|2 zhhQT}r)gFg{B2k!aRZfWtrN*fv)s^eQel^dejaGgA%`R^7tXOW*~4U!+1Y*5c!j|b zP|#pqJa}AozzA+u=&t$gp7b}_SBZZ&-_KIR|BQubs&LJx&NAT3o~4)=IZf6Z*GFRH zES~XrB@7HPR?`l^MMZ2eq#>OR9S5$XCl#vb12cgYgx!Jbqx0Q4p%g3YxdF~;;p?#p za!uCw0kY-7T@ccOxLdeH@D9Se2r{`EN-~n(jMO&!|Cn3*tj(M-Ds;hBv@HtYU52q9 znvQFhsbRo=)XQpgSq*HULPL<(6K^8CkMJQvyQ1bSQ5Oivk&X@@cf7MhBRy(H9qN^O zt5F({lam+#HlLCqfl^99L6Md5=&I?K9I>ix|7rF`ZklZ2sQ$?Pn3pDU-fc=N1wJhP z0@zM|Fj>Sr6ydou3b*d&?+v#??Q-3;V~zae;K!K6{$>{(%<2k)%l^t=*_2nd+b~Sm z^#T(wnD`7gbC<2@M`{Kki!g#PfgryE6_k7g><`z2up$bSJ=j9+Ed&hzpTv;f`BIsE If8e!$0iVj5dH?_b delta 1801 zcmZuyOKcle6wRBD9ox0@wc|MCgHyGd(uB|--wIl(Nm@$UCWK!_hspRQ8D%`<-WfZN zN*h#!klHNTzEu?=@mU}*gQZF=!j4r}Y(RPfspc)l%Gt-+r)3vz3*I|*h-yM2xk|SF@ps|N(5-puTbblO>Yy@>|1SuWZAYp$R26c z(Uya5yVQajfu1H=x~17I{U8|-9(PAtY|ZvdYmj@IT8Zo>gKRVMhzzmY(R5;G;;raW zV$^?+ex;1_glQINXgO|Rn!Npq%%hK$GcYD1b1kIR5Qf>k_!s>c$xyg5KEa8-t06a0 z=^THmkoupADe^i7F`_)~1dbPkmS#U)9zKHHq`ByL1=C)3JQ~nO_B3)%vU^Ey`KS<$IJ|_Kml0%^y(qnc@Tvf=E%?3* zDvGM(g`f+`h{6FR;5xu@G?9Rl6;3KVx+~cUB&69!YL1+)KS=$m>}yW8ro!`RokW;I zXqtq_!>(%)90MF+i``S57f_r-95S9teeLySSlB(7Y1^Pz(A&K;WSE70bKR` zRUUH!f6<4c6@uK<*r}>bOQGX?TrUB(ha;$UaZN-Fcw}W&KGnck;_(H)EQTo+vBk2- zuJ`onXD~F|?8!~afym?UpjB#@F{nu^qNv7Z$_qd!Pf!3K8Wyh5s@U8iEK+u#uK(S$ zqL4G}K`P7EGy2$@O_Q{r-Tf-h@-3nJ1@$v*D|71bITX($yp1rA@D9Se2=5^j5H2G$ zk}{+3x11~jP#H8Wl9Og5*%wHa&1Qd2x6UDtBzYL;*zx}7$UJlUhYsNt23vrk!J2sR zxSW6)u9fJH^OI>G=l;I`4RYm9zL%wP{|sJ5SA}ZZmbK)O{>qgtWB5>A$n$PdnkDd@_zPg$ z`M%@_Scf7aw@2aTo&4QgGu4jJ4L?@TkM(U}5zjWJ5MovDBUIR5`T2EuWjjpM@;pDZ z@PdiYaHDoPnha922m=U%2=Y@PzXDZ!y@G)A;d)pui3a5iZlLxi0;c~@VMuR(t;p_= Gp7;1*A{^3vh-Fw&T2We8N z>PU0v&b{}XbLXCOzH`o`>*@j$uD|~6cl-bGq$J&EFRQN_y!-;UIfq7Yx> zc&bUIUyZ6V8`F|nzbEQJ-(z@nub~>=%~Fr1`*i=17WL^h7!8O~%zAsgdaWMBj9+i% z6rpwVe!X6A81h7GsF!8rbR>N6CjqxG>nsks`EyIRXCBOt zu(|7bXZ&R8>`nLh#Rv1F59ZGVwzY-b(R@G@SyJ}uU{g?VRw!nyQL zapB~H`LW`i^J2Ej)ctVAnY^_0$yK)ZkKc1o+*w*Y7Wi>nxO5jw^X|l?GjXcAI=po| z*dGt4NuoEAiYH^4xV6s=Gup8`Y|=fu=#GElj!e4OJ}pgOU%O4``j~t2=5OA7EfOfh z0;Gb9<`he{G^z~BgEADRWS_#a;@#u;5O?bH;_Nl?UE=(p7U$m3k>?Zr z@m|Ahe=ENA$j+_N=eE8O>)hJbzUSG_wk%wm0A;NW9yMiE)1BjK=h$(Ub-`D3f}|Jk)EX3=+2t zoh!x;t<_VvfK#-IDkyx^N3}kUv{4_ZJUR8i2C9Ue6CW+j z%(R0+aeltt9l7G3y2fUfJxX}Q(%=HDw>&(~yl>fTg*dSdF5{uxUj7PK2Vu5Y!j0>n zJ0hCxF*C`8WqXq8Lk1ztI&BY>kVr@TqycZFkqtGi=%y!;G*aErWc*gFq#2VYGkwjg$xVtTYqDQyk{e_{ zdYY`{gM5szB@Z{j02A#8x3%}B(ub0U-fOfov1Yqn+Z8vBScVuqiNoFO`<5?e(d_1) z_zXH!B3`QKa#kXKreO;sR321vswGnu{;3SYmYDwGlWhz{neLZ98(JD0bjD7FUyqYM zJ$)z@7J4p!a1Z(io)%{>EG=9YPzzLD(R}IpY4=MY+ZjGo`W(KU$4U&ipXS~37o9(h z6&J4q)dUbi;XtM0;yD<{D*XIUUtU3k!-o$uIF5t`ld7n>F(JyCxKfo?0l=xeGs)0K zAa{B2vNLiN*k9qqF@FrkzS_!!4Pu8CW1PEGTsY1Qf22_U=mHno5mD^cSSn5WHK=L| z`95CP;K?QoFg)V6Fap(5N#F_4ST;03by@$}(YL;bk`t;y9|eqEz}-Lpx8l?ACrgVa9^ zS{O1XNhs;{hSpbdopTZP$raMWmxF6bL+W1&`8LEl81 zIQ;;gHGL8pppCkpHgPE%^Z?y(P$mPqS3GGAZEBVDTF9xTzCutB0!e+?PcxMZb^NWa zRie$9-w2wGl~ZW}Hu`CR@uV%7*J!QS2i9o^!g?sziW3Xb*5wl;f5yqx(^ef{!_H7| z7zaYHU1Q#0=NW0t{vCR$TJ5rfRJ~64+Ik1!X-LHl2N&QkRcQR*9Ea~ic(OA&RQhaO z2rd1GZh7nXkv-3> z8R0|UnmBYuIAv!f%cA+p9y+&0kP&%$lJQc4I`!7YJdDN;t->-1AN>(l(6>M8XF-S8NHX77D;4muW7_d_det) z*{;Z@6}X6oUVZ(Q*skCHdRNEpSKsK^9j#6D^RUDc81g-~tsvv$;Sns_xE!ToOi&C- zOEkcSVqAI@n@1m8iv}3JERXU&8x1AX>AtRbcV8@-=tn&Db{Rl6BU+!%7%2ov(r1uZ zCT&LRkqTvzd-8EMP=|yOWTw&EZ=?{2<2~45w4TR9L{S2AEVI4+30+Sb(Yj2Q7_sHe z;!v~6WGqXPc0D$V9FFKQ0jbeY3h`7VQ~Z;enpVOtc^a=fahtzF!$Pd41aUXx*5sfX zK$u0KML(nlWxOeVWNHBfUN*9Cd@viXljGpEH`|mshsbcILowE#a0$J5oeLz znqC%Tfv;*6Pl2lyJk_XXwupQ#=K-c_98(WhFjc8ws)r*SLsf>ZH5GIXP=5iq3gq$t zxljV-Q1}2*YYR146`~5y1M6x1$|^q7U;#^Mh&CV(ti?)TD6K=r*GU^eU7rgc*hzzA zgw_F1H9UhW^gvk|Xvt*yfq%fz+y=ddZU8<85H*^q2b^m-J_0+bhe8s2!#dA_Csxfq zG6Mvx&He?4Rz<*7ak~nFNHzZ-5u3|&uU#x(xv-q1tgrM|{GL@tl^lb`eO_L21}N&B znLF;xL~-_t7+z+E>?w}QQB-N)`U#vFNV>pp;>y<)a;S_L1b-bPk(DfS=mv*N@39_DJjvks)bi?KXz^F=iNAV5kL00poD3<;15 zshY}agZ0$`z)21&Psxw5l#g-h#=Yi(Ux}2D)d9LFwwVf+j^D?;KmUFJe^frrmu`Miy88iaPFSf} zFG>WFLzMGpoY`5J9K2)iid+4c+5KKf5yhi>`j)deT%NvLoV^InC>Kg6KXR{KsHRBx zhvM8jEeebK`GL~ZK+==4CcnPJ~$WbW7nLk-NaT&9$f*KnT)k}GL&Y7D4 zE1Xv8(ttP>A-!4v;cNr|a0L!k<(8-NY`0&Wgh2=rFc<&DP|!n`5Wx;|l|mU#?yUeN zN`Gf!W|h&eHGg|^uTiatNxDB{#d@;|J&qqu_h6+a`y&F=W>Gw%kniBp5vj9%v6!Cj zj>SaQ<+a|o e*v`DiWC*26eqRvTK#TgAFTA;>`KcXE+y4!ihAJ}v literal 0 HcmV?d00001 diff --git a/utils/__pycache__/intent_matcher.cpython-310.pyc b/utils/__pycache__/intent_matcher.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..addad3c517bcfa467d97f23bda4a1f99952692f3 GIT binary patch literal 2517 zcmZ`*TW=dh6rS0OZ*h`xQD_lD%L9}MNGc(5soFxhsTGi-wtxtrmFw9grq0^T?m|;J zQbOX^g@guZ(w3x3TymjOF0`P;agtVoKY)1LSG?=>Q(u6GzQCE;#B~d-wd370Gv7IL z&i5TVRbL-LaQ*t*dmp`9htTirrS=NK%ZqRuT`;_j2x>9(deSf` zx7QK*2x`8^ieiK!?%-Kx{JJx=TFDMOD>F`Z<>vag&SbGv{L;z(SUI=oEe$JAC|dV1 zjgW-isd!5ZrPVX#@j_{R-6_tNza1q7{dHwEvB*itA)2oZ={ey2GR1mn-FsQGjc8a^n#0ZPW^+A2kcM^hUxk z?a-mLnMmoeWb?yT04DtN@kjT<5**tS!Rjona+5Q&!AxN=x!3ZQ^NW?s7x)4%Kjbes zyHr}6v4umg^H*>AM`wD{pP8O46;JWU%EkzLi~>sOM&9jTau$c&(Vu)nVYqzmdoK?q z%36(VMPaxy(dW%VUbv0ZO;eAj1bneuO-T3mMw$~0Gep7wxz~TKzUOm!g->as(Xw1;mDjN(t zKsD2%dK|E-7jPa)aQ_0g@e&OE*aW#*m<9*JPzW!Bo1Hur=pd89EpGvi$v{ZOfOT;sLsl2Iu)0?wT5RmEZ{P~1!T<{)L{w(sjs5iMx|dJBJ2W9bBC zjItl>)XbWss>hf^HD>haaVoO~a+*RcwwTm(8Un)=L>u7i)&R|>8 zJ@yV&(+!Ji|1R6Zu-Rbs7y3e?grj&H-j3-uFqHp#{DZSU|LMJ8M=V35>c^Hwd+e~; zMKz-VQs5#KDj$yn+Vs0H#fR zMjSv;cIg1nfXF0}l)B|!0Vp9%1#*Z}0_J)}cn@YoGn5y4rHn+MF~b?4hmJ|GSI)>8 zd|akSO(7#qMKW?8??Al)vyOx^G6|1MB$5dLk?L{?#^|Ns7M(~vP^!WD144#Irzj3@ zri>Q374P~tPId-_;f&0d3Pr!>e$S2Y#rtWev70}!t+%tZTe$;Z?6f;EUtLyB;O-jY z+|}nR7jjPDI_uGE7og88yIP)oj^VX$o}K@^o?2FYn#ZW&Ds^H``k_ z3ylzDvUvm+^U*jX{MN-P%KGfna2wlU=m%MT80^IvsOBf=9jMR80zr^5+{S4K{aa|+ zWedkYy@Khzuz_1C>vwi|Pp779C(?A^vkZ=DKnZs9mmI5YNb!GsoUb-Yg@xUUk?7ugQfzs;<7C~FiZ5QW zcjw8-&L zk};YvdwjwADi^zBCu}*P6YT_L3Ip}?6x~-vZRA1R*1H|8A|*7VJqQ2{yS-sE^iBEt zNp4~=Jkw48ozcf(f$ZSwefD5n4%YAEP7hM**}gbicg4J|aF8`3I>@yLA8n literal 0 HcmV?d00001 diff --git a/utils/__pycache__/word_generator.cpython-310.pyc b/utils/__pycache__/word_generator.cpython-310.pyc index 157de4901a3be04ed1a17bc28eb2cbf352d7994c..5e4a4d2b6569e97028593e4ce4ae2b53d7b13ee7 100644 GIT binary patch literal 22084 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

Hq3=uYxNHA1KZ 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 aUB4B7!NvaRh@U~=a73C-Fwc+5_x~3Pk7$Vi diff --git a/workflows/.workflow_baike.py.swp b/workflows/.workflow_baike.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..e3896daef06f45235651ff98f339cf2d662ad460 GIT binary patch literal 16384 zcmeHNZEPGz8J+?me3gFi2mTG`ik!>!(gZ}|Bm-`fMk$SxCZR%2)7AQJ@4U%*_p-Z( zYeJ~)#IYTJoYOd;HgV$GjS0!wNlI+TcKoMQ>YqvlgeahbTJPQ+1?3Mym6}qXnb|$x zoznp7FT}3&-0sdh?>jTk%=^yF&ecEo^8=XTWE`XTWE`XTWE`XTWE`XTWE`XTWFR|B?YYM#z`Y@?Wxz zGB(fN{{QA(guDvu1@-_NfQNv0KTpWtfdcR+;7#BX5CfvXSAlo$ggxM8U|}H$XQ~ z2W$c&z!!muPZ81p)B@iHz60D1+y%V0l8{$`!@y&}!@vsQkDnyu55Vt%6F@)k4DclI zj}?SW0hfU$-~nJIa0hVn6NJ14yaBWW2Jk3wAMidtPP_$N0Q!Jlpa^%?&@VlKkpU?ix9zExbpS4eS z+e72dP*>q(f9}))XQ(B=aMpc8w_kyZeX!lm&g2&c^JjbVr+e-G&Kq+*h1LbTy*HO_v3rj?-E&UgnB6{J=*S4m zZ@0AZiY(4APr`a`VA$#GFSN9wu&uQl zR@;Nu^OKjH7g};Jji5%k?hCn}cRRyl`H2x!*3OK11u_O?vssJMm=dRk(qyQXB5~tV zlWH|c4XSDBMsv`#WJ`(ACu+B`hidx3x9K`HYV9yBBSdSp=A9AH4oYFeN*fy88_*Ob zCgIIcl^@Eg8=iHG~@o?Wo>ZuWA_M+*<-%uf(|WtYyei3vvsOLL3NGdwtFA7vm2f zrSN>Gb4rAi0i7)xH#>`Lr#F8 zLu7X@jup<2-k3X;@0+qS$3$Z8(ySXt7tT7Z6GhfooI~%6f7E&Hm7==Fy6Hb6tqOi! zcG3Kywp`nc-7)VRovakPAxkj~-SE620^$rxt7u{1WPV~|ajAx8Iss?FsM)HdlB!}x z=;H{$JIX`tPNr(lUdv~*_TXuI)SZUx&g1#BptNj2uCRtgqgu@5$4O>Y^Qys*S~D;sj+Lsf4@7Po~-(VY#K3{}y} zRBpdKs+82LUe}Y{*mCFOiWMGk8}{=H9nS2i-QUMn3w&weT3JJflJPr4g?U9$D5T!>?&qN1cJNWVgVr* z8mKUjb=*dI_~RycdU@O{nzpAd!;RscXfoC`&d>$aJ~unOINie5LYWuf?T7_Q(Ld&U&(2+ZI)oE#EaV46RP19SO1h-=p$C$@dN62ztwX>(#VvXzG!dKlT zq9JQBy^(ltiuc5qj&3TCzp75+m|C&mVDqc#EiudHs}Ox;^QKLk>%93(XrwJQX@=EC z%#IR&_HC33*c!zvqquqzIW0Jt8D{>?pS)a44o}29B*I5YGVcMQU92#B z^(+L5D4_Vq>TuD$WDWcm8(lgF3YDa+C3tyZ{_9!ftKbbRjD_g7?X_FChv@pcb#617 zQ&@3%Ig6t)RE%&TzI5ix<`9qXgqY1{nDulR|NmCBSWtIgW2&Lajmn3<%k?Hx3rbR4 zO)8R9jSGX`nVPi^ohh81$qi%<5D@XH_``I4F+bl7^ot(Eb6Z!q~?)GJ_4Kp%X8>V7gf;kE^K3Nl{ zLXP-iKi|X2TMEE@^W;v1#rd4%uOG47$1t;d5ur0bf5GlKhDZbxcHgi_=TQj(@a>jX z?0j-v9Zn{bUzo%zb~}aPDijzmC2BIp8Ni3Wx$Vzz4|p1~3Br6lezy0Q-S9pcP=Yo&)^LXTWE`XTWE`XTWE` zXTWE`XTWE`XTWE`XP}G$ip%}r6gJXY(8711Aolt!o&T^0c8QZO@ljPARCwQL*(Y5b zt%z^40asTjd1~>%)0&WrL(GVKI98d<>Fl=;4PzUZoDntMP literal 0 HcmV?d00001 diff --git a/workflows/__init__.py b/workflows/__init__.py index 12a2821..08d3aab 100644 --- a/workflows/__init__.py +++ b/workflows/__init__.py @@ -2,7 +2,7 @@ 工作流模块 包含所有工作流Agent """ -from .workflow_fault_diagnosis import run_fault_diagnosis_workflow +from .workflow_baike import run_baike_workflow # 其他工作流暂未实现,待后续添加 # from .workflow_repair import run_repair_workflow @@ -11,10 +11,11 @@ from .workflow_fault_diagnosis import run_fault_diagnosis_workflow # from .workflow_report import run_report_workflow __all__ = [ - "run_fault_diagnosis_workflow", + "run_baike_workflow", # "run_repair_workflow", # "run_statistics_workflow", # "run_qa_workflow", # "run_report_workflow" ] + diff --git a/workflows/__pycache__/__init__.cpython-310.pyc b/workflows/__pycache__/__init__.cpython-310.pyc index 7975dd023d9c7f9fc6a7bfefa06fe57a12478f81..8e55d2e2b02f32cd7744b5ff41330ee38e89936e 100644 GIT binary patch delta 36 pcmbQkG>3^dpO=@50SGQ!ip-Ll$h(9wYT_m>HddfW5$nW9Rsg2(36%f< delta 36 pcmbQkG>3^dpO=@50SIR2oX*sn$h(B`*2GO(Y-~W0BDRT-tN^Px3IPBB diff --git a/workflows/__pycache__/history_manager.cpython-310.pyc b/workflows/__pycache__/history_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fa781b2485acaaa92b649137260497dc29123df GIT binary patch literal 6474 zcmb7ITX5Xe8J3Q;+Fg0QcANlCfS{yYvXnv@n6{)$3q!)C1ZSFQ-cY6*owSt!wo+Ot4X9SaO72m! zdQwkzSSzj6;&H3f?y%B$;&yT%VRc#Edt%vCVcxs~i*1!M*H(AoLVEqe+q0jJ1_$>BhdvCZKbxCAQ9n0TpV%85J+0SX z`zV+gtBsG=j*kapduy+~Q-5=hF`VO$Z9Y?Q+zQ`e++mpQ9Zy#aMXNc%bX<-}1BIe% zbF)y&4ccav7ae1M^upYwm-TrZLh8$?zH)zghp%oZ5I~HC-Vk&wt691ow>s{KhLqI_ zv2<8#t**smR<{kMtdy3l(S-10+0JA-nYO;QWZm+{x)srS9V!eJlU_1=>4`!`by+ux z6R)znaRPlnv#zzSgKC#G?h)MrH@x0jjXU(%%Nt{H^xa??d(_=>c7>IO!f*1^4~I&7 zELVmac(org4D}DaUJ(Lc%&+WeGt5wJ5elq7ccww&W&XdOGZ-ch{R4KRJEZL5O6FB~IYDLq` z{4>B-Ucuz%kLnE119+Tn6rSu!rsOe{us~dkRS%_D-y&AFptS!OBJ((S`L z{Df&1$_3Xn(^%lBC^TJ{cV4qtpSL$>`CZW-p`d+@+isPY^CZgVFOEtAj}Yy9sCcrY z4oX|4oe#LEyYggID&4d(2&WHF)7&vV&unXD_@HlHq^hz}9_zl~;8@l|kF}ueyKJrp-@m z++aTZ$kzVNTb?wxY~HkStFHk;cG(ix@{=34=WRiqnK&oF@E#P{CFkb+L}YsLMs49d z0*+xpmgdIb2ADxrpiq2m33K@RhGbihDVpva<|Mzd*qayklEKeSG0jrY|_Wq z(vBU^+V-%??K~v0$jk}SWariQ8(F3nnWv+Xy43i%syvd#dl*ohTy7Ua!sXR+KIhus$GAiGqB2$P!L65= zUvs9P3U6rUE7h{=_e88=HfQANzRe#|1lgjv`wS{{ccM1joh`b%>cPXIXSt^cX^?%% zo(gF&lE#PzdyIP;?D4LVgv-2ytH6f)y~LzyDX>R2k`#4yN*=!3k|$$R(unRQt=O}S zZ}8%)hW;!`-UpkuxIBCZgbRpcdL zl*Zcn;nTITPu3gTw{L%{9K3j@HvW=mtTpQAr)#h8s~vwovOwq%(`U~gG=_PlH0&C+ z-=Bshw%S%`lE#vFqjioi^|c_J4JPpGs31%5V=&Uf zFu#H7>QJRn&UCbZh@i_eRFK8^F`@7**q0D549azUxzutZase$i*XdTw@V*o{-G@QP zK%we{IYONZE7M_XJ@`*guduzH*R@&QqHPw@zVIH{Qo!wJs5DIt;9;H&@E9Ws$XpQ) zU>%4WCO8GJJEVZT!!`hd8k;0Q$}2I~0xTjbPk0nxs?`J7{K`Ps47R%e5u?dPMnu>3 zV;|KHzB@ZJ2~8WDHa*s=$q)_oqnGNZUYb_fWzxexUxl=}>i1I2o$si5AM3gOl zdHS>9{9ywQ4uOy{`{`MuEw(Y{f3qh%L6}jH!lR=tjRxCRuQ&eVx%bx&PuKSEGAN0u zjlFJ=KaMu1;IdUq>y6qQ)4{}h;S~{c1QQ35TfyOcF*COG#$U{ggX88eyfJs~q|wi- z_QJ8Zh-j@Xj%|o};vl4J{ABCqJ|p7F=t%rl;Gu8+Yy2KOBLhTbr^;z}LIj$6Xq&?Zt8L|g~z(qIodTxkMi24{M6n@xPfMoXxS=uB7w-Q1HV zVGW2jna6nAm51=3()9zZjAR|L1EN(+Jx&;oRm-gyCOw^fmw04sqBWKbaSJ`oT|OKf zd#fFM>i~?5@$L3*XB`10V+4oxLr1l9AGfkl$Ya68p5T*rXD=SFeY^`~7aV#g7&{T1 zdI|6c+7NHxov;kp%KW|q!N~)p^u>wx+y}d{)&C85k?A+>@{0J5&~!x@{&H=b39VgE z=5d?R-tS<5#4r-=_aOfsVTLr7VPRC--L{#NQTUBCNdR;`&1?i|B8b(52rOUWxoz#( zONkVzvEM$dbTkxt$JOwcJFjhnSL@t7fDrwNStEAs!H9>1%R{6o*i~xDiX@R0Nh31Y zHQmClM#!#Sl(7g_CVpM#AxeteY_{QM;~2RI&NSgEA``j|E;otD49;~@53?xj5Oc8u z--Xr4q~N4)9*Ltj?qW3`M1+RuYzi1hyfz?OU~9yC2`_=RhP@MF??=TN%IuyKy@=b8 zdp(MbY}8UGJ3I}J9AP*d66~GU3?!gVHEeZT1X?VE9DAh6Q2pn?(IuHAGJ;w$&|0Upe>a4 zb~i*=>PmFSe}I$od#E6C3j@0_n9Em&cZe8|e+MI(qyQ+t7xnD2@cp(GIeA|xIfH(@ zY;QAB2~VKYyXk9~>ts`ne9tVncF7M%p3fDlb~|btTeo}}djoHDHlR>7a3v$Iq}V}M z5KV}*ixRH{L+T~ilM=8L;tceTQaXkRgQdm)6jRwwqYd~=B#Dfa@XmD<8M!aR`ufhu z{h6!iqeD4YvQ5+1O|w+7szs`&O!K*FuGkofo2FICo2H1Ie5Fu!`7+v!zB6#5X0A{n zT#hj77&UL9;!Y~=LgA}VS1LvRLu&t++9~3{LcV0X&r~e_5cMy(8BwG3lnd)d#UYg@ zB=RJ$>WovQzMv=pOCDt#OLA!vv+#8DZO9|isp-j+8?Lh#Uc`T_zU$vK?P0g7xBoV$bf`K@DnniZy>G| z-kYR=TQhNMdY^cpTb2&BXlw?M7mI!ZyFsjc6dCL2)B7 z?O|#@f+DkwKSp(OhrR}iE0hOA>mp=^CMIl3XiL0D7@L4gA59^-heouSYLS_>$W`1^ xC^2zwXy`v8>^tPBRVfkcmD7s!EA7733X7A0r9nrbk+H9ss(;y|_r_Mo{tXL9bJ_p^ literal 0 HcmV?d00001 diff --git a/workflows/__pycache__/workflow_baike.cpython-310.pyc b/workflows/__pycache__/workflow_baike.cpython-310.pyc index 9f5cb82df20b4ccf4d16007065a9cdb88256b98e..0e285efd5e8bfb7fc7f238ea95472c3ac964ed5c 100644 GIT binary patch literal 21645 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 OAl7PhJ2YCSjsJgs^)S=` literal 17260 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 literal 46091 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 dq+Dv3PS4N&f`-yHlE?=yq# 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 List[Dict[str, Any]]: def build_history_str( history_messages: List[Dict[str, Any]], - recent_count: int = 10, - assistant_truncate: int = 300, + recent_count: int = DEFAULT_HISTORY_MAX_MESSAGES, + assistant_truncate: int = DEFAULT_ASSISTANT_TRUNCATE, ) -> str: """兼容旧调用的模块级函数""" mgr = HistoryManager(max_messages=recent_count, max_assistant_chars=assistant_truncate) diff --git a/workflows/history_manager.py.bak b/workflows/history_manager.py.bak new file mode 100644 index 0000000..523de8c --- /dev/null +++ b/workflows/history_manager.py.bak @@ -0,0 +1,217 @@ +""" +统一历史对话管理器 +整合所有分散的 parse_history / _parse_history / build_history_str / filter_image_urls 逻辑 +""" + +import json +from typing import Any, Dict, List, Optional + + +class HistoryManager: + """统一的历史对话管理器,提供解析、截断、格式化、过滤等功能""" + + def __init__( + self, + max_messages: int = 10, + max_assistant_chars: int = 300, + ): + """ + Args: + max_messages: 保留的最近消息条数 + max_assistant_chars: assistant 角色内容截断字符数 + """ + self.max_messages = max_messages + self.max_assistant_chars = max_assistant_chars + + # ==================== 解析 ==================== + + def parse(self, history_message: Any) -> List[Dict[str, Any]]: + """ + 解析历史消息为列表,保留最近 max_messages 条。 + 兼容字符串和列表输入,替代所有 parse_history / _parse_history。 + """ + if not history_message: + return [] + try: + data = json.loads(history_message) if isinstance(history_message, str) else history_message + return data[-self.max_messages:] if isinstance(data, list) else [] + except Exception: + return [] + + # ==================== 截断 ==================== + + def truncate(self, messages: List[Dict[str, Any]], keep_recent: Optional[int] = None) -> List[Dict[str, Any]]: + """按数量截断,保留最近 keep_recent 条""" + if not messages: + return [] + n = keep_recent if keep_recent is not None else self.max_messages + return messages[-n:] + + # ==================== 格式化 ==================== + + def to_prompt_str( + self, + messages: List[Dict[str, Any]], + recent_count: Optional[int] = None, + assistant_truncate: Optional[int] = None, + ) -> str: + """ + 格式化为 "用户: xxx\\n助手: xxx" 纯文本字符串,用于 prompt 注入。 + 替代 build_history_str。 + """ + if not messages: + return "" + n = recent_count if recent_count is not None else self.max_messages + trunc = assistant_truncate if assistant_truncate is not None else self.max_assistant_chars + recent = messages[-n:] + parts = [] + for m in recent: + if isinstance(m, dict): + role = m.get("role", "") + content = (m.get("content") or "").strip() + if role == "user": + parts.append(f"用户: {content}") + elif role == "assistant": + parts.append(f"助手: {content[:trunc]}") + return "\n".join(parts) if parts else "" + + def to_openai_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + 格式化为 OpenAI messages 列表,直接传入 LLM。 + 用于百科问答等需要完整消息格式的场景。 + """ + if not messages: + return [] + result = [] + for m in messages: + if isinstance(m, dict) and m.get("role") and m.get("content"): + result.append({"role": m["role"], "content": m["content"]}) + return result + + # ==================== 请求预处理 ==================== + + def preprocess_from_request(self, history_message: Any, exclude_last: bool = True) -> str: + """ + 处理前端传入的历史消息:解析、移除当前用户消息、序列化。 + 对应 app.py 中 history_message 预处理逻辑。 + + Args: + history_message: 前端传入的历史消息,支持 str 或 list + exclude_last: 是否移除最后一条(当前用户消息),默认 True + + Returns: + 处理后的 JSON 字符串 + """ + if not history_message: + return "" + try: + data = json.loads(history_message) if isinstance(history_message, str) else history_message + except (json.JSONDecodeError, TypeError): + return "" + if not isinstance(data, list): + return "" + if exclude_last and data: + data = data[:-1] + return json.dumps(data, ensure_ascii=False) + + # ==================== 状态初始化 ==================== + + def init_history(self, history_message: Any) -> Dict[str, Any]: + """ + 一步完成历史消息解析,返回 history_message(str) + history_messages(list) 双字段。 + 用于各工作流入口初始化 state,消除重复的 parse_history 调用。 + + Args: + history_message: 历史消息,支持 str 或 list + + Returns: + {"history_message": str, "history_messages": list} + """ + parsed = self.parse(history_message) + raw = json.dumps(parsed, ensure_ascii=False) if parsed else "" + return {"history_message": raw, "history_messages": parsed} + + # ==================== 过滤 ==================== + + @staticmethod + def filter_image_urls(history_message: Any) -> Any: + """ + 过滤历史消息中的图片URL,将多模态内容转为纯文本。 + 替代 main_agent.filter_image_urls。 + """ + if not history_message: + return history_message + + try: + history_messages = json.loads(history_message) if isinstance(history_message, str) else history_message + except (json.JSONDecodeError, TypeError): + return history_message + + if not isinstance(history_messages, list): + return history_messages + + filtered_messages = [] + for msg in history_messages: + if not isinstance(msg, dict): + filtered_messages.append(msg) + continue + + new_msg = msg.copy() + content = msg.get("content") + + if isinstance(content, list): + text_parts = [] + for content_item in content: + if isinstance(content_item, dict): + if content_item.get("type") == "text": + text_value = content_item.get("text", "") + if text_value: + text_parts.append(text_value) + elif isinstance(content_item, str): + text_parts.append(content_item) + + if text_parts: + new_msg["content"] = "".join(text_parts) + filtered_messages.append(new_msg) + else: + filtered_messages.append(new_msg) + + return filtered_messages + + +# ==================== 模块级便捷实例 ==================== +# 提供与旧函数签名兼容的模块级函数,方便渐进式迁移 + +_default_manager = HistoryManager() + + +def parse_history(history_message: Any) -> List[Dict[str, Any]]: + """兼容旧调用的模块级函数""" + return _default_manager.parse(history_message) + + +def build_history_str( + history_messages: List[Dict[str, Any]], + recent_count: int = 10, + assistant_truncate: int = 300, +) -> str: + """兼容旧调用的模块级函数""" + mgr = HistoryManager(max_messages=recent_count, max_assistant_chars=assistant_truncate) + return mgr.to_prompt_str(history_messages, recent_count=recent_count, assistant_truncate=assistant_truncate) + + +def filter_image_urls(history_message: Any) -> Any: + """兼容旧调用的模块级函数""" + return HistoryManager.filter_image_urls(history_message) + + +def preprocess_from_request(history_message: Any, exclude_last: bool = True) -> str: + """兼容旧调用的模块级函数""" + return _default_manager.preprocess_from_request(history_message, exclude_last=exclude_last) + + +def init_history(history_message: Any) -> Dict[str, Any]: + """兼容旧调用的模块级函数""" + return _default_manager.init_history(history_message) + + diff --git a/workflows/history_manager.py_0723 b/workflows/history_manager.py_0723 new file mode 100644 index 0000000..88333f6 --- /dev/null +++ b/workflows/history_manager.py_0723 @@ -0,0 +1,233 @@ +""" +统一历史对话管理器 +整合所有分散的 parse_history / _parse_history / build_history_str / filter_image_urls 逻辑 +""" + +import json +from typing import Any, Dict, List, Optional + + +class HistoryManager: + """统一的历史对话管理器,提供解析、截断、格式化、过滤等功能""" + ALLOWED_HISTORY_ROLES = {"user", "assistant"} + + def __init__( + self, + max_messages: int = 10, + max_assistant_chars: int = 300, + ): + """ + Args: + max_messages: 保留的最近消息条数 + max_assistant_chars: assistant 角色内容截断字符数 + """ + self.max_messages = max_messages + self.max_assistant_chars = max_assistant_chars + + # ==================== 解析 ==================== + + def parse(self, history_message: Any) -> List[Dict[str, Any]]: + """ + 解析历史消息为列表,保留最近 max_messages 条。 + 兼容字符串和列表输入,替代所有 parse_history / _parse_history。 + """ + if not history_message: + return [] + try: + data = json.loads(history_message) if isinstance(history_message, str) else history_message + if not isinstance(data, list): + return [] + + messages = [] + for message in data: + if not isinstance(message, dict): + continue + role = message.get("role") + content = message.get("content") + if role in self.ALLOWED_HISTORY_ROLES and content: + messages.append({"role": role, "content": content}) + return messages[-self.max_messages:] + except Exception: + return [] + + # ==================== 截断 ==================== + + def truncate(self, messages: List[Dict[str, Any]], keep_recent: Optional[int] = None) -> List[Dict[str, Any]]: + """按数量截断,保留最近 keep_recent 条""" + if not messages: + return [] + n = keep_recent if keep_recent is not None else self.max_messages + return messages[-n:] + + # ==================== 格式化 ==================== + + def to_prompt_str( + self, + messages: List[Dict[str, Any]], + recent_count: Optional[int] = None, + assistant_truncate: Optional[int] = None, + ) -> str: + """ + 格式化为 "用户: xxx\\n助手: xxx" 纯文本字符串,用于 prompt 注入。 + 替代 build_history_str。 + """ + if not messages: + return "" + n = recent_count if recent_count is not None else self.max_messages + trunc = assistant_truncate if assistant_truncate is not None else self.max_assistant_chars + recent = messages[-n:] + parts = [] + for m in recent: + if isinstance(m, dict): + role = m.get("role", "") + content = (m.get("content") or "").strip() + if role == "user": + parts.append(f"用户: {content}") + elif role == "assistant": + parts.append(f"助手: {content[:trunc]}") + return "\n".join(parts) if parts else "" + + def to_openai_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + 格式化为 OpenAI messages 列表,直接传入 LLM。 + 用于百科问答等需要完整消息格式的场景。 + """ + if not messages: + return [] + result = [] + for m in messages: + if ( + isinstance(m, dict) + and m.get("role") in self.ALLOWED_HISTORY_ROLES + and m.get("content") + ): + result.append({"role": m["role"], "content": m["content"]}) + return result + + # ==================== 请求预处理 ==================== + + def preprocess_from_request(self, history_message: Any, exclude_last: bool = True) -> str: + """ + 处理前端传入的历史消息:解析、移除当前用户消息、序列化。 + 对应 app.py 中 history_message 预处理逻辑。 + + Args: + history_message: 前端传入的历史消息,支持 str 或 list + exclude_last: 是否移除最后一条(当前用户消息),默认 True + + Returns: + 处理后的 JSON 字符串 + """ + if not history_message: + return "" + try: + data = json.loads(history_message) if isinstance(history_message, str) else history_message + except (json.JSONDecodeError, TypeError): + return "" + if not isinstance(data, list): + return "" + if exclude_last and data: + data = data[:-1] + return json.dumps(data, ensure_ascii=False) + + # ==================== 状态初始化 ==================== + + def init_history(self, history_message: Any) -> Dict[str, Any]: + """ + 一步完成历史消息解析,返回 history_message(str) + history_messages(list) 双字段。 + 用于各工作流入口初始化 state,消除重复的 parse_history 调用。 + + Args: + history_message: 历史消息,支持 str 或 list + + Returns: + {"history_message": str, "history_messages": list} + """ + parsed = self.parse(history_message) + raw = json.dumps(parsed, ensure_ascii=False) if parsed else "" + return {"history_message": raw, "history_messages": parsed} + + # ==================== 过滤 ==================== + + @staticmethod + def filter_image_urls(history_message: Any) -> Any: + """ + 过滤历史消息中的图片URL,将多模态内容转为纯文本。 + 替代 main_agent.filter_image_urls。 + """ + if not history_message: + return history_message + + try: + history_messages = json.loads(history_message) if isinstance(history_message, str) else history_message + except (json.JSONDecodeError, TypeError): + return history_message + + if not isinstance(history_messages, list): + return history_messages + + filtered_messages = [] + for msg in history_messages: + if not isinstance(msg, dict): + filtered_messages.append(msg) + continue + + new_msg = msg.copy() + content = msg.get("content") + + if isinstance(content, list): + text_parts = [] + for content_item in content: + if isinstance(content_item, dict): + if content_item.get("type") == "text": + text_value = content_item.get("text", "") + if text_value: + text_parts.append(text_value) + elif isinstance(content_item, str): + text_parts.append(content_item) + + if text_parts: + new_msg["content"] = "".join(text_parts) + filtered_messages.append(new_msg) + else: + filtered_messages.append(new_msg) + + return filtered_messages + + +# ==================== 模块级便捷实例 ==================== +# 提供与旧函数签名兼容的模块级函数,方便渐进式迁移 + +_default_manager = HistoryManager() + + +def parse_history(history_message: Any) -> List[Dict[str, Any]]: + """兼容旧调用的模块级函数""" + return _default_manager.parse(history_message) + + +def build_history_str( + history_messages: List[Dict[str, Any]], + recent_count: int = 10, + assistant_truncate: int = 300, +) -> str: + """兼容旧调用的模块级函数""" + mgr = HistoryManager(max_messages=recent_count, max_assistant_chars=assistant_truncate) + return mgr.to_prompt_str(history_messages, recent_count=recent_count, assistant_truncate=assistant_truncate) + + +def filter_image_urls(history_message: Any) -> Any: + """兼容旧调用的模块级函数""" + return HistoryManager.filter_image_urls(history_message) + + +def preprocess_from_request(history_message: Any, exclude_last: bool = True) -> str: + """兼容旧调用的模块级函数""" + return _default_manager.preprocess_from_request(history_message, exclude_last=exclude_last) + + +def init_history(history_message: Any) -> Dict[str, Any]: + """兼容旧调用的模块级函数""" + return _default_manager.init_history(history_message) + + diff --git a/workflows/workflow_baike.py b/workflows/workflow_baike.py index 891318f..2f5e598 100644 --- a/workflows/workflow_baike.py +++ b/workflows/workflow_baike.py @@ -1,594 +1,560 @@ """ -工作流:普通问答Agent(使用LangGraph) -输入:文本(转化后和原文本query)、图片描述、语音文本 -步骤: -1. 使用RAG和GraphRAG检索相关信息 -2. 生成回答 -使用 OpenaiAPI 方法调用大模型,尽可能使用异步 +百科问答工作流。 + +核心链路借鉴 WeKnora: +1. 意图判断默认偏向检索 +2. 基于历史重写检索 query,并生成多个检索变体 +3. RAG / Graph 并行召回 +4. 多源 evidence 去重、融合排序、rerank +5. 只基于 evidence 生成可追溯回答 """ -import sys -import os - -# 添加项目根目录到Python搜索路径 -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from typing import TypedDict, Optional, Dict, Any, List -from langgraph.graph import StateGraph, START, END -from workflows.history_manager import init_history - -from tools.function_tool import ( - graph_rag_search -) -from tools.rag_tools import rag_search -from modelsAPI.model_api import OpenaiAPI -from utils.function_tracker import track_function_calls, get_current_stream_handler, get_all_callbacks -from utils.image_utils import extract_image_paths, find_closest_image_path, normalize_markdown_images, get_image_prompt_guidance -from prompts import BAIKE_PROMPTS, COMMON_PROMPTS import asyncio import json -import re +import os import random +import re +import sys +from typing import Any, Dict, List, Optional, TypedDict + +from langgraph.graph import END, START, StateGraph + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from modelsAPI.model_api import OpenaiAPI +from prompts import BAIKE_PROMPTS +from tools.function_tool import graph_rag_search +from tools.rag_tools import rag_search +from utils.function_tracker import get_all_callbacks, track_function_calls +from utils.image_utils import extract_image_paths +from workflows.history_manager import DEFAULT_HISTORY_MAX_MESSAGES, init_history +from workflows.workflow_utils import dedupe_rag_results, stream_generate_and_postprocess + + +BAIKE_RAG_TOP_K = 8 +BAIKE_GRAPH_TOP_K = 240 +BAIKE_RESULT_LIMIT = 8 +BAIKE_EVIDENCE_LIMIT = 10 -# =============== 定义状态 =============== class QAState(TypedDict): - """普通问答工作流状态""" - extracted_text: str # 已提取的文本(统一接口,只接收文本) + extracted_text: str combined_query: str - retrieval_query: str # 用于RAG/图谱检索的重写后query - route_flag: str # 路由标识 - history_message: str # 历史消息(JSON格式) - history_messages: List[Dict[str, Any]] # 过滤后的历史消息列表 - need_retrieval: Optional[bool] # 是否需要检索 + retrieval_query: str + retrieval_queries: List[str] + route_flag: str + history_message: str + history_messages: List[Dict[str, Any]] + need_retrieval: Optional[bool] + rag_search_result: Optional[List[Dict[str, Any]]] + graph_search_result: Optional[str] + evidence_items: Optional[List[Dict[str, Any]]] + response: str + actions: List[Dict[str, Any]] + suggestedReplies: List[Dict[str, Any]] + error_message: str - # RAG检索结果 - rag_search_result: Optional[List[Dict[str, Any]]] # RAG检索结果 - graph_search_result: Optional[str] # 图谱检索结果 - - # 最终响应 - response: str # 最终响应 - actions: List[Dict[str, Any]] # 按钮信息列表 [{"type": "...", "label": "...", "icon": "..."}] - suggestedReplies: List[Dict[str, Any]] # 建议回复问题列表 - error_message: str # 错误信息 - - -# =============== 节点函数 =============== def get_baike_history_context(state: QAState) -> Dict[str, Any]: - """ - 知识问答步骤:获取问答类历史信息 - 问答工作流只关注历史问答记录,用于提供上下文 - """ - history_message = state.get("history_message", "") or "" - return init_history(history_message) + return init_history(state.get("history_message", "") or "") async def judge_need_retrieval(state: QAState) -> Dict[str, Any]: - """ - 判断用户问题是否需要检索知识库 - """ original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip() history_messages = state.get("history_messages", []) or [] - prompt = BAIKE_PROMPTS["judge_need_retrieval"].format( - history_messages=history_messages if history_messages else '无', - original_query=original_query + history_messages=history_messages if history_messages else "无", + original_query=original_query, ) - - need_retrieval = True + + substantive_query = _looks_like_substantive_query(original_query) + need_retrieval = substantive_query try: result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None) - if result: - result = result.strip().lower() - if result == "false": - need_retrieval = False - except Exception as e: - print(f"判断是否需要检索失败: {e}") - need_retrieval = True - - + if not substantive_query and result and result.strip().lower() == "false": + need_retrieval = False + except Exception as exc: + print(f"判断是否需要检索失败: {exc}") print(f"是否需要检索: {need_retrieval}") return {"need_retrieval": need_retrieval} +def _looks_like_substantive_query(query: str) -> bool: + text = re.sub(r"\s+", "", (query or "").strip()) + if not text: + return False + if re.fullmatch(r"(你好|您好|hi|hello|在吗|谢谢|多谢|好的|好|嗯|哦|确认|取消|不用了)[。!!?\s]*", text, re.I): + return False + if len(text) >= 4: + return True + return bool(re.search(r"[??]|[A-Za-z0-9]{2,}", text)) + + async def rewrite_query_with_history(state: QAState) -> Dict[str, Any]: - """ - 使用历史上下文对当前问题进行query重写,生成适合RAG/图谱检索的独立查询语句。 - 只让大模型在有限的最近历史中挑选与当前问题语义相关的内容进行融合,避免简单拼接全量历史。 - """ - import json - - # 原始问题:优先使用 combined_query,其次使用 extracted_text original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip() - history_messages = state.get("history_messages", []) or [] - # 控制历史长度:只保留最近的若干条,避免上下文过长 - trimmed_history = history_messages[-8:] if history_messages else [] - - try: - history_str = json.dumps(trimmed_history, ensure_ascii=False) - except Exception: - history_str = str(trimmed_history) - - # 如果没有历史或当前问题过短,就直接跳过重写 - if not original_query: - return {"retrieval_query": original_query} - # 在构造 prompt 前,提取上一轮用户 query(如果有) + trimmed_history = history_messages[-DEFAULT_HISTORY_MAX_MESSAGES:] if history_messages else [] + history_str = _json_dumps(trimmed_history) last_user_query = "" - if trimmed_history: - # 从后往前找最近一条 role == "user" 的消息 - for msg in reversed(trimmed_history): - if msg.get("role") == "user": - last_user_query = msg.get("content", "").strip() - break + for msg in reversed(trimmed_history): + if msg.get("role") == "user": + last_user_query = str(msg.get("content") or "").strip() + break + + if not original_query: + return {"retrieval_query": "", "retrieval_queries": []} + prompt = BAIKE_PROMPTS["rewrite_query_with_history"].format( - last_user_query=last_user_query or '(无)', + last_user_query=last_user_query or "无", history_str=history_str, - original_query=original_query + original_query=original_query, ) rewritten_query = original_query + query_variants: List[str] = [] try: - result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None) - if result: - candidate = result.strip().strip("“”\"'") - # 只在模型返回的内容明显为单行短文本时替换,避免异常输出 - if candidate and len(candidate) <= 200 and "\n" not in candidate: - rewritten_query = candidate - except Exception as e: - print(f"query 重写失败: {e}") - - - title_options = [f"✨正在分析中,请稍等..."] - start_event = { - "type": "function_execution", - "title": random.choice(title_options), - "details": f"正在调取百科数据..." - } - for callback in get_all_callbacks(): - try: - callback(start_event) - except Exception: - pass + raw = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None, json_output=True) + data = _parse_json_object(raw) + if data: + candidate = str(data.get("rewrite_query") or "").strip() + if candidate: + rewritten_query = candidate[:180] + for q in data.get("search_queries") or []: + if isinstance(q, str) and q.strip(): + query_variants.append(q.strip()[:180]) + except Exception as exc: + print(f"query 重写失败: {exc}") + query_variants = _build_search_queries(rewritten_query, original_query, query_variants) + _emit_event("✨正在分析中,请稍等...", "正在生成检索策略...") print("原始检索query:", original_query) - print("重写后检索query:", last_user_query+rewritten_query) - - return {"retrieval_query":last_user_query+rewritten_query} + print("重写后检索query:", rewritten_query) + print("检索query变体:", query_variants) + return {"retrieval_query": rewritten_query, "retrieval_queries": query_variants} @track_function_calls async def search_rag_and_graph(state: QAState) -> Dict[str, Any]: - """ - 进行文本知识检索和图谱知识检索 - """ - kb_id = None - if state.get("route_flag") == "rules": - kb_id = '3' - - # 优先使用重写后的检索query,其次使用 combined_query,最后回退到原始 extracted_text - query = state.get("retrieval_query") or state.get("combined_query") or state.get("extracted_text", "") - if "用户问题:" in query: - query = query.split("用户问题:", 1)[1] - - print("Graph 检索query", query) - extracted_text = state.get("extracted_text", "") - if "用户问题:" in extracted_text: - extracted_text = extracted_text.split("用户问题:", 1)[1] - + kb_id = "3" if state.get("route_flag") == "rules" else None + query = (state.get("retrieval_query") or state.get("combined_query") or state.get("extracted_text") or "").strip() + extracted_text = (state.get("extracted_text") or "").strip() + query_variants = _build_search_queries(query, extracted_text, state.get("retrieval_queries") or []) try: - # 异步并行调用RAG检索和图谱检索 - print(kb_id) - print(33333333333333333333333333333333333333) - - async def _do_rag_search(): - if kb_id: - return await rag_search( - query=extracted_text, - kb_id=kb_id, - top_k=2 - ) - else: - return await rag_search( - query=extracted_text, - top_k=2 - ) - - async def _do_graph_search(): - return await graph_rag_search.ainvoke({ - "query": query, - "top_k": 3 - }) - - rag_result, graph_results = await asyncio.gather( - _do_rag_search(), - _do_graph_search() + rag_task = _search_rag_variants(query_variants[:3], kb_id=kb_id) + graph_task = graph_rag_search.ainvoke({"query": query, "top_k": BAIKE_GRAPH_TOP_K}) + rag_results, graph_results = await asyncio.gather(rag_task, graph_task) + evidence_items = _build_evidence_items( + rag_results=rag_results, + graph_results=graph_results if isinstance(graph_results, str) else "", + query=query, ) - - # 处理 RAG 结果,提取成列表格式 - rag_search_result = [] - if rag_result.get("success", False): - source_citation = rag_result.get("sourceCitation", {}) - for resource, items in source_citation.items(): - for item in items: - rag_search_result.append({ - "text": item.get("text", ""), - "filename": item.get("filename", ""), - "resource": item.get("resource", ""), - "score": item.get("score", 0.0) - }) - - # 只保留前 4 条数据 -# rag_search_result = rag_search_result[:2] - rag_search_result = sorted(rag_search_result,key=lambda x:float(x.get("socre") or 0), reverse=True)[:4] + evidence_items = await _rerank_evidence(query, evidence_items) return { - "rag_search_result": rag_search_result, - "graph_search_result": graph_results if isinstance(graph_results, str) else "" + "rag_search_result": rag_results, + "graph_search_result": graph_results if isinstance(graph_results, str) else "", + "evidence_items": evidence_items, } - except Exception as e: + except Exception as exc: return { "rag_search_result": [], "graph_search_result": "", - "error_message": f"检索失败: {str(e)}" + "evidence_items": [], + "error_message": f"检索失败: {exc}", } - async def generate_suggested_replies_baike(answer: str, question: str) -> List[Dict[str, Any]]: - """ - 生成针对问答结果的建议回复问题 - 使用大模型生成两个相关问题 - """ - prompt = BAIKE_PROMPTS["generate_suggested_replies"].format( - question=question, - answer=answer[:500] - ) + prompt = BAIKE_PROMPTS["generate_suggested_replies"].format(question=question, answer=answer[:500]) try: result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None, json_output=True) - # 尝试解析JSON - # 提取JSON部分 - json_match = re.search(r'\[.*\]', result, re.DOTALL) + json_match = re.search(r"\[.*\]", result or "", re.DOTALL) if json_match: - json_str = json_match.group(0) - suggested_replies = json.loads(json_str) - # 验证格式 - if isinstance(suggested_replies, list) and len(suggested_replies) >= 2: - # 确保每个元素都有title和content - formatted_replies = [] - for reply in suggested_replies[:2]: - if isinstance(reply, dict) and "title" in reply and "content" in reply: - formatted_replies.append({ - "title": str(reply["title"]), - "content": str(reply["content"]) - }) - if len(formatted_replies) == 2: - return formatted_replies - - # 如果解析失败,返回默认问题 - return [ - {"title": "这个设备的具体工作原理是什么?", "content": "这个设备的具体工作原理是什么?"}, - {"title": "还有哪些相关的知识点?", "content": "还有哪些相关的知识点?"} - ] - except Exception as e: - # 如果生成失败,返回默认问题 - return [ - {"title": "这个设备的具体工作原理是什么?", "content": "这个设备的具体工作原理是什么?"}, - {"title": "还有哪些相关的知识点?", "content": "还有哪些相关的知识点?"} - ] + replies = json.loads(json_match.group(0)) + formatted = [ + {"title": str(x["title"]), "content": str(x["content"])} + for x in replies[:2] + if isinstance(x, dict) and x.get("title") and x.get("content") + ] + if len(formatted) == 2: + return formatted + except Exception: + pass + return [ + {"title": "查看相关依据", "content": "这个回答对应的资料依据有哪些?"}, + {"title": "继续展开说明", "content": "请把这个问题再展开说明一下。"}, + ] @track_function_calls async def generate_answer(state: QAState) -> Dict[str, Any]: - """ - 正在生成问答结果 - """ - extracted_text = state.get("combined_query", "") - rag_results = state.get("rag_search_result", []) - graph_results = state.get("graph_search_result", "") - history_messages = state.get("history_messages", []) or [] + question = state.get("combined_query") or state.get("extracted_text") or "" + evidence_items = state.get("evidence_items") or [] need_retrieval = state.get("need_retrieval", True) - print("history_message(qa)", history_messages) - print("是否使用检索结果:", need_retrieval) + history_messages = state.get("history_messages", []) or [] try: - # 1. 处理检索结果(如果有) - rag_ctx_parts: List[str] = [] - file_name_list = [] - all_source_text = "" - original_image_paths = [] - - if need_retrieval: - # 收集所有 RAG 结果文本(只取前 4 条) - for item in sorted(rag_results or [], key=lambda x: float(x.get("score") or 0) if isinstance(x,dict) else 0, reverse=True)[:4]: -# for item in (rag_results or [])[:1]: - print(item) - print(111111111111111111111111111) - if isinstance(item, dict): - text = (item.get("text") or "").strip() - file_name = (item.get("filename") or "").strip() - if text: - rag_ctx_parts.append(text) - if file_name: - file_name_list.append(file_name) - - # 处理图谱结果 - print("123456", graph_results) - if graph_results and "图谱检索完成,但未返回有效数据。" in graph_results: - graph_results = "" - print("654321", graph_results) - - # 从所有结果中提取图片路径 - if graph_results: - all_source_text = graph_results + "\n" + "\n".join(rag_ctx_parts) - else: - all_source_text = "\n".join(rag_ctx_parts) - original_image_paths = extract_image_paths(all_source_text) - print("提取到的原始图片路径:", original_image_paths) - - # 发送事件通知(可选) - title_options = [f"✨已综合 {len(rag_ctx_parts)} 条检索结果", f"✨本次检索策略为综合多源信息"] - start_event = { - "type": "function_execution", - "title": random.choice(title_options), - "details": f"综合了 {len(rag_ctx_parts)} 条检索结果" - } - for callback in get_all_callbacks(): - try: - callback(start_event) - except Exception: - pass - - query_desc = extracted_text if extracted_text else "当前未明确描述问题" - - # 构建系统提示词 - if state.get("route_flag") == "rules": - domain_desc = "舰船修理相关的法规、规范、行业标准及技术文件" - else: - domain_desc = "工业设备的结构原理、维护规程、故障诊断与维修操作" - - # ========== 第一步:专注于生成高质量内容 ========== - # 根据是否有检索结果构建不同的提示词 - if need_retrieval and all_source_text.strip(): - content_system_prompt = BAIKE_PROMPTS["generate_answer_with_retrieval_system"].format(domain_desc=domain_desc) - content_user_content = BAIKE_PROMPTS["generate_answer_with_retrieval_user"].format( - extracted_text=extracted_text, - rag_count=len(rag_ctx_parts), - all_source_text=all_source_text + source_text = "" + original_image_paths: List[str] = [] + if need_retrieval and evidence_items: + source_text = _format_evidence_context(evidence_items) + original_image_paths = extract_image_paths(source_text) + source_counts = _count_sources(evidence_items) + _emit_event( + f"✨已融合 {len(evidence_items)} 条证据", + f"RAG {source_counts.get('rag', 0)} 条", ) - else: - content_system_prompt = BAIKE_PROMPTS["generate_answer_without_retrieval_system"].format(domain_desc=domain_desc) - content_user_content = BAIKE_PROMPTS["generate_answer_without_retrieval_user"].format(extracted_text=extracted_text) - content_messages = [] - if history_messages: - content_messages.extend(history_messages) - content_messages.append({"role": "user", "content": content_user_content}) - - # 第一步:生成内容 - raw_content = await OpenaiAPI.open_api_chat_without_thinking( - model=None, - system_prompt=content_system_prompt, - messages=content_messages, - temperature=0.5 + domain_desc = ( + "舰船维修相关的法规、规范、行业标准及技术文件" + if state.get("route_flag") == "rules" + else "工业设备的结构原理、维护规程、故障诊断与维修操作" ) - # 过滤掉异常标签(如果有) - raw_content = re.sub(r'ynchroneg>.*?ost switching>', '', raw_content, flags=re.DOTALL) - raw_content = raw_content.strip() if raw_content else "抱歉,无法生成回答。" + if need_retrieval and source_text.strip(): + system_prompt = BAIKE_PROMPTS["generate_answer_with_retrieval_system"].format(domain_desc=domain_desc) + user_prompt = BAIKE_PROMPTS["generate_answer_with_retrieval_user"].format( + extracted_text=question, + rag_count=len(evidence_items), + all_source_text=source_text, + ) + else: + system_prompt = BAIKE_PROMPTS["generate_answer_without_retrieval_system"].format(domain_desc=domain_desc) + user_prompt = BAIKE_PROMPTS["generate_answer_without_retrieval_user"].format(extracted_text=question) - title_options = ["🤖 诊断分析中", "🤖 故障预检中"] - start_event = { - "type": "function_execution", - "title": random.choice(title_options), - "details": raw_content[:30] + "..." - } - for callback in get_all_callbacks(): + messages = [] + if history_messages: + messages.extend(history_messages) + messages.append({"role": "user", "content": user_prompt}) + + _emit_event(random.choice(["🔍 证据整理中", "🧠 正在生成回答"]), "正在基于检索资料生成回答...") + answer = await stream_generate_and_postprocess( + system_prompt=system_prompt, + messages=messages, + original_image_paths=original_image_paths, + temperature=0.2, + fallback="抱歉,无法生成回答。", + ) + suggested_replies = await generate_suggested_replies_baike(answer, question) + return {"response": answer, "actions": [], "result_tag": "qa", "suggestedReplies": suggested_replies} + except Exception as exc: + return {"response": f"生成回答失败:{exc}", "actions": [], "suggestedReplies": []} + + +async def _search_rag_variants(queries: List[str], kb_id: Optional[str] = None) -> List[Dict[str, Any]]: + tasks = [rag_search(query=q, kb_id=kb_id, top_k=BAIKE_RAG_TOP_K) for q in queries if q.strip()] + results = await asyncio.gather(*tasks, return_exceptions=True) if tasks else [] + items: List[Dict[str, Any]] = [] + for result in results: + if isinstance(result, Exception) or not result.get("success"): + continue + for resource, rows in (result.get("sourceCitation") or {}).items(): + for row in rows: + item = dict(row) + item.setdefault("resource", resource) + items.append(item) + items = sorted(items, key=lambda x: float(x.get("score") or 0), reverse=True) + return dedupe_rag_results(items, limit=BAIKE_RESULT_LIMIT) + + +def _build_evidence_items( + rag_results: List[Dict[str, Any]], + graph_results: str, + query: str, +) -> List[Dict[str, Any]]: + candidates: List[Dict[str, Any]] = [] + for item in rag_results or []: + candidates.append({ + "source_type": "rag", + "title": item.get("filename") or item.get("resource") or "RAG 检索结果", + "filename": item.get("filename") or item.get("resource") or "", + "text": item.get("text") or "", + "score": float(item.get("score") or 0), + "file_id": str(item.get("file_id") or ""), + "slice_id": str(item.get("id") or item.get("slice_id") or ""), + "metadata": item, + }) + + graph_text = str(graph_results or "").strip() + if graph_text and "图谱检索完成,但未返回有效数据。" not in graph_text and graph_text != "查询无结果": + candidates.append({ + "source_type": "graph", + "title": "图谱检索结果", + "filename": "", + "text": graph_text, + "score": 0.45, + "file_id": "", + "slice_id": "", + "metadata": {}, + }) + + deduped: Dict[str, Dict[str, Any]] = {} + for item in candidates: + text = _clean_text(item.get("text") or "") + if not text: + continue + item["text"] = text[:5000] + key = item.get("slice_id") or _content_key(text) + local_score = _local_evidence_score(query, item) + item["local_score"] = local_score + existing = deduped.get(key) + if not existing or local_score > float(existing.get("local_score") or 0): + deduped[key] = item + return sorted(deduped.values(), key=lambda x: float(x.get("local_score") or 0), reverse=True) + + +async def _rerank_evidence(query: str, evidence_items: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + if not query.strip() or not evidence_items: + return evidence_items[:BAIKE_EVIDENCE_LIMIT] + selected = evidence_items[:24] + documents = [ + "\n".join([ + str(item.get("title") or ""), + str(item.get("filename") or ""), + str(item.get("text") or ""), + ])[:4000] + for item in selected + ] + rows = await OpenaiAPI.rerank(query=query, documents=documents, top_k=BAIKE_EVIDENCE_LIMIT) + if not rows: + return evidence_items[:BAIKE_EVIDENCE_LIMIT] + + ranked: List[Dict[str, Any]] = [] + seen = set() + for row in rows: + idx = int(row.get("index") or 0) + if idx < 0 or idx >= len(selected) or idx in seen: + continue + seen.add(idx) + item = dict(selected[idx]) + item["rerank_score"] = float(row.get("relevance_score") or 0.0) + ranked.append(item) + for idx, item in enumerate(selected): + if idx not in seen and len(ranked) < BAIKE_EVIDENCE_LIMIT: + ranked.append(item) + return ranked[:BAIKE_EVIDENCE_LIMIT] + + +def _format_evidence_context(items: List[Dict[str, Any]]) -> str: + parts = [] + label_map = {"rag": "RAG", "graph": "Graph"} + for idx, item in enumerate(items[:BAIKE_EVIDENCE_LIMIT], start=1): + source = label_map.get(item.get("source_type"), item.get("source_type") or "unknown") + score = item.get("rerank_score", item.get("local_score", item.get("score", ""))) + meta_lines = [ + f"[Evidence {idx}] source={source} score={score}", + f"Title: {item.get('title') or ''}", + ] + if item.get("filename"): + meta_lines.append(f"File: {item.get('filename')}") + if item.get("file_id"): + meta_lines.append(f"File ID: {item.get('file_id')}") + if item.get("slice_id"): + meta_lines.append(f"Slice ID: {item.get('slice_id')}") + meta_lines.append("Content:") + meta_lines.append(str(item.get("text") or "")) + parts.append("\n".join(meta_lines)) + return "\n\n---\n\n".join(parts) + + +def _local_evidence_score(query: str, item: Dict[str, Any]) -> float: + base = {"rag": 1.0, "graph": 0.65}.get(item.get("source_type"), 0.8) + raw = float(item.get("score") or 0) + score = base + min(max(raw, 0.0), 1.0) + text = " ".join([ + str(item.get("title") or ""), + str(item.get("filename") or ""), + str(item.get("text") or "")[:1500], + ]).lower() + title = str(item.get("title") or "").lower() + for term in _extract_search_terms(query): + t = term.lower() + if not t: + continue + if t in title: + score += 0.7 + elif t in text: + score += 0.25 + return score + + +def _build_search_queries(main_query: str, original_query: str = "", extra_queries: Optional[List[str]] = None) -> List[str]: + # search_queries 是模型生成的最终检索列表,优先保持其顺序。 + candidates: List[str] = list(extra_queries or []) + main_query = _strip_query_noise(main_query or original_query) + if main_query: + main_key = _query_dedupe_key(main_query) + if not any(_query_dedupe_key(text) == main_key for text in candidates): + # 最多保留两个前置问题,最后一条固定为原始综合问题。 + candidates = candidates[:2] + [main_query] + + result: List[str] = [] + seen = set() + for text in candidates: + text = _strip_query_noise(text) + if not text: + continue + key = _query_dedupe_key(text) + if not key or key in seen: + continue + seen.add(key) + result.append(text) + return result[:3] + + +def _query_dedupe_key(text: str) -> str: + """忽略空白和句末标点,对相同检索问题去重。""" + text = re.sub(r"\s+", "", str(text or "")).lower() + return re.sub(r"[??。!!;;,,、]+$", "", text) + + +def _extract_search_terms(query: str) -> List[str]: + stop = {"什么", "如何", "怎么", "哪些", "主要", "内容", "相关", "信息", "一下", "这个", "那个", "是否", "有没有"} + terms = [] + for token in re.findall(r"[\u4e00-\u9fff]{2,12}|[A-Za-z0-9][A-Za-z0-9_\-]{1,30}", query or ""): + token = token.strip("::,,。.;;、()()[]【】") + if len(token) < 2 or token in stop or re.fullmatch(r"\d+", token): + continue + terms.append(token) + return _dedupe_strings(terms) + + +def _strip_query_noise(text: str) -> str: + text = str(text or "").strip().strip('"“”') + if "用户问题:" in text: + text = text.split("用户问题:", 1)[1].strip() + return re.sub(r"\s+", " ", text) + + +def _parse_json_object(raw: str) -> Dict[str, Any]: + if not raw: + return {} + try: + return json.loads(raw) + except Exception: + match = re.search(r"\{.*\}", raw, re.DOTALL) + if match: try: - callback(start_event) + return json.loads(match.group(0)) except Exception: - pass + return {} + return {} - # ========== 第二步:专注于规范格式 ========== - # 构建第二步的提示词:只关注格式规范,不修改内容 - format_system_prompt = COMMON_PROMPTS["format_system"] - - format_user_content = COMMON_PROMPTS["format_user_with_image_examples"].format(raw_content=raw_content) +def _json_dumps(value: Any) -> str: + try: + return json.dumps(value, ensure_ascii=False) + except Exception: + return str(value) - # 第二步:规范格式(流式输出) - stream_handler = get_current_stream_handler() - answer = "" - accumulated_content = "" # 用于逐步发送给前端的内容 - - # 使用 async for 遍历流式生成器 - async for chunk in OpenaiAPI.open_api_chat_stream( - model=None, - system_prompt=format_system_prompt, - messages=[{"role": "user", "content": format_user_content}], - temperature=0.1 - ): - answer += chunk - accumulated_content += chunk - # 每收到新的 chunk 就发送一次更新 - if stream_handler: - stream_handler.send_stream_content(accumulated_content) - answer = answer.strip() if answer else raw_content +def _content_key(text: str) -> str: + return re.sub(r"\s+", "", text or "")[:220] - # LaTeX 公式空格规范化 - def normalize_latex_spacing(text: str) -> str: - placeholder = "\x00DOUBLE_DOLLAR\x00" - text = text.replace("$$", placeholder) - text = re.sub(r'(? str: + return re.sub(r"\n{3,}", "\n\n", str(text or "")).strip() - # 使用增强的图片路径匹配与规范化(仅在有检索结果时) - if need_retrieval and original_image_paths: - answer = normalize_markdown_images(answer, original_paths=original_image_paths) - print("llm output", answer) - # 生成建议回复问题 - suggested_replies = await generate_suggested_replies_baike(answer, extracted_text) +def _dedupe_strings(values: List[str]) -> List[str]: + out = [] + seen = set() + for value in values: + value = str(value or "").strip() + if not value or value in seen: + continue + seen.add(value) + out.append(value) + return out + + +def _count_sources(items: List[Dict[str, Any]]) -> Dict[str, int]: + counts: Dict[str, int] = {} + for item in items: + key = str(item.get("source_type") or "unknown") + counts[key] = counts.get(key, 0) + 1 + return counts + + +def _emit_event(title: str, details: str) -> None: + event = {"type": "function_execution", "title": title, "details": details} + for callback in get_all_callbacks(): + try: + callback(event) + except Exception: + pass + - return { - "response": answer, - "actions": [], - "result_tag": "qa", - "suggestedReplies": suggested_replies - } - except Exception as e: - return { - "response": f"生成回答失败:{str(e)}", - "actions": [], - "suggestedReplies": [] - } def route_after_judge(state: QAState) -> str: - """ - 判断后的路由:根据need_retrieval决定下一步 - """ - need_retrieval = state.get("need_retrieval", True) - if need_retrieval: - return "rewrite_query_with_history" - else: - return "generate_answer" + return "rewrite_query_with_history" if state.get("need_retrieval", True) else "generate_answer" -# =============== 构建工作流 =============== def create_baike_workflow(): - """创建普通问答工作流""" workflow = StateGraph(QAState) - - # 添加节点 workflow.add_node("get_baike_history_context", get_baike_history_context) workflow.add_node("judge_need_retrieval", judge_need_retrieval) workflow.add_node("rewrite_query_with_history", rewrite_query_with_history) workflow.add_node("search_rag_and_graph", search_rag_and_graph) workflow.add_node("generate_answer", generate_answer) - # 设置边 workflow.add_edge(START, "get_baike_history_context") workflow.add_edge("get_baike_history_context", "judge_need_retrieval") workflow.add_conditional_edges( "judge_need_retrieval", route_after_judge, - { - "rewrite_query_with_history": "rewrite_query_with_history", - "generate_answer": "generate_answer" - } + {"rewrite_query_with_history": "rewrite_query_with_history", "generate_answer": "generate_answer"}, ) workflow.add_edge("rewrite_query_with_history", "search_rag_and_graph") workflow.add_edge("search_rag_and_graph", "generate_answer") workflow.add_edge("generate_answer", END) - - # 编译工作流 return workflow.compile() -# =============== 工作流执行函数 =============== async def run_baike_workflow( extracted_text: str, combined_query: str, history_message: str = "", - route_flag: str = "" + route_flag: str = "", ) -> Dict[str, Any]: - """ - 执行普通问答工作流(统一接口) - - Args: - extracted_text: 文本描述(统一的输入参数) - history_message: 历史消息(目前只接收,不做处理) - - Returns: - 工作流执行结果 - """ - # 创建并编译工作流 app = create_baike_workflow() - - # 初始化状态(统一接口,只使用extracted_text和history_message) initial_state = { "extracted_text": extracted_text, "combined_query": combined_query, "retrieval_query": "", + "retrieval_queries": [], "route_flag": route_flag, "history_message": history_message, "history_messages": [], "need_retrieval": None, "rag_search_result": None, "graph_search_result": None, + "evidence_items": None, "response": "", "actions": [], "suggestedReplies": [], - "error_message": "" + "error_message": "", } - + try: - # 执行工作流(使用异步方式) final_state = await app.ainvoke(initial_state) - - # 检查是否有错误 if final_state.get("error_message"): return { "response": f"普通问答工作流执行失败: {final_state['error_message']}", "actions": [], "result_tag": "qa", - "suggestedReplies": [] + "suggestedReplies": [], } - - # 统一输出格式:完全透传 generate_answer 的结果 return { "response": final_state.get("response", ""), "actions": final_state.get("actions", []), "result_tag": "qa", - "suggestedReplies": final_state.get("suggestedReplies", []) + "suggestedReplies": final_state.get("suggestedReplies", []), } - - except Exception as e: + except Exception as exc: return { - "response": f"普通问答工作流执行失败: {str(e)}", + "response": f"普通问答工作流执行失败: {exc}", "actions": [], "result_tag": "qa", - "suggestedReplies": [] + "suggestedReplies": [], } - - -# =============== 测试 =============== -if __name__ == "__main__": - import asyncio - - # 测试用例 - test_cases = [ - "船舰主发动机的安全警告是什么", - ] - - async def test(): - for i, test_text in enumerate(test_cases, 1): - print(f"\n{'='*60}") - print(f"测试用例 {i}") - print(f"{'='*60}") - - # ✅ 修复:传入 combined_query 参数 - result = await run_baike_workflow( - extracted_text=test_text, - combined_query=test_text - ) - - print(f"\n执行结果:") - print(f"成功:{result.get('success', False)}") - print(f"\n响应内容:") - print(result.get("response", "无响应")) - - asyncio.run(test()) - - - - - diff --git a/workflows/workflow_baike.py_0721 b/workflows/workflow_baike.py_0721 new file mode 100644 index 0000000..07a8f08 --- /dev/null +++ b/workflows/workflow_baike.py_0721 @@ -0,0 +1,597 @@ +""" +工作流:普通问答Agent(使用LangGraph) +输入:文本(转化后和原文本query)、图片描述、语音文本 +步骤: +1. 使用RAG和GraphRAG检索相关信息 +2. 生成回答 +使用 OpenaiAPI 方法调用大模型,尽可能使用异步 +""" +import sys +import os + +# 添加项目根目录到Python搜索路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from typing import TypedDict, Optional, Dict, Any, List +from langgraph.graph import StateGraph, START, END +from workflows.history_manager import init_history + +from tools.function_tool import ( + graph_rag_search +) +from tools.rag_tools import rag_search +from modelsAPI.model_api import OpenaiAPI +from utils.function_tracker import track_function_calls, get_current_stream_handler, get_all_callbacks +from utils.image_utils import extract_image_paths, find_closest_image_path, normalize_markdown_images, get_image_prompt_guidance +from prompts import BAIKE_PROMPTS, COMMON_PROMPTS +import asyncio +import json +import re +import random + + +# =============== 定义状态 =============== +class QAState(TypedDict): + """普通问答工作流状态""" + extracted_text: str # 已提取的文本(统一接口,只接收文本) + combined_query: str + retrieval_query: str # 用于RAG/图谱检索的重写后query + route_flag: str # 路由标识 + history_message: str # 历史消息(JSON格式) + history_messages: List[Dict[str, Any]] # 过滤后的历史消息列表 + need_retrieval: Optional[bool] # 是否需要检索 + + # RAG检索结果 + rag_search_result: Optional[List[Dict[str, Any]]] # RAG检索结果 + graph_search_result: Optional[str] # 图谱检索结果 + + # 最终响应 + response: str # 最终响应 + actions: List[Dict[str, Any]] # 按钮信息列表 [{"type": "...", "label": "...", "icon": "..."}] + suggestedReplies: List[Dict[str, Any]] # 建议回复问题列表 + error_message: str # 错误信息 + +# =============== 辅助函数 =============== + + + +# =============== 节点函数 =============== + +def get_baike_history_context(state: QAState) -> Dict[str, Any]: + """ + 知识问答步骤:获取问答类历史信息 + 问答工作流只关注历史问答记录,用于提供上下文 + """ + history_message = state.get("history_message", "") or "" + return init_history(history_message) + + +async def judge_need_retrieval(state: QAState) -> Dict[str, Any]: + """ + 判断用户问题是否需要检索知识库 + """ + original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip() + history_messages = state.get("history_messages", []) or [] + + prompt = BAIKE_PROMPTS["judge_need_retrieval"].format( + history_messages=history_messages if history_messages else '无', + original_query=original_query + ) + + need_retrieval = True + try: + result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None) + if result: + result = result.strip().lower() + if result == "false": + need_retrieval = False + except Exception as e: + print(f"判断是否需要检索失败: {e}") + need_retrieval = True + + + + print(f"是否需要检索: {need_retrieval}") + return {"need_retrieval": need_retrieval} + + +async def rewrite_query_with_history(state: QAState) -> Dict[str, Any]: + """ + 使用历史上下文对当前问题进行query重写,生成适合RAG/图谱检索的独立查询语句。 + 只让大模型在有限的最近历史中挑选与当前问题语义相关的内容进行融合,避免简单拼接全量历史。 + """ + import json + + # 原始问题:优先使用 combined_query,其次使用 extracted_text + original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip() + + history_messages = state.get("history_messages", []) or [] + # 控制历史长度:只保留最近的若干条,避免上下文过长 + trimmed_history = history_messages[-8:] if history_messages else [] + + try: + history_str = json.dumps(trimmed_history, ensure_ascii=False) + except Exception: + history_str = str(trimmed_history) + + # 如果没有历史或当前问题过短,就直接跳过重写 + if not original_query: + return {"retrieval_query": original_query} + # 在构造 prompt 前,提取上一轮用户 query(如果有) + last_user_query = "" + if trimmed_history: + # 从后往前找最近一条 role == "user" 的消息 + for msg in reversed(trimmed_history): + if msg.get("role") == "user": + last_user_query = msg.get("content", "").strip() + break + prompt = BAIKE_PROMPTS["rewrite_query_with_history"].format( + last_user_query=last_user_query or '(无)', + history_str=history_str, + original_query=original_query + ) + + rewritten_query = original_query + try: + result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None) + if result: + candidate = result.strip().strip("“”\"'") + # 只在模型返回的内容明显为单行短文本时替换,避免异常输出 + if candidate and len(candidate) <= 200 and "\n" not in candidate: + rewritten_query = candidate + except Exception as e: + print(f"query 重写失败: {e}") + + + title_options = [f"✨正在分析中,请稍等..."] + start_event = { + "type": "function_execution", + "title": random.choice(title_options), + "details": f"正在调取百科数据..." + } + for callback in get_all_callbacks(): + try: + callback(start_event) + except Exception: + pass + + print("原始检索query:", original_query) + print("重写后检索query:", last_user_query+rewritten_query) + + return {"retrieval_query":last_user_query+rewritten_query} + + +@track_function_calls +async def search_rag_and_graph(state: QAState) -> Dict[str, Any]: + """ + 进行文本知识检索和图谱知识检索 + """ + kb_id = None + if state.get("route_flag") == "rules": + kb_id = '3' + + # 优先使用重写后的检索query,其次使用 combined_query,最后回退到原始 extracted_text + query = state.get("retrieval_query") or state.get("combined_query") or state.get("extracted_text", "") + if "用户问题:" in query: + query = query.split("用户问题:", 1)[1] + + print("Graph 检索query", query) + extracted_text = state.get("extracted_text", "") + if "用户问题:" in extracted_text: + extracted_text = extracted_text.split("用户问题:", 1)[1] + + try: + # 异步并行调用RAG检索和图谱检索 + print(kb_id) + print(33333333333333333333333333333333333333) + + async def _do_rag_search(): + if kb_id: + return await rag_search( + query=extracted_text, + kb_id=kb_id, + top_k=2 + ) + else: + return await rag_search( + query=extracted_text, + top_k=2 + ) + + async def _do_graph_search(): + return await graph_rag_search.ainvoke({ + "query": query, + "top_k": 3 + }) + + rag_result, graph_results = await asyncio.gather( + _do_rag_search(), + _do_graph_search() + ) + + # 处理 RAG 结果,提取成列表格式 + rag_search_result = [] + if rag_result.get("success", False): + source_citation = rag_result.get("sourceCitation", {}) + for resource, items in source_citation.items(): + for item in items: + rag_search_result.append({ + "text": item.get("text", ""), + "filename": item.get("filename", ""), + "resource": item.get("resource", ""), + "score": item.get("score", 0.0) + }) + + # 只保留前 4 条数据 +# rag_search_result = rag_search_result[:2] + rag_search_result = sorted(rag_search_result,key=lambda x:float(x.get("socre") or 0), reverse=True)[:4] + + return { + "rag_search_result": rag_search_result, + "graph_search_result": graph_results if isinstance(graph_results, str) else "" + } + except Exception as e: + return { + "rag_search_result": [], + "graph_search_result": "", + "error_message": f"检索失败: {str(e)}" + } + + + +async def generate_suggested_replies_baike(answer: str, question: str) -> List[Dict[str, Any]]: + """ + 生成针对问答结果的建议回复问题 + 使用大模型生成两个相关问题 + """ + prompt = BAIKE_PROMPTS["generate_suggested_replies"].format( + question=question, + answer=answer[:500] + ) + try: + result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None, json_output=True) + # 尝试解析JSON + # 提取JSON部分 + json_match = re.search(r'\[.*\]', result, re.DOTALL) + if json_match: + json_str = json_match.group(0) + suggested_replies = json.loads(json_str) + # 验证格式 + if isinstance(suggested_replies, list) and len(suggested_replies) >= 2: + # 确保每个元素都有title和content + formatted_replies = [] + for reply in suggested_replies[:2]: + if isinstance(reply, dict) and "title" in reply and "content" in reply: + formatted_replies.append({ + "title": str(reply["title"]), + "content": str(reply["content"]) + }) + if len(formatted_replies) == 2: + return formatted_replies + + # 如果解析失败,返回默认问题 + return [ + {"title": "这个设备的具体工作原理是什么?", "content": "这个设备的具体工作原理是什么?"}, + {"title": "还有哪些相关的知识点?", "content": "还有哪些相关的知识点?"} + ] + except Exception as e: + # 如果生成失败,返回默认问题 + return [ + {"title": "这个设备的具体工作原理是什么?", "content": "这个设备的具体工作原理是什么?"}, + {"title": "还有哪些相关的知识点?", "content": "还有哪些相关的知识点?"} + ] + + +@track_function_calls +async def generate_answer(state: QAState) -> Dict[str, Any]: + """ + 正在生成问答结果 + """ + extracted_text = state.get("combined_query", "") + rag_results = state.get("rag_search_result", []) + graph_results = state.get("graph_search_result", "") + history_messages = state.get("history_messages", []) or [] + need_retrieval = state.get("need_retrieval", True) + print("history_message(qa)", history_messages) + print("是否使用检索结果:", need_retrieval) + + try: + # 1. 处理检索结果(如果有) + rag_ctx_parts: List[str] = [] + file_name_list = [] + all_source_text = "" + original_image_paths = [] + + if need_retrieval: + # 收集所有 RAG 结果文本(只取前 4 条) + for item in sorted(rag_results or [], key=lambda x: float(x.get("score") or 0) if isinstance(x,dict) else 0, reverse=True)[:4]: +# for item in (rag_results or [])[:1]: + print(item) + print(111111111111111111111111111) + if isinstance(item, dict): + text = (item.get("text") or "").strip() + file_name = (item.get("filename") or "").strip() + if text: + rag_ctx_parts.append(text) + if file_name: + file_name_list.append(file_name) + + # 处理图谱结果 + print("123456", graph_results) + if graph_results and "图谱检索完成,但未返回有效数据。" in graph_results: + graph_results = "" + print("654321", graph_results) + + # 从所有结果中提取图片路径 + if graph_results: + all_source_text = graph_results + "\n" + "\n".join(rag_ctx_parts) + else: + all_source_text = "\n".join(rag_ctx_parts) + original_image_paths = extract_image_paths(all_source_text) + print("提取到的原始图片路径:", original_image_paths) + + # 发送事件通知(可选) + title_options = [f"✨已综合 {len(rag_ctx_parts)} 条检索结果", f"✨本次检索策略为综合多源信息"] + start_event = { + "type": "function_execution", + "title": random.choice(title_options), + "details": f"综合了 {len(rag_ctx_parts)} 条检索结果" + } + for callback in get_all_callbacks(): + try: + callback(start_event) + except Exception: + pass + + query_desc = extracted_text if extracted_text else "当前未明确描述问题" + + # 构建系统提示词 + if state.get("route_flag") == "rules": + domain_desc = "舰船修理相关的法规、规范、行业标准及技术文件" + else: + domain_desc = "工业设备的结构原理、维护规程、故障诊断与维修操作" + + # ========== 第一步:专注于生成高质量内容 ========== + # 根据是否有检索结果构建不同的提示词 + if need_retrieval and all_source_text.strip(): + content_system_prompt = BAIKE_PROMPTS["generate_answer_with_retrieval_system"].format(domain_desc=domain_desc) + content_user_content = BAIKE_PROMPTS["generate_answer_with_retrieval_user"].format( + extracted_text=extracted_text, + rag_count=len(rag_ctx_parts), + all_source_text=all_source_text + ) + else: + content_system_prompt = BAIKE_PROMPTS["generate_answer_without_retrieval_system"].format(domain_desc=domain_desc) + content_user_content = BAIKE_PROMPTS["generate_answer_without_retrieval_user"].format(extracted_text=extracted_text) + + content_messages = [] + if history_messages: + content_messages.extend(history_messages) + content_messages.append({"role": "user", "content": content_user_content}) + + # 第一步:生成内容 + raw_content = await OpenaiAPI.open_api_chat_without_thinking( + model=None, + system_prompt=content_system_prompt, + messages=content_messages, + temperature=0.5 + ) + + # 过滤掉异常标签(如果有) + raw_content = re.sub(r'ynchroneg>.*?ost switching>', '', raw_content, flags=re.DOTALL) + raw_content = raw_content.strip() if raw_content else "抱歉,无法生成回答。" + + title_options = ["🤖 诊断分析中", "🤖 故障预检中"] + start_event = { + "type": "function_execution", + "title": random.choice(title_options), + "details": raw_content[:30] + "..." + } + for callback in get_all_callbacks(): + try: + callback(start_event) + except Exception: + pass + + + # ========== 第二步:专注于规范格式 ========== + # 构建第二步的提示词:只关注格式规范,不修改内容 + format_system_prompt = COMMON_PROMPTS["format_system"] + + format_user_content = COMMON_PROMPTS["format_user_with_image_examples"].format(raw_content=raw_content) + + # 第二步:规范格式(流式输出) + stream_handler = get_current_stream_handler() + answer = "" + accumulated_content = "" # 用于逐步发送给前端的内容 + + # 使用 async for 遍历流式生成器 + async for chunk in OpenaiAPI.open_api_chat_stream( + model=None, + system_prompt=format_system_prompt, + messages=[{"role": "user", "content": format_user_content}], + temperature=0.1 + ): + answer += chunk + accumulated_content += chunk + # 每收到新的 chunk 就发送一次更新 + if stream_handler: + stream_handler.send_stream_content(accumulated_content) + + answer = answer.strip() if answer else raw_content + + # LaTeX 公式空格规范化 + def normalize_latex_spacing(text: str) -> str: + placeholder = "\x00DOUBLE_DOLLAR\x00" + text = text.replace("$$", placeholder) + text = re.sub(r'(? str: + """ + 判断后的路由:根据need_retrieval决定下一步 + """ + need_retrieval = state.get("need_retrieval", True) + if need_retrieval: + return "rewrite_query_with_history" + else: + return "generate_answer" + + +# =============== 构建工作流 =============== +def create_baike_workflow(): + """创建普通问答工作流""" + workflow = StateGraph(QAState) + + # 添加节点 + workflow.add_node("get_baike_history_context", get_baike_history_context) + workflow.add_node("judge_need_retrieval", judge_need_retrieval) + workflow.add_node("rewrite_query_with_history", rewrite_query_with_history) + workflow.add_node("search_rag_and_graph", search_rag_and_graph) + workflow.add_node("generate_answer", generate_answer) + + # 设置边 + workflow.add_edge(START, "get_baike_history_context") + workflow.add_edge("get_baike_history_context", "judge_need_retrieval") + workflow.add_conditional_edges( + "judge_need_retrieval", + route_after_judge, + { + "rewrite_query_with_history": "rewrite_query_with_history", + "generate_answer": "generate_answer" + } + ) + workflow.add_edge("rewrite_query_with_history", "search_rag_and_graph") + workflow.add_edge("search_rag_and_graph", "generate_answer") + workflow.add_edge("generate_answer", END) + + # 编译工作流 + return workflow.compile() + + +# =============== 工作流执行函数 =============== +async def run_baike_workflow( + extracted_text: str, + combined_query: str, + history_message: str = "", + route_flag: str = "" +) -> Dict[str, Any]: + """ + 执行普通问答工作流(统一接口) + + Args: + extracted_text: 文本描述(统一的输入参数) + history_message: 历史消息(目前只接收,不做处理) + + Returns: + 工作流执行结果 + """ + # 创建并编译工作流 + app = create_baike_workflow() + + # 初始化状态(统一接口,只使用extracted_text和history_message) + initial_state = { + "extracted_text": extracted_text, + "combined_query": combined_query, + "retrieval_query": "", + "route_flag": route_flag, + "history_message": history_message, + "history_messages": [], + "need_retrieval": None, + "rag_search_result": None, + "graph_search_result": None, + "response": "", + "actions": [], + "suggestedReplies": [], + "error_message": "" + } + + try: + # 执行工作流(使用异步方式) + final_state = await app.ainvoke(initial_state) + + # 检查是否有错误 + if final_state.get("error_message"): + return { + "response": f"普通问答工作流执行失败: {final_state['error_message']}", + "actions": [], + "result_tag": "qa", + "suggestedReplies": [] + } + + # 统一输出格式:完全透传 generate_answer 的结果 + return { + "response": final_state.get("response", ""), + "actions": final_state.get("actions", []), + "result_tag": "qa", + "suggestedReplies": final_state.get("suggestedReplies", []) + } + + except Exception as e: + return { + "response": f"普通问答工作流执行失败: {str(e)}", + "actions": [], + "result_tag": "qa", + "suggestedReplies": [] + } + + + +# =============== 测试 =============== +if __name__ == "__main__": + import asyncio + + # 测试用例 + test_cases = [ + "船舰主发动机的安全警告是什么", + ] + + async def test(): + for i, test_text in enumerate(test_cases, 1): + print(f"\n{'='*60}") + print(f"测试用例 {i}") + print(f"{'='*60}") + + # ✅ 修复:传入 combined_query 参数 + result = await run_baike_workflow( + extracted_text=test_text, + combined_query=test_text + ) + + print(f"\n执行结果:") + print(f"成功:{result.get('success', False)}") + print(f"\n响应内容:") + print(result.get("response", "无响应")) + + asyncio.run(test()) + + + + + diff --git a/workflows/workflow_fault_diagnosis.py b/workflows/workflow_fault_diagnosis.py deleted file mode 100644 index b9c3725..0000000 --- a/workflows/workflow_fault_diagnosis.py +++ /dev/null @@ -1,1651 +0,0 @@ -""" -故障诊断工作流 checkpointer 版本 -""" -from typing import TypedDict, Optional, Dict, Any, List -from langgraph.graph import StateGraph, START, END -from langgraph.types import interrupt, Command -from tools.function_tool import graph_rag_search -from tools.rag_tools import rag_search -from tools.graph_tools import atlas_retrieval, format_atlas_results, extract_entity_names_from_history -from modelsAPI.model_api import OpenaiAPI -from utils.function_tracker import get_all_callbacks -from utils.image_utils import extract_image_paths, normalize_markdown_images, get_image_prompt_guidance -from workflows.workflow_baike import run_baike_workflow -from workflows.workflow_utils import ( - safe_json_extract, parse_history, normalize_latex_spacing, - remove_duplicate_content, convert_rag_result, calculate_info_completeness, - emit_callback_event, build_history_str, build_detail_info, - append_atlas_section, stream_format_and_postprocess, -) -from utils.word_generator import generate_report_word -from utils.intent_matcher import is_confirmation_intent -from config import KB_TREE_CONFIG, SERVER_CONFIG -from prompts import FAULT_DIAGNOSIS_PROMPTS, COMMON_PROMPTS -from tools.fault_record_db import record_fault_from_state -import json -import re -import random -import os -import time - -class FaultDiagnosisState(TypedDict): - """故障诊断工作流状态""" - extracted_text: str - combined_query: str - history_messages: List[Dict[str, Any]] - - ship_number: str - device_name: str - fault: str - additional_info: str - fault_code: str - fault_time: str - fault_frequency: str - tried_measures: str - running_condition: str - - rag_search_result: Optional[List[Dict[str, Any]]] - graph_search_result: Optional[str] - deep_rag_results: Optional[Dict[str, Any]] - - response: str - suggestedReplies: List[Dict[str, Any]] - actions: List[Dict[str, Any]] - error_message: str - - agent_decision: str - agent_reasoning: str - tools_to_call: List[str] - missing_info: List[str] - info_completeness: float - - matched_kb_id: str - matched_kb_name: str - - iteration_count: int - max_iterations: int - scheme_generated: bool - waiting_for_supplement: bool - - extracted_info: Dict[str, Any] - has_rag_result: bool - has_graph_result: bool - ask_round: int - user_confirmed_info: bool - - need_model_confirm: bool - user_confirmed_model: bool - - user_feedback: str - waiting_report_confirm: bool - report_confirmed: bool - - initialized: bool - current_node: str - - # 意图分类相关字段 - user_intent: str # 用户初始意图: '维修', '百科' - - # 后续处理相关字段 - follow_up_intent: str # 用户后续意图: 'not_solved', 'related_question', 'has_error', 'modify_info', 'other' - user_feedback_for_regenerate: str # 用户用于重新生成方案的反馈 - is_regenerating: bool # 是否正在重新生成方案 - last_generated_scheme: str # 最后一次生成的方案(用于基于反馈修改) - scheme_type: str # 方案类型:'normal' 普通方案,'deep' 深度方案 - deep_analysis_points: Optional[List[str]] # 深度分析点(用于针对性检索) - atlas_results: Optional[Dict[str, Any]] # 图册检索结果 - last_combined_query: str # 上一轮生成方案时的用户输入 - -def _preprocess_scheme_for_word(content: str) -> str: - """ - 预处理方案内容:将相对路径图片URL转为绝对URL - 图片格式解析和公式渲染由 word_generator.py 直接处理 - """ - if not content: - return content - - base_url = SERVER_CONFIG.get("base_url", "").rstrip("/") - - kb_base_url = KB_TREE_CONFIG.get("url", "").rstrip("/") - kb_host = "" - if kb_base_url: - from urllib.parse import urlparse - kb_parsed = urlparse(kb_base_url) - kb_host = f"{kb_parsed.scheme}://{kb_parsed.netloc}" - - # 知识库图片由知识库服务提供 - if kb_host: - content = re.sub( - r'(/api/v1/knowledge/files/[^)\s]+)', - lambda m: f"{kb_host}{m.group(1)}" if not m.group(1).startswith("http") else m.group(1), - content - ) - elif base_url: - content = re.sub( - r'(/api/v1/knowledge/files/[^)\s]+)', - lambda m: f"{base_url}{m.group(1)}" if not m.group(1).startswith("http") else m.group(1), - content - ) - - # /picture/ 图片由本应用提供 - if base_url: - content = re.sub( - r'(/picture/[^)\s]+)', - lambda m: f"{base_url}{m.group(1)}" if not m.group(1).startswith("http") else m.group(1), - content - ) - - return content - - -def _generate_report_from_scheme(scheme_content: str, ship_number: str = "", device_name: str = "", fault: str = "") -> List[Dict[str, Any]]: - """ - 从方案内容直接生成报告并返回下载按钮 - - Args: - scheme_content: 方案内容(Markdown 格式) - ship_number: 舷号 - device_name: 设备名称 - fault: 故障现象 - - Returns: - actions 列表,包含下载按钮(如果生成成功) - """ - try: - # 构建报告标题 - report_title = "维修方案报告" - if ship_number: - report_title = f"{ship_number} - {report_title}" - if device_name: - report_title = f"{report_title} - {device_name}" - - # 预处理方案内容:解析图片URL和公式格式 - processed_content = _preprocess_scheme_for_word(scheme_content) - - # 准备报告内容 - 直接使用方案内容,添加标题信息 - random_letters = ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ', k=2)) - report_number = time.strftime(f"{random_letters}-%Y-%m-%d-%H%M") - compile_time = time.strftime("%Y-%m-%d %H:%M") - - # 构建报告内容 - report_content = f"""## **{report_title}** - -报告编号:{report_number} -编制时间:{compile_time} - ---- - -{processed_content} -""" - - # 确保 created_word 目录存在 - current_dir = os.path.dirname(os.path.abspath(__file__)) - parent_dir = os.path.dirname(current_dir) - word_dir = os.path.join(parent_dir, "created_word") - os.makedirs(word_dir, exist_ok=True) - - # 生成 Word 文档 - timestamp = time.strftime("%Y%m%d_%H%M%S") - safe_title = re.sub(r'[\\/:*?"<>|]', '_', report_title) - file_name = f"{safe_title}_{timestamp}.docx" - output_path = os.path.join(word_dir, file_name) - - word_result = generate_report_word(markdown=report_content,title=report_title,output=output_path) - - # 构建下载按钮 - actions = [] - if word_result and os.path.exists(word_result): - base_url = SERVER_CONFIG.get("base_url", "") - download_url = f"{base_url}/api/download/{file_name}" - - actions.append({"type": "download","label": f"{report_title}.docx","url": download_url,"fileName": file_name,"content": report_content}) - - print(f"[_generate_report_from_scheme] 报告生成成功: {output_path}") - - return actions - - except Exception as e: - print(f"[_generate_report_from_scheme] 生成报告失败: {e}") - import traceback - traceback.print_exc() - return [] - -FAULT_INFO_WEIGHTS = {"ship_number": 0.20, "device_name": 0.20, "fault": 0.25, "fault_code": 0.08, "fault_time": 0.06, "fault_frequency": 0.06, "tried_measures": 0.06, "running_condition": 0.05, "additional_info": 0.04} - -def _calculate_info_completeness(ship_number: str = "", device_name: str = "", fault: str = "", fault_code: str = "", fault_time: str = "", fault_frequency: str = "", tried_measures: str = "", running_condition: str = "", additional_info: str = "") -> float: - values = {"ship_number": ship_number, "device_name": device_name, "fault": fault, "fault_code": fault_code, "fault_time": fault_time, "fault_frequency": fault_frequency, "tried_measures": tried_measures, "running_condition": running_condition, "additional_info": additional_info} - return calculate_info_completeness(FAULT_INFO_WEIGHTS, values) - -async def classify_intent(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - 意图分类节点 - """ - combined_query = state.get("combined_query", "") or "" - extracted_info = state.get("extracted_info", {}) or {} - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - - # 如果已经提取过维修信息(有设备名或故障现象),说明已在维修流程中,继续维修流程 - if extracted_info or device_name or fault or ship_number: - print("[classify_intent] 已在维修流程中,跳过意图分类") - return {"user_intent": "维修","current_node": "extract_info",} - - system_prompt = FAULT_DIAGNOSIS_PROMPTS["classify_intent_system"] - - prompt = COMMON_PROMPTS["classify_intent_user"].format(combined_query=combined_query) - - try: - response_text = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,json_output=True,system_prompt=system_prompt,messages=[]) - - parsed = safe_json_extract(response_text) - intent = "维修" - if isinstance(parsed, dict): - intent = parsed.get("intent", "维修") - - print(f"[意图分类] 用户意图: {intent}") - - if intent == "维修": - return {"user_intent": intent,"scheme_generated": False,"current_node": "extract_info",} - else: - return {"user_intent": intent,"current_node": "follow_up_qa",} - - except Exception as e: - print(f"[意图分类失败: {str(e)},默认按维修处理") - return {"user_intent": "维修","scheme_generated": False,"current_node": "extract_info",} - -def _route_after_classify(state: FaultDiagnosisState) -> str: - """意图分类后的路由""" - intent = state.get("user_intent", "维修") - if intent == "百科": - return "follow_up_qa" - return "extract_info" - -async def extract_info(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - 信息提取节点 - """ - if state.get("initialized", False): - print("[extract_info] 已初始化,跳过") - return {"current_node": "agent_think"} - - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - - history_str = build_history_str(history_messages, recent_count=10) - - existing_info = [] - existing_ship_number = state.get("ship_number", "") or "" - existing_device_name = state.get("device_name", "") or "" - existing_fault = state.get("fault", "") or "" - existing_fault_code = state.get("fault_code", "") or "" - existing_fault_time = state.get("fault_time", "") or "" - existing_fault_frequency = state.get("fault_frequency", "") or "" - existing_tried_measures = state.get("tried_measures", "") or "" - existing_running_condition = state.get("running_condition", "") or "" - existing_additional_info = state.get("additional_info", "") or "" - - if existing_ship_number: - existing_info.append(f"舷号: {existing_ship_number}") - if existing_device_name: - existing_info.append(f"设备名称: {existing_device_name}") - if existing_fault: - existing_info.append(f"故障现象: {existing_fault}") - if existing_fault_code: - existing_info.append(f"故障码: {existing_fault_code}") - if existing_fault_time: - existing_info.append(f"故障时间: {existing_fault_time}") - if existing_fault_frequency: - existing_info.append(f"故障频率: {existing_fault_frequency}") - if existing_tried_measures: - existing_info.append(f"已尝试措施: {existing_tried_measures}") - if existing_running_condition: - existing_info.append(f"运行工况: {existing_running_condition}") - if existing_additional_info: - existing_info.append(f"其他信息: {existing_additional_info}") - - existing_info_str = "\n".join(existing_info) if existing_info else "(无已提取信息)" - - system_prompt = FAULT_DIAGNOSIS_PROMPTS["extract_info_system"] - - prompt = COMMON_PROMPTS["extract_info_user"].format( - biz_label="维修", - existing_info_str=existing_info_str, - history_str=history_str or '(无历史对话)', - combined_query=combined_query - ) - - try: - response_text = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,json_output=True,system_prompt=system_prompt,messages=[]) - - parsed = safe_json_extract(response_text) - if not isinstance(parsed, dict): - parsed = {} - - new_ship_number = parsed.get("ship_number", "") or existing_ship_number - new_device_name = parsed.get("device_name", "") or existing_device_name - new_fault = parsed.get("fault", "") or existing_fault - new_fault_code = parsed.get("fault_code", "") or existing_fault_code - new_fault_time = parsed.get("fault_time", "") or existing_fault_time - new_fault_frequency = parsed.get("fault_frequency", "") or existing_fault_frequency - new_tried_measures = parsed.get("tried_measures", "") or existing_tried_measures - new_running_condition = parsed.get("running_condition", "") or existing_running_condition - new_additional_info = parsed.get("additional_info", "") or existing_additional_info - - extracted_info = {"ship_number": new_ship_number,"device_name": new_device_name,"fault": new_fault,"fault_code": new_fault_code,"fault_time": new_fault_time,"fault_frequency": new_fault_frequency,"tried_measures": new_tried_measures,"running_condition": new_running_condition,"additional_info": new_additional_info,} - - print(f"[信息提取] 舷号={extracted_info['ship_number']}, 设备={extracted_info['device_name']}, 故障={extracted_info['fault']}") - - return {**extracted_info,"extracted_info": extracted_info,"initialized": True,"current_node": "agent_think",} - - except Exception as e: - print(f"信息提取失败: {str(e)}") - return {"initialized": True,"current_node": "agent_think",} - -async def agent_think(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - Agent 思考节点:基于当前状态决定下一步动作 - """ - combined_query = state.get("combined_query", "") or "" - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - fault_code = state.get("fault_code", "") or "" - fault_time = state.get("fault_time", "") or "" - fault_frequency = state.get("fault_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - additional_info = state.get("additional_info", "") or "" - history_messages = state.get("history_messages", []) or [] - - scheme_generated = state.get("scheme_generated", False) - has_rag_result = state.get("has_rag_result", False) - has_graph_result = state.get("has_graph_result", False) - - rag_search_result = state.get("rag_search_result") - graph_search_result = state.get("graph_search_result") - iteration_count = state.get("iteration_count", 0) - - user_confirmed_info = state.get("user_confirmed_info", False) - waiting_feedback = state.get("waiting_feedback", False) - - has_ship = bool(ship_number and ship_number.strip()) - has_device = bool(device_name and device_name.strip()) - has_fault = bool(fault and fault.strip()) - has_rag = (rag_search_result is not None and len(rag_search_result) > 0) or has_rag_result - has_graph = (graph_search_result is not None and len(graph_search_result) > 100) or has_graph_result - - info_completeness = _calculate_info_completeness(ship_number=ship_number,device_name=device_name,fault=fault,fault_code=fault_code,fault_time=fault_time,fault_frequency=fault_frequency,tried_measures=tried_measures,running_condition=running_condition,additional_info=additional_info) - - MIN_COMPLETENESS_FOR_GENERATE = 0.25 - - if scheme_generated: - _re_diagnose_keywords = ["开始诊断", "信息已足够", "信息已足够,开始诊断", "开始", "诊断"] - _is_re_diagnose = any(kw in combined_query for kw in _re_diagnose_keywords) - if _is_re_diagnose: - print(f"[重新诊断] 检测到重新诊断意图: '{combined_query}',重置状态直接调用工具生成方案") - tools_to_call = ["rag_search"] - if device_name and fault: - tools_to_call.append("graph_rag_search") - return {"scheme_generated": False,"rag_search_result": None,"graph_search_result": None,"deep_rag_results": None,"has_rag_result": False,"has_graph_result": False,"is_regenerating": False,"user_feedback_for_regenerate": "","follow_up_intent": "","user_confirmed_info": True,"initialized": True,"iteration_count": 0,"ask_round": 0,"agent_decision": "call_tools","tools_to_call": tools_to_call,"current_node": "call_tools",} - - history_str = build_history_str(history_messages, recent_count=8, assistant_truncate=300) - - intent_system_prompt = FAULT_DIAGNOSIS_PROMPTS["agent_think_intent_system"] - - intent_prompt = FAULT_DIAGNOSIS_PROMPTS["agent_think_intent_user"].format( - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供', - history_str=history_str or '无', - combined_query=combined_query - ) - - try: - intent_response = await OpenaiAPI.open_api_chat_without_thinking( - query=intent_prompt, - model=None, - json_output=True, - system_prompt=intent_system_prompt, - messages=[] - ) - - parsed_intent = safe_json_extract(intent_response) - intent = "other" - if isinstance(parsed_intent, dict): - intent = parsed_intent.get("intent", "other") - - print(f"[Agent 意图分析] 用户意图: {intent}") - - # 根据大模型判断的意图来决定下一步 - if intent == "generate_report": - decision = "generate_report" - reasoning = "用户请求生成报告" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - elif intent == "modify_info": - # 修改信息,跳转到 confirm_info,重置状态 - decision = "confirm_info" - reasoning = "用户需要修改或补充信息" - return {"agent_decision": decision,"agent_reasoning": reasoning,"rag_search_result": None,"graph_search_result": None,"deep_rag_results": None,"has_rag_result": False,"has_graph_result": False,"scheme_generated": False,"is_regenerating": False,"user_feedback_for_regenerate": "","follow_up_intent": "","user_confirmed_info": False,"initialized": False,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - elif intent == "has_error": - decision = "handle_feedback" - reasoning = "用户反馈方案有错误" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - elif intent == "not_solved": - decision = "analyze_follow_up_intent" - reasoning = "已生成方案,分析用户后续意图" - # 设置 follow_up_intent,让 analyze_follow_up_intent 直接知道 - return {"agent_decision": decision,"agent_reasoning": reasoning,"follow_up_intent": "not_solved","info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - elif intent == "related_question": - decision = "analyze_follow_up_intent" - reasoning = "已生成方案,分析用户后续意图" - return {"agent_decision": decision,"agent_reasoning": reasoning,"follow_up_intent": "related_question","info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - else: - # other 情况,直接跳转到 follow_up_qa,不需要经过 analyze_follow_up_intent - decision = "follow_up_qa" - reasoning = "其他意图,直接调用 baike" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - except Exception as e: - print(f"[Agent 意图分析失败: {str(e)}") - # 大模型判断失败,降级到原来的逻辑 - decision = "analyze_follow_up_intent" - reasoning = "已生成方案,分析用户后续意图" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning} (降级)") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - if not has_device or not has_fault: - decision = "ask_user" - missing_info = [] - if not has_device: - missing_info.append("设备名称") - if not has_fault: - missing_info.append("故障现象") - reasoning = f"缺少基本信息: {', '.join(missing_info)}" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"missing_info": missing_info,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - if info_completeness < MIN_COMPLETENESS_FOR_GENERATE: - decision = "ask_user" - reasoning = f"信息完整性评分 {info_completeness} 低于最低要求 {MIN_COMPLETENESS_FOR_GENERATE}" - missing_info = [] - if not ship_number: - missing_info.append("舷号(可选)") - if not fault_code: - missing_info.append("故障码") - if not fault_time: - missing_info.append("故障时间") - if not fault_frequency: - missing_info.append("故障频率") - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"missing_info": missing_info or ["更多详细信息"],"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - if has_rag or has_graph: - decision = "generate_response" - reasoning = "信息充足,已有检索结果,直接生成回复" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - if not user_confirmed_info: - decision = "confirm_info" - reasoning = "信息充足,需要用户确认后再检索知识库" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - decision = "call_tools" - reasoning = "信息充足,用户已确认,开始检索知识库" - tools_to_call = ["rag_search"] - if has_device and has_fault: - tools_to_call.append("graph_rag_search") - - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - print(f"[Agent 提取] 舷号={ship_number}, 设备={device_name}, 故障={fault}") - - emit_callback_event(["🤖 Agent 思考中", "🤖 智能分析", "🤖 决策判断"], f"决策: {decision} | 理由: {reasoning[:50]}...") - - return {"agent_decision": decision,"agent_reasoning": reasoning,"tools_to_call": tools_to_call,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - -async def ask_user(state: FaultDiagnosisState) -> Command: - """ - 询问用户节点:使用 interrupt 暂停等待用户输入 - """ - missing_info = state.get("missing_info", []) - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - fault_code = state.get("fault_code", "") or "" - fault_time = state.get("fault_time", "") or "" - fault_frequency = state.get("fault_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - additional_info = state.get("additional_info", "") or "" - combined_query = state.get("combined_query", "") or "" - info_completeness = state.get("info_completeness", 0) - ask_round = state.get("ask_round", 0) - - MAX_ASK_ROUNDS = 4 - if ask_round >= MAX_ASK_ROUNDS: - print(f"[ask_user] 已达到最大询问轮次 {MAX_ASK_ROUNDS},强制进入下一步") - tools_to_call = ["rag_search"] - if device_name and fault: - tools_to_call.append("graph_rag_search") - return Command( - update={"ask_round": ask_round + 1,"current_node": "call_tools","tools_to_call": tools_to_call,}, - goto="call_tools" - ) - - existing_parts = [] - if ship_number: - existing_parts.append(f"舷号: {ship_number}") - if device_name: - existing_parts.append(f"设备名称: {device_name}") - if fault: - existing_parts.append(f"故障现象: {fault}") - if fault_code: - existing_parts.append(f"故障码: {fault_code}") - if fault_time: - existing_parts.append(f"发生时间: {fault_time}") - if fault_frequency: - existing_parts.append(f"故障频率: {fault_frequency}") - if tried_measures: - existing_parts.append(f"已尝试措施: {tried_measures}") - if running_condition: - existing_parts.append(f"运行工况: {running_condition}") - if additional_info: - existing_parts.append(f"其他信息: {additional_info}") - - existing_str = ";".join(existing_parts) if existing_parts else "暂无" - missing_str = "、".join(missing_info) if missing_info else "相关信息" - - system_prompt = COMMON_PROMPTS["ask_user_system"].format(biz_label="故障诊断", biz_label_short="诊断") - - prompt = COMMON_PROMPTS["ask_user_prompt"].format( - info_completeness=info_completeness, - existing_str=existing_str, - missing_str=missing_str, - combined_query=combined_query - ) - - response = "" - try: - response = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,system_prompt=system_prompt,messages=[]) - response = response.strip() if response else "" - except Exception as e: - print(f"[ask_user] API调用失败: {str(e)}") - response = "" - - if not response: - response = f"**【需要更多信息】**\n\n请补充:{missing_str}\n\n提供这些信息后,我可以更准确地为您诊断故障。" - - if existing_parts: - existing_info_section = f"\n**📋 已获取的信息:**\n\n" + "\n".join( - f"- {part}" for part in existing_parts) + "\n\n---\n" - response = existing_info_section + response - - suggested_replies = [] - if "舷号" in missing_info: - suggested_replies.append({"title": "补充舷号", "content": "舷号:"}) - if "设备名称" in missing_info: - suggested_replies.append({"title": "补充设备名称", "content": "设备名称:"}) - if "故障现象" in missing_info: - suggested_replies.append({"title": "补充故障现象", "content": "故障现象:"}) - - user_input = interrupt({"response": response,"suggestedReplies": suggested_replies,"waiting_for": "user_input","missing_info": missing_info,}) - - user_query = user_input.get("query", "") - - update_dict = {"combined_query": user_query,"current_node": "agent_think","ask_round": ask_round + 1,} - - if user_query: - extract_prompt = FAULT_DIAGNOSIS_PROMPTS["ask_user_extract"].format( - missing_str=missing_str, - user_query=user_query - ) - - try: - extract_response = await OpenaiAPI.open_api_chat_without_thinking(query=extract_prompt,model=None,json_output=True,system_prompt="你是信息提取专家。",messages=[]) - extracted = safe_json_extract(extract_response) - if isinstance(extracted, dict): - if extracted.get("ship_number"): - update_dict["ship_number"] = extracted["ship_number"] - print(f"[ask_user] 提取到舷号: {extracted['ship_number']}") - if extracted.get("device_name"): - update_dict["device_name"] = extracted["device_name"] - print(f"[ask_user] 提取到设备名称: {extracted['device_name']}") - if extracted.get("fault"): - update_dict["fault"] = extracted["fault"] - print(f"[ask_user] 提取到故障现象: {extracted['fault']}") - except Exception as e: - print(f"[ask_user] 信息提取失败: {str(e)}") - - return Command( - update=update_dict, - goto="agent_think" - ) - -async def confirm_info(state: FaultDiagnosisState) -> Command: - """ - 确认信息节点:重点询问用户是否需要补充信息 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - fault_code = state.get("fault_code", "") or "" - fault_time = state.get("fault_time", "") or "" - fault_frequency = state.get("fault_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - additional_info = state.get("additional_info", "") or "" - info_completeness = state.get("info_completeness", 0) - - existing_parts = [] - if ship_number: - existing_parts.append(f"舷号: {ship_number}") - if device_name: - existing_parts.append(f"设备名称: {device_name}") - if fault: - existing_parts.append(f"故障现象: {fault}") - if fault_code: - existing_parts.append(f"故障码: {fault_code}") - if fault_time: - existing_parts.append(f"发生时间: {fault_time}") - if fault_frequency: - existing_parts.append(f"故障频率: {fault_frequency}") - if tried_measures: - existing_parts.append(f"已尝试措施: {tried_measures}") - if running_condition: - existing_parts.append(f"运行工况: {running_condition}") - if additional_info: - existing_parts.append(f"其他信息: {additional_info}") - - existing_str = "\n".join(f"- {part}" for part in existing_parts) if existing_parts else "暂无" - - additional_prompt = "" - if not ship_number: - additional_prompt = "\n\n💡 **提示**:如果您知道舷号,请补充,这将有助于我们更准确地生成报告。" - - response = f"""**📋 已收集到的信息:** - -{existing_str} - -**信息完整性评分:** {info_completeness:.0%} - ---- - -**💡 请问是否需要补充其他信息?** -- 如故障码、故障时间、故障频率、已尝试措施、运行工况等 -- 如果信息已足够,请点击"开始诊断"{additional_prompt} -""" - - emit_callback_event(["✅ 信息确认", "📋 请确认信息", "🔍 准备检索"], f"等待用户确认或补充信息 (完整性: {info_completeness:.0%})") - - suggested_replies = [{"title": "开始诊断", "content": "信息已足够,开始诊断"},{"title": "补充信息", "content": "我需要补充:"}] - - user_input = interrupt({"response": response,"suggestedReplies": suggested_replies,"waiting_for": "user_confirm",}) - - user_query = user_input.get("query", "") - - if await is_confirmation_intent(user_query): - print(f"[confirm_info] 用户确认信息足够,准备调用工具") - tools_to_call = ["rag_search"] - if device_name and fault: - tools_to_call.append("graph_rag_search") - return Command( - update={"user_confirmed_info": True,"current_node": "call_tools","tools_to_call": tools_to_call,}, - goto="call_tools" - ) - else: - print(f"[confirm_info] 用户需要补充信息: {user_query}") - return Command( - update={"user_confirmed_info": False,"combined_query": user_query,"initialized": False,"current_node": "extract_info",}, - goto="extract_info" - ) - -async def call_tools(state: FaultDiagnosisState) -> Dict[str, Any]: - """调用检索工具""" - tools_to_call = state.get("tools_to_call", []) - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - additional_info = state.get("additional_info", "") or "" - - rag_results = state.get("rag_search_result") - graph_results = state.get("graph_search_result") - atlas_results = {} - - matched_kb_name = "" - matched_kb_id = "" - mapped_ship_number = "" - - if "rag_search" in tools_to_call and rag_results is None: - from utils.ship_number_search import search_with_ship_number_strategy - - base_query = f"{device_name}发生{fault}该如何解决" - - rag_results, matched_kb_name, matched_kb_id, mapped_ship_number = await search_with_ship_number_strategy(base_query=base_query,ship_number=ship_number,top_k=5,search_type="fault") - - if not matched_kb_name: - matched_kb_name = "通用知识库" - - print(f"RAG 检索来源: {matched_kb_name}") - - if mapped_ship_number and mapped_ship_number != ship_number: - print(f"[call_tools] 舷号映射: '{ship_number}' -> '{mapped_ship_number}',更新工作流状态") - ship_number = mapped_ship_number - - if "graph_rag_search" in tools_to_call and graph_results is None: - if device_name and fault: - try: - graph_query = f"{device_name}发生{fault}该如何解决" - graph_results = await graph_rag_search.ainvoke({"query": graph_query}) - print(f"图谱检索完成,结果长度: {len(graph_results) if graph_results else 0}") - except Exception as e: - print(f"图谱检索失败: {str(e)}") - graph_results = "" - - # 图册检索:只传入设备名称 - if device_name: - try: - print(f"[图册检索] 使用设备名称: {device_name}") - atlas_result = await atlas_retrieval(node_names=[device_name], top_k=10) - if atlas_result.get("success", False): - atlas_results = atlas_result.get("data", {}) - print(f"[图册检索] 图册检索结果: {list(atlas_results.keys())}") - except Exception as e: - print(f"[图册检索] 图册检索失败: {str(e)}") - - has_rag = bool(rag_results and len(rag_results) > 0) - has_graph = bool(graph_results and len(graph_results) > 100) - - if not has_rag and not has_graph: - print(f"[call_tools] 未找到任何相关资料,需要用户确认使用模型能力") - return {"rag_search_result": rag_results if isinstance(rag_results, list) else [],"graph_search_result": graph_results if isinstance(graph_results, str) else "","atlas_results": atlas_results,"matched_kb_id": matched_kb_id,"matched_kb_name": matched_kb_name,"ship_number": ship_number,"has_rag_result": False,"has_graph_result": False,"need_model_confirm": True,"current_node": "model_confirm",} - - return {"rag_search_result": rag_results,"graph_search_result": graph_results,"atlas_results": atlas_results,"matched_kb_id": matched_kb_id,"matched_kb_name": matched_kb_name,"ship_number": ship_number,"has_rag_result": has_rag,"has_graph_result": has_graph,"need_model_confirm": False,"current_node": "generate_response",} - -async def model_confirm(state: FaultDiagnosisState) -> Command: - """ - 模型能力确认节点:当没有找到任何资料时,让用户确认是否使用模型本身能力 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - - response = f"""**⚠️ 未找到相关资料** - -抱歉,在知识库中未找到与以下信息相关的资料: -- 舷号:{ship_number or '未提供'} -- 设备名称:{device_name or '未提供'} -- 故障现象:{fault or '未提供'} - -**💡 您可以选择:** -- 使用 AI 模型的知识库为您生成一般性建议 -- 提供更详细的信息重新检索 - -请问是否使用 AI 模型为您生成建议?""" - - emit_callback_event(["🤖 AI 模型确认", "⚠️ 资料未找到", "💡 使用模型能力"], "使用模型自身知识进行回复") - - suggested_replies = [{"title": "使用 AI 模型", "content": "确认,使用 AI 模型生成建议"},{"title": "补充信息", "content": "我需要补充更多信息:"},] - - user_input = interrupt({"response": response,"suggestedReplies": suggested_replies,"waiting_for": "model_confirm",}) - - user_query = user_input.get("query", "") - - if "确认" in user_query or "AI" in user_query or "模型" in user_query: - print(f"[model_confirm] 用户确认使用模型能力") - return Command( - update={"user_confirmed_model": True,"combined_query": user_query,"current_node": "generate_response",}, - goto="generate_response" - ) - else: - print(f"[model_confirm] 用户选择补充信息: {user_query}") - return Command( - update={"user_confirmed_model": False,"combined_query": user_query,"current_node": "agent_think",}, - goto="agent_think" - ) - -async def generate_response(state: FaultDiagnosisState) -> Dict[str, Any]: - """生成最终回复""" - print("========================生成最终回复========================") - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - additional_info = state.get("additional_info", "") or "" - fault_code = state.get("fault_code", "") or "" - fault_time = state.get("fault_time", "") or "" - fault_frequency = state.get("fault_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - rag_results = state.get("rag_search_result") or [] - graph_results = state.get("graph_search_result") or "" - scheme_generated = state.get("scheme_generated", False) - matched_kb_name = state.get("matched_kb_name", "") - - print("rag_results", rag_results) - history_str = build_history_str(history_messages, recent_count=12, assistant_truncate=500) - - has_rag = rag_results and len(rag_results) > 0 - has_graph = graph_results and len(graph_results) > 100 - - if not has_rag and not has_graph: - if not ship_number and not device_name and not fault: - system_prompt = COMMON_PROMPTS["generate_response_friendly_system"].format(biz_label="故障诊断") - prompt = COMMON_PROMPTS["generate_response_friendly_user"].format(combined_query=combined_query) - else: - system_prompt = COMMON_PROMPTS["generate_response_no_rag_system"].format(biz_label="故障诊断") - prompt = FAULT_DIAGNOSIS_PROMPTS["generate_response_no_rag_user"].format( - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供', - fault_code=fault_code or '未提供', - fault_time=fault_time or '未提供', - fault_frequency=fault_frequency or '未提供', - tried_measures=tried_measures or '未提供', - running_condition=running_condition or '未提供', - additional_info=additional_info or '无' - ) - - try: - response = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,system_prompt=system_prompt,messages=[]) - return {"response": response.strip() if response else "抱歉,未能找到相关资料。","suggestedReplies": [{"title": "提供更详细信息", "content": "我来提供更详细的信息"},],"current_node": "end",} - except Exception as e: - return {"response": f"抱歉,处理过程中出现错误: {str(e)}","suggestedReplies": [],"current_node": "end",} - - rag_ctx_parts: List[str] = [] - if isinstance(rag_results, str) and rag_results.strip(): - rag_ctx_parts = [rag_results.strip()] - elif isinstance(rag_results, list): - for item in sorted(rag_results or [], key=lambda x: float(x.get("score") or 0) if isinstance(x,dict) else 0, reverse=True)[:4]: -# for item in rag_results[:8]: - if isinstance(item, dict): - text = (item.get("text") or "").strip() - if text: - rag_ctx_parts.append(text) - elif isinstance(item, str) and item.strip(): - rag_ctx_parts.append(item.strip()) - - if len(graph_results) <= 50: - graph_results = "" - - all_source_text = graph_results + "\n" + "\n".join(rag_ctx_parts) if graph_results else "\n".join(rag_ctx_parts) - original_image_paths = extract_image_paths(all_source_text) - print("提取到的原始图片路径:", original_image_paths) - - rag_answer = "" - rag_type = "none" - - if graph_results and len(graph_results) > 50: - rag_answer = graph_results - rag_type = "graph" - elif rag_ctx_parts: - rag_answer = "\n\n".join(rag_ctx_parts) - rag_type = "rag" - - if not rag_answer or rag_answer == "NO_RELEVANT_RESULT": - system_prompt = COMMON_PROMPTS["generate_response_no_rag_simple_system"].format(biz_label="故障诊断") - prompt = FAULT_DIAGNOSIS_PROMPTS["generate_response_no_rag_simple_user"].format( - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供' - ) - - try: - response = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,system_prompt=system_prompt,messages=[]) - return {"response": response.strip() if response else "抱歉,未能找到相关资料。","suggestedReplies": [],"current_node": "end",} - except Exception as e: - return {"response": f"抱歉,处理过程中出现错误: {str(e)}","suggestedReplies": [],"current_node": "end",} - - rag_type = "图谱" if rag_type == "graph" else "文本" - - emit_callback_event([f"✨当前使用的检索类型为{rag_type}", f"✨本次检索策略为{rag_type}"], f"内容为{rag_answer[:50]}...") - - detail_info = "" - if fault_code: - detail_info += f"- 故障码/报警代码:{fault_code}\n" - if fault_time: - detail_info += f"- 故障发生时间:{fault_time}\n" - if fault_frequency: - detail_info += f"- 故障频率/持续时间:{fault_frequency}\n" - if tried_measures: - detail_info += f"- 已尝试的维修措施:{tried_measures}\n" - if running_condition: - detail_info += f"- 设备运行工况:{running_condition}\n" - if additional_info: - detail_info += f"- 其他相关信息:{additional_info}\n" - - is_regenerating = state.get("is_regenerating", False) - user_feedback_for_regenerate = state.get("user_feedback_for_regenerate", "") - is_follow_up = scheme_generated and (has_rag or has_graph) and not is_regenerating - - image_guidance = get_image_prompt_guidance() - if is_regenerating and user_feedback_for_regenerate: - prompt = COMMON_PROMPTS["generate_response_regenerating"].format( - biz_label="维修", - item_label="故障现象", - item_name=fault or '未提供', - item_action="分析故障原因,给出维修方案", - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供', - detail_info=detail_info, - user_feedback_for_regenerate=user_feedback_for_regenerate, - rag_answer=rag_answer, - image_guidance=image_guidance - ) - elif is_follow_up: - prompt = COMMON_PROMPTS["generate_response_follow_up"].format( - biz_label="维修", - item_label="故障现象", - item_name=fault or '未提供', - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供', - detail_info=detail_info, - rag_answer=rag_answer, - combined_query=combined_query, - image_guidance=image_guidance - ) - else: - prompt = COMMON_PROMPTS["generate_response_normal"].format( - biz_label="维修", - item_label="故障现象", - item_name=fault or '未提供', - item_action="分析故障原因,给出维修方案", - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供', - detail_info=detail_info, - rag_answer=rag_answer - ) - - try: - # ========== 第一步:专注于生成高质量内容 ========== - content_system_prompt = COMMON_PROMPTS["generate_response_content_system"].format(biz_label="维修") - - raw_content = await OpenaiAPI.open_api_chat_without_thinking(model=None,system_prompt=content_system_prompt,messages=[{"role": "user", "content": prompt}],) - - raw_content = raw_content.strip() if raw_content else "抱歉,无法生成回答。" - - emit_callback_event(["🤖 诊断分析中", "🤖 故障预检中"], raw_content[:30] + "...") - - answer = await stream_format_and_postprocess(raw_content, original_image_paths, temperature=0.3) - -# scheme_source = f"# 本方案依据{rag_type}知识库生成。" -# answer = f"{scheme_source}\n\n{answer}" - answer = answer -# answer = "" - - atlas_results = state.get("atlas_results", {}) - answer = append_atlas_section(answer, atlas_results) - - if not is_follow_up: - supplement_prompt = """ - ---- - -**💡 请您确认维修方案是否正确:** -- 如果方案有误或需要调整,请告诉我具体的错误之处 -- 我会根据您的反馈重新生成更准确的方案 - -请问方案是否有需要修改的地方?""" - answer += supplement_prompt - - suggested_replies = [{"title": "方案有误", "content": "方案有误,需要修改:"},] - - # 生成报告并添加下载按钮 - actions = _generate_report_from_scheme(scheme_content=answer,ship_number=ship_number,device_name=device_name,fault=fault) - - if not is_follow_up: - try: - await record_fault_from_state({"ship_number": ship_number,"device_name": device_name,"fault": fault,"last_generated_scheme": answer,}) - except Exception as rec_err: - print(f"[generate_response] 记录故障信息失败: {rec_err}") - - return {"response": answer,"actions": actions,"suggestedReplies": suggested_replies,"scheme_generated": True,"waiting_feedback": not is_follow_up,"last_generated_scheme": answer,"scheme_type": "normal","current_node": "end","last_combined_query": combined_query,} - - except Exception as e: - return {"response": f"分析生成失败: {str(e)}","actions": [],"suggestedReplies": [],"current_node": "end",} - -async def analyze_follow_up_intent(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - 分析用户后续意图节点:直接使用 agent_think 已经判断好的意图 - agent_think 已经用大模型判断并设置了 follow_up_intent - """ - # 直接使用已经判断好的意图 - intent = state.get("follow_up_intent", "other") - - print(f"[意图分析] 使用已判断的意图: {intent}") - - return {"follow_up_intent": intent,} - -async def deep_rag_search(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - 深层次RAG检索节点:分析之前方案并选择深度分析点进行检索 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - fault_code = state.get("fault_code", "") or "" - additional_info = state.get("additional_info", "") or "" - fault_time = state.get("fault_time", "") or "" - fault_frequency = state.get("fault_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - last_generated_scheme = state.get("last_generated_scheme", "") or "" - - deep_results = {} - deep_analysis_points = [] - - emit_callback_event(["🔍 深度检索中", "🔍 多维度检索", "🔍 精细分析"], "分析问题并选择深度分析点") - - # ========== 第一步:分析之前方案和用户问题,选择深度分析点 ========== - if last_generated_scheme or combined_query: - try: - history_str = build_history_str(history_messages, recent_count=8, assistant_truncate=300) - - detail_info = "" - if fault_code: - detail_info += f"- 故障码/报警代码:{fault_code}\n" - if fault_time: - detail_info += f"- 故障发生时间:{fault_time}\n" - if fault_frequency: - detail_info += f"- 故障频率/持续时间:{fault_frequency}\n" - if tried_measures: - detail_info += f"- 已尝试的维修措施:{tried_measures}\n" - if running_condition: - detail_info += f"- 设备运行工况:{running_condition}\n" - if additional_info: - detail_info += f"- 其他相关信息:{additional_info}\n" - - analysis_system_prompt = COMMON_PROMPTS["deep_rag_analysis_system"].format(biz_label="维修") - - analysis_prompt = COMMON_PROMPTS["deep_rag_analysis_user"].format( - item_label="故障现象", - item_name=fault or '未提供', - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供', - detail_info=detail_info, - history_str=history_str or '(无历史对话)', - last_generated_scheme=last_generated_scheme or '(无之前的方案)', - combined_query=combined_query - ) - - analysis_response = await OpenaiAPI.open_api_chat_without_thinking(query=analysis_prompt,model=None,json_output=True,system_prompt=analysis_system_prompt,messages=[]) - print("深度分析点", analysis_response) - parsed_analysis = safe_json_extract(analysis_response) - deep_analysis_points = parsed_analysis - print(f"[深度RAG] 选择的分析点: {deep_analysis_points}") - except Exception as e: - print(f"[深度RAG] 分析失败: {str(e)}") - - # ========== 第二步:对选中的分析点进行检索 ========== - for idx, point in enumerate(deep_analysis_points): - try: - print(point) - # 结合设备名和故障名进行检索,提高准确性 - search_query = point - if device_name and device_name not in point: - search_query = f"{device_name} {search_query}" - if fault and fault not in search_query: - search_query = f"{search_query} {fault}" - - rag_result = await rag_search(search_query, top_k=3) - if rag_result.get("success", False): - deep_results[f"analysis_point_{idx}"] = convert_rag_result(rag_result) - else: - deep_results[f"analysis_point_{idx}"] = [] - except Exception as e: - print(f"[深度RAG] 分析点 {point} 检索失败: {str(e)}") - deep_results[f"analysis_point_{idx}"] = [] - - # ========== 第三步:图册检索 ========== - atlas_results = {} - try: - # 从历史记录和当前信息中抽取实体名称 - entity_names = await extract_entity_names_from_history(history_messages=history_messages,device_name=device_name,fault=fault,last_generated_scheme=last_generated_scheme) - - if entity_names: - print(f"[深度RAG] 图册检索实体: {entity_names}") - atlas_result = await atlas_retrieval(node_names=entity_names, top_k=10) - if atlas_result.get("success", False): - atlas_results = atlas_result.get("data", {}) - print(f"[深度RAG] 图册检索结果: {list(atlas_results.keys())}") - except Exception as e: - print(f"[深度RAG] 图册检索失败: {str(e)}") - - return {"deep_rag_results": deep_results,"deep_analysis_points": deep_analysis_points,"atlas_results": atlas_results,"current_node": "generate_deep_response",} - -async def generate_deep_response(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - 生成深度响应节点:基于深度RAG结果进行深度润色和推理 - """ - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - fault_code = state.get("fault_code", "") or "" - fault_time = state.get("fault_time", "") or "" - fault_frequency = state.get("fault_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - additional_info = state.get("additional_info", "") or "" - - deep_rag_results = state.get("deep_rag_results", {}) - deep_analysis_points = state.get("deep_analysis_points", []) - atlas_results = state.get("atlas_results", {}) - graph_results = state.get("graph_search_result", "") or "" - original_rag_results = state.get("rag_search_result", []) - - # 整理深度分析点资料 - analysis_points_text = "" - if deep_analysis_points: - for idx, point in enumerate(deep_analysis_points): - key = f"analysis_point_{idx}" - results = deep_rag_results.get(key, []) - if results: - analysis_points_text += f"\n【分析点:{point}】\n" - for i, item in enumerate(results[:2], 1): - if isinstance(item, dict): - text = item.get("text", "") - if text: - analysis_points_text += f"[{i}] {text}\n" - - # 整理原始RAG结果 - original_rag_text = "" - if original_rag_results: - original_rag_text = "\n【原始检索资料】\n" - for i, item in enumerate(original_rag_results[:3], 1): - if isinstance(item, dict): - text = item.get("text", "") - if text: - original_rag_text += f"[{i}] {text}\n" - - # 整理图册资料 - atlas_text = format_atlas_results(atlas_results) - - # 提取所有资料中的图片路径 - all_source_text = analysis_points_text + original_rag_text + atlas_text + graph_results - original_image_paths = extract_image_paths(all_source_text) - - detail_info = "" - if fault_code: - detail_info += f"- 故障码/报警代码:{fault_code}\n" - if fault_time: - detail_info += f"- 故障发生时间:{fault_time}\n" - if fault_frequency: - detail_info += f"- 故障频率/持续时间:{fault_frequency}\n" - if tried_measures: - detail_info += f"- 已尝试的维修措施:{tried_measures}\n" - if running_condition: - detail_info += f"- 设备运行工况:{running_condition}\n" - if additional_info: - detail_info += f"- 其他相关信息:{additional_info}\n" - - # 构建深度分析点的提示 - analysis_points_prompt = "" - if deep_analysis_points: - analysis_points_prompt = f"\n【本次深度分析重点】\n" + "\n".join([f"- {p}" for p in deep_analysis_points]) + "\n" - - prompt = COMMON_PROMPTS["generate_deep_response"].format( - biz_label="维修", - item_label="故障现象", - item_name=fault or '未提供', - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供', - detail_info=detail_info, - combined_query=combined_query, - analysis_points_prompt=analysis_points_prompt, - analysis_points_text=analysis_points_text, - original_rag_text=original_rag_text, - graph_results=graph_results if len(graph_results) > 50 else '(无)' - ) - - try: - # ========== 第一步:专注于生成高质量内容 ========== - content_system_prompt = COMMON_PROMPTS["generate_deep_response_content_system"].format(biz_label="维修") - - raw_content = await OpenaiAPI.open_api_chat_without_thinking(model=None,system_prompt=content_system_prompt,messages=[{"role": "user", "content": prompt}],) - - raw_content = raw_content.strip() if raw_content else "抱歉,无法生成深度分析。" - - emit_callback_event(["🤖 诊断分析中", "🤖 故障预检中"], raw_content[:30] + "...") - - answer = await stream_format_and_postprocess(raw_content, original_image_paths, temperature=0.1) - - answer = append_atlas_section(answer, atlas_results) - - suggested_replies = [{"title": "方案有误", "content": "方案还是有问题:"},] - - actions = _generate_report_from_scheme(scheme_content=answer,ship_number=ship_number,device_name=device_name,fault=fault) - - try: - await record_fault_from_state({"ship_number": ship_number,"device_name": device_name,"fault": fault,"last_generated_scheme": answer,}) - except Exception as rec_err: - print(f"[generate_deep_response] 记录故障信息失败: {rec_err}") - - return {"response": answer,"actions": actions,"suggestedReplies": suggested_replies,"scheme_generated": True,"last_generated_scheme": answer,"scheme_type": "deep","current_node": "end","last_combined_query": combined_query,} - - except Exception as e: - return {"response": f"深度分析生成失败: {str(e)}","actions": [],"suggestedReplies": [],"current_node": "end",} - -async def regenerate_scheme_from_feedback(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - 基于用户反馈直接修改方案节点:使用之前保存的方案和用户反馈进行修改 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - additional_info = state.get("additional_info", "") or "" - fault_code = state.get("fault_code", "") or "" - fault_time = state.get("fault_time", "") or "" - fault_frequency = state.get("fault_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - - user_feedback = state.get("user_feedback_for_regenerate", "") or "" - last_scheme = state.get("last_generated_scheme", "") or "" - scheme_type = state.get("scheme_type", "normal") - - if not last_scheme: - # 如果没有保存的方案,回退到重新检索 - print("[regenerate_scheme_from_feedback] 没有保存的方案,回退到重新检索") - return {"is_regenerating": True,"rag_search_result": None,"graph_search_result": None,"has_rag_result": False,"has_graph_result": False,"scheme_generated": False,"current_node": "call_tools","tools_to_call": ["rag_search", "graph_rag_search"],} - - detail_info = "" - if fault_code: - detail_info += f"- 故障码/报警代码:{fault_code}\n" - if fault_time: - detail_info += f"- 故障发生时间:{fault_time}\n" - if fault_frequency: - detail_info += f"- 故障频率/持续时间:{fault_frequency}\n" - if tried_measures: - detail_info += f"- 已尝试的维修措施:{tried_measures}\n" - if running_condition: - detail_info += f"- 设备运行工况:{running_condition}\n" - if additional_info: - detail_info += f"- 其他相关信息:{additional_info}\n" - - emit_callback_event(["✏️ 修改方案中", "🔧 调整方案", "📝 优化方案"], f"根据反馈修改方案: {user_feedback[:30]}...") - - image_guidance = get_image_prompt_guidance() - - prompt = COMMON_PROMPTS["regenerate_scheme_from_feedback"].format( - biz_label="维修", - item_label="故障现象", - item_name=fault or '未提供', - item_action="分析故障原因,给出维修方案", - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - fault=fault or '未提供', - detail_info=detail_info, - last_scheme=last_scheme, - user_feedback=user_feedback, - image_guidance=image_guidance - ) - - try: - # ========== 第一步:专注于生成高质量内容 ========== - content_system_prompt = COMMON_PROMPTS["regenerate_scheme_from_feedback_system"].format(biz_label="维修") - - # 构建第一步的内容提示词(简化格式要求) - content_prompt = prompt.replace("【输出要求】", "【输出要求】\n- 请专注于内容的准确性、完整性和逻辑性,不要担心格式的美观性\n- 可以适当使用简单的段落分隔,但不要使用复杂的格式") - - raw_content = await OpenaiAPI.open_api_chat_without_thinking(model=None,system_prompt=content_system_prompt,messages=[{"role": "user", "content": content_prompt}],) - - raw_content = raw_content.strip() if raw_content else "抱歉,无法根据反馈修改方案。" - - original_image_paths = extract_image_paths(last_scheme) - answer = await stream_format_and_postprocess(raw_content, original_image_paths, temperature=0.3, content_label="维修方案") - - suggested_replies = [{"title": "方案有误", "content": "方案还是有问题:"},] - - actions = _generate_report_from_scheme(scheme_content=answer,ship_number=ship_number,device_name=device_name,fault=fault) - - try: - await record_fault_from_state({"ship_number": ship_number,"device_name": device_name,"fault": fault,"last_generated_scheme": answer,}) - except Exception as rec_err: - print(f"[regenerate_scheme_from_feedback] 记录故障信息失败: {rec_err}") - - return {"response": answer,"actions": actions,"suggestedReplies": suggested_replies,"scheme_generated": True,"last_generated_scheme": answer,"scheme_type": scheme_type,"current_node": "end","last_combined_query": combined_query,} - - except Exception as e: - print(f"[regenerate_scheme_from_feedback] 修改方案失败: {str(e)}") - # 如果修改失败,回退到重新检索 - return {"is_regenerating": True,"rag_search_result": None,"graph_search_result": None,"has_rag_result": False,"has_graph_result": False,"scheme_generated": False,"current_node": "call_tools","tools_to_call": ["rag_search", "graph_rag_search"],} - -async def follow_up_qa(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - 后续问答节点:调用 baike 智能体处理用户的后续问题 - """ - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - - history_message_json = json.dumps(history_messages, ensure_ascii=False) if history_messages else "" - - context_prefix = "" - if ship_number or device_name or fault: - context_parts = [] - if ship_number: - context_parts.append(f"舷号: {ship_number}") - if device_name: - context_parts.append(f"设备名称: {device_name}") - if fault: - context_parts.append(f"故障现象: {fault}") - context_prefix = f"【当前故障诊断上下文】\n{'; '.join(context_parts)}\n\n" - - enhanced_query = f"{context_prefix}用户后续问题: {combined_query}" - - emit_callback_event(["💬 后续问答", "💬 执行指导", "💬 问题解答"], f"调用 百科 智能体: {combined_query[:30]}...") - - try: - baike_result = await run_baike_workflow(extracted_text=enhanced_query,combined_query=enhanced_query,history_message=history_message_json,route_flag="baike") - - response = baike_result.get("response", "") - suggested_replies = baike_result.get("suggestedReplies", []) - - return {"response": response,"suggestedReplies": suggested_replies,"actions": [],"scheme_generated": True,"current_node": "end",} - - except Exception as e: - return {"response": f"处理您的问题时出现错误: {str(e)}","suggestedReplies": [],"actions": [],"current_node": "end",} - -async def generate_report(state: FaultDiagnosisState) -> Dict[str, Any]: - """报告生成节点""" - from workflows.workflow_other_report import run_other_report_workflow - - combined_query = state.get("combined_query", "") or "" - history_message = state.get("history_messages", []) - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - - history_message_json = json.dumps(history_message, ensure_ascii=False) if history_message else "" - - try: - report_result = await run_other_report_workflow(extracted_text=combined_query,combined_query=combined_query,history_message=history_message_json,route_flag="report") - - return {"response": report_result.get("response", ""),"actions": report_result.get("actions", []),"suggestedReplies": report_result.get("suggestedReplies", []),"scheme_generated": True,"current_node": "end",} - except Exception as e: - return {"response": f"报告生成失败: {str(e)}","actions": [],"suggestedReplies": [],"current_node": "end",} - -async def handle_feedback(state: FaultDiagnosisState) -> Command: - """ - 处理用户反馈节点:接收用户对方案的反馈,提供上报和重新生成方案按钮 - """ - combined_query = state.get("combined_query", "") or "" - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - - response = f"""**📝 已收到您的反馈** - -感谢您对维修方案的建议!您的反馈已记录: - -{combined_query} - ---- - -**📋 反馈信息摘要:** -- 舷号:{ship_number or '未提供'} -- 设备名称:{device_name or '未提供'} -- 故障现象:{fault or '未提供'} -- 反馈内容:{combined_query[:100]}{'...' if len(combined_query) > 100 else ''} - ---- - -**💡 您可以选择:** -- 点击「重新生成方案」根据您的反馈重新生成方案 -- 点击「上报」将反馈提交给相关部门 -- 继续对话获取更多帮助""" - - actions = [ - {"type": "content","label": "重新生成方案","text": "重新生成方案"}, - {"type": "content","label": "上报","text": "上报"} - ] - - suggested_replies = [] - - emit_callback_event(["📝 反馈处理", "📋 方案反馈", "✅ 反馈确认"], f"处理用户反馈: {combined_query[:30]}...") - - user_input = interrupt({"response": response,"actions": actions,"suggestedReplies": suggested_replies,"waiting_for": "feedback_confirm",}) - - user_text = user_input.get("query", "") or "" - - if user_text == "重新生成方案": - print(f"[handle_feedback] 用户选择重新生成方案") - return Command( - update={"user_feedback_for_regenerate": combined_query,"current_node": "regenerate_scheme_from_feedback",}, - goto="regenerate_scheme_from_feedback" - ) - elif user_text == "上报": - print(f"[handle_feedback] 用户选择上报反馈") - return Command( - update={"user_feedback": combined_query,"waiting_report_confirm": True,"current_node": "confirm_report",}, - goto="confirm_report" - ) - else: - print(f"[handle_feedback] 用户继续对话: {user_text}") - return Command( - update={"user_feedback": combined_query,"combined_query": user_text,"current_node": "agent_think",}, - goto="agent_think" - ) - -async def confirm_report(state: FaultDiagnosisState) -> Dict[str, Any]: - """ - 上报确认节点:用户点击上报后确认完成 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - fault = state.get("fault", "") or "" - user_feedback = state.get("user_feedback", "") or "" - - response = f"""**✅ 上报成功** - -您的反馈已成功上报! - ---- - -**📋 上报信息:** -- 舷号:{ship_number or '未提供'} -- 设备名称:{device_name or '未提供'} -- 故障现象:{fault or '未提供'} -- 反馈内容:{user_feedback[:100]}{'...' if len(user_feedback) > 100 else ''} - ---- - -**💡 感谢您的反馈,相关部门将及时处理。**""" - - suggested_replies = [{"title": "生成维修报告", "content": "请帮我生成维修报告"},] - - emit_callback_event(["✅ 上报成功", "📋 反馈已提交", "🎉 完成"], "反馈上报成功") - - return {"response": response,"actions": [],"suggestedReplies": suggested_replies,"report_confirmed": True,"waiting_report_confirm": False,"current_node": "end",} - -def _route_after_think(state: FaultDiagnosisState) -> str: - """Agent 思考后的路由""" - decision = state.get("agent_decision", "ask_user") - - return decision - -def _route_after_intent(state: FaultDiagnosisState) -> str: - """意图分析后的路由""" - intent = state.get("follow_up_intent", "other") - - if intent == "not_solved": - return "deep_rag_search" - elif intent == "related_question": - return "follow_up_qa" - elif intent == "has_error": - return "handle_feedback" - elif intent == "modify_info": - # 修改信息,重置相关状态,重新开始信息提取 - return "extract_info" - else: - return "follow_up_qa" - -def _route_after_tools(state: FaultDiagnosisState) -> str: - """检索工具调用后的路由""" - need_model_confirm = state.get("need_model_confirm", False) - if need_model_confirm: - return "model_confirm" - return "generate_response" - -def create_fault_diagnosis_workflow(checkpointer=None): - """ - 创建故障诊断工作流(基于 Checkpointer 的状态持久化版本) - """ - workflow = StateGraph(FaultDiagnosisState) - - workflow.add_node("classify_intent", classify_intent) - workflow.add_node("extract_info", extract_info) - workflow.add_node("agent_think", agent_think) - workflow.add_node("ask_user", ask_user) - workflow.add_node("confirm_info", confirm_info) - workflow.add_node("call_tools", call_tools) - workflow.add_node("generate_response", generate_response) - workflow.add_node("follow_up_qa", follow_up_qa) - workflow.add_node("model_confirm", model_confirm) - workflow.add_node("generate_report", generate_report) - workflow.add_node("handle_feedback", handle_feedback) - workflow.add_node("confirm_report", confirm_report) - workflow.add_node("analyze_follow_up_intent", analyze_follow_up_intent) - workflow.add_node("deep_rag_search", deep_rag_search) - workflow.add_node("generate_deep_response", generate_deep_response) - workflow.add_node("regenerate_scheme_from_feedback", regenerate_scheme_from_feedback) - - workflow.add_edge(START, "classify_intent") - workflow.add_conditional_edges("classify_intent", _route_after_classify, - {"extract_info": "extract_info","follow_up_qa": "follow_up_qa",}) - workflow.add_edge("extract_info", "agent_think") - workflow.add_conditional_edges("agent_think", _route_after_think, - {"ask_user": "ask_user","confirm_info": "confirm_info","call_tools": "call_tools","generate_response": "generate_response","follow_up": "follow_up_qa","follow_up_qa": "follow_up_qa","generate_report": "generate_report","handle_feedback": "handle_feedback","extract_info": "extract_info","analyze_follow_up_intent": "analyze_follow_up_intent",}) - workflow.add_conditional_edges("call_tools", _route_after_tools, {"model_confirm": "model_confirm","generate_response": "generate_response",}) - workflow.add_conditional_edges("analyze_follow_up_intent", _route_after_intent, {"deep_rag_search": "deep_rag_search","follow_up_qa": "follow_up_qa","handle_feedback": "handle_feedback","extract_info": "extract_info",}) - workflow.add_edge("deep_rag_search", "generate_deep_response") - workflow.add_edge("generate_deep_response", END) - workflow.add_edge("generate_response", END) - workflow.add_edge("follow_up_qa", END) - workflow.add_edge("generate_report", END) - workflow.add_edge("regenerate_scheme_from_feedback", END) - workflow.add_edge("handle_feedback", END) - workflow.add_edge("confirm_report", END) - - return workflow.compile(checkpointer=checkpointer) - -async def run_fault_diagnosis_workflow(extracted_text: str,combined_query: str,history_message: str = "",route_flag: str = "",previous_state_json: str = "",chat_id: str = "",message_id: str = "",checkpointer=None,user_response: Dict[str, Any] = None) -> Dict[str, Any]: - """ - 执行故障诊断工作流 - - Args: - extracted_text: 文本描述 - combined_query: 组合查询 - history_message: 历史消息(JSON格式) - route_flag: 路由标识 - previous_state_json: 兼容参数(已废弃) - chat_id: 聊天会话 ID - message_id: 消息 ID - checkpointer: LangGraph checkpointer 实例 - user_response: 用户恢复执行的响应数据 - - Returns: - 工作流执行结果 - """ - thread_id = chat_id if chat_id else None - - config = {"configurable": {"thread_id": thread_id}} if thread_id else {} - - print(f"[DEBUG] run_fault_diagnosis_workflow: thread_id={thread_id}, checkpointer={checkpointer is not None}") - - if checkpointer is None: - from checkpointer_config import checkpointer_manager - checkpointer = await checkpointer_manager.get_async_checkpointer() - - app = create_fault_diagnosis_workflow(checkpointer=checkpointer) - - if thread_id: - try: - current_state = await app.aget_state(config) - print( - f"[DEBUG] current_state exists: {current_state is not None}, has values: {current_state.values is not None if current_state else False}, tasks: {len(current_state.tasks) if current_state else 0}") - - if current_state and current_state.values: - saved_state = current_state.values - print( - f"[Checkpointer] 发现已保存状态: ship_number={saved_state.get('ship_number')}, device_name={saved_state.get('device_name')}, fault={saved_state.get('fault')}") - - if current_state.tasks: - print(f"[Checkpointer] 恢复执行,注入用户响应") - resume_value = user_response if user_response else {"query": combined_query} - result = await app.ainvoke(Command(resume=resume_value), config) - else: - # 只使用当前的输入,不拼接之前的! - merged_query = combined_query - - # 如果当前输入是确认意图,且已有完整信息,直接视为用户已确认 - _is_confirm = await is_confirmation_intent(combined_query) - _has_full_info = (saved_state.get("ship_number") and saved_state.get("device_name") and saved_state.get("fault")) - _override_confirmed = _is_confirm and _has_full_info and not saved_state.get("user_confirmed_info", False) - - initial_state = {**saved_state,"combined_query": merged_query,"history_messages": parse_history(history_message),"initialized": False,"user_intent": "",} - if _override_confirmed: - print(f"[Checkpointer] 检测到确认意图,自动设置 user_confirmed_info=True") - initial_state["user_confirmed_info"] = True - result = await app.ainvoke(initial_state, config) - else: - initial_state = {"extracted_text": extracted_text,"combined_query": combined_query,"history_messages": parse_history(history_message),"ship_number": "","device_name": "","fault": "","additional_info": "", - "fault_code": "","fault_time": "","fault_frequency": "","tried_measures": "","running_condition": "","rag_search_result": None,"graph_search_result": None,"response": "", - "suggestedReplies": [],"actions": [],"error_message": "","agent_decision": "","agent_reasoning": "","tools_to_call": [],"missing_info": [],"info_completeness": 0.0, - "matched_kb_id": "","matched_kb_name": "","iteration_count": 0,"max_iterations": 5,"scheme_generated": False,"waiting_for_supplement": False,"extracted_info": {}, - "has_rag_result": False,"has_graph_result": False,"is_execution_feedback": False,"execution_detail": "","ask_round": 0,"user_confirmed_info": False,"need_model_confirm": False, - "user_confirmed_model": False,"waiting_feedback": False,"user_feedback": "","waiting_report_confirm": False,"report_confirmed": False,"initialized": False,"current_node": "","user_intent": "","last_combined_query": "",} - result = await app.ainvoke(initial_state, config) - except Exception as e: - print(f"[Checkpointer] 状态恢复失败: {e}") - import traceback - traceback.print_exc() - result = {} - else: - initial_state = {"extracted_text": extracted_text,"combined_query": combined_query,"history_messages": parse_history(history_message),"ship_number": "","device_name": "","fault": "","additional_info": "","fault_code": "","fault_time": "", - "fault_frequency": "","tried_measures": "","running_condition": "","rag_search_result": None,"graph_search_result": None,"response": "","suggestedReplies": [],"actions": [],"error_message": "","agent_decision": "","agent_reasoning": "","tools_to_call": [], - "missing_info": [],"info_completeness": 0.0,"matched_kb_id": "","matched_kb_name": "","iteration_count": 0,"max_iterations": 5,"scheme_generated": False,"waiting_for_supplement": False,"extracted_info": {},"has_rag_result": False,"has_graph_result": False, - "is_execution_feedback": False,"execution_detail": "","ask_round": 0,"user_confirmed_info": False,"need_model_confirm": False,"user_confirmed_model": False,"waiting_feedback": False,"user_feedback": "","waiting_report_confirm": False,"report_confirmed": False,"initialized": False,"current_node": "","user_intent": "","last_combined_query": "",} - - try: - result = await app.ainvoke(initial_state, config) - except Exception as e: - print(f"[工作流执行失败] {str(e)}") - import traceback - traceback.print_exc() - return {"response": f"故障诊断工作流执行失败: {str(e)}","actions": [],"result_tag": "fault_diagnosis","suggestedReplies": [],} - - if result is None: - result = {} - - interrupts = result.get("__interrupt__", []) - if interrupts: - interrupt_data = interrupts[0].value if interrupts else {} - print(f"[Interrupt] 工作流中断,等待用户输入: {interrupt_data}") - return {"response": interrupt_data.get("response", "请提供更多信息"),"actions": interrupt_data.get("actions", []),"result_tag": "fault_diagnosis","suggestedReplies": interrupt_data.get("suggestedReplies", []),"waiting_for": interrupt_data.get("waiting_for", ""),} - - if result.get("error_message"): - return {"response": f"故障诊断工作流执行失败: {result['error_message']}","actions": [],"result_tag": "fault_diagnosis","suggestedReplies": [],} - - route_flag = result.get("route_flag", "") - if not route_flag: - route_flag = "fault_diagnosis" - - return {"response": result.get("response", ""),"actions": result.get("actions", []),"result_tag": route_flag,"suggestedReplies": result.get("suggestedReplies", []),"route_flag": route_flag,} - - diff --git a/workflows/workflow_operate.py b/workflows/workflow_operate.py deleted file mode 100644 index 27dcb90..0000000 --- a/workflows/workflow_operate.py +++ /dev/null @@ -1,1457 +0,0 @@ -""" -操作指导工作流 checkpointer 版本 -""" -from typing import TypedDict, Optional, Dict, Any, List -from langgraph.graph import StateGraph, START, END -from langgraph.types import interrupt, Command -from tools.function_tool import graph_rag_search -from tools.rag_tools import rag_search -from tools.graph_tools import atlas_retrieval, format_atlas_results, extract_entity_names_from_history -from modelsAPI.model_api import OpenaiAPI -from utils.function_tracker import get_all_callbacks -from utils.image_utils import extract_image_paths, get_image_prompt_guidance -from utils.intent_matcher import is_confirmation_intent -from workflows.workflow_baike import run_baike_workflow -from workflows.workflow_utils import ( - safe_json_extract, parse_history, normalize_latex_spacing, - remove_duplicate_content, convert_rag_result, calculate_info_completeness, - emit_callback_event, build_history_str, build_detail_info, - append_atlas_section, stream_format_and_postprocess, -) -from prompts import OPERATE_PROMPTS, COMMON_PROMPTS -import json -import re -import random - -class OperateGuideState(TypedDict): - """操作指导工作流状态""" - extracted_text: str - combined_query: str - history_messages: List[Dict[str, Any]] - - ship_number: str - device_name: str - operation_item: str - additional_info: str - operation_code: str - operation_time: str - operation_frequency: str - tried_measures: str - running_condition: str - - rag_search_result: Optional[List[Dict[str, Any]]] - graph_search_result: Optional[str] - deep_rag_results: Optional[Dict[str, Any]] - - response: str - suggestedReplies: List[Dict[str, Any]] - actions: List[Dict[str, Any]] - error_message: str - - agent_decision: str - agent_reasoning: str - tools_to_call: List[str] - missing_info: List[str] - info_completeness: float - - matched_kb_id: str - matched_kb_name: str - - iteration_count: int - max_iterations: int - scheme_generated: bool - waiting_for_supplement: bool - - extracted_info: Dict[str, Any] - has_rag_result: bool - has_graph_result: bool - ask_round: int - user_confirmed_info: bool - - need_model_confirm: bool - user_confirmed_model: bool - - user_feedback: str - waiting_report_confirm: bool - report_confirmed: bool - - initialized: bool - current_node: str - - # 意图分类相关字段 - user_intent: str # 用户初始意图: '操作', '百科' - - # 后续处理相关字段 - follow_up_intent: str # 用户后续意图: 'not_solved', 'related_question', 'has_error', 'modify_info', 'other' - user_feedback_for_regenerate: str # 用户用于重新生成方案的反馈 - is_regenerating: bool # 是否正在重新生成方案 - last_generated_scheme: str # 最后一次生成的方案(用于基于反馈修改) - scheme_type: str # 方案类型:'normal' 普通方案,'deep' 深度方案 - deep_analysis_points: Optional[List[str]] # 深度分析点(用于针对性检索) - atlas_results: Optional[Dict[str, Any]] # 图册检索结果 - last_combined_query: str # 上一轮生成方案时的用户输入 - -OPERATE_INFO_WEIGHTS = {"ship_number": 0.20, "device_name": 0.20, "operation_item": 0.25, "operation_code": 0.08, "operation_time": 0.06, "operation_frequency": 0.06, "tried_measures": 0.06, "running_condition": 0.05, "additional_info": 0.04} - -def _calculate_info_completeness(ship_number: str = "", device_name: str = "", operation_item: str = "", operation_code: str = "", operation_time: str = "", operation_frequency: str = "", tried_measures: str = "", running_condition: str = "", additional_info: str = "") -> float: - values = {"ship_number": ship_number, "device_name": device_name, "operation_item": operation_item, "operation_code": operation_code, "operation_time": operation_time, "operation_frequency": operation_frequency, "tried_measures": tried_measures, "running_condition": running_condition, "additional_info": additional_info} - return calculate_info_completeness(OPERATE_INFO_WEIGHTS, values) - -async def classify_intent(state: OperateGuideState) -> Dict[str, Any]: - """ - 意图分类节点 - """ - combined_query = state.get("combined_query", "") or "" - extracted_info = state.get("extracted_info", {}) or {} - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - - if extracted_info or device_name or operation_item or ship_number: - print("[classify_intent] 已在操作流程中,跳过意图分类") - return {"user_intent": "操作","current_node": "extract_info",} - - system_prompt = OPERATE_PROMPTS["classify_intent_system"] - - prompt = COMMON_PROMPTS["classify_intent_user"].format(combined_query=combined_query) - - try: - response_text = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,json_output=True,system_prompt=system_prompt,messages=[]) - - parsed = safe_json_extract(response_text) - intent = "操作" - if isinstance(parsed, dict): - intent = parsed.get("intent", "操作") - - print(f"[意图分类] 用户意图: {intent}") - - if intent == "操作": - return {"user_intent": intent,"scheme_generated": False,"current_node": "extract_info",} - else: - return {"user_intent": intent,"current_node": "follow_up_qa",} - - except Exception as e: - print(f"[意图分类失败: {str(e)},默认按操作处理") - return {"user_intent": "操作","scheme_generated": False,"current_node": "extract_info",} - -def _route_after_classify(state: OperateGuideState) -> str: - """意图分类后的路由""" - intent = state.get("user_intent", "操作") - if intent == "百科": - return "follow_up_qa" - return "extract_info" - -async def extract_info(state: OperateGuideState) -> Dict[str, Any]: - """ - 信息提取节点 - """ - if state.get("initialized", False): - print("[extract_info] 已初始化,跳过") - return {"current_node": "agent_think"} - - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - - history_str = build_history_str(history_messages, recent_count=10) - - existing_info = [] - existing_ship_number = state.get("ship_number", "") or "" - existing_device_name = state.get("device_name", "") or "" - existing_operation_item = state.get("operation_item", "") or "" - existing_operation_code = state.get("operation_code", "") or "" - existing_operation_time = state.get("operation_time", "") or "" - existing_operation_frequency = state.get("operation_frequency", "") or "" - existing_tried_measures = state.get("tried_measures", "") or "" - existing_running_condition = state.get("running_condition", "") or "" - existing_additional_info = state.get("additional_info", "") or "" - - if existing_ship_number: - existing_info.append(f"舷号: {existing_ship_number}") - if existing_device_name: - existing_info.append(f"设备名称: {existing_device_name}") - if existing_operation_item: - existing_info.append(f"操作项目: {existing_operation_item}") - if existing_operation_code: - existing_info.append(f"操作代码: {existing_operation_code}") - if existing_operation_time: - existing_info.append(f"操作时间: {existing_operation_time}") - if existing_operation_frequency: - existing_info.append(f"操作频率: {existing_operation_frequency}") - if existing_tried_measures: - existing_info.append(f"已尝试措施: {existing_tried_measures}") - if existing_running_condition: - existing_info.append(f"运行工况: {existing_running_condition}") - if existing_additional_info: - existing_info.append(f"其他信息: {existing_additional_info}") - - existing_info_str = "\n".join(existing_info) if existing_info else "(无已提取信息)" - - system_prompt = OPERATE_PROMPTS["extract_info_system"] - - prompt = COMMON_PROMPTS["extract_info_user"].format( - biz_label="操作", - existing_info_str=existing_info_str, - history_str=history_str or '(无历史对话)', - combined_query=combined_query - ) - - try: - response_text = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,json_output=True,system_prompt=system_prompt,messages=[]) - - parsed = safe_json_extract(response_text) - if not isinstance(parsed, dict): - parsed = {} - - new_ship_number = parsed.get("ship_number", "") or existing_ship_number - new_device_name = parsed.get("device_name", "") or existing_device_name - new_operation_item = parsed.get("operation_item", "") or existing_operation_item - new_operation_code = parsed.get("operation_code", "") or existing_operation_code - new_operation_time = parsed.get("operation_time", "") or existing_operation_time - new_operation_frequency = parsed.get("operation_frequency", "") or existing_operation_frequency - new_tried_measures = parsed.get("tried_measures", "") or existing_tried_measures - new_running_condition = parsed.get("running_condition", "") or existing_running_condition - new_additional_info = parsed.get("additional_info", "") or existing_additional_info - - extracted_info = {"ship_number": new_ship_number,"device_name": new_device_name,"operation_item": new_operation_item,"operation_code": new_operation_code,"operation_time": new_operation_time,"operation_frequency": new_operation_frequency,"tried_measures": new_tried_measures,"running_condition": new_running_condition,"additional_info": new_additional_info,} - - print(f"[信息提取] 舷号={extracted_info['ship_number']}, 设备={extracted_info['device_name']}, 操作={extracted_info['operation_item']}") - - return {**extracted_info,"extracted_info": extracted_info,"initialized": True,"current_node": "agent_think",} - - except Exception as e: - print(f"信息提取失败: {str(e)}") - return {"initialized": True,"current_node": "agent_think",} - -async def agent_think(state: OperateGuideState) -> Dict[str, Any]: - """ - Agent 思考节点:基于当前状态决定下一步动作 - """ - combined_query = state.get("combined_query", "") or "" - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - operation_code = state.get("operation_code", "") or "" - operation_time = state.get("operation_time", "") or "" - operation_frequency = state.get("operation_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - additional_info = state.get("additional_info", "") or "" - history_messages = state.get("history_messages", []) or [] - - scheme_generated = state.get("scheme_generated", False) - has_rag_result = state.get("has_rag_result", False) - has_graph_result = state.get("has_graph_result", False) - - rag_search_result = state.get("rag_search_result") - graph_search_result = state.get("graph_search_result") - iteration_count = state.get("iteration_count", 0) - - user_confirmed_info = state.get("user_confirmed_info", False) - waiting_feedback = state.get("waiting_feedback", False) - - has_ship = bool(ship_number and ship_number.strip()) - has_device = bool(device_name and device_name.strip()) - has_operation_item = bool(operation_item and operation_item.strip()) - has_rag = (rag_search_result is not None and len(rag_search_result) > 0) or has_rag_result - has_graph = (graph_search_result is not None and len(graph_search_result) > 100) or has_graph_result - - info_completeness = _calculate_info_completeness(ship_number=ship_number,device_name=device_name,operation_item=operation_item,operation_code=operation_code,operation_time=operation_time,operation_frequency=operation_frequency,tried_measures=tried_measures,running_condition=running_condition,additional_info=additional_info) - - MIN_COMPLETENESS_FOR_GENERATE = 0.25 - - if scheme_generated: - _re_diagnose_keywords = ["开始操作", "信息已足够", "信息已足够,开始操作", "开始", "操作"] - _is_re_diagnose = any(kw in combined_query for kw in _re_diagnose_keywords) - if _is_re_diagnose: - print(f"[重新操作] 检测到重新操作意图: '{combined_query}',重置状态直接调用工具生成方案") - tools_to_call = ["rag_search"] - if device_name and operation_item: - tools_to_call.append("graph_rag_search") - return {"scheme_generated": False,"rag_search_result": None,"graph_search_result": None,"deep_rag_results": None,"has_rag_result": False,"has_graph_result": False,"is_regenerating": False,"user_feedback_for_regenerate": "","follow_up_intent": "","user_confirmed_info": True,"initialized": True,"iteration_count": 0,"ask_round": 0,"agent_decision": "call_tools","tools_to_call": tools_to_call,"current_node": "call_tools",} - - history_str = build_history_str(history_messages, recent_count=8, assistant_truncate=300) - - intent_system_prompt = OPERATE_PROMPTS["agent_think_intent_system"] - - intent_prompt = OPERATE_PROMPTS["agent_think_intent_user"].format( - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供', - history_str=history_str or '无', - combined_query=combined_query - ) - - try: - intent_response = await OpenaiAPI.open_api_chat_without_thinking( - query=intent_prompt, - model=None, - json_output=True, - system_prompt=intent_system_prompt, - messages=[] - ) - - parsed_intent = safe_json_extract(intent_response) - intent = "other" - if isinstance(parsed_intent, dict): - intent = parsed_intent.get("intent", "other") - - print(f"[Agent 意图分析] 用户意图: {intent}") - - if intent == "modify_info": - decision = "confirm_info" - reasoning = "用户需要修改或补充信息" - return {"agent_decision": decision,"agent_reasoning": reasoning,"rag_search_result": None,"graph_search_result": None,"deep_rag_results": None,"has_rag_result": False,"has_graph_result": False,"scheme_generated": False,"is_regenerating": False,"user_feedback_for_regenerate": "","follow_up_intent": "","user_confirmed_info": False,"initialized": False,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - elif intent == "has_error": - decision = "handle_feedback" - reasoning = "用户反馈方案有错误" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - elif intent == "not_solved": - decision = "analyze_follow_up_intent" - reasoning = "已生成方案,分析用户后续意图" - return {"agent_decision": decision,"agent_reasoning": reasoning,"follow_up_intent": "not_solved","info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - elif intent == "related_question": - decision = "analyze_follow_up_intent" - reasoning = "已生成方案,分析用户后续意图" - return {"agent_decision": decision,"agent_reasoning": reasoning,"follow_up_intent": "related_question","info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - else: - decision = "follow_up_qa" - reasoning = "其他意图,直接调用 baike" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - except Exception as e: - print(f"[Agent 意图分析失败: {str(e)}") - decision = "analyze_follow_up_intent" - reasoning = "已生成方案,分析用户后续意图" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning} (降级)") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - if not has_device or not has_operation_item: - decision = "ask_user" - missing_info = [] - if not has_device: - missing_info.append("设备名称") - if not has_operation_item: - missing_info.append("操作项目") - reasoning = f"缺少基本信息: {', '.join(missing_info)}" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"missing_info": missing_info,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - if info_completeness < MIN_COMPLETENESS_FOR_GENERATE: - decision = "ask_user" - reasoning = f"信息完整性评分 {info_completeness} 低于最低要求 {MIN_COMPLETENESS_FOR_GENERATE}" - missing_info = [] - if not ship_number: - missing_info.append("舷号(可选)") - if not operation_code: - missing_info.append("操作代码") - if not operation_time: - missing_info.append("操作时间") - if not operation_frequency: - missing_info.append("操作频率") - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"missing_info": missing_info or ["更多详细信息"],"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - if has_rag or has_graph: - decision = "generate_response" - reasoning = "信息充足,已有检索结果,直接生成回复" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - if not user_confirmed_info: - decision = "confirm_info" - reasoning = "信息充足,需要用户确认后再检索知识库" - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - return {"agent_decision": decision,"agent_reasoning": reasoning,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - - decision = "call_tools" - reasoning = "信息充足,用户已确认,开始检索知识库" - tools_to_call = ["rag_search"] - if has_device and has_operation_item: - tools_to_call.append("graph_rag_search") - - print(f"[Agent 决策] decision={decision}, reasoning={reasoning}") - print(f"[Agent 提取] 舷号={ship_number}, 设备={device_name}, 操作={operation_item}") - - emit_callback_event(["🤖 Agent 思考中", "🤖 智能分析", "🤖 决策判断"], f"决策: {decision} | 理由: {reasoning[:50]}...") - - return {"agent_decision": decision,"agent_reasoning": reasoning,"tools_to_call": tools_to_call,"info_completeness": info_completeness,"iteration_count": iteration_count + 1,"current_node": decision,} - -async def ask_user(state: OperateGuideState) -> Command: - """ - 询问用户节点:使用 interrupt 暂停等待用户输入 - """ - missing_info = state.get("missing_info", []) - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - operation_code = state.get("operation_code", "") or "" - operation_time = state.get("operation_time", "") or "" - operation_frequency = state.get("operation_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - additional_info = state.get("additional_info", "") or "" - combined_query = state.get("combined_query", "") or "" - info_completeness = state.get("info_completeness", 0) - ask_round = state.get("ask_round", 0) - - MAX_ASK_ROUNDS = 4 - if ask_round >= MAX_ASK_ROUNDS: - print(f"[ask_user] 已达到最大询问轮次 {MAX_ASK_ROUNDS},强制进入下一步") - tools_to_call = ["rag_search"] - if device_name and operation_item: - tools_to_call.append("graph_rag_search") - return Command( - update={"ask_round": ask_round + 1,"current_node": "call_tools","tools_to_call": tools_to_call,}, - goto="call_tools" - ) - - existing_parts = [] - if ship_number: - existing_parts.append(f"舷号: {ship_number}") - if device_name: - existing_parts.append(f"设备名称: {device_name}") - if operation_item: - existing_parts.append(f"操作项目: {operation_item}") - if operation_code: - existing_parts.append(f"操作代码: {operation_code}") - if operation_time: - existing_parts.append(f"发生时间: {operation_time}") - if operation_frequency: - existing_parts.append(f"操作频率: {operation_frequency}") - if tried_measures: - existing_parts.append(f"已尝试措施: {tried_measures}") - if running_condition: - existing_parts.append(f"运行工况: {running_condition}") - if additional_info: - existing_parts.append(f"其他信息: {additional_info}") - - existing_str = ";".join(existing_parts) if existing_parts else "暂无" - missing_str = "、".join(missing_info) if missing_info else "相关信息" - - system_prompt = COMMON_PROMPTS["ask_user_system"].format(biz_label="操作指导", biz_label_short="指导") - - prompt = COMMON_PROMPTS["ask_user_prompt"].format( - info_completeness=info_completeness, - existing_str=existing_str, - missing_str=missing_str, - combined_query=combined_query - ) - - response = "" - try: - response = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,system_prompt=system_prompt,messages=[]) - response = response.strip() if response else "" - except Exception as e: - print(f"[ask_user] API调用失败: {str(e)}") - response = "" - - if not response: - response = f"**【需要更多信息】**\n\n请补充:{missing_str}\n\n提供这些信息后,我可以更准确地为您指导操作。" - - if existing_parts: - existing_info_section = f"\n**📋 已获取的信息:**\n\n" + "\n".join( - f"- {part}" for part in existing_parts) + "\n\n---\n" - response = existing_info_section + response - - suggested_replies = [] - if "舷号" in missing_info: - suggested_replies.append({"title": "补充舷号", "content": "舷号:"}) - if "设备名称" in missing_info: - suggested_replies.append({"title": "补充设备名称", "content": "设备名称:"}) - if "操作项目" in missing_info: - suggested_replies.append({"title": "补充操作项目", "content": "操作项目:"}) - - user_input = interrupt({"response": response,"suggestedReplies": suggested_replies,"waiting_for": "user_input","missing_info": missing_info,}) - - user_query = user_input.get("query", "") - - update_dict = {"combined_query": user_query,"current_node": "agent_think","ask_round": ask_round + 1,} - - if user_query: - extract_prompt = OPERATE_PROMPTS["ask_user_extract"].format( - missing_str=missing_str, - user_query=user_query - ) - - try: - extract_response = await OpenaiAPI.open_api_chat_without_thinking(query=extract_prompt,model=None,json_output=True,system_prompt="你是信息提取专家。",messages=[]) - extracted = safe_json_extract(extract_response) - if isinstance(extracted, dict): - if extracted.get("ship_number"): - update_dict["ship_number"] = extracted["ship_number"] - print(f"[ask_user] 提取到舷号: {extracted['ship_number']}") - if extracted.get("device_name"): - update_dict["device_name"] = extracted["device_name"] - print(f"[ask_user] 提取到设备名称: {extracted['device_name']}") - if extracted.get("operation_item"): - update_dict["operation_item"] = extracted["operation_item"] - print(f"[ask_user] 提取到操作项目: {extracted['operation_item']}") - except Exception as e: - print(f"[ask_user] 信息提取失败: {str(e)}") - - return Command( - update=update_dict, - goto="agent_think" - ) - -async def confirm_info(state: OperateGuideState) -> Command: - """ - 确认信息节点:重点询问用户是否需要补充信息 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - operation_code = state.get("operation_code", "") or "" - operation_time = state.get("operation_time", "") or "" - operation_frequency = state.get("operation_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - additional_info = state.get("additional_info", "") or "" - info_completeness = state.get("info_completeness", 0) - - existing_parts = [] - if ship_number: - existing_parts.append(f"舷号: {ship_number}") - if device_name: - existing_parts.append(f"设备名称: {device_name}") - if operation_item: - existing_parts.append(f"操作项目: {operation_item}") - if operation_code: - existing_parts.append(f"操作代码: {operation_code}") - if operation_time: - existing_parts.append(f"发生时间: {operation_time}") - if operation_frequency: - existing_parts.append(f"操作频率: {operation_frequency}") - if tried_measures: - existing_parts.append(f"已尝试措施: {tried_measures}") - if running_condition: - existing_parts.append(f"运行工况: {running_condition}") - if additional_info: - existing_parts.append(f"其他信息: {additional_info}") - - existing_str = "\n".join(f"- {part}" for part in existing_parts) if existing_parts else "暂无" - - additional_prompt = "" - if not ship_number: - additional_prompt = "\n\n💡 **提示**:如果您知道舷号,请补充,这将有助于我们更准确地提供操作指导。" - - response = f"""**📋 已收集到的信息:** - -{existing_str} - -**信息完整性评分:** {info_completeness:.0%} - ---- - -**💡 请问是否需要补充其他信息?** -- 如操作代码、操作时间、操作频率、已尝试措施、运行工况等 -- 如果信息已足够,请点击"开始操作"{additional_prompt} -""" - - emit_callback_event(["✅ 信息确认", "📋 请确认信息", "🔍 准备检索"], f"等待用户确认或补充信息 (完整性: {info_completeness:.0%})") - - suggested_replies = [{"title": "开始操作", "content": "信息已足够,开始操作"},{"title": "补充信息", "content": "我需要补充:"}] - - user_input = interrupt({"response": response,"suggestedReplies": suggested_replies,"waiting_for": "user_confirm",}) - - user_query = user_input.get("query", "") - - if await is_confirmation_intent(user_query): - print(f"[confirm_info] 用户确认信息足够,准备调用工具") - tools_to_call = ["rag_search"] - if device_name and operation_item: - tools_to_call.append("graph_rag_search") - return Command( - update={"user_confirmed_info": True,"current_node": "call_tools","tools_to_call": tools_to_call,}, - goto="call_tools" - ) - else: - print(f"[confirm_info] 用户需要补充信息: {user_query}") - return Command( - update={"user_confirmed_info": False,"combined_query": user_query,"initialized": False,"current_node": "extract_info",}, - goto="extract_info" - ) - -async def call_tools(state: OperateGuideState) -> Dict[str, Any]: - """调用检索工具""" - tools_to_call = state.get("tools_to_call", []) - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - additional_info = state.get("additional_info", "") or "" - - rag_results = state.get("rag_search_result") - graph_results = state.get("graph_search_result") - atlas_results = {} - - matched_kb_name = "" - matched_kb_id = "" - - if "rag_search" in tools_to_call and rag_results is None: - from utils.ship_number_search import search_with_ship_number_strategy - - base_query = f"{device_name}进行{operation_item}的操作方案" - - rag_results, matched_kb_name, matched_kb_id, _mapped_ship_number = await search_with_ship_number_strategy(base_query=base_query,ship_number=ship_number,top_k=5,search_type="operate") - - if not matched_kb_name: - matched_kb_name = "通用知识库" - - print(f"RAG 检索来源: {matched_kb_name}") - - if "graph_rag_search" in tools_to_call and graph_results is None: - if device_name and operation_item: - try: - graph_query = f"{device_name}进行{operation_item}的操作方案" - graph_results = await graph_rag_search.ainvoke({"query": graph_query}) - print(f"图谱检索完成,结果长度: {len(graph_results) if graph_results else 0}") - except Exception as e: - print(f"图谱检索失败: {str(e)}") - graph_results = "" - - if device_name: - try: - print(f"[图册检索] 使用设备名称: {device_name}") - atlas_result = await atlas_retrieval(node_names=[device_name], top_k=10) - if atlas_result.get("success", False): - atlas_results = atlas_result.get("data", {}) - print(f"[图册检索] 图册检索结果: {list(atlas_results.keys())}") - except Exception as e: - print(f"[图册检索] 图册检索失败: {str(e)}") - - has_rag = bool(rag_results and len(rag_results) > 0) - has_graph = bool(graph_results and len(graph_results) > 100) - - if not has_rag and not has_graph: - print(f"[call_tools] 未找到任何相关资料,需要用户确认使用模型能力") - return {"rag_search_result": rag_results if isinstance(rag_results, list) else [],"graph_search_result": graph_results if isinstance(graph_results, str) else "","atlas_results": atlas_results,"matched_kb_id": matched_kb_id,"matched_kb_name": matched_kb_name,"has_rag_result": False,"has_graph_result": False,"need_model_confirm": True,"current_node": "model_confirm",} - - return {"rag_search_result": rag_results,"graph_search_result": graph_results,"atlas_results": atlas_results,"matched_kb_id": matched_kb_id,"matched_kb_name": matched_kb_name,"has_rag_result": has_rag,"has_graph_result": has_graph,"need_model_confirm": False,"current_node": "generate_response",} - -async def model_confirm(state: OperateGuideState) -> Command: - """ - 模型能力确认节点:当没有找到任何资料时,让用户确认是否使用模型本身能力 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - - response = f"""**⚠️ 未找到相关资料** - -抱歉,在知识库中未找到与以下信息相关的资料: -- 舷号:{ship_number or '未提供'} -- 设备名称:{device_name or '未提供'} -- 操作项目:{operation_item or '未提供'} - -**💡 您可以选择:** -- 使用 AI 模型的知识库为您生成一般性建议 -- 提供更详细的信息重新检索 - -请问是否使用 AI 模型为您生成建议?""" - - emit_callback_event(["🤖 AI 模型确认", "⚠️ 资料未找到", "💡 使用模型能力"], "使用模型自身知识进行回复") - - suggested_replies = [{"title": "使用 AI 模型", "content": "确认,使用 AI 模型生成建议"},{"title": "补充信息", "content": "我需要补充更多信息:"},] - - user_input = interrupt({"response": response,"suggestedReplies": suggested_replies,"waiting_for": "model_confirm",}) - - user_query = user_input.get("query", "") - - if "确认" in user_query or "AI" in user_query or "模型" in user_query: - print(f"[model_confirm] 用户确认使用模型能力") - return Command( - update={"user_confirmed_model": True,"combined_query": user_query,"current_node": "generate_response",}, - goto="generate_response" - ) - else: - print(f"[model_confirm] 用户选择补充信息: {user_query}") - return Command( - update={"user_confirmed_model": False,"combined_query": user_query,"current_node": "agent_think",}, - goto="agent_think" - ) - -async def generate_response(state: OperateGuideState) -> Dict[str, Any]: - """生成最终回复""" - print("========================生成最终回复========================") - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - additional_info = state.get("additional_info", "") or "" - operation_code = state.get("operation_code", "") or "" - operation_time = state.get("operation_time", "") or "" - operation_frequency = state.get("operation_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - rag_results = state.get("rag_search_result") or [] - graph_results = state.get("graph_search_result") or "" - atlas_results = state.get("atlas_results", {}) - scheme_generated = state.get("scheme_generated", False) - matched_kb_name = state.get("matched_kb_name", "") - - print("rag_results", rag_results) - history_str = build_history_str(history_messages, recent_count=12, assistant_truncate=500) - - has_rag = rag_results and len(rag_results) > 0 - has_graph = graph_results and len(graph_results) > 100 - - if not has_rag and not has_graph: - if not ship_number and not device_name and not operation_item: - system_prompt = COMMON_PROMPTS["generate_response_friendly_system"].format(biz_label="操作指导") - prompt = COMMON_PROMPTS["generate_response_friendly_user"].format(combined_query=combined_query) - else: - system_prompt = COMMON_PROMPTS["generate_response_no_rag_system"].format(biz_label="操作指导") - prompt = OPERATE_PROMPTS["generate_response_no_rag_user"].format( - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供', - operation_code=operation_code or '未提供', - operation_time=operation_time or '未提供', - operation_frequency=operation_frequency or '未提供', - tried_measures=tried_measures or '未提供', - running_condition=running_condition or '未提供', - additional_info=additional_info or '无' - ) - - try: - response = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,system_prompt=system_prompt,messages=[]) - return {"response": response.strip() if response else "抱歉,未能找到相关资料。","suggestedReplies": [{"title": "提供更详细信息", "content": "我来提供更详细的信息"},],"current_node": "end",} - except Exception as e: - return {"response": f"抱歉,处理过程中出现错误: {str(e)}","suggestedReplies": [],"current_node": "end",} - - rag_ctx_parts: List[str] = [] - if isinstance(rag_results, str) and rag_results.strip(): - rag_ctx_parts = [rag_results.strip()] - elif isinstance(rag_results, list): - for item in sorted(rag_results or [], key=lambda x: float(x.get("score") or 0) if isinstance(x,dict) else 0, reverse=True)[:4]: -# for item in rag_results[:8]: - if isinstance(item, dict): - text = (item.get("text") or "").strip() - if text: - rag_ctx_parts.append(text) - elif isinstance(item, str) and item.strip(): - rag_ctx_parts.append(item.strip()) - - if len(graph_results) <= 50: - graph_results = "" - - all_source_text = graph_results + "\n" + "\n".join(rag_ctx_parts) if graph_results else "\n".join(rag_ctx_parts) - original_image_paths = extract_image_paths(all_source_text) - print("提取到的原始图片路径:", original_image_paths) - - rag_answer = "" - rag_type = "none" - - if graph_results and len(graph_results) > 50: - rag_answer = graph_results - rag_type = "graph" - elif rag_ctx_parts: - rag_answer = "\n\n".join(rag_ctx_parts) - rag_type = "rag" - - if not rag_answer or rag_answer == "NO_RELEVANT_RESULT": - system_prompt = COMMON_PROMPTS["generate_response_no_rag_simple_system"].format(biz_label="操作指导") - prompt = OPERATE_PROMPTS["generate_response_no_rag_simple_user"].format( - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供' - ) - - try: - response = await OpenaiAPI.open_api_chat_without_thinking(query=prompt,model=None,system_prompt=system_prompt,messages=[]) - return {"response": response.strip() if response else "抱歉,未能找到相关资料。","suggestedReplies": [],"current_node": "end",} - except Exception as e: - return {"response": f"抱歉,处理过程中出现错误: {str(e)}","suggestedReplies": [],"current_node": "end",} - - rag_type = "图谱" if rag_type == "graph" else "文本" - - emit_callback_event([f"✨当前使用的检索类型为{rag_type}", f"✨本次检索策略为{rag_type}"], f"内容为{rag_answer[:50]}...") - - detail_info = "" - if operation_code: - detail_info += f"- 操作代码/报警代码:{operation_code}\n" - if operation_time: - detail_info += f"- 操作发生时间:{operation_time}\n" - if operation_frequency: - detail_info += f"- 操作频率/持续时间:{operation_frequency}\n" - if tried_measures: - detail_info += f"- 已尝试的操作措施:{tried_measures}\n" - if running_condition: - detail_info += f"- 设备运行工况:{running_condition}\n" - if additional_info: - detail_info += f"- 其他相关信息:{additional_info}\n" - - is_regenerating = state.get("is_regenerating", False) - user_feedback_for_regenerate = state.get("user_feedback_for_regenerate", "") - is_follow_up = scheme_generated and (has_rag or has_graph) and not is_regenerating - - image_guidance = get_image_prompt_guidance() - if is_regenerating and user_feedback_for_regenerate: - prompt = COMMON_PROMPTS["generate_response_regenerating"].format( - biz_label="操作指导", - item_label="操作项目", - item_name=operation_item or '未提供', - item_action="分析操作要点,给出操作指导方案", - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供', - detail_info=detail_info, - user_feedback_for_regenerate=user_feedback_for_regenerate, - rag_answer=rag_answer, - image_guidance=image_guidance - ) - elif is_follow_up: - prompt = COMMON_PROMPTS["generate_response_follow_up"].format( - biz_label="操作指导", - item_label="操作项目", - item_name=operation_item or '未提供', - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供', - detail_info=detail_info, - rag_answer=rag_answer, - combined_query=combined_query, - image_guidance=image_guidance - ) - else: - prompt = COMMON_PROMPTS["generate_response_normal"].format( - biz_label="操作指导", - item_label="操作项目", - item_name=operation_item or '未提供', - item_action="分析操作要点,给出操作指导方案", - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供', - detail_info=detail_info, - rag_answer=rag_answer - ) - - try: - content_system_prompt = COMMON_PROMPTS["generate_response_content_system"].format(biz_label="操作指导") - - raw_content = await OpenaiAPI.open_api_chat_without_thinking(model=None,system_prompt=content_system_prompt,messages=[{"role": "user", "content": prompt}],) - - raw_content = raw_content.strip() if raw_content else "抱歉,无法生成回答。" - - emit_callback_event(["🤖 操作分析中", "🤖 方案生成中"], raw_content[:30] + "...") - - answer = await stream_format_and_postprocess(raw_content, original_image_paths, temperature=0.1) -# answer = "" - answer = append_atlas_section(answer, atlas_results) - - suggested_replies = [{"title": "方案有误", "content": "方案还是有问题:"},] - - return {"response": answer,"actions": [],"suggestedReplies": suggested_replies,"scheme_generated": True,"last_generated_scheme": answer,"scheme_type": "deep","current_node": "end","last_combined_query": combined_query,} - - except Exception as e: - return {"response": f"深度分析生成失败: {str(e)}","actions": [],"suggestedReplies": [],"current_node": "end",} - -async def regenerate_scheme_from_feedback(state: OperateGuideState) -> Dict[str, Any]: - """ - 基于用户反馈直接修改方案节点:使用之前保存的方案和用户反馈进行修改 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - additional_info = state.get("additional_info", "") or "" - operation_code = state.get("operation_code", "") or "" - operation_time = state.get("operation_time", "") or "" - operation_frequency = state.get("operation_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - combined_query = state.get("combined_query", "") or "" - - user_feedback = state.get("user_feedback_for_regenerate", "") or "" - last_scheme = state.get("last_generated_scheme", "") or "" - scheme_type = state.get("scheme_type", "normal") - - if not last_scheme: - print("[regenerate_scheme_from_feedback] 没有保存的方案,回退到重新检索") - return {"is_regenerating": True,"rag_search_result": None,"graph_search_result": None,"has_rag_result": False,"has_graph_result": False,"scheme_generated": False,"current_node": "call_tools","tools_to_call": ["rag_search", "graph_rag_search"],} - - detail_info = "" - if operation_code: - detail_info += f"- 操作代码/报警代码:{operation_code}\n" - if operation_time: - detail_info += f"- 操作发生时间:{operation_time}\n" - if operation_frequency: - detail_info += f"- 操作频率/持续时间:{operation_frequency}\n" - if tried_measures: - detail_info += f"- 已尝试的操作措施:{tried_measures}\n" - if running_condition: - detail_info += f"- 设备运行工况:{running_condition}\n" - if additional_info: - detail_info += f"- 其他相关信息:{additional_info}\n" - - emit_callback_event(["✏️ 修改方案中", "🔧 调整方案", "📝 优化方案"], f"根据反馈修改方案: {user_feedback[:30]}...") - - image_guidance = get_image_prompt_guidance() - - prompt = COMMON_PROMPTS["regenerate_scheme_from_feedback"].format( - biz_label="操作指导", - item_label="操作项目", - item_name=operation_item or '未提供', - item_action="分析操作要点,给出操作指导方案", - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供', - detail_info=detail_info, - last_scheme=last_scheme, - user_feedback=user_feedback, - image_guidance=image_guidance - ) - - try: - content_system_prompt = COMMON_PROMPTS["regenerate_scheme_from_feedback_system"].format(biz_label="操作指导") - - content_prompt = prompt.replace("【输出要求】", "【输出要求】\n- 请专注于内容的准确性、完整性和逻辑性,不要担心格式的美观性\n- 可以适当使用简单的段落分隔,但不要使用复杂的格式") - - raw_content = await OpenaiAPI.open_api_chat_without_thinking(model=None,system_prompt=content_system_prompt,messages=[{"role": "user", "content": content_prompt}],) - - raw_content = raw_content.strip() if raw_content else "抱歉,无法根据反馈修改方案。" - - original_image_paths = extract_image_paths(last_scheme) - answer = await stream_format_and_postprocess(raw_content, original_image_paths, temperature=0.3, content_label="操作指导方案") - - suggested_replies = [{"title": "方案有误", "content": "方案还是有问题:"},] - - return {"response": answer,"actions": [],"suggestedReplies": suggested_replies,"scheme_generated": True,"last_generated_scheme": answer,"scheme_type": scheme_type,"current_node": "end","last_combined_query": combined_query,} - - except Exception as e: - print(f"[regenerate_scheme_from_feedback] 修改方案失败: {str(e)}") - return {"is_regenerating": True,"rag_search_result": None,"graph_search_result": None,"has_rag_result": False,"has_graph_result": False,"scheme_generated": False,"current_node": "call_tools","tools_to_call": ["rag_search", "graph_rag_search"],} - -async def follow_up_qa(state: OperateGuideState) -> Dict[str, Any]: - """ - 后续问答节点:调用 baike 智能体处理用户的后续问题 - """ - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - - history_message_json = json.dumps(history_messages, ensure_ascii=False) if history_messages else "" - - context_prefix = "" - if ship_number or device_name or operation_item: - context_parts = [] - if ship_number: - context_parts.append(f"舷号: {ship_number}") - if device_name: - context_parts.append(f"设备名称: {device_name}") - if operation_item: - context_parts.append(f"操作项目: {operation_item}") - context_prefix = f"【当前操作指导上下文】\n{'; '.join(context_parts)}\n\n" - - enhanced_query = f"{context_prefix}用户后续问题: {combined_query}" - - emit_callback_event(["💬 后续问答", "💬 执行指导", "💬 问题解答"], f"调用 百科 智能体: {combined_query[:30]}...") - - try: - baike_result = await run_baike_workflow(extracted_text=enhanced_query,combined_query=enhanced_query,history_message=history_message_json,route_flag="baike") - - response = baike_result.get("response", "") - suggested_replies = baike_result.get("suggestedReplies", []) - - return {"response": response,"suggestedReplies": suggested_replies,"actions": [],"scheme_generated": True,"current_node": "end",} - - except Exception as e: - return {"response": f"处理您的问题时出现错误: {str(e)}","suggestedReplies": [],"actions": [],"current_node": "end",} - -async def handle_feedback(state: OperateGuideState) -> Command: - """ - 处理用户反馈节点:接收用户对方案的反馈,提供上报和重新生成方案按钮 - """ - combined_query = state.get("combined_query", "") or "" - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - - response = f"""**📝 已收到您的反馈** - -感谢您对操作指导方案的建议!您的反馈已记录: - -{combined_query} - ---- - -**📋 反馈信息摘要:** -- 舷号:{ship_number or '未提供'} -- 设备名称:{device_name or '未提供'} -- 操作项目:{operation_item or '未提供'} -- 反馈内容:{combined_query[:100]}{'...' if len(combined_query) > 100 else ''} - ---- - -**💡 您可以选择:** -- 点击「重新生成方案」根据您的反馈重新生成方案 -- 点击「上报」将反馈提交给相关部门 -- 继续对话获取更多帮助""" - - actions = [ - {"type": "content","label": "重新生成方案","text": "重新生成方案"}, - {"type": "content","label": "上报","text": "上报"} - ] - - suggested_replies = [] - - emit_callback_event(["📝 反馈处理", "📋 方案反馈", "✅ 反馈确认"], f"处理用户反馈: {combined_query[:30]}...") - - user_input = interrupt({"response": response,"actions": actions,"suggestedReplies": suggested_replies,"waiting_for": "feedback_confirm",}) - - user_text = user_input.get("query", "") or "" - - if user_text == "重新生成方案": - print(f"[handle_feedback] 用户选择重新生成方案") - return Command( - update={"user_feedback_for_regenerate": combined_query,"current_node": "regenerate_scheme_from_feedback",}, - goto="regenerate_scheme_from_feedback" - ) - elif user_text == "上报": - print(f"[handle_feedback] 用户选择上报反馈") - return Command( - update={"user_feedback": combined_query,"waiting_report_confirm": True,"current_node": "confirm_report",}, - goto="confirm_report" - ) - else: - print(f"[handle_feedback] 用户继续对话: {user_text}") - return Command( - update={"user_feedback": combined_query,"combined_query": user_text,"current_node": "agent_think",}, - goto="agent_think" - ) - -async def confirm_report(state: OperateGuideState) -> Dict[str, Any]: - """ - 上报确认节点:用户点击上报后确认完成 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - user_feedback = state.get("user_feedback", "") or "" - - response = f"""**✅ 上报成功** - -您的反馈已成功上报! - ---- - -**📋 上报信息:** -- 舷号:{ship_number or '未提供'} -- 设备名称:{device_name or '未提供'} -- 操作项目:{operation_item or '未提供'} -- 反馈内容:{user_feedback[:100]}{'...' if len(user_feedback) > 100 else ''} - ---- - -**💡 感谢您的反馈,相关部门将及时处理。**""" - - suggested_replies = [{"title": "继续对话", "content": "我还有其他问题"},] - - emit_callback_event(["✅ 上报成功", "📋 反馈已提交", "🎉 完成"], "反馈上报成功") - - return {"response": response,"actions": [],"suggestedReplies": suggested_replies,"report_confirmed": True,"waiting_report_confirm": False,"current_node": "end",} - -async def analyze_follow_up_intent(state: OperateGuideState) -> Dict[str, Any]: - """ - 分析用户后续意图节点:直接使用 agent_think 已经判断好的意图 - agent_think 已经用大模型判断并设置了 follow_up_intent - """ - intent = state.get("follow_up_intent", "other") - - print(f"[意图分析] 使用已判断的意图: {intent}") - - return {"follow_up_intent": intent,} - - -async def deep_rag_search(state: OperateGuideState) -> Dict[str, Any]: - """ - 深层次RAG检索节点:分析之前方案并选择深度分析点进行检索 - """ - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - operation_code = state.get("operation_code", "") or "" - additional_info = state.get("additional_info", "") or "" - operation_time = state.get("operation_time", "") or "" - operation_frequency = state.get("operation_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - last_generated_scheme = state.get("last_generated_scheme", "") or "" - - deep_results = {} - deep_analysis_points = [] - - emit_callback_event(["🔍 深度检索中", "🔍 多维度检索", "🔍 精细分析"], "分析问题并选择深度分析点") - - # ========== 第一步:分析之前方案和用户问题,选择深度分析点 ========== - if last_generated_scheme or combined_query: - try: - history_str = build_history_str(history_messages, recent_count=8, assistant_truncate=300) - - detail_info = "" - if operation_code: - detail_info += f"- 操作代码:{operation_code}\n" - if operation_time: - detail_info += f"- 操作时间:{operation_time}\n" - if operation_frequency: - detail_info += f"- 操作频率/持续时间:{operation_frequency}\n" - if tried_measures: - detail_info += f"- 已尝试的措施:{tried_measures}\n" - if running_condition: - detail_info += f"- 设备运行工况:{running_condition}\n" - if additional_info: - detail_info += f"- 其他相关信息:{additional_info}\n" - - analysis_system_prompt = COMMON_PROMPTS["deep_rag_analysis_system"].format(biz_label="操作指导") - - analysis_prompt = COMMON_PROMPTS["deep_rag_analysis_user"].format( - item_label="操作项目", - item_name=operation_item or '未提供', - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供', - detail_info=detail_info, - history_str=history_str or '(无历史对话)', - last_generated_scheme=last_generated_scheme or '(无之前的方案)', - combined_query=combined_query - ) - - analysis_response = await OpenaiAPI.open_api_chat_without_thinking(query=analysis_prompt,model=None,json_output=True,system_prompt=analysis_system_prompt,messages=[]) - print("深度分析点", analysis_response) - parsed_analysis = safe_json_extract(analysis_response) - deep_analysis_points = parsed_analysis - print(f"[深度RAG] 选择的分析点: {deep_analysis_points}") - except Exception as e: - print(f"[深度RAG] 分析失败: {str(e)}") - - # ========== 第二步:对选中的分析点进行检索 ========== - for idx, point in enumerate(deep_analysis_points): - try: - print(point) - search_query = point - if device_name and device_name not in point: - search_query = f"{device_name} {search_query}" - if operation_item and operation_item not in search_query: - search_query = f"{search_query} {operation_item}" - - rag_result = await rag_search(search_query, top_k=3) - if rag_result.get("success", False): - deep_results[f"analysis_point_{idx}"] = convert_rag_result(rag_result) - else: - deep_results[f"analysis_point_{idx}"] = [] - except Exception as e: - print(f"[深度RAG] 分析点 {point} 检索失败: {str(e)}") - deep_results[f"analysis_point_{idx}"] = [] - - # ========== 第三步:图谱检索 ========== - graph_deep_results = "" - try: - if device_name and operation_item: - graph_query = f"设备{device_name},操作{operation_item}的操作指导方案" - graph_deep_results = await graph_rag_search.ainvoke({"query": graph_query}) - if graph_deep_results: - print(f"[深度RAG] 图谱检索结果长度: {len(graph_deep_results)}") - except Exception as e: - print(f"[深度RAG] 图谱检索失败: {str(e)}") - - # ========== 第四步:图册检索 ========== - atlas_results = {} - try: - entity_names = await extract_entity_names_from_history(history_messages=history_messages,device_name=device_name,operation_item=operation_item,last_generated_scheme=last_generated_scheme) - - if entity_names: - print(f"[深度RAG] 图册检索实体: {entity_names}") - atlas_result = await atlas_retrieval(node_names=entity_names, top_k=10) - if atlas_result.get("success", False): - atlas_results = atlas_result.get("data", {}) - print(f"[深度RAG] 图册检索结果: {list(atlas_results.keys())}") - except Exception as e: - print(f"[深度RAG] 图册检索失败: {str(e)}") - - return {"deep_rag_results": deep_results,"deep_analysis_points": deep_analysis_points,"graph_search_result": graph_deep_results or (state.get("graph_search_result", "") or ""),"atlas_results": atlas_results,"current_node": "generate_deep_response",} - - -async def generate_deep_response(state: OperateGuideState) -> Dict[str, Any]: - """ - 生成深度响应节点:基于深度RAG结果进行深度润色和推理 - """ - combined_query = state.get("combined_query", "") or "" - history_messages = state.get("history_messages", []) or [] - ship_number = state.get("ship_number", "") or "" - device_name = state.get("device_name", "") or "" - operation_item = state.get("operation_item", "") or "" - operation_code = state.get("operation_code", "") or "" - operation_time = state.get("operation_time", "") or "" - operation_frequency = state.get("operation_frequency", "") or "" - tried_measures = state.get("tried_measures", "") or "" - running_condition = state.get("running_condition", "") or "" - additional_info = state.get("additional_info", "") or "" - - deep_rag_results = state.get("deep_rag_results", {}) - deep_analysis_points = state.get("deep_analysis_points", []) - atlas_results = state.get("atlas_results", {}) - graph_results = state.get("graph_search_result", "") or "" - original_rag_results = state.get("rag_search_result", []) - - # 整理深度分析点资料 - analysis_points_text = "" - if deep_analysis_points: - for idx, point in enumerate(deep_analysis_points): - key = f"analysis_point_{idx}" - results = deep_rag_results.get(key, []) - if results: - analysis_points_text += f"\n【分析点:{point}】\n" - for i, item in enumerate(results[:2], 1): - if isinstance(item, dict): - text = item.get("text", "") - if text: - analysis_points_text += f"[{i}] {text}\n" - - # 整理原始RAG结果 - original_rag_text = "" - if original_rag_results: - original_rag_text = "\n【原始检索资料】\n" - for i, item in enumerate(original_rag_results[:3], 1): - if isinstance(item, dict): - text = item.get("text", "") - if text: - original_rag_text += f"[{i}] {text}\n" - - # 整理图册资料 - atlas_text = format_atlas_results(atlas_results) - - # 提取所有资料中的图片路径 - all_source_text = analysis_points_text + original_rag_text + atlas_text + graph_results - original_image_paths = extract_image_paths(all_source_text) - - detail_info = "" - if operation_code: - detail_info += f"- 操作代码:{operation_code}\n" - if operation_time: - detail_info += f"- 操作时间:{operation_time}\n" - if operation_frequency: - detail_info += f"- 操作频率/持续时间:{operation_frequency}\n" - if tried_measures: - detail_info += f"- 已尝试的措施:{tried_measures}\n" - if running_condition: - detail_info += f"- 设备运行工况:{running_condition}\n" - if additional_info: - detail_info += f"- 其他相关信息:{additional_info}\n" - - # 构建深度分析点的提示 - analysis_points_prompt = "" - if deep_analysis_points: - analysis_points_prompt = f"\n【本次深度分析重点】\n" + "\n".join([f"- {p}" for p in deep_analysis_points]) + "\n" - - prompt = COMMON_PROMPTS["generate_deep_response"].format( - biz_label="操作指导", - item_label="操作项目", - item_name=operation_item or '未提供', - ship_number=ship_number or '未提供', - device_name=device_name or '未提供', - operation_item=operation_item or '未提供', - detail_info=detail_info, - combined_query=combined_query, - analysis_points_prompt=analysis_points_prompt, - analysis_points_text=analysis_points_text, - original_rag_text=original_rag_text, - graph_results=graph_results if len(graph_results) > 50 else '(无)' - ) - - try: - content_system_prompt = COMMON_PROMPTS["generate_deep_response_content_system"].format(biz_label="操作指导") - - raw_content = await OpenaiAPI.open_api_chat_without_thinking(model=None,system_prompt=content_system_prompt,messages=[{"role": "user", "content": prompt}],) - - raw_content = raw_content.strip() if raw_content else "抱歉,无法生成深度分析。" - - emit_callback_event(["🤖 分析中", "🤖 操作指导生成中"], raw_content[:30] + "...") - - answer = await stream_format_and_postprocess(raw_content, original_image_paths, temperature=0.1) - - answer = append_atlas_section(answer, atlas_results) - - suggested_replies = [{"title": "方案有误", "content": "方案还是有问题:"},] - - return {"response": answer,"actions": [],"suggestedReplies": suggested_replies,"scheme_generated": True,"last_generated_scheme": answer,"scheme_type": "deep","current_node": "end","last_combined_query": combined_query,} - - except Exception as e: - return {"response": f"深度分析生成失败: {str(e)}","actions": [],"suggestedReplies": [],"current_node": "end",} - - -def _route_after_think(state: OperateGuideState) -> str: - """Agent 思考后的路由""" - decision = state.get("agent_decision", "ask_user") - - return decision - -def _route_after_intent(state: OperateGuideState) -> str: - """意图分析后的路由""" - intent = state.get("follow_up_intent", "other") - - if intent == "not_solved": - return "deep_rag_search" - elif intent == "related_question": - return "follow_up_qa" - elif intent == "has_error": - return "handle_feedback" - elif intent == "modify_info": - return "extract_info" - else: - return "follow_up_qa" - -def _route_after_tools(state: OperateGuideState) -> str: - """检索工具调用后的路由""" - need_model_confirm = state.get("need_model_confirm", False) - if need_model_confirm: - return "model_confirm" - return "generate_response" - -def create_operate_workflow(checkpointer=None): - """ - 创建操作指导工作流(基于 Checkpointer 的状态持久化版本) - """ - workflow = StateGraph(OperateGuideState) - - workflow.add_node("classify_intent", classify_intent) - workflow.add_node("extract_info", extract_info) - workflow.add_node("agent_think", agent_think) - workflow.add_node("ask_user", ask_user) - workflow.add_node("confirm_info", confirm_info) - workflow.add_node("call_tools", call_tools) - workflow.add_node("generate_response", generate_response) - workflow.add_node("follow_up_qa", follow_up_qa) - workflow.add_node("model_confirm", model_confirm) - workflow.add_node("handle_feedback", handle_feedback) - workflow.add_node("confirm_report", confirm_report) - workflow.add_node("analyze_follow_up_intent", analyze_follow_up_intent) - workflow.add_node("deep_rag_search", deep_rag_search) - workflow.add_node("generate_deep_response", generate_deep_response) - workflow.add_node("regenerate_scheme_from_feedback", regenerate_scheme_from_feedback) - - workflow.add_edge(START, "classify_intent") - workflow.add_conditional_edges("classify_intent", _route_after_classify, - {"extract_info": "extract_info","follow_up_qa": "follow_up_qa",}) - workflow.add_edge("extract_info", "agent_think") - workflow.add_conditional_edges("agent_think", _route_after_think, - {"ask_user": "ask_user","confirm_info": "confirm_info","call_tools": "call_tools","generate_response": "generate_response","follow_up": "follow_up_qa","follow_up_qa": "follow_up_qa","handle_feedback": "handle_feedback","extract_info": "extract_info","analyze_follow_up_intent": "analyze_follow_up_intent",}) - workflow.add_conditional_edges("call_tools", _route_after_tools, {"model_confirm": "model_confirm","generate_response": "generate_response",}) - workflow.add_conditional_edges("analyze_follow_up_intent", _route_after_intent, {"deep_rag_search": "deep_rag_search","follow_up_qa": "follow_up_qa","handle_feedback": "handle_feedback","extract_info": "extract_info",}) - workflow.add_edge("deep_rag_search", "generate_deep_response") - workflow.add_edge("generate_deep_response", END) - workflow.add_edge("generate_response", END) - workflow.add_edge("follow_up_qa", END) - workflow.add_edge("regenerate_scheme_from_feedback", END) - workflow.add_edge("handle_feedback", END) - workflow.add_edge("confirm_report", END) - - return workflow.compile(checkpointer=checkpointer) - -async def run_operate_workflow(extracted_text: str,combined_query: str,history_message: str = "",route_flag: str = "",previous_state_json: str = "",chat_id: str = "",message_id: str = "",checkpointer=None,user_response: Dict[str, Any] = None) -> Dict[str, Any]: - """ - 执行操作指导工作流 - - Args: - extracted_text: 文本描述 - combined_query: 组合查询 - history_message: 历史消息(JSON格式) - route_flag: 路由标识 - previous_state_json: 兼容参数(已废弃) - chat_id: 聊天会话 ID - message_id: 消息 ID - checkpointer: LangGraph checkpointer 实例 - user_response: 用户恢复执行的响应数据 - - Returns: - 工作流执行结果 - """ - thread_id = chat_id if chat_id else None - - config = {"configurable": {"thread_id": thread_id}} if thread_id else {} - - print(f"[DEBUG] run_operate_workflow: thread_id={thread_id}, checkpointer={checkpointer is not None}") - - if checkpointer is None: - from checkpointer_config import checkpointer_manager - checkpointer = await checkpointer_manager.get_async_checkpointer() - - app = create_operate_workflow(checkpointer=checkpointer) - - if thread_id: - try: - current_state = await app.aget_state(config) - print( - f"[DEBUG] current_state exists: {current_state is not None}, has values: {current_state.values is not None if current_state else False}, tasks: {len(current_state.tasks) if current_state else 0}") - - if current_state and current_state.values: - saved_state = current_state.values - print( - f"[Checkpointer] 发现已保存状态: ship_number={saved_state.get('ship_number')}, device_name={saved_state.get('device_name')}, operation_item={saved_state.get('operation_item')}") - - if current_state.tasks: - print(f"[Checkpointer] 恢复执行,注入用户响应") - resume_value = user_response if user_response else {"query": combined_query} - result = await app.ainvoke(Command(resume=resume_value), config) - else: - merged_query = combined_query - - _is_confirm = await is_confirmation_intent(combined_query) - _has_full_info = (saved_state.get("ship_number") and saved_state.get("device_name") and saved_state.get("operation_item")) - _override_confirmed = _is_confirm and _has_full_info and not saved_state.get("user_confirmed_info", False) - - initial_state = {**saved_state,"combined_query": merged_query,"history_messages": parse_history(history_message),"initialized": False,"user_intent": "",} - if _override_confirmed: - print(f"[Checkpointer] 检测到确认意图,自动设置 user_confirmed_info=True") - initial_state["user_confirmed_info"] = True - result = await app.ainvoke(initial_state, config) - else: - initial_state = {"extracted_text": extracted_text,"combined_query": combined_query,"history_messages": parse_history(history_message),"ship_number": "","device_name": "","operation_item": "","additional_info": "", - "operation_code": "","operation_time": "","operation_frequency": "","tried_measures": "","running_condition": "","rag_search_result": None,"graph_search_result": None,"response": "", - "suggestedReplies": [],"actions": [],"error_message": "","agent_decision": "","agent_reasoning": "","tools_to_call": [],"missing_info": [],"info_completeness": 0.0, - "matched_kb_id": "","matched_kb_name": "","iteration_count": 0,"max_iterations": 5,"scheme_generated": False,"waiting_for_supplement": False,"extracted_info": {}, - "has_rag_result": False,"has_graph_result": False,"is_execution_feedback": False,"execution_detail": "","ask_round": 0,"user_confirmed_info": False,"need_model_confirm": False, - "user_confirmed_model": False,"waiting_feedback": False,"user_feedback": "","waiting_report_confirm": False,"report_confirmed": False,"initialized": False,"current_node": "","user_intent": "","last_combined_query": "",} - result = await app.ainvoke(initial_state, config) - except Exception as e: - print(f"[Checkpointer] 状态恢复失败: {e}") - import traceback - traceback.print_exc() - result = {} - else: - initial_state = {"extracted_text": extracted_text,"combined_query": combined_query,"history_messages": parse_history(history_message),"ship_number": "","device_name": "","operation_item": "","additional_info": "","operation_code": "","operation_time": "", - "operation_frequency": "","tried_measures": "","running_condition": "","rag_search_result": None,"graph_search_result": None,"response": "","suggestedReplies": [],"actions": [],"error_message": "","agent_decision": "","agent_reasoning": "","tools_to_call": [], - "missing_info": [],"info_completeness": 0.0,"matched_kb_id": "","matched_kb_name": "","iteration_count": 0,"max_iterations": 5,"scheme_generated": False,"waiting_for_supplement": False,"extracted_info": {},"has_rag_result": False,"has_graph_result": False, - "is_execution_feedback": False,"execution_detail": "","ask_round": 0,"user_confirmed_info": False,"need_model_confirm": False,"user_confirmed_model": False,"waiting_feedback": False,"user_feedback": "","waiting_report_confirm": False,"report_confirmed": False,"initialized": False,"current_node": "","user_intent": "","last_combined_query": "",} - - try: - result = await app.ainvoke(initial_state, config) - except Exception as e: - print(f"[工作流执行失败] {str(e)}") - import traceback - traceback.print_exc() - return {"response": f"操作工作流执行失败: {str(e)}","actions": [],"result_tag": "operate","suggestedReplies": [],} - - if result is None: - result = {} - - interrupts = result.get("__interrupt__", []) - if interrupts: - interrupt_data = interrupts[0].value if interrupts else {} - print(f"[Interrupt] 工作流中断,等待用户输入: {interrupt_data}") - return {"response": interrupt_data.get("response", "请提供更多信息"),"actions": interrupt_data.get("actions", []),"result_tag": "operate","suggestedReplies": interrupt_data.get("suggestedReplies", []),"waiting_for": interrupt_data.get("waiting_for", ""),} - - if result.get("error_message"): - return {"response": f"操作工作流执行失败: {result['error_message']}","actions": [],"result_tag": "operate","suggestedReplies": [],} - - route_flag = result.get("route_flag", "") - if not route_flag: - route_flag = "operate" - - return {"response": result.get("response", ""),"actions": result.get("actions", []),"result_tag": route_flag,"suggestedReplies": result.get("suggestedReplies", []),"route_flag": route_flag,} - - diff --git a/workflows/workflow_utils.py b/workflows/workflow_utils.py index f3b9382..39e7f28 100644 --- a/workflows/workflow_utils.py +++ b/workflows/workflow_utils.py @@ -4,13 +4,21 @@ """ from typing import TypedDict, Optional, Dict, Any, List -from prompts import COMMON_PROMPTS +from difflib import SequenceMatcher from modelsAPI.model_api import OpenaiAPI from utils.function_tracker import get_all_callbacks, get_current_stream_handler from utils.image_utils import extract_image_paths, normalize_markdown_images +import asyncio import json import re import random +import inspect +import unicodedata + + +RAG_DEDUP_SIMILARITY_THRESHOLD = 0.86 +RAG_DEDUP_CONTAINMENT_THRESHOLD = 0.92 +RAG_DEDUP_MIN_TEXT_LENGTH = 20 def safe_json_extract(text: str) -> Optional[Any]: @@ -39,41 +47,1203 @@ def safe_json_extract(text: str) -> Optional[Any]: from workflows.history_manager import parse_history, build_history_str, filter_image_urls, init_history # noqa: F401 +import re + def normalize_latex_spacing(text: str) -> str: - placeholder = "\x00DOUBLE_DOLLAR\x00" - text = text.replace("$$", placeholder) - text = re.sub(r'(? str: + token = ( + f"\uE000LATEX_PROTECTED_" + f"{len(protected_parts)}" + f"\uE001" + ) + protected_parts.append(content) + return token + + def restore(content: str) -> str: + # 保护内容可能再次包含更早生成的占位符,必须按后进先出恢复。 + for index in reversed(range(len(protected_parts))): + original = protected_parts[index] + token = f"\uE000LATEX_PROTECTED_{index}\uE001" + content = content.replace(token, original) + + return content + + def is_escaped(content: str, position: int) -> bool: + """ + 判断 content[position] 是否被奇数个反斜杠转义。 + """ + slash_count = 0 + position -= 1 + + while position >= 0 and content[position] == "\\": + slash_count += 1 + position -= 1 + + return slash_count % 2 == 1 + + # ================================================================ + # 1. 保护 Markdown 围栏代码块 + # ================================================================ + + lines = text.splitlines(keepends=True) + protected_lines: list[str] = [] + line_index = 0 + + while line_index < len(lines): + line = lines[line_index] + + opening_match = re.match( + r"^[ \t]{0,3}(`{3,}|~{3,})", + line, + ) + + if not opening_match: + protected_lines.append(line) + line_index += 1 + continue + + opening_fence = opening_match.group(1) + fence_character = opening_fence[0] + fence_length = len(opening_fence) + + closing_pattern = re.compile( + rf"^[ \t]{{0,3}}" + rf"{re.escape(fence_character)}" + rf"{{{fence_length},}}" + rf"[ \t]*(?:\n|$)" + ) + + block_lines = [line] + line_index += 1 + + while line_index < len(lines): + current_line = lines[line_index] + block_lines.append(current_line) + line_index += 1 + + if closing_pattern.match(current_line): + break + + protected_lines.append( + protect("".join(block_lines)) + ) + + text = "".join(protected_lines) + + # ================================================================ + # 2. 保护 HTML 代码区域和注释 + # ================================================================ + + text = re.sub( + r"(?is)" + r"<(?Ppre|code|script|style)\b[^>]*>" + r".*?" + r"", + lambda match: protect(match.group(0)), + text, + ) + + text = re.sub( + r"(?s)", + lambda match: protect(match.group(0)), + text, + ) + + # ================================================================ + # 3. 保护 Markdown 链接地址 + # ================================================================ + + text = re.sub( + r"(!?\[[^\]\n]*\]\()([^)\n]*)(\))", + lambda match: ( + match.group(1) + + protect(match.group(2)) + + match.group(3) + ), + text, + ) + + text = re.sub( + r"(?im)" + r"^([ \t]{0,3}\[[^\]\n]+\]:[ \t]*)" + r"(\S+(?:[ \t]+.*)?)$", + lambda match: ( + match.group(1) + + protect(match.group(2)) + ), + text, + ) + + # ================================================================ + # 4. 保护 Markdown 行内代码 + # ================================================================ + + inline_code_result: list[str] = [] + position = 0 + + while position < len(text): + if text[position] != "`": + inline_code_result.append(text[position]) + position += 1 + continue + + delimiter_end = position + + while ( + delimiter_end < len(text) + and text[delimiter_end] == "`" + ): + delimiter_end += 1 + + delimiter = text[position:delimiter_end] + search_position = delimiter_end + closing_position = -1 + + while search_position < len(text): + candidate = text.find( + delimiter, + search_position, + ) + + if candidate < 0: + break + + previous_character = ( + text[candidate - 1] + if candidate > 0 + else "" + ) + + candidate_end = candidate + len(delimiter) + + next_character = ( + text[candidate_end] + if candidate_end < len(text) + else "" + ) + + if ( + previous_character != "`" + and next_character != "`" + ): + closing_position = candidate + break + + search_position = candidate + 1 + + if closing_position < 0: + inline_code_result.append(delimiter) + position = delimiter_end + continue + + end_position = closing_position + len(delimiter) + + inline_code_result.append( + protect(text[position:end_position]) + ) + + position = end_position + + text = "".join(inline_code_result) + + # ================================================================ + # 5. 转换其他 LaTeX 公式定界符 + # ================================================================ + + # \( x + y \) -> $x + y$ + text = re.sub( + r"(? $$x + y$$ + text = re.sub( + r"(?s)(? bool: + compact_body = re.sub(r"\s+", "", formula_body) + if len(compact_body) >= 46: + return True + + has_relation = bool( + re.search( + r"(?]" + r"|\\(?:leq?|geq?|approx|sim|equiv|propto|rightarrow|mapsto)\b", + formula_body, + ) + ) + has_large_operator = bool( + re.search( + r"\\(?:sum|int|iint|iiint|prod|lim|begin|cases|matrix)\b", + formula_body, + ) + ) + has_structured_expression = bool( + re.search( + r"\\(?:d?frac|sqrt|left|right|overbrace|underbrace)\b", + formula_body, + ) + ) + + return ( + has_large_operator + or ( + has_relation + and has_structured_expression + and len(compact_body) >= 24 + ) + ) + + promoted_result: list[str] = [] + position = 0 + + while position < len(text): + is_inline_opening = ( + text[position] == "$" + and not is_escaped(text, position) + and not text.startswith("$$", position) + and not ( + position > 0 + and text[position - 1] == "$" + ) + ) + + if not is_inline_opening: + promoted_result.append(text[position]) + position += 1 + continue + + opening_position = position + closing_position = -1 + search_position = position + 1 + + while search_position < len(text): + candidate = text.find("$", search_position) + if candidate < 0: + break + if "\n" in text[opening_position + 1:candidate]: + break + if is_escaped(text, candidate): + search_position = candidate + 1 + continue + if text.startswith("$$", candidate): + search_position = candidate + 2 + continue + closing_position = candidate + break + + if closing_position < 0: + promoted_result.append("$") + position += 1 + continue + + formula_body = text[ + opening_position + 1:closing_position + ].strip() + line_start = text.rfind("\n", 0, opening_position) + 1 + line_end = text.find("\n", closing_position + 1) + if line_end < 0: + line_end = len(text) + stripped_line = text[line_start:line_end].strip() + is_markdown_table_row = ( + stripped_line.startswith("|") + and stripped_line.endswith("|") + ) + + if ( + not formula_body + or is_markdown_table_row + or not should_promote_inline_formula(formula_body) + ): + promoted_result.append( + text[opening_position:closing_position + 1] + ) + position = closing_position + 1 + continue + + before_text = "".join(promoted_result) + if before_text and not before_text.endswith("\n\n"): + before_text = before_text.rstrip(" \t\n") + "\n\n" + promoted_result = [before_text] if before_text else [] + promoted_result.append( + "$$\n" + formula_body + "\n$$" + ) + + position = closing_position + 1 + while position < len(text) and text[position] in " \t": + position += 1 + + # 独立公式后的句号不再单独占一段;逗号和冒号仍保留语义。 + if position < len(text) and text[position] in "。.": + position += 1 + + if position < len(text): + while position < len(text) and text[position] == "\n": + position += 1 + promoted_result.append("\n\n") + + text = "".join(promoted_result) + + # ================================================================ + # 7. 完整提取并保护 $$...$$ 段落公式 + # ================================================================ + + display_result: list[str] = [] + position = 0 + + while position < len(text): + if not ( + text.startswith("$$", position) + and not is_escaped(text, position) + ): + display_result.append(text[position]) + position += 1 + continue + + opening_position = position + search_position = position + 2 + closing_position = -1 + + while search_position < len(text): + candidate = text.find( + "$$", + search_position, + ) + + if candidate < 0: + break + + if not is_escaped(text, candidate): + closing_position = candidate + break + + search_position = candidate + 2 + + # 未找到闭合 $$,剩余内容原样保留。 + if closing_position < 0: + display_result.append( + text[opening_position:] + ) + position = len(text) + break + + complete_formula = text[ + opening_position:closing_position + 2 + ] + + formula_body = text[ + opening_position + 2:closing_position + ] + + # ------------------------------------------------------------ + # 多行段落公式也统一重建,并确保公式块前后各有一个空行。 + # 相邻公式若只有一个换行,部分 Markdown/KaTeX 解析器会把它们 + # 合并到同一个显示块中,导致两条公式横向排在同一行。 + # ------------------------------------------------------------ + if "\n" in complete_formula: + formula_lines = [ + line.strip() + for line in formula_body.splitlines() + if line.strip() + ] + contains_multiline_environment = bool( + re.search( + r"\\(?:begin|end|substack)\b" + r"|\\\\", + formula_body, + ) + ) + lines_are_independent_equations = ( + len(formula_lines) > 1 + and not contains_multiline_environment + and all( + re.search( + r"(?]" + r"|\\(?:leq?|geq?|approx|sim|equiv|propto)\b", + line, + ) + for line in formula_lines + ) + ) + + if lines_are_independent_equations: + normalized_formula = "\n\n".join( + "$$\n" + line + "\n$$" + for line in formula_lines + ) + else: + normalized_formula = ( + "$$\n" + + formula_body.strip() + + "\n$$" + ) + before_text = "".join(display_result) + if before_text and not before_text.endswith("\n\n"): + before_text = before_text.rstrip(" \t\n") + "\n\n" + display_result = [before_text] if before_text else [] + display_result.append( + protect(normalized_formula) + ) + position = closing_position + 2 + + while ( + position < len(text) + and text[position] in " \t" + ): + position += 1 + + if position < len(text): + while ( + position < len(text) + and text[position] == "\n" + ): + position += 1 + display_result.append("\n\n") + continue + + # ------------------------------------------------------------ + # 判断公式是否位于 Markdown 表格内。 + # 表格中的公式不能强制拆成多行。 + # ------------------------------------------------------------ + + line_start = text.rfind( + "\n", + 0, + opening_position, + ) + 1 + + line_end = text.find( + "\n", + closing_position + 2, + ) + + if line_end < 0: + line_end = len(text) + + current_line = text[line_start:line_end] + clean_body = formula_body.strip() + + stripped_line = current_line.strip() + is_markdown_table_row = ( + stripped_line.startswith("|") + and stripped_line.endswith("|") + ) + + if is_markdown_table_row: + display_result.append( + protect( + "$$" + clean_body + "$$" + ) + ) + + position = closing_position + 2 + continue + + # ------------------------------------------------------------ + # 单行段落公式转换成标准多行格式。 + # + # $$R = K$$ + # + # 转换为: + # + # $$ + # R = K + # $$ + # ------------------------------------------------------------ + + normalized_formula = ( + "$$\n" + + clean_body + + "\n$$" + ) + + before_text = "".join(display_result) + + # 公式前存在普通正文时,确保前面有空行。 + if before_text and not before_text.endswith("\n\n"): + before_text = before_text.rstrip(" \t\n") + "\n\n" + + display_result = [before_text] if before_text else [] + + display_result.append( + protect(normalized_formula) + ) + + position = closing_position + 2 + + # 清理公式后已有的普通空格。 + while ( + position < len(text) + and text[position] in " \t" + ): + position += 1 + + # 后面还有内容时,确保公式后有空行。 + if position < len(text): + if text[position] == "\n": + while ( + position < len(text) + and text[position] == "\n" + ): + position += 1 + + display_result.append("\n\n") + + text = "".join(display_result) + + # ================================================================ + # 8. 规范 $...$ 行内公式 + # ================================================================ + + inline_result: list[str] = [] + position = 0 + + while position < len(text): + is_inline_opening = ( + text[position] == "$" + and not is_escaped(text, position) + and not text.startswith("$$", position) + and not ( + position > 0 + and text[position - 1] == "$" + ) + ) + + if not is_inline_opening: + inline_result.append(text[position]) + position += 1 + continue + + opening_position = position + + # 避免把 $100 识别成行内公式开始。 + next_opening_character = ( + text[position + 1] + if position + 1 < len(text) + else "" + ) + + if next_opening_character.isdigit(): + inline_result.append("$") + position += 1 + continue + + search_position = position + 1 + closing_position = -1 + + while search_position < len(text): + candidate = text.find( + "$", + search_position, + ) + + if candidate < 0: + break + + # 行内公式不能跨行。 + if "\n" in text[ + opening_position + 1:candidate + ]: + break + + if is_escaped(text, candidate): + search_position = candidate + 1 + continue + + # 跳过属于 $$ 的美元符号。 + if text.startswith("$$", candidate): + search_position = candidate + 2 + continue + + closing_position = candidate + break + + # 未找到闭合符时原样保留。 + if closing_position < 0: + inline_result.append("$") + position += 1 + continue + + formula_body = text[ + opening_position + 1:closing_position + ].strip() + + # 空公式不处理。 + if not formula_body: + inline_result.append( + text[ + opening_position:closing_position + 1 + ] + ) + + position = closing_position + 1 + continue + + # 折叠公式内部普通空格,不修改 LaTeX 命令。 + formula_body = re.sub( + r"[ \t]+", + " ", + formula_body, + ) + + previous_character = "" + + for part in reversed(inline_result): + if part: + previous_character = part[-1] + break + + next_character = ( + text[closing_position + 1] + if closing_position + 1 < len(text) + else "" + ) + + # 公式前只要存在非空白字符,就补空格。 + # + # 参数$T$ -> 参数 $T$ + # ($T$) -> ( $T$ + # ($T$) -> ( $T$ + if ( + previous_character + and not previous_character.isspace() + ): + inline_result.append(" ") + + inline_result.append( + "$" + formula_body + "$" + ) + + # 公式后只要存在非空白字符,就补空格。 + # + # $T$参数 -> $T$ 参数 + # ($T$) -> $T$ ) + # ($T$) -> $T$ ) + if ( + next_character + and not next_character.isspace() + ): + inline_result.append(" ") + + position = closing_position + 1 + + return restore("".join(inline_result)) + + +def convert_display_math_for_frontend(text: str) -> str: + """将段落公式改为 16000 前端能正确识别的块级定界符。""" + return re.sub( + r"(?ms)^[ \t]*\$\$[ \t]*\n" + r"(.*?)" + r"\n[ \t]*\$\$[ \t]*$", + lambda match: ( + "\\[\n" + + match.group(1).strip() + + "\n\\]" + ), + text, + ) def remove_duplicate_content(text: str) -> str: + """ + 删除普通 Markdown 正文中的完全重复行。 + + 不对以下内容进行去重: + - $$ ... $$ 段落公式 + - \\[ ... \\] 段落公式 + - equation、align、gather 等 LaTeX 环境 + - Markdown 围栏代码块 + - Markdown 缩进代码块 + - Markdown 标题、列表、引用、表格等结构行 + + 这样不会删除段落公式末尾的第二个 $$。 + """ if not text or len(text) < 10: return text - lines = text.split('\n') - result = [] - seen_segments = set() + if not isinstance(text, str): + raise TypeError( + f"text 必须是 str,实际类型为 {type(text).__name__}" + ) + + text = text.replace("\r\n", "\n").replace("\r", "\n") + lines = text.split("\n") + + result: list[str] = [] + seen_segments: set[str] = set() + + in_code_block = False + code_fence_character = "" + code_fence_length = 0 + + in_dollar_math = False + in_bracket_math = False + + latex_environment_stack: list[str] = [] + + display_environments = { + "equation", + "equation*", + "align", + "align*", + "alignat", + "alignat*", + "gather", + "gather*", + "multline", + "multline*", + "flalign", + "flalign*", + "eqnarray", + "eqnarray*", + "displaymath", + "math", + } + + def is_escaped(value: str, position: int) -> bool: + slash_count = 0 + position -= 1 + + while position >= 0 and value[position] == "\\": + slash_count += 1 + position -= 1 + + return slash_count % 2 == 1 + + def count_unescaped_double_dollars(value: str) -> int: + """ + 统计一行中未转义的 $$ 数量。 + """ + count = 0 + position = 0 + + while position < len(value) - 1: + if ( + value[position:position + 2] == "$$" + and not is_escaped(value, position) + ): + count += 1 + position += 2 + else: + position += 1 + + return count + + def is_markdown_structure_line(value: str) -> bool: + """ + Markdown 结构行不参与去重,避免破坏语义。 + """ + stripped = value.strip() + + if not stripped: + return True + + patterns = ( + # 标题 + r"^#{1,6}\s+", + + # 无序列表 + r"^[-+*]\s+", + + # 有序列表 + r"^\d+[.)]\s+", + + # 引用 + r"^>\s*", + + # 任务列表 + r"^[-+*]\s+\[[ xX]\]\s+", + + # 水平分隔线 + r"^(?:-{3,}|\*{3,}|_{3,})$", + + # 表格分隔行 + r"^\|?\s*:?-{3,}:?" + r"(?:\s*\|\s*:?-{3,}:?)+\s*\|?$", + + # 普通表格行 + r"^\|.*\|$", + + # HTML 标签 + r"^]*>$", + + # LaTeX 环境边界 + r"^\\(?:begin|end)\{[^}]+\}", + + # 公式定界符 + r"^(?:\$\$|\\\[|\\\])$", + + # Markdown 链接引用定义 + r"^\[[^\]]+\]:\s*\S+", + ) + + return any( + re.match(pattern, stripped) + for pattern in patterns + ) for line in lines: line = line.rstrip() - if not line: + stripped = line.strip() + + # ============================================================ + # 1. Markdown 围栏代码块 + # ============================================================ + + fence_match = re.match( + r"^[ \t]{0,3}(`{3,}|~{3,})", + line, + ) + + if not in_code_block and fence_match: + fence = fence_match.group(1) + + in_code_block = True + code_fence_character = fence[0] + code_fence_length = len(fence) + result.append(line) continue - normalized_line = line.strip().lower() + if in_code_block: + result.append(line) + + closing_pattern = re.compile( + rf"^[ \t]{{0,3}}" + rf"{re.escape(code_fence_character)}" + rf"{{{code_fence_length},}}" + rf"[ \t]*$" + ) + + if closing_pattern.match(line): + in_code_block = False + code_fence_character = "" + code_fence_length = 0 + + continue + + # Markdown 缩进代码块不参与去重。 + if re.match(r"^(?:\t| {4})", line): + result.append(line) + continue + + # ============================================================ + # 2. \[ ... \] 段落公式 + # ============================================================ + + if in_bracket_math: + result.append(line) + + if re.search(r"(? bracket_close_count: + in_bracket_math = True + + continue + + # ============================================================ + # 3. LaTeX display 环境 + # ============================================================ + + begin_matches = re.findall( + r"\\begin\{([^}]+)\}", + line, + ) + + end_matches = re.findall( + r"\\end\{([^}]+)\}", + line, + ) + + relevant_begins = [ + environment + for environment in begin_matches + if environment in display_environments + ] + + relevant_ends = [ + environment + for environment in end_matches + if environment in display_environments + ] + + if latex_environment_stack: + result.append(line) + + for environment in relevant_begins: + latex_environment_stack.append(environment) + + for environment in relevant_ends: + if ( + latex_environment_stack + and latex_environment_stack[-1] + == environment + ): + latex_environment_stack.pop() + elif environment in latex_environment_stack: + latex_environment_stack.remove(environment) + + continue + + if relevant_begins: + result.append(line) + + for environment in relevant_begins: + latex_environment_stack.append(environment) + + for environment in relevant_ends: + if ( + latex_environment_stack + and latex_environment_stack[-1] + == environment + ): + latex_environment_stack.pop() + elif environment in latex_environment_stack: + latex_environment_stack.remove(environment) + + continue + + # ============================================================ + # 4. $$ ... $$ 段落公式 + # ============================================================ + + double_dollar_count = ( + count_unescaped_double_dollars(line) + ) + + if in_dollar_math: + # 公式块中的内容全部保留,包括最后一行的 $$。 + result.append(line) + + if double_dollar_count % 2 == 1: + in_dollar_math = False + + continue + + if double_dollar_count > 0: + # 含有 $$ 的行永远不参与去重。 + result.append(line) + + # 奇数个 $$ 代表开启了跨行段落公式。 + if double_dollar_count % 2 == 1: + in_dollar_math = True + + continue + + # ============================================================ + # 5. 空行和 Markdown 结构行 + # ============================================================ + + if not stripped: + result.append(line) + continue + + if is_markdown_structure_line(line): + result.append(line) + continue + + # ============================================================ + # 6. 仅对普通正文行去重 + # ============================================================ + + normalized_line = re.sub( + r"\s+", + " ", + stripped.lower(), + ) + if normalized_line in seen_segments: continue seen_segments.add(normalized_line) result.append(line) - cleaned_text = '\n'.join(result) - return cleaned_text + return "\n".join(result) + + + + +# 推荐调用顺序: +# +# text = remove_duplicate_content(text) +# text = normalize_latex_spacing(text) + + + +def _extract_rag_text(item: Any) -> str: + if isinstance(item, dict): + return str(item.get("text") or item.get("content") or "") + if isinstance(item, str): + return item + return "" + + +def _extract_rag_score(item: Any) -> float: + if not isinstance(item, dict): + return 0.0 + try: + return float(item.get("score") or item.get("socre") or 0.0) + except (TypeError, ValueError): + return 0.0 + + +def _normalize_rag_text_for_dedupe(text: Any) -> str: + value = unicodedata.normalize("NFKC", str(text or "")) + value = re.sub(r"!\[[^\]]*\]\([^)]+\)", "", value) + value = re.sub(r"\[([^\]]*)\]\([^)]+\)", r"\1", value) + value = re.sub(r"https?://\S+", "", value, flags=re.IGNORECASE) + value = re.sub(r"[/\\]?picture[/\\]\S+", "", value, flags=re.IGNORECASE) + value = re.sub(r"\s+", "", value).lower() + return "".join(ch for ch in value if ch.isalnum()) + + +def _char_ngrams(text: str, n: int) -> set: + if len(text) <= n: + return {text} if text else set() + return {text[i:i + n] for i in range(len(text) - n + 1)} + + +def _rag_texts_are_similar(text_a: str, text_b: str, threshold: float) -> bool: + if not text_a or not text_b: + return False + if text_a == text_b: + return True + + min_len = min(len(text_a), len(text_b)) + max_len = max(len(text_a), len(text_b)) + if min_len < 8: + return False + + if min_len >= RAG_DEDUP_MIN_TEXT_LENGTH and (text_a in text_b or text_b in text_a): + return (min_len / max_len) >= RAG_DEDUP_CONTAINMENT_THRESHOLD + + if min_len < RAG_DEDUP_MIN_TEXT_LENGTH: + return SequenceMatcher(None, text_a, text_b).ratio() >= 0.95 + + ngram_size = 3 if min_len >= 30 else 2 + grams_a = _char_ngrams(text_a, ngram_size) + grams_b = _char_ngrams(text_b, ngram_size) + if not grams_a or not grams_b: + return False + + jaccard = len(grams_a & grams_b) / len(grams_a | grams_b) + if jaccard >= threshold: + return True + + return SequenceMatcher(None, text_a, text_b).ratio() >= threshold + + +def dedupe_rag_results( + items: Any, + limit: Optional[int] = None, + similarity_threshold: float = RAG_DEDUP_SIMILARITY_THRESHOLD, +) -> Any: + """ + Deduplicate RAG result chunks while preserving the original item shape. + + Higher-score items are kept first. Graph results are intentionally not handled + here; callers should pass only text RAG chunks. + """ + if not isinstance(items, list): + return items + if len(items) < 2: + return items[:limit] if limit else items + + prepared = [] + for idx, item in enumerate(items): + prepared.append({ + "item": item, + "index": idx, + "score": _extract_rag_score(item), + "normalized_text": _normalize_rag_text_for_dedupe(_extract_rag_text(item)), + }) + + prepared.sort(key=lambda entry: (-entry["score"], entry["index"])) + + kept = [] + for entry in prepared: + current_text = entry["normalized_text"] + is_duplicate = False + if current_text: + for kept_entry in kept: + if _rag_texts_are_similar( + current_text, + kept_entry["normalized_text"], + similarity_threshold, + ): + is_duplicate = True + break + if not is_duplicate: + kept.append(entry) + + deduped = [entry["item"] for entry in kept] + return deduped[:limit] if limit else deduped def convert_rag_result(rag_result): @@ -105,6 +1275,204 @@ def emit_callback_event(title_options: List[str], details: str): pass +async def resolve_ship_number_for_workflow( + ship_number: str, + context: str = "workflow", +) -> tuple[str, Dict[str, Any]]: + """ + 工作流内舷号/舰名标准化。 + + 能映射到具体舷号时返回映射后的舷号;失败或未匹配时返回原值。 + """ + original_ship_number = str(ship_number or "").strip() + if not original_ship_number: + return original_ship_number, {} + + try: + from utils.ship_number_search import smart_ship_number_mapping + + matched_ship_number, matched_model, all_numbers, match_type = await smart_ship_number_mapping( + original_ship_number + ) + result = { + "matched_ship_number": matched_ship_number, + "matched_model": matched_model, + "all_numbers": all_numbers, + "match_type": match_type, + } + if match_type in ["exact_number", "exact_name", "embedding"] and matched_ship_number: + mapped_ship_number = str(matched_ship_number).strip() + if mapped_ship_number and mapped_ship_number != original_ship_number: + print( + f"[{context}] 舷号映射: '{original_ship_number}' -> " + f"'{mapped_ship_number}',匹配类型={match_type}" + ) + return mapped_ship_number or original_ship_number, result + + print(f"[{context}] 舷号未映射,继续使用原值: {original_ship_number}") + return original_ship_number, result + except Exception as e: + print(f"[{context}] 舷号映射异常,继续使用原值: {original_ship_number}; 错误: {str(e)}") + return original_ship_number, {} + + +async def resolve_ship_scope_for_workflow( + ship_number: str, + context: str = "workflow", +) -> tuple[str, List[str], Dict[str, Any]]: + """ + Resolve user ship input into a display hull number and a hull-number scope. + + Hull number and ship-name matches collapse to one hull. Model matches keep the + user's original value for display but return every hull number under that model + so shared device normalization and system lookup can search the whole scope. + """ + original_ship_number = str(ship_number or "").strip() + if not original_ship_number: + return original_ship_number, [], {} + + try: + from utils.ship_number_search import smart_ship_number_mapping + + matched_ship_number, matched_model, all_numbers, match_type = await smart_ship_number_mapping( + original_ship_number + ) + scope_numbers = [str(num).strip() for num in (all_numbers or []) if str(num or "").strip()] + result = { + "matched_ship_number": matched_ship_number, + "matched_model": matched_model, + "all_numbers": scope_numbers, + "match_type": match_type, + } + + if match_type in ["exact_number", "exact_name", "embedding"] and matched_ship_number: + mapped_ship_number = str(matched_ship_number).strip() + return mapped_ship_number or original_ship_number, [mapped_ship_number], result + + if match_type == "exact_model" and scope_numbers: + print( + f"[{context}] ship model scope resolved: '{original_ship_number}' -> " + f"{matched_model}, hull_numbers={scope_numbers}" + ) + return original_ship_number, scope_numbers, result + + print(f"[{context}] ship scope not resolved, continuing with original value: {original_ship_number}") + return original_ship_number, [], result + except Exception as e: + print(f"[{context}] ship scope resolve failed, continuing with original value: {original_ship_number}; error: {str(e)}") + return original_ship_number, [], {} + + +async def normalize_device_name_for_workflow( + device_name: str, + ship_number: str = "", + ship_numbers: Optional[List[str]] = None, + context: str = "workflow", +) -> tuple[str, Dict[str, Any]]: + """ + 工作流内设备名称标准化。 + + 标准化失败时返回原设备名,避免检索流程被 Neo4j 或 embedding 服务异常阻断。 + """ + original_device_name = str(device_name or "").strip() + if not original_device_name: + return original_device_name, {} + + try: + from tools.graph_tools import normalize_device_name_by_ship_graph + + print( + f"[{context}] 开始设备名称标准化: device='{original_device_name}', " + f"ship_number='{ship_number or '未提供'}'" + ) + call_kwargs = {} + if "ship_number" in inspect.signature(normalize_device_name_by_ship_graph).parameters: + call_kwargs["ship_number"] = str(ship_number or "").strip() or None + if "ship_numbers" in inspect.signature(normalize_device_name_by_ship_graph).parameters: + scoped_numbers = [str(num).strip() for num in (ship_numbers or []) if str(num or "").strip()] + call_kwargs["ship_numbers"] = scoped_numbers or None + elif ship_number: + print(f"[{context}] 当前进程加载的设备标准化函数不支持 ship_number,请重启服务加载最新 graph_tools.py") + + result = await normalize_device_name_by_ship_graph(original_device_name, **call_kwargs) + if result.get("success", False): + normalized_device_name = str(result.get("device_name") or original_device_name).strip() + print( + f"[{context}] 设备名称标准化完成: '{original_device_name}' -> " + f"'{normalized_device_name or original_device_name}',舷号={ship_number or '未提供'}" + ) + emit_callback_event( + ["🔎 设备名称标准化", "🔧 设备匹配"], + f"{original_device_name} -> {normalized_device_name or original_device_name}" + ) + return normalized_device_name or original_device_name, result + + print( + f"[{context}] 设备名称标准化失败,继续使用原设备名: {original_device_name}; " + f"原因: {result.get('error', '未知错误')}" + ) + return original_device_name, result + except Exception as e: + print(f"[{context}] 设备名称标准化异常,继续使用原设备名: {original_device_name}; 错误: {str(e)}") + return original_device_name, {} + + +async def resolve_device_system_for_workflow( + device_name: str, + ship_number: str = "", + ship_numbers: Optional[List[str]] = None, + context: str = "workflow", +) -> tuple[str, Dict[str, Any]]: + """ + 工作流内根据设备名称反推所属系统。 + + 失败时返回空字符串,调用方可继续执行主流程。 + """ + device_name = str(device_name or "").strip() + if not device_name: + return "", {} + + try: + print( + f"[{context}] 开始设备反推系统: device='{device_name}', " + f"ship_number='{ship_number or '未提供'}'" + ) + from tools.graph_tools import find_system_by_device + + call_kwargs = {"device_name": device_name} + if "ship_number" in inspect.signature(find_system_by_device).parameters: + call_kwargs["ship_number"] = str(ship_number or "").strip() or None + if "ship_numbers" in inspect.signature(find_system_by_device).parameters: + scoped_numbers = [str(num).strip() for num in (ship_numbers or []) if str(num or "").strip()] + call_kwargs["ship_numbers"] = scoped_numbers or None + elif ship_number: + print(f"[{context}] 当前进程加载的设备反推系统函数不支持 ship_number,请重启服务加载最新 graph_tools.py") + + result = await find_system_by_device(**call_kwargs) + if not result.get("success", False): + print(f"[{context}] 设备反推系统失败: {result.get('error', '未知错误')}") + return "", result + + systems = result.get("systems") or [] + first_system = systems[0] if systems else {} + system_name = "" + if isinstance(first_system, dict): + system_name = str(first_system.get("name") or first_system.get("系统名称") or first_system.get("名称") or "").strip() + + if system_name: + print(f"[{context}] 设备反推系统完成: {device_name} -> {system_name}") + emit_callback_event( + ["🔎 设备所属系统识别", "🧭 系统反查"], + f"{device_name} -> {system_name}" + ) + else: + print(f"[{context}] 设备反推系统无结果: {device_name}") + return system_name, result + except Exception as e: + print(f"[{context}] 设备反推系统异常: {str(e)}") + return "", {} + + def build_detail_info(fields: Dict[str, str]) -> str: detail_info = "" for label, value in fields.items(): @@ -128,42 +1496,145 @@ def append_atlas_section(answer: str, atlas_results: Dict[str, Any]) -> str: -async def stream_format_and_postprocess( - raw_content: str, +async def stream_generate_and_postprocess( + system_prompt: str, + messages: List[Dict[str, Any]], original_image_paths: List[str], temperature: float = 0.3, - content_label: str = None, + fallback: str = "抱歉,无法生成回答。", ) -> str: - if content_label: - format_user_content = COMMON_PROMPTS["format_user_simple"].format( - content_label=content_label, - raw_content=raw_content, - ) - else: - format_user_content = COMMON_PROMPTS["format_user_with_image_examples"].format(raw_content=raw_content) - - format_system_prompt = COMMON_PROMPTS["format_system"] + """非流式生成完整答案,统一校正后再按安全语义块模拟流式发送。 + 公式、代码块、表格和图片会作为完整单元发送,避免前端在内容尚未闭合时 + 错误解析。函数返回值与已经发送的所有片段严格一致。 + """ stream_handler = get_current_stream_handler() - answer = "" - accumulated_content = "" - async for chunk in OpenaiAPI.open_api_chat_stream( + raw_answer = await OpenaiAPI.open_api_chat_without_thinking( + query=None, model=None, - system_prompt=format_system_prompt, - messages=[{"role": "user", "content": format_user_content}], + system_prompt=system_prompt, + messages=messages, + enable_thinking=False, temperature=temperature, - ): - answer += chunk - accumulated_content += chunk - if stream_handler: - stream_handler.send_stream_content(accumulated_content) + ) - answer = answer.strip() if answer else raw_content + answer = (raw_answer or fallback).strip() + answer = re.sub( + r"ynchroneg>.*?ost switching>", + "", + answer, + flags=re.DOTALL, + ) + # 修复模型把下一个无序列表项接在上一句末尾的情况。 + answer = re.sub( + r"(?<=[。!?;:])\s+\*\s+(?=\S)", + "\n* ", + answer, + ) answer = remove_duplicate_content(answer) - answer = normalize_markdown_images(answer, original_paths=original_image_paths) - answer = normalize_latex_spacing(answer) + answer = normalize_markdown_images( + answer, + original_paths=original_image_paths, + ) + answer = normalize_latex_spacing(answer).strip() or fallback + # 16000 前端同时注册了两套 KaTeX 插件,其中旧插件会抢先把标准 + # $$...$$ 识别为 displayMode=False。改用同为标准 LaTeX 的 \[...\] + # 可命中新插件的块级规则,生成真正居中且独占一行的 katex-display。 + answer = convert_display_math_for_frontend(answer) + + def split_prose_line(line: str) -> List[str]: + """在闭合的行内公式/代码之外按中文语义停顿切分。""" + if len(line) <= 52: + return [line] + + pieces: List[str] = [] + start = 0 + for index, character in enumerate(line): + length = index + 1 - start + is_sentence_end = character in "。!?;" + is_long_pause = character in ",," and length >= 92 + if length < 36 or not (is_sentence_end or is_long_pause): + continue + + candidate = line[start:index + 1] + if candidate.count("$") % 2 or candidate.count("`") % 2: + continue + pieces.append(candidate) + start = index + 1 + + if start < len(line): + pieces.append(line[start:]) + return pieces or [line] + + def build_fake_stream_chunks(content: str) -> List[str]: + """构造不会截断特殊 Markdown 单元、且可原样拼回正文的片段。""" + lines = content.splitlines(keepends=True) + chunks: List[str] = [] + index = 0 + + while index < len(lines): + line = lines[index] + stripped = line.strip() + + if stripped in ("$$", "\\["): + closing_delimiter = ( + "$$" if stripped == "$$" else "\\]" + ) + block = line + index += 1 + while index < len(lines): + block += lines[index] + closing = ( + lines[index].strip() + == closing_delimiter + ) + index += 1 + if closing: + break + chunks.append(block) + continue + + if stripped.startswith("```"): + block = line + index += 1 + while index < len(lines): + block += lines[index] + closing = lines[index].strip().startswith("```") + index += 1 + if closing: + break + chunks.append(block) + continue + + if stripped.startswith("|"): + block = line + index += 1 + while index < len(lines) and lines[index].strip().startswith("|"): + block += lines[index] + index += 1 + chunks.append(block) + continue + + if "![" in line: + chunks.append(line) + else: + chunks.extend(split_prose_line(line)) + index += 1 + + return [chunk for chunk in chunks if chunk] + + chunks = build_fake_stream_chunks(answer) + for index, chunk in enumerate(chunks): + if stream_handler: + stream_handler.send_stream_content(chunk) + + if index < len(chunks) - 1: + visible_length = len(re.sub(r"\s+", "", chunk)) + # 总体约 120 字/秒,并保留 160~600ms 的自然停顿。 + delay = max(0.16, min(0.60, visible_length / 120)) + if chunk.lstrip().startswith(("$$", "\\[", "```", "|", "![")): + delay = max(delay, 0.22) + await asyncio.sleep(delay) return answer - - diff --git a/workflows/workflow_utils.py_0723 b/workflows/workflow_utils.py_0723 new file mode 100644 index 0000000..881d319 --- /dev/null +++ b/workflows/workflow_utils.py_0723 @@ -0,0 +1,1332 @@ +""" +工作流公共工具函数 +提取自 workflow_fault_diagnosis.py 和 workflow_operate.py 的共享代码 +""" + +from typing import TypedDict, Optional, Dict, Any, List +from difflib import SequenceMatcher +from modelsAPI.model_api import OpenaiAPI +from utils.function_tracker import get_all_callbacks, get_current_stream_handler +from utils.image_utils import extract_image_paths, normalize_markdown_images +import json +import re +import random +import inspect +import unicodedata + + +RAG_DEDUP_SIMILARITY_THRESHOLD = 0.86 +RAG_DEDUP_CONTAINMENT_THRESHOLD = 0.92 +RAG_DEDUP_MIN_TEXT_LENGTH = 20 + + +def safe_json_extract(text: str) -> Optional[Any]: + if not text: + return None + cleaned = text.strip() + cleaned = re.sub(r"```json\s*", "", cleaned, flags=re.IGNORECASE) + cleaned = re.sub(r"```\s*", "", cleaned) + m = re.search(r"\[[\s\S]*\]", cleaned) + if m: + try: + return json.loads(m.group(0)) + except Exception: + pass + m = re.search(r"\{[\s\S]*\}", cleaned) + if m: + try: + return json.loads(m.group(0)) + except Exception: + pass + try: + return json.loads(cleaned) + except Exception: + return None + + +from workflows.history_manager import parse_history, build_history_str, filter_image_urls, init_history # noqa: F401 + +import re + + +def normalize_latex_spacing(text: str) -> str: + """ + 规范 Markdown 中的 LaTeX 公式。 + + 规则: + 1. 行内公式 $...$ + - 清除公式定界符内部首尾空格 + - 公式前后只要存在非空白字符,就补一个空格 + - ($T$) 会转换为 ( $T$ ) + + 2. 段落公式 $$...$$ + - 多行公式完整保留,不会删除结尾的 $$ + - 单行 $$公式$$ 转换为标准多行段落公式 + - Markdown 表格中的 $$公式$$ 不拆行 + + 3. 转换: + - \\(...\\) 转换为 $...$ + - \\[...\\] 转换为 $$...$$ + + 4. 不处理: + - Markdown 围栏代码块 + - Markdown 行内代码 + - HTML code、pre、script、style + - HTML 注释 + - Markdown 链接地址 + - 转义美元符号 \\$ + """ + if not isinstance(text, str): + raise TypeError( + f"text 必须是 str,实际类型为 {type(text).__name__}" + ) + + if not text: + return text + + text = text.replace("\r\n", "\n").replace("\r", "\n") + + protected_parts: list[str] = [] + + def protect(content: str) -> str: + token = ( + f"\uE000LATEX_PROTECTED_" + f"{len(protected_parts)}" + f"\uE001" + ) + protected_parts.append(content) + return token + + def restore(content: str) -> str: + for index, original in enumerate(protected_parts): + token = f"\uE000LATEX_PROTECTED_{index}\uE001" + content = content.replace(token, original) + + return content + + def is_escaped(content: str, position: int) -> bool: + """ + 判断 content[position] 是否被奇数个反斜杠转义。 + """ + slash_count = 0 + position -= 1 + + while position >= 0 and content[position] == "\\": + slash_count += 1 + position -= 1 + + return slash_count % 2 == 1 + + # ================================================================ + # 1. 保护 Markdown 围栏代码块 + # ================================================================ + + lines = text.splitlines(keepends=True) + protected_lines: list[str] = [] + line_index = 0 + + while line_index < len(lines): + line = lines[line_index] + + opening_match = re.match( + r"^[ \t]{0,3}(`{3,}|~{3,})", + line, + ) + + if not opening_match: + protected_lines.append(line) + line_index += 1 + continue + + opening_fence = opening_match.group(1) + fence_character = opening_fence[0] + fence_length = len(opening_fence) + + closing_pattern = re.compile( + rf"^[ \t]{{0,3}}" + rf"{re.escape(fence_character)}" + rf"{{{fence_length},}}" + rf"[ \t]*(?:\n|$)" + ) + + block_lines = [line] + line_index += 1 + + while line_index < len(lines): + current_line = lines[line_index] + block_lines.append(current_line) + line_index += 1 + + if closing_pattern.match(current_line): + break + + protected_lines.append( + protect("".join(block_lines)) + ) + + text = "".join(protected_lines) + + # ================================================================ + # 2. 保护 HTML 代码区域和注释 + # ================================================================ + + text = re.sub( + r"(?is)" + r"<(?Ppre|code|script|style)\b[^>]*>" + r".*?" + r"", + lambda match: protect(match.group(0)), + text, + ) + + text = re.sub( + r"(?s)", + lambda match: protect(match.group(0)), + text, + ) + + # ================================================================ + # 3. 保护 Markdown 链接地址 + # ================================================================ + + text = re.sub( + r"(!?\[[^\]\n]*\]\()([^)\n]*)(\))", + lambda match: ( + match.group(1) + + protect(match.group(2)) + + match.group(3) + ), + text, + ) + + text = re.sub( + r"(?im)" + r"^([ \t]{0,3}\[[^\]\n]+\]:[ \t]*)" + r"(\S+(?:[ \t]+.*)?)$", + lambda match: ( + match.group(1) + + protect(match.group(2)) + ), + text, + ) + + # ================================================================ + # 4. 保护 Markdown 行内代码 + # ================================================================ + + inline_code_result: list[str] = [] + position = 0 + + while position < len(text): + if text[position] != "`": + inline_code_result.append(text[position]) + position += 1 + continue + + delimiter_end = position + + while ( + delimiter_end < len(text) + and text[delimiter_end] == "`" + ): + delimiter_end += 1 + + delimiter = text[position:delimiter_end] + search_position = delimiter_end + closing_position = -1 + + while search_position < len(text): + candidate = text.find( + delimiter, + search_position, + ) + + if candidate < 0: + break + + previous_character = ( + text[candidate - 1] + if candidate > 0 + else "" + ) + + candidate_end = candidate + len(delimiter) + + next_character = ( + text[candidate_end] + if candidate_end < len(text) + else "" + ) + + if ( + previous_character != "`" + and next_character != "`" + ): + closing_position = candidate + break + + search_position = candidate + 1 + + if closing_position < 0: + inline_code_result.append(delimiter) + position = delimiter_end + continue + + end_position = closing_position + len(delimiter) + + inline_code_result.append( + protect(text[position:end_position]) + ) + + position = end_position + + text = "".join(inline_code_result) + + # ================================================================ + # 5. 转换其他 LaTeX 公式定界符 + # ================================================================ + + # \( x + y \) -> $x + y$ + text = re.sub( + r"(? $$x + y$$ + text = re.sub( + r"(?s)(? 0 + and text[position - 1] == "$" + ) + ) + + if not is_inline_opening: + inline_result.append(text[position]) + position += 1 + continue + + opening_position = position + + # 避免把 $100 识别成行内公式开始。 + next_opening_character = ( + text[position + 1] + if position + 1 < len(text) + else "" + ) + + if next_opening_character.isdigit(): + inline_result.append("$") + position += 1 + continue + + search_position = position + 1 + closing_position = -1 + + while search_position < len(text): + candidate = text.find( + "$", + search_position, + ) + + if candidate < 0: + break + + # 行内公式不能跨行。 + if "\n" in text[ + opening_position + 1:candidate + ]: + break + + if is_escaped(text, candidate): + search_position = candidate + 1 + continue + + # 跳过属于 $$ 的美元符号。 + if text.startswith("$$", candidate): + search_position = candidate + 2 + continue + + closing_position = candidate + break + + # 未找到闭合符时原样保留。 + if closing_position < 0: + inline_result.append("$") + position += 1 + continue + + formula_body = text[ + opening_position + 1:closing_position + ].strip() + + # 空公式不处理。 + if not formula_body: + inline_result.append( + text[ + opening_position:closing_position + 1 + ] + ) + + position = closing_position + 1 + continue + + # 折叠公式内部普通空格,不修改 LaTeX 命令。 + formula_body = re.sub( + r"[ \t]+", + " ", + formula_body, + ) + + previous_character = "" + + for part in reversed(inline_result): + if part: + previous_character = part[-1] + break + + next_character = ( + text[closing_position + 1] + if closing_position + 1 < len(text) + else "" + ) + + # 公式前只要存在非空白字符,就补空格。 + # + # 参数$T$ -> 参数 $T$ + # ($T$) -> ( $T$ + # ($T$) -> ( $T$ + if ( + previous_character + and not previous_character.isspace() + ): + inline_result.append(" ") + + inline_result.append( + "$" + formula_body + "$" + ) + + # 公式后只要存在非空白字符,就补空格。 + # + # $T$参数 -> $T$ 参数 + # ($T$) -> $T$ ) + # ($T$) -> $T$ ) + if ( + next_character + and not next_character.isspace() + ): + inline_result.append(" ") + + position = closing_position + 1 + + return restore("".join(inline_result)) + + +def remove_duplicate_content(text: str) -> str: + """ + 删除普通 Markdown 正文中的完全重复行。 + + 不对以下内容进行去重: + - $$ ... $$ 段落公式 + - \\[ ... \\] 段落公式 + - equation、align、gather 等 LaTeX 环境 + - Markdown 围栏代码块 + - Markdown 缩进代码块 + - Markdown 标题、列表、引用、表格等结构行 + + 这样不会删除段落公式末尾的第二个 $$。 + """ + if not text or len(text) < 10: + return text + + if not isinstance(text, str): + raise TypeError( + f"text 必须是 str,实际类型为 {type(text).__name__}" + ) + + text = text.replace("\r\n", "\n").replace("\r", "\n") + lines = text.split("\n") + + result: list[str] = [] + seen_segments: set[str] = set() + + in_code_block = False + code_fence_character = "" + code_fence_length = 0 + + in_dollar_math = False + in_bracket_math = False + + latex_environment_stack: list[str] = [] + + display_environments = { + "equation", + "equation*", + "align", + "align*", + "alignat", + "alignat*", + "gather", + "gather*", + "multline", + "multline*", + "flalign", + "flalign*", + "eqnarray", + "eqnarray*", + "displaymath", + "math", + } + + def is_escaped(value: str, position: int) -> bool: + slash_count = 0 + position -= 1 + + while position >= 0 and value[position] == "\\": + slash_count += 1 + position -= 1 + + return slash_count % 2 == 1 + + def count_unescaped_double_dollars(value: str) -> int: + """ + 统计一行中未转义的 $$ 数量。 + """ + count = 0 + position = 0 + + while position < len(value) - 1: + if ( + value[position:position + 2] == "$$" + and not is_escaped(value, position) + ): + count += 1 + position += 2 + else: + position += 1 + + return count + + def is_markdown_structure_line(value: str) -> bool: + """ + Markdown 结构行不参与去重,避免破坏语义。 + """ + stripped = value.strip() + + if not stripped: + return True + + patterns = ( + # 标题 + r"^#{1,6}\s+", + + # 无序列表 + r"^[-+*]\s+", + + # 有序列表 + r"^\d+[.)]\s+", + + # 引用 + r"^>\s*", + + # 任务列表 + r"^[-+*]\s+\[[ xX]\]\s+", + + # 水平分隔线 + r"^(?:-{3,}|\*{3,}|_{3,})$", + + # 表格分隔行 + r"^\|?\s*:?-{3,}:?" + r"(?:\s*\|\s*:?-{3,}:?)+\s*\|?$", + + # 普通表格行 + r"^\|.*\|$", + + # HTML 标签 + r"^]*>$", + + # LaTeX 环境边界 + r"^\\(?:begin|end)\{[^}]+\}", + + # 公式定界符 + r"^(?:\$\$|\\\[|\\\])$", + + # Markdown 链接引用定义 + r"^\[[^\]]+\]:\s*\S+", + ) + + return any( + re.match(pattern, stripped) + for pattern in patterns + ) + + for line in lines: + line = line.rstrip() + stripped = line.strip() + + # ============================================================ + # 1. Markdown 围栏代码块 + # ============================================================ + + fence_match = re.match( + r"^[ \t]{0,3}(`{3,}|~{3,})", + line, + ) + + if not in_code_block and fence_match: + fence = fence_match.group(1) + + in_code_block = True + code_fence_character = fence[0] + code_fence_length = len(fence) + + result.append(line) + continue + + if in_code_block: + result.append(line) + + closing_pattern = re.compile( + rf"^[ \t]{{0,3}}" + rf"{re.escape(code_fence_character)}" + rf"{{{code_fence_length},}}" + rf"[ \t]*$" + ) + + if closing_pattern.match(line): + in_code_block = False + code_fence_character = "" + code_fence_length = 0 + + continue + + # Markdown 缩进代码块不参与去重。 + if re.match(r"^(?:\t| {4})", line): + result.append(line) + continue + + # ============================================================ + # 2. \[ ... \] 段落公式 + # ============================================================ + + if in_bracket_math: + result.append(line) + + if re.search(r"(? bracket_close_count: + in_bracket_math = True + + continue + + # ============================================================ + # 3. LaTeX display 环境 + # ============================================================ + + begin_matches = re.findall( + r"\\begin\{([^}]+)\}", + line, + ) + + end_matches = re.findall( + r"\\end\{([^}]+)\}", + line, + ) + + relevant_begins = [ + environment + for environment in begin_matches + if environment in display_environments + ] + + relevant_ends = [ + environment + for environment in end_matches + if environment in display_environments + ] + + if latex_environment_stack: + result.append(line) + + for environment in relevant_begins: + latex_environment_stack.append(environment) + + for environment in relevant_ends: + if ( + latex_environment_stack + and latex_environment_stack[-1] + == environment + ): + latex_environment_stack.pop() + elif environment in latex_environment_stack: + latex_environment_stack.remove(environment) + + continue + + if relevant_begins: + result.append(line) + + for environment in relevant_begins: + latex_environment_stack.append(environment) + + for environment in relevant_ends: + if ( + latex_environment_stack + and latex_environment_stack[-1] + == environment + ): + latex_environment_stack.pop() + elif environment in latex_environment_stack: + latex_environment_stack.remove(environment) + + continue + + # ============================================================ + # 4. $$ ... $$ 段落公式 + # ============================================================ + + double_dollar_count = ( + count_unescaped_double_dollars(line) + ) + + if in_dollar_math: + # 公式块中的内容全部保留,包括最后一行的 $$。 + result.append(line) + + if double_dollar_count % 2 == 1: + in_dollar_math = False + + continue + + if double_dollar_count > 0: + # 含有 $$ 的行永远不参与去重。 + result.append(line) + + # 奇数个 $$ 代表开启了跨行段落公式。 + if double_dollar_count % 2 == 1: + in_dollar_math = True + + continue + + # ============================================================ + # 5. 空行和 Markdown 结构行 + # ============================================================ + + if not stripped: + result.append(line) + continue + + if is_markdown_structure_line(line): + result.append(line) + continue + + # ============================================================ + # 6. 仅对普通正文行去重 + # ============================================================ + + normalized_line = re.sub( + r"\s+", + " ", + stripped.lower(), + ) + + if normalized_line in seen_segments: + continue + + seen_segments.add(normalized_line) + result.append(line) + + return "\n".join(result) + + + + +# 推荐调用顺序: +# +# text = remove_duplicate_content(text) +# text = normalize_latex_spacing(text) + + + +def _extract_rag_text(item: Any) -> str: + if isinstance(item, dict): + return str(item.get("text") or item.get("content") or "") + if isinstance(item, str): + return item + return "" + + +def _extract_rag_score(item: Any) -> float: + if not isinstance(item, dict): + return 0.0 + try: + return float(item.get("score") or item.get("socre") or 0.0) + except (TypeError, ValueError): + return 0.0 + + +def _normalize_rag_text_for_dedupe(text: Any) -> str: + value = unicodedata.normalize("NFKC", str(text or "")) + value = re.sub(r"!\[[^\]]*\]\([^)]+\)", "", value) + value = re.sub(r"\[([^\]]*)\]\([^)]+\)", r"\1", value) + value = re.sub(r"https?://\S+", "", value, flags=re.IGNORECASE) + value = re.sub(r"[/\\]?picture[/\\]\S+", "", value, flags=re.IGNORECASE) + value = re.sub(r"\s+", "", value).lower() + return "".join(ch for ch in value if ch.isalnum()) + + +def _char_ngrams(text: str, n: int) -> set: + if len(text) <= n: + return {text} if text else set() + return {text[i:i + n] for i in range(len(text) - n + 1)} + + +def _rag_texts_are_similar(text_a: str, text_b: str, threshold: float) -> bool: + if not text_a or not text_b: + return False + if text_a == text_b: + return True + + min_len = min(len(text_a), len(text_b)) + max_len = max(len(text_a), len(text_b)) + if min_len < 8: + return False + + if min_len >= RAG_DEDUP_MIN_TEXT_LENGTH and (text_a in text_b or text_b in text_a): + return (min_len / max_len) >= RAG_DEDUP_CONTAINMENT_THRESHOLD + + if min_len < RAG_DEDUP_MIN_TEXT_LENGTH: + return SequenceMatcher(None, text_a, text_b).ratio() >= 0.95 + + ngram_size = 3 if min_len >= 30 else 2 + grams_a = _char_ngrams(text_a, ngram_size) + grams_b = _char_ngrams(text_b, ngram_size) + if not grams_a or not grams_b: + return False + + jaccard = len(grams_a & grams_b) / len(grams_a | grams_b) + if jaccard >= threshold: + return True + + return SequenceMatcher(None, text_a, text_b).ratio() >= threshold + + +def dedupe_rag_results( + items: Any, + limit: Optional[int] = None, + similarity_threshold: float = RAG_DEDUP_SIMILARITY_THRESHOLD, +) -> Any: + """ + Deduplicate RAG result chunks while preserving the original item shape. + + Higher-score items are kept first. Graph results are intentionally not handled + here; callers should pass only text RAG chunks. + """ + if not isinstance(items, list): + return items + if len(items) < 2: + return items[:limit] if limit else items + + prepared = [] + for idx, item in enumerate(items): + prepared.append({ + "item": item, + "index": idx, + "score": _extract_rag_score(item), + "normalized_text": _normalize_rag_text_for_dedupe(_extract_rag_text(item)), + }) + + prepared.sort(key=lambda entry: (-entry["score"], entry["index"])) + + kept = [] + for entry in prepared: + current_text = entry["normalized_text"] + is_duplicate = False + if current_text: + for kept_entry in kept: + if _rag_texts_are_similar( + current_text, + kept_entry["normalized_text"], + similarity_threshold, + ): + is_duplicate = True + break + if not is_duplicate: + kept.append(entry) + + deduped = [entry["item"] for entry in kept] + return deduped[:limit] if limit else deduped + + +def convert_rag_result(rag_result): + if not rag_result or not rag_result.get("success", False): + return [] + source_citation = rag_result.get("sourceCitation", {}) + flat_results = [] + for resource, items in source_citation.items(): + for item in items: + flat_results.append(item) + return flat_results + + +def calculate_info_completeness(weights: Dict[str, float], values: Dict[str, str]) -> float: + score = 0.0 + for field, weight in weights.items(): + value = values.get(field, "") or "" + if value and str(value).strip(): + score += weight + return round(score, 2) + + +def emit_callback_event(title_options: List[str], details: str): + start_event = {"type": "function_execution", "title": random.choice(title_options), "details": details} + for callback in get_all_callbacks(): + try: + callback(start_event) + except Exception: + pass + + +async def resolve_ship_number_for_workflow( + ship_number: str, + context: str = "workflow", +) -> tuple[str, Dict[str, Any]]: + """ + 工作流内舷号/舰名标准化。 + + 能映射到具体舷号时返回映射后的舷号;失败或未匹配时返回原值。 + """ + original_ship_number = str(ship_number or "").strip() + if not original_ship_number: + return original_ship_number, {} + + try: + from utils.ship_number_search import smart_ship_number_mapping + + matched_ship_number, matched_model, all_numbers, match_type = await smart_ship_number_mapping( + original_ship_number + ) + result = { + "matched_ship_number": matched_ship_number, + "matched_model": matched_model, + "all_numbers": all_numbers, + "match_type": match_type, + } + if match_type in ["exact_number", "exact_name", "embedding"] and matched_ship_number: + mapped_ship_number = str(matched_ship_number).strip() + if mapped_ship_number and mapped_ship_number != original_ship_number: + print( + f"[{context}] 舷号映射: '{original_ship_number}' -> " + f"'{mapped_ship_number}',匹配类型={match_type}" + ) + return mapped_ship_number or original_ship_number, result + + print(f"[{context}] 舷号未映射,继续使用原值: {original_ship_number}") + return original_ship_number, result + except Exception as e: + print(f"[{context}] 舷号映射异常,继续使用原值: {original_ship_number}; 错误: {str(e)}") + return original_ship_number, {} + + +async def resolve_ship_scope_for_workflow( + ship_number: str, + context: str = "workflow", +) -> tuple[str, List[str], Dict[str, Any]]: + """ + Resolve user ship input into a display hull number and a hull-number scope. + + Hull number and ship-name matches collapse to one hull. Model matches keep the + user's original value for display but return every hull number under that model + so shared device normalization and system lookup can search the whole scope. + """ + original_ship_number = str(ship_number or "").strip() + if not original_ship_number: + return original_ship_number, [], {} + + try: + from utils.ship_number_search import smart_ship_number_mapping + + matched_ship_number, matched_model, all_numbers, match_type = await smart_ship_number_mapping( + original_ship_number + ) + scope_numbers = [str(num).strip() for num in (all_numbers or []) if str(num or "").strip()] + result = { + "matched_ship_number": matched_ship_number, + "matched_model": matched_model, + "all_numbers": scope_numbers, + "match_type": match_type, + } + + if match_type in ["exact_number", "exact_name", "embedding"] and matched_ship_number: + mapped_ship_number = str(matched_ship_number).strip() + return mapped_ship_number or original_ship_number, [mapped_ship_number], result + + if match_type == "exact_model" and scope_numbers: + print( + f"[{context}] ship model scope resolved: '{original_ship_number}' -> " + f"{matched_model}, hull_numbers={scope_numbers}" + ) + return original_ship_number, scope_numbers, result + + print(f"[{context}] ship scope not resolved, continuing with original value: {original_ship_number}") + return original_ship_number, [], result + except Exception as e: + print(f"[{context}] ship scope resolve failed, continuing with original value: {original_ship_number}; error: {str(e)}") + return original_ship_number, [], {} + + +async def normalize_device_name_for_workflow( + device_name: str, + ship_number: str = "", + ship_numbers: Optional[List[str]] = None, + context: str = "workflow", +) -> tuple[str, Dict[str, Any]]: + """ + 工作流内设备名称标准化。 + + 标准化失败时返回原设备名,避免检索流程被 Neo4j 或 embedding 服务异常阻断。 + """ + original_device_name = str(device_name or "").strip() + if not original_device_name: + return original_device_name, {} + + try: + from tools.graph_tools import normalize_device_name_by_ship_graph + + print( + f"[{context}] 开始设备名称标准化: device='{original_device_name}', " + f"ship_number='{ship_number or '未提供'}'" + ) + call_kwargs = {} + if "ship_number" in inspect.signature(normalize_device_name_by_ship_graph).parameters: + call_kwargs["ship_number"] = str(ship_number or "").strip() or None + if "ship_numbers" in inspect.signature(normalize_device_name_by_ship_graph).parameters: + scoped_numbers = [str(num).strip() for num in (ship_numbers or []) if str(num or "").strip()] + call_kwargs["ship_numbers"] = scoped_numbers or None + elif ship_number: + print(f"[{context}] 当前进程加载的设备标准化函数不支持 ship_number,请重启服务加载最新 graph_tools.py") + + result = await normalize_device_name_by_ship_graph(original_device_name, **call_kwargs) + if result.get("success", False): + normalized_device_name = str(result.get("device_name") or original_device_name).strip() + print( + f"[{context}] 设备名称标准化完成: '{original_device_name}' -> " + f"'{normalized_device_name or original_device_name}',舷号={ship_number or '未提供'}" + ) + emit_callback_event( + ["🔎 设备名称标准化", "🔧 设备匹配"], + f"{original_device_name} -> {normalized_device_name or original_device_name}" + ) + return normalized_device_name or original_device_name, result + + print( + f"[{context}] 设备名称标准化失败,继续使用原设备名: {original_device_name}; " + f"原因: {result.get('error', '未知错误')}" + ) + return original_device_name, result + except Exception as e: + print(f"[{context}] 设备名称标准化异常,继续使用原设备名: {original_device_name}; 错误: {str(e)}") + return original_device_name, {} + + +async def resolve_device_system_for_workflow( + device_name: str, + ship_number: str = "", + ship_numbers: Optional[List[str]] = None, + context: str = "workflow", +) -> tuple[str, Dict[str, Any]]: + """ + 工作流内根据设备名称反推所属系统。 + + 失败时返回空字符串,调用方可继续执行主流程。 + """ + device_name = str(device_name or "").strip() + if not device_name: + return "", {} + + try: + print( + f"[{context}] 开始设备反推系统: device='{device_name}', " + f"ship_number='{ship_number or '未提供'}'" + ) + from tools.graph_tools import find_system_by_device + + call_kwargs = {"device_name": device_name} + if "ship_number" in inspect.signature(find_system_by_device).parameters: + call_kwargs["ship_number"] = str(ship_number or "").strip() or None + if "ship_numbers" in inspect.signature(find_system_by_device).parameters: + scoped_numbers = [str(num).strip() for num in (ship_numbers or []) if str(num or "").strip()] + call_kwargs["ship_numbers"] = scoped_numbers or None + elif ship_number: + print(f"[{context}] 当前进程加载的设备反推系统函数不支持 ship_number,请重启服务加载最新 graph_tools.py") + + result = await find_system_by_device(**call_kwargs) + if not result.get("success", False): + print(f"[{context}] 设备反推系统失败: {result.get('error', '未知错误')}") + return "", result + + systems = result.get("systems") or [] + first_system = systems[0] if systems else {} + system_name = "" + if isinstance(first_system, dict): + system_name = str(first_system.get("name") or first_system.get("系统名称") or first_system.get("名称") or "").strip() + + if system_name: + print(f"[{context}] 设备反推系统完成: {device_name} -> {system_name}") + emit_callback_event( + ["🔎 设备所属系统识别", "🧭 系统反查"], + f"{device_name} -> {system_name}" + ) + else: + print(f"[{context}] 设备反推系统无结果: {device_name}") + return system_name, result + except Exception as e: + print(f"[{context}] 设备反推系统异常: {str(e)}") + return "", {} + + +def build_detail_info(fields: Dict[str, str]) -> str: + detail_info = "" + for label, value in fields.items(): + if value: + detail_info += f"- {label}:{value}\n" + return detail_info + + +def append_atlas_section(answer: str, atlas_results: Dict[str, Any]) -> str: + if not atlas_results: + return answer + atlas_section = "\n\n---\n\n**参考图册:**\n" + for node_name, node_data in atlas_results.items(): + if isinstance(node_data, dict): + for title, urls in node_data.items(): + if isinstance(urls, list) and urls: + for url in urls: + atlas_section += f"- {title}: [ `{title}` ]({url})\n" + return answer + atlas_section + + + + +async def stream_generate_and_postprocess( + system_prompt: str, + messages: List[Dict[str, Any]], + original_image_paths: List[str], + temperature: float = 0.3, + fallback: str = "抱歉,无法生成回答。", +) -> str: + """单次流式生成最终回复,并在本地做稳定的格式与图片校验。""" + stream_handler = get_current_stream_handler() + answer = "" + accumulated_content = "" + + async for chunk in OpenaiAPI.open_api_chat_stream( + model=None, + system_prompt=system_prompt, + messages=messages, + temperature=temperature, + ): + answer += chunk + accumulated_content += chunk + if stream_handler: + stream_handler.send_stream_content(accumulated_content) + + answer = answer.strip() if answer else fallback + print("111-",answer) + answer = re.sub(r'ynchroneg>.*?ost switching>', '', answer, flags=re.DOTALL) + print("222-",answer) + answer = remove_duplicate_content(answer) + print("333-",answer) + answer = normalize_markdown_images(answer, original_paths=original_image_paths) + print("444-",answer) + answer = normalize_latex_spacing(answer) + + return answer.strip() if answer else fallback +