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,465 @@
|
||||
# ClawdDotNet – Tool-Entwicklungsanleitung
|
||||
|
||||
## Übersicht
|
||||
|
||||
Tools sind eigenständige Plugins, die Agenten Zugriff auf externe Systeme geben (Datenbanken, Dateisysteme, E-Mail, APIs etc.). Jedes Tool ist ein separates .NET-Projekt, das ausschließlich `ClawdDotNet.Core` referenziert.
|
||||
|
||||
**Kernregeln:**
|
||||
- Tools sind **niemals voneinander abhängig** (Tool A darf Tool B nicht kennen)
|
||||
- Der Core kompiliert und läuft **ohne jedes Tool**
|
||||
- Jedes Tool ist **pro Agent konfiguriert** (via `AgentConfig.Tools`)
|
||||
- Tools werden zur Laufzeit über die `ToolRegistry` registriert
|
||||
|
||||
---
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
ClawdDotNet.sln
|
||||
├── src/
|
||||
│ ├── ClawdDotNet.Core/ ← Klassenbibliothek (keine Tool-Verweise!)
|
||||
│ ├── ClawdDotNet.Tools.MeinTool/ ← Dein Tool-Plugin
|
||||
│ │ ├── ClawdDotNet.Tools.MeinTool.csproj
|
||||
│ │ └── MeinToolTool.cs
|
||||
│ └── ClawdDotNet.Host/ ← WinForms-App (verweist auf Core + alle Tools)
|
||||
```
|
||||
|
||||
### Neues Tool-Projekt anlegen
|
||||
|
||||
```xml
|
||||
<!-- ClawdDotNet.Tools.MeinTool.csproj -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ClawdDotNet.Tools.MeinTool</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- NUR Core referenzieren, KEINE anderen Tools -->
|
||||
<ProjectReference Include="..\ClawdDotNet.Core\ClawdDotNet.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Tool-spezifische NuGet-Packages hier -->
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IAgentTool implementieren
|
||||
|
||||
Jedes Tool implementiert das Interface `ClawdDotNet.Core.Tools.IAgentTool`:
|
||||
|
||||
```csharp
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
|
||||
namespace ClawdDotNet.Tools.MeinTool;
|
||||
|
||||
public sealed class MeinToolTool : IAgentTool
|
||||
{
|
||||
// 1. Eindeutiger Name – wird vom LLM in tool_calls verwendet
|
||||
public string Name => "MeinTool";
|
||||
|
||||
// 2. Beschreibung für das LLM (geht in den System-Prompt)
|
||||
public string Description => "Beschreibung, was dieses Tool kann...";
|
||||
|
||||
// 3. JSON Schema des Input-Objekts (OpenAI Function Calling Format)
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["read", "write"],
|
||||
"description": "Die auszuführende Aktion"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Eingabedaten"
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
""").RootElement.Clone();
|
||||
|
||||
// 4. Ausführung
|
||||
public async Task<ToolResult> ExecuteAsync(
|
||||
JsonElement input,
|
||||
AgentToolContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Action aus dem Input lesen
|
||||
var action = input.GetProperty("action").GetString()
|
||||
?? throw new ArgumentException("'action' is required");
|
||||
|
||||
// Konfiguration aus dem AgentToolContext lesen
|
||||
// NIEMALS hardcodierte Werte, immer aus context.ToolConfig!
|
||||
var meineSetting = context.ToolConfig.TryGetValue("meineSetting", out var val)
|
||||
? val?.ToString() ?? ""
|
||||
: "";
|
||||
|
||||
// Logger verwenden
|
||||
context.Logger.LogInformation("MeinTool executing action: {Action}", action);
|
||||
|
||||
return action switch
|
||||
{
|
||||
"read" => await HandleReadAsync(input, context, ct),
|
||||
"write" => await HandleWriteAsync(input, context, ct),
|
||||
_ => ToolResult.Fail($"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ToolResult> HandleReadAsync(
|
||||
JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
// Implementierung...
|
||||
await Task.CompletedTask; // Platzhalter
|
||||
return ToolResult.Ok("Ergebnis als JSON oder Text");
|
||||
}
|
||||
|
||||
private async Task<ToolResult> HandleWriteAsync(
|
||||
JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
// Implementierung...
|
||||
await Task.CompletedTask; // Platzhalter
|
||||
return ToolResult.Ok("Erfolgreich geschrieben");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Konfiguration pro Agent
|
||||
|
||||
Die Tool-Konfiguration ist **pro Agent** definiert, nicht global. Das bedeutet: Agent A kann auf Datenbank X zugreifen, Agent B auf Datenbank Y.
|
||||
|
||||
### In der Config-Datei (z.B. `config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "analyst",
|
||||
"tools": {
|
||||
"MeinTool": {
|
||||
"meineSetting": "wert-fuer-agent-analyst",
|
||||
"andereOption": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "developer",
|
||||
"tools": {
|
||||
"MeinTool": {
|
||||
"meineSetting": "wert-fuer-agent-developer",
|
||||
"andereOption": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Konfiguration im Tool auslesen:
|
||||
|
||||
```csharp
|
||||
// Im ExecuteAsync:
|
||||
var setting = context.ToolConfig["meineSetting"]?.ToString();
|
||||
var option = context.ToolConfig.TryGetValue("andereOption", out var v)
|
||||
&& v is JsonElement je
|
||||
&& je.GetBoolean();
|
||||
```
|
||||
|
||||
**Wichtig:** `context.ToolConfig` enthält nur die Config für dieses Tool und diesen Agent. Du bekommst nie die Config eines anderen Agents oder eines anderen Tools.
|
||||
|
||||
---
|
||||
|
||||
## Tool im Host registrieren
|
||||
|
||||
Im `Host/Program.cs` (oder einer Startup-Klasse) wird das Tool registriert:
|
||||
|
||||
```csharp
|
||||
var registry = new ToolRegistry();
|
||||
|
||||
// Jedes Tool einzeln registrieren
|
||||
registry.Register(new MeinToolTool());
|
||||
registry.Register(new DatabaseTool());
|
||||
registry.Register(new FileRWTool());
|
||||
// ...
|
||||
```
|
||||
|
||||
Der `AgentEngine` nutzt dann die `ToolRegistry`, um für jeden Agent nur die erlaubten Tools bereitzustellen.
|
||||
|
||||
---
|
||||
|
||||
## Geplante Tools (Referenz)
|
||||
|
||||
### Database (`ClawdDotNet.Tools.Database`)
|
||||
|
||||
| Eigenschaft | Wert |
|
||||
|---|---|
|
||||
| Name | `Database` |
|
||||
| NuGet | `MySqlConnector`, `MongoDB.Driver` |
|
||||
| Aktionen | `query`, `insert`, `upsert` |
|
||||
|
||||
**Config-Felder:**
|
||||
- `connectionString` – Datenbankverbindung (pro Agent!)
|
||||
- `type` – `"mysql"` oder `"mongodb"`
|
||||
- `allowedTables` – Liste erlaubter Tabellen (Whitelist)
|
||||
|
||||
**Sicherheit:**
|
||||
- Parameterized Queries gegen SQL-Injection
|
||||
- Nur Tabellen aus `allowedTables` erlaubt
|
||||
- Connection String kommt IMMER aus `context.ToolConfig`
|
||||
|
||||
---
|
||||
|
||||
### FileRW (`ClawdDotNet.Tools.FileRW`)
|
||||
|
||||
| Eigenschaft | Wert |
|
||||
|---|---|
|
||||
| Name | `FileRW` |
|
||||
| NuGet | (keine) |
|
||||
| Aktionen | `read`, `write`, `append`, `list`, `delete` |
|
||||
|
||||
**Config-Felder:**
|
||||
- `rootPath` – Basisverzeichnis (Agent ist darin eingesperrt)
|
||||
- `allowWrite` – `true`/`false`
|
||||
- `allowedExtensions` – z.B. `[".txt", ".json", ".html"]`
|
||||
|
||||
**Sicherheit (Pflicht):**
|
||||
- Path-Traversal-Check: `Path.GetFullPath(requested).StartsWith(Path.GetFullPath(rootPath))`
|
||||
- Nur erlaubte Dateiendungen
|
||||
- Schreibzugriff nur wenn `allowWrite = true`
|
||||
|
||||
---
|
||||
|
||||
### Mail (`ClawdDotNet.Tools.Mail`)
|
||||
|
||||
| Eigenschaft | Wert |
|
||||
|---|---|
|
||||
| Name | `Mail` |
|
||||
| NuGet | `MailKit` |
|
||||
| Aktionen | `send`, `read_inbox`, `read_message`, `mark_read` |
|
||||
|
||||
**Config-Felder:**
|
||||
- `imapHost`, `imapPort` – IMAP-Server
|
||||
- `smtpHost`, `smtpPort` – SMTP-Server
|
||||
- `username`, `password` – Zugangsdaten
|
||||
- `allowedRecipients` – Whitelist erlaubter Empfänger
|
||||
|
||||
**Sicherheit:**
|
||||
- Empfänger müssen in `allowedRecipients` stehen
|
||||
|
||||
---
|
||||
|
||||
## AgentToolContext – Referenz
|
||||
|
||||
```csharp
|
||||
public sealed record AgentToolContext(
|
||||
string AgentId, // ID des ausführenden Agents
|
||||
string InstanceId, // ID der laufenden ClawdDotNet-Instanz
|
||||
IReadOnlyDictionary<string, object?> ToolConfig, // Tool-Config für DIESEN Agent
|
||||
ILogger Logger, // Logger (schreibt in Tool_{ToolName}.log)
|
||||
CancellationToken CancellationToken
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ToolResult – Referenz
|
||||
|
||||
```csharp
|
||||
public sealed record ToolResult(
|
||||
bool Success,
|
||||
string Content, // Geht zurück ans LLM
|
||||
string? ErrorMessage = null
|
||||
);
|
||||
|
||||
// Hilfsmethoden:
|
||||
ToolResult.Ok("Ergebnis als JSON oder Text");
|
||||
ToolResult.Fail("Fehlerbeschreibung");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Background Jobs (optional)
|
||||
|
||||
Manche Tools müssen regelmäßig im Hintergrund arbeiten, ohne dabei jedes Mal den Agenten (und damit das LLM) aufzuwecken. Beispiel: Ein Telegram-Tool prüft alle 30 Sekunden auf neue Nachrichten, weckt den Agenten aber **nur** wenn tatsächlich eine neue Nachricht vorliegt.
|
||||
|
||||
Dafür gibt es das optionale Interface `IToolJobProvider`. Ein Tool kann es **zusätzlich** zu `IAgentTool` implementieren — es ist kein Ersatz.
|
||||
|
||||
### IToolJobProvider implementieren
|
||||
|
||||
```csharp
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using ClawdDotNet.Core.State;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public sealed class MeinToolTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
// ... IAgentTool-Implementierung wie oben ...
|
||||
|
||||
// 1. Verfügbare Job-Typen deklarieren
|
||||
public IReadOnlyList<ToolJobDefinition> GetJobDefinitions() =>
|
||||
[
|
||||
new("meintool_check", "MeinTool Check", "Prüft regelmäßig auf Änderungen")
|
||||
];
|
||||
|
||||
// 2. Job-Tick ausführen (kein LLM, nur Tool-Code!)
|
||||
public async Task<ToolJobResult> ExecuteJobAsync(
|
||||
string jobTypeId,
|
||||
IReadOnlyDictionary<string, object?> toolConfig,
|
||||
IStateStore stateStore,
|
||||
ILogger logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (jobTypeId != "meintool_check")
|
||||
return ToolJobResult.NoAction($"Unbekannter Job: {jobTypeId}");
|
||||
|
||||
// Config auslesen (gleiche Felder wie in ExecuteAsync)
|
||||
var apiKey = toolConfig.TryGetValue("apiKey", out var v)
|
||||
? v?.ToString() ?? "" : "";
|
||||
|
||||
// Zustand über IStateStore persistieren (überlebt Neustarts)
|
||||
var lastOffset = await stateStore.GetAsync("meintool_last_offset") ?? "0";
|
||||
|
||||
// Prüfung durchführen...
|
||||
var newItems = await CheckForUpdatesAsync(apiKey, lastOffset, ct);
|
||||
|
||||
if (newItems.Count == 0)
|
||||
return ToolJobResult.NoAction("Keine neuen Einträge");
|
||||
|
||||
// Offset speichern
|
||||
await stateStore.SetAsync("meintool_last_offset", newItems.Last().Id);
|
||||
|
||||
// Agent aufwecken mit Zusammenfassung
|
||||
return ToolJobResult.Wake(
|
||||
$"{newItems.Count} neue Einträge gefunden:\n" +
|
||||
string.Join("\n", newItems.Select(i => $"- {i.Title}")),
|
||||
logSummary: $"{newItems.Count} neue Einträge"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ToolJobResult
|
||||
|
||||
Das Tool gibt deklarativ zurück, ob der Agent geweckt werden soll:
|
||||
|
||||
| Methode | Effekt |
|
||||
|---------|--------|
|
||||
| `ToolJobResult.NoAction(logSummary?)` | Nichts tun, optional Log-Eintrag |
|
||||
| `ToolJobResult.Wake(wakeMessage, logSummary?)` | Agent **im bestehenden Chat** aufwecken (Default) |
|
||||
| `ToolJobResult.WakeStateless(wakeMessage, logSummary?)` | Agent in **isolierter Session** aufwecken (kein Chatverlauf) |
|
||||
|
||||
**`Wake` vs `WakeStateless`:**
|
||||
- `Wake` (Default) nutzt `ChatAsync` — die Nachricht erscheint im Chat-Tab, der Agent behält den gesamten Konversationsverlauf. Ideal für alles was Teil einer laufenden Interaktion ist (Telegram-Nachrichten, fertige Transkripte, E-Mail-Antworten).
|
||||
- `WakeStateless` nutzt `RunAsync` — komplett isolierte Ausführung ohne Kontext. Nur für einmalige, kontextfreie Aufgaben verwenden.
|
||||
|
||||
**Wichtig:** Das Tool hat keinen direkten Zugriff auf den `AgentEngine`. Der `ToolJobScheduler` wertet das Ergebnis aus und weckt den Agenten bei Bedarf.
|
||||
|
||||
### IStateStore für Zustandstracking
|
||||
|
||||
`IStateStore` ist ein einfacher Key-Value-Store (SQLite-basiert), der zwischen Job-Ticks und Neustarts persistiert:
|
||||
|
||||
```csharp
|
||||
// Wert lesen (null wenn nicht vorhanden)
|
||||
string? value = await stateStore.GetAsync("mein_key");
|
||||
|
||||
// Wert schreiben
|
||||
await stateStore.SetAsync("mein_key", "neuer_wert");
|
||||
```
|
||||
|
||||
Verwende aussagekräftige Keys, z.B. `meintool_poll_offset` oder `meintool_last_check`.
|
||||
|
||||
### JSON-Konfiguration
|
||||
|
||||
Tool Jobs werden pro Agent in der `config.json` konfiguriert, parallel zum bestehenden `scheduler`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agentId": "support-bot",
|
||||
"tools": {
|
||||
"MeinTool": { "apiKey": "..." }
|
||||
},
|
||||
"scheduler": [
|
||||
{ "cron": "0 8 * * 1-5", "taskMessage": "Morgenbericht erstellen" }
|
||||
],
|
||||
"toolJobs": [
|
||||
{
|
||||
"jobId": "abc12345",
|
||||
"toolName": "MeinTool",
|
||||
"jobTypeId": "meintool_check",
|
||||
"cron": "*/5 * * * *",
|
||||
"enabled": true,
|
||||
"runOnStart": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `jobId` | Eindeutige ID (wird automatisch generiert) |
|
||||
| `toolName` | Name des Tools (muss `IToolJobProvider` implementieren) |
|
||||
| `jobTypeId` | Einer der von `GetJobDefinitions()` deklarierten IDs |
|
||||
| `cron` | Cron-Ausdruck für die Ausführungsintervalle |
|
||||
| `enabled` | Job aktiv/inaktiv |
|
||||
| `runOnStart` | Beim Programmstart sofort einmal ausführen |
|
||||
|
||||
### Unterschied: Agent Wakeup vs Tool Job
|
||||
|
||||
| | Agent Wakeup (`scheduler`) | Tool Job (`toolJobs`) |
|
||||
|---|---|---|
|
||||
| **Ausführung** | Voller LLM-Run | Nur Tool-Code (kein LLM) |
|
||||
| **Kosten** | Token pro Tick | Kostenlos pro Tick |
|
||||
| **Agent-Aufruf** | Immer | Nur bei `ShouldWakeAgent` |
|
||||
| **Anwendungsfall** | Regelmäßige Aufgaben | Polling, Monitoring, Checks |
|
||||
|
||||
---
|
||||
|
||||
## Checkliste für neue Tools
|
||||
|
||||
- [ ] Eigenes Projekt `ClawdDotNet.Tools.{Name}` anlegen
|
||||
- [ ] Nur `ClawdDotNet.Core` referenzieren
|
||||
- [ ] `IAgentTool` implementieren
|
||||
- [ ] `InputSchema` als gültiges JSON Schema definieren
|
||||
- [ ] Alle Konfiguration aus `context.ToolConfig` lesen
|
||||
- [ ] Alle `await`-Aufrufe mit `CancellationToken` versehen
|
||||
- [ ] `context.Logger` für Logging verwenden (kein `Console.WriteLine`)
|
||||
- [ ] Sicherheitsprüfungen implementieren (je nach Tool-Typ)
|
||||
- [ ] Im Host registrieren: `registry.Register(new MeinToolTool());`
|
||||
- [ ] In der Solution-Datei (.slnx) einbinden
|
||||
- [ ] NuGet-Packages in `NuGet.Config` PackageSourceMapping ergänzen
|
||||
- [ ] *Optional:* `IToolJobProvider` implementieren (wenn Hintergrund-Polling nötig)
|
||||
- [ ] *Optional:* `GetJobDefinitions()` mit eindeutigen `JobTypeId`s definieren
|
||||
- [ ] *Optional:* `ExecuteJobAsync()` implementieren, `IStateStore` für Zustandstracking nutzen
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
Das Logging-System trennt automatisch nach Modul. Wenn dein Tool den Logger aus dem `AgentToolContext` verwendet, landen die Logs automatisch in:
|
||||
|
||||
```
|
||||
Logs/
|
||||
├── 2026-05-12/
|
||||
│ ├── Core.log ← Engine, Scheduler, Config
|
||||
│ ├── Host.log ← WinForms UI
|
||||
│ ├── Tool_Database.log ← Database-Tool
|
||||
│ ├── Tool_FileRW.log ← FileRW-Tool
|
||||
│ ├── Tool_MeinTool.log ← Dein Tool!
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
Die Zuordnung geschieht über den Namespace:
|
||||
- `ClawdDotNet.Core.*` → `Core.log`
|
||||
- `ClawdDotNet.Tools.{Name}.*` → `Tool_{Name}.log`
|
||||
- `ClawdDotNet.Host.*` → `Host.log`
|
||||
|
||||
Log-Level: `Debug`, `Info`, `Warn`, `Error`
|
||||
Reference in New Issue
Block a user