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:
1 parent
1effc6d00f
commit
02f5fe2faf
11 files changed
+422
-69
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in new issue
Block a user