P0 deep-dive: core, providers, agent, memory, TUI, setup wizard

DEEP DIVE — Agent Loop (gyandoot-agent):
- Real conversation loop with interactive (run()) and non-interactive
  (run_conversation()) modes
- Retry logic with exponential backoff and error classification
  (RateLimit, Auth, Timeout, ServerError, ContextOverflow)
- Context window management — token estimation, 75% threshold compression
- 7 built-in tools: Read, Write, Edit, Bash, Grep, Find, Ls
- BashTool with destructive-command security blocks
- ContextCompressor — prune tool results, summarize middle messages

DEEP DIVE — Memory (gyandoot-memory):
- Real MemPalace with ~/.gyandoot/palace/wings/<wing>/ directory tree
- Daily transcript files (YYYY-MM-DD_transcript.md) with append-only format
- L0/L1 identity persistence (< 157 tokens for L0)
- Session digest to drawer.md
- File persistence support for transcripts and identity store

DEEP DIVE — Core (gyandoot-core):
- GyandootConfig with TOML deserialization for all sections
- ConfigLoader with env > profile > file > default chain
- Rich types: Message, ToolCall, ToolResult, SessionId
- Comprehensive error types with thiserror derive

DEEP DIVE — Providers (gyandoot-providers):
- Provider trait with chat/chat_stream/models
- OpenAI provider with streaming SSE and function calling
- Anthropic provider with Messages API + tool use
- Ollama provider with merge_system/no_tools auto-detect
- SseParser, StreamEvent, TokenStream for SSE parsing
- ProviderFactory for creation from config

NEW — TUI crate (gyandoot-tui):
- Banner: Kapil-approved robot head ASCII art with ANSI colors
- Setup wizard: 7 providers, interactive config, connection test
- Terminal helpers: spinner, confirm, text input, selection menu
- Ratatui TUI: keyboard input, commands, status display

All 12 crates + CLI binary compile with 0 errors.
This commit is contained in:
2026-06-11 20:53:38 +05:30
parent 03c103061a
commit 54d8ca3774
29 changed files with 7211 additions and 755 deletions

View File

@@ -1,10 +1,248 @@
//! 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};
/// Top-level agent configuration.
// ─── 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,
}
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(),
}
}
}
/// `[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,
}
}
}
// ─── 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,
@@ -12,42 +250,80 @@ pub struct AgentConfig {
pub max_tokens: usize,
pub temperature: f64,
pub system_prompt: String,
pub provider: ProviderConfig,
pub platform: PlatformConfig,
pub memory: MemoryConfig,
pub security: SecurityConfig,
pub kanban: KanbanConfig,
pub skills: SkillsConfig,
pub provider: ProviderRootConfig,
pub platform: PlatformRootConfig,
pub memory: MemoryRootConfig,
pub security: SecurityRootConfig,
pub kanban: KanbanRootConfig,
pub skills: SkillsRootConfig,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
name: "gyandoot".to_string(),
model: "gpt-4o".to_string(),
max_tokens: 4096,
temperature: 0.7,
name: default_agent_name(),
model: default_agent_model(),
max_tokens: default_max_tokens(),
temperature: default_temperature(),
system_prompt: String::new(),
provider: ProviderConfig::default(),
platform: PlatformConfig::default(),
memory: MemoryConfig::default(),
security: SecurityConfig::default(),
kanban: KanbanConfig::default(),
skills: SkillsConfig::default(),
provider: ProviderRootConfig::default(),
platform: PlatformRootConfig::default(),
memory: MemoryRootConfig::default(),
security: SecurityRootConfig::default(),
kanban: KanbanRootConfig::default(),
skills: SkillsRootConfig::default(),
}
}
}
/// LLM provider configuration.
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(),
}
}
}
/// Provider configurations (backward-compatible).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub struct ProviderRootConfig {
pub default: String,
pub openai: Option<OpenAIConfig>,
pub anthropic: Option<AnthropicConfig>,
pub ollama: Option<OllamaConfig>,
}
impl Default for ProviderConfig {
impl Default for ProviderRootConfig {
fn default() -> Self {
Self {
default: "openai".to_string(),
@@ -58,6 +334,7 @@ impl Default for ProviderConfig {
}
}
/// Per-provider config struct used by gyandoot-agent's `create_provider`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIConfig {
pub api_key: String,
@@ -66,6 +343,21 @@ pub struct OpenAIConfig {
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 {
@@ -84,6 +376,20 @@ pub struct AnthropicConfig {
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 {
@@ -101,6 +407,20 @@ pub struct OllamaConfig {
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(),
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 {
@@ -111,16 +431,18 @@ impl Default for OllamaConfig {
}
}
/// Platform adapter configuration.
// Backward-compatible wrappers
pub type ProviderConfig = ProviderRootConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformConfig {
pub struct PlatformRootConfig {
pub default: String,
pub telegram: Option<TelegramConfig>,
pub discord: Option<DiscordConfig>,
pub webhook: Option<WebhookConfig>,
}
impl Default for PlatformConfig {
impl Default for PlatformRootConfig {
fn default() -> Self {
Self {
default: "cli".to_string(),
@@ -150,16 +472,15 @@ pub struct WebhookConfig {
pub secret: Option<String>,
}
/// Memory system configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
pub struct MemoryRootConfig {
pub backend: String,
pub storage_path: Option<PathBuf>,
pub max_entries: usize,
pub auto_compress: bool,
}
impl Default for MemoryConfig {
impl Default for MemoryRootConfig {
fn default() -> Self {
Self {
backend: "local".to_string(),
@@ -170,15 +491,14 @@ impl Default for MemoryConfig {
}
}
/// Security configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub struct SecurityRootConfig {
pub vault_path: Option<PathBuf>,
pub encryption_enabled: bool,
pub threat_detection: bool,
}
impl Default for SecurityConfig {
impl Default for SecurityRootConfig {
fn default() -> Self {
Self {
vault_path: None,
@@ -188,15 +508,14 @@ impl Default for SecurityConfig {
}
}
/// Kanban task system configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KanbanConfig {
pub struct KanbanRootConfig {
pub enabled: bool,
pub max_concurrent_tasks: usize,
pub auto_decompose: bool,
}
impl Default for KanbanConfig {
impl Default for KanbanRootConfig {
fn default() -> Self {
Self {
enabled: true,
@@ -206,15 +525,14 @@ impl Default for KanbanConfig {
}
}
/// Skills system configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillsConfig {
pub struct SkillsRootConfig {
pub enabled: bool,
pub skills_dir: Option<PathBuf>,
pub auto_load: bool,
}
impl Default for SkillsConfig {
impl Default for SkillsRootConfig {
fn default() -> Self {
Self {
enabled: true,
@@ -224,18 +542,218 @@ impl Default for SkillsConfig {
}
}
/// Load configuration from a TOML file.
pub fn load_config_from_file(path: &std::path::Path) -> anyhow::Result<AgentConfig> {
let content = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read config file {}: {}", path.display(), e))?;
let config: AgentConfig = toml::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse config file {}: {}", path.display(), e))?;
Ok(config)
// ─── 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) {
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() {
@@ -246,8 +764,97 @@ mod tests {
#[test]
fn test_provider_default() {
let provider = ProviderConfig::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);
}
}