Files
geo/server/internal/tenant/prompts/loader.go
T
root dca36ed1f6 chore(prompts): drop unused target_platform supplement
The target-platform supplement only fed legacy prompt builders that read
the now-removed wizard_state platforms field. Drop the YAML entry,
loader struct field, and runtime helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:02:33 +08:00

246 lines
8.9 KiB
Go

package prompts
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"gopkg.in/yaml.v3"
)
const promptsConfigEnv = "PROMPTS_CONFIG_FILE"
type promptsConfig struct {
Runtime runtimePromptsConfig `yaml:"runtime"`
PlatformTemplates map[string]platformTemplatePromptEntry `yaml:"platform_templates"`
}
type runtimePromptsConfig struct {
DefaultGenerationBasePromptTemplate string `yaml:"default_generation_base_prompt_template"`
PromptContextHeading string `yaml:"prompt_context_heading"`
GenerationWritingRequirementsSection string `yaml:"generation_writing_requirements_section"`
GenerationTemplateSpecificRulesHeading string `yaml:"generation_template_specific_rules_heading"`
GenerationLengthGuidanceHeading string `yaml:"generation_length_guidance_heading"`
TopXBrandPriorityRulesTemplate string `yaml:"top_x_brand_priority_rules_template"`
EnglishLengthGuidanceTemplate string `yaml:"english_length_guidance_template"`
ChineseLengthGuidanceTemplate string `yaml:"chinese_length_guidance_template"`
ContextLabels map[string]string `yaml:"context_labels"`
TemplateContextWithJSONExampleTemplate string `yaml:"template_context_with_json_example_template"`
JSONOutputExampleHeading string `yaml:"json_output_example_heading"`
AnalyzeOutputExample string `yaml:"analyze_output_example"`
AnalyzeFallbackPromptTemplate string `yaml:"analyze_fallback_prompt_template"`
TitleCustomOutputRequirementsSection string `yaml:"title_custom_output_requirements_section"`
TitleOutputExample string `yaml:"title_output_example"`
TitleFallbackPromptTemplate string `yaml:"title_fallback_prompt_template"`
OutlineOutputExample string `yaml:"outline_output_example"`
OutlineCustomOutputRequirementsSection string `yaml:"outline_custom_output_requirements_section"`
OutlineFallbackPromptTemplate string `yaml:"outline_fallback_prompt_template"`
OutlineResponseFormatDescription string `yaml:"outline_response_format_description"`
PromptRuleTaskNameSupplementFormat string `yaml:"prompt_rule_task_name_supplement_format"`
PromptRuleSceneSupplementFormat string `yaml:"prompt_rule_scene_supplement_format"`
PromptRuleToneSupplementFormat string `yaml:"prompt_rule_tone_supplement_format"`
PromptRuleWordCountSupplementFormat string `yaml:"prompt_rule_word_count_supplement_format"`
PromptRuleSupplementHeading string `yaml:"prompt_rule_supplement_heading"`
PromptRuleOutputRequirementsSection string `yaml:"prompt_rule_output_requirements_section"`
ArticleImitationPromptTemplate string `yaml:"article_imitation_prompt_template"`
ArticleImitationLocaleInstructions map[string]string `yaml:"article_imitation_locale_instructions"`
ArticleImitationSettingLabels map[string]string `yaml:"article_imitation_setting_labels"`
KnowledgePromptIntroLines []string `yaml:"knowledge_prompt_intro_lines"`
KnowledgeSnippetLabelFormat string `yaml:"knowledge_snippet_label_format"`
KnowledgeWebsiteMarkdown knowledgeWebsiteMarkdownConfig `yaml:"knowledge_website_markdown"`
}
type knowledgeWebsiteMarkdownConfig struct {
AssistantRole string `yaml:"assistant_role"`
Task string `yaml:"task"`
Requirements []string `yaml:"requirements"`
TitlePrefix string `yaml:"title_prefix"`
URLPrefix string `yaml:"url_prefix"`
ScopeSingle string `yaml:"scope_single"`
ScopeMultiFormat string `yaml:"scope_multi_format"`
}
type platformTemplatePromptEntry struct {
PromptTemplate string `yaml:"prompt_template"`
AnalyzePromptTemplate string `yaml:"analyze_prompt_template"`
TitlePromptTemplate string `yaml:"title_prompt_template"`
OutlinePromptTemplate string `yaml:"outline_prompt_template"`
}
type promptsCache struct {
path string
modTime time.Time
size int64
cfg *promptsConfig
}
var (
promptsMu sync.RWMutex
configuredFilePath string
cachedPrompts promptsCache
)
func SetConfigFile(path string) {
promptsMu.Lock()
defer promptsMu.Unlock()
configuredFilePath = strings.TrimSpace(path)
cachedPrompts = promptsCache{}
}
func loadPromptsConfig() *promptsConfig {
path := resolvePromptsConfigFile()
info, statErr := os.Stat(path)
promptsMu.RLock()
cached := cachedPrompts
promptsMu.RUnlock()
if statErr == nil && cached.cfg != nil && cached.path == path && cached.modTime.Equal(info.ModTime()) && cached.size == info.Size() {
return cached.cfg
}
if statErr != nil && cached.cfg != nil && cached.path == path {
return cached.cfg
}
promptsMu.Lock()
defer promptsMu.Unlock()
if info, statErr = os.Stat(path); statErr == nil {
if cachedPrompts.cfg != nil && cachedPrompts.path == path && cachedPrompts.modTime.Equal(info.ModTime()) && cachedPrompts.size == info.Size() {
return cachedPrompts.cfg
}
} else if cachedPrompts.cfg != nil && cachedPrompts.path == path {
return cachedPrompts.cfg
}
if statErr != nil {
panic(fmt.Sprintf("prompts: stat config %q failed: %v", path, statErr))
}
data, err := os.ReadFile(path)
if err != nil {
if cachedPrompts.cfg != nil && cachedPrompts.path == path {
return cachedPrompts.cfg
}
panic(fmt.Sprintf("prompts: read config %q failed: %v", path, err))
}
var cfg promptsConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
if cachedPrompts.cfg != nil && cachedPrompts.path == path {
return cachedPrompts.cfg
}
panic(fmt.Sprintf("prompts: parse config %q failed: %v", path, err))
}
cachedPrompts = promptsCache{
path: path,
modTime: info.ModTime(),
size: info.Size(),
cfg: &cfg,
}
return cachedPrompts.cfg
}
func loadRuntimePrompts() runtimePromptsConfig {
return loadPromptsConfig().Runtime
}
func resolvePromptsConfigFile() string {
promptsMu.RLock()
configured := configuredFilePath
promptsMu.RUnlock()
if configured != "" {
return configured
}
if value := strings.TrimSpace(os.Getenv(promptsConfigEnv)); value != "" {
return value
}
if value := strings.TrimSpace(os.Getenv("CONFIG_PATH")); value != "" {
return filepath.Join(filepath.Dir(value), "prompts.yml")
}
if _, file, _, ok := runtime.Caller(0); ok {
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "..", "configs", "prompts.yml"))
}
return filepath.Join("configs", "prompts.yml")
}
func platformTemplatePromptEntryFor(templateKey string) (platformTemplatePromptEntry, bool) {
configs := loadPromptsConfig().PlatformTemplates
if len(configs) == 0 {
return platformTemplatePromptEntry{}, false
}
entry, ok := configs[strings.TrimSpace(templateKey)]
return entry, ok
}
func ApplyPlatformTemplatePromptOverrides(templateKey string, promptTemplate *string, cardConfigJSON []byte) (*string, []byte) {
entry, ok := platformTemplatePromptEntryFor(templateKey)
if !ok {
return clonePromptTemplate(promptTemplate), cloneBytes(cardConfigJSON)
}
nextPrompt := clonePromptTemplate(promptTemplate)
if value := strings.TrimSpace(entry.PromptTemplate); value != "" {
nextPrompt = &value
}
nextCardConfig := cloneBytes(cardConfigJSON)
if len(nextCardConfig) == 0 {
nextCardConfig = []byte("{}")
}
var payload map[string]interface{}
if err := json.Unmarshal(nextCardConfig, &payload); err != nil {
return nextPrompt, nextCardConfig
}
wizard, _ := payload["wizard"].(map[string]interface{})
if wizard == nil {
wizard = make(map[string]interface{})
payload["wizard"] = wizard
}
if value := strings.TrimSpace(entry.AnalyzePromptTemplate); value != "" {
wizard["analyze_prompt_template"] = value
}
if value := strings.TrimSpace(entry.TitlePromptTemplate); value != "" {
wizard["title_prompt_template"] = value
}
if value := strings.TrimSpace(entry.OutlinePromptTemplate); value != "" {
wizard["outline_prompt_template"] = value
}
marshaled, err := json.Marshal(payload)
if err != nil {
return nextPrompt, nextCardConfig
}
return nextPrompt, marshaled
}
func clonePromptTemplate(value *string) *string {
if value == nil {
return nil
}
cloned := *value
return &cloned
}
func cloneBytes(value []byte) []byte {
if len(value) == 0 {
return nil
}
cloned := make([]byte, len(value))
copy(cloned, value)
return cloned
}