import os import aiosqlite DB_PATH = os.environ.get("DB_PATH", "/app/data/conversations.db") async def init_db(): """Initialize database with schema""" async with aiosqlite.connect(DB_PATH) as db: # Conversations table await db.execute(""" CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT UNIQUE NOT NULL, project_path TEXT NOT NULL, git_branch TEXT, started_at DATETIME NOT NULL, last_updated_at DATETIME NOT NULL, message_count INTEGER DEFAULT 0, user_message_count INTEGER DEFAULT 0, assistant_message_count INTEGER DEFAULT 0, total_input_tokens INTEGER DEFAULT 0, total_output_tokens INTEGER DEFAULT 0, model TEXT, slug TEXT, file_path TEXT NOT NULL ) """) # Messages table await db.execute(""" CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, message_uuid TEXT UNIQUE NOT NULL, parent_uuid TEXT, role TEXT NOT NULL, content_text TEXT, timestamp DATETIME NOT NULL, prompt_id TEXT, model TEXT, input_tokens INTEGER, output_tokens INTEGER, has_tool_use BOOLEAN DEFAULT 0, has_thinking BOOLEAN DEFAULT 0, stop_reason TEXT ) """) # Tool calls table await db.execute(""" CREATE TABLE IF NOT EXISTS tool_calls ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_uuid TEXT NOT NULL, tool_use_id TEXT UNIQUE NOT NULL, tool_name TEXT NOT NULL, tool_input TEXT, tool_result TEXT, is_error BOOLEAN DEFAULT 0, timestamp DATETIME NOT NULL ) """) # Daily summaries table await db.execute(""" CREATE TABLE IF NOT EXISTS daily_summaries ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT UNIQUE NOT NULL, project_path TEXT, conversation_count INTEGER, total_messages INTEGER, total_tokens INTEGER, summary_content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) # Checkpoints table await db.execute(""" CREATE TABLE IF NOT EXISTS checkpoints ( file_path TEXT PRIMARY KEY, last_line_count INTEGER NOT NULL, last_modified DATETIME NOT NULL ) """) # Create indexes await db.execute("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)") await db.execute("CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)") await db.execute("CREATE INDEX IF NOT EXISTS idx_tool_calls_message ON tool_calls(message_uuid)") await db.execute("CREATE INDEX IF NOT EXISTS idx_conversations_project ON conversations(project_path)") await db.execute("CREATE INDEX IF NOT EXISTS idx_conversations_started ON conversations(started_at)") await db.commit() async def upsert_conversation(session_id, data): """Insert or update conversation""" async with aiosqlite.connect(DB_PATH) as db: await db.execute(""" INSERT INTO conversations ( session_id, project_path, git_branch, started_at, last_updated_at, message_count, user_message_count, assistant_message_count, total_input_tokens, total_output_tokens, model, slug, file_path ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET last_updated_at = excluded.last_updated_at, message_count = excluded.message_count, user_message_count = excluded.user_message_count, assistant_message_count = excluded.assistant_message_count, total_input_tokens = excluded.total_input_tokens, total_output_tokens = excluded.total_output_tokens, model = excluded.model, slug = excluded.slug, git_branch = excluded.git_branch """, ( session_id, data['project_path'], data.get('git_branch'), data['started_at'], data['last_updated_at'], data['message_count'], data['user_message_count'], data['assistant_message_count'], data['total_input_tokens'], data['total_output_tokens'], data.get('model'), data.get('slug'), data['file_path'] )) await db.commit() async def insert_message(message_data): """Insert message (skip if exists)""" async with aiosqlite.connect(DB_PATH) as db: try: await db.execute(""" INSERT INTO messages ( session_id, message_uuid, parent_uuid, role, content_text, timestamp, prompt_id, model, input_tokens, output_tokens, has_tool_use, has_thinking, stop_reason ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( message_data['session_id'], message_data['message_uuid'], message_data.get('parent_uuid'), message_data['role'], message_data.get('content_text'), message_data['timestamp'], message_data.get('prompt_id'), message_data.get('model'), message_data.get('input_tokens'), message_data.get('output_tokens'), message_data.get('has_tool_use', False), message_data.get('has_thinking', False), message_data.get('stop_reason') )) await db.commit() except aiosqlite.IntegrityError: # Message already exists, skip pass async def insert_tool_call(tool_data): """Insert tool call (skip if exists)""" async with aiosqlite.connect(DB_PATH) as db: try: await db.execute(""" INSERT INTO tool_calls ( message_uuid, tool_use_id, tool_name, tool_input, tool_result, is_error, timestamp ) VALUES (?, ?, ?, ?, ?, ?, ?) """, ( tool_data['message_uuid'], tool_data['tool_use_id'], tool_data['tool_name'], tool_data.get('tool_input'), tool_data.get('tool_result'), tool_data.get('is_error', False), tool_data['timestamp'] )) await db.commit() except aiosqlite.IntegrityError: # Tool call already exists, skip pass async def get_checkpoint(file_path): """Get checkpoint for a file""" async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( "SELECT last_line_count, last_modified FROM checkpoints WHERE file_path = ?", (file_path,) ) row = await cursor.fetchone() return {"last_line_count": row[0], "last_modified": row[1]} if row else None async def update_checkpoint(file_path, line_count, modified_time): """Update checkpoint for a file""" async with aiosqlite.connect(DB_PATH) as db: await db.execute(""" INSERT INTO checkpoints (file_path, last_line_count, last_modified) VALUES (?, ?, ?) ON CONFLICT(file_path) DO UPDATE SET last_line_count = excluded.last_line_count, last_modified = excluded.last_modified """, (file_path, line_count, modified_time)) await db.commit() async def upsert_summary(date, data): """Insert or update daily summary""" async with aiosqlite.connect(DB_PATH) as db: await db.execute(""" INSERT INTO daily_summaries ( date, project_path, conversation_count, total_messages, total_tokens, summary_content ) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(date) DO UPDATE SET project_path = excluded.project_path, conversation_count = excluded.conversation_count, total_messages = excluded.total_messages, total_tokens = excluded.total_tokens, summary_content = excluded.summary_content, created_at = CURRENT_TIMESTAMP """, ( date, data.get('project_path'), data['conversation_count'], data['total_messages'], data['total_tokens'], data['summary_content'] )) await db.commit()