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.
- openai_sdk_helpers/__init__.py +62 -0
- openai_sdk_helpers/agent/__init__.py +31 -0
- openai_sdk_helpers/agent/base.py +330 -0
- openai_sdk_helpers/agent/config.py +66 -0
- openai_sdk_helpers/agent/project_manager.py +511 -0
- openai_sdk_helpers/agent/prompt_utils.py +9 -0
- openai_sdk_helpers/agent/runner.py +215 -0
- openai_sdk_helpers/agent/summarizer.py +85 -0
- openai_sdk_helpers/agent/translator.py +139 -0
- openai_sdk_helpers/agent/utils.py +47 -0
- openai_sdk_helpers/agent/validation.py +97 -0
- openai_sdk_helpers/agent/vector_search.py +462 -0
- openai_sdk_helpers/agent/web_search.py +404 -0
- openai_sdk_helpers/config.py +199 -0
- openai_sdk_helpers/enums/__init__.py +7 -0
- openai_sdk_helpers/enums/base.py +29 -0
- openai_sdk_helpers/environment.py +27 -0
- openai_sdk_helpers/prompt/__init__.py +77 -0
- openai_sdk_helpers/py.typed +0 -0
- openai_sdk_helpers/response/__init__.py +20 -0
- openai_sdk_helpers/response/base.py +505 -0
- openai_sdk_helpers/response/messages.py +211 -0
- openai_sdk_helpers/response/runner.py +104 -0
- openai_sdk_helpers/response/tool_call.py +70 -0
- openai_sdk_helpers/response/vector_store.py +84 -0
- openai_sdk_helpers/structure/__init__.py +43 -0
- openai_sdk_helpers/structure/agent_blueprint.py +224 -0
- openai_sdk_helpers/structure/base.py +713 -0
- openai_sdk_helpers/structure/plan/__init__.py +13 -0
- openai_sdk_helpers/structure/plan/enum.py +64 -0
- openai_sdk_helpers/structure/plan/plan.py +253 -0
- openai_sdk_helpers/structure/plan/task.py +122 -0
- openai_sdk_helpers/structure/prompt.py +24 -0
- openai_sdk_helpers/structure/responses.py +132 -0
- openai_sdk_helpers/structure/summary.py +65 -0
- openai_sdk_helpers/structure/validation.py +47 -0
- openai_sdk_helpers/structure/vector_search.py +86 -0
- openai_sdk_helpers/structure/web_search.py +46 -0
- openai_sdk_helpers/utils/__init__.py +25 -0
- openai_sdk_helpers/utils/core.py +300 -0
- openai_sdk_helpers/vector_storage/__init__.py +15 -0
- openai_sdk_helpers/vector_storage/cleanup.py +91 -0
- openai_sdk_helpers/vector_storage/storage.py +564 -0
- openai_sdk_helpers/vector_storage/types.py +58 -0
- {openai_sdk_helpers-0.0.5.dist-info → openai_sdk_helpers-0.0.7.dist-info}/METADATA +6 -3
- openai_sdk_helpers-0.0.7.dist-info/RECORD +51 -0
- openai_sdk_helpers-0.0.5.dist-info/RECORD +0 -7
- {openai_sdk_helpers-0.0.5.dist-info → openai_sdk_helpers-0.0.7.dist-info}/WHEEL +0 -0
- {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)
|