129 lines
3.3 KiB
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
|
||
|
|
}
|