1use std::env;
7use std::path::PathBuf;
8
9use tracing::{debug, warn};
10
11use crate::error::AppError;
12
13#[derive(Debug, Clone)]
15pub struct Config {
16 pub langbase: LangbaseConfig,
18 pub database: DatabaseConfig,
20 pub logging: LoggingConfig,
22 pub request: RequestConfig,
24 pub pipes: PipeConfig,
26 pub error_handling: ErrorHandlingConfig,
28}
29
30#[derive(Debug, Clone, Default)]
37pub struct ErrorHandlingConfig {
38 }
41
42#[derive(Debug, Clone)]
44pub struct LangbaseConfig {
45 pub api_key: String,
47 pub base_url: String,
49}
50
51#[derive(Debug, Clone)]
53pub struct DatabaseConfig {
54 pub path: PathBuf,
56 pub max_connections: u32,
58}
59
60#[derive(Debug, Clone)]
62pub struct LoggingConfig {
63 pub level: String,
65 pub format: LogFormat,
67}
68
69#[derive(Debug, Clone, PartialEq)]
71pub enum LogFormat {
72 Pretty,
74 Json,
76}
77
78#[derive(Debug, Clone)]
80pub struct RequestConfig {
81 pub timeout_ms: u64,
83 pub max_retries: u32,
85 pub retry_delay_ms: u64,
87}
88
89#[derive(Debug, Clone)]
91pub struct PipeConfig {
92 pub linear: String,
94 pub tree: String,
96 pub divergent: String,
98 pub reflection: String,
100 pub auto_router: String,
102 pub auto: Option<String>,
104 pub backtracking: Option<String>,
106 pub got: Option<GotPipeConfig>,
108 pub detection: Option<DetectionPipeConfig>,
110 pub decision: Option<DecisionPipeConfig>,
112 pub evidence: Option<EvidencePipeConfig>,
114}
115
116#[derive(Debug, Clone)]
118pub struct DetectionPipeConfig {
119 pub pipe: Option<String>,
121}
122
123#[derive(Debug, Clone)]
125pub struct GotPipeConfig {
126 pub pipe: Option<String>,
128 pub max_nodes: Option<usize>,
130 pub max_depth: Option<usize>,
132 pub default_k: Option<usize>,
134 pub prune_threshold: Option<f64>,
136}
137
138#[derive(Debug, Clone)]
140pub struct DecisionPipeConfig {
141 pub pipe: Option<String>,
143}
144
145#[derive(Debug, Clone)]
147pub struct EvidencePipeConfig {
148 pub pipe: Option<String>,
150}
151
152impl Config {
153 pub fn from_env() -> Result<Self, AppError> {
155 match dotenvy::dotenv() {
157 Ok(path) => {
158 debug!(path = %path.display(), "Loaded .env file");
159 }
160 Err(dotenvy::Error::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound => {
161 debug!("No .env file found, using environment variables");
163 }
164 Err(e) => {
165 warn!(
166 error = %e,
167 "Failed to load .env file - check file permissions and syntax"
168 );
169 }
170 }
171
172 let langbase = LangbaseConfig {
173 api_key: env::var("LANGBASE_API_KEY").map_err(|_| AppError::Config {
174 message: "LANGBASE_API_KEY is required".to_string(),
175 })?,
176 base_url: env::var("LANGBASE_BASE_URL")
177 .unwrap_or_else(|_| "https://api.langbase.com".to_string()),
178 };
179
180 let database = DatabaseConfig {
181 path: PathBuf::from(
182 env::var("DATABASE_PATH").unwrap_or_else(|_| "./data/reasoning.db".to_string()),
183 ),
184 max_connections: env::var("DATABASE_MAX_CONNECTIONS")
185 .ok()
186 .and_then(|s| s.parse().ok())
187 .unwrap_or(5),
188 };
189
190 let logging = LoggingConfig {
191 level: env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()),
192 format: match env::var("LOG_FORMAT")
193 .unwrap_or_else(|_| "pretty".to_string())
194 .to_lowercase()
195 .as_str()
196 {
197 "json" => LogFormat::Json,
198 _ => LogFormat::Pretty,
199 },
200 };
201
202 let request = RequestConfig {
203 timeout_ms: env::var("REQUEST_TIMEOUT_MS")
204 .ok()
205 .and_then(|s| s.parse().ok())
206 .unwrap_or(30000),
207 max_retries: env::var("MAX_RETRIES")
208 .ok()
209 .and_then(|s| s.parse().ok())
210 .unwrap_or(3),
211 retry_delay_ms: env::var("RETRY_DELAY_MS")
212 .ok()
213 .and_then(|s| s.parse().ok())
214 .unwrap_or(1000),
215 };
216
217 let got_config = {
219 let pipe = env::var("PIPE_GOT").ok();
220 let max_nodes = env::var("GOT_MAX_NODES").ok().and_then(|s| s.parse().ok());
221 let max_depth = env::var("GOT_MAX_DEPTH").ok().and_then(|s| s.parse().ok());
222 let default_k = env::var("GOT_DEFAULT_K").ok().and_then(|s| s.parse().ok());
223 let prune_threshold = env::var("GOT_PRUNE_THRESHOLD")
224 .ok()
225 .and_then(|s| s.parse().ok());
226
227 if pipe.is_some()
229 || max_nodes.is_some()
230 || max_depth.is_some()
231 || default_k.is_some()
232 || prune_threshold.is_some()
233 {
234 Some(GotPipeConfig {
235 pipe,
236 max_nodes,
237 max_depth,
238 default_k,
239 prune_threshold,
240 })
241 } else {
242 None
243 }
244 };
245
246 let detection_pipe_env = env::var("PIPE_DETECTION").ok().filter(|s| !s.is_empty());
248 debug!(
249 pipe_detection_env = ?detection_pipe_env,
250 "Loading PIPE_DETECTION from environment"
251 );
252 let detection_config = Some(DetectionPipeConfig {
253 pipe: detection_pipe_env,
254 });
255
256 let decision_pipe_env = env::var("PIPE_DECISION_FRAMEWORK")
258 .ok()
259 .filter(|s| !s.is_empty());
260 debug!(
261 pipe_decision_env = ?decision_pipe_env,
262 "Loading PIPE_DECISION_FRAMEWORK from environment"
263 );
264 let decision_config = Some(DecisionPipeConfig {
265 pipe: decision_pipe_env.clone(),
266 });
267
268 let evidence_config = Some(EvidencePipeConfig {
270 pipe: decision_pipe_env,
271 });
272
273 let pipes = PipeConfig {
274 linear: env::var("PIPE_LINEAR").unwrap_or_else(|_| "linear-reasoning-v1".to_string()),
275 tree: env::var("PIPE_TREE").unwrap_or_else(|_| "tree-reasoning-v1".to_string()),
276 divergent: env::var("PIPE_DIVERGENT")
277 .unwrap_or_else(|_| "divergent-reasoning-v1".to_string()),
278 reflection: env::var("PIPE_REFLECTION").unwrap_or_else(|_| "reflection-v1".to_string()),
279 auto_router: env::var("PIPE_AUTO").unwrap_or_else(|_| "mode-router-v1".to_string()),
280 auto: env::var("PIPE_AUTO").ok(),
281 backtracking: env::var("PIPE_BACKTRACKING").ok(),
282 got: got_config,
283 detection: detection_config,
284 decision: decision_config,
285 evidence: evidence_config,
286 };
287
288 let error_handling = ErrorHandlingConfig::default();
290 debug!("Strict error handling enabled - all parse/API failures propagate as errors");
291
292 Ok(Config {
293 langbase,
294 database,
295 logging,
296 request,
297 pipes,
298 error_handling,
299 })
300 }
301}
302
303impl Default for RequestConfig {
304 fn default() -> Self {
305 Self {
306 timeout_ms: 30000,
307 max_retries: 3,
308 retry_delay_ms: 1000,
309 }
310 }
311}
312
313impl Default for PipeConfig {
314 fn default() -> Self {
315 Self {
316 linear: "linear-reasoning-v1".to_string(),
317 tree: "tree-reasoning-v1".to_string(),
318 divergent: "divergent-reasoning-v1".to_string(),
319 reflection: "reflection-v1".to_string(),
320 auto_router: "mode-router-v1".to_string(),
321 auto: None,
322 backtracking: None,
323 got: None,
324 detection: None,
325 decision: None,
326 evidence: None,
327 }
328 }
329}
330
331impl Default for DetectionPipeConfig {
332 fn default() -> Self {
333 Self {
334 pipe: Some("detection-v1".to_string()),
335 }
336 }
337}
338
339impl Default for GotPipeConfig {
340 fn default() -> Self {
341 Self {
342 pipe: Some("got-reasoning-v1".to_string()),
343 max_nodes: Some(100),
344 max_depth: Some(10),
345 default_k: Some(3),
346 prune_threshold: Some(0.3),
347 }
348 }
349}
350
351impl Default for DecisionPipeConfig {
352 fn default() -> Self {
353 Self {
354 pipe: Some("decision-framework-v1".to_string()),
355 }
356 }
357}
358
359impl Default for EvidencePipeConfig {
360 fn default() -> Self {
361 Self {
362 pipe: Some("decision-framework-v1".to_string()),
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn test_request_config_default() {
373 let config = RequestConfig::default();
374 assert_eq!(config.timeout_ms, 30000);
375 assert_eq!(config.max_retries, 3);
376 assert_eq!(config.retry_delay_ms, 1000);
377 }
378
379 #[test]
380 fn test_pipe_config_default() {
381 let config = PipeConfig::default();
382 assert_eq!(config.linear, "linear-reasoning-v1");
383 assert_eq!(config.tree, "tree-reasoning-v1");
384 assert_eq!(config.divergent, "divergent-reasoning-v1");
385 assert_eq!(config.reflection, "reflection-v1");
386 assert_eq!(config.auto_router, "mode-router-v1");
387 assert!(config.auto.is_none());
388 assert!(config.backtracking.is_none());
389 assert!(config.got.is_none());
390 assert!(config.detection.is_none());
391 assert!(config.decision.is_none());
392 assert!(config.evidence.is_none());
393 }
394
395 #[test]
396 fn test_detection_pipe_config_default() {
397 let config = DetectionPipeConfig::default();
398 assert_eq!(config.pipe, Some("detection-v1".to_string()));
399 }
400
401 #[test]
402 fn test_got_pipe_config_default() {
403 let config = GotPipeConfig::default();
404 assert_eq!(config.pipe, Some("got-reasoning-v1".to_string()));
405 assert_eq!(config.max_nodes, Some(100));
406 assert_eq!(config.max_depth, Some(10));
407 assert_eq!(config.default_k, Some(3));
408 assert_eq!(config.prune_threshold, Some(0.3));
409 }
410
411 #[test]
412 fn test_log_format_variants() {
413 assert_eq!(LogFormat::Pretty, LogFormat::Pretty);
414 assert_eq!(LogFormat::Json, LogFormat::Json);
415 assert_ne!(LogFormat::Pretty, LogFormat::Json);
416 }
417
418 #[test]
419 fn test_decision_pipe_config_default() {
420 let config = DecisionPipeConfig::default();
421 assert_eq!(config.pipe, Some("decision-framework-v1".to_string()));
422 }
423
424 #[test]
425 fn test_evidence_pipe_config_default() {
426 let config = EvidencePipeConfig::default();
427 assert_eq!(config.pipe, Some("decision-framework-v1".to_string()));
428 }
429
430 #[test]
435 fn test_database_config_struct() {
436 let config = DatabaseConfig {
437 path: PathBuf::from("/test/path.db"),
438 max_connections: 10,
439 };
440 assert_eq!(config.path, PathBuf::from("/test/path.db"));
441 assert_eq!(config.max_connections, 10);
442 }
443
444 #[test]
445 fn test_langbase_config_struct() {
446 let config = LangbaseConfig {
447 api_key: "test-key".to_string(),
448 base_url: "https://test.api.com".to_string(),
449 };
450 assert_eq!(config.api_key, "test-key");
451 assert_eq!(config.base_url, "https://test.api.com");
452 }
453
454 #[test]
455 fn test_logging_config_struct() {
456 let config_pretty = LoggingConfig {
457 level: "debug".to_string(),
458 format: LogFormat::Pretty,
459 };
460 assert_eq!(config_pretty.level, "debug");
461 assert_eq!(config_pretty.format, LogFormat::Pretty);
462
463 let config_json = LoggingConfig {
464 level: "info".to_string(),
465 format: LogFormat::Json,
466 };
467 assert_eq!(config_json.level, "info");
468 assert_eq!(config_json.format, LogFormat::Json);
469 }
470
471 #[test]
472 fn test_request_config_struct() {
473 let config = RequestConfig {
474 timeout_ms: 60000,
475 max_retries: 5,
476 retry_delay_ms: 2000,
477 };
478 assert_eq!(config.timeout_ms, 60000);
479 assert_eq!(config.max_retries, 5);
480 assert_eq!(config.retry_delay_ms, 2000);
481 }
482
483 #[test]
484 fn test_pipe_config_struct_all_fields() {
485 let config = PipeConfig {
486 linear: "linear-v1".to_string(),
487 tree: "tree-v1".to_string(),
488 divergent: "divergent-v1".to_string(),
489 reflection: "reflection-v1".to_string(),
490 auto_router: "router-v1".to_string(),
491 auto: Some("auto-v1".to_string()),
492 backtracking: Some("backtrack-v1".to_string()),
493 got: Some(GotPipeConfig::default()),
494 detection: Some(DetectionPipeConfig::default()),
495 decision: Some(DecisionPipeConfig::default()),
496 evidence: Some(EvidencePipeConfig::default()),
497 };
498
499 assert_eq!(config.linear, "linear-v1");
500 assert_eq!(config.tree, "tree-v1");
501 assert_eq!(config.divergent, "divergent-v1");
502 assert_eq!(config.reflection, "reflection-v1");
503 assert_eq!(config.auto_router, "router-v1");
504 assert_eq!(config.auto, Some("auto-v1".to_string()));
505 assert_eq!(config.backtracking, Some("backtrack-v1".to_string()));
506 assert!(config.got.is_some());
507 assert!(config.detection.is_some());
508 assert!(config.decision.is_some());
509 assert!(config.evidence.is_some());
510 }
511
512 #[test]
513 fn test_detection_pipe_config_struct() {
514 let config = DetectionPipeConfig {
515 pipe: Some("detection-v1".to_string()),
516 };
517 assert_eq!(config.pipe, Some("detection-v1".to_string()));
518 }
519
520 #[test]
521 fn test_detection_pipe_config_none_values() {
522 let config = DetectionPipeConfig { pipe: None };
523 assert!(config.pipe.is_none());
524 }
525
526 #[test]
527 fn test_got_pipe_config_struct_all_fields() {
528 let config = GotPipeConfig {
529 pipe: Some("got-reasoning-v1".to_string()),
530 max_nodes: Some(50),
531 max_depth: Some(5),
532 default_k: Some(2),
533 prune_threshold: Some(0.5),
534 };
535
536 assert_eq!(config.pipe, Some("got-reasoning-v1".to_string()));
537 assert_eq!(config.max_nodes, Some(50));
538 assert_eq!(config.max_depth, Some(5));
539 assert_eq!(config.default_k, Some(2));
540 assert_eq!(config.prune_threshold, Some(0.5));
541 }
542
543 #[test]
544 fn test_got_pipe_config_none_values() {
545 let config = GotPipeConfig {
546 pipe: None,
547 max_nodes: None,
548 max_depth: None,
549 default_k: None,
550 prune_threshold: None,
551 };
552
553 assert!(config.pipe.is_none());
554 assert!(config.max_nodes.is_none());
555 assert!(config.max_depth.is_none());
556 assert!(config.default_k.is_none());
557 assert!(config.prune_threshold.is_none());
558 }
559
560 #[test]
561 fn test_decision_pipe_config_struct() {
562 let config = DecisionPipeConfig {
563 pipe: Some("decision-framework-v1".to_string()),
564 };
565 assert_eq!(config.pipe, Some("decision-framework-v1".to_string()));
566 }
567
568 #[test]
569 fn test_decision_pipe_config_none_values() {
570 let config = DecisionPipeConfig { pipe: None };
571 assert!(config.pipe.is_none());
572 }
573
574 #[test]
575 fn test_evidence_pipe_config_struct() {
576 let config = EvidencePipeConfig {
577 pipe: Some("decision-framework-v1".to_string()),
578 };
579 assert_eq!(config.pipe, Some("decision-framework-v1".to_string()));
580 }
581
582 #[test]
583 fn test_evidence_pipe_config_none_values() {
584 let config = EvidencePipeConfig { pipe: None };
585 assert!(config.pipe.is_none());
586 }
587
588 #[test]
589 fn test_config_struct_clone() {
590 let config = RequestConfig::default();
591 let cloned = config.clone();
592 assert_eq!(config.timeout_ms, cloned.timeout_ms);
593 assert_eq!(config.max_retries, cloned.max_retries);
594 assert_eq!(config.retry_delay_ms, cloned.retry_delay_ms);
595 }
596
597 #[test]
598 fn test_pipe_config_clone() {
599 let config = PipeConfig::default();
600 let cloned = config.clone();
601 assert_eq!(config.linear, cloned.linear);
602 assert_eq!(config.tree, cloned.tree);
603 assert_eq!(config.divergent, cloned.divergent);
604 }
605
606 #[test]
607 fn test_log_format_debug() {
608 let pretty = LogFormat::Pretty;
609 let json = LogFormat::Json;
610 assert!(format!("{:?}", pretty).contains("Pretty"));
611 assert!(format!("{:?}", json).contains("Json"));
612 }
613
614 #[test]
615 fn test_database_config_debug() {
616 let config = DatabaseConfig {
617 path: PathBuf::from("/test.db"),
618 max_connections: 5,
619 };
620 let debug_str = format!("{:?}", config);
621 assert!(debug_str.contains("DatabaseConfig"));
622 assert!(debug_str.contains("test.db"));
623 }
624
625 #[test]
626 fn test_langbase_config_debug() {
627 let config = LangbaseConfig {
628 api_key: "key123".to_string(),
629 base_url: "https://api.test.com".to_string(),
630 };
631 let debug_str = format!("{:?}", config);
632 assert!(debug_str.contains("LangbaseConfig"));
633 assert!(debug_str.contains("key123"));
634 }
635
636 #[test]
637 fn test_got_pipe_config_default_values() {
638 let config = GotPipeConfig::default();
639
640 assert_eq!(config.pipe.as_deref(), Some("got-reasoning-v1"));
642
643 assert_eq!(config.max_nodes, Some(100));
645 assert_eq!(config.max_depth, Some(10));
646 assert_eq!(config.default_k, Some(3));
647 assert_eq!(config.prune_threshold, Some(0.3));
648 }
649
650 #[test]
651 fn test_detection_pipe_config_default_values() {
652 let config = DetectionPipeConfig::default();
653 assert_eq!(config.pipe.as_deref(), Some("detection-v1"));
654 }
655
656 #[test]
657 fn test_decision_pipe_config_default_values() {
658 let config = DecisionPipeConfig::default();
659 assert_eq!(config.pipe.as_deref(), Some("decision-framework-v1"));
660 }
661
662 #[test]
663 fn test_evidence_pipe_config_default_values() {
664 let config = EvidencePipeConfig::default();
665 assert_eq!(config.pipe.as_deref(), Some("decision-framework-v1"));
666 }
667
668 #[test]
669 fn test_request_config_default_values() {
670 let config = RequestConfig::default();
671
672 assert_eq!(config.timeout_ms, 30000);
674 assert_eq!(config.max_retries, 3);
675 assert_eq!(config.retry_delay_ms, 1000);
676 }
677
678 #[test]
679 fn test_pipe_config_default_values() {
680 let config = PipeConfig::default();
681
682 assert_eq!(config.linear, "linear-reasoning-v1");
684 assert_eq!(config.tree, "tree-reasoning-v1");
685 assert_eq!(config.divergent, "divergent-reasoning-v1");
686 assert_eq!(config.reflection, "reflection-v1");
687 assert_eq!(config.auto_router, "mode-router-v1");
688
689 assert!(config.auto.is_none());
691 assert!(config.backtracking.is_none());
692 assert!(config.got.is_none());
693 assert!(config.detection.is_none());
694 assert!(config.decision.is_none());
695 assert!(config.evidence.is_none());
696 }
697
698 #[test]
699 fn test_log_format_clone() {
700 let original = LogFormat::Pretty;
701 let cloned = original.clone();
702 assert_eq!(original, cloned);
703
704 let original_json = LogFormat::Json;
705 let cloned_json = original_json.clone();
706 assert_eq!(original_json, cloned_json);
707 }
708
709 #[test]
710 fn test_detection_pipe_config_clone() {
711 let config = DetectionPipeConfig::default();
712 let cloned = config.clone();
713 assert_eq!(config.pipe, cloned.pipe);
714 }
715
716 #[test]
717 fn test_got_pipe_config_clone() {
718 let config = GotPipeConfig::default();
719 let cloned = config.clone();
720 assert_eq!(config.pipe, cloned.pipe);
721 assert_eq!(config.max_nodes, cloned.max_nodes);
722 assert_eq!(config.prune_threshold, cloned.prune_threshold);
723 }
724
725 #[test]
726 fn test_decision_pipe_config_clone() {
727 let config = DecisionPipeConfig::default();
728 let cloned = config.clone();
729 assert_eq!(config.pipe, cloned.pipe);
730 }
731
732 #[test]
733 fn test_evidence_pipe_config_clone() {
734 let config = EvidencePipeConfig::default();
735 let cloned = config.clone();
736 assert_eq!(config.pipe, cloned.pipe);
737 }
738
739 #[test]
740 fn test_database_config_clone() {
741 let config = DatabaseConfig {
742 path: PathBuf::from("/test.db"),
743 max_connections: 10,
744 };
745 let cloned = config.clone();
746 assert_eq!(config.path, cloned.path);
747 assert_eq!(config.max_connections, cloned.max_connections);
748 }
749
750 #[test]
751 fn test_langbase_config_clone() {
752 let config = LangbaseConfig {
753 api_key: "test-key".to_string(),
754 base_url: "https://test.com".to_string(),
755 };
756 let cloned = config.clone();
757 assert_eq!(config.api_key, cloned.api_key);
758 assert_eq!(config.base_url, cloned.base_url);
759 }
760
761 #[test]
762 fn test_logging_config_clone() {
763 let config = LoggingConfig {
764 level: "debug".to_string(),
765 format: LogFormat::Pretty,
766 };
767 let cloned = config.clone();
768 assert_eq!(config.level, cloned.level);
769 assert_eq!(config.format, cloned.format);
770 }
771
772 #[test]
773 fn test_request_config_clone() {
774 let config = RequestConfig {
775 timeout_ms: 5000,
776 max_retries: 2,
777 retry_delay_ms: 500,
778 };
779 let cloned = config.clone();
780 assert_eq!(config.timeout_ms, cloned.timeout_ms);
781 assert_eq!(config.max_retries, cloned.max_retries);
782 assert_eq!(config.retry_delay_ms, cloned.retry_delay_ms);
783 }
784
785 #[test]
788 fn test_error_handling_config_default() {
789 let _config = ErrorHandlingConfig::default();
790 }
792
793 #[test]
794 fn test_error_handling_config_clone() {
795 let config = ErrorHandlingConfig::default();
796 let _cloned = config.clone();
797 }
799
800 #[test]
801 fn test_error_handling_config_debug() {
802 let config = ErrorHandlingConfig::default();
803 let debug_str = format!("{:?}", config);
804 assert!(debug_str.contains("ErrorHandlingConfig"));
805 }
806}