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 archives ( id INTEGER PRIMARY KEY AUTOINCREMENT, archive_id TEXT NOT NULL UNIQUE, 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); "); } // ── Archives ── public async Task CreateArchive(string name = null) { using var conn = CreateConnection(); // Zähle aktive Chains und Messages var chainCount = await conn.ExecuteScalarAsync( "SELECT COUNT(*) FROM chains WHERE archive_id IS NULL"); 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)"); var archive = new ArchiveRecord { ArchiveId = Guid.NewGuid().ToString("N"), Name = name, ChainCount = chainCount, MessageCount = messageCount, CreatedAt = DateTime.UtcNow.ToString("o") }; await conn.ExecuteAsync(@" INSERT INTO archives (archive_id, name, chain_count, message_count, created_at) VALUES (@ArchiveId, @Name, @ChainCount, @MessageCount, @CreatedAt) ", archive); // Alle aktiven Chains diesem Archiv zuweisen await conn.ExecuteAsync( "UPDATE chains SET archive_id = @archiveId WHERE archive_id IS NULL", new { archiveId = archive.ArchiveId }); return archive; } public async Task> GetArchives(int limit = 20) { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM archives ORDER BY id DESC LIMIT @limit", new { limit }); } 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() { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM chains WHERE archive_id IS NULL ORDER BY id DESC"); } public async Task> GetSessionChains(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(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 ORDER BY m.id LIMIT @limit", new { afterId, limit }); } }