openai-sdk-helpers 0.2.0__py3-none-any.whl → 0.4.0__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 (58) hide show
  1. openai_sdk_helpers/__init__.py +6 -6
  2. openai_sdk_helpers/agent/__init__.py +4 -2
  3. openai_sdk_helpers/agent/base.py +391 -106
  4. openai_sdk_helpers/agent/config.py +405 -44
  5. openai_sdk_helpers/agent/coordination.py +68 -31
  6. openai_sdk_helpers/agent/runner.py +29 -19
  7. openai_sdk_helpers/agent/search/base.py +103 -54
  8. openai_sdk_helpers/agent/search/vector.py +99 -68
  9. openai_sdk_helpers/agent/search/web.py +84 -50
  10. openai_sdk_helpers/agent/summarizer.py +33 -7
  11. openai_sdk_helpers/agent/translator.py +58 -24
  12. openai_sdk_helpers/agent/validation.py +35 -4
  13. openai_sdk_helpers/cli.py +42 -0
  14. openai_sdk_helpers/config.py +0 -1
  15. openai_sdk_helpers/environment.py +3 -2
  16. openai_sdk_helpers/files_api.py +35 -3
  17. openai_sdk_helpers/prompt/base.py +6 -0
  18. openai_sdk_helpers/response/__init__.py +3 -3
  19. openai_sdk_helpers/response/base.py +161 -22
  20. openai_sdk_helpers/response/config.py +50 -200
  21. openai_sdk_helpers/response/files.py +5 -5
  22. openai_sdk_helpers/response/messages.py +3 -3
  23. openai_sdk_helpers/response/runner.py +7 -7
  24. openai_sdk_helpers/response/tool_call.py +94 -4
  25. openai_sdk_helpers/response/vector_store.py +3 -3
  26. openai_sdk_helpers/streamlit_app/app.py +16 -16
  27. openai_sdk_helpers/streamlit_app/config.py +38 -37
  28. openai_sdk_helpers/streamlit_app/streamlit_web_search.py +2 -2
  29. openai_sdk_helpers/structure/__init__.py +6 -2
  30. openai_sdk_helpers/structure/agent_blueprint.py +2 -2
  31. openai_sdk_helpers/structure/base.py +8 -99
  32. openai_sdk_helpers/structure/plan/plan.py +2 -2
  33. openai_sdk_helpers/structure/plan/task.py +9 -9
  34. openai_sdk_helpers/structure/prompt.py +2 -2
  35. openai_sdk_helpers/structure/responses.py +15 -15
  36. openai_sdk_helpers/structure/summary.py +3 -3
  37. openai_sdk_helpers/structure/translation.py +32 -0
  38. openai_sdk_helpers/structure/validation.py +2 -2
  39. openai_sdk_helpers/structure/vector_search.py +7 -7
  40. openai_sdk_helpers/structure/web_search.py +6 -6
  41. openai_sdk_helpers/tools.py +41 -15
  42. openai_sdk_helpers/utils/__init__.py +19 -5
  43. openai_sdk_helpers/utils/instructions.py +35 -0
  44. openai_sdk_helpers/utils/json/__init__.py +55 -0
  45. openai_sdk_helpers/utils/json/base_model.py +181 -0
  46. openai_sdk_helpers/utils/{json_utils.py → json/data_class.py} +43 -70
  47. openai_sdk_helpers/utils/json/ref.py +113 -0
  48. openai_sdk_helpers/utils/json/utils.py +203 -0
  49. openai_sdk_helpers/utils/output_validation.py +21 -1
  50. openai_sdk_helpers/utils/path_utils.py +34 -1
  51. openai_sdk_helpers/utils/registry.py +194 -0
  52. openai_sdk_helpers/vector_storage/storage.py +10 -0
  53. {openai_sdk_helpers-0.2.0.dist-info → openai_sdk_helpers-0.4.0.dist-info}/METADATA +7 -7
  54. openai_sdk_helpers-0.4.0.dist-info/RECORD +86 -0
  55. openai_sdk_helpers-0.2.0.dist-info/RECORD +0 -79
  56. {openai_sdk_helpers-0.2.0.dist-info → openai_sdk_helpers-0.4.0.dist-info}/WHEEL +0 -0
  57. {openai_sdk_helpers-0.2.0.dist-info → openai_sdk_helpers-0.4.0.dist-info}/entry_points.txt +0 -0
  58. {openai_sdk_helpers-0.2.0.dist-info → openai_sdk_helpers-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,35 +5,23 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
  from typing import Generic, Optional, Sequence, Type, TypeVar
8
- from openai.types.responses.response_text_config_param import ResponseTextConfigParam
9
8
 
10
9
  from ..config import OpenAISettings
11
- from ..structure.base import BaseStructure
12
- from ..response.base import BaseResponse, ToolHandler
13
- from ..utils import JSONSerializable
14
- from ..utils.path_utils import ensure_directory
10
+ from ..structure.base import StructureBase
11
+ from ..response.base import ResponseBase, ToolHandler
12
+ from ..utils.json.data_class import DataclassJSONSerializable
13
+ from ..utils.registry import BaseRegistry
14
+ from ..utils.instructions import resolve_instructions_from_path
15
15
 
16
- TIn = TypeVar("TIn", bound="BaseStructure")
17
- TOut = TypeVar("TOut", bound="BaseStructure")
16
+ TIn = TypeVar("TIn", bound="StructureBase")
17
+ TOut = TypeVar("TOut", bound="StructureBase")
18
18
 
19
19
 
20
- class ResponseRegistry:
20
+ class ResponseRegistry(BaseRegistry["ResponseConfiguration"]):
21
21
  """Registry for managing ResponseConfiguration instances.
22
22
 
23
- Provides centralized storage and retrieval of response configurations,
24
- enabling reusable response specs across the application. Configurations
25
- are stored by name and can be retrieved or listed as needed.
26
-
27
- Methods
28
- -------
29
- register(config)
30
- Add a ResponseConfiguration to the registry.
31
- get(name)
32
- Retrieve a configuration by name.
33
- list_names()
34
- Return all registered configuration names.
35
- clear()
36
- Remove all registered configurations.
23
+ Inherits from BaseRegistry to provide centralized storage and retrieval
24
+ of response configurations, enabling reusable response specs across the application.
37
25
 
38
26
  Examples
39
27
  --------
@@ -51,134 +39,7 @@ class ResponseRegistry:
51
39
  'test'
52
40
  """
53
41
 
54
- def __init__(self) -> None:
55
- """Initialize an empty registry."""
56
- self._configs: dict[str, ResponseConfiguration] = {}
57
-
58
- def register(self, config: ResponseConfiguration) -> None:
59
- """Add a ResponseConfiguration to the registry.
60
-
61
- Parameters
62
- ----------
63
- config : ResponseConfiguration
64
- Configuration to register.
65
-
66
- Raises
67
- ------
68
- ValueError
69
- If a configuration with the same name is already registered.
70
-
71
- Examples
72
- --------
73
- >>> registry = ResponseRegistry()
74
- >>> config = ResponseConfiguration(...)
75
- >>> registry.register(config)
76
- """
77
- if config.name in self._configs:
78
- raise ValueError(
79
- f"Configuration '{config.name}' is already registered. "
80
- "Use a unique name or clear the registry first."
81
- )
82
- self._configs[config.name] = config
83
-
84
- def get(self, name: str) -> ResponseConfiguration:
85
- """Retrieve a configuration by name.
86
-
87
- Parameters
88
- ----------
89
- name : str
90
- Configuration name to look up.
91
-
92
- Returns
93
- -------
94
- ResponseConfiguration
95
- The registered configuration.
96
-
97
- Raises
98
- ------
99
- KeyError
100
- If no configuration with the given name exists.
101
-
102
- Examples
103
- --------
104
- >>> registry = ResponseRegistry()
105
- >>> config = registry.get("test")
106
- """
107
- if name not in self._configs:
108
- raise KeyError(
109
- f"No configuration named '{name}' found. "
110
- f"Available: {list(self._configs.keys())}"
111
- )
112
- return self._configs[name]
113
-
114
- def list_names(self) -> list[str]:
115
- """Return all registered configuration names.
116
-
117
- Returns
118
- -------
119
- list[str]
120
- Sorted list of configuration names.
121
-
122
- Examples
123
- --------
124
- >>> registry = ResponseRegistry()
125
- >>> registry.list_names()
126
- []
127
- """
128
- return sorted(self._configs.keys())
129
-
130
- def clear(self) -> None:
131
- """Remove all registered configurations.
132
-
133
- Examples
134
- --------
135
- >>> registry = ResponseRegistry()
136
- >>> registry.clear()
137
- """
138
- self._configs.clear()
139
-
140
- def save_to_directory(self, path: Path | str) -> None:
141
- """Export all registered configurations to JSON files in a directory.
142
-
143
- Serializes each registered ResponseConfiguration to an individual JSON file
144
- named after the configuration. Creates the directory if it does not exist.
145
-
146
- Parameters
147
- ----------
148
- path : Path or str
149
- Directory path where JSON files will be saved. Will be created if
150
- it does not already exist.
151
-
152
- Returns
153
- -------
154
- None
155
-
156
- Raises
157
- ------
158
- OSError
159
- If the directory cannot be created or files cannot be written.
160
-
161
- Examples
162
- --------
163
- >>> registry = ResponseRegistry()
164
- >>> registry.save_to_directory("./data")
165
- >>> registry.save_to_directory(Path("exports"))
166
- """
167
- dir_path = ensure_directory(Path(path))
168
- config_names = self.list_names()
169
-
170
- if not config_names:
171
- return
172
-
173
- for config_name in config_names:
174
- config = self.get(config_name)
175
- filename = f"{config_name}.json"
176
- filepath = dir_path / filename
177
- config.to_json_file(filepath)
178
-
179
-
180
- # Global default registry instance
181
- _default_registry = ResponseRegistry()
42
+ pass
182
43
 
183
44
 
184
45
  def get_default_registry() -> ResponseRegistry:
@@ -199,13 +60,13 @@ def get_default_registry() -> ResponseRegistry:
199
60
 
200
61
 
201
62
  @dataclass(frozen=True, slots=True)
202
- class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
63
+ class ResponseConfiguration(DataclassJSONSerializable, Generic[TIn, TOut]):
203
64
  """
204
65
  Represent an immutable configuration describing input and output structures.
205
66
 
206
67
  Encapsulate all metadata required to define how a request is interpreted and
207
68
  how a response is structured, while enforcing strict type and runtime safety.
208
- Inherits from JSONSerializable to support serialization to JSON format.
69
+ Inherits from DataclassJSONSerializable to support serialization to JSON format.
209
70
 
210
71
  Parameters
211
72
  ----------
@@ -216,13 +77,13 @@ class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
216
77
  contents are loaded at runtime.
217
78
  tools : Sequence[object], optional
218
79
  Tool definitions associated with the configuration. Default is None.
219
- input_structure : Type[BaseStructure], optional
80
+ input_structure : Type[StructureBase], optional
220
81
  Structure class used to parse or validate input. Must subclass
221
- BaseStructure. Default is None.
222
- output_structure : Type[BaseStructure], optional
82
+ StructureBase. Default is None.
83
+ output_structure : Type[StructureBase], optional
223
84
  Structure class used to format or validate output. Schema is
224
85
  automatically generated from this structure. Must subclass
225
- BaseStructure. Default is None.
86
+ StructureBase. Default is None.
226
87
  system_vector_store : list[str], optional
227
88
  Optional list of vector store names to attach as system context.
228
89
  Default is None.
@@ -237,7 +98,7 @@ class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
237
98
  If instructions is not a string or Path.
238
99
  If tools is provided and is not a sequence.
239
100
  If input_structure or output_structure is not a class.
240
- If input_structure or output_structure does not subclass BaseStructure.
101
+ If input_structure or output_structure does not subclass StructureBase.
241
102
  ValueError
242
103
  If instructions is a string that is empty or only whitespace.
243
104
  FileNotFoundError
@@ -246,7 +107,7 @@ class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
246
107
  Methods
247
108
  -------
248
109
  __post_init__()
249
- Validate configuration invariants and enforce BaseStructure subclassing.
110
+ Validate configuration invariants and enforce StructureBase subclassing.
250
111
  instructions_text
251
112
  Return the resolved instruction content as a string.
252
113
  to_json()
@@ -276,14 +137,14 @@ class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
276
137
  input_structure: Optional[Type[TIn]]
277
138
  output_structure: Optional[Type[TOut]]
278
139
  system_vector_store: Optional[list[str]] = None
279
- data_path: Optional[Path | str] = None
140
+ add_output_instructions: bool = True
280
141
 
281
142
  def __post_init__(self) -> None:
282
143
  """
283
144
  Validate configuration invariants after initialization.
284
145
 
285
146
  Enforce non-empty naming, correct typing of structures, and ensure that
286
- any declared structure subclasses BaseStructure.
147
+ any declared structure subclasses StructureBase.
287
148
 
288
149
  Raises
289
150
  ------
@@ -291,7 +152,7 @@ class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
291
152
  If name is not a non-empty string.
292
153
  If tools is provided and is not a sequence.
293
154
  If input_structure or output_structure is not a class.
294
- If input_structure or output_structure does not subclass BaseStructure.
155
+ If input_structure or output_structure does not subclass StructureBase.
295
156
  """
296
157
  if not self.name or not isinstance(self.name, str):
297
158
  raise TypeError("Configuration.name must be a non-empty str")
@@ -315,10 +176,10 @@ class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
315
176
  continue
316
177
  if not isinstance(cls, type):
317
178
  raise TypeError(
318
- f"Configuration.{attr} must be a class (Type[BaseStructure]) or None"
179
+ f"Configuration.{attr} must be a class (Type[StructureBase]) or None"
319
180
  )
320
- if not issubclass(cls, BaseStructure):
321
- raise TypeError(f"Configuration.{attr} must subclass BaseStructure")
181
+ if not issubclass(cls, StructureBase):
182
+ raise TypeError(f"Configuration.{attr} must subclass StructureBase")
322
183
 
323
184
  if self.tools is not None and not isinstance(self.tools, Sequence):
324
185
  raise TypeError("Configuration.tools must be a Sequence or None")
@@ -332,62 +193,51 @@ class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
332
193
  str
333
194
  Plain-text instructions, loading template files when necessary.
334
195
  """
335
- return self._resolve_instructions()
336
-
337
- def _resolve_instructions(self) -> str:
338
- if isinstance(self.instructions, Path):
339
- instruction_path = self.instructions.expanduser()
340
- try:
341
- return instruction_path.read_text(encoding="utf-8")
342
- except OSError as exc:
343
- raise ValueError(
344
- f"Unable to read instructions at '{instruction_path}': {exc}"
345
- ) from exc
346
- return self.instructions
196
+ resolved_instructions: str = resolve_instructions_from_path(self.instructions)
197
+ output_instructions = ""
198
+ if self.output_structure is not None and self.add_output_instructions:
199
+ output_instructions = self.output_structure.get_prompt(
200
+ add_enum_values=False
201
+ )
202
+ if output_instructions:
203
+ return f"{resolved_instructions}\n{output_instructions}"
204
+
205
+ return resolved_instructions
347
206
 
348
207
  def gen_response(
349
208
  self,
209
+ *,
350
210
  openai_settings: OpenAISettings,
351
- tool_handlers: dict[str, ToolHandler] = {},
352
- add_output_instructions: bool = True,
353
- ) -> BaseResponse[TOut]:
354
- """Generate a BaseResponse instance based on the configuration.
211
+ data_path: Optional[Path] = None,
212
+ tool_handlers: dict[str, ToolHandler] | None = None,
213
+ ) -> ResponseBase[TOut]:
214
+ """Generate a ResponseBase instance based on the configuration.
355
215
 
356
216
  Parameters
357
217
  ----------
358
218
  openai_settings : OpenAISettings
359
219
  Authentication and model settings applied to the generated
360
- :class:`BaseResponse`.
220
+ :class:`ResponseBase`.
361
221
  tool_handlers : dict[str, Callable], optional
362
222
  Mapping of tool names to handler callables. Defaults to an empty
363
223
  dictionary when not provided.
364
- add_output_instructions : bool, default=True
365
- Whether to append the structured output prompt to the instructions.
366
224
 
367
225
  Returns
368
226
  -------
369
- BaseResponse[TOut]
370
- An instance of BaseResponse configured with ``openai_settings``.
227
+ ResponseBase[TOut]
228
+ An instance of ResponseBase configured with ``openai_settings``.
371
229
  """
372
- output_instructions = ""
373
- if self.output_structure is not None and add_output_instructions:
374
- output_instructions = self.output_structure.get_prompt(
375
- add_enum_values=False
376
- )
377
-
378
- instructions = (
379
- f"{self.instructions_text}\n{output_instructions}"
380
- if output_instructions
381
- else self.instructions_text
382
- )
383
-
384
- return BaseResponse[TOut](
230
+ return ResponseBase[TOut](
385
231
  name=self.name,
386
- instructions=instructions,
232
+ instructions=self.instructions_text,
387
233
  tools=self.tools,
388
234
  output_structure=self.output_structure,
389
235
  system_vector_store=self.system_vector_store,
390
- data_path=self.data_path,
236
+ data_path=data_path,
391
237
  tool_handlers=tool_handlers,
392
238
  openai_settings=openai_settings,
393
239
  )
240
+
241
+
242
+ # Global default registry instance
243
+ _default_registry = ResponseRegistry()
@@ -23,11 +23,11 @@ from openai.types.responses.response_input_image_content_param import (
23
23
  from ..utils import create_file_data_url, create_image_data_url, is_image_file, log
24
24
 
25
25
  if TYPE_CHECKING: # pragma: no cover
26
- from .base import BaseResponse
26
+ from .base import ResponseBase
27
27
 
28
28
 
29
29
  def process_files(
30
- response: BaseResponse[Any],
30
+ response: ResponseBase[Any],
31
31
  files: list[str],
32
32
  use_vector_store: bool = False,
33
33
  batch_size: int = 10,
@@ -45,7 +45,7 @@ def process_files(
45
45
 
46
46
  Parameters
47
47
  ----------
48
- response : BaseResponse[Any]
48
+ response : ResponseBase[Any]
49
49
  Response instance that will use the processed files.
50
50
  files : list[str]
51
51
  List of file paths to process.
@@ -114,7 +114,7 @@ def process_files(
114
114
 
115
115
 
116
116
  def _upload_to_vector_store(
117
- response: BaseResponse[Any], document_files: list[str]
117
+ response: ResponseBase[Any], document_files: list[str]
118
118
  ) -> list[ResponseInputFileParam]:
119
119
  """Upload documents to vector store and return file references.
120
120
 
@@ -123,7 +123,7 @@ def _upload_to_vector_store(
123
123
 
124
124
  Parameters
125
125
  ----------
126
- response : BaseResponse[Any]
126
+ response : ResponseBase[Any]
127
127
  Response instance with vector storage.
128
128
  document_files : list[str]
129
129
  List of document file paths to upload.
@@ -24,12 +24,12 @@ from openai.types.responses.response_input_param import (
24
24
  )
25
25
  from openai.types.responses.response_output_message import ResponseOutputMessage
26
26
 
27
- from ..utils import JSONSerializable
27
+ from ..utils.json.data_class import DataclassJSONSerializable
28
28
  from .tool_call import ResponseToolCall
29
29
 
30
30
 
31
31
  @dataclass
32
- class ResponseMessage(JSONSerializable):
32
+ class ResponseMessage(DataclassJSONSerializable):
33
33
  """Single message exchanged with the OpenAI API.
34
34
 
35
35
  Represents a complete message with role, content, timestamp, and
@@ -91,7 +91,7 @@ class ResponseMessage(JSONSerializable):
91
91
 
92
92
 
93
93
  @dataclass
94
- class ResponseMessages(JSONSerializable):
94
+ class ResponseMessages(DataclassJSONSerializable):
95
95
  """Collection of messages in a conversation.
96
96
 
97
97
  Manages the complete history of messages exchanged during an OpenAI
@@ -10,10 +10,10 @@ from __future__ import annotations
10
10
  import asyncio
11
11
  from typing import Any, TypeVar
12
12
 
13
- from .base import BaseResponse
13
+ from .base import ResponseBase
14
14
 
15
15
 
16
- R = TypeVar("R", bound=BaseResponse[Any])
16
+ R = TypeVar("R", bound=ResponseBase[Any])
17
17
 
18
18
 
19
19
  def run_sync(
@@ -29,7 +29,7 @@ def run_sync(
29
29
 
30
30
  Parameters
31
31
  ----------
32
- response_cls : type[BaseResponse]
32
+ response_cls : type[ResponseBase]
33
33
  Response class to instantiate for the workflow.
34
34
  content : str
35
35
  Prompt text to send to the OpenAI API.
@@ -39,7 +39,7 @@ def run_sync(
39
39
  Returns
40
40
  -------
41
41
  Any
42
- Parsed response from BaseResponse.run_sync, typically a structured
42
+ Parsed response from ResponseBase.run_sync, typically a structured
43
43
  output or None.
44
44
 
45
45
  Examples
@@ -71,7 +71,7 @@ async def run_async(
71
71
 
72
72
  Parameters
73
73
  ----------
74
- response_cls : type[BaseResponse]
74
+ response_cls : type[ResponseBase]
75
75
  Response class to instantiate for the workflow.
76
76
  content : str
77
77
  Prompt text to send to the OpenAI API.
@@ -81,7 +81,7 @@ async def run_async(
81
81
  Returns
82
82
  -------
83
83
  Any
84
- Parsed response from BaseResponse.run_async, typically a structured
84
+ Parsed response from ResponseBase.run_async, typically a structured
85
85
  output or None.
86
86
 
87
87
  Examples
@@ -114,7 +114,7 @@ def run_streamed(
114
114
 
115
115
  Parameters
116
116
  ----------
117
- response_cls : type[BaseResponse]
117
+ response_cls : type[ResponseBase]
118
118
  Response class to instantiate for the workflow.
119
119
  content : str
120
120
  Prompt text to send to the OpenAI API.
@@ -9,16 +9,18 @@ from __future__ import annotations
9
9
 
10
10
  import ast
11
11
  import json
12
+ import re
12
13
  from dataclasses import dataclass
13
14
 
14
15
  from openai.types.responses.response_function_tool_call_param import (
15
16
  ResponseFunctionToolCallParam,
16
17
  )
17
18
  from openai.types.responses.response_input_param import FunctionCallOutput
19
+ from ..utils.json.data_class import DataclassJSONSerializable
18
20
 
19
21
 
20
22
  @dataclass
21
- class ResponseToolCall:
23
+ class ResponseToolCall(DataclassJSONSerializable):
22
24
  """Container for tool call data in a conversation.
23
25
 
24
26
  Stores the complete information about a tool invocation including
@@ -94,6 +96,85 @@ class ResponseToolCall:
94
96
  return function_call, function_call_output
95
97
 
96
98
 
99
+ def _to_snake_case(name: str) -> str:
100
+ """Convert a PascalCase or camelCase string to snake_case.
101
+
102
+ Parameters
103
+ ----------
104
+ name : str
105
+ The name to convert.
106
+
107
+ Returns
108
+ -------
109
+ str
110
+ The snake_case version of the name.
111
+
112
+ Examples
113
+ --------
114
+ >>> _to_snake_case("ExampleStructure")
115
+ 'example_structure'
116
+ >>> _to_snake_case("MyToolName")
117
+ 'my_tool_name'
118
+ """
119
+ # First regex: Insert underscore before uppercase letters followed by
120
+ # lowercase letters (e.g., "Tool" in "ExampleTool" becomes "_Tool")
121
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
122
+ # Second regex: Insert underscore between lowercase/digit and uppercase
123
+ # (e.g., "e3" followed by "T" becomes "e3_T")
124
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
125
+
126
+
127
+ def _unwrap_arguments(parsed: dict, tool_name: str) -> dict:
128
+ """Unwrap arguments if wrapped in a single-key dict.
129
+
130
+ Some responses wrap arguments under a key matching the structure class
131
+ name (e.g., {"ExampleStructure": {...}}) or snake_case variant
132
+ (e.g., {"example_structure": {...}}). This function detects and unwraps
133
+ such wrappers to normalize the payload.
134
+
135
+ Parameters
136
+ ----------
137
+ parsed : dict
138
+ The parsed arguments dictionary.
139
+ tool_name : str
140
+ The tool name, used to match potential wrapper keys.
141
+
142
+ Returns
143
+ -------
144
+ dict
145
+ Unwrapped arguments dictionary, or original if no wrapper detected.
146
+
147
+ Examples
148
+ --------
149
+ >>> _unwrap_arguments({"ExampleTool": {"arg": "value"}}, "ExampleTool")
150
+ {'arg': 'value'}
151
+ >>> _unwrap_arguments({"example_tool": {"arg": "value"}}, "ExampleTool")
152
+ {'arg': 'value'}
153
+ >>> _unwrap_arguments({"arg": "value"}, "ExampleTool")
154
+ {'arg': 'value'}
155
+ """
156
+ # Only unwrap if dict has exactly one key
157
+ if not isinstance(parsed, dict) or len(parsed) != 1:
158
+ return parsed
159
+
160
+ wrapper_key = next(iter(parsed))
161
+ wrapped_value = parsed[wrapper_key]
162
+
163
+ # Only unwrap if the value is also a dict
164
+ if not isinstance(wrapped_value, dict):
165
+ return parsed
166
+
167
+ # Check if wrapper key matches tool name (case-insensitive or snake_case)
168
+ tool_name_lower = tool_name.lower()
169
+ tool_name_snake = _to_snake_case(tool_name)
170
+ wrapper_key_lower = wrapper_key.lower()
171
+
172
+ if wrapper_key_lower in (tool_name_lower, tool_name_snake):
173
+ return wrapped_value
174
+
175
+ return parsed
176
+
177
+
97
178
  def parse_tool_arguments(arguments: str, tool_name: str) -> dict:
98
179
  """Parse tool call arguments with fallback for malformed JSON.
99
180
 
@@ -102,6 +183,9 @@ def parse_tool_arguments(arguments: str, tool_name: str) -> dict:
102
183
  formatting issues like single quotes instead of double quotes.
103
184
  Provides clear error context including tool name and raw payload.
104
185
 
186
+ Also handles unwrapping of arguments that are wrapped in a single-key
187
+ dictionary matching the tool name (e.g., {"ExampleStructure": {...}}).
188
+
105
189
  Parameters
106
190
  ----------
107
191
  arguments : str
@@ -112,7 +196,7 @@ def parse_tool_arguments(arguments: str, tool_name: str) -> dict:
112
196
  Returns
113
197
  -------
114
198
  dict
115
- Parsed dictionary of tool arguments.
199
+ Parsed dictionary of tool arguments, with wrapper unwrapped if present.
116
200
 
117
201
  Raises
118
202
  ------
@@ -127,12 +211,15 @@ def parse_tool_arguments(arguments: str, tool_name: str) -> dict:
127
211
 
128
212
  >>> parse_tool_arguments("{'key': 'value'}", tool_name="search")
129
213
  {'key': 'value'}
214
+
215
+ >>> parse_tool_arguments('{"ExampleTool": {"arg": "value"}}', "ExampleTool")
216
+ {'arg': 'value'}
130
217
  """
131
218
  try:
132
- return json.loads(arguments)
219
+ parsed = json.loads(arguments)
133
220
  except json.JSONDecodeError:
134
221
  try:
135
- return ast.literal_eval(arguments)
222
+ parsed = ast.literal_eval(arguments)
136
223
  except Exception as exc: # noqa: BLE001
137
224
  # Build informative error message with context
138
225
  payload_preview = (
@@ -142,3 +229,6 @@ def parse_tool_arguments(arguments: str, tool_name: str) -> dict:
142
229
  f"Failed to parse tool arguments for tool '{tool_name}'. "
143
230
  f"Raw payload: {payload_preview}"
144
231
  ) from exc
232
+
233
+ # Unwrap if wrapped in a single-key dict matching tool name
234
+ return _unwrap_arguments(parsed, tool_name)
@@ -11,11 +11,11 @@ from typing import Any, Sequence
11
11
  from openai import OpenAI
12
12
 
13
13
  from ..utils import ensure_list
14
- from .base import BaseResponse
14
+ from .base import ResponseBase
15
15
 
16
16
 
17
17
  def attach_vector_store(
18
- response: BaseResponse[Any],
18
+ response: ResponseBase[Any],
19
19
  vector_stores: str | Sequence[str],
20
20
  api_key: str | None = None,
21
21
  ) -> list[str]:
@@ -27,7 +27,7 @@ def attach_vector_store(
27
27
 
28
28
  Parameters
29
29
  ----------
30
- response : BaseResponse[Any]
30
+ response : ResponseBase[Any]
31
31
  Response instance whose tool configuration will be updated.
32
32
  vector_stores : str or Sequence[str]
33
33
  Single vector store name or sequence of names to attach.