diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index f6de954..d2d75de 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -26,6 +26,7 @@ public static class SessionEndpoint 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}/payload-preview", HandlePayloadPreview); app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming); app.MapPost("/api/sessions/{sessionId}/exec", HandleExec); app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus); @@ -266,6 +267,75 @@ public static class SessionEndpoint await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content })); } + private static async Task HandlePayloadPreview(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService, ChatService chatService, ToolService toolService, ServerSettingsService settings) + { + var result = await ResolvePromptContext(ctx, db, memoryService); + if (result == null) return; + var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value; + + // System-Prompt + var promptBlocks = SystemPromptBuilder.BuildBlocks(agent, session, marked, memoryService, memDir, + skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode); + var systemPromptText = string.Join("\n\n", promptBlocks.Select(b => b.Content)); + + var systemPrompt = new List(); + if (!string.IsNullOrWhiteSpace(systemPromptText)) + systemPrompt.Add(new { type = "text", text = systemPromptText }); + + // Model + var modelId = ChatService.ResolveModelId(agent.Model); + + // Messages aus der DB (exakt wie beim Chat) + var messages = await chatService.BuildHistoryFromDb(session.SessionId, promptBlocks: promptBlocks); + + // Tools (exakt wie AddToolsToPayload es machen würde) + var hasWorkspace = !string.IsNullOrEmpty(workspace); + var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId); + var includeGitHubSearch = agent.UseGitHubSearch && toolService.HasGitHubToken; + var includeSerperSearch = agent.UseSerperSearch && toolService.HasSerperKey; + var hasTools = hasWorkspace || includeMemory || includeGitHubSearch || includeSerperSearch; + + var tools = new List(); + if (hasTools) + { + var allTools = toolService.GetToolDefinitions( + includeFileTools: hasWorkspace, + includeMemory: includeMemory, + includeUserChoice: agent.AllowUserChoice, + includeGitHubSearch: includeGitHubSearch, + includeSerperSearch: includeSerperSearch); + if (planMode) + allTools = toolService.FilterReadOnlyTools(allTools); + tools.AddRange(allTools); + } + if (!string.IsNullOrEmpty(agent.WebSearchVersion)) + tools.Add(new { type = agent.WebSearchVersion, name = "web_search", max_uses = 5 }); + + var toolChoice = hasTools || !string.IsNullOrEmpty(agent.WebSearchVersion) ? "any" : null; + var effectiveChoice = toolChoice == "any" ? "any" : "auto"; + + // Payload zusammenbauen (Anthropic-Format) + var payload = new Dictionary + { + ["model"] = modelId, + ["max_tokens"] = 16384, + ["messages"] = messages, + ["stream"] = true + }; + + if (systemPrompt.Count > 0) + payload["system"] = systemPrompt; + + if (tools.Count > 0) + { + payload["tools"] = tools; + payload["tool_choice"] = new { type = effectiveChoice }; + } + + ctx.Response.ContentType = "application/json; charset=utf-8"; + await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); + } + private static async Task HandleStreaming(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker) { var userId = ctx.GetUserId(); diff --git a/Services/ChatService.cs b/Services/ChatService.cs index a7e3b1e..2c0ec84 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -549,9 +549,10 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? history.RemoveRange(0, removeCount); } - // Reminder einfügen: alle ~30 user/assistant-Paare ein Reminder-Paar einfügen + // Reminder einfügen: von hinten gezählt – halbes Intervall vor Ende, dann volle Intervalle var reminder = SystemPromptBuilder.BuildReminder(promptBlocks); - if (!string.IsNullOrWhiteSpace(reminder) && ReminderInterval > 0 && history.Count > ReminderInterval) + int halfInterval = ReminderInterval / 2; + if (!string.IsNullOrWhiteSpace(reminder) && ReminderInterval > 0 && history.Count > halfInterval) { var reminderUser = new Dictionary { @@ -564,17 +565,29 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? ["content"] = new List { new { type = "text", text = "Verstanden." } } }; - // Alle 30 Messages ein Reminder-Paar einfügen (von hinten nach vorne, damit Indizes stimmen) - var insertPositions = new List(); - int messageCount = 0; - for (int i = 0; i < history.Count; i++) + // Zielpositionen von hinten berechnen: + // Erste Position: history.Count - halfInterval (z.B. 15 vom Ende) + // Weitere Positionen: jeweils - ReminderInterval (z.B. 30er-Schritte rückwärts) + var targetPositions = new List(); + int pos = history.Count - halfInterval; + while (pos > 0) { - messageCount++; - if (messageCount > 0 && messageCount % ReminderInterval == 0) + targetPositions.Add(pos); + pos -= ReminderInterval; + } + + // Für jede Zielposition die nächste valide Einfügestelle finden + var insertPositions = new List(); + foreach (int target in targetPositions) + { + // Suche ab target rückwärts nach einer assistant-Message, + // deren Folge-Message kein tool_result ist + for (int i = Math.Min(target, history.Count - 1); i >= 1; i--) { - // Position nach dem aktuellen Element – aber nur einfügen wenn es eine - // assistant-Message ist (damit user/assistant-Alternierung nicht bricht) - // UND die nächste Message kein tool_result ist (sonst bricht tool_use/tool_result-Paar) + if (!(history[i] is Dictionary d && + d.TryGetValue("role", out var r) && r.ToString() == "assistant")) + continue; + bool nextIsToolResult = false; if (i + 1 < history.Count && history[i + 1] is Dictionary nextDict && nextDict.TryGetValue("role", out var nextRole) && nextRole.ToString() == "user" && @@ -587,20 +600,20 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? }); } - if (!nextIsToolResult && history[i] is Dictionary d && - d.TryGetValue("role", out var r) && r.ToString() == "assistant") + if (!nextIsToolResult) { insertPositions.Add(i + 1); + break; } } } - // Von hinten nach vorne einfügen, damit Indizes nicht verschoben werden - for (int j = insertPositions.Count - 1; j >= 0; j--) + // Deduplizieren und von hinten nach vorne einfügen, damit Indizes nicht verschoben werden + insertPositions = insertPositions.Distinct().OrderByDescending(x => x).ToList(); + foreach (int insertPos in insertPositions) { - var pos = insertPositions[j]; - history.Insert(pos, reminderAssistant); - history.Insert(pos, reminderUser); + history.Insert(insertPos, reminderAssistant); + history.Insert(insertPos, reminderUser); } } diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 06dcfe9..9db48ce 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -874,6 +874,23 @@ padding: 2em; } +/* Payload Preview Modal */ + +.payload-preview-modal { + max-width: 90vw; +} + +.payload-json { + font-family: var(--mono); + white-space: pre-wrap; + word-break: break-all; + margin: 0; + padding: var(--box-padding); + background: var(--f3-bg); + border-radius: var(--border-radius); + border: var(--border-width) solid var(--f2-border); +} + /* Command Output Modal */ .cmd-output-modal { @@ -1276,7 +1293,7 @@ /* ── OpenClaw Import Modal ── */ .openclaw-import-modal { - max-width: 700px; + max-width: var(--modal-width); } .openclaw-hint { @@ -1314,8 +1331,8 @@ } .openclaw-session-id { - font-family: var(--mono-font); - font-size: var(--small-font-size); + font-family: var(--mono); + font-size: var(--mono-size); color: var(--primary); font-weight: 600; } diff --git a/wwwroot/index.html b/wwwroot/index.html index 7efbfd5..4ebafbf 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -386,6 +386,7 @@ diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 02293ed..1d6e88c 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -111,6 +111,11 @@ function apiGetSystemPromptBlocks(sessionId, planMode) { return _get(url); } +function apiGetPayloadPreview(sessionId, planMode) { + var url = '/api/sessions/' + sessionId + '/payload-preview' + (planMode ? '?planMode=true' : ''); + return _get(url); +} + function apiIsStreaming(sessionId) { return _get('/api/sessions/' + sessionId + '/streaming'); } function apiGitStatus(sessionId) { return _get('/api/sessions/' + sessionId + '/git-status', null); } diff --git a/wwwroot/js/modals.js b/wwwroot/js/modals.js index e1dc498..e81a191 100644 --- a/wwwroot/js/modals.js +++ b/wwwroot/js/modals.js @@ -76,6 +76,26 @@ async function showSystemPromptModal(sessionId) { } } +// ── Payload Preview Modal ── + +async function showPayloadPreviewModal(sessionId) { + const { body } = _createModal('Payload Preview', { modal: 'payload-preview-modal' }); + body.innerHTML = '
Laden...
'; + + try { + var planMode = typeof _planModeActive !== 'undefined' && _planModeActive; + var payload = await apiGetPayloadPreview(sessionId, planMode); + body.innerHTML = ''; + + var pre = document.createElement('pre'); + pre.className = 'payload-json'; + pre.textContent = JSON.stringify(payload, null, 2); + body.appendChild(pre); + } catch (e) { + body.innerHTML = '
Fehler beim Laden des Payloads
'; + } +} + // ── Command Output Modal ── function showCommandOutputModal(name, command, result) { diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index f57d297..162e698 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -416,6 +416,13 @@ registerPage('chat', { showSystemPromptModal(sessionId); }); + // Payload anzeigen + const payloadPreviewBtn = document.getElementById('payloadPreviewBtn'); + if (payloadPreviewBtn) payloadPreviewBtn.addEventListener('click', () => { + if (chatMenuDropdown) chatMenuDropdown.hidden = true; + showPayloadPreviewModal(sessionId); + }); + // Archivieren const archiveBtn = document.getElementById('archiveBtn'); if (archiveBtn) archiveBtn.addEventListener('click', () => {