99 ConvertibleFromGeneratedContent ,
1010)
1111from .generation_property import Property
12+ from .errors import InvalidGenerationSchemaError
1213from dataclasses import dataclass , field
13- from typing import Optional , Union , get_type_hints , get_args , Type , List
14+ from typing import (
15+ Optional ,
16+ Union ,
17+ get_type_hints ,
18+ get_args ,
19+ Type ,
20+ List ,
21+ Callable ,
22+ overload ,
23+ )
1424import logging
1525
1626logger = logging .getLogger (__name__ )
1727
1828
19- def generable (description : Optional [str ] = None ):
29+ class GenerableDecoratorError (InvalidGenerationSchemaError ):
30+ """Error raised when the @fm.generable decorator is used incorrectly."""
31+
32+ pass
33+
34+
35+ # Overload signatures for type checkers
36+ @overload
37+ def generable (arg : Type [object ], / ) -> Type [Generable ]:
38+ """When used without parentheses: @generable"""
39+ ...
40+
41+
42+ @overload
43+ def generable (arg : None = ..., / ) -> Callable [[Type [object ]], Type [Generable ]]:
44+ """When used with empty parentheses: @generable()"""
45+ ...
46+
47+
48+ @overload
49+ def generable (arg : str , / ) -> Callable [[Type [object ]], Type [Generable ]]:
50+ """When used with a description: @generable("description")"""
51+ ...
52+
53+
54+ def generable (
55+ arg : Optional [Union [type , str ]] = None ,
56+ ) -> Union [Type [Generable ], Callable [[Type ], Type [Generable ]]]:
2057 """
2158 Decorator that makes a class generable for use with Foundation Models.
2259
@@ -34,12 +71,19 @@ def generable(description: Optional[str] = None):
3471 5. Creates a ``PartiallyGenerated`` inner class for streaming support
3572 6. Adds required methods for structured generation
3673
37- :param description: Optional human-readable description of what this type
38- represents. This description is included in the generation schema and
39- can help guide the model's generation behavior.
40- :type description: Optional[str]
41- :return: A decorator function that transforms the class
42- :rtype: Callable[[Type], Type[Generable]]
74+ This decorator can be used with or without parentheses:
75+ - ``@fm.generable`` - without parentheses
76+ - ``@fm.generable()`` - with empty parentheses
77+ - ``@fm.generable("description")`` - with a description
78+
79+ :param arg: Either a class (when used without parentheses) or an optional
80+ human-readable description of what this type represents. This description
81+ is included in the generation schema and can help guide the model's
82+ generation behavior.
83+ :type arg: Optional[Union[type, str]]
84+ :return: Either the decorated class (when used without parentheses) or a
85+ decorator function (when used with parentheses)
86+ :rtype: Union[Type[Generable], Callable[[Type], Type[Generable]]]
4387
4488 Example:
4589 Basic usage with a dataclass::
@@ -51,6 +95,14 @@ class Cat:
5195 name: str = fm.guide("Cat's name")
5296 age: int = fm.guide("Age in years", range=(0, 20))
5397 profile: str = fm.guide("What makes this cat unique")
98+
99+ Usage without parentheses::
100+
101+ @fm.generable
102+ class Dog:
103+ name: str
104+ breed: str
105+
54106 Using with Session for guided generation::
55107
56108 session = fm.LanguageModelSession()
@@ -79,32 +131,124 @@ class is not already a dataclass.
79131 :class:`Session` for using generable types in generation.
80132 """
81133
82- def decorator (cls ) -> type [Generable ]:
83- # Convert to dataclass if not already
84- if not hasattr (cls , "__dataclass_fields__" ):
85- cls = dataclass (cls )
134+ # If arg is a class, we're being used without parentheses: @generable
135+ if isinstance (arg , type ):
136+ return _apply_generable_decorator (arg , description = None )
86137
87- # Store generable metadata.
88- # We need _generable as an alternative to protocols for certain dynamic type scenarios.
89- cls ._generable = True
90- cls ._generable_description = description
138+ # Otherwise, we're being used with parentheses: @generable() or @generable("description")
139+ description = arg
91140
92- cls .generation_schema = classmethod (
93- generation_schema
94- ) # makes schema generation a class method
141+ def decorator (cls : type ) -> type [Generable ]:
142+ return _apply_generable_decorator (cls , description = description )
95143
96- # Add ConvertibleFromGeneratedContent support
97- cls ._from_generated_content = classmethod (_from_generated_content )
144+ return decorator
98145
99- # Add ConvertibleToGeneratedContent support
100- cls .generated_content = property (generated_content )
101146
102- # Create PartiallyGenerated inner class
103- cls .PartiallyGenerated = create_partially_generated (cls )
147+ def _apply_generable_decorator (
148+ cls : type , description : Optional [str ]
149+ ) -> type [Generable ]:
150+ """
151+ Internal function that applies the generable transformation to a class.
104152
105- return cls
153+ :param cls: The class to transform
154+ :param description: Optional description for the generable type
155+ :return: The transformed class
156+ """
157+ # Validate that we're decorating a class
158+ if not isinstance (cls , type ):
159+ raise GenerableDecoratorError (
160+ f"@fm.generable can only be applied to classes, not { type (cls ).__name__ } .\n \n "
161+ "Correct usage:\n "
162+ " @fm.generable\n "
163+ " class MyClass:\n "
164+ " field: str\n \n "
165+ "Or with a description:\n "
166+ " @fm.generable('A description of MyClass')\n "
167+ " class MyClass:\n "
168+ " field: str"
169+ )
106170
107- return decorator
171+ # Validate that the class has type annotations
172+ if not hasattr (cls , "__annotations__" ) or not cls .__annotations__ :
173+ raise GenerableDecoratorError (
174+ f"@fm.generable requires the class '{ cls .__name__ } ' to have type-annotated fields.\n \n "
175+ "Correct usage:\n "
176+ " @fm.generable\n "
177+ f" class { cls .__name__ } :\n "
178+ " name: str # Type annotation is required\n "
179+ " age: int # Type annotation is required\n \n "
180+ "Incorrect usage:\n "
181+ f" class { cls .__name__ } :\n "
182+ " name = '' # Missing type annotation\n "
183+ " age = 0 # Missing type annotation"
184+ )
185+
186+ # Convert to dataclass if not already
187+ try :
188+ if not hasattr (cls , "__dataclass_fields__" ):
189+ cls = dataclass (cls )
190+ except Exception as e :
191+ raise GenerableDecoratorError (
192+ f"Failed to convert '{ cls .__name__ } ' to a dataclass: { e } \n \n "
193+ "The @fm.generable decorator requires classes to be compatible with @dataclass.\n "
194+ "Common issues:\n "
195+ " - Fields must have type annotations\n "
196+ " - Mutable default values (like lists or dicts) must use field(default_factory=...)\n "
197+ " - Class must not have conflicting __init__ or other special methods\n \n "
198+ "Example of correct usage:\n "
199+ " from dataclasses import field\n "
200+ " import apple_fm_sdk as fm\n \n "
201+ " @fm.generable\n "
202+ f" class { cls .__name__ } :\n "
203+ " name: str\n "
204+ " tags: list[str] = field(default_factory=list) # Use field() for mutable defaults"
205+ ) from e
206+
207+ # Validate field types are supported
208+ try :
209+ get_type_hints (cls , localns = {cls .__name__ : cls }, include_extras = True )
210+ except Exception as e :
211+ raise GenerableDecoratorError (
212+ f"Failed to resolve type hints for '{ cls .__name__ } ': { e } \n \n "
213+ "This usually happens when:\n "
214+ " - Forward references are not properly quoted\n "
215+ " - Type annotations use undefined types\n "
216+ " - Circular imports prevent type resolution\n \n "
217+ "Example of correct usage with forward references:\n "
218+ " @fm.generable\n "
219+ f" class { cls .__name__ } :\n "
220+ " name: str\n "
221+ " parent: Optional['MyClass'] = None # Quote self-references"
222+ ) from e
223+
224+ # Store generable metadata.
225+ # We need _generable as an alternative to protocols for certain dynamic type scenarios.
226+ cls ._generable = True
227+ cls ._generable_description = description
228+
229+ cls .generation_schema = classmethod (
230+ generation_schema
231+ ) # makes schema generation a class method
232+
233+ # Add ConvertibleFromGeneratedContent support
234+ cls ._from_generated_content = classmethod (_from_generated_content )
235+
236+ # Add ConvertibleToGeneratedContent support
237+ cls .generated_content = property (generated_content )
238+
239+ # Create PartiallyGenerated inner class
240+ try :
241+ cls .PartiallyGenerated = create_partially_generated (cls )
242+ except Exception as e :
243+ raise GenerableDecoratorError (
244+ f"Failed to create PartiallyGenerated class for '{ cls .__name__ } ': { e } \n \n "
245+ "This is an internal error. Please ensure:\n "
246+ " - All field types are properly annotated\n "
247+ " - Field types are serializable (str, int, float, bool, list, dict, or other @fm.generable types)\n "
248+ " - No unsupported types like datetime, custom objects without @fm.generable, etc."
249+ ) from e
250+
251+ return cls
108252
109253
110254# MARK: - Schema Helpers
0 commit comments