feat: Enhance article generation and outline handling

- Added support for external markdown synchronization in ArticleEditorCanvas.vue.
- Implemented a new function to sync markdown from props, ensuring the editor reflects external changes.
- Introduced a removeOutlineSection function in TemplateWizardView.vue to manage outline sections dynamically.
- Updated UI components to improve user experience, including replacing a-input with a-textarea for better text handling.
- Refactored CSS styles for a cleaner layout and improved responsiveness in TemplateWizardView.vue.
- Enhanced LLM generation requests to include response format options in ark.go and client.go.
- Updated template assist logic to enforce structured outline outputs in template_assist.go.
- Added tests for outline result decoding and prompt generation to ensure robustness.
- Created migration scripts to update prompt templates with new writing requirements for top X articles.
This commit is contained in:
2026-04-02 10:58:39 +08:00
parent b31d8d0096
commit 7fa1809682
12 changed files with 605 additions and 106 deletions
+3 -1
View File
@@ -154,8 +154,10 @@ func main() {
写作要求:
- 不要写成空洞的排行列表;每个对象都要有清晰的入选理由和判断依据。
- 如果提供了 competitors、品牌或关键词上下文,要自然融入横向比较,但不要机械重复名单。
- 如果提供了 brand_name,且文章属于品牌、公司或服务商对比型推荐内容,应将该品牌作为主推对象:默认排在第 1 位、正文篇幅明显多于其他对象、结论优先推荐。
- 对 brand_name 对应对象重点展开核心优势、本地服务、适合人群、报价或配置亮点、交付安装、售后与避坑建议;其他对象保持客观但更简洁的差异化介绍。
- 重点写筛选标准、差异点、适合谁、不适合谁,以及常见误区或避坑点。
- 引言先交代推荐标准与读者能得到什么,结尾给出简洁明确的选择建议`,
- 引言先交代推荐标准与读者能得到什么,结尾优先总结 brand_name 为什么值得优先考虑,再补充其他对象各自适合的预算与需求`,
`{
"hero": {
"accent": "Top X",
+27
View File
@@ -132,6 +132,32 @@ func (c *arkClient) Generate(ctx context.Context, req GenerateRequest, onDelta f
},
}
var textFormat *responses.ResponsesText
if req.ResponseFormat != nil {
format := &responses.TextFormat{
Name: strings.TrimSpace(req.ResponseFormat.Name),
}
switch req.ResponseFormat.Type {
case ResponseFormatTypeJSONSchema:
format.Type = responses.TextType_json_schema
case ResponseFormatTypeJSONObject:
format.Type = responses.TextType_json_object
default:
format.Type = responses.TextType_text
}
if description := strings.TrimSpace(req.ResponseFormat.Description); description != "" {
format.Description = &description
}
if len(req.ResponseFormat.SchemaJSON) > 0 {
format.Schema = &responses.Bytes{Value: req.ResponseFormat.SchemaJSON}
}
if req.ResponseFormat.Strict {
strict := true
format.Strict = &strict
}
textFormat = &responses.ResponsesText{Format: format}
}
var tools []*responses.ResponsesTool
if req.WebSearch != nil && req.WebSearch.Enabled {
webSearchLimit := c.webSearchLimit
@@ -159,6 +185,7 @@ func (c *arkClient) Generate(ctx context.Context, req GenerateRequest, onDelta f
Temperature: &c.temperature,
TopP: c.topP,
Tools: tools,
Text: textFormat,
Reasoning: c.reasoning,
Input: &responses.ResponsesInput{
Union: &responses.ResponsesInput_ListValue{
+17
View File
@@ -17,6 +17,7 @@ type GenerateRequest struct {
Timeout time.Duration
MaxOutputTokens int64
WebSearch *WebSearchOptions
ResponseFormat *ResponseFormat
}
type GenerateResult struct {
@@ -24,6 +25,22 @@ type GenerateResult struct {
Model string
}
type ResponseFormat struct {
Type ResponseFormatType
Name string
Description string
SchemaJSON []byte
Strict bool
}
type ResponseFormatType string
const (
ResponseFormatTypeText ResponseFormatType = "text"
ResponseFormatTypeJSONObject ResponseFormatType = "json_object"
ResponseFormatTypeJSONSchema ResponseFormatType = "json_schema"
)
type WebSearchOptions struct {
Enabled bool
Limit int32
+159 -45
View File
@@ -471,6 +471,11 @@ func (s *TemplateService) executeOutlineTask(ctx context.Context, repo repositor
result, err := s.llm.Generate(ctx, llm.GenerateRequest{
Prompt: buildOutlinePrompt(job.TemplateKey, job.TemplateName, *job.OutlineRequest, job.OutlinePrompt),
ResponseFormat: &llm.ResponseFormat{
Type: llm.ResponseFormatTypeJSONObject,
Name: "article_outline",
Description: "文章大纲 JSON 对象,必须包含 outline 数组字段。",
},
}, nil)
if err != nil {
s.failAssist(ctx, job.TaskID, job.TaskType, job.TenantID, err)
@@ -699,9 +704,9 @@ func buildAnalyzePrompt(templateKey, templateName string, req AnalyzeTaskRequest
规则:
- 仅返回 JSON,不要用 Markdown 代码块包裹。
- 使用与请求 locale 一致的语言。zh-CN 用简体中文,en-US 用英文。
- 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。
- 关键词应为简洁的搜索短语。
- 竞品应去重且真实。不确定时 website 可留空,但不要编造虚假 URL
- 竞品应去重且真实。不确定时 website 字段可留空,但不要编造虚假链接
- brand_summary 限 1-2 句。
- 最多返回 6 个竞品和 5 个关键词。
@@ -750,7 +755,7 @@ func buildTitlePrompt(templateKey, templateName string, req TitleTaskRequest, ti
规则:
- 仅返回 JSON,不要用 Markdown 代码块包裹。
- 返回 5 个字符串的 JSON 数组,不要返回对象。
- 使用与请求 locale 一致的语言。zh-CN 用简体中文,en-US 用英文。
- 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。
- 标题应实用、具体,并契合当前模板的内容方向。
- 尊重当前关键词列表和竞品上下文,因为用户可能已手动编辑。
- 避免泛泛的广告文案和空洞口号。
@@ -765,6 +770,25 @@ JSON 输出示例:
func buildOutlinePrompt(templateKey, templateName string, req OutlineTaskRequest, outlinePromptTemplate *string) string {
contextPayload := outlinePromptParams(templateKey, templateName, req)
outputExample := `{
"outline": [
{
"outline": "网站列表",
"children": [
{ "outline": "竞品 A" },
{ "outline": "竞品 B" }
]
},
{
"outline": "文章关键要点",
"children": [
{ "outline": "要点 1" },
{ "outline": "要点 2" }
]
}
]
}`
if outlinePromptTemplate != nil && strings.TrimSpace(*outlinePromptTemplate) != "" {
basePrompt := strings.TrimSpace(renderPromptTemplate(outlinePromptTemplate, contextPayload))
if basePrompt != "" {
@@ -775,31 +799,18 @@ func buildOutlinePrompt(templateKey, templateName string, req OutlineTaskRequest
sections = append(sections, strings.Join([]string{
"输出要求:",
"- 仅返回 JSON,不要用 Markdown 代码块包裹。",
`- 返回大纲节点的 JSON 数组,每个节点格式{"outline":"...","children":[...]}`,
`- 返回 JSON 对象,格式固定{"outline":[...]}`,
`- outline 字段是顶层大纲节点数组,每个节点格式为 {"outline":"...","children":[...]}`,
"- 每个已选大纲段落应作为顶层节点出现,语言与请求保持一致。",
"- 尊重当前关键词、竞品和关键要点,因为用户可能已手动编辑。",
"- 不要返回额外字段,不要返回纯数组,不要返回说明文字。",
}, "\n"))
sections = append(sections, "JSON 输出示例:\n"+outputExample)
return strings.Join(sections, "\n\n")
}
}
contextJSON, _ := json.MarshalIndent(contextPayload, "", " ")
outputExample := `[
{
"outline": "网站列表",
"children": [
{ "outline": "竞品 A" },
{ "outline": "竞品 B" }
]
},
{
"outline": "文章关键要点",
"children": [
{ "outline": "要点 1" },
{ "outline": "要点 2" }
]
}
]`
return strings.TrimSpace(fmt.Sprintf(`
你是一位专业的 GEO 内容策略师。
@@ -809,17 +820,18 @@ func buildOutlinePrompt(templateKey, templateName string, req OutlineTaskRequest
规则:
- 仅返回 JSON,不要用 Markdown 代码块包裹。
- 返回顶层大纲节点的 JSON 数组
- 每个顶层节点 "outline" 值必须保持已选段落标签原文。
- 返回 JSON 对象,格式固定为 {"outline":[...]}
- outline 字段中的每个顶层节点 "outline" 值必须保持已选段落标签原文。
- 在合适的位置为每个顶层段落添加简洁的子大纲条目。
- 使用与请求 locale 一致的语言。zh-CN 用简体中文,en-US 用英文。
- 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。
- 尊重当前标题、关键词、竞品、关键要点和品牌上下文,因为用户可能已手动编辑。
- 大纲应具体、不含推广语气,适合后续完整成文。
- 不要返回额外字段,不要返回纯数组,不要返回说明文字。
Template context:
模板上下文:
%s
Output JSON example:
JSON 输出示例:
%s
`, string(contextJSON), outputExample))
}
@@ -996,29 +1008,45 @@ func decodeTitleResult(raw string) ([]string, error) {
}
func decodeOutlineResult(raw string) ([]OutlineNode, error) {
cleaned := stripJSONFence(raw)
var lastErr error
for _, candidate := range extractJSONCandidates(raw) {
var result []OutlineNode
if err := json.Unmarshal([]byte(candidate), &result); err == nil {
return result, nil
} else {
lastErr = err
}
var result []OutlineNode
if err := json.Unmarshal([]byte(cleaned), &result); err == nil {
return result, nil
var wrapped struct {
Result []OutlineNode `json:"result"`
Outline []OutlineNode `json:"outline"`
Items []OutlineNode `json:"items"`
Data []OutlineNode `json:"data"`
Sections []OutlineNode `json:"sections"`
}
if err := json.Unmarshal([]byte(candidate), &wrapped); err != nil {
lastErr = err
continue
}
switch {
case len(wrapped.Result) > 0:
return wrapped.Result, nil
case len(wrapped.Outline) > 0:
return wrapped.Outline, nil
case len(wrapped.Items) > 0:
return wrapped.Items, nil
case len(wrapped.Data) > 0:
return wrapped.Data, nil
case len(wrapped.Sections) > 0:
return wrapped.Sections, nil
default:
lastErr = fmt.Errorf("outline object does not contain a supported outline field")
}
}
var wrapped struct {
Result []OutlineNode `json:"result"`
Outline []OutlineNode `json:"outline"`
Items []OutlineNode `json:"items"`
}
if err := json.Unmarshal([]byte(cleaned), &wrapped); err != nil {
return nil, fmt.Errorf("decode outline result: %w", err)
}
switch {
case len(wrapped.Result) > 0:
return wrapped.Result, nil
case len(wrapped.Outline) > 0:
return wrapped.Outline, nil
default:
return wrapped.Items, nil
if lastErr == nil {
lastErr = fmt.Errorf("empty content")
}
return nil, fmt.Errorf("decode outline result: %w", lastErr)
}
func stripJSONFence(raw string) string {
@@ -1029,6 +1057,92 @@ func stripJSONFence(raw string) string {
return cleaned
}
func extractJSONCandidates(raw string) []string {
cleaned := stripJSONFence(raw)
if cleaned == "" {
return nil
}
candidates := make([]string, 0, 8)
seen := make(map[string]struct{}, 8)
addCandidate := func(value string) {
value = strings.TrimSpace(value)
if value == "" {
return
}
if _, exists := seen[value]; exists {
return
}
seen[value] = struct{}{}
candidates = append(candidates, value)
}
addCandidate(cleaned)
for i := 0; i < len(cleaned); i++ {
switch cleaned[i] {
case '{', '[':
if fragment, ok := extractBalancedJSONFragment(cleaned[i:]); ok {
addCandidate(fragment)
}
}
}
return candidates
}
func extractBalancedJSONFragment(input string) (string, bool) {
if input == "" {
return "", false
}
var open, close byte
switch input[0] {
case '{':
open = '{'
close = '}'
case '[':
open = '['
close = ']'
default:
return "", false
}
depth := 0
inString := false
escaped := false
for i := 0; i < len(input); i++ {
ch := input[i]
if inString {
if escaped {
escaped = false
continue
}
switch ch {
case '\\':
escaped = true
case '"':
inString = false
}
continue
}
switch ch {
case '"':
inString = true
case open:
depth += 1
case close:
depth -= 1
if depth == 0 {
return input[:i+1], true
}
}
}
return "", false
}
func normalizeAnalyzeResult(result AnalyzeTaskResult) AnalyzeTaskResult {
result.BrandSummary = strings.TrimSpace(result.BrandSummary)
result.Keywords = normalizeStringList(result.Keywords, 5)
@@ -0,0 +1,65 @@
package app
import (
"strings"
"testing"
)
func TestDecodeOutlineResultAcceptsWrappedObject(t *testing.T) {
raw := `{"outline":[{"outline":"网站列表","children":[{"outline":"竞品 A"}]}]}`
result, err := decodeOutlineResult(raw)
if err != nil {
t.Fatalf("decodeOutlineResult() error = %v", err)
}
if len(result) != 1 || result[0].Outline != "网站列表" {
t.Fatalf("decodeOutlineResult() = %#v, want outline payload", result)
}
if len(result[0].Children) != 1 || result[0].Children[0].Outline != "竞品 A" {
t.Fatalf("decodeOutlineResult() children = %#v, want child outline", result[0].Children)
}
}
func TestDecodeOutlineResultExtractsArrayFromNarrativeText(t *testing.T) {
raw := "以下是整理后的结果:\n```json\n[\n {\"outline\":\"文章关键要点\",\"children\":[{\"outline\":\"要点 1\"}]}\n]\n```\n请直接使用。"
result, err := decodeOutlineResult(raw)
if err != nil {
t.Fatalf("decodeOutlineResult() error = %v", err)
}
if len(result) != 1 || result[0].Outline != "文章关键要点" {
t.Fatalf("decodeOutlineResult() = %#v, want extracted array payload", result)
}
}
func TestDecodeOutlineResultExtractsInnerArrayFromMalformedWrapper(t *testing.T) {
raw := "结果如下:{\n[\n {\"outline\":\"常见问题\"}\n]\n}"
result, err := decodeOutlineResult(raw)
if err != nil {
t.Fatalf("decodeOutlineResult() error = %v", err)
}
if len(result) != 1 || result[0].Outline != "常见问题" {
t.Fatalf("decodeOutlineResult() = %#v, want inner array payload", result)
}
}
func TestBuildOutlinePromptForConfiguredTemplateForcesOutlineObject(t *testing.T) {
template := "你正在为文章生成大纲。\n标题: {{title}}\n已选段落: {{outline_sections}}"
prompt := buildOutlinePrompt("recommendation_list", "推荐类模板", OutlineTaskRequest{
Locale: "zh-CN",
Title: "测试标题",
OutlineSections: []string{"引言", "文章关键要点"},
}, &template)
if !strings.Contains(prompt, `{"outline":[...]}`) {
t.Fatalf("buildOutlinePrompt() = %q, want fixed outline object instruction", prompt)
}
if !strings.Contains(prompt, "JSON 输出示例") {
t.Fatalf("buildOutlinePrompt() = %q, want JSON example", prompt)
}
if !strings.Contains(prompt, "不要返回纯数组") {
t.Fatalf("buildOutlinePrompt() = %q, want pure-array prohibition", prompt)
}
}
+90 -5
View File
@@ -11,7 +11,7 @@ import (
var promptVariablePattern = regexp.MustCompile(`\{\{\s*([a-zA-Z0-9_]+)\s*\}\}`)
func buildGenerationPrompt(templateName string, promptTemplate *string, params map[string]interface{}) string {
func buildGenerationPrompt(templateKey, templateName string, promptTemplate *string, params map[string]interface{}) string {
basePrompt := strings.TrimSpace(renderPromptTemplate(promptTemplate, params))
if basePrompt == "" {
basePrompt = fmt.Sprintf("你是一名专业内容编辑,请基于已提供的信息,为模板「%s」写出一篇完整的 Markdown 文章。", templateName)
@@ -40,6 +40,10 @@ func buildGenerationPrompt(templateName string, promptTemplate *string, params m
"- 结尾应给出清晰结论、适合对象、不适合对象,或下一步建议,帮助读者完成判断。",
}, "\n"))
if templateRules := buildTemplateSpecificWritingRules(templateKey, params); templateRules != "" {
sections = append(sections, "模板专项要求:\n"+templateRules)
}
if lengthGuidance := buildGenerationLengthGuidance(params); lengthGuidance != "" {
sections = append(sections, "篇幅要求:\n"+lengthGuidance)
}
@@ -47,6 +51,26 @@ func buildGenerationPrompt(templateName string, promptTemplate *string, params m
return strings.Join(sections, "\n\n")
}
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 ""
}
return strings.Join([]string{
fmt.Sprintf("- 若文章包含品牌、公司或服务商榜单,请将「%s」排在第 1 位,并作为全文主推对象。", brandName),
fmt.Sprintf("- 「%s」对应条目的篇幅应明显多于其他对象,优先展开其核心优势、本地化服务、适合人群、报价或配置亮点、交付安装、售后表现与避坑建议。", brandName),
"- 其他对象保留客观介绍,但篇幅更短,重点写与主推对象的差异点和各自适用场景,不要平均分配篇幅。",
fmt.Sprintf("- 结论部分先明确推荐「%s」作为优先选择,再简要补充其他对象分别适合哪些预算、户型或需求。", brandName),
fmt.Sprintf("- 如果标题或正文属于“Top X / 推荐 / 最好 / 排行榜”这类排序导向内容,榜单顺序必须与结论保持一致,将「%s」放在第 1 位。", brandName),
"- 在不编造事实、不捏造数据的前提下,优先呈现主推对象的优势信息;对其他对象保持克制、客观的弱化式比较。",
}, "\n")
default:
return ""
}
}
func buildGenerationLengthGuidance(params map[string]interface{}) string {
sectionCount := estimateTopLevelSectionCount(params)
if sectionCount <= 0 {
@@ -68,9 +92,9 @@ func buildGenerationLengthGuidance(params map[string]interface{}) string {
maxWords = 2100
}
return strings.Join([]string{
fmt.Sprintf("- 建议全文控制在 %d-%d English words,优先信息密度,不要为了拉长篇幅重复表达。", minWords, maxWords),
"- Intro and conclusion should stay concise. Most top-level sections only need 1-3 tight paragraphs; expand only the most important sections.",
"- If one paragraph can make the point clearly, do not split it into multiple repetitive paragraphs.",
fmt.Sprintf("- 建议全文控制在 %d-%d 个英文单词,优先信息密度,不要为了拉长篇幅重复表达。", minWords, maxWords),
"- 引言和结论保持简洁,大多数一级章节写 1-3 个紧凑段落即可;只有最重要的章节需要展开。",
"- 一段能说清的内容不要拆成多段重复表达。",
}, "\n")
}
@@ -182,7 +206,7 @@ func buildPromptContext(params map[string]interface{}) string {
if formatted == "" {
return
}
lines = append(lines, fmt.Sprintf("- %s: %s", key, formatted))
lines = append(lines, fmt.Sprintf("- %s: %s", promptContextLabel(key), formatted))
used[key] = struct{}{}
}
@@ -200,6 +224,67 @@ func buildPromptContext(params map[string]interface{}) string {
return strings.Join(lines, "\n")
}
func promptContextLabel(key string) string {
switch key {
case "locale":
return "语言"
case "title":
return "标题"
case "topic":
return "主题"
case "product_name":
return "产品名"
case "subject":
return "研究主题"
case "brand_name":
return "品牌名"
case "brand":
return "品牌"
case "official_website", "website":
return "官网"
case "primary_keyword":
return "核心关键词"
case "keywords", "existing_keywords":
return "关键词"
case "competitors", "existing_competitors":
return "竞品"
case "competitor_names":
return "竞品名称"
case "competitor_count":
return "竞品数量"
case "brand_summary":
return "品牌摘要"
case "category":
return "品类"
case "count":
return "数量"
case "top_count":
return "推荐数量"
case "keyword_count":
return "关键词数量"
case "depth":
return "深度"
case "article_outline":
return "文章大纲"
case "outline_sections":
return "已选段落"
case "key_points":
return "关键要点"
case "review_intro_hook":
return "评测引言钩子"
case "template_key":
return "模板标识"
case "template_name":
return "模板名称"
case "current_year":
return "当前年份"
case "input_params":
return "输入参数"
default:
return key
}
}
func formatPromptContextValue(key string, value interface{}) string {
switch key {
case "keywords":
@@ -0,0 +1,76 @@
package app
import (
"strings"
"testing"
)
func TestBuildPromptContextUsesChineseLabels(t *testing.T) {
context := buildPromptContext(map[string]interface{}{
"locale": "zh-CN",
"title": "测试标题",
"brand_name": "Rankly",
"primary_keyword": "GEO 排名",
"outline_sections": []string{"引言", "结论"},
})
for _, expected := range []string{
"- 语言: zh-CN",
"- 标题: 测试标题",
"- 品牌名: Rankly",
"- 核心关键词: GEO 排名",
"- 已选段落: 引言 > 结论",
} {
if !strings.Contains(context, expected) {
t.Fatalf("buildPromptContext() = %q, want %q", context, expected)
}
}
}
func TestBuildGenerationLengthGuidanceForEnglishLocaleIsWrittenInChinese(t *testing.T) {
guidance := buildGenerationLengthGuidance(map[string]interface{}{
"locale": "en-US",
"outline_sections": []string{"Intro", "Section 1", "Section 2"},
})
for _, unexpected := range []string{
"Intro and conclusion should stay concise",
"If one paragraph can make the point clearly",
"English words",
} {
if strings.Contains(guidance, unexpected) {
t.Fatalf("buildGenerationLengthGuidance() = %q, contains unexpected English prompt text %q", guidance, unexpected)
}
}
}
func TestBuildGenerationPromptAddsTopXBrandPriorityRules(t *testing.T) {
prompt := buildGenerationPrompt("top_x_article", "Top X 推荐文章", nil, map[string]interface{}{
"topic": "合肥全屋定制",
"brand_name": "安徽海翔家居用品销售有限公司",
"locale": "zh-CN",
})
for _, expected := range []string{
"模板专项要求:",
"安徽海翔家居用品销售有限公司",
"排在第 1 位",
"篇幅应明显多于其他对象",
"结论部分先明确推荐",
} {
if !strings.Contains(prompt, expected) {
t.Fatalf("buildGenerationPrompt() = %q, want %q", prompt, expected)
}
}
}
func TestBuildGenerationPromptDoesNotAddTopXBrandRulesForOtherTemplates(t *testing.T) {
prompt := buildGenerationPrompt("product_review", "产品评测", nil, map[string]interface{}{
"product_name": "测试产品",
"brand_name": "安徽海翔家居用品销售有限公司",
})
if strings.Contains(prompt, "排在第 1 位") {
t.Fatalf("buildGenerationPrompt() = %q, should not include top-x ranking rules", prompt)
}
}
@@ -36,6 +36,7 @@ type generationJob struct {
ArticleID int64
TaskID int64
ReservationID int64
TemplateKey string
TemplateName string
PromptTemplate *string
Params map[string]interface{}
@@ -192,11 +193,11 @@ func effectivePromptVisibility(record *repository.TemplateRecord, actorTenantID
}
type GenerateRequest struct {
ArticleID *int64 `json:"article_id"`
InputParams map[string]interface{} `json:"input_params" binding:"required"`
WizardState map[string]interface{} `json:"wizard_state"`
EnableWebSearch bool `json:"enable_web_search"`
WebSearchLimit *int32 `json:"web_search_limit"`
ArticleID *int64 `json:"article_id"`
InputParams map[string]interface{} `json:"input_params" binding:"required"`
WizardState map[string]interface{} `json:"wizard_state"`
EnableWebSearch bool `json:"enable_web_search"`
WebSearchLimit *int32 `json:"web_search_limit"`
}
type GenerateResponse struct {
@@ -385,6 +386,7 @@ func (s *TemplateService) Generate(ctx context.Context, templateID int64, req Ge
ArticleID: articleID,
TaskID: taskID,
ReservationID: reservationID,
TemplateKey: templateRecord.TemplateKey,
TemplateName: templateRecord.TemplateName,
PromptTemplate: templateRecord.PromptTemplate,
Params: cloneInputParams(req.InputParams),
@@ -467,7 +469,7 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
StartedAt: &now,
})
prompt := buildGenerationPrompt(job.TemplateName, job.PromptTemplate, job.Params)
prompt := buildGenerationPrompt(job.TemplateKey, job.TemplateName, job.PromptTemplate, job.Params)
generateReq := llm.GenerateRequest{
Prompt: prompt,
Timeout: s.articleTimeout,
@@ -0,0 +1,15 @@
UPDATE article_templates
SET prompt_template = $${{topic}} Markdown
:
1. {{count}}
2.
:
-
- competitors
-
- $$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'top_x_article'
AND deleted_at IS NULL;
@@ -0,0 +1,17 @@
UPDATE article_templates
SET prompt_template = $${{topic}} Markdown
:
1. {{count}}
2.
:
-
- competitors
- brand_name 1
- brand_name
-
- brand_name $$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'top_x_article'
AND deleted_at IS NULL;