gemini_adk_fluent_rs/
patterns.rs

1//! Pre-built patterns — common multi-agent workflows.
2//!
3//! High-level functions that compose agents into standard patterns:
4//! review loops, cascades, fan-out-merge, supervised workflows, etc.
5//!
6//! Each function returns a [`Composable`] that can be compiled into an
7//! executable [`TextAgent`](gemini_adk_rs::text::TextAgent) via
8//! [`Composable::compile()`](crate::operators::Composable::compile).
9//!
10//! # Examples
11//!
12//! ```rust,ignore
13//! use gemini_adk_fluent_rs::prelude::*;
14//!
15//! // Review loop: author writes, reviewer checks, loop until approved
16//! let draft = review_loop(
17//!     AgentBuilder::new("author").instruction("Write an essay"),
18//!     AgentBuilder::new("reviewer").instruction("Review and set approved=true when good"),
19//!     3,
20//! );
21//!
22//! // Cascade: try agents in order, first success wins
23//! let robust = cascade(vec![
24//!     AgentBuilder::new("primary"),
25//!     AgentBuilder::new("fallback"),
26//! ]);
27//!
28//! // Fan-out-merge: parallel agents, then merge
29//! let research = fan_out_merge(
30//!     vec![AgentBuilder::new("web"), AgentBuilder::new("db")],
31//!     AgentBuilder::new("synthesizer"),
32//! );
33//! ```
34
35use crate::builder::AgentBuilder;
36use crate::operators::{Composable, Fallback, FanOut, Loop, LoopPredicate, Pipeline};
37
38/// Review loop: author writes, reviewer checks, loop until approved.
39///
40/// The author agent produces output, then the reviewer evaluates it.
41/// The loop terminates when the reviewer sets `"approved"` to `true`
42/// in the state, or after `max_rounds` iterations.
43///
44/// # Arguments
45///
46/// * `author` — The agent that produces drafts.
47/// * `reviewer` — The agent that evaluates and sets `"approved": true` when satisfied.
48/// * `max_rounds` — Maximum number of author-reviewer cycles.
49///
50/// # Example
51///
52/// ```rust,ignore
53/// let workflow = review_loop(
54///     AgentBuilder::new("writer").instruction("Write a blog post"),
55///     AgentBuilder::new("editor").instruction("Review. Set approved=true if publication-ready."),
56///     3,
57/// );
58/// let agent = workflow.compile(llm);
59/// ```
60pub fn review_loop(author: AgentBuilder, reviewer: AgentBuilder, max_rounds: usize) -> Composable {
61    let inner = Composable::Pipeline(Pipeline::new(vec![
62        Composable::Agent(author),
63        Composable::Agent(reviewer),
64    ]));
65
66    Composable::Loop(Loop {
67        body: Box::new(inner),
68        max: max_rounds as u32,
69        middleware: Vec::new(),
70        until: Some(LoopPredicate::new(|state| {
71            state
72                .get("approved")
73                .and_then(|v| v.as_bool())
74                .unwrap_or(false)
75        })),
76    })
77}
78
79/// Review loop with a custom quality key and target value.
80///
81/// Like [`review_loop`] but allows specifying which state key the reviewer
82/// writes to and what value signals completion.
83///
84/// # Arguments
85///
86/// * `worker` — The agent that produces output.
87/// * `reviewer` — The agent that evaluates quality.
88/// * `quality_key` — State key the reviewer writes (e.g., `"quality"`).
89/// * `target` — Value of `quality_key` that signals completion (e.g., `"good"`).
90/// * `max_rounds` — Maximum iterations.
91pub fn review_loop_keyed(
92    worker: AgentBuilder,
93    reviewer: AgentBuilder,
94    quality_key: &str,
95    target: &str,
96    max_rounds: u32,
97) -> Composable {
98    let key = quality_key.to_string();
99    let target = target.to_string();
100
101    let inner = Composable::Pipeline(Pipeline::new(vec![
102        Composable::Agent(worker),
103        Composable::Agent(reviewer),
104    ]));
105
106    Composable::Loop(Loop {
107        body: Box::new(inner),
108        max: max_rounds,
109        middleware: Vec::new(),
110        until: Some(LoopPredicate::new(move |state| {
111            state
112                .get(&key)
113                .and_then(|v| v.as_str())
114                .map(|v| v == target)
115                .unwrap_or(false)
116        })),
117    })
118}
119
120/// Cascade: try agents in sequence, first success wins.
121///
122/// This is an alias for a fallback chain. Each agent is tried in order;
123/// the first one that succeeds provides the result.
124///
125/// # Example
126///
127/// ```rust,ignore
128/// let robust = cascade(vec![
129///     AgentBuilder::new("fast").instruction("Quick answer"),
130///     AgentBuilder::new("thorough").instruction("Detailed answer"),
131/// ]);
132/// ```
133pub fn cascade(agents: Vec<AgentBuilder>) -> Composable {
134    Composable::Fallback(Fallback::new(
135        agents.into_iter().map(Composable::Agent).collect(),
136    ))
137}
138
139/// Fan-out-merge: run agents in parallel, then merge results with a merger agent.
140///
141/// All `agents` execute concurrently via fan-out. Their combined output is
142/// then fed into the `merger` agent, which synthesizes a final result.
143///
144/// # Arguments
145///
146/// * `agents` — Agents to run in parallel.
147/// * `merger` — Agent that merges the parallel results.
148///
149/// # Example
150///
151/// ```rust,ignore
152/// let research = fan_out_merge(
153///     vec![
154///         AgentBuilder::new("web-search").instruction("Search the web"),
155///         AgentBuilder::new("db-lookup").instruction("Query the database"),
156///     ],
157///     AgentBuilder::new("synthesizer").instruction("Combine research findings"),
158/// );
159/// ```
160pub fn fan_out_merge(agents: Vec<AgentBuilder>, merger: AgentBuilder) -> Composable {
161    let fan_out = Composable::FanOut(FanOut::new(
162        agents.into_iter().map(Composable::Agent).collect(),
163    ));
164
165    Composable::Pipeline(Pipeline::new(vec![fan_out, Composable::Agent(merger)]))
166}
167
168/// Chain: simple sequential pipeline of agents.
169///
170/// This is an alias for the `>>` operator but accepts a `Vec`.
171/// Each agent runs in order, with the output of one feeding into the next.
172///
173/// # Example
174///
175/// ```rust,ignore
176/// let pipeline = chain(vec![
177///     AgentBuilder::new("extract"),
178///     AgentBuilder::new("transform"),
179///     AgentBuilder::new("load"),
180/// ]);
181/// ```
182pub fn chain(agents: Vec<AgentBuilder>) -> Composable {
183    Composable::Pipeline(Pipeline::new(
184        agents.into_iter().map(Composable::Agent).collect(),
185    ))
186}
187
188/// Conditional: route to one of two agents based on a state predicate.
189///
190/// Evaluates `predicate` against the current state. If it returns `true`,
191/// the `if_true` agent runs; otherwise, the `if_false` agent runs.
192///
193/// # Arguments
194///
195/// * `predicate` — Function that inspects state (as `serde_json::Value`) and returns a bool.
196/// * `if_true` — Agent to run when the predicate is true.
197/// * `if_false` — Agent to run when the predicate is false.
198///
199/// # Example
200///
201/// ```rust,ignore
202/// let routed = conditional(
203///     |state| state.get("premium").and_then(|v| v.as_bool()).unwrap_or(false),
204///     AgentBuilder::new("premium-agent").instruction("Full-featured response"),
205///     AgentBuilder::new("basic-agent").instruction("Basic response"),
206/// );
207/// ```
208pub fn conditional(
209    predicate: impl Fn(&serde_json::Value) -> bool + Send + Sync + 'static,
210    if_true: AgentBuilder,
211    if_false: AgentBuilder,
212) -> Composable {
213    let pred = std::sync::Arc::new(predicate);
214    let pred_clone = pred.clone();
215
216    let true_branch = AgentBuilder::new(if_true.name())
217        .instruction(if_true.get_instruction().unwrap_or_default());
218    let false_branch = AgentBuilder::new(if_false.name())
219        .instruction(if_false.get_instruction().unwrap_or_default());
220
221    // Store predicate in a loop with max=1 for the true branch,
222    // fall back to false branch.
223    let guarded = Composable::Loop(Loop {
224        body: Box::new(Composable::Agent(true_branch)),
225        max: 1,
226        middleware: Vec::new(),
227        until: Some(LoopPredicate::new(move |state| pred_clone(state))),
228    });
229
230    Composable::Fallback(Fallback::new(vec![
231        guarded,
232        Composable::Agent(false_branch),
233    ]))
234}
235
236/// Supervised: worker with supervisor oversight loop.
237///
238/// The worker agent produces output, then the supervisor reviews it.
239/// The loop repeats until the supervisor sets `"approved"` to `true`
240/// in the state, or after `max_rounds` iterations.
241///
242/// This is semantically similar to [`review_loop`] but framed as a
243/// worker-supervisor relationship rather than author-reviewer.
244///
245/// # Arguments
246///
247/// * `worker` — The agent that performs the task.
248/// * `supervisor` — The agent that oversees and approves work.
249/// * `max_rounds` — Maximum number of worker-supervisor cycles.
250///
251/// # Example
252///
253/// ```rust,ignore
254/// let managed = supervised(
255///     AgentBuilder::new("coder").instruction("Write the implementation"),
256///     AgentBuilder::new("lead").instruction("Code review. Set approved=true if ready to merge."),
257///     5,
258/// );
259/// ```
260pub fn supervised(worker: AgentBuilder, supervisor: AgentBuilder, max_rounds: usize) -> Composable {
261    let inner = Composable::Pipeline(Pipeline::new(vec![
262        Composable::Agent(worker),
263        Composable::Agent(supervisor),
264    ]));
265
266    Composable::Loop(Loop {
267        body: Box::new(inner),
268        max: max_rounds as u32,
269        middleware: Vec::new(),
270        until: Some(LoopPredicate::new(|state| {
271            state
272                .get("approved")
273                .and_then(|v| v.as_bool())
274                .unwrap_or(false)
275        })),
276    })
277}
278
279/// Supervised with a custom approval key.
280///
281/// Like [`supervised`] but allows specifying which state key signals approval.
282///
283/// # Arguments
284///
285/// * `worker` — The agent that performs the task.
286/// * `supervisor` — The agent that oversees work.
287/// * `approval_key` — State key the supervisor sets to `true` when satisfied.
288/// * `max_revisions` — Maximum iterations.
289pub fn supervised_keyed(
290    worker: AgentBuilder,
291    supervisor: AgentBuilder,
292    approval_key: &str,
293    max_revisions: u32,
294) -> Composable {
295    let key = approval_key.to_string();
296
297    let inner = Composable::Pipeline(Pipeline::new(vec![
298        Composable::Agent(worker),
299        Composable::Agent(supervisor),
300    ]));
301
302    Composable::Loop(Loop {
303        body: Box::new(inner),
304        max: max_revisions,
305        middleware: Vec::new(),
306        until: Some(LoopPredicate::new(move |state| {
307            state.get(&key).and_then(|v| v.as_bool()).unwrap_or(false)
308        })),
309    })
310}
311
312/// Map-over: apply a single agent to multiple items concurrently.
313///
314/// Returns a `MapOver` composable that stores the agent template and concurrency limit.
315pub fn map_over(agent: AgentBuilder, concurrency: usize) -> MapOver {
316    MapOver { agent, concurrency }
317}
318
319/// A map-over workflow node — applies one agent to many items.
320#[derive(Clone, Debug)]
321pub struct MapOver {
322    /// The agent template applied to each item.
323    pub agent: AgentBuilder,
324    /// Maximum number of concurrent executions.
325    pub concurrency: usize,
326}
327
328/// Map-reduce: apply a mapper agent to items, then a reducer to combine results.
329pub fn map_reduce(mapper: AgentBuilder, reducer: AgentBuilder, concurrency: usize) -> MapReduce {
330    MapReduce {
331        mapper,
332        reducer,
333        concurrency,
334    }
335}
336
337/// A map-reduce workflow node.
338#[derive(Clone, Debug)]
339pub struct MapReduce {
340    /// The mapper agent applied to each item.
341    pub mapper: AgentBuilder,
342    /// The reducer agent that combines mapped results.
343    pub reducer: AgentBuilder,
344    /// Maximum concurrency for the map phase.
345    pub concurrency: usize,
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    fn agent(name: &str) -> AgentBuilder {
353        AgentBuilder::new(name)
354    }
355
356    #[test]
357    fn review_loop_creates_loop_with_pipeline() {
358        let result = review_loop(agent("writer"), agent("reviewer"), 3);
359        match &result {
360            Composable::Loop(l) => {
361                assert_eq!(l.max, 3);
362                assert!(l.until.is_some());
363                assert!(matches!(&*l.body, Composable::Pipeline(p) if p.steps.len() == 2));
364            }
365            _ => panic!("expected Loop"),
366        }
367    }
368
369    #[test]
370    fn review_loop_predicate_checks_approved() {
371        let result = review_loop(agent("w"), agent("r"), 3);
372        if let Composable::Loop(l) = result {
373            let pred = l.until.unwrap();
374            assert!(!pred.check(&serde_json::json!({"approved": false})));
375            assert!(pred.check(&serde_json::json!({"approved": true})));
376            assert!(!pred.check(&serde_json::json!({})));
377        }
378    }
379
380    #[test]
381    fn review_loop_keyed_predicate_works() {
382        let result = review_loop_keyed(agent("w"), agent("r"), "quality", "good", 3);
383        if let Composable::Loop(l) = result {
384            let pred = l.until.unwrap();
385            assert!(!pred.check(&serde_json::json!({"quality": "bad"})));
386            assert!(pred.check(&serde_json::json!({"quality": "good"})));
387        }
388    }
389
390    #[test]
391    fn cascade_creates_fallback() {
392        let result = cascade(vec![agent("a"), agent("b"), agent("c")]);
393        match result {
394            Composable::Fallback(f) => assert_eq!(f.candidates.len(), 3),
395            _ => panic!("expected Fallback"),
396        }
397    }
398
399    #[test]
400    fn fan_out_merge_creates_pipeline_with_fan_out_then_merger() {
401        let result = fan_out_merge(vec![agent("a"), agent("b")], agent("merger"));
402        match &result {
403            Composable::Pipeline(p) => {
404                assert_eq!(p.steps.len(), 2);
405                assert!(matches!(&p.steps[0], Composable::FanOut(f) if f.branches.len() == 2));
406                assert!(matches!(&p.steps[1], Composable::Agent(a) if a.name() == "merger"));
407            }
408            _ => panic!("expected Pipeline"),
409        }
410    }
411
412    #[test]
413    fn chain_creates_pipeline() {
414        let result = chain(vec![agent("a"), agent("b"), agent("c")]);
415        match result {
416            Composable::Pipeline(p) => assert_eq!(p.steps.len(), 3),
417            _ => panic!("expected Pipeline"),
418        }
419    }
420
421    #[test]
422    fn conditional_creates_fallback_with_guard() {
423        let result = conditional(
424            |state| state.get("flag").and_then(|v| v.as_bool()).unwrap_or(false),
425            agent("yes").instruction("true branch"),
426            agent("no").instruction("false branch"),
427        );
428        match &result {
429            Composable::Fallback(f) => assert_eq!(f.candidates.len(), 2),
430            _ => panic!("expected Fallback"),
431        }
432    }
433
434    #[test]
435    fn supervised_creates_loop() {
436        let result = supervised(agent("worker"), agent("supervisor"), 5);
437        match &result {
438            Composable::Loop(l) => {
439                assert_eq!(l.max, 5);
440                assert!(l.until.is_some());
441                assert!(matches!(&*l.body, Composable::Pipeline(p) if p.steps.len() == 2));
442            }
443            _ => panic!("expected Loop"),
444        }
445    }
446
447    #[test]
448    fn supervised_predicate_checks_approved() {
449        let result = supervised(agent("w"), agent("s"), 5);
450        if let Composable::Loop(l) = result {
451            let pred = l.until.unwrap();
452            assert!(!pred.check(&serde_json::json!({"approved": false})));
453            assert!(pred.check(&serde_json::json!({"approved": true})));
454        }
455    }
456
457    #[test]
458    fn supervised_keyed_predicate_works() {
459        let result = supervised_keyed(agent("w"), agent("s"), "approved", 5);
460        if let Composable::Loop(l) = result {
461            let pred = l.until.unwrap();
462            assert!(!pred.check(&serde_json::json!({"approved": false})));
463            assert!(pred.check(&serde_json::json!({"approved": true})));
464        }
465    }
466
467    #[test]
468    fn map_over_stores_params() {
469        let m = map_over(agent("processor"), 4);
470        assert_eq!(m.agent.name(), "processor");
471        assert_eq!(m.concurrency, 4);
472    }
473
474    #[test]
475    fn map_reduce_stores_params() {
476        let mr = map_reduce(agent("mapper"), agent("reducer"), 8);
477        assert_eq!(mr.mapper.name(), "mapper");
478        assert_eq!(mr.reducer.name(), "reducer");
479        assert_eq!(mr.concurrency, 8);
480    }
481}