|
|
|
|
@@ -1,14 +1,20 @@
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
from datetime import date
|
|
|
|
|
from typing import Any, TypedDict
|
|
|
|
|
|
|
|
|
|
from langgraph.graph import END, START, StateGraph
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
from app.schemas.steward import StewardPlanRequest, StewardPlanResponse
|
|
|
|
|
from app.services import steward_intent_bootstrap # noqa: F401 导入即注册全部业务意图
|
|
|
|
|
from app.services.agent_conversations import AgentConversationService
|
|
|
|
|
from app.services.scenes import REGISTRY as SCENE_REGISTRY
|
|
|
|
|
from app.services.scenes.gate_rules import GateRule, SceneRoute
|
|
|
|
|
from app.services.steward_action_contracts import StewardActionPlanBuilder
|
|
|
|
|
from app.services.steward_constants import BUSINESS_CANONICAL_FIELD_ORDER
|
|
|
|
|
from app.services.steward_context_resume import attach_resumed_task, should_resume_recent_task
|
|
|
|
|
from app.services.steward_intent_agent import StewardIntentAgent, StewardIntentAgentResult
|
|
|
|
|
from app.services.steward_model_plan_builder import StewardModelPlanBuilder
|
|
|
|
|
from app.services.steward_off_topic_agent import StewardOffTopicAgent
|
|
|
|
|
@@ -16,8 +22,37 @@ from app.services.steward_planner_extraction import StewardPlannerExtractionMixi
|
|
|
|
|
from app.services.steward_planner_fallback import StewardPlannerFallbackMixin
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 模块级辅助函数:gate_classify 的判断逻辑 ----
|
|
|
|
|
|
|
|
|
|
def _matches_any_signal(message: str) -> bool:
|
|
|
|
|
"""聚合 scene_registry 所有 signal_keywords,判断输入是否为业务相关。"""
|
|
|
|
|
from app.services.scenes import REGISTRY
|
|
|
|
|
compact = _compact_text(message)
|
|
|
|
|
if not compact:
|
|
|
|
|
return False
|
|
|
|
|
return any(kw in compact for kw in REGISTRY.all_signal_keywords())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _compact_text(text: str) -> str:
|
|
|
|
|
return re.sub(r"\s+", "", str(text or ""))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _scene_route_to_gate_decision(route: SceneRoute) -> str:
|
|
|
|
|
"""SceneRoute 映射到 gate_decision 字符串。"""
|
|
|
|
|
if route == SceneRoute.HANDLER_ONLY:
|
|
|
|
|
return "handler_only"
|
|
|
|
|
if route == SceneRoute.OFF_TOPIC:
|
|
|
|
|
return "off_topic"
|
|
|
|
|
if route == SceneRoute.RESUME:
|
|
|
|
|
return "resume"
|
|
|
|
|
if route == SceneRoute.AMBIGUOUS:
|
|
|
|
|
return "ambiguous"
|
|
|
|
|
return "model_intent"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StewardGraphState(TypedDict, total=False):
|
|
|
|
|
request: StewardPlanRequest
|
|
|
|
|
db: Session
|
|
|
|
|
message: str
|
|
|
|
|
base_date: date
|
|
|
|
|
scenario: str | None
|
|
|
|
|
@@ -26,10 +61,25 @@ class StewardGraphState(TypedDict, total=False):
|
|
|
|
|
plan: StewardPlanResponse
|
|
|
|
|
model_call_traces: list[dict[str, Any]]
|
|
|
|
|
fallback_reason: str
|
|
|
|
|
# 新增:上下文状态
|
|
|
|
|
recent_history: list[dict[str, Any]]
|
|
|
|
|
steward_state: dict[str, Any]
|
|
|
|
|
# 新增:门控裁决
|
|
|
|
|
gate_decision: str # "off_topic" | "handler_only" | "resume" | "ambiguous" | "model_intent" | "fallback"
|
|
|
|
|
gate_scene_id: str | None
|
|
|
|
|
# 新增:回填的 conversation_id
|
|
|
|
|
conversation_id: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtractionMixin):
|
|
|
|
|
"""用 LangGraph 编排小财管家的意图识别、流程判断和兜底路径。"""
|
|
|
|
|
"""用 LangGraph 编排小财管家的意图识别、流程判断和兜底路径。
|
|
|
|
|
|
|
|
|
|
Phase 1 P1.3-P1.7:LangGraph 是唯一编排者,endpoint 退化为 3 行。
|
|
|
|
|
图拓扑:
|
|
|
|
|
load_context → gate_classify → {off_topic/handler_only/resume/ambiguous/model_intent}
|
|
|
|
|
→ {build_off_topic_plan / execute_scene_handler / resume_recent_task / detect_model_intent}
|
|
|
|
|
→ attach_action_steps → persist_state
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
@@ -40,10 +90,358 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|
|
|
|
self.off_topic_agent = off_topic_agent
|
|
|
|
|
self._graph = self._build_graph()
|
|
|
|
|
|
|
|
|
|
def build_plan(self, request: StewardPlanRequest) -> StewardPlanResponse:
|
|
|
|
|
# ---- 上下文加载 + 门控裁决(P1.3-P1.4 新增) ----
|
|
|
|
|
|
|
|
|
|
def _load_context(self, state: StewardGraphState) -> dict[str, Any]:
|
|
|
|
|
"""读最近 10 条对话历史 + steward_state,注入 state。
|
|
|
|
|
|
|
|
|
|
替代 endpoint 的 _hydrate_required_application_gate + _inject_recent_conversation_history。
|
|
|
|
|
"""
|
|
|
|
|
request = state.get("request")
|
|
|
|
|
if request is None:
|
|
|
|
|
return {}
|
|
|
|
|
db = state.get("db")
|
|
|
|
|
|
|
|
|
|
# 1. 读对话历史(只给模型用,不返回前端)
|
|
|
|
|
recent_history: list[dict[str, Any]] = []
|
|
|
|
|
context_json = dict(request.context_json or {})
|
|
|
|
|
conversation_id = self._extract_conversation_id(context_json)
|
|
|
|
|
if db is not None and conversation_id:
|
|
|
|
|
try:
|
|
|
|
|
recent_history = AgentConversationService(db).list_message_history(
|
|
|
|
|
conversation_id, limit=10
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
recent_history = []
|
|
|
|
|
|
|
|
|
|
# 2. 读 steward_state(DB 优先,前端传次之)
|
|
|
|
|
steward_state: dict[str, Any] = {}
|
|
|
|
|
if db is not None and conversation_id:
|
|
|
|
|
try:
|
|
|
|
|
conversation = AgentConversationService(db).get_conversation(conversation_id)
|
|
|
|
|
if conversation is not None and isinstance(conversation.state_json, dict):
|
|
|
|
|
stored = conversation.state_json.get("steward_state")
|
|
|
|
|
if isinstance(stored, dict):
|
|
|
|
|
steward_state = stored
|
|
|
|
|
except Exception:
|
|
|
|
|
steward_state = {}
|
|
|
|
|
if not steward_state:
|
|
|
|
|
incoming = context_json.get("steward_state") or context_json.get("stewardState")
|
|
|
|
|
if isinstance(incoming, dict):
|
|
|
|
|
steward_state = incoming
|
|
|
|
|
|
|
|
|
|
# 3. 注入 recent_history 到 context_json(供 LLM 使用)
|
|
|
|
|
if recent_history:
|
|
|
|
|
request = request.model_copy(
|
|
|
|
|
update={
|
|
|
|
|
"context_json": {
|
|
|
|
|
**context_json,
|
|
|
|
|
"recent_history": recent_history,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"request": request,
|
|
|
|
|
"recent_history": recent_history,
|
|
|
|
|
"steward_state": steward_state,
|
|
|
|
|
"message": str(request.message or "").strip(),
|
|
|
|
|
"base_date": self._resolve_base_date_from_request(request),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _gate_classify(self, state: StewardGraphState) -> dict[str, Any]:
|
|
|
|
|
"""统一门控裁决:单一决策点,按固定顺序决定场景和路由。
|
|
|
|
|
|
|
|
|
|
顺序:
|
|
|
|
|
① resume 门(用户说"再提交"+ state 有可恢复 flow → 上下文恢复)
|
|
|
|
|
② off_topic 门(聚合 scene_registry signal_keywords)
|
|
|
|
|
③ 规则匹配门(按 priority 遍历 scene_registry,命中 CHOICE 规则的)
|
|
|
|
|
④ LLM 门(规则未命中,走 model function call)
|
|
|
|
|
"""
|
|
|
|
|
from app.services.scenes import REGISTRY
|
|
|
|
|
from app.services.scenes.gate_rules import GateRule, SceneRoute
|
|
|
|
|
|
|
|
|
|
message = str(state.get("message") or "").strip()
|
|
|
|
|
steward_state = state.get("steward_state") or {}
|
|
|
|
|
|
|
|
|
|
# ① resume 门(优先:即使消息不命中业务关键词,只要是"再提交"类话术就尝试恢复)
|
|
|
|
|
resume_scene = should_resume_recent_task(message, steward_state)
|
|
|
|
|
if resume_scene:
|
|
|
|
|
return {"gate_decision": "resume", "gate_scene_id": resume_scene}
|
|
|
|
|
|
|
|
|
|
# ② off_topic 门
|
|
|
|
|
if not _matches_any_signal(message):
|
|
|
|
|
return {"gate_decision": "off_topic", "gate_scene_id": None}
|
|
|
|
|
|
|
|
|
|
# ③ 规则匹配门(按 priority 遍历,命中 CHOICE 规则的)
|
|
|
|
|
for scene in REGISTRY.scenes_sorted_by_priority():
|
|
|
|
|
if scene.gate != GateRule.CHOICE:
|
|
|
|
|
continue
|
|
|
|
|
if not scene.signal_keywords:
|
|
|
|
|
continue
|
|
|
|
|
compact = _compact_text(message)
|
|
|
|
|
if any(kw in compact for kw in scene.signal_keywords):
|
|
|
|
|
route = _scene_route_to_gate_decision(scene.route)
|
|
|
|
|
return {"gate_decision": route, "gate_scene_id": scene.scene_id}
|
|
|
|
|
|
|
|
|
|
# ④ LLM 门(规则未命中)
|
|
|
|
|
# 走 model_intent 时,如 state 已有 active_flow 且 LLM 准备 fallback,可优先做 candidate_flow
|
|
|
|
|
request = state.get("request")
|
|
|
|
|
if request is not None and self._looks_like_ambiguous_travel_flow(
|
|
|
|
|
message, self._resolve_base_date_from_request(request), request
|
|
|
|
|
):
|
|
|
|
|
return {"gate_decision": "ambiguous", "gate_scene_id": None}
|
|
|
|
|
|
|
|
|
|
return {"gate_decision": "model_intent", "gate_scene_id": None}
|
|
|
|
|
|
|
|
|
|
def _execute_scene_handler(self, state: StewardGraphState) -> dict[str, Any]:
|
|
|
|
|
"""HANDLER_ONLY 路由:不调 LLM,直接执行 scene 的 handler。
|
|
|
|
|
|
|
|
|
|
当前只有 query_travel_standard 走这条路径。handler 签名约定:
|
|
|
|
|
handler(executor_self, request, current_user, trace) -> StewardActionExecuteResponse
|
|
|
|
|
"""
|
|
|
|
|
from app.services.scenes import REGISTRY
|
|
|
|
|
from app.schemas.steward import (
|
|
|
|
|
StewardActionExecuteRequest,
|
|
|
|
|
StewardActionExecuteResponse,
|
|
|
|
|
StewardActionStep,
|
|
|
|
|
StewardPlanResponse,
|
|
|
|
|
StewardTask,
|
|
|
|
|
StewardThinkingEvent,
|
|
|
|
|
)
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
scene_id = state.get("gate_scene_id")
|
|
|
|
|
scene = REGISTRY.get(scene_id or "") if scene_id else None
|
|
|
|
|
if scene is None or scene.handler is None:
|
|
|
|
|
plan = self._build_rule_fallback_graph_plan(state)
|
|
|
|
|
return {"plan": plan}
|
|
|
|
|
|
|
|
|
|
request = state.get("request")
|
|
|
|
|
if request is None:
|
|
|
|
|
plan = self._build_rule_fallback_graph_plan(state)
|
|
|
|
|
return {"plan": plan}
|
|
|
|
|
|
|
|
|
|
# 构造 handler 期望的 StewardActionExecuteRequest
|
|
|
|
|
from app.services.steward_action_contracts import StewardActionPlanBuilder
|
|
|
|
|
builder = StewardActionPlanBuilder()
|
|
|
|
|
if scene.action_steps_builder is not None:
|
|
|
|
|
task = StewardTask(
|
|
|
|
|
task_id=f"task_handler_{int(time.time() * 1000)}",
|
|
|
|
|
task_type=scene.scene_id,
|
|
|
|
|
assigned_agent=scene.assigned_agent or "policy_query_assistant",
|
|
|
|
|
title=scene.label,
|
|
|
|
|
summary=str(request.message or "").strip(),
|
|
|
|
|
status="delegated",
|
|
|
|
|
requested_action="preview",
|
|
|
|
|
ontology_fields={},
|
|
|
|
|
missing_fields=[],
|
|
|
|
|
confirmation_required=False,
|
|
|
|
|
)
|
|
|
|
|
action_steps = scene.action_steps_builder(task)
|
|
|
|
|
else:
|
|
|
|
|
action_steps = []
|
|
|
|
|
|
|
|
|
|
# 构造一个最小 action step 用于构造 handler request
|
|
|
|
|
if not action_steps:
|
|
|
|
|
action_steps = [StewardActionStep(
|
|
|
|
|
step_id=f"handler_{int(time.time() * 1000)}",
|
|
|
|
|
action_type=scene.side_effect_actions[0] if scene.side_effect_actions else scene.scene_id,
|
|
|
|
|
label=scene.label,
|
|
|
|
|
target_task_id="",
|
|
|
|
|
status="planned",
|
|
|
|
|
requires_confirmation=False,
|
|
|
|
|
payload={},
|
|
|
|
|
)]
|
|
|
|
|
step = action_steps[0]
|
|
|
|
|
|
|
|
|
|
action_request = StewardActionExecuteRequest(
|
|
|
|
|
action_type=step.action_type,
|
|
|
|
|
message=str(request.message or "").strip(),
|
|
|
|
|
task=task if scene.action_steps_builder else None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
response = scene.handler(self, action_request, current_user=None, trace=[])
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
plan = self._build_rule_fallback_graph_plan(state)
|
|
|
|
|
plan.thinking_events = list(plan.thinking_events) + [
|
|
|
|
|
StewardThinkingEvent(
|
|
|
|
|
event_id=f"handler_error_{scene.scene_id}",
|
|
|
|
|
stage="llm_function_call",
|
|
|
|
|
title=f"{scene.label}执行失败",
|
|
|
|
|
content=f"handler 抛错: {type(exc).__name__}: {str(exc)[:80]}",
|
|
|
|
|
status="completed",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
return {"plan": plan}
|
|
|
|
|
|
|
|
|
|
# handler 返回 StewardActionExecuteResponse,转换为 StewardPlanResponse
|
|
|
|
|
answer = ""
|
|
|
|
|
result_payload: dict[str, Any] = {}
|
|
|
|
|
if isinstance(response, StewardActionExecuteResponse):
|
|
|
|
|
answer = response.message or response.result_payload.get("answer_markdown", "")
|
|
|
|
|
result_payload = dict(response.result_payload or {})
|
|
|
|
|
|
|
|
|
|
# 把查询结果放进 summary(给前端展示)和 thinking_event(过程摘要)
|
|
|
|
|
plan = StewardPlanResponse(
|
|
|
|
|
plan_id=f"steward_handler_{int(time.time() * 1000)}",
|
|
|
|
|
planning_source=f"scene_handler:{scene.scene_id}",
|
|
|
|
|
summary=answer or scene.label,
|
|
|
|
|
next_action="answer_only",
|
|
|
|
|
tasks=[task] if scene.action_steps_builder else [],
|
|
|
|
|
thinking_events=[
|
|
|
|
|
StewardThinkingEvent(
|
|
|
|
|
event_id=f"handler_{scene.scene_id}_done",
|
|
|
|
|
stage="llm_function_call",
|
|
|
|
|
title=f"已执行{scene.label}",
|
|
|
|
|
content=answer or f"场景 {scene.scene_id} 已执行",
|
|
|
|
|
status="completed",
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
pending_flow_confirmation={"status": "none"},
|
|
|
|
|
conversation_id=state.get("conversation_id") or "",
|
|
|
|
|
steward_state=state.get("steward_state") or {},
|
|
|
|
|
action_steps=[],
|
|
|
|
|
)
|
|
|
|
|
return {"plan": plan}
|
|
|
|
|
|
|
|
|
|
def _resume_recent_task(self, state: StewardGraphState) -> dict[str, Any]:
|
|
|
|
|
"""RESUME 路由:从 steward_state 恢复之前被拦/中断的 task。
|
|
|
|
|
|
|
|
|
|
保险①:100% 可靠,覆盖"再提交""继续提交"等确认类话术。
|
|
|
|
|
"""
|
|
|
|
|
steward_state = state.get("steward_state") or {}
|
|
|
|
|
scene_id = state.get("gate_scene_id")
|
|
|
|
|
# 先建一个空 plan(无 task),让 attach_resumed_task 把恢复的 task 挂上
|
|
|
|
|
from app.schemas.steward import StewardPlanResponse
|
|
|
|
|
empty_plan = StewardPlanResponse(
|
|
|
|
|
plan_id="steward_resume_pending",
|
|
|
|
|
planning_source="rule_fallback",
|
|
|
|
|
summary="恢复上下文中的待办任务。",
|
|
|
|
|
next_action="confirm_task",
|
|
|
|
|
tasks=[],
|
|
|
|
|
thinking_events=[],
|
|
|
|
|
pending_flow_confirmation={"status": "none"},
|
|
|
|
|
)
|
|
|
|
|
if not scene_id:
|
|
|
|
|
return {"plan": empty_plan}
|
|
|
|
|
resumed = attach_resumed_task(empty_plan, steward_state, scene_id)
|
|
|
|
|
return {"plan": resumed}
|
|
|
|
|
|
|
|
|
|
def _persist_state(
|
|
|
|
|
self,
|
|
|
|
|
db: Session,
|
|
|
|
|
request: StewardPlanRequest,
|
|
|
|
|
plan: StewardPlanResponse,
|
|
|
|
|
final_state: StewardGraphState,
|
|
|
|
|
) -> StewardPlanResponse:
|
|
|
|
|
"""图执行后的副作用:写 message + steward_state 到 DB。
|
|
|
|
|
|
|
|
|
|
替代 endpoint 的 _attach_conversation_state。
|
|
|
|
|
"""
|
|
|
|
|
if db is None:
|
|
|
|
|
return plan
|
|
|
|
|
try:
|
|
|
|
|
context_json = dict(request.context_json or {})
|
|
|
|
|
context_json["session_type"] = str(context_json.get("session_type") or "steward").strip() or "steward"
|
|
|
|
|
conversation_id = self._extract_conversation_id(context_json)
|
|
|
|
|
conversation_service = AgentConversationService(db)
|
|
|
|
|
conversation = conversation_service.get_or_create_conversation(
|
|
|
|
|
conversation_id=conversation_id,
|
|
|
|
|
user_id=request.user_id,
|
|
|
|
|
source="user_message",
|
|
|
|
|
context_json=context_json,
|
|
|
|
|
)
|
|
|
|
|
current_state = self._resolve_steward_state_for_persist(conversation.state_json, final_state)
|
|
|
|
|
from app.services.steward_flow_state import StewardFlowStateService
|
|
|
|
|
steward_state = StewardFlowStateService().merge_plan(current_state, plan)
|
|
|
|
|
conversation = conversation_service.update_state(
|
|
|
|
|
conversation_id=conversation.conversation_id,
|
|
|
|
|
run_id=None,
|
|
|
|
|
scenario="steward",
|
|
|
|
|
intent="plan",
|
|
|
|
|
context_json={**context_json, "steward_state": steward_state},
|
|
|
|
|
) or conversation
|
|
|
|
|
conversation_service.append_message(
|
|
|
|
|
conversation_id=conversation.conversation_id,
|
|
|
|
|
role="user",
|
|
|
|
|
content=request.message,
|
|
|
|
|
message_json={"source": "steward_plan_request"},
|
|
|
|
|
)
|
|
|
|
|
conversation_service.append_message(
|
|
|
|
|
conversation_id=conversation.conversation_id,
|
|
|
|
|
role="assistant",
|
|
|
|
|
content=plan.summary,
|
|
|
|
|
message_json={
|
|
|
|
|
"source": "steward_plan_response",
|
|
|
|
|
"plan_id": plan.plan_id,
|
|
|
|
|
"steward_state": steward_state,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return plan.model_copy(
|
|
|
|
|
update={
|
|
|
|
|
"conversation_id": conversation.conversation_id,
|
|
|
|
|
"steward_state": steward_state,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
return plan
|
|
|
|
|
|
|
|
|
|
# ---- 路由函数 ----
|
|
|
|
|
|
|
|
|
|
def _route_after_gate_classify(self, state: StewardGraphState) -> str:
|
|
|
|
|
"""gate_classify 后的路由:把 gate_decision 映射到下一个节点。"""
|
|
|
|
|
decision = str(state.get("gate_decision") or "model_intent")
|
|
|
|
|
return decision
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _extract_conversation_id(context_json: dict[str, Any]) -> str | None:
|
|
|
|
|
return str(
|
|
|
|
|
context_json.get("conversation_id")
|
|
|
|
|
or context_json.get("conversationId")
|
|
|
|
|
or ""
|
|
|
|
|
).strip() or None
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _resolve_base_date_from_request(request: StewardPlanRequest | None) -> date:
|
|
|
|
|
if request is None:
|
|
|
|
|
return date.today()
|
|
|
|
|
from app.services.steward_planner_extraction import StewardPlannerExtractionMixin
|
|
|
|
|
return StewardPlannerExtractionMixin._resolve_base_date(
|
|
|
|
|
request.client_now_iso,
|
|
|
|
|
dict(request.context_json or {}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _resolve_steward_state_for_persist(
|
|
|
|
|
conversation_state: Any,
|
|
|
|
|
final_state: StewardGraphState,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
state_json = conversation_state if isinstance(conversation_state, dict) else {}
|
|
|
|
|
stored = state_json.get("steward_state")
|
|
|
|
|
if isinstance(stored, dict) and stored:
|
|
|
|
|
return stored
|
|
|
|
|
graph_state = final_state.get("steward_state")
|
|
|
|
|
if isinstance(graph_state, dict) and graph_state:
|
|
|
|
|
return graph_state
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
def build_plan(
|
|
|
|
|
self,
|
|
|
|
|
request: StewardPlanRequest,
|
|
|
|
|
db: Session | None = None,
|
|
|
|
|
) -> StewardPlanResponse:
|
|
|
|
|
"""编排一次 steward 计划请求,内部执行:load → classify → plan。
|
|
|
|
|
|
|
|
|
|
P1 中间状态:签名保持 build_plan(request) 不变以兼容现有测试/消费者。
|
|
|
|
|
显式传 db 时,load_context 节点会读历史/state;不传时图内 IO 静默跳过。
|
|
|
|
|
持久化由 endpoint 显式调用 _attach_conversation_state 完成(P3 收敛到图内)。
|
|
|
|
|
"""
|
|
|
|
|
final_state = self._graph.invoke(
|
|
|
|
|
{
|
|
|
|
|
"request": request,
|
|
|
|
|
"db": db,
|
|
|
|
|
"model_call_traces": [],
|
|
|
|
|
"fallback_reason": "",
|
|
|
|
|
}
|
|
|
|
|
@@ -55,13 +453,34 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|
|
|
|
|
|
|
|
|
def _build_graph(self):
|
|
|
|
|
graph = StateGraph(StewardGraphState)
|
|
|
|
|
# 节点
|
|
|
|
|
graph.add_node("load_context", self._load_context)
|
|
|
|
|
graph.add_node("gate_classify", self._gate_classify)
|
|
|
|
|
graph.add_node("execute_scene_handler", self._execute_scene_handler)
|
|
|
|
|
graph.add_node("resume_recent_task", self._resume_recent_task)
|
|
|
|
|
graph.add_node("prepare_context", self._prepare_context)
|
|
|
|
|
graph.add_node("detect_model_intent", self._detect_model_intent)
|
|
|
|
|
graph.add_node("build_off_topic_plan", self._build_off_topic_graph_plan)
|
|
|
|
|
graph.add_node("build_rule_fallback_plan", self._build_rule_fallback_graph_plan)
|
|
|
|
|
graph.add_node("build_pending_flow_plan", self._build_pending_flow_fallback_graph_plan)
|
|
|
|
|
graph.add_node("attach_action_steps", self._attach_action_steps)
|
|
|
|
|
|
|
|
|
|
graph.add_edge(START, "prepare_context")
|
|
|
|
|
# 拓扑
|
|
|
|
|
graph.add_edge(START, "load_context")
|
|
|
|
|
graph.add_edge("load_context", "gate_classify")
|
|
|
|
|
graph.add_conditional_edges(
|
|
|
|
|
"gate_classify",
|
|
|
|
|
self._route_after_gate_classify,
|
|
|
|
|
{
|
|
|
|
|
"off_topic": "build_off_topic_plan",
|
|
|
|
|
"handler_only": "execute_scene_handler",
|
|
|
|
|
"resume": "resume_recent_task",
|
|
|
|
|
"ambiguous": "build_pending_flow_plan",
|
|
|
|
|
"model_intent": "prepare_context",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
graph.add_edge("execute_scene_handler", "attach_action_steps")
|
|
|
|
|
graph.add_edge("resume_recent_task", "attach_action_steps")
|
|
|
|
|
graph.add_conditional_edges(
|
|
|
|
|
"prepare_context",
|
|
|
|
|
self._route_after_prepare_context,
|
|
|
|
|
@@ -82,6 +501,7 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|
|
|
|
)
|
|
|
|
|
graph.add_edge("build_off_topic_plan", "attach_action_steps")
|
|
|
|
|
graph.add_edge("build_rule_fallback_plan", "attach_action_steps")
|
|
|
|
|
graph.add_edge("build_pending_flow_plan", "attach_action_steps")
|
|
|
|
|
graph.add_edge("attach_action_steps", END)
|
|
|
|
|
return graph.compile()
|
|
|
|
|
|
|
|
|
|
@@ -188,8 +608,22 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|
|
|
|
return {
|
|
|
|
|
"plan": self._build_off_topic_plan(
|
|
|
|
|
state["request"],
|
|
|
|
|
scenario=str(state["scenario"] or ""),
|
|
|
|
|
model_call_traces=state.get("model_call_traces"),
|
|
|
|
|
scenario=str(state.get("scenario") or ""),
|
|
|
|
|
model_call_traces=state.get("model_call_traces") or [],
|
|
|
|
|
fallback_reason=str(state.get("fallback_reason") or ""),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _build_pending_flow_fallback_graph_plan(
|
|
|
|
|
self,
|
|
|
|
|
state: StewardGraphState,
|
|
|
|
|
) -> dict[str, StewardPlanResponse]:
|
|
|
|
|
request = state["request"]
|
|
|
|
|
return {
|
|
|
|
|
"plan": self._build_pending_flow_fallback_plan(
|
|
|
|
|
request,
|
|
|
|
|
base_date=state["base_date"],
|
|
|
|
|
model_call_traces=state.get("model_call_traces") or [],
|
|
|
|
|
fallback_reason=str(state.get("fallback_reason") or ""),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|