openai_tools/responses/
mod.rs

1//! OpenAI Responses API Module
2//!
3//! This module provides functionality for interacting with the OpenAI Responses API,
4//! which is designed for creating AI assistants that can handle various types of input
5//! including text, images, and structured data. The Responses API offers a more flexible
6//! and powerful way to build conversational AI applications compared to the traditional
7//! Chat Completions API.
8//!
9//! # Key Features
10//!
11//! - **Multi-modal Input**: Support for text, images, and other content types
12//! - **Structured Output**: JSON schema-based response formatting
13//! - **Tool Integration**: Function calling capabilities with custom tools
14//! - **Flexible Instructions**: System-level instructions for AI behavior
15//! - **Rich Content Handling**: Support for complex message structures
16//!
17//! # Quick Start
18//!
19//! ```rust,no_run
20//! use openai_tools::responses::request::Responses;
21//! use openai_tools::common::message::Message;
22//! use openai_tools::common::role::Role;
23//!
24//! #[tokio::main]
25//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
26//!     // Initialize the responses client
27//!     let mut responses = Responses::new();
28//!     
29//!     // Configure basic parameters
30//!     responses
31//!         .model_id("gpt-4o-mini")
32//!         .instructions("You are a helpful assistant.");
33//!     
34//!     // Simple text input
35//!     responses.plain_text_input("Hello! How are you today?");
36//!
37//!     // Send the request
38//!     let response = responses.complete().await?;
39//!     
40//!     println!("AI Response: {}",
41//!              response.output[0].content.as_ref().unwrap()[0].text);
42//!     Ok(())
43//! }
44//! ```
45//!
46//! # Advanced Usage Examples
47//!
48//! ## Using Message-based Conversations
49//!
50//! ```rust,no_run
51//! use openai_tools::responses::request::Responses;
52//! use openai_tools::common::message::Message;
53//! use openai_tools::common::role::Role;
54//!
55//! #[tokio::main]
56//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
57//!     let mut responses = Responses::new();
58//!     
59//!     responses
60//!         .model_id("gpt-4o-mini")
61//!         .instructions("You are a knowledgeable assistant.");
62//!     
63//!     // Create a conversation with multiple messages
64//!     let messages = vec![
65//!         Message::from_string(Role::User, "What is artificial intelligence?"),
66//!         Message::from_string(Role::Assistant, "AI is a field of computer science..."),
67//!         Message::from_string(Role::User, "Can you give me a simple example?"),
68//!     ];
69//!     
70//!     responses.messages(messages);
71//!     
72//!     let response = responses.complete().await?;
73//!     println!("Response: {}", response.output[0].content.as_ref().unwrap()[0].text);
74//!     Ok(())
75//! }
76//! ```
77//!
78//! ## Multi-modal Input with Images
79//!
80//! ```rust,no_run
81//! use openai_tools::responses::request::Responses;
82//! use openai_tools::common::message::{Message, Content};
83//! use openai_tools::common::role::Role;
84//!
85//! #[tokio::main]
86//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
87//!     let mut responses = Responses::new();
88//!     
89//!     responses
90//!         .model_id("gpt-4o-mini")
91//!         .instructions("You are an image analysis assistant.");
92//!     
93//!     // Create a message with both text and image content
94//!     let message = Message::from_message_array(
95//!         Role::User,
96//!         vec![
97//!             Content::from_text("What do you see in this image?"),
98//!             Content::from_image_file("path/to/image.jpg"),
99//!         ],
100//!     );
101//!     
102//!     responses.messages(vec![message]);
103//!     
104//!     let response = responses.complete().await?;
105//!     println!("Image analysis: {}", response.output[0].content.as_ref().unwrap()[0].text);
106//!     Ok(())
107//! }
108//! ```
109//!
110//! ## Structured Output with JSON Schema
111//!
112//! ```rust,no_run
113//! use openai_tools::responses::request::Responses;
114//! use openai_tools::common::message::Message;
115//! use openai_tools::common::role::Role;
116//! use openai_tools::common::structured_output::Schema;
117//! use serde::{Deserialize, Serialize};
118//!
119//! #[derive(Debug, Serialize, Deserialize)]
120//! struct ProductInfo {
121//!     name: String,
122//!     price: f64,
123//!     category: String,
124//!     in_stock: bool,
125//! }
126//!
127//! #[tokio::main]
128//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
129//!     let mut responses = Responses::new();
130//!     
131//!     responses.model_id("gpt-4o-mini");
132//!     
133//!     let messages = vec![
134//!         Message::from_string(Role::User,
135//!             "Extract product information: 'MacBook Pro 16-inch, $2499, Electronics, Available'")
136//!     ];
137//!     responses.messages(messages);
138//!     
139//!     // Define JSON schema for structured output
140//!     let mut schema = Schema::responses_json_schema("product_info");
141//!     schema.add_property("name", "string", "Product name");
142//!     schema.add_property("price", "number", "Product price");
143//!     schema.add_property("category", "string", "Product category");
144//!     schema.add_property("in_stock", "boolean", "Availability status");
145//!     
146//!     responses.text(schema);
147//!     
148//!     let response = responses.complete().await?;
149//!     
150//!     // Parse structured response
151//!     let product: ProductInfo = serde_json::from_str(
152//!         &response.output[0].content.as_ref().unwrap()[0].text
153//!     )?;
154//!     
155//!     println!("Product: {} - ${} ({})", product.name, product.price, product.category);
156//!     Ok(())
157//! }
158//! ```
159//!
160//! ## Function Calling with Tools
161//!
162//! ```rust,no_run
163//! use openai_tools::responses::request::Responses;
164//! use openai_tools::common::message::Message;
165//! use openai_tools::common::role::Role;
166//! use openai_tools::common::tool::Tool;
167//! use openai_tools::common::parameters::{Parameters, ParameterProp};
168//!
169//! #[tokio::main]
170//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
171//!     let mut responses = Responses::new();
172//!     
173//!     responses
174//!         .model_id("gpt-4o-mini")
175//!         .instructions("You are a helpful calculator assistant.");
176//!     
177//!     // Define a calculator tool
178//!     let calculator_tool = Tool::function(
179//!         "calculator",
180//!         "Perform basic arithmetic operations",
181//!         vec![
182//!             ("operation", ParameterProp::string("add, subtract, multiply, or divide")),
183//!             ("a", ParameterProp::number("First number")),
184//!             ("b", ParameterProp::number("Second number")),
185//!         ],
186//!         false,
187//!     );
188//!     
189//!     let messages = vec![
190//!         Message::from_string(Role::User, "Calculate 15 * 7 using the calculator tool")
191//!     ];
192//!     
193//!     responses
194//!         .messages(messages)
195//!         .tools(vec![calculator_tool]);
196//!     
197//!     let response = responses.complete().await?;
198//!     
199//!     // Check if the model made a function call
200//!     if response.output[0].type_name == "function_call" {
201//!         println!("Function called: {}", response.output[0].name.as_ref().unwrap());
202//!         println!("Call ID: {}", response.output[0].call_id.as_ref().unwrap());
203//!         // Handle the function call and continue the conversation...
204//!     } else {
205//!         println!("Text response: {}", response.output[0].content.as_ref().unwrap()[0].text);
206//!     }
207//!     Ok(())
208//! }
209//! ```
210//!
211//! # API Differences from Chat Completions
212//!
213//! The Responses API differs from the Chat Completions API in several key ways:
214//!
215//! - **Input Format**: More flexible input handling with support for various content types
216//! - **Output Structure**: Different response format optimized for assistant-style interactions
217//! - **Instructions**: Dedicated field for system-level instructions
218//! - **Multi-modal**: Native support for images and other media types
219//! - **Tool Integration**: Enhanced function calling capabilities
220//!
221//! # Environment Setup
222//!
223//! Ensure your OpenAI API key is configured:
224//!
225//! ```bash
226//! export OPENAI_API_KEY="your-api-key-here"
227//! ```
228//!
229//! Or in a `.env` file:
230//!
231//! ```text
232//! OPENAI_API_KEY=your-api-key-here
233//! ```
234//!
235//! # Error Handling
236//!
237//! All operations return `Result` types for proper error handling:
238//!
239//! ```rust,no_run
240//! use openai_tools::responses::request::Responses;
241//! use openai_tools::common::errors::OpenAIToolError;
242//!
243//! #[tokio::main]
244//! async fn main() {
245//!     let mut responses = Responses::new();
246//!     
247//!     match responses.model_id("gpt-4o-mini").complete().await {
248//!         Ok(response) => {
249//!             println!("Success: {}", response.output[0].content.as_ref().unwrap()[0].text);
250//!         }
251//!         Err(OpenAIToolError::RequestError(e)) => {
252//!             eprintln!("Network error: {}", e);
253//!         }
254//!         Err(OpenAIToolError::SerdeJsonError(e)) => {
255//!             eprintln!("JSON parsing error: {}", e);
256//!         }
257//!         Err(e) => {
258//!             eprintln!("Other error: {}", e);
259//!         }
260//!     }
261//! }
262//! ```
263
264pub mod request;
265pub mod response;
266
267#[cfg(test)]
268mod tests {
269    use crate::common::{
270        message::{Content, Message},
271        parameters::ParameterProp,
272        role::Role,
273        structured_output::Schema,
274        tool::Tool,
275    };
276    use crate::responses::request::Responses;
277
278    use serde::Deserialize;
279    use std::sync::Once;
280    use tracing_subscriber::EnvFilter;
281
282    static INIT: Once = Once::new();
283
284    fn init_tracing() {
285        INIT.call_once(|| {
286            // `RUST_LOG` 環境変数があればそれを使い、なければ "info"
287            let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
288            tracing_subscriber::fmt()
289                .with_env_filter(filter)
290                .with_test_writer() // `cargo test` / nextest 用
291                .init();
292        });
293    }
294
295    #[tokio::test]
296    async fn test_responses_with_plain_text() {
297        init_tracing();
298        let mut responses = Responses::new();
299        responses.model_id("gpt-4o-mini");
300        responses.instructions("test instructions");
301        responses.plain_text_input("Hello world!");
302
303        let body_json = serde_json::to_string_pretty(&responses.request_body).unwrap();
304        tracing::info!("Request body: {}", body_json);
305
306        let mut counter = 3;
307        loop {
308            match responses.complete().await {
309                Ok(res) => {
310                    tracing::info!("Response: {}", serde_json::to_string_pretty(&res).unwrap());
311                    assert!(res.output[0].content.as_ref().unwrap()[0].text.len() > 0);
312                    break;
313                }
314                Err(e) => {
315                    tracing::error!("Error: {} (retrying... {})", e, counter);
316                    counter -= 1;
317                    if counter == 0 {
318                        assert!(false, "Failed to complete responses after 3 attempts");
319                    }
320                }
321            }
322        }
323    }
324
325    #[tokio::test]
326    async fn test_responses_with_messages() {
327        init_tracing();
328        let mut responses = Responses::new();
329        responses.model_id("gpt-4o-mini");
330        responses.instructions("test instructions");
331        let messages = vec![Message::from_string(Role::User, "Hello world!")];
332        responses.messages(messages);
333
334        let body_json = serde_json::to_string_pretty(&responses.request_body).unwrap();
335        tracing::info!("Request body: {}", body_json);
336
337        let mut counter = 3;
338        loop {
339            match responses.complete().await {
340                Ok(res) => {
341                    tracing::info!("Response: {}", serde_json::to_string_pretty(&res).unwrap());
342                    assert!(res.output[0].content.as_ref().unwrap()[0].text.len() > 0);
343                    break;
344                }
345                Err(e) => {
346                    tracing::error!("Error: {} (retrying... {})", e, counter);
347                    counter -= 1;
348                    if counter == 0 {
349                        assert!(false, "Failed to complete responses after 3 attempts");
350                    }
351                }
352            }
353        }
354    }
355
356    #[tokio::test]
357    async fn test_responses_with_tools() {
358        init_tracing();
359        let mut responses = Responses::new();
360        responses.model_id("gpt-4o-mini");
361        responses.instructions("test instructions");
362        let messages = vec![Message::from_string(Role::User, "Calculate 2 + 2 using a calculator tool.")];
363        responses.messages(messages);
364
365        let tool = Tool::function(
366            "calculator",
367            "A simple calculator tool",
368            vec![("a", ParameterProp::number("The first number")), ("b", ParameterProp::number("The second number"))],
369            false,
370        );
371        responses.tools(vec![tool]);
372
373        let body_json = serde_json::to_string_pretty(&responses.request_body).unwrap();
374        println!("Request body: {}", body_json);
375
376        let mut counter = 3;
377        loop {
378            match responses.complete().await {
379                Ok(res) => {
380                    tracing::info!("Response: {}", serde_json::to_string_pretty(&res).unwrap());
381                    assert_eq!(res.output[0].type_name, "function_call");
382                    assert_eq!(res.output[0].name.as_ref().unwrap(), "calculator");
383                    assert!(res.output[0].call_id.as_ref().unwrap().len() > 0);
384                    break;
385                }
386                Err(e) => {
387                    tracing::error!("Error: {} (retrying... {})", e, counter);
388                    counter -= 1;
389                    if counter == 0 {
390                        assert!(false, "Failed to complete responses after 3 attempts");
391                    }
392                }
393            }
394        }
395    }
396
397    #[derive(Debug, Deserialize)]
398    struct TestResponse {
399        pub capital: String,
400    }
401    #[tokio::test]
402    async fn test_responses_with_json_schema() {
403        init_tracing();
404        let mut responses = Responses::new();
405        responses.model_id("gpt-4o-mini");
406
407        let messages = vec![Message::from_string(Role::User, "What is the capital of France?")];
408        responses.messages(messages);
409
410        let mut schema = Schema::responses_json_schema("capital");
411        schema.add_property("capital", "string", "The capital city of France");
412        responses.text(schema);
413
414        let mut counter = 3;
415        loop {
416            match responses.complete().await {
417                Ok(res) => {
418                    tracing::info!("Response: {}", serde_json::to_string_pretty(&res).unwrap());
419                    let res = serde_json::from_str::<TestResponse>(res.output[0].content.as_ref().unwrap()[0].text.as_str()).unwrap();
420                    assert_eq!(res.capital, "Paris");
421                    break;
422                }
423                Err(e) => {
424                    tracing::error!("Error: {} (retrying... {})", e, counter);
425                    counter -= 1;
426                    if counter == 0 {
427                        assert!(false, "Failed to complete responses after 3 attempts");
428                    }
429                }
430            }
431        }
432    }
433
434    #[tokio::test]
435    async fn test_responses_with_image_input() {
436        init_tracing();
437        let mut responses = Responses::new();
438        responses.model_id("gpt-4o-mini");
439        responses.instructions("test instructions");
440
441        let message = Message::from_message_array(
442            Role::User,
443            vec![Content::from_text("Do you find a clock in this image?"), Content::from_image_file("src/test_rsc/sample_image.jpg")],
444        );
445        responses.messages(vec![message]);
446
447        let body_json = serde_json::to_string_pretty(&responses.request_body).unwrap();
448        tracing::info!("Request body: {}", body_json);
449
450        let mut counter = 3;
451        loop {
452            match responses.complete().await {
453                Ok(res) => {
454                    tracing::info!("Response: {}", serde_json::to_string_pretty(&res).unwrap());
455                    assert!(res.output[0].content.as_ref().unwrap()[0].text.len() > 0);
456                    break;
457                }
458                Err(e) => {
459                    tracing::error!("Error: {} (retrying... {})", e, counter);
460                    counter -= 1;
461                    if counter == 0 {
462                        assert!(false, "Failed to complete responses after 3 attempts");
463                    }
464                }
465            }
466        }
467    }
468}