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

132 lines
3.2 KiB
Go
Raw Normal View History

2026-06-26 08:37:04 +00:00
package knowledge
import (
"strings"
"unicode/utf8"
"github.com/google/uuid"
)
const (
maxPatrolLabelRunes = 14
minBreadthNodeCount = 12
)
func SupplementGraphFromResearchMap(g *Graph, seed string, pillars, questions []string) {
if g == nil {
return
}
seen := nodeLabelSet(g.Nodes)
ensureSeedCore(g, seed, seen)
for _, pillar := range pillars {
if len(g.Nodes) >= 32 {
break
}
addBreadthNode(g, strings.TrimSpace(pillar), 1, "symptom", seen, pillar, "")
}
for _, question := range questions {
if len(g.Nodes) >= 32 {
break
}
q := strings.TrimSpace(question)
if q == "" {
continue
}
label := patrolLabelFromQuestion(q)
addBreadthNode(g, label, 2, "pain", seen, q, defaultPlacementForQuestion(q))
}
}
func ensureSeedCore(g *Graph, seed string, seen map[string]struct{}) {
seed = strings.TrimSpace(seed)
if seed == "" {
return
}
if _, ok := seen[strings.ToLower(seed)]; ok {
return
}
g.Nodes = append([]Node{{
ID: uuid.NewString(),
Label: seed,
NodeKind: "pain",
Type: "core",
Layer: 0,
Relation: "核心種子主題,使用者圍繞此困擾在社群發文求助",
PlacementValue: "核心討論串最常求推薦與真實心得,適合以產品使用經驗自然回覆",
ProductFitScore: 90,
}}, g.Nodes...)
seen[strings.ToLower(seed)] = struct{}{}
}
func addBreadthNode(g *Graph, label string, layer int, kind string, seen map[string]struct{}, relation, placement string) {
label = strings.TrimSpace(label)
if label == "" {
return
}
key := strings.ToLower(label)
if _, ok := seen[key]; ok {
return
}
if relation == "" {
relation = "與種子主題相關的「" + label + "」討論,常見於 Threads 求助串"
}
if placement == "" {
placement = "這類帖常求產品推薦或使用心得,適合以自身經驗自然回覆"
}
g.Nodes = append(g.Nodes, Node{
ID: uuid.NewString(),
Label: label,
NodeKind: kind,
Type: breadthNodeType(layer, kind),
Layer: layer,
Relation: relation,
PlacementValue: placement,
ProductFitScore: defaultProductFit(kind, layer),
})
seen[key] = struct{}{}
}
func breadthNodeType(layer int, kind string) string {
if layer == 0 {
return "core"
}
if kind == "cause" {
return "cause"
}
if kind == "symptom" {
return "symptom"
}
return "mechanism"
}
func nodeLabelSet(nodes []Node) map[string]struct{} {
seen := map[string]struct{}{}
for _, node := range nodes {
label := strings.ToLower(strings.TrimSpace(node.Label))
if label != "" {
seen[label] = struct{}{}
}
}
return seen
}
func patrolLabelFromQuestion(question string) string {
question = strings.TrimSpace(question)
if question == "" {
return ""
}
if utf8.RuneCountInString(question) <= maxPatrolLabelRunes {
return question
}
runes := []rune(question)
return string(runes[:maxPatrolLabelRunes])
}
func defaultPlacementForQuestion(question string) string {
return "受眾常這樣發文求助,適合在留言區以產品使用經驗回覆「" + strings.TrimSpace(question) + "」這類問題"
}
func GraphNeedsBootstrap(g Graph) bool {
return len(g.Nodes) < minBreadthNodeCount
}