Bugfix GLPI-KB-Category
release-tag / release-image (push) Successful in 1m34s

This commit is contained in:
2026-08-05 15:33:56 +02:00
parent 985e40ffcb
commit 31fa84f243
120 changed files with 3021 additions and 2704 deletions
+28 -8
View File
@@ -348,11 +348,13 @@ func (s *Server) diagnosticKnowledgeSearch(w http.ResponseWriter, r *http.Reques
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"`
ID string `json:"id"`
Title string `json:"title"`
Source string `json:"source"`
AutoReply bool `json:"auto_reply"`
AutoReplyDecision string `json:"auto_reply_decision,omitempty"`
AutoReplyDetail string `json:"auto_reply_detail,omitempty"`
Unmapped []string `json:"unmapped_categories,omitempty"`
}
out := make([]item, 0, limit)
for _, d := range s.knowledge.List() {
@@ -360,7 +362,7 @@ func (s *Server) diagnosticKnowledgeSearch(w http.ResponseWriter, r *http.Reques
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...)})
out = append(out, item{ID: d.ID, Title: d.Title, Source: d.Source, AutoReply: d.AutoReply, AutoReplyDecision: d.AutoReplyDecision, AutoReplyDetail: d.AutoReplyDetail, Unmapped: append([]string(nil), d.UnmappedExternalCategories...)})
if len(out) >= limit {
break
}
@@ -372,6 +374,24 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) {
g, o := s.metrics.Health()
poll := s.metrics.PollStatus()
kbOK, kbDocs, kbLastSync, kbLastErr := s.metrics.GLPIKBStatus()
glpiKBAutoReplyApproved := 0
glpiKBAutoReplyBlocked := 0
glpiKBAutoReplyDecisions := map[string]int{}
for _, doc := range s.knowledge.List() {
if !strings.EqualFold(strings.TrimSpace(doc.Source), strings.TrimSpace(s.cfg.GLPIKBSource)) {
continue
}
if doc.AutoReply {
glpiKBAutoReplyApproved++
} else {
glpiKBAutoReplyBlocked++
}
decision := strings.TrimSpace(doc.AutoReplyDecision)
if decision == "" {
decision = "legacy_or_unspecified"
}
glpiKBAutoReplyDecisions[decision]++
}
loadStats := s.knowledge.LoadStats()
initStatus := s.knowledge.InitStatus()
ollamaNodes := []model.OllamaNodeStatus{}
@@ -405,14 +425,14 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) {
"context_enabled": s.cfg.ContextEnabled, "context_fetches": s.metrics.ContextFetches.Load(), "context_errors": s.metrics.ContextErrors.Load(),
"change_calendar_enabled": s.cfg.ChangeCalendarEnabled, "major_incidents_enabled": s.cfg.MajorIncidentsEnabled, "user_device_context_enabled": s.cfg.UserDeviceContextEnabled,
"knowledge_edit_enabled": s.cfg.KnowledgeWebEditEnabled, "learning_enabled": s.cfg.LearningEnabled, "learning_examples": s.feedback.LearningCount(),
"glpi_kb_enabled": s.cfg.GLPIKBEnabled, "glpi_kb_ok": kbOK, "glpi_kb_documents": kbDocs, "glpi_kb_last_sync": kbLastSync, "glpi_kb_last_error": kbLastErr, "glpi_kb_source": s.cfg.GLPIKBSource, "glpi_kb_sync_interval": s.cfg.GLPIKBSyncInterval.String(),
"glpi_kb_enabled": s.cfg.GLPIKBEnabled, "glpi_kb_ok": kbOK, "glpi_kb_documents": kbDocs, "glpi_kb_last_sync": kbLastSync, "glpi_kb_last_error": kbLastErr, "glpi_kb_source": s.cfg.GLPIKBSource, "glpi_kb_sync_interval": s.cfg.GLPIKBSyncInterval.String(), "glpi_kb_auto_reply_approved": glpiKBAutoReplyApproved, "glpi_kb_auto_reply_blocked": glpiKBAutoReplyBlocked, "glpi_kb_auto_reply_decisions": glpiKBAutoReplyDecisions,
"uptime_kuma_enabled": s.cfg.UptimeKumaEnabled, "uptime_kuma_mode": s.cfg.UptimeKumaMode, "uptime_kuma_status_pages": s.cfg.UptimeKumaStatusPages, "context_fail_closed": s.cfg.ContextBlockReplyOnError, "context_incident_block": s.cfg.ContextBlockReplyOnIncident,
"context_status_reply_enabled": s.cfg.ContextStatusReplyEnabled, "context_status_reply_min_relevance": s.cfg.ContextStatusReplyMinRelevance, "context_status_reply_min_ai_confidence": s.cfg.ContextStatusReplyMinAIConfidence, "context_status_reply_min_final_score": s.cfg.ContextStatusReplyMinFinalScore, "context_incident_reply_text_configured": strings.TrimSpace(s.cfg.ContextIncidentReplyText) != "", "context_maintenance_reply_text_configured": strings.TrimSpace(s.cfg.ContextMaintenanceReplyText) != "",
"workers": s.cfg.Workers, "queue_size": s.cfg.QueueSize, "glpi_api_version": s.cfg.GLPIAPIVersion, "glpi_poll_interval": s.cfg.GLPIPollInterval.String(), "glpi_poll_limit": s.cfg.GLPIPollLimit, "glpi_allowed_status_ids": s.cfg.GLPIAllowedStatusIDs, "glpi_ticket_filter_configured": strings.TrimSpace(s.cfg.GLPITicketFilter) != "", "glpi_timeout": s.cfg.GLPITimeout.String(),
"ollama_model": s.cfg.OllamaModel, "ollama_embedding_model": s.cfg.OllamaEmbeddingModel, "ollama_timeout": s.cfg.OllamaTimeout.String(), "ollama_num_predict": s.cfg.OllamaNumPredict, "ollama_keep_alive": s.cfg.OllamaKeepAlive.String(), "ollama_think": s.cfg.OllamaThink, "ollama_max_concurrent": s.cfg.OllamaMaxConcurrent, "ollama_json_retries": s.cfg.OllamaJSONRetries,
"ollama_nodes": ollamaNodes, "ollama_node_count": len(ollamaNodes), "ollama_healthy_nodes": ollamaHealthyNodes, "ollama_available_nodes": ollamaAvailableNodes, "ollama_routing_mode": ollamaRoutingMode, "ollama_node_max_inflight": s.cfg.OllamaNodeMaxInflight, "ollama_node_health_interval": s.cfg.OllamaNodeHealthInterval.String(), "ollama_node_failure_cooldown": s.cfg.OllamaNodeFailureCooldown.String(), "ollama_node_request_timeout": s.cfg.OllamaNodeRequestTimeout.String(), "ollama_failover_enabled": s.cfg.OllamaFailoverEnabled, "ollama_failover_attempts": s.cfg.OllamaFailoverAttempts, "ollama_require_same_model_digest": s.cfg.OllamaRequireSameDigest, "ollama_require_embedding_model": s.cfg.OllamaRequireEmbeddingModel,
"rag_enabled": s.cfg.RAGEnabled, "knowledge_top_k": s.cfg.KnowledgeTopK, "knowledge_audit_top_k": s.cfg.KnowledgeAuditTopK, "knowledge_candidate_max_gap": s.cfg.KnowledgeCandidateMaxGap, "category_prompt_limit": s.cfg.CategoryPromptLimit, "knowledge_max_query_chunks": s.cfg.KnowledgeMaxQueryChunks,
"glpi_kb_path": s.cfg.GLPIKBPath, "glpi_kb_filter_configured": strings.TrimSpace(s.cfg.GLPIKBFilter) != "", "glpi_kb_limit": s.cfg.GLPIKBLimit, "glpi_kb_auto_reply": s.cfg.GLPIKBAutoReply, "glpi_kb_auto_reply_category_ids": s.cfg.GLPIKBAutoReplyCategoryIDs,
"glpi_kb_path": s.cfg.GLPIKBPath, "glpi_kb_filter_configured": strings.TrimSpace(s.cfg.GLPIKBFilter) != "", "glpi_kb_limit": s.cfg.GLPIKBLimit, "glpi_kb_auto_reply": s.cfg.GLPIKBAutoReply, "glpi_kb_auto_reply_category_ids": s.cfg.GLPIKBAutoReplyCategoryIDs, "glpi_kb_auto_reply_itil_category_ids": s.cfg.GLPIKBAutoReplyITILCategoryIDs,
"learning_max_examples": s.cfg.LearningMaxExamples, "learning_examples_per_category": s.cfg.LearningExamplesPerCategory,
"context_timeout": s.cfg.ContextTimeout.String(), "context_relevance_min_score": s.cfg.ContextRelevanceMinScore, "change_lookback": s.cfg.ChangeLookback.String(), "change_lookahead": s.cfg.ChangeLookahead.String(),
})
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -82,8 +82,8 @@ function renderRun(){
$('#categoryAIReason').textContent=r.category_ai_reason||(legacy?r.ai_reason:'–')||'–';$('#priorityAIReason').textContent=r.priority_ai_reason||'–';$('#statusAIReason').textContent=r.status_ai_reason||'–';$('#replyAIReason').textContent=r.reply_ai_reason||(legacy?'Im historischen Lauf gemeinsam mit der Kategorie erzeugt.':'–');$('#policyReason').textContent=r.policy_reason||'–';$('#errorText').textContent=r.error||'–';$('#kbDiagnostic').innerHTML='<div class="empty">Knowledge-Artikel suchen oder einen Kandidaten anklicken.</div>'
}
function selectionText(code){return({sent_to_ai:'An die KI gesendet',below_retrieval_floor:'Unter Retrieval-Floor',outside_candidate_gap:'Außerhalb des dynamischen Abstands',max_candidates_reached:'Maximale Kandidatenzahl erreicht',not_selected:'Nicht ausgewählt',source_not_allowed_for_purpose:'Quelle für diese Analyse nicht freigegeben'})[code]||code||'–'}
async function diagnoseKB(id,purpose=$('#kbPurpose').value){if(!current)return;$('#kbPurpose').value=purpose;$('#kbDiagnostic').innerHTML='<div class="empty">Berechne Diagnose …</div>';try{const d=await api(`/api/diagnostics/run/${encodeURIComponent(current.run_id)}/knowledge?knowledge_id=${encodeURIComponent(id)}&purpose=${encodeURIComponent(purpose)}`);const warn=d.current_ticket_changed?'<div class="warning">Das Ticket wurde seit diesem historischen Lauf verändert. Die KB-Bewertung unten ist eine aktuelle Neu-Berechnung gegen den jetzigen Ticketstand/Index; die gespeicherten Laufregeln oben bleiben historisch verbindlich.</div>':'';const scores=[['Retrieval',d.retrieval_score],['Semantik',d.semantic_score],['Titel',d.title_score],['Lexikalisch',d.lexical_score],['Kategorie',d.category_score]];const purposeLabel=d.purpose==='category'?'Kategorieanalyse':'Antwortanalyse';const evidence=d.purpose==='reply'?`${statusChip(`Evidenz ${pct(d.evidence_score)}`,d.evidence_score>=d.required_score?'good':'warn')}${statusChip(`erforderlich ${pct(d.required_score)}`,'info')}${d.document.auto_reply?statusChip('auto_reply=true','good'):statusChip('auto_reply=false','bad')}`:'';$('#kbDiagnostic').innerHTML=`${warn}<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start"><div><div class="eyebrow">KB Inspector · ${esc(purposeLabel)}</div><h2 style="margin:4px 0">${esc(d.title)}</h2><div class="muted">${esc(d.knowledge_id)} · ${esc(d.source)}</div></div><div class="chips">${d.retrieval_rank?statusChip(`Rang #${d.retrieval_rank}`,'info'):statusChip('nicht in Quellenliste','warn')}${d.sent_to_ai?statusChip(`an ${purposeLabel}-KI gesendet`,'good'):statusChip(selectionText(d.selection_reason),'warn')}${d.ai_selected?statusChip('von Antwort-KI gewählt','good'):''}</div></div><div class="scoregrid">${scores.map(x=>`<div class="scorebox"><div class="label">${esc(x[0])}</div><div class="n">${pct(x[1])}</div><div class="bar"><i style="width:${Math.max(0,Math.min(100,Number(x[1]||0)*100))}%"></i></div></div>`).join('')}</div><div class="kbmeta">${statusChip(`Cutoff ${pct(d.candidate_cutoff)}`,'info')}${evidence}${(d.unmapped_categories||[]).length?statusChip(`ungemappt: ${d.unmapped_categories.join(', ')}`,'bad'):''}</div><div class="grid2"><div><div class="label">Passender Ticketabschnitt</div><div class="excerpt">${esc(d.best_query_excerpt||'–')}</div></div><div><div class="label">Passender KB-Abschnitt</div><div class="excerpt">${esc(d.best_chunk_excerpt||'–')}</div></div></div><div style="margin-top:13px"><div class="panel-title">Regelprüfung für diesen Artikel</div>${(d.checks||[]).map(ruleHTML).join('')}</div>`}catch(e){$('#kbDiagnostic').innerHTML=`<div class="warning">${esc(e.message)}</div>`}}
async function searchKB(){const q=$('#kbSearch').value.trim();if(q.length<2){$('#kbResults').classList.add('hidden');return}try{const rows=await api(`/api/diagnostics/knowledge?q=${encodeURIComponent(q)}&limit=30`);$('#kbResults').innerHTML=rows.map(x=>`<div class="result" data-search-kb="${esc(x.id)}"><strong>${esc(x.title)}</strong><div class="muted" style="font-size:11px;margin-top:3px">${esc(x.id)} · ${esc(x.source)} · auto_reply=${x.auto_reply?'true':'false'}${(x.unmapped_categories||[]).length?` · ungemappt: ${esc(x.unmapped_categories.join(', '))}`:''}</div></div>`).join('')||'<div class="empty">Keine Treffer.</div>';$('#kbResults').classList.remove('hidden');document.querySelectorAll('[data-search-kb]').forEach(x=>x.onclick=()=>{$('#kbResults').classList.add('hidden');$('#kbSearch').value=x.dataset.searchKb;diagnoseKB(x.dataset.searchKb,$('#kbPurpose').value)})}catch(e){$('#kbResults').innerHTML=`<div class="warning">${esc(e.message)}</div>`;$('#kbResults').classList.remove('hidden')}}
async function diagnoseKB(id,purpose=$('#kbPurpose').value){if(!current)return;$('#kbPurpose').value=purpose;$('#kbDiagnostic').innerHTML='<div class="empty">Berechne Diagnose …</div>';try{const d=await api(`/api/diagnostics/run/${encodeURIComponent(current.run_id)}/knowledge?knowledge_id=${encodeURIComponent(id)}&purpose=${encodeURIComponent(purpose)}`);const warn=d.current_ticket_changed?'<div class="warning">Das Ticket wurde seit diesem historischen Lauf verändert. Die KB-Bewertung unten ist eine aktuelle Neu-Berechnung gegen den jetzigen Ticketstand/Index; die gespeicherten Laufregeln oben bleiben historisch verbindlich.</div>':'';const scores=[['Retrieval',d.retrieval_score],['Semantik',d.semantic_score],['Titel',d.title_score],['Lexikalisch',d.lexical_score],['Kategorie',d.category_score]];const purposeLabel=d.purpose==='category'?'Kategorieanalyse':'Antwortanalyse';const evidence=d.purpose==='reply'?`${statusChip(`Evidenz ${pct(d.evidence_score)}`,d.evidence_score>=d.required_score?'good':'warn')}${statusChip(`erforderlich ${pct(d.required_score)}`,'info')}${d.document.auto_reply?statusChip('auto_reply=true','good'):statusChip('auto_reply=false','bad')}${d.document.auto_reply_decision?statusChip(d.document.auto_reply_decision,d.document.auto_reply?'good':'warn'):''}`:'';$('#kbDiagnostic').innerHTML=`${warn}<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start"><div><div class="eyebrow">KB Inspector · ${esc(purposeLabel)}</div><h2 style="margin:4px 0">${esc(d.title)}</h2><div class="muted">${esc(d.knowledge_id)} · ${esc(d.source)}</div></div><div class="chips">${d.retrieval_rank?statusChip(`Rang #${d.retrieval_rank}`,'info'):statusChip('nicht in Quellenliste','warn')}${d.sent_to_ai?statusChip(`an ${purposeLabel}-KI gesendet`,'good'):statusChip(selectionText(d.selection_reason),'warn')}${d.ai_selected?statusChip('von Antwort-KI gewählt','good'):''}</div></div><div class="scoregrid">${scores.map(x=>`<div class="scorebox"><div class="label">${esc(x[0])}</div><div class="n">${pct(x[1])}</div><div class="bar"><i style="width:${Math.max(0,Math.min(100,Number(x[1]||0)*100))}%"></i></div></div>`).join('')}</div><div class="kbmeta">${statusChip(`Cutoff ${pct(d.candidate_cutoff)}`,'info')}${evidence}${(d.unmapped_categories||[]).length?statusChip(`ungemappt: ${d.unmapped_categories.join(', ')}`,'bad'):''}</div><div class="grid2"><div><div class="label">Passender Ticketabschnitt</div><div class="excerpt">${esc(d.best_query_excerpt||'–')}</div></div><div><div class="label">Passender KB-Abschnitt</div><div class="excerpt">${esc(d.best_chunk_excerpt||'–')}</div></div></div>${d.document.auto_reply_detail?`<div style="margin-top:13px"><div class="label">Auto-Reply-Freigabe</div><div class="excerpt">${esc(d.document.auto_reply_detail)}</div></div>`:''}<div style="margin-top:13px"><div class="panel-title">Regelprüfung für diesen Artikel</div>${(d.checks||[]).map(ruleHTML).join('')}</div>`}catch(e){$('#kbDiagnostic').innerHTML=`<div class="warning">${esc(e.message)}</div>`}}
async function searchKB(){const q=$('#kbSearch').value.trim();if(q.length<2){$('#kbResults').classList.add('hidden');return}try{const rows=await api(`/api/diagnostics/knowledge?q=${encodeURIComponent(q)}&limit=30`);$('#kbResults').innerHTML=rows.map(x=>`<div class="result" data-search-kb="${esc(x.id)}"><strong>${esc(x.title)}</strong><div class="muted" style="font-size:11px;margin-top:3px">${esc(x.id)} · ${esc(x.source)} · auto_reply=${x.auto_reply?'true':'false'}${x.auto_reply_decision?` · ${esc(x.auto_reply_decision)}`:''}${(x.unmapped_categories||[]).length?` · ungemappt: ${esc(x.unmapped_categories.join(', '))}`:''}</div></div>`).join('')||'<div class="empty">Keine Treffer.</div>';$('#kbResults').classList.remove('hidden');document.querySelectorAll('[data-search-kb]').forEach(x=>x.onclick=()=>{$('#kbResults').classList.add('hidden');$('#kbSearch').value=x.dataset.searchKb;diagnoseKB(x.dataset.searchKb,$('#kbPurpose').value)})}catch(e){$('#kbResults').innerHTML=`<div class="warning">${esc(e.message)}</div>`;$('#kbResults').classList.remove('hidden')}}
$('#runSearch').addEventListener('input',filterRuns);$('#kbSearch').addEventListener('input',()=>{clearTimeout(kbTimer);kbTimer=setTimeout(searchKB,220)});$('#refreshBtn').onclick=()=>loadRuns();document.addEventListener('click',e=>{if(!e.target.closest('#kbSearch')&&!e.target.closest('#kbResults'))$('#kbResults').classList.add('hidden')});loadRuns().catch(e=>{$('#runList').innerHTML=`<div class="warning">${esc(e.message)}</div>`});
</script>
</body></html>