thread-master/backend/internal/library/knowledge/graph_rescore.go

111 lines
2.7 KiB
Go
Raw Normal View History

2026-07-01 08:42:51 +00:00
package knowledge
// RescoreGraphIntentFit recomputes node productFitScore using:
// 1. weighted intent similarity (product + research map)
// 2. graph proximity propagation from high-fit core nodes
// 3. tangential-topic penalty
func RescoreGraphIntentFit(graph *Graph, in PatrolTagInput) {
if graph == nil || len(graph.Nodes) == 0 {
return
}
profile := BuildIntentProfile(in)
intentScores := make([]int, len(graph.Nodes))
for i, node := range graph.Nodes {
intentScores[i] = ScoreIntentSimilarity(NodeIntentText(node), profile)
}
propagated := propagateIntentAlongGraph(graph.Nodes, graph.Edges, intentScores)
for i := range graph.Nodes {
node := &graph.Nodes[i]
semantic := intentScores[i]
if propagated[i] > semantic {
semantic = propagated[i]
}
llmScore := node.ProductFitScore
if llmScore <= 0 {
llmScore = defaultProductFit(node.NodeKind, node.Layer)
}
layerBlend := 0.5 + float64(minInt(node.Layer, 3))*0.08
blended := int(float64(semantic)*layerBlend + float64(llmScore)*(1.0-layerBlend))
if IsTangentialToIntent(NodeIntentText(*node), profile) {
blended = minInt(blended, 28)
}
if blended < 0 {
blended = 0
}
if blended > 100 {
blended = 100
}
node.ProductFitScore = blended
}
}
func propagateIntentAlongGraph(nodes []Node, edges []Edge, intentScores []int) []int {
if len(nodes) == 0 {
return nil
}
idIndex := map[string]int{}
for i, node := range nodes {
idIndex[node.ID] = i
}
adj := make([][]int, len(nodes))
addEdge := func(from, to string) {
fi, okFrom := idIndex[from]
ti, okTo := idIndex[to]
if !okFrom || !okTo || fi == ti {
return
}
adj[fi] = append(adj[fi], ti)
adj[ti] = append(adj[ti], fi)
}
for _, edge := range edges {
addEdge(edge.From, edge.To)
}
out := make([]int, len(nodes))
copy(out, intentScores)
visited := make([]bool, len(nodes))
type queueItem struct {
idx int
dist int
}
queue := make([]queueItem, 0, len(nodes))
for i, score := range intentScores {
if score >= 55 || nodes[i].Layer == 0 {
queue = append(queue, queueItem{idx: i, dist: 0})
visited[i] = true
}
}
for len(queue) > 0 {
item := queue[0]
queue = queue[1:]
if item.dist >= 3 {
continue
}
base := intentScores[item.idx]
if base <= 0 {
base = out[item.idx]
}
decayed := int(float64(base) * powDecay(0.74, item.dist+1))
for _, next := range adj[item.idx] {
if decayed > out[next] {
out[next] = decayed
}
if !visited[next] {
visited[next] = true
queue = append(queue, queueItem{idx: next, dist: item.dist + 1})
}
}
}
return out
}
func powDecay(base float64, exp int) float64 {
out := 1.0
for i := 0; i < exp; i++ {
out *= base
}
return out
2026-07-03 14:42:19 +00:00
}