Debounce and caching

This commit is contained in:
HugeFrog24
2026-07-24 23:41:28 +02:00
parent 0543283b8a
commit c36e1846f5
16 changed files with 1332 additions and 81 deletions
+91 -4
View File
@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
)
type MCPServer struct {
@@ -15,6 +16,15 @@ type MCPServer struct {
AllowedTools []string `json:"allowed_tools,omitempty"`
}
type WebSearchConfig struct {
AllowedDomains []string `json:"allowed_domains,omitempty"`
BlockedDomains []string `json:"blocked_domains,omitempty"`
FetchAllowedDomains []string `json:"fetch_allowed_domains,omitempty"`
MaxUses int `json:"max_uses,omitempty"`
Fetch bool `json:"fetch,omitempty"`
MaxContentTokens int `json:"max_content_tokens,omitempty"`
}
const (
ThinkingModeAdaptive = "adaptive"
ThinkingModeDisabled = "disabled"
@@ -22,6 +32,28 @@ const (
ThinkingDisplayOmitted = "omitted"
)
// maxDebounceMs bounds debounce_ms. Beyond this the bot reads as unresponsive
// rather than deliberate, and the coalesced turn drifts far enough from the
// user's last message that the reply feels stale.
const maxDebounceMs = 30000
// DebounceWindow is the quiet period an intake buffer waits before dispatching a
// coalesced turn. Zero disables debouncing entirely (the default), matching the
// opt-in behavior of comparable gateways.
func (c *BotConfig) DebounceWindow() time.Duration {
if c.DebounceMs <= 0 {
return 0
}
return time.Duration(c.DebounceMs) * time.Millisecond
}
// CacheHistoryEnabled reports whether a cache_control breakpoint should be placed
// on the trailing conversation block in addition to the system prompt. Defaults
// to true; set "cache_history": false to opt out.
func (c *BotConfig) CacheHistoryEnabled() bool {
return c.CacheHistory == nil || *c.CacheHistory
}
type BotConfig struct {
ID string `json:"id"`
TelegramToken string `json:"telegram_token"`
@@ -34,6 +66,8 @@ type BotConfig struct {
MaxTokens int `json:"max_tokens,omitempty"`
Thinking string `json:"thinking,omitempty"`
ThinkingDisplay string `json:"thinking_display,omitempty"`
DebounceMs int `json:"debounce_ms,omitempty"`
CacheHistory *bool `json:"cache_history,omitempty"`
SystemPrompts map[string]string `json:"system_prompts"`
Active bool `json:"active"`
OwnerTelegramID int64 `json:"owner_telegram_id"`
@@ -43,6 +77,7 @@ type BotConfig struct {
ElevenLabsModel string `json:"elevenlabs_model"`
DebugScreening bool `json:"debug_screening"`
MCPServers []MCPServer `json:"mcp_servers,omitempty"`
WebSearch *WebSearchConfig `json:"web_search,omitempty"`
ConfigFilePath string `json:"-"`
}
@@ -107,10 +142,7 @@ func loadAllConfigs(dir string) ([]BotConfig, error) {
continue
}
if config.Thinking == ThinkingModeAdaptive && config.MaxTokens > 0 && config.MaxTokens < 4000 {
InfoLogger.Printf("[%s] thinking=adaptive with max_tokens=%d: thinking tokens count toward max_tokens; consider >= 4000",
config.ID, config.MaxTokens)
}
logConfigAdvisories(&config)
config.ConfigFilePath = validPath
configs = append(configs, config)
@@ -124,6 +156,41 @@ func loadAllConfigs(dir string) ([]BotConfig, error) {
return configs, nil
}
// logConfigAdvisories emits non-fatal boot-time notes about settings that are
// valid but likely to surprise: silently ineffective, or costlier than intended.
func logConfigAdvisories(config *BotConfig) {
if config.Thinking == ThinkingModeAdaptive && config.MaxTokens > 0 && config.MaxTokens < 4000 {
InfoLogger.Printf("[%s] thinking=adaptive with max_tokens=%d: thinking tokens count toward max_tokens; consider >= 4000",
config.ID, config.MaxTokens)
}
if config.DebounceMs > 0 {
InfoLogger.Printf("[%s] intake debounce enabled: coalescing rapid text messages over a %dms quiet window",
config.ID, config.DebounceMs)
} else {
InfoLogger.Printf("[%s] intake debounce disabled: every message dispatches its own turn (set debounce_ms to coalesce rapid follow-ups)",
config.ID)
}
if ws := config.WebSearch; ws != nil && len(ws.AllowedDomains) == 0 && len(ws.BlockedDomains) == 0 {
InfoLogger.Printf("[%s] web_search enabled with no allowed_domains/blocked_domains: the model may search the open web",
config.ID)
}
if ws := config.WebSearch; ws != nil && len(ws.FetchAllowedDomains) > 0 {
if !ws.Fetch {
InfoLogger.Printf("[%s] web_search.fetch_allowed_domains is set but fetch is disabled: it has no effect",
config.ID)
}
for _, d := range ws.FetchAllowedDomains {
if strings.Contains(d, "/") {
InfoLogger.Printf("[%s] web_search.fetch_allowed_domains entry %q includes a path: web_fetch matches host-only, so the whole host is fetchable",
config.ID, d)
}
}
}
}
func validateConfig(config *BotConfig, ids, tokens map[string]bool) error {
if config.ID == "" {
return fmt.Errorf("missing 'id' field")
@@ -168,6 +235,26 @@ func validateConfig(config *BotConfig, ids, tokens map[string]bool) error {
return fmt.Errorf("'max_tokens' must be greater than 0 when set")
}
if config.DebounceMs < 0 {
return fmt.Errorf("'debounce_ms' must be greater than 0 when set")
}
if config.DebounceMs > maxDebounceMs {
return fmt.Errorf("'debounce_ms' of %d exceeds the maximum of %d (Telegram drops long-idle updates and users read silence as failure)",
config.DebounceMs, maxDebounceMs)
}
if ws := config.WebSearch; ws != nil {
if len(ws.AllowedDomains) > 0 && len(ws.BlockedDomains) > 0 {
return fmt.Errorf("'web_search' cannot set both allowed_domains and blocked_domains (the API rejects that)")
}
if ws.MaxUses < 0 {
return fmt.Errorf("'web_search.max_uses' must be greater than 0 when set")
}
if ws.MaxContentTokens < 0 {
return fmt.Errorf("'web_search.max_content_tokens' must be greater than 0 when set")
}
}
if config.MessagePerHour <= 0 {
return fmt.Errorf("'messages_per_hour' must be greater than 0")
}