285 lines
11 KiB
C#
285 lines
11 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 Microsoft.AspNetCore.Http;
|
|
|
|
namespace Gott.Services;
|
|
|
|
public class AnthropicService
|
|
{
|
|
private readonly IHttpClientFactory _httpFactory;
|
|
private readonly string _token;
|
|
private readonly ToolService _toolService;
|
|
|
|
public AnthropicService(IHttpClientFactory httpFactory, string token, ToolService toolService)
|
|
{
|
|
_httpFactory = httpFactory;
|
|
_token = token;
|
|
_toolService = toolService;
|
|
}
|
|
|
|
public async Task StreamChat(
|
|
HttpResponse response,
|
|
string modelId,
|
|
List<object> systemPrompt,
|
|
List<object> messages,
|
|
int maxIterations = 25)
|
|
{
|
|
var usage = new TokenUsage();
|
|
int iterations = 0;
|
|
|
|
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();
|
|
await WriteSseEvent(response, "error", new { error = $"API {(int)httpResp.StatusCode}: {errBody}" });
|
|
await WriteSseEvent(response, "done", new { });
|
|
return;
|
|
}
|
|
|
|
var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage);
|
|
|
|
messages.Add(new { role = "assistant", content = assistantContent });
|
|
|
|
if (stopReason != "tool_use")
|
|
{
|
|
await WriteSseEvent(response, "done", new
|
|
{
|
|
model = modelId,
|
|
inputTokens = usage.InputTokens,
|
|
outputTokens = usage.OutputTokens,
|
|
iterations
|
|
});
|
|
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 WriteSseEvent(response, "error", new { error = "Max iterations erreicht" });
|
|
await WriteSseEvent(response, "done", new { });
|
|
}
|
|
|
|
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; }
|
|
}
|