Source code for apple_fm_sdk.generable_utils

# For licensing see accompanying LICENSE file.
# Copyright (C) 2026 Apple Inc. All Rights Reserved.

"""
Utilities for making Python classes generable with Foundation Models.

This module provides the decorator pattern and helper functions for converting
regular Python classes (particularly dataclasses) into generable types that can
be used with Foundation Models' guided generation features.

The main component is the :func:`generable` decorator, which transforms a class
to support structured generation by adding schema generation, content conversion,
and partial generation capabilities.

Example:
    Basic usage:
        import apple_fm_sdk as fm

        @generable("A cat's profile")
        class Cat:
            name: str = fm.guide("Cat's name")
            age: int = fm.guide("Age in years", range=(0, 20))
            breed: str = fm.guide("Cat breed")

        # The class now has generation_schema() method
        schema = Cat.generation_schema()

        # Can be used with Session.respond() for guided generation
        cat = session.respond(Cat, prompt="Generate a cat named Maomao who is 2 years old")

.. note::
    This module handles the internal mechanics of the generable decorator.
    Most users will only need to use the :func:`generable` decorator itself.
"""

from .generable import (
    Generable,
    GenerationSchema,
    GeneratedContent,
    GenerationID,
    ConvertibleFromGeneratedContent,
)
from .generation_property import Property
from dataclasses import dataclass, field
from typing import Optional, Union, get_type_hints, get_args, Type, List
import logging

logger = logging.getLogger(__name__)


[docs] def generable(description: Optional[str] = None): """ Decorator that makes a class generable for use with Foundation Models. This decorator transforms a regular Python class (typically a dataclass) into a generable type that can be used with Foundation Models' guided generation features. It adds all necessary methods and attributes to support schema generation, content conversion, and partial generation during streaming. The decorator performs the following transformations: 1. Converts the class to a dataclass if it isn't already 2. Adds a ``generation_schema()`` class method for schema introspection 3. Adds ``ConvertibleFromGeneratedContent`` support for deserialization 4. Adds ``ConvertibleToGeneratedContent`` support for serialization 5. Creates a ``PartiallyGenerated`` inner class for streaming support 6. Adds required methods for structured generation :param description: Optional human-readable description of what this type represents. This description is included in the generation schema and can help guide the model's generation behavior. :type description: Optional[str] :return: A decorator function that transforms the class :rtype: Callable[[Type], Type[Generable]] Example: Basic usage with a dataclass:: import apple_fm_sdk as fm @fm.generable("A cat's profile") class Cat: name: str = fm.guide("Cat's name") age: int = fm.guide("Age in years", range=(0, 20)) profile: str = fm.guide("What makes this cat unique") Using with Session for guided generation:: session = fm.LanguageModelSession() cat = await session.respond( Cat, prompt="Generate a cat named Maomao who is 2 years old and has a fluffy tail" ) print(f"{cat.name} is {cat.age} years old: {cat.profile}") Nested generable types:: import apple_fm_sdk as fm @fm.generable("Pet club") class PetClub: name: str = fm.guide("Club name") cats: [Cat] = fm.guide("List of cats in the club") .. note:: @fm.generable automatically applies the ``@dataclass`` decorator if the class is not already a dataclass. .. seealso:: :func:`guide` for adding constraints to individual fields. :class:`GenerationSchema` for the schema representation. :class:`Session` for using generable types in generation. """ def decorator(cls) -> type[Generable]: # Convert to dataclass if not already if not hasattr(cls, "__dataclass_fields__"): cls = dataclass(cls) # Store generable metadata. # We need _generable as an alternative to protocols for certain dynamic type scenarios. cls._generable = True cls._generable_description = description cls.generation_schema = classmethod( generation_schema ) # makes schema generation a class method # Add ConvertibleFromGeneratedContent support cls._from_generated_content = classmethod(_from_generated_content) # Add ConvertibleToGeneratedContent support cls.generated_content = property(generated_content) # Create PartiallyGenerated inner class cls.PartiallyGenerated = create_partially_generated(cls) return cls return decorator
# MARK: - Schema Helpers def resolve_referenced_generables( field_type, outer_class_name: str ) -> Optional[List[GenerationSchema]]: """ Resolve nested generable types referenced by a field. This helper function recursively examines a field's type to find any nested generable types (for example, a field of type ``Cat`` where ``Cat`` is itself a generable class). It handles collections (List, Optional) and prevents infinite recursion for self-referential types. :param field_type: The type annotation of the field to examine :type field_type: Type :param outer_class_name: Name of the outer class to detect self-references :type outer_class_name: str :return: List of GenerationSchema objects for nested generable types, or None if no nested generables are found or if a self-reference is detected :rtype: Optional[List[GenerationSchema]] .. note:: This function is used internally by the schema generation process to build the complete schema graph including nested types. """ # Check if the field_type is a generable class itself if hasattr(field_type, "_generable") and field_type._generable is True: if field_type.__name__ == outer_class_name: return None # Avoid infinite recursion on self-references schema = field_type.generation_schema() return [ schema, *schema.dynamic_nested_types, ] # Include nested references # Unpack collections or optional types to find generable inner types for inner_type in get_args(field_type): return resolve_referenced_generables(inner_type, outer_class_name) def generation_schema(cls_inner, description: Optional[str] = None) -> GenerationSchema: """ Generate a GenerationSchema from a generable class. This function introspects a generable class to create a complete schema representation including all properties, their types, descriptions, guides, and any nested generable types. The schema can then be used for guided generation with Foundation Models. :param cls_inner: The generable class to create a schema for :type cls_inner: Type :param description: Optional description override. If not provided, uses the description from the generable decorator :type description: Optional[str] :return: A GenerationSchema representing the class structure :rtype: GenerationSchema .. note:: This function is typically called automatically via the class method added by the generable decorator. Users don't usually need to call this directly. .. seealso:: :func:`generable` decorator which adds this as a class method. :class:`GenerationSchema` for the schema representation. """ properties = [] referenced_schemas: list[GenerationSchema] = [] referenced_schema_names: set[str] = set() type_hints = get_type_hints( cls_inner, localns={cls_inner.__name__: cls_inner}, include_extras=True ) # Namespace annotation needed for self-referential types for field_name, field_info in cls_inner.__dataclass_fields__.items(): field_type = type_hints.get(field_name, str) # Get any referenced generable types reference = resolve_referenced_generables(field_type, cls_inner.__name__) if reference: for schema in reference: # Add only unique schemas to avoid duplicate types if schema.type_class.__name__ not in referenced_schema_names: referenced_schema_names.add(schema.type_class.__name__) referenced_schemas.append(schema) # Get description and guides from field metadata field_description = None field_guides = [] if hasattr(field_info, "metadata") and field_info.metadata: field_description = field_info.metadata.get("description") field_guides = field_info.metadata.get("guides", []) prop = Property( name=field_name, type_class=field_type, description=field_description, guides=field_guides, ) properties.append(prop) return GenerationSchema( type_class=cls_inner, description=description, properties=properties, dynamic_nested_types=referenced_schemas, ) # MARK: - GeneratedContent Helpers # Add ConvertibleFromGeneratedContent support def _from_generated_content(cls_inner, content: GeneratedContent): """Create instance from GeneratedContent.""" kwargs = {} type_hints = get_type_hints(cls_inner) for field_name in cls_inner.__dataclass_fields__: try: field_type = type_hints[field_name] value = content.value(field_type, for_property=field_name) kwargs[field_name] = value except Exception as error: raise ValueError( f"Failed to convert GeneratedContent to {cls_inner.__name__}: " f"could not set field '{field_name}' with error: {error}" ) return cls_inner(**kwargs) # Add ConvertibleToGeneratedContent support def generated_content(self) -> GeneratedContent: """Convert this instance to GeneratedContent.""" content_dict = {} for field_name in self.__dataclass_fields__: content_dict[field_name] = getattr(self, field_name) return GeneratedContent(content_dict) # MARK: - PartiallyGenerated Helpers # Add _from_generated_content to PartiallyGenerated def partial_from_generated_content(cls, partial_cls, content: GeneratedContent): """Create partial instance from GeneratedContent.""" kwargs: dict = {"id": content.id} for field_name in cls.__dataclass_fields__: try: field_type = get_type_hints(cls)[field_name] value = content.value(field_type, for_property=field_name) kwargs[field_name] = value except Exception as e: # Field not available - leave as None logger.debug(f"Field '{field_name}' not available in partial content: {e}") kwargs[field_name] = None return partial_cls(**kwargs) def create_partially_generated(cls) -> Type: # Create PartiallyGenerated inner class partial_fields = {} partial_annotations = {} type_hints = get_type_hints( cls, localns={cls.__name__: cls} ) # Namespace annotation needed for self-referential types for field_name, field_info in cls.__dataclass_fields__.items(): field_type = type_hints.get(field_name, str) # Make all fields optional for partial generation if hasattr(field_type, "__origin__") and field_type.__origin__ is Union: # Already optional partial_annotations[field_name] = field_type else: # Make optional partial_annotations[field_name] = Optional[field_type] # All fields get default None for partial generation partial_fields[field_name] = field(default=None) # Add ID field for partial generation partial_annotations["id"] = GenerationID partial_fields["id"] = field(default_factory=GenerationID) # Create the PartiallyGenerated class partial_class = type( f"{cls.__name__}PartiallyGenerated", (ConvertibleFromGeneratedContent,), { "__annotations__": partial_annotations, "__module__": cls.__module__, "_from_generated_content": classmethod(partial_from_generated_content), **partial_fields, }, ) partial_class = dataclass(partial_class) return partial_class