1extern crate proc_macro;
2
3use convert_case::{Case, Casing};
4use proc_macro::TokenStream;
5use quote::{format_ident, quote};
6use std::{collections::HashMap, ops::Deref};
7use syn::{
8 Attribute, DeriveInput, Expr, ExprLit, Ident, Lit, Meta, PathArguments, ReturnType, Token,
9 Type,
10 parse::{Parse, ParseStream},
11 parse_macro_input,
12 punctuated::Punctuated,
13};
14
15mod basic;
16mod custom;
17mod embed;
18
19pub(crate) const EMBED: &str = "embed";
20
21pub(crate) fn rig_core_path() -> proc_macro2::TokenStream {
22 match proc_macro_crate::crate_name("rig-core") {
23 Ok(proc_macro_crate::FoundCrate::Itself) => quote!(crate),
24 Ok(proc_macro_crate::FoundCrate::Name(name)) => {
25 let ident = format_ident!("{name}");
26 quote!(::#ident)
27 }
28 Err(_) => match proc_macro_crate::crate_name("rig") {
29 Ok(proc_macro_crate::FoundCrate::Itself) => quote!(crate),
30 Ok(proc_macro_crate::FoundCrate::Name(name)) => {
31 let ident = format_ident!("{name}");
32 quote!(::#ident)
33 }
34 Err(_) => quote!(::rig_core),
35 },
36 }
37}
38
39#[proc_macro_derive(Embed, attributes(embed))]
57pub fn derive_embedding_trait(item: TokenStream) -> TokenStream {
58 let mut input = parse_macro_input!(item as DeriveInput);
59
60 embed::expand_derive_embedding(&mut input)
61 .unwrap_or_else(syn::Error::into_compile_error)
62 .into()
63}
64
65struct MacroArgs {
66 name: Option<String>,
67 description: Option<String>,
68 param_descriptions: HashMap<String, String>,
69 required: Option<Vec<String>>,
70}
71
72fn parse_string_literal(expr: &Expr, field_name: &str) -> syn::Result<String> {
73 match expr {
74 Expr::Lit(ExprLit {
75 lit: Lit::Str(lit_str),
76 ..
77 }) => Ok(lit_str.value()),
78 _ => Err(syn::Error::new_spanned(
79 expr,
80 format!("`{field_name}` must be a string literal"),
81 )),
82 }
83}
84
85fn validate_explicit_tool_name(name: &str, expr: &Expr) -> syn::Result<()> {
86 if name.is_empty() || name.len() > 64 {
87 return Err(syn::Error::new_spanned(
88 expr,
89 "`name` must be between 1 and 64 characters long",
90 ));
91 }
92
93 let mut chars = name.chars();
94 let Some(first_char) = chars.next() else {
95 return Err(syn::Error::new_spanned(
96 expr,
97 "`name` must be between 1 and 64 characters long",
98 ));
99 };
100
101 if !first_char.is_ascii_alphabetic() && first_char != '_' {
102 return Err(syn::Error::new_spanned(
103 expr,
104 "`name` must start with an ASCII letter or underscore",
105 ));
106 }
107
108 if chars.any(|ch| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-') {
109 return Err(syn::Error::new_spanned(
110 expr,
111 "`name` may only contain ASCII letters, digits, underscores, or hyphens",
112 ));
113 }
114
115 Ok(())
116}
117
118impl Parse for MacroArgs {
119 fn parse(input: ParseStream) -> syn::Result<Self> {
120 let mut name = None;
121 let mut description = None;
122 let mut param_descriptions = HashMap::new();
123 let mut required = None;
124
125 if input.is_empty() {
127 return Ok(MacroArgs {
128 name,
129 description,
130 param_descriptions,
131 required,
132 });
133 }
134
135 let meta_list: Punctuated<Meta, Token![,]> = Punctuated::parse_terminated(input)?;
136
137 for meta in meta_list {
138 match meta {
139 Meta::NameValue(nv) => {
140 let ident = nv.path.get_ident().ok_or_else(|| {
141 syn::Error::new_spanned(
142 &nv.path,
143 "unsupported top-level #[rig_tool] argument",
144 )
145 })?;
146
147 match ident.to_string().as_str() {
148 "name" => {
149 let parsed_name = parse_string_literal(&nv.value, "name")?;
150 validate_explicit_tool_name(&parsed_name, &nv.value)?;
151 name = Some(parsed_name);
152 }
153 "description" => {
154 description = Some(parse_string_literal(&nv.value, "description")?);
155 }
156 _ => {
157 return Err(syn::Error::new_spanned(
158 &nv.path,
159 format!("unsupported top-level #[rig_tool] argument `{}`", ident),
160 ));
161 }
162 }
163 }
164 Meta::List(list) => {
165 let ident = list.path.get_ident().ok_or_else(|| {
166 syn::Error::new_spanned(
167 &list.path,
168 "unsupported top-level #[rig_tool] argument",
169 )
170 })?;
171
172 match ident.to_string().as_str() {
173 "params" => {
174 let nested: Punctuated<Meta, Token![,]> =
175 list.parse_args_with(Punctuated::parse_terminated)?;
176
177 for meta in nested {
178 if let Meta::NameValue(nv) = meta
179 && let Expr::Lit(ExprLit {
180 lit: Lit::Str(lit_str),
181 ..
182 }) = nv.value
183 {
184 let Some(param_ident) = nv.path.get_ident() else {
185 return Err(syn::Error::new_spanned(
186 &nv.path,
187 "parameter descriptions must use identifier keys",
188 ));
189 };
190 let param_name = param_ident.to_string();
191 param_descriptions.insert(param_name, lit_str.value());
192 }
193 }
194 }
195 "required" => {
196 let required_variables: Punctuated<Ident, Token![,]> =
197 list.parse_args_with(Punctuated::parse_terminated)?;
198
199 required = Some(
200 required_variables
201 .into_iter()
202 .map(|x| x.to_string())
203 .collect(),
204 );
205 }
206 _ => {
207 return Err(syn::Error::new_spanned(
208 &list.path,
209 format!("unsupported top-level #[rig_tool] argument `{}`", ident),
210 ));
211 }
212 }
213 }
214 Meta::Path(path) => {
215 let message = if let Some(ident) = path.get_ident() {
216 format!("unsupported top-level #[rig_tool] argument `{ident}`")
217 } else {
218 "unsupported top-level #[rig_tool] argument".to_string()
219 };
220
221 return Err(syn::Error::new_spanned(path, message));
222 }
223 }
224 }
225
226 Ok(MacroArgs {
227 name,
228 description,
229 param_descriptions,
230 required,
231 })
232 }
233}
234
235fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
237 let lines: Vec<String> = attrs
238 .iter()
239 .filter_map(|attr| {
240 if !attr.path().is_ident("doc") {
241 return None;
242 }
243 if let Meta::NameValue(nv) = &attr.meta
244 && let Expr::Lit(ExprLit {
245 lit: Lit::Str(s), ..
246 }) = &nv.value
247 {
248 return Some(s.value());
249 }
250 None
251 })
252 .collect();
253
254 if lines.is_empty() {
255 return None;
256 }
257
258 Some(
259 lines
260 .iter()
261 .map(|l| l.strip_prefix(' ').unwrap_or(l))
262 .collect::<Vec<_>>()
263 .join("\n")
264 .trim()
265 .to_string(),
266 )
267}
268
269fn is_option_type(ty: &Type) -> bool {
271 if let Type::Path(type_path) = ty
272 && let Some(segment) = type_path.path.segments.last()
273 {
274 return segment.ident == "Option";
275 }
276 false
277}
278
279fn result_type_tokens(
280 return_type: &ReturnType,
281) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
282 let ReturnType::Type(_, ty) = return_type else {
283 return Err(syn::Error::new_spanned(
284 return_type,
285 "function must have a return type of Result<T, E>",
286 ));
287 };
288
289 let Type::Path(type_path) = ty.deref() else {
290 return Err(syn::Error::new_spanned(
291 ty,
292 "return type must be Result<T, E>",
293 ));
294 };
295
296 let Some(last_segment) = type_path.path.segments.last() else {
297 return Err(syn::Error::new_spanned(
298 &type_path.path,
299 "return type must be Result<T, E>",
300 ));
301 };
302
303 if last_segment.ident != "Result" {
304 return Err(syn::Error::new_spanned(
305 &last_segment.ident,
306 "return type must be Result<T, E>",
307 ));
308 }
309
310 let PathArguments::AngleBracketed(args) = &last_segment.arguments else {
311 return Err(syn::Error::new_spanned(
312 &last_segment.arguments,
313 "expected angle-bracketed type parameters for Result<T, E>",
314 ));
315 };
316
317 let mut generic_args = args.args.iter();
318 let Some(output) = generic_args.next() else {
319 return Err(syn::Error::new_spanned(
320 &args.args,
321 "expected Result<T, E> with exactly two type parameters",
322 ));
323 };
324 let Some(error) = generic_args.next() else {
325 return Err(syn::Error::new_spanned(
326 &args.args,
327 "expected Result<T, E> with exactly two type parameters",
328 ));
329 };
330
331 if generic_args.next().is_some() {
332 return Err(syn::Error::new_spanned(
333 &args.args,
334 "expected Result<T, E> with exactly two type parameters",
335 ));
336 }
337
338 Ok((quote!(#output), quote!(#error)))
339}
340
341#[proc_macro_attribute]
405pub fn rig_tool(args: TokenStream, input: TokenStream) -> TokenStream {
406 let args = parse_macro_input!(args as MacroArgs);
407 let input_fn = parse_macro_input!(input as syn::ItemFn);
408
409 let fn_name = &input_fn.sig.ident;
411 let fn_name_str = fn_name.to_string();
412 let tool_name = args.name.clone().unwrap_or_else(|| fn_name_str.clone());
413 let vis = &input_fn.vis;
414 let is_async = input_fn.sig.asyncness.is_some();
415
416 let cleaned_fn = {
419 let mut f = input_fn.clone();
420 for arg in f.sig.inputs.iter_mut() {
421 if let syn::FnArg::Typed(pat_type) = arg {
422 pat_type.attrs.retain(|a| !a.path().is_ident("doc"));
423 }
424 }
425 f
426 };
427
428 let return_type = &input_fn.sig.output;
430 let (output_type, error_type) = match result_type_tokens(return_type) {
431 Ok(types) => types,
432 Err(error) => return error.into_compile_error().into(),
433 };
434
435 let struct_name = format_ident!("{}", { fn_name_str.to_case(Case::Pascal) });
437
438 let fn_doc = extract_doc_comment(&input_fn.attrs);
440 let tool_description = match args.description {
441 Some(desc) => quote! { #desc.to_string() },
442 None => match fn_doc {
443 Some(doc) => quote! { #doc.to_string() },
444 None => quote! { format!("Function to {}", Self::NAME) },
445 },
446 };
447
448 let mut param_names = Vec::new();
450 let mut field_tokens = Vec::new();
451
452 for arg in input_fn.sig.inputs.iter() {
453 if let syn::FnArg::Typed(pat_type) = arg
454 && let syn::Pat::Ident(param_ident) = &*pat_type.pat
455 {
456 let param_name = ¶m_ident.ident;
457 let param_name_str = param_name.to_string();
458 let ty = &pat_type.ty;
459
460 let field_doc_attr =
463 if let Some(explicit) = args.param_descriptions.get(¶m_name_str) {
464 quote! { #[schemars(description = #explicit)] }
466 } else if let Some(doc) = extract_doc_comment(&pat_type.attrs) {
467 quote! { #[doc = #doc] }
469 } else {
470 let default_desc = format!("Parameter {param_name_str}");
472 quote! { #[schemars(description = #default_desc)] }
473 };
474
475 let serde_default = if is_option_type(ty) {
477 quote! { #[serde(default)] }
478 } else {
479 quote! {}
480 };
481
482 field_tokens.push(quote! {
483 #field_doc_attr
484 #serde_default
485 #vis #param_name: #ty
486 });
487
488 param_names.push(param_name);
489 }
490 }
491
492 let required_args: Vec<String> = args
494 .required
495 .unwrap_or_else(|| param_names.iter().map(|n| n.to_string()).collect());
496
497 let params_struct_name = format_ident!("{}Parameters", struct_name);
498 let static_name = format_ident!("{}", fn_name_str.to_uppercase());
499
500 let call_impl = if is_async {
502 quote! {
503 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
504 #fn_name(#(args.#param_names,)*).await
505 }
506 }
507 } else {
508 quote! {
509 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
510 #fn_name(#(args.#param_names,)*)
511 }
512 }
513 };
514
515 let rig_core = rig_core_path();
516 let schemars_crate = format!("{}::schemars", rig_core.to_string().replace(' ', ""));
517 let expanded = quote! {
518 #[derive(serde::Deserialize, #rig_core::schemars::JsonSchema)]
519 #[schemars(crate = #schemars_crate)]
520 #vis struct #params_struct_name {
521 #(#field_tokens,)*
522 }
523
524 #cleaned_fn
525
526 #[derive(Default)]
527 #vis struct #struct_name;
528
529 impl #rig_core::tool::Tool for #struct_name {
530 const NAME: &'static str = #tool_name;
531
532 type Args = #params_struct_name;
533 type Output = #output_type;
534 type Error = #error_type;
535
536 fn name(&self) -> String {
537 #tool_name.to_string()
538 }
539
540 fn description(&self) -> String {
541 #tool_description.to_string()
542 }
543
544 fn parameters(&self) -> serde_json::Value {
545 let mut schema = serde_json::to_value(
546 #rig_core::schemars::schema_for!(#params_struct_name)
547 ).expect("schema serialization");
548 schema["required"] = serde_json::json!([#(#required_args),*]);
549 schema
550 }
551
552 #call_impl
553 }
554
555 #vis static #static_name: #struct_name = #struct_name;
556 };
557
558 TokenStream::from(expanded)
559}