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 __type field - it's metadata that shouldn't be in examples
75        // It's typically marked with #[serde(skip_serializing)] or #[serde(default)]
76        // and won't appear in actual JSON output
77        if field_name_str == "__type" {
78            continue;
79        }
80
81        // Skip if marked to skip
82        if attrs.skip {
83            continue;
84        }
85
86        // Check if field has example attribute
87        if let Some(example) = attrs.example {
88            // Use the provided example value
89            field_values.push(quote! {
90                json_obj.insert(#field_name_str.to_string(), serde_json::Value::String(#example.to_string()));
91            });
92        } else if has_default {
93            // Use Default value if available
94            field_values.push(quote! {
95                let default_value = serde_json::to_value(&default_instance.#field_name)
96                    .unwrap_or(serde_json::Value::Null);
97                json_obj.insert(#field_name_str.to_string(), default_value);
98            });
99        } else {
100            // Use self's actual value
101            field_values.push(quote! {
102                let value = serde_json::to_value(&self.#field_name)
103                    .unwrap_or(serde_json::Value::Null);
104                json_obj.insert(#field_name_str.to_string(), value);
105            });
106        }
107    }
108
109    if has_default {
110        quote! {
111            {
112                let default_instance = Self::default();
113                let mut json_obj = serde_json::Map::new();
114                #(#field_values)*
115                let json_value = serde_json::Value::Object(json_obj);
116                let json_str = serde_json::to_string_pretty(&json_value)
117                    .unwrap_or_else(|_| "{}".to_string());
118                vec![#crate_path::prompt::PromptPart::Text(json_str)]
119            }
120        }
121    } else {
122        quote! {
123            {
124                let mut json_obj = serde_json::Map::new();
125                #(#field_values)*
126                let json_value = serde_json::Value::Object(json_obj);
127                let json_str = serde_json::to_string_pretty(&json_value)
128                    .unwrap_or_else(|_| "{}".to_string());
129                vec![#crate_path::prompt::PromptPart::Text(json_str)]
130            }
131        }
132    }
133}
134
135/// Generate schema-only representation for a struct
136fn generate_schema_only_parts(
137    struct_name: &str,
138    struct_docs: &str,
139    fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
140    crate_path: &proc_macro2::TokenStream,
141    _has_type_marker: bool,
142) -> proc_macro2::TokenStream {
143    let mut field_schema_parts = vec![];
144    let mut nested_type_collectors = vec![];
145
146    // Process fields to build runtime schema generation
147    for field in fields.iter() {
148        let field_name = field.ident.as_ref().unwrap();
149        let field_name_str = field_name.to_string();
150        let attrs = parse_field_prompt_attrs(&field.attrs);
151
152        // Skip __type field - it's metadata that shouldn't be in the schema
153        // LLMs misinterpret "__type": "string" as "output the literal string 'string'"
154        // The __type field will be automatically added during deserialization via #[serde(default)]
155        if field_name_str == "__type" {
156            continue;
157        }
158
159        // Skip if marked to skip
160        if attrs.skip {
161            continue;
162        }
163
164        // Get field documentation
165        let field_docs = extract_doc_comments(&field.attrs);
166
167        // Check if this is a Vec<T> where T might implement ToPrompt
168        let (is_vec, inner_type) = extract_vec_inner_type(&field.ty);
169
170        if is_vec {
171            // For Vec<T>, use TypeScript array syntax: T[]
172            // Format: field_name: TypeName[];  // comment
173            let comment = if !field_docs.is_empty() {
174                format!("  // {}", field_docs)
175            } else {
176                String::new()
177            };
178
179            field_schema_parts.push(quote! {
180                {
181                    let type_name = stringify!(#inner_type);
182                    format!("  {}: {}[];{}", #field_name_str, type_name, #comment)
183                }
184            });
185
186            // Collect nested type schema if not primitive
187            if let Some(inner) = inner_type
188                && !is_primitive_type(inner)
189            {
190                nested_type_collectors.push(quote! {
191                    <#inner as #crate_path::prompt::ToPrompt>::prompt_schema()
192                });
193            }
194        } else {
195            // Check if this is a custom type that implements ToPrompt (nested object)
196            let field_type = &field.ty;
197            let is_primitive = is_primitive_type(field_type);
198
199            if !is_primitive {
200                // For nested objects, use TypeScript type reference AND collect nested schema
201                // Format: field_name: TypeName;  // comment
202                let comment = if !field_docs.is_empty() {
203                    format!("  // {}", field_docs)
204                } else {
205                    String::new()
206                };
207
208                field_schema_parts.push(quote! {
209                    {
210                        let type_name = stringify!(#field_type);
211                        format!("  {}: {};{}", #field_name_str, type_name, #comment)
212                    }
213                });
214
215                // Collect nested type schema for type definitions section
216                nested_type_collectors.push(quote! {
217                    <#field_type as #crate_path::prompt::ToPrompt>::prompt_schema()
218                });
219            } else {
220                // Primitive type - use TypeScript formatting
221                // Format: field_name: type;  // comment
222                let type_str = format_type_for_schema(&field.ty);
223                let comment = if !field_docs.is_empty() {
224                    format!("  // {}", field_docs)
225                } else {
226                    String::new()
227                };
228
229                field_schema_parts.push(quote! {
230                    format!("  {}: {};{}", #field_name_str, #type_str, #comment)
231                });
232            }
233        }
234    }
235
236    // Build TypeScript-style type definitions with nested types first
237    // Format:
238    // type NestedType1 = { ... }
239    //
240    // type NestedType2 = { ... }
241    //
242    // /**
243    //  * Struct description
244    //  */
245    // type StructName = {
246    //   field1: NestedType1;  // comment1
247    //   field2: NestedType2;  // comment2
248    // }
249
250    let mut header_lines = Vec::new();
251
252    // Add JSDoc comment if struct has description
253    if !struct_docs.is_empty() {
254        header_lines.push("/**".to_string());
255        header_lines.push(format!(" * {}", struct_docs));
256        header_lines.push(" */".to_string());
257    }
258
259    // Add type definition line
260    header_lines.push(format!("type {} = {{", struct_name));
261
262    quote! {
263        {
264            let mut all_lines: Vec<String> = Vec::new();
265
266            // Collect nested type definitions
267            let nested_schemas: Vec<String> = vec![#(#nested_type_collectors),*];
268            let mut seen_types = std::collections::HashSet::<String>::new();
269
270            for schema in nested_schemas {
271                if !schema.is_empty() {
272                    // Avoid duplicates by checking if we've seen this schema
273                    if seen_types.insert(schema.clone()) {
274                        all_lines.push(schema);
275                        all_lines.push(String::new());  // Empty line separator
276                    }
277                }
278            }
279
280            // Add main type definition
281            let mut lines: Vec<String> = Vec::new();
282            #(lines.push(#header_lines.to_string());)*
283            #(lines.push(#field_schema_parts);)*
284            lines.push("}".to_string());
285            all_lines.push(lines.join("\n"));
286
287            vec![#crate_path::prompt::PromptPart::Text(all_lines.join("\n"))]
288        }
289    }
290}
291
292/// Extract inner type from Vec<T>, returns (is_vec, inner_type)
293fn extract_vec_inner_type(ty: &syn::Type) -> (bool, Option<&syn::Type>) {
294    if let syn::Type::Path(type_path) = ty
295        && let Some(last_segment) = type_path.path.segments.last()
296        && last_segment.ident == "Vec"
297        && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
298        && let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
299    {
300        return (true, Some(inner_type));
301    }
302    (false, None)
303}
304
305/// Check if a type is a primitive type (should not be expanded as nested object)
306fn is_primitive_type(ty: &syn::Type) -> bool {
307    if let syn::Type::Path(type_path) = ty
308        && let Some(last_segment) = type_path.path.segments.last()
309    {
310        let type_name = last_segment.ident.to_string();
311        matches!(
312            type_name.as_str(),
313            "String"
314                | "str"
315                | "i8"
316                | "i16"
317                | "i32"
318                | "i64"
319                | "i128"
320                | "isize"
321                | "u8"
322                | "u16"
323                | "u32"
324                | "u64"
325                | "u128"
326                | "usize"
327                | "f32"
328                | "f64"
329                | "bool"
330                | "Vec"
331                | "Option"
332                | "HashMap"
333                | "BTreeMap"
334                | "HashSet"
335                | "BTreeSet"
336        )
337    } else {
338        // References, arrays, etc. are considered primitive for now
339        true
340    }
341}
342
343/// Format a type for schema representation
344fn format_type_for_schema(ty: &syn::Type) -> String {
345    // Simple type formatting - can be enhanced
346    match ty {
347        syn::Type::Path(type_path) => {
348            let path = &type_path.path;
349            if let Some(last_segment) = path.segments.last() {
350                let type_name = last_segment.ident.to_string();
351
352                // Handle Option<T>
353                if type_name == "Option"
354                    && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
355                    && let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
356                {
357                    return format!("{} | null", format_type_for_schema(inner_type));
358                }
359
360                // Map common types
361                match type_name.as_str() {
362                    "String" | "str" => "string".to_string(),
363                    "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32"
364                    | "u64" | "u128" | "usize" => "number".to_string(),
365                    "f32" | "f64" => "number".to_string(),
366                    "bool" => "boolean".to_string(),
367                    "Vec" => {
368                        if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
369                            && let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
370                        {
371                            return format!("{}[]", format_type_for_schema(inner_type));
372                        }
373                        "array".to_string()
374                    }
375                    _ => type_name.to_lowercase(),
376                }
377            } else {
378                "unknown".to_string()
379            }
380        }
381        _ => "unknown".to_string(),
382    }
383}
384
385/// Result of parsing prompt attributes on a variant
386#[derive(Default)]
387struct PromptAttributes {
388    skip: bool,
389    rename: Option<String>,
390    description: Option<String>,
391}
392
393/// Parse #[prompt(...)] attributes on enum variant
394/// Collects all prompt attributes (rename, description, skip) from multiple attributes
395fn parse_prompt_attributes(attrs: &[syn::Attribute]) -> PromptAttributes {
396    let mut result = PromptAttributes::default();
397
398    for attr in attrs {
399        if attr.path().is_ident("prompt") {
400            // Check for #[prompt(rename = "...")], #[prompt(description = "...")], etc.
401            if let Ok(meta_list) = attr.meta.require_list() {
402                // Try parsing as key-value pairs
403                if let Ok(metas) =
404                    meta_list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
405                {
406                    for meta in metas {
407                        if let Meta::NameValue(nv) = meta {
408                            if nv.path.is_ident("rename") {
409                                if let syn::Expr::Lit(syn::ExprLit {
410                                    lit: syn::Lit::Str(lit_str),
411                                    ..
412                                }) = nv.value
413                                {
414                                    result.rename = Some(lit_str.value());
415                                }
416                            } else if nv.path.is_ident("description")
417                                && let syn::Expr::Lit(syn::ExprLit {
418                                    lit: syn::Lit::Str(lit_str),
419                                    ..
420                                }) = nv.value
421                            {
422                                result.description = Some(lit_str.value());
423                            }
424                        } else if let Meta::Path(path) = meta
425                            && path.is_ident("skip")
426                        {
427                            result.skip = true;
428                        }
429                    }
430                }
431
432                // Fallback: check for simple #[prompt(skip)]
433                let tokens_str = meta_list.tokens.to_string();
434                if tokens_str == "skip" {
435                    result.skip = true;
436                }
437            }
438
439            // Check for #[prompt("description")] (shorthand)
440            if let Ok(lit_str) = attr.parse_args::<syn::LitStr>() {
441                result.description = Some(lit_str.value());
442            }
443        }
444    }
445    result
446}
447
448/// Parse #[serde(rename = "...")] attribute on enum variant
449fn parse_serde_variant_rename(attrs: &[syn::Attribute]) -> Option<String> {
450    for attr in attrs {
451        if attr.path().is_ident("serde")
452            && let Ok(meta_list) = attr.meta.require_list()
453            && let Ok(metas) =
454                meta_list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
455        {
456            for meta in metas {
457                if let Meta::NameValue(nv) = meta
458                    && nv.path.is_ident("rename")
459                    && let syn::Expr::Lit(syn::ExprLit {
460                        lit: syn::Lit::Str(lit_str),
461                        ..
462                    }) = nv.value
463                {
464                    return Some(lit_str.value());
465                }
466            }
467        }
468    }
469    None
470}
471
472/// Serde rename rules
473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
474enum RenameRule {
475    #[allow(dead_code)]
476    None,
477    LowerCase,
478    UpperCase,
479    PascalCase,
480    CamelCase,
481    SnakeCase,
482    ScreamingSnakeCase,
483    KebabCase,
484    ScreamingKebabCase,
485}
486
487impl RenameRule {
488    /// Parse from serde rename_all string
489    fn from_str(s: &str) -> Option<Self> {
490        match s {
491            "lowercase" => Some(Self::LowerCase),
492            "UPPERCASE" => Some(Self::UpperCase),
493            "PascalCase" => Some(Self::PascalCase),
494            "camelCase" => Some(Self::CamelCase),
495            "snake_case" => Some(Self::SnakeCase),
496            "SCREAMING_SNAKE_CASE" => Some(Self::ScreamingSnakeCase),
497            "kebab-case" => Some(Self::KebabCase),
498            "SCREAMING-KEBAB-CASE" => Some(Self::ScreamingKebabCase),
499            _ => None,
500        }
501    }
502
503    /// Apply rename rule to a variant name
504    fn apply(&self, name: &str) -> String {
505        match self {
506            Self::None => name.to_string(),
507            Self::LowerCase => name.to_lowercase(),
508            Self::UpperCase => name.to_uppercase(),
509            Self::PascalCase => name.to_string(), // PascalCase is the Rust default
510            Self::CamelCase => {
511                // Convert PascalCase to camelCase
512                let mut chars = name.chars();
513                match chars.next() {
514                    None => String::new(),
515                    Some(first) => first.to_lowercase().chain(chars).collect(),
516                }
517            }
518            Self::SnakeCase => {
519                // Convert PascalCase to snake_case
520                let mut result = String::new();
521                for (i, ch) in name.chars().enumerate() {
522                    if ch.is_uppercase() && i > 0 {
523                        result.push('_');
524                    }
525                    result.push(ch.to_lowercase().next().unwrap());
526                }
527                result
528            }
529            Self::ScreamingSnakeCase => {
530                // Convert PascalCase to SCREAMING_SNAKE_CASE
531                let mut result = String::new();
532                for (i, ch) in name.chars().enumerate() {
533                    if ch.is_uppercase() && i > 0 {
534                        result.push('_');
535                    }
536                    result.push(ch.to_uppercase().next().unwrap());
537                }
538                result
539            }
540            Self::KebabCase => {
541                // Convert PascalCase to kebab-case
542                let mut result = String::new();
543                for (i, ch) in name.chars().enumerate() {
544                    if ch.is_uppercase() && i > 0 {
545                        result.push('-');
546                    }
547                    result.push(ch.to_lowercase().next().unwrap());
548                }
549                result
550            }
551            Self::ScreamingKebabCase => {
552                // Convert PascalCase to SCREAMING-KEBAB-CASE
553                let mut result = String::new();
554                for (i, ch) in name.chars().enumerate() {
555                    if ch.is_uppercase() && i > 0 {
556                        result.push('-');
557                    }
558                    result.push(ch.to_uppercase().next().unwrap());
559                }
560                result
561            }
562        }
563    }
564}
565
566/// Parse #[serde(rename_all = "...")] attribute on enum/struct
567fn parse_serde_rename_all(attrs: &[syn::Attribute]) -> Option<RenameRule> {
568    for attr in attrs {
569        if attr.path().is_ident("serde")
570            && let Ok(meta_list) = attr.meta.require_list()
571        {
572            // Parse the tokens inside the parentheses
573            if let Ok(metas) =
574                meta_list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
575            {
576                for meta in metas {
577                    if let Meta::NameValue(nv) = meta
578                        && nv.path.is_ident("rename_all")
579                        && let syn::Expr::Lit(syn::ExprLit {
580                            lit: syn::Lit::Str(lit_str),
581                            ..
582                        }) = nv.value
583                    {
584                        return RenameRule::from_str(&lit_str.value());
585                    }
586                }
587            }
588        }
589    }
590    None
591}
592
593/// Parsed field-level prompt attributes
594#[derive(Debug, Default)]
595struct FieldPromptAttrs {
596    skip: bool,
597    rename: Option<String>,
598    format_with: Option<String>,
599    image: bool,
600    example: Option<String>,
601}
602
603/// Parse #[prompt(...)] attributes for struct fields
604fn parse_field_prompt_attrs(attrs: &[syn::Attribute]) -> FieldPromptAttrs {
605    let mut result = FieldPromptAttrs::default();
606
607    for attr in attrs {
608        if attr.path().is_ident("prompt") {
609            // Try to parse as meta list #[prompt(key = value, ...)]
610            if let Ok(meta_list) = attr.meta.require_list() {
611                // Parse the tokens inside the parentheses
612                if let Ok(metas) =
613                    meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
614                {
615                    for meta in metas {
616                        match meta {
617                            Meta::Path(path) if path.is_ident("skip") => {
618                                result.skip = true;
619                            }
620                            Meta::NameValue(nv) if nv.path.is_ident("rename") => {
621                                if let syn::Expr::Lit(syn::ExprLit {
622                                    lit: syn::Lit::Str(lit_str),
623                                    ..
624                                }) = nv.value
625                                {
626                                    result.rename = Some(lit_str.value());
627                                }
628                            }
629                            Meta::NameValue(nv) if nv.path.is_ident("format_with") => {
630                                if let syn::Expr::Lit(syn::ExprLit {
631                                    lit: syn::Lit::Str(lit_str),
632                                    ..
633                                }) = nv.value
634                                {
635                                    result.format_with = Some(lit_str.value());
636                                }
637                            }
638                            Meta::Path(path) if path.is_ident("image") => {
639                                result.image = true;
640                            }
641                            Meta::NameValue(nv) if nv.path.is_ident("example") => {
642                                if let syn::Expr::Lit(syn::ExprLit {
643                                    lit: syn::Lit::Str(lit_str),
644                                    ..
645                                }) = nv.value
646                                {
647                                    result.example = Some(lit_str.value());
648                                }
649                            }
650                            _ => {}
651                        }
652                    }
653                } else if meta_list.tokens.to_string() == "skip" {
654                    // Handle simple #[prompt(skip)] case
655                    result.skip = true;
656                } else if meta_list.tokens.to_string() == "image" {
657                    // Handle simple #[prompt(image)] case
658                    result.image = true;
659                }
660            }
661        }
662    }
663
664    result
665}
666
667/// Derives the `ToPrompt` trait for a struct or enum.
668///
669/// This macro provides two main functionalities depending on the type.
670///
671/// ## For Structs
672///
673/// It can generate a prompt based on a template string or by creating a key-value representation of the struct's fields.
674///
675/// ### Template-based Prompt
676///
677/// 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`.
678///
679/// ```rust,ignore
680/// #[derive(ToPrompt, Serialize)]
681/// #[prompt(template = "User {{ name }} is a {{ role }}.")]
682/// struct UserProfile {
683///     name: &'static str,
684///     role: &'static str,
685/// }
686/// ```
687///
688/// ### Tip: Handling Special Characters in Templates
689///
690/// 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.
691///
692/// **Problematic Example:**
693/// ```rust,ignore
694/// // This might fail to parse correctly
695/// #[prompt(template = r#"{"color": "#FFFFFF"}"#)]
696/// struct Color { /* ... */ }
697/// ```
698///
699/// **Solution:**
700/// ```rust,ignore
701/// // Use r##"..."## to avoid ambiguity with the inner '#'
702/// #[prompt(template = r##"{"color": "#FFFFFF"}"##)]
703/// struct Color { /* ... */ }
704/// ```
705///
706/// ## For Enums
707///
708/// 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.
709#[proc_macro_derive(ToPrompt, attributes(prompt))]
710pub fn to_prompt_derive(input: TokenStream) -> TokenStream {
711    let input = parse_macro_input!(input as DeriveInput);
712
713    let found_crate =
714        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
715    let crate_path = match found_crate {
716        FoundCrate::Itself => {
717            // Even when it's the same crate, use absolute path to support examples/tests/bins
718            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
719            quote!(::#ident)
720        }
721        FoundCrate::Name(name) => {
722            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
723            quote!(::#ident)
724        }
725    };
726
727    // Check if this is a struct or enum
728    match &input.data {
729        Data::Enum(data_enum) => {
730            // For enums, generate prompt from doc comments
731            let enum_name = &input.ident;
732            let enum_docs = extract_doc_comments(&input.attrs);
733
734            // Check for #[serde(rename_all = "...")] attribute
735            let rename_rule = parse_serde_rename_all(&input.attrs);
736
737            // Generate TypeScript-style union type with descriptions
738            // Format:
739            // /**
740            //  * Enum description
741            //  */
742            // type EnumName =
743            //   | "Variant1"  // Description1
744            //   | "Variant2"  // Description2
745            //   | "Variant3"; // Description3
746            //
747            // Example value: "Variant1"
748
749            let mut variant_lines = Vec::new();
750            let mut first_variant_name = None;
751
752            for variant in &data_enum.variants {
753                let variant_name = &variant.ident;
754                let variant_name_str = variant_name.to_string();
755
756                // Parse prompt attributes
757                let prompt_attrs = parse_prompt_attributes(&variant.attrs);
758
759                // Skip if marked with #[prompt(skip)]
760                if prompt_attrs.skip {
761                    continue;
762                }
763
764                // Determine variant value with priority:
765                // 1. #[prompt(rename = "...")]
766                // 2. #[serde(rename = "...")]
767                // 3. #[serde(rename_all = "...")] rule
768                // 4. Default (variant name as-is)
769                let variant_value = if let Some(prompt_rename) = &prompt_attrs.rename {
770                    prompt_rename.clone()
771                } else if let Some(serde_rename) = parse_serde_variant_rename(&variant.attrs) {
772                    serde_rename
773                } else if let Some(rule) = rename_rule {
774                    rule.apply(&variant_name_str)
775                } else {
776                    variant_name_str.clone()
777                };
778
779                // Check variant type: Unit, Struct, or Tuple
780                let variant_line = match &variant.fields {
781                    syn::Fields::Unit => {
782                        // Unit variant: "VariantName"
783                        if let Some(desc) = &prompt_attrs.description {
784                            format!("  | \"{}\"  // {}", variant_value, desc)
785                        } else {
786                            let docs = extract_doc_comments(&variant.attrs);
787                            if !docs.is_empty() {
788                                format!("  | \"{}\"  // {}", variant_value, docs)
789                            } else {
790                                format!("  | \"{}\"", variant_value)
791                            }
792                        }
793                    }
794                    syn::Fields::Named(fields) => {
795                        // Struct variant: { type: "VariantName", field1: Type1, ... }
796                        let mut field_parts = vec![format!("type: \"{}\"", variant_value)];
797
798                        for field in &fields.named {
799                            let field_name = field.ident.as_ref().unwrap().to_string();
800                            let field_type = format_type_for_schema(&field.ty);
801                            field_parts.push(format!("{}: {}", field_name, field_type));
802                        }
803
804                        let field_str = field_parts.join(", ");
805                        let comment = if let Some(desc) = &prompt_attrs.description {
806                            format!("  // {}", desc)
807                        } else {
808                            let docs = extract_doc_comments(&variant.attrs);
809                            if !docs.is_empty() {
810                                format!("  // {}", docs)
811                            } else {
812                                String::new()
813                            }
814                        };
815
816                        format!("  | {{ {} }}{}", field_str, comment)
817                    }
818                    syn::Fields::Unnamed(fields) => {
819                        // Tuple variant: TypeScript tuple type [Type1, Type2, ...]
820                        let field_types: Vec<String> = fields
821                            .unnamed
822                            .iter()
823                            .map(|f| format_type_for_schema(&f.ty))
824                            .collect();
825
826                        let tuple_str = field_types.join(", ");
827                        let comment = if let Some(desc) = &prompt_attrs.description {
828                            format!("  // {}", desc)
829                        } else {
830                            let docs = extract_doc_comments(&variant.attrs);
831                            if !docs.is_empty() {
832                                format!("  // {}", docs)
833                            } else {
834                                String::new()
835                            }
836                        };
837
838                        format!("  | [{}]{}", tuple_str, comment)
839                    }
840                };
841
842                variant_lines.push(variant_line);
843
844                if first_variant_name.is_none() {
845                    first_variant_name = Some(variant_value);
846                }
847            }
848
849            // Build complete TypeScript-style schema
850            let mut lines = Vec::new();
851
852            // Add JSDoc comment if enum has description
853            if !enum_docs.is_empty() {
854                lines.push("/**".to_string());
855                lines.push(format!(" * {}", enum_docs));
856                lines.push(" */".to_string());
857            }
858
859            // Add type definition header
860            lines.push(format!("type {} =", enum_name));
861
862            // Add all variant lines
863            for line in &variant_lines {
864                lines.push(line.clone());
865            }
866
867            // Add semicolon to last variant
868            if let Some(last) = lines.last_mut()
869                && !last.ends_with(';')
870            {
871                last.push(';');
872            }
873
874            // Add example value (first non-skipped variant)
875            if let Some(first_name) = first_variant_name {
876                lines.push("".to_string()); // Empty line
877                lines.push(format!("Example value: \"{}\"", first_name));
878            }
879
880            let prompt_string = lines.join("\n");
881            let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
882
883            // Generate match arms for instance-level to_prompt()
884            let mut match_arms = Vec::new();
885            for variant in &data_enum.variants {
886                let variant_name = &variant.ident;
887                let variant_name_str = variant_name.to_string();
888
889                // Parse prompt attributes
890                let prompt_attrs = parse_prompt_attributes(&variant.attrs);
891
892                // Determine variant value with same priority as schema generation:
893                // 1. #[prompt(rename = "...")]
894                // 2. #[serde(rename = "...")]
895                // 3. #[serde(rename_all = "...")] rule
896                // 4. Default (variant name as-is)
897                let variant_value = if let Some(prompt_rename) = &prompt_attrs.rename {
898                    prompt_rename.clone()
899                } else if let Some(serde_rename) = parse_serde_variant_rename(&variant.attrs) {
900                    serde_rename
901                } else if let Some(rule) = rename_rule {
902                    rule.apply(&variant_name_str)
903                } else {
904                    variant_name_str.clone()
905                };
906
907                // Generate match arm based on variant type
908                match &variant.fields {
909                    syn::Fields::Unit => {
910                        // Unit variant - existing behavior
911                        if prompt_attrs.skip {
912                            match_arms.push(quote! {
913                                Self::#variant_name => stringify!(#variant_name).to_string()
914                            });
915                        } else if let Some(desc) = &prompt_attrs.description {
916                            match_arms.push(quote! {
917                                Self::#variant_name => format!("{}: {}", #variant_value, #desc)
918                            });
919                        } else {
920                            let variant_docs = extract_doc_comments(&variant.attrs);
921                            if !variant_docs.is_empty() {
922                                match_arms.push(quote! {
923                                    Self::#variant_name => format!("{}: {}", #variant_value, #variant_docs)
924                                });
925                            } else {
926                                match_arms.push(quote! {
927                                    Self::#variant_name => #variant_value.to_string()
928                                });
929                            }
930                        }
931                    }
932                    syn::Fields::Named(fields) => {
933                        // Struct variant - serialize fields to JSON-like string
934                        let field_bindings: Vec<_> = fields
935                            .named
936                            .iter()
937                            .map(|f| f.ident.as_ref().unwrap())
938                            .collect();
939
940                        let field_displays: Vec<_> = fields
941                            .named
942                            .iter()
943                            .map(|f| {
944                                let field_name = f.ident.as_ref().unwrap();
945                                let field_name_str = field_name.to_string();
946                                quote! {
947                                    format!("{}: {:?}", #field_name_str, #field_name)
948                                }
949                            })
950                            .collect();
951
952                        let doc_or_desc = if let Some(desc) = &prompt_attrs.description {
953                            desc.clone()
954                        } else {
955                            let docs = extract_doc_comments(&variant.attrs);
956                            if !docs.is_empty() {
957                                docs
958                            } else {
959                                String::new()
960                            }
961                        };
962
963                        if doc_or_desc.is_empty() {
964                            match_arms.push(quote! {
965                                Self::#variant_name { #(#field_bindings),* } => {
966                                    let fields = vec![#(#field_displays),*];
967                                    format!("{} {{ {} }}", #variant_value, fields.join(", "))
968                                }
969                            });
970                        } else {
971                            match_arms.push(quote! {
972                                Self::#variant_name { #(#field_bindings),* } => {
973                                    let fields = vec![#(#field_displays),*];
974                                    format!("{}: {} {{ {} }}", #variant_value, #doc_or_desc, fields.join(", "))
975                                }
976                            });
977                        }
978                    }
979                    syn::Fields::Unnamed(fields) => {
980                        // Tuple variant - bind fields and display them
981                        let field_count = fields.unnamed.len();
982                        let field_bindings: Vec<_> = (0..field_count)
983                            .map(|i| {
984                                syn::Ident::new(
985                                    &format!("field{}", i),
986                                    proc_macro2::Span::call_site(),
987                                )
988                            })
989                            .collect();
990
991                        let field_displays: Vec<_> = field_bindings
992                            .iter()
993                            .map(|field_name| {
994                                quote! {
995                                    format!("{:?}", #field_name)
996                                }
997                            })
998                            .collect();
999
1000                        let doc_or_desc = if let Some(desc) = &prompt_attrs.description {
1001                            desc.clone()
1002                        } else {
1003                            let docs = extract_doc_comments(&variant.attrs);
1004                            if !docs.is_empty() {
1005                                docs
1006                            } else {
1007                                String::new()
1008                            }
1009                        };
1010
1011                        if doc_or_desc.is_empty() {
1012                            match_arms.push(quote! {
1013                                Self::#variant_name(#(#field_bindings),*) => {
1014                                    let fields = vec![#(#field_displays),*];
1015                                    format!("{}({})", #variant_value, fields.join(", "))
1016                                }
1017                            });
1018                        } else {
1019                            match_arms.push(quote! {
1020                                Self::#variant_name(#(#field_bindings),*) => {
1021                                    let fields = vec![#(#field_displays),*];
1022                                    format!("{}: {}({})", #variant_value, #doc_or_desc, fields.join(", "))
1023                                }
1024                            });
1025                        }
1026                    }
1027                }
1028            }
1029
1030            let to_prompt_impl = if match_arms.is_empty() {
1031                // Empty enum: no variants to match
1032                quote! {
1033                    fn to_prompt(&self) -> String {
1034                        match *self {}
1035                    }
1036                }
1037            } else {
1038                quote! {
1039                    fn to_prompt(&self) -> String {
1040                        match self {
1041                            #(#match_arms),*
1042                        }
1043                    }
1044                }
1045            };
1046
1047            let expanded = quote! {
1048                impl #impl_generics #crate_path::prompt::ToPrompt for #enum_name #ty_generics #where_clause {
1049                    fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
1050                        vec![#crate_path::prompt::PromptPart::Text(self.to_prompt())]
1051                    }
1052
1053                    #to_prompt_impl
1054
1055                    fn prompt_schema() -> String {
1056                        #prompt_string.to_string()
1057                    }
1058                }
1059            };
1060
1061            TokenStream::from(expanded)
1062        }
1063        Data::Struct(data_struct) => {
1064            // Parse struct-level prompt attributes for template, template_file, mode, and validate
1065            let mut template_attr = None;
1066            let mut template_file_attr = None;
1067            let mut mode_attr = None;
1068            let mut validate_attr = false;
1069            let mut type_marker_attr = false;
1070
1071            for attr in &input.attrs {
1072                if attr.path().is_ident("prompt") {
1073                    // Try to parse the attribute arguments
1074                    if let Ok(metas) =
1075                        attr.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1076                    {
1077                        for meta in metas {
1078                            match meta {
1079                                Meta::NameValue(nv) if nv.path.is_ident("template") => {
1080                                    if let syn::Expr::Lit(expr_lit) = nv.value
1081                                        && let syn::Lit::Str(lit_str) = expr_lit.lit
1082                                    {
1083                                        template_attr = Some(lit_str.value());
1084                                    }
1085                                }
1086                                Meta::NameValue(nv) if nv.path.is_ident("template_file") => {
1087                                    if let syn::Expr::Lit(expr_lit) = nv.value
1088                                        && let syn::Lit::Str(lit_str) = expr_lit.lit
1089                                    {
1090                                        template_file_attr = Some(lit_str.value());
1091                                    }
1092                                }
1093                                Meta::NameValue(nv) if nv.path.is_ident("mode") => {
1094                                    if let syn::Expr::Lit(expr_lit) = nv.value
1095                                        && let syn::Lit::Str(lit_str) = expr_lit.lit
1096                                    {
1097                                        mode_attr = Some(lit_str.value());
1098                                    }
1099                                }
1100                                Meta::NameValue(nv) if nv.path.is_ident("validate") => {
1101                                    if let syn::Expr::Lit(expr_lit) = nv.value
1102                                        && let syn::Lit::Bool(lit_bool) = expr_lit.lit
1103                                    {
1104                                        validate_attr = lit_bool.value();
1105                                    }
1106                                }
1107                                Meta::NameValue(nv) if nv.path.is_ident("type_marker") => {
1108                                    if let syn::Expr::Lit(expr_lit) = nv.value
1109                                        && let syn::Lit::Bool(lit_bool) = expr_lit.lit
1110                                    {
1111                                        type_marker_attr = lit_bool.value();
1112                                    }
1113                                }
1114                                Meta::Path(path) if path.is_ident("type_marker") => {
1115                                    // Support both #[prompt(type_marker)] and #[prompt(type_marker = true)]
1116                                    type_marker_attr = true;
1117                                }
1118                                _ => {}
1119                            }
1120                        }
1121                    }
1122                }
1123            }
1124
1125            // Check for mutual exclusivity between template and template_file
1126            if template_attr.is_some() && template_file_attr.is_some() {
1127                return syn::Error::new(
1128                    input.ident.span(),
1129                    "The `template` and `template_file` attributes are mutually exclusive. Please use only one.",
1130                ).to_compile_error().into();
1131            }
1132
1133            // Load template from file if template_file is specified
1134            let template_str = if let Some(file_path) = template_file_attr {
1135                // Try multiple strategies to find the template file
1136                // This is necessary to support both normal compilation and trybuild tests
1137
1138                let mut full_path = None;
1139
1140                // Strategy 1: Try relative to CARGO_MANIFEST_DIR (normal compilation)
1141                if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
1142                    // Check if this is a trybuild temporary directory
1143                    let is_trybuild = manifest_dir.contains("target/tests/trybuild");
1144
1145                    if !is_trybuild {
1146                        // Normal compilation - use CARGO_MANIFEST_DIR directly
1147                        let candidate = std::path::Path::new(&manifest_dir).join(&file_path);
1148                        if candidate.exists() {
1149                            full_path = Some(candidate);
1150                        }
1151                    } else {
1152                        // For trybuild, we need to find the original source directory
1153                        // The manifest_dir looks like: .../target/tests/trybuild/llm-toolkit-macros
1154                        // We need to get back to the original llm-toolkit-macros source directory
1155
1156                        // Extract the workspace root from the path
1157                        if let Some(target_pos) = manifest_dir.find("/target/tests/trybuild") {
1158                            let workspace_root = &manifest_dir[..target_pos];
1159                            // Now construct the path to the original llm-toolkit-macros source
1160                            let original_macros_dir = std::path::Path::new(workspace_root)
1161                                .join("crates")
1162                                .join("llm-toolkit-macros");
1163
1164                            let candidate = original_macros_dir.join(&file_path);
1165                            if candidate.exists() {
1166                                full_path = Some(candidate);
1167                            }
1168                        }
1169                    }
1170                }
1171
1172                // Strategy 2: Try as an absolute path or relative to current directory
1173                if full_path.is_none() {
1174                    let candidate = std::path::Path::new(&file_path).to_path_buf();
1175                    if candidate.exists() {
1176                        full_path = Some(candidate);
1177                    }
1178                }
1179
1180                // Strategy 3: For trybuild tests - try to find the file by looking in parent directories
1181                // This handles the case where trybuild creates a temporary project
1182                if full_path.is_none()
1183                    && let Ok(current_dir) = std::env::current_dir()
1184                {
1185                    let mut search_dir = current_dir.as_path();
1186                    // Search up to 10 levels up
1187                    for _ in 0..10 {
1188                        // Try from the llm-toolkit-macros directory
1189                        let macros_dir = search_dir.join("crates/llm-toolkit-macros");
1190                        if macros_dir.exists() {
1191                            let candidate = macros_dir.join(&file_path);
1192                            if candidate.exists() {
1193                                full_path = Some(candidate);
1194                                break;
1195                            }
1196                        }
1197                        // Try directly
1198                        let candidate = search_dir.join(&file_path);
1199                        if candidate.exists() {
1200                            full_path = Some(candidate);
1201                            break;
1202                        }
1203                        if let Some(parent) = search_dir.parent() {
1204                            search_dir = parent;
1205                        } else {
1206                            break;
1207                        }
1208                    }
1209                }
1210
1211                // Validate file existence at compile time
1212                if full_path.is_none() {
1213                    // Build helpful error message with search locations
1214                    let mut error_msg = format!(
1215                        "Template file '{}' not found at compile time.\n\nSearched in:",
1216                        file_path
1217                    );
1218
1219                    if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
1220                        let candidate = std::path::Path::new(&manifest_dir).join(&file_path);
1221                        error_msg.push_str(&format!("\n  - {}", candidate.display()));
1222                    }
1223
1224                    if let Ok(current_dir) = std::env::current_dir() {
1225                        let candidate = current_dir.join(&file_path);
1226                        error_msg.push_str(&format!("\n  - {}", candidate.display()));
1227                    }
1228
1229                    error_msg.push_str("\n\nPlease ensure:");
1230                    error_msg.push_str("\n  1. The template file exists");
1231                    error_msg.push_str("\n  2. The path is relative to CARGO_MANIFEST_DIR");
1232                    error_msg.push_str("\n  3. There are no typos in the path");
1233
1234                    return syn::Error::new(input.ident.span(), error_msg)
1235                        .to_compile_error()
1236                        .into();
1237                }
1238
1239                let final_path = full_path.unwrap();
1240
1241                // Read the file at compile time
1242                match std::fs::read_to_string(&final_path) {
1243                    Ok(content) => Some(content),
1244                    Err(e) => {
1245                        return syn::Error::new(
1246                            input.ident.span(),
1247                            format!(
1248                                "Failed to read template file '{}': {}\n\nPath resolved to: {}",
1249                                file_path,
1250                                e,
1251                                final_path.display()
1252                            ),
1253                        )
1254                        .to_compile_error()
1255                        .into();
1256                    }
1257                }
1258            } else {
1259                template_attr
1260            };
1261
1262            // Perform validation if requested
1263            if validate_attr && let Some(template) = &template_str {
1264                // Validate Jinja syntax
1265                let mut env = minijinja::Environment::new();
1266                if let Err(e) = env.add_template("validation", template) {
1267                    // Generate a compile warning using deprecated const hack
1268                    let warning_msg =
1269                        format!("Template validation warning: Invalid Jinja syntax - {}", e);
1270                    let warning_ident = syn::Ident::new(
1271                        "TEMPLATE_VALIDATION_WARNING",
1272                        proc_macro2::Span::call_site(),
1273                    );
1274                    let _warning_tokens = quote! {
1275                        #[deprecated(note = #warning_msg)]
1276                        const #warning_ident: () = ();
1277                        let _ = #warning_ident;
1278                    };
1279                    // We'll inject this warning into the generated code
1280                    eprintln!("cargo:warning={}", warning_msg);
1281                }
1282
1283                // Extract variables from template and check against struct fields
1284                let fields = if let syn::Fields::Named(fields) = &data_struct.fields {
1285                    &fields.named
1286                } else {
1287                    panic!("Template validation is only supported for structs with named fields.");
1288                };
1289
1290                let field_names: std::collections::HashSet<String> = fields
1291                    .iter()
1292                    .filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
1293                    .collect();
1294
1295                // Parse template placeholders
1296                let placeholders = parse_template_placeholders_with_mode(template);
1297
1298                for (placeholder_name, _mode) in &placeholders {
1299                    if placeholder_name != "self" && !field_names.contains(placeholder_name) {
1300                        let warning_msg = format!(
1301                            "Template validation warning: Variable '{}' used in template but not found in struct fields",
1302                            placeholder_name
1303                        );
1304                        eprintln!("cargo:warning={}", warning_msg);
1305                    }
1306                }
1307            }
1308
1309            let name = input.ident;
1310            let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1311
1312            // Extract struct name and doc comment for use in schema generation
1313            let struct_docs = extract_doc_comments(&input.attrs);
1314
1315            // Check if this is a mode-based struct (mode attribute present)
1316            let is_mode_based =
1317                mode_attr.is_some() || (template_str.is_none() && struct_docs.contains("mode"));
1318
1319            let expanded = if is_mode_based || mode_attr.is_some() {
1320                // Mode-based generation: support schema_only, example_only, full
1321                let fields = if let syn::Fields::Named(fields) = &data_struct.fields {
1322                    &fields.named
1323                } else {
1324                    panic!(
1325                        "Mode-based prompt generation is only supported for structs with named fields."
1326                    );
1327                };
1328
1329                let struct_name_str = name.to_string();
1330
1331                // Check if struct derives Default
1332                let has_default = input.attrs.iter().any(|attr| {
1333                    if attr.path().is_ident("derive")
1334                        && let Ok(meta_list) = attr.meta.require_list()
1335                    {
1336                        let tokens_str = meta_list.tokens.to_string();
1337                        tokens_str.contains("Default")
1338                    } else {
1339                        false
1340                    }
1341                });
1342
1343                // Note: type_marker_attr is used as a marker/flag indicating this struct uses the TypeMarker pattern
1344                // When type_marker is set (via #[prompt(type_marker)]), it indicates:
1345                // - This struct is used for type-based retrieval in Orchestrator
1346                // - The __type field must be manually defined by the user (for custom configurations)
1347                // - The __type field will be automatically excluded from LLM schema (see Line 154)
1348                //
1349                // For standard cases, users should use #[type_marker] attribute macro instead,
1350                // which automatically adds the __type field.
1351
1352                // Generate schema-only parts (type_marker_attr comes from prompt attribute parsing above)
1353                let schema_parts = generate_schema_only_parts(
1354                    &struct_name_str,
1355                    &struct_docs,
1356                    fields,
1357                    &crate_path,
1358                    type_marker_attr,
1359                );
1360
1361                // Generate example parts
1362                let example_parts = generate_example_only_parts(fields, has_default, &crate_path);
1363
1364                quote! {
1365                    impl #impl_generics #crate_path::prompt::ToPrompt for #name #ty_generics #where_clause {
1366                        fn to_prompt_parts_with_mode(&self, mode: &str) -> Vec<#crate_path::prompt::PromptPart> {
1367                            match mode {
1368                                "schema_only" => #schema_parts,
1369                                "example_only" => #example_parts,
1370                                "full" | _ => {
1371                                    // Combine schema and example
1372                                    let mut parts = Vec::new();
1373
1374                                    // Add schema
1375                                    let schema_parts = #schema_parts;
1376                                    parts.extend(schema_parts);
1377
1378                                    // Add separator and example header
1379                                    parts.push(#crate_path::prompt::PromptPart::Text("\n### Example".to_string()));
1380                                    parts.push(#crate_path::prompt::PromptPart::Text(
1381                                        format!("Here is an example of a valid `{}` object:", #struct_name_str)
1382                                    ));
1383
1384                                    // Add example
1385                                    let example_parts = #example_parts;
1386                                    parts.extend(example_parts);
1387
1388                                    parts
1389                                }
1390                            }
1391                        }
1392
1393                        fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
1394                            self.to_prompt_parts_with_mode("full")
1395                        }
1396
1397                        fn to_prompt(&self) -> String {
1398                            self.to_prompt_parts()
1399                                .into_iter()
1400                                .filter_map(|part| match part {
1401                                    #crate_path::prompt::PromptPart::Text(text) => Some(text),
1402                                    _ => None,
1403                                })
1404                                .collect::<Vec<_>>()
1405                                .join("\n")
1406                        }
1407
1408                        fn prompt_schema() -> String {
1409                            use std::sync::OnceLock;
1410                            static SCHEMA_CACHE: OnceLock<String> = OnceLock::new();
1411
1412                            SCHEMA_CACHE.get_or_init(|| {
1413                                let schema_parts = #schema_parts;
1414                                schema_parts
1415                                    .into_iter()
1416                                    .filter_map(|part| match part {
1417                                        #crate_path::prompt::PromptPart::Text(text) => Some(text),
1418                                        _ => None,
1419                                    })
1420                                    .collect::<Vec<_>>()
1421                                    .join("\n")
1422                            }).clone()
1423                        }
1424                    }
1425                }
1426            } else if let Some(template) = template_str {
1427                // Use template-based approach if template is provided
1428                // Collect image fields separately for to_prompt_parts()
1429                let fields = if let syn::Fields::Named(fields) = &data_struct.fields {
1430                    &fields.named
1431                } else {
1432                    panic!(
1433                        "Template prompt generation is only supported for structs with named fields."
1434                    );
1435                };
1436
1437                // Parse template to detect mode syntax
1438                let placeholders = parse_template_placeholders_with_mode(&template);
1439                // Only use custom mode processing if template actually contains :mode syntax
1440                let has_mode_syntax = placeholders.iter().any(|(field_name, mode)| {
1441                    mode.is_some()
1442                        && fields
1443                            .iter()
1444                            .any(|f| f.ident.as_ref().unwrap() == field_name)
1445                });
1446
1447                let mut image_field_parts = Vec::new();
1448                for f in fields.iter() {
1449                    let field_name = f.ident.as_ref().unwrap();
1450                    let attrs = parse_field_prompt_attrs(&f.attrs);
1451
1452                    if attrs.image {
1453                        // This field is marked as an image
1454                        image_field_parts.push(quote! {
1455                            parts.extend(self.#field_name.to_prompt_parts());
1456                        });
1457                    }
1458                }
1459
1460                // Generate appropriate code based on whether mode syntax is used
1461                if has_mode_syntax {
1462                    // Build custom context for fields with mode specifications
1463                    let mut context_fields = Vec::new();
1464                    let mut modified_template = template.clone();
1465
1466                    // Process each placeholder with mode
1467                    for (field_name, mode_opt) in &placeholders {
1468                        if let Some(mode) = mode_opt {
1469                            // Create a unique key for this field:mode combination
1470                            let unique_key = format!("{}__{}", field_name, mode);
1471
1472                            // Replace {{ field:mode }} with {{ field__mode }} in template
1473                            let pattern = format!("{{{{ {}:{} }}}}", field_name, mode);
1474                            let replacement = format!("{{{{ {} }}}}", unique_key);
1475                            modified_template = modified_template.replace(&pattern, &replacement);
1476
1477                            // Find the corresponding field
1478                            let field_ident =
1479                                syn::Ident::new(field_name, proc_macro2::Span::call_site());
1480
1481                            // Add to context with mode specification
1482                            context_fields.push(quote! {
1483                                context.insert(
1484                                    #unique_key.to_string(),
1485                                    minijinja::Value::from(self.#field_ident.to_prompt_with_mode(#mode))
1486                                );
1487                            });
1488                        }
1489                    }
1490
1491                    // Add individual fields via direct access (for non-mode fields)
1492                    for field in fields.iter() {
1493                        let field_name = field.ident.as_ref().unwrap();
1494                        let field_name_str = field_name.to_string();
1495
1496                        // Skip if this field already has a mode-specific entry
1497                        let has_mode_entry = placeholders
1498                            .iter()
1499                            .any(|(name, mode)| name == &field_name_str && mode.is_some());
1500
1501                        if !has_mode_entry {
1502                            // Check if field type is likely a struct that implements ToPrompt
1503                            // (not a primitive type)
1504                            let is_primitive = match &field.ty {
1505                                syn::Type::Path(type_path) => {
1506                                    if let Some(segment) = type_path.path.segments.last() {
1507                                        let type_name = segment.ident.to_string();
1508                                        matches!(
1509                                            type_name.as_str(),
1510                                            "String"
1511                                                | "str"
1512                                                | "i8"
1513                                                | "i16"
1514                                                | "i32"
1515                                                | "i64"
1516                                                | "i128"
1517                                                | "isize"
1518                                                | "u8"
1519                                                | "u16"
1520                                                | "u32"
1521                                                | "u64"
1522                                                | "u128"
1523                                                | "usize"
1524                                                | "f32"
1525                                                | "f64"
1526                                                | "bool"
1527                                                | "char"
1528                                        )
1529                                    } else {
1530                                        false
1531                                    }
1532                                }
1533                                _ => false,
1534                            };
1535
1536                            if is_primitive {
1537                                context_fields.push(quote! {
1538                                    context.insert(
1539                                        #field_name_str.to_string(),
1540                                        minijinja::Value::from_serialize(&self.#field_name)
1541                                    );
1542                                });
1543                            } else {
1544                                // For non-primitive types, use to_prompt()
1545                                context_fields.push(quote! {
1546                                    context.insert(
1547                                        #field_name_str.to_string(),
1548                                        minijinja::Value::from(self.#field_name.to_prompt())
1549                                    );
1550                                });
1551                            }
1552                        }
1553                    }
1554
1555                    quote! {
1556                        impl #impl_generics #crate_path::prompt::ToPrompt for #name #ty_generics #where_clause {
1557                            fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
1558                                let mut parts = Vec::new();
1559
1560                                // Add image parts first
1561                                #(#image_field_parts)*
1562
1563                                // Build custom context and render template
1564                                let text = {
1565                                    let mut env = minijinja::Environment::new();
1566                                    env.add_template("prompt", #modified_template).unwrap_or_else(|e| {
1567                                        panic!("Failed to parse template: {}", e)
1568                                    });
1569
1570                                    let tmpl = env.get_template("prompt").unwrap();
1571
1572                                    let mut context = std::collections::HashMap::new();
1573                                    #(#context_fields)*
1574
1575                                    tmpl.render(context).unwrap_or_else(|e| {
1576                                        format!("Failed to render prompt: {}", e)
1577                                    })
1578                                };
1579
1580                                if !text.is_empty() {
1581                                    parts.push(#crate_path::prompt::PromptPart::Text(text));
1582                                }
1583
1584                                parts
1585                            }
1586
1587                            fn to_prompt(&self) -> String {
1588                                // Same logic for to_prompt
1589                                let mut env = minijinja::Environment::new();
1590                                env.add_template("prompt", #modified_template).unwrap_or_else(|e| {
1591                                    panic!("Failed to parse template: {}", e)
1592                                });
1593
1594                                let tmpl = env.get_template("prompt").unwrap();
1595
1596                                let mut context = std::collections::HashMap::new();
1597                                #(#context_fields)*
1598
1599                                tmpl.render(context).unwrap_or_else(|e| {
1600                                    format!("Failed to render prompt: {}", e)
1601                                })
1602                            }
1603
1604                            fn prompt_schema() -> String {
1605                                String::new() // Template-based structs don't have auto-generated schema
1606                            }
1607                        }
1608                    }
1609                } else {
1610                    // No mode syntax, use direct template rendering with render_prompt
1611                    quote! {
1612                        impl #impl_generics #crate_path::prompt::ToPrompt for #name #ty_generics #where_clause {
1613                            fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
1614                                let mut parts = Vec::new();
1615
1616                                // Add image parts first
1617                                #(#image_field_parts)*
1618
1619                                // Add the rendered template as text
1620                                let text = #crate_path::prompt::render_prompt(#template, self).unwrap_or_else(|e| {
1621                                    format!("Failed to render prompt: {}", e)
1622                                });
1623                                if !text.is_empty() {
1624                                    parts.push(#crate_path::prompt::PromptPart::Text(text));
1625                                }
1626
1627                                parts
1628                            }
1629
1630                            fn to_prompt(&self) -> String {
1631                                #crate_path::prompt::render_prompt(#template, self).unwrap_or_else(|e| {
1632                                    format!("Failed to render prompt: {}", e)
1633                                })
1634                            }
1635
1636                            fn prompt_schema() -> String {
1637                                String::new() // Template-based structs don't have auto-generated schema
1638                            }
1639                        }
1640                    }
1641                }
1642            } else {
1643                // Use default key-value format if no template is provided
1644                // Now also generate to_prompt_parts() for multimodal support
1645                let fields = if let syn::Fields::Named(fields) = &data_struct.fields {
1646                    &fields.named
1647                } else {
1648                    panic!(
1649                        "Default prompt generation is only supported for structs with named fields."
1650                    );
1651                };
1652
1653                // Separate image fields from text fields
1654                let mut text_field_parts = Vec::new();
1655                let mut image_field_parts = Vec::new();
1656
1657                for f in fields.iter() {
1658                    let field_name = f.ident.as_ref().unwrap();
1659                    let attrs = parse_field_prompt_attrs(&f.attrs);
1660
1661                    // Skip if #[prompt(skip)] is present
1662                    if attrs.skip {
1663                        continue;
1664                    }
1665
1666                    if attrs.image {
1667                        // This field is marked as an image
1668                        image_field_parts.push(quote! {
1669                            parts.extend(self.#field_name.to_prompt_parts());
1670                        });
1671                    } else {
1672                        // This is a regular text field
1673                        // Determine the key based on priority:
1674                        // 1. #[prompt(rename = "new_name")]
1675                        // 2. Doc comment
1676                        // 3. Field name (fallback)
1677                        let key = if let Some(rename) = attrs.rename {
1678                            rename
1679                        } else {
1680                            let doc_comment = extract_doc_comments(&f.attrs);
1681                            if !doc_comment.is_empty() {
1682                                doc_comment
1683                            } else {
1684                                field_name.to_string()
1685                            }
1686                        };
1687
1688                        // Determine the value based on format_with attribute
1689                        let value_expr = if let Some(format_with) = attrs.format_with {
1690                            // Parse the function path string into a syn::Path
1691                            let func_path: syn::Path =
1692                                syn::parse_str(&format_with).unwrap_or_else(|_| {
1693                                    panic!("Invalid function path: {}", format_with)
1694                                });
1695                            quote! { #func_path(&self.#field_name) }
1696                        } else {
1697                            quote! { self.#field_name.to_prompt() }
1698                        };
1699
1700                        text_field_parts.push(quote! {
1701                            text_parts.push(format!("{}: {}", #key, #value_expr));
1702                        });
1703                    }
1704                }
1705
1706                // Generate the implementation with to_prompt_parts()
1707                quote! {
1708                    impl #impl_generics #crate_path::prompt::ToPrompt for #name #ty_generics #where_clause {
1709                        fn to_prompt_parts(&self) -> Vec<#crate_path::prompt::PromptPart> {
1710                            let mut parts = Vec::new();
1711
1712                            // Add image parts first
1713                            #(#image_field_parts)*
1714
1715                            // Collect text parts and add as a single text prompt part
1716                            let mut text_parts = Vec::new();
1717                            #(#text_field_parts)*
1718
1719                            if !text_parts.is_empty() {
1720                                parts.push(#crate_path::prompt::PromptPart::Text(text_parts.join("\n")));
1721                            }
1722
1723                            parts
1724                        }
1725
1726                        fn to_prompt(&self) -> String {
1727                            let mut text_parts = Vec::new();
1728                            #(#text_field_parts)*
1729                            text_parts.join("\n")
1730                        }
1731
1732                        fn prompt_schema() -> String {
1733                            String::new() // Default key-value format doesn't have auto-generated schema
1734                        }
1735                    }
1736                }
1737            };
1738
1739            TokenStream::from(expanded)
1740        }
1741        Data::Union(_) => {
1742            panic!("`#[derive(ToPrompt)]` is not supported for unions");
1743        }
1744    }
1745}
1746
1747/// Information about a prompt target
1748#[derive(Debug, Clone)]
1749struct TargetInfo {
1750    name: String,
1751    template: Option<String>,
1752    field_configs: std::collections::HashMap<String, FieldTargetConfig>,
1753}
1754
1755/// Configuration for how a field should be handled for a specific target
1756#[derive(Debug, Clone, Default)]
1757struct FieldTargetConfig {
1758    skip: bool,
1759    rename: Option<String>,
1760    format_with: Option<String>,
1761    image: bool,
1762    include_only: bool, // true if this field is specifically included for this target
1763}
1764
1765/// Parse #[prompt_for(...)] attributes for ToPromptSet
1766fn parse_prompt_for_attrs(attrs: &[syn::Attribute]) -> Vec<(String, FieldTargetConfig)> {
1767    let mut configs = Vec::new();
1768
1769    for attr in attrs {
1770        if attr.path().is_ident("prompt_for")
1771            && let Ok(meta_list) = attr.meta.require_list()
1772        {
1773            // Try to parse as meta list
1774            if meta_list.tokens.to_string() == "skip" {
1775                // Simple #[prompt_for(skip)] applies to all targets
1776                let config = FieldTargetConfig {
1777                    skip: true,
1778                    ..Default::default()
1779                };
1780                configs.push(("*".to_string(), config));
1781            } else if let Ok(metas) =
1782                meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1783            {
1784                let mut target_name = None;
1785                let mut config = FieldTargetConfig::default();
1786
1787                for meta in metas {
1788                    match meta {
1789                        Meta::NameValue(nv) if nv.path.is_ident("name") => {
1790                            if let syn::Expr::Lit(syn::ExprLit {
1791                                lit: syn::Lit::Str(lit_str),
1792                                ..
1793                            }) = nv.value
1794                            {
1795                                target_name = Some(lit_str.value());
1796                            }
1797                        }
1798                        Meta::Path(path) if path.is_ident("skip") => {
1799                            config.skip = true;
1800                        }
1801                        Meta::NameValue(nv) if nv.path.is_ident("rename") => {
1802                            if let syn::Expr::Lit(syn::ExprLit {
1803                                lit: syn::Lit::Str(lit_str),
1804                                ..
1805                            }) = nv.value
1806                            {
1807                                config.rename = Some(lit_str.value());
1808                            }
1809                        }
1810                        Meta::NameValue(nv) if nv.path.is_ident("format_with") => {
1811                            if let syn::Expr::Lit(syn::ExprLit {
1812                                lit: syn::Lit::Str(lit_str),
1813                                ..
1814                            }) = nv.value
1815                            {
1816                                config.format_with = Some(lit_str.value());
1817                            }
1818                        }
1819                        Meta::Path(path) if path.is_ident("image") => {
1820                            config.image = true;
1821                        }
1822                        _ => {}
1823                    }
1824                }
1825
1826                if let Some(name) = target_name {
1827                    config.include_only = true;
1828                    configs.push((name, config));
1829                }
1830            }
1831        }
1832    }
1833
1834    configs
1835}
1836
1837/// Parse struct-level #[prompt_for(...)] attributes to find target templates
1838fn parse_struct_prompt_for_attrs(attrs: &[syn::Attribute]) -> Vec<TargetInfo> {
1839    let mut targets = Vec::new();
1840
1841    for attr in attrs {
1842        if attr.path().is_ident("prompt_for")
1843            && let Ok(meta_list) = attr.meta.require_list()
1844            && let Ok(metas) =
1845                meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1846        {
1847            let mut target_name = None;
1848            let mut template = None;
1849
1850            for meta in metas {
1851                match meta {
1852                    Meta::NameValue(nv) if nv.path.is_ident("name") => {
1853                        if let syn::Expr::Lit(syn::ExprLit {
1854                            lit: syn::Lit::Str(lit_str),
1855                            ..
1856                        }) = nv.value
1857                        {
1858                            target_name = Some(lit_str.value());
1859                        }
1860                    }
1861                    Meta::NameValue(nv) if nv.path.is_ident("template") => {
1862                        if let syn::Expr::Lit(syn::ExprLit {
1863                            lit: syn::Lit::Str(lit_str),
1864                            ..
1865                        }) = nv.value
1866                        {
1867                            template = Some(lit_str.value());
1868                        }
1869                    }
1870                    _ => {}
1871                }
1872            }
1873
1874            if let Some(name) = target_name {
1875                targets.push(TargetInfo {
1876                    name,
1877                    template,
1878                    field_configs: std::collections::HashMap::new(),
1879                });
1880            }
1881        }
1882    }
1883
1884    targets
1885}
1886
1887#[proc_macro_derive(ToPromptSet, attributes(prompt_for))]
1888pub fn to_prompt_set_derive(input: TokenStream) -> TokenStream {
1889    let input = parse_macro_input!(input as DeriveInput);
1890
1891    let found_crate =
1892        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
1893    let crate_path = match found_crate {
1894        FoundCrate::Itself => {
1895            // Even when it's the same crate, use absolute path to support examples/tests/bins
1896            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
1897            quote!(::#ident)
1898        }
1899        FoundCrate::Name(name) => {
1900            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
1901            quote!(::#ident)
1902        }
1903    };
1904
1905    // Only support structs with named fields
1906    let data_struct = match &input.data {
1907        Data::Struct(data) => data,
1908        _ => {
1909            return syn::Error::new(
1910                input.ident.span(),
1911                "`#[derive(ToPromptSet)]` is only supported for structs",
1912            )
1913            .to_compile_error()
1914            .into();
1915        }
1916    };
1917
1918    let fields = match &data_struct.fields {
1919        syn::Fields::Named(fields) => &fields.named,
1920        _ => {
1921            return syn::Error::new(
1922                input.ident.span(),
1923                "`#[derive(ToPromptSet)]` is only supported for structs with named fields",
1924            )
1925            .to_compile_error()
1926            .into();
1927        }
1928    };
1929
1930    // Parse struct-level attributes to find targets
1931    let mut targets = parse_struct_prompt_for_attrs(&input.attrs);
1932
1933    // Parse field-level attributes
1934    for field in fields.iter() {
1935        let field_name = field.ident.as_ref().unwrap().to_string();
1936        let field_configs = parse_prompt_for_attrs(&field.attrs);
1937
1938        for (target_name, config) in field_configs {
1939            if target_name == "*" {
1940                // Apply to all targets
1941                for target in &mut targets {
1942                    target
1943                        .field_configs
1944                        .entry(field_name.clone())
1945                        .or_insert_with(FieldTargetConfig::default)
1946                        .skip = config.skip;
1947                }
1948            } else {
1949                // Find or create the target
1950                let target_exists = targets.iter().any(|t| t.name == target_name);
1951                if !target_exists {
1952                    // Add implicit target if not defined at struct level
1953                    targets.push(TargetInfo {
1954                        name: target_name.clone(),
1955                        template: None,
1956                        field_configs: std::collections::HashMap::new(),
1957                    });
1958                }
1959
1960                let target = targets.iter_mut().find(|t| t.name == target_name).unwrap();
1961
1962                target.field_configs.insert(field_name.clone(), config);
1963            }
1964        }
1965    }
1966
1967    // Generate match arms for each target
1968    let mut match_arms = Vec::new();
1969
1970    for target in &targets {
1971        let target_name = &target.name;
1972
1973        if let Some(template_str) = &target.template {
1974            // Template-based generation
1975            let mut image_parts = Vec::new();
1976
1977            for field in fields.iter() {
1978                let field_name = field.ident.as_ref().unwrap();
1979                let field_name_str = field_name.to_string();
1980
1981                if let Some(config) = target.field_configs.get(&field_name_str)
1982                    && config.image
1983                {
1984                    image_parts.push(quote! {
1985                        parts.extend(self.#field_name.to_prompt_parts());
1986                    });
1987                }
1988            }
1989
1990            match_arms.push(quote! {
1991                #target_name => {
1992                    let mut parts = Vec::new();
1993
1994                    #(#image_parts)*
1995
1996                    let text = #crate_path::prompt::render_prompt(#template_str, self)
1997                        .map_err(|e| #crate_path::prompt::PromptSetError::RenderFailed {
1998                            target: #target_name.to_string(),
1999                            source: e,
2000                        })?;
2001
2002                    if !text.is_empty() {
2003                        parts.push(#crate_path::prompt::PromptPart::Text(text));
2004                    }
2005
2006                    Ok(parts)
2007                }
2008            });
2009        } else {
2010            // Key-value based generation
2011            let mut text_field_parts = Vec::new();
2012            let mut image_field_parts = Vec::new();
2013
2014            for field in fields.iter() {
2015                let field_name = field.ident.as_ref().unwrap();
2016                let field_name_str = field_name.to_string();
2017
2018                // Check if field should be included for this target
2019                let config = target.field_configs.get(&field_name_str);
2020
2021                // Skip if explicitly marked to skip
2022                if let Some(cfg) = config
2023                    && cfg.skip
2024                {
2025                    continue;
2026                }
2027
2028                // For non-template targets, only include fields that are:
2029                // 1. Explicitly marked for this target with #[prompt_for(name = "Target")]
2030                // 2. Not marked for any specific target (default fields)
2031                let is_explicitly_for_this_target = config.is_some_and(|c| c.include_only);
2032                let has_any_target_specific_config = parse_prompt_for_attrs(&field.attrs)
2033                    .iter()
2034                    .any(|(name, _)| name != "*");
2035
2036                if has_any_target_specific_config && !is_explicitly_for_this_target {
2037                    continue;
2038                }
2039
2040                if let Some(cfg) = config {
2041                    if cfg.image {
2042                        image_field_parts.push(quote! {
2043                            parts.extend(self.#field_name.to_prompt_parts());
2044                        });
2045                    } else {
2046                        let key = cfg.rename.clone().unwrap_or_else(|| field_name_str.clone());
2047
2048                        let value_expr = if let Some(format_with) = &cfg.format_with {
2049                            // Parse the function path - if it fails, generate code that will produce a compile error
2050                            match syn::parse_str::<syn::Path>(format_with) {
2051                                Ok(func_path) => quote! { #func_path(&self.#field_name) },
2052                                Err(_) => {
2053                                    // Generate a compile error by using an invalid identifier
2054                                    let error_msg = format!(
2055                                        "Invalid function path in format_with: '{}'",
2056                                        format_with
2057                                    );
2058                                    quote! {
2059                                        compile_error!(#error_msg);
2060                                        String::new()
2061                                    }
2062                                }
2063                            }
2064                        } else {
2065                            quote! { self.#field_name.to_prompt() }
2066                        };
2067
2068                        text_field_parts.push(quote! {
2069                            text_parts.push(format!("{}: {}", #key, #value_expr));
2070                        });
2071                    }
2072                } else {
2073                    // Default handling for fields without specific config
2074                    text_field_parts.push(quote! {
2075                        text_parts.push(format!("{}: {}", #field_name_str, self.#field_name.to_prompt()));
2076                    });
2077                }
2078            }
2079
2080            match_arms.push(quote! {
2081                #target_name => {
2082                    let mut parts = Vec::new();
2083
2084                    #(#image_field_parts)*
2085
2086                    let mut text_parts = Vec::new();
2087                    #(#text_field_parts)*
2088
2089                    if !text_parts.is_empty() {
2090                        parts.push(#crate_path::prompt::PromptPart::Text(text_parts.join("\n")));
2091                    }
2092
2093                    Ok(parts)
2094                }
2095            });
2096        }
2097    }
2098
2099    // Collect all target names for error reporting
2100    let target_names: Vec<String> = targets.iter().map(|t| t.name.clone()).collect();
2101
2102    // Add default case for unknown targets
2103    match_arms.push(quote! {
2104        _ => {
2105            let available = vec![#(#target_names.to_string()),*];
2106            Err(#crate_path::prompt::PromptSetError::TargetNotFound {
2107                target: target.to_string(),
2108                available,
2109            })
2110        }
2111    });
2112
2113    let struct_name = &input.ident;
2114    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
2115
2116    let expanded = quote! {
2117        impl #impl_generics #crate_path::prompt::ToPromptSet for #struct_name #ty_generics #where_clause {
2118            fn to_prompt_parts_for(&self, target: &str) -> Result<Vec<#crate_path::prompt::PromptPart>, #crate_path::prompt::PromptSetError> {
2119                match target {
2120                    #(#match_arms)*
2121                }
2122            }
2123        }
2124    };
2125
2126    TokenStream::from(expanded)
2127}
2128
2129/// Wrapper struct for parsing a comma-separated list of types
2130struct TypeList {
2131    types: Punctuated<syn::Type, Token![,]>,
2132}
2133
2134impl Parse for TypeList {
2135    fn parse(input: ParseStream) -> syn::Result<Self> {
2136        Ok(TypeList {
2137            types: Punctuated::parse_terminated(input)?,
2138        })
2139    }
2140}
2141
2142/// Generates a formatted Markdown examples section for the provided types.
2143///
2144/// This macro accepts a comma-separated list of types and generates a single
2145/// formatted Markdown string containing examples of each type.
2146///
2147/// # Example
2148///
2149/// ```rust,ignore
2150/// let examples = examples_section!(User, Concept);
2151/// // Produces a string like:
2152/// // ---
2153/// // ### Examples
2154/// //
2155/// // Here are examples of the data structures you should use.
2156/// //
2157/// // ---
2158/// // #### `User`
2159/// // {...json...}
2160/// // ---
2161/// // #### `Concept`
2162/// // {...json...}
2163/// // ---
2164/// ```
2165#[proc_macro]
2166pub fn examples_section(input: TokenStream) -> TokenStream {
2167    let input = parse_macro_input!(input as TypeList);
2168
2169    let found_crate =
2170        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
2171    let _crate_path = match found_crate {
2172        FoundCrate::Itself => quote!(crate),
2173        FoundCrate::Name(name) => {
2174            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
2175            quote!(::#ident)
2176        }
2177    };
2178
2179    // Generate code for each type
2180    let mut type_sections = Vec::new();
2181
2182    for ty in input.types.iter() {
2183        // Extract the type name as a string
2184        let type_name_str = quote!(#ty).to_string();
2185
2186        // Generate the section for this type
2187        type_sections.push(quote! {
2188            {
2189                let type_name = #type_name_str;
2190                let json_example = <#ty as Default>::default().to_prompt_with_mode("example_only");
2191                format!("---\n#### `{}`\n{}", type_name, json_example)
2192            }
2193        });
2194    }
2195
2196    // Build the complete examples string
2197    let expanded = quote! {
2198        {
2199            let mut sections = Vec::new();
2200            sections.push("---".to_string());
2201            sections.push("### Examples".to_string());
2202            sections.push("".to_string());
2203            sections.push("Here are examples of the data structures you should use.".to_string());
2204            sections.push("".to_string());
2205
2206            #(sections.push(#type_sections);)*
2207
2208            sections.push("---".to_string());
2209
2210            sections.join("\n")
2211        }
2212    };
2213
2214    TokenStream::from(expanded)
2215}
2216
2217/// Helper function to parse struct-level #[prompt_for(target = "...", template = "...")] attribute
2218fn parse_to_prompt_for_attribute(attrs: &[syn::Attribute]) -> (syn::Type, String) {
2219    for attr in attrs {
2220        if attr.path().is_ident("prompt_for")
2221            && let Ok(meta_list) = attr.meta.require_list()
2222            && let Ok(metas) =
2223                meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
2224        {
2225            let mut target_type = None;
2226            let mut template = None;
2227
2228            for meta in metas {
2229                match meta {
2230                    Meta::NameValue(nv) if nv.path.is_ident("target") => {
2231                        if let syn::Expr::Lit(syn::ExprLit {
2232                            lit: syn::Lit::Str(lit_str),
2233                            ..
2234                        }) = nv.value
2235                        {
2236                            // Parse the type string into a syn::Type
2237                            target_type = syn::parse_str::<syn::Type>(&lit_str.value()).ok();
2238                        }
2239                    }
2240                    Meta::NameValue(nv) if nv.path.is_ident("template") => {
2241                        if let syn::Expr::Lit(syn::ExprLit {
2242                            lit: syn::Lit::Str(lit_str),
2243                            ..
2244                        }) = nv.value
2245                        {
2246                            template = Some(lit_str.value());
2247                        }
2248                    }
2249                    _ => {}
2250                }
2251            }
2252
2253            if let (Some(target), Some(tmpl)) = (target_type, template) {
2254                return (target, tmpl);
2255            }
2256        }
2257    }
2258
2259    panic!("ToPromptFor requires #[prompt_for(target = \"TargetType\", template = \"...\")]");
2260}
2261
2262/// A procedural attribute macro that generates prompt-building functions and extractor structs for intent enums.
2263///
2264/// This macro should be applied to an enum to generate:
2265/// 1. A prompt-building function that incorporates enum documentation
2266/// 2. An extractor struct that implements `IntentExtractor`
2267///
2268/// # Requirements
2269///
2270/// The enum must have an `#[intent(...)]` attribute with:
2271/// - `prompt`: The prompt template (supports Jinja-style variables)
2272/// - `extractor_tag`: The tag to use for extraction
2273///
2274/// # Example
2275///
2276/// ```rust,ignore
2277/// #[define_intent]
2278/// #[intent(
2279///     prompt = "Analyze the intent: {{ user_input }}",
2280///     extractor_tag = "intent"
2281/// )]
2282/// enum MyIntent {
2283///     /// Create a new item
2284///     Create,
2285///     /// Update an existing item
2286///     Update,
2287///     /// Delete an item
2288///     Delete,
2289/// }
2290/// ```
2291///
2292/// This will generate:
2293/// - `pub fn build_my_intent_prompt(user_input: &str) -> String`
2294/// - `pub struct MyIntentExtractor;` with `IntentExtractor<MyIntent>` implementation
2295#[proc_macro_attribute]
2296pub fn define_intent(_attr: TokenStream, item: TokenStream) -> TokenStream {
2297    let input = parse_macro_input!(item as DeriveInput);
2298
2299    let found_crate =
2300        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
2301    let crate_path = match found_crate {
2302        FoundCrate::Itself => {
2303            // Even when it's the same crate, use absolute path to support examples/tests/bins
2304            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
2305            quote!(::#ident)
2306        }
2307        FoundCrate::Name(name) => {
2308            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
2309            quote!(::#ident)
2310        }
2311    };
2312
2313    // Verify this is an enum
2314    let enum_data = match &input.data {
2315        Data::Enum(data) => data,
2316        _ => {
2317            return syn::Error::new(
2318                input.ident.span(),
2319                "`#[define_intent]` can only be applied to enums",
2320            )
2321            .to_compile_error()
2322            .into();
2323        }
2324    };
2325
2326    // Parse the #[intent(...)] attribute
2327    let mut prompt_template = None;
2328    let mut extractor_tag = None;
2329    let mut mode = None;
2330
2331    for attr in &input.attrs {
2332        if attr.path().is_ident("intent")
2333            && let Ok(metas) =
2334                attr.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
2335        {
2336            for meta in metas {
2337                match meta {
2338                    Meta::NameValue(nv) if nv.path.is_ident("prompt") => {
2339                        if let syn::Expr::Lit(syn::ExprLit {
2340                            lit: syn::Lit::Str(lit_str),
2341                            ..
2342                        }) = nv.value
2343                        {
2344                            prompt_template = Some(lit_str.value());
2345                        }
2346                    }
2347                    Meta::NameValue(nv) if nv.path.is_ident("extractor_tag") => {
2348                        if let syn::Expr::Lit(syn::ExprLit {
2349                            lit: syn::Lit::Str(lit_str),
2350                            ..
2351                        }) = nv.value
2352                        {
2353                            extractor_tag = Some(lit_str.value());
2354                        }
2355                    }
2356                    Meta::NameValue(nv) if nv.path.is_ident("mode") => {
2357                        if let syn::Expr::Lit(syn::ExprLit {
2358                            lit: syn::Lit::Str(lit_str),
2359                            ..
2360                        }) = nv.value
2361                        {
2362                            mode = Some(lit_str.value());
2363                        }
2364                    }
2365                    _ => {}
2366                }
2367            }
2368        }
2369    }
2370
2371    // Parse the mode parameter (default to "single")
2372    let mode = mode.unwrap_or_else(|| "single".to_string());
2373
2374    // Validate mode
2375    if mode != "single" && mode != "multi_tag" {
2376        return syn::Error::new(
2377            input.ident.span(),
2378            "`mode` must be either \"single\" or \"multi_tag\"",
2379        )
2380        .to_compile_error()
2381        .into();
2382    }
2383
2384    // Validate required attributes
2385    let prompt_template = match prompt_template {
2386        Some(p) => p,
2387        None => {
2388            return syn::Error::new(
2389                input.ident.span(),
2390                "`#[intent(...)]` attribute must include `prompt = \"...\"`",
2391            )
2392            .to_compile_error()
2393            .into();
2394        }
2395    };
2396
2397    // Handle multi_tag mode
2398    if mode == "multi_tag" {
2399        let enum_name = &input.ident;
2400        let actions_doc = generate_multi_tag_actions_doc(&enum_data.variants);
2401        return generate_multi_tag_output(
2402            &input,
2403            enum_name,
2404            enum_data,
2405            prompt_template,
2406            actions_doc,
2407        );
2408    }
2409
2410    // Continue with single mode logic
2411    let extractor_tag = match extractor_tag {
2412        Some(t) => t,
2413        None => {
2414            return syn::Error::new(
2415                input.ident.span(),
2416                "`#[intent(...)]` attribute must include `extractor_tag = \"...\"`",
2417            )
2418            .to_compile_error()
2419            .into();
2420        }
2421    };
2422
2423    // Generate the intents documentation
2424    let enum_name = &input.ident;
2425    let enum_docs = extract_doc_comments(&input.attrs);
2426
2427    let mut intents_doc_lines = Vec::new();
2428
2429    // Add enum description if present
2430    if !enum_docs.is_empty() {
2431        intents_doc_lines.push(format!("{}: {}", enum_name, enum_docs));
2432    } else {
2433        intents_doc_lines.push(format!("{}:", enum_name));
2434    }
2435    intents_doc_lines.push(String::new()); // Empty line
2436    intents_doc_lines.push("Possible values:".to_string());
2437
2438    // Add each variant with its documentation
2439    for variant in &enum_data.variants {
2440        let variant_name = &variant.ident;
2441        let variant_docs = extract_doc_comments(&variant.attrs);
2442
2443        if !variant_docs.is_empty() {
2444            intents_doc_lines.push(format!("- {}: {}", variant_name, variant_docs));
2445        } else {
2446            intents_doc_lines.push(format!("- {}", variant_name));
2447        }
2448    }
2449
2450    let intents_doc_str = intents_doc_lines.join("\n");
2451
2452    // Parse template variables (excluding intents_doc which we'll inject)
2453    let placeholders = parse_template_placeholders_with_mode(&prompt_template);
2454    let user_variables: Vec<String> = placeholders
2455        .iter()
2456        .filter_map(|(name, _)| {
2457            if name != "intents_doc" {
2458                Some(name.clone())
2459            } else {
2460                None
2461            }
2462        })
2463        .collect();
2464
2465    // Generate function name (snake_case)
2466    let enum_name_str = enum_name.to_string();
2467    let snake_case_name = to_snake_case(&enum_name_str);
2468    let function_name = syn::Ident::new(
2469        &format!("build_{}_prompt", snake_case_name),
2470        proc_macro2::Span::call_site(),
2471    );
2472
2473    // Generate function parameters (all &str for simplicity)
2474    let function_params: Vec<proc_macro2::TokenStream> = user_variables
2475        .iter()
2476        .map(|var| {
2477            let ident = syn::Ident::new(var, proc_macro2::Span::call_site());
2478            quote! { #ident: &str }
2479        })
2480        .collect();
2481
2482    // Generate context insertions
2483    let context_insertions: Vec<proc_macro2::TokenStream> = user_variables
2484        .iter()
2485        .map(|var| {
2486            let var_str = var.clone();
2487            let ident = syn::Ident::new(var, proc_macro2::Span::call_site());
2488            quote! {
2489                __template_context.insert(#var_str.to_string(), minijinja::Value::from(#ident));
2490            }
2491        })
2492        .collect();
2493
2494    // Template is already in Jinja syntax, no conversion needed
2495    let converted_template = prompt_template.clone();
2496
2497    // Generate extractor struct name
2498    let extractor_name = syn::Ident::new(
2499        &format!("{}Extractor", enum_name),
2500        proc_macro2::Span::call_site(),
2501    );
2502
2503    // Filter out the #[intent(...)] attribute from the enum attributes
2504    let filtered_attrs: Vec<_> = input
2505        .attrs
2506        .iter()
2507        .filter(|attr| !attr.path().is_ident("intent"))
2508        .collect();
2509
2510    // Rebuild the enum with filtered attributes
2511    let vis = &input.vis;
2512    let generics = &input.generics;
2513    let variants = &enum_data.variants;
2514    let enum_output = quote! {
2515        #(#filtered_attrs)*
2516        #vis enum #enum_name #generics {
2517            #variants
2518        }
2519    };
2520
2521    // Generate the complete output
2522    let expanded = quote! {
2523        // Output the enum without the #[intent(...)] attribute
2524        #enum_output
2525
2526        // Generate the prompt-building function
2527        pub fn #function_name(#(#function_params),*) -> String {
2528            let mut env = minijinja::Environment::new();
2529            env.add_template("prompt", #converted_template)
2530                .expect("Failed to parse intent prompt template");
2531
2532            let tmpl = env.get_template("prompt").unwrap();
2533
2534            let mut __template_context = std::collections::HashMap::new();
2535
2536            // Add intents_doc
2537            __template_context.insert("intents_doc".to_string(), minijinja::Value::from(#intents_doc_str));
2538
2539            // Add user-provided variables
2540            #(#context_insertions)*
2541
2542            tmpl.render(&__template_context)
2543                .unwrap_or_else(|e| format!("Failed to render intent prompt: {}", e))
2544        }
2545
2546        // Generate the extractor struct
2547        pub struct #extractor_name;
2548
2549        impl #extractor_name {
2550            pub const EXTRACTOR_TAG: &'static str = #extractor_tag;
2551        }
2552
2553        impl #crate_path::intent::IntentExtractor<#enum_name> for #extractor_name {
2554            fn extract_intent(&self, response: &str) -> Result<#enum_name, #crate_path::intent::IntentExtractionError> {
2555                // Use the common extraction function with our tag
2556                #crate_path::intent::extract_intent_from_response(response, Self::EXTRACTOR_TAG)
2557            }
2558        }
2559    };
2560
2561    TokenStream::from(expanded)
2562}
2563
2564/// Convert PascalCase to snake_case
2565fn to_snake_case(s: &str) -> String {
2566    let mut result = String::new();
2567    let mut prev_upper = false;
2568
2569    for (i, ch) in s.chars().enumerate() {
2570        if ch.is_uppercase() {
2571            if i > 0 && !prev_upper {
2572                result.push('_');
2573            }
2574            result.push(ch.to_lowercase().next().unwrap());
2575            prev_upper = true;
2576        } else {
2577            result.push(ch);
2578            prev_upper = false;
2579        }
2580    }
2581
2582    result
2583}
2584
2585/// Parse #[action(...)] attributes for enum variants
2586#[derive(Debug, Default)]
2587struct ActionAttrs {
2588    tag: Option<String>,
2589}
2590
2591fn parse_action_attrs(attrs: &[syn::Attribute]) -> ActionAttrs {
2592    let mut result = ActionAttrs::default();
2593
2594    for attr in attrs {
2595        if attr.path().is_ident("action")
2596            && let Ok(meta_list) = attr.meta.require_list()
2597            && let Ok(metas) =
2598                meta_list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
2599        {
2600            for meta in metas {
2601                if let Meta::NameValue(nv) = meta
2602                    && nv.path.is_ident("tag")
2603                    && let syn::Expr::Lit(syn::ExprLit {
2604                        lit: syn::Lit::Str(lit_str),
2605                        ..
2606                    }) = nv.value
2607                {
2608                    result.tag = Some(lit_str.value());
2609                }
2610            }
2611        }
2612    }
2613
2614    result
2615}
2616
2617/// Parse #[action(...)] attributes for struct fields in variants
2618#[derive(Debug, Default)]
2619struct FieldActionAttrs {
2620    is_attribute: bool,
2621    is_inner_text: bool,
2622}
2623
2624fn parse_field_action_attrs(attrs: &[syn::Attribute]) -> FieldActionAttrs {
2625    let mut result = FieldActionAttrs::default();
2626
2627    for attr in attrs {
2628        if attr.path().is_ident("action")
2629            && let Ok(meta_list) = attr.meta.require_list()
2630        {
2631            let tokens_str = meta_list.tokens.to_string();
2632            if tokens_str == "attribute" {
2633                result.is_attribute = true;
2634            } else if tokens_str == "inner_text" {
2635                result.is_inner_text = true;
2636            }
2637        }
2638    }
2639
2640    result
2641}
2642
2643/// Generate actions_doc for multi_tag mode
2644fn generate_multi_tag_actions_doc(
2645    variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
2646) -> String {
2647    let mut doc_lines = Vec::new();
2648
2649    for variant in variants {
2650        let action_attrs = parse_action_attrs(&variant.attrs);
2651
2652        if let Some(tag) = action_attrs.tag {
2653            let variant_docs = extract_doc_comments(&variant.attrs);
2654
2655            match &variant.fields {
2656                syn::Fields::Unit => {
2657                    // Simple tag without parameters
2658                    doc_lines.push(format!("- `<{} />`: {}", tag, variant_docs));
2659                }
2660                syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
2661                    // Tuple variant with inner text
2662                    doc_lines.push(format!("- `<{}>...</{}>`: {}", tag, tag, variant_docs));
2663                }
2664                syn::Fields::Named(fields) => {
2665                    // Struct variant with attributes and/or inner text
2666                    let mut attrs_str = Vec::new();
2667                    let mut has_inner_text = false;
2668
2669                    for field in &fields.named {
2670                        let field_name = field.ident.as_ref().unwrap();
2671                        let field_attrs = parse_field_action_attrs(&field.attrs);
2672
2673                        if field_attrs.is_attribute {
2674                            attrs_str.push(format!("{}=\"...\"", field_name));
2675                        } else if field_attrs.is_inner_text {
2676                            has_inner_text = true;
2677                        }
2678                    }
2679
2680                    let attrs_part = if !attrs_str.is_empty() {
2681                        format!(" {}", attrs_str.join(" "))
2682                    } else {
2683                        String::new()
2684                    };
2685
2686                    if has_inner_text {
2687                        doc_lines.push(format!(
2688                            "- `<{}{}>...</{}>`: {}",
2689                            tag, attrs_part, tag, variant_docs
2690                        ));
2691                    } else if !attrs_str.is_empty() {
2692                        doc_lines.push(format!("- `<{}{} />`: {}", tag, attrs_part, variant_docs));
2693                    } else {
2694                        doc_lines.push(format!("- `<{} />`: {}", tag, variant_docs));
2695                    }
2696
2697                    // Add field documentation
2698                    for field in &fields.named {
2699                        let field_name = field.ident.as_ref().unwrap();
2700                        let field_attrs = parse_field_action_attrs(&field.attrs);
2701                        let field_docs = extract_doc_comments(&field.attrs);
2702
2703                        if field_attrs.is_attribute {
2704                            doc_lines
2705                                .push(format!("  - `{}` (attribute): {}", field_name, field_docs));
2706                        } else if field_attrs.is_inner_text {
2707                            doc_lines
2708                                .push(format!("  - `{}` (inner_text): {}", field_name, field_docs));
2709                        }
2710                    }
2711                }
2712                _ => {
2713                    // Other field types not supported
2714                }
2715            }
2716        }
2717    }
2718
2719    doc_lines.join("\n")
2720}
2721
2722/// Generate regex for matching any of the defined action tags
2723fn generate_tags_regex(
2724    variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
2725) -> String {
2726    let mut tag_names = Vec::new();
2727
2728    for variant in variants {
2729        let action_attrs = parse_action_attrs(&variant.attrs);
2730        if let Some(tag) = action_attrs.tag {
2731            tag_names.push(tag);
2732        }
2733    }
2734
2735    if tag_names.is_empty() {
2736        return String::new();
2737    }
2738
2739    let tags_pattern = tag_names.join("|");
2740    // Match both self-closing tags like <Tag /> and content-based tags like <Tag>...</Tag>
2741    // (?is) enables case-insensitive and single-line mode where . matches newlines
2742    format!(
2743        r"(?is)<(?:{})\b[^>]*/>|<(?:{})\b[^>]*>.*?</(?:{})>",
2744        tags_pattern, tags_pattern, tags_pattern
2745    )
2746}
2747
2748/// Generate output for multi_tag mode
2749fn generate_multi_tag_output(
2750    input: &DeriveInput,
2751    enum_name: &syn::Ident,
2752    enum_data: &syn::DataEnum,
2753    prompt_template: String,
2754    actions_doc: String,
2755) -> TokenStream {
2756    let found_crate =
2757        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
2758    let crate_path = match found_crate {
2759        FoundCrate::Itself => {
2760            // Even when it's the same crate, use absolute path to support examples/tests/bins
2761            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
2762            quote!(::#ident)
2763        }
2764        FoundCrate::Name(name) => {
2765            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
2766            quote!(::#ident)
2767        }
2768    };
2769
2770    // Parse template placeholders
2771    let placeholders = parse_template_placeholders_with_mode(&prompt_template);
2772    let user_variables: Vec<String> = placeholders
2773        .iter()
2774        .filter_map(|(name, _)| {
2775            if name != "actions_doc" {
2776                Some(name.clone())
2777            } else {
2778                None
2779            }
2780        })
2781        .collect();
2782
2783    // Generate function name (snake_case)
2784    let enum_name_str = enum_name.to_string();
2785    let snake_case_name = to_snake_case(&enum_name_str);
2786    let function_name = syn::Ident::new(
2787        &format!("build_{}_prompt", snake_case_name),
2788        proc_macro2::Span::call_site(),
2789    );
2790
2791    // Generate function parameters (all &str for simplicity)
2792    let function_params: Vec<proc_macro2::TokenStream> = user_variables
2793        .iter()
2794        .map(|var| {
2795            let ident = syn::Ident::new(var, proc_macro2::Span::call_site());
2796            quote! { #ident: &str }
2797        })
2798        .collect();
2799
2800    // Generate context insertions
2801    let context_insertions: Vec<proc_macro2::TokenStream> = user_variables
2802        .iter()
2803        .map(|var| {
2804            let var_str = var.clone();
2805            let ident = syn::Ident::new(var, proc_macro2::Span::call_site());
2806            quote! {
2807                __template_context.insert(#var_str.to_string(), minijinja::Value::from(#ident));
2808            }
2809        })
2810        .collect();
2811
2812    // Generate extractor struct name
2813    let extractor_name = syn::Ident::new(
2814        &format!("{}Extractor", enum_name),
2815        proc_macro2::Span::call_site(),
2816    );
2817
2818    // Filter out the #[intent(...)] and #[action(...)] attributes
2819    let filtered_attrs: Vec<_> = input
2820        .attrs
2821        .iter()
2822        .filter(|attr| !attr.path().is_ident("intent"))
2823        .collect();
2824
2825    // Filter action attributes from variants
2826    let filtered_variants: Vec<proc_macro2::TokenStream> = enum_data
2827        .variants
2828        .iter()
2829        .map(|variant| {
2830            let variant_name = &variant.ident;
2831            let variant_attrs: Vec<_> = variant
2832                .attrs
2833                .iter()
2834                .filter(|attr| !attr.path().is_ident("action"))
2835                .collect();
2836            let fields = &variant.fields;
2837
2838            // Filter field attributes
2839            let filtered_fields = match fields {
2840                syn::Fields::Named(named_fields) => {
2841                    let filtered: Vec<_> = named_fields
2842                        .named
2843                        .iter()
2844                        .map(|field| {
2845                            let field_name = &field.ident;
2846                            let field_type = &field.ty;
2847                            let field_vis = &field.vis;
2848                            let filtered_attrs: Vec<_> = field
2849                                .attrs
2850                                .iter()
2851                                .filter(|attr| !attr.path().is_ident("action"))
2852                                .collect();
2853                            quote! {
2854                                #(#filtered_attrs)*
2855                                #field_vis #field_name: #field_type
2856                            }
2857                        })
2858                        .collect();
2859                    quote! { { #(#filtered,)* } }
2860                }
2861                syn::Fields::Unnamed(unnamed_fields) => {
2862                    let types: Vec<_> = unnamed_fields
2863                        .unnamed
2864                        .iter()
2865                        .map(|field| {
2866                            let field_type = &field.ty;
2867                            quote! { #field_type }
2868                        })
2869                        .collect();
2870                    quote! { (#(#types),*) }
2871                }
2872                syn::Fields::Unit => quote! {},
2873            };
2874
2875            quote! {
2876                #(#variant_attrs)*
2877                #variant_name #filtered_fields
2878            }
2879        })
2880        .collect();
2881
2882    let vis = &input.vis;
2883    let generics = &input.generics;
2884
2885    // Generate XML parsing logic for extract_actions
2886    let parsing_arms = generate_parsing_arms(&enum_data.variants, enum_name);
2887
2888    // Generate the regex pattern for matching tags
2889    let tags_regex = generate_tags_regex(&enum_data.variants);
2890
2891    let expanded = quote! {
2892        // Output the enum without the #[intent(...)] and #[action(...)] attributes
2893        #(#filtered_attrs)*
2894        #vis enum #enum_name #generics {
2895            #(#filtered_variants),*
2896        }
2897
2898        // Generate the prompt-building function
2899        pub fn #function_name(#(#function_params),*) -> String {
2900            let mut env = minijinja::Environment::new();
2901            env.add_template("prompt", #prompt_template)
2902                .expect("Failed to parse intent prompt template");
2903
2904            let tmpl = env.get_template("prompt").unwrap();
2905
2906            let mut __template_context = std::collections::HashMap::new();
2907
2908            // Add actions_doc
2909            __template_context.insert("actions_doc".to_string(), minijinja::Value::from(#actions_doc));
2910
2911            // Add user-provided variables
2912            #(#context_insertions)*
2913
2914            tmpl.render(&__template_context)
2915                .unwrap_or_else(|e| format!("Failed to render intent prompt: {}", e))
2916        }
2917
2918        // Generate the extractor struct
2919        pub struct #extractor_name;
2920
2921        impl #extractor_name {
2922            fn parse_single_action(&self, text: &str) -> Option<#enum_name> {
2923                use ::quick_xml::events::Event;
2924                use ::quick_xml::Reader;
2925
2926                let mut actions = Vec::new();
2927                let mut reader = Reader::from_str(text);
2928                reader.config_mut().trim_text(true);
2929
2930                let mut buf = Vec::new();
2931
2932                loop {
2933                    match reader.read_event_into(&mut buf) {
2934                        Ok(Event::Start(e)) => {
2935                            let owned_e = e.into_owned();
2936                            let tag_name = String::from_utf8_lossy(owned_e.name().as_ref()).to_string();
2937                            let is_empty = false;
2938
2939                            #parsing_arms
2940                        }
2941                        Ok(Event::Empty(e)) => {
2942                            let owned_e = e.into_owned();
2943                            let tag_name = String::from_utf8_lossy(owned_e.name().as_ref()).to_string();
2944                            let is_empty = true;
2945
2946                            #parsing_arms
2947                        }
2948                        Ok(Event::Eof) => break,
2949                        Err(_) => {
2950                            // Silently ignore XML parsing errors
2951                            break;
2952                        }
2953                        _ => {}
2954                    }
2955                    buf.clear();
2956                }
2957
2958                actions.into_iter().next()
2959            }
2960
2961            pub fn extract_actions(&self, text: &str) -> Result<Vec<#enum_name>, #crate_path::intent::IntentError> {
2962                use ::quick_xml::events::Event;
2963                use ::quick_xml::Reader;
2964
2965                let mut actions = Vec::new();
2966                let mut reader = Reader::from_str(text);
2967                reader.config_mut().trim_text(true);
2968
2969                let mut buf = Vec::new();
2970
2971                loop {
2972                    match reader.read_event_into(&mut buf) {
2973                        Ok(Event::Start(e)) => {
2974                            let owned_e = e.into_owned();
2975                            let tag_name = String::from_utf8_lossy(owned_e.name().as_ref()).to_string();
2976                            let is_empty = false;
2977
2978                            #parsing_arms
2979                        }
2980                        Ok(Event::Empty(e)) => {
2981                            let owned_e = e.into_owned();
2982                            let tag_name = String::from_utf8_lossy(owned_e.name().as_ref()).to_string();
2983                            let is_empty = true;
2984
2985                            #parsing_arms
2986                        }
2987                        Ok(Event::Eof) => break,
2988                        Err(_) => {
2989                            // Silently ignore XML parsing errors
2990                            break;
2991                        }
2992                        _ => {}
2993                    }
2994                    buf.clear();
2995                }
2996
2997                Ok(actions)
2998            }
2999
3000            pub fn transform_actions<F>(&self, text: &str, mut transformer: F) -> String
3001            where
3002                F: FnMut(#enum_name) -> String,
3003            {
3004                use ::regex::Regex;
3005
3006                let regex_pattern = #tags_regex;
3007                if regex_pattern.is_empty() {
3008                    return text.to_string();
3009                }
3010
3011                let re = Regex::new(&regex_pattern).unwrap_or_else(|e| {
3012                    panic!("Failed to compile regex for action tags: {}", e);
3013                });
3014
3015                re.replace_all(text, |caps: &::regex::Captures| {
3016                    let matched = caps.get(0).map(|m| m.as_str()).unwrap_or("");
3017
3018                    // Try to parse the matched tag as an action
3019                    if let Some(action) = self.parse_single_action(matched) {
3020                        transformer(action)
3021                    } else {
3022                        // If parsing fails, return the original text
3023                        matched.to_string()
3024                    }
3025                }).to_string()
3026            }
3027
3028            pub fn strip_actions(&self, text: &str) -> String {
3029                self.transform_actions(text, |_| String::new())
3030            }
3031        }
3032    };
3033
3034    TokenStream::from(expanded)
3035}
3036
3037/// Generate parsing arms for XML extraction
3038fn generate_parsing_arms(
3039    variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
3040    enum_name: &syn::Ident,
3041) -> proc_macro2::TokenStream {
3042    let mut arms = Vec::new();
3043
3044    for variant in variants {
3045        let variant_name = &variant.ident;
3046        let action_attrs = parse_action_attrs(&variant.attrs);
3047
3048        if let Some(tag) = action_attrs.tag {
3049            match &variant.fields {
3050                syn::Fields::Unit => {
3051                    // Simple tag without parameters
3052                    arms.push(quote! {
3053                        if &tag_name == #tag {
3054                            actions.push(#enum_name::#variant_name);
3055                        }
3056                    });
3057                }
3058                syn::Fields::Unnamed(_fields) => {
3059                    // Tuple variant with inner text - use reader.read_text()
3060                    arms.push(quote! {
3061                        if &tag_name == #tag && !is_empty {
3062                            // Use read_text to get inner text as owned String
3063                            match reader.read_text(owned_e.name()) {
3064                                Ok(text) => {
3065                                    actions.push(#enum_name::#variant_name(text.to_string()));
3066                                }
3067                                Err(_) => {
3068                                    // If reading text fails, push empty string
3069                                    actions.push(#enum_name::#variant_name(String::new()));
3070                                }
3071                            }
3072                        }
3073                    });
3074                }
3075                syn::Fields::Named(fields) => {
3076                    // Struct variant with attributes and/or inner text
3077                    let mut field_names = Vec::new();
3078                    let mut has_inner_text_field = None;
3079
3080                    for field in &fields.named {
3081                        let field_name = field.ident.as_ref().unwrap();
3082                        let field_attrs = parse_field_action_attrs(&field.attrs);
3083
3084                        if field_attrs.is_attribute {
3085                            field_names.push(field_name.clone());
3086                        } else if field_attrs.is_inner_text {
3087                            has_inner_text_field = Some(field_name.clone());
3088                        }
3089                    }
3090
3091                    if let Some(inner_text_field) = has_inner_text_field {
3092                        // Handle inner text
3093                        // Build attribute extraction code
3094                        let attr_extractions: Vec<_> = field_names.iter().map(|field_name| {
3095                            quote! {
3096                                let mut #field_name = String::new();
3097                                for attr in owned_e.attributes() {
3098                                    if let Ok(attr) = attr {
3099                                        if attr.key.as_ref() == stringify!(#field_name).as_bytes() {
3100                                            #field_name = String::from_utf8_lossy(&attr.value).to_string();
3101                                            break;
3102                                        }
3103                                    }
3104                                }
3105                            }
3106                        }).collect();
3107
3108                        arms.push(quote! {
3109                            if &tag_name == #tag {
3110                                #(#attr_extractions)*
3111
3112                                // Check if it's a self-closing tag
3113                                if is_empty {
3114                                    let #inner_text_field = String::new();
3115                                    actions.push(#enum_name::#variant_name {
3116                                        #(#field_names,)*
3117                                        #inner_text_field,
3118                                    });
3119                                } else {
3120                                    // Use read_text to get inner text as owned String
3121                                    match reader.read_text(owned_e.name()) {
3122                                        Ok(text) => {
3123                                            let #inner_text_field = text.to_string();
3124                                            actions.push(#enum_name::#variant_name {
3125                                                #(#field_names,)*
3126                                                #inner_text_field,
3127                                            });
3128                                        }
3129                                        Err(_) => {
3130                                            // If reading text fails, push with empty string
3131                                            let #inner_text_field = String::new();
3132                                            actions.push(#enum_name::#variant_name {
3133                                                #(#field_names,)*
3134                                                #inner_text_field,
3135                                            });
3136                                        }
3137                                    }
3138                                }
3139                            }
3140                        });
3141                    } else {
3142                        // Only attributes
3143                        let attr_extractions: Vec<_> = field_names.iter().map(|field_name| {
3144                            quote! {
3145                                let mut #field_name = String::new();
3146                                for attr in owned_e.attributes() {
3147                                    if let Ok(attr) = attr {
3148                                        if attr.key.as_ref() == stringify!(#field_name).as_bytes() {
3149                                            #field_name = String::from_utf8_lossy(&attr.value).to_string();
3150                                            break;
3151                                        }
3152                                    }
3153                                }
3154                            }
3155                        }).collect();
3156
3157                        arms.push(quote! {
3158                            if &tag_name == #tag {
3159                                #(#attr_extractions)*
3160                                actions.push(#enum_name::#variant_name {
3161                                    #(#field_names),*
3162                                });
3163                            }
3164                        });
3165                    }
3166                }
3167            }
3168        }
3169    }
3170
3171    quote! {
3172        #(#arms)*
3173    }
3174}
3175
3176/// Derives the `ToPromptFor` trait for a struct
3177#[proc_macro_derive(ToPromptFor, attributes(prompt_for))]
3178pub fn to_prompt_for_derive(input: TokenStream) -> TokenStream {
3179    let input = parse_macro_input!(input as DeriveInput);
3180
3181    let found_crate =
3182        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
3183    let crate_path = match found_crate {
3184        FoundCrate::Itself => {
3185            // Even when it's the same crate, use absolute path to support examples/tests/bins
3186            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
3187            quote!(::#ident)
3188        }
3189        FoundCrate::Name(name) => {
3190            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
3191            quote!(::#ident)
3192        }
3193    };
3194
3195    // Parse the struct-level prompt_for attribute
3196    let (target_type, template) = parse_to_prompt_for_attribute(&input.attrs);
3197
3198    let struct_name = &input.ident;
3199    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
3200
3201    // Parse the template to find placeholders
3202    let placeholders = parse_template_placeholders_with_mode(&template);
3203
3204    // Convert template to minijinja syntax and build context generation code
3205    let mut converted_template = template.clone();
3206    let mut context_fields = Vec::new();
3207
3208    // Get struct fields for validation
3209    let fields = match &input.data {
3210        Data::Struct(data_struct) => match &data_struct.fields {
3211            syn::Fields::Named(fields) => &fields.named,
3212            _ => panic!("ToPromptFor is only supported for structs with named fields"),
3213        },
3214        _ => panic!("ToPromptFor is only supported for structs"),
3215    };
3216
3217    // Check if the struct has mode support (has #[prompt(mode = ...)] attribute)
3218    let has_mode_support = input.attrs.iter().any(|attr| {
3219        if attr.path().is_ident("prompt")
3220            && let Ok(metas) =
3221                attr.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
3222        {
3223            for meta in metas {
3224                if let Meta::NameValue(nv) = meta
3225                    && nv.path.is_ident("mode")
3226                {
3227                    return true;
3228                }
3229            }
3230        }
3231        false
3232    });
3233
3234    // Process each placeholder
3235    for (placeholder_name, mode_opt) in &placeholders {
3236        if placeholder_name == "self" {
3237            if let Some(specific_mode) = mode_opt {
3238                // {self:some_mode} - use a unique key
3239                let unique_key = format!("self__{}", specific_mode);
3240
3241                // Replace {{ self:mode }} with {{ self__mode }} in template
3242                let pattern = format!("{{{{ self:{} }}}}", specific_mode);
3243                let replacement = format!("{{{{ {} }}}}", unique_key);
3244                converted_template = converted_template.replace(&pattern, &replacement);
3245
3246                // Add to context with the specific mode
3247                context_fields.push(quote! {
3248                    context.insert(
3249                        #unique_key.to_string(),
3250                        minijinja::Value::from(self.to_prompt_with_mode(#specific_mode))
3251                    );
3252                });
3253            } else {
3254                // {{self}} - already in correct format, no replacement needed
3255
3256                if has_mode_support {
3257                    // If the struct has mode support, use to_prompt_with_mode with the mode parameter
3258                    context_fields.push(quote! {
3259                        context.insert(
3260                            "self".to_string(),
3261                            minijinja::Value::from(self.to_prompt_with_mode(mode))
3262                        );
3263                    });
3264                } else {
3265                    // If the struct doesn't have mode support, use to_prompt() which gives key-value format
3266                    context_fields.push(quote! {
3267                        context.insert(
3268                            "self".to_string(),
3269                            minijinja::Value::from(self.to_prompt())
3270                        );
3271                    });
3272                }
3273            }
3274        } else {
3275            // It's a field placeholder
3276            // Check if the field exists
3277            let field_exists = fields.iter().any(|f| {
3278                f.ident
3279                    .as_ref()
3280                    .is_some_and(|ident| ident == placeholder_name)
3281            });
3282
3283            if field_exists {
3284                let field_ident = syn::Ident::new(placeholder_name, proc_macro2::Span::call_site());
3285
3286                // {{field}} - already in correct format, no replacement needed
3287
3288                // Add field to context - serialize the field value
3289                context_fields.push(quote! {
3290                    context.insert(
3291                        #placeholder_name.to_string(),
3292                        minijinja::Value::from_serialize(&self.#field_ident)
3293                    );
3294                });
3295            }
3296            // If field doesn't exist, we'll let minijinja handle the error at runtime
3297        }
3298    }
3299
3300    let expanded = quote! {
3301        impl #impl_generics #crate_path::prompt::ToPromptFor<#target_type> for #struct_name #ty_generics #where_clause
3302        where
3303            #target_type: serde::Serialize,
3304        {
3305            fn to_prompt_for_with_mode(&self, target: &#target_type, mode: &str) -> String {
3306                // Create minijinja environment and add template
3307                let mut env = minijinja::Environment::new();
3308                env.add_template("prompt", #converted_template).unwrap_or_else(|e| {
3309                    panic!("Failed to parse template: {}", e)
3310                });
3311
3312                let tmpl = env.get_template("prompt").unwrap();
3313
3314                // Build context
3315                let mut context = std::collections::HashMap::new();
3316                // Add self to the context for field access in templates
3317                context.insert(
3318                    "self".to_string(),
3319                    minijinja::Value::from_serialize(self)
3320                );
3321                // Add target to the context
3322                context.insert(
3323                    "target".to_string(),
3324                    minijinja::Value::from_serialize(target)
3325                );
3326                #(#context_fields)*
3327
3328                // Render template
3329                tmpl.render(context).unwrap_or_else(|e| {
3330                    format!("Failed to render prompt: {}", e)
3331                })
3332            }
3333        }
3334    };
3335
3336    TokenStream::from(expanded)
3337}
3338
3339// ============================================================================
3340// Agent Derive Macro
3341// ============================================================================
3342
3343/// Attribute parameters for #[agent(...)]
3344struct AgentAttrs {
3345    expertise: Option<String>,
3346    output: Option<syn::Type>,
3347    backend: Option<String>,
3348    model: Option<String>,
3349    inner: Option<String>,
3350    default_inner: Option<String>,
3351    max_retries: Option<u32>,
3352    profile: Option<String>,
3353}
3354
3355impl Parse for AgentAttrs {
3356    fn parse(input: ParseStream) -> syn::Result<Self> {
3357        let mut expertise = None;
3358        let mut output = None;
3359        let mut backend = None;
3360        let mut model = None;
3361        let mut inner = None;
3362        let mut default_inner = None;
3363        let mut max_retries = None;
3364        let mut profile = None;
3365
3366        let pairs = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
3367
3368        for meta in pairs {
3369            match meta {
3370                Meta::NameValue(nv) if nv.path.is_ident("expertise") => {
3371                    if let syn::Expr::Lit(syn::ExprLit {
3372                        lit: syn::Lit::Str(lit_str),
3373                        ..
3374                    }) = &nv.value
3375                    {
3376                        expertise = Some(lit_str.value());
3377                    }
3378                }
3379                Meta::NameValue(nv) if nv.path.is_ident("output") => {
3380                    if let syn::Expr::Lit(syn::ExprLit {
3381                        lit: syn::Lit::Str(lit_str),
3382                        ..
3383                    }) = &nv.value
3384                    {
3385                        let ty: syn::Type = syn::parse_str(&lit_str.value())?;
3386                        output = Some(ty);
3387                    }
3388                }
3389                Meta::NameValue(nv) if nv.path.is_ident("backend") => {
3390                    if let syn::Expr::Lit(syn::ExprLit {
3391                        lit: syn::Lit::Str(lit_str),
3392                        ..
3393                    }) = &nv.value
3394                    {
3395                        backend = Some(lit_str.value());
3396                    }
3397                }
3398                Meta::NameValue(nv) if nv.path.is_ident("model") => {
3399                    if let syn::Expr::Lit(syn::ExprLit {
3400                        lit: syn::Lit::Str(lit_str),
3401                        ..
3402                    }) = &nv.value
3403                    {
3404                        model = Some(lit_str.value());
3405                    }
3406                }
3407                Meta::NameValue(nv) if nv.path.is_ident("inner") => {
3408                    if let syn::Expr::Lit(syn::ExprLit {
3409                        lit: syn::Lit::Str(lit_str),
3410                        ..
3411                    }) = &nv.value
3412                    {
3413                        inner = Some(lit_str.value());
3414                    }
3415                }
3416                Meta::NameValue(nv) if nv.path.is_ident("default_inner") => {
3417                    if let syn::Expr::Lit(syn::ExprLit {
3418                        lit: syn::Lit::Str(lit_str),
3419                        ..
3420                    }) = &nv.value
3421                    {
3422                        default_inner = Some(lit_str.value());
3423                    }
3424                }
3425                Meta::NameValue(nv) if nv.path.is_ident("max_retries") => {
3426                    if let syn::Expr::Lit(syn::ExprLit {
3427                        lit: syn::Lit::Int(lit_int),
3428                        ..
3429                    }) = &nv.value
3430                    {
3431                        max_retries = Some(lit_int.base10_parse()?);
3432                    }
3433                }
3434                Meta::NameValue(nv) if nv.path.is_ident("profile") => {
3435                    if let syn::Expr::Lit(syn::ExprLit {
3436                        lit: syn::Lit::Str(lit_str),
3437                        ..
3438                    }) = &nv.value
3439                    {
3440                        profile = Some(lit_str.value());
3441                    }
3442                }
3443                _ => {}
3444            }
3445        }
3446
3447        Ok(AgentAttrs {
3448            expertise,
3449            output,
3450            backend,
3451            model,
3452            inner,
3453            default_inner,
3454            max_retries,
3455            profile,
3456        })
3457    }
3458}
3459
3460/// Parse #[agent(...)] attributes from a struct
3461fn parse_agent_attrs(attrs: &[syn::Attribute]) -> syn::Result<AgentAttrs> {
3462    for attr in attrs {
3463        if attr.path().is_ident("agent") {
3464            return attr.parse_args::<AgentAttrs>();
3465        }
3466    }
3467
3468    Ok(AgentAttrs {
3469        expertise: None,
3470        output: None,
3471        backend: None,
3472        model: None,
3473        inner: None,
3474        default_inner: None,
3475        max_retries: None,
3476        profile: None,
3477    })
3478}
3479
3480/// Generate backend-specific convenience constructors
3481fn generate_backend_constructors(
3482    struct_name: &syn::Ident,
3483    backend: &str,
3484    _model: Option<&str>,
3485    _profile: Option<&str>,
3486    crate_path: &proc_macro2::TokenStream,
3487) -> proc_macro2::TokenStream {
3488    match backend {
3489        "claude" => {
3490            quote! {
3491                impl #struct_name {
3492                    /// Create a new agent with ClaudeCodeAgent backend
3493                    pub fn with_claude() -> Self {
3494                        Self::new(#crate_path::agent::impls::ClaudeCodeAgent::new())
3495                    }
3496
3497                    /// Create a new agent with ClaudeCodeAgent backend and specific model
3498                    pub fn with_claude_model(model: &str) -> Self {
3499                        Self::new(
3500                            #crate_path::agent::impls::ClaudeCodeAgent::new()
3501                                .with_model_str(model)
3502                        )
3503                    }
3504                }
3505            }
3506        }
3507        "gemini" => {
3508            quote! {
3509                impl #struct_name {
3510                    /// Create a new agent with GeminiAgent backend
3511                    pub fn with_gemini() -> Self {
3512                        Self::new(#crate_path::agent::impls::GeminiAgent::new())
3513                    }
3514
3515                    /// Create a new agent with GeminiAgent backend and specific model
3516                    pub fn with_gemini_model(model: &str) -> Self {
3517                        Self::new(
3518                            #crate_path::agent::impls::GeminiAgent::new()
3519                                .with_model_str(model)
3520                        )
3521                    }
3522                }
3523            }
3524        }
3525        _ => quote! {},
3526    }
3527}
3528
3529/// Generate Default implementation for the agent
3530fn generate_default_impl(
3531    struct_name: &syn::Ident,
3532    backend: &str,
3533    model: Option<&str>,
3534    profile: Option<&str>,
3535    crate_path: &proc_macro2::TokenStream,
3536) -> proc_macro2::TokenStream {
3537    // Parse profile string to ExecutionProfile
3538    let profile_expr = if let Some(profile_str) = profile {
3539        match profile_str.to_lowercase().as_str() {
3540            "creative" => quote! { #crate_path::agent::ExecutionProfile::Creative },
3541            "balanced" => quote! { #crate_path::agent::ExecutionProfile::Balanced },
3542            "deterministic" => quote! { #crate_path::agent::ExecutionProfile::Deterministic },
3543            _ => quote! { #crate_path::agent::ExecutionProfile::Balanced }, // Default fallback
3544        }
3545    } else {
3546        quote! { #crate_path::agent::ExecutionProfile::default() }
3547    };
3548
3549    let agent_init = match backend {
3550        "gemini" => {
3551            let mut builder = quote! { #crate_path::agent::impls::GeminiAgent::new() };
3552
3553            if let Some(model_str) = model {
3554                builder = quote! { #builder.with_model_str(#model_str) };
3555            }
3556
3557            builder = quote! { #builder.with_execution_profile(#profile_expr) };
3558            builder
3559        }
3560        _ => {
3561            // Default to Claude
3562            let mut builder = quote! { #crate_path::agent::impls::ClaudeCodeAgent::new() };
3563
3564            if let Some(model_str) = model {
3565                builder = quote! { #builder.with_model_str(#model_str) };
3566            }
3567
3568            builder = quote! { #builder.with_execution_profile(#profile_expr) };
3569            builder
3570        }
3571    };
3572
3573    quote! {
3574        impl Default for #struct_name {
3575            fn default() -> Self {
3576                Self::new(#agent_init)
3577            }
3578        }
3579    }
3580}
3581
3582/// Derive macro for implementing the Agent trait
3583///
3584/// # Usage
3585/// ```ignore
3586/// #[derive(Agent)]
3587/// #[agent(expertise = "Rust expert", output = "MyOutputType")]
3588/// struct MyAgent;
3589/// ```
3590#[proc_macro_derive(Agent, attributes(agent))]
3591pub fn derive_agent(input: TokenStream) -> TokenStream {
3592    let input = parse_macro_input!(input as DeriveInput);
3593    let struct_name = &input.ident;
3594
3595    // Parse #[agent(...)] attributes
3596    let agent_attrs = match parse_agent_attrs(&input.attrs) {
3597        Ok(attrs) => attrs,
3598        Err(e) => return e.to_compile_error().into(),
3599    };
3600
3601    let expertise = agent_attrs
3602        .expertise
3603        .unwrap_or_else(|| String::from("general AI assistant"));
3604    let output_type = agent_attrs
3605        .output
3606        .unwrap_or_else(|| syn::parse_str::<syn::Type>("String").unwrap());
3607    let backend = agent_attrs
3608        .backend
3609        .unwrap_or_else(|| String::from("claude"));
3610    let model = agent_attrs.model;
3611    let _profile = agent_attrs.profile; // Not used in simple derive macro
3612    let max_retries = agent_attrs.max_retries.unwrap_or(3); // Default: 3 retries
3613
3614    // Determine crate path
3615    let found_crate =
3616        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
3617    let crate_path = match found_crate {
3618        FoundCrate::Itself => {
3619            // Even when it's the same crate, use absolute path to support examples/tests/bins
3620            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
3621            quote!(::#ident)
3622        }
3623        FoundCrate::Name(name) => {
3624            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
3625            quote!(::#ident)
3626        }
3627    };
3628
3629    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
3630
3631    // Check if output type is String (no JSON enforcement needed)
3632    let output_type_str = quote!(#output_type).to_string().replace(" ", "");
3633    let is_string_output = output_type_str == "String" || output_type_str == "&str";
3634
3635    // Generate enhanced expertise with JSON schema instruction
3636    let enhanced_expertise = if is_string_output {
3637        // Plain text output - no JSON enforcement
3638        quote! { #expertise }
3639    } else {
3640        // Structured output - try to use ToPrompt::prompt_schema(), fallback to type name
3641        let type_name = quote!(#output_type).to_string();
3642        quote! {
3643            {
3644                use std::sync::OnceLock;
3645                static EXPERTISE_CACHE: OnceLock<String> = OnceLock::new();
3646
3647                EXPERTISE_CACHE.get_or_init(|| {
3648                    // Try to get detailed schema from ToPrompt
3649                    let schema = <#output_type as #crate_path::prompt::ToPrompt>::prompt_schema();
3650
3651                    if schema.is_empty() {
3652                        // Fallback: type name only
3653                        format!(
3654                            concat!(
3655                                #expertise,
3656                                "\n\nIMPORTANT: You must respond with valid JSON matching the {} type structure. ",
3657                                "Do not include any text outside the JSON object."
3658                            ),
3659                            #type_name
3660                        )
3661                    } else {
3662                        // Use detailed schema from ToPrompt
3663                        format!(
3664                            concat!(
3665                                #expertise,
3666                                "\n\nIMPORTANT: Respond with valid JSON matching this schema:\n\n{}"
3667                            ),
3668                            schema
3669                        )
3670                    }
3671                }).as_str()
3672            }
3673        }
3674    };
3675
3676    // Generate agent initialization code based on backend
3677    let agent_init = match backend.as_str() {
3678        "gemini" => {
3679            if let Some(model_str) = model {
3680                quote! {
3681                    use #crate_path::agent::impls::GeminiAgent;
3682                    let agent = GeminiAgent::new().with_model_str(#model_str);
3683                }
3684            } else {
3685                quote! {
3686                    use #crate_path::agent::impls::GeminiAgent;
3687                    let agent = GeminiAgent::new();
3688                }
3689            }
3690        }
3691        "claude" => {
3692            if let Some(model_str) = model {
3693                quote! {
3694                    use #crate_path::agent::impls::ClaudeCodeAgent;
3695                    let agent = ClaudeCodeAgent::new().with_model_str(#model_str);
3696                }
3697            } else {
3698                quote! {
3699                    use #crate_path::agent::impls::ClaudeCodeAgent;
3700                    let agent = ClaudeCodeAgent::new();
3701                }
3702            }
3703        }
3704        _ => {
3705            // Default to Claude
3706            if let Some(model_str) = model {
3707                quote! {
3708                    use #crate_path::agent::impls::ClaudeCodeAgent;
3709                    let agent = ClaudeCodeAgent::new().with_model_str(#model_str);
3710                }
3711            } else {
3712                quote! {
3713                    use #crate_path::agent::impls::ClaudeCodeAgent;
3714                    let agent = ClaudeCodeAgent::new();
3715                }
3716            }
3717        }
3718    };
3719
3720    let expanded = quote! {
3721        #[async_trait::async_trait]
3722        impl #impl_generics #crate_path::agent::Agent for #struct_name #ty_generics #where_clause {
3723            type Output = #output_type;
3724
3725            fn expertise(&self) -> &str {
3726                #enhanced_expertise
3727            }
3728
3729            async fn execute(&self, intent: #crate_path::agent::Payload) -> Result<Self::Output, #crate_path::agent::AgentError> {
3730                // Create internal agent based on backend configuration
3731                #agent_init
3732
3733                // Use the unified retry_execution function (DRY principle)
3734                let agent_ref = &agent;
3735                #crate_path::agent::retry::retry_execution(
3736                    #max_retries,
3737                    &intent,
3738                    move |payload| {
3739                        let payload = payload.clone();
3740                        async move {
3741                            // Execute and get response
3742                            let response = agent_ref.execute(payload).await?;
3743
3744                            // Extract JSON from the response
3745                            let json_str = #crate_path::extract_json(&response)
3746                                .map_err(|e| #crate_path::agent::AgentError::ParseError {
3747                                    message: format!("Failed to extract JSON: {}", e),
3748                                    reason: #crate_path::agent::error::ParseErrorReason::MarkdownExtractionFailed,
3749                                })?;
3750
3751                            // Deserialize into output type
3752                            serde_json::from_str::<Self::Output>(&json_str)
3753                                .map_err(|e| {
3754                                    // Determine the parse error reason based on serde_json error type
3755                                    let reason = if e.is_eof() {
3756                                        #crate_path::agent::error::ParseErrorReason::UnexpectedEof
3757                                    } else if e.is_syntax() {
3758                                        #crate_path::agent::error::ParseErrorReason::InvalidJson
3759                                    } else {
3760                                        #crate_path::agent::error::ParseErrorReason::SchemaMismatch
3761                                    };
3762
3763                                    #crate_path::agent::AgentError::ParseError {
3764                                        message: format!("Failed to parse JSON: {}", e),
3765                                        reason,
3766                                    }
3767                                })
3768                        }
3769                    }
3770                ).await
3771            }
3772
3773            async fn is_available(&self) -> Result<(), #crate_path::agent::AgentError> {
3774                // Create internal agent and check availability
3775                #agent_init
3776                agent.is_available().await
3777            }
3778        }
3779    };
3780
3781    TokenStream::from(expanded)
3782}
3783
3784// ============================================================================
3785// Agent Attribute Macro (Generic version with injection support)
3786// ============================================================================
3787
3788/// Attribute macro for implementing the Agent trait with Generic support
3789///
3790/// This version generates a struct definition with Generic inner agent,
3791/// allowing for agent injection and testing with mock agents.
3792///
3793/// # Usage
3794/// ```ignore
3795/// #[agent(expertise = "Rust expert", output = "MyOutputType")]
3796/// struct MyAgent;
3797/// ```
3798#[proc_macro_attribute]
3799pub fn agent(attr: TokenStream, item: TokenStream) -> TokenStream {
3800    // Parse attributes
3801    let agent_attrs = match syn::parse::<AgentAttrs>(attr) {
3802        Ok(attrs) => attrs,
3803        Err(e) => return e.to_compile_error().into(),
3804    };
3805
3806    // Parse the input struct
3807    let input = parse_macro_input!(item as DeriveInput);
3808    let struct_name = &input.ident;
3809    let vis = &input.vis;
3810
3811    let expertise = agent_attrs
3812        .expertise
3813        .unwrap_or_else(|| String::from("general AI assistant"));
3814    let output_type = agent_attrs
3815        .output
3816        .unwrap_or_else(|| syn::parse_str::<syn::Type>("String").unwrap());
3817    let backend = agent_attrs
3818        .backend
3819        .unwrap_or_else(|| String::from("claude"));
3820    let model = agent_attrs.model;
3821    let profile = agent_attrs.profile;
3822
3823    // Check if output type is String (no JSON enforcement needed)
3824    let output_type_str = quote!(#output_type).to_string().replace(" ", "");
3825    let is_string_output = output_type_str == "String" || output_type_str == "&str";
3826
3827    // Determine crate path
3828    let found_crate =
3829        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
3830    let crate_path = match found_crate {
3831        FoundCrate::Itself => {
3832            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
3833            quote!(::#ident)
3834        }
3835        FoundCrate::Name(name) => {
3836            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
3837            quote!(::#ident)
3838        }
3839    };
3840
3841    // Determine generic parameter name for inner agent (default: "A")
3842    let inner_generic_name = agent_attrs.inner.unwrap_or_else(|| String::from("A"));
3843    let inner_generic_ident = syn::Ident::new(&inner_generic_name, proc_macro2::Span::call_site());
3844
3845    // Determine default agent type - prioritize default_inner, fallback to backend
3846    let default_agent_type = if let Some(ref custom_type) = agent_attrs.default_inner {
3847        // Custom type specified via default_inner attribute
3848        let type_path: syn::Type =
3849            syn::parse_str(custom_type).expect("default_inner must be a valid type path");
3850        quote! { #type_path }
3851    } else {
3852        // Use backend to determine default type
3853        match backend.as_str() {
3854            "gemini" => quote! { #crate_path::agent::impls::GeminiAgent },
3855            _ => quote! { #crate_path::agent::impls::ClaudeCodeAgent },
3856        }
3857    };
3858
3859    // Generate struct definition
3860    let struct_def = quote! {
3861        #vis struct #struct_name<#inner_generic_ident = #default_agent_type> {
3862            inner: #inner_generic_ident,
3863        }
3864    };
3865
3866    // Generate basic constructor
3867    let constructors = quote! {
3868        impl<#inner_generic_ident> #struct_name<#inner_generic_ident> {
3869            /// Create a new agent with a custom inner agent implementation
3870            pub fn new(inner: #inner_generic_ident) -> Self {
3871                Self { inner }
3872            }
3873        }
3874    };
3875
3876    // Generate backend-specific constructors and Default implementation
3877    let (backend_constructors, default_impl) = if agent_attrs.default_inner.is_some() {
3878        // Custom type - generate Default impl for the default type
3879        let default_impl = quote! {
3880            impl Default for #struct_name {
3881                fn default() -> Self {
3882                    Self {
3883                        inner: <#default_agent_type as Default>::default(),
3884                    }
3885                }
3886            }
3887        };
3888        (quote! {}, default_impl)
3889    } else {
3890        // Built-in backend - generate backend-specific constructors
3891        let backend_constructors = generate_backend_constructors(
3892            struct_name,
3893            &backend,
3894            model.as_deref(),
3895            profile.as_deref(),
3896            &crate_path,
3897        );
3898        let default_impl = generate_default_impl(
3899            struct_name,
3900            &backend,
3901            model.as_deref(),
3902            profile.as_deref(),
3903            &crate_path,
3904        );
3905        (backend_constructors, default_impl)
3906    };
3907
3908    // Generate enhanced expertise with JSON schema instruction (same as derive macro)
3909    let enhanced_expertise = if is_string_output {
3910        // Plain text output - no JSON enforcement
3911        quote! { #expertise }
3912    } else {
3913        // Structured output - try to use ToPrompt::prompt_schema(), fallback to type name
3914        let type_name = quote!(#output_type).to_string();
3915        quote! {
3916            {
3917                use std::sync::OnceLock;
3918                static EXPERTISE_CACHE: OnceLock<String> = OnceLock::new();
3919
3920                EXPERTISE_CACHE.get_or_init(|| {
3921                    // Try to get detailed schema from ToPrompt
3922                    let schema = <#output_type as #crate_path::prompt::ToPrompt>::prompt_schema();
3923
3924                    if schema.is_empty() {
3925                        // Fallback: type name only
3926                        format!(
3927                            concat!(
3928                                #expertise,
3929                                "\n\nIMPORTANT: You must respond with valid JSON matching the {} type structure. ",
3930                                "Do not include any text outside the JSON object."
3931                            ),
3932                            #type_name
3933                        )
3934                    } else {
3935                        // Use detailed schema from ToPrompt
3936                        format!(
3937                            concat!(
3938                                #expertise,
3939                                "\n\nIMPORTANT: Respond with valid JSON matching this schema:\n\n{}"
3940                            ),
3941                            schema
3942                        )
3943                    }
3944                }).as_str()
3945            }
3946        }
3947    };
3948
3949    // Generate Agent trait implementation
3950    let agent_impl = quote! {
3951        #[async_trait::async_trait]
3952        impl<#inner_generic_ident> #crate_path::agent::Agent for #struct_name<#inner_generic_ident>
3953        where
3954            #inner_generic_ident: #crate_path::agent::Agent<Output = String>,
3955        {
3956            type Output = #output_type;
3957
3958            fn expertise(&self) -> &str {
3959                #enhanced_expertise
3960            }
3961
3962            async fn execute(&self, intent: #crate_path::agent::Payload) -> Result<Self::Output, #crate_path::agent::AgentError> {
3963                // Prepend expertise to the payload
3964                let enhanced_payload = intent.prepend_text(self.expertise());
3965
3966                // Use the inner agent with the enhanced payload
3967                let response = self.inner.execute(enhanced_payload).await?;
3968
3969                // Extract JSON from the response
3970                let json_str = #crate_path::extract_json(&response)
3971                    .map_err(|e| #crate_path::agent::AgentError::ParseError {
3972                        message: e.to_string(),
3973                        reason: #crate_path::agent::error::ParseErrorReason::MarkdownExtractionFailed,
3974                    })?;
3975
3976                // Deserialize into output type
3977                serde_json::from_str(&json_str).map_err(|e| {
3978                    let reason = if e.is_eof() {
3979                        #crate_path::agent::error::ParseErrorReason::UnexpectedEof
3980                    } else if e.is_syntax() {
3981                        #crate_path::agent::error::ParseErrorReason::InvalidJson
3982                    } else {
3983                        #crate_path::agent::error::ParseErrorReason::SchemaMismatch
3984                    };
3985                    #crate_path::agent::AgentError::ParseError {
3986                        message: e.to_string(),
3987                        reason,
3988                    }
3989                })
3990            }
3991
3992            async fn is_available(&self) -> Result<(), #crate_path::agent::AgentError> {
3993                self.inner.is_available().await
3994            }
3995        }
3996    };
3997
3998    let expanded = quote! {
3999        #struct_def
4000        #constructors
4001        #backend_constructors
4002        #default_impl
4003        #agent_impl
4004    };
4005
4006    TokenStream::from(expanded)
4007}
4008
4009/// Derive macro for TypeMarker trait.
4010///
4011/// Automatically implements the TypeMarker trait and adds a `__type` field
4012/// with a default value based on the struct name.
4013///
4014/// # Example
4015///
4016/// ```ignore
4017/// use llm_toolkit::orchestrator::TypeMarker;
4018/// use serde::{Serialize, Deserialize};
4019///
4020/// #[derive(Serialize, Deserialize, TypeMarker)]
4021/// pub struct HighConceptResponse {
4022///     pub reasoning: String,
4023///     pub high_concept: String,
4024/// }
4025///
4026/// // Expands to:
4027/// // - Adds __type: String field with #[serde(default = "...")]
4028/// // - Implements TypeMarker with TYPE_NAME = "HighConceptResponse"
4029/// ```
4030#[proc_macro_derive(TypeMarker)]
4031pub fn derive_type_marker(input: TokenStream) -> TokenStream {
4032    let input = parse_macro_input!(input as DeriveInput);
4033    let struct_name = &input.ident;
4034    let type_name_str = struct_name.to_string();
4035
4036    // Get the crate path for llm_toolkit
4037    let found_crate =
4038        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
4039    let crate_path = match found_crate {
4040        FoundCrate::Itself => {
4041            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
4042            quote!(::#ident)
4043        }
4044        FoundCrate::Name(name) => {
4045            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
4046            quote!(::#ident)
4047        }
4048    };
4049
4050    let expanded = quote! {
4051        impl #crate_path::orchestrator::TypeMarker for #struct_name {
4052            const TYPE_NAME: &'static str = #type_name_str;
4053        }
4054    };
4055
4056    TokenStream::from(expanded)
4057}
4058
4059/// Attribute macro that adds a `__type` field to a struct and implements TypeMarker.
4060///
4061/// This macro transforms a struct by:
4062/// 1. Adding a `__type: String` field with `#[serde(default = "...", skip_serializing)]`
4063/// 2. Generating a default function that returns the struct's type name
4064/// 3. Implementing the `TypeMarker` trait
4065///
4066/// # Example
4067///
4068/// ```ignore
4069/// use llm_toolkit_macros::type_marker;
4070/// use serde::{Serialize, Deserialize};
4071///
4072/// #[type_marker]
4073/// #[derive(Serialize, Deserialize, Debug)]
4074/// pub struct WorldConceptResponse {
4075///     pub concept: String,
4076/// }
4077///
4078/// // Expands to:
4079/// #[derive(Serialize, Deserialize, Debug)]
4080/// pub struct WorldConceptResponse {
4081///     #[serde(default = "default_world_concept_response_type", skip_serializing)]
4082///     __type: String,
4083///     pub concept: String,
4084/// }
4085///
4086/// fn default_world_concept_response_type() -> String {
4087///     "WorldConceptResponse".to_string()
4088/// }
4089///
4090/// impl TypeMarker for WorldConceptResponse {
4091///     const TYPE_NAME: &'static str = "WorldConceptResponse";
4092/// }
4093/// ```
4094#[proc_macro_attribute]
4095pub fn type_marker(_attr: TokenStream, item: TokenStream) -> TokenStream {
4096    let input = parse_macro_input!(item as syn::DeriveInput);
4097    let struct_name = &input.ident;
4098    let vis = &input.vis;
4099    let type_name_str = struct_name.to_string();
4100
4101    // Generate default function name (snake_case)
4102    let default_fn_name = syn::Ident::new(
4103        &format!("default_{}_type", to_snake_case(&type_name_str)),
4104        struct_name.span(),
4105    );
4106
4107    // Get the crate path for llm_toolkit
4108    let found_crate =
4109        crate_name("llm-toolkit").expect("llm-toolkit should be present in `Cargo.toml`");
4110    let crate_path = match found_crate {
4111        FoundCrate::Itself => {
4112            let ident = syn::Ident::new("llm_toolkit", proc_macro2::Span::call_site());
4113            quote!(::#ident)
4114        }
4115        FoundCrate::Name(name) => {
4116            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
4117            quote!(::#ident)
4118        }
4119    };
4120
4121    // Extract struct fields
4122    let fields = match &input.data {
4123        syn::Data::Struct(data_struct) => match &data_struct.fields {
4124            syn::Fields::Named(fields) => &fields.named,
4125            _ => {
4126                return syn::Error::new_spanned(
4127                    struct_name,
4128                    "type_marker only works with structs with named fields",
4129                )
4130                .to_compile_error()
4131                .into();
4132            }
4133        },
4134        _ => {
4135            return syn::Error::new_spanned(struct_name, "type_marker only works with structs")
4136                .to_compile_error()
4137                .into();
4138        }
4139    };
4140
4141    // Create new fields with __type prepended
4142    let mut new_fields = vec![];
4143
4144    // Convert function name to string literal for serde attribute
4145    let default_fn_name_str = default_fn_name.to_string();
4146    let default_fn_name_lit = syn::LitStr::new(&default_fn_name_str, default_fn_name.span());
4147
4148    // Add __type field first
4149    // Note: We don't use skip_serializing here because:
4150    // 1. ToPrompt already excludes __type from LLM prompts at macro generation time
4151    // 2. Orchestrator needs __type in serialized JSON for type-based retrieval (get_typed_output)
4152    new_fields.push(quote! {
4153        #[serde(default = #default_fn_name_lit)]
4154        __type: String
4155    });
4156
4157    // Add original fields
4158    for field in fields {
4159        new_fields.push(quote! { #field });
4160    }
4161
4162    // Get original attributes (like #[derive(...)])
4163    let attrs = &input.attrs;
4164    let generics = &input.generics;
4165
4166    let expanded = quote! {
4167        // Generate the default function
4168        fn #default_fn_name() -> String {
4169            #type_name_str.to_string()
4170        }
4171
4172        // Generate the struct with __type field
4173        #(#attrs)*
4174        #vis struct #struct_name #generics {
4175            #(#new_fields),*
4176        }
4177
4178        // Implement TypeMarker trait
4179        impl #crate_path::orchestrator::TypeMarker for #struct_name {
4180            const TYPE_NAME: &'static str = #type_name_str;
4181        }
4182    };
4183
4184    TokenStream::from(expanded)
4185}