Skip to main content

mockforge_intelligence/voice/
conversation.rs

1//! Conversation state management for multi-turn voice interactions
2//!
3//! This module manages conversation context and state for iterative API building
4//! through voice commands.
5
6use mockforge_foundation::Result;
7use mockforge_openapi::OpenApiSpec;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12/// Conversation manager for handling multi-turn voice interactions
13pub struct ConversationManager {
14    /// Active conversations indexed by ID
15    conversations: HashMap<String, ConversationState>,
16}
17
18impl ConversationManager {
19    /// Create a new conversation manager
20    pub fn new() -> Self {
21        Self {
22            conversations: HashMap::new(),
23        }
24    }
25
26    /// Start a new conversation
27    pub fn start_conversation(&mut self) -> String {
28        let id = Uuid::new_v4().to_string();
29        let state = ConversationState {
30            id: id.clone(),
31            context: ConversationContext {
32                conversation_id: id.clone(),
33                current_spec: None,
34                history: Vec::new(),
35                metadata: HashMap::new(),
36            },
37            created_at: chrono::Utc::now(),
38            updated_at: chrono::Utc::now(),
39        };
40        self.conversations.insert(id.clone(), state);
41        id
42    }
43
44    /// Get conversation state by ID
45    pub fn get_conversation(&self, id: &str) -> Option<&ConversationState> {
46        self.conversations.get(id)
47    }
48
49    /// Get mutable conversation state by ID
50    pub fn get_conversation_mut(&mut self, id: &str) -> Option<&mut ConversationState> {
51        self.conversations.get_mut(id)
52    }
53
54    /// Update conversation with new command and resulting spec
55    pub fn update_conversation(
56        &mut self,
57        id: &str,
58        command: &str,
59        spec: Option<OpenApiSpec>,
60    ) -> Result<()> {
61        let state = self.conversations.get_mut(id).ok_or_else(|| {
62            mockforge_foundation::Error::internal(format!("Conversation {} not found", id))
63        })?;
64
65        // Add command to history
66        state.context.history.push(ConversationEntry {
67            timestamp: chrono::Utc::now(),
68            command: command.to_string(),
69            spec_snapshot: spec
70                .as_ref()
71                .map(|s| serde_json::to_value(s.spec.clone()).unwrap_or(serde_json::Value::Null)),
72        });
73
74        // Update current spec
75        state.context.current_spec = spec;
76
77        // Update timestamp
78        state.updated_at = chrono::Utc::now();
79
80        Ok(())
81    }
82
83    /// Remove a conversation
84    pub fn remove_conversation(&mut self, id: &str) -> bool {
85        self.conversations.remove(id).is_some()
86    }
87
88    /// List all active conversations
89    pub fn list_conversations(&self) -> Vec<&ConversationState> {
90        self.conversations.values().collect()
91    }
92}
93
94impl Default for ConversationManager {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100/// Conversation state for a single conversation
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ConversationState {
103    /// Conversation ID
104    pub id: String,
105    /// Conversation context
106    pub context: ConversationContext,
107    /// When conversation was created
108    pub created_at: chrono::DateTime<chrono::Utc>,
109    /// Last update timestamp
110    pub updated_at: chrono::DateTime<chrono::Utc>,
111}
112
113/// Conversation context containing current state and history
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ConversationContext {
116    /// Conversation ID
117    pub conversation_id: String,
118    /// Current OpenAPI spec (if any)
119    #[serde(skip)]
120    pub current_spec: Option<OpenApiSpec>,
121    /// Command history
122    pub history: Vec<ConversationEntry>,
123    /// Additional metadata
124    #[serde(default)]
125    pub metadata: HashMap<String, serde_json::Value>,
126}
127
128/// Entry in conversation history
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ConversationEntry {
131    /// Timestamp of the entry
132    pub timestamp: chrono::DateTime<chrono::Utc>,
133    /// Command that was executed
134    pub command: String,
135    /// Snapshot of the spec after this command (as JSON)
136    #[serde(default)]
137    pub spec_snapshot: Option<serde_json::Value>,
138}
139
140impl ConversationContext {
141    /// Create a new conversation context
142    pub fn new(conversation_id: String) -> Self {
143        Self {
144            conversation_id,
145            current_spec: None,
146            history: Vec::new(),
147            metadata: HashMap::new(),
148        }
149    }
150
151    /// Update the current spec
152    pub fn update_spec(&mut self, spec: OpenApiSpec) {
153        self.current_spec = Some(spec);
154    }
155
156    /// Get the current spec
157    pub fn get_spec(&self) -> Option<&OpenApiSpec> {
158        self.current_spec.as_ref()
159    }
160
161    /// Add a command to history
162    pub fn add_command(&mut self, command: String, spec_snapshot: Option<serde_json::Value>) {
163        self.history.push(ConversationEntry {
164            timestamp: chrono::Utc::now(),
165            command,
166            spec_snapshot,
167        });
168    }
169
170    /// Get conversation summary for LLM context
171    pub fn get_summary(&self) -> String {
172        let mut summary = format!(
173            "Conversation ID: {}\nHistory: {} commands\n",
174            self.conversation_id,
175            self.history.len()
176        );
177
178        if let Some(ref spec) = self.current_spec {
179            summary.push_str(&format!(
180                "Current API: {}\nVersion: {}\n",
181                spec.title(),
182                spec.version()
183            ));
184        } else {
185            summary.push_str("Current API: None (new conversation)\n");
186        }
187
188        if !self.history.is_empty() {
189            summary.push_str("\nRecent commands:\n");
190            for entry in self.history.iter().rev().take(5) {
191                summary.push_str(&format!("- {}\n", entry.command));
192            }
193        }
194
195        summary
196    }
197}