Files
glpi-ai-agent/README.md
jbergner f21da92dc6
All checks were successful
release-tag / release-image (push) Successful in 1m29s
Bugfix
2026-07-27 19:18:36 +02:00

323 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<Projekt>\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/<slug>` und `/api/status-page/heartbeat/<slug>` 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.