diff --git a/Endpoints/AgentEndpoint.cs b/Endpoints/AgentEndpoint.cs index ff29be6..2d56646 100644 --- a/Endpoints/AgentEndpoint.cs +++ b/Endpoints/AgentEndpoint.cs @@ -64,6 +64,7 @@ public static class AgentEndpoint a.UseIdentity, a.UseUserInfo, a.UseMemory, + a.UseAutoContext, a.UseWebSearch, a.WebSearchVersion, a.AllowUserChoice, @@ -119,6 +120,7 @@ public static class AgentEndpoint UseIdentity = !body.TryGetProperty("useIdentity", out var ui) || ui.GetBoolean(), UseUserInfo = !body.TryGetProperty("useUserInfo", out var uu) || uu.GetBoolean(), UseMemory = !body.TryGetProperty("useMemory", out var um) || um.GetBoolean(), + UseAutoContext = !body.TryGetProperty("useAutoContext", out var uac) || uac.GetBoolean(), UseWebSearch = body.TryGetProperty("webSearchVersion", out var wsv) && !string.IsNullOrEmpty(wsv.GetString()), WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : "", AllowUserChoice = !body.TryGetProperty("allowUserChoice", out var auc) || auc.GetBoolean(), @@ -160,6 +162,7 @@ public static class AgentEndpoint if (body.TryGetProperty("useIdentity", out var ui)) agent.UseIdentity = ui.GetBoolean(); if (body.TryGetProperty("useUserInfo", out var uu)) agent.UseUserInfo = uu.GetBoolean(); if (body.TryGetProperty("useMemory", out var um)) agent.UseMemory = um.GetBoolean(); + if (body.TryGetProperty("useAutoContext", out var uac)) agent.UseAutoContext = uac.GetBoolean(); if (body.TryGetProperty("webSearchVersion", out var wsv)) { agent.WebSearchVersion = wsv.GetString() ?? ""; diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 4a9597c..a2d3f70 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -208,7 +208,7 @@ public static class ChatEndpoint try { - await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr); + await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr); } finally { diff --git a/Models/AgentRecord.cs b/Models/AgentRecord.cs index 2d312b7..f4e6e33 100644 --- a/Models/AgentRecord.cs +++ b/Models/AgentRecord.cs @@ -16,6 +16,7 @@ public class AgentRecord public bool UseIdentity { get; set; } = true; public bool UseUserInfo { get; set; } = true; public bool UseMemory { get; set; } = true; + public bool UseAutoContext { get; set; } = true; public bool UseWebSearch { get; set; } public string WebSearchVersion { get; set; } = ""; public bool AllowUserChoice { get; set; } = true; diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 332d9f0..fdc2f2e 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -55,6 +55,7 @@ public class AnthropicService SubAgentContext finalizer = null, bool allowUserChoice = true, bool planMode = false, + bool useAutoContext = true, CancellationToken workCt = default, CancellationToken hardCt = default, CancellationToken sseCt = default, @@ -76,6 +77,46 @@ public class AnthropicService try { + // ── Automatische Kontext-Injektion via Embedding-Suche ── + if (useAutoContext && _vectorSearch != null && systemPrompt != null && messages.Count > 0) + { + try + { + // Letzte User-Message als Query extrahieren + var lastUserText = ExtractLastUserText(messages); + if (!string.IsNullOrWhiteSpace(lastUserText)) + { + var contextResults = await _vectorSearch.SearchAsync(lastUserText, agentId, topN: 5, minScore: 0.45f); + // Nur Ergebnisse aus anderen Sessions (nicht aktuelle Chain) und nicht zu kurze + contextResults.RemoveAll(r => r.ChainId == chainId || (r.TextPreview?.Length ?? 0) < 20); + // Memory-Dateien durchsuchen + var memoryResults = await _vectorSearch.SearchMemoryFilesAsync(lastUserText, agentId, topN: 3, minScore: 0.45f); + + if (contextResults.Count > 0 || memoryResults.Count > 0) + { + var ctxSb = new StringBuilder(); + ctxSb.AppendLine("\n"); + ctxSb.AppendLine("Die folgende Information wurde automatisch abgerufen und könnte relevant sein:"); + foreach (var r in contextResults) + { + var date = DateTime.TryParse(r.CreatedAt, out var dt) ? dt.ToString("yyyy-MM-dd HH:mm") : r.CreatedAt; + ctxSb.AppendLine($"- [Gespräch, {r.Role}, {date}]: {r.TextPreview}"); + } + foreach (var r in memoryResults) + { + ctxSb.AppendLine($"- [Memory, {r.FilePath}]: {r.TextPreview}"); + } + ctxSb.AppendLine(""); + systemPrompt.Add(new { type = "text", text = ctxSb.ToString() }); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[AutoContext] Error: {ex.Message}"); + } + } + while (iterations < maxIterations) { // Hard-Stop: Sofortiger Abbruch aus while-Loop (Doppel-Klick Stop) @@ -838,7 +879,7 @@ public class AnthropicService string toolResult; bool isError = false; - try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch); } + try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); } catch (OperationCanceledException) { throw; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } @@ -1044,7 +1085,7 @@ public class AnthropicService string toolResult; bool isError = false; - try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch); } + try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); } catch (OperationCanceledException) { throw; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } @@ -1345,7 +1386,7 @@ public class AnthropicService string toolResult; bool isError = false; - try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch); } + try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); } catch (OperationCanceledException) { throw; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } @@ -1468,6 +1509,35 @@ public class AnthropicService : result; } + /// + /// Extrahiert den Text der letzten User-Message aus der Message-Liste. + /// + private static string ExtractLastUserText(List messages) + { + for (int i = messages.Count - 1; i >= 0; i--) + { + var json = JsonSerializer.Serialize(messages[i]); + var el = JsonSerializer.Deserialize(json); + if (!el.TryGetProperty("role", out var role) || role.GetString() != "user") continue; + if (!el.TryGetProperty("content", out var content)) continue; + + if (content.ValueKind == JsonValueKind.String) + return content.GetString(); + + if (content.ValueKind == JsonValueKind.Array) + { + foreach (var block in content.EnumerateArray()) + { + var type = block.TryGetProperty("type", out var t) ? t.GetString() : ""; + if (type == "text" && block.TryGetProperty("text", out var txt)) + return txt.GetString(); + } + } + break; + } + return null; + } + /// /// Extrahiert Web-Search-Sources aus einem web_search_tool_result-Block. /// diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 11a9e95..4b794f7 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -149,6 +149,7 @@ public class DatabaseService TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1"); + TryAddColumn(conn, "agents", "use_auto_context", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "messages", "deleted_on", "TEXT"); TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0"); @@ -472,8 +473,8 @@ public class DatabaseService agent.CreatedAt = now; agent.UpdatedAt = now; await conn.ExecuteAsync(@" - INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at) - VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt) + INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at) + VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt) ", agent); return agent; } @@ -507,7 +508,7 @@ public class DatabaseService UPDATE agents SET name = @Name, avatar = @Avatar, avatar_small = @AvatarSmall, description = @Description, model = @Model, tool_choice = @ToolChoice, custom_commands = @CustomCommands, auto_commit = @AutoCommit, - use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion, + use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_auto_context = @UseAutoContext, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion, allow_user_choice = @AllowUserChoice, task_instruction = @TaskInstruction, avatar_scale = @AvatarScale, avatar_offset_x = @AvatarOffsetX, avatar_offset_y = @AvatarOffsetY, is_public = @IsPublic, updated_at = @UpdatedAt diff --git a/Services/EmbeddingBackgroundService.cs b/Services/EmbeddingBackgroundService.cs index 180dc15..d1d8cb6 100644 --- a/Services/EmbeddingBackgroundService.cs +++ b/Services/EmbeddingBackgroundService.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; @@ -21,12 +22,14 @@ public class EmbeddingBackgroundService : BackgroundService readonly string _dbConnectionString; readonly ConcurrentQueue _queue = new(); readonly SemaphoreSlim _signal = new(0); + readonly string _dataPath; public EmbeddingBackgroundService(VectorSearchService vectorSearch, EmbeddingService embedding, string dbPath) { _vectorSearch = vectorSearch; _embedding = embedding; _dbConnectionString = $"Data Source={dbPath}"; + _dataPath = Path.GetDirectoryName(dbPath); } /// Enqueue eine neue Message zum Embedden. @@ -50,6 +53,9 @@ public class EmbeddingBackgroundService : BackgroundService // Backfill bestehender Messages await BackfillAsync(ct); + // Memory-Dateien embedden + await BackfillMemoryFilesAsync(ct); + // Live-Embedding neuer Messages Console.WriteLine("[EmbeddingBG] Waiting for new messages..."); while (!ct.IsCancellationRequested) @@ -62,12 +68,21 @@ public class EmbeddingBackgroundService : BackgroundService if (ct.IsCancellationRequested) break; try { - await _vectorSearch.EmbedMessageAsync(job.MessageId, job.ChainId, job.SessionId, - job.AgentId, job.Role, job.Text); + if (job.Role.StartsWith("memory:")) + { + // Memory-File-Embedding + var filePath = job.Role.Substring("memory:".Length); + await _vectorSearch.EmbedMemoryFileAsync(job.AgentId, filePath, job.Text); + } + else + { + await _vectorSearch.EmbedMessageAsync(job.MessageId, job.ChainId, job.SessionId, + job.AgentId, job.Role, job.Text); + } } catch (Exception ex) { - Console.WriteLine($"[EmbeddingBG] Error embedding message {job.MessageId}: {ex.Message}"); + Console.WriteLine($"[EmbeddingBG] Error embedding job: {ex.Message}"); } } } @@ -133,6 +148,54 @@ public class EmbeddingBackgroundService : BackgroundService Console.WriteLine($"[EmbeddingBG] Backfill complete: {embedded} new embeddings, {skipped} skipped, {total} total messages."); } + async Task BackfillMemoryFilesAsync(CancellationToken ct) + { + Console.WriteLine("[EmbeddingBG] Starting memory file backfill..."); + + var agentsDir = Path.Combine(_dataPath, "agents"); + if (!Directory.Exists(agentsDir)) + { + Console.WriteLine("[EmbeddingBG] No agents directory found, skipping memory backfill."); + return; + } + + int total = 0; + foreach (var agentDir in Directory.GetDirectories(agentsDir)) + { + if (ct.IsCancellationRequested) break; + var agentId = Path.GetFileName(agentDir); + + // Alle .md Dateien im Agent-Verzeichnis (rekursiv) + var mdFiles = Directory.GetFiles(agentDir, "*.md", SearchOption.AllDirectories); + foreach (var mdFile in mdFiles) + { + if (ct.IsCancellationRequested) break; + try + { + var text = await File.ReadAllTextAsync(mdFile, ct); + if (string.IsNullOrWhiteSpace(text) || text.Length < 10) continue; + + var relativePath = Path.GetRelativePath(agentDir, mdFile); + await _vectorSearch.EmbedMemoryFileAsync(agentId, relativePath, text); + total++; + } + catch (Exception ex) + { + Console.WriteLine($"[EmbeddingBG] Error embedding memory file {mdFile}: {ex.Message}"); + } + } + } + + Console.WriteLine($"[EmbeddingBG] Memory file backfill complete: {total} files embedded."); + } + + /// Embeddet eine einzelne Memory-Datei (nach memory_write). + public void EnqueueMemoryFile(string agentId, string filePath, string text) + { + _queue.Enqueue(new EmbeddingJob(0, "", "", agentId, "memory:" + filePath, text)); + _signal.Release(); + } + record EmbeddingJob(long MessageId, string ChainId, string SessionId, string AgentId, string Role, string Text); class BackfillRow diff --git a/Services/ToolService.cs b/Services/ToolService.cs index 8067fe5..72464fe 100644 --- a/Services/ToolService.cs +++ b/Services/ToolService.cs @@ -354,7 +354,7 @@ public class ToolService }; } - public async Task ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null, VectorSearchService vectorSearch = null) + public async Task ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null) { switch (name) { @@ -628,6 +628,13 @@ public class ToolService memoryService.AppendMemoryFile(agentId, path, content); else memoryService.WriteMemoryFile(agentId, path, content); + // Memory-Datei nach Schreiben embedden + if (embeddingBg != null) + { + var fullText = memoryService.ReadMemoryFile(agentId, path); + if (!string.IsNullOrEmpty(fullText)) + embeddingBg.EnqueueMemoryFile(agentId, path, fullText); + } return $"Memory-Datei {(append ? "erweitert" : "geschrieben")}: {path} ({content.Length} Zeichen)"; } case "memory_list": diff --git a/Services/VectorSearchService.cs b/Services/VectorSearchService.cs index 19e5785..8a2f84c 100644 --- a/Services/VectorSearchService.cs +++ b/Services/VectorSearchService.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -49,6 +50,17 @@ public class VectorSearchService CREATE INDEX IF NOT EXISTS idx_emb_session ON message_embeddings(session_id); CREATE INDEX IF NOT EXISTS idx_emb_agent ON message_embeddings(agent_id); CREATE INDEX IF NOT EXISTS idx_emb_message ON message_embeddings(message_id); + + CREATE TABLE IF NOT EXISTS memory_file_embeddings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + agent_id TEXT NOT NULL, + file_path TEXT NOT NULL, + chunk_index INTEGER NOT NULL, + text_preview TEXT, + vector BLOB NOT NULL, + updated_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_memfile_agent ON memory_file_embeddings(agent_id); "); Console.WriteLine("[VectorSearch] Schema initialized."); } @@ -188,6 +200,135 @@ public class VectorSearchService return new HashSet(ids); } + /// + /// Generiert und speichert ein Embedding für eine Memory-Datei (oder aktualisiert es). + /// + public async Task EmbedMemoryFileAsync(string agentId, string filePath, string text) + { + if (!_embedding.IsAvailable || string.IsNullOrWhiteSpace(text)) return; + if (text.Length < 10) return; + + // Chunks erstellen: Memory-Dateien können lang sein → in 500-Zeichen-Chunks aufteilen + var chunks = ChunkText(text, 500); + + using var conn = CreateConnection(); + + // Alte Embeddings für diese Datei löschen (bei Update) + await conn.ExecuteAsync( + "DELETE FROM memory_file_embeddings WHERE agent_id = @agentId AND file_path = @filePath", + new { agentId, filePath }); + + for (int i = 0; i < chunks.Count; i++) + { + float[] vector; + await _embeddingLock.WaitAsync(); + try { vector = _embedding.GenerateEmbedding(chunks[i]); } + finally { _embeddingLock.Release(); } + if (vector.Length == 0) continue; + + var preview = chunks[i].Length > 200 ? chunks[i].Substring(0, 200) : chunks[i]; + await conn.ExecuteAsync(@" + INSERT INTO memory_file_embeddings + (agent_id, file_path, chunk_index, text_preview, vector, updated_at) + VALUES + (@agentId, @filePath, @chunkIndex, @preview, @vector, @updatedAt)", + new + { + agentId, + filePath, + chunkIndex = i, + preview, + vector = EmbeddingService.ToBytes(vector), + updatedAt = DateTime.UtcNow.ToString("o") + }); + } + } + + /// + /// Sucht über Memory-Dateien eines Agents. + /// + public async Task> SearchMemoryFilesAsync(string query, string agentId, int topN = 5, float minScore = 0.4f) + { + if (!_embedding.IsAvailable || string.IsNullOrEmpty(agentId)) return new List(); + + float[] queryVec; + await _embeddingLock.WaitAsync(); + try { queryVec = _embedding.GenerateEmbedding(query); } + finally { _embeddingLock.Release(); } + if (queryVec.Length == 0) return new List(); + + using var conn = CreateConnection(); + var rows = await conn.QueryAsync( + "SELECT agent_id, file_path, chunk_index, text_preview, vector, updated_at FROM memory_file_embeddings WHERE agent_id = @agentId", + new { agentId }); + + var results = new List(); + foreach (var row in rows) + { + var vec = EmbeddingService.FromBytes(row.Vector); + var score = EmbeddingService.CosineSimilarity(queryVec, vec); + if (score >= minScore) + { + results.Add(new MemorySearchResult + { + AgentId = row.AgentId, + FilePath = row.FilePath, + ChunkIndex = row.ChunkIndex, + TextPreview = row.TextPreview, + Score = score, + UpdatedAt = row.UpdatedAt + }); + } + } + + return results.OrderByDescending(r => r.Score).Take(topN).ToList(); + } + + static List ChunkText(string text, int chunkSize) + { + var chunks = new List(); + if (string.IsNullOrEmpty(text)) return chunks; + + // Versuche an Absätzen zu trennen + var paragraphs = text.Split(new[] { "\n\n", "\r\n\r\n" }, StringSplitOptions.RemoveEmptyEntries); + var current = new StringBuilder(); + + foreach (var para in paragraphs) + { + if (current.Length + para.Length > chunkSize && current.Length > 0) + { + chunks.Add(current.ToString().Trim()); + current.Clear(); + } + if (current.Length > 0) current.AppendLine(); + current.Append(para); + } + if (current.Length > 0) + chunks.Add(current.ToString().Trim()); + + return chunks; + } + + public class MemoryFileRow + { + public string AgentId { get; set; } + public string FilePath { get; set; } + public int ChunkIndex { get; set; } + public string TextPreview { get; set; } + public byte[] Vector { get; set; } + public string UpdatedAt { get; set; } + } + + public class MemorySearchResult + { + public string AgentId { get; set; } + public string FilePath { get; set; } + public int ChunkIndex { get; set; } + public string TextPreview { get; set; } + public float Score { get; set; } + public string UpdatedAt { get; set; } + } + public class EmbeddingRow { public long MessageId { get; set; } diff --git a/TODO.md b/TODO.md index 67a6cff..b73a3f8 100644 --- a/TODO.md +++ b/TODO.md @@ -42,7 +42,7 @@ - [x] Anstelle von `[Bild: ...]`-Platzhalter direkt das gesendete Bild anzeigen ## Ideas / Backlog -- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search) +- [x] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search) - [x] ~~Claude (Sub-)Agents~~ – Nicht umsetzbar: Claude Code Sub-Agents sind ein CLI-Feature, nicht über die Messages API verfügbar. Eigenes Sub-Agent-System (Preparer/Review/Finalizer) deckt den Use-Case bereits ab. ## Features aus Devy übernehmen diff --git a/wwwroot/index.html b/wwwroot/index.html index d5b63b9..93861c4 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -227,6 +227,10 @@ Zugriff auf Erinnerungen (Memory-Tools verfügbar) +