// Package wiki implements the "wiki" variant of KnowledgeCompiler: a // document artifact pipeline MAP -> REDUCE -> PLAN -> REFINE that produces a // wiki-style page (and supporting section products) with in-memory state only. // The stage semantics are aligned with the Python wiki.py design, but the Go // port keeps all intermediate artifacts in memory instead of persisting them to // ES between stages. package wiki import ( "bytes" "context" "encoding/json" "fmt" "math" "net/url" "regexp" "sort" "strings" "ragflow/internal/ingestion/component/knowledge_compiler/common" "ragflow/internal/ingestion/component/knowledge_compiler/structure" ) type wikiPipeline struct { ctx context.Context deps common.Deps param common.Param inputs common.Inputs docID string tenantID string datasetID string llmID string mapExtracts []wikiExtract reduced wikiExtract plan wikiPlan pages []wikiPageResult } type wikiExtract struct { Entities []wikiEntity `json:"entities"` Concepts []wikiConcept `json:"concepts"` Claims []wikiClaim `json:"claims"` Relations []wikiRelation `json:"relations"` Topics []string `json:"topics"` } type wikiEntity struct { Name string `json:"name"` Type string `json:"type"` Aliases []string `json:"aliases,omitempty"` SourceChunkIDs []string `json:"source_chunk_ids,omitempty"` } type wikiConcept struct { Term string `json:"term"` Definition string `json:"definition_excerpt"` SourceChunkIDs []string `json:"source_chunk_ids,omitempty"` } type wikiClaim struct { Statement string `json:"statement"` Subject string `json:"subject"` Confidence string `json:"confidence"` SourceChunkIDs []string `json:"source_chunk_ids,omitempty"` } type wikiRelation struct { From string `json:"from"` To string `json:"to"` Type string `json:"type"` SourceChunkIDs []string `json:"source_chunk_ids,omitempty"` } type wikiPlan struct { Title string `json:"title"` Slug string `json:"slug"` Lead string `json:"lead"` Sections []wikiPlanSection `json:"sections"` PageType string `json:"page_type,omitempty"` Topic string `json:"topic,omitempty"` Entities []string `json:"entity_names,omitempty"` Related []string `json:"related_kb_pages,omitempty"` Pages []wikiPlanPage `json:"pages,omitempty"` } type wikiPlanSection struct { Heading string `json:"heading"` Points []string `json:"points"` } type wikiPlanPage struct { Action string `json:"action"` Slug string `json:"slug"` Title string `json:"title"` PageType string `json:"page_type"` Topic string `json:"topic"` EntityNames []string `json:"entity_names"` RelatedKB []string `json:"related_kb_pages"` Priority int `json:"priority"` Lead string `json:"lead"` Sections []wikiPlanSection `json:"sections"` } const ( wikiPlanUpdateThreshold = 0.92 wikiPlanMaybeThreshold = 0.78 wikiPlanSearchTopK = 3 wikiMergeShrinkRatio = 0.7 ) // Run executes the wiki variant. func Run(ctx context.Context, deps common.Deps, param common.Param, inputs common.Inputs) (common.Outputs, error) { docID := inputs.DocID llmID := firstNonEmpty(inputs.LLMID, param.LLMID) if docID == "" { docID = "unknown" } p := &wikiPipeline{ ctx: ctx, deps: deps, param: param, inputs: inputs, docID: docID, tenantID: deps.TenantID, datasetID: deps.DatasetID, llmID: llmID, } if err := p.run(); err != nil { return common.Outputs{}, err } products := buildWikiPageProducts(p.tenantID, p.docID, p.pages) for i := range products { if products[i].Meta == nil { products[i].Meta = map[string]any{} } } if len(products) > 0 { if deps.Chat == nil { return common.Outputs{}, fmt.Errorf("wiki: chat model required for page generation") } if deps.Embed == nil { return common.Outputs{}, fmt.Errorf("wiki: embedder required (page + sections must carry vectors)") } texts := make([]string, len(products)) for i, p := range products { texts[i] = p.Content } vectors, err := deps.Embed.Encode(ctx, texts) if err != nil { return common.Outputs{}, err } for i := range products { if i < len(vectors) { products[i].Vector = vectors[i] } } } store := common.NewMemStore() decider := structure.CosineDecider{Threshold: param.SimilarityThreshold} stats := structure.MergeStats{} for _, p := range products { s, err := structure.MergeIntoStore(ctx, store, decider, []common.Product{p}) if err != nil { return common.Outputs{}, err } stats.Inserted += s.Inserted stats.Updated += s.Updated stats.DuplicatesDropped += s.DuplicatesDropped } prod := store.Snapshot() if param.EnableHistoricalDedup || len(inputs.HistoricalCandidates) > 0 { survivors, dropped, err := dedupHistorical(ctx, deps, param, p.tenantID, p.datasetID, inputs.HistoricalCandidates, prod) if err != nil { return common.Outputs{}, err } prod = survivors stats.DuplicatesDropped += dropped } // Buffer every product in one slice; the component merges them into the // upstream chunk stream (matching Python, which appends compiled units onto // the chunk list). out := common.Outputs{ Products: prod, DuplicatesDropped: stats.DuplicatesDropped, } return out, nil } func (p *wikiPipeline) run() error { if len(p.inputs.Chunks) == 0 { return nil } if err := p.runMap(); err != nil { return err } p.reduced = reduceExtracts(p.mapExtracts) plan, err := p.runPlan() if err != nil { return err } p.plan = plan pages, err := p.runRefine() if err != nil { return err } p.pages = pages return nil } func (p *wikiPipeline) runMap() error { batches := common.PackBatches(p.inputs.Chunks, 4096, p.deps.Tokenizer) for _, batch := range batches { if err := p.ctx.Err(); err != nil { return err } extract, err := p.mapBatch(batch) if err != nil { return err } p.mapExtracts = append(p.mapExtracts, extract) } return nil } func (p *wikiPipeline) mapBatch(batch []common.Chunk) (wikiExtract, error) { parserConfig, _ := p.inputs.VariantSpecific["parser_config"].(map[string]any) user, _ := buildWikiMapPrompt(p.docID, batch, parserConfig, p.param.Language) raw, err := common.GenJSON(p.ctx, p.deps.Chat, common.ChatRequest{ LLMID: p.llmID, SystemPrompt: wikiMapSystem, UserPrompt: user, }) if err != nil { return wikiExtract{}, err } return parseWikiExtract(raw), nil } func (p *wikiPipeline) runPlan() (wikiPlan, error) { batches := packWikiPlanBatches(p.reduced, wikiPlanTokenBudget) if len(batches) == 0 { batches = []wikiExtract{p.reduced} } plans := make([]wikiPlan, 0, len(batches)) for i, batch := range batches { plan, err := p.runPlanBatch(batch, i+1, len(batches)) if err != nil { return wikiPlan{}, err } plans = append(plans, plan) } if len(plans) == 1 { plan := normalizeWikiPlan(plans[0], p.docID, p.reduced) return p.reconcilePlan(plan) } plan, err := p.mergePlanCandidates(plans) if err != nil { return wikiPlan{}, err } return p.reconcilePlan(plan) } func (p *wikiPipeline) runRefine() ([]wikiPageResult, error) { pages := normalizeWikiPlanPages(p.plan.Pages, p.reduced) if len(pages) == 0 { return nil, nil } pageTitles := map[string]string{} allPlanSlugs := make([]string, 0, len(pages)) for _, page := range pages { if page.Slug == "" { continue } allPlanSlugs = append(allPlanSlugs, page.Slug) pageTitles[page.Slug] = page.Title } entityLookup := buildWikiEntityLookup(p.reduced.Entities) conceptLookup := buildWikiConceptLookup(p.reduced.Concepts) results := make([]wikiPageResult, 0, len(pages)) for _, planItem := range pages { if p.ctx.Err() != nil { return nil, p.ctx.Err() } evidence := assembleWikiPageEvidence(planItem, p.reduced.Claims, entityLookup, conceptLookup) sourceChunkIDs := collectWikiEvidenceChunkIDs(evidence) sourceContext := buildSourceContext(p.inputs.Chunks, sourceChunkIDs) if strings.TrimSpace(sourceContext) == "" { sourceContext = buildSourceContext(p.inputs.Chunks, p.reduced.sourceChunkIDs()) } available := make([]string, 0, len(allPlanSlugs)) for _, slug := range allPlanSlugs { if slug != planItem.Slug { available = append(available, "- [["+slug+"]]") } } if len(available) == 0 { available = []string{"(none — this is the only page)"} } var existing *common.WikiPageCandidate var err error if strings.EqualFold(planItem.Action, "UPDATE") && p.deps.WikiPages != nil { existing, err = p.deps.WikiPages.GetPageBySlug(p.ctx, p.tenantID, p.datasetID, planItem.Slug) if err != nil { return nil, err } } existingSection := "" existingRaw := "" if existing != nil { existingRaw = firstNonEmpty(existing.ContentMDRaw, existing.ContentMD) if strings.TrimSpace(existingRaw) != "" { existingSection = "## Existing page content (UPDATE — integrate new evidence into this)\n\n" + existingRaw + "\n" } } user := renderWikiTemplate(wikiRefineWriterUserTemplate, map[string]string{ "action": firstNonEmpty(planItem.Action, "CREATE"), "slug": planItem.Slug, "title": firstNonEmpty(planItem.Title, planItem.Slug), "page_type": firstNonEmpty(planItem.PageType, "concept"), "all_plan_slugs": strings.Join(available, "\n"), "existing_section": existingSection, "source_context": sourceContext, "evidence_count": fmt.Sprintf("%d", len(evidence)), "evidence_blocks": formatWikiEvidenceBlocks(evidence), }) resp, err := p.deps.Chat.Chat(p.ctx, common.ChatRequest{ LLMID: p.llmID, SystemPrompt: buildWikiRefineWriterSystem(""), UserPrompt: user, }) if err != nil { return nil, err } if resp == nil { return nil, fmt.Errorf("knowledge_compiler: wiki refine returned no response") } contentRaw := strings.TrimSpace(firstNonEmpty(resp.Content)) if contentRaw == "" { contentRaw = "# " + firstNonEmpty(planItem.Title, planItem.Slug) + "\n\n(Page generation produced no content.)" } if strings.TrimSpace(existingRaw) != "" { contentRaw, err = p.mergeWikiPageContent(existingRaw, contentRaw, planItem.Slug) if err != nil { return nil, err } } contentRendered, outlinks := transformWikiLinks(contentRaw, firstNonEmpty(p.datasetID, p.docID), pageTitles) sourceDocIDs := collectWikiSourceDocIDs(p.inputs.Chunks, sourceChunkIDs, p.docID) summary := firstParagraph(contentRendered) if summary == "" { summary = firstNonEmpty(planItem.Title, planItem.Slug) } topic := firstNonEmpty(planItem.Topic, planItem.Title, planItem.Slug) results = append(results, wikiPageResult{ Slug: planItem.Slug, Title: firstNonEmpty(planItem.Title, planItem.Slug), PageType: firstNonEmpty(planItem.PageType, "concept"), Topic: topic, Action: firstNonEmpty(planItem.Action, "CREATE"), EntityNames: uniqueStrings(planItem.EntityNames), RelatedKBPages: uniqueStrings(planItem.RelatedKB), ContentRaw: contentRaw, Content: contentRendered, Summary: summary, Outlinks: outlinks, SourceChunkIDs: sourceChunkIDs, SourceDocIDs: sourceDocIDs, }) } return results, nil } func (p *wikiPipeline) runPlanBatch(batch wikiExtract, batchIndex, batchTotal int) (wikiPlan, error) { user := renderWikiTemplate(wikiPlanBatchUserTemplate, map[string]string{ "doc_id": p.docID, "batch_index": fmt.Sprintf("%d", batchIndex), "batch_total": fmt.Sprintf("%d", batchTotal), "entities": mustJSON(batch.Entities), "concepts": mustJSON(batch.Concepts), "claims": mustJSON(batch.Claims), "relations": mustJSON(batch.Relations), "topics": mustJSON(batch.Topics), }) raw, err := common.GenJSON(p.ctx, p.deps.Chat, common.ChatRequest{ LLMID: p.llmID, SystemPrompt: wikiPlanSystem, UserPrompt: user, }) if err != nil { return wikiPlan{}, err } return parseWikiPlan(raw, p.docID, batch), nil } func (p *wikiPipeline) mergePlanCandidates(plans []wikiPlan) (wikiPlan, error) { user := renderWikiTemplate(wikiPlanMergeUserTemplate, map[string]string{ "doc_id": p.docID, "candidates": mustPrettyJSON(plans), }) raw, err := common.GenJSON(p.ctx, p.deps.Chat, common.ChatRequest{ LLMID: p.llmID, SystemPrompt: wikiPlanSystem, UserPrompt: user, }) if err != nil { return wikiPlan{}, err } return normalizeWikiPlan(parseWikiPlan(raw, p.docID, p.reduced), p.docID, p.reduced), nil } func (p *wikiPipeline) reconcilePlan(plan wikiPlan) (wikiPlan, error) { if p.deps.WikiPages == nil || p.deps.Embed == nil || strings.TrimSpace(p.datasetID) == "" || len(plan.Pages) == 0 { return plan, nil } queryTexts := make([]string, 0, len(plan.Pages)) pageIdx := make([]int, 0, len(plan.Pages)) for i, page := range plan.Pages { page = normalizeWikiPlanPage(page) plan.Pages[i] = page query := buildWikiPageQueryText(page) if strings.TrimSpace(query) == "" { continue } queryTexts = append(queryTexts, query) pageIdx = append(pageIdx, i) } if len(queryTexts) == 0 { return plan, nil } vectors, err := p.deps.Embed.Encode(p.ctx, queryTexts) if err != nil { return wikiPlan{}, err } for i, idx := range pageIdx { if idx >= len(plan.Pages) || i >= len(vectors) { continue } page := plan.Pages[idx] existing, err := p.reconcilePlanPage(page, vectors[i]) if err != nil { return wikiPlan{}, err } if existing == nil { continue } page.Action = "UPDATE" page.Slug = firstNonEmpty(existing.Slug, page.Slug) page.Title = firstNonEmpty(existing.Title, page.Title) page.PageType = firstNonEmpty(existing.PageType, page.PageType) page.Topic = firstNonEmpty(existing.Topic, page.Topic) page.RelatedKB = mergeStrings(page.RelatedKB, existing.RelatedKBPages) page.EntityNames = mergeStrings(page.EntityNames, existing.EntityNames) plan.Pages[idx] = page } plan.Pages = normalizeWikiPlanPages(plan.Pages, p.reduced) return plan, nil } func (p *wikiPipeline) reconcilePlanPage(page wikiPlanPage, queryVec []float32) (*common.WikiPageCandidate, error) { if p.deps.WikiPages == nil { return nil, nil } if hit, err := p.deps.WikiPages.GetPageBySlug(p.ctx, p.tenantID, p.datasetID, page.Slug); err != nil { return nil, err } else if hit != nil { return hit, nil } cands, err := p.deps.WikiPages.FindSimilarPages(p.ctx, p.tenantID, p.datasetID, queryVec, wikiPlanSearchTopK) if err != nil || len(cands) == 0 { return nil, err } cands = rerankWikiPlanCandidates(page, cands) targetNames := normalizedStringSet(page.EntityNames) pageTitle := normKey(page.Title) pageTopic := normKey(page.Topic) maybe := make([]common.WikiPageCandidate, 0, len(cands)) for i := range cands { cand := cands[i] if cand.Score >= wikiPlanUpdateThreshold { return &cand, nil } if cand.Score < wikiPlanMaybeThreshold { continue } if normKey(cand.Title) == pageTitle || normKey(cand.Topic) == pageTopic { return &cand, nil } if intersectsNormalized(targetNames, cand.EntityNames) { return &cand, nil } maybe = append(maybe, cand) } if len(maybe) > 0 { return p.resolveMaybePlanPage(page, maybe) } return nil, nil } func rerankWikiPlanCandidates(page wikiPlanPage, candidates []common.WikiPageCandidate) []common.WikiPageCandidate { if len(candidates) <= 1 { return candidates } type scored struct { candidate common.WikiPageCandidate boost float64 } targetNames := normalizedStringSet(page.EntityNames) pageType := normKey(page.PageType) pageTitle := normKey(page.Title) pageTopic := normKey(page.Topic) scoredCands := make([]scored, 0, len(candidates)) for _, cand := range candidates { boost := cand.Score if pageType != "" && normKey(cand.PageType) == pageType { boost += 0.08 } if pageTitle != "" && normKey(cand.Title) == pageTitle { boost += 0.12 } if pageTopic != "" && normKey(cand.Topic) == pageTopic { boost += 0.08 } if overlap := normalizedOverlapCount(targetNames, cand.EntityNames); overlap > 0 { boost += 0.05 * float64(overlap) } scoredCands = append(scoredCands, scored{candidate: cand, boost: boost}) } sort.SliceStable(scoredCands, func(i, j int) bool { if scoredCands[i].boost == scoredCands[j].boost { return scoredCands[i].candidate.Slug < scoredCands[j].candidate.Slug } return scoredCands[i].boost > scoredCands[j].boost }) out := make([]common.WikiPageCandidate, 0, len(scoredCands)) for _, item := range scoredCands { out = append(out, item.candidate) } return out } func (p *wikiPipeline) resolveMaybePlanPage(page wikiPlanPage, candidates []common.WikiPageCandidate) (*common.WikiPageCandidate, error) { if len(candidates) == 0 { return nil, nil } raw, err := common.GenJSON(p.ctx, p.deps.Chat, common.ChatRequest{ LLMID: p.llmID, SystemPrompt: wikiPlanReconcileSystem, UserPrompt: renderWikiTemplate(wikiPlanReconcileUserTemplate, map[string]string{ "planned_page": mustPrettyJSON(page), "candidates": mustPrettyJSON(candidates), }), }) if err != nil { return nil, err } action := strings.ToUpper(strings.TrimSpace(firstString(raw["action"]))) if action != "UPDATE" { return nil, nil } slug := strings.TrimSpace(firstString(raw["slug"])) if slug == "" { return nil, nil } for i := range candidates { if candidates[i].Slug == slug { return &candidates[i], nil } } return nil, nil } func buildWikiPageQueryText(page wikiPlanPage) string { parts := []string{page.Title, page.Topic, strings.Join(page.EntityNames, " ")} var out []string for _, part := range parts { if s := strings.TrimSpace(part); s != "" { out = append(out, s) } } return strings.Join(out, "\n") } func (p *wikiPipeline) mergeWikiPageContent(existingMD, newMD, slug string) (string, error) { if strings.TrimSpace(existingMD) == "" { return newMD, nil } if strings.TrimSpace(newMD) == "" { return existingMD, nil } if strings.TrimSpace(existingMD) == strings.TrimSpace(newMD) { return newMD, nil } fallback := conservativeMergeWikiMarkdown(existingMD, newMD) user := fmt.Sprintf("Merge these two versions of wiki page `%s`:\n\n## EXISTING VERSION\n\n%s\n\n---\n\n## INCOMING VERSION\n\n%s\n\n---\n\nProduce the merged page now. Return only the markdown content.", slug, existingMD, newMD) resp, err := p.deps.Chat.Chat(p.ctx, common.ChatRequest{ LLMID: p.llmID, SystemPrompt: wikiRefineMergeSystem, UserPrompt: user, }) if err != nil { return "", err } if resp == nil { return fallback, nil } merged := strings.TrimSpace(firstNonEmpty(resp.Content)) if merged == "" { return fallback, nil } minAcceptable := int(float64(maxInt(len(existingMD), len(newMD))) * wikiMergeShrinkRatio) if len(merged) < minAcceptable { return fallback, nil } if wikiMarkdownDropsContent(merged, existingMD) || wikiMarkdownDropsContent(merged, newMD) { return fallback, nil } return merged, nil } func conservativeMergeWikiMarkdown(existingMD, newMD string) string { if strings.Contains(existingMD, newMD) { return strings.TrimSpace(existingMD) } if strings.Contains(newMD, existingMD) { return strings.TrimSpace(newMD) } title := firstNonEmpty(wikiMarkdownTitle(existingMD), wikiMarkdownTitle(newMD)) blocks := make([]string, 0, 8) seen := map[string]bool{} appendBlocks := func(body string) { for _, block := range splitWikiMarkdownBlocks(body) { key := normKey(block) if key == "" || seen[key] { continue } seen[key] = true blocks = append(blocks, block) } } appendBlocks(wikiMarkdownBody(existingMD)) appendBlocks(wikiMarkdownBody(newMD)) if title != "" { if len(blocks) == 0 { return "# " + title } return "# " + title + "\n\n" + strings.Join(blocks, "\n\n") } return strings.Join(blocks, "\n\n") } func wikiMarkdownDropsContent(mergedMD, sourceMD string) bool { mergedNorm := normKey(mergedMD) for _, block := range splitWikiMarkdownBlocks(wikiMarkdownBody(sourceMD)) { if !wikiMarkdownContentBlock(block) { continue } if !strings.Contains(mergedNorm, normKey(block)) { return true } } return false } func wikiMarkdownTitle(md string) string { for _, line := range strings.Split(md, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "# ") { return strings.TrimSpace(strings.TrimPrefix(line, "# ")) } if line != "" { break } } return "" } func wikiMarkdownBody(md string) string { lines := strings.Split(md, "\n") for i, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "# ") { return strings.TrimSpace(strings.Join(lines[i+1:], "\n")) } if line != "" { break } } return strings.TrimSpace(md) } func splitWikiMarkdownBlocks(md string) []string { parts := strings.Split(strings.TrimSpace(md), "\n\n") out := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part != "" { out = append(out, part) } } return out } func wikiMarkdownContentBlock(block string) bool { block = strings.TrimSpace(block) if block == "" { return false } if strings.HasPrefix(block, "#") { return false } return true } func reduceExtracts(extracts []wikiExtract) wikiExtract { out := wikiExtract{} type entityKey struct { Name string Type string } type conceptKey struct { Term string } entities := map[entityKey]*wikiEntity{} concepts := map[conceptKey]*wikiConcept{} seenRelations := map[string]bool{} seenTopics := map[string]bool{} for _, ex := range extracts { for _, e := range ex.Entities { key := entityKey{Name: normKey(e.Name), Type: normKey(e.Type)} if cur, ok := entities[key]; ok { cur.Aliases = mergeStrings(cur.Aliases, e.Aliases) cur.SourceChunkIDs = mergeStrings(cur.SourceChunkIDs, e.SourceChunkIDs) if cur.Type == "" { cur.Type = e.Type } continue } item := e item.Name = strings.TrimSpace(item.Name) item.Type = strings.TrimSpace(item.Type) item.Aliases = uniqueStrings(item.Aliases) item.SourceChunkIDs = uniqueStrings(item.SourceChunkIDs) entities[key] = &item } for _, c := range ex.Concepts { key := conceptKey{Term: normKey(c.Term)} if cur, ok := concepts[key]; ok { if cur.Definition == "" { cur.Definition = c.Definition } cur.SourceChunkIDs = mergeStrings(cur.SourceChunkIDs, c.SourceChunkIDs) continue } item := c item.Term = strings.TrimSpace(item.Term) item.Definition = strings.TrimSpace(item.Definition) item.SourceChunkIDs = uniqueStrings(item.SourceChunkIDs) concepts[key] = &item } for _, c := range ex.Claims { c.SourceChunkIDs = uniqueStrings(c.SourceChunkIDs) out.Claims = append(out.Claims, c) } for _, r := range ex.Relations { key := normKey(r.From) + "\x00" + normKey(r.Type) + "\x00" + normKey(r.To) if seenRelations[key] { continue } r.SourceChunkIDs = uniqueStrings(r.SourceChunkIDs) seenRelations[key] = true out.Relations = append(out.Relations, r) } for _, t := range ex.Topics { t = strings.TrimSpace(t) if t == "" { continue } key := normKey(t) if seenTopics[key] { continue } seenTopics[key] = true out.Topics = append(out.Topics, t) } } out.Entities = make([]wikiEntity, 0, len(entities)) keys := make([]entityKey, 0, len(entities)) for k := range entities { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { if keys[i].Name == keys[j].Name { return keys[i].Type < keys[j].Type } return keys[i].Name < keys[j].Name }) for _, k := range keys { out.Entities = append(out.Entities, *entities[k]) } out.Concepts = make([]wikiConcept, 0, len(concepts)) ckeys := make([]conceptKey, 0, len(concepts)) for k := range concepts { ckeys = append(ckeys, k) } sort.Slice(ckeys, func(i, j int) bool { return ckeys[i].Term < ckeys[j].Term }) for _, k := range ckeys { out.Concepts = append(out.Concepts, *concepts[k]) } return out } func parseWikiExtract(raw map[string]any) wikiExtract { out := wikiExtract{} out.Entities = parseWikiEntities(raw["entities"]) out.Concepts = parseWikiConcepts(raw["concepts"]) out.Claims = parseWikiClaims(raw["claims"]) out.Relations = parseWikiRelations(raw["relations"]) out.Topics = parseWikiStrings(raw["topics"]) return out } func parseWikiPlan(raw map[string]any, docID string, reduced wikiExtract) wikiPlan { plan := wikiPlan{ Title: firstString(raw["title"]), Slug: firstString(raw["slug"]), Lead: firstString(raw["lead"]), Sections: parseWikiPlanSections(raw["sections"]), PageType: firstString(raw["page_type"]), Topic: firstString(raw["topic"]), Entities: parseWikiStrings(raw["entity_names"]), Related: parseWikiStrings(raw["related_kb_pages"]), Pages: parseWikiPlanPages(raw["pages"]), } if len(plan.Pages) > 0 { first := plan.Pages[0] if plan.Title == "" { plan.Title = first.Title } if plan.Slug == "" { plan.Slug = first.Slug } if plan.Lead == "" { plan.Lead = first.Lead } if plan.PageType == "" { plan.PageType = first.PageType } if plan.Topic == "" { plan.Topic = first.Topic } if len(plan.Entities) == 0 { plan.Entities = first.EntityNames } if len(plan.Related) == 0 { plan.Related = first.RelatedKB } if len(plan.Sections) == 0 { plan.Sections = first.Sections } } if plan.Title == "" { if len(reduced.Topics) > 0 { plan.Title = reduced.Topics[0] } else if len(reduced.Entities) > 0 { plan.Title = reduced.Entities[0].Name } else if len(reduced.Concepts) > 0 { plan.Title = reduced.Concepts[0].Term } else { plan.Title = docID } } if plan.Slug == "" { plan.Slug = slugify(plan.Title) } if plan.Lead == "" { plan.Lead = plan.Title } if len(plan.Sections) == 0 { plan.Sections = []wikiPlanSection{{Heading: "Overview", Points: []string{plan.Lead}}} } return plan } func normalizeWikiPlan(plan wikiPlan, docID string, reduced wikiExtract) wikiPlan { plan.Pages = normalizeWikiPlanPages(plan.Pages, reduced) if plan.Title == "" { plan.Title = firstPlanTitle(plan.Pages, reduced, docID) } if plan.Slug == "" { plan.Slug = slugify(plan.Title) } if plan.Lead == "" { plan.Lead = plan.Title } if len(plan.Sections) == 0 { plan.Sections = []wikiPlanSection{{Heading: "Overview", Points: []string{plan.Lead}}} } return plan } func normalizeWikiPlanPages(pages []wikiPlanPage, reduced wikiExtract) []wikiPlanPage { existing := map[string]wikiPlanPage{} out := make([]wikiPlanPage, 0, len(pages)) for _, page := range pages { page = normalizeWikiPlanPage(page) if page.Slug == "" { continue } if _, ok := existing[page.Slug]; ok { continue } existing[page.Slug] = page out = append(out, page) } fallback := buildWikiFallbackPages(reduced) if len(out) == 0 { for _, page := range fallback { if page.Slug == "" { continue } existing[page.Slug] = page out = append(out, page) } } sort.SliceStable(out, func(i, j int) bool { if out[i].Priority == out[j].Priority { return out[i].Slug < out[j].Slug } return out[i].Priority < out[j].Priority }) for i := range out { if out[i].Priority <= 0 { out[i].Priority = i + 1 } } return out } func normalizeWikiPlanPage(page wikiPlanPage) wikiPlanPage { page.Action = strings.ToUpper(strings.TrimSpace(page.Action)) if page.Action == "" { page.Action = "CREATE" } page.Slug = strings.TrimSpace(page.Slug) if page.Slug == "" { page.Slug = slugify(page.Title) } page.Title = strings.TrimSpace(page.Title) if page.Title == "" { page.Title = page.Slug } page.PageType = strings.TrimSpace(page.PageType) if page.PageType == "" { page.PageType = "concept" } page.Topic = strings.TrimSpace(page.Topic) if page.Topic == "" { page.Topic = page.Title } page.EntityNames = uniqueStrings(page.EntityNames) page.RelatedKB = uniqueStrings(page.RelatedKB) page.Sections = normalizeWikiPlanSections(page.Sections) if page.Priority <= 0 { page.Priority = 99 } return page } func normalizeWikiPlanSections(sections []wikiPlanSection) []wikiPlanSection { if len(sections) == 0 { return []wikiPlanSection{{Heading: "Overview", Points: nil}} } out := make([]wikiPlanSection, 0, len(sections)) for _, sec := range sections { sec.Heading = strings.TrimSpace(sec.Heading) sec.Points = uniqueStrings(sec.Points) if sec.Heading == "" { continue } out = append(out, sec) } if len(out) == 0 { return []wikiPlanSection{{Heading: "Overview", Points: nil}} } return out } func buildWikiFallbackPages(reduced wikiExtract) []wikiPlanPage { var out []wikiPlanPage seen := map[string]bool{} appendPage := func(page wikiPlanPage) { page = normalizeWikiPlanPage(page) if page.Slug == "" || seen[page.Slug] { return } seen[page.Slug] = true out = append(out, page) } for _, e := range reduced.Entities { appendPage(wikiPlanPage{ Action: "CREATE", Slug: "entity/" + slugify(e.Name), Title: e.Name, PageType: "entity", Topic: e.Name, EntityNames: append([]string{e.Name}, e.Aliases...), Priority: len(out) + 1, Lead: e.Name, Sections: []wikiPlanSection{{Heading: "Overview", Points: []string{e.Name}}}, }) } for _, c := range reduced.Concepts { appendPage(wikiPlanPage{ Action: "CREATE", Slug: "concept/" + slugify(c.Term), Title: c.Term, PageType: "concept", Topic: c.Term, EntityNames: []string{c.Term}, Priority: len(out) + 1, Lead: c.Definition, Sections: []wikiPlanSection{{Heading: "Overview", Points: []string{c.Term}}}, }) } if len(out) == 0 { for _, t := range reduced.Topics { appendPage(wikiPlanPage{ Action: "CREATE", Slug: "topic/" + slugify(t), Title: t, PageType: "topic", Topic: t, Priority: len(out) + 1, Lead: t, Sections: []wikiPlanSection{{Heading: "Overview", Points: []string{t}}}, }) } } if len(out) == 0 { appendPage(wikiPlanPage{ Action: "CREATE", Slug: slugify(firstPlanTitle(nil, reduced, "")), Title: firstPlanTitle(nil, reduced, ""), PageType: "concept", Topic: firstPlanTitle(nil, reduced, ""), Priority: 1, Lead: firstPlanTitle(nil, reduced, ""), Sections: []wikiPlanSection{{Heading: "Overview", Points: []string{firstPlanTitle(nil, reduced, "")}}}, }) } return out } func firstPlanTitle(pages []wikiPlanPage, reduced wikiExtract, docID string) string { for _, page := range pages { if s := strings.TrimSpace(page.Title); s != "" { return s } } if len(reduced.Topics) > 0 { return reduced.Topics[0] } if len(reduced.Entities) > 0 { return reduced.Entities[0].Name } if len(reduced.Concepts) > 0 { return reduced.Concepts[0].Term } if docID != "" { return docID } return "wiki" } func (p wikiPlan) firstPageCandidate() wikiPlanPage { if len(p.Pages) > 0 { return p.Pages[0] } return wikiPlanPage{ Action: "CREATE", Slug: p.Slug, Title: p.Title, PageType: p.PageType, Topic: p.Topic, EntityNames: uniqueStrings(p.Entities), RelatedKB: uniqueStrings(p.Related), Priority: 1, Lead: p.Lead, Sections: p.Sections, } } func packWikiPlanBatches(reduced wikiExtract, budget int) []wikiExtract { if budget <= 0 { budget = wikiPlanTokenBudget } var batches []wikiExtract cur := wikiExtract{} curTokens := 0 flush := func() { if len(cur.Entities)+len(cur.Concepts)+len(cur.Claims)+len(cur.Relations)+len(cur.Topics) > 0 { batches = append(batches, cur) cur = wikiExtract{} curTokens = 0 } } add := func(itemTokens int, addFn func()) { if curTokens > 0 && curTokens+itemTokens > budget { flush() } addFn() curTokens += itemTokens } for _, e := range reduced.Entities { add(common.EstimateTokens(mustJSON(e)), func() { cur.Entities = append(cur.Entities, e) }) } for _, c := range reduced.Concepts { add(common.EstimateTokens(mustJSON(c)), func() { cur.Concepts = append(cur.Concepts, c) }) } for _, c := range reduced.Claims { add(common.EstimateTokens(mustJSON(c)), func() { cur.Claims = append(cur.Claims, c) }) } for _, r := range reduced.Relations { add(common.EstimateTokens(mustJSON(r)), func() { cur.Relations = append(cur.Relations, r) }) } for _, t := range reduced.Topics { add(common.EstimateTokens(mustJSON(t)), func() { cur.Topics = append(cur.Topics, t) }) } flush() return batches } func parseWikiPlanPages(raw any) []wikiPlanPage { arr, ok := raw.([]any) if !ok { return nil } out := make([]wikiPlanPage, 0, len(arr)) for _, item := range arr { m, ok := item.(map[string]any) if !ok { continue } page := wikiPlanPage{ Action: strings.TrimSpace(firstString(m["action"])), Slug: strings.TrimSpace(firstString(m["slug"])), Title: strings.TrimSpace(firstString(m["title"])), PageType: strings.TrimSpace(firstString(m["page_type"])), Topic: strings.TrimSpace(firstString(m["topic"])), EntityNames: parseWikiStrings(m["entity_names"]), RelatedKB: parseWikiStrings(m["related_kb_pages"]), Priority: int(firstNumber(m["priority"])), Lead: strings.TrimSpace(firstString(m["lead"])), Sections: parseWikiPlanSections(m["sections"]), } if page.Slug != "" || page.Title != "" { out = append(out, page) } } return out } func parseWikiEntities(raw any) []wikiEntity { arr, ok := raw.([]any) if !ok { return nil } out := make([]wikiEntity, 0, len(arr)) for _, item := range arr { m, ok := item.(map[string]any) if !ok { continue } e := wikiEntity{ Name: strings.TrimSpace(firstString(m["name"])), Type: strings.TrimSpace(firstString(m["type"])), Aliases: parseWikiStrings(m["aliases"]), SourceChunkIDs: parseWikiStrings(m["source_chunk_ids"]), } if len(e.SourceChunkIDs) == 0 { if s := strings.TrimSpace(firstString(m["source_chunk_id"])); s != "" { e.SourceChunkIDs = []string{s} } } if e.Name != "" { out = append(out, e) } } return out } func parseWikiConcepts(raw any) []wikiConcept { arr, ok := raw.([]any) if !ok { return nil } out := make([]wikiConcept, 0, len(arr)) for _, item := range arr { m, ok := item.(map[string]any) if !ok { continue } c := wikiConcept{ Term: strings.TrimSpace(firstString(m["term"])), Definition: strings.TrimSpace(firstString(m["definition_excerpt"])), SourceChunkIDs: parseWikiStrings(m["source_chunk_ids"]), } if len(c.SourceChunkIDs) == 0 { if s := strings.TrimSpace(firstString(m["source_chunk_id"])); s != "" { c.SourceChunkIDs = []string{s} } } if c.Term != "" { out = append(out, c) } } return out } func parseWikiClaims(raw any) []wikiClaim { arr, ok := raw.([]any) if !ok { return nil } out := make([]wikiClaim, 0, len(arr)) for _, item := range arr { m, ok := item.(map[string]any) if !ok { continue } c := wikiClaim{ Statement: strings.TrimSpace(firstString(m["statement"])), Subject: strings.TrimSpace(firstString(m["subject"])), Confidence: strings.TrimSpace(firstString(m["confidence"])), SourceChunkIDs: parseWikiStrings(m["source_chunk_ids"]), } if len(c.SourceChunkIDs) == 0 { if s := strings.TrimSpace(firstString(m["source_chunk_id"])); s != "" { c.SourceChunkIDs = []string{s} } } if c.Statement != "" { out = append(out, c) } } return out } func parseWikiRelations(raw any) []wikiRelation { arr, ok := raw.([]any) if !ok { return nil } out := make([]wikiRelation, 0, len(arr)) for _, item := range arr { m, ok := item.(map[string]any) if !ok { continue } r := wikiRelation{ From: strings.TrimSpace(firstString(m["from"])), To: strings.TrimSpace(firstString(m["to"])), Type: strings.TrimSpace(firstString(m["type"])), SourceChunkIDs: parseWikiStrings(m["source_chunk_ids"]), } if len(r.SourceChunkIDs) == 0 { if s := strings.TrimSpace(firstString(m["source_chunk_id"])); s != "" { r.SourceChunkIDs = []string{s} } } if r.From != "" && r.To != "" { out = append(out, r) } } return out } func parseWikiPlanSections(raw any) []wikiPlanSection { arr, ok := raw.([]any) if !ok { return nil } out := make([]wikiPlanSection, 0, len(arr)) for _, item := range arr { m, ok := item.(map[string]any) if !ok { continue } sec := wikiPlanSection{ Heading: strings.TrimSpace(firstString(m["heading"])), Points: parseWikiStrings(m["points"]), } if sec.Heading != "" { out = append(out, sec) } } return out } func firstNumber(v any) float64 { switch x := v.(type) { case int: return float64(x) case int64: return float64(x) case float32: return float64(x) case float64: return x default: return 0 } } func parseWikiStrings(raw any) []string { switch v := raw.(type) { case []string: return uniqueStrings(v) case []any: out := make([]string, 0, len(v)) for _, item := range v { if s := strings.TrimSpace(firstString(item)); s != "" { out = append(out, s) } } return uniqueStrings(out) case string: if s := strings.TrimSpace(v); s != "" { return []string{s} } } return nil } func buildSourceContext(chunks []common.Chunk, sourceChunkIDs []string) string { if len(sourceChunkIDs) == 0 { var b strings.Builder for _, ch := range chunks { text := firstNonEmpty(ch.Text, ch.Content) if strings.TrimSpace(text) == "" { continue } b.WriteString(text) b.WriteString("\n\n") } return b.String() } lookup := map[string]string{} for i, ch := range chunks { id := strings.TrimSpace(ch.ID) if id == "" { id = fmt.Sprintf("chunk-%d", i+1) } lookup[id] = firstNonEmpty(ch.Text, ch.Content) } var b strings.Builder for _, id := range sourceChunkIDs { if text := strings.TrimSpace(lookup[id]); text != "" { b.WriteString("[CHUNK_ID ") b.WriteString(id) b.WriteString("]\n") b.WriteString(text) b.WriteString("\n\n") } } return b.String() } func buildEvidenceChecklist(reduced wikiExtract) string { var lines []string for _, e := range reduced.Entities { lines = append(lines, fmt.Sprintf("- entity: %s (%s)", e.Name, e.Type)) } for _, c := range reduced.Concepts { lines = append(lines, fmt.Sprintf("- concept: %s", c.Term)) } for _, c := range reduced.Claims { lines = append(lines, fmt.Sprintf("- claim: %s", c.Statement)) } for _, r := range reduced.Relations { lines = append(lines, fmt.Sprintf("- relation: %s -> %s (%s)", r.From, r.To, r.Type)) } for _, t := range reduced.Topics { lines = append(lines, fmt.Sprintf("- topic: %s", t)) } if len(lines) == 0 { return "- (none)" } return strings.Join(lines, "\n") } type wikiEvidenceItem struct { Statement string Subject string Confidence string ChunkIDs []string Synthetic bool } func assembleWikiPageEvidence(planItem wikiPlanPage, claims []wikiClaim, entityByName, conceptByTerm map[string]wikiExtractItem) []wikiEvidenceItem { rawNames := uniqueStrings(append([]string{}, planItem.EntityNames...)) if len(rawNames) == 0 { if t := strings.TrimSpace(planItem.Title); t != "" { rawNames = []string{t} } else if s := strings.TrimSpace(planItem.Slug); s != "" { rawNames = []string{s} } } if len(rawNames) == 0 { return nil } namesLower := make([]string, 0, len(rawNames)) patterns := make([]*regexp.Regexp, 0, len(rawNames)) for _, n := range rawNames { namesLower = append(namesLower, strings.ToLower(n)) patterns = append(patterns, regexp.MustCompile(`(?i)\b`+regexp.QuoteMeta(n)+`\b`)) } var evidence []wikiEvidenceItem for _, claim := range claims { subjRaw := strings.TrimSpace(claim.Subject) if subjRaw == "" { continue } subjLower := strings.ToLower(subjRaw) matched := false for _, n := range namesLower { if subjLower == n { matched = true break } } if !matched { for _, re := range patterns { if re.MatchString(subjRaw) { matched = true break } } } if !matched { continue } evidence = append(evidence, wikiEvidenceItem{ Statement: claim.Statement, Subject: claim.Subject, Confidence: firstNonEmpty(claim.Confidence, "explicit"), ChunkIDs: uniqueStrings(claim.SourceChunkIDs), }) } if len(evidence) > 0 { return evidence } var fallbackChunkIDs []string matchedNames := make([]string, 0, len(rawNames)) for _, name := range rawNames { key := strings.ToLower(strings.TrimSpace(name)) var hit wikiExtractItem var ok bool if entityByName != nil { hit, ok = entityByName[key] } if !ok && conceptByTerm != nil { hit, ok = conceptByTerm[key] } if !ok { continue } fallbackChunkIDs = mergeStrings(fallbackChunkIDs, hit.SourceChunkIDs) matchedNames = append(matchedNames, name) } if len(fallbackChunkIDs) == 0 { return nil } subject := rawNames[0] if len(matchedNames) > 0 { subject = matchedNames[0] } return []wikiEvidenceItem{{ Statement: "", Subject: subject, Confidence: "inferred", ChunkIDs: fallbackChunkIDs, Synthetic: true, }} } type wikiExtractItem struct { SourceChunkIDs []string } func buildWikiEntityLookup(items []wikiEntity) map[string]wikiExtractItem { out := map[string]wikiExtractItem{} for _, item := range items { hit := wikiExtractItem{SourceChunkIDs: uniqueStrings(item.SourceChunkIDs)} name := strings.TrimSpace(item.Name) if name != "" { out[strings.ToLower(name)] = hit } for _, alias := range item.Aliases { if alias = strings.TrimSpace(alias); alias != "" { out[strings.ToLower(alias)] = hit } } } return out } func buildWikiConceptLookup(items []wikiConcept) map[string]wikiExtractItem { out := map[string]wikiExtractItem{} for _, item := range items { hit := wikiExtractItem{SourceChunkIDs: uniqueStrings(item.SourceChunkIDs)} term := strings.TrimSpace(item.Term) if term != "" { out[strings.ToLower(term)] = hit } } return out } func formatWikiEvidenceBlocks(evidence []wikiEvidenceItem) string { var lines []string for _, ev := range evidence { if ev.Synthetic { continue } confidence := strings.ToUpper(firstNonEmpty(ev.Confidence, "explicit")) lines = append(lines, fmt.Sprintf("%d. [%s] %s\n %s", len(lines)+1, confidence, ev.Subject, ev.Statement)) } if len(lines) == 0 { return "(no pre-extracted evidence — extract facts directly from the source document text above)" } return strings.Join(lines, "\n\n") } func collectWikiEvidenceChunkIDs(evidence []wikiEvidenceItem) []string { var out []string for _, ev := range evidence { out = mergeStrings(out, ev.ChunkIDs) } return out } func collectWikiSourceDocIDs(chunks []common.Chunk, chunkIDs []string, fallback string) []string { if len(chunkIDs) == 0 { if fallback != "" { return []string{fallback} } return nil } lookup := map[string][]string{} for _, ch := range chunks { id := strings.TrimSpace(ch.ID) if id == "" { continue } docID := strings.TrimSpace(firstString(ch.Meta["doc_id"])) if docID == "" { docID = strings.TrimSpace(firstString(ch.Meta["doc_id_kwd"])) } if docID != "" { lookup[id] = []string{docID} } } out := make([]string, 0, len(chunkIDs)) for _, cid := range chunkIDs { if ids := lookup[strings.TrimSpace(cid)]; len(ids) > 0 { out = mergeStrings(out, ids) } } if len(out) == 0 && fallback != "" { return []string{fallback} } return out } var ( wikiWikilinkPipeRe = regexp.MustCompile(`\[\[([^\[\]\|]+?)\|([^\[\]]+?)\]\]`) wikiWikilinkSimpleRe = regexp.MustCompile(`\[\[([^\[\]\|]+?)\]\]`) wikiArtifactMarkdownLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) ) func transformWikiLinks(content, kbID string, pageTitles map[string]string) (string, []string) { kbID = strings.TrimSpace(kbID) seen := map[string]bool{} var outlinks []string track := func(slug string) { slug = strings.TrimSpace(slug) if slug == "" || seen[slug] { return } seen[slug] = true outlinks = append(outlinks, slug) } displayText := func(label, slug string) string { label = strings.TrimSpace(label) if label != slug && label != slugify(lastPathSlug(slug)) { return label } if title := strings.TrimSpace(pageTitles[slug]); title != "" { return title } readable := strings.ReplaceAll(lastPathSlug(slug), "-", " ") readable = strings.ReplaceAll(readable, "_", " ") readable = strings.TrimSpace(readable) if readable == "" { return label } return strings.Title(readable) } artifactSlug := func(href string) string { parsed, err := url.Parse(href) if err != nil { return "" } path := parsed.Path if path == "" && parsed.Opaque != "" { path = parsed.Opaque } parts := strings.Split(strings.Trim(path, "/"), "/") if len(parts) >= 2 && parts[0] == "artifact" && parts[1] == kbID { return strings.Join(parts[2:], "/") } if len(parts) >= 2 && parts[0] == kbID { return strings.Join(parts[1:], "/") } return "" } rewriteMD := func(label, href string) string { slug := artifactSlug(href) if slug == "" { return "[" + label + "](" + href + ")" } track(slug) return "[" + displayText(label, slug) + "](artifact/" + kbID + "/" + slug + ")" } out := wikiArtifactMarkdownLink.ReplaceAllStringFunc(content, func(match string) string { sub := wikiArtifactMarkdownLink.FindStringSubmatch(match) if len(sub) != 3 { return match } return rewriteMD(sub[1], sub[2]) }) out = wikiWikilinkPipeRe.ReplaceAllStringFunc(out, func(match string) string { sub := wikiWikilinkPipeRe.FindStringSubmatch(match) if len(sub) != 3 { return match } slug := strings.TrimSpace(sub[1]) track(slug) return "[" + sub[2] + "](artifact/" + kbID + "/" + slug + ")" }) out = wikiWikilinkSimpleRe.ReplaceAllStringFunc(out, func(match string) string { sub := wikiWikilinkSimpleRe.FindStringSubmatch(match) if len(sub) != 2 { return match } slug := strings.TrimSpace(sub[1]) track(slug) return "[" + displayText(slug, slug) + "](artifact/" + kbID + "/" + slug + ")" }) return out, outlinks } func lastPathSlug(slug string) string { slug = strings.TrimSpace(slug) if idx := strings.LastIndex(slug, "/"); idx >= 0 && idx < len(slug)-1 { return slug[idx+1:] } return slug } func mustJSON(v any) string { b, err := json.Marshal(v) if err != nil { return "[]" } return string(b) } func mustPrettyJSON(v any) string { b, err := json.MarshalIndent(v, "", " ") if err != nil { return "{}" } return string(b) } func renderWikiTemplate(tmpl string, values map[string]string) string { out := tmpl keys := make([]string, 0, len(values)) for k := range values { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { out = strings.ReplaceAll(out, "{"+k+"}", values[k]) } return out } func prefixWithDash(items []string) []string { out := make([]string, 0, len(items)) for _, item := range items { out = append(out, "- "+item) } return out } func firstNonEmpty(vals ...string) string { for _, v := range vals { if s := strings.TrimSpace(v); s != "" { return s } } return "" } func uniqueStrings(in []string) []string { if len(in) == 0 { return nil } seen := map[string]bool{} out := make([]string, 0, len(in)) for _, s := range in { s = strings.TrimSpace(s) if s == "" || seen[s] { continue } seen[s] = true out = append(out, s) } return out } func mergeStrings(a, b []string) []string { return uniqueStrings(append(append([]string{}, a...), b...)) } func normalizedStringSet(items []string) map[string]struct{} { out := make(map[string]struct{}, len(items)) for _, item := range items { if key := normKey(item); key != "" { out[key] = struct{}{} } } return out } func intersectsNormalized(needle map[string]struct{}, haystack []string) bool { if len(needle) == 0 { return false } for _, item := range haystack { if _, ok := needle[normKey(item)]; ok { return true } } return false } func normalizedOverlapCount(needle map[string]struct{}, haystack []string) int { if len(needle) == 0 { return 0 } count := 0 seen := map[string]struct{}{} for _, item := range haystack { key := normKey(item) if key == "" { continue } if _, dup := seen[key]; dup { continue } seen[key] = struct{}{} if _, ok := needle[key]; ok { count++ } } return count } func normKey(s string) string { return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(s)), " ")) } func firstString(v any) string { switch x := v.(type) { case string: return x case fmt.Stringer: return x.String() default: return "" } } func (e wikiExtract) sourceChunkIDs() []string { seen := map[string]bool{} var out []string add := func(s string) { s = strings.TrimSpace(s) if s == "" || seen[s] { return } seen[s] = true out = append(out, s) } for _, item := range e.Entities { for _, id := range item.SourceChunkIDs { add(id) } } for _, item := range e.Concepts { for _, id := range item.SourceChunkIDs { add(id) } } for _, item := range e.Claims { for _, id := range item.SourceChunkIDs { add(id) } } for _, item := range e.Relations { for _, id := range item.SourceChunkIDs { add(id) } } return out } func (e wikiExtract) asPlanPayload() map[string]any { return map[string]any{ "entities": e.Entities, "concepts": e.Concepts, "claims": e.Claims, "relations": e.Relations, "topics": e.Topics, } } func compactJSON(v any) string { b, err := json.Marshal(v) if err != nil { return "{}" } return string(bytes.TrimSpace(b)) } func (p *wikiPipeline) maybeSourceIDs() []string { if ids := p.reduced.sourceChunkIDs(); len(ids) > 0 { return ids } seen := map[string]bool{} var out []string for _, ch := range p.inputs.Chunks { id := strings.TrimSpace(ch.ID) if id == "" || seen[id] { continue } seen[id] = true out = append(out, id) } return out } func (p *wikiPipeline) runMapBatch(batch []common.Chunk) error { extract, err := p.mapBatch(batch) if err != nil { return err } p.mapExtracts = append(p.mapExtracts, extract) return nil } // dedupHistorical drops products that are near-duplicates of existing historical // artifacts, implementing cross-run dedup for the wiki variant. This remains a // read-only historical lookup and does not store any wiki intermediate state. func dedupHistorical(ctx context.Context, deps common.Deps, param common.Param, tenantID, datasetID string, override []common.Candidate, products []common.Product) (survivors []common.Product, dropped int, err error) { if len(products) == 0 { return products, 0, nil } threshold := param.SimilarityThreshold if len(override) > 0 { for _, p := range products { if nearDuplicateOf(p.Vector, override, threshold) { dropped++ continue } survivors = append(survivors, p) } return survivors, dropped, nil } if deps.HistoricalKNN == nil { return products, 0, nil } for _, p := range products { hits, e := deps.HistoricalKNN.TopKHistory(ctx, tenantID, datasetID, string(common.VariantWiki), p.Vector, wikiHistoricalK, threshold) if e != nil { return survivors, dropped, e } if len(hits) > 0 { dropped++ continue } survivors = append(survivors, p) } return survivors, dropped, nil } func nearDuplicateOf(vec []float32, cands []common.Candidate, threshold float64) bool { qn := l2Norm32(vec) if qn == 0 { qn = 1 } for _, c := range cands { cn := l2Norm32(c.Vector) if cn == 0 { cn = 1 } var dot float64 for i := 0; i < len(vec) && i < len(c.Vector); i++ { dot += float64(vec[i]) * float64(c.Vector[i]) } if dot/(qn*cn) >= threshold { return true } } return false } func l2Norm32(v []float32) float64 { var s float64 for _, x := range v { s += float64(x) * float64(x) } return math.Sqrt(s) } func maxInt(a, b int) int { if a > b { return a } return b } // wikiHistoricalK is the K used for the historical-dedup KNN lookup. const wikiHistoricalK = 5 // wikiPlanTokenBudget caps one planning round's approximate token load. const wikiPlanTokenBudget = 3500