1#![warn(missing_docs)]
2use proc_macro::TokenStream;
29use proc_macro2::TokenStream as TokenStream2;
30use quote::{format_ident, quote};
31use syn::{
32 parse_macro_input, Attribute, Expr, ExprLit, FnArg, Ident, ItemFn, Lit, Meta, MetaNameValue,
33 Pat, PatType, Result, Signature, Type,
34};
35
36const PARAM_ATTR: &str = "param";
38
39#[proc_macro_attribute]
54pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
55 let func = parse_macro_input!(item as ItemFn);
56
57 let description = match parse_tool_attr(attr) {
59 Ok(d) => d,
60 Err(err) => return err.to_compile_error().into(),
61 };
62
63 match tool_impl(description, func) {
64 Ok(tokens) => tokens.into(),
65 Err(err) => err.to_compile_error().into(),
66 }
67}
68
69fn parse_tool_attr(attr: TokenStream) -> Result<String> {
71 if attr.is_empty() {
72 return Err(syn::Error::new(
73 proc_macro2::Span::call_site(),
74 "#[tool(description = \"...\")] is required",
75 ));
76 }
77
78 let meta: Meta = syn::parse(attr)?;
80 if let Meta::NameValue(MetaNameValue { path, value, .. }) = &meta {
81 if path.is_ident("description") {
82 if let Expr::Lit(ExprLit {
83 lit: Lit::Str(lit), ..
84 }) = value
85 {
86 return Ok(lit.value());
87 }
88 }
89 }
90
91 Err(syn::Error::new(
92 proc_macro2::Span::call_site(),
93 "expected #[tool(description = \"...\")]",
94 ))
95}
96
97fn tool_impl(description: String, mut func: ItemFn) -> Result<TokenStream2> {
98 let func_name_str = func.sig.ident.to_string();
100 let pascal = to_pascal_case(&func_name_str);
104 if !is_valid_ident_seed(&pascal) {
105 return Err(syn::Error::new_spanned(
106 &func.sig.ident,
107 format!(
108 "cannot derive `{pascal}Tool`/`{pascal}Input` from function name `{func_name_str}`: \
109 generated identifiers must start with an alphabetic or underscore character"
110 ),
111 ));
112 }
113 let tool_struct_name = format_ident!("{}Tool", pascal);
114 let input_struct_name = format_ident!("{}Input", pascal);
115 let func_name = func.sig.ident.clone();
116
117 if func.sig.asyncness.is_some() {
121 return Err(syn::Error::new_spanned(
122 &func.sig.ident,
123 "async tool functions are not supported by #[tool]: make the function synchronous \
124 (the derived Tool::invoke / BaseTool::run are already async)",
125 ));
126 }
127
128 let params = extract_params(&func.sig)?;
130 let field_names: Vec<Ident> = params.iter().map(|p| p.name.clone()).collect();
131
132 let output_type = match &func.sig.output {
134 syn::ReturnType::Default => quote! { () },
135 syn::ReturnType::Type(_, ty) => {
136 if let Some(inner) = extract_result_ok(&func.sig.output) {
138 quote! { #inner }
139 } else {
140 quote! { #ty }
141 }
142 }
143 };
144
145 let invoke_body = if return_type_is_tool_error(&func.sig.output) {
149 quote! { #func_name(#(#field_names),*) }
150 } else {
151 quote! { #func_name(#(#field_names),*).map_err(|e| ::lc_core::tools::ToolError::ExecutionFailed(e.to_string())) }
152 };
153
154 let input_fields = generate_input_fields(¶ms);
156
157 let input_field_attrs = generate_field_attrs(¶ms);
159
160 strip_param_attrs(&mut func);
163
164 let expanded = quote! {
166 #func
168
169 #[derive(Debug, Clone)]
171 pub struct #tool_struct_name;
172
173 impl ::std::default::Default for #tool_struct_name {
174 fn default() -> Self {
175 Self
176 }
177 }
178
179 impl #tool_struct_name {
180 pub fn new() -> Self {
181 Self
182 }
183 }
184
185 #[derive(serde::Deserialize, schemars::JsonSchema)]
187 pub struct #input_struct_name {
188 #(#input_field_attrs)*
189 #(#input_fields)*
190 }
191
192 #[::async_trait::async_trait]
194 impl ::lc_core::tools::Tool for #tool_struct_name {
195 type Input = #input_struct_name;
196 type Output = #output_type;
197
198 async fn invoke(&self, input: Self::Input) -> ::std::result::Result<Self::Output, ::lc_core::tools::ToolError> {
199 let #input_struct_name { #(#field_names),* } = input;
200 #invoke_body
201 }
202 }
203
204 #[::async_trait::async_trait]
206 impl ::lc_core::tools::BaseTool for #tool_struct_name {
207 fn name(&self) -> &str {
208 #func_name_str
209 }
210
211 fn description(&self) -> &str {
212 #description
213 }
214
215 async fn run(&self, input: ::std::string::String) -> ::std::result::Result<::std::string::String, ::lc_core::tools::ToolError> {
216 let parsed: #input_struct_name = ::serde_json::from_str(&input)
217 .map_err(|e| ::lc_core::tools::ToolError::InvalidInput(format!("JSON parse error: {}", e)))?;
218 let #input_struct_name { #(#field_names),* } = parsed;
219 let result = #func_name(#(#field_names),*)
220 .map_err(|e| ::lc_core::tools::ToolError::ExecutionFailed(e.to_string()))?;
221 let serialized = ::serde_json::to_string(&result)
224 .map_err(|e| ::lc_core::tools::ToolError::ExecutionFailed(format!("Failed to serialize tool output: {}", e)))?;
225 Ok(serialized)
226 }
227
228 fn args_schema(&self) -> ::std::option::Option<::serde_json::Value> {
229 use ::schemars::schema_for;
230 Some(
233 ::serde_json::to_value(schema_for!(#input_struct_name)).expect(
234 "[lc-tools-derive] internal error: generated Input schema failed to serialize \
235 (Input must derive schemars::JsonSchema)",
236 ),
237 )
238 }
239 }
240 };
241
242 Ok(expanded)
243}
244
245struct ParamInfo {
247 name: Ident,
248 ty: Type,
249 desc: Option<String>,
250}
251
252fn extract_params(sig: &Signature) -> Result<Vec<ParamInfo>> {
254 let mut params = Vec::new();
255
256 for arg in &sig.inputs {
257 if let FnArg::Receiver(_) = arg {
259 continue;
260 }
261
262 if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = arg {
263 let name = match pat.as_ref() {
264 Pat::Ident(ident) => ident.ident.clone(),
265 other => {
269 return Err(syn::Error::new_spanned(
270 other,
271 "tool parameters must be plain identifiers \
272 (ascription / tuple / wildcard patterns are not supported)",
273 ));
274 }
275 };
276
277 let desc = extract_param_desc(attrs)?;
279
280 params.push(ParamInfo {
281 name,
282 ty: (*(*ty)).clone(),
283 desc,
284 });
285 }
286 }
287
288 Ok(params)
289}
290
291fn result_generics(ret: &syn::ReturnType) -> Option<(Type, Type)> {
298 let syn::ReturnType::Type(_, ty) = ret else {
299 return None;
300 };
301 let Type::Path(type_path) = &**ty else {
302 return None;
303 };
304 let seg = type_path.path.segments.last()?;
305 if seg.ident != "Result" {
306 return None;
307 }
308 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
309 return None;
310 };
311 let mut generic = args.args.iter().filter_map(|a| match a {
312 syn::GenericArgument::Type(t) => Some(t),
313 _ => None,
314 });
315 Some((generic.next()?.clone(), generic.next()?.clone()))
316}
317
318fn extract_result_ok(ret: &syn::ReturnType) -> Option<Type> {
320 result_generics(ret).map(|(ok, _)| ok)
321}
322
323fn return_type_is_tool_error(ret: &syn::ReturnType) -> bool {
332 let Some((_, err)) = result_generics(ret) else {
333 return false;
334 };
335 let Type::Path(err_path) = err else {
336 return false;
337 };
338 let segs = err_path.path.segments;
339 let Some(last) = segs.last() else {
340 return false;
341 };
342 if last.ident != "ToolError" {
343 return false;
344 }
345 segs.len() == 1
347 || segs
348 .get(segs.len().saturating_sub(2))
349 .is_some_and(|s| s.ident == "tools")
350}
351
352fn is_valid_ident_seed(s: &str) -> bool {
354 match s.chars().next() {
355 Some(c) => c == '_' || c.is_ascii_alphabetic(),
356 None => false,
357 }
358}
359
360fn extract_param_desc(attrs: &[Attribute]) -> Result<Option<String>> {
365 for attr in attrs {
366 if attr.path().is_ident(PARAM_ATTR) {
367 let meta: Meta = attr.parse_args().map_err(|e| {
368 syn::Error::new_spanned(
369 attr,
370 format!("failed to parse `#[{PARAM_ATTR}(...)]`: {e}"),
371 )
372 })?;
373 if let Meta::NameValue(MetaNameValue { path, value, .. }) = &meta {
374 if path.is_ident("desc") {
375 if let Expr::Lit(ExprLit {
376 lit: Lit::Str(lit), ..
377 }) = value
378 {
379 return Ok(Some(lit.value()));
380 }
381 }
382 }
383 }
384 }
385 Ok(None)
386}
387
388fn generate_input_fields(params: &[ParamInfo]) -> Vec<TokenStream2> {
390 params
391 .iter()
392 .map(|p| {
393 let name = &p.name;
394 let ty = &p.ty;
395 quote! {
396 pub #name: #ty,
397 }
398 })
399 .collect()
400}
401
402fn generate_field_attrs(params: &[ParamInfo]) -> Vec<TokenStream2> {
409 params
410 .iter()
411 .map(|p| {
412 if let Some(desc) = &p.desc {
413 quote! {
414 #[doc = #desc]
415 }
416 } else {
417 quote! {}
418 }
419 })
420 .collect()
421}
422
423fn to_pascal_case(s: &str) -> String {
425 s.split('_')
426 .map(|word| {
427 let mut chars = word.chars();
428 match chars.next() {
429 None => String::new(),
430 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
431 }
432 })
433 .collect()
434}
435
436fn strip_param_attrs(func: &mut ItemFn) {
440 for arg in &mut func.sig.inputs {
441 if let FnArg::Typed(pat_type) = arg {
442 pat_type
443 .attrs
444 .retain(|attr| !attr.path().is_ident(PARAM_ATTR));
445 }
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use quote::quote;
453 use syn::ReturnType;
454
455 fn rt(src: &str) -> ReturnType {
456 syn::parse_str(src).unwrap()
457 }
458
459 fn ty_str(ret: &ReturnType) -> Option<String> {
460 extract_result_ok(ret).map(|t| quote! { #t }.to_string())
461 }
462
463 #[test]
465 fn result_generics_bare_and_qualified_agree() {
466 assert_eq!(ty_str(&rt("-> Result<f64, String>")), Some("f64".into()));
467 assert_eq!(
468 ty_str(&rt("-> std::result::Result<f64, String>")),
469 Some("f64".into())
470 );
471 assert_eq!(ty_str(&rt("-> f64")), None);
472 assert_eq!(ty_str(&rt("-> Result<f64>")), None); }
474
475 #[test]
478 fn return_type_is_tool_error_qualified_forms_only() {
479 assert!(return_type_is_tool_error(&rt("-> Result<String, ToolError>")));
480 assert!(return_type_is_tool_error(&rt("-> Result<String, lc_core::tools::ToolError>")));
481 assert!(return_type_is_tool_error(&rt("-> Result<String, tools::ToolError>")));
482 assert!(!return_type_is_tool_error(&rt("-> Result<String, MyToolError>")));
483 assert!(!return_type_is_tool_error(&rt("-> Result<String, other::ToolError>")));
484 assert!(!return_type_is_tool_error(&rt("-> Result<String, anyhow::Error>")));
485 assert!(!return_type_is_tool_error(&rt("-> String")));
486 }
487
488 #[test]
490 fn ident_seed_validation() {
491 assert!(is_valid_ident_seed("Calculator"));
492 assert!(is_valid_ident_seed("_private"));
493 assert!(!is_valid_ident_seed("9lives"));
494 assert!(!is_valid_ident_seed(""));
495 }
496}