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:
1 parent
eccecf8f14
commit
1526891cd9
2 files changed
+111
-7
No files matched your search
@@ -142,14 +142,14 @@ public static class ChatEndpoint
|
|||||||
if (ctx.Preparer != null && !isChoiceResponse)
|
if (ctx.Preparer != null && !isChoiceResponse)
|
||||||
{
|
{
|
||||||
var preparerApiToken = userApiKey?.ApiKey;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Kein Key und kein CLI → Preparer überspringen (main agent wird auch CLI nutzen oder fehlermelden)
|
||||||
|
bool skipPreparer = string.IsNullOrEmpty(ctx.Preparer.Provider)
|
||||||
|
&& string.IsNullOrEmpty(preparerApiToken)
|
||||||
|
&& !anthropic.IsCliAvailable;
|
||||||
|
|
||||||
|
if (!skipPreparer)
|
||||||
|
{
|
||||||
// Preparer bekommt History MIT seinen eigenen vorherigen Messages (Tag "preparer"),
|
// Preparer bekommt History MIT seinen eigenen vorherigen Messages (Tag "preparer"),
|
||||||
// damit er den Kontext seiner Rückfragen kennt
|
// damit er den Kontext seiner Rückfragen kennt
|
||||||
var preparerHistory = await chatService.BuildHistoryFromDb(sessionId, includeTag: "preparer", promptBlocks: ctx.PromptBlocks);
|
var preparerHistory = await chatService.BuildHistoryFromDb(sessionId, includeTag: "preparer", promptBlocks: ctx.PromptBlocks);
|
||||||
@@ -182,6 +182,7 @@ public static class ChatEndpoint
|
|||||||
Content = $"[Vorbereitet von {ctx.Preparer.Agent?.Name ?? "Vorbereiter-Agent"}]:\n{preparerResult.ForwardedPrompt}"
|
Content = $"[Vorbereitet von {ctx.Preparer.Agent?.Name ?? "Vorbereiter-Agent"}]:\n{preparerResult.ForwardedPrompt}"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} // !skipPreparer
|
||||||
}
|
}
|
||||||
|
|
||||||
// History für den Session-Agent: OHNE Sub-Agent-Messages (saubere History)
|
// History für den Session-Agent: OHNE Sub-Agent-Messages (saubere History)
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ public partial class AnthropicService
|
|||||||
text = await _openAiService.ChatNonStreaming(subCtx, preparerMessages);
|
text = await _openAiService.ChatNonStreaming(subCtx, preparerMessages);
|
||||||
iterations = 1;
|
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
|
else
|
||||||
{
|
{
|
||||||
(text, iterations) = await RunAgentChat(
|
(text, iterations) = await RunAgentChat(
|
||||||
@@ -164,6 +171,13 @@ public partial class AnthropicService
|
|||||||
text = await _openAiService.ChatNonStreaming(subCtx, reviewMessages);
|
text = await _openAiService.ChatNonStreaming(subCtx, reviewMessages);
|
||||||
iterations = 1;
|
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
|
else
|
||||||
{
|
{
|
||||||
(text, iterations) = await RunAgentChat(
|
(text, iterations) = await RunAgentChat(
|
||||||
@@ -228,6 +242,13 @@ public partial class AnthropicService
|
|||||||
text = await _openAiService.ChatNonStreaming(subCtx, finalizerMessages);
|
text = await _openAiService.ChatNonStreaming(subCtx, finalizerMessages);
|
||||||
iterations = 1;
|
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
|
else
|
||||||
{
|
{
|
||||||
(text, iterations) = await RunAgentChat(
|
(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>
|
/// <summary>
|
||||||
/// Non-Streaming Chat für Telegram. Gibt den finalen Antworttext zurück.
|
/// Non-Streaming Chat für Telegram. Gibt den finalen Antworttext zurück.
|
||||||
/// Wrapper um RunNonStreamingToolLoop mit Chain-Management und Embedding-Enqueue.
|
/// Wrapper um RunNonStreamingToolLoop mit Chain-Management und Embedding-Enqueue.
|
||||||
|
|||||||
Reference in new issue
Block a user