初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,247 @@
|
||||
"""Run 与 SSE API。"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Header, Query, Request
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from app.application.agent.confirmation import ConfirmationRegistry
|
||||
from app.application.agent.run_registry import AgentRunRegistry
|
||||
from app.core.config import settings
|
||||
from app.core.errors import AppError
|
||||
from app.core.time import to_iso, utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.repositories import runtime as runtime_repo
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.runtime import (
|
||||
AgentControlRequest,
|
||||
AgentControlResponse,
|
||||
ConfirmationResolutionResponse,
|
||||
EventResponse,
|
||||
RunResponse,
|
||||
)
|
||||
from app.services.event_bus import bus
|
||||
from app.services.runtime import (
|
||||
cancel_run,
|
||||
confirmation_exists_in_run,
|
||||
get_run_response,
|
||||
list_run_event_responses,
|
||||
list_runs_response,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/runs", tags=["runs"])
|
||||
|
||||
|
||||
@router.get("", response_model=Page[RunResponse])
|
||||
async def list_runs(
|
||||
character_id: str | None = Query(default=None, description="按人物过滤。"),
|
||||
session_id: str | None = Query(default=None, description="按会话过滤。"),
|
||||
status: str | None = Query(default=None, description="按状态过滤。"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
) -> Page[RunResponse]:
|
||||
"""Run 列表(按创建时间倒序);归档页运行时间河数据源。"""
|
||||
return await list_runs_response(
|
||||
character_id=character_id,
|
||||
session_id=session_id,
|
||||
status=status,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{run_id}", response_model=RunResponse)
|
||||
async def get_run(run_id: str) -> RunResponse:
|
||||
return await get_run_response(run_id)
|
||||
|
||||
|
||||
@router.get("/{run_id}/events", response_model=list[EventResponse])
|
||||
async def list_run_events(
|
||||
run_id: str,
|
||||
limit: int = Query(default=200, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> list[EventResponse]:
|
||||
"""Run 的事件时间线(按 sequence 升序)。"""
|
||||
return await list_run_event_responses(run_id, limit=limit, offset=offset)
|
||||
|
||||
|
||||
def _get_agent_run_registry(request: Request) -> AgentRunRegistry:
|
||||
registry: AgentRunRegistry | None = getattr(request.app.state, "agent_run_registry", None)
|
||||
if registry is None:
|
||||
raise AppError("service_unavailable", "Agent 运行控制未启用。", status_code=503)
|
||||
return registry
|
||||
|
||||
|
||||
@router.post("/{run_id}/cancel", response_model=RunResponse)
|
||||
async def cancel_run_api(run_id: str, request: Request) -> RunResponse:
|
||||
registry = getattr(request.app.state, "agent_run_registry", None)
|
||||
if registry is not None:
|
||||
await registry.abort(run_id)
|
||||
return await cancel_run(run_id)
|
||||
|
||||
|
||||
@router.post("/{run_id}/steer", response_model=AgentControlResponse)
|
||||
async def steer_run(
|
||||
run_id: str,
|
||||
payload: AgentControlRequest,
|
||||
request: Request,
|
||||
) -> AgentControlResponse:
|
||||
if not _get_agent_run_registry(request).steer(run_id, payload.content):
|
||||
raise AppError("conflict", "Run 当前不在本进程执行,无法 steer。", status_code=409)
|
||||
return AgentControlResponse()
|
||||
|
||||
|
||||
@router.post("/{run_id}/follow-up", response_model=AgentControlResponse)
|
||||
async def follow_up_run(
|
||||
run_id: str,
|
||||
payload: AgentControlRequest,
|
||||
request: Request,
|
||||
) -> AgentControlResponse:
|
||||
if not _get_agent_run_registry(request).follow_up(run_id, payload.content):
|
||||
raise AppError("conflict", "Run 当前不在本进程执行,无法追加 follow-up。", status_code=409)
|
||||
return AgentControlResponse()
|
||||
|
||||
|
||||
def _get_confirmation_registry(request: Request) -> ConfirmationRegistry:
|
||||
"""从 app.state 取 ConfirmationRegistry;未挂载时 503。"""
|
||||
registry: ConfirmationRegistry | None = getattr(
|
||||
request.app.state, "confirmation_registry", None
|
||||
)
|
||||
if registry is None:
|
||||
raise AppError(
|
||||
"service_unavailable",
|
||||
"确认机制未启用(进程内 Future 注册表未挂载)",
|
||||
status_code=503,
|
||||
)
|
||||
return registry
|
||||
|
||||
|
||||
async def _resolve_confirmation(
|
||||
request: Request,
|
||||
run_id: str,
|
||||
confirmation_id: str,
|
||||
approved: bool,
|
||||
) -> ConfirmationResolutionResponse:
|
||||
"""approve/deny 共用逻辑:通过 ConfirmationRegistry 更新确认状态。"""
|
||||
registry = _get_confirmation_registry(request)
|
||||
if await registry.resolve(run_id, approved, confirmation_id=confirmation_id):
|
||||
return ConfirmationResolutionResponse(approved=approved)
|
||||
|
||||
# 记录已处理/不存在——查 events 表区分 410 与 404
|
||||
event_store = getattr(request.app.state, "event_store", None)
|
||||
if event_store is not None and await confirmation_exists_in_run(
|
||||
event_store, run_id, confirmation_id
|
||||
):
|
||||
raise AppError(
|
||||
"confirmation_expired",
|
||||
"确认已失效(服务可能已重启或已处理),请重新发起对话",
|
||||
status_code=410,
|
||||
)
|
||||
raise AppError("confirmation_not_found", "确认项不存在", status_code=404)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{run_id}/confirmations/{confirmation_id}/approve",
|
||||
response_model=ConfirmationResolutionResponse,
|
||||
)
|
||||
async def approve_confirmation(
|
||||
run_id: str,
|
||||
confirmation_id: str,
|
||||
request: Request,
|
||||
) -> ConfirmationResolutionResponse:
|
||||
"""批准高风险工具执行:触发 Agent 循环恢复(confirmed=True 重试)。"""
|
||||
return await _resolve_confirmation(request, run_id, confirmation_id, approved=True)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{run_id}/confirmations/{confirmation_id}/deny",
|
||||
response_model=ConfirmationResolutionResponse,
|
||||
)
|
||||
async def deny_confirmation(
|
||||
run_id: str,
|
||||
confirmation_id: str,
|
||||
request: Request,
|
||||
) -> ConfirmationResolutionResponse:
|
||||
"""拒绝高风险工具执行:Agent 循环回填拒绝 ToolMessage。"""
|
||||
return await _resolve_confirmation(request, run_id, confirmation_id, approved=False)
|
||||
|
||||
|
||||
@router.get("/{run_id}/stream")
|
||||
async def stream_run(
|
||||
run_id: str,
|
||||
last_event_id: str | None = Header(default=None, alias="Last-Event-ID"),
|
||||
) -> EventSourceResponse:
|
||||
"""Run 事件流(SSE):按 run_id 实时订阅事件增量。
|
||||
|
||||
事件通过进程内 ``bus`` 实时推送(``append_event`` 自动 publish),
|
||||
DB 持久化事件供断线重连(``Last-Event-ID`` 回放)。
|
||||
"""
|
||||
if not settings.enable_sse:
|
||||
raise AppError("invalid_request", "SSE 当前未启用。")
|
||||
async with AsyncSessionLocal() as session:
|
||||
run = await runtime_repo.get_run(session, run_id)
|
||||
if run is None:
|
||||
raise AppError("not_found", "Run 不存在。")
|
||||
return EventSourceResponse(_event_generator(run_id, last_event_id))
|
||||
|
||||
|
||||
async def _event_generator(
|
||||
run_id: str, last_event_id: str | None
|
||||
) -> AsyncGenerator[dict[str, Any], None]:
|
||||
# 断线重连:解析 Last-Event-ID 对应的 sequence 作为回放起点
|
||||
after_sequence = 0
|
||||
if last_event_id:
|
||||
async with AsyncSessionLocal() as session:
|
||||
previous_event = await runtime_repo.get_event(session, last_event_id)
|
||||
if previous_event and previous_event.run_id == run_id:
|
||||
after_sequence = previous_event.sequence
|
||||
|
||||
last_sequence = after_sequence
|
||||
# 先订阅再回放,避免回放与订阅之间的事件落入间隙
|
||||
queue = bus.subscribe(run_id)
|
||||
try:
|
||||
for event in bus.list_after(run_id, after_sequence=last_sequence):
|
||||
last_sequence = event.sequence
|
||||
yield {
|
||||
"id": event.event_id,
|
||||
"event": event.type,
|
||||
"data": event.model_dump_json(),
|
||||
}
|
||||
if bus.is_terminal(event):
|
||||
return
|
||||
|
||||
while True:
|
||||
try:
|
||||
event = await asyncio.wait_for(queue.get(), timeout=settings.sse_heartbeat_seconds)
|
||||
except TimeoutError:
|
||||
yield {
|
||||
"event": "heartbeat",
|
||||
"data": json.dumps(
|
||||
{
|
||||
"event_id": "heartbeat",
|
||||
"run_id": run_id,
|
||||
"type": "heartbeat",
|
||||
"sequence": last_sequence,
|
||||
"visibility": "public",
|
||||
"payload": {},
|
||||
"created_at": to_iso(utc_now()),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
continue
|
||||
if event.sequence <= last_sequence:
|
||||
continue # 回放阶段已转发,去重
|
||||
last_sequence = event.sequence
|
||||
yield {
|
||||
"id": event.event_id,
|
||||
"event": event.type,
|
||||
"data": event.model_dump_json(),
|
||||
}
|
||||
if bus.is_terminal(event):
|
||||
return
|
||||
finally:
|
||||
bus.unsubscribe(run_id, queue)
|
||||
Reference in new issue
Block a user