emergence-mini-dilles/engine/governance.py
Jeuners ddf9598518 Emergence-Mini: minimaler Klon von Emergence-World
4 Agenten, 14 Landmarks, 15 Tools, 240x240 Grid, SQLite-Persistenz.
Round-Robin Turn-Manager mit Reactive Triggern, Town-Hall-Voting
(70%-Threshold) mit Live-Constitution-Amendment.

- engine/: db, world, agents, needs, tools, reasoning, governance, turn
- web/: Canvas-basierte Live-View mit WebSocket-Stream
- server.py: FastAPI + WebSocket auf 127.0.0.1:8080
- tests/: 70 Unit + Integration Tests (pytest), alle gruen
- smoke_test.py: 50+ End-to-End-Checks
- README: Quickstart, Architektur, Security, Tests, Lizenz
- .gitignore: DB, Cache, Logs

Basiert auf https://github.com/EmergenceAI/Emergence-World
(Lizenz: CC-BY-NC-4.0, Research-only)
2026-06-15 01:07:38 +02:00

152 lines
4.9 KiB
Python

"""Constitution + Town Hall proposal lifecycle."""
import json
import sqlite3
import time
from pathlib import Path
from . import db
PASS_THRESHOLD = 0.7 # 70%
def load_constitution():
"""Load constitution. Prefer the latest saved version from the DB; fall
back to the seed JSON file on disk."""
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
c.row_factory = sqlite3.Row
try:
r = c.execute(
"SELECT json FROM constitution ORDER BY version DESC LIMIT 1"
).fetchone()
if r:
return json.loads(r["json"])
finally:
c.close()
path = Path(__file__).resolve().parent.parent / "data" / "constitution.json"
return json.loads(path.read_text())
def save_constitution(c: dict):
con = sqlite3.connect(db.DB_PATH, check_same_thread=False)
try:
version = con.execute("SELECT COALESCE(MAX(version),0)+1 AS v FROM constitution").fetchone()[0]
con.execute("INSERT INTO constitution(version,json,ts) VALUES(?,?,?)",
(version, json.dumps(c), time.time()))
con.commit()
return version
finally:
con.close()
def active_proposals():
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
c.row_factory = sqlite3.Row
try:
rows = c.execute(
"SELECT p.*, COUNT(v.agent_id) AS votes "
"FROM proposals p LEFT JOIN votes v ON v.proposal_id=p.id "
"WHERE p.status='active' GROUP BY p.id ORDER BY p.id DESC"
).fetchall()
return [dict(r) for r in rows]
finally:
c.close()
def all_proposals():
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
c.row_factory = sqlite3.Row
try:
return [dict(r) for r in c.execute(
"SELECT * FROM proposals ORDER BY id DESC LIMIT 50"
).fetchall()]
finally:
c.close()
def live_agent_count() -> int:
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
try:
return c.execute("SELECT COUNT(*) FROM agents WHERE alive=1").fetchone()[0]
finally:
c.close()
def vote_counts(proposal_id: int):
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
try:
f = c.execute("SELECT COUNT(*) FROM votes WHERE proposal_id=? AND vote='for'",
(proposal_id,)).fetchone()[0]
a = c.execute("SELECT COUNT(*) FROM votes WHERE proposal_id=? AND vote='against'",
(proposal_id,)).fetchone()[0]
return f, a
finally:
c.close()
def maybe_close_proposal(proposal_id: int):
"""If all live agents have voted, or threshold is unreachable, close the proposal."""
total_live = live_agent_count()
f, a = vote_counts(proposal_id)
cast = f + a
remaining = total_live - cast
if cast < total_live:
# threshold unreachable?
if (f + remaining) < int(total_live * PASS_THRESHOLD) + 1:
_close(proposal_id, "rejected")
return {"status": "rejected", "reason": "threshold unreachable"}
return {"status": "active", "for": f, "against": a, "remaining": remaining}
# all voted
pct = f / total_live if total_live else 0
if pct >= PASS_THRESHOLD:
_close(proposal_id, "accepted")
return {"status": "accepted", "for": f, "against": a, "pct": pct}
_close(proposal_id, "rejected")
return {"status": "rejected", "for": f, "against": a, "pct": pct}
def _close(proposal_id: int, status: str):
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
try:
c.execute("UPDATE proposals SET status=? WHERE id=?", (status, proposal_id))
c.commit()
finally:
c.close()
db.log_event("system", f"proposal_{status}", {"proposal_id": proposal_id})
def apply_accepted_proposals_to_constitution():
"""If a proposal is about amending the constitution and was accepted, apply it.
For demo: any accepted proposal is logged. If the title begins with
'Article' the article is appended to the current constitution and a
new version is written.
"""
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
c.row_factory = sqlite3.Row
try:
rows = c.execute(
"SELECT * FROM proposals WHERE status='accepted' AND applied=0"
).fetchall()
finally:
c.close()
if not rows:
return []
con = load_constitution()
new_ids = []
for r in rows:
title = r["title"]
body = r["body"]
if title.lower().startswith("article"):
next_id = max(a["id"] for a in con["articles"]) + 1
con["articles"].append({"id": next_id, "title": title, "body": body})
new_ids.append(next_id)
# mark applied
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
try:
c.execute("UPDATE proposals SET applied=1 WHERE id=?", (r["id"],))
c.commit()
finally:
c.close()
if new_ids:
save_constitution(con)
return new_ids