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_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/// Runtime state for an active DTMF digit collection session
90#[derive(Debug, Clone)]
91pub struct CollectorState {
92    /// Name of the collector type being used (key into dtmf_collectors)
93    pub collector_type: String,
94    /// Variable name to store the collected digits
95    pub var_name: String,
96    /// Resolved config from the collector template
97    pub config: super::DtmfCollectorConfig,
98    /// Buffer of collected digits so far
99    pub buffer: String,
100    /// When collection started
101    pub start_time: std::time::Instant,
102    /// When the last digit was received
103    pub last_digit_time: std::time::Instant,
104    /// Number of retries attempted
105    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    /// Active DTMF digit collector state (None when not collecting)
131    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        // Load tool instructions - either custom or language-specific default
235        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                    // Fallback to English if loading fails
242                    Self::load_feature_snippet("tool_instructions", "en")
243                        .unwrap_or_else(|_| {
244                            // Ultimate fallback to hardcoded English
245                            "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    /// Generate LLM prompt instructions for available DTMF digit collectors
277    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        // Sort by key for deterministic output
301        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    /// Check if the collector has timed out and handle accordingly.
343    /// Returns commands to execute (e.g., retry prompt or failure notification).
344    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        // Check overall timeout
354        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                // Try to validate what we have
368                return self
369                    .do_finish_collection(buffer, var_name, collector_type, config, retry_count)
370                    .await;
371            }
372
373            // Nothing collected - notify LLM
374            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        // Check inter-digit timeout (only if we have some digits)
385        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    /// Handle a DTMF digit while in collector mode
407    async fn handle_collector_digit(&mut self, digit: &str) -> Result<Vec<Command>> {
408        let state = self.collector_state.as_mut().unwrap();
409
410        // Check if it's the finish key
411        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        // Append digit to buffer
427        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        // Check if we've reached the required digit count
436        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 no finish_key is configured, auto-complete at max digits
441                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    /// Internal: finish collection with full state available
460    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        // Validate min digits
469        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        // Validate pattern
483        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        // Validation passed - store the variable
498        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        // Notify LLM of the result
508        self.history.push(ChatMessage {
509            role: "system".to_string(),
510            content: format!("[DTMF collection completed for '{}': {}]", var_name, buffer),
511        });
512
513        // Let LLM continue
514        self.generate_response().await
515    }
516
517    /// Retry collection or fail after max retries
518    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        // Restart collection with incremented retry count
552        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        // Play error message
564        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    /// Start a DTMF collector from an LLM-generated <collect> command
574    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    /// Returns true if currently in DTMF digit collection mode
608    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    /// Get current extras (variables) from the call for dynamic template rendering.
660    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    /// Render a scene's prompt template using the latest variables from call_state.
669    /// This enables `set_var` values to be reflected in system prompts dynamically.
670    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            // Dynamically render the scene prompt with the latest variables
684            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        // Send debug event - LLM call started
810        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        // Send debug event - LLM response received
879        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; // Store hangup prefix and position
926
927        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            // Find the first occurrence
939            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                        // Flush TTS
1005                        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                        // Execute HTTP request synchronously and capture response
1015                        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                        // Send request and wait for response
1027                        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                                // Add response to history for LLM context
1034                                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                                // Add error to history
1049                                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                        // Don't execute hangup immediately, store it for later
1060                        // This allows set_var commands after hangup to still be processed
1061                        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                        // Continue processing remaining buffer for set_var commands
1067                        // Don't return yet!
1068                    }
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                        // Play audio
1114                        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                        // Goto Scene
1137                        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                            // Dynamically render scene prompt with the latest variables
1154                            let rendered_prompt = self.render_scene_prompt(&scene).await;
1155                            // Update system prompt in history
1156                            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                        // Flush any text before the <collect> tag as TTS
1180                        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                        // Play the collector prompt if provided
1190                        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                        // Start the collector
1197                        if !self.start_collector(&collector_type, &var_name) {
1198                            // Collector type not found, notify LLM
1199                            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                        // Sentence
1216                        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        // Process pending hangup after all other commands (especially set_var)
1234        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        // Get the list of SIP header keys stored during extraction
1427        // If not present, sip dict will be empty (no headers were configured for extraction)
1428        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            // Skip internal keys
1435            if k.starts_with('_') {
1436                continue;
1437            }
1438            context.insert(k.clone(), v.clone());
1439            // Only include keys that were extracted as SIP headers
1440            if sip_header_keys.contains(k) {
1441                sip_headers.insert(k.clone(), v.clone());
1442            }
1443        }
1444
1445        // Add sip dictionary for template access
1446        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        // Protection period check
1696        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        // Filler word filter
1704        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        // Stale event check
1716        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        // Check if current scene has an audio file to play
1950        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        // When in DTMF collection mode, only handle DTMF events and track lifecycle
1977        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                    // Check collector timeout on silence events
1985                    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                    // Allow hangup to pass through
1997                    self.collector_state = None;
1998                }
1999                // Ignore ASR/Speaking/Eou during collection (not interruptible by default)
2000                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                    // If interruptible, fall through to normal handling
2013                }
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}