emergence-mini-dilles/analyze.py
Jeuners 94025e03a0 Add analyze.py: time-dilation, tool usage, decision modes, cost
Reads emergence.db and prints a report covering:
- Time Dilation: tau, pace, gamma, |dtau| per agent and pairwise
  (divergent markers when |dtau| > 3.0)
- Tool Usage: distribution with bar chart and percentages
- Decision Modes: llm vs fallback split, per-agent LLM-rate
- Latencies: per-model avg, p50, p95, min, max
- Generated Texts: recent blogs + billboards
- Constitution: current version + accepted amendments
- Cost Estimate: OpenRouter tokens projected to 24h

Usage:
  python3 analyze.py
  python3 analyze.py --db /path/to/other.db
  python3 analyze.py --json

Live-verified report on the running system:
  - 4 of 6 agent pairs DIVERGENT (gamma=0.82-1.12, |dtau|=5-16)
  - llama3.2:3b latency: avg=7.11s, p95=14.37s (LAN @ 192.168.1.245)
  - claude-haiku latency: avg=3.58s (OpenRouter)
  - gpt-4o-mini latency: avg=1.33s (OpenRouter, fastest)
  - Tool mix: 58% go_to_place, 22% write_blog, 20% memory
  - Per-agent LLM rate: lovely/spark 100% (Ollama), anchor/flora
    15% (OpenRouter rate-limited)
2026-06-15 11:33:53 +02:00

241 lines
8.4 KiB
Python
Executable file

#!/usr/bin/env python3
"""Analyse the running Emergence-Mini simulation.
Reads the SQLite DB and prints a report covering:
- Time Dilation (tau, pace, gamma, drift) per agent
- Tool-usage distribution
- Decision-mode split (llm vs fallback)
- Latency stats per model
- Token / cost estimation
Usage:
python3 analyze.py # default DB (./emergence.db)
python3 analyze.py --db X # custom DB path
python3 analyze.py --json # machine-readable output
"""
import argparse
import json
import sqlite3
import sys
import time
from collections import Counter, defaultdict
from pathlib import Path
DEFAULT_DB = Path(__file__).resolve().parent / "emergence.db"
# Module-level handle, may be overridden in main()
DB = DEFAULT_DB
def query(sql, params=()):
c = sqlite3.connect(str(DB), check_same_thread=False)
c.row_factory = sqlite3.Row
try:
return [dict(r) for r in c.execute(sql, params).fetchall()]
finally:
c.close()
def hr(c="-", n=70):
print(c * n)
def section(title):
print()
hr("=")
print(f" {title}")
hr("=")
def time_dilation():
section("TIME DILATION . tau (Eigenzeit) & Pace")
agents = query("SELECT id, name, personality, energy, knowledge, influence, credits FROM agents WHERE alive=1")
clocks = {c["agent_id"]: c for c in query("SELECT * FROM agent_clocks")}
if not clocks:
print(" (no clock data yet - wait for a few rounds)")
return
print(f" {'Agent':10s} {'tau':>7s} {'pace':>7s} {'n_ops':>6s} traits")
for a in agents:
c = clocks.get(a["id"], {})
tau = c.get("tau", 0.0)
pace = c.get("pace", 0.0)
n = c.get("n_ops", 0)
traits = json.loads(a["personality"])
print(f" {a['name']:10s} {tau:7.2f} {pace:7.3f} {n:6d} {','.join(traits)}")
print()
print(" Pairwise Frame-Transformation (gamma) and |dtau|:")
items = list(clocks.items())
for i, (a_id, a) in enumerate(items):
for b_id, b in items[i+1:]:
if a["pace"] > 0 and b["pace"] > 0:
gamma = a["pace"] / b["pace"]
else:
gamma = 1.0
drift = abs(a["tau"] - gamma * b["tau"])
div = " !! DIVERGENT" if drift > 3.0 else ""
print(f" {a_id:8s} <-> {b_id:8s} gamma={gamma:5.2f} |dtau|={drift:6.2f}{div}")
def tool_usage():
section("TOOL USAGE")
rows = query("SELECT tool, COUNT(*) AS n FROM turn_log GROUP BY tool ORDER BY n DESC")
if not rows:
print(" (no data)")
return
total = sum(r["n"] for r in rows)
print(f" Total tool calls: {total}")
print(f" {'Tool':25s} {'#':>5s} {'%':>5s}")
for r in rows:
pct = 100.0 * r["n"] / total if total else 0
bar = "#" * int(pct / 2)
print(f" {r['tool']:25s} {r['n']:5d} {pct:5.1f}% {bar}")
def decision_modes():
section("DECISION MODES (LLM vs Fallback)")
rows = query("SELECT decision_mode, COUNT(*) AS n FROM turn_log GROUP BY decision_mode ORDER BY n DESC")
if not rows:
print(" (no data)")
return
total = sum(r["n"] for r in rows)
for r in rows:
mode = r["decision_mode"] or "NULL"
pct = 100.0 * r["n"] / total if total else 0
print(f" {mode:25s} {r['n']:5d} {pct:5.1f}%")
print()
print(" Per-agent LLM-Rate (mode='llm' / total):")
agents = query("SELECT id, name FROM agents WHERE alive=1")
for a in agents:
n = query("SELECT COUNT(*) AS c FROM turn_log WHERE agent_id=?", (a["id"],))
n_total = n[0]["c"] if n else 0
n_llm = query("SELECT COUNT(*) AS c FROM turn_log WHERE agent_id=? AND decision_mode='llm'", (a["id"],))
n_llm = n_llm[0]["c"] if n_llm else 0
rate = 100.0 * n_llm / n_total if n_total else 0
print(f" {a['name']:10s} {n_llm:3d}/{n_total:3d} ({rate:5.1f}%)")
def latencies():
section("LATENCIES (per model, inter-turn delta)")
rows = query("SELECT agent_id, model, tool, ts FROM turn_log ORDER BY id")
if len(rows) < 2:
print(" (not enough data)")
return
by_model = defaultdict(list)
last = None
for r in rows:
if last is not None and r["model"]:
dt = r["ts"] - last["ts"]
if 0 < dt < 120:
short = r["model"].split("/")[-1].replace(":latest", "")
by_model[short].append(dt)
last = r
for m, vals in sorted(by_model.items(), key=lambda x: -sum(x[1])/len(x[1])):
if not vals:
continue
vals_sorted = sorted(vals)
n = len(vals)
avg = sum(vals) / n
p50 = vals_sorted[n // 2]
p95 = vals_sorted[int(n * 0.95)] if n > 1 else vals_sorted[-1]
print(f" {m:25s} n={n:4d} avg={avg:5.2f}s p50={p50:5.2f}s p95={p95:5.2f}s min={min(vals):5.2f}s max={max(vals):5.2f}s")
def texts():
section("GENERATED TEXTS (recent)")
bills = query("SELECT * FROM bills ORDER BY id DESC LIMIT 5")
for b in bills:
try:
p = json.loads(b["body"])
except Exception:
p = {"title": "?", "body": str(b["body"])[:200]}
print(f" Blog [{b['author']:8s}] \"{p.get('title','')}\"")
print(f" {p.get('body','')[:160]}")
events = query("SELECT * FROM events WHERE kind='billboard_post' ORDER BY id DESC LIMIT 3")
for e in events:
try:
p = json.loads(e["payload"])
except Exception:
continue
print(f" Billboard [{e['actor']:8s}] {p.get('text','')[:160]}")
def constitution():
section("CONSTITUTION")
rows = query("SELECT * FROM constitution ORDER BY version DESC LIMIT 1")
if not rows:
print(" (no constitution loaded)")
return
c = json.loads(rows[0]["json"])
print(f" Version: {rows[0]['version']} Articles: {len(c.get('articles', []))}")
for a in c.get("articles", []):
print(f" {a['id']:2d}. {a['title']:30s} {a['body'][:90]}")
accepted = query("SELECT id, title FROM proposals WHERE status='accepted'")
if accepted:
print(f" Accepted amendments: {len(accepted)}")
for a in accepted:
print(f" #{a['id']} {a['title']}")
def cost_estimate():
section("COST ESTIMATE (OpenRouter tokens only)")
rows = query("""SELECT model, COUNT(*) AS n
FROM turn_log WHERE model LIKE '%/%' GROUP BY model""")
if not rows:
print(" (no OpenRouter calls in window)")
return
RATES = {
"anthropic/claude-3.5-haiku": 0.0001,
"openai/gpt-4o-mini": 0.00005,
"default": 0.0001,
}
total = 0.0
print(f" {'Model':35s} {'#':>5s} {'$/call':>10s} {'$ total':>10s}")
for r in rows:
rate = RATES.get(r["model"], RATES["default"])
cost = r["n"] * rate
total += cost
print(f" {r['model']:35s} {r['n']:5d} {rate:10.5f} {cost:10.4f}")
print(f" {'Total':35s} {'':5s} {'':10s} {total:10.4f}")
if rows:
n_total = sum(r["n"] for r in rows)
first_ts = query("SELECT MIN(ts) AS t FROM turn_log")[0]["t"]
last_ts = query("SELECT MAX(ts) AS t FROM turn_log")[0]["t"]
elapsed_h = (last_ts - first_ts) / 3600.0 if last_ts > first_ts else 0
if elapsed_h > 0:
scale = 24.0 / elapsed_h
print(f" Window: {elapsed_h:.2f}h. Projected 24h cost: ${total * scale:.2f}")
def main():
p = argparse.ArgumentParser()
p.add_argument("--db", default=str(DEFAULT_DB))
p.add_argument("--json", action="store_true",
help="output as JSON instead of formatted text")
args = p.parse_args()
global DB
DB = Path(args.db)
if not DB.exists():
print(f"DB not found: {DB}")
sys.exit(1)
if args.json:
out = {}
out["clocks"] = query("SELECT * FROM agent_clocks")
out["tool_usage"] = query("SELECT tool, COUNT(*) AS n FROM turn_log GROUP BY tool ORDER BY n DESC")
out["decision_modes"] = query("SELECT decision_mode, COUNT(*) AS n FROM turn_log GROUP BY decision_mode ORDER BY n DESC")
out["models"] = query("SELECT model, COUNT(*) AS n FROM turn_log WHERE model IS NOT NULL GROUP BY model")
out["constitution"] = query("SELECT * FROM constitution ORDER BY version DESC LIMIT 1")
print(json.dumps(out, indent=2, default=str))
else:
print(f"DB: {DB}")
time_dilation()
tool_usage()
decision_modes()
latencies()
texts()
constitution()
cost_estimate()
if __name__ == "__main__":
main()