feat: CLI-Fallback für Sub-Agents (Preparer/Review/Finalizer) wenn kein API-Key zugewiesen

Statt Hard-Error "Kein API-Key zugewiesen" nutzen Sub-Agents jetzt
Claude CLI als Fallback. Preparer wird nur übersprungen wenn weder
Key noch CLI verfügbar sind.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-04-17 10:11:34 +02:00
1 parent eccecf8f14
commit 1526891cd9
2 files changed
+111 -7

No files matched your search

+103
View File
@@ -63,6 +63,13 @@ public partial class AnthropicService
text = await _openAiService.ChatNonStreaming(subCtx, preparerMessages);
iterations = 1;
}
else if (string.IsNullOrEmpty(apiToken) && _cliService != null && _cliService.IsAvailable)
{
// Kein API-Key → CLI-Fallback
(text, iterations) = await RunSubAgentViaCli(
response, preparer, preparerMessages, chainId, sessionId,
"preparer", cancellationToken, cancellationToken);
}
else
{
(text, iterations) = await RunAgentChat(
@@ -164,6 +171,13 @@ public partial class AnthropicService
text = await _openAiService.ChatNonStreaming(subCtx, reviewMessages);
iterations = 1;
}
else if (string.IsNullOrEmpty(apiToken) && _cliService != null && _cliService.IsAvailable)
{
// Kein API-Key → CLI-Fallback
(text, iterations) = await RunSubAgentViaCli(
response, review, reviewMessages, chainId, sessionId,
"review", workCt, sseCt);
}
else
{
(text, iterations) = await RunAgentChat(
@@ -228,6 +242,13 @@ public partial class AnthropicService
text = await _openAiService.ChatNonStreaming(subCtx, finalizerMessages);
iterations = 1;
}
else if (string.IsNullOrEmpty(apiToken) && _cliService != null && _cliService.IsAvailable)
{
// Kein API-Key → CLI-Fallback
(text, iterations) = await RunSubAgentViaCli(
response, finalizer, finalizerMessages, chainId, sessionId,
"finalizer", workCt, sseCt);
}
else
{
(text, iterations) = await RunAgentChat(
@@ -624,6 +645,88 @@ public partial class AnthropicService
});
}
/// <summary>
/// Führt einen Sub-Agent (Preparer/Review/Finalizer) über die Claude CLI aus.
/// Sammelt den gesamten Text und gibt ihn zurück. Wird als Fallback genutzt,
/// wenn kein API-Key vorhanden ist.
/// </summary>
private async Task<(string text, int iterations)> RunSubAgentViaCli(
HttpResponse response,
SubAgentContext sub,
List<object> historyMessages,
string chainId,
string sessionId,
string agentType,
CancellationToken workCt,
CancellationToken sseCt)
{
if (_cliService == null || !_cliService.IsAvailable)
return ("", 0);
// System-Prompt als Text extrahieren
var systemText = new StringBuilder();
if (sub.SystemPrompt != null)
{
foreach (var block in sub.SystemPrompt)
{
var json = JsonSerializer.Serialize(block);
var el = JsonSerializer.Deserialize<JsonElement>(json);
if (el.TryGetProperty("text", out var t))
systemText.AppendLine(t.GetString());
}
}
// Letzte User-Message als Prompt extrahieren
var prompt = ExtractLastUserText(historyMessages) ?? "";
// Konversations-History als Kontext
var historyContext = BuildHistoryContext(historyMessages);
if (!string.IsNullOrEmpty(historyContext))
prompt = $"<conversation_history>\n{historyContext}\n</conversation_history>\n\n<current_request>\n{prompt}\n</current_request>";
var fullText = new StringBuilder();
var result = await _cliService.ChatStreamingAsync(
prompt,
async evt =>
{
switch (evt.Type)
{
case CliEventType.TextDelta:
fullText.Append(evt.Text);
await SseHelper.TryWriteSse(response, $"{agentType}_text",
new { text = evt.Text, agentName = sub.Agent?.Name ?? agentType }, sseCt);
break;
case CliEventType.ToolResult:
await SseHelper.TryWriteSse(response, $"{agentType}_tool",
new { name = evt.ToolName, result = evt.ToolResultContent, isError = evt.ToolResultIsError, agentName = sub.Agent?.Name ?? agentType }, sseCt);
break;
}
},
modelId: sub.ModelId,
systemPrompt: systemText.ToString(),
workspace: sub.Workspace,
hasWorkspace: !string.IsNullOrEmpty(sub.Workspace),
cancellationToken: workCt
);
// Text in DB speichern
if (fullText.Length > 0)
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId,
Iteration = 0,
Role = "assistant",
Type = "text",
Content = fullText.ToString(),
Tag = agentType
});
}
return (fullText.ToString(), 1);
}
/// <summary>
/// Non-Streaming Chat für Telegram. Gibt den finalen Antworttext zurück.
/// Wrapper um RunNonStreamingToolLoop mit Chain-Management und Embedding-Enqueue.