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