Update
This commit is contained in:
@@ -1010,7 +1010,7 @@ func auditKnowledgeCandidates(hits []model.KnowledgeHit, globalMin float64, limi
|
||||
SemanticScore: h.SemanticScore, TitleScore: h.TitleScore, LexicalScore: h.LexicalScore, KeywordScore: h.KeywordScore,
|
||||
CategoryScore: h.CategoryScore, RequiredScore: required, AutoReply: h.Doc.AutoReply,
|
||||
AutoReplyDecision: h.Doc.AutoReplyDecision, AutoReplyDetail: h.Doc.AutoReplyDetail,
|
||||
BestChunkExcerpt: h.BestChunkExcerpt, BestQueryExcerpt: h.BestQueryExcerpt,
|
||||
BestChunkExcerpt: h.BestChunkExcerpt, BestQueryExcerpt: h.BestQueryExcerpt,
|
||||
QueryChunkCount: h.QueryChunkCount, DocumentChunkCount: h.DocumentChunkCount, SentToAI: wasSent, RetrievalRank: idx + 1, SelectionReason: reason,
|
||||
})
|
||||
}
|
||||
|
||||
+26
-28
@@ -159,7 +159,12 @@ func (p Policy) Evaluate(t model.Ticket, d model.Decision, categories []model.Ca
|
||||
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))
|
||||
}
|
||||
|
||||
selectedAutoReplyAllowed := false
|
||||
if selected != nil {
|
||||
effectiveCategoryID := t.CategoryID
|
||||
if res.ChangeCategory {
|
||||
effectiveCategoryID = res.CategoryID
|
||||
}
|
||||
sourceAllowed := p.sourceAllowed(selected.Doc.Source)
|
||||
sourceReplyAllowed := p.sourceAllowedForReply(selected.Doc.Source)
|
||||
langOK := strings.EqualFold(strings.TrimSpace(selected.Doc.Language), p.CommunicationLanguage)
|
||||
@@ -186,7 +191,8 @@ func (p Policy) Evaluate(t model.Ticket, d model.Decision, categories []model.Ca
|
||||
}
|
||||
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))
|
||||
selectedAutoReplyAllowed = knowledgeAutoReplyAllowed(selected.Doc)
|
||||
res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_knowledge_auto_reply", "Artikel darf für Auto-Reply verwendet werden", passFail(selectedAutoReplyAllowed), !selectedAutoReplyAllowed, boolText(selectedAutoReplyAllowed), "true", autoDetail))
|
||||
|
||||
threshold := p.KnowledgeMinScore
|
||||
if selected.Doc.MinScore > threshold {
|
||||
@@ -198,12 +204,7 @@ func (p Policy) Evaluate(t model.Ticket, d model.Decision, categories []model.Ca
|
||||
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
|
||||
}
|
||||
catIDForEvidence := effectiveCategoryID
|
||||
categoryEvidence, categoryAvailable := 0.0, false
|
||||
if len(selected.Doc.Categories) > 0 && catIDForEvidence != 0 {
|
||||
categoryAvailable = true
|
||||
@@ -226,34 +227,23 @@ func (p Policy) Evaluate(t model.Ticket, d model.Decision, categories []model.Ca
|
||||
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
|
||||
}
|
||||
categoryAllowed := knowledgeCategoryAllowed(selected.Doc, t.CategoryID, res.ChangeCategory, res.CategoryID)
|
||||
categoryDetail := "Keine gemappte ITIL-Kategorie am Artikel verfügbar; die fachliche Eignung wird über Retrieval, KI-Auswahl und Evidenz geprüft."
|
||||
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)
|
||||
categoryDetail = fmt.Sprintf("Gemappte Artikel-ITIL-Kategorien: %v; effektive Ticketkategorie: #%d", selected.Doc.Categories, effectiveCategoryID)
|
||||
}
|
||||
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))
|
||||
res.ReplyChecks = append(res.ReplyChecks, check("knowledge", "reply_category_scope", "Artikel passt zur effektiven Ticketkategorie", passFail(categoryAllowed), !categoryAllowed, boolText(categoryAllowed), "true", categoryDetail))
|
||||
} else {
|
||||
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_knowledge_auto_reply", "Artikel darf für Auto-Reply verwendet werden"},
|
||||
{"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"},
|
||||
{"reply_category_scope", "Artikel passt zur effektiven Ticketkategorie"},
|
||||
} {
|
||||
res.ReplyChecks = append(res.ReplyChecks, check("knowledge", spec.code, spec.label, "na", false, "–", "Knowledge-Artikel erforderlich", "Kein gültiger Knowledge-Artikel ausgewählt."))
|
||||
}
|
||||
@@ -286,8 +276,8 @@ func (p Policy) Evaluate(t model.Ticket, d model.Decision, categories []model.Ca
|
||||
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 !selectedAutoReplyAllowed:
|
||||
res.ReplyDecision = "reply_knowledge_auto_reply_not_approved"
|
||||
case selected.Score < p.KnowledgeRetrievalFloor:
|
||||
res.ReplyDecision = "reply_knowledge_retrieval_below_floor"
|
||||
case res.KnowledgeEvidenceScore < res.KnowledgeThreshold:
|
||||
@@ -345,8 +335,16 @@ func knowledgeCategoryAllowed(d model.KnowledgeDoc, currentCategory int64, chang
|
||||
if len(d.Categories) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, id := range d.Categories {
|
||||
if id == catID {
|
||||
return containsPolicyInt64(d.Categories, catID)
|
||||
}
|
||||
|
||||
func knowledgeAutoReplyAllowed(d model.KnowledgeDoc) bool {
|
||||
return d.AutoReply
|
||||
}
|
||||
|
||||
func containsPolicyInt64(values []int64, target int64) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,3 +220,53 @@ func TestPolicyCanDisableAIContentLabel(t *testing.T) {
|
||||
t.Fatalf("disabled AI content label must preserve plain reply behaviour: %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyAllowsExplicitlyApprovedUncategorizedGLPIArticle(t *testing.T) {
|
||||
p := NewPolicy(true, true, .70, .70, .70, .30, .45, .35, .20,
|
||||
[]string{"glpi-kb"}, []string{"glpi-kb"}, "de-DE", "formal", "", "", "", false, false, false, .2)
|
||||
d := replyDecision()
|
||||
d.Category.ID = 67
|
||||
d.Category.Confidence = .99
|
||||
d.Reply.Confidence = .99
|
||||
hits := []model.KnowledgeHit{{
|
||||
Doc: model.KnowledgeDoc{
|
||||
ID: "GLPI-KB-21", Answer: "Drucker neu verbinden.", AutoReply: true,
|
||||
AutoReplyDecision: "glpi_kb_uncategorized_article_approved",
|
||||
Source: "glpi-kb", Language: "de-DE", CommunicationStyle: "formal",
|
||||
},
|
||||
Score: .95,
|
||||
}}
|
||||
d.Reply.KnowledgeID = "GLPI-KB-21"
|
||||
r, err := p.Evaluate(model.Ticket{CategoryID: 67}, d, []model.Category{{ID: 67}}, hits, model.ContextSnapshot{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !r.Reply || r.ReplyDecision != "reply_accepted" {
|
||||
t.Fatalf("expected explicitly approved uncategorized GLPI article to be accepted: %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyDoesNotApplyLegacyITILAutoReplyGateToUncategorizedArticle(t *testing.T) {
|
||||
p := NewPolicy(true, true, .70, .70, .70, .30, .45, .35, .20,
|
||||
[]string{"glpi-kb"}, []string{"glpi-kb"}, "de-DE", "formal", "", "", "", false, false, false, .2)
|
||||
d := replyDecision()
|
||||
d.Category.ID = 66
|
||||
d.Category.Confidence = .99
|
||||
d.Reply.Confidence = .99
|
||||
d.Reply.KnowledgeID = "GLPI-KB-21"
|
||||
hits := []model.KnowledgeHit{{
|
||||
Doc: model.KnowledgeDoc{
|
||||
ID: "GLPI-KB-21", Answer: "Drucker neu verbinden.", AutoReply: true,
|
||||
AutoReplyDecision: "glpi_kb_uncategorized_article_approved",
|
||||
Source: "glpi-kb", Language: "de-DE", CommunicationStyle: "formal",
|
||||
},
|
||||
Score: .95,
|
||||
}}
|
||||
r, err := p.Evaluate(model.Ticket{CategoryID: 66}, d, []model.Category{{ID: 66}}, hits, model.ContextSnapshot{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !r.Reply || r.ReplyDecision != "reply_accepted" {
|
||||
t.Fatalf("legacy ITIL auto-reply gate must not block explicitly approved uncategorized article: %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user