Skip to main content

active_call/playbook/handler/
mod.rs

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