Files
gyandoot-v2/crates/gyandoot-core/src/config.rs
Tara Menon 8c9a32075d Slash commands (/quit, /help) + zero compiler warnings
1. /quit and /help commands restored:
   - /quit: exits the interactive loop
   - /help: shows formatted help box with available commands & tips
   - Unknown commands get a friendly error pointing to /help
   - Slash commands intercepted before LLM processing (no token waste)

2. Zero warnings across entire workspace (13 files fixed):
   - Fixed ambiguous glob re-exports in gyandoot-core
   - Added #[allow(dead_code)] on intentional dead fields
   - Fixed unused imports, variables, and macros
   - Fixed unused assignments in subagents
   - cleaned up service.rs, profile, kanban warnings

install.sh now produces a completely clean build with zero warnings.
2026-06-12 12:15:01 +05:30

903 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Configuration types for GyanDoot agents and platform.
//!
//! Provides [`GyandootConfig`] as the canonical TOML-deserializable
//! configuration, [`ConfigLoader`] for layered resolution
//! (env vars > profile > default file > hardcoded defaults), and
//! backward-compatible aliases used by the rest of the workspace.
use std::collections::HashMap;
use std::path::PathBuf;
use crate::ConfigError;
use serde::{Deserialize, Serialize};
// ─── GyandootConfig (canonical TOML schema) ───────────────────────────────
/// Top-level TOML-deserializable configuration for GyanDoot.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GyandootConfig {
#[serde(default)]
pub agent: AgentSection,
#[serde(default)]
pub providers: ProvidersSection,
#[serde(default)]
pub platform: PlatformSection,
#[serde(default)]
pub memory: MemorySection,
#[serde(default)]
pub gateway: GatewaySection,
#[serde(default)]
pub browser: BrowserSection,
#[serde(default)]
pub identity: IdentitySection,
}
impl Default for GyandootConfig {
fn default() -> Self {
Self {
agent: AgentSection::default(),
providers: ProvidersSection::default(),
platform: PlatformSection::default(),
memory: MemorySection::default(),
gateway: GatewaySection::default(),
browser: BrowserSection::default(),
identity: IdentitySection::default(),
}
}
}
/// `[agent]` section.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSection {
/// Agent display name.
#[serde(default = "default_agent_name")]
pub name: String,
/// Default model identifier.
#[serde(default = "default_agent_model")]
pub model: String,
/// Maximum tokens per response.
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
/// Sampling temperature (0.0 – 2.0).
#[serde(default = "default_temperature")]
pub temperature: f64,
/// System prompt prepended to every conversation.
#[serde(default)]
pub system_prompt: String,
}
fn default_agent_name() -> String {
"gyandoot".to_string()
}
fn default_agent_model() -> String {
"gpt-4o".to_string()
}
fn default_max_tokens() -> usize {
4096
}
fn default_temperature() -> f64 {
0.7
}
impl Default for AgentSection {
fn default() -> Self {
Self {
name: default_agent_name(),
model: default_agent_model(),
max_tokens: default_max_tokens(),
temperature: default_temperature(),
system_prompt: String::new(),
}
}
}
/// `[providers]` section — maps provider name → per-provider config.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidersSection {
/// Default provider key (e.g. "openai").
#[serde(default = "default_provider_default")]
pub default: String,
/// Provider-specific settings keyed by name.
#[serde(default, flatten)]
pub providers: HashMap<String, toml::Value>,
}
fn default_provider_default() -> String {
"openai".to_string()
}
impl Default for ProvidersSection {
fn default() -> Self {
let mut providers = HashMap::new();
providers.insert(
"openai".to_string(),
toml::Value::Table({
let mut m = toml::map::Map::new();
m.insert("model".to_string(), toml::Value::String("gpt-4o".to_string()));
m
}),
);
Self {
default: default_provider_default(),
providers,
}
}
}
/// `[platform]` section.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformSection {
/// Default platform (e.g. "cli", "telegram", "discord").
#[serde(default = "default_platform_default")]
pub default: String,
/// Platform-specific config (flattened for extensibility).
#[serde(default, flatten)]
pub platforms: HashMap<String, toml::Value>,
}
fn default_platform_default() -> String {
"cli".to_string()
}
impl Default for PlatformSection {
fn default() -> Self {
Self {
default: default_platform_default(),
platforms: HashMap::new(),
}
}
}
/// `[memory]` section.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemorySection {
/// Memory backend (e.g. "local", "redis").
#[serde(default = "default_memory_backend")]
pub backend: String,
/// Storage path for local backend.
#[serde(default)]
pub path: Option<PathBuf>,
/// Maximum entries before compression / eviction.
#[serde(default = "default_memory_max_entries")]
pub max_entries: usize,
/// Whether auto-compression is enabled.
#[serde(default = "default_true")]
pub auto_compress: bool,
}
fn default_memory_backend() -> String {
"local".to_string()
}
fn default_memory_max_entries() -> usize {
10000
}
fn default_true() -> bool {
true
}
impl Default for MemorySection {
fn default() -> Self {
Self {
backend: default_memory_backend(),
path: None,
max_entries: default_memory_max_entries(),
auto_compress: true,
}
}
}
/// `[gateway]` section.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewaySection {
/// Whether the gateway server is enabled.
#[serde(default = "default_false")]
pub enabled: bool,
/// TCP port for the gateway.
#[serde(default = "default_gateway_port")]
pub port: u16,
/// Bind address / host.
#[serde(default = "default_gateway_host")]
pub host: String,
}
fn default_false() -> bool {
false
}
fn default_gateway_port() -> u16 {
8765
}
fn default_gateway_host() -> String {
"127.0.0.1".to_string()
}
impl Default for GatewaySection {
fn default() -> Self {
Self {
enabled: false,
port: default_gateway_port(),
host: default_gateway_host(),
}
}
}
/// `[browser]` section.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserSection {
/// Whether the browser automation layer is enabled.
#[serde(default = "default_false")]
pub enabled: bool,
/// Camofox URL for browser automation.
#[serde(default)]
pub camofox_url: Option<String>,
}
impl Default for BrowserSection {
fn default() -> Self {
Self {
enabled: false,
camofox_url: None,
}
}
}
/// `[identity]` section — agent and user identity for MemPalace onboarding.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentitySection {
/// Agent's name (e.g. "GyanDoot", "Maya").
#[serde(default)]
pub agent_name: String,
/// Agent's role / personality description.
#[serde(default)]
pub agent_role: String,
/// User's name (the human operating the agent).
#[serde(default)]
pub user_name: String,
/// User's role / relationship to the agent.
#[serde(default)]
pub user_role: String,
/// Whether identity onboarding has been completed.
#[serde(default)]
pub setup_complete: bool,
}
impl Default for IdentitySection {
fn default() -> Self {
Self {
agent_name: String::new(),
agent_role: String::new(),
user_name: String::new(),
user_role: String::new(),
setup_complete: false,
}
}
}
// ─── Legacy / backward-compatible config types ──────────────────────────
// These are re-exported under their original names so downstream crates
// (gyandoot-agent, gyandoot-providers, etc.) continue to compile unchanged.
/// Top-level agent configuration (backward-compatible).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub name: String,
pub model: String,
pub max_tokens: usize,
pub temperature: f64,
pub system_prompt: String,
pub provider: ProviderRootConfig,
pub platform: PlatformRootConfig,
pub memory: MemoryRootConfig,
pub security: SecurityRootConfig,
pub kanban: KanbanRootConfig,
pub skills: SkillsRootConfig,
pub identity: IdentitySection,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
name: default_agent_name(),
model: default_agent_model(),
max_tokens: default_max_tokens(),
temperature: default_temperature(),
system_prompt: String::new(),
provider: ProviderRootConfig::default(),
platform: PlatformRootConfig::default(),
memory: MemoryRootConfig::default(),
security: SecurityRootConfig::default(),
kanban: KanbanRootConfig::default(),
skills: SkillsRootConfig::default(),
identity: IdentitySection::default(),
}
}
}
impl From<GyandootConfig> for AgentConfig {
fn from(cfg: GyandootConfig) -> Self {
// Extract provider-specific configs from the providers map
let openai = cfg.providers.providers.get("openai").map(|v| OpenAIConfig::from_toml_value(v));
let anthropic = cfg.providers.providers.get("anthropic").map(|v| AnthropicConfig::from_toml_value(v));
let ollama = cfg.providers.providers.get("ollama").map(|v| OllamaConfig::from_toml_value(v));
Self {
name: cfg.agent.name,
model: cfg.agent.model,
max_tokens: cfg.agent.max_tokens,
temperature: cfg.agent.temperature,
system_prompt: cfg.agent.system_prompt,
provider: ProviderRootConfig {
default: cfg.providers.default,
openai,
anthropic,
ollama,
},
platform: PlatformRootConfig {
default: cfg.platform.default,
telegram: None,
discord: None,
webhook: None,
},
memory: MemoryRootConfig {
backend: cfg.memory.backend,
storage_path: cfg.memory.path,
max_entries: cfg.memory.max_entries,
auto_compress: cfg.memory.auto_compress,
},
security: SecurityRootConfig::default(),
kanban: KanbanRootConfig::default(),
skills: SkillsRootConfig::default(),
identity: cfg.identity,
}
}
}
/// Provider configurations (backward-compatible).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderRootConfig {
pub default: String,
pub openai: Option<OpenAIConfig>,
pub anthropic: Option<AnthropicConfig>,
pub ollama: Option<OllamaConfig>,
}
impl Default for ProviderRootConfig {
fn default() -> Self {
Self {
default: "openai".to_string(),
openai: Some(OpenAIConfig::default()),
anthropic: None,
ollama: None,
}
}
}
/// Per-provider config struct used by gyandoot-agent's `create_provider`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIConfig {
pub api_key: String,
pub model: String,
pub base_url: Option<String>,
pub max_retries: u32,
}
impl OpenAIConfig {
fn from_toml_value(v: &toml::Value) -> Self {
let table = match v.as_table() {
Some(t) => t,
None => return Self::default(),
};
Self {
api_key: table.get("api_key").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
model: table.get("model").and_then(|v| v.as_str()).unwrap_or_else(|| "gpt-4o").to_string(),
base_url: table.get("base_url").and_then(|v| v.as_str()).map(|s| s.to_string()),
max_retries: table.get("max_retries").and_then(|v| v.as_integer()).unwrap_or(3) as u32,
}
}
}
impl Default for OpenAIConfig {
fn default() -> Self {
Self {
api_key: String::new(),
model: "gpt-4o".to_string(),
base_url: None,
max_retries: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicConfig {
pub api_key: String,
pub model: String,
pub max_retries: u32,
}
impl AnthropicConfig {
fn from_toml_value(v: &toml::Value) -> Self {
let table = match v.as_table() {
Some(t) => t,
None => return Self::default(),
};
Self {
api_key: table.get("api_key").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
model: table.get("model").and_then(|v| v.as_str()).unwrap_or_else(|| "claude-3-5-sonnet-20241022").to_string(),
max_retries: table.get("max_retries").and_then(|v| v.as_integer()).unwrap_or(3) as u32,
}
}
}
impl Default for AnthropicConfig {
fn default() -> Self {
Self {
api_key: String::new(),
model: "claude-3-5-sonnet-20241022".to_string(),
max_retries: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OllamaConfig {
pub base_url: String,
pub model: String,
#[serde(default)]
pub api_key: Option<String>,
pub max_retries: u32,
}
impl OllamaConfig {
fn from_toml_value(v: &toml::Value) -> Self {
let table = match v.as_table() {
Some(t) => t,
None => return Self::default(),
};
Self {
base_url: table.get("base_url").and_then(|v| v.as_str()).unwrap_or("http://localhost:11434").to_string(),
model: table.get("model").and_then(|v| v.as_str()).unwrap_or("llama3").to_string(),
api_key: table.get("api_key").and_then(|v| v.as_str()).map(|s| s.to_string()),
max_retries: table.get("max_retries").and_then(|v| v.as_integer()).unwrap_or(2) as u32,
}
}
}
impl Default for OllamaConfig {
fn default() -> Self {
Self {
base_url: "http://localhost:11434".to_string(),
model: "llama3".to_string(),
api_key: None,
max_retries: 2,
}
}
}
// Backward-compatible wrappers
pub type ProviderConfig = ProviderRootConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformRootConfig {
pub default: String,
pub telegram: Option<TelegramConfig>,
pub discord: Option<DiscordConfig>,
pub webhook: Option<WebhookConfig>,
}
impl Default for PlatformRootConfig {
fn default() -> Self {
Self {
default: "cli".to_string(),
telegram: None,
discord: None,
webhook: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelegramConfig {
pub bot_token: String,
pub allowed_users: Vec<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscordConfig {
pub bot_token: String,
pub guild_id: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookConfig {
pub listen_host: String,
pub listen_port: u16,
pub secret: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRootConfig {
pub backend: String,
pub storage_path: Option<PathBuf>,
pub max_entries: usize,
pub auto_compress: bool,
}
impl Default for MemoryRootConfig {
fn default() -> Self {
Self {
backend: "local".to_string(),
storage_path: None,
max_entries: 10000,
auto_compress: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityRootConfig {
pub vault_path: Option<PathBuf>,
pub encryption_enabled: bool,
pub threat_detection: bool,
}
impl Default for SecurityRootConfig {
fn default() -> Self {
Self {
vault_path: None,
encryption_enabled: true,
threat_detection: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KanbanRootConfig {
pub enabled: bool,
pub max_concurrent_tasks: usize,
pub auto_decompose: bool,
}
impl Default for KanbanRootConfig {
fn default() -> Self {
Self {
enabled: true,
max_concurrent_tasks: 5,
auto_decompose: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillsRootConfig {
pub enabled: bool,
pub skills_dir: Option<PathBuf>,
pub auto_load: bool,
}
impl Default for SkillsRootConfig {
fn default() -> Self {
Self {
enabled: true,
skills_dir: None,
auto_load: true,
}
}
}
// ─── ConfigLoader ─────────────────────────────────────────────────────────
/// Layered configuration loader.
///
/// Resolution order (later overrides earlier):
/// 1. Hardcoded defaults
/// 2. Default file (`~/.config/gyandoot/config.toml`)
/// 3. Profile file (`~/.gyandoot/config.yaml` — legacy)
/// 4. Environment variables (`GYANDOOT_*`)
///
/// The final result is a [`GyandootConfig`] which can also be converted
/// to the legacy [`AgentConfig`] via `.into()`.
#[derive(Debug, Clone)]
pub struct ConfigLoader {
env_prefix: String,
}
impl Default for ConfigLoader {
fn default() -> Self {
Self::new()
}
}
impl ConfigLoader {
/// Create a new loader with the default `GYANDOOT_` prefix.
pub fn new() -> Self {
Self {
env_prefix: "GYANDOOT_".to_string(),
}
}
/// Create a loader with a custom environment-variable prefix.
pub fn with_prefix(prefix: impl Into<String>) -> Self {
Self {
env_prefix: prefix.into(),
}
}
/// Read and parse a single TOML config file.
pub fn from_file(path: &std::path::Path) -> Result<GyandootConfig, ConfigError> {
let content = std::fs::read_to_string(path)
.map_err(|e| ConfigError::FileNotFound {
path: path.to_path_buf(),
source: e,
})?;
toml::from_str(&content).map_err(|e| ConfigError::ParseError {
path: path.to_path_buf(),
detail: e.to_string(),
})
}
/// Override a config with environment variables.
///
/// Supported variables:
/// - `GYANDOOT_AGENT_NAME`, `GYANDOOT_AGENT_MODEL`,
/// `GYANDOOT_AGENT_MAX_TOKENS`, `GYANDOOT_AGENT_TEMPERATURE`,
/// `GYANDOOT_AGENT_SYSTEM_PROMPT`
/// - `GYANDOOT_PROVIDERS_DEFAULT`
/// - `GYANDOOT_PLATFORM_DEFAULT`
/// - `GYANDOOT_MEMORY_BACKEND`, `GYANDOOT_MEMORY_PATH`
/// - `GYANDOOT_GATEWAY_ENABLED`, `GYANDOOT_GATEWAY_PORT`, `GYANDOOT_GATEWAY_HOST`
/// - `GYANDOOT_BROWSER_ENABLED`, `GYANDOOT_BROWSER_CAMOFOX_URL`
/// - Provider-specific: `GYANDOOT_OPENAI_API_KEY`, `GYANDOOT_OPENAI_MODEL`,
/// `GYANDOOT_OPENAI_BASE_URL`, `GYANDOOT_ANTHROPIC_API_KEY`, etc.
pub fn from_env(&self, config: &mut GyandootConfig) {
#[allow(unused_macros)]
macro_rules! env_opt {
($key:literal, $target:expr) => {
if let Ok(val) = std::env::var(concat!(env_prefix_str!(), $key)) {
$target = val;
}
};
}
// We can't use concat! with a runtime string, so we build keys manually.
let prefix = &self.env_prefix;
let p = |k: &str| format!("{prefix}{k}");
// Agent
if let Ok(v) = std::env::var(p("AGENT_NAME")) {
config.agent.name = v;
}
if let Ok(v) = std::env::var(p("AGENT_MODEL")) {
config.agent.model = v;
}
if let Ok(v) = std::env::var(p("AGENT_MAX_TOKENS")) {
if let Ok(n) = v.parse::<usize>() {
config.agent.max_tokens = n;
}
}
if let Ok(v) = std::env::var(p("AGENT_TEMPERATURE")) {
if let Ok(n) = v.parse::<f64>() {
config.agent.temperature = n;
}
}
if let Ok(v) = std::env::var(p("AGENT_SYSTEM_PROMPT")) {
config.agent.system_prompt = v;
}
// Providers
if let Ok(v) = std::env::var(p("PROVIDERS_DEFAULT")) {
config.providers.default = v;
}
// Provider-specific API keys / settings stored in the flatten map
for (provider_key, env_suffix, toml_field) in [
("openai", "OPENAI_API_KEY", "api_key"),
("openai", "OPENAI_MODEL", "model"),
("openai", "OPENAI_BASE_URL", "base_url"),
("anthropic", "ANTHROPIC_API_KEY", "api_key"),
("anthropic", "ANTHROPIC_MODEL", "model"),
("ollama", "OLLAMA_BASE_URL", "base_url"),
("ollama", "OLLAMA_MODEL", "model"),
] {
if let Ok(val) = std::env::var(p(env_suffix)) {
let entry = config
.providers
.providers
.entry(provider_key.to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
if let toml::Value::Table(ref mut table) = entry {
table.insert(toml_field.to_string(), toml::Value::String(val));
}
}
}
// Platform
if let Ok(v) = std::env::var(p("PLATFORM_DEFAULT")) {
config.platform.default = v;
}
// Memory
if let Ok(v) = std::env::var(p("MEMORY_BACKEND")) {
config.memory.backend = v;
}
if let Ok(v) = std::env::var(p("MEMORY_PATH")) {
config.memory.path = Some(PathBuf::from(v));
}
// Gateway
if let Ok(v) = std::env::var(p("GATEWAY_ENABLED")) {
config.gateway.enabled = v == "true" || v == "1";
}
if let Ok(v) = std::env::var(p("GATEWAY_PORT")) {
if let Ok(n) = v.parse::<u16>() {
config.gateway.port = n;
}
}
if let Ok(v) = std::env::var(p("GATEWAY_HOST")) {
config.gateway.host = v;
}
// Browser
if let Ok(v) = std::env::var(p("BROWSER_ENABLED")) {
config.browser.enabled = v == "true" || v == "1";
}
if let Ok(v) = std::env::var(p("BROWSER_CAMOFOX_URL")) {
config.browser.camofox_url = Some(v);
}
}
/// Load configuration using the layered resolution strategy.
///
/// Order: hardcoded defaults → `~/.config/gyandoot/config.toml` →
/// `~/.gyandoot/config.toml` (legacy) → env vars.
pub fn load(&self) -> Result<GyandootConfig, ConfigError> {
let mut config = GyandootConfig::default();
// 1. Try XDG-style config file (primary)
if let Some(cfg_dir) = dirs::config_dir() {
let xdg_path = cfg_dir.join("gyandoot").join("config.toml");
if xdg_path.exists() {
if let Ok(loaded) = Self::from_file(&xdg_path) {
config = loaded;
}
}
}
// 2. Try legacy ~/.gyandoot/config.toml (profile directory)
if let Some(home) = dirs::home_dir() {
let legacy_path = home.join(".gyandoot").join("config.toml");
if legacy_path.exists() {
if let Ok(loaded) = Self::from_file(&legacy_path) {
// Legacy overrides XDG for backward compatibility
config = loaded;
}
}
}
// 3. Environment variable overrides
self.from_env(&mut config);
Ok(config)
}
/// Load and convert to the legacy [`AgentConfig`].
pub fn load_agent_config(&self) -> Result<AgentConfig, ConfigError> {
self.load().map(AgentConfig::from)
}
}
/// Convenience: load config from a single TOML file (legacy API).
pub fn load_config_from_file(path: &std::path::Path) -> anyhow::Result<AgentConfig> {
let _loader = ConfigLoader::new();
let gyandoot = ConfigLoader::from_file(path)?;
Ok(AgentConfig::from(gyandoot))
}
// ─── Tests ────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_default_config() {
let config = AgentConfig::default();
assert_eq!(config.name, "gyandoot");
assert_eq!(config.max_tokens, 4096);
}
#[test]
fn test_provider_default() {
let provider = ProviderRootConfig::default();
assert_eq!(provider.default, "openai");
assert!(provider.openai.is_some());
}
#[test]
fn test_gyandoot_config_defaults() {
let cfg = GyandootConfig::default();
assert_eq!(cfg.agent.name, "gyandoot");
assert_eq!(cfg.agent.max_tokens, 4096);
assert_eq!(cfg.gateway.port, 8765);
assert!(!cfg.browser.enabled);
}
#[test]
fn test_gyandoot_to_agent_config() {
let gyandoot = GyandootConfig::default();
let agent: AgentConfig = gyandoot.into();
assert_eq!(agent.name, "gyandoot");
}
#[test]
fn test_config_loader_from_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.toml");
let toml_content = r#"
[agent]
name = "test-agent"
model = "gpt-4o"
max_tokens = 2048
temperature = 0.5
[providers]
default = "ollama"
[memory]
backend = "redis"
path = "/tmp/gyandoot-memory"
[gateway]
enabled = true
port = 9999
host = "0.0.0.0"
[browser]
enabled = true
camofox_url = "http://camofox:9222"
"#;
std::fs::write(&path, toml_content).unwrap();
let config = ConfigLoader::from_file(&path).unwrap();
assert_eq!(config.agent.name, "test-agent");
assert_eq!(config.agent.max_tokens, 2048);
assert_eq!(config.agent.temperature, 0.5);
assert_eq!(config.providers.default, "ollama");
assert_eq!(config.memory.backend, "redis");
assert!(config.gateway.enabled);
assert_eq!(config.gateway.port, 9999);
assert!(config.browser.enabled);
assert_eq!(
config.browser.camofox_url.as_deref(),
Some("http://camofox:9222")
);
}
#[test]
fn test_config_loader_file_not_found() {
let err = ConfigLoader::from_file(std::path::Path::new("/nonexistent/gyandoot.toml"));
assert!(err.is_err());
}
#[test]
fn test_config_loader_invalid_toml() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bad.toml");
std::fs::write(&path, "[[[invalid]]]\n").unwrap();
let err = ConfigLoader::from_file(&path);
assert!(err.is_err());
match err {
Err(ConfigError::ParseError { .. }) => {} // expected
_ => panic!("Expected ParseError"),
}
}
#[test]
fn test_agent_config_from_gyandoot_roundtrip() {
let gyandoot = GyandootConfig::default();
let agent: AgentConfig = gyandoot.clone().into();
// Verify the conversion preserves defaults
assert_eq!(agent.name, gyandoot.agent.name);
assert_eq!(agent.model, gyandoot.agent.model);
assert_eq!(agent.max_tokens, gyandoot.agent.max_tokens);
assert_eq!(agent.temperature, gyandoot.agent.temperature);
}
}