582ada7e60
Add /dashboard with daily PnL overview and loss alerts. Extend AI coach chat with history sidebar, delete/switch sessions, message copy, and trading vs general bot modes. Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
4.8 KiB
Python
151 lines
4.8 KiB
Python
"""中控 AI FastAPI 路由。"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable
|
|
|
|
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
|
from pydantic import BaseModel, Field
|
|
|
|
from hub_ai.chat import (
|
|
get_chat_state,
|
|
remove_chat_session,
|
|
send_chat_message,
|
|
start_new_chat,
|
|
switch_chat_session,
|
|
)
|
|
from hub_ai.client import model_label
|
|
from hub_ai.config import trading_day_reset_hour
|
|
from hub_ai.context import build_daily_context
|
|
from hub_ai.store import get_latest_summary, list_summaries
|
|
from hub_ai.summary import generate_daily_summary
|
|
from hub_trades_lib import current_trading_day
|
|
|
|
|
|
class ChatSendBody(BaseModel):
|
|
message: str = ""
|
|
trading_day: str = ""
|
|
|
|
|
|
class SummaryGenerateBody(BaseModel):
|
|
trading_day: str = ""
|
|
force: bool = False
|
|
|
|
|
|
class ChatNewBody(BaseModel):
|
|
trading_day: str = ""
|
|
bot_mode: str = "trading"
|
|
|
|
|
|
class ChatSwitchBody(BaseModel):
|
|
session_id: str = Field(..., min_length=1)
|
|
|
|
|
|
def create_hub_ai_router(*, load_all_exchanges: Callable[[], list]) -> APIRouter:
|
|
router = APIRouter(prefix="/api/ai", tags=["hub-ai"])
|
|
|
|
def _day(raw: str = "") -> str:
|
|
d = (raw or "").strip()[:10]
|
|
return d or current_trading_day(reset_hour=trading_day_reset_hour())
|
|
|
|
@router.get("/meta")
|
|
def api_ai_meta():
|
|
return {
|
|
"ok": True,
|
|
"model": model_label(),
|
|
"trading_day_reset_hour": trading_day_reset_hour(),
|
|
"trading_day": current_trading_day(reset_hour=trading_day_reset_hour()),
|
|
"storage": {
|
|
"summaries": "hub_ai_summaries.json",
|
|
"chat": "hub_ai_chat.json",
|
|
},
|
|
}
|
|
|
|
@router.get("/context")
|
|
def api_ai_context(trading_day: str = ""):
|
|
exchanges = load_all_exchanges()
|
|
ctx = build_daily_context(exchanges, trading_day=_day(trading_day))
|
|
return {"ok": True, **ctx}
|
|
|
|
@router.get("/summary")
|
|
def api_ai_summary_list(trading_day: str = ""):
|
|
day = _day(trading_day) if trading_day.strip() else ""
|
|
items = list_summaries(trading_day=day or None, limit=20)
|
|
latest = get_latest_summary(_day(trading_day)) if trading_day.strip() else (
|
|
items[0] if items else None
|
|
)
|
|
return {
|
|
"ok": True,
|
|
"trading_day": _day(trading_day) if trading_day.strip() else None,
|
|
"summaries": items,
|
|
"latest": latest,
|
|
"model": model_label(),
|
|
}
|
|
|
|
@router.post("/summary/generate")
|
|
def api_ai_summary_generate(body: SummaryGenerateBody = SummaryGenerateBody()):
|
|
exchanges = load_all_exchanges()
|
|
result = generate_daily_summary(
|
|
exchanges,
|
|
trading_day=_day(body.trading_day) if body.trading_day.strip() else None,
|
|
force=bool(body.force),
|
|
)
|
|
if not result.get("ok"):
|
|
raise HTTPException(status_code=502, detail=result.get("msg") or "生成失败")
|
|
result.pop("context", None)
|
|
return result
|
|
|
|
@router.get("/chat/session")
|
|
def api_ai_chat_session():
|
|
state = get_chat_state()
|
|
return {"ok": True, **state, "model": model_label()}
|
|
|
|
@router.post("/chat/new")
|
|
def api_ai_chat_new(body: ChatNewBody = ChatNewBody()):
|
|
day = _day(body.trading_day)
|
|
return start_new_chat(trading_day=day, bot_mode=body.bot_mode or "trading")
|
|
|
|
@router.post("/chat/switch")
|
|
def api_ai_chat_switch(body: ChatSwitchBody):
|
|
try:
|
|
return switch_chat_session(body.session_id.strip())
|
|
except KeyError:
|
|
raise HTTPException(status_code=404, detail="会话不存在")
|
|
|
|
@router.delete("/chat/session/{session_id}")
|
|
def api_ai_chat_delete(session_id: str):
|
|
result = remove_chat_session(session_id.strip())
|
|
if not result.get("ok"):
|
|
raise HTTPException(status_code=404, detail="会话不存在")
|
|
return result
|
|
|
|
@router.post("/chat/send")
|
|
async def api_ai_chat_send(
|
|
message: str = Form(""),
|
|
trading_day: str = Form(""),
|
|
files: list[UploadFile] = File(default=[]),
|
|
):
|
|
exchanges = load_all_exchanges()
|
|
raw_attachments = []
|
|
for f in files or []:
|
|
if not f or not f.filename:
|
|
continue
|
|
data = await f.read()
|
|
raw_attachments.append(
|
|
{
|
|
"filename": f.filename,
|
|
"content_type": f.content_type or "",
|
|
"data": data,
|
|
}
|
|
)
|
|
result = send_chat_message(
|
|
exchanges,
|
|
message,
|
|
trading_day=_day(trading_day) if trading_day.strip() else None,
|
|
raw_attachments=raw_attachments,
|
|
)
|
|
if not result.get("ok"):
|
|
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
|
return result
|
|
|
|
return router
|