Panel members now stream token-by-token and show tool uses in real time (like the session agent) instead of delivering text only at completion. The key constraint is byte-level safety: N members flushing SSE frames concurrently on the shared HttpResponse would interleave and corrupt the stream, so a SemaphoreSlim gate serializes the writes (not the model work). - SseHelper.TryWriteSse takes an optional write-lock (default null, backward compatible) - NonStreamingLoopCallbacks carries SseGate + MemberIndex; _text/_tool/_error events are tagged with the member index (review/finalizer stay at -1, no gate) - CLI members forward TextDelta/ToolStart/ToolResult live instead of discarding everything but the final text - Anthropic-API members stream per tool-round via the loop callbacks; OpenAI members fall back to collected text at panel_member_done - Frontend demuxes by member index into per-bubble interleaved text/tool rendering Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
3.3 KiB
C#
99 lines
3.3 KiB
C#
using System.Diagnostics;
|
||
using System.Text.Json;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Gott.Models;
|
||
using Microsoft.AspNetCore.Http;
|
||
|
||
namespace Gott.Services;
|
||
|
||
/// <summary>
|
||
/// Shared SSE-Streaming-Utilities für AnthropicService und OpenAiService.
|
||
/// </summary>
|
||
public static class SseHelper
|
||
{
|
||
/// <summary>
|
||
/// Schreibt ein SSE-Event (wirft bei CancellationToken).
|
||
/// </summary>
|
||
public static async Task WriteSseEvent(HttpResponse response, string eventType, object data, CancellationToken ct = default)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
var json = JsonSerializer.Serialize(data);
|
||
await response.WriteAsync($"event: {eventType}\ndata: {json}\n\n", ct);
|
||
await response.Body.FlushAsync(ct);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Versucht ein SSE-Event zu schreiben, ignoriert Fehler (Client disconnected).
|
||
/// Optionaler <paramref name="gate"/>: serialisiert die Byte-Writes, wenn mehrere
|
||
/// Producer (z.B. parallele Panel-Member) auf DIESELBE HttpResponse schreiben –
|
||
/// sonst vermischen sich die SSE-Frames auf Byte-Ebene.
|
||
/// </summary>
|
||
public static async Task TryWriteSse(HttpResponse response, string eventType, object data, CancellationToken ct, SemaphoreSlim gate = null)
|
||
{
|
||
if (ct.IsCancellationRequested) return;
|
||
bool acquired = false;
|
||
try
|
||
{
|
||
if (gate != null)
|
||
{
|
||
await gate.WaitAsync(ct);
|
||
acquired = true;
|
||
}
|
||
await WriteSseEvent(response, eventType, data, ct);
|
||
}
|
||
catch
|
||
{
|
||
// Client bereits disconnected / abgebrochen – ignorieren
|
||
}
|
||
finally
|
||
{
|
||
if (acquired) gate.Release();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finalisiert eine Chain in der Datenbank (Token-Counts, Stopgrund, Dauer).
|
||
/// </summary>
|
||
public static async Task FinalizeChain(DatabaseService db, ChainRecord chain, TokenUsage usage, int iterations, string stopReason, Stopwatch sw, string streamErrorInfo = null)
|
||
{
|
||
chain.InputTokens = usage.InputTokens;
|
||
chain.OutputTokens = usage.OutputTokens;
|
||
chain.CacheCreationTokens = usage.CacheCreationTokens;
|
||
chain.CacheReadTokens = usage.CacheReadTokens;
|
||
chain.Iterations = iterations;
|
||
chain.StopReason = stopReason;
|
||
chain.StreamErrorInfo = streamErrorInfo;
|
||
chain.DurationMs = sw.ElapsedMilliseconds;
|
||
await db.UpdateChain(chain);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Kürzt Tool-Results auf max. Zeichenzahl.
|
||
/// </summary>
|
||
public const int MaxToolResultChars = 20_000;
|
||
|
||
public static string TruncateToolResult(string result)
|
||
{
|
||
return result.Length > MaxToolResultChars
|
||
? result[..MaxToolResultChars] + $"\n\n[... gekürzt, {result.Length - MaxToolResultChars} weitere Zeichen ...]"
|
||
: result;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Token-Zähler für eine Chat-Session (Input, Output, Cache).
|
||
/// </summary>
|
||
public class TokenUsage
|
||
{
|
||
public long InputTokens { get; set; }
|
||
public long OutputTokens { get; set; }
|
||
public long CacheCreationTokens { get; set; }
|
||
public long CacheReadTokens { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ergebnis einer Tool-Ausführung.
|
||
/// </summary>
|
||
public record ToolResult(string Id, string Result, bool IsError);
|