Moduł harness: asystent AI

Moduł harness pozwala agentowi AI pracować z danymi aplikacji z poziomu panelu administracyjnego.

Agent nie wykonuje dowolnego kodu ani dowolnych zapytań SQL. Ma do dyspozycji wyłącznie mały, jawny zestaw narzędzi, wygenerowany na podstawie modelu aplikacji:

  • narzędzia do odczytu danych,
  • narzędzia otwierające formularz lub okno potwierdzenia usunięcia.

Sam pakiet app/harness nie zna domeny aplikacji. Katalog narzędzi, rejestr pól encji i reguły uprawnień dostarcza kod wygenerowany z herd/_model.goat, podobnie jak w przypadku dashboardu.

Najważniejsza zasada: AI nie zmienia danych

Narzędzia harness dzielą się na dwa rodzaje:

  • backend — wykonywane na serwerze, wyłącznie do odczytu,
  • frontend — nigdy niewykonywane na serwerze.

Narzędzie frontendowe nie zapisuje ani nie usuwa danych. Zamienia prośbę agenta w akcję przeglądarki: otwarcie wstępnie wypełnionego formularza albo okna potwierdzenia usunięcia z prawdziwymi danymi rekordu.

Zmiana następuje dopiero wtedy, gdy człowiek sam zatwierdzi formularz lub potwierdzi usunięcie.

Dodatkowo w jednej turze rozmowy może wystąpić najwyżej jedna akcja frontendowa. Pętla zatrzymuje się na pierwszym takim wywołaniu, aby nic nie działo się bez wiedzy użytkownika.

Te same reguły trafiają do promptu systemowego modelu:

  • model nie może sam tworzyć, zmieniać ani usuwać danych,
  • narzędzia backendowe służą tylko do odczytu,
  • model nie może wymyślać nazw narzędzi, pól ani identyfikatorów rekordów,
  • odpowiada w języku, w którym pisze użytkownik.

Włączenie modułu dla encji

Definicja modułu

Moduł jest zdefiniowany w modelu jako moduł encji z dwoma zbiorami pól:

entity:module:def --name harness --property:list:list=<<MODULEEOF
    def --required
MODULEEOF --property:list:persist=<<MODULEEOF
    def --required
MODULEEOF
  • list — pola, które agent może czytać: zwracane przez narzędzie zapytań i pokazywane w oknie potwierdzenia usunięcia,
  • persist — pola, które agent może wstępnie wypełnić w formularzu tworzenia lub edycji.

Dodanie modułu do encji

Encja udostępnia się agentowi przez module:add --name=harness. Przykład dla encji 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"

Zbiór persist celowo pomija password i email_verified_at. Agent nie może zaproponować wartości tych pól nawet wtedy, gdy rola użytkownika pozwala je zapisywać.

Dobór pól traktuj tak samo jak dobór pól dla modułu crud: to jawna decyzja, co asystent może zobaczyć i co może podpowiedzieć.

Encje rozmów

Historia rozmów jest zapisywana w dwóch encjach modelu:

  • ai_conversation — rozmowa należąca do jednego użytkownika,
  • ai_message — pojedyncza wiadomość w rozmowie.

Obie encje muszą istnieć w herd/_model.goat. Nie mają modułu crud, więc nie są udostępniane przez publiczne API CRUD. Czyta je i zapisuje wyłącznie endpoint harness.

Integracja jest aktywna tylko wtedy, gdy model jednocześnie:

  • zawiera co najmniej jedną encję z modułem harness,
  • zawiera encje ai_conversation i ai_message.

W przeciwnym razie wygenerowana funkcja InitHarness nic nie robi, a endpoint nie jest rejestrowany.

Generowanie kodu

Po zmianie modelu wygeneruj aplikację ponownie:

goat re

Generator tworzy między innymi:

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

oraz w pakiecie modelu każdej encji maski:

<Encja>HarnessListMask
<Encja>HarnessPersistMask

Maska list zawsze zawiera pole id, aby agent mógł wskazać konkretny rekord.

Nie edytuj tych plików ręcznie. Katalog goatapp/ jest w całości generowany.

Generowane narzędzia

Dla każdej encji z modułem harness powstaje trójka narzędzi:

NarzędzieRodzajDziałanie
query_<encja>backendOdczyt rekordów do analizy. Nigdy nie zmienia danych.
open_<encja>_formfrontendOtwiera formularz tworzenia lub edycji, opcjonalnie wstępnie wypełniony.
confirm_delete_<encja>frontendOtwiera okno potwierdzenia usunięcia z aktualnymi danymi rekordu.

Na przykład dla encji product są to query_product, open_product_form i confirm_delete_product.

Argumenty query_<encja>

{
  "fields": ["title", "sku"],
  "filters": {"status": "active"},
  "limit": 20
}
  • fields — podzbiór pól do zwrócenia; pominięcie oznacza wszystkie dostępne pola,
  • filters — filtry dokładnego dopasowania według nazwy pola,
  • limit — maksymalna liczba wierszy; domyślnie 20, maksymalnie 50.

Filtry porównują wyłącznie równość. Nie ma operatorów zakresu, wyszukiwania pełnotekstowego ani sortowania.

Argumenty open_<encja>_form

{
  "id": "opcjonalny-identyfikator-rekordu",
  "fields": {"title": "Nowy produkt"}
}
  • bez id otwierany jest formularz tworzenia,
  • z id otwierany jest formularz edycji istniejącego rekordu,
  • fields zawiera wartości do wstępnego wypełnienia.

Argumenty confirm_delete_<encja>

{"id": "identyfikator-rekordu"}

Pole id jest wymagane.

Uprawnienia

Efektywny dostęp agenta jest zawsze koniunkcją dwóch masek:

  • maski roli zalogowanego użytkownika,
  • maski modułu harness danej encji.

Żadna z nich osobno nie wystarcza. Agent działający w imieniu użytkownika z rolą user nie zobaczy pól, których ta rola nie może czytać, nawet jeśli są w zbiorze list. Nie zobaczy też pól spoza list, nawet jeśli rola może je czytać.

W praktyce oznacza to, że:

  • pole nieznane encji jest odrzucane z błędem,
  • pole niedostępne dla użytkownika jest pomijane w wyniku zapytania,
  • filtr po polu niedostępnym kończy się błędem,
  • wartości do wstępnego wypełnienia formularza są filtrowane maską zapisu roli i zbiorem persist,
  • podgląd rekordu w oknie usuwania zawiera tylko pola dostępne do odczytu.

Jeżeli rekord nie istnieje albo żadne jego pole nie jest dostępne, podgląd jest po prostu pusty. Użytkownik nie może w ten sposób odróżnić braku dostępu od braku rekordu.

Nazwy tabel i kolumn pochodzą z wygenerowanego rejestru i są walidowane. Wartości przekazane przez model trafiają do SQL wyłącznie jako parametry zapytania.

Endpoint POST /api/ai/chat

Endpoint wymaga zalogowanej sesji.

Żądanie:

{
  "conversationId": "opcjonalny-identyfikator-rozmowy",
  "message": "Pokaż ostatnie zamówienia"
}

Odpowiedź:

{
  "conversationId": "identyfikator-rozmowy",
  "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\":\"Nowy produkt\"}"}
  }
}

Pole action występuje tylko wtedy, gdy model poprosił o narzędzie frontendowe. W takim przypadku reply jest stałym komunikatem generowanym przez serwer (obecnie po angielsku), a nie tekstem napisanym przez model.

Kody błędów:

  • 401 — brak zalogowanej sesji,
  • 400 — pusta wiadomość lub niepoprawny JSON,
  • 500 — błąd odczytu historii albo wywołania modelu.

Przebieg jednej tury

historia rozmowy + nowa wiadomość
        ↓
model wybiera narzędzie
        ↓
backend: wykonanie odczytu i powrót do modelu
frontend: zwrócenie akcji do przeglądarki i koniec tury
        ↓
odpowiedź tekstowa

Jedna tura może wykonać najwyżej 5 rund narzędzi backendowych. Po przekroczeniu limitu endpoint zwraca błąd, aby model nie mógł zapętlić się w nieskończoność.

Historia rozmów

Zapisywane są tylko wiadomości użytkownika i asystenta. Pośrednie wywołania narzędzi istnieją wyłącznie w ramach jednej tury i nie wracają do kolejnych zapytań.

Rozmowa należy do użytkownika, który ją rozpoczął. Podanie conversationId cudzej rozmowy działa tak, jakby taka rozmowa nie istniała.

Frontend

Widget dashboardu

Czat jest widgetem dashboardu typu ai_chat. W tej aplikacji jest zdefiniowany w herd/_model.goat:

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

Widget ai_chat nie przyjmuje źródła danych, agregacji, wykresu ani zapytania. Rozmawia wyłącznie z /api/ai/chat.

Wykonanie akcji

Gdy odpowiedź zawiera action, serwis AiChatService przechodzi do wygenerowanego widoku encji:

/admin/<encja-w-kebab-case>

z parametrami:

  • aiAction — create, edit albo delete,
  • aiPrefill — JSON z wartościami do wstępnego wypełnienia formularza tworzenia,
  • aiId — identyfikator rekordu do edycji lub usunięcia,
  • aiPreview — JSON z podglądem rekordu w oknie potwierdzenia usunięcia.

Wygenerowany menedżer encji odczytuje te parametry, otwiera odpowiedni formularz lub okno i usuwa parametry z adresu URL.

Wstępne wypełnienie dotyczy formularza tworzenia. Dla edit menedżer otwiera istniejący rekord na podstawie aiId i pokazuje jego aktualne wartości.

Konfiguracja

Klient OpenAI jest konfigurowany przez zmienne środowiskowe:

ZmiennaZnaczenieDomyślnie
GOAT_AI_OPENAI_API_KEYKlucz API OpenAI dla asystentabrak
GOAT_AI_OPENAI_MODELModel czatu używany przez asystentagpt-4o-mini

Przy pustym kluczu endpoint jest nadal rejestrowany, ale każde zapytanie kończy się błędem, a widget pokazuje komunikat o niedostępności asystenta.

Nie myl tych zmiennych z GOAT_OPENAI_API_KEY, używaną przez skrypty tłumaczeń (herd/translate.goat, translate-docs.goat).

Klucz jest sekretem. Przechowuj go w lokalnym .env lub w konfiguracji wdrożenia i nigdy nie commituj go do repozytorium. herd/deploy.goat deklaruje GOAT_AI_OPENAI_API_KEY jako sekret, a GOAT_AI_OPENAI_MODEL jako zwykłą zmienną.

Prywatność danych

Wyniki narzędzi backendowych są wysyłane do modelu jako część rozmowy. Wszystkie pola ze zbioru list, które użytkownik może czytać, mogą więc trafić do OpenAI.

Dodanie pola do list jest decyzją o udostępnieniu go zewnętrznemu dostawcy AI. Nie umieszczaj tam haseł, tokenów ani danych, których nie wolno przetwarzać poza aplikacją.

Testowanie

Logika pakietu jest testowana bez połączenia z OpenAI. Funkcja Run przyjmuje interfejs Completer, który testy zastępują fałszywym klientem:

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

Wygenerowany test E2E goatapp/web/e2e-server/harness.spec.ts zawsze sprawdza odrzucenie anonimowych żądań (401) i pustych wiadomości (400).

Pełna rozmowa wywołuje skonfigurowanego dostawcę AI, dlatego jest uruchamiana tylko po jawnym ustawieniu zmiennej podczas uruchamiania testów Playwright:

GOAT_E2E_HARNESS_CHAT=1

Podsumowanie

  • Moduł harness udostępnia encję asystentowi AI przez trzy wygenerowane narzędzia.
  • Odczyt jest ograniczony koniunkcją maski roli i zbioru list.
  • Wstępne wypełnienie formularza jest ograniczone maską zapisu roli i zbiorem persist.
  • Asystent nigdy sam nie zapisuje ani nie usuwa danych; zawsze robi to człowiek.
  • Do działania potrzebne są encje ai_conversation i ai_message oraz klucz GOAT_AI_OPENAI_API_KEY.