110 lines
2.7 KiB
Go
110 lines
2.7 KiB
Go
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
|
|
} |