Files
gott/Services/SystemPromptBuilder.cs
MelekiandClaude Opus 4.6 92ecaf9dac fix: ask_user_choice Short-Poll, Freitext-Option, Stop-Kontext-Fix
- Long-Poll durch Short-Poll ersetzt (MCP-Server pollt jede Sekunde statt einer blockierenden HTTP-Anfrage)
- Expliziter Lambda-Rückgabetyp Task<IResult> für ASP.NET Minimal API Endpoints (behebt 200 empty body Bug)
- Freitext-Option bei Choice-Buttons ("Eigene Antwort…") für freie User-Eingabe
- Stop während ask_user_choice markiert Chain als awaiting_user_choice statt stopped (Kontext bleibt erhalten)
- ask_user_choice System-Prompt-Instruktion breiter formuliert (Quiz, Multiple-Choice etc.)
- Text wird jetzt immer in DB gespeichert, auch bei ask_user_choice
- Key-Check Model auf claude-haiku-4-5 geändert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 11:32:25 +01:00

276 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Linq;
using Gott.Models;
namespace Gott.Services;
public class PromptBlock
{
public string Icon { get; set; }
public string Label { get; set; }
public string Content { get; set; }
}
public static class SystemPromptBuilder
{
public static List<PromptBlock> BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null, string userPersona = null, string resolvedWorkspace = null, bool planMode = false)
{
var blocks = new List<PromptBlock>();
// IDENTITY.md
var hasIdentity = false;
if (agent.UseIdentity && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId))
{
var identity = memoryService.ReadMemoryFile(agent.AgentId, "IDENTITY.md");
if (!string.IsNullOrEmpty(identity))
{
blocks.Add(new PromptBlock { Icon = "fa-id-badge", Label = "Agent · IDENTITY.md (UseIdentity)", Content = identity });
hasIdentity = true;
}
}
if (!hasIdentity && agent.UseIdentity)
blocks.Add(new PromptBlock { Icon = "fa-id-badge", Label = "Agent · IDENTITY.md (UseIdentity, Fallback)", Content = $"Du bist {agent.Name}." });
// SOUL.md
if (agent.UseSoul && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId))
{
var soul = memoryService.ReadMemoryFile(agent.AgentId, "SOUL.md");
if (!string.IsNullOrEmpty(soul))
blocks.Add(new PromptBlock { Icon = "fa-heart", Label = "Agent · SOUL.md (UseSoul)", Content = soul });
}
// Anweisungen
if (!string.IsNullOrWhiteSpace(agent.TaskInstruction))
{
blocks.Add(new PromptBlock { Icon = "fa-clipboard", Label = "Agent · Anweisungen", Content = $"## Anweisungen\n{agent.TaskInstruction.Trim()}" });
}
// User-Info: Persona aus dem Profil des Users der gerade schreibt
if (agent.UseUserInfo && !string.IsNullOrWhiteSpace(userPersona))
{
blocks.Add(new PromptBlock { Icon = "fa-user", Label = "User · Persona (UseUserInfo)", Content = $"## Dein Gegenüber\nDie folgende Beschreibung stammt von der Person, die dir gerade schreibt:\n\n{userPersona}" });
}
var effectiveWorkspace = resolvedWorkspace ?? session.Workspace;
if (!string.IsNullOrWhiteSpace(effectiveWorkspace))
{
if (planMode)
{
blocks.Add(new PromptBlock
{
Icon = "fa-wrench",
Label = "Session · Workspace (Plan-Modus)",
Content = @$"## Tools
Du hast Read-only-Tools zur Verfügung (read_file, list_files, glob, grep, execute). Benutze sie um den Code zu analysieren und zu verstehen.
Der Workspace-Pfad ist: `{effectiveWorkspace}`
Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder absolute Pfade verwenden, die mit diesem Pfad beginnen."
});
}
else
{
blocks.Add(new PromptBlock
{
Icon = "fa-wrench",
Label = "Session · Workspace",
Content = @$"## Tools
Du hast Tools zur Verfügung (read_file, write_file, edit_file, list_files, delete_file, execute). Benutze sie um Dateien zu lesen, schreiben und bearbeiten.
Der Workspace-Pfad ist: `{effectiveWorkspace}`
Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder absolute Pfade verwenden, die mit diesem Pfad beginnen."
});
}
}
// Rückfragen-Hinweis
if (agent.AllowUserChoice)
{
blocks.Add(new PromptBlock
{
Icon = "fa-question-circle",
Label = "Agent · Rückfragen (AllowUserChoice)",
Content = "## Rückfragen\nWenn du dem User Optionen, Auswahlmöglichkeiten oder Fragen mit mehreren Antwortmöglichkeiten präsentierst, nutze IMMER das `ask_user_choice`-Tool (bzw. `mcp__gott-remote__ask_user_choice`). Das gilt für:\n- Rückfragen bei Unsicherheit oder mehreren sinnvollen Wegen\n- Quiz-Fragen, Multiple-Choice-Fragen\n- Jede Situation in der der User zwischen Optionen wählen soll\n\nNutze NIEMALS A/B/C/D-Aufzählungen oder nummerierte Listen als Ersatz – IMMER das Tool verwenden."
});
}
if (!string.IsNullOrWhiteSpace(session.Context))
blocks.Add(new PromptBlock { Icon = "fa-info-circle", Label = "Session · Kontext", Content = $"## Kontext\n{session.Context}" });
// Skills
if (skillSlugs != null && skillService != null)
{
var skillParts = new List<string>();
foreach (var slug in skillSlugs)
{
var skillPrompt = skillService.GetSkillPrompt(slug);
if (string.IsNullOrEmpty(skillPrompt)) continue;
var skill = skillService.GetSkill(slug);
var header = skill != null ? $"### {skill.Name}" : $"### {slug}";
if (skill?.Version != null) header += $" (v{skill.Version})";
header += $"\nPfad: `{skill?.Path ?? slug}`";
skillParts.Add($"{header}\n\n{skillPrompt}");
}
if (skillParts.Count > 0)
{
var skillNames = new List<string>();
foreach (var slug in skillSlugs)
{
var skill = skillService.GetSkill(slug);
if (skill != null && skillService.GetSkillPrompt(slug) != null)
skillNames.Add(skill.Name ?? slug);
}
var skillLabel = "Agent · Skills: " + string.Join(", ", skillNames);
var skillIntro = planMode
? "## Skills\nFolgende Skills sind verfügbar (im Plan-Modus nur zur Kenntnis – nicht ausführbar):\n\n"
: "## Skills\nDu hast folgende Skills zur Verfügung. Nutze sie über das `execute`-Tool wenn der User etwas anfragt, das ein Skill abdeckt.\n\n";
var skillSection = skillIntro
+ string.Join("\n\n---\n\n", skillParts);
blocks.Add(new PromptBlock { Icon = "fa-puzzle-piece", Label = skillLabel, Content = skillSection });
}
}
// Memory
if (agent.UseMemory && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId))
{
var memoryLines = new List<string>
{
"## Memory",
"Du hast ein persistentes Gedächtnis über Memory-Tools (`memory_read`, `memory_write`, `memory_list`).",
$"Dein Memory-Verzeichnis: `{agentMemoryDir ?? $"data/agents/{agent.AgentId}/"}`",
"",
"### Regeln",
"- Nutze `memory_read` um gespeicherte Erinnerungen abzurufen, bevor du über vergangene Entscheidungen, Präferenzen oder Fakten sprichst.",
"- Nutze `memory_write` um wichtige Informationen dauerhaft zu speichern – z.B. Entscheidungen, Präferenzen des Users, Projekt-Kontext.",
"- `MEMORY.md` ist dein Haupt-Memory-File für kuratierte, dauerhafte Informationen.",
"- Nutze Unterordner `memory/` für Tages-Logs oder thematische Notizen. Bei Tages-Logs IMMER zuerst `date '+%Y-%m-%d'` ausführen und das tatsächliche Datum verwenden – NIEMALS ein Datum raten oder aus Beispielen übernehmen.",
"- Speichere proaktiv wenn der User etwas Wichtiges sagt oder eine Entscheidung getroffen wird.",
"- Wenn der User sagt \"merke dir das\" oder ähnliches, speichere es sofort.",
"",
"### Wichtig: Was NICHT ins Memory gehört",
"- Dein Memory ist **agent-übergreifend** und wird in ALLEN Sessions geteilt, in denen du eingesetzt wirst.",
"- Speichere KEINE session-spezifischen Informationen (z.B. aktuelle Aufgabe, Kontext der laufenden Konversation, temporäre Zustände).",
"- Kopiere NIEMALS Inhalte aus dem System-Prompt oder Session-Kontext ins Memory – diese Informationen sind bereits im Prompt enthalten und müssen nicht dupliziert werden.",
"- Speichere nur Informationen, die **über die aktuelle Session hinaus** relevant sind: langfristige User-Präferenzen, wiederkehrende Projekt-Konventionen, explizite Anweisungen des Users."
};
var mainMemory = memoryService.GetMainMemory(agent.AgentId);
if (!string.IsNullOrEmpty(mainMemory))
{
memoryLines.Add("");
memoryLines.Add("### Aktueller Inhalt von MEMORY.md");
memoryLines.Add(mainMemory);
}
blocks.Add(new PromptBlock { Icon = "fa-database", Label = "Agent · Memory (UseMemory)", Content = string.Join("\n", memoryLines) });
}
// Git-Commit-Verhalten: relevant sobald Workspace vorhanden (nicht im Plan-Modus)
if (!string.IsNullOrWhiteSpace(effectiveWorkspace) && !planMode)
{
var autoCommit = (agent.AutoCommit ?? "").ToLowerInvariant();
if (autoCommit == "mention")
{
blocks.Add(new PromptBlock
{
Icon = "fa-code-branch",
Label = "Agent · Git-Verhalten (Workspace, Anmerken)",
Content = "## Git-Commits\n- Git-Commits NIEMALS eigenständig erstellen. Commits werden NUR durchgeführt wenn der User es EXPLIZIT anfordert (z.B. \"erstelle einen Commit\", \"commit\"). Auch nach abgeschlossenen Aufgaben NICHT automatisch committen."
});
}
else if (autoCommit == "enforce")
{
blocks.Add(new PromptBlock
{
Icon = "fa-code-branch",
Label = "Agent · Git-Verhalten (Workspace, Erzwungen)",
Content = "## Git-Commits\n- Nach JEDER abgeschlossenen Änderung MUSS ein Git-Commit erstellt werden. Erstelle aussagekräftige Commit-Messages die beschreiben was geändert wurde."
});
}
else if (autoCommit == "forbid")
{
blocks.Add(new PromptBlock
{
Icon = "fa-code-branch",
Label = "Agent · Git-Verhalten (Workspace, Verboten)",
Content = "## Git-Commits\n- Git-Commits sind STRIKT VERBOTEN. Du darfst unter KEINEN Umständen eigenständig Git-Commits erstellen, auch nicht wenn der User danach fragt. Verweise stattdessen darauf, dass der User den Commit selbst durchführen soll."
});
}
// autoCommit == "" → kein Block, Thema wird nicht erwähnt
}
// Plan-Modus-Block
if (planMode)
{
blocks.Add(new PromptBlock
{
Icon = "fa-map",
Label = "Plan-Modus aktiv",
Content = "## Plan-Modus\nDer User hat den Plan-Modus aktiviert. Du DARFST KEINE Änderungen vornehmen:\n" +
"- KEINE Dateien schreiben, editieren oder löschen\n" +
"- KEINE Shell-Befehle ausführen (außer read-only wie cat, ls, find, grep)\n" +
"- KEINE Commits erstellen\n\n" +
"Stattdessen: Analysiere, stelle Rückfragen und erstelle einen detaillierten Plan.\n" +
"Der User wird den Plan-Modus deaktivieren wenn du mit der Umsetzung beginnen sollst."
});
}
var marked = markedMessages?.Where(m => !string.IsNullOrWhiteSpace(m.Content)).ToList();
if (marked != null && marked.Count > 0)
{
var lines = new List<string> { "## Markierte Nachrichten\nDer User hat folgende Nachrichten als wichtig markiert:" };
foreach (var m in marked)
{
var role = m.Role == "user" ? "User" : "Assistant";
lines.Add($"- [{role}]: {m.Content.Trim()}");
}
blocks.Add(new PromptBlock { Icon = "fa-star", Label = "Markierte Nachrichten", Content = string.Join("\n", lines) });
}
return blocks;
}
public static string Build(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null, string userPersona = null, string resolvedWorkspace = null, bool planMode = false)
{
var blocks = BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, skillSlugs, skillService, userPersona, resolvedWorkspace, planMode);
return string.Join("\n\n", blocks.Select(b => b.Content));
}
/// <summary>
/// Generiert automatisch einen kompakten Reminder-Text basierend auf den aktiven Prompt-Blöcken.
/// Gibt null zurück wenn weniger als 2 Blöcke aktiv sind (kein Reminder nötig).
/// </summary>
public static string BuildReminder(List<PromptBlock> blocks)
{
if (blocks == null || blocks.Count < 2)
return null;
var parts = new List<string>();
foreach (var block in blocks)
{
var label = block.Label;
if (label.Contains("IDENTITY")) parts.Add("Identity");
else if (label.Contains("SOUL")) parts.Add("Soul");
else if (label.Contains("Anweisungen")) parts.Add("Anweisungen");
else if (label.Contains("Persona")) parts.Add("User-Info");
else if (label.Contains("Workspace")) parts.Add("Workspace");
else if (label.Contains("Rückfragen")) parts.Add("Rückfragen");
else if (label.Contains("Kontext")) parts.Add("Kontext");
else if (label.Contains("Skills")) parts.Add("Skills");
else if (label.Contains("Memory")) parts.Add("Memory");
else if (label.Contains("Git")) parts.Add("Git-Regeln");
else if (label.Contains("Plan-Modus")) parts.Add("Plan-Modus");
else if (label.Contains("Markierte")) parts.Add("Markierte Nachrichten");
}
if (parts.Count == 0)
return null;
return "Beachte weiterhin den System-Prompt (" + string.Join(", ", parts) + ").";
}
}