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:
Meleki committed 2026-03-10 11:34:43 +01:00
1 parent 6499864f13
commit 3153cb6c6b
4 files changed
+142 -36

No files matched your search

+21 -30
View File
@@ -161,16 +161,13 @@ public class AnthropicService
}
else if (blockType == "web_search_tool_result")
{
var sources = ExtractWebSearchSources(el);
if (sources.Count > 0)
// Raw-JSON speichern für History-Rebuild (API braucht den vollständigen Block)
await _db.SaveMessage(new MessageRecord
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "web_search_result",
Content = JsonSerializer.Serialize(sources)
});
}
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "web_search_result",
Content = el.GetRawText()
});
}
else if (blockType.EndsWith("_tool_result"))
{
@@ -838,19 +835,16 @@ public class AnthropicService
else if (blockType == "web_search_tool_result")
{
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
var sources = ExtractWebSearchSources(block);
if (sources.Count > 0)
// Raw-JSON speichern für History-Rebuild (API braucht den vollständigen Block)
await _db.SaveMessage(new MessageRecord
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId,
Iteration = baseIterations + iterations,
Role = "assistant",
Type = "web_search_result",
Content = JsonSerializer.Serialize(sources),
Tag = agentType
});
}
ChainId = chainId,
Iteration = baseIterations + iterations,
Role = "assistant",
Type = "web_search_result",
Content = block.GetRawText(),
Tag = agentType
});
}
else if (blockType == "server_tool_use")
{
@@ -1019,16 +1013,13 @@ public class AnthropicService
else if (blockType == "web_search_tool_result")
{
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
var sources = ExtractWebSearchSources(block);
if (sources.Count > 0)
// Raw-JSON speichern für History-Rebuild (API braucht den vollständigen Block)
await _db.SaveMessage(new MessageRecord
{
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "assistant", Type = "web_search_result",
Content = JsonSerializer.Serialize(sources)
});
}
ChainId = ctx.ChainId, Iteration = iterations,
Role = "assistant", Type = "web_search_result",
Content = block.GetRawText()
});
}
else if (blockType == "server_tool_use")
{
+95
View File
@@ -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")
{
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;
}
/// <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>
/// Baut einen SubAgentContext für Preparer/Review/Finalizer.
/// Gemeinsame Logik: Agent laden, Model auflösen, SystemPrompt bauen, ToolChoice bestimmen.