Files
gott/Services/AnthropicService.cs
T
MelekiandClaude Opus 4.6 3e19da4d48 refactor: Backend-Deduplizierung – Parameter-Objekte, Shared Helpers, KeyCheckDispatcher
- ToolDefinitionOptions + ToolExecutionContext ersetzen lange Parameterlisten in ToolService
- AppendToLastMessage-Helper in ChatService eliminiert 5x duplizierte Logik
- OpenAiService: ResolveProviderConfig + BuildApiMessages + ConvertToOpenAiTools statt doppeltem Code
- Tool-Definitionen aus einer Quelle (Anthropic→OpenAI Konvertierung statt hardcodierter Liste)
- KeyCheckDispatcher zentralisiert Key-Check-Dispatch (ApiKeysEndpoint, KeyScanner, GitHubEventScanner)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 08:01:49 +01:00

344 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Gott.Endpoints;
using Gott.Models;
using Microsoft.AspNetCore.Http;
namespace Gott.Services;
public partial class AnthropicService
{
/// <summary>
/// Optionen für AddToolsToPayload – ersetzt die 12 bool-Parameter.
/// </summary>
public record ToolPayloadOptions
{
public string Workspace { get; init; }
public string AgentId { get; init; }
public bool UseMemory { get; init; }
public string WebSearchVersion { get; init; }
public string ToolChoice { get; init; }
public int Iteration { get; init; }
public bool AllowUserChoice { get; init; }
public bool PlanMode { get; init; }
public bool IncludeRemote { get; init; }
public bool IncludeGitHubSearch { get; init; }
public bool IncludeSerperSearch { get; init; }
}
private readonly IHttpClientFactory _httpFactory;
private readonly ToolService _toolService;
private readonly DatabaseService _db;
private readonly MemoryService _memoryService;
private readonly ServerSettingsService _settings;
private readonly VectorSearchService _vectorSearch;
private readonly EmbeddingBackgroundService _embeddingBg;
private ChatService _chatService;
private TelegramService _telegramService;
private ClaudeCliService _cliService;
public AnthropicService(IHttpClientFactory httpFactory, ToolService toolService, DatabaseService db, MemoryService memoryService = null, ServerSettingsService settings = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null)
{
_httpFactory = httpFactory;
_toolService = toolService;
_db = db;
_memoryService = memoryService;
_settings = settings;
_vectorSearch = vectorSearch;
_embeddingBg = embeddingBg;
}
public void SetCliService(ClaudeCliService cliService)
{
_cliService = cliService;
}
/// <summary>
/// Prüft ob die Claude CLI verfügbar ist.
/// </summary>
public bool IsCliAvailable => _cliService?.IsAvailable ?? false;
/// <summary>
/// Wird nach der Konstruktion gesetzt um zirkuläre Abhängigkeit zu vermeiden
/// (ChatService → AnthropicService → ChatService).
/// </summary>
public void SetChatService(ChatService chatService)
{
_chatService = chatService;
}
public void SetTelegramService(TelegramService telegramService)
{
_telegramService = telegramService;
}
private OpenAiService _openAiService;
public void SetOpenAiService(OpenAiService openAiService)
{
_openAiService = openAiService;
}
/// <summary>
/// Liest Usage-Header aus einer Anthropic-Response und aktualisiert die api_keys-Tabelle.
/// Fire-and-forget – Fehler werden stillschweigend ignoriert.
/// </summary>
private void TryUpdateKeyMeta(HttpResponseMessage httpResp, string apiToken)
{
try
{
var meta = AnthropicKeyChecker.ExtractMetaFromHeaders(httpResp.Headers);
if (meta == null || string.IsNullOrEmpty(apiToken)) return;
_ = _db.UpdateApiKeyMetaByToken(apiToken, meta.Status, meta.Plan, meta.Usage7d, meta.Usage5h);
}
catch { /* silent */ }
}
// ── Shared Helpers ──
// TruncateToolResult → SseHelper.cs
internal static string TruncateToolResult(string result) => SseHelper.TruncateToolResult(result);
/// <summary>
/// Extrahiert den Text der letzten User-Message aus der Message-Liste.
/// </summary>
private static string ExtractLastUserText(List<object> messages)
{
for (int i = messages.Count - 1; i >= 0; i--)
{
var json = JsonSerializer.Serialize(messages[i]);
var el = JsonSerializer.Deserialize<JsonElement>(json);
if (!el.TryGetProperty("role", out var role) || role.GetString() != "user") continue;
if (!el.TryGetProperty("content", out var content)) continue;
if (content.ValueKind == JsonValueKind.String)
return content.GetString();
if (content.ValueKind == JsonValueKind.Array)
{
foreach (var block in content.EnumerateArray())
{
var type = block.TryGetProperty("type", out var t) ? t.GetString() : "";
if (type == "text" && block.TryGetProperty("text", out var txt))
return txt.GetString();
}
}
break;
}
return null;
}
/// <summary>
/// Extrahiert Bild-Blöcke aus der letzten User-Message, speichert sie als Temp-Dateien
/// und gibt Datei-Referenz-Hinweise zurück, die der CLI-Prompt vorangestellt werden.
/// </summary>
private static string ExtractLastUserImageRefs(List<object> messages, List<string> tempPaths)
{
for (int i = messages.Count - 1; i >= 0; i--)
{
var json = JsonSerializer.Serialize(messages[i]);
var el = JsonSerializer.Deserialize<JsonElement>(json);
if (!el.TryGetProperty("role", out var role) || role.GetString() != "user") continue;
if (!el.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array) continue;
var refs = new List<string>();
var tempDir = Path.Combine(Path.GetTempPath(), "gott-cli-images");
Directory.CreateDirectory(tempDir);
foreach (var block in content.EnumerateArray())
{
var blockType = block.TryGetProperty("type", out var bt) ? bt.GetString() ?? "" : "";
if (blockType != "image" && blockType != "file") continue;
var source = block.TryGetProperty("source", out var src) ? src : default;
if (source.ValueKind == JsonValueKind.Undefined) continue;
var sourceType = source.TryGetProperty("type", out var st) ? st.GetString() ?? "" : "";
if (sourceType != "base64") continue;
var data = source.TryGetProperty("data", out var d) ? d.GetString() ?? "" : "";
var mediaType = source.TryGetProperty("media_type", out var mt) ? mt.GetString() ?? "" : "";
var ext = mediaType switch
{
"image/png" => ".png",
"image/jpeg" => ".jpg",
"image/gif" => ".gif",
"image/webp" => ".webp",
"application/pdf" => ".pdf",
_ => ".bin"
};
var fileName = $"{Guid.NewGuid():N}{ext}";
var filePath = Path.Combine(tempDir, fileName);
try
{
File.WriteAllBytes(filePath, Convert.FromBase64String(data));
tempPaths.Add(filePath);
var label = blockType == "image" ? "Screenshot" : "Datei";
refs.Add($"[{label}: {filePath}]");
}
catch
{
// Base64-Decode fehlgeschlagen → überspringen
}
}
if (refs.Count > 0)
{
return string.Join(" ", refs) + "\n<hint>Lies die referenzierten Dateien mit dem Read-Tool um sie zu sehen.</hint>";
}
break;
}
return null;
}
/// <summary>
/// Löscht temporäre Bilddateien die für den CLI-Aufruf erstellt wurden.
/// </summary>
private static void CleanupTempImages(List<string> paths)
{
if (paths == null || paths.Count == 0) return;
foreach (var path in paths)
{
try { if (File.Exists(path)) File.Delete(path); } catch { }
}
}
private void LogPayload(string json, string sessionId, string chainId, int iteration)
{
if (_settings == null || !_settings.GetBool("PayloadLogging", false)) return;
try
{
var logsDir = Path.Combine(AppContext.BaseDirectory, "data", "logs");
Directory.CreateDirectory(logsDir);
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var baseName = $"{timestamp}-{sessionId}-{chainId}-iter{iteration}-payload";
if (_settings?.GetBool("PayloadLoggingGzip", false) ?? false)
{
var filePath = Path.Combine(logsDir, baseName + ".json.gz");
using var fs = File.Create(filePath);
using var gz = new GZipStream(fs, CompressionLevel.Fastest);
using var writer = new StreamWriter(gz, Encoding.UTF8);
writer.Write(json);
}
else
{
File.WriteAllText(Path.Combine(logsDir, baseName + ".json"), json);
}
}
catch { /* Logging darf nie die App crashen */ }
}
private Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw, string streamErrorInfo = null)
=> SseHelper.FinalizeChain(_db, chain, usage, iterations, stopReason, sw, streamErrorInfo);
/// <summary>
/// Fügt Tool-Definitionen und WebSearch zum Payload hinzu.
/// </summary>
private void AddToolsToPayload(Dictionary<string, object> payload, ToolPayloadOptions opts)
{
var hasWorkspace = !string.IsNullOrEmpty(opts.Workspace);
var includeMemory = opts.UseMemory && _memoryService != null && !string.IsNullOrEmpty(opts.AgentId);
var includeMemorySearch = _vectorSearch != null && _vectorSearch.GetEmbeddingCount() > 0;
var includeSendToSession = _db != null && _chatService != null;
var hasTools = hasWorkspace || includeMemory || opts.IncludeRemote || includeMemorySearch || includeSendToSession || opts.IncludeGitHubSearch || opts.IncludeSerperSearch;
var hasWebSearch = !string.IsNullOrEmpty(opts.WebSearchVersion);
if (!hasTools && !hasWebSearch) return;
var tools = new List<object>();
if (hasTools)
{
var allTools = _toolService.GetToolDefinitions(new ToolDefinitionOptions
{
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
IncludeUserChoice = opts.AllowUserChoice, IncludeRemote = opts.IncludeRemote,
IncludeMemorySearch = includeMemorySearch, IncludeSendToSession = includeSendToSession,
IncludeGitHubSearch = opts.IncludeGitHubSearch, IncludeSerperSearch = opts.IncludeSerperSearch
});
if (opts.PlanMode)
allTools = _toolService.FilterReadOnlyTools(allTools);
tools.AddRange(allTools);
}
if (hasWebSearch)
{
tools.Add(new { type = opts.WebSearchVersion, name = "web_search", max_uses = 5 });
// web_search_20260209: Code Execution wird von der API automatisch injected – NICHT manuell hinzufügen!
}
payload["tools"] = tools;
var effectiveChoice = (!opts.PlanMode && opts.ToolChoice == "any" && opts.Iteration == 1) ? "any" : "auto";
payload["tool_choice"] = new { type = effectiveChoice };
}
/// <summary>
/// Entfernt Messages vom Anfang der Liste, ohne tool_use/tool_result-Paare zu trennen.
/// Gibt die tatsächliche Anzahl entfernter Messages zurück.
/// </summary>
private static int TrimMessagesCleanly(List<object> messages, int targetRemoveCount)
{
if (targetRemoveCount <= 0 || targetRemoveCount >= messages.Count)
return 0;
int removeCount = targetRemoveCount;
// Sicherstellen, dass wir nicht mitten in einem tool_use/tool_result-Paar abschneiden
while (removeCount < messages.Count - 1)
{
var firstRemaining = messages[removeCount];
var firstRemainingJson = JsonSerializer.Serialize(firstRemaining);
// Wenn die erste verbleibende Message ein tool_result ist → vorherige auch behalten
if (firstRemainingJson.Contains("\"tool_result\""))
{
removeCount--;
if (removeCount <= 0) return 0;
continue;
}
// Wenn die letzte entfernte Message ein assistant mit tool_use ist → tool_result auch entfernen
var lastRemovedJson = JsonSerializer.Serialize(messages[removeCount - 1]);
if (lastRemovedJson.Contains("\"tool_use\"") && lastRemovedJson.Contains("\"assistant\""))
{
removeCount++;
continue;
}
break;
}
if (removeCount > 0 && removeCount < messages.Count)
{
messages.RemoveRange(0, removeCount);
return removeCount;
}
return 0;
}
private HttpClient CreateClient(string apiToken)
{
var client = _httpFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
client.DefaultRequestHeaders.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20,code-execution-2025-08-25");
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;
}
// WriteSseEvent, TryWriteSse, TokenUsage, ToolResult → SseHelper.cs
}