thread-master/internal/library/outreach/generate.go

129 lines
3.3 KiB
Go

package outreach
import (
"encoding/json"
"fmt"
"regexp"
"strings"
libprompt "haixun-backend/internal/library/prompt"
)
const maxChars = 500
type Draft struct {
Text string `json:"text"`
Angle string `json:"angle"`
Rationale string `json:"rationale"`
}
type GenerateResult struct {
Relevance float64 `json:"relevance"`
Reason string `json:"reason"`
Drafts []Draft `json:"drafts"`
}
type GenerateInput struct {
Persona string
TopicLabel string
AudienceBrief string
ProductBrief string
PlacementReason string
TargetText string
AuthorName string
Count int
}
var codeFenceRE = regexp.MustCompile(`(?s)^` + "```(?:json)?\\s*(.*?)\\s*" + "```$")
func BuildUserPrompt(in GenerateInput) (string, error) {
count := in.Count
if count <= 0 {
count = 2
}
personaBlock := ""
if strings.TrimSpace(in.Persona) != "" {
personaBlock = "人設與語氣:\n" + strings.TrimSpace(in.Persona) + "\n"
}
audienceLine := ""
if strings.TrimSpace(in.AudienceBrief) != "" {
audienceLine = "受眾與情境:" + strings.TrimSpace(in.AudienceBrief)
}
productBrief := strings.TrimSpace(in.ProductBrief)
if productBrief == "" {
productBrief = "(尚未填寫品牌與產品,請先給實用建議,不要捏造品牌)"
}
reasonLine := ""
if strings.TrimSpace(in.PlacementReason) != "" {
reasonLine = "為何適合留言:" + strings.TrimSpace(in.PlacementReason)
}
topic := strings.TrimSpace(in.TopicLabel)
if topic == "" {
topic = "未指定"
}
author := strings.TrimSpace(in.AuthorName)
if author == "" {
author = "匿名"
}
return libprompt.OutreachPlacementUser(map[string]string{
"persona_block": personaBlock,
"topic_label": topic,
"audience_line": audienceLine,
"product_brief": productBrief,
"placement_reason_line": reasonLine,
"author_name": author,
"target_text": strings.TrimSpace(in.TargetText),
"count": fmt.Sprintf("%d", count),
})
}
func ParseGenerateOutput(raw string) (GenerateResult, error) {
payload, err := extractJSONObject(raw)
if err != nil {
return GenerateResult{}, err
}
var out GenerateResult
if err := json.Unmarshal(payload, &out); err != nil {
return GenerateResult{}, fmt.Errorf("parse outreach json: %w", err)
}
if len(out.Drafts) == 0 {
return GenerateResult{}, fmt.Errorf("outreach drafts missing")
}
for i := range out.Drafts {
out.Drafts[i].Text = trimDraftText(out.Drafts[i].Text)
if out.Drafts[i].Text == "" {
return GenerateResult{}, fmt.Errorf("outreach draft %d empty", i+1)
}
}
if out.Relevance < 0 {
out.Relevance = 0
}
if out.Relevance > 1 {
out.Relevance = 1
}
out.Reason = strings.TrimSpace(out.Reason)
return out, nil
}
func trimDraftText(text string) string {
text = strings.TrimSpace(text)
runes := []rune(text)
if len(runes) > maxChars {
return string(runes[:maxChars])
}
return text
}
func extractJSONObject(raw string) ([]byte, error) {
raw = strings.TrimSpace(raw)
if m := codeFenceRE.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("outreach output missing json object")
}
return []byte(raw[start : end+1]), nil
}