openai-sdk-helpers 0.0.4__py3-none-any.whl → 0.0.6__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 (51) hide show
  1. openai_sdk_helpers/__init__.py +62 -0
  2. openai_sdk_helpers/agent/__init__.py +31 -0
  3. openai_sdk_helpers/agent/base.py +330 -0
  4. openai_sdk_helpers/agent/config.py +66 -0
  5. openai_sdk_helpers/agent/project_manager.py +511 -0
  6. openai_sdk_helpers/agent/prompt_utils.py +9 -0
  7. openai_sdk_helpers/agent/runner.py +215 -0
  8. openai_sdk_helpers/agent/summarizer.py +85 -0
  9. openai_sdk_helpers/agent/translator.py +139 -0
  10. openai_sdk_helpers/agent/utils.py +47 -0
  11. openai_sdk_helpers/agent/validation.py +97 -0
  12. openai_sdk_helpers/agent/vector_search.py +462 -0
  13. openai_sdk_helpers/agent/web_search.py +404 -0
  14. openai_sdk_helpers/config.py +153 -0
  15. openai_sdk_helpers/enums/__init__.py +7 -0
  16. openai_sdk_helpers/enums/base.py +29 -0
  17. openai_sdk_helpers/environment.py +27 -0
  18. openai_sdk_helpers/prompt/__init__.py +77 -0
  19. openai_sdk_helpers/prompt/summarizer.jinja +7 -0
  20. openai_sdk_helpers/prompt/translator.jinja +7 -0
  21. openai_sdk_helpers/prompt/validator.jinja +7 -0
  22. openai_sdk_helpers/py.typed +0 -0
  23. openai_sdk_helpers/response/__init__.py +18 -0
  24. openai_sdk_helpers/response/base.py +501 -0
  25. openai_sdk_helpers/response/messages.py +211 -0
  26. openai_sdk_helpers/response/runner.py +104 -0
  27. openai_sdk_helpers/response/tool_call.py +70 -0
  28. openai_sdk_helpers/structure/__init__.py +43 -0
  29. openai_sdk_helpers/structure/agent_blueprint.py +224 -0
  30. openai_sdk_helpers/structure/base.py +713 -0
  31. openai_sdk_helpers/structure/plan/__init__.py +13 -0
  32. openai_sdk_helpers/structure/plan/enum.py +64 -0
  33. openai_sdk_helpers/structure/plan/plan.py +253 -0
  34. openai_sdk_helpers/structure/plan/task.py +122 -0
  35. openai_sdk_helpers/structure/prompt.py +24 -0
  36. openai_sdk_helpers/structure/responses.py +132 -0
  37. openai_sdk_helpers/structure/summary.py +65 -0
  38. openai_sdk_helpers/structure/validation.py +47 -0
  39. openai_sdk_helpers/structure/vector_search.py +86 -0
  40. openai_sdk_helpers/structure/web_search.py +46 -0
  41. openai_sdk_helpers/utils/__init__.py +13 -0
  42. openai_sdk_helpers/utils/core.py +208 -0
  43. openai_sdk_helpers/vector_storage/__init__.py +15 -0
  44. openai_sdk_helpers/vector_storage/cleanup.py +91 -0
  45. openai_sdk_helpers/vector_storage/storage.py +501 -0
  46. openai_sdk_helpers/vector_storage/types.py +58 -0
  47. {openai_sdk_helpers-0.0.4.dist-info → openai_sdk_helpers-0.0.6.dist-info}/METADATA +1 -1
  48. openai_sdk_helpers-0.0.6.dist-info/RECORD +50 -0
  49. openai_sdk_helpers-0.0.4.dist-info/RECORD +0 -4
  50. {openai_sdk_helpers-0.0.4.dist-info → openai_sdk_helpers-0.0.6.dist-info}/WHEEL +0 -0
  51. {openai_sdk_helpers-0.0.4.dist-info → openai_sdk_helpers-0.0.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,7 @@
1
+ You are a professional translator.
2
+
3
+ Instructions:
4
+ - Rewrite the provided text into the requested target language.
5
+ - Preserve the intent and meaning of the original content.
6
+ - Keep terminology consistent and avoid embellishment.
7
+ - If the input text is empty, respond with "No text provided."
@@ -0,0 +1,7 @@
1
+ You are a meticulous safety validator that enforces product guardrails.
2
+
3
+ Instructions:
4
+ - Inspect the provided user_input and optional agent_output for policy violations, safety risks, or missing disclaimers.
5
+ - Highlight any prohibited content (violence, hate, sensitive data, PII) and note why it violates guardrails.
6
+ - Recommend concise remediation steps (redaction, refusal, rephrasing) and provide sanitized_output when possible.
7
+ - If everything is within guardrails, confirm both input_safe and output_safe are true and keep violations empty.
File without changes
@@ -0,0 +1,18 @@
1
+ """Shared response helpers for OpenAI interactions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import ResponseBase
6
+ from .messages import ResponseMessage, ResponseMessages
7
+ from .runner import run_sync, run_async, run_streamed
8
+ from .tool_call import ResponseToolCall
9
+
10
+ __all__ = [
11
+ "ResponseBase",
12
+ "ResponseMessage",
13
+ "ResponseMessages",
14
+ "run_sync",
15
+ "run_async",
16
+ "run_streamed",
17
+ "ResponseToolCall",
18
+ ]
@@ -0,0 +1,501 @@
1
+ """Base response handling for OpenAI interactions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ import json
8
+ import logging
9
+ import threading
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import (
13
+ Any,
14
+ Callable,
15
+ Generic,
16
+ List,
17
+ Optional,
18
+ Tuple,
19
+ Type,
20
+ TypeVar,
21
+ Union,
22
+ cast,
23
+ )
24
+
25
+ from openai import OpenAI
26
+ from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall
27
+ from openai.types.responses.response_input_file_param import ResponseInputFileParam
28
+ from openai.types.responses.response_input_message_content_list_param import (
29
+ ResponseInputMessageContentListParam,
30
+ )
31
+ from openai.types.responses.response_input_param import ResponseInputItemParam
32
+ from openai.types.responses.response_input_text_param import ResponseInputTextParam
33
+ from openai.types.responses.response_output_message import ResponseOutputMessage
34
+
35
+ from .messages import ResponseMessages
36
+ from ..structure import BaseStructure
37
+ from ..utils import ensure_list, log
38
+
39
+ T = TypeVar("T", bound=BaseStructure)
40
+ ToolHandler = Callable[[ResponseFunctionToolCall], Union[str, Any]]
41
+ ProcessContent = Callable[[str], Tuple[str, List[str]]]
42
+
43
+
44
+ class ResponseBase(Generic[T]):
45
+ """Manage OpenAI interactions for structured responses.
46
+
47
+ This base class handles input construction, OpenAI requests, tool calls,
48
+ and optional parsing into structured output models.
49
+
50
+ Methods
51
+ -------
52
+ run_async(content, attachments)
53
+ Generate a response asynchronously and return parsed output.
54
+ run_sync(content, attachments)
55
+ Synchronous wrapper around ``run_async``.
56
+ run_streamed(content, attachments)
57
+ Await ``run_async`` to mirror the agent API.
58
+ save(filepath)
59
+ Serialize the message history to disk.
60
+ close()
61
+ Clean up remote resources (vector stores).
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ *,
67
+ instructions: str,
68
+ tools: Optional[list],
69
+ schema: Optional[Any],
70
+ output_structure: Optional[Type[T]],
71
+ tool_handlers: dict[str, ToolHandler],
72
+ process_content: Optional[ProcessContent] = None,
73
+ module_name: Optional[str] = None,
74
+ vector_storage_cls: Optional[type] = None,
75
+ client: Optional[OpenAI] = None,
76
+ model: Optional[str] = None,
77
+ api_key: Optional[str] = None,
78
+ attachments: Optional[Union[Tuple[str, str], list[Tuple[str, str]]]] = None,
79
+ data_path_fn: Optional[Callable[[str], Path]] = None,
80
+ save_path: Optional[Path | str] = None,
81
+ ) -> None:
82
+ """Initialize a response session.
83
+
84
+ Parameters
85
+ ----------
86
+ instructions : str
87
+ System instructions for the OpenAI response.
88
+ tools : list or None
89
+ Tool definitions for the OpenAI request.
90
+ schema : object or None
91
+ Optional response schema configuration.
92
+ output_structure : type[BaseStructure] or None
93
+ Structure type used to parse tool call outputs.
94
+ tool_handlers : dict[str, ToolHandler]
95
+ Mapping of tool names to handler callables.
96
+ process_content : callable, optional
97
+ Callback that cleans input text and extracts attachments.
98
+ module_name : str, optional
99
+ Module name used to build the data path.
100
+ vector_storage_cls : type, optional
101
+ Vector storage class used for file uploads.
102
+ client : OpenAI or None, default=None
103
+ Optional pre-initialized OpenAI client.
104
+ model : str or None, default=None
105
+ Optional OpenAI model name override.
106
+ api_key : str or None, default=None
107
+ Optional OpenAI API key override.
108
+ attachments : tuple or list of tuples, optional
109
+ File attachments in the form ``(file_path, tool_type)``.
110
+ data_path_fn : callable or None, default=None
111
+ Function that maps ``module_name`` to a base data path.
112
+ save_path : Path | str or None, default=None
113
+ Optional path to a directory or file for persisted messages.
114
+
115
+ Raises
116
+ ------
117
+ ValueError
118
+ If API key or model is missing.
119
+ RuntimeError
120
+ If the OpenAI client fails to initialize.
121
+ """
122
+ self._tool_handlers = tool_handlers
123
+ self._process_content = process_content
124
+ self._module_name = module_name
125
+ self._vector_storage_cls = vector_storage_cls
126
+ self._data_path_fn = data_path_fn
127
+ self._save_path = Path(save_path) if save_path is not None else None
128
+ self._instructions = instructions
129
+ self._tools = tools if tools is not None else []
130
+ self._schema = schema
131
+ self._output_structure = output_structure
132
+
133
+ if client is None:
134
+ if api_key is None:
135
+ raise ValueError("OpenAI API key is required")
136
+ try:
137
+ self._client = OpenAI(api_key=api_key)
138
+ except Exception as exc:
139
+ raise RuntimeError("Failed to initialize OpenAI client") from exc
140
+ else:
141
+ self._client = client
142
+
143
+ self._model = model
144
+ if not self._model:
145
+ raise ValueError("OpenAI model is required")
146
+
147
+ self.uuid = uuid.uuid4()
148
+ self.name = self.__class__.__name__.lower()
149
+
150
+ system_content: ResponseInputMessageContentListParam = [
151
+ ResponseInputTextParam(type="input_text", text=instructions)
152
+ ]
153
+
154
+ self._system_vector_storage: Optional[Any] = None
155
+ self._user_vector_storage: Optional[Any] = None
156
+
157
+ if attachments:
158
+ if self._vector_storage_cls is None:
159
+ raise RuntimeError("vector_storage_cls is required for attachments.")
160
+ self.file_objects: dict[str, List[str]] = {}
161
+ storage_name = f"{self.__class__.__name__.lower()}_{self.name}_system"
162
+ self._system_vector_storage = self._vector_storage_cls(
163
+ store_name=storage_name, client=self._client, model=self._model
164
+ )
165
+ system_vector_storage = cast(Any, self._system_vector_storage)
166
+ for file_path, tool_type in attachments:
167
+ uploaded_file = system_vector_storage.upload_file(file_path=file_path)
168
+ self.file_objects.setdefault(tool_type, []).append(uploaded_file.id)
169
+
170
+ self.tool_resources = {}
171
+ required_tools = []
172
+
173
+ for tool_type, file_ids in self.file_objects.items():
174
+ required_tools.append({"type": tool_type})
175
+ self.tool_resources[tool_type] = {"file_ids": file_ids}
176
+ if tool_type == "file_search":
177
+ self.tool_resources[tool_type]["vector_store_ids"] = [
178
+ system_vector_storage.id
179
+ ]
180
+
181
+ existing_tool_types = {tool["type"] for tool in self._tools}
182
+ for tool in required_tools:
183
+ tool_type = tool["type"]
184
+ if tool_type == "file_search":
185
+ tool["vector_store_ids"] = [system_vector_storage.id]
186
+ if tool_type not in existing_tool_types:
187
+ self._tools.append(tool)
188
+
189
+ self.messages = ResponseMessages()
190
+ self.messages.add_system_message(content=system_content)
191
+ if self._save_path is not None or (
192
+ self._data_path_fn is not None and self._module_name is not None
193
+ ):
194
+ self.save()
195
+
196
+ @property
197
+ def data_path(self) -> Path:
198
+ """Return the directory used to persist artifacts for this session.
199
+
200
+ Returns
201
+ -------
202
+ Path
203
+ Absolute path for persisting response artifacts.
204
+ """
205
+ if self._data_path_fn is None or self._module_name is None:
206
+ raise RuntimeError(
207
+ "data_path_fn and module_name are required to build data paths."
208
+ )
209
+ base_path = self._data_path_fn(self._module_name)
210
+ return base_path / self.__class__.__name__.lower() / self.name
211
+
212
+ def _build_input(
213
+ self,
214
+ content: Union[str, List[str]],
215
+ attachments: Optional[List[str]] = None,
216
+ ) -> None:
217
+ """Build the list of input messages for the OpenAI request.
218
+
219
+ Parameters
220
+ ----------
221
+ content
222
+ String or list of strings to include as user messages.
223
+ attachments
224
+ Optional list of file paths to upload and attach.
225
+ """
226
+ contents = ensure_list(content)
227
+
228
+ for raw_content in contents:
229
+ if self._process_content is None:
230
+ processed_text, content_attachments = raw_content, []
231
+ else:
232
+ processed_text, content_attachments = self._process_content(raw_content)
233
+ input_content: List[
234
+ Union[ResponseInputTextParam, ResponseInputFileParam]
235
+ ] = [ResponseInputTextParam(type="input_text", text=processed_text)]
236
+
237
+ all_attachments = (attachments or []) + content_attachments
238
+
239
+ for file_path in all_attachments:
240
+ if self._user_vector_storage is None:
241
+ if self._vector_storage_cls is None:
242
+ raise RuntimeError(
243
+ "vector_storage_cls is required for attachments."
244
+ )
245
+ store_name = f"{self.__class__.__name__.lower()}_{self.name}_{self.uuid}_user"
246
+ self._user_vector_storage = self._vector_storage_cls(
247
+ store_name=store_name,
248
+ client=self._client,
249
+ model=self._model,
250
+ )
251
+ user_vector_storage = cast(Any, self._user_vector_storage)
252
+ if not any(
253
+ tool.get("type") == "file_search" for tool in self._tools
254
+ ):
255
+ self._tools.append(
256
+ {
257
+ "type": "file_search",
258
+ "vector_store_ids": [user_vector_storage.id],
259
+ }
260
+ )
261
+ else:
262
+ for tool in self._tools:
263
+ if tool.get("type") == "file_search":
264
+ if self._system_vector_storage is not None:
265
+ tool["vector_store_ids"] = [
266
+ cast(Any, self._system_vector_storage).id,
267
+ user_vector_storage.id,
268
+ ]
269
+ user_vector_storage = cast(Any, self._user_vector_storage)
270
+ uploaded_file = user_vector_storage.upload_file(file_path)
271
+ input_content.append(
272
+ ResponseInputFileParam(type="input_file", file_id=uploaded_file.id)
273
+ )
274
+
275
+ message = cast(
276
+ ResponseInputItemParam,
277
+ {"role": "user", "content": input_content},
278
+ )
279
+ self.messages.add_user_message(message)
280
+
281
+ async def run_async(
282
+ self,
283
+ content: Union[str, List[str]],
284
+ attachments: Optional[Union[str, List[str]]] = None,
285
+ ) -> Optional[T]:
286
+ """Generate a response asynchronously.
287
+
288
+ Parameters
289
+ ----------
290
+ content
291
+ Prompt text or list of texts.
292
+ attachments
293
+ Optional file path or list of paths to upload and attach.
294
+
295
+ Returns
296
+ -------
297
+ Optional[T]
298
+ Parsed response object or ``None``.
299
+
300
+ Raises
301
+ ------
302
+ RuntimeError
303
+ If the API returns no output or a tool handler errors.
304
+ ValueError
305
+ If no handler is found for a tool invoked by the API.
306
+ """
307
+ log(f"{self.__class__.__name__}::run_response")
308
+ parsed_result: Optional[T] = None
309
+
310
+ self._build_input(
311
+ content=content,
312
+ attachments=(ensure_list(attachments) if attachments else None),
313
+ )
314
+
315
+ kwargs = {
316
+ "input": self.messages.to_openai_payload(),
317
+ "model": self._model,
318
+ }
319
+ if self._schema is not None:
320
+ kwargs["text"] = self._schema
321
+
322
+ if self._tools:
323
+ kwargs["tools"] = self._tools
324
+ kwargs["tool_choice"] = "auto"
325
+ response = self._client.responses.create(**kwargs)
326
+
327
+ if not response.output:
328
+ log("No output returned from OpenAI.", level=logging.ERROR)
329
+ raise RuntimeError("No output returned from OpenAI.")
330
+
331
+ for response_output in response.output:
332
+ if isinstance(response_output, ResponseFunctionToolCall):
333
+ log(
334
+ f"Tool call detected. Executing {response_output.name}.",
335
+ level=logging.INFO,
336
+ )
337
+
338
+ tool_name = response_output.name
339
+ handler = self._tool_handlers.get(tool_name)
340
+
341
+ if handler is None:
342
+ log(
343
+ f"No handler found for tool '{tool_name}'",
344
+ level=logging.ERROR,
345
+ )
346
+ raise ValueError(f"No handler for tool: {tool_name}")
347
+
348
+ try:
349
+ if inspect.iscoroutinefunction(handler):
350
+ tool_result_json = await handler(response_output)
351
+ else:
352
+ tool_result_json = handler(response_output)
353
+ if isinstance(tool_result_json, str):
354
+ tool_result = json.loads(tool_result_json)
355
+ tool_output = tool_result_json
356
+ else:
357
+ tool_result = tool_result_json
358
+ tool_output = json.dumps(tool_result)
359
+ self.messages.add_tool_message(
360
+ content=response_output, output=tool_output
361
+ )
362
+ self.save()
363
+ except Exception as exc:
364
+ log(
365
+ f"Error executing tool handler '{tool_name}': {exc}",
366
+ level=logging.ERROR,
367
+ )
368
+ raise RuntimeError(f"Error in tool handler '{tool_name}': {exc}")
369
+
370
+ if self._output_structure:
371
+ output_dict = self._output_structure.from_raw_input(tool_result)
372
+ output_dict.console_print()
373
+ parsed_result = output_dict
374
+ else:
375
+ print(tool_result)
376
+ parsed_result = cast(T, tool_result)
377
+
378
+ if isinstance(response_output, ResponseOutputMessage):
379
+ self.messages.add_assistant_message(response_output, kwargs)
380
+ self.save()
381
+ if hasattr(response, "output_text") and response.output_text:
382
+ raw_text = response.output_text
383
+ log("No tool call. Parsing output_text.")
384
+ try:
385
+ output_dict = json.loads(raw_text)
386
+ if self._output_structure and self._schema:
387
+ return self._output_structure.from_raw_input(output_dict)
388
+ return output_dict
389
+ except Exception:
390
+ print(raw_text)
391
+ if parsed_result is not None:
392
+ return parsed_result
393
+ return None
394
+
395
+ def run_sync(
396
+ self,
397
+ content: Union[str, List[str]],
398
+ attachments: Optional[Union[str, List[str]]] = None,
399
+ ) -> Optional[T]:
400
+ """Run :meth:`run_response_async` synchronously."""
401
+
402
+ async def runner() -> Optional[T]:
403
+ return await self.run_async(content=content, attachments=attachments)
404
+
405
+ try:
406
+ asyncio.get_running_loop()
407
+ except RuntimeError:
408
+ return asyncio.run(runner())
409
+ result: Optional[T] = None
410
+
411
+ def _thread_func() -> None:
412
+ nonlocal result
413
+ result = asyncio.run(runner())
414
+
415
+ thread = threading.Thread(target=_thread_func)
416
+ thread.start()
417
+ thread.join()
418
+ return result
419
+
420
+ def run_streamed(
421
+ self,
422
+ content: Union[str, List[str]],
423
+ attachments: Optional[Union[str, List[str]]] = None,
424
+ ) -> Optional[T]:
425
+ """Generate a response asynchronously and return the awaited result.
426
+
427
+ Streaming is not yet supported for responses, so this helper simply
428
+ awaits :meth:`run_async` to mirror the agent API.
429
+
430
+ Parameters
431
+ ----------
432
+ content
433
+ Prompt text or list of texts.
434
+ attachments
435
+ Optional file path or list of paths to upload and attach.
436
+
437
+ Returns
438
+ -------
439
+ Optional[T]
440
+ Parsed response object or ``None``.
441
+ """
442
+ return asyncio.run(self.run_async(content=content, attachments=attachments))
443
+
444
+ def save(self, filepath: Optional[str | Path] = None) -> None:
445
+ """Serialize the message history to a JSON file."""
446
+ if filepath is not None:
447
+ target = Path(filepath)
448
+ elif self._save_path is not None:
449
+ if self._save_path.suffix == ".json":
450
+ target = self._save_path
451
+ else:
452
+ filename = f"{str(self.uuid).lower()}.json"
453
+ target = self._save_path / filename
454
+ elif self._data_path_fn is not None and self._module_name is not None:
455
+ filename = f"{str(self.uuid).lower()}.json"
456
+ target = self.data_path / filename
457
+ else:
458
+ log(
459
+ "Skipping save: no filepath, save_path, or data_path_fn configured.",
460
+ level=logging.DEBUG,
461
+ )
462
+ return
463
+
464
+ self.messages.to_json_file(str(target))
465
+ log(f"Saved messages to {target}")
466
+
467
+ def __repr__(self) -> str:
468
+ """Return an unambiguous representation including model and UUID."""
469
+ data_path = None
470
+ if self._data_path_fn is not None and self._module_name is not None:
471
+ data_path = self.data_path
472
+ return (
473
+ f"<{self.__class__.__name__}(model={self._model}, uuid={self.uuid}, "
474
+ f"messages={len(self.messages.messages)}, data_path={data_path}>"
475
+ )
476
+
477
+ def __enter__(self) -> "ResponseBase[T]":
478
+ """Enter the context manager for this response session."""
479
+ return self
480
+
481
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
482
+ """Exit the context manager and close remote resources."""
483
+ self.close()
484
+
485
+ def close(self) -> None:
486
+ """Delete remote vector stores and clean up the session."""
487
+ log(f"Closing session {self.uuid} for {self.__class__.__name__}")
488
+
489
+ try:
490
+ if self._user_vector_storage:
491
+ self._user_vector_storage.delete()
492
+ log("User vector store deleted.")
493
+ except Exception as exc:
494
+ log(f"Error deleting user vector store: {exc}", level=logging.WARNING)
495
+ try:
496
+ if self._system_vector_storage:
497
+ self._system_vector_storage.delete()
498
+ log("System vector store deleted.")
499
+ except Exception as exc:
500
+ log(f"Error deleting system vector store: {exc}", level=logging.WARNING)
501
+ log(f"Session {self.uuid} closed.")