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