Files
gott/Services/AnthropicService.cs
T

364 lines
14 KiB
C#

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<object> systemPrompt,
List<object> messages,
string chainId,
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<string, object>
{
["model"] = modelId,
["max_tokens"] = 4096,
["system"] = systemPrompt,
["messages"] = messages,
["tools"] = _toolService.GetToolDefinitions(),
["stream"] = true
};
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);
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<JsonElement>(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<object>();
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<object> Content, List<ToolResult> Tools)> ProcessStream(
HttpResponse response,
HttpResponseMessage httpResp,
TokenUsage usage)
{
var stream = await httpResp.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var stopReason = "";
var assistantContent = new List<object>();
var currentToolId = "";
var currentToolName = "";
var currentToolInputJson = new StringBuilder();
var currentTextBlock = new StringBuilder();
var blockTypes = new Dictionary<int, string>();
var executedTools = new List<ToolResult>();
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<JsonElement>(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<JsonElement>(currentToolInputJson.ToString()); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
assistantContent.Add(new
{
type = "tool_use",
id = currentToolId,
name = currentToolName,
input = inputEl
});
string toolResult;
bool isError = false;
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl); }
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; }
}