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
@@ -0,0 +1,19 @@
package generate
import (
"strings"
"testing"
)
func TestBuildKolGenerationPromptPlacesBrandBeforeKnowledge(t *testing.T) {
prompt := buildKolGenerationPrompt("TASK_SENTINEL", "BRAND_SENTINEL", "KNOWLEDGE_SENTINEL")
markers := []string{"TASK_SENTINEL", "BRAND_SENTINEL", "KNOWLEDGE_SENTINEL"}
previous := -1
for _, marker := range markers {
index := strings.Index(prompt, marker)
if index < 0 || index <= previous {
t.Fatalf("buildKolGenerationPrompt() = %q, markers out of order: %#v", prompt, markers)
}
previous = index
}
}
@@ -114,8 +114,16 @@ func (w *KolGenerationWorker) Process(ctx context.Context, task tenantapp.Claime
w.HandleTaskFailure(ctx, task, "task_load", err)
return
}
brandID, brandPromptContext, err := tenantapp.ResolveGenerationBrandPromptContext(ctx, w.pool, task.TenantID, task.ArticleID, task.InputParams)
if err != nil {
w.HandleTaskFailure(ctx, task, "brand_context_load", err)
return
}
task.InputParams["brand_id"] = brandID
brandPrompt := tenantapp.RenderBrandPromptContext(brandPromptContext)
knowledgePrompt := ""
var knowledgeFactConstraints []tenantapp.KnowledgeFactConstraint
if knowledgeGroupIDs := kolKnowledgeGroupIDs(task.InputParams["knowledge_group_ids"]); len(knowledgeGroupIDs) > 0 {
if w.knowledge == nil {
w.HandleTaskFailure(ctx, task, "knowledge_resolve", fmt.Errorf("knowledge service is not configured"))
@@ -125,7 +133,7 @@ func (w *KolGenerationWorker) Process(ctx context.Context, task tenantapp.Claime
knowledgeContext, err := w.knowledge.ResolveContext(
ctx,
task.TenantID,
int64(kolIntInput(task.InputParams, "brand_id")),
brandID,
knowledgeGroupIDs,
buildKolKnowledgeQuery(task.InputParams),
)
@@ -134,12 +142,10 @@ func (w *KolGenerationWorker) Process(ctx context.Context, task tenantapp.Claime
return
}
knowledgePrompt = w.knowledge.RenderPromptSection(knowledgeContext)
knowledgeFactConstraints = knowledgeContext.FactConstraints
}
prompt := renderedPrompt
if strings.TrimSpace(knowledgePrompt) != "" {
prompt = strings.TrimSpace(renderedPrompt + "\n\n" + knowledgePrompt)
}
prompt := buildKolGenerationPrompt(renderedPrompt, brandPrompt, knowledgePrompt)
articleTimeout, maxOutputTokens := w.runtimeConfig()
generateReq := llm.GenerateRequest{
@@ -151,7 +157,8 @@ func (w *KolGenerationWorker) Process(ctx context.Context, task tenantapp.Claime
generateReq.WebSearch = &llm.WebSearchOptions{Enabled: true}
}
result, err := w.llm.Generate(ctx, generateReq, nil)
knowledgeFactConstraints = tenantapp.MergeKnowledgeFactConstraints(brandPromptContext.FactConstraints, knowledgeFactConstraints)
result, err := tenantapp.GenerateArticleWithKnowledgeFactGuard(ctx, w.llm, generateReq, knowledgeFactConstraints, nil)
if err != nil {
w.HandleTaskFailure(ctx, task, "llm_generate", err)
return
@@ -263,6 +270,16 @@ func (w *KolGenerationWorker) Process(ctx context.Context, task tenantapp.Claime
})
}
func buildKolGenerationPrompt(renderedPrompt string, brandPrompt string, knowledgePrompt string) string {
sections := make([]string, 0, 3)
for _, section := range []string{renderedPrompt, brandPrompt, knowledgePrompt} {
if section = strings.TrimSpace(section); section != "" {
sections = append(sections, section)
}
}
return strings.Join(sections, "\n\n")
}
func (w *KolGenerationWorker) HandleTaskFailure(ctx context.Context, task tenantapp.ClaimedGenerationTask, stage string, taskErr error) {
if err := tenantapp.HandleKolGenerationTaskFailure(ctx, w.pool, w.cache, task, stage, taskErr); err != nil && w.logger != nil {
w.logger.Warn("kol generation task failure handling failed",