openai-sdk-helpers 0.0.5__py3-none-any.whl → 0.0.7__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.
Files changed (49) hide show
  1. openai_sdk_helpers/__init__.py +62 -0
  2. openai_sdk_helpers/agent/__init__.py +31 -0
  3. openai_sdk_helpers/agent/base.py +330 -0
  4. openai_sdk_helpers/agent/config.py +66 -0
  5. openai_sdk_helpers/agent/project_manager.py +511 -0
  6. openai_sdk_helpers/agent/prompt_utils.py +9 -0
  7. openai_sdk_helpers/agent/runner.py +215 -0
  8. openai_sdk_helpers/agent/summarizer.py +85 -0
  9. openai_sdk_helpers/agent/translator.py +139 -0
  10. openai_sdk_helpers/agent/utils.py +47 -0
  11. openai_sdk_helpers/agent/validation.py +97 -0
  12. openai_sdk_helpers/agent/vector_search.py +462 -0
  13. openai_sdk_helpers/agent/web_search.py +404 -0
  14. openai_sdk_helpers/config.py +199 -0
  15. openai_sdk_helpers/enums/__init__.py +7 -0
  16. openai_sdk_helpers/enums/base.py +29 -0
  17. openai_sdk_helpers/environment.py +27 -0
  18. openai_sdk_helpers/prompt/__init__.py +77 -0
  19. openai_sdk_helpers/py.typed +0 -0
  20. openai_sdk_helpers/response/__init__.py +20 -0
  21. openai_sdk_helpers/response/base.py +505 -0
  22. openai_sdk_helpers/response/messages.py +211 -0
  23. openai_sdk_helpers/response/runner.py +104 -0
  24. openai_sdk_helpers/response/tool_call.py +70 -0
  25. openai_sdk_helpers/response/vector_store.py +84 -0
  26. openai_sdk_helpers/structure/__init__.py +43 -0
  27. openai_sdk_helpers/structure/agent_blueprint.py +224 -0
  28. openai_sdk_helpers/structure/base.py +713 -0
  29. openai_sdk_helpers/structure/plan/__init__.py +13 -0
  30. openai_sdk_helpers/structure/plan/enum.py +64 -0
  31. openai_sdk_helpers/structure/plan/plan.py +253 -0
  32. openai_sdk_helpers/structure/plan/task.py +122 -0
  33. openai_sdk_helpers/structure/prompt.py +24 -0
  34. openai_sdk_helpers/structure/responses.py +132 -0
  35. openai_sdk_helpers/structure/summary.py +65 -0
  36. openai_sdk_helpers/structure/validation.py +47 -0
  37. openai_sdk_helpers/structure/vector_search.py +86 -0
  38. openai_sdk_helpers/structure/web_search.py +46 -0
  39. openai_sdk_helpers/utils/__init__.py +25 -0
  40. openai_sdk_helpers/utils/core.py +300 -0
  41. openai_sdk_helpers/vector_storage/__init__.py +15 -0
  42. openai_sdk_helpers/vector_storage/cleanup.py +91 -0
  43. openai_sdk_helpers/vector_storage/storage.py +564 -0
  44. openai_sdk_helpers/vector_storage/types.py +58 -0
  45. {openai_sdk_helpers-0.0.5.dist-info → openai_sdk_helpers-0.0.7.dist-info}/METADATA +6 -3
  46. openai_sdk_helpers-0.0.7.dist-info/RECORD +51 -0
  47. openai_sdk_helpers-0.0.5.dist-info/RECORD +0 -7
  48. {openai_sdk_helpers-0.0.5.dist-info → openai_sdk_helpers-0.0.7.dist-info}/WHEEL +0 -0
  49. {openai_sdk_helpers-0.0.5.dist-info → openai_sdk_helpers-0.0.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,713 @@
1
+ """Base structure definitions for shared agent structures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Standard library imports
6
+ import inspect
7
+ import json
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from collections.abc import Mapping, Sequence
13
+ from dataclasses import dataclass
14
+ from typing import (
15
+ Any,
16
+ ClassVar,
17
+ Dict,
18
+ List,
19
+ Optional,
20
+ Type,
21
+ TypeVar,
22
+ Union,
23
+ get_args,
24
+ get_origin,
25
+ cast,
26
+ )
27
+
28
+ # Third-party imports
29
+ from pydantic import BaseModel, ConfigDict, Field
30
+ from openai.types.responses.response_text_config_param import ResponseTextConfigParam
31
+
32
+
33
+ # Internal imports
34
+
35
+ from ..utils import check_filepath, customJSONEncoder, log
36
+
37
+ T = TypeVar("T", bound="BaseStructure")
38
+ DEFAULT_DATA_PATH: Path | None = None
39
+
40
+
41
+ class BaseStructure(BaseModel):
42
+ """Base class for defining structured output formats for OpenAI Assistants.
43
+
44
+ This class provides Pydantic-based schema definition and serialization
45
+ helpers that support structured output formatting.
46
+
47
+ Methods
48
+ -------
49
+ assistant_format()
50
+ Build a response format payload for Assistant APIs.
51
+ assistant_tool_definition(name, description)
52
+ Build a function tool definition payload for Assistant APIs.
53
+ get_prompt(add_enum_values)
54
+ Format structured prompt lines into a single output string.
55
+ get_input_prompt_list(add_enum_values)
56
+ Build a structured prompt including inherited fields.
57
+ get_schema(force_required)
58
+ Generate a JSON schema for the structure.
59
+ response_format()
60
+ Build a response format payload for chat completions.
61
+ response_tool_definition(tool_name, tool_description)
62
+ Build a function tool definition payload for chat completions.
63
+ save_schema_to_file(force_required)
64
+ Persist the schema to disk within the application data path.
65
+ to_json()
66
+ Serialize the structure to a JSON-compatible dictionary.
67
+ to_json_file(filepath)
68
+ Write the serialized payload to ``filepath``.
69
+ from_raw_input(data)
70
+ Construct an instance from raw assistant tool-call arguments.
71
+ format_output(label, value)
72
+ Format a label/value pair for console output.
73
+ schema_overrides()
74
+ Produce ``Field`` overrides for dynamic schema customisation.
75
+ print()
76
+ Return a string representation of the structure.
77
+ console_print()
78
+ Print the string representation to stdout.
79
+ """
80
+
81
+ model_config = ConfigDict(
82
+ title="OutputStructure", use_enum_values=False, strict=True, extra="forbid"
83
+ )
84
+ DATA_PATH: ClassVar[Path | None] = DEFAULT_DATA_PATH
85
+ """Optional location for saving schema files."""
86
+
87
+ @classmethod
88
+ def get_prompt(cls, add_enum_values: bool = True) -> str:
89
+ """Format structured prompt lines into a single output string.
90
+
91
+ Parameters
92
+ ----------
93
+ add_enum_values : bool, default=True
94
+ Whether enum choices should be included in the prompt lines.
95
+
96
+ Returns
97
+ -------
98
+ str
99
+ Formatted prompt ready for display.
100
+ """
101
+ prompt_lines = cls.get_input_prompt_list(add_enum_values)
102
+ if not prompt_lines:
103
+ return "No structured prompt available."
104
+ return "# Output Format\n" + "\n".join(prompt_lines)
105
+
106
+ @classmethod
107
+ def _get_all_fields(cls) -> dict[Any, Any]:
108
+ """Collect all fields, including inherited ones, from the class hierarchy.
109
+
110
+ Returns
111
+ -------
112
+ dict[Any, Any]
113
+ Mapping of field names to model fields.
114
+ """
115
+ fields = {}
116
+ for base in reversed(cls.__mro__): # Traverse inheritance tree
117
+ if issubclass(base, BaseModel) and hasattr(base, "model_fields"):
118
+ fields.update(base.model_fields) # Merge fields from parent
119
+ return fields
120
+
121
+ @classmethod
122
+ def _get_field_prompt(
123
+ cls, field_name: str, field, add_enum_values: bool = True
124
+ ) -> str:
125
+ """Return a formatted prompt line for a single field.
126
+
127
+ Parameters
128
+ ----------
129
+ field_name : str
130
+ Name of the field being processed.
131
+ field
132
+ Pydantic ``ModelField`` instance.
133
+ add_enum_values : bool, default=True
134
+ Whether enum choices should be included.
135
+
136
+ Returns
137
+ -------
138
+ str
139
+ Single line describing the field for inclusion in the prompt.
140
+ """
141
+ title = field.title or field_name.capitalize()
142
+ description = field.description or f"Provide relevant {field_name}."
143
+ type_hint = field.annotation
144
+
145
+ # Check for enums or list of enums
146
+ enum_cls = cls._extract_enum_class(type_hint)
147
+ if enum_cls:
148
+ enum_choices_str = "\n\t\t• ".join(f"{e.name}: {e.value}" for e in enum_cls)
149
+ if add_enum_values:
150
+ enum_prompt = f" \n\t Choose from: \n\t\t• {enum_choices_str}"
151
+ else:
152
+ enum_prompt = ""
153
+
154
+ return f"- **{title}**: {description}{enum_prompt}"
155
+
156
+ # Otherwise check normal types
157
+ type_mapping = {
158
+ str: f"- **{title}**: {description}",
159
+ bool: f"- **{title}**: {description} Specify if the {title} is true or false.",
160
+ int: f"- **{title}**: {description} Provide the relevant integer value for {title}.",
161
+ float: f"- **{title}**: {description} Provide the relevant float value for {title}.",
162
+ }
163
+
164
+ return type_mapping.get(
165
+ type_hint, f"- **{title}**: Provide the relevant {title}."
166
+ )
167
+
168
+ @classmethod
169
+ def get_input_prompt_list(cls, add_enum_values: bool = True) -> list[str]:
170
+ """Dynamically build a structured prompt including inherited fields.
171
+
172
+ Parameters
173
+ ----------
174
+ add_enum_values : bool, default=True
175
+ Whether enumeration values should be included.
176
+
177
+ Returns
178
+ -------
179
+ list[str]
180
+ Prompt lines describing each field.
181
+ """
182
+ prompt_lines = []
183
+ all_fields = cls._get_all_fields()
184
+ for field_name, field in all_fields.items():
185
+ prompt_lines.append(
186
+ cls._get_field_prompt(field_name, field, add_enum_values)
187
+ )
188
+ return prompt_lines
189
+
190
+ @classmethod
191
+ def assistant_tool_definition(cls, name: str, description: str) -> dict:
192
+ """Build an assistant function tool definition for this structure.
193
+
194
+ Parameters
195
+ ----------
196
+ name : str
197
+ Name of the function tool.
198
+ description : str
199
+ Description of what the function tool does.
200
+
201
+ Returns
202
+ -------
203
+ dict
204
+ Assistant tool definition payload.
205
+ """
206
+ from .responses import assistant_tool_definition
207
+
208
+ return assistant_tool_definition(cls, name, description)
209
+
210
+ @classmethod
211
+ def assistant_format(cls) -> dict:
212
+ """Build an assistant response format definition for this structure.
213
+
214
+ Returns
215
+ -------
216
+ dict
217
+ Assistant response format definition.
218
+ """
219
+ from .responses import assistant_format
220
+
221
+ return assistant_format(cls)
222
+
223
+ @classmethod
224
+ def response_tool_definition(cls, tool_name: str, tool_description: str) -> dict:
225
+ """Build a chat completion tool definition for this structure.
226
+
227
+ Parameters
228
+ ----------
229
+ tool_name : str
230
+ Name of the function tool.
231
+ tool_description : str
232
+ Description of what the function tool does.
233
+
234
+ Returns
235
+ -------
236
+ dict
237
+ Tool definition payload for chat completions.
238
+ """
239
+ from .responses import response_tool_definition
240
+
241
+ return response_tool_definition(cls, tool_name, tool_description)
242
+
243
+ @classmethod
244
+ def response_format(cls) -> ResponseTextConfigParam:
245
+ """Build a chat completion response format for this structure.
246
+
247
+ Returns
248
+ -------
249
+ ResponseTextConfigParam
250
+ Response format definition.
251
+ """
252
+ from .responses import response_format
253
+
254
+ return response_format(cls)
255
+
256
+ @classmethod
257
+ def get_schema(cls) -> dict[str, Any]:
258
+ """Generate a JSON schema for the class.
259
+
260
+ All object properties are marked as required to produce fully specified
261
+ schemas. Fields with a default value of ``None`` are treated as nullable
262
+ and gain an explicit ``null`` entry in the resulting schema.
263
+
264
+ Parameters
265
+ ----------
266
+ force_required : bool, default=False
267
+ Retained for compatibility; all schemas declare required properties.
268
+
269
+ Returns
270
+ -------
271
+ dict[str, Any]
272
+ JSON schema describing the structure.
273
+ """
274
+ schema = cls.model_json_schema()
275
+
276
+ def clean_refs(obj):
277
+ if isinstance(obj, dict):
278
+ if "$ref" in obj:
279
+ for key in list(obj.keys()):
280
+ if key != "$ref":
281
+ obj.pop(key, None)
282
+ for v in obj.values():
283
+ clean_refs(v)
284
+ elif isinstance(obj, list):
285
+ for item in obj:
286
+ clean_refs(item)
287
+ return obj
288
+
289
+ cleaned_schema = cast(Dict[str, Any], clean_refs(schema))
290
+
291
+ def add_required_fields(target: dict[str, Any]) -> None:
292
+ """Ensure every object declares its required properties."""
293
+ properties = target.get("properties")
294
+ if isinstance(properties, dict) and properties:
295
+ target["required"] = sorted(properties.keys())
296
+ for value in target.values():
297
+ if isinstance(value, dict):
298
+ add_required_fields(value)
299
+ elif isinstance(value, list):
300
+ for item in value:
301
+ if isinstance(item, dict):
302
+ add_required_fields(item)
303
+
304
+ nullable_fields = {
305
+ name
306
+ for name, model_field in getattr(cls, "model_fields", {}).items()
307
+ if getattr(model_field, "default", inspect.Signature.empty) is None
308
+ }
309
+
310
+ properties = cleaned_schema.get("properties", {})
311
+ if isinstance(properties, dict) and nullable_fields:
312
+ for field_name in nullable_fields:
313
+ field_props = properties.get(field_name)
314
+ if not isinstance(field_props, dict):
315
+ continue
316
+
317
+ field_type = field_props.get("type")
318
+ if isinstance(field_type, str):
319
+ field_props["type"] = [field_type, "null"]
320
+ elif isinstance(field_type, list):
321
+ if "null" not in field_type:
322
+ field_type.append("null")
323
+ else:
324
+ any_of = field_props.get("anyOf")
325
+ if isinstance(any_of, list):
326
+ has_null = any(
327
+ isinstance(item, dict) and item.get("type") == "null"
328
+ for item in any_of
329
+ )
330
+ if not has_null:
331
+ any_of.append({"type": "null"})
332
+
333
+ add_required_fields(cleaned_schema)
334
+ return cleaned_schema
335
+
336
+ @classmethod
337
+ def save_schema_to_file(cls) -> Path:
338
+ """
339
+ Save the generated JSON schema to a file.
340
+
341
+ The schema is generated using :meth:`get_schema` and saved in the
342
+ application's data path.
343
+
344
+ Parameters
345
+ ----------
346
+ force_required : bool, default=False
347
+ When ``True``, mark all object properties as required.
348
+
349
+ Returns
350
+ -------
351
+ Path
352
+ Path to the saved schema file.
353
+ """
354
+ schema = cls.get_schema()
355
+ if cls.DATA_PATH is None:
356
+ raise RuntimeError(
357
+ "DATA_PATH is not set. Set BaseStructure.DATA_PATH before saving."
358
+ )
359
+ file_path = cls.DATA_PATH / f"{cls.__name__}_schema.json"
360
+ check_filepath(file_path)
361
+ with file_path.open("w", encoding="utf-8") as file_handle:
362
+ json.dump(schema, file_handle, indent=2, ensure_ascii=False)
363
+ return file_path
364
+
365
+ def to_json(self) -> Dict[str, Any]:
366
+ """
367
+ Serialize the Pydantic model instance to a JSON-compatible dictionary.
368
+
369
+ Enum members are converted to their values. Lists and nested dictionaries
370
+ are recursively processed.
371
+
372
+ Returns
373
+ -------
374
+ dict[str, Any]
375
+ Model instance serialized as a dictionary.
376
+ """
377
+
378
+ def convert(obj: Any) -> Any:
379
+ if isinstance(obj, Enum):
380
+ return obj.value
381
+ if isinstance(obj, BaseStructure):
382
+ return obj.to_json()
383
+ if isinstance(obj, Mapping):
384
+ return {str(k): convert(v) for k, v in obj.items()}
385
+ if isinstance(obj, Sequence) and not isinstance(
386
+ obj, (str, bytes, bytearray)
387
+ ):
388
+ return [convert(item) for item in obj]
389
+ return obj
390
+
391
+ payload = convert(self.model_dump())
392
+
393
+ def is_list_field(field) -> bool:
394
+ annotation = getattr(field, "annotation", None)
395
+ if annotation is None:
396
+ return False
397
+
398
+ origins_to_match = {list, List, Sequence, tuple, set}
399
+
400
+ origin = get_origin(annotation)
401
+ if origin in origins_to_match or annotation in origins_to_match:
402
+ return True
403
+
404
+ if origin is Union:
405
+ return any(
406
+ get_origin(arg) in origins_to_match or arg in origins_to_match
407
+ for arg in get_args(annotation)
408
+ )
409
+ return False
410
+
411
+ for name, field in self.__class__.model_fields.items():
412
+ if name not in payload:
413
+ continue
414
+ if not is_list_field(field):
415
+ continue
416
+ value = payload[name]
417
+ if value is None:
418
+ continue
419
+ if isinstance(value, (str, bytes, bytearray)):
420
+ payload[name] = [value]
421
+ elif not isinstance(value, list):
422
+ payload[name] = [value]
423
+
424
+ return payload
425
+
426
+ def to_json_file(self, filepath: str) -> str:
427
+ """Write :meth:`to_json` output to ``filepath``.
428
+
429
+ Parameters
430
+ ----------
431
+ filepath : str
432
+ Destination path for the JSON file.
433
+
434
+ Returns
435
+ -------
436
+ str
437
+ Path to the written file.
438
+ """
439
+ check_filepath(fullfilepath=filepath)
440
+ with open(file=filepath, mode="w", encoding="utf-8") as f:
441
+ json.dump(
442
+ self.to_json(), f, ensure_ascii=False, indent=4, cls=customJSONEncoder
443
+ )
444
+ return filepath
445
+
446
+ @classmethod
447
+ def _extract_enum_class(cls, field_type: Any) -> Optional[Type[Enum]]:
448
+ """
449
+ Extract an Enum class from a field's type annotation.
450
+
451
+ Handles direct Enum types, List[Enum], and Optional[Enum] (via Union).
452
+
453
+ Parameters
454
+ ----------
455
+ field_type
456
+ Type annotation of a field.
457
+
458
+ Returns
459
+ -------
460
+ type[Enum] or None
461
+ Enum class if found, otherwise ``None``.
462
+ """
463
+ origin = get_origin(field_type)
464
+ args = get_args(field_type)
465
+
466
+ if inspect.isclass(field_type) and issubclass(field_type, Enum):
467
+ return field_type
468
+ elif (
469
+ origin in {list, List}
470
+ and args
471
+ and inspect.isclass(args[0])
472
+ and issubclass(args[0], Enum)
473
+ ):
474
+ return args[0]
475
+ elif origin is Union:
476
+ for arg in args:
477
+ enum_cls = cls._extract_enum_class(arg)
478
+ if enum_cls:
479
+ return enum_cls
480
+ return None
481
+
482
+ @classmethod
483
+ def _build_enum_field_mapping(cls) -> dict[str, Type[Enum]]:
484
+ """
485
+ Build a mapping from field names to their Enum classes.
486
+
487
+ This is used by `from_raw_input` to correctly process enum values.
488
+
489
+ Returns
490
+ -------
491
+ dict[str, type[Enum]]
492
+ Mapping of field names to Enum types.
493
+ """
494
+ mapping: dict[str, Type[Enum]] = {}
495
+
496
+ for name, model_field in cls.model_fields.items():
497
+ field_type = model_field.annotation
498
+ enum_cls = cls._extract_enum_class(field_type)
499
+
500
+ if enum_cls is not None:
501
+ mapping[name] = enum_cls
502
+
503
+ return mapping
504
+
505
+ @classmethod
506
+ def from_raw_input(cls: Type[T], data: dict) -> T:
507
+ """
508
+ Construct an instance of the class from a dictionary of raw input data.
509
+
510
+ This method is particularly useful for converting data received from an
511
+ OpenAI Assistant (e.g., tool call arguments) into a Pydantic model.
512
+ It handles the conversion of string values to Enum members for fields
513
+ typed as Enum or List[Enum]. Warnings are logged for invalid enum values.
514
+
515
+ Parameters
516
+ ----------
517
+ data : dict
518
+ Raw input data payload.
519
+
520
+ Returns
521
+ -------
522
+ T
523
+ Instance populated with the processed data.
524
+ """
525
+ mapping = cls._build_enum_field_mapping()
526
+ clean_data = data.copy()
527
+
528
+ for field, enum_cls in mapping.items():
529
+ raw_value = clean_data.get(field)
530
+
531
+ if raw_value is None:
532
+ continue
533
+
534
+ # List of enum values
535
+ if isinstance(raw_value, list):
536
+ converted = []
537
+ for v in raw_value:
538
+ if isinstance(v, enum_cls):
539
+ converted.append(v)
540
+ elif isinstance(v, str):
541
+ # Check if it's a valid value
542
+ if v in enum_cls._value2member_map_:
543
+ converted.append(enum_cls(v))
544
+ # Check if it's a valid name
545
+ elif v in enum_cls.__members__:
546
+ converted.append(enum_cls.__members__[v])
547
+ else:
548
+ log(
549
+ f"[{cls.__name__}] Skipping invalid value for '{field}': '{v}' not in {enum_cls.__name__}",
550
+ level=logging.WARNING,
551
+ )
552
+ clean_data[field] = converted
553
+
554
+ # Single enum value
555
+ elif (
556
+ isinstance(raw_value, str) and raw_value in enum_cls._value2member_map_
557
+ ):
558
+ clean_data[field] = enum_cls(raw_value)
559
+
560
+ elif isinstance(raw_value, enum_cls):
561
+ # already the correct type
562
+ continue
563
+
564
+ else:
565
+ log(
566
+ message=f"[{cls.__name__}] Invalid value for '{field}': '{raw_value}' not in {enum_cls.__name__}",
567
+ level=logging.WARNING,
568
+ )
569
+ clean_data[field] = None
570
+
571
+ return cls(**clean_data)
572
+
573
+ @staticmethod
574
+ def format_output(label: str, value: Any) -> str:
575
+ """
576
+ Format a label and value for string output.
577
+
578
+ Handles None values and lists appropriately.
579
+
580
+ Parameters
581
+ ----------
582
+ label : str
583
+ Label describing the value.
584
+ value : Any
585
+ Value to format for display.
586
+
587
+ Returns
588
+ -------
589
+ str
590
+ Formatted string (for example ``"- Label: Value"``).
591
+ """
592
+ if not value:
593
+ return f"- {label}: None"
594
+ if isinstance(value, list):
595
+ return f"- {label}: {', '.join(str(v) for v in value)}"
596
+ return f"- {label}: {str(value)}"
597
+
598
+ @classmethod
599
+ def schema_overrides(cls) -> Dict[str, Any]:
600
+ """
601
+ Generate Pydantic ``Field`` overrides.
602
+
603
+ Returns
604
+ -------
605
+ dict[str, Any]
606
+ Mapping of field names to ``Field`` overrides.
607
+ """
608
+ return {}
609
+
610
+ def print(self) -> str:
611
+ """
612
+ Generate a string representation of the structure.
613
+
614
+ Returns
615
+ -------
616
+ str
617
+ Formatted string for the ``logic`` field.
618
+ """
619
+ return "\n".join(
620
+ [
621
+ BaseStructure.format_output(field, value)
622
+ for field, value in self.model_dump().items()
623
+ ]
624
+ )
625
+
626
+ def console_print(self) -> None:
627
+ """Output the result of :meth:`print` to stdout.
628
+
629
+ Returns
630
+ -------
631
+ None
632
+ """
633
+ print(self.print())
634
+
635
+
636
+ @dataclass(frozen=True)
637
+ class SchemaOptions:
638
+ """Options for schema generation helpers.
639
+
640
+ Methods
641
+ -------
642
+ to_kwargs()
643
+ Return keyword arguments for schema helper calls.
644
+
645
+ Parameters
646
+ ----------
647
+ force_required : bool, default=False
648
+ When ``True``, mark all object properties as required.
649
+ """
650
+
651
+ force_required: bool = False
652
+
653
+ def to_kwargs(self) -> dict[str, Any]:
654
+ """Return keyword arguments for schema helper calls.
655
+
656
+ Returns
657
+ -------
658
+ dict[str, Any]
659
+ Keyword arguments for schema helper methods.
660
+ """
661
+ return {"force_required": self.force_required}
662
+
663
+
664
+ def spec_field(
665
+ name: str,
666
+ *,
667
+ allow_null: bool = True,
668
+ description: str | None = None,
669
+ **overrides: Any,
670
+ ) -> Any:
671
+ """Return a Pydantic ``Field`` with sensible defaults for nullable specs.
672
+
673
+ Parameters
674
+ ----------
675
+ name : str
676
+ Name of the field to use as the default title.
677
+ allow_null : bool, default=True
678
+ When ``True``, set ``None`` as the default value to allow explicit
679
+ ``null`` in generated schemas.
680
+ description : str or None, default=None
681
+ Optional description to include. When ``allow_null`` is ``True``, the
682
+ nullable hint "Return null if none apply." is appended.
683
+ **overrides
684
+ Additional keyword arguments forwarded to ``pydantic.Field``.
685
+
686
+ Returns
687
+ -------
688
+ Any
689
+ Pydantic ``Field`` configured with a default title and null behavior.
690
+ """
691
+ field_kwargs: Dict[str, Any] = {"title": name.replace("_", " ").title()}
692
+ field_kwargs.update(overrides)
693
+
694
+ base_description = field_kwargs.pop("description", description)
695
+
696
+ has_default = "default" in field_kwargs
697
+ has_default_factory = "default_factory" in field_kwargs
698
+
699
+ if allow_null:
700
+ if not has_default and not has_default_factory:
701
+ field_kwargs["default"] = None
702
+ nullable_hint = "Return null if none apply."
703
+ if base_description:
704
+ field_kwargs["description"] = f"{base_description} {nullable_hint}"
705
+ else:
706
+ field_kwargs["description"] = nullable_hint
707
+ else:
708
+ if not has_default and not has_default_factory:
709
+ field_kwargs["default"] = ...
710
+ if base_description is not None:
711
+ field_kwargs["description"] = base_description
712
+
713
+ return Field(**field_kwargs)