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}