This commit is contained in:
@@ -3,6 +3,7 @@ package engine
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -179,6 +180,15 @@ func TestEnrichRelationWithOnlyTwoSourcesDoesNotCreateArticle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSynthesisResearchesUnclearKnowledgeThenLearnsAndLinksArticle(t *testing.T) {
|
||||
sourceA := graph.ID("knowledge", "A")
|
||||
sourceB := graph.ID("knowledge", "B")
|
||||
sourceC := graph.ID("knowledge", "C")
|
||||
pageServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(`<html><head><title>Hersteller-Dokumentation VPN Gateway</title></head><body><main><h1>Gateway-Verbindung prüfen</h1><p>Prüfen Sie zuerst die Internetverbindung des Rechners und kontrollieren Sie anschließend die konfigurierte Gateway-Adresse.</p><p>Testen Sie die Erreichbarkeit des Gateways mit dem vom Hersteller dokumentierten Verbindungstest. Starten Sie danach den VPN-Client neu.</p><p>Die Prüfung ist erfolgreich, wenn der Tunnel aufgebaut wird und eine interne Testressource erreichbar ist. Erfassen Sie bei einem anhaltenden Fehler Zeitstempel und Fehlermeldung für die Eskalation.</p></main></body></html>`))
|
||||
}))
|
||||
defer pageServer.Close()
|
||||
|
||||
var chatCalls int
|
||||
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -204,11 +214,17 @@ func TestSynthesisResearchesUnclearKnowledgeThenLearnsAndLinksArticle(t *testing
|
||||
case 2:
|
||||
content = `{"action":"create","target_article_id":"","reason":"Die produktiven Beiträge lassen sich konsolidieren.","expected_value":"Ein vollständiger Diagnoseablauf.","article_type":"troubleshooting","source_node_ids":[],"missing_information":[],"contradictions":[],"needs_research":false,"research_query":""}`
|
||||
case 3:
|
||||
content = `{"topic":"VPN Gateway","purpose":"VPN-Fehler beheben","scope":[],"facts":[],"symptoms":[{"text":"Der VPN-Tunnel wird nicht aufgebaut.","source_refs":["A"]}],"prerequisites":[],"solution_steps":[{"text":"Internetverbindung prüfen.","source_refs":["A"]}],"validation_steps":[],"troubleshooting":[],"contradictions":[{"topic":"Offizieller Gateway-Prüfweg","statements":["Die internen Beiträge nennen keinen verlässlichen Gateway-Test."],"source_refs":["A","B","C"],"resolution":"","needs_research":true,"research_query":"Hersteller VPN Gateway Erreichbarkeit Diagnose"}],"missing_information":["Offizieller Gateway-Prüfweg"],"research_queries":["Hersteller VPN Gateway Erreichbarkeit Diagnose"],"ready_for_article":false}`
|
||||
content = fmt.Sprintf(`{"topic":"VPN Gateway","purpose":"VPN-Fehler beheben","scope":[],"facts":[],"symptoms":[{"text":"Der VPN-Tunnel wird nicht aufgebaut.","source_refs":[%q]}],"prerequisites":[],"solution_steps":[{"text":"Internetverbindung prüfen.","source_refs":[%q]}],"validation_steps":[],"troubleshooting":[],"contradictions":[{"topic":"Offizieller Gateway-Prüfweg","statements":["Die internen Beiträge nennen keinen verlässlichen Gateway-Test."],"source_refs":[%q,%q,%q],"resolution":"","severity":"critical","needs_research":true,"research_query":"VPN Gateway Erreichbarkeit Hersteller Diagnose"}],"critical_gaps":[{"id":"G-C-1","description":"Ein belastbarer Gateway-Prüf- und Validierungsweg fehlt.","reason":"Ohne ihn ist die Anleitung nicht ausführbar.","research_queries":["VPN Gateway Erreichbarkeit Hersteller Diagnose"]}],"optional_gaps":[],"resolved_gaps":[],"missing_information":["Ein belastbarer Gateway-Prüf- und Validierungsweg fehlt."],"research_queries":["VPN Gateway Erreichbarkeit Hersteller Diagnose"],"ready_for_article":false}`, sourceA, sourceA, sourceA, sourceB, sourceC)
|
||||
case 4:
|
||||
content = `{"topic":"VPN Gateway","purpose":"VPN-Fehler beheben","scope":[{"text":"Gilt für den dokumentierten VPN-Client.","source_refs":["A"]}],"facts":[{"text":"Der Hersteller empfiehlt, die Gateway-Adresse und die Netzwerkverbindung zu prüfen.","source_refs":["R1"]}],"symptoms":[{"text":"Der VPN-Tunnel wird nicht aufgebaut.","source_refs":["A"]}],"prerequisites":[{"text":"Fehlermeldung und Zeitpunkt liegen vor.","source_refs":["C"]}],"solution_steps":[{"text":"Prüfen Sie die Internetverbindung.","source_refs":["A"]},{"text":"Prüfen Sie die konfigurierte Gateway-Adresse und deren Erreichbarkeit.","source_refs":["R1"]},{"text":"Starten Sie den VPN-Client neu.","source_refs":["B"]}],"validation_steps":[{"text":"Der VPN-Tunnel wird aufgebaut.","source_refs":["B"]}],"troubleshooting":[{"text":"Bei anhaltendem Fehler mit Zeitstempel und Meldung eskalieren.","source_refs":["C"]}],"contradictions":[],"missing_information":[],"research_queries":[],"ready_for_article":true}`
|
||||
content = `{"questions":[{"gap_id":"G-C-1","question":"Wie werden Gateway-Adresse und VPN-Verbindung laut Hersteller geprüft und validiert?","critical":true,"expect_actionable":true,"queries_de":["VPN Gateway Verbindung prüfen Hersteller Dokumentation"],"queries_en":[],"preferred_domains":[]}]}`
|
||||
case 5:
|
||||
content = `{"title":"VPN-Gateway-Verbindung diagnostizieren","problem_description":"Der VPN-Client kann keine Verbindung zum konfigurierten Gateway aufbauen. Der Tunnel bleibt getrennt und der Remotezugriff ist nicht möglich.","scope":"Die Anleitung gilt für den dokumentierten VPN-Client und das konfigurierte zentrale Gateway.","symptoms":["Der VPN-Tunnel wird nicht aufgebaut.","Der Client meldet ein nicht erreichbares Gateway."],"prerequisites":["Fehlermeldung und Zeitpunkt des Fehlers liegen vor."],"solution_steps":["Prüfen Sie die Internetverbindung des Rechners.","Kontrollieren Sie die konfigurierte Gateway-Adresse und testen Sie deren Erreichbarkeit.","Starten Sie den VPN-Client neu und wiederholen Sie den Verbindungsaufbau.","Eskalieren Sie einen anhaltenden Fehler mit Zeitstempel und Fehlermeldung."],"validation_steps":["Der VPN-Tunnel wird aufgebaut.","Eine interne Ressource ist erreichbar."],"troubleshooting":["Prüfen Sie bei erneutem Fehler die erfasste Meldung und den dokumentierten Eskalationsweg."],"categories":["Netzwerk","VPN"],"keywords":["VPN","Gateway"],"open_questions":[]}`
|
||||
content = `{"assessments":[{"index":1,"relevant":true,"relevance":0.94,"source_quality":"primary","source_quality_score":0.91,"actionable":true,"covered_gap_ids":["G-C-1"],"reason":"Direkte Herstelleranleitung mit Prüf- und Validierungsschritten."}]}`
|
||||
case 6:
|
||||
content = `{"assessments":[{"index":1,"relevant":true,"relevance":0.97,"source_quality":"primary","source_quality_score":0.94,"actionable":true,"covered_gap_ids":["G-C-1"],"reason":"Der Volltext enthält konkrete Diagnose-, Neustart- und Validierungsschritte."}]}`
|
||||
case 7:
|
||||
content = fmt.Sprintf(`{"topic":"VPN Gateway","purpose":"VPN-Fehler beheben","scope":[{"text":"Gilt für den dokumentierten VPN-Client.","source_refs":[%q]}],"facts":[{"text":"Gateway-Adresse und Netzwerkverbindung müssen geprüft werden.","source_refs":["R1"]}],"symptoms":[{"text":"Der VPN-Tunnel wird nicht aufgebaut.","source_refs":[%q]}],"prerequisites":[{"text":"Fehlermeldung und Zeitpunkt liegen vor.","source_refs":[%q]}],"solution_steps":[{"text":"Prüfen Sie die Internetverbindung.","source_refs":[%q,"R1"]},{"text":"Prüfen Sie die konfigurierte Gateway-Adresse und deren Erreichbarkeit.","source_refs":["R1"]},{"text":"Starten Sie den VPN-Client neu.","source_refs":[%q,"R1"]}],"validation_steps":[{"text":"Prüfen Sie, ob der VPN-Tunnel aufgebaut wird und eine interne Ressource erreichbar ist.","source_refs":["R1"]}],"troubleshooting":[{"text":"Bei anhaltendem Fehler mit Zeitstempel und Meldung eskalieren.","source_refs":[%q,"R1"]}],"contradictions":[],"critical_gaps":[],"optional_gaps":[{"id":"G-O-1","description":"Produktspezifische Screenshots fehlen.","reason":"Für die Ausführung nicht erforderlich.","research_queries":[]}],"resolved_gaps":[{"id":"G-C-1","description":"Gateway-Prüf- und Validierungsweg","source_refs":["R1"]}],"missing_information":["Produktspezifische Screenshots fehlen."],"research_queries":[],"ready_for_article":true}`, sourceA, sourceA, sourceC, sourceA, sourceB, sourceC)
|
||||
case 8:
|
||||
content = `{"title":"VPN-Gateway-Verbindung diagnostizieren","problem_description":"Der VPN-Client kann keine Verbindung zum konfigurierten Gateway aufbauen. Der Tunnel bleibt getrennt und der Remotezugriff ist nicht möglich.","scope":"Die Anleitung gilt für den dokumentierten VPN-Client und das konfigurierte zentrale Gateway.","symptoms":["Der VPN-Tunnel wird nicht aufgebaut.","Der Client meldet ein nicht erreichbares Gateway."],"key_points":[],"decision_criteria":[],"prerequisites":["Fehlermeldung und Zeitpunkt des Fehlers liegen vor."],"solution_steps":["Prüfen Sie die Internetverbindung des Rechners.","Kontrollieren Sie die konfigurierte Gateway-Adresse und testen Sie deren Erreichbarkeit.","Starten Sie den VPN-Client neu und wiederholen Sie den Verbindungsaufbau.","Eskalieren Sie einen anhaltenden Fehler mit Zeitstempel und Fehlermeldung."],"validation_steps":["Der VPN-Tunnel wird aufgebaut.","Eine interne Ressource ist erreichbar."],"troubleshooting":["Prüfen Sie bei erneutem Fehler die erfasste Meldung und den dokumentierten Eskalationsweg."],"categories":["Netzwerk","VPN"],"keywords":["VPN","Gateway"],"open_questions":[]}`
|
||||
default:
|
||||
content = `{"accepted":true,"confidence":0.93,"meta_content_detected":false,"unsupported_claims":[],"issues":[]}`
|
||||
}
|
||||
@@ -219,9 +235,10 @@ func TestSynthesisResearchesUnclearKnowledgeThenLearnsAndLinksArticle(t *testing
|
||||
}))
|
||||
defer ollama.Close()
|
||||
|
||||
pageURL := pageServer.URL + "/vpn-gateway"
|
||||
searx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"results":[{"title":"Hersteller-Dokumentation","url":"https://vendor.example/vpn-gateway","content":"Prüfen Sie die konfigurierte Gateway-Adresse und die Netzwerkverbindung. Starten Sie danach den Client neu."}]}`))
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{{"title": "Hersteller-Dokumentation VPN Gateway", "url": pageURL, "content": "Konkrete Herstelleranleitung zur Prüfung von Gateway-Adresse, Netzwerkverbindung und Tunnelaufbau."}}})
|
||||
}))
|
||||
defer searx.Close()
|
||||
|
||||
@@ -245,8 +262,8 @@ func TestSynthesisResearchesUnclearKnowledgeThenLearnsAndLinksArticle(t *testing
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := config.Config{DataDir: data, KnowledgeDirs: []string{knowledge}, StagingDirs: []string{staging}, OllamaURL: ollama.URL, ChatModel: "qwen3:8b", EmbeddingModel: "embeddinggemma", SearXNGURL: searx.URL, ResearchEnabled: true, SimilarityThreshold: .5, RelationThreshold: .7, ArticleSynthesisEnabled: true, ArticleMinSources: 3, ArticleMaxSources: 6, ArticleMinProductionRatio: .7, ArticleMaxGenerationDepth: 2, ArticleMinConfidence: .7, ArticleMinTextChars: 80, ArticleMinAnswerChars: 180, ArticleMaxResearchQueries: 3, ArticleResearchResults: 4, MaxContextChars: 16000}
|
||||
broker := activity.New(100)
|
||||
cfg := config.Config{DataDir: data, KnowledgeDirs: []string{knowledge}, StagingDirs: []string{staging}, OllamaURL: ollama.URL, ChatModel: "qwen3:8b", EmbeddingModel: "embeddinggemma", SearXNGURL: searx.URL, ResearchEnabled: true, SimilarityThreshold: .5, RelationThreshold: .7, ArticleSynthesisEnabled: true, ArticleMinSources: 3, ArticleMaxSources: 6, ArticleMinProductionRatio: .7, ArticleMaxGenerationDepth: 2, ArticleMinConfidence: .7, ArticleMinTextChars: 80, ArticleMinAnswerChars: 180, ArticleMaxResearchQueries: 2, ArticleResearchResults: 4, ArticleResearchRounds: 2, ArticleResearchFetchResults: 1, ArticleResearchMinRelevance: .6, ArticleResearchMinQuality: .4, ArticleResearchPageMaxBytes: 1 << 20, ArticleResearchPageMaxChars: 8000, ArticleResearchFetchTimeout: time.Second, ArticleResearchAllowPrivate: true, MaxContextChars: 16000}
|
||||
broker := activity.New(200)
|
||||
e := New(cfg, g, broker)
|
||||
if err := e.Scan(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -274,7 +291,30 @@ func TestSynthesisResearchesUnclearKnowledgeThenLearnsAndLinksArticle(t *testing
|
||||
if vector, ok := g.Vector(articleNodeID); !ok || len(vector) == 0 {
|
||||
t.Fatal("new article was not learned immediately")
|
||||
}
|
||||
researchID := graph.ID("external", "https://vendor.example/vpn-gateway")
|
||||
researchID := graph.ID("external", pageURL)
|
||||
if vector, ok := g.Vector(researchID); !ok || len(vector) == 0 {
|
||||
t.Fatal("accepted web evidence was not learned immediately")
|
||||
}
|
||||
researchNode, ok := g.GetNode(researchID)
|
||||
if !ok || !matchesCategories(researchNode, []string{"VPN"}) {
|
||||
t.Fatalf("accepted web evidence did not inherit source categories: %+v", researchNode)
|
||||
}
|
||||
evidenceFiles, err := filepath.Glob(filepath.Join(data, "research-evidence", "*.json"))
|
||||
if err != nil || len(evidenceFiles) != 1 {
|
||||
t.Fatalf("expected persisted full-text research evidence, files=%v err=%v", evidenceFiles, err)
|
||||
}
|
||||
var priorSources []articleSource
|
||||
for _, externalID := range []string{"A", "B", "C"} {
|
||||
node, ok := g.LookupExternal(externalID)
|
||||
if !ok {
|
||||
t.Fatalf("missing source node %s", externalID)
|
||||
}
|
||||
priorSources = append(priorSources, articleSource{Node: node})
|
||||
}
|
||||
reused := e.researchEvidenceForSources(priorSources)
|
||||
if len(reused) != 1 || reused[0].URL != pageURL || !strings.Contains(reused[0].Content, "Gateway-Adresse") {
|
||||
t.Fatalf("accepted full-text evidence was not reusable: %+v", reused)
|
||||
}
|
||||
linked := false
|
||||
for _, edge := range g.Snapshot().Edges {
|
||||
if edge.Source == articleNodeID && edge.Target == researchID && edge.Type == "grounded_by" {
|
||||
@@ -283,26 +323,33 @@ func TestSynthesisResearchesUnclearKnowledgeThenLearnsAndLinksArticle(t *testing
|
||||
}
|
||||
}
|
||||
if !linked {
|
||||
t.Fatal("new article is not linked to its research evidence")
|
||||
t.Fatal("new article is not linked to its accepted full-text research evidence")
|
||||
}
|
||||
if chatCalls != 6 {
|
||||
t.Fatalf("expected relation, plan, two consolidations, article and quality calls, got %d", chatCalls)
|
||||
if chatCalls != 9 {
|
||||
t.Fatalf("expected relation, plan, brief, research plan, two relevance gates, reconsolidation, article and quality calls, got %d", chatCalls)
|
||||
}
|
||||
var resultEvent, ingestEvent *model.Activity
|
||||
var resultEvent, candidateEvent, fetchEvent, acceptedEvent, ingestEvent, learnedEvent, roundEvent *model.Activity
|
||||
for _, event := range broker.Recent() {
|
||||
event := event
|
||||
switch event.Type {
|
||||
case "article.research.results":
|
||||
resultEvent = &event
|
||||
case "article.research.candidates":
|
||||
candidateEvent = &event
|
||||
case "article.research.fetch.completed":
|
||||
fetchEvent = &event
|
||||
case "article.research.evidence.accepted":
|
||||
acceptedEvent = &event
|
||||
case "article.research.ingested":
|
||||
ingestEvent = &event
|
||||
case "article.research.learned":
|
||||
learnedEvent = &event
|
||||
case "article.research.round.completed":
|
||||
roundEvent = &event
|
||||
}
|
||||
}
|
||||
if resultEvent == nil || ingestEvent == nil {
|
||||
t.Fatalf("expected visible SearXNG result and ingest events, results=%v ingest=%v", resultEvent != nil, ingestEvent != nil)
|
||||
}
|
||||
if count, _ := resultEvent.Metadata["result_count"].(int); count != 1 {
|
||||
t.Fatalf("unexpected result count metadata: %#v", resultEvent.Metadata["result_count"])
|
||||
if resultEvent == nil || candidateEvent == nil || fetchEvent == nil || acceptedEvent == nil || ingestEvent == nil || learnedEvent == nil || roundEvent == nil {
|
||||
t.Fatalf("expected complete visible research pipeline, result=%v candidate=%v fetch=%v accepted=%v ingest=%v learned=%v round=%v", resultEvent != nil, candidateEvent != nil, fetchEvent != nil, acceptedEvent != nil, ingestEvent != nil, learnedEvent != nil, roundEvent != nil)
|
||||
}
|
||||
resultNodeIDs, _ := ingestEvent.Metadata["result_node_ids"].([]string)
|
||||
if len(resultNodeIDs) != 1 || resultNodeIDs[0] != researchID {
|
||||
|
||||
Reference in New Issue
Block a user