openai-sdk-helpers 0.3.0__py3-none-any.whl → 0.4.1__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 +6 -6
- openai_sdk_helpers/agent/__init__.py +4 -4
- openai_sdk_helpers/agent/base.py +254 -113
- openai_sdk_helpers/agent/config.py +91 -37
- openai_sdk_helpers/agent/coordination.py +64 -28
- openai_sdk_helpers/agent/runner.py +16 -15
- openai_sdk_helpers/agent/search/base.py +94 -45
- openai_sdk_helpers/agent/search/vector.py +86 -58
- openai_sdk_helpers/agent/search/web.py +71 -40
- openai_sdk_helpers/agent/summarizer.py +32 -7
- openai_sdk_helpers/agent/translator.py +57 -24
- openai_sdk_helpers/agent/validation.py +34 -4
- openai_sdk_helpers/cli.py +42 -0
- openai_sdk_helpers/config.py +0 -1
- openai_sdk_helpers/environment.py +3 -2
- openai_sdk_helpers/files_api.py +35 -3
- openai_sdk_helpers/prompt/base.py +6 -0
- openai_sdk_helpers/response/__init__.py +3 -3
- openai_sdk_helpers/response/base.py +142 -73
- openai_sdk_helpers/response/config.py +63 -58
- openai_sdk_helpers/response/files.py +5 -5
- openai_sdk_helpers/response/messages.py +3 -3
- openai_sdk_helpers/response/runner.py +7 -7
- openai_sdk_helpers/response/tool_call.py +94 -4
- openai_sdk_helpers/response/vector_store.py +3 -3
- openai_sdk_helpers/streamlit_app/__init__.py +4 -4
- openai_sdk_helpers/streamlit_app/app.py +16 -16
- openai_sdk_helpers/streamlit_app/config.py +82 -70
- openai_sdk_helpers/streamlit_app/streamlit_web_search.py +2 -2
- openai_sdk_helpers/structure/__init__.py +6 -2
- openai_sdk_helpers/structure/agent_blueprint.py +2 -2
- openai_sdk_helpers/structure/base.py +8 -99
- openai_sdk_helpers/structure/plan/plan.py +2 -2
- openai_sdk_helpers/structure/plan/task.py +9 -9
- openai_sdk_helpers/structure/prompt.py +2 -2
- openai_sdk_helpers/structure/responses.py +15 -15
- openai_sdk_helpers/structure/summary.py +3 -3
- openai_sdk_helpers/structure/translation.py +32 -0
- openai_sdk_helpers/structure/validation.py +2 -2
- openai_sdk_helpers/structure/vector_search.py +7 -7
- openai_sdk_helpers/structure/web_search.py +6 -6
- openai_sdk_helpers/tools.py +41 -15
- openai_sdk_helpers/utils/__init__.py +19 -5
- openai_sdk_helpers/utils/json/__init__.py +55 -0
- openai_sdk_helpers/utils/json/base_model.py +181 -0
- openai_sdk_helpers/utils/{json_utils.py → json/data_class.py} +33 -68
- openai_sdk_helpers/utils/json/ref.py +113 -0
- openai_sdk_helpers/utils/json/utils.py +203 -0
- openai_sdk_helpers/utils/output_validation.py +21 -1
- openai_sdk_helpers/utils/path_utils.py +34 -1
- openai_sdk_helpers/utils/registry.py +46 -8
- openai_sdk_helpers/vector_storage/storage.py +10 -0
- {openai_sdk_helpers-0.3.0.dist-info → openai_sdk_helpers-0.4.1.dist-info}/METADATA +7 -7
- openai_sdk_helpers-0.4.1.dist-info/RECORD +86 -0
- openai_sdk_helpers-0.3.0.dist-info/RECORD +0 -81
- {openai_sdk_helpers-0.3.0.dist-info → openai_sdk_helpers-0.4.1.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.3.0.dist-info → openai_sdk_helpers-0.4.1.dist-info}/entry_points.txt +0 -0
- {openai_sdk_helpers-0.3.0.dist-info → openai_sdk_helpers-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Pydantic BaseModel JSON serialization support.
|
|
2
|
+
|
|
3
|
+
This module provides BaseModelJSONSerializable for Pydantic models,
|
|
4
|
+
with to_json, to_json_file, from_json, from_json_file methods and
|
|
5
|
+
customizable _serialize_fields/_deserialize_fields hooks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, TypeVar
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from ..path_utils import check_filepath
|
|
15
|
+
from .utils import _to_jsonable, customJSONEncoder
|
|
16
|
+
|
|
17
|
+
P = TypeVar("P", bound="BaseModelJSONSerializable")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseModelJSONSerializable(BaseModel):
|
|
21
|
+
"""Pydantic BaseModel subclass with JSON serialization support.
|
|
22
|
+
|
|
23
|
+
Adds to_json(), to_json_file(path), from_json(data), from_json_file(path),
|
|
24
|
+
plus overridable _serialize_fields(data) and _deserialize_fields(data) hooks.
|
|
25
|
+
|
|
26
|
+
Methods
|
|
27
|
+
-------
|
|
28
|
+
to_json()
|
|
29
|
+
Return a JSON-compatible dict representation.
|
|
30
|
+
to_json_file(filepath)
|
|
31
|
+
Write serialized JSON data to a file path.
|
|
32
|
+
from_json(data)
|
|
33
|
+
Create an instance from a JSON-compatible dict (class method).
|
|
34
|
+
from_json_file(filepath)
|
|
35
|
+
Load an instance from a JSON file (class method).
|
|
36
|
+
_serialize_fields(data)
|
|
37
|
+
Customize serialization (override in subclasses).
|
|
38
|
+
_deserialize_fields(data)
|
|
39
|
+
Customize deserialization (override in subclasses).
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> from pydantic import BaseModel
|
|
44
|
+
>>> class MyConfig(BaseModelJSONSerializable, BaseModel):
|
|
45
|
+
... name: str
|
|
46
|
+
... value: int
|
|
47
|
+
>>> cfg = MyConfig(name="test", value=42)
|
|
48
|
+
>>> cfg.to_json()
|
|
49
|
+
{'name': 'test', 'value': 42}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def to_json(self) -> dict[str, Any]:
|
|
53
|
+
"""Return a JSON-compatible dict representation.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
dict[str, Any]
|
|
58
|
+
Serialized model data.
|
|
59
|
+
"""
|
|
60
|
+
if hasattr(self, "model_dump"):
|
|
61
|
+
data = getattr(self, "model_dump")()
|
|
62
|
+
else:
|
|
63
|
+
data = self.__dict__.copy()
|
|
64
|
+
return self._serialize_fields(_to_jsonable(data))
|
|
65
|
+
|
|
66
|
+
def to_json_file(self, filepath: str | Path) -> str:
|
|
67
|
+
"""Write serialized JSON data to a file path.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
filepath : str or Path
|
|
72
|
+
Path where the JSON file will be written.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
str
|
|
77
|
+
Absolute path to the written file.
|
|
78
|
+
"""
|
|
79
|
+
target = Path(filepath)
|
|
80
|
+
check_filepath(fullfilepath=str(target))
|
|
81
|
+
with open(target, "w", encoding="utf-8") as handle:
|
|
82
|
+
json.dump(
|
|
83
|
+
self.to_json(),
|
|
84
|
+
handle,
|
|
85
|
+
indent=2,
|
|
86
|
+
ensure_ascii=False,
|
|
87
|
+
cls=customJSONEncoder,
|
|
88
|
+
)
|
|
89
|
+
return str(target)
|
|
90
|
+
|
|
91
|
+
def _serialize_fields(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
92
|
+
"""Customize field serialization.
|
|
93
|
+
|
|
94
|
+
Override this method in subclasses to add custom serialization logic.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
data : dict[str, Any]
|
|
99
|
+
Pre-serialized data dictionary.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
dict[str, Any]
|
|
104
|
+
Modified data dictionary.
|
|
105
|
+
"""
|
|
106
|
+
return data
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def _deserialize_fields(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
110
|
+
"""Customize field deserialization.
|
|
111
|
+
|
|
112
|
+
Override this method in subclasses to add custom deserialization logic.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
data : dict[str, Any]
|
|
117
|
+
Raw data dictionary from JSON.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
dict[str, Any]
|
|
122
|
+
Modified data dictionary.
|
|
123
|
+
"""
|
|
124
|
+
return data
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def from_json(cls: type[P], data: dict[str, Any]) -> P:
|
|
128
|
+
"""Create an instance from a JSON-compatible dict.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
data : dict[str, Any]
|
|
133
|
+
JSON-compatible dictionary containing the instance data.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
P
|
|
138
|
+
New instance of the class.
|
|
139
|
+
|
|
140
|
+
Examples
|
|
141
|
+
--------
|
|
142
|
+
>>> json_data = {"name": "test", "value": 42}
|
|
143
|
+
>>> instance = MyConfig.from_json(json_data)
|
|
144
|
+
"""
|
|
145
|
+
processed_data = cls._deserialize_fields(data)
|
|
146
|
+
return cls(**processed_data) # type: ignore[return-value]
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_json_file(cls: type[P], filepath: str | Path) -> P:
|
|
150
|
+
"""Load an instance from a JSON file.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
filepath : str or Path
|
|
155
|
+
Path to the JSON file to load.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
P
|
|
160
|
+
New instance of the class loaded from the file.
|
|
161
|
+
|
|
162
|
+
Raises
|
|
163
|
+
------
|
|
164
|
+
FileNotFoundError
|
|
165
|
+
If the file does not exist.
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
--------
|
|
169
|
+
>>> instance = MyConfig.from_json_file("config.json")
|
|
170
|
+
"""
|
|
171
|
+
target = Path(filepath)
|
|
172
|
+
if not target.exists():
|
|
173
|
+
raise FileNotFoundError(f"JSON file not found: {target}")
|
|
174
|
+
|
|
175
|
+
with open(target, "r", encoding="utf-8") as handle:
|
|
176
|
+
data = json.load(handle)
|
|
177
|
+
|
|
178
|
+
return cls.from_json(data)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
__all__ = ["BaseModelJSONSerializable"]
|
|
@@ -1,73 +1,24 @@
|
|
|
1
|
-
"""JSON serialization
|
|
1
|
+
"""Dataclass JSON serialization mixin.
|
|
2
|
+
|
|
3
|
+
This module provides the DataclassJSONSerializable mixin for dataclasses,
|
|
4
|
+
adding to_json, to_json_file, from_json, and from_json_file methods.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
9
|
import json
|
|
6
10
|
from dataclasses import asdict, fields, is_dataclass
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from enum import Enum
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
|
|
11
13
|
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from
|
|
20
|
-
|
|
21
|
-
if value is None:
|
|
22
|
-
return None
|
|
23
|
-
if isinstance(value, Enum):
|
|
24
|
-
return value.value
|
|
25
|
-
if isinstance(value, Path):
|
|
26
|
-
return str(value)
|
|
27
|
-
if isinstance(value, datetime):
|
|
28
|
-
return value.isoformat()
|
|
29
|
-
if is_dataclass(value) and not isinstance(value, type):
|
|
30
|
-
return {k: _to_jsonable(v) for k, v in asdict(value).items()}
|
|
31
|
-
if hasattr(value, "model_dump"):
|
|
32
|
-
model_dump = getattr(value, "model_dump")
|
|
33
|
-
return model_dump()
|
|
34
|
-
if isinstance(value, dict):
|
|
35
|
-
return {str(k): _to_jsonable(v) for k, v in value.items()}
|
|
36
|
-
if isinstance(value, (list, tuple, set)):
|
|
37
|
-
return [_to_jsonable(v) for v in value]
|
|
38
|
-
if isinstance(value, BaseStructure):
|
|
39
|
-
return value.model_dump()
|
|
40
|
-
return value
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def coerce_jsonable(value: Any) -> Any:
|
|
44
|
-
"""Convert value into a JSON-serializable representation."""
|
|
45
|
-
from openai_sdk_helpers.response.base import BaseResponse
|
|
46
|
-
|
|
47
|
-
if value is None:
|
|
48
|
-
return None
|
|
49
|
-
if isinstance(value, BaseResponse):
|
|
50
|
-
return coerce_jsonable(value.messages.to_json())
|
|
51
|
-
if is_dataclass(value) and not isinstance(value, type):
|
|
52
|
-
return {key: coerce_jsonable(item) for key, item in asdict(value).items()}
|
|
53
|
-
coerced = _to_jsonable(value)
|
|
54
|
-
try:
|
|
55
|
-
json.dumps(coerced)
|
|
56
|
-
return coerced
|
|
57
|
-
except TypeError:
|
|
58
|
-
return str(coerced)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class customJSONEncoder(json.JSONEncoder):
|
|
62
|
-
"""JSON encoder for common helper types like enums and paths."""
|
|
63
|
-
|
|
64
|
-
def default(self, o: Any) -> Any: # noqa: D401
|
|
65
|
-
"""Return JSON-serializable representation of ``o``."""
|
|
66
|
-
return _to_jsonable(o)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class JSONSerializable:
|
|
70
|
-
"""Mixin for classes that can be serialized to and from JSON.
|
|
14
|
+
from ..path_utils import check_filepath
|
|
15
|
+
from .utils import _to_jsonable, customJSONEncoder
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T", bound="DataclassJSONSerializable")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DataclassJSONSerializable:
|
|
21
|
+
"""Mixin for dataclasses that can be serialized to and from JSON.
|
|
71
22
|
|
|
72
23
|
Methods
|
|
73
24
|
-------
|
|
@@ -79,10 +30,28 @@ class JSONSerializable:
|
|
|
79
30
|
Create an instance from a JSON-compatible dict (class method).
|
|
80
31
|
from_json_file(filepath)
|
|
81
32
|
Load an instance from a JSON file (class method).
|
|
33
|
+
|
|
34
|
+
Examples
|
|
35
|
+
--------
|
|
36
|
+
>>> from dataclasses import dataclass
|
|
37
|
+
>>> from pathlib import Path
|
|
38
|
+
>>> @dataclass
|
|
39
|
+
... class MyData(DataclassJSONSerializable):
|
|
40
|
+
... name: str
|
|
41
|
+
... path: Path
|
|
42
|
+
>>> instance = MyData(name="test", path=Path("/tmp/data"))
|
|
43
|
+
>>> json_data = instance.to_json()
|
|
44
|
+
>>> restored = MyData.from_json(json_data)
|
|
82
45
|
"""
|
|
83
46
|
|
|
84
47
|
def to_json(self) -> dict[str, Any]:
|
|
85
|
-
"""Return a JSON-compatible dict representation.
|
|
48
|
+
"""Return a JSON-compatible dict representation.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
dict[str, Any]
|
|
53
|
+
Serialized data dictionary.
|
|
54
|
+
"""
|
|
86
55
|
if is_dataclass(self) and not isinstance(self, type):
|
|
87
56
|
return {k: _to_jsonable(v) for k, v in asdict(self).items()}
|
|
88
57
|
if hasattr(self, "model_dump"):
|
|
@@ -223,8 +192,4 @@ class JSONSerializable:
|
|
|
223
192
|
return cls.from_json(data)
|
|
224
193
|
|
|
225
194
|
|
|
226
|
-
__all__ = [
|
|
227
|
-
"coerce_jsonable",
|
|
228
|
-
"JSONSerializable",
|
|
229
|
-
"customJSONEncoder",
|
|
230
|
-
]
|
|
195
|
+
__all__ = ["DataclassJSONSerializable"]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Reference encoding helpers for object reconstruction.
|
|
2
|
+
|
|
3
|
+
This module provides helpers for encoding and decoding object references
|
|
4
|
+
using module and qualname information, enabling serialization of class
|
|
5
|
+
references for later reconstruction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_module_qualname(obj: Any) -> tuple[str, str] | None:
|
|
15
|
+
"""Retrieve module and qualname for an object.
|
|
16
|
+
|
|
17
|
+
Safe retrieval that returns None if module or qualname cannot be determined.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
obj : Any
|
|
22
|
+
Object to get module and qualname from.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
tuple[str, str] or None
|
|
27
|
+
Tuple of (module, qualname) or None if cannot be determined.
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> class MyClass:
|
|
32
|
+
... pass
|
|
33
|
+
>>> get_module_qualname(MyClass)
|
|
34
|
+
('__main__', 'MyClass')
|
|
35
|
+
"""
|
|
36
|
+
module = getattr(obj, "__module__", None)
|
|
37
|
+
qualname = getattr(obj, "__qualname__", None)
|
|
38
|
+
if module and qualname:
|
|
39
|
+
return (module, qualname)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def encode_module_qualname(obj: Any) -> dict[str, Any] | None:
|
|
44
|
+
"""Encode object reference for import reconstruction.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
obj : Any
|
|
49
|
+
Object to encode (typically a class).
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
dict[str, Any] or None
|
|
54
|
+
Dictionary with 'module' and 'qualname' keys, or None if encoding fails.
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
>>> class MyClass:
|
|
59
|
+
... pass
|
|
60
|
+
>>> encode_module_qualname(MyClass)
|
|
61
|
+
{'module': '__main__', 'qualname': 'MyClass'}
|
|
62
|
+
"""
|
|
63
|
+
result = get_module_qualname(obj)
|
|
64
|
+
if result is None:
|
|
65
|
+
return None
|
|
66
|
+
module, qualname = result
|
|
67
|
+
return {"module": module, "qualname": qualname}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def decode_module_qualname(ref: dict[str, Any]) -> Any | None:
|
|
71
|
+
"""Import and retrieve object by encoded reference.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
ref : dict[str, Any]
|
|
76
|
+
Dictionary with 'module' and 'qualname' keys.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
Any or None
|
|
81
|
+
Retrieved object or None if import/retrieval fails.
|
|
82
|
+
|
|
83
|
+
Examples
|
|
84
|
+
--------
|
|
85
|
+
>>> ref = {'module': 'pathlib', 'qualname': 'Path'}
|
|
86
|
+
>>> decode_module_qualname(ref)
|
|
87
|
+
<class 'pathlib.Path'>
|
|
88
|
+
"""
|
|
89
|
+
if not isinstance(ref, dict):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
module_name = ref.get("module")
|
|
93
|
+
qualname = ref.get("qualname")
|
|
94
|
+
|
|
95
|
+
if not module_name or not qualname:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
module = importlib.import_module(module_name)
|
|
100
|
+
# Handle nested qualnames (e.g., "OuterClass.InnerClass")
|
|
101
|
+
obj = module
|
|
102
|
+
for attr in qualname.split("."):
|
|
103
|
+
obj = getattr(obj, attr)
|
|
104
|
+
return obj
|
|
105
|
+
except (ImportError, AttributeError):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
"get_module_qualname",
|
|
111
|
+
"encode_module_qualname",
|
|
112
|
+
"decode_module_qualname",
|
|
113
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Core JSON serialization utilities.
|
|
2
|
+
|
|
3
|
+
This module provides the core functions for converting common types to
|
|
4
|
+
JSON-serializable forms, including to_jsonable, coerce_jsonable, and
|
|
5
|
+
customJSONEncoder.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import asdict, is_dataclass
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .ref import encode_module_qualname
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def to_jsonable(value: Any) -> Any:
|
|
21
|
+
"""Convert common types to JSON-safe forms.
|
|
22
|
+
|
|
23
|
+
Recursively converts containers, dicts, dataclasses, Pydantic models, enums,
|
|
24
|
+
paths, datetimes, and StructureBase instances/classes to JSON-serializable forms.
|
|
25
|
+
Private properties (starting with underscore) are excluded from serialization.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
value : Any
|
|
30
|
+
Value to convert to JSON-serializable form.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
Any
|
|
35
|
+
JSON-serializable representation of the value.
|
|
36
|
+
|
|
37
|
+
Notes
|
|
38
|
+
-----
|
|
39
|
+
Serialization rules:
|
|
40
|
+
- Enums: use enum.value
|
|
41
|
+
- Paths: serialize to string
|
|
42
|
+
- Datetimes: ISO8601 datetime.isoformat()
|
|
43
|
+
- Dataclasses (instances): asdict followed by recursive conversion
|
|
44
|
+
- Pydantic-like objects: use model_dump() if available
|
|
45
|
+
- Dicts/containers: recursively convert values; dict keys coerced to str
|
|
46
|
+
- Private properties: keys starting with underscore are excluded
|
|
47
|
+
- StructureBase instances: use .model_dump()
|
|
48
|
+
- StructureBase classes: encode with {module, qualname, "__structure_class__": True}
|
|
49
|
+
- Sets: converted to lists
|
|
50
|
+
|
|
51
|
+
Examples
|
|
52
|
+
--------
|
|
53
|
+
>>> from enum import Enum
|
|
54
|
+
>>> class Color(Enum):
|
|
55
|
+
... RED = "red"
|
|
56
|
+
>>> to_jsonable(Color.RED)
|
|
57
|
+
'red'
|
|
58
|
+
>>> to_jsonable(Path("/tmp/test"))
|
|
59
|
+
'/tmp/test'
|
|
60
|
+
>>> to_jsonable({"public": 1, "_private": 2})
|
|
61
|
+
{'public': 1}
|
|
62
|
+
"""
|
|
63
|
+
return _to_jsonable(value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _to_jsonable(value: Any) -> Any:
|
|
67
|
+
"""Convert common helper types to JSON-serializable forms (internal)."""
|
|
68
|
+
from openai_sdk_helpers.structure.base import StructureBase
|
|
69
|
+
|
|
70
|
+
if value is None:
|
|
71
|
+
return None
|
|
72
|
+
if isinstance(value, Enum):
|
|
73
|
+
return value.value
|
|
74
|
+
if isinstance(value, Path):
|
|
75
|
+
return str(value)
|
|
76
|
+
if isinstance(value, datetime):
|
|
77
|
+
return value.isoformat()
|
|
78
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
79
|
+
return {
|
|
80
|
+
k: _to_jsonable(v)
|
|
81
|
+
for k, v in asdict(value).items()
|
|
82
|
+
if not str(k).startswith("_")
|
|
83
|
+
}
|
|
84
|
+
# Check for StructureBase class (not instance) before model_dump check
|
|
85
|
+
if isinstance(value, type):
|
|
86
|
+
try:
|
|
87
|
+
if issubclass(value, StructureBase):
|
|
88
|
+
encoded = encode_module_qualname(value)
|
|
89
|
+
if encoded:
|
|
90
|
+
encoded["__structure_class__"] = True
|
|
91
|
+
return encoded
|
|
92
|
+
return str(value)
|
|
93
|
+
except TypeError:
|
|
94
|
+
# Some type-like objects may pass isinstance(value, type) but
|
|
95
|
+
# still not be valid arguments to issubclass; ignore these.
|
|
96
|
+
pass
|
|
97
|
+
if isinstance(value, StructureBase):
|
|
98
|
+
return value.model_dump()
|
|
99
|
+
# Check for model_dump on instances (after class checks)
|
|
100
|
+
if hasattr(value, "model_dump") and not isinstance(value, type):
|
|
101
|
+
model_dump = getattr(value, "model_dump")
|
|
102
|
+
return model_dump()
|
|
103
|
+
if isinstance(value, dict):
|
|
104
|
+
return {
|
|
105
|
+
str(k): _to_jsonable(v)
|
|
106
|
+
for k, v in value.items()
|
|
107
|
+
if not str(k).startswith("_")
|
|
108
|
+
}
|
|
109
|
+
if isinstance(value, (list, tuple, set)):
|
|
110
|
+
return [_to_jsonable(v) for v in value]
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def coerce_jsonable(value: Any) -> Any:
|
|
115
|
+
"""Ensure json.dumps succeeds.
|
|
116
|
+
|
|
117
|
+
Falls back to str when necessary. Special-cases ResponseBase.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
value : Any
|
|
122
|
+
Value to coerce to JSON-serializable form.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
Any
|
|
127
|
+
JSON-serializable representation, or str(value) as fallback.
|
|
128
|
+
|
|
129
|
+
Notes
|
|
130
|
+
-----
|
|
131
|
+
This function first attempts to convert the value using to_jsonable(),
|
|
132
|
+
then validates it can be serialized with json.dumps(). If serialization
|
|
133
|
+
fails, it falls back to str(value).
|
|
134
|
+
|
|
135
|
+
Special handling for ResponseBase: serialized as messages.to_json().
|
|
136
|
+
|
|
137
|
+
Examples
|
|
138
|
+
--------
|
|
139
|
+
>>> coerce_jsonable({"key": "value"})
|
|
140
|
+
{'key': 'value'}
|
|
141
|
+
>>> class CustomObj:
|
|
142
|
+
... def __str__(self):
|
|
143
|
+
... return "custom"
|
|
144
|
+
>>> coerce_jsonable(CustomObj())
|
|
145
|
+
'custom'
|
|
146
|
+
"""
|
|
147
|
+
from openai_sdk_helpers.response.base import ResponseBase
|
|
148
|
+
|
|
149
|
+
if value is None:
|
|
150
|
+
return None
|
|
151
|
+
if isinstance(value, ResponseBase):
|
|
152
|
+
return coerce_jsonable(value.messages.to_json())
|
|
153
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
154
|
+
return {
|
|
155
|
+
key: coerce_jsonable(item)
|
|
156
|
+
for key, item in asdict(value).items()
|
|
157
|
+
if not (isinstance(key, str) and key.startswith("_"))
|
|
158
|
+
}
|
|
159
|
+
coerced = _to_jsonable(value)
|
|
160
|
+
try:
|
|
161
|
+
json.dumps(coerced)
|
|
162
|
+
return coerced
|
|
163
|
+
except TypeError:
|
|
164
|
+
return str(coerced)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class customJSONEncoder(json.JSONEncoder):
|
|
168
|
+
"""JSON encoder delegating to to_jsonable.
|
|
169
|
+
|
|
170
|
+
This encoder handles common types like Enum, Path, datetime, dataclasses,
|
|
171
|
+
sets, StructureBase instances/classes, and Pydantic-like objects.
|
|
172
|
+
|
|
173
|
+
Examples
|
|
174
|
+
--------
|
|
175
|
+
>>> import json
|
|
176
|
+
>>> from enum import Enum
|
|
177
|
+
>>> class Color(Enum):
|
|
178
|
+
... RED = "red"
|
|
179
|
+
>>> json.dumps({"color": Color.RED}, cls=customJSONEncoder)
|
|
180
|
+
'{"color": "red"}'
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def default(self, o: Any) -> Any:
|
|
184
|
+
"""Return JSON-serializable representation of object.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
o : Any
|
|
189
|
+
Object to serialize.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
Any
|
|
194
|
+
JSON-serializable representation.
|
|
195
|
+
"""
|
|
196
|
+
return _to_jsonable(o)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
__all__ = [
|
|
200
|
+
"to_jsonable",
|
|
201
|
+
"coerce_jsonable",
|
|
202
|
+
"customJSONEncoder",
|
|
203
|
+
]
|
|
@@ -82,7 +82,7 @@ class JSONSchemaValidator(ValidationRule):
|
|
|
82
82
|
Parameters
|
|
83
83
|
----------
|
|
84
84
|
schema : dict
|
|
85
|
-
JSON schema to validate against.
|
|
85
|
+
A dictionary representing the JSON schema to validate against.
|
|
86
86
|
|
|
87
87
|
Examples
|
|
88
88
|
--------
|
|
@@ -100,6 +100,11 @@ class JSONSchemaValidator(ValidationRule):
|
|
|
100
100
|
----------
|
|
101
101
|
schema : dict
|
|
102
102
|
JSON schema dictionary.
|
|
103
|
+
|
|
104
|
+
Raises
|
|
105
|
+
------
|
|
106
|
+
ValueError
|
|
107
|
+
If the schema is not a valid dictionary.
|
|
103
108
|
"""
|
|
104
109
|
self.schema = schema
|
|
105
110
|
|
|
@@ -186,6 +191,11 @@ class SemanticValidator(ValidationRule):
|
|
|
186
191
|
Phrases that must not appear.
|
|
187
192
|
must_reference_sources : bool
|
|
188
193
|
Check for source references.
|
|
194
|
+
|
|
195
|
+
Raises
|
|
196
|
+
------
|
|
197
|
+
ValueError
|
|
198
|
+
If any of the parameters are not of the expected type.
|
|
189
199
|
"""
|
|
190
200
|
self.must_contain = must_contain or []
|
|
191
201
|
self.must_not_contain = must_not_contain or []
|
|
@@ -281,6 +291,11 @@ class LengthValidator(ValidationRule):
|
|
|
281
291
|
Minimum word count.
|
|
282
292
|
max_words : int, optional
|
|
283
293
|
Maximum word count.
|
|
294
|
+
|
|
295
|
+
Raises
|
|
296
|
+
------
|
|
297
|
+
ValueError
|
|
298
|
+
If any of the parameters are not integers.
|
|
284
299
|
"""
|
|
285
300
|
self.min_length = min_length
|
|
286
301
|
self.max_length = max_length
|
|
@@ -354,6 +369,11 @@ class OutputValidator:
|
|
|
354
369
|
----------
|
|
355
370
|
rules : list[ValidationRule], optional
|
|
356
371
|
Initial validation rules.
|
|
372
|
+
|
|
373
|
+
Raises
|
|
374
|
+
------
|
|
375
|
+
ValueError
|
|
376
|
+
If rules is not a list of ValidationRule instances.
|
|
357
377
|
"""
|
|
358
378
|
self.rules = rules or []
|
|
359
379
|
|