TaskMonkey Handbuch

Rezept: Daten-Browser per Chat

Mitarbeiter fragen Datenbank-Auswertungen in natürlicher Sprache an.

„Zeig mir alle Kunden mit mehr als 5 Bestellungen letzten Monat" — solche Anfragen sind klassisch BI-Tool-Material, oder Tickets ans Tech-Team. Mit TaskMonkey kannst du es deinem Team direkt verfügbar machen, sicher und ohne SQL-Kenntnisse.

Ziel

  • Mitarbeitende fragen in natürlicher Sprache nach Daten
  • Plattform übersetzt in SQL gegen Supabase
  • Ergebnis kommt als Tabelle in den Chat
  • Read-only — keine Schreib-Operationen, kein Risiko

Voraussetzungen

  • Supabase-Projekt mit den geschäftlichen Daten
  • Service-Key mit nur SELECT-Rechten (oder RLS-Setup)

1. API in apis.php

<?php
return [
    'apis' => [
        'supabase_readonly' => [
            'base_url' => 'https://xxx.supabase.co/rest/v1/',
            'headers' => [
                'apikey' => 'eyJhbGciOi…',  // read-only Key
                'Authorization' => 'Bearer eyJhbGciOi…',
                'Accept' => 'application/json',
            ],
        ],
    ],
];

Wichtig: Verwende einen Key, der nur SELECT kann. Entweder über RLS-Policies oder ein dediziertes DB-Rolle. Sonst kann das Modell theoretisch DELETE/UPDATE schicken.

2. Generisches Such-Tool

tools/data/queryTable.php:

<?php
return [
    'tools' => [
        'queryTable' => [
            'description' => 'Liest Daten aus einer der bekannten Tabellen mit PostgREST-Syntax. Tabellen: orders, customers, products, line_items.',
            'parameters' => [
                'type' => 'object',
                'properties' => [
                    'table' => [
                        'type' => 'string',
                        'enum' => ['orders', 'customers', 'products', 'line_items'],
                    ],
                    'select' => [
                        'type' => 'string',
                        'description' => 'Spalten, kommasepariert. Z.B. "id,total,customer_id"',
                    ],
                    'filter' => [
                        'type' => 'object',
                        'description' => 'PostgREST-Filter, z.B. {"status": "eq.pending", "total": "gt.100"}',
                    ],
                    'order' => [
                        'type' => 'string',
                        'description' => 'z.B. "created_at.desc"',
                    ],
                    'limit' => [
                        'type' => 'integer',
                        'description' => 'Max 100',
                    ],
                ],
                'required' => ['table'],
            ],
            'handler' => function (array $results, array $args, array $ctx): array {
                $params = [];
                if (!empty($args['select'])) $params['select'] = $args['select'];
                foreach (($args['filter'] ?? []) as $col => $val) {
                    $params[$col] = $val;
                }
                if (!empty($args['order'])) $params['order'] = $args['order'];
                $params['limit'] = min((int)($args['limit'] ?? 25), 100);

                $http = new \Cake\Http\Client();
                $url = "https://xxx.supabase.co/rest/v1/{$args['table']}?" . http_build_query($params);
                $apiKey = 'eyJhbGciOi…';

                $res = $http->get($url, [], [
                    'headers' => [
                        'apikey' => $apiKey,
                        'Authorization' => 'Bearer ' . $apiKey,
                    ],
                ]);

                return [
                    'rows' => $res->getJson() ?? [],
                    'count' => count($res->getJson() ?? []),
                ];
            },
        ],
    ],
];

3. Schema-Hilfs-Tool (optional)

Damit das Modell weiß, welche Spalten welche Tabelle hat:

tools/data/getSchema.php:

<?php
return [
    'tools' => [
        'getSchema' => [
            'description' => 'Liefert das Schema (Spalten und Typen) der bekannten Tabellen.',
            'parameters' => ['type' => 'object', 'properties' => []],
            'handler' => function (array $results, array $args, array $ctx) {
                return [
                    'orders' => [
                        'id', 'created_at', 'customer_id', 'total', 'status',
                        'shipping_method', 'payment_method',
                    ],
                    'customers' => [
                        'id', 'created_at', 'email', 'name', 'company', 'country',
                    ],
                    'products' => [
                        'id', 'sku', 'name', 'price', 'category', 'stock',
                    ],
                    'line_items' => [
                        'id', 'order_id', 'product_id', 'quantity', 'line_total',
                    ],
                ];
            },
        ],
    ],
];

4. Assistant

assistants/daten.php:

<?php
return [
    'assistants.daten' => [
        'name' => 'Daten-Browser',
        'description' => 'Datenbank-Auswertungen in natürlicher Sprache.',
        'icon' => '🔎',
        'tools' => [
            'getSchema',
            'queryTable',
            'setSuggestions',
        ],
        'greeting' => 'Welche Daten interessieren dich?',
        'suggestions' => [
            'Bestellungen heute',
            'Top-Kunden letzten Monat',
            'Lagerbestand kritisch',
        ],
    ],

    'assistants.daten.prompt' => <<<PROMPT
    Du bist der Daten-Browser. Mitarbeitende fragen nach
    geschäftlichen Daten in natürlicher Sprache, du wandelst sie in
    queryTable-Aufrufe.

    Vorgehensweise:
    1. Wenn unklar welche Tabelle: getSchema einmal aufrufen, um die
       Struktur zu kennen
    2. queryTable mit passenden Filtern bauen
    3. Ergebnis als Markdown-Tabelle zurückgeben — maximal 25 Zeilen
       in der Anzeige; bei mehr: "weitere X Zeilen vorhanden, soll ich
       filtern?"
    4. Bei Kennzahl-Fragen ("wie viele …"): nur die Zahl + Kontext

    Wichtig:
    - Niemals nach UPDATE/INSERT/DELETE fragen — du hast nur Lesezugriff
    - Bei Filter-Beispielen aus der natürlichen Sprache:
      "letzte Woche" = created_at >= heute - 7 Tage
      "über 100€" = total > 100
      "offen/pending" = status = 'pending'
    - PostgREST-Operatoren: eq, neq, gt, gte, lt, lte, like, ilike, in
    - Komplexere Auswertungen (Joins, Aggregate): erkläre was theoretisch
      möglich wäre, biete einen Workaround an

    Sprich kurz und sachlich. Keine Floskeln.
    PROMPT,
];

5. Testen

tm chat --task daten

You: zeig mir alle offenen Bestellungen über 200€
⚡ getSchema()
⚡ queryTable(table=orders, filter={status: eq.pending, total: gt.200}, ...)
Bot:
| ID  | Kunde       | Betrag  | Status  |
| --- | ----------- | ------- | ------- |
| 1042 | Müller GmbH | 287,90 € | pending |
| 1067 | Schmidt KG | 425,00 € | pending |
...

Sicherheitshinweise

  • Keine sensiblen Spalten im getSchema listen, die das Modell nicht ausliefern soll (z. B. internal_notes, password_hash)
  • RLS in Supabase aktivieren und den Read-Key entsprechend einschränken
  • Audit: tm monitor zeigt alle Queries — regelmäßig durchschauen
  • Keine personenbezogenen Daten an LLM-Provider geben, wenn nicht durch Auftragsverarbeitung gedeckt

Erweiterungen

  • Mehr Tabellen freigeben schrittweise, je nach Bedarf
  • Vorgefertigte Reports als zusätzliche Tools (z. B. topCustomersByRevenue) — wenn dieselbe Anfrage immer wieder kommt
  • Export: Tool, das das Ergebnis als CSV in Dropbox legt
  • Visualisierung: bei aggregierten Daten ein Chart-Tool aufrufen, das eine QuickChart-URL zurückgibt
Zuletzt aktualisiert: 2026-04-20