Kansho/backend/db.py

154 lines
5.0 KiB
Python

"""SQLite persistence for the local frame. PostgreSQL remains the later target."""
from __future__ import annotations
import json
import os
import sqlite3
from contextlib import contextmanager
from pathlib import Path
DATA_DIR = Path(__file__).resolve().parent / "data"
SCHEMA_PATH = Path(__file__).resolve().parent / "schema.sql"
SEED_PATH = Path(__file__).resolve().parent / "config" / "platform_seed.json"
PROMPTS_SEED_PATH = Path(__file__).resolve().parent / "config" / "prompts.seed.json"
_env_db = os.environ.get("KANSHO_DB_PATH")
DB_PATH = Path(_env_db) if _env_db else DATA_DIR / "kansho.sqlite"
_PROFILE_COLUMNS = {
"status": "TEXT NOT NULL DEFAULT 'active'",
"tier_id": "TEXT NOT NULL DEFAULT 'local'",
}
_PROMPT_COLUMNS = {
"description": "TEXT NOT NULL DEFAULT ''",
"category": "TEXT NOT NULL DEFAULT 'uncategorized'",
"stages_json": "TEXT",
"graph_json": "TEXT",
"output_format": "TEXT NOT NULL DEFAULT 'text'",
"output_schema_json": "TEXT",
"required_feature": "TEXT NOT NULL DEFAULT 'ai_calls'",
"default_template": "TEXT NOT NULL DEFAULT ''",
"sort_order": "INTEGER NOT NULL DEFAULT 0",
"updated": "TEXT NOT NULL DEFAULT (datetime('now'))",
}
def _connect() -> sqlite3.Connection:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH, timeout=30)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
@contextmanager
def get_db():
conn = _connect()
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def row_to_dict(row: sqlite3.Row | None) -> dict | None:
if row is None:
return None
return dict(row)
def _column_names(conn: sqlite3.Connection, table: str) -> set[str]:
rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
return {row["name"] for row in rows}
def _ensure_columns(conn: sqlite3.Connection, table: str, columns: dict[str, str]) -> None:
existing = _column_names(conn, table)
for name, ddl in columns.items():
if name not in existing:
conn.execute(f"ALTER TABLE {table} ADD COLUMN {name} {ddl}")
def _mark(conn: sqlite3.Connection, migration_id: str) -> None:
conn.execute(
"INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)",
(migration_id,),
)
def _seed_platform(conn: sqlite3.Connection) -> None:
seed = json.loads(SEED_PATH.read_text(encoding="utf-8"))
for tier in seed.get("tiers", []):
conn.execute(
"""
INSERT OR IGNORE INTO tiers (id, name, description, sort_order)
VALUES (?, ?, ?, ?)
""",
(tier["id"], tier["name"], tier.get("description") or "", tier.get("sort_order") or 0),
)
for feature in seed.get("features", []):
conn.execute(
"""
INSERT OR IGNORE INTO features
(id, name, description, category, limit_type, default_limit, reset_period)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
feature["id"],
feature["name"],
feature.get("description") or "",
feature.get("category") or "platform",
feature["limit_type"],
feature.get("default_limit"),
feature.get("reset_period") or "none",
),
)
for limit in seed.get("tier_limits", []):
conn.execute(
"""
INSERT OR IGNORE INTO tier_limits (tier_id, feature_id, limit_value)
VALUES (?, ?, ?)
""",
(limit["tier_id"], limit["feature_id"], limit.get("limit_value")),
)
def _seed_prompts(conn: sqlite3.Connection) -> None:
"""Prompts come from JSON/DB, never from Python string literals."""
items = json.loads(PROMPTS_SEED_PATH.read_text(encoding="utf-8"))
for item in items:
conn.execute(
"""
INSERT OR IGNORE INTO ai_prompts
(id, slug, name, description, category, prompt_type, template,
required_feature, is_system_default, default_template)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
""",
(
item["id"],
item["slug"],
item["name"],
item.get("description") or "",
item.get("category") or "uncategorized",
item.get("prompt_type") or "base",
item.get("template") or "",
item.get("required_feature") or "ai_calls",
item.get("template") or "",
),
)
def init_db() -> None:
schema = SCHEMA_PATH.read_text(encoding="utf-8")
with get_db() as conn:
conn.executescript(schema)
_seed_platform(conn)
_seed_prompts(conn)
_ensure_columns(conn, "profiles", _PROFILE_COLUMNS)
_ensure_columns(conn, "ai_prompts", _PROMPT_COLUMNS)
_mark(conn, "001_frame")
_mark(conn, "002_platform")
_mark(conn, "003_dialogue_memory")