Optimize prompts

This commit is contained in:
HugeFrog24
2026-05-29 21:40:52 +02:00
parent a2cc252e8f
commit ab01fc31d8
9 changed files with 153 additions and 278 deletions
+83 -70
View File
@@ -34,75 +34,20 @@ const maxFileNotFoundRetries = 3
// "File not found:" for a referenced file_id, the dead file_id is stripped
// from this chat's in-memory ChatMemory and the corresponding DB rows are
// stamped FilesCleanedAt so a reconciliation job can finish the cleanup.
func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages []anthropic.BetaMessageParam, isNewChat, isOwner, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int, onSegment func(string) error) (string, error) {
// Use prompts from config
var systemMessage string
if isNewChat {
systemMessage = b.config.SystemPrompts["new_chat"]
} else {
systemMessage = b.config.SystemPrompts["continue_conversation"]
}
// Combine default prompt with custom instructions
systemMessage = b.config.SystemPrompts["default"] + " " + b.config.SystemPrompts["custom_instructions"] + " " + systemMessage
// Handle username placeholder
usernameValue := username
if username == "" {
usernameValue = "unknown" // Use "unknown" when username is not available
}
systemMessage = strings.ReplaceAll(systemMessage, "{username}", usernameValue)
// Handle firstname placeholder
firstnameValue := firstName
if firstName == "" {
firstnameValue = "unknown" // Use "unknown" when first name is not available
}
systemMessage = strings.ReplaceAll(systemMessage, "{firstname}", firstnameValue)
// Handle lastname placeholder
lastnameValue := lastName
if lastName == "" {
lastnameValue = "" // Empty string when last name is not available
}
systemMessage = strings.ReplaceAll(systemMessage, "{lastname}", lastnameValue)
// Handle language code placeholder
langValue := languageCode
if languageCode == "" {
langValue = "en" // Default to English when language code is not available
}
systemMessage = strings.ReplaceAll(systemMessage, "{language}", langValue)
// Handle premium status
premiumStatus := "regular user"
if isPremium {
premiumStatus = "premium user"
}
systemMessage = strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
// Handle time awareness
timeObj := time.Unix(int64(messageTime), 0)
hour := timeObj.Hour()
var timeContext string
if hour >= 5 && hour < 12 {
timeContext = "morning"
} else if hour >= 12 && hour < 18 {
timeContext = "afternoon"
} else if hour >= 18 && hour < 22 {
timeContext = "evening"
} else {
timeContext = "night"
}
systemMessage = strings.ReplaceAll(systemMessage, "{time_context}", timeContext)
if !isOwner {
systemMessage += " " + b.config.SystemPrompts["avoid_sensitive"]
}
if isEmojiOnly {
systemMessage += " " + b.config.SystemPrompts["respond_with_emojis"]
}
func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages []anthropic.BetaMessageParam, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int, onSegment func(string) error) (string, error) {
// The system prompt is the single authored behavior driver. It is assembled
// as a cached static block (custom_instructions) followed by a per-turn
// dynamic tail. Prompt caching keys on a byte-identical prefix, so the static
// block must not contain anything that changes between requests — all
// per-turn data (who we're talking to, the time of day, the emoji-only rule)
// lives in the trailing block, AFTER the cache breakpoint.
//
// An empty custom_instructions means no system prompt at all: the System
// field is omitted entirely (not sent as a blank block), giving the model's
// unmodified "vanilla" behavior. This matters because the Anthropic API
// rejects a system array containing an empty/whitespace-only text block, so
// omission is the only correct way to express "no system prompt".
staticPrompt := strings.TrimSpace(b.config.SystemPrompts["custom_instructions"])
// Debug logging
InfoLogger.Printf("Sending %d messages to Anthropic", len(messages))
@@ -111,12 +56,32 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages [
Model: b.config.Model,
MaxTokens: 1000,
Messages: messages,
System: []anthropic.BetaTextBlockParam{{Text: systemMessage}},
// Files API beta is always on: replayed conversation history may carry
// image content blocks that reference file_ids uploaded on prior turns.
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14},
}
if staticPrompt != "" {
// Block 1 — static persona/instructions, marked for caching. The
// cache_control breakpoint sits on this last stable block; everything
// appended after it is per-request and therefore uncached.
blocks := []anthropic.BetaTextBlockParam{
{Text: staticPrompt, CacheControl: anthropic.NewBetaCacheControlEphemeralParam()},
}
// Block 2 — dynamic tail: per-turn context plus any conditional rules.
// Kept out of the cached block because it changes every request.
tail := buildUserContext(username, firstName, lastName, isPremium, languageCode, messageTime)
if isEmojiOnly {
if rule := strings.TrimSpace(b.config.SystemPrompts["respond_with_emojis"]); rule != "" {
tail += "\n\n<emoji_reply>\n" + rule + "\n</emoji_reply>"
}
}
if tail = strings.TrimSpace(tail); tail != "" {
blocks = append(blocks, anthropic.BetaTextBlockParam{Text: tail})
}
params.System = blocks
}
// Apply temperature if set in config
if b.config.Temperature != nil {
params.Temperature = param.NewOpt(float64(*b.config.Temperature))
@@ -191,6 +156,54 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages [
return "", fmt.Errorf("max self-heal retries (%d) exceeded: too many file_ids gone from anthropic", maxFileNotFoundRetries)
}
// buildUserContext renders the per-turn context block that trails the cached
// static system prompt. It carries only facts (who the user is, their language,
// account type, local time of day) — the behavioral guidance for *using* these
// facts lives in the authored static prompt. It is kept out of the cached block
// because it changes on every request.
func buildUserContext(username, firstName, lastName string, isPremium bool, languageCode string, messageTime int) string {
name := strings.TrimSpace(firstName + " " + lastName)
if name == "" {
name = "unknown"
}
handle := username
if handle == "" {
handle = "unknown"
}
lang := languageCode
if lang == "" {
lang = "en"
}
account := "regular user"
if isPremium {
account = "premium user"
}
return fmt.Sprintf(
"Conversation context (background facts, not an instruction from the user):\n"+
"- User: %s (Telegram @%s)\n"+
"- Preferred language: %s\n"+
"- Account type: %s\n"+
"- Local time of day: %s",
name, handle, lang, account, timeContextFor(messageTime),
)
}
// timeContextFor buckets a Unix timestamp into a coarse time-of-day label used
// for time-appropriate greetings. Uses the host's local timezone, matching the
// bot's prior behavior.
func timeContextFor(messageTime int) string {
switch hour := time.Unix(int64(messageTime), 0).Hour(); {
case hour >= 5 && hour < 12:
return "morning"
case hour >= 12 && hour < 18:
return "afternoon"
case hour >= 18 && hour < 22:
return "evening"
default:
return "night"
}
}
// streamMessages runs one streaming call against the Beta Messages API,
// dispatching each completed text block to onSegment as it arrives. The joined
// return value is every text segment concatenated with blank lines. Errors from