aad34202f1
Co-Authored-By: Claude <noreply@anthropic.com>
116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
"""AI 大盘复盘 API — 流式复盘 + 报告持久化。
|
|
|
|
路由前缀: /api/market-recap
|
|
|
|
端点:
|
|
POST /analyze AI 流式大盘复盘(NDJSON)
|
|
GET /reports 历史复盘列表
|
|
POST /reports 保存一条复盘报告
|
|
DELETE /reports/{report_id} 删除一条复盘报告
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
|
|
from app.services import market_recap_reports
|
|
from app.services.market_recap import recap_market_stream
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/market-recap", tags=["market-recap"])
|
|
|
|
|
|
class AnalyzeRequest(BaseModel):
|
|
"""AI 大盘复盘请求。"""
|
|
as_of: str | None = None # 可选:复盘日期(YYYY-MM-DD),缺省取最新有数据日
|
|
focus: str = "" # 可选:用户追加的复盘关注点
|
|
|
|
|
|
@router.post("/analyze")
|
|
async def analyze_market(request: Request, req: AnalyzeRequest):
|
|
"""AI 大盘复盘 — NDJSON 流式返回。
|
|
|
|
装配市场总览(指数/涨跌/连板/封板/板块/情绪雷达)→ 复盘提示词 →
|
|
流式调用 LLM → 逐 chunk 以 NDJSON 推给前端(每行一个 JSON)。
|
|
|
|
协议:
|
|
{"type":"meta","as_of","emotion_score","emotion_label","summary"}
|
|
{"type":"delta","content":"..."}
|
|
{"type":"error","message":"..."}
|
|
{"type":"done"}
|
|
"""
|
|
from datetime import date as date_cls
|
|
|
|
repo = request.app.state.repo
|
|
quote_service = getattr(request.app.state, "quote_service", None)
|
|
depth_service = getattr(request.app.state, "depth_service", None)
|
|
|
|
as_of = None
|
|
if req.as_of:
|
|
try:
|
|
as_of = date_cls.fromisoformat(req.as_of)
|
|
except ValueError:
|
|
raise HTTPException(400, f"as_of 格式应为 YYYY-MM-DD,收到: {req.as_of}")
|
|
|
|
async def stream_gen():
|
|
async for chunk in recap_market_stream(repo, quote_service, depth_service, as_of, req.focus):
|
|
yield chunk + "\n"
|
|
|
|
return StreamingResponse(
|
|
stream_gen(),
|
|
media_type="application/x-ndjson",
|
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
|
)
|
|
|
|
|
|
# ================================================================
|
|
# 报告 CRUD(历史复盘持久化)
|
|
# ================================================================
|
|
|
|
class SaveReportRequest(BaseModel):
|
|
"""保存一条 AI 大盘复盘报告。"""
|
|
as_of: str
|
|
focus: str = ""
|
|
content: str
|
|
summary: str = ""
|
|
emotion_score: int | None = None
|
|
emotion_label: str = ""
|
|
|
|
|
|
@router.get("/reports")
|
|
def list_reports(request: Request):
|
|
"""获取全部历史复盘(按时间降序,后端已裁剪到上限)。"""
|
|
return {"reports": market_recap_reports.list_reports()}
|
|
|
|
|
|
@router.post("/reports")
|
|
def save_report(request: Request, req: SaveReportRequest):
|
|
"""保存一条复盘报告。"""
|
|
report = market_recap_reports.save_report({
|
|
"as_of": req.as_of,
|
|
"focus": req.focus,
|
|
"content": req.content,
|
|
"summary": req.summary,
|
|
"emotion_score": req.emotion_score,
|
|
"emotion_label": req.emotion_label,
|
|
})
|
|
# 推送到飞书(可选): 与定时复盘共用同一开关 review_push_enabled 与 _maybe_push_review。
|
|
# 内部 try/except 静默降级, 不影响归档返回值。
|
|
from app.jobs.daily_pipeline import _maybe_push_review
|
|
_maybe_push_review(req.content, {
|
|
"as_of": req.as_of,
|
|
"emotion_label": req.emotion_label,
|
|
})
|
|
return {"ok": True, "report": report}
|
|
|
|
|
|
@router.delete("/reports/{report_id}")
|
|
def delete_report(request: Request, report_id: str):
|
|
"""删除一条复盘报告。"""
|
|
ok = market_recap_reports.delete_report(report_id)
|
|
return {"ok": ok}
|