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}