- config.py: all constants, env vars, limits, skill registry, profiles - db.py: schema init, connection factory, skill state helpers - security.py: PIN hashing, audit logging, rate limiting, CSRF, request helpers - auth.py: session management, PIN verify, auth routes - memory.py: FTS5 CRUD + remember/forget command processing - search.py: SearXNG integration, perplexity scoring, refusal/hedge detection - gpu.py: rocm-smi stats - rag.py: Qdrant vector search + system prompt assembly - routers/: conversations, memories, models, presets, profile, settings, skills, chat, search - app.py: slim entry point, middleware, router registration only Bumps to v1.9.0
161 lines
6.3 KiB
Python
161 lines
6.3 KiB
Python
"""
|
|
JarvisChat - Database layer.
|
|
Schema init, connection factory, settings helpers, skill state management.
|
|
"""
|
|
import logging
|
|
import os
|
|
import re
|
|
import sqlite3
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from config import (
|
|
BUILTIN_SKILLS, DEFAULT_MODEL, DEFAULT_PRESETS, DEFAULT_PROFILE,
|
|
MAX_SKILL_PROMPT_CHARS, ALLOWED_NETWORKS,
|
|
)
|
|
|
|
log = logging.getLogger("jarvischat")
|
|
|
|
BASE_DIR = Path(__file__).parent
|
|
DB_PATH = BASE_DIR / "jarvischat.db"
|
|
|
|
|
|
def get_db():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA foreign_keys = ON")
|
|
return conn
|
|
|
|
|
|
def get_setting(db, key: str, default: str = "") -> str:
|
|
row = db.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
|
|
return row["value"] if row else default
|
|
|
|
|
|
def list_skills_with_state(db) -> list:
|
|
rows = db.execute("SELECT skill_key, enabled, updated_at FROM skills").fetchall()
|
|
state_by_key = {
|
|
row["skill_key"]: {"enabled": bool(row["enabled"]), "updated_at": row["updated_at"]}
|
|
for row in rows
|
|
}
|
|
merged = []
|
|
for skill in BUILTIN_SKILLS:
|
|
state = state_by_key.get(skill["key"], {"enabled": True, "updated_at": ""})
|
|
merged.append({**skill, "enabled": state["enabled"], "updated_at": state["updated_at"]})
|
|
return sorted(merged, key=lambda s: (s["category"], s["name"]))
|
|
|
|
|
|
def set_skill_enabled(db, skill_key: str, enabled: bool) -> None:
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
db.execute(
|
|
"INSERT OR REPLACE INTO skills (skill_key, enabled, updated_at) VALUES (?, ?, ?)",
|
|
(skill_key, 1 if enabled else 0, now),
|
|
)
|
|
|
|
|
|
def format_active_skills_prompt(skills: list) -> str:
|
|
lines = [
|
|
"## Active Skills",
|
|
"Use these skills only when needed. Prefer concise answers over unnecessary tool usage.",
|
|
]
|
|
for skill in skills:
|
|
lines.append(f"- {skill['key']} ({skill['risk']} risk): {skill['description']}")
|
|
text = "\n".join(lines)
|
|
if len(text) > MAX_SKILL_PROMPT_CHARS:
|
|
return text[:MAX_SKILL_PROMPT_CHARS - 3] + "..."
|
|
return text
|
|
|
|
|
|
def init_db():
|
|
from security import hash_pin
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
id TEXT PRIMARY KEY, title TEXT NOT NULL DEFAULT 'New Chat',
|
|
model TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id TEXT NOT NULL,
|
|
role TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS system_presets (
|
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, prompt TEXT NOT NULL,
|
|
is_default INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS profile (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1), content TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS skills (
|
|
skill_key TEXT PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories USING fts5(
|
|
fact, topic, source, created_at UNINDEXED
|
|
)
|
|
""")
|
|
|
|
if not conn.execute("SELECT id FROM profile WHERE id = 1").fetchone():
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
conn.execute("INSERT INTO profile (id, content, updated_at) VALUES (1, ?, ?)", (DEFAULT_PROFILE, now))
|
|
|
|
if conn.execute("SELECT COUNT(*) as c FROM system_presets").fetchone()["c"] == 0:
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
for preset in DEFAULT_PRESETS:
|
|
conn.execute(
|
|
"INSERT INTO system_presets (id, name, prompt, is_default, created_at) VALUES (?, ?, ?, 1, ?)",
|
|
(str(uuid.uuid4()), preset["name"], preset["prompt"], now),
|
|
)
|
|
|
|
defaults = {
|
|
"profile_enabled": "true", "default_model": DEFAULT_MODEL,
|
|
"search_enabled": "true", "memory_enabled": "true", "skills_enabled": "true",
|
|
}
|
|
for key, value in defaults.items():
|
|
if not conn.execute("SELECT key FROM settings WHERE key = ?", (key,)).fetchone():
|
|
conn.execute("INSERT INTO settings (key, value) VALUES (?, ?)", (key, value))
|
|
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
for skill in BUILTIN_SKILLS:
|
|
if not conn.execute("SELECT skill_key FROM skills WHERE skill_key = ?", (skill["key"],)).fetchone():
|
|
conn.execute("INSERT INTO skills (skill_key, enabled, updated_at) VALUES (?, 1, ?)", (skill["key"], now))
|
|
|
|
existing_pin_hash = conn.execute("SELECT value FROM settings WHERE key = 'admin_pin_hash'").fetchone()
|
|
existing_pin_salt = conn.execute("SELECT value FROM settings WHERE key = 'admin_pin_salt'").fetchone()
|
|
if not existing_pin_hash or not existing_pin_salt:
|
|
from config import ALLOW_DEFAULT_PIN
|
|
configured_pin = os.getenv("JARVISCHAT_ADMIN_PIN", "").strip()
|
|
if re.fullmatch(r"\d{4}", configured_pin):
|
|
seed_pin, pin_source = configured_pin, "env"
|
|
elif ALLOW_DEFAULT_PIN:
|
|
seed_pin, pin_source = "1234", "default"
|
|
else:
|
|
raise RuntimeError(
|
|
"Admin PIN bootstrap blocked: set JARVISCHAT_ADMIN_PIN to a 4-digit PIN "
|
|
"or set JARVISCHAT_ALLOW_DEFAULT_PIN=true."
|
|
)
|
|
salt_hex, pin_hash_hex = hash_pin(seed_pin)
|
|
conn.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", ("admin_pin_hash", pin_hash_hex))
|
|
conn.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", ("admin_pin_salt", salt_hex))
|
|
if pin_source == "default":
|
|
log.warning("Admin PIN seeded from insecure default 1234 (override enabled).")
|
|
else:
|
|
log.info("Admin PIN hash seeded from configured environment PIN.")
|
|
|
|
conn.commit()
|
|
conn.close()
|