From 7ec10c490bcbd6e85a1524d26c8f49d5a1d2d862 Mon Sep 17 00:00:00 2001 From: jbergner Date: Wed, 29 Jul 2026 22:19:43 +0200 Subject: [PATCH] Diagnose impllementiert --- .env.example | 18 ++ Dockerfile | 4 +- README.md | 14 + UPGRADE.md | 6 + go.mod | 2 +- internal/agent/agent.go | 145 +++++++++- internal/agent/agent_test.go | 52 ++++ internal/agent/policy.go | 359 ++++++++++++++++-------- internal/knowledge/store.go | 26 ++ internal/model/model.go | 96 +++++-- internal/web/server.go | 102 ++++++- internal/web/server_test.go | 14 +- internal/web/templates/dashboard.html | 1 + internal/web/templates/diagnostics.html | 54 ++++ knowledge/KB-AD-001.json | 23 ++ knowledge/KB-AD-002.json | 22 ++ knowledge/KB-AD-003.json | 23 ++ knowledge/KB-AD-004.json | 22 ++ knowledge/KB-BRO-001.json | 21 ++ knowledge/KB-BRO-002.json | 21 ++ knowledge/KB-BRO-003.json | 21 ++ knowledge/KB-BRO-004.json | 21 ++ knowledge/KB-BRW-001.json | 22 ++ knowledge/KB-CHR-001.json | 21 ++ knowledge/KB-CHR-002.json | 21 ++ knowledge/KB-CHR-003.json | 20 ++ knowledge/KB-EDGE-001.json | 21 ++ knowledge/KB-EDGE-002.json | 20 ++ knowledge/KB-EDGE-003.json | 20 ++ knowledge/KB-EXC-001.json | 24 ++ knowledge/KB-EXC-002.json | 22 ++ knowledge/KB-EXC-003.json | 22 ++ knowledge/KB-EXC-004.json | 22 ++ knowledge/KB-EXC-005.json | 21 ++ knowledge/KB-EXC-006.json | 21 ++ knowledge/KB-EXC-007.json | 22 ++ knowledge/KB-EXC-008.json | 21 ++ knowledge/KB-FF-001.json | 21 ++ knowledge/KB-FF-002.json | 20 ++ knowledge/KB-FILE-001.json | 20 ++ knowledge/KB-HW-001.json | 22 ++ knowledge/KB-HW-002.json | 20 ++ knowledge/KB-HW-003.json | 21 ++ knowledge/KB-HW-004.json | 21 ++ knowledge/KB-INF-001.json | 21 ++ knowledge/KB-INF-002.json | 22 ++ knowledge/KB-INF-003.json | 20 ++ knowledge/KB-INF-004.json | 20 ++ knowledge/KB-INF-005.json | 20 ++ knowledge/KB-M42-001.json | 22 ++ knowledge/KB-M42-002.json | 20 ++ knowledge/KB-M42-003.json | 21 ++ knowledge/KB-M42-004.json | 20 ++ knowledge/KB-M42-005.json | 20 ++ knowledge/KB-NET-001.json | 22 ++ knowledge/KB-NET-002.json | 21 ++ knowledge/KB-NET-003.json | 21 ++ knowledge/KB-NET-004.json | 21 ++ knowledge/KB-OFF-001.json | 22 ++ knowledge/KB-OFF-002.json | 21 ++ knowledge/KB-OFF-003.json | 21 ++ knowledge/KB-OFF-004.json | 22 ++ knowledge/KB-PRN-001.json | 20 ++ knowledge/KB-PRN-002.json | 20 ++ knowledge/KB-RDP-001.json | 20 ++ knowledge/KB-SEC-001.json | 21 ++ knowledge/KB-SEC-002.json | 21 ++ knowledge/KB-TA-001.json | 21 ++ knowledge/KB-TA-002.json | 20 ++ knowledge/KB-TA-003.json | 20 ++ knowledge/KB-TA-004.json | 21 ++ knowledge/KB-WEB-001.json | 21 ++ knowledge/KB-WIN-001.json | 21 ++ knowledge/KB-WIN-002.json | 21 ++ 74 files changed, 1992 insertions(+), 164 deletions(-) create mode 100644 internal/web/templates/diagnostics.html create mode 100644 knowledge/KB-AD-001.json create mode 100644 knowledge/KB-AD-002.json create mode 100644 knowledge/KB-AD-003.json create mode 100644 knowledge/KB-AD-004.json create mode 100644 knowledge/KB-BRO-001.json create mode 100644 knowledge/KB-BRO-002.json create mode 100644 knowledge/KB-BRO-003.json create mode 100644 knowledge/KB-BRO-004.json create mode 100644 knowledge/KB-BRW-001.json create mode 100644 knowledge/KB-CHR-001.json create mode 100644 knowledge/KB-CHR-002.json create mode 100644 knowledge/KB-CHR-003.json create mode 100644 knowledge/KB-EDGE-001.json create mode 100644 knowledge/KB-EDGE-002.json create mode 100644 knowledge/KB-EDGE-003.json create mode 100644 knowledge/KB-EXC-001.json create mode 100644 knowledge/KB-EXC-002.json create mode 100644 knowledge/KB-EXC-003.json create mode 100644 knowledge/KB-EXC-004.json create mode 100644 knowledge/KB-EXC-005.json create mode 100644 knowledge/KB-EXC-006.json create mode 100644 knowledge/KB-EXC-007.json create mode 100644 knowledge/KB-EXC-008.json create mode 100644 knowledge/KB-FF-001.json create mode 100644 knowledge/KB-FF-002.json create mode 100644 knowledge/KB-FILE-001.json create mode 100644 knowledge/KB-HW-001.json create mode 100644 knowledge/KB-HW-002.json create mode 100644 knowledge/KB-HW-003.json create mode 100644 knowledge/KB-HW-004.json create mode 100644 knowledge/KB-INF-001.json create mode 100644 knowledge/KB-INF-002.json create mode 100644 knowledge/KB-INF-003.json create mode 100644 knowledge/KB-INF-004.json create mode 100644 knowledge/KB-INF-005.json create mode 100644 knowledge/KB-M42-001.json create mode 100644 knowledge/KB-M42-002.json create mode 100644 knowledge/KB-M42-003.json create mode 100644 knowledge/KB-M42-004.json create mode 100644 knowledge/KB-M42-005.json create mode 100644 knowledge/KB-NET-001.json create mode 100644 knowledge/KB-NET-002.json create mode 100644 knowledge/KB-NET-003.json create mode 100644 knowledge/KB-NET-004.json create mode 100644 knowledge/KB-OFF-001.json create mode 100644 knowledge/KB-OFF-002.json create mode 100644 knowledge/KB-OFF-003.json create mode 100644 knowledge/KB-OFF-004.json create mode 100644 knowledge/KB-PRN-001.json create mode 100644 knowledge/KB-PRN-002.json create mode 100644 knowledge/KB-RDP-001.json create mode 100644 knowledge/KB-SEC-001.json create mode 100644 knowledge/KB-SEC-002.json create mode 100644 knowledge/KB-TA-001.json create mode 100644 knowledge/KB-TA-002.json create mode 100644 knowledge/KB-TA-003.json create mode 100644 knowledge/KB-TA-004.json create mode 100644 knowledge/KB-WEB-001.json create mode 100644 knowledge/KB-WIN-001.json create mode 100644 knowledge/KB-WIN-002.json diff --git a/.env.example b/.env.example index 7a0f3c0..76e2b4e 100644 --- a/.env.example +++ b/.env.example @@ -75,6 +75,12 @@ KNOWLEDGE_CHUNK_WORDS=160 KNOWLEDGE_CHUNK_OVERLAP_WORDS=30 KNOWLEDGE_MAX_CHUNKS_PER_DOC=24 KNOWLEDGE_MAX_QUERY_CHUNKS=64 +# Persistent local index: incremental loads the last snapshot immediately and scans for changes in the background. +KNOWLEDGE_INDEX_MODE=incremental +# Number of texts sent per Ollama /api/embed request. +KNOWLEDGE_EMBED_BATCH_SIZE=64 +# 0 disables recurring scans; an initial background delta scan still runs after startup in incremental mode. +KNOWLEDGE_INDEX_SCAN_INTERVAL=5m CATEGORY_PROMPT_LIMIT=80 # Fail-closed source policy. Only documents carrying one of these source labels are indexed/searched. KNOWLEDGE_ALLOWED_SOURCES=internal-kb,glpi-kb @@ -84,6 +90,16 @@ KNOWLEDGE_AUTO_REPLY_SOURCES=internal-kb # the static KNOWLEDGE_DIR stays read-only. KNOWLEDGE_WEB_EDIT_ENABLED=false +# Compatibility for shared KB JSON files whose `categories` contain external string labels. +# unscoped: load the document, keep external labels, disable auto-reply while labels are unmapped. +# skip: ignore the whole document if any external category cannot be mapped. +# strict: fail startup on any unmapped external category. +KNOWLEDGE_CATEGORY_MODE=unscoped +# Optional JSON mapping file. Recommended location in Docker: /app/data/knowledge-category-map.json +KNOWLEDGE_CATEGORY_MAP_FILE= +# Optional comma-separated filename globs, e.g. KB-SEC-ATTCK-*.json,legacy-*.json +KNOWLEDGE_IGNORE_GLOBS= + # GLPI Knowledge Base (read-only connector). The service account's GLPI rights # determine which articles are visible. "auto" discovers the KnowbaseItem # collection route from /api.php/doc.json at runtime. @@ -112,6 +128,8 @@ COMMUNICATION_STYLE=formal COMMUNICATION_SALUTATION=Guten Tag, COMMUNICATION_CLOSING=Mit freundlichen Grüßen COMMUNICATION_SIGNATURE=IT-Service +# Kennzeichnet automatisch vom Agenten ausgewaehlte Antworten mit dem TrustedNet-KI-Badge. +AI_CONTENT_LABEL_ENABLED=true # Read-only operational context. These sources never receive write access. diff --git a/Dockerfile b/Dockerfile index 1043322..37f1c88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.26-alpine AS build +FROM golang:1.23-alpine AS build WORKDIR /src COPY go.mod ./ COPY cmd ./cmd @@ -7,7 +7,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/glpi-ai # One-shot helper used by docker compose to prepare the persistent volume for # the distroless non-root runtime user (UID/GID 65532). -FROM golang:1.26-alpine AS data-init +FROM golang:1.23-alpine AS data-init ENTRYPOINT ["sh", "-c", "mkdir -p /app/data && chown -R 65532:65532 /app/data && chmod 0750 /app/data"] FROM gcr.io/distroless/static-debian12:nonroot diff --git a/README.md b/README.md index 7ef6236..b8f0b2b 100644 --- a/README.md +++ b/README.md @@ -544,3 +544,17 @@ Beim allerersten Aufbau ohne Snapshot startet das Dashboard weiterhin sofort und ### KI-Kennzeichnung automatischer Antworten Mit `AI_CONTENT_LABEL_ENABLED=true` wird der konfigurierte TrustedNet-Kennzeichnungsblock unveraendert an den Anfang jeder automatisch vom Agenten ausgewaehlten Antwort gesetzt. Da der Badge HTML verwendet, werden auch Plaintext-KB-Antworten fuer den Versand sicher nach HTML escaped. + +## Entscheidungsdiagnose + +Neben dem normalen Control Center steht unter `/diagnostics` ein separates Diagnose-Cockpit zur Verfügung. Es verwendet die vom Agenten selbst gespeicherten Policy-Regeln und zeigt pro Ticketlauf u. a.: + +- alle Kategorie-Gates mit Ist-/Sollwert und Blockierstatus, +- alle Auto-Reply-Gates (Quelle, Sprache, Stil, Artikel-Freigabe, Retrieval-Floor, Evidenz, Kategoriebindung, Kontext), +- Ausführungs-/Race-Protection (Followups, Dry-Run, Ticket-Recheck, GLPI-Write), +- die Audit-KB-Kandidaten mit Retrieval-Rang und Auswahlgrund (`sent_to_ai`, `below_retrieval_floor`, `outside_candidate_gap`, `max_candidates_reached`), +- einen KB-Inspector für beliebige Artikel aus dem aktiven Index. + +Der KB-Inspector rechnet einen Artikel auf Wunsch gegen den aktuellen Ticketstand neu. Hat sich das Ticket seit dem historischen Lauf verändert, kennzeichnet die UI diese Neu-Bewertung ausdrücklich als nicht historisch identisch. Für neue Läufe sind die gespeicherten Regelchecks die maßgebliche historische Erklärung. + +Bei Knowledge-Dateien mit nicht gemappten externen Kategorien zeigt die Diagnose die betreffenden Kategorien explizit an. Im Modus `unscoped` bleibt der Artikel für Retrieval nutzbar; wenn die Kompatibilitätslogik die effektive Auto-Reply-Freigabe deaktiviert hat, wird dies als eigenes fehlgeschlagenes Gate dargestellt. diff --git a/UPGRADE.md b/UPGRADE.md index 9c05272..32ec2b7 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -187,3 +187,9 @@ Normaler Neustart in `incremental`: ## KI-Kennzeichnung Automatische Antworten tragen standardmaessig den TrustedNet-Kennzeichnungsblock am Anfang. Zum expliziten Aktivieren/Deaktivieren: `AI_CONTENT_LABEL_ENABLED=true|false`. + +## Diagnose-Cockpit + +Nach dem Upgrade ist keine neue ENV-Variable erforderlich. Das neue Interface ist unter `/diagnostics` erreichbar und verwendet dieselbe Web-Authentifizierung wie das normale Dashboard. + +Neue Ticketläufe speichern strukturierte `category_checks`, `reply_checks` und `execution_checks` in `runs.jsonl`. Alte Laufdatensätze bleiben kompatibel, enthalten diese historischen Checks naturgemäß jedoch nicht rückwirkend. diff --git a/go.mod b/go.mod index 1983a6a..334dce7 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/example/glpi-ai-agent -go 1.26 +go 1.23 diff --git a/internal/agent/agent.go b/internal/agent/agent.go index a04f485..604b539 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -157,14 +157,19 @@ func (s *Service) Process(ctx context.Context, id int64) error { run.TicketName = t.Name run.SourceVersion = sourceVersion(t) run.CategoryBefore = t.CategoryID - if s.state.Seen(t.ID, run.SourceVersion) { + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_ticket_loaded", Group: "eligibility", Label: "Ticket konnte geladen werden", Status: "pass", Actual: "ja", Expected: "ja"}) + alreadySeen := s.state.Seen(t.ID, run.SourceVersion) + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_not_already_processed", Group: "eligibility", Label: "Diese Ticketversion wurde noch nicht verarbeitet", Status: passFail(!alreadySeen), Blocking: alreadySeen, Actual: boolText(!alreadySeen), Expected: "ja"}) + if alreadySeen { run.Outcome = "skipped" run.Reason = "already_processed" s.metrics.Skipped.Add(1) finish(nil) return nil } - if !s.statusAllowed(t.StatusID) { + statusAllowed := s.statusAllowed(t.StatusID) + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_status_allowed", Group: "eligibility", Label: "Ticketstatus ist zur Verarbeitung freigegeben", Status: passFail(statusAllowed), Blocking: !statusAllowed, Actual: fmt.Sprintf("Status #%d", t.StatusID), Expected: fmt.Sprintf("einer von %v", s.cfg.GLPIAllowedStatusIDs)}) + if !statusAllowed { run.Outcome = "skipped" run.Reason = "status_not_allowed" s.metrics.Skipped.Add(1) @@ -178,6 +183,7 @@ func (s *Service) Process(ctx context.Context, id int64) error { return err } canReply := len(followups) == 0 + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_no_existing_followup", Group: "execution", Label: "Ticket hat noch keine Antwort / kein Followup", Status: passFail(canReply), Blocking: !canReply, Actual: fmt.Sprintf("%d Followups", len(followups)), Expected: "0 Followups"}) categories, err := s.getCategories(ctx) if err != nil { run.Reason = "categories_failed" @@ -234,7 +240,7 @@ func (s *Service) Process(ctx context.Context, id int64) error { // alignment signal before the final policy gate. hits := s.knowledge.RerankForCategory(retrievalHits, decision.Category.ID) if len(hits) > 0 { - run.KnowledgeCandidates = auditKnowledgeCandidates(hits, s.cfg.KnowledgeMinScore, auditTopK, llmCandidateIDs) + run.KnowledgeCandidates = auditKnowledgeCandidates(hits, s.cfg.KnowledgeMinScore, auditTopK, llmCandidateIDs, candidateCutoff, s.cfg.KnowledgeRetrievalFloor, llmTopK) run.KnowledgeTopID = hits[0].Doc.ID run.KnowledgeTopTitle = hits[0].Doc.Title run.KnowledgeScore = hits[0].Score @@ -280,6 +286,9 @@ func (s *Service) Process(ctx context.Context, id int64) error { run.KnowledgeEvidenceScore = result.KnowledgeEvidenceScore run.KnowledgeRetrievalFloor = result.KnowledgeRetrievalFloor run.KnowledgeCategoryAligned = result.KnowledgeCategoryAligned + run.CategoryChecks = append([]model.RuleCheck(nil), result.CategoryChecks...) + run.ReplyChecks = append([]model.RuleCheck(nil), result.ReplyChecks...) + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_dry_run", Group: "execution", Label: "Live-Schreibmodus aktiv", Status: map[bool]string{true: "info", false: "pass"}[s.cfg.DryRun], Blocking: false, Actual: map[bool]string{true: "DRY RUN", false: "LIVE"}[s.cfg.DryRun], Expected: "LIVE für tatsächliche Änderungen", Detail: "Im DRY RUN werden freigegebene Aktionen nur simuliert."}) run.PolicyReason = result.CategoryDecision + "; " + result.ReplyDecision if !canReply && result.Reply { run.ReplyProposed = false @@ -297,6 +306,7 @@ func (s *Service) Process(ctx context.Context, id int64) error { return err } if sourceVersion(fresh) != run.SourceVersion { + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_ticket_unchanged", Group: "execution", Label: "Ticket seit Analyse unverändert", Status: "fail", Blocking: true, Actual: "geändert", Expected: "unverändert"}) run.Outcome = "skipped" run.Reason = "ticket_changed_before_write" if result.ChangeCategory { @@ -310,16 +320,19 @@ func (s *Service) Process(ctx context.Context, id int64) error { finish(nil) return nil } + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_ticket_unchanged", Group: "execution", Label: "Ticket seit Analyse unverändert", Status: "pass", Actual: "unverändert", Expected: "unverändert"}) } if result.ChangeCategory && !s.cfg.DryRun { if err := s.glpi.SetCategory(ctx, id, result.CategoryID); err != nil { + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_category_write", Group: "execution", Label: "Kategorie konnte in GLPI geschrieben werden", Status: "fail", Blocking: true, Actual: err.Error(), Expected: "erfolgreich"}) run.Reason = "category_write_failed" run.CategoryDecision = "category_write_failed" run.PolicyReason = run.CategoryDecision + "; " + run.ReplyDecision finish(err) return err } + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_category_write", Group: "execution", Label: "Kategorie konnte in GLPI geschrieben werden", Status: "pass", Actual: fmt.Sprintf("#%d", result.CategoryID), Expected: "erfolgreich"}) run.CategoryChanged = true run.CategoryDecision = "category_written" s.metrics.CategoryChanged.Add(1) @@ -361,17 +374,21 @@ func (s *Service) Process(ctx context.Context, id int64) error { return err } if len(followups) > 0 { + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_followup_recheck", Group: "execution", Label: "Unmittelbar vor Antwort ist weiterhin kein Followup vorhanden", Status: "fail", Blocking: true, Actual: fmt.Sprintf("%d Followups", len(followups)), Expected: "0 Followups"}) run.ReplyProposed = false run.Reason = "followup_appeared_before_write" run.ReplyDecision = "reply_followup_appeared_before_write" } else if !s.cfg.DryRun { + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_followup_recheck", Group: "execution", Label: "Unmittelbar vor Antwort ist weiterhin kein Followup vorhanden", Status: "pass", Actual: "0 Followups", Expected: "0 Followups"}) if err := s.glpi.AddFollowup(ctx, id, result.ReplyText, result.ReplyIsHTML); err != nil { + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_reply_write", Group: "execution", Label: "Antwort konnte in GLPI geschrieben werden", Status: "fail", Blocking: true, Actual: err.Error(), Expected: "erfolgreich"}) run.Reason = "reply_write_failed" run.ReplyDecision = "reply_write_failed" run.PolicyReason = run.CategoryDecision + "; " + run.ReplyDecision finish(err) return err } + run.ExecutionChecks = append(run.ExecutionChecks, model.RuleCheck{Code: "execution_reply_write", Group: "execution", Label: "Antwort konnte in GLPI geschrieben werden", Status: "pass", Actual: "erfolgreich", Expected: "erfolgreich"}) run.ReplyWritten = true run.ReplyDecision = "reply_written" s.metrics.Replies.Add(1) @@ -394,23 +411,139 @@ func (s *Service) Process(ctx context.Context, id int64) error { finish(nil) return nil } -func auditKnowledgeCandidates(hits []model.KnowledgeHit, globalMin float64, limit int, sentToAI map[string]struct{}) []model.KnowledgeCandidateAudit { + +// DiagnoseRun returns the persisted, historical decision record. The rule +// checks stored on the run are the authoritative explanation of the policy at +// execution time. +func (s *Service) DiagnoseRun(ctx context.Context, runID string) (model.RunRecord, error) { + _ = ctx + r, ok := s.state.FindRun(strings.TrimSpace(runID)) + if !ok { + return model.RunRecord{}, fmt.Errorf("run %q not found", runID) + } + return r, nil +} + +// DiagnoseKnowledge recalculates one arbitrary knowledge article against the +// current ticket/index. This is intentionally marked as a current re-evaluation +// when the GLPI ticket changed since the historical run. +func (s *Service) DiagnoseKnowledge(ctx context.Context, runID, knowledgeID string) (model.KnowledgeDiagnostic, error) { + run, ok := s.state.FindRun(strings.TrimSpace(runID)) + if !ok { + return model.KnowledgeDiagnostic{}, fmt.Errorf("run %q not found", runID) + } + doc, ok := s.knowledge.ByID(strings.TrimSpace(knowledgeID)) + if !ok { + return model.KnowledgeDiagnostic{}, fmt.Errorf("knowledge %q not found", knowledgeID) + } + t, err := s.glpi.GetTicket(ctx, run.TicketID) + if err != nil { + return model.KnowledgeDiagnostic{}, fmt.Errorf("load current ticket: %w", err) + } + cats, err := s.getCategories(ctx) + if err != nil { + return model.KnowledgeDiagnostic{}, fmt.Errorf("load categories: %w", err) + } + query := t.Name + "\n" + stripHTML(t.Content) + allHits, err := s.knowledge.Search(ctx, query, 0, cats) + if err != nil { + return model.KnowledgeDiagnostic{}, err + } + maxCandidates := s.cfg.KnowledgeTopK + if maxCandidates <= 0 { + maxCandidates = 6 + } + llmHits, cutoff := selectKnowledgeCandidates(allHits, maxCandidates, s.cfg.KnowledgeRetrievalFloor, s.cfg.KnowledgeCandidateMaxGap) + llmSet := knowledgeHitIDSet(llmHits) + initialRank := 0 + initialScore := 0.0 + for i, h := range allHits { + if h.Doc.ID == doc.ID { + initialRank = i + 1 + initialScore = h.Score + break + } + } + post := s.knowledge.RerankForCategory(allHits, run.AIRecommendedCategoryID) + var hit *model.KnowledgeHit + for i := range post { + if post[i].Doc.ID == doc.ID { + hit = &post[i] + break + } + } + if hit == nil { + return model.KnowledgeDiagnostic{}, fmt.Errorf("knowledge %q is not in active index", knowledgeID) + } + _, sent := llmSet[doc.ID] + reason := candidateSelectionReason(initialRank, initialScore, sent, cutoff, s.cfg.KnowledgeRetrievalFloor, maxCandidates) + decision := model.Decision{} + decision.Category.ID = run.AIRecommendedCategoryID + decision.Category.Confidence = run.AICategoryConfidence + decision.Reply.Allowed = run.AIReplyRecommended + decision.Reply.Confidence = run.AIReplyConfidence + decision.Reply.KnowledgeID = doc.ID + decision.Reason = run.AIReason + ctxData := model.ContextSnapshot{} + if s.context != nil && s.cfg.ContextEnabled { + ctxData = s.context.Collect(ctx, t) + } + res, _ := s.policy.Evaluate(t, decision, cats, []model.KnowledgeHit{*hit}, ctxData) + required := s.cfg.KnowledgeMinScore + if doc.MinScore > required { + required = doc.MinScore + } + checks := append([]model.RuleCheck(nil), res.ReplyChecks...) + checks = append([]model.RuleCheck{ + {Code: "candidate_in_active_index", Group: "retrieval", Label: "Artikel ist im aktiven Knowledge-Index", Status: "pass", Actual: "ja", Expected: "ja"}, + {Code: "candidate_retrieval_floor", Group: "retrieval", Label: "Retrieval-Score erreicht Floor", Status: passFail(initialScore >= s.cfg.KnowledgeRetrievalFloor), Blocking: initialScore < s.cfg.KnowledgeRetrievalFloor, Actual: percentText(initialScore), Expected: ">= " + percentText(s.cfg.KnowledgeRetrievalFloor)}, + {Code: "candidate_dynamic_cutoff", Group: "retrieval", Label: "Artikel liegt innerhalb des dynamischen Top-K-Abstands", Status: passFail(initialScore >= cutoff), Blocking: initialScore < cutoff, Actual: percentText(initialScore), Expected: ">= " + percentText(cutoff), Detail: fmt.Sprintf("Bester Treffer minus %.1f Prozentpunkte, mindestens Retrieval-Floor.", s.cfg.KnowledgeCandidateMaxGap*100)}, + {Code: "candidate_sent_to_ai", Group: "retrieval", Label: "Artikel wurde an die KI übergeben", Status: passFail(sent), Blocking: !sent, Actual: boolText(sent), Expected: "ja", Detail: reason}, + }, checks...) + return model.KnowledgeDiagnostic{ + RunID: run.RunID, TicketID: run.TicketID, KnowledgeID: doc.ID, Title: doc.Title, Source: doc.Source, + CurrentTicketChanged: sourceVersion(t) != run.SourceVersion, RetrievalRank: initialRank, RetrievalScore: initialScore, + SemanticScore: hit.SemanticScore, TitleScore: hit.TitleScore, LexicalScore: hit.LexicalScore, KeywordScore: hit.KeywordScore, CategoryScore: hit.CategoryScore, + CandidateCutoff: cutoff, SentToAI: sent, SelectionReason: reason, AISelected: run.AIKnowledgeID == doc.ID, + EvidenceScore: res.KnowledgeEvidenceScore, RequiredScore: required, BestChunkExcerpt: hit.BestChunkExcerpt, BestQueryExcerpt: hit.BestQueryExcerpt, + ExternalCategories: append([]string(nil), doc.ExternalCategories...), UnmappedCategories: append([]string(nil), doc.UnmappedExternalCategories...), Checks: checks, Document: doc, + }, nil +} + +func candidateSelectionReason(rank int, score float64, sent bool, cutoff, floor float64, maxCandidates int) string { + if sent { + return "sent_to_ai" + } + if score < floor { + return "below_retrieval_floor" + } + if score < cutoff { + return "outside_candidate_gap" + } + if maxCandidates > 0 && rank > maxCandidates { + return "max_candidates_reached" + } + return "not_selected" +} + +func auditKnowledgeCandidates(hits []model.KnowledgeHit, globalMin float64, limit int, sentToAI map[string]struct{}, cutoff, retrievalFloor float64, maxCandidates int) []model.KnowledgeCandidateAudit { if limit <= 0 || limit > len(hits) { limit = len(hits) } out := make([]model.KnowledgeCandidateAudit, 0, limit) - for _, h := range hits[:limit] { + for idx, h := range hits[:limit] { required := globalMin if h.Doc.MinScore > required { required = h.Doc.MinScore } _, wasSent := sentToAI[h.Doc.ID] + reason := candidateSelectionReason(idx+1, h.Score, wasSent, cutoff, retrievalFloor, maxCandidates) out = append(out, model.KnowledgeCandidateAudit{ ID: h.Doc.ID, Title: h.Doc.Title, Source: h.Doc.Source, Score: h.Score, SemanticScore: h.SemanticScore, TitleScore: h.TitleScore, LexicalScore: h.LexicalScore, KeywordScore: h.KeywordScore, CategoryScore: h.CategoryScore, RequiredScore: required, AutoReply: h.Doc.AutoReply, BestChunkExcerpt: h.BestChunkExcerpt, BestQueryExcerpt: h.BestQueryExcerpt, - QueryChunkCount: h.QueryChunkCount, DocumentChunkCount: h.DocumentChunkCount, SentToAI: wasSent, + QueryChunkCount: h.QueryChunkCount, DocumentChunkCount: h.DocumentChunkCount, SentToAI: wasSent, RetrievalRank: idx + 1, SelectionReason: reason, }) } return out diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 67ea357..b3a7748 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -164,3 +164,55 @@ func TestSemanticHintsImproveActiveDirectoryCategory(t *testing.T) { t.Fatalf("expected identity hints, got %q", h) } } + +func TestRunStoresStructuredPolicyChecks(t *testing.T) { + g := &fakeGLPI{ticket: model.Ticket{ID: 1, Name: "konto", Content: "anmeldung", DateMod: "v1", StatusID: 1, CategoryID: 1}, cats: []model.Category{{ID: 1, Name: "Allgemein"}, {ID: 2, Name: "Active Directory"}}} + var d model.Decision + d.Category.ID, d.Category.Confidence = 2, .82 + d.Reply.Allowed = false + svc := newTestService(t, g, d, false) + if err := svc.Process(context.Background(), 1); err != nil { + t.Fatal(err) + } + r := svc.state.Recent(1)[0] + if len(r.CategoryChecks) < 4 { + t.Fatalf("category checks=%d", len(r.CategoryChecks)) + } + if len(r.ReplyChecks) < 8 { + t.Fatalf("reply checks=%d", len(r.ReplyChecks)) + } + found := false + for _, c := range r.CategoryChecks { + if c.Code == "category_confidence" && c.Status == "fail" && c.Blocking { + found = true + } + } + if !found { + t.Fatalf("missing blocking confidence rule: %+v", r.CategoryChecks) + } +} + +func TestDiagnoseKnowledgeExplainsCandidate(t *testing.T) { + g := &fakeGLPI{ticket: model.Ticket{ID: 1, Name: "VPN gateway", Content: "gateway nicht erreichbar", DateMod: "v1", StatusID: 1, CategoryID: 2}, cats: []model.Category{{ID: 2, Name: "VPN"}}} + var d model.Decision + d.Category.ID, d.Category.Confidence = 2, 1 + d.Reply.Allowed, d.Reply.Confidence, d.Reply.KnowledgeID = true, 1, "KB1" + svc := newTestService(t, g, d, true) + if err := svc.Process(context.Background(), 1); err != nil { + t.Fatal(err) + } + r := svc.state.Recent(1)[0] + diag, err := svc.DiagnoseKnowledge(context.Background(), r.RunID, "KB1") + if err != nil { + t.Fatal(err) + } + if diag.KnowledgeID != "KB1" || diag.RetrievalRank != 1 { + t.Fatalf("unexpected diagnostic: %+v", diag) + } + if len(diag.Checks) == 0 { + t.Fatal("expected diagnostic checks") + } + if diag.CurrentTicketChanged { + t.Fatal("ticket should not be marked changed") + } +} diff --git a/internal/agent/policy.go b/internal/agent/policy.go index 31d896e..7e0f877 100644 --- a/internal/agent/policy.go +++ b/internal/agent/policy.go @@ -1,6 +1,7 @@ package agent import ( + "fmt" "html" "strings" @@ -72,8 +73,34 @@ func (p Policy) Evaluate(t model.Ticket, d model.Decision, categories []model.Ca res.CategoryRecommendationName = categoryDisplayName(c) } - // The model only recommends a category and a confidence score. Whether a - // write is allowed is entirely deterministic and owned by this policy. + // Category rules. "already correct" is not a failure; it means that no + // write action is necessary even though the recommendation is valid. + res.CategoryChecks = append(res.CategoryChecks, + check("category", "category_auto_enabled", "Automatische Kategorisierung aktiviert", boolStatus(p.AutoCategory), !p.AutoCategory, boolText(p.AutoCategory), "true", "Globale Schreibfreigabe für Kategorien."), + ) + if d.Category.ID == 0 { + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_recommendation_present", "KI hat eine Kategorie empfohlen", "fail", true, "#0", "gültige GLPI-Kategorie", "Kategorie 0 bedeutet: keine fachlich vertretbare Empfehlung.")) + } else { + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_recommendation_present", "KI hat eine Kategorie empfohlen", "pass", false, fmt.Sprintf("#%d", d.Category.ID), "gültige GLPI-Kategorie", "")) + } + _, categoryKnown := known[d.Category.ID] + if d.Category.ID == 0 { + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_known", "Empfohlene Kategorie existiert in GLPI", "na", false, "–", "bekannte Kategorie", "Keine Kategorie empfohlen.")) + } else if categoryKnown { + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_known", "Empfohlene Kategorie existiert in GLPI", "pass", false, fmt.Sprintf("#%d %s", d.Category.ID, res.CategoryRecommendationName), "bekannte Kategorie", "")) + } else { + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_known", "Empfohlene Kategorie existiert in GLPI", "fail", true, fmt.Sprintf("#%d", d.Category.ID), "bekannte Kategorie", "Die KI darf nur IDs aus der bereitgestellten GLPI-Kategorieliste verwenden.")) + } + if d.Category.ID != 0 && d.Category.ID == t.CategoryID { + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_change_required", "Kategorieänderung ist notwendig", "info", false, fmt.Sprintf("bereits #%d", t.CategoryID), "andere Kategorie", "Die aktuelle Kategorie entspricht bereits der KI-Empfehlung.")) + } else if d.Category.ID != 0 { + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_change_required", "Kategorieänderung ist notwendig", "pass", false, fmt.Sprintf("#%d → #%d", t.CategoryID, d.Category.ID), "abweichende Kategorie", "")) + } else { + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_change_required", "Kategorieänderung ist notwendig", "na", false, "–", "abweichende Kategorie", "Keine Empfehlung vorhanden.")) + } + catConfidenceOK := d.Category.Confidence >= p.CategoryConfidence + res.CategoryChecks = append(res.CategoryChecks, check("category", "category_confidence", "KI-Confidence erreicht Schwellwert", passFail(catConfidenceOK), !catConfidenceOK, percentText(d.Category.Confidence), ">= "+percentText(p.CategoryConfidence), "")) + switch { case !p.AutoCategory: res.CategoryDecision = "category_auto_disabled" @@ -81,9 +108,9 @@ func (p Policy) Evaluate(t model.Ticket, d model.Decision, categories []model.Ca res.CategoryDecision = "category_no_recommendation" case d.Category.ID == t.CategoryID: res.CategoryDecision = "category_already_correct" - case known[d.Category.ID].ID == 0: + case !categoryKnown: res.CategoryDecision = "category_unknown" - case d.Category.Confidence < p.CategoryConfidence: + case !catConfidenceOK: res.CategoryDecision = "category_confidence_below_threshold" default: res.ChangeCategory = true @@ -91,141 +118,229 @@ func (p Policy) Evaluate(t model.Ticket, d model.Decision, categories []model.Ca res.CategoryDecision = "category_accepted" } - // Auto-reply uses the same explainable, fail-closed approach. The first - // failed gate becomes the audit reason shown in the dashboard. - switch { - case !p.AutoReply: - res.ReplyDecision = "reply_auto_disabled" - return res, nil - case len(hits) == 0: - res.ReplyDecision = "reply_no_knowledge_candidates" - return res, nil - case !d.Reply.Allowed: - res.ReplyDecision = "reply_model_not_recommended" - return res, nil - case d.Reply.Confidence < p.ReplyConfidence: - res.ReplyDecision = "reply_confidence_below_threshold" - return res, nil - case res.ReplyKnowledgeID == "": - res.ReplyDecision = "reply_no_knowledge_selected" - return res, nil - case p.BlockReplyOnContextError && contextData.Incomplete: - res.ReplyDecision = "reply_context_incomplete" - return res, nil - case p.BlockReplyOnIncident && contextData.HasRelevantIncident(p.ContextRelevanceMinScore): - res.ReplyDecision = "reply_relevant_incident" - return res, nil - } - - var hit *model.KnowledgeHit + // Resolve the selected knowledge article once. All gates below are evaluated + // even when an earlier gate failed, so diagnostics can show the complete rule + // picture instead of only the first short-circuit reason. + var selected *model.KnowledgeHit for i := range hits { if hits[i].Doc.ID == res.ReplyKnowledgeID { - hit = &hits[i] + selected = &hits[i] break } } - if hit == nil { - res.ReplyDecision = "reply_knowledge_not_found" - return res, nil - } - if !p.sourceAllowed(hit.Doc.Source) { - res.ReplyDecision = "reply_source_not_allowed" - return res, nil - } - if !p.sourceAllowedForReply(hit.Doc.Source) { - res.ReplyDecision = "reply_source_not_allowed_for_auto_reply" - return res, nil - } - if !strings.EqualFold(strings.TrimSpace(hit.Doc.Language), p.CommunicationLanguage) { - res.ReplyDecision = "reply_language_mismatch" - return res, nil - } - if !strings.EqualFold(strings.TrimSpace(hit.Doc.CommunicationStyle), p.CommunicationStyle) { - res.ReplyDecision = "reply_style_mismatch" - return res, nil - } - threshold := p.KnowledgeMinScore - if hit.Doc.MinScore > threshold { - threshold = hit.Doc.MinScore - } - res.KnowledgeThreshold = threshold - res.KnowledgeRetrievalScore = hit.Score - res.KnowledgeRetrievalFloor = p.KnowledgeRetrievalFloor - if !hit.Doc.AutoReply { - res.ReplyDecision = "reply_knowledge_auto_reply_disabled" - return res, nil - } - if hit.Score < p.KnowledgeRetrievalFloor { - res.ReplyDecision = "reply_knowledge_retrieval_below_floor" - return res, nil - } - catIDForEvidence := t.CategoryID - if res.ChangeCategory { - catIDForEvidence = res.CategoryID - } else if d.Category.ID != 0 && d.Category.ID == t.CategoryID { - catIDForEvidence = d.Category.ID - } - categoryEvidence, categoryAvailable := 0.0, false - if len(hit.Doc.Categories) > 0 && catIDForEvidence != 0 { - categoryAvailable = true - for _, id := range hit.Doc.Categories { - if id == catIDForEvidence { - categoryEvidence = 1 - res.KnowledgeCategoryAligned = true - break - } - } - } - res.KnowledgeEvidenceScore = evidenceScore( - hit.Score, d.Reply.Confidence, categoryEvidence, categoryAvailable, - p.KnowledgeEvidenceRetrievalWeight, p.KnowledgeEvidenceAIWeight, p.KnowledgeEvidenceCategoryWeight, + + relevantIncident := contextData.HasRelevantIncident(p.ContextRelevanceMinScore) + res.ReplyChecks = append(res.ReplyChecks, + check("reply", "reply_auto_enabled", "Auto-Reply global aktiviert", boolStatus(p.AutoReply), !p.AutoReply, boolText(p.AutoReply), "true", ""), + check("reply", "reply_candidates_available", "Mindestens ein Knowledge-Kandidat vorhanden", passFail(len(hits) > 0), len(hits) == 0, fmt.Sprintf("%d Kandidaten", len(hits)), "> 0", ""), + check("reply", "reply_model_recommended", "KI empfiehlt eine Antwort", passFail(d.Reply.Allowed), !d.Reply.Allowed, boolText(d.Reply.Allowed), "true", ""), + check("reply", "reply_confidence", "KI-Reply-Confidence erreicht Schwellwert", passFail(d.Reply.Confidence >= p.ReplyConfidence), d.Reply.Confidence < p.ReplyConfidence, percentText(d.Reply.Confidence), ">= "+percentText(p.ReplyConfidence), ""), ) - if res.KnowledgeEvidenceScore < threshold { - res.ReplyDecision = "reply_knowledge_evidence_below_threshold" - return res, nil + if res.ReplyKnowledgeID == "" { + res.ReplyChecks = append(res.ReplyChecks, check("reply", "reply_knowledge_selected", "KI hat einen Knowledge-Artikel ausgewählt", "fail", true, "keine ID", "ID eines bereitgestellten Kandidaten", "")) + } else { + res.ReplyChecks = append(res.ReplyChecks, check("reply", "reply_knowledge_selected", "KI hat einen Knowledge-Artikel ausgewählt", "pass", false, res.ReplyKnowledgeID, "ID eines bereitgestellten Kandidaten", "")) } - if strings.TrimSpace(hit.Doc.Answer) == "" { - res.ReplyDecision = "reply_knowledge_answer_empty" - return res, nil + if p.BlockReplyOnContextError { + res.ReplyChecks = append(res.ReplyChecks, check("context", "reply_context_complete", "Kontextquellen vollständig", passFail(!contextData.Incomplete), contextData.Incomplete, boolText(!contextData.Incomplete), "true", strings.Join(contextData.Warnings, "; "))) + } else { + res.ReplyChecks = append(res.ReplyChecks, check("context", "reply_context_complete", "Kontextquellen vollständig", "na", false, boolText(!contextData.Incomplete), "nicht blockierend", "CONTEXT_BLOCK_AUTO_REPLY_ON_ERRORS ist deaktiviert.")) } - catID := t.CategoryID - if res.ChangeCategory { - catID = res.CategoryID + if p.BlockReplyOnIncident { + res.ReplyChecks = append(res.ReplyChecks, check("context", "reply_no_relevant_incident", "Keine relevante zentrale Störung", passFail(!relevantIncident), relevantIncident, boolText(!relevantIncident), "true", "Major Incidents und Uptime-Kuma-Störungen werden berücksichtigt.")) + } else { + res.ReplyChecks = append(res.ReplyChecks, check("context", "reply_no_relevant_incident", "Keine relevante zentrale Störung", "na", false, boolText(!relevantIncident), "nicht blockierend", "Incident-Blockierung ist deaktiviert.")) } - if len(hit.Doc.Categories) > 0 { - allowed := false - for _, id := range hit.Doc.Categories { - if id == catID { - allowed = true - break + if res.ReplyKnowledgeID == "" { + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_knowledge_exists", "Ausgewählter Knowledge-Artikel ist verfügbar", "na", false, "–", "vorhandener Artikel", "Keine Knowledge-ID ausgewählt.")) + } else if selected == nil { + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_knowledge_exists", "Ausgewählter Knowledge-Artikel ist verfügbar", "fail", true, res.ReplyKnowledgeID, "Kandidat im übergebenen Set", "Die KI hat eine ID ausgewählt, die nicht im Kandidatenset vorhanden ist.")) + } else { + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_knowledge_exists", "Ausgewählter Knowledge-Artikel ist verfügbar", "pass", false, selected.Doc.ID, "Kandidat im übergebenen Set", selected.Doc.Title)) + } + + if selected != nil { + sourceAllowed := p.sourceAllowed(selected.Doc.Source) + sourceReplyAllowed := p.sourceAllowedForReply(selected.Doc.Source) + langOK := strings.EqualFold(strings.TrimSpace(selected.Doc.Language), p.CommunicationLanguage) + styleOK := strings.EqualFold(strings.TrimSpace(selected.Doc.CommunicationStyle), p.CommunicationStyle) + res.ReplyChecks = append(res.ReplyChecks, + check("knowledge", "reply_source_allowed", "Knowledge-Quelle ist für Retrieval erlaubt", passFail(sourceAllowed), !sourceAllowed, selected.Doc.Source, "KNOWLEDGE_ALLOWED_SOURCES", ""), + check("knowledge", "reply_source_auto_allowed", "Knowledge-Quelle ist für Auto-Reply erlaubt", passFail(sourceReplyAllowed), !sourceReplyAllowed, selected.Doc.Source, "KNOWLEDGE_AUTO_REPLY_SOURCES", ""), + check("communication", "reply_language_match", "Sprache des Artikels passt zur Kommunikationspolicy", passFail(langOK), !langOK, selected.Doc.Language, p.CommunicationLanguage, ""), + check("communication", "reply_style_match", "Stil des Artikels passt zur Kommunikationspolicy", passFail(styleOK), !styleOK, selected.Doc.CommunicationStyle, p.CommunicationStyle, ""), + ) + autoDetail := "" + if len(selected.Doc.UnmappedExternalCategories) > 0 { + autoDetail = "Nicht zugeordnete externe Kategorien: " + strings.Join(selected.Doc.UnmappedExternalCategories, ", ") + } + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_knowledge_auto_reply", "Artikel ist für Auto-Reply freigegeben", passFail(selected.Doc.AutoReply), !selected.Doc.AutoReply, boolText(selected.Doc.AutoReply), "true", autoDetail)) + + threshold := p.KnowledgeMinScore + if selected.Doc.MinScore > threshold { + threshold = selected.Doc.MinScore + } + res.KnowledgeThreshold = threshold + res.KnowledgeRetrievalScore = selected.Score + res.KnowledgeRetrievalFloor = p.KnowledgeRetrievalFloor + retrievalOK := selected.Score >= p.KnowledgeRetrievalFloor + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_retrieval_floor", "Retrieval-Score erreicht Mindestfloor", passFail(retrievalOK), !retrievalOK, percentText(selected.Score), ">= "+percentText(p.KnowledgeRetrievalFloor), "Der Retrieval-Floor entscheidet, ob ein Artikel überhaupt als plausibler Kandidat gilt.")) + + catIDForEvidence := t.CategoryID + if res.ChangeCategory { + catIDForEvidence = res.CategoryID + } else if d.Category.ID != 0 && d.Category.ID == t.CategoryID { + catIDForEvidence = d.Category.ID + } + categoryEvidence, categoryAvailable := 0.0, false + if len(selected.Doc.Categories) > 0 && catIDForEvidence != 0 { + categoryAvailable = true + for _, id := range selected.Doc.Categories { + if id == catIDForEvidence { + categoryEvidence = 1 + res.KnowledgeCategoryAligned = true + break + } } } - if !allowed { - res.ReplyDecision = "reply_category_not_allowed" - return res, nil + res.KnowledgeEvidenceScore = evidenceScore(selected.Score, d.Reply.Confidence, categoryEvidence, categoryAvailable, p.KnowledgeEvidenceRetrievalWeight, p.KnowledgeEvidenceAIWeight, p.KnowledgeEvidenceCategoryWeight) + evidenceOK := res.KnowledgeEvidenceScore >= threshold + catDetail := "Kategorie nicht als Evidenz verfügbar; Gewichte werden normalisiert." + if categoryAvailable { + catDetail = "Kategorie-Evidenz: " + percentText(categoryEvidence) } - } - res.Reply = true - if p.AIContentLabelEnabled { - // The disclosure is HTML, therefore every AI-selected automatic reply is - // sent as HTML. Plain-text KB answers are escaped before wrapping. - if strings.TrimSpace(hit.Doc.AnswerHTML) != "" { - res.ReplyText = p.formatRichReply(hit.Doc.AnswerHTML) - } else { - res.ReplyText = p.formatRichReply(p.plainTextToHTML(hit.Doc.Answer)) + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_evidence_threshold", "Finale Knowledge-Evidenz erreicht Schwellwert", passFail(evidenceOK), !evidenceOK, percentText(res.KnowledgeEvidenceScore), ">= "+percentText(threshold), catDetail)) + + answerPresent := strings.TrimSpace(selected.Doc.Answer) != "" || strings.TrimSpace(selected.Doc.AnswerHTML) != "" + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_answer_present", "Freigegebener Antwortinhalt vorhanden", passFail(answerPresent), !answerPresent, boolText(answerPresent), "true", "")) + + categoryAllowed := true + categoryDetail := "Artikel gilt für alle ITIL-Kategorien." + catID := t.CategoryID + if res.ChangeCategory { + catID = res.CategoryID } - res.ReplyIsHTML = true - } else if strings.TrimSpace(hit.Doc.AnswerHTML) != "" { - res.ReplyText = p.formatRichReply(hit.Doc.AnswerHTML) - res.ReplyIsHTML = true + if len(selected.Doc.Categories) > 0 { + categoryAllowed = false + for _, id := range selected.Doc.Categories { + if id == catID { + categoryAllowed = true + break + } + } + categoryDetail = fmt.Sprintf("Artikel-Kategorien: %v; effektive Ticketkategorie: #%d", selected.Doc.Categories, catID) + } + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_category_scope", "Artikel ist für die effektive Ticketkategorie freigegeben", passFail(categoryAllowed), !categoryAllowed, boolText(categoryAllowed), "true", categoryDetail)) } else { - res.ReplyText = p.formatReply(hit.Doc.Answer) + for _, spec := range []struct{ code, label string }{ + {"reply_source_allowed", "Knowledge-Quelle ist für Retrieval erlaubt"}, + {"reply_source_auto_allowed", "Knowledge-Quelle ist für Auto-Reply erlaubt"}, + {"reply_language_match", "Sprache des Artikels passt zur Kommunikationspolicy"}, + {"reply_style_match", "Stil des Artikels passt zur Kommunikationspolicy"}, + {"reply_knowledge_auto_reply", "Artikel ist für Auto-Reply freigegeben"}, + {"reply_retrieval_floor", "Retrieval-Score erreicht Mindestfloor"}, + {"reply_evidence_threshold", "Finale Knowledge-Evidenz erreicht Schwellwert"}, + {"reply_answer_present", "Freigegebener Antwortinhalt vorhanden"}, + {"reply_category_scope", "Artikel ist für die effektive Ticketkategorie freigegeben"}, + } { + res.ReplyChecks = append(res.ReplyChecks, check("knowledge", spec.code, spec.label, "na", false, "–", "Knowledge-Artikel erforderlich", "Kein gültiger Knowledge-Artikel ausgewählt.")) + } + } + + // Keep the historical decision codes stable. The first failed gate in this + // ordered list is the actual blocking reason. + switch { + case !p.AutoReply: + res.ReplyDecision = "reply_auto_disabled" + case len(hits) == 0: + res.ReplyDecision = "reply_no_knowledge_candidates" + case !d.Reply.Allowed: + res.ReplyDecision = "reply_model_not_recommended" + case d.Reply.Confidence < p.ReplyConfidence: + res.ReplyDecision = "reply_confidence_below_threshold" + case res.ReplyKnowledgeID == "": + res.ReplyDecision = "reply_no_knowledge_selected" + case p.BlockReplyOnContextError && contextData.Incomplete: + res.ReplyDecision = "reply_context_incomplete" + case p.BlockReplyOnIncident && relevantIncident: + res.ReplyDecision = "reply_relevant_incident" + case selected == nil: + res.ReplyDecision = "reply_knowledge_not_found" + case !p.sourceAllowed(selected.Doc.Source): + res.ReplyDecision = "reply_source_not_allowed" + case !p.sourceAllowedForReply(selected.Doc.Source): + res.ReplyDecision = "reply_source_not_allowed_for_auto_reply" + case !strings.EqualFold(strings.TrimSpace(selected.Doc.Language), p.CommunicationLanguage): + res.ReplyDecision = "reply_language_mismatch" + case !strings.EqualFold(strings.TrimSpace(selected.Doc.CommunicationStyle), p.CommunicationStyle): + res.ReplyDecision = "reply_style_mismatch" + case !selected.Doc.AutoReply: + res.ReplyDecision = "reply_knowledge_auto_reply_disabled" + case selected.Score < p.KnowledgeRetrievalFloor: + res.ReplyDecision = "reply_knowledge_retrieval_below_floor" + case res.KnowledgeEvidenceScore < res.KnowledgeThreshold: + res.ReplyDecision = "reply_knowledge_evidence_below_threshold" + case strings.TrimSpace(selected.Doc.Answer) == "" && strings.TrimSpace(selected.Doc.AnswerHTML) == "": + res.ReplyDecision = "reply_knowledge_answer_empty" + case !knowledgeCategoryAllowed(selected.Doc, t.CategoryID, res.ChangeCategory, res.CategoryID): + res.ReplyDecision = "reply_category_not_allowed" + default: + res.Reply = true + if p.AIContentLabelEnabled { + if strings.TrimSpace(selected.Doc.AnswerHTML) != "" { + res.ReplyText = p.formatRichReply(selected.Doc.AnswerHTML) + } else { + res.ReplyText = p.formatRichReply(p.plainTextToHTML(selected.Doc.Answer)) + } + res.ReplyIsHTML = true + } else if strings.TrimSpace(selected.Doc.AnswerHTML) != "" { + res.ReplyText = p.formatRichReply(selected.Doc.AnswerHTML) + res.ReplyIsHTML = true + } else { + res.ReplyText = p.formatReply(selected.Doc.Answer) + } + res.KnowledgeID = selected.Doc.ID + res.ReplyDecision = "reply_accepted" } - res.KnowledgeID = hit.Doc.ID - res.ReplyDecision = "reply_accepted" return res, nil } +func check(group, code, label, status string, blocking bool, actual, expected, detail string) model.RuleCheck { + return model.RuleCheck{Group: group, Code: code, Label: label, Status: status, Blocking: blocking && status == "fail", Actual: actual, Expected: expected, Detail: detail} +} + +func passFail(ok bool) string { + if ok { + return "pass" + } + return "fail" +} + +func boolStatus(ok bool) string { return passFail(ok) } +func boolText(v bool) string { + if v { + return "ja" + } + return "nein" +} +func percentText(v float64) string { return fmt.Sprintf("%.1f %%", clampPolicy01(v)*100) } + +func knowledgeCategoryAllowed(d model.KnowledgeDoc, currentCategory int64, change bool, target int64) bool { + catID := currentCategory + if change { + catID = target + } + if len(d.Categories) == 0 { + return true + } + for _, id := range d.Categories { + if id == catID { + return true + } + } + return false +} + func categoryDisplayName(c model.Category) string { if strings.TrimSpace(c.CompleteName) != "" { return strings.TrimSpace(c.CompleteName) @@ -283,7 +398,7 @@ func (p Policy) plainTextToHTML(body string) string { if normalized == "" { return "" } - paragraphs := strings.Split(normalized, "\\n\\n") + paragraphs := strings.Split(normalized, "\n\n") out := make([]string, 0, len(paragraphs)) for _, paragraph := range paragraphs { paragraph = strings.TrimSpace(paragraph) diff --git a/internal/knowledge/store.go b/internal/knowledge/store.go index 77d3db2..c6c5a02 100644 --- a/internal/knowledge/store.go +++ b/internal/knowledge/store.go @@ -1020,6 +1020,32 @@ func (s *Store) persistVectorCache() error { return s.persistSnapshot() } +// FindMetadata searches knowledge metadata without copying the complete corpus. +// It is intended for interactive UI lookups on very large knowledge stores. +func (s *Store) FindMetadata(query string, limit int) []model.KnowledgeDoc { + if s == nil { + return nil + } + q := strings.ToLower(strings.TrimSpace(query)) + if limit <= 0 { + limit = 30 + } + s.mu.RLock() + defer s.mu.RUnlock() + out := make([]model.KnowledgeDoc, 0, limit) + for _, d := range s.docs { + hay := strings.ToLower(d.ID + " " + d.Title + " " + strings.Join(d.Keywords, " ") + " " + strings.Join(d.ExternalCategories, " ")) + if q != "" && !strings.Contains(hay, q) { + continue + } + out = append(out, d) + if len(out) >= limit { + break + } + } + return out +} + func (s *Store) ManagedDir() string { if s == nil { return "" diff --git a/internal/model/model.go b/internal/model/model.go index 2a47366..e2b1353 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -194,29 +194,76 @@ type Decision struct { Reason string `json:"reason"` } +// RuleCheck is a machine-readable explanation of one deterministic decision gate. +// Status is one of: pass, fail, warn, info, na. +type RuleCheck struct { + Code string `json:"code"` + Group string `json:"group,omitempty"` + Label string `json:"label"` + Status string `json:"status"` + Blocking bool `json:"blocking,omitempty"` + Actual string `json:"actual,omitempty"` + Expected string `json:"expected,omitempty"` + Detail string `json:"detail,omitempty"` +} + +// KnowledgeDiagnostic explains why one knowledge article was or was not +// considered for a specific ticket run. The score is recalculated against the +// current knowledge index; CurrentTicketChanged tells the UI when the ticket has +// changed since the historical run. +type KnowledgeDiagnostic struct { + RunID string `json:"run_id"` + TicketID int64 `json:"ticket_id"` + KnowledgeID string `json:"knowledge_id"` + Title string `json:"title"` + Source string `json:"source"` + CurrentTicketChanged bool `json:"current_ticket_changed"` + RetrievalRank int `json:"retrieval_rank"` + RetrievalScore float64 `json:"retrieval_score"` + SemanticScore float64 `json:"semantic_score,omitempty"` + TitleScore float64 `json:"title_score,omitempty"` + LexicalScore float64 `json:"lexical_score,omitempty"` + KeywordScore float64 `json:"keyword_score,omitempty"` + CategoryScore float64 `json:"category_score,omitempty"` + CandidateCutoff float64 `json:"candidate_cutoff,omitempty"` + SentToAI bool `json:"sent_to_ai"` + SelectionReason string `json:"selection_reason"` + AISelected bool `json:"ai_selected"` + EvidenceScore float64 `json:"evidence_score,omitempty"` + RequiredScore float64 `json:"required_score,omitempty"` + BestChunkExcerpt string `json:"best_chunk_excerpt,omitempty"` + BestQueryExcerpt string `json:"best_query_excerpt,omitempty"` + ExternalCategories []string `json:"external_categories,omitempty"` + UnmappedCategories []string `json:"unmapped_categories,omitempty"` + Checks []RuleCheck `json:"checks"` + Document KnowledgeDoc `json:"document"` +} + type PolicyResult struct { - ChangeCategory bool `json:"change_category"` - CategoryID int64 `json:"category_id"` - CategoryRecommendationID int64 `json:"category_recommendation_id"` - CategoryRecommendationName string `json:"category_recommendation_name,omitempty"` - CategoryConfidence float64 `json:"category_confidence"` - CategoryThreshold float64 `json:"category_threshold"` - CategoryDecision string `json:"category_decision"` - Reply bool `json:"reply"` - ReplyText string `json:"reply_text,omitempty"` - ReplyIsHTML bool `json:"reply_is_html,omitempty"` - KnowledgeID string `json:"knowledge_id,omitempty"` - ReplyRecommendation bool `json:"reply_recommendation"` - ReplyConfidence float64 `json:"reply_confidence"` - ReplyThreshold float64 `json:"reply_threshold"` - ReplyKnowledgeID string `json:"reply_knowledge_id,omitempty"` - ReplyDecision string `json:"reply_decision"` - KnowledgeThreshold float64 `json:"knowledge_threshold,omitempty"` - KnowledgeRetrievalScore float64 `json:"knowledge_retrieval_score,omitempty"` - KnowledgeEvidenceScore float64 `json:"knowledge_evidence_score,omitempty"` - KnowledgeRetrievalFloor float64 `json:"knowledge_retrieval_floor,omitempty"` - KnowledgeCategoryAligned bool `json:"knowledge_category_aligned,omitempty"` - AIReason string `json:"ai_reason,omitempty"` + ChangeCategory bool `json:"change_category"` + CategoryID int64 `json:"category_id"` + CategoryRecommendationID int64 `json:"category_recommendation_id"` + CategoryRecommendationName string `json:"category_recommendation_name,omitempty"` + CategoryConfidence float64 `json:"category_confidence"` + CategoryThreshold float64 `json:"category_threshold"` + CategoryDecision string `json:"category_decision"` + Reply bool `json:"reply"` + ReplyText string `json:"reply_text,omitempty"` + ReplyIsHTML bool `json:"reply_is_html,omitempty"` + KnowledgeID string `json:"knowledge_id,omitempty"` + ReplyRecommendation bool `json:"reply_recommendation"` + ReplyConfidence float64 `json:"reply_confidence"` + ReplyThreshold float64 `json:"reply_threshold"` + ReplyKnowledgeID string `json:"reply_knowledge_id,omitempty"` + ReplyDecision string `json:"reply_decision"` + KnowledgeThreshold float64 `json:"knowledge_threshold,omitempty"` + KnowledgeRetrievalScore float64 `json:"knowledge_retrieval_score,omitempty"` + KnowledgeEvidenceScore float64 `json:"knowledge_evidence_score,omitempty"` + KnowledgeRetrievalFloor float64 `json:"knowledge_retrieval_floor,omitempty"` + KnowledgeCategoryAligned bool `json:"knowledge_category_aligned,omitempty"` + AIReason string `json:"ai_reason,omitempty"` + CategoryChecks []RuleCheck `json:"category_checks,omitempty"` + ReplyChecks []RuleCheck `json:"reply_checks,omitempty"` } // KnowledgeCandidateAudit captures the top retrieval candidates used for a run. @@ -238,6 +285,8 @@ type KnowledgeCandidateAudit struct { QueryChunkCount int `json:"query_chunk_count,omitempty"` DocumentChunkCount int `json:"document_chunk_count,omitempty"` SentToAI bool `json:"sent_to_ai,omitempty"` + RetrievalRank int `json:"retrieval_rank,omitempty"` + SelectionReason string `json:"selection_reason,omitempty"` } // ContextAuditItem is a compact snapshot of context that influenced a run. @@ -261,6 +310,9 @@ type RunRecord struct { Reason string `json:"reason"` AIReason string `json:"ai_reason,omitempty"` PolicyReason string `json:"policy_reason,omitempty"` + CategoryChecks []RuleCheck `json:"category_checks,omitempty"` + ReplyChecks []RuleCheck `json:"reply_checks,omitempty"` + ExecutionChecks []RuleCheck `json:"execution_checks,omitempty"` CategoryBefore int64 `json:"category_before"` CategoryBeforeName string `json:"category_before_name,omitempty"` AIRecommendedCategoryID int64 `json:"ai_recommended_category_id,omitempty"` diff --git a/internal/web/server.go b/internal/web/server.go index 49d7474..a080f6d 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -25,7 +25,7 @@ import ( "github.com/example/glpi-ai-agent/internal/state" ) -//go:embed templates/dashboard.html +//go:embed templates/dashboard.html templates/diagnostics.html var files embed.FS type KnowledgeManager interface { @@ -38,6 +38,7 @@ type KnowledgeManager interface { LoadStats() knowledgepkg.LoadStats InitStatus() knowledgepkg.InitStatus Ready() bool + FindMetadata(string, int) []model.KnowledgeDoc } type FeedbackManager interface { Categories(context.Context) ([]model.Category, error) @@ -47,22 +48,32 @@ type FeedbackManager interface { LearningCount() int } +type DiagnosticsManager interface { + DiagnoseRun(context.Context, string) (model.RunRecord, error) + DiagnoseKnowledge(context.Context, string, string) (model.KnowledgeDiagnostic, error) +} + type Server struct { - cfg config.Config - metrics *metrics.Metrics - state *state.Store - q *queue.Queue - knowledge KnowledgeManager - feedback FeedbackManager - tpl *template.Template + cfg config.Config + metrics *metrics.Metrics + state *state.Store + q *queue.Queue + knowledge KnowledgeManager + feedback FeedbackManager + diagnostics DiagnosticsManager + tpl *template.Template } func New(cfg config.Config, m *metrics.Metrics, s *state.Store, q *queue.Queue, k KnowledgeManager, f FeedbackManager) (*Server, error) { - t, err := template.ParseFS(files, "templates/dashboard.html") + t, err := template.ParseFS(files, "templates/*.html") if err != nil { return nil, err } - return &Server{cfg: cfg, metrics: m, state: s, q: q, knowledge: k, feedback: f, tpl: t}, nil + srv := &Server{cfg: cfg, metrics: m, state: s, q: q, knowledge: k, feedback: f, tpl: t} + if d, ok := f.(DiagnosticsManager); ok { + srv.diagnostics = d + } + return srv, nil } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() @@ -70,6 +81,10 @@ func (s *Server) Handler() http.Handler { mux.HandleFunc("GET /readyz", s.ready) mux.HandleFunc("GET /metrics", s.prom) mux.Handle("GET /", s.auth(http.HandlerFunc(s.dashboard))) + mux.Handle("GET /diagnostics", s.auth(http.HandlerFunc(s.diagnosticsPage))) + mux.Handle("GET /api/diagnostics/run/{id}", s.auth(http.HandlerFunc(s.diagnosticRun))) + mux.Handle("GET /api/diagnostics/run/{id}/knowledge", s.auth(http.HandlerFunc(s.diagnosticKnowledge))) + mux.Handle("GET /api/diagnostics/knowledge", s.auth(http.HandlerFunc(s.diagnosticKnowledgeSearch))) mux.Handle("GET /api/status", s.auth(http.HandlerFunc(s.status))) mux.Handle("GET /api/runs", s.auth(http.HandlerFunc(s.runs))) mux.Handle("GET /api/categories", s.auth(http.HandlerFunc(s.categories))) @@ -110,6 +125,73 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = s.tpl.ExecuteTemplate(w, "dashboard.html", map[string]any{"DryRun": s.cfg.DryRun, "AutoReply": s.cfg.AutoReply, "AutoCategory": s.cfg.AutoCategory, "CommunicationLanguage": s.cfg.CommunicationLanguage, "CommunicationStyle": s.cfg.CommunicationStyle}) } +func (s *Server) diagnosticsPage(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/diagnostics" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = s.tpl.ExecuteTemplate(w, "diagnostics.html", nil) +} + +func (s *Server) diagnosticRun(w http.ResponseWriter, r *http.Request) { + if s.diagnostics == nil { + http.Error(w, "diagnostics unavailable", http.StatusNotImplemented) + return + } + run, err := s.diagnostics.DiagnoseRun(r.Context(), strings.TrimSpace(r.PathValue("id"))) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + respondJSON(w, run) +} + +func (s *Server) diagnosticKnowledge(w http.ResponseWriter, r *http.Request) { + if s.diagnostics == nil { + http.Error(w, "diagnostics unavailable", http.StatusNotImplemented) + return + } + kbID := strings.TrimSpace(r.URL.Query().Get("knowledge_id")) + if kbID == "" { + http.Error(w, "knowledge_id required", http.StatusBadRequest) + return + } + d, err := s.diagnostics.DiagnoseKnowledge(r.Context(), strings.TrimSpace(r.PathValue("id")), kbID) + if err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + respondJSON(w, d) +} + +func (s *Server) diagnosticKnowledgeSearch(w http.ResponseWriter, r *http.Request) { + q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q"))) + limit := 30 + if n, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && n > 0 && n <= 100 { + limit = n + } + type item struct { + ID string `json:"id"` + Title string `json:"title"` + Source string `json:"source"` + AutoReply bool `json:"auto_reply"` + Unmapped []string `json:"unmapped_categories,omitempty"` + } + out := make([]item, 0, limit) + for _, d := range s.knowledge.List() { + hay := strings.ToLower(d.ID + " " + d.Title + " " + strings.Join(d.Keywords, " ") + " " + strings.Join(d.ExternalCategories, " ")) + if q != "" && !strings.Contains(hay, q) { + continue + } + out = append(out, item{ID: d.ID, Title: d.Title, Source: d.Source, AutoReply: d.AutoReply, Unmapped: append([]string(nil), d.UnmappedExternalCategories...)}) + if len(out) >= limit { + break + } + } + respondJSON(w, out) +} + func (s *Server) status(w http.ResponseWriter, r *http.Request) { g, o := s.metrics.Health() kbOK, kbDocs, kbLastSync, kbLastErr := s.metrics.GLPIKBStatus() diff --git a/internal/web/server_test.go b/internal/web/server_test.go index 1385c15..0dbe9fe 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -42,7 +42,7 @@ func TestExtractTicketID(t *testing.T) { } func TestDashboardTemplateParses(t *testing.T) { - if _, err := template.ParseFS(files, "templates/dashboard.html"); err != nil { + if _, err := template.ParseFS(files, "templates/*.html"); err != nil { t.Fatal(err) } } @@ -171,3 +171,15 @@ func TestDashboardContainsControlCenterSections(t *testing.T) { } } } + +func TestDiagnosticsTemplateAndKnowledgeSearch(t *testing.T) { + h, _ := newKnowledgeTestServer(t) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/diagnostics", nil)) + if rr.Code != http.StatusOK { + t.Fatalf("diagnostics = %d: %s", rr.Code, rr.Body.String()) + } + if !bytes.Contains(rr.Body.Bytes(), []byte("Entscheidungsdiagnose")) { + t.Fatal("diagnostics page missing title") + } +} diff --git a/internal/web/templates/dashboard.html b/internal/web/templates/dashboard.html index 9bcf45f..d3c9364 100644 --- a/internal/web/templates/dashboard.html +++ b/internal/web/templates/dashboard.html @@ -31,6 +31,7 @@ button,input,textarea,select{font:inherit}button{color:inherit}.app{display:grid + Diagnose
Status wird geladen…
diff --git a/internal/web/templates/diagnostics.html b/internal/web/templates/diagnostics.html new file mode 100644 index 0000000..3eb738a --- /dev/null +++ b/internal/web/templates/diagnostics.html @@ -0,0 +1,54 @@ + + + + +GLPI AI Agent · Entscheidungsdiagnose + + + +
+
AI
Entscheidungsdiagnose
Regeln, Retrieval und Auto-Reply nachvollziehbar prüfen
← Control Center
+
+ +
+

Ticketlauf auswählen

Links einen Lauf wählen, um alle Entscheidungsregeln und Knowledge-Kandidaten zu prüfen.

+ +
+ + diff --git a/knowledge/KB-AD-001.json b/knowledge/KB-AD-001.json new file mode 100644 index 0000000..fc682f7 --- /dev/null +++ b/knowledge/KB-AD-001.json @@ -0,0 +1,23 @@ +{ + "id": "KB-AD-001", + "title": "Benutzerkonto gesperrt", + "text": "Typischer Fall: Ein Benutzer kann sich mit seinem Domänen- oder Firmenkonto nicht mehr anmelden und meldet „Konto gesperrt“, „Account locked“, „Benutzer gesperrt“ oder wiederholte Anmeldefehler. Ursache kann eine Kontosperre nach Fehlversuchen, ein veraltetes Kennwort auf einem weiteren Gerät, ein gespeichertes Kennwort in einer Anwendung oder eine Richtlinie sein. Vor einer Entsperrung sollte geprüft werden, ob die Meldung auf mehreren Diensten auftritt und ob kürzlich das Kennwort geändert wurde.", + "answer": "Bitte prüfen Sie zunächst, ob die Anmeldung auch an einem anderen Unternehmensdienst fehlschlägt. Falls Sie Ihr Kennwort kürzlich geändert haben, schließen Sie bitte weitere Geräte oder Anwendungen aus, die möglicherweise noch das alte Kennwort verwenden. Teilen Sie uns anschließend die genaue Fehlermeldung und den betroffenen Dienst mit. Eine notwendige Entsperrung des Kontos erfolgt durch den IT-Service.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Benutzerkonto", + "Konto gesperrt", + "Account locked", + "Anmeldung", + "Domänenkonto", + "Active Directory", + "AD", + "gesperrt" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ad-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-AD-002.json b/knowledge/KB-AD-002.json new file mode 100644 index 0000000..13378ff --- /dev/null +++ b/knowledge/KB-AD-002.json @@ -0,0 +1,22 @@ +{ + "id": "KB-AD-002", + "title": "Kennwort abgelaufen oder Kennwortänderung erforderlich", + "text": "Typische Meldungen sind „Kennwort abgelaufen“, „Passwort muss geändert werden“, „password expired“ oder eine Anmeldung, die erst nach Kennwortänderung fortgesetzt werden kann. Der Fall betrifft in der Regel das zentrale Firmenkonto. Nach einer Kennwortänderung können Anwendungen, Mobilgeräte, VPN-Clients oder gespeicherte Windows-Anmeldeinformationen noch das alte Kennwort verwenden und Folgefehler verursachen.", + "answer": "Bitte ändern Sie Ihr Kennwort über den bei Ihnen vorgesehenen Unternehmensweg. Verwenden Sie anschließend das neue Kennwort auch in Anwendungen und auf weiteren Geräten, die mit Ihrem Firmenkonto verbunden sind. Sollte die Änderung nicht möglich sein oder weiterhin eine Fehlermeldung erscheinen, senden Sie uns bitte den genauen Wortlaut der Meldung.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Kennwort abgelaufen", + "Passwort abgelaufen", + "Kennwort ändern", + "password expired", + "Anmeldung", + "Active Directory", + "Domänenkonto" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ad-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-AD-003.json b/knowledge/KB-AD-003.json new file mode 100644 index 0000000..c1676ec --- /dev/null +++ b/knowledge/KB-AD-003.json @@ -0,0 +1,23 @@ +{ + "id": "KB-AD-003", + "title": "SSO oder LDAP meldet unbekannten Benutzer", + "text": "Ein Dienst mit Single Sign-on, LDAP oder zentraler Anmeldung meldet „unbekannter Benutzer“, „user unknown“, „Benutzer nicht gefunden“ oder verweigert die Anmeldung, obwohl das Firmenkonto grundsätzlich funktioniert. Prüfrelevant sind der betroffene Dienst, ob andere SSO-Dienste funktionieren, ob der Benutzer neu angelegt oder umbenannt wurde und ob das Problem erst seit kurzer Zeit besteht. Mögliche Ursachen sind fehlende Synchronisierung, falsche Benutzerzuordnung oder eine dienstspezifische Berechtigung.", + "answer": "Bitte prüfen Sie, ob Sie sich an einem anderen Unternehmensdienst mit demselben Firmenkonto anmelden können. Teilen Sie uns außerdem den Namen der betroffenen Anwendung sowie den genauen Wortlaut der Meldung „unbekannter Benutzer“ oder einer ähnlichen Meldung mit. Wir prüfen anschließend die Zuordnung und Synchronisierung des Kontos für diesen Dienst.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "SSO", + "LDAP", + "unbekannter Benutzer", + "user unknown", + "Benutzer nicht gefunden", + "Single Sign-on", + "Anmeldung", + "Authentifizierung" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ad-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-AD-004.json b/knowledge/KB-AD-004.json new file mode 100644 index 0000000..2032025 --- /dev/null +++ b/knowledge/KB-AD-004.json @@ -0,0 +1,22 @@ +{ + "id": "KB-AD-004", + "title": "Zugriff auf Netzlaufwerk oder Freigabe nicht möglich", + "text": "Der Benutzer kann ein gemapptes Netzlaufwerk oder eine SMB-Dateifreigabe nicht öffnen. Typische Symptome sind rotes X am Laufwerk, „Netzwerkpfad nicht gefunden“, „Zugriff verweigert“, wiederholte Anmeldung oder ein plötzlich nicht mehr erreichbarer Pfad. Zu unterscheiden sind Netzwerk/VPN-Erreichbarkeit, DNS/Server-Erreichbarkeit und fehlende Berechtigung. Bei Homeoffice ist häufig eine aktive Unternehmens-VPN-Verbindung Voraussetzung.", + "answer": "Bitte prüfen Sie zuerst, ob Sie mit dem Unternehmensnetz beziehungsweise dem VPN verbunden sind. Öffnen Sie das betroffene Laufwerk anschließend erneut. Falls der Fehler bestehen bleibt, senden Sie uns bitte den Laufwerksbuchstaben oder vollständigen Freigabepfad sowie die genaue Fehlermeldung. Nehmen Sie bitte keine Änderungen an Berechtigungen oder gespeicherten Zugangsdaten vor.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Netzlaufwerk", + "Freigabe", + "SMB", + "Netzwerkpfad", + "Zugriff verweigert", + "rotes X", + "Dateiserver" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ad-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-BRO-001.json b/knowledge/KB-BRO-001.json new file mode 100644 index 0000000..7a7dd6f --- /dev/null +++ b/knowledge/KB-BRO-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-BRO-001", + "title": "Brother Laserdrucker ist offline oder druckt nicht", + "text": "Ein Brother-Laserdrucker wird unter Windows als offline/pausiert angezeigt oder nimmt keine Druckaufträge an. Brother empfiehlt zunächst Stromversorgung und Gerätestatus, Fehlermeldungen wie Papierstau/Toner, Verbindung und die Auswahl des richtigen Druckers zu prüfen. Bei Netzwerkdruckern kann zusätzlich die Erreichbarkeit beziehungsweise IP-Zuordnung betroffen sein.", + "answer": "Bitte prüfen Sie, ob der Brother-Drucker eingeschaltet ist und am Display keine Fehlermeldung wie Papierstau oder Tonerproblem angezeigt wird. Stellen Sie sicher, dass Sie in der Anwendung den richtigen Drucker ausgewählt haben. Falls der Drucker weiterhin offline ist, senden Sie uns bitte Modellname, Standort und die Meldung am Display.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Brother", + "Laserdrucker", + "Drucker offline", + "Druckt nicht", + "Printer offline", + "Pausiert" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-bro-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-BRO-002.json b/knowledge/KB-BRO-002.json new file mode 100644 index 0000000..8ae5cd3 --- /dev/null +++ b/knowledge/KB-BRO-002.json @@ -0,0 +1,21 @@ +{ + "id": "KB-BRO-002", + "title": "Brother Laserdrucker meldet Papierstau oder Tonerproblem", + "text": "Das Brother-Gerät zeigt Papierstau, Toner leer/ersetzen, Trommel- oder Verbrauchsmaterialmeldung. Solche Gerätefehler müssen zuerst am Gerät behoben werden, bevor Windows-Druckerdiagnose sinnvoll ist. Verbrauchsmaterial darf nur nach den lokalen Betriebsregeln gewechselt werden.", + "answer": "Bitte lesen Sie die Meldung direkt am Display des Brother-Geräts ab. Bei Papierstau entfernen Sie Papier nur entsprechend den Hinweisen am Gerät und ohne Gewalt. Bei Toner- oder Verbrauchsmaterialmeldungen teilen Sie uns bitte Modell, Standort und den genauen Meldungstext mit, damit der passende Austausch veranlasst werden kann.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Brother Papierstau", + "Brother Toner", + "Trommel", + "Verbrauchsmaterial", + "Paper Jam", + "Replace Toner" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-bro-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-BRO-003.json b/knowledge/KB-BRO-003.json new file mode 100644 index 0000000..a3b7f0b --- /dev/null +++ b/knowledge/KB-BRO-003.json @@ -0,0 +1,21 @@ +{ + "id": "KB-BRO-003", + "title": "Brother Laserdrucker: schlechte Druckqualität", + "text": "Ausdrucke eines Brother-Laserdruckers sind blass, verschmiert, haben Streifen, Punkte oder ungleichmäßige Flächen. Ursachen können Toner/Trommel, Papier, Verschmutzung oder gerätespezifische Kalibrierung sein. Für die IT ist ein Musterausdruck und die Information wichtig, ob alle Benutzer betroffen sind.", + "answer": "Bitte prüfen Sie, ob die Qualitätsstörung bei mehreren Dokumenten auftritt. Drucken Sie – sofern möglich – eine interne Test- oder Statusseite des Geräts. Wenn die Störung auch dort sichtbar ist, teilen Sie uns Modell, Standort und Art des Fehlers mit; ein Foto des Testausdrucks ist hilfreich.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Brother Druckqualität", + "Streifen", + "blass", + "verschmiert", + "Toner", + "Trommel" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-bro-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-BRO-004.json b/knowledge/KB-BRO-004.json new file mode 100644 index 0000000..5d919dd --- /dev/null +++ b/knowledge/KB-BRO-004.json @@ -0,0 +1,21 @@ +{ + "id": "KB-BRO-004", + "title": "Brother Multifunktionsgerät scannt nicht zum Arbeitsplatz", + "text": "Ein Brother-Multifunktionsgerät kann nicht mehr zum PC oder in die verwendete Scananwendung scannen. Zu unterscheiden sind Gerätefehler, Netzwerk/USB, Scansoftware, Zielcomputer und Berechtigung. Bei zentral konfigurierten Scan-to-Mail-/SMB-Zielen sollte keine lokale Neukonfiguration durch Benutzer erfolgen.", + "answer": "Bitte teilen Sie uns mit, ob Sie direkt am Brother-Gerät oder aus einer Anwendung am PC scannen möchten. Prüfen Sie, ob am Gerät eine Fehlermeldung angezeigt wird und ob Drucken weiterhin funktioniert. Senden Sie uns Modell, Standort und den genauen Fehlertext; ändern Sie keine hinterlegten Netzwerk- oder Scan-Ziele eigenständig.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Brother Scan", + "Scanner", + "Scan to PC", + "MFC", + "DCP", + "Scannen nicht möglich" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-bro-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-BRW-001.json b/knowledge/KB-BRW-001.json new file mode 100644 index 0000000..4148d88 --- /dev/null +++ b/knowledge/KB-BRW-001.json @@ -0,0 +1,22 @@ +{ + "id": "KB-BRW-001", + "title": "Browser zeigt Zertifikats- oder Sicherheitswarnung", + "text": "Chrome, Edge oder Firefox warnt vor einem ungültigen, abgelaufenen oder nicht vertrauenswürdigen Zertifikat. Bei internen Unternehmensdiensten kann dies auf Zertifikatskette, Proxy, Gerätezeit oder eine zentrale Störung hinweisen. Solche Warnungen sollten nicht durch „trotzdem fortfahren“ umgangen werden, insbesondere bei Anmelde- oder Verwaltungsseiten.", + "answer": "Bitte brechen Sie den Aufruf bei einer Zertifikats- oder Sicherheitswarnung ab und umgehen Sie die Warnung nicht. Senden Sie uns die vollständige Adresse der Webseite sowie den genauen Fehlertext oder Fehlercode. Prüfen Sie außerdem, ob Datum und Uhrzeit Ihres Rechners korrekt sind.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Zertifikat", + "NET::ERR_CERT", + "unsichere Verbindung", + "Sicherheitswarnung", + "Certificate", + "SSL", + "TLS" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-brw-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-CHR-001.json b/knowledge/KB-CHR-001.json new file mode 100644 index 0000000..04d4dab --- /dev/null +++ b/knowledge/KB-CHR-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-CHR-001", + "title": "Google Chrome: einzelne Webseite lädt nicht korrekt", + "text": "Eine Webseite funktioniert in Google Chrome nicht, lädt unvollständig, zeigt alte Inhalte oder verhält sich nur in diesem Browser fehlerhaft. Sinnvolle Ersttests sind Neuladen, privates Fenster und die Prüfung, ob Erweiterungen oder gespeicherte Websitedaten beteiligt sind. Unternehmensrichtlinien und verwaltete Erweiterungen dürfen nicht entfernt werden.", + "answer": "Bitte laden Sie die Seite vollständig neu und testen Sie sie anschließend in einem Inkognito-Fenster. Funktioniert sie dort, teilen Sie uns bitte die betroffene Adresse mit; dann prüfen wir gezielt Websitedaten oder Erweiterungen. Entfernen oder deaktivieren Sie keine von der Organisation verwalteten Browser-Erweiterungen.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Chrome", + "Webseite lädt nicht", + "Cache", + "Cookies", + "Inkognito", + "Browserfehler" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-chr-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-CHR-002.json b/knowledge/KB-CHR-002.json new file mode 100644 index 0000000..0e06921 --- /dev/null +++ b/knowledge/KB-CHR-002.json @@ -0,0 +1,21 @@ +{ + "id": "KB-CHR-002", + "title": "Google Chrome: SSO oder Anmeldung an interner Webseite funktioniert nicht", + "text": "Eine interne oder cloudbasierte Webseite meldet in Chrome Anmeldefehler, dreht in einer Login-Schleife oder übernimmt SSO nicht. Zu prüfen sind anderes Browserprofil/Inkognito, funktionierende Anmeldung in einem anderen Browser, Cookies und ob Unternehmensrichtlinien greifen. Das Firmenkonto selbst ist getrennt von einem Browserprofil zu bewerten.", + "answer": "Bitte testen Sie die betroffene Seite einmal in einem Inkognito-Fenster und – sofern möglich – in Microsoft Edge. Teilen Sie uns mit, ob die Anmeldung dort funktioniert und senden Sie die vollständige Adresse sowie die genaue Meldung. Löschen Sie nicht pauschal alle gespeicherten Passwörter oder Browserprofile.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Chrome SSO", + "Chrome Login", + "Anmeldeschleife", + "Cookies", + "Single Sign-on", + "Browserprofil" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-chr-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-CHR-003.json b/knowledge/KB-CHR-003.json new file mode 100644 index 0000000..9a07eb6 --- /dev/null +++ b/knowledge/KB-CHR-003.json @@ -0,0 +1,20 @@ +{ + "id": "KB-CHR-003", + "title": "Google Chrome: Erweiterung verursacht möglicherweise Fehler", + "text": "Eine Webseite oder Browserfunktion verhält sich nach Installation oder Aktualisierung einer Erweiterung ungewöhnlich. Chrome-Erweiterungen können Webseiten verändern, Anfragen blockieren oder zusätzliche Authentifizierungsflüsse beeinflussen. Auf verwalteten Firmenrechnern können Erweiterungen zentral vorgegeben sein.", + "answer": "Bitte testen Sie die betroffene Webseite zunächst in einem Inkognito-Fenster, sofern Ihre Unternehmensrichtlinien dies erlauben. Teilen Sie uns mit, welche Erweiterungen zuletzt neu installiert oder aktualisiert wurden. Entfernen Sie keine von der Organisation verwalteten Erweiterungen eigenständig.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Chrome Erweiterung", + "Extension", + "Plugin", + "Browser Erweiterung", + "Inkognito" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-chr-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EDGE-001.json b/knowledge/KB-EDGE-001.json new file mode 100644 index 0000000..0726e25 --- /dev/null +++ b/knowledge/KB-EDGE-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-EDGE-001", + "title": "Microsoft Edge: Webseite lädt nicht oder Browser reagiert nicht", + "text": "Microsoft Edge lädt eine Seite nicht, zeigt Darstellungsfehler, reagiert langsam oder stürzt ab. Typische Erstmaßnahmen sind Browser neu starten, unnötige Tabs schließen, InPrivate-Vergleich und Cache-/Websitedaten gezielt prüfen. Netzwerk- oder Dienststörungen müssen von einem lokalen Browserproblem unterschieden werden.", + "answer": "Bitte schließen Sie alle Edge-Fenster und öffnen Sie den Browser erneut. Testen Sie die betroffene Seite anschließend in einem InPrivate-Fenster. Falls sie dort funktioniert, senden Sie uns bitte die Adresse der Seite; wir prüfen dann gezielt Browserdaten oder Erweiterungen.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Edge", + "Webseite lädt nicht", + "Edge hängt", + "InPrivate", + "Browserfehler", + "Cache" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-edge-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EDGE-002.json b/knowledge/KB-EDGE-002.json new file mode 100644 index 0000000..b288c67 --- /dev/null +++ b/knowledge/KB-EDGE-002.json @@ -0,0 +1,20 @@ +{ + "id": "KB-EDGE-002", + "title": "Microsoft Edge: SSO oder Firmenanmeldung funktioniert nicht", + "text": "Eine interne oder Microsoft-365-Seite meldet in Edge wiederholt eine Anmeldung, einen falschen Benutzer oder eine Login-Schleife. Zu unterscheiden sind Windows-/Entra-Konto, Edge-Profil, Cookies und Dienstberechtigung. Auf verwalteten Geräten kann die Browseranmeldung durch Unternehmensrichtlinien gesteuert sein.", + "answer": "Bitte prüfen Sie, ob oben im Edge-Profil das erwartete Firmenkonto angezeigt wird. Testen Sie die betroffene Webseite anschließend in einem InPrivate-Fenster. Teilen Sie uns bitte die vollständige Adresse und die genaue Anmeldemeldung mit, falls das Problem bestehen bleibt.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Edge SSO", + "Edge Anmeldung", + "Edge Firmenkonto", + "InPrivate", + "Login Schleife" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-edge-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EDGE-003.json b/knowledge/KB-EDGE-003.json new file mode 100644 index 0000000..323fe49 --- /dev/null +++ b/knowledge/KB-EDGE-003.json @@ -0,0 +1,20 @@ +{ + "id": "KB-EDGE-003", + "title": "Microsoft Edge: Erweiterung oder Richtlinie beeinflusst Webseite", + "text": "Eine Webseite verhält sich in Edge anders als in anderen Browsern oder bestimmte Funktionen sind blockiert. Ursache können Erweiterungen, Tracking-/Sicherheitsregeln oder zentrale Unternehmensrichtlinien sein. Verwaltete Einstellungen dürfen nicht umgangen oder entfernt werden.", + "answer": "Bitte testen Sie die Webseite in einem InPrivate-Fenster und vergleichen Sie sie, sofern zulässig, mit einem anderen freigegebenen Browser. Teilen Sie uns die betroffene Funktion und die vollständige Adresse mit. Deaktivieren Sie keine zentral verwalteten Sicherheits- oder Browsererweiterungen.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Edge Erweiterung", + "Edge Policy", + "Richtlinie", + "Extension", + "Webseite blockiert" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-edge-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EXC-001.json b/knowledge/KB-EXC-001.json new file mode 100644 index 0000000..109d9c4 --- /dev/null +++ b/knowledge/KB-EXC-001.json @@ -0,0 +1,24 @@ +{ + "id": "KB-EXC-001", + "title": "Outlook 2024 ist getrennt oder empfängt keine neuen E-Mails", + "text": "Outlook 2024 zeigt „Getrennt“, „Verbindungsversuch“, „Offline arbeiten“ oder empfängt keine aktuellen Nachrichten. In einer Exchange-On-Premises-Umgebung sind für die Erstdiagnose insbesondere Netzwerkverbindung, Erreichbarkeit des Exchange-Dienstes, Outlook im Web/OWA (sofern bereitgestellt), Offline-Modus und das lokale Outlook-Profil relevant. Funktioniert OWA, während Outlook nicht verbindet, spricht dies eher für ein lokales Outlook-, Profil- oder Autodiscover-Problem.", + "answer": "Bitte prüfen Sie zunächst Ihre Netzwerkverbindung und stellen Sie sicher, dass Outlook nicht auf „Offline arbeiten“ steht. Öffnen Sie Ihr Postfach anschließend testweise über Outlook im Web/OWA, sofern dies bei Ihnen bereitgestellt wird. Funktioniert das Postfach dort, schließen Sie Outlook vollständig und starten Sie es erneut. Falls Outlook weiterhin keine Verbindung aufbaut, teilen Sie uns bitte die angezeigte Statusmeldung mit.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Outlook 2024", + "Outlook getrennt", + "keine E-Mail", + "Verbindungsversuch", + "Offline arbeiten", + "Exchange", + "Exchange Server", + "OWA", + "Outlook verbindet nicht" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-exc-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EXC-002.json b/knowledge/KB-EXC-002.json new file mode 100644 index 0000000..3572c85 --- /dev/null +++ b/knowledge/KB-EXC-002.json @@ -0,0 +1,22 @@ +{ + "id": "KB-EXC-002", + "title": "Outlook 2024 startet nicht oder schließt sofort", + "text": "Outlook 2024 lässt sich nicht öffnen, bleibt beim Start hängen oder schließt unmittelbar wieder. Mögliche Ursachen sind ein lokales Outlook-Profil, Add-ins, ein beschädigter Office-Zustand oder ein problematisches Datendatei-/Cacheverhalten. Eine Gegenprobe über OWA hilft zu unterscheiden, ob das Exchange-Postfach grundsätzlich erreichbar ist.", + "answer": "Bitte starten Sie den Rechner einmal neu und versuchen Sie Outlook anschließend erneut zu öffnen. Prüfen Sie zusätzlich, ob Ihr Postfach über Outlook im Web/OWA erreichbar ist, sofern dieser Zugang bereitgestellt wird. Teilen Sie uns bitte mit, ob Outlook eine Fehlermeldung anzeigt oder an welcher Stelle der Startvorgang hängen bleibt.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Outlook startet nicht", + "Outlook schließt", + "Outlook hängt", + "Outlook 2024", + "Exchange", + "OWA", + "Outlook Profil" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-exc-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EXC-003.json b/knowledge/KB-EXC-003.json new file mode 100644 index 0000000..3be9d19 --- /dev/null +++ b/knowledge/KB-EXC-003.json @@ -0,0 +1,22 @@ +{ + "id": "KB-EXC-003", + "title": "Outlook 2024 Suche findet E-Mails nicht", + "text": "Die Outlook-Suche liefert keine, unvollständige oder offensichtlich veraltete Ergebnisse. Zu unterscheiden sind lokale Outlook-Suche, der lokal verfügbare Cache, Windows-Suche und die Suche über Outlook im Web/OWA. Nach Profiländerungen oder Indexierungsproblemen können lokale Suchergebnisse zeitweise unvollständig sein.", + "answer": "Bitte prüfen Sie, ob Sie die gesuchte Nachricht über Outlook im Web/OWA finden können, sofern dieser Zugang bereitgestellt wird. Falls sie dort vorhanden ist, schließen Sie Outlook und starten Sie den Rechner einmal neu. Teilen Sie uns bitte ein Beispiel mit Betreff und ungefährem Datum einer Nachricht mit, die in der Outlook-Suche fehlt.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Outlook Suche", + "E-Mail nicht gefunden", + "Suche unvollständig", + "Index", + "Outlook 2024", + "OWA", + "Exchange Suche" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-exc-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EXC-004.json b/knowledge/KB-EXC-004.json new file mode 100644 index 0000000..4b20db6 --- /dev/null +++ b/knowledge/KB-EXC-004.json @@ -0,0 +1,22 @@ +{ + "id": "KB-EXC-004", + "title": "Freigegebenes Postfach oder zusätzlicher Kalender fehlt in Outlook 2024", + "text": "Ein freigegebenes Postfach, Funktionspostfach oder zusätzlicher Kalender ist in Outlook 2024 nicht sichtbar oder kann nicht geöffnet werden. Ursache kann eine fehlende oder noch nicht wirksame Exchange-Berechtigung, Automapping, eine falsche Einbindung oder ein lokaler Outlook-Cache sein. Berechtigungen dürfen nicht automatisch vergeben oder verändert werden.", + "answer": "Bitte nennen Sie uns den vollständigen Namen beziehungsweise die E-Mail-Adresse des fehlenden Postfachs oder Kalenders und welche Aktion Sie dort ausführen müssen. Prüfen Sie außerdem, ob der Zugriff über Outlook im Web/OWA bereits möglich ist, sofern dieser Zugang bereitgestellt wird. Wir prüfen anschließend die vorhandenen Exchange-Berechtigungen und die Einbindung in Outlook.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "freigegebenes Postfach", + "Shared Mailbox", + "Funktionspostfach", + "Kalender fehlt", + "Exchange Berechtigung", + "Automapping", + "Outlook 2024" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-exc-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EXC-005.json b/knowledge/KB-EXC-005.json new file mode 100644 index 0000000..1e0f01f --- /dev/null +++ b/knowledge/KB-EXC-005.json @@ -0,0 +1,21 @@ +{ + "id": "KB-EXC-005", + "title": "Outlook 2024 fragt wiederholt nach Benutzername oder Kennwort", + "text": "Outlook 2024 fordert wiederholt Benutzername oder Kennwort an, obwohl die Anmeldung zuvor funktioniert hat. In Exchange-On-Premises-Umgebungen können Konto-/Kennwortstatus, gespeicherte Anmeldeinformationen, Netzwerk, Autodiscover oder die Exchange-Authentifizierung beteiligt sein. Kennwörter oder gespeicherte Unternehmensanmeldedaten sollten nicht automatisiert verändert oder gelöscht werden.", + "answer": "Bitte brechen Sie wiederholte Kennwortabfragen ab und teilen Sie uns mit, ob die Anmeldung mit Ihrem Firmenkonto an anderen Unternehmensdiensten funktioniert. Prüfen Sie außerdem, ob Outlook im Web/OWA erreichbar ist, sofern bereitgestellt. Senden Sie uns bitte den genauen Wortlaut der Abfrage beziehungsweise eine Fehlermeldung. Ändern oder löschen Sie gespeicherte Zugangsdaten bitte nicht eigenständig.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Outlook Kennwort", + "Passwortabfrage", + "Anmeldeinformationen", + "Exchange Authentifizierung", + "Outlook 2024", + "Kennwort immer wieder" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-exc-005", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EXC-006.json b/knowledge/KB-EXC-006.json new file mode 100644 index 0000000..2fae12e --- /dev/null +++ b/knowledge/KB-EXC-006.json @@ -0,0 +1,21 @@ +{ + "id": "KB-EXC-006", + "title": "Outlook-Profil oder Exchange-Autodiscover funktioniert nicht", + "text": "Outlook 2024 kann ein Exchange-Postfach nicht automatisch einrichten, findet den Server nicht oder meldet bei der Profilerstellung ein Verbindungsproblem. Exchange Autodiscover liefert Outlook die für das Postfach benötigten Verbindungseinstellungen. Ursachen können DNS, Autodiscover-Konfiguration, Erreichbarkeit oder das Benutzer-/Postfachobjekt sein. Änderungen an DNS, Registry oder Profilen sind administrative Schritte und werden nicht automatisch durchgeführt.", + "answer": "Bitte teilen Sie uns die genaue Fehlermeldung beim Einrichten beziehungsweise Starten von Outlook mit und geben Sie an, ob Outlook im Web/OWA funktioniert. Falls bereits ein Outlook-Profil vorhanden ist, löschen oder verändern Sie dieses bitte nicht eigenständig. Wir prüfen anschließend Autodiscover, Exchange-Erreichbarkeit und das Benutzer-/Postfachobjekt.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Autodiscover", + "Outlook Profil", + "Profil einrichten", + "Exchange Server nicht gefunden", + "Konto einrichten", + "Outlook 2024" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-exc-006", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EXC-007.json b/knowledge/KB-EXC-007.json new file mode 100644 index 0000000..72e31a3 --- /dev/null +++ b/knowledge/KB-EXC-007.json @@ -0,0 +1,22 @@ +{ + "id": "KB-EXC-007", + "title": "Outlook kann E-Mails empfangen, aber nicht senden", + "text": "Outlook empfängt Nachrichten, aber das Senden schlägt fehl, Nachrichten bleiben im Postausgang oder es kommt eine Unzustellbarkeitsmeldung/NDR zurück. Für die Erstdiagnose sind Empfängeradresse, Größe/Anhang, genauer NDR-Text und die Gegenprobe über OWA hilfreich. Transportregeln, Größenlimits und Exchange-Connectoren sind administrative Ursachen und werden nicht automatisch verändert.", + "answer": "Bitte prüfen Sie, ob nur eine bestimmte Nachricht oder jeder Versand betroffen ist. Entfernen Sie testweise große Anhänge und prüfen Sie die Empfängeradresse. Falls eine Unzustellbarkeitsmeldung zurückkommt, senden Sie uns bitte deren Fehlercode und Text. Sofern Outlook im Web/OWA verfügbar ist, können Sie zusätzlich prüfen, ob der Versand dort funktioniert.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Outlook sendet nicht", + "Postausgang", + "Unzustellbar", + "NDR", + "Mailversand", + "Exchange Transport", + "Outlook 2024" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-exc-007", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-EXC-008.json b/knowledge/KB-EXC-008.json new file mode 100644 index 0000000..4dfac96 --- /dev/null +++ b/knowledge/KB-EXC-008.json @@ -0,0 +1,21 @@ +{ + "id": "KB-EXC-008", + "title": "Outlook im Web funktioniert, Outlook 2024 auf dem PC jedoch nicht", + "text": "Wenn Outlook im Web/OWA funktioniert, Outlook 2024 auf demselben Benutzerkonto jedoch nicht, ist der Exchange-Dienst für das Postfach grundsätzlich erreichbar. Der Fehler liegt dann mit höherer Wahrscheinlichkeit im lokalen Outlook-Client, Profil, Cache, Add-in oder in der lokalen Verbindung/Autodiscover-Auflösung.", + "answer": "Da Ihr Postfach über Outlook im Web/OWA funktioniert, starten Sie bitte zunächst den Rechner neu und öffnen Sie Outlook anschließend erneut. Prüfen Sie, ob die Statuszeile weiterhin „Getrennt“, „Verbindungsversuch“ oder eine andere Meldung zeigt. Senden Sie uns bitte diese Statusmeldung und gegebenenfalls den Wortlaut einer Fehlermeldung. Nehmen Sie bitte noch keine Änderungen am Outlook-Profil vor.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "OWA funktioniert", + "Outlook funktioniert nicht", + "Outlook lokal", + "Outlook Profil", + "Outlook 2024", + "Exchange erreichbar" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-exc-008", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-FF-001.json b/knowledge/KB-FF-001.json new file mode 100644 index 0000000..26bf6db --- /dev/null +++ b/knowledge/KB-FF-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-FF-001", + "title": "Mozilla Firefox: Webseite lädt nicht korrekt", + "text": "Eine Webseite lädt in Firefox nicht, zeigt alte Inhalte oder funktioniert nur in diesem Browser nicht. Mozilla empfiehlt für typische Firefox-Probleme unter anderem Neustart, Cookies/Cache und den Problembehandlungsmodus, um Add-on-Einflüsse einzugrenzen. Auf Firmenrechnern müssen Unternehmensrichtlinien berücksichtigt werden.", + "answer": "Bitte starten Sie Firefox zunächst neu und testen Sie die betroffene Webseite erneut. Falls das Problem bestehen bleibt, testen Sie – sofern auf Ihrem Rechner verfügbar – den Firefox-Problembehandlungsmodus. Teilen Sie uns anschließend mit, ob die Seite dort korrekt funktioniert und welche Adresse betroffen ist.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Firefox", + "Webseite lädt nicht", + "Problembehandlungsmodus", + "Troubleshoot Mode", + "Cache", + "Cookies" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ff-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-FF-002.json b/knowledge/KB-FF-002.json new file mode 100644 index 0000000..a01a9cf --- /dev/null +++ b/knowledge/KB-FF-002.json @@ -0,0 +1,20 @@ +{ + "id": "KB-FF-002", + "title": "Mozilla Firefox: Add-on oder Profil verursacht möglicherweise Fehler", + "text": "Firefox zeigt Abstürze, Darstellungsfehler oder Probleme, die in anderen Browsern nicht auftreten. Der Problembehandlungsmodus deaktiviert vorübergehend Erweiterungen und bestimmte Anpassungen und ist daher ein guter Vergleichstest. Ein neues Profil oder eine Firefox-Bereinigung sollte erst nach Sicherung und IT-Abstimmung erfolgen.", + "answer": "Bitte starten Sie Firefox im Problembehandlungsmodus und prüfen Sie die betroffene Funktion erneut. Wenn das Problem dort nicht auftritt, teilen Sie uns dies bitte mit. Erstellen oder löschen Sie vorerst kein Firefox-Profil und entfernen Sie keine zentral verwalteten Erweiterungen.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Firefox Add-on", + "Firefox Profil", + "Troubleshoot Mode", + "Problembehandlungsmodus", + "Firefox Absturz" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ff-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-FILE-001.json b/knowledge/KB-FILE-001.json new file mode 100644 index 0000000..dd2626f --- /dev/null +++ b/knowledge/KB-FILE-001.json @@ -0,0 +1,20 @@ +{ + "id": "KB-FILE-001", + "title": "Datei oder Ordner: Zugriff verweigert", + "text": "Beim Öffnen eines Ordners oder einer Datei erscheint „Zugriff verweigert“, „Keine Berechtigung“ oder eine vergleichbare Meldung. Der Agent sollte zwischen fehlender Netzwerkverbindung und echter Zugriffsberechtigung unterscheiden. Berechtigungen dürfen nicht automatisch erweitert werden; erforderlich sind Pfad, benötigte Aufgabe und gegebenenfalls Freigabe durch Datenverantwortliche.", + "answer": "Bitte senden Sie uns den vollständigen Pfad beziehungsweise Link zur Datei oder zum Ordner und die genaue Fehlermeldung. Teilen Sie außerdem kurz mit, welche Tätigkeit Sie dort durchführen müssen. Ändern Sie keine Datei- oder Ordnerberechtigungen eigenständig; wir prüfen die vorhandene Freigabe.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Zugriff verweigert", + "keine Berechtigung", + "Access denied", + "Ordner Berechtigung", + "Datei Berechtigung" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-file-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-HW-001.json b/knowledge/KB-HW-001.json new file mode 100644 index 0000000..f646231 --- /dev/null +++ b/knowledge/KB-HW-001.json @@ -0,0 +1,22 @@ +{ + "id": "KB-HW-001", + "title": "Dockingstation: externer Monitor oder Netzwerk funktioniert nicht", + "text": "Nach dem Andocken funktionieren externe Monitore, USB-Geräte oder LAN über die Dockingstation nicht. Häufig helfen ein vollständiges Neuverbinden, Stromversorgung prüfen und ein Windows-Neustart. Bei mehreren gleichzeitig ausgefallenen Funktionen ist Dock/USB-C/Thunderbolt-Verbindung wahrscheinlicher als ein einzelner Monitorfehler.", + "answer": "Bitte speichern Sie Ihre Arbeit, trennen Sie das Notebook von der Dockingstation und verbinden Sie es anschließend erneut. Prüfen Sie, ob die Dockingstation mit Strom versorgt ist, und starten Sie den Rechner einmal neu. Teilen Sie uns bitte mit, ob Monitor, Netzwerk und USB-Geräte gleichzeitig betroffen sind.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Dockingstation", + "Dock", + "Monitor", + "USB-C", + "Thunderbolt", + "LAN Dock", + "kein Bild" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-hw-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-HW-002.json b/knowledge/KB-HW-002.json new file mode 100644 index 0000000..a0338d6 --- /dev/null +++ b/knowledge/KB-HW-002.json @@ -0,0 +1,20 @@ +{ + "id": "KB-HW-002", + "title": "Externer Monitor zeigt kein Bild", + "text": "Ein externer Monitor bleibt schwarz, zeigt „Kein Signal“ oder wird in Windows nicht erkannt. Prüfrelevant sind Stromversorgung, Eingang am Monitor, Kabel/Dock, Windows-Anzeigeeinstellungen und ein zweiter Ausgang/Monitor. Bei Dockingstationen kann die Ursache außerhalb des Monitors liegen.", + "answer": "Bitte prüfen Sie, ob der Monitor eingeschaltet ist und der richtige Eingang ausgewählt ist. Kontrollieren Sie die Kabelverbindung beziehungsweise Dockingstation und starten Sie den Rechner einmal neu. Teilen Sie uns bitte mit, ob Windows den Monitor in den Anzeigeeinstellungen erkennt und welche Anschlussart verwendet wird.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Monitor kein Bild", + "Kein Signal", + "Display", + "Bildschirm schwarz", + "externer Monitor" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-hw-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-HW-003.json b/knowledge/KB-HW-003.json new file mode 100644 index 0000000..91c598d --- /dev/null +++ b/knowledge/KB-HW-003.json @@ -0,0 +1,21 @@ +{ + "id": "KB-HW-003", + "title": "USB-Headset wird nicht erkannt oder kein Ton", + "text": "Ein USB- oder Bluetooth-Headset wird nicht erkannt, Ton kommt über das falsche Gerät oder das Mikrofon funktioniert nicht. Ursachen können falsches Standardgerät, USB/Dock, Bluetooth-Verbindung oder Anwendungseinstellungen sein. Vergleichstests in den Windows-Audioeinstellungen und einer zweiten Anwendung helfen bei der Eingrenzung.", + "answer": "Bitte trennen Sie das Headset und verbinden Sie es erneut. Prüfen Sie in Windows sowie in der betroffenen Anwendung, ob das Headset als Ein- und Ausgabegerät ausgewählt ist. Testen Sie nach Möglichkeit einen anderen USB-Anschluss und teilen Sie uns Modell und betroffenes Programm mit.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Headset", + "kein Ton", + "Mikrofon", + "USB Audio", + "Bluetooth Headset", + "Audio" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-hw-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-HW-004.json b/knowledge/KB-HW-004.json new file mode 100644 index 0000000..ef27f17 --- /dev/null +++ b/knowledge/KB-HW-004.json @@ -0,0 +1,21 @@ +{ + "id": "KB-HW-004", + "title": "Webcam funktioniert nicht", + "text": "Die interne oder externe Kamera liefert kein Bild, wird nicht erkannt oder ist in einer Anwendung beziehungsweise im Browser blockiert. Zu prüfen sind physischer Kameraschieber, Windows-Datenschutz, Anwendungsauswahl, USB/Dock und ob eine andere Anwendung die Kamera bereits verwendet.", + "answer": "Bitte prüfen Sie zunächst einen vorhandenen Kameraschieber beziehungsweise eine Abdeckung. Schließen Sie andere Anwendungen, die möglicherweise auf die Kamera zugreifen, und wählen Sie in der betroffenen Anwendung die richtige Kamera aus. Bei einer USB-Kamera trennen Sie sie kurz und verbinden Sie sie erneut.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Webcam", + "Kamera", + "kein Bild", + "Kamera nicht erkannt", + "Windows Kamera", + "Browser Kamera" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-hw-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-INF-001.json b/knowledge/KB-INF-001.json new file mode 100644 index 0000000..fbf3bb1 --- /dev/null +++ b/knowledge/KB-INF-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-INF-001", + "title": "Infoma newsystem: Anmeldung funktioniert nicht", + "text": "Infoma newsystem akzeptiert die Anmeldung nicht, meldet einen unbekannten Benutzer, fehlende Berechtigung oder öffnet die Anwendung nach der Anmeldung nicht. Je nach Betriebsmodell können Benutzerkonto, SSO, Rollenprofil, Client/Browser und zentrale Infoma-Dienste beteiligt sein. Die öffentliche Infoma-Dokumentation weist auf benutzerspezifische Profile und rollenbasierte Administration hin; konkrete Rechte müssen organisationsspezifisch geprüft werden.", + "answer": "Bitte senden Sie uns die genaue Anmeldemeldung und teilen Sie mit, ob Infoma newsystem gestern beziehungsweise zuvor mit demselben Konto funktioniert hat. Prüfen Sie bitte außerdem, ob Ihre normale Firmenanmeldung an anderen Diensten funktioniert. Wir prüfen anschließend Benutzerzuordnung, Profil und Berechtigung in Infoma.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Infoma", + "newsystem", + "Infoma Anmeldung", + "unbekannter Benutzer", + "Login", + "Berechtigung" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-inf-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-INF-002.json b/knowledge/KB-INF-002.json new file mode 100644 index 0000000..a812611 --- /dev/null +++ b/knowledge/KB-INF-002.json @@ -0,0 +1,22 @@ +{ + "id": "KB-INF-002", + "title": "Infoma newsystem: Funktion oder Menüpunkt fehlt", + "text": "Ein Menüpunkt, Mandant, Modul, Arbeitsbereich oder eine Funktion ist in Infoma newsystem nicht sichtbar, obwohl sie erwartet wird. Häufig ist dies profil-, rollen- oder berechtigungsabhängig. Da Infoma benutzerspezifische Oberflächen und Rollen unterstützt, ist ein Vergleich mit einem Kollegen nur aussagekräftig, wenn beide dieselbe Rolle und Aufgabe besitzen.", + "answer": "Bitte nennen Sie uns den fehlenden Menüpunkt beziehungsweise die benötigte Funktion sowie – wenn möglich – einen Benutzer mit vergleichbarer Aufgabe, bei dem die Funktion vorhanden ist. Ändern Sie keine Profile oder Berechtigungen selbst. Wir prüfen die zugewiesene Rolle und das Benutzerprofil.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Infoma", + "newsystem", + "Menü fehlt", + "Funktion fehlt", + "Berechtigung", + "Profil", + "Rolle" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-inf-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-INF-003.json b/knowledge/KB-INF-003.json new file mode 100644 index 0000000..95812e5 --- /dev/null +++ b/knowledge/KB-INF-003.json @@ -0,0 +1,20 @@ +{ + "id": "KB-INF-003", + "title": "Infoma newsystem: Anwendung ist ungewöhnlich langsam", + "text": "Infoma newsystem reagiert langsam, Masken öffnen verzögert oder Abfragen dauern ungewöhnlich lange. Zu unterscheiden sind lokaler Arbeitsplatz, Netzwerk/VPN, einzelne Infoma-Funktion, mehrere Benutzer und zentrale Backend-/Datenbankprobleme. Zeitpunkt und betroffene Maske sind für die Eingrenzung wichtig.", + "answer": "Bitte teilen Sie uns mit, seit wann die Verlangsamung auftritt, welche Infoma-Maske oder Funktion betroffen ist und ob Kolleginnen oder Kollegen das gleiche Verhalten beobachten. Falls Sie im Homeoffice arbeiten, geben Sie bitte auch an, ob eine VPN-Verbindung verwendet wird. Starten Sie die Anwendung einmal neu und prüfen Sie den Vorgang erneut.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Infoma langsam", + "newsystem langsam", + "Performance", + "Maske langsam", + "Abfrage dauert" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-inf-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-INF-004.json b/knowledge/KB-INF-004.json new file mode 100644 index 0000000..799e17a --- /dev/null +++ b/knowledge/KB-INF-004.json @@ -0,0 +1,20 @@ +{ + "id": "KB-INF-004", + "title": "Infoma newsystem: Drucken, PDF oder Export funktioniert nicht", + "text": "Ein Bericht, Bescheid, PDF-Export oder Ausdruck aus Infoma newsystem wird nicht erzeugt, öffnet nicht oder landet nicht am erwarteten Drucker. Infoma wird in kommunalen Prozessen auch für PDF-/Druckausgaben eingesetzt; die konkrete Fehlerursache kann Anwendung, Berechtigung, Drucker, PDF-Viewer oder Ausgabeprozess betreffen. Dokumentinhalte und personenbezogene Daten müssen geschützt bleiben.", + "answer": "Bitte nennen Sie den betroffenen Bericht beziehungsweise Vorgang und beschreiben Sie, ob bereits die Vorschau/PDF-Erzeugung oder erst der Ausdruck fehlschlägt. Senden Sie uns die genaue Fehlermeldung, aber keine vertraulichen Dokumentinhalte. Prüfen Sie testweise, ob ein anderes unkritisches Dokument auf demselben Drucker ausgegeben werden kann.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Infoma Drucken", + "Infoma PDF", + "newsystem Export", + "Bericht drucken", + "Bescheid PDF" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-inf-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-INF-005.json b/knowledge/KB-INF-005.json new file mode 100644 index 0000000..e60b747 --- /dev/null +++ b/knowledge/KB-INF-005.json @@ -0,0 +1,20 @@ +{ + "id": "KB-INF-005", + "title": "Infoma newsystem: persönliche Ansicht oder Oberfläche verändert", + "text": "Spalten, Filter, Startseiten, Ansichten oder persönliche Oberflächeneinstellungen in Infoma newsystem sind verändert oder fehlen. Infoma unterstützt benutzerspezifische Anpassungen und Profile. Bevor Profile zurückgesetzt werden, sollte geklärt werden, ob nur ein Benutzer oder eine ganze Rolle betroffen ist.", + "answer": "Bitte beschreiben Sie, welche Ansicht, Spalte oder Einstellung fehlt beziehungsweise verändert ist. Wenn möglich, nennen Sie einen vergleichbaren Arbeitsplatz, bei dem die gewünschte Ansicht noch vorhanden ist. Wir prüfen anschließend, ob eine persönliche Einstellung oder ein zentrales Profil betroffen ist.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Infoma Oberfläche", + "newsystem Ansicht", + "Spalten fehlen", + "Profil", + "persönliche Einstellungen" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-inf-005", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-M42-001.json b/knowledge/KB-M42-001.json new file mode 100644 index 0000000..44d712f --- /dev/null +++ b/knowledge/KB-M42-001.json @@ -0,0 +1,22 @@ +{ + "id": "KB-M42-001", + "title": "Matrix42 UEM: zugewiesene Software wird nicht installiert", + "text": "Ein über Matrix42 UEM/Empirum zugewiesenes Softwarepaket erscheint nicht oder wird nicht installiert. Relevante Prüfpunkte sind Erreichbarkeit des Geräts, UEM-Agentstatus, ausstehender Neustart, Paketzuweisung, Downloadstatus und Fehlerlogs. Matrix42 dokumentiert für den UEM Agent eigene Diagnose- und Troubleshooting-Möglichkeiten; tiefere Eingriffe gehören in die Administration.", + "answer": "Bitte starten Sie den Rechner einmal neu und lassen Sie ihn anschließend für einige Minuten mit dem Unternehmensnetz beziehungsweise VPN verbunden. Öffnen Sie danach den vorgesehenen Software-Kiosk erneut. Falls die Software weiterhin fehlt oder die Installation fehlschlägt, senden Sie uns den Softwarenamen, den Zeitpunkt des letzten Versuchs und eine angezeigte Fehlermeldung.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Matrix42", + "Empirum", + "UEM Agent", + "Software Installation", + "Paket", + "Softwareverteilung", + "Kiosk" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-m42-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-M42-002.json b/knowledge/KB-M42-002.json new file mode 100644 index 0000000..904499d --- /dev/null +++ b/knowledge/KB-M42-002.json @@ -0,0 +1,20 @@ +{ + "id": "KB-M42-002", + "title": "Matrix42 Software-Kiosk oder Installationsdialog öffnet nicht", + "text": "Der Matrix42 Software-Kiosk oder Installationsdialog wird nicht angezeigt, bleibt leer oder fordert unerwartet Anmeldedaten. Matrix42 beschreibt den Kiosk als lokal ausgeführte Weboberfläche; lokale Browser-/Proxy-/Agentbedingungen können die Darstellung beeinflussen. Änderungen an Agent-, Proxy- oder Registry-Einstellungen sollten durch die Administration erfolgen.", + "answer": "Bitte schließen Sie den Software-Kiosk vollständig, starten Sie den Rechner neu und öffnen Sie den Kiosk anschließend erneut. Falls die Oberfläche leer bleibt oder Anmeldedaten verlangt, senden Sie uns bitte einen Screenshot und den Zeitpunkt des Fehlers. Nehmen Sie keine Änderungen an Proxy-, Registry- oder Agent-Einstellungen vor.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Matrix42 Software Kiosk", + "Empirum Kiosk", + "Installationsdialog", + "Kiosk leer", + "UEM Agent" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-m42-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-M42-003.json b/knowledge/KB-M42-003.json new file mode 100644 index 0000000..ccd5395 --- /dev/null +++ b/knowledge/KB-M42-003.json @@ -0,0 +1,21 @@ +{ + "id": "KB-M42-003", + "title": "Matrix42 Inventardaten eines Geräts sind veraltet", + "text": "Ein Gerät wird in Matrix42/Empirum mit veralteten Inventardaten, altem Last-Seen-Zeitpunkt oder fehlenden Softwareinformationen angezeigt. Ursachen können ein nicht aktiver Agent, fehlende Netzwerkverbindung, ein lange ausgeschaltetes Gerät oder eine Störung der Inventarübertragung sein. Der Benutzer sollte keine Agentdateien oder lokalen Inventardaten manuell löschen.", + "answer": "Bitte verbinden Sie das Gerät mit dem Unternehmensnetz beziehungsweise VPN und lassen Sie es für einige Minuten eingeschaltet. Starten Sie den Rechner einmal neu, falls dies seit längerer Zeit nicht erfolgt ist. Teilen Sie uns anschließend Gerätename und Zeitpunkt mit; wir prüfen den Matrix42-Agentstatus und die Inventarübertragung.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Matrix42 Inventar", + "Empirum Inventar", + "Last Seen", + "Inventardaten", + "UEM Agent", + "Gerät veraltet" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-m42-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-M42-004.json b/knowledge/KB-M42-004.json new file mode 100644 index 0000000..028e154 --- /dev/null +++ b/knowledge/KB-M42-004.json @@ -0,0 +1,20 @@ +{ + "id": "KB-M42-004", + "title": "Matrix42 Paketdownload oder Softwareinstallation bricht ab", + "text": "Ein Matrix42-UEM-Paket bleibt beim Download stehen oder bricht während der Installation ab. Matrix42 sieht Wiederholungsmechanismen für temporäre Downloadprobleme vor; mögliche Ursachen sind Netzwerk, Depot-Erreichbarkeit, Paketfehler, fehlender Speicherplatz oder ein ausstehender Neustart. Eine manuelle Manipulation des Agent-Caches sollte im First Level vermieden werden.", + "answer": "Bitte prüfen Sie Ihre Netzwerk- beziehungsweise VPN-Verbindung und starten Sie den Rechner einmal neu. Versuchen Sie die Installation anschließend erneut und notieren Sie die genaue Fehlermeldung. Falls der Fehler wiederholt auftritt, senden Sie uns bitte den Softwarenamen und den Zeitpunkt des letzten Installationsversuchs.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Matrix42 Paketdownload", + "Empirum Paket", + "Download fehlgeschlagen", + "Installation fehlgeschlagen", + "Softwareverteilung" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-m42-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-M42-005.json b/knowledge/KB-M42-005.json new file mode 100644 index 0000000..e39dde8 --- /dev/null +++ b/knowledge/KB-M42-005.json @@ -0,0 +1,20 @@ +{ + "id": "KB-M42-005", + "title": "Matrix42 meldet ausstehenden Neustart vor Softwareinstallation", + "text": "Eine Softwareinstallation wird durch einen notwendigen Windows-Neustart verzögert oder der UEM-Agent wartet auf den Abschluss vorheriger Installationen. Matrix42 kann Installationen und Neustartverhalten abhängig von Agent- und Paketkonfiguration steuern. Ein normaler Benutzerneustart ist ein sicherer erster Schritt.", + "answer": "Bitte speichern Sie Ihre Arbeit und starten Sie den Rechner über „Neu starten“ vollständig neu. Verbinden Sie das Gerät danach mit dem Unternehmensnetz beziehungsweise VPN und prüfen Sie die Softwareinstallation erneut. Falls weiterhin ein Neustart verlangt wird, senden Sie uns bitte einen Screenshot der Meldung.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Matrix42 Neustart", + "Empirum Neustart", + "Reboot required", + "Softwareinstallation wartet", + "UEM Agent" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-m42-005", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-NET-001.json b/knowledge/KB-NET-001.json new file mode 100644 index 0000000..c0c298e --- /dev/null +++ b/knowledge/KB-NET-001.json @@ -0,0 +1,22 @@ +{ + "id": "KB-NET-001", + "title": "Keine Netzwerk- oder Internetverbindung unter Windows", + "text": "Der Arbeitsplatz erreicht weder interne Dienste noch Internetseiten oder meldet „Kein Internet“, „Nicht verbunden“ oder Netzwerkfehler. Prüfrelevant sind Ethernet/WLAN, Dockingstation, andere Geräte am Standort, VPN sowie bekannte zentrale Störungen. Ein lokales Problem ist wahrscheinlicher, wenn nur ein einzelner Arbeitsplatz betroffen ist; mehrere gleichzeitig betroffene Benutzer sprechen eher für eine Infrastrukturstörung.", + "answer": "Bitte prüfen Sie, ob das Netzwerk- oder WLAN-Symbol in Windows eine Verbindung anzeigt. Bei einer Dockingstation kontrollieren Sie bitte auch die physische Netzwerkverbindung. Starten Sie den Rechner einmal neu. Falls weiterhin keine Verbindung besteht, teilen Sie uns bitte mit, ob andere Geräte am gleichen Standort ebenfalls betroffen sind.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "kein Internet", + "Netzwerk", + "LAN", + "Ethernet", + "WLAN", + "nicht verbunden", + "Netzwerkverbindung" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-net-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-NET-002.json b/knowledge/KB-NET-002.json new file mode 100644 index 0000000..49fd7bc --- /dev/null +++ b/knowledge/KB-NET-002.json @@ -0,0 +1,21 @@ +{ + "id": "KB-NET-002", + "title": "WLAN verbunden, aber kein Zugriff auf Dienste", + "text": "Windows zeigt eine WLAN-Verbindung, Webseiten oder interne Dienste sind dennoch nicht erreichbar. Ursachen können ein Gastnetz, fehlende Netzwerkauthentifizierung, DNS-/Gateway-Probleme, eine captive portal Anmeldung oder eine zentrale WLAN-Störung sein. Das Ticket sollte Standort, WLAN-Name und mindestens einen nicht erreichbaren Dienst enthalten.", + "answer": "Bitte trennen Sie die WLAN-Verbindung kurz und verbinden Sie sich erneut mit dem vorgesehenen Unternehmens-WLAN. Prüfen Sie danach eine bekannte interne Anwendung und eine externe Webseite. Falls beides oder nur einer der beiden Tests fehlschlägt, teilen Sie uns bitte den Standort, den Namen des verwendeten WLANs und die nicht erreichbare Seite beziehungsweise Anwendung mit.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "WLAN", + "WiFi", + "verbunden kein Internet", + "kein Zugriff", + "Wireless", + "Funknetz" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-net-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-NET-003.json b/knowledge/KB-NET-003.json new file mode 100644 index 0000000..5508030 --- /dev/null +++ b/knowledge/KB-NET-003.json @@ -0,0 +1,21 @@ +{ + "id": "KB-NET-003", + "title": "VPN verbindet nicht oder trennt die Verbindung", + "text": "Ein VPN-Client stellt keine Verbindung her, bleibt beim Verbindungsaufbau hängen oder trennt wiederholt. Zuerst muss eine funktionierende lokale Internetverbindung bestehen. Relevante Informationen sind VPN-Client, genaue Fehlermeldung, Zeitpunkt, Standort/Homeoffice und ob eine bekannte zentrale VPN-Störung vorliegt. Kennwort- und MFA-Probleme sind von technischen Tunnelproblemen zu unterscheiden.", + "answer": "Bitte prüfen Sie zuerst, ob Internetseiten ohne VPN erreichbar sind. Beenden Sie den VPN-Client vollständig und starten Sie ihn anschließend erneut. Falls die Verbindung weiterhin nicht hergestellt wird, senden Sie uns bitte die genaue Fehlermeldung und den Namen des verwendeten VPN-Clients. Wiederholte Kennwortversuche sollten vermieden werden, um eine Kontosperre zu verhindern.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "VPN", + "Tunnel", + "Gateway", + "Verbindung fehlgeschlagen", + "Homeoffice", + "Remote Access" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-net-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-NET-004.json b/knowledge/KB-NET-004.json new file mode 100644 index 0000000..a9e96f7 --- /dev/null +++ b/knowledge/KB-NET-004.json @@ -0,0 +1,21 @@ +{ + "id": "KB-NET-004", + "title": "Hostname oder interne Webseite wird nicht gefunden", + "text": "Ein interner Servername, Intranet-Link oder Dienst kann per Namen nicht aufgerufen werden. Typische Meldungen sind „Server nicht gefunden“, „DNS_PROBE“, „Name konnte nicht aufgelöst werden“ oder ein Browserfehler zur Namensauflösung. Prüfrelevant sind Unternehmensnetz/VPN, exakte URL bzw. Hostname und ob andere interne Ressourcen funktionieren.", + "answer": "Bitte prüfen Sie, ob Sie mit dem Unternehmensnetz oder VPN verbunden sind, und rufen Sie die Adresse anschließend erneut auf. Senden Sie uns bitte die vollständige Adresse beziehungsweise den Servernamen und die genaue Fehlermeldung. Falls nur dieser eine Dienst betroffen ist, geben Sie dies bitte ebenfalls an.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "DNS", + "Server nicht gefunden", + "Name nicht aufgelöst", + "Hostname", + "Intranet", + "DNS_PROBE" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-net-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-OFF-001.json b/knowledge/KB-OFF-001.json new file mode 100644 index 0000000..b326130 --- /dev/null +++ b/knowledge/KB-OFF-001.json @@ -0,0 +1,22 @@ +{ + "id": "KB-OFF-001", + "title": "Office 2024 meldet Aktivierung erforderlich oder nicht lizenziertes Produkt", + "text": "Word, Excel, Outlook oder andere Office-2024-Anwendungen melden eine erforderliche Aktivierung, „Nicht lizenziertes Produkt“ oder einen Aktivierungsfehler. Bei Office LTSC/Office 2024 hängt die Aktivierung von der in der Organisation eingesetzten Lizenzierungs- und Aktivierungsmethode ab. Lizenz-, KMS-, MAK- oder Bereitstellungsänderungen gehören in einen administrativen Workflow.", + "answer": "Bitte öffnen Sie eine betroffene Office-Anwendung und senden Sie uns den genauen Wortlaut der Aktivierungsmeldung. Teilen Sie außerdem mit, ob Office auf diesem Gerät zuvor bereits aktiviert war und seit wann die Meldung erscheint. Nehmen Sie bitte keine Änderungen an Produktschlüsseln oder Aktivierungseinstellungen vor; wir prüfen die für Ihr Gerät vorgesehene Office-2024-Aktivierung.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Office 2024 Aktivierung", + "nicht lizenziertes Produkt", + "Aktivierung erforderlich", + "Office LTSC 2024", + "KMS", + "MAK", + "Lizenz" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-off-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-OFF-002.json b/knowledge/KB-OFF-002.json new file mode 100644 index 0000000..42672af --- /dev/null +++ b/knowledge/KB-OFF-002.json @@ -0,0 +1,21 @@ +{ + "id": "KB-OFF-002", + "title": "Word oder Excel 2024 startet nicht oder stürzt ab", + "text": "Word oder Excel 2024 startet nicht, friert ein oder beendet sich. Häufige Ursachen sind Add-ins, ein bestimmtes Dokument, lokale Office-Komponenten oder ein problematischer Update-/Systemzustand. Für die Erstdiagnose ist wichtig, ob das Problem mit jeder Datei oder nur mit einem bestimmten Dokument auftritt.", + "answer": "Bitte starten Sie den Rechner einmal neu und öffnen Sie anschließend Word beziehungsweise Excel ohne das betroffene Dokument. Erstellen Sie testweise eine neue leere Datei. Teilen Sie uns bitte mit, ob das Problem bei allen Dateien oder nur bei einem bestimmten Dokument auftritt und welche Fehlermeldung angezeigt wird.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Word 2024 startet nicht", + "Excel 2024 startet nicht", + "Office Absturz", + "Word hängt", + "Excel hängt", + "Office 2024" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-off-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-OFF-003.json b/knowledge/KB-OFF-003.json new file mode 100644 index 0000000..e6d82cc --- /dev/null +++ b/knowledge/KB-OFF-003.json @@ -0,0 +1,21 @@ +{ + "id": "KB-OFF-003", + "title": "Word- oder Excel-Datei ist zur Bearbeitung gesperrt", + "text": "Ein Word- oder Excel-Dokument meldet, dass es von einem anderen Benutzer oder Prozess zur Bearbeitung gesperrt ist. Ursache kann eine tatsächlich parallel geöffnete Datei, eine noch aktive Office-Sitzung, eine Freigabe im Dateisystem oder eine verwaiste Sperrinformation sein. Sperrdateien sollten nicht blind gelöscht werden, um Datenverlust zu vermeiden.", + "answer": "Bitte prüfen Sie, ob die Datei noch auf einem anderen Rechner, in einer anderen Office-Sitzung oder durch eine Kollegin beziehungsweise einen Kollegen geöffnet ist. Schließen Sie alle betroffenen Office-Fenster und öffnen Sie die Datei anschließend erneut. Falls die Sperre bestehen bleibt, senden Sie uns bitte den vollständigen Speicherort und die genaue Meldung. Löschen Sie keine Sperr- oder temporären Dateien eigenständig.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Datei gesperrt", + "zur Bearbeitung gesperrt", + "Word Datei gesperrt", + "Excel Datei gesperrt", + "Office 2024", + "Dateisperre" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-off-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-OFF-004.json b/knowledge/KB-OFF-004.json new file mode 100644 index 0000000..1cf2a65 --- /dev/null +++ b/knowledge/KB-OFF-004.json @@ -0,0 +1,22 @@ +{ + "id": "KB-OFF-004", + "title": "Office-Add-in verursacht möglicherweise Fehler in Word, Excel oder Outlook", + "text": "Office-Anwendungen können durch ein Add-in ungewöhnlich langsam starten, hängen oder nur bei bestimmten Aktionen Fehler zeigen. Für die Erstdiagnose ist relevant, ob das Problem nach einem Neustart weiterhin besteht, welche Anwendung betroffen ist und ob kürzlich ein Add-in oder eine Fachanwendung installiert beziehungsweise aktualisiert wurde. Das automatische Deaktivieren geschäftskritischer Add-ins ist nicht vorgesehen.", + "answer": "Bitte starten Sie den Rechner einmal neu und prüfen Sie den Vorgang erneut. Teilen Sie uns mit, welche Office-Anwendung und welche Aktion betroffen ist und ob kürzlich eine Erweiterung, Fachanwendung oder ein Update installiert wurde. Deaktivieren oder entfernen Sie geschäftlich benötigte Add-ins bitte nicht eigenständig; wir prüfen die Erweiterungen anschließend gezielt.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Office Add-in", + "Outlook Add-in", + "Word Add-in", + "Excel Add-in", + "Office langsam", + "Office Fehler", + "Office 2024" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-off-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-PRN-001.json b/knowledge/KB-PRN-001.json new file mode 100644 index 0000000..25c86bd --- /dev/null +++ b/knowledge/KB-PRN-001.json @@ -0,0 +1,20 @@ +{ + "id": "KB-PRN-001", + "title": "Druckauftrag bleibt in Windows-Druckwarteschlange hängen", + "text": "Ein Druckauftrag verbleibt in der Windows-Warteschlange, mehrere Dokumente warten oder der Drucker reagiert nach einem fehlerhaften Job nicht mehr. Vor administrativen Spooler-Eingriffen sollte geprüft werden, ob nur ein Dokument oder alle Benutzer betroffen sind und ob der physische Drucker betriebsbereit ist.", + "answer": "Bitte prüfen Sie zunächst den Drucker auf Papier-, Toner- oder Fehlermeldungen. Öffnen Sie danach die Windows-Druckwarteschlange und brechen Sie nur Ihren eigenen offensichtlich fehlerhaften Auftrag ab. Falls weitere Aufträge hängen oder der Abbruch nicht möglich ist, teilen Sie uns bitte Druckername und Standort mit.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Druckwarteschlange", + "Print Queue", + "Druckauftrag hängt", + "Spooler", + "Dokument wird nicht gedruckt" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-prn-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-PRN-002.json b/knowledge/KB-PRN-002.json new file mode 100644 index 0000000..5ef76e8 --- /dev/null +++ b/knowledge/KB-PRN-002.json @@ -0,0 +1,20 @@ +{ + "id": "KB-PRN-002", + "title": "PDF lässt sich nicht drucken, andere Dokumente schon", + "text": "Nur PDF-Dateien oder ein bestimmtes PDF lassen sich nicht drucken, während Word/Excel oder eine Testseite funktionieren. Ursache kann die PDF-Datei, der verwendete Viewer, eingebettete Schriften/Grafiken oder der Druckertreiber sein. Wichtig ist der Vergleich mit einem anderen PDF und gegebenenfalls einem anderen freigegebenen PDF-Viewer.", + "answer": "Bitte testen Sie zunächst ein anderes PDF auf demselben Drucker. Falls nur eine bestimmte Datei betroffen ist, öffnen Sie sie erneut und versuchen Sie einen erneuten Ausdruck. Teilen Sie uns bitte mit, ob andere PDFs funktionieren und welche Anwendung Sie zum Öffnen verwenden.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "PDF druckt nicht", + "Adobe", + "PDF Druck", + "Druckproblem", + "PDF Viewer" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-prn-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-RDP-001.json b/knowledge/KB-RDP-001.json new file mode 100644 index 0000000..6578b89 --- /dev/null +++ b/knowledge/KB-RDP-001.json @@ -0,0 +1,20 @@ +{ + "id": "KB-RDP-001", + "title": "Remotedesktop-Verbindung kann nicht hergestellt werden", + "text": "Eine RDP-/Remotedesktop-Verbindung schlägt fehl, meldet nicht erreichbaren Computer, Anmeldefehler oder getrennte Sitzung. Prüfrelevant sind VPN/Unternehmensnetz, Zielrechnername, Erreichbarkeit und Berechtigung. RDP-Freischaltungen oder Firewalländerungen dürfen nicht automatisch vorgenommen werden.", + "answer": "Bitte prüfen Sie, ob Sie mit dem Unternehmensnetz beziehungsweise VPN verbunden sind. Senden Sie uns den Namen des Zielrechners und die genaue Remotedesktop-Fehlermeldung. Wiederholen Sie Anmeldeversuche nicht unnötig, wenn eine Kennwort- oder Kontosperrmeldung erscheint.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "RDP", + "Remotedesktop", + "Remote Desktop", + "Zielcomputer nicht erreichbar", + "Remoteverbindung" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-rdp-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-SEC-001.json b/knowledge/KB-SEC-001.json new file mode 100644 index 0000000..76862d1 --- /dev/null +++ b/knowledge/KB-SEC-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-SEC-001", + "title": "MFA-Anmeldung oder Bestätigung funktioniert nicht", + "text": "Eine Anmeldung verlangt eine MFA-Bestätigung, die nicht ankommt, abgelehnt wird oder nicht mehr zum vorhandenen Gerät passt. Der Fall kann VPN, Webanwendungen oder andere SSO-Dienste betreffen. MFA-Methoden und Identitätsprüfung sind sicherheitskritisch und dürfen nicht automatisiert zurückgesetzt werden.", + "answer": "Bitte brechen Sie wiederholte Anmeldeversuche ab und teilen Sie uns mit, bei welchem Dienst die MFA-Abfrage erscheint und welche Methode erwartet wird. Senden Sie keine Einmalcodes oder Freigabecodes per Ticket. Ein notwendiger MFA-Reset erfolgt nach der vorgesehenen Identitätsprüfung durch den IT-Service.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "MFA", + "Multi-Faktor", + "Authenticator", + "Bestätigung kommt nicht", + "2FA", + "Einmalcode" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-sec-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-SEC-002.json b/knowledge/KB-SEC-002.json new file mode 100644 index 0000000..dbc00c4 --- /dev/null +++ b/knowledge/KB-SEC-002.json @@ -0,0 +1,21 @@ +{ + "id": "KB-SEC-002", + "title": "Verdächtige Anmeldung oder unerwartete MFA-Anfrage", + "text": "Ein Benutzer erhält eine MFA-Abfrage, Anmeldebenachrichtigung oder Passwortabfrage, obwohl er selbst keine Anmeldung gestartet hat. Dies kann ein Sicherheitsvorfall sein. Es darf keine Bestätigung erfolgen. Der Fall muss zeitnah an den IT-/Security-Prozess übergeben werden.", + "answer": "Bitte bestätigen Sie keine unerwartete MFA- oder Anmeldeanfrage. Brechen Sie die Anfrage ab und informieren Sie den IT-Service über Zeitpunkt, betroffenen Dienst und die angezeigte Meldung. Senden Sie keine Kennwörter oder Einmalcodes im Ticket.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "unerwartete MFA", + "verdächtige Anmeldung", + "Authenticator Anfrage", + "Security", + "Phishing", + "Anmeldeversuch" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-sec-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-TA-001.json b/knowledge/KB-TA-001.json new file mode 100644 index 0000000..f93bb4c --- /dev/null +++ b/knowledge/KB-TA-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-TA-001", + "title": "Triumph-Adler Kopierer oder MFP druckt nicht", + "text": "Ein Triumph-Adler Multifunktionssystem ist nicht erreichbar, Druckaufträge bleiben stehen oder das Gerät meldet einen Fehler. Zuerst sind Strom/Gerätestatus, Papier/Toner, ausgewählter Drucker und Netzwerkverbindung zu prüfen. Gerätespezifische Servicecodes sollten vollständig erfasst und nicht durch ungezielte Einstellungen verändert werden.", + "answer": "Bitte prüfen Sie, ob das Triumph-Adler-Gerät betriebsbereit ist und am Display eine Fehler- oder Wartungsmeldung angezeigt wird. Stellen Sie sicher, dass in der Anwendung der richtige Drucker ausgewählt ist. Senden Sie uns bitte Modell, Standort und den genauen Text beziehungsweise Fehlercode am Display.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Triumph-Adler", + "Triumph Adler", + "Kopierer", + "MFP", + "Druckt nicht", + "Drucker offline" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ta-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-TA-002.json b/knowledge/KB-TA-002.json new file mode 100644 index 0000000..a5d66e8 --- /dev/null +++ b/knowledge/KB-TA-002.json @@ -0,0 +1,20 @@ +{ + "id": "KB-TA-002", + "title": "Triumph-Adler: Scan-to-Mail funktioniert nicht", + "text": "Das Multifunktionssystem kann ein Dokument nicht per E-Mail versenden. Mögliche Ursachen sind Netzwerk, SMTP-/Maildienst, Absenderkonfiguration, Adressbuch, Dateigröße oder zentrale Sicherheitsänderungen. Benutzer sollten SMTP-, DNS- oder Zertifikatseinstellungen am Gerät nicht selbst ändern.", + "answer": "Bitte notieren Sie die genaue Meldung am Display und prüfen Sie, ob das Scannen an eine andere bekannte interne Adresse funktioniert. Teilen Sie uns Modell, Standort, Zeitpunkt und Fehlercode mit. Ändern Sie keine Mailserver-, Netzwerk- oder Zertifikatseinstellungen am Gerät.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Triumph-Adler Scan to Mail", + "Scan E-Mail", + "SMTP", + "Kopierer scannen", + "Fehlercode" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ta-002", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-TA-003.json b/knowledge/KB-TA-003.json new file mode 100644 index 0000000..50a8bde --- /dev/null +++ b/knowledge/KB-TA-003.json @@ -0,0 +1,20 @@ +{ + "id": "KB-TA-003", + "title": "Triumph-Adler: Scan in Netzwerkordner funktioniert nicht", + "text": "Scan-to-Folder/SMB auf einem Triumph-Adler-Gerät schlägt fehl. Häufig beteiligt sind Netzwerkpfad, Zielserver, Berechtigung, gespeichertes Dienstkonto oder geänderte Sicherheitsanforderungen. Da dabei Zugangsdaten am Gerät hinterlegt sein können, dürfen Benutzer keine Credentials oder SMB-Einstellungen eigenständig ändern.", + "answer": "Bitte notieren Sie die genaue Fehlermeldung am Triumph-Adler-Gerät und teilen Sie uns mit, welcher Scan-Zielname verwendet wird. Prüfen Sie nicht eigenständig gespeicherte Kennwörter oder Netzwerkpfade am Gerät. Wir prüfen Zielserver, Berechtigung und Gerätekonfiguration.", + "auto_reply": false, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Triumph-Adler Scan Ordner", + "Scan to Folder", + "SMB", + "Netzwerkordner", + "Kopierer Scan" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ta-003", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-TA-004.json b/knowledge/KB-TA-004.json new file mode 100644 index 0000000..4ae8852 --- /dev/null +++ b/knowledge/KB-TA-004.json @@ -0,0 +1,21 @@ +{ + "id": "KB-TA-004", + "title": "Triumph-Adler: Papier, Toner oder Wartungsmeldung", + "text": "Das Triumph-Adler-System meldet Papierstau, Toner/Resttoner, Wartung oder einen gerätespezifischen Servicecode. Zuerst ist die genaue Displaymeldung zu erfassen. Servicecodes und mechanische Fehler sollten nicht durch wiederholtes Aus-/Einschalten oder Öffnen nicht vorgesehener Baugruppen verschlimmert werden.", + "answer": "Bitte lesen Sie die vollständige Meldung beziehungsweise den Fehlercode am Display ab. Bei einem einfachen Papierstau folgen Sie ausschließlich den grafischen Hinweisen am Gerät. Bei Toner-, Wartungs- oder Servicecode-Meldungen senden Sie uns bitte Modell, Standort und den exakten Meldungstext.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Triumph-Adler Toner", + "Papierstau", + "Wartung", + "Servicecode", + "Resttoner", + "Kopierer Fehler" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-ta-004", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-WEB-001.json b/knowledge/KB-WEB-001.json new file mode 100644 index 0000000..5ea383f --- /dev/null +++ b/knowledge/KB-WEB-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-WEB-001", + "title": "Interne Webanwendung funktioniert nur bei einzelnen Benutzern nicht", + "text": "Eine interne Webanwendung funktioniert für andere Benutzer, aber nicht für den meldenden Benutzer. Das spricht eher für Konto, Berechtigung, Browserprofil, SSO oder benutzerspezifische Daten als für einen globalen Ausfall. Wichtig sind Browser-Vergleich, genaue URL, Zeitpunkt und Fehlertext.", + "answer": "Bitte testen Sie die Anwendung in einem privaten Browserfenster und – sofern zulässig – in einem zweiten freigegebenen Browser. Teilen Sie uns die vollständige Adresse, den verwendeten Browser und den genauen Fehlertext mit. Wenn Kolleginnen oder Kollegen die Anwendung gleichzeitig nutzen können, geben Sie dies bitte ebenfalls an.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Webanwendung", + "nur bei mir", + "Browser", + "SSO", + "Berechtigung", + "interne Anwendung" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-web-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-WIN-001.json b/knowledge/KB-WIN-001.json new file mode 100644 index 0000000..0ed8e7c --- /dev/null +++ b/knowledge/KB-WIN-001.json @@ -0,0 +1,21 @@ +{ + "id": "KB-WIN-001", + "title": "Windows fordert Neustart oder verhält sich nach Updates ungewöhnlich", + "text": "Nach Windows-, Treiber- oder Softwareupdates können ein ausstehender Neustart, langsamere Anmeldung, nicht gestartete Dienste oder temporäre Anwendungsprobleme auftreten. Ein kontrollierter Neustart ist ein sinnvoller erster Schritt, sofern keine ungespeicherten Arbeiten oder laufenden Installationen betroffen sind.", + "answer": "Bitte speichern und schließen Sie Ihre geöffneten Arbeiten und starten Sie den Rechner über „Neu starten“ einmal vollständig neu. Prüfen Sie das Problem anschließend erneut. Falls es weiterhin besteht, teilen Sie uns bitte die betroffene Anwendung und die genaue Fehlermeldung mit.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Windows Update", + "Neustart erforderlich", + "pending reboot", + "Update", + "nach Update", + "Reboot" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-win-001", + "language": "de-DE", + "communication_style": "formal" +} diff --git a/knowledge/KB-WIN-002.json b/knowledge/KB-WIN-002.json new file mode 100644 index 0000000..ae0ce6e --- /dev/null +++ b/knowledge/KB-WIN-002.json @@ -0,0 +1,21 @@ +{ + "id": "KB-WIN-002", + "title": "Zu wenig freier Speicherplatz auf Laufwerk C", + "text": "Windows oder Anwendungen melden zu wenig Speicherplatz, Updates scheitern oder Anwendungen können temporäre Dateien nicht anlegen. Vor automatischen Bereinigungen muss zwischen persönlichen Daten, temporären Dateien und verwalteten Systemdateien unterschieden werden. Große Unternehmensanwendungen sollten nicht eigenständig deinstalliert werden.", + "answer": "Bitte prüfen Sie unter „Dieser PC“, wie viel freier Speicher auf Laufwerk C: verfügbar ist. Löschen Sie nur eindeutig nicht mehr benötigte persönliche Dateien, beispielsweise aus dem Papierkorb oder dem Download-Ordner. Deinstallieren Sie keine Unternehmenssoftware eigenständig. Teilen Sie uns bitte den verfügbaren Speicherplatz mit, falls die Meldung bestehen bleibt.", + "auto_reply": true, + "min_score": 0.7, + "categories": [], + "keywords": [ + "Speicherplatz", + "Laufwerk C voll", + "Festplatte voll", + "disk full", + "kein Speicher", + "zu wenig Speicher" + ], + "source": "internal-kb", + "source_uri": "kb://starter/kb-win-002", + "language": "de-DE", + "communication_style": "formal" +}