- AgentRecord: TaskInstruction (string, nullable) - DB-Migration: task_instruction TEXT Spalte - AgentEndpoint: Create/Update lesen taskInstruction - SystemPromptBuilder: '## Anweisungen' Block nach Soul, nur wenn nicht leer - HTML: Textarea #agentTaskInstruction nach Rückfragen-Checkbox - agent-page.js: EasyMDE-Editor mit Init, Laden, Speichern, Destroy
273 lines
14 KiB
C#
273 lines
14 KiB
C#
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}" });
|
||
}
|
||
|
||
bool toolAny = string.Equals(agent.ToolChoice, "any", System.StringComparison.OrdinalIgnoreCase);
|
||
|
||
var effectiveWorkspace = resolvedWorkspace ?? session.Workspace;
|
||
if (!string.IsNullOrWhiteSpace(effectiveWorkspace))
|
||
{
|
||
if (toolAny && !planMode)
|
||
{
|
||
blocks.Add(new PromptBlock
|
||
{
|
||
Icon = "fa-exclamation-triangle",
|
||
Label = "System · Workspace + ToolChoice: any",
|
||
Content = @$"## KRITISCH: Tool-Nutzung
|
||
JEDE Datei-Operation und JEDER Shell-Befehl MUSS über einen Tool-Aufruf erfolgen. NIEMALS eine Aktion nur beschreiben oder behaupten - IMMER das Tool tatsächlich aufrufen."
|
||
});
|
||
}
|
||
|
||
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 dir bei einer Aufgabe unsicher bist oder es mehrere sinnvolle Wege gibt, nutze das `ask_user_choice`-Tool um dem User eine Auswahlliste zu präsentieren. Nutze es, wenn eine Entscheidung vom User getroffen werden soll."
|
||
});
|
||
}
|
||
|
||
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."
|
||
};
|
||
|
||
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
|
||
}
|
||
|
||
if (toolAny && !string.IsNullOrWhiteSpace(effectiveWorkspace) && !planMode)
|
||
{
|
||
blocks.Add(new PromptBlock
|
||
{
|
||
Icon = "fa-cog",
|
||
Label = "Agent · Verhalten (Workspace + ToolChoice: any)",
|
||
Content = string.Join("\n", new[]
|
||
{
|
||
"## Verhalten",
|
||
"- Wenn du Dateien lesen, schreiben, bearbeiten, erstellen oder löschen willst: IMMER das entsprechende Tool aufrufen. NIEMALS den Inhalt nur in die Antwort schreiben.",
|
||
"- Wenn du Shell-Befehle ausführen willst: IMMER das execute-Tool verwenden.",
|
||
"- Behaupte NIEMALS etwas getan zu haben (Datei geschrieben, Commit gemacht, Befehl ausgeführt) ohne das entsprechende Tool tatsächlich aufgerufen zu haben. Wenn kein Tool-Aufruf stattgefunden hat, ist nichts passiert.",
|
||
"- Plane nicht nur - handle sofort über Tool-Aufrufe."
|
||
})
|
||
});
|
||
|
||
blocks.Add(new PromptBlock
|
||
{
|
||
Icon = "fa-exclamation-circle",
|
||
Label = "System · Erinnerung (Workspace + ToolChoice: any)",
|
||
Content = @"## ERINNERUNG
|
||
Diese Regel hat höchste Priorität und gilt für JEDE Antwort: Wenn eine Datei gelesen, geschrieben, bearbeitet, erstellt oder gelöscht werden soll, oder ein Befehl ausgeführt werden soll, MUSS der entsprechende Tool-Aufruf erfolgen. Ohne Tool-Aufruf ist NICHTS passiert. Beschreibe keine Änderungen - führe sie durch."
|
||
});
|
||
}
|
||
// kein extra Erinnerungsblock ohne toolAny – Zeilenreferenz-Hinweise kommen direkt über die Nachricht
|
||
|
||
// 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));
|
||
}
|
||
}
|