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