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:
@@ -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 }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user