Initial commit — GeoOptions Intelligence Cockpit v2.0
Stack: FastAPI + React/TypeScript + SQLite + GPT-4o Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM, Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA. Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
309
backend/routers/knowledge.py
Normal file
309
backend/routers/knowledge.py
Normal file
@@ -0,0 +1,309 @@
|
||||
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"),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user