1use std::sync::Arc;
16use tracing::{info, warn};
17
18use crate::agent::ConversationMemory;
19use crate::config::Config;
20use crate::error::RavenClawsError;
21use crate::llm::{LLMProviderTrait, MultiModelManager};
22use crate::ravenfabric::RavenFabricClient;
23
24#[derive(Debug, Clone)]
28pub struct PatternConfig {
29 pub max_rounds: usize,
31 pub max_review_iterations: usize,
33 pub research_agent_count: usize,
35 pub voter_count: usize,
37 pub verbose: bool,
39}
40
41impl Default for PatternConfig {
42 fn default() -> Self {
43 Self {
44 max_rounds: 3,
45 max_review_iterations: 3,
46 research_agent_count: 3,
47 voter_count: 3,
48 verbose: false,
49 }
50 }
51}
52
53pub async fn run_debate(
60 llm: Arc<dyn LLMProviderTrait>,
61 config: Config,
62 ravenfabric: Option<RavenFabricClient>,
63 pattern_config: PatternConfig,
64) -> crate::error::Result<()> {
65 info!(
66 "Starting debate mode with {} max rounds",
67 pattern_config.max_rounds
68 );
69
70 let system_prompt = &config.llm.system_prompt;
71 let task = "Analyze the given task and provide your solution.";
72
73 let positions = [
75 ("Proponent", "You argue FOR the proposition. Focus on benefits, opportunities, and strengths. Be persuasive and evidence-based."),
76 ("Opponent", "You argue AGAINST the proposition. Focus on risks, drawbacks, and weaknesses. Be critical and thorough."),
77 ("Synthesizer", "You are the neutral judge. Listen to both sides, identify common ground, and synthesize a balanced conclusion."),
78 ];
79
80 let mut debate_history = ConversationMemory::new(system_prompt, 50);
81 debate_history.add_user_message(&format!(
82 "Debate topic: {}\n\nProponent, present your opening argument.",
83 task
84 ));
85
86 for round in 0..pattern_config.max_rounds {
88 info!(round = round + 1, "Debate round starting");
89
90 for (role, persona) in &positions {
91 if *role == "Synthesizer" && round < pattern_config.max_rounds - 1 {
92 continue; }
94
95 let mut agent_memory = ConversationMemory::new(persona, 20);
96 for msg in debate_history.history() {
98 if msg.role == "system" {
99 continue;
100 }
101 if msg.role == "user" {
102 agent_memory.add_user_message(&msg.content);
103 } else {
104 agent_memory.add_assistant_message(&msg.content);
105 }
106 }
107
108 let messages = agent_memory.history().to_vec();
109 match llm.chat(messages).await {
110 Ok(response) => {
111 if let Some(choice) = response.choices.first() {
112 let content = &choice.message.content;
113 info!(role = %role, round = round + 1, "Debate contribution received");
114
115 if pattern_config.verbose {
116 println!("\n── {} (Round {}) ──\n{}", role, round + 1, content);
117 }
118
119 debate_history.add_assistant_message(&format!("{}: {}", role, content));
120 }
121 }
122 Err(e) => {
123 warn!(error = %e, role = %role, round = round + 1, "Debate LLM request failed");
124 }
125 }
126 }
127 }
128
129 let synthesizer_persona = "You are a neutral synthesizer. Produce a final balanced conclusion that incorporates the best arguments from both sides.";
131 let mut final_memory = ConversationMemory::new(synthesizer_persona, 30);
132 for msg in debate_history.history() {
133 if msg.role == "system" {
134 continue;
135 }
136 if msg.role == "user" {
137 final_memory.add_user_message(&msg.content);
138 } else {
139 final_memory.add_assistant_message(&msg.content);
140 }
141 }
142 final_memory
143 .add_user_message("Now produce your FINAL synthesis that balances all perspectives:");
144
145 let messages = final_memory.history().to_vec();
146 match llm.chat(messages).await {
147 Ok(response) => {
148 if let Some(choice) = response.choices.first() {
149 let result = &choice.message.content;
150 println!("\n🐦⬛ Debate Synthesis:\n{}", result);
151
152 if let Some(ref rf) = ravenfabric {
153 if rf.is_enabled() {
154 let preview = result.chars().take(500).collect::<String>();
155 let _ = rf.broadcast(&preview, 30).await;
156 }
157 }
158 }
159 }
160 Err(e) => {
161 warn!(error = %e, "Debate synthesis failed");
162 return Err(RavenClawsError::CommandExecution(format!(
163 "Debate synthesis failed: {}",
164 e
165 )));
166 }
167 }
168
169 Ok(())
170}
171
172pub async fn run_review_loop(
180 llm: Arc<dyn LLMProviderTrait>,
181 config: Config,
182 ravenfabric: Option<RavenFabricClient>,
183 pattern_config: PatternConfig,
184) -> crate::error::Result<()> {
185 info!(
186 "Starting review-loop mode with max {} iterations",
187 pattern_config.max_review_iterations
188 );
189
190 let _system_prompt = &config.llm.system_prompt;
191 let task = "Analyze the given task and provide your solution.";
192
193 let producer_persona = "You are a producer. Create high-quality content based on the requirements. Be thorough and detailed.";
194 let reviewer_persona = "You are a reviewer. Critically evaluate the content. Identify specific issues, gaps, and improvements needed. Be constructive and precise. If the content meets quality standards, respond with APPROVED: followed by your final sign-off.";
195
196 let mut current_content = String::new();
197 let mut approved = false;
198
199 for iteration in 0..pattern_config.max_review_iterations {
200 info!(iteration = iteration + 1, "Review-loop iteration");
201
202 if iteration == 0 {
203 let mut producer_memory = ConversationMemory::new(producer_persona, 10);
205 producer_memory.add_user_message(&format!(
206 "Create content for the following task:\n\n{}",
207 task
208 ));
209
210 let messages = producer_memory.history().to_vec();
211 match llm.chat(messages).await {
212 Ok(response) => {
213 if let Some(choice) = response.choices.first() {
214 current_content = choice.message.content.clone();
215 info!("Initial content produced: {} chars", current_content.len());
216
217 if pattern_config.verbose {
218 println!("\n── Initial Content ──\n{}", current_content);
219 }
220 }
221 }
222 Err(e) => {
223 warn!(error = %e, "Producer LLM request failed");
224 return Err(RavenClawsError::CommandExecution(format!(
225 "Producer failed: {}",
226 e
227 )));
228 }
229 }
230 } else {
231 let mut reviewer_memory = ConversationMemory::new(reviewer_persona, 10);
233 reviewer_memory.add_user_message(&format!(
234 "Review the following content and provide constructive feedback:\n\n{}",
235 current_content
236 ));
237
238 let messages = reviewer_memory.history().to_vec();
239 let review = match llm.chat(messages).await {
240 Ok(response) => response
241 .choices
242 .first()
243 .map(|c| c.message.content.clone())
244 .unwrap_or_default(),
245 Err(e) => {
246 warn!(error = %e, "Reviewer LLM request failed");
247 continue;
248 }
249 };
250
251 if pattern_config.verbose {
252 println!("\n── Review (Iteration {}) ──\n{}", iteration + 1, review);
253 }
254
255 if review.contains("APPROVED:") {
257 info!("Content approved after {} iterations", iteration + 1);
258 let final_content = review.split("APPROVED:").nth(1).unwrap_or(¤t_content);
259 current_content = final_content.trim().to_string();
260 approved = true;
261 break;
262 }
263
264 let mut producer_memory = ConversationMemory::new(producer_persona, 10);
266 producer_memory.add_user_message(&format!(
267 "Your previous content:\n\n{}\n\nReviewer feedback:\n\n{}\n\nPlease revise the content addressing all feedback.",
268 current_content, review
269 ));
270
271 let messages = producer_memory.history().to_vec();
272 match llm.chat(messages).await {
273 Ok(response) => {
274 if let Some(choice) = response.choices.first() {
275 current_content = choice.message.content.clone();
276 info!("Content revised: {} chars", current_content.len());
277
278 if pattern_config.verbose {
279 println!(
280 "\n── Revised Content (Iteration {}) ──\n{}",
281 iteration + 1,
282 current_content
283 );
284 }
285 }
286 }
287 Err(e) => {
288 warn!(error = %e, "Producer revision failed");
289 continue;
290 }
291 }
292 }
293 }
294
295 if !approved {
296 warn!("Review-loop reached max iterations without approval");
297 }
298
299 println!("\n🐦⬛ Review-Loop Final Content:\n{}", current_content);
300
301 if let Some(ref rf) = ravenfabric {
302 if rf.is_enabled() {
303 let preview = current_content.chars().take(500).collect::<String>();
304 let _ = rf.broadcast(&preview, 30).await;
305 }
306 }
307
308 Ok(())
309}
310
311pub async fn run_research_synthesize(
318 llm: Arc<dyn LLMProviderTrait>,
319 config: Config,
320 ravenfabric: Option<RavenFabricClient>,
321 pattern_config: PatternConfig,
322) -> crate::error::Result<()> {
323 info!(
324 "Starting research-synthesize mode with {} research agents",
325 pattern_config.research_agent_count
326 );
327
328 let _system_prompt = &config.llm.system_prompt;
329 let task = "Analyze the given task and provide your solution.";
330
331 let perspectives = [
333 ("Fact-Finder", "You are a fact-finding researcher. Focus on verifiable facts, data, statistics, and concrete evidence. Cite specific sources and numbers."),
334 ("Analyst", "You are an analytical researcher. Focus on patterns, trends, cause-and-effect relationships, and strategic implications."),
335 ("Innovator", "You are an innovative researcher. Focus on novel approaches, emerging trends, creative solutions, and future possibilities."),
336 ];
337
338 let agent_count = pattern_config.research_agent_count.min(perspectives.len());
339 let mut research_results: Vec<(String, String)> = Vec::new();
340
341 for (role, persona) in perspectives.iter().take(agent_count) {
343 info!(role = %role, "Research agent starting");
344
345 let mut memory = ConversationMemory::new(persona, 10);
346 memory.add_user_message(&format!(
347 "Research the following topic from your perspective:\n\n{}",
348 task
349 ));
350
351 let messages = memory.history().to_vec();
352 match llm.chat(messages).await {
353 Ok(response) => {
354 if let Some(choice) = response.choices.first() {
355 let content = choice.message.content.clone();
356 info!(role = %role, "Research completed: {} chars", content.len());
357 research_results.push((role.to_string(), content));
358 }
359 }
360 Err(e) => {
361 warn!(error = %e, role = %role, "Research agent failed");
362 research_results.push((role.to_string(), format!("[Research failed: {}]", e)));
363 }
364 }
365 }
366
367 if pattern_config.verbose {
369 println!("\n── Research Findings ──");
370 for (role, content) in &research_results {
371 println!("\n--- {} ---\n{}", role, content);
372 }
373 }
374
375 let synthesizer_persona = "You are a synthesis specialist. Combine multiple research perspectives into a coherent, well-structured report. Identify common themes, resolve contradictions, and present a unified analysis.";
377 let mut synth_memory = ConversationMemory::new(synthesizer_persona, 20);
378
379 let mut synthesis_input =
380 String::from("Synthesize the following research findings into a comprehensive report:\n\n");
381 for (role, content) in &research_results {
382 synthesis_input.push_str(&format!("\n=== {} ===\n{}\n", role, content));
383 }
384 synth_memory.add_user_message(&synthesis_input);
385
386 let messages = synth_memory.history().to_vec();
387 match llm.chat(messages).await {
388 Ok(response) => {
389 if let Some(choice) = response.choices.first() {
390 let result = &choice.message.content;
391 println!("\n🐦⬛ Research Synthesis:\n{}", result);
392
393 if let Some(ref rf) = ravenfabric {
394 if rf.is_enabled() {
395 let preview = result.chars().take(500).collect::<String>();
396 let _ = rf.broadcast(&preview, 30).await;
397 }
398 }
399 }
400 }
401 Err(e) => {
402 warn!(error = %e, "Synthesis failed");
403 return Err(RavenClawsError::CommandExecution(format!(
404 "Synthesis failed: {}",
405 e
406 )));
407 }
408 }
409
410 Ok(())
411}
412
413pub async fn run_voting(
421 llm: Arc<dyn LLMProviderTrait>,
422 config: Config,
423 ravenfabric: Option<RavenFabricClient>,
424 pattern_config: PatternConfig,
425) -> crate::error::Result<()> {
426 info!(
427 "Starting voting mode with {} voters",
428 pattern_config.voter_count
429 );
430
431 let system_prompt = &config.llm.system_prompt;
432 let task = "Analyze the given task and provide your solution.";
433
434 let voter_personas = [
436 "You are a conservative voter. You prefer safe, proven approaches. Prioritize stability and risk mitigation. Respond with: VOTE: <your choice> REASONING: <your reasoning>",
437 "You are an aggressive voter. You prefer bold, ambitious approaches. Prioritize maximum impact and innovation. Respond with: VOTE: <your choice> REASONING: <your reasoning>",
438 "You are a balanced voter. You weigh pros and cons carefully. Prioritize pragmatic, well-rounded solutions. Respond with: VOTE: <your choice> REASONING: <your reasoning>",
439 "You are a detail-oriented voter. You focus on implementation feasibility and technical soundness. Respond with: VOTE: <your choice> REASONING: <your reasoning>",
440 "You are a user-centric voter. You prioritize user experience, accessibility, and usability. Respond with: VOTE: <your choice> REASONING: <your reasoning>",
441 ];
442
443 let voter_count = pattern_config.voter_count.min(voter_personas.len());
444 let mut votes: Vec<(String, String, String)> = Vec::new(); for (i, persona) in voter_personas.iter().enumerate().take(voter_count) {
448 let persona_name = persona
449 .split('.')
450 .next()
451 .unwrap_or(&format!("Voter {}", i + 1))
452 .to_string();
453
454 let mut memory = ConversationMemory::new(&format!("{}\n\n{}", system_prompt, persona), 10);
455 memory.add_user_message(&format!(
456 "Evaluate the following and cast your vote:\n\n{}",
457 task
458 ));
459
460 let messages = memory.history().to_vec();
461 match llm.chat(messages).await {
462 Ok(response) => {
463 if let Some(choice) = response.choices.first() {
464 let content = choice.message.content.clone();
465 info!(voter = %persona_name, "Vote cast");
466
467 let vote = content
469 .split("VOTE:")
470 .nth(1)
471 .and_then(|s| s.split("REASONING:").next())
472 .map(|s| s.trim().to_string())
473 .unwrap_or_else(|| "Unknown".to_string());
474
475 let reasoning = content
476 .split("REASONING:")
477 .nth(1)
478 .map(|s| s.trim().to_string())
479 .unwrap_or_default();
480
481 if pattern_config.verbose {
482 println!(
483 "\n── {} ──\nVOTE: {}\nREASONING: {}",
484 persona_name, vote, reasoning
485 );
486 }
487
488 votes.push((persona_name, vote, reasoning));
489 }
490 }
491 Err(e) => {
492 warn!(error = %e, voter = %persona_name, "Voter LLM request failed");
493 votes.push((
494 persona_name,
495 "Error".to_string(),
496 format!("Vote failed: {}", e),
497 ));
498 }
499 }
500 }
501
502 let mut tally: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
504 for (_, vote, _) in &votes {
505 *tally.entry(vote.clone()).or_insert(0) += 1;
506 }
507
508 let max_votes = tally.values().cloned().max().unwrap_or(0);
510 let winners: Vec<String> = tally
511 .iter()
512 .filter(|(_, count)| **count == max_votes)
513 .map(|(vote, _)| vote.clone())
514 .collect();
515
516 println!("\n🐦⬛ Voting Results:");
517 println!("───");
518 for (persona, vote, reasoning) in &votes {
519 println!("{}: VOTE = {} | {}", persona, vote, reasoning);
520 }
521 println!("───");
522 println!("Tally: {:?}", tally);
523 println!("\n🏆 Decision: {}", winners.join(", "));
524
525 if let Some(ref rf) = ravenfabric {
526 if rf.is_enabled() {
527 let summary = format!(
528 "Voting completed: {} voters, decision: {}",
529 votes.len(),
530 winners.join(", ")
531 );
532 let _ = rf.broadcast(&summary, 30).await;
533 }
534 }
535
536 Ok(())
537}
538
539pub async fn run_debate_multi(
543 multi_llm: MultiModelManager,
544 config: Config,
545 _ravenfabric: Option<RavenFabricClient>,
546 pattern_config: PatternConfig,
547) -> crate::error::Result<()> {
548 info!(
549 "Starting debate mode (multi-model) with {} providers",
550 multi_llm.client_count()
551 );
552
553 let system_prompt = &config.llm.system_prompt;
554 let task = "Analyze the given task and provide your solution.";
555
556 let positions = [
557 (
558 "Proponent",
559 "You argue FOR the proposition. Focus on benefits, opportunities, and strengths.",
560 ),
561 (
562 "Opponent",
563 "You argue AGAINST the proposition. Focus on risks, drawbacks, and weaknesses.",
564 ),
565 (
566 "Synthesizer",
567 "You are the neutral judge. Synthesize a balanced conclusion.",
568 ),
569 ];
570
571 let mut debate_history = ConversationMemory::new(system_prompt, 50);
572 debate_history.add_user_message(&format!("Debate topic: {}", task));
573
574 for round in 0..pattern_config.max_rounds {
575 info!(round = round + 1, "Multi-model debate round");
576
577 for (role, persona) in &positions {
578 if *role == "Synthesizer" && round < pattern_config.max_rounds - 1 {
579 continue;
580 }
581
582 let provider_idx = round % multi_llm.client_count();
583 let client = multi_llm.get_client(provider_idx);
584
585 if let Some(client) = client {
586 let mut agent_memory = ConversationMemory::new(persona, 20);
587 for msg in debate_history.history() {
588 if msg.role == "system" {
589 continue;
590 }
591 if msg.role == "user" {
592 agent_memory.add_user_message(&msg.content);
593 } else {
594 agent_memory.add_assistant_message(&msg.content);
595 }
596 }
597
598 let messages = agent_memory.history().to_vec();
599 match client.chat(messages).await {
600 Ok(response) => {
601 if let Some(choice) = response.choices.first() {
602 let content = &choice.message.content;
603 info!(role = %role, provider = client.provider_name(), round = round + 1, "Debate contribution");
604 if pattern_config.verbose {
605 println!(
606 "\n── {} ({} via {}, Round {}) ──\n{}",
607 role,
608 client.provider_name(),
609 client.model(),
610 round + 1,
611 content
612 );
613 }
614 debate_history.add_assistant_message(&format!(
615 "{} ({}): {}",
616 role,
617 client.provider_name(),
618 content
619 ));
620 }
621 }
622 Err(e) => warn!(error = %e, role = %role, "Multi-model debate failed"),
623 }
624 }
625 }
626 }
627
628 if let Some(client) = multi_llm.get_client(0) {
630 let synthesizer_persona =
631 "You are a neutral synthesizer. Produce a final balanced conclusion.";
632 let mut final_memory = ConversationMemory::new(synthesizer_persona, 30);
633 for msg in debate_history.history() {
634 if msg.role == "system" {
635 continue;
636 }
637 if msg.role == "user" {
638 final_memory.add_user_message(&msg.content);
639 } else {
640 final_memory.add_assistant_message(&msg.content);
641 }
642 }
643 final_memory.add_user_message("Now produce your FINAL synthesis:");
644
645 let messages = final_memory.history().to_vec();
646 match client.chat(messages).await {
647 Ok(response) => {
648 if let Some(choice) = response.choices.first() {
649 println!(
650 "\n🐦⬛ Multi-Model Debate Synthesis:\n{}",
651 choice.message.content
652 );
653 }
654 }
655 Err(e) => warn!(error = %e, "Multi-model synthesis failed"),
656 }
657 }
658
659 Ok(())
660}
661
662pub async fn run_review_loop_multi(
664 multi_llm: MultiModelManager,
665 _config: Config,
666 _ravenfabric: Option<RavenFabricClient>,
667 pattern_config: PatternConfig,
668) -> crate::error::Result<()> {
669 info!("Starting review-loop mode (multi-model)");
670
671 let _system_prompt = &_config.llm.system_prompt;
672 let _ = &_system_prompt;
673 let task = "Analyze the given task and provide your solution.";
674
675 let producer_persona = "You are a producer. Create high-quality content.";
676 let reviewer_persona = "You are a reviewer. Critically evaluate content. Respond with APPROVED: when quality is met.";
677
678 let mut current_content = String::new();
679 let mut approved = false;
680
681 for iteration in 0..pattern_config.max_review_iterations {
682 info!(iteration = iteration + 1, "Multi-model review-loop");
683
684 let provider_idx = iteration % multi_llm.client_count();
685 let client = multi_llm.get_client(provider_idx);
686
687 if let Some(client) = client {
688 if iteration == 0 {
689 let mut memory = ConversationMemory::new(producer_persona, 10);
690 memory.add_user_message(&format!("Create content for: {}", task));
691 let messages = memory.history().to_vec();
692 match client.chat(messages).await {
693 Ok(response) => {
694 if let Some(choice) = response.choices.first() {
695 current_content = choice.message.content.clone();
696 info!(
697 "Initial content: {} chars via {}",
698 current_content.len(),
699 client.provider_name()
700 );
701 }
702 }
703 Err(e) => warn!(error = %e, "Producer failed"),
704 }
705 } else {
706 let mut rev_memory = ConversationMemory::new(reviewer_persona, 10);
708 rev_memory.add_user_message(&format!("Review:\n\n{}", current_content));
709 let messages = rev_memory.history().to_vec();
710 match client.chat(messages).await {
711 Ok(response) => {
712 if let Some(choice) = response.choices.first() {
713 let review = &choice.message.content;
714 if review.contains("APPROVED:") {
715 info!(
716 "Approved after {} iterations via {}",
717 iteration + 1,
718 client.provider_name()
719 );
720 current_content = review
721 .split("APPROVED:")
722 .nth(1)
723 .unwrap_or(¤t_content)
724 .trim()
725 .to_string();
726 approved = true;
727 break;
728 }
729 let mut prod_memory = ConversationMemory::new(producer_persona, 10);
731 prod_memory.add_user_message(&format!(
732 "Content:\n{}\n\nFeedback:\n{}\n\nRevise:",
733 current_content, review
734 ));
735 let msgs = prod_memory.history().to_vec();
736 if let Some(next_client) =
737 multi_llm.get_client((iteration + 1) % multi_llm.client_count())
738 {
739 if let Ok(rev_resp) = next_client.chat(msgs).await {
740 if let Some(rev_choice) = rev_resp.choices.first() {
741 current_content = rev_choice.message.content.clone();
742 info!(
743 "Revised: {} chars via {}",
744 current_content.len(),
745 next_client.provider_name()
746 );
747 }
748 }
749 }
750 }
751 }
752 Err(e) => warn!(error = %e, "Review failed"),
753 }
754 }
755 }
756 }
757
758 if !approved {
759 warn!("Review-loop reached max iterations without approval");
760 }
761
762 println!("\n🐦⬛ Multi-Model Review-Loop Final:\n{}", current_content);
763 Ok(())
764}
765
766pub async fn run_research_synthesize_multi(
768 multi_llm: MultiModelManager,
769 config: Config,
770 _ravenfabric: Option<RavenFabricClient>,
771 pattern_config: PatternConfig,
772) -> crate::error::Result<()> {
773 info!("Starting research-synthesize mode (multi-model)");
774
775 let system_prompt = &config.llm.system_prompt;
776 let task = "Analyze the given task and provide your solution.";
777
778 let perspectives = [
779 (
780 "Fact-Finder",
781 "Focus on verifiable facts, data, statistics.",
782 ),
783 (
784 "Analyst",
785 "Focus on patterns, trends, strategic implications.",
786 ),
787 (
788 "Innovator",
789 "Focus on novel approaches and future possibilities.",
790 ),
791 ];
792
793 let agent_count = pattern_config.research_agent_count.min(perspectives.len());
794 let mut results: Vec<(String, String, String)> = Vec::new(); for (i, (role, persona)) in perspectives.iter().enumerate().take(agent_count) {
797 let client = multi_llm.get_client(i % multi_llm.client_count());
798
799 if let Some(client) = client {
800 let mut memory =
801 ConversationMemory::new(&format!("{}\n\n{}", system_prompt, persona), 10);
802 memory.add_user_message(&format!("Research: {}", task));
803 let messages = memory.history().to_vec();
804
805 match client.chat(messages).await {
806 Ok(response) => {
807 if let Some(choice) = response.choices.first() {
808 let content = choice.message.content.clone();
809 info!(role = %role, provider = client.provider_name(), "Research completed");
810 results.push((
811 role.to_string(),
812 client.provider_name().to_string(),
813 content,
814 ));
815 }
816 }
817 Err(e) => warn!(error = %e, role = %role, "Research failed"),
818 }
819 }
820 }
821
822 if let Some(client) = multi_llm.get_client(0) {
824 let mut synth_memory = ConversationMemory::new("You are a synthesis specialist.", 20);
825 let mut input = String::from("Synthesize these findings:\n\n");
826 for (role, provider, content) in &results {
827 input.push_str(&format!("=== {} ({}) ===\n{}\n", role, provider, content));
828 }
829 synth_memory.add_user_message(&input);
830 let messages = synth_memory.history().to_vec();
831
832 match client.chat(messages).await {
833 Ok(response) => {
834 if let Some(choice) = response.choices.first() {
835 println!(
836 "\n🐦⬛ Multi-Model Research Synthesis:\n{}",
837 choice.message.content
838 );
839 }
840 }
841 Err(e) => warn!(error = %e, "Synthesis failed"),
842 }
843 }
844
845 Ok(())
846}
847
848pub async fn run_voting_multi(
850 multi_llm: MultiModelManager,
851 config: Config,
852 _ravenfabric: Option<RavenFabricClient>,
853 pattern_config: PatternConfig,
854) -> crate::error::Result<()> {
855 info!(
856 "Starting voting mode (multi-model) with {} providers",
857 multi_llm.client_count()
858 );
859
860 let system_prompt = &config.llm.system_prompt;
861 let task = "Analyze the given task and provide your solution.";
862
863 let voter_personas = [
864 "Conservative: Prefer safe, proven approaches. VOTE: <choice> REASONING: <reasoning>",
865 "Aggressive: Prefer bold, ambitious approaches. VOTE: <choice> REASONING: <reasoning>",
866 "Balanced: Weigh pros and cons. VOTE: <choice> REASONING: <reasoning>",
867 "Detail-oriented: Focus on feasibility. VOTE: <choice> REASONING: <reasoning>",
868 "User-centric: Prioritize UX. VOTE: <choice> REASONING: <reasoning>",
869 ];
870
871 let voter_count = pattern_config
872 .voter_count
873 .min(voter_personas.len())
874 .min(multi_llm.client_count());
875 let mut votes: Vec<(String, String, String, String)> = Vec::new(); for (i, persona) in voter_personas.iter().enumerate().take(voter_count) {
878 let client = multi_llm.get_client(i % multi_llm.client_count());
879
880 if let Some(client) = client {
881 let mut memory =
882 ConversationMemory::new(&format!("{}\n\n{}", system_prompt, persona), 10);
883 memory.add_user_message(&format!("Cast your vote on: {}", task));
884 let messages = memory.history().to_vec();
885
886 match client.chat(messages).await {
887 Ok(response) => {
888 if let Some(choice) = response.choices.first() {
889 let content = choice.message.content.clone();
890 let vote = content
891 .split("VOTE:")
892 .nth(1)
893 .and_then(|s| s.split("REASONING:").next())
894 .map(|s| s.trim().to_string())
895 .unwrap_or_else(|| "Unknown".to_string());
896 let reasoning = content
897 .split("REASONING:")
898 .nth(1)
899 .map(|s| s.trim().to_string())
900 .unwrap_or_default();
901 votes.push((
902 format!("Voter {}", i + 1),
903 client.provider_name().to_string(),
904 vote,
905 reasoning,
906 ));
907 }
908 }
909 Err(e) => warn!(error = %e, "Voter {} failed", i + 1),
910 }
911 }
912 }
913
914 let mut tally: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
916 for (_, _, vote, _) in &votes {
917 *tally.entry(vote.clone()).or_insert(0) += 1;
918 }
919
920 println!("\n🐦⬛ Multi-Model Voting Results:");
921 for (persona, provider, vote, reasoning) in &votes {
922 println!(
923 "{} ({}): VOTE = {} | {}",
924 persona, provider, vote, reasoning
925 );
926 }
927 println!("Tally: {:?}", tally);
928 println!(
929 "🏆 Decision: {}",
930 tally
931 .iter()
932 .max_by_key(|(_, c)| *c)
933 .map(|(v, _)| v.clone())
934 .unwrap_or_else(|| "No decision".to_string())
935 );
936
937 Ok(())
938}
939
940#[cfg(test)]
941mod tests {
942 use super::*;
943
944 #[test]
945 fn test_pattern_config_defaults() {
946 let cfg = PatternConfig::default();
947 assert_eq!(cfg.max_rounds, 3);
948 assert_eq!(cfg.max_review_iterations, 3);
949 assert_eq!(cfg.research_agent_count, 3);
950 assert_eq!(cfg.voter_count, 3);
951 assert!(!cfg.verbose);
952 }
953
954 #[test]
955 fn test_pattern_config_custom() {
956 let cfg = PatternConfig {
957 max_rounds: 5,
958 max_review_iterations: 5,
959 research_agent_count: 5,
960 voter_count: 7,
961 verbose: true,
962 };
963 assert_eq!(cfg.max_rounds, 5);
964 assert_eq!(cfg.voter_count, 7);
965 assert!(cfg.verbose);
966 }
967
968 #[test]
969 fn test_debate_function_exists() {
970 let _ = std::mem::size_of::<PatternConfig>();
972 }
973
974 #[test]
975 fn test_review_loop_function_exists() {
976 let cfg = PatternConfig::default();
977 assert_eq!(cfg.max_review_iterations, 3);
978 }
979
980 #[test]
981 fn test_research_synthesize_function_exists() {
982 let cfg = PatternConfig::default();
983 assert_eq!(cfg.research_agent_count, 3);
984 }
985
986 #[test]
987 fn test_voting_function_exists() {
988 let cfg = PatternConfig::default();
989 assert_eq!(cfg.voter_count, 3);
990 }
991}