schemez 1.1.1__py3-none-any.whl → 1.2.0__py3-none-any.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.
- schemez/__init__.py +21 -0
- schemez/bind_kwargs.py +193 -0
- schemez/create_type.py +340 -0
- schemez/executable.py +211 -0
- schemez/functionschema.py +772 -0
- schemez/schema_generators.py +215 -0
- schemez/typedefs.py +205 -0
- {schemez-1.1.1.dist-info → schemez-1.2.0.dist-info}/METADATA +2 -1
- schemez-1.2.0.dist-info/RECORD +19 -0
- schemez-1.1.1.dist-info/RECORD +0 -13
- {schemez-1.1.1.dist-info → schemez-1.2.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,772 @@
|
|
1
|
+
"""Module for creating OpenAI function schemas from Python functions."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from collections.abc import (
|
6
|
+
Callable, # noqa: TC003
|
7
|
+
Sequence, # noqa: F401
|
8
|
+
)
|
9
|
+
import dataclasses
|
10
|
+
from datetime import date, datetime, time, timedelta, timezone
|
11
|
+
import decimal
|
12
|
+
import enum
|
13
|
+
import inspect
|
14
|
+
import ipaddress
|
15
|
+
import logging
|
16
|
+
from pathlib import Path
|
17
|
+
import re
|
18
|
+
import types
|
19
|
+
import typing
|
20
|
+
from typing import Annotated, Any, Literal, NotRequired, Required, TypeGuard
|
21
|
+
from uuid import UUID
|
22
|
+
|
23
|
+
import docstring_parser
|
24
|
+
import pydantic
|
25
|
+
|
26
|
+
from schemez.typedefs import (
|
27
|
+
OpenAIFunctionDefinition,
|
28
|
+
OpenAIFunctionTool,
|
29
|
+
ToolParameters,
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
if typing.TYPE_CHECKING:
|
34
|
+
from schemez.typedefs import Property
|
35
|
+
|
36
|
+
|
37
|
+
logger = logging.getLogger(__name__)
|
38
|
+
|
39
|
+
|
40
|
+
class FunctionType(str, enum.Enum):
|
41
|
+
"""Enum representing different function types."""
|
42
|
+
|
43
|
+
SYNC = "sync"
|
44
|
+
ASYNC = "async"
|
45
|
+
SYNC_GENERATOR = "sync_generator"
|
46
|
+
ASYNC_GENERATOR = "async_generator"
|
47
|
+
|
48
|
+
|
49
|
+
def get_param_type(param_details: Property) -> type[Any]:
|
50
|
+
"""Get the Python type for a parameter based on its schema details."""
|
51
|
+
if "enum" in param_details:
|
52
|
+
# For enum parameters, we just use str since we can't reconstruct
|
53
|
+
# the exact enum class
|
54
|
+
return str
|
55
|
+
|
56
|
+
type_map = {
|
57
|
+
"string": str,
|
58
|
+
"integer": int,
|
59
|
+
"number": float,
|
60
|
+
"boolean": bool,
|
61
|
+
"array": list,
|
62
|
+
"object": dict,
|
63
|
+
}
|
64
|
+
return type_map.get(param_details.get("type", "string"), Any) # type: ignore
|
65
|
+
|
66
|
+
|
67
|
+
class FunctionSchema(pydantic.BaseModel):
|
68
|
+
"""Schema representing an OpenAI function definition and metadata.
|
69
|
+
|
70
|
+
This class encapsulates all the necessary information to describe a function to the
|
71
|
+
OpenAI API, including its name, description, parameters, return type, and execution
|
72
|
+
characteristics. It follows the OpenAI function calling format while adding
|
73
|
+
additional metadata useful for Python function handling.
|
74
|
+
"""
|
75
|
+
|
76
|
+
name: str
|
77
|
+
"""The name of the function as it will be presented to the OpenAI API."""
|
78
|
+
|
79
|
+
description: str | None = None
|
80
|
+
"""
|
81
|
+
Optional description of what the function does. This helps the AI understand
|
82
|
+
when and how to use the function.
|
83
|
+
"""
|
84
|
+
|
85
|
+
parameters: ToolParameters = pydantic.Field(
|
86
|
+
default_factory=lambda: ToolParameters(type="object", properties={}),
|
87
|
+
)
|
88
|
+
"""
|
89
|
+
JSON Schema object describing the function's parameters. Contains type information,
|
90
|
+
descriptions, and constraints for each parameter.
|
91
|
+
"""
|
92
|
+
|
93
|
+
required: list[str] = pydantic.Field(default_factory=list)
|
94
|
+
"""
|
95
|
+
List of parameter names that are required (do not have default values).
|
96
|
+
These parameters must be provided when calling the function.
|
97
|
+
"""
|
98
|
+
|
99
|
+
returns: dict[str, Any] = pydantic.Field(
|
100
|
+
default_factory=lambda: {"type": "object"},
|
101
|
+
)
|
102
|
+
"""
|
103
|
+
JSON Schema object describing the function's return type. Used for type checking
|
104
|
+
and documentation purposes.
|
105
|
+
"""
|
106
|
+
|
107
|
+
function_type: FunctionType = FunctionType.SYNC
|
108
|
+
"""
|
109
|
+
The execution pattern of the function (sync, async, generator, or async generator).
|
110
|
+
Used to determine how to properly invoke the function.
|
111
|
+
"""
|
112
|
+
|
113
|
+
model_config = pydantic.ConfigDict(frozen=True)
|
114
|
+
|
115
|
+
def _create_pydantic_model(self) -> type[pydantic.BaseModel]:
|
116
|
+
"""Create a Pydantic model from the schema parameters."""
|
117
|
+
fields: dict[str, tuple[type[Any] | Literal, pydantic.Field]] = {} # type: ignore
|
118
|
+
properties = self.parameters.get("properties", {})
|
119
|
+
required = self.parameters.get("required", self.required)
|
120
|
+
|
121
|
+
for name, details in properties.items():
|
122
|
+
# Get base type
|
123
|
+
if "enum" in details:
|
124
|
+
values = tuple(details["enum"]) # type: ignore
|
125
|
+
param_type = Literal[values] # type: ignore
|
126
|
+
else:
|
127
|
+
type_map = {
|
128
|
+
"string": str,
|
129
|
+
"integer": int,
|
130
|
+
"number": float,
|
131
|
+
"boolean": bool,
|
132
|
+
"array": list[Any], # type: ignore
|
133
|
+
"object": dict[str, Any], # type: ignore
|
134
|
+
}
|
135
|
+
param_type = type_map.get(details.get("type", "string"), Any)
|
136
|
+
|
137
|
+
# Handle optional types (if there's a default of None)
|
138
|
+
default_value = details.get("default")
|
139
|
+
if default_value is None and name not in required:
|
140
|
+
param_type = param_type | None # type: ignore
|
141
|
+
|
142
|
+
# Create a proper pydantic Field
|
143
|
+
field = (
|
144
|
+
param_type,
|
145
|
+
pydantic.Field(default=... if name in required else default_value),
|
146
|
+
)
|
147
|
+
fields[name] = field
|
148
|
+
|
149
|
+
return pydantic.create_model(f"{self.name}_params", **fields) # type: ignore
|
150
|
+
|
151
|
+
def model_dump_openai(self) -> OpenAIFunctionTool:
|
152
|
+
"""Convert the schema to OpenAI's function calling format.
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
A dictionary matching OpenAI's complete function tool definition format.
|
156
|
+
|
157
|
+
Example:
|
158
|
+
```python
|
159
|
+
schema = FunctionSchema(
|
160
|
+
name="get_weather",
|
161
|
+
description="Get weather information for a location",
|
162
|
+
parameters={
|
163
|
+
"type": "object",
|
164
|
+
"properties": {
|
165
|
+
"location": {"type": "string"},
|
166
|
+
"unit": {"type": "string", "enum": ["C", "F"]}
|
167
|
+
}
|
168
|
+
},
|
169
|
+
required=["location"]
|
170
|
+
)
|
171
|
+
|
172
|
+
openai_schema = schema.model_dump_openai()
|
173
|
+
# Result:
|
174
|
+
# {
|
175
|
+
# "type": "function",
|
176
|
+
# "function": {
|
177
|
+
# "name": "get_weather",
|
178
|
+
# "description": "Get weather information for a location",
|
179
|
+
# "parameters": {
|
180
|
+
# "type": "object",
|
181
|
+
# "properties": {
|
182
|
+
# "location": {"type": "string"},
|
183
|
+
# "unit": {"type": "string", "enum": ["C", "F"]}
|
184
|
+
# },
|
185
|
+
# "required": ["location"]
|
186
|
+
# }
|
187
|
+
# }
|
188
|
+
# }
|
189
|
+
```
|
190
|
+
"""
|
191
|
+
parameters: ToolParameters = {
|
192
|
+
"type": "object",
|
193
|
+
"properties": self.parameters["properties"],
|
194
|
+
"required": self.required,
|
195
|
+
}
|
196
|
+
|
197
|
+
# First create the function definition
|
198
|
+
function_def = OpenAIFunctionDefinition(
|
199
|
+
name=self.name,
|
200
|
+
description=self.description or "",
|
201
|
+
parameters=parameters,
|
202
|
+
)
|
203
|
+
|
204
|
+
return OpenAIFunctionTool(type="function", function=function_def)
|
205
|
+
|
206
|
+
def to_python_signature(self) -> inspect.Signature:
|
207
|
+
"""Convert the schema back to a Python function signature.
|
208
|
+
|
209
|
+
This method creates a Python function signature from the OpenAI schema,
|
210
|
+
mapping JSON schema types back to their Python equivalents.
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
A function signature representing the schema parameters
|
214
|
+
|
215
|
+
Example:
|
216
|
+
```python
|
217
|
+
schema = FunctionSchema(...)
|
218
|
+
sig = schema.to_python_signature()
|
219
|
+
print(str(sig)) # -> (location: str, unit: str = None, ...)
|
220
|
+
```
|
221
|
+
"""
|
222
|
+
model = self._create_pydantic_model()
|
223
|
+
parameters: list[inspect.Parameter] = []
|
224
|
+
for name, field in model.model_fields.items():
|
225
|
+
default = inspect.Parameter.empty if field.is_required() else field.default
|
226
|
+
param = inspect.Parameter(
|
227
|
+
name=name,
|
228
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
229
|
+
annotation=field.annotation,
|
230
|
+
default=default,
|
231
|
+
)
|
232
|
+
parameters.append(param)
|
233
|
+
return inspect.Signature(parameters=parameters, return_annotation=Any)
|
234
|
+
|
235
|
+
def to_pydantic_model_code(self, class_name: str | None = None) -> str:
|
236
|
+
"""Generate Pydantic model code using datamodel-codegen.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
class_name: Name for the generated class (default: {name}Response)
|
240
|
+
model_type: Output model type for datamodel-codegen
|
241
|
+
|
242
|
+
Returns:
|
243
|
+
Generated Python code string
|
244
|
+
|
245
|
+
Raises:
|
246
|
+
RuntimeError: If datamodel-codegen is not available
|
247
|
+
subprocess.CalledProcessError: If code generation fails
|
248
|
+
"""
|
249
|
+
import subprocess
|
250
|
+
import tempfile
|
251
|
+
|
252
|
+
try:
|
253
|
+
# Check if datamodel-codegen is available
|
254
|
+
subprocess.run(
|
255
|
+
["datamodel-codegen", "--version"],
|
256
|
+
check=True,
|
257
|
+
capture_output=True,
|
258
|
+
)
|
259
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
260
|
+
msg = "datamodel-codegen not available"
|
261
|
+
raise RuntimeError(msg) from e
|
262
|
+
|
263
|
+
name = class_name or f"{self.name.title()}Response"
|
264
|
+
|
265
|
+
# Create temporary file with returns schema
|
266
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
267
|
+
json.dump(self.returns, f)
|
268
|
+
schema_file = Path(f.name)
|
269
|
+
|
270
|
+
try:
|
271
|
+
# Generate model using datamodel-codegen
|
272
|
+
result = subprocess.run(
|
273
|
+
[
|
274
|
+
"datamodel-codegen",
|
275
|
+
"--input",
|
276
|
+
str(schema_file),
|
277
|
+
"--input-file-type",
|
278
|
+
"jsonschema",
|
279
|
+
"--output-model-type",
|
280
|
+
"pydantic.BaseModel",
|
281
|
+
"--class-name",
|
282
|
+
name,
|
283
|
+
"--disable-timestamp",
|
284
|
+
"--use-union-operator",
|
285
|
+
"--use-schema-description",
|
286
|
+
"--enum-field-as-literal",
|
287
|
+
"all",
|
288
|
+
"--target-python-version",
|
289
|
+
"3.12",
|
290
|
+
],
|
291
|
+
capture_output=True,
|
292
|
+
text=True,
|
293
|
+
check=True,
|
294
|
+
)
|
295
|
+
|
296
|
+
return result.stdout.strip()
|
297
|
+
|
298
|
+
finally:
|
299
|
+
# Cleanup temp file
|
300
|
+
schema_file.unlink(missing_ok=True)
|
301
|
+
|
302
|
+
def get_annotations(self, return_type: Any = str) -> dict[str, type[Any]]:
|
303
|
+
"""Get a dictionary of parameter names to their Python types.
|
304
|
+
|
305
|
+
This can be used directly for __annotations__ assignment.
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
Dictionary mapping parameter names to their Python types.
|
309
|
+
"""
|
310
|
+
model = self._create_pydantic_model()
|
311
|
+
annotations: dict[str, type[Any]] = {}
|
312
|
+
for name, field in model.model_fields.items():
|
313
|
+
annotations[name] = field.annotation # type: ignore
|
314
|
+
annotations["return"] = return_type
|
315
|
+
return annotations
|
316
|
+
|
317
|
+
@classmethod
|
318
|
+
def from_dict(cls, schema: dict[str, Any]) -> FunctionSchema:
|
319
|
+
"""Create a FunctionSchema from a raw schema dictionary.
|
320
|
+
|
321
|
+
Args:
|
322
|
+
schema: OpenAI function schema dictionary.
|
323
|
+
Can be either a direct function definition or a tool wrapper.
|
324
|
+
|
325
|
+
Returns:
|
326
|
+
New FunctionSchema instance
|
327
|
+
|
328
|
+
Raises:
|
329
|
+
ValueError: If schema format is invalid or missing required fields
|
330
|
+
"""
|
331
|
+
from schemez.typedefs import _convert_complex_property
|
332
|
+
|
333
|
+
# Handle tool wrapper format
|
334
|
+
if isinstance(schema, dict):
|
335
|
+
if "type" in schema and schema["type"] == "function":
|
336
|
+
if "function" not in schema:
|
337
|
+
msg = 'Tool with type "function" must have a "function" field'
|
338
|
+
raise ValueError(msg)
|
339
|
+
schema = schema["function"]
|
340
|
+
elif "type" in schema and schema.get("type") != "function":
|
341
|
+
msg = f"Unknown tool type: {schema.get('type')}"
|
342
|
+
raise ValueError(msg)
|
343
|
+
|
344
|
+
# Validate we have a proper function definition
|
345
|
+
if not isinstance(schema, dict):
|
346
|
+
msg = "Schema must be a dictionary"
|
347
|
+
raise ValueError(msg) # noqa: TRY004
|
348
|
+
|
349
|
+
# Get function name
|
350
|
+
name = schema.get("name", schema.get("function", {}).get("name"))
|
351
|
+
if not name:
|
352
|
+
msg = 'Schema must have a "name" field'
|
353
|
+
raise ValueError(msg)
|
354
|
+
|
355
|
+
# Extract parameters
|
356
|
+
param_dict = schema.get("parameters", {"type": "object", "properties": {}})
|
357
|
+
if not isinstance(param_dict, dict):
|
358
|
+
msg = "Schema parameters must be a dictionary"
|
359
|
+
raise ValueError(msg) # noqa: TRY004
|
360
|
+
|
361
|
+
# Clean up properties that have advanced JSON Schema features
|
362
|
+
properties = param_dict.get("properties", {})
|
363
|
+
cleaned_props: dict[str, Property] = {}
|
364
|
+
for prop_name, prop in properties.items():
|
365
|
+
cleaned_props[prop_name] = _convert_complex_property(prop)
|
366
|
+
|
367
|
+
# Get required fields
|
368
|
+
required = param_dict.get("required", [])
|
369
|
+
|
370
|
+
# Create parameters with cleaned properties
|
371
|
+
parameters: ToolParameters = {"type": "object", "properties": cleaned_props}
|
372
|
+
if required:
|
373
|
+
parameters["required"] = required
|
374
|
+
|
375
|
+
# Create new instance
|
376
|
+
return cls(
|
377
|
+
name=name,
|
378
|
+
description=schema.get("description"),
|
379
|
+
parameters=parameters,
|
380
|
+
required=required,
|
381
|
+
returns={"type": "object"},
|
382
|
+
function_type=FunctionType.SYNC,
|
383
|
+
)
|
384
|
+
|
385
|
+
|
386
|
+
def _is_optional_type(typ: type) -> TypeGuard[type]:
|
387
|
+
"""Check if a type is Optional[T] or T | None.
|
388
|
+
|
389
|
+
Args:
|
390
|
+
typ: Type to check
|
391
|
+
|
392
|
+
Returns:
|
393
|
+
True if the type is Optional, False otherwise
|
394
|
+
"""
|
395
|
+
origin = typing.get_origin(typ)
|
396
|
+
if origin not in (typing.Union, types.UnionType): # pyright: ignore
|
397
|
+
return False
|
398
|
+
args = typing.get_args(typ)
|
399
|
+
# Check if any of the union members is None or NoneType
|
400
|
+
return any(arg is type(None) for arg in args)
|
401
|
+
|
402
|
+
|
403
|
+
def _resolve_type_annotation(
|
404
|
+
typ: Any,
|
405
|
+
description: str | None = None,
|
406
|
+
default: Any = inspect.Parameter.empty,
|
407
|
+
is_parameter: bool = True,
|
408
|
+
) -> Property:
|
409
|
+
"""Resolve a type annotation into an OpenAI schema type.
|
410
|
+
|
411
|
+
Args:
|
412
|
+
typ: Type to resolve
|
413
|
+
description: Optional description
|
414
|
+
default: Default value if any
|
415
|
+
is_parameter: Whether this is for a parameter (affects dict schema)
|
416
|
+
"""
|
417
|
+
from schemez.typedefs import _create_simple_property
|
418
|
+
|
419
|
+
schema: dict[str, Any] = {}
|
420
|
+
|
421
|
+
# Handle anyOf/oneOf fields
|
422
|
+
if isinstance(typ, dict) and ("anyOf" in typ or "oneOf" in typ):
|
423
|
+
# For simplicity, we'll treat it as a string that can be null
|
424
|
+
# This is a common pattern for optional fields
|
425
|
+
schema["type"] = "string"
|
426
|
+
if default is not None:
|
427
|
+
schema["default"] = default
|
428
|
+
if description:
|
429
|
+
schema["description"] = description
|
430
|
+
return _create_simple_property(
|
431
|
+
type_str="string",
|
432
|
+
description=description,
|
433
|
+
default=default,
|
434
|
+
)
|
435
|
+
|
436
|
+
# Handle Annotated types first
|
437
|
+
if typing.get_origin(typ) is Annotated:
|
438
|
+
# Get the underlying type (first argument)
|
439
|
+
base_type = typing.get_args(typ)[0]
|
440
|
+
return _resolve_type_annotation(
|
441
|
+
base_type,
|
442
|
+
description=description,
|
443
|
+
default=default,
|
444
|
+
is_parameter=is_parameter,
|
445
|
+
)
|
446
|
+
|
447
|
+
origin = typing.get_origin(typ)
|
448
|
+
args = typing.get_args(typ)
|
449
|
+
|
450
|
+
# Handle Union types (including Optional)
|
451
|
+
if origin in (typing.Union, types.UnionType): # pyright: ignore
|
452
|
+
# For Optional (union with None), filter out None type
|
453
|
+
non_none_types = [t for t in args if t is not type(None)]
|
454
|
+
if non_none_types:
|
455
|
+
prop = _resolve_type_annotation(
|
456
|
+
non_none_types[0],
|
457
|
+
description=description,
|
458
|
+
default=default,
|
459
|
+
is_parameter=is_parameter,
|
460
|
+
)
|
461
|
+
schema.update(prop)
|
462
|
+
else:
|
463
|
+
schema["type"] = "string" # Fallback for Union[]
|
464
|
+
|
465
|
+
# Handle dataclasses
|
466
|
+
elif dataclasses.is_dataclass(typ):
|
467
|
+
schema["type"] = "object"
|
468
|
+
elif typing.is_typeddict(typ):
|
469
|
+
properties = {}
|
470
|
+
required = []
|
471
|
+
for field_name, field_type in typ.__annotations__.items():
|
472
|
+
# Check if field is wrapped in Required/NotRequired
|
473
|
+
origin = typing.get_origin(field_type)
|
474
|
+
if origin is Required:
|
475
|
+
is_required = True
|
476
|
+
field_type = typing.get_args(field_type)[0]
|
477
|
+
elif origin is NotRequired:
|
478
|
+
is_required = False
|
479
|
+
field_type = typing.get_args(field_type)[0]
|
480
|
+
else:
|
481
|
+
# Fall back to checking __required_keys__
|
482
|
+
is_required = field_name in getattr(
|
483
|
+
typ, "__required_keys__", {field_name}
|
484
|
+
)
|
485
|
+
|
486
|
+
properties[field_name] = _resolve_type_annotation(
|
487
|
+
field_type,
|
488
|
+
is_parameter=is_parameter,
|
489
|
+
)
|
490
|
+
if is_required:
|
491
|
+
required.append(field_name)
|
492
|
+
|
493
|
+
schema.update({"type": "object", "properties": properties})
|
494
|
+
if required:
|
495
|
+
schema["required"] = required
|
496
|
+
# Handle mappings - updated check
|
497
|
+
elif (
|
498
|
+
origin in (dict, typing.Dict) # noqa: UP006
|
499
|
+
or (origin is not None and isinstance(origin, type) and issubclass(origin, dict))
|
500
|
+
):
|
501
|
+
schema["type"] = "object"
|
502
|
+
if is_parameter: # Only add additionalProperties for parameters
|
503
|
+
schema["additionalProperties"] = True
|
504
|
+
|
505
|
+
# Handle sequences
|
506
|
+
elif origin in (
|
507
|
+
list,
|
508
|
+
set,
|
509
|
+
tuple,
|
510
|
+
frozenset,
|
511
|
+
typing.List, # noqa: UP006 # pyright: ignore
|
512
|
+
typing.Set, # noqa: UP006 # pyright: ignore
|
513
|
+
) or (
|
514
|
+
origin is not None
|
515
|
+
and origin.__module__ == "collections.abc"
|
516
|
+
and origin.__name__ in {"Sequence", "MutableSequence", "Collection"}
|
517
|
+
):
|
518
|
+
schema["type"] = "array"
|
519
|
+
item_type = args[0] if args else Any
|
520
|
+
schema["items"] = _resolve_type_annotation(
|
521
|
+
item_type,
|
522
|
+
is_parameter=is_parameter,
|
523
|
+
)
|
524
|
+
|
525
|
+
# Handle literals
|
526
|
+
elif origin is typing.Literal:
|
527
|
+
schema["type"] = "string"
|
528
|
+
schema["enum"] = list(args)
|
529
|
+
|
530
|
+
# Handle basic types
|
531
|
+
elif isinstance(typ, type):
|
532
|
+
if issubclass(typ, enum.Enum):
|
533
|
+
schema["type"] = "string"
|
534
|
+
schema["enum"] = [e.value for e in typ]
|
535
|
+
|
536
|
+
# Basic types
|
537
|
+
elif typ in (str, Path, UUID, re.Pattern):
|
538
|
+
schema["type"] = "string"
|
539
|
+
elif typ is int:
|
540
|
+
schema["type"] = "integer"
|
541
|
+
elif typ in (float, decimal.Decimal):
|
542
|
+
schema["type"] = "number"
|
543
|
+
elif typ is bool:
|
544
|
+
schema["type"] = "boolean"
|
545
|
+
|
546
|
+
# String formats
|
547
|
+
elif typ is datetime:
|
548
|
+
schema["type"] = "string"
|
549
|
+
schema["format"] = "date-time"
|
550
|
+
if description:
|
551
|
+
description = f"{description} (ISO 8601 format)"
|
552
|
+
elif typ is date:
|
553
|
+
schema["type"] = "string"
|
554
|
+
schema["format"] = "date"
|
555
|
+
if description:
|
556
|
+
description = f"{description} (ISO 8601 format)"
|
557
|
+
elif typ is time:
|
558
|
+
schema["type"] = "string"
|
559
|
+
schema["format"] = "time"
|
560
|
+
if description:
|
561
|
+
description = f"{description} (ISO 8601 format)"
|
562
|
+
elif typ is timedelta:
|
563
|
+
schema["type"] = "string"
|
564
|
+
if description:
|
565
|
+
description = f"{description} (ISO 8601 duration)"
|
566
|
+
elif typ is timezone:
|
567
|
+
schema["type"] = "string"
|
568
|
+
if description:
|
569
|
+
description = f"{description} (IANA timezone name)"
|
570
|
+
elif typ is UUID:
|
571
|
+
schema["type"] = "string"
|
572
|
+
elif typ in (bytes, bytearray):
|
573
|
+
schema["type"] = "string"
|
574
|
+
if description:
|
575
|
+
description = f"{description} (base64 encoded)"
|
576
|
+
elif typ is ipaddress.IPv4Address or typ is ipaddress.IPv6Address:
|
577
|
+
schema["type"] = "string"
|
578
|
+
elif typ is complex:
|
579
|
+
schema.update({
|
580
|
+
"type": "object",
|
581
|
+
"properties": {
|
582
|
+
"real": {"type": "number"},
|
583
|
+
"imag": {"type": "number"},
|
584
|
+
},
|
585
|
+
})
|
586
|
+
# Default to object for unknown types
|
587
|
+
else:
|
588
|
+
schema["type"] = "object"
|
589
|
+
else:
|
590
|
+
# Default for unmatched types
|
591
|
+
schema["type"] = "string"
|
592
|
+
|
593
|
+
# Add description if provided
|
594
|
+
if description is not None:
|
595
|
+
schema["description"] = description
|
596
|
+
|
597
|
+
# Add default if provided and not empty
|
598
|
+
if default is not inspect.Parameter.empty:
|
599
|
+
schema["default"] = default
|
600
|
+
|
601
|
+
from schemez.typedefs import (
|
602
|
+
_create_array_property,
|
603
|
+
_create_object_property,
|
604
|
+
_create_simple_property,
|
605
|
+
)
|
606
|
+
|
607
|
+
if schema["type"] == "array":
|
608
|
+
return _create_array_property(
|
609
|
+
items=schema["items"],
|
610
|
+
description=schema.get("description"),
|
611
|
+
)
|
612
|
+
if schema["type"] == "object":
|
613
|
+
prop = _create_object_property(description=schema.get("description"))
|
614
|
+
if "properties" in schema:
|
615
|
+
prop["properties"] = schema["properties"]
|
616
|
+
if "additionalProperties" in schema:
|
617
|
+
prop["additionalProperties"] = schema["additionalProperties"]
|
618
|
+
if "required" in schema:
|
619
|
+
prop["required"] = schema["required"]
|
620
|
+
return prop
|
621
|
+
|
622
|
+
return _create_simple_property(
|
623
|
+
type_str=schema["type"],
|
624
|
+
description=schema.get("description"),
|
625
|
+
enum_values=schema.get("enum"),
|
626
|
+
default=default if default is not inspect.Parameter.empty else None,
|
627
|
+
fmt=schema.get("format"),
|
628
|
+
)
|
629
|
+
|
630
|
+
|
631
|
+
def _determine_function_type(func: Callable[..., Any]) -> FunctionType:
|
632
|
+
"""Determine the type of the function.
|
633
|
+
|
634
|
+
Args:
|
635
|
+
func: Function to check
|
636
|
+
|
637
|
+
Returns:
|
638
|
+
FunctionType indicating the function's type
|
639
|
+
"""
|
640
|
+
if inspect.isasyncgenfunction(func):
|
641
|
+
return FunctionType.ASYNC_GENERATOR
|
642
|
+
if inspect.isgeneratorfunction(func):
|
643
|
+
return FunctionType.SYNC_GENERATOR
|
644
|
+
if inspect.iscoroutinefunction(func):
|
645
|
+
return FunctionType.ASYNC
|
646
|
+
return FunctionType.SYNC
|
647
|
+
|
648
|
+
|
649
|
+
def create_schema(
|
650
|
+
func: Callable[..., Any],
|
651
|
+
name_override: str | None = None,
|
652
|
+
) -> FunctionSchema:
|
653
|
+
"""Create an OpenAI function schema from a Python function.
|
654
|
+
|
655
|
+
Args:
|
656
|
+
func: Function to create schema for
|
657
|
+
name_override: Optional name override (otherwise the function name)
|
658
|
+
|
659
|
+
Returns:
|
660
|
+
Schema representing the function
|
661
|
+
|
662
|
+
Raises:
|
663
|
+
TypeError: If input is not callable
|
664
|
+
|
665
|
+
Note:
|
666
|
+
Variable arguments (*args) and keyword arguments (**kwargs) are not
|
667
|
+
supported in OpenAI function schemas and will be ignored with a warning.
|
668
|
+
"""
|
669
|
+
if not callable(func):
|
670
|
+
msg = f"Expected callable, got {type(func)}"
|
671
|
+
raise TypeError(msg)
|
672
|
+
|
673
|
+
# Parse function signature and docstring
|
674
|
+
sig = inspect.signature(func)
|
675
|
+
docstring = docstring_parser.parse(func.__doc__ or "")
|
676
|
+
|
677
|
+
# Get clean type hints without extras
|
678
|
+
try:
|
679
|
+
hints = typing.get_type_hints(func, localns=locals())
|
680
|
+
except NameError:
|
681
|
+
msg = "Unable to resolve type hints for function %s, skipping"
|
682
|
+
logger.warning(msg, getattr(func, "__name__", "unknown"))
|
683
|
+
hints = {}
|
684
|
+
|
685
|
+
parameters: ToolParameters = {"type": "object", "properties": {}}
|
686
|
+
required: list[str] = []
|
687
|
+
params = list(sig.parameters.items())
|
688
|
+
skip_first = (
|
689
|
+
inspect.isfunction(func)
|
690
|
+
and not inspect.ismethod(func)
|
691
|
+
and params
|
692
|
+
and params[0][0] == "self"
|
693
|
+
)
|
694
|
+
|
695
|
+
for i, (name, param) in enumerate(sig.parameters.items()):
|
696
|
+
# Skip the first parameter for bound methods
|
697
|
+
if skip_first and i == 0:
|
698
|
+
continue
|
699
|
+
if param.kind in (
|
700
|
+
inspect.Parameter.VAR_POSITIONAL,
|
701
|
+
inspect.Parameter.VAR_KEYWORD,
|
702
|
+
):
|
703
|
+
continue
|
704
|
+
|
705
|
+
param_doc = next(
|
706
|
+
(p.description for p in docstring.params if p.arg_name == name),
|
707
|
+
None,
|
708
|
+
)
|
709
|
+
|
710
|
+
param_type = hints.get(name, Any)
|
711
|
+
parameters["properties"][name] = _resolve_type_annotation(
|
712
|
+
param_type,
|
713
|
+
description=param_doc,
|
714
|
+
default=param.default,
|
715
|
+
is_parameter=True,
|
716
|
+
)
|
717
|
+
|
718
|
+
if param.default is inspect.Parameter.empty:
|
719
|
+
required.append(name)
|
720
|
+
|
721
|
+
# Add required fields to parameters if any exist
|
722
|
+
if required:
|
723
|
+
parameters["required"] = required
|
724
|
+
|
725
|
+
# Handle return type with is_parameter=False
|
726
|
+
function_type = _determine_function_type(func)
|
727
|
+
return_hint = hints.get("return", Any)
|
728
|
+
|
729
|
+
if function_type in (FunctionType.SYNC_GENERATOR, FunctionType.ASYNC_GENERATOR):
|
730
|
+
element_type = next(
|
731
|
+
(t for t in typing.get_args(return_hint) if t is not type(None)),
|
732
|
+
Any,
|
733
|
+
)
|
734
|
+
prop = _resolve_type_annotation(element_type, is_parameter=False)
|
735
|
+
returns_dct = {"type": "array", "items": prop}
|
736
|
+
else:
|
737
|
+
returns = _resolve_type_annotation(return_hint, is_parameter=False)
|
738
|
+
returns_dct = dict(returns) # type: ignore
|
739
|
+
|
740
|
+
return FunctionSchema(
|
741
|
+
name=name_override or getattr(func, "__name__", "unknown") or "unknown",
|
742
|
+
description=docstring.short_description,
|
743
|
+
parameters=parameters, # Now includes required fields
|
744
|
+
required=required,
|
745
|
+
returns=returns_dct,
|
746
|
+
function_type=function_type,
|
747
|
+
)
|
748
|
+
|
749
|
+
|
750
|
+
if __name__ == "__main__":
|
751
|
+
import json
|
752
|
+
|
753
|
+
def get_weather(
|
754
|
+
location: str,
|
755
|
+
unit: typing.Literal["C", "F"] = "C",
|
756
|
+
detailed: bool = False,
|
757
|
+
) -> dict[str, str | float]:
|
758
|
+
"""Get the weather for a location.
|
759
|
+
|
760
|
+
Args:
|
761
|
+
location: City or address to get weather for
|
762
|
+
unit: Temperature unit (Celsius or Fahrenheit)
|
763
|
+
detailed: Include extended forecast
|
764
|
+
"""
|
765
|
+
return {"temp": 22.5, "conditions": "sunny"}
|
766
|
+
|
767
|
+
# Create schema and executable function
|
768
|
+
schema = create_schema(get_weather)
|
769
|
+
|
770
|
+
# Print the schema
|
771
|
+
print("OpenAI Function Schema:")
|
772
|
+
print(json.dumps(schema.model_dump_openai(), indent=2))
|