# GLPI AI Agent (Go + Ollama) Produktionsorientierter, bewusst **policy-gesteuerter** Ticket-Agent für GLPI 11. Er liest neue/geänderte Tickets über die GLPI High-Level API, schlägt eine Kategorie vor, sucht freigegebene Wissenseinträge und kann – nach mehreren harten Sicherheitsprüfungen – eine erste Antwort schreiben. ## Sicherheitsmodell - `DRY_RUN=true` ist der Default. - `AUTO_REPLY=false` ist der Default. - Das LLM erhält **keine GLPI-Tools** und kann keine Schreiboperation direkt auslösen. - Kategorie-IDs werden gegen die aus GLPI geladene Kategorie-Liste validiert. - Automatische Antworten stammen **nicht aus freiem LLM-Text**, sondern aus einem freigegebenen Knowledge-Dokument (`auto_reply=true`). - Die Knowledge-Suche ist fail-closed: Nur Quellen aus `KNOWLEDGE_ALLOWED_SOURCES` werden überhaupt geladen/indexiert. - Auto-Replies benötigen zusätzlich eine Quelle aus `KNOWLEDGE_AUTO_REPLY_SOURCES` sowie passende Sprach-/Stil-Metadaten. - Endnutzer-Antworten werden zentral mit konfigurierter Anrede, Grußformel und Signatur gerahmt. - Vor einer Antwort werden Followups zweimal geprüft: vor der KI-Analyse und unmittelbar vor dem Schreiben. - Sobald irgendein Followup existiert, antwortet der Agent nicht. - Pro Ticket wird innerhalb eines Prozesses seriell gearbeitet; Polling/Webhook-Ereignisse werden dedupliziert. - GLPI-Schreibfehler werden nicht automatisch wiederholt, um Doppelwrites zu vermeiden. - Das Dashboard ist read-only und standardmäßig mit HTTP Basic Auth geschützt. - Audit-Trail: `data/runs.jsonl`. > Wichtige Grenze: Die zweite Followup-Prüfung minimiert Race Conditions, kann ohne einen atomaren Conditional-Write auf GLPI-Seite aber kein mathematisch vollständig atomisches "check-and-write" garantieren. Für einen einzelnen Agent-Prozess ist zusätzlich ein Ticket-Lock aktiv. ## Voraussetzungen - GLPI 11.0.6+ empfohlen (API v2.3). - High-Level API in GLPI aktiviert. - OAuth Client in **Setup > OAuth Clients** mit Password Grant und `api` Scope. - Dedizierter GLPI-Servicebenutzer mit minimal nötigen Rechten: Tickets lesen, Kategorien lesen/ändern (falls genutzt), Followups lesen/hinzufügen (falls Auto-Reply genutzt). - Ollama mit Chat- und Embedding-Modell. Beim Start lädt der Agent `/api.php/doc.json` und prüft, ob die erwarteten Kernrouten vorhanden sind. Dadurch schlägt ein API-Mismatch früh und sichtbar fehl. Die mitgelieferten Tests laufen gegen HTTP-Mocks; eine echte GLPI-Instanz konnte in dieser Build-Umgebung nicht angebunden werden, daher ist der Shadow-Mode auf deiner Installation vor Live-Schreibzugriff zwingend. ## Start nativ unter Windows / PowerShell Für einen nativen Windows-Start **nicht** die Docker-Pfade `/app/data`, `/app/knowledge` oder den Docker-Hostnamen `ollama` verwenden. Die mitgelieferte `.env.example` enthält deshalb jetzt native, plattformneutrale Defaults: ```env DATA_DIR=./data KNOWLEDGE_DIR=./knowledge OLLAMA_URL=http://localhost:11434 ``` Einmalig: ```powershell Copy-Item .env.example .env # Danach .env mit den echten GLPI-Zugangsdaten bearbeiten. ollama pull qwen3:8b ollama pull embeddinggemma ``` Start: ```powershell .\run.ps1 ``` `run.ps1` lädt `.env`, startet immer aus dem Projektverzeichnis und erkennt zur Migration auch alte Docker-Werte. Beispielsweise wird ein vorhandenes `KNOWLEDGE_DIR=/app/knowledge` beim nativen Windows-Start auf `\knowledge` umgesetzt und mit einer Warnung ausgegeben. Das Datenverzeichnis wird bei Bedarf erstellt; ein fehlendes Knowledge-Verzeichnis führt zu einer verständlichen Fehlermeldung statt zu einem Panic. Docker Compose überschreibt diese drei nativen Werte im Container weiterhin explizit mit `/app/data`, `/app/knowledge` und `http://ollama:11434`. ## Start mit Docker Compose ```bash cp .env.example .env $EDITOR .env docker compose up -d ollama docker compose exec ollama ollama pull qwen3:8b docker compose exec ollama ollama pull embeddinggemma docker compose up -d --build agent ``` Dashboard: `http://127.0.0.1:8080/` Vor dem ersten Live-Betrieb unbedingt mehrere Tage/Wochen im Shadow Mode lassen: ```env DRY_RUN=true AUTO_CATEGORY=true AUTO_REPLY=false ``` Danach zunächst nur Kategorieänderungen: ```env DRY_RUN=false AUTO_CATEGORY=true AUTO_REPLY=false ``` Erst nach Auswertung der Audit-Daten einzelne KB-Einträge mit `auto_reply=true` freigeben und anschließend: ```env AUTO_REPLY=true GLPI_AGENT_USER_ID=123 ``` ## GLPI-Endpunkte Default ist `GLPI_API_VERSION=v2.3`. Der Client verwendet: - OAuth: `POST /api.php/token` - Tickets: `/api.php/v2.3/Assistance/Ticket` - Followups: `/api.php/v2.3/Assistance/Ticket/{id}/Timeline/Followup` - Kategorien: `/api.php/v2.3/Dropdowns/ITILCategory` - OpenAPI-Prüfung: `/api.php/doc.json` Die OpenAPI-Dokumentation deiner Installation ist die maßgebliche Quelle, weil GLPI die API-Dokumentation dynamisch aus Core und aktivierten Plugins erzeugt. ## Wissensbasis, Quellen und Kommunikationspolicy Jede Datei in `knowledge/` ist JSON und trägt eine explizite Herkunft sowie Kommunikations-Metadaten: ```json { "id": "KB-128", "title": "GlobalProtect Gateway nicht erreichbar", "text": "Beschreibung, Fehlermeldungen, Voraussetzungen ...", "answer": "Bitte trennen Sie die bestehende VPN-Verbindung vollständig und starten Sie den VPN-Client anschließend neu.", "auto_reply": true, "min_score": 0.92, "categories": [22], "keywords": ["GlobalProtect", "Gateway not responding"], "source": "internal-kb", "source_uri": "kb://network/vpn/128", "language": "de-DE", "communication_style": "formal" } ``` Die aktive Source-Policy wird über die Umgebung festgelegt: ```env # Nur diese Quellen werden geladen, eingebettet und durchsucht. KNOWLEDGE_ALLOWED_SOURCES=internal-kb,glpi-kb,vendor-docs # Nur diese Teilmenge darf eine automatische Antwort auslösen. KNOWLEDGE_AUTO_REPLY_SOURCES=internal-kb,glpi-kb ``` `KNOWLEDGE_AUTO_REPLY_SOURCES` muss eine Teilmenge von `KNOWLEDGE_ALLOWED_SOURCES` sein. Mit `KNOWLEDGE_AUTO_REPLY_SOURCES=none` kann die Quellenfreigabe für Auto-Replies vollständig deaktiviert werden. Dokumente aus nicht erlaubten Quellen werden nicht in die Suchmenge aufgenommen und damit auch nicht an Ollama übergeben. Ein Knowledge-Dokument ohne `source` führt absichtlich zu einem Startfehler, damit die Herkunft nicht implizit geraten wird. ### GLPI Knowledge Base als echter Connector Die GLPI-Wissensdatenbank kann jetzt direkt read-only synchronisiert werden. Der Agent ermittelt bei `GLPI_KB_PATH=auto` den lesbaren `KnowbaseItem`-Collection-Endpunkt aus `/api.php/doc.json`. GLPI selbst entscheidet anhand der Rechte des OAuth-Service-Accounts, welche Artikel sichtbar sind. ```env KNOWLEDGE_ALLOWED_SOURCES=internal-kb,glpi-kb GLPI_KB_ENABLED=true GLPI_KB_PATH=auto GLPI_KB_SYNC_INTERVAL=10m GLPI_KB_LIMIT=500 GLPI_KB_SOURCE=glpi-kb ``` Erfolgreich synchronisierte Artikel werden normalisiert, lokal unter `DATA_DIR/glpi-kb-cache.json` gecacht und in denselben RAG-Index wie lokale Knowledge-Dokumente aufgenommen. Unveränderte Dokumente behalten ihre gecachten Embeddings; nur neue oder geänderte Artikel werden erneut eingebettet. Fällt GLPI bei einem späteren Start/Sync aus, kann der zuletzt gespeicherte Cache weiter als read-only Wissensstand geladen werden. GLPI-KB-Auto-Replies sind absichtlich separat gesperrt. Für eine Freigabe müssen **alle** Bedingungen erfüllt sein: `glpi-kb` ist in `KNOWLEDGE_AUTO_REPLY_SOURCES`, `GLPI_KB_AUTO_REPLY=true`, die GLPI-KB-Kategorie des Artikels steht in `GLPI_KB_AUTO_REPLY_CATEGORY_IDS`, und GLPI liefert eine Zuordnung dieser KB-Kategorie zu mindestens einer ITIL-Kategorie. Beispiel: ```env KNOWLEDGE_AUTO_REPLY_SOURCES=internal-kb,glpi-kb GLPI_KB_AUTO_REPLY=true GLPI_KB_AUTO_REPLY_CATEGORY_IDS=4,7 ``` `GLPI_KB_AUTO_REPLY_CATEGORY_IDS` enthält **Knowledge-Base-Kategorie-IDs aus GLPI**, nicht Ticket-/ITIL-Kategorie-IDs. Ohne diese explizite Whitelist dienen GLPI-Artikel nur Recherche, RAG und Kategorie-Hinweisen. Optional kann `GLPI_KB_FILTER` gesetzt werden, um die von GLPI gelieferte Collection zusätzlich serverseitig einzuschränken. Die konkrete Filtersyntax und verfügbare Felder stammen aus der OpenAPI-Dokumentation deiner installierten GLPI-Version. Für die Kommunikation gelten zentrale Vorgaben: ```env COMMUNICATION_LANGUAGE=de-DE COMMUNICATION_STYLE=formal COMMUNICATION_SALUTATION=Guten Tag, COMMUNICATION_CLOSING=Mit freundlichen Grüßen COMMUNICATION_SIGNATURE=IT-Service ``` Ein Auto-Reply ist nur erlaubt, wenn `language` und `communication_style` des freigegebenen Knowledge-Dokuments exakt zur aktiven Policy passen. Das Feld `answer` enthält nur den fachlich freigegebenen Nachrichtentext; Anrede, Grußformel und Signatur werden von der Go-Policy zentral ergänzt. Damit kann das Modell diese Kommunikationsvorgaben nicht überschreiben. `categories` begrenzt Auto-Reply auf die angegebenen Zielkategorien. Eine leere Liste bedeutet keine zusätzliche Kategorie-Einschränkung. `min_score` kann die globale Schwelle je Artikel verschärfen. Bei aktiviertem RAG erzeugt Ollama Embeddings über `/api/embed`; der Cache landet in `data/embeddings.json`. Für Ticket und Knowledge wird dasselbe Embedding-Modell verwendet. ## Operativer Kontext: Changes, Major Incidents, Uptime Kuma und Geräte Der Agent kann vor der LLM-Entscheidung zusätzliche **read-only** Betriebsdaten einsammeln. Diese Daten werden normalisiert und als Fakten in den Prompt aufgenommen; das Modell erhält keine direkten Zugangsdaten und keine zusätzlichen Schreibwerkzeuge. ### Change Calendar ```env CHANGE_CALENDAR_ENABLED=true GLPI_CHANGE_PATH=/Assistance/Change GLPI_CHANGE_FILTER= GLPI_CHANGE_LIMIT=100 CHANGE_LOOKBACK=48h CHANGE_LOOKAHEAD=24h ``` Der Agent lädt Changes im konfigurierten Zeitfenster, berechnet eine deterministische Relevanz zum Ticket (u. a. Tickettext, verknüpfte Geräte/Standorte) und übergibt höchstens die relevantesten Einträge an Ollama. `GLPI_CHANGE_PATH` wird beim Start gegen `/api.php/doc.json` geprüft. Bei einer Installation mit abweichender Route oder Filter-Syntax muss die Konfiguration an das OpenAPI-Schema der eigenen Instanz angepasst werden. ### Aktive Major Incidents ```env MAJOR_INCIDENTS_ENABLED=false GLPI_MAJOR_INCIDENT_FILTER= GLPI_MAJOR_INCIDENT_LIMIT=20 ``` Major Incidents werden bewusst **nicht automatisch aus beliebigen Tickets erraten**. Sie sind eine explizit vom Betreiber definierte Teilmenge der GLPI-Tickets. Erst wenn `GLPI_MAJOR_INCIDENT_FILTER` die in deiner Umgebung gültige Filterdefinition enthält, sollte `MAJOR_INCIDENTS_ENABLED=true` gesetzt werden. Ein zum aktuellen Ticket relevanter Major Incident blockiert standardmäßig einen normalen Auto-Reply; die Kategorieanalyse darf weiterlaufen. Beispiel-Idee (die konkrete Syntax muss zu deinem GLPI-OpenAPI-Schema passen): ein Filter auf eine dedizierte Kategorie, Priorität/Impact oder ein eigenes Kennzeichen für Major Incidents. ### Aktuelle Störungen mit Uptime Kuma Für interne Uptime-Kuma-Instanzen ist der authentifizierte Prometheus-Endpunkt der empfohlene Modus: ```env UPTIME_KUMA_ENABLED=true UPTIME_KUMA_URL=https://uptime.example.org UPTIME_KUMA_MODE=metrics UPTIME_KUMA_API_KEY=CHANGE_ME UPTIME_KUMA_TIMEOUT=10s UPTIME_KUMA_MAX_ISSUES=20 ``` Der Client liest ausschließlich `/metrics`, verwendet den Uptime-Kuma-API-Key als HTTP-Basic-Auth-Passwort und gibt nur Monitore weiter, die nicht `UP` sind. Der API-Key wird nicht an Ollama übergeben. Alternativ können bereits veröffentlichte Statusseiten gelesen werden: ```env UPTIME_KUMA_MODE=status_page UPTIME_KUMA_STATUS_PAGES=it-services,network UPTIME_KUMA_INCLUDE_MAINTENANCE=true ``` In diesem Modus liest der Agent `/api/status-page/` und `/api/status-page/heartbeat/` und berücksichtigt gepinnte Incidents, DOWN/PENDING-Monitore und optional Wartungen. Dieser Modus eignet sich nur für Informationen, die auf der betreffenden Statusseite ohnehin veröffentlicht werden dürfen. ### Beziehungen zwischen Benutzer und Gerät ```env USER_DEVICE_CONTEXT_ENABLED=true GLPI_USER_DEVICE_PATHS=/Assets/Computer GLPI_USER_DEVICE_FILTER_TEMPLATE=user.id=={{user_id}} GLPI_USER_DEVICE_LIMIT=20 ``` Der Agent nutzt zunächst direkt am Ticket verknüpfte GLPI-Items. Zusätzlich werden – soweit der Ticket-Response Requester-IDs enthält – über die konfigurierten Asset-Routen dem Benutzer zugeordnete Geräte gelesen. Die Pfade werden beim Start gegen die OpenAPI-Dokumentation geprüft; die Filter-Syntax ist installationsabhängig und sollte im Shadow Mode verifiziert werden. Die normalisierten Gerätedaten dienen u. a. dazu, Changes und Störungen besser zum Ticket zuzuordnen. Es werden keine Assets geändert. ### Fail-closed Verhalten ```env CONTEXT_ENABLED=true CONTEXT_TIMEOUT=12s CONTEXT_RELEVANCE_MIN_SCORE=0.20 CONTEXT_BLOCK_AUTO_REPLY_ON_ERRORS=true CONTEXT_BLOCK_AUTO_REPLY_ON_INCIDENT=true ``` Mit den sicheren Defaults gilt: - Fällt eine aktivierte Kontextquelle aus, wird der Lauf als unvollständig markiert und **kein Auto-Reply** gesendet. - Ein relevanter Major Incident oder eine relevante Uptime-Kuma-Störung blockiert einen normalen Standard-Auto-Reply. - Kategorieanalyse und Auditierung können trotzdem stattfinden. - Kontextquellen haben ausschließlich Leserechte. - Im Dashboard/Audit erscheinen pro Lauf die Anzahl der gefundenen Changes, Incidents, Uptime-Issues und Geräte sowie Warnungen bei unvollständigem Kontext. ## Web- und Monitoring-Endpunkte - `/` – Dashboard (Basic Auth) - `/api/status` – Status JSON inklusive aktiver Sprache/Stil- und Quellenpolicy (Basic Auth) - `/api/runs?limit=50` – letzte Audit-Läufe (Basic Auth) - `/healthz` – Prozess lebt - `/readyz` – GLPI und Ollama erreichbar - `/metrics` – Prometheus Textformat - `POST /webhook/glpi` – optionaler Webhook-Eingang, geschützt durch `X-Webhook-Secret` Das Polling bleibt immer aktiv und dient als Fallback. Der Webhook-Parser akzeptiert übliche Ticket-ID-Felder sowie Ticket-URLs; prüfe die konkrete Payload deiner GLPI-Webhook-Konfiguration im Shadow Mode. ### Erklärbare Entscheidungen im Dashboard Für den Shadow-/Einführungsbetrieb speichert jeder Lauf die **rohe KI-Empfehlung** getrennt von der **Policy-Entscheidung**. Das Modell liefert bei Kategorien nur noch `id` und `confidence`; ein eigenes `change=true/false` gibt es nicht mehr. Ob tatsächlich geändert werden darf, entscheidet ausschließlich Go anhand der aktuellen Kategorie, der bekannten GLPI-Kategorien und `CATEGORY_CONFIDENCE`. Das Dashboard zeigt deshalb unter anderem: - aktuelle Kategorie mit ID und Name, - von der KI empfohlene Kategorie mit ID und Name, - KI-Confidence und konfigurierten Schwellwert, - expliziten Entscheidungsgrund wie `category_confidence_below_threshold`, `category_already_correct` oder `category_written`, - KI-Empfehlung für Auto-Reply samt Confidence und Reply-Schwellwert, - besten Knowledge-Treffer mit Score, - den ersten Policy-Blocker für einen Reply, z. B. fehlendes Knowledge, vorhandenes Followup, unvollständigen Kontext oder einen relevanten Incident, - die fachliche KI-Begründung separat von den technischen Policy-Codes. Damit ist auch ein Lauf ohne Schreibaktion nachvollziehbar. Beispiel: „KI empfiehlt Active Directory (#17) mit 82 %, Schwellwert 90 % → nicht geändert“. Die JSON-Details stehen zusätzlich unter `/api/runs?limit=50` zur Verfügung. ## Keine Doppelantworten Der Schreibpfad ist bewusst streng: 1. Ticket laden. 2. Followups laden. Existiert eines: **Stop**. 3. Knowledge sowie read-only Betriebskontext (Changes, Major Incidents, Uptime Kuma, Benutzer/Geräte) laden. 4. KI empfiehlt Kategorie-ID + Confidence und optional einen Knowledge-basierten Reply; Kontextdaten sind nur Fakten, keine ausführbaren Anweisungen. 5. Policy Engine entscheidet deterministisch über Kategorieänderung und Reply und protokolliert jeden akzeptierten oder blockierten Gate-Grund. 6. Optional Kategorie ändern. 7. Direkt vor Auto-Reply Ticket und Followups **erneut** laden. Existiert jetzt ein Followup oder hat sich die Entscheidungsgrundlage geändert: **Stop**. 8. Freigegebenen KB-Antworttext als Followup schreiben. `GLPI_AGENT_USER_ID` wird bei `AUTO_REPLY=true` absichtlich verlangt, damit die Betreiberkonfiguration eindeutig einem dedizierten GLPI-Konto zugeordnet ist. Der aktuelle Code blockiert bei *jedem* vorhandenen Followup – einschließlich eines früheren Agent-Followups – und ist damit konservativer als nur "fremde" Antworten zu prüfen. ## Produktionshinweise - Dashboard hinter Reverse Proxy mit TLS betreiben; Compose bindet Port 8080 absichtlich nur an `127.0.0.1`. - GLPI über HTTPS anbinden. Plain HTTP wird standardmäßig abgelehnt (`GLPI_ALLOW_INSECURE_HTTP=false`). - `.env` niemals committen; besser Docker/Kubernetes Secrets oder systemd `EnvironmentFile` mit restriktiven Dateirechten verwenden. - Servicekonto nach Least-Privilege-Prinzip konfigurieren. - Für mehrere parallele Agent-Replikate muss die lokale Queue/State-Sperre durch einen verteilten Store/Lock (z. B. PostgreSQL/Redis) ersetzt werden. Die mitgelieferte Version ist für **eine aktive Agent-Instanz** ausgelegt. - Vor Live-Auto-Reply Tests mit echten anonymisierten Ticketmustern durchführen. - Knowledge-Antworten fachlich freigeben und versionieren. ## Build & Tests Das Projekt verwendet nur die Go-Standardbibliothek; damit gibt es keine Laufzeit-Abhängigkeiten im Agent-Binary. ```bash make fmt make test make vet make build ``` ## Docker troubleshooting: `/app/data` permission denied and slow Ollama The Compose stack contains a one-shot `agent-data-init` service. It prepares the named `agent-data` volume for the non-root agent user before the agent starts. The agent also probes `runs.jsonl` at startup and exits immediately with a clear error if the volume is not writable. For local LLMs, the default request budget is intentionally longer than a typical HTTP API call: ```env OLLAMA_TIMEOUT=10m OLLAMA_NUM_PREDICT=256 OLLAMA_KEEP_ALIVE=10m OLLAMA_THINK=false OLLAMA_MAX_CONCURRENT=1 ``` `OLLAMA_NUM_PREDICT` limits the maximum generated tokens for the small structured decision. `OLLAMA_KEEP_ALIVE` asks Ollama to keep the analysis model loaded between tickets. `OLLAMA_THINK=false` disables optional model thinking for this deterministic classification task. `OLLAMA_MAX_CONCURRENT=1` serializes local Ollama inference even when multiple ticket workers are active, so queued requests do not consume their HTTP timeout while waiting for the model. On very slow CPU-only hosts, use a smaller local model and/or increase `OLLAMA_TIMEOUT`. After upgrading an existing Compose deployment, recreate the stack so the init service runs: ```bash docker compose down docker compose build --no-cache agent agent-data-init docker compose up -d ``` You do **not** need to delete `agent-data`; the init service fixes ownership on the existing named volume. ## Human-in-the-loop-Lernen und Web-Knowledge-Base Der Agent lernt **nicht aus seinen eigenen Entscheidungen**. Im Dashboard kann eine Kategorie eines verarbeiteten Tickets ausdrücklich bestätigt oder korrigiert werden. Diese menschlich bestätigten Beispiele werden in `DATA_DIR/category-learning.json` persistiert und bei ähnlichen Tickets als `confirmed_examples` an das Klassifikationsmodell übergeben. Zusätzlich werden Kategorie-Hinweise aus freigegebenen KB-Keywords und konservativen IT-Semantik-Hinweisen aufgebaut. Konfiguration: ```env LEARNING_ENABLED=true LEARNING_MAX_EXAMPLES=500 LEARNING_EXAMPLES_PER_CATEGORY=5 ``` Das Dashboard zeigt außerdem den Status und die Anzahl der synchronisierten GLPI-KB-Artikel. Synchronisierte GLPI-Artikel sind read-only und als GLPI-Sync gekennzeichnet. Das Dashboard enthält außerdem einen CRUD-Editor für interne Knowledge-Einträge. Er ist absichtlich nur bei authentifiziertem Dashboard aktiv: ```env WEB_ALLOW_ANONYMOUS=false KNOWLEDGE_WEB_EDIT_ENABLED=true ``` Web-verwaltete Artikel landen **nicht** im statischen `KNOWLEDGE_DIR`, sondern unter `DATA_DIR/knowledge-managed/`. Dadurch kann `knowledge/` weiterhin read-only aus Git/Image gemountet werden. Statische Artikel werden im Web angezeigt, können dort aber nicht überschrieben oder gelöscht werden. Neue bzw. im Web verwaltete Artikel werden nach dem Speichern sofort in den laufenden Such-/RAG-Store aufgenommen; ein Neustart ist nicht nötig. Für stabilere Structured Outputs sind die empfohlenen Startwerte: ```env OLLAMA_NUM_PREDICT=768 OLLAMA_JSON_RETRIES=1 ``` Bei unvollständigem/ungültigem JSON wird genau einmal erneut eine schema-konforme Antwort angefordert. ### Deployment mit Gitea Container Registry unter Linux Für ein bereits in Gitea gebautes Image ist `docker-compose.registry.yml` vorgesehen; lokal wird nichts gebaut. ```bash export AGENT_IMAGE=gitea.example.de/organisation/glpi-ai-agent:latest mkdir -p data knowledge sudo chown 65532:65532 data # knowledge bleibt absichtlich read-only; Web-KB landet unter data/knowledge-managed/ docker compose -f docker-compose.registry.yml pull docker compose -f docker-compose.registry.yml up -d ``` Bei neuen Gitea-Builds genügt: ```bash docker compose -f docker-compose.registry.yml up -d --pull always ```