From d42e5077c4aa4b4164999eefe4e70c00e137d61f Mon Sep 17 00:00:00 2001 From: Meleki Date: Sun, 1 Mar 2026 21:31:38 +0100 Subject: [PATCH] =?UTF-8?q?System-Prompt-Modal:=20strukturierte=20Bl=C3=B6?= =?UTF-8?q?cke=20via=20API=20statt=20Frontend-Parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SystemPromptBuilder.BuildBlocks() liefert List 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 --- Endpoints/SessionEndpoint.cs | 33 +++++++++++++ Services/SystemPromptBuilder.cs | 82 ++++++++++++++++++++++++--------- wwwroot/css/chat.css | 25 ++++++++++ wwwroot/js/api.js | 6 +++ wwwroot/js/ui.js | 27 ++++++++++- 5 files changed, 148 insertions(+), 25 deletions(-) diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index b26d8cb..c883809 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -21,6 +21,7 @@ public static class SessionEndpoint app.MapPut("/api/sessions/{sessionId}", HandleUpdate); app.MapDelete("/api/sessions/{sessionId}", HandleDelete); 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.MapPost("/api/sessions/{sessionId}/exec", HandleExec); app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus); @@ -132,6 +133,38 @@ public static class SessionEndpoint 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) { var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); diff --git a/Services/SystemPromptBuilder.cs b/Services/SystemPromptBuilder.cs index d9bac6b..6d3e735 100644 --- a/Services/SystemPromptBuilder.cs +++ b/Services/SystemPromptBuilder.cs @@ -4,35 +4,42 @@ 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 string Build(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable skillSlugs = null, SkillService skillService = null) + public static List BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable skillSlugs = null, SkillService skillService = null) { - var parts = new List(); + var blocks = new List(); - // IDENTITY.md - Wer bin ich (ersetzt "Du bist {Name}.") + // 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)) { - parts.Add(identity); + blocks.Add(new PromptBlock { Icon = "fa-id-badge", Label = "Agent · IDENTITY.md (UseIdentity)", Content = identity }); hasIdentity = true; } } 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)) { var soul = memoryService.ReadMemoryFile(agent.AgentId, "SOUL.md"); 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)) { var userFile = System.IO.Path.Combine(dataDir, "USER.md"); @@ -42,7 +49,7 @@ public static class SystemPromptBuilder if (!string.IsNullOrWhiteSpace(userContent)) { 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) { - parts.Add(@$"## 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."); + 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." + }); } - 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. 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. -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)) - parts.Add($"## Kontext\n{session.Context}"); + blocks.Add(new PromptBlock { Icon = "fa-info-circle", Label = "Session · Kontext", Content = $"## Kontext\n{session.Context}" }); // Skills 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) { + var skillNames = new List(); + 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" + 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)) { var memoryLines = new List @@ -119,10 +144,10 @@ Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: fo 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 { @@ -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."); } - 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 -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."); + 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." + }); } 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"; 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 markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable 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)); } } diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index f2b76a6..ca61051 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -548,6 +548,31 @@ padding: var(--page-padding); overflow-y: auto; 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 { diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 9c7e622..13bbed2 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -122,6 +122,12 @@ async function apiGetSystemPrompt(sessionId) { 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) { const resp = await fetch(`/api/sessions/${sessionId}/streaming`); return resp.json(); diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 186669d..58e825d 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -607,6 +607,28 @@ async function loadAndShowAttachments(messageId, indicator) { // ── 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 = ' ' + 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) { // Overlay const overlay = document.createElement('div'); @@ -650,8 +672,9 @@ async function showSystemPromptModal(sessionId) { // Laden try { - const prompt = await apiGetSystemPrompt(sessionId); - body.innerHTML = renderMarkdown(prompt); + const blocks = await apiGetSystemPromptBlocks(sessionId); + body.innerHTML = ''; + _renderSystemPromptBlocks(body, blocks); } catch (e) { body.innerHTML = '
Fehler beim Laden des System-Prompts
'; }