from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import Any, Dict, List, Optional import json import os from services.database import ( get_kb_entries, get_all_kb_entries, save_kb_entry, update_kb_entry_status, get_latest_reasoning_state, get_reasoning_history, get_reasoning_state_by_id, save_reasoning_state, list_ai_reports, get_mtm_trades_with_traces, ) router = APIRouter(prefix="/api/knowledge", tags=["knowledge"]) def _build_synthesis_prompt(reports: List[Dict], trades: List[Dict], kb_entries: List[Dict]): """Build the GPT-4o synthesis prompt from all accumulated data.""" now_str = __import__("datetime").datetime.utcnow().strftime("%Y-%m-%d %H:%M") # Portfolio reports summary reports_block = "" for r in reports[:10]: rpt = r.get("report") or {} stats = r.get("stats") or {} date = r.get("created_at", "")[:16] headline = rpt.get("headline", "") winners = rpt.get("winners_analysis", "") losers = rpt.get("losers_analysis", "") lessons = rpt.get("key_lessons", []) blind = rpt.get("blind_spots", "") next_p = rpt.get("next_cycle_priorities", "") lessons_str = " | ".join(lessons) if isinstance(lessons, list) else str(lessons) reports_block += f""" --- Rapport du {date} --- Headline: {headline} Stats: {stats} Gagnants: {winners[:300]} Perdants: {losers[:300]} Leçons clés: {lessons_str[:400]} Angles morts: {blind[:200]} Priorités cycle suivant: {next_p[:200]} """ # Trade history winners = [t for t in trades if (t.get("pnl_pct") or 0) > 0.5] losers = [t for t in trades if (t.get("pnl_pct") or 0) < -0.5] neutral = [t for t in trades if t not in winners and t not in losers] def trade_line(t): return (f"{t.get('underlying','?')} {t.get('strategy','?')} " f"P&L={t.get('pnl_pct',0):.2f}% score={t.get('latest_score','?')} " f"regime={t.get('macro_regime','?')}") trades_block = f""" Gagnants ({len(winners)}): {' | '.join(trade_line(t) for t in winners[:8])} Perdants ({len(losers)}): {' | '.join(trade_line(t) for t in losers[:8])} Neutres ({len(neutral)}): {len(neutral)} trades sans signal fort """ # Existing KB kb_block = "" if kb_entries: by_cat: Dict[str, List] = {} for e in kb_entries: cat = e.get("category", "général") by_cat.setdefault(cat, []).append(e) for cat, items in by_cat.items(): kb_block += f"\n[{cat.upper()}]\n" for item in items[:5]: kb_block += f" - [{item['confidence']}%] {item['title']}: {item['content'][:150]}\n" system = """Tu es l'intelligence analytique centrale d'un système de trading d'options géopolitiques. Tu dois synthétiser TOUT l'historique disponible pour produire un document de raisonnement évolutif. Ce document sera utilisé comme contexte enrichi pour tous les prochains cycles d'analyse. Réponds UNIQUEMENT en JSON valide selon le schéma spécifié.""" user = f"""Date: {now_str} === HISTORIQUE DES RAPPORTS DE PERFORMANCE ({len(reports)} rapports) === {reports_block} === HISTORIQUE DES TRADES ({len(trades)} trades) === {trades_block} === BASE DE CONNAISSANCES EXISTANTE === {kb_block if kb_block else "Aucune entrée existante — première synthèse."} === MISSION === Produis un JSON avec ces champs: {{ "narrative": "Un texte narratif riche (500-800 mots) qui décrit l'état actuel du raisonnement du système, les patterns qui fonctionnent, les erreurs récurrentes, les corrélations géopolitiques/macro identifiées, les régimes qui favorisent nos stratégies, et les priorités d'amélioration. C'est le 'cerveau' du système.", "regime_insights": [ {{"regime": "nom du régime macro", "observation": "ce qu'on sait de ce régime", "confidence": 0-100, "trade_count": N}} ], "pattern_insights": [ {{"pattern": "nom du pattern", "observation": "performance et conditions", "confidence": 0-100, "win_rate_pct": 0-100}} ], "macro_correlations": [ {{"trigger": "événement géopolitique/macro", "market_reaction": "réaction observée", "reliability": "haute/moyenne/faible"}} ], "recurring_mistakes": [ {{"mistake": "description de l'erreur", "frequency": "souvent/parfois", "mitigation": "comment l'éviter"}} ], "strengths": ["point fort 1", "point fort 2"], "blind_spots": ["angle mort 1", "angle mort 2"], "strategic_priorities": ["priorité 1", "priorité 2", "priorité 3"], "risk_parameters": {{ "avoid_when": ["condition 1", "condition 2"], "prefer_when": ["condition 1", "condition 2"] }} }}""" return system, user @router.get("/state") def get_state(): """Latest synthesized reasoning state.""" state = get_latest_reasoning_state() return {"state": state} @router.get("/history") def get_history(limit: int = 10): """List of reasoning state versions.""" return {"history": get_reasoning_history(limit)} @router.get("/history/{state_id}") def get_state_version(state_id: int): state = get_reasoning_state_by_id(state_id) if not state: raise HTTPException(404, "Version introuvable") return {"state": state} @router.get("/entries") def list_entries(status: str = "all"): if status == "all": entries = get_all_kb_entries() else: entries = get_kb_entries(status) by_cat: Dict[str, List] = {} for e in entries: by_cat.setdefault(e.get("category", "général"), []).append(e) return {"entries": entries, "by_category": by_cat, "total": len(entries)} class KbEntryIn(BaseModel): category: str title: str content: str confidence: int = 50 tags: str = "" existing_id: Optional[int] = None @router.post("/entries") def add_entry(body: KbEntryIn): entry_id = save_kb_entry( category=body.category, title=body.title, content=body.content, confidence=body.confidence, tags=body.tags, existing_id=body.existing_id, ) return {"id": entry_id} @router.patch("/entries/{entry_id}/status") def patch_entry_status(entry_id: int, body: Dict[str, str]): status = body.get("status", "active") if status not in ("active", "tentative", "invalidated"): raise HTTPException(400, "status must be active | tentative | invalidated") update_kb_entry_status(entry_id, status) return {"id": entry_id, "status": status} @router.post("/synthesize") async def synthesize(): """Run GPT-4o synthesis over all historical data and save new reasoning state.""" ai_key = os.environ.get("OPENAI_API_KEY", "") if not ai_key: raise HTTPException(400, "OpenAI API key not configured") import openai client = openai.OpenAI(api_key=ai_key) reports = list_ai_reports(limit=10) mtm_data = get_mtm_trades_with_traces(days=90) trades = mtm_data.get("all_trades", []) if isinstance(mtm_data, dict) else [] kb_entries = get_all_kb_entries() system_msg, user_msg = _build_synthesis_prompt(reports, trades, kb_entries) try: resp = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_msg}, {"role": "user", "content": user_msg}, ], temperature=0.3, max_tokens=2500, response_format={"type": "json_object"}, ) raw = resp.choices[0].message.content or "{}" synthesis = json.loads(raw) except Exception as e: raise HTTPException(500, f"GPT-4o error: {e}") narrative = synthesis.pop("narrative", "Synthèse non disponible.") state_id = save_reasoning_state( narrative=narrative, synthesis=synthesis, sources_count=len(reports) + len(trades), reports_used=len(reports), trades_analyzed=len(trades), ) # Persist KB entries from synthesis for regime in synthesis.get("regime_insights", []): if regime.get("observation"): save_kb_entry( category="régimes", title=f"Régime: {regime.get('regime', '?')}", content=regime.get("observation", ""), confidence=regime.get("confidence", 50), tags="auto-synth", ) for pattern in synthesis.get("pattern_insights", []): if pattern.get("observation"): save_kb_entry( category="patterns", title=f"Pattern: {pattern.get('pattern', '?')}", content=pattern.get("observation", ""), confidence=pattern.get("confidence", 50), tags="auto-synth", ) for mistake in synthesis.get("recurring_mistakes", []): if mistake.get("mistake"): save_kb_entry( category="erreurs", title=mistake.get("mistake", "")[:80], content=f"{mistake.get('mistake','')} → {mistake.get('mitigation','')}", confidence=70, tags="auto-synth", ) return { "state_id": state_id, "narrative_preview": narrative[:200], "kb_entries_added": ( len(synthesis.get("regime_insights", [])) + len(synthesis.get("pattern_insights", [])) + len(synthesis.get("recurring_mistakes", [])) ), "sources": {"reports": len(reports), "trades": len(trades)}, } @router.get("/context-for-cycle") def context_for_cycle(): """Compact context to inject into AI cycle prompts.""" state = get_latest_reasoning_state() if not state: return {"available": False, "context": ""} synthesis = state.get("synthesis") or {} narrative = state.get("narrative", "") priorities = synthesis.get("strategic_priorities", []) avoid = synthesis.get("risk_parameters", {}).get("avoid_when", []) prefer = synthesis.get("risk_parameters", {}).get("prefer_when", []) mistakes = [m.get("mistake", "") for m in synthesis.get("recurring_mistakes", [])[:3]] strengths = synthesis.get("strengths", []) context = f"""=== SUPER CONTEXTE — BASE DE RAISONNEMENT ({state.get('created_at','')[:16]}) === {narrative[:600]} PRIORITÉS STRATÉGIQUES: {' | '.join(priorities[:3])} ERREURS À ÉVITER: {' | '.join(mistakes)} PRÉFÉRER QUAND: {' | '.join(prefer[:2])} ÉVITER QUAND: {' | '.join(avoid[:2])} FORCES: {' | '.join(strengths[:2])} """ return { "available": True, "context": context, "version": state.get("version"), "created_at": state.get("created_at"), "sources": { "reports_used": state.get("reports_used"), "trades_analyzed": state.get("trades_analyzed"), }, }