fix(openai): Auto-Context, Tool-Result-Matching, Payload-Preview, leere Bubbles
- Auto-Context (Embedding-Suche) für OpenAI-Provider aktiviert: BuildAutoContextAsync auf internal static refactored und in StreamChat + ChatNonStreaming integriert - Tool-Results mit Role="tool" (OpenAI-Konvention) werden in BuildHistoryFromDb jetzt korrekt erkannt (vorher nur Role="user"/Anthropic) - Payload-Preview zeigt für OpenAI-Provider echtes API-Format (function-Wrapper, tool_choice:"auto", max_tokens, stream_options, extra_body) - Tool-Input (Argumente) wird im SSE tool_end-Event mitgesendet (fehlte im OpenAI-Pfad) - Leere Bubbles durch Whitespace-only Text nach </think>-Extraktion behoben (Trim in DB-Save, BuildHistoryFromDb und Frontend-Guard) - History-Toggle-Bug: var→const in for-Loop (Block-Scoping für Closures), tool_result-Matching per data-tool-call-id statt blindes letztes Element - Non-Streaming max_tokens für MiniMax auf 32768 + extra_body.reasoning_split Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ecddbe5f36
commit
1fe04025d3
9 files changed
+164
-42
No files matched your search
@@ -240,7 +240,7 @@ public static class ChatEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
await openAi.StreamChat(res, ctx.ModelId, ctx.SystemPromptText, messages, ctx.ChainId, workspace: ctx.Workspace, sessionId: sessionId, agentId: ctx.AgentId, apiKeyOverride: openAiKeyOverride, workCt: workCt, sseCt: sseCt);
|
||||
await openAi.StreamChat(res, ctx.ModelId, ctx.SystemPromptText, messages, ctx.ChainId, workspace: ctx.Workspace, sessionId: sessionId, agentId: ctx.AgentId, apiKeyOverride: openAiKeyOverride, useAutoContext: ctx.Agent?.UseAutoContext ?? true, workCt: workCt, sseCt: sseCt);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -317,8 +317,54 @@ public static class SessionEndpoint
|
||||
var toolChoice = hasTools || !string.IsNullOrEmpty(agent.WebSearchVersion) ? "any" : null;
|
||||
var effectiveChoice = toolChoice == "any" ? "any" : "auto";
|
||||
|
||||
// Payload zusammenbauen (Anthropic-Format)
|
||||
var payload = new Dictionary<string, object>
|
||||
// OpenAI-kompatible Provider: Payload im tatsächlichen API-Format anzeigen
|
||||
if (previewProvider)
|
||||
{
|
||||
var (provider, _) = OpenAiService.ParseModelString(modelId);
|
||||
var maxTokens = provider == "minimax" ? 32768 : 16384;
|
||||
|
||||
// Messages konvertieren (Anthropic → OpenAI-Format)
|
||||
var openAiMessages = OpenAiService.BuildApiMessages(systemPromptText, messages);
|
||||
|
||||
// Tools konvertieren (input_schema → function/parameters)
|
||||
var openAiTools = tools.Count > 0
|
||||
? OpenAiService.ConvertTo_openAiToolDefs(tools.ToArray())
|
||||
: new List<object>();
|
||||
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = modelId,
|
||||
["max_tokens"] = maxTokens,
|
||||
["messages"] = openAiMessages,
|
||||
["stream"] = true,
|
||||
["stream_options"] = new { include_usage = true }
|
||||
};
|
||||
|
||||
if (openAiTools.Count > 0)
|
||||
{
|
||||
payload["tools"] = openAiTools;
|
||||
payload["tool_choice"] = "auto";
|
||||
}
|
||||
|
||||
if (provider == "minimax")
|
||||
payload["extra_body"] = new { reasoning_split = true };
|
||||
|
||||
if (provider == "nvidia")
|
||||
{
|
||||
var nvidiaModelId = modelId.Contains(":") ? modelId.Split(':')[1] : modelId;
|
||||
if (nvidiaModelId.Contains("kimi-k2", StringComparison.OrdinalIgnoreCase)
|
||||
|| nvidiaModelId.Contains("qwq", StringComparison.OrdinalIgnoreCase)
|
||||
|| nvidiaModelId.Contains("deepseek-r1", StringComparison.OrdinalIgnoreCase))
|
||||
payload["chat_template_kwargs"] = new { thinking = true };
|
||||
}
|
||||
|
||||
ctx.Response.ContentType = "application/json; charset=utf-8";
|
||||
await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Anthropic-Format Payload
|
||||
var anthropicPayload = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = modelId,
|
||||
["max_tokens"] = 16384,
|
||||
@@ -327,16 +373,16 @@ public static class SessionEndpoint
|
||||
};
|
||||
|
||||
if (systemPrompt.Count > 0)
|
||||
payload["system"] = systemPrompt;
|
||||
anthropicPayload["system"] = systemPrompt;
|
||||
|
||||
if (tools.Count > 0)
|
||||
{
|
||||
payload["tools"] = tools;
|
||||
payload["tool_choice"] = new { type = effectiveChoice };
|
||||
anthropicPayload["tools"] = tools;
|
||||
anthropicPayload["tool_choice"] = new { type = effectiveChoice };
|
||||
}
|
||||
|
||||
ctx.Response.ContentType = "application/json; charset=utf-8";
|
||||
await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }));
|
||||
await ctx.Response.WriteAsync(JsonSerializer.Serialize(anthropicPayload, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
private static async Task HandleStreaming(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker)
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ builder.Services.AddSingleton<AnthropicService>(sp =>
|
||||
|
||||
// OpenAI-kompatibler Service (Nvidia etc.)
|
||||
builder.Services.AddSingleton<OpenAiService>(sp =>
|
||||
new OpenAiService(sp.GetRequiredService<IHttpClientFactory>(), dbService, sp.GetRequiredService<ToolService>(), serverSettings, dataDir));
|
||||
new OpenAiService(sp.GetRequiredService<IHttpClientFactory>(), dbService, sp.GetRequiredService<ToolService>(), serverSettings, dataDir, vectorSearchService));
|
||||
|
||||
// Telegram Bot (pro Session – Token aus Session-Config)
|
||||
builder.Services.AddSingleton<TelegramService>(sp =>
|
||||
|
||||
@@ -92,7 +92,7 @@ public partial class AnthropicService
|
||||
{
|
||||
try
|
||||
{
|
||||
var autoCtx = await BuildAutoContextAsync(messages, chainId, agentId);
|
||||
var autoCtx = await BuildAutoContextAsync(_vectorSearch, messages, chainId, agentId);
|
||||
if (autoCtx != null)
|
||||
{
|
||||
systemText.AppendLine(autoCtx);
|
||||
|
||||
@@ -62,7 +62,7 @@ public partial class AnthropicService
|
||||
{
|
||||
try
|
||||
{
|
||||
var autoCtx = await BuildAutoContextAsync(messages, chainId, agentId);
|
||||
var autoCtx = await BuildAutoContextAsync(_vectorSearch, messages, chainId, agentId);
|
||||
if (autoCtx != null)
|
||||
{
|
||||
systemPrompt.Add(new { type = "text", text = autoCtx });
|
||||
|
||||
@@ -138,7 +138,7 @@ public partial class AnthropicService
|
||||
}
|
||||
|
||||
/// <summary>Extrahiert die letzten N User-Texte für Query-Erweiterung.</summary>
|
||||
private static string ExtractRecentUserTexts(List<object> messages, int count = 3, int maxLength = 500)
|
||||
internal static string ExtractRecentUserTexts(List<object> messages, int count = 3, int maxLength = 500)
|
||||
{
|
||||
var texts = new List<string>();
|
||||
for (int i = messages.Count - 1; i >= 0 && texts.Count < count; i--)
|
||||
@@ -171,21 +171,21 @@ public partial class AnthropicService
|
||||
|
||||
/// <summary>
|
||||
/// Baut den AutoContext-String aus Embedding-Suche.
|
||||
/// Shared zwischen StreamChat und StreamChatViaCli.
|
||||
/// Shared zwischen StreamChat, StreamChatViaCli und OpenAiService.
|
||||
/// </summary>
|
||||
private async Task<string> BuildAutoContextAsync(List<object> messages, string chainId, string agentId)
|
||||
internal static async Task<string> BuildAutoContextAsync(VectorSearchService vectorSearch, List<object> messages, string chainId, string agentId)
|
||||
{
|
||||
if (_vectorSearch == null || messages.Count == 0) return null;
|
||||
if (vectorSearch == null || messages.Count == 0) return null;
|
||||
|
||||
// Query-Erweiterung: letzte 2-3 User-Messages statt nur die letzte
|
||||
var queryText = ExtractRecentUserTexts(messages, count: 3, maxLength: 500);
|
||||
if (string.IsNullOrWhiteSpace(queryText)) return null;
|
||||
|
||||
var contextResults = await _vectorSearch.SearchAsync(queryText, agentId, topN: 5, minScore: 0.35f);
|
||||
var contextResults = await vectorSearch.SearchAsync(queryText, agentId, topN: 5, minScore: 0.35f);
|
||||
// Note: threshold lowered from 0.45 to 0.35 because hybrid search is now more precise
|
||||
contextResults.RemoveAll(r => r.ChainId == chainId || (r.TextPreview?.Length ?? 0) < 20);
|
||||
|
||||
var memoryResults = await _vectorSearch.SearchMemoryFilesAsync(queryText, agentId, topN: 3, minScore: 0.35f);
|
||||
var memoryResults = await vectorSearch.SearchMemoryFilesAsync(queryText, agentId, topN: 3, minScore: 0.35f);
|
||||
|
||||
if (contextResults.Count == 0 && memoryResults.Count == 0) return null;
|
||||
|
||||
|
||||
+11
-5
@@ -299,12 +299,12 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
||||
["content"] = new List<object> { new { type = "text", text = msg.Content } }
|
||||
});
|
||||
}
|
||||
else if (msg.Role == "assistant" && msg.Type == "text" && !string.IsNullOrEmpty(msg.Content))
|
||||
else if (msg.Role == "assistant" && msg.Type == "text" && !string.IsNullOrWhiteSpace(msg.Content))
|
||||
{
|
||||
history.Add(new Dictionary<string, object>
|
||||
{
|
||||
["role"] = "assistant",
|
||||
["content"] = new List<object> { new { type = "text", text = msg.Content } }
|
||||
["content"] = new List<object> { new { type = "text", text = msg.Content.Trim() } }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -383,11 +383,14 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
||||
}
|
||||
else if (msg.Role == "assistant" && msg.Type == "text")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(msg.Content))
|
||||
// Trim: Nach </think>-Extraktion bleibt oft nur "\n" als Text übrig.
|
||||
// Whitespace-only Text nicht in die History aufnehmen (verursacht leere Bubbles).
|
||||
var trimmedContent = msg.Content?.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmedContent))
|
||||
{
|
||||
// Wenn die letzte Message bereits assistant ist (z.B. mit Thinking-Block
|
||||
// aus derselben Iteration), Text dort anhängen statt neue Message zu starten.
|
||||
AppendToLastMessage(history, "assistant", new { type = "text", text = msg.Content });
|
||||
AppendToLastMessage(history, "assistant", new { type = "text", text = trimmedContent });
|
||||
}
|
||||
}
|
||||
else if (msg.Role == "assistant" && msg.Type == "tool_use")
|
||||
@@ -436,8 +439,11 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
||||
|
||||
AppendToLastMessage(history, "assistant", resultEl);
|
||||
}
|
||||
else if (msg.Role == "user" && msg.Type == "tool_result")
|
||||
else if ((msg.Role == "user" || msg.Role == "tool") && msg.Type == "tool_result")
|
||||
{
|
||||
// Anthropic speichert tool_results mit Role="user", OpenAI mit Role="tool".
|
||||
// Beide werden in Anthropic-Format (user-Message mit tool_result-Block) eingefügt,
|
||||
// da BuildApiMessages später das Format für OpenAI-Provider konvertiert.
|
||||
var toolResultBlock = new { type = "tool_result", tool_use_id = msg.ToolCallId, content = SseHelper.TruncateToolResult(msg.Content ?? ""), is_error = msg.IsError };
|
||||
AppendToLastMessage(history, "user", toolResultBlock);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public class OpenAiService
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly DatabaseService _db;
|
||||
private readonly ToolService _toolService;
|
||||
private readonly VectorSearchService _vectorSearch;
|
||||
|
||||
// Provider-Keys (statisch für ChatService.ResolveModelId)
|
||||
public static readonly HashSet<string> ProviderKeys = new() { "nvidia", "pollinations", "gemini", "openai", "minimax" };
|
||||
@@ -36,7 +37,7 @@ public class OpenAiService
|
||||
/// Konvertiert Anthropic-Format Tool-Definitionen (input_schema) in OpenAI-Format (function/parameters).
|
||||
/// Single source of truth: ToolService.GetToolDefinitions()
|
||||
/// </summary>
|
||||
private static List<object> ConvertTo_openAiToolDefs(object[] anthropicTools)
|
||||
public static List<object> ConvertTo_openAiToolDefs(object[] anthropicTools)
|
||||
{
|
||||
var result = new List<object>();
|
||||
foreach (var tool in anthropicTools)
|
||||
@@ -86,11 +87,12 @@ public class OpenAiService
|
||||
|
||||
private readonly ServerSettingsService _settings;
|
||||
|
||||
public OpenAiService(IHttpClientFactory httpFactory, DatabaseService db, ToolService toolService, ServerSettingsService settings, string dataDir)
|
||||
public OpenAiService(IHttpClientFactory httpFactory, DatabaseService db, ToolService toolService, ServerSettingsService settings, string dataDir, VectorSearchService vectorSearch = null)
|
||||
{
|
||||
_httpFactory = httpFactory;
|
||||
_db = db;
|
||||
_toolService = toolService;
|
||||
_vectorSearch = vectorSearch;
|
||||
_settings = settings;
|
||||
_cacheFile = Path.Combine(dataDir, "models_cache.json");
|
||||
Providers = new Dictionary<string, (string BaseUrl, string ApiKey)>
|
||||
@@ -295,6 +297,7 @@ public class OpenAiService
|
||||
string agentId = null,
|
||||
int maxIterations = 10,
|
||||
string apiKeyOverride = null,
|
||||
bool useAutoContext = true,
|
||||
CancellationToken workCt = default,
|
||||
CancellationToken sseCt = default)
|
||||
{
|
||||
@@ -328,6 +331,32 @@ public class OpenAiService
|
||||
|
||||
try
|
||||
{
|
||||
// ── Automatische Kontext-Injektion via Embedding-Suche ──
|
||||
if (useAutoContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var autoCtx = await AnthropicService.BuildAutoContextAsync(_vectorSearch, messages, chainId, agentId);
|
||||
if (autoCtx != null)
|
||||
{
|
||||
// Auto-Context an System-Prompt anhängen
|
||||
systemPromptText = (systemPromptText ?? "") + "\n" + autoCtx;
|
||||
await SseHelper.TryWriteSse(response, "auto_context", new { context = autoCtx }, sseCt);
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId,
|
||||
Role = "system",
|
||||
Type = "auto_context",
|
||||
Content = autoCtx
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[AutoContext/OpenAI] Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var apiMessages = BuildApiMessages(systemPromptText, messages);
|
||||
|
||||
// Agentic Loop
|
||||
@@ -786,8 +815,8 @@ public class OpenAiService
|
||||
});
|
||||
}
|
||||
|
||||
// Text in DB speichern
|
||||
var finalText = fullText.ToString();
|
||||
// Text in DB speichern (Whitespace-only nach </think> ignorieren)
|
||||
var finalText = fullText.ToString().Trim();
|
||||
if (!string.IsNullOrEmpty(finalText))
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
@@ -925,6 +954,7 @@ public class OpenAiService
|
||||
{
|
||||
name = tcName,
|
||||
id = tcId,
|
||||
input = tcArgs.ToString(),
|
||||
result = toolResult,
|
||||
isError
|
||||
}, sseCt);
|
||||
@@ -1029,7 +1059,32 @@ public class OpenAiService
|
||||
Model = ctx.ModelId
|
||||
};
|
||||
|
||||
var apiMessages = BuildApiMessages(ctx.SystemPromptText, messages);
|
||||
// ── Automatische Kontext-Injektion via Embedding-Suche ──
|
||||
var systemPromptText = ctx.SystemPromptText;
|
||||
if (ctx.Agent?.UseAutoContext ?? true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var autoCtx = await AnthropicService.BuildAutoContextAsync(_vectorSearch, messages, ctx.ChainId, ctx.AgentId);
|
||||
if (autoCtx != null)
|
||||
{
|
||||
systemPromptText = (systemPromptText ?? "") + "\n" + autoCtx;
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = ctx.ChainId,
|
||||
Role = "system",
|
||||
Type = "auto_context",
|
||||
Content = autoCtx
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[AutoContext/OpenAI-NonStreaming] Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var apiMessages = BuildApiMessages(systemPromptText, messages);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1039,12 +1094,13 @@ public class OpenAiService
|
||||
onIteration?.Invoke(iterations);
|
||||
|
||||
var client = CreateClient(providerConfig.ApiKey);
|
||||
var maxTokens = provider == "minimax" ? 32768 : 16384;
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = modelId,
|
||||
["messages"] = apiMessages,
|
||||
["stream"] = false,
|
||||
["max_tokens"] = 16384,
|
||||
["max_tokens"] = maxTokens,
|
||||
["tools"] = _openAiToolDefs,
|
||||
["tool_choice"] = "auto"
|
||||
};
|
||||
@@ -1052,6 +1108,9 @@ public class OpenAiService
|
||||
if (provider == "nvidia" && IsNvidiaThinkingModel(modelId))
|
||||
payload["chat_template_kwargs"] = new { thinking = true };
|
||||
|
||||
if (provider == "minimax")
|
||||
payload["extra_body"] = new { reasoning_split = true };
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
|
||||
@@ -1364,7 +1423,7 @@ public class OpenAiService
|
||||
/// <summary>
|
||||
/// Konvertiert Anthropic-Format Messages in OpenAI-kompatibles Format.
|
||||
/// </summary>
|
||||
private static List<object> BuildApiMessages(string systemPromptText, List<object> messages)
|
||||
public static List<object> BuildApiMessages(string systemPromptText, List<object> messages)
|
||||
{
|
||||
var apiMessages = new List<object>();
|
||||
if (!string.IsNullOrEmpty(systemPromptText))
|
||||
|
||||
+25
-14
@@ -220,7 +220,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
||||
// ── Grouped Review/Preparer/Finalizer Bubble (tag-based) ──
|
||||
if (msg.tag && (msg.tag === 'review' || msg.tag === 'preparer' || msg.tag === 'finalizer')) {
|
||||
// Collect all consecutive messages with the same tag
|
||||
var tagGroup = [msg];
|
||||
const tagGroup = [msg];
|
||||
while (_msgIdx + 1 < msgs.length && msgs[_msgIdx + 1].tag === msg.tag) {
|
||||
_msgIdx++;
|
||||
tagGroup.push(msgs[_msgIdx]);
|
||||
@@ -250,6 +250,8 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
||||
_addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked });
|
||||
fragment.appendChild(div);
|
||||
} else if (msg.role === 'assistant') {
|
||||
// Whitespace-only Content nicht rendern (z.B. "\n" nach </think>-Extraktion)
|
||||
if (!msg.content || !msg.content.trim()) break;
|
||||
const div = _createMessageBubble({ role: 'assistant', timestamp: msg.createdAt, messageId: msg.id, isMarked: msg.isMarked, deletedOn: msg.deletedOn, archived });
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
parseFluentEmojis(div);
|
||||
@@ -261,12 +263,12 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
||||
}
|
||||
case 'auto_context': {
|
||||
if (msg.content) {
|
||||
var acDiv = document.createElement('div');
|
||||
const acDiv = document.createElement('div');
|
||||
acDiv.className = 'tool-box auto-context-box' + (archived ? ' archived' : '');
|
||||
var acHeader = document.createElement('div');
|
||||
const acHeader = document.createElement('div');
|
||||
acHeader.className = 'tool-box-header clickable';
|
||||
acHeader.innerHTML = '<span class="tool-name">Auto-Context</span>';
|
||||
var acContent = document.createElement('div');
|
||||
const acContent = document.createElement('div');
|
||||
acContent.className = 'auto-context-content markdown';
|
||||
acContent.innerHTML = renderMarkdown(msg.content.replace(/<\/?recalled_context>/g, '').trim());
|
||||
acContent.hidden = true;
|
||||
@@ -279,12 +281,12 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
||||
}
|
||||
case 'thinking': {
|
||||
if (msg.role === 'assistant' && msg.content) {
|
||||
var thinkDiv = document.createElement('div');
|
||||
const thinkDiv = document.createElement('div');
|
||||
thinkDiv.className = 'tool-box thinking-box' + (archived ? ' archived' : '');
|
||||
var thinkHeader = document.createElement('div');
|
||||
const thinkHeader = document.createElement('div');
|
||||
thinkHeader.className = 'tool-box-header clickable';
|
||||
thinkHeader.innerHTML = '<span class="tool-name">Denkprozess</span>';
|
||||
var thinkContent = document.createElement('div');
|
||||
const thinkContent = document.createElement('div');
|
||||
thinkContent.className = 'thinking-content markdown';
|
||||
thinkContent.innerHTML = renderMarkdown(msg.content);
|
||||
thinkContent.hidden = true;
|
||||
@@ -301,17 +303,18 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
||||
continue;
|
||||
}
|
||||
if (!isToolVisible(msg.toolName)) continue;
|
||||
var toolDiv = document.createElement('div');
|
||||
const toolDiv = document.createElement('div');
|
||||
toolDiv.className = 'tool-box' + (archived ? ' archived' : '');
|
||||
toolDiv.dataset.toolName = msg.toolName;
|
||||
if (msg.toolCallId) toolDiv.dataset.toolCallId = msg.toolCallId;
|
||||
renderToolBoxContent(toolDiv, msg.toolName, msg.content, null, false);
|
||||
fragment.appendChild(toolDiv);
|
||||
break;
|
||||
case 'web_search_result': {
|
||||
if (!isToolVisible('web_search')) continue;
|
||||
var wsSources = _extractWebSearchSources(msg.content);
|
||||
const wsSources = _extractWebSearchSources(msg.content);
|
||||
if (wsSources.length === 0) continue;
|
||||
var wsDiv = document.createElement('div');
|
||||
const wsDiv = document.createElement('div');
|
||||
wsDiv.className = 'tool-box' + (archived ? ' archived' : '');
|
||||
wsDiv.dataset.toolName = 'web_search';
|
||||
renderToolBoxContent(wsDiv, 'web_search', null, null, false, wsSources);
|
||||
@@ -320,10 +323,18 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
||||
}
|
||||
case 'tool_result': {
|
||||
if (msg._choiceHandled) continue;
|
||||
const prevBoxes = fragment.querySelectorAll('.tool-box');
|
||||
const lastToolBox = prevBoxes[prevBoxes.length - 1];
|
||||
// Passendes Tool-Box per toolCallId finden (falls verfügbar),
|
||||
// sonst Fallback auf letztes Tool-Box (ältere DB-Einträge ohne toolCallId)
|
||||
let lastToolBox = null;
|
||||
if (msg.toolCallId) {
|
||||
lastToolBox = fragment.querySelector('.tool-box[data-tool-call-id="' + msg.toolCallId + '"]');
|
||||
}
|
||||
if (!lastToolBox) {
|
||||
const prevBoxes = fragment.querySelectorAll('.tool-box');
|
||||
lastToolBox = prevBoxes[prevBoxes.length - 1];
|
||||
}
|
||||
if (lastToolBox) {
|
||||
var existingResult = lastToolBox.querySelector('.tool-result');
|
||||
const existingResult = lastToolBox.querySelector('.tool-result');
|
||||
// todo_write: nachträglich die Todo-Liste rendern (sichtbar)
|
||||
if (lastToolBox.dataset.toolName === 'todo_write' && !lastToolBox.querySelector('.todo-list')) {
|
||||
lastToolBox.appendChild(buildTodoList(msg.content || '', msg.isError));
|
||||
@@ -339,7 +350,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
||||
rd.hidden = true;
|
||||
lastToolBox.appendChild(rd);
|
||||
// Header clickable machen
|
||||
var hdr = lastToolBox.querySelector('.tool-box-header');
|
||||
const hdr = lastToolBox.querySelector('.tool-box-header');
|
||||
if (hdr && !hdr.classList.contains('clickable')) {
|
||||
hdr.classList.add('clickable');
|
||||
hdr.addEventListener('click', function() { rd.hidden = !rd.hidden; });
|
||||
|
||||
Reference in new issue
Block a user