feat(tenant): inject brand description context into every generation path

Include the current brand description in template, custom, imitation,
and KOL article-generation prompts while bounding prompt growth.

- Add a shared brand-context builder that resolves the current brand,
  compacts long descriptions, and caps rendered output at 2000 runes.
- Snapshot a structured brand prompt context on queued tasks so old
  jobs stay reproducible and retries stay stable.
- Route brand-profile facts through the existing typed fact guard so
  founding dates and year counts get the same pre-persistence validation
  as RAG facts, with brand facts outranking conflicting RAG facts.
- Fix the KOL creator to attach the brand scope its worker consumes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 20:10:39 +08:00
parent 418b3c4998
commit 0bf4b73ebc
12 changed files with 998 additions and 32 deletions
@@ -1,6 +1,10 @@
package app
import "github.com/geo-platform/tenant-api/internal/tenant/repository"
import (
"encoding/json"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
const (
generationPayloadTemplateKey = "__snapshot_template_key"
@@ -13,6 +17,8 @@ const (
generationPayloadPromptRuleTone = "__snapshot_prompt_rule_tone"
generationPayloadPromptRuleWordCount = "__snapshot_prompt_rule_word_count"
generationPayloadKnowledgePrompt = "__snapshot_knowledge_prompt"
generationPayloadKnowledgeFacts = "__snapshot_knowledge_fact_constraints"
generationPayloadBrandPromptContext = "__snapshot_brand_prompt_context"
)
func attachTemplateSnapshot(payload map[string]interface{}, record *repository.TemplateRecord) map[string]interface{} {
@@ -32,6 +38,65 @@ func attachTemplateSnapshot(payload map[string]interface{}, record *repository.T
return payload
}
func attachBrandPromptContext(payload map[string]interface{}, brandCtx *BrandPromptContext) map[string]interface{} {
if payload == nil {
payload = map[string]interface{}{}
}
if brandCtx == nil {
delete(payload, generationPayloadBrandPromptContext)
return payload
}
normalized := normalizeBrandPromptContext(*brandCtx)
payload[generationPayloadBrandPromptContext] = &normalized
return payload
}
func extractBrandPromptContext(payload map[string]interface{}) *BrandPromptContext {
if payload == nil || payload[generationPayloadBrandPromptContext] == nil {
return nil
}
encoded, err := json.Marshal(payload[generationPayloadBrandPromptContext])
if err != nil {
return nil
}
var brandCtx BrandPromptContext
if err := json.Unmarshal(encoded, &brandCtx); err != nil {
return nil
}
if brandCtx.BrandID <= 0 {
return nil
}
brandCtx = normalizeBrandPromptContext(brandCtx)
return &brandCtx
}
func attachKnowledgeFactConstraints(payload map[string]interface{}, constraints []KnowledgeFactConstraint) map[string]interface{} {
if payload == nil {
payload = map[string]interface{}{}
}
if len(constraints) == 0 {
delete(payload, generationPayloadKnowledgeFacts)
return payload
}
payload[generationPayloadKnowledgeFacts] = constraints
return payload
}
func extractKnowledgeFactConstraints(payload map[string]interface{}) []KnowledgeFactConstraint {
if payload == nil || payload[generationPayloadKnowledgeFacts] == nil {
return nil
}
encoded, err := json.Marshal(payload[generationPayloadKnowledgeFacts])
if err != nil {
return nil
}
var constraints []KnowledgeFactConstraint
if err := json.Unmarshal(encoded, &constraints); err != nil {
return nil
}
return constraints
}
func extractTemplateSnapshot(payload map[string]interface{}) (string, string, *string) {
templateKey := extractString(payload, generationPayloadTemplateKey)
templateName := extractString(payload, generationPayloadTemplateName)