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,695 @@
|
||||
# ClawdDotNet – Prompt-Anhang: Internet-Tools
|
||||
|
||||
Dieser Abschnitt ergänzt die bestehenden Prompt-Anhänge und definiert drei
|
||||
unabhängige Internet-Tools sowie ein spezialisiertes Monitoring-Tool für
|
||||
strukturierte Webseiten (z.B. Capitol Trades).
|
||||
|
||||
Alle Tools folgen den bekannten Prinzipien: IAgentTool implementiert,
|
||||
Konfiguration ausschließlich aus AgentToolContext, keine Tool-zu-Tool-Abhängigkeiten.
|
||||
|
||||
---
|
||||
|
||||
## Pflicht-Regel: Timestamp in jedem ToolResult
|
||||
|
||||
**Diese Regel gilt für ALLE Internet-Tools ohne Ausnahme.**
|
||||
|
||||
Jedes `ToolResult.Content` das Finanzdaten, Nachrichten oder externe Daten enthält,
|
||||
muss ein JSON-Objekt zurückgeben das mindestens enthält:
|
||||
|
||||
```json
|
||||
{
|
||||
"fetchedAt": "2026-05-13T07:42:00Z",
|
||||
"dataAsOf": "2026-05-13T07:40:00Z",
|
||||
"source": "https://...",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
- `fetchedAt` = Zeitpunkt des HTTP-Requests (UTC, immer `DateTime.UtcNow`)
|
||||
- `dataAsOf` = Zeitpunkt der Daten laut Quelle (aus Response-Header, HTML oder API-Feld)
|
||||
Falls nicht ermittelbar: `null` — NIEMALS schätzen oder weglassen
|
||||
- `source` = exakte URL die abgerufen wurde
|
||||
|
||||
Der System-Prompt jedes Agenten der Internet-Tools nutzt MUSS enthalten:
|
||||
> "Verwende niemals Daten ohne `fetchedAt`-Feld. Wenn `dataAsOf` null ist,
|
||||
> teile dem Nutzer mit dass der Datenzeitpunkt unbekannt ist.
|
||||
> Erfinde niemals Kurse, Preise oder Daten aus dem Gedächtnis."
|
||||
|
||||
---
|
||||
|
||||
## Tool 1: DirectAPI — Echtzeit-Finanzdaten
|
||||
|
||||
**Datei: `ClawdDotNet.Tools.DirectAPI/DirectApiTool.cs`**
|
||||
|
||||
Direkter HTTP-Zugriff auf Finanz-APIs. Kein HTML-Parsing.
|
||||
Strukturiertes JSON mit verifizierbaren Timestamps.
|
||||
|
||||
### AgentConfig-Beispiel
|
||||
|
||||
```json
|
||||
"DirectAPI": {
|
||||
"providers": {
|
||||
"twelvedata": {
|
||||
"apiKey": "your-key-here",
|
||||
"baseUrl": "https://api.twelvedata.com"
|
||||
},
|
||||
"alphavantage": {
|
||||
"apiKey": "your-key-here",
|
||||
"baseUrl": "https://www.alphavantage.co"
|
||||
},
|
||||
"coingecko": {
|
||||
"baseUrl": "https://api.coingecko.com/api/v3"
|
||||
},
|
||||
"yahoo": {
|
||||
"baseUrl": "https://query1.finance.yahoo.com"
|
||||
}
|
||||
},
|
||||
"defaultProvider": "twelvedata",
|
||||
"cacheTtlSeconds": 60
|
||||
}
|
||||
```
|
||||
|
||||
### Tool-Implementierung
|
||||
|
||||
```csharp
|
||||
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 => 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|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;
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(
|
||||
JsonElement input, AgentToolContext ctx, CancellationToken ct)
|
||||
{
|
||||
// Config aus Context lesen — nie aus statischen Feldern
|
||||
var config = ctx.ToolConfig["DirectAPI"] as Dictionary<string, object?>
|
||||
?? throw new InvalidOperationException("DirectAPI config missing");
|
||||
|
||||
var providers = config["providers"] as Dictionary<string, object?> ?? new();
|
||||
var cacheTtl = Convert.ToInt32(config.GetValueOrDefault("cacheTtlSeconds") ?? 60);
|
||||
|
||||
var action = input.GetProperty("action").GetString()!;
|
||||
var symbol = input.GetProperty("symbol").GetString()!;
|
||||
var provider = input.TryGetProperty("provider", out var p)
|
||||
? p.GetString()
|
||||
: config.GetValueOrDefault("defaultProvider")?.ToString()
|
||||
?? "twelvedata";
|
||||
|
||||
// Cache-Check: agentId + symbol + action als Key
|
||||
var cacheKey = $"directapi:{ctx.AgentId}:{provider}:{action}:{symbol}";
|
||||
// (Cache-Implementierung über IMemoryCache oder Redis aus Core)
|
||||
|
||||
return action switch
|
||||
{
|
||||
"quote" => await FetchQuoteAsync(symbol, provider, providers, ct),
|
||||
"history" => await FetchHistoryAsync(input, symbol, provider, providers, ct),
|
||||
"crypto" => await FetchCryptoAsync(symbol, providers, ct),
|
||||
"forex" => await FetchForexAsync(symbol, provider, providers, ct),
|
||||
"search" => await SearchSymbolAsync(symbol, provider, providers, ct),
|
||||
_ => new ToolResult(false, "", $"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ToolResult> FetchQuoteAsync(
|
||||
string symbol, string provider,
|
||||
Dictionary<string, object?> providers, CancellationToken ct)
|
||||
{
|
||||
// Jeder Provider hat eigene URL-Struktur
|
||||
// Gemeinsam: immer Cache-Control: no-cache Header setzen
|
||||
// Gemeinsam: dataAsOf aus Response extrahieren (nicht schätzen)
|
||||
|
||||
// Twelve Data Quote:
|
||||
// GET https://api.twelvedata.com/quote?symbol={symbol}&apikey={key}
|
||||
// Response enthält: "datetime" → das ist dataAsOf
|
||||
// Response enthält: "timestamp" (Unix) → ebenfalls verwertbar
|
||||
|
||||
// Yahoo Finance Quote (kein Key nötig):
|
||||
// GET https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1m&range=1d
|
||||
// Response: result[0].meta.regularMarketTime (Unix timestamp) → dataAsOf
|
||||
|
||||
// Alpha Vantage Quote:
|
||||
// GET https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={key}
|
||||
// Response: "Global Quote"."07. latest trading day" → dataAsOf (nur Datum, keine Zeit)
|
||||
|
||||
// IMPLEMENTIERUNGSREGEL: dataAsOf IMMER aus der API-Antwort lesen.
|
||||
// Wenn das Feld fehlt oder leer ist → dataAsOf = null, NICHT DateTime.UtcNow.
|
||||
|
||||
throw new NotImplementedException("Implement per provider");
|
||||
}
|
||||
|
||||
// history, crypto, forex, search analog implementieren
|
||||
}
|
||||
```
|
||||
|
||||
### NuGet
|
||||
|
||||
Keine externen HTTP-Bibliotheken. Nur `System.Net.Http.HttpClient` via `IHttpClientFactory`.
|
||||
|
||||
---
|
||||
|
||||
## Tool 2: WebFetch — Nachrichten & strukturiertes HTML
|
||||
|
||||
**Datei: `ClawdDotNet.Tools.WebFetch/WebFetchTool.cs`**
|
||||
|
||||
HTTP-Abruf mit Whitelist, Timestamp-Extraktion und HTML-zu-Text-Konvertierung.
|
||||
Kein JavaScript-Rendering (statisches HTML only).
|
||||
|
||||
### AgentConfig-Beispiel
|
||||
|
||||
```json
|
||||
"WebFetch": {
|
||||
"allowedDomains": [
|
||||
"reuters.com",
|
||||
"bloomberg.com",
|
||||
"sec.gov",
|
||||
"feeds.finance.yahoo.com",
|
||||
"reddit.com",
|
||||
"capitoltrades.com"
|
||||
],
|
||||
"maxResponseKb": 512,
|
||||
"timeoutSeconds": 15,
|
||||
"userAgent": "ClawdDotNet-Agent/1.0 (Research Bot)"
|
||||
}
|
||||
```
|
||||
|
||||
### Tool-Implementierung
|
||||
|
||||
```csharp
|
||||
namespace ClawdDotNet.Tools.WebFetch;
|
||||
|
||||
public sealed class WebFetchTool : IAgentTool
|
||||
{
|
||||
public string Name => "WebFetch";
|
||||
public string Description => """
|
||||
Ruft statische Webseiten oder RSS-Feeds ab und extrahiert Text + Timestamps.
|
||||
Nur Domains aus der Whitelist erlaubt. Kein JavaScript-Rendering.
|
||||
Aktionen: fetch, rss
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema => JsonDocument.Parse("""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["action", "url"],
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["fetch", "rss"],
|
||||
"description": "fetch=HTML-Seite abrufen und zu Text konvertieren, rss=RSS/Atom-Feed parsen"
|
||||
},
|
||||
"url": { "type": "string" },
|
||||
"selector":{ "type": "string",
|
||||
"description": "optional: CSS-ähnlicher Hint welcher Teil relevant ist, z.B. 'table', 'article'" }
|
||||
}
|
||||
}
|
||||
""").RootElement;
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(
|
||||
JsonElement input, AgentToolContext ctx, CancellationToken ct)
|
||||
{
|
||||
var config = ctx.ToolConfig["WebFetch"] as Dictionary<string, object?> ?? new();
|
||||
var allowedDomains = (config.GetValueOrDefault("allowedDomains")
|
||||
as List<string>) ?? new List<string>();
|
||||
|
||||
var url = input.GetProperty("url").GetString()!;
|
||||
var action = input.GetProperty("action").GetString()!;
|
||||
|
||||
// Domain-Whitelist prüfen
|
||||
var host = new Uri(url).Host.Replace("www.", "");
|
||||
if (!allowedDomains.Any(d => host == d || host.EndsWith("." + d)))
|
||||
return new ToolResult(false, "",
|
||||
$"Domain '{host}' nicht in der Whitelist dieses Agenten.");
|
||||
|
||||
return action switch
|
||||
{
|
||||
"fetch" => await FetchPageAsync(url, input, config, ct),
|
||||
"rss" => await FetchRssAsync(url, ct),
|
||||
_ => new ToolResult(false, "", $"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ToolResult> FetchPageAsync(
|
||||
string url, JsonElement input,
|
||||
Dictionary<string, object?> config, CancellationToken ct)
|
||||
{
|
||||
using var http = CreateHttpClient(config);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Kein Cache — immer frische Daten anfordern
|
||||
request.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue
|
||||
{ NoCache = true, NoStore = true };
|
||||
|
||||
using var response = await http.SendAsync(request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
// dataAsOf aus HTTP-Headern extrahieren (Reihenfolge: Last-Modified > Date)
|
||||
DateTimeOffset? dataAsOf = response.Content.Headers.LastModified
|
||||
?? response.Headers.Date;
|
||||
|
||||
var html = await response.Content.ReadAsStringAsync(ct);
|
||||
var maxKb = Convert.ToInt32(config.GetValueOrDefault("maxResponseKb") ?? 512);
|
||||
|
||||
if (html.Length > maxKb * 1024)
|
||||
html = html[..(maxKb * 1024)];
|
||||
|
||||
// HTML → lesbarer Text (einfache Implementierung ohne externe Libs)
|
||||
var text = StripHtml(html);
|
||||
|
||||
// Versuche dataAsOf aus HTML-Meta-Tags zu verfeinern falls Header fehlt
|
||||
if (dataAsOf == null)
|
||||
dataAsOf = ExtractDateFromHtml(html);
|
||||
|
||||
var result = new
|
||||
{
|
||||
fetchedAt = DateTime.UtcNow,
|
||||
dataAsOf = dataAsOf?.UtcDateTime,
|
||||
source = url,
|
||||
data = new { text }
|
||||
};
|
||||
|
||||
return new ToolResult(true, JsonSerializer.Serialize(result));
|
||||
}
|
||||
|
||||
private async Task<ToolResult> FetchRssAsync(string url, CancellationToken ct)
|
||||
{
|
||||
// XML parsen mit System.Xml.Linq
|
||||
// Einträge: title, link, pubDate (→ dataAsOf), description
|
||||
// Neueste Einträge zuerst, max 20
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static string StripHtml(string html)
|
||||
{
|
||||
// Einfaches Regex-basiertes Stripping
|
||||
// Script- und Style-Tags zuerst entfernen, dann alle anderen Tags
|
||||
// Anschließend HTML-Entities dekodieren (System.Net.WebUtility.HtmlDecode)
|
||||
// Mehrfache Leerzeilen auf max. 2 reduzieren
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static DateTime? ExtractDateFromHtml(string html)
|
||||
{
|
||||
// Suche nach: <time datetime="...">, og:article:published_time,
|
||||
// datePublished JSON-LD, <meta name="date" content="...">
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient(Dictionary<string, object?> config)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
var timeout = Convert.ToInt32(config.GetValueOrDefault("timeoutSeconds") ?? 15);
|
||||
client.Timeout = TimeSpan.FromSeconds(timeout);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(
|
||||
config.GetValueOrDefault("userAgent")?.ToString()
|
||||
?? "ClawdDotNet-Agent/1.0");
|
||||
return client;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NuGet
|
||||
|
||||
`System.Xml.Linq` (im SDK enthalten) für RSS-Parsing. Kein externes HTML-Parser-Paket nötig.
|
||||
|
||||
---
|
||||
|
||||
## Tool 3: WebMonitor — Strukturiertes Seiten-Monitoring
|
||||
|
||||
**Datei: `ClawdDotNet.Tools.WebMonitor/WebMonitorTool.cs`**
|
||||
|
||||
Spezialisiert auf wiederkehrende Überprüfung von Seiten auf **neue Einträge**.
|
||||
Speichert den zuletzt gesehenen Stand in der Datenbank und liefert nur Deltas.
|
||||
|
||||
Primärer Anwendungsfall: Capitol Trades, SEC-Filings, jede tabellarische Seite
|
||||
mit eindeutigen IDs oder fortlaufenden Einträgen.
|
||||
|
||||
### Wie Capitol Trades funktioniert
|
||||
|
||||
Die Seite `https://www.capitoltrades.com/trades?pageSize=96` liefert:
|
||||
- Sauber strukturiertes HTML mit einer Tabelle
|
||||
- Jeder Trade hat eine eindeutige Trade-ID in der Detail-URL: `/trades/20003797558`
|
||||
- Trade-IDs sind fortlaufend und numerisch aufsteigend
|
||||
- Kein JavaScript-Rendering nötig — Daten sind im initialen HTML
|
||||
|
||||
Strategie: Höchste bekannte Trade-ID als Anker speichern. Bei jedem Check:
|
||||
alle IDs auf Seite 1 extrahieren, mit gespeicherter Max-ID vergleichen,
|
||||
nur neue Einträge melden.
|
||||
|
||||
### AgentConfig-Beispiel
|
||||
|
||||
```json
|
||||
"WebMonitor": {
|
||||
"monitors": {
|
||||
"capitol_trades": {
|
||||
"url": "https://www.capitoltrades.com/trades?pageSize=96",
|
||||
"checkIntervalMinutes": 30,
|
||||
"idPattern": "/trades/(\\d+)",
|
||||
"idField": "tradeId",
|
||||
"parser": "capitol_trades",
|
||||
"alertOnNew": true,
|
||||
"storeHistory": true
|
||||
},
|
||||
"sec_filings": {
|
||||
"url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcurrent&type=4&dateb=&owner=include&count=40",
|
||||
"checkIntervalMinutes": 60,
|
||||
"idPattern": "CIK=(\\d+)",
|
||||
"parser": "sec_form4",
|
||||
"alertOnNew": true,
|
||||
"storeHistory": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tool-Implementierung
|
||||
|
||||
```csharp
|
||||
namespace ClawdDotNet.Tools.WebMonitor;
|
||||
|
||||
public sealed class WebMonitorTool : IAgentTool
|
||||
{
|
||||
public string Name => "WebMonitor";
|
||||
public string Description => """
|
||||
Überwacht Webseiten auf neue Einträge und liefert nur die Deltas seit dem letzten Check.
|
||||
Speichert den Stand in der Datenbank. Ideal für Capitol Trades, SEC-Filings, etc.
|
||||
Aktionen: check, history, status
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema => JsonDocument.Parse("""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["action", "monitorId"],
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["check", "history", "status"],
|
||||
"description": "check=jetzt prüfen und Deltas liefern, history=bisherige Einträge, status=letzter Check-Zeitpunkt"
|
||||
},
|
||||
"monitorId": {
|
||||
"type": "string",
|
||||
"description": "z.B. capitol_trades, sec_filings — muss in Config definiert sein"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "max. Anzahl Einträge für history, default 50"
|
||||
}
|
||||
}
|
||||
}
|
||||
""").RootElement;
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(
|
||||
JsonElement input, AgentToolContext ctx, CancellationToken ct)
|
||||
{
|
||||
var config = ctx.ToolConfig["WebMonitor"] as Dictionary<string, object?> ?? new();
|
||||
var monitors = config["monitors"] as Dictionary<string, object?> ?? new();
|
||||
|
||||
var action = input.GetProperty("action").GetString()!;
|
||||
var monitorId = input.GetProperty("monitorId").GetString()!;
|
||||
|
||||
if (!monitors.ContainsKey(monitorId))
|
||||
return new ToolResult(false, "",
|
||||
$"Monitor '{monitorId}' nicht in der Config dieses Agenten definiert.");
|
||||
|
||||
var monitorConfig = monitors[monitorId] as Dictionary<string, object?> ?? new();
|
||||
|
||||
return action switch
|
||||
{
|
||||
"check" => await CheckForNewEntriesAsync(monitorId, monitorConfig, ctx, ct),
|
||||
"history" => await GetHistoryAsync(monitorId, input, ctx, ct),
|
||||
"status" => await GetStatusAsync(monitorId, ctx, ct),
|
||||
_ => new ToolResult(false, "", $"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ToolResult> CheckForNewEntriesAsync(
|
||||
string monitorId, Dictionary<string, object?> monitorConfig,
|
||||
AgentToolContext ctx, CancellationToken ct)
|
||||
{
|
||||
var url = monitorConfig["url"]?.ToString()!;
|
||||
var parser = monitorConfig["parser"]?.ToString() ?? "generic";
|
||||
var idPattern = monitorConfig["idPattern"]?.ToString();
|
||||
|
||||
// 1. Seite abrufen
|
||||
using var http = new HttpClient();
|
||||
http.DefaultRequestHeaders.CacheControl =
|
||||
new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
|
||||
var html = await http.GetStringAsync(url, ct);
|
||||
var fetchedAt = DateTime.UtcNow;
|
||||
|
||||
// 2. Einträge parsen (parser-spezifisch)
|
||||
var entries = parser switch
|
||||
{
|
||||
"capitol_trades" => ParseCapitolTrades(html),
|
||||
"sec_form4" => ParseSecForm4(html),
|
||||
_ => ParseGeneric(html, idPattern)
|
||||
};
|
||||
|
||||
// 3. Letzte bekannte Max-ID aus State laden
|
||||
// State-Key: "webmonitor:{agentId}:{monitorId}:maxId"
|
||||
// State-Speicher: über den StateManager aus dem Core
|
||||
// (wird als Dependency über AgentToolContext injiziert — siehe Core-Erweiterung unten)
|
||||
var stateKey = $"webmonitor:{ctx.AgentId}:{monitorId}:maxId";
|
||||
var lastMaxId = await ctx.StateStore.GetAsync(stateKey, ct); // string? → long?
|
||||
var lastKnown = long.TryParse(lastMaxId, out var l) ? l : 0L;
|
||||
|
||||
// 4. Neue Einträge = alle mit ID > lastKnown
|
||||
var newEntries = entries
|
||||
.Where(e => e.NumericId > lastKnown)
|
||||
.OrderBy(e => e.NumericId)
|
||||
.ToList();
|
||||
|
||||
// 5. Neue Max-ID persistieren
|
||||
if (newEntries.Count > 0)
|
||||
{
|
||||
var newMax = newEntries.Max(e => e.NumericId).ToString();
|
||||
await ctx.StateStore.SetAsync(stateKey, newMax, ct);
|
||||
|
||||
// Optional: Einträge in History-Tabelle speichern
|
||||
if (monitorConfig.GetValueOrDefault("storeHistory") is true)
|
||||
await StoreHistoryAsync(monitorId, newEntries, ctx, ct);
|
||||
}
|
||||
|
||||
// 6. Letzten Check-Zeitpunkt aktualisieren
|
||||
await ctx.StateStore.SetAsync(
|
||||
$"webmonitor:{ctx.AgentId}:{monitorId}:lastCheck",
|
||||
fetchedAt.ToString("O"), ct);
|
||||
|
||||
// 7. Ergebnis
|
||||
var result = new
|
||||
{
|
||||
fetchedAt = fetchedAt,
|
||||
dataAsOf = fetchedAt, // Capitol Trades: Seite ist immer aktuell
|
||||
source = url,
|
||||
monitorId = monitorId,
|
||||
newCount = newEntries.Count,
|
||||
data = new
|
||||
{
|
||||
newEntries = newEntries,
|
||||
message = newEntries.Count == 0
|
||||
? "Keine neuen Einträge seit dem letzten Check."
|
||||
: $"{newEntries.Count} neue Einträge gefunden."
|
||||
}
|
||||
};
|
||||
|
||||
return new ToolResult(true, JsonSerializer.Serialize(result));
|
||||
}
|
||||
|
||||
private static List<MonitorEntry> ParseCapitolTrades(string html)
|
||||
{
|
||||
// HTML-Tabelle parsen mit System.Text.RegularExpressions + String-Operationen
|
||||
//
|
||||
// Zu extrahieren pro Zeile:
|
||||
// tradeId: aus /trades/(\d+) in der Detail-URL → NumericId
|
||||
// politician: Linktext des Politiker-Links
|
||||
// party: "Republican" | "Democrat" aus dem Text
|
||||
// chamber: "House" | "Senate"
|
||||
// state: 2-Buchstaben-Code
|
||||
// issuer: Unternehmensname
|
||||
// ticker: z.B. "AVGO:US" → nur "AVGO"
|
||||
// published: Datum "8 May 2026" → DateTime
|
||||
// traded: Datum "27 Apr 2026" → DateTime
|
||||
// filedAfterDays: Zahl aus "days N"
|
||||
// owner: "Undisclosed" | "Spouse" | "Joint" | etc.
|
||||
// tradeType: "buy" | "sell"
|
||||
// size: "1K–15K" | "15K–50K" | etc.
|
||||
// price: "$418.20" → decimal
|
||||
// detailUrl: vollständige URL
|
||||
|
||||
// WICHTIG: Duplikate durch pageSize=96 möglich (selbe Transaktion, 2 IDs).
|
||||
// Beide IDs einliefern — der Agent entscheidet ob relevant.
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static List<MonitorEntry> ParseSecForm4(string html)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
private static List<MonitorEntry> ParseGeneric(string html, string? idPattern)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
private Task StoreHistoryAsync(string monitorId,
|
||||
List<MonitorEntry> entries, AgentToolContext ctx, CancellationToken ct)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
private Task<ToolResult> GetHistoryAsync(string monitorId,
|
||||
JsonElement input, AgentToolContext ctx, CancellationToken ct)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
private Task<ToolResult> GetStatusAsync(string monitorId,
|
||||
AgentToolContext ctx, CancellationToken ct)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public sealed record MonitorEntry(
|
||||
long NumericId,
|
||||
string RawId,
|
||||
string DetailUrl,
|
||||
DateTime? PublishedAt,
|
||||
DateTime? TradedAt,
|
||||
Dictionary<string, string> Fields // flexible Felder je nach Parser
|
||||
);
|
||||
```
|
||||
|
||||
### Core-Erweiterung: IStateStore
|
||||
|
||||
`WebMonitor` braucht persistenten State zwischen Runs (die letzte bekannte Trade-ID).
|
||||
Dafür muss `AgentToolContext` um ein `IStateStore` erweitert werden:
|
||||
|
||||
```csharp
|
||||
// Core/Tools/AgentToolContext.cs — erweitern:
|
||||
public sealed record AgentToolContext(
|
||||
string AgentId,
|
||||
string InstanceId,
|
||||
IReadOnlyDictionary<string, object?> ToolConfig,
|
||||
IStateStore StateStore, // NEU
|
||||
ILogger Logger,
|
||||
CancellationToken CancellationToken
|
||||
);
|
||||
|
||||
// Core/State/IStateStore.cs — neues Interface:
|
||||
namespace ClawdDotNet.Core.State;
|
||||
|
||||
public interface IStateStore
|
||||
{
|
||||
Task<string?> GetAsync(string key, CancellationToken ct);
|
||||
Task SetAsync(string key, string value, CancellationToken ct);
|
||||
Task DeleteAsync(string key, CancellationToken ct);
|
||||
}
|
||||
|
||||
// Implementierungen (im Core oder als separates Projekt):
|
||||
// - JsonFileStateStore → speichert in ./data/{instanceId}/state.json
|
||||
// - SqliteStateStore → speichert in ./data/{instanceId}/state.db (empfohlen)
|
||||
// Beide implementieren IStateStore.
|
||||
// SqliteStateStore ist bevorzugt: atomic writes, kein Datenverlust bei Absturz.
|
||||
// NuGet: Microsoft.Data.Sqlite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatisches Monitoring via Scheduler
|
||||
|
||||
Das `WebMonitor`-Tool wird typischerweise nicht interaktiv genutzt, sondern
|
||||
vom Scheduler getriggert. Konfiguration in der AgentConfig:
|
||||
|
||||
```json
|
||||
{
|
||||
"agentId": "capitol-watcher",
|
||||
"displayName": "Capitol Trades Monitor",
|
||||
"model": "google/gemini-flash-1.5",
|
||||
"systemPrompt": "Du überwachst Politiker-Trades auf Capitol Trades. Bei neuen Trades analysierst du: Welcher Sektor? Auffälliges Timing? Cluster mehrerer Politiker beim selben Wert? Fasse neue Trades prägnant zusammen. Verwende niemals Daten ohne fetchedAt-Feld.",
|
||||
"tools": {
|
||||
"WebMonitor": {
|
||||
"monitors": {
|
||||
"capitol_trades": {
|
||||
"url": "https://www.capitoltrades.com/trades?pageSize=96",
|
||||
"checkIntervalMinutes": 30,
|
||||
"idPattern": "/trades/(\\d+)",
|
||||
"parser": "capitol_trades",
|
||||
"alertOnNew": true,
|
||||
"storeHistory": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Database": {
|
||||
"connectionString": "...",
|
||||
"allowedTables": ["capitol_trades_history", "trade_alerts"]
|
||||
},
|
||||
"Mail": {
|
||||
"smtpHost": "...",
|
||||
"allowedRecipients": ["owner@example.com"]
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"cron": "*/30 * * * *",
|
||||
"runOnStart": true
|
||||
},
|
||||
"loopGuard": {
|
||||
"maxSteps": 10,
|
||||
"maxTokens": 30000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ablauf eines automatischen Runs:
|
||||
1. Scheduler feuert alle 30 Minuten
|
||||
2. Agent ruft `WebMonitor.check(capitol_trades)` auf
|
||||
3. Tool liefert neue Trades als Delta
|
||||
4. Agent analysiert: Cluster? Insider-Timing? Sektor-Häufung?
|
||||
5. Bei relevanten Funden: `Mail.send` oder `Database.insert` zur Archivierung
|
||||
6. Run beendet — kein manueller Eingriff nötig
|
||||
|
||||
---
|
||||
|
||||
## Implementierungsreihenfolge (für Claude Code)
|
||||
|
||||
Bearbeite diesen Abschnitt nach Abschluss der Core- und WinForms-Phase:
|
||||
|
||||
1. `IStateStore` Interface + `SqliteStateStore` Implementierung in Core anlegen
|
||||
2. `AgentToolContext` um `IStateStore` erweitern, alle bestehenden Tool-Calls anpassen
|
||||
3. `DirectApiTool` implementieren:
|
||||
- Twelve Data quote + history
|
||||
- Yahoo Finance quote (kein Key nötig, als Fallback)
|
||||
- CoinGecko crypto
|
||||
- Timestamp-Extraktion aus jeweiligem Response-Format
|
||||
4. `WebFetchTool` implementieren:
|
||||
- Domain-Whitelist-Check
|
||||
- HttpClient mit no-cache Headers
|
||||
- Einfaches HTML-Stripping (ohne externe Libs)
|
||||
- RSS/Atom-Parser mit System.Xml.Linq
|
||||
- Timestamp-Extraktion aus HTML-Meta-Tags
|
||||
5. `WebMonitorTool` implementieren:
|
||||
- `ParseCapitolTrades` als ersten Parser (Regex auf HTML-Tabelle)
|
||||
- `CheckForNewEntriesAsync` mit IStateStore-Integration
|
||||
- `GetHistory` und `GetStatus` Aktionen
|
||||
- `ParseSecForm4` als zweiten Parser
|
||||
6. xUnit-Tests:
|
||||
- `ParseCapitolTrades` gegen gespeichertes HTML-Sample testen
|
||||
- Delta-Logik: lastKnownId=X, neue IDs=[X-1, X, X+1, X+2] → nur X+1 und X+2
|
||||
- Domain-Whitelist: erlaubte und gesperrte Domain testen
|
||||
- Timestamp-Extraktion: Last-Modified Header, og:article:published_time, kein Header
|
||||
7. Beispiel-Config `capitol-team.json` anlegen
|
||||
8. In `Program.cs`: WebMonitorTool registrieren, SqliteStateStore als IStateStore in DI
|
||||
|
||||
**Beginne mit Schritt 1 dieses Abschnitts.**
|
||||
Reference in New Issue
Block a user