use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::{BattalionConfig, BattalionError};
use crate::platform::container::execution_result::PaladinResult;
use crate::platform::container::paladin::Paladin;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ObservabilityLevel {
Minimal,
#[default]
Standard,
Verbose,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConclaveConfig {
pub name: String,
pub battalion_config: BattalionConfig,
pub timeout_seconds: u64,
pub retry_attempts: u32,
pub synthesis_prompt: Option<String>,
pub include_expert_names: bool,
pub max_expert_output_tokens: Option<usize>,
pub observability_level: ObservabilityLevel,
}
impl ConclaveConfig {
pub fn new(name: impl Into<String>, battalion_config: BattalionConfig) -> Self {
Self {
name: name.into(),
battalion_config,
timeout_seconds: 300,
retry_attempts: 2,
synthesis_prompt: None,
include_expert_names: true,
max_expert_output_tokens: None,
observability_level: ObservabilityLevel::default(),
}
}
pub fn with_timeout(mut self, seconds: u64) -> Self {
self.timeout_seconds = seconds;
self
}
pub fn with_retry_attempts(mut self, attempts: u32) -> Self {
self.retry_attempts = attempts.min(5);
self
}
pub fn with_synthesis_prompt(mut self, prompt: impl Into<String>) -> Self {
self.synthesis_prompt = Some(prompt.into());
self
}
pub fn with_expert_names(mut self, include: bool) -> Self {
self.include_expert_names = include;
self
}
pub fn with_max_expert_tokens(mut self, max_tokens: usize) -> Self {
self.max_expert_output_tokens = Some(max_tokens);
self
}
pub fn with_observability(mut self, level: ObservabilityLevel) -> Self {
self.observability_level = level;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conclave {
pub experts: Vec<Paladin>,
pub aggregator: Paladin,
pub config: ConclaveConfig,
}
impl Conclave {
pub fn new(
experts: Vec<Paladin>,
aggregator: Paladin,
config: ConclaveConfig,
) -> Result<Self, BattalionError> {
let conclave = Self {
experts,
aggregator,
config,
};
conclave.validate()?;
Ok(conclave)
}
pub fn validate(&self) -> Result<(), BattalionError> {
if self.experts.len() < 2 {
return Err(BattalionError::ValidationError(format!(
"Conclave requires at least 2 experts, found {}",
self.experts.len()
)));
}
let mut expert_names = std::collections::HashSet::new();
for expert in &self.experts {
let name = &expert.node.name;
if !expert_names.insert(name.clone()) {
return Err(BattalionError::ValidationError(format!(
"Duplicate expert name: '{}'",
name
)));
}
}
let aggregator_name = &self.aggregator.node.name;
if expert_names.contains(aggregator_name) {
return Err(BattalionError::ValidationError(format!(
"Aggregator name '{}' conflicts with an expert name",
aggregator_name
)));
}
if self.config.retry_attempts > 5 {
return Err(BattalionError::ValidationError(format!(
"Retry attempts must be 0-5, found {}",
self.config.retry_attempts
)));
}
if self.config.timeout_seconds < 10 {
return Err(BattalionError::ValidationError(
"Timeout must be at least 10 seconds".to_string(),
));
}
if self.config.timeout_seconds > 3600 {
return Err(BattalionError::ValidationError(
"Timeout cannot exceed 3600 seconds (1 hour)".to_string(),
));
}
Ok(())
}
pub fn name(&self) -> &str {
&self.config.name
}
pub fn expert_count(&self) -> usize {
self.experts.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConclaveStatus {
Success,
PartialSuccess,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConclaveResult {
pub expert_outputs: HashMap<String, PaladinResult>,
pub aggregated_output: PaladinResult,
pub execution_time_ms: u64,
pub expert_execution_times: HashMap<String, u64>,
pub retry_counts: HashMap<String, u32>,
pub status: ConclaveStatus,
}
impl ConclaveResult {
pub fn successful_expert_count(&self) -> usize {
self.expert_outputs.len()
}
pub fn all_experts_succeeded(&self) -> bool {
self.status == ConclaveStatus::Success
}
pub fn is_completed(&self) -> bool {
matches!(
self.status,
ConclaveStatus::Success | ConclaveStatus::PartialSuccess
)
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum ConclaveError {
#[error("All experts failed after retries")]
AllExpertsFailed,
#[error("Aggregator failed: {0}")]
AggregatorFailed(String),
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Execution timeout after {0} seconds")]
Timeout(u64),
#[error("Expert '{0}' failed: {1}")]
ExpertError(String, String),
}
impl From<ConclaveError> for BattalionError {
fn from(error: ConclaveError) -> Self {
match error {
ConclaveError::AllExpertsFailed => {
BattalionError::ValidationError("All experts failed".to_string())
}
ConclaveError::AggregatorFailed(msg) => BattalionError::AggregationError(msg),
ConclaveError::ConfigurationError(msg) => BattalionError::ConfigurationError(msg),
ConclaveError::Timeout(seconds) => BattalionError::Timeout(seconds),
ConclaveError::ExpertError(name, msg) => {
BattalionError::PaladinError(format!("Expert '{}': {}", name, msg))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::entity::node::Node;
use crate::platform::container::paladin::{MaxLoops, PaladinData, PaladinStatus};
fn create_test_paladin(name: &str) -> Paladin {
let paladin_data = PaladinData {
system_prompt: format!("You are {}", name),
name: name.to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4o".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
Node::new(paladin_data, Some(name.to_string()))
}
fn create_test_config() -> ConclaveConfig {
let battalion_config = BattalionConfig::new("test_conclave");
ConclaveConfig::new("TestConclave", battalion_config)
}
#[test]
fn test_conclave_creation_success() {
let experts = vec![
create_test_paladin("Expert1"),
create_test_paladin("Expert2"),
create_test_paladin("Expert3"),
];
let aggregator = create_test_paladin("Aggregator");
let config = create_test_config();
let result = Conclave::new(experts, aggregator, config);
assert!(result.is_ok());
let conclave = result.unwrap();
assert_eq!(conclave.expert_count(), 3);
assert_eq!(conclave.name(), "TestConclave");
}
#[test]
fn test_conclave_insufficient_experts() {
let experts = vec![create_test_paladin("Expert1")];
let aggregator = create_test_paladin("Aggregator");
let config = create_test_config();
let result = Conclave::new(experts, aggregator, config);
assert!(result.is_err());
if let Err(BattalionError::ValidationError(msg)) = result {
assert!(msg.contains("at least 2 experts"));
} else {
panic!("Expected ValidationError");
}
}
#[test]
fn test_conclave_duplicate_expert_names() {
let experts = vec![
create_test_paladin("Expert1"),
create_test_paladin("Expert1"), create_test_paladin("Expert3"),
];
let aggregator = create_test_paladin("Aggregator");
let config = create_test_config();
let result = Conclave::new(experts, aggregator, config);
assert!(result.is_err());
if let Err(BattalionError::ValidationError(msg)) = result {
assert!(msg.contains("Duplicate expert name"));
} else {
panic!("Expected ValidationError");
}
}
#[test]
fn test_conclave_aggregator_name_conflicts() {
let experts = vec![
create_test_paladin("Expert1"),
create_test_paladin("Expert2"),
];
let aggregator = create_test_paladin("Expert1"); let config = create_test_config();
let result = Conclave::new(experts, aggregator, config);
assert!(result.is_err());
if let Err(BattalionError::ValidationError(msg)) = result {
assert!(msg.contains("conflicts with an expert name"));
} else {
panic!("Expected ValidationError");
}
}
#[test]
fn test_conclave_config_builder() {
let battalion_config = BattalionConfig::new("test");
let config = ConclaveConfig::new("TestConclave", battalion_config)
.with_timeout(600)
.with_retry_attempts(3)
.with_expert_names(false)
.with_observability(ObservabilityLevel::Verbose)
.with_synthesis_prompt("Custom prompt");
assert_eq!(config.timeout_seconds, 600);
assert_eq!(config.retry_attempts, 3);
assert!(!config.include_expert_names);
assert_eq!(config.observability_level, ObservabilityLevel::Verbose);
assert!(config.synthesis_prompt.is_some());
}
#[test]
fn test_conclave_config_retry_attempts_clamping() {
let battalion_config = BattalionConfig::new("test");
let config = ConclaveConfig::new("TestConclave", battalion_config).with_retry_attempts(10);
assert_eq!(config.retry_attempts, 5); }
#[test]
fn test_conclave_timeout_validation() {
let experts = vec![
create_test_paladin("Expert1"),
create_test_paladin("Expert2"),
];
let aggregator = create_test_paladin("Aggregator");
let battalion_config = BattalionConfig::new("test");
let mut config = ConclaveConfig::new("TestConclave", battalion_config);
config.timeout_seconds = 5;
let result = Conclave::new(experts.clone(), aggregator.clone(), config);
assert!(result.is_err());
let battalion_config = BattalionConfig::new("test");
let mut config = ConclaveConfig::new("TestConclave", battalion_config);
config.timeout_seconds = 4000;
let result = Conclave::new(experts, aggregator, config);
assert!(result.is_err());
}
#[test]
fn test_observability_level_default() {
assert_eq!(ObservabilityLevel::default(), ObservabilityLevel::Standard);
}
#[test]
fn test_conclave_status_equality() {
assert_eq!(ConclaveStatus::Success, ConclaveStatus::Success);
assert_ne!(ConclaveStatus::Success, ConclaveStatus::Failed);
assert_ne!(ConclaveStatus::Success, ConclaveStatus::PartialSuccess);
}
#[test]
fn test_conclave_error_conversion() {
let conclave_error = ConclaveError::AllExpertsFailed;
let battalion_error: BattalionError = conclave_error.into();
match battalion_error {
BattalionError::ValidationError(msg) => {
assert!(msg.contains("All experts failed"));
}
_ => panic!("Expected ValidationError"),
}
}
}