struct_llm/
lib.rs

1//! # struct-llm
2//!
3//! A lightweight, WASM-compatible Rust library for generating structured outputs from LLMs
4//! using a tool-based approach.
5//!
6//! ## Quick Start
7//!
8//! ```rust,ignore
9//! use struct_llm::StructuredOutput;
10//! use serde::{Deserialize, Serialize};
11//!
12//! #[derive(Debug, Serialize, Deserialize, StructuredOutput)]
13//! #[structured_output(
14//!     name = "sentiment_analysis",
15//!     description = "Analyzes the sentiment of the given text"
16//! )]
17//! struct SentimentAnalysis {
18//!     sentiment: String,
19//!     confidence: f32,
20//!     reasoning: String,
21//! }
22//!
23//! // Generate tool definition
24//! let tool = SentimentAnalysis::tool_definition();
25//!
26//! // Make API request with your HTTP client
27//! let response = make_api_request_with_tools(&prompt, &[tool]).await?;
28//!
29//! // Extract and validate structured response
30//! let tool_calls = extract_tool_calls(&response, Provider::OpenAI)?;
31//! let result: SentimentAnalysis = parse_tool_response(&tool_calls[0])?;
32//! ```
33
34use serde::{de::DeserializeOwned, Deserialize, Serialize};
35
36pub mod error;
37pub mod provider;
38pub mod schema;
39pub mod streaming;
40pub mod tool;
41
42pub use error::{Error, Result};
43pub use provider::{build_enforced_tool_request, build_request_with_tools, Provider};
44pub use schema::validate as validate_schema;
45pub use streaming::{StreamParser, ToolDelta};
46pub use tool::{extract_tool_calls, parse_tool_response, ToolCall, ToolDefinition};
47
48// Re-export derive macro when feature is enabled
49#[cfg(feature = "derive")]
50pub use struct_llm_derive::StructuredOutput;
51
52/// Core trait for types that can be used as structured LLM outputs.
53///
54/// This trait enables a type to be used as a structured output from an LLM by:
55/// 1. Generating a JSON Schema describing the type's structure
56/// 2. Creating a tool definition that the LLM can call
57/// 3. Validating and deserializing tool call arguments
58///
59/// The derive macro provides automatic implementation for most use cases:
60///
61/// ```rust,ignore
62/// #[derive(Serialize, Deserialize, StructuredOutput)]
63/// #[structured_output(
64///     name = "final_answer",
65///     description = "Final response with structured data"
66/// )]
67/// struct Answer {
68///     response: String,
69///     confidence: f32,
70/// }
71/// ```
72pub trait StructuredOutput: Serialize + DeserializeOwned {
73    /// Tool name used in API requests (e.g., "final_answer", "create_character")
74    fn tool_name() -> &'static str;
75
76    /// Human-readable description of what this output represents
77    fn tool_description() -> &'static str;
78
79    /// JSON Schema describing this type's structure
80    ///
81    /// The schema should follow the JSON Schema specification and will be used
82    /// to validate the LLM's output before deserialization.
83    fn json_schema() -> serde_json::Value;
84
85    /// Complete tool definition ready for API requests
86    ///
87    /// This combines the tool name, description, and schema into the format
88    /// expected by LLM APIs (OpenAI, Anthropic, etc.)
89    fn tool_definition() -> ToolDefinition {
90        ToolDefinition {
91            name: Self::tool_name().to_string(),
92            description: Self::tool_description().to_string(),
93            parameters: Self::json_schema(),
94        }
95    }
96}
97
98/// Message in a conversation
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Message {
101    pub role: String,
102    pub content: String,
103}
104
105impl Message {
106    pub fn system(content: impl Into<String>) -> Self {
107        Self {
108            role: "system".to_string(),
109            content: content.into(),
110        }
111    }
112
113    pub fn user(content: impl Into<String>) -> Self {
114        Self {
115            role: "user".to_string(),
116            content: content.into(),
117        }
118    }
119
120    pub fn assistant(content: impl Into<String>) -> Self {
121        Self {
122            role: "assistant".to_string(),
123            content: content.into(),
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_message_creation() {
134        let msg = Message::user("Hello");
135        assert_eq!(msg.role, "user");
136        assert_eq!(msg.content, "Hello");
137    }
138}