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.
Files changed (46) hide show
  1. openai_sdk_helpers/__init__.py +63 -5
  2. openai_sdk_helpers/agent/base.py +5 -1
  3. openai_sdk_helpers/agent/coordination.py +4 -5
  4. openai_sdk_helpers/agent/runner.py +4 -1
  5. openai_sdk_helpers/agent/search/base.py +1 -0
  6. openai_sdk_helpers/agent/search/vector.py +2 -0
  7. openai_sdk_helpers/cli.py +265 -0
  8. openai_sdk_helpers/config.py +120 -31
  9. openai_sdk_helpers/context_manager.py +1 -1
  10. openai_sdk_helpers/deprecation.py +167 -0
  11. openai_sdk_helpers/environment.py +3 -2
  12. openai_sdk_helpers/errors.py +0 -12
  13. openai_sdk_helpers/logging_config.py +24 -95
  14. openai_sdk_helpers/prompt/base.py +56 -6
  15. openai_sdk_helpers/response/__init__.py +5 -2
  16. openai_sdk_helpers/response/base.py +84 -115
  17. openai_sdk_helpers/response/config.py +142 -0
  18. openai_sdk_helpers/response/messages.py +1 -0
  19. openai_sdk_helpers/response/tool_call.py +15 -4
  20. openai_sdk_helpers/retry.py +1 -1
  21. openai_sdk_helpers/streamlit_app/app.py +14 -3
  22. openai_sdk_helpers/streamlit_app/streamlit_web_search.py +15 -8
  23. openai_sdk_helpers/structure/__init__.py +3 -0
  24. openai_sdk_helpers/structure/base.py +6 -6
  25. openai_sdk_helpers/structure/plan/__init__.py +15 -1
  26. openai_sdk_helpers/structure/plan/helpers.py +173 -0
  27. openai_sdk_helpers/structure/plan/plan.py +13 -9
  28. openai_sdk_helpers/structure/plan/task.py +7 -7
  29. openai_sdk_helpers/structure/plan/types.py +15 -0
  30. openai_sdk_helpers/tools.py +296 -0
  31. openai_sdk_helpers/utils/__init__.py +82 -31
  32. openai_sdk_helpers/{async_utils.py → utils/async_utils.py} +5 -6
  33. openai_sdk_helpers/utils/coercion.py +138 -0
  34. openai_sdk_helpers/utils/deprecation.py +167 -0
  35. openai_sdk_helpers/utils/json_utils.py +98 -0
  36. openai_sdk_helpers/utils/output_validation.py +448 -0
  37. openai_sdk_helpers/utils/path_utils.py +46 -0
  38. openai_sdk_helpers/{validation.py → utils/validation.py} +7 -3
  39. openai_sdk_helpers/vector_storage/storage.py +9 -6
  40. {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.1.dist-info}/METADATA +59 -3
  41. openai_sdk_helpers-0.1.1.dist-info/RECORD +76 -0
  42. openai_sdk_helpers-0.1.1.dist-info/entry_points.txt +2 -0
  43. openai_sdk_helpers/utils/core.py +0 -468
  44. openai_sdk_helpers-0.0.9.dist-info/RECORD +0 -66
  45. {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.1.dist-info}/WHEEL +0 -0
  46. {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, Callable, Coroutine, cast
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: Mapping[
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 : Mapping[AgentEnum | str, Callable[..., Any]]
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 agent_registry:
157
+ if callable_key not in normalized_registry:
154
158
  raise KeyError(f"No agent registered for '{callable_key}'.")
155
159
 
156
- agent_callable = agent_registry[callable_key]
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: Callable[..., object | Coroutine[Any, Any, object]],
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 : Callable[..., Any]
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
+ ]