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)
This commit is contained in:
parent
da168b0efd
commit
94025e03a0
1 changed files with 241 additions and 0 deletions
241
analyze.py
Executable file
241
analyze.py
Executable file
|
|
@ -0,0 +1,241 @@
|
||||||
|
#!/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()
|
||||||
Loading…
Reference in a new issue