openai-sdk-helpers 0.1.2__py3-none-any.whl → 0.2.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.
- openai_sdk_helpers/agent/coordination.py +8 -0
- openai_sdk_helpers/response/base.py +14 -8
- openai_sdk_helpers/response/config.py +61 -1
- openai_sdk_helpers/response/messages.py +16 -0
- openai_sdk_helpers/streamlit_app/app.py +122 -27
- openai_sdk_helpers/utils/json_utils.py +128 -4
- {openai_sdk_helpers-0.1.2.dist-info → openai_sdk_helpers-0.2.0.dist-info}/METADATA +1 -1
- {openai_sdk_helpers-0.1.2.dist-info → openai_sdk_helpers-0.2.0.dist-info}/RECORD +11 -11
- {openai_sdk_helpers-0.1.2.dist-info → openai_sdk_helpers-0.2.0.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.1.2.dist-info → openai_sdk_helpers-0.2.0.dist-info}/entry_points.txt +0 -0
- {openai_sdk_helpers-0.1.2.dist-info → openai_sdk_helpers-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -44,6 +44,14 @@ class CoordinatorAgent(AgentBase, JSONSerializable):
|
|
|
44
44
|
Return a JSON-serializable snapshot of stored project data.
|
|
45
45
|
save()
|
|
46
46
|
Persist the stored project data to a JSON file.
|
|
47
|
+
to_json()
|
|
48
|
+
Return a JSON-compatible dict representation (inherited from JSONSerializable).
|
|
49
|
+
to_json_file(filepath)
|
|
50
|
+
Write serialized JSON data to a file path (inherited from JSONSerializable).
|
|
51
|
+
from_json(data)
|
|
52
|
+
Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
|
|
53
|
+
from_json_file(filepath)
|
|
54
|
+
Load an instance from a JSON file (class method, inherited from JSONSerializable).
|
|
47
55
|
"""
|
|
48
56
|
|
|
49
57
|
def __init__(
|
|
@@ -126,10 +126,10 @@ class BaseResponse(Generic[T]):
|
|
|
126
126
|
instructions: str,
|
|
127
127
|
tools: list | None,
|
|
128
128
|
output_structure: type[T] | None,
|
|
129
|
-
tool_handlers: dict[str, ToolHandler],
|
|
130
|
-
openai_settings: OpenAISettings,
|
|
131
129
|
system_vector_store: list[str] | None = None,
|
|
132
130
|
data_path: Path | str | None = None,
|
|
131
|
+
tool_handlers: dict[str, ToolHandler] | None = None,
|
|
132
|
+
openai_settings: OpenAISettings | None = None,
|
|
133
133
|
) -> None:
|
|
134
134
|
"""Initialize a response session with OpenAI configuration.
|
|
135
135
|
|
|
@@ -150,18 +150,19 @@ class BaseResponse(Generic[T]):
|
|
|
150
150
|
Structure class used to parse tool call outputs. When provided,
|
|
151
151
|
the schema is automatically generated using the structure's
|
|
152
152
|
response_format() method. Pass None for unstructured responses.
|
|
153
|
-
tool_handlers : dict[str, ToolHandler]
|
|
154
|
-
Mapping from tool names to callable handlers. Each handler receives
|
|
155
|
-
a ResponseFunctionToolCall and returns a string or any serializable
|
|
156
|
-
result.
|
|
157
|
-
openai_settings : OpenAISettings
|
|
158
|
-
Fully configured OpenAI settings with API key and default model.
|
|
159
153
|
system_vector_store : list[str] or None, default None
|
|
160
154
|
Optional list of vector store names to attach as system context.
|
|
161
155
|
data_path : Path, str, or None, default None
|
|
162
156
|
Optional absolute directory path for storing artifacts. If not provided,
|
|
163
157
|
defaults to get_data_path(class_name). Session files are saved as
|
|
164
158
|
data_path / uuid.json.
|
|
159
|
+
tool_handlers : dict[str, ToolHandler] or None, default None
|
|
160
|
+
Mapping from tool names to callable handlers. Each handler receives
|
|
161
|
+
a ResponseFunctionToolCall and returns a string or any serializable
|
|
162
|
+
result. Defaults to an empty dict when not provided.
|
|
163
|
+
openai_settings : OpenAISettings or None, default None
|
|
164
|
+
Fully configured OpenAI settings with API key and default model.
|
|
165
|
+
Required for normal operation.
|
|
165
166
|
|
|
166
167
|
Raises
|
|
167
168
|
------
|
|
@@ -184,6 +185,11 @@ class BaseResponse(Generic[T]):
|
|
|
184
185
|
... openai_settings=settings,
|
|
185
186
|
... )
|
|
186
187
|
"""
|
|
188
|
+
if tool_handlers is None:
|
|
189
|
+
tool_handlers = {}
|
|
190
|
+
if openai_settings is None:
|
|
191
|
+
raise ValueError("openai_settings is required")
|
|
192
|
+
|
|
187
193
|
self._tool_handlers = tool_handlers
|
|
188
194
|
self._name = name
|
|
189
195
|
|
|
@@ -10,6 +10,8 @@ from openai.types.responses.response_text_config_param import ResponseTextConfig
|
|
|
10
10
|
from ..config import OpenAISettings
|
|
11
11
|
from ..structure.base import BaseStructure
|
|
12
12
|
from ..response.base import BaseResponse, ToolHandler
|
|
13
|
+
from ..utils import JSONSerializable
|
|
14
|
+
from ..utils.path_utils import ensure_directory
|
|
13
15
|
|
|
14
16
|
TIn = TypeVar("TIn", bound="BaseStructure")
|
|
15
17
|
TOut = TypeVar("TOut", bound="BaseStructure")
|
|
@@ -135,6 +137,45 @@ class ResponseRegistry:
|
|
|
135
137
|
"""
|
|
136
138
|
self._configs.clear()
|
|
137
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
|
+
|
|
138
179
|
|
|
139
180
|
# Global default registry instance
|
|
140
181
|
_default_registry = ResponseRegistry()
|
|
@@ -158,12 +199,13 @@ def get_default_registry() -> ResponseRegistry:
|
|
|
158
199
|
|
|
159
200
|
|
|
160
201
|
@dataclass(frozen=True, slots=True)
|
|
161
|
-
class ResponseConfiguration(Generic[TIn, TOut]):
|
|
202
|
+
class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
|
|
162
203
|
"""
|
|
163
204
|
Represent an immutable configuration describing input and output structures.
|
|
164
205
|
|
|
165
206
|
Encapsulate all metadata required to define how a request is interpreted and
|
|
166
207
|
how a response is structured, while enforcing strict type and runtime safety.
|
|
208
|
+
Inherits from JSONSerializable to support serialization to JSON format.
|
|
167
209
|
|
|
168
210
|
Parameters
|
|
169
211
|
----------
|
|
@@ -181,6 +223,12 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
181
223
|
Structure class used to format or validate output. Schema is
|
|
182
224
|
automatically generated from this structure. Must subclass
|
|
183
225
|
BaseStructure. Default is None.
|
|
226
|
+
system_vector_store : list[str], optional
|
|
227
|
+
Optional list of vector store names to attach as system context.
|
|
228
|
+
Default is None.
|
|
229
|
+
data_path : Path, str, or None, optional
|
|
230
|
+
Optional absolute directory path for storing artifacts. If not provided,
|
|
231
|
+
defaults to get_data_path(class_name). Default is None.
|
|
184
232
|
|
|
185
233
|
Raises
|
|
186
234
|
------
|
|
@@ -201,6 +249,14 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
201
249
|
Validate configuration invariants and enforce BaseStructure subclassing.
|
|
202
250
|
instructions_text
|
|
203
251
|
Return the resolved instruction content as a string.
|
|
252
|
+
to_json()
|
|
253
|
+
Return a JSON-compatible dict representation (inherited from JSONSerializable).
|
|
254
|
+
to_json_file(filepath)
|
|
255
|
+
Write serialized JSON data to a file path (inherited from JSONSerializable).
|
|
256
|
+
from_json(data)
|
|
257
|
+
Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
|
|
258
|
+
from_json_file(filepath)
|
|
259
|
+
Load an instance from a JSON file (class method, inherited from JSONSerializable).
|
|
204
260
|
|
|
205
261
|
Examples
|
|
206
262
|
--------
|
|
@@ -219,6 +275,8 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
219
275
|
tools: Optional[list]
|
|
220
276
|
input_structure: Optional[Type[TIn]]
|
|
221
277
|
output_structure: Optional[Type[TOut]]
|
|
278
|
+
system_vector_store: Optional[list[str]] = None
|
|
279
|
+
data_path: Optional[Path | str] = None
|
|
222
280
|
|
|
223
281
|
def __post_init__(self) -> None:
|
|
224
282
|
"""
|
|
@@ -328,6 +386,8 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
328
386
|
instructions=instructions,
|
|
329
387
|
tools=self.tools,
|
|
330
388
|
output_structure=self.output_structure,
|
|
389
|
+
system_vector_store=self.system_vector_store,
|
|
390
|
+
data_path=self.data_path,
|
|
331
391
|
tool_handlers=tool_handlers,
|
|
332
392
|
openai_settings=openai_settings,
|
|
333
393
|
)
|
|
@@ -50,6 +50,14 @@ class ResponseMessage(JSONSerializable):
|
|
|
50
50
|
-------
|
|
51
51
|
to_openai_format()
|
|
52
52
|
Return the message content in OpenAI API format.
|
|
53
|
+
to_json()
|
|
54
|
+
Return a JSON-compatible dict representation (inherited from JSONSerializable).
|
|
55
|
+
to_json_file(filepath)
|
|
56
|
+
Write serialized JSON data to a file path (inherited from JSONSerializable).
|
|
57
|
+
from_json(data)
|
|
58
|
+
Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
|
|
59
|
+
from_json_file(filepath)
|
|
60
|
+
Load an instance from a JSON file (class method, inherited from JSONSerializable).
|
|
53
61
|
"""
|
|
54
62
|
|
|
55
63
|
role: str # "user", "assistant", "tool", etc.
|
|
@@ -113,6 +121,14 @@ class ResponseMessages(JSONSerializable):
|
|
|
113
121
|
Return the most recent tool message or None.
|
|
114
122
|
get_last_user_message()
|
|
115
123
|
Return the most recent user message or None.
|
|
124
|
+
to_json()
|
|
125
|
+
Return a JSON-compatible dict representation (inherited from JSONSerializable).
|
|
126
|
+
to_json_file(filepath)
|
|
127
|
+
Write serialized JSON data to a file path (inherited from JSONSerializable).
|
|
128
|
+
from_json(data)
|
|
129
|
+
Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
|
|
130
|
+
from_json_file(filepath)
|
|
131
|
+
Load an instance from a JSON file (class method, inherited from JSONSerializable).
|
|
116
132
|
"""
|
|
117
133
|
|
|
118
134
|
messages: list[ResponseMessage] = field(default_factory=list)
|
|
@@ -8,6 +8,7 @@ rendering, response execution, and resource cleanup.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import os
|
|
11
12
|
import tempfile
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import Any
|
|
@@ -30,22 +31,30 @@ from openai_sdk_helpers.utils import (
|
|
|
30
31
|
log,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
|
-
# Supported file extensions for OpenAI Assistants file search
|
|
34
|
+
# Supported file extensions for OpenAI Assistants file search and vision
|
|
34
35
|
SUPPORTED_FILE_EXTENSIONS = (
|
|
35
36
|
".csv",
|
|
36
37
|
".docx",
|
|
38
|
+
".gif",
|
|
37
39
|
".html",
|
|
38
40
|
".json",
|
|
41
|
+
".jpeg",
|
|
42
|
+
".jpg",
|
|
39
43
|
".md",
|
|
40
44
|
".pdf",
|
|
45
|
+
".png",
|
|
41
46
|
".pptx",
|
|
42
47
|
".txt",
|
|
48
|
+
".webp",
|
|
43
49
|
".xlsx",
|
|
44
50
|
)
|
|
45
51
|
|
|
46
52
|
|
|
47
53
|
def _validate_file_type(filename: str) -> bool:
|
|
48
|
-
"""Check if a file has a supported extension for
|
|
54
|
+
"""Check if a file has a supported extension for upload.
|
|
55
|
+
|
|
56
|
+
Supports both document formats (for file search) and image formats
|
|
57
|
+
(for vision analysis).
|
|
49
58
|
|
|
50
59
|
Parameters
|
|
51
60
|
----------
|
|
@@ -61,6 +70,32 @@ def _validate_file_type(filename: str) -> bool:
|
|
|
61
70
|
return file_ext in SUPPORTED_FILE_EXTENSIONS
|
|
62
71
|
|
|
63
72
|
|
|
73
|
+
def _cleanup_temp_files(file_paths: list[str] | None = None) -> None:
|
|
74
|
+
"""Delete temporary files that were created for uploads.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
file_paths : list[str] or None, default None
|
|
79
|
+
Specific file paths to delete. If None, deletes all tracked
|
|
80
|
+
temporary files from session state.
|
|
81
|
+
|
|
82
|
+
Notes
|
|
83
|
+
-----
|
|
84
|
+
Silently ignores errors when deleting files that may have already
|
|
85
|
+
been removed or are inaccessible.
|
|
86
|
+
"""
|
|
87
|
+
paths_to_delete = file_paths or st.session_state.get("temp_file_paths", [])
|
|
88
|
+
for path in paths_to_delete:
|
|
89
|
+
try:
|
|
90
|
+
if os.path.exists(path):
|
|
91
|
+
os.remove(path)
|
|
92
|
+
except (OSError, IOError):
|
|
93
|
+
pass # Silently ignore if file already deleted or inaccessible
|
|
94
|
+
|
|
95
|
+
if file_paths is None:
|
|
96
|
+
st.session_state["temp_file_paths"] = []
|
|
97
|
+
|
|
98
|
+
|
|
64
99
|
def _extract_assistant_text(response: BaseResponse[Any]) -> str:
|
|
65
100
|
"""Extract the latest assistant message as readable text.
|
|
66
101
|
|
|
@@ -86,15 +121,33 @@ def _extract_assistant_text(response: BaseResponse[Any]) -> str:
|
|
|
86
121
|
if message is None:
|
|
87
122
|
return ""
|
|
88
123
|
|
|
124
|
+
# Check if the message content has output_text attribute
|
|
125
|
+
output_text = getattr(message.content, "output_text", None)
|
|
126
|
+
if output_text:
|
|
127
|
+
return str(output_text)
|
|
128
|
+
|
|
89
129
|
content = getattr(message.content, "content", None)
|
|
90
130
|
if content is None:
|
|
91
131
|
return ""
|
|
92
132
|
|
|
93
133
|
text_parts: list[str] = []
|
|
94
134
|
for part in ensure_list(content):
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
135
|
+
# Handle both dict-like parts and object-like parts
|
|
136
|
+
text_content = None
|
|
137
|
+
if hasattr(part, "text"):
|
|
138
|
+
text_content = getattr(part, "text", None)
|
|
139
|
+
elif isinstance(part, dict):
|
|
140
|
+
text_content = part.get("text")
|
|
141
|
+
|
|
142
|
+
if text_content:
|
|
143
|
+
# If text_content is a string, use it directly (dict-style)
|
|
144
|
+
if isinstance(text_content, str):
|
|
145
|
+
text_parts.append(text_content)
|
|
146
|
+
# If text_content is an object with a value attribute, extract that value (object-style)
|
|
147
|
+
else:
|
|
148
|
+
text_value = getattr(text_content, "value", None)
|
|
149
|
+
if text_value:
|
|
150
|
+
text_parts.append(text_value)
|
|
98
151
|
if text_parts:
|
|
99
152
|
return "\n\n".join(text_parts)
|
|
100
153
|
return ""
|
|
@@ -223,7 +276,8 @@ def _reset_chat(close_response: bool = True) -> None:
|
|
|
223
276
|
"""Clear conversation and optionally close the response session.
|
|
224
277
|
|
|
225
278
|
Saves the current conversation to disk, closes the response to clean
|
|
226
|
-
up resources, and clears the chat history from session state.
|
|
279
|
+
up resources, and clears the chat history from session state. Also
|
|
280
|
+
cleans up any temporary files that were created for uploads.
|
|
227
281
|
|
|
228
282
|
Parameters
|
|
229
283
|
----------
|
|
@@ -234,13 +288,17 @@ def _reset_chat(close_response: bool = True) -> None:
|
|
|
234
288
|
Notes
|
|
235
289
|
-----
|
|
236
290
|
This function mutates st.session_state in-place, clearing the
|
|
237
|
-
chat_history and
|
|
291
|
+
chat_history, response_instance, and temp_file_paths keys.
|
|
238
292
|
"""
|
|
239
293
|
response = st.session_state.get("response_instance")
|
|
240
294
|
if close_response and isinstance(response, BaseResponse):
|
|
241
295
|
filepath = f"./data/{response.name}.{response.uuid}.json"
|
|
242
296
|
response.save(filepath)
|
|
243
297
|
response.close()
|
|
298
|
+
|
|
299
|
+
# Clean up temporary files
|
|
300
|
+
_cleanup_temp_files()
|
|
301
|
+
|
|
244
302
|
st.session_state["chat_history"] = []
|
|
245
303
|
st.session_state.pop("response_instance", None)
|
|
246
304
|
|
|
@@ -249,7 +307,8 @@ def _init_session_state() -> None:
|
|
|
249
307
|
"""Initialize Streamlit session state for chat functionality.
|
|
250
308
|
|
|
251
309
|
Creates the chat_history list in session state if it doesn't exist,
|
|
252
|
-
enabling conversation persistence across Streamlit reruns.
|
|
310
|
+
enabling conversation persistence across Streamlit reruns. Also
|
|
311
|
+
initializes a list for tracking temporary file paths that need cleanup.
|
|
253
312
|
|
|
254
313
|
Notes
|
|
255
314
|
-----
|
|
@@ -258,8 +317,12 @@ def _init_session_state() -> None:
|
|
|
258
317
|
"""
|
|
259
318
|
if "chat_history" not in st.session_state:
|
|
260
319
|
st.session_state["chat_history"] = []
|
|
261
|
-
if "
|
|
262
|
-
st.session_state["
|
|
320
|
+
if "temp_file_paths" not in st.session_state:
|
|
321
|
+
st.session_state["temp_file_paths"] = []
|
|
322
|
+
if "current_attachments" not in st.session_state:
|
|
323
|
+
st.session_state["current_attachments"] = []
|
|
324
|
+
if "attachment_names" not in st.session_state:
|
|
325
|
+
st.session_state["attachment_names"] = []
|
|
263
326
|
|
|
264
327
|
|
|
265
328
|
def _render_chat_history() -> None:
|
|
@@ -293,7 +356,10 @@ def _render_chat_history() -> None:
|
|
|
293
356
|
|
|
294
357
|
|
|
295
358
|
def _handle_user_message(
|
|
296
|
-
prompt: str,
|
|
359
|
+
prompt: str,
|
|
360
|
+
config: StreamlitAppConfig,
|
|
361
|
+
attachment_paths: list[str] | None = None,
|
|
362
|
+
attachment_names: list[str] | None = None,
|
|
297
363
|
) -> None:
|
|
298
364
|
"""Process user input and generate assistant response.
|
|
299
365
|
|
|
@@ -309,6 +375,8 @@ def _handle_user_message(
|
|
|
309
375
|
Loaded configuration with response handler definition.
|
|
310
376
|
attachment_paths : list[str] or None, default None
|
|
311
377
|
Optional list of file paths to attach to the message.
|
|
378
|
+
attachment_names : list[str] or None, default None
|
|
379
|
+
Optional list of original filenames for display purposes.
|
|
312
380
|
|
|
313
381
|
Notes
|
|
314
382
|
-----
|
|
@@ -316,11 +384,15 @@ def _handle_user_message(
|
|
|
316
384
|
chat transcript rather than crashing the application. The function
|
|
317
385
|
triggers a Streamlit rerun after successful response generation.
|
|
318
386
|
"""
|
|
319
|
-
|
|
320
|
-
|
|
387
|
+
# Use provided display names or fall back to extracting from paths
|
|
388
|
+
display_names = (
|
|
389
|
+
attachment_names
|
|
390
|
+
if attachment_names
|
|
391
|
+
else [Path(p).name for p in attachment_paths] if attachment_paths else []
|
|
321
392
|
)
|
|
393
|
+
|
|
322
394
|
st.session_state["chat_history"].append(
|
|
323
|
-
{"role": "user", "content": prompt, "attachments":
|
|
395
|
+
{"role": "user", "content": prompt, "attachments": display_names}
|
|
324
396
|
)
|
|
325
397
|
try:
|
|
326
398
|
response = _get_response_instance(config)
|
|
@@ -388,40 +460,63 @@ def main(config_path: Path) -> None:
|
|
|
388
460
|
|
|
389
461
|
_render_chat_history()
|
|
390
462
|
|
|
391
|
-
# File uploader
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
463
|
+
# File uploader form - auto-clears on submit
|
|
464
|
+
with st.form("file_upload_form", clear_on_submit=True):
|
|
465
|
+
uploaded_files = st.file_uploader(
|
|
466
|
+
"Attach files (optional)",
|
|
467
|
+
accept_multiple_files=True,
|
|
468
|
+
help=f"Supported formats: {', '.join(sorted(SUPPORTED_FILE_EXTENSIONS))}",
|
|
469
|
+
)
|
|
470
|
+
submit_files = st.form_submit_button("Attach files")
|
|
398
471
|
|
|
399
|
-
#
|
|
472
|
+
# Process uploaded files if form was submitted
|
|
400
473
|
attachment_paths: list[str] = []
|
|
401
|
-
|
|
474
|
+
original_filenames: list[str] = []
|
|
475
|
+
if submit_files and uploaded_files:
|
|
402
476
|
invalid_files = []
|
|
403
477
|
for uploaded_file in uploaded_files:
|
|
404
478
|
if not _validate_file_type(uploaded_file.name):
|
|
405
479
|
invalid_files.append(uploaded_file.name)
|
|
406
480
|
continue
|
|
481
|
+
|
|
482
|
+
# Create temporary file with the uploaded content
|
|
407
483
|
with tempfile.NamedTemporaryFile(
|
|
408
484
|
delete=False, suffix=Path(uploaded_file.name).suffix
|
|
409
485
|
) as tmp_file:
|
|
410
486
|
tmp_file.write(uploaded_file.getbuffer())
|
|
487
|
+
tmp_file.flush()
|
|
411
488
|
attachment_paths.append(tmp_file.name)
|
|
489
|
+
original_filenames.append(uploaded_file.name)
|
|
490
|
+
# Track for cleanup
|
|
491
|
+
if tmp_file.name not in st.session_state.get("temp_file_paths", []):
|
|
492
|
+
st.session_state["temp_file_paths"].append(tmp_file.name)
|
|
412
493
|
|
|
413
494
|
if invalid_files:
|
|
414
495
|
st.warning(
|
|
415
|
-
f"⚠️ Unsupported file types
|
|
416
|
-
f"Supported
|
|
496
|
+
f"⚠️ Unsupported file types: {', '.join(invalid_files)}. "
|
|
497
|
+
f"Supported: {', '.join(sorted(SUPPORTED_FILE_EXTENSIONS))}"
|
|
417
498
|
)
|
|
418
499
|
if attachment_paths:
|
|
419
|
-
st.
|
|
500
|
+
st.session_state["current_attachments"] = attachment_paths
|
|
501
|
+
st.session_state["attachment_names"] = original_filenames
|
|
502
|
+
st.info(f"📎 {len(attachment_paths)} file(s) attached")
|
|
503
|
+
|
|
504
|
+
# Get attachment paths from session state if they were previously attached
|
|
505
|
+
attachment_paths = st.session_state.get("current_attachments", [])
|
|
506
|
+
attachment_display_names = st.session_state.get("attachment_names", [])
|
|
507
|
+
if attachment_paths:
|
|
508
|
+
st.caption(f"Ready to send: {', '.join(attachment_display_names)}")
|
|
420
509
|
|
|
421
510
|
prompt = st.chat_input("Message the assistant")
|
|
422
511
|
if prompt:
|
|
512
|
+
# Clear attachments before rerun to prevent them from being sent again
|
|
513
|
+
st.session_state["current_attachments"] = []
|
|
514
|
+
st.session_state["attachment_names"] = []
|
|
423
515
|
_handle_user_message(
|
|
424
|
-
prompt,
|
|
516
|
+
prompt,
|
|
517
|
+
config,
|
|
518
|
+
attachment_paths or None,
|
|
519
|
+
attachment_display_names or None,
|
|
425
520
|
)
|
|
426
521
|
|
|
427
522
|
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from dataclasses import asdict, is_dataclass
|
|
6
|
+
from dataclasses import asdict, fields, is_dataclass
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
|
|
11
11
|
|
|
12
12
|
from .path_utils import check_filepath
|
|
13
13
|
|
|
14
|
+
T = TypeVar("T", bound="JSONSerializable")
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
def _to_jsonable(value: Any) -> Any:
|
|
16
18
|
"""Convert common helper types to JSON-serializable forms."""
|
|
@@ -65,7 +67,19 @@ class customJSONEncoder(json.JSONEncoder):
|
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
class JSONSerializable:
|
|
68
|
-
"""Mixin for classes that can be serialized to JSON.
|
|
70
|
+
"""Mixin for classes that can be serialized to and from JSON.
|
|
71
|
+
|
|
72
|
+
Methods
|
|
73
|
+
-------
|
|
74
|
+
to_json()
|
|
75
|
+
Return a JSON-compatible dict representation.
|
|
76
|
+
to_json_file(filepath)
|
|
77
|
+
Write serialized JSON data to a file path.
|
|
78
|
+
from_json(data)
|
|
79
|
+
Create an instance from a JSON-compatible dict (class method).
|
|
80
|
+
from_json_file(filepath)
|
|
81
|
+
Load an instance from a JSON file (class method).
|
|
82
|
+
"""
|
|
69
83
|
|
|
70
84
|
def to_json(self) -> dict[str, Any]:
|
|
71
85
|
"""Return a JSON-compatible dict representation."""
|
|
@@ -77,7 +91,18 @@ class JSONSerializable:
|
|
|
77
91
|
return _to_jsonable(self.__dict__)
|
|
78
92
|
|
|
79
93
|
def to_json_file(self, filepath: str | Path) -> str:
|
|
80
|
-
"""Write serialized JSON data to a file path.
|
|
94
|
+
"""Write serialized JSON data to a file path.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
filepath : str or Path
|
|
99
|
+
Path where the JSON file will be written.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
str
|
|
104
|
+
Absolute path to the written file.
|
|
105
|
+
"""
|
|
81
106
|
target = Path(filepath)
|
|
82
107
|
check_filepath(fullfilepath=str(target))
|
|
83
108
|
with open(target, "w", encoding="utf-8") as handle:
|
|
@@ -90,6 +115,105 @@ class JSONSerializable:
|
|
|
90
115
|
)
|
|
91
116
|
return str(target)
|
|
92
117
|
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_json(cls: type[T], data: dict[str, Any]) -> T:
|
|
120
|
+
"""Create an instance from a JSON-compatible dict.
|
|
121
|
+
|
|
122
|
+
For dataclasses, this reconstructs Path objects and passes the
|
|
123
|
+
dict keys directly as constructor arguments.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
data : dict[str, Any]
|
|
128
|
+
JSON-compatible dictionary containing the instance data.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
T
|
|
133
|
+
New instance of the class.
|
|
134
|
+
|
|
135
|
+
Examples
|
|
136
|
+
--------
|
|
137
|
+
>>> json_data = {"name": "test", "path": "/tmp/data"}
|
|
138
|
+
>>> instance = MyClass.from_json(json_data)
|
|
139
|
+
"""
|
|
140
|
+
if is_dataclass(cls):
|
|
141
|
+
# Get resolved field types using get_type_hints
|
|
142
|
+
try:
|
|
143
|
+
field_types = get_type_hints(cls)
|
|
144
|
+
except Exception:
|
|
145
|
+
# Fallback to raw annotations if get_type_hints fails
|
|
146
|
+
field_types = {f.name: f.type for f in fields(cls)}
|
|
147
|
+
|
|
148
|
+
converted_data = {}
|
|
149
|
+
|
|
150
|
+
for key, value in data.items():
|
|
151
|
+
if key in field_types:
|
|
152
|
+
field_type = field_types[key]
|
|
153
|
+
|
|
154
|
+
# Check if this field should be converted to Path
|
|
155
|
+
should_convert_to_path = False
|
|
156
|
+
|
|
157
|
+
if field_type is Path:
|
|
158
|
+
should_convert_to_path = True
|
|
159
|
+
else:
|
|
160
|
+
# Handle Union/Optional types
|
|
161
|
+
origin = get_origin(field_type)
|
|
162
|
+
if origin is Union:
|
|
163
|
+
type_args = get_args(field_type)
|
|
164
|
+
# Check if Path is one of the union types
|
|
165
|
+
if Path in type_args:
|
|
166
|
+
should_convert_to_path = True
|
|
167
|
+
|
|
168
|
+
# Convert string to Path if needed
|
|
169
|
+
if (
|
|
170
|
+
should_convert_to_path
|
|
171
|
+
and value is not None
|
|
172
|
+
and isinstance(value, str)
|
|
173
|
+
):
|
|
174
|
+
converted_data[key] = Path(value)
|
|
175
|
+
else:
|
|
176
|
+
converted_data[key] = value
|
|
177
|
+
else:
|
|
178
|
+
converted_data[key] = value
|
|
179
|
+
|
|
180
|
+
return cls(**converted_data) # type: ignore[return-value]
|
|
181
|
+
|
|
182
|
+
# For non-dataclass types, try to instantiate with data as kwargs
|
|
183
|
+
return cls(**data) # type: ignore[return-value]
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_json_file(cls: type[T], filepath: str | Path) -> T:
|
|
187
|
+
"""Load an instance from a JSON file.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
filepath : str or Path
|
|
192
|
+
Path to the JSON file to load.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
T
|
|
197
|
+
New instance of the class loaded from the file.
|
|
198
|
+
|
|
199
|
+
Raises
|
|
200
|
+
------
|
|
201
|
+
FileNotFoundError
|
|
202
|
+
If the file does not exist.
|
|
203
|
+
|
|
204
|
+
Examples
|
|
205
|
+
--------
|
|
206
|
+
>>> instance = MyClass.from_json_file("config.json")
|
|
207
|
+
"""
|
|
208
|
+
target = Path(filepath)
|
|
209
|
+
if not target.exists():
|
|
210
|
+
raise FileNotFoundError(f"JSON file not found: {target}")
|
|
211
|
+
|
|
212
|
+
with open(target, "r", encoding="utf-8") as handle:
|
|
213
|
+
data = json.load(handle)
|
|
214
|
+
|
|
215
|
+
return cls.from_json(data)
|
|
216
|
+
|
|
93
217
|
|
|
94
218
|
__all__ = [
|
|
95
219
|
"coerce_jsonable",
|
|
@@ -14,7 +14,7 @@ openai_sdk_helpers/types.py,sha256=xzldCRfwCZ3rZl18IBmfgA-PVdoZKSWNrlSIhirumSo,1
|
|
|
14
14
|
openai_sdk_helpers/agent/__init__.py,sha256=giowU8jke0z0h7FFUG9V6Vssja8AYwvJMQbiMb3s64k,960
|
|
15
15
|
openai_sdk_helpers/agent/base.py,sha256=8ZkW57vL8_fYzuLr6f9kMvBChYq5lN5vQ8MMJtcWD9s,11784
|
|
16
16
|
openai_sdk_helpers/agent/config.py,sha256=htqy5bcrJeMf3rIpRdL9CKlYwyQI4po420rcgR3i8XI,1971
|
|
17
|
-
openai_sdk_helpers/agent/coordination.py,sha256=
|
|
17
|
+
openai_sdk_helpers/agent/coordination.py,sha256=mAIEjWJ7xnagKssLCrOEn1oOp-XoqYpQOdRlZpUyw90,16610
|
|
18
18
|
openai_sdk_helpers/agent/prompt_utils.py,sha256=-1M66tqQxh9wWCFg6X-K7cCcqauca3yA04ZjvOpN3bA,337
|
|
19
19
|
openai_sdk_helpers/agent/runner.py,sha256=1_azIWx1Bcy7RRlEbizTD0LXBdYgof_tYMpDUcnJJuM,4164
|
|
20
20
|
openai_sdk_helpers/agent/summarizer.py,sha256=fH8AnYK_68ERf2U7mv0nwXL8KyhrluE-TDY_M5TCdD0,3266
|
|
@@ -33,15 +33,15 @@ openai_sdk_helpers/prompt/summarizer.jinja,sha256=jliSetWDISbql1EkWi1RB8-L_BXUg8
|
|
|
33
33
|
openai_sdk_helpers/prompt/translator.jinja,sha256=SZhW8ipEzM-9IA4wyS_r2wIMTAclWrilmk1s46njoL0,291
|
|
34
34
|
openai_sdk_helpers/prompt/validator.jinja,sha256=6t8q_IdxFd3mVBGX6SFKNOert1Wo3YpTOji2SNEbbtE,547
|
|
35
35
|
openai_sdk_helpers/response/__init__.py,sha256=td-HTSPLtl1d5AkUFZ0rrUBUfsacM_CGtZQNj1_GWB8,1886
|
|
36
|
-
openai_sdk_helpers/response/base.py,sha256=
|
|
37
|
-
openai_sdk_helpers/response/config.py,sha256=
|
|
36
|
+
openai_sdk_helpers/response/base.py,sha256=Y77LbnNB50-CG_KPYxM6XU0lIj9pYvAQt7FzekeHhF4,30176
|
|
37
|
+
openai_sdk_helpers/response/config.py,sha256=pZgjh5GOb4juy1Afnyn28QS3yIArByU2MAJIc1_i3WM,13237
|
|
38
38
|
openai_sdk_helpers/response/files.py,sha256=ANCoedNHXmpTXSaaGUvesAGq2DIUXT7SKZDCIJlXOv8,13226
|
|
39
|
-
openai_sdk_helpers/response/messages.py,sha256=
|
|
39
|
+
openai_sdk_helpers/response/messages.py,sha256=AbxLy2Q3sDHFLhhhoKfCcrRVlA8M7Ts9SuYx0PODi54,10061
|
|
40
40
|
openai_sdk_helpers/response/runner.py,sha256=Rf13cQGsR7sN9gA81Y5th1tfH2DCCAwQ6RMs3bVgjnk,4269
|
|
41
41
|
openai_sdk_helpers/response/tool_call.py,sha256=VYPvKUR-Ren0Y_nYS4jUSinhTyXKzFwQLxu-d3r_YuM,4506
|
|
42
42
|
openai_sdk_helpers/response/vector_store.py,sha256=MyHUu6P9ueNsd9erbBkyVqq3stLK6qVuehdvmFAHq9E,3074
|
|
43
43
|
openai_sdk_helpers/streamlit_app/__init__.py,sha256=RjJbnBDS5_YmAmxvaa3phB5u9UcXsXDEk_jMlY_pa5Q,793
|
|
44
|
-
openai_sdk_helpers/streamlit_app/app.py,sha256=
|
|
44
|
+
openai_sdk_helpers/streamlit_app/app.py,sha256=ejJGuy5WNuUXapxlZnYkPWz0gyQdF77l-gOFOzzDtyA,17402
|
|
45
45
|
openai_sdk_helpers/streamlit_app/config.py,sha256=EK6LWACo7YIkDko1oesvupOx56cTuWWnwnXRiu8EYbs,15986
|
|
46
46
|
openai_sdk_helpers/streamlit_app/streamlit_web_search.py,sha256=0RjB545dIvEeZiiLWM7C4CufbD3DITOWLZEVgxAL6mo,2812
|
|
47
47
|
openai_sdk_helpers/structure/__init__.py,sha256=QUvRdJMbKsumjwJdWq9ihfcOED4ZbJMBQbmA1nmYJVw,3339
|
|
@@ -64,7 +64,7 @@ openai_sdk_helpers/utils/async_utils.py,sha256=9KbPEVfi6IXdbwkTUE0h5DleK8TI7I6P_
|
|
|
64
64
|
openai_sdk_helpers/utils/coercion.py,sha256=Pq1u7tAbD7kTZ84lK-7Fb9CyYKKKQt4fypG5BlSI6oQ,3774
|
|
65
65
|
openai_sdk_helpers/utils/deprecation.py,sha256=VF0VDDegawYhsu5f-vE6dop9ob-jv8egxsm0KsPvP9E,4753
|
|
66
66
|
openai_sdk_helpers/utils/encoding.py,sha256=oDtlNGZ5p-edXiHW76REs-0-8NXkQNReKJdj6sHLkt8,4615
|
|
67
|
-
openai_sdk_helpers/utils/json_utils.py,sha256=
|
|
67
|
+
openai_sdk_helpers/utils/json_utils.py,sha256=Z-9AugR0CbNMLXcaJk1IJ-SwGNdo_d0a3r6MqP8rbYs,7089
|
|
68
68
|
openai_sdk_helpers/utils/output_validation.py,sha256=O9Adt-fxL5DtnMd1GuZ9E2YxX3yj4uzSZuBNKVH2GkI,12152
|
|
69
69
|
openai_sdk_helpers/utils/path_utils.py,sha256=qGGDpuDnY5EODOACzH23MYECQOE2rKhrQ3sbDvefwEg,1307
|
|
70
70
|
openai_sdk_helpers/utils/validation.py,sha256=ZjnZNOy5AoFlszRxarNol6YZwfgw6LnwPtkCekZmwAU,7826
|
|
@@ -72,8 +72,8 @@ openai_sdk_helpers/vector_storage/__init__.py,sha256=L5LxO09puh9_yBB9IDTvc1CvVkA
|
|
|
72
72
|
openai_sdk_helpers/vector_storage/cleanup.py,sha256=ImWIE-9lli-odD8qIARvmeaa0y8ZD4pYYP-kT0O3178,3552
|
|
73
73
|
openai_sdk_helpers/vector_storage/storage.py,sha256=1juu3Qq6hy33afvVfQeI5A35fQzIPjVZumZ-aP_MxhU,23305
|
|
74
74
|
openai_sdk_helpers/vector_storage/types.py,sha256=jTCcOYMeOpZWvcse0z4T3MVs-RBOPC-fqWTBeQrgafU,1639
|
|
75
|
-
openai_sdk_helpers-0.
|
|
76
|
-
openai_sdk_helpers-0.
|
|
77
|
-
openai_sdk_helpers-0.
|
|
78
|
-
openai_sdk_helpers-0.
|
|
79
|
-
openai_sdk_helpers-0.
|
|
75
|
+
openai_sdk_helpers-0.2.0.dist-info/METADATA,sha256=4uIwpDW0oqwdAqsf5uhb_GmutPvrlek0pbO6G4zYVXU,23557
|
|
76
|
+
openai_sdk_helpers-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
77
|
+
openai_sdk_helpers-0.2.0.dist-info/entry_points.txt,sha256=gEOD1ZeXe8d2OP-KzUlG-b_9D9yUZTCt-GFW3EDbIIY,63
|
|
78
|
+
openai_sdk_helpers-0.2.0.dist-info/licenses/LICENSE,sha256=CUhc1NrE50bs45tcXF7OcTQBKEvkUuLqeOHgrWQ5jaA,1067
|
|
79
|
+
openai_sdk_helpers-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|