feat: desk ia

This commit is contained in:
OpenSquared
2026-06-29 15:45:21 +02:00
parent 9c30b58f22
commit 69418ab650
7 changed files with 1238 additions and 3 deletions

1086
Economic Calendar2906.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1125,6 +1125,108 @@ def _build_graph_json_from_spec(spec: dict) -> dict:
} }
def auto_assign_template(event_id: int) -> dict:
"""
Auto-assign or auto-create a causal template for a market event.
Called by the eco detector when auto_template=True in desk config.
Returns {"template_id": int, "action": "assigned"|"created", "name": str} or {"error": ...}
"""
try:
from services.database import get_conn, get_config
from services.causal_graphs import get_templates, init_tables, seed_templates
import openai
key = get_config("openai_api_key") or ""
if not key:
return {"error": "Clé OpenAI manquante"}
conn = get_conn()
init_tables(conn)
seed_templates(conn)
ev_row = conn.execute("SELECT * FROM market_events WHERE id = ?", (event_id,)).fetchone()
if not ev_row:
conn.close()
return {"error": f"Event {event_id} introuvable"}
event = dict(ev_row)
templates = get_templates(conn)
# Step 1 — recommend an existing template
recommendation = _gpt4o_recommend(event, templates)
tmpl_id = recommendation.get("template_id")
confidence = float(recommendation.get("confidence") or 0.0)
if tmpl_id and confidence >= 0.6:
conn.execute("UPDATE market_events SET template_id = ? WHERE id = ?", (tmpl_id, event_id))
conn.commit()
row = conn.execute("SELECT name FROM causal_graph_templates WHERE id = ?", (tmpl_id,)).fetchone()
conn.close()
return {"template_id": tmpl_id, "action": "assigned", "name": row["name"] if row else "", "confidence": confidence}
# Step 2 — no good match → generate a new template
conn.close()
client = openai.OpenAI(api_key=key)
prompt = f"""Tu es un analyste financier spécialisé en graphes causaux macro.
Crée un graphe causal pour l'événement suivant.
Événement :
- Nom : {event.get('name')}
- Catégorie : {event.get('category')} / {event.get('sub_type', '')}
- Description : {(event.get('description') or '')[:400]}
- Réel : {event.get('actual_value')} | Attendu : {event.get('expected_value')} | Surprise % : {event.get('surprise_pct')}
Retourne UNIQUEMENT ce JSON (sans commentaire ni markdown) :
{{
"name": "<nom court, ex: 'CPI US — Taux/USD'>",
"category": "macro_us",
"description": "<1 phrase décrivant le mécanisme>",
"input_node": {{"id": "<snake_case>", "label": "<label court>", "unit": "<unité>"}},
"intermediate_nodes": [{{"id": "<snake_case>", "label": "<label>", "coef_name": "<coef_snake_case>", "coef_value": 0.5, "coef_desc": "<description>"}}],
"output_nodes": [{{"id": "<instr_lower>_pip", "label": "<INSTRUMENT>", "instrument": "<EURUSD|XAUUSD|SP500|BRENT|US10Y>", "source_intermediate": "<id>", "coef_name": "<coef_snake_case>", "coef_value": 100, "coef_desc": "<description>"}}]
}}
Règles : coef intermédiaire ∈ [-5,5] ; coef output en pips, |val| ∈ [30,200]."""
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0.3,
max_tokens=900,
)
spec = json.loads(resp.choices[0].message.content or "{}")
graph_json = _build_graph_json_from_spec(spec)
category = spec.get("category", "macro_us")
instruments = graph_json.get("instruments", ["EURUSD"])
conn2 = get_conn()
cur = conn2.execute("""
INSERT INTO causal_graph_templates
(name, category, sub_type, description, instruments, graph_json, heuristic_ver)
VALUES (?, ?, ?, ?, ?, ?, 1)
""", (
spec.get("name", event.get("name", "Template IA")),
category,
event.get("sub_type", ""),
spec.get("description", ""),
json.dumps(instruments),
json.dumps(graph_json),
))
new_id = cur.lastrowid
conn2.execute("UPDATE market_events SET template_id = ? WHERE id = ?", (new_id, event_id))
conn2.commit()
conn2.close()
logger.info(f"[auto_template] Created template #{new_id} '{spec.get('name')}' for event #{event_id}")
return {"template_id": new_id, "action": "created", "name": spec.get("name", "Template IA"), "confidence": confidence}
except Exception as e:
logger.error(f"[auto_template] event #{event_id}: {e}")
return {"error": str(e)}
@router.post("/api/causal-lab/create-from-event") @router.post("/api/causal-lab/create-from-event")
def create_template_from_event(body: CreateFromEventRequest): def create_template_from_event(body: CreateFromEventRequest):
"""GPT-4o génère et enregistre un template causal adapté à l'événement.""" """GPT-4o génère et enregistre un template causal adapté à l'événement."""

View File

@@ -806,6 +806,7 @@ def init_db():
("sub_type", "TEXT DEFAULT NULL"), ("sub_type", "TEXT DEFAULT NULL"),
("origin", "TEXT DEFAULT NULL"), ("origin", "TEXT DEFAULT NULL"),
("source_refs", "TEXT DEFAULT '[]'"), ("source_refs", "TEXT DEFAULT '[]'"),
("template_id", "INTEGER DEFAULT NULL"),
]: ]:
try: try:
c.execute(f"ALTER TABLE market_events ADD COLUMN {_col} {_def}") c.execute(f"ALTER TABLE market_events ADD COLUMN {_col} {_def}")

View File

@@ -1654,4 +1654,21 @@ def check_new_market_events(
results["total_created"] = sum(len(results[s]) for s in ALL_SOURCES) results["total_created"] = sum(len(results[s]) for s in ALL_SOURCES)
counts = " ".join(f"{s}={len(results[s])}" for s in ALL_SOURCES) counts = " ".join(f"{s}={len(results[s])}" for s in ALL_SOURCES)
logger.info(f"[check_events] Done — {results['total_created']} new events: {counts}") logger.info(f"[check_events] Done — {results['total_created']} new events: {counts}")
# Auto-assign or auto-create causal templates for eco events
if "eco" in sources and bool(eco_cfg.get("auto_template", False)):
eco_events = results.get("eco", [])
if eco_events:
try:
from routers.causal_lab import auto_assign_template
for ev in eco_events:
eid = ev.get("event_id")
if eid:
res = auto_assign_template(eid)
action = res.get("action", "error")
name = res.get("name", "")
logger.info(f"[check_events/auto_template] event #{eid}{action}: '{name}'")
except Exception as e:
logger.warning(f"[check_events/auto_template] failed: {e}")
return results return results

View File

@@ -90,7 +90,7 @@ function KeepAlivePage({ path, component: Component }: { path: string; component
if (!kept.has(path) && !isActive) return null if (!kept.has(path) && !isActive) return null
return ( return (
<div className={isActive ? 'flex-1 min-h-0 overflow-hidden' : 'hidden'}> <div className={isActive ? 'flex-1 min-h-0 overflow-y-auto' : 'hidden'}>
<Component /> <Component />
</div> </div>
) )

View File

@@ -1,5 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import { X } from 'lucide-react' import { X, ExternalLink } from 'lucide-react'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useTabs } from '../../context/TabsContext' import { useTabs } from '../../context/TabsContext'
import { KEEP_ALIVE_META } from '../../config/keepAliveMeta' import { KEEP_ALIVE_META } from '../../config/keepAliveMeta'
@@ -33,6 +33,19 @@ export default function TabBar() {
<span <span
role="button" role="button"
tabIndex={-1} tabIndex={-1}
title="Ouvrir dans une nouvelle fenêtre"
onClick={e => {
e.stopPropagation()
window.open(path, '_blank')
}}
className="ml-1 opacity-0 group-hover:opacity-100 text-slate-600 hover:text-slate-300 rounded hover:bg-slate-600/40 p-0.5 transition-opacity leading-none"
>
<ExternalLink size={9} />
</span>
<span
role="button"
tabIndex={-1}
title="Fermer"
onClick={e => { onClick={e => {
e.stopPropagation() e.stopPropagation()
const remaining = visible.filter(m => m.path !== path) const remaining = visible.filter(m => m.path !== path)
@@ -41,7 +54,7 @@ export default function TabBar() {
navigate(remaining[remaining.length - 1].path) navigate(remaining[remaining.length - 1].path)
} }
}} }}
className="ml-1 opacity-0 group-hover:opacity-100 text-slate-600 hover:text-white rounded hover:bg-slate-600/40 p-0.5 transition-opacity leading-none" className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-white rounded hover:bg-slate-600/40 p-0.5 transition-opacity leading-none"
> >
<X size={9} /> <X size={9} />
</span> </span>

View File

@@ -488,6 +488,22 @@ function EcoConfig({
<p className="text-xs text-slate-600">Si désactivé, les surprises sont détectées mais pas enregistrées</p> <p className="text-xs text-slate-600">Si désactivé, les surprises sont détectées mais pas enregistrées</p>
</div> </div>
</div> </div>
{/* auto_template toggle — only meaningful when create_market_event is on */}
<div className={`flex items-center gap-3 ${!(config.create_market_event ?? true) ? 'opacity-40 pointer-events-none' : ''}`}>
<button
onClick={() => set('auto_template', !(config.auto_template ?? false))}
className="shrink-0"
>
{(config.auto_template ?? false)
? <ToggleRight className="w-5 h-5 text-violet-400" />
: <ToggleLeft className="w-5 h-5 text-slate-600" />}
</button>
<div>
<span className="text-sm text-slate-300">Associer template causal automatiquement</span>
<p className="text-xs text-slate-600">L'IA sélectionne le template le plus proche ( 60% de confiance) ou en génère un nouveau via GPT-4o</p>
</div>
</div>
</div> </div>
) )
} }