gemini_adk_fluent_rs/
motifs.rs1use gemini_adk_rs::flow::Guard;
27use gemini_adk_rs::frame::Frame;
28
29use crate::conversation::{CommitSpec, OverlaySpec, Resume, StageSpec, TransitionSpec};
30
31pub struct Motif;
33
34impl Motif {
35 pub fn collect_frame<F: Frame>(id: impl Into<String>) -> StageSpec {
37 let frame = F::frame();
38 StageSpec {
39 id: id.into(),
40 collect: frame.slot_keys(),
41 frame: Some(frame),
42 ..Default::default()
43 }
44 }
45
46 pub fn say(id: impl Into<String>, text: impl Into<String>) -> StageSpec {
48 StageSpec {
49 id: id.into(),
50 say: Some(text.into()),
51 done: Some(Guard::always()),
52 ..Default::default()
53 }
54 }
55
56 pub fn disclosure(id: impl Into<String>, ack_key: impl Into<String>) -> StageSpec {
58 let ack = ack_key.into();
59 StageSpec {
60 id: id.into(),
61 say: Some("Read the required disclosure, then continue.".into()),
62 done: Some(Guard::is_true(ack)),
63 ..Default::default()
64 }
65 }
66
67 pub fn confirm_then_commit(
70 id: impl Into<String>,
71 tool: impl Into<String>,
72 confirm_key: impl Into<String>,
73 ) -> StageSpec {
74 let tool = tool.into();
75 StageSpec {
76 id: id.into(),
77 allow: vec![tool.clone()],
78 commit: Some(CommitSpec {
79 tool: tool.clone(),
80 when: Guard::is_true(confirm_key),
81 }),
82 done: Some(Guard::called_ok(tool)),
83 ..Default::default()
84 }
85 }
86
87 pub fn identity_verification(
89 id: impl Into<String>,
90 verified_key: impl Into<String>,
91 ) -> StageSpec {
92 StageSpec {
93 id: id.into(),
94 say: Some("Verify the caller's identity before proceeding.".into()),
95 done: Some(Guard::is_true(verified_key)),
96 ..Default::default()
97 }
98 }
99
100 pub fn handoff(id: impl Into<String>) -> StageSpec {
102 StageSpec {
103 id: id.into(),
104 say: Some("Hand off to a human agent with a summary.".into()),
105 terminal: true,
106 ..Default::default()
107 }
108 }
109
110 pub fn faq_digression(
113 name: impl Into<String>,
114 trigger_key: impl Into<String>,
115 answered_key: impl Into<String>,
116 ) -> OverlaySpec {
117 let answered = answered_key.into();
118 OverlaySpec {
119 name: name.into(),
120 trigger: Guard::is_true(trigger_key),
121 stages: vec![
122 StageSpec {
123 id: "answer".into(),
124 say: Some("Answer the user's question.".into()),
125 done: Some(Guard::is_true(answered.clone())),
126 next: vec![TransitionSpec {
127 to: "faq_end".into(),
128 when: Guard::is_true(answered),
129 }],
130 ..Default::default()
131 },
132 StageSpec {
133 id: "faq_end".into(),
134 terminal: true,
135 ..Default::default()
136 },
137 ],
138 require: Vec::new(),
139 resume: Resume::Previous,
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::conversation::Conversation;
148 use crate::simulation::Sim;
149 use gemini_adk_rs::flow::Enforcement;
150 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec};
151
152 struct Booking;
153 impl Frame for Booking {
154 fn frame() -> FrameSpec {
155 FrameSpec {
156 name: "booking".into(),
157 slots: vec![SlotSpec {
158 recognizer: Some(SlotRecognizer::IntegerNear(vec!["people".into()])),
159 ..SlotSpec::new("party_size")
160 }],
161 }
162 }
163 }
164
165 #[tokio::test]
166 async fn motifs_compose_into_a_runnable_conversation() {
167 let convo = Conversation::new("booking")
168 .add_stage(Motif::identity_verification("verify", "verified"))
169 .next("collect", Guard::is_true("verified"))
170 .add_stage(Motif::collect_frame::<Booking>("collect"))
171 .next("confirm", Guard::captured(["party_size"]))
172 .add_stage(Motif::confirm_then_commit(
173 "confirm",
174 "book",
175 "user_confirmed",
176 ))
177 .next("done", Guard::called_ok("book"))
178 .add_stage(Motif::handoff("done"))
179 .require(["done"])
180 .compile()
181 .expect("motif conversation compiles");
182
183 let mut sim = Sim::new(&convo, Enforcement::Enforce);
184 assert!(sim.active().contains(&"verify".to_string()));
185
186 sim.set("verified", true);
187 sim.turn();
188 assert!(sim.active().contains(&"collect".to_string()));
189
190 sim.user("a table for 4 people").await;
191 assert_eq!(sim.slot::<u32>("party_size"), Some(4));
192 assert!(sim.active().contains(&"confirm".to_string()));
193
194 assert!(!sim.allowed("book"));
196 sim.set("user_confirmed", true);
197 sim.turn();
198 assert!(sim.allowed("book"));
199 sim.tool_ok("book");
200 assert!(sim.is_complete());
201 }
202
203 #[tokio::test]
204 async fn faq_digression_motif_suspends_and_resumes() {
205 let convo = Conversation::new("support")
206 .stage("triage")
207 .next("resolve", Guard::is_true("triaged"))
208 .stage("resolve")
209 .terminal()
210 .add_overlay(Motif::faq_digression("faq", "intent:faq", "faq_answered"))
211 .compile()
212 .expect("compiles");
213
214 let mut sim = Sim::new(&convo, Enforcement::Enforce);
215 assert!(sim.active().contains(&"triage".to_string()));
216
217 sim.set("intent:faq", true);
218 sim.turn();
219 assert_eq!(sim.active_overlay(), Some("faq"));
220
221 sim.set("faq_answered", true);
222 sim.set("intent:faq", false);
223 sim.turn();
224 assert!(sim.active_overlay().is_none());
225 assert!(sim.active().contains(&"triage".to_string())); }
227
228 #[test]
229 fn unguarded_confirm_motif_would_be_rejected() {
230 let mut stage = Motif::confirm_then_commit("confirm", "book", "user_confirmed");
233 stage.commit = Some(CommitSpec {
234 tool: "book".into(),
235 when: Guard::always(),
236 });
237 let err = Conversation::new("x")
238 .add_stage(stage)
239 .next("done", Guard::called_ok("book"))
240 .stage("done")
241 .terminal()
242 .compile()
243 .expect_err("unguarded commit must fail");
244 assert!(matches!(
245 err,
246 crate::conversation::ConversationError::Compile(_)
247 ));
248 }
249}