apple-foundation-models 0.2.2__cp312-cp312-macosx_26_0_universal2.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ """
2
+ Tool calling utilities for applefoundationmodels.
3
+
4
+ Provides utilities for extracting JSON schemas from Python functions
5
+ and managing tool registrations.
6
+ """
7
+
8
+ import inspect
9
+ from typing import (
10
+ Callable,
11
+ Dict,
12
+ Any,
13
+ Optional,
14
+ get_type_hints,
15
+ get_origin,
16
+ get_args,
17
+ Union,
18
+ )
19
+ from .exceptions import ToolCallError
20
+
21
+
22
+ # Type mapping for efficient schema generation
23
+ # Maps Python types and their string names to JSON schema types
24
+ _TYPE_MAP = {
25
+ str: "string",
26
+ int: "integer",
27
+ float: "number",
28
+ bool: "boolean",
29
+ "str": "string",
30
+ "int": "integer",
31
+ "float": "number",
32
+ "bool": "boolean",
33
+ }
34
+
35
+
36
+ def is_optional_type(python_type: Any) -> bool:
37
+ """
38
+ Check if a type is Optional (Union[X, None]).
39
+
40
+ Args:
41
+ python_type: Python type hint
42
+
43
+ Returns:
44
+ True if the type is Optional, False otherwise
45
+ """
46
+ origin = get_origin(python_type)
47
+ if origin is Union:
48
+ args = get_args(python_type)
49
+ return type(None) in args
50
+ return False
51
+
52
+
53
+ def unwrap_optional(python_type: Any) -> Any:
54
+ """
55
+ Extract the non-None type from Optional[X] or Union[X, None].
56
+
57
+ Args:
58
+ python_type: Python type hint that may be Optional
59
+
60
+ Returns:
61
+ The unwrapped type (X from Optional[X])
62
+ """
63
+ origin = get_origin(python_type)
64
+ if origin is Union:
65
+ args = get_args(python_type)
66
+ # Filter out None and return the first non-None type
67
+ non_none_types = [arg for arg in args if arg is not type(None)]
68
+ if non_none_types:
69
+ return non_none_types[0]
70
+ return python_type
71
+
72
+
73
+ def _handle_list_type(python_type: Any) -> Dict[str, Any]:
74
+ """
75
+ Extract list/array type with optional item schema.
76
+
77
+ Args:
78
+ python_type: List or generic list type hint
79
+
80
+ Returns:
81
+ JSON Schema for array type
82
+ """
83
+ args = get_args(python_type)
84
+ if args:
85
+ items_schema = python_type_to_json_schema(args[0])
86
+ return {"type": "array", "items": items_schema}
87
+ return {"type": "array"}
88
+
89
+
90
+ def _handle_dict_type(python_type: Any) -> Dict[str, Any]:
91
+ """
92
+ Extract dict/object type with optional value schema.
93
+
94
+ Args:
95
+ python_type: Dict or generic dict type hint
96
+
97
+ Returns:
98
+ JSON Schema for object type
99
+ """
100
+ args = get_args(python_type)
101
+ if len(args) == 2 and args[0] is str:
102
+ value_schema = python_type_to_json_schema(args[1])
103
+ return {"type": "object", "additionalProperties": value_schema}
104
+ return {"type": "object"}
105
+
106
+
107
+ def python_type_to_json_schema(python_type: Any) -> Dict[str, Any]:
108
+ """
109
+ Convert a Python type hint to a JSON Schema type definition.
110
+
111
+ Args:
112
+ python_type: Python type hint
113
+
114
+ Returns:
115
+ JSON Schema type definition
116
+
117
+ Raises:
118
+ ToolCallError: If type cannot be converted
119
+ """
120
+ # Handle None type
121
+ if python_type is type(None):
122
+ return {"type": "null"}
123
+
124
+ # Handle Optional[X] / Union[X, None] - unwrap and process the inner type
125
+ if is_optional_type(python_type):
126
+ inner_type = unwrap_optional(python_type)
127
+ return python_type_to_json_schema(inner_type)
128
+
129
+ # Check basic types and string annotations via unified lookup table
130
+ if python_type in _TYPE_MAP:
131
+ return {"type": _TYPE_MAP[python_type]}
132
+
133
+ # Get origin for generic types
134
+ origin = get_origin(python_type)
135
+
136
+ # Handle container types
137
+ if python_type is list or origin is list:
138
+ return _handle_list_type(python_type)
139
+
140
+ if python_type is dict or origin is dict:
141
+ return _handle_dict_type(python_type)
142
+
143
+ # Default fallback for unknown types
144
+ return {"type": "string"}
145
+
146
+
147
+ def extract_function_schema(func: Callable) -> Dict[str, Any]:
148
+ """
149
+ Extract JSON Schema from a Python function's signature and docstring.
150
+
151
+ Args:
152
+ func: Python function to extract schema from
153
+
154
+ Returns:
155
+ Dictionary containing:
156
+ - name: Function name
157
+ - description: Function description from docstring
158
+ - parameters: JSON Schema for function parameters
159
+
160
+ Raises:
161
+ ToolCallError: If schema cannot be extracted
162
+ """
163
+ try:
164
+ # Get function signature
165
+ sig = inspect.signature(func)
166
+
167
+ # Get type hints
168
+ try:
169
+ type_hints = get_type_hints(func)
170
+ except Exception:
171
+ # If type hints fail (e.g., forward references), inspect parameters directly
172
+ type_hints = {}
173
+
174
+ # Extract parameter schemas
175
+ properties = {}
176
+ required = []
177
+
178
+ for param_name, param in sig.parameters.items():
179
+ # Skip self and cls parameters
180
+ if param_name in ("self", "cls"):
181
+ continue
182
+
183
+ # Get type annotation
184
+ param_type = type_hints.get(param_name, param.annotation)
185
+
186
+ # Handle parameters without type hints
187
+ if param_type is inspect.Parameter.empty:
188
+ # Default to string type if no annotation
189
+ param_schema = {"type": "string"}
190
+ else:
191
+ param_schema = python_type_to_json_schema(param_type)
192
+
193
+ properties[param_name] = param_schema
194
+
195
+ # Mark as required if no default value
196
+ if param.default is inspect.Parameter.empty:
197
+ required.append(param_name)
198
+
199
+ # Build parameters schema
200
+ parameters_schema = {
201
+ "type": "object",
202
+ "properties": properties,
203
+ }
204
+
205
+ if required:
206
+ parameters_schema["required"] = required
207
+
208
+ # Extract description from docstring
209
+ description = ""
210
+ if func.__doc__:
211
+ # Get the first line or paragraph of the docstring
212
+ lines = func.__doc__.strip().split("\n")
213
+ description = lines[0].strip()
214
+
215
+ # Get function name
216
+ name = func.__name__
217
+
218
+ return {
219
+ "name": name,
220
+ "description": description,
221
+ "parameters": parameters_schema,
222
+ }
223
+
224
+ except Exception as e:
225
+ raise ToolCallError(
226
+ f"Failed to extract schema from function '{func.__name__}': {e}", -98
227
+ ) from e
228
+
229
+
230
+ def register_tool_for_function(func: Callable) -> Dict[str, Any]:
231
+ """
232
+ Extract schema from a function and prepare it for registration.
233
+
234
+ This is used when registering tools with a session. It extracts the
235
+ schema from the function signature, attaches metadata to the function
236
+ object, and returns the schema ready for FFI registration.
237
+
238
+ Args:
239
+ func: Function to register as a tool
240
+
241
+ Returns:
242
+ Complete tool schema ready for registration
243
+
244
+ Raises:
245
+ ToolCallError: If schema cannot be extracted
246
+ """
247
+ schema = extract_function_schema(func)
248
+
249
+ # Use shared helper to attach metadata
250
+ return attach_tool_metadata(func, schema)
251
+
252
+
253
+ def attach_tool_metadata(
254
+ func: Callable,
255
+ schema: Dict[str, Any],
256
+ description: Optional[str] = None,
257
+ name: Optional[str] = None,
258
+ ) -> Dict[str, Any]:
259
+ """
260
+ Attach tool metadata to a function and return final schema.
261
+
262
+ This is a shared helper used by both the standalone @tool decorator
263
+ and the Session.tool() method to avoid code duplication.
264
+
265
+ Args:
266
+ func: Function to attach metadata to
267
+ schema: Base schema from extract_function_schema
268
+ description: Optional override for description
269
+ name: Optional override for name
270
+
271
+ Returns:
272
+ Final schema with overrides applied
273
+ """
274
+ # Override with provided values
275
+ if description is not None:
276
+ schema["description"] = description
277
+ if name is not None:
278
+ schema["name"] = name
279
+
280
+ # Attach metadata to function
281
+ func._tool_name = schema["name"] # type: ignore[attr-defined]
282
+ func._tool_description = schema["description"] # type: ignore[attr-defined]
283
+ func._tool_parameters = schema["parameters"] # type: ignore[attr-defined]
284
+
285
+ return schema
@@ -0,0 +1,284 @@
1
+ """
2
+ Type definitions for libai Python bindings.
3
+
4
+ This module provides TypedDicts, enums, and type aliases for type-safe
5
+ interaction with the library.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from enum import IntEnum
10
+ from typing import (
11
+ TypedDict,
12
+ Optional,
13
+ Callable,
14
+ Any,
15
+ Union,
16
+ Dict,
17
+ List,
18
+ Type,
19
+ TYPE_CHECKING,
20
+ cast,
21
+ )
22
+ from typing_extensions import NotRequired
23
+
24
+ if TYPE_CHECKING:
25
+ from pydantic import BaseModel
26
+
27
+
28
+ class Result(IntEnum):
29
+ """
30
+ Result codes for AI operations.
31
+
32
+ These codes indicate the success or failure state of library operations.
33
+ """
34
+
35
+ SUCCESS = 0
36
+ INIT_FAILED = -1
37
+ NOT_AVAILABLE = -2
38
+ INVALID_PARAMS = -3
39
+ MEMORY = -4
40
+ JSON_PARSE = -5
41
+ GENERATION = -6
42
+ TIMEOUT = -7
43
+ SESSION_NOT_FOUND = -8
44
+ STREAM_NOT_FOUND = -9
45
+ GUARDRAIL_VIOLATION = -10
46
+ TOOL_NOT_FOUND = -11
47
+ TOOL_EXECUTION = -12
48
+ BUFFER_TOO_SMALL = -13
49
+ UNKNOWN = -99
50
+
51
+
52
+ class Availability(IntEnum):
53
+ """
54
+ Apple Intelligence availability status.
55
+
56
+ Indicates whether Apple Intelligence is available and ready for use
57
+ on the current device and system configuration.
58
+ """
59
+
60
+ AVAILABLE = 1
61
+ DEVICE_NOT_ELIGIBLE = -1
62
+ NOT_ENABLED = -2
63
+ MODEL_NOT_READY = -3
64
+ AVAILABILITY_UNKNOWN = -99
65
+
66
+
67
+ class SessionConfig(TypedDict, total=False):
68
+ """
69
+ Session configuration options.
70
+
71
+ Configuration for creating an AI session. Sessions maintain conversation
72
+ state and can be configured with tools and instructions.
73
+
74
+ Attributes:
75
+ instructions: Optional system instructions to guide AI behavior
76
+ """
77
+
78
+ instructions: NotRequired[Optional[str]]
79
+
80
+
81
+ class GenerationParams(TypedDict, total=False):
82
+ """
83
+ Text generation parameters.
84
+
85
+ Controls various aspects of AI text generation including randomness
86
+ and length limits.
87
+
88
+ Attributes:
89
+ temperature: Generation randomness (0.0 = deterministic, 2.0 = very random)
90
+ max_tokens: Maximum response tokens (0 = use system default)
91
+ """
92
+
93
+ temperature: NotRequired[float]
94
+ max_tokens: NotRequired[int]
95
+
96
+
97
+ @dataclass
98
+ class Function:
99
+ """
100
+ Function call information from a tool call.
101
+
102
+ Represents the function that was called, including its name and arguments.
103
+ Follows OpenAI's pattern for tool call representation.
104
+
105
+ Attributes:
106
+ name: The name of the function that was called
107
+ arguments: JSON string containing the function arguments
108
+
109
+ Example:
110
+ >>> func = Function(name="get_weather", arguments='{"location": "Paris"}')
111
+ >>> print(func.name)
112
+ get_weather
113
+ """
114
+
115
+ name: str
116
+ arguments: str # JSON string of arguments
117
+
118
+
119
+ @dataclass
120
+ class ToolCall:
121
+ """
122
+ A tool call made during generation.
123
+
124
+ Represents a single tool/function call that occurred during text generation.
125
+ Follows OpenAI's pattern where tool calls are exposed directly on the response.
126
+
127
+ Attributes:
128
+ id: Unique identifier for this tool call
129
+ type: Type of tool call (currently only "function" is supported)
130
+ function: The function call details (name and arguments)
131
+
132
+ Example:
133
+ >>> tool_call = ToolCall(
134
+ ... id="call_123",
135
+ ... type="function",
136
+ ... function=Function(name="get_weather", arguments='{"location": "Paris"}')
137
+ ... )
138
+ >>> print(tool_call.function.name)
139
+ get_weather
140
+ """
141
+
142
+ id: str
143
+ type: str # "function" - matches OpenAI's pattern
144
+ function: Function
145
+
146
+
147
+ @dataclass
148
+ class GenerationResponse:
149
+ """
150
+ Response from non-streaming generation.
151
+
152
+ Provides a unified interface for both text and structured generation results.
153
+ Use the .text property for text responses and .parsed for structured outputs.
154
+
155
+ Attributes:
156
+ content: The generated content (str for text, dict for structured)
157
+ is_structured: True if response is structured JSON, False for text
158
+ tool_calls: List of tool calls made during generation (None if no tools called)
159
+ finish_reason: Reason generation stopped ("stop", "tool_calls", "length", etc.)
160
+ metadata: Optional metadata about the generation
161
+
162
+ Example (text):
163
+ >>> response = session.generate("Hello")
164
+ >>> print(response.text)
165
+
166
+ Example (structured):
167
+ >>> response = session.generate("Extract name and age", schema={...})
168
+ >>> data = response.parsed
169
+ >>> person = Person(**data) # Parse into Pydantic model
170
+
171
+ Example (tool calls):
172
+ >>> response = session.generate("What's the weather in Paris?")
173
+ >>> if response.tool_calls:
174
+ ... for tool_call in response.tool_calls:
175
+ ... print(f"Called {tool_call.function.name}")
176
+ """
177
+
178
+ content: Union[str, Dict[str, Any]]
179
+ is_structured: bool
180
+ tool_calls: Optional[List[ToolCall]] = None
181
+ finish_reason: Optional[str] = None
182
+ metadata: Optional[Dict[str, Any]] = None
183
+
184
+ @property
185
+ def text(self) -> str:
186
+ """
187
+ Get response as text.
188
+
189
+ Returns:
190
+ The generated text
191
+
192
+ Raises:
193
+ ValueError: If response is structured (use .parsed instead)
194
+ """
195
+ if self.is_structured:
196
+ raise ValueError(
197
+ "Response is structured output. Use .parsed property instead of .text"
198
+ )
199
+ return cast(str, self.content)
200
+
201
+ @property
202
+ def parsed(self) -> Dict[str, Any]:
203
+ """
204
+ Get response as structured data.
205
+
206
+ Returns:
207
+ The parsed JSON dictionary
208
+
209
+ Raises:
210
+ ValueError: If response is text (use .text instead)
211
+ """
212
+ if not self.is_structured:
213
+ raise ValueError(
214
+ "Response is text output. Use .text property instead of .parsed"
215
+ )
216
+ return cast(Dict[str, Any], self.content)
217
+
218
+ def parse_as(self, model: "Type[BaseModel]") -> "BaseModel":
219
+ """
220
+ Parse structured response into a Pydantic model.
221
+
222
+ Args:
223
+ model: Pydantic BaseModel class to parse into
224
+
225
+ Returns:
226
+ Instantiated Pydantic model
227
+
228
+ Raises:
229
+ ValueError: If response is not structured
230
+ ImportError: If pydantic is not installed
231
+
232
+ Example:
233
+ >>> from pydantic import BaseModel
234
+ >>> class Person(BaseModel):
235
+ ... name: str
236
+ ... age: int
237
+ >>> response = session.generate("Extract: Alice is 28", schema=Person)
238
+ >>> person = response.parse_as(Person)
239
+ >>> print(person.name, person.age)
240
+ """
241
+ return model(**self.parsed)
242
+
243
+
244
+ @dataclass
245
+ class StreamChunk:
246
+ """
247
+ A chunk from streaming generation.
248
+
249
+ Represents a single delta in the streaming response. Multiple chunks
250
+ combine to form the complete response.
251
+
252
+ Attributes:
253
+ content: The text content delta for this chunk
254
+ finish_reason: Reason streaming ended (None for intermediate chunks)
255
+ index: Chunk sequence index (usually 0 for single-stream responses)
256
+
257
+ Example:
258
+ >>> for chunk in session.generate("Tell a story", stream=True):
259
+ ... print(chunk.content, end='', flush=True)
260
+ ... if chunk.finish_reason:
261
+ ... print(f"\\n[Finished: {chunk.finish_reason}]")
262
+ """
263
+
264
+ content: str
265
+ finish_reason: Optional[str] = None
266
+ index: int = 0
267
+
268
+
269
+ # Callback type aliases
270
+ StreamCallback = Callable[[Optional[str]], None]
271
+ """
272
+ Callback function for streaming text generation.
273
+
274
+ Called incrementally during streaming generation for each token or chunk.
275
+ None indicates completion or error.
276
+ """
277
+
278
+ ToolCallback = Callable[[dict], Any]
279
+ """
280
+ Callback function for tool execution.
281
+
282
+ Receives tool parameters as a dict and should return the tool result.
283
+ The result will be automatically JSON-serialized.
284
+ """