# For licensing see accompanying LICENSE file.
# Copyright (C) 2026 Apple Inc. All Rights Reserved.
"""
Guided generation support for Foundation Models SDK for Python.
This module provides classes and decorators that mirror Swift's Foundation Models
GenerationSchema, GeneratedContent, and @Generable macro functionality.
"""
from .c_helpers import _ManagedObject, _get_error_string
from .generation_schema import GenerationSchema
from .errors import GenerationErrorCode, _status_code_to_exception
import logging
from typing import (
Any,
Dict,
Optional,
Union,
Type,
Protocol,
runtime_checkable,
get_args,
)
import json
import ctypes
logger = logging.getLogger(__name__)
try:
from . import _ctypes_bindings as lib
except ImportError:
raise (ImportError("Python C bindings missing"))
# MARK: GenerationID
class GenerationID:
"""Represents a unique identifier for generated content."""
def __init__(self):
import uuid
self._id = str(uuid.uuid4())
def __str__(self):
return self._id
def __eq__(self, other):
return isinstance(other, GenerationID) and self._id == other._id
def __hash__(self):
return hash(self._id)
# MARK: Generated Content
[docs]
class GeneratedContent(_ManagedObject):
"""
Represents generated content, similar to Swift's GeneratedContent.
This is the actual content generated according to a schema.
"""
[docs]
def __init__(
self,
content_dict: Optional[Dict] = None,
id: Optional[GenerationID] = None,
_ptr=None,
):
"""
Create GeneratedContent.
:param content_dict: Dictionary representation of the content
:type content_dict: Optional[Dict]
:param id: Optional GenerationID
:type id: Optional[GenerationID]
"""
if _ptr is not None:
# Internal constructor for specific ptr
super().__init__(_ptr)
# Extract data from the C pointer if available
if _ptr:
# Get JSON string from C pointer
json_cstr = lib.FMGeneratedContentGetJSONString(_ptr)
# Check if we got a valid result
if json_cstr and not (
hasattr(json_cstr, "data") and json_cstr.data is None
):
# The return value is wrapped in a String object by ctypes
# The String wrapper handles memory, so we don't need to manually free
json_str = str(json_cstr)
content_dict = json.loads(json_str)
else:
raise ValueError("Failed to get content from C pointer")
else:
# Create from dictionary using C bindings
if content_dict:
json_str = json.dumps(content_dict).encode("utf-8")
error_code = ctypes.c_int32() # C error status code
error_description = ctypes.POINTER(
ctypes.c_char
)() # C error description pointer
ptr = lib.FMGeneratedContentCreateFromJSON(
json_str, ctypes.byref(error_code), ctypes.byref(error_description)
)
if error_code.value != GenerationErrorCode.SUCCESS:
# An error occurred, raise appropriate exception
err_code, err_desc = _get_error_string(
error_code, error_description
)
error_msg = "Failed to create GeneratedContent from JSON"
if err_desc:
error_msg = error_msg + ": " + err_desc
raise _status_code_to_exception(
err_code or error_code.value, error_msg
)
super().__init__(ptr)
self.id = id or GenerationID()
self._content_dict = content_dict or {}
[docs]
@classmethod
def from_json(cls, json_str: str) -> "GeneratedContent":
"""Create GeneratedContent from JSON string."""
content_dict = json.loads(json_str)
return cls(content_dict)
[docs]
def to_json(self) -> str:
"""Convert to JSON string."""
if lib and self._ptr:
# Use C binding to get JSON string
json_cstr = lib.FMGeneratedContentGetJSONString(self._ptr)
if json_cstr:
# The return value is wrapped in a String object by ctypes
# The String wrapper handles memory, so we don't need to manually free
return str(json_cstr)
# Fallback
return json.dumps(self._content_dict)
[docs]
def value(
self, type_class: Optional[Type] = None, for_property: Optional[str] = None
) -> Any:
"""
Extract a value from the generated content.
:param type_class: The type to convert to
:type type_class: Optional[Type]
:param for_property: The property name to extract (if None, extract the whole content)
:type for_property: Optional[str]
:return: The extracted value converted to the specified type
:rtype: Any
"""
if for_property:
# Extract specific property
raw_value = self._content_dict.get(for_property)
else:
# Extract whole content
raw_value = self._content_dict
# Handle potential nested Generable types
if type_class:
return self._unpack_nested_generables(type_class, raw_value, for_property)
# Default return raw value
return raw_value
def _convert_value(self, value_str: str, type_class: Type) -> Any:
"""Convert a string value to the specified type."""
if type_class is str:
return value_str
elif type_class is int:
try:
return int(value_str)
except Exception as e:
logger.warning(
f"Failed to convert '{value_str}' to int: {e}, returning 0"
)
return 0
elif type_class is float:
try:
return float(value_str)
except Exception as e:
logger.warning(
f"Failed to convert '{value_str}' to float: {e}, returning 0.0"
)
return 0.0
elif type_class is bool:
return value_str.lower() in ("true", "1", "yes")
elif hasattr(type_class, "__origin__") and type_class.__origin__ is list:
# Handle List[T] types
try:
# First try to parse as JSON array
return json.loads(value_str)
except Exception as e:
# If that fails, split by common delimiters and clean up
logger.debug(
f"Failed to parse '{value_str}' as JSON list: {e}, trying delimiter split"
)
if "," in value_str:
return [
item.strip() for item in value_str.split(",") if item.strip()
]
elif ";" in value_str:
return [
item.strip() for item in value_str.split(";") if item.strip()
]
else:
# Single item - return as single-element list
return [value_str.strip()] if value_str.strip() else []
else:
# Try to parse as JSON
try:
return json.loads(value_str)
except Exception as e:
logger.debug(
f"Failed to parse '{value_str}' as JSON: {e}, returning as string"
)
return value_str
def _unpack_nested_generables(
self,
type_class: Type,
raw_value: Any,
for_property: Optional[str] = None,
) -> Any:
"""Recursively unpack nested Generable types."""
# Get outer container type if any
origin_type = (
type_class.__origin__
if type_class and hasattr(type_class, "__origin__")
else None
)
# Handle simple Generable type
if isinstance(type_class, Generable):
content = GeneratedContent(raw_value) # Wrap raw value
return type_class._from_generated_content(content)
# Handle list of Generable type
if origin_type is list:
non_none_types = [
arg for arg in get_args(type_class) if arg is not type(None)
]
if isinstance(raw_value, list) and len(non_none_types) == 1:
# Only one non-None type supported
actual_type = non_none_types[0]
# Recursively unpack inner types
return [
self._unpack_nested_generables(actual_type, item, for_property)
for item in raw_value
]
elif raw_value is None:
return [] # Return empty list for None
else:
raise TypeError(
f"Expected list for property '{for_property}', got {type(raw_value)}"
)
# Handle optional type (Union[T, None])
if origin_type is Union:
non_none_types = [
arg for arg in get_args(type_class) if arg is not type(None)
]
# Only one non-None type supported
if len(non_none_types) == 1:
actual_type = non_none_types[0]
if raw_value is None:
return None # Valid since it's optional
# Recursively unpack since it might be a Generable or list of Generables
return self._unpack_nested_generables(
actual_type, raw_value, for_property
)
# Default return raw value, no Generable found
return raw_value
@property
def is_complete(self) -> bool:
"""Check if the generated content is complete."""
if lib and self._ptr:
return lib.FMGeneratedContentIsComplete(self._ptr)
# Fallback - assume complete if we have content
return bool(self._content_dict)
# MARK: Protocols
class ConvertibleFromGeneratedContent(Protocol):
"""
Protocol for types that can be created from GeneratedContent.
Equivalent to Swift's ConvertibleFromGeneratedContent.
"""
@classmethod
def _from_generated_content(cls, content: GeneratedContent):
"""Create instance from GeneratedContent."""
raise NotImplementedError(
"Subclasses must implement _from_generated_content class method"
)
class ConvertibleToGeneratedContent(Protocol):
"""
Protocol for types that can be converted to GeneratedContent.
Equivalent to Swift's ConvertibleToGeneratedContent.
"""
@property
def generated_content(self) -> GeneratedContent:
"""Convert this object to GeneratedContent."""
raise NotImplementedError(
"Subclasses must implement generated_content property"
)
# MARK: Generable
[docs]
@runtime_checkable
class Generable(
ConvertibleFromGeneratedContent, ConvertibleToGeneratedContent, Protocol
):
"""
Protocol for types that support structured generation.
Equivalent to Swift's Generable protocol.
"""
# Attributes for generable types
_generable: bool = True
_generable_description: Optional[str] = None
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
raise TypeError(
"Subclassing Protocol Generable is not allowed. "
"Use the @fm.generable() decorator instead."
)
[docs]
@classmethod
def generation_schema(cls) -> GenerationSchema:
"""Get the generation schema for this type."""
raise NotImplementedError(
"Generable types must implement generation_schema class method"
)
# Default PartiallyGenerated type - can be overridden
PartiallyGenerated = None