feat(prompts): add prompts loader and configuration management
- Implemented a new prompts loader in `loader.go` to manage prompt templates and configurations. - Introduced caching mechanism for prompt configurations to optimize loading. - Added functions to apply platform-specific prompt template overrides. - Created unit tests in `loader_test.go` to validate prompt configuration loading and reloading behavior. - Ensured that the last valid configuration is retained in case of errors during reload.
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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"`
|
||||
PromptRuleTargetPlatformSupplementFormat string `yaml:"prompt_rule_target_platform_supplement_format"`
|
||||
PromptRuleSupplementHeading string `yaml:"prompt_rule_supplement_heading"`
|
||||
PromptRuleOutputRequirementsSection string `yaml:"prompt_rule_output_requirements_section"`
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user