1use proc_macro::TokenStream;
4use proc_macro2::Span;
5use quote::{ToTokens, quote};
6use std::collections::HashMap;
7use syn::parse_macro_input;
8
9#[proc_macro_attribute]
57pub fn tool(_attr: TokenStream, item: TokenStream) -> TokenStream {
58 let input = parse_macro_input!(item as syn::ItemFn);
59 let doc_comments = input.attrs.iter().filter_map(|attr| {
60 let meta = attr.meta.require_name_value().ok()?;
61 meta.path.is_ident("doc").then_some(())?;
62 let syn::Expr::Lit(expr_lit) = &meta.value else {
63 return None;
64 };
65 let syn::Lit::Str(lit_str) = &expr_lit.lit else {
66 return None;
67 };
68 Some(lit_str.value())
69 });
70 let mut pycomments = String::new();
71 let mut prev = None;
72 let mut description = String::new();
73 let mut attr_map = HashMap::<String, String>::new();
74 for mut line in doc_comments {
75 pycomments.push_str("#");
76 pycomments.push_str(&line);
77 pycomments.push_str("\n");
78 line = line.split_off(line.find(line.trim_start()).unwrap_or(0));
79 if let Some(i) = line.find(':') {
80 if let Some((param, desc)) = prev {
81 attr_map.insert(param, desc);
82 }
83
84 let mut desc = line.split_off(i);
85 desc = desc.split_off(1);
86 desc = desc.split_off(desc.find(desc.trim_start()).unwrap_or(0));
87
88 prev = Some((line, desc));
89 } else if let Some((_, desc)) = &mut prev {
90 desc.push('\n');
91 desc.push_str(&line);
92 } else {
93 description.push('\n');
94 description.push_str(&line);
95 }
96 }
97 if let Some((param, desc)) = prev {
98 attr_map.insert(param, desc);
99 }
100
101 let mut params = Vec::new();
102 let mut pyparams = Vec::new();
103 let mut required_params = Vec::new();
104 let mut args = Vec::new();
105 let mut call_stmts = Vec::new();
106 let mut state_arg = quote!();
107 let mut state_ty = quote!();
108 let (mut i, mut j) = (0, 0);
109 for arg in &input.sig.inputs {
110 if let syn::FnArg::Typed(syn::PatType { pat, ty, .. }) = arg {
111 if let syn::Type::Reference(syn::TypeReference { elem, .. }) = &**ty {
112 let ident = syn::Ident::new("state", Span::call_site());
113 state_arg = quote!(#ident : #ty,);
114 state_ty = quote!(type State = #elem;);
115 args.push(ident);
116 continue;
117 }
118 if let syn::Pat::Ident(pat_ident) = &**pat {
119 let ident = &pat_ident.ident;
120 let name = ident.to_string();
121 let name_ref = &name;
122 let index = if name.starts_with("_") {
123 j += 1;
124 (j - 1).to_string() + "_"
125 } else {
126 i += 1;
127 (i - 1).to_string()
128 };
129 let desc = attr_map.get(&name).cloned().unwrap_or_default();
130 let (schema, required) = jsonschema(ty.clone(), desc);
131 let pyschema = pythonschema(&ty);
132
133 args.push(ident.clone());
134 if required {
135 call_stmts.push(quote! {
136 let #ident : #ty = ::promptrs::serde_json::from_value(
137 arguments.remove(#name_ref).or_else(|| arguments.remove(#index)).ok_or(::promptrs::serde::de::Error::missing_field(#name_ref))?
138 )?;
139 });
140 } else {
141 call_stmts.push(quote! {
142 let #ident : #ty = ::promptrs::serde_json::from_value(
143 arguments.remove(#name_ref).or_else(|| arguments.remove(#index)).unwrap_or(::promptrs::serde_json::Value::Null)
144 )?;
145 });
146 }
147 if name.starts_with('_') {
148 continue;
149 }
150
151 params.push(format!(r#""{}": {}"#, name, schema));
152 pyparams.push(format!("{}: {}", name, pyschema));
153 if required {
154 required_params.push(name);
155 }
156 }
157 }
158 }
159 let arguments = format!(
160 r#"{{ "description": "{}", "properties": {{ {} }}, "required": ["{}"] }}"#,
161 description,
162 params.join(", "),
163 required_params.join(r#"", ""#)
164 );
165
166 let syn::ItemFn {
167 vis,
168 sig: syn::Signature {
169 ident,
170 inputs,
171 output,
172 generics,
173 ..
174 },
175 block,
176 ..
177 } = input;
178 let name = ident.to_string();
179 let pydef = format!("{}def {}({})", pycomments, name, pyparams.join(", "));
180 let output = quote! {
181 #[allow(non_camel_case_types)]
182 struct #ident;
183
184 impl #ident {
185 #vis fn run #generics (#inputs) #output {
186 #block
187 }
188 }
189
190 impl ::promptrs::Tool for #ident {
191 #state_ty
192
193 fn name(&self) -> &str {
194 #name
195 }
196
197 fn arguments(&self) -> &str {
198 #arguments
199 }
200
201 fn pydef(&self) -> &str {
202 #pydef
203 }
204
205 fn call(&self, #state_arg arguments: &::promptrs::Arguments) -> ::promptrs::serde_json::Result<String> {
206 let mut arguments = arguments.clone();
207 #(#call_stmts)*
208 let result = #ident::run(#(#args,)*);
209 Ok(::std::string::ToString::to_string(&result))
210 }
211 }
212 };
213
214 output.into()
215}
216
217fn jsonschema(mut ty: Box<syn::Type>, description: String) -> (String, bool) {
218 if let syn::Type::Reference(syn::TypeReference { elem, .. }) = &*ty {
219 ty = elem.clone();
220 }
221 let syn::Type::Path(syn::TypePath { path, .. }) = &*ty else {
222 return ("unknown".into(), true);
223 };
224 if let Some(syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
225 args,
226 ..
227 })) = path.segments.last().and_then(|l| {
228 l.ident
229 .to_string()
230 .as_str()
231 .eq("Option")
232 .then(|| &l.arguments)
233 }) {
234 if let Some(syn::GenericArgument::Type(ty)) = args.first() {
235 return (
236 format!(
237 r#"{}, "description": "{}" }}"#,
238 _jsonschema(ty),
239 description
240 ),
241 false,
242 );
243 }
244 }
245 (
246 format!(
247 r#"{}, "description": "{}" }}"#,
248 _jsonschema(&ty),
249 description
250 ),
251 true,
252 )
253}
254
255fn _jsonschema(ty: &syn::Type) -> String {
256 match ty {
257 syn::Type::Array(syn::TypeArray { elem, .. }) => {
258 format!(r#"{{ "type": "array", "items": {} }}"#, _jsonschema(&elem))
259 }
260 syn::Type::Reference(syn::TypeReference { elem, .. }) => _jsonschema(&elem),
261 syn::Type::Slice(syn::TypeSlice { elem, .. }) => {
262 let inner = _jsonschema(&elem);
263 if &inner == "str" {
264 return "string".into();
265 }
266 format!(r#"{{ "type": "array", "items": {} }}"#, inner)
267 }
268 syn::Type::Path(syn::TypePath { path, .. }) => {
269 let (ident, arguments) = match path.segments.last() {
270 Some(syn::PathSegment {
271 ident, arguments, ..
272 }) => (ident.into_token_stream().to_string(), arguments),
273 None => return "".into(),
274 };
275 let ty = match ident.as_str() {
276 "String" | "str" | "char" => "string".into(),
277 "f32" | "f64" => "number".into(),
278 "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" => "integer".into(),
279 "bool" => "boolean".into(),
280 "Vec" => {
281 let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
282 args,
283 ..
284 }) = arguments
285 else {
286 return "".into();
287 };
288 let Some(syn::GenericArgument::Type(ty)) = args.first() else {
289 return "".into();
290 };
291 format!(r#"{{ "type": "array", "items": {} }}"#, _jsonschema(&ty))
292 }
293 _ => "".into(),
294 };
295 format!(r#"{{ "type": "{}""#, ty)
296 }
297 _ => "unknown".into(),
298 }
299}
300
301fn pythonschema(ty: &syn::Type) -> String {
302 match ty {
303 syn::Type::Array(syn::TypeArray { elem, .. }) => {
304 format!("list[{}]", pythonschema(&elem))
305 }
306 syn::Type::Reference(syn::TypeReference { elem, .. }) => pythonschema(&elem),
307 syn::Type::Slice(syn::TypeSlice { elem, .. }) => {
308 let inner = pythonschema(&elem);
309 if &inner == "str" {
310 return "str".into();
311 }
312 format!("list[{}]", pythonschema(&elem))
313 }
314 syn::Type::Path(syn::TypePath { path, .. }) => {
315 let (ident, arguments) = match path.segments.last() {
316 Some(syn::PathSegment {
317 ident, arguments, ..
318 }) => (ident.into_token_stream().to_string(), arguments),
319 None => return "".into(),
320 };
321 match ident.as_str() {
322 "String" | "str" | "char" => "str".into(),
323 "f32" | "f64" => "float".into(),
324 "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" => "int".into(),
325 "bool" => "bool".into(),
326 "Option" => {
327 let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
328 args,
329 ..
330 }) = arguments
331 else {
332 return "".into();
333 };
334 let Some(syn::GenericArgument::Type(ty)) = args.first() else {
335 return "".into();
336 };
337 format!("Optional[{}]", pythonschema(ty))
338 }
339 "Vec" => {
340 let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
341 args,
342 ..
343 }) = arguments
344 else {
345 return "".into();
346 };
347 let Some(syn::GenericArgument::Type(ty)) = args.first() else {
348 return "".into();
349 };
350 format!("list[{}]", pythonschema(ty))
351 }
352 _ => "".into(),
353 }
354 }
355 _ => "unknown".into(),
356 }
357}