using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Gott.Models;
using Microsoft.AspNetCore.Http;
namespace Gott.Services;
public partial class AnthropicService
{
// ── Preparer-Agent Logik ──
public class PreparerResult
{
public bool ShouldForward { get; set; }
public string ForwardedPrompt { get; set; }
public string ResponseText { get; set; }
public int IterationCount { get; set; }
}
///
/// Führt den Preparer-Agent aus. Der Preparer bekommt die User-Nachricht und die bisherige Konversation.
/// Antwortet er mit [FORWARD] → der Text nach [FORWARD] wird an den Session-Agent weitergegeben.
/// Sonst → seine Antwort wird als Rückfrage an den User gestreamt.
///
public async Task RunPreparer(
HttpResponse response,
SubAgentContext preparer,
List historyMessages,
string chainId,
string workspace,
string sessionId,
string apiToken,
CancellationToken cancellationToken)
{
// Preparer läuft VOR dem Worker und nutzt den Request-CancellationToken direkt.
// Bei Client-Disconnect wird der Preparer abgebrochen (kein Sinn weiterzumachen,
// da der Worker noch nicht gestartet hat).
cancellationToken.ThrowIfCancellationRequested();
await SseHelper.TryWriteSse(response, "preparer_start", new
{
agentId = preparer.AgentId,
agentName = preparer.Agent?.Name ?? "Vorbereiter-Agent"
}, cancellationToken);
// Preparer bekommt die History als Kontext
var preparerMessages = new List(historyMessages);
var usage = new TokenUsage();
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 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(
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, preparer.Effort);
}
var result = new PreparerResult { IterationCount = iterations, ResponseText = text };
// Logik: Standardmäßig wird IMMER weitergeleitet.
// Nur wenn der Preparer explizit [QUESTION] nutzt, wird gestoppt (= Rückfrage an User).
// [FORWARD] wird weiterhin erkannt und entfernt, ist aber nicht mehr zwingend nötig.
if (!string.IsNullOrEmpty(text) && text.Contains("[QUESTION]"))
{
// Explizite Rückfrage an User
var questionText = text.Replace("[QUESTION]", "").Trim();
result.ShouldForward = false;
result.ResponseText = questionText;
await SseHelper.TryWriteSse(response, "preparer_done", new
{
agentName = preparer.Agent?.Name ?? "Vorbereiter-Agent",
forwarded = false,
responseText = questionText
}, cancellationToken);
}
else
{
// Weiterleitung (Standard): [FORWARD]-Tag entfernen falls vorhanden
var forwardedText = text ?? "";
if (forwardedText.Contains("[FORWARD]"))
{
var idx = forwardedText.IndexOf("[FORWARD]");
forwardedText = forwardedText.Substring(idx + "[FORWARD]".Length).Trim();
}
result.ShouldForward = true;
result.ForwardedPrompt = forwardedText;
await SseHelper.TryWriteSse(response, "preparer_done", new
{
agentName = preparer.Agent?.Name ?? "Vorbereiter-Agent",
forwarded = true
}, cancellationToken);
}
return result;
}
// ── Review-Agent Logik ──
private class ReviewResult
{
public bool HasFeedback { get; set; }
public string Feedback { get; set; }
public int ReviewIterationCount { get; set; }
}
///
/// Führt den Review-Agent aus. Der Review-Agent bekommt die Text-Konversation
/// und kann Tools nutzen (z.B. read_file um die Arbeit zu prüfen).
/// Default = Approved. Nur bei explizitem [FEEDBACK]-Tag wird Feedback an den Worker zurückgegeben.
/// reviewRound = aktuelle Runde (0-basiert), wird vom Caller hochgezählt.
///
private async Task RunReview(
HttpResponse response,
SubAgentContext review,
List workerMessages,
string chainId,
string workspace,
string sessionId,
string apiToken,
TokenUsage usage,
int baseIterations,
int reviewRound,
CancellationToken workCt,
CancellationToken sseCt)
{
await SseHelper.TryWriteSse(response, "review_start", new
{
agentId = review.AgentId,
agentName = review.Agent?.Name ?? "Review-Agent",
round = reviewRound
}, sseCt);
// Review-Agent bekommt die Worker-Konversation als Kontext
var reviewMessages = BuildReviewMessages(workerMessages);
string text;
int iterations;
if (!string.IsNullOrEmpty(review.Provider) && _openAiService != null)
{
// OpenAI-kompatibler Reviewer
var subCtx = BuildSubAgentChatContext(review, chainId, sessionId, apiToken);
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(
response, review.ModelId, review.SystemPrompt, review.AgentId,
review.Workspace, review.UseMemory, review.WebSearchVersion,
review.ToolChoice, reviewMessages, chainId, sessionId, apiToken, usage,
baseIterations, "review", review.Agent?.Name ?? "Review-Agent",
workCt, sseCt, review.Effort);
}
var result = new ReviewResult { ReviewIterationCount = iterations };
if (!string.IsNullOrEmpty(text) && text.Contains("[FEEDBACK]"))
{
var feedbackText = text.Substring(text.IndexOf("[FEEDBACK]") + "[FEEDBACK]".Length).Trim();
result.HasFeedback = true;
result.Feedback = $"[Review-Feedback, Runde {reviewRound + 1}]: {feedbackText}";
}
await SseHelper.TryWriteSse(response, "review_done", new
{
agentName = review.Agent?.Name ?? "Review-Agent",
round = reviewRound,
hasFeedback = result.HasFeedback
}, sseCt);
return result;
}
///
/// Führt den Finalizer-Agent aus. Wird einmal nach der Review-Phase (oder direkt nach dem Worker) aufgerufen.
/// Der Finalizer kann Tools nutzen, gibt aber kein Feedback an den Worker zurück.
///
private async Task RunFinalizer(
HttpResponse response,
SubAgentContext finalizer,
List workerMessages,
string chainId,
string workspace,
string sessionId,
string apiToken,
TokenUsage usage,
int baseIterations,
CancellationToken workCt,
CancellationToken sseCt)
{
await SseHelper.TryWriteSse(response, "finalizer_start", new
{
agentId = finalizer.AgentId,
agentName = finalizer.Agent?.Name ?? "Finalizer-Agent"
}, sseCt);
// Finalizer bekommt die Konversation als Kontext (gleiche Methode wie Review)
var finalizerMessages = BuildReviewMessages(workerMessages);
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 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(
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, finalizer.Effort);
}
await SseHelper.TryWriteSse(response, "finalizer_done", new
{
agentName = finalizer.Agent?.Name ?? "Finalizer-Agent"
}, sseCt);
return iterations;
}
///
/// Baut die Messages für den Review-Agent auf.
/// Der Review-Agent bekommt die komplette Worker-Konversation als eine User-Message.
///
private List BuildReviewMessages(List workerMessages)
{
// Nur die Text-Konversation extrahieren – Tool-Calls und Tool-Results weglassen.
// Der Review/Finalizer-Agent hat eigene Tools und kann bei Bedarf selbst Dateien lesen.
// Aufeinanderfolgende Messages gleicher Rolle werden zusammengeführt,
// da die Anthropic API abwechselnde user/assistant-Rollen erfordert.
var messages = new List();
string lastRole = null;
var currentParts = new List();
foreach (var msg in workerMessages)
{
var json = JsonSerializer.Serialize(msg);
var el = JsonSerializer.Deserialize(json);
if (!el.TryGetProperty("role", out var role)) continue;
var roleStr = role.GetString() ?? "";
if (!el.TryGetProperty("content", out var content)) continue;
string textContent = null;
if (content.ValueKind == JsonValueKind.String)
{
textContent = content.GetString();
}
else if (content.ValueKind == JsonValueKind.Array)
{
var parts = new List();
foreach (var block in content.EnumerateArray())
{
var blockType = block.TryGetProperty("type", out var bt) ? bt.GetString() ?? "" : "";
if (blockType == "text" && block.TryGetProperty("text", out var txt))
parts.Add(txt.GetString() ?? "");
}
if (parts.Count > 0)
textContent = string.Join("\n", parts);
}
if (string.IsNullOrEmpty(textContent)) continue;
if (roleStr == lastRole)
{
// Gleiche Rolle → Text anhängen
currentParts.Add(textContent);
}
else
{
// Rolle wechselt → vorherige Message abschließen
if (lastRole != null && currentParts.Count > 0)
{
messages.Add(new
{
role = lastRole,
content = string.Join("\n\n", currentParts)
});
}
lastRole = roleStr;
currentParts = new List { textContent };
}
}
// Letzte Message abschließen
if (lastRole != null && currentParts.Count > 0)
{
messages.Add(new
{
role = lastRole,
content = string.Join("\n\n", currentParts)
});
}
// Sicherstellen, dass die Messages mit "user" beginnen
if (messages.Count > 0)
{
var firstJson = JsonSerializer.Serialize(messages[0]);
var firstEl = JsonSerializer.Deserialize(firstJson);
if (firstEl.GetProperty("role").GetString() != "user")
{
messages.Insert(0, new { role = "user", content = "Bitte überprüfe die folgende Konversation und die Arbeit des Agents." });
}
}
else
{
messages.Add(new { role = "user", content = "Es gibt keine Konversation zum Überprüfen." });
}
return messages;
}
///
/// Baut einen ChatContext für Sub-Agents (Preparer/Review/Finalizer) auf,
/// wenn diese einen OpenAI-kompatiblen Provider nutzen.
///
private ChatContext BuildSubAgentChatContext(SubAgentContext sub, string chainId, string sessionId, string apiToken)
{
// SystemPrompt als Text extrahieren (SubAgent hat List mit { type, text } Blöcken)
var systemText = "";
if (sub.SystemPrompt != null)
{
var parts = new List();
foreach (var block in sub.SystemPrompt)
{
var json = JsonSerializer.Serialize(block);
var el = JsonSerializer.Deserialize(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
};
}
// ── Shared Non-Streaming Tool Loop ──
///
/// Optionale Callbacks für RunNonStreamingToolLoop.
///
private class NonStreamingLoopCallbacks
{
/// Wird zu Beginn jeder Iteration aufgerufen (z.B. Typing-Indicator).
public Func OnIteration { get; set; }
/// SSE-Response für Client-Events. Null = keine SSE-Events.
public HttpResponse SseResponse { get; set; }
public CancellationToken SseCt { get; set; }
/// SSE-Event-Prefix (z.B. "review" → "review_text", "review_tool").
public string SsePrefix { get; set; }
public string AgentName { get; set; }
/// Tag für DB-Records (z.B. "review", "finalizer").
public string Tag { get; set; }
/// Ob Text-Embeddings in die Queue geschrieben werden.
public bool EnqueueEmbeddings { get; set; }
///
/// Optionaler Tool-Executor. Parameter: (toolName, input, ct) → result.
/// Bei Fehler Exception werfen. Wenn null, wird Standard-Executor verwendet.
///
public Func> ExecuteTool { get; set; }
}
///
/// Gemeinsamer Non-Streaming Tool-Loop für RunAgentChat und ChatNonStreaming.
/// Baut Payload, sendet API-Request, verarbeitet Content-Blöcke, führt Tools aus.
/// systemPrompt kann List<object> (Agent) oder string (ChatContext) sein.
///
private async Task<(string text, int iterations)> RunNonStreamingToolLoop(
string modelId,
object systemPrompt,
ToolPayloadOptions toolOpts,
List messages,
string chainId,
string sessionId,
string apiToken,
TokenUsage usage,
int baseIteration,
int maxIterations,
CancellationToken ct,
NonStreamingLoopCallbacks cb = null,
string effort = null)
{
cb ??= new NonStreamingLoopCallbacks();
int iterations = 0;
var finalText = new StringBuilder();
while (iterations < maxIterations)
{
ct.ThrowIfCancellationRequested();
iterations++;
if (cb.OnIteration != null) await cb.OnIteration(iterations);
var payload = new Dictionary
{
["model"] = modelId,
["max_tokens"] = 16384,
["messages"] = messages
};
// Reasoning-Effort (Opus 4.5+, Sonnet 4.6+). "" / "high" / null = Default weglassen.
if (!string.IsNullOrEmpty(effort) && effort != "high")
payload["output_config"] = new { effort };
if (systemPrompt is List spBlocks && spBlocks.Count > 0)
{
// Cache-Breakpoint auf den letzten System-Prompt-Block (1h TTL).
ApplyCacheControlToLast(spBlocks);
payload["system"] = spBlocks;
}
else if (systemPrompt is string spText && !string.IsNullOrEmpty(spText))
{
// String-System-Prompt → in Block-Form mit cache_control konvertieren.
payload["system"] = new List
{
new Dictionary
{
["type"] = "text",
["text"] = spText,
["cache_control"] = new { type = "ephemeral", ttl = "1h" }
}
};
}
AddToolsToPayload(payload, toolOpts with { Iteration = iterations });
var client = CreateClient(apiToken);
var json = JsonSerializer.Serialize(payload);
LogPayload(json, sessionId, chainId, baseIteration + iterations);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
using var httpResp = await client.SendAsync(
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
HttpCompletionOption.ResponseHeadersRead,
ct
);
TryUpdateKeyMeta(httpResp, apiToken);
if (!httpResp.IsSuccessStatusCode)
{
var errBody = await httpResp.Content.ReadAsStringAsync(ct);
var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}";
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = baseIteration + iterations,
Role = "system", Type = "error",
Content = errMsg, IsError = true, Tag = cb.Tag
});
if (cb.SseResponse != null)
await SseHelper.TryWriteSse(cb.SseResponse, $"{cb.SsePrefix}_error", new { error = errMsg }, cb.SseCt);
break;
}
var respJson = await httpResp.Content.ReadAsStringAsync(ct);
var respDoc = JsonSerializer.Deserialize(respJson);
if (respDoc.TryGetProperty("usage", out var u))
TrackUsage(usage, u);
var stopReason = respDoc.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
var contentArr = respDoc.TryGetProperty("content", out var c) ? c : default;
if (contentArr.ValueKind != JsonValueKind.Array) break;
var assistantContent = new List();
var toolResults = new List();
foreach (var block in contentArr.EnumerateArray())
{
var blockType = block.TryGetProperty("type", out var bt) ? bt.GetString() ?? "" : "";
if (blockType == "text")
{
var text = block.GetProperty("text").GetString() ?? "";
finalText.Append(text);
assistantContent.Add(new { type = "text", text });
var msgId = await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = baseIteration + iterations,
Role = "assistant", Type = "text",
Content = text, Tag = cb.Tag
});
if (cb.EnqueueEmbeddings)
_embeddingBg?.Enqueue(msgId, chainId, sessionId, toolOpts.AgentId, "assistant", text);
if (cb.SseResponse != null)
await SseHelper.TryWriteSse(cb.SseResponse, $"{cb.SsePrefix}_text", new { text, agentName = cb.AgentName }, cb.SseCt);
}
else if (blockType == "tool_use")
{
var toolId = block.GetProperty("id").GetString() ?? "";
var toolName = block.GetProperty("name").GetString() ?? "";
JsonElement inputEl;
try { inputEl = block.GetProperty("input"); }
catch { inputEl = JsonSerializer.Deserialize("{}"); }
assistantContent.Add(new { type = "tool_use", id = toolId, name = toolName, input = inputEl });
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = baseIteration + iterations,
Role = "assistant", Type = "tool_use",
ToolName = toolName, ToolCallId = toolId,
Content = inputEl.ToString(), Tag = cb.Tag
});
ct.ThrowIfCancellationRequested();
string toolResult;
bool isError = false;
try
{
toolResult = cb.ExecuteTool != null
? await cb.ExecuteTool(toolName, inputEl, ct)
: await _toolService.ExecuteTool(toolName, inputEl, new ToolExecutionContext
{
Workspace = toolOpts.Workspace, MemoryService = _memoryService,
AgentId = toolOpts.AgentId, CancellationToken = ct,
VectorSearch = _vectorSearch, EmbeddingBg = _embeddingBg
});
}
catch (OperationCanceledException) { throw; }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
ct.ThrowIfCancellationRequested();
toolResults.Add(new
{
type = "tool_result",
tool_use_id = toolId,
content = TruncateToolResult(toolResult),
is_error = isError
});
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = baseIteration + iterations,
Role = "user", Type = "tool_result",
ToolCallId = toolId,
Content = toolResult, IsError = isError, Tag = cb.Tag
});
if (cb.SseResponse != null)
await SseHelper.TryWriteSse(cb.SseResponse, $"{cb.SsePrefix}_tool", new { name = toolName, result = TruncateToolResult(toolResult), isError, agentName = cb.AgentName }, cb.SseCt);
}
}
messages.Add(new { role = "assistant", content = assistantContent });
if (stopReason != "tool_use" || toolResults.Count == 0)
break;
messages.Add(new { role = "user", content = toolResults });
}
return (finalText.ToString(), iterations);
}
///
/// Agent-Chat (non-streaming API, SSE-Events für Client).
/// Wrapper um RunNonStreamingToolLoop für Review-/Finalizer-Agents.
///
private Task<(string text, int iterations)> RunAgentChat(
HttpResponse response,
string modelId,
List systemPrompt,
string agentId,
string agentWorkspace,
bool useMemory,
string webSearchVersion,
string agentToolChoice,
List messages,
string chainId,
string sessionId,
string apiToken,
TokenUsage usage,
int baseIterations,
string agentType,
string agentName,
CancellationToken workCt,
CancellationToken sseCt,
string effort = null)
{
return RunNonStreamingToolLoop(
modelId, systemPrompt,
new ToolPayloadOptions
{
Workspace = agentWorkspace, AgentId = agentId,
UseMemory = useMemory, WebSearchVersion = webSearchVersion,
ToolChoice = agentToolChoice
},
messages, chainId, sessionId, apiToken, usage,
baseIterations, 20, workCt,
new NonStreamingLoopCallbacks
{
SseResponse = response, SseCt = sseCt,
SsePrefix = agentType, AgentName = agentName,
Tag = agentType
},
effort);
}
///
/// 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.
///
private async Task<(string text, int iterations)> RunSubAgentViaCli(
HttpResponse response,
SubAgentContext sub,
List 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(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 = $"\n{historyContext}\n \n\n\n{prompt}\n ";
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),
effort: sub.Effort,
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);
}
///
/// Non-Streaming Chat für Telegram. Gibt den finalen Antworttext zurück.
/// Wrapper um RunNonStreamingToolLoop mit Chain-Management und Embedding-Enqueue.
/// onIteration wird nach jeder Iteration aufgerufen (z.B. für Typing-Indicator).
///
public async Task ChatNonStreaming(
ChatContext ctx,
List messages,
Func onIteration = null)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var usage = new TokenUsage();
var chain = new ChainRecord
{
ChainId = ctx.ChainId,
SessionId = ctx.SessionId,
Model = ctx.ModelId
};
try
{
var (text, iterations) = await RunNonStreamingToolLoop(
ctx.ModelId, ctx.SystemPromptText,
new ToolPayloadOptions
{
Workspace = ctx.Workspace, AgentId = ctx.AgentId,
UseMemory = ctx.UseMemory, WebSearchVersion = ctx.WebSearchVersion,
ToolChoice = "auto"
},
messages, ctx.ChainId, ctx.SessionId, ctx.ApiToken, usage,
0, ctx.MaxIterations, CancellationToken.None,
new NonStreamingLoopCallbacks
{
OnIteration = onIteration,
EnqueueEmbeddings = true,
ExecuteTool = (name, input, ct) =>
_toolService.ExecuteTool(name, input, new ToolExecutionContext
{
Workspace = ctx.Workspace, MemoryService = _memoryService, AgentId = ctx.AgentId,
CancellationToken = ct, VectorSearch = _vectorSearch, EmbeddingBg = _embeddingBg,
Db = _db, ChatService = _chatService, AnthropicService = this, OpenAiService = _openAiService
})
},
ctx.Effort);
var finalStopReason = text.Length > 0 ? "end_turn" : "error";
await FinalizeChain(chain, usage, iterations, finalStopReason, sw);
return text;
}
catch (Exception ex)
{
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = 0,
Role = "system", Type = "error",
Content = $"Fehler: {ex.Message}", IsError = true
});
await FinalizeChain(chain, usage, 0, "error", sw);
return "";
}
}
}