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}