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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user