Skip to content

Commit 8d56a2d

Browse files
authored
Make @generable decorator more flexible (#10)
1 parent 0f65c9b commit 8d56a2d

4 files changed

Lines changed: 470 additions & 27 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@ async def main():
7272
asyncio.run(main())
7373
```
7474

75+
You can also use guided generation to ask the model to generate an
76+
object from a specific Python class marked with the `generable` decorator:
77+
78+
```python
79+
import apple_fm_sdk as fm
80+
81+
@fm.generable # This decorator signals this type be generated by a model
82+
class Cat:
83+
name: str
84+
age:int = fm.guide("Age in years", range=(0, 20))
85+
86+
async def generate_cat():
87+
# Get the default system foundation model
88+
model = fm.SystemLanguageModel()
89+
90+
# Check if the model is available
91+
is_available, reason = model.is_available()
92+
if is_available:
93+
# Create a session
94+
session = fm.LanguageModelSession()
95+
96+
# Generate a response of the type Cat
97+
cat = await session.respond("Generate an adorable rescue cat", generating=Cat)
98+
print(f"Model response: {cat}")
99+
else:
100+
print(f"Foundation Models not available: {reason}")
101+
```
102+
75103
### Development Installation
76104

77105
If you need to modify the SDK or install from source:

src/apple_fm_sdk/generable_utils.py

Lines changed: 171 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,51 @@
99
ConvertibleFromGeneratedContent,
1010
)
1111
from .generation_property import Property
12+
from .errors import InvalidGenerationSchemaError
1213
from 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+
)
1424
import logging
1525

1626
logger = 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

tests/doc_tests/test_readme_snippets.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,42 @@ async def main():
5353
await main()
5454
##############################################################################
5555
print("✅ Installation snippet - PASSED")
56+
57+
58+
@pytest.mark.asyncio
59+
async def test_guided_generation_snippet(model):
60+
"""Test the guided generation snippet with generable decorator"""
61+
print("\n=== Testing Guided Generation Snippet ===")
62+
63+
##############################################################################
64+
# README Section: Basic Usage - Guided Generation
65+
import apple_fm_sdk as fm
66+
67+
@fm.generable # This decorator signals this type be generated by a model
68+
class Cat:
69+
name: str
70+
age: int = fm.guide("Age in years", range=(0, 20))
71+
72+
async def generate_cat():
73+
# Get the default system foundation model
74+
model = fm.SystemLanguageModel()
75+
76+
# Check if the model is available
77+
is_available, reason = model.is_available()
78+
if is_available:
79+
# Create a session
80+
session = fm.LanguageModelSession()
81+
82+
# Generate a response of the type Cat
83+
cat = await session.respond(
84+
"Generate an adorable rescue cat", generating=Cat
85+
)
86+
print(f"Model response: {cat}")
87+
else:
88+
print(f"Foundation Models not available: {reason}")
89+
90+
##############################################################################
91+
92+
# Execute the async function
93+
await generate_cat()
94+
print("✅ Guided generation snippet - PASSED")

0 commit comments

Comments
 (0)