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 call.set_extra(&var_name, serde_json::Value::String(buffer.clone()));
505 }
506
507 self.history.push(ChatMessage {
509 role: "system".to_string(),
510 content: format!("[DTMF collection completed for '{}': {}]", var_name, buffer),
511 });
512
513 self.generate_response().await
515 }
516
517 async fn retry_or_fail(
519 &mut self,
520 collector_type: String,
521 config: super::DtmfCollectorConfig,
522 retry_count: u32,
523 var_name: String,
524 reason: &str,
525 ) -> Result<Vec<Command>> {
526 let max_retries = config.retry_times.unwrap_or(3);
527
528 if retry_count >= max_retries {
529 info!(
530 "DTMF collector: max retries ({}) reached for var '{}'",
531 max_retries, var_name
532 );
533 self.history.push(ChatMessage {
534 role: "system".to_string(),
535 content: format!(
536 "[DTMF collection failed for '{}' after {} retries: {}. Please guide the user to try again or use an alternative method.]",
537 var_name, max_retries, reason
538 ),
539 });
540 return self.generate_response().await;
541 }
542
543 info!(
544 "DTMF collector: retry {}/{} for var '{}': {}",
545 retry_count + 1,
546 max_retries,
547 var_name,
548 reason
549 );
550
551 let now = std::time::Instant::now();
553 self.collector_state = Some(CollectorState {
554 collector_type,
555 var_name,
556 config: config.clone(),
557 buffer: String::new(),
558 start_time: now,
559 last_digit_time: now,
560 retry_count: retry_count + 1,
561 });
562
563 let error_msg = config
565 .validation
566 .as_ref()
567 .and_then(|v| v.error_message.clone())
568 .unwrap_or_else(|| reason.to_string());
569
570 Ok(vec![self.create_tts_command(error_msg, None, None)])
571 }
572
573 fn start_collector(&mut self, collector_type: &str, var_name: &str) -> bool {
575 let config = match &self.dtmf_collectors {
576 Some(collectors) => match collectors.get(collector_type) {
577 Some(c) => c.clone(),
578 None => {
579 warn!("Unknown DTMF collector type: {}", collector_type);
580 return false;
581 }
582 },
583 None => {
584 warn!("No DTMF collectors configured");
585 return false;
586 }
587 };
588
589 let now = std::time::Instant::now();
590 self.collector_state = Some(CollectorState {
591 collector_type: collector_type.to_string(),
592 var_name: var_name.to_string(),
593 config,
594 buffer: String::new(),
595 start_time: now,
596 last_digit_time: now,
597 retry_count: 0,
598 });
599
600 info!(
601 "DTMF collector started: type={}, var={}",
602 collector_type, var_name
603 );
604 true
605 }
606
607 pub fn is_collecting(&self) -> bool {
609 self.collector_state.is_some()
610 }
611
612 fn get_dtmf_action(&self, digit: &str) -> Option<super::DtmfAction> {
613 if let Some(scene_id) = &self.current_scene_id {
614 if let Some(scene) = self.scenes.get(scene_id) {
615 if let Some(dtmf) = &scene.dtmf {
616 if let Some(action) = dtmf.get(digit) {
617 return Some(action.clone());
618 }
619 }
620 }
621 }
622
623 if let Some(dtmf) = &self.dtmf_config {
624 if let Some(action) = dtmf.get(digit) {
625 return Some(action.clone());
626 }
627 }
628
629 None
630 }
631
632 async fn handle_dtmf_action(&mut self, action: super::DtmfAction) -> Result<Vec<Command>> {
633 match action {
634 super::DtmfAction::Goto { scene } => {
635 info!("DTMF action: switch to scene {}", scene);
636 self.switch_to_scene(&scene, true).await
637 }
638 super::DtmfAction::Transfer { target } => {
639 info!("DTMF action: transfer to {}", target);
640 Ok(vec![Command::Refer {
641 caller: String::new(),
642 callee: target,
643 options: None,
644 }])
645 }
646 super::DtmfAction::Hangup => {
647 info!("DTMF action: hangup");
648 let headers = self.render_sip_headers().await;
649 Ok(vec![Command::Hangup {
650 reason: Some("DTMF Hangup".to_string()),
651 initiator: Some("ai".to_string()),
652 headers,
653 refer: None,
654 }])
655 }
656 }
657 }
658
659 async fn get_current_extras(&self) -> HashMap<String, serde_json::Value> {
661 if let Some(call) = &self.call {
662 call.extras.load_full().as_ref().clone()
663 } else {
664 HashMap::new()
665 }
666 }
667
668 async fn render_scene_prompt(&self, scene: &super::Scene) -> String {
671 let extras = self.get_current_extras().await;
672 super::render_scene_prompt(scene, &extras)
673 }
674
675 async fn switch_to_scene(
676 &mut self,
677 scene_id: &str,
678 trigger_response: bool,
679 ) -> Result<Vec<Command>> {
680 if let Some(scene) = self.scenes.get(scene_id).cloned() {
681 info!("Switching to scene: {}", scene_id);
682 self.current_scene_id = Some(scene_id.to_string());
683 let rendered_prompt = self.render_scene_prompt(&scene).await;
685 let system_prompt = Self::build_system_prompt(
686 &self.config,
687 Some(&rendered_prompt),
688 self.dtmf_collectors.as_ref(),
689 );
690 if let Some(first_msg) = self.history.get_mut(0) {
691 if first_msg.role == "system" {
692 first_msg.content = system_prompt;
693 }
694 }
695
696 let mut commands = Vec::new();
697 if let Some(url) = &scene.play {
698 commands.push(Command::Play {
699 url: url.clone(),
700 play_id: None,
701 auto_hangup: None,
702 wait_input_timeout: None,
703 offset_ms: None,
704 });
705 }
706
707 if trigger_response {
708 let response_cmds = self.generate_response().await?;
709 commands.extend(response_cmds);
710 }
711 Ok(commands)
712 } else {
713 warn!("Scene not found: {}", scene_id);
714 Ok(vec![])
715 }
716 }
717
718 pub fn get_history_ref(&self) -> &[ChatMessage] {
719 &self.history
720 }
721
722 pub fn get_current_scene_id(&self) -> Option<String> {
723 self.current_scene_id.clone()
724 }
725
726 pub fn set_call(&mut self, call: crate::call::ActiveCallRef) {
727 self.call = Some(call);
728 }
729
730 pub fn set_event_sender(&mut self, sender: crate::event::EventSender) {
731 self.event_sender = Some(sender.clone());
732 if let Some(greeting) = &self.config.greeting {
733 let _ = sender.send(crate::event::SessionEvent::AddHistory {
734 sender: Some("system".to_string()),
735 timestamp: crate::media::get_timestamp(),
736 speaker: "assistant".to_string(),
737 text: greeting.clone(),
738 });
739 }
740 }
741
742 fn send_debug_event(&self, key: &str, data: serde_json::Value) {
743 if let Some(sender) = &self.event_sender {
744 let timestamp = crate::media::get_timestamp();
745 if key == "llm_response" {
746 if let Some(text) = data.get("response").and_then(|v| v.as_str()) {
747 let _ = sender.send(crate::event::SessionEvent::AddHistory {
748 sender: Some("llm".to_string()),
749 timestamp,
750 speaker: "assistant".to_string(),
751 text: text.to_string(),
752 });
753 }
754 }
755
756 let event = crate::event::SessionEvent::Metrics {
757 timestamp,
758 key: key.to_string(),
759 duration: 0,
760 data,
761 };
762 let _ = sender.send(event);
763 }
764 }
765
766 async fn call_llm(&self) -> Result<String> {
767 self.provider.call(&self.config, &self.history).await
768 }
769
770 fn create_tts_command(
771 &self,
772 text: String,
773 wait_input_timeout: Option<u32>,
774 auto_hangup: Option<bool>,
775 ) -> Command {
776 let timeout = wait_input_timeout.unwrap_or(10000);
777 let play_id = uuid::Uuid::new_v4().to_string();
778
779 if let Some(sender) = &self.event_sender {
780 let _ = sender.send(crate::event::SessionEvent::Metrics {
781 timestamp: crate::media::get_timestamp(),
782 key: "tts_play_id_map".to_string(),
783 duration: 0,
784 data: serde_json::json!({
785 "playId": play_id,
786 "text": text,
787 }),
788 });
789 }
790
791 Command::Tts {
792 text,
793 speaker: None,
794 play_id: Some(play_id),
795 auto_hangup,
796 streaming: None,
797 end_of_stream: Some(true),
798 option: None,
799 wait_input_timeout: Some(timeout),
800 base64: None,
801 cache_key: None,
802 }
803 }
804
805 async fn generate_response(&mut self) -> Result<Vec<Command>> {
806 let start_time = crate::media::get_timestamp();
807 let play_id = uuid::Uuid::new_v4().to_string();
808
809 self.send_debug_event(
811 "llm_call_start",
812 json!({
813 "history_length": self.history.len(),
814 "playId": play_id,
815 }),
816 );
817
818 let mut stream = self
819 .provider
820 .call_stream(&self.config, &self.history)
821 .await?;
822
823 let mut full_content = String::new();
824 let mut full_reasoning = String::new();
825 let mut buffer = String::new();
826 let mut commands = Vec::new();
827 let mut is_json_mode = false;
828 let mut checked_json_mode = false;
829 let mut first_token_time = None;
830
831 while let Some(chunk_result) = stream.next().await {
832 let event = match chunk_result {
833 Ok(c) => c,
834 Err(e) => {
835 warn!("LLM stream error: {}", e);
836 break;
837 }
838 };
839
840 match event {
841 LlmStreamEvent::Reasoning(text) => {
842 full_reasoning.push_str(&text);
843 }
844 LlmStreamEvent::Content(chunk) => {
845 if first_token_time.is_none() && !chunk.trim().is_empty() {
846 first_token_time = Some(crate::media::get_timestamp());
847 }
848
849 full_content.push_str(&chunk);
850 buffer.push_str(&chunk);
851
852 if !checked_json_mode {
853 let trimmed = full_content.trim();
854 if !trimmed.is_empty() {
855 if trimmed.starts_with('{') || trimmed.starts_with('`') {
856 is_json_mode = true;
857 }
858 checked_json_mode = true;
859 }
860 }
861
862 if checked_json_mode && !is_json_mode {
863 let extracted = self
864 .extract_streaming_commands(&mut buffer, &play_id, false)
865 .await;
866 for cmd in extracted {
867 if let Some(call) = &self.call {
868 let _ = call.enqueue_command(cmd).await;
869 } else {
870 commands.push(cmd);
871 }
872 }
873 }
874 }
875 }
876 }
877
878 let end_time = crate::media::get_timestamp();
880 self.send_debug_event(
881 "llm_response",
882 json!({
883 "response": full_content,
884 "reasoning": full_reasoning,
885 "is_json_mode": is_json_mode,
886 "duration": end_time - start_time,
887 "ttfb": first_token_time.map(|t| t - start_time).unwrap_or(0),
888 "playId": play_id,
889 }),
890 );
891
892 if is_json_mode {
893 self.interpret_response(full_content).await
894 } else {
895 let extracted = self
896 .extract_streaming_commands(&mut buffer, &play_id, true)
897 .await;
898 for cmd in extracted {
899 if let Some(call) = &self.call {
900 let _ = call.enqueue_command(cmd).await;
901 } else {
902 commands.push(cmd);
903 }
904 }
905 if !full_content.trim().is_empty() {
906 self.history.push(ChatMessage {
907 role: "assistant".to_string(),
908 content: full_content,
909 });
910 self.last_robot_msg_at = Some(std::time::Instant::now());
911 self.is_speaking = true;
912 self.last_tts_start_at = Some(std::time::Instant::now());
913 }
914 Ok(commands)
915 }
916 }
917
918 async fn extract_streaming_commands(
919 &mut self,
920 buffer: &mut String,
921 play_id: &str,
922 is_final: bool,
923 ) -> Vec<Command> {
924 let mut commands = Vec::new();
925 let mut pending_hangup: Option<(String, usize)> = None; loop {
928 let hangup_pos = RE_HANGUP.find(buffer);
929 let refer_pos = RE_REFER.captures(buffer);
930 let message_pos = RE_MESSAGE.captures(buffer);
931 let play_pos = RE_PLAY.captures(buffer);
932 let goto_pos = RE_GOTO.captures(buffer);
933 let set_var_pos = RE_SET_VAR.captures(buffer);
934 let http_pos = RE_HTTP.captures(buffer);
935 let collect_pos = RE_COLLECT.captures(buffer);
936 let sentence_pos = RE_SENTENCE.find(buffer);
937
938 let mut positions: Vec<(usize, CommandKind)> = Vec::new();
940 if let Some(m) = hangup_pos {
941 positions.push((m.start(), CommandKind::Hangup));
942 }
943 if let Some(caps) = &refer_pos {
944 positions.push((caps.get(0).unwrap().start(), CommandKind::Refer));
945 }
946 if let Some(caps) = &message_pos {
947 positions.push((caps.get(0).unwrap().start(), CommandKind::Message));
948 }
949 if let Some(caps) = &play_pos {
950 positions.push((caps.get(0).unwrap().start(), CommandKind::Play));
951 }
952 if let Some(caps) = &goto_pos {
953 positions.push((caps.get(0).unwrap().start(), CommandKind::Goto));
954 }
955 if let Some(caps) = &set_var_pos {
956 positions.push((caps.get(0).unwrap().start(), CommandKind::SetVar));
957 }
958 if let Some(caps) = &http_pos {
959 positions.push((caps.get(0).unwrap().start(), CommandKind::Http));
960 }
961 if let Some(caps) = &collect_pos {
962 positions.push((caps.get(0).unwrap().start(), CommandKind::Collect));
963 }
964 if let Some(m) = sentence_pos {
965 positions.push((m.start(), CommandKind::Sentence));
966 }
967
968 positions.sort_by_key(|p| p.0);
969
970 if let Some((pos, kind)) = positions.first() {
971 let pos = *pos;
972 match kind {
973 CommandKind::SetVar => {
974 let caps = RE_SET_VAR.captures(buffer).unwrap();
975 let mat = caps.get(0).unwrap();
976 let key = caps.get(1).unwrap().as_str().to_string();
977 let value = caps.get(2).unwrap().as_str().to_string();
978
979 let prefix = buffer[..pos].to_string();
980 if !prefix.trim().is_empty() {
981 commands.push(self.create_tts_command_with_id(
982 prefix,
983 play_id.to_string(),
984 None,
985 ));
986 }
987
988 if let Some(call) = &self.call {
989 call.set_extra(&key, serde_json::Value::String(value));
990 }
991
992 buffer.drain(..mat.end());
993 }
994 CommandKind::Http => {
995 let caps = RE_HTTP.captures(buffer).unwrap();
996 let mat = caps.get(0).unwrap();
997 let url = caps.get(1).unwrap().as_str().to_string();
998 let method = caps
999 .get(2)
1000 .map(|m| m.as_str().to_string())
1001 .unwrap_or("GET".to_string());
1002 let body = caps.get(3).map(|m| m.as_str().to_string());
1003
1004 let prefix = buffer[..pos].to_string();
1006 if !prefix.trim().is_empty() {
1007 commands.push(self.create_tts_command_with_id(
1008 prefix,
1009 play_id.to_string(),
1010 None,
1011 ));
1012 }
1013
1014 let client = self.client.clone();
1016 let mut req = match method.to_uppercase().as_str() {
1017 "POST" => client.post(&url),
1018 "PUT" => client.put(&url),
1019 _ => client.get(&url),
1020 };
1021
1022 if let Some(b) = body {
1023 req = req.body(b);
1024 }
1025
1026 match req.send().await {
1028 Ok(res) => {
1029 let status = res.status();
1030 let text = res.text().await.unwrap_or_default();
1031 info!(url, method, status=?status, "HTTP command executed from stream");
1032
1033 self.history.push(ChatMessage {
1035 role: "system".to_string(),
1036 content: format!(
1037 "HTTP {} {} returned ({}): {}",
1038 method, url, status, text
1039 ),
1040 });
1041 }
1042 Err(e) => {
1043 warn!(
1044 url,
1045 method, "Failed to execute HTTP command from stream: {}", e
1046 );
1047
1048 self.history.push(ChatMessage {
1050 role: "system".to_string(),
1051 content: format!("HTTP {} {} failed: {}", method, url, e),
1052 });
1053 }
1054 }
1055
1056 buffer.drain(..mat.end());
1057 }
1058 CommandKind::Hangup => {
1059 let prefix = buffer[..pos].to_string();
1062 let hangup_match = RE_HANGUP.find(buffer).unwrap();
1063 pending_hangup = Some((prefix, hangup_match.end()));
1064 buffer.drain(..hangup_match.end());
1065
1066 }
1069 CommandKind::Refer => {
1070 let caps = RE_REFER.captures(buffer).unwrap();
1071 let mat = caps.get(0).unwrap();
1072 let callee = caps.get(1).unwrap().as_str().to_string();
1073
1074 let prefix = buffer[..pos].to_string();
1075 if !prefix.trim().is_empty() {
1076 commands.push(self.create_tts_command_with_id(
1077 prefix,
1078 play_id.to_string(),
1079 None,
1080 ));
1081 }
1082 commands.push(Command::Refer {
1083 caller: String::new(),
1084 callee,
1085 options: None,
1086 });
1087 buffer.drain(..mat.end());
1088 }
1089 CommandKind::Message => {
1090 let caps = RE_MESSAGE.captures(buffer).unwrap();
1091 let mat = caps.get(0).unwrap();
1092 let body = caps.get(1).unwrap().as_str().to_string();
1093 let content_type = caps.get(2).map(|m| m.as_str().to_string());
1094 let refer = caps.get(3).map(|m| m.as_str() == "true");
1095
1096 let prefix = buffer[..pos].to_string();
1097 if !prefix.trim().is_empty() {
1098 commands.push(self.create_tts_command_with_id(
1099 prefix,
1100 play_id.to_string(),
1101 None,
1102 ));
1103 }
1104 commands.push(Command::Message {
1105 body,
1106 content_type,
1107 headers: None,
1108 refer,
1109 });
1110 buffer.drain(..mat.end());
1111 }
1112 CommandKind::Play => {
1113 let caps = RE_PLAY.captures(buffer).unwrap();
1115 let mat = caps.get(0).unwrap();
1116 let url = caps.get(1).unwrap().as_str().to_string();
1117
1118 let prefix = buffer[..pos].to_string();
1119 if !prefix.trim().is_empty() {
1120 commands.push(self.create_tts_command_with_id(
1121 prefix,
1122 play_id.to_string(),
1123 None,
1124 ));
1125 }
1126 commands.push(Command::Play {
1127 url,
1128 play_id: None,
1129 auto_hangup: None,
1130 wait_input_timeout: None,
1131 offset_ms: None,
1132 });
1133 buffer.drain(..mat.end());
1134 }
1135 CommandKind::Goto => {
1136 let caps = RE_GOTO.captures(buffer).unwrap();
1138 let mat = caps.get(0).unwrap();
1139 let scene_id = caps.get(1).unwrap().as_str().to_string();
1140
1141 let prefix = buffer[..pos].to_string();
1142 if !prefix.trim().is_empty() {
1143 commands.push(self.create_tts_command_with_id(
1144 prefix,
1145 play_id.to_string(),
1146 None,
1147 ));
1148 }
1149
1150 info!("Switching to scene (from stream): {}", scene_id);
1151 if let Some(scene) = self.scenes.get(&scene_id).cloned() {
1152 self.current_scene_id = Some(scene_id);
1153 let rendered_prompt = self.render_scene_prompt(&scene).await;
1155 let system_prompt = Self::build_system_prompt(
1157 &self.config,
1158 Some(&rendered_prompt),
1159 self.dtmf_collectors.as_ref(),
1160 );
1161 if let Some(first_msg) = self.history.get_mut(0) {
1162 if first_msg.role == "system" {
1163 first_msg.content = system_prompt;
1164 }
1165 }
1166 } else {
1167 warn!("Scene not found: {}", scene_id);
1168 }
1169
1170 buffer.drain(..mat.end());
1171 }
1172 CommandKind::Collect => {
1173 let caps = RE_COLLECT.captures(buffer).unwrap();
1174 let mat = caps.get(0).unwrap();
1175 let collector_type = caps.get(1).unwrap().as_str().to_string();
1176 let var_name = caps.get(2).unwrap().as_str().to_string();
1177 let prompt = caps.get(3).map(|m| m.as_str().to_string());
1178
1179 let prefix = buffer[..pos].to_string();
1181 if !prefix.trim().is_empty() {
1182 commands.push(self.create_tts_command_with_id(
1183 prefix,
1184 play_id.to_string(),
1185 None,
1186 ));
1187 }
1188
1189 if let Some(p) = prompt {
1191 if !p.trim().is_empty() {
1192 commands.push(self.create_tts_command(p, None, None));
1193 }
1194 }
1195
1196 if !self.start_collector(&collector_type, &var_name) {
1198 self.history.push(ChatMessage {
1200 role: "system".to_string(),
1201 content: format!(
1202 "[Unknown DTMF collector type '{}'. Available types: {}]",
1203 collector_type,
1204 self.dtmf_collectors
1205 .as_ref()
1206 .map(|c| c.keys().cloned().collect::<Vec<_>>().join(", "))
1207 .unwrap_or_default()
1208 ),
1209 });
1210 }
1211
1212 buffer.drain(..mat.end());
1213 }
1214 CommandKind::Sentence => {
1215 let mat = sentence_pos.unwrap();
1217 let sentence = buffer[..mat.end()].to_string();
1218 if !sentence.trim().is_empty() {
1219 commands.push(self.create_tts_command_with_id(
1220 sentence,
1221 play_id.to_string(),
1222 None,
1223 ));
1224 }
1225 buffer.drain(..mat.end());
1226 }
1227 }
1228 } else {
1229 break;
1230 }
1231 }
1232
1233 if let Some((prefix, _)) = pending_hangup {
1235 let headers = self.render_sip_headers().await;
1236
1237 if let Some(call) = &self.call {
1238 let h_val = serde_json::to_value(&headers).unwrap_or_default();
1239 call.set_extra("_hangup_headers", h_val);
1240 }
1241
1242 if !prefix.trim().is_empty() {
1243 let mut cmd =
1244 self.create_tts_command_with_id(prefix, play_id.to_string(), Some(true));
1245 if let Command::Tts { end_of_stream, .. } = &mut cmd {
1246 *end_of_stream = Some(true);
1247 }
1248 self.is_hanging_up = true;
1249 commands.push(cmd);
1250 } else {
1251 let mut cmd = self.create_tts_command_with_id(
1252 "".to_string(),
1253 play_id.to_string(),
1254 Some(true),
1255 );
1256 if let Command::Tts { end_of_stream, .. } = &mut cmd {
1257 *end_of_stream = Some(true);
1258 }
1259 self.is_hanging_up = true;
1260 commands.push(cmd);
1261 }
1262
1263 return commands;
1264 }
1265
1266 if is_final {
1267 let remaining = buffer.trim().to_string();
1268 if !remaining.is_empty() {
1269 commands.push(self.create_tts_command_with_id(
1270 remaining,
1271 play_id.to_string(),
1272 None,
1273 ));
1274 }
1275 buffer.clear();
1276
1277 if let Some(last) = commands.last_mut() {
1278 if let Command::Tts { end_of_stream, .. } = last {
1279 *end_of_stream = Some(true);
1280 }
1281 } else if !self.is_hanging_up {
1282 commands.push(Command::Tts {
1283 text: "".to_string(),
1284 speaker: None,
1285 play_id: Some(play_id.to_string()),
1286 auto_hangup: None,
1287 streaming: Some(true),
1288 end_of_stream: Some(true),
1289 option: None,
1290 wait_input_timeout: None,
1291 base64: None,
1292 cache_key: None,
1293 });
1294 }
1295 }
1296
1297 commands
1298 }
1299
1300 fn create_tts_command_with_id(
1301 &self,
1302 text: String,
1303 play_id: String,
1304 auto_hangup: Option<bool>,
1305 ) -> Command {
1306 Command::Tts {
1307 text,
1308 speaker: None,
1309 play_id: Some(play_id),
1310 auto_hangup,
1311 streaming: Some(true),
1312 end_of_stream: None,
1313 option: None,
1314 wait_input_timeout: Some(10000),
1315 base64: None,
1316 cache_key: None,
1317 }
1318 }
1319
1320 async fn handle_tool_invocation(
1321 &mut self,
1322 tool: ToolInvocation,
1323 tool_commands: &mut Vec<Command>,
1324 ) -> Result<bool> {
1325 match tool {
1326 ToolInvocation::Hangup {
1327 ref reason,
1328 ref initiator,
1329 } => {
1330 self.send_debug_event(
1331 "tool_invocation",
1332 json!({
1333 "tool": "Hangup",
1334 "params": {
1335 "reason": reason,
1336 "initiator": initiator,
1337 }
1338 }),
1339 );
1340
1341 let headers = self.render_sip_headers().await;
1342
1343 tool_commands.push(Command::Hangup {
1344 reason: reason.clone(),
1345 initiator: initiator.clone(),
1346 headers,
1347 refer: None,
1348 });
1349 Ok(false)
1350 }
1351 ToolInvocation::Refer {
1352 ref caller,
1353 ref callee,
1354 ref options,
1355 } => {
1356 self.send_debug_event(
1357 "tool_invocation",
1358 json!({
1359 "tool": "Refer",
1360 "params": {
1361 "caller": caller,
1362 "callee": callee,
1363 }
1364 }),
1365 );
1366 tool_commands.push(Command::Refer {
1367 caller: caller.clone(),
1368 callee: callee.clone(),
1369 options: options.clone(),
1370 });
1371 Ok(false)
1372 }
1373 ToolInvocation::Rag {
1374 ref query,
1375 ref source,
1376 } => {
1377 self.handle_rag_tool(query, source).await?;
1378 Ok(true)
1379 }
1380 ToolInvocation::Accept { ref options } => {
1381 self.send_debug_event("tool_invocation", json!({ "tool": "Accept" }));
1382 tool_commands.push(Command::Accept {
1383 option: options.clone().unwrap_or_default(),
1384 });
1385 Ok(false)
1386 }
1387 ToolInvocation::Reject { ref reason, code } => {
1388 self.send_debug_event(
1389 "tool_invocation",
1390 json!({
1391 "tool": "Reject",
1392 "params": {
1393 "reason": reason,
1394 "code": code,
1395 }
1396 }),
1397 );
1398 tool_commands.push(Command::Reject {
1399 reason: reason
1400 .clone()
1401 .unwrap_or_else(|| "Rejected by agent".to_string()),
1402 code,
1403 });
1404 Ok(false)
1405 }
1406 ToolInvocation::Http {
1407 ref url,
1408 ref method,
1409 ref body,
1410 ref headers,
1411 } => {
1412 self.handle_http_tool(url, method, body, headers).await?;
1413 Ok(true)
1414 }
1415 }
1416 }
1417
1418 async fn render_sip_headers(&self) -> Option<HashMap<String, String>> {
1419 let hangup_template = self.sip_config.as_ref()?.hangup_headers.as_ref()?;
1420 let call = self.call.as_ref()?;
1421 let extras = call.extras.load_full();
1422
1423 let mut context = HashMap::new();
1424 let mut sip_headers = HashMap::new();
1425
1426 let sip_header_keys: Vec<String> = extras
1429 .get("_sip_header_keys")
1430 .and_then(|v| serde_json::from_value(v.clone()).ok())
1431 .unwrap_or_default();
1432
1433 for (k, v) in extras.iter() {
1434 if k.starts_with('_') {
1436 continue;
1437 }
1438 context.insert(k.clone(), v.clone());
1439 if sip_header_keys.contains(k) {
1441 sip_headers.insert(k.clone(), v.clone());
1442 }
1443 }
1444
1445 context.insert(
1447 "sip".to_string(),
1448 serde_json::to_value(&sip_headers).unwrap_or(serde_json::Value::Null),
1449 );
1450
1451 let env = minijinja::Environment::new();
1452 let mut rendered_headers = HashMap::new();
1453 for (k, v) in hangup_template {
1454 if let Ok(rendered) = env.render_str(v, &context) {
1455 rendered_headers.insert(k.clone(), rendered);
1456 } else {
1457 rendered_headers.insert(k.clone(), v.clone());
1458 }
1459 }
1460 Some(rendered_headers)
1461 }
1462
1463 async fn handle_rag_tool(&mut self, query: &str, source: &Option<String>) -> Result<()> {
1464 self.send_debug_event(
1465 "tool_invocation",
1466 json!({
1467 "tool": "Rag",
1468 "params": {
1469 "query": query,
1470 "source": source,
1471 }
1472 }),
1473 );
1474
1475 let rag_result = self.rag_retriever.retrieve(query).await?;
1476
1477 self.send_debug_event(
1478 "rag_result",
1479 json!({
1480 "query": query,
1481 "result": rag_result,
1482 }),
1483 );
1484
1485 let summary = if let Some(source) = source {
1486 format!("[{}] {}", source, rag_result)
1487 } else {
1488 rag_result
1489 };
1490
1491 self.history.push(ChatMessage {
1492 role: "system".to_string(),
1493 content: format!("RAG result for {}: {}", query, summary),
1494 });
1495
1496 Ok(())
1497 }
1498
1499 async fn handle_http_tool(
1500 &mut self,
1501 url: &str,
1502 method: &Option<String>,
1503 body: &Option<serde_json::Value>,
1504 headers: &Option<HashMap<String, String>>,
1505 ) -> Result<()> {
1506 let method_str = method.as_deref().unwrap_or("GET").to_uppercase();
1507 let method =
1508 reqwest::Method::from_bytes(method_str.as_bytes()).unwrap_or(reqwest::Method::GET);
1509
1510 self.send_debug_event(
1511 "tool_invocation",
1512 json!({
1513 "tool": "Http",
1514 "params": {
1515 "url": url,
1516 "method": method_str,
1517 }
1518 }),
1519 );
1520
1521 let mut req = self.client.request(method, url);
1522 if let Some(body) = body {
1523 req = req.json(body);
1524 }
1525 if let Some(headers) = headers {
1526 for (k, v) in headers {
1527 req = req.header(k, v);
1528 }
1529 }
1530
1531 match req.send().await {
1532 Ok(res) => {
1533 let status = res.status();
1534 let text = res.text().await.unwrap_or_default();
1535 self.history.push(ChatMessage {
1536 role: "system".to_string(),
1537 content: format!(
1538 "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.",
1539 status, text
1540 ),
1541 });
1542 }
1543 Err(e) => {
1544 warn!("HTTP tool failed: {}", e);
1545 self.history.push(ChatMessage {
1546 role: "system".to_string(),
1547 content: format!("HTTP tool failed: {}", e),
1548 });
1549 }
1550 }
1551
1552 Ok(())
1553 }
1554
1555 async fn handle_asr_final(&mut self, text: &str) -> Result<Vec<Command>> {
1556 if text.trim().is_empty() {
1557 return Ok(vec![]);
1558 }
1559
1560 self.apply_context_repair(text);
1561 self.apply_rolling_summary().await;
1562
1563 self.last_asr_final_at = Some(std::time::Instant::now());
1564 self.last_interaction_at = std::time::Instant::now();
1565 self.is_speaking = false;
1566 self.consecutive_follow_ups = 0;
1567
1568 self.generate_response().await
1569 }
1570
1571 fn apply_context_repair(&mut self, text: &str) {
1572 let enable_repair = self
1573 .config
1574 .features
1575 .as_ref()
1576 .map(|f| f.contains(&"context_repair".to_string()))
1577 .unwrap_or(false);
1578
1579 if !enable_repair {
1580 self.history.push(ChatMessage {
1581 role: "user".to_string(),
1582 content: text.to_string(),
1583 });
1584 return;
1585 }
1586
1587 let repair_window_ms = self.config.repair_window_ms.unwrap_or(3000) as u128;
1588 let mut merged = false;
1589
1590 if let Some(last_robot_at) = self.last_robot_msg_at {
1591 if last_robot_at.elapsed().as_millis() < repair_window_ms {
1592 if let Some(last_msg) = self.history.last() {
1593 if last_msg.role == "assistant" && last_msg.content.chars().count() < 15 {
1594 info!(
1595 "Context Repair: Detected potential fragmentation. Triggering merge."
1596 );
1597 self.history.pop();
1598 if let Some(prev_user) = self.history.last_mut() {
1599 if prev_user.role == "user" {
1600 prev_user.content.push_str(",");
1601 prev_user.content.push_str(text);
1602 merged = true;
1603 }
1604 }
1605 }
1606 }
1607 }
1608 }
1609
1610 if !merged {
1611 self.history.push(ChatMessage {
1612 role: "user".to_string(),
1613 content: text.to_string(),
1614 });
1615 }
1616 }
1617
1618 async fn apply_rolling_summary(&mut self) {
1619 let enable_summary = self
1620 .config
1621 .features
1622 .as_ref()
1623 .map(|f| f.contains(&"rolling_summary".to_string()))
1624 .unwrap_or(false);
1625
1626 if !enable_summary {
1627 return;
1628 }
1629
1630 let summary_limit = self.config.summary_limit.unwrap_or(20);
1631 if self.history.len() <= summary_limit {
1632 return;
1633 }
1634
1635 info!("Rolling Summary: History limit reached. Triggering background summary.");
1636 let keep_recent = 6;
1637 if self.history.len() <= summary_limit + keep_recent
1638 || self.history.len() <= keep_recent + 1
1639 {
1640 return;
1641 }
1642
1643 let split_idx = self.history.len() - keep_recent;
1644 let to_summarize = self.history[1..split_idx].to_vec();
1645 let recent = self.history[split_idx..].to_vec();
1646
1647 let summary_prompt =
1648 "Summarize the above conversation so far, focusing on key details and user intent.";
1649 let mut summary_req_history = to_summarize;
1650 summary_req_history.push(ChatMessage {
1651 role: "user".to_string(),
1652 content: summary_prompt.to_string(),
1653 });
1654
1655 match self.provider.call(&self.config, &summary_req_history).await {
1656 Ok(summary) => {
1657 let mut new_history = Vec::new();
1658 if let Some(sys) = self.history.first() {
1659 let mut new_sys = sys.clone();
1660 new_sys.content.push_str("\n\n[Previous Context Summary]: ");
1661 new_sys.content.push_str(&summary);
1662 new_history.push(new_sys);
1663 }
1664 new_history.extend(recent);
1665 self.history = new_history;
1666 info!(
1667 "Rolling Summary: Applied summary. New history len: {}",
1668 self.history.len()
1669 );
1670 }
1671 Err(e) => {
1672 warn!("Rolling Summary failed: {}", e);
1673 }
1674 }
1675 }
1676
1677 fn check_interruption(
1678 &mut self,
1679 event: &SessionEvent,
1680 is_filler: &Option<bool>,
1681 ) -> Option<Command> {
1682 let strategy = self.interruption_config.strategy;
1683 let should_check = match (strategy, event) {
1684 (InterruptionStrategy::None, _) => false,
1685 (InterruptionStrategy::Vad, SessionEvent::Speaking { .. }) => true,
1686 (InterruptionStrategy::Asr, SessionEvent::AsrDelta { .. }) => true,
1687 (InterruptionStrategy::Both, _) => true,
1688 _ => false,
1689 };
1690
1691 if !self.is_speaking || self.is_hanging_up || !should_check {
1692 return None;
1693 }
1694
1695 if let Some(last_start) = self.last_tts_start_at {
1697 let ignore_ms = self.interruption_config.ignore_first_ms.unwrap_or(800);
1698 if last_start.elapsed().as_millis() < ignore_ms as u128 {
1699 return None;
1700 }
1701 }
1702
1703 if self.interruption_config.filler_word_filter.unwrap_or(false) {
1705 if let Some(true) = is_filler {
1706 return None;
1707 }
1708 if let SessionEvent::AsrDelta { text, .. } = event {
1709 if is_likely_filler(text) {
1710 return None;
1711 }
1712 }
1713 }
1714
1715 if let Some(last_final) = self.last_asr_final_at {
1717 if last_final.elapsed().as_millis() < 500 {
1718 return None;
1719 }
1720 }
1721
1722 info!("Smart interruption detected, stopping playback");
1723 self.is_speaking = false;
1724 Some(Command::Interrupt {
1725 graceful: Some(true),
1726 fade_out_ms: self.interruption_config.volume_fade_ms,
1727 })
1728 }
1729
1730 async fn handle_silence(&mut self) -> Result<Vec<Command>> {
1731 let follow_up_config = if let Some(scene_id) = &self.current_scene_id {
1732 self.scenes
1733 .get(scene_id)
1734 .and_then(|s| s.follow_up)
1735 .or(self.global_follow_up_config)
1736 } else {
1737 self.global_follow_up_config
1738 };
1739
1740 let Some(config) = follow_up_config else {
1741 return Ok(vec![]);
1742 };
1743
1744 if self.is_speaking
1745 || self.last_interaction_at.elapsed().as_millis() < config.timeout as u128
1746 {
1747 return Ok(vec![]);
1748 }
1749
1750 if self.consecutive_follow_ups >= config.max_count {
1751 info!("Max follow-up count reached, hanging up");
1752 let headers = self.render_sip_headers().await;
1753 return Ok(vec![Command::Hangup {
1754 reason: Some("Max follow-up reached".to_string()),
1755 initiator: Some("system".to_string()),
1756 headers,
1757 refer: None,
1758 }]);
1759 }
1760
1761 info!(
1762 "Silence timeout detected ({}ms), triggering follow-up ({}/{})",
1763 self.last_interaction_at.elapsed().as_millis(),
1764 self.consecutive_follow_ups + 1,
1765 config.max_count
1766 );
1767 self.consecutive_follow_ups += 1;
1768 self.last_interaction_at = std::time::Instant::now();
1769 self.generate_response().await
1770 }
1771
1772 async fn handle_function_call(&mut self, name: &str, arguments: &str) -> Result<Vec<Command>> {
1773 info!(
1774 "Function call from Realtime: {} with args {}",
1775 name, arguments
1776 );
1777 let args: serde_json::Value = serde_json::from_str(arguments).unwrap_or_default();
1778
1779 match name {
1780 "hangup_call" => {
1781 let headers = self.render_sip_headers().await;
1782 Ok(vec![Command::Hangup {
1783 reason: args["reason"].as_str().map(|s| s.to_string()),
1784 initiator: Some("ai".to_string()),
1785 headers,
1786 refer: None,
1787 }])
1788 }
1789 "transfer_call" | "refer_call" => {
1790 if let Some(callee) = args["callee"]
1791 .as_str()
1792 .or_else(|| args["callee_uri"].as_str())
1793 {
1794 Ok(vec![Command::Refer {
1795 caller: String::new(),
1796 callee: callee.to_string(),
1797 options: None,
1798 }])
1799 } else {
1800 warn!("No callee provided for transfer_call");
1801 Ok(vec![])
1802 }
1803 }
1804 "goto_scene" => {
1805 if let Some(scene) = args["scene"].as_str() {
1806 self.switch_to_scene(scene, false).await
1807 } else {
1808 Ok(vec![])
1809 }
1810 }
1811 _ => {
1812 warn!("Unhandled function call: {}", name);
1813 Ok(vec![])
1814 }
1815 }
1816 }
1817
1818 async fn interpret_response(&mut self, initial: String) -> Result<Vec<Command>> {
1819 let mut tool_commands = Vec::new();
1820 let mut wait_input_timeout = None;
1821 let mut attempts = 0;
1822 let mut raw = initial;
1823
1824 let final_text = loop {
1825 attempts += 1;
1826
1827 let Some(structured) = parse_structured_response(&raw) else {
1828 break Some(raw);
1829 };
1830
1831 if wait_input_timeout.is_none() {
1832 wait_input_timeout = structured.wait_input_timeout;
1833 }
1834
1835 let has_tools = structured
1836 .tools
1837 .as_ref()
1838 .map(|tools| !tools.is_empty())
1839 .unwrap_or(false);
1840
1841 if attempts >= MAX_RAG_ATTEMPTS
1842 && has_tools
1843 && structured
1844 .text
1845 .as_ref()
1846 .map(|text| text.trim().is_empty())
1847 .unwrap_or(true)
1848 {
1849 warn!(
1850 "Reached RAG iteration limit with tool-only response; suppressing raw tool JSON"
1851 );
1852 break None;
1853 }
1854
1855 let mut rerun_for_rag = false;
1856 if let Some(tools) = structured.tools {
1857 for tool in tools {
1858 let needs_rerun = self
1859 .handle_tool_invocation(tool, &mut tool_commands)
1860 .await?;
1861 rerun_for_rag = rerun_for_rag || needs_rerun;
1862 }
1863 }
1864
1865 if !rerun_for_rag {
1866 break structured.text;
1867 }
1868
1869 if attempts >= MAX_RAG_ATTEMPTS {
1870 warn!("Reached RAG iteration limit, using last response");
1871 break structured.text.or(Some(raw));
1872 }
1873
1874 raw = self.call_llm().await?;
1875 };
1876
1877 let has_hangup = tool_commands
1878 .iter()
1879 .any(|c| matches!(c, Command::Hangup { .. }));
1880 let mut commands = Vec::new();
1881
1882 if let Some(text) = final_text {
1883 if !text.trim().is_empty() {
1884 self.history.push(ChatMessage {
1885 role: "assistant".to_string(),
1886 content: text.clone(),
1887 });
1888 self.last_tts_start_at = Some(std::time::Instant::now());
1889 self.is_speaking = true;
1890
1891 let auto_hangup = has_hangup.then_some(true);
1892 commands.push(self.create_tts_command(text, wait_input_timeout, auto_hangup));
1893
1894 if has_hangup {
1895 tool_commands.retain(|c| !matches!(c, Command::Hangup { .. }));
1896 self.is_hanging_up = true;
1897 }
1898 }
1899 }
1900
1901 commands.extend(tool_commands);
1902 Ok(commands)
1903 }
1904}
1905
1906fn parse_structured_response(raw: &str) -> Option<StructuredResponse> {
1907 let payload = extract_json_block(raw)?;
1908 serde_json::from_str(payload).ok()
1909}
1910
1911fn is_likely_filler(text: &str) -> bool {
1912 let trimmed = text.trim().to_lowercase();
1913 FILLERS.contains(&trimmed)
1914}
1915
1916fn extract_json_block(raw: &str) -> Option<&str> {
1917 let trimmed = raw.trim();
1918 if trimmed.starts_with('`') {
1919 if let Some(end) = trimmed.rfind("```") {
1920 if end <= 3 {
1921 return None;
1922 }
1923 let mut inner = &trimmed[3..end];
1924 inner = inner.trim();
1925 if inner.to_lowercase().starts_with("json") {
1926 if let Some(newline) = inner.find('\n') {
1927 inner = inner[newline + 1..].trim();
1928 } else if inner.len() > 4 {
1929 inner = inner[4..].trim();
1930 } else {
1931 inner = inner.trim();
1932 }
1933 }
1934 return Some(inner);
1935 }
1936 } else if trimmed.starts_with('{') || trimmed.starts_with('[') {
1937 return Some(trimmed);
1938 }
1939 None
1940}
1941
1942#[async_trait]
1943impl DialogueHandler for LlmHandler {
1944 async fn on_start(&mut self) -> Result<Vec<Command>> {
1945 self.last_tts_start_at = Some(std::time::Instant::now());
1946
1947 let mut commands = Vec::new();
1948
1949 if let Some(scene_id) = &self.current_scene_id {
1951 if let Some(scene) = self.scenes.get(scene_id) {
1952 if let Some(audio_file) = &scene.play {
1953 commands.push(Command::Play {
1954 url: audio_file.clone(),
1955 play_id: None,
1956 auto_hangup: None,
1957 wait_input_timeout: None,
1958 offset_ms: None,
1959 });
1960 }
1961 }
1962 }
1963
1964 if let Some(greeting) = &self.config.greeting {
1965 self.is_speaking = true;
1966 commands.push(self.create_tts_command(greeting.clone(), None, None));
1967 return Ok(commands);
1968 }
1969
1970 let response_commands = self.generate_response().await?;
1971 commands.extend(response_commands);
1972 Ok(commands)
1973 }
1974
1975 async fn on_event(&mut self, event: &SessionEvent) -> Result<Vec<Command>> {
1976 if self.collector_state.is_some() {
1978 match event {
1979 SessionEvent::Dtmf { digit, .. } => {
1980 info!("DTMF received (collecting): {}", digit);
1981 return self.handle_collector_digit(digit).await;
1982 }
1983 SessionEvent::Silence { .. } => {
1984 return self.check_collector_timeout().await;
1986 }
1987 SessionEvent::TrackEnd { .. } => {
1988 self.is_speaking = false;
1989 return Ok(vec![]);
1990 }
1991 SessionEvent::TrackStart { .. } => {
1992 self.is_speaking = true;
1993 return Ok(vec![]);
1994 }
1995 SessionEvent::Hangup { .. } => {
1996 self.collector_state = None;
1998 }
1999 SessionEvent::AsrFinal { .. }
2001 | SessionEvent::AsrDelta { .. }
2002 | SessionEvent::Speaking { .. }
2003 | SessionEvent::Eou { .. } => {
2004 let interruptible = self
2005 .collector_state
2006 .as_ref()
2007 .and_then(|s| s.config.interruptible)
2008 .unwrap_or(false);
2009 if !interruptible {
2010 return Ok(vec![]);
2011 }
2012 }
2014 _ => return Ok(vec![]),
2015 }
2016 }
2017
2018 match event {
2019 SessionEvent::Dtmf { digit, .. } => {
2020 info!("DTMF received: {}", digit);
2021 if let Some(action) = self.get_dtmf_action(digit) {
2022 self.handle_dtmf_action(action).await
2023 } else {
2024 Ok(vec![])
2025 }
2026 }
2027 SessionEvent::AsrFinal { text, .. } => self.handle_asr_final(text).await,
2028 SessionEvent::AsrDelta { is_filler, .. } | SessionEvent::Speaking { is_filler, .. } => {
2029 Ok(self
2030 .check_interruption(event, is_filler)
2031 .into_iter()
2032 .collect())
2033 }
2034 SessionEvent::Eou { completed, .. } => {
2035 if *completed && !self.is_speaking {
2036 info!("EOU detected, triggering early response");
2037 self.generate_response().await
2038 } else {
2039 Ok(vec![])
2040 }
2041 }
2042 SessionEvent::Silence { .. } => self.handle_silence().await,
2043 SessionEvent::TrackStart { .. } => {
2044 self.is_speaking = true;
2045 Ok(vec![])
2046 }
2047 SessionEvent::TrackEnd { .. } => {
2048 self.is_speaking = false;
2049 self.is_hanging_up = false;
2050 self.last_interaction_at = std::time::Instant::now();
2051 Ok(vec![])
2052 }
2053 SessionEvent::FunctionCall {
2054 name, arguments, ..
2055 } => self.handle_function_call(name, arguments).await,
2056 _ => Ok(vec![]),
2057 }
2058 }
2059
2060 async fn get_history(&self) -> Vec<ChatMessage> {
2061 self.history.clone()
2062 }
2063
2064 async fn summarize(&mut self, prompt: &str) -> Result<String> {
2065 info!("Generating summary with prompt: {}", prompt);
2066 let mut summary_history = self.history.clone();
2067 summary_history.push(ChatMessage {
2068 role: "user".to_string(),
2069 content: prompt.to_string(),
2070 });
2071
2072 self.provider.call(&self.config, &summary_history).await
2073 }
2074}