Files
ClawdDotNet/ClawdDotNet_Prompt_InternetTools.md
T
RichardandClaude Opus 4.8 2fed388c99 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>
2026-07-26 18:21:46 +02:00

25 KiB
Raw Blame History

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:

{
  "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

"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

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

"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

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

"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

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:       "1K15K" | "15K50K" | 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:

// 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:

{
  "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.