feat: graph patch grammar DSL for causal editor
New grammar panel replaces free-text AI prompt with a two-step workflow: 1. NL to Grammar: GPT-4o converts description to patch ops (new endpoint ai-to-grammar) 2. Apply: frontend parser applies grammar locally without AI Grammar syntax: (+|-|~)(node|edge|coef|input|instruments) key=val key=val Sign shortcuts: + positive, - negative, = neutral Auto-positions new nodes when x/y omitted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1849,13 +1849,126 @@ def generate_theory(template_id: int):
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# ── AI graph modifier ─────────────────────────────────────────────────────────
|
||||
# ── AI graph modifier & grammar compiler ─────────────────────────────────────
|
||||
|
||||
class AiModifyRequest(BaseModel):
|
||||
prompt: str
|
||||
current_graph: dict = {} # client can pass current in-memory state
|
||||
|
||||
|
||||
GRAMMAR_SPEC = """\
|
||||
Grammar for causal graph patch operations (one operation per line, # = comment):
|
||||
|
||||
NODE OPERATIONS
|
||||
+node id=<id> label="<text>" type=<macro_event|observable|latent|market_asset> [x=<int>] [y=<int>] [instrument=<str>] [formula=<expr>] [unit=<str>]
|
||||
~node id=<id> [label="<text>"] [type=...] [x=<int>] [y=<int>] [instrument=<str>]
|
||||
-node id=<id>
|
||||
|
||||
EDGE OPERATIONS (sign: + positive - negative = neutral)
|
||||
+edge from=<id> to=<id> [sign=+|-|=] [s=1|2|3] [style=solid|dashed] [label="<text>"] [lag=<int>] [decay=<float|null>]
|
||||
~edge from=<id> to=<id> [sign=...] [s=...] [label="<text>"] [lag=<int>] [decay=<float|null>]
|
||||
-edge from=<id> to=<id>
|
||||
|
||||
COEFFICIENT OPERATIONS
|
||||
+coef key=<str> value=<float> [desc="<text>"]
|
||||
~coef key=<str> [value=<float>] [desc="<text>"]
|
||||
-coef key=<str>
|
||||
|
||||
INPUT MAPPING
|
||||
~input node=<id> source=<surprise|actual_value|user_input|impact_score_scaled> [key=<str>]
|
||||
-input node=<id>
|
||||
|
||||
GRAPH METADATA
|
||||
~instruments EURUSD,SP500,...
|
||||
|
||||
Vocabulary:
|
||||
- macro_event: source event node (NFP surprise, etc.)
|
||||
- observable: directly measurable market variable (OIS rate, US 2Y yield, spread)
|
||||
- latent: hidden variable (risk aversion, growth expectations, sentiment)
|
||||
- market_asset: traded instrument (EUR/USD, S&P 500 — final output nodes)
|
||||
- s=1 weak / s=2 medium (default) / s=3 strong causal link
|
||||
- style=dashed for indirect/uncertain channels
|
||||
- lag: trading days before effect onset; decay: half-life in days (null = permanent shift)
|
||||
"""
|
||||
|
||||
|
||||
class AiToGrammarRequest(BaseModel):
|
||||
prompt: str
|
||||
current_graph: dict = {}
|
||||
|
||||
|
||||
@router.post("/api/causal-lab/template/{template_id}/ai-to-grammar")
|
||||
def ai_to_grammar(template_id: int, body: AiToGrammarRequest):
|
||||
"""
|
||||
Convert a natural-language modification request into graph patch grammar.
|
||||
Returns only the grammar lines — user reviews before applying.
|
||||
"""
|
||||
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é")
|
||||
|
||||
gj = body.current_graph if body.current_graph.get("nodes") else t["graph_json"]
|
||||
|
||||
# Compact node/edge summary for context
|
||||
node_summary = "\n".join(
|
||||
f" {n['id']} ({n['type']}) — {n['label']}"
|
||||
for n in gj.get("nodes", [])
|
||||
)
|
||||
edge_summary = "\n".join(
|
||||
f" {e['from']} → {e['to']} sign={e.get('sign','?')} lag={e.get('lag_days','?')} [{e.get('label','')}]"
|
||||
for e in gj.get("edges", [])
|
||||
)
|
||||
|
||||
system_prompt = (
|
||||
"You are a causal graph patch generator for the GeoOptions macro-finance platform.\n"
|
||||
"Convert the user's natural language modification request into graph patch operations.\n"
|
||||
"Return ONLY the grammar lines — no explanation, no markdown, no fences.\n\n"
|
||||
+ GRAMMAR_SPEC
|
||||
)
|
||||
|
||||
user_prompt = (
|
||||
f'Current graph nodes:\n{node_summary}\n\n'
|
||||
f'Current graph edges:\n{edge_summary}\n\n'
|
||||
f'Modification request:\n{body.prompt}\n\n'
|
||||
"Return ONLY the grammar patch lines, one per line."
|
||||
)
|
||||
|
||||
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},
|
||||
],
|
||||
temperature=0.15,
|
||||
max_tokens=800,
|
||||
)
|
||||
grammar = (resp.choices[0].message.content or "").strip()
|
||||
# Strip any accidental markdown fences
|
||||
grammar = "\n".join(
|
||||
l for l in grammar.splitlines()
|
||||
if not l.strip().startswith("```")
|
||||
)
|
||||
return {"grammar": grammar}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[causal_lab] ai_to_grammar {template_id}: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@router.post("/api/causal-lab/template/{template_id}/ai-modify")
|
||||
def ai_modify_template(template_id: int, body: AiModifyRequest):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user