Die Avalonia-Portierung ist abgeschlossen, damit ist die in ClawdDotNet.slnx angekuendigte Aufgabe "WinForms-Oberflaeche entfernen" faellig. Der Stand davor liegt unter dem Tag vor-fruehjahrsputz-2026-08. Entfernt (56 Dateien, seit dem Herausloesen der Anwendungsschicht nicht mehr Teil des Builds): ClawdDotNet.csproj, Program.cs, sieben frm_*-Formulare, UI/, Models/, EmbeddedUI/, Properties/, Resources/, Services/, das alte Anwendungssymbol und Deploy-Build.ps1 (ersetzt durch deploy/publish.py). Dazu configs/*.json - Beispielkonfigurationen aus der Zeit vor dem Instanzverzeichnis, auf die nur noch die alten Prompts verwiesen. Die vier Entwicklungs-Prompts der Anfangszeit ziehen nach docs/archiv/ um, mit README, das ihren Stand einordnet. Eine Regel darin gilt weiter - die Pflichtfelder fetchedAt/dataAsOf/source der Internet-Tools -, deshalb Archiv statt Loeschen; der WebSearch-Plan verweist auf den neuen Pfad. Toter Code - PlaceholderPageViewModel samt Ansicht: Es gibt keinen Platzhalter-Bereich mehr, seit alle neun Seiten portiert sind. - Snappier als direkter Paketverweis: MongoDB.Driver loest es ohnehin auf dieselbe Fassung auf, der Verweis hob nichts an. Zwei Fehler, die dabei sichtbar wurden - Die taegliche Sicherung lief ins Leere. Die Oberflaeche bot sie an und schrieb Uhrzeit, Zielordner und Anzahl in die Einstellungen, aber der BackupScheduler wurde nirgends erzeugt. Jetzt am AppHost verdrahtet und in den geordneten Abbau aufgenommen. - SettingsPageViewModel hielt die vier Sicherungs-Einstellungen doppelt. Aus der Ansicht waren sie laengst verschwunden, gelesen und beim Speichern zurueckgeschrieben wurden sie weiter: Wer die Uhrzeit auf der Sicherungs-Seite aenderte und danach die Einstellungen speicherte, bekam den alten Wert zurueck. Pakete: keine bekannten Sicherheitsluecken mehr - SQLitePCLRaw.bundle_e_sqlite3 auf 2.1.13 angehoben. Microsoft.Data.Sqlite bringt 2.1.11 mit, darin steckt GHSA-2m69-gcr7-jv3q (NU1903, hoch). - SharpCompress bleibt als direkter Verweis stehen. Beim Aufraeumen erst als ungenutzt entfernt - dabei kam die von MongoDB.Driver gezogene Fassung 0.30.1 mit GHSA-6c8g-7p36-r338 zurueck. Der Verweis ist eine Anhebung, kein Ballast; das steht jetzt als Kommentar dabei. Dokumentation - Roadmap mit Statusblock: A1 und A3 erledigt, A2 nur zur Haelfte - Gate, Policy und Dienst greifen, aber keine Ansicht ruft ApproveAsync auf, ein gestagter Aufruf liegt unbeantwortet. Das ist jetzt Punkt 1 der Reihung. Rocket.Chat steht und kollidiert mit A5 (Matrix) - Entscheidung faellig. - Avalonia-Portierungsleitfaden -> Oberflaechen-Leitfaden: kein Auftrag mehr, sondern Beschreibung des Stands. - Bestandsaufnahme und Linux-Analyse als datierte Befunde gekennzeichnet; der teure Teil der Linux-Analyse (8.900 Zeilen WinForms) ist hinfaellig. - Verweise auf frm_*, WebView2 und ClawdDotNet.csproj in den lebenden Dokumenten richtiggestellt. Build fehlerfrei, 585 Tests gruen (6 uebersprungen). Co-Authored-By: Claude Opus 5 <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.**
|