feat: causal lab — chip filters, editor zoom, AI modify prompt

Library:
- Color chip filters: category (with per-category colors), sub_type, instrument
- Date range filter (created_at) + text search
- Category labels unified to cover event_calendar, fundamental, calendar_event etc.
- Editer button on selected template → opens in editor tab
- Show created_at + created_by in template detail

Editor:
- Accepts initialId prop so Library can pre-load a template
- AI Modify zone: textarea prompt + button calls /api/causal-lab/template/{id}/ai-modify
- Returns modified graph_json from GPT-4o, applied live — user reviews then saves

Backend:
- New POST /api/causal-lab/template/{id}/ai-modify endpoint
  - Sends current graph_json + user prompt to GPT-4o
  - Returns validated modified graph_json (nodes/edges required)
  - Does not auto-save (user controls save)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-30 09:04:15 +02:00
parent 589faa633b
commit 40d6f14901
2 changed files with 369 additions and 31 deletions

View File

@@ -1847,3 +1847,90 @@ def generate_theory(template_id: int):
except Exception as e:
logger.error(f"[causal_lab] generate_theory {template_id}: {e}")
raise HTTPException(500, str(e))
# ── AI graph modifier ─────────────────────────────────────────────────────────
class AiModifyRequest(BaseModel):
prompt: str
current_graph: dict = {} # client can pass current in-memory state
@router.post("/api/causal-lab/template/{template_id}/ai-modify")
def ai_modify_template(template_id: int, body: AiModifyRequest):
"""
GPT-4o modifie un graph_json selon un prompt utilisateur.
Retourne le graph_json modifié sans sauvegarder — l'utilisateur valide puis sauvegarde.
"""
try:
from services.database import get_conn, get_config
from services.causal_graphs import get_template
import openai
key = get_config("openai_api_key") or ""
if not key:
raise HTTPException(400, "Clé OpenAI manquante dans la configuration")
conn = get_conn()
t = get_template(conn, template_id)
conn.close()
if not t:
raise HTTPException(404, f"Template {template_id} non trouvé")
# Use the graph sent by client (latest in-memory edits) or fall back to DB
gj = body.current_graph if body.current_graph.get("nodes") else t["graph_json"]
system_prompt = (
"You are a causal graph editor for the GeoOptions macro-finance platform.\n"
"You receive a causal graph in JSON and a modification request in natural language.\n"
"Return ONLY the complete modified graph_json as a valid JSON object — no explanation, no markdown.\n\n"
"Graph format:\n"
"{\n"
' "nodes": [{"id": str, "label": str, "type": "macro_event|observable|latent|market_asset",\n'
' "x": int, "y": int, "formula": str_opt, "unit": str_opt, "instrument": str_opt}],\n'
' "edges": [{"from": str, "to": str, "style": "solid|dashed", "strength": 1|2|3,\n'
' "sign": "positive|negative|neutral", "label": str_opt,\n'
' "lag_days": int_opt, "decay_days": float_opt}],\n'
' "coefficients": {"key": {"value": float, "calibrated": null, "description": str}},\n'
' "instruments": [str],\n'
' "input_mapping": {"node_id": {"source": "user_input|surprise|actual_value|impact_score_scaled",\n'
' "key": str_opt, "field": str_opt}}\n'
"}\n\n"
"Rules:\n"
"- Preserve existing node x/y positions unless layout is clearly broken\n"
"- All edge from/to must reference valid node ids\n"
"- instruments list must include instrument field of all market_asset nodes\n"
"- Return the COMPLETE graph_json (not just the changed parts)"
)
user_prompt = (
f'Current graph — template "{t["name"]}" ({t["category"]}, sub_type: {t.get("sub_type","")}):\n'
f"{json.dumps(gj, ensure_ascii=False, indent=2)}\n\n"
f"Modification request:\n{body.prompt}\n\n"
"Return the complete modified graph_json as JSON only."
)
client = openai.OpenAI(api_key=key)
resp = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
response_format={"type": "json_object"},
temperature=0.25,
max_tokens=3000,
)
raw = resp.choices[0].message.content or "{}"
modified = json.loads(raw)
if "nodes" not in modified or "edges" not in modified:
raise ValueError("L'IA n'a pas retourné un graph_json valide (nodes/edges manquants)")
return {"graph_json": modified, "template_name": t["name"]}
except HTTPException:
raise
except Exception as e:
logger.error(f"[causal_lab] ai_modify_template {template_id}: {e}")
raise HTTPException(500, str(e))