feat: portfolio context injection + AI call log viewer

Portfolio context (portfolio_context.py):
- get_open_trades_with_moves(): fetches open trades + 1d/5d yfinance price moves
- get_portfolio_concentration(): counts by asset_class
- build_portfolio_context_block(): formatted prompt block with strict AI instructions
  (no double positions, flag contradictions, avoid overweight classes)

AI call logging:
- ai_call_logs table in DB (run_id, call_type, system/user prompt, response, tokens, ms)
- _chat() now accepts log_meta dict → saves call to DB non-blocking after each call
- suggest and score_batch calls pass run_id + call_type for full traceability

auto_cycle.py:
- Builds portfolio context before snapshot and both AI calls
- Context snapshot now includes portfolio_open_positions key

SystemLogs.tsx:
- "Contexte IA" tab gains sub-tabs: Contexte / Appels IA
- AiCallRow: expandable with 3 panes (user prompt / system prompt / response)
  shows model, tokens breakdown, duration, call type badge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-21 10:36:05 +02:00
parent 5d3ff19393
commit 4ad3a9a782
7 changed files with 457 additions and 17 deletions

View File

@@ -407,6 +407,26 @@ def init_db():
context_json TEXT NOT NULL
)""")
c.execute("""CREATE TABLE IF NOT EXISTS ai_call_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
call_type TEXT NOT NULL,
pattern_id TEXT,
pattern_name TEXT,
system_prompt TEXT,
user_prompt TEXT,
response_json TEXT,
model TEXT DEFAULT 'gpt-4o',
tokens_prompt INTEGER DEFAULT 0,
tokens_completion INTEGER DEFAULT 0,
duration_ms INTEGER DEFAULT 0,
called_at TEXT NOT NULL DEFAULT (datetime('now'))
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_acl_run ON ai_call_logs(run_id, called_at DESC)")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS news_price_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_hash TEXT NOT NULL,
@@ -2239,6 +2259,60 @@ def list_cycle_context_snapshots(limit: int = 30) -> list:
return [{"run_id": r["run_id"], "ts": r["ts"]} for r in rows]
# ── AI Call Logs ──────────────────────────────────────────────────────────────
def save_ai_call_log(
run_id: str,
call_type: str,
system_prompt: str,
user_prompt: str,
response_json: str,
model: str = "gpt-4o",
tokens_prompt: int = 0,
tokens_completion: int = 0,
duration_ms: int = 0,
pattern_id: str = None,
pattern_name: str = None,
) -> None:
import json as _json
conn = get_conn()
conn.execute(
"""INSERT INTO ai_call_logs
(run_id, call_type, pattern_id, pattern_name, system_prompt, user_prompt,
response_json, model, tokens_prompt, tokens_completion, duration_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(run_id, call_type, pattern_id, pattern_name,
system_prompt[:8000], # cap system prompt at 8KB
user_prompt[:80000], # cap user prompt at 80KB
response_json[:50000], # cap response at 50KB
model, tokens_prompt, tokens_completion, duration_ms)
)
conn.commit()
conn.close()
def get_ai_call_logs(run_id: str) -> list:
import json as _json
conn = get_conn()
rows = conn.execute(
"""SELECT id, call_type, pattern_id, pattern_name, system_prompt, user_prompt,
response_json, model, tokens_prompt, tokens_completion, duration_ms, called_at
FROM ai_call_logs WHERE run_id = ? ORDER BY called_at ASC""",
(run_id,)
).fetchall()
conn.close()
result = []
for r in rows:
d = dict(r)
# Try to parse response_json for display
try:
d["response"] = _json.loads(d["response_json"]) if d["response_json"] else None
except Exception:
d["response"] = d["response_json"]
result.append(d)
return result
# ── News Price Snapshots (Phase 4 — Price Discovery) ─────────────────────────
def save_news_price_snapshot(