openai-sdk-helpers 0.0.9__py3-none-any.whl → 0.1.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 +63 -5
- openai_sdk_helpers/agent/base.py +5 -1
- openai_sdk_helpers/agent/coordination.py +4 -5
- openai_sdk_helpers/agent/runner.py +4 -1
- openai_sdk_helpers/agent/search/base.py +1 -0
- openai_sdk_helpers/agent/search/vector.py +2 -0
- openai_sdk_helpers/cli.py +265 -0
- openai_sdk_helpers/config.py +120 -31
- openai_sdk_helpers/context_manager.py +1 -1
- openai_sdk_helpers/deprecation.py +167 -0
- openai_sdk_helpers/environment.py +3 -2
- openai_sdk_helpers/errors.py +0 -12
- openai_sdk_helpers/logging_config.py +24 -95
- openai_sdk_helpers/prompt/base.py +56 -6
- openai_sdk_helpers/response/__init__.py +5 -2
- openai_sdk_helpers/response/base.py +84 -115
- openai_sdk_helpers/response/config.py +142 -0
- openai_sdk_helpers/response/messages.py +1 -0
- openai_sdk_helpers/response/tool_call.py +15 -4
- openai_sdk_helpers/retry.py +1 -1
- openai_sdk_helpers/streamlit_app/app.py +14 -3
- openai_sdk_helpers/streamlit_app/streamlit_web_search.py +15 -8
- openai_sdk_helpers/structure/__init__.py +3 -0
- openai_sdk_helpers/structure/base.py +6 -6
- openai_sdk_helpers/structure/plan/__init__.py +15 -1
- openai_sdk_helpers/structure/plan/helpers.py +173 -0
- openai_sdk_helpers/structure/plan/plan.py +13 -9
- openai_sdk_helpers/structure/plan/task.py +7 -7
- openai_sdk_helpers/structure/plan/types.py +15 -0
- openai_sdk_helpers/tools.py +296 -0
- openai_sdk_helpers/utils/__init__.py +82 -31
- openai_sdk_helpers/{async_utils.py → utils/async_utils.py} +5 -6
- openai_sdk_helpers/utils/coercion.py +138 -0
- openai_sdk_helpers/utils/deprecation.py +167 -0
- openai_sdk_helpers/utils/json_utils.py +98 -0
- openai_sdk_helpers/utils/output_validation.py +448 -0
- openai_sdk_helpers/utils/path_utils.py +46 -0
- openai_sdk_helpers/{validation.py → utils/validation.py} +7 -3
- openai_sdk_helpers/vector_storage/storage.py +9 -6
- {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.1.dist-info}/METADATA +59 -3
- openai_sdk_helpers-0.1.1.dist-info/RECORD +76 -0
- openai_sdk_helpers-0.1.1.dist-info/entry_points.txt +2 -0
- openai_sdk_helpers/utils/core.py +0 -468
- openai_sdk_helpers-0.0.9.dist-info/RECORD +0 -66
- {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.1.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -233,7 +233,7 @@ class BaseStructure(BaseModel):
|
|
|
233
233
|
return prompt_lines
|
|
234
234
|
|
|
235
235
|
@classmethod
|
|
236
|
-
def assistant_tool_definition(cls, name: str, description: str) -> dict:
|
|
236
|
+
def assistant_tool_definition(cls, name: str, *, description: str) -> dict:
|
|
237
237
|
"""Build an Assistant API function tool definition for this structure.
|
|
238
238
|
|
|
239
239
|
Creates a tool definition compatible with the OpenAI Assistant API,
|
|
@@ -255,7 +255,7 @@ class BaseStructure(BaseModel):
|
|
|
255
255
|
--------
|
|
256
256
|
>>> tool = MyStructure.assistant_tool_definition(
|
|
257
257
|
... "analyze_data",
|
|
258
|
-
... "Analyze the provided data"
|
|
258
|
+
... description="Analyze the provided data"
|
|
259
259
|
... )
|
|
260
260
|
"""
|
|
261
261
|
from .responses import assistant_tool_definition
|
|
@@ -283,7 +283,7 @@ class BaseStructure(BaseModel):
|
|
|
283
283
|
return assistant_format(cls)
|
|
284
284
|
|
|
285
285
|
@classmethod
|
|
286
|
-
def response_tool_definition(cls, tool_name: str, tool_description: str) -> dict:
|
|
286
|
+
def response_tool_definition(cls, tool_name: str, *, tool_description: str) -> dict:
|
|
287
287
|
"""Build a chat completion tool definition for this structure.
|
|
288
288
|
|
|
289
289
|
Creates a function tool definition compatible with the chat
|
|
@@ -305,7 +305,7 @@ class BaseStructure(BaseModel):
|
|
|
305
305
|
--------
|
|
306
306
|
>>> tool = MyStructure.response_tool_definition(
|
|
307
307
|
... "process_data",
|
|
308
|
-
... "Process the input data"
|
|
308
|
+
... tool_description="Process the input data"
|
|
309
309
|
... )
|
|
310
310
|
"""
|
|
311
311
|
from .responses import response_tool_definition
|
|
@@ -725,7 +725,7 @@ class BaseStructure(BaseModel):
|
|
|
725
725
|
return cls.from_raw_input(structured_data)
|
|
726
726
|
|
|
727
727
|
@staticmethod
|
|
728
|
-
def format_output(label: str, value: Any) -> str:
|
|
728
|
+
def format_output(label: str, *, value: Any) -> str:
|
|
729
729
|
"""
|
|
730
730
|
Format a label and value for string output.
|
|
731
731
|
|
|
@@ -772,7 +772,7 @@ class BaseStructure(BaseModel):
|
|
|
772
772
|
"""
|
|
773
773
|
return "\n".join(
|
|
774
774
|
[
|
|
775
|
-
BaseStructure.format_output(field, value)
|
|
775
|
+
BaseStructure.format_output(field, value=value)
|
|
776
776
|
for field, value in self.model_dump().items()
|
|
777
777
|
]
|
|
778
778
|
)
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This package provides Pydantic models for representing agent execution plans,
|
|
4
4
|
including task definitions, agent type enumerations, and plan structures with
|
|
5
|
-
sequential execution support.
|
|
5
|
+
sequential execution support. Also includes helper functions for creating and
|
|
6
|
+
executing plans.
|
|
6
7
|
|
|
7
8
|
Classes
|
|
8
9
|
-------
|
|
@@ -12,6 +13,15 @@ TaskStructure
|
|
|
12
13
|
Individual agent task with status tracking and results.
|
|
13
14
|
AgentEnum
|
|
14
15
|
Enumeration of available agent types.
|
|
16
|
+
|
|
17
|
+
Functions
|
|
18
|
+
---------
|
|
19
|
+
create_plan
|
|
20
|
+
Create a PlanStructure from a sequence of tasks.
|
|
21
|
+
execute_task
|
|
22
|
+
Execute a single task with an agent callable.
|
|
23
|
+
execute_plan
|
|
24
|
+
Execute a complete plan using registered agent callables.
|
|
15
25
|
"""
|
|
16
26
|
|
|
17
27
|
from __future__ import annotations
|
|
@@ -19,9 +29,13 @@ from __future__ import annotations
|
|
|
19
29
|
from .plan import PlanStructure
|
|
20
30
|
from .task import TaskStructure
|
|
21
31
|
from .enum import AgentEnum
|
|
32
|
+
from .helpers import create_plan, execute_task, execute_plan
|
|
22
33
|
|
|
23
34
|
__all__ = [
|
|
24
35
|
"PlanStructure",
|
|
25
36
|
"TaskStructure",
|
|
26
37
|
"AgentEnum",
|
|
38
|
+
"create_plan",
|
|
39
|
+
"execute_task",
|
|
40
|
+
"execute_plan",
|
|
27
41
|
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Helper functions for creating and executing agent plans.
|
|
2
|
+
|
|
3
|
+
This module provides convenience functions for working with PlanStructure
|
|
4
|
+
and TaskStructure, simplifying common workflows like plan creation, task
|
|
5
|
+
execution, and result aggregation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .enum import AgentEnum
|
|
11
|
+
from .plan import PlanStructure
|
|
12
|
+
from .task import TaskStructure
|
|
13
|
+
from .types import AgentCallable, AgentRegistry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_plan(*tasks: TaskStructure) -> PlanStructure:
|
|
17
|
+
"""Create a PlanStructure from a sequence of tasks.
|
|
18
|
+
|
|
19
|
+
Convenience factory function that constructs a plan from individual
|
|
20
|
+
tasks. Tasks are executed in the order they are provided.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
*tasks : TaskStructure
|
|
25
|
+
Variable number of task definitions to include in the plan.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
PlanStructure
|
|
30
|
+
New plan containing the provided tasks in order.
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> task1 = TaskStructure(
|
|
35
|
+
... task_type=AgentEnum.WEB_SEARCH,
|
|
36
|
+
... prompt="Search for AI trends"
|
|
37
|
+
... )
|
|
38
|
+
>>> task2 = TaskStructure(
|
|
39
|
+
... task_type=AgentEnum.SUMMARIZER,
|
|
40
|
+
... prompt="Summarize findings"
|
|
41
|
+
... )
|
|
42
|
+
>>> plan = create_plan(task1, task2)
|
|
43
|
+
>>> len(plan)
|
|
44
|
+
2
|
|
45
|
+
"""
|
|
46
|
+
return PlanStructure(tasks=list(tasks))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def execute_task(
|
|
50
|
+
task: TaskStructure,
|
|
51
|
+
agent_callable: AgentCallable,
|
|
52
|
+
) -> list[str]:
|
|
53
|
+
"""Execute a single task with an agent callable.
|
|
54
|
+
|
|
55
|
+
Runs one task using the provided agent function. Updates task status,
|
|
56
|
+
timing, and results. Context from previous tasks is not supported in this
|
|
57
|
+
helper - use execute_plan() for multi-task execution with context passing.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
task : TaskStructure
|
|
62
|
+
Task definition containing prompt and metadata.
|
|
63
|
+
agent_callable : AgentCallable
|
|
64
|
+
Synchronous or asynchronous callable responsible for executing the task.
|
|
65
|
+
Should accept the task prompt and an optional context keyword argument.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
list[str]
|
|
70
|
+
Normalized string results from task execution.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
------
|
|
74
|
+
Exception
|
|
75
|
+
Any exception raised by the agent_callable is propagated after
|
|
76
|
+
task status is updated.
|
|
77
|
+
|
|
78
|
+
Examples
|
|
79
|
+
--------
|
|
80
|
+
>>> def agent_fn(prompt, context=None):
|
|
81
|
+
... return f"Result for {prompt}"
|
|
82
|
+
>>> task = TaskStructure(prompt="Test task")
|
|
83
|
+
>>> results = execute_task(task, agent_fn)
|
|
84
|
+
>>> task.status
|
|
85
|
+
'done'
|
|
86
|
+
"""
|
|
87
|
+
from datetime import datetime, timezone
|
|
88
|
+
|
|
89
|
+
task.start_date = datetime.now(timezone.utc)
|
|
90
|
+
task.status = "running"
|
|
91
|
+
|
|
92
|
+
# Build plan with single task and execute
|
|
93
|
+
# Normalize task_type to string value for registry key to match PlanStructure.execute lookup
|
|
94
|
+
plan = PlanStructure(tasks=[task])
|
|
95
|
+
# Convert AgentEnum to its string value for registry key
|
|
96
|
+
registry_key = (
|
|
97
|
+
task.task_type.value
|
|
98
|
+
if isinstance(task.task_type, AgentEnum)
|
|
99
|
+
else task.task_type
|
|
100
|
+
)
|
|
101
|
+
registry: dict[str, AgentCallable] = {
|
|
102
|
+
registry_key: agent_callable,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Execute the plan - it will update task status
|
|
106
|
+
aggregated = plan.execute(
|
|
107
|
+
agent_registry=registry,
|
|
108
|
+
halt_on_error=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# If task failed, raise the exception
|
|
112
|
+
if task.status == "error":
|
|
113
|
+
# Extract error message from results
|
|
114
|
+
error_msg = task.results[0] if task.results else "Task execution failed"
|
|
115
|
+
# Raise RuntimeError with the error message
|
|
116
|
+
# The original exception type information is lost but the message is preserved
|
|
117
|
+
raise RuntimeError(f"Task execution error: {error_msg}")
|
|
118
|
+
|
|
119
|
+
return aggregated
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def execute_plan(
|
|
123
|
+
plan: PlanStructure,
|
|
124
|
+
agent_registry: AgentRegistry,
|
|
125
|
+
*,
|
|
126
|
+
halt_on_error: bool = True,
|
|
127
|
+
) -> list[str]:
|
|
128
|
+
"""Execute a plan using registered agent callables.
|
|
129
|
+
|
|
130
|
+
Convenience wrapper around PlanStructure.execute() for cleaner syntax.
|
|
131
|
+
Runs all tasks in sequence, passing results between tasks as context.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
plan : PlanStructure
|
|
136
|
+
Plan containing ordered tasks to execute.
|
|
137
|
+
agent_registry : AgentRegistry
|
|
138
|
+
Lookup of agent identifiers to callables. Keys may be AgentEnum
|
|
139
|
+
instances or their string values.
|
|
140
|
+
halt_on_error : bool, default True
|
|
141
|
+
Whether execution should stop when a task raises an exception.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
list[str]
|
|
146
|
+
Flattened list of normalized outputs from all executed tasks.
|
|
147
|
+
|
|
148
|
+
Raises
|
|
149
|
+
------
|
|
150
|
+
KeyError
|
|
151
|
+
If a task references an agent not in the registry.
|
|
152
|
+
|
|
153
|
+
Examples
|
|
154
|
+
--------
|
|
155
|
+
>>> def search_agent(prompt, context=None):
|
|
156
|
+
... return ["search results"]
|
|
157
|
+
>>> def summary_agent(prompt, context=None):
|
|
158
|
+
... return ["summary"]
|
|
159
|
+
>>> registry = {
|
|
160
|
+
... AgentEnum.WEB_SEARCH: search_agent,
|
|
161
|
+
... AgentEnum.SUMMARIZER: summary_agent,
|
|
162
|
+
... }
|
|
163
|
+
>>> plan = PlanStructure(tasks=[...]) # doctest: +SKIP
|
|
164
|
+
>>> results = execute_plan(plan, registry) # doctest: +SKIP
|
|
165
|
+
"""
|
|
166
|
+
return plan.execute(agent_registry, halt_on_error=halt_on_error)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
__all__ = [
|
|
170
|
+
"create_plan",
|
|
171
|
+
"execute_task",
|
|
172
|
+
"execute_plan",
|
|
173
|
+
]
|
|
@@ -10,12 +10,13 @@ import asyncio
|
|
|
10
10
|
import inspect
|
|
11
11
|
import threading
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
|
-
from typing import Any, Awaitable,
|
|
13
|
+
from typing import Any, Awaitable, Coroutine, cast
|
|
14
14
|
from collections.abc import Mapping
|
|
15
15
|
|
|
16
16
|
from .enum import AgentEnum
|
|
17
17
|
from ..base import BaseStructure, spec_field
|
|
18
18
|
from .task import TaskStructure
|
|
19
|
+
from .types import AgentCallable, AgentRegistry
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class PlanStructure(BaseStructure):
|
|
@@ -108,9 +109,7 @@ class PlanStructure(BaseStructure):
|
|
|
108
109
|
|
|
109
110
|
def execute(
|
|
110
111
|
self,
|
|
111
|
-
agent_registry:
|
|
112
|
-
AgentEnum | str, Callable[..., object | Coroutine[Any, Any, object]]
|
|
113
|
-
],
|
|
112
|
+
agent_registry: AgentRegistry,
|
|
114
113
|
*,
|
|
115
114
|
halt_on_error: bool = True,
|
|
116
115
|
) -> list[str]:
|
|
@@ -121,7 +120,7 @@ class PlanStructure(BaseStructure):
|
|
|
121
120
|
|
|
122
121
|
Parameters
|
|
123
122
|
----------
|
|
124
|
-
agent_registry :
|
|
123
|
+
agent_registry : AgentRegistry
|
|
125
124
|
Lookup of agent identifiers to callables. Keys may be AgentEnum
|
|
126
125
|
instances or their string values. Each callable receives the task
|
|
127
126
|
prompt (augmented with prior context) and an optional context
|
|
@@ -147,13 +146,18 @@ class PlanStructure(BaseStructure):
|
|
|
147
146
|
>>> plan = PlanStructure(tasks=[TaskStructure(prompt="Test")])
|
|
148
147
|
>>> results = plan.execute(registry) # doctest: +SKIP
|
|
149
148
|
"""
|
|
149
|
+
normalized_registry: dict[str, AgentCallable] = {
|
|
150
|
+
self._resolve_registry_key(key): value
|
|
151
|
+
for key, value in agent_registry.items()
|
|
152
|
+
}
|
|
153
|
+
|
|
150
154
|
aggregated_results: list[str] = []
|
|
151
155
|
for task in self.tasks:
|
|
152
156
|
callable_key = self._resolve_registry_key(task.task_type)
|
|
153
|
-
if callable_key not in
|
|
157
|
+
if callable_key not in normalized_registry:
|
|
154
158
|
raise KeyError(f"No agent registered for '{callable_key}'.")
|
|
155
159
|
|
|
156
|
-
agent_callable =
|
|
160
|
+
agent_callable = normalized_registry[callable_key]
|
|
157
161
|
task.start_date = datetime.now(timezone.utc)
|
|
158
162
|
task.status = "running"
|
|
159
163
|
|
|
@@ -207,7 +211,7 @@ class PlanStructure(BaseStructure):
|
|
|
207
211
|
def _run_task(
|
|
208
212
|
task: TaskStructure,
|
|
209
213
|
*,
|
|
210
|
-
agent_callable:
|
|
214
|
+
agent_callable: AgentCallable,
|
|
211
215
|
aggregated_context: list[str],
|
|
212
216
|
) -> object | Coroutine[Any, Any, object]:
|
|
213
217
|
"""Execute a single task using the supplied callable.
|
|
@@ -219,7 +223,7 @@ class PlanStructure(BaseStructure):
|
|
|
219
223
|
----------
|
|
220
224
|
task : TaskStructure
|
|
221
225
|
Task definition containing inputs and metadata.
|
|
222
|
-
agent_callable :
|
|
226
|
+
agent_callable : AgentCallable
|
|
223
227
|
Function responsible for performing the task.
|
|
224
228
|
aggregated_context : list[str]
|
|
225
229
|
Accumulated results from previously executed tasks.
|
|
@@ -140,13 +140,13 @@ class TaskStructure(BaseStructure):
|
|
|
140
140
|
"""
|
|
141
141
|
return "\n".join(
|
|
142
142
|
[
|
|
143
|
-
BaseStructure.format_output("Task type", self.task_type),
|
|
144
|
-
BaseStructure.format_output("Prompt", self.prompt),
|
|
145
|
-
BaseStructure.format_output("Context", self.context),
|
|
146
|
-
BaseStructure.format_output("Status", self.status),
|
|
147
|
-
BaseStructure.format_output("Start date", self.start_date),
|
|
148
|
-
BaseStructure.format_output("End date", self.end_date),
|
|
149
|
-
BaseStructure.format_output("Results", self.results),
|
|
143
|
+
BaseStructure.format_output("Task type", value=self.task_type),
|
|
144
|
+
BaseStructure.format_output("Prompt", value=self.prompt),
|
|
145
|
+
BaseStructure.format_output("Context", value=self.context),
|
|
146
|
+
BaseStructure.format_output("Status", value=self.status),
|
|
147
|
+
BaseStructure.format_output("Start date", value=self.start_date),
|
|
148
|
+
BaseStructure.format_output("End date", value=self.end_date),
|
|
149
|
+
BaseStructure.format_output("Results", value=self.results),
|
|
150
150
|
]
|
|
151
151
|
)
|
|
152
152
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Type aliases for plan execution helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any, Callable, Coroutine, TypeAlias
|
|
7
|
+
|
|
8
|
+
from .enum import AgentEnum
|
|
9
|
+
|
|
10
|
+
AgentCallable = Callable[..., object | Coroutine[Any, Any, object]]
|
|
11
|
+
AgentRegistry: TypeAlias = (
|
|
12
|
+
Mapping[str, AgentCallable] | Mapping[AgentEnum, AgentCallable]
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = ["AgentCallable", "AgentRegistry"]
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Tool handler utilities for OpenAI SDK interactions.
|
|
2
|
+
|
|
3
|
+
This module provides generic tool handling infrastructure including argument
|
|
4
|
+
parsing, Pydantic validation, function execution, and result serialization.
|
|
5
|
+
These utilities reduce boilerplate and ensure consistent tool behavior.
|
|
6
|
+
|
|
7
|
+
Also provides declarative tool specification helpers for building tool
|
|
8
|
+
definitions from named metadata structures.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import inspect
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Callable, TypeAlias, TypeVar
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ValidationError
|
|
18
|
+
|
|
19
|
+
from openai_sdk_helpers.response.tool_call import parse_tool_arguments
|
|
20
|
+
from openai_sdk_helpers.structure.base import BaseStructure
|
|
21
|
+
from openai_sdk_helpers.utils import coerce_jsonable, customJSONEncoder
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T", bound=BaseModel)
|
|
25
|
+
StructureType: TypeAlias = type[BaseStructure]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def serialize_tool_result(result: Any) -> str:
|
|
29
|
+
"""Serialize tool results into a standardized JSON string.
|
|
30
|
+
|
|
31
|
+
Handles Pydantic models, lists, dicts, and plain strings with consistent
|
|
32
|
+
JSON formatting. Pydantic models are serialized using model_dump(),
|
|
33
|
+
while other types are converted to JSON or string representation.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
result : Any
|
|
38
|
+
Tool result to serialize. Can be a Pydantic model, list, dict, str,
|
|
39
|
+
or any JSON-serializable type.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
str
|
|
44
|
+
JSON-formatted string representation of the result.
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
>>> from pydantic import BaseModel
|
|
49
|
+
>>> class Result(BaseModel):
|
|
50
|
+
... value: int
|
|
51
|
+
>>> serialize_tool_result(Result(value=42))
|
|
52
|
+
'{"value": 42}'
|
|
53
|
+
|
|
54
|
+
>>> serialize_tool_result(["item1", "item2"])
|
|
55
|
+
'["item1", "item2"]'
|
|
56
|
+
|
|
57
|
+
>>> serialize_tool_result("plain text")
|
|
58
|
+
'"plain text"'
|
|
59
|
+
|
|
60
|
+
>>> serialize_tool_result({"key": "value"})
|
|
61
|
+
'{"key": "value"}'
|
|
62
|
+
"""
|
|
63
|
+
if isinstance(result, BaseModel):
|
|
64
|
+
return result.model_dump_json()
|
|
65
|
+
|
|
66
|
+
payload = coerce_jsonable(result)
|
|
67
|
+
return json.dumps(payload, cls=customJSONEncoder)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def tool_handler_factory(
|
|
71
|
+
func: Callable[..., Any],
|
|
72
|
+
*,
|
|
73
|
+
input_model: type[T] | None = None,
|
|
74
|
+
) -> Callable[[Any], str]:
|
|
75
|
+
"""Create a generic tool handler that parses, validates, and serializes.
|
|
76
|
+
|
|
77
|
+
Wraps a tool function with automatic argument parsing, optional Pydantic
|
|
78
|
+
validation, execution, and result serialization. This eliminates
|
|
79
|
+
repetitive boilerplate for tool implementations.
|
|
80
|
+
|
|
81
|
+
The returned handler:
|
|
82
|
+
1. Parses tool_call.arguments using parse_tool_arguments
|
|
83
|
+
2. Validates arguments with input_model if provided
|
|
84
|
+
3. Calls func with validated/parsed arguments
|
|
85
|
+
4. Serializes the result using serialize_tool_result
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
func : Callable[..., Any]
|
|
90
|
+
The actual tool implementation function. Should accept keyword
|
|
91
|
+
arguments matching the tool's parameter schema. Can be synchronous
|
|
92
|
+
or asynchronous.
|
|
93
|
+
input_model : type[BaseModel] or None, default None
|
|
94
|
+
Optional Pydantic model for input validation. When provided,
|
|
95
|
+
arguments are validated and converted to this model before being
|
|
96
|
+
passed to func.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
Callable[[Any], str]
|
|
101
|
+
Handler function that accepts a tool_call object (with arguments
|
|
102
|
+
and name attributes) and returns a JSON string result.
|
|
103
|
+
|
|
104
|
+
Raises
|
|
105
|
+
------
|
|
106
|
+
ValidationError
|
|
107
|
+
If input_model is provided and validation fails.
|
|
108
|
+
ValueError
|
|
109
|
+
If argument parsing fails.
|
|
110
|
+
|
|
111
|
+
Examples
|
|
112
|
+
--------
|
|
113
|
+
Basic usage without validation:
|
|
114
|
+
|
|
115
|
+
>>> def search_tool(query: str, limit: int = 10):
|
|
116
|
+
... return {"results": [f"Result for {query}"]}
|
|
117
|
+
>>> handler = tool_handler_factory(search_tool)
|
|
118
|
+
|
|
119
|
+
With Pydantic validation:
|
|
120
|
+
|
|
121
|
+
>>> from pydantic import BaseModel
|
|
122
|
+
>>> class SearchInput(BaseModel):
|
|
123
|
+
... query: str
|
|
124
|
+
... limit: int = 10
|
|
125
|
+
>>> def search_tool(query: str, limit: int = 10):
|
|
126
|
+
... return {"results": [f"Result for {query}"]}
|
|
127
|
+
>>> handler = tool_handler_factory(search_tool, SearchInput)
|
|
128
|
+
|
|
129
|
+
The handler can then be used with OpenAI tool calls:
|
|
130
|
+
|
|
131
|
+
>>> class ToolCall:
|
|
132
|
+
... def __init__(self):
|
|
133
|
+
... self.arguments = '{"query": "test", "limit": 5}'
|
|
134
|
+
... self.name = "search"
|
|
135
|
+
>>> tool_call = ToolCall()
|
|
136
|
+
>>> result = handler(tool_call) # doctest: +SKIP
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def handler(tool_call: Any) -> str:
|
|
140
|
+
"""Handle tool execution with parsing, validation, and serialization.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
tool_call : Any
|
|
145
|
+
Tool call object with 'arguments' and 'name' attributes.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
str
|
|
150
|
+
JSON-formatted result from the tool function.
|
|
151
|
+
|
|
152
|
+
Raises
|
|
153
|
+
------
|
|
154
|
+
ValueError
|
|
155
|
+
If argument parsing fails.
|
|
156
|
+
ValidationError
|
|
157
|
+
If Pydantic validation fails (when input_model is provided).
|
|
158
|
+
"""
|
|
159
|
+
# Extract tool name for error context (required)
|
|
160
|
+
tool_name = getattr(tool_call, "name", "unknown")
|
|
161
|
+
|
|
162
|
+
# Parse arguments with error context
|
|
163
|
+
parsed_args = parse_tool_arguments(tool_call.arguments, tool_name=tool_name)
|
|
164
|
+
|
|
165
|
+
# Validate with Pydantic if model provided
|
|
166
|
+
if input_model is not None:
|
|
167
|
+
validated_input = input_model(**parsed_args)
|
|
168
|
+
# Convert back to dict for function call
|
|
169
|
+
call_kwargs = validated_input.model_dump()
|
|
170
|
+
else:
|
|
171
|
+
call_kwargs = parsed_args
|
|
172
|
+
|
|
173
|
+
# Execute function (sync only - async functions not supported)
|
|
174
|
+
if inspect.iscoroutinefunction(func):
|
|
175
|
+
raise TypeError(
|
|
176
|
+
f"Async functions are not supported by tool_handler_factory. "
|
|
177
|
+
f"Function '{func.__name__}' is async. "
|
|
178
|
+
"Wrap async functions in a synchronous adapter before passing to tool_handler_factory."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
result = func(**call_kwargs)
|
|
182
|
+
|
|
183
|
+
# Serialize result
|
|
184
|
+
return serialize_tool_result(result)
|
|
185
|
+
|
|
186
|
+
return handler
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True)
|
|
190
|
+
class ToolSpec:
|
|
191
|
+
"""Capture tool metadata for response configuration.
|
|
192
|
+
|
|
193
|
+
Provides a named structure for representing tool specifications, making
|
|
194
|
+
tool definitions explicit and eliminating ambiguous tuple ordering.
|
|
195
|
+
|
|
196
|
+
Supports tools with separate input and output structures, where the input
|
|
197
|
+
structure defines the tool's parameter schema and the output structure
|
|
198
|
+
documents the expected return type (for reference only).
|
|
199
|
+
|
|
200
|
+
Attributes
|
|
201
|
+
----------
|
|
202
|
+
structure : StructureType
|
|
203
|
+
The BaseStructure class that defines the tool's input parameter schema.
|
|
204
|
+
Used to generate the OpenAI tool definition.
|
|
205
|
+
tool_name : str
|
|
206
|
+
Name identifier for the tool.
|
|
207
|
+
tool_description : str
|
|
208
|
+
Human-readable description of what the tool does.
|
|
209
|
+
output_structure : StructureType or None, default=None
|
|
210
|
+
Optional BaseStructure class that defines the tool's output schema.
|
|
211
|
+
This is for documentation/reference only and is not sent to OpenAI.
|
|
212
|
+
Useful when a tool accepts one type of input but returns a different
|
|
213
|
+
structured output.
|
|
214
|
+
|
|
215
|
+
Examples
|
|
216
|
+
--------
|
|
217
|
+
Define a tool with same input/output structure:
|
|
218
|
+
|
|
219
|
+
>>> from openai_sdk_helpers import ToolSpec
|
|
220
|
+
>>> from openai_sdk_helpers.structure import PromptStructure
|
|
221
|
+
>>> spec = ToolSpec(
|
|
222
|
+
... structure=PromptStructure,
|
|
223
|
+
... tool_name="web_agent",
|
|
224
|
+
... tool_description="Run a web research workflow"
|
|
225
|
+
... )
|
|
226
|
+
|
|
227
|
+
Define a tool with different input and output structures:
|
|
228
|
+
|
|
229
|
+
>>> from openai_sdk_helpers.structure import PromptStructure, SummaryStructure
|
|
230
|
+
>>> spec = ToolSpec(
|
|
231
|
+
... structure=PromptStructure,
|
|
232
|
+
... tool_name="summarizer",
|
|
233
|
+
... tool_description="Summarize the provided prompt",
|
|
234
|
+
... output_structure=SummaryStructure
|
|
235
|
+
... )
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
structure: StructureType
|
|
239
|
+
tool_name: str
|
|
240
|
+
tool_description: str
|
|
241
|
+
output_structure: StructureType | None = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def build_tool_definitions(tool_specs: list[ToolSpec]) -> list[dict]:
|
|
245
|
+
"""Build tool definitions from named tool specs.
|
|
246
|
+
|
|
247
|
+
Converts a list of ToolSpec objects into OpenAI-compatible tool
|
|
248
|
+
definitions for use in response configurations. Each ToolSpec is
|
|
249
|
+
transformed into a tool definition using the structure's
|
|
250
|
+
response_tool_definition method.
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
tool_specs : list[ToolSpec]
|
|
255
|
+
List of tool specifications to convert.
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
list[dict]
|
|
260
|
+
List of tool definition dictionaries ready for OpenAI API.
|
|
261
|
+
|
|
262
|
+
Examples
|
|
263
|
+
--------
|
|
264
|
+
Build multiple tool definitions:
|
|
265
|
+
|
|
266
|
+
>>> from openai_sdk_helpers import ToolSpec, build_tool_definitions
|
|
267
|
+
>>> from openai_sdk_helpers.structure import PromptStructure
|
|
268
|
+
>>> tools = build_tool_definitions([
|
|
269
|
+
... ToolSpec(
|
|
270
|
+
... structure=PromptStructure,
|
|
271
|
+
... tool_name="web_agent",
|
|
272
|
+
... tool_description="Run a web research workflow"
|
|
273
|
+
... ),
|
|
274
|
+
... ToolSpec(
|
|
275
|
+
... structure=PromptStructure,
|
|
276
|
+
... tool_name="vector_agent",
|
|
277
|
+
... tool_description="Run a vector search workflow"
|
|
278
|
+
... ),
|
|
279
|
+
... ])
|
|
280
|
+
"""
|
|
281
|
+
return [
|
|
282
|
+
spec.structure.response_tool_definition(
|
|
283
|
+
tool_name=spec.tool_name,
|
|
284
|
+
tool_description=spec.tool_description,
|
|
285
|
+
)
|
|
286
|
+
for spec in tool_specs
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
__all__ = [
|
|
291
|
+
"serialize_tool_result",
|
|
292
|
+
"tool_handler_factory",
|
|
293
|
+
"StructureType",
|
|
294
|
+
"ToolSpec",
|
|
295
|
+
"build_tool_definitions",
|
|
296
|
+
]
|