1use std::time::Duration;
7
8use anyhow::{Context as _, Result};
9use serde::{Deserialize, Serialize};
10use validator::{Validate, ValidationError};
11
12use crate::error::Error;
13
14#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
16pub struct BotConfig {
17 #[validate(custom(function = "validate_model"))]
19 pub model: String,
20
21 #[validate(range(min = 0.0, max = 1.0))]
23 pub temperature: f32,
24
25 #[validate(range(min = 1, max = 100_000))]
27 pub max_tokens: usize,
28
29 #[serde(with = "humantime_serde")]
31 pub timeout: Duration,
32
33 #[validate(range(min = 0, max = 10))]
35 pub max_retries: u32,
36
37 pub enable_logging: bool,
39
40 pub enable_cost_tracking: bool,
42
43 pub context_config: ContextConfig,
45
46 pub pipeline_config: PipelineConfig,
48
49 pub plugin_config: PluginConfig,
51}
52
53impl BotConfig {
54 #[must_use]
56 pub fn builder() -> BotConfigBuilder {
57 BotConfigBuilder::default()
58 }
59
60 pub fn validate(&self) -> Result<()> {
66 Validate::validate(self).map_err(|e| Error::Validation(e.to_string()))?;
67 Ok(())
68 }
69
70 pub fn from_env() -> Result<Self> {
76 let model = std::env::var("DEFAULT_MODEL")
77 .unwrap_or_else(|_| "anthropic.claude-opus-4-1".to_string());
78
79 let temperature = std::env::var("TEMPERATURE")
80 .unwrap_or_else(|_| "0.1".to_string())
81 .parse()
82 .context("Invalid TEMPERATURE value")?;
83
84 let max_tokens = std::env::var("MAX_TOKENS")
85 .unwrap_or_else(|_| "2048".to_string())
86 .parse()
87 .context("Invalid MAX_TOKENS value")?;
88
89 Ok(Self {
90 model,
91 temperature,
92 max_tokens,
93 ..Default::default()
94 })
95 }
96}
97
98impl Default for BotConfig {
99 fn default() -> Self {
100 Self {
101 model: "anthropic.claude-opus-4-1".to_string(),
102 temperature: 0.1,
103 max_tokens: 2048,
104 timeout: Duration::from_secs(30),
105 max_retries: 3,
106 enable_logging: true,
107 enable_cost_tracking: true,
108 context_config: ContextConfig::default(),
109 pipeline_config: PipelineConfig::default(),
110 plugin_config: PluginConfig::default(),
111 }
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
117pub struct ContextConfig {
118 #[validate(range(min = 100, max = 100_000))]
120 pub max_context_tokens: usize,
121
122 #[serde(with = "humantime_serde")]
124 pub context_ttl: Duration,
125
126 pub persist_context: bool,
128
129 pub storage_backend: StorageBackend,
131}
132
133impl Default for ContextConfig {
134 fn default() -> Self {
135 Self {
136 max_context_tokens: 4096,
137 context_ttl: Duration::from_secs(3600),
138 persist_context: false,
139 storage_backend: StorageBackend::Memory,
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "lowercase")]
147pub enum StorageBackend {
148 Memory,
150 Redis {
152 url: String,
154 },
155 Postgres {
157 url: String,
159 },
160 Sqlite {
162 path: String,
164 },
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
169pub struct PipelineConfig {
170 pub enable_sanitization: bool,
172
173 pub enable_enrichment: bool,
175
176 #[serde(with = "humantime_serde")]
178 pub max_processing_time: Duration,
179
180 pub enabled_stages: Vec<String>,
182}
183
184impl Default for PipelineConfig {
185 fn default() -> Self {
186 Self {
187 enable_sanitization: true,
188 enable_enrichment: true,
189 max_processing_time: Duration::from_secs(10),
190 enabled_stages: vec![
191 "sanitize".to_string(),
192 "enrich".to_string(),
193 "route".to_string(),
194 "process".to_string(),
195 "format".to_string(),
196 ],
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct PluginConfig {
204 pub enable_plugins: bool,
206
207 pub plugin_dirs: Vec<String>,
209
210 pub auto_load: Vec<String>,
212
213 #[serde(with = "humantime_serde")]
215 pub plugin_timeout: Duration,
216}
217
218impl Default for PluginConfig {
219 fn default() -> Self {
220 Self {
221 enable_plugins: true,
222 plugin_dirs: vec!["plugins".to_string()],
223 auto_load: Vec::new(),
224 plugin_timeout: Duration::from_secs(5),
225 }
226 }
227}
228
229#[derive(Default)]
231pub struct BotConfigBuilder {
232 model: Option<String>,
233 temperature: Option<f32>,
234 max_tokens: Option<usize>,
235 timeout: Option<Duration>,
236 max_retries: Option<u32>,
237 enable_logging: Option<bool>,
238 enable_cost_tracking: Option<bool>,
239 context_config: Option<ContextConfig>,
240 pipeline_config: Option<PipelineConfig>,
241 plugin_config: Option<PluginConfig>,
242}
243
244impl BotConfigBuilder {
245 #[must_use]
247 pub fn model(mut self, model: impl Into<String>) -> Self {
248 self.model = Some(model.into());
249 self
250 }
251
252 #[must_use]
254 pub fn temperature(mut self, temperature: f32) -> Self {
255 self.temperature = Some(temperature);
256 self
257 }
258
259 #[must_use]
261 pub fn max_tokens(mut self, max_tokens: usize) -> Self {
262 self.max_tokens = Some(max_tokens);
263 self
264 }
265
266 #[must_use]
268 pub fn timeout(mut self, timeout: Duration) -> Self {
269 self.timeout = Some(timeout);
270 self
271 }
272
273 #[must_use]
275 pub fn max_retries(mut self, max_retries: u32) -> Self {
276 self.max_retries = Some(max_retries);
277 self
278 }
279
280 #[must_use]
282 pub fn enable_logging(mut self, enable: bool) -> Self {
283 self.enable_logging = Some(enable);
284 self
285 }
286
287 #[must_use]
289 pub fn enable_cost_tracking(mut self, enable: bool) -> Self {
290 self.enable_cost_tracking = Some(enable);
291 self
292 }
293
294 #[must_use]
296 pub fn context_config(mut self, config: ContextConfig) -> Self {
297 self.context_config = Some(config);
298 self
299 }
300
301 #[must_use]
303 pub fn pipeline_config(mut self, config: PipelineConfig) -> Self {
304 self.pipeline_config = Some(config);
305 self
306 }
307
308 #[must_use]
310 pub fn plugin_config(mut self, config: PluginConfig) -> Self {
311 self.plugin_config = Some(config);
312 self
313 }
314
315 pub fn build(self) -> Result<BotConfig> {
321 let config = BotConfig {
322 model: self.model.context("model is required")?,
323 temperature: self.temperature.unwrap_or(0.1),
324 max_tokens: self.max_tokens.unwrap_or(2048),
325 timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
326 max_retries: self.max_retries.unwrap_or(3),
327 enable_logging: self.enable_logging.unwrap_or(true),
328 enable_cost_tracking: self.enable_cost_tracking.unwrap_or(true),
329 context_config: self.context_config.unwrap_or_default(),
330 pipeline_config: self.pipeline_config.unwrap_or_default(),
331 plugin_config: self.plugin_config.unwrap_or_default(),
332 };
333
334 config.validate()?;
335 Ok(config)
336 }
337}
338
339fn validate_model(model: &str) -> Result<(), ValidationError> {
341 const ALLOWED_MODELS: &[&str] = &[
342 "anthropic.claude-opus-4-1",
343 "us.anthropic.claude-opus-4-1-20250805-v1:0", "anthropic.claude-sonnet-4",
345 "anthropic.claude-haiku",
346 "meta.llama3-70b-instruct",
347 "meta.llama3-8b-instruct",
348 "amazon.titan-text-express",
349 "ai21.j2-ultra",
350 "ai21.j2-mid",
351 ];
352
353 if ALLOWED_MODELS.contains(&model) {
354 Ok(())
355 } else {
356 Err(ValidationError::new("invalid_model"))
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_default_config() {
366 let config = BotConfig::default();
367 assert_eq!(config.model, "anthropic.claude-opus-4-1");
368 assert!((config.temperature - 0.1).abs() < f32::EPSILON);
369 assert_eq!(config.max_tokens, 2048);
370 assert!(config.validate().is_ok());
371 }
372
373 #[test]
374 fn test_config_builder() {
375 let config = BotConfig::builder()
376 .model("anthropic.claude-sonnet-4")
377 .temperature(0.5)
378 .max_tokens(4096)
379 .timeout(Duration::from_secs(60))
380 .build();
381
382 assert!(config.is_ok());
383 let config = config.unwrap();
384 assert_eq!(config.model, "anthropic.claude-sonnet-4");
385 assert!((config.temperature - 0.5).abs() < f32::EPSILON);
386 assert_eq!(config.max_tokens, 4096);
387 }
388
389 #[test]
390 fn test_invalid_model() {
391 let config = BotConfig::builder().model("invalid-model").build();
392
393 assert!(config.is_err());
394 }
395
396 #[test]
397 fn test_invalid_temperature() {
398 let config = BotConfig {
399 temperature: 1.5,
400 ..Default::default()
401 };
402 assert!(config.validate().is_err());
403
404 let config = BotConfig {
405 temperature: -0.1,
406 ..Default::default()
407 };
408 assert!(config.validate().is_err());
409 }
410
411 #[test]
412 fn test_from_env() {
413 std::env::set_var("DEFAULT_MODEL", "anthropic.claude-opus-4-1");
414 std::env::set_var("TEMPERATURE", "0.7");
415 std::env::set_var("MAX_TOKENS", "4096");
416
417 let config = BotConfig::from_env();
418 assert!(config.is_ok());
419
420 let config = config.unwrap();
421 assert!((config.temperature - 0.7).abs() < f32::EPSILON);
422 assert_eq!(config.max_tokens, 4096);
423 }
424
425 #[cfg(feature = "property-testing")]
426 mod property_tests {
427 use super::*;
428 use proptest::prelude::*;
429
430 proptest! {
431 #[test]
432 fn test_temperature_validation(temp in -10.0f32..10.0) {
433 let config = BotConfig {
434 temperature: temp,
435 ..Default::default()
436 };
437
438 let result = config.validate();
439 if (0.0..=1.0).contains(&temp) {
440 prop_assert!(result.is_ok());
441 } else {
442 prop_assert!(result.is_err());
443 }
444 }
445
446 #[test]
447 fn test_max_tokens_validation(tokens in 0usize..200_000) {
448 let config = BotConfig {
449 max_tokens: tokens,
450 ..Default::default()
451 };
452
453 let result = config.validate();
454 if tokens > 0 && tokens <= 100_000 {
455 prop_assert!(result.is_ok());
456 } else {
457 prop_assert!(result.is_err());
458 }
459 }
460 }
461 }
462}