152 lines
4.5 KiB
Go
152 lines
4.5 KiB
Go
|
|
package placement
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
branddomain "haixun-backend/internal/model/brand/domain/usecase"
|
|||
|
|
topicdomain "haixun-backend/internal/model/placement_topic/domain/usecase"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// PlacementTopicContext bundles brand, product, and user-entered topic fields for prompts.
|
|||
|
|
type PlacementTopicContext struct {
|
|||
|
|
BrandDisplayName string
|
|||
|
|
TopicName string
|
|||
|
|
SeedQuery string
|
|||
|
|
Brief string
|
|||
|
|
Goals string
|
|||
|
|
TargetAudience string
|
|||
|
|
ProductContext string
|
|||
|
|
ProductBrief string
|
|||
|
|
ProductLabel string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func PlacementTopicContextFromTopic(topic *topicdomain.TopicSummary, brand *branddomain.BrandSummary, productBrief string) PlacementTopicContext {
|
|||
|
|
if topic == nil {
|
|||
|
|
return PlacementTopicContextFromBrand(brand, productBrief)
|
|||
|
|
}
|
|||
|
|
merged := brandSummaryForTopic(topic, brand)
|
|||
|
|
return PlacementTopicContextFromBrand(merged, productBrief)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func brandSummaryForTopic(topic *topicdomain.TopicSummary, brand *branddomain.BrandSummary) *branddomain.BrandSummary {
|
|||
|
|
if brand == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
out := *brand
|
|||
|
|
out.TopicName = strings.TrimSpace(topic.TopicName)
|
|||
|
|
out.SeedQuery = strings.TrimSpace(topic.SeedQuery)
|
|||
|
|
out.Brief = strings.TrimSpace(topic.Brief)
|
|||
|
|
out.ProductID = strings.TrimSpace(topic.ProductID)
|
|||
|
|
out.ResearchMap = topic.ResearchMap
|
|||
|
|
return &out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func PlacementTopicContextFromBrand(brand *branddomain.BrandSummary, productBrief string) PlacementTopicContext {
|
|||
|
|
if brand == nil {
|
|||
|
|
return PlacementTopicContext{}
|
|||
|
|
}
|
|||
|
|
ctx := PlacementTopicContext{
|
|||
|
|
BrandDisplayName: strings.TrimSpace(brand.DisplayName),
|
|||
|
|
TopicName: strings.TrimSpace(brand.TopicName),
|
|||
|
|
SeedQuery: strings.TrimSpace(brand.SeedQuery),
|
|||
|
|
Brief: strings.TrimSpace(brand.Brief),
|
|||
|
|
Goals: strings.TrimSpace(brand.Goals),
|
|||
|
|
TargetAudience: strings.TrimSpace(brand.TargetAudience),
|
|||
|
|
ProductContext: strings.TrimSpace(brand.ProductContext),
|
|||
|
|
ProductBrief: strings.TrimSpace(productBrief),
|
|||
|
|
ProductLabel: productLabelFromBrand(brand),
|
|||
|
|
}
|
|||
|
|
if ctx.ProductBrief == "" {
|
|||
|
|
ctx.ProductBrief = strings.TrimSpace(brand.ProductBrief)
|
|||
|
|
}
|
|||
|
|
return ctx
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func productLabelFromBrand(brand *branddomain.BrandSummary) string {
|
|||
|
|
if brand == nil {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
productID := strings.TrimSpace(brand.ProductID)
|
|||
|
|
if productID == "" {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
for _, item := range brand.Products {
|
|||
|
|
if item.ID == productID {
|
|||
|
|
return strings.TrimSpace(item.Label)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (c PlacementTopicContext) ToResearchMapInput() ResearchMapInput {
|
|||
|
|
return ResearchMapInput{
|
|||
|
|
BrandDisplayName: c.BrandDisplayName,
|
|||
|
|
TopicName: c.TopicName,
|
|||
|
|
SeedQuery: c.SeedQuery,
|
|||
|
|
Brief: c.Brief,
|
|||
|
|
Goals: c.Goals,
|
|||
|
|
TargetAudience: c.TargetAudience,
|
|||
|
|
ProductContext: c.ProductContext,
|
|||
|
|
ProductBrief: c.ProductBrief,
|
|||
|
|
ProductLabel: c.ProductLabel,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (c PlacementTopicContext) WritePromptBlock(b *strings.Builder) {
|
|||
|
|
writePromptSection(b, "品牌", c.BrandDisplayName)
|
|||
|
|
writePromptSection(b, "置入產品", c.ProductDisplayName())
|
|||
|
|
writePromptSection(b, "主題名稱", c.TopicName)
|
|||
|
|
writePromptSection(b, "種子關鍵字", c.SeedQuery)
|
|||
|
|
writePromptSection(b, "主題目標", c.Brief)
|
|||
|
|
writePromptSection(b, "置入目標/備註", c.Goals)
|
|||
|
|
writePromptSection(b, "已知受眾描述", c.TargetAudience)
|
|||
|
|
productDetail := FormatProductContextForPrompt(c.ProductContext)
|
|||
|
|
if productDetail == "" && c.ProductBrief != "" {
|
|||
|
|
productDetail = c.ProductBrief
|
|||
|
|
}
|
|||
|
|
if productDetail != "" {
|
|||
|
|
b.WriteString("【產品詳情】\n")
|
|||
|
|
b.WriteString(productDetail)
|
|||
|
|
b.WriteString("\n")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (c PlacementTopicContext) ProductDisplayName() string {
|
|||
|
|
fields := ParseProductContext(c.ProductContext)
|
|||
|
|
if fields.Product != "" {
|
|||
|
|
return fields.Product
|
|||
|
|
}
|
|||
|
|
return c.ProductLabel
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func writePromptSection(b *strings.Builder, label, value string) {
|
|||
|
|
value = strings.TrimSpace(value)
|
|||
|
|
if value == "" {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
b.WriteString("【")
|
|||
|
|
b.WriteString(label)
|
|||
|
|
b.WriteString("】")
|
|||
|
|
b.WriteString(value)
|
|||
|
|
b.WriteString("\n")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PromptLines returns optional labeled lines for knowledge graph template vars.
|
|||
|
|
func (c PlacementTopicContext) PromptLines() map[string]string {
|
|||
|
|
productName := c.ProductDisplayName()
|
|||
|
|
return map[string]string{
|
|||
|
|
"brand_line": optionalPromptLine("品牌", c.BrandDisplayName),
|
|||
|
|
"topic_line": optionalPromptLine("主題名稱", c.TopicName),
|
|||
|
|
"product_line": optionalPromptLine("置入產品", productName),
|
|||
|
|
"goals_line": optionalPromptLine("置入目標", c.Goals),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func optionalPromptLine(label, value string) string {
|
|||
|
|
value = strings.TrimSpace(value)
|
|||
|
|
if value == "" {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return label + ":" + value + "\n"
|
|||
|
|
}
|