Files
X-Financial/server/src/app/api/v1/endpoints/steward.py

127 lines
4.5 KiB
Python
Raw Normal View History

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
from app.schemas.steward import (
StewardPlanRequest,
StewardPlanResponse,
StewardRuntimeDecisionRequest,
StewardRuntimeDecisionResponse,
StewardSlotDecisionRequest,
StewardSlotDecisionResponse,
StewardThinkingEvent,
)
from app.services.runtime_chat import RuntimeChatService
from app.services.steward_intent_agent import StewardIntentAgent
from app.services.steward_planner import StewardPlannerService
from app.services.steward_runtime_decision_agent import StewardRuntimeDecisionAgent
from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent
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
@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)
@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]:
yield _encode_stream_event(
"thinking",
StewardThinkingEvent(
event_id="intent_agent_stream_start",
stage="stream_start",
title="读取用户输入",
content="我先判断这句话里是否同时包含申请、报销或附件归集事项,再决定处理顺序。",
status="running",
).model_dump(mode="json"),
)
await asyncio.sleep(0)
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"))
await asyncio.sleep(0.6)
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)),
)