agentroot_core/config/
mod.rs

1//! Configuration management
2
3pub mod virtual_path;
4
5use crate::error::Result;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10/// Main configuration structure
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct Config {
13    /// Global context applied to all searches
14    #[serde(default)]
15    pub global_context: Option<String>,
16
17    /// Collection configurations
18    #[serde(default)]
19    pub collections: HashMap<String, CollectionConfig>,
20
21    /// LLM service configuration
22    #[serde(default)]
23    pub llm_service: LLMServiceConfig,
24}
25
26/// LLM service configuration for external inference
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct LLMServiceConfig {
29    /// Base URL of the LLM service for chat/completions
30    pub url: String,
31
32    /// Model name for chat completions (query parsing, metadata generation)
33    #[serde(default = "default_chat_model")]
34    pub model: String,
35
36    /// Base URL for embeddings service (can be different from LLM URL)
37    #[serde(default)]
38    pub embedding_url: Option<String>,
39
40    /// Model name for embeddings
41    #[serde(default = "default_embedding_model")]
42    pub embedding_model: String,
43
44    /// Embedding dimensions (will be auto-detected if not specified)
45    #[serde(default)]
46    pub embedding_dimensions: Option<usize>,
47
48    /// API key (optional, for authenticated services)
49    #[serde(default)]
50    pub api_key: Option<String>,
51
52    /// Request timeout in seconds
53    #[serde(default = "default_timeout")]
54    pub timeout_secs: u64,
55}
56
57impl LLMServiceConfig {
58    /// Get the embeddings URL (falls back to main URL if not specified)
59    pub fn embeddings_url(&self) -> &str {
60        self.embedding_url.as_deref().unwrap_or(&self.url)
61    }
62}
63
64impl Default for LLMServiceConfig {
65    fn default() -> Self {
66        Self {
67            url: std::env::var("AGENTROOT_LLM_URL")
68                .unwrap_or_else(|_| "http://localhost:8000".to_string()),
69            model: default_chat_model(),
70            embedding_url: std::env::var("AGENTROOT_EMBEDDING_URL").ok(),
71            embedding_model: default_embedding_model(),
72            embedding_dimensions: std::env::var("AGENTROOT_EMBEDDING_DIMS")
73                .ok()
74                .and_then(|s| s.parse().ok()),
75            api_key: std::env::var("AGENTROOT_LLM_API_KEY").ok(),
76            timeout_secs: default_timeout(),
77        }
78    }
79}
80
81fn default_chat_model() -> String {
82    std::env::var("AGENTROOT_LLM_MODEL")
83        .unwrap_or_else(|_| "meta-llama/Llama-3.1-8B-Instruct".to_string())
84}
85
86fn default_embedding_model() -> String {
87    std::env::var("AGENTROOT_EMBEDDING_MODEL")
88        .unwrap_or_else(|_| "sentence-transformers/all-MiniLM-L6-v2".to_string())
89}
90
91fn default_timeout() -> u64 {
92    30
93}
94
95/// Per-collection configuration
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct CollectionConfig {
98    /// Root path of the collection
99    pub path: PathBuf,
100
101    /// Glob pattern for files to index
102    #[serde(default = "default_pattern")]
103    pub pattern: String,
104
105    /// Context strings keyed by path prefix
106    #[serde(default)]
107    pub context: HashMap<String, String>,
108
109    /// Command to run before updating (e.g., git pull)
110    #[serde(default)]
111    pub update: Option<String>,
112}
113
114fn default_pattern() -> String {
115    "**/*.md".to_string()
116}
117
118impl Config {
119    /// Load config from default path
120    pub fn load() -> Result<Self> {
121        let path = Self::default_path();
122        if path.exists() {
123            let content = std::fs::read_to_string(&path)?;
124            let config: Config = serde_yaml::from_str(&content)?;
125            Ok(config)
126        } else {
127            Ok(Config::default())
128        }
129    }
130
131    /// Save config to default path
132    pub fn save(&self) -> Result<()> {
133        let path = Self::default_path();
134        if let Some(parent) = path.parent() {
135            std::fs::create_dir_all(parent)?;
136        }
137        let content = serde_yaml::to_string(self)?;
138        std::fs::write(path, content)?;
139        Ok(())
140    }
141
142    /// Get default config path
143    pub fn default_path() -> PathBuf {
144        dirs::config_dir()
145            .unwrap_or_else(|| PathBuf::from("."))
146            .join(crate::CONFIG_DIR_NAME)
147            .join("config.yml")
148    }
149
150    /// Get context for a path (uses hierarchical inheritance)
151    pub fn get_context_for_path(&self, collection: &str, path: &str) -> Option<String> {
152        let collection_config = self.collections.get(collection)?;
153
154        // Collect all matching contexts
155        let mut matching: Vec<(&str, &str)> = collection_config
156            .context
157            .iter()
158            .filter(|(prefix, _)| path.starts_with(*prefix) || prefix.is_empty() || *prefix == "/")
159            .map(|(prefix, ctx)| (prefix.as_str(), ctx.as_str()))
160            .collect();
161
162        // Sort by prefix length (shortest first for inheritance)
163        matching.sort_by_key(|(prefix, _)| prefix.len());
164
165        // Combine contexts (general to specific)
166        if matching.is_empty() {
167            self.global_context.clone()
168        } else {
169            let combined: Vec<&str> = matching.iter().map(|(_, ctx)| *ctx).collect();
170            let mut result = combined.join("\n\n");
171            if let Some(ref global) = self.global_context {
172                result = format!("{}\n\n{}", global, result);
173            }
174            Some(result)
175        }
176    }
177}