openai-sdk-helpers 0.4.3__py3-none-any.whl → 0.5.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 +41 -7
- openai_sdk_helpers/agent/__init__.py +1 -2
- openai_sdk_helpers/agent/base.py +169 -190
- openai_sdk_helpers/agent/configuration.py +12 -20
- openai_sdk_helpers/agent/coordinator.py +14 -17
- openai_sdk_helpers/agent/runner.py +3 -45
- openai_sdk_helpers/agent/search/base.py +49 -71
- openai_sdk_helpers/agent/search/vector.py +82 -110
- openai_sdk_helpers/agent/search/web.py +103 -81
- openai_sdk_helpers/agent/summarizer.py +20 -28
- openai_sdk_helpers/agent/translator.py +17 -23
- openai_sdk_helpers/agent/validator.py +17 -23
- 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/files_api.py +1 -0
- openai_sdk_helpers/logging.py +12 -1
- 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/response/__init__.py +2 -6
- openai_sdk_helpers/response/base.py +233 -164
- openai_sdk_helpers/response/configuration.py +39 -14
- openai_sdk_helpers/response/files.py +41 -2
- 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/app.py +1 -9
- 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/utils/__init__.py +7 -0
- openai_sdk_helpers/utils/json/base_model.py +315 -32
- openai_sdk_helpers/utils/langextract.py +194 -0
- openai_sdk_helpers/vector_storage/cleanup.py +7 -2
- openai_sdk_helpers/vector_storage/storage.py +37 -7
- {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.1.dist-info}/METADATA +21 -6
- openai_sdk_helpers-0.5.1.dist-info/RECORD +95 -0
- openai_sdk_helpers/streamlit_app/streamlit_web_search.py +0 -75
- openai_sdk_helpers-0.4.3.dist-info/RECORD +0 -86
- {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.1.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.1.dist-info}/entry_points.txt +0 -0
- {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.1.dist-info}/licenses/LICENSE +0 -0
openai_sdk_helpers/tools.py
CHANGED
|
@@ -10,81 +10,207 @@ definitions from named metadata structures.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import ast
|
|
13
14
|
import asyncio
|
|
14
15
|
import inspect
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
15
18
|
import threading
|
|
16
19
|
from dataclasses import dataclass
|
|
17
|
-
from typing import Any, Callable, TypeAlias,
|
|
18
|
-
|
|
19
|
-
from pydantic import BaseModel, ValidationError
|
|
20
|
+
from typing import Any, Callable, TypeAlias, get_type_hints
|
|
20
21
|
|
|
21
|
-
from openai_sdk_helpers.response.tool_call import parse_tool_arguments
|
|
22
22
|
from openai_sdk_helpers.structure.base import StructureBase
|
|
23
|
-
from openai_sdk_helpers.utils import
|
|
24
|
-
import json
|
|
23
|
+
from openai_sdk_helpers.utils import customJSONEncoder
|
|
25
24
|
|
|
26
|
-
T = TypeVar("T", bound=BaseModel)
|
|
27
25
|
StructureType: TypeAlias = type[StructureBase]
|
|
26
|
+
ToolHandler: TypeAlias = Callable[[Any], str | Any]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ToolHandlerRegistration:
|
|
31
|
+
"""Bundle a tool handler with optional ToolSpec metadata.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
handler : ToolHandler
|
|
36
|
+
Callable that executes the tool and returns a serializable payload.
|
|
37
|
+
tool_spec : ToolSpec or None, default None
|
|
38
|
+
Optional ToolSpec used to parse tool outputs based on the tool name.
|
|
28
39
|
|
|
40
|
+
Attributes
|
|
41
|
+
----------
|
|
42
|
+
handler : ToolHandler
|
|
43
|
+
Callable that executes the tool and returns a serializable payload.
|
|
44
|
+
tool_spec : ToolSpec
|
|
45
|
+
ToolSpec describing the tool input/output structures.
|
|
46
|
+
|
|
47
|
+
Methods
|
|
48
|
+
-------
|
|
49
|
+
__init__(handler, tool_spec)
|
|
50
|
+
Initialize the registration with a handler and ToolSpec.
|
|
51
|
+
"""
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
handler: ToolHandler
|
|
54
|
+
tool_spec: ToolSpec
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
|
|
57
|
+
def _to_snake_case(name: str) -> str:
|
|
58
|
+
"""Convert a PascalCase or camelCase string to snake_case.
|
|
36
59
|
|
|
37
60
|
Parameters
|
|
38
61
|
----------
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
or any JSON-serializable type.
|
|
62
|
+
name : str
|
|
63
|
+
The name to convert.
|
|
42
64
|
|
|
43
65
|
Returns
|
|
44
66
|
-------
|
|
45
67
|
str
|
|
46
|
-
|
|
68
|
+
The snake_case version of the name.
|
|
47
69
|
|
|
48
70
|
Examples
|
|
49
71
|
--------
|
|
50
|
-
>>>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
>>> _to_snake_case("ExampleStructure")
|
|
73
|
+
'example_structure'
|
|
74
|
+
>>> _to_snake_case("MyToolName")
|
|
75
|
+
'my_tool_name'
|
|
76
|
+
"""
|
|
77
|
+
# First regex: Insert underscore before uppercase letters followed by
|
|
78
|
+
# lowercase letters (e.g., "Tool" in "ExampleTool" becomes "_Tool")
|
|
79
|
+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
80
|
+
# Second regex: Insert underscore between lowercase/digit and uppercase
|
|
81
|
+
# (e.g., "e3" followed by "T" becomes "e3_T")
|
|
82
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _unwrap_arguments(parsed: dict, tool_name: str) -> dict:
|
|
86
|
+
"""Unwrap arguments if wrapped in a single-key dict.
|
|
55
87
|
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
Some responses wrap arguments under a key matching the structure class
|
|
89
|
+
name (e.g., {"ExampleStructure": {...}}) or snake_case variant
|
|
90
|
+
(e.g., {"example_structure": {...}}). This function detects and unwraps
|
|
91
|
+
such wrappers to normalize the payload.
|
|
58
92
|
|
|
59
|
-
|
|
60
|
-
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
parsed : dict
|
|
96
|
+
The parsed arguments dictionary.
|
|
97
|
+
tool_name : str
|
|
98
|
+
The tool name, used to match potential wrapper keys.
|
|
61
99
|
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
dict
|
|
103
|
+
Unwrapped arguments dictionary, or original if no wrapper detected.
|
|
104
|
+
|
|
105
|
+
Examples
|
|
106
|
+
--------
|
|
107
|
+
>>> _unwrap_arguments({"ExampleTool": {"arg": "value"}}, "ExampleTool")
|
|
108
|
+
{'arg': 'value'}
|
|
109
|
+
>>> _unwrap_arguments({"example_tool": {"arg": "value"}}, "ExampleTool")
|
|
110
|
+
{'arg': 'value'}
|
|
111
|
+
>>> _unwrap_arguments({"arg": "value"}, "ExampleTool")
|
|
112
|
+
{'arg': 'value'}
|
|
64
113
|
"""
|
|
65
|
-
if
|
|
66
|
-
|
|
114
|
+
# Only unwrap if dict has exactly one key
|
|
115
|
+
if not isinstance(parsed, dict) or len(parsed) != 1:
|
|
116
|
+
return parsed
|
|
117
|
+
|
|
118
|
+
wrapper_key = next(iter(parsed))
|
|
119
|
+
wrapped_value = parsed[wrapper_key]
|
|
67
120
|
|
|
68
|
-
|
|
69
|
-
|
|
121
|
+
# Only unwrap if the value is also a dict
|
|
122
|
+
if not isinstance(wrapped_value, dict):
|
|
123
|
+
return parsed
|
|
124
|
+
|
|
125
|
+
# Check if wrapper key matches tool name (case-insensitive or snake_case)
|
|
126
|
+
tool_name_lower = tool_name.lower()
|
|
127
|
+
tool_name_snake = _to_snake_case(tool_name)
|
|
128
|
+
wrapper_key_lower = wrapper_key.lower()
|
|
129
|
+
|
|
130
|
+
if wrapper_key_lower in (tool_name_lower, tool_name_snake):
|
|
131
|
+
return wrapped_value
|
|
132
|
+
|
|
133
|
+
return parsed
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parse_tool_arguments(arguments: str, tool_name: str) -> dict:
|
|
137
|
+
"""Parse tool call arguments with fallback for malformed JSON.
|
|
138
|
+
|
|
139
|
+
Attempts to parse arguments as JSON first, then falls back to
|
|
140
|
+
ast.literal_eval for cases where the OpenAI API returns minor
|
|
141
|
+
formatting issues like single quotes instead of double quotes.
|
|
142
|
+
Provides clear error context including tool name and raw payload.
|
|
143
|
+
|
|
144
|
+
Also handles unwrapping of arguments that are wrapped in a single-key
|
|
145
|
+
dictionary matching the tool name (e.g., {"ExampleStructure": {...}}).
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
arguments : str
|
|
150
|
+
Raw argument string from a tool call, expected to be JSON.
|
|
151
|
+
tool_name : str
|
|
152
|
+
Tool name for improved error context (required).
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
dict
|
|
157
|
+
Parsed dictionary of tool arguments, with wrapper unwrapped if present.
|
|
158
|
+
|
|
159
|
+
Raises
|
|
160
|
+
------
|
|
161
|
+
ValueError
|
|
162
|
+
If the arguments cannot be parsed as valid JSON or Python literal.
|
|
163
|
+
Error message includes tool name and payload excerpt for debugging.
|
|
164
|
+
|
|
165
|
+
Examples
|
|
166
|
+
--------
|
|
167
|
+
>>> _parse_tool_arguments('{"key": "value"}', tool_name="search")
|
|
168
|
+
{'key': 'value'}
|
|
169
|
+
|
|
170
|
+
>>> _parse_tool_arguments("{'key': 'value'}", tool_name="search")
|
|
171
|
+
{'key': 'value'}
|
|
172
|
+
|
|
173
|
+
>>> _parse_tool_arguments('{"ExampleTool": {"arg": "value"}}', "ExampleTool")
|
|
174
|
+
{'arg': 'value'}
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
parsed = json.loads(arguments)
|
|
178
|
+
except json.JSONDecodeError:
|
|
179
|
+
try:
|
|
180
|
+
parsed = ast.literal_eval(arguments)
|
|
181
|
+
except Exception as exc: # noqa: BLE001
|
|
182
|
+
# Build informative error message with context
|
|
183
|
+
payload_preview = (
|
|
184
|
+
arguments[:100] + "..." if len(arguments) > 100 else arguments
|
|
185
|
+
)
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"Failed to parse tool arguments for tool '{tool_name}'. "
|
|
188
|
+
f"Raw payload: {payload_preview}"
|
|
189
|
+
) from exc
|
|
190
|
+
|
|
191
|
+
# Unwrap if wrapped in a single-key dict matching tool name
|
|
192
|
+
return _unwrap_arguments(parsed, tool_name)
|
|
70
193
|
|
|
71
194
|
|
|
72
195
|
def tool_handler_factory(
|
|
73
196
|
func: Callable[..., Any],
|
|
74
197
|
*,
|
|
75
|
-
|
|
198
|
+
tool_spec: "ToolSpec",
|
|
76
199
|
) -> Callable[[Any], str]:
|
|
77
200
|
"""Create a generic tool handler that parses, validates, and serializes.
|
|
78
201
|
|
|
79
|
-
Wraps a tool function with automatic argument parsing,
|
|
202
|
+
Wraps a tool function with automatic argument parsing, structured
|
|
80
203
|
validation, execution, and result serialization. This eliminates
|
|
81
204
|
repetitive boilerplate for tool implementations.
|
|
82
205
|
|
|
83
206
|
The returned handler:
|
|
84
|
-
1. Parses tool_call.arguments using
|
|
85
|
-
2. Validates arguments with
|
|
86
|
-
3. Calls func with
|
|
87
|
-
4. Serializes the result using serialize_tool_result
|
|
207
|
+
1. Parses tool_call.arguments using ToolSpec.unserialize_tool_arguments
|
|
208
|
+
2. Validates arguments with the input structure
|
|
209
|
+
3. Calls func with structured input (handles both sync and async)
|
|
210
|
+
4. Serializes the result using ToolSpec.serialize_tool_result
|
|
211
|
+
|
|
212
|
+
Forward-referenced annotations on single-argument tool callables are
|
|
213
|
+
resolved when possible to decide whether to pass the structured input.
|
|
88
214
|
|
|
89
215
|
Parameters
|
|
90
216
|
----------
|
|
@@ -92,10 +218,10 @@ def tool_handler_factory(
|
|
|
92
218
|
The actual tool implementation function. Should accept keyword
|
|
93
219
|
arguments matching the tool's parameter schema. Can be synchronous
|
|
94
220
|
or asynchronous.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
221
|
+
tool_spec : ToolSpec
|
|
222
|
+
Tool specification describing input and output structures. When
|
|
223
|
+
provided, input parsing uses the input structure and output
|
|
224
|
+
serialization uses the output structure.
|
|
99
225
|
|
|
100
226
|
Returns
|
|
101
227
|
-------
|
|
@@ -105,34 +231,42 @@ def tool_handler_factory(
|
|
|
105
231
|
|
|
106
232
|
Raises
|
|
107
233
|
------
|
|
108
|
-
ValidationError
|
|
109
|
-
If input_model is provided and validation fails.
|
|
110
234
|
ValueError
|
|
111
235
|
If argument parsing fails.
|
|
236
|
+
ValidationError
|
|
237
|
+
If input validation fails.
|
|
112
238
|
|
|
113
239
|
Examples
|
|
114
240
|
--------
|
|
115
|
-
Basic usage
|
|
116
|
-
|
|
117
|
-
>>> def search_tool(query: str, limit: int = 10):
|
|
118
|
-
... return {"results": [f"Result for {query}"]}
|
|
119
|
-
>>> handler = tool_handler_factory(search_tool)
|
|
241
|
+
Basic usage with ToolSpec:
|
|
120
242
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
>>>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
...
|
|
127
|
-
|
|
128
|
-
...
|
|
129
|
-
|
|
243
|
+
>>> from openai_sdk_helpers import ToolSpec
|
|
244
|
+
>>> from openai_sdk_helpers.structure import PromptStructure
|
|
245
|
+
>>> def search_tool(prompt: PromptStructure):
|
|
246
|
+
... return {"prompt": prompt.prompt}
|
|
247
|
+
>>> handler = tool_handler_factory(
|
|
248
|
+
... search_tool,
|
|
249
|
+
... tool_spec=ToolSpec(
|
|
250
|
+
... tool_name="search",
|
|
251
|
+
... tool_description="Run a search query",
|
|
252
|
+
... input_structure=PromptStructure,
|
|
253
|
+
... output_structure=PromptStructure,
|
|
254
|
+
... ),
|
|
255
|
+
... )
|
|
130
256
|
|
|
131
257
|
With async function:
|
|
132
258
|
|
|
133
|
-
>>> async def async_search_tool(
|
|
134
|
-
... return {"
|
|
135
|
-
>>> handler = tool_handler_factory(
|
|
259
|
+
>>> async def async_search_tool(prompt: PromptStructure):
|
|
260
|
+
... return {"prompt": prompt.prompt}
|
|
261
|
+
>>> handler = tool_handler_factory(
|
|
262
|
+
... async_search_tool,
|
|
263
|
+
... tool_spec=ToolSpec(
|
|
264
|
+
... tool_name="async_search",
|
|
265
|
+
... tool_description="Run an async search query",
|
|
266
|
+
... input_structure=PromptStructure,
|
|
267
|
+
... output_structure=PromptStructure,
|
|
268
|
+
... ),
|
|
269
|
+
... )
|
|
136
270
|
|
|
137
271
|
The handler can then be used with OpenAI tool calls:
|
|
138
272
|
|
|
@@ -145,6 +279,29 @@ def tool_handler_factory(
|
|
|
145
279
|
"""
|
|
146
280
|
is_async = inspect.iscoroutinefunction(func)
|
|
147
281
|
|
|
282
|
+
def _call_with_input(validated_input: StructureBase) -> Any:
|
|
283
|
+
signature = inspect.signature(func)
|
|
284
|
+
params = list(signature.parameters.values())
|
|
285
|
+
if len(params) == 1:
|
|
286
|
+
param = params[0]
|
|
287
|
+
try:
|
|
288
|
+
type_hints = get_type_hints(func)
|
|
289
|
+
except (NameError, TypeError):
|
|
290
|
+
type_hints = {}
|
|
291
|
+
annotated_type = type_hints.get(param.name, param.annotation)
|
|
292
|
+
if isinstance(annotated_type, str):
|
|
293
|
+
if (
|
|
294
|
+
annotated_type == tool_spec.input_structure.__name__
|
|
295
|
+
or annotated_type.endswith(f".{tool_spec.input_structure.__name__}")
|
|
296
|
+
):
|
|
297
|
+
return func(validated_input)
|
|
298
|
+
if annotated_type is tool_spec.input_structure or (
|
|
299
|
+
inspect.isclass(annotated_type)
|
|
300
|
+
and issubclass(annotated_type, StructureBase)
|
|
301
|
+
):
|
|
302
|
+
return func(validated_input)
|
|
303
|
+
return func(**validated_input.model_dump())
|
|
304
|
+
|
|
148
305
|
def handler(tool_call: Any) -> str:
|
|
149
306
|
"""Handle tool execution with parsing, validation, and serialization.
|
|
150
307
|
|
|
@@ -163,21 +320,9 @@ def tool_handler_factory(
|
|
|
163
320
|
ValueError
|
|
164
321
|
If argument parsing fails.
|
|
165
322
|
ValidationError
|
|
166
|
-
If
|
|
323
|
+
If input validation fails.
|
|
167
324
|
"""
|
|
168
|
-
|
|
169
|
-
tool_name = getattr(tool_call, "name", "unknown")
|
|
170
|
-
|
|
171
|
-
# Parse arguments with error context
|
|
172
|
-
parsed_args = parse_tool_arguments(tool_call.arguments, tool_name=tool_name)
|
|
173
|
-
|
|
174
|
-
# Validate with Pydantic if model provided
|
|
175
|
-
if input_model is not None:
|
|
176
|
-
validated_input = input_model(**parsed_args)
|
|
177
|
-
# Convert back to dict for function call
|
|
178
|
-
call_kwargs = validated_input.model_dump()
|
|
179
|
-
else:
|
|
180
|
-
call_kwargs = parsed_args
|
|
325
|
+
validated_input = tool_spec.unserialize_arguments(tool_call)
|
|
181
326
|
|
|
182
327
|
# Execute function (sync or async with event loop detection)
|
|
183
328
|
if is_async:
|
|
@@ -189,7 +334,9 @@ def tool_handler_factory(
|
|
|
189
334
|
|
|
190
335
|
def _thread_func() -> None:
|
|
191
336
|
try:
|
|
192
|
-
result_holder["value"] = asyncio.run(
|
|
337
|
+
result_holder["value"] = asyncio.run(
|
|
338
|
+
_call_with_input(validated_input)
|
|
339
|
+
)
|
|
193
340
|
except Exception as exc:
|
|
194
341
|
result_holder["exception"] = exc
|
|
195
342
|
|
|
@@ -202,12 +349,12 @@ def tool_handler_factory(
|
|
|
202
349
|
result = result_holder["value"]
|
|
203
350
|
except RuntimeError:
|
|
204
351
|
# No event loop running, can use asyncio.run directly
|
|
205
|
-
result = asyncio.run(
|
|
352
|
+
result = asyncio.run(_call_with_input(validated_input))
|
|
206
353
|
else:
|
|
207
|
-
result =
|
|
354
|
+
result = _call_with_input(validated_input)
|
|
208
355
|
|
|
209
356
|
# Serialize result
|
|
210
|
-
return
|
|
357
|
+
return tool_spec.serialize_tool_results(result)
|
|
211
358
|
|
|
212
359
|
return handler
|
|
213
360
|
|
|
@@ -225,15 +372,15 @@ class ToolSpec:
|
|
|
225
372
|
|
|
226
373
|
Attributes
|
|
227
374
|
----------
|
|
228
|
-
structure : StructureType
|
|
229
|
-
The StructureBase class that defines the tool's input parameter schema.
|
|
230
|
-
Used to generate the OpenAI tool definition.
|
|
231
375
|
tool_name : str
|
|
232
376
|
Name identifier for the tool.
|
|
233
377
|
tool_description : str
|
|
234
378
|
Human-readable description of what the tool does.
|
|
235
|
-
|
|
236
|
-
|
|
379
|
+
input_structure : StructureType
|
|
380
|
+
The StructureBase class that defines the tool's input parameter schema.
|
|
381
|
+
Used to generate the OpenAI tool definition.
|
|
382
|
+
output_structure : StructureType
|
|
383
|
+
StructureBase class that defines the tool's output schema.
|
|
237
384
|
This is for documentation/reference only and is not sent to OpenAI.
|
|
238
385
|
Useful when a tool accepts one type of input but returns a different
|
|
239
386
|
structured output.
|
|
@@ -245,29 +392,126 @@ class ToolSpec:
|
|
|
245
392
|
>>> from openai_sdk_helpers import ToolSpec
|
|
246
393
|
>>> from openai_sdk_helpers.structure import PromptStructure
|
|
247
394
|
>>> spec = ToolSpec(
|
|
248
|
-
... structure=PromptStructure,
|
|
249
395
|
... tool_name="web_agent",
|
|
250
|
-
... tool_description="Run a web research workflow"
|
|
396
|
+
... tool_description="Run a web research workflow",
|
|
397
|
+
... input_structure=PromptStructure,
|
|
398
|
+
... output_structure=PromptStructure
|
|
251
399
|
... )
|
|
252
400
|
|
|
253
401
|
Define a tool with different input and output structures:
|
|
254
402
|
|
|
255
403
|
>>> from openai_sdk_helpers.structure import PromptStructure, SummaryStructure
|
|
256
404
|
>>> spec = ToolSpec(
|
|
257
|
-
... structure=PromptStructure,
|
|
258
405
|
... tool_name="summarizer",
|
|
259
406
|
... tool_description="Summarize the provided prompt",
|
|
407
|
+
... input_structure=PromptStructure,
|
|
260
408
|
... output_structure=SummaryStructure
|
|
261
409
|
... )
|
|
262
410
|
"""
|
|
263
411
|
|
|
264
|
-
structure: StructureType
|
|
265
412
|
tool_name: str
|
|
266
|
-
tool_description: str
|
|
267
|
-
|
|
413
|
+
tool_description: str | None
|
|
414
|
+
input_structure: StructureType
|
|
415
|
+
output_structure: StructureType
|
|
416
|
+
|
|
417
|
+
def __post_init__(self) -> None:
|
|
418
|
+
"""Validate required ToolSpec fields."""
|
|
419
|
+
if self.output_structure is None:
|
|
420
|
+
raise ValueError("ToolSpec.output_structure must be set.")
|
|
421
|
+
|
|
422
|
+
def serialize_tool_results(self, tool_results: Any) -> str:
|
|
423
|
+
"""Serialize tool results into a standardized JSON string.
|
|
424
|
+
|
|
425
|
+
Handles structured outputs with consistent JSON formatting. Outputs are
|
|
426
|
+
validated and serialized through the ToolSpec output structure.
|
|
427
|
+
|
|
428
|
+
Parameters
|
|
429
|
+
----------
|
|
430
|
+
result : Any
|
|
431
|
+
Tool result to serialize. Can be a structure instance or a compatible
|
|
432
|
+
mapping for validation.
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
str
|
|
437
|
+
JSON-formatted string representation of the result.
|
|
438
|
+
|
|
439
|
+
Examples
|
|
440
|
+
--------
|
|
441
|
+
>>> from openai_sdk_helpers import ToolSpec
|
|
442
|
+
>>> from openai_sdk_helpers.structure import PromptStructure
|
|
443
|
+
>>> spec = ToolSpec(
|
|
444
|
+
... tool_name="echo",
|
|
445
|
+
... tool_description="Echo a prompt",
|
|
446
|
+
... input_structure=PromptStructure,
|
|
447
|
+
... output_structure=PromptStructure,
|
|
448
|
+
... )
|
|
449
|
+
>>> spec.serialize_tool_result({"prompt": "hello"})
|
|
450
|
+
'{"prompt": "hello"}'
|
|
451
|
+
"""
|
|
452
|
+
output_structure = self.output_structure
|
|
453
|
+
payload = output_structure.model_validate(tool_results).to_json()
|
|
454
|
+
return json.dumps(payload, cls=customJSONEncoder)
|
|
455
|
+
|
|
456
|
+
def unserialize_arguments(self, tool_call: Any) -> StructureBase:
|
|
457
|
+
"""Unserialize tool call arguments into a structured input instance.
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
tool_call : Any
|
|
462
|
+
Tool call object with 'arguments' and 'name' attributes.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
StructureBase
|
|
467
|
+
Validated input structure instance.
|
|
468
|
+
|
|
469
|
+
Raises
|
|
470
|
+
------
|
|
471
|
+
ValueError
|
|
472
|
+
If argument parsing fails.
|
|
473
|
+
ValidationError
|
|
474
|
+
If input validation fails.
|
|
475
|
+
"""
|
|
476
|
+
tool_name = getattr(tool_call, "name", self.tool_name)
|
|
477
|
+
parsed_args = _parse_tool_arguments(tool_call.arguments, tool_name=tool_name)
|
|
478
|
+
return self.input_structure.from_json(parsed_args)
|
|
479
|
+
|
|
480
|
+
def as_tool_definition(self) -> dict:
|
|
481
|
+
"""Generate OpenAI-compatible tool definition from the ToolSpec.
|
|
482
|
+
|
|
483
|
+
Uses the input structure to create a tool definition dictionary
|
|
484
|
+
suitable for inclusion in OpenAI API calls.
|
|
485
|
+
|
|
486
|
+
Returns
|
|
487
|
+
-------
|
|
488
|
+
dict
|
|
489
|
+
Tool definition dictionary.
|
|
490
|
+
|
|
491
|
+
Examples
|
|
492
|
+
--------
|
|
493
|
+
>>> from openai_sdk_helpers import ToolSpec
|
|
494
|
+
>>> from openai_sdk_helpers.structure import PromptStructure
|
|
495
|
+
>>> spec = ToolSpec(
|
|
496
|
+
... tool_name="web_agent",
|
|
497
|
+
... tool_description="Run a web research workflow",
|
|
498
|
+
... input_structure=PromptStructure,
|
|
499
|
+
... output_structure=PromptStructure
|
|
500
|
+
... )
|
|
501
|
+
>>> spec.as_tool_definition()
|
|
502
|
+
{
|
|
503
|
+
"name": "web_agent",
|
|
504
|
+
"description": "Run a web research workflow",
|
|
505
|
+
"parameters": { ... } # Schema from PromptStructure
|
|
506
|
+
}
|
|
507
|
+
"""
|
|
508
|
+
return self.input_structure.response_tool_definition(
|
|
509
|
+
tool_name=self.tool_name,
|
|
510
|
+
tool_description=self.tool_description,
|
|
511
|
+
)
|
|
268
512
|
|
|
269
513
|
|
|
270
|
-
def
|
|
514
|
+
def build_tool_definition_list(tool_specs: list[ToolSpec]) -> list[dict]:
|
|
271
515
|
"""Build tool definitions from named tool specs.
|
|
272
516
|
|
|
273
517
|
Converts a list of ToolSpec objects into OpenAI-compatible tool
|
|
@@ -289,23 +533,25 @@ def build_tool_definitions(tool_specs: list[ToolSpec]) -> list[dict]:
|
|
|
289
533
|
--------
|
|
290
534
|
Build multiple tool definitions:
|
|
291
535
|
|
|
292
|
-
>>> from openai_sdk_helpers import ToolSpec,
|
|
536
|
+
>>> from openai_sdk_helpers import ToolSpec, build_tool_definition_list
|
|
293
537
|
>>> from openai_sdk_helpers.structure import PromptStructure
|
|
294
|
-
>>> tools =
|
|
538
|
+
>>> tools = build_tool_definition_list([
|
|
295
539
|
... ToolSpec(
|
|
296
|
-
... structure=PromptStructure,
|
|
297
540
|
... tool_name="web_agent",
|
|
298
|
-
... tool_description="Run a web research workflow"
|
|
541
|
+
... tool_description="Run a web research workflow",
|
|
542
|
+
... input_structure=PromptStructure,
|
|
543
|
+
... output_structure=PromptStructure
|
|
299
544
|
... ),
|
|
300
545
|
... ToolSpec(
|
|
301
|
-
... structure=PromptStructure,
|
|
302
546
|
... tool_name="vector_agent",
|
|
303
|
-
... tool_description="Run a vector search workflow"
|
|
547
|
+
... tool_description="Run a vector search workflow",
|
|
548
|
+
... input_structure=PromptStructure,
|
|
549
|
+
... output_structure=PromptStructure
|
|
304
550
|
... ),
|
|
305
551
|
... ])
|
|
306
552
|
"""
|
|
307
553
|
return [
|
|
308
|
-
spec.
|
|
554
|
+
spec.input_structure.response_tool_definition(
|
|
309
555
|
tool_name=spec.tool_name,
|
|
310
556
|
tool_description=spec.tool_description,
|
|
311
557
|
)
|
|
@@ -314,9 +560,10 @@ def build_tool_definitions(tool_specs: list[ToolSpec]) -> list[dict]:
|
|
|
314
560
|
|
|
315
561
|
|
|
316
562
|
__all__ = [
|
|
317
|
-
"serialize_tool_result",
|
|
318
563
|
"tool_handler_factory",
|
|
319
564
|
"StructureType",
|
|
565
|
+
"ToolHandler",
|
|
566
|
+
"ToolHandlerRegistration",
|
|
320
567
|
"ToolSpec",
|
|
321
|
-
"
|
|
568
|
+
"build_tool_definition_list",
|
|
322
569
|
]
|
|
@@ -87,6 +87,10 @@ from .encoding import (
|
|
|
87
87
|
get_mime_type,
|
|
88
88
|
is_image_file,
|
|
89
89
|
)
|
|
90
|
+
from .langextract import (
|
|
91
|
+
LangExtractAdapter,
|
|
92
|
+
build_langextract_adapter,
|
|
93
|
+
)
|
|
90
94
|
|
|
91
95
|
__all__ = [
|
|
92
96
|
"ensure_list",
|
|
@@ -130,6 +134,9 @@ __all__ = [
|
|
|
130
134
|
"create_image_data_url",
|
|
131
135
|
"create_file_data_url",
|
|
132
136
|
"is_image_file",
|
|
137
|
+
# LangExtract
|
|
138
|
+
"LangExtractAdapter",
|
|
139
|
+
"build_langextract_adapter",
|
|
133
140
|
# Registry
|
|
134
141
|
"RegistryBase",
|
|
135
142
|
]
|