Initial commit: ClawdDotNet

Import des bestehenden Projektstands in Git.
- .NET 10 WinForms Anwendung (Multi-Agent / Tool-System)
- .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-26 18:21:46 +02:00
co-authored by Claude Opus 4.8
commit 2fed388c99
154 changed files with 29736 additions and 0 deletions
@@ -0,0 +1,356 @@
using System.Net.Http.Json;
using System.Text.Json;
using ClawdDotNet.Core.Tools;
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.Tools.DirectAPI;
public sealed class DirectApiTool : IAgentTool
{
public string Name => "DirectAPI";
public string Description => """
Ruft Echtzeit-Finanzdaten von verifizierten APIs ab.
Alle Antworten enthalten fetchedAt und dataAsOf Timestamps.
Aktionen: quote, history, crypto, forex, search
""";
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
{
"type": "object",
"required": ["action", "symbol"],
"properties": {
"action": {
"type": "string",
"enum": ["quote", "history", "crypto", "forex", "search"],
"description": "quote=aktueller Kurs, history=Kursverlauf, crypto=Krypto, forex=Wechselkurs, search=Symbol suchen"
},
"symbol": { "type": "string", "description": "z.B. NVDA, BTC, EUR/USD" },
"provider": { "type": "string", "description": "optional: twelvedata|alphavantage|massive|coingecko|yahoo" },
"interval": { "type": "string", "description": "für history: 1min|5min|1h|1day" },
"outputsize":{ "type": "integer","description": "für history: Anzahl Datenpunkte, max 500" }
}
}
""").RootElement.Clone();
public async Task<ToolResult> ExecuteAsync(
JsonElement input,
AgentToolContext context,
CancellationToken ct)
{
try
{
var action = input.GetProperty("action").GetString()!;
var symbol = input.GetProperty("symbol").GetString()!;
// Config laden
IReadOnlyDictionary<string, object?>? config = context.ToolConfig.TryGetValue("DirectAPI", out var c) && c is JsonElement je
? JsonSerializer.Deserialize<Dictionary<string, object?>>(je.GetRawText())
: context.ToolConfig;
if (config == null) return ToolResult.Fail("DirectAPI Konfiguration fehlt.");
var providers = config.GetValueOrDefault("providers") as JsonElement?;
var defaultProvider = config.GetValueOrDefault("defaultProvider")?.ToString() ?? "twelvedata";
var provider = input.TryGetProperty("provider", out var p) ? p.GetString() : defaultProvider;
return action switch
{
"quote" => await FetchQuoteAsync(symbol, provider!, providers, context, ct),
"history" => await FetchHistoryAsync(symbol, input, provider!, providers, context, ct),
"crypto" => await FetchCryptoAsync(symbol, providers, context, ct),
"forex" => await FetchForexAsync(symbol, provider!, providers, context, ct),
"search" => await SearchSymbolAsync(symbol, provider!, providers, context, ct),
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
};
}
catch (Exception ex)
{
context.Logger.LogError(ex, "Fehler in DirectAPI");
return ToolResult.Fail($"Fehler: {ex.Message}");
}
}
private async Task<ToolResult> FetchQuoteAsync(string symbol, string provider, JsonElement? providers, AgentToolContext context, CancellationToken ct)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
var fetchedAt = DateTime.UtcNow;
if (provider == "twelvedata")
{
var apiKey = GetApiKey(providers, "twelvedata");
var url = $"https://api.twelvedata.com/quote?symbol={symbol}&apikey={apiKey}";
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
if (json.Value.TryGetProperty("code", out var code) && code.GetInt32() != 200)
return ToolResult.Fail(json.Value.GetProperty("message").GetString() ?? "API Fehler");
DateTime? dataAsOf = null;
if (json.Value.TryGetProperty("timestamp", out var ts))
dataAsOf = DateTimeOffset.FromUnixTimeSeconds(ts.GetInt64()).UtcDateTime;
return CreateSuccessResult(fetchedAt, dataAsOf, url, json.Value);
}
else if (provider == "alphavantage")
{
var apiKey = GetApiKey(providers, "alphavantage");
var url = $"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apiKey}";
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
if (json.Value.TryGetProperty("Note", out var note))
return ToolResult.Fail($"Alpha Vantage Rate Limit: {note.GetString()}");
if (json.Value.TryGetProperty("Error Message", out var err))
return ToolResult.Fail($"Alpha Vantage Fehler: {err.GetString()}");
return CreateSuccessResult(fetchedAt, fetchedAt, url, json.Value);
}
else if (provider == "massive")
{
var apiKey = GetApiKey(providers, "massive");
var url = $"https://api.massive.com/v3/snapshot?ticker={symbol}";
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
return CreateSuccessResult(fetchedAt, fetchedAt, url, json.Value);
}
else if (provider == "yahoo")
{
var url = $"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1m&range=1d";
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
if (!json.Value.TryGetProperty("chart", out var chartProp) ||
!chartProp.TryGetProperty("result", out var resultArr) ||
resultArr.GetArrayLength() == 0)
return ToolResult.Fail($"Yahoo Finance: Keine Daten für '{symbol}' erhalten.");
var chart = resultArr[0];
var meta = chart.GetProperty("meta");
DateTime? dataAsOf = null;
if (meta.TryGetProperty("regularMarketTime", out var ts))
dataAsOf = DateTimeOffset.FromUnixTimeSeconds(ts.GetInt64()).UtcDateTime;
return CreateSuccessResult(fetchedAt, dataAsOf, url, chart);
}
return ToolResult.Fail($"Provider '{provider}' wird für 'quote' noch nicht unterstützt.");
}
private async Task<ToolResult> FetchHistoryAsync(string symbol, JsonElement input, string provider, JsonElement? providers, AgentToolContext context, CancellationToken ct)
{
var interval = input.TryGetProperty("interval", out var i) ? i.GetString() : "1day";
var outputSize = input.TryGetProperty("outputsize", out var o) ? o.GetInt32() : 30;
using var http = new HttpClient();
var fetchedAt = DateTime.UtcNow;
if (provider == "twelvedata")
{
var apiKey = GetApiKey(providers, "twelvedata");
var url = $"https://api.twelvedata.com/time_series?symbol={symbol}&interval={interval}&outputsize={outputSize}&apikey={apiKey}";
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
if (json.Value.TryGetProperty("status", out var status) && status.GetString() != "ok")
return ToolResult.Fail(json.Value.GetProperty("message").GetString() ?? "API Fehler");
return CreateSuccessResult(fetchedAt, null, url, json.Value);
}
else if (provider == "alphavantage")
{
var apiKey = GetApiKey(providers, "alphavantage");
var function_ = interval switch
{
"1min" or "5min" or "15min" or "30min" or "1h" => "TIME_SERIES_INTRADAY",
_ => "TIME_SERIES_DAILY"
};
var avInterval = interval switch
{
"1min" => "1min", "5min" => "5min", "15min" => "15min", "30min" => "30min", "1h" => "60min",
_ => ""
};
var urlBase = $"https://www.alphavantage.co/query?function={function_}&symbol={symbol}&apikey={apiKey}&outputsize=compact";
if (!string.IsNullOrEmpty(avInterval)) urlBase += $"&interval={avInterval}";
var (json, error) = await SafeGetJsonAsync(http, urlBase, ct);
if (error != null) return error;
if (json.Value.TryGetProperty("Note", out var note))
return ToolResult.Fail($"Alpha Vantage Rate Limit: {note.GetString()}");
if (json.Value.TryGetProperty("Error Message", out var err))
return ToolResult.Fail($"Alpha Vantage Fehler: {err.GetString()}");
return CreateSuccessResult(fetchedAt, null, urlBase, json.Value);
}
else if (provider == "massive")
{
var apiKey = GetApiKey(providers, "massive");
var timespan = interval switch
{
"1min" => "minute", "5min" => "minute", "15min" => "minute", "1h" => "hour", "1day" => "day",
_ => "day"
};
var multiplier = interval switch
{
"5min" => 5, "15min" => 15, _ => 1
};
var from = DateTime.UtcNow.AddDays(-outputSize).ToString("yyyy-MM-dd");
var to = DateTime.UtcNow.ToString("yyyy-MM-dd");
var url = $"https://api.massive.com/v2/aggs/ticker/{symbol}/range/{multiplier}/{timespan}/{from}/{to}?limit={outputSize}";
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
return CreateSuccessResult(fetchedAt, null, url, json.Value);
}
else if (provider == "yahoo")
{
var yahooInterval = interval switch
{
"1min" => "1m", "5min" => "5m", "15min" => "15m", "1h" => "1h", "1day" => "1d",
_ => "1d"
};
var range = interval is "1min" or "5min" or "15min" ? "1d" : $"{outputSize}d";
var url = $"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval={yahooInterval}&range={range}";
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
if (!json.Value.TryGetProperty("chart", out var chartProp) ||
!chartProp.TryGetProperty("result", out var resultArr) ||
resultArr.GetArrayLength() == 0)
return ToolResult.Fail($"Yahoo Finance: Keine Historien-Daten für '{symbol}' erhalten.");
var chart = resultArr[0];
return CreateSuccessResult(fetchedAt, null, url, chart);
}
return ToolResult.Fail($"Provider '{provider}' wird für 'history' noch nicht unterstützt.");
}
private async Task<ToolResult> FetchCryptoAsync(string symbol, JsonElement? providers, AgentToolContext context, CancellationToken ct)
{
using var http = new HttpClient();
var fetchedAt = DateTime.UtcNow;
var url = $"https://api.coingecko.com/api/v3/simple/price?ids={symbol.ToLowerInvariant()}&vs_currencies=usd&include_last_updated_at=true";
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
if (!json.Value.TryGetProperty(symbol.ToLowerInvariant(), out var data))
return ToolResult.Fail($"Symbol '{symbol}' nicht gefunden.");
DateTime? dataAsOf = null;
if (data.TryGetProperty("last_updated_at", out var ts))
dataAsOf = DateTimeOffset.FromUnixTimeSeconds(ts.GetInt64()).UtcDateTime;
return CreateSuccessResult(fetchedAt, dataAsOf, url, data);
}
private async Task<ToolResult> FetchForexAsync(string symbol, string provider, JsonElement? providers, AgentToolContext context, CancellationToken ct)
{
if (provider != "twelvedata") return ToolResult.Fail("Forex wird aktuell nur über twelvedata unterstützt.");
var apiKey = GetApiKey(providers, "twelvedata");
using var http = new HttpClient();
var fetchedAt = DateTime.UtcNow;
var url = $"https://api.twelvedata.com/exchange_rate?symbol={symbol}&apikey={apiKey}";
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
DateTime? dataAsOf = null;
if (json.Value.TryGetProperty("timestamp", out var ts))
dataAsOf = DateTimeOffset.FromUnixTimeSeconds(ts.GetInt64()).UtcDateTime;
return CreateSuccessResult(fetchedAt, dataAsOf, url, json.Value);
}
private async Task<ToolResult> SearchSymbolAsync(string symbol, string provider, JsonElement? providers, AgentToolContext context, CancellationToken ct)
{
if (provider != "twelvedata") return ToolResult.Fail("Search wird aktuell nur über twelvedata unterstützt.");
var apiKey = GetApiKey(providers, "twelvedata");
using var http = new HttpClient();
var fetchedAt = DateTime.UtcNow;
var url = $"https://api.twelvedata.com/symbol_search?symbol={symbol}"; // search often doesn't need key or uses same
var (json, error) = await SafeGetJsonAsync(http, url, ct);
if (error != null) return error;
return CreateSuccessResult(fetchedAt, null, url, json.Value);
}
/// <summary>
/// Sichere JSON-Antwort von einer API abrufen.
/// Prüft HTTP-Status und ob die Antwort tatsächlich JSON ist, bevor geparst wird.
/// </summary>
private static async Task<(JsonElement? json, ToolResult? error)> SafeGetJsonAsync(
HttpClient http, string url, CancellationToken ct)
{
HttpResponseMessage response;
try
{
response = await http.GetAsync(url, ct);
}
catch (HttpRequestException ex)
{
return (null, ToolResult.Fail($"API nicht erreichbar: {url} → {ex.Message}"));
}
var body = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
var preview = body.Length > 300 ? body[..300] + "…" : body;
return (null, ToolResult.Fail(
$"API Fehler: HTTP {(int)response.StatusCode} {response.ReasonPhrase} von {url}\nAntwort: {preview}"));
}
// Prüfen ob die Antwort überhaupt JSON ist
var trimmed = body.TrimStart();
if (trimmed.Length == 0 || (trimmed[0] != '{' && trimmed[0] != '['))
{
var preview = body.Length > 300 ? body[..300] + "…" : body;
return (null, ToolResult.Fail(
$"API hat kein JSON zurückgegeben ({url}). Antwort: {preview}"));
}
try
{
var json = JsonDocument.Parse(body).RootElement.Clone();
return (json, null);
}
catch (JsonException ex)
{
var preview = body.Length > 300 ? body[..300] + "…" : body;
return (null, ToolResult.Fail(
$"Ungültiges JSON von {url}: {ex.Message}\nAntwort: {preview}"));
}
}
private string? GetApiKey(JsonElement? providers, string name)
{
if (providers == null) return null;
if (providers.Value.TryGetProperty(name, out var p) && p.TryGetProperty("apiKey", out var k))
return k.GetString();
return null;
}
private ToolResult CreateSuccessResult(DateTime fetchedAt, DateTime? dataAsOf, string source, object data)
{
var result = new
{
fetchedAt = fetchedAt,
dataAsOf = dataAsOf,
source = source,
data = data
};
return ToolResult.Ok(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
}
}