Skip to main content

active_call/playbook/handler/
mod.rs

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