Files
OpenFin/backend/routers/ai_chat.py
2026-07-15 08:47:16 +02:00

110 lines
3.8 KiB
Python

"""
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"}