llm_toolkit_macros/
lib.rs

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
12/// Parse template placeholders using regex to find :mode patterns
13/// Returns a list of (field_name, optional_mode)
14fn 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    // First, find all {{ field:mode }} patterns
19    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    // Then, find all standard {{ field }} patterns (without mode)
28    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        // Check if this field was already captured with a mode
32        if !seen_fields.contains(&field_name) {
33            placeholders.push((field_name, None));
34        }
35    }
36
37    placeholders
38}
39
40/// Extract doc comments from attributes
41fn 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
61/// Generate example JSON representation for a struct
62fn 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        // Skip if marked to skip
75        if attrs.skip {
76            continue;
77        }
78
79        // Check if field has example attribute
80        if let Some(example) = attrs.example {
81            // Use the provided example value
82            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            // Use Default value if available
87            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            // Use self's actual value
94            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
128/// Generate schema-only representation for a struct
129fn 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    // Process fields to build runtime schema generation
138    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        // Skip if marked to skip
144        if attrs.skip {
145            continue;
146        }
147
148        // Get field documentation
149        let field_docs = extract_doc_comments(&field.attrs);
150
151        // Check if this is a Vec<T> where T might implement ToPrompt
152        let (is_vec, inner_type) = extract_vec_inner_type(&field.ty);
153
154        // Add comma logic
155        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            // For Vec<T>, try to expand T's schema if T implements ToPrompt
167            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                        // Fallback: just show type name
178                        format!("  \"{}\": \"{}[]\"{}{}", #field_name_str, stringify!(#inner_type).to_lowercase(), #comment, #comma)
179                    } else {
180                        // Expand nested schema inline
181                        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            // Check if this is a custom type that implements ToPrompt (nested object)
200            let field_type = &field.ty;
201            let is_primitive = is_primitive_type(field_type);
202
203            if !is_primitive {
204                // Try to expand nested object schema
205                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                            // Fallback: just show type name
216                            let type_str = stringify!(#field_type).to_lowercase();
217                            format!("  \"{}\": \"{}\"{}{}", #field_name_str, type_str, #comment, #comma)
218                        } else {
219                            // Extract the inner schema content (skip header and outer braces)
220                            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                                // Fallback if parsing fails
227                                let type_str = stringify!(#field_type).to_lowercase();
228                                format!("  \"{}\": \"{}\"{}{}", #field_name_str, type_str, #comment, #comma)
229                            } else {
230                                // Inline the nested schema
231                                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                // Primitive type - use static type formatting
242                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    // Build header
257    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
273/// Extract inner type from Vec<T>, returns (is_vec, inner_type)
274fn 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
286/// Check if a type is a primitive type (should not be expanded as nested object)
287fn 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        // References, arrays, etc. are considered primitive for now
320        true
321    }
322}
323
324/// Format a type for schema representation
325fn format_type_for_schema(ty: &syn::Type) -> String {
326    // Simple type formatting - can be enhanced
327    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                // Handle Option<T>
334                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                // Map common types
342                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
366/// Result of parsing prompt attribute
367enum PromptAttribute {
368    Skip,
369    Description(String),
370    None,
371}
372
373/// Parse #[prompt(...)] attribute on enum variant
374fn parse_prompt_attribute(attrs: &[syn::Attribute]) -> PromptAttribute {
375    for attr in attrs {
376        if attr.path().is_ident("prompt") {
377            // Check for #[prompt(skip)]
378            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            // Check for #[prompt("description")]
387            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/// Parsed field-level prompt attributes
396#[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
405/// Parse #[prompt(...)] attributes for struct fields
406fn 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            // Try to parse as meta list #[prompt(key = value, ...)]
412            if let Ok(meta_list) = attr.meta.require_list() {
413                // Parse the tokens inside the parentheses
414                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                    // Handle simple #[prompt(skip)] case
457                    result.skip = true;
458                } else if meta_list.tokens.to_string() == "image" {
459                    // Handle simple #[prompt(image)] case
460                    result.image = true;
461                }
462            }
463        }
464    }
465
466    result
467}
468
469/// Derives the `ToPrompt` trait for a struct or enum.
470///
471/// This macro provides two main functionalities depending on the type.
472///
473/// ## For Structs
474///
475/// It can generate a prompt based on a template string or by creating a key-value representation of the struct's fields.
476///
477/// ### Template-based Prompt
478///
479/// Use the `#[prompt(template = "...")]` attribute to provide a `minijinja` template. The struct fields will be available as variables in the template. The struct must also derive `serde::Serialize`.
480///
481/// ```rust,ignore
482/// #[derive(ToPrompt, Serialize)]
483/// #[prompt(template = "User {{ name }} is a {{ role }}.")]
484/// struct UserProfile {
485///     name: &'static str,
486///     role: &'static str,
487/// }
488/// ```
489///
490/// ### Tip: Handling Special Characters in Templates
491///
492/// When using raw string literals (e.g., `r#"..."#`) for your templates, be aware of a potential parsing issue if your template content includes the `#` character. To avoid this, use a different number of `#` symbols for the raw string delimiter.
493///
494/// **Problematic Example:**
495/// ```rust,ignore
496/// // This might fail to parse correctly
497/// #[prompt(template = r#"{"color": "#FFFFFF"}"#)]
498/// struct Color { /* ... */ }
499/// ```
500///
501/// **Solution:**
502/// ```rust,ignore
503/// // Use r##"..."## to avoid ambiguity with the inner '#'
504/// #[prompt(template = r##"{"color": "#FFFFFF"}"##)]
505/// struct Color { /* ... */ }
506/// ```
507///
508/// ## For Enums
509///
510/// For enums, the macro generates a descriptive prompt based on doc comments and attributes, outlining the available variants. See the documentation on the `ToPrompt` trait for more details.
511#[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            // Even when it's the same crate, use absolute path to support examples/tests/bins
520            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    // Check if this is a struct or enum
530    match &input.data {
531        Data::Enum(data_enum) => {
532            // For enums, generate prompt from doc comments
533            let enum_name = &input.ident;
534            let enum_docs = extract_doc_comments(&input.attrs);
535
536            let mut prompt_lines = Vec::new();
537
538            // Add enum description
539            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()); // Empty line
545            prompt_lines.push("Possible values:".to_string());
546
547            // Add each variant with its documentation based on priority
548            for variant in &data_enum.variants {
549                let variant_name = &variant.ident;
550
551                // Apply fallback logic with priority
552                match parse_prompt_attribute(&variant.attrs) {
553                    PromptAttribute::Skip => {
554                        // Skip this variant completely
555                        continue;
556                    }
557                    PromptAttribute::Description(desc) => {
558                        // Use custom description from #[prompt("...")]
559                        prompt_lines.push(format!("- {}: {}", variant_name, desc));
560                    }
561                    PromptAttribute::None => {
562                        // Fall back to doc comment or just variant name
563                        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            // Generate match arms for instance-level to_prompt()
577            let mut match_arms = Vec::new();
578            for variant in &data_enum.variants {
579                let variant_name = &variant.ident;
580
581                // Apply same priority logic as schema generation
582                match parse_prompt_attribute(&variant.attrs) {
583                    PromptAttribute::Skip => {
584                        // For skipped variants, return the variant name only
585                        match_arms.push(quote! {
586                            Self::#variant_name => stringify!(#variant_name).to_string()
587                        });
588                    }
589                    PromptAttribute::Description(desc) => {
590                        // Use custom description
591                        match_arms.push(quote! {
592                            Self::#variant_name => format!("{}: {}", stringify!(#variant_name), #desc)
593                        });
594                    }
595                    PromptAttribute::None => {
596                        // Fall back to doc comment or just variant name
597                        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                // Empty enum: no variants to match
613                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            // Parse struct-level prompt attributes for template, template_file, mode, and validate
646            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                    // Try to parse the attribute arguments
654                    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            // Check for mutual exclusivity between template and template_file
695            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            // Load template from file if template_file is specified
703            let template_str = if let Some(file_path) = template_file_attr {
704                // Try multiple strategies to find the template file
705                // This is necessary to support both normal compilation and trybuild tests
706
707                let mut full_path = None;
708
709                // Strategy 1: Try relative to CARGO_MANIFEST_DIR (normal compilation)
710                if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
711                    // Check if this is a trybuild temporary directory
712                    let is_trybuild = manifest_dir.contains("target/tests/trybuild");
713
714                    if !is_trybuild {
715                        // Normal compilation - use CARGO_MANIFEST_DIR directly
716                        let candidate = std::path::Path::new(&manifest_dir).join(&file_path);
717                        if candidate.exists() {
718                            full_path = Some(candidate);
719                        }
720                    } else {
721                        // For trybuild, we need to find the original source directory
722                        // The manifest_dir looks like: .../target/tests/trybuild/llm-toolkit-macros
723                        // We need to get back to the original llm-toolkit-macros source directory
724
725                        // Extract the workspace root from the path
726                        if let Some(target_pos) = manifest_dir.find("/target/tests/trybuild") {
727                            let workspace_root = &manifest_dir[..target_pos];
728                            // Now construct the path to the original llm-toolkit-macros source
729                            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                // Strategy 2: Try as an absolute path or relative to current directory
742                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                // Strategy 3: For trybuild tests - try to find the file by looking in parent directories
750                // This handles the case where trybuild creates a temporary project
751                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                    // Search up to 10 levels up
756                    for _ in 0..10 {
757                        // Try from the llm-toolkit-macros directory
758                        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                        // Try directly
767                        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                // Validate file existence at compile time
781                if full_path.is_none() {
782                    // Build helpful error message with search locations
783                    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                // Read the file at compile time
811                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            // Perform validation if requested
832            if validate_attr && let Some(template) = &template_str {
833                // Validate Jinja syntax
834                let mut env = minijinja::Environment::new();
835                if let Err(e) = env.add_template("validation", template) {
836                    // Generate a compile warning using deprecated const hack
837                    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                    // We'll inject this warning into the generated code
849                    eprintln!("cargo:warning={}", warning_msg);
850                }
851
852                // Extract variables from template and check against struct fields
853                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                // Parse template placeholders
865                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            // Extract struct name and doc comment for use in schema generation
882            let struct_docs = extract_doc_comments(&input.attrs);
883
884            // Check if this is a mode-based struct (mode attribute present)
885            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                // Mode-based generation: support schema_only, example_only, full
890                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                // Check if struct derives Default
901                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                // Generate schema-only parts
913                let schema_parts =
914                    generate_schema_only_parts(&struct_name_str, &struct_docs, fields, &crate_path);
915
916                // Generate example parts
917                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                                    // Combine schema and example
927                                    let mut parts = Vec::new();
928
929                                    // Add schema
930                                    let schema_parts = #schema_parts;
931                                    parts.extend(schema_parts);
932
933                                    // Add separator and example header
934                                    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                                    // Add example
940                                    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                // Use template-based approach if template is provided
983                // Collect image fields separately for to_prompt_parts()
984                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                // Parse template to detect mode syntax
993                let placeholders = parse_template_placeholders_with_mode(&template);
994                // Only use custom mode processing if template actually contains :mode syntax
995                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                        // This field is marked as an image
1009                        image_field_parts.push(quote! {
1010                            parts.extend(self.#field_name.to_prompt_parts());
1011                        });
1012                    }
1013                }
1014
1015                // Generate appropriate code based on whether mode syntax is used
1016                if has_mode_syntax {
1017                    // Build custom context for fields with mode specifications
1018                    let mut context_fields = Vec::new();
1019                    let mut modified_template = template.clone();
1020
1021                    // Process each placeholder with mode
1022                    for (field_name, mode_opt) in &placeholders {
1023                        if let Some(mode) = mode_opt {
1024                            // Create a unique key for this field:mode combination
1025                            let unique_key = format!("{}__{}", field_name, mode);
1026
1027                            // Replace {{ field:mode }} with {{ field__mode }} in template
1028                            let pattern = format!("{{{{ {}:{} }}}}", field_name, mode);
1029                            let replacement = format!("{{{{ {} }}}}", unique_key);
1030                            modified_template = modified_template.replace(&pattern, &replacement);
1031
1032                            // Find the corresponding field
1033                            let field_ident =
1034                                syn::Ident::new(field_name, proc_macro2::Span::call_site());
1035
1036                            // Add to context with mode specification
1037                            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                    // Add individual fields via direct access (for non-mode fields)
1047                    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                        // Skip if this field already has a mode-specific entry
1052                        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                            // Check if field type is likely a struct that implements ToPrompt
1058                            // (not a primitive type)
1059                            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                                // For non-primitive types, use to_prompt()
1100                                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                                // Add image parts first
1116                                #(#image_field_parts)*
1117
1118                                // Build custom context and render template
1119                                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                                // Same logic for to_prompt
1144                                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() // Template-based structs don't have auto-generated schema
1161                            }
1162                        }
1163                    }
1164                } else {
1165                    // No mode syntax, use direct template rendering with render_prompt
1166                    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                                // Add image parts first
1172                                #(#image_field_parts)*
1173
1174                                // Add the rendered template as text
1175                                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() // Template-based structs don't have auto-generated schema
1193                            }
1194                        }
1195                    }
1196                }
1197            } else {
1198                // Use default key-value format if no template is provided
1199                // Now also generate to_prompt_parts() for multimodal support
1200                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                // Separate image fields from text fields
1209                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                    // Skip if #[prompt(skip)] is present
1217                    if attrs.skip {
1218                        continue;
1219                    }
1220
1221                    if attrs.image {
1222                        // This field is marked as an image
1223                        image_field_parts.push(quote! {
1224                            parts.extend(self.#field_name.to_prompt_parts());
1225                        });
1226                    } else {
1227                        // This is a regular text field
1228                        // Determine the key based on priority:
1229                        // 1. #[prompt(rename = "new_name")]
1230                        // 2. Doc comment
1231                        // 3. Field name (fallback)
1232                        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                        // Determine the value based on format_with attribute
1244                        let value_expr = if let Some(format_with) = attrs.format_with {
1245                            // Parse the function path string into a syn::Path
1246                            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                // Generate the implementation with to_prompt_parts()
1262                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                            // Add image parts first
1268                            #(#image_field_parts)*
1269
1270                            // Collect text parts and add as a single text prompt part
1271                            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() // Default key-value format doesn't have auto-generated schema
1289                        }
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/// Information about a prompt target
1303#[derive(Debug, Clone)]
1304struct TargetInfo {
1305    name: String,
1306    template: Option<String>,
1307    field_configs: std::collections::HashMap<String, FieldTargetConfig>,
1308}
1309
1310/// Configuration for how a field should be handled for a specific target
1311#[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, // true if this field is specifically included for this target
1318}
1319
1320/// Parse #[prompt_for(...)] attributes for ToPromptSet
1321fn 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            // Try to parse as meta list
1329            if meta_list.tokens.to_string() == "skip" {
1330                // Simple #[prompt_for(skip)] applies to all targets
1331                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
1392/// Parse struct-level #[prompt_for(...)] attributes to find target templates
1393fn 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            // Even when it's the same crate, use absolute path to support examples/tests/bins
1451            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    // Only support structs with named fields
1461    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    // Parse struct-level attributes to find targets
1486    let mut targets = parse_struct_prompt_for_attrs(&input.attrs);
1487
1488    // Parse field-level attributes
1489    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                // Apply to all targets
1496                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                // Find or create the target
1505                let target_exists = targets.iter().any(|t| t.name == target_name);
1506                if !target_exists {
1507                    // Add implicit target if not defined at struct level
1508                    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    // Generate match arms for each target
1523    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            // Template-based generation
1530            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            // Key-value based generation
1566            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                // Check if field should be included for this target
1574                let config = target.field_configs.get(&field_name_str);
1575
1576                // Skip if explicitly marked to skip
1577                if let Some(cfg) = config
1578                    && cfg.skip
1579                {
1580                    continue;
1581                }
1582
1583                // For non-template targets, only include fields that are:
1584                // 1. Explicitly marked for this target with #[prompt_for(name = "Target")]
1585                // 2. Not marked for any specific target (default fields)
1586                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                            // Parse the function path - if it fails, generate code that will produce a compile error
1605                            match syn::parse_str::<syn::Path>(format_with) {
1606                                Ok(func_path) => quote! { #func_path(&self.#field_name) },
1607                                Err(_) => {
1608                                    // Generate a compile error by using an invalid identifier
1609                                    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                    // Default handling for fields without specific config
1629                    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    // Collect all target names for error reporting
1655    let target_names: Vec<String> = targets.iter().map(|t| t.name.clone()).collect();
1656
1657    // Add default case for unknown targets
1658    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
1684/// Wrapper struct for parsing a comma-separated list of types
1685struct 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/// Generates a formatted Markdown examples section for the provided types.
1698///
1699/// This macro accepts a comma-separated list of types and generates a single
1700/// formatted Markdown string containing examples of each type.
1701///
1702/// # Example
1703///
1704/// ```rust,ignore
1705/// let examples = examples_section!(User, Concept);
1706/// // Produces a string like:
1707/// // ---
1708/// // ### Examples
1709/// //
1710/// // Here are examples of the data structures you should use.
1711/// //
1712/// // ---
1713/// // #### `User`
1714/// // {...json...}
1715/// // ---
1716/// // #### `Concept`
1717/// // {...json...}
1718/// // ---
1719/// ```
1720#[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    // Generate code for each type
1735    let mut type_sections = Vec::new();
1736
1737    for ty in input.types.iter() {
1738        // Extract the type name as a string
1739        let type_name_str = quote!(#ty).to_string();
1740
1741        // Generate the section for this type
1742        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    // Build the complete examples string
1752    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
1772/// Helper function to parse struct-level #[prompt_for(target = "...", template = "...")] attribute
1773fn 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                            // Parse the type string into a syn::Type
1792                            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/// A procedural attribute macro that generates prompt-building functions and extractor structs for intent enums.
1818///
1819/// This macro should be applied to an enum to generate:
1820/// 1. A prompt-building function that incorporates enum documentation
1821/// 2. An extractor struct that implements `IntentExtractor`
1822///
1823/// # Requirements
1824///
1825/// The enum must have an `#[intent(...)]` attribute with:
1826/// - `prompt`: The prompt template (supports Jinja-style variables)
1827/// - `extractor_tag`: The tag to use for extraction
1828///
1829/// # Example
1830///
1831/// ```rust,ignore
1832/// #[define_intent]
1833/// #[intent(
1834///     prompt = "Analyze the intent: {{ user_input }}",
1835///     extractor_tag = "intent"
1836/// )]
1837/// enum MyIntent {
1838///     /// Create a new item
1839///     Create,
1840///     /// Update an existing item
1841///     Update,
1842///     /// Delete an item
1843///     Delete,
1844/// }
1845/// ```
1846///
1847/// This will generate:
1848/// - `pub fn build_my_intent_prompt(user_input: &str) -> String`
1849/// - `pub struct MyIntentExtractor;` with `IntentExtractor<MyIntent>` implementation
1850#[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            // Even when it's the same crate, use absolute path to support examples/tests/bins
1859            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    // Verify this is an enum
1869    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    // Parse the #[intent(...)] attribute
1882    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    // Parse the mode parameter (default to "single")
1927    let mode = mode.unwrap_or_else(|| "single".to_string());
1928
1929    // Validate mode
1930    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    // Validate required attributes
1940    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    // Handle multi_tag mode
1953    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    // Continue with single mode logic
1966    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    // Generate the intents documentation
1979    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    // Add enum description if present
1985    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()); // Empty line
1991    intents_doc_lines.push("Possible values:".to_string());
1992
1993    // Add each variant with its documentation
1994    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    // Parse template variables (excluding intents_doc which we'll inject)
2008    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    // Generate function name (snake_case)
2021    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    // Generate function parameters (all &str for simplicity)
2029    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    // Generate context insertions
2038    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    // Template is already in Jinja syntax, no conversion needed
2050    let converted_template = prompt_template.clone();
2051
2052    // Generate extractor struct name
2053    let extractor_name = syn::Ident::new(
2054        &format!("{}Extractor", enum_name),
2055        proc_macro2::Span::call_site(),
2056    );
2057
2058    // Filter out the #[intent(...)] attribute from the enum attributes
2059    let filtered_attrs: Vec<_> = input
2060        .attrs
2061        .iter()
2062        .filter(|attr| !attr.path().is_ident("intent"))
2063        .collect();
2064
2065    // Rebuild the enum with filtered attributes
2066    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    // Generate the complete output
2077    let expanded = quote! {
2078        // Output the enum without the #[intent(...)] attribute
2079        #enum_output
2080
2081        // Generate the prompt-building function
2082        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            // Add intents_doc
2092            __template_context.insert("intents_doc".to_string(), minijinja::Value::from(#intents_doc_str));
2093
2094            // Add user-provided variables
2095            #(#context_insertions)*
2096
2097            tmpl.render(&__template_context)
2098                .unwrap_or_else(|e| format!("Failed to render intent prompt: {}", e))
2099        }
2100
2101        // Generate the extractor struct
2102        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                // Use the common extraction function with our tag
2111                #crate_path::intent::extract_intent_from_response(response, Self::EXTRACTOR_TAG)
2112            }
2113        }
2114    };
2115
2116    TokenStream::from(expanded)
2117}
2118
2119/// Convert PascalCase to snake_case
2120fn 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/// Parse #[action(...)] attributes for enum variants
2141#[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/// Parse #[action(...)] attributes for struct fields in variants
2173#[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
2198/// Generate actions_doc for multi_tag mode
2199fn 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                    // Simple tag without parameters
2213                    doc_lines.push(format!("- `<{} />`: {}", tag, variant_docs));
2214                }
2215                syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
2216                    // Tuple variant with inner text
2217                    doc_lines.push(format!("- `<{}>...</{}>`: {}", tag, tag, variant_docs));
2218                }
2219                syn::Fields::Named(fields) => {
2220                    // Struct variant with attributes and/or inner text
2221                    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                    // Add field documentation
2253                    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                    // Other field types not supported
2269                }
2270            }
2271        }
2272    }
2273
2274    doc_lines.join("\n")
2275}
2276
2277/// Generate regex for matching any of the defined action tags
2278fn 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    // Match both self-closing tags like <Tag /> and content-based tags like <Tag>...</Tag>
2296    // (?is) enables case-insensitive and single-line mode where . matches newlines
2297    format!(
2298        r"(?is)<(?:{})\b[^>]*/>|<(?:{})\b[^>]*>.*?</(?:{})>",
2299        tags_pattern, tags_pattern, tags_pattern
2300    )
2301}
2302
2303/// Generate output for multi_tag mode
2304fn 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            // Even when it's the same crate, use absolute path to support examples/tests/bins
2316            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    // Parse template placeholders
2326    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    // Generate function name (snake_case)
2339    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    // Generate function parameters (all &str for simplicity)
2347    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    // Generate context insertions
2356    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    // Generate extractor struct name
2368    let extractor_name = syn::Ident::new(
2369        &format!("{}Extractor", enum_name),
2370        proc_macro2::Span::call_site(),
2371    );
2372
2373    // Filter out the #[intent(...)] and #[action(...)] attributes
2374    let filtered_attrs: Vec<_> = input
2375        .attrs
2376        .iter()
2377        .filter(|attr| !attr.path().is_ident("intent"))
2378        .collect();
2379
2380    // Filter action attributes from variants
2381    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            // Filter field attributes
2394            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    // Generate XML parsing logic for extract_actions
2441    let parsing_arms = generate_parsing_arms(&enum_data.variants, enum_name);
2442
2443    // Generate the regex pattern for matching tags
2444    let tags_regex = generate_tags_regex(&enum_data.variants);
2445
2446    let expanded = quote! {
2447        // Output the enum without the #[intent(...)] and #[action(...)] attributes
2448        #(#filtered_attrs)*
2449        #vis enum #enum_name #generics {
2450            #(#filtered_variants),*
2451        }
2452
2453        // Generate the prompt-building function
2454        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            // Add actions_doc
2464            __template_context.insert("actions_doc".to_string(), minijinja::Value::from(#actions_doc));
2465
2466            // Add user-provided variables
2467            #(#context_insertions)*
2468
2469            tmpl.render(&__template_context)
2470                .unwrap_or_else(|e| format!("Failed to render intent prompt: {}", e))
2471        }
2472
2473        // Generate the extractor struct
2474        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                            // Silently ignore XML parsing errors
2506                            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                            // Silently ignore XML parsing errors
2545                            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(&regex_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                    // Try to parse the matched tag as an action
2574                    if let Some(action) = self.parse_single_action(matched) {
2575                        transformer(action)
2576                    } else {
2577                        // If parsing fails, return the original text
2578                        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
2592/// Generate parsing arms for XML extraction
2593fn 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                    // Simple tag without parameters
2607                    arms.push(quote! {
2608                        if &tag_name == #tag {
2609                            actions.push(#enum_name::#variant_name);
2610                        }
2611                    });
2612                }
2613                syn::Fields::Unnamed(_fields) => {
2614                    // Tuple variant with inner text - use reader.read_text()
2615                    arms.push(quote! {
2616                        if &tag_name == #tag && !is_empty {
2617                            // Use read_text to get inner text as owned String
2618                            match reader.read_text(owned_e.name()) {
2619                                Ok(text) => {
2620                                    actions.push(#enum_name::#variant_name(text.to_string()));
2621                                }
2622                                Err(_) => {
2623                                    // If reading text fails, push empty string
2624                                    actions.push(#enum_name::#variant_name(String::new()));
2625                                }
2626                            }
2627                        }
2628                    });
2629                }
2630                syn::Fields::Named(fields) => {
2631                    // Struct variant with attributes and/or inner text
2632                    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                        // Handle inner text
2648                        // Build attribute extraction code
2649                        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                                // Check if it's a self-closing tag
2668                                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                                    // Use read_text to get inner text as owned String
2676                                    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                                            // If reading text fails, push with empty string
2686                                            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                        // Only attributes
2698                        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/// Derives the `ToPromptFor` trait for a struct
2732#[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            // Even when it's the same crate, use absolute path to support examples/tests/bins
2741            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    // Parse the struct-level prompt_for attribute
2751    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    // Parse the template to find placeholders
2757    let placeholders = parse_template_placeholders_with_mode(&template);
2758
2759    // Convert template to minijinja syntax and build context generation code
2760    let mut converted_template = template.clone();
2761    let mut context_fields = Vec::new();
2762
2763    // Get struct fields for validation
2764    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    // Check if the struct has mode support (has #[prompt(mode = ...)] attribute)
2773    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    // Process each placeholder
2790    for (placeholder_name, mode_opt) in &placeholders {
2791        if placeholder_name == "self" {
2792            if let Some(specific_mode) = mode_opt {
2793                // {self:some_mode} - use a unique key
2794                let unique_key = format!("self__{}", specific_mode);
2795
2796                // Replace {{ self:mode }} with {{ self__mode }} in template
2797                let pattern = format!("{{{{ self:{} }}}}", specific_mode);
2798                let replacement = format!("{{{{ {} }}}}", unique_key);
2799                converted_template = converted_template.replace(&pattern, &replacement);
2800
2801                // Add to context with the specific mode
2802                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                // {{self}} - already in correct format, no replacement needed
2810
2811                if has_mode_support {
2812                    // If the struct has mode support, use to_prompt_with_mode with the mode parameter
2813                    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                    // If the struct doesn't have mode support, use to_prompt() which gives key-value format
2821                    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            // It's a field placeholder
2831            // Check if the field exists
2832            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                // {{field}} - already in correct format, no replacement needed
2842
2843                // Add field to context - serialize the field value
2844                context_fields.push(quote! {
2845                    context.insert(
2846                        #placeholder_name.to_string(),
2847                        minijinja::Value::from_serialize(&self.#field_ident)
2848                    );
2849                });
2850            }
2851            // If field doesn't exist, we'll let minijinja handle the error at runtime
2852        }
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                // Create minijinja environment and add template
2862                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                // Build context
2870                let mut context = std::collections::HashMap::new();
2871                // Add self to the context for field access in templates
2872                context.insert(
2873                    "self".to_string(),
2874                    minijinja::Value::from_serialize(self)
2875                );
2876                // Add target to the context
2877                context.insert(
2878                    "target".to_string(),
2879                    minijinja::Value::from_serialize(target)
2880                );
2881                #(#context_fields)*
2882
2883                // Render template
2884                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
2894// ============================================================================
2895// Agent Derive Macro
2896// ============================================================================
2897
2898/// Attribute parameters for #[agent(...)]
2899struct 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
3015/// Parse #[agent(...)] attributes from a struct
3016fn 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
3035/// Generate backend-specific convenience constructors
3036fn 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                    /// Create a new agent with ClaudeCodeAgent backend
3048                    pub fn with_claude() -> Self {
3049                        Self::new(#crate_path::agent::impls::ClaudeCodeAgent::new())
3050                    }
3051
3052                    /// Create a new agent with ClaudeCodeAgent backend and specific model
3053                    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                    /// Create a new agent with GeminiAgent backend
3066                    pub fn with_gemini() -> Self {
3067                        Self::new(#crate_path::agent::impls::GeminiAgent::new())
3068                    }
3069
3070                    /// Create a new agent with GeminiAgent backend and specific model
3071                    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
3084/// Generate Default implementation for the agent
3085fn 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    // Parse profile string to ExecutionProfile
3093    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 }, // Default fallback
3099        }
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            // Default to Claude
3117            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/// Derive macro for implementing the Agent trait
3138///
3139/// # Usage
3140/// ```ignore
3141/// #[derive(Agent)]
3142/// #[agent(expertise = "Rust expert", output = "MyOutputType")]
3143/// struct MyAgent;
3144/// ```
3145#[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    // Parse #[agent(...)] attributes
3151    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; // Not used in simple derive macro
3167    let max_retries = agent_attrs.max_retries.unwrap_or(3); // Default: 3 retries
3168
3169    // Determine crate path
3170    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            // Even when it's the same crate, use absolute path to support examples/tests/bins
3175            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    // Check if output type is String (no JSON enforcement needed)
3187    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    // Generate enhanced expertise with JSON schema instruction
3191    let enhanced_expertise = if is_string_output {
3192        // Plain text output - no JSON enforcement
3193        quote! { #expertise }
3194    } else {
3195        // Structured output - try to use ToPrompt::prompt_schema(), fallback to type name
3196        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                    // Try to get detailed schema from ToPrompt
3204                    let schema = <#output_type as #crate_path::prompt::ToPrompt>::prompt_schema();
3205
3206                    if schema.is_empty() {
3207                        // Fallback: type name only
3208                        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                        // Use detailed schema from ToPrompt
3218                        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    // Generate agent initialization code based on backend
3232    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            // Default to Claude
3261            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                // Create internal agent based on backend configuration
3286                #agent_init
3287
3288                // Retry configuration
3289                let max_retries: u32 = #max_retries;
3290                let mut attempts = 0u32;
3291
3292                loop {
3293                    attempts += 1;
3294
3295                    // Execute and get response
3296                    let result = async {
3297                        let response = agent.execute(intent.clone()).await?;
3298
3299                        // Extract JSON from the response
3300                        let json_str = #crate_path::extract_json(&response)
3301                            .map_err(|e| #crate_path::agent::AgentError::ParseError(e.to_string()))?;
3302
3303                        // Deserialize into output type
3304                        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 retry attempt
3312                            log::warn!(
3313                                "Agent execution failed (attempt {}/{}): {}. Retrying...",
3314                                attempts,
3315                                max_retries,
3316                                e
3317                            );
3318
3319                            // Simple fixed delay: 100ms * attempt number
3320                            let delay_ms = 100 * attempts as u64;
3321                            tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
3322
3323                            // Continue to next iteration
3324                            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                // Create internal agent and check availability
3342                #agent_init
3343                agent.is_available().await
3344            }
3345        }
3346    };
3347
3348    TokenStream::from(expanded)
3349}
3350
3351// ============================================================================
3352// Agent Attribute Macro (Generic version with injection support)
3353// ============================================================================
3354
3355/// Attribute macro for implementing the Agent trait with Generic support
3356///
3357/// This version generates a struct definition with Generic inner agent,
3358/// allowing for agent injection and testing with mock agents.
3359///
3360/// # Usage
3361/// ```ignore
3362/// #[agent(expertise = "Rust expert", output = "MyOutputType")]
3363/// struct MyAgent;
3364/// ```
3365#[proc_macro_attribute]
3366pub fn agent(attr: TokenStream, item: TokenStream) -> TokenStream {
3367    // Parse attributes
3368    let agent_attrs = match syn::parse::<AgentAttrs>(attr) {
3369        Ok(attrs) => attrs,
3370        Err(e) => return e.to_compile_error().into(),
3371    };
3372
3373    // Parse the input struct
3374    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    // Check if output type is String (no JSON enforcement needed)
3391    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    // Determine crate path
3395    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    // Determine generic parameter name for inner agent (default: "A")
3409    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    // Determine default agent type - prioritize default_inner, fallback to backend
3413    let default_agent_type = if let Some(ref custom_type) = agent_attrs.default_inner {
3414        // Custom type specified via default_inner attribute
3415        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        // Use backend to determine default type
3420        match backend.as_str() {
3421            "gemini" => quote! { #crate_path::agent::impls::GeminiAgent },
3422            _ => quote! { #crate_path::agent::impls::ClaudeCodeAgent },
3423        }
3424    };
3425
3426    // Generate struct definition
3427    let struct_def = quote! {
3428        #vis struct #struct_name<#inner_generic_ident = #default_agent_type> {
3429            inner: #inner_generic_ident,
3430        }
3431    };
3432
3433    // Generate basic constructor
3434    let constructors = quote! {
3435        impl<#inner_generic_ident> #struct_name<#inner_generic_ident> {
3436            /// Create a new agent with a custom inner agent implementation
3437            pub fn new(inner: #inner_generic_ident) -> Self {
3438                Self { inner }
3439            }
3440        }
3441    };
3442
3443    // Generate backend-specific constructors and Default implementation
3444    let (backend_constructors, default_impl) = if agent_attrs.default_inner.is_some() {
3445        // Custom type - generate Default impl for the default type
3446        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        // Built-in backend - generate backend-specific constructors
3458        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    // Generate enhanced expertise with JSON schema instruction (same as derive macro)
3476    let enhanced_expertise = if is_string_output {
3477        // Plain text output - no JSON enforcement
3478        quote! { #expertise }
3479    } else {
3480        // Structured output - try to use ToPrompt::prompt_schema(), fallback to type name
3481        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                    // Try to get detailed schema from ToPrompt
3489                    let schema = <#output_type as #crate_path::prompt::ToPrompt>::prompt_schema();
3490
3491                    if schema.is_empty() {
3492                        // Fallback: type name only
3493                        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                        // Use detailed schema from ToPrompt
3503                        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    // Generate Agent trait implementation
3517    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                // Prepend expertise to the payload
3531                let enhanced_payload = intent.prepend_text(self.expertise());
3532
3533                // Use the inner agent with the enhanced payload
3534                let response = self.inner.execute(enhanced_payload).await?;
3535
3536                // Extract JSON from the response
3537                let json_str = #crate_path::extract_json(&response)
3538                    .map_err(|e| #crate_path::agent::AgentError::ParseError(e.to_string()))?;
3539
3540                // Deserialize into output type
3541                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}