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.
@@ -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))