All checks were successful
release-tag / release-image (push) Successful in 1m29s
323 lines
16 KiB
Markdown
323 lines
16 KiB
Markdown
# 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.
|