feat: desk ia
This commit is contained in:
1086
Economic Calendar2906.txt
Normal file
1086
Economic Calendar2906.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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."""
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user