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}