154 lines
5.0 KiB
Python
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")
|