2026-07-29 21:06:48 +08:00
// 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
}
2026-07-30 19:56:17 +08:00
// 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).
2026-07-29 21:06:48 +08:00
out := common . Outputs {
2026-07-30 19:56:17 +08:00
Products : prod ,
2026-07-29 21:06:48 +08:00
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