2026-06-04 11:03:29 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import json
|
|
|
|
|
from collections.abc import AsyncIterator
|
|
|
|
|
from typing import Annotated, Any
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
|
|
|
from fastapi.responses import StreamingResponse
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
from app.api.deps import get_db
|
|
|
|
|
from app.schemas.common import ErrorResponse
|
2026-06-06 17:19:07 +08:00
|
|
|
from app.schemas.steward import (
|
|
|
|
|
StewardPlanRequest,
|
|
|
|
|
StewardPlanResponse,
|
|
|
|
|
StewardRuntimeDecisionRequest,
|
|
|
|
|
StewardRuntimeDecisionResponse,
|
|
|
|
|
StewardSlotDecisionRequest,
|
|
|
|
|
StewardSlotDecisionResponse,
|
|
|
|
|
StewardThinkingEvent,
|
|
|
|
|
)
|
2026-06-04 11:03:29 +08:00
|
|
|
from app.services.runtime_chat import RuntimeChatService
|
|
|
|
|
from app.services.steward_intent_agent import StewardIntentAgent
|
|
|
|
|
from app.services.steward_planner import StewardPlannerService
|
2026-06-06 17:19:07 +08:00
|
|
|
from app.services.steward_runtime_decision_agent import StewardRuntimeDecisionAgent
|
|
|
|
|
from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent
|
2026-06-04 11:03:29 +08:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/steward")
|
|
|
|
|
DbSession = Annotated[Session, Depends(get_db)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/plans",
|
|
|
|
|
response_model=StewardPlanResponse,
|
|
|
|
|
summary="生成小财管家任务计划",
|
|
|
|
|
description="把首页自然语言和附件元信息拆解为可确认、可追踪、可分派的财务任务计划。",
|
|
|
|
|
responses={
|
|
|
|
|
status.HTTP_400_BAD_REQUEST: {
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
"description": "请求缺少任务描述,无法生成小财管家计划。",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
def create_steward_plan(payload: StewardPlanRequest, db: DbSession) -> StewardPlanResponse:
|
|
|
|
|
try:
|
|
|
|
|
return _build_steward_planner(db).build_plan(payload)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
@router.post(
|
|
|
|
|
"/slot-decisions",
|
|
|
|
|
response_model=StewardSlotDecisionResponse,
|
|
|
|
|
summary="判断小财管家当前任务字段缺口",
|
|
|
|
|
description="结合当前任务、本体字段和用户上下文,使用 function calling 判断下一步应先追问用户还是展示核对结果。",
|
|
|
|
|
)
|
|
|
|
|
def create_steward_slot_decision(
|
|
|
|
|
payload: StewardSlotDecisionRequest,
|
|
|
|
|
db: DbSession,
|
|
|
|
|
) -> StewardSlotDecisionResponse:
|
|
|
|
|
return StewardSlotDecisionAgent(RuntimeChatService(db)).decide(payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/runtime-decisions",
|
|
|
|
|
response_model=StewardRuntimeDecisionResponse,
|
|
|
|
|
summary="判断小财管家运行时下一步动作",
|
|
|
|
|
description="结合任务队列、当前结构化结果和用户输入,使用 function calling 判断应提交当前单据、继续下一任务、补字段或重新规划。",
|
|
|
|
|
)
|
|
|
|
|
def create_steward_runtime_decision(
|
|
|
|
|
payload: StewardRuntimeDecisionRequest,
|
|
|
|
|
db: DbSession,
|
|
|
|
|
) -> StewardRuntimeDecisionResponse:
|
|
|
|
|
return StewardRuntimeDecisionAgent(RuntimeChatService(db)).decide(payload)
|
|
|
|
|
|
|
|
|
|
|
2026-06-04 11:03:29 +08:00
|
|
|
@router.post(
|
|
|
|
|
"/plans/stream",
|
|
|
|
|
summary="流式生成小财管家任务计划",
|
|
|
|
|
description="以 NDJSON 逐条返回小财管家的过程摘要事件,最后返回完整任务计划。",
|
|
|
|
|
)
|
|
|
|
|
async def stream_steward_plan(payload: StewardPlanRequest, db: DbSession) -> StreamingResponse:
|
|
|
|
|
return StreamingResponse(
|
|
|
|
|
_iter_steward_plan_events(payload, _build_steward_planner(db)),
|
|
|
|
|
media_type="application/x-ndjson",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _iter_steward_plan_events(
|
|
|
|
|
payload: StewardPlanRequest,
|
|
|
|
|
planner: StewardPlannerService,
|
|
|
|
|
) -> AsyncIterator[str]:
|
2026-06-04 14:25:14 +08:00
|
|
|
yield _encode_stream_event(
|
|
|
|
|
"thinking",
|
|
|
|
|
StewardThinkingEvent(
|
|
|
|
|
event_id="intent_agent_stream_start",
|
|
|
|
|
stage="stream_start",
|
2026-06-06 17:19:07 +08:00
|
|
|
title="读取用户输入",
|
|
|
|
|
content="我先判断这句话里是否同时包含申请、报销或附件归集事项,再决定处理顺序。",
|
2026-06-04 14:25:14 +08:00
|
|
|
status="running",
|
|
|
|
|
).model_dump(mode="json"),
|
|
|
|
|
)
|
|
|
|
|
await asyncio.sleep(0)
|
|
|
|
|
|
2026-06-04 11:03:29 +08:00
|
|
|
try:
|
|
|
|
|
plan = planner.build_plan(payload)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
yield _encode_stream_event("error", {"message": str(exc)})
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for event in plan.thinking_events:
|
|
|
|
|
yield _encode_stream_event("thinking", event.model_dump(mode="json"))
|
2026-06-06 17:19:07 +08:00
|
|
|
await asyncio.sleep(0.6)
|
2026-06-04 11:03:29 +08:00
|
|
|
|
|
|
|
|
yield _encode_stream_event("plan", plan.model_dump(mode="json"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
return StewardPlannerService(
|
|
|
|
|
intent_agent=StewardIntentAgent(RuntimeChatService(db)),
|
|
|
|
|
)
|