refactor(server): steward 决策链路改用 LangGraph 编排
- 新增 StewardGraphPlannerService,用 LangGraph 状态图编排意图识别→流程判断→模型/规则分支→兜底,替代原 planner 内线性调用 - 新增 StewardGraphRuntimeService 编排运行时决策与槽位决策;StewardActionContracts/Executor 统一动作合约与执行 - steward_intent_agent/application_fact_resolver/runtime_chat 适配图执行器,config 暴露图相关开关 - pyproject/uv.lock 新增 langgraph 依赖 - 新增 graph_planner/graph_runtime/action_executor 测试,更新 intent_agent/planner/fact_resolver/runtime_chat/reimbursement 测试
This commit is contained in:
214
server/src/app/services/steward_graph_runtime.py
Normal file
214
server/src/app/services/steward_graph_runtime.py
Normal file
@@ -0,0 +1,214 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
|
||||
from app.schemas.steward import (
|
||||
StewardRuntimeDecisionRequest,
|
||||
StewardRuntimeDecisionResponse,
|
||||
StewardSlotDecisionRequest,
|
||||
StewardSlotDecisionResponse,
|
||||
)
|
||||
from app.services.runtime_chat import RuntimeChatService
|
||||
from app.services.steward_runtime_decision_agent import StewardRuntimeDecisionAgent
|
||||
from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent
|
||||
|
||||
|
||||
class StewardSlotGraphState(TypedDict, total=False):
|
||||
request: StewardSlotDecisionRequest
|
||||
normalized_request: StewardSlotDecisionRequest
|
||||
decision: StewardSlotDecisionResponse
|
||||
model_call_traces: list[dict[str, Any]]
|
||||
fallback_reason: str
|
||||
|
||||
|
||||
class StewardRuntimeGraphState(TypedDict, total=False):
|
||||
request: StewardRuntimeDecisionRequest
|
||||
normalized_request: StewardRuntimeDecisionRequest
|
||||
action_decision: StewardRuntimeDecisionResponse
|
||||
decision: StewardRuntimeDecisionResponse
|
||||
model_call_traces: list[dict[str, Any]]
|
||||
fallback_reason: str
|
||||
|
||||
|
||||
class StewardGraphRuntime:
|
||||
"""用 LangGraph 编排会话内槽位、记忆和行动决策。"""
|
||||
|
||||
def __init__(self, runtime_chat_service: RuntimeChatService) -> None:
|
||||
self.slot_agent = StewardSlotDecisionAgent(runtime_chat_service)
|
||||
self.runtime_agent = StewardRuntimeDecisionAgent(runtime_chat_service)
|
||||
self._slot_graph = self._build_slot_graph()
|
||||
self._runtime_graph = self._build_runtime_graph()
|
||||
|
||||
def decide_slot(self, request: StewardSlotDecisionRequest) -> StewardSlotDecisionResponse:
|
||||
final_state = self._slot_graph.invoke(
|
||||
{
|
||||
"request": request,
|
||||
"model_call_traces": [],
|
||||
"fallback_reason": "",
|
||||
}
|
||||
)
|
||||
decision = final_state.get("decision")
|
||||
if not isinstance(decision, StewardSlotDecisionResponse):
|
||||
raise RuntimeError("LangGraph 槽位决策未生成有效结果。")
|
||||
return decision
|
||||
|
||||
def decide_runtime(self, request: StewardRuntimeDecisionRequest) -> StewardRuntimeDecisionResponse:
|
||||
final_state = self._runtime_graph.invoke(
|
||||
{
|
||||
"request": request,
|
||||
"model_call_traces": [],
|
||||
"fallback_reason": "",
|
||||
}
|
||||
)
|
||||
decision = final_state.get("decision")
|
||||
if not isinstance(decision, StewardRuntimeDecisionResponse):
|
||||
raise RuntimeError("LangGraph 运行时决策未生成有效结果。")
|
||||
return decision
|
||||
|
||||
def _build_slot_graph(self):
|
||||
graph = StateGraph(StewardSlotGraphState)
|
||||
graph.add_node("slot_prepare_context", self._slot_prepare_context)
|
||||
graph.add_node("slot_tool_decision", self._slot_tool_decision)
|
||||
graph.add_node("slot_rule_fallback", self._slot_rule_fallback)
|
||||
|
||||
graph.add_edge(START, "slot_prepare_context")
|
||||
graph.add_edge("slot_prepare_context", "slot_tool_decision")
|
||||
graph.add_conditional_edges(
|
||||
"slot_tool_decision",
|
||||
self._route_after_slot_tool_decision,
|
||||
{
|
||||
"done": END,
|
||||
"fallback": "slot_rule_fallback",
|
||||
},
|
||||
)
|
||||
graph.add_edge("slot_rule_fallback", END)
|
||||
return graph.compile()
|
||||
|
||||
def _build_runtime_graph(self):
|
||||
graph = StateGraph(StewardRuntimeGraphState)
|
||||
graph.add_node("runtime_memory_context", self._runtime_memory_context)
|
||||
graph.add_node("runtime_action_decision", self._runtime_action_decision)
|
||||
graph.add_node("runtime_tool_decision", self._runtime_tool_decision)
|
||||
graph.add_node("runtime_rule_fallback", self._runtime_rule_fallback)
|
||||
|
||||
graph.add_edge(START, "runtime_memory_context")
|
||||
graph.add_conditional_edges(
|
||||
"runtime_memory_context",
|
||||
self._route_after_runtime_memory_context,
|
||||
{
|
||||
"action": "runtime_action_decision",
|
||||
"tool": "runtime_tool_decision",
|
||||
},
|
||||
)
|
||||
graph.add_edge("runtime_action_decision", END)
|
||||
graph.add_conditional_edges(
|
||||
"runtime_tool_decision",
|
||||
self._route_after_runtime_tool_decision,
|
||||
{
|
||||
"done": END,
|
||||
"fallback": "runtime_rule_fallback",
|
||||
},
|
||||
)
|
||||
graph.add_edge("runtime_rule_fallback", END)
|
||||
return graph.compile()
|
||||
|
||||
def _slot_prepare_context(self, state: StewardSlotGraphState) -> dict[str, Any]:
|
||||
return {
|
||||
"normalized_request": self.slot_agent._normalize_request(state["request"]),
|
||||
}
|
||||
|
||||
def _slot_tool_decision(self, state: StewardSlotGraphState) -> dict[str, Any]:
|
||||
request = state.get("normalized_request") or state["request"]
|
||||
try:
|
||||
return {"decision": self.slot_agent.decide(request)}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"model_call_traces": [
|
||||
*state.get("model_call_traces", []),
|
||||
self._build_failure_trace("langgraph_slot_decision", exc),
|
||||
],
|
||||
"fallback_reason": f"LangGraph 槽位工具节点失败,已切换规则兜底:{exc}",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _route_after_slot_tool_decision(state: StewardSlotGraphState) -> str:
|
||||
if isinstance(state.get("decision"), StewardSlotDecisionResponse):
|
||||
return "done"
|
||||
return "fallback"
|
||||
|
||||
def _slot_rule_fallback(self, state: StewardSlotGraphState) -> dict[str, StewardSlotDecisionResponse]:
|
||||
request = state.get("normalized_request") or self.slot_agent._normalize_request(state["request"])
|
||||
return {
|
||||
"decision": self.slot_agent._build_rule_fallback(
|
||||
request,
|
||||
state.get("model_call_traces", []),
|
||||
)
|
||||
}
|
||||
|
||||
def _runtime_memory_context(self, state: StewardRuntimeGraphState) -> dict[str, Any]:
|
||||
request = self.runtime_agent._normalize_request(state["request"])
|
||||
action_decision = self.runtime_agent._build_selected_flow_decision(request, [])
|
||||
update: dict[str, Any] = {"normalized_request": request}
|
||||
if action_decision is not None:
|
||||
update["action_decision"] = action_decision
|
||||
return update
|
||||
|
||||
@staticmethod
|
||||
def _route_after_runtime_memory_context(state: StewardRuntimeGraphState) -> str:
|
||||
if isinstance(state.get("action_decision"), StewardRuntimeDecisionResponse):
|
||||
return "action"
|
||||
return "tool"
|
||||
|
||||
def _runtime_action_decision(
|
||||
self,
|
||||
state: StewardRuntimeGraphState,
|
||||
) -> dict[str, StewardRuntimeDecisionResponse]:
|
||||
return {"decision": state["action_decision"]}
|
||||
|
||||
def _runtime_tool_decision(self, state: StewardRuntimeGraphState) -> dict[str, Any]:
|
||||
request = state.get("normalized_request") or state["request"]
|
||||
try:
|
||||
return {"decision": self.runtime_agent.decide(request)}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"model_call_traces": [
|
||||
*state.get("model_call_traces", []),
|
||||
self._build_failure_trace("langgraph_runtime_decision", exc),
|
||||
],
|
||||
"fallback_reason": f"LangGraph 运行时工具节点失败,已切换规则兜底:{exc}",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _route_after_runtime_tool_decision(state: StewardRuntimeGraphState) -> str:
|
||||
if isinstance(state.get("decision"), StewardRuntimeDecisionResponse):
|
||||
return "done"
|
||||
return "fallback"
|
||||
|
||||
def _runtime_rule_fallback(
|
||||
self,
|
||||
state: StewardRuntimeGraphState,
|
||||
) -> dict[str, StewardRuntimeDecisionResponse]:
|
||||
request = state.get("normalized_request") or self.runtime_agent._normalize_request(state["request"])
|
||||
decision = self.runtime_agent._build_rule_fallback(
|
||||
request,
|
||||
state.get("model_call_traces", []),
|
||||
)
|
||||
return {
|
||||
"decision": self.runtime_agent._attach_updated_steward_state(
|
||||
decision,
|
||||
request,
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_failure_trace(slot: str, exc: Exception) -> dict[str, Any]:
|
||||
return {
|
||||
"slot": slot,
|
||||
"provider": "langgraph",
|
||||
"model": "",
|
||||
"attempt": 1,
|
||||
"status": "failed",
|
||||
"error": str(exc),
|
||||
}
|
||||
Reference in New Issue
Block a user