aither_derive/lib.rs
1//! # aither-derive
2//!
3//! Procedural macros for converting Rust functions into AI tools that can be called by language models.
4//!
5//! This crate provides the `#[tool]` attribute macro that automatically generates the necessary
6//! boilerplate code to make your async functions callable by AI models through the `aither` framework.
7//!
8//! ## Quick Start
9//!
10//! Transform any async function into an AI tool by adding the `#[tool]` attribute.
11//! Tool description comes from rustdoc on the Args struct:
12//!
13//! ```rust
14//! use aither::Result;
15//! use aither_derive::tool;
16//! use schemars::JsonSchema;
17//! use serde::Deserialize;
18//!
19//! /// Get the current UTC time.
20//! #[derive(JsonSchema, Deserialize)]
21//! pub struct GetTimeArgs;
22//!
23//! #[tool]
24//! pub async fn get_time(_args: GetTimeArgs) -> Result<&'static str> {
25//! Ok("2023-10-01T12:00:00Z")
26//! }
27//! ```
28//!
29//! ## Function Patterns
30//!
31//! ### Simple Parameters
32//!
33//! ```rust
34//! use serde::Serialize;
35//! use schemars::JsonSchema;
36//! use serde::Deserialize;
37//!
38//! #[derive(Debug, Serialize)]
39//! pub struct SearchResult {
40//! title: String,
41//! url: String,
42//! }
43//!
44//! /// Search the web for content.
45//! #[derive(JsonSchema, Deserialize)]
46//! pub struct SearchArgs {
47//! pub keywords: Vec<String>,
48//! pub limit: u32,
49//! }
50//!
51//! #[tool]
52//! pub async fn search(args: SearchArgs) -> Result<Vec<SearchResult>> {
53//! Ok(vec![])
54//! }
55//! ```
56//!
57//! ### Complex Parameters with Documentation
58//!
59//! ```rust
60//! use schemars::JsonSchema;
61//! use serde::Deserialize;
62//!
63//! /// Generate an image from a text prompt.
64//! #[derive(Debug, JsonSchema, Deserialize)]
65//! pub struct ImageArgs {
66//! /// The text prompt for image generation
67//! pub prompt: String,
68//! /// Image width in pixels
69//! #[serde(default = "default_width")]
70//! pub width: u32,
71//! /// Image height in pixels
72//! #[serde(default = "default_height")]
73//! pub height: u32,
74//! }
75//!
76//! fn default_width() -> u32 { 512 }
77//! fn default_height() -> u32 { 512 }
78//!
79//! #[tool]
80//! pub async fn generate_image(args: ImageArgs) -> Result<String> {
81//! Ok(format!("Generated image: {}", args.prompt))
82//! }
83//! ```
84//!
85//! ## Requirements
86//!
87//! - Functions must be `async`
88//! - Return type must be `Result<T>` where `T: serde::Serialize`
89//! - Parameters must implement `serde::Deserialize` and `schemars::JsonSchema`
90//! - No `self` parameters (static functions only)
91//! - No lifetime or generic parameters
92
93use convert_case::{Case, Casing};
94use proc_macro::TokenStream;
95use quote::{format_ident, quote};
96use syn::{
97 FnArg, Ident, ItemFn, LitStr, Token, Type, Visibility,
98 parse::{Parse, ParseStream},
99 parse_macro_input, parse_quote,
100};
101
102/// Arguments for the `#[tool]` attribute macro
103struct ToolArgs {
104 rename: Option<String>,
105}
106
107impl Parse for ToolArgs {
108 /// Parse the arguments from the `#[tool(...)]` attribute.
109 ///
110 /// Supports:
111 /// - `rename = "..."` (optional): Custom name for the tool (defaults to function name)
112 fn parse(input: ParseStream) -> syn::Result<Self> {
113 let mut rename = None;
114
115 while !input.is_empty() {
116 let ident: Ident = input.parse()?;
117 let _: Token![=] = input.parse()?;
118 let value: LitStr = input.parse()?;
119
120 match ident.to_string().as_str() {
121 "rename" => rename = Some(value.value()),
122 _ => {
123 return Err(syn::Error::new_spanned(
124 ident,
125 "unknown attribute. Supported: rename",
126 ));
127 }
128 }
129
130 if input.peek(Token![,]) {
131 let _: Token![,] = input.parse()?;
132 }
133 }
134
135 Ok(Self { rename })
136 }
137}
138
139/// Converts an async function into an AI tool that can be called by language models.
140///
141/// This procedural macro generates the necessary boilerplate code to make your function
142/// callable through the `aither::llm::Tool` trait.
143///
144/// Tool description is extracted from rustdoc on the Args struct via `schemars::JsonSchema`.
145///
146/// # Arguments
147///
148/// - `rename` (optional): A custom name for the tool. If not provided, uses the function name.
149///
150/// # Examples
151///
152/// ## Basic Usage
153///
154/// ```rust
155/// use aither::Result;
156/// use aither_derive::tool;
157/// use schemars::JsonSchema;
158/// use serde::Deserialize;
159///
160/// /// Get the current system time.
161/// #[derive(JsonSchema, Deserialize)]
162/// pub struct CurrentTimeArgs;
163///
164/// #[tool]
165/// pub async fn current_time(_args: CurrentTimeArgs) -> Result<String> {
166/// Ok(chrono::Utc::now().to_rfc3339())
167/// }
168/// ```
169///
170/// ## With Parameters
171///
172/// ```rust
173/// use schemars::JsonSchema;
174/// use serde::Deserialize;
175///
176/// /// Send an email to a recipient.
177/// #[derive(JsonSchema, Deserialize)]
178/// pub struct EmailRequest {
179/// /// Recipient email address
180/// pub to: String,
181/// /// Email subject line
182/// pub subject: String,
183/// /// Email body content
184/// pub body: String,
185/// }
186///
187/// #[tool]
188/// pub async fn send_email(request: EmailRequest) -> Result<String> {
189/// Ok(format!("Email sent to {}", request.to))
190/// }
191/// ```
192///
193/// ## With Custom Name
194///
195/// ```rust
196/// /// Perform complex mathematical calculations.
197/// #[derive(JsonSchema, Deserialize)]
198/// pub struct CalcArgs {
199/// pub expression: String,
200/// }
201///
202/// #[tool(rename = "calculator")]
203/// pub async fn complex_math_function(args: CalcArgs) -> Result<f64> {
204/// Ok(42.0)
205/// }
206/// ```
207///
208/// # Generated Code
209///
210/// For a function named `search`, the macro generates:
211///
212/// 1. A `SearchArgs` struct (if the function has multiple parameters)
213/// 2. A `Search` struct that implements `aither::llm::Tool`
214/// 3. All necessary trait implementations for JSON schema generation and deserialization
215///
216/// # Requirements
217///
218/// - Function must be `async`
219/// - Return type must be `Result<T>` where `T` implements `serde::Serialize`
220/// - Parameters must implement `serde::Deserialize` and `schemars::JsonSchema`
221/// - No `self` parameters (only free functions are supported)
222/// - No lifetime parameters or generics
223///
224/// # Errors
225///
226/// The macro will produce compile-time errors if:
227/// - The function is not async
228/// - The function has `self` parameters
229/// - The function has more than the supported number of parameters
230/// - Required attributes are missing
231#[proc_macro_attribute]
232pub fn tool(args: TokenStream, input: TokenStream) -> TokenStream {
233 let args = parse_macro_input!(args as ToolArgs);
234 let input_fn = parse_macro_input!(input as ItemFn);
235
236 match tool_impl(args, input_fn) {
237 Ok(tokens) => tokens.into(),
238 Err(err) => err.to_compile_error().into(),
239 }
240}
241
242/// Implementation details for the `#[tool]` macro.
243///
244/// This function performs the actual code generation, transforming the annotated async function
245/// into a struct that implements the `Tool` trait.
246fn tool_impl(args: ToolArgs, input_fn: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
247 let fn_name = &input_fn.sig.ident;
248 let tool_name = args.rename.unwrap_or_else(|| fn_name.to_string());
249 let fn_vis = &input_fn.vis;
250
251 let tool_struct_name = format_ident!("{}", fn_name.to_string().to_case(Case::Pascal));
252
253 // Analyze function signature
254 let AnalyzedArgs {
255 args_type,
256 params,
257 stream,
258 } = analyze_function_args(fn_vis, &tool_struct_name, &input_fn.sig.inputs)?;
259
260 if input_fn.sig.asyncness.is_none() {
261 return Err(syn::Error::new_spanned(
262 input_fn.sig,
263 "Tool functions must be async",
264 ));
265 }
266
267 let call_expr = if params.is_empty() {
268 // No parameters, call the function directly
269 quote! { #fn_name().await }
270 } else {
271 // Call the function with extracted parameters
272 let args_tuple = quote! { #(#params),* };
273 quote! { #fn_name(#args_tuple).await }
274 };
275
276 let extractor = if params.len() <= 1 {
277 quote! {}
278 } else {
279 quote! { let Self::Arguments { #(#params),* } = args; }
280 };
281
282 let expanded = quote! {
283 #input_fn
284
285 #stream
286
287
288 #[derive(::core::default::Default,::core::fmt::Debug)]
289 #fn_vis struct #tool_struct_name;
290
291 impl ::aither::llm::Tool for #tool_struct_name {
292 fn name(&self) -> ::aither::__hidden::CowStr {
293 #tool_name.into()
294 }
295 type Arguments = #args_type;
296 type Res = ::aither::llm::ToolResult;
297
298 async fn call(&self, args: Self::Arguments) -> ::aither::Result<Self::Res> {
299 #extractor
300 ::aither::llm::IntoToolResult::into_tool_result(#call_expr)
301 }
302 }
303 };
304
305 Ok(expanded)
306}
307
308/// Container for analyzed function arguments and generated types.
309struct AnalyzedArgs {
310 /// The type used for the Tool's Arguments associated type
311 args_type: Type,
312 /// Parameter names extracted from the function signature
313 params: Vec<Ident>,
314 /// Generated argument struct definition (if needed)
315 stream: proc_macro2::TokenStream,
316}
317
318/// Analyzes function parameters and generates appropriate argument types.
319///
320/// This function handles three cases:
321/// - No parameters: Uses unit type `()`
322/// - Single parameter: Uses the parameter type directly
323/// - Multiple parameters: Generates a new struct with all parameters as fields
324fn analyze_function_args(
325 fn_vis: &Visibility,
326 struct_name: &Ident,
327 inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
328) -> syn::Result<AnalyzedArgs> {
329 match inputs.len() {
330 0 => {
331 // No arguments - use unit type
332
333 Ok(AnalyzedArgs {
334 args_type: parse_quote! { () },
335 params: vec![],
336 stream: quote! {},
337 })
338 }
339 1 => {
340 // Single argument
341 if let FnArg::Typed(pat_type) = &inputs[0] {
342 Ok(AnalyzedArgs {
343 args_type: (*pat_type.ty).clone(),
344 params: vec![format_ident!("args")],
345 stream: quote! {},
346 })
347 } else {
348 Err(syn::Error::new_spanned(
349 &inputs[0],
350 "self parameters are not supported in tool functions",
351 ))
352 }
353 }
354 _ => {
355 let mut attributes = Vec::new();
356
357 for arg in inputs {
358 if let FnArg::Typed(pat_type) = arg {
359 let pat = &pat_type.pat;
360 let ty = &pat_type.ty;
361 attributes.push(quote! {
362 #pat: #ty,
363 });
364 } else {
365 return Err(syn::Error::new_spanned(
366 arg,
367 "self parameters are not supported in tool functions",
368 ));
369 }
370 }
371
372 let arg_struct_name = format_ident!("{}Args", struct_name);
373
374 let new_type_gen = quote! {
375 #[derive(::schemars::JsonSchema, ::serde::Deserialize,::core::fmt::Debug)]
376 #fn_vis struct #arg_struct_name {
377 #(
378 #attributes
379 )*
380 }
381 };
382
383 let params = inputs
384 .iter()
385 .map(|arg| match arg {
386 FnArg::Typed(pat_type) => {
387 let pat = &pat_type.pat;
388 Ok(format_ident!("{}", quote! {#pat}.to_string()))
389 }
390 // A `self` receiver: point at it rather than panicking, so
391 // the user gets a diagnostic on the offending argument
392 // instead of "proc macro panicked".
393 FnArg::Receiver(receiver) => Err(syn::Error::new_spanned(
394 receiver,
395 "#[tool] cannot be applied to a method taking `self`; \
396 use a free function",
397 )),
398 })
399 .collect::<Result<Vec<_>, syn::Error>>()?;
400
401 Ok(AnalyzedArgs {
402 args_type: parse_quote! { #arg_struct_name },
403 params,
404 stream: new_type_gen,
405 })
406 }
407 }
408}