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

105 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}