Skip to main content

openai_tools/conversations/
request.rs

1//! OpenAI Conversations API Request Module
2//!
3//! This module provides the functionality to interact with the OpenAI Conversations API.
4//! The Conversations API allows you to create and manage long-running conversations
5//! with the Responses API.
6//!
7//! # Key Features
8//!
9//! - **Create Conversations**: Create new conversations with optional metadata and items
10//! - **Retrieve Conversations**: Get details of a specific conversation
11//! - **Update Conversations**: Modify conversation metadata
12//! - **Delete Conversations**: Remove conversations
13//! - **Manage Items**: Add and list conversation items
14//!
15//! # Quick Start
16//!
17//! ```rust,no_run
18//! use openai_tools::conversations::request::Conversations;
19//! use openai_tools::conversations::response::InputItem;
20//! use std::collections::HashMap;
21//!
22//! #[tokio::main]
23//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//!     let conversations = Conversations::new()?;
25//!
26//!     // Create a new conversation
27//!     let mut metadata = HashMap::new();
28//!     metadata.insert("topic".to_string(), "demo".to_string());
29//!
30//!     let conversation = conversations.create(Some(metadata), None).await?;
31//!     println!("Created conversation: {}", conversation.id);
32//!
33//!     // Add items to the conversation
34//!     let items = vec![InputItem::user_message("Hello!")];
35//!     let added_items = conversations.create_items(&conversation.id, items).await?;
36//!
37//!     Ok(())
38//! }
39//! ```
40
41use crate::common::auth::AuthProvider;
42use crate::common::client::create_http_client;
43use crate::common::errors::{ErrorResponse, OpenAIToolError, Result};
44use crate::conversations::response::{Conversation, ConversationItemListResponse, ConversationListResponse, DeleteConversationResponse, InputItem};
45use serde::{Deserialize, Serialize};
46use std::collections::HashMap;
47use std::time::Duration;
48
49/// Default API path for Conversations
50const CONVERSATIONS_PATH: &str = "conversations";
51
52/// Specifies additional data to include in conversation item responses.
53///
54/// This enum defines various types of additional information that can be
55/// included when listing conversation items.
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[non_exhaustive]
58pub enum ConversationInclude {
59    /// Include web search call action sources
60    #[serde(rename = "web_search_call.action.sources")]
61    WebSearchCallSources,
62    /// Include code interpreter call outputs
63    #[serde(rename = "code_interpreter_call.outputs")]
64    CodeInterpreterCallOutputs,
65    /// Include file search call results
66    #[serde(rename = "file_search_call.results")]
67    FileSearchCallResults,
68    /// Include image URLs from input messages
69    #[serde(rename = "message.input_image.image_url")]
70    MessageInputImageUrl,
71    /// Include encrypted reasoning content
72    #[serde(rename = "reasoning.encrypted_content")]
73    ReasoningEncryptedContent,
74}
75
76impl ConversationInclude {
77    /// Returns the string representation for API requests.
78    pub fn as_str(&self) -> &'static str {
79        match self {
80            ConversationInclude::WebSearchCallSources => "web_search_call.action.sources",
81            ConversationInclude::CodeInterpreterCallOutputs => "code_interpreter_call.outputs",
82            ConversationInclude::FileSearchCallResults => "file_search_call.results",
83            ConversationInclude::MessageInputImageUrl => "message.input_image.image_url",
84            ConversationInclude::ReasoningEncryptedContent => "reasoning.encrypted_content",
85        }
86    }
87}
88
89/// Request body for creating a conversation.
90#[derive(Debug, Clone, Serialize)]
91struct CreateConversationRequest {
92    #[serde(skip_serializing_if = "Option::is_none")]
93    metadata: Option<HashMap<String, String>>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    items: Option<Vec<InputItem>>,
96}
97
98/// Request body for updating a conversation.
99#[derive(Debug, Clone, Serialize)]
100struct UpdateConversationRequest {
101    metadata: HashMap<String, String>,
102}
103
104/// Request body for creating conversation items.
105#[derive(Debug, Clone, Serialize)]
106struct CreateItemsRequest {
107    items: Vec<InputItem>,
108}
109
110/// Client for interacting with the OpenAI Conversations API.
111///
112/// This struct provides methods to create, retrieve, update, delete conversations,
113/// and manage conversation items. Use [`Conversations::new()`] to create a new instance.
114///
115/// # Example
116///
117/// ```rust,no_run
118/// use openai_tools::conversations::request::Conversations;
119/// use std::collections::HashMap;
120///
121/// #[tokio::main]
122/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
123///     let conversations = Conversations::new()?;
124///
125///     // Create a conversation with metadata
126///     let mut metadata = HashMap::new();
127///     metadata.insert("user_id".to_string(), "user123".to_string());
128///
129///     let conv = conversations.create(Some(metadata), None).await?;
130///     println!("Created: {}", conv.id);
131///
132///     // Retrieve the conversation
133///     let retrieved = conversations.retrieve(&conv.id).await?;
134///     println!("Retrieved: {:?}", retrieved.metadata);
135///
136///     Ok(())
137/// }
138/// ```
139pub struct Conversations {
140    /// Authentication provider (OpenAI or Azure)
141    auth: AuthProvider,
142    /// Optional request timeout duration
143    timeout: Option<Duration>,
144}
145
146impl Conversations {
147    /// Creates a new Conversations client for OpenAI API.
148    ///
149    /// Initializes the client by loading the OpenAI API key from
150    /// the environment variable `OPENAI_API_KEY`. Supports `.env` file loading
151    /// via dotenvy.
152    ///
153    /// # Returns
154    ///
155    /// * `Ok(Conversations)` - A new Conversations client ready for use
156    /// * `Err(OpenAIToolError)` - If the API key is not found in the environment
157    ///
158    /// # Example
159    ///
160    /// ```rust,no_run
161    /// use openai_tools::conversations::request::Conversations;
162    ///
163    /// let conversations = Conversations::new().expect("API key should be set");
164    /// ```
165    pub fn new() -> Result<Self> {
166        let auth = AuthProvider::openai_from_env()?;
167        Ok(Self { auth, timeout: None })
168    }
169
170    /// Creates a new Conversations client with a custom authentication provider
171    pub fn with_auth(auth: AuthProvider) -> Self {
172        Self { auth, timeout: None }
173    }
174
175    /// Creates a new Conversations client for Azure OpenAI API
176    pub fn azure() -> Result<Self> {
177        let auth = AuthProvider::azure_from_env()?;
178        Ok(Self { auth, timeout: None })
179    }
180
181    /// Creates a new Conversations client by auto-detecting the provider
182    pub fn detect_provider() -> Result<Self> {
183        let auth = AuthProvider::from_env()?;
184        Ok(Self { auth, timeout: None })
185    }
186
187    /// Creates a new Conversations client with URL-based provider detection
188    pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
189        let auth = AuthProvider::from_url_with_key(base_url, api_key);
190        Self { auth, timeout: None }
191    }
192
193    /// Creates a new Conversations client from URL using environment variables
194    pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
195        let auth = AuthProvider::from_url(url)?;
196        Ok(Self { auth, timeout: None })
197    }
198
199    /// Returns the authentication provider
200    pub fn auth(&self) -> &AuthProvider {
201        &self.auth
202    }
203
204    /// Sets the request timeout duration.
205    ///
206    /// # Arguments
207    ///
208    /// * `timeout` - The maximum time to wait for a response
209    ///
210    /// # Returns
211    ///
212    /// A mutable reference to self for method chaining
213    pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
214        self.timeout = Some(timeout);
215        self
216    }
217
218    /// Creates the HTTP client with default headers.
219    fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
220        let client = create_http_client(self.timeout)?;
221        let mut headers = request::header::HeaderMap::new();
222        self.auth.apply_headers(&mut headers)?;
223        headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
224        headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
225        Ok((client, headers))
226    }
227
228    /// Handles API error responses.
229    fn handle_error(status: request::StatusCode, content: &str) -> OpenAIToolError {
230        if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(content) {
231            OpenAIToolError::Error(error_resp.error.message.unwrap_or_default())
232        } else {
233            OpenAIToolError::Error(format!("API error ({}): {}", status, content))
234        }
235    }
236
237    /// Creates a new conversation.
238    ///
239    /// You can optionally provide metadata and initial items to include
240    /// in the conversation.
241    ///
242    /// # Arguments
243    ///
244    /// * `metadata` - Optional key-value pairs for storing additional information
245    /// * `items` - Optional initial items to add to the conversation (up to 20 items)
246    ///
247    /// # Returns
248    ///
249    /// * `Ok(Conversation)` - The created conversation object
250    /// * `Err(OpenAIToolError)` - If the request fails
251    ///
252    /// # Example
253    ///
254    /// ```rust,no_run
255    /// use openai_tools::conversations::request::Conversations;
256    /// use openai_tools::conversations::response::InputItem;
257    /// use std::collections::HashMap;
258    ///
259    /// #[tokio::main]
260    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
261    ///     let conversations = Conversations::new()?;
262    ///
263    ///     // Create with metadata and initial message
264    ///     let mut metadata = HashMap::new();
265    ///     metadata.insert("topic".to_string(), "greeting".to_string());
266    ///
267    ///     let items = vec![InputItem::user_message("Hello!")];
268    ///
269    ///     let conv = conversations.create(Some(metadata), Some(items)).await?;
270    ///     println!("Created conversation: {}", conv.id);
271    ///     Ok(())
272    /// }
273    /// ```
274    pub async fn create(&self, metadata: Option<HashMap<String, String>>, items: Option<Vec<InputItem>>) -> Result<Conversation> {
275        let (client, headers) = self.create_client()?;
276
277        let request_body = CreateConversationRequest { metadata, items };
278        let body = serde_json::to_string(&request_body)?;
279
280        let url = self.auth.endpoint(CONVERSATIONS_PATH);
281        let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
282
283        let status = response.status();
284        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
285
286        if cfg!(test) {
287            tracing::info!("Response content: {}", content);
288        }
289
290        if !status.is_success() {
291            return Err(Self::handle_error(status, &content));
292        }
293
294        serde_json::from_str::<Conversation>(&content).map_err(OpenAIToolError::SerdeJsonError)
295    }
296
297    /// Retrieves a specific conversation.
298    ///
299    /// # Arguments
300    ///
301    /// * `conversation_id` - The ID of the conversation to retrieve
302    ///
303    /// # Returns
304    ///
305    /// * `Ok(Conversation)` - The conversation object
306    /// * `Err(OpenAIToolError)` - If the conversation is not found or the request fails
307    ///
308    /// # Example
309    ///
310    /// ```rust,no_run
311    /// use openai_tools::conversations::request::Conversations;
312    ///
313    /// #[tokio::main]
314    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
315    ///     let conversations = Conversations::new()?;
316    ///     let conv = conversations.retrieve("conv_abc123").await?;
317    ///
318    ///     println!("Conversation: {}", conv.id);
319    ///     println!("Created at: {}", conv.created_at);
320    ///     Ok(())
321    /// }
322    /// ```
323    pub async fn retrieve(&self, conversation_id: &str) -> Result<Conversation> {
324        let (client, headers) = self.create_client()?;
325        let url = format!("{}/{}", self.auth.endpoint(CONVERSATIONS_PATH), conversation_id);
326
327        let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
328
329        let status = response.status();
330        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
331
332        if cfg!(test) {
333            tracing::info!("Response content: {}", content);
334        }
335
336        if !status.is_success() {
337            return Err(Self::handle_error(status, &content));
338        }
339
340        serde_json::from_str::<Conversation>(&content).map_err(OpenAIToolError::SerdeJsonError)
341    }
342
343    /// Updates a conversation's metadata.
344    ///
345    /// # Arguments
346    ///
347    /// * `conversation_id` - The ID of the conversation to update
348    /// * `metadata` - The new metadata to set
349    ///
350    /// # Returns
351    ///
352    /// * `Ok(Conversation)` - The updated conversation object
353    /// * `Err(OpenAIToolError)` - If the request fails
354    ///
355    /// # Example
356    ///
357    /// ```rust,no_run
358    /// use openai_tools::conversations::request::Conversations;
359    /// use std::collections::HashMap;
360    ///
361    /// #[tokio::main]
362    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
363    ///     let conversations = Conversations::new()?;
364    ///
365    ///     let mut metadata = HashMap::new();
366    ///     metadata.insert("topic".to_string(), "updated-topic".to_string());
367    ///
368    ///     let conv = conversations.update("conv_abc123", metadata).await?;
369    ///     println!("Updated: {:?}", conv.metadata);
370    ///     Ok(())
371    /// }
372    /// ```
373    pub async fn update(&self, conversation_id: &str, metadata: HashMap<String, String>) -> Result<Conversation> {
374        let (client, headers) = self.create_client()?;
375        let url = format!("{}/{}", self.auth.endpoint(CONVERSATIONS_PATH), conversation_id);
376
377        let request_body = UpdateConversationRequest { metadata };
378        let body = serde_json::to_string(&request_body)?;
379
380        let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
381
382        let status = response.status();
383        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
384
385        if cfg!(test) {
386            tracing::info!("Response content: {}", content);
387        }
388
389        if !status.is_success() {
390            return Err(Self::handle_error(status, &content));
391        }
392
393        serde_json::from_str::<Conversation>(&content).map_err(OpenAIToolError::SerdeJsonError)
394    }
395
396    /// Deletes a conversation.
397    ///
398    /// # Arguments
399    ///
400    /// * `conversation_id` - The ID of the conversation to delete
401    ///
402    /// # Returns
403    ///
404    /// * `Ok(DeleteConversationResponse)` - Confirmation of deletion
405    /// * `Err(OpenAIToolError)` - If the request fails
406    ///
407    /// # Example
408    ///
409    /// ```rust,no_run
410    /// use openai_tools::conversations::request::Conversations;
411    ///
412    /// #[tokio::main]
413    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
414    ///     let conversations = Conversations::new()?;
415    ///     let result = conversations.delete("conv_abc123").await?;
416    ///
417    ///     if result.deleted {
418    ///         println!("Conversation {} was deleted", result.id);
419    ///     }
420    ///     Ok(())
421    /// }
422    /// ```
423    pub async fn delete(&self, conversation_id: &str) -> Result<DeleteConversationResponse> {
424        let (client, headers) = self.create_client()?;
425        let url = format!("{}/{}", self.auth.endpoint(CONVERSATIONS_PATH), conversation_id);
426
427        let response = client.delete(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
428
429        let status = response.status();
430        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
431
432        if cfg!(test) {
433            tracing::info!("Response content: {}", content);
434        }
435
436        if !status.is_success() {
437            return Err(Self::handle_error(status, &content));
438        }
439
440        serde_json::from_str::<DeleteConversationResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
441    }
442
443    /// Creates items in a conversation.
444    ///
445    /// You can add up to 20 items at a time.
446    ///
447    /// # Arguments
448    ///
449    /// * `conversation_id` - The ID of the conversation
450    /// * `items` - The items to add to the conversation
451    ///
452    /// # Returns
453    ///
454    /// * `Ok(ConversationItemListResponse)` - The created items
455    /// * `Err(OpenAIToolError)` - If the request fails
456    ///
457    /// # Example
458    ///
459    /// ```rust,no_run
460    /// use openai_tools::conversations::request::Conversations;
461    /// use openai_tools::conversations::response::InputItem;
462    ///
463    /// #[tokio::main]
464    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
465    ///     let conversations = Conversations::new()?;
466    ///
467    ///     let items = vec![
468    ///         InputItem::user_message("What is the weather like?"),
469    ///         InputItem::assistant_message("I'd be happy to help with weather information!"),
470    ///     ];
471    ///
472    ///     let result = conversations.create_items("conv_abc123", items).await?;
473    ///     println!("Added {} items", result.data.len());
474    ///     Ok(())
475    /// }
476    /// ```
477    pub async fn create_items(&self, conversation_id: &str, items: Vec<InputItem>) -> Result<ConversationItemListResponse> {
478        let (client, headers) = self.create_client()?;
479        let url = format!("{}/{}/items", self.auth.endpoint(CONVERSATIONS_PATH), conversation_id);
480
481        let request_body = CreateItemsRequest { items };
482        let body = serde_json::to_string(&request_body)?;
483
484        let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
485
486        let status = response.status();
487        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
488
489        if cfg!(test) {
490            tracing::info!("Response content: {}", content);
491        }
492
493        if !status.is_success() {
494            return Err(Self::handle_error(status, &content));
495        }
496
497        serde_json::from_str::<ConversationItemListResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
498    }
499
500    /// Lists items in a conversation.
501    ///
502    /// # Arguments
503    ///
504    /// * `conversation_id` - The ID of the conversation
505    /// * `limit` - Maximum number of items to return (1-100, default 20)
506    /// * `after` - Cursor for pagination (item ID to start after)
507    /// * `order` - Sort order ("asc" or "desc", default "desc")
508    /// * `include` - Additional data to include in the response
509    ///
510    /// # Returns
511    ///
512    /// * `Ok(ConversationItemListResponse)` - The list of items
513    /// * `Err(OpenAIToolError)` - If the request fails
514    ///
515    /// # Example
516    ///
517    /// ```rust,no_run
518    /// use openai_tools::conversations::request::{Conversations, ConversationInclude};
519    ///
520    /// #[tokio::main]
521    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
522    ///     let conversations = Conversations::new()?;
523    ///
524    ///     // List items with pagination
525    ///     let items = conversations.list_items(
526    ///         "conv_abc123",
527    ///         Some(20),
528    ///         None,
529    ///         Some("desc"),
530    ///         None,
531    ///     ).await?;
532    ///
533    ///     for item in &items.data {
534    ///         println!("Item: {} ({})", item.id, item.item_type);
535    ///     }
536    ///     Ok(())
537    /// }
538    /// ```
539    pub async fn list_items(
540        &self,
541        conversation_id: &str,
542        limit: Option<u32>,
543        after: Option<&str>,
544        order: Option<&str>,
545        include: Option<Vec<ConversationInclude>>,
546    ) -> Result<ConversationItemListResponse> {
547        let (client, headers) = self.create_client()?;
548
549        // Build query parameters
550        let mut params = Vec::new();
551        if let Some(l) = limit {
552            params.push(format!("limit={}", l));
553        }
554        if let Some(a) = after {
555            params.push(format!("after={}", a));
556        }
557        if let Some(o) = order {
558            params.push(format!("order={}", o));
559        }
560        if let Some(inc) = include {
561            for i in inc {
562                params.push(format!("include[]={}", i.as_str()));
563            }
564        }
565
566        let url = if params.is_empty() {
567            format!("{}/{}/items", self.auth.endpoint(CONVERSATIONS_PATH), conversation_id)
568        } else {
569            format!("{}/{}/items?{}", self.auth.endpoint(CONVERSATIONS_PATH), conversation_id, params.join("&"))
570        };
571
572        let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
573
574        let status = response.status();
575        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
576
577        if cfg!(test) {
578            tracing::info!("Response content: {}", content);
579        }
580
581        if !status.is_success() {
582            return Err(Self::handle_error(status, &content));
583        }
584
585        serde_json::from_str::<ConversationItemListResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
586    }
587
588    /// Lists all conversations (if available).
589    ///
590    /// Note: This endpoint may not be available in all API versions.
591    ///
592    /// # Arguments
593    ///
594    /// * `limit` - Maximum number of conversations to return (1-100, default 20)
595    /// * `after` - Cursor for pagination (conversation ID to start after)
596    ///
597    /// # Returns
598    ///
599    /// * `Ok(ConversationListResponse)` - The list of conversations
600    /// * `Err(OpenAIToolError)` - If the request fails
601    ///
602    /// # Example
603    ///
604    /// ```rust,no_run
605    /// use openai_tools::conversations::request::Conversations;
606    ///
607    /// #[tokio::main]
608    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
609    ///     let conversations = Conversations::new()?;
610    ///
611    ///     let response = conversations.list(Some(10), None).await?;
612    ///     for conv in &response.data {
613    ///         println!("Conversation: {} (created: {})", conv.id, conv.created_at);
614    ///     }
615    ///     Ok(())
616    /// }
617    /// ```
618    pub async fn list(&self, limit: Option<u32>, after: Option<&str>) -> Result<ConversationListResponse> {
619        let (client, headers) = self.create_client()?;
620
621        // Build query parameters
622        let mut params = Vec::new();
623        if let Some(l) = limit {
624            params.push(format!("limit={}", l));
625        }
626        if let Some(a) = after {
627            params.push(format!("after={}", a));
628        }
629
630        let url = if params.is_empty() {
631            self.auth.endpoint(CONVERSATIONS_PATH)
632        } else {
633            format!("{}?{}", self.auth.endpoint(CONVERSATIONS_PATH), params.join("&"))
634        };
635
636        let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
637
638        let status = response.status();
639        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
640
641        if cfg!(test) {
642            tracing::info!("Response content: {}", content);
643        }
644
645        if !status.is_success() {
646            return Err(Self::handle_error(status, &content));
647        }
648
649        serde_json::from_str::<ConversationListResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
650    }
651}