livespeech_sdk/types/
config.rs

1//! Configuration types for the LiveSpeech SDK
2
3use serde::Serialize;
4use std::time::Duration;
5
6/// Pipeline mode for audio processing
7/// 
8/// - `Live`: Direct audio-to-audio conversation (default, lower latency)
9/// - `Composed`: Uses separate STT + LLM + TTS services (more customizable)
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum PipelineMode {
12    /// Direct audio-to-audio conversation (lower latency)
13    #[default]
14    Live,
15    /// Composed pipeline - separate STT + LLM + TTS services
16    Composed,
17}
18
19impl PipelineMode {
20    /// Get the string representation for the protocol
21    pub fn as_str(&self) -> &'static str {
22        match self {
23            PipelineMode::Live => "live",
24            PipelineMode::Composed => "composed",
25        }
26    }
27}
28
29impl std::fmt::Display for PipelineMode {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{}", self.as_str())
32    }
33}
34
35/// Available regions for LiveSpeech service
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Region {
38    /// Asia Pacific (Seoul)
39    ApNortheast2,
40    /// US West (Oregon) - Coming soon
41    UsWest2,
42}
43
44impl Region {
45    /// Get the WebSocket endpoint URL for this region
46    pub fn endpoint(&self) -> &'static str {
47        match self {
48            Region::ApNortheast2 => "wss://talk.drawdream.co.kr",
49            Region::UsWest2 => "wss://talk.drawdream.ca", // Coming soon
50        }
51    }
52
53    /// Get the region identifier string
54    pub fn as_str(&self) -> &'static str {
55        match self {
56            Region::ApNortheast2 => "ap-northeast-2",
57            Region::UsWest2 => "us-west-2",
58        }
59    }
60}
61
62impl std::fmt::Display for Region {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        write!(f, "{}", self.as_str())
65    }
66}
67
68/// Configuration for the LiveSpeech client
69#[derive(Debug, Clone)]
70pub struct Config {
71    /// WebSocket endpoint URL
72    pub endpoint: String,
73    /// API key for authentication
74    pub api_key: String,
75    /// User identifier for conversation memory persistence.
76    pub user_id: Option<String>,
77    /// Connection timeout
78    pub connection_timeout: Duration,
79    /// Enable automatic reconnection
80    pub auto_reconnect: bool,
81    /// Maximum reconnection attempts
82    pub max_reconnect_attempts: u32,
83    /// Base delay between reconnection attempts
84    pub reconnect_delay: Duration,
85    /// Enable debug logging
86    pub debug: bool,
87}
88
89impl Config {
90    /// Create a new config builder
91    pub fn builder() -> ConfigBuilder {
92        ConfigBuilder::default()
93    }
94}
95
96/// Builder for Config
97#[derive(Debug, Default)]
98pub struct ConfigBuilder {
99    region: Option<Region>,
100    api_key: Option<String>,
101    user_id: Option<String>,
102    connection_timeout: Option<Duration>,
103    auto_reconnect: Option<bool>,
104    max_reconnect_attempts: Option<u32>,
105    reconnect_delay: Option<Duration>,
106    debug: Option<bool>,
107}
108
109impl ConfigBuilder {
110    /// Set the region (required)
111    /// 
112    /// This will automatically resolve the correct endpoint URL for the region.
113    /// 
114    /// # Example
115    /// ```
116    /// use livespeech_sdk::{Config, Region};
117    /// 
118    /// let config = Config::builder()
119    ///     .region(Region::ApNortheast2)
120    ///     .api_key("your-api-key")
121    ///     .build()
122    ///     .unwrap();
123    /// ```
124    pub fn region(mut self, region: Region) -> Self {
125        self.region = Some(region);
126        self
127    }
128
129    /// Set the API key (required)
130    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
131        self.api_key = Some(api_key.into());
132        self
133    }
134
135    /// Set the user ID for conversation memory persistence.
136    /// 
137    /// # Example
138    /// ```
139    /// use livespeech_sdk::{Config, Region};
140    /// 
141    /// let config = Config::builder()
142    ///     .region(Region::ApNortheast2)
143    ///     .api_key("your-api-key")
144    ///     .user_id("user-123")
145    ///     .build()
146    ///     .unwrap();
147    /// ```
148    pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
149        self.user_id = Some(user_id.into());
150        self
151    }
152
153    /// Set the connection timeout
154    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
155        self.connection_timeout = Some(timeout);
156        self
157    }
158
159    /// Enable or disable auto reconnection
160    pub fn auto_reconnect(mut self, enabled: bool) -> Self {
161        self.auto_reconnect = Some(enabled);
162        self
163    }
164
165    /// Set maximum reconnection attempts
166    pub fn max_reconnect_attempts(mut self, attempts: u32) -> Self {
167        self.max_reconnect_attempts = Some(attempts);
168        self
169    }
170
171    /// Set base reconnection delay
172    pub fn reconnect_delay(mut self, delay: Duration) -> Self {
173        self.reconnect_delay = Some(delay);
174        self
175    }
176
177    /// Enable or disable debug mode
178    pub fn debug(mut self, enabled: bool) -> Self {
179        self.debug = Some(enabled);
180        self
181    }
182
183    /// Build the Config
184    pub fn build(self) -> Result<Config, ConfigError> {
185        let region = self.region.ok_or(ConfigError::MissingRegion)?;
186        let api_key = self.api_key.ok_or(ConfigError::MissingApiKey)?;
187
188        Ok(Config {
189            endpoint: region.endpoint().to_string(),
190            api_key,
191            user_id: self.user_id,
192            connection_timeout: self.connection_timeout.unwrap_or(Duration::from_secs(30)),
193            auto_reconnect: self.auto_reconnect.unwrap_or(true),
194            max_reconnect_attempts: self.max_reconnect_attempts.unwrap_or(5),
195            reconnect_delay: self.reconnect_delay.unwrap_or(Duration::from_secs(1)),
196            debug: self.debug.unwrap_or(false),
197        })
198    }
199}
200
201/// Configuration errors
202#[derive(Debug, thiserror::Error)]
203pub enum ConfigError {
204    #[error("region is required")]
205    MissingRegion,
206    #[error("api_key is required")]
207    MissingApiKey,
208}
209
210// =============================================================================
211// Tool (Function Calling) Types
212// =============================================================================
213
214/// Tool definition for function calling
215/// 
216/// Define functions that the AI can call during conversation.
217/// When the AI decides to call a function, you'll receive a `ToolCall` event.
218/// 
219/// # Example
220/// ```
221/// use livespeech_sdk::{Tool, FunctionParameters};
222/// 
223/// let tool = Tool {
224///     name: "open_login".to_string(),
225///     description: "Opens the login popup when user wants to sign in".to_string(),
226///     parameters: Some(FunctionParameters {
227///         r#type: "OBJECT".to_string(),
228///         properties: serde_json::json!({}),
229///         required: vec![],
230///     }),
231/// };
232/// ```
233#[derive(Debug, Clone, Serialize)]
234pub struct Tool {
235    /// Unique function name
236    pub name: String,
237    /// Description of when the AI should use this function
238    pub description: String,
239    /// Parameters schema (optional)
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub parameters: Option<FunctionParameters>,
242}
243
244/// Function parameters schema
245#[derive(Debug, Clone, Serialize)]
246pub struct FunctionParameters {
247    /// Type (usually "OBJECT")
248    #[serde(rename = "type")]
249    pub r#type: String,
250    /// Properties schema
251    pub properties: serde_json::Value,
252    /// Required parameter names
253    #[serde(skip_serializing_if = "Vec::is_empty")]
254    pub required: Vec<String>,
255}
256
257impl Tool {
258    /// Create a new tool with no parameters
259    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
260        Self {
261            name: name.into(),
262            description: description.into(),
263            parameters: None,
264        }
265    }
266
267    /// Create a tool with parameters
268    pub fn with_parameters(
269        name: impl Into<String>,
270        description: impl Into<String>,
271        properties: serde_json::Value,
272        required: Vec<String>,
273    ) -> Self {
274        Self {
275            name: name.into(),
276            description: description.into(),
277            parameters: Some(FunctionParameters {
278                r#type: "OBJECT".to_string(),
279                properties,
280                required,
281            }),
282        }
283    }
284}
285
286// =============================================================================
287// Session Configuration
288// =============================================================================
289
290/// Session configuration
291#[derive(Debug, Clone)]
292pub struct SessionConfig {
293    /// System prompt for the AI (optional)
294    pub pre_prompt: Option<String>,
295    /// Language code for speech recognition (e.g., "en-US", "ko-KR")
296    pub language: Option<String>,
297    /// Pipeline mode for audio processing (default: Live)
298    pub pipeline_mode: PipelineMode,
299    /// Enable AI to speak first before user input (live mode only)
300    /// When enabled, the AI will initiate the conversation based on the pre_prompt.
301    /// Make sure your pre_prompt includes instructions for how the AI should greet the user.
302    /// Default: false
303    pub ai_speaks_first: bool,
304    /// Allow harmful content categories in AI responses
305    /// Default: false
306    pub allow_harm_category: bool,
307    /// Tools (functions) that the AI can call during conversation (live mode only)
308    /// Default: None
309    pub tools: Option<Vec<Tool>>,
310}
311
312impl Default for SessionConfig {
313    fn default() -> Self {
314        Self {
315            pre_prompt: None,
316            language: None,
317            pipeline_mode: PipelineMode::Live,
318            ai_speaks_first: false,
319            allow_harm_category: false,
320            tools: None,
321        }
322    }
323}
324
325impl SessionConfig {
326    /// Create a new session config with a pre-prompt
327    pub fn new(pre_prompt: impl Into<String>) -> Self {
328        Self {
329            pre_prompt: Some(pre_prompt.into()),
330            language: None,
331            pipeline_mode: PipelineMode::Live,
332            ai_speaks_first: false,
333            allow_harm_category: false,
334            tools: None,
335        }
336    }
337
338    /// Create an empty session config
339    pub fn empty() -> Self {
340        Self::default()
341    }
342
343    /// Set the language for speech recognition
344    pub fn with_language(mut self, language: impl Into<String>) -> Self {
345        self.language = Some(language.into());
346        self
347    }
348
349    /// Set the pre-prompt
350    pub fn with_pre_prompt(mut self, pre_prompt: impl Into<String>) -> Self {
351        self.pre_prompt = Some(pre_prompt.into());
352        self
353    }
354
355    /// Set the pipeline mode
356    /// 
357    /// - `PipelineMode::Live`: Direct audio-to-audio conversation (default, lower latency)
358    /// - `PipelineMode::Composed`: Separate STT + LLM + TTS services (more customizable)
359    pub fn with_pipeline_mode(mut self, mode: PipelineMode) -> Self {
360        self.pipeline_mode = mode;
361        self
362    }
363
364    /// Enable AI to speak first before user input (live mode only)
365    /// 
366    /// When enabled, the AI will initiate the conversation based on the pre_prompt.
367    /// Make sure your pre_prompt includes instructions for how the AI should greet the user.
368    /// 
369    /// # Example
370    /// ```
371    /// use livespeech_sdk::SessionConfig;
372    /// 
373    /// let config = SessionConfig::new("You are a helpful assistant. Start by greeting the user.")
374    ///     .with_ai_speaks_first(true);
375    /// ```
376    pub fn with_ai_speaks_first(mut self, enabled: bool) -> Self {
377        self.ai_speaks_first = enabled;
378        self
379    }
380
381    /// Set whether to allow harmful content categories in AI responses
382    /// 
383    /// # Example
384    /// ```
385    /// use livespeech_sdk::SessionConfig;
386    /// 
387    /// // Enable content safety filtering
388    /// let config = SessionConfig::empty()
389    ///     .with_allow_harm_category(false);
390    /// ```
391    pub fn with_allow_harm_category(mut self, allow: bool) -> Self {
392        self.allow_harm_category = allow;
393        self
394    }
395
396    /// Set tools (functions) that the AI can call during conversation
397    /// 
398    /// # Example
399    /// ```
400    /// use livespeech_sdk::{SessionConfig, Tool};
401    /// 
402    /// let tools = vec![
403    ///     Tool::new("open_login", "Opens the login popup when user wants to sign in"),
404    /// ];
405    /// 
406    /// let config = SessionConfig::new("You are a helpful assistant.")
407    ///     .with_tools(tools);
408    /// ```
409    pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
410        self.tools = Some(tools);
411        self
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_config_builder_with_region() {
421        let config = Config::builder()
422            .region(Region::ApNortheast2)
423            .api_key("test-key")
424            .debug(true)
425            .build()
426            .unwrap();
427
428        assert_eq!(config.endpoint, "wss://talk.drawdream.co.kr");
429        assert_eq!(config.api_key, "test-key");
430        assert!(config.debug);
431    }
432
433    #[test]
434    fn test_config_builder_missing_region() {
435        let result = Config::builder().api_key("test-key").build();
436        assert!(matches!(result, Err(ConfigError::MissingRegion)));
437    }
438
439    #[test]
440    fn test_config_builder_missing_api_key() {
441        let result = Config::builder().region(Region::ApNortheast2).build();
442        assert!(matches!(result, Err(ConfigError::MissingApiKey)));
443    }
444
445    #[test]
446    fn test_region_endpoint() {
447        assert_eq!(Region::ApNortheast2.endpoint(), "wss://talk.drawdream.co.kr");
448        assert_eq!(Region::ApNortheast2.as_str(), "ap-northeast-2");
449    }
450
451    #[test]
452    fn test_session_config() {
453        let config = SessionConfig::new("You are a helpful assistant");
454        assert_eq!(config.pre_prompt, Some("You are a helpful assistant".to_string()));
455    }
456
457    #[test]
458    fn test_session_config_empty() {
459        let config = SessionConfig::empty();
460        assert_eq!(config.pre_prompt, None);
461    }
462
463    #[test]
464    fn test_pipeline_mode_default() {
465        let config = SessionConfig::empty();
466        assert_eq!(config.pipeline_mode, PipelineMode::Live);
467    }
468
469    #[test]
470    fn test_pipeline_mode_composed() {
471        let config = SessionConfig::empty().with_pipeline_mode(PipelineMode::Composed);
472        assert_eq!(config.pipeline_mode, PipelineMode::Composed);
473        assert_eq!(config.pipeline_mode.as_str(), "composed");
474    }
475}