use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use crate::embedding::types::EmbeddingConfig;
use crate::storage;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMConfig {
pub description_model: String,
pub relationship_model: String,
pub ai_batch_size: usize,
pub max_batch_tokens: usize,
pub batch_timeout_seconds: u64,
pub fallback_to_individual: bool,
pub max_sample_tokens: usize,
pub confidence_threshold: f32,
pub architectural_weight: f32,
pub relationship_system_prompt: String,
pub description_system_prompt: String,
}
impl Default for LLMConfig {
fn default() -> Self {
panic!("LLM config must be loaded from template file - defaults not allowed")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphRAGConfig {
pub enabled: bool,
pub use_llm: bool,
pub llm: LLMConfig,
}
impl Default for GraphRAGConfig {
fn default() -> Self {
panic!("GraphRAG config must be loaded from template file - defaults not allowed")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
pub model: String,
pub timeout: u64,
pub temperature: f32,
pub max_tokens: usize,
}
impl Default for LlmConfig {
fn default() -> Self {
Self {
model: "openrouter:openai/gpt-4o-mini".to_string(),
timeout: 120,
temperature: 0.7,
max_tokens: 4000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexConfig {
pub chunk_size: usize,
pub chunk_overlap: usize,
pub embeddings_batch_size: usize,
pub embeddings_max_tokens_per_batch: usize,
pub flush_frequency: usize,
pub require_git: bool,
pub quantization: bool,
#[serde(default)]
pub contextual_descriptions: bool,
pub contextual_model: String,
#[serde(default = "default_contextual_batch_size")]
pub contextual_batch_size: usize,
}
impl Default for IndexConfig {
fn default() -> Self {
Self {
chunk_size: 2000,
chunk_overlap: 100,
embeddings_batch_size: 16,
embeddings_max_tokens_per_batch: 100000,
flush_frequency: 2,
require_git: true,
quantization: true,
contextual_descriptions: false,
contextual_model: "openrouter:openai/gpt-4o-mini".to_string(),
contextual_batch_size: 10,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RerankerConfig {
pub enabled: bool,
pub model: String,
pub top_k_candidates: usize,
pub final_top_k: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HybridSearchConfig {
pub enabled: bool,
pub default_vector_weight: f32,
pub default_keyword_weight: f32,
pub keyword_path_weight: f32,
pub keyword_content_weight: f32,
pub keyword_symbols_weight: f32,
pub keyword_title_weight: f32,
}
impl Default for HybridSearchConfig {
fn default() -> Self {
panic!("Hybrid search config must be loaded from template file - defaults not allowed")
}
}
impl Default for RerankerConfig {
fn default() -> Self {
panic!("Reranker config must be loaded from template file - defaults not allowed")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchConfig {
pub max_results: usize,
pub similarity_threshold: f32,
pub output_format: String,
pub max_files: usize,
pub context_lines: usize,
pub search_block_max_characters: usize,
pub reranker: RerankerConfig,
pub hybrid: HybridSearchConfig,
}
impl Default for SearchConfig {
fn default() -> Self {
panic!("Search config must be loaded from template file - defaults not allowed")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CommitsConfig {
pub use_llm: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default = "default_version")]
pub version: u32,
#[serde(default)]
pub llm: LlmConfig,
#[serde(default)]
pub index: IndexConfig,
#[serde(default)]
pub search: SearchConfig,
#[serde(default)]
pub embedding: EmbeddingConfig,
#[serde(default)]
pub graphrag: GraphRAGConfig,
#[serde(default)]
pub commits: CommitsConfig,
}
fn default_version() -> u32 {
1
}
fn default_contextual_batch_size() -> usize {
10
}
impl Default for Config {
fn default() -> Self {
Self {
version: default_version(),
llm: LlmConfig::default(),
index: IndexConfig::default(),
search: SearchConfig::default(),
embedding: EmbeddingConfig::default(),
graphrag: GraphRAGConfig::default(),
commits: CommitsConfig::default(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = Self::get_system_config_path()?;
let config = if config_path.exists() {
let content = fs::read_to_string(&config_path)?;
toml::from_str(&content)?
} else {
let template_config = Self::load_from_template()?;
if let Some(parent) = config_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let toml_content = toml::to_string_pretty(&template_config)?;
fs::write(&config_path, toml_content)?;
template_config
};
Ok(config)
}
pub fn load_from_template() -> Result<Self> {
let template_content = Self::get_default_template_content()?;
let config: Config = toml::from_str(&template_content)?;
Ok(config)
}
fn get_default_template_content() -> Result<String> {
let template_path = std::path::Path::new("config-templates/default.toml");
if template_path.exists() {
return Ok(fs::read_to_string(template_path)?);
}
Ok(include_str!("../config-templates/default.toml").to_string())
}
pub fn save(&self) -> Result<()> {
let config_path = Self::get_system_config_path()?;
if let Some(parent) = config_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let toml_content = toml::to_string_pretty(self)?;
fs::write(config_path, toml_content)?;
Ok(())
}
pub fn get_system_config_path() -> Result<PathBuf> {
let system_storage = storage::get_system_storage_dir()?;
Ok(system_storage.join("config.toml"))
}
pub fn get_model(&self) -> &str {
&self.llm.model
}
pub fn get_timeout(&self) -> u64 {
self.llm.timeout
}
pub fn get_temperature(&self) -> f32 {
self.llm.temperature
}
pub fn get_max_tokens(&self) -> usize {
self.llm.max_tokens
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::load_from_template().expect("Failed to load template config");
assert_eq!(config.version, 1);
assert_eq!(config.llm.model, "openrouter:openai/gpt-4o-mini");
assert_eq!(config.index.chunk_size, 2000);
assert_eq!(config.search.max_results, 20);
assert_eq!(
config
.embedding
.get_active_provider()
.expect("embedding provider should be set"),
crate::embedding::types::EmbeddingProviderType::Voyage
);
assert!(!config.graphrag.enabled);
assert!(!config.graphrag.use_llm);
assert_eq!(
config.graphrag.llm.description_model,
"openrouter:openai/gpt-4o-mini"
);
assert_eq!(
config.graphrag.llm.relationship_model,
"openrouter:openai/gpt-4o-mini"
);
assert_eq!(config.graphrag.llm.ai_batch_size, 8);
assert_eq!(config.graphrag.llm.max_batch_tokens, 16384);
assert_eq!(config.graphrag.llm.batch_timeout_seconds, 60);
assert!(config.graphrag.llm.fallback_to_individual);
assert_eq!(config.graphrag.llm.max_sample_tokens, 1500);
assert_eq!(config.graphrag.llm.confidence_threshold, 0.6);
assert_eq!(config.graphrag.llm.architectural_weight, 0.9);
assert!(config
.graphrag
.llm
.relationship_system_prompt
.contains("expert software architect"));
assert!(config
.graphrag
.llm
.description_system_prompt
.contains("ROLE and PURPOSE"));
}
#[test]
fn test_template_loading() {
let result = Config::load_from_template();
assert!(result.is_ok(), "Should be able to load from template");
let config = result.expect("Template config should load successfully");
assert_eq!(config.version, 1);
assert_eq!(config.llm.model, "openrouter:openai/gpt-4o-mini");
assert_eq!(config.index.chunk_size, 2000);
assert_eq!(config.search.max_results, 20);
assert_eq!(config.embedding.code_model, "voyage:voyage-code-3");
assert_eq!(config.embedding.text_model, "voyage:voyage-3.5-lite");
assert!(!config.graphrag.enabled);
assert!(!config.graphrag.use_llm);
assert_eq!(
config.graphrag.llm.description_model,
"openrouter:openai/gpt-4o-mini"
);
assert_eq!(
config.graphrag.llm.relationship_model,
"openrouter:openai/gpt-4o-mini"
);
assert_eq!(config.graphrag.llm.ai_batch_size, 8);
assert_eq!(config.graphrag.llm.max_batch_tokens, 16384);
assert_eq!(config.graphrag.llm.batch_timeout_seconds, 60);
assert!(config.graphrag.llm.fallback_to_individual);
assert_eq!(config.graphrag.llm.max_sample_tokens, 1500);
assert_eq!(config.graphrag.llm.confidence_threshold, 0.6);
assert_eq!(config.graphrag.llm.architectural_weight, 0.9);
assert!(config
.graphrag
.llm
.relationship_system_prompt
.contains("expert software architect"));
assert!(config
.graphrag
.llm
.description_system_prompt
.contains("ROLE and PURPOSE"));
}
#[test]
#[should_panic(expected = "GraphRAG config must be loaded from template file")]
fn test_graphrag_default_panics() {
let _ = GraphRAGConfig::default();
}
}