1use crate::call::Command;
2use crate::event::SessionEvent;
3use anyhow::Result;
4use async_trait::async_trait;
5use futures::StreamExt;
6use once_cell::sync::Lazy;
7use regex::Regex;
8use reqwest::Client;
9use serde_json::json;
10use std::collections::HashMap;
11use std::sync::Arc;
12use tracing::{info, warn};
13
14#[cfg(test)]
15mod tests;
16
17#[cfg(test)]
18mod dtmf_collector_tests;
19
20static RE_HANGUP: Lazy<Regex> = Lazy::new(|| Regex::new(r"<hangup\s*/>").unwrap());
21static RE_REFER: Lazy<Regex> = Lazy::new(|| Regex::new(r#"<refer\s+to="([^"]+)"\s*/>"#).unwrap());
22static RE_PLAY: Lazy<Regex> = Lazy::new(|| Regex::new(r#"<play\s+file="([^"]+)"\s*/>"#).unwrap());
23static RE_GOTO: Lazy<Regex> = Lazy::new(|| Regex::new(r#"<goto\s+scene="([^"]+)"\s*/>"#).unwrap());
24static RE_SET_VAR: Lazy<Regex> =
25 Lazy::new(|| Regex::new(r#"<set_var\s+key="([^"]+)"\s+value=["'](.+?)["']\s*/>"#).unwrap());
26static RE_HTTP: Lazy<Regex> = Lazy::new(|| {
27 Regex::new(r#"<http\s+url="([^"]+)"(?:\s+method="([^"]+)")?(?:\s+body="([^"]+)")?\s*/>"#)
28 .unwrap()
29});
30static RE_COLLECT: Lazy<Regex> = Lazy::new(|| {
31 Regex::new(r#"<collect\s+type="([^"]+)"\s+var="([^"]+)"(?:\s+prompt="([^"]*)")?\s*/>"#).unwrap()
32});
33static RE_SENTENCE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)[.!?。!?\n]\s*").unwrap());
34static FILLERS: Lazy<std::collections::HashSet<String>> = Lazy::new(|| {
35 let mut s = std::collections::HashSet::new();
36 let default_fillers = ["嗯", "啊", "哦", "那个", "那个...", "uh", "um", "ah"];
37
38 if let Ok(content) = std::fs::read_to_string("config/fillers.txt") {
39 for line in content.lines() {
40 let trimmed = line.trim().to_lowercase();
41 if !trimmed.is_empty() {
42 s.insert(trimmed);
43 }
44 }
45 }
46
47 if s.is_empty() {
48 for f in default_fillers {
49 s.insert(f.to_string());
50 }
51 }
52 s
53});
54
55use super::ChatMessage;
56use super::InterruptionStrategy;
57use super::LlmConfig;
58use super::dialogue::DialogueHandler;
59
60pub mod provider;
61pub mod rag;
62pub mod types;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65enum CommandKind {
66 Hangup,
67 Refer,
68 Sentence,
69 Play,
70 Goto,
71 SetVar,
72 Http,
73 Collect,
74}
75
76pub use provider::*;
77pub use rag::*;
78pub use types::*;
79
80const MAX_RAG_ATTEMPTS: usize = 3;
81
82#[derive(Debug, Clone)]
84pub struct CollectorState {
85 pub collector_type: String,
87 pub var_name: String,
89 pub config: super::DtmfCollectorConfig,
91 pub buffer: String,
93 pub start_time: std::time::Instant,
95 pub last_digit_time: std::time::Instant,
97 pub retry_count: u32,
99}
100
101pub struct LlmHandler {
102 config: LlmConfig,
103 interruption_config: super::InterruptionConfig,
104 global_follow_up_config: Option<super::FollowUpConfig>,
105 dtmf_config: Option<HashMap<String, super::DtmfAction>>,
106 dtmf_collectors: Option<HashMap<String, super::DtmfCollectorConfig>>,
107 history: Vec<ChatMessage>,
108 provider: Arc<dyn LlmProvider>,
109 rag_retriever: Arc<dyn RagRetriever>,
110 is_speaking: bool,
111 is_hanging_up: bool,
112 consecutive_follow_ups: u32,
113 last_interaction_at: std::time::Instant,
114 event_sender: Option<crate::event::EventSender>,
115 last_asr_final_at: Option<std::time::Instant>,
116 last_tts_start_at: Option<std::time::Instant>,
117 last_robot_msg_at: Option<std::time::Instant>,
118 call: Option<crate::call::ActiveCallRef>,
119 scenes: HashMap<String, super::Scene>,
120 current_scene_id: Option<String>,
121 client: Client,
122 sip_config: Option<crate::SipOption>,
123 collector_state: Option<CollectorState>,
125}
126
127impl LlmHandler {
128 pub fn new(
129 config: LlmConfig,
130 interruption: super::InterruptionConfig,
131 global_follow_up_config: Option<super::FollowUpConfig>,
132 scenes: HashMap<String, super::Scene>,
133 dtmf: Option<HashMap<String, super::DtmfAction>>,
134 dtmf_collectors: Option<HashMap<String, super::DtmfCollectorConfig>>,
135 initial_scene_id: Option<String>,
136 sip_config: Option<crate::SipOption>,
137 ) -> Self {
138 Self::with_provider(
139 config,
140 Arc::new(DefaultLlmProvider::new()),
141 Arc::new(NoopRagRetriever),
142 interruption,
143 global_follow_up_config,
144 scenes,
145 dtmf,
146 dtmf_collectors,
147 initial_scene_id,
148 sip_config,
149 )
150 }
151
152 pub fn with_provider(
153 config: LlmConfig,
154 provider: Arc<dyn LlmProvider>,
155 rag_retriever: Arc<dyn RagRetriever>,
156 interruption: super::InterruptionConfig,
157 global_follow_up_config: Option<super::FollowUpConfig>,
158 scenes: HashMap<String, super::Scene>,
159 dtmf: Option<HashMap<String, super::DtmfAction>>,
160 dtmf_collectors: Option<HashMap<String, super::DtmfCollectorConfig>>,
161 initial_scene_id: Option<String>,
162 sip_config: Option<crate::SipOption>,
163 ) -> Self {
164 let mut history = Vec::new();
165 let system_prompt = Self::build_system_prompt(&config, None, dtmf_collectors.as_ref());
166
167 history.push(ChatMessage {
168 role: "system".to_string(),
169 content: system_prompt,
170 });
171
172 Self {
173 config,
174 interruption_config: interruption,
175 global_follow_up_config,
176 dtmf_config: dtmf,
177 dtmf_collectors,
178 history,
179 provider,
180 rag_retriever,
181 is_speaking: false,
182 is_hanging_up: false,
183 consecutive_follow_ups: 0,
184 last_interaction_at: std::time::Instant::now(),
185 event_sender: None,
186 last_asr_final_at: None,
187 last_tts_start_at: None,
188 last_robot_msg_at: None,
189 call: None,
190 scenes,
191 current_scene_id: initial_scene_id,
192 client: Client::new(),
193 sip_config,
194 collector_state: None,
195 }
196 }
197
198 fn build_system_prompt(
199 config: &LlmConfig,
200 scene_prompt: Option<&str>,
201 dtmf_collectors: Option<&HashMap<String, super::DtmfCollectorConfig>>,
202 ) -> String {
203 let base_prompt =
204 scene_prompt.unwrap_or_else(|| config.prompt.as_deref().unwrap_or_default());
205 let mut features_prompt = String::new();
206
207 if let Some(features) = &config.features {
208 let lang = config.language.as_deref().unwrap_or("zh");
209 for feature in features {
210 match Self::load_feature_snippet(feature, lang) {
211 Ok(snippet) => {
212 features_prompt.push_str(&format!("\n- {}", snippet));
213 }
214 Err(e) => {
215 warn!("Failed to load feature snippet {}: {}", feature, e);
216 }
217 }
218 }
219 }
220
221 let features_section = if features_prompt.is_empty() {
222 String::new()
223 } else {
224 format!("\n\n### Enhanced Capabilities:{}\n", features_prompt)
225 };
226
227 let tool_instructions = if let Some(custom) = &config.tool_instructions {
229 custom.clone()
230 } else {
231 let lang = config.language.as_deref().unwrap_or("zh");
232 Self::load_feature_snippet("tool_instructions", lang)
233 .unwrap_or_else(|_| {
234 Self::load_feature_snippet("tool_instructions", "en")
236 .unwrap_or_else(|_| {
237 "Tool usage instructions:\n\
239 - To hang up the call, output: <hangup/>\n\
240 - To transfer the call, output: <refer to=\"sip:xxxx\"/>\n\
241 - To play an audio file, output: <play file=\"path/to/file.wav\"/>\n\
242 - To switch to another scene, output: <goto scene=\"scene_id\"/>\n\
243 - To call an external HTTP API, output JSON:\n\
244 ```json\n\
245 {{ \"tools\": [{{ \"name\": \"http\", \"url\": \"...\", \"method\": \"POST\", \"body\": {{ ... }} }}] }}\n\
246 ```\n\
247 Please use XML tags for simple actions and JSON blocks for tool calls. \
248 Output your response in short sentences. Each sentence will be played as soon as it is finished."
249 .to_string()
250 })
251 })
252 };
253
254 let collector_section = Self::generate_collector_instructions(dtmf_collectors);
255
256 format!(
257 "{}{}\n\n{}\n{}",
258 base_prompt, features_section, tool_instructions, collector_section
259 )
260 }
261
262 fn load_feature_snippet(feature: &str, lang: &str) -> Result<String> {
263 let path = format!("features/{}.{}.md", feature, lang);
264 let content = std::fs::read_to_string(path)?;
265 Ok(content.trim().to_string())
266 }
267
268 fn generate_collector_instructions(
270 collectors: Option<&HashMap<String, super::DtmfCollectorConfig>>,
271 ) -> String {
272 let collectors = match collectors {
273 Some(c) if !c.is_empty() => c,
274 _ => return String::new(),
275 };
276
277 let mut doc = String::from("\n### DTMF Digit Collection\n\n");
278 doc.push_str(
279 "When you need to collect numeric input from the user (such as phone numbers, \
280 verification codes, ID numbers, etc.), use the DTMF digit collection command. \
281 This is more accurate than voice recognition for numeric input.\n\n",
282 );
283 doc.push_str("**Usage:** Output the following XML tag to start collecting:\n");
284 doc.push_str(
285 "```\n<collect type=\"TYPE\" var=\"VAR_NAME\" prompt=\"PROMPT_TEXT\" />\n```\n\n",
286 );
287 doc.push_str("- `type`: The collector type (see available types below)\n");
288 doc.push_str("- `var`: Variable name to store the collected digits\n");
289 doc.push_str("- `prompt`: The voice prompt to play before collecting (tell the user what to input)\n\n");
290 doc.push_str("**Available collector types:**\n\n");
291
292 let mut sorted: Vec<_> = collectors.iter().collect();
294 sorted.sort_by_key(|(k, _)| (*k).clone());
295
296 for (name, config) in &sorted {
297 let desc = config.description.as_deref().unwrap_or("No description");
298 let mut details = Vec::new();
299 if let Some(d) = config.digits {
300 details.push(format!("{} digits", d));
301 } else {
302 if let Some(min) = config.min_digits {
303 details.push(format!("min {} digits", min));
304 }
305 if let Some(max) = config.max_digits {
306 details.push(format!("max {} digits", max));
307 }
308 }
309 if let Some(fk) = &config.finish_key {
310 details.push(format!("press {} to finish", fk));
311 }
312 let detail_str = if details.is_empty() {
313 String::new()
314 } else {
315 format!(" ({})", details.join(", "))
316 };
317 doc.push_str(&format!("- `{}`: {}{}\n", name, desc, detail_str));
318 }
319
320 doc.push_str("\n**Flow:**\n");
321 doc.push_str("1. You output `<collect .../>` with a voice prompt\n");
322 doc.push_str("2. The system plays your prompt, then enters digit collection mode (voice input is ignored)\n");
323 doc.push_str("3. When collection completes, the system notifies you with the result\n");
324 doc.push_str("4. You can access the collected value via `{{ var_name }}` in subsequent responses\n\n");
325 doc.push_str(
326 "**Important:** During collection the user can only input digits, not speak. ",
327 );
328 doc.push_str("If validation fails, the system will automatically retry. ");
329 doc.push_str("After collection success or failure, continue the conversation naturally.\n");
330
331 doc
332 }
333
334 pub async fn check_collector_timeout(&mut self) -> Result<Vec<Command>> {
337 let state = match &self.collector_state {
338 Some(s) => s,
339 None => return Ok(vec![]),
340 };
341
342 let timeout_secs = state.config.timeout.unwrap_or(15) as u64;
343 let inter_digit_timeout_secs = state.config.inter_digit_timeout.unwrap_or(5) as u64;
344
345 if state.start_time.elapsed().as_secs() >= timeout_secs {
347 info!(
348 "DTMF collector overall timeout ({}s) for var={}",
349 timeout_secs, state.var_name
350 );
351 let var_name = state.var_name.clone();
352 let buffer = state.buffer.clone();
353 let collector_type = state.collector_type.clone();
354 let config = state.config.clone();
355 let retry_count = state.retry_count;
356 self.collector_state = None;
357
358 if !buffer.is_empty() {
359 return self
361 .do_finish_collection(buffer, var_name, collector_type, config, retry_count)
362 .await;
363 }
364
365 self.history.push(ChatMessage {
367 role: "system".to_string(),
368 content: format!(
369 "[DTMF collection timed out for '{}'. No digits were entered. Please guide the user.]",
370 var_name
371 ),
372 });
373 return self.generate_response().await;
374 }
375
376 if !state.buffer.is_empty()
378 && state.last_digit_time.elapsed().as_secs() >= inter_digit_timeout_secs
379 {
380 info!(
381 "DTMF collector inter-digit timeout ({}s) for var={}, buffer={}",
382 inter_digit_timeout_secs, state.var_name, state.buffer
383 );
384 let buffer = state.buffer.clone();
385 let var_name = state.var_name.clone();
386 let collector_type = state.collector_type.clone();
387 let config = state.config.clone();
388 let retry_count = state.retry_count;
389 self.collector_state = None;
390 return self
391 .do_finish_collection(buffer, var_name, collector_type, config, retry_count)
392 .await;
393 }
394
395 Ok(vec![])
396 }
397
398 async fn handle_collector_digit(&mut self, digit: &str) -> Result<Vec<Command>> {
400 let state = self.collector_state.as_mut().unwrap();
401
402 if let Some(ref finish_key) = state.config.finish_key.clone() {
404 if digit == finish_key {
405 info!("DTMF collector: finish key '{}' received", digit);
406 let buffer = state.buffer.clone();
407 let var_name = state.var_name.clone();
408 let collector_type = state.collector_type.clone();
409 let config = state.config.clone();
410 let retry_count = state.retry_count;
411 self.collector_state = None;
412 return self
413 .do_finish_collection(buffer, var_name, collector_type, config, retry_count)
414 .await;
415 }
416 }
417
418 state.buffer.push_str(digit);
420 state.last_digit_time = std::time::Instant::now();
421
422 info!(
423 "DTMF collector: digit '{}', buffer now '{}'",
424 digit, state.buffer
425 );
426
427 let effective_max = state.config.digits.or(state.config.max_digits);
429
430 if let Some(max) = effective_max {
431 if state.buffer.len() >= max as usize {
432 if state.config.finish_key.is_none() {
434 info!("DTMF collector: reached max digits ({})", max);
435 let buffer = state.buffer.clone();
436 let var_name = state.var_name.clone();
437 let collector_type = state.collector_type.clone();
438 let config = state.config.clone();
439 let retry_count = state.retry_count;
440 self.collector_state = None;
441 return self
442 .do_finish_collection(buffer, var_name, collector_type, config, retry_count)
443 .await;
444 }
445 }
446 }
447
448 Ok(vec![])
449 }
450
451 async fn do_finish_collection(
453 &mut self,
454 buffer: String,
455 var_name: String,
456 collector_type: String,
457 config: super::DtmfCollectorConfig,
458 retry_count: u32,
459 ) -> Result<Vec<Command>> {
460 let min = config.digits.or(config.min_digits).unwrap_or(0);
462 if min > 0 && (buffer.len() as u32) < min {
463 return self
464 .retry_or_fail(
465 collector_type,
466 config,
467 retry_count,
468 var_name,
469 &format!("Expected at least {} digits, got {}", min, buffer.len()),
470 )
471 .await;
472 }
473
474 if let Some(validation) = &config.validation {
476 if let Ok(re) = regex::Regex::new(&validation.pattern) {
477 if !re.is_match(&buffer) {
478 let msg = validation
479 .error_message
480 .clone()
481 .unwrap_or_else(|| "Input format is incorrect".to_string());
482 return self
483 .retry_or_fail(collector_type, config, retry_count, var_name, &msg)
484 .await;
485 }
486 }
487 }
488
489 info!(
491 "DTMF collector: successfully collected '{}' for var '{}'",
492 buffer, var_name
493 );
494
495 if let Some(call) = &self.call {
496 let mut state = call.call_state.write().await;
497 let mut extras = state.extras.take().unwrap_or_default();
498 extras.insert(var_name.clone(), serde_json::Value::String(buffer.clone()));
499 state.extras = Some(extras);
500 }
501
502 self.history.push(ChatMessage {
504 role: "system".to_string(),
505 content: format!("[DTMF collection completed for '{}': {}]", var_name, buffer),
506 });
507
508 self.generate_response().await
510 }
511
512 async fn retry_or_fail(
514 &mut self,
515 collector_type: String,
516 config: super::DtmfCollectorConfig,
517 retry_count: u32,
518 var_name: String,
519 reason: &str,
520 ) -> Result<Vec<Command>> {
521 let max_retries = config.retry_times.unwrap_or(3);
522
523 if retry_count >= max_retries {
524 info!(
525 "DTMF collector: max retries ({}) reached for var '{}'",
526 max_retries, var_name
527 );
528 self.history.push(ChatMessage {
529 role: "system".to_string(),
530 content: format!(
531 "[DTMF collection failed for '{}' after {} retries: {}. Please guide the user to try again or use an alternative method.]",
532 var_name, max_retries, reason
533 ),
534 });
535 return self.generate_response().await;
536 }
537
538 info!(
539 "DTMF collector: retry {}/{} for var '{}': {}",
540 retry_count + 1,
541 max_retries,
542 var_name,
543 reason
544 );
545
546 let now = std::time::Instant::now();
548 self.collector_state = Some(CollectorState {
549 collector_type,
550 var_name,
551 config: config.clone(),
552 buffer: String::new(),
553 start_time: now,
554 last_digit_time: now,
555 retry_count: retry_count + 1,
556 });
557
558 let error_msg = config
560 .validation
561 .as_ref()
562 .and_then(|v| v.error_message.clone())
563 .unwrap_or_else(|| reason.to_string());
564
565 Ok(vec![self.create_tts_command(error_msg, None, None)])
566 }
567
568 fn start_collector(&mut self, collector_type: &str, var_name: &str) -> bool {
570 let config = match &self.dtmf_collectors {
571 Some(collectors) => match collectors.get(collector_type) {
572 Some(c) => c.clone(),
573 None => {
574 warn!("Unknown DTMF collector type: {}", collector_type);
575 return false;
576 }
577 },
578 None => {
579 warn!("No DTMF collectors configured");
580 return false;
581 }
582 };
583
584 let now = std::time::Instant::now();
585 self.collector_state = Some(CollectorState {
586 collector_type: collector_type.to_string(),
587 var_name: var_name.to_string(),
588 config,
589 buffer: String::new(),
590 start_time: now,
591 last_digit_time: now,
592 retry_count: 0,
593 });
594
595 info!(
596 "DTMF collector started: type={}, var={}",
597 collector_type, var_name
598 );
599 true
600 }
601
602 pub fn is_collecting(&self) -> bool {
604 self.collector_state.is_some()
605 }
606
607 fn get_dtmf_action(&self, digit: &str) -> Option<super::DtmfAction> {
608 if let Some(scene_id) = &self.current_scene_id {
609 if let Some(scene) = self.scenes.get(scene_id) {
610 if let Some(dtmf) = &scene.dtmf {
611 if let Some(action) = dtmf.get(digit) {
612 return Some(action.clone());
613 }
614 }
615 }
616 }
617
618 if let Some(dtmf) = &self.dtmf_config {
619 if let Some(action) = dtmf.get(digit) {
620 return Some(action.clone());
621 }
622 }
623
624 None
625 }
626
627 async fn handle_dtmf_action(&mut self, action: super::DtmfAction) -> Result<Vec<Command>> {
628 match action {
629 super::DtmfAction::Goto { scene } => {
630 info!("DTMF action: switch to scene {}", scene);
631 self.switch_to_scene(&scene, true).await
632 }
633 super::DtmfAction::Transfer { target } => {
634 info!("DTMF action: transfer to {}", target);
635 Ok(vec![Command::Refer {
636 caller: String::new(),
637 callee: target,
638 options: None,
639 }])
640 }
641 super::DtmfAction::Hangup => {
642 info!("DTMF action: hangup");
643 let headers = self.render_sip_headers().await;
644 Ok(vec![Command::Hangup {
645 reason: Some("DTMF Hangup".to_string()),
646 initiator: Some("ai".to_string()),
647 headers,
648 refer: None,
649 }])
650 }
651 }
652 }
653
654 async fn get_current_extras(&self) -> HashMap<String, serde_json::Value> {
656 if let Some(call) = &self.call {
657 let state = call.call_state.read().await;
658 state.extras.clone().unwrap_or_default()
659 } else {
660 HashMap::new()
661 }
662 }
663
664 async fn render_scene_prompt(&self, scene: &super::Scene) -> String {
667 let extras = self.get_current_extras().await;
668 super::render_scene_prompt(scene, &extras)
669 }
670
671 async fn switch_to_scene(
672 &mut self,
673 scene_id: &str,
674 trigger_response: bool,
675 ) -> Result<Vec<Command>> {
676 if let Some(scene) = self.scenes.get(scene_id).cloned() {
677 info!("Switching to scene: {}", scene_id);
678 self.current_scene_id = Some(scene_id.to_string());
679 let rendered_prompt = self.render_scene_prompt(&scene).await;
681 let system_prompt = Self::build_system_prompt(
682 &self.config,
683 Some(&rendered_prompt),
684 self.dtmf_collectors.as_ref(),
685 );
686 if let Some(first_msg) = self.history.get_mut(0) {
687 if first_msg.role == "system" {
688 first_msg.content = system_prompt;
689 }
690 }
691
692 let mut commands = Vec::new();
693 if let Some(url) = &scene.play {
694 commands.push(Command::Play {
695 url: url.clone(),
696 play_id: None,
697 auto_hangup: None,
698 wait_input_timeout: None,
699 offset_ms: None,
700 });
701 }
702
703 if trigger_response {
704 let response_cmds = self.generate_response().await?;
705 commands.extend(response_cmds);
706 }
707 Ok(commands)
708 } else {
709 warn!("Scene not found: {}", scene_id);
710 Ok(vec![])
711 }
712 }
713
714 pub fn get_history_ref(&self) -> &[ChatMessage] {
715 &self.history
716 }
717
718 pub fn get_current_scene_id(&self) -> Option<String> {
719 self.current_scene_id.clone()
720 }
721
722 pub fn set_call(&mut self, call: crate::call::ActiveCallRef) {
723 self.call = Some(call);
724 }
725
726 pub fn set_event_sender(&mut self, sender: crate::event::EventSender) {
727 self.event_sender = Some(sender.clone());
728 if let Some(greeting) = &self.config.greeting {
729 let _ = sender.send(crate::event::SessionEvent::AddHistory {
730 sender: Some("system".to_string()),
731 timestamp: crate::media::get_timestamp(),
732 speaker: "assistant".to_string(),
733 text: greeting.clone(),
734 });
735 }
736 }
737
738 fn send_debug_event(&self, key: &str, data: serde_json::Value) {
739 if let Some(sender) = &self.event_sender {
740 let timestamp = crate::media::get_timestamp();
741 if key == "llm_response" {
742 if let Some(text) = data.get("response").and_then(|v| v.as_str()) {
743 let _ = sender.send(crate::event::SessionEvent::AddHistory {
744 sender: Some("llm".to_string()),
745 timestamp,
746 speaker: "assistant".to_string(),
747 text: text.to_string(),
748 });
749 }
750 }
751
752 let event = crate::event::SessionEvent::Metrics {
753 timestamp,
754 key: key.to_string(),
755 duration: 0,
756 data,
757 };
758 let _ = sender.send(event);
759 }
760 }
761
762 async fn call_llm(&self) -> Result<String> {
763 self.provider.call(&self.config, &self.history).await
764 }
765
766 fn create_tts_command(
767 &self,
768 text: String,
769 wait_input_timeout: Option<u32>,
770 auto_hangup: Option<bool>,
771 ) -> Command {
772 let timeout = wait_input_timeout.unwrap_or(10000);
773 let play_id = uuid::Uuid::new_v4().to_string();
774
775 if let Some(sender) = &self.event_sender {
776 let _ = sender.send(crate::event::SessionEvent::Metrics {
777 timestamp: crate::media::get_timestamp(),
778 key: "tts_play_id_map".to_string(),
779 duration: 0,
780 data: serde_json::json!({
781 "playId": play_id,
782 "text": text,
783 }),
784 });
785 }
786
787 Command::Tts {
788 text,
789 speaker: None,
790 play_id: Some(play_id),
791 auto_hangup,
792 streaming: None,
793 end_of_stream: Some(true),
794 option: None,
795 wait_input_timeout: Some(timeout),
796 base64: None,
797 cache_key: None,
798 }
799 }
800
801 async fn generate_response(&mut self) -> Result<Vec<Command>> {
802 let start_time = crate::media::get_timestamp();
803 let play_id = uuid::Uuid::new_v4().to_string();
804
805 self.send_debug_event(
807 "llm_call_start",
808 json!({
809 "history_length": self.history.len(),
810 "playId": play_id,
811 }),
812 );
813
814 let mut stream = self
815 .provider
816 .call_stream(&self.config, &self.history)
817 .await?;
818
819 let mut full_content = String::new();
820 let mut full_reasoning = String::new();
821 let mut buffer = String::new();
822 let mut commands = Vec::new();
823 let mut is_json_mode = false;
824 let mut checked_json_mode = false;
825 let mut first_token_time = None;
826
827 while let Some(chunk_result) = stream.next().await {
828 let event = match chunk_result {
829 Ok(c) => c,
830 Err(e) => {
831 warn!("LLM stream error: {}", e);
832 break;
833 }
834 };
835
836 match event {
837 LlmStreamEvent::Reasoning(text) => {
838 full_reasoning.push_str(&text);
839 }
840 LlmStreamEvent::Content(chunk) => {
841 if first_token_time.is_none() && !chunk.trim().is_empty() {
842 first_token_time = Some(crate::media::get_timestamp());
843 }
844
845 full_content.push_str(&chunk);
846 buffer.push_str(&chunk);
847
848 if !checked_json_mode {
849 let trimmed = full_content.trim();
850 if !trimmed.is_empty() {
851 if trimmed.starts_with('{') || trimmed.starts_with('`') {
852 is_json_mode = true;
853 }
854 checked_json_mode = true;
855 }
856 }
857
858 if checked_json_mode && !is_json_mode {
859 let extracted = self
860 .extract_streaming_commands(&mut buffer, &play_id, false)
861 .await;
862 for cmd in extracted {
863 if let Some(call) = &self.call {
864 let _ = call.enqueue_command(cmd).await;
865 } else {
866 commands.push(cmd);
867 }
868 }
869 }
870 }
871 }
872 }
873
874 let end_time = crate::media::get_timestamp();
876 self.send_debug_event(
877 "llm_response",
878 json!({
879 "response": full_content,
880 "reasoning": full_reasoning,
881 "is_json_mode": is_json_mode,
882 "duration": end_time - start_time,
883 "ttfb": first_token_time.map(|t| t - start_time).unwrap_or(0),
884 "playId": play_id,
885 }),
886 );
887
888 if is_json_mode {
889 self.interpret_response(full_content).await
890 } else {
891 let extracted = self
892 .extract_streaming_commands(&mut buffer, &play_id, true)
893 .await;
894 for cmd in extracted {
895 if let Some(call) = &self.call {
896 let _ = call.enqueue_command(cmd).await;
897 } else {
898 commands.push(cmd);
899 }
900 }
901 if !full_content.trim().is_empty() {
902 self.history.push(ChatMessage {
903 role: "assistant".to_string(),
904 content: full_content,
905 });
906 self.last_robot_msg_at = Some(std::time::Instant::now());
907 self.is_speaking = true;
908 self.last_tts_start_at = Some(std::time::Instant::now());
909 }
910 Ok(commands)
911 }
912 }
913
914 async fn extract_streaming_commands(
915 &mut self,
916 buffer: &mut String,
917 play_id: &str,
918 is_final: bool,
919 ) -> Vec<Command> {
920 let mut commands = Vec::new();
921 let mut pending_hangup: Option<(String, usize)> = None; loop {
924 let hangup_pos = RE_HANGUP.find(buffer);
925 let refer_pos = RE_REFER.captures(buffer);
926 let play_pos = RE_PLAY.captures(buffer);
927 let goto_pos = RE_GOTO.captures(buffer);
928 let set_var_pos = RE_SET_VAR.captures(buffer);
929 let http_pos = RE_HTTP.captures(buffer);
930 let collect_pos = RE_COLLECT.captures(buffer);
931 let sentence_pos = RE_SENTENCE.find(buffer);
932
933 let mut positions: Vec<(usize, CommandKind)> = Vec::new();
935 if let Some(m) = hangup_pos {
936 positions.push((m.start(), CommandKind::Hangup));
937 }
938 if let Some(caps) = &refer_pos {
939 positions.push((caps.get(0).unwrap().start(), CommandKind::Refer));
940 }
941 if let Some(caps) = &play_pos {
942 positions.push((caps.get(0).unwrap().start(), CommandKind::Play));
943 }
944 if let Some(caps) = &goto_pos {
945 positions.push((caps.get(0).unwrap().start(), CommandKind::Goto));
946 }
947 if let Some(caps) = &set_var_pos {
948 positions.push((caps.get(0).unwrap().start(), CommandKind::SetVar));
949 }
950 if let Some(caps) = &http_pos {
951 positions.push((caps.get(0).unwrap().start(), CommandKind::Http));
952 }
953 if let Some(caps) = &collect_pos {
954 positions.push((caps.get(0).unwrap().start(), CommandKind::Collect));
955 }
956 if let Some(m) = sentence_pos {
957 positions.push((m.start(), CommandKind::Sentence));
958 }
959
960 positions.sort_by_key(|p| p.0);
961
962 if let Some((pos, kind)) = positions.first() {
963 let pos = *pos;
964 match kind {
965 CommandKind::SetVar => {
966 let caps = RE_SET_VAR.captures(buffer).unwrap();
967 let mat = caps.get(0).unwrap();
968 let key = caps.get(1).unwrap().as_str().to_string();
969 let value = caps.get(2).unwrap().as_str().to_string();
970
971 let prefix = buffer[..pos].to_string();
972 if !prefix.trim().is_empty() {
973 commands.push(self.create_tts_command_with_id(
974 prefix,
975 play_id.to_string(),
976 None,
977 ));
978 }
979
980 if let Some(call) = &self.call {
981 let mut state = call.call_state.write().await;
982 let mut extras = state.extras.take().unwrap_or_default();
983 extras.insert(key, serde_json::Value::String(value));
984 state.extras = Some(extras);
985 }
986
987 buffer.drain(..mat.end());
988 }
989 CommandKind::Http => {
990 let caps = RE_HTTP.captures(buffer).unwrap();
991 let mat = caps.get(0).unwrap();
992 let url = caps.get(1).unwrap().as_str().to_string();
993 let method = caps
994 .get(2)
995 .map(|m| m.as_str().to_string())
996 .unwrap_or("GET".to_string());
997 let body = caps.get(3).map(|m| m.as_str().to_string());
998
999 let prefix = buffer[..pos].to_string();
1001 if !prefix.trim().is_empty() {
1002 commands.push(self.create_tts_command_with_id(
1003 prefix,
1004 play_id.to_string(),
1005 None,
1006 ));
1007 }
1008
1009 let client = self.client.clone();
1011 let mut req = match method.to_uppercase().as_str() {
1012 "POST" => client.post(&url),
1013 "PUT" => client.put(&url),
1014 _ => client.get(&url),
1015 };
1016
1017 if let Some(b) = body {
1018 req = req.body(b);
1019 }
1020
1021 match req.send().await {
1023 Ok(res) => {
1024 let status = res.status();
1025 let text = res.text().await.unwrap_or_default();
1026 info!(url, method, status=?status, "HTTP command executed from stream");
1027
1028 self.history.push(ChatMessage {
1030 role: "system".to_string(),
1031 content: format!(
1032 "HTTP {} {} returned ({}): {}",
1033 method, url, status, text
1034 ),
1035 });
1036 }
1037 Err(e) => {
1038 warn!(
1039 url,
1040 method, "Failed to execute HTTP command from stream: {}", e
1041 );
1042
1043 self.history.push(ChatMessage {
1045 role: "system".to_string(),
1046 content: format!("HTTP {} {} failed: {}", method, url, e),
1047 });
1048 }
1049 }
1050
1051 buffer.drain(..mat.end());
1052 }
1053 CommandKind::Hangup => {
1054 let prefix = buffer[..pos].to_string();
1057 let hangup_match = RE_HANGUP.find(buffer).unwrap();
1058 pending_hangup = Some((prefix, hangup_match.end()));
1059 buffer.drain(..hangup_match.end());
1060
1061 }
1064 CommandKind::Refer => {
1065 let caps = RE_REFER.captures(buffer).unwrap();
1066 let mat = caps.get(0).unwrap();
1067 let callee = caps.get(1).unwrap().as_str().to_string();
1068
1069 let prefix = buffer[..pos].to_string();
1070 if !prefix.trim().is_empty() {
1071 commands.push(self.create_tts_command_with_id(
1072 prefix,
1073 play_id.to_string(),
1074 None,
1075 ));
1076 }
1077 commands.push(Command::Refer {
1078 caller: String::new(),
1079 callee,
1080 options: None,
1081 });
1082 buffer.drain(..mat.end());
1083 }
1084 CommandKind::Play => {
1085 let caps = RE_PLAY.captures(buffer).unwrap();
1087 let mat = caps.get(0).unwrap();
1088 let url = caps.get(1).unwrap().as_str().to_string();
1089
1090 let prefix = buffer[..pos].to_string();
1091 if !prefix.trim().is_empty() {
1092 commands.push(self.create_tts_command_with_id(
1093 prefix,
1094 play_id.to_string(),
1095 None,
1096 ));
1097 }
1098 commands.push(Command::Play {
1099 url,
1100 play_id: None,
1101 auto_hangup: None,
1102 wait_input_timeout: None,
1103 offset_ms: None,
1104 });
1105 buffer.drain(..mat.end());
1106 }
1107 CommandKind::Goto => {
1108 let caps = RE_GOTO.captures(buffer).unwrap();
1110 let mat = caps.get(0).unwrap();
1111 let scene_id = caps.get(1).unwrap().as_str().to_string();
1112
1113 let prefix = buffer[..pos].to_string();
1114 if !prefix.trim().is_empty() {
1115 commands.push(self.create_tts_command_with_id(
1116 prefix,
1117 play_id.to_string(),
1118 None,
1119 ));
1120 }
1121
1122 info!("Switching to scene (from stream): {}", scene_id);
1123 if let Some(scene) = self.scenes.get(&scene_id).cloned() {
1124 self.current_scene_id = Some(scene_id);
1125 let rendered_prompt = self.render_scene_prompt(&scene).await;
1127 let system_prompt = Self::build_system_prompt(
1129 &self.config,
1130 Some(&rendered_prompt),
1131 self.dtmf_collectors.as_ref(),
1132 );
1133 if let Some(first_msg) = self.history.get_mut(0) {
1134 if first_msg.role == "system" {
1135 first_msg.content = system_prompt;
1136 }
1137 }
1138 } else {
1139 warn!("Scene not found: {}", scene_id);
1140 }
1141
1142 buffer.drain(..mat.end());
1143 }
1144 CommandKind::Collect => {
1145 let caps = RE_COLLECT.captures(buffer).unwrap();
1146 let mat = caps.get(0).unwrap();
1147 let collector_type = caps.get(1).unwrap().as_str().to_string();
1148 let var_name = caps.get(2).unwrap().as_str().to_string();
1149 let prompt = caps.get(3).map(|m| m.as_str().to_string());
1150
1151 let prefix = buffer[..pos].to_string();
1153 if !prefix.trim().is_empty() {
1154 commands.push(self.create_tts_command_with_id(
1155 prefix,
1156 play_id.to_string(),
1157 None,
1158 ));
1159 }
1160
1161 if let Some(p) = prompt {
1163 if !p.trim().is_empty() {
1164 commands.push(self.create_tts_command(p, None, None));
1165 }
1166 }
1167
1168 if !self.start_collector(&collector_type, &var_name) {
1170 self.history.push(ChatMessage {
1172 role: "system".to_string(),
1173 content: format!(
1174 "[Unknown DTMF collector type '{}'. Available types: {}]",
1175 collector_type,
1176 self.dtmf_collectors
1177 .as_ref()
1178 .map(|c| c.keys().cloned().collect::<Vec<_>>().join(", "))
1179 .unwrap_or_default()
1180 ),
1181 });
1182 }
1183
1184 buffer.drain(..mat.end());
1185 }
1186 CommandKind::Sentence => {
1187 let mat = sentence_pos.unwrap();
1189 let sentence = buffer[..mat.end()].to_string();
1190 if !sentence.trim().is_empty() {
1191 commands.push(self.create_tts_command_with_id(
1192 sentence,
1193 play_id.to_string(),
1194 None,
1195 ));
1196 }
1197 buffer.drain(..mat.end());
1198 }
1199 }
1200 } else {
1201 break;
1202 }
1203 }
1204
1205 if let Some((prefix, _)) = pending_hangup {
1207 let headers = self.render_sip_headers().await;
1208
1209 if let Some(call) = &self.call {
1210 let h_val = serde_json::to_value(&headers).unwrap_or_default();
1211 let mut state = call.call_state.write().await;
1212 let mut extras = state.extras.take().unwrap_or_default();
1213 extras.insert("_hangup_headers".to_string(), h_val);
1214 state.extras = Some(extras);
1215 }
1216
1217 if !prefix.trim().is_empty() {
1218 let mut cmd =
1219 self.create_tts_command_with_id(prefix, play_id.to_string(), Some(true));
1220 if let Command::Tts { end_of_stream, .. } = &mut cmd {
1221 *end_of_stream = Some(true);
1222 }
1223 self.is_hanging_up = true;
1224 commands.push(cmd);
1225 } else {
1226 let mut cmd = self.create_tts_command_with_id(
1227 "".to_string(),
1228 play_id.to_string(),
1229 Some(true),
1230 );
1231 if let Command::Tts { end_of_stream, .. } = &mut cmd {
1232 *end_of_stream = Some(true);
1233 }
1234 self.is_hanging_up = true;
1235 commands.push(cmd);
1236 }
1237
1238 return commands;
1239 }
1240
1241 if is_final {
1242 let remaining = buffer.trim().to_string();
1243 if !remaining.is_empty() {
1244 commands.push(self.create_tts_command_with_id(
1245 remaining,
1246 play_id.to_string(),
1247 None,
1248 ));
1249 }
1250 buffer.clear();
1251
1252 if let Some(last) = commands.last_mut() {
1253 if let Command::Tts { end_of_stream, .. } = last {
1254 *end_of_stream = Some(true);
1255 }
1256 } else if !self.is_hanging_up {
1257 commands.push(Command::Tts {
1258 text: "".to_string(),
1259 speaker: None,
1260 play_id: Some(play_id.to_string()),
1261 auto_hangup: None,
1262 streaming: Some(true),
1263 end_of_stream: Some(true),
1264 option: None,
1265 wait_input_timeout: None,
1266 base64: None,
1267 cache_key: None,
1268 });
1269 }
1270 }
1271
1272 commands
1273 }
1274
1275 fn create_tts_command_with_id(
1276 &self,
1277 text: String,
1278 play_id: String,
1279 auto_hangup: Option<bool>,
1280 ) -> Command {
1281 Command::Tts {
1282 text,
1283 speaker: None,
1284 play_id: Some(play_id),
1285 auto_hangup,
1286 streaming: Some(true),
1287 end_of_stream: None,
1288 option: None,
1289 wait_input_timeout: Some(10000),
1290 base64: None,
1291 cache_key: None,
1292 }
1293 }
1294
1295 async fn handle_tool_invocation(
1296 &mut self,
1297 tool: ToolInvocation,
1298 tool_commands: &mut Vec<Command>,
1299 ) -> Result<bool> {
1300 match tool {
1301 ToolInvocation::Hangup {
1302 ref reason,
1303 ref initiator,
1304 } => {
1305 self.send_debug_event(
1306 "tool_invocation",
1307 json!({
1308 "tool": "Hangup",
1309 "params": {
1310 "reason": reason,
1311 "initiator": initiator,
1312 }
1313 }),
1314 );
1315
1316 let headers = self.render_sip_headers().await;
1317
1318 tool_commands.push(Command::Hangup {
1319 reason: reason.clone(),
1320 initiator: initiator.clone(),
1321 headers,
1322 refer: None,
1323 });
1324 Ok(false)
1325 }
1326 ToolInvocation::Refer {
1327 ref caller,
1328 ref callee,
1329 ref options,
1330 } => {
1331 self.send_debug_event(
1332 "tool_invocation",
1333 json!({
1334 "tool": "Refer",
1335 "params": {
1336 "caller": caller,
1337 "callee": callee,
1338 }
1339 }),
1340 );
1341 tool_commands.push(Command::Refer {
1342 caller: caller.clone(),
1343 callee: callee.clone(),
1344 options: options.clone(),
1345 });
1346 Ok(false)
1347 }
1348 ToolInvocation::Rag {
1349 ref query,
1350 ref source,
1351 } => {
1352 self.handle_rag_tool(query, source).await?;
1353 Ok(true)
1354 }
1355 ToolInvocation::Accept { ref options } => {
1356 self.send_debug_event("tool_invocation", json!({ "tool": "Accept" }));
1357 tool_commands.push(Command::Accept {
1358 option: options.clone().unwrap_or_default(),
1359 });
1360 Ok(false)
1361 }
1362 ToolInvocation::Reject { ref reason, code } => {
1363 self.send_debug_event(
1364 "tool_invocation",
1365 json!({
1366 "tool": "Reject",
1367 "params": {
1368 "reason": reason,
1369 "code": code,
1370 }
1371 }),
1372 );
1373 tool_commands.push(Command::Reject {
1374 reason: reason
1375 .clone()
1376 .unwrap_or_else(|| "Rejected by agent".to_string()),
1377 code,
1378 });
1379 Ok(false)
1380 }
1381 ToolInvocation::Http {
1382 ref url,
1383 ref method,
1384 ref body,
1385 ref headers,
1386 } => {
1387 self.handle_http_tool(url, method, body, headers).await?;
1388 Ok(true)
1389 }
1390 }
1391 }
1392
1393 async fn render_sip_headers(&self) -> Option<HashMap<String, String>> {
1394 let hangup_template = self.sip_config.as_ref()?.hangup_headers.as_ref()?;
1395 let call = self.call.as_ref()?;
1396 let state = call.call_state.read().await;
1397
1398 let mut context = HashMap::new();
1399 let mut sip_headers = HashMap::new();
1400
1401 let sip_header_keys: Vec<String> = state
1404 .extras
1405 .as_ref()
1406 .and_then(|e| e.get("_sip_header_keys"))
1407 .and_then(|v| serde_json::from_value(v.clone()).ok())
1408 .unwrap_or_default();
1409
1410 if let Some(extras) = &state.extras {
1411 for (k, v) in extras {
1412 if k.starts_with('_') {
1414 continue;
1415 }
1416 context.insert(k.clone(), v.clone());
1417 if sip_header_keys.contains(k) {
1419 sip_headers.insert(k.clone(), v.clone());
1420 }
1421 }
1422 }
1423
1424 context.insert(
1426 "sip".to_string(),
1427 serde_json::to_value(&sip_headers).unwrap_or(serde_json::Value::Null),
1428 );
1429
1430 let env = minijinja::Environment::new();
1431 let mut rendered_headers = HashMap::new();
1432 for (k, v) in hangup_template {
1433 if let Ok(rendered) = env.render_str(v, &context) {
1434 rendered_headers.insert(k.clone(), rendered);
1435 } else {
1436 rendered_headers.insert(k.clone(), v.clone());
1437 }
1438 }
1439 Some(rendered_headers)
1440 }
1441
1442 async fn handle_rag_tool(&mut self, query: &str, source: &Option<String>) -> Result<()> {
1443 self.send_debug_event(
1444 "tool_invocation",
1445 json!({
1446 "tool": "Rag",
1447 "params": {
1448 "query": query,
1449 "source": source,
1450 }
1451 }),
1452 );
1453
1454 let rag_result = self.rag_retriever.retrieve(query).await?;
1455
1456 self.send_debug_event(
1457 "rag_result",
1458 json!({
1459 "query": query,
1460 "result": rag_result,
1461 }),
1462 );
1463
1464 let summary = if let Some(source) = source {
1465 format!("[{}] {}", source, rag_result)
1466 } else {
1467 rag_result
1468 };
1469
1470 self.history.push(ChatMessage {
1471 role: "system".to_string(),
1472 content: format!("RAG result for {}: {}", query, summary),
1473 });
1474
1475 Ok(())
1476 }
1477
1478 async fn handle_http_tool(
1479 &mut self,
1480 url: &str,
1481 method: &Option<String>,
1482 body: &Option<serde_json::Value>,
1483 headers: &Option<HashMap<String, String>>,
1484 ) -> Result<()> {
1485 let method_str = method.as_deref().unwrap_or("GET").to_uppercase();
1486 let method =
1487 reqwest::Method::from_bytes(method_str.as_bytes()).unwrap_or(reqwest::Method::GET);
1488
1489 self.send_debug_event(
1490 "tool_invocation",
1491 json!({
1492 "tool": "Http",
1493 "params": {
1494 "url": url,
1495 "method": method_str,
1496 }
1497 }),
1498 );
1499
1500 let mut req = self.client.request(method, url);
1501 if let Some(body) = body {
1502 req = req.json(body);
1503 }
1504 if let Some(headers) = headers {
1505 for (k, v) in headers {
1506 req = req.header(k, v);
1507 }
1508 }
1509
1510 match req.send().await {
1511 Ok(res) => {
1512 let status = res.status();
1513 let text = res.text().await.unwrap_or_default();
1514 self.history.push(ChatMessage {
1515 role: "system".to_string(),
1516 content: format!(
1517 "HTTP tool response ({}): {}\nThe HTTP request has already completed. Answer the user from this result in natural language; do not emit another http tool call for the same user request.",
1518 status, text
1519 ),
1520 });
1521 }
1522 Err(e) => {
1523 warn!("HTTP tool failed: {}", e);
1524 self.history.push(ChatMessage {
1525 role: "system".to_string(),
1526 content: format!("HTTP tool failed: {}", e),
1527 });
1528 }
1529 }
1530
1531 Ok(())
1532 }
1533
1534 async fn handle_asr_final(&mut self, text: &str) -> Result<Vec<Command>> {
1535 if text.trim().is_empty() {
1536 return Ok(vec![]);
1537 }
1538
1539 self.apply_context_repair(text);
1540 self.apply_rolling_summary().await;
1541
1542 self.last_asr_final_at = Some(std::time::Instant::now());
1543 self.last_interaction_at = std::time::Instant::now();
1544 self.is_speaking = false;
1545 self.consecutive_follow_ups = 0;
1546
1547 self.generate_response().await
1548 }
1549
1550 fn apply_context_repair(&mut self, text: &str) {
1551 let enable_repair = self
1552 .config
1553 .features
1554 .as_ref()
1555 .map(|f| f.contains(&"context_repair".to_string()))
1556 .unwrap_or(false);
1557
1558 if !enable_repair {
1559 self.history.push(ChatMessage {
1560 role: "user".to_string(),
1561 content: text.to_string(),
1562 });
1563 return;
1564 }
1565
1566 let repair_window_ms = self.config.repair_window_ms.unwrap_or(3000) as u128;
1567 let mut merged = false;
1568
1569 if let Some(last_robot_at) = self.last_robot_msg_at {
1570 if last_robot_at.elapsed().as_millis() < repair_window_ms {
1571 if let Some(last_msg) = self.history.last() {
1572 if last_msg.role == "assistant" && last_msg.content.chars().count() < 15 {
1573 info!(
1574 "Context Repair: Detected potential fragmentation. Triggering merge."
1575 );
1576 self.history.pop();
1577 if let Some(prev_user) = self.history.last_mut() {
1578 if prev_user.role == "user" {
1579 prev_user.content.push_str(",");
1580 prev_user.content.push_str(text);
1581 merged = true;
1582 }
1583 }
1584 }
1585 }
1586 }
1587 }
1588
1589 if !merged {
1590 self.history.push(ChatMessage {
1591 role: "user".to_string(),
1592 content: text.to_string(),
1593 });
1594 }
1595 }
1596
1597 async fn apply_rolling_summary(&mut self) {
1598 let enable_summary = self
1599 .config
1600 .features
1601 .as_ref()
1602 .map(|f| f.contains(&"rolling_summary".to_string()))
1603 .unwrap_or(false);
1604
1605 if !enable_summary {
1606 return;
1607 }
1608
1609 let summary_limit = self.config.summary_limit.unwrap_or(20);
1610 if self.history.len() <= summary_limit {
1611 return;
1612 }
1613
1614 info!("Rolling Summary: History limit reached. Triggering background summary.");
1615 let keep_recent = 6;
1616 if self.history.len() <= summary_limit + keep_recent
1617 || self.history.len() <= keep_recent + 1
1618 {
1619 return;
1620 }
1621
1622 let split_idx = self.history.len() - keep_recent;
1623 let to_summarize = self.history[1..split_idx].to_vec();
1624 let recent = self.history[split_idx..].to_vec();
1625
1626 let summary_prompt =
1627 "Summarize the above conversation so far, focusing on key details and user intent.";
1628 let mut summary_req_history = to_summarize;
1629 summary_req_history.push(ChatMessage {
1630 role: "user".to_string(),
1631 content: summary_prompt.to_string(),
1632 });
1633
1634 match self.provider.call(&self.config, &summary_req_history).await {
1635 Ok(summary) => {
1636 let mut new_history = Vec::new();
1637 if let Some(sys) = self.history.first() {
1638 let mut new_sys = sys.clone();
1639 new_sys.content.push_str("\n\n[Previous Context Summary]: ");
1640 new_sys.content.push_str(&summary);
1641 new_history.push(new_sys);
1642 }
1643 new_history.extend(recent);
1644 self.history = new_history;
1645 info!(
1646 "Rolling Summary: Applied summary. New history len: {}",
1647 self.history.len()
1648 );
1649 }
1650 Err(e) => {
1651 warn!("Rolling Summary failed: {}", e);
1652 }
1653 }
1654 }
1655
1656 fn check_interruption(
1657 &mut self,
1658 event: &SessionEvent,
1659 is_filler: &Option<bool>,
1660 ) -> Option<Command> {
1661 let strategy = self.interruption_config.strategy;
1662 let should_check = match (strategy, event) {
1663 (InterruptionStrategy::None, _) => false,
1664 (InterruptionStrategy::Vad, SessionEvent::Speaking { .. }) => true,
1665 (InterruptionStrategy::Asr, SessionEvent::AsrDelta { .. }) => true,
1666 (InterruptionStrategy::Both, _) => true,
1667 _ => false,
1668 };
1669
1670 if !self.is_speaking || self.is_hanging_up || !should_check {
1671 return None;
1672 }
1673
1674 if let Some(last_start) = self.last_tts_start_at {
1676 let ignore_ms = self.interruption_config.ignore_first_ms.unwrap_or(800);
1677 if last_start.elapsed().as_millis() < ignore_ms as u128 {
1678 return None;
1679 }
1680 }
1681
1682 if self.interruption_config.filler_word_filter.unwrap_or(false) {
1684 if let Some(true) = is_filler {
1685 return None;
1686 }
1687 if let SessionEvent::AsrDelta { text, .. } = event {
1688 if is_likely_filler(text) {
1689 return None;
1690 }
1691 }
1692 }
1693
1694 if let Some(last_final) = self.last_asr_final_at {
1696 if last_final.elapsed().as_millis() < 500 {
1697 return None;
1698 }
1699 }
1700
1701 info!("Smart interruption detected, stopping playback");
1702 self.is_speaking = false;
1703 Some(Command::Interrupt {
1704 graceful: Some(true),
1705 fade_out_ms: self.interruption_config.volume_fade_ms,
1706 })
1707 }
1708
1709 async fn handle_silence(&mut self) -> Result<Vec<Command>> {
1710 let follow_up_config = if let Some(scene_id) = &self.current_scene_id {
1711 self.scenes
1712 .get(scene_id)
1713 .and_then(|s| s.follow_up)
1714 .or(self.global_follow_up_config)
1715 } else {
1716 self.global_follow_up_config
1717 };
1718
1719 let Some(config) = follow_up_config else {
1720 return Ok(vec![]);
1721 };
1722
1723 if self.is_speaking
1724 || self.last_interaction_at.elapsed().as_millis() < config.timeout as u128
1725 {
1726 return Ok(vec![]);
1727 }
1728
1729 if self.consecutive_follow_ups >= config.max_count {
1730 info!("Max follow-up count reached, hanging up");
1731 let headers = self.render_sip_headers().await;
1732 return Ok(vec![Command::Hangup {
1733 reason: Some("Max follow-up reached".to_string()),
1734 initiator: Some("system".to_string()),
1735 headers,
1736 refer: None,
1737 }]);
1738 }
1739
1740 info!(
1741 "Silence timeout detected ({}ms), triggering follow-up ({}/{})",
1742 self.last_interaction_at.elapsed().as_millis(),
1743 self.consecutive_follow_ups + 1,
1744 config.max_count
1745 );
1746 self.consecutive_follow_ups += 1;
1747 self.last_interaction_at = std::time::Instant::now();
1748 self.generate_response().await
1749 }
1750
1751 async fn handle_function_call(&mut self, name: &str, arguments: &str) -> Result<Vec<Command>> {
1752 info!(
1753 "Function call from Realtime: {} with args {}",
1754 name, arguments
1755 );
1756 let args: serde_json::Value = serde_json::from_str(arguments).unwrap_or_default();
1757
1758 match name {
1759 "hangup_call" => {
1760 let headers = self.render_sip_headers().await;
1761 Ok(vec![Command::Hangup {
1762 reason: args["reason"].as_str().map(|s| s.to_string()),
1763 initiator: Some("ai".to_string()),
1764 headers,
1765 refer: None,
1766 }])
1767 }
1768 "transfer_call" | "refer_call" => {
1769 if let Some(callee) = args["callee"]
1770 .as_str()
1771 .or_else(|| args["callee_uri"].as_str())
1772 {
1773 Ok(vec![Command::Refer {
1774 caller: String::new(),
1775 callee: callee.to_string(),
1776 options: None,
1777 }])
1778 } else {
1779 warn!("No callee provided for transfer_call");
1780 Ok(vec![])
1781 }
1782 }
1783 "goto_scene" => {
1784 if let Some(scene) = args["scene"].as_str() {
1785 self.switch_to_scene(scene, false).await
1786 } else {
1787 Ok(vec![])
1788 }
1789 }
1790 _ => {
1791 warn!("Unhandled function call: {}", name);
1792 Ok(vec![])
1793 }
1794 }
1795 }
1796
1797 async fn interpret_response(&mut self, initial: String) -> Result<Vec<Command>> {
1798 let mut tool_commands = Vec::new();
1799 let mut wait_input_timeout = None;
1800 let mut attempts = 0;
1801 let mut raw = initial;
1802
1803 let final_text = loop {
1804 attempts += 1;
1805
1806 let Some(structured) = parse_structured_response(&raw) else {
1807 break Some(raw);
1808 };
1809
1810 if wait_input_timeout.is_none() {
1811 wait_input_timeout = structured.wait_input_timeout;
1812 }
1813
1814 let has_tools = structured
1815 .tools
1816 .as_ref()
1817 .map(|tools| !tools.is_empty())
1818 .unwrap_or(false);
1819
1820 if attempts >= MAX_RAG_ATTEMPTS
1821 && has_tools
1822 && structured
1823 .text
1824 .as_ref()
1825 .map(|text| text.trim().is_empty())
1826 .unwrap_or(true)
1827 {
1828 warn!("Reached RAG iteration limit with tool-only response; suppressing raw tool JSON");
1829 break None;
1830 }
1831
1832 let mut rerun_for_rag = false;
1833 if let Some(tools) = structured.tools {
1834 for tool in tools {
1835 let needs_rerun = self
1836 .handle_tool_invocation(tool, &mut tool_commands)
1837 .await?;
1838 rerun_for_rag = rerun_for_rag || needs_rerun;
1839 }
1840 }
1841
1842 if !rerun_for_rag {
1843 break structured.text;
1844 }
1845
1846 if attempts >= MAX_RAG_ATTEMPTS {
1847 warn!("Reached RAG iteration limit, using last response");
1848 break structured.text.or(Some(raw));
1849 }
1850
1851 raw = self.call_llm().await?;
1852 };
1853
1854 let has_hangup = tool_commands
1855 .iter()
1856 .any(|c| matches!(c, Command::Hangup { .. }));
1857 let mut commands = Vec::new();
1858
1859 if let Some(text) = final_text {
1860 if !text.trim().is_empty() {
1861 self.history.push(ChatMessage {
1862 role: "assistant".to_string(),
1863 content: text.clone(),
1864 });
1865 self.last_tts_start_at = Some(std::time::Instant::now());
1866 self.is_speaking = true;
1867
1868 let auto_hangup = has_hangup.then_some(true);
1869 commands.push(self.create_tts_command(text, wait_input_timeout, auto_hangup));
1870
1871 if has_hangup {
1872 tool_commands.retain(|c| !matches!(c, Command::Hangup { .. }));
1873 self.is_hanging_up = true;
1874 }
1875 }
1876 }
1877
1878 commands.extend(tool_commands);
1879 Ok(commands)
1880 }
1881}
1882
1883fn parse_structured_response(raw: &str) -> Option<StructuredResponse> {
1884 let payload = extract_json_block(raw)?;
1885 serde_json::from_str(payload).ok()
1886}
1887
1888fn is_likely_filler(text: &str) -> bool {
1889 let trimmed = text.trim().to_lowercase();
1890 FILLERS.contains(&trimmed)
1891}
1892
1893fn extract_json_block(raw: &str) -> Option<&str> {
1894 let trimmed = raw.trim();
1895 if trimmed.starts_with('`') {
1896 if let Some(end) = trimmed.rfind("```") {
1897 if end <= 3 {
1898 return None;
1899 }
1900 let mut inner = &trimmed[3..end];
1901 inner = inner.trim();
1902 if inner.to_lowercase().starts_with("json") {
1903 if let Some(newline) = inner.find('\n') {
1904 inner = inner[newline + 1..].trim();
1905 } else if inner.len() > 4 {
1906 inner = inner[4..].trim();
1907 } else {
1908 inner = inner.trim();
1909 }
1910 }
1911 return Some(inner);
1912 }
1913 } else if trimmed.starts_with('{') || trimmed.starts_with('[') {
1914 return Some(trimmed);
1915 }
1916 None
1917}
1918
1919#[async_trait]
1920impl DialogueHandler for LlmHandler {
1921 async fn on_start(&mut self) -> Result<Vec<Command>> {
1922 self.last_tts_start_at = Some(std::time::Instant::now());
1923
1924 let mut commands = Vec::new();
1925
1926 if let Some(scene_id) = &self.current_scene_id {
1928 if let Some(scene) = self.scenes.get(scene_id) {
1929 if let Some(audio_file) = &scene.play {
1930 commands.push(Command::Play {
1931 url: audio_file.clone(),
1932 play_id: None,
1933 auto_hangup: None,
1934 wait_input_timeout: None,
1935 offset_ms: None,
1936 });
1937 }
1938 }
1939 }
1940
1941 if let Some(greeting) = &self.config.greeting {
1942 self.is_speaking = true;
1943 commands.push(self.create_tts_command(greeting.clone(), None, None));
1944 return Ok(commands);
1945 }
1946
1947 let response_commands = self.generate_response().await?;
1948 commands.extend(response_commands);
1949 Ok(commands)
1950 }
1951
1952 async fn on_event(&mut self, event: &SessionEvent) -> Result<Vec<Command>> {
1953 if self.collector_state.is_some() {
1955 match event {
1956 SessionEvent::Dtmf { digit, .. } => {
1957 info!("DTMF received (collecting): {}", digit);
1958 return self.handle_collector_digit(digit).await;
1959 }
1960 SessionEvent::Silence { .. } => {
1961 return self.check_collector_timeout().await;
1963 }
1964 SessionEvent::TrackEnd { .. } => {
1965 self.is_speaking = false;
1966 return Ok(vec![]);
1967 }
1968 SessionEvent::TrackStart { .. } => {
1969 self.is_speaking = true;
1970 return Ok(vec![]);
1971 }
1972 SessionEvent::Hangup { .. } => {
1973 self.collector_state = None;
1975 }
1976 SessionEvent::AsrFinal { .. }
1978 | SessionEvent::AsrDelta { .. }
1979 | SessionEvent::Speaking { .. }
1980 | SessionEvent::Eou { .. } => {
1981 let interruptible = self
1982 .collector_state
1983 .as_ref()
1984 .and_then(|s| s.config.interruptible)
1985 .unwrap_or(false);
1986 if !interruptible {
1987 return Ok(vec![]);
1988 }
1989 }
1991 _ => return Ok(vec![]),
1992 }
1993 }
1994
1995 match event {
1996 SessionEvent::Dtmf { digit, .. } => {
1997 info!("DTMF received: {}", digit);
1998 if let Some(action) = self.get_dtmf_action(digit) {
1999 self.handle_dtmf_action(action).await
2000 } else {
2001 Ok(vec![])
2002 }
2003 }
2004 SessionEvent::AsrFinal { text, .. } => self.handle_asr_final(text).await,
2005 SessionEvent::AsrDelta { is_filler, .. } | SessionEvent::Speaking { is_filler, .. } => {
2006 Ok(self
2007 .check_interruption(event, is_filler)
2008 .into_iter()
2009 .collect())
2010 }
2011 SessionEvent::Eou { completed, .. } => {
2012 if *completed && !self.is_speaking {
2013 info!("EOU detected, triggering early response");
2014 self.generate_response().await
2015 } else {
2016 Ok(vec![])
2017 }
2018 }
2019 SessionEvent::Silence { .. } => self.handle_silence().await,
2020 SessionEvent::TrackStart { .. } => {
2021 self.is_speaking = true;
2022 Ok(vec![])
2023 }
2024 SessionEvent::TrackEnd { .. } => {
2025 self.is_speaking = false;
2026 self.is_hanging_up = false;
2027 self.last_interaction_at = std::time::Instant::now();
2028 Ok(vec![])
2029 }
2030 SessionEvent::FunctionCall {
2031 name, arguments, ..
2032 } => self.handle_function_call(name, arguments).await,
2033 _ => Ok(vec![]),
2034 }
2035 }
2036
2037 async fn get_history(&self) -> Vec<ChatMessage> {
2038 self.history.clone()
2039 }
2040
2041 async fn summarize(&mut self, prompt: &str) -> Result<String> {
2042 info!("Generating summary with prompt: {}", prompt);
2043 let mut summary_history = self.history.clone();
2044 summary_history.push(ChatMessage {
2045 role: "user".to_string(),
2046 content: prompt.to_string(),
2047 });
2048
2049 self.provider.call(&self.config, &summary_history).await
2050 }
2051}