System-Prompt-Modal: strukturierte Blöcke via API statt Frontend-Parsing

- SystemPromptBuilder.BuildBlocks() liefert List<PromptBlock> mit Icon/Label/Content
- Neuer Endpoint GET /api/sessions/{id}/system-prompt-blocks (JSON-Array)
- Labels beschreiben präzise die Herkunft jedes Blocks (Agent, Session, System, Skills)
- Verhalten + Erinnerung nur noch mit Workspace + ToolChoice:any (Bugfix)
- Frontend: simples Block-Rendering statt komplexer Heuristik-Logik
- SP-Block Styling mit Label-Header und Content-Box
This commit is contained in:
Meleki committed 2026-03-01 21:31:38 +01:00
1 parent 40e56acc30
commit d42e5077c4
5 files changed
+148 -25

No files matched your search

+33
View File
@@ -21,6 +21,7 @@ public static class SessionEndpoint
app.MapPut("/api/sessions/{sessionId}", HandleUpdate); app.MapPut("/api/sessions/{sessionId}", HandleUpdate);
app.MapDelete("/api/sessions/{sessionId}", HandleDelete); app.MapDelete("/api/sessions/{sessionId}", HandleDelete);
app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt); app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt);
app.MapGet("/api/sessions/{sessionId}/system-prompt-blocks", HandleSystemPromptBlocks);
app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming); app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming);
app.MapPost("/api/sessions/{sessionId}/exec", HandleExec); app.MapPost("/api/sessions/{sessionId}/exec", HandleExec);
app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus); app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus);
@@ -132,6 +133,38 @@ public static class SessionEndpoint
await ctx.Response.WriteAsync(systemPrompt); await ctx.Response.WriteAsync(systemPrompt);
} }
private static async Task HandleSystemPromptBlocks(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
{
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
var session = await db.GetSession(sessionId);
if (session == null) { ctx.Response.StatusCode = 404; return; }
if (string.IsNullOrEmpty(session.AgentId))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
return;
}
var agent = await db.GetAgent(session.AgentId);
if (agent == null)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
return;
}
var markedMessages = await db.GetMarkedMessages(sessionId);
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
var dataDir = memoryService.DataDir;
var agentSkills = await db.GetAgentSkills(agent.AgentId);
var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, agentSkills, skillService);
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
}
private static async Task HandleStreaming(HttpContext ctx, StreamingTracker streamingTracker) private static async Task HandleStreaming(HttpContext ctx, StreamingTracker streamingTracker)
{ {
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
+59 -23
View File
@@ -4,35 +4,42 @@ using Gott.Models;
namespace Gott.Services; 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 class SystemPromptBuilder
{ {
public static string Build(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null) public static List<PromptBlock> BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null)
{ {
var parts = new List<string>(); var blocks = new List<PromptBlock>();
// IDENTITY.md - Wer bin ich (ersetzt "Du bist {Name}.") // IDENTITY.md
var hasIdentity = false; var hasIdentity = false;
if (agent.UseIdentity && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId)) if (agent.UseIdentity && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId))
{ {
var identity = memoryService.ReadMemoryFile(agent.AgentId, "IDENTITY.md"); var identity = memoryService.ReadMemoryFile(agent.AgentId, "IDENTITY.md");
if (!string.IsNullOrEmpty(identity)) if (!string.IsNullOrEmpty(identity))
{ {
parts.Add(identity); blocks.Add(new PromptBlock { Icon = "fa-id-badge", Label = "Agent · IDENTITY.md (UseIdentity)", Content = identity });
hasIdentity = true; hasIdentity = true;
} }
} }
if (!hasIdentity && agent.UseIdentity) if (!hasIdentity && agent.UseIdentity)
parts.Add($"Du bist {agent.Name}."); blocks.Add(new PromptBlock { Icon = "fa-id-badge", Label = "Agent · IDENTITY.md (UseIdentity, Fallback)", Content = $"Du bist {agent.Name}." });
// SOUL.md - Persönlichkeit + Regeln // SOUL.md
if (agent.UseSoul && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId)) if (agent.UseSoul && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId))
{ {
var soul = memoryService.ReadMemoryFile(agent.AgentId, "SOUL.md"); var soul = memoryService.ReadMemoryFile(agent.AgentId, "SOUL.md");
if (!string.IsNullOrEmpty(soul)) if (!string.IsNullOrEmpty(soul))
parts.Add(soul); blocks.Add(new PromptBlock { Icon = "fa-heart", Label = "Agent · SOUL.md (UseSoul)", Content = soul });
} }
// USER.md - Global (aus data-Ordner) // USER.md
if (agent.UseUserInfo && !string.IsNullOrWhiteSpace(dataDir)) if (agent.UseUserInfo && !string.IsNullOrWhiteSpace(dataDir))
{ {
var userFile = System.IO.Path.Combine(dataDir, "USER.md"); var userFile = System.IO.Path.Combine(dataDir, "USER.md");
@@ -42,7 +49,7 @@ public static class SystemPromptBuilder
if (!string.IsNullOrWhiteSpace(userContent)) if (!string.IsNullOrWhiteSpace(userContent))
{ {
var absolutePath = System.IO.Path.GetFullPath(userFile); var absolutePath = System.IO.Path.GetFullPath(userFile);
parts.Add($"{userContent}\n\n_Pfad: `{absolutePath}`_"); blocks.Add(new PromptBlock { Icon = "fa-user", Label = "Agent · USER.md (UseUserInfo)", Content = $"{userContent}\n\n_Pfad: `{absolutePath}`_" });
} }
} }
} }
@@ -53,21 +60,31 @@ public static class SystemPromptBuilder
{ {
if (toolAny) if (toolAny)
{ {
parts.Add(@$"## KRITISCH: Tool-Nutzung blocks.Add(new PromptBlock
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."); {
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."
});
} }
parts.Add(@$"## Tools 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. 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: `{session.Workspace}` Der Workspace-Pfad ist: `{session.Workspace}`
Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder absolute Pfade verwenden, die mit diesem Pfad beginnen. Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder absolute Pfade verwenden, die mit diesem Pfad beginnen.
Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: foo.txt:11]`), zähle Leerzeilen immer mit. Verwende `cat -n` oder ein ähnliches Tool um Zeilennummern exakt zu bestimmen - NIEMALS Leerzeilen beim Zählen überspringen."); Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: foo.txt:11]`), zähle Leerzeilen immer mit. Verwende `cat -n` oder ein ähnliches Tool um Zeilennummern exakt zu bestimmen - NIEMALS Leerzeilen beim Zählen überspringen."
});
} }
if (!string.IsNullOrWhiteSpace(session.Context)) if (!string.IsNullOrWhiteSpace(session.Context))
parts.Add($"## Kontext\n{session.Context}"); blocks.Add(new PromptBlock { Icon = "fa-info-circle", Label = "Session · Kontext", Content = $"## Kontext\n{session.Context}" });
// Skills // Skills
if (skillSlugs != null && skillService != null) if (skillSlugs != null && skillService != null)
@@ -87,13 +104,21 @@ Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: fo
if (skillParts.Count > 0) 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 skillSection = "## 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 = "## 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"
+ string.Join("\n\n---\n\n", skillParts); + string.Join("\n\n---\n\n", skillParts);
parts.Add(skillSection); blocks.Add(new PromptBlock { Icon = "fa-puzzle-piece", Label = skillLabel, Content = skillSection });
} }
} }
// Memory-System // Memory
if (agent.UseMemory && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId)) if (agent.UseMemory && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId))
{ {
var memoryLines = new List<string> var memoryLines = new List<string>
@@ -119,10 +144,10 @@ Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: fo
memoryLines.Add(mainMemory); memoryLines.Add(mainMemory);
} }
parts.Add(string.Join("\n", memoryLines)); blocks.Add(new PromptBlock { Icon = "fa-database", Label = "Agent · Memory (UseMemory)", Content = string.Join("\n", memoryLines) });
} }
if (toolAny) if (toolAny && !string.IsNullOrWhiteSpace(session.Workspace))
{ {
var verhaltensRegeln = new List<string> var verhaltensRegeln = new List<string>
{ {
@@ -142,10 +167,15 @@ Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: fo
verhaltensRegeln.Add(@"- 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."); verhaltensRegeln.Add(@"- 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.");
} }
parts.Add(string.Join("\n", verhaltensRegeln)); blocks.Add(new PromptBlock { Icon = "fa-cog", Label = "Agent · Verhalten (Workspace + ToolChoice: any" + (agent.AutoCommit ? ", AutoCommit" : "") + ")", Content = string.Join("\n", verhaltensRegeln) });
parts.Add(@"## ERINNERUNG blocks.Add(new PromptBlock
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."); {
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."
});
} }
var marked = markedMessages?.Where(m => !string.IsNullOrWhiteSpace(m.Content)).ToList(); var marked = markedMessages?.Where(m => !string.IsNullOrWhiteSpace(m.Content)).ToList();
@@ -157,9 +187,15 @@ Diese Regel hat höchste Priorität und gilt für JEDE Antwort: Wenn eine Datei
var role = m.Role == "user" ? "User" : "Assistant"; var role = m.Role == "user" ? "User" : "Assistant";
lines.Add($"- [{role}]: {m.Content.Trim()}"); lines.Add($"- [{role}]: {m.Content.Trim()}");
} }
parts.Add(string.Join("\n", lines)); blocks.Add(new PromptBlock { Icon = "fa-star", Label = "Markierte Nachrichten", Content = string.Join("\n", lines) });
} }
return string.Join("\n\n", parts); return blocks;
}
public static string Build(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null)
{
var blocks = BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, skillSlugs, skillService);
return string.Join("\n\n", blocks.Select(b => b.Content));
} }
} }
+25
View File
@@ -548,6 +548,31 @@
padding: var(--page-padding); padding: var(--page-padding);
overflow-y: auto; overflow-y: auto;
flex: 1; flex: 1;
display: flex;
flex-direction: column;
gap: var(--box-spacing);
}
.sp-block {
border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
}
.sp-block-label {
display: flex;
align-items: center;
gap: var(--badge-spacing);
padding: var(--box-padding);
background: rgb(from var(--f1-bg) r g b / var(--semi-transparent));
border-bottom: var(--border-width) solid var(--f1-border);
font-size: var(--font-size);
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
.sp-block-content {
background: rgb(from var(--f3-bg) r g b / var(--semi-transparent));
padding: var(--box-padding);
border-radius: 0 0 var(--border-radius) var(--border-radius);
} }
.sp-loading { .sp-loading {
+6
View File
@@ -122,6 +122,12 @@ async function apiGetSystemPrompt(sessionId) {
return resp.text(); return resp.text();
} }
async function apiGetSystemPromptBlocks(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/system-prompt-blocks`);
if (!resp.ok) throw new Error('System-Prompt konnte nicht geladen werden');
return resp.json();
}
async function apiIsStreaming(sessionId) { async function apiIsStreaming(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/streaming`); const resp = await fetch(`/api/sessions/${sessionId}/streaming`);
return resp.json(); return resp.json();
+25 -2
View File
@@ -607,6 +607,28 @@ async function loadAndShowAttachments(messageId, indicator) {
// ── System-Prompt Modal ── // ── System-Prompt Modal ──
function _renderSystemPromptBlocks(container, blocks) {
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (!block.content) continue;
var wrapper = document.createElement('div');
wrapper.className = 'sp-block';
var label = document.createElement('div');
label.className = 'sp-block-label';
label.innerHTML = '<i class="fa ' + esc(block.icon || 'fa-cog') + '"></i> ' + esc(block.label || 'Block');
wrapper.appendChild(label);
var content = document.createElement('div');
content.className = 'sp-block-content markdown';
content.innerHTML = renderMarkdown(block.content);
wrapper.appendChild(content);
container.appendChild(wrapper);
}
}
async function showSystemPromptModal(sessionId) { async function showSystemPromptModal(sessionId) {
// Overlay // Overlay
const overlay = document.createElement('div'); const overlay = document.createElement('div');
@@ -650,8 +672,9 @@ async function showSystemPromptModal(sessionId) {
// Laden // Laden
try { try {
const prompt = await apiGetSystemPrompt(sessionId); const blocks = await apiGetSystemPromptBlocks(sessionId);
body.innerHTML = renderMarkdown(prompt); body.innerHTML = '';
_renderSystemPromptBlocks(body, blocks);
} catch (e) { } catch (e) {
body.innerHTML = '<div class="sp-loading" style="color:var(--dangerous)">Fehler beim Laden des System-Prompts</div>'; body.innerHTML = '<div class="sp-loading" style="color:var(--dangerous)">Fehler beim Laden des System-Prompts</div>';
} }