wx-agent/workflows/workflow_operate.py
2026-07-15 16:30:58 +08:00

1563 lines
76 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
操作指导工作流 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 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, dedupe_rag_results, calculate_info_completeness,
emit_callback_event, build_history_str, build_detail_info,
append_atlas_section, stream_generate_and_postprocess,
normalize_device_name_for_workflow,
resolve_ship_number_for_workflow,
resolve_ship_scope_for_workflow,
resolve_device_system_for_workflow,
)
from prompts import OPERATE_PROMPTS, COMMON_PROMPTS
import asyncio
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
system_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}
HISTORY_RECENT_COUNT = 12
HISTORY_ASSISTANT_TRUNCATE = 1000
RAG_TOP_K = 8
DEEP_RAG_TOP_K = 6
ATLAS_TOP_K = 20
GRAPH_TOP_K = 240
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 ""
system_name = state.get("system_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=HISTORY_RECENT_COUNT)
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 ""
system_name = state.get("system_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=HISTORY_RECENT_COUNT, assistant_truncate=HISTORY_ASSISTANT_TRUNCATE)
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 ""
system_name = state.get("system_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 ""
system_name = state.get("system_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 ""
system_name = state.get("system_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 = ""
mapped_ship_number = ""
ship_scope_numbers = []
if ship_number:
ship_number, ship_scope_numbers, _ = await resolve_ship_scope_for_workflow(
ship_number=ship_number,
context="操作指导-call_tools",
)
if device_name:
device_name, _ = await normalize_device_name_for_workflow(
device_name=device_name,
ship_number=ship_number,
ship_numbers=ship_scope_numbers,
context="操作指导-call_tools",
)
system_name, _ = await resolve_device_system_for_workflow(
device_name=device_name,
ship_number=ship_number,
ship_numbers=ship_scope_numbers,
context="操作指导-call_tools",
)
async def _do_rag_search():
from utils.ship_number_search import search_with_ship_number_strategy
base_query = f"设备{device_name},操作{operation_item}的操作指导方案"
results, kb_name, kb_id, mapped_number = await search_with_ship_number_strategy(
base_query=base_query, ship_number=ship_number, top_k=RAG_TOP_K, search_type="operate"
)
if not kb_name:
kb_name = "通用知识库"
print(f"RAG 检索来源: {kb_name}")
return results, kb_name, kb_id, mapped_number
async def _do_graph_search():
if not (device_name and operation_item):
return ""
try:
graph_query = f"设备{device_name},操作{operation_item}的操作指导方案"
results = await graph_rag_search.ainvoke({"query": graph_query, "top_k": GRAPH_TOP_K})
print(f"图谱检索完成,结果长度: {len(results) if results else 0}")
return results
except Exception as e:
print(f"图谱检索失败: {str(e)}")
return ""
need_rag = "rag_search" in tools_to_call and rag_results is None
need_graph = "graph_rag_search" in tools_to_call and graph_results is None
if need_rag:
rag_results, matched_kb_name, matched_kb_id, mapped_ship_number = await _do_rag_search()
if mapped_ship_number and mapped_ship_number != ship_number:
print(f"[call_tools] RAG 舷号映射: '{ship_number}' -> '{mapped_ship_number}',重新标准化设备名")
ship_number = mapped_ship_number
if device_name:
device_name, _ = await normalize_device_name_for_workflow(
device_name=device_name,
ship_number=ship_number,
ship_numbers=[ship_number],
context="操作指导-call_tools-rag_mapped",
)
system_name, _ = await resolve_device_system_for_workflow(
device_name=device_name,
ship_number=ship_number,
ship_numbers=[ship_number],
context="操作指导-call_tools-rag_mapped",
)
if need_graph:
graph_results = await _do_graph_search()
if device_name:
try:
print(f"[图册检索] 使用设备名称: {device_name}")
atlas_result = await atlas_retrieval(node_names=[device_name], top_k=ATLAS_TOP_K)
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,"device_name": device_name,"system_name": system_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,"ship_number": ship_number,"device_name": device_name,"system_name": system_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 ""
system_name = state.get("system_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=HISTORY_RECENT_COUNT, assistant_truncate=HISTORY_ASSISTANT_TRUNCATE)
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=[])
response = normalize_markdown_images(response.strip(), original_paths=[]) if response else ""
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",}
if len(graph_results) <= 50:
graph_results = ""
rag_ctx_parts: List[str] = []
if not graph_results:
if isinstance(rag_results, str) and rag_results.strip():
rag_ctx_parts = [rag_results.strip()]
elif isinstance(rag_results, list):
deduped_rag_results = dedupe_rag_results(rag_results, limit=4)
for item in deduped_rag_results:
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())
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"
original_image_paths = extract_image_paths(rag_answer)
print(f"提取到的{rag_type}原始图片路径:", original_image_paths)
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=[])
response = normalize_markdown_images(response.strip(), original_paths=[]) if response else ""
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 system_name:
detail_info += f"- 所属系统:{system_name}\n"
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="操作指导")
emit_callback_event(["🤖 操作分析中", "🤖 方案生成中"], "正在生成操作指导方案...")
answer = await stream_generate_and_postprocess(
system_prompt=content_system_prompt,
messages=[{"role": "user", "content": prompt}],
original_image_paths=original_image_paths,
temperature=0.1,
fallback="抱歉,无法生成回答。"
)
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 ""
system_name = state.get("system_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 system_name:
detail_info += f"- 所属系统:{system_name}\n"
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="操作指导")
original_image_paths = extract_image_paths(last_scheme)
answer = await stream_generate_and_postprocess(
system_prompt=content_system_prompt,
messages=[{"role": "user", "content": prompt}],
original_image_paths=original_image_paths,
temperature=0.3,
fallback="抱歉,无法根据反馈修改方案。"
)
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 ""
system_name = state.get("system_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 ""
ship_scope_numbers = []
if ship_number:
ship_number, ship_scope_numbers, _ = await resolve_ship_scope_for_workflow(
ship_number=ship_number,
context="操作指导-deep_rag_search",
)
if device_name:
device_name, _ = await normalize_device_name_for_workflow(
device_name=device_name,
ship_number=ship_number,
ship_numbers=ship_scope_numbers,
context="操作指导-deep_rag_search",
)
system_name, _ = await resolve_device_system_for_workflow(
device_name=device_name,
ship_number=ship_number,
ship_numbers=ship_scope_numbers,
context="操作指导-deep_rag_search",
)
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=HISTORY_RECENT_COUNT, assistant_truncate=HISTORY_ASSISTANT_TRUNCATE)
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=DEEP_RAG_TOP_K)
if rag_result.get("success", False):
deep_results[f"analysis_point_{idx}"] = dedupe_rag_results(
convert_rag_result(rag_result),
limit=DEEP_RAG_TOP_K,
)
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, "top_k": GRAPH_TOP_K})
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=ATLAS_TOP_K)
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,"ship_number": ship_number,"device_name": device_name,"system_name": system_name,"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 ""
system_name = state.get("system_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(sorted(results, key=lambda x: float(x.get("score") or 0) if isinstance(x, dict) else 0, reverse=True)[: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(dedupe_rag_results(original_rag_results, limit=2), 1):
if isinstance(item, dict):
text = item.get("text", "")
if text:
original_rag_text += f"[{i}] {text}\n"
if graph_results and len(graph_results) > 50:
original_image_paths = extract_image_paths(graph_results)
print("提取到的deep graph原始图片路径:", original_image_paths)
else:
original_image_paths = extract_image_paths(analysis_points_text + original_rag_text)
print("提取到的deep rag原始图片路径:", original_image_paths)
detail_info = ""
if system_name:
detail_info += f"- 所属系统:{system_name}\n"
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="操作指导")
emit_callback_event(["🤖 分析中", "🤖 操作指导生成中"], "正在生成深度操作指导...")
answer = await stream_generate_and_postprocess(
system_prompt=content_system_prompt,
messages=[{"role": "user", "content": prompt}],
original_image_paths=original_image_paths,
temperature=0.1,
fallback="抱歉,无法生成深度分析。"
)
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": "","system_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": "","system_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,}