diff --git a/.doc_syle/DOCUMENTATION_CRITIQUE.md b/.doc_syle/DOCUMENTATION_CRITIQUE.md new file mode 100644 index 000000000..e69de29bb diff --git a/.doc_syle/DOC_STYLE.md b/.doc_syle/DOC_STYLE.md new file mode 100644 index 000000000..e69de29bb diff --git a/.doc_syle/DOC_STYLE_UPDATES.md b/.doc_syle/DOC_STYLE_UPDATES.md new file mode 100644 index 000000000..e69de29bb diff --git a/.doc_syle/IMPROVEMENTS_SUMMARY.md b/.doc_syle/IMPROVEMENTS_SUMMARY.md new file mode 100644 index 000000000..e69de29bb diff --git a/.gitignore b/.gitignore index d81b13ca1..7fa852da8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ target/ # nodejs stuff node_modules/ dist/ +.* \ No newline at end of file diff --git a/rig-core/src/client/completion.rs b/rig-core/src/client/completion.rs index 5a8ca2dd3..90b91e872 100644 --- a/rig-core/src/client/completion.rs +++ b/rig-core/src/client/completion.rs @@ -11,46 +11,160 @@ use serde::{Deserialize, Serialize}; use std::future::Future; use std::sync::Arc; -/// A provider client with completion capabilities. -/// Clone is required for conversions between client types. +/// A provider client with text completion capabilities. +/// +/// This trait extends [`ProviderClient`] to provide text generation functionality. +/// Providers that implement this trait can create completion models and agents. +/// +/// # When to Implement +/// +/// Implement this trait for provider clients that support: +/// - Text generation (chat completions, prompts) +/// - Multi-turn conversations +/// - Tool/function calling +/// - Streaming responses +/// +/// # Examples +/// +/// ```no_run +/// use rig::prelude::*; +/// use rig::providers::openai::{Client, self}; +/// use rig::completion::Prompt; +/// +/// # async fn example() -> Result<(), Box> { +/// let client = Client::new("api-key"); +/// +/// // Create a completion model +/// let model = client.completion_model(openai::GPT_4O); +/// +/// // Or create an agent with configuration +/// let agent = client.agent(openai::GPT_4O) +/// .preamble("You are a helpful assistant") +/// .temperature(0.7) +/// .max_tokens(1000) +/// .build(); +/// +/// let response = agent.prompt("Hello!").await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// # See Also +/// +/// - [`crate::completion::CompletionModel`] - The model trait for making completion requests +/// - [`crate::agent::Agent`] - High-level agent abstraction built on completion models +/// - [`CompletionClientDyn`] - Dynamic dispatch version for runtime polymorphism pub trait CompletionClient: ProviderClient + Clone { /// The type of CompletionModel used by the client. type CompletionModel: CompletionModel; - /// Create a completion model with the given name. + /// Creates a completion model with the specified model identifier. /// - /// # Example with OpenAI - /// ``` + /// This method constructs a completion model that can be used to generate + /// text completions directly or as part of an agent. + /// + /// # Arguments + /// + /// * `model` - The model identifier (e.g., "gpt-4o", "claude-3-7-sonnet") + /// + /// # Examples + /// + /// ```no_run /// use rig::prelude::*; /// use rig::providers::openai::{Client, self}; + /// use rig::completion::CompletionModel; /// - /// // Initialize the OpenAI client - /// let openai = Client::new("your-open-ai-api-key"); + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("your-api-key"); + /// let model = client.completion_model(openai::GPT_4O); /// - /// let gpt4 = openai.completion_model(openai::GPT_4); + /// // Use the model to generate a completion + /// let response = model + /// .completion_request("What is Rust?") + /// .send() + /// .await?; + /// # Ok(()) + /// # } /// ``` fn completion_model(&self, model: &str) -> Self::CompletionModel; - /// Create an agent builder with the given completion model. + /// Creates an agent builder configured with the specified completion model. /// - /// # Example with OpenAI - /// ``` + /// Agents provide a higher-level abstraction over completion models, adding + /// features like conversation management, tool integration, and persistent state. + /// + /// # Arguments + /// + /// * `model` - The model identifier (e.g., "gpt-4o", "claude-3-7-sonnet") + /// + /// # Returns + /// + /// An [`AgentBuilder`] that can be configured with preamble, tools, and other options. + /// + /// # Examples + /// + /// ```no_run /// use rig::prelude::*; /// use rig::providers::openai::{Client, self}; + /// use rig::completion::Prompt; /// - /// // Initialize the OpenAI client - /// let openai = Client::new("your-open-ai-api-key"); + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("your-api-key"); /// - /// let agent = openai.agent(openai::GPT_4) - /// .preamble("You are comedian AI with a mission to make people laugh.") - /// .temperature(0.0) - /// .build(); + /// let agent = client.agent(openai::GPT_4O) + /// .preamble("You are a helpful assistant") + /// .temperature(0.7) + /// .build(); + /// + /// let response = agent.prompt("Hello!").await?; + /// # Ok(()) + /// # } /// ``` fn agent(&self, model: &str) -> AgentBuilder { AgentBuilder::new(self.completion_model(model)) } - /// Create an extractor builder with the given completion model. + /// Creates an extractor builder for structured data extraction. + /// + /// Extractors use the completion model to extract structured data from text, + /// automatically generating the appropriate schema and parsing responses. + /// + /// # Type Parameters + /// + /// * `T` - The type to extract, must implement `JsonSchema`, `Deserialize`, and `Serialize` + /// + /// # Arguments + /// + /// * `model` - The model identifier (e.g., "gpt-4o", "claude-3-7-sonnet") + /// + /// # Returns + /// + /// An [`ExtractorBuilder`] that can be configured and used to extract structured data. + /// + /// # Examples + /// + /// ```no_run + /// use rig::prelude::*; + /// use rig::providers::openai::{Client, self}; + /// use serde::{Deserialize, Serialize}; + /// use schemars::JsonSchema; + /// + /// #[derive(Debug, Deserialize, Serialize, JsonSchema)] + /// struct Person { + /// name: String, + /// age: u32, + /// } + /// + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("api-key"); + /// + /// let extractor = client.extractor::(openai::GPT_4O) + /// .build(); + /// + /// let person = extractor.extract("John Doe is 30 years old").await?; + /// # Ok(()) + /// # } + /// ``` fn extractor(&self, model: &str) -> ExtractorBuilder where T: JsonSchema + for<'a> Deserialize<'a> + Serialize + Send + Sync, @@ -59,9 +173,30 @@ pub trait CompletionClient: ProviderClient + Clone { } } -/// Wraps a CompletionModel in a dyn-compatible way for AgentBuilder. +/// A dynamic handle for completion models enabling trait object usage. +/// +/// This struct wraps a [`CompletionModel`] in a way that allows it to be used +/// as a trait object with [`AgentBuilder`] and other generic contexts. +/// It uses `Arc` internally for efficient cloning. +/// +/// # Examples +/// +/// This type is primarily used internally by the dynamic client builder, +/// but can be useful when you need to store completion models of different types: +/// +/// ```no_run +/// use rig::client::completion::CompletionModelHandle; +/// use rig::agent::AgentBuilder; +/// +/// // CompletionModelHandle allows storing models from different providers +/// fn create_agent(model: CompletionModelHandle) -> AgentBuilder { +/// AgentBuilder::new(model) +/// .preamble("You are a helpful assistant") +/// } +/// ``` #[derive(Clone)] pub struct CompletionModelHandle<'a> { + /// The inner dynamic completion model. pub inner: Arc, } @@ -87,11 +222,25 @@ impl CompletionModel for CompletionModelHandle<'_> { } } +/// Dynamic dispatch version of [`CompletionClient`]. +/// +/// This trait provides the same functionality as [`CompletionClient`] but returns +/// trait objects instead of associated types, enabling runtime polymorphism. +/// It is automatically implemented for all types that implement [`CompletionClient`]. +/// +/// # When to Use +/// +/// Use this trait when you need to work with completion clients of different types +/// at runtime, such as in the [`DynClientBuilder`](crate::client::builder::DynClientBuilder). pub trait CompletionClientDyn: ProviderClient { - /// Create a completion model with the given name. + /// Creates a boxed completion model with the specified model identifier. + /// + /// Returns a trait object that can be used for dynamic dispatch. fn completion_model<'a>(&self, model: &str) -> Box; - /// Create an agent builder with the given completion model. + /// Creates an agent builder with a dynamically-dispatched completion model. + /// + /// Returns an agent builder using [`CompletionModelHandle`] for dynamic dispatch. fn agent<'a>(&self, model: &str) -> AgentBuilder>; } diff --git a/rig-core/src/client/embeddings.rs b/rig-core/src/client/embeddings.rs index 1f3707066..0223af6c0 100644 --- a/rig-core/src/client/embeddings.rs +++ b/rig-core/src/client/embeddings.rs @@ -3,79 +3,198 @@ use crate::client::{AsEmbeddings, ProviderClient}; use crate::embeddings::embedding::EmbeddingModelDyn; use crate::embeddings::{EmbeddingModel, EmbeddingsBuilder}; -/// A provider client with embedding capabilities. -/// Clone is required for conversions between client types. +/// A provider client with vector embedding capabilities. +/// +/// This trait extends [`ProviderClient`] to provide text-to-vector embedding functionality. +/// Providers that implement this trait can create embedding models for semantic search, +/// similarity comparison, and other vector-based operations. +/// +/// # When to Implement +/// +/// Implement this trait for provider clients that support: +/// - Text to vector embeddings +/// - Document embeddings for search +/// - Semantic similarity calculations +/// - Vector database integration +/// +/// # Examples +/// +/// ```no_run +/// use rig::prelude::*; +/// use rig::providers::openai::{Client, self}; +/// +/// # async fn example() -> Result<(), Box> { +/// let client = Client::new("api-key"); +/// +/// // Create an embedding model +/// let model = client.embedding_model(openai::TEXT_EMBEDDING_3_LARGE); +/// +/// // Embed a single text +/// let embedding = model.embed_text("Hello, world!").await?; +/// println!("Vector dimension: {}", embedding.vec.len()); +/// +/// // Or build embeddings for multiple documents +/// let embeddings = client.embeddings(openai::TEXT_EMBEDDING_3_LARGE) +/// .documents(vec![ +/// "First document".to_string(), +/// "Second document".to_string(), +/// ])? +/// .build() +/// .await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// # See Also +/// +/// - [`crate::embeddings::EmbeddingModel`] - The model trait for creating embeddings +/// - [`crate::embeddings::EmbeddingsBuilder`] - Builder for batch embedding operations +/// - [`EmbeddingsClientDyn`] - Dynamic dispatch version for runtime polymorphism pub trait EmbeddingsClient: ProviderClient + Clone { /// The type of EmbeddingModel used by the Client type EmbeddingModel: EmbeddingModel; - /// Create an embedding model with the given name. - /// Note: default embedding dimension of 0 will be used if model is not known. - /// If this is the case, it's better to use function `embedding_model_with_ndims` + /// Creates an embedding model with the specified model identifier. /// - /// # Example - /// ``` + /// This method constructs an embedding model that can convert text into vector embeddings. + /// If the model is not recognized, a default dimension of 0 is used. For unknown models, + /// prefer using [`embedding_model_with_ndims`](Self::embedding_model_with_ndims) to specify the dimension explicitly. + /// + /// # Arguments + /// + /// * `model` - The model identifier (e.g., "text-embedding-3-large", "embed-english-v2.0") + /// + /// # Returns + /// + /// An embedding model that can be used to generate vector embeddings. + /// + /// # Examples + /// + /// ```no_run /// use rig::prelude::*; /// use rig::providers::openai::{Client, self}; + /// use rig::embeddings::EmbeddingModel; /// - /// // Initialize the OpenAI client - /// let openai = Client::new("your-open-ai-api-key"); + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("your-api-key"); + /// let model = client.embedding_model(openai::TEXT_EMBEDDING_3_LARGE); /// - /// let embedding_model = openai.embedding_model(openai::TEXT_EMBEDDING_3_LARGE); + /// // Use the model to generate embeddings + /// let embedding = model.embed_text("Hello, world!").await?; + /// println!("Embedding dimension: {}", embedding.vec.len()); + /// # Ok(()) + /// # } /// ``` fn embedding_model(&self, model: &str) -> Self::EmbeddingModel; - /// Create an embedding model with the given name and the number of dimensions in the embedding generated by the model. + /// Creates an embedding model with explicit dimension specification. /// - /// # Example with OpenAI - /// ``` + /// Use this method when working with models that are not pre-configured in Rig + /// or when you need to explicitly control the embedding dimension. + /// + /// # Arguments + /// + /// * `model` - The model identifier (e.g., "custom-model", "text-embedding-3-large") + /// * `ndims` - The number of dimensions in the generated embeddings + /// + /// # Returns + /// + /// An embedding model configured with the specified dimension. + /// + /// # Examples + /// + /// ```no_run /// use rig::prelude::*; /// use rig::providers::openai::{Client, self}; + /// use rig::embeddings::EmbeddingModel; /// - /// // Initialize the OpenAI client - /// let openai = Client::new("your-open-ai-api-key"); + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("your-api-key"); + /// let model = client.embedding_model_with_ndims("custom-model", 1536); /// - /// let embedding_model = openai.embedding_model("model-unknown-to-rig", 3072); + /// let embedding = model.embed_text("Test text").await?; + /// assert_eq!(embedding.vec.len(), 1536); + /// # Ok(()) + /// # } /// ``` fn embedding_model_with_ndims(&self, model: &str, ndims: usize) -> Self::EmbeddingModel; - /// Create an embedding builder with the given embedding model. + /// Creates an embeddings builder for batch embedding operations. /// - /// # Example with OpenAI - /// ``` + /// The embeddings builder allows you to embed multiple documents at once, + /// which is more efficient than embedding them individually. + /// + /// # Type Parameters + /// + /// * `D` - The document type that implements [`Embed`] + /// + /// # Arguments + /// + /// * `model` - The model identifier (e.g., "text-embedding-3-large") + /// + /// # Returns + /// + /// An [`EmbeddingsBuilder`] that can be used to add documents and build embeddings. + /// + /// # Examples + /// + /// ```no_run /// use rig::prelude::*; /// use rig::providers::openai::{Client, self}; /// - /// // Initialize the OpenAI client - /// let openai = Client::new("your-open-ai-api-key"); + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("your-api-key"); /// - /// let embeddings = openai.embeddings(openai::TEXT_EMBEDDING_3_LARGE) - /// .simple_document("doc0", "Hello, world!") - /// .simple_document("doc1", "Goodbye, world!") + /// let embeddings = client.embeddings(openai::TEXT_EMBEDDING_3_LARGE) + /// .documents(vec![ + /// "Hello, world!".to_string(), + /// "Goodbye, world!".to_string(), + /// ])? /// .build() - /// .await - /// .expect("Failed to embed documents"); + /// .await?; + /// # Ok(()) + /// # } /// ``` fn embeddings(&self, model: &str) -> EmbeddingsBuilder { EmbeddingsBuilder::new(self.embedding_model(model)) } - /// Create an embedding builder with the given name and the number of dimensions in the embedding generated by the model. + /// Creates an embeddings builder with explicit dimension specification. /// - /// # Example with OpenAI - /// ``` + /// This is equivalent to [`embeddings`](Self::embeddings) but allows you to specify + /// the embedding dimension explicitly for unknown models. + /// + /// # Type Parameters + /// + /// * `D` - The document type that implements [`Embed`] + /// + /// # Arguments + /// + /// * `model` - The model identifier + /// * `ndims` - The number of dimensions in the generated embeddings + /// + /// # Returns + /// + /// An [`EmbeddingsBuilder`] configured with the specified dimension. + /// + /// # Examples + /// + /// ```no_run /// use rig::prelude::*; /// use rig::providers::openai::{Client, self}; /// - /// // Initialize the OpenAI client - /// let openai = Client::new("your-open-ai-api-key"); + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("your-api-key"); /// - /// let embeddings = openai.embeddings_with_ndims(openai::TEXT_EMBEDDING_3_LARGE, 3072) - /// .simple_document("doc0", "Hello, world!") - /// .simple_document("doc1", "Goodbye, world!") + /// let embeddings = client.embeddings_with_ndims(openai::TEXT_EMBEDDING_3_LARGE, 3072) + /// .documents(vec![ + /// "Hello, world!".to_string(), + /// "Goodbye, world!".to_string(), + /// ])? /// .build() - /// .await - /// .expect("Failed to embed documents"); + /// .await?; + /// # Ok(()) + /// # } /// ``` fn embeddings_with_ndims( &self, @@ -86,13 +205,28 @@ pub trait EmbeddingsClient: ProviderClient + Clone { } } +/// Dynamic dispatch version of [`EmbeddingsClient`]. +/// +/// This trait provides the same functionality as [`EmbeddingsClient`] but returns +/// trait objects instead of associated types, enabling runtime polymorphism. +/// It is automatically implemented for all types that implement [`EmbeddingsClient`]. +/// +/// # When to Use +/// +/// Use this trait when you need to work with embedding clients of different types +/// at runtime, such as in the [`DynClientBuilder`](crate::client::builder::DynClientBuilder). pub trait EmbeddingsClientDyn: ProviderClient { - /// Create an embedding model with the given name. - /// Note: default embedding dimension of 0 will be used if model is not known. - /// If this is the case, it's better to use function `embedding_model_with_ndims` + /// Creates a boxed embedding model with the specified model identifier. + /// + /// Note: A default embedding dimension of 0 is used if the model is not recognized. + /// For unknown models, prefer using [`embedding_model_with_ndims`](Self::embedding_model_with_ndims). + /// + /// Returns a trait object that can be used for dynamic dispatch. fn embedding_model<'a>(&self, model: &str) -> Box; - /// Create an embedding model with the given name and the number of dimensions in the embedding generated by the model. + /// Creates a boxed embedding model with explicit dimension specification. + /// + /// Returns a trait object configured with the specified dimension. fn embedding_model_with_ndims<'a>( &self, model: &str, diff --git a/rig-core/src/client/mod.rs b/rig-core/src/client/mod.rs index 3a5dc5707..d62f776bf 100644 --- a/rig-core/src/client/mod.rs +++ b/rig-core/src/client/mod.rs @@ -1,6 +1,329 @@ -//! This module provides traits for defining and creating provider clients. -//! Clients are used to create models for completion, embeddings, etc. -//! Dyn-compatible traits have been provided to allow for more provider-agnostic code. +//! Provider client traits and utilities for LLM integration. +//! +//! This module defines the core abstractions for creating and managing provider clients +//! that interface with different LLM services (OpenAI, Anthropic, Cohere, etc.). It provides +//! both static and dynamic dispatch mechanisms for working with multiple providers. +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────────────────────────────────────────────┐ +//! │ ProviderClient (Base Trait) │ +//! │ Conversion: AsCompletion, AsEmbeddings, etc. │ +//! └────────────┬────────────────────────────────────────┘ +//! │ +//! ├─> CompletionClient (text generation) +//! ├─> EmbeddingsClient (vector embeddings) +//! ├─> TranscriptionClient (audio to text) +//! ├─> ImageGenerationClient (text to image) +//! └─> AudioGenerationClient (text to speech) +//! ``` +//! +//! # Quick Start +//! +//! ## Using a Static Client +//! +//! ```no_run +//! use rig::prelude::*; +//! use rig::providers::openai::{Client, self}; +//! +//! # async fn example() -> Result<(), Box> { +//! // Create an OpenAI client +//! let client = Client::new("your-api-key"); +//! +//! // Create a completion model +//! let model = client.completion_model(openai::GPT_4O); +//! +//! // Generate a completion +//! let response = model +//! .completion_request("What is the capital of France?") +//! .send() +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Using a Dynamic Client Builder +//! +//! ```no_run +//! use rig::client::builder::DynClientBuilder; +//! +//! # async fn example() -> Result<(), Box> { +//! // Create a dynamic client builder +//! let builder = DynClientBuilder::new(); +//! +//! // Build agents for different providers +//! let openai_agent = builder.agent("openai", "gpt-4o")? +//! .preamble("You are a helpful assistant") +//! .build(); +//! +//! let anthropic_agent = builder.agent("anthropic", "claude-3-7-sonnet")? +//! .preamble("You are a helpful assistant") +//! .build(); +//! # Ok(()) +//! # } +//! ``` +//! +//! # Main Components +//! +//! ## Core Traits +//! +//! - [`ProviderClient`] - Base trait for all provider clients +//! - **Use when:** Implementing a new LLM provider integration +//! +//! ## Capability Traits +//! +//! - [`CompletionClient`] - Text generation capabilities +//! - **Use when:** You need to generate text completions or build agents +//! +//! - [`EmbeddingsClient`] - Vector embedding capabilities +//! - **Use when:** You need to convert text to vector embeddings for search or similarity +//! +//! - [`TranscriptionClient`] - Audio transcription capabilities +//! - **Use when:** You need to convert audio to text +//! +//! - [`ImageGenerationClient`] - Image generation capabilities (feature: `image`) +//! - **Use when:** You need to generate images from text prompts +//! +//! - [`AudioGenerationClient`] - Audio generation capabilities (feature: `audio`) +//! - **Use when:** You need to generate audio from text (text-to-speech) +//! +//! ## Dynamic Client Builder +//! +//! - [`builder::DynClientBuilder`] - Dynamic client factory for multiple providers +//! - **Use when:** You need to support multiple providers at runtime +//! +//! # Common Patterns +//! +//! ## Pattern 1: Single Provider with Static Dispatch +//! +//! ```no_run +//! use rig::prelude::*; +//! use rig::providers::openai::{Client, self}; +//! +//! # async fn example() -> Result<(), Box> { +//! let client = Client::new("api-key"); +//! let agent = client.agent(openai::GPT_4O) +//! .preamble("You are a helpful assistant") +//! .build(); +//! +//! let response = agent.prompt("Hello!").await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Pattern 2: Multiple Providers with Dynamic Dispatch +//! +//! ```no_run +//! use rig::client::builder::DynClientBuilder; +//! +//! # async fn example() -> Result<(), Box> { +//! let builder = DynClientBuilder::new(); +//! +//! // User selects provider at runtime +//! let provider = "openai"; +//! let model = "gpt-4o"; +//! +//! let agent = builder.agent(provider, model)? +//! .preamble("You are a helpful assistant") +//! .build(); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Pattern 3: Provider Capability Detection +//! +//! ```no_run +//! use rig::client::{ProviderClient, AsCompletion, AsEmbeddings}; +//! use rig::providers::openai::Client; +//! +//! # fn example() { +//! let client = Client::from_env(); +//! +//! // Check if provider supports completions +//! if let Some(completion_client) = client.as_completion() { +//! let model = completion_client.completion_model("gpt-4o"); +//! // Use model... +//! } +//! +//! // Check if provider supports embeddings +//! if let Some(embeddings_client) = client.as_embeddings() { +//! let model = embeddings_client.embedding_model("text-embedding-3-large"); +//! // Use model... +//! } +//! # } +//! ``` +//! +//! ## Pattern 4: Creating Provider from Environment +//! +//! ```no_run +//! use rig::client::ProviderClient; +//! use rig::providers::openai::Client; +//! +//! # fn example() { +//! // Reads API key from OPENAI_API_KEY environment variable +//! let client = Client::from_env(); +//! # } +//! ``` +//! +//! # Performance Characteristics +//! +//! - **Client creation**: Lightweight, typically O(1) memory allocation for configuration +//! - **Model cloning**: Inexpensive due to internal `Arc` usage (reference counting only) +//! - **Static vs Dynamic Dispatch**: +//! - Static dispatch (using concrete types): Zero runtime overhead +//! - Dynamic dispatch (trait objects): Small vtable lookup overhead +//! - Use static dispatch when working with a single provider +//! - Use dynamic dispatch when provider selection happens at runtime +//! +//! For most applications, the network latency of API calls (100-1000ms) far exceeds +//! any client-side overhead +//! +//! # Implementing a New Provider +//! +//! ```rust +//! use rig::client::{ProviderClient, ProviderValue, CompletionClient}; +//! use rig::completion::CompletionModel; +//! # use std::fmt::Debug; +//! +//! // Step 1: Define your client struct +//! #[derive(Clone, Debug)] +//! struct MyProvider { +//! api_key: String, +//! base_url: String, +//! } +//! +//! // Step 2: Implement ProviderClient +//! impl ProviderClient for MyProvider { +//! fn from_env() -> Self { +//! Self { +//! api_key: std::env::var("MY_API_KEY") +//! .expect("MY_API_KEY environment variable not set"), +//! base_url: std::env::var("MY_BASE_URL") +//! .unwrap_or_else(|_| "https://api.myprovider.com".to_string()), +//! } +//! } +//! +//! fn from_val(input: ProviderValue) -> Self { +//! match input { +//! ProviderValue::Simple(api_key) => Self { +//! api_key, +//! base_url: "https://api.myprovider.com".to_string(), +//! }, +//! ProviderValue::ApiKeyWithOptionalKey(api_key, Some(base_url)) => Self { +//! api_key, +//! base_url, +//! }, +//! _ => panic!("Invalid ProviderValue for MyProvider"), +//! } +//! } +//! } +//! +//! // Step 3: Implement capability traits (e.g., CompletionClient) +//! // impl CompletionClient for MyProvider { ... } +//! +//! // Step 4: Register the capabilities +//! // rig::impl_conversion_traits!(AsCompletion for MyProvider); +//! ``` +//! +//! # Troubleshooting +//! +//! ## Provider Not Found +//! +//! ```compile_fail +//! // ❌ BAD: Provider not registered +//! let builder = DynClientBuilder::empty(); +//! let agent = builder.agent("openai", "gpt-4o")?; // Error: UnknownProvider +//! ``` +//! +//! ```no_run +//! // ✅ GOOD: Use default registry or register manually +//! use rig::client::builder::DynClientBuilder; +//! # fn example() -> Result<(), Box> { +//! let builder = DynClientBuilder::new(); // Includes all providers +//! let agent = builder.agent("openai", "gpt-4o")?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Unsupported Feature for Provider +//! +//! Not all providers support all features. Check the provider capability table below. +//! +//! ### Provider Capabilities +//! +//! | Provider | Completions | Embeddings | Transcription | Image Gen | Audio Gen | +//! |------------|-------------|------------|---------------|-----------|-----------| +//! | OpenAI | ✅ | ✅ | ✅ | ✅ | ✅ | +//! | Anthropic | ✅ | ❌ | ❌ | ❌ | ❌ | +//! | Cohere | ✅ | ✅ | ❌ | ❌ | ❌ | +//! | Gemini | ✅ | ✅ | ✅ | ❌ | ❌ | +//! | Azure | ✅ | ✅ | ✅ | ✅ | ✅ | +//! | Together | ✅ | ✅ | ❌ | ❌ | ❌ | +//! +//! ### Handling Unsupported Features +//! +//! ```no_run +//! use rig::client::builder::{DynClientBuilder, ClientBuildError}; +//! +//! # fn example() -> Result<(), Box> { +//! let builder = DynClientBuilder::new(); +//! +//! // Attempt to use embeddings with a provider +//! match builder.embeddings("anthropic", "any-model") { +//! Ok(model) => { +//! // Use embeddings +//! }, +//! Err(ClientBuildError::UnsupportedFeature(provider, feature)) => { +//! eprintln!("{} doesn't support {}", provider, feature); +//! // Fallback to a provider that supports embeddings +//! let model = builder.embeddings("openai", "text-embedding-3-large")?; +//! }, +//! Err(e) => return Err(e.into()), +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! # Feature Flags +//! +//! The client module supports optional features for additional functionality: +//! +//! ## `image` - Image Generation Support +//! +//! Enables [`ImageGenerationClient`] and image generation capabilities: +//! +//! ```toml +//! [dependencies] +//! rig-core = { version = "0.x", features = ["image"] } +//! ``` +//! +//! ## `audio` - Audio Generation Support +//! +//! Enables [`AudioGenerationClient`] for text-to-speech: +//! +//! ```toml +//! [dependencies] +//! rig-core = { version = "0.x", features = ["audio"] } +//! ``` +//! +//! ## `derive` - Derive Macros +//! +//! Enables the `#[derive(ProviderClient)]` macro for automatic trait implementation: +//! +//! ```toml +//! [dependencies] +//! rig-core = { version = "0.x", features = ["derive"] } +//! ``` +//! +//! Without these features, the corresponding traits and functionality are not available. +//! +//! # See Also +//! +//! - [`crate::completion`] - Text completion functionality +//! - [`crate::embeddings`] - Vector embedding functionality +//! - [`crate::agent`] - High-level agent abstraction +//! - [`crate::providers`] - Available provider implementations pub mod audio_generation; pub mod builder; @@ -15,34 +338,121 @@ pub use rig_derive::ProviderClient; use std::fmt::Debug; use thiserror::Error; +/// Errors that can occur when building a client. +/// +/// This enum represents errors that may arise during client construction, +/// such as HTTP configuration errors or invalid property values. #[derive(Debug, Error)] #[non_exhaustive] pub enum ClientBuilderError { + /// An error occurred in the HTTP client (reqwest). + /// + /// This typically indicates issues with network configuration, + /// TLS setup, or invalid URLs. #[error("reqwest error: {0}")] HttpError( #[from] #[source] reqwest::Error, ), + + /// An invalid property value was provided during client construction. + /// + /// # Examples + /// + /// This error may be returned when: + /// - An API key format is invalid + /// - A required configuration field is missing + /// - A property value is outside acceptable bounds #[error("invalid property: {0}")] InvalidProperty(&'static str), } -/// The base ProviderClient trait, facilitates conversion between client types -/// and creating a client from the environment. +/// Base trait for all LLM provider clients. +/// +/// This trait defines the common interface that all provider clients must implement. +/// It includes methods for creating clients from environment variables or explicit values, +/// and provides automatic conversion to capability-specific clients through the +/// `As*` conversion traits. +/// +/// # Implementing ProviderClient +/// +/// When implementing a new provider, you must: +/// 1. Implement [`ProviderClient`] with `from_env()` and `from_val()` +/// 2. Implement capability traits as needed ([`CompletionClient`], [`EmbeddingsClient`], etc.) +/// 3. Use the [`impl_conversion_traits!`] macro to register capabilities +/// +/// # Examples +/// +/// ```rust +/// use rig::client::{ProviderClient, ProviderValue}; +/// # use std::fmt::Debug; +/// +/// #[derive(Clone, Debug)] +/// struct MyProvider { +/// api_key: String, +/// } +/// +/// impl ProviderClient for MyProvider { +/// fn from_env() -> Self { +/// Self { +/// api_key: std::env::var("MY_API_KEY") +/// .expect("MY_API_KEY environment variable not set"), +/// } +/// } +/// +/// fn from_val(input: ProviderValue) -> Self { +/// match input { +/// ProviderValue::Simple(api_key) => Self { api_key }, +/// _ => panic!("Expected simple API key"), +/// } +/// } +/// } +/// ``` +/// +/// # Panics /// -/// All conversion traits must be implemented, they are automatically -/// implemented if the respective client trait is implemented. +/// The `from_env()` and `from_env_boxed()` methods panic if the environment +/// is improperly configured (e.g., required environment variables are missing). pub trait ProviderClient: AsCompletion + AsTranscription + AsEmbeddings + AsImageGeneration + AsAudioGeneration + Debug { - /// Create a client from the process's environment. - /// Panics if an environment is improperly configured. + /// Creates a client from the process environment variables. + /// + /// This method reads configuration (typically API keys) from environment variables. + /// Each provider defines its own required environment variables. + /// + /// # Panics + /// + /// Panics if required environment variables are not set or have invalid values. + /// + /// # Examples + /// + /// ```no_run + /// use rig::client::ProviderClient; + /// use rig::providers::openai::Client; + /// + /// // Reads from OPENAI_API_KEY environment variable + /// let client = Client::from_env(); + /// ``` fn from_env() -> Self where Self: Sized; - /// A helper method to box the client. + /// Wraps this client in a `Box`. + /// + /// This is a convenience method for converting a concrete client type + /// into a trait object for dynamic dispatch. + /// + /// # Examples + /// + /// ```no_run + /// use rig::client::ProviderClient; + /// use rig::providers::openai::Client; + /// + /// let client = Client::from_env().boxed(); + /// // client is now Box + /// ``` fn boxed(self) -> Box where Self: Sized + 'static, @@ -50,8 +460,23 @@ pub trait ProviderClient: Box::new(self) } - /// Create a boxed client from the process's environment. - /// Panics if an environment is improperly configured. + /// Creates a boxed client from the process environment variables. + /// + /// This is equivalent to `Self::from_env().boxed()` but more convenient. + /// + /// # Panics + /// + /// Panics if required environment variables are not set or have invalid values. + /// + /// # Examples + /// + /// ```no_run + /// use rig::client::ProviderClient; + /// use rig::providers::openai::Client; + /// + /// let client = Client::from_env_boxed(); + /// // client is Box + /// ``` fn from_env_boxed<'a>() -> Box where Self: Sized, @@ -60,12 +485,36 @@ pub trait ProviderClient: Box::new(Self::from_env()) } + /// Creates a client from a provider-specific value. + /// + /// This method allows creating clients with explicit configuration values + /// rather than reading from environment variables. + /// + /// # Examples + /// + /// ```no_run + /// use rig::client::{ProviderClient, ProviderValue}; + /// use rig::providers::openai::Client; + /// + /// let client = Client::from_val(ProviderValue::Simple("api-key".to_string())); + /// ``` fn from_val(input: ProviderValue) -> Self where Self: Sized; - /// Create a boxed client from the process's environment. - /// Panics if an environment is improperly configured. + /// Creates a boxed client from a provider-specific value. + /// + /// This is equivalent to `Self::from_val(input).boxed()` but more convenient. + /// + /// # Examples + /// + /// ```no_run + /// use rig::client::{ProviderClient, ProviderValue}; + /// use rig::providers::openai::Client; + /// + /// let client = Client::from_val_boxed(ProviderValue::Simple("api-key".to_string())); + /// // client is Box + /// ``` fn from_val_boxed<'a>(input: ProviderValue) -> Box where Self: Sized, @@ -75,10 +524,56 @@ pub trait ProviderClient: } } +/// Configuration values for creating provider clients. +/// +/// This enum supports different provider authentication schemes, +/// allowing flexibility in how clients are configured. +/// +/// # Examples +/// +/// ```rust +/// use rig::client::ProviderValue; +/// +/// // Simple API key +/// let simple = ProviderValue::Simple("sk-abc123".to_string()); +/// +/// // API key with optional organization ID (e.g., OpenAI) +/// let with_org = ProviderValue::ApiKeyWithOptionalKey( +/// "sk-abc123".to_string(), +/// Some("org-xyz".to_string()) +/// ); +/// +/// // API key with version and header (e.g., Azure) +/// let azure = ProviderValue::ApiKeyWithVersionAndHeader( +/// "api-key".to_string(), +/// "2024-01-01".to_string(), +/// "api-key".to_string() +/// ); +/// ``` #[derive(Clone)] pub enum ProviderValue { + /// Simple API key authentication. + /// + /// This is the most common authentication method, used by providers + /// that only require a single API key. Simple(String), + + /// API key with an optional secondary key. + /// + /// Used by providers that support optional organization IDs or + /// project keys (e.g., OpenAI with organization ID). + /// + /// # Format + /// `(api_key, optional_key)` ApiKeyWithOptionalKey(String, Option), + + /// API key with version and header name. + /// + /// Used by providers that require API versioning and custom + /// header names (e.g., Azure OpenAI). + /// + /// # Format + /// `(api_key, version, header_name)` ApiKeyWithVersionAndHeader(String, String, String), } @@ -119,45 +614,182 @@ where } } -/// Attempt to convert a ProviderClient to a CompletionClient +/// Trait for converting a [`ProviderClient`] to a [`CompletionClient`]. +/// +/// This trait enables capability detection and conversion for text completion features. +/// It is automatically implemented for types that implement [`CompletionClientDyn`]. +/// +/// # Examples +/// +/// ```no_run +/// use rig::client::{ProviderClient, AsCompletion}; +/// use rig::providers::openai::Client; +/// +/// let client = Client::from_env(); +/// +/// if let Some(completion_client) = client.as_completion() { +/// let model = completion_client.completion_model("gpt-4o"); +/// // Use the completion model... +/// } +/// ``` pub trait AsCompletion { + /// Attempts to convert this client to a completion client. + /// + /// Returns `Some` if the provider supports completion features, + /// `None` otherwise. fn as_completion(&self) -> Option> { None } } -/// Attempt to convert a ProviderClient to a TranscriptionClient +/// Trait for converting a [`ProviderClient`] to a [`TranscriptionClient`]. +/// +/// This trait enables capability detection and conversion for audio transcription features. +/// It is automatically implemented for types that implement [`TranscriptionClientDyn`]. +/// +/// # Examples +/// +/// ```no_run +/// use rig::client::{ProviderClient, AsTranscription}; +/// use rig::providers::openai::Client; +/// +/// let client = Client::from_env(); +/// +/// if let Some(transcription_client) = client.as_transcription() { +/// let model = transcription_client.transcription_model("whisper-1"); +/// // Use the transcription model... +/// } +/// ``` pub trait AsTranscription { + /// Attempts to convert this client to a transcription client. + /// + /// Returns `Some` if the provider supports transcription features, + /// `None` otherwise. fn as_transcription(&self) -> Option> { None } } -/// Attempt to convert a ProviderClient to a EmbeddingsClient +/// Trait for converting a [`ProviderClient`] to an [`EmbeddingsClient`]. +/// +/// This trait enables capability detection and conversion for vector embedding features. +/// It is automatically implemented for types that implement [`EmbeddingsClientDyn`]. +/// +/// # Examples +/// +/// ```no_run +/// use rig::client::{ProviderClient, AsEmbeddings}; +/// use rig::providers::openai::Client; +/// +/// let client = Client::from_env(); +/// +/// if let Some(embeddings_client) = client.as_embeddings() { +/// let model = embeddings_client.embedding_model("text-embedding-3-large"); +/// // Use the embedding model... +/// } +/// ``` pub trait AsEmbeddings { + /// Attempts to convert this client to an embeddings client. + /// + /// Returns `Some` if the provider supports embedding features, + /// `None` otherwise. fn as_embeddings(&self) -> Option> { None } } -/// Attempt to convert a ProviderClient to a AudioGenerationClient +/// Trait for converting a [`ProviderClient`] to an [`AudioGenerationClient`]. +/// +/// This trait enables capability detection and conversion for audio generation (TTS) features. +/// It is automatically implemented for types that implement [`AudioGenerationClientDyn`]. +/// Only available with the `audio` feature enabled. +/// +/// # Examples +/// +/// ```no_run +/// # #[cfg(feature = "audio")] +/// # { +/// use rig::client::{ProviderClient, AsAudioGeneration}; +/// use rig::providers::openai::Client; +/// +/// let client = Client::from_env(); +/// +/// if let Some(audio_client) = client.as_audio_generation() { +/// let model = audio_client.audio_generation_model("tts-1"); +/// // Use the audio generation model... +/// } +/// # } +/// ``` pub trait AsAudioGeneration { + /// Attempts to convert this client to an audio generation client. + /// + /// Returns `Some` if the provider supports audio generation features, + /// `None` otherwise. Only available with the `audio` feature enabled. #[cfg(feature = "audio")] fn as_audio_generation(&self) -> Option> { None } } -/// Attempt to convert a ProviderClient to a ImageGenerationClient +/// Trait for converting a [`ProviderClient`] to an [`ImageGenerationClient`]. +/// +/// This trait enables capability detection and conversion for image generation features. +/// It is automatically implemented for types that implement [`ImageGenerationClientDyn`]. +/// Only available with the `image` feature enabled. +/// +/// # Examples +/// +/// ```no_run +/// # #[cfg(feature = "image")] +/// # { +/// use rig::client::{ProviderClient, AsImageGeneration}; +/// use rig::providers::openai::Client; +/// +/// let client = Client::from_env(); +/// +/// if let Some(image_client) = client.as_image_generation() { +/// let model = image_client.image_generation_model("dall-e-3"); +/// // Use the image generation model... +/// } +/// # } +/// ``` pub trait AsImageGeneration { + /// Attempts to convert this client to an image generation client. + /// + /// Returns `Some` if the provider supports image generation features, + /// `None` otherwise. Only available with the `image` feature enabled. #[cfg(feature = "image")] fn as_image_generation(&self) -> Option> { None } } -/// Attempt to convert a ProviderClient to a VerifyClient +/// Trait for converting a [`ProviderClient`] to a [`VerifyClient`]. +/// +/// This trait enables capability detection and conversion for client verification features. +/// It is automatically implemented for types that implement [`VerifyClientDyn`]. +/// +/// # Examples +/// +/// ```no_run +/// use rig::client::{ProviderClient, AsVerify}; +/// use rig::providers::openai::Client; +/// +/// # async fn example() -> Result<(), Box> { +/// let client = Client::from_env(); +/// +/// if let Some(verify_client) = client.as_verify() { +/// verify_client.verify().await?; +/// println!("Client configuration is valid"); +/// } +/// # Ok(()) +/// # } +/// ``` pub trait AsVerify { + /// Attempts to convert this client to a verify client. + /// + /// Returns `Some` if the provider supports verification features, + /// `None` otherwise. fn as_verify(&self) -> Option> { None } @@ -169,14 +801,116 @@ impl AsAudioGeneration for T {} #[cfg(not(feature = "image"))] impl AsImageGeneration for T {} -/// Implements the conversion traits for a given struct +/// Registers capability traits for a provider client. +/// +/// This macro automatically implements the conversion traits ([`AsCompletion`], +/// [`AsEmbeddings`], etc.) for your provider client type, enabling capability +/// detection and dynamic dispatch through the [`ProviderClient`] trait. +/// +/// # When to Use +/// +/// Use this macro after implementing [`ProviderClient`] and the specific +/// capability traits (like [`CompletionClient`], [`EmbeddingsClient`]) for +/// your provider. This macro wires up the automatic conversions that allow +/// your client to work with the dynamic client builder and capability detection. +/// +/// # What This Does +/// +/// For each trait listed, this macro implements the empty conversion trait, +/// which signals to the Rig system that your client supports that capability. +/// The actual conversion logic is provided by blanket implementations. +/// +/// # Syntax +/// +/// ```text +/// impl_conversion_traits!(Trait1, Trait2, ... for YourType); +/// ``` +/// +/// # Examples +/// +/// ## Complete Provider Implementation +/// /// ```rust -/// pub struct Client; -/// impl ProviderClient for Client { -/// ... +/// use rig::client::{ProviderClient, ProviderValue, CompletionClient}; +/// use rig::completion::CompletionModel; +/// # use std::fmt::Debug; +/// +/// // 1. Define your client +/// #[derive(Clone, Debug)] +/// pub struct MyClient { +/// api_key: String, +/// } +/// +/// // 2. Implement ProviderClient +/// impl ProviderClient for MyClient { +/// fn from_env() -> Self { +/// Self { +/// api_key: std::env::var("MY_API_KEY") +/// .expect("MY_API_KEY not set"), +/// } +/// } +/// +/// fn from_val(input: ProviderValue) -> Self { +/// match input { +/// ProviderValue::Simple(key) => Self { api_key: key }, +/// _ => panic!("Invalid value"), +/// } +/// } /// } -/// impl_conversion_traits!(AsCompletion, AsEmbeddings for Client); +/// +/// // 3. Implement capability traits +/// // impl CompletionClient for MyClient { +/// // type CompletionModel = MyCompletionModel; +/// // fn completion_model(&self, model: &str) -> Self::CompletionModel { +/// // MyCompletionModel { /* ... */ } +/// // } +/// // } +/// +/// // 4. Register the capabilities with this macro +/// rig::impl_conversion_traits!(AsCompletion for MyClient); +/// ``` +/// +/// ## Multiple Capabilities +/// +/// ```rust +/// # use rig::client::{ProviderClient, ProviderValue}; +/// # use std::fmt::Debug; +/// # #[derive(Clone, Debug)] +/// # pub struct MultiClient; +/// # impl ProviderClient for MultiClient { +/// # fn from_env() -> Self { MultiClient } +/// # fn from_val(_: ProviderValue) -> Self { MultiClient } +/// # } +/// // Register multiple capabilities at once +/// rig::impl_conversion_traits!(AsCompletion, AsEmbeddings, AsTranscription for MultiClient); /// ``` +/// +/// ## With Feature Gates +/// +/// ```rust +/// # use rig::client::{ProviderClient, ProviderValue}; +/// # use std::fmt::Debug; +/// # #[derive(Clone, Debug)] +/// # pub struct MyClient; +/// # impl ProviderClient for MyClient { +/// # fn from_env() -> Self { MyClient } +/// # fn from_val(_: ProviderValue) -> Self { MyClient } +/// # } +/// // The macro automatically handles feature gates for image/audio +/// rig::impl_conversion_traits!( +/// AsCompletion, +/// AsEmbeddings, +/// AsImageGeneration, // Only available with "image" feature +/// AsAudioGeneration // Only available with "audio" feature +/// for MyClient +/// ); +/// ``` +/// +/// # See Also +/// +/// - [`ProviderClient`] - The base trait to implement first +/// - [`CompletionClient`], [`EmbeddingsClient`] - Capability traits to implement +/// - [`AsCompletion`], [`AsEmbeddings`] - Conversion traits this macro implements #[macro_export] macro_rules! impl_conversion_traits { ($( $trait_:ident ),* for $struct_:ident ) => { diff --git a/rig-core/src/client/transcription.rs b/rig-core/src/client/transcription.rs index e1a821b01..211f7869f 100644 --- a/rig-core/src/client/transcription.rs +++ b/rig-core/src/client/transcription.rs @@ -5,29 +5,116 @@ use crate::transcription::{ }; use std::sync::Arc; -/// A provider client with transcription capabilities. -/// Clone is required for conversions between client types. +/// A provider client with audio transcription capabilities. +/// +/// This trait extends [`ProviderClient`] to provide audio-to-text transcription functionality. +/// Providers that implement this trait can create transcription models for converting +/// audio files to text. +/// +/// # When to Implement +/// +/// Implement this trait for provider clients that support: +/// - Audio to text transcription +/// - Speech recognition +/// - Multiple audio format support +/// - Language detection and translation +/// +/// # Examples +/// +/// ```no_run +/// use rig::prelude::*; +/// use rig::providers::openai::{Client, self}; +/// use rig::transcription::TranscriptionRequest; +/// use std::fs; +/// +/// # async fn example() -> Result<(), Box> { +/// let client = Client::new("api-key"); +/// +/// // Create a transcription model +/// let model = client.transcription_model(openai::WHISPER_1); +/// +/// // Read audio file +/// let audio_data = fs::read("audio.mp3")?; +/// +/// // Transcribe audio +/// let response = model.transcription(TranscriptionRequest { +/// data: audio_data, +/// filename: "audio.mp3".to_string(), +/// language: "en".to_string(), +/// prompt: None, +/// temperature: None, +/// additional_params: None, +/// }).await?; +/// +/// println!("Transcription: {}", response.text); +/// # Ok(()) +/// # } +/// ``` +/// +/// # See Also +/// +/// - [`crate::transcription::TranscriptionModel`] - The model trait for transcription operations +/// - [`crate::transcription::TranscriptionRequest`] - Request structure for transcriptions +/// - [`TranscriptionClientDyn`] - Dynamic dispatch version for runtime polymorphism pub trait TranscriptionClient: ProviderClient + Clone { /// The type of TranscriptionModel used by the Client type TranscriptionModel: TranscriptionModel; - /// Create a transcription model with the given name. + /// Creates a transcription model with the specified model identifier. /// - /// # Example with OpenAI - /// ``` + /// This method constructs a transcription model that can convert audio to text. + /// + /// # Arguments + /// + /// * `model` - The model identifier (e.g., "whisper-1", "whisper-large-v3") + /// + /// # Returns + /// + /// A transcription model that can process audio files. + /// + /// # Examples + /// + /// ```no_run /// use rig::prelude::*; /// use rig::providers::openai::{Client, self}; + /// use rig::transcription::{TranscriptionModel, TranscriptionRequest}; + /// use std::fs; /// - /// // Initialize the OpenAI client - /// let openai = Client::new("your-open-ai-api-key"); + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("your-api-key"); + /// let model = client.transcription_model(openai::WHISPER_1); /// - /// let whisper = openai.transcription_model(openai::WHISPER_1); + /// let audio_data = fs::read("audio.mp3")?; + /// let response = model.transcription(TranscriptionRequest { + /// data: audio_data, + /// filename: "audio.mp3".to_string(), + /// language: "en".to_string(), + /// prompt: None, + /// temperature: None, + /// additional_params: None, + /// }).await?; + /// + /// println!("Transcription: {}", response.text); + /// # Ok(()) + /// # } /// ``` fn transcription_model(&self, model: &str) -> Self::TranscriptionModel; } +/// Dynamic dispatch version of [`TranscriptionClient`]. +/// +/// This trait provides the same functionality as [`TranscriptionClient`] but returns +/// trait objects instead of associated types, enabling runtime polymorphism. +/// It is automatically implemented for all types that implement [`TranscriptionClient`]. +/// +/// # When to Use +/// +/// Use this trait when you need to work with transcription clients of different types +/// at runtime, such as in the [`DynClientBuilder`](crate::client::builder::DynClientBuilder). pub trait TranscriptionClientDyn: ProviderClient { - /// Create a transcription model with the given name. + /// Creates a boxed transcription model with the specified model identifier. + /// + /// Returns a trait object that can be used for dynamic dispatch. fn transcription_model<'a>(&self, model: &str) -> Box; } @@ -50,9 +137,16 @@ where } } -/// Wraps a TranscriptionModel in a dyn-compatible way for TranscriptionRequestBuilder. +/// A dynamic handle for transcription models enabling trait object usage. +/// +/// This struct wraps a [`TranscriptionModel`] in a way that allows it to be used +/// as a trait object in generic contexts. It uses `Arc` internally for efficient cloning. +/// +/// This type is primarily used internally by the dynamic client builder for +/// runtime polymorphism over different transcription model implementations. #[derive(Clone)] pub struct TranscriptionModelHandle<'a> { + /// The inner dynamic transcription model. pub inner: Arc, } diff --git a/rig-core/src/client/verify.rs b/rig-core/src/client/verify.rs index 0867d5e95..0685fa44b 100644 --- a/rig-core/src/client/verify.rs +++ b/rig-core/src/client/verify.rs @@ -2,12 +2,29 @@ use crate::client::{AsVerify, ProviderClient}; use futures::future::BoxFuture; use thiserror::Error; +/// Errors that can occur during client verification. +/// +/// This enum represents the possible failure modes when verifying +/// a provider client's configuration and authentication. #[derive(Debug, Error)] pub enum VerifyError { + /// Authentication credentials are invalid or have been revoked. + /// + /// This typically indicates that the API key or other authentication + /// method is incorrect or no longer valid. #[error("invalid authentication")] InvalidAuthentication, + + /// The provider returned an error during verification. + /// + /// This wraps provider-specific error messages that occur during + /// the verification process. #[error("provider error: {0}")] ProviderError(String), + + /// An HTTP error occurred while communicating with the provider. + /// + /// This typically indicates network issues, timeouts, or server errors. #[error("http error: {0}")] HttpError( #[from] @@ -16,15 +33,85 @@ pub enum VerifyError { ), } -/// A provider client that can verify the configuration. -/// Clone is required for conversions between client types. +/// A provider client that can verify its configuration and authentication. +/// +/// This trait extends [`ProviderClient`] to provide configuration verification functionality. +/// Providers that implement this trait can validate that their API keys and settings +/// are correct before making actual API calls. +/// +/// # When to Implement +/// +/// Implement this trait for provider clients that support: +/// - API key validation +/// - Configuration testing +/// - Authentication verification +/// - Connectivity checks +/// +/// # Examples +/// +/// ```no_run +/// use rig::client::{ProviderClient, VerifyClient}; +/// use rig::providers::openai::Client; +/// +/// # async fn example() -> Result<(), Box> { +/// let client = Client::new("api-key"); +/// +/// // Verify the client configuration +/// match client.verify().await { +/// Ok(()) => println!("Client configuration is valid"), +/// Err(e) => eprintln!("Verification failed: {}", e), +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// # See Also +/// +/// - [`VerifyError`] - Errors that can occur during verification +/// - [`VerifyClientDyn`] - Dynamic dispatch version for runtime polymorphism pub trait VerifyClient: ProviderClient + Clone { - /// Verify the configuration. + /// Verifies the client configuration and authentication. + /// + /// This method tests whether the client is properly configured and can + /// successfully authenticate with the provider. It typically makes a + /// minimal API call to validate credentials. + /// + /// # Errors + /// + /// Returns [`VerifyError::InvalidAuthentication`] if the credentials are invalid. + /// Returns [`VerifyError::HttpError`] if there are network connectivity issues. + /// Returns [`VerifyError::ProviderError`] for provider-specific errors. + /// + /// # Examples + /// + /// ```no_run + /// use rig::client::VerifyClient; + /// use rig::providers::openai::Client; + /// + /// # async fn example() -> Result<(), Box> { + /// let client = Client::new("api-key"); + /// client.verify().await?; + /// println!("Configuration verified successfully"); + /// # Ok(()) + /// # } + /// ``` fn verify(&self) -> impl Future> + Send; } +/// Dynamic dispatch version of [`VerifyClient`]. +/// +/// This trait provides the same functionality as [`VerifyClient`] but uses +/// boxed futures for trait object compatibility, enabling runtime polymorphism. +/// It is automatically implemented for all types that implement [`VerifyClient`]. +/// +/// # When to Use +/// +/// Use this trait when you need to work with verify clients of different types +/// at runtime, such as in the [`DynClientBuilder`](crate::client::builder::DynClientBuilder). pub trait VerifyClientDyn: ProviderClient { - /// Verify the configuration. + /// Verifies the client configuration and authentication. + /// + /// Returns a boxed future for trait object compatibility. fn verify(&self) -> BoxFuture<'_, Result<(), VerifyError>>; } diff --git a/rig-core/src/completion/message.rs b/rig-core/src/completion/message.rs index a0fe667e1..f00a5c33e 100644 --- a/rig-core/src/completion/message.rs +++ b/rig-core/src/completion/message.rs @@ -1,3 +1,206 @@ +//! Message types for LLM conversations. +//! +//! This module provides the core message structures used in conversations with +//! Large Language Models. Messages are the fundamental building blocks of +//! interactions, representing both user inputs and assistant responses. +//! +//! # Main Components +//! +//! - [`Message`]: The primary message type with user and assistant variants +//! - [`UserContent`]: Content types that can appear in user messages (text, images, audio, etc.) +//! - [`AssistantContent`]: Content types in assistant responses (text, tool calls, reasoning) +//! - [`ConvertMessage`]: Trait for converting messages to custom formats +//! +//! # Quick Start +//! +//! ``` +//! use rig::completion::Message; +//! +//! // Create a simple user message +//! let user_msg = Message::user("What is the capital of France?"); +//! +//! // Create an assistant response +//! let assistant_msg = Message::assistant("The capital of France is Paris."); +//! ``` +//! +//! # Multimodal Messages +//! +//! Messages can contain multiple types of content: +//! +//! ```ignore +//! use rig::completion::message::{Message, UserContent, ImageMediaType, ImageDetail}; +//! use rig::OneOrMany; +//! +//! let msg = Message::User { +//! content: OneOrMany::many(vec![ +//! UserContent::text("What's in this image?"), +//! UserContent::image_url( +//! "https://example.com/image.png", +//! Some(ImageMediaType::PNG), +//! Some(ImageDetail::High) +//! ), +//! ]) +//! }; +//! ``` +//! +//! # Provider Compatibility +//! +//! Different LLM providers support different content types and features. +//! Rig handles conversion between its generic message format and provider-specific +//! formats automatically, with some potential loss of information for unsupported features. +//! +//! # Common Patterns +//! +//! ## Building Conversation History +//! +//! ``` +//! use rig::completion::Message; +//! +//! let conversation = vec![ +//! Message::user("What's the capital of France?"), +//! Message::assistant("The capital of France is Paris."), +//! Message::user("What's its population?"), +//! Message::assistant("Paris has approximately 2.2 million inhabitants."), +//! ]; +//! ``` +//! +//! ## Multimodal Messages +//! +//! Combining text with images for vision-capable models: +//! +//! ```ignore +//! use rig::completion::message::{Message, UserContent, ImageMediaType, ImageDetail}; +//! use rig::OneOrMany; +//! +//! let msg = Message::User { +//! content: OneOrMany::many(vec![ +//! UserContent::text("Analyze this architecture diagram and explain the data flow:"), +//! UserContent::image_url( +//! "https://example.com/architecture.png", +//! Some(ImageMediaType::PNG), +//! Some(ImageDetail::High) +//! ), +//! ]) +//! }; +//! ``` +//! +//! ## Working with Tool Results +//! +//! ``` +//! use rig::completion::message::{Message, UserContent, ToolResult, ToolResultContent, Text}; +//! use rig::OneOrMany; +//! +//! // After the model requests a tool call, provide the result +//! let tool_result = ToolResult { +//! id: "call_123".to_string(), +//! call_id: Some("msg_456".to_string()), +//! content: OneOrMany::one(ToolResultContent::Text(Text { +//! text: "The current temperature is 72°F".to_string(), +//! })), +//! }; +//! +//! let msg = Message::User { +//! content: OneOrMany::one(UserContent::ToolResult(tool_result)), +//! }; +//! ``` +//! +//! # Troubleshooting +//! +//! ## Common Issues +//! +//! ### "Media type required" Error +//! +//! When creating images from base64 data, you must specify the media type: +//! +//! ``` +//! # use rig::completion::message::UserContent; +//! // ❌ This may fail during provider conversion +//! let img = UserContent::image_base64("iVBORw0KGgo...", None, None); +//! ``` +//! +//! ``` +//! # use rig::completion::message::{UserContent, ImageMediaType}; +//! // ✅ Correct: always specify media type for base64 +//! let img = UserContent::image_base64( +//! "iVBORw0KGgo...", +//! Some(ImageMediaType::PNG), +//! None +//! ); +//! ``` +//! +//! ### Provider Doesn't Support Content Type +//! +//! Not all providers support all content types: +//! +//! | Content Type | Supported By | +//! |--------------|--------------| +//! | Text | All providers | +//! | Images | GPT-4V, GPT-4o, Claude 3+, Gemini Pro Vision | +//! | Audio | OpenAI Whisper, specific models | +//! | Video | Gemini 1.5+, very limited support | +//! | Tool calls | GPT-4+, Claude 3+, most modern models | +//! +//! **Solution**: Check your provider's documentation before using multimedia content. +//! +//! ### Large Base64 Images Failing +//! +//! Base64-encoded images count heavily toward token limits: +//! +//! ``` +//! # use rig::completion::message::{UserContent, ImageMediaType, ImageDetail}; +//! // ❌ Large base64 image (may exceed limits) +//! // let huge_image = UserContent::image_base64(very_large_base64, ...); +//! +//! // ✅ Better: use URL for large images +//! let img = UserContent::image_url( +//! "https://example.com/large-image.png", +//! Some(ImageMediaType::PNG), +//! Some(ImageDetail::High) +//! ); +//! ``` +//! +//! **Tips**: +//! - Resize images before encoding (768x768 is often sufficient) +//! - Use URLs for images >1MB +//! - Use `ImageDetail::Low` for thumbnails or simple images +//! +//! ### Builder Pattern Not Chaining +//! +//! Make sure to capture the return value from builder methods: +//! +//! ``` +//! # use rig::completion::message::Reasoning; +//! // ❌ This doesn't work (discards the returned value) +//! let mut reasoning = Reasoning::new("step 1"); +//! reasoning.with_id("id-123".to_string()); // Returns new value, not stored! +//! // reasoning.id is still None +//! ``` +//! +//! ``` +//! # use rig::completion::message::Reasoning; +//! // ✅ Correct: chain the calls or reassign +//! let reasoning = Reasoning::new("step 1") +//! .with_id("id-123".to_string()); // Proper chaining +//! assert_eq!(reasoning.id, Some("id-123".to_string())); +//! ``` +//! +//! # Performance Tips +//! +//! ## Message Size +//! - Keep conversation history manageable (typically last 10-20 messages) +//! - Summarize old context rather than sending full history +//! - Images use 85-765 tokens each depending on size +//! +//! ## Content Type Selection +//! - Prefer URLs over base64 for multimedia (faster, fewer tokens) +//! - Use `ImageDetail::Low` when high detail isn't needed (saves tokens) +//! - Remove tool results from history after they've been used +//! +//! # See also +//! +//! - [`crate::completion::request`] for sending messages to models +//! - [`crate::providers`] for provider-specific implementations + use std::{convert::Infallible, str::FromStr}; use crate::OneOrMany; @@ -10,72 +213,448 @@ use super::CompletionError; // Message models // ================================================================ -/// A useful trait to help convert `rig::completion::Message` to your own message type. +/// A trait for converting [`Message`] to custom message types. +/// +/// This trait provides a clean way to convert Rig's generic message format +/// into your own custom message types without running into Rust's orphan rule. +/// Since `Vec` is a foreign type (owned by stdlib), implementing `TryFrom` +/// for `Vec` would violate the orphan rule. This trait solves that problem. +/// +/// # When to implement +/// +/// Implement this trait when: +/// - You need to integrate Rig with existing message-based systems +/// - You want to convert between Rig's format and your own message types +/// - You need custom conversion logic beyond simple type mapping +/// +/// # Examples +/// +/// ``` +/// use rig::completion::message::{ConvertMessage, Message, UserContent, AssistantContent}; +/// use rig::OneOrMany; +/// +/// #[derive(Debug)] +/// struct MyMessage { +/// role: String, +/// content: String, +/// } +/// +/// #[derive(Debug)] +/// struct ConversionError(String); +/// +/// impl std::fmt::Display for ConversionError { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// write!(f, "Conversion error: {}", self.0) +/// } +/// } +/// +/// impl std::error::Error for ConversionError {} +/// +/// impl ConvertMessage for MyMessage { +/// type Error = ConversionError; +/// +/// fn convert_from_message(message: Message) -> Result, Self::Error> { +/// match message { +/// Message::User { content } => { +/// // Extract text from all text content items +/// let mut messages = Vec::new(); /// -/// Particularly useful if you don't want to create a free-standing function as -/// when trying to use `TryFrom`, you would normally run into the orphan rule as Vec is -/// technically considered a foreign type (it's owned by stdlib). +/// for item in content.iter() { +/// if let UserContent::Text(text) = item { +/// messages.push(MyMessage { +/// role: "user".to_string(), +/// content: text.text.clone(), +/// }); +/// } +/// } +/// +/// if messages.is_empty() { +/// return Err(ConversionError("No text content found".to_string())); +/// } +/// +/// Ok(messages) +/// } +/// Message::Assistant { content, .. } => { +/// // Extract text from assistant content +/// let mut messages = Vec::new(); +/// +/// for item in content.iter() { +/// if let AssistantContent::Text(text) = item { +/// messages.push(MyMessage { +/// role: "assistant".to_string(), +/// content: text.text.clone(), +/// }); +/// } +/// } +/// +/// if messages.is_empty() { +/// return Err(ConversionError("No text content found".to_string())); +/// } +/// +/// Ok(messages) +/// } +/// } +/// } +/// } +/// +/// // Usage +/// let msg = Message::user("Hello, world!"); +/// let converted = MyMessage::convert_from_message(msg).unwrap(); +/// assert_eq!(converted[0].role, "user"); +/// assert_eq!(converted[0].content, "Hello, world!"); +/// ``` +/// +/// # See also +/// +/// - [`Message`] for the source message type +/// - [`From`] and [`TryFrom`] for simpler conversions pub trait ConvertMessage: Sized + Send + Sync { + /// The error type returned when conversion fails. type Error: std::error::Error + Send; + /// Converts a Rig message into a vector of custom message types. + /// + /// Returns a vector because a single Rig message may map to multiple + /// messages in your format (e.g., separating user content and tool results). + /// + /// # Errors + /// + /// Returns an error if the conversion cannot be performed, such as when + /// the message contains content types unsupported by your format. fn convert_from_message(message: Message) -> Result, Self::Error>; } -/// A message represents a run of input (user) and output (assistant). -/// Each message type (based on it's `role`) can contain a atleast one bit of content such as text, -/// images, audio, documents, or tool related information. While each message type can contain -/// multiple content, most often, you'll only see one content type per message -/// (an image w/ a description, etc). +/// A message in a conversation between a user and an AI assistant. +/// +/// Messages form the core communication structure in Rig. Each message has a role +/// (user or assistant) and can contain various types of content such as text, images, +/// audio, documents, or tool-related information. +/// +/// While messages can contain multiple content items, most commonly you'll see one +/// content type per message (e.g., an image with a text description, or just text). +/// +/// # Provider Compatibility +/// +/// Each LLM provider converts these generic messages to their provider-specific format +/// using [`From`] or [`TryFrom`] traits. Since not all providers support all features, +/// conversion may be lossy (e.g., images might be discarded for non-vision models). /// -/// Each provider is responsible with converting the generic message into it's provider specific -/// type using `From` or `TryFrom` traits. Since not every provider supports every feature, the -/// conversion can be lossy (providing an image might be discarded for a non-image supporting -/// provider) though the message being converted back and forth should always be the same. +/// # Conversions +/// +/// This type implements several convenient conversions: +/// +/// ``` +/// use rig::completion::Message; +/// +/// // From string types +/// let msg: Message = "Hello".into(); +/// let msg: Message = String::from("Hello").into(); +/// ``` +/// +/// # Examples +/// +/// Creating a simple text message: +/// +/// ``` +/// use rig::completion::Message; +/// +/// let msg = Message::user("Hello, world!"); +/// ``` +/// +/// Creating a message with an assistant response: +/// +/// ``` +/// use rig::completion::Message; +/// +/// let response = Message::assistant("I'm doing well, thank you!"); +/// ``` +/// +/// # See also +/// +/// - [`UserContent`] for user message content types +/// - [`AssistantContent`] for assistant response content types #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(tag = "role", rename_all = "lowercase")] pub enum Message { - /// User message containing one or more content types defined by `UserContent`. + /// User message containing one or more content types. + /// + /// User messages typically contain prompts, questions, or follow-up responses. + /// They can include text, images, audio, documents, and tool results. + /// + /// See [`UserContent`] for all supported content types. User { content: OneOrMany }, - /// Assistant message containing one or more content types defined by `AssistantContent`. + /// Assistant message containing one or more content types. + /// + /// Assistant messages contain the AI's responses, which can be text, + /// tool calls, or reasoning steps. + /// + /// The optional `id` field identifies specific response turns in multi-turn + /// conversations. + /// + /// See [`AssistantContent`] for all supported content types. Assistant { id: Option, content: OneOrMany, }, } -/// Describes the content of a message, which can be text, a tool result, an image, audio, or -/// a document. Dependent on provider supporting the content type. Multimedia content is generally -/// base64 (defined by it's format) encoded but additionally supports urls (for some providers). +/// Content types that can be included in user messages. +/// +/// User messages can contain various types of content including text, multimedia +/// (images, audio, video), documents, and tool execution results. Provider support +/// for each content type varies. +/// +/// # Content Type Support +/// +/// - **Text**: Universally supported by all providers +/// - **Images**: Supported by vision-capable models (GPT-4V, Claude 3, Gemini Pro Vision, etc.) +/// - **Audio**: Supported by audio-capable models (Whisper, etc.) +/// - **Video**: Supported by select multimodal models +/// - **Documents**: Supported by document-aware models +/// - **Tool Results**: Supported by function-calling capable models +/// +/// # Multimedia Encoding +/// +/// Multimedia content (images, audio, video) can be provided in two formats: +/// - **Base64-encoded**: Data embedded directly in the message +/// - **URL**: Reference to externally hosted content (provider support varies) +/// +/// # Choosing the Right Content Type +/// +/// - **Text**: Use for all text-based user input. Universal support across all providers. +/// - **Image**: Use for visual analysis tasks. Requires vision-capable models (GPT-4V, Claude 3+, Gemini Pro Vision). +/// - URLs are preferred for large images (faster, less token usage) +/// - Base64 for small images or when URLs aren't available +/// - **Audio**: Use for transcription or audio analysis. Limited provider support (OpenAI Whisper, etc.). +/// - **Video**: Use for video understanding. Very limited provider support (Gemini 1.5+). +/// - **Document**: Use for document analysis (PDFs, etc.). Provider-specific support. +/// - **ToolResult**: Use only for returning tool execution results to the model in multi-turn conversations. +/// +/// # Size Limitations +/// +/// Be aware of size limits: +/// - **Base64 images**: Typically 20MB max, counts heavily toward token limits (85-765 tokens per image) +/// - **URLs**: Fetched by provider, usually larger limits +/// - **Documents**: Provider-specific, often 10-100 pages +/// +/// # Examples +/// +/// Creating text content: +/// +/// ``` +/// use rig::completion::message::UserContent; +/// +/// let content = UserContent::text("Hello, world!"); +/// ``` +/// +/// Creating image content from a URL (preferred): +/// +/// ``` +/// use rig::completion::message::{UserContent, ImageMediaType, ImageDetail}; +/// +/// let image = UserContent::image_url( +/// "https://example.com/image.png", +/// Some(ImageMediaType::PNG), +/// Some(ImageDetail::High) +/// ); +/// ``` +/// +/// Creating image content from base64 data: +/// +/// ``` +/// use rig::completion::message::{UserContent, ImageMediaType, ImageDetail}; +/// +/// let base64_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; +/// let image = UserContent::image_base64( +/// base64_data, +/// Some(ImageMediaType::PNG), +/// Some(ImageDetail::Low) // Use Low for small images +/// ); +/// ``` +/// +/// # Performance Tips +/// +/// - Prefer URLs over base64 for images when possible (saves tokens and is faster) +/// - Resize images to appropriate dimensions before sending (768x768 is often sufficient) +/// - Use `ImageDetail::Low` for thumbnails or simple images (saves ~200 tokens per image) +/// - For multi-image scenarios, consider whether all images are needed or if quality can be reduced +/// +/// # See also +/// +/// - [`Text`] for text content +/// - [`Image`] for image content +/// - [`ToolResult`] for tool execution results +/// - [`Audio`] for audio content +/// - [`Video`] for video content +/// - [`Document`] for document content #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(tag = "type", rename_all = "lowercase")] pub enum UserContent { + /// Plain text content. Text(Text), + + /// Result from a tool execution. ToolResult(ToolResult), + + /// Image content (base64-encoded or URL). Image(Image), + + /// Audio content (base64-encoded or URL). Audio(Audio), + + /// Video content (base64-encoded or URL). Video(Video), + + /// Document content (base64-encoded or URL). Document(Document), } -/// Describes responses from a provider which is either text or a tool call. +/// Content types that can be included in assistant messages. +/// +/// Assistant responses can contain text, requests to call tools/functions, +/// or reasoning steps (for models that support chain-of-thought reasoning). +/// +/// # Examples +/// +/// Creating text content: +/// +/// ``` +/// use rig::completion::message::AssistantContent; +/// +/// let content = AssistantContent::text("The answer is 42."); +/// ``` +/// +/// # See also +/// +/// - [`Text`] for text responses +/// - [`ToolCall`] for function/tool calls +/// - [`Reasoning`] for chain-of-thought reasoning #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(untagged)] pub enum AssistantContent { + /// Plain text response from the assistant. Text(Text), + + /// A request to call a tool or function. ToolCall(ToolCall), + + /// Chain-of-thought reasoning steps (for reasoning-capable models). Reasoning(Reasoning), } +/// Chain-of-thought reasoning from an AI model. +/// +/// Some advanced AI models can provide explicit reasoning steps alongside their +/// responses, allowing you to understand the model's thought process. This is +/// particularly useful for complex problem-solving, mathematical proofs, and +/// transparent decision-making. +/// +/// # Model Support +/// +/// As of 2024, reasoning is supported by: +/// - **OpenAI o1 models** (o1-preview, o1-mini) - Native reasoning support +/// - **Anthropic Claude 3.5 Sonnet** - With extended thinking mode +/// - **Google Gemini Pro** - With chain-of-thought prompting +/// +/// Check your provider's documentation for the latest support and capabilities. +/// +/// # Use Cases +/// +/// Reasoning is particularly valuable for: +/// - **Complex problem-solving**: Multi-step analytical tasks +/// - **Mathematical proofs**: Step-by-step mathematical reasoning +/// - **Debugging and troubleshooting**: Understanding decision paths +/// - **Transparent AI**: Making AI decisions explainable +/// - **Educational applications**: Showing work and explanations +/// +/// # Performance Impact +/// +/// Note that enabling reasoning: +/// - **Increases latency**: Models think longer before responding (5-30 seconds typical) +/// - **Increases token usage**: Each reasoning step counts as tokens (can add 500-2000 tokens) +/// - **May improve accuracy**: Particularly for complex, multi-step tasks +/// - **Not always necessary**: Simple tasks don't benefit from reasoning overhead +/// +/// # Invariants +/// +/// - The `reasoning` vector should not be empty when used in a response +/// - Each reasoning step should be a complete thought or sentence +/// - Steps are ordered chronologically (first thought to last) +/// +/// # Examples +/// +/// Creating reasoning from a single step: +/// +/// ``` +/// use rig::completion::message::Reasoning; +/// +/// let reasoning = Reasoning::new("First, I'll analyze the input data"); +/// assert_eq!(reasoning.reasoning.len(), 1); +/// ``` +/// +/// Creating reasoning from multiple steps: +/// +/// ``` +/// use rig::completion::message::Reasoning; +/// +/// let steps = vec![ +/// "First, analyze the problem structure".to_string(), +/// "Then, identify the key variables".to_string(), +/// "Next, apply the relevant formula".to_string(), +/// "Finally, verify the result".to_string(), +/// ]; +/// let reasoning = Reasoning::multi(steps); +/// assert_eq!(reasoning.reasoning.len(), 4); +/// ``` +/// +/// Using reasoning-capable models: +/// +/// ```ignore +/// # use rig::providers::openai; +/// # use rig::client::completion::CompletionClient; +/// # use rig::completion::Prompt; +/// # async fn example() -> Result<(), Box> { +/// let client = openai::Client::new("your-api-key"); +/// // Use a reasoning-capable model +/// let model = client.completion_model(openai::O1_PREVIEW); +/// +/// // The model will automatically include reasoning in complex tasks +/// let response = model.prompt( +/// "Prove that the square root of 2 is irrational using proof by contradiction" +/// ).await?; +/// +/// // Response includes both reasoning steps and final answer +/// # Ok(()) +/// # } +/// ``` #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[non_exhaustive] pub struct Reasoning { + /// Optional identifier for this reasoning instance. + /// + /// Used to associate reasoning with specific response turns in + /// multi-turn conversations. pub id: Option, + + /// The individual reasoning steps. + /// + /// Each string represents one step in the model's reasoning process. pub reasoning: Vec, } impl Reasoning { - /// Create a new reasoning item from a single item + /// Creates reasoning from a single step. + /// + /// # Examples + /// + /// ``` + /// use rig::completion::message::Reasoning; + /// + /// let reasoning = Reasoning::new("Analyzing the problem requirements"); + /// assert_eq!(reasoning.reasoning.len(), 1); + /// assert!(reasoning.id.is_none()); + /// ``` pub fn new(input: &str) -> Self { Self { id: None, @@ -83,15 +662,52 @@ impl Reasoning { } } + /// Sets an optional ID for this reasoning. + /// + /// # Examples + /// + /// ``` + /// use rig::completion::message::Reasoning; + /// + /// let reasoning = Reasoning::new("Step 1") + /// .optional_id(Some("reasoning-123".to_string())); + /// assert_eq!(reasoning.id, Some("reasoning-123".to_string())); + /// ``` pub fn optional_id(mut self, id: Option) -> Self { self.id = id; self } + + /// Sets the ID for this reasoning. + /// + /// # Examples + /// + /// ``` + /// use rig::completion::message::Reasoning; + /// + /// let reasoning = Reasoning::new("Step 1") + /// .with_id("reasoning-456".to_string()); + /// assert_eq!(reasoning.id, Some("reasoning-456".to_string())); + /// ``` pub fn with_id(mut self, id: String) -> Self { self.id = Some(id); self } + /// Creates reasoning from multiple steps. + /// + /// # Examples + /// + /// ``` + /// use rig::completion::message::Reasoning; + /// + /// let steps = vec![ + /// "First step".to_string(), + /// "Second step".to_string(), + /// ]; + /// let reasoning = Reasoning::multi(steps); + /// assert_eq!(reasoning.reasoning.len(), 2); + /// ``` pub fn multi(input: Vec) -> Self { Self { id: None, diff --git a/rig-core/src/completion/mod.rs b/rig-core/src/completion/mod.rs index 7dcef0ac6..df6ce53fe 100644 --- a/rig-core/src/completion/mod.rs +++ b/rig-core/src/completion/mod.rs @@ -1,3 +1,264 @@ +//! Core LLM completion functionality for Rig. +//! +//! This module forms the foundation of Rig's LLM interaction layer, providing +//! a unified, provider-agnostic interface for sending prompts to and receiving +//! responses from various Large Language Model providers. +//! +//! # Architecture +//! +//! The completion module is organized into two main submodules: +//! +//! - [`message`]: Defines the message format for conversations. Messages are +//! provider-agnostic and automatically converted to each provider's specific format. +//! - [`request`]: Defines the traits and types for building completion requests, +//! handling responses, and defining completion models. +//! +//! ## Abstraction Layers +//! +//! Rig provides three levels of abstraction for LLM interactions: +//! +//! ```text +//! ┌─────────────────────────────────────┐ +//! │ User Application Code │ +//! └──────────┬──────────────────────────┘ +//! │ +//! ├─> Prompt (simple one-shot) +//! ├─> Chat (multi-turn with history) +//! └─> Completion (full control) +//! │ +//! ┌──────────┴──────────────────────────┐ +//! │ CompletionModel Trait │ ← Implemented by providers +//! └──────────┬──────────────────────────┘ +//! │ +//! ┌───────┴────────┬────────────┐ +//! │ │ │ +//! OpenAI Anthropic Custom +//! Provider Provider Provider +//! ``` +//! +//! ### High-level: [`Prompt`] +//! +//! Simple one-shot prompting for straightforward requests: +//! +//! ```ignore +//! # use rig::providers::openai; +//! # use rig::client::completion::CompletionClient; +//! # use rig::completion::Prompt; +//! # async fn example() -> Result<(), Box> { +//! let client = openai::Client::new("your-api-key"); +//! let model = client.completion_model(openai::GPT_4); +//! +//! let response = model.prompt("Explain quantum computing").await?; +//! println!("{}", response); +//! # Ok(()) +//! # } +//! ``` +//! +//! **Use when:** You need a single response without conversation history. +//! +//! ### Mid-level: [`Chat`] +//! +//! Multi-turn conversations with context: +//! +//! ```ignore +//! # use rig::providers::openai; +//! # use rig::client::completion::CompletionClient; +//! # use rig::completion::{Chat, Message}; +//! # async fn example() -> Result<(), Box> { +//! let client = openai::Client::new("your-api-key"); +//! let model = client.completion_model(openai::GPT_4); +//! +//! let history = vec![ +//! Message::user("What is 2+2?"), +//! Message::assistant("2+2 equals 4."), +//! ]; +//! +//! let response = model.chat("What about 3+3?", history).await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! **Use when:** You need context from previous messages in the conversation. +//! +//! ### Low-level: [`Completion`] +//! +//! Full control over request parameters: +//! +//! ```ignore +//! # use rig::providers::openai; +//! # use rig::client::completion::CompletionClient; +//! # use rig::completion::Completion; +//! # async fn example() -> Result<(), Box> { +//! let client = openai::Client::new("your-api-key"); +//! let model = client.completion_model(openai::GPT_4); +//! +//! let request = model.completion_request("Explain quantum computing") +//! .temperature(0.7) +//! .max_tokens(500) +//! .build()?; +//! +//! let response = model.completion(request).await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! **Use when:** You need fine-grained control over temperature, tokens, or other parameters. +//! +//! # Provider-Agnostic Design +//! +//! All completion operations work identically across providers. Simply swap the client: +//! +//! ```ignore +//! # use rig::client::completion::CompletionClient; +//! # use rig::completion::Prompt; +//! # async fn example() -> Result<(), Box> { +//! // OpenAI +//! let openai_model = rig::providers::openai::Client::new("key") +//! .completion_model(rig::providers::openai::GPT_4); +//! +//! // Anthropic +//! let anthropic_model = rig::providers::anthropic::Client::new("key") +//! .completion_model(rig::providers::anthropic::CLAUDE_3_5_SONNET); +//! +//! // Same API for both +//! let response1 = openai_model.prompt("Hello").await?; +//! let response2 = anthropic_model.prompt("Hello").await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Common Patterns +//! +//! ## Error Handling with Retry +//! +//! ```ignore +//! use rig::providers::openai; +//! use rig::client::completion::CompletionClient; +//! use rig::completion::{Prompt, CompletionError}; +//! use std::time::Duration; +//! use tokio::time::sleep; +//! +//! # async fn example() -> Result<(), Box> { +//! let client = openai::Client::new("your-api-key"); +//! let model = client.completion_model(openai::GPT_4); +//! +//! let mut retries = 0; +//! let max_retries = 3; +//! +//! loop { +//! match model.prompt("Hello").await { +//! Ok(response) => { +//! println!("{}", response); +//! break; +//! } +//! Err(CompletionError::HttpError(_)) if retries < max_retries => { +//! retries += 1; +//! let delay = Duration::from_secs(2_u64.pow(retries)); +//! eprintln!("Network error. Retrying in {:?}...", delay); +//! sleep(delay).await; +//! } +//! Err(e) => return Err(e.into()), +//! } +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Streaming Responses +//! +//! ```ignore +//! # use rig::providers::openai; +//! # use rig::client::completion::CompletionClient; +//! # use rig::completion::Completion; +//! # use futures::StreamExt; +//! # async fn example() -> Result<(), Box> { +//! let client = openai::Client::new("your-api-key"); +//! let model = client.completion_model(openai::GPT_4); +//! +//! let request = model.completion_request("Write a story").build()?; +//! let mut stream = model.completion_stream(request).await?; +//! +//! while let Some(chunk) = stream.next().await { +//! print!("{}", chunk?); +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Building Conversation History +//! +//! ```ignore +//! # use rig::providers::openai; +//! # use rig::client::completion::CompletionClient; +//! # use rig::completion::{Message, Chat}; +//! # async fn example() -> Result<(), Box> { +//! let client = openai::Client::new("your-api-key"); +//! let model = client.completion_model(openai::GPT_4); +//! +//! let mut conversation = Vec::new(); +//! +//! // First exchange +//! conversation.push(Message::user("What's 2+2?")); +//! let response1 = model.chat("What's 2+2?", conversation.clone()).await?; +//! conversation.push(Message::assistant(response1)); +//! +//! // Second exchange with context +//! let response2 = model.chat("What about 3+3?", conversation.clone()).await?; +//! conversation.push(Message::assistant(response2)); +//! # Ok(()) +//! # } +//! ``` +//! +//! # Async Runtime +//! +//! All completion operations are async and require a runtime like Tokio: +//! +//! ```toml +//! [dependencies] +//! rig-core = "0.21" +//! tokio = { version = "1", features = ["full"] } +//! ``` +//! +//! ```ignore +//! use rig::providers::openai; +//! use rig::client::completion::CompletionClient; +//! use rig::completion::Prompt; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let client = openai::Client::new("your-api-key"); +//! let model = client.completion_model(openai::GPT_4); +//! let response = model.prompt("Hello").await?; +//! println!("{}", response); +//! Ok(()) +//! } +//! ``` +//! +//! # Performance Considerations +//! +//! ## Token Usage +//! - Text: ~1 token per 4 characters (English) +//! - Images (URL): 85-765 tokens depending on size and detail level +//! - Images (base64): Same as URL plus encoding overhead +//! - Each message in history adds to total token count +//! +//! ## Latency +//! - Simple prompts: 1-3 seconds typical +//! - Complex prompts with reasoning: 5-15 seconds +//! - Streaming: First token in <1 second +//! +//! ## Cost Optimization +//! - Use smaller models (GPT-3.5, Claude Haiku) for simple tasks +//! - Implement caching for repeated requests +//! - Limit conversation history length +//! - Use streaming for better perceived performance +//! +//! # See also +//! +//! - [`crate::providers`] for provider implementations (OpenAI, Anthropic, Cohere, etc.) +//! - [`crate::agent`] for building autonomous agents with tool use +//! - [`crate::embeddings`] for semantic search and RAG patterns + pub mod message; pub mod request; diff --git a/rig-core/src/completion/request.rs b/rig-core/src/completion/request.rs index 4119f3155..3a0fad2ef 100644 --- a/rig-core/src/completion/request.rs +++ b/rig-core/src/completion/request.rs @@ -23,41 +23,48 @@ //! The module also provides various structs and enums for representing generic completion requests, //! responses, and errors. //! -//! Example Usage: -//! ```rust +//! # Examples +//! +//! ## Basic Completion +//! +//! ```ignore //! use rig::providers::openai::{Client, self}; -//! use rig::completion::*; +//! use rig::client::completion::CompletionClient; +//! use rig::completion::Prompt; //! +//! # async fn example() -> Result<(), Box> { //! // Initialize the OpenAI client and a completion model //! let openai = Client::new("your-openai-api-key"); +//! let gpt_4 = openai.completion_model(openai::GPT_4); +//! +//! // Send a simple prompt +//! let response = gpt_4.prompt("Who are you?").await?; +//! println!("Response: {}", response); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Custom Completion Request //! +//! ```ignore +//! use rig::providers::openai::{Client, self}; +//! use rig::client::completion::CompletionClient; +//! use rig::completion::Completion; +//! +//! # async fn example() -> Result<(), Box> { +//! let openai = Client::new("your-openai-api-key"); //! let gpt_4 = openai.completion_model(openai::GPT_4); //! -//! // Create the completion request +//! // Build a custom completion request //! let request = gpt_4.completion_request("Who are you?") -//! .preamble("\ -//! You are Marvin, an extremely smart but depressed robot who is \ -//! nonetheless helpful towards humanity.\ -//! ") +//! .preamble("You are Marvin, a depressed but helpful robot.") //! .temperature(0.5) -//! .build(); +//! .build()?; //! -//! // Send the completion request and get the completion response -//! let response = gpt_4.completion(request) -//! .await -//! .expect("Failed to get completion response"); -//! -//! // Handle the completion response -//! match completion_response.choice { -//! ModelChoice::Message(message) => { -//! // Handle the completion response as a message -//! println!("Received message: {}", message); -//! } -//! ModelChoice::ToolCall(tool_name, tool_params) => { -//! // Handle the completion response as a tool call -//! println!("Received tool call: {} {:?}", tool_name, tool_params); -//! } -//! } +//! // Send the request +//! let response = gpt_4.completion(request).await?; +//! # Ok(()) +//! # } //! ``` //! //! For more information on how to use the completion functionality, refer to the documentation of @@ -81,60 +88,389 @@ use std::ops::{Add, AddAssign}; use std::sync::Arc; use thiserror::Error; -// Errors +// ================================================================ +// Error types +// ================================================================ + +/// Errors that can occur during completion operations. +/// +/// This enum covers all possible errors that may occur when making completion +/// requests to LLM providers, from network issues to provider-specific errors. +/// +/// # Examples +/// +/// ## Basic Error Handling +/// +/// ```ignore +/// use rig::completion::{CompletionError, Prompt}; +/// use rig::providers::openai; +/// use rig::client::completion::CompletionClient; +/// +/// # async fn example() -> Result<(), Box> { +/// let client = openai::Client::new("api-key"); +/// let model = client.completion_model(openai::GPT_4); +/// +/// match model.prompt("Hello").await { +/// Ok(response) => println!("Success: {}", response), +/// Err(CompletionError::HttpError(e)) => { +/// eprintln!("Network error: {}. Check your internet connection.", e); +/// } +/// Err(CompletionError::ProviderError(msg)) if msg.contains("rate_limit") => { +/// eprintln!("Rate limited. Please wait and try again."); +/// } +/// Err(CompletionError::ProviderError(msg)) if msg.contains("invalid_api_key") => { +/// eprintln!("Invalid API key. Check your credentials."); +/// } +/// Err(CompletionError::ProviderError(msg)) => { +/// eprintln!("Provider error: {}", msg); +/// } +/// Err(e) => eprintln!("Unexpected error: {}", e), +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// ## Retry with Exponential Backoff +/// +/// ```ignore +/// use rig::completion::{CompletionError, Prompt}; +/// use rig::providers::openai; +/// use rig::client::completion::CompletionClient; +/// use std::time::Duration; +/// use tokio::time::sleep; +/// +/// # async fn example() -> Result> { +/// let client = openai::Client::new("api-key"); +/// let model = client.completion_model(openai::GPT_4); +/// +/// let mut retries = 0; +/// let max_retries = 3; +/// +/// loop { +/// match model.prompt("Hello").await { +/// Ok(response) => return Ok(response), +/// Err(CompletionError::HttpError(_)) if retries < max_retries => { +/// retries += 1; +/// let delay = Duration::from_secs(2_u64.pow(retries)); +/// eprintln!("Network error. Retry {}/{} in {:?}", retries, max_retries, delay); +/// sleep(delay).await; +/// } +/// Err(CompletionError::ProviderError(msg)) if msg.contains("rate_limit") && retries < max_retries => { +/// retries += 1; +/// let delay = Duration::from_secs(5 * retries as u64); +/// eprintln!("Rate limited. Waiting {:?} before retry...", delay); +/// sleep(delay).await; +/// } +/// Err(e) => return Err(e.into()), +/// } +/// } +/// # } +/// ``` +/// +/// ## Fallback to Different Model +/// +/// ```ignore +/// use rig::completion::{CompletionError, Prompt}; +/// use rig::providers::openai; +/// use rig::client::completion::CompletionClient; +/// +/// # async fn example() -> Result> { +/// let client = openai::Client::new("api-key"); +/// +/// // Try GPT-4 first +/// let gpt4 = client.completion_model(openai::GPT_4); +/// match gpt4.prompt("Explain quantum computing").await { +/// Ok(response) => return Ok(response), +/// Err(CompletionError::ProviderError(msg)) if msg.contains("rate_limit") => { +/// eprintln!("GPT-4 rate limited, falling back to GPT-3.5..."); +/// // Fall back to cheaper model +/// let gpt35 = client.completion_model(openai::GPT_3_5_TURBO); +/// return Ok(gpt35.prompt("Explain quantum computing").await?); +/// } +/// Err(e) => return Err(e.into()), +/// } +/// # } +/// ``` #[derive(Debug, Error)] +#[non_exhaustive] pub enum CompletionError { - /// Http error (e.g.: connection error, timeout, etc.) - #[error("HttpError: {0}")] + /// HTTP request failed. + /// + /// This occurs when there are network connectivity issues, timeouts, + /// or other HTTP-level problems communicating with the provider. + /// + /// Common causes: + /// - No internet connection + /// - Request timeout + /// - DNS resolution failure + /// - SSL/TLS errors + #[error("HTTP request failed: {0}")] HttpError(#[from] reqwest::Error), - /// Json error (e.g.: serialization, deserialization) - #[error("JsonError: {0}")] + /// JSON serialization or deserialization failed. + /// + /// This occurs when: + /// - The provider returns malformed JSON + /// - Request data cannot be serialized + /// - Response data doesn't match expected schema + #[error("JSON error: {0}")] JsonError(#[from] serde_json::Error), - /// Url error (e.g.: invalid URL) - #[error("UrlError: {0}")] + /// Invalid URL provided. + /// + /// This occurs when attempting to construct an invalid URL for the provider endpoint. + #[error("Invalid URL: {0}")] UrlError(#[from] url::ParseError), - /// Error building the completion request - #[error("RequestError: {0}")] + /// Error building the completion request. + /// + /// This occurs when there's an issue constructing the request, + /// such as invalid parameters or missing required fields. + #[error("Request building error: {0}")] RequestError(#[from] Box), - /// Error parsing the completion response - #[error("ResponseError: {0}")] + /// Error parsing the completion response. + /// + /// This occurs when the provider returns a valid HTTP response + /// but the content cannot be parsed or understood. + #[error("Response parsing error: {0}")] ResponseError(String), - /// Error returned by the completion model provider - #[error("ProviderError: {0}")] + /// Error returned by the LLM provider. + /// + /// This represents errors from the provider's API, such as: + /// - Invalid API key + /// - Rate limits exceeded + /// - Model not found + /// - Content policy violations + /// - Insufficient credits/quota + #[error("Provider error: {0}")] ProviderError(String), } -/// Prompt errors +/// Errors that can occur during prompt operations. +/// +/// This enum covers errors specific to high-level prompt operations, +/// including multi-turn conversations and tool calling. +/// +/// # Examples +/// +/// ```ignore +/// use rig::completion::{PromptError, Prompt}; +/// use rig::providers::openai; +/// use rig::client::completion::CompletionClient; +/// +/// # async fn example() -> Result<(), PromptError> { +/// let client = openai::Client::new("api-key"); +/// let model = client.completion_model(openai::GPT_4); +/// +/// match model.prompt("Hello").await { +/// Ok(response) => println!("Success: {}", response), +/// Err(PromptError::MaxDepthError { max_depth, .. }) => { +/// eprintln!("Too many tool calls (limit: {})", max_depth); +/// }, +/// Err(e) => eprintln!("Error: {}", e), +/// } +/// # Ok(()) +/// # } +/// ``` #[derive(Debug, Error)] +#[non_exhaustive] pub enum PromptError { - /// Something went wrong with the completion - #[error("CompletionError: {0}")] + /// Underlying completion operation failed. + /// + /// See [`CompletionError`] for details on specific failure modes. + #[error("Completion error: {0}")] CompletionError(#[from] CompletionError), - /// There was an error while using a tool - #[error("ToolCallError: {0}")] + /// Tool execution failed. + /// + /// This occurs when the LLM requests a tool call but the tool + /// execution encounters an error. + #[error("Tool call error: {0}")] ToolError(#[from] ToolSetError), - /// The LLM tried to call too many tools during a multi-turn conversation. - /// To fix this, you may either need to lower the amount of tools your model has access to (and then create other agents to share the tool load) - /// or increase the amount of turns given in `.multi_turn()`. - #[error("MaxDepthError: (reached limit: {max_depth})")] + /// Maximum conversation depth exceeded. + /// + /// This occurs when the LLM attempts too many tool calls in a multi-turn + /// conversation, exceeding the configured maximum depth. + /// + /// # Solutions + /// + /// To resolve this: + /// - Reduce the number of available tools (distribute across multiple agents) + /// - Increase the maximum depth with `.multi_turn(depth)` + /// - Simplify the task to require fewer tool calls + #[error("Maximum conversation depth exceeded (limit: {max_depth})")] MaxDepthError { + /// The maximum depth that was exceeded. max_depth: usize, + + /// The conversation history up to the point of failure. chat_history: Box>, + + /// The prompt that triggered the error. prompt: Message, }, } +/// A document that can be provided as context to an LLM. +/// +/// Documents are structured pieces of text that models can reference during +/// completion. They differ from regular text messages in important ways: +/// - Have unique IDs for reference and citation +/// - Are formatted as files (typically with XML-like tags) +/// - Can include metadata via `additional_props` +/// - Are treated as "reference material" rather than conversation turns +/// +/// # When to Use Documents vs. Messages +/// +/// **Use Documents when:** +/// - Providing reference material (documentation, knowledge base articles, code files) +/// - The content is factual/static rather than conversational +/// - You want the model to cite sources by ID +/// - Implementing RAG (Retrieval-Augmented Generation) patterns +/// - Building code analysis or document Q&A systems +/// +/// **Use Messages when:** +/// - Having a conversation with the model +/// - The text is a question or response +/// - Building chat history +/// - User input or model output +/// +/// # Formatting +/// +/// Documents are typically formatted as XML-like files when sent to the model: +/// +/// ```text +/// +/// [metadata: source=API Docs, version=1.0] +/// Content of the document... +/// +/// ``` +/// +/// # Examples +/// +/// ## Basic Document +/// +/// ``` +/// use rig::completion::request::Document; +/// use std::collections::HashMap; +/// +/// let doc = Document { +/// id: "rust-ownership".to_string(), +/// text: "Rust's ownership system ensures memory safety without garbage collection.".to_string(), +/// additional_props: HashMap::new(), +/// }; +/// ``` +/// +/// ## Document with Metadata +/// +/// ``` +/// use rig::completion::request::Document; +/// use std::collections::HashMap; +/// +/// let mut metadata = HashMap::new(); +/// metadata.insert("source".to_string(), "Rust Book".to_string()); +/// metadata.insert("chapter".to_string(), "4".to_string()); +/// metadata.insert("topic".to_string(), "Ownership".to_string()); +/// metadata.insert("last_updated".to_string(), "2024-01-15".to_string()); +/// +/// let doc = Document { +/// id: "rust-book-ch4".to_string(), +/// text: "Ownership is Rust's most unique feature...".to_string(), +/// additional_props: metadata, +/// }; +/// ``` +/// +/// ## RAG Pattern (Retrieval-Augmented Generation) +/// +/// ```ignore +/// # use rig::providers::openai; +/// # use rig::client::completion::CompletionClient; +/// # use rig::completion::{Prompt, request::Document}; +/// # use std::collections::HashMap; +/// # async fn example() -> Result<(), Box> { +/// let client = openai::Client::new("your-api-key"); +/// let model = client.completion_model(openai::GPT_4); +/// +/// // Retrieved from vector database based on user query +/// let relevant_docs = vec![ +/// Document { +/// id: "ownership-basics".to_string(), +/// text: "Rust's ownership system manages memory through a set of rules...".to_string(), +/// additional_props: HashMap::new(), +/// }, +/// Document { +/// id: "borrowing-rules".to_string(), +/// text: "Borrowing allows you to have references to a value...".to_string(), +/// additional_props: HashMap::new(), +/// }, +/// ]; +/// +/// // Build prompt with context +/// let context = relevant_docs.iter() +/// .map(|doc| format!("\n{}\n", doc.id, doc.text)) +/// .collect::>() +/// .join("\n\n"); +/// +/// let prompt = format!( +/// "Context:\n{}\n\nQuestion: How does Rust prevent memory leaks?\n\nAnswer based on the context above:", +/// context +/// ); +/// +/// let response = model.prompt(&prompt).await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// ## Code Documentation Example +/// +/// ``` +/// use rig::completion::request::Document; +/// use std::collections::HashMap; +/// +/// let mut metadata = HashMap::new(); +/// metadata.insert("file".to_string(), "src/main.rs".to_string()); +/// metadata.insert("language".to_string(), "rust".to_string()); +/// metadata.insert("lines".to_string(), "1-50".to_string()); +/// +/// let code_doc = Document { +/// id: "main-rs".to_string(), +/// text: r#" +/// fn main() { +/// let config = load_config(); +/// run_server(config); +/// } +/// "#.to_string(), +/// additional_props: metadata, +/// }; +/// ``` +/// +/// # Performance Tips +/// +/// - Keep document size reasonable (typically 1000-5000 words each) +/// - Use concise, relevant excerpts rather than full documents +/// - Limit the number of documents (3-5 most relevant is often optimal) +/// - Include metadata to help the model understand context and provenance +/// - Consider pre-processing documents to remove irrelevant information +/// +/// # See also +/// +/// - [`Message`] for conversational messages +/// - Vector embeddings for retrieving relevant documents #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Document { + /// Unique identifier for this document. pub id: String, + + /// The text content of the document. pub text: String, + + /// Additional properties for this document. + /// + /// These are flattened during serialization and can contain + /// metadata like author, date, source, etc. #[serde(flatten)] pub additional_props: HashMap, } @@ -469,11 +805,12 @@ impl CompletionRequest { /// Builder struct for constructing a completion request. /// /// Example usage: -/// ```rust +/// ```ignore /// use rig::{ /// providers::openai::{Client, self}, /// completion::CompletionRequestBuilder, /// }; +/// use rig::client::completion::CompletionClient; /// /// let openai = Client::new("your-openai-api-key"); /// let model = openai.completion_model(openai::GPT_4O).build(); @@ -490,11 +827,12 @@ impl CompletionRequest { /// ``` /// /// Alternatively, you can execute the completion request directly from the builder: -/// ```rust +/// ```ignore /// use rig::{ /// providers::openai::{Client, self}, /// completion::CompletionRequestBuilder, /// }; +/// use rig::client::completion::CompletionClient; /// /// let openai = Client::new("your-openai-api-key"); /// let model = openai.completion_model(openai::GPT_4O).build();