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>
696 lines
25 KiB
Markdown
696 lines
25 KiB
Markdown
# 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.**
|