Tool als Config-Datei anlegen
Der empfohlene Weg für produktive Tools — versioniert, review-bar, testbar.
Für alles, was produktiv laufen soll, sind Config-Dateien der richtige Ort. Sie landen in Git, lassen sich im Review prüfen und können Helfer-Funktionen nutzen.
Auto-Discover — keine Registrierung nötig
Der Tenant-Loader scannt bei jedem Request alle .php-Dateien in config/tenants/<code>/ rekursiv und merged sie. Ein neues Tool tools/xyz.php mit return ['tools' => ['xyz' => [...]]] ist beim nächsten Request live.
Nicht in main.php registrieren. Nicht irgendwo importieren. Datei anlegen → fertig. Wenn du "Tool xyz nicht in tools registriert" siehst: der PHP-OPcache ist noch nicht durch — 1× Reload reicht typischerweise.
Sicherheits-Regeln für Handler-Code
Tool-Handler dürfen keine nativen PHP-Funktionen für Datei-I/O oder Prozesse benutzen. writeTenantConfig scannt jeden neuen Handler-Code darauf und lehnt sonst ab.
Verboten:
- Schreiben:
file_put_contents,fwrite,fputs,fputcsv,unlink,mkdir,rename,copy,chmod,touch - Lesen:
file_get_contents,file,readfile,fread,fgets,fgetcsv,fopen,glob,scandir,opendir - Prozess:
exec,shell_exec,system,passthru,popen,proc_open - Code-Injection:
eval,create_function,unserialize, dynamischeinclude $var
Nutze stattdessen die vorgesehenen _shared-Tools:
| Use-Case | Tool |
|---|---|
Datei in <tenant>/files/ schreiben |
writeFile |
Datei aus <tenant>/files/ lesen |
readFile |
Verzeichnis-Listing in <tenant>/files/ |
listFilesInPath |
Config-File schreiben (<tenant>/*.php) |
writeTenantConfig |
| Config-File lesen | readTenantConfig |
| Config-File löschen | deleteTenantConfig |
| Datei zum User-Download bereitstellen | provideDownload |
| Google Drive lesen/schreiben | readDriveFile / uploadDriveFile |
| Datenbank | CakePHP ConnectionManager::get('default') |
Der Grund: die vorgesehenen Tools haben Path-Traversal-Schutz, Auth-Checks, Backup, Logging und Tenant-Isolation. Deine native file_put_contents() hat nichts davon.
Wo landen Dateien?
<tenant>/files/— Runtime-Daten (Uploads, Reports, Caches). NICHT im Git,.gitignoreschließt es aus. Genutzt viawriteFile/readFile.<tenant>/*.php— Config (Assistants, Tools, APIs, Scheduled). IM Git, versioniert, review-bar. Genutzt viawriteTenantConfig/deleteTenantConfig.<tenant>/_backups/— Auto-Backups von writeTenantConfig/deleteTenantConfig. NICHT im Git.<tenant>/logs/,<tenant>/tmp/— Runtime-Logs/Temp. NICHT im Git.
Ordnerstruktur
In deinem Workspace:
tools/
├── dropbox/
│ ├── uploads/
│ │ └── uploadFile.php
│ └── listing/
│ └── listFiles.php
├── jtl/
│ └── orders/
│ └── getOrder.php
└── knowledge/
└── getKnowledge.php
Die Gruppierung nach API und Kategorie ist Konvention. Du darfst sie brechen, wenn du willst — der Loader scannt rekursiv, die Ordnernamen sind nur für dich.
Datei-Template
Jede .php-Datei gibt ein Array zurück, das einen oder mehrere Tools definiert. Der Schlüssel tools.<toolName> ist dabei zwingend.
<?php
return [
'tools' => [
'getInvoice' => [
'description' => 'Rechnung per ID abrufen.',
'parameters' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
'description' => 'Rechnungs-ID wie in JTL angezeigt.',
],
],
'required' => ['id'],
],
'api' => 'jtl',
'method' => 'GET',
'path' => '/invoices/{id}',
'mapping' => [
'id' => 'invoice_id',
'total' => 'amounts.gross',
'customer' => 'customer.name',
],
],
],
];
Mit Preprocess und Postprocess
<?php
return [
'tools' => [
'getInvoice' => [
// ... wie oben ...
'preprocess' => function (array $args, $ctx): array {
$args['id'] = strtoupper($args['id']);
return $args;
},
'postprocess' => function (array $result, array $args, $ctx): array {
$result['totalWithTax'] = round($result['total'] * 1.19, 2);
return $result;
},
],
],
];
Handler-only
Ohne API, reine PHP-Logik:
<?php
return [
'tools' => [
'calculateTax' => [
'description' => 'Netto-Betrag mit MwSt. berechnen.',
'parameters' => [
'type' => 'object',
'properties' => [
'net' => ['type' => 'number'],
'rate' => ['type' => 'number', 'description' => 'z. B. 0.19'],
],
'required' => ['net', 'rate'],
],
'handler' => function (array $results, array $args, array $ctx): array {
$gross = $args['net'] * (1 + $args['rate']);
return [
'net' => $args['net'],
'tax' => round($gross - $args['net'], 2),
'gross' => round($gross, 2),
];
},
],
],
];
Handler-Signatur — genau drei Parameter
Der Handler bekommt IMMER drei Parameter in dieser Reihenfolge:
'handler' => function (array $results, array $args, array $ctx): array { … }
$results— Ergebnisse vorheriger Tool-Aufrufe im selben Turn (Nested-Calls). Meistens leer, aber Positional-Argument, nicht weglassen.$args— die vom LLM übergebenen Argumente (validiert gegen deinparameters-Schema).$ctx— der Runtime-Context:$ctx['tenant'],$ctx['chat_id'],$ctx['config'],$ctx['files_path'],$ctx['logger'], …
Falsche Signaturen wie function ($args, $context) (zwei Parameter, ohne array-Typing) crashen zur Laufzeit — der ToolRunner erwartet drei Argumente.
❌ Anti-Pattern — NICHT so schreiben
Verwechsele unser Tool-Schema nicht mit dem OpenAI Function-Calling-Schema. Sie sehen ähnlich aus, sind aber nicht kompatibel.
Falsch (OpenAI-Style, wird vom Runner ignoriert):
return [
'tools' => [
'getInvoice' => [
'type' => 'function', // ❌ existiert bei uns nicht
'function' => [ // ❌ falsch verschachtelt
'name' => 'getInvoice',
'description' => '…',
'parameters' => [ … ],
],
'handler' => function ($args, $ctx) { … }, // ❌ falsche Signatur
],
],
];
Richtig (unser Schema, flach unter dem Tool-Namen):
return [
'tools' => [
'getInvoice' => [
'description' => '…', // ✅ direkt hier, kein Nest
'parameters' => [ … ],
'handler' => function (array $results, array $args, array $ctx): array { … },
],
],
];
Wenn du type => function oder function => [...] innerhalb eines Tool-Eintrags siehst: das ist OpenAI-Function-Call-Notation, die ChatGPT/Claude beim Bauen halluziniert hat. Umbauen zum flachen Schema — sonst wird das Tool nie aufgerufen.
Mehrere Tools pro Datei
Möglich, aber: eine Datei pro Tool hält Diffs klein und Namen eindeutig. Nur gruppieren, wenn Tools sehr eng zusammengehören und z. B. dieselbe Helferfunktion teilen.
Test-Fixture
Für schnelle lokale Tests kannst du einem Tool ein Args-Fixture mitgeben:
'args_fixture' => ['id' => 'INV-2026-001'],
Damit läuft:
tm test-tool getInvoice
ohne dass du die Args manuell eintippen musst — das Fixture wird automatisch verwendet.
Shared Tools
Tools, die über mehrere Workspaces hinweg sinnvoll sind (z. B. getKnowledge), leben in einem geteilten Bereich. Du referenzierst sie mit extends:
return [
'tools' => [
'getKnowledge' => [
'extends' => 'getKnowledge',
'description' => 'Durchsucht unsere Produkt-Wissensbasis.',
'args_fixture' => ['query' => 'Tomaten'],
],
],
];
Das zieht die Basis-Definition aus dem Shared-Bereich und erlaubt dir, einzelne Felder zu überschreiben.
Nach dem Anlegen
- Konfig wird beim nächsten Request automatisch neu geladen (Dev).
- In Production: je nach Setup ggf.
bin/cake cache clear_alloder Deploy nötig. - Das Tool ist erst sichtbar, wenn es in der
tools-Liste eines Chats oder Assistants steht.