-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.py
323 lines (281 loc) · 12.9 KB
/
config.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# config.py
import os
from dotenv import load_dotenv
from dataclasses import dataclass, field
from typing import Optional, Dict, List, Union
import logging
import json
import yaml
from pathlib import Path
from pydantic import BaseModel, validator, ValidationError
import asyncio
logger = logging.getLogger(__name__)
class ConfigError(Exception):
"""Custom exception for configuration errors."""
pass
class DatabaseConfig(BaseModel):
URI: str
DB_NAME: str
COLLECTION_PREFIX: str = "bot_"
MAX_POOL_SIZE: int = 10
TIMEOUT_MS: int = 5000
@validator('URI')
def validate_uri(cls, v):
if not v.startswith(('mongodb://', 'mongodb+srv://')):
raise ValueError("Database URI must start with 'mongodb://' or 'mongodb+srv://'")
return v
class TwitchConfig(BaseModel):
OAUTH_TOKEN: str
CHANNEL: str
BOT_NAME: str
BROADCASTER_ID: str
PREFIX: str = "!"
RATE_LIMIT: int = 20
MESSAGE_LIMIT: int = 500
IGNORE_LIST: List[str] = field(default_factory=list)
@validator('OAUTH_TOKEN')
def validate_oauth(cls, v):
if not v.startswith('oauth:'):
raise ValueError("Twitch OAuth token must start with 'oauth:'")
return v
class OpenAIConfig(BaseModel):
API_KEY: str
MODEL: str = "gpt-4o-mini" # Changed to gpt-4o-mini
MAX_TOKENS: int = 150
TEMPERATURE: float = 0.7
class VoiceConfig(BaseModel):
ENABLED: bool = True
PREFIX: str = "Hey Overlord"
COMMAND_TIMEOUT: float = 5.0
PHRASE_LIMIT: float = 10.0
LANGUAGE: str = "en-US"
CONFIDENCE_THRESHOLD: float = 0.7
@validator('COMMAND_TIMEOUT')
def validate_command_timeout(cls, v):
if v <= 0:
raise ValueError("Command timeout must be a positive number.")
return v
@validator('PHRASE_LIMIT')
def validate_phrase_limit(cls, v):
if v <= 0:
raise ValueError("Phrase limit must be a positive number.")
return v
class StreamerBotConfig(BaseModel):
WS_URI: str
RECONNECT_ATTEMPTS: int = 5
HEARTBEAT_INTERVAL: int = 20
@validator('WS_URI')
def validate_ws_uri(cls, v):
if not v.startswith('ws://'):
raise ValueError("WebSocket URI must start with 'ws://'")
return v
@validator('RECONNECT_ATTEMPTS')
def validate_reconnect_attempts(cls, v):
if v <= 0:
raise ValueError("Reconnect attempts must be a positive number.")
return v
@validator('HEARTBEAT_INTERVAL')
def validate_heartbeat_interval(cls, v):
if v <= 0:
raise ValueError("Heartbeat interval must be a positive number.")
return v
class LittleNavMapConfig(BaseModel):
BASE_URL: str = "http://localhost:8965"
UPDATE_INTERVAL: float = 1.0
CACHE_TTL: int = 30
@validator('UPDATE_INTERVAL')
def validate_update_interval(cls, v):
if v <= 0:
raise ValueError("Update interval must be a positive number.")
return v
@validator('CACHE_TTL')
def validate_cache_ttl(cls, v):
if v <= 0:
raise ValueError("Cache TTL must be a positive number.")
return v
class AviationWeatherConfig(BaseModel):
BASE_URL: str = "https://api.checkwx.com/metar"
TIMEOUT_MS: int = 5000
@dataclass
class Config:
twitch: TwitchConfig
database: DatabaseConfig
openai: OpenAIConfig
voice: VoiceConfig
streamerbot: StreamerBotConfig
littlenavmap: LittleNavMapConfig
aviationweather: AviationWeatherConfig
bot_trigger_words: List[str] = field(default_factory=lambda: ["bot", "assistant"])
bot_personality: str = "You are an AI Overlord managing a flight simulation Twitch channel."
verbose: bool = False
environment: str = field(default_factory=lambda: os.getenv('ENV', 'development'))
command_cooldowns: Dict[str, int] = field(default_factory=dict)
custom_commands_enabled: bool = True
log_level: str = "INFO"
sentry_dsn: Optional[str] = None
checkwx_api_key: Optional[str] = None
openweathermap_api_key: Optional[str] = None
command_permissions: Dict[str, Dict[str, Union[bool, List[str]]]] = field(default_factory=dict)
_file_path: Optional[str] = None
logger: logging.Logger = field(default_factory=lambda: logging.getLogger(__name__))
def __post_init__(self):
self.validate()
self.setup_derived_values()
self.load_command_permissions()
if self._file_path:
self.logger.info(f"Configuration loaded from file: {self._file_path}")
def validate(self):
"""Validate configuration values."""
if self.environment not in ['development', 'production', 'testing']:
raise ConfigError("Invalid environment specified")
def setup_derived_values(self):
"""Set up any derived configuration values."""
self.is_production = self.environment == 'production'
self.is_development = self.environment == 'development'
self.is_testing = self.environment == 'testing'
@classmethod
def load_from_env(cls) -> 'Config':
"""Load configuration from environment variables."""
load_dotenv()
try:
twitch_config = TwitchConfig(
OAUTH_TOKEN=os.getenv('TWITCH_OAUTH_TOKEN'),
CHANNEL=os.getenv('TWITCH_CHANNEL'),
BOT_NAME=os.getenv('BOT_NAME'),
BROADCASTER_ID=os.getenv('BROADCASTER_ID'),
PREFIX=os.getenv('BOT_PREFIX', '!')
)
database_config = DatabaseConfig(
URI=os.getenv('MONGO_URI'),
DB_NAME=os.getenv('MONGO_DB_NAME')
)
openai_config = OpenAIConfig(
API_KEY=os.getenv('CHATGPT_API_KEY'),
MODEL=os.getenv('OPENAI_MODEL', 'gpt-4o-mini') # Changed to gpt-4o-mini
)
voice_config = VoiceConfig(
ENABLED=os.getenv('VOICE_ENABLED', 'True').lower() == 'true',
PREFIX=os.getenv('VOICE_PREFIX', 'Hey Overlord'),
COMMAND_TIMEOUT=float(os.getenv('VOICE_COMMAND_TIMEOUT', '5')),
PHRASE_LIMIT=float(os.getenv('VOICE_COMMAND_PHRASE_LIMIT', '10')),
LANGUAGE=os.getenv('VOICE_COMMAND_LANGUAGE', 'en-US')
)
streamerbot_config = StreamerBotConfig(
WS_URI=os.getenv('STREAMERBOT_WS_URI')
)
littlenavmap_config = LittleNavMapConfig(
BASE_URL=os.getenv('LITTLENAVMAP_URL', 'http://localhost:8965')
)
aviationweather_config = AviationWeatherConfig(
)
openweathermap_api_key = os.getenv('OPENWEATHERMAP_API_KEY')
checkwx_api_key = os.getenv('CHECKWX_API_KEY')
config_file = os.getenv('CONFIG_FILE')
return cls(
twitch=twitch_config,
database=database_config,
openai=openai_config,
voice=voice_config,
streamerbot=streamerbot_config,
littlenavmap=littlenavmap_config,
aviationweather = aviationweather_config,
bot_trigger_words=os.getenv('BOT_TRIGGER_WORDS', 'bot,assistant').split(','),
bot_personality=os.getenv('BOT_PERSONALITY', 'You are an AI Overlord managing a flight simulation Twitch channel.'),
verbose=os.getenv('VERBOSE', 'False').lower() == 'true',
sentry_dsn=os.getenv('SENTRY_DSN'),
checkwx_api_key=checkwx_api_key,
openweathermap_api_key = openweathermap_api_key,
_file_path = config_file,
command_permissions = {},
logger = logging.getLogger(__name__)
)
except ValidationError as e:
logger.error(f"Pydantic validation error: {e}")
raise ConfigError(f"Configuration validation failed: {e}")
except ValueError as e:
logger.error(f"Value error during config loading: {e}")
raise ConfigError(f"Configuration loading failed: {e}")
except TypeError as e:
logger.error(f"Type error during config loading: {e}")
raise ConfigError(f"Configuration loading failed: {e}")
except Exception as e:
logger.error(f"Failed to load configuration: {e}")
raise ConfigError(f"Configuration loading failed: {e}")
@classmethod
def load_from_file(cls, file_path: str) -> 'Config':
"""Load configuration from a YAML file."""
try:
with open(file_path, 'r') as f:
config_data = yaml.safe_load(f)
twitch_config = TwitchConfig(**config_data.get('twitch', {}))
database_config = DatabaseConfig(**config_data.get('database', {}))
openai_config = OpenAIConfig(**config_data.get('openai', {}))
voice_config = VoiceConfig(**config_data.get('voice', {}))
streamerbot_config = StreamerBotConfig(**config_data.get('streamerbot', {}))
littlenavmap_config = LittleNavMapConfig(**config_data.get('littlenavmap', {}))
aviationweather_config = AviationWeatherConfig(**config_data.get('aviationweather', {}))
openweathermap_api_key = config_data.get('openweathermap_api_key')
checkwx_api_key = config_data.get('checkwx_api_key')
return cls(
twitch=twitch_config,
database=database_config,
openai=openai_config,
voice=voice_config,
streamerbot=streamerbot_config,
littlenavmap=littlenavmap_config,
aviationweather = aviationweather_config,
bot_trigger_words=config_data.get('bot_trigger_words', ["bot", "assistant"]),
bot_personality=config_data.get('bot_personality', 'You are an AI Overlord managing a flight simulation Twitch channel.'),
verbose=config_data.get('verbose', False),
sentry_dsn=os.getenv('SENTRY_DSN'),
checkwx_api_key = checkwx_api_key,
openweathermap_api_key = openweathermap_api_key,
command_permissions = config_data.get('command_permissions', {}),
_file_path = file_path,
logger = logging.getLogger(__name__)
)
except FileNotFoundError as e:
logger.error(f"Config file not found at {file_path}: {e}")
raise ConfigError(f"Config file not found: {file_path}") from e
except yaml.YAMLError as e:
logger.error(f"YAML parsing error: {e}")
raise ConfigError(f"YAML parsing failed: {e}") from e
except ValidationError as e:
logger.error(f"Pydantic validation error: {e}")
raise ConfigError(f"Configuration validation failed: {e}") from e
except ValueError as e:
logger.error(f"Value error during config loading: {e}")
raise ConfigError(f"Configuration loading failed: {e}") from e
except TypeError as e:
logger.error(f"Type error during config loading: {e}")
raise ConfigError(f"Configuration loading failed: {e}") from e
except Exception as e:
logger.error(f"Failed to load configuration from file: {e}")
raise ConfigError(f"Configuration file loading failed: {e}") from e
def reload(self):
"""Reload configuration from file."""
if self._file_path:
new_config = Config.load_from_file(self._file_path)
self.__dict__.update(new_config.__dict__)
self.logger.info(f"Configuration reloaded from: {self._file_path}")
else:
self.logger.warning("No config file path available to reload from.")
def load_command_permissions(self):
"""Load command permissions from the configuration."""
if self.command_permissions:
self.logger.info("Loading command permissions from configuration...")
for command_name, permissions in self.command_permissions.items():
self.command_cooldowns[command_name] = permissions.get("cooldown", 0)
def load_config() -> Config:
"""Load configuration from environment or file."""
try:
if config_file := os.getenv('CONFIG_FILE'):
if Path(config_file).exists():
return Config.load_from_file(config_file)
else:
logger.warning(f"Config file not found at {config_file}, falling back to environment variables.")
return Config.load_from_env()
return Config.load_from_env()
except ConfigError as e:
logger.error(f"Failed to load configuration: {e}")
raise