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