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:
1 parent
0097ce69db
commit
dcedbaa074
13 files changed
+447
-975
No files matched your search
+105
-181
@@ -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();
|
||||
|
||||
+55
-108
@@ -114,29 +114,18 @@ public class ChatService
|
||||
if (!string.IsNullOrWhiteSpace(systemPromptText))
|
||||
systemPrompt.Add(new { type = "text", text = systemPromptText });
|
||||
|
||||
// Preparer-Agent vorbereiten (falls konfiguriert)
|
||||
PreparerContext preparerContext = null;
|
||||
if (!string.IsNullOrEmpty(session.PreparerAgentId))
|
||||
// Sub-Agents vorbereiten (falls konfiguriert)
|
||||
var preparerContext = await BuildSubAgentContext(
|
||||
session.PreparerAgentId, sessionId, workspace, session, user);
|
||||
var reviewContext = await BuildSubAgentContext(
|
||||
session.ReviewAgentId, sessionId, workspace, session, user);
|
||||
var finalizerContext = await BuildSubAgentContext(
|
||||
session.FinalizerAgentId, sessionId, workspace, session, user);
|
||||
|
||||
if (preparerContext != null)
|
||||
{
|
||||
var preparerAgent = await _db.GetAgent(session.PreparerAgentId);
|
||||
if (preparerAgent != null)
|
||||
{
|
||||
var preparerModelId = ResolveModelId(preparerAgent.Model);
|
||||
var preparerMarked = await _db.GetMarkedMessages(sessionId);
|
||||
var preparerMemDir = _memoryService.GetAgentMemoryDir(preparerAgent.AgentId);
|
||||
var preparerSkills = await _db.GetAgentSkills(preparerAgent.AgentId);
|
||||
var preparerWorkspace = workspace;
|
||||
var preparerSystemText = SystemPromptBuilder.Build(
|
||||
preparerAgent, session, preparerMarked, _memoryService, preparerMemDir,
|
||||
preparerSkills, _skillService, userPersona: user?.GetEffectivePersona(),
|
||||
resolvedWorkspace: preparerWorkspace);
|
||||
|
||||
var preparerPrompt = new List<object>();
|
||||
if (!string.IsNullOrWhiteSpace(preparerSystemText))
|
||||
preparerPrompt.Add(new { type = "text", text = preparerSystemText });
|
||||
|
||||
// Preparer-Instruktion: erklärt Vorbereiter-Mechanismus
|
||||
preparerPrompt.Add(new { type = "text", text = @"## KRITISCH: Deine Rolle als Vorbereiter-Agent
|
||||
preparerContext.AllowUserChoice = preparerContext.Agent?.AllowUserChoice ?? false;
|
||||
preparerContext.SystemPrompt.Add(new { type = "text", text = @"## KRITISCH: Deine Rolle als Vorbereiter-Agent
|
||||
|
||||
Du bist ein Vorbereiter-Agent. Deine Aufgabe: Die Anfrage des Users aufbereiten, deine Fähigkeiten nutzen, und das Ergebnis an den Session-Agent weiterleiten.
|
||||
|
||||
@@ -165,95 +154,11 @@ Rückfrage (NUR mit [QUESTION] – stoppt die Weiterleitung):
|
||||
[QUESTION]
|
||||
Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
||||
```" });
|
||||
|
||||
var preparerHasTools = !string.IsNullOrEmpty(preparerWorkspace) || preparerAgent.UseMemory;
|
||||
preparerContext = new PreparerContext
|
||||
{
|
||||
ModelId = preparerModelId,
|
||||
SystemPrompt = preparerPrompt,
|
||||
AgentId = preparerAgent.AgentId,
|
||||
Workspace = preparerWorkspace,
|
||||
UseMemory = preparerAgent.UseMemory,
|
||||
WebSearchVersion = preparerAgent.WebSearchVersion,
|
||||
ToolChoice = preparerHasTools ? "any" : "auto",
|
||||
AllowUserChoice = preparerAgent.AllowUserChoice,
|
||||
Agent = preparerAgent
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Review-Agent vorbereiten (falls konfiguriert)
|
||||
ReviewContext reviewContext = null;
|
||||
if (!string.IsNullOrEmpty(session.ReviewAgentId))
|
||||
{
|
||||
var reviewAgent = await _db.GetAgent(session.ReviewAgentId);
|
||||
if (reviewAgent != null)
|
||||
{
|
||||
var reviewModelId = ResolveModelId(reviewAgent.Model);
|
||||
var reviewMarked = await _db.GetMarkedMessages(sessionId);
|
||||
var reviewMemDir = _memoryService.GetAgentMemoryDir(reviewAgent.AgentId);
|
||||
var reviewSkills = await _db.GetAgentSkills(reviewAgent.AgentId);
|
||||
var reviewWorkspace = workspace; // gleicher Workspace wie Worker
|
||||
var reviewSystemText = SystemPromptBuilder.Build(
|
||||
reviewAgent, session, reviewMarked, _memoryService, reviewMemDir,
|
||||
reviewSkills, _skillService, userPersona: user?.GetEffectivePersona(),
|
||||
resolvedWorkspace: reviewWorkspace);
|
||||
if (reviewContext != null)
|
||||
reviewContext.MaxReviewIterations = session.MaxReviewIterations > 0 ? session.MaxReviewIterations : 3;
|
||||
|
||||
var reviewPrompt = new List<object>();
|
||||
if (!string.IsNullOrWhiteSpace(reviewSystemText))
|
||||
reviewPrompt.Add(new { type = "text", text = reviewSystemText });
|
||||
|
||||
var reviewHasTools = !string.IsNullOrEmpty(reviewWorkspace) || reviewAgent.UseMemory;
|
||||
reviewContext = new ReviewContext
|
||||
{
|
||||
ModelId = reviewModelId,
|
||||
SystemPrompt = reviewPrompt,
|
||||
AgentId = reviewAgent.AgentId,
|
||||
Workspace = reviewWorkspace,
|
||||
UseMemory = reviewAgent.UseMemory,
|
||||
WebSearchVersion = reviewAgent.WebSearchVersion,
|
||||
ToolChoice = reviewHasTools ? "any" : "auto",
|
||||
MaxReviewIterations = session.MaxReviewIterations > 0 ? session.MaxReviewIterations : 3,
|
||||
Agent = reviewAgent
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Finalizer-Agent vorbereiten (falls konfiguriert)
|
||||
FinalizerContext finalizerContext = null;
|
||||
if (!string.IsNullOrEmpty(session.FinalizerAgentId))
|
||||
{
|
||||
var finalizerAgent = await _db.GetAgent(session.FinalizerAgentId);
|
||||
if (finalizerAgent != null)
|
||||
{
|
||||
var finalizerModelId = ResolveModelId(finalizerAgent.Model);
|
||||
var finalizerMarked = await _db.GetMarkedMessages(sessionId);
|
||||
var finalizerMemDir = _memoryService.GetAgentMemoryDir(finalizerAgent.AgentId);
|
||||
var finalizerSkills = await _db.GetAgentSkills(finalizerAgent.AgentId);
|
||||
var finalizerWorkspace = workspace;
|
||||
var finalizerSystemText = SystemPromptBuilder.Build(
|
||||
finalizerAgent, session, finalizerMarked, _memoryService, finalizerMemDir,
|
||||
finalizerSkills, _skillService, userPersona: user?.GetEffectivePersona(),
|
||||
resolvedWorkspace: finalizerWorkspace);
|
||||
|
||||
var finalizerPrompt = new List<object>();
|
||||
if (!string.IsNullOrWhiteSpace(finalizerSystemText))
|
||||
finalizerPrompt.Add(new { type = "text", text = finalizerSystemText });
|
||||
|
||||
var finalizerHasTools = !string.IsNullOrEmpty(finalizerWorkspace) || finalizerAgent.UseMemory;
|
||||
finalizerContext = new FinalizerContext
|
||||
{
|
||||
ModelId = finalizerModelId,
|
||||
SystemPrompt = finalizerPrompt,
|
||||
AgentId = finalizerAgent.AgentId,
|
||||
Workspace = finalizerWorkspace,
|
||||
UseMemory = finalizerAgent.UseMemory,
|
||||
WebSearchVersion = finalizerAgent.WebSearchVersion,
|
||||
ToolChoice = finalizerHasTools ? "any" : "auto",
|
||||
Agent = finalizerAgent
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new ChatContext
|
||||
{
|
||||
@@ -531,4 +436,46 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut einen SubAgentContext für Preparer/Review/Finalizer.
|
||||
/// Gemeinsame Logik: Agent laden, Model auflösen, SystemPrompt bauen, ToolChoice bestimmen.
|
||||
/// </summary>
|
||||
private async Task<SubAgentContext> BuildSubAgentContext(
|
||||
string agentId, string sessionId, string workspace, SessionRecord session, UserRecord user)
|
||||
{
|
||||
if (string.IsNullOrEmpty(agentId))
|
||||
return null;
|
||||
|
||||
var agent = await _db.GetAgent(agentId);
|
||||
if (agent == null)
|
||||
return null;
|
||||
|
||||
var modelId = ResolveModelId(agent.Model);
|
||||
var markedMessages = await _db.GetMarkedMessages(sessionId);
|
||||
var memDir = _memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||
var skills = await _db.GetAgentSkills(agent.AgentId);
|
||||
var systemText = SystemPromptBuilder.Build(
|
||||
agent, session, markedMessages, _memoryService, memDir,
|
||||
skills, _skillService, userPersona: user?.GetEffectivePersona(),
|
||||
resolvedWorkspace: workspace);
|
||||
|
||||
var prompt = new List<object>();
|
||||
if (!string.IsNullOrWhiteSpace(systemText))
|
||||
prompt.Add(new { type = "text", text = systemText });
|
||||
|
||||
var hasTools = !string.IsNullOrEmpty(workspace) || agent.UseMemory;
|
||||
return new SubAgentContext
|
||||
{
|
||||
ModelId = modelId,
|
||||
SystemPrompt = prompt,
|
||||
AgentId = agent.AgentId,
|
||||
Workspace = workspace,
|
||||
UseMemory = agent.UseMemory,
|
||||
WebSearchVersion = agent.WebSearchVersion,
|
||||
ToolChoice = hasTools ? "any" : "auto",
|
||||
Agent = agent
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -338,7 +338,7 @@ public class DatabaseService
|
||||
|
||||
foreach (var (agentId, avatar) in agentsWithoutThumb)
|
||||
{
|
||||
var small = ResizeDataUrlToWebP(avatar, smallSize, smallQuality);
|
||||
var small = ConvertDataUrlToWebP(avatar, smallQuality, maxSide: smallSize);
|
||||
if (small != null)
|
||||
conn.Execute("UPDATE agents SET avatar_small = @small WHERE agent_id = @agentId", new { small, agentId });
|
||||
}
|
||||
@@ -350,7 +350,7 @@ public class DatabaseService
|
||||
|
||||
foreach (var (sessionId, image) in sessionsWithoutThumb)
|
||||
{
|
||||
var small = ResizeDataUrlToWebP(image, smallSize, smallQuality);
|
||||
var small = ConvertDataUrlToWebP(image, smallQuality, maxSide: smallSize);
|
||||
if (small != null)
|
||||
conn.Execute("UPDATE sessions SET image_small = @small WHERE session_id = @sessionId", new { small, sessionId });
|
||||
}
|
||||
@@ -359,7 +359,10 @@ public class DatabaseService
|
||||
Console.WriteLine($"[Migration] Generated {agentsWithoutThumb.Count} agent thumbnails and {sessionsWithoutThumb.Count} session thumbnails");
|
||||
}
|
||||
|
||||
private static string ResizeDataUrlToWebP(string dataUrl, int maxSide, int quality)
|
||||
/// <summary>
|
||||
/// Konvertiert ein DataUrl-Bild zu WebP. Optional Resize wenn maxSide > 0.
|
||||
/// </summary>
|
||||
private static string ConvertDataUrlToWebP(string dataUrl, int quality, int maxSide = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -370,7 +373,7 @@ public class DatabaseService
|
||||
var bytes = Convert.FromBase64String(base64);
|
||||
|
||||
using var image = Image.Load(bytes);
|
||||
if (image.Width > maxSide || image.Height > maxSide)
|
||||
if (maxSide > 0 && (image.Width > maxSide || image.Height > maxSide))
|
||||
{
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
@@ -390,28 +393,6 @@ public class DatabaseService
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertDataUrlToWebP(string dataUrl, int quality)
|
||||
{
|
||||
try
|
||||
{
|
||||
var commaIndex = dataUrl.IndexOf(',');
|
||||
if (commaIndex < 0) return null;
|
||||
|
||||
var base64 = dataUrl.Substring(commaIndex + 1);
|
||||
var bytes = Convert.FromBase64String(base64);
|
||||
|
||||
using var image = Image.Load(bytes);
|
||||
using var ms = new MemoryStream();
|
||||
image.Save(ms, new WebpEncoder { Quality = quality });
|
||||
|
||||
return "data:image/webp;base64," + Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null; // Beschädigte Bilder überspringen
|
||||
}
|
||||
}
|
||||
|
||||
private void TryAddColumn(SqliteConnection conn, string table, string column, string type)
|
||||
{
|
||||
try { conn.Execute($"ALTER TABLE {table} ADD COLUMN {column} {type}"); }
|
||||
@@ -573,13 +554,6 @@ public class DatabaseService
|
||||
return session;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SessionRecord>> GetSessions(string userId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.QueryAsync<SessionRecord>(
|
||||
"SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC", new { userId });
|
||||
}
|
||||
|
||||
public async Task<List<SessionWithStats>> GetSessionsWithStats(string userId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
@@ -1303,23 +1277,6 @@ public class DatabaseService
|
||||
new { sessionId, chatId, now });
|
||||
}
|
||||
|
||||
public async Task SetSessionTelegramChatId(string sessionId, string chatId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE sessions SET telegram_chat_id = @chatId, updated_at = @now WHERE session_id = @sessionId",
|
||||
new { sessionId, chatId, now });
|
||||
}
|
||||
|
||||
public async Task ClearSessionTelegramChatId(string sessionId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE sessions SET telegram_chat_id = NULL, updated_at = @now WHERE session_id = @sessionId",
|
||||
new { sessionId, now = DateTime.UtcNow.ToString("o") });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SessionRecord>> GetSessionsByTelegramBot(string botId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
@@ -1345,14 +1302,6 @@ public class DatabaseService
|
||||
new { botId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SessionRecord>> GetUserSessions(string userId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.QueryAsync<SessionRecord>(
|
||||
"SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC",
|
||||
new { userId });
|
||||
}
|
||||
|
||||
// ── Cronjobs ──
|
||||
|
||||
public async Task<CronjobRecord> CreateCronjob(CronjobRecord cronjob)
|
||||
|
||||
@@ -62,4 +62,37 @@ public class ImageService
|
||||
|
||||
return "data:image/webp;base64," + webpBase64;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generiert ein WebP-Thumbnail (max 320px) mit Caching.
|
||||
/// Gibt den Pfad zum Thumbnail zurück, oder null bei Fehler.
|
||||
/// </summary>
|
||||
public static async System.Threading.Tasks.Task<string> GenerateThumbnail(string originalPath, string cacheDir, int maxSize = 320, int quality = 75)
|
||||
{
|
||||
var thumbFilename = Path.GetFileNameWithoutExtension(originalPath) + ".webp";
|
||||
var thumbPath = Path.Combine(cacheDir, thumbFilename);
|
||||
|
||||
if (File.Exists(thumbPath) && File.GetLastWriteTimeUtc(originalPath) <= File.GetLastWriteTimeUtc(thumbPath))
|
||||
return thumbPath;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
using var image = await Image.LoadAsync(originalPath);
|
||||
if (image.Width > maxSize || image.Height > maxSize)
|
||||
{
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(maxSize, maxSize),
|
||||
Mode = ResizeMode.Max
|
||||
}));
|
||||
}
|
||||
await image.SaveAsync(thumbPath, new WebpEncoder { Quality = quality });
|
||||
return thumbPath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user