Skip to main content

instructors/
lib.rs

1//! # instructors
2//!
3//! Type-safe structured output extraction from LLMs.
4//!
5//! Define a Rust struct, and `instructors` will make the LLM return data that
6//! deserializes directly into it — with automatic schema generation, validation,
7//! and retry on failure.
8//!
9//! ## Quick start
10//!
11//! ```rust,no_run
12//! use instructors::prelude::*;
13//!
14//! #[derive(Debug, Deserialize, JsonSchema)]
15//! struct Contact {
16//!     name: String,
17//!     email: Option<String>,
18//!     phone: Option<String>,
19//! }
20//!
21//! # async fn run() -> instructors::Result<()> {
22//! let client = Client::openai("sk-...");
23//! let result: ExtractResult<Contact> = client
24//!     .extract("Contact John Doe at john@example.com")
25//!     .model("gpt-4o")
26//!     .await?;
27//!
28//! println!("{}: {:?}", result.value.name, result.value.email);
29//! println!("tokens: {}, cost: {:?}", result.usage.total_tokens, result.usage.cost);
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ## Validation
35//!
36//! ```rust,no_run
37//! use instructors::prelude::*;
38//!
39//! #[derive(Debug, Deserialize, JsonSchema)]
40//! struct User {
41//!     name: String,
42//!     age: u32,
43//! }
44//!
45//! # async fn run() -> instructors::Result<()> {
46//! let client = Client::openai("sk-...");
47//!
48//! // closure-based validation
49//! let user: User = client.extract("...")
50//!     .validate(|u: &User| {
51//!         if u.age > 150 { Err("age unrealistic".into()) } else { Ok(()) }
52//!     })
53//!     .await?.value;
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! ## Features
59//!
60//! - **Multi-provider** — OpenAI (`response_format` strict), Anthropic (`tool_use`),
61//!   plus any OpenAI/Anthropic-compatible API
62//! - **List extraction** — `extract_many::<T>()` returns `Vec<T>`
63//! - **Batch processing** — `extract_batch::<T>()` with configurable concurrency
64//! - **Multi-turn** — `.messages()` for conversation history
65//! - **Validation** — closure-based `.validate()` or trait-based `.validated()`
66//! - **Lifecycle hooks** — `.on_request()` / `.on_response()`
67//! - **Cost tracking** — token counting and cost estimation via `tiktoken` (optional)
68
69mod batch;
70mod client;
71mod error;
72mod provider;
73mod schema;
74mod usage;
75mod validate;
76
77pub use batch::BatchBuilder;
78pub use client::{Client, ExtractBuilder, ExtractResult};
79pub use error::{Error, Result};
80pub use provider::Message;
81pub use usage::Usage;
82pub use validate::{Validate, ValidationError};
83
84// re-export for user convenience
85pub use schemars::JsonSchema;
86pub use serde;
87
88/// Common imports for working with instructors.
89///
90/// ```rust
91/// use instructors::prelude::*;
92/// ```
93pub mod prelude {
94    pub use crate::{
95        BatchBuilder, Client, ExtractResult, Message, Usage, Validate, ValidationError,
96    };
97    pub use schemars::JsonSchema;
98    pub use serde::Deserialize;
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use schemars::JsonSchema;
105    use serde::Deserialize;
106
107    #[derive(Debug, Deserialize, JsonSchema)]
108    struct TestStruct {
109        name: String,
110        age: u32,
111    }
112
113    #[derive(Debug, Deserialize, JsonSchema)]
114    struct WithOptional {
115        title: String,
116        subtitle: Option<String>,
117    }
118
119    #[derive(Debug, Deserialize, JsonSchema)]
120    enum Category {
121        Bug,
122        Feature,
123        Question,
124    }
125
126    #[test]
127    fn deserialize_from_json() {
128        let json = r#"{"name": "Alice", "age": 30}"#;
129        let result: TestStruct = serde_json::from_str(json).unwrap();
130        assert_eq!(result.name, "Alice");
131        assert_eq!(result.age, 30);
132    }
133
134    #[test]
135    fn optional_field_present() {
136        let json = r#"{"title": "Hello", "subtitle": "World"}"#;
137        let result: WithOptional = serde_json::from_str(json).unwrap();
138        assert_eq!(result.subtitle, Some("World".into()));
139    }
140
141    #[test]
142    fn optional_field_null() {
143        let json = r#"{"title": "Hello", "subtitle": null}"#;
144        let result: WithOptional = serde_json::from_str(json).unwrap();
145        assert_eq!(result.subtitle, None);
146    }
147
148    #[test]
149    fn optional_field_missing() {
150        let json = r#"{"title": "Hello"}"#;
151        let result: WithOptional = serde_json::from_str(json).unwrap();
152        assert_eq!(result.subtitle, None);
153    }
154
155    #[test]
156    fn enum_deserialize() {
157        let json = r#""Bug""#;
158        let result: Category = serde_json::from_str(json).unwrap();
159        assert!(matches!(result, Category::Bug));
160
161        let json = r#""Feature""#;
162        let result: Category = serde_json::from_str(json).unwrap();
163        assert!(matches!(result, Category::Feature));
164
165        let json = r#""Question""#;
166        let result: Category = serde_json::from_str(json).unwrap();
167        assert!(matches!(result, Category::Question));
168    }
169
170    #[test]
171    fn schema_generation() {
172        let schema = schemars::schema_for!(TestStruct);
173        let value = serde_json::to_value(&schema).unwrap();
174        assert_eq!(value["type"], "object");
175        assert!(value["properties"]["name"].is_object());
176        assert!(value["properties"]["age"].is_object());
177    }
178
179    #[test]
180    fn usage_accumulate() {
181        let mut usage = Usage::default();
182        usage.accumulate(100, 50);
183        assert_eq!(usage.input_tokens, 100);
184        assert_eq!(usage.output_tokens, 50);
185        assert_eq!(usage.total_tokens, 150);
186
187        usage.accumulate(200, 100);
188        assert_eq!(usage.input_tokens, 300);
189        assert_eq!(usage.output_tokens, 150);
190        assert_eq!(usage.total_tokens, 450);
191    }
192
193    #[test]
194    fn prelude_re_exports() {
195        // verify all prelude items are accessible
196        fn _check() {
197            use crate::prelude::*;
198            let _: fn() -> std::result::Result<(), ValidationError> = || Ok(());
199            fn _accepts_client(_: &Client) {}
200            fn _accepts_usage(_: &Usage) {}
201        }
202    }
203
204    #[test]
205    fn re_exports_available() {
206        // verify top-level re-exports
207        let _: fn() -> Result<()> = || Ok(());
208        fn _check_json_schema<T: JsonSchema>() {}
209        _check_json_schema::<TestStruct>();
210    }
211}