1use proc_macro::TokenStream;
2use proc_macro_crate::{FoundCrate, crate_name};
3use quote::quote;
4use regex::Regex;
5use syn::{
6 Data, DeriveInput, Meta, Token,
7 parse::{Parse, ParseStream},
8 parse_macro_input,
9 punctuated::Punctuated,
10};
11
12fn parse_template_placeholders_with_mode(template: &str) -> Vec<(String, Option<String>)> {
15 let mut placeholders = Vec::new();
16 let mut seen_fields = std::collections::HashSet::new();
17
18 let mode_pattern = Regex::new(r"\{\{\s*(\w+)\s*:\s*(\w+)\s*\}\}").unwrap();
20 for cap in mode_pattern.captures_iter(template) {
21 let field_name = cap[1].to_string();
22 let mode = cap[2].to_string();
23 placeholders.push((field_name.clone(), Some(mode)));
24 seen_fields.insert(field_name);
25 }
26
27 let standard_pattern = Regex::new(r"\{\{\s*(\w+)\s*\}\}").unwrap();
29 for cap in standard_pattern.captures_iter(template) {
30 let field_name = cap[1].to_string();
31 if !seen_fields.contains(&field_name) {
33 placeholders.push((field_name, None));
34 }
35 }
36
37 placeholders
38}
39
40fn extract_doc_comments(attrs: &[syn::Attribute]) -> String {
42 attrs
43 .iter()
44 .filter_map(|attr| {
45 if attr.path().is_ident("doc")
46 && let syn::Meta::NameValue(meta_name_value) = &attr.meta
47 && let syn::Expr::Lit(syn::ExprLit {
48 lit: syn::Lit::Str(lit_str),
49 ..
50 }) = &meta_name_value.value
51 {
52 return Some(lit_str.value());
53 }
54 None
55 })
56 .map(|s| s.trim().to_string())
57 .collect::<Vec<_>>()
58 .join(" ")
59}
60
61fn generate_example_only_parts(
63 fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
64 has_default: bool,
65 crate_path: &proc_macro2::TokenStream,
66) -> proc_macro2::TokenStream {
67 let mut field_values = Vec::new();
68
69 for field in fields.iter() {
70 let field_name = field.ident.as_ref().unwrap();
71 let field_name_str = field_name.to_string();
72 let attrs = parse_field_prompt_attrs(&field.attrs);
73
74 if attrs.skip {
76 continue;
77 }
78
79 if let Some(example) = attrs.example {
81 field_values.push(quote! {
83 json_obj.insert(#field_name_str.to_string(), serde_json::Value::String(#example.to_string()));
84 });
85 } else if has_default {
86 field_values.push(quote! {
88 let default_value = serde_json::to_value(&default_instance.#field_name)
89 .unwrap_or(serde_json::Value::Null);
90 json_obj.insert(#field_name_str.to_string(), default_value);
91 });
92 } else {
93 field_values.push(quote! {
95 let value = serde_json::to_value(&self.#field_name)
96 .unwrap_or(serde_json::Value::Null);
97 json_obj.insert(#field_name_str.to_string(), value);
98 });
99 }
100 }
101
102 if has_default {
103 quote! {
104 {
105 let default_instance = Self::default();
106 let mut json_obj = serde_json::Map::new();
107 #(#field_values)*
108 let json_value = serde_json::Value::Object(json_obj);
109 let json_str = serde_json::to_string_pretty(&json_value)
110 .unwrap_or_else(|_| "{}".to_string());
111 vec![#crate_path::prompt::PromptPart::Text(json_str)]
112 }
113 }
114 } else {
115 quote! {
116 {
117 let mut json_obj = serde_json::Map::new();
118 #(#field_values)*
119 let json_value = serde_json::Value::Object(json_obj);
120 let json_str = serde_json::to_string_pretty(&json_value)
121 .unwrap_or_else(|_| "{}".to_string());
122 vec![#crate_path::prompt::PromptPart::Text(json_str)]
123 }
124 }
125 }
126}
127
128fn generate_schema_only_parts(
130 struct_name: &str,
131 struct_docs: &str,
132 fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
133 crate_path: &proc_macro2::TokenStream,
134) -> proc_macro2::TokenStream {
135 let mut field_schema_parts = vec![];
136
137 for (i, field) in fields.iter().enumerate() {
139 let field_name = field.ident.as_ref().unwrap();
140 let field_name_str = field_name.to_string();
141 let attrs = parse_field_prompt_attrs(&field.attrs);
142
143 if attrs.skip {
145 continue;
146 }
147
148 let field_docs = extract_doc_comments(&field.attrs);
150
151 let (is_vec, inner_type) = extract_vec_inner_type(&field.ty);
153
154 let remaining_fields = fields
156 .iter()
157 .skip(i + 1)
158 .filter(|f| {
159 let attrs = parse_field_prompt_attrs(&f.attrs);
160 !attrs.skip
161 })
162 .count();
163 let comma = if remaining_fields > 0 { "," } else { "" };
164
165 if is_vec {
166 let comment = if !field_docs.is_empty() {
168 format!(", // {}", field_docs)
169 } else {
170 String::new()
171 };
172
173 field_schema_parts.push(quote! {
174 {
175 let inner_schema = <#inner_type as #crate_path::prompt::ToPrompt>::prompt_schema();
176 if inner_schema.is_empty() {
177 format!(" \"{}\": \"{}[]\"{}{}", #field_name_str, stringify!(#inner_type).to_lowercase(), #comment, #comma)
179 } else {
180 let inner_lines: Vec<&str> = inner_schema.lines()
182 .skip_while(|line| line.starts_with("###") || line.trim() == "{")
183 .take_while(|line| line.trim() != "}")
184 .collect();
185 let inner_content = inner_lines.join("\n");
186 format!(" \"{}\": [\n {{\n{}\n }}\n ]{}{}",
187 #field_name_str,
188 inner_content.lines()
189 .map(|line| format!(" {}", line))
190 .collect::<Vec<_>>()
191 .join("\n"),
192 #comment,
193 #comma
194 )
195 }
196 }
197 });
198 } else {
199 let field_type = &field.ty;
201 let is_primitive = is_primitive_type(field_type);
202
203 if !is_primitive {
204 let comment = if !field_docs.is_empty() {
206 format!(", // {}", field_docs)
207 } else {
208 String::new()
209 };
210
211 field_schema_parts.push(quote! {
212 {
213 let nested_schema = <#field_type as #crate_path::prompt::ToPrompt>::prompt_schema();
214 if nested_schema.is_empty() {
215 let type_str = stringify!(#field_type).to_lowercase();
217 format!(" \"{}\": \"{}\"{}{}", #field_name_str, type_str, #comment, #comma)
218 } else {
219 let nested_lines: Vec<&str> = nested_schema.lines()
221 .skip_while(|line| line.starts_with("###") || line.trim() == "{")
222 .take_while(|line| line.trim() != "}")
223 .collect();
224
225 if nested_lines.is_empty() {
226 let type_str = stringify!(#field_type).to_lowercase();
228 format!(" \"{}\": \"{}\"{}{}", #field_name_str, type_str, #comment, #comma)
229 } else {
230 let indented_content = nested_lines.iter()
232 .map(|line| format!(" {}", line))
233 .collect::<Vec<_>>()
234 .join("\n");
235 format!(" \"{}\": {{\n{}\n }}{}{}", #field_name_str, indented_content, #comment, #comma)
236 }
237 }
238 }
239 });
240 } else {
241 let type_str = format_type_for_schema(&field.ty);
243 let comment = if !field_docs.is_empty() {
244 format!(", // {}", field_docs)
245 } else {
246 String::new()
247 };
248
249 field_schema_parts.push(quote! {
250 format!(" \"{}\": \"{}\"{}{}", #field_name_str, #type_str, #comment, #comma)
251 });
252 }
253 }
254 }
255
256 let header = if !struct_docs.is_empty() {
258 format!("### Schema for `{}`\n{}", struct_name, struct_docs)
259 } else {
260 format!("### Schema for `{}`", struct_name)
261 };
262
263 quote! {
264 {
265 let mut lines = vec![#header.to_string(), "{".to_string()];
266 #(lines.push(#field_schema_parts);)*
267 lines.push("}".to_string());
268 vec![#crate_path::prompt::PromptPart::Text(lines.join("\n"))]
269 }
270 }
271}
272
273fn extract_vec_inner_type(ty: &syn::Type) -> (bool, Option<&syn::Type>) {
275 if let syn::Type::Path(type_path) = ty
276 && let Some(last_segment) = type_path.path.segments.last()
277 && last_segment.ident == "Vec"
278 && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
279 && let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
280 {
281 return (true, Some(inner_type));
282 }
283 (false, None)
284}
285
286fn is_primitive_type(ty: &syn::Type) -> bool {
288 if let syn::Type::Path(type_path) = ty
289 && let Some(last_segment) = type_path.path.segments.last()
290 {
291 let type_name = last_segment.ident.to_string();
292 matches!(
293 type_name.as_str(),
294 "String"
295 | "str"
296 | "i8"
297 | "i16"
298 | "i32"
299 | "i64"
300 | "i128"
301 | "isize"
302 | "u8"
303 | "u16"
304 | "u32"
305 | "u64"
306 | "u128"
307 | "usize"
308 | "f32"
309 | "f64"
310 | "bool"
311 | "Vec"
312 | "Option"
313 | "HashMap"
314 | "BTreeMap"
315 | "HashSet"
316 | "BTreeSet"
317 )
318 } else {
319 true
321 }
322}
323
324fn format_type_for_schema(ty: &syn::Type) -> String {
326 match ty {
328 syn::Type::Path(type_path) => {
329 let path = &type_path.path;
330 if let Some(last_segment) = path.segments.last() {
331 let type_name = last_segment.ident.to_string();
332
333 if type_name == "Option"
335 && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
336 && let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
337 {
338 return format!("{} | null", format_type_for_schema(inner_type));
339 }
340
341 match type_name.as_str() {
343 "String" | "str" => "string".to_string(),
344 "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32"
345 | "u64" | "u128" | "usize" => "number".to_string(),
346 "f32" | "f64" => "number".to_string(),
347 "bool" => "boolean".to_string(),
348 "Vec" => {
349 if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
350 && let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
351 {
352 return format!("{}[]", format_type_for_schema(inner_type));
353 }
354 "array".to_string()
355 }
356 _ => type_name.to_lowercase(),
357 }
358 } else {
359 "unknown".to_string()
360 }
361 }
362 _ => "unknown".to_string(),
363 }
364}
365
366enum PromptAttribute {
368 Skip,
369 Description(String),
370 None,
371}
372
373fn parse_prompt_attribute(attrs: &[syn::Attribute]) -> PromptAttribute {
375 for attr in attrs {
376 if attr.path().is_ident("prompt") {
377 if let Ok(meta_list) = attr.meta.require_list() {
379 let tokens = &meta_list.tokens;
380 let tokens_str = tokens.to_string();
381 if tokens_str == "skip" {
382 return PromptAttribute::Skip;
383 }
384 }
385
386 if let Ok(lit_str) = attr.parse_args::<syn::LitStr>() {
388 return PromptAttribute::Description(lit_str.value());
389 }
390 }
391 }
392 PromptAttribute::None
393}
394
395#[derive(Debug, Default)]
397struct FieldPromptAttrs {
398 skip: bool,
399 rename: Option<String>,
400 format_with: Option<String>,
401 image: bool,
402 example: Option<String>,
403}
404
405fn parse_field_prompt_attrs(attrs: &[syn::Attribute]) -> FieldPromptAttrs {
407 let mut result = FieldPromptAttrs::default();
408
409 for attr in attrs {
410 if attr.path().is_ident("prompt") {
411 if let Ok(meta_list) = attr.meta.require_list() {
413 if let Ok(metas) =
415 meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
416 {
417 for meta in metas {
418 match meta {
419 Meta::Path(path) if path.is_ident("skip") => {
420 result.skip = true;
421 }
422 Meta::NameValue(nv) if nv.path.is_ident("rename") => {
423 if let syn::Expr::Lit(syn::ExprLit {
424 lit: syn::Lit::Str(lit_str),
425 ..
426 }) = nv.value
427 {
428 result.rename = Some(lit_str.value());
429 }
430 }
431 Meta::NameValue(nv) if nv.path.is_ident("format_with") => {
432 if let syn::Expr::Lit(syn::ExprLit {
433 lit: syn::Lit::Str(lit_str),
434 ..
435 }) = nv.value
436 {
437 result.format_with = Some(lit_str.value());
438 }
439 }
440 Meta::Path(path) if path.is_ident("image") => {
441 result.image = true;
442 }
443 Meta::NameValue(nv) if nv.path.is_ident("example") => {
444 if let syn::Expr::Lit(syn::ExprLit {
445 lit: syn::Lit::Str(lit_str),
446 ..
447 }) = nv.value
448 {
449 result.example = Some(lit_str.value());
450 }
451 }
452 _ => {}
453 }
454 }
455 } else if meta_list.tokens.to_string() == "skip" {
456 result.skip = true;
458 } else if meta_list.tokens.to_string() == "image" {
459 result.image = true;
461 }
462 }
463 }
464 }
465
466 result
467}
468
469#[proc_macro_derive(ToPrompt, attributes(prompt))]
512pub fn to_prompt_derive(input: TokenStream) -> TokenStream {
513 let input = parse_macro_input!(input as DeriveInput);
514
515 let found_crate =
516 crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
517 let crate_path = match found_crate {
518 FoundCrate::Itself => {
519 let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
521 quote!(::#ident)
522 }
523 FoundCrate::Name(name) => {
524 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
525 quote!(::#ident)
526 }
527 };
528
529 match &input.data {
531 Data::Enum(data_enum) => {
532 let enum_name = &input.ident;
534 let enum_docs = extract_doc_comments(&input.attrs);
535
536 let mut prompt_lines = Vec::new();
537
538 if !enum_docs.is_empty() {
540 prompt_lines.push(format!("{}: {}", enum_name, enum_docs));
541 } else {
542 prompt_lines.push(format!("{}:", enum_name));
543 }
544 prompt_lines.push(String::new()); prompt_lines.push("Possible values:".to_string());
546
547 for variant in &data_enum.variants {
549 let variant_name = &variant.ident;
550
551 match parse_prompt_attribute(&variant.attrs) {
553 PromptAttribute::Skip => {
554 continue;
556 }
557 PromptAttribute::Description(desc) => {
558 prompt_lines.push(format!("- {}: {}", variant_name, desc));
560 }
561 PromptAttribute::None => {
562 let variant_docs = extract_doc_comments(&variant.attrs);
564 if !variant_docs.is_empty() {
565 prompt_lines.push(format!("- {}: {}", variant_name, variant_docs));
566 } else {
567 prompt_lines.push(format!("- {}", variant_name));
568 }
569 }
570 }
571 }
572
573 let prompt_string = prompt_lines.join("\n");
574 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
575
576 let mut match_arms = Vec::new();
578 for variant in &data_enum.variants {
579 let variant_name = &variant.ident;
580
581 match parse_prompt_attribute(&variant.attrs) {
583 PromptAttribute::Skip => {
584 match_arms.push(quote! {
586 Self::#variant_name => stringify!(#variant_name).to_string()
587 });
588 }
589 PromptAttribute::Description(desc) => {
590 match_arms.push(quote! {
592 Self::#variant_name => format!("{}: {}", stringify!(#variant_name), #desc)
593 });
594 }
595 PromptAttribute::None => {
596 let variant_docs = extract_doc_comments(&variant.attrs);
598 if !variant_docs.is_empty() {
599 match_arms.push(quote! {
600 Self::#variant_name => format!("{}: {}", stringify!(#variant_name), #variant_docs)
601 });
602 } else {
603 match_arms.push(quote! {
604 Self::#variant_name => stringify!(#variant_name).to_string()
605 });
606 }
607 }
608 }
609 }
610
611 let to_prompt_impl = if match_arms.is_empty() {
612 quote! {
614 fn to_prompt(&self) -> String {
615 match *self {}
616 }
617 }
618 } else {
619 quote! {
620 fn to_prompt(&self) -> String {
621 match self {
622 #(#match_arms),*
623 }
624 }
625 }
626 };
627
628 let expanded = quote! {
629 impl #impl_generics #crate_path::prompt::ToPrompt for #enum_name #ty_generics #where_clause {
630 fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
631 vec![#crate_path::prompt::PromptPart::Text(self.to_prompt())]
632 }
633
634 #to_prompt_impl
635
636 fn prompt_schema() -> String {
637 #prompt_string.to_string()
638 }
639 }
640 };
641
642 TokenStream::from(expanded)
643 }
644 Data::Struct(data_struct) => {
645 let mut template_attr = None;
647 let mut template_file_attr = None;
648 let mut mode_attr = None;
649 let mut validate_attr = false;
650
651 for attr in &input.attrs {
652 if attr.path().is_ident("prompt") {
653 if let Ok(metas) =
655 attr.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
656 {
657 for meta in metas {
658 match meta {
659 Meta::NameValue(nv) if nv.path.is_ident("template") => {
660 if let syn::Expr::Lit(expr_lit) = nv.value
661 && let syn::Lit::Str(lit_str) = expr_lit.lit
662 {
663 template_attr = Some(lit_str.value());
664 }
665 }
666 Meta::NameValue(nv) if nv.path.is_ident("template_file") => {
667 if let syn::Expr::Lit(expr_lit) = nv.value
668 && let syn::Lit::Str(lit_str) = expr_lit.lit
669 {
670 template_file_attr = Some(lit_str.value());
671 }
672 }
673 Meta::NameValue(nv) if nv.path.is_ident("mode") => {
674 if let syn::Expr::Lit(expr_lit) = nv.value
675 && let syn::Lit::Str(lit_str) = expr_lit.lit
676 {
677 mode_attr = Some(lit_str.value());
678 }
679 }
680 Meta::NameValue(nv) if nv.path.is_ident("validate") => {
681 if let syn::Expr::Lit(expr_lit) = nv.value
682 && let syn::Lit::Bool(lit_bool) = expr_lit.lit
683 {
684 validate_attr = lit_bool.value();
685 }
686 }
687 _ => {}
688 }
689 }
690 }
691 }
692 }
693
694 if template_attr.is_some() && template_file_attr.is_some() {
696 return syn::Error::new(
697 input.ident.span(),
698 "The `template` and `template_file` attributes are mutually exclusive. Please use only one.",
699 ).to_compile_error().into();
700 }
701
702 let template_str = if let Some(file_path) = template_file_attr {
704 let mut full_path = None;
708
709 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
711 let is_trybuild = manifest_dir.contains("target/tests/trybuild");
713
714 if !is_trybuild {
715 let candidate = std::path::Path::new(&manifest_dir).join(&file_path);
717 if candidate.exists() {
718 full_path = Some(candidate);
719 }
720 } else {
721 if let Some(target_pos) = manifest_dir.find("/target/tests/trybuild") {
727 let workspace_root = &manifest_dir[..target_pos];
728 let original_macros_dir = std::path::Path::new(workspace_root)
730 .join("crates")
731 .join("llm-toolkit-macros");
732
733 let candidate = original_macros_dir.join(&file_path);
734 if candidate.exists() {
735 full_path = Some(candidate);
736 }
737 }
738 }
739 }
740
741 if full_path.is_none() {
743 let candidate = std::path::Path::new(&file_path).to_path_buf();
744 if candidate.exists() {
745 full_path = Some(candidate);
746 }
747 }
748
749 if full_path.is_none()
752 && let Ok(current_dir) = std::env::current_dir()
753 {
754 let mut search_dir = current_dir.as_path();
755 for _ in 0..10 {
757 let macros_dir = search_dir.join("crates/llm-toolkit-macros");
759 if macros_dir.exists() {
760 let candidate = macros_dir.join(&file_path);
761 if candidate.exists() {
762 full_path = Some(candidate);
763 break;
764 }
765 }
766 let candidate = search_dir.join(&file_path);
768 if candidate.exists() {
769 full_path = Some(candidate);
770 break;
771 }
772 if let Some(parent) = search_dir.parent() {
773 search_dir = parent;
774 } else {
775 break;
776 }
777 }
778 }
779
780 if full_path.is_none() {
782 let mut error_msg = format!(
784 "Template file '{}' not found at compile time.\n\nSearched in:",
785 file_path
786 );
787
788 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
789 let candidate = std::path::Path::new(&manifest_dir).join(&file_path);
790 error_msg.push_str(&format!("\n - {}", candidate.display()));
791 }
792
793 if let Ok(current_dir) = std::env::current_dir() {
794 let candidate = current_dir.join(&file_path);
795 error_msg.push_str(&format!("\n - {}", candidate.display()));
796 }
797
798 error_msg.push_str("\n\nPlease ensure:");
799 error_msg.push_str("\n 1. The template file exists");
800 error_msg.push_str("\n 2. The path is relative to CARGO_MANIFEST_DIR");
801 error_msg.push_str("\n 3. There are no typos in the path");
802
803 return syn::Error::new(input.ident.span(), error_msg)
804 .to_compile_error()
805 .into();
806 }
807
808 let final_path = full_path.unwrap();
809
810 match std::fs::read_to_string(&final_path) {
812 Ok(content) => Some(content),
813 Err(e) => {
814 return syn::Error::new(
815 input.ident.span(),
816 format!(
817 "Failed to read template file '{}': {}\n\nPath resolved to: {}",
818 file_path,
819 e,
820 final_path.display()
821 ),
822 )
823 .to_compile_error()
824 .into();
825 }
826 }
827 } else {
828 template_attr
829 };
830
831 if validate_attr && let Some(template) = &template_str {
833 let mut env = minijinja::Environment::new();
835 if let Err(e) = env.add_template("validation", template) {
836 let warning_msg =
838 format!("Template validation warning: Invalid Jinja syntax - {}", e);
839 let warning_ident = syn::Ident::new(
840 "TEMPLATE_VALIDATION_WARNING",
841 proc_macro2::Span::call_site(),
842 );
843 let _warning_tokens = quote! {
844 #[deprecated(note = #warning_msg)]
845 const #warning_ident: () = ();
846 let _ = #warning_ident;
847 };
848 eprintln!("cargo:warning={}", warning_msg);
850 }
851
852 let fields = if let syn::Fields::Named(fields) = &data_struct.fields {
854 &fields.named
855 } else {
856 panic!("Template validation is only supported for structs with named fields.");
857 };
858
859 let field_names: std::collections::HashSet<String> = fields
860 .iter()
861 .filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
862 .collect();
863
864 let placeholders = parse_template_placeholders_with_mode(template);
866
867 for (placeholder_name, _mode) in &placeholders {
868 if placeholder_name != "self" && !field_names.contains(placeholder_name) {
869 let warning_msg = format!(
870 "Template validation warning: Variable '{}' used in template but not found in struct fields",
871 placeholder_name
872 );
873 eprintln!("cargo:warning={}", warning_msg);
874 }
875 }
876 }
877
878 let name = input.ident;
879 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
880
881 let struct_docs = extract_doc_comments(&input.attrs);
883
884 let is_mode_based =
886 mode_attr.is_some() || (template_str.is_none() && struct_docs.contains("mode"));
887
888 let expanded = if is_mode_based || mode_attr.is_some() {
889 let fields = if let syn::Fields::Named(fields) = &data_struct.fields {
891 &fields.named
892 } else {
893 panic!(
894 "Mode-based prompt generation is only supported for structs with named fields."
895 );
896 };
897
898 let struct_name_str = name.to_string();
899
900 let has_default = input.attrs.iter().any(|attr| {
902 if attr.path().is_ident("derive")
903 && let Ok(meta_list) = attr.meta.require_list()
904 {
905 let tokens_str = meta_list.tokens.to_string();
906 tokens_str.contains("Default")
907 } else {
908 false
909 }
910 });
911
912 let schema_parts =
914 generate_schema_only_parts(&struct_name_str, &struct_docs, fields, &crate_path);
915
916 let example_parts = generate_example_only_parts(fields, has_default, &crate_path);
918
919 quote! {
920 impl #impl_generics #crate_path::prompt::ToPrompt for #name #ty_generics #where_clause {
921 fn to_prompt_parts_with_mode(&self, mode: &str) -> Vec<#crate_path::prompt::PromptPart> {
922 match mode {
923 "schema_only" => #schema_parts,
924 "example_only" => #example_parts,
925 "full" | _ => {
926 let mut parts = Vec::new();
928
929 let schema_parts = #schema_parts;
931 parts.extend(schema_parts);
932
933 parts.push(#crate_path::prompt::PromptPart::Text("\n### Example".to_string()));
935 parts.push(#crate_path::prompt::PromptPart::Text(
936 format!("Here is an example of a valid `{}` object:", #struct_name_str)
937 ));
938
939 let example_parts = #example_parts;
941 parts.extend(example_parts);
942
943 parts
944 }
945 }
946 }
947
948 fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
949 self.to_prompt_parts_with_mode("full")
950 }
951
952 fn to_prompt(&self) -> String {
953 self.to_prompt_parts()
954 .into_iter()
955 .filter_map(|part| match part {
956 #crate_path::prompt::PromptPart::Text(text) => Some(text),
957 _ => None,
958 })
959 .collect::<Vec<_>>()
960 .join("\n")
961 }
962
963 fn prompt_schema() -> String {
964 use std::sync::OnceLock;
965 static SCHEMA_CACHE: OnceLock<String> = OnceLock::new();
966
967 SCHEMA_CACHE.get_or_init(|| {
968 let schema_parts = #schema_parts;
969 schema_parts
970 .into_iter()
971 .filter_map(|part| match part {
972 #crate_path::prompt::PromptPart::Text(text) => Some(text),
973 _ => None,
974 })
975 .collect::<Vec<_>>()
976 .join("\n")
977 }).clone()
978 }
979 }
980 }
981 } else if let Some(template) = template_str {
982 let fields = if let syn::Fields::Named(fields) = &data_struct.fields {
985 &fields.named
986 } else {
987 panic!(
988 "Template prompt generation is only supported for structs with named fields."
989 );
990 };
991
992 let placeholders = parse_template_placeholders_with_mode(&template);
994 let has_mode_syntax = placeholders.iter().any(|(field_name, mode)| {
996 mode.is_some()
997 && fields
998 .iter()
999 .any(|f| f.ident.as_ref().unwrap() == field_name)
1000 });
1001
1002 let mut image_field_parts = Vec::new();
1003 for f in fields.iter() {
1004 let field_name = f.ident.as_ref().unwrap();
1005 let attrs = parse_field_prompt_attrs(&f.attrs);
1006
1007 if attrs.image {
1008 image_field_parts.push(quote! {
1010 parts.extend(self.#field_name.to_prompt_parts());
1011 });
1012 }
1013 }
1014
1015 if has_mode_syntax {
1017 let mut context_fields = Vec::new();
1019 let mut modified_template = template.clone();
1020
1021 for (field_name, mode_opt) in &placeholders {
1023 if let Some(mode) = mode_opt {
1024 let unique_key = format!("{}__{}", field_name, mode);
1026
1027 let pattern = format!("{{{{ {}:{} }}}}", field_name, mode);
1029 let replacement = format!("{{{{ {} }}}}", unique_key);
1030 modified_template = modified_template.replace(&pattern, &replacement);
1031
1032 let field_ident =
1034 syn::Ident::new(field_name, proc_macro2::Span::call_site());
1035
1036 context_fields.push(quote! {
1038 context.insert(
1039 #unique_key.to_string(),
1040 minijinja::Value::from(self.#field_ident.to_prompt_with_mode(#mode))
1041 );
1042 });
1043 }
1044 }
1045
1046 for field in fields.iter() {
1048 let field_name = field.ident.as_ref().unwrap();
1049 let field_name_str = field_name.to_string();
1050
1051 let has_mode_entry = placeholders
1053 .iter()
1054 .any(|(name, mode)| name == &field_name_str && mode.is_some());
1055
1056 if !has_mode_entry {
1057 let is_primitive = match &field.ty {
1060 syn::Type::Path(type_path) => {
1061 if let Some(segment) = type_path.path.segments.last() {
1062 let type_name = segment.ident.to_string();
1063 matches!(
1064 type_name.as_str(),
1065 "String"
1066 | "str"
1067 | "i8"
1068 | "i16"
1069 | "i32"
1070 | "i64"
1071 | "i128"
1072 | "isize"
1073 | "u8"
1074 | "u16"
1075 | "u32"
1076 | "u64"
1077 | "u128"
1078 | "usize"
1079 | "f32"
1080 | "f64"
1081 | "bool"
1082 | "char"
1083 )
1084 } else {
1085 false
1086 }
1087 }
1088 _ => false,
1089 };
1090
1091 if is_primitive {
1092 context_fields.push(quote! {
1093 context.insert(
1094 #field_name_str.to_string(),
1095 minijinja::Value::from_serialize(&self.#field_name)
1096 );
1097 });
1098 } else {
1099 context_fields.push(quote! {
1101 context.insert(
1102 #field_name_str.to_string(),
1103 minijinja::Value::from(self.#field_name.to_prompt())
1104 );
1105 });
1106 }
1107 }
1108 }
1109
1110 quote! {
1111 impl #impl_generics #crate_path::prompt::ToPrompt for #name #ty_generics #where_clause {
1112 fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
1113 let mut parts = Vec::new();
1114
1115 #(#image_field_parts)*
1117
1118 let text = {
1120 let mut env = minijinja::Environment::new();
1121 env.add_template("prompt", #modified_template).unwrap_or_else(|e| {
1122 panic!("Failed to parse template: {}", e)
1123 });
1124
1125 let tmpl = env.get_template("prompt").unwrap();
1126
1127 let mut context = std::collections::HashMap::new();
1128 #(#context_fields)*
1129
1130 tmpl.render(context).unwrap_or_else(|e| {
1131 format!("Failed to render prompt: {}", e)
1132 })
1133 };
1134
1135 if !text.is_empty() {
1136 parts.push(#crate_path::prompt::PromptPart::Text(text));
1137 }
1138
1139 parts
1140 }
1141
1142 fn to_prompt(&self) -> String {
1143 let mut env = minijinja::Environment::new();
1145 env.add_template("prompt", #modified_template).unwrap_or_else(|e| {
1146 panic!("Failed to parse template: {}", e)
1147 });
1148
1149 let tmpl = env.get_template("prompt").unwrap();
1150
1151 let mut context = std::collections::HashMap::new();
1152 #(#context_fields)*
1153
1154 tmpl.render(context).unwrap_or_else(|e| {
1155 format!("Failed to render prompt: {}", e)
1156 })
1157 }
1158
1159 fn prompt_schema() -> String {
1160 String::new() }
1162 }
1163 }
1164 } else {
1165 quote! {
1167 impl #impl_generics #crate_path::prompt::ToPrompt for #name #ty_generics #where_clause {
1168 fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
1169 let mut parts = Vec::new();
1170
1171 #(#image_field_parts)*
1173
1174 let text = #crate_path::prompt::render_prompt(#template, self).unwrap_or_else(|e| {
1176 format!("Failed to render prompt: {}", e)
1177 });
1178 if !text.is_empty() {
1179 parts.push(#crate_path::prompt::PromptPart::Text(text));
1180 }
1181
1182 parts
1183 }
1184
1185 fn to_prompt(&self) -> String {
1186 #crate_path::prompt::render_prompt(#template, self).unwrap_or_else(|e| {
1187 format!("Failed to render prompt: {}", e)
1188 })
1189 }
1190
1191 fn prompt_schema() -> String {
1192 String::new() }
1194 }
1195 }
1196 }
1197 } else {
1198 let fields = if let syn::Fields::Named(fields) = &data_struct.fields {
1201 &fields.named
1202 } else {
1203 panic!(
1204 "Default prompt generation is only supported for structs with named fields."
1205 );
1206 };
1207
1208 let mut text_field_parts = Vec::new();
1210 let mut image_field_parts = Vec::new();
1211
1212 for f in fields.iter() {
1213 let field_name = f.ident.as_ref().unwrap();
1214 let attrs = parse_field_prompt_attrs(&f.attrs);
1215
1216 if attrs.skip {
1218 continue;
1219 }
1220
1221 if attrs.image {
1222 image_field_parts.push(quote! {
1224 parts.extend(self.#field_name.to_prompt_parts());
1225 });
1226 } else {
1227 let key = if let Some(rename) = attrs.rename {
1233 rename
1234 } else {
1235 let doc_comment = extract_doc_comments(&f.attrs);
1236 if !doc_comment.is_empty() {
1237 doc_comment
1238 } else {
1239 field_name.to_string()
1240 }
1241 };
1242
1243 let value_expr = if let Some(format_with) = attrs.format_with {
1245 let func_path: syn::Path =
1247 syn::parse_str(&format_with).unwrap_or_else(|_| {
1248 panic!("Invalid function path: {}", format_with)
1249 });
1250 quote! { #func_path(&self.#field_name) }
1251 } else {
1252 quote! { self.#field_name.to_prompt() }
1253 };
1254
1255 text_field_parts.push(quote! {
1256 text_parts.push(format!("{}: {}", #key, #value_expr));
1257 });
1258 }
1259 }
1260
1261 quote! {
1263 impl #impl_generics #crate_path::prompt::ToPrompt for #name #ty_generics #where_clause {
1264 fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
1265 let mut parts = Vec::new();
1266
1267 #(#image_field_parts)*
1269
1270 let mut text_parts = Vec::new();
1272 #(#text_field_parts)*
1273
1274 if !text_parts.is_empty() {
1275 parts.push(#crate_path::prompt::PromptPart::Text(text_parts.join("\n")));
1276 }
1277
1278 parts
1279 }
1280
1281 fn to_prompt(&self) -> String {
1282 let mut text_parts = Vec::new();
1283 #(#text_field_parts)*
1284 text_parts.join("\n")
1285 }
1286
1287 fn prompt_schema() -> String {
1288 String::new() }
1290 }
1291 }
1292 };
1293
1294 TokenStream::from(expanded)
1295 }
1296 Data::Union(_) => {
1297 panic!("`#[derive(ToPrompt)]` is not supported for unions");
1298 }
1299 }
1300}
1301
1302#[derive(Debug, Clone)]
1304struct TargetInfo {
1305 name: String,
1306 template: Option<String>,
1307 field_configs: std::collections::HashMap<String, FieldTargetConfig>,
1308}
1309
1310#[derive(Debug, Clone, Default)]
1312struct FieldTargetConfig {
1313 skip: bool,
1314 rename: Option<String>,
1315 format_with: Option<String>,
1316 image: bool,
1317 include_only: bool, }
1319
1320fn parse_prompt_for_attrs(attrs: &[syn::Attribute]) -> Vec<(String, FieldTargetConfig)> {
1322 let mut configs = Vec::new();
1323
1324 for attr in attrs {
1325 if attr.path().is_ident("prompt_for")
1326 && let Ok(meta_list) = attr.meta.require_list()
1327 {
1328 if meta_list.tokens.to_string() == "skip" {
1330 let config = FieldTargetConfig {
1332 skip: true,
1333 ..Default::default()
1334 };
1335 configs.push(("*".to_string(), config));
1336 } else if let Ok(metas) =
1337 meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1338 {
1339 let mut target_name = None;
1340 let mut config = FieldTargetConfig::default();
1341
1342 for meta in metas {
1343 match meta {
1344 Meta::NameValue(nv) if nv.path.is_ident("name") => {
1345 if let syn::Expr::Lit(syn::ExprLit {
1346 lit: syn::Lit::Str(lit_str),
1347 ..
1348 }) = nv.value
1349 {
1350 target_name = Some(lit_str.value());
1351 }
1352 }
1353 Meta::Path(path) if path.is_ident("skip") => {
1354 config.skip = true;
1355 }
1356 Meta::NameValue(nv) if nv.path.is_ident("rename") => {
1357 if let syn::Expr::Lit(syn::ExprLit {
1358 lit: syn::Lit::Str(lit_str),
1359 ..
1360 }) = nv.value
1361 {
1362 config.rename = Some(lit_str.value());
1363 }
1364 }
1365 Meta::NameValue(nv) if nv.path.is_ident("format_with") => {
1366 if let syn::Expr::Lit(syn::ExprLit {
1367 lit: syn::Lit::Str(lit_str),
1368 ..
1369 }) = nv.value
1370 {
1371 config.format_with = Some(lit_str.value());
1372 }
1373 }
1374 Meta::Path(path) if path.is_ident("image") => {
1375 config.image = true;
1376 }
1377 _ => {}
1378 }
1379 }
1380
1381 if let Some(name) = target_name {
1382 config.include_only = true;
1383 configs.push((name, config));
1384 }
1385 }
1386 }
1387 }
1388
1389 configs
1390}
1391
1392fn parse_struct_prompt_for_attrs(attrs: &[syn::Attribute]) -> Vec<TargetInfo> {
1394 let mut targets = Vec::new();
1395
1396 for attr in attrs {
1397 if attr.path().is_ident("prompt_for")
1398 && let Ok(meta_list) = attr.meta.require_list()
1399 && let Ok(metas) =
1400 meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1401 {
1402 let mut target_name = None;
1403 let mut template = None;
1404
1405 for meta in metas {
1406 match meta {
1407 Meta::NameValue(nv) if nv.path.is_ident("name") => {
1408 if let syn::Expr::Lit(syn::ExprLit {
1409 lit: syn::Lit::Str(lit_str),
1410 ..
1411 }) = nv.value
1412 {
1413 target_name = Some(lit_str.value());
1414 }
1415 }
1416 Meta::NameValue(nv) if nv.path.is_ident("template") => {
1417 if let syn::Expr::Lit(syn::ExprLit {
1418 lit: syn::Lit::Str(lit_str),
1419 ..
1420 }) = nv.value
1421 {
1422 template = Some(lit_str.value());
1423 }
1424 }
1425 _ => {}
1426 }
1427 }
1428
1429 if let Some(name) = target_name {
1430 targets.push(TargetInfo {
1431 name,
1432 template,
1433 field_configs: std::collections::HashMap::new(),
1434 });
1435 }
1436 }
1437 }
1438
1439 targets
1440}
1441
1442#[proc_macro_derive(ToPromptSet, attributes(prompt_for))]
1443pub fn to_prompt_set_derive(input: TokenStream) -> TokenStream {
1444 let input = parse_macro_input!(input as DeriveInput);
1445
1446 let found_crate =
1447 crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
1448 let crate_path = match found_crate {
1449 FoundCrate::Itself => {
1450 let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
1452 quote!(::#ident)
1453 }
1454 FoundCrate::Name(name) => {
1455 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
1456 quote!(::#ident)
1457 }
1458 };
1459
1460 let data_struct = match &input.data {
1462 Data::Struct(data) => data,
1463 _ => {
1464 return syn::Error::new(
1465 input.ident.span(),
1466 "`#[derive(ToPromptSet)]` is only supported for structs",
1467 )
1468 .to_compile_error()
1469 .into();
1470 }
1471 };
1472
1473 let fields = match &data_struct.fields {
1474 syn::Fields::Named(fields) => &fields.named,
1475 _ => {
1476 return syn::Error::new(
1477 input.ident.span(),
1478 "`#[derive(ToPromptSet)]` is only supported for structs with named fields",
1479 )
1480 .to_compile_error()
1481 .into();
1482 }
1483 };
1484
1485 let mut targets = parse_struct_prompt_for_attrs(&input.attrs);
1487
1488 for field in fields.iter() {
1490 let field_name = field.ident.as_ref().unwrap().to_string();
1491 let field_configs = parse_prompt_for_attrs(&field.attrs);
1492
1493 for (target_name, config) in field_configs {
1494 if target_name == "*" {
1495 for target in &mut targets {
1497 target
1498 .field_configs
1499 .entry(field_name.clone())
1500 .or_insert_with(FieldTargetConfig::default)
1501 .skip = config.skip;
1502 }
1503 } else {
1504 let target_exists = targets.iter().any(|t| t.name == target_name);
1506 if !target_exists {
1507 targets.push(TargetInfo {
1509 name: target_name.clone(),
1510 template: None,
1511 field_configs: std::collections::HashMap::new(),
1512 });
1513 }
1514
1515 let target = targets.iter_mut().find(|t| t.name == target_name).unwrap();
1516
1517 target.field_configs.insert(field_name.clone(), config);
1518 }
1519 }
1520 }
1521
1522 let mut match_arms = Vec::new();
1524
1525 for target in &targets {
1526 let target_name = &target.name;
1527
1528 if let Some(template_str) = &target.template {
1529 let mut image_parts = Vec::new();
1531
1532 for field in fields.iter() {
1533 let field_name = field.ident.as_ref().unwrap();
1534 let field_name_str = field_name.to_string();
1535
1536 if let Some(config) = target.field_configs.get(&field_name_str)
1537 && config.image
1538 {
1539 image_parts.push(quote! {
1540 parts.extend(self.#field_name.to_prompt_parts());
1541 });
1542 }
1543 }
1544
1545 match_arms.push(quote! {
1546 #target_name => {
1547 let mut parts = Vec::new();
1548
1549 #(#image_parts)*
1550
1551 let text = #crate_path::prompt::render_prompt(#template_str, self)
1552 .map_err(|e| #crate_path::prompt::PromptSetError::RenderFailed {
1553 target: #target_name.to_string(),
1554 source: e,
1555 })?;
1556
1557 if !text.is_empty() {
1558 parts.push(#crate_path::prompt::PromptPart::Text(text));
1559 }
1560
1561 Ok(parts)
1562 }
1563 });
1564 } else {
1565 let mut text_field_parts = Vec::new();
1567 let mut image_field_parts = Vec::new();
1568
1569 for field in fields.iter() {
1570 let field_name = field.ident.as_ref().unwrap();
1571 let field_name_str = field_name.to_string();
1572
1573 let config = target.field_configs.get(&field_name_str);
1575
1576 if let Some(cfg) = config
1578 && cfg.skip
1579 {
1580 continue;
1581 }
1582
1583 let is_explicitly_for_this_target = config.is_some_and(|c| c.include_only);
1587 let has_any_target_specific_config = parse_prompt_for_attrs(&field.attrs)
1588 .iter()
1589 .any(|(name, _)| name != "*");
1590
1591 if has_any_target_specific_config && !is_explicitly_for_this_target {
1592 continue;
1593 }
1594
1595 if let Some(cfg) = config {
1596 if cfg.image {
1597 image_field_parts.push(quote! {
1598 parts.extend(self.#field_name.to_prompt_parts());
1599 });
1600 } else {
1601 let key = cfg.rename.clone().unwrap_or_else(|| field_name_str.clone());
1602
1603 let value_expr = if let Some(format_with) = &cfg.format_with {
1604 match syn::parse_str::<syn::Path>(format_with) {
1606 Ok(func_path) => quote! { #func_path(&self.#field_name) },
1607 Err(_) => {
1608 let error_msg = format!(
1610 "Invalid function path in format_with: '{}'",
1611 format_with
1612 );
1613 quote! {
1614 compile_error!(#error_msg);
1615 String::new()
1616 }
1617 }
1618 }
1619 } else {
1620 quote! { self.#field_name.to_prompt() }
1621 };
1622
1623 text_field_parts.push(quote! {
1624 text_parts.push(format!("{}: {}", #key, #value_expr));
1625 });
1626 }
1627 } else {
1628 text_field_parts.push(quote! {
1630 text_parts.push(format!("{}: {}", #field_name_str, self.#field_name.to_prompt()));
1631 });
1632 }
1633 }
1634
1635 match_arms.push(quote! {
1636 #target_name => {
1637 let mut parts = Vec::new();
1638
1639 #(#image_field_parts)*
1640
1641 let mut text_parts = Vec::new();
1642 #(#text_field_parts)*
1643
1644 if !text_parts.is_empty() {
1645 parts.push(#crate_path::prompt::PromptPart::Text(text_parts.join("\n")));
1646 }
1647
1648 Ok(parts)
1649 }
1650 });
1651 }
1652 }
1653
1654 let target_names: Vec<String> = targets.iter().map(|t| t.name.clone()).collect();
1656
1657 match_arms.push(quote! {
1659 _ => {
1660 let available = vec![#(#target_names.to_string()),*];
1661 Err(#crate_path::prompt::PromptSetError::TargetNotFound {
1662 target: target.to_string(),
1663 available,
1664 })
1665 }
1666 });
1667
1668 let struct_name = &input.ident;
1669 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1670
1671 let expanded = quote! {
1672 impl #impl_generics #crate_path::prompt::ToPromptSet for #struct_name #ty_generics #where_clause {
1673 fn to_prompt_parts_for(&self, target: &str) -> Result<Vec<#crate_path::prompt::PromptPart>, #crate_path::prompt::PromptSetError> {
1674 match target {
1675 #(#match_arms)*
1676 }
1677 }
1678 }
1679 };
1680
1681 TokenStream::from(expanded)
1682}
1683
1684struct TypeList {
1686 types: Punctuated<syn::Type, Token![,]>,
1687}
1688
1689impl Parse for TypeList {
1690 fn parse(input: ParseStream) -> syn::Result<Self> {
1691 Ok(TypeList {
1692 types: Punctuated::parse_terminated(input)?,
1693 })
1694 }
1695}
1696
1697#[proc_macro]
1721pub fn examples_section(input: TokenStream) -> TokenStream {
1722 let input = parse_macro_input!(input as TypeList);
1723
1724 let found_crate =
1725 crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
1726 let _crate_path = match found_crate {
1727 FoundCrate::Itself => quote!(crate),
1728 FoundCrate::Name(name) => {
1729 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
1730 quote!(::#ident)
1731 }
1732 };
1733
1734 let mut type_sections = Vec::new();
1736
1737 for ty in input.types.iter() {
1738 let type_name_str = quote!(#ty).to_string();
1740
1741 type_sections.push(quote! {
1743 {
1744 let type_name = #type_name_str;
1745 let json_example = <#ty as Default>::default().to_prompt_with_mode("example_only");
1746 format!("---\n#### `{}`\n{}", type_name, json_example)
1747 }
1748 });
1749 }
1750
1751 let expanded = quote! {
1753 {
1754 let mut sections = Vec::new();
1755 sections.push("---".to_string());
1756 sections.push("### Examples".to_string());
1757 sections.push("".to_string());
1758 sections.push("Here are examples of the data structures you should use.".to_string());
1759 sections.push("".to_string());
1760
1761 #(sections.push(#type_sections);)*
1762
1763 sections.push("---".to_string());
1764
1765 sections.join("\n")
1766 }
1767 };
1768
1769 TokenStream::from(expanded)
1770}
1771
1772fn parse_to_prompt_for_attribute(attrs: &[syn::Attribute]) -> (syn::Type, String) {
1774 for attr in attrs {
1775 if attr.path().is_ident("prompt_for")
1776 && let Ok(meta_list) = attr.meta.require_list()
1777 && let Ok(metas) =
1778 meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1779 {
1780 let mut target_type = None;
1781 let mut template = None;
1782
1783 for meta in metas {
1784 match meta {
1785 Meta::NameValue(nv) if nv.path.is_ident("target") => {
1786 if let syn::Expr::Lit(syn::ExprLit {
1787 lit: syn::Lit::Str(lit_str),
1788 ..
1789 }) = nv.value
1790 {
1791 target_type = syn::parse_str::<syn::Type>(&lit_str.value()).ok();
1793 }
1794 }
1795 Meta::NameValue(nv) if nv.path.is_ident("template") => {
1796 if let syn::Expr::Lit(syn::ExprLit {
1797 lit: syn::Lit::Str(lit_str),
1798 ..
1799 }) = nv.value
1800 {
1801 template = Some(lit_str.value());
1802 }
1803 }
1804 _ => {}
1805 }
1806 }
1807
1808 if let (Some(target), Some(tmpl)) = (target_type, template) {
1809 return (target, tmpl);
1810 }
1811 }
1812 }
1813
1814 panic!("ToPromptFor requires #[prompt_for(target = \"TargetType\", template = \"...\")]");
1815}
1816
1817#[proc_macro_attribute]
1851pub fn define_intent(_attr: TokenStream, item: TokenStream) -> TokenStream {
1852 let input = parse_macro_input!(item as DeriveInput);
1853
1854 let found_crate =
1855 crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
1856 let crate_path = match found_crate {
1857 FoundCrate::Itself => {
1858 let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
1860 quote!(::#ident)
1861 }
1862 FoundCrate::Name(name) => {
1863 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
1864 quote!(::#ident)
1865 }
1866 };
1867
1868 let enum_data = match &input.data {
1870 Data::Enum(data) => data,
1871 _ => {
1872 return syn::Error::new(
1873 input.ident.span(),
1874 "`#[define_intent]` can only be applied to enums",
1875 )
1876 .to_compile_error()
1877 .into();
1878 }
1879 };
1880
1881 let mut prompt_template = None;
1883 let mut extractor_tag = None;
1884 let mut mode = None;
1885
1886 for attr in &input.attrs {
1887 if attr.path().is_ident("intent")
1888 && let Ok(metas) =
1889 attr.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1890 {
1891 for meta in metas {
1892 match meta {
1893 Meta::NameValue(nv) if nv.path.is_ident("prompt") => {
1894 if let syn::Expr::Lit(syn::ExprLit {
1895 lit: syn::Lit::Str(lit_str),
1896 ..
1897 }) = nv.value
1898 {
1899 prompt_template = Some(lit_str.value());
1900 }
1901 }
1902 Meta::NameValue(nv) if nv.path.is_ident("extractor_tag") => {
1903 if let syn::Expr::Lit(syn::ExprLit {
1904 lit: syn::Lit::Str(lit_str),
1905 ..
1906 }) = nv.value
1907 {
1908 extractor_tag = Some(lit_str.value());
1909 }
1910 }
1911 Meta::NameValue(nv) if nv.path.is_ident("mode") => {
1912 if let syn::Expr::Lit(syn::ExprLit {
1913 lit: syn::Lit::Str(lit_str),
1914 ..
1915 }) = nv.value
1916 {
1917 mode = Some(lit_str.value());
1918 }
1919 }
1920 _ => {}
1921 }
1922 }
1923 }
1924 }
1925
1926 let mode = mode.unwrap_or_else(|| "single".to_string());
1928
1929 if mode != "single" && mode != "multi_tag" {
1931 return syn::Error::new(
1932 input.ident.span(),
1933 "`mode` must be either \"single\" or \"multi_tag\"",
1934 )
1935 .to_compile_error()
1936 .into();
1937 }
1938
1939 let prompt_template = match prompt_template {
1941 Some(p) => p,
1942 None => {
1943 return syn::Error::new(
1944 input.ident.span(),
1945 "`#[intent(...)]` attribute must include `prompt = \"...\"`",
1946 )
1947 .to_compile_error()
1948 .into();
1949 }
1950 };
1951
1952 if mode == "multi_tag" {
1954 let enum_name = &input.ident;
1955 let actions_doc = generate_multi_tag_actions_doc(&enum_data.variants);
1956 return generate_multi_tag_output(
1957 &input,
1958 enum_name,
1959 enum_data,
1960 prompt_template,
1961 actions_doc,
1962 );
1963 }
1964
1965 let extractor_tag = match extractor_tag {
1967 Some(t) => t,
1968 None => {
1969 return syn::Error::new(
1970 input.ident.span(),
1971 "`#[intent(...)]` attribute must include `extractor_tag = \"...\"`",
1972 )
1973 .to_compile_error()
1974 .into();
1975 }
1976 };
1977
1978 let enum_name = &input.ident;
1980 let enum_docs = extract_doc_comments(&input.attrs);
1981
1982 let mut intents_doc_lines = Vec::new();
1983
1984 if !enum_docs.is_empty() {
1986 intents_doc_lines.push(format!("{}: {}", enum_name, enum_docs));
1987 } else {
1988 intents_doc_lines.push(format!("{}:", enum_name));
1989 }
1990 intents_doc_lines.push(String::new()); intents_doc_lines.push("Possible values:".to_string());
1992
1993 for variant in &enum_data.variants {
1995 let variant_name = &variant.ident;
1996 let variant_docs = extract_doc_comments(&variant.attrs);
1997
1998 if !variant_docs.is_empty() {
1999 intents_doc_lines.push(format!("- {}: {}", variant_name, variant_docs));
2000 } else {
2001 intents_doc_lines.push(format!("- {}", variant_name));
2002 }
2003 }
2004
2005 let intents_doc_str = intents_doc_lines.join("\n");
2006
2007 let placeholders = parse_template_placeholders_with_mode(&prompt_template);
2009 let user_variables: Vec<String> = placeholders
2010 .iter()
2011 .filter_map(|(name, _)| {
2012 if name != "intents_doc" {
2013 Some(name.clone())
2014 } else {
2015 None
2016 }
2017 })
2018 .collect();
2019
2020 let enum_name_str = enum_name.to_string();
2022 let snake_case_name = to_snake_case(&enum_name_str);
2023 let function_name = syn::Ident::new(
2024 &format!("build_{}_prompt", snake_case_name),
2025 proc_macro2::Span::call_site(),
2026 );
2027
2028 let function_params: Vec<proc_macro2::TokenStream> = user_variables
2030 .iter()
2031 .map(|var| {
2032 let ident = syn::Ident::new(var, proc_macro2::Span::call_site());
2033 quote! { #ident: &str }
2034 })
2035 .collect();
2036
2037 let context_insertions: Vec<proc_macro2::TokenStream> = user_variables
2039 .iter()
2040 .map(|var| {
2041 let var_str = var.clone();
2042 let ident = syn::Ident::new(var, proc_macro2::Span::call_site());
2043 quote! {
2044 __template_context.insert(#var_str.to_string(), minijinja::Value::from(#ident));
2045 }
2046 })
2047 .collect();
2048
2049 let converted_template = prompt_template.clone();
2051
2052 let extractor_name = syn::Ident::new(
2054 &format!("{}Extractor", enum_name),
2055 proc_macro2::Span::call_site(),
2056 );
2057
2058 let filtered_attrs: Vec<_> = input
2060 .attrs
2061 .iter()
2062 .filter(|attr| !attr.path().is_ident("intent"))
2063 .collect();
2064
2065 let vis = &input.vis;
2067 let generics = &input.generics;
2068 let variants = &enum_data.variants;
2069 let enum_output = quote! {
2070 #(#filtered_attrs)*
2071 #vis enum #enum_name #generics {
2072 #variants
2073 }
2074 };
2075
2076 let expanded = quote! {
2078 #enum_output
2080
2081 pub fn #function_name(#(#function_params),*) -> String {
2083 let mut env = minijinja::Environment::new();
2084 env.add_template("prompt", #converted_template)
2085 .expect("Failed to parse intent prompt template");
2086
2087 let tmpl = env.get_template("prompt").unwrap();
2088
2089 let mut __template_context = std::collections::HashMap::new();
2090
2091 __template_context.insert("intents_doc".to_string(), minijinja::Value::from(#intents_doc_str));
2093
2094 #(#context_insertions)*
2096
2097 tmpl.render(&__template_context)
2098 .unwrap_or_else(|e| format!("Failed to render intent prompt: {}", e))
2099 }
2100
2101 pub struct #extractor_name;
2103
2104 impl #extractor_name {
2105 pub const EXTRACTOR_TAG: &'static str = #extractor_tag;
2106 }
2107
2108 impl #crate_path::intent::IntentExtractor<#enum_name> for #extractor_name {
2109 fn extract_intent(&self, response: &str) -> Result<#enum_name, #crate_path::intent::IntentExtractionError> {
2110 #crate_path::intent::extract_intent_from_response(response, Self::EXTRACTOR_TAG)
2112 }
2113 }
2114 };
2115
2116 TokenStream::from(expanded)
2117}
2118
2119fn to_snake_case(s: &str) -> String {
2121 let mut result = String::new();
2122 let mut prev_upper = false;
2123
2124 for (i, ch) in s.chars().enumerate() {
2125 if ch.is_uppercase() {
2126 if i > 0 && !prev_upper {
2127 result.push('_');
2128 }
2129 result.push(ch.to_lowercase().next().unwrap());
2130 prev_upper = true;
2131 } else {
2132 result.push(ch);
2133 prev_upper = false;
2134 }
2135 }
2136
2137 result
2138}
2139
2140#[derive(Debug, Default)]
2142struct ActionAttrs {
2143 tag: Option<String>,
2144}
2145
2146fn parse_action_attrs(attrs: &[syn::Attribute]) -> ActionAttrs {
2147 let mut result = ActionAttrs::default();
2148
2149 for attr in attrs {
2150 if attr.path().is_ident("action")
2151 && let Ok(meta_list) = attr.meta.require_list()
2152 && let Ok(metas) =
2153 meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
2154 {
2155 for meta in metas {
2156 if let Meta::NameValue(nv) = meta
2157 && nv.path.is_ident("tag")
2158 && let syn::Expr::Lit(syn::ExprLit {
2159 lit: syn::Lit::Str(lit_str),
2160 ..
2161 }) = nv.value
2162 {
2163 result.tag = Some(lit_str.value());
2164 }
2165 }
2166 }
2167 }
2168
2169 result
2170}
2171
2172#[derive(Debug, Default)]
2174struct FieldActionAttrs {
2175 is_attribute: bool,
2176 is_inner_text: bool,
2177}
2178
2179fn parse_field_action_attrs(attrs: &[syn::Attribute]) -> FieldActionAttrs {
2180 let mut result = FieldActionAttrs::default();
2181
2182 for attr in attrs {
2183 if attr.path().is_ident("action")
2184 && let Ok(meta_list) = attr.meta.require_list()
2185 {
2186 let tokens_str = meta_list.tokens.to_string();
2187 if tokens_str == "attribute" {
2188 result.is_attribute = true;
2189 } else if tokens_str == "inner_text" {
2190 result.is_inner_text = true;
2191 }
2192 }
2193 }
2194
2195 result
2196}
2197
2198fn generate_multi_tag_actions_doc(
2200 variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
2201) -> String {
2202 let mut doc_lines = Vec::new();
2203
2204 for variant in variants {
2205 let action_attrs = parse_action_attrs(&variant.attrs);
2206
2207 if let Some(tag) = action_attrs.tag {
2208 let variant_docs = extract_doc_comments(&variant.attrs);
2209
2210 match &variant.fields {
2211 syn::Fields::Unit => {
2212 doc_lines.push(format!("- `<{} />`: {}", tag, variant_docs));
2214 }
2215 syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
2216 doc_lines.push(format!("- `<{}>...</{}>`: {}", tag, tag, variant_docs));
2218 }
2219 syn::Fields::Named(fields) => {
2220 let mut attrs_str = Vec::new();
2222 let mut has_inner_text = false;
2223
2224 for field in &fields.named {
2225 let field_name = field.ident.as_ref().unwrap();
2226 let field_attrs = parse_field_action_attrs(&field.attrs);
2227
2228 if field_attrs.is_attribute {
2229 attrs_str.push(format!("{}=\"...\"", field_name));
2230 } else if field_attrs.is_inner_text {
2231 has_inner_text = true;
2232 }
2233 }
2234
2235 let attrs_part = if !attrs_str.is_empty() {
2236 format!(" {}", attrs_str.join(" "))
2237 } else {
2238 String::new()
2239 };
2240
2241 if has_inner_text {
2242 doc_lines.push(format!(
2243 "- `<{}{}>...</{}>`: {}",
2244 tag, attrs_part, tag, variant_docs
2245 ));
2246 } else if !attrs_str.is_empty() {
2247 doc_lines.push(format!("- `<{}{} />`: {}", tag, attrs_part, variant_docs));
2248 } else {
2249 doc_lines.push(format!("- `<{} />`: {}", tag, variant_docs));
2250 }
2251
2252 for field in &fields.named {
2254 let field_name = field.ident.as_ref().unwrap();
2255 let field_attrs = parse_field_action_attrs(&field.attrs);
2256 let field_docs = extract_doc_comments(&field.attrs);
2257
2258 if field_attrs.is_attribute {
2259 doc_lines
2260 .push(format!(" - `{}` (attribute): {}", field_name, field_docs));
2261 } else if field_attrs.is_inner_text {
2262 doc_lines
2263 .push(format!(" - `{}` (inner_text): {}", field_name, field_docs));
2264 }
2265 }
2266 }
2267 _ => {
2268 }
2270 }
2271 }
2272 }
2273
2274 doc_lines.join("\n")
2275}
2276
2277fn generate_tags_regex(
2279 variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
2280) -> String {
2281 let mut tag_names = Vec::new();
2282
2283 for variant in variants {
2284 let action_attrs = parse_action_attrs(&variant.attrs);
2285 if let Some(tag) = action_attrs.tag {
2286 tag_names.push(tag);
2287 }
2288 }
2289
2290 if tag_names.is_empty() {
2291 return String::new();
2292 }
2293
2294 let tags_pattern = tag_names.join("|");
2295 format!(
2298 r"(?is)<(?:{})\b[^>]*/>|<(?:{})\b[^>]*>.*?</(?:{})>",
2299 tags_pattern, tags_pattern, tags_pattern
2300 )
2301}
2302
2303fn generate_multi_tag_output(
2305 input: &DeriveInput,
2306 enum_name: &syn::Ident,
2307 enum_data: &syn::DataEnum,
2308 prompt_template: String,
2309 actions_doc: String,
2310) -> TokenStream {
2311 let found_crate =
2312 crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
2313 let crate_path = match found_crate {
2314 FoundCrate::Itself => {
2315 let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
2317 quote!(::#ident)
2318 }
2319 FoundCrate::Name(name) => {
2320 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
2321 quote!(::#ident)
2322 }
2323 };
2324
2325 let placeholders = parse_template_placeholders_with_mode(&prompt_template);
2327 let user_variables: Vec<String> = placeholders
2328 .iter()
2329 .filter_map(|(name, _)| {
2330 if name != "actions_doc" {
2331 Some(name.clone())
2332 } else {
2333 None
2334 }
2335 })
2336 .collect();
2337
2338 let enum_name_str = enum_name.to_string();
2340 let snake_case_name = to_snake_case(&enum_name_str);
2341 let function_name = syn::Ident::new(
2342 &format!("build_{}_prompt", snake_case_name),
2343 proc_macro2::Span::call_site(),
2344 );
2345
2346 let function_params: Vec<proc_macro2::TokenStream> = user_variables
2348 .iter()
2349 .map(|var| {
2350 let ident = syn::Ident::new(var, proc_macro2::Span::call_site());
2351 quote! { #ident: &str }
2352 })
2353 .collect();
2354
2355 let context_insertions: Vec<proc_macro2::TokenStream> = user_variables
2357 .iter()
2358 .map(|var| {
2359 let var_str = var.clone();
2360 let ident = syn::Ident::new(var, proc_macro2::Span::call_site());
2361 quote! {
2362 __template_context.insert(#var_str.to_string(), minijinja::Value::from(#ident));
2363 }
2364 })
2365 .collect();
2366
2367 let extractor_name = syn::Ident::new(
2369 &format!("{}Extractor", enum_name),
2370 proc_macro2::Span::call_site(),
2371 );
2372
2373 let filtered_attrs: Vec<_> = input
2375 .attrs
2376 .iter()
2377 .filter(|attr| !attr.path().is_ident("intent"))
2378 .collect();
2379
2380 let filtered_variants: Vec<proc_macro2::TokenStream> = enum_data
2382 .variants
2383 .iter()
2384 .map(|variant| {
2385 let variant_name = &variant.ident;
2386 let variant_attrs: Vec<_> = variant
2387 .attrs
2388 .iter()
2389 .filter(|attr| !attr.path().is_ident("action"))
2390 .collect();
2391 let fields = &variant.fields;
2392
2393 let filtered_fields = match fields {
2395 syn::Fields::Named(named_fields) => {
2396 let filtered: Vec<_> = named_fields
2397 .named
2398 .iter()
2399 .map(|field| {
2400 let field_name = &field.ident;
2401 let field_type = &field.ty;
2402 let field_vis = &field.vis;
2403 let filtered_attrs: Vec<_> = field
2404 .attrs
2405 .iter()
2406 .filter(|attr| !attr.path().is_ident("action"))
2407 .collect();
2408 quote! {
2409 #(#filtered_attrs)*
2410 #field_vis #field_name: #field_type
2411 }
2412 })
2413 .collect();
2414 quote! { { #(#filtered,)* } }
2415 }
2416 syn::Fields::Unnamed(unnamed_fields) => {
2417 let types: Vec<_> = unnamed_fields
2418 .unnamed
2419 .iter()
2420 .map(|field| {
2421 let field_type = &field.ty;
2422 quote! { #field_type }
2423 })
2424 .collect();
2425 quote! { (#(#types),*) }
2426 }
2427 syn::Fields::Unit => quote! {},
2428 };
2429
2430 quote! {
2431 #(#variant_attrs)*
2432 #variant_name #filtered_fields
2433 }
2434 })
2435 .collect();
2436
2437 let vis = &input.vis;
2438 let generics = &input.generics;
2439
2440 let parsing_arms = generate_parsing_arms(&enum_data.variants, enum_name);
2442
2443 let tags_regex = generate_tags_regex(&enum_data.variants);
2445
2446 let expanded = quote! {
2447 #(#filtered_attrs)*
2449 #vis enum #enum_name #generics {
2450 #(#filtered_variants),*
2451 }
2452
2453 pub fn #function_name(#(#function_params),*) -> String {
2455 let mut env = minijinja::Environment::new();
2456 env.add_template("prompt", #prompt_template)
2457 .expect("Failed to parse intent prompt template");
2458
2459 let tmpl = env.get_template("prompt").unwrap();
2460
2461 let mut __template_context = std::collections::HashMap::new();
2462
2463 __template_context.insert("actions_doc".to_string(), minijinja::Value::from(#actions_doc));
2465
2466 #(#context_insertions)*
2468
2469 tmpl.render(&__template_context)
2470 .unwrap_or_else(|e| format!("Failed to render intent prompt: {}", e))
2471 }
2472
2473 pub struct #extractor_name;
2475
2476 impl #extractor_name {
2477 fn parse_single_action(&self, text: &str) -> Option<#enum_name> {
2478 use ::quick_xml::events::Event;
2479 use ::quick_xml::Reader;
2480
2481 let mut actions = Vec::new();
2482 let mut reader = Reader::from_str(text);
2483 reader.config_mut().trim_text(true);
2484
2485 let mut buf = Vec::new();
2486
2487 loop {
2488 match reader.read_event_into(&mut buf) {
2489 Ok(Event::Start(e)) => {
2490 let owned_e = e.into_owned();
2491 let tag_name = String::from_utf8_lossy(owned_e.name().as_ref()).to_string();
2492 let is_empty = false;
2493
2494 #parsing_arms
2495 }
2496 Ok(Event::Empty(e)) => {
2497 let owned_e = e.into_owned();
2498 let tag_name = String::from_utf8_lossy(owned_e.name().as_ref()).to_string();
2499 let is_empty = true;
2500
2501 #parsing_arms
2502 }
2503 Ok(Event::Eof) => break,
2504 Err(_) => {
2505 break;
2507 }
2508 _ => {}
2509 }
2510 buf.clear();
2511 }
2512
2513 actions.into_iter().next()
2514 }
2515
2516 pub fn extract_actions(&self, text: &str) -> Result<Vec<#enum_name>, #crate_path::intent::IntentError> {
2517 use ::quick_xml::events::Event;
2518 use ::quick_xml::Reader;
2519
2520 let mut actions = Vec::new();
2521 let mut reader = Reader::from_str(text);
2522 reader.config_mut().trim_text(true);
2523
2524 let mut buf = Vec::new();
2525
2526 loop {
2527 match reader.read_event_into(&mut buf) {
2528 Ok(Event::Start(e)) => {
2529 let owned_e = e.into_owned();
2530 let tag_name = String::from_utf8_lossy(owned_e.name().as_ref()).to_string();
2531 let is_empty = false;
2532
2533 #parsing_arms
2534 }
2535 Ok(Event::Empty(e)) => {
2536 let owned_e = e.into_owned();
2537 let tag_name = String::from_utf8_lossy(owned_e.name().as_ref()).to_string();
2538 let is_empty = true;
2539
2540 #parsing_arms
2541 }
2542 Ok(Event::Eof) => break,
2543 Err(_) => {
2544 break;
2546 }
2547 _ => {}
2548 }
2549 buf.clear();
2550 }
2551
2552 Ok(actions)
2553 }
2554
2555 pub fn transform_actions<F>(&self, text: &str, mut transformer: F) -> String
2556 where
2557 F: FnMut(#enum_name) -> String,
2558 {
2559 use ::regex::Regex;
2560
2561 let regex_pattern = #tags_regex;
2562 if regex_pattern.is_empty() {
2563 return text.to_string();
2564 }
2565
2566 let re = Regex::new(®ex_pattern).unwrap_or_else(|e| {
2567 panic!("Failed to compile regex for action tags: {}", e);
2568 });
2569
2570 re.replace_all(text, |caps: &::regex::Captures| {
2571 let matched = caps.get(0).map(|m| m.as_str()).unwrap_or("");
2572
2573 if let Some(action) = self.parse_single_action(matched) {
2575 transformer(action)
2576 } else {
2577 matched.to_string()
2579 }
2580 }).to_string()
2581 }
2582
2583 pub fn strip_actions(&self, text: &str) -> String {
2584 self.transform_actions(text, |_| String::new())
2585 }
2586 }
2587 };
2588
2589 TokenStream::from(expanded)
2590}
2591
2592fn generate_parsing_arms(
2594 variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
2595 enum_name: &syn::Ident,
2596) -> proc_macro2::TokenStream {
2597 let mut arms = Vec::new();
2598
2599 for variant in variants {
2600 let variant_name = &variant.ident;
2601 let action_attrs = parse_action_attrs(&variant.attrs);
2602
2603 if let Some(tag) = action_attrs.tag {
2604 match &variant.fields {
2605 syn::Fields::Unit => {
2606 arms.push(quote! {
2608 if &tag_name == #tag {
2609 actions.push(#enum_name::#variant_name);
2610 }
2611 });
2612 }
2613 syn::Fields::Unnamed(_fields) => {
2614 arms.push(quote! {
2616 if &tag_name == #tag && !is_empty {
2617 match reader.read_text(owned_e.name()) {
2619 Ok(text) => {
2620 actions.push(#enum_name::#variant_name(text.to_string()));
2621 }
2622 Err(_) => {
2623 actions.push(#enum_name::#variant_name(String::new()));
2625 }
2626 }
2627 }
2628 });
2629 }
2630 syn::Fields::Named(fields) => {
2631 let mut field_names = Vec::new();
2633 let mut has_inner_text_field = None;
2634
2635 for field in &fields.named {
2636 let field_name = field.ident.as_ref().unwrap();
2637 let field_attrs = parse_field_action_attrs(&field.attrs);
2638
2639 if field_attrs.is_attribute {
2640 field_names.push(field_name.clone());
2641 } else if field_attrs.is_inner_text {
2642 has_inner_text_field = Some(field_name.clone());
2643 }
2644 }
2645
2646 if let Some(inner_text_field) = has_inner_text_field {
2647 let attr_extractions: Vec<_> = field_names.iter().map(|field_name| {
2650 quote! {
2651 let mut #field_name = String::new();
2652 for attr in owned_e.attributes() {
2653 if let Ok(attr) = attr {
2654 if attr.key.as_ref() == stringify!(#field_name).as_bytes() {
2655 #field_name = String::from_utf8_lossy(&attr.value).to_string();
2656 break;
2657 }
2658 }
2659 }
2660 }
2661 }).collect();
2662
2663 arms.push(quote! {
2664 if &tag_name == #tag {
2665 #(#attr_extractions)*
2666
2667 if is_empty {
2669 let #inner_text_field = String::new();
2670 actions.push(#enum_name::#variant_name {
2671 #(#field_names,)*
2672 #inner_text_field,
2673 });
2674 } else {
2675 match reader.read_text(owned_e.name()) {
2677 Ok(text) => {
2678 let #inner_text_field = text.to_string();
2679 actions.push(#enum_name::#variant_name {
2680 #(#field_names,)*
2681 #inner_text_field,
2682 });
2683 }
2684 Err(_) => {
2685 let #inner_text_field = String::new();
2687 actions.push(#enum_name::#variant_name {
2688 #(#field_names,)*
2689 #inner_text_field,
2690 });
2691 }
2692 }
2693 }
2694 }
2695 });
2696 } else {
2697 let attr_extractions: Vec<_> = field_names.iter().map(|field_name| {
2699 quote! {
2700 let mut #field_name = String::new();
2701 for attr in owned_e.attributes() {
2702 if let Ok(attr) = attr {
2703 if attr.key.as_ref() == stringify!(#field_name).as_bytes() {
2704 #field_name = String::from_utf8_lossy(&attr.value).to_string();
2705 break;
2706 }
2707 }
2708 }
2709 }
2710 }).collect();
2711
2712 arms.push(quote! {
2713 if &tag_name == #tag {
2714 #(#attr_extractions)*
2715 actions.push(#enum_name::#variant_name {
2716 #(#field_names),*
2717 });
2718 }
2719 });
2720 }
2721 }
2722 }
2723 }
2724 }
2725
2726 quote! {
2727 #(#arms)*
2728 }
2729}
2730
2731#[proc_macro_derive(ToPromptFor, attributes(prompt_for))]
2733pub fn to_prompt_for_derive(input: TokenStream) -> TokenStream {
2734 let input = parse_macro_input!(input as DeriveInput);
2735
2736 let found_crate =
2737 crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
2738 let crate_path = match found_crate {
2739 FoundCrate::Itself => {
2740 let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
2742 quote!(::#ident)
2743 }
2744 FoundCrate::Name(name) => {
2745 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
2746 quote!(::#ident)
2747 }
2748 };
2749
2750 let (target_type, template) = parse_to_prompt_for_attribute(&input.attrs);
2752
2753 let struct_name = &input.ident;
2754 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
2755
2756 let placeholders = parse_template_placeholders_with_mode(&template);
2758
2759 let mut converted_template = template.clone();
2761 let mut context_fields = Vec::new();
2762
2763 let fields = match &input.data {
2765 Data::Struct(data_struct) => match &data_struct.fields {
2766 syn::Fields::Named(fields) => &fields.named,
2767 _ => panic!("ToPromptFor is only supported for structs with named fields"),
2768 },
2769 _ => panic!("ToPromptFor is only supported for structs"),
2770 };
2771
2772 let has_mode_support = input.attrs.iter().any(|attr| {
2774 if attr.path().is_ident("prompt")
2775 && let Ok(metas) =
2776 attr.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
2777 {
2778 for meta in metas {
2779 if let Meta::NameValue(nv) = meta
2780 && nv.path.is_ident("mode")
2781 {
2782 return true;
2783 }
2784 }
2785 }
2786 false
2787 });
2788
2789 for (placeholder_name, mode_opt) in &placeholders {
2791 if placeholder_name == "self" {
2792 if let Some(specific_mode) = mode_opt {
2793 let unique_key = format!("self__{}", specific_mode);
2795
2796 let pattern = format!("{{{{ self:{} }}}}", specific_mode);
2798 let replacement = format!("{{{{ {} }}}}", unique_key);
2799 converted_template = converted_template.replace(&pattern, &replacement);
2800
2801 context_fields.push(quote! {
2803 context.insert(
2804 #unique_key.to_string(),
2805 minijinja::Value::from(self.to_prompt_with_mode(#specific_mode))
2806 );
2807 });
2808 } else {
2809 if has_mode_support {
2812 context_fields.push(quote! {
2814 context.insert(
2815 "self".to_string(),
2816 minijinja::Value::from(self.to_prompt_with_mode(mode))
2817 );
2818 });
2819 } else {
2820 context_fields.push(quote! {
2822 context.insert(
2823 "self".to_string(),
2824 minijinja::Value::from(self.to_prompt())
2825 );
2826 });
2827 }
2828 }
2829 } else {
2830 let field_exists = fields.iter().any(|f| {
2833 f.ident
2834 .as_ref()
2835 .is_some_and(|ident| ident == placeholder_name)
2836 });
2837
2838 if field_exists {
2839 let field_ident = syn::Ident::new(placeholder_name, proc_macro2::Span::call_site());
2840
2841 context_fields.push(quote! {
2845 context.insert(
2846 #placeholder_name.to_string(),
2847 minijinja::Value::from_serialize(&self.#field_ident)
2848 );
2849 });
2850 }
2851 }
2853 }
2854
2855 let expanded = quote! {
2856 impl #impl_generics #crate_path::prompt::ToPromptFor<#target_type> for #struct_name #ty_generics #where_clause
2857 where
2858 #target_type: serde::Serialize,
2859 {
2860 fn to_prompt_for_with_mode(&self, target: &#target_type, mode: &str) -> String {
2861 let mut env = minijinja::Environment::new();
2863 env.add_template("prompt", #converted_template).unwrap_or_else(|e| {
2864 panic!("Failed to parse template: {}", e)
2865 });
2866
2867 let tmpl = env.get_template("prompt").unwrap();
2868
2869 let mut context = std::collections::HashMap::new();
2871 context.insert(
2873 "self".to_string(),
2874 minijinja::Value::from_serialize(self)
2875 );
2876 context.insert(
2878 "target".to_string(),
2879 minijinja::Value::from_serialize(target)
2880 );
2881 #(#context_fields)*
2882
2883 tmpl.render(context).unwrap_or_else(|e| {
2885 format!("Failed to render prompt: {}", e)
2886 })
2887 }
2888 }
2889 };
2890
2891 TokenStream::from(expanded)
2892}
2893
2894struct AgentAttrs {
2900 expertise: Option<String>,
2901 output: Option<syn::Type>,
2902 backend: Option<String>,
2903 model: Option<String>,
2904 inner: Option<String>,
2905 default_inner: Option<String>,
2906 max_retries: Option<u32>,
2907 profile: Option<String>,
2908}
2909
2910impl Parse for AgentAttrs {
2911 fn parse(input: ParseStream) -> syn::Result<Self> {
2912 let mut expertise = None;
2913 let mut output = None;
2914 let mut backend = None;
2915 let mut model = None;
2916 let mut inner = None;
2917 let mut default_inner = None;
2918 let mut max_retries = None;
2919 let mut profile = None;
2920
2921 let pairs = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
2922
2923 for meta in pairs {
2924 match meta {
2925 Meta::NameValue(nv) if nv.path.is_ident("expertise") => {
2926 if let syn::Expr::Lit(syn::ExprLit {
2927 lit: syn::Lit::Str(lit_str),
2928 ..
2929 }) = &nv.value
2930 {
2931 expertise = Some(lit_str.value());
2932 }
2933 }
2934 Meta::NameValue(nv) if nv.path.is_ident("output") => {
2935 if let syn::Expr::Lit(syn::ExprLit {
2936 lit: syn::Lit::Str(lit_str),
2937 ..
2938 }) = &nv.value
2939 {
2940 let ty: syn::Type = syn::parse_str(&lit_str.value())?;
2941 output = Some(ty);
2942 }
2943 }
2944 Meta::NameValue(nv) if nv.path.is_ident("backend") => {
2945 if let syn::Expr::Lit(syn::ExprLit {
2946 lit: syn::Lit::Str(lit_str),
2947 ..
2948 }) = &nv.value
2949 {
2950 backend = Some(lit_str.value());
2951 }
2952 }
2953 Meta::NameValue(nv) if nv.path.is_ident("model") => {
2954 if let syn::Expr::Lit(syn::ExprLit {
2955 lit: syn::Lit::Str(lit_str),
2956 ..
2957 }) = &nv.value
2958 {
2959 model = Some(lit_str.value());
2960 }
2961 }
2962 Meta::NameValue(nv) if nv.path.is_ident("inner") => {
2963 if let syn::Expr::Lit(syn::ExprLit {
2964 lit: syn::Lit::Str(lit_str),
2965 ..
2966 }) = &nv.value
2967 {
2968 inner = Some(lit_str.value());
2969 }
2970 }
2971 Meta::NameValue(nv) if nv.path.is_ident("default_inner") => {
2972 if let syn::Expr::Lit(syn::ExprLit {
2973 lit: syn::Lit::Str(lit_str),
2974 ..
2975 }) = &nv.value
2976 {
2977 default_inner = Some(lit_str.value());
2978 }
2979 }
2980 Meta::NameValue(nv) if nv.path.is_ident("max_retries") => {
2981 if let syn::Expr::Lit(syn::ExprLit {
2982 lit: syn::Lit::Int(lit_int),
2983 ..
2984 }) = &nv.value
2985 {
2986 max_retries = Some(lit_int.base10_parse()?);
2987 }
2988 }
2989 Meta::NameValue(nv) if nv.path.is_ident("profile") => {
2990 if let syn::Expr::Lit(syn::ExprLit {
2991 lit: syn::Lit::Str(lit_str),
2992 ..
2993 }) = &nv.value
2994 {
2995 profile = Some(lit_str.value());
2996 }
2997 }
2998 _ => {}
2999 }
3000 }
3001
3002 Ok(AgentAttrs {
3003 expertise,
3004 output,
3005 backend,
3006 model,
3007 inner,
3008 default_inner,
3009 max_retries,
3010 profile,
3011 })
3012 }
3013}
3014
3015fn parse_agent_attrs(attrs: &[syn::Attribute]) -> syn::Result<AgentAttrs> {
3017 for attr in attrs {
3018 if attr.path().is_ident("agent") {
3019 return attr.parse_args::<AgentAttrs>();
3020 }
3021 }
3022
3023 Ok(AgentAttrs {
3024 expertise: None,
3025 output: None,
3026 backend: None,
3027 model: None,
3028 inner: None,
3029 default_inner: None,
3030 max_retries: None,
3031 profile: None,
3032 })
3033}
3034
3035fn generate_backend_constructors(
3037 struct_name: &syn::Ident,
3038 backend: &str,
3039 _model: Option<&str>,
3040 _profile: Option<&str>,
3041 crate_path: &proc_macro2::TokenStream,
3042) -> proc_macro2::TokenStream {
3043 match backend {
3044 "claude" => {
3045 quote! {
3046 impl #struct_name {
3047 pub fn with_claude() -> Self {
3049 Self::new(#crate_path::agent::impls::ClaudeCodeAgent::new())
3050 }
3051
3052 pub fn with_claude_model(model: &str) -> Self {
3054 Self::new(
3055 #crate_path::agent::impls::ClaudeCodeAgent::new()
3056 .with_model_str(model)
3057 )
3058 }
3059 }
3060 }
3061 }
3062 "gemini" => {
3063 quote! {
3064 impl #struct_name {
3065 pub fn with_gemini() -> Self {
3067 Self::new(#crate_path::agent::impls::GeminiAgent::new())
3068 }
3069
3070 pub fn with_gemini_model(model: &str) -> Self {
3072 Self::new(
3073 #crate_path::agent::impls::GeminiAgent::new()
3074 .with_model_str(model)
3075 )
3076 }
3077 }
3078 }
3079 }
3080 _ => quote! {},
3081 }
3082}
3083
3084fn generate_default_impl(
3086 struct_name: &syn::Ident,
3087 backend: &str,
3088 model: Option<&str>,
3089 profile: Option<&str>,
3090 crate_path: &proc_macro2::TokenStream,
3091) -> proc_macro2::TokenStream {
3092 let profile_expr = if let Some(profile_str) = profile {
3094 match profile_str.to_lowercase().as_str() {
3095 "creative" => quote! { #crate_path::agent::ExecutionProfile::Creative },
3096 "balanced" => quote! { #crate_path::agent::ExecutionProfile::Balanced },
3097 "deterministic" => quote! { #crate_path::agent::ExecutionProfile::Deterministic },
3098 _ => quote! { #crate_path::agent::ExecutionProfile::Balanced }, }
3100 } else {
3101 quote! { #crate_path::agent::ExecutionProfile::default() }
3102 };
3103
3104 let agent_init = match backend {
3105 "gemini" => {
3106 let mut builder = quote! { #crate_path::agent::impls::GeminiAgent::new() };
3107
3108 if let Some(model_str) = model {
3109 builder = quote! { #builder.with_model_str(#model_str) };
3110 }
3111
3112 builder = quote! { #builder.with_execution_profile(#profile_expr) };
3113 builder
3114 }
3115 _ => {
3116 let mut builder = quote! { #crate_path::agent::impls::ClaudeCodeAgent::new() };
3118
3119 if let Some(model_str) = model {
3120 builder = quote! { #builder.with_model_str(#model_str) };
3121 }
3122
3123 builder = quote! { #builder.with_execution_profile(#profile_expr) };
3124 builder
3125 }
3126 };
3127
3128 quote! {
3129 impl Default for #struct_name {
3130 fn default() -> Self {
3131 Self::new(#agent_init)
3132 }
3133 }
3134 }
3135}
3136
3137#[proc_macro_derive(Agent, attributes(agent))]
3146pub fn derive_agent(input: TokenStream) -> TokenStream {
3147 let input = parse_macro_input!(input as DeriveInput);
3148 let struct_name = &input.ident;
3149
3150 let agent_attrs = match parse_agent_attrs(&input.attrs) {
3152 Ok(attrs) => attrs,
3153 Err(e) => return e.to_compile_error().into(),
3154 };
3155
3156 let expertise = agent_attrs
3157 .expertise
3158 .unwrap_or_else(|| String::from("general AI assistant"));
3159 let output_type = agent_attrs
3160 .output
3161 .unwrap_or_else(|| syn::parse_str::<syn::Type>("String").unwrap());
3162 let backend = agent_attrs
3163 .backend
3164 .unwrap_or_else(|| String::from("claude"));
3165 let model = agent_attrs.model;
3166 let _profile = agent_attrs.profile; let max_retries = agent_attrs.max_retries.unwrap_or(3); let found_crate =
3171 crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
3172 let crate_path = match found_crate {
3173 FoundCrate::Itself => {
3174 let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
3176 quote!(::#ident)
3177 }
3178 FoundCrate::Name(name) => {
3179 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
3180 quote!(::#ident)
3181 }
3182 };
3183
3184 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
3185
3186 let output_type_str = quote!(#output_type).to_string().replace(" ", "");
3188 let is_string_output = output_type_str == "String" || output_type_str == "&str";
3189
3190 let enhanced_expertise = if is_string_output {
3192 quote! { #expertise }
3194 } else {
3195 let type_name = quote!(#output_type).to_string();
3197 quote! {
3198 {
3199 use std::sync::OnceLock;
3200 static EXPERTISE_CACHE: OnceLock<String> = OnceLock::new();
3201
3202 EXPERTISE_CACHE.get_or_init(|| {
3203 let schema = <#output_type as #crate_path::prompt::ToPrompt>::prompt_schema();
3205
3206 if schema.is_empty() {
3207 format!(
3209 concat!(
3210 #expertise,
3211 "\n\nIMPORTANT: You must respond with valid JSON matching the {} type structure. ",
3212 "Do not include any text outside the JSON object."
3213 ),
3214 #type_name
3215 )
3216 } else {
3217 format!(
3219 concat!(
3220 #expertise,
3221 "\n\nIMPORTANT: Respond with valid JSON matching this schema:\n\n{}"
3222 ),
3223 schema
3224 )
3225 }
3226 }).as_str()
3227 }
3228 }
3229 };
3230
3231 let agent_init = match backend.as_str() {
3233 "gemini" => {
3234 if let Some(model_str) = model {
3235 quote! {
3236 use #crate_path::agent::impls::GeminiAgent;
3237 let agent = GeminiAgent::new().with_model_str(#model_str);
3238 }
3239 } else {
3240 quote! {
3241 use #crate_path::agent::impls::GeminiAgent;
3242 let agent = GeminiAgent::new();
3243 }
3244 }
3245 }
3246 "claude" => {
3247 if let Some(model_str) = model {
3248 quote! {
3249 use #crate_path::agent::impls::ClaudeCodeAgent;
3250 let agent = ClaudeCodeAgent::new().with_model_str(#model_str);
3251 }
3252 } else {
3253 quote! {
3254 use #crate_path::agent::impls::ClaudeCodeAgent;
3255 let agent = ClaudeCodeAgent::new();
3256 }
3257 }
3258 }
3259 _ => {
3260 if let Some(model_str) = model {
3262 quote! {
3263 use #crate_path::agent::impls::ClaudeCodeAgent;
3264 let agent = ClaudeCodeAgent::new().with_model_str(#model_str);
3265 }
3266 } else {
3267 quote! {
3268 use #crate_path::agent::impls::ClaudeCodeAgent;
3269 let agent = ClaudeCodeAgent::new();
3270 }
3271 }
3272 }
3273 };
3274
3275 let expanded = quote! {
3276 #[async_trait::async_trait]
3277 impl #impl_generics #crate_path::agent::Agent for #struct_name #ty_generics #where_clause {
3278 type Output = #output_type;
3279
3280 fn expertise(&self) -> &str {
3281 #enhanced_expertise
3282 }
3283
3284 async fn execute(&self, intent: #crate_path::agent::Payload) -> Result<Self::Output, #crate_path::agent::AgentError> {
3285 #agent_init
3287
3288 let max_retries: u32 = #max_retries;
3290 let mut attempts = 0u32;
3291
3292 loop {
3293 attempts += 1;
3294
3295 let result = async {
3297 let response = agent.execute(intent.clone()).await?;
3298
3299 let json_str = #crate_path::extract_json(&response)
3301 .map_err(|e| #crate_path::agent::AgentError::ParseError(e.to_string()))?;
3302
3303 serde_json::from_str::<Self::Output>(&json_str)
3305 .map_err(|e| #crate_path::agent::AgentError::ParseError(e.to_string()))
3306 }.await;
3307
3308 match result {
3309 Ok(output) => return Ok(output),
3310 Err(e) if e.is_retryable() && attempts < max_retries => {
3311 log::warn!(
3313 "Agent execution failed (attempt {}/{}): {}. Retrying...",
3314 attempts,
3315 max_retries,
3316 e
3317 );
3318
3319 let delay_ms = 100 * attempts as u64;
3321 tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
3322
3323 continue;
3325 }
3326 Err(e) => {
3327 if attempts > 1 {
3328 log::error!(
3329 "Agent execution failed after {} attempts: {}",
3330 attempts,
3331 e
3332 );
3333 }
3334 return Err(e);
3335 }
3336 }
3337 }
3338 }
3339
3340 async fn is_available(&self) -> Result<(), #crate_path::agent::AgentError> {
3341 #agent_init
3343 agent.is_available().await
3344 }
3345 }
3346 };
3347
3348 TokenStream::from(expanded)
3349}
3350
3351#[proc_macro_attribute]
3366pub fn agent(attr: TokenStream, item: TokenStream) -> TokenStream {
3367 let agent_attrs = match syn::parse::<AgentAttrs>(attr) {
3369 Ok(attrs) => attrs,
3370 Err(e) => return e.to_compile_error().into(),
3371 };
3372
3373 let input = parse_macro_input!(item as DeriveInput);
3375 let struct_name = &input.ident;
3376 let vis = &input.vis;
3377
3378 let expertise = agent_attrs
3379 .expertise
3380 .unwrap_or_else(|| String::from("general AI assistant"));
3381 let output_type = agent_attrs
3382 .output
3383 .unwrap_or_else(|| syn::parse_str::<syn::Type>("String").unwrap());
3384 let backend = agent_attrs
3385 .backend
3386 .unwrap_or_else(|| String::from("claude"));
3387 let model = agent_attrs.model;
3388 let profile = agent_attrs.profile;
3389
3390 let output_type_str = quote!(#output_type).to_string().replace(" ", "");
3392 let is_string_output = output_type_str == "String" || output_type_str == "&str";
3393
3394 let found_crate =
3396 crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
3397 let crate_path = match found_crate {
3398 FoundCrate::Itself => {
3399 let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
3400 quote!(::#ident)
3401 }
3402 FoundCrate::Name(name) => {
3403 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
3404 quote!(::#ident)
3405 }
3406 };
3407
3408 let inner_generic_name = agent_attrs.inner.unwrap_or_else(|| String::from("A"));
3410 let inner_generic_ident = syn::Ident::new(&inner_generic_name, proc_macro2::Span::call_site());
3411
3412 let default_agent_type = if let Some(ref custom_type) = agent_attrs.default_inner {
3414 let type_path: syn::Type =
3416 syn::parse_str(custom_type).expect("default_inner must be a valid type path");
3417 quote! { #type_path }
3418 } else {
3419 match backend.as_str() {
3421 "gemini" => quote! { #crate_path::agent::impls::GeminiAgent },
3422 _ => quote! { #crate_path::agent::impls::ClaudeCodeAgent },
3423 }
3424 };
3425
3426 let struct_def = quote! {
3428 #vis struct #struct_name<#inner_generic_ident = #default_agent_type> {
3429 inner: #inner_generic_ident,
3430 }
3431 };
3432
3433 let constructors = quote! {
3435 impl<#inner_generic_ident> #struct_name<#inner_generic_ident> {
3436 pub fn new(inner: #inner_generic_ident) -> Self {
3438 Self { inner }
3439 }
3440 }
3441 };
3442
3443 let (backend_constructors, default_impl) = if agent_attrs.default_inner.is_some() {
3445 let default_impl = quote! {
3447 impl Default for #struct_name {
3448 fn default() -> Self {
3449 Self {
3450 inner: <#default_agent_type as Default>::default(),
3451 }
3452 }
3453 }
3454 };
3455 (quote! {}, default_impl)
3456 } else {
3457 let backend_constructors = generate_backend_constructors(
3459 struct_name,
3460 &backend,
3461 model.as_deref(),
3462 profile.as_deref(),
3463 &crate_path,
3464 );
3465 let default_impl = generate_default_impl(
3466 struct_name,
3467 &backend,
3468 model.as_deref(),
3469 profile.as_deref(),
3470 &crate_path,
3471 );
3472 (backend_constructors, default_impl)
3473 };
3474
3475 let enhanced_expertise = if is_string_output {
3477 quote! { #expertise }
3479 } else {
3480 let type_name = quote!(#output_type).to_string();
3482 quote! {
3483 {
3484 use std::sync::OnceLock;
3485 static EXPERTISE_CACHE: OnceLock<String> = OnceLock::new();
3486
3487 EXPERTISE_CACHE.get_or_init(|| {
3488 let schema = <#output_type as #crate_path::prompt::ToPrompt>::prompt_schema();
3490
3491 if schema.is_empty() {
3492 format!(
3494 concat!(
3495 #expertise,
3496 "\n\nIMPORTANT: You must respond with valid JSON matching the {} type structure. ",
3497 "Do not include any text outside the JSON object."
3498 ),
3499 #type_name
3500 )
3501 } else {
3502 format!(
3504 concat!(
3505 #expertise,
3506 "\n\nIMPORTANT: Respond with valid JSON matching this schema:\n\n{}"
3507 ),
3508 schema
3509 )
3510 }
3511 }).as_str()
3512 }
3513 }
3514 };
3515
3516 let agent_impl = quote! {
3518 #[async_trait::async_trait]
3519 impl<#inner_generic_ident> #crate_path::agent::Agent for #struct_name<#inner_generic_ident>
3520 where
3521 #inner_generic_ident: #crate_path::agent::Agent<Output = String>,
3522 {
3523 type Output = #output_type;
3524
3525 fn expertise(&self) -> &str {
3526 #enhanced_expertise
3527 }
3528
3529 async fn execute(&self, intent: #crate_path::agent::Payload) -> Result<Self::Output, #crate_path::agent::AgentError> {
3530 let enhanced_payload = intent.prepend_text(self.expertise());
3532
3533 let response = self.inner.execute(enhanced_payload).await?;
3535
3536 let json_str = #crate_path::extract_json(&response)
3538 .map_err(|e| #crate_path::agent::AgentError::ParseError(e.to_string()))?;
3539
3540 serde_json::from_str(&json_str)
3542 .map_err(|e| #crate_path::agent::AgentError::ParseError(e.to_string()))
3543 }
3544
3545 async fn is_available(&self) -> Result<(), #crate_path::agent::AgentError> {
3546 self.inner.is_available().await
3547 }
3548 }
3549 };
3550
3551 let expanded = quote! {
3552 #struct_def
3553 #constructors
3554 #backend_constructors
3555 #default_impl
3556 #agent_impl
3557 };
3558
3559 TokenStream::from(expanded)
3560}