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
+18 -2
View File
@@ -287,6 +287,10 @@ func (s *TemplateService) Generate(ctx context.Context, templateID int64, req Ge
if err := s.llm.Validate(); err != nil {
return nil, response.ErrServiceUnavailable(50311, "llm_unavailable", err.Error())
}
brandPromptContext, err := loadBrandPromptContext(ctx, s.pool, actor.TenantID, brandID)
if err != nil {
return nil, err
}
initialTitle := resolveArticleTitle(req.InputParams, "")
wizardStateJSON, err := json.Marshal(normalizeWizardState(req.WizardState, wizardStateCurrentStep(req.WizardState)))
@@ -300,11 +304,13 @@ func (s *TemplateService) Generate(ctx context.Context, templateID int64, req Ge
}
taskInputParams := cloneInputParams(req.InputParams)
taskInputParams["brand_id"] = brandID
taskInputParams["enable_web_search"] = req.EnableWebSearch
if req.WebSearchLimit != nil {
taskInputParams["web_search_limit"] = *req.WebSearchLimit
}
taskInputParams = attachTemplateSnapshot(taskInputParams, templateRecord)
taskInputParams = attachBrandPromptContext(taskInputParams, brandPromptContext)
inputJSON, _ := json.Marshal(taskInputParams)
tx, err := s.pool.Begin(ctx)
@@ -480,8 +486,16 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
}
RecordGenerationTaskEvent(GenerationTaskEventTransition, generationJobLogContext(job, "task_status_running", "", GenerationTaskStatusRunning, GenerationTaskResultSuccess))
LogGenerationTaskInfo(s.logger, "article generation task marked running", generationJobLogContext(job, "task_status_running", "", GenerationTaskStatusRunning, GenerationTaskResultSuccess))
brandID, brandPromptContext, err := ResolveGenerationBrandPromptContext(ctx, s.pool, job.TenantID, job.ArticleID, job.Params)
if err != nil {
s.failGeneration(ctx, job, title, "brand_context_load", err)
return
}
job.Params["brand_id"] = brandID
job.Params = attachBrandPromptContext(job.Params, brandPromptContext)
knowledgePrompt := ""
var knowledgeFactConstraints []KnowledgeFactConstraint
if knowledgeGroupIDs := extractKnowledgeGroupIDs(job.Params["knowledge_group_ids"]); len(knowledgeGroupIDs) > 0 {
if s.knowledge == nil {
s.failGeneration(ctx, job, title, "knowledge_resolve", fmt.Errorf("knowledge service is not configured"))
@@ -490,7 +504,7 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
ctx,
job.TenantID,
int64(extractInt(job.Params, "brand_id")),
brandID,
knowledgeGroupIDs,
buildGenerationKnowledgeQuery(job.TemplateKey, job.TemplateName, job.Params),
)
@@ -499,6 +513,7 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
return
}
knowledgePrompt = s.knowledge.RenderPromptSection(knowledgeContext)
knowledgeFactConstraints = knowledgeContext.FactConstraints
}
prompt := buildGenerationPrompt(job.TemplateKey, job.TemplateName, job.PromptTemplate, job.Params, knowledgePrompt)
@@ -511,7 +526,8 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
if job.WebSearch.Enabled {
generateReq.WebSearch = &job.WebSearch
}
result, err := s.llm.Generate(ctx, generateReq, func(delta string) {
knowledgeFactConstraints = MergeKnowledgeFactConstraints(brandPromptContext.FactConstraints, knowledgeFactConstraints)
result, err := GenerateArticleWithKnowledgeFactGuard(ctx, s.llm, generateReq, knowledgeFactConstraints, func(delta string) {
if runtimeCfg.StreamEnabled && delta != "" {
s.streams.AppendDelta(job.ArticleID, job.TaskID, title, delta)
}