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