gemini_adk_rs/flow/
timing.rs1use std::time::Duration;
30
31use serde::{Deserialize, Serialize};
32
33use crate::live::steering::ContextDelivery;
34
35pub const VOICE_TIMING_KEY: &str = "session:voice_timing";
37
38pub const DEFAULT_REPROMPT: &str = "The user has not answered. Briefly repeat or rephrase your last question, and do not add anything new.";
40
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
44#[serde(default)]
45pub struct VoiceTiming {
46 #[serde(skip_serializing_if = "Option::is_none")]
49 pub reprompt_after_ms: Option<u64>,
50 #[serde(skip_serializing_if = "Option::is_none")]
53 pub reprompt: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub filler_after_ms: Option<u64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
60 pub interruptible: Option<bool>,
61 #[serde(skip_serializing_if = "Option::is_none")]
64 pub end_of_speech_ms: Option<u64>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub context_delivery: Option<ContextDelivery>,
68}
69
70impl VoiceTiming {
71 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn reprompt_after(mut self, silence: Duration) -> Self {
78 self.reprompt_after_ms = Some(millis(silence));
79 self
80 }
81
82 pub fn reprompt_with(mut self, silence: Duration, text: impl Into<String>) -> Self {
84 self.reprompt_after_ms = Some(millis(silence));
85 self.reprompt = Some(text.into());
86 self
87 }
88
89 pub fn filler_after(mut self, after: Duration) -> Self {
91 self.filler_after_ms = Some(millis(after));
92 self
93 }
94
95 pub fn uninterruptible(mut self) -> Self {
97 self.interruptible = Some(false);
98 self
99 }
100
101 pub fn end_of_speech(mut self, pause: Duration) -> Self {
103 self.end_of_speech_ms = Some(millis(pause));
104 self
105 }
106
107 pub fn context_delivery(mut self, delivery: ContextDelivery) -> Self {
109 self.context_delivery = Some(delivery);
110 self
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self == &Self::default()
116 }
117
118 pub fn holds_floor(&self) -> bool {
120 self.interruptible == Some(false)
121 }
122
123 pub fn reprompt_text(&self) -> &str {
125 self.reprompt.as_deref().unwrap_or(DEFAULT_REPROMPT)
126 }
127
128 pub fn merge(&self, other: &Self) -> Self {
133 fn min(a: Option<u64>, b: Option<u64>) -> Option<u64> {
134 match (a, b) {
135 (Some(a), Some(b)) => Some(a.min(b)),
136 (a, b) => a.or(b),
137 }
138 }
139 Self {
140 reprompt_after_ms: min(self.reprompt_after_ms, other.reprompt_after_ms),
141 reprompt: self.reprompt.clone().or_else(|| other.reprompt.clone()),
142 filler_after_ms: min(self.filler_after_ms, other.filler_after_ms),
143 interruptible: match (self.interruptible, other.interruptible) {
144 (Some(false), _) | (_, Some(false)) => Some(false),
145 (a, b) => a.or(b),
146 },
147 end_of_speech_ms: self.end_of_speech_ms.max(other.end_of_speech_ms),
148 context_delivery: self.context_delivery.or(other.context_delivery),
149 }
150 }
151}
152
153fn millis(d: Duration) -> u64 {
154 u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn merge_takes_the_cautious_setting() {
163 let a = VoiceTiming::new()
164 .reprompt_after(Duration::from_secs(8))
165 .end_of_speech(Duration::from_millis(400));
166 let b = VoiceTiming::new()
167 .reprompt_after(Duration::from_secs(5))
168 .uninterruptible()
169 .end_of_speech(Duration::from_millis(900));
170 let m = a.merge(&b);
171 assert_eq!(m.reprompt_after_ms, Some(5_000));
172 assert!(m.holds_floor());
173 assert_eq!(m.end_of_speech_ms, Some(900));
174 }
175
176 #[test]
177 fn round_trips_json_and_omits_unset_fields() {
178 let t = VoiceTiming::new()
179 .filler_after(Duration::from_millis(1500))
180 .context_delivery(ContextDelivery::Deferred);
181 let json = serde_json::to_value(&t).unwrap();
182 assert_eq!(
183 json,
184 serde_json::json!({ "filler_after_ms": 1500, "context_delivery": "deferred" })
185 );
186 assert_eq!(serde_json::from_value::<VoiceTiming>(json).unwrap(), t);
187 assert!(VoiceTiming::new().is_empty());
188 }
189}