fix: web_search_tool_result als Raw-JSON speichern für History-Rebuild
Problem: API 400 'web_search tool use without corresponding web_search_tool_result block' – weil web_search_tool_result nur als Sources-Array gespeichert wurde und in BuildHistoryFromDb kein Handler dafür existierte. Der server_tool_use-Block war in der History, aber der zugehörige web_search_tool_result fehlte. Backend (AnthropicService): - 3 Speicher-Stellen (StreamChat, RunAgentChat, ChatNonStreaming): Raw-JSON statt ExtractWebSearchSources-Array speichern Backend (ChatService): - Neuer Handler für web_search_result in BuildHistoryFromDb: Raw-JSON als web_search_tool_result in assistant content einfügen - Rückwärtskompatibilität: Alte Array-Einträge überspringen - CleanOrphanedWebSearchBlocks(): Post-Processing entfernt orphaned server_tool_use mit name web_search (alte DB-Einträge) Frontend (history.js, shared-page.js): - _extractWebSearchSources(): Unterstützt altes Array-Format und neues Raw-JSON Format (content[].type === web_search_result)
This commit is contained in:
1 parent
6499864f13
commit
3153cb6c6b
4 files changed
+142
-36
No files matched your search
@@ -161,16 +161,13 @@ public class AnthropicService
|
|||||||
}
|
}
|
||||||
else if (blockType == "web_search_tool_result")
|
else if (blockType == "web_search_tool_result")
|
||||||
{
|
{
|
||||||
var sources = ExtractWebSearchSources(el);
|
// Raw-JSON speichern für History-Rebuild (API braucht den vollständigen Block)
|
||||||
if (sources.Count > 0)
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
await _db.SaveMessage(new MessageRecord
|
ChainId = chainId, Iteration = iterations,
|
||||||
{
|
Role = "assistant", Type = "web_search_result",
|
||||||
ChainId = chainId, Iteration = iterations,
|
Content = el.GetRawText()
|
||||||
Role = "assistant", Type = "web_search_result",
|
});
|
||||||
Content = JsonSerializer.Serialize(sources)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (blockType.EndsWith("_tool_result"))
|
else if (blockType.EndsWith("_tool_result"))
|
||||||
{
|
{
|
||||||
@@ -838,19 +835,16 @@ public class AnthropicService
|
|||||||
else if (blockType == "web_search_tool_result")
|
else if (blockType == "web_search_tool_result")
|
||||||
{
|
{
|
||||||
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
|
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
|
||||||
var sources = ExtractWebSearchSources(block);
|
// Raw-JSON speichern für History-Rebuild (API braucht den vollständigen Block)
|
||||||
if (sources.Count > 0)
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
await _db.SaveMessage(new MessageRecord
|
ChainId = chainId,
|
||||||
{
|
Iteration = baseIterations + iterations,
|
||||||
ChainId = chainId,
|
Role = "assistant",
|
||||||
Iteration = baseIterations + iterations,
|
Type = "web_search_result",
|
||||||
Role = "assistant",
|
Content = block.GetRawText(),
|
||||||
Type = "web_search_result",
|
Tag = agentType
|
||||||
Content = JsonSerializer.Serialize(sources),
|
});
|
||||||
Tag = agentType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (blockType == "server_tool_use")
|
else if (blockType == "server_tool_use")
|
||||||
{
|
{
|
||||||
@@ -1019,16 +1013,13 @@ public class AnthropicService
|
|||||||
else if (blockType == "web_search_tool_result")
|
else if (blockType == "web_search_tool_result")
|
||||||
{
|
{
|
||||||
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
|
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
|
||||||
var sources = ExtractWebSearchSources(block);
|
// Raw-JSON speichern für History-Rebuild (API braucht den vollständigen Block)
|
||||||
if (sources.Count > 0)
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
await _db.SaveMessage(new MessageRecord
|
ChainId = ctx.ChainId, Iteration = iterations,
|
||||||
{
|
Role = "assistant", Type = "web_search_result",
|
||||||
ChainId = ctx.ChainId, Iteration = iterations,
|
Content = block.GetRawText()
|
||||||
Role = "assistant", Type = "web_search_result",
|
});
|
||||||
Content = JsonSerializer.Serialize(sources)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (blockType == "server_tool_use")
|
else if (blockType == "server_tool_use")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -419,6 +419,34 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (msg.Role == "assistant" && msg.Type == "web_search_result")
|
||||||
|
{
|
||||||
|
// web_search_tool_result – Raw-JSON in den Assistant-Content-Block einfügen
|
||||||
|
// (API erwartet web_search_tool_result nach server_tool_use vom Typ web_search)
|
||||||
|
JsonElement resultEl;
|
||||||
|
try { resultEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
|
||||||
|
catch { continue; }
|
||||||
|
|
||||||
|
// Rückwärtskompatibilität: Alte Einträge enthalten nur Sources-Array statt Raw-JSON
|
||||||
|
// Diese überspringen, da sie kein gültiger web_search_tool_result-Block sind
|
||||||
|
if (resultEl.ValueKind == JsonValueKind.Array) continue;
|
||||||
|
|
||||||
|
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null;
|
||||||
|
if (lastAssistant is Dictionary<string, object> dict &&
|
||||||
|
dict.TryGetValue("role", out var role) && role.ToString() == "assistant" &&
|
||||||
|
dict.TryGetValue("content", out var existingContent) && existingContent is List<object> contentList)
|
||||||
|
{
|
||||||
|
contentList.Add(resultEl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
history.Add(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["role"] = "assistant",
|
||||||
|
["content"] = new List<object> { resultEl }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (msg.Role == "user" && msg.Type == "tool_result")
|
else if (msg.Role == "user" && msg.Type == "tool_result")
|
||||||
{
|
{
|
||||||
var lastUser = history.Count > 0 ? history[history.Count - 1] : null;
|
var lastUser = history.Count > 0 ? history[history.Count - 1] : null;
|
||||||
@@ -562,9 +590,76 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post-Processing: Orphaned web_search server_tool_use entfernen
|
||||||
|
// Bei alten DB-Einträgen wurde web_search_tool_result nur als Sources-Array gespeichert,
|
||||||
|
// nicht als Raw-JSON → BuildHistoryFromDb überspringt sie (Array-Check) →
|
||||||
|
// server_tool_use mit name "web_search" hat kein zugehöriges web_search_tool_result →
|
||||||
|
// API 400 Fehler. Lösung: Solche orphaned Blöcke aus der History entfernen.
|
||||||
|
CleanOrphanedWebSearchBlocks(history);
|
||||||
|
|
||||||
return history;
|
return history;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entfernt server_tool_use-Blöcke vom Typ "web_search" aus assistant-Messages,
|
||||||
|
/// wenn kein zugehöriger web_search_tool_result-Block in derselben Message existiert.
|
||||||
|
/// </summary>
|
||||||
|
private static void CleanOrphanedWebSearchBlocks(List<object> history)
|
||||||
|
{
|
||||||
|
foreach (var msg in history)
|
||||||
|
{
|
||||||
|
if (msg is not Dictionary<string, object> dict) continue;
|
||||||
|
if (!dict.TryGetValue("role", out var role) || role.ToString() != "assistant") continue;
|
||||||
|
if (!dict.TryGetValue("content", out var content) || content is not List<object> contentList) continue;
|
||||||
|
|
||||||
|
// Prüfe ob es web_search server_tool_use gibt
|
||||||
|
var webSearchToolUseIds = new HashSet<string>();
|
||||||
|
var webSearchToolResultIds = new HashSet<string>();
|
||||||
|
|
||||||
|
foreach (var block in contentList)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(block);
|
||||||
|
JsonElement el;
|
||||||
|
try { el = JsonSerializer.Deserialize<JsonElement>(json); }
|
||||||
|
catch { continue; }
|
||||||
|
|
||||||
|
var type = el.TryGetProperty("type", out var t) ? t.GetString() : "";
|
||||||
|
if (type == "server_tool_use")
|
||||||
|
{
|
||||||
|
var name = el.TryGetProperty("name", out var n) ? n.GetString() : "";
|
||||||
|
var id = el.TryGetProperty("id", out var idProp) ? idProp.GetString() : "";
|
||||||
|
if (name == "web_search" && !string.IsNullOrEmpty(id))
|
||||||
|
webSearchToolUseIds.Add(id);
|
||||||
|
}
|
||||||
|
else if (type == "web_search_tool_result")
|
||||||
|
{
|
||||||
|
var toolUseId = el.TryGetProperty("tool_use_id", out var tid) ? tid.GetString() : "";
|
||||||
|
if (!string.IsNullOrEmpty(toolUseId))
|
||||||
|
webSearchToolResultIds.Add(toolUseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orphaned = server_tool_use IDs ohne zugehörige web_search_tool_result
|
||||||
|
var orphanedIds = webSearchToolUseIds.Except(webSearchToolResultIds).ToHashSet();
|
||||||
|
if (orphanedIds.Count == 0) continue;
|
||||||
|
|
||||||
|
// Entferne orphaned server_tool_use Blöcke
|
||||||
|
contentList.RemoveAll(block =>
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(block);
|
||||||
|
JsonElement el;
|
||||||
|
try { el = JsonSerializer.Deserialize<JsonElement>(json); }
|
||||||
|
catch { return false; }
|
||||||
|
|
||||||
|
var type = el.TryGetProperty("type", out var t) ? t.GetString() : "";
|
||||||
|
if (type != "server_tool_use") return false;
|
||||||
|
|
||||||
|
var id = el.TryGetProperty("id", out var idProp) ? idProp.GetString() : "";
|
||||||
|
return orphanedIds.Contains(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Baut einen SubAgentContext für Preparer/Review/Finalizer.
|
/// Baut einen SubAgentContext für Preparer/Review/Finalizer.
|
||||||
/// Gemeinsame Logik: Agent laden, Model auflösen, SystemPrompt bauen, ToolChoice bestimmen.
|
/// Gemeinsame Logik: Agent laden, Model auflösen, SystemPrompt bauen, ToolChoice bestimmen.
|
||||||
|
|||||||
+25
-4
@@ -1,3 +1,26 @@
|
|||||||
|
// ── Web Search Sources Extraction ──
|
||||||
|
// Unterstützt zwei Formate:
|
||||||
|
// - Alt: JSON-Array von {url, title} (nur Sources)
|
||||||
|
// - Neu: Raw web_search_tool_result JSON (content[].type === 'web_search_result')
|
||||||
|
function _extractWebSearchSources(content) {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(content || '[]');
|
||||||
|
// Altes Format: direktes Array von Sources
|
||||||
|
if (Array.isArray(parsed)) return parsed;
|
||||||
|
// Neues Format: Raw web_search_tool_result mit content-Array
|
||||||
|
if (parsed.content && Array.isArray(parsed.content)) {
|
||||||
|
var sources = [];
|
||||||
|
parsed.content.forEach(function(item) {
|
||||||
|
if (item.type === 'web_search_result' && item.url) {
|
||||||
|
sources.push({ url: item.url, title: item.title || '' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch(e) { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Image Attachments Auto-Loading ──
|
// ── Image Attachments Auto-Loading ──
|
||||||
|
|
||||||
async function _autoLoadImageAttachments(messageId) {
|
async function _autoLoadImageAttachments(messageId) {
|
||||||
@@ -246,8 +269,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
|||||||
break;
|
break;
|
||||||
case 'web_search_result': {
|
case 'web_search_result': {
|
||||||
if (!isToolVisible('web_search')) continue;
|
if (!isToolVisible('web_search')) continue;
|
||||||
var wsSources = [];
|
var wsSources = _extractWebSearchSources(msg.content);
|
||||||
try { wsSources = JSON.parse(msg.content || '[]'); } catch(e) {}
|
|
||||||
if (wsSources.length === 0) continue;
|
if (wsSources.length === 0) continue;
|
||||||
var wsDiv = document.createElement('div');
|
var wsDiv = document.createElement('div');
|
||||||
wsDiv.className = 'tool-box' + (archived ? ' archived' : '');
|
wsDiv.className = 'tool-box' + (archived ? ' archived' : '');
|
||||||
@@ -350,8 +372,7 @@ function _renderReviewGroup(tagGroup, archived) {
|
|||||||
} else if (m.type === 'web_search_result') {
|
} else if (m.type === 'web_search_result') {
|
||||||
if (!isToolVisible('web_search')) continue;
|
if (!isToolVisible('web_search')) continue;
|
||||||
flushText();
|
flushText();
|
||||||
var wsSources = [];
|
var wsSources = _extractWebSearchSources(m.content);
|
||||||
try { wsSources = JSON.parse(m.content || '[]'); } catch(e) {}
|
|
||||||
if (wsSources.length > 0) {
|
if (wsSources.length > 0) {
|
||||||
var wsDiv = document.createElement('div');
|
var wsDiv = document.createElement('div');
|
||||||
wsDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : '');
|
wsDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : '');
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ registerPage('shared', {
|
|||||||
|
|
||||||
// Web-Search-Results als Tool-Box mit Sources rendern
|
// Web-Search-Results als Tool-Box mit Sources rendern
|
||||||
if (msg.type === 'web_search_result') {
|
if (msg.type === 'web_search_result') {
|
||||||
var wsSources = [];
|
var wsSources = _extractWebSearchSources(c);
|
||||||
try { wsSources = JSON.parse(c || '[]'); } catch(e) {}
|
|
||||||
if (wsSources.length === 0) continue;
|
if (wsSources.length === 0) continue;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'tool-box';
|
div.className = 'tool-box';
|
||||||
|
|||||||
Reference in new issue
Block a user