Files
signalscout-backend/app/api/routes.py
T

121 lines
4.5 KiB
Python

from __future__ import annotations
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.agent.runner import AgentRunner
from app.agent.streaming_runner import stream_daily_agent_events
from app.core.config import Settings, get_settings
from app.db.models import AgentRun, Report, Signal, Topic
from app.db.session import get_db
from app.schemas.agent import (
DashboardRead,
ReportRead,
RunRead,
RunResponse,
SignalRead,
TopicCreate,
TopicRead,
)
router = APIRouter()
@router.get("/health")
def health(settings: Settings = Depends(get_settings)) -> dict:
# 健康检查只返回服务身份,避免暴露密钥和内部地址。
return {"status": "ok", "app": settings.app_name, "env": settings.env}
@router.post("/agent/runs/daily", response_model=RunResponse)
def run_daily_agent(
run_date: Optional[date] = None,
db: Session = Depends(get_db),
settings: Settings = Depends(get_settings),
) -> RunResponse:
# 手动触发和定时任务共用同一个Runner,保证行为一致。
return AgentRunner(db=db, settings=settings).run_daily(run_date=run_date)
@router.post("/agent/runs/daily/stream")
def stream_daily_agent(
run_date: Optional[date] = None,
settings: Settings = Depends(get_settings),
) -> StreamingResponse:
# 流式接口立即启动Agent,并把最终结构化情报逐条推送给前端。
return StreamingResponse(
stream_daily_agent_events(settings=settings, run_date=run_date),
media_type="text/event-stream",
)
@router.get("/agent/runs", response_model=list[RunRead])
def list_runs(db: Session = Depends(get_db)) -> list[AgentRun]:
# 运行列表保持紧凑,方便仪表盘和命令行查看最近状态。
return list(db.scalars(select(AgentRun).order_by(AgentRun.id.desc()).limit(20)).all())
@router.get("/signals", response_model=list[SignalRead])
def list_signals(limit: int = 50, db: Session = Depends(get_db)) -> list[Signal]:
# 情报列表按采集时间倒序返回,是仪表盘的主要阅读入口。
safe_limit = max(1, min(limit, 200))
return list(
db.scalars(_visible_signals_query().order_by(Signal.id.desc()).limit(safe_limit)).all()
)
@router.get("/reports/latest", response_model=Optional[ReportRead])
def latest_report(db: Session = Depends(get_db)) -> Optional[Report]:
# 最新日报提供一份可直接阅读的每日简报。
return db.scalar(select(Report).order_by(Report.id.desc()).limit(1))
@router.get("/dashboard", response_model=DashboardRead)
def dashboard(db: Session = Depends(get_db)) -> DashboardRead:
# 单个仪表盘接口返回运行、信号、日报和主题,前端不需要理解内部调度细节。
runs = list(db.scalars(select(AgentRun).order_by(AgentRun.id.desc()).limit(10)).all())
signals = list(db.scalars(_visible_signals_query().order_by(Signal.id.desc()).limit(80)).all())
report = db.scalar(select(Report).order_by(Report.id.desc()).limit(1))
topics = list(db.scalars(select(Topic).order_by(Topic.id.asc())).all())
return DashboardRead(
runs=runs,
signals=signals,
latest_report=report,
topics=topics,
)
@router.get("/logs/recent", response_model=list[str])
def recent_logs(settings: Settings = Depends(get_settings)) -> list[str]:
# 最近日志用于定位Agent和模型供应商调用问题。
if not settings.log_file.exists():
return []
lines = settings.log_file.read_text(encoding="utf-8", errors="replace").splitlines()
return lines[-settings.log_tail_lines :]
@router.post("/topics", response_model=TopicRead)
def create_topic(payload: TopicCreate, db: Session = Depends(get_db)) -> Topic:
# 主题让Agent可以在不改代码的情况下扩展关注范围。
topic = Topic(name=payload.name, description=payload.description, enabled=payload.enabled)
db.add(topic)
db.commit()
db.refresh(topic)
return topic
@router.get("/topics", response_model=list[TopicRead])
def list_topics(db: Session = Depends(get_db)) -> list[Topic]:
# 主题列表让操作者确认当前情报目标。
return list(db.scalars(select(Topic).order_by(Topic.id.asc())).all())
def _visible_signals_query():
# 运行中的信号和完成后的信号走同一张表,查询函数保留统一排序入口。
return select(Signal).join(AgentRun, Signal.run_id == AgentRun.id)