using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Gott.Models; using Microsoft.AspNetCore.Http; namespace Gott.Services; public class AnthropicService { private readonly IHttpClientFactory _httpFactory; private readonly string _token; private readonly ToolService _toolService; private readonly DatabaseService _db; public AnthropicService(IHttpClientFactory httpFactory, string token, ToolService toolService, DatabaseService db) { _httpFactory = httpFactory; _token = token; _toolService = toolService; _db = db; } public async Task StreamChat( HttpResponse response, string modelId, List systemPrompt, List messages, string chainId, string workspace, string sessionId = null, int maxIterations = 25) { var sw = System.Diagnostics.Stopwatch.StartNew(); var usage = new TokenUsage(); int iterations = 0; string lastStopReason = ""; var chain = new ChainRecord { ChainId = chainId, SessionId = sessionId, Model = modelId }; while (iterations < maxIterations) { iterations++; var payload = new Dictionary { ["model"] = modelId, ["max_tokens"] = 4096, ["system"] = systemPrompt, ["messages"] = messages, ["stream"] = true }; if (!string.IsNullOrEmpty(workspace)) payload["tools"] = _toolService.GetToolDefinitions(); var client = CreateClient(); var json = JsonSerializer.Serialize(payload); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); var httpResp = await client.SendAsync( new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent }, HttpCompletionOption.ResponseHeadersRead ); if (!httpResp.IsSuccessStatusCode) { var errBody = await httpResp.Content.ReadAsStringAsync(); var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}"; await _db.SaveMessage(new MessageRecord { ChainId = chainId, Iteration = iterations, Role = "system", Type = "error", Content = errMsg, IsError = true }); await WriteSseEvent(response, "error", new { error = errMsg }); await FinalizeChain(chain, usage, iterations, "error", sw); await WriteSseEvent(response, "done", new { }); return; } var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage, workspace); lastStopReason = stopReason; messages.Add(new { role = "assistant", content = assistantContent }); // Assistant-Blöcke persistieren foreach (var block in assistantContent) { var blockJson = JsonSerializer.Serialize(block); var el = JsonSerializer.Deserialize(blockJson); var blockType = el.GetProperty("type").GetString() ?? ""; if (blockType == "text") { await _db.SaveMessage(new MessageRecord { ChainId = chainId, Iteration = iterations, Role = "assistant", Type = "text", Content = el.GetProperty("text").GetString() }); } else if (blockType == "tool_use") { await _db.SaveMessage(new MessageRecord { ChainId = chainId, Iteration = iterations, Role = "assistant", Type = "tool_use", ToolName = el.GetProperty("name").GetString(), ToolCallId = el.GetProperty("id").GetString(), Content = el.GetProperty("input").ToString() }); } } // Tool Results persistieren foreach (var tr in executedTools) { await _db.SaveMessage(new MessageRecord { ChainId = chainId, Iteration = iterations, Role = "user", Type = "tool_result", ToolCallId = tr.Id, Content = tr.Result, IsError = tr.IsError }); } if (stopReason != "tool_use") { await FinalizeChain(chain, usage, iterations, stopReason, sw); await WriteSseEvent(response, "done", new { model = modelId, inputTokens = usage.InputTokens, outputTokens = usage.OutputTokens, iterations, chainId, durationMs = sw.ElapsedMilliseconds }); return; } var toolResults = new List(); foreach (var tr in executedTools) { toolResults.Add(new { type = "tool_result", tool_use_id = tr.Id, content = tr.Result, is_error = tr.IsError }); } messages.Add(new { role = "user", content = toolResults }); } await FinalizeChain(chain, usage, iterations, "max_iterations", sw); await WriteSseEvent(response, "error", new { error = "Max iterations erreicht" }); await WriteSseEvent(response, "done", new { }); } private async Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw) { chain.InputTokens = usage.InputTokens; chain.OutputTokens = usage.OutputTokens; chain.Iterations = iterations; chain.StopReason = stopReason; chain.DurationMs = sw.ElapsedMilliseconds; await _db.UpdateChain(chain); } private async Task<(string StopReason, List Content, List Tools)> ProcessStream( HttpResponse response, HttpResponseMessage httpResp, TokenUsage usage, string workspace) { var stream = await httpResp.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); var stopReason = ""; var assistantContent = new List(); var currentToolId = ""; var currentToolName = ""; var currentToolInputJson = new StringBuilder(); var currentTextBlock = new StringBuilder(); var blockTypes = new Dictionary(); var executedTools = new List(); string line; while ((line = await reader.ReadLineAsync()) != null) { if (!line.StartsWith("data: ")) continue; var data = line["data: ".Length..]; if (string.IsNullOrWhiteSpace(data)) continue; JsonElement evt; try { evt = JsonSerializer.Deserialize(data); } catch { continue; } var evtType = evt.TryGetProperty("type", out var t) ? t.GetString() ?? "" : ""; switch (evtType) { case "message_start": { if (evt.TryGetProperty("message", out var msg) && msg.TryGetProperty("usage", out var u) && u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64(); break; } case "content_block_start": { var idx = evt.GetProperty("index").GetInt32(); var cb = evt.GetProperty("content_block"); var cbType = cb.GetProperty("type").GetString() ?? ""; blockTypes[idx] = cbType; if (cbType == "text") { currentTextBlock.Clear(); await WriteSseEvent(response, "text_start", new { index = idx }); } else if (cbType == "tool_use") { if (currentTextBlock.Length > 0) { assistantContent.Add(new { type = "text", text = currentTextBlock.ToString() }); currentTextBlock.Clear(); } currentToolId = cb.GetProperty("id").GetString() ?? ""; currentToolName = cb.GetProperty("name").GetString() ?? ""; currentToolInputJson.Clear(); await WriteSseEvent(response, "tool_start", new { name = currentToolName, id = currentToolId }); } break; } case "content_block_delta": { var delta = evt.GetProperty("delta"); var deltaType = delta.GetProperty("type").GetString() ?? ""; if (deltaType == "text_delta") { var text = delta.GetProperty("text").GetString() ?? ""; currentTextBlock.Append(text); await WriteSseEvent(response, "text_delta", new { text }); } else if (deltaType == "input_json_delta") { var partial = delta.GetProperty("partial_json").GetString() ?? ""; currentToolInputJson.Append(partial); } break; } case "content_block_stop": { var idx = evt.GetProperty("index").GetInt32(); if (blockTypes.TryGetValue(idx, out var bt)) { if (bt == "text") { var text = currentTextBlock.ToString(); if (text.Length > 0) { assistantContent.Add(new { type = "text", text }); await WriteSseEvent(response, "text_end", new { }); } currentTextBlock.Clear(); } else if (bt == "tool_use") { JsonElement inputEl; try { inputEl = JsonSerializer.Deserialize(currentToolInputJson.ToString()); } catch { inputEl = JsonSerializer.Deserialize("{}"); } assistantContent.Add(new { type = "tool_use", id = currentToolId, name = currentToolName, input = inputEl }); string toolResult; bool isError = false; try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace); } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } executedTools.Add(new ToolResult(currentToolId, toolResult, isError)); await WriteSseEvent(response, "tool_end", new { name = currentToolName, input = currentToolInputJson.ToString(), result = toolResult.Length > 500 ? toolResult[..500] + "..." : toolResult, isError }); } } break; } case "message_delta": { if (evt.TryGetProperty("delta", out var d) && d.TryGetProperty("stop_reason", out var sr)) stopReason = sr.GetString() ?? ""; if (evt.TryGetProperty("usage", out var u2) && u2.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64(); break; } } } if (currentTextBlock.Length > 0) assistantContent.Add(new { type = "text", text = currentTextBlock.ToString() }); return (stopReason, assistantContent, executedTools); } private HttpClient CreateClient() { var client = _httpFactory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token); client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); client.DefaultRequestHeaders.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20"); client.DefaultRequestHeaders.Add("User-Agent", "claude-cli/2.1.2 (external, cli)"); client.DefaultRequestHeaders.Add("x-app", "cli"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Add("anthropic-dangerous-direct-browser-access", "true"); return client; } public static async Task WriteSseEvent(HttpResponse response, string eventType, object data) { var json = JsonSerializer.Serialize(data); await response.WriteAsync($"event: {eventType}\ndata: {json}\n\n"); await response.Body.FlushAsync(); } } public record ToolResult(string Id, string Result, bool IsError); public class TokenUsage { public long InputTokens { get; set; } public long OutputTokens { get; set; } }