pub mod virtual_path;
use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub global_context: Option<String>,
#[serde(default)]
pub collections: HashMap<String, CollectionConfig>,
#[serde(default)]
pub llm_service: LLMServiceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMServiceConfig {
pub url: String,
#[serde(default = "default_chat_model")]
pub model: String,
#[serde(default)]
pub embedding_url: Option<String>,
#[serde(default = "default_embedding_model")]
pub embedding_model: String,
#[serde(default)]
pub embedding_dimensions: Option<usize>,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
impl LLMServiceConfig {
pub fn embeddings_url(&self) -> &str {
self.embedding_url.as_deref().unwrap_or(&self.url)
}
}
impl Default for LLMServiceConfig {
fn default() -> Self {
Self {
url: std::env::var("AGENTROOT_LLM_URL")
.unwrap_or_else(|_| "http://localhost:8000".to_string()),
model: default_chat_model(),
embedding_url: std::env::var("AGENTROOT_EMBEDDING_URL").ok(),
embedding_model: default_embedding_model(),
embedding_dimensions: std::env::var("AGENTROOT_EMBEDDING_DIMS")
.ok()
.and_then(|s| s.parse().ok()),
api_key: std::env::var("AGENTROOT_LLM_API_KEY").ok(),
timeout_secs: default_timeout(),
}
}
}
fn default_chat_model() -> String {
std::env::var("AGENTROOT_LLM_MODEL")
.unwrap_or_else(|_| "meta-llama/Llama-3.1-8B-Instruct".to_string())
}
fn default_embedding_model() -> String {
std::env::var("AGENTROOT_EMBEDDING_MODEL")
.unwrap_or_else(|_| "sentence-transformers/all-MiniLM-L6-v2".to_string())
}
fn default_timeout() -> u64 {
30
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollectionConfig {
pub path: PathBuf,
#[serde(default = "default_pattern")]
pub pattern: String,
#[serde(default)]
pub context: HashMap<String, String>,
#[serde(default)]
pub update: Option<String>,
}
fn default_pattern() -> String {
"**/*.md".to_string()
}
impl Config {
pub fn load() -> Result<Self> {
let path = Self::default_path();
if path.exists() {
let content = std::fs::read_to_string(&path)?;
let config: Config = serde_yaml::from_str(&content)?;
Ok(config)
} else {
Ok(Config::default())
}
}
pub fn save(&self) -> Result<()> {
let path = Self::default_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = serde_yaml::to_string(self)?;
std::fs::write(path, content)?;
Ok(())
}
pub fn default_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(crate::CONFIG_DIR_NAME)
.join("config.yml")
}
pub fn get_context_for_path(&self, collection: &str, path: &str) -> Option<String> {
let collection_config = self.collections.get(collection)?;
let mut matching: Vec<(&str, &str)> = collection_config
.context
.iter()
.filter(|(prefix, _)| path.starts_with(*prefix) || prefix.is_empty() || *prefix == "/")
.map(|(prefix, ctx)| (prefix.as_str(), ctx.as_str()))
.collect();
matching.sort_by_key(|(prefix, _)| prefix.len());
if matching.is_empty() {
self.global_context.clone()
} else {
let combined: Vec<&str> = matching.iter().map(|(_, ctx)| *ctx).collect();
let mut result = combined.join("\n\n");
if let Some(ref global) = self.global_context {
result = format!("{}\n\n{}", global, result);
}
Some(result)
}
}
}