Agent-Arbeit vom Client-Disconnect entkoppeln
Zwei separate CancellationTokens: - workCt (nur Stop-Button): Anthropic API-Calls, Tool-Ausfuehrungen, DB-Writes Agent laeuft bei SSE-Disconnect weiter, Client pollt Ergebnisse - sseCt (Stop-Button + Client-Disconnect): SSE-Writes an Browser Bei Disconnect werden Writes zu stillen No-Ops Vorher: HttpContext.RequestAborted killte den gesamten Agent-Lauf wenn die SSE-Verbindung abriss (Browser-Tab idle, Netzwerk-Hickup). Jetzt arbeitet der Agent im Hintergrund weiter. OperationCanceledException = 'stopped' (nur Stop-Button) statt 'client_disconnected' (war falsch verknuepft)
This commit is contained in:
1 parent
dd99216913
commit
e9b1e8ef20
2 files changed
+72
-58
No files matched your search
@@ -175,9 +175,15 @@ public static class ChatEndpoint
|
|||||||
// Bei Choice-Response: toolChoice auf "auto" setzen, damit Claude frei antworten kann
|
// Bei Choice-Response: toolChoice auf "auto" setzen, damit Claude frei antworten kann
|
||||||
var effectiveToolChoice = isChoiceResponse ? "auto" : ctx.ToolChoice;
|
var effectiveToolChoice = isChoiceResponse ? "auto" : ctx.ToolChoice;
|
||||||
|
|
||||||
// CancellationToken: Verknüpft Stop-Button (über StreamingTracker) + Client-Disconnect
|
// Zwei separate CancellationTokens:
|
||||||
|
// - workCt: Nur Stop-Button → für Anthropic API-Calls, Tool-Ausführungen, DB-Writes
|
||||||
|
// Agent-Arbeit läuft bei Client-Disconnect weiter (Client pollt ja)
|
||||||
|
// - sseCt: Stop-Button + Client-Disconnect → für SSE-Writes an den Browser
|
||||||
|
// Bei Client-Disconnect werden SSE-Writes zu stillen No-Ops
|
||||||
var stopCts = streamingTracker.Start(sessionId, ctx.ChainId);
|
var stopCts = streamingTracker.Start(sessionId, ctx.ChainId);
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, stopCts.Token);
|
var workCt = stopCts.Token;
|
||||||
|
using var sseLinkCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, stopCts.Token);
|
||||||
|
var sseCt = sseLinkCts.Token;
|
||||||
|
|
||||||
// SSE-Heartbeat: Alle 15s ein Kommentar senden, damit die Verbindung
|
// SSE-Heartbeat: Alle 15s ein Kommentar senden, damit die Verbindung
|
||||||
// bei langen Tool-Ausführungen nicht von Browser/Netzwerk getrennt wird.
|
// bei langen Tool-Ausführungen nicht von Browser/Netzwerk getrennt wird.
|
||||||
@@ -185,25 +191,25 @@ public static class ChatEndpoint
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (!linkedCts.Token.IsCancellationRequested)
|
while (!sseCt.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Delay(15000, linkedCts.Token);
|
await Task.Delay(15000, sseCt);
|
||||||
await res.WriteAsync(": heartbeat\n\n", linkedCts.Token);
|
await res.WriteAsync(": heartbeat\n\n", sseCt);
|
||||||
await res.Body.FlushAsync(linkedCts.Token);
|
await res.Body.FlushAsync(sseCt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
catch { }
|
catch { }
|
||||||
}, linkedCts.Token);
|
}, sseCt);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, 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, cancellationToken: linkedCts.Token);
|
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, 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, workCt: workCt, sseCt: sseCt);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
streamingTracker.Stop(sessionId);
|
streamingTracker.Stop(sessionId);
|
||||||
// Heartbeat-Task beenden (linkedCts wird disposed → Token cancelled)
|
// Heartbeat-Task beenden (sseLinkCts wird disposed → Token cancelled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ public class AnthropicService
|
|||||||
SubAgentContext finalizer = null,
|
SubAgentContext finalizer = null,
|
||||||
bool allowUserChoice = true,
|
bool allowUserChoice = true,
|
||||||
bool planMode = false,
|
bool planMode = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken workCt = default,
|
||||||
|
CancellationToken sseCt = default)
|
||||||
{
|
{
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
var usage = new TokenUsage();
|
var usage = new TokenUsage();
|
||||||
@@ -70,7 +71,7 @@ public class AnthropicService
|
|||||||
{
|
{
|
||||||
while (iterations < maxIterations)
|
while (iterations < maxIterations)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
workCt.ThrowIfCancellationRequested();
|
||||||
iterations++;
|
iterations++;
|
||||||
|
|
||||||
var payload = new Dictionary<string, object>
|
var payload = new Dictionary<string, object>
|
||||||
@@ -95,12 +96,12 @@ public class AnthropicService
|
|||||||
using var httpResp = await client.SendAsync(
|
using var httpResp = await client.SendAsync(
|
||||||
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
|
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
|
||||||
HttpCompletionOption.ResponseHeadersRead,
|
HttpCompletionOption.ResponseHeadersRead,
|
||||||
cancellationToken
|
workCt
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!httpResp.IsSuccessStatusCode)
|
if (!httpResp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errBody = await httpResp.Content.ReadAsStringAsync(cancellationToken);
|
var errBody = await httpResp.Content.ReadAsStringAsync(workCt);
|
||||||
var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}";
|
var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}";
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
@@ -108,13 +109,13 @@ public class AnthropicService
|
|||||||
Role = "system", Type = "error",
|
Role = "system", Type = "error",
|
||||||
Content = errMsg, IsError = true
|
Content = errMsg, IsError = true
|
||||||
});
|
});
|
||||||
await TryWriteSse(response, "error", new { error = errMsg }, cancellationToken);
|
await TryWriteSse(response, "error", new { error = errMsg }, sseCt);
|
||||||
await FinalizeChain(chain, usage, iterations, "error", sw);
|
await FinalizeChain(chain, usage, iterations, "error", sw);
|
||||||
await TryWriteSse(response, "done", new { chainId = chain.ChainId }, cancellationToken);
|
await TryWriteSse(response, "done", new { chainId = chain.ChainId }, sseCt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, cancellationToken);
|
var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, workCt, sseCt);
|
||||||
lastStopReason = stopReason;
|
lastStopReason = stopReason;
|
||||||
|
|
||||||
messages.Add(new { role = "assistant", content = assistantContent });
|
messages.Add(new { role = "assistant", content = assistantContent });
|
||||||
@@ -198,7 +199,7 @@ public class AnthropicService
|
|||||||
// ── Review-Agent Phase ──
|
// ── Review-Agent Phase ──
|
||||||
if (review != null && stopReason == "end_turn" && reviewRound < review.MaxReviewIterations)
|
if (review != null && stopReason == "end_turn" && reviewRound < review.MaxReviewIterations)
|
||||||
{
|
{
|
||||||
var reviewResult = await RunReview(response, review, messages, chainId, workspace, sessionId, usage, iterations, reviewRound, cancellationToken);
|
var reviewResult = await RunReview(response, review, messages, chainId, workspace, sessionId, usage, iterations, reviewRound, workCt, sseCt);
|
||||||
iterations += reviewResult.ReviewIterationCount;
|
iterations += reviewResult.ReviewIterationCount;
|
||||||
reviewRound++;
|
reviewRound++;
|
||||||
|
|
||||||
@@ -223,7 +224,7 @@ public class AnthropicService
|
|||||||
// ── Finalizer-Agent Phase ──
|
// ── Finalizer-Agent Phase ──
|
||||||
if (finalizer != null && stopReason == "end_turn")
|
if (finalizer != null && stopReason == "end_turn")
|
||||||
{
|
{
|
||||||
var finalizerIterations = await RunFinalizer(response, finalizer, messages, chainId, workspace, sessionId, usage, iterations, cancellationToken);
|
var finalizerIterations = await RunFinalizer(response, finalizer, messages, chainId, workspace, sessionId, usage, iterations, workCt, sseCt);
|
||||||
iterations += finalizerIterations;
|
iterations += finalizerIterations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +243,7 @@ public class AnthropicService
|
|||||||
stopReason = finalStopReason,
|
stopReason = finalStopReason,
|
||||||
streamErrorInfo,
|
streamErrorInfo,
|
||||||
planMode
|
planMode
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,13 +263,13 @@ public class AnthropicService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await FinalizeChain(chain, usage, iterations, "max_iterations", sw);
|
await FinalizeChain(chain, usage, iterations, "max_iterations", sw);
|
||||||
await TryWriteSse(response, "error", new { error = "Max iterations erreicht" }, cancellationToken);
|
await TryWriteSse(response, "error", new { error = "Max iterations erreicht" }, sseCt);
|
||||||
await TryWriteSse(response, "done", new { chainId = chain.ChainId }, cancellationToken);
|
await TryWriteSse(response, "done", new { chainId = chain.ChainId }, sseCt);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Client hat die Verbindung getrennt (z.B. Browser-Refresh)
|
// Stop-Button gedrückt (workCt wurde gecancelled)
|
||||||
await FinalizeChain(chain, usage, iterations, "client_disconnected", sw);
|
await FinalizeChain(chain, usage, iterations, "stopped", sw);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -281,8 +282,8 @@ public class AnthropicService
|
|||||||
Content = errMsg, IsError = true
|
Content = errMsg, IsError = true
|
||||||
});
|
});
|
||||||
await FinalizeChain(chain, usage, iterations, "error", sw);
|
await FinalizeChain(chain, usage, iterations, "error", sw);
|
||||||
await TryWriteSse(response, "error", new { error = errMsg }, cancellationToken);
|
await TryWriteSse(response, "error", new { error = errMsg }, sseCt);
|
||||||
await TryWriteSse(response, "done", new { chainId = chain.ChainId }, cancellationToken);
|
await TryWriteSse(response, "done", new { chainId = chain.ChainId }, sseCt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +311,9 @@ public class AnthropicService
|
|||||||
string sessionId,
|
string sessionId,
|
||||||
CancellationToken cancellationToken)
|
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();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await TryWriteSse(response, "preparer_start", new
|
await TryWriteSse(response, "preparer_start", new
|
||||||
@@ -327,7 +331,7 @@ public class AnthropicService
|
|||||||
preparer.Workspace, preparer.UseMemory, preparer.WebSearchVersion,
|
preparer.Workspace, preparer.UseMemory, preparer.WebSearchVersion,
|
||||||
preparer.ToolChoice, preparerMessages, chainId, sessionId, usage,
|
preparer.ToolChoice, preparerMessages, chainId, sessionId, usage,
|
||||||
0, "preparer", preparer.Agent?.Name ?? "Vorbereiter-Agent",
|
0, "preparer", preparer.Agent?.Name ?? "Vorbereiter-Agent",
|
||||||
cancellationToken);
|
cancellationToken, cancellationToken);
|
||||||
|
|
||||||
var result = new PreparerResult { IterationCount = iterations, ResponseText = text };
|
var result = new PreparerResult { IterationCount = iterations, ResponseText = text };
|
||||||
|
|
||||||
@@ -396,9 +400,10 @@ public class AnthropicService
|
|||||||
TokenUsage usage,
|
TokenUsage usage,
|
||||||
int baseIterations,
|
int baseIterations,
|
||||||
int reviewRound,
|
int reviewRound,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken workCt,
|
||||||
|
CancellationToken sseCt)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
workCt.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// SSE: Review-Phase Start
|
// SSE: Review-Phase Start
|
||||||
await TryWriteSse(response, "review_start", new
|
await TryWriteSse(response, "review_start", new
|
||||||
@@ -407,7 +412,7 @@ public class AnthropicService
|
|||||||
maxRounds = review.MaxReviewIterations,
|
maxRounds = review.MaxReviewIterations,
|
||||||
agentId = review.AgentId,
|
agentId = review.AgentId,
|
||||||
agentName = review.Agent?.Name ?? "Review-Agent"
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
|
|
||||||
// Review-Agent bekommt eigene Message-History
|
// Review-Agent bekommt eigene Message-History
|
||||||
var reviewMessages = BuildReviewMessages(workerMessages);
|
var reviewMessages = BuildReviewMessages(workerMessages);
|
||||||
@@ -418,7 +423,7 @@ public class AnthropicService
|
|||||||
workspace, review.UseMemory, review.WebSearchVersion,
|
workspace, review.UseMemory, review.WebSearchVersion,
|
||||||
review.ToolChoice, reviewMessages, chainId, sessionId, usage,
|
review.ToolChoice, reviewMessages, chainId, sessionId, usage,
|
||||||
baseIterations, "review", review.Agent?.Name ?? "Review-Agent",
|
baseIterations, "review", review.Agent?.Name ?? "Review-Agent",
|
||||||
cancellationToken);
|
workCt, sseCt);
|
||||||
|
|
||||||
var result = new ReviewResult { ReviewIterationCount = reviewIterations };
|
var result = new ReviewResult { ReviewIterationCount = reviewIterations };
|
||||||
|
|
||||||
@@ -438,7 +443,7 @@ public class AnthropicService
|
|||||||
maxRounds = review.MaxReviewIterations,
|
maxRounds = review.MaxReviewIterations,
|
||||||
feedback = feedbackText,
|
feedback = feedbackText,
|
||||||
agentName = review.Agent?.Name ?? "Review-Agent"
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -447,7 +452,7 @@ public class AnthropicService
|
|||||||
{
|
{
|
||||||
round = reviewRound + 1,
|
round = reviewRound + 1,
|
||||||
agentName = review.Agent?.Name ?? "Review-Agent"
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
result.HasFeedback = false;
|
result.HasFeedback = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,13 +474,14 @@ public class AnthropicService
|
|||||||
string sessionId,
|
string sessionId,
|
||||||
TokenUsage usage,
|
TokenUsage usage,
|
||||||
int baseIterations,
|
int baseIterations,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken workCt,
|
||||||
|
CancellationToken sseCt)
|
||||||
{
|
{
|
||||||
await TryWriteSse(response, "finalizer_start", new
|
await TryWriteSse(response, "finalizer_start", new
|
||||||
{
|
{
|
||||||
agentId = finalizer.AgentId,
|
agentId = finalizer.AgentId,
|
||||||
agentName = finalizer.Agent?.Name ?? "Finalizer-Agent"
|
agentName = finalizer.Agent?.Name ?? "Finalizer-Agent"
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
|
|
||||||
// Finalizer bekommt die Konversation als Kontext (gleiche Methode wie Review)
|
// Finalizer bekommt die Konversation als Kontext (gleiche Methode wie Review)
|
||||||
var finalizerMessages = BuildReviewMessages(workerMessages);
|
var finalizerMessages = BuildReviewMessages(workerMessages);
|
||||||
@@ -485,12 +491,12 @@ public class AnthropicService
|
|||||||
finalizer.Workspace, finalizer.UseMemory, finalizer.WebSearchVersion,
|
finalizer.Workspace, finalizer.UseMemory, finalizer.WebSearchVersion,
|
||||||
finalizer.ToolChoice, finalizerMessages, chainId, sessionId, usage,
|
finalizer.ToolChoice, finalizerMessages, chainId, sessionId, usage,
|
||||||
baseIterations, "finalizer", finalizer.Agent?.Name ?? "Finalizer-Agent",
|
baseIterations, "finalizer", finalizer.Agent?.Name ?? "Finalizer-Agent",
|
||||||
cancellationToken);
|
workCt, sseCt);
|
||||||
|
|
||||||
await TryWriteSse(response, "finalizer_done", new
|
await TryWriteSse(response, "finalizer_done", new
|
||||||
{
|
{
|
||||||
agentName = finalizer.Agent?.Name ?? "Finalizer-Agent"
|
agentName = finalizer.Agent?.Name ?? "Finalizer-Agent"
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
|
|
||||||
return iterations;
|
return iterations;
|
||||||
}
|
}
|
||||||
@@ -634,7 +640,8 @@ public class AnthropicService
|
|||||||
int baseIterations,
|
int baseIterations,
|
||||||
string agentType,
|
string agentType,
|
||||||
string agentName,
|
string agentName,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken workCt,
|
||||||
|
CancellationToken sseCt = default)
|
||||||
{
|
{
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
var finalText = new StringBuilder();
|
var finalText = new StringBuilder();
|
||||||
@@ -643,7 +650,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
while (iterations < maxToolIterations)
|
while (iterations < maxToolIterations)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
workCt.ThrowIfCancellationRequested();
|
||||||
iterations++;
|
iterations++;
|
||||||
|
|
||||||
var payload = new Dictionary<string, object>
|
var payload = new Dictionary<string, object>
|
||||||
@@ -667,9 +674,9 @@ public class AnthropicService
|
|||||||
|
|
||||||
using var httpResp = await client.SendAsync(
|
using var httpResp = await client.SendAsync(
|
||||||
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
|
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
|
||||||
cancellationToken: cancellationToken);
|
cancellationToken: workCt);
|
||||||
|
|
||||||
var respBody = await httpResp.Content.ReadAsStringAsync(cancellationToken);
|
var respBody = await httpResp.Content.ReadAsStringAsync(workCt);
|
||||||
|
|
||||||
if (!httpResp.IsSuccessStatusCode)
|
if (!httpResp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -702,7 +709,7 @@ public class AnthropicService
|
|||||||
text,
|
text,
|
||||||
agentName,
|
agentName,
|
||||||
agentId
|
agentId
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
|
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
@@ -728,7 +735,7 @@ public class AnthropicService
|
|||||||
toolId,
|
toolId,
|
||||||
input = toolInput.ToString(),
|
input = toolInput.ToString(),
|
||||||
agentName
|
agentName
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
|
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
@@ -744,7 +751,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
bool isError = false;
|
||||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, cancellationToken); }
|
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt); }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
toolResults.Add(new
|
toolResults.Add(new
|
||||||
@@ -763,7 +770,7 @@ public class AnthropicService
|
|||||||
result = toolResult,
|
result = toolResult,
|
||||||
isError,
|
isError,
|
||||||
agentName
|
agentName
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
|
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
@@ -1056,9 +1063,10 @@ public class AnthropicService
|
|||||||
TokenUsage usage,
|
TokenUsage usage,
|
||||||
string workspace,
|
string workspace,
|
||||||
string agentId = null,
|
string agentId = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken workCt = default,
|
||||||
|
CancellationToken sseCt = default)
|
||||||
{
|
{
|
||||||
var stream = await httpResp.Content.ReadAsStreamAsync(cancellationToken);
|
var stream = await httpResp.Content.ReadAsStreamAsync(workCt);
|
||||||
using var reader = new StreamReader(stream);
|
using var reader = new StreamReader(stream);
|
||||||
|
|
||||||
var stopReason = "";
|
var stopReason = "";
|
||||||
@@ -1076,7 +1084,7 @@ public class AnthropicService
|
|||||||
var streamErrors = new List<object>();
|
var streamErrors = new List<object>();
|
||||||
|
|
||||||
string line;
|
string line;
|
||||||
while ((line = await reader.ReadLineAsync(cancellationToken)) != null)
|
while ((line = await reader.ReadLineAsync(workCt)) != null)
|
||||||
{
|
{
|
||||||
if (!line.StartsWith("data: ")) continue;
|
if (!line.StartsWith("data: ")) continue;
|
||||||
var data = line["data: ".Length..];
|
var data = line["data: ".Length..];
|
||||||
@@ -1108,7 +1116,7 @@ public class AnthropicService
|
|||||||
if (cbType == "text")
|
if (cbType == "text")
|
||||||
{
|
{
|
||||||
currentTextBlock.Clear();
|
currentTextBlock.Clear();
|
||||||
await WriteSseEvent(response, "text_start", new { index = idx }, cancellationToken);
|
await WriteSseEvent(response, "text_start", new { index = idx }, sseCt);
|
||||||
}
|
}
|
||||||
else if (cbType == "tool_use")
|
else if (cbType == "tool_use")
|
||||||
{
|
{
|
||||||
@@ -1120,7 +1128,7 @@ public class AnthropicService
|
|||||||
currentToolId = cb.GetProperty("id").GetString() ?? "";
|
currentToolId = cb.GetProperty("id").GetString() ?? "";
|
||||||
currentToolName = cb.GetProperty("name").GetString() ?? "";
|
currentToolName = cb.GetProperty("name").GetString() ?? "";
|
||||||
currentToolInputJson.Clear();
|
currentToolInputJson.Clear();
|
||||||
await WriteSseEvent(response, "tool_start", new { name = currentToolName, id = currentToolId }, cancellationToken);
|
await WriteSseEvent(response, "tool_start", new { name = currentToolName, id = currentToolId }, sseCt);
|
||||||
}
|
}
|
||||||
else if (cbType == "server_tool_use")
|
else if (cbType == "server_tool_use")
|
||||||
{
|
{
|
||||||
@@ -1132,7 +1140,7 @@ public class AnthropicService
|
|||||||
currentToolId = cb.GetProperty("id").GetString() ?? "";
|
currentToolId = cb.GetProperty("id").GetString() ?? "";
|
||||||
currentToolName = cb.GetProperty("name").GetString() ?? "";
|
currentToolName = cb.GetProperty("name").GetString() ?? "";
|
||||||
currentToolInputJson.Clear();
|
currentToolInputJson.Clear();
|
||||||
await WriteSseEvent(response, "web_search_start", new { name = currentToolName, id = currentToolId }, cancellationToken);
|
await WriteSseEvent(response, "web_search_start", new { name = currentToolName, id = currentToolId }, sseCt);
|
||||||
}
|
}
|
||||||
else if (cbType == "web_search_tool_result")
|
else if (cbType == "web_search_tool_result")
|
||||||
{
|
{
|
||||||
@@ -1157,7 +1165,7 @@ public class AnthropicService
|
|||||||
{
|
{
|
||||||
var text = delta.GetProperty("text").GetString() ?? "";
|
var text = delta.GetProperty("text").GetString() ?? "";
|
||||||
currentTextBlock.Append(text);
|
currentTextBlock.Append(text);
|
||||||
await WriteSseEvent(response, "text_delta", new { text }, cancellationToken);
|
await WriteSseEvent(response, "text_delta", new { text }, sseCt);
|
||||||
}
|
}
|
||||||
else if (deltaType == "input_json_delta")
|
else if (deltaType == "input_json_delta")
|
||||||
{
|
{
|
||||||
@@ -1178,7 +1186,7 @@ public class AnthropicService
|
|||||||
if (text.Length > 0)
|
if (text.Length > 0)
|
||||||
{
|
{
|
||||||
assistantContent.Add(new { type = "text", text });
|
assistantContent.Add(new { type = "text", text });
|
||||||
await WriteSseEvent(response, "text_end", new { }, cancellationToken);
|
await WriteSseEvent(response, "text_end", new { }, sseCt);
|
||||||
}
|
}
|
||||||
currentTextBlock.Clear();
|
currentTextBlock.Clear();
|
||||||
}
|
}
|
||||||
@@ -1220,7 +1228,7 @@ public class AnthropicService
|
|||||||
toolCallId = currentToolId,
|
toolCallId = currentToolId,
|
||||||
question,
|
question,
|
||||||
choices
|
choices
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
|
|
||||||
// Stream restlich konsumieren ohne weitere Verarbeitung
|
// Stream restlich konsumieren ohne weitere Verarbeitung
|
||||||
// (Claude sendet nach dem tool_use noch message_delta mit stop_reason)
|
// (Claude sendet nach dem tool_use noch message_delta mit stop_reason)
|
||||||
@@ -1229,15 +1237,15 @@ public class AnthropicService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cancellation prüfen bevor Tool ausgeführt wird
|
// Cancellation prüfen bevor Tool ausgeführt wird
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
workCt.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
bool isError = false;
|
||||||
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, cancellationToken); }
|
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt); }
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
workCt.ThrowIfCancellationRequested();
|
||||||
executedTools.Add(new ToolResult(currentToolId, toolResult, isError));
|
executedTools.Add(new ToolResult(currentToolId, toolResult, isError));
|
||||||
|
|
||||||
await WriteSseEvent(response, "tool_end", new
|
await WriteSseEvent(response, "tool_end", new
|
||||||
@@ -1246,7 +1254,7 @@ public class AnthropicService
|
|||||||
input = currentToolInputJson.ToString(),
|
input = currentToolInputJson.ToString(),
|
||||||
result = toolResult,
|
result = toolResult,
|
||||||
isError
|
isError
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
}
|
}
|
||||||
else if (bt == "server_tool_use")
|
else if (bt == "server_tool_use")
|
||||||
{
|
{
|
||||||
@@ -1268,7 +1276,7 @@ public class AnthropicService
|
|||||||
// Web-Search-Ergebnis von Anthropic – als Raw-JSON in assistantContent aufnehmen
|
// Web-Search-Ergebnis von Anthropic – als Raw-JSON in assistantContent aufnehmen
|
||||||
assistantContent.Add(webSearchResultBlocks[idx]);
|
assistantContent.Add(webSearchResultBlocks[idx]);
|
||||||
var sources = webSearchSources.TryGetValue(idx, out var s) ? s : new List<object>();
|
var sources = webSearchSources.TryGetValue(idx, out var s) ? s : new List<object>();
|
||||||
await WriteSseEvent(response, "web_search_end", new { id = currentToolId, sources }, cancellationToken);
|
await WriteSseEvent(response, "web_search_end", new { id = currentToolId, sources }, sseCt);
|
||||||
}
|
}
|
||||||
else if (bt.EndsWith("_tool_result") && serverToolResultBlocks.TryGetValue(idx, out var srvResultBlock))
|
else if (bt.EndsWith("_tool_result") && serverToolResultBlocks.TryGetValue(idx, out var srvResultBlock))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user