openai-sdk-helpers 0.4.2__py3-none-any.whl → 0.5.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.
- openai_sdk_helpers/__init__.py +45 -41
- openai_sdk_helpers/agent/__init__.py +4 -6
- openai_sdk_helpers/agent/base.py +110 -191
- openai_sdk_helpers/agent/{config.py → configuration.py} +24 -32
- openai_sdk_helpers/agent/{coordination.py → coordinator.py} +22 -23
- openai_sdk_helpers/agent/runner.py +3 -45
- openai_sdk_helpers/agent/search/base.py +54 -76
- openai_sdk_helpers/agent/search/vector.py +92 -108
- openai_sdk_helpers/agent/search/web.py +104 -82
- openai_sdk_helpers/agent/summarizer.py +22 -28
- openai_sdk_helpers/agent/translator.py +22 -24
- openai_sdk_helpers/agent/{validation.py → validator.py} +19 -23
- openai_sdk_helpers/cli.py +8 -22
- openai_sdk_helpers/environment.py +8 -13
- openai_sdk_helpers/errors.py +9 -0
- openai_sdk_helpers/extract/__init__.py +23 -0
- openai_sdk_helpers/extract/extractor.py +157 -0
- openai_sdk_helpers/extract/generator.py +476 -0
- openai_sdk_helpers/prompt/extractor_config_agent_instructions.jinja +6 -0
- openai_sdk_helpers/prompt/extractor_config_generator.jinja +37 -0
- openai_sdk_helpers/prompt/extractor_config_generator_instructions.jinja +9 -0
- openai_sdk_helpers/prompt/extractor_prompt_optimizer_agent_instructions.jinja +4 -0
- openai_sdk_helpers/prompt/extractor_prompt_optimizer_request.jinja +11 -0
- openai_sdk_helpers/prompt/vector_planner.jinja +7 -0
- openai_sdk_helpers/prompt/vector_search.jinja +6 -0
- openai_sdk_helpers/prompt/vector_writer.jinja +7 -0
- openai_sdk_helpers/response/__init__.py +3 -7
- openai_sdk_helpers/response/base.py +89 -98
- openai_sdk_helpers/response/{config.py → configuration.py} +45 -20
- openai_sdk_helpers/response/files.py +2 -0
- openai_sdk_helpers/response/planner.py +1 -1
- openai_sdk_helpers/response/prompter.py +1 -1
- openai_sdk_helpers/response/runner.py +1 -48
- openai_sdk_helpers/response/tool_call.py +0 -141
- openai_sdk_helpers/response/vector_store.py +8 -5
- openai_sdk_helpers/streamlit_app/__init__.py +1 -1
- openai_sdk_helpers/streamlit_app/app.py +17 -18
- openai_sdk_helpers/streamlit_app/{config.py → configuration.py} +13 -13
- openai_sdk_helpers/structure/__init__.py +16 -0
- openai_sdk_helpers/structure/base.py +239 -278
- openai_sdk_helpers/structure/extraction.py +1228 -0
- openai_sdk_helpers/structure/plan/plan.py +0 -20
- openai_sdk_helpers/structure/plan/task.py +0 -33
- openai_sdk_helpers/structure/prompt.py +16 -0
- openai_sdk_helpers/structure/responses.py +2 -2
- openai_sdk_helpers/structure/web_search.py +0 -10
- openai_sdk_helpers/tools.py +346 -99
- openai_sdk_helpers/types.py +3 -3
- openai_sdk_helpers/utils/__init__.py +9 -6
- openai_sdk_helpers/utils/json/base_model.py +316 -33
- openai_sdk_helpers/utils/json/data_class.py +1 -1
- openai_sdk_helpers/utils/langextract.py +194 -0
- openai_sdk_helpers/utils/registry.py +19 -15
- openai_sdk_helpers/vector_storage/storage.py +1 -1
- {openai_sdk_helpers-0.4.2.dist-info → openai_sdk_helpers-0.5.0.dist-info}/METADATA +25 -11
- openai_sdk_helpers-0.5.0.dist-info/RECORD +95 -0
- openai_sdk_helpers/agent/prompt_utils.py +0 -15
- openai_sdk_helpers/context_manager.py +0 -241
- openai_sdk_helpers/deprecation.py +0 -167
- openai_sdk_helpers/retry.py +0 -175
- openai_sdk_helpers/streamlit_app/streamlit_web_search.py +0 -75
- openai_sdk_helpers/utils/deprecation.py +0 -167
- openai_sdk_helpers-0.4.2.dist-info/RECORD +0 -88
- /openai_sdk_helpers/{logging_config.py → logging.py} +0 -0
- /openai_sdk_helpers/{config.py → settings.py} +0 -0
- {openai_sdk_helpers-0.4.2.dist-info → openai_sdk_helpers-0.5.0.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.4.2.dist-info → openai_sdk_helpers-0.5.0.dist-info}/entry_points.txt +0 -0
- {openai_sdk_helpers-0.4.2.dist-info → openai_sdk_helpers-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,33 +8,158 @@ generation, validation, and serialization.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
# Standard library imports
|
|
11
|
-
import
|
|
11
|
+
import copy
|
|
12
|
+
import dataclasses
|
|
12
13
|
import inspect
|
|
13
14
|
import json
|
|
14
|
-
import logging
|
|
15
15
|
from dataclasses import dataclass
|
|
16
|
-
from enum import Enum
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from typing import (
|
|
19
18
|
Any,
|
|
20
19
|
ClassVar,
|
|
20
|
+
Mapping,
|
|
21
21
|
TypeVar,
|
|
22
22
|
cast,
|
|
23
|
-
get_args,
|
|
24
|
-
get_origin,
|
|
25
23
|
)
|
|
26
24
|
|
|
27
25
|
# Third-party imports
|
|
28
26
|
from pydantic import BaseModel, ConfigDict, Field
|
|
29
27
|
from openai.types.responses.response_text_config_param import ResponseTextConfigParam
|
|
30
28
|
|
|
31
|
-
|
|
32
29
|
# Internal imports
|
|
33
30
|
|
|
34
|
-
from ..utils import check_filepath,
|
|
31
|
+
from ..utils import check_filepath, BaseModelJSONSerializable
|
|
35
32
|
|
|
36
33
|
T = TypeVar("T", bound="StructureBase")
|
|
37
|
-
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _add_required_fields(target: dict[str, Any]) -> None:
|
|
37
|
+
"""Ensure every object declares its required properties."""
|
|
38
|
+
properties = target.get("properties")
|
|
39
|
+
if isinstance(properties, dict) and properties:
|
|
40
|
+
target["required"] = sorted(properties.keys())
|
|
41
|
+
for value in target.values():
|
|
42
|
+
if isinstance(value, dict):
|
|
43
|
+
_add_required_fields(value)
|
|
44
|
+
elif isinstance(value, list):
|
|
45
|
+
for item in value:
|
|
46
|
+
if isinstance(item, dict):
|
|
47
|
+
_add_required_fields(item)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _enforce_additional_properties(target: Any) -> None:
|
|
51
|
+
"""Ensure every object schema disallows additional properties."""
|
|
52
|
+
if isinstance(target, dict):
|
|
53
|
+
schema_type = target.get("type")
|
|
54
|
+
allows_object_type = schema_type == "object" or (
|
|
55
|
+
isinstance(schema_type, list)
|
|
56
|
+
and "object" in schema_type
|
|
57
|
+
and set(schema_type).issubset({"object", "null"})
|
|
58
|
+
)
|
|
59
|
+
if (allows_object_type or "properties" in target) and "$ref" not in target:
|
|
60
|
+
target.setdefault("properties", {})
|
|
61
|
+
target["additionalProperties"] = False
|
|
62
|
+
any_of = target.get("anyOf")
|
|
63
|
+
if isinstance(any_of, list):
|
|
64
|
+
for entry in any_of:
|
|
65
|
+
if not isinstance(entry, dict):
|
|
66
|
+
continue
|
|
67
|
+
entry_type = entry.get("type")
|
|
68
|
+
entry_allows_object_type = entry_type == "object" or (
|
|
69
|
+
isinstance(entry_type, list)
|
|
70
|
+
and "object" in entry_type
|
|
71
|
+
and set(entry_type).issubset({"object", "null"})
|
|
72
|
+
)
|
|
73
|
+
if (
|
|
74
|
+
entry_allows_object_type or "properties" in entry
|
|
75
|
+
) and "$ref" not in entry:
|
|
76
|
+
entry.setdefault("properties", {})
|
|
77
|
+
entry["additionalProperties"] = False
|
|
78
|
+
for value in target.values():
|
|
79
|
+
_enforce_additional_properties(value)
|
|
80
|
+
elif isinstance(target, list):
|
|
81
|
+
for item in target:
|
|
82
|
+
_enforce_additional_properties(item)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _build_any_value_schema(depth: int = 0) -> dict[str, Any]:
|
|
86
|
+
"""Return a JSON schema fragment describing a permissive JSON value.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
depth : int, optional
|
|
91
|
+
Current recursion depth for nested arrays. Defaults to 0.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
dict[str, Any]
|
|
96
|
+
JSON schema fragment describing a permissive value.
|
|
97
|
+
"""
|
|
98
|
+
value_types = ["string", "integer", "number", "null"]
|
|
99
|
+
any_of: list[dict[str, Any]] = [{"type": value_type} for value_type in value_types]
|
|
100
|
+
|
|
101
|
+
any_of.append(
|
|
102
|
+
{
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {},
|
|
105
|
+
"additionalProperties": False,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
if depth < 1:
|
|
109
|
+
any_of.append(
|
|
110
|
+
{
|
|
111
|
+
"type": "array",
|
|
112
|
+
"items": _build_any_value_schema(depth + 1),
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return {"anyOf": any_of}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _ensure_items_have_schema(target: Any) -> None:
|
|
120
|
+
"""Ensure array item schemas include type information."""
|
|
121
|
+
if isinstance(target, dict):
|
|
122
|
+
items = target.get("items")
|
|
123
|
+
if isinstance(items, dict):
|
|
124
|
+
if not items:
|
|
125
|
+
target["items"] = _build_any_value_schema()
|
|
126
|
+
else:
|
|
127
|
+
_ensure_schema_has_type(items)
|
|
128
|
+
for value in target.values():
|
|
129
|
+
_ensure_items_have_schema(value)
|
|
130
|
+
elif isinstance(target, list):
|
|
131
|
+
for item in target:
|
|
132
|
+
_ensure_items_have_schema(item)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _ensure_schema_has_type(schema: dict[str, Any]) -> None:
|
|
136
|
+
"""Ensure a schema dictionary includes a type entry when possible."""
|
|
137
|
+
if "type" in schema or "$ref" in schema:
|
|
138
|
+
return
|
|
139
|
+
any_of = schema.get("anyOf")
|
|
140
|
+
if isinstance(any_of, list):
|
|
141
|
+
inferred_types: set[str] = set()
|
|
142
|
+
for entry in any_of:
|
|
143
|
+
if not isinstance(entry, dict):
|
|
144
|
+
continue
|
|
145
|
+
entry_type = entry.get("type")
|
|
146
|
+
if isinstance(entry_type, str):
|
|
147
|
+
inferred_types.add(entry_type)
|
|
148
|
+
elif isinstance(entry_type, list):
|
|
149
|
+
inferred_types.update(
|
|
150
|
+
element for element in entry_type if isinstance(element, str)
|
|
151
|
+
)
|
|
152
|
+
if inferred_types:
|
|
153
|
+
schema["type"] = sorted(inferred_types)
|
|
154
|
+
return
|
|
155
|
+
if "properties" in schema:
|
|
156
|
+
schema["type"] = "object"
|
|
157
|
+
schema.setdefault("additionalProperties", False)
|
|
158
|
+
return
|
|
159
|
+
if "items" in schema:
|
|
160
|
+
schema["type"] = "array"
|
|
161
|
+
return
|
|
162
|
+
schema.update(_build_any_value_schema())
|
|
38
163
|
|
|
39
164
|
|
|
40
165
|
class StructureBase(BaseModelJSONSerializable):
|
|
@@ -51,9 +176,9 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
51
176
|
|
|
52
177
|
Attributes
|
|
53
178
|
----------
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
179
|
+
model_config : ConfigDict
|
|
180
|
+
Pydantic model configuration with strict validation and enum handling.
|
|
181
|
+
|
|
57
182
|
|
|
58
183
|
Methods
|
|
59
184
|
-------
|
|
@@ -85,8 +210,6 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
85
210
|
Produce Field overrides for dynamic schema customization.
|
|
86
211
|
print()
|
|
87
212
|
Return a string representation of the structure.
|
|
88
|
-
console_print()
|
|
89
|
-
Print the string representation to stdout.
|
|
90
213
|
|
|
91
214
|
Examples
|
|
92
215
|
--------
|
|
@@ -113,10 +236,9 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
113
236
|
"""
|
|
114
237
|
|
|
115
238
|
model_config = ConfigDict(
|
|
116
|
-
title=
|
|
239
|
+
title=__qualname__, use_enum_values=False, strict=True, extra="forbid"
|
|
117
240
|
)
|
|
118
|
-
|
|
119
|
-
"""Optional location for saving schema files."""
|
|
241
|
+
_schema_cache: ClassVar[dict[type["StructureBase"], dict[str, Any]]] = {}
|
|
120
242
|
|
|
121
243
|
@classmethod
|
|
122
244
|
def get_prompt(cls, add_enum_values: bool = True) -> str:
|
|
@@ -137,31 +259,6 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
137
259
|
return "No structured prompt available."
|
|
138
260
|
return "# Output Format\n" + "\n".join(prompt_lines)
|
|
139
261
|
|
|
140
|
-
@classmethod
|
|
141
|
-
def _get_all_fields(cls) -> dict[Any, Any]:
|
|
142
|
-
"""Collect all fields from the class hierarchy including inherited ones.
|
|
143
|
-
|
|
144
|
-
Traverses the method resolution order (MRO) to gather fields from
|
|
145
|
-
all parent classes that inherit from BaseModel, ensuring inherited
|
|
146
|
-
fields are included in schema generation.
|
|
147
|
-
|
|
148
|
-
Results are computed once per class and cached for performance.
|
|
149
|
-
|
|
150
|
-
Returns
|
|
151
|
-
-------
|
|
152
|
-
dict[Any, Any]
|
|
153
|
-
Mapping of field names to Pydantic ModelField instances.
|
|
154
|
-
"""
|
|
155
|
-
# Use class-level caching for performance
|
|
156
|
-
cache_attr = "_all_fields_cache"
|
|
157
|
-
if not hasattr(cls, cache_attr):
|
|
158
|
-
fields = {}
|
|
159
|
-
for base in reversed(cls.__mro__): # Traverse inheritance tree
|
|
160
|
-
if issubclass(base, BaseModel) and hasattr(base, "model_fields"):
|
|
161
|
-
fields.update(base.model_fields) # Merge fields from parent
|
|
162
|
-
setattr(cls, cache_attr, fields)
|
|
163
|
-
return getattr(cls, cache_attr)
|
|
164
|
-
|
|
165
262
|
@classmethod
|
|
166
263
|
def _get_field_prompt(
|
|
167
264
|
cls, field_name: str, field, add_enum_values: bool = True
|
|
@@ -210,12 +307,12 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
210
307
|
)
|
|
211
308
|
|
|
212
309
|
@classmethod
|
|
213
|
-
def get_input_prompt_list(cls,
|
|
310
|
+
def get_input_prompt_list(cls, add_enums: bool = True) -> list[str]:
|
|
214
311
|
"""Dynamically build a structured prompt including inherited fields.
|
|
215
312
|
|
|
216
313
|
Parameters
|
|
217
314
|
----------
|
|
218
|
-
|
|
315
|
+
add_enums : bool, default=True
|
|
219
316
|
Whether enumeration values should be included.
|
|
220
317
|
|
|
221
318
|
Returns
|
|
@@ -226,9 +323,7 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
226
323
|
prompt_lines = []
|
|
227
324
|
all_fields = cls._get_all_fields()
|
|
228
325
|
for field_name, field in all_fields.items():
|
|
229
|
-
prompt_lines.append(
|
|
230
|
-
cls._get_field_prompt(field_name, field, add_enum_values)
|
|
231
|
-
)
|
|
326
|
+
prompt_lines.append(cls._get_field_prompt(field_name, field, add_enums))
|
|
232
327
|
return prompt_lines
|
|
233
328
|
|
|
234
329
|
@classmethod
|
|
@@ -282,7 +377,9 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
282
377
|
return assistant_format(cls)
|
|
283
378
|
|
|
284
379
|
@classmethod
|
|
285
|
-
def response_tool_definition(
|
|
380
|
+
def response_tool_definition(
|
|
381
|
+
cls, tool_name: str, *, tool_description: str | None
|
|
382
|
+
) -> dict:
|
|
286
383
|
"""Build a chat completion tool definition for this structure.
|
|
287
384
|
|
|
288
385
|
Creates a function tool definition compatible with the chat
|
|
@@ -357,12 +454,17 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
357
454
|
- Adds null type for fields with None default
|
|
358
455
|
- Cleans up $ref entries for better compatibility
|
|
359
456
|
- Recursively processes nested structures
|
|
457
|
+
- Caches the computed schema per class
|
|
360
458
|
|
|
361
459
|
Examples
|
|
362
460
|
--------
|
|
363
461
|
>>> schema = MyStructure.get_schema()
|
|
364
462
|
>>> print(json.dumps(schema, indent=2))
|
|
365
463
|
"""
|
|
464
|
+
cached_schema = cls._schema_cache.get(cls)
|
|
465
|
+
if cached_schema is not None:
|
|
466
|
+
return copy.deepcopy(cached_schema)
|
|
467
|
+
|
|
366
468
|
schema = cls.model_json_schema()
|
|
367
469
|
|
|
368
470
|
def clean_refs(obj: Any) -> Any:
|
|
@@ -380,18 +482,60 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
380
482
|
|
|
381
483
|
cleaned_schema = cast(dict[str, Any], clean_refs(schema))
|
|
382
484
|
|
|
383
|
-
def
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
485
|
+
def _resolve_ref(
|
|
486
|
+
ref: str,
|
|
487
|
+
root: dict[str, Any],
|
|
488
|
+
seen: set[str],
|
|
489
|
+
) -> dict[str, Any] | None:
|
|
490
|
+
if not ref.startswith("#/"):
|
|
491
|
+
return None
|
|
492
|
+
if ref in seen:
|
|
493
|
+
return None
|
|
494
|
+
seen.add(ref)
|
|
495
|
+
|
|
496
|
+
current: Any = root
|
|
497
|
+
for part in ref.lstrip("#/").split("/"):
|
|
498
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
499
|
+
if isinstance(current, dict) and part in current:
|
|
500
|
+
current = current[part]
|
|
501
|
+
else:
|
|
502
|
+
seen.discard(ref)
|
|
503
|
+
return None
|
|
504
|
+
if isinstance(current, dict):
|
|
505
|
+
resolved = cast(dict[str, Any], json.loads(json.dumps(current)))
|
|
506
|
+
else:
|
|
507
|
+
resolved = None
|
|
508
|
+
seen.discard(ref)
|
|
509
|
+
return resolved
|
|
510
|
+
|
|
511
|
+
def _inline_anyof_refs(obj: Any, root: dict[str, Any], seen: set[str]) -> Any:
|
|
512
|
+
if isinstance(obj, dict):
|
|
513
|
+
updated: dict[str, Any] = {}
|
|
514
|
+
for key, value in obj.items():
|
|
515
|
+
if key == "anyOf" and isinstance(value, list):
|
|
516
|
+
updated_items = []
|
|
517
|
+
for item in value:
|
|
518
|
+
if (
|
|
519
|
+
isinstance(item, dict)
|
|
520
|
+
and "$ref" in item
|
|
521
|
+
and "type" not in item
|
|
522
|
+
):
|
|
523
|
+
resolved = _resolve_ref(item["$ref"], root, seen)
|
|
524
|
+
if resolved is not None:
|
|
525
|
+
item = resolved
|
|
526
|
+
updated_items.append(_inline_anyof_refs(item, root, seen))
|
|
527
|
+
updated[key] = updated_items
|
|
528
|
+
else:
|
|
529
|
+
updated[key] = _inline_anyof_refs(value, root, seen)
|
|
530
|
+
return updated
|
|
531
|
+
if isinstance(obj, list):
|
|
532
|
+
return [_inline_anyof_refs(item, root, seen) for item in obj]
|
|
533
|
+
return obj
|
|
534
|
+
|
|
535
|
+
cleaned_schema = cast(
|
|
536
|
+
dict[str, Any], _inline_anyof_refs(cleaned_schema, schema, set())
|
|
537
|
+
)
|
|
538
|
+
_ensure_items_have_schema(cleaned_schema)
|
|
395
539
|
|
|
396
540
|
nullable_fields = {
|
|
397
541
|
name
|
|
@@ -422,242 +566,41 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
422
566
|
if not has_null:
|
|
423
567
|
any_of.append({"type": "null"})
|
|
424
568
|
|
|
425
|
-
|
|
426
|
-
|
|
569
|
+
_add_required_fields(cleaned_schema)
|
|
570
|
+
_enforce_additional_properties(cleaned_schema)
|
|
571
|
+
cls._schema_cache[cls] = cleaned_schema
|
|
572
|
+
return copy.deepcopy(cleaned_schema)
|
|
427
573
|
|
|
428
574
|
@classmethod
|
|
429
|
-
def save_schema_to_file(cls) -> Path:
|
|
575
|
+
def save_schema_to_file(cls, file_path: Path) -> Path:
|
|
430
576
|
"""Save the generated JSON schema to a file.
|
|
431
577
|
|
|
432
|
-
Generates the schema using get_schema and saves it to
|
|
433
|
-
|
|
434
|
-
|
|
578
|
+
Generates the schema using get_schema and saves it to the provided
|
|
579
|
+
file path.
|
|
580
|
+
|
|
581
|
+
Parameters
|
|
582
|
+
----------
|
|
583
|
+
file_path : Path
|
|
584
|
+
Full path (including filename) where the schema should be saved.
|
|
435
585
|
|
|
436
586
|
Returns
|
|
437
587
|
-------
|
|
438
588
|
Path
|
|
439
589
|
Absolute path to the saved schema file.
|
|
440
590
|
|
|
441
|
-
Raises
|
|
442
|
-
------
|
|
443
|
-
RuntimeError
|
|
444
|
-
If DATA_PATH is not set on the class.
|
|
445
591
|
|
|
446
592
|
Examples
|
|
447
593
|
--------
|
|
448
594
|
>>> MyStructure.DATA_PATH = Path("./schemas")
|
|
449
|
-
>>> schema_path = MyStructure.save_schema_to_file()
|
|
595
|
+
>>> schema_path = MyStructure.save_schema_to_file(file_path=MyStructure.DATA_PATH / "MyStructure_schema.json")
|
|
450
596
|
>>> print(schema_path)
|
|
451
597
|
PosixPath('./schemas/MyStructure_schema.json')
|
|
452
598
|
"""
|
|
453
|
-
schema = cls.get_schema()
|
|
454
|
-
if cls.DATA_PATH is None:
|
|
455
|
-
raise RuntimeError(
|
|
456
|
-
"DATA_PATH is not set. Set StructureBase.DATA_PATH before saving."
|
|
457
|
-
)
|
|
458
|
-
file_path = cls.DATA_PATH / f"{cls.__name__}_schema.json"
|
|
459
599
|
check_filepath(file_path)
|
|
460
600
|
with file_path.open("w", encoding="utf-8") as file_handle:
|
|
461
|
-
json.dump(
|
|
601
|
+
json.dump(cls.get_schema(), file_handle, indent=2, ensure_ascii=False)
|
|
462
602
|
return file_path
|
|
463
603
|
|
|
464
|
-
@classmethod
|
|
465
|
-
def _extract_enum_class(cls, field_type: Any) -> type[Enum] | None:
|
|
466
|
-
"""Extract an Enum class from a field's type annotation.
|
|
467
|
-
|
|
468
|
-
Handles direct Enum types, list[Enum], and optional Enums.
|
|
469
|
-
|
|
470
|
-
Parameters
|
|
471
|
-
----------
|
|
472
|
-
field_type : Any
|
|
473
|
-
Type annotation of a field.
|
|
474
|
-
|
|
475
|
-
Returns
|
|
476
|
-
-------
|
|
477
|
-
type[Enum] or None
|
|
478
|
-
Enum class if found, otherwise None.
|
|
479
|
-
"""
|
|
480
|
-
origin = get_origin(field_type)
|
|
481
|
-
args = get_args(field_type)
|
|
482
|
-
|
|
483
|
-
if inspect.isclass(field_type) and issubclass(field_type, Enum):
|
|
484
|
-
return field_type
|
|
485
|
-
elif (
|
|
486
|
-
origin is list
|
|
487
|
-
and args
|
|
488
|
-
and inspect.isclass(args[0])
|
|
489
|
-
and issubclass(args[0], Enum)
|
|
490
|
-
):
|
|
491
|
-
return args[0]
|
|
492
|
-
elif origin is not None:
|
|
493
|
-
# Handle Union types
|
|
494
|
-
for arg in args:
|
|
495
|
-
enum_cls = cls._extract_enum_class(arg)
|
|
496
|
-
if enum_cls:
|
|
497
|
-
return enum_cls
|
|
498
|
-
return None
|
|
499
|
-
|
|
500
|
-
@classmethod
|
|
501
|
-
def _build_enum_field_mapping(cls) -> dict[str, type[Enum]]:
|
|
502
|
-
"""Build a mapping from field names to their Enum classes.
|
|
503
|
-
|
|
504
|
-
Used by from_raw_input to correctly process enum values from
|
|
505
|
-
raw API responses.
|
|
506
|
-
|
|
507
|
-
Returns
|
|
508
|
-
-------
|
|
509
|
-
dict[str, type[Enum]]
|
|
510
|
-
Mapping of field names to Enum types.
|
|
511
|
-
"""
|
|
512
|
-
mapping: dict[str, type[Enum]] = {}
|
|
513
|
-
|
|
514
|
-
for name, model_field in cls.model_fields.items():
|
|
515
|
-
field_type = model_field.annotation
|
|
516
|
-
enum_cls = cls._extract_enum_class(field_type)
|
|
517
|
-
|
|
518
|
-
if enum_cls is not None:
|
|
519
|
-
mapping[name] = enum_cls
|
|
520
|
-
|
|
521
|
-
return mapping
|
|
522
|
-
|
|
523
|
-
@classmethod
|
|
524
|
-
def from_raw_input(cls: type[T], data: dict) -> T:
|
|
525
|
-
"""Construct an instance from a dictionary of raw input data.
|
|
526
|
-
|
|
527
|
-
Particularly useful for converting data from OpenAI API tool calls
|
|
528
|
-
or assistant outputs into validated structure instances. Handles
|
|
529
|
-
enum value conversion automatically.
|
|
530
|
-
|
|
531
|
-
Parameters
|
|
532
|
-
----------
|
|
533
|
-
data : dict
|
|
534
|
-
Raw input data dictionary from API response.
|
|
535
|
-
|
|
536
|
-
Returns
|
|
537
|
-
-------
|
|
538
|
-
T
|
|
539
|
-
Validated instance of the structure class.
|
|
540
|
-
|
|
541
|
-
Examples
|
|
542
|
-
--------
|
|
543
|
-
>>> raw_data = {"title": "Test", "score": 0.95}
|
|
544
|
-
>>> instance = MyStructure.from_raw_input(raw_data)
|
|
545
|
-
"""
|
|
546
|
-
mapping = cls._build_enum_field_mapping()
|
|
547
|
-
clean_data = data.copy()
|
|
548
|
-
|
|
549
|
-
for field, enum_cls in mapping.items():
|
|
550
|
-
raw_value = clean_data.get(field)
|
|
551
|
-
|
|
552
|
-
if raw_value is None:
|
|
553
|
-
continue
|
|
554
|
-
|
|
555
|
-
# List of enum values
|
|
556
|
-
if isinstance(raw_value, list):
|
|
557
|
-
converted = []
|
|
558
|
-
for v in raw_value:
|
|
559
|
-
if isinstance(v, enum_cls):
|
|
560
|
-
converted.append(v)
|
|
561
|
-
elif isinstance(v, str):
|
|
562
|
-
# Check if it's a valid value
|
|
563
|
-
if v in enum_cls._value2member_map_:
|
|
564
|
-
converted.append(enum_cls(v))
|
|
565
|
-
# Check if it's a valid name
|
|
566
|
-
elif v in enum_cls.__members__:
|
|
567
|
-
converted.append(enum_cls.__members__[v])
|
|
568
|
-
else:
|
|
569
|
-
log(
|
|
570
|
-
f"[{cls.__name__}] Skipping invalid value for '{field}': '{v}' not in {enum_cls.__name__}",
|
|
571
|
-
level=logging.WARNING,
|
|
572
|
-
)
|
|
573
|
-
clean_data[field] = converted
|
|
574
|
-
|
|
575
|
-
# Single enum value
|
|
576
|
-
elif (
|
|
577
|
-
isinstance(raw_value, str) and raw_value in enum_cls._value2member_map_
|
|
578
|
-
):
|
|
579
|
-
clean_data[field] = enum_cls(raw_value)
|
|
580
|
-
|
|
581
|
-
elif isinstance(raw_value, enum_cls):
|
|
582
|
-
# already the correct type
|
|
583
|
-
continue
|
|
584
|
-
|
|
585
|
-
else:
|
|
586
|
-
log(
|
|
587
|
-
message=f"[{cls.__name__}] Invalid value for '{field}': '{raw_value}' not in {enum_cls.__name__}",
|
|
588
|
-
level=logging.WARNING,
|
|
589
|
-
)
|
|
590
|
-
clean_data[field] = None
|
|
591
|
-
|
|
592
|
-
return cls(**clean_data)
|
|
593
|
-
|
|
594
|
-
@classmethod
|
|
595
|
-
def from_tool_arguments(cls: type[T], arguments: str) -> T:
|
|
596
|
-
"""Parse tool call arguments which may not be valid JSON.
|
|
597
|
-
|
|
598
|
-
The OpenAI API is expected to return well-formed JSON for tool arguments,
|
|
599
|
-
but minor formatting issues (such as the use of single quotes) can occur.
|
|
600
|
-
This helper first tries ``json.loads`` and falls back to
|
|
601
|
-
``ast.literal_eval`` for simple cases.
|
|
602
|
-
|
|
603
|
-
Parameters
|
|
604
|
-
----------
|
|
605
|
-
arguments
|
|
606
|
-
Raw argument string from the tool call.
|
|
607
|
-
|
|
608
|
-
Returns
|
|
609
|
-
-------
|
|
610
|
-
dict
|
|
611
|
-
Parsed dictionary of arguments.
|
|
612
|
-
|
|
613
|
-
Raises
|
|
614
|
-
------
|
|
615
|
-
ValueError
|
|
616
|
-
If the arguments cannot be parsed as JSON.
|
|
617
|
-
|
|
618
|
-
Examples
|
|
619
|
-
--------
|
|
620
|
-
>>> parse_tool_arguments('{"key": "value"}')["key"]
|
|
621
|
-
'value'
|
|
622
|
-
"""
|
|
623
|
-
try:
|
|
624
|
-
structured_data = json.loads(arguments)
|
|
625
|
-
|
|
626
|
-
except json.JSONDecodeError:
|
|
627
|
-
try:
|
|
628
|
-
structured_data = ast.literal_eval(arguments)
|
|
629
|
-
except (SyntaxError, ValueError) as exc:
|
|
630
|
-
raise ValueError(
|
|
631
|
-
f"Invalid JSON arguments: {arguments}. "
|
|
632
|
-
f"Expected valid JSON or Python literal."
|
|
633
|
-
) from exc
|
|
634
|
-
return cls.from_raw_input(structured_data)
|
|
635
|
-
|
|
636
|
-
@staticmethod
|
|
637
|
-
def format_output(label: str, *, value: Any) -> str:
|
|
638
|
-
"""
|
|
639
|
-
Format a label and value for string output.
|
|
640
|
-
|
|
641
|
-
Handles None values and lists appropriately.
|
|
642
|
-
|
|
643
|
-
Parameters
|
|
644
|
-
----------
|
|
645
|
-
label : str
|
|
646
|
-
Label describing the value.
|
|
647
|
-
value : Any
|
|
648
|
-
Value to format for display.
|
|
649
|
-
|
|
650
|
-
Returns
|
|
651
|
-
-------
|
|
652
|
-
str
|
|
653
|
-
Formatted string (for example ``"- Label: Value"``).
|
|
654
|
-
"""
|
|
655
|
-
if not value:
|
|
656
|
-
return f"- {label}: None"
|
|
657
|
-
if isinstance(value, list):
|
|
658
|
-
return f"- {label}: {', '.join(str(v) for v in value)}"
|
|
659
|
-
return f"- {label}: {str(value)}"
|
|
660
|
-
|
|
661
604
|
@classmethod
|
|
662
605
|
def schema_overrides(cls) -> dict[str, Any]:
|
|
663
606
|
"""
|
|
@@ -686,14 +629,32 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
686
629
|
]
|
|
687
630
|
)
|
|
688
631
|
|
|
689
|
-
|
|
690
|
-
|
|
632
|
+
@classmethod
|
|
633
|
+
def from_dataclass(cls: type[T], data: Any) -> T:
|
|
634
|
+
"""Create an instance from a dataclass object.
|
|
635
|
+
|
|
636
|
+
Parameters
|
|
637
|
+
----------
|
|
638
|
+
data : Any
|
|
639
|
+
Dataclass instance, mapping, or object with attributes to convert.
|
|
640
|
+
Private attributes (prefixed with ``_``) are ignored.
|
|
691
641
|
|
|
692
642
|
Returns
|
|
693
643
|
-------
|
|
694
|
-
|
|
644
|
+
T
|
|
645
|
+
New instance of the structure populated from the dataclass.
|
|
695
646
|
"""
|
|
696
|
-
|
|
647
|
+
|
|
648
|
+
def _filter_private(items: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
649
|
+
return {name: value for name, value in items if not name.startswith("_")}
|
|
650
|
+
|
|
651
|
+
if dataclasses.is_dataclass(data) and not isinstance(data, type):
|
|
652
|
+
payload = dataclasses.asdict(data, dict_factory=_filter_private)
|
|
653
|
+
elif isinstance(data, Mapping):
|
|
654
|
+
payload = _filter_private(list(data.items()))
|
|
655
|
+
else:
|
|
656
|
+
payload = _filter_private(list(vars(data).items()))
|
|
657
|
+
return cls(**payload)
|
|
697
658
|
|
|
698
659
|
|
|
699
660
|
@dataclass(frozen=True)
|