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
@@ -38,16 +38,13 @@ from .messages import ResponseMessage, ResponseMessages
38
38
  from ..config import OpenAISettings
39
39
  from ..structure import BaseStructure
40
40
  from ..types import OpenAIClient
41
- from ..utils import ensure_list, log
41
+ from ..utils import check_filepath, coerce_jsonable, customJSONEncoder, ensure_list, log
42
42
 
43
43
  if TYPE_CHECKING: # pragma: no cover - only for typing hints
44
44
  from openai_sdk_helpers.streamlit_app.config import StreamlitAppConfig
45
45
 
46
46
  T = TypeVar("T", bound=BaseStructure)
47
47
  ToolHandler = Callable[[ResponseFunctionToolCall], str | Any]
48
- ProcessContent = Callable[[str], tuple[str, list[str]]]
49
-
50
-
51
48
  RB = TypeVar("RB", bound="BaseResponse[BaseStructure]")
52
49
 
53
50
 
@@ -111,16 +108,14 @@ class BaseResponse(Generic[T]):
111
108
  def __init__(
112
109
  self,
113
110
  *,
111
+ name: str,
114
112
  instructions: str,
115
113
  tools: list | None,
116
114
  output_structure: type[T] | None,
117
115
  tool_handlers: dict[str, ToolHandler],
118
116
  openai_settings: OpenAISettings,
119
- process_content: ProcessContent | None = None,
120
- name: str | None = None,
121
117
  system_vector_store: list[str] | None = None,
122
- data_path_fn: Callable[[str], Path] | None = None,
123
- save_path: Path | str | None = None,
118
+ data_path: Path | str | None = None,
124
119
  ) -> None:
125
120
  """Initialize a response session with OpenAI configuration.
126
121
 
@@ -130,6 +125,9 @@ class BaseResponse(Generic[T]):
130
125
 
131
126
  Parameters
132
127
  ----------
128
+ name : str
129
+ Name for this response session, used for organizing artifacts
130
+ and naming vector stores.
133
131
  instructions : str
134
132
  System instructions provided to the OpenAI API for context.
135
133
  tools : list or None
@@ -144,18 +142,12 @@ class BaseResponse(Generic[T]):
144
142
  result.
145
143
  openai_settings : OpenAISettings
146
144
  Fully configured OpenAI settings with API key and default model.
147
- process_content : callable or None, default None
148
- Optional callback that processes input text and extracts file
149
- attachments. Must return a tuple of (processed_text, attachment_list).
150
- name : str or None, default None
151
- Module name used for data path construction when data_path_fn is set.
152
145
  system_vector_store : list[str] or None, default None
153
146
  Optional list of vector store names to attach as system context.
154
- data_path_fn : callable or None, default None
155
- Function mapping name to a base directory path for artifact storage.
156
- save_path : Path, str, or None, default None
157
- Optional path to a directory or file where message history is saved.
158
- If a directory, files are named using the session UUID.
147
+ data_path : Path, str, or None, default None
148
+ Optional absolute directory path for storing artifacts. If not provided,
149
+ defaults to get_data_path(class_name). Session files are saved as
150
+ data_path / uuid.json.
159
151
 
160
152
  Raises
161
153
  ------
@@ -170,18 +162,30 @@ class BaseResponse(Generic[T]):
170
162
  >>> from openai_sdk_helpers import BaseResponse, OpenAISettings
171
163
  >>> settings = OpenAISettings(api_key="sk-...", default_model="gpt-4")
172
164
  >>> response = BaseResponse(
165
+ ... name="my_session",
173
166
  ... instructions="You are helpful",
174
167
  ... tools=None,
175
168
  ... output_structure=None,
176
169
  ... tool_handlers={},
177
- ... openai_settings=settings
170
+ ... openai_settings=settings,
178
171
  ... )
179
172
  """
180
173
  self._tool_handlers = tool_handlers
181
- self._process_content = process_content
182
174
  self._name = name
183
- self._data_path_fn = data_path_fn
184
- self._save_path = Path(save_path) if save_path is not None else None
175
+
176
+ # Resolve data_path with class name appended
177
+ class_name = self.__class__.__name__.lower()
178
+ if data_path is not None:
179
+ data_path_obj = Path(data_path)
180
+ if data_path_obj.name == class_name:
181
+ self._data_path = data_path_obj
182
+ else:
183
+ self._data_path = data_path_obj / class_name
184
+ else:
185
+ from ..environment import get_data_path
186
+
187
+ self._data_path = get_data_path(class_name)
188
+
185
189
  self._instructions = instructions
186
190
  self._tools = tools if tools is not None else []
187
191
  self._output_structure = output_structure
@@ -203,7 +207,6 @@ class BaseResponse(Generic[T]):
203
207
  )
204
208
 
205
209
  self.uuid = uuid.uuid4()
206
- self.name = self.__class__.__name__.lower()
207
210
 
208
211
  system_content: ResponseInputMessageContentListParam = [
209
212
  ResponseInputTextParam(type="input_text", text=instructions)
@@ -227,40 +230,19 @@ class BaseResponse(Generic[T]):
227
230
 
228
231
  self.messages = ResponseMessages()
229
232
  self.messages.add_system_message(content=system_content)
230
- if self._save_path is not None or (
231
- self._data_path_fn is not None and self._name is not None
232
- ):
233
+ if self._data_path is not None:
233
234
  self.save()
234
235
 
235
236
  @property
236
- def data_path(self) -> Path:
237
- """Return the directory for persisting session artifacts.
238
-
239
- Constructs a path using data_path_fn, name, class name, and the
240
- session name. Both data_path_fn and name must be set during
241
- initialization for this property to work.
237
+ def name(self) -> str:
238
+ """Return the name of this response session.
242
239
 
243
240
  Returns
244
241
  -------
245
- Path
246
- Absolute path for persisting response artifacts and message history.
247
-
248
- Raises
249
- ------
250
- RuntimeError
251
- If data_path_fn or name were not provided during initialization.
252
-
253
- Examples
254
- --------
255
- >>> response.data_path
256
- PosixPath('/data/myapp/baseresponse/session_123')
242
+ str
243
+ Name used for organizing artifacts and naming vector stores.
257
244
  """
258
- if self._data_path_fn is None or self._name is None:
259
- raise RuntimeError(
260
- "data_path_fn and name are required to build data paths."
261
- )
262
- base_path = self._data_path_fn(self._name)
263
- return base_path / self.__class__.__name__.lower() / self.name
245
+ return self._name
264
246
 
265
247
  def _build_input(
266
248
  self,
@@ -269,16 +251,15 @@ class BaseResponse(Generic[T]):
269
251
  ) -> None:
270
252
  """Construct input messages for the OpenAI API request.
271
253
 
272
- Processes content through the optional process_content callback,
273
- uploads any file attachments to vector stores, and adds all
274
- messages to the conversation history.
254
+ Uploads any file attachments to vector stores and adds all messages
255
+ to the conversation history.
275
256
 
276
257
  Parameters
277
258
  ----------
278
259
  content : str or list[str]
279
260
  String or list of strings to include as user messages.
280
261
  attachments : list[str] or None, default None
281
- Optional list of file paths to upload and attach to the message.
262
+ Optional list of file paths to upload and attach to all messages.
282
263
 
283
264
  Notes
284
265
  -----
@@ -287,45 +268,46 @@ class BaseResponse(Generic[T]):
287
268
  the tools list.
288
269
  """
289
270
  contents = ensure_list(content)
271
+ all_attachments = attachments or []
272
+
273
+ # Upload files once and collect their IDs
274
+ file_ids: list[str] = []
275
+ if all_attachments:
276
+ if self._user_vector_storage is None:
277
+ from openai_sdk_helpers.vector_storage import VectorStorage
290
278
 
279
+ store_name = (
280
+ f"{self.__class__.__name__.lower()}_{self._name}_{self.uuid}_user"
281
+ )
282
+ self._user_vector_storage = VectorStorage(
283
+ store_name=store_name,
284
+ client=self._client,
285
+ model=self._model,
286
+ )
287
+ user_vector_storage = cast(Any, self._user_vector_storage)
288
+ if not any(tool.get("type") == "file_search" for tool in self._tools):
289
+ self._tools.append(
290
+ {
291
+ "type": "file_search",
292
+ "vector_store_ids": [user_vector_storage.id],
293
+ }
294
+ )
295
+
296
+ user_vector_storage = cast(Any, self._user_vector_storage)
297
+ for file_path in all_attachments:
298
+ uploaded_file = user_vector_storage.upload_file(file_path)
299
+ file_ids.append(uploaded_file.id)
300
+
301
+ # Add each content as a separate message with the same attachments
291
302
  for raw_content in contents:
292
- if self._process_content is None:
293
- processed_text, content_attachments = raw_content, []
294
- else:
295
- processed_text, content_attachments = self._process_content(raw_content)
303
+ processed_text = raw_content.strip()
296
304
  input_content: list[ResponseInputTextParam | ResponseInputFileParam] = [
297
305
  ResponseInputTextParam(type="input_text", text=processed_text)
298
306
  ]
299
307
 
300
- all_attachments = (attachments or []) + content_attachments
301
-
302
- for file_path in all_attachments:
303
- if self._user_vector_storage is None:
304
- from openai_sdk_helpers.vector_storage import VectorStorage
305
-
306
- store_name = f"{self.__class__.__name__.lower()}_{self.name}_{self.uuid}_user"
307
- self._user_vector_storage = VectorStorage(
308
- store_name=store_name,
309
- client=self._client,
310
- model=self._model,
311
- )
312
- user_vector_storage = cast(Any, self._user_vector_storage)
313
- if not any(
314
- tool.get("type") == "file_search" for tool in self._tools
315
- ):
316
- self._tools.append(
317
- {
318
- "type": "file_search",
319
- "vector_store_ids": [user_vector_storage.id],
320
- }
321
- )
322
- else:
323
- # If system vector store is attached, its ID will be in tool config
324
- pass
325
- user_vector_storage = cast(Any, self._user_vector_storage)
326
- uploaded_file = user_vector_storage.upload_file(file_path)
308
+ for file_id in file_ids:
327
309
  input_content.append(
328
- ResponseInputFileParam(type="input_file", file_id=uploaded_file.id)
310
+ ResponseInputFileParam(type="input_file", file_id=file_id)
329
311
  )
330
312
 
331
313
  message = cast(
@@ -421,8 +403,8 @@ class BaseResponse(Generic[T]):
421
403
  tool_result = json.loads(tool_result_json)
422
404
  tool_output = tool_result_json
423
405
  else:
424
- tool_result = tool_result_json
425
- tool_output = json.dumps(tool_result)
406
+ tool_result = coerce_jsonable(tool_result_json)
407
+ tool_output = json.dumps(tool_result, cls=customJSONEncoder)
426
408
  self.messages.add_tool_message(
427
409
  content=response_output, output=tool_output
428
410
  )
@@ -443,7 +425,7 @@ class BaseResponse(Generic[T]):
443
425
  parsed_result = cast(T, tool_result)
444
426
 
445
427
  if isinstance(response_output, ResponseOutputMessage):
446
- self.messages.add_assistant_message(response_output, kwargs)
428
+ self.messages.add_assistant_message(response_output, metadata=kwargs)
447
429
  self.save()
448
430
  if hasattr(response, "output_text") and response.output_text:
449
431
  raw_text = response.output_text
@@ -462,6 +444,7 @@ class BaseResponse(Generic[T]):
462
444
  def run_sync(
463
445
  self,
464
446
  content: str | list[str],
447
+ *,
465
448
  attachments: str | list[str] | None = None,
466
449
  ) -> T | None:
467
450
  """Execute run_async synchronously with proper event loop handling.
@@ -509,6 +492,7 @@ class BaseResponse(Generic[T]):
509
492
  def run_streamed(
510
493
  self,
511
494
  content: str | list[str],
495
+ *,
512
496
  attachments: str | list[str] | None = None,
513
497
  ) -> T | None:
514
498
  """Execute run_async and await the result.
@@ -629,44 +613,32 @@ class BaseResponse(Generic[T]):
629
613
  """Serialize the message history to a JSON file.
630
614
 
631
615
  Saves the complete conversation history to disk. The target path
632
- is determined by filepath parameter, save_path from initialization,
633
- or data_path_fn if configured.
616
+ is determined by filepath parameter, or data_path if configured.
634
617
 
635
618
  Parameters
636
619
  ----------
637
620
  filepath : str, Path, or None, default None
638
- Optional explicit path for the JSON file. If None, uses save_path
639
- or constructs path from data_path_fn and session UUID.
621
+ Optional explicit path for the JSON file. If None, constructs
622
+ path from data_path and session UUID.
640
623
 
641
624
  Notes
642
625
  -----
643
- If no save location is configured (no filepath, save_path, or
644
- data_path_fn), the save operation is silently skipped.
626
+ If no filepath is provided and no data_path was configured during
627
+ initialization, the save operation is silently skipped.
645
628
 
646
629
  Examples
647
630
  --------
648
631
  >>> response.save("/path/to/session.json")
649
- >>> response.save() # Uses configured save_path or data_path
632
+ >>> response.save() # Uses data_path / uuid.json
650
633
  """
651
634
  if filepath is not None:
652
635
  target = Path(filepath)
653
- elif self._save_path is not None:
654
- if self._save_path.suffix == ".json":
655
- target = self._save_path
656
- else:
657
- filename = f"{str(self.uuid).lower()}.json"
658
- target = self._save_path / filename
659
- elif self._data_path_fn is not None and self._name is not None:
660
- filename = f"{str(self.uuid).lower()}.json"
661
- target = self.data_path / filename
662
636
  else:
663
- log(
664
- "Skipping save: no filepath, save_path, or data_path_fn configured.",
665
- level=logging.DEBUG,
666
- )
667
- return
637
+ filename = f"{str(self.uuid).lower()}.json"
638
+ target = self._data_path / self._name / filename
668
639
 
669
- self.messages.to_json_file(str(target))
640
+ checked = check_filepath(filepath=target)
641
+ self.messages.to_json_file(str(checked))
670
642
  log(f"Saved messages to {target}")
671
643
 
672
644
  def __repr__(self) -> str:
@@ -677,12 +649,9 @@ class BaseResponse(Generic[T]):
677
649
  str
678
650
  String showing class name, model, UUID, message count, and data path.
679
651
  """
680
- data_path = None
681
- if self._data_path_fn is not None and self._name is not None:
682
- data_path = self.data_path
683
652
  return (
684
653
  f"<{self.__class__.__name__}(model={self._model}, uuid={self.uuid}, "
685
- f"messages={len(self.messages.messages)}, data_path={data_path}>"
654
+ f"messages={len(self.messages.messages)}, data_path={self._data_path}>"
686
655
  )
687
656
 
688
657
  def __enter__(self) -> BaseResponse[T]:
@@ -15,6 +15,148 @@ TIn = TypeVar("TIn", bound="BaseStructure")
15
15
  TOut = TypeVar("TOut", bound="BaseStructure")
16
16
 
17
17
 
18
+ class ResponseRegistry:
19
+ """Registry for managing ResponseConfiguration instances.
20
+
21
+ Provides centralized storage and retrieval of response configurations,
22
+ enabling reusable response specs across the application. Configurations
23
+ are stored by name and can be retrieved or listed as needed.
24
+
25
+ Methods
26
+ -------
27
+ register(config)
28
+ Add a ResponseConfiguration to the registry.
29
+ get(name)
30
+ Retrieve a configuration by name.
31
+ list_names()
32
+ Return all registered configuration names.
33
+ clear()
34
+ Remove all registered configurations.
35
+
36
+ Examples
37
+ --------
38
+ >>> registry = ResponseRegistry()
39
+ >>> config = ResponseConfiguration(
40
+ ... name="test",
41
+ ... instructions="Test instructions",
42
+ ... tools=None,
43
+ ... input_structure=None,
44
+ ... output_structure=None
45
+ ... )
46
+ >>> registry.register(config)
47
+ >>> retrieved = registry.get("test")
48
+ >>> retrieved.name
49
+ 'test'
50
+ """
51
+
52
+ def __init__(self) -> None:
53
+ """Initialize an empty registry."""
54
+ self._configs: dict[str, ResponseConfiguration] = {}
55
+
56
+ def register(self, config: ResponseConfiguration) -> None:
57
+ """Add a ResponseConfiguration to the registry.
58
+
59
+ Parameters
60
+ ----------
61
+ config : ResponseConfiguration
62
+ Configuration to register.
63
+
64
+ Raises
65
+ ------
66
+ ValueError
67
+ If a configuration with the same name is already registered.
68
+
69
+ Examples
70
+ --------
71
+ >>> registry = ResponseRegistry()
72
+ >>> config = ResponseConfiguration(...)
73
+ >>> registry.register(config)
74
+ """
75
+ if config.name in self._configs:
76
+ raise ValueError(
77
+ f"Configuration '{config.name}' is already registered. "
78
+ "Use a unique name or clear the registry first."
79
+ )
80
+ self._configs[config.name] = config
81
+
82
+ def get(self, name: str) -> ResponseConfiguration:
83
+ """Retrieve a configuration by name.
84
+
85
+ Parameters
86
+ ----------
87
+ name : str
88
+ Configuration name to look up.
89
+
90
+ Returns
91
+ -------
92
+ ResponseConfiguration
93
+ The registered configuration.
94
+
95
+ Raises
96
+ ------
97
+ KeyError
98
+ If no configuration with the given name exists.
99
+
100
+ Examples
101
+ --------
102
+ >>> registry = ResponseRegistry()
103
+ >>> config = registry.get("test")
104
+ """
105
+ if name not in self._configs:
106
+ raise KeyError(
107
+ f"No configuration named '{name}' found. "
108
+ f"Available: {list(self._configs.keys())}"
109
+ )
110
+ return self._configs[name]
111
+
112
+ def list_names(self) -> list[str]:
113
+ """Return all registered configuration names.
114
+
115
+ Returns
116
+ -------
117
+ list[str]
118
+ Sorted list of configuration names.
119
+
120
+ Examples
121
+ --------
122
+ >>> registry = ResponseRegistry()
123
+ >>> registry.list_names()
124
+ []
125
+ """
126
+ return sorted(self._configs.keys())
127
+
128
+ def clear(self) -> None:
129
+ """Remove all registered configurations.
130
+
131
+ Examples
132
+ --------
133
+ >>> registry = ResponseRegistry()
134
+ >>> registry.clear()
135
+ """
136
+ self._configs.clear()
137
+
138
+
139
+ # Global default registry instance
140
+ _default_registry = ResponseRegistry()
141
+
142
+
143
+ def get_default_registry() -> ResponseRegistry:
144
+ """Return the global default registry instance.
145
+
146
+ Returns
147
+ -------
148
+ ResponseRegistry
149
+ Singleton registry for application-wide configuration storage.
150
+
151
+ Examples
152
+ --------
153
+ >>> registry = get_default_registry()
154
+ >>> config = ResponseConfiguration(...)
155
+ >>> registry.register(config)
156
+ """
157
+ return _default_registry
158
+
159
+
18
160
  @dataclass(frozen=True, slots=True)
19
161
  class ResponseConfiguration(Generic[TIn, TOut]):
20
162
  """
@@ -155,6 +155,7 @@ class ResponseMessages(JSONSerializable):
155
155
  def add_assistant_message(
156
156
  self,
157
157
  content: ResponseOutputMessage,
158
+ *,
158
159
  metadata: dict[str, str | float | bool],
159
160
  ) -> None:
160
161
  """Append an assistant message to the conversation.
@@ -94,17 +94,20 @@ class ResponseToolCall:
94
94
  return function_call, function_call_output
95
95
 
96
96
 
97
- def parse_tool_arguments(arguments: str) -> dict:
97
+ def parse_tool_arguments(arguments: str, tool_name: str) -> dict:
98
98
  """Parse tool call arguments with fallback for malformed JSON.
99
99
 
100
100
  Attempts to parse arguments as JSON first, then falls back to
101
101
  ast.literal_eval for cases where the OpenAI API returns minor
102
102
  formatting issues like single quotes instead of double quotes.
103
+ Provides clear error context including tool name and raw payload.
103
104
 
104
105
  Parameters
105
106
  ----------
106
107
  arguments : str
107
108
  Raw argument string from a tool call, expected to be JSON.
109
+ tool_name : str
110
+ Tool name for improved error context (required).
108
111
 
109
112
  Returns
110
113
  -------
@@ -115,13 +118,14 @@ def parse_tool_arguments(arguments: str) -> dict:
115
118
  ------
116
119
  ValueError
117
120
  If the arguments cannot be parsed as valid JSON or Python literal.
121
+ Error message includes tool name and payload excerpt for debugging.
118
122
 
119
123
  Examples
120
124
  --------
121
- >>> parse_tool_arguments('{"key": "value"}')
125
+ >>> parse_tool_arguments('{"key": "value"}', tool_name="search")
122
126
  {'key': 'value'}
123
127
 
124
- >>> parse_tool_arguments("{'key': 'value'}")
128
+ >>> parse_tool_arguments("{'key': 'value'}", tool_name="search")
125
129
  {'key': 'value'}
126
130
  """
127
131
  try:
@@ -130,4 +134,11 @@ def parse_tool_arguments(arguments: str) -> dict:
130
134
  try:
131
135
  return ast.literal_eval(arguments)
132
136
  except Exception as exc: # noqa: BLE001
133
- raise ValueError(f"Invalid JSON arguments: {arguments}") from exc
137
+ # Build informative error message with context
138
+ payload_preview = (
139
+ arguments[:100] + "..." if len(arguments) > 100 else arguments
140
+ )
141
+ raise ValueError(
142
+ f"Failed to parse tool arguments for tool '{tool_name}'. "
143
+ f"Raw payload: {payload_preview}"
144
+ ) from exc
@@ -15,7 +15,7 @@ from typing import Any, Callable, ParamSpec, TypeVar
15
15
  from openai import APIError, RateLimitError
16
16
 
17
17
  from openai_sdk_helpers.errors import AsyncExecutionError
18
- from openai_sdk_helpers.utils.core import log
18
+ from openai_sdk_helpers.logging_config import log
19
19
 
20
20
  P = ParamSpec("P")
21
21
  T = TypeVar("T")
@@ -22,7 +22,12 @@ from openai_sdk_helpers.streamlit_app import (
22
22
  _load_configuration,
23
23
  )
24
24
  from openai_sdk_helpers.structure.base import BaseStructure
25
- from openai_sdk_helpers.utils import coerce_jsonable, ensure_list, log
25
+ from openai_sdk_helpers.utils import (
26
+ coerce_jsonable,
27
+ customJSONEncoder,
28
+ ensure_list,
29
+ log,
30
+ )
26
31
 
27
32
 
28
33
  def _extract_assistant_text(response: BaseResponse[Any]) -> str:
@@ -90,10 +95,16 @@ def _render_summary(result: Any, response: BaseResponse[Any]) -> str:
90
95
  """
91
96
  if isinstance(result, BaseStructure):
92
97
  return result.print()
98
+ if isinstance(result, str):
99
+ return result
93
100
  if isinstance(result, dict):
94
- return json.dumps(result, indent=2)
101
+ return json.dumps(coerce_jsonable(result), indent=2, cls=customJSONEncoder)
95
102
  if result:
96
- return str(result)
103
+ coerced = coerce_jsonable(result)
104
+ try:
105
+ return json.dumps(coerced, indent=2, cls=customJSONEncoder)
106
+ except TypeError:
107
+ return str(result)
97
108
 
98
109
  fallback_text = _extract_assistant_text(response)
99
110
  if fallback_text:
@@ -6,7 +6,8 @@ from openai_sdk_helpers.config import OpenAISettings
6
6
  from openai_sdk_helpers.response.base import BaseResponse
7
7
  from openai_sdk_helpers.structure.web_search import WebSearchStructure
8
8
  from openai_sdk_helpers.structure.prompt import PromptStructure
9
- from openai_sdk_helpers.utils.core import customJSONEncoder
9
+ from openai_sdk_helpers.tools import ToolSpec, build_tool_definitions
10
+ from openai_sdk_helpers.utils import coerce_jsonable, customJSONEncoder
10
11
  from openai_sdk_helpers.environment import DEFAULT_MODEL
11
12
 
12
13
 
@@ -24,13 +25,18 @@ class StreamlitWebSearch(BaseResponse[WebSearchStructure]):
24
25
  if not settings.default_model:
25
26
  settings = settings.model_copy(update={"default_model": DEFAULT_MODEL})
26
27
  super().__init__(
28
+ name="streamlit_web_search",
27
29
  instructions="Perform web searches and generate reports.",
28
- tools=[
29
- PromptStructure.response_tool_definition(
30
- tool_name="perform_search",
31
- tool_description="Tool to perform web searches and generate reports.",
32
- )
33
- ],
30
+ tools=build_tool_definitions(
31
+ [
32
+ ToolSpec(
33
+ structure=PromptStructure,
34
+ tool_name="perform_search",
35
+ tool_description="Tool to perform web searches and generate reports.",
36
+ output_structure=WebSearchStructure,
37
+ )
38
+ ]
39
+ ),
34
40
  output_structure=WebSearchStructure,
35
41
  tool_handlers={"perform_search": perform_search},
36
42
  openai_settings=settings,
@@ -43,7 +49,8 @@ async def perform_search(tool) -> str:
43
49
  web_result = await WebAgentSearch(default_model=DEFAULT_MODEL).run_web_agent_async(
44
50
  structured_data.prompt
45
51
  )
46
- return json.dumps(web_result.to_json(), cls=customJSONEncoder)
52
+ payload = coerce_jsonable(web_result)
53
+ return json.dumps(payload, cls=customJSONEncoder)
47
54
 
48
55
 
49
56
  APP_CONFIG = {
@@ -86,6 +86,9 @@ __all__ = [
86
86
  "AgentEnum",
87
87
  "TaskStructure",
88
88
  "PlanStructure",
89
+ "create_plan",
90
+ "execute_task",
91
+ "execute_plan",
89
92
  "PromptStructure",
90
93
  "SummaryTopic",
91
94
  "SummaryStructure",