haixunMaster/haixun-backend/internal/library/placement/research_map.go

105 lines
3.2 KiB
Go
Raw Normal View History

2026-06-24 10:02:42 +00:00
package placement
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
type ResearchMap struct {
AudienceSummary string `json:"audienceSummary"`
ContentGoal string `json:"contentGoal"`
Questions []string `json:"questions"`
Pillars []string `json:"pillars"`
Exclusions []string `json:"exclusions"`
}
type ResearchMapInput struct {
Label string
SeedQuery string
Brief string
ProductContext string
}
var researchMapFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```")
func BuildResearchMapSystemPrompt() string {
return strings.TrimSpace(`你是 Threads產品置入研究顧問目標是幫品牌找到近期發文作者有需求現在留言還來得及自然推薦產品的貼文
規則
1. questions pillars 會直接拿去 Threads 搜尋每句 520 像真人求助
2. questions 至少 5 pillars 至少 4 exclusions 至少 4
3. contentGoal 要寫找到近期發文且可自然留言置入的貼文
4. 全部繁體中文貼近台灣 Threads
5. 只回傳一個 JSONaudienceSummary, contentGoal, questions, pillars, exclusions`)
}
func BuildResearchMapUserPrompt(in ResearchMapInput) string {
var b strings.Builder
b.WriteString("【主題名稱】")
b.WriteString(strings.TrimSpace(in.Label))
b.WriteString("\n【種子關鍵字】")
b.WriteString(strings.TrimSpace(in.SeedQuery))
b.WriteString("\n【這個主題想做什么】\n")
b.WriteString(strings.TrimSpace(in.Brief))
b.WriteString("\n【產品置入】\n")
product := FormatProductContextForPrompt(in.ProductContext)
if product == "" {
product = "(尚未填寫)"
}
b.WriteString(product)
b.WriteString("\n\n請產出研究地圖 JSON。")
return b.String()
}
func ParseResearchMapOutput(raw string) (ResearchMap, error) {
payload, err := extractResearchJSONObject(raw)
if err != nil {
return ResearchMap{}, err
}
var out ResearchMap
if err := json.Unmarshal(payload, &out); err != nil {
return ResearchMap{}, fmt.Errorf("parse research map json: %w", err)
}
out.AudienceSummary = strings.TrimSpace(out.AudienceSummary)
out.ContentGoal = strings.TrimSpace(out.ContentGoal)
out.Questions = cleanStringList(out.Questions)
out.Pillars = cleanStringList(out.Pillars)
out.Exclusions = cleanStringList(out.Exclusions)
if out.AudienceSummary == "" && len(out.Questions) == 0 {
return ResearchMap{}, fmt.Errorf("research map missing audience or questions")
}
return out, nil
}
func cleanStringList(items []string) []string {
out := make([]string, 0, len(items))
seen := map[string]struct{}{}
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
out = append(out, item)
}
return out
}
func extractResearchJSONObject(raw string) ([]byte, error) {
raw = strings.TrimSpace(raw)
if m := researchMapFenceRE.FindStringSubmatch(raw); len(m) == 2 {
raw = strings.TrimSpace(m[1])
}
start := strings.Index(raw, "{")
end := strings.LastIndex(raw, "}")
if start < 0 || end <= start {
return nil, fmt.Errorf("research map output missing json object")
}
return []byte(raw[start : end+1]), nil
}