API-Features — was du deklarativ machen kannst, ohne Handler
Preprocess, mapping, collection, body_transform, _query, Placeholder — die vollständige Feature-Liste für API-Tools.
API-Features — deklarativ statt Handler
Wenn dein Tool mit einer externen HTTP-API redet: schreibe keinen Handler, wenn du es vermeiden kannst. Der ToolRunner hat einen kompletten deklarativen Layer der HTTP-Requests, Response-Umformung und Auth für dich macht.
Ein Handler ist nur dann sinnvoll wenn:
- die Logik echte Berechnung ist (nicht nur Daten-Umformung),
- mehrere API-Calls verkettet werden müssen,
- oder wenn du auf
$ctx-Zeug angewiesen bist das die deklarativen Features nicht abdecken.
Sonst: api + endpoint + optional mapping/preprocess/postprocess reicht.
Minimal-Beispiel — API-only, kein Handler
'getInvoice' => [
'description' => 'Rechnung per ID abrufen.',
'parameters' => [
'type' => 'object',
'properties' => ['id' => ['type' => 'string']],
'required' => ['id'],
],
'api' => 'jtl', // → apis.jtl aus tenantConfig
'method' => 'GET', // Default: GET
'endpoint' => '/invoices/{id}', // {id} wird aus $args['id'] ersetzt
],
Der Runner:
- Löst
apis.jtl.base_url+apis.jtl.headersauf (inkl.{{secret:...}}-Expansion). - Ersetzt
{id}im endpoint durch$args['id']. - Feuert GET, gibt die JSON-Response ans LLM.
Kein Handler, kein Cake\Http\Client, kein ->getJson() — alles vom Runner erledigt.
URL-Placeholder — jeder skalare Arg ersetzt {arg_name}
Alle scalaren Argumente ersetzen automatisch {arg_name} im endpoint. Nested/Object-Args werden übersprungen.
'endpoint' => '/customers/{customer_id}/orders/{order_id}',
Wenn $args = ['customer_id' => 42, 'order_id' => 'ORD-99']: ergibt /customers/42/orders/ORD-99.
Query-String — via _query aus preprocess
Wenn du dynamische Query-Parameter brauchst (Filter, Pagination), setze sie im preprocess-Callback als _query-Array. Der Runner url-encoded das automatisch an den endpoint an.
'listOrders' => [
'description' => 'Bestellungen auflisten (Filter: status, datum-von/bis).',
'parameters' => [
'type' => 'object',
'properties' => [
'status' => ['type' => 'string', 'enum' => ['open', 'shipped']],
'since' => ['type' => 'string', 'description' => 'YYYY-MM-DD'],
],
],
'api' => 'shopify',
'method' => 'GET',
'endpoint' => '/orders.json',
'preprocess' => function (array $args, array $ctx): array {
$q = [];
if (!empty($args['status'])) $q['status'] = $args['status'];
if (!empty($args['since'])) $q['created_at_min'] = $args['since'];
return ['_query' => $q];
},
],
mapping — Response-Felder umbenennen und flach ziehen
Wenn die API ein verschachteltes JSON zurückgibt und du dem LLM nur die relevanten Felder mit sauberen Namen zeigen willst:
'getInvoice' => [
'api' => 'jtl',
'endpoint' => '/invoices/{id}',
'mapping' => [
'id' => 'invoice_id', // → response.invoice_id
'total' => 'amounts.gross', // → response.amounts.gross (dot-notation)
'customer' => 'customer.name',
'items' => 'lines',
],
],
Response { "invoice_id": "INV-1", "amounts": { "gross": 119, "net": 100 }, "customer": { "name": "ACME" } } wird zu { "id": "INV-1", "total": 119, "customer": "ACME" }.
collection — Array-Response entnesten
Wenn die API ein Wrapping-Objekt zurückgibt (z.B. { "data": [...], "meta": {...} }) und du nur das Array willst:
'listInvoices' => [
'api' => 'jtl',
'endpoint' => '/invoices',
'collection' => 'data', // greift auf response.data zu
'mapping' => [
'id' => 'invoice_id',
'total' => 'amounts.gross',
],
],
collection wird BEFORE mapping angewendet — jede Zeile im Array wird dann durch das Mapping gejagt.
Dot-Notation funktioniert auch: 'collection' => 'response.items' greift auf response.items zu.
body_transform — Body vor dem POST umformen
Bei POST/PUT/PATCH werden $args per default 1:1 als Body geschickt. Wenn die API einen anderen Body-Shape erwartet:
'createTicket' => [
'api' => 'zendesk',
'method' => 'POST',
'endpoint' => '/tickets.json',
'json' => true,
'body_transform' => function (array $args): array {
return [
'ticket' => [
'subject' => $args['title'],
'comment' => ['body' => $args['message']],
'priority' => $args['priority'] ?? 'normal',
],
];
},
],
$args = ['title' => 'X', 'message' => 'Y'] → Body wird zu {"ticket": {"subject":"X", "comment":{"body":"Y"}, "priority":"normal"}}.
json — JSON-Encoding aktivieren
Standardmäßig serialisiert der Runner den Body als form-encoded. Für JSON-APIs:
'createTicket' => [
'api' => 'zendesk',
'method' => 'POST',
'endpoint' => '/tickets.json',
'json' => true, // → Content-Type: application/json, Body als JSON
],
postprocess — Response nachbearbeiten (statt Handler)
Für Berechnungen die auf dem API-Result laufen sollen — bevor der Handler-Fallback greift.
'getInvoice' => [
'api' => 'jtl',
'endpoint' => '/invoices/{id}',
'postprocess' => function (array $result, array $args, array $ctx): array {
$result['totalWithTax'] = round($result['net'] * 1.19, 2);
return $result;
},
],
Entscheidungs-Baum: brauche ich einen Handler?
| Situation | Handler nötig? | Alternative |
|---|---|---|
| Ein HTTP-Call, Response direkt zurückgeben | ❌ | api + endpoint |
| Response-Felder umbenennen/entnesten | ❌ | mapping + evtl. collection |
| Dynamischer Query-String (Filter, Pagination) | ❌ | preprocess mit _query |
| Body für POST muss umgeformt werden | ❌ | body_transform + json: true |
| Netto → Brutto rechnen auf Response | ❌ | postprocess |
| Mehrere API-Calls verketten | ✅ | Handler oder Nested-Tool-Call |
| Datei generieren und speichern | ✅ | Handler mit writeFile |
| Reine Berechnung ohne API | ✅ | Handler-only |
| DB-Query mit ConnectionManager | ✅ | Handler-only |
Vor jedem Handler-Prompt: frag dich ob 3-4 deklarative Zeilen dasselbe können. Wenn ja: keinen Handler.
Kompletter Feature-Katalog
Alle Keys die im Tool-Config-Array erkannt werden:
| Key | Typ | Zweck |
|---|---|---|
api |
string | Referenz auf apis.<name> im Tenant-Config |
method |
string | GET/POST/PUT/PATCH/DELETE (Default: GET) |
endpoint |
string | Pfad, mit {arg}-Placeholder |
json |
bool | Body als JSON encoden (Content-Type: application/json) |
content_type |
string | Override für Content-Type (z.B. image/png für Media-Upload) |
content_disposition_filename |
string | Für Multipart-Uploads, {arg}-Placeholder erlaubt |
preprocess |
callable | function(array $args, array $ctx): array — merged Result in $args. Special-Key _query wird zum URL-Query-String |
postprocess |
callable | function(array $result, array $args, array $ctx): array — läuft nach API-Response, vor Mapping |
body_transform |
callable | function(array $args): array — formt POST-Body um |
mapping |
array | ['newKey' => 'response.dot.path'] — flacht/renamed Response-Felder |
collection |
string | Dot-Path zum Array in der Response (z.B. 'data' oder 'response.items') |
handler |
callable | Fallback für alles was nicht deklarativ geht. function(array $results, array $args, array $ctx): array |
description |
string | Was das Tool tut (fürs LLM) |
parameters |
array | JSON-Schema für die Args |
statusMessages |
array | Kurze Meldungen die während der Ausführung im Chat gezeigt werden |
args_fixture |
array | Default-Args für tm test-tool |
internal |
bool | Ist das Tool "intern" (nicht dem User erwähnenswert) |
async |
bool | Läuft in Background-Queue statt inline |
extends |
string | Basis-Definition aus _shared erben |