Skip to main content

ravenclaws/
patterns.rs

1//! Multi-agent pattern primitives
2//!
3//! Built-in multi-agent collaboration patterns that extend beyond simple
4//! swarm/supervisor modes. Each pattern implements a distinct collaboration
5//! strategy:
6//!
7//! - **Debate** — Multiple agents argue different positions, then converge
8//! - **Review-Loop** — One agent produces, another reviews, iterating to quality
9//! - **Research-Synthesize** — Parallel research agents feed a synthesizer
10//! - **Voting** — Multiple agents vote on options, majority decides
11//!
12//! These patterns are first-class modes accessible via `--mode debate`,
13//! `--mode review-loop`, `--mode research-synthesize`, or `--mode voting`.
14
15use 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// ── Pattern Configuration ──────────────────────────────────────────────────
25
26/// Configuration for multi-agent patterns
27#[derive(Debug, Clone)]
28pub struct PatternConfig {
29    /// Maximum debate rounds (default: 3)
30    pub max_rounds: usize,
31    /// Maximum review iterations (default: 3)
32    pub max_review_iterations: usize,
33    /// Number of research agents (default: 3)
34    pub research_agent_count: usize,
35    /// Number of voters (default: 3)
36    pub voter_count: usize,
37    /// Whether to show intermediate results
38    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
53// ── Pattern 1: Debate ──────────────────────────────────────────────────────
54
55/// Run a debate between multiple agents with different positions.
56///
57/// Each agent argues a distinct position, then a judge synthesizes the
58/// final conclusion after configurable rounds of debate.
59pub 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    // Define debate positions
74    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    // Debate rounds
87    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; // Synthesizer only speaks in final round
93            }
94
95            let mut agent_memory = ConversationMemory::new(persona, 20);
96            // Feed debate history
97            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    // Final synthesis
130    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
172// ── Pattern 2: Review-Loop ─────────────────────────────────────────────────
173
174/// Run a producer-reviewer loop that iterates until quality is met.
175///
176/// A producer agent creates content, a reviewer agent critiques it,
177/// and the producer revises based on feedback. Repeats until the
178/// reviewer approves or max iterations reached.
179pub 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            // Producer creates initial content
204            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            // Reviewer critiques
232            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            // Check if approved
256            if review.contains("APPROVED:") {
257                info!("Content approved after {} iterations", iteration + 1);
258                let final_content = review.split("APPROVED:").nth(1).unwrap_or(&current_content);
259                current_content = final_content.trim().to_string();
260                approved = true;
261                break;
262            }
263
264            // Producer revises based on feedback
265            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
311// ── Pattern 3: Research-Synthesize ─────────────────────────────────────────
312
313/// Run parallel research agents followed by a synthesizer.
314///
315/// Multiple research agents explore different aspects of a topic in parallel.
316/// A synthesizer agent then combines their findings into a coherent report.
317pub 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    // Define research perspectives
332    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    // Phase 1: Parallel research
342    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    // Print intermediate research if verbose
368    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    // Phase 2: Synthesis
376    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
413// ── Pattern 4: Voting ──────────────────────────────────────────────────────
414
415/// Run a voting process where multiple agents evaluate options.
416///
417/// Each voter agent independently evaluates the options and provides
418/// their choice with reasoning. Results are tallied and the majority
419/// decision is reported.
420pub 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    // Define voter personas for diversity
435    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(); // (persona_name, vote, reasoning)
445
446    // Phase 1: Independent voting
447    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                    // Extract vote and reasoning
468                    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    // Phase 2: Tally results
503    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    // Find winner(s)
509    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
539// ── Multi-model variants ───────────────────────────────────────────────────
540
541/// Run debate mode with multiple LLM providers (round-robin)
542pub 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    // Final synthesis using first provider
629    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
662/// Run review-loop mode with multiple LLM providers
663pub 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                // Review
707                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(&current_content)
724                                    .trim()
725                                    .to_string();
726                                approved = true;
727                                break;
728                            }
729                            // Revise
730                            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
766/// Run research-synthesize mode with multiple LLM providers
767pub 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(); // (role, provider, content)
795
796    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    // Synthesize
823    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
848/// Run voting mode with multiple LLM providers
849pub 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(); // (persona, provider, vote, reasoning)
876
877    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    // Tally
915    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        // Compile-time check that debate function signature is valid
971        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}