@@ -257,11 +257,15 @@ func (e *Engine) synthesizeKnowledgeArticle(ctx context.Context, trigger string,
|
||||
var quality model.ArticleQualityDecision
|
||||
var finalReviewEvidence []model.ResearchResult
|
||||
var finalCPUQuality articlequality.Result
|
||||
var bestCPUDraft model.KnowledgeArticleDraft
|
||||
var bestCPUQuality articlequality.Result
|
||||
hasBestCPUQuality := false
|
||||
var reviewFeedback *model.ArticleQualityDecision
|
||||
rewritten := false
|
||||
repairAttempts := 0
|
||||
cpuRepairAttempts := 0
|
||||
authorResearchAttempted := false
|
||||
articleTypeReconsidered := false
|
||||
for {
|
||||
content, wasRewritten, generationErr := e.generateArticleContent(ctx, articleRunID, selected, plan, brief, researchResults, reviewFeedback)
|
||||
if generationErr != nil {
|
||||
@@ -311,6 +315,40 @@ func (e *Engine) synthesizeKnowledgeArticle(ctx context.Context, trigger string,
|
||||
metadata["trigger"] = trigger
|
||||
metadata["article_type"] = normalizeArticleType(plan.ArticleType)
|
||||
metadata["author_research_attempted"] = authorResearchAttempted
|
||||
var validationErr *articleDraftValidationError
|
||||
reconsiderable := errors.As(structureErr, &validationErr) && isOperationalArticleType(plan.ArticleType) && !articleTypeReconsidered && (validationErr.Code == "insufficient_solution_steps" || validationErr.Code == "missing_validation_steps")
|
||||
if reconsiderable {
|
||||
articleTypeReconsidered = true
|
||||
previousType := normalizeArticleType(plan.ArticleType)
|
||||
reconsideration, reconsiderErr := e.reconsiderOperationalArticleType(ctx, plan, content, selected, researchResults)
|
||||
if reconsiderErr != nil {
|
||||
e.Broker.Publish(model.Activity{Type: "article.type.reconsideration.failed", Source: "brain", Phase: "knowledge-planning", NodeIDs: plan.SourceNodeIDs, Message: "Die einmalige Artikeltyp-Neubewertung ist fehlgeschlagen", Strength: .34, Metadata: articleRunMetadata(articleRunID, map[string]any{"trigger": trigger, "previous_article_type": previousType, "reason": validationErr.Code, "error": reconsiderErr.Error()})})
|
||||
} else if reconsideration.Action == "reclassify" {
|
||||
plan.ArticleType = reconsideration.ArticleType
|
||||
plan.Reason = strings.TrimSpace(strings.Join([]string{plan.Reason, "v10 type reconsideration: " + reconsideration.Reason}, " · "))
|
||||
// Type is part of article identity. Recompute the fingerprint and avoid
|
||||
// generating a duplicate non-operational draft if one already exists.
|
||||
sourceFingerprint = articleSourceFingerprint(selected, plan, selectedPlanningResearch, e.articlePipelineFingerprintIdentity())
|
||||
if e.hasArticleSourceFingerprint(sourceFingerprint) || e.hasEquivalentArticleDraft(selected, plan, sourceFingerprint) {
|
||||
e.Broker.Publish(model.Activity{Type: "article.duplicate", Source: "brain", Phase: "knowledge-planning", NodeIDs: plan.SourceNodeIDs, Message: "Die korrigierte Artikeltyp-Variante existiert bereits", Strength: .36, Metadata: articleRunMetadata(articleRunID, map[string]any{"trigger": trigger, "reason": "reclassified_article_duplicate", "previous_article_type": previousType, "article_type": plan.ArticleType, "source_fingerprint": sourceFingerprint})})
|
||||
return articleSynthesisOutcome{Skipped: true, Reason: "reclassified_article_duplicate", Action: plan.Action}, nil
|
||||
}
|
||||
if e.RuntimeSettings().ProcessingMode == "clustered" {
|
||||
brief = fallbackSynthesisBrief(plan, selected)
|
||||
} else if rebuiltBrief, briefErr := e.buildKnowledgeBrief(ctx, selected, researchResults, plan.ArticleType); briefErr == nil {
|
||||
brief = rebuiltBrief
|
||||
}
|
||||
reviewFeedback = nil
|
||||
cpuRepairAttempts = 0
|
||||
e.Broker.Publish(model.Activity{Type: "article.type.reconsidered", Source: "brain", Phase: "knowledge-planning", NodeIDs: plan.SourceNodeIDs, Message: fmt.Sprintf("Operationaler Artikeltyp wurde einmalig von %s auf %s korrigiert", previousType, plan.ArticleType), Strength: .7, Metadata: articleRunMetadata(articleRunID, map[string]any{"trigger": trigger, "previous_article_type": previousType, "article_type": plan.ArticleType, "reason": reconsideration.Reason, "structure_failure": validationErr.Code, "author_research_attempted": authorResearchAttempted})})
|
||||
continue
|
||||
} else {
|
||||
metadata["reason"] = "wrong_article_type_or_insufficient_operational_evidence"
|
||||
metadata["reconsideration_reason"] = reconsideration.Reason
|
||||
e.Broker.Publish(model.Activity{Type: "article.type.reconsidered", Source: "brain", Phase: "knowledge-planning", NodeIDs: plan.SourceNodeIDs, Message: "Operationaler Artikeltyp trägt trotz Evidenzsuche keinen belastbaren Lösungsablauf und wird nicht künstlich umgedeutet", Strength: .46, Metadata: articleRunMetadata(articleRunID, metadata)})
|
||||
return articleSynthesisOutcome{Skipped: true, Reason: "wrong_article_type_or_insufficient_operational_evidence", Action: plan.Action}, nil
|
||||
}
|
||||
}
|
||||
e.Broker.Publish(model.Activity{Type: "article.draft.rejected", Source: "brain", Phase: "quality-gate", NodeIDs: plan.SourceNodeIDs, Message: "Der Entwurf erfüllt die Mindeststruktur seines Artikeltyps nicht", Strength: .4, Metadata: articleRunMetadata(articleRunID, metadata)})
|
||||
return articleSynthesisOutcome{Skipped: true, Reason: metadata["reason"].(string), Action: plan.Action}, nil
|
||||
}
|
||||
@@ -323,13 +361,18 @@ func (e *Engine) synthesizeKnowledgeArticle(ctx context.Context, trigger string,
|
||||
return articleSynthesisOutcome{}, fmt.Errorf("deterministic article quality evaluation failed: %w", cpuErr)
|
||||
}
|
||||
finalCPUQuality = cpuQuality.Result
|
||||
if !hasBestCPUQuality || articleCPUQualityBetter(cpuQuality.Result, bestCPUQuality) {
|
||||
bestCPUDraft = draft
|
||||
bestCPUQuality = cpuQuality.Result
|
||||
hasBestCPUQuality = true
|
||||
}
|
||||
e.Broker.Publish(model.Activity{Type: "article.cpu_quality.completed", Source: map[bool]string{true: "agent", false: "brain"}[cpuQuality.Offloaded], Phase: "quality-gate", NodeIDs: draft.SourceNodeIDs, Message: fmt.Sprintf("Modellfreie Artikelprüfung: Score %.2f · %d Wörter · %d Abschnitte", cpuQuality.Result.Score, cpuQuality.Result.WordCount, cpuQuality.Result.SectionCount), Strength: .64, Metadata: articleRunMetadata(articleRunID, map[string]any{
|
||||
"trigger": trigger, "article_type": normalizeArticleType(plan.ArticleType), "passed": cpuQuality.Result.Passed, "score": cpuQuality.Result.Score,
|
||||
"word_count": cpuQuality.Result.WordCount, "content_word_count": cpuQuality.Result.ContentWordCount, "section_count": cpuQuality.Result.SectionCount,
|
||||
"lexical_diversity": cpuQuality.Result.LexicalDiversity, "redundancy": cpuQuality.Result.Redundancy, "evidence_alignment": cpuQuality.Result.EvidenceAlignment,
|
||||
"source_utilization": cpuQuality.Result.SourceUtilization, "technical_specificity": cpuQuality.Result.TechnicalSpecificity, "type_depth_score": cpuQuality.Result.TypeDepthScore,
|
||||
"hard_failures": cpuQuality.Result.HardFailures, "recommendations": cpuQuality.Result.Recommendations, "algorithm": cpuQuality.Result.Algorithm,
|
||||
"no_model_call": true, "agent_offloaded": cpuQuality.Offloaded, "agent_id": cpuQuality.AgentID, "compute_ms": cpuQuality.ComputeMS, "fallback_reason": cpuQuality.FallbackReason,
|
||||
"evidence_alignment_mode": "diagnostic_only_semantic_grounding_by_qwen", "no_model_call": true, "agent_offloaded": cpuQuality.Offloaded, "agent_id": cpuQuality.AgentID, "compute_ms": cpuQuality.ComputeMS, "fallback_reason": cpuQuality.FallbackReason,
|
||||
})})
|
||||
if !cpuQuality.Result.Passed {
|
||||
if cpuRepairAttempts < 1 {
|
||||
@@ -339,7 +382,18 @@ func (e *Engine) synthesizeKnowledgeArticle(ctx context.Context, trigger string,
|
||||
e.Broker.Publish(model.Activity{Type: "article.cpu_quality.revision", Source: "brain", Phase: "knowledge-synthesis", NodeIDs: draft.SourceNodeIDs, Message: "Der modellfreie Quality-Layer fordert vor dem LLM-Review eine substanziellere Fassung an", Strength: .72, Metadata: articleRunMetadata(articleRunID, map[string]any{"trigger": trigger, "score": cpuQuality.Result.Score, "hard_failures": cpuQuality.Result.HardFailures, "recommendations": cpuQuality.Result.Recommendations, "repair_attempt": cpuRepairAttempts})})
|
||||
continue
|
||||
}
|
||||
e.Broker.Publish(model.Activity{Type: "article.draft.rejected", Source: "brain", Phase: "quality-gate", NodeIDs: draft.SourceNodeIDs, Message: "Der Artikel ist mathematisch/strukturell noch zu kurz, redundant oder nutzt die Evidenz nicht ausreichend", Strength: .46, Metadata: articleRunMetadata(articleRunID, map[string]any{"trigger": trigger, "reason": "article_cpu_quality_rejected", "score": cpuQuality.Result.Score, "hard_failures": cpuQuality.Result.HardFailures, "recommendations": cpuQuality.Result.Recommendations, "agent_offloaded": cpuQuality.Offloaded})})
|
||||
// A repair must never erase a stronger previous draft. This mattered in
|
||||
// analysis(21), where several CPU-requested rewrites became materially
|
||||
// shorter. Restore the strongest structurally evaluated version for final
|
||||
// diagnostics/rejection instead of silently keeping the regression.
|
||||
if hasBestCPUQuality && articleCPUQualityBetter(bestCPUQuality, cpuQuality.Result) {
|
||||
previousWords, previousScore := cpuQuality.Result.WordCount, cpuQuality.Result.Score
|
||||
draft = bestCPUDraft
|
||||
cpuQuality.Result = bestCPUQuality
|
||||
finalCPUQuality = bestCPUQuality
|
||||
e.Broker.Publish(model.Activity{Type: "article.cpu_quality.revision.regressed", Source: "brain", Phase: "quality-gate", NodeIDs: draft.SourceNodeIDs, Message: "Die CPU-Revision war schwächer als die Vorfassung; die bessere Fassung bleibt erhalten", Strength: .5, Metadata: articleRunMetadata(articleRunID, map[string]any{"trigger": trigger, "repair_word_count": previousWords, "repair_score": previousScore, "restored_word_count": bestCPUQuality.WordCount, "restored_score": bestCPUQuality.Score, "hard_failures": bestCPUQuality.HardFailures})})
|
||||
}
|
||||
e.Broker.Publish(model.Activity{Type: "article.draft.rejected", Source: "brain", Phase: "quality-gate", NodeIDs: draft.SourceNodeIDs, Message: "Der Artikel verfehlt nach einer gezielten Revision weiterhin harte mathematische Struktur-/Dichtegrenzen", Strength: .46, Metadata: articleRunMetadata(articleRunID, map[string]any{"trigger": trigger, "reason": "article_cpu_quality_rejected", "score": cpuQuality.Result.Score, "hard_failures": cpuQuality.Result.HardFailures, "recommendations": cpuQuality.Result.Recommendations, "agent_offloaded": cpuQuality.Offloaded})})
|
||||
return articleSynthesisOutcome{Skipped: true, Reason: "article_cpu_quality_rejected", Action: plan.Action, Title: draft.Title}, nil
|
||||
}
|
||||
|
||||
@@ -1265,11 +1319,12 @@ merge: Mehrere produktive Artikel überschneiden sich und sollten als Staging-En
|
||||
skip: Kein echter Mehrwert, bloße Dublette oder ein Thema, das auch nach realistischer Recherche keinen eigenständigen Helpdesk-Nutzen hätte. Fehlende recherchierbare Fakten sind allein kein skip-Grund: Wähle in diesem Fall create, update oder merge und setze needs_research=true mit einer präzisen ersten Suchfrage.
|
||||
|
||||
Artikeltyp-Regeln:
|
||||
- troubleshooting: konkreter Fehlerzustand, Fehlercode, Ausfall, Diagnose oder Wiederherstellung. Ein einzelner operativer Fehlercode ist troubleshooting, nicht how_to.
|
||||
- how_to: bewusst auszuführende Einrichtung, Konfiguration oder Prozedur ohne primären Fehlerzustand.
|
||||
- reference: mehrere Fehlercodes, Mechanismen, Statuswerte oder technische Zuordnungen, wenn kein einzelner Lösungsablauf im Mittelpunkt steht.
|
||||
- concept: technische Grundlagen und Zusammenhänge ohne operativen Ablauf.
|
||||
- troubleshooting: nur wenn ein konkreter Fehlerzustand, Fehlercode, Ausfall, Diagnose oder Wiederherstellung im Mittelpunkt steht UND die Quellen bereits einen realistischen ausführbaren Diagnose-/Recovery-Pfad tragen. Ein einzelner operativer Fehlercode ist troubleshooting, nicht how_to.
|
||||
- how_to: nur für eine bewusst auszuführende Einrichtung, Konfiguration oder Prozedur, deren konkrete Schritte aus den Quellen ableitbar sind.
|
||||
- reference: mehrere Fehlercodes, Mechanismen, Statuswerte, Profile, TTPs, Artefakte oder technische Zuordnungen, wenn kein einzelner Lösungsablauf im Mittelpunkt steht.
|
||||
- concept: technische Grundlagen, Architektur und Zusammenhänge ohne operativen Ablauf.
|
||||
- decision_guide: Auswahl oder Abgrenzung anhand belastbarer Kriterien.
|
||||
WICHTIG: Allgemeine Härtung, Prävention, Framework-/Standard-Zuordnung, Threat-Intelligence-Profile oder technische Mechanismen sind nicht automatisch troubleshooting. Wenn die Quellen keine mindestens drei konkreten belegbaren Schritte erwarten lassen, bevorzuge reference/concept/decision_guide statt einen künstlichen operationalen Artikel zu erzwingen.
|
||||
|
||||
Erfinde keine Fakten. Bevorzuge konkrete Problemlösung gegenüber technischer Meta-Analyse. target_article_id ist bei update/merge zwingend eine SOURCE_NODE_ID einer produktiven Quelle. source_node_ids dürfen nur IDs aus dem Kontext enthalten. Wenn notwendige Fakten fehlen, setze needs_research=true. Gib ausschließlich JSON nach Schema zurück.`
|
||||
}
|
||||
@@ -2233,7 +2288,7 @@ func (e *Engine) writeKnowledgeArticleDraft(sources []articleSource, plan model.
|
||||
"solution_step_count": countMarkdownNumberedSteps(draft.Answer), "validation_step_count": len(cleanArticleItems(draft.Validation)),
|
||||
"productive_source_count": productionCount, "ai_source_count": aiCount, "production_ratio": productionRatio,
|
||||
"source_fingerprint": fingerprint, "synthesis_model": e.Cfg.ArticleSynthesisModel, "review_model": e.Cfg.ArticleReviewModel,
|
||||
"pipeline": "adaptive_generate_review/v3-quality-v2",
|
||||
"pipeline": "adaptive_generate_review/v5-quality-gate-v12",
|
||||
"cpu_quality_algorithm": cpuQuality.Algorithm, "cpu_quality_score": cpuQuality.Score, "cpu_quality_passed": cpuQuality.Passed,
|
||||
"cpu_quality_word_count": cpuQuality.WordCount, "cpu_quality_section_count": cpuQuality.SectionCount,
|
||||
},
|
||||
@@ -2267,7 +2322,7 @@ func (e *Engine) writeKnowledgeArticleDraft(sources []articleSource, plan model.
|
||||
"source_nodes": externalIDsFromArticleSources(sources), "source_node_ids": sourceIDs,
|
||||
"productive_source_count": productionCount, "ai_source_count": aiCount, "production_ratio": productionRatio,
|
||||
"generation_depth": generationDepth, "confidence": draft.Confidence, "open_questions": draft.OpenQuestions, "language": articleLanguageTag(e.Cfg.ArticleLanguage), "source_fingerprint": fingerprint,
|
||||
"synthesis_model": e.Cfg.ArticleSynthesisModel, "review_model": e.Cfg.ArticleReviewModel, "pipeline": "adaptive_generate_review/v3-quality-v2",
|
||||
"synthesis_model": e.Cfg.ArticleSynthesisModel, "review_model": e.Cfg.ArticleReviewModel, "pipeline": "adaptive_generate_review/v5-quality-gate-v12",
|
||||
"knowledge_brief": brief, "research_query": plan.ResearchQuery, "research_material": evidence,
|
||||
"grounded_research_evidence": researchEvidenceMetadata(groundedResearch),
|
||||
"article_review": quality, "article_cpu_quality": cpuQuality, "review_repair_attempts": repairAttempts,
|
||||
|
||||
Reference in New Issue
Block a user