2026-04-01 00:58:42 +08:00
package app
import (
"encoding/json"
"fmt"
"regexp"
"strings"
2026-04-02 00:31:28 +08:00
"github.com/geo-platform/tenant-api/internal/shared/contentstats"
2026-04-05 17:14:13 +08:00
"github.com/geo-platform/tenant-api/internal/tenant/prompts"
2026-04-01 00:58:42 +08:00
)
var promptVariablePattern = regexp . MustCompile ( ` \ { \ { \s*([a-zA-Z0-9_]+)\s*\}\} ` )
2026-04-05 17:14:13 +08:00
func buildGenerationPrompt (
templateKey , templateName string ,
promptTemplate * string ,
params map [ string ] interface { } ,
knowledgePrompt string ,
) string {
2026-04-01 00:58:42 +08:00
basePrompt := strings . TrimSpace ( renderPromptTemplate ( promptTemplate , params ) )
if basePrompt == "" {
2026-04-05 17:14:13 +08:00
basePrompt = prompts . DefaultGenerationBasePrompt ( templateName )
2026-04-01 00:58:42 +08:00
}
var sections [ ] string
sections = append ( sections , basePrompt )
contextBlock := buildPromptContext ( params )
if contextBlock != "" {
2026-04-05 17:14:13 +08:00
sections = append ( sections , prompts . PromptContextSection ( contextBlock ) )
2026-04-01 00:58:42 +08:00
}
2026-07-10 20:10:39 +08:00
if brandPrompt := RenderBrandPromptContext ( extractBrandPromptContext ( params ) ) ; brandPrompt != "" {
sections = append ( sections , brandPrompt )
}
2026-04-01 00:58:42 +08:00
2026-04-05 17:14:13 +08:00
if strings . TrimSpace ( knowledgePrompt ) != "" {
sections = append ( sections , knowledgePrompt )
}
sections = append ( sections , prompts . GenerationWritingRequirementsSection ( ) )
2026-04-01 00:58:42 +08:00
2026-04-02 10:58:39 +08:00
if templateRules := buildTemplateSpecificWritingRules ( templateKey , params ) ; templateRules != "" {
2026-04-05 17:14:13 +08:00
sections = append ( sections , prompts . GenerationTemplateSpecificRulesHeading ( ) + "\n" + templateRules )
2026-04-02 10:58:39 +08:00
}
2026-04-02 00:31:28 +08:00
if lengthGuidance := buildGenerationLengthGuidance ( params ) ; lengthGuidance != "" {
2026-04-05 17:14:13 +08:00
sections = append ( sections , prompts . GenerationLengthGuidanceHeading ( ) + "\n" + lengthGuidance )
2026-04-02 00:31:28 +08:00
}
2026-07-09 19:38:55 +08:00
if localeGuard := buildGenerationLocaleGuard ( params ) ; localeGuard != "" {
sections = append ( sections , localeGuard )
}
2026-04-01 00:58:42 +08:00
return strings . Join ( sections , "\n\n" )
}
2026-07-09 19:38:55 +08:00
func buildGenerationLocaleGuard ( params map [ string ] interface { } ) string {
if strings . TrimSpace ( stringValue ( params [ "locale" ] ) ) != "en-US" {
return ""
}
return strings . Join ( [ ] string {
"High-priority language consistency requirement:" ,
"- The final article output must be entirely in natural, professional English." ,
"- Treat any Chinese template text, Chinese examples, Chinese section names, or Chinese disclaimers as internal instructions only; translate or adapt them into English before writing." ,
"- Do not output Chinese prose, Chinese headings, Chinese punctuation patterns, or Chinese-only required sentences unless they are official brand, company, product, or legal names that must remain unchanged." ,
"- If title, article_outline, outline_sections, key_points, or knowledge snippets contain Chinese, rewrite the generated article content in English while preserving the intended meaning." ,
} , "\n" )
}
2026-06-02 14:50:12 +08:00
func buildGenerationKnowledgeQuery ( templateKey , templateName string , params map [ string ] interface { } ) string {
2026-04-05 17:14:13 +08:00
if len ( params ) == 0 {
return ""
}
2026-06-02 14:50:12 +08:00
intent := knowledgeQueryIntent {
TaskType : templateKnowledgeTaskType ( templateKey , templateName ) ,
TemplateName : templateName ,
Title : firstNonEmptyPromptString ( params , "title" , "topic" , "product_name" , "subject" ) ,
Topic : firstNonEmptyPromptString ( params , "topic" , "subject" , "product_name" , "title" ) ,
BrandName : stringValue ( params [ "brand_name" ] ) ,
Region : firstNonEmptyPromptString ( params , "region" , "city" , "location" , "area" , "target_region" , "service_area" ) ,
PrimaryKeyword : firstNonEmptyPromptString ( params , "brand_question" , "primary_keyword" ) ,
Keywords : mergeKnowledgeQueryKeywords ( params [ "keywords" ] , params [ "supplemental_questions" ] , params [ "primary_keyword" ] ) ,
KeyPoints : firstNonEmptyPromptString ( params , "key_points" , "review_intro_hook" ) ,
2026-04-05 17:14:13 +08:00
}
2026-06-02 14:50:12 +08:00
return buildKnowledgeQueryInputText ( intent )
}
2026-04-05 17:14:13 +08:00
2026-06-02 14:50:12 +08:00
func templateKnowledgeTaskType ( templateKey , templateName string ) string {
switch strings . TrimSpace ( templateKey ) {
case "top_x_article" :
return "推荐榜文章"
case "product_review" :
return "产品评测文章"
case "research_report" :
return "研究报告"
default :
if name := strings . TrimSpace ( templateName ) ; name != "" {
return name
}
return "文章生成"
}
2026-04-05 17:14:13 +08:00
}
2026-04-02 10:58:39 +08:00
func buildTemplateSpecificWritingRules ( templateKey string , params map [ string ] interface { } ) string {
switch strings . TrimSpace ( templateKey ) {
case "top_x_article" :
brandName := strings . TrimSpace ( stringValue ( params [ "brand_name" ] ) )
if brandName == "" {
return ""
}
2026-04-05 17:14:13 +08:00
return prompts . TopXBrandPriorityRules ( brandName )
2026-04-02 10:58:39 +08:00
default :
return ""
}
}
2026-04-02 00:31:28 +08:00
func buildGenerationLengthGuidance ( params map [ string ] interface { } ) string {
sectionCount := estimateTopLevelSectionCount ( params )
if sectionCount <= 0 {
sectionCount = 4
}
locale := strings . TrimSpace ( stringValue ( params [ "locale" ] ) )
if locale == "en-US" {
minWords := 900
targetWords := 320 + sectionCount * 130
if targetWords > minWords {
minWords = targetWords
}
if minWords > 1600 {
minWords = 1600
}
maxWords := minWords + 400
if maxWords > 2100 {
maxWords = 2100
}
2026-04-05 17:14:13 +08:00
return prompts . EnglishLengthGuidance ( minWords , maxWords )
2026-04-02 00:31:28 +08:00
}
minChars := 1100
targetChars := 320 + sectionCount * 180
if targetChars > minChars {
minChars = targetChars
}
if minChars > 1800 {
minChars = 1800
}
maxChars := minChars + 500
if maxChars > 2400 {
maxChars = 2400
}
2026-04-05 17:14:13 +08:00
return prompts . ChineseLengthGuidance ( minChars , maxChars )
2026-04-02 00:31:28 +08:00
}
func estimateTopLevelSectionCount ( params map [ string ] interface { } ) int {
if params == nil {
return 0
}
switch value := params [ "article_outline" ] . ( type ) {
case [ ] interface { } :
if len ( value ) > 0 {
return len ( value )
}
case [ ] map [ string ] interface { } :
if len ( value ) > 0 {
return len ( value )
}
}
switch value := params [ "outline_sections" ] . ( type ) {
case [ ] interface { } :
if len ( value ) > 0 {
return len ( value )
}
case [ ] string :
if len ( value ) > 0 {
return len ( value )
}
}
return 0
}
2026-04-01 00:58:42 +08:00
func renderPromptTemplate ( promptTemplate * string , params map [ string ] interface { } ) string {
if promptTemplate == nil {
return ""
}
return promptVariablePattern . ReplaceAllStringFunc ( * promptTemplate , func ( match string ) string {
submatches := promptVariablePattern . FindStringSubmatch ( match )
if len ( submatches ) != 2 {
return match
}
value , ok := params [ submatches [ 1 ] ]
if ! ok {
return ""
}
return formatPromptValue ( value )
} )
}
func buildPromptContext ( params map [ string ] interface { } ) string {
if len ( params ) == 0 {
return ""
}
orderedKeys := [ ] string {
"locale" ,
"title" ,
"topic" ,
"product_name" ,
"subject" ,
"brand_name" ,
2026-05-13 15:59:39 +08:00
"brand_question" ,
"primary_question" ,
2026-04-02 00:31:28 +08:00
"primary_keyword" ,
2026-05-13 15:59:39 +08:00
"supplemental_questions" ,
2026-04-01 00:58:42 +08:00
"brand" ,
"category" ,
"count" ,
"depth" ,
2026-04-02 00:31:28 +08:00
"article_outline" ,
2026-04-01 00:58:42 +08:00
"outline_sections" ,
"key_points" ,
2026-04-02 00:31:28 +08:00
"review_intro_hook" ,
2026-04-01 00:58:42 +08:00
"keywords" ,
"competitors" ,
}
used := make ( map [ string ] struct { } , len ( params ) )
lines := make ( [ ] string , 0 , len ( params ) )
appendLine := func ( key string , value interface { } ) {
if value == nil {
return
}
2026-04-05 17:14:13 +08:00
switch key {
2026-05-24 22:19:43 +08:00
case "knowledge_group_ids" , "knowledge_groups" , "knowledge_context" , "official_website" , "website" , "input_params" :
2026-04-05 17:14:13 +08:00
return
}
2026-04-02 00:31:28 +08:00
if key == "outline_sections" && hasStructuredOutline ( params [ "article_outline" ] ) {
return
}
formatted := strings . TrimSpace ( formatPromptContextValue ( key , value ) )
2026-04-01 00:58:42 +08:00
if formatted == "" {
return
}
2026-04-02 10:58:39 +08:00
lines = append ( lines , fmt . Sprintf ( "- %s: %s" , promptContextLabel ( key ) , formatted ) )
2026-04-01 00:58:42 +08:00
used [ key ] = struct { } { }
}
for _ , key := range orderedKeys {
appendLine ( key , params [ key ] )
}
for key , value := range params {
if _ , ok := used [ key ] ; ok {
continue
}
appendLine ( key , value )
}
return strings . Join ( lines , "\n" )
}
2026-04-02 10:58:39 +08:00
func promptContextLabel ( key string ) string {
2026-04-05 17:14:13 +08:00
return prompts . ContextLabel ( key )
2026-04-02 10:58:39 +08:00
}
2026-04-02 00:31:28 +08:00
func formatPromptContextValue ( key string , value interface { } ) string {
switch key {
case "keywords" :
return formatKeywordList ( value , 6 )
case "competitors" :
return formatCompetitorList ( value , 6 )
case "outline_sections" :
return formatSectionList ( value , 10 )
case "article_outline" :
return formatOutlineValue ( value )
default :
return formatPromptValue ( value )
}
}
2026-04-01 00:58:42 +08:00
func formatPromptValue ( value interface { } ) string {
switch v := value . ( type ) {
case nil :
return ""
case string :
return strings . TrimSpace ( v )
case [ ] string :
return strings . Join ( v , ", " )
case [ ] interface { } :
2026-04-02 00:31:28 +08:00
if hasOutlineItems ( v ) {
return formatOutlineItems ( v , 0 )
}
if hasNamedItems ( v ) {
return formatNamedItems ( v , 6 )
}
2026-04-01 00:58:42 +08:00
parts := make ( [ ] string , 0 , len ( v ) )
for _ , item := range v {
formatted := strings . TrimSpace ( formatPromptValue ( item ) )
if formatted != "" {
parts = append ( parts , formatted )
}
}
return strings . Join ( parts , ", " )
default :
bytes , err := json . Marshal ( v )
if err != nil {
return fmt . Sprint ( v )
}
return string ( bytes )
}
}
2026-04-02 00:31:28 +08:00
func formatKeywordList ( value interface { } , limit int ) string {
items := extractStringList ( value , limit )
return strings . Join ( items , ", " )
}
func formatSectionList ( value interface { } , limit int ) string {
items := extractStringList ( value , limit )
return strings . Join ( items , " > " )
}
func formatCompetitorList ( value interface { } , limit int ) string {
switch items := value . ( type ) {
case [ ] interface { } :
names := make ( [ ] string , 0 , len ( items ) )
for _ , item := range items {
if data , ok := item . ( map [ string ] interface { } ) ; ok {
name := strings . TrimSpace ( stringValue ( data [ "name" ] ) )
if name == "" {
name = strings . TrimSpace ( stringValue ( data [ "brand_name" ] ) )
}
if name != "" {
names = append ( names , name )
}
}
if len ( names ) >= limit {
break
}
}
return strings . Join ( names , ", " )
default :
return formatPromptValue ( value )
}
}
func formatOutlineValue ( value interface { } ) string {
switch items := value . ( type ) {
case [ ] interface { } :
return formatOutlineItems ( items , 0 )
default :
return formatPromptValue ( value )
}
}
func hasStructuredOutline ( value interface { } ) bool {
switch items := value . ( type ) {
case [ ] interface { } :
return hasOutlineItems ( items )
default :
return false
}
}
func hasOutlineItems ( items [ ] interface { } ) bool {
for _ , item := range items {
if data , ok := item . ( map [ string ] interface { } ) ; ok {
if strings . TrimSpace ( stringValue ( data [ "outline" ] ) ) != "" {
return true
}
}
}
return false
}
func hasNamedItems ( items [ ] interface { } ) bool {
for _ , item := range items {
if data , ok := item . ( map [ string ] interface { } ) ; ok {
if strings . TrimSpace ( stringValue ( data [ "name" ] ) ) != "" || strings . TrimSpace ( stringValue ( data [ "brand_name" ] ) ) != "" {
return true
}
}
}
return false
}
func formatNamedItems ( items [ ] interface { } , limit int ) string {
names := make ( [ ] string , 0 , len ( items ) )
for _ , item := range items {
data , ok := item . ( map [ string ] interface { } )
if ! ok {
continue
}
name := strings . TrimSpace ( stringValue ( data [ "name" ] ) )
if name == "" {
name = strings . TrimSpace ( stringValue ( data [ "brand_name" ] ) )
}
if name == "" {
continue
}
names = append ( names , name )
if len ( names ) >= limit {
break
}
}
return strings . Join ( names , ", " )
}
func formatOutlineItems ( items [ ] interface { } , level int ) string {
lines := make ( [ ] string , 0 )
indent := strings . Repeat ( " " , level )
for _ , item := range items {
node , ok := item . ( map [ string ] interface { } )
if ! ok {
continue
}
outline := strings . TrimSpace ( stringValue ( node [ "outline" ] ) )
if outline == "" {
continue
}
lines = append ( lines , fmt . Sprintf ( "%s- %s" , indent , outline ) )
children , ok := node [ "children" ] . ( [ ] interface { } )
if ok && len ( children ) > 0 {
childText := formatOutlineItems ( children , level + 1 )
if childText != "" {
lines = append ( lines , childText )
}
}
}
return strings . Join ( lines , "\n" )
}
func extractStringList ( value interface { } , limit int ) [ ] string {
items := make ( [ ] string , 0 )
appendItem := func ( text string ) {
text = strings . TrimSpace ( text )
if text == "" {
return
}
items = append ( items , text )
}
switch list := value . ( type ) {
case [ ] string :
for _ , item := range list {
appendItem ( item )
if limit > 0 && len ( items ) >= limit {
break
}
}
case [ ] interface { } :
for _ , item := range list {
appendItem ( formatPromptValue ( item ) )
if limit > 0 && len ( items ) >= limit {
break
}
}
}
return items
}
2026-04-01 00:58:42 +08:00
func resolveArticleTitle ( params map [ string ] interface { } , markdown string ) string {
if title := strings . TrimSpace ( stringValue ( params [ "title" ] ) ) ; title != "" {
return title
}
if topic := strings . TrimSpace ( stringValue ( params [ "topic" ] ) ) ; topic != "" {
return topic
}
if product := strings . TrimSpace ( stringValue ( params [ "product_name" ] ) ) ; product != "" {
return product + " Review"
}
if subject := strings . TrimSpace ( stringValue ( params [ "subject" ] ) ) ; subject != "" {
return subject + " Report"
}
for _ , line := range strings . Split ( markdown , "\n" ) {
line = strings . TrimSpace ( line )
if strings . HasPrefix ( line , "#" ) {
return strings . TrimSpace ( strings . TrimLeft ( line , "#" ) )
}
}
return "Generated Article"
}
func estimateWordCount ( markdown string ) int {
2026-04-02 00:31:28 +08:00
return contentstats . CountWords ( markdown )
2026-04-01 00:58:42 +08:00
}
func stringValue ( value interface { } ) string {
if value == nil {
return ""
}
text , ok := value . ( string )
if ! ok {
return ""
}
return text
}