mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-08-28 22:11:38 +00:00
Adaptive thinking
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MCPServer struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
AuthorizationToken string `json:"authorization_token,omitempty"`
|
||||
AllowedTools []string `json:"allowed_tools,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
ThinkingModeAdaptive = "adaptive"
|
||||
ThinkingModeDisabled = "disabled"
|
||||
ThinkingDisplaySummarized = "summarized"
|
||||
ThinkingDisplayOmitted = "omitted"
|
||||
)
|
||||
|
||||
type BotConfig struct {
|
||||
ID string `json:"id"`
|
||||
TelegramToken string `json:"telegram_token"`
|
||||
MemorySize int `json:"memory_size"`
|
||||
MessagePerHour int `json:"messages_per_hour"`
|
||||
MessagePerDay int `json:"messages_per_day"`
|
||||
TempBanDuration string `json:"temp_ban_duration"`
|
||||
Model string `json:"model"`
|
||||
Temperature *float32 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Thinking string `json:"thinking,omitempty"`
|
||||
ThinkingDisplay string `json:"thinking_display,omitempty"`
|
||||
SystemPrompts map[string]string `json:"system_prompts"`
|
||||
Active bool `json:"active"`
|
||||
OwnerTelegramID int64 `json:"owner_telegram_id"`
|
||||
AnthropicAPIKey string `json:"anthropic_api_key"`
|
||||
ElevenLabsAPIKey string `json:"elevenlabs_api_key"`
|
||||
ElevenLabsVoiceID string `json:"elevenlabs_voice_id"`
|
||||
ElevenLabsModel string `json:"elevenlabs_model"`
|
||||
DebugScreening bool `json:"debug_screening"`
|
||||
MCPServers []MCPServer `json:"mcp_servers,omitempty"`
|
||||
ConfigFilePath string `json:"-"`
|
||||
}
|
||||
|
||||
func validateConfigPath(configDir, filename string) (string, error) {
|
||||
configDir = filepath.Clean(configDir)
|
||||
filename = filepath.Clean(filename)
|
||||
|
||||
absConfigDir, err := filepath.Abs(configDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path for config directory: %w", err)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(absConfigDir, filename)
|
||||
absPath, err := filepath.Abs(fullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path for config file: %w", err)
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(absConfigDir, absPath)
|
||||
if err != nil || strings.HasPrefix(rel, "..") || strings.Contains(rel, "..") {
|
||||
return "", fmt.Errorf("invalid config path: file must be within the config directory")
|
||||
}
|
||||
|
||||
if filepath.Ext(absPath) != ".json" {
|
||||
return "", fmt.Errorf("invalid file extension: must be .json")
|
||||
}
|
||||
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
func loadAllConfigs(dir string) ([]BotConfig, error) {
|
||||
var configs []BotConfig
|
||||
ids := make(map[string]bool)
|
||||
tokens := make(map[string]bool)
|
||||
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config directory: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) == ".json" {
|
||||
validPath, err := validateConfigPath(dir, file.Name())
|
||||
if err != nil {
|
||||
InfoLogger.Printf("Invalid config path for %s: %v", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
config, err := loadConfig(validPath)
|
||||
if err != nil {
|
||||
InfoLogger.Printf("Failed to load config %s: %v", validPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !config.Active {
|
||||
InfoLogger.Printf("Skipping inactive bot: %s", config.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateConfig(&config, ids, tokens); err != nil {
|
||||
InfoLogger.Printf("Config validation failed for %s: %v", validPath, err)
|
||||
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)
|
||||
}
|
||||
|
||||
config.ConfigFilePath = validPath
|
||||
configs = append(configs, config)
|
||||
}
|
||||
}
|
||||
|
||||
if len(configs) == 0 {
|
||||
return nil, fmt.Errorf("no valid configs found")
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func validateConfig(config *BotConfig, ids, tokens map[string]bool) error {
|
||||
if config.ID == "" {
|
||||
return fmt.Errorf("missing 'id' field")
|
||||
}
|
||||
if _, exists := ids[config.ID]; exists {
|
||||
return fmt.Errorf("duplicate bot id '%s'", config.ID)
|
||||
}
|
||||
ids[config.ID] = true
|
||||
|
||||
if config.TelegramToken == "" {
|
||||
return fmt.Errorf("missing 'telegram_token' field")
|
||||
}
|
||||
if _, exists := tokens[config.TelegramToken]; exists {
|
||||
return fmt.Errorf("duplicate telegram_token")
|
||||
}
|
||||
tokens[config.TelegramToken] = true
|
||||
|
||||
if config.Model == "" {
|
||||
return fmt.Errorf("missing 'model' field")
|
||||
}
|
||||
|
||||
switch config.Thinking {
|
||||
case "", ThinkingModeAdaptive, ThinkingModeDisabled:
|
||||
default:
|
||||
return fmt.Errorf("invalid 'thinking' value %q: must be %q or %q (or omitted)",
|
||||
config.Thinking, ThinkingModeAdaptive, ThinkingModeDisabled)
|
||||
}
|
||||
|
||||
switch config.ThinkingDisplay {
|
||||
case "":
|
||||
case ThinkingDisplaySummarized, ThinkingDisplayOmitted:
|
||||
if config.Thinking != ThinkingModeAdaptive {
|
||||
return fmt.Errorf("'thinking_display' requires 'thinking': %q (the API rejects display with thinking disabled)",
|
||||
ThinkingModeAdaptive)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid 'thinking_display' value %q: must be %q or %q (or omitted)",
|
||||
config.ThinkingDisplay, ThinkingDisplaySummarized, ThinkingDisplayOmitted)
|
||||
}
|
||||
|
||||
if config.MaxTokens < 0 {
|
||||
return fmt.Errorf("'max_tokens' must be greater than 0 when set")
|
||||
}
|
||||
|
||||
if config.MessagePerHour <= 0 {
|
||||
return fmt.Errorf("'messages_per_hour' must be greater than 0")
|
||||
}
|
||||
|
||||
if config.MessagePerDay <= 0 {
|
||||
return fmt.Errorf("'messages_per_day' must be greater than 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadConfig(filename string) (BotConfig, error) {
|
||||
var config BotConfig
|
||||
file, err := os.OpenFile(filepath.Clean(filename), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("failed to open config file %s: %w", filename, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
InfoLogger.Printf("Failed to close config file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&config); err != nil {
|
||||
return config, fmt.Errorf("failed to decode JSON from %s: %w", filename, err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *BotConfig) Reload(configDir, filename string) error {
|
||||
validPath, err := validateConfigPath(configDir, filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config path: %w", err)
|
||||
}
|
||||
|
||||
cleanPath := filepath.Clean(validPath)
|
||||
file, err := os.OpenFile(cleanPath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open config file %s: %w", cleanPath, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
InfoLogger.Printf("Failed to close config file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(c); err != nil {
|
||||
return fmt.Errorf("failed to decode JSON from %s: %w", validPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BotConfig) PersistModel(newModel string) error {
|
||||
if c.ConfigFilePath == "" {
|
||||
return fmt.Errorf("config file path not set; cannot persist model")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(c.ConfigFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config for update: %w", err)
|
||||
}
|
||||
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return fmt.Errorf("failed to parse config for update: %w", err)
|
||||
}
|
||||
|
||||
raw["model"] = newModel
|
||||
|
||||
updated, err := json.MarshalIndent(raw, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to re-encode config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(c.ConfigFilePath, updated, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
c.Model = newModel
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user