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
|
@@ -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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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.
|
|
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
|
|
237
|
-
"""Return the
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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,
|
|
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,
|
|
639
|
-
|
|
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
|
|
644
|
-
|
|
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
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
"""
|
|
@@ -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
|
-
|
|
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
|
openai_sdk_helpers/retry.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
52
|
+
payload = coerce_jsonable(web_result)
|
|
53
|
+
return json.dumps(payload, cls=customJSONEncoder)
|
|
47
54
|
|
|
48
55
|
|
|
49
56
|
APP_CONFIG = {
|