refactor: Backend-Deduplizierung – Parameter-Objekte, Shared Helpers, KeyCheckDispatcher

- ToolDefinitionOptions + ToolExecutionContext ersetzen lange Parameterlisten in ToolService
- AppendToLastMessage-Helper in ChatService eliminiert 5x duplizierte Logik
- OpenAiService: ResolveProviderConfig + BuildApiMessages + ConvertToOpenAiTools statt doppeltem Code
- Tool-Definitionen aus einer Quelle (Anthropic→OpenAI Konvertierung statt hardcodierter Liste)
- KeyCheckDispatcher zentralisiert Key-Check-Dispatch (ApiKeysEndpoint, KeyScanner, GitHubEventScanner)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-03-19 08:01:49 +01:00
1 parent f3069cedf7
commit 3e19da4d48
12 files changed
+428 -487

No files matched your search

+42 -89
View File
@@ -182,72 +182,56 @@ public static class ApiKeysEndpoint
return; return;
} }
switch (existing.Provider) var check = await KeyCheckDispatcher.CheckByProvider(httpFactory, existing.Provider, existing.ApiKey);
if (check != null)
{ {
case "anthropic": await db.UpdateApiKeyMeta(keyId, check.DbStatus, check.Plan, check.Usage7d, check.Usage5h);
// Provider-spezifische Response-Details
if (check.ProviderResult is KeyCheckResult anthropic)
{ {
var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey);
var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h);
await ctx.Response.WriteAsJsonAsync(new await ctx.Response.WriteAsJsonAsync(new
{ {
status = dbStatus, status = check.DbStatus,
message = result.Message, message = check.Message,
plan = result.Plan, plan = check.Plan,
result.IsSubscription, anthropic.IsSubscription,
result.Usage7d, anthropic.Usage7d,
result.Usage5h, anthropic.Usage5h,
result.Overage, anthropic.Overage,
status7d = result.Status7d, status7d = anthropic.Status7d,
status5h = result.Status5h, status5h = anthropic.Status5h,
statusOverage = result.StatusOverage statusOverage = anthropic.StatusOverage
}); });
break;
} }
case "openai": else if (check.ProviderResult is StripeCheckResult stripe)
{ {
var result = await OpenAiKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey); await ctx.Response.WriteAsJsonAsync(new
var dbStatus = result.Status == "ok" ? "valid" : result.Status; {
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null); status = check.DbStatus,
await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message }); message = check.Message,
break; plan = check.Plan,
} stripe.AccountId,
case "google": stripe.AccountName,
{ stripe.AccountEmail,
var result = await GoogleKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey); stripe.AccountCountry,
var dbStatus = result.Status == "ok" ? "valid" : result.Status; stripe.Currency,
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null); stripe.BalanceAvailable,
await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message }); stripe.BalancePending,
break; stripe.ChargesEnabled,
} stripe.PayoutsEnabled
case "stripe":
{
var result = await StripeKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey);
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null);
await ctx.Response.WriteAsJsonAsync(new {
status = dbStatus,
message = result.Message,
plan = result.IsLiveKey ? "Live" : "Test",
result.AccountId,
result.AccountName,
result.AccountEmail,
result.AccountCountry,
result.Currency,
result.BalanceAvailable,
result.BalancePending,
result.ChargesEnabled,
result.PayoutsEnabled
}); });
break;
} }
default: else
{ {
ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { status = check.DbStatus, message = check.Message, plan = check.Plan });
await ctx.Response.WriteAsJsonAsync(new { error = $"Check für Provider '{existing.Provider}' nicht unterstützt" });
break;
} }
} }
else
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = $"Check für Provider '{existing.Provider}' nicht unterstützt" });
}
} }
private static async Task HandleCheckAll(HttpContext ctx, IHttpClientFactory httpFactory, DatabaseService db) private static async Task HandleCheckAll(HttpContext ctx, IHttpClientFactory httpFactory, DatabaseService db)
@@ -259,41 +243,10 @@ public static class ApiKeysEndpoint
foreach (var key in allKeys) foreach (var key in allKeys)
{ {
switch (key.Provider) var check = await KeyCheckDispatcher.CheckByProvider(httpFactory, key.Provider, key.ApiKey);
{ if (check == null) continue;
case "anthropic": await db.UpdateApiKeyMeta(key.KeyId, check.DbStatus, check.Plan, check.Usage7d, check.Usage5h);
{ results.Add(new { key.KeyId, key.Name, status = check.DbStatus, check.Plan, check.Usage5h, check.Usage7d });
var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h);
results.Add(new { key.KeyId, key.Name, status = dbStatus, result.Plan, result.Usage5h, result.Usage7d });
break;
}
case "openai":
{
var result = await OpenAiKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null);
results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null });
break;
}
case "google":
{
var result = await GoogleKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null);
results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null });
break;
}
case "stripe":
{
var result = await StripeKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null);
results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.IsLiveKey ? "Live" : "Test", Usage5h = (double?)null, Usage7d = (double?)null });
break;
}
}
} }
await ctx.Response.WriteAsJsonAsync(new { checked_ = results.Count, results }); await ctx.Response.WriteAsJsonAsync(new { checked_ = results.Count, results });
+6 -6
View File
@@ -298,12 +298,12 @@ public static class SessionEndpoint
var tools = new List<object>(); var tools = new List<object>();
if (hasTools) if (hasTools)
{ {
var allTools = toolService.GetToolDefinitions( var allTools = toolService.GetToolDefinitions(new ToolDefinitionOptions
includeFileTools: hasWorkspace, {
includeMemory: includeMemory, IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
includeUserChoice: agent.AllowUserChoice, IncludeUserChoice = agent.AllowUserChoice,
includeGitHubSearch: includeGitHubSearch, IncludeGitHubSearch = includeGitHubSearch, IncludeSerperSearch = includeSerperSearch
includeSerperSearch: includeSerperSearch); });
if (planMode) if (planMode)
allTools = toolService.FilterReadOnlyTools(allTools); allTools = toolService.FilterReadOnlyTools(allTools);
tools.AddRange(allTools); tools.AddRange(allTools);
+12 -4
View File
@@ -538,7 +538,12 @@ public partial class AnthropicService
{ {
toolResult = cb.ExecuteTool != null toolResult = cb.ExecuteTool != null
? await cb.ExecuteTool(toolName, inputEl, ct) ? await cb.ExecuteTool(toolName, inputEl, ct)
: await _toolService.ExecuteTool(toolName, inputEl, toolOpts.Workspace, _memoryService, toolOpts.AgentId, ct, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); : await _toolService.ExecuteTool(toolName, inputEl, new ToolExecutionContext
{
Workspace = toolOpts.Workspace, MemoryService = _memoryService,
AgentId = toolOpts.AgentId, CancellationToken = ct,
VectorSearch = _vectorSearch, EmbeddingBg = _embeddingBg
});
} }
catch (OperationCanceledException) { throw; } catch (OperationCanceledException) { throw; }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
@@ -656,9 +661,12 @@ public partial class AnthropicService
OnIteration = onIteration, OnIteration = onIteration,
EnqueueEmbeddings = true, EnqueueEmbeddings = true,
ExecuteTool = (name, input, ct) => ExecuteTool = (name, input, ct) =>
_toolService.ExecuteTool(name, input, ctx.Workspace, _memoryService, ctx.AgentId, ct, _toolService.ExecuteTool(name, input, new ToolExecutionContext
vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, {
chatService: _chatService, anthropicService: this, openAiService: _openAiService) Workspace = ctx.Workspace, MemoryService = _memoryService, AgentId = ctx.AgentId,
CancellationToken = ct, VectorSearch = _vectorSearch, EmbeddingBg = _embeddingBg,
Db = _db, ChatService = _chatService, AnthropicService = this, OpenAiService = _openAiService
})
}); });
var finalStopReason = text.Length > 0 ? "end_turn" : "error"; var finalStopReason = text.Length > 0 ? "end_turn" : "error";
+8 -1
View File
@@ -589,7 +589,14 @@ public partial class AnthropicService
string toolResult; string toolResult;
bool isError = false; bool isError = false;
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, openAiService: _openAiService, userId: userId, telegramService: _telegramService, apiToken: apiToken); } try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, new ToolExecutionContext
{
Workspace = workspace, MemoryService = _memoryService, AgentId = agentId,
CancellationToken = workCt, RemoteMgr = remoteMgr, SessionId = sessionId,
VectorSearch = _vectorSearch, EmbeddingBg = _embeddingBg, Db = _db,
ChatService = _chatService, AnthropicService = this, OpenAiService = _openAiService,
UserId = userId, TelegramService = _telegramService, ApiToken = apiToken
}); }
catch (OperationCanceledException) { throw; } catch (OperationCanceledException) { throw; }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
+7 -1
View File
@@ -261,7 +261,13 @@ public partial class AnthropicService
var tools = new List<object>(); var tools = new List<object>();
if (hasTools) if (hasTools)
{ {
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: opts.AllowUserChoice, includeRemote: opts.IncludeRemote, includeMemorySearch: includeMemorySearch, includeSendToSession: includeSendToSession, includeGitHubSearch: opts.IncludeGitHubSearch, includeSerperSearch: opts.IncludeSerperSearch); var allTools = _toolService.GetToolDefinitions(new ToolDefinitionOptions
{
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
IncludeUserChoice = opts.AllowUserChoice, IncludeRemote = opts.IncludeRemote,
IncludeMemorySearch = includeMemorySearch, IncludeSendToSession = includeSendToSession,
IncludeGitHubSearch = opts.IncludeGitHubSearch, IncludeSerperSearch = opts.IncludeSerperSearch
});
if (opts.PlanMode) if (opts.PlanMode)
allTools = _toolService.FilterReadOnlyTools(allTools); allTools = _toolService.FilterReadOnlyTools(allTools);
tools.AddRange(allTools); tools.AddRange(allTools);
+31 -79
View File
@@ -86,7 +86,12 @@ public class ChatService
var toolChoice = hasTools ? "any" : null; var toolChoice = hasTools ? "any" : null;
var toolDefs = hasTools var toolDefs = hasTools
? _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: agent.AllowUserChoice, includeRemote: hasRemoteConnection, includeGitHubSearch: includeGitHubSearch, includeSerperSearch: includeSerperSearch) ? _toolService.GetToolDefinitions(new ToolDefinitionOptions
{
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
IncludeUserChoice = agent.AllowUserChoice, IncludeRemote = hasRemoteConnection,
IncludeGitHubSearch = includeGitHubSearch, IncludeSerperSearch = includeSerperSearch
})
: Array.Empty<object>(); : Array.Empty<object>();
var toolNames = toolDefs.Length > 0 var toolNames = toolDefs.Length > 0
? JsonSerializer.Serialize(toolDefs.Select(td => ? JsonSerializer.Serialize(toolDefs.Select(td =>
@@ -370,21 +375,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
? (object)new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl, thought_signature = msg.ThoughtSignature } ? (object)new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl, thought_signature = msg.ThoughtSignature }
: new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl }; : new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl };
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null; AppendToLastMessage(history, "assistant", toolUseBlock);
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(toolUseBlock);
}
else
{
history.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = new List<object> { toolUseBlock }
});
}
} }
else if (msg.Role == "assistant" && msg.Type == "server_tool_use") else if (msg.Role == "assistant" && msg.Type == "server_tool_use")
{ {
@@ -393,21 +384,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
try { useEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); } try { useEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
catch { continue; } catch { continue; }
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null; AppendToLastMessage(history, "assistant", useEl);
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(useEl);
}
else
{
history.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = new List<object> { useEl }
});
}
} }
else if (msg.Role == "assistant" && msg.Type == "server_tool_result") else if (msg.Role == "assistant" && msg.Type == "server_tool_result")
{ {
@@ -417,21 +394,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
try { resultEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); } try { resultEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
catch { continue; } catch { continue; }
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null; AppendToLastMessage(history, "assistant", resultEl);
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 == "assistant" && msg.Type == "web_search_result") else if (msg.Role == "assistant" && msg.Type == "web_search_result")
{ {
@@ -445,42 +408,12 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
// Diese überspringen, da sie kein gültiger web_search_tool_result-Block sind // Diese überspringen, da sie kein gültiger web_search_tool_result-Block sind
if (resultEl.ValueKind == JsonValueKind.Array) continue; if (resultEl.ValueKind == JsonValueKind.Array) continue;
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null; AppendToLastMessage(history, "assistant", resultEl);
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 toolResultBlock = new { type = "tool_result", tool_use_id = msg.ToolCallId, content = SseHelper.TruncateToolResult(msg.Content ?? ""), is_error = msg.IsError };
if (lastUser is Dictionary<string, object> dict && AppendToLastMessage(history, "user", toolResultBlock);
dict.TryGetValue("role", out var role) && role.ToString() == "user" &&
dict.TryGetValue("content", out var existingContent) && existingContent is List<object> contentList)
{
contentList.Add(new { type = "tool_result", tool_use_id = msg.ToolCallId, content = SseHelper.TruncateToolResult(msg.Content ?? ""), is_error = msg.IsError });
}
else
{
history.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = new List<object>
{
new { type = "tool_result", tool_use_id = msg.ToolCallId, content = SseHelper.TruncateToolResult(msg.Content ?? ""), is_error = msg.IsError }
}
});
}
} }
} }
} }
@@ -630,6 +563,25 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
return history; return history;
} }
private static void AppendToLastMessage(List<object> history, string role, object block)
{
var last = history.Count > 0 ? history[history.Count - 1] : null;
if (last is Dictionary<string, object> dict &&
dict.TryGetValue("role", out var r) && r.ToString() == role &&
dict.TryGetValue("content", out var existing) && existing is List<object> contentList)
{
contentList.Add(block);
}
else
{
history.Add(new Dictionary<string, object>
{
["role"] = role,
["content"] = new List<object> { block }
});
}
}
/// <summary> /// <summary>
/// Entfernt verwaiste web_search-Blöcke aus assistant-Messages: /// Entfernt verwaiste web_search-Blöcke aus assistant-Messages:
/// - server_tool_use ohne zugehörigen web_search_tool_result (z.B. alte DB-Einträge) /// - server_tool_use ohne zugehörigen web_search_tool_result (z.B. alte DB-Einträge)
+7 -22
View File
@@ -414,29 +414,14 @@ public class GitHubEventScannerService : BackgroundService
try try
{ {
if (KeyPatterns.IsAnthropic(type)) var check = await KeyCheckDispatcher.CheckByKeyType(_httpFactory, type, sk.ApiKey);
if (check != null)
{ {
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, check.DbStatus, check.Plan, check.Usage7d, check.Usage5h);
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; var checkMsg = check.Usage7d.HasValue
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h); ? $"🔍 {typeName} geprüft: {check.DbStatus} · Plan: {check.Plan} · {keyPrefix}"
var checkMsg = $"🔍 {typeName} geprüft: {status} · Plan: {result.Plan} · {keyPrefix}"; : $"🔍 {typeName} geprüft: {check.DbStatus} · {check.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, plan = result.Plan, usage_7d = result.Usage7d, usage_5h = result.Usage5h, source_url = sk.SourceUrl })); AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status = check.DbStatus, plan = check.Plan, usage_7d = check.Usage7d, usage_5h = check.Usage5h, message = check.Message, source_url = sk.SourceUrl }));
}
else if (type == "openai_project" || type == "openai_service" || type == "openai_legacy")
{
var result = await OpenAiKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else if (type == "google_api")
{
var result = await GoogleKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
} }
else else
{ {
+96
View File
@@ -0,0 +1,96 @@
using System.Net.Http;
using System.Threading.Tasks;
namespace Gott.Services;
/// <summary>
/// Normalisiertes Key-Check-Ergebnis für alle Provider.
/// Provider-spezifische Details über ProviderResult (cast zum konkreten Typ).
/// </summary>
public class UnifiedKeyCheckResult
{
/// <summary>Normalisierter DB-Status: "valid", "invalid", "rate_limited", "quota_exceeded", "error"</summary>
public string DbStatus { get; set; }
public string Message { get; set; }
public string Plan { get; set; }
public double? Usage7d { get; set; }
public double? Usage5h { get; set; }
/// <summary>Original-Result des Providers (KeyCheckResult, OpenAiCheckResult, etc.)</summary>
public object ProviderResult { get; set; }
}
/// <summary>
/// Zentraler Dispatcher für Key-Checks aller Provider.
/// Normalisiert den Status und gibt ein einheitliches Ergebnis zurück.
/// Eliminiert die wiederholten switch/if-Ketten in ApiKeysEndpoint, KeyScannerService, GitHubEventScannerService.
/// </summary>
public static class KeyCheckDispatcher
{
/// <summary>
/// Prüft einen API-Key anhand des Providers.
/// Provider: "anthropic", "openai", "google", "stripe".
/// Gibt null zurück bei unbekanntem Provider.
/// </summary>
public static async Task<UnifiedKeyCheckResult> CheckByProvider(IHttpClientFactory httpFactory, string provider, string apiKey)
{
switch (provider)
{
case "anthropic":
{
var r = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, apiKey);
return new UnifiedKeyCheckResult
{
DbStatus = (r.Status == "ok" || r.Status == "usage_limited") ? "valid" : r.Status,
Message = r.Message, Plan = r.Plan,
Usage7d = r.Usage7d, Usage5h = r.Usage5h,
ProviderResult = r
};
}
case "openai":
{
var r = await OpenAiKeyChecker.CheckKeyAsync(httpFactory, apiKey);
return new UnifiedKeyCheckResult
{
DbStatus = r.Status == "ok" ? "valid" : r.Status,
Message = r.Message, Plan = r.Message,
ProviderResult = r
};
}
case "google":
{
var r = await GoogleKeyChecker.CheckKeyAsync(httpFactory, apiKey);
return new UnifiedKeyCheckResult
{
DbStatus = r.Status == "ok" ? "valid" : r.Status,
Message = r.Message, Plan = r.Message,
ProviderResult = r
};
}
case "stripe":
{
var r = await StripeKeyChecker.CheckKeyAsync(httpFactory, apiKey);
return new UnifiedKeyCheckResult
{
DbStatus = r.Status == "ok" ? "valid" : r.Status,
Message = r.Message, Plan = r.IsLiveKey ? "Live" : "Test",
ProviderResult = r
};
}
default:
return null;
}
}
/// <summary>
/// Prüft einen API-Key anhand des Key-Typs (z.B. "openai_project", "anthropic_oauth").
/// Nutzt KeyPatterns.GetCheckerProvider() für die Zuordnung.
/// Gibt null zurück wenn kein Checker für den Typ existiert.
/// </summary>
public static Task<UnifiedKeyCheckResult> CheckByKeyType(IHttpClientFactory httpFactory, string keyType, string apiKey)
{
var provider = KeyPatterns.GetCheckerProvider(keyType);
if (provider == null) return Task.FromResult<UnifiedKeyCheckResult>(null);
return CheckByProvider(httpFactory, provider, apiKey);
}
}
+15
View File
@@ -1,6 +1,8 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Gott.Services; namespace Gott.Services;
@@ -102,6 +104,19 @@ public static class KeyPatterns
return type == "anthropic_oauth" || type == "anthropic_api" || type == "anthropic_unknown"; return type == "anthropic_oauth" || type == "anthropic_api" || type == "anthropic_unknown";
} }
/// <summary>
/// Mappt einen Key-Typ auf den Provider-Namen für den Checker.
/// Gibt null zurück wenn kein Checker verfügbar ist.
/// </summary>
public static string GetCheckerProvider(string type)
{
if (IsAnthropic(type)) return "anthropic";
if (type == "openai_project" || type == "openai_service" || type == "openai_legacy") return "openai";
if (type == "google_api") return "google";
if (type == "stripe_live" || type == "stripe_test") return "stripe";
return null;
}
/// <summary>Menschenlesbarer Name für den Key-Typ.</summary> /// <summary>Menschenlesbarer Name für den Key-Typ.</summary>
public static string DisplayName(string type) public static string DisplayName(string type)
{ {
+7 -30
View File
@@ -965,37 +965,14 @@ public class KeyScannerService : BackgroundService
try try
{ {
if (KeyPatterns.IsAnthropic(type)) var check = await KeyCheckDispatcher.CheckByKeyType(_httpFactory, type, sk.ApiKey);
if (check != null)
{ {
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, check.DbStatus, check.Plan, check.Usage7d, check.Usage5h);
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; var checkMsg = check.Usage7d.HasValue
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h); ? $"🔍 {typeName} geprüft: {check.DbStatus} · Plan: {check.Plan} · Usage 7d: {check.Usage7d?.ToString("F2") ?? "n/a"} · Usage 5h: {check.Usage5h?.ToString("F2") ?? "n/a"} · {keyPrefix}"
var checkMsg = $"🔍 {typeName} geprüft: {status} · Plan: {result.Plan} · Usage 7d: {result.Usage7d?.ToString("F2") ?? "n/a"} · Usage 5h: {result.Usage5h?.ToString("F2") ?? "n/a"} · {keyPrefix}"; : $"🔍 {typeName} geprüft: {check.DbStatus} · {check.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, plan = result.Plan, usage_7d = result.Usage7d, usage_5h = result.Usage5h, source_url = sk.SourceUrl })); AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status = check.DbStatus, plan = check.Plan, usage_7d = check.Usage7d, usage_5h = check.Usage5h, message = check.Message, source_url = sk.SourceUrl }));
}
else if (type == "openai_project" || type == "openai_service" || type == "openai_legacy")
{
var result = await OpenAiKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else if (type == "google_api")
{
var result = await GoogleKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else if (type == "stripe_live" || type == "stripe_test")
{
var result = await StripeKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, is_live = result.IsLiveKey, account_id = result.AccountId, account_name = result.AccountName, account_email = result.AccountEmail, country = result.AccountCountry, balance_available = result.BalanceAvailable, balance_pending = result.BalancePending, currency = result.Currency, charges_enabled = result.ChargesEnabled, payouts_enabled = result.PayoutsEnabled, source_url = sk.SourceUrl }));
} }
else else
{ {
+86 -175
View File
@@ -30,123 +30,38 @@ public class OpenAiService
// Provider-Definitionen (inkl. API-Keys, instanzgebunden) // Provider-Definitionen (inkl. API-Keys, instanzgebunden)
private readonly Dictionary<string, (string BaseUrl, string ApiKey)> Providers; private readonly Dictionary<string, (string BaseUrl, string ApiKey)> Providers;
// Tools die für OpenAI-kompatible Provider verfügbar sind // Tools die für OpenAI-kompatible Provider verfügbar sind (konvertiert aus ToolService Anthropic-Format)
private static readonly List<object> OpenAiTools = new() private List<object> _openAiToolDefs;
/// <summary>
/// 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)
{ {
new var result = new List<object>();
foreach (var tool in anthropicTools)
{ {
type = "function", var json = JsonSerializer.Serialize(tool);
function = new var el = JsonSerializer.Deserialize<JsonElement>(json);
var name = el.TryGetProperty("name", out var n) ? n.GetString() : "";
var desc = el.TryGetProperty("description", out var d) ? d.GetString() : "";
var schema = el.TryGetProperty("input_schema", out var s) ? s : default;
result.Add(new
{ {
name = "list_files", type = "function",
description = "Listet Dateien und Ordner in einem Verzeichnis auf.", function = new
parameters = new
{ {
type = "object", name,
properties = new Dictionary<string, object> description = desc,
{ parameters = schema.ValueKind != JsonValueKind.Undefined
["path"] = new { type = "string", description = "Relativer Verzeichnispfad (leer = Root)" } ? JsonSerializer.Deserialize<object>(schema.GetRawText())
}, : new { type = "object", properties = new Dictionary<string, object>(), required = Array.Empty<string>() } as object
required = Array.Empty<string>()
} }
} });
},
new
{
type = "function",
function = new
{
name = "read_file",
description = "Liest den Inhalt einer Datei. Standardmäßig werden maximal 2000 Zeilen gelesen. Bei großen Dateien offset und limit verwenden.",
parameters = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Dateipfad" },
["offset"] = new { type = "integer", description = "Erste Zeile die gelesen werden soll (1-basiert, Default: 1)" },
["limit"] = new { type = "integer", description = "Maximale Anzahl Zeilen die gelesen werden sollen (Default: 2000)" }
},
required = new[] { "path" }
}
}
},
new
{
type = "function",
function = new
{
name = "write_file",
description = "Erstellt oder überschreibt eine Datei mit dem angegebenen Inhalt.",
parameters = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Dateipfad" },
["content"] = new { type = "string", description = "Dateiinhalt" }
},
required = new[] { "path", "content" }
}
}
},
new
{
type = "function",
function = new
{
name = "edit_file",
description = "Ersetzt einen exakten Textabschnitt in einer Datei. oldText muss exakt matchen.",
parameters = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Dateipfad" },
["old_text"] = new { type = "string", description = "Exakter Text der ersetzt werden soll" },
["new_text"] = new { type = "string", description = "Neuer Text" }
},
required = new[] { "path", "old_text", "new_text" }
}
}
},
new
{
type = "function",
function = new
{
name = "delete_file",
description = "Löscht eine Datei oder ein leeres Verzeichnis.",
parameters = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Pfad zur Datei oder zum Verzeichnis" }
},
required = new[] { "path" }
}
}
},
new
{
type = "function",
function = new
{
name = "execute",
description = "Führt einen Shell-Befehl im Workspace-Verzeichnis aus. Gibt stdout und stderr zurück.",
parameters = new
{
type = "object",
properties = new Dictionary<string, object>
{
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" }
},
required = new[] { "command" }
}
}
} }
}; return result;
}
public record ProviderModel(string Id, string DisplayName); public record ProviderModel(string Id, string DisplayName);
@@ -172,6 +87,9 @@ public class OpenAiService
["gemini"] = ("https://generativelanguage.googleapis.com/v1beta/openai", ""), // Key kommt dynamisch aus Settings ["gemini"] = ("https://generativelanguage.googleapis.com/v1beta/openai", ""), // Key kommt dynamisch aus Settings
["openai"] = ("https://api.openai.com/v1", "") // Key kommt dynamisch aus DB ["openai"] = ("https://api.openai.com/v1", "") // Key kommt dynamisch aus DB
}; };
// Tool-Definitionen aus ToolService konvertieren (single source of truth)
_openAiToolDefs = ConvertTo_openAiToolDefs(toolService.GetToolDefinitions(new ToolDefinitionOptions { IncludeFileTools = true }));
} }
/// <summary> /// <summary>
@@ -347,10 +265,10 @@ public class OpenAiService
Model = modelString Model = modelString
}; };
var (provider, modelId) = ParseModelString(modelString); var resolved = ResolveProviderConfig(modelString, apiKeyOverride);
if (!Providers.TryGetValue(provider, out var providerConfig)) if (resolved == null)
{ {
var errMsg = $"Unbekannter Provider: '{provider}'"; var errMsg = $"Unbekannter Provider: '{ParseModelString(modelString).provider}'";
await _db.SaveMessage(new MessageRecord await _db.SaveMessage(new MessageRecord
{ {
ChainId = chainId, Iteration = 1, ChainId = chainId, Iteration = 1,
@@ -362,39 +280,11 @@ public class OpenAiService
return; return;
} }
// API-Key Override: User-Key aus DB hat Vorrang vor hardcoded Provider-Key var (provider, modelId, providerConfig) = resolved.Value;
if (!string.IsNullOrEmpty(apiKeyOverride))
providerConfig = (providerConfig.BaseUrl, apiKeyOverride);
// Gemini: Key dynamisch aus ServerSettings holen (falls nicht per Override gesetzt)
if (provider == "gemini" && string.IsNullOrEmpty(providerConfig.ApiKey))
providerConfig = (providerConfig.BaseUrl, _settings.Get("GeminiApiKey"));
try try
{ {
// OpenAI-kompatibles Messages-Format aufbauen var apiMessages = BuildApiMessages(systemPromptText, messages);
var apiMessages = new List<object>();
// System-Prompt als erste System-Message
if (!string.IsNullOrEmpty(systemPromptText))
apiMessages.Add(new { role = "system", content = systemPromptText });
// History-Messages konvertieren (Anthropic-Format → OpenAI-Format)
foreach (var msg in messages)
{
var converted = ConvertMessage(msg);
if (converted == null) continue;
// Tool-Results kommen als List<object> zurück → einzeln expandieren
if (converted is List<object> toolResultList)
{
foreach (var tr in toolResultList)
apiMessages.Add(tr);
continue;
}
apiMessages.Add(converted);
}
// Agentic Loop // Agentic Loop
while (iteration < maxIterations) while (iteration < maxIterations)
@@ -409,7 +299,7 @@ public class OpenAiService
["messages"] = apiMessages, ["messages"] = apiMessages,
["stream"] = true, ["stream"] = true,
["max_tokens"] = 16384, ["max_tokens"] = 16384,
["tools"] = OpenAiTools, ["tools"] = _openAiToolDefs,
["tool_choice"] = "auto" ["tool_choice"] = "auto"
}; };
@@ -665,7 +555,7 @@ public class OpenAiService
try { inputEl = JsonSerializer.Deserialize<JsonElement>(tcArgs.ToString()); } try { inputEl = JsonSerializer.Deserialize<JsonElement>(tcArgs.ToString()); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); } catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
var toolResult = await _toolService.ExecuteTool(tcName, inputEl, workspace); var toolResult = await _toolService.ExecuteTool(tcName, inputEl, new ToolExecutionContext { Workspace = workspace });
var isError = toolResult.StartsWith("Fehler:") || toolResult.StartsWith("Error:"); var isError = toolResult.StartsWith("Fehler:") || toolResult.StartsWith("Error:");
// Tool-End SSE // Tool-End SSE
@@ -746,17 +636,11 @@ public class OpenAiService
int iterations = 0; int iterations = 0;
var finalText = new StringBuilder(); var finalText = new StringBuilder();
var (provider, modelId) = ParseModelString(ctx.ModelId); var resolved = ResolveProviderConfig(ctx.ModelId, ctx.ApiToken);
if (!Providers.TryGetValue(provider, out var providerConfig)) if (resolved == null)
throw new Exception($"Unbekannter Provider: '{provider}'"); throw new Exception($"Unbekannter Provider: '{ParseModelString(ctx.ModelId).provider}'");
// API-Key Override: User-Key hat Vorrang var (provider, modelId, providerConfig) = resolved.Value;
if (!string.IsNullOrEmpty(ctx.ApiToken))
providerConfig = (providerConfig.BaseUrl, ctx.ApiToken);
// Gemini: Key dynamisch aus ServerSettings
if (provider == "gemini" && string.IsNullOrEmpty(providerConfig.ApiKey))
providerConfig = (providerConfig.BaseUrl, _settings.Get("GeminiApiKey"));
var chain = new ChainRecord var chain = new ChainRecord
{ {
@@ -765,23 +649,7 @@ public class OpenAiService
Model = ctx.ModelId Model = ctx.ModelId
}; };
// Messages in OpenAI-Format konvertieren var apiMessages = BuildApiMessages(ctx.SystemPromptText, messages);
var apiMessages = new List<object>();
if (!string.IsNullOrEmpty(ctx.SystemPromptText))
apiMessages.Add(new { role = "system", content = ctx.SystemPromptText });
foreach (var msg in messages)
{
var converted = ConvertMessage(msg);
if (converted == null) continue;
if (converted is List<object> toolResultList)
{
foreach (var tr in toolResultList)
apiMessages.Add(tr);
continue;
}
apiMessages.Add(converted);
}
try try
{ {
@@ -797,7 +665,7 @@ public class OpenAiService
["messages"] = apiMessages, ["messages"] = apiMessages,
["stream"] = false, ["stream"] = false,
["max_tokens"] = 16384, ["max_tokens"] = 16384,
["tools"] = OpenAiTools, ["tools"] = _openAiToolDefs,
["tool_choice"] = "auto" ["tool_choice"] = "auto"
}; };
@@ -896,7 +764,7 @@ public class OpenAiService
string toolResult; string toolResult;
bool isError = false; bool isError = false;
try { toolResult = await _toolService.ExecuteTool(tcName, inputEl, ctx.Workspace); } try { toolResult = await _toolService.ExecuteTool(tcName, inputEl, new ToolExecutionContext { Workspace = ctx.Workspace }); }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
toolResults.Add(new toolResults.Add(new
@@ -1028,6 +896,49 @@ public class OpenAiService
return null; return null;
} }
/// <summary>
/// Löst Provider-Config auf: Provider-Name, Model-ID, BaseUrl + ApiKey.
/// Berücksichtigt API-Key-Override und dynamische Gemini-Keys.
/// </summary>
private (string provider, string modelId, (string BaseUrl, string ApiKey) config)? ResolveProviderConfig(string modelString, string apiKeyOverride = null)
{
var (provider, modelId) = ParseModelString(modelString);
if (!Providers.TryGetValue(provider, out var providerConfig))
return null;
if (!string.IsNullOrEmpty(apiKeyOverride))
providerConfig = (providerConfig.BaseUrl, apiKeyOverride);
if (provider == "gemini" && string.IsNullOrEmpty(providerConfig.ApiKey))
providerConfig = (providerConfig.BaseUrl, _settings.Get("GeminiApiKey"));
return (provider, modelId, providerConfig);
}
/// <summary>
/// Konvertiert Anthropic-Format Messages in OpenAI-kompatibles Format.
/// </summary>
private static List<object> BuildApiMessages(string systemPromptText, List<object> messages)
{
var apiMessages = new List<object>();
if (!string.IsNullOrEmpty(systemPromptText))
apiMessages.Add(new { role = "system", content = systemPromptText });
foreach (var msg in messages)
{
var converted = ConvertMessage(msg);
if (converted == null) continue;
if (converted is List<object> toolResultList)
{
foreach (var tr in toolResultList)
apiMessages.Add(tr);
continue;
}
apiMessages.Add(converted);
}
return apiMessages;
}
private Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw) private Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw)
=> SseHelper.FinalizeChain(_db, chain, usage, iterations, stopReason, sw); => SseHelper.FinalizeChain(_db, chain, usage, iterations, stopReason, sw);
+111 -80
View File
@@ -15,6 +15,37 @@ using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
namespace Gott.Services; namespace Gott.Services;
public class ToolDefinitionOptions
{
public bool IncludeFileTools { get; set; }
public bool IncludeMemory { get; set; }
public bool IncludeUserChoice { get; set; } = true;
public bool IncludeRemote { get; set; }
public bool IncludeMemorySearch { get; set; }
public bool IncludeSendToSession { get; set; }
public bool IncludeGitHubSearch { get; set; }
public bool IncludeSerperSearch { get; set; }
}
public class ToolExecutionContext
{
public string Workspace { get; set; }
public MemoryService MemoryService { get; set; }
public string AgentId { get; set; }
public CancellationToken CancellationToken { get; set; }
public RemoteConnectionManager RemoteMgr { get; set; }
public string SessionId { get; set; }
public VectorSearchService VectorSearch { get; set; }
public EmbeddingBackgroundService EmbeddingBg { get; set; }
public DatabaseService Db { get; set; }
public ChatService ChatService { get; set; }
public AnthropicService AnthropicService { get; set; }
public OpenAiService OpenAiService { get; set; }
public string UserId { get; set; }
public TelegramService TelegramService { get; set; }
public string ApiToken { get; set; }
}
public class ToolService public class ToolService
{ {
private readonly ServerSettingsService _settings; private readonly ServerSettingsService _settings;
@@ -63,24 +94,24 @@ public class ToolService
return filtered.ToArray(); return filtered.ToArray();
} }
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false, bool includeSendToSession = false, bool includeGitHubSearch = false, bool includeSerperSearch = false) public object[] GetToolDefinitions(ToolDefinitionOptions opts)
{ {
var allTools = new List<object>(); var allTools = new List<object>();
if (includeUserChoice) if (opts.IncludeUserChoice)
allTools.Add(GetAskUserChoiceTool()); allTools.Add(GetAskUserChoiceTool());
if (includeFileTools) if (opts.IncludeFileTools)
allTools.AddRange(GetFileTools()); allTools.AddRange(GetFileTools());
if (includeMemory) if (opts.IncludeMemory)
allTools.AddRange(GetMemoryToolDefinitions()); allTools.AddRange(GetMemoryToolDefinitions());
if (includeMemorySearch) if (opts.IncludeMemorySearch)
allTools.Add(GetMemorySearchTool()); allTools.Add(GetMemorySearchTool());
if (includeRemote) if (opts.IncludeRemote)
allTools.Add(GetRemoteExecuteTool()); allTools.Add(GetRemoteExecuteTool());
if (includeSendToSession) if (opts.IncludeSendToSession)
allTools.Add(GetSendToSessionTool()); allTools.Add(GetSendToSessionTool());
if (includeGitHubSearch) if (opts.IncludeGitHubSearch)
allTools.Add(GetGitHubCodeSearchTool()); allTools.Add(GetGitHubCodeSearchTool());
if (includeSerperSearch) if (opts.IncludeSerperSearch)
allTools.Add(GetSerperWebSearchTool()); allTools.Add(GetSerperWebSearchTool());
return allTools.ToArray(); return allTools.ToArray();
} }
@@ -390,17 +421,17 @@ public class ToolService
}; };
} }
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null, DatabaseService db = null, ChatService chatService = null, AnthropicService anthropicService = null, OpenAiService openAiService = null, string userId = null, TelegramService telegramService = null, string apiToken = null) public async Task<string> ExecuteTool(string name, JsonElement input, ToolExecutionContext ctx)
{ {
switch (name) switch (name)
{ {
case "read_file": case "read_file":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); var path = ResolvePath(input.GetProperty("path").GetString() ?? "", ctx.Workspace);
if (!File.Exists(path)) if (!File.Exists(path))
return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}"; return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(ctx.Workspace, path)}";
var allLines = File.ReadAllLines(path); var allLines = File.ReadAllLines(path);
var totalLines = allLines.Length; var totalLines = allLines.Length;
var offset = input.TryGetProperty("offset", out var offsetProp) ? Math.Max(1, offsetProp.GetInt32()) : 1; var offset = input.TryGetProperty("offset", out var offsetProp) ? Math.Max(1, offsetProp.GetInt32()) : 1;
@@ -417,22 +448,22 @@ public class ToolService
} }
case "write_file": case "write_file":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); var path = ResolvePath(input.GetProperty("path").GetString() ?? "", ctx.Workspace);
var dir = Path.GetDirectoryName(path); var dir = Path.GetDirectoryName(path);
if (dir != null) Directory.CreateDirectory(dir); if (dir != null) Directory.CreateDirectory(dir);
var content = input.GetProperty("content").GetString() ?? ""; var content = input.GetProperty("content").GetString() ?? "";
File.WriteAllText(path, content); File.WriteAllText(path, content);
return $"Datei geschrieben: {WorkspaceHelper.GetRelativePath(workspace, path)} ({content.Length} Zeichen)"; return $"Datei geschrieben: {WorkspaceHelper.GetRelativePath(ctx.Workspace, path)} ({content.Length} Zeichen)";
} }
case "edit_file": case "edit_file":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); var path = ResolvePath(input.GetProperty("path").GetString() ?? "", ctx.Workspace);
if (!File.Exists(path)) if (!File.Exists(path))
return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}"; return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(ctx.Workspace, path)}";
var fileContent = File.ReadAllText(path); var fileContent = File.ReadAllText(path);
var oldText = input.GetProperty("old_text").GetString() ?? ""; var oldText = input.GetProperty("old_text").GetString() ?? "";
var newText = input.GetProperty("new_text").GetString() ?? ""; var newText = input.GetProperty("new_text").GetString() ?? "";
@@ -440,42 +471,42 @@ public class ToolService
return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig."; return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig.";
fileContent = fileContent.Replace(oldText, newText); fileContent = fileContent.Replace(oldText, newText);
File.WriteAllText(path, fileContent); File.WriteAllText(path, fileContent);
return $"Datei bearbeitet: {WorkspaceHelper.GetRelativePath(workspace, path)}"; return $"Datei bearbeitet: {WorkspaceHelper.GetRelativePath(ctx.Workspace, path)}";
} }
case "list_files": case "list_files":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "", workspace); var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "", ctx.Workspace);
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return $"Fehler: Verzeichnis nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}"; return $"Fehler: Verzeichnis nicht gefunden: {WorkspaceHelper.GetRelativePath(ctx.Workspace, path)}";
var entries = new List<string>(); var entries = new List<string>();
foreach (var dir in Directory.GetDirectories(path)) foreach (var dir in Directory.GetDirectories(path))
entries.Add(WorkspaceHelper.GetRelativePath(workspace, dir) + "/"); entries.Add(WorkspaceHelper.GetRelativePath(ctx.Workspace, dir) + "/");
foreach (var file in Directory.GetFiles(path)) foreach (var file in Directory.GetFiles(path))
entries.Add(WorkspaceHelper.GetRelativePath(workspace, file)); entries.Add(WorkspaceHelper.GetRelativePath(ctx.Workspace, file));
return entries.Count == 0 ? "(leer)" : string.Join("\n", entries); return entries.Count == 0 ? "(leer)" : string.Join("\n", entries);
} }
case "delete_file": case "delete_file":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); var path = ResolvePath(input.GetProperty("path").GetString() ?? "", ctx.Workspace);
if (File.Exists(path)) if (File.Exists(path))
{ {
File.Delete(path); File.Delete(path);
return $"Datei gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}"; return $"Datei gelöscht: {WorkspaceHelper.GetRelativePath(ctx.Workspace, path)}";
} }
if (Directory.Exists(path)) if (Directory.Exists(path))
{ {
Directory.Delete(path, recursive: false); Directory.Delete(path, recursive: false);
return $"Verzeichnis gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}"; return $"Verzeichnis gelöscht: {WorkspaceHelper.GetRelativePath(ctx.Workspace, path)}";
} }
return $"Fehler: Nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}"; return $"Fehler: Nicht gefunden: {WorkspaceHelper.GetRelativePath(ctx.Workspace, path)}";
} }
case "execute": case "execute":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var command = input.GetProperty("command").GetString() ?? ""; var command = input.GetProperty("command").GetString() ?? "";
// Unter Windows: Befehl in temp .cmd-Datei schreiben und mit /c ausführen. // Unter Windows: Befehl in temp .cmd-Datei schreiben und mit /c ausführen.
@@ -484,7 +515,7 @@ public class ToolService
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
FileName = WorkspaceHelper.ShellExecutable, FileName = WorkspaceHelper.ShellExecutable,
WorkingDirectory = workspace, WorkingDirectory = ctx.Workspace,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, UseShellExecute = false,
@@ -513,7 +544,7 @@ public class ToolService
} }
// Timeout UND externen CancellationToken verknüpfen // Timeout UND externen CancellationToken verknüpfen
var timeoutSeconds = ExecuteTimeoutSeconds; var timeoutSeconds = ExecuteTimeoutSeconds;
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctx.CancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
// stdout/stderr VOR WaitForExit lesen – verhindert Deadlock // stdout/stderr VOR WaitForExit lesen – verhindert Deadlock
// wenn Pipe-Buffer volllaufen (Windows: nur 4KB!) // wenn Pipe-Buffer volllaufen (Windows: nur 4KB!)
@@ -528,7 +559,7 @@ public class ToolService
proc.Kill(entireProcessTree: true); proc.Kill(entireProcessTree: true);
if (scriptPath != null) try { File.Delete(scriptPath); } catch { } if (scriptPath != null) try { File.Delete(scriptPath); } catch { }
// Wenn der externe Token abgebrochen wurde, weiterwerfen // Wenn der externe Token abgebrochen wurde, weiterwerfen
cancellationToken.ThrowIfCancellationRequested(); ctx.CancellationToken.ThrowIfCancellationRequested();
return $"Fehler: Timeout nach {timeoutSeconds} Sekunden - Prozess abgebrochen"; return $"Fehler: Timeout nach {timeoutSeconds} Sekunden - Prozess abgebrochen";
} }
if (scriptPath != null) try { File.Delete(scriptPath); } catch { } if (scriptPath != null) try { File.Delete(scriptPath); } catch { }
@@ -547,7 +578,7 @@ public class ToolService
} }
case "background_execute": case "background_execute":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var command = input.GetProperty("command").GetString() ?? ""; var command = input.GetProperty("command").GetString() ?? "";
// Befehl in temporäre Script-Datei schreiben und losgelöst starten. // Befehl in temporäre Script-Datei schreiben und losgelöst starten.
@@ -572,7 +603,7 @@ public class ToolService
{ {
FileName = WorkspaceHelper.IsWindows ? "cmd.exe" : scriptPath, FileName = WorkspaceHelper.IsWindows ? "cmd.exe" : scriptPath,
Arguments = WorkspaceHelper.IsWindows ? $"/c \"{scriptPath}\"" : "", Arguments = WorkspaceHelper.IsWindows ? $"/c \"{scriptPath}\"" : "",
WorkingDirectory = workspace, WorkingDirectory = ctx.Workspace,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true
}; };
@@ -582,13 +613,13 @@ public class ToolService
} }
case "glob": case "glob":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var pattern = input.GetProperty("pattern").GetString() ?? "*"; var pattern = input.GetProperty("pattern").GetString() ?? "*";
var basePath = input.TryGetProperty("path", out var gp) ? gp.GetString() ?? "" : ""; var basePath = input.TryGetProperty("path", out var gp) ? gp.GetString() ?? "" : "";
var searchRoot = string.IsNullOrEmpty(basePath) var searchRoot = string.IsNullOrEmpty(basePath)
? workspace ? ctx.Workspace
: ResolvePath(basePath, workspace); : ResolvePath(basePath, ctx.Workspace);
if (!Directory.Exists(searchRoot)) if (!Directory.Exists(searchRoot))
return $"Fehler: Verzeichnis nicht gefunden: {basePath}"; return $"Fehler: Verzeichnis nicht gefunden: {basePath}";
var matcher = new Matcher(); var matcher = new Matcher();
@@ -596,14 +627,14 @@ public class ToolService
var dirInfo = new DirectoryInfoWrapper(new DirectoryInfo(searchRoot)); var dirInfo = new DirectoryInfoWrapper(new DirectoryInfo(searchRoot));
var result = matcher.Execute(dirInfo); var result = matcher.Execute(dirInfo);
var matches = result.Files var matches = result.Files
.Select(f => WorkspaceHelper.GetRelativePath(workspace, Path.Combine(searchRoot, f.Path))) .Select(f => WorkspaceHelper.GetRelativePath(ctx.Workspace, Path.Combine(searchRoot, f.Path)))
.OrderBy(f => f) .OrderBy(f => f)
.ToList(); .ToList();
return matches.Count == 0 ? "(keine Treffer)" : string.Join("\n", matches); return matches.Count == 0 ? "(keine Treffer)" : string.Join("\n", matches);
} }
case "grep": case "grep":
{ {
if (string.IsNullOrEmpty(workspace)) if (string.IsNullOrEmpty(ctx.Workspace))
return "Fehler: Kein Workspace konfiguriert"; return "Fehler: Kein Workspace konfiguriert";
var pattern = input.GetProperty("pattern").GetString() ?? ""; var pattern = input.GetProperty("pattern").GetString() ?? "";
var searchPath = input.TryGetProperty("path", out var sp) ? sp.GetString() ?? "" : ""; var searchPath = input.TryGetProperty("path", out var sp) ? sp.GetString() ?? "" : "";
@@ -614,8 +645,8 @@ public class ToolService
try { regex = new Regex(pattern, regexOptions); } try { regex = new Regex(pattern, regexOptions); }
catch { regex = new Regex(Regex.Escape(pattern), regexOptions); } catch { regex = new Regex(Regex.Escape(pattern), regexOptions); }
var searchRoot = string.IsNullOrEmpty(searchPath) var searchRoot = string.IsNullOrEmpty(searchPath)
? workspace ? ctx.Workspace
: ResolvePath(searchPath, workspace); : ResolvePath(searchPath, ctx.Workspace);
IEnumerable<string> files; IEnumerable<string> files;
if (File.Exists(searchRoot)) if (File.Exists(searchRoot))
files = new[] { searchRoot }; files = new[] { searchRoot };
@@ -632,7 +663,7 @@ public class ToolService
for (var i = 0; i < lines.Length; i++) for (var i = 0; i < lines.Length; i++)
{ {
if (regex.IsMatch(lines[i])) if (regex.IsMatch(lines[i]))
results.Add($"{WorkspaceHelper.GetRelativePath(workspace, file)}:{i + 1}: {lines[i].Trim()}"); results.Add($"{WorkspaceHelper.GetRelativePath(ctx.Workspace, file)}:{i + 1}: {lines[i].Trim()}");
} }
} }
if (results.Count == 0) return "(keine Treffer)"; if (results.Count == 0) return "(keine Treffer)";
@@ -657,51 +688,51 @@ public class ToolService
} }
case "memory_read": case "memory_read":
{ {
if (memoryService == null || string.IsNullOrEmpty(agentId)) if (ctx.MemoryService == null || string.IsNullOrEmpty(ctx.AgentId))
return "Fehler: Memory-Service nicht verfügbar"; return "Fehler: Memory-Service nicht verfügbar";
var path = input.GetProperty("path").GetString() ?? ""; var path = input.GetProperty("path").GetString() ?? "";
var content = memoryService.ReadMemoryFile(agentId, path); var content = ctx.MemoryService.ReadMemoryFile(ctx.AgentId, path);
if (content == null) if (content == null)
return $"Datei nicht gefunden: {path}"; return $"Datei nicht gefunden: {path}";
return content; return content;
} }
case "memory_write": case "memory_write":
{ {
if (memoryService == null || string.IsNullOrEmpty(agentId)) if (ctx.MemoryService == null || string.IsNullOrEmpty(ctx.AgentId))
return "Fehler: Memory-Service nicht verfügbar"; return "Fehler: Memory-Service nicht verfügbar";
var path = input.GetProperty("path").GetString() ?? ""; var path = input.GetProperty("path").GetString() ?? "";
var content = input.GetProperty("content").GetString() ?? ""; var content = input.GetProperty("content").GetString() ?? "";
var append = input.TryGetProperty("append", out var a) && a.GetBoolean(); var append = input.TryGetProperty("append", out var a) && a.GetBoolean();
if (append) if (append)
memoryService.AppendMemoryFile(agentId, path, content); ctx.MemoryService.AppendMemoryFile(ctx.AgentId, path, content);
else else
memoryService.WriteMemoryFile(agentId, path, content); ctx.MemoryService.WriteMemoryFile(ctx.AgentId, path, content);
// Memory-Datei nach Schreiben embedden // Memory-Datei nach Schreiben embedden
if (embeddingBg != null) if (ctx.EmbeddingBg != null)
{ {
var fullText = memoryService.ReadMemoryFile(agentId, path); var fullText = ctx.MemoryService.ReadMemoryFile(ctx.AgentId, path);
if (!string.IsNullOrEmpty(fullText)) if (!string.IsNullOrEmpty(fullText))
embeddingBg.EnqueueMemoryFile(agentId, path, fullText); ctx.EmbeddingBg.EnqueueMemoryFile(ctx.AgentId, path, fullText);
} }
return $"Memory-Datei {(append ? "erweitert" : "geschrieben")}: {path} ({content.Length} Zeichen)"; return $"Memory-Datei {(append ? "erweitert" : "geschrieben")}: {path} ({content.Length} Zeichen)";
} }
case "memory_list": case "memory_list":
{ {
if (memoryService == null || string.IsNullOrEmpty(agentId)) if (ctx.MemoryService == null || string.IsNullOrEmpty(ctx.AgentId))
return "Fehler: Memory-Service nicht verfügbar"; return "Fehler: Memory-Service nicht verfügbar";
var subPath = input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; var subPath = input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "";
var files = memoryService.ListMemoryFiles(agentId, subPath); var files = ctx.MemoryService.ListMemoryFiles(ctx.AgentId, subPath);
return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files); return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files);
} }
case "memory_search": case "memory_search":
{ {
if (vectorSearch == null) if (ctx.VectorSearch == null)
return "Fehler: Semantische Suche nicht verfügbar"; return "Fehler: Semantische Suche nicht verfügbar";
var query = input.GetProperty("query").GetString() ?? ""; var query = input.GetProperty("query").GetString() ?? "";
var topN = input.TryGetProperty("top_n", out var tn) ? tn.GetInt32() : 10; var topN = input.TryGetProperty("top_n", out var tn) ? tn.GetInt32() : 10;
if (topN > 30) topN = 30; if (topN > 30) topN = 30;
if (topN < 1) topN = 1; if (topN < 1) topN = 1;
var results = await vectorSearch.SearchAsync(query, agentId, topN); var results = await ctx.VectorSearch.SearchAsync(query, ctx.AgentId, topN);
if (results.Count == 0) if (results.Count == 0)
return "Keine relevanten Ergebnisse gefunden."; return "Keine relevanten Ergebnisse gefunden.";
var sb = new StringBuilder(); var sb = new StringBuilder();
@@ -718,13 +749,13 @@ public class ToolService
} }
case "remote_execute": case "remote_execute":
{ {
if (remoteMgr == null || string.IsNullOrEmpty(sessionId)) if (ctx.RemoteMgr == null || string.IsNullOrEmpty(ctx.SessionId))
return "Fehler: Keine Remote-Verbindung verfügbar"; return "Fehler: Keine Remote-Verbindung verfügbar";
var conn = remoteMgr.GetBySessionId(sessionId); var conn = ctx.RemoteMgr.GetBySessionId(ctx.SessionId);
if (conn == null) if (conn == null)
return "Fehler: Keine Remote-Verbindung für diese Session zugeordnet"; return "Fehler: Keine Remote-Verbindung für diese Session zugeordnet";
var command = input.GetProperty("command").GetString() ?? ""; var command = input.GetProperty("command").GetString() ?? "";
var result = await remoteMgr.ExecuteCommand(conn.Id, command, ExecuteTimeoutSeconds, cancellationToken); var result = await ctx.RemoteMgr.ExecuteCommand(conn.Id, command, ExecuteTimeoutSeconds, ctx.CancellationToken);
var sb = new StringBuilder(); var sb = new StringBuilder();
if (!string.IsNullOrEmpty(result.Output)) if (!string.IsNullOrEmpty(result.Output))
sb.Append(result.Output); sb.Append(result.Output);
@@ -742,7 +773,7 @@ public class ToolService
} }
case "send_to_session": case "send_to_session":
{ {
if (db == null || chatService == null || (anthropicService == null && openAiService == null)) if (ctx.Db == null || ctx.ChatService == null || (ctx.AnthropicService == null && ctx.OpenAiService == null))
return "Fehler: send_to_session nicht verfügbar"; return "Fehler: send_to_session nicht verfügbar";
var sessionIdOrName = input.GetProperty("session_id_or_name").GetString() ?? ""; var sessionIdOrName = input.GetProperty("session_id_or_name").GetString() ?? "";
@@ -753,62 +784,62 @@ public class ToolService
return "Fehler: message fehlt"; return "Fehler: message fehlt";
// Ziel-Session per ID oder Name suchen (instanzweit, kein User-Check) // Ziel-Session per ID oder Name suchen (instanzweit, kein User-Check)
var targetSession = await db.GetSession(sessionIdOrName); var targetSession = await ctx.Db.GetSession(sessionIdOrName);
if (targetSession == null) if (targetSession == null)
targetSession = await db.GetSessionByName(sessionIdOrName); targetSession = await ctx.Db.GetSessionByName(sessionIdOrName);
if (targetSession == null) if (targetSession == null)
return $"Fehler: Session '{sessionIdOrName}' nicht gefunden"; return $"Fehler: Session '{sessionIdOrName}' nicht gefunden";
var targetSessionId = targetSession.SessionId; var targetSessionId = targetSession.SessionId;
if (targetSessionId == sessionId) if (targetSessionId == ctx.SessionId)
return "Fehler: Kann keine Nachricht an die eigene Session senden"; return "Fehler: Kann keine Nachricht an die eigene Session senden";
// Context für Ziel-Session vorbereiten (analog zu CronjobService) // Context für Ziel-Session vorbereiten (analog zu CronjobService)
var ctx = await chatService.PrepareContext(targetSessionId, userId: userId); var targetCtx = await ctx.ChatService.PrepareContext(targetSessionId, userId: ctx.UserId);
if (ctx == null) if (targetCtx == null)
return $"Fehler: Session '{targetSession.Name}' ist nicht konfiguriert (kein Agent zugewiesen?)"; return $"Fehler: Session '{targetSession.Name}' ist nicht konfiguriert (kein Agent zugewiesen?)";
// API-Key des Ziel-Agents auflösen (nicht den Key des aufrufenden Agents nutzen) // API-Key des Ziel-Agents auflösen (nicht den Key des aufrufenden Agents nutzen)
var targetApiKey = await db.GetAgentApiKeyRecord(userId, ctx.AgentId); var targetApiKey = await ctx.Db.GetAgentApiKeyRecord(ctx.UserId, targetCtx.AgentId);
ctx.ApiToken = targetApiKey?.ApiKey ?? apiToken; targetCtx.ApiToken = targetApiKey?.ApiKey ?? ctx.ApiToken;
// History aufbauen – User-Message NICHT in DB speichern (wie Cronjob) // History aufbauen – User-Message NICHT in DB speichern (wie Cronjob)
// Stattdessen als kontextualisierte Nachricht in die History injizieren // Stattdessen als kontextualisierte Nachricht in die History injizieren
var history = await chatService.BuildHistoryFromDb(targetSessionId, promptBlocks: ctx.PromptBlocks); var history = await ctx.ChatService.BuildHistoryFromDb(targetSessionId, promptBlocks: targetCtx.PromptBlocks);
var senderAgent = agentId != null ? await db.GetAgent(agentId) : null; var senderAgent = ctx.AgentId != null ? await ctx.Db.GetAgent(ctx.AgentId) : null;
var senderName = senderAgent?.Name ?? agentId ?? "Unbekannter Agent"; var senderName = senderAgent?.Name ?? ctx.AgentId ?? "Unbekannter Agent";
var senderInfo = $"[Nachricht von Agent \"{senderName}\"]: {message}"; var senderInfo = $"[Nachricht von Agent \"{senderName}\"]: {message}";
var userContent = new List<object> { new { type = "text", text = senderInfo } }; var userContent = new List<object> { new { type = "text", text = senderInfo } };
history.Add(new Dictionary<string, object> { ["role"] = "user", ["content"] = userContent }); history.Add(new Dictionary<string, object> { ["role"] = "user", ["content"] = userContent });
try try
{ {
if (!string.IsNullOrEmpty(ctx.Provider) && openAiService != null) if (!string.IsNullOrEmpty(targetCtx.Provider) && ctx.OpenAiService != null)
await openAiService.ChatNonStreaming(ctx, history); await ctx.OpenAiService.ChatNonStreaming(targetCtx, history);
else else
await anthropicService.ChatNonStreaming(ctx, history); await ctx.AnthropicService.ChatNonStreaming(targetCtx, history);
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[send_to_session] ChatNonStreaming Fehler: {ex}"); Console.WriteLine($"[send_to_session] ChatNonStreaming Fehler: {ex}");
return $"Fehler beim Senden an Session \"{targetSession.Name}\": {ex.Message}"; return $"Fehler beim Senden an Session \"{targetSession.Name}\": {ex.Message}";
} }
await db.UpdateSessionTimestamp(targetSessionId); await ctx.Db.UpdateSessionTimestamp(targetSessionId);
// Telegram-Benachrichtigung senden falls Session einen Bot hat // Telegram-Benachrichtigung senden falls Session einen Bot hat
if (telegramService != null) if (ctx.TelegramService != null)
{ {
try try
{ {
var updatedSession = await db.GetSession(targetSessionId); var updatedSession = await ctx.Db.GetSession(targetSessionId);
if (updatedSession != null && !string.IsNullOrEmpty(updatedSession.TelegramChatId)) if (updatedSession != null && !string.IsNullOrEmpty(updatedSession.TelegramChatId))
{ {
var recentMsgs = await db.GetLatestActiveMessages(targetSessionId, 5); var recentMsgs = await ctx.Db.GetLatestActiveMessages(targetSessionId, 5);
var lastMsg = recentMsgs var lastMsg = recentMsgs
.Where(m => m.Role == "assistant" && m.Type == "text") .Where(m => m.Role == "assistant" && m.Type == "text")
.LastOrDefault(); .LastOrDefault();
if (lastMsg != null) if (lastMsg != null)
await telegramService.SendNotification(targetSessionId, lastMsg.Content); await ctx.TelegramService.SendNotification(targetSessionId, lastMsg.Content);
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -821,11 +852,11 @@ public class ToolService
} }
case "github_code_search": case "github_code_search":
{ {
return await ExecuteGitHubCodeSearch(input, cancellationToken); return await ExecuteGitHubCodeSearch(input, ctx.CancellationToken);
} }
case "serper_web_search": case "serper_web_search":
{ {
return await ExecuteSerperWebSearch(input, cancellationToken); return await ExecuteSerperWebSearch(input, ctx.CancellationToken);
} }
default: default:
return $"Fehler: Unbekanntes Tool: {name}"; return $"Fehler: Unbekanntes Tool: {name}";