Provider-Abstraktion: Agents unterstützen jetzt OpenAI + Anthropic

Backend:
- ChatEndpoint: Globaler OpenAI-Fallback entfernt, Key-Check pro Provider
- TelegramService/CronjobService: Provider-Weiche statt hardcoded Anthropic
- AnthropicService: Preparer/Review/Finalizer sind Provider-aware
- OpenAiService: Neue ChatNonStreaming()-Methode für agentic loop
- ToolService: send_to_session lädt Key des Ziel-Agents
- SubAgentContext: Provider-Feld hinzugefügt
- ChatService: BuildSubAgentContext setzt Provider

Frontend:
- Key-Switch-Modal: 'Kein Key'-Option zum Entfernen der Zuweisung
- ApiKeysEndpoint: keyId='none' löscht Key-Zuweisung
This commit is contained in:
Meleki committed 2026-03-14 22:59:33 +01:00
1 parent 1effc6d00f
commit 02f5fe2faf
11 files changed
+422 -69

No files matched your search

+16 -8
View File
@@ -524,6 +524,22 @@ public static class ApiKeysEndpoint
var userId = ctx.GetUserId();
if (userId == null) { ctx.Response.StatusCode = 401; return; }
var agentId = ctx.Request.Query["agentId"].FirstOrDefault();
if (string.IsNullOrEmpty(agentId))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "agentId required" });
return;
}
// "none" = Key-Zuweisung entfernen
if (keyId == "none")
{
await db.SetAgentApiKey(userId, agentId, null);
await ctx.Response.WriteAsJsonAsync(new { ok = true, keyId = (string)null, agentId, name = "Kein Key" });
return;
}
var key = await db.GetApiKey(keyId);
if (key == null)
{
@@ -540,14 +556,6 @@ public static class ApiKeysEndpoint
return;
}
var agentId = ctx.Request.Query["agentId"].FirstOrDefault();
if (string.IsNullOrEmpty(agentId))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "agentId required" });
return;
}
await db.SetAgentApiKey(userId, agentId, keyId);
await ctx.Response.WriteAsJsonAsync(new { ok = true, keyId, agentId, name = key.Name });
}
+29 -28
View File
@@ -62,14 +62,8 @@ public static class ChatEndpoint
return;
}
// API-Key des Users für diesen Agent auflösen
// API-Key des Users für diesen Agent auflösen (kann null sein)
var userApiKey = await db.GetAgentApiKeyRecord(userId, ctx.AgentId);
if (userApiKey == null)
{
res.StatusCode = 400;
await res.WriteAsJsonAsync(new { error = "Kein API-Key für diesen Agent zugewiesen. Bitte weise einen Key zu." });
return;
}
if (isChoiceResponse)
{
@@ -146,12 +140,21 @@ public static class ChatEndpoint
// ── Preparer-Agent Phase (vor dem Worker) ──
if (ctx.Preparer != null && !isChoiceResponse)
{
var preparerApiToken = userApiKey?.ApiKey;
// Anthropic-Preparer braucht zwingend einen Key, OpenAI-Provider können ohne auskommen
if (string.IsNullOrEmpty(ctx.Preparer.Provider) && string.IsNullOrEmpty(preparerApiToken))
{
res.StatusCode = 400;
await res.WriteAsJsonAsync(new { error = "Kein API-Key für diesen Agent zugewiesen. Bitte weise einen Key zu." });
return;
}
// Preparer bekommt History MIT seinen eigenen vorherigen Messages (Tag "preparer"),
// damit er den Kontext seiner Rückfragen kennt
var preparerHistory = await chatService.BuildHistoryFromDb(sessionId, includeTag: "preparer", promptBlocks: ctx.PromptBlocks);
var preparerResult = await anthropic.RunPreparer(
res, ctx.Preparer, preparerHistory, ctx.ChainId, ctx.Workspace, sessionId, userApiKey.ApiKey, cancellationToken);
res, ctx.Preparer, preparerHistory, ctx.ChainId, ctx.Workspace, sessionId, preparerApiToken, cancellationToken);
if (!preparerResult.ShouldForward)
{
@@ -219,33 +222,31 @@ public static class ChatEndpoint
{
if (!string.IsNullOrEmpty(ctx.Provider))
{
// OpenAI-kompatibler Provider – für "openai" den User-Key aus DB verwenden
string openAiKeyOverride = null;
if (ctx.Provider == "openai")
{
// Zuerst prüfen ob der ausgewählte Key ein OpenAI-Key ist
if (userApiKey.Provider == "openai")
openAiKeyOverride = userApiKey.ApiKey;
else
{
// Fallback: ersten aktiven OpenAI-Key aus DB holen
openAiKeyOverride = await db.GetFirstActiveKeyByProvider("openai");
}
// OpenAI-kompatibler Provider – Key des Users für diesen Agent verwenden
string openAiKeyOverride = userApiKey?.ApiKey;
if (string.IsNullOrEmpty(openAiKeyOverride))
{
await SseHelper.TryWriteSse(res, "error", new { error = "Kein OpenAI API-Key vorhanden. Bitte einen OpenAI-Key in den API-Keys hinzufügen." }, sseCt);
await SseHelper.TryWriteSse(res, "done", new { chainId = ctx.ChainId }, sseCt);
return;
}
// OpenAI-Provider braucht zwingend einen Key
if (ctx.Provider == "openai" && string.IsNullOrEmpty(openAiKeyOverride))
{
await SseHelper.TryWriteSse(res, "error", new { error = "Kein API-Key für diesen Agent zugewiesen. Bitte einen OpenAI-Key zuweisen." }, sseCt);
await SseHelper.TryWriteSse(res, "done", new { chainId = ctx.ChainId }, sseCt);
return;
}
await openAi.StreamChat(res, ctx.ModelId, ctx.SystemPromptText, messages, ctx.ChainId, workspace: ctx.Workspace, sessionId: sessionId, agentId: ctx.AgentId, apiKeyOverride: openAiKeyOverride, workCt: workCt, sseCt: sseCt);
}
else
{
// Standard: Anthropic
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: userApiKey.ApiKey, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
// Standard: Anthropic – Key wird zwingend benötigt
var anthropicToken = userApiKey?.ApiKey;
if (string.IsNullOrEmpty(anthropicToken))
{
await SseHelper.TryWriteSse(res, "error", new { error = "Kein API-Key für diesen Agent zugewiesen. Bitte weise einen Key zu." }, sseCt);
await SseHelper.TryWriteSse(res, "done", new { chainId = ctx.ChainId }, sseCt);
return;
}
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: anthropicToken, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
}
}
finally
+1
View File
@@ -36,6 +36,7 @@ public class ChatContext
public class SubAgentContext
{
public string ModelId { get; set; }
public string Provider { get; set; } // "" = Anthropic, "nvidia"/"openai"/etc. = OpenAI-kompatibel
public List<object> SystemPrompt { get; set; }
public string AgentId { get; set; }
public string Workspace { get; set; }
+2
View File
@@ -96,6 +96,7 @@ builder.Services.AddSingleton<TelegramService>(sp =>
sp.GetRequiredService<IHttpClientFactory>(),
dbService, chatService,
sp.GetRequiredService<AnthropicService>(),
sp.GetRequiredService<OpenAiService>(),
serverSettings));
// Krea Image Generation
@@ -132,6 +133,7 @@ app.Use(async (context, next) =>
toolService.SetHttpClientFactory(app.Services.GetRequiredService<IHttpClientFactory>());
app.Services.GetRequiredService<AnthropicService>().SetTelegramService(app.Services.GetRequiredService<TelegramService>());
app.Services.GetRequiredService<AnthropicService>().SetOpenAiService(app.Services.GetRequiredService<OpenAiService>());
await app.Services.GetRequiredService<OpenAiService>().LoadModelsAsync();
app.UseWebSockets();
+101 -22
View File
@@ -50,6 +50,12 @@ public class AnthropicService
_telegramService = telegramService;
}
private OpenAiService _openAiService;
public void SetOpenAiService(OpenAiService openAiService)
{
_openAiService = openAiService;
}
/// <summary>
/// Liest Usage-Header aus einer Anthropic-Response und aktualisiert die api_keys-Tabelle.
/// Fire-and-forget – Fehler werden stillschweigend ignoriert.
@@ -474,12 +480,25 @@ public class AnthropicService
var preparerMessages = new List<object>(historyMessages);
var usage = new TokenUsage();
var (text, iterations) = await RunAgentChat(
response, preparer.ModelId, preparer.SystemPrompt, preparer.AgentId,
preparer.Workspace, preparer.UseMemory, preparer.WebSearchVersion,
preparer.ToolChoice, preparerMessages, chainId, sessionId, apiToken, usage,
0, "preparer", preparer.Agent?.Name ?? "Vorbereiter-Agent",
cancellationToken, cancellationToken);
string text;
int iterations;
if (!string.IsNullOrEmpty(preparer.Provider) && _openAiService != null)
{
// OpenAI-kompatibler Preparer
var subCtx = BuildSubAgentChatContext(preparer, chainId, sessionId, apiToken);
text = await _openAiService.ChatNonStreaming(subCtx, preparerMessages);
iterations = 1;
}
else
{
(text, iterations) = await RunAgentChat(
response, preparer.ModelId, preparer.SystemPrompt, preparer.AgentId,
preparer.Workspace, preparer.UseMemory, preparer.WebSearchVersion,
preparer.ToolChoice, preparerMessages, chainId, sessionId, apiToken, usage,
0, "preparer", preparer.Agent?.Name ?? "Vorbereiter-Agent",
cancellationToken, cancellationToken);
}
var result = new PreparerResult { IterationCount = iterations, ResponseText = text };
@@ -566,13 +585,24 @@ public class AnthropicService
// Review-Agent bekommt eigene Message-History
var reviewMessages = BuildReviewMessages(workerMessages);
// Review-Agent iteriert mit eigenen Tools
var (reviewText, reviewIterations) = await RunAgentChat(
response, review.ModelId, review.SystemPrompt, review.AgentId,
workspace, review.UseMemory, review.WebSearchVersion,
review.ToolChoice, reviewMessages, chainId, sessionId, apiToken, usage,
baseIterations, "review", review.Agent?.Name ?? "Review-Agent",
workCt, sseCt);
string reviewText;
int reviewIterations;
if (!string.IsNullOrEmpty(review.Provider) && _openAiService != null)
{
var subCtx = BuildSubAgentChatContext(review, chainId, sessionId, apiToken);
reviewText = await _openAiService.ChatNonStreaming(subCtx, reviewMessages);
reviewIterations = 1;
}
else
{
(reviewText, reviewIterations) = await RunAgentChat(
response, review.ModelId, review.SystemPrompt, review.AgentId,
workspace, review.UseMemory, review.WebSearchVersion,
review.ToolChoice, reviewMessages, chainId, sessionId, apiToken, usage,
baseIterations, "review", review.Agent?.Name ?? "Review-Agent",
workCt, sseCt);
}
var result = new ReviewResult { ReviewIterationCount = reviewIterations };
@@ -636,12 +666,24 @@ public class AnthropicService
// Finalizer bekommt die Konversation als Kontext (gleiche Methode wie Review)
var finalizerMessages = BuildReviewMessages(workerMessages);
var (text, iterations) = await RunAgentChat(
response, finalizer.ModelId, finalizer.SystemPrompt, finalizer.AgentId,
finalizer.Workspace, finalizer.UseMemory, finalizer.WebSearchVersion,
finalizer.ToolChoice, finalizerMessages, chainId, sessionId, apiToken, usage,
baseIterations, "finalizer", finalizer.Agent?.Name ?? "Finalizer-Agent",
workCt, sseCt);
string text;
int iterations;
if (!string.IsNullOrEmpty(finalizer.Provider) && _openAiService != null)
{
var subCtx = BuildSubAgentChatContext(finalizer, chainId, sessionId, apiToken);
text = await _openAiService.ChatNonStreaming(subCtx, finalizerMessages);
iterations = 1;
}
else
{
(text, iterations) = await RunAgentChat(
response, finalizer.ModelId, finalizer.SystemPrompt, finalizer.AgentId,
finalizer.Workspace, finalizer.UseMemory, finalizer.WebSearchVersion,
finalizer.ToolChoice, finalizerMessages, chainId, sessionId, apiToken, usage,
baseIterations, "finalizer", finalizer.Agent?.Name ?? "Finalizer-Agent",
workCt, sseCt);
}
await SseHelper.TryWriteSse(response, "finalizer_done", new
{
@@ -769,6 +811,43 @@ public class AnthropicService
return messages;
}
/// <summary>
/// Baut einen ChatContext für Sub-Agents (Preparer/Review/Finalizer) auf,
/// wenn diese einen OpenAI-kompatiblen Provider nutzen.
/// </summary>
private ChatContext BuildSubAgentChatContext(SubAgentContext sub, string chainId, string sessionId, string apiToken)
{
// SystemPrompt als Text extrahieren (SubAgent hat List<object> mit { type, text } Blöcken)
var systemText = "";
if (sub.SystemPrompt != null)
{
var parts = new List<string>();
foreach (var block in sub.SystemPrompt)
{
var json = JsonSerializer.Serialize(block);
var el = JsonSerializer.Deserialize<JsonElement>(json);
if (el.TryGetProperty("text", out var t))
parts.Add(t.GetString() ?? "");
}
systemText = string.Join("\n\n", parts);
}
return new ChatContext
{
ChainId = chainId,
SessionId = sessionId,
ModelId = sub.ModelId,
Provider = sub.Provider,
Workspace = sub.Workspace,
AgentId = sub.AgentId,
SystemPromptText = systemText,
UseMemory = sub.UseMemory,
WebSearchVersion = sub.WebSearchVersion,
MaxIterations = 20,
ApiToken = apiToken
};
}
/// <summary>
/// Generischer Agent-Chat (non-streaming für den Agent, aber SSE-Events für den Client).
/// Wird für Review-Agent und Finalizer-Agent verwendet.
@@ -918,7 +997,7 @@ public class AnthropicService
string toolResult;
bool isError = false;
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, telegramService: _telegramService, apiToken: apiToken); }
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, openAiService: _openAiService, telegramService: _telegramService, apiToken: apiToken); }
catch (OperationCanceledException) { throw; }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
@@ -1126,7 +1205,7 @@ public class AnthropicService
string toolResult;
bool isError = false;
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, userId: ctx.Session?.UserId, telegramService: _telegramService, apiToken: ctx.ApiToken); }
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, openAiService: _openAiService, userId: ctx.Session?.UserId, telegramService: _telegramService, apiToken: ctx.ApiToken); }
catch (OperationCanceledException) { throw; }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
@@ -1419,7 +1498,7 @@ public class AnthropicService
string toolResult;
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, userId: userId, telegramService: _telegramService, apiToken: apiToken); }
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); }
catch (OperationCanceledException) { throw; }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
+2
View File
@@ -806,10 +806,12 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
if (!string.IsNullOrWhiteSpace(systemText))
prompt.Add(new { type = "text", text = systemText });
var (provider, _) = OpenAiService.ParseModelString(modelId);
var hasTools = !string.IsNullOrEmpty(workspace) || agent.UseMemory;
return new SubAgentContext
{
ModelId = modelId,
Provider = provider,
SystemPrompt = prompt,
AgentId = agent.AgentId,
Workspace = workspace,
+14 -3
View File
@@ -15,6 +15,7 @@ public class CronjobService : BackgroundService
private readonly DatabaseService _db;
private readonly ChatService _chatService;
private readonly AnthropicService _anthropicService;
private readonly OpenAiService _openAiService;
private readonly TelegramService _telegramService;
private readonly ILogger<CronjobService> _logger;
@@ -22,12 +23,14 @@ public class CronjobService : BackgroundService
DatabaseService db,
ChatService chatService,
AnthropicService anthropicService,
OpenAiService openAiService,
TelegramService telegramService,
ILogger<CronjobService> logger)
{
_db = db;
_chatService = chatService;
_anthropicService = anthropicService;
_openAiService = openAiService;
_telegramService = telegramService;
_logger = logger;
}
@@ -109,8 +112,12 @@ public class CronjobService : BackgroundService
Content = job.Prompt
});
// Non-Streaming Chat ausführen
var responseText = await _anthropicService.ChatNonStreaming(ctx, history);
// Non-Streaming Chat ausführen – Provider-Weiche
string responseText;
if (!string.IsNullOrEmpty(ctx.Provider))
responseText = await _openAiService.ChatNonStreaming(ctx, history);
else
responseText = await _anthropicService.ChatNonStreaming(ctx, history);
// Session-Timestamp aktualisieren
await _db.UpdateSessionTimestamp(job.SessionId);
@@ -172,7 +179,11 @@ public class CronjobService : BackgroundService
["content"] = new List<object> { new { type = "text", text = summary } }
}
};
var notifyResponse = await _anthropicService.ChatNonStreaming(notifyCtx, notifyHistory);
string notifyResponse;
if (!string.IsNullOrEmpty(notifyCtx.Provider))
notifyResponse = await _openAiService.ChatNonStreaming(notifyCtx, notifyHistory);
else
notifyResponse = await _anthropicService.ChatNonStreaming(notifyCtx, notifyHistory);
await _db.UpdateSessionTimestamp(job.NotifySessionId);
+198
View File
@@ -732,6 +732,204 @@ public class OpenAiService
}
}
/// <summary>
/// Non-Streaming Chat für Telegram, Cronjobs und send_to_session.
/// Gibt den finalen Antworttext zurück. Unterstützt Tool-Calling (agentic loop).
/// </summary>
public async Task<string> ChatNonStreaming(
ChatContext ctx,
List<object> messages,
Func<int, Task> onIteration = null)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var usage = new TokenUsage();
int iterations = 0;
var finalText = new StringBuilder();
var (provider, modelId) = ParseModelString(ctx.ModelId);
if (!Providers.TryGetValue(provider, out var providerConfig))
throw new Exception($"Unbekannter Provider: '{provider}'");
// API-Key Override: User-Key hat Vorrang
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
{
ChainId = ctx.ChainId,
SessionId = ctx.SessionId,
Model = ctx.ModelId
};
// Messages in OpenAI-Format konvertieren
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
{
while (iterations < ctx.MaxIterations)
{
iterations++;
onIteration?.Invoke(iterations);
var client = CreateClient(providerConfig.ApiKey);
var payload = new Dictionary<string, object>
{
["model"] = modelId,
["messages"] = apiMessages,
["stream"] = false,
["max_tokens"] = 16384,
["tools"] = OpenAiTools,
["tool_choice"] = "auto"
};
var jsonPayload = JsonSerializer.Serialize(payload);
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var httpResp = await client.SendAsync(
new HttpRequestMessage(HttpMethod.Post, $"{providerConfig.BaseUrl}/chat/completions") { Content = httpContent });
var respBody = await httpResp.Content.ReadAsStringAsync();
if (!httpResp.IsSuccessStatusCode)
{
var errMsg = $"API {(int)httpResp.StatusCode}: {respBody}";
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "system", Type = "error",
Content = errMsg, IsError = true
});
await FinalizeChain(chain, usage, iterations, "error", sw);
throw new Exception($"OpenAI API Fehler: {(int)httpResp.StatusCode}");
}
var resp = JsonSerializer.Deserialize<JsonElement>(respBody);
// Usage tracken
if (resp.TryGetProperty("usage", out var usageProp))
{
if (usageProp.TryGetProperty("prompt_tokens", out var pt))
usage.InputTokens = pt.GetInt64();
if (usageProp.TryGetProperty("completion_tokens", out var ct2))
usage.OutputTokens = ct2.GetInt64();
}
// Erste Choice auswerten
var choices = resp.GetProperty("choices");
if (choices.GetArrayLength() == 0) break;
var choice = choices[0];
var message = choice.GetProperty("message");
var finishReason = choice.TryGetProperty("finish_reason", out var frProp) ? frProp.GetString() ?? "" : "";
// Text extrahieren
if (message.TryGetProperty("content", out var contentProp) && contentProp.ValueKind == JsonValueKind.String)
{
var text = contentProp.GetString() ?? "";
if (!string.IsNullOrEmpty(text))
{
finalText.Append(text);
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "assistant", Type = "text",
Content = text
});
}
}
// Tool-Calls prüfen
if (!message.TryGetProperty("tool_calls", out var toolCallsProp) || toolCallsProp.GetArrayLength() == 0)
{
await FinalizeChain(chain, usage, iterations, "end_turn", sw);
break;
}
// Assistant-Message mit tool_calls in History
var assistantMsg = JsonSerializer.Deserialize<JsonElement>(message.GetRawText());
apiMessages.Add(new
{
role = "assistant",
content = message.TryGetProperty("content", out var c) && c.ValueKind == JsonValueKind.String ? c.GetString() ?? "" : "",
tool_calls = JsonSerializer.Deserialize<List<object>>(toolCallsProp.GetRawText())
});
// Tool-Calls ausführen
var toolResults = new List<object>();
foreach (var tc in toolCallsProp.EnumerateArray())
{
var tcId = tc.GetProperty("id").GetString() ?? "";
var func = tc.GetProperty("function");
var tcName = func.GetProperty("name").GetString() ?? "";
var tcArgs = func.TryGetProperty("arguments", out var argsProp) ? argsProp.GetString() ?? "{}" : "{}";
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "assistant", Type = "tool_use",
Content = tcArgs,
ToolCallId = tcId, ToolName = tcName
});
JsonElement inputEl;
try { inputEl = JsonSerializer.Deserialize<JsonElement>(tcArgs); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
string toolResult;
bool isError = false;
try { toolResult = await _toolService.ExecuteTool(tcName, inputEl, ctx.Workspace); }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
toolResults.Add(new
{
role = "tool",
tool_call_id = tcId,
content = toolResult
});
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "tool", Type = "tool_result",
Content = toolResult,
ToolCallId = tcId, ToolName = tcName,
IsError = isError
});
}
foreach (var tr in toolResults)
apiMessages.Add(tr);
}
}
catch
{
if (iterations > 0)
await FinalizeChain(chain, usage, iterations, "error", sw);
throw;
}
return finalText.Length > 0 ? finalText.ToString() : "(Keine Textantwort)";
}
/// <summary>
/// Konvertiert eine Anthropic-format Message in das OpenAI-format.
/// </summary>
+19 -4
View File
@@ -15,6 +15,7 @@ public class TelegramService
private readonly DatabaseService _db;
private readonly ChatService _chatService;
private readonly AnthropicService _anthropic;
private readonly OpenAiService _openAi;
private readonly ServerSettingsService _settings;
public TelegramService(
@@ -22,12 +23,14 @@ public class TelegramService
DatabaseService db,
ChatService chatService,
AnthropicService anthropic,
OpenAiService openAi,
ServerSettingsService settings)
{
_httpFactory = httpFactory;
_db = db;
_chatService = chatService;
_anthropic = anthropic;
_openAi = openAi;
_settings = settings;
}
@@ -197,11 +200,23 @@ public class TelegramService
Content = userText
});
var reply = await _anthropic.ChatNonStreaming(ctx, history, onIteration: async (iteration) =>
string reply;
if (!string.IsNullOrEmpty(ctx.Provider))
{
if (iteration % 3 == 0)
await SendChatAction(chatId, botToken, "typing");
});
reply = await _openAi.ChatNonStreaming(ctx, history, onIteration: async (iteration) =>
{
if (iteration % 3 == 0)
await SendChatAction(chatId, botToken, "typing");
});
}
else
{
reply = await _anthropic.ChatNonStreaming(ctx, history, onIteration: async (iteration) =>
{
if (iteration % 3 == 0)
await SendChatAction(chatId, botToken, "typing");
});
}
await SendLongMessage(chatId, botToken, reply);
}
+10 -4
View File
@@ -390,7 +390,7 @@ 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, string userId = null, TelegramService telegramService = null, string apiToken = null)
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)
{
switch (name)
{
@@ -742,7 +742,7 @@ public class ToolService
}
case "send_to_session":
{
if (db == null || chatService == null || anthropicService == null)
if (db == null || chatService == null || (anthropicService == null && openAiService == null))
return "Fehler: send_to_session nicht verfügbar";
var sessionIdOrName = input.GetProperty("session_id_or_name").GetString() ?? "";
@@ -767,7 +767,10 @@ public class ToolService
var ctx = await chatService.PrepareContext(targetSessionId, userId: userId);
if (ctx == null)
return $"Fehler: Session '{targetSession.Name}' ist nicht konfiguriert (kein Agent zugewiesen?)";
ctx.ApiToken = apiToken;
// API-Key des Ziel-Agents auflösen (nicht den Key des aufrufenden Agents nutzen)
var targetApiKey = await db.GetAgentApiKeyRecord(userId, ctx.AgentId);
ctx.ApiToken = targetApiKey?.ApiKey ?? apiToken;
// History aufbauen – User-Message NICHT in DB speichern (wie Cronjob)
// Stattdessen als kontextualisierte Nachricht in die History injizieren
@@ -780,7 +783,10 @@ public class ToolService
try
{
await anthropicService.ChatNonStreaming(ctx, history);
if (!string.IsNullOrEmpty(ctx.Provider) && openAiService != null)
await openAiService.ChatNonStreaming(ctx, history);
else
await anthropicService.ChatNonStreaming(ctx, history);
}
catch (Exception ex)
{
+30
View File
@@ -152,6 +152,36 @@ async function _showKeySwitchModal() {
var list = document.createElement('div');
list.className = 'key-switch-list';
// "Kein Key"-Option als erste Zeile
var noneRow = document.createElement('div');
noneRow.className = 'key-switch-row';
if (!selectedKeyId) noneRow.classList.add('active');
var noneInfo = document.createElement('div');
noneInfo.className = 'key-switch-info';
var noneName = document.createElement('div');
noneName.className = 'key-switch-name';
noneName.textContent = 'Kein Key';
noneInfo.appendChild(noneName);
var noneMeta = document.createElement('div');
noneMeta.className = 'key-switch-meta';
noneMeta.textContent = 'Agent ohne API-Key nutzen';
noneInfo.appendChild(noneMeta);
noneRow.appendChild(noneInfo);
if (selectedKeyId) {
noneRow.classList.add('clickable');
noneRow.addEventListener('click', async function() {
noneRow.classList.add('loading-state');
try {
await apiSelectApiKey('none', getCurrentAgentId());
close();
await _initChatUsageBadge();
} catch(e) {
noneRow.classList.remove('loading-state');
}
});
}
list.appendChild(noneRow);
keys.forEach(function(key) {
var row = document.createElement('div');
row.className = 'key-switch-row';