""" Free-form chat with GPT-4o about the current cockpit state. The only tool the model can call (propose_trade) just writes a pending row to ai_trade_proposals — it never touches the real portfolio. Confirm/reject below are the only way a proposal turns into (or is discarded from) a real position. """ from typing import List, Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel from services.ai_chat_context import CONTEXT_BLOCKS router = APIRouter(prefix="/api/ai-chat", tags=["ai-chat"]) class ChatMessageBody(BaseModel): session_id: str message: str enabled_blocks: Optional[List[str]] = None refresh_context: bool = False class ClearBody(BaseModel): session_id: str @router.get("/blocks") def list_context_blocks(): """Static list of context block keys the frontend can offer as toggles.""" return {"blocks": CONTEXT_BLOCKS} @router.post("/") def send_message(body: ChatMessageBody): from services.ai_chat import send_chat_message if not body.message.strip(): raise HTTPException(400, "Message vide.") try: return send_chat_message( session_id=body.session_id, message=body.message.strip(), enabled_blocks=body.enabled_blocks, refresh_context=body.refresh_context, ) except RuntimeError as exc: raise HTTPException(400, str(exc)) from exc @router.get("/history") def get_history(session_id: str): from services.database import get_chat_messages return {"messages": get_chat_messages(session_id)} @router.post("/clear") def clear_session(body: ClearBody): from services.database import clear_chat_session from services.ai_chat_context import clear_context_cache clear_chat_session(body.session_id) clear_context_cache(body.session_id) return {"cleared": body.session_id} @router.get("/trade-proposals") def list_trade_proposals(status: str = "pending"): from services.database import get_ai_trade_proposals return {"proposals": get_ai_trade_proposals(status=status)} @router.post("/trade-proposals/{proposal_id}/confirm") def confirm_trade_proposal(proposal_id: str): """Promotes a pending AI proposal into a real open position, reusing the same enrichment (live price, Black-Scholes leg pricing) as a manual add.""" from services.database import get_ai_trade_proposal, resolve_ai_trade_proposal from routers.portfolio import add_pos, AddPositionRequest proposal = get_ai_trade_proposal(proposal_id) if not proposal: raise HTTPException(404, "Proposition introuvable.") if proposal["status"] != "pending": raise HTTPException(400, f"Proposition deja {proposal['status']}.") req = AddPositionRequest( title=proposal["title"], underlying=proposal["underlying"], strategy=proposal["strategy"], asset_class=proposal.get("asset_class") or "indices", expiry_days=proposal.get("expiry_days") or 90, legs=proposal.get("legs") or [], capital_invested=proposal["capital_invested"], geo_trigger=proposal.get("geo_trigger") or "", rationale=proposal.get("rationale") or "", ) result = add_pos(req) resolve_ai_trade_proposal(proposal_id, "confirmed", portfolio_id=result["id"]) return {"status": "confirmed", "portfolio_id": result["id"]} @router.post("/trade-proposals/{proposal_id}/reject") def reject_trade_proposal(proposal_id: str): from services.database import get_ai_trade_proposal, resolve_ai_trade_proposal proposal = get_ai_trade_proposal(proposal_id) if not proposal: raise HTTPException(404, "Proposition introuvable.") if proposal["status"] != "pending": raise HTTPException(400, f"Proposition deja {proposal['status']}.") resolve_ai_trade_proposal(proposal_id, "rejected") return {"status": "rejected"}