from app.tools import SQLQueryTool,WebSearchTool,VectorSearchTool from app.decision_engine import DeepSeekDecisionEngine from app.config import Settings from dataclasses import dataclass from concurrent.futures import ThreadPoolExecutor from langgraph.graph import END, START, StateGraph from app.state import AgentState from app.schemas import RouteName, ToolResult,PlanStep @dataclass class GraphBuilder: settings:Settings engine:DeepSeekDecisionEngine sql_tool:SQLQueryTool vector_tool:VectorSearchTool web_tool:WebSearchTool def trace_event(state:AgentState,node:str,detail:dict)->list[dict]: return[ *state.get("trace",[]), { "node":node, "detail":detail } ] def build_graph(builder:GraphBuilder): def normalize_query(state: AgentState) -> dict: normalized = " ".join(state["original_query"].strip().split()) return { "current_query": normalized, "retrieval_round": state.get("retrieval_round", 0), "trace": trace_event(state, "normalize_query", {"query": normalized}), } def route_query(state:AgentState)->dict: decision=builder.engine.route(state["current_query"],builder.settings.max_retrieval_rounds) return{ "route_decision":decision, "trace":trace_event( state, node="route_query", detail={ "routes": decision.routes } ) } def after_route(state:AgentState)->str: routes=set(state["route_decision"].routes) if RouteName.CLARIFY in routes: return "clarify" if RouteName.REFUSE in routes: return "refuse" if RouteName.DIRECT_ANSWER in routes: return "generate" return "plan" def plan_query(state:AgentState)->dict: plan=builder.engine.plan(state["current_query"],state["route_decision"]) return{ "retrieval_plan":plan, "trace":trace_event( state, node="plan_query", detail={ "steps":plan.steps } ) } def execute_step(step:PlanStep)->ToolResult: if step.tool==RouteName.MILVUS_SEARCH: return builder.vector_tool.invoke( query=step.query, service=step.arguments.get("service",""), fault_type=step.arguments.get("fault_type",""), client_os=step.arguments.get("client_os",""), top_k=step.arguments.get("top_k",5) ) if step.tool==RouteName.SQL_QUERY: return builder.sql_tool.invoke( service=step.arguments.get("service",""), fault_type=step.arguments.get("fault_type",""), days=step.arguments.get("days",30), client_os=step.arguments.get("client_os","") ) if step.tool==RouteName.WEB_SEARCH: return builder.web_tool.invoke( query=step.query, max_results=step.arguments.get("max_results",5) ) return ToolResult( status="error", tool=step.tool.value, error_code="UNSUPPORTED_TOOL", error_message=f"未注册工具:{step.tool.value}", ) def execute_plan(state:AgentState)->dict: steps=state["retrieval_plan"].steps if not steps: results:list[ToolResult]=[] else: with ThreadPoolExecutor(max_workers=min(len(steps),4)) as executor: results=list(executor.map(execute_step, steps)) evidence=[] for result in results: evidence.extend(result.evidence) errors = [ { "tool": result.tool, "code": result.error_code, "message": result.error_message, } for result in results if result.status == "error" ] executed_queries = [ { "step_id": step.id, "tool": result.tool, "query": step.query, "arguments": step.arguments, "status": result.status, "latency_ms": result.latency_ms, } for step, result in zip(steps, results, strict=True) ] return { "tool_results": results, "executed_queries": executed_queries, "evidence": evidence, "errors": [*state.get("errors", []), *errors], "trace": trace_event( state, "execute_plan", { "tools": [result.tool for result in results], "statuses": [result.status for result in results], "evidence_count": len(evidence), "executed_queries": executed_queries, }, ), } def grade_evidence(state:AgentState)->dict: current_round = state.get("retrieval_round", 0) max_rounds = state["route_decision"].max_rounds grade=builder.engine.grade( query=state["current_query"], decision=state["route_decision"], evidence=state["evidence"], current_round=current_round, max_rounds=max_rounds, min_score=builder.settings.min_evidence_score ) if(grade.recommended_action=="rewrite_query" and current_round >= max_rounds): grade = grade.model_copy( update={ "sufficient": False, "recommended_action": "stop", "reason": ( f"{grade.reason};已达到最大检索轮数 {max_rounds}," "程序层强制停止。" ), } ) return { "quality_grade":grade, "trace":trace_event( state=state, node="grade_evidence", detail=grade.model_dump(mode="json"), ), } def after_grade(state:AgentState)->str: action=state["quality_grade"].recommended_action if action=="accept": return "generate" if action=="rewrite_query": return "rewrite" return "generate_partial" def rewrite_query(state:AgentState)->dict: rewritten=builder.engine.rewrite( query=state["current_query"], grade=state["quality_grade"]) next_round=state.get("retrieval_round",0)+1 return{ "current_query":rewritten, "retrieval_round":next_round, "trace":trace_event(state=state, node="rewrite_query", detail={ "round":next_round, "rewritten":rewritten }) } def generate_answer(state: AgentState) -> dict: answer = builder.engine.answer( state["original_query"], state.get("evidence", []), partial=False ) return { "final_answer": answer, "termination_reason": "evidence_accepted" if state.get("evidence") else "direct_answer", "trace": trace_event(state, "generate_answer", {"partial": False}), } def generate_partial_answer(state: AgentState) -> dict: answer = builder.engine.answer( state["original_query"], state.get("evidence", []), partial=True ) return { "final_answer": answer, "termination_reason": "retrieval_budget_exhausted", "trace": trace_event(state, "generate_partial_answer", {"partial": True}), } def clarify(state: AgentState) -> dict: return { "final_answer": "当前流程仅处理阿里云 SSL-VPN 的认证超时故障;请确认是否出现认证超时提示,并补充客户端系统或版本信息。", "termination_reason": "clarification_required", "trace": trace_event(state, "clarify", {}), } def refuse(state: AgentState) -> dict: return { "final_answer": "当前请求涉及受限数据,系统拒绝执行。", "termination_reason": "security_policy", "trace": trace_event(state, "refuse", {}), } graph=StateGraph(AgentState) graph.add_node("normalize_query",normalize_query) graph.add_node("route_query",route_query) graph.add_node("plan_query",plan_query) graph.add_node("execute_plan",execute_plan) graph.add_node("grade_evidence",grade_evidence) graph.add_node("rewrite_query",rewrite_query) graph.add_node("generate_answer",generate_answer) graph.add_node("generate_partial_answer",generate_partial_answer) graph.add_node("clarify",clarify) graph.add_node("refuse",refuse) graph.add_edge(START,"normalize_query") graph.add_edge("normalize_query","route_query") graph.add_conditional_edges( "route_query", after_route, { "clarify": "clarify", "refuse": "refuse", "generate": "generate_answer", "plan": "plan_query", } ) graph.add_edge("plan_query","execute_plan") graph.add_edge("execute_plan","grade_evidence") graph.add_conditional_edges( "grade_evidence", after_grade, { "generate":"generate_answer", "rewrite":"rewrite_query", "generate_partial":"generate_partial_answer" } ) graph.add_edge("rewrite_query","plan_query") graph.add_edge("generate_answer",END) graph.add_edge("generate_partial_answer",END) graph.add_edge("clarify", END) graph.add_edge("refuse", END) return graph.compile()