Skip to main content

aisdk_macros/
lib.rs

1#![deny(missing_docs)]
2//! Macros for the `aisdk` library.
3
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::parse::Parser;
7use syn::{
8    Expr, ExprLit, FnArg, ItemFn, Lit, Meta, MetaNameValue, Pat, Token, parse_macro_input,
9    punctuated::Punctuated,
10};
11
12#[proc_macro_attribute]
13/// Constructs a tool from a function defnition. A tool has a name, a description,
14/// an input and a body. all three components are infered from a standard rust
15/// function. The name is the defined name of the function,
16/// The description is infered from the doc comments of the function, The input
17/// infered from the function arguments.
18///
19/// # Example
20///
21/// ```rust,no_run
22/// use aisdk::macros::tool;
23/// use aisdk::core::tools::Tool;
24///
25/// #[tool]
26/// /// Returns the username
27/// fn get_username(id: String) -> Tool {
28///     // Your code here
29///     Ok(format!("user_{}", id))
30/// }
31/// ```
32///
33/// - `get_username` becomes the name of the tool
34/// - `"Returns the username"` becomes the description of the tool
35/// - `id: String` becomes the input of the tool. converted to `{"id": "string"}`
36///   as json schema
37///
38/// The function should return a `Result<String, String>` eventhough the return statement
39/// returns a `Tool` object. This is because the macro will automatically convert the
40/// function into a `Tool` object and return it. You should return what the model can
41/// understand as a `String`.
42///
43/// In the event that the model refuses to send an argument, the default implementation
44/// will be used. this works perfectly for arguments that are `Option`s. Make sure to
45/// use `Option` types for arguments that are optional or implement a default for those
46/// that are not and handle those defaults accordingly in the tool body.
47///
48/// You can override name and description using the macro arguments `name` and `desc`.
49///
50/// # Example with overrides
51/// ```rust,no_run
52/// use aisdk::macros::tool;
53/// use aisdk::core::tools::Tool;
54///
55///     #[tool(
56///         name = "the-name-for-this-tool",
57///         desc = "the-description-for-this-tool"
58///     )]
59///     fn get_username(id: String) -> Tool {
60///         // Your code here
61///         Ok(format!("user_{}", id))
62///     }
63/// ```
64pub fn tool(_attr: TokenStream, item: TokenStream) -> TokenStream {
65    let input_fn = parse_macro_input!(item as ItemFn);
66    let fn_name = &input_fn.sig.ident;
67    let vis = &input_fn.vis;
68    let return_type = &input_fn.sig.output;
69    let block = &input_fn.block;
70    let inputs = &input_fn.sig.inputs;
71    let attrs = &input_fn.attrs;
72    let args_parser = Punctuated::<MetaNameValue, Token![,]>::parse_terminated;
73    let args = args_parser.parse(_attr);
74
75    let (name_arg, description_arg) = if let Ok(args) = args {
76        let mut name: Option<String> = None;
77        let mut description: Option<String> = None;
78
79        for arg in args {
80            if arg.path.is_ident("desc")
81                && let Expr::Lit(lit) = &arg.value
82                && let Lit::Str(str_lit) = &lit.lit
83            {
84                description = Some(str_lit.value());
85            } else if arg.path.is_ident("name")
86                && let Expr::Lit(lit) = &arg.value
87                && let Lit::Str(str_lit) = &lit.lit
88            {
89                name = Some(str_lit.value());
90            }
91        }
92
93        (name, description)
94    } else {
95        (None, None)
96    };
97
98    let description = if let Some(desc) = description_arg {
99        desc
100    } else {
101        // Extract doc comments
102        let doc_comments: Vec<String> = attrs
103            .iter()
104            .filter_map(|attr| {
105                if attr.path().is_ident("doc") {
106                    if let Meta::NameValue(meta_name_value) = &attr.meta {
107                        if let Expr::Lit(ExprLit {
108                            lit: Lit::Str(lit_str),
109                            ..
110                        }) = &meta_name_value.value
111                        {
112                            let doc = lit_str.value();
113                            Some(doc)
114                        } else {
115                            None
116                        }
117                    } else {
118                        None
119                    }
120                } else {
121                    None
122                }
123            })
124            .collect();
125
126        doc_comments.join("\n")
127    };
128
129    let name = if let Some(name) = name_arg {
130        name
131    } else {
132        fn_name.to_string()
133    };
134
135    let binding_tokens: Vec<_> = inputs
136        .iter()
137        .filter_map(|arg| {
138            if let FnArg::Typed(pat_type) = arg {
139                if let Pat::Ident(pat_ident) = &*pat_type.pat {
140                    let ident = &pat_ident.ident;
141                    let ty = &*pat_type.ty;
142                    let ident_str = ident.to_string();
143                    Some(quote! {
144                        let #ident: #ty = ::aisdk::__private::serde_json::from_value(
145                            inp.as_object()
146                                .unwrap()
147                                .get(#ident_str)
148                                .unwrap()
149                                .clone()
150                        ).unwrap_or_default();  // use default value if model doesn't send arg
151                    })
152                } else {
153                    None
154                }
155            } else {
156                None
157            }
158        })
159        .collect();
160
161    // Generate the struct definition
162    let struct_fields = inputs.iter().filter_map(|arg| {
163        if let FnArg::Typed(pat_type) = arg {
164            if let Pat::Ident(pat_ident) = &*pat_type.pat {
165                let ident = &pat_ident.ident;
166                let ty = &*pat_type.ty;
167                Some(quote! { #ident: #ty })
168            } else {
169                None
170            }
171        } else {
172            None
173        }
174    });
175
176    let expanded = quote! {
177        #vis fn #fn_name() #return_type  {
178            // use schemars::{schema_for, JsonSchema, Schema};
179            use std::collections::HashMap;
180            use ::aisdk::__private::schemars::{schema_for, JsonSchema, Schema};
181
182            #[derive(::aisdk::__private::schemars::JsonSchema, Debug)]
183            #[schemars(crate = "::aisdk::__private::schemars")]
184            #[allow(dead_code)]
185            //#[schemars(deny_unknown_fields)]
186            struct Function {
187                // Please add struct fields here
188                #(#struct_fields),*
189            }
190
191            let input_schema = schema_for!(Function);
192            // End
193
194            let mut tool = ::aisdk::core::tools::Tool::builder()
195                .name(#name.to_string())
196                .description(#description.to_string())
197                .input_schema(input_schema)
198                .execute(::aisdk::core::tools::ToolExecute::new(Box::new(|inp| -> std::result::Result<String, String> {
199                    #(#binding_tokens)*
200                    #block
201                })));
202
203            tool.build().expect("Failed to build tool")
204        }
205    };
206
207    TokenStream::from(expanded)
208}