From 14e721f95b84e7432c7dc5bda81f5ede77cb8995 Mon Sep 17 00:00:00 2001 From: Meleki Date: Sat, 7 Mar 2026 16:35:29 +0100 Subject: [PATCH] fix(preparer): Logik invertiert - Default=Forward, [QUESTION]=Stopp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kernproblem: Claude vergisst [FORWARD]-Tag → Preparer leitet nie weiter. Lösung - Logik umgedreht: - Jede Antwort wird automatisch an Session-Agent weitergeleitet (Default) - Nur bei explizitem [QUESTION]-Tag wird gestoppt (Rückfrage an User) - [FORWARD] wird weiterhin erkannt und entfernt, ist aber optional Weitere Fixes: - Preparer bekommt eigene History mit includeTag='preparer', damit er den Kontext seiner vorherigen Rückfragen sieht - Session-Agent bekommt saubere History ohne Sub-Agent-Messages (nach Preparer-Phase frisch aus DB geladen) - [QUESTION] wird im Frontend-Markdown ausgeblendet --- Endpoints/ChatEndpoint.cs | 25 ++++++--------- Services/AnthropicService.cs | 41 ++++++++++++++---------- Services/ChatService.cs | 61 +++++++++++++++++------------------- wwwroot/js/ui.js | 3 +- 4 files changed, 65 insertions(+), 65 deletions(-) diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 9be98d7..6d853da 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -128,9 +128,6 @@ public static class ChatEndpoint }, attachmentRecords); } - // History komplett aus DB aufbauen (inkl. gerade gespeicherter Message/tool_result) - var messages = await chatService.BuildHistoryFromDb(sessionId); - res.Headers["Content-Type"] = "text/event-stream"; res.Headers["Cache-Control"] = "no-cache"; res.Headers["Connection"] = "keep-alive"; @@ -138,8 +135,12 @@ public static class ChatEndpoint // ── Preparer-Agent Phase (vor dem Worker) ── if (ctx.Preparer != null && !isChoiceResponse) { + // Preparer bekommt History MIT seinen eigenen vorherigen Messages (Tag "preparer"), + // damit er den Kontext seiner Rückfragen kennt + var preparerHistory = await chatService.BuildHistoryFromDb(sessionId, includeTag: "preparer"); + var preparerResult = await anthropic.RunPreparer( - res, ctx.Preparer, messages, ctx.ChainId, ctx.Workspace, sessionId, cancellationToken); + res, ctx.Preparer, preparerHistory, ctx.ChainId, ctx.Workspace, sessionId, cancellationToken); if (!preparerResult.ShouldForward) { @@ -154,20 +155,9 @@ public static class ChatEndpoint return; } - // Preparer ist zufrieden → aufbereiteten Prompt als zusätzlichen Kontext an die History anhängen + // Preparer ist zufrieden → aufbereiteten Prompt in DB speichern if (!string.IsNullOrWhiteSpace(preparerResult.ForwardedPrompt)) { - // Den aufbereiteten Prompt als System-Context-Message einfügen - messages.Add(new Dictionary - { - ["role"] = "user", - ["content"] = new List - { - new { type = "text", text = $"[Vorbereitet von {ctx.Preparer.Agent?.Name ?? "Vorbereiter-Agent"}]:\n{preparerResult.ForwardedPrompt}" } - } - }); - - // Auch in DB speichern await db.SaveMessage(new MessageRecord { ChainId = ctx.ChainId, @@ -179,6 +169,9 @@ public static class ChatEndpoint } } + // History für den Session-Agent: OHNE Sub-Agent-Messages (saubere History) + var messages = await chatService.BuildHistoryFromDb(sessionId); + // Bei Choice-Response: toolChoice auf "auto" setzen, damit Claude frei antworten kann var effectiveToolChoice = isChoiceResponse ? "auto" : ctx.ToolChoice; diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index b52ccd8..f63b1a5 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -359,11 +359,32 @@ public class AnthropicService var result = new PreparerResult { IterationCount = iterations, ResponseText = text }; - // Prüfe ob [FORWARD] enthalten ist - if (!string.IsNullOrEmpty(text) && text.Contains("[FORWARD]")) + // Logik: Standardmäßig wird IMMER weitergeleitet. + // Nur wenn der Preparer explizit [QUESTION] nutzt, wird gestoppt (= Rückfrage an User). + // [FORWARD] wird weiterhin erkannt und entfernt, ist aber nicht mehr zwingend nötig. + if (!string.IsNullOrEmpty(text) && text.Contains("[QUESTION]")) { - var idx = text.IndexOf("[FORWARD]"); - var forwardedText = text.Substring(idx + "[FORWARD]".Length).Trim(); + // Explizite Rückfrage an User + var questionText = text.Replace("[QUESTION]", "").Trim(); + result.ShouldForward = false; + result.ResponseText = questionText; + + await TryWriteSse(response, "preparer_done", new + { + agentName = preparer.Agent?.Name ?? "Vorbereiter-Agent", + forwarded = false, + responseText = questionText + }, cancellationToken); + } + else + { + // Weiterleitung (Standard): [FORWARD]-Tag entfernen falls vorhanden + var forwardedText = text ?? ""; + if (forwardedText.Contains("[FORWARD]")) + { + var idx = forwardedText.IndexOf("[FORWARD]"); + forwardedText = forwardedText.Substring(idx + "[FORWARD]".Length).Trim(); + } result.ShouldForward = true; result.ForwardedPrompt = forwardedText; @@ -374,18 +395,6 @@ public class AnthropicService forwarded = true }, cancellationToken); } - else - { - // Rückfrage an User – Text streamen - result.ShouldForward = false; - - await TryWriteSse(response, "preparer_done", new - { - agentName = preparer.Agent?.Name ?? "Vorbereiter-Agent", - forwarded = false, - responseText = text - }, cancellationToken); - } return result; } diff --git a/Services/ChatService.cs b/Services/ChatService.cs index ca28ddb..5b51cf8 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -128,44 +128,36 @@ public class ChatService if (!string.IsNullOrWhiteSpace(preparerSystemText)) preparerPrompt.Add(new { type = "text", text = preparerSystemText }); - // Preparer-Instruktion: erklärt [FORWARD]-Mechanismus - preparerPrompt.Add(new { type = "text", text = @"## Deine Rolle als Vorbereiter-Agent -Du bist ein Vorbereiter-Agent. Deine Aufgabe ist es, die Anfrage des Users zu analysieren, vorzubereiten und – wenn möglich – Teile der Aufgabe bereits selbst zu erledigen, BEVOR das Ergebnis an den eigentlichen Session-Agent weitergegeben wird. + // Preparer-Instruktion: erklärt Vorbereiter-Mechanismus + preparerPrompt.Add(new { type = "text", text = @"## KRITISCH: Deine Rolle als Vorbereiter-Agent + +Du bist ein Vorbereiter-Agent. Deine Aufgabe: Die Anfrage des Users aufbereiten, deine Fähigkeiten nutzen, und das Ergebnis an den Session-Agent weiterleiten. + +### Standardverhalten: Automatische Weiterleitung + +Deine Antwort wird AUTOMATISCH an den Session-Agent weitergeleitet. Du musst nichts Besonderes tun – antworte einfach mit dem aufbereiteten Ergebnis. Optional kannst du `[FORWARD]` an den Anfang schreiben, aber es ist nicht nötig. + +### Ausnahme: Rückfrage an den User + +NUR wenn du eine echte Rückfrage hast (Anfrage ist unklar/mehrdeutig), schreibe `[QUESTION]` an den Anfang deiner Antwort. Dann wird deine Antwort dem User als Rückfrage angezeigt und NICHT an den Session-Agent weitergeleitet. ### Nutze deine Fähigkeiten aktiv! -Du hast eigene Fähigkeiten und Tools. Nutze sie! Wenn du Teile der Aufgabe selbst erledigen kannst (z.B. Dateien lesen, Daten aufbereiten, Strukturen erstellen, Recherchen durchführen), dann tu das BEVOR du weiterleitest. Der Session-Agent soll von dir ein möglichst gut vorbereitetes Paket bekommen. +Du hast eigene Tools. Nutze sie! Erledige was du kannst (Dateien lesen, Daten aufbereiten, Strukturen erstellen, recherchieren). Der Session-Agent soll ein möglichst gut vorbereitetes Paket bekommen. -### Dein Ablauf: -1. **Analysiere** die User-Anfrage -2. **Stelle Rückfragen**, wenn die Anfrage unklar, unvollständig oder mehrdeutig ist (antworte einfach mit deiner Frage als normaler Text) -3. **Erledige** was du kannst – nutze deine Fähigkeiten und Tools, um Vorarbeit zu leisten -4. **Leite weiter** mit dem Ergebnis deiner Arbeit +### Beispiele: -### Weiterleitung: -Wenn du fertig bist, schreibe `[FORWARD]` gefolgt von dem aufbereiteten Ergebnis/Prompt, der an den Session-Agent weitergegeben werden soll. Alles nach `[FORWARD]` wird als Prompt an den Session-Agent übergeben. - -Beispiel – Anfrage nur verfeinert: +Weiterleitung (wird automatisch an den Session-Agent übergeben): ``` -[FORWARD] -Der User möchte eine REST-API mit folgenden Endpunkten erstellen: -- GET /users – Liste aller User -- POST /users – Neuen User anlegen -Technologie: .NET 10, SQLite +Der User möchte Produktdaten als JSON. Ich habe es bereits erstellt: +{""products"": [{""id"": 1, ""name"": ""Widget""}]} +Bitte speichere es in products.json. ``` -Beispiel – Vorarbeit geleistet: +Rückfrage (NUR mit [QUESTION] – stoppt die Weiterleitung): ``` -[FORWARD] -Der User möchte Produktdaten als JSON. Ich habe das JSON-Objekt bereits erstellt: -{""products"": [{""id"": 1, ""name"": ""Widget""}, {""id"": 2, ""name"": ""Gadget""}]} -Bitte speichere es in die Datei products.json im Workspace. -``` - -### Wichtige Regeln: -- Antworte OHNE `[FORWARD]` wenn du Rückfragen hast → der User sieht deine Antwort und kann darauf reagieren -- Antworte MIT `[FORWARD]` wenn du fertig bist → der Text nach [FORWARD] wird an den Session-Agent weitergegeben -- Nutze deine Tools und Fähigkeiten aktiv, um dem Session-Agent möglichst viel Arbeit abzunehmen -- Sei effizient: Wenn die Anfrage offensichtlich klar ist und du nichts vorbereiten kannst, leite sofort weiter" }); +[QUESTION] +Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? +```" }); preparerContext = new PreparerContext { @@ -299,7 +291,12 @@ Bitte speichere es in die Datei products.json im Workspace. /// Optimiert: Nur Text-Messages (user/assistant) für ältere Chains, /// Tool-Calls nur für die aktuelle Chain, Attachments nur für die letzte User-Message. /// - public async Task> BuildHistoryFromDb(string sessionId) + /// + /// Baut die Message-History aus der DB (aktive Chains). + /// includeTag: Wenn gesetzt, werden Messages mit diesem Tag NICHT ausgefiltert + /// (z.B. "preparer" damit der Preparer seine eigenen vorherigen Messages sieht). + /// + public async Task> BuildHistoryFromDb(string sessionId, string includeTag = null) { var chainsList = (await _db.GetActiveChains(sessionId)).ToList(); // chronologisch sortieren (älteste zuerst) @@ -330,7 +327,7 @@ Bitte speichere es in die Datei products.json im Workspace. isLastChain = (i == chainsList.Count - 1); var msgs = (await _db.GetChainMessages(chain.ChainId)) - .Where(m => string.IsNullOrEmpty(m.Tag)) // Sub-Agent-Messages (preparer/review/finalizer) aus API-History ausfiltern + .Where(m => string.IsNullOrEmpty(m.Tag) || m.Tag == includeTag) // Sub-Agent-Messages ausfiltern, außer dem angeforderten Tag .ToList(); // Letzte User-Message-ID der letzten Chain merken (für Attachments) diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 4f258f9..2599ebc 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -200,9 +200,10 @@ function renderMarkdown(text) { return '
Feedback von ' + esc(agentName.trim()) + ' Runde ' + esc(round) + '/' + esc(maxRounds) + '
'; }); - // 4c) [APPROVED] und [FORWARD] ausblenden + // 4c) [APPROVED], [FORWARD] und [QUESTION] ausblenden html = html.replace(/\[APPROVED\]/g, ''); html = html.replace(/\[FORWARD\]\s*/g, ''); + html = html.replace(/\[QUESTION\]\s*/g, ''); // 4d) [Krea: dateiname.png] → Inline-Thumbnail mit Klick für Vollbild html = html.replace(/\[Krea:\s*([^\]]+)\]/g, function(match, filename) {