using System.Diagnostics; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Gott.Models; using Microsoft.AspNetCore.Http; namespace Gott.Services; /// /// Shared SSE-Streaming-Utilities für AnthropicService und OpenAiService. /// public static class SseHelper { /// /// Schreibt ein SSE-Event (wirft bei CancellationToken). /// 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); } /// /// Versucht ein SSE-Event zu schreiben, ignoriert Fehler (Client disconnected). /// Optionaler : 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. /// 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(); } } /// /// Finalisiert eine Chain in der Datenbank (Token-Counts, Stopgrund, Dauer). /// 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); } /// /// Kürzt Tool-Results auf max. Zeichenzahl. /// 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; } } /// /// Token-Zähler für eine Chat-Session (Input, Output, Cache). /// public class TokenUsage { public long InputTokens { get; set; } public long OutputTokens { get; set; } public long CacheCreationTokens { get; set; } public long CacheReadTokens { get; set; } /// /// Addiert die Werte von auf. NICHT thread-safe – der einzige /// Aufrufer (Panel-Engine) summiert die Member-Usage erst NACH der Task.WhenAll-Barrier, /// also single-threaded. Jeder Member befüllt seine EIGENE TokenUsage parallel; geteilt /// wird nichts → kein Lock/Interlocked nötig. /// public void Add(TokenUsage other) { if (other == null) return; InputTokens += other.InputTokens; OutputTokens += other.OutputTokens; CacheCreationTokens += other.CacheCreationTokens; CacheReadTokens += other.CacheReadTokens; } } /// /// Ergebnis einer Tool-Ausführung. /// public record ToolResult(string Id, string Result, bool IsError);