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

139 lines
3.8 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 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
Regenerate bool
PreviousDraftsHint string
}
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 = "匿名"
}
regenerateLine := ""
if in.Regenerate || strings.TrimSpace(in.PreviousDraftsHint) != "" {
regenerateLine = "\n【重新生成】請產出與舊版明顯不同的新草稿換開頭、換切角、換句式與情境細節不要複製舊文案。"
if hint := strings.TrimSpace(in.PreviousDraftsHint); hint != "" {
regenerateLine += "\n" + hint
}
}
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),
"regenerate_line": regenerateLine,
"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 = NormalizeDraftText(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
}