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:
1 parent
f3069cedf7
commit
3e19da4d48
12 files changed
+428
-487
No files matched your search
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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}";
|
||||||
|
|||||||
Reference in new issue
Block a user