Skip to main content

quickstart/
quickstart.rs

1//! # `OpenAI` Ergonomic Quickstart Guide
2//!
3//! This example demonstrates how to get started with the openai-ergonomic crate
4//! in under 5 minutes. It covers the most common use cases and patterns you'll
5//! need for building AI-powered applications.
6//!
7//! ## Setup Instructions
8//!
9//! 1. Set your `OpenAI` API key:
10//!    ```bash
11//!    export OPENAI_API_KEY="sk-your-api-key-here"
12//!    ```
13//!
14//! 2. Run this example:
15//!    ```bash
16//!    cargo run --example quickstart
17//!    ```
18//!
19//! ## What This Example Shows
20//!
21//! - Environment setup and client creation
22//! - Basic chat completions
23//! - Streaming responses (real-time text generation)
24//! - Function/tool calling for external data
25//! - Robust error handling patterns
26//! - Usage tracking and cost monitoring
27//!
28//! This example is designed to be your first step into building with `OpenAI`.
29
30use futures::StreamExt;
31use openai_ergonomic::responses::tool_function;
32use openai_ergonomic::{Client, Error, Response, Result, ToolCallExt};
33use serde_json::json;
34use std::io::{self, Write};
35
36#[tokio::main]
37#[allow(clippy::too_many_lines)] // This is an example showing many features
38async fn main() -> Result<()> {
39    // Initialize logging to see what's happening under the hood
40    tracing_subscriber::fmt().with_env_filter("info").init();
41
42    println!(" OpenAI Ergonomic Quickstart");
43    println!("==============================\n");
44
45    // ==========================================
46    // 1. ENVIRONMENT SETUP & CLIENT CREATION
47    // ==========================================
48
49    println!(" Step 1: Setting up the client");
50
51    // The simplest way to get started - reads OPENAI_API_KEY from environment
52    let client = match Client::from_env() {
53        Ok(client_builder) => {
54            println!(" Client created successfully!");
55            client_builder.build()
56        }
57        Err(e) => {
58            eprintln!(" Failed to create client: {e}");
59            eprintln!(" Make sure you've set OPENAI_API_KEY environment variable");
60            eprintln!("   Example: export OPENAI_API_KEY=\"sk-your-key-here\"");
61            return Err(e);
62        }
63    };
64
65    // ==========================================
66    // 2. BASIC CHAT COMPLETION
67    // ==========================================
68
69    println!("\n Step 2: Basic chat completion");
70
71    // The simplest way to get a response from ChatGPT
72    let builder = client.chat_simple("What is Rust programming language in one sentence?");
73    let response = client.send_chat(builder).await;
74
75    match response {
76        Ok(chat_response) => {
77            println!(" Got response!");
78            if let Some(content) = chat_response.content() {
79                println!(" AI: {content}");
80            }
81
82            // Show usage information for cost tracking
83            if let Some(usage) = &chat_response.inner().usage {
84                println!(
85                    " Usage: {} prompt + {} completion = {} total tokens",
86                    usage.prompt_tokens, usage.completion_tokens, usage.total_tokens
87                );
88            }
89        }
90        Err(e) => {
91            println!(" Chat completion failed: {e}");
92            // Continue with other examples even if this one fails
93        }
94    }
95
96    // ==========================================
97    // 3. CHAT WITH SYSTEM MESSAGE
98    // ==========================================
99
100    println!("\n Step 3: Chat with system context");
101
102    // System messages help set the AI's behavior and context
103    let builder = client.chat_with_system(
104        "You are a helpful coding mentor who explains things simply",
105        "Explain what a HashMap is in Rust",
106    );
107    let response = client.send_chat(builder).await;
108
109    match response {
110        Ok(chat_response) => {
111            println!(" Got contextual response!");
112            if let Some(content) = chat_response.content() {
113                println!("‍ Mentor: {content}");
114            }
115        }
116        Err(e) => {
117            println!(" Contextual chat failed: {e}");
118        }
119    }
120
121    // ==========================================
122    // 4. STREAMING RESPONSES
123    // ==========================================
124
125    println!("\n Step 4: Streaming response (real-time)");
126
127    // Streaming lets you see the response as it's being generated
128    // This is great for chatbots and interactive applications
129    print!(" AI is typing: ");
130    io::stdout().flush().unwrap();
131
132    let builder = client
133        .responses()
134        .user("Write a short haiku about programming")
135        .temperature(0.7);
136
137    // Use send_responses_stream for real streaming
138    let stream_result = client.send_responses_stream(builder).await;
139
140    match stream_result {
141        Ok(mut stream) => {
142            // Process each chunk as it arrives
143            while let Some(chunk_result) = stream.next().await {
144                match chunk_result {
145                    Ok(chunk) => {
146                        if let Some(content) = chunk.content() {
147                            print!("{content}");
148                            io::stdout().flush().unwrap();
149                        }
150                    }
151                    Err(e) => {
152                        println!("\n Error processing chunk: {e}");
153                        break;
154                    }
155                }
156            }
157            println!(); // New line after streaming
158        }
159        Err(e) => {
160            println!("\n Failed to get streaming response: {e}");
161        }
162    }
163
164    // ==========================================
165    // 5. FUNCTION/TOOL CALLING
166    // ==========================================
167
168    println!("\n Step 5: Using tools/functions");
169
170    // Tools let the AI call external functions to get real data
171    // Here we define a weather function as an example
172    let weather_tool = tool_function(
173        "get_current_weather",
174        "Get the current weather for a given location",
175        json!({
176            "type": "object",
177            "properties": {
178                "location": {
179                    "type": "string",
180                    "description": "The city name, e.g. 'San Francisco, CA'"
181                },
182                "unit": {
183                    "type": "string",
184                    "enum": ["celsius", "fahrenheit"],
185                    "description": "Temperature unit"
186                }
187            },
188            "required": ["location"]
189        }),
190    );
191
192    let builder = client
193        .responses()
194        .user("What's the weather like in Tokyo?")
195        .tool(weather_tool);
196    let response = client.send_responses(builder).await;
197
198    match response {
199        Ok(chat_response) => {
200            println!(" Got response with potential tool calls!");
201
202            // Check if the AI wants to call our weather function
203            let tool_calls = chat_response.tool_calls();
204            if !tool_calls.is_empty() {
205                println!(" AI requested tool calls:");
206                for tool_call in tool_calls {
207                    let function_name = tool_call.function_name();
208                    println!("   Function: {function_name}");
209                    let function_args = tool_call.function_arguments();
210                    println!("   Arguments: {function_args}");
211
212                    // In a real app, you'd execute the function here
213                    // and send the result back to the AI
214                    println!("    In a real app, you'd call your weather API here");
215                }
216            } else if let Some(content) = chat_response.content() {
217                println!(" AI: {content}");
218            }
219        }
220        Err(e) => {
221            println!(" Tool calling example failed: {e}");
222        }
223    }
224
225    // ==========================================
226    // 6. ERROR HANDLING PATTERNS
227    // ==========================================
228
229    println!("\n Step 6: Error handling patterns");
230
231    // Show how to handle different types of errors gracefully
232    let builder = client.chat_simple(""); // Empty message might cause an error
233    let bad_response = client.send_chat(builder).await;
234
235    match bad_response {
236        Ok(response) => {
237            println!(" Unexpectedly succeeded with empty message");
238            if let Some(content) = response.content() {
239                println!(" AI: {content}");
240            }
241        }
242        Err(Error::Api {
243            status, message, ..
244        }) => {
245            println!(" API Error (HTTP {status}):");
246            println!("   Message: {message}");
247            println!(" This is normal - we sent an invalid request");
248        }
249        Err(Error::RateLimit { .. }) => {
250            println!(" Rate limited - you're sending requests too fast");
251            println!(" In a real app, you'd implement exponential backoff");
252        }
253        Err(Error::Http(_)) => {
254            println!(" HTTP/Network error");
255            println!(" Check your internet connection and API key");
256        }
257        Err(e) => {
258            println!(" Other error: {e}");
259        }
260    }
261
262    // ==========================================
263    // 7. COMPLETE REAL-WORLD EXAMPLE
264    // ==========================================
265
266    println!("\n Step 7: Complete real-world example");
267    println!("Building a simple AI assistant that can:");
268    println!("- Answer questions with context");
269    println!("- Track conversation costs");
270    println!("- Handle errors gracefully");
271
272    let mut total_tokens = 0;
273
274    // Simulate a conversation with context and cost tracking
275    let questions = [
276        "What is the capital of France?",
277        "What's special about that city?",
278        "How many people live there?",
279    ];
280
281    for (i, question) in questions.iter().enumerate() {
282        println!("\n User: {question}");
283
284        let builder = client
285            .responses()
286            .system(
287                "You are a knowledgeable geography expert. Keep answers concise but informative.",
288            )
289            .user(*question)
290            .temperature(0.1); // Lower temperature for more factual responses
291        let response = client.send_responses(builder).await;
292
293        match response {
294            Ok(chat_response) => {
295                if let Some(content) = chat_response.content() {
296                    println!(" Assistant: {content}");
297                }
298
299                // Track token usage for cost monitoring
300                if let Some(usage) = chat_response.usage() {
301                    total_tokens += usage.total_tokens;
302                    println!(
303                        " This exchange: {} tokens (Running total: {})",
304                        usage.total_tokens, total_tokens
305                    );
306                }
307            }
308            Err(e) => {
309                println!(" Question {} failed: {}", i + 1, e);
310                // In a real app, you might retry or log this error
311            }
312        }
313    }
314
315    // ==========================================
316    // 8. WRAP UP & NEXT STEPS
317    // ==========================================
318
319    println!("\n Quickstart Complete!");
320    println!("======================");
321    println!("You've successfully:");
322    println!(" Created an OpenAI client");
323    println!(" Made basic chat completions");
324    println!(" Used streaming responses");
325    println!(" Implemented tool/function calling");
326    println!(" Handled errors gracefully");
327    println!(" Built a complete conversational AI");
328    println!("\n Total tokens used in examples: {total_tokens}");
329    println!(
330        " Estimated cost: ~${:.4} (assuming GPT-4 pricing)",
331        f64::from(total_tokens) * 0.03 / 1000.0
332    );
333
334    println!("\n Next Steps:");
335    println!("- Check out other examples in the examples/ directory");
336    println!("- Read the documentation: https://docs.rs/openai-ergonomic");
337    println!("- Explore advanced features like vision, audio, and assistants");
338    println!("- Build your own AI-powered applications!");
339
340    Ok(())
341}
342
343/// Example helper function demonstrating custom error handling.
344///
345/// In real applications, you might want to wrap API calls in functions
346/// like this to add custom retry logic, logging, or error transformation.
347#[allow(dead_code)]
348async fn robust_chat_call(client: &Client, message: &str) -> Result<String> {
349    const MAX_RETRIES: usize = 3;
350    let mut last_error = None;
351
352    for attempt in 1..=MAX_RETRIES {
353        let builder = client.chat_simple(message);
354        match client.send_chat(builder).await {
355            Ok(response) => {
356                if let Some(content) = response.content() {
357                    return Ok(content.to_string());
358                }
359                return Err(Error::Api {
360                    status: 200,
361                    message: "No content in response".to_string(),
362                    error_type: None,
363                    error_code: None,
364                });
365            }
366            Err(Error::RateLimit { .. }) if attempt < MAX_RETRIES => {
367                // Exponential backoff for rate limits
368                let delay = std::time::Duration::from_millis(1000 * attempt as u64);
369                tokio::time::sleep(delay).await;
370                // Brief delay before retry
371                tokio::time::sleep(std::time::Duration::from_millis(500)).await;
372            }
373            Err(e) => {
374                last_error = Some(e);
375                if attempt < MAX_RETRIES {
376                    // Brief delay before retry
377                    tokio::time::sleep(std::time::Duration::from_millis(500)).await;
378                }
379            }
380        }
381    }
382
383    Err(last_error.unwrap_or_else(|| Error::Api {
384        status: 0,
385        message: "Unknown error after retries".to_string(),
386        error_type: None,
387        error_code: None,
388    }))
389}