# 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. > In dieser Version ist `knowledge/` weiterhin der einzige physische Knowledge-Connector. `source` ist ein verbindliches Herkunfts-/Vertrauenslabel für importierte Dokumente. Ein späterer GLPI-KB-, Wiki- oder Hersteller-Connector kann dieselbe Policy unverändert verwenden. 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. ## 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 klassifizieren lassen; Kontextdaten sind nur Fakten, keine ausführbaren Anweisungen. 5. Policy Engine validiert IDs, Confidence, RAG-Score, Quellenfreigabe, Sprache/Stil und die Kontext-Gates. 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.