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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user