Moduł harness: asystent AI
Jak udostępnić encje asystentowi AI, jakie narzędzia powstają i jak działają uprawnienia, endpoint czatu oraz konfiguracja OpenAI.
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
MODULEEOFlist— 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_conversationiai_message.
W przeciwnym razie wygenerowana funkcja InitHarness nic nie robi, a endpoint nie jest rejestrowany.
Generowanie kodu
Po zmianie modelu wygeneruj aplikację ponownie:
goat reGenerator tworzy między innymi:
goatapp/app/api/modelapi/harness_gen.go
goatapp/app/harness/store_gen.gooraz w pakiecie modelu każdej encji maski:
<Encja>HarnessListMask
<Encja>HarnessPersistMaskMaska 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ędzie | Rodzaj | Działanie |
query_<encja> | backend | Odczyt rekordów do analizy. Nigdy nie zmienia danych. |
open_<encja>_form | frontend | Otwiera formularz tworzenia lub edycji, opcjonalnie wstępnie wypełniony. |
confirm_delete_<encja> | frontend | Otwiera 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
idotwierany jest formularz tworzenia, - z
idotwierany jest formularz edycji istniejącego rekordu, fieldszawiera 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
harnessdanej 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ź tekstowaJedna 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,editalbodelete,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:
| Zmienna | Znaczenie | Domyślnie |
GOAT_AI_OPENAI_API_KEY | Klucz API OpenAI dla asystenta | brak |
GOAT_AI_OPENAI_MODEL | Model czatu używany przez asystenta | gpt-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=1Podsumowanie
- Moduł
harnessudostę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_conversationiai_messageoraz kluczGOAT_AI_OPENAI_API_KEY.