mockforge_core/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 crate::openapi::OpenApiSpec;
7use crate::Result;
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
62            .conversations
63            .get_mut(id)
64            .ok_or_else(|| crate::Error::generic(format!("Conversation {} not found", id)))?;
65
66        // Add command to history
67        state.context.history.push(ConversationEntry {
68            timestamp: chrono::Utc::now(),
69            command: command.to_string(),
70            spec_snapshot: spec
71                .as_ref()
72                .map(|s| serde_json::to_value(s.spec.clone()).unwrap_or(serde_json::Value::Null)),
73        });
74
75        // Update current spec
76        state.context.current_spec = spec;
77
78        // Update timestamp
79        state.updated_at = chrono::Utc::now();
80
81        Ok(())
82    }
83
84    /// Remove a conversation
85    pub fn remove_conversation(&mut self, id: &str) -> bool {
86        self.conversations.remove(id).is_some()
87    }
88
89    /// List all active conversations
90    pub fn list_conversations(&self) -> Vec<&ConversationState> {
91        self.conversations.values().collect()
92    }
93}
94
95impl Default for ConversationManager {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101/// Conversation state for a single conversation
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ConversationState {
104    /// Conversation ID
105    pub id: String,
106    /// Conversation context
107    pub context: ConversationContext,
108    /// When conversation was created
109    pub created_at: chrono::DateTime<chrono::Utc>,
110    /// Last update timestamp
111    pub updated_at: chrono::DateTime<chrono::Utc>,
112}
113
114/// Conversation context containing current state and history
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ConversationContext {
117    /// Conversation ID
118    pub conversation_id: String,
119    /// Current OpenAPI spec (if any)
120    #[serde(skip)]
121    pub current_spec: Option<OpenApiSpec>,
122    /// Command history
123    pub history: Vec<ConversationEntry>,
124    /// Additional metadata
125    #[serde(default)]
126    pub metadata: HashMap<String, serde_json::Value>,
127}
128
129/// Entry in conversation history
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ConversationEntry {
132    /// Timestamp of the entry
133    pub timestamp: chrono::DateTime<chrono::Utc>,
134    /// Command that was executed
135    pub command: String,
136    /// Snapshot of the spec after this command (as JSON)
137    #[serde(default)]
138    pub spec_snapshot: Option<serde_json::Value>,
139}
140
141impl ConversationContext {
142    /// Create a new conversation context
143    pub fn new(conversation_id: String) -> Self {
144        Self {
145            conversation_id,
146            current_spec: None,
147            history: Vec::new(),
148            metadata: HashMap::new(),
149        }
150    }
151
152    /// Update the current spec
153    pub fn update_spec(&mut self, spec: OpenApiSpec) {
154        self.current_spec = Some(spec);
155    }
156
157    /// Get the current spec
158    pub fn get_spec(&self) -> Option<&OpenApiSpec> {
159        self.current_spec.as_ref()
160    }
161
162    /// Add a command to history
163    pub fn add_command(&mut self, command: String, spec_snapshot: Option<serde_json::Value>) {
164        self.history.push(ConversationEntry {
165            timestamp: chrono::Utc::now(),
166            command,
167            spec_snapshot,
168        });
169    }
170
171    /// Get conversation summary for LLM context
172    pub fn get_summary(&self) -> String {
173        let mut summary = format!(
174            "Conversation ID: {}\nHistory: {} commands\n",
175            self.conversation_id,
176            self.history.len()
177        );
178
179        if let Some(ref spec) = self.current_spec {
180            summary.push_str(&format!(
181                "Current API: {}\nVersion: {}\n",
182                spec.title(),
183                spec.version()
184            ));
185        } else {
186            summary.push_str("Current API: None (new conversation)\n");
187        }
188
189        if !self.history.is_empty() {
190            summary.push_str("\nRecent commands:\n");
191            for entry in self.history.iter().rev().take(5) {
192                summary.push_str(&format!("- {}\n", entry.command));
193            }
194        }
195
196        summary
197    }
198}