Das Harness-Modul: KI-Assistent

Das Modul harness ermöglicht es einem KI-Agenten, im Administrationsbereich mit den Daten der Anwendung zu arbeiten.

Der Agent führt weder beliebigen Code noch beliebige SQL-Abfragen aus. Ihm steht nur ein kleiner, expliziter Satz von Werkzeugen zur Verfügung, der aus dem Anwendungsmodell generiert wird:

  • Werkzeuge zum Lesen von Daten,
  • Werkzeuge, die ein Formular oder einen Löschbestätigungsdialog öffnen.

Das Paket app/harness selbst kennt die Domäne der Anwendung nicht. Werkzeugkatalog, Feldregister der Entitäten und Berechtigungsregeln liefert der aus herd/_model.goat generierte Code, genauso wie beim Dashboard.

Die wichtigste Regel: Die KI ändert keine Daten

Harness-Werkzeuge gibt es in zwei Arten:

  • backend — werden auf dem Server ausgeführt, nur lesend,
  • frontend — werden nie auf dem Server ausgeführt.

Ein Frontend-Werkzeug speichert oder löscht nichts. Es wandelt die Anfrage des Agenten in eine Browser-Aktion um: das Öffnen eines vorausgefüllten Formulars oder eines Löschbestätigungsdialogs mit den echten Daten des Datensatzes.

Eine Änderung erfolgt erst, wenn ein Mensch das Formular selbst absendet oder das Löschen selbst bestätigt.

Außerdem darf ein Gesprächszug höchstens eine Frontend-Aktion enthalten. Die Schleife stoppt beim ersten solchen Aufruf, damit nichts ohne Wissen des Nutzers passiert.

Dieselben Regeln sind Teil des System-Prompts des Modells:

  • das Modell darf Daten nie selbst anlegen, ändern oder löschen,
  • Backend-Werkzeuge dienen nur zum Lesen,
  • das Modell darf keine Werkzeugnamen, Feldnamen oder Datensatz-IDs erfinden,
  • es antwortet in der Sprache, in der der Nutzer schreibt.

Modul für eine Entität aktivieren

Moduldefinition

Das Modul ist im Modell als Entitätsmodul mit zwei Feldmengen definiert:

entity:module:def --name harness --property:list:list=<<MODULEEOF
    def --required
MODULEEOF --property:list:persist=<<MODULEEOF
    def --required
MODULEEOF
  • list — Felder, die der Agent lesen darf: vom Abfragewerkzeug zurückgegeben und im Löschbestätigungsdialog angezeigt,
  • persist — Felder, die der Agent in einem Formular zum Anlegen oder Bearbeiten vorausfüllen darf.

Modul zu einer Entität hinzufügen

Eine Entität wird dem Agenten mit module:add --name=harness zugänglich gemacht. Beispiel für die Entität user:

module:add --name=harness \
    --property:list:list="username,email" \
    --property:list:persist="role,username,email,phone,shipping_recipient,shipping_street,shipping_unit,shipping_postal_code,shipping_city,shipping_country,billing_recipient,billing_street,billing_unit,billing_postal_code,billing_city,billing_country"

Die Menge persist lässt password und email_verified_at bewusst aus. Der Agent kann für diese Felder keine Werte vorschlagen, selbst wenn die Rolle des Nutzers sie schreiben darf.

Wähle diese Felder genauso sorgfältig wie die Felder für das Modul crud: Es ist eine bewusste Entscheidung darüber, was der Assistent sehen und was er vorschlagen darf.

Gesprächsentitäten

Der Gesprächsverlauf wird in zwei Modellentitäten gespeichert:

  • ai_conversation — ein Gespräch, das einem Nutzer gehört,
  • ai_message — eine einzelne Nachricht in einem Gespräch.

Beide Entitäten müssen in herd/_model.goat vorhanden sein. Sie haben kein crud-Modul und werden daher nicht über die öffentliche CRUD-API bereitgestellt. Nur der Harness-Endpunkt liest und schreibt sie.

Die Integration ist nur aktiv, wenn das Modell gleichzeitig:

  • mindestens eine Entität mit dem Modul harness enthält,
  • die Entitäten ai_conversation und ai_message enthält.

Andernfalls tut die generierte Funktion InitHarness nichts, und der Endpunkt wird nicht registriert.

Codegenerierung

Generiere die Anwendung nach einer Modelländerung neu:

goat re

Der Generator erzeugt unter anderem:

goatapp/app/api/modelapi/harness_gen.go
goatapp/app/harness/store_gen.go

sowie im Modellpaket jeder Entität die Masken:

<Entität>HarnessListMask
<Entität>HarnessPersistMask

Die list-Maske enthält immer das Feld id, damit der Agent auf einen bestimmten Datensatz verweisen kann.

Bearbeite diese Dateien nicht von Hand. Das gesamte Verzeichnis goatapp/ wird generiert.

Generierte Werkzeuge

Für jede Entität mit dem Modul harness entstehen drei Werkzeuge:

WerkzeugArtVerhalten
query_<entität>backendLiest Datensätze zur Analyse. Ändert nie Daten.
open_<entität>_formfrontendÖffnet ein Formular zum Anlegen oder Bearbeiten, optional vorausgefüllt.
confirm_delete_<entität>frontendÖffnet einen Löschbestätigungsdialog mit den aktuellen Daten des Datensatzes.

Die Entität product erhält zum Beispiel query_product, open_product_form und confirm_delete_product.

Argumente von query_<entität>

{
  "fields": ["title", "sku"],
  "filters": {"status": "active"},
  "limit": 20
}
  • fields — Teilmenge der zurückzugebenden Felder; ohne Angabe werden alle verfügbaren Felder geliefert,
  • filters — Filter mit exakter Übereinstimmung, nach Feldname,
  • limit — maximale Anzahl der Zeilen; standardmäßig 20, höchstens 50.

Filter vergleichen nur auf Gleichheit. Es gibt keine Bereichsoperatoren, keine Volltextsuche und keine Sortierung.

Argumente von open_<entität>_form

{
  "id": "optionale-datensatz-id",
  "fields": {"title": "Neues Produkt"}
}
  • ohne id wird ein Formular zum Anlegen geöffnet,
  • mit id wird das Bearbeitungsformular eines bestehenden Datensatzes geöffnet,
  • fields enthält die vorauszufüllenden Werte.

Argumente von confirm_delete_<entität>

{"id": "datensatz-id"}

Das Feld id ist Pflicht.

Berechtigungen

Der effektive Zugriff des Agenten ist immer die Konjunktion zweier Masken:

  • der Maske der Rolle des angemeldeten Nutzers,
  • der Maske des Moduls harness der Entität.

Keine der beiden reicht allein. Ein Agent, der für einen Nutzer mit der Rolle user handelt, sieht keine Felder, die diese Rolle nicht lesen darf, selbst wenn sie in der Menge list stehen. Er sieht auch keine Felder außerhalb von list, selbst wenn die Rolle sie lesen darf.

In der Praxis bedeutet das:

  • ein der Entität unbekanntes Feld wird mit einem Fehler abgelehnt,
  • ein für den Nutzer nicht zugängliches Feld fehlt im Abfrageergebnis,
  • ein Filter auf ein nicht zugängliches Feld führt zu einem Fehler,
  • Vorausfüllwerte für Formulare werden durch die Schreibmaske der Rolle und die Menge persist gefiltert,
  • die Datensatzvorschau im Löschdialog enthält nur lesbare Felder.

Existiert der Datensatz nicht oder ist keines seiner Felder lesbar, bleibt die Vorschau einfach leer. So kann der Nutzer „kein Zugriff“ nicht von „kein solcher Datensatz“ unterscheiden.

Tabellen- und Spaltennamen stammen aus dem generierten Register und werden validiert. Vom Modell gelieferte Werte gelangen nur als Abfrageparameter in SQL.

Der Endpunkt POST /api/ai/chat

Der Endpunkt erfordert eine angemeldete Sitzung.

Anfrage:

{
  "conversationId": "optionale-gespraechs-id",
  "message": "Zeig die neuesten Bestellungen"
}

Antwort:

{
  "conversationId": "gespraechs-id",
  "reply": "I've opened a form to add this product. Please review it and submit to confirm.",
  "action": {
    "path": "/admin/product",
    "queryParams": {"aiAction": "create", "aiPrefill": "{\"title\":\"Neues Produkt\"}"}
  }
}

Das Feld action ist nur vorhanden, wenn das Modell ein Frontend-Werkzeug angefordert hat. In diesem Fall ist reply eine feste, vom Server erzeugte Meldung (derzeit auf Englisch) und kein vom Modell geschriebener Text.

Fehlercodes:

  • 401 — keine angemeldete Sitzung,
  • 400 — leere Nachricht oder ungültiges JSON,
  • 500 — Fehler beim Laden des Verlaufs oder beim Aufruf des Modells.

Ablauf eines Gesprächszugs

Gesprächsverlauf + neue Nachricht
        ↓
das Modell wählt ein Werkzeug
        ↓
backend: Lesen ausführen und zum Modell zurückkehren
frontend: Aktion an den Browser zurückgeben und den Zug beenden
        ↓
Textantwort

Ein Zug darf höchstens 5 Runden mit Backend-Werkzeugen ausführen. Wird das Limit überschritten, gibt der Endpunkt einen Fehler zurück, damit das Modell nicht endlos in einer Schleife läuft.

Gesprächsverlauf

Gespeichert werden nur Nachrichten des Nutzers und des Assistenten. Zwischenzeitliche Werkzeugaufrufe existieren nur innerhalb eines Zugs und werden nie in spätere Anfragen übernommen.

Ein Gespräch gehört dem Nutzer, der es begonnen hat. Wird die conversationId eines fremden Gesprächs gesendet, verhält es sich so, als gäbe es dieses Gespräch nicht.

Frontend

Dashboard-Widget

Der Chat ist ein Dashboard-Widget vom Typ ai_chat. In dieser Anwendung ist er in herd/_model.goat definiert:

section:add --name=assistant --type=cards --roles=admin,manager,user
widget:add --name=ai_assistant --type=ai_chat --label="AI Assistant"

Das Widget ai_chat akzeptiert keine Datenquelle, keine Aggregation, kein Diagramm und keine Abfrage. Es kommuniziert ausschließlich mit /api/ai/chat.

Ausführen einer Aktion

Enthält die Antwort action, navigiert AiChatService zur generierten Ansicht der Entität:

/admin/<entität-in-kebab-case>

mit diesen Parametern:

  • aiAction — create, edit oder delete,
  • aiPrefill — JSON mit Werten zum Vorausfüllen des Formulars zum Anlegen,
  • aiId — ID des zu bearbeitenden oder zu löschenden Datensatzes,
  • aiPreview — JSON mit der Datensatzvorschau für den Löschbestätigungsdialog.

Der generierte Entitätsmanager liest diese Parameter, öffnet das passende Formular oder den Dialog und entfernt die Parameter aus der URL.

Das Vorausfüllen gilt für das Formular zum Anlegen. Bei edit öffnet der Manager den bestehenden Datensatz anhand von aiId und zeigt seine aktuellen Werte.

Konfiguration

Der OpenAI-Client wird über Umgebungsvariablen konfiguriert:

VariableBedeutungStandard
GOAT_AI_OPENAI_API_KEYOpenAI-API-Schlüssel für den Assistentenkeiner
GOAT_AI_OPENAI_MODELVom Assistenten verwendetes Chat-Modellgpt-4o-mini

Bei leerem Schlüssel wird der Endpunkt trotzdem registriert, aber jede Anfrage schlägt fehl, und das Widget meldet, dass der Assistent nicht verfügbar ist.

Verwechsle diese Variablen nicht mit GOAT_OPENAI_API_KEY, die von den Übersetzungsskripten (herd/translate.goat, translate-docs.goat) verwendet wird.

Der Schlüssel ist ein Geheimnis. Bewahre ihn in deiner lokalen .env oder in der Deployment-Konfiguration auf und committe ihn nie ins Repository. herd/deploy.goat deklariert GOAT_AI_OPENAI_API_KEY als Geheimnis und GOAT_AI_OPENAI_MODEL als normale Variable.

Datenschutz

Ergebnisse der Backend-Werkzeuge werden als Teil des Gesprächs an das Modell gesendet. Jedes Feld der Menge list, das der Nutzer lesen darf, kann also an OpenAI gelangen.

Ein Feld in list aufzunehmen ist eine Entscheidung, es mit einem externen KI-Anbieter zu teilen. Nimm dort keine Passwörter, Tokens oder Daten auf, die nicht außerhalb der Anwendung verarbeitet werden dürfen.

Tests

Die Logik des Pakets wird ohne Verbindung zu OpenAI getestet. Run nimmt eine Completer-Schnittstelle entgegen, die die Tests durch einen gefälschten Client ersetzen:

(cd goatapp && GOWORK=off go test ./app/harness/)

Der generierte E2E-Test goatapp/web/e2e-server/harness.spec.ts prüft immer, dass anonyme Anfragen (401) und leere Nachrichten (400) abgelehnt werden.

Ein vollständiges Gespräch ruft den konfigurierten KI-Anbieter auf und läuft deshalb nur, wenn du diese Variable beim Ausführen der Playwright-Tests ausdrücklich setzt:

GOAT_E2E_HARNESS_CHAT=1

Zusammenfassung

  • Das Modul harness stellt eine Entität dem KI-Assistenten über drei generierte Werkzeuge bereit.
  • Lesen ist durch die Konjunktion aus Rollenmaske und Menge list begrenzt.
  • Das Vorausfüllen von Formularen ist durch die Schreibmaske der Rolle und die Menge persist begrenzt.
  • Der Assistent speichert oder löscht nie selbst Daten; das tut immer ein Mensch.
  • Benötigt werden die Entitäten ai_conversation und ai_message sowie der Schlüssel GOAT_AI_OPENAI_API_KEY.