use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::{BattalionError, CouncilError};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TurnStrategy {
RoundRobin,
ModeratorDirected,
Random,
VoluntaryWithTimeout {
timeout_ms: u64,
},
}
impl Default for TurnStrategy {
fn default() -> Self {
Self::RoundRobin
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TerminationCondition {
MaxRounds,
Consensus,
ModeratorDecision,
Keyword(String),
}
impl Default for TerminationCondition {
fn default() -> Self {
Self::MaxRounds
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CouncilMessage {
pub speaker: String,
pub content: String,
pub round: u32,
pub timestamp: DateTime<Utc>,
}
impl CouncilMessage {
pub fn new(speaker: impl Into<String>, content: impl Into<String>, round: u32) -> Self {
Self {
speaker: speaker.into(),
content: content.into(),
round,
timestamp: Utc::now(),
}
}
pub fn format(&self) -> String {
format!("[Round {}] {}: {}", self.round, self.speaker, self.content)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CouncilConfig {
pub max_rounds: u32,
pub turn_strategy: TurnStrategy,
pub termination_condition: TerminationCondition,
pub include_history: bool,
}
impl Default for CouncilConfig {
fn default() -> Self {
Self {
max_rounds: 10,
turn_strategy: TurnStrategy::default(),
termination_condition: TerminationCondition::default(),
include_history: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CouncilData {
pub name: String,
pub participant_ids: Vec<String>,
pub moderator_id: Option<String>,
pub config: CouncilConfig,
}
pub type Council = crate::base::entity::node::Node<CouncilData>;
#[derive(Debug)]
pub struct CouncilBuilder {
name: Option<String>,
participant_ids: Vec<String>,
moderator_id: Option<String>,
config: CouncilConfig,
}
impl CouncilBuilder {
pub fn new() -> Self {
Self {
name: None,
participant_ids: Vec::new(),
moderator_id: None,
config: CouncilConfig::default(),
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn add_participant(mut self, paladin_id: impl Into<String>) -> Self {
self.participant_ids.push(paladin_id.into());
self
}
pub fn moderator(mut self, paladin_id: impl Into<String>) -> Self {
self.moderator_id = Some(paladin_id.into());
self
}
pub fn config(mut self, config: CouncilConfig) -> Self {
self.config = config;
self
}
pub fn max_rounds(mut self, rounds: u32) -> Self {
self.config.max_rounds = rounds;
self
}
pub fn turn_strategy(mut self, strategy: TurnStrategy) -> Self {
self.config.turn_strategy = strategy;
self
}
pub fn termination_condition(mut self, condition: TerminationCondition) -> Self {
self.config.termination_condition = condition;
self
}
pub fn include_history(mut self, include: bool) -> Self {
self.config.include_history = include;
self
}
pub fn build(self) -> Result<Council, BattalionError> {
let name = self.name.ok_or_else(|| {
BattalionError::ValidationError("Council name is required".to_string())
})?;
if self.participant_ids.is_empty() {
return Err(CouncilError::NoParticipants.into());
}
if self.participant_ids.len() < 2 {
return Err(BattalionError::ValidationError(
"Council requires at least 2 participants for meaningful discussion".to_string(),
));
}
if matches!(self.config.turn_strategy, TurnStrategy::ModeratorDirected)
&& self.moderator_id.is_none()
{
return Err(CouncilError::ModeratorRequired.into());
}
if self.config.max_rounds == 0 {
return Err(CouncilError::InvalidMaxRounds.into());
}
let data = CouncilData {
name: name.clone(),
participant_ids: self.participant_ids,
moderator_id: self.moderator_id,
config: self.config,
};
Ok(crate::base::entity::node::Node::new(data, Some(name)))
}
}
impl Default for CouncilBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_turn_strategy_default() {
let strategy = TurnStrategy::default();
assert_eq!(strategy, TurnStrategy::RoundRobin);
}
#[test]
fn test_termination_condition_default() {
let condition = TerminationCondition::default();
assert_eq!(condition, TerminationCondition::MaxRounds);
}
#[test]
fn test_council_config_default() {
let config = CouncilConfig::default();
assert_eq!(config.max_rounds, 10);
assert_eq!(config.turn_strategy, TurnStrategy::RoundRobin);
assert_eq!(
config.termination_condition,
TerminationCondition::MaxRounds
);
assert!(config.include_history);
}
#[test]
fn test_council_message_creation() {
let message = CouncilMessage::new("expert_1", "Hello", 1);
assert_eq!(message.speaker, "expert_1");
assert_eq!(message.content, "Hello");
assert_eq!(message.round, 1);
}
#[test]
fn test_council_message_format() {
let message = CouncilMessage::new("expert_1", "Test message", 2);
let formatted = message.format();
assert!(formatted.contains("Round 2"));
assert!(formatted.contains("expert_1"));
assert!(formatted.contains("Test message"));
}
#[test]
fn test_council_builder_basic() {
let result = CouncilBuilder::new()
.name("Test Council")
.add_participant("p1")
.add_participant("p2")
.build();
assert!(result.is_ok());
let council = result.unwrap();
assert_eq!(council.node.name, "Test Council");
assert_eq!(council.node.participant_ids.len(), 2);
}
#[test]
fn test_council_builder_validation_no_name() {
let result = CouncilBuilder::new()
.add_participant("p1")
.add_participant("p2")
.build();
assert!(result.is_err());
match result {
Err(BattalionError::ValidationError(msg)) => {
assert!(msg.contains("name is required"));
}
_ => panic!("Expected ValidationError for missing name"),
}
}
#[test]
fn test_council_builder_validation_too_few_participants() {
let result = CouncilBuilder::new()
.name("Test")
.add_participant("p1")
.build();
assert!(result.is_err());
match result {
Err(BattalionError::ValidationError(msg)) => {
assert!(msg.contains("at least 2 participants"));
}
_ => panic!("Expected ValidationError for too few participants"),
}
}
#[test]
fn test_council_builder_validation_moderator_required() {
let result = CouncilBuilder::new()
.name("Test")
.add_participant("p1")
.add_participant("p2")
.turn_strategy(TurnStrategy::ModeratorDirected)
.build();
assert!(result.is_err());
match result {
Err(BattalionError::CouncilError(CouncilError::ModeratorRequired)) => {
}
other => panic!("Expected CouncilError::ModeratorRequired, got {:?}", other),
}
}
#[test]
fn test_council_builder_with_moderator() {
let result = CouncilBuilder::new()
.name("Test")
.add_participant("p1")
.add_participant("p2")
.moderator("mod1")
.turn_strategy(TurnStrategy::ModeratorDirected)
.build();
assert!(result.is_ok());
let council = result.unwrap();
assert_eq!(council.node.moderator_id, Some("mod1".to_string()));
}
#[test]
fn test_council_builder_fluent_interface() {
let result = CouncilBuilder::new()
.name("Expert Panel")
.add_participant("expert_1")
.add_participant("expert_2")
.add_participant("expert_3")
.max_rounds(5)
.turn_strategy(TurnStrategy::RoundRobin)
.termination_condition(TerminationCondition::Consensus)
.include_history(false)
.build();
assert!(result.is_ok());
let council = result.unwrap();
assert_eq!(council.node.name, "Expert Panel");
assert_eq!(council.node.participant_ids.len(), 3);
assert_eq!(council.node.config.max_rounds, 5);
assert_eq!(council.node.config.turn_strategy, TurnStrategy::RoundRobin);
assert_eq!(
council.node.config.termination_condition,
TerminationCondition::Consensus
);
assert!(!council.node.config.include_history);
}
#[test]
fn test_turn_strategy_serialization() {
let strategy = TurnStrategy::VoluntaryWithTimeout { timeout_ms: 5000 };
let json = serde_json::to_string(&strategy).unwrap();
let deserialized: TurnStrategy = serde_json::from_str(&json).unwrap();
match deserialized {
TurnStrategy::VoluntaryWithTimeout { timeout_ms } => {
assert_eq!(timeout_ms, 5000);
}
_ => panic!("Expected VoluntaryWithTimeout"),
}
}
#[test]
fn test_termination_condition_serialization() {
let condition = TerminationCondition::Keyword("DONE".to_string());
let json = serde_json::to_string(&condition).unwrap();
let deserialized: TerminationCondition = serde_json::from_str(&json).unwrap();
match deserialized {
TerminationCondition::Keyword(keyword) => {
assert_eq!(keyword, "DONE");
}
_ => panic!("Expected Keyword termination"),
}
}
}