using System; using System.Collections.Generic; using System.Threading.Tasks; using Dapper; using Gott.Models; using Microsoft.Data.Sqlite; namespace Gott.Services; public class DatabaseService { private readonly string _connectionString; public DatabaseService(string dbPath) { _connectionString = $"Data Source={dbPath}"; InitializeDatabase(); } private SqliteConnection CreateConnection() { var conn = new SqliteConnection(_connectionString); conn.Open(); return conn; } private void InitializeDatabase() { using var conn = CreateConnection(); Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; conn.Execute(@" CREATE TABLE IF NOT EXISTS sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL UNIQUE, name TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS archives ( id INTEGER PRIMARY KEY AUTOINCREMENT, archive_id TEXT NOT NULL UNIQUE, session_id TEXT NOT NULL, name TEXT, chain_count INTEGER NOT NULL DEFAULT 0, message_count INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS chains ( id INTEGER PRIMARY KEY AUTOINCREMENT, chain_id TEXT NOT NULL UNIQUE, session_id TEXT, archive_id TEXT, model TEXT NOT NULL, system_prompt TEXT, input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0, iterations INTEGER NOT NULL DEFAULT 0, stop_reason TEXT, duration_ms INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, chain_id TEXT NOT NULL, role TEXT NOT NULL, type TEXT NOT NULL, content TEXT, tool_name TEXT, tool_call_id TEXT, is_error INTEGER NOT NULL DEFAULT 0, iteration INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, FOREIGN KEY (chain_id) REFERENCES chains(chain_id) ); CREATE INDEX IF NOT EXISTS idx_chains_session ON chains(session_id); CREATE INDEX IF NOT EXISTS idx_chains_archive ON chains(archive_id); CREATE INDEX IF NOT EXISTS idx_messages_chain ON messages(chain_id); "); } // ── Sessions ── public async Task CreateSession(string name = null) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); var session = new SessionRecord { SessionId = Guid.NewGuid().ToString("N"), Name = name, CreatedAt = now, UpdatedAt = now }; await conn.ExecuteAsync(@" INSERT INTO sessions (session_id, name, created_at, updated_at) VALUES (@SessionId, @Name, @CreatedAt, @UpdatedAt) ", session); return session; } public async Task> GetSessions() { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM sessions ORDER BY updated_at DESC"); } public async Task GetSession(string sessionId) { using var conn = CreateConnection(); return await conn.QuerySingleOrDefaultAsync( "SELECT * FROM sessions WHERE session_id = @sessionId", new { sessionId }); } public async Task UpdateSessionTimestamp(string sessionId) { using var conn = CreateConnection(); await conn.ExecuteAsync( "UPDATE sessions SET updated_at = @now WHERE session_id = @sessionId", new { sessionId, now = DateTime.UtcNow.ToString("o") }); } public async Task UpdateSessionName(string sessionId, string name) { using var conn = CreateConnection(); await conn.ExecuteAsync( "UPDATE sessions SET name = @name, updated_at = @now WHERE session_id = @sessionId", new { sessionId, name, now = DateTime.UtcNow.ToString("o") }); } // ── Archives ── public async Task CreateArchive(string sessionId, string name = null) { using var conn = CreateConnection(); var chainCount = await conn.ExecuteScalarAsync( "SELECT COUNT(*) FROM chains WHERE archive_id IS NULL AND session_id = @sessionId", new { sessionId }); if (chainCount == 0) return null; var messageCount = await conn.ExecuteScalarAsync(@" SELECT COUNT(*) FROM messages WHERE chain_id IN (SELECT chain_id FROM chains WHERE archive_id IS NULL AND session_id = @sessionId)", new { sessionId }); var archive = new ArchiveRecord { ArchiveId = Guid.NewGuid().ToString("N"), SessionId = sessionId, Name = name, ChainCount = chainCount, MessageCount = messageCount, CreatedAt = DateTime.UtcNow.ToString("o") }; await conn.ExecuteAsync(@" INSERT INTO archives (archive_id, session_id, name, chain_count, message_count, created_at) VALUES (@ArchiveId, @SessionId, @Name, @ChainCount, @MessageCount, @CreatedAt) ", archive); await conn.ExecuteAsync( "UPDATE chains SET archive_id = @archiveId WHERE archive_id IS NULL AND session_id = @sessionId", new { archiveId = archive.ArchiveId, sessionId }); return archive; } public async Task> GetArchives(string sessionId) { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM archives WHERE session_id = @sessionId ORDER BY id DESC", new { sessionId }); } public async Task> GetArchiveChains(string archiveId) { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM chains WHERE archive_id = @archiveId ORDER BY id", new { archiveId }); } // ── Chains ── public async Task CreateChain(ChainRecord chain) { using var conn = CreateConnection(); chain.CreatedAt = DateTime.UtcNow.ToString("o"); await conn.ExecuteAsync(@" INSERT INTO chains (chain_id, session_id, archive_id, model, system_prompt, input_tokens, output_tokens, iterations, stop_reason, duration_ms, created_at) VALUES (@ChainId, @SessionId, @ArchiveId, @Model, @SystemPrompt, @InputTokens, @OutputTokens, @Iterations, @StopReason, @DurationMs, @CreatedAt) ", chain); } public async Task UpdateChain(ChainRecord chain) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" UPDATE chains SET input_tokens = @InputTokens, output_tokens = @OutputTokens, iterations = @Iterations, stop_reason = @StopReason, duration_ms = @DurationMs WHERE chain_id = @ChainId ", chain); } public async Task GetChain(string chainId) { using var conn = CreateConnection(); return await conn.QuerySingleOrDefaultAsync( "SELECT * FROM chains WHERE chain_id = @chainId", new { chainId } ); } public async Task> GetActiveChains(string sessionId) { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM chains WHERE session_id = @sessionId AND archive_id IS NULL ORDER BY id DESC", new { sessionId }); } // ── Messages ── public async Task SaveMessage(MessageRecord record) { using var conn = CreateConnection(); record.CreatedAt = DateTime.UtcNow.ToString("o"); return await conn.ExecuteScalarAsync(@" INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, created_at) VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @CreatedAt); SELECT last_insert_rowid(); ", record); } public async Task> GetChainMessages(string chainId) { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM messages WHERE chain_id = @chainId ORDER BY id", new { chainId } ); } public async Task> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200) { using var conn = CreateConnection(); return await conn.QueryAsync(@" SELECT m.* FROM messages m JOIN chains c ON m.chain_id = c.chain_id WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId ORDER BY m.id LIMIT @limit", new { sessionId, afterId, limit }); } }