package knowledge import ( "encoding/json" "fmt" "strings" "sync" libprompt "haixun-backend/internal/library/prompt" ) type queryConfig struct { MaxPlanQueries int `json:"max_plan_queries"` MaxSupplemental int `json:"max_supplemental_queries"` MinPainTagCandidates int `json:"min_pain_tag_candidates"` MinTotalTagCandidates int `json:"min_total_tag_candidates"` PlanBase []string `json:"plan_base"` PlanPeripheral []string `json:"plan_peripheral"` PlanAudience string `json:"plan_audience"` PlanL1Cause string `json:"plan_l1_cause"` PlanL1Pain string `json:"plan_l1_pain"` Supplemental []string `json:"supplemental"` SupplementalL1 string `json:"supplemental_l1"` RecencySuffix string `json:"recency_suffix"` RecencyHelpMarkers string `json:"recency_help_markers"` } var ( queryCfgOnce sync.Once queryCfg queryConfig queryCfgErr error ) func loadQueryConfig() (queryConfig, error) { queryCfgOnce.Do(func() { raw, err := libprompt.KnowledgeGraphQueryConfig() if err != nil { queryCfgErr = err return } payload, err := json.Marshal(raw) if err != nil { queryCfgErr = err return } queryCfgErr = json.Unmarshal(payload, &queryCfg) }) return queryCfg, queryCfgErr } func MaxPlanQueriesPerRound() int { cfg, err := loadQueryConfig() if err != nil || cfg.MaxPlanQueries <= 0 { return 15 } return cfg.MaxPlanQueries } func MaxSupplementalQueries() int { cfg, err := loadQueryConfig() if err != nil || cfg.MaxSupplemental <= 0 { return 5 } return cfg.MaxSupplemental } func MinPainTagCandidates() int { cfg, err := loadQueryConfig() if err != nil || cfg.MinPainTagCandidates <= 0 { return 8 } return cfg.MinPainTagCandidates } type PlanInput struct { Seed string TargetAudience string ProductBrief string L1Labels []string Supplemental bool } func PlanQueries(in PlanInput) []string { cfg, err := loadQueryConfig() if err != nil { return nil } seed := strings.TrimSpace(in.Seed) if seed == "" { return nil } if in.Supplemental { return supplementalQueries(cfg, seed, in.L1Labels) } seen := map[string]struct{}{} out := make([]string, 0, cfg.MaxPlanQueries) add := func(q string) { q = strings.TrimSpace(q) if q == "" { return } if _, ok := seen[q]; ok { return } seen[q] = struct{}{} out = append(out, q) } vars := map[string]string{"seed": seed, "audience": strings.TrimSpace(in.TargetAudience)} for _, tpl := range cfg.PlanBase { add(renderQueryTemplate(tpl, vars)) if len(out) >= cfg.MaxPlanQueries { return capQueries(out, cfg.MaxPlanQueries) } } for _, tpl := range cfg.PlanPeripheral { add(renderQueryTemplate(tpl, vars)) if len(out) >= cfg.MaxPlanQueries { return capQueries(out, cfg.MaxPlanQueries) } } if vars["audience"] != "" && strings.TrimSpace(cfg.PlanAudience) != "" { add(renderQueryTemplate(cfg.PlanAudience, vars)) } for _, label := range in.L1Labels { label = strings.TrimSpace(label) if label == "" || label == seed { continue } l1vars := map[string]string{"seed": seed, "label": label} add(renderQueryTemplate(cfg.PlanL1Cause, l1vars)) if len(out) >= cfg.MaxPlanQueries { return capQueries(out, cfg.MaxPlanQueries) } add(renderQueryTemplate(cfg.PlanL1Pain, l1vars)) if len(out) >= cfg.MaxPlanQueries { return capQueries(out, cfg.MaxPlanQueries) } } return capQueries(out, cfg.MaxPlanQueries) } func supplementalQueries(cfg queryConfig, seed string, l1Labels []string) []string { seed = strings.TrimSpace(seed) if seed == "" { return nil } seen := map[string]struct{}{} out := make([]string, 0, cfg.MaxSupplemental) add := func(q string) { q = strings.TrimSpace(q) if q == "" { return } if _, ok := seen[q]; ok { return } seen[q] = struct{}{} out = append(out, q) } vars := map[string]string{"seed": seed} for _, tpl := range cfg.Supplemental { add(renderQueryTemplate(tpl, vars)) } for _, label := range l1Labels { label = strings.TrimSpace(label) if label == "" { continue } add(renderQueryTemplate(cfg.SupplementalL1, map[string]string{"seed": seed, "label": label})) if len(out) >= cfg.MaxSupplemental { break } } return capQueries(out, cfg.MaxSupplemental) } func BuildRecencyQuery(label string) string { cfg, err := loadQueryConfig() if err != nil { return "" } label = strings.TrimSpace(label) if label == "" { return "" } if strings.ContainsAny(label, cfg.RecencyHelpMarkers) { return label } suffix := strings.TrimSpace(cfg.RecencySuffix) if suffix == "" { suffix = "請問" } return fmt.Sprintf("%s %s", label, suffix) } func renderQueryTemplate(tpl string, vars map[string]string) string { out := tpl for key, value := range vars { out = strings.ReplaceAll(out, "{{"+key+"}}", value) } return strings.TrimSpace(out) } func capQueries(items []string, max int) []string { if max <= 0 || len(items) <= max { return items } return items[:max] } func L1LabelsFromNodes(nodes []Node) []string { out := make([]string, 0, len(nodes)) for _, node := range nodes { if node.Layer != 1 { continue } label := strings.TrimSpace(node.Label) if label != "" { out = append(out, label) } } return out }