refactor: Backend deduplizieren – 13 Dateien, -528 Zeilen netto

ChatContext: 3 identische Context-Klassen (PreparerContext, ReviewContext,
FinalizerContext) zu einer SubAgentContext-Klasse zusammengefasst.

ChatService: 3x40 Zeilen Sub-Agent-Setup durch BuildSubAgentContext() ersetzt.

AnthropicService: 5 Hilfsmethoden extrahiert – ExtractWebSearchSources(),
TruncateToolResult(), AddToolsToPayload(), ParseServerToolUse(), TrackUsage().

WorkspaceEndpoint: 5 Session/Agent-Endpoint-Paare durch generische Core-Handler
mit basePath-Parameter ersetzt. Ungenutzte rmdir-Endpoints entfernt.

AgentEndpoint: Doppeltes Agent-DTO-Mapping durch MapAgentList() ersetzt.

SessionEndpoint: Doppelte Session/Agent-Auflösung durch ResolvePromptContext() ersetzt.

DatabaseService: Toten Code entfernt (SetSessionTelegramChatId,
ClearSessionTelegramChatId, GetSessions, GetUserSessions). ConvertDataUrlToWebP
und ResizeDataUrlToWebP zu einer Methode zusammengefasst.

TelegramEndpoint: Bot-DTO-Mapping durch MapBotResponse() dedupliziert.

KreaEndpoint + SkillEndpoint: Thumbnail-Generierung in ImageService.GenerateThumbnail()
extrahiert, SixLabors-Imports aus Endpoints entfernt.

CronjobEndpoint: 7x ctx.Items[UserId] durch ctx.GetUserId() ersetzt.
This commit is contained in:
Meleki committed 2026-03-09 09:18:49 +01:00
1 parent 0097ce69db
commit dcedbaa074
13 files changed
+447 -975

No files matched your search

+105 -181
View File
@@ -47,8 +47,8 @@ public class AnthropicService
string webSearchVersion = null,
int maxIterations = 100,
string toolChoice = "any",
ReviewContext review = null,
FinalizerContext finalizer = null,
SubAgentContext review = null,
SubAgentContext finalizer = null,
bool allowUserChoice = true,
bool planMode = false,
CancellationToken cancellationToken = default)
@@ -85,30 +85,7 @@ public class AnthropicService
if (systemPrompt != null && systemPrompt.Count > 0)
payload["system"] = systemPrompt;
var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
var hasTools = hasWorkspace || includeMemory;
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
if (hasTools || hasWebSearch)
{
var tools = new List<object>();
if (hasTools)
{
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice);
if (planMode)
allTools = _toolService.FilterReadOnlyTools(allTools);
tools.AddRange(allTools);
}
if (hasWebSearch)
tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 });
payload["tools"] = tools;
// "any" erzwingt Tool-Nutzung, aber nur in der ersten Iteration,
// damit Claude nicht endlos Tools aufruft wenn die Aufgabe erledigt ist.
// Im Plan-Modus nie "any" erzwingen – Claude soll frei antworten können.
var effectiveChoice = (!planMode && toolChoice == "any" && iterations == 1) ? "any" : "auto";
payload["tool_choice"] = new { type = effectiveChoice };
}
AddToolsToPayload(payload, workspace, agentId, useMemory, webSearchVersion, toolChoice, iterations, allowUserChoice, planMode);
var client = CreateClient();
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
@@ -171,22 +148,7 @@ public class AnthropicService
}
else if (blockType == "web_search_tool_result")
{
// Sources aus dem Ergebnis-Block extrahieren und speichern
var sources = new List<object>();
if (el.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt) ? tt.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
var sources = ExtractWebSearchSources(el);
if (sources.Count > 0)
{
await _db.SaveMessage(new MessageRecord
@@ -268,20 +230,11 @@ public class AnthropicService
var toolResults = new List<object>();
foreach (var tr in executedTools)
{
// Tool-Results in der History auf 20.000 Zeichen kürzen.
// Das vollständige Ergebnis wurde bereits in der DB gespeichert.
// 20k Zeichen ≈ ~5k Tokens – verhindert Token-Explosion bei großen
// read_file / execute-Outputs die sich über alle weiteren Iterationen schleppen.
const int MaxToolResultChars = 20_000;
var content = tr.Result.Length > MaxToolResultChars
? tr.Result[..MaxToolResultChars] + $"\n\n[... gekürzt, {tr.Result.Length - MaxToolResultChars} weitere Zeichen ...]"
: tr.Result;
toolResults.Add(new
{
type = "tool_result",
tool_use_id = tr.Id,
content,
content = TruncateToolResult(tr.Result),
is_error = tr.IsError
});
}
@@ -331,7 +284,7 @@ public class AnthropicService
/// </summary>
public async Task<PreparerResult> RunPreparer(
HttpResponse response,
PreparerContext preparer,
SubAgentContext preparer,
List<object> historyMessages,
string chainId,
string workspace,
@@ -416,7 +369,7 @@ public class AnthropicService
/// </summary>
private async Task<ReviewResult> RunReview(
HttpResponse response,
ReviewContext review,
SubAgentContext review,
List<object> workerMessages,
string chainId,
string workspace,
@@ -490,7 +443,7 @@ public class AnthropicService
/// </summary>
private async Task<int> RunFinalizer(
HttpResponse response,
FinalizerContext finalizer,
SubAgentContext finalizer,
List<object> workerMessages,
string chainId,
string workspace,
@@ -686,22 +639,7 @@ public class AnthropicService
if (systemPrompt != null && systemPrompt.Count > 0)
payload["system"] = systemPrompt;
var hasWorkspace = !string.IsNullOrEmpty(agentWorkspace);
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
var hasTools = hasWorkspace || includeMemory;
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
if (hasTools || hasWebSearch)
{
var tools = new List<object>();
if (hasTools)
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: false));
if (hasWebSearch)
tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 });
payload["tools"] = tools;
var effectiveChoice = (agentToolChoice == "any" && iterations == 1) ? "any" : "auto";
payload["tool_choice"] = new { type = effectiveChoice };
}
AddToolsToPayload(payload, agentWorkspace, agentId, useMemory, webSearchVersion, agentToolChoice, iterations);
var client = CreateClient();
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
@@ -722,12 +660,7 @@ public class AnthropicService
var resp = JsonSerializer.Deserialize<JsonElement>(respBody);
if (resp.TryGetProperty("usage", out var u))
{
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
}
TrackUsage(usage, u);
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
var content = resp.GetProperty("content");
@@ -795,16 +728,11 @@ public class AnthropicService
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, cancellationToken); }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
const int MaxToolResultChars = 20_000;
var truncatedResult = toolResult.Length > MaxToolResultChars
? toolResult[..MaxToolResultChars] + $"\n\n[... gekürzt, {toolResult.Length - MaxToolResultChars} weitere Zeichen ...]"
: toolResult;
toolResults.Add(new
{
type = "tool_result",
tool_use_id = toolId,
content = truncatedResult,
content = TruncateToolResult(toolResult),
is_error = isError
});
@@ -834,23 +762,7 @@ public class AnthropicService
else if (blockType == "web_search_tool_result")
{
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
var sources = new List<object>();
if (block.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u2) ? u2.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt2) ? tt2.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
var sources = ExtractWebSearchSources(block);
if (sources.Count > 0)
{
await _db.SaveMessage(new MessageRecord
@@ -866,11 +778,7 @@ public class AnthropicService
}
else if (blockType == "server_tool_use")
{
var toolId = block.GetProperty("id").GetString() ?? "";
var toolName = block.GetProperty("name").GetString() ?? "";
JsonElement inputEl;
try { inputEl = block.GetProperty("input"); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
var (toolId, toolName, inputEl) = ParseServerToolUse(block);
assistantContent.Add(new { type = "server_tool_use", id = toolId, name = toolName, input = inputEl });
}
}
@@ -926,22 +834,7 @@ public class AnthropicService
if (ctx.SystemPrompt != null && ctx.SystemPrompt.Count > 0)
payload["system"] = ctx.SystemPrompt;
var hasWorkspace = !string.IsNullOrEmpty(ctx.Workspace);
var includeMemory = ctx.UseMemory && _memoryService != null && !string.IsNullOrEmpty(ctx.AgentId);
var hasTools = hasWorkspace || includeMemory;
var hasWebSearch = !string.IsNullOrEmpty(ctx.WebSearchVersion);
if (hasTools || hasWebSearch)
{
var tools = new List<object>();
if (hasTools)
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: false));
if (hasWebSearch)
tools.Add(new { type = ctx.WebSearchVersion, name = "web_search", max_uses = 5 });
payload["tools"] = tools;
var effectiveChoice = (ctx.ToolChoice == "any" && iterations == 1) ? "any" : "auto";
payload["tool_choice"] = new { type = effectiveChoice };
}
AddToolsToPayload(payload, ctx.Workspace, ctx.AgentId, ctx.UseMemory, ctx.WebSearchVersion, ctx.ToolChoice, iterations);
var client = CreateClient();
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
@@ -970,12 +863,7 @@ public class AnthropicService
// Usage tracken
if (resp.TryGetProperty("usage", out var u))
{
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
}
TrackUsage(usage, u);
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
var content = resp.GetProperty("content");
@@ -1021,17 +909,11 @@ public class AnthropicService
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId); }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
// Tool-Result in der History kürzen (wie bei StreamChat)
const int MaxToolResultChars = 20_000;
var truncatedResult = toolResult.Length > MaxToolResultChars
? toolResult[..MaxToolResultChars] + $"\n\n[... gekürzt, {toolResult.Length - MaxToolResultChars} weitere Zeichen ...]"
: toolResult;
toolResults.Add(new
{
type = "tool_result",
tool_use_id = toolId,
content = truncatedResult,
content = TruncateToolResult(toolResult),
is_error = isError
});
@@ -1047,21 +929,7 @@ public class AnthropicService
else if (blockType == "web_search_tool_result")
{
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
var sources = new List<object>();
if (block.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u2) ? u2.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt2) ? tt2.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
var sources = ExtractWebSearchSources(block);
if (sources.Count > 0)
{
await _db.SaveMessage(new MessageRecord
@@ -1074,11 +942,7 @@ public class AnthropicService
}
else if (blockType == "server_tool_use")
{
var toolId = block.GetProperty("id").GetString() ?? "";
var toolName = block.GetProperty("name").GetString() ?? "";
JsonElement inputEl;
try { inputEl = block.GetProperty("input"); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
var (toolId, toolName, inputEl) = ParseServerToolUse(block);
assistantContent.Add(new { type = "server_tool_use", id = toolId, name = toolName, input = inputEl });
}
}
@@ -1186,14 +1050,7 @@ public class AnthropicService
{
if (evt.TryGetProperty("message", out var msg) &&
msg.TryGetProperty("usage", out var u))
{
if (u.TryGetProperty("input_tokens", out var it))
usage.InputTokens += it.GetInt64();
if (u.TryGetProperty("cache_creation_input_tokens", out var cc))
usage.CacheCreationTokens += cc.GetInt64();
if (u.TryGetProperty("cache_read_input_tokens", out var cr))
usage.CacheReadTokens += cr.GetInt64();
}
TrackUsage(usage, u);
break;
}
@@ -1235,27 +1092,9 @@ public class AnthropicService
}
else if (cbType == "web_search_tool_result")
{
// Gesamten Block als JSON speichern für die Conversation-History
var rawBlock = JsonSerializer.Deserialize<JsonElement>(cb.GetRawText());
webSearchResultBlocks[idx] = rawBlock;
// Sources extrahieren für Frontend
var sources = new List<object>();
if (cb.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt) ? tt.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
webSearchSources[idx] = sources;
webSearchSources[idx] = ExtractWebSearchSources(cb);
}
break;
}
@@ -1452,6 +1291,91 @@ public class AnthropicService
return (stopReason, assistantContent, executedTools, streamErrorInfo);
}
// ── Shared Helpers ──
private const int MaxToolResultChars = 20_000;
private static string TruncateToolResult(string result)
{
return result.Length > MaxToolResultChars
? result[..MaxToolResultChars] + $"\n\n[... gekürzt, {result.Length - MaxToolResultChars} weitere Zeichen ...]"
: result;
}
/// <summary>
/// Extrahiert Web-Search-Sources aus einem web_search_tool_result-Block.
/// </summary>
private static List<object> ExtractWebSearchSources(JsonElement block)
{
var sources = new List<object>();
if (block.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt) ? tt.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
return sources;
}
/// <summary>
/// Parst einen server_tool_use-Block und gibt (id, name, input) zurück.
/// </summary>
private static (string id, string name, JsonElement input) ParseServerToolUse(JsonElement block)
{
var toolId = block.GetProperty("id").GetString() ?? "";
var toolName = block.GetProperty("name").GetString() ?? "";
JsonElement inputEl;
try { inputEl = block.GetProperty("input"); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
return (toolId, toolName, inputEl);
}
/// <summary>
/// Aktualisiert den TokenUsage-Tracker aus einem usage-JSON-Element.
/// </summary>
private static void TrackUsage(TokenUsage usage, JsonElement u)
{
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
}
/// <summary>
/// Fügt Tool-Definitionen und WebSearch zum Payload hinzu.
/// </summary>
private void AddToolsToPayload(Dictionary<string, object> payload, string workspace, string agentId, bool useMemory, string webSearchVersion, string toolChoice, int iteration, bool allowUserChoice = false, bool planMode = false)
{
var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
var hasTools = hasWorkspace || includeMemory;
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
if (!hasTools && !hasWebSearch) return;
var tools = new List<object>();
if (hasTools)
{
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice);
if (planMode)
allTools = _toolService.FilterReadOnlyTools(allTools);
tools.AddRange(allTools);
}
if (hasWebSearch)
tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 });
payload["tools"] = tools;
var effectiveChoice = (!planMode && toolChoice == "any" && iteration == 1) ? "any" : "auto";
payload["tool_choice"] = new { type = effectiveChoice };
}
private HttpClient CreateClient()
{
var client = _httpFactory.CreateClient();