diff --git a/chat/db/migrations/0004_entities.sql b/chat/db/migrations/0004_entities.sql new file mode 100644 index 0000000..ef7b9f3 --- /dev/null +++ b/chat/db/migrations/0004_entities.sql @@ -0,0 +1,18 @@ +CREATE TABLE bots ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + persona TEXT NOT NULL, + voice_samples_json TEXT NOT NULL DEFAULT '[]', + traits_json TEXT NOT NULL DEFAULT '[]', + backstory TEXT NOT NULL DEFAULT '', + initial_relationship_to_you TEXT NOT NULL DEFAULT '', + kickoff_prose TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE you_entity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + name TEXT NOT NULL, + pronouns TEXT NOT NULL DEFAULT '', + persona TEXT NOT NULL DEFAULT '' +); diff --git a/chat/state/__init__.py b/chat/state/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat/state/entities.py b/chat/state/entities.py new file mode 100644 index 0000000..57d0cc3 --- /dev/null +++ b/chat/state/entities.py @@ -0,0 +1,54 @@ +from __future__ import annotations +import json +from sqlite3 import Connection +from chat.eventlog.projector import on +from chat.eventlog.log import Event + + +@on("bot_authored") +def _apply_bot_authored(conn: Connection, e: Event) -> None: + p = e.payload + conn.execute( + "INSERT OR REPLACE INTO bots " + "(id, name, persona, voice_samples_json, traits_json, backstory, " + " initial_relationship_to_you, kickoff_prose) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (p["id"], p["name"], p["persona"], + json.dumps(p.get("voice_samples", [])), + json.dumps(p.get("traits", [])), + p.get("backstory", ""), + p.get("initial_relationship_to_you", ""), + p.get("kickoff_prose", "")), + ) + + +@on("you_authored") +def _apply_you_authored(conn: Connection, e: Event) -> None: + p = e.payload + conn.execute( + "INSERT OR REPLACE INTO you_entity (id, name, pronouns, persona) VALUES (1, ?, ?, ?)", + (p["name"], p.get("pronouns", ""), p.get("persona", "")), + ) + + +def get_bot(conn: Connection, bot_id: str) -> dict | None: + row = conn.execute("SELECT * FROM bots WHERE id = ?", (bot_id,)).fetchone() + if not row: + return None + cols = [c[1] for c in conn.execute("PRAGMA table_info(bots)").fetchall()] + d = dict(zip(cols, row)) + d["voice_samples"] = json.loads(d.pop("voice_samples_json")) + d["traits"] = json.loads(d.pop("traits_json")) + return d + + +def list_bots(conn: Connection) -> list[dict]: + cur = conn.execute("SELECT id, name FROM bots ORDER BY name") + return [{"id": r[0], "name": r[1]} for r in cur] + + +def get_you(conn: Connection) -> dict | None: + row = conn.execute("SELECT name, pronouns, persona FROM you_entity WHERE id = 1").fetchone() + if not row: + return None + return {"name": row[0], "pronouns": row[1], "persona": row[2]} diff --git a/tests/test_entities.py b/tests/test_entities.py new file mode 100644 index 0000000..5ef6f17 --- /dev/null +++ b/tests/test_entities.py @@ -0,0 +1,38 @@ +from chat.db.migrate import apply_migrations +from chat.db.connection import open_db +from chat.eventlog.log import append_event +from chat.eventlog.projector import project +from chat.state.entities import get_bot, list_bots, get_you +import chat.state.entities # registers handlers + + +def test_bot_authored_creates_bot_row(tmp_path): + db = tmp_path / "t.db" + apply_migrations(db) + with open_db(db) as conn: + append_event(conn, kind="bot_authored", payload={ + "id": "bot_a", "name": "BotA", + "persona": "...", "voice_samples": ["sample"], "traits": ["shy"], + "backstory": "...", + "initial_relationship_to_you": "coworker", + "kickoff_prose": "you stay late", + }) + project(conn) + bot = get_bot(conn, "bot_a") + assert bot is not None + assert bot["name"] == "BotA" + assert bot["traits"] == ["shy"] + assert "bot_a" in [b["id"] for b in list_bots(conn)] + + +def test_you_authored_creates_you_singleton(tmp_path): + db = tmp_path / "t.db" + apply_migrations(db) + with open_db(db) as conn: + append_event(conn, kind="you_authored", payload={ + "name": "Me", "pronouns": "they/them", "persona": "engineer", + }) + project(conn) + you = get_you(conn) + assert you is not None + assert you["name"] == "Me"