Alfred Ang
Upload folder using huggingface_hub
532ffe7 verified
Raw
History Blame Contribute Delete
60.4 kB
"""
API Configuration Database Module
This module provides SQLite-based storage for API configuration metadata.
Actual API key values are stored in .streamlit/secrets.toml for security.
SQLite stores:
- LLM model configurations (both built-in and custom)
- Provider settings (enabled status, base URLs)
Author: Claude Code
Date: 26 January 2026
"""
import sqlite3
import os
from typing import Dict, List, Any, Optional
from contextlib import contextmanager
# Database file location
DB_PATH = "settings/config/api_config.db"
# Database version for migrations
DB_VERSION = 2
def get_db_path() -> str:
"""Get the database file path, ensuring directory exists"""
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
return DB_PATH
@contextmanager
def get_connection():
"""Context manager for database connections"""
conn = sqlite3.connect(get_db_path())
conn.row_factory = sqlite3.Row
try:
yield conn
conn.commit()
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
def init_database():
"""Initialize the database with required tables"""
with get_connection() as conn:
cursor = conn.cursor()
# Models table (both built-in and custom)
cursor.execute("""
CREATE TABLE IF NOT EXISTS llm_models (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
provider TEXT NOT NULL DEFAULT 'OpenAIChatCompletionClient',
model_id TEXT NOT NULL,
base_url TEXT DEFAULT 'https://openrouter.ai/api/v1',
temperature REAL DEFAULT 0.2,
api_provider TEXT DEFAULT 'OPENROUTER',
is_builtin INTEGER DEFAULT 0,
sort_order INTEGER DEFAULT 100,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Provider settings table
cursor.execute("""
CREATE TABLE IF NOT EXISTS provider_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_name TEXT UNIQUE NOT NULL,
is_enabled INTEGER DEFAULT 1,
base_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Database version table
cursor.execute("""
CREATE TABLE IF NOT EXISTS db_version (
version INTEGER PRIMARY KEY
)
""")
# API keys table (for custom API keys beyond OpenRouter/OpenAI)
cursor.execute("""
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_name TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
base_url TEXT,
description TEXT,
is_configured INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Default models per provider table
cursor.execute("""
CREATE TABLE IF NOT EXISTS default_models (
id INTEGER PRIMARY KEY AUTOINCREMENT,
api_provider TEXT UNIQUE NOT NULL,
model_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Task model assignments table - assigns specific API provider and model to each task
cursor.execute("""
CREATE TABLE IF NOT EXISTS task_model_assignments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT UNIQUE NOT NULL,
task_name TEXT NOT NULL,
api_provider TEXT NOT NULL,
model_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Admin credentials table
cursor.execute("""
CREATE TABLE IF NOT EXISTS admin_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Prompt templates table
cursor.execute("""
CREATE TABLE IF NOT EXISTS prompt_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
name TEXT NOT NULL,
display_name TEXT NOT NULL,
description TEXT,
content TEXT NOT NULL,
variables TEXT,
is_builtin INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(category, name)
)
""")
conn.commit()
# Run migrations for new columns (inside the connection context)
_run_migrations(conn)
# Seed built-in API key configurations
_seed_builtin_api_keys()
# Seed built-in models if not already present
_seed_builtin_models()
# Seed default admin credentials if not exists
_seed_admin_credentials()
# Seed built-in prompt templates if not exists
_seed_builtin_prompt_templates()
def _run_migrations(conn):
"""Run database migrations to add new columns"""
cursor = conn.cursor()
# Check if is_enabled column exists in llm_models
cursor.execute("PRAGMA table_info(llm_models)")
columns = [col[1] for col in cursor.fetchall()]
if 'is_enabled' not in columns:
cursor.execute("ALTER TABLE llm_models ADD COLUMN is_enabled INTEGER DEFAULT 1")
conn.commit()
# ============ Built-in API Keys Seed Data ============
BUILTIN_API_KEYS = [
{
"key_name": "OPENROUTER_API_KEY",
"display_name": "OpenRouter",
"base_url": "https://openrouter.ai/api/v1",
"description": "Access 38+ models (OpenAI, Anthropic, Google, DeepSeek, Meta, Qwen, Mistral) through a single key"
},
{
"key_name": "OPENAI_API_KEY",
"display_name": "OpenAI",
"base_url": "https://api.openai.com/v1",
"description": "Direct access to OpenAI models (GPT-4, GPT-4o, o1, etc.)"
},
{
"key_name": "ANTHROPIC_API_KEY",
"display_name": "Anthropic",
"base_url": "https://api.anthropic.com/v1",
"description": "Direct access to Claude models"
},
{
"key_name": "GEMINI_API_KEY",
"display_name": "Gemini",
"base_url": "https://generativelanguage.googleapis.com/v1beta",
"description": "Direct access to Google Gemini models"
},
{
"key_name": "GROQ_API_KEY",
"display_name": "Groq",
"base_url": "https://api.groq.com/openai/v1",
"description": "Fast inference with Groq LPU"
},
{
"key_name": "GROK_API_KEY",
"display_name": "Grok",
"base_url": "https://api.x.ai/v1",
"description": "Direct access to xAI Grok models"
},
{
"key_name": "DEEPSEEK_API_KEY",
"display_name": "DeepSeek",
"base_url": "https://api.deepseek.com/v1",
"description": "Direct access to DeepSeek models"
},
]
def _seed_builtin_api_keys():
"""Seed the database with built-in API key configurations"""
with get_connection() as conn:
cursor = conn.cursor()
for api_key in BUILTIN_API_KEYS:
try:
cursor.execute("""
INSERT OR IGNORE INTO api_keys
(key_name, display_name, base_url, description)
VALUES (?, ?, ?, ?)
""", (api_key["key_name"], api_key["display_name"],
api_key["base_url"], api_key["description"]))
except Exception as e:
print(f"Error seeding API key {api_key['key_name']}: {e}")
conn.commit()
def refresh_builtin_api_keys():
"""Refresh built-in API key configs (update existing, add new ones)"""
with get_connection() as conn:
cursor = conn.cursor()
for api_key in BUILTIN_API_KEYS:
try:
cursor.execute("""
INSERT INTO api_keys
(key_name, display_name, base_url, description)
VALUES (?, ?, ?, ?)
ON CONFLICT(key_name) DO UPDATE SET
display_name = excluded.display_name,
base_url = excluded.base_url,
description = excluded.description,
updated_at = CURRENT_TIMESTAMP
""", (api_key["key_name"], api_key["display_name"],
api_key["base_url"], api_key["description"]))
except Exception as e:
print(f"Error refreshing API key {api_key['key_name']}: {e}")
conn.commit()
# ============ Built-in Models Seed Data ============
BUILTIN_MODELS = [
# === OpenAI Models (via OpenRouter) ===
{"name": "GPT-5.2", "model_id": "openai/gpt-5.2", "api_provider": "OPENROUTER", "sort_order": 1},
{"name": "GPT-5", "model_id": "openai/gpt-5", "api_provider": "OPENROUTER", "sort_order": 2},
{"name": "GPT-5-Mini", "model_id": "openai/gpt-5-mini", "api_provider": "OPENROUTER", "sort_order": 3},
{"name": "GPT-4.1", "model_id": "openai/gpt-4.1", "api_provider": "OPENROUTER", "sort_order": 3},
{"name": "GPT-4.1-Mini", "model_id": "openai/gpt-4.1-mini", "api_provider": "OPENROUTER", "sort_order": 4},
{"name": "GPT-4.1-Nano", "model_id": "openai/gpt-4.1-nano", "api_provider": "OPENROUTER", "sort_order": 5},
{"name": "GPT-4o", "model_id": "openai/gpt-4o", "api_provider": "OPENROUTER", "sort_order": 6},
{"name": "GPT-4o-Mini", "model_id": "openai/gpt-4o-mini", "api_provider": "OPENROUTER", "sort_order": 7},
{"name": "o3", "model_id": "openai/o3", "api_provider": "OPENROUTER", "sort_order": 8},
{"name": "o3-Mini", "model_id": "openai/o3-mini", "api_provider": "OPENROUTER", "sort_order": 9},
{"name": "o3-Pro", "model_id": "openai/o3-pro", "api_provider": "OPENROUTER", "sort_order": 10},
{"name": "o4-Mini", "model_id": "openai/o4-mini", "api_provider": "OPENROUTER", "sort_order": 11},
{"name": "o1", "model_id": "openai/o1", "api_provider": "OPENROUTER", "sort_order": 12},
# === OpenAI Models (Native API) ===
{"name": "OpenAI GPT-5.2", "model_id": "gpt-5.2", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 98},
{"name": "OpenAI GPT-5", "model_id": "gpt-5", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 99},
{"name": "OpenAI GPT-4.1", "model_id": "gpt-4.1", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 100},
{"name": "OpenAI GPT-4.1-Mini", "model_id": "gpt-4.1-mini", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 101},
{"name": "OpenAI GPT-4.1-Nano", "model_id": "gpt-4.1-nano", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 102},
{"name": "OpenAI GPT-4o", "model_id": "gpt-4o", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 103},
{"name": "OpenAI GPT-4o-Mini", "model_id": "gpt-4o-mini", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 104},
{"name": "OpenAI o3", "model_id": "o3", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 105},
{"name": "OpenAI o3-Mini", "model_id": "o3-mini", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 106},
{"name": "OpenAI o1", "model_id": "o1", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 107},
{"name": "OpenAI o1-Mini", "model_id": "o1-mini", "api_provider": "OPENAI", "base_url": "https://api.openai.com/v1", "sort_order": 108},
# === Anthropic Claude Models (via OpenRouter) ===
{"name": "Claude-Opus-4.5", "model_id": "anthropic/claude-opus-4.5", "api_provider": "OPENROUTER", "sort_order": 20},
{"name": "Claude-Sonnet-4.5", "model_id": "anthropic/claude-sonnet-4.5", "api_provider": "OPENROUTER", "sort_order": 21},
{"name": "Claude-Opus-4", "model_id": "anthropic/claude-opus-4", "api_provider": "OPENROUTER", "sort_order": 22},
{"name": "Claude-Sonnet-4", "model_id": "anthropic/claude-sonnet-4", "api_provider": "OPENROUTER", "sort_order": 23},
{"name": "Claude-Haiku-4.5", "model_id": "anthropic/claude-haiku-4.5", "api_provider": "OPENROUTER", "sort_order": 24},
{"name": "Claude-3.5-Sonnet", "model_id": "anthropic/claude-3.5-sonnet", "api_provider": "OPENROUTER", "sort_order": 25},
# === Anthropic Claude Models (Native API) - Sonnet 4.5 is default (sort_order 0) ===
{"name": "Anthropic Claude-Sonnet-4.5", "model_id": "claude-sonnet-4-5-20251101", "api_provider": "ANTHROPIC", "base_url": "https://api.anthropic.com/v1", "sort_order": 0},
{"name": "Anthropic Claude-Opus-4.5", "model_id": "claude-opus-4-5-20251101", "api_provider": "ANTHROPIC", "base_url": "https://api.anthropic.com/v1", "sort_order": 1},
{"name": "Anthropic Claude-Sonnet-4", "model_id": "claude-sonnet-4-20250514", "api_provider": "ANTHROPIC", "base_url": "https://api.anthropic.com/v1", "sort_order": 2},
{"name": "Anthropic Claude-Opus-4", "model_id": "claude-opus-4-20250514", "api_provider": "ANTHROPIC", "base_url": "https://api.anthropic.com/v1", "sort_order": 3},
{"name": "Anthropic Claude-3.5-Sonnet", "model_id": "claude-3-5-sonnet-20241022", "api_provider": "ANTHROPIC", "base_url": "https://api.anthropic.com/v1", "sort_order": 4},
{"name": "Anthropic Claude-3.5-Haiku", "model_id": "claude-3-5-haiku-20241022", "api_provider": "ANTHROPIC", "base_url": "https://api.anthropic.com/v1", "sort_order": 5},
{"name": "Anthropic Claude-3-Opus", "model_id": "claude-3-opus-20240229", "api_provider": "ANTHROPIC", "base_url": "https://api.anthropic.com/v1", "sort_order": 6},
# === Google Gemini Models (via OpenRouter) ===
{"name": "Gemini-3-Pro", "model_id": "google/gemini-3-pro-preview", "api_provider": "OPENROUTER", "sort_order": 30},
{"name": "Gemini-2.5-Pro", "model_id": "google/gemini-2.5-pro", "api_provider": "OPENROUTER", "sort_order": 31},
{"name": "Gemini-2.5-Flash", "model_id": "google/gemini-2.5-flash", "api_provider": "OPENROUTER", "sort_order": 32},
{"name": "Gemini-2.5-Flash-Lite", "model_id": "google/gemini-2.5-flash-lite", "api_provider": "OPENROUTER", "sort_order": 33},
{"name": "Gemini-2.0-Flash", "model_id": "google/gemini-2.0-flash-exp", "api_provider": "OPENROUTER", "sort_order": 34},
{"name": "Gemini-Pro-1.5", "model_id": "google/gemini-pro-1.5", "api_provider": "OPENROUTER", "sort_order": 35},
# === Google Gemini Models (Native API) ===
{"name": "Gemini 2.0-Flash", "model_id": "gemini-2.0-flash", "api_provider": "GEMINI", "base_url": "https://generativelanguage.googleapis.com/v1beta", "sort_order": 130},
{"name": "Gemini 2.0-Flash-Lite", "model_id": "gemini-2.0-flash-lite", "api_provider": "GEMINI", "base_url": "https://generativelanguage.googleapis.com/v1beta", "sort_order": 131},
{"name": "Gemini 1.5-Pro", "model_id": "gemini-1.5-pro", "api_provider": "GEMINI", "base_url": "https://generativelanguage.googleapis.com/v1beta", "sort_order": 132},
{"name": "Gemini 1.5-Flash", "model_id": "gemini-1.5-flash", "api_provider": "GEMINI", "base_url": "https://generativelanguage.googleapis.com/v1beta", "sort_order": 133},
{"name": "Gemini 1.5-Flash-8B", "model_id": "gemini-1.5-flash-8b", "api_provider": "GEMINI", "base_url": "https://generativelanguage.googleapis.com/v1beta", "sort_order": 134},
# === DeepSeek Models (via OpenRouter) ===
{"name": "DeepSeek-V3", "model_id": "deepseek/deepseek-chat", "api_provider": "OPENROUTER", "sort_order": 40},
{"name": "DeepSeek-R1", "model_id": "deepseek/deepseek-r1", "api_provider": "OPENROUTER", "sort_order": 41},
{"name": "DeepSeek-R1-Distill-Qwen-32B", "model_id": "deepseek/deepseek-r1-distill-qwen-32b", "api_provider": "OPENROUTER", "sort_order": 42},
# === DeepSeek Models (Native API) ===
{"name": "DeepSeek Chat", "model_id": "deepseek-chat", "api_provider": "DEEPSEEK", "base_url": "https://api.deepseek.com/v1", "sort_order": 140},
{"name": "DeepSeek Reasoner", "model_id": "deepseek-reasoner", "api_provider": "DEEPSEEK", "base_url": "https://api.deepseek.com/v1", "sort_order": 141},
# === Groq Models (Native API) ===
{"name": "Groq Llama-3.3-70B", "model_id": "llama-3.3-70b-versatile", "api_provider": "GROQ", "base_url": "https://api.groq.com/openai/v1", "sort_order": 150},
{"name": "Groq Llama-3.1-8B", "model_id": "llama-3.1-8b-instant", "api_provider": "GROQ", "base_url": "https://api.groq.com/openai/v1", "sort_order": 151},
{"name": "Groq Mixtral-8x7B", "model_id": "mixtral-8x7b-32768", "api_provider": "GROQ", "base_url": "https://api.groq.com/openai/v1", "sort_order": 152},
{"name": "Groq Gemma2-9B", "model_id": "gemma2-9b-it", "api_provider": "GROQ", "base_url": "https://api.groq.com/openai/v1", "sort_order": 153},
# === xAI Grok Models (Native API) ===
{"name": "Grok 3", "model_id": "grok-3", "api_provider": "GROK", "base_url": "https://api.x.ai/v1", "sort_order": 160},
{"name": "Grok 3-Mini", "model_id": "grok-3-mini", "api_provider": "GROK", "base_url": "https://api.x.ai/v1", "sort_order": 161},
{"name": "Grok 2", "model_id": "grok-2-1212", "api_provider": "GROK", "base_url": "https://api.x.ai/v1", "sort_order": 162},
{"name": "Grok Vision", "model_id": "grok-2-vision-1212", "api_provider": "GROK", "base_url": "https://api.x.ai/v1", "sort_order": 163},
# === Qwen Models ===
{"name": "QwQ-32B", "model_id": "qwen/qwq-32b", "api_provider": "OPENROUTER", "sort_order": 50},
{"name": "Qwen-2.5-72B-Instruct", "model_id": "qwen/qwen-2.5-72b-instruct", "api_provider": "OPENROUTER", "sort_order": 51},
{"name": "Qwen-2.5-32B-Instruct", "model_id": "qwen/qwen-2.5-32b-instruct", "api_provider": "OPENROUTER", "sort_order": 52},
{"name": "Qwen-2.5-Coder-32B", "model_id": "qwen/qwen-2.5-coder-32b-instruct", "api_provider": "OPENROUTER", "sort_order": 53},
{"name": "Qwen3-VL-32B", "model_id": "qwen/qwen3-vl-32b", "api_provider": "OPENROUTER", "sort_order": 54},
# === Meta Llama Models ===
{"name": "Llama-3.3-70B", "model_id": "meta-llama/llama-3.3-70b-instruct", "api_provider": "OPENROUTER", "sort_order": 60},
{"name": "Llama-3.1-405B", "model_id": "meta-llama/llama-3.1-405b-instruct", "api_provider": "OPENROUTER", "sort_order": 61},
{"name": "Llama-3.1-70B", "model_id": "meta-llama/llama-3.1-70b-instruct", "api_provider": "OPENROUTER", "sort_order": 62},
# === Mistral Models ===
{"name": "Mistral-Large", "model_id": "mistralai/mistral-large", "api_provider": "OPENROUTER", "sort_order": 70},
{"name": "Mixtral-8x22B", "model_id": "mistralai/mixtral-8x22b-instruct", "api_provider": "OPENROUTER", "sort_order": 71},
{"name": "Codestral", "model_id": "mistralai/codestral", "api_provider": "OPENROUTER", "sort_order": 72},
]
def _seed_builtin_models():
"""Seed the database with built-in models, updating existing ones with correct model_id"""
with get_connection() as conn:
cursor = conn.cursor()
for model in BUILTIN_MODELS:
try:
base_url = model.get("base_url", "https://openrouter.ai/api/v1")
# Use ON CONFLICT to update existing built-in models with correct model_id
cursor.execute("""
INSERT INTO llm_models
(name, provider, model_id, base_url, temperature, api_provider, is_builtin, sort_order)
VALUES (?, 'OpenAIChatCompletionClient', ?, ?, 0.2, ?, 1, ?)
ON CONFLICT(name) DO UPDATE SET
model_id = excluded.model_id,
base_url = excluded.base_url,
api_provider = excluded.api_provider,
sort_order = excluded.sort_order,
updated_at = CURRENT_TIMESTAMP
WHERE is_builtin = 1
""", (model["name"], model["model_id"], base_url, model["api_provider"], model["sort_order"]))
except Exception as e:
print(f"Error seeding model {model['name']}: {e}")
conn.commit()
def refresh_builtin_models():
"""Refresh built-in models (update existing, add new ones)"""
with get_connection() as conn:
cursor = conn.cursor()
for model in BUILTIN_MODELS:
try:
base_url = model.get("base_url", "https://openrouter.ai/api/v1")
# Update if exists, insert if not
cursor.execute("""
INSERT INTO llm_models
(name, provider, model_id, base_url, temperature, api_provider, is_builtin, sort_order)
VALUES (?, 'OpenAIChatCompletionClient', ?, ?, 0.2, ?, 1, ?)
ON CONFLICT(name) DO UPDATE SET
model_id = excluded.model_id,
base_url = excluded.base_url,
api_provider = excluded.api_provider,
sort_order = excluded.sort_order,
updated_at = CURRENT_TIMESTAMP
WHERE is_builtin = 1
""", (model["name"], model["model_id"], base_url, model["api_provider"], model["sort_order"]))
except Exception as e:
print(f"Error refreshing model {model['name']}: {e}")
conn.commit()
# ============ Model Operations ============
def get_all_models(include_builtin: bool = True) -> List[Dict[str, Any]]:
"""Get all models from database"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
if include_builtin:
cursor.execute("SELECT * FROM llm_models ORDER BY sort_order, name")
else:
cursor.execute("SELECT * FROM llm_models WHERE is_builtin = 0 ORDER BY name")
rows = cursor.fetchall()
models = []
for row in rows:
# Handle case where is_enabled column might not exist yet
try:
is_enabled = bool(row["is_enabled"])
except (IndexError, KeyError):
is_enabled = True
models.append({
"id": row["id"],
"name": row["name"],
"provider": row["provider"],
"config": {
"model": row["model_id"],
"temperature": row["temperature"],
"base_url": row["base_url"]
},
"api_provider": row["api_provider"],
"is_builtin": bool(row["is_builtin"]),
"is_enabled": is_enabled
})
return models
def get_all_custom_models() -> List[Dict[str, Any]]:
"""Get only custom (non-built-in) models from database"""
return get_all_models(include_builtin=False)
def get_builtin_models() -> List[Dict[str, Any]]:
"""Get only built-in models from database"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM llm_models WHERE is_builtin = 1 ORDER BY sort_order, name")
rows = cursor.fetchall()
models = []
for row in rows:
models.append({
"id": row["id"],
"name": row["name"],
"provider": row["provider"],
"config": {
"model": row["model_id"],
"temperature": row["temperature"],
"base_url": row["base_url"]
},
"api_provider": row["api_provider"],
"is_builtin": True
})
return models
def get_model_by_name(name: str) -> Optional[Dict[str, Any]]:
"""Get a specific model by name"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM llm_models WHERE name = ?", (name,))
row = cursor.fetchone()
if row:
return {
"id": row["id"],
"name": row["name"],
"provider": row["provider"],
"config": {
"model": row["model_id"],
"temperature": row["temperature"],
"base_url": row["base_url"]
},
"api_provider": row["api_provider"],
"is_builtin": bool(row["is_builtin"])
}
return None
# Alias for backwards compatibility
def get_custom_model_by_name(name: str) -> Optional[Dict[str, Any]]:
"""Get a specific custom model by name (alias for get_model_by_name)"""
return get_model_by_name(name)
def add_custom_model(
name: str,
model_id: str,
provider: str = "OpenAIChatCompletionClient",
base_url: str = "https://openrouter.ai/api/v1",
temperature: float = 0.2,
api_provider: str = "OPENROUTER"
) -> bool:
"""Add a new custom model to database"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO llm_models (name, provider, model_id, base_url, temperature, api_provider, is_builtin, sort_order)
VALUES (?, ?, ?, ?, ?, ?, 0, 999)
""", (name, provider, model_id, base_url, temperature, api_provider))
return True
except sqlite3.IntegrityError:
# Model with this name already exists
return False
except Exception as e:
print(f"Error adding custom model: {e}")
return False
def update_model(
name: str,
model_id: Optional[str] = None,
provider: Optional[str] = None,
base_url: Optional[str] = None,
temperature: Optional[float] = None,
api_provider: Optional[str] = None
) -> bool:
"""Update an existing model (custom only)"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
# Build dynamic update query
updates = []
params = []
if model_id is not None:
updates.append("model_id = ?")
params.append(model_id)
if provider is not None:
updates.append("provider = ?")
params.append(provider)
if base_url is not None:
updates.append("base_url = ?")
params.append(base_url)
if temperature is not None:
updates.append("temperature = ?")
params.append(temperature)
if api_provider is not None:
updates.append("api_provider = ?")
params.append(api_provider)
if not updates:
return True # Nothing to update
updates.append("updated_at = CURRENT_TIMESTAMP")
params.append(name)
# Only update custom models
query = f"UPDATE llm_models SET {', '.join(updates)} WHERE name = ? AND is_builtin = 0"
cursor.execute(query, params)
return cursor.rowcount > 0
except Exception as e:
print(f"Error updating model: {e}")
return False
# Alias for backwards compatibility
def update_custom_model(*args, **kwargs) -> bool:
return update_model(*args, **kwargs)
def delete_custom_model(name: str) -> bool:
"""Delete a custom model from database (cannot delete built-in models)"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM llm_models WHERE name = ? AND is_builtin = 0", (name,))
return cursor.rowcount > 0
except Exception as e:
print(f"Error deleting custom model: {e}")
return False
def model_exists(name: str) -> bool:
"""Check if a model with the given name exists"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM llm_models WHERE name = ?", (name,))
return cursor.fetchone() is not None
# ============ Provider Settings Operations ============
def get_provider_settings(provider_name: str) -> Optional[Dict[str, Any]]:
"""Get settings for a specific provider"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM provider_settings WHERE provider_name = ?", (provider_name,))
row = cursor.fetchone()
if row:
return {
"provider_name": row["provider_name"],
"is_enabled": bool(row["is_enabled"]),
"base_url": row["base_url"]
}
return None
def set_provider_settings(provider_name: str, is_enabled: bool = True, base_url: str = None) -> bool:
"""Set or update provider settings"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO provider_settings (provider_name, is_enabled, base_url)
VALUES (?, ?, ?)
ON CONFLICT(provider_name) DO UPDATE SET
is_enabled = excluded.is_enabled,
base_url = excluded.base_url,
updated_at = CURRENT_TIMESTAMP
""", (provider_name, int(is_enabled), base_url))
return True
except Exception as e:
print(f"Error setting provider settings: {e}")
return False
# ============ Migration Helper ============
def migrate_from_json(json_models: List[Dict[str, Any]]) -> int:
"""Migrate custom models from JSON format to SQLite"""
init_database()
migrated = 0
for model in json_models:
name = model.get("name", "")
config = model.get("config", {})
if not name or not config.get("model"):
continue
success = add_custom_model(
name=name,
model_id=config.get("model", ""),
provider=model.get("provider", "OpenAIChatCompletionClient"),
base_url=config.get("base_url", "https://openrouter.ai/api/v1"),
temperature=config.get("temperature", 0.2),
api_provider=model.get("api_provider", "OPENROUTER")
)
if success:
migrated += 1
return migrated
def migrate_from_old_schema():
"""Migrate from old custom_models table to new llm_models table"""
with get_connection() as conn:
cursor = conn.cursor()
# Check if old table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='custom_models'")
if not cursor.fetchone():
return # No migration needed
# Check if new table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='llm_models'")
if cursor.fetchone():
# Migrate data from old to new
try:
cursor.execute("""
INSERT OR IGNORE INTO llm_models
(name, provider, model_id, base_url, temperature, api_provider, is_builtin, sort_order)
SELECT name, provider, model_id, base_url, temperature, api_provider, 0, 999
FROM custom_models
""")
# Drop old table after migration
cursor.execute("DROP TABLE IF EXISTS custom_models")
conn.commit()
print("Migrated custom_models to llm_models table")
except Exception as e:
print(f"Error during schema migration: {e}")
# ============ API Keys Operations ============
def get_all_api_key_configs() -> List[Dict[str, Any]]:
"""Get all API key configurations from database"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM api_keys ORDER BY id")
rows = cursor.fetchall()
return [
{
"id": row["id"],
"key_name": row["key_name"],
"display_name": row["display_name"],
"base_url": row["base_url"],
"description": row["description"],
"is_configured": bool(row["is_configured"])
}
for row in rows
]
def get_api_key_config(key_name: str) -> Optional[Dict[str, Any]]:
"""Get a specific API key configuration"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM api_keys WHERE key_name = ?", (key_name,))
row = cursor.fetchone()
if row:
return {
"id": row["id"],
"key_name": row["key_name"],
"display_name": row["display_name"],
"base_url": row["base_url"],
"description": row["description"],
"is_configured": bool(row["is_configured"])
}
return None
def add_api_key_config(
key_name: str,
display_name: str,
base_url: str = "",
description: str = ""
) -> bool:
"""Add a new API key configuration"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO api_keys (key_name, display_name, base_url, description)
VALUES (?, ?, ?, ?)
""", (key_name, display_name, base_url, description))
return True
except sqlite3.IntegrityError:
return False
except Exception as e:
print(f"Error adding API key config: {e}")
return False
def update_api_key_configured_status(key_name: str, is_configured: bool) -> bool:
"""Update the configured status of an API key"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE api_keys SET is_configured = ?, updated_at = CURRENT_TIMESTAMP
WHERE key_name = ?
""", (int(is_configured), key_name))
return cursor.rowcount > 0
except Exception as e:
print(f"Error updating API key status: {e}")
return False
def delete_api_key_config(key_name: str) -> bool:
"""Delete an API key configuration (only custom ones)"""
init_database()
# Don't allow deleting built-in keys
builtin_keys = {k["key_name"] for k in BUILTIN_API_KEYS}
if key_name in builtin_keys:
return False
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM api_keys WHERE key_name = ?", (key_name,))
return cursor.rowcount > 0
except Exception as e:
print(f"Error deleting API key config: {e}")
return False
def api_key_config_exists(key_name: str) -> bool:
"""Check if an API key configuration exists"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM api_keys WHERE key_name = ?", (key_name,))
return cursor.fetchone() is not None
# ============ Default Model Operations ============
def get_default_model(api_provider: str) -> Optional[str]:
"""Get the default model name for an API provider"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT model_name FROM default_models WHERE api_provider = ?", (api_provider,))
row = cursor.fetchone()
return row["model_name"] if row else None
def set_default_model(api_provider: str, model_name: str) -> bool:
"""Set the default model for an API provider"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO default_models (api_provider, model_name)
VALUES (?, ?)
ON CONFLICT(api_provider) DO UPDATE SET
model_name = excluded.model_name,
updated_at = CURRENT_TIMESTAMP
""", (api_provider, model_name))
return True
except Exception as e:
print(f"Error setting default model: {e}")
return False
def get_all_default_models() -> Dict[str, str]:
"""Get all default models as a dictionary {api_provider: model_name}"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT api_provider, model_name FROM default_models")
rows = cursor.fetchall()
return {row["api_provider"]: row["model_name"] for row in rows}
# ============ Model Enabled/Selected Status ============
def is_model_enabled(model_name: str) -> bool:
"""Check if a model is enabled (selected for display in sidebar)"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT is_enabled FROM llm_models WHERE name = ?", (model_name,))
row = cursor.fetchone()
return bool(row["is_enabled"]) if row else True
def set_model_enabled(model_name: str, enabled: bool) -> bool:
"""Set the enabled status for a model"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE llm_models SET is_enabled = ?, updated_at = CURRENT_TIMESTAMP
WHERE name = ?
""", (1 if enabled else 0, model_name))
return cursor.rowcount > 0
except Exception as e:
print(f"Error setting model enabled status: {e}")
return False
def get_enabled_models_by_provider(api_provider: str) -> List[Dict[str, Any]]:
"""Get only enabled models for a specific API provider"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM llm_models
WHERE api_provider = ? AND is_enabled = 1
ORDER BY sort_order, name
""", (api_provider,))
rows = cursor.fetchall()
models = []
for row in rows:
models.append({
"id": row["id"],
"name": row["name"],
"provider": row["provider"],
"config": {
"model": row["model_id"],
"temperature": row["temperature"],
"base_url": row["base_url"]
},
"api_provider": row["api_provider"],
"is_builtin": bool(row["is_builtin"]),
"is_enabled": bool(row["is_enabled"])
})
return models
# ============ Task Model Assignments ============
# Define available tasks
AVAILABLE_TASKS = [
{"task_id": "global", "task_name": "Global Default", "icon": "🌐"},
{"task_id": "chatbot", "task_name": "Chatbot", "icon": "💬"},
{"task_id": "generate_cp", "task_name": "Generate CP", "icon": "📄"},
{"task_id": "generate_courseware", "task_name": "Generate AP/FG/LG/LP", "icon": "📚"},
{"task_id": "generate_assessment", "task_name": "Generate Assessment", "icon": "✅"},
{"task_id": "generate_slides", "task_name": "Generate Slides", "icon": "🎯"},
{"task_id": "generate_brochure", "task_name": "Generate Brochure", "icon": "📰"},
{"task_id": "add_assessment_ap", "task_name": "Add Assessment to AP", "icon": "📎"},
{"task_id": "check_documents", "task_name": "Check Documents", "icon": "🔍"},
]
def get_task_model_assignment(task_id: str) -> Optional[Dict[str, Any]]:
"""Get the model assignment for a specific task"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM task_model_assignments WHERE task_id = ?
""", (task_id,))
row = cursor.fetchone()
if row:
return {
"task_id": row["task_id"],
"task_name": row["task_name"],
"api_provider": row["api_provider"],
"model_name": row["model_name"]
}
return None
def set_task_model_assignment(task_id: str, task_name: str, api_provider: str, model_name: str) -> bool:
"""Set or update the model assignment for a task"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO task_model_assignments (task_id, task_name, api_provider, model_name)
VALUES (?, ?, ?, ?)
ON CONFLICT(task_id) DO UPDATE SET
task_name = excluded.task_name,
api_provider = excluded.api_provider,
model_name = excluded.model_name,
updated_at = CURRENT_TIMESTAMP
""", (task_id, task_name, api_provider, model_name))
return True
except Exception as e:
print(f"Error setting task model assignment: {e}")
return False
def get_all_task_model_assignments() -> Dict[str, Dict[str, Any]]:
"""Get all task model assignments as a dictionary {task_id: assignment}"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM task_model_assignments")
rows = cursor.fetchall()
return {
row["task_id"]: {
"task_id": row["task_id"],
"task_name": row["task_name"],
"api_provider": row["api_provider"],
"model_name": row["model_name"]
}
for row in rows
}
def delete_task_model_assignment(task_id: str) -> bool:
"""Delete a task model assignment"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM task_model_assignments WHERE task_id = ?", (task_id,))
return cursor.rowcount > 0
except Exception as e:
print(f"Error deleting task model assignment: {e}")
return False
# ============ Admin Credentials Operations ============
import hashlib
def _hash_password(password: str) -> str:
"""Hash password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest()
def _seed_admin_credentials():
"""Seed default admin credentials if not exists (only on first run)"""
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM admin_credentials")
count = cursor.fetchone()[0]
if count == 0:
# Seed admin credentials from environment variables or Streamlit secrets
import os
try:
import streamlit as st
username = os.environ.get("ADMIN_USERNAME") or st.secrets.get("ADMIN_USERNAME", "admin")
password = os.environ.get("ADMIN_PASSWORD") or st.secrets.get("ADMIN_PASSWORD", "")
except Exception:
username = os.environ.get("ADMIN_USERNAME", "admin")
password = os.environ.get("ADMIN_PASSWORD", "")
if password:
password_hash = _hash_password(password)
cursor.execute("""
INSERT INTO admin_credentials (username, password_hash)
VALUES (?, ?)
""", (username, password_hash))
conn.commit()
def get_admin_credentials_from_db() -> Optional[Dict[str, str]]:
"""Get admin credentials from database"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT username, password_hash FROM admin_credentials LIMIT 1")
row = cursor.fetchone()
if row:
return {
"username": row["username"],
"password_hash": row["password_hash"]
}
return None
def set_admin_credentials(username: str, password: str) -> bool:
"""Set or update admin credentials in database"""
init_database()
try:
password_hash = _hash_password(password)
with get_connection() as conn:
cursor = conn.cursor()
# Delete existing and insert new
cursor.execute("DELETE FROM admin_credentials")
cursor.execute("""
INSERT INTO admin_credentials (username, password_hash)
VALUES (?, ?)
""", (username, password_hash))
return True
except Exception as e:
print(f"Error setting admin credentials: {e}")
return False
def verify_admin_password(username: str, password: str) -> bool:
"""Verify admin credentials against database"""
creds = get_admin_credentials_from_db()
if not creds:
# No credentials in DB - seed from environment/secrets and verify
_seed_admin_credentials()
creds = get_admin_credentials_from_db()
if not creds:
return False
password_hash = _hash_password(password)
return creds["username"] == username and creds["password_hash"] == password_hash
def admin_credentials_exist() -> bool:
"""Check if admin credentials have been set up"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM admin_credentials")
count = cursor.fetchone()[0]
return count > 0
# ============ Prompt Templates Operations ============
# Built-in prompt templates metadata (content loaded from files)
BUILTIN_PROMPT_TEMPLATES = [
# --- Courseware: Learner Guide (LG) ---
{
"category": "courseware",
"name": "learner_guide",
"display_name": "[LG] Learner Guide - Content Generation",
"description": "Generate Course Overview and Learning Outcome descriptions for the Learner Guide",
"variables": ""
},
# --- Courseware: Lesson Plan (LP) ---
{
"category": "courseware",
"name": "timetable_generation",
"display_name": "[LP] Lesson Plan - Timetable Generation",
"description": "Generate WSQ-compliant lesson plan timetables with session scheduling",
"variables": "num_of_days, list_of_im"
},
# --- Courseware: Facilitator Guide (FG) ---
{
"category": "courseware",
"name": "facilitator_guide",
"display_name": "[FG] Facilitator Guide - Content Generation",
"description": "Generate structured content for the Facilitator Guide document",
"variables": ""
},
# --- Courseware: Assessment Plan (AP) ---
{
"category": "courseware",
"name": "assessment_plan",
"display_name": "[AP] Assessment Plan - Evidence & Justification",
"description": "Generate structured justifications for assessment methods (CS, PP, OQ, RP) including Assessment Record & Summary",
"variables": "course_title, learning_outcomes, extracted_content, assessment_methods"
},
# --- Course Proposal: CP Interpretation ---
{
"category": "course_proposal",
"name": "cp_interpretation",
"display_name": "CP Interpretation",
"description": "Extract and structure Course Proposal data for document generation",
"variables": "schema"
},
{
"category": "course_proposal",
"name": "tsc_agent",
"display_name": "TSC Agent",
"description": "Parse and correct TSC (Training Standards and Competencies) data",
"variables": "tsc_data"
},
{
"category": "assessment",
"name": "saq_generation",
"display_name": "SAQ Generation",
"description": "Generate Short Answer Questions for assessments",
"variables": ""
},
{
"category": "assessment",
"name": "practical_performance",
"display_name": "Practical Performance",
"description": "Generate Practical Performance assessment tasks",
"variables": ""
},
{
"category": "assessment",
"name": "case_study",
"display_name": "Case Study",
"description": "Generate Case Study assessment scenarios",
"variables": ""
},
{
"category": "assessment",
"name": "project",
"display_name": "Project",
"description": "Generate Project-based assessment briefs with rubrics and deliverables",
"variables": ""
},
{
"category": "assessment",
"name": "assignment",
"display_name": "Assignment",
"description": "Generate Assignment tasks with marking criteria",
"variables": ""
},
{
"category": "assessment",
"name": "oral_interview",
"display_name": "Oral Interview",
"description": "Generate Oral Interview assessment questions and expected responses",
"variables": ""
},
{
"category": "assessment",
"name": "demonstration",
"display_name": "Demonstration",
"description": "Generate Demonstration tasks with observation checklists",
"variables": ""
},
{
"category": "assessment",
"name": "role_play",
"display_name": "Role Play",
"description": "Generate Role Play scenarios with evaluation criteria",
"variables": ""
},
{
"category": "assessment",
"name": "oral_questioning",
"display_name": "Oral Questioning",
"description": "Generate Oral Questioning assessment with probing questions",
"variables": ""
},
{
"category": "brochure",
"name": "brochure_generation",
"display_name": "Brochure Content Generation",
"description": "Generate marketing-quality content for WSQ course brochures",
"variables": "course_title, course_topics, entry_requirements, certification_info"
},
]
def _load_prompt_file_content(category: str, name: str) -> str:
"""Load prompt content from markdown file"""
import os
# Get the project root directory
current_file = os.path.abspath(__file__)
settings_dir = os.path.dirname(current_file)
project_root = os.path.dirname(settings_dir)
# Try different possible locations
possible_paths = [
os.path.join(project_root, "utils", "prompt_templates", category, f"{name}.md"),
os.path.join(project_root, "prompt_templates", category, f"{name}.md"),
]
for prompt_path in possible_paths:
if os.path.exists(prompt_path):
try:
with open(prompt_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
print(f"Error reading prompt file {prompt_path}: {e}")
return ""
def _seed_builtin_prompt_templates():
"""Seed built-in prompt templates from markdown files"""
with get_connection() as conn:
cursor = conn.cursor()
for template in BUILTIN_PROMPT_TEMPLATES:
try:
# Check if template already exists
cursor.execute(
"SELECT id FROM prompt_templates WHERE category = ? AND name = ?",
(template["category"], template["name"])
)
if cursor.fetchone():
continue # Already exists, skip
# Load content from file
content = _load_prompt_file_content(template["category"], template["name"])
if not content:
print(f"Warning: No content found for prompt {template['category']}/{template['name']}")
continue
cursor.execute("""
INSERT INTO prompt_templates
(category, name, display_name, description, content, variables, is_builtin)
VALUES (?, ?, ?, ?, ?, ?, 1)
""", (
template["category"],
template["name"],
template["display_name"],
template["description"],
content,
template["variables"]
))
except Exception as e:
print(f"Error seeding prompt template {template['name']}: {e}")
conn.commit()
def get_all_prompt_templates() -> List[Dict[str, Any]]:
"""Get all prompt templates from database"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM prompt_templates
ORDER BY category, display_name
""")
rows = cursor.fetchall()
return [
{
"id": row["id"],
"category": row["category"],
"name": row["name"],
"display_name": row["display_name"],
"description": row["description"],
"content": row["content"],
"variables": row["variables"],
"is_builtin": bool(row["is_builtin"]),
"is_active": bool(row["is_active"]),
}
for row in rows
]
def get_prompt_templates_by_category(category: str) -> List[Dict[str, Any]]:
"""Get prompt templates for a specific category"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM prompt_templates
WHERE category = ? AND is_active = 1
ORDER BY display_name
""", (category,))
rows = cursor.fetchall()
return [
{
"id": row["id"],
"category": row["category"],
"name": row["name"],
"display_name": row["display_name"],
"description": row["description"],
"content": row["content"],
"variables": row["variables"],
"is_builtin": bool(row["is_builtin"]),
"is_active": bool(row["is_active"]),
}
for row in rows
]
def get_prompt_template(category: str, name: str) -> Optional[Dict[str, Any]]:
"""Get a specific prompt template by category and name"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM prompt_templates
WHERE category = ? AND name = ?
""", (category, name))
row = cursor.fetchone()
if row:
return {
"id": row["id"],
"category": row["category"],
"name": row["name"],
"display_name": row["display_name"],
"description": row["description"],
"content": row["content"],
"variables": row["variables"],
"is_builtin": bool(row["is_builtin"]),
"is_active": bool(row["is_active"]),
}
return None
def get_prompt_template_by_id(template_id: int) -> Optional[Dict[str, Any]]:
"""Get a specific prompt template by ID"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM prompt_templates WHERE id = ?", (template_id,))
row = cursor.fetchone()
if row:
return {
"id": row["id"],
"category": row["category"],
"name": row["name"],
"display_name": row["display_name"],
"description": row["description"],
"content": row["content"],
"variables": row["variables"],
"is_builtin": bool(row["is_builtin"]),
"is_active": bool(row["is_active"]),
}
return None
def update_prompt_template(
template_id: int,
content: Optional[str] = None,
display_name: Optional[str] = None,
description: Optional[str] = None,
variables: Optional[str] = None,
is_active: Optional[bool] = None
) -> bool:
"""Update a prompt template"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
updates = []
params = []
if content is not None:
updates.append("content = ?")
params.append(content)
if display_name is not None:
updates.append("display_name = ?")
params.append(display_name)
if description is not None:
updates.append("description = ?")
params.append(description)
if variables is not None:
updates.append("variables = ?")
params.append(variables)
if is_active is not None:
updates.append("is_active = ?")
params.append(1 if is_active else 0)
if not updates:
return True
updates.append("updated_at = CURRENT_TIMESTAMP")
params.append(template_id)
query = f"UPDATE prompt_templates SET {', '.join(updates)} WHERE id = ?"
cursor.execute(query, params)
return cursor.rowcount > 0
except Exception as e:
print(f"Error updating prompt template: {e}")
return False
def add_prompt_template(
category: str,
name: str,
display_name: str,
content: str,
description: str = "",
variables: str = ""
) -> bool:
"""Add a new custom prompt template"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO prompt_templates
(category, name, display_name, description, content, variables, is_builtin)
VALUES (?, ?, ?, ?, ?, ?, 0)
""", (category, name, display_name, description, content, variables))
return True
except sqlite3.IntegrityError:
return False
except Exception as e:
print(f"Error adding prompt template: {e}")
return False
def delete_prompt_template(template_id: int) -> bool:
"""Delete a custom prompt template (cannot delete built-in)"""
init_database()
try:
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"DELETE FROM prompt_templates WHERE id = ? AND is_builtin = 0",
(template_id,)
)
return cursor.rowcount > 0
except Exception as e:
print(f"Error deleting prompt template: {e}")
return False
def reset_prompt_template_to_default(template_id: int) -> bool:
"""Reset a built-in prompt template to its default content from file"""
init_database()
template = get_prompt_template_by_id(template_id)
if not template or not template["is_builtin"]:
return False
# Load original content from file
content = _load_prompt_file_content(template["category"], template["name"])
if not content:
return False
return update_prompt_template(template_id, content=content)
def get_prompt_template_categories() -> List[str]:
"""Get list of unique prompt template categories"""
init_database()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT DISTINCT category FROM prompt_templates ORDER BY category")
rows = cursor.fetchall()
return [row["category"] for row in rows]