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:
@@ -10,10 +10,13 @@ from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.api.deps import CurrentUserContext, get_current_user, get_db
|
||||
from app.core.config import get_settings
|
||||
from app.models.financial_record import ExpenseClaim
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.schemas.steward import (
|
||||
StewardActionExecuteRequest,
|
||||
StewardActionExecuteResponse,
|
||||
StewardPlanRequest,
|
||||
StewardPlanResponse,
|
||||
StewardRuntimeDecisionRequest,
|
||||
@@ -27,6 +30,9 @@ from app.services.expense_claim_draft_flow import APPROVED_APPLICATION_LINK_STAT
|
||||
from app.services.expense_claims import ExpenseClaimService
|
||||
from app.services.runtime_chat import RuntimeChatService
|
||||
from app.services.steward_flow_state import StewardFlowStateService
|
||||
from app.services.steward_graph_action_runtime import StewardGraphActionRuntime
|
||||
from app.services.steward_graph_planner import StewardGraphPlannerService
|
||||
from app.services.steward_graph_runtime import StewardGraphRuntime
|
||||
from app.services.steward_intent_agent import StewardIntentAgent
|
||||
from app.services.steward_off_topic_agent import StewardOffTopicAgent
|
||||
from app.services.steward_planner import StewardPlannerService
|
||||
@@ -35,6 +41,8 @@ from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent
|
||||
|
||||
router = APIRouter(prefix="/steward")
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)]
|
||||
StewardPlannerLike = StewardPlannerService | StewardGraphPlannerService
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -69,7 +77,7 @@ def create_steward_slot_decision(
|
||||
payload: StewardSlotDecisionRequest,
|
||||
db: DbSession,
|
||||
) -> StewardSlotDecisionResponse:
|
||||
return StewardSlotDecisionAgent(RuntimeChatService(db)).decide(payload)
|
||||
return _decide_steward_slot(payload, RuntimeChatService(db))
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -83,10 +91,27 @@ def create_steward_runtime_decision(
|
||||
db: DbSession,
|
||||
) -> StewardRuntimeDecisionResponse:
|
||||
hydrated_payload = _hydrate_runtime_decision_payload(db, payload)
|
||||
decision = StewardRuntimeDecisionAgent(RuntimeChatService(db)).decide(hydrated_payload)
|
||||
decision = _decide_steward_runtime(hydrated_payload, RuntimeChatService(db))
|
||||
return _attach_runtime_conversation_state(db, hydrated_payload, decision)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/actions/execute",
|
||||
response_model=StewardActionExecuteResponse,
|
||||
summary="执行小财管家白名单动作",
|
||||
description=(
|
||||
"按 LangGraph 规划出的 action step 执行确定性业务动作;"
|
||||
"当 action 未知、缺字段、缺确认或预检查未通过时直接阻断并返回结构化兜底结果。"
|
||||
),
|
||||
)
|
||||
def execute_steward_action(
|
||||
payload: StewardActionExecuteRequest,
|
||||
db: DbSession,
|
||||
current_user: CurrentUser,
|
||||
) -> StewardActionExecuteResponse:
|
||||
return StewardGraphActionRuntime(db).execute(payload, current_user)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/plans/stream",
|
||||
summary="流式生成小财管家任务计划",
|
||||
@@ -101,7 +126,7 @@ async def stream_steward_plan(payload: StewardPlanRequest, db: DbSession) -> Str
|
||||
|
||||
async def _iter_steward_plan_events(
|
||||
payload: StewardPlanRequest,
|
||||
planner: StewardPlannerService,
|
||||
planner: StewardPlannerLike,
|
||||
db: Session,
|
||||
) -> AsyncIterator[str]:
|
||||
yield _encode_stream_event(
|
||||
@@ -135,18 +160,53 @@ def _encode_stream_event(event: str, data: dict[str, Any]) -> str:
|
||||
return json.dumps({"event": event, "data": data}, ensure_ascii=False) + "\n"
|
||||
|
||||
|
||||
def _build_steward_planner(db: Session) -> StewardPlannerService:
|
||||
def _build_steward_planner(db: Session) -> StewardPlannerLike:
|
||||
runtime_chat = RuntimeChatService(db)
|
||||
if get_settings().steward_agent_runtime.strip().lower() == "langgraph":
|
||||
return StewardGraphPlannerService(
|
||||
intent_agent=StewardIntentAgent(runtime_chat),
|
||||
off_topic_agent=StewardOffTopicAgent(runtime_chat),
|
||||
)
|
||||
return StewardPlannerService(
|
||||
intent_agent=StewardIntentAgent(runtime_chat),
|
||||
off_topic_agent=StewardOffTopicAgent(runtime_chat),
|
||||
)
|
||||
|
||||
|
||||
def _decide_steward_slot(
|
||||
payload: StewardSlotDecisionRequest,
|
||||
runtime_chat: Any,
|
||||
) -> StewardSlotDecisionResponse:
|
||||
legacy_agent = StewardSlotDecisionAgent(runtime_chat)
|
||||
if not _should_use_langgraph_runtime():
|
||||
return legacy_agent.decide(payload)
|
||||
try:
|
||||
return StewardGraphRuntime(runtime_chat).decide_slot(payload)
|
||||
except Exception:
|
||||
return legacy_agent.decide(payload)
|
||||
|
||||
|
||||
def _decide_steward_runtime(
|
||||
payload: StewardRuntimeDecisionRequest,
|
||||
runtime_chat: Any,
|
||||
) -> StewardRuntimeDecisionResponse:
|
||||
legacy_agent = StewardRuntimeDecisionAgent(runtime_chat)
|
||||
if not _should_use_langgraph_runtime():
|
||||
return legacy_agent.decide(payload)
|
||||
try:
|
||||
return StewardGraphRuntime(runtime_chat).decide_runtime(payload)
|
||||
except Exception:
|
||||
return legacy_agent.decide(payload)
|
||||
|
||||
|
||||
def _should_use_langgraph_runtime() -> bool:
|
||||
return get_settings().steward_agent_runtime.strip().lower() == "langgraph"
|
||||
|
||||
|
||||
def _hydrate_required_application_gate(
|
||||
db: Session,
|
||||
payload: StewardPlanRequest,
|
||||
planner: StewardPlannerService,
|
||||
planner: StewardPlannerLike,
|
||||
) -> StewardPlanRequest:
|
||||
context_json = dict(payload.context_json or {})
|
||||
required_gate = context_json.get("required_application_gate")
|
||||
|
||||
Reference in New Issue
Block a user