use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::{audio::TTSConfig, OxydeError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentPersonality {
pub name: String,
pub role: String,
pub backstory: Vec<String>,
pub knowledge: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EmbeddingModelType {
MiniBert,
DistilBert,
Custom,
}
impl Default for EmbeddingModelType {
fn default() -> Self {
Self::MiniBert
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
#[serde(default = "default_memory_capacity")]
pub capacity: usize,
#[serde(default)]
pub persistence: bool,
#[serde(default = "default_memory_decay")]
pub decay_rate: f64,
#[serde(default = "default_memory_threshold")]
pub importance_threshold: f64,
#[serde(default = "default_short_term_capacity")]
pub short_term_capacity: usize,
#[serde(default)]
pub use_embeddings: bool,
#[serde(default)]
pub embedding_model: EmbeddingModelType,
pub custom_model_path: Option<String>,
#[serde(default = "default_embedding_dim")]
pub embedding_dimension: usize,
#[serde(default)]
pub priority_categories: Vec<String>,
}
fn default_memory_capacity() -> usize {
100
}
fn default_memory_decay() -> f64 {
0.05
}
fn default_memory_threshold() -> f64 {
0.2
}
fn default_short_term_capacity() -> usize {
10
}
fn default_embedding_dim() -> usize {
384 }
impl Default for MemoryConfig {
fn default() -> Self {
Self {
capacity: default_memory_capacity(),
persistence: false,
decay_rate: default_memory_decay(),
importance_threshold: default_memory_threshold(),
short_term_capacity: default_short_term_capacity(),
use_embeddings: false,
embedding_model: EmbeddingModelType::default(),
custom_model_path: None,
embedding_dimension: default_embedding_dim(),
priority_categories: Vec::new(),
}
}
}
impl MemoryConfig {
pub fn validate(&self) -> Result<()> {
if self.capacity == 0 {
return Err(OxydeError::ConfigurationError(
"Memory capacity must be greater than 0".to_string()
));
}
if self.short_term_capacity == 0 {
return Err(OxydeError::ConfigurationError(
"Short-term memory capacity must be greater than 0".to_string()
));
}
if self.short_term_capacity > self.capacity {
return Err(OxydeError::ConfigurationError(
format!(
"Short-term capacity ({}) cannot exceed total capacity ({})",
self.short_term_capacity, self.capacity
)
));
}
if !(0.0..=1.0).contains(&self.decay_rate) {
return Err(OxydeError::ConfigurationError(
format!(
"Decay rate must be between 0.0 and 1.0, got {}",
self.decay_rate
)
));
}
if !(0.0..=1.0).contains(&self.importance_threshold) {
return Err(OxydeError::ConfigurationError(
format!(
"Importance threshold must be between 0.0 and 1.0, got {}",
self.importance_threshold
)
));
}
if self.use_embeddings && self.embedding_dimension == 0 {
return Err(OxydeError::ConfigurationError(
"Embedding dimension must be greater than 0 when embeddings are enabled".to_string()
));
}
if self.embedding_model == EmbeddingModelType::Custom {
if self.custom_model_path.is_none() {
return Err(OxydeError::ConfigurationError(
"Custom model path must be provided when using custom embedding model".to_string()
));
}
if let Some(ref path) = self.custom_model_path {
if path.is_empty() {
return Err(OxydeError::ConfigurationError(
"Custom model path cannot be empty".to_string()
));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceConfig {
#[serde(default = "default_model")]
pub model: String,
#[serde(default)]
pub use_local: bool,
pub local_model_path: Option<String>,
pub api_endpoint: Option<String>,
pub api_key: Option<String>,
#[serde(default = "default_temperature")]
pub temperature: f32,
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
#[serde(default = "default_timeout")]
pub timeout_ms: u64,
pub fallback_api: Option<String>,
}
fn default_model() -> String {
"llama2-7b".to_string()
}
fn default_temperature() -> f32 {
0.7
}
fn default_max_tokens() -> usize {
256
}
fn default_timeout() -> u64 {
5000
}
impl Default for InferenceConfig {
fn default() -> Self {
Self {
model: default_model(),
use_local: false,
local_model_path: None,
api_endpoint: Some("https://api.openai.com/v1/chat/completions".to_string()),
api_key: None,
temperature: default_temperature(),
max_tokens: default_max_tokens(),
timeout_ms: default_timeout(),
fallback_api: None,
}
}
}
impl InferenceConfig {
pub fn validate(&self) -> Result<()> {
if !(0.0..=2.0).contains(&self.temperature) {
return Err(OxydeError::ConfigurationError(
format!(
"Temperature must be between 0.0 and 2.0, got {}",
self.temperature
)
));
}
if self.max_tokens == 0 {
return Err(OxydeError::ConfigurationError(
"Max tokens must be greater than 0".to_string()
));
}
if self.max_tokens > 100000 {
return Err(OxydeError::ConfigurationError(
format!(
"Max tokens ({}) exceeds reasonable limit (100000)",
self.max_tokens
)
));
}
if self.timeout_ms == 0 {
return Err(OxydeError::ConfigurationError(
"Timeout must be greater than 0ms".to_string()
));
}
if self.timeout_ms > 300000 {
return Err(OxydeError::ConfigurationError(
format!(
"Timeout ({}ms) exceeds maximum allowed (300000ms / 5 minutes)",
self.timeout_ms
)
));
}
if self.use_local {
if self.local_model_path.is_none() {
return Err(OxydeError::ConfigurationError(
"Local model path must be provided when use_local is true".to_string()
));
}
if let Some(ref path) = self.local_model_path {
if path.is_empty() {
return Err(OxydeError::ConfigurationError(
"Local model path cannot be empty".to_string()
));
}
}
}
if !self.use_local {
if self.api_endpoint.is_none() {
return Err(OxydeError::ConfigurationError(
"API endpoint must be provided when using cloud inference".to_string()
));
}
if let Some(ref endpoint) = self.api_endpoint {
if endpoint.is_empty() {
return Err(OxydeError::ConfigurationError(
"API endpoint cannot be empty".to_string()
));
}
if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
return Err(OxydeError::ConfigurationError(
format!(
"API endpoint must be a valid HTTP(S) URL, got: {}",
endpoint
)
));
}
}
}
if self.model.is_empty() {
return Err(OxydeError::ConfigurationError(
"Model name cannot be empty".to_string()
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorConfig {
pub trigger: String,
#[serde(default)]
pub cooldown: u64,
#[serde(default)]
pub priority: u32,
#[serde(flatten)]
pub parameters: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModerationConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_moderation_response")]
pub response_message: String,
#[serde(default)]
pub use_cloud_moderation: bool,
pub cloud_moderation_api_key: Option<String>,
}
fn default_moderation_response() -> String {
"Sorry, I can't respond to that.".to_string()
}
impl Default for ModerationConfig {
fn default() -> Self {
Self {
enabled: false,
response_message: default_moderation_response(),
use_cloud_moderation: false,
cloud_moderation_api_key: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub agent: AgentPersonality,
#[serde(default)]
pub memory: MemoryConfig,
#[serde(default)]
pub inference: InferenceConfig,
#[serde(default)]
pub behavior: HashMap<String, BehaviorConfig>,
#[serde(default)]
pub moderation: ModerationConfig,
pub tts: Option<TTSConfig>,
}
impl AgentConfig {
pub fn validate(&self) -> Result<()> {
if self.agent.name.is_empty() {
return Err(OxydeError::ConfigurationError(
"Agent name cannot be empty".to_string()
));
}
if self.agent.role.is_empty() {
return Err(OxydeError::ConfigurationError(
"Agent role cannot be empty".to_string()
));
}
self.memory.validate()?;
self.inference.validate()?;
for (name, behavior_config) in &self.behavior {
if name.is_empty() {
return Err(OxydeError::ConfigurationError(
"Behavior name cannot be empty".to_string()
));
}
if behavior_config.trigger.is_empty() {
return Err(OxydeError::ConfigurationError(
format!("Behavior '{}' must have a non-empty trigger", name)
));
}
}
Ok(())
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path.as_ref()).map_err(|e| {
OxydeError::ConfigurationError(format!("Failed to open config file: {}", e))
})?;
let reader = BufReader::new(file);
let extension = path.as_ref().extension().and_then(|ext| ext.to_str());
let config: AgentConfig = match extension {
Some("json") => {
serde_json::from_reader(reader).map_err(|e| {
OxydeError::ConfigurationError(format!("Failed to parse JSON config: {}", e))
})?
},
Some("yaml") | Some("yml") => {
serde_yaml::from_reader(reader).map_err(|e| {
OxydeError::ConfigurationError(format!("Failed to parse YAML config: {}", e))
})?
},
_ => {
return Err(OxydeError::ConfigurationError(
"Unknown config file format. Expected .json, .yaml, or .yml".to_string()
));
}
};
config.validate()?;
Ok(config)
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let file = File::create(path.as_ref()).map_err(|e| {
OxydeError::ConfigurationError(format!("Failed to create config file: {}", e))
})?;
let extension = path.as_ref().extension().and_then(|ext| ext.to_str());
match extension {
Some("json") => serde_json::to_writer_pretty(file, self).map_err(|e| {
OxydeError::ConfigurationError(format!("Failed to write JSON config: {}", e))
}),
Some("yaml") | Some("yml") => serde_yaml::to_writer(file, self).map_err(|e| {
OxydeError::ConfigurationError(format!("Failed to write YAML config: {}", e))
}),
_ => Err(OxydeError::ConfigurationError(
"Unknown config file format. Expected .json, .yaml, or .yml".to_string(),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_configs() {
let memory_config = MemoryConfig::default();
assert_eq!(memory_config.capacity, 100);
assert_eq!(memory_config.persistence, false);
assert_eq!(memory_config.decay_rate, 0.05);
let inference_config = InferenceConfig::default();
assert_eq!(inference_config.model, "llama2-7b");
assert_eq!(inference_config.temperature, 0.7);
assert_eq!(inference_config.max_tokens, 256);
}
#[test]
fn test_serialization() {
let config = AgentConfig {
agent: AgentPersonality {
name: "Test Agent".to_string(),
role: "Tester".to_string(),
backstory: vec!["A test agent".to_string()],
knowledge: vec!["Testing knowledge".to_string()],
},
memory: MemoryConfig::default(),
inference: InferenceConfig::default(),
behavior: HashMap::new(),
moderation: ModerationConfig::default(),
tts: None
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: AgentConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.agent.name, "Test Agent");
assert_eq!(deserialized.agent.role, "Tester");
}
#[test]
fn test_memory_config_validation_success() {
let config = MemoryConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_memory_config_validation_zero_capacity() {
let mut config = MemoryConfig::default();
config.capacity = 0;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("capacity must be greater than 0"));
}
#[test]
fn test_memory_config_validation_short_term_exceeds_capacity() {
let mut config = MemoryConfig::default();
config.capacity = 50;
config.short_term_capacity = 100;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot exceed total capacity"));
}
#[test]
fn test_memory_config_validation_invalid_decay_rate() {
let mut config = MemoryConfig::default();
config.decay_rate = 1.5;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Decay rate must be between 0.0 and 1.0"));
}
#[test]
fn test_memory_config_validation_invalid_importance_threshold() {
let mut config = MemoryConfig::default();
config.importance_threshold = -0.1;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Importance threshold must be between 0.0 and 1.0"));
}
#[test]
fn test_memory_config_validation_custom_model_without_path() {
let mut config = MemoryConfig::default();
config.embedding_model = EmbeddingModelType::Custom;
config.custom_model_path = None;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Custom model path must be provided"));
}
#[test]
fn test_inference_config_validation_success() {
let config = InferenceConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_inference_config_validation_invalid_temperature() {
let mut config = InferenceConfig::default();
config.temperature = 3.0;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Temperature must be between 0.0 and 2.0"));
}
#[test]
fn test_inference_config_validation_zero_max_tokens() {
let mut config = InferenceConfig::default();
config.max_tokens = 0;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Max tokens must be greater than 0"));
}
#[test]
fn test_inference_config_validation_excessive_max_tokens() {
let mut config = InferenceConfig::default();
config.max_tokens = 200000;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("exceeds reasonable limit"));
}
#[test]
fn test_inference_config_validation_zero_timeout() {
let mut config = InferenceConfig::default();
config.timeout_ms = 0;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Timeout must be greater than 0ms"));
}
#[test]
fn test_inference_config_validation_local_without_path() {
let mut config = InferenceConfig::default();
config.use_local = true;
config.local_model_path = None;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Local model path must be provided"));
}
#[test]
fn test_inference_config_validation_cloud_without_endpoint() {
let mut config = InferenceConfig::default();
config.use_local = false;
config.api_endpoint = None;
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("API endpoint must be provided"));
}
#[test]
fn test_inference_config_validation_invalid_url() {
let mut config = InferenceConfig::default();
config.api_endpoint = Some("not-a-valid-url".to_string());
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("must be a valid HTTP(S) URL"));
}
#[test]
fn test_agent_config_validation_success() {
let config = AgentConfig {
agent: AgentPersonality {
name: "Test".to_string(),
role: "Tester".to_string(),
backstory: vec![],
knowledge: vec![],
},
memory: MemoryConfig::default(),
inference: InferenceConfig::default(),
behavior: HashMap::new(),
moderation: ModerationConfig::default(),
tts: None
};
assert!(config.validate().is_ok());
}
#[test]
fn test_agent_config_validation_empty_name() {
let config = AgentConfig {
agent: AgentPersonality {
name: "".to_string(),
role: "Tester".to_string(),
backstory: vec![],
knowledge: vec![],
},
memory: MemoryConfig::default(),
inference: InferenceConfig::default(),
behavior: HashMap::new(),
moderation: ModerationConfig::default(),
tts: None
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Agent name cannot be empty"));
}
#[test]
fn test_agent_config_validation_empty_role() {
let config = AgentConfig {
agent: AgentPersonality {
name: "Test".to_string(),
role: "".to_string(),
backstory: vec![],
knowledge: vec![],
},
memory: MemoryConfig::default(),
inference: InferenceConfig::default(),
behavior: HashMap::new(),
moderation: ModerationConfig::default(),
tts: None
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Agent role cannot be empty"));
}
#[test]
fn test_agent_config_validation_cascades_to_memory() {
let config = AgentConfig {
agent: AgentPersonality {
name: "Test".to_string(),
role: "Tester".to_string(),
backstory: vec![],
knowledge: vec![],
},
memory: MemoryConfig {
capacity: 0, ..Default::default()
},
inference: InferenceConfig::default(),
behavior: HashMap::new(),
moderation: ModerationConfig::default(),
tts: None
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("capacity"));
}
#[test]
fn test_agent_config_validation_cascades_to_inference() {
let config = AgentConfig {
agent: AgentPersonality {
name: "Test".to_string(),
role: "Tester".to_string(),
backstory: vec![],
knowledge: vec![],
},
memory: MemoryConfig::default(),
inference: InferenceConfig {
temperature: 5.0, ..Default::default()
},
behavior: HashMap::new(),
moderation: ModerationConfig::default(),
tts: None
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Temperature"));
}
}