fix(preparer): Logik invertiert - Default=Forward, [QUESTION]=Stopp

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
This commit is contained in:
Meleki committed 2026-03-07 16:35:29 +01:00
1 parent 43d3e9dc26
commit 14e721f95b
4 files changed
+65 -65

No files matched your search

+9 -16
View File
@@ -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<string, object>
{
["role"] = "user",
["content"] = new List<object>
{
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;
+25 -16
View File
@@ -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;
}
+29 -32
View File
@@ -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.
/// </summary>
public async Task<List<object>> BuildHistoryFromDb(string sessionId)
/// <summary>
/// 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).
/// </summary>
public async Task<List<object>> 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)
+2 -1
View File
@@ -200,9 +200,10 @@ function renderMarkdown(text) {
return '<div class="review-inline-feedback"><i class="fa fa-repeat"></i> <strong>Feedback von ' + esc(agentName.trim()) + '</strong> <span class="review-round">Runde ' + esc(round) + '/' + esc(maxRounds) + '</span></div>';
});
// 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) {