openai-sdk-helpers 0.0.9__py3-none-any.whl → 0.1.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/__init__.py +24 -0
- openai_sdk_helpers/config.py +27 -29
- openai_sdk_helpers/prompt/base.py +55 -5
- openai_sdk_helpers/response/__init__.py +5 -2
- openai_sdk_helpers/response/config.py +142 -0
- openai_sdk_helpers/response/tool_call.py +15 -4
- openai_sdk_helpers/structure/__init__.py +3 -0
- openai_sdk_helpers/structure/plan/__init__.py +15 -1
- openai_sdk_helpers/structure/plan/helpers.py +172 -0
- openai_sdk_helpers/structure/plan/plan.py +13 -9
- openai_sdk_helpers/structure/plan/types.py +15 -0
- openai_sdk_helpers/tools.py +193 -0
- openai_sdk_helpers/utils/__init__.py +6 -2
- openai_sdk_helpers/utils/core.py +128 -0
- {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.0.dist-info}/METADATA +1 -1
- {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.0.dist-info}/RECORD +18 -15
- {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.0.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.0.dist-info}/licenses/LICENSE +0 -0
openai_sdk_helpers/__init__.py
CHANGED
|
@@ -46,6 +46,9 @@ from .structure import (
|
|
|
46
46
|
ExtendedSummaryStructure,
|
|
47
47
|
ValidationResultStructure,
|
|
48
48
|
AgentBlueprint,
|
|
49
|
+
create_plan,
|
|
50
|
+
execute_task,
|
|
51
|
+
execute_plan,
|
|
49
52
|
)
|
|
50
53
|
from .prompt import PromptRenderer
|
|
51
54
|
from .config import OpenAISettings
|
|
@@ -66,8 +69,19 @@ from .response import (
|
|
|
66
69
|
ResponseMessage,
|
|
67
70
|
ResponseMessages,
|
|
68
71
|
ResponseToolCall,
|
|
72
|
+
ResponseConfiguration,
|
|
73
|
+
ResponseRegistry,
|
|
74
|
+
get_default_registry,
|
|
75
|
+
parse_tool_arguments,
|
|
69
76
|
attach_vector_store,
|
|
70
77
|
)
|
|
78
|
+
from .tools import (
|
|
79
|
+
serialize_tool_result,
|
|
80
|
+
tool_handler_factory,
|
|
81
|
+
)
|
|
82
|
+
from .utils import (
|
|
83
|
+
build_openai_settings,
|
|
84
|
+
)
|
|
71
85
|
|
|
72
86
|
__all__ = [
|
|
73
87
|
# Async utilities
|
|
@@ -133,5 +147,15 @@ __all__ = [
|
|
|
133
147
|
"ResponseMessage",
|
|
134
148
|
"ResponseMessages",
|
|
135
149
|
"ResponseToolCall",
|
|
150
|
+
"ResponseConfiguration",
|
|
151
|
+
"ResponseRegistry",
|
|
152
|
+
"get_default_registry",
|
|
153
|
+
"parse_tool_arguments",
|
|
136
154
|
"attach_vector_store",
|
|
155
|
+
"serialize_tool_result",
|
|
156
|
+
"tool_handler_factory",
|
|
157
|
+
"build_openai_settings",
|
|
158
|
+
"create_plan",
|
|
159
|
+
"execute_task",
|
|
160
|
+
"execute_plan",
|
|
137
161
|
]
|
openai_sdk_helpers/config.py
CHANGED
|
@@ -141,39 +141,37 @@ class OpenAISettings(BaseModel):
|
|
|
141
141
|
ValueError
|
|
142
142
|
If OPENAI_API_KEY is not found in environment or dotenv file.
|
|
143
143
|
"""
|
|
144
|
-
env_file_values: Mapping[str, str | None]
|
|
144
|
+
env_file_values: Mapping[str, str | None] = {}
|
|
145
145
|
if dotenv_path is not None:
|
|
146
146
|
env_file_values = dotenv_values(dotenv_path)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
147
|
+
|
|
148
|
+
def first_non_none(*candidates: Any) -> Any:
|
|
149
|
+
for candidate in candidates:
|
|
150
|
+
if candidate is not None:
|
|
151
|
+
return candidate
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def resolve_value(override_key: str, env_var: str) -> Any:
|
|
155
|
+
if dotenv_path is not None:
|
|
156
|
+
return first_non_none(
|
|
157
|
+
overrides.get(override_key),
|
|
158
|
+
env_file_values.get(env_var),
|
|
159
|
+
os.getenv(env_var),
|
|
160
|
+
)
|
|
161
|
+
return first_non_none(
|
|
162
|
+
overrides.get(override_key),
|
|
163
|
+
os.getenv(env_var),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
timeout_raw = resolve_value("timeout", "OPENAI_TIMEOUT")
|
|
167
|
+
max_retries_raw = resolve_value("max_retries", "OPENAI_MAX_RETRIES")
|
|
160
168
|
|
|
161
169
|
values: dict[str, Any] = {
|
|
162
|
-
"api_key":
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
or os.getenv("OPENAI_ORG_ID"),
|
|
168
|
-
"project_id": overrides.get("project_id")
|
|
169
|
-
or env_file_values.get("OPENAI_PROJECT_ID")
|
|
170
|
-
or os.getenv("OPENAI_PROJECT_ID"),
|
|
171
|
-
"base_url": overrides.get("base_url")
|
|
172
|
-
or env_file_values.get("OPENAI_BASE_URL")
|
|
173
|
-
or os.getenv("OPENAI_BASE_URL"),
|
|
174
|
-
"default_model": overrides.get("default_model")
|
|
175
|
-
or env_file_values.get("OPENAI_MODEL")
|
|
176
|
-
or os.getenv("OPENAI_MODEL"),
|
|
170
|
+
"api_key": resolve_value("api_key", "OPENAI_API_KEY"),
|
|
171
|
+
"org_id": resolve_value("org_id", "OPENAI_ORG_ID"),
|
|
172
|
+
"project_id": resolve_value("project_id", "OPENAI_PROJECT_ID"),
|
|
173
|
+
"base_url": resolve_value("base_url", "OPENAI_BASE_URL"),
|
|
174
|
+
"default_model": resolve_value("default_model", "OPENAI_MODEL"),
|
|
177
175
|
"timeout": coerce_optional_float(timeout_raw),
|
|
178
176
|
"max_retries": coerce_optional_int(max_retries_raw),
|
|
179
177
|
"extra_client_kwargs": coerce_dict(overrides.get("extra_client_kwargs")),
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This module provides the PromptRenderer class for loading and rendering
|
|
4
4
|
Jinja2 templates with context variables. Templates can be loaded from a
|
|
5
|
-
specified directory or by absolute path.
|
|
5
|
+
specified directory or by absolute path. Includes template caching for
|
|
6
|
+
improved performance.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from __future__ import annotations
|
|
9
10
|
|
|
10
11
|
import warnings
|
|
12
|
+
from functools import lru_cache
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Any
|
|
13
15
|
|
|
@@ -23,7 +25,8 @@ class PromptRenderer:
|
|
|
23
25
|
|
|
24
26
|
Loads and renders Jinja2 templates from a base directory or by absolute
|
|
25
27
|
path. The renderer supports variable substitution, template inheritance,
|
|
26
|
-
and all standard Jinja2 features for creating dynamic prompts.
|
|
28
|
+
and all standard Jinja2 features for creating dynamic prompts. Templates
|
|
29
|
+
are cached using LRU cache for improved performance on repeated renders.
|
|
27
30
|
|
|
28
31
|
Templates are loaded from a base directory (defaulting to the built-in
|
|
29
32
|
prompt package directory) or can be specified with absolute paths.
|
|
@@ -38,6 +41,8 @@ class PromptRenderer:
|
|
|
38
41
|
-------
|
|
39
42
|
render(template_path, context=None)
|
|
40
43
|
Render a Jinja2 template with the given context variables.
|
|
44
|
+
clear_cache()
|
|
45
|
+
Clear the template compilation cache.
|
|
41
46
|
|
|
42
47
|
Examples
|
|
43
48
|
--------
|
|
@@ -103,12 +108,30 @@ class PromptRenderer:
|
|
|
103
108
|
autoescape=False, # Prompts are plain text
|
|
104
109
|
)
|
|
105
110
|
|
|
111
|
+
@lru_cache(maxsize=128)
|
|
112
|
+
def _compile_template(self, template_path_str: str) -> Template:
|
|
113
|
+
"""Compile a template by path with LRU caching.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
template_path_str : str
|
|
118
|
+
Absolute path to the template file.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
Template
|
|
123
|
+
Compiled Jinja2 template ready for rendering.
|
|
124
|
+
"""
|
|
125
|
+
template_text = Path(template_path_str).read_text()
|
|
126
|
+
return Template(template_text)
|
|
127
|
+
|
|
106
128
|
def render(self, template_path: str, context: dict[str, Any] | None = None) -> str:
|
|
107
129
|
"""Render a Jinja2 template with the given context variables.
|
|
108
130
|
|
|
109
131
|
Loads the template from either an absolute path or a path relative
|
|
110
132
|
to the base directory. The template is rendered with the provided
|
|
111
|
-
context dictionary using Jinja2's template engine.
|
|
133
|
+
context dictionary using Jinja2's template engine. Templates are
|
|
134
|
+
cached for improved performance on repeated renders.
|
|
112
135
|
|
|
113
136
|
For security, relative paths are validated to prevent path traversal
|
|
114
137
|
attacks. Absolute paths are allowed but should be used with caution
|
|
@@ -135,6 +158,8 @@ class PromptRenderer:
|
|
|
135
158
|
InputValidationError
|
|
136
159
|
If the path contains suspicious patterns or attempts to escape
|
|
137
160
|
the base directory.
|
|
161
|
+
TemplateNotFound
|
|
162
|
+
If the template cannot be loaded by Jinja2.
|
|
138
163
|
|
|
139
164
|
Examples
|
|
140
165
|
--------
|
|
@@ -164,9 +189,34 @@ class PromptRenderer:
|
|
|
164
189
|
base_dir=self.base_dir,
|
|
165
190
|
field_name="template_path",
|
|
166
191
|
)
|
|
167
|
-
|
|
168
|
-
template
|
|
192
|
+
|
|
193
|
+
# Check if template exists and provide clear error message
|
|
194
|
+
if not template_path_.exists():
|
|
195
|
+
raise FileNotFoundError(
|
|
196
|
+
f"Template not found: {template_path_}. "
|
|
197
|
+
f"Ensure the template exists in {self.base_dir} or provide an absolute path."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Cache-compile template by path (not by content)
|
|
201
|
+
template = self._compile_template(str(template_path_))
|
|
169
202
|
return template.render(context or {})
|
|
170
203
|
|
|
204
|
+
def clear_cache(self) -> None:
|
|
205
|
+
"""Clear the template compilation cache.
|
|
206
|
+
|
|
207
|
+
Useful when templates are modified during runtime and need to be
|
|
208
|
+
reloaded. Call this method to force re-compilation of all templates
|
|
209
|
+
on next render.
|
|
210
|
+
|
|
211
|
+
Examples
|
|
212
|
+
--------
|
|
213
|
+
>>> renderer = PromptRenderer()
|
|
214
|
+
>>> renderer.render("template.jinja", {}) # Compiles and caches
|
|
215
|
+
>>> # ... modify template.jinja ...
|
|
216
|
+
>>> renderer.clear_cache() # Clear cache
|
|
217
|
+
>>> renderer.render("template.jinja", {}) # Re-compiles
|
|
218
|
+
"""
|
|
219
|
+
self._compile_template.cache_clear()
|
|
220
|
+
|
|
171
221
|
|
|
172
222
|
__all__ = ["PromptRenderer"]
|
|
@@ -33,20 +33,23 @@ attach_vector_store
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
35
|
from .base import BaseResponse
|
|
36
|
-
from .config import ResponseConfiguration
|
|
36
|
+
from .config import ResponseConfiguration, ResponseRegistry, get_default_registry
|
|
37
37
|
from .messages import ResponseMessage, ResponseMessages
|
|
38
38
|
from .runner import run_async, run_streamed, run_sync
|
|
39
|
-
from .tool_call import ResponseToolCall
|
|
39
|
+
from .tool_call import ResponseToolCall, parse_tool_arguments
|
|
40
40
|
from .vector_store import attach_vector_store
|
|
41
41
|
|
|
42
42
|
__all__ = [
|
|
43
43
|
"BaseResponse",
|
|
44
44
|
"ResponseConfiguration",
|
|
45
|
+
"ResponseRegistry",
|
|
46
|
+
"get_default_registry",
|
|
45
47
|
"ResponseMessage",
|
|
46
48
|
"ResponseMessages",
|
|
47
49
|
"run_sync",
|
|
48
50
|
"run_async",
|
|
49
51
|
"run_streamed",
|
|
50
52
|
"ResponseToolCall",
|
|
53
|
+
"parse_tool_arguments",
|
|
51
54
|
"attach_vector_store",
|
|
52
55
|
]
|
|
@@ -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
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This package provides Pydantic models for representing agent execution plans,
|
|
4
4
|
including task definitions, agent type enumerations, and plan structures with
|
|
5
|
-
sequential execution support.
|
|
5
|
+
sequential execution support. Also includes helper functions for creating and
|
|
6
|
+
executing plans.
|
|
6
7
|
|
|
7
8
|
Classes
|
|
8
9
|
-------
|
|
@@ -12,6 +13,15 @@ TaskStructure
|
|
|
12
13
|
Individual agent task with status tracking and results.
|
|
13
14
|
AgentEnum
|
|
14
15
|
Enumeration of available agent types.
|
|
16
|
+
|
|
17
|
+
Functions
|
|
18
|
+
---------
|
|
19
|
+
create_plan
|
|
20
|
+
Create a PlanStructure from a sequence of tasks.
|
|
21
|
+
execute_task
|
|
22
|
+
Execute a single task with an agent callable.
|
|
23
|
+
execute_plan
|
|
24
|
+
Execute a complete plan using registered agent callables.
|
|
15
25
|
"""
|
|
16
26
|
|
|
17
27
|
from __future__ import annotations
|
|
@@ -19,9 +29,13 @@ from __future__ import annotations
|
|
|
19
29
|
from .plan import PlanStructure
|
|
20
30
|
from .task import TaskStructure
|
|
21
31
|
from .enum import AgentEnum
|
|
32
|
+
from .helpers import create_plan, execute_task, execute_plan
|
|
22
33
|
|
|
23
34
|
__all__ = [
|
|
24
35
|
"PlanStructure",
|
|
25
36
|
"TaskStructure",
|
|
26
37
|
"AgentEnum",
|
|
38
|
+
"create_plan",
|
|
39
|
+
"execute_task",
|
|
40
|
+
"execute_plan",
|
|
27
41
|
]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Helper functions for creating and executing agent plans.
|
|
2
|
+
|
|
3
|
+
This module provides convenience functions for working with PlanStructure
|
|
4
|
+
and TaskStructure, simplifying common workflows like plan creation, task
|
|
5
|
+
execution, and result aggregation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .enum import AgentEnum
|
|
11
|
+
from .plan import PlanStructure
|
|
12
|
+
from .task import TaskStructure
|
|
13
|
+
from .types import AgentCallable, AgentRegistry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_plan(*tasks: TaskStructure) -> PlanStructure:
|
|
17
|
+
"""Create a PlanStructure from a sequence of tasks.
|
|
18
|
+
|
|
19
|
+
Convenience factory function that constructs a plan from individual
|
|
20
|
+
tasks. Tasks are executed in the order they are provided.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
*tasks : TaskStructure
|
|
25
|
+
Variable number of task definitions to include in the plan.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
PlanStructure
|
|
30
|
+
New plan containing the provided tasks in order.
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> task1 = TaskStructure(
|
|
35
|
+
... task_type=AgentEnum.WEB_SEARCH,
|
|
36
|
+
... prompt="Search for AI trends"
|
|
37
|
+
... )
|
|
38
|
+
>>> task2 = TaskStructure(
|
|
39
|
+
... task_type=AgentEnum.SUMMARIZER,
|
|
40
|
+
... prompt="Summarize findings"
|
|
41
|
+
... )
|
|
42
|
+
>>> plan = create_plan(task1, task2)
|
|
43
|
+
>>> len(plan)
|
|
44
|
+
2
|
|
45
|
+
"""
|
|
46
|
+
return PlanStructure(tasks=list(tasks))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def execute_task(
|
|
50
|
+
task: TaskStructure,
|
|
51
|
+
agent_callable: AgentCallable,
|
|
52
|
+
) -> list[str]:
|
|
53
|
+
"""Execute a single task with an agent callable.
|
|
54
|
+
|
|
55
|
+
Runs one task using the provided agent function. Updates task status,
|
|
56
|
+
timing, and results. Context from previous tasks is not supported in this
|
|
57
|
+
helper - use execute_plan() for multi-task execution with context passing.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
task : TaskStructure
|
|
62
|
+
Task definition containing prompt and metadata.
|
|
63
|
+
agent_callable : AgentCallable
|
|
64
|
+
Synchronous or asynchronous callable responsible for executing the task.
|
|
65
|
+
Should accept the task prompt and an optional context keyword argument.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
list[str]
|
|
70
|
+
Normalized string results from task execution.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
------
|
|
74
|
+
Exception
|
|
75
|
+
Any exception raised by the agent_callable is propagated after
|
|
76
|
+
task status is updated.
|
|
77
|
+
|
|
78
|
+
Examples
|
|
79
|
+
--------
|
|
80
|
+
>>> def agent_fn(prompt, context=None):
|
|
81
|
+
... return f"Result for {prompt}"
|
|
82
|
+
>>> task = TaskStructure(prompt="Test task")
|
|
83
|
+
>>> results = execute_task(task, agent_fn)
|
|
84
|
+
>>> task.status
|
|
85
|
+
'done'
|
|
86
|
+
"""
|
|
87
|
+
from datetime import datetime, timezone
|
|
88
|
+
|
|
89
|
+
task.start_date = datetime.now(timezone.utc)
|
|
90
|
+
task.status = "running"
|
|
91
|
+
|
|
92
|
+
# Build plan with single task and execute
|
|
93
|
+
# Normalize task_type to string value for registry key to match PlanStructure.execute lookup
|
|
94
|
+
plan = PlanStructure(tasks=[task])
|
|
95
|
+
# Convert AgentEnum to its string value for registry key
|
|
96
|
+
registry_key = (
|
|
97
|
+
task.task_type.value
|
|
98
|
+
if isinstance(task.task_type, AgentEnum)
|
|
99
|
+
else task.task_type
|
|
100
|
+
)
|
|
101
|
+
registry: dict[str, AgentCallable] = {
|
|
102
|
+
registry_key: agent_callable,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Execute the plan - it will update task status
|
|
106
|
+
aggregated = plan.execute(
|
|
107
|
+
agent_registry=registry,
|
|
108
|
+
halt_on_error=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# If task failed, raise the exception
|
|
112
|
+
if task.status == "error":
|
|
113
|
+
# Extract error message from results
|
|
114
|
+
error_msg = task.results[0] if task.results else "Task execution failed"
|
|
115
|
+
# Raise RuntimeError with the error message
|
|
116
|
+
# The original exception type information is lost but the message is preserved
|
|
117
|
+
raise RuntimeError(f"Task execution error: {error_msg}")
|
|
118
|
+
|
|
119
|
+
return aggregated
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def execute_plan(
|
|
123
|
+
plan: PlanStructure,
|
|
124
|
+
agent_registry: AgentRegistry,
|
|
125
|
+
halt_on_error: bool = True,
|
|
126
|
+
) -> list[str]:
|
|
127
|
+
"""Execute a plan using registered agent callables.
|
|
128
|
+
|
|
129
|
+
Convenience wrapper around PlanStructure.execute() for cleaner syntax.
|
|
130
|
+
Runs all tasks in sequence, passing results between tasks as context.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
plan : PlanStructure
|
|
135
|
+
Plan containing ordered tasks to execute.
|
|
136
|
+
agent_registry : AgentRegistry
|
|
137
|
+
Lookup of agent identifiers to callables. Keys may be AgentEnum
|
|
138
|
+
instances or their string values.
|
|
139
|
+
halt_on_error : bool, default True
|
|
140
|
+
Whether execution should stop when a task raises an exception.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
list[str]
|
|
145
|
+
Flattened list of normalized outputs from all executed tasks.
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
KeyError
|
|
150
|
+
If a task references an agent not in the registry.
|
|
151
|
+
|
|
152
|
+
Examples
|
|
153
|
+
--------
|
|
154
|
+
>>> def search_agent(prompt, context=None):
|
|
155
|
+
... return ["search results"]
|
|
156
|
+
>>> def summary_agent(prompt, context=None):
|
|
157
|
+
... return ["summary"]
|
|
158
|
+
>>> registry = {
|
|
159
|
+
... AgentEnum.WEB_SEARCH: search_agent,
|
|
160
|
+
... AgentEnum.SUMMARIZER: summary_agent,
|
|
161
|
+
... }
|
|
162
|
+
>>> plan = PlanStructure(tasks=[...]) # doctest: +SKIP
|
|
163
|
+
>>> results = execute_plan(plan, registry) # doctest: +SKIP
|
|
164
|
+
"""
|
|
165
|
+
return plan.execute(agent_registry, halt_on_error=halt_on_error)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
__all__ = [
|
|
169
|
+
"create_plan",
|
|
170
|
+
"execute_task",
|
|
171
|
+
"execute_plan",
|
|
172
|
+
]
|
|
@@ -10,12 +10,13 @@ import asyncio
|
|
|
10
10
|
import inspect
|
|
11
11
|
import threading
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
|
-
from typing import Any, Awaitable,
|
|
13
|
+
from typing import Any, Awaitable, Coroutine, cast
|
|
14
14
|
from collections.abc import Mapping
|
|
15
15
|
|
|
16
16
|
from .enum import AgentEnum
|
|
17
17
|
from ..base import BaseStructure, spec_field
|
|
18
18
|
from .task import TaskStructure
|
|
19
|
+
from .types import AgentCallable, AgentRegistry
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class PlanStructure(BaseStructure):
|
|
@@ -108,9 +109,7 @@ class PlanStructure(BaseStructure):
|
|
|
108
109
|
|
|
109
110
|
def execute(
|
|
110
111
|
self,
|
|
111
|
-
agent_registry:
|
|
112
|
-
AgentEnum | str, Callable[..., object | Coroutine[Any, Any, object]]
|
|
113
|
-
],
|
|
112
|
+
agent_registry: AgentRegistry,
|
|
114
113
|
*,
|
|
115
114
|
halt_on_error: bool = True,
|
|
116
115
|
) -> list[str]:
|
|
@@ -121,7 +120,7 @@ class PlanStructure(BaseStructure):
|
|
|
121
120
|
|
|
122
121
|
Parameters
|
|
123
122
|
----------
|
|
124
|
-
agent_registry :
|
|
123
|
+
agent_registry : AgentRegistry
|
|
125
124
|
Lookup of agent identifiers to callables. Keys may be AgentEnum
|
|
126
125
|
instances or their string values. Each callable receives the task
|
|
127
126
|
prompt (augmented with prior context) and an optional context
|
|
@@ -147,13 +146,18 @@ class PlanStructure(BaseStructure):
|
|
|
147
146
|
>>> plan = PlanStructure(tasks=[TaskStructure(prompt="Test")])
|
|
148
147
|
>>> results = plan.execute(registry) # doctest: +SKIP
|
|
149
148
|
"""
|
|
149
|
+
normalized_registry: dict[str, AgentCallable] = {
|
|
150
|
+
self._resolve_registry_key(key): value
|
|
151
|
+
for key, value in agent_registry.items()
|
|
152
|
+
}
|
|
153
|
+
|
|
150
154
|
aggregated_results: list[str] = []
|
|
151
155
|
for task in self.tasks:
|
|
152
156
|
callable_key = self._resolve_registry_key(task.task_type)
|
|
153
|
-
if callable_key not in
|
|
157
|
+
if callable_key not in normalized_registry:
|
|
154
158
|
raise KeyError(f"No agent registered for '{callable_key}'.")
|
|
155
159
|
|
|
156
|
-
agent_callable =
|
|
160
|
+
agent_callable = normalized_registry[callable_key]
|
|
157
161
|
task.start_date = datetime.now(timezone.utc)
|
|
158
162
|
task.status = "running"
|
|
159
163
|
|
|
@@ -207,7 +211,7 @@ class PlanStructure(BaseStructure):
|
|
|
207
211
|
def _run_task(
|
|
208
212
|
task: TaskStructure,
|
|
209
213
|
*,
|
|
210
|
-
agent_callable:
|
|
214
|
+
agent_callable: AgentCallable,
|
|
211
215
|
aggregated_context: list[str],
|
|
212
216
|
) -> object | Coroutine[Any, Any, object]:
|
|
213
217
|
"""Execute a single task using the supplied callable.
|
|
@@ -219,7 +223,7 @@ class PlanStructure(BaseStructure):
|
|
|
219
223
|
----------
|
|
220
224
|
task : TaskStructure
|
|
221
225
|
Task definition containing inputs and metadata.
|
|
222
|
-
agent_callable :
|
|
226
|
+
agent_callable : AgentCallable
|
|
223
227
|
Function responsible for performing the task.
|
|
224
228
|
aggregated_context : list[str]
|
|
225
229
|
Accumulated results from previously executed tasks.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Type aliases for plan execution helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any, Callable, Coroutine, TypeAlias
|
|
7
|
+
|
|
8
|
+
from .enum import AgentEnum
|
|
9
|
+
|
|
10
|
+
AgentCallable = Callable[..., object | Coroutine[Any, Any, object]]
|
|
11
|
+
AgentRegistry: TypeAlias = (
|
|
12
|
+
Mapping[str, AgentCallable] | Mapping[AgentEnum, AgentCallable]
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = ["AgentCallable", "AgentRegistry"]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Tool handler utilities for OpenAI SDK interactions.
|
|
2
|
+
|
|
3
|
+
This module provides generic tool handling infrastructure including argument
|
|
4
|
+
parsing, Pydantic validation, function execution, and result serialization.
|
|
5
|
+
These utilities reduce boilerplate and ensure consistent tool behavior.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any, Callable, TypeVar
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ValidationError
|
|
15
|
+
|
|
16
|
+
from openai_sdk_helpers.response.tool_call import parse_tool_arguments
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T", bound=BaseModel)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def serialize_tool_result(result: Any) -> str:
|
|
22
|
+
"""Serialize tool results into a standardized JSON string.
|
|
23
|
+
|
|
24
|
+
Handles Pydantic models, lists, dicts, and plain strings with consistent
|
|
25
|
+
JSON formatting. Pydantic models are serialized using model_dump(),
|
|
26
|
+
while other types are converted to JSON or string representation.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
result : Any
|
|
31
|
+
Tool result to serialize. Can be a Pydantic model, list, dict, str,
|
|
32
|
+
or any JSON-serializable type.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
str
|
|
37
|
+
JSON-formatted string representation of the result.
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
>>> from pydantic import BaseModel
|
|
42
|
+
>>> class Result(BaseModel):
|
|
43
|
+
... value: int
|
|
44
|
+
>>> serialize_tool_result(Result(value=42))
|
|
45
|
+
'{"value": 42}'
|
|
46
|
+
|
|
47
|
+
>>> serialize_tool_result(["item1", "item2"])
|
|
48
|
+
'["item1", "item2"]'
|
|
49
|
+
|
|
50
|
+
>>> serialize_tool_result("plain text")
|
|
51
|
+
'"plain text"'
|
|
52
|
+
|
|
53
|
+
>>> serialize_tool_result({"key": "value"})
|
|
54
|
+
'{"key": "value"}'
|
|
55
|
+
"""
|
|
56
|
+
# Handle Pydantic models
|
|
57
|
+
if isinstance(result, BaseModel):
|
|
58
|
+
return result.model_dump_json()
|
|
59
|
+
|
|
60
|
+
# Handle strings - wrap in JSON string format
|
|
61
|
+
if isinstance(result, str):
|
|
62
|
+
return json.dumps(result)
|
|
63
|
+
|
|
64
|
+
# Handle other JSON-serializable types (lists, dicts, primitives)
|
|
65
|
+
try:
|
|
66
|
+
return json.dumps(result)
|
|
67
|
+
except (TypeError, ValueError):
|
|
68
|
+
# Fallback to string representation for non-JSON types
|
|
69
|
+
return json.dumps(str(result))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def tool_handler_factory(
|
|
73
|
+
func: Callable[..., Any],
|
|
74
|
+
input_model: type[T] | None = None,
|
|
75
|
+
) -> Callable[[Any], str]:
|
|
76
|
+
"""Create a generic tool handler that parses, validates, and serializes.
|
|
77
|
+
|
|
78
|
+
Wraps a tool function with automatic argument parsing, optional Pydantic
|
|
79
|
+
validation, execution, and result serialization. This eliminates
|
|
80
|
+
repetitive boilerplate for tool implementations.
|
|
81
|
+
|
|
82
|
+
The returned handler:
|
|
83
|
+
1. Parses tool_call.arguments using parse_tool_arguments
|
|
84
|
+
2. Validates arguments with input_model if provided
|
|
85
|
+
3. Calls func with validated/parsed arguments
|
|
86
|
+
4. Serializes the result using serialize_tool_result
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
func : Callable[..., Any]
|
|
91
|
+
The actual tool implementation function. Should accept keyword
|
|
92
|
+
arguments matching the tool's parameter schema. Can be synchronous
|
|
93
|
+
or asynchronous.
|
|
94
|
+
input_model : type[BaseModel] or None, default None
|
|
95
|
+
Optional Pydantic model for input validation. When provided,
|
|
96
|
+
arguments are validated and converted to this model before being
|
|
97
|
+
passed to func.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
Callable[[Any], str]
|
|
102
|
+
Handler function that accepts a tool_call object (with arguments
|
|
103
|
+
and name attributes) and returns a JSON string result.
|
|
104
|
+
|
|
105
|
+
Raises
|
|
106
|
+
------
|
|
107
|
+
ValidationError
|
|
108
|
+
If input_model is provided and validation fails.
|
|
109
|
+
ValueError
|
|
110
|
+
If argument parsing fails.
|
|
111
|
+
|
|
112
|
+
Examples
|
|
113
|
+
--------
|
|
114
|
+
Basic usage without validation:
|
|
115
|
+
|
|
116
|
+
>>> def search_tool(query: str, limit: int = 10):
|
|
117
|
+
... return {"results": [f"Result for {query}"]}
|
|
118
|
+
>>> handler = tool_handler_factory(search_tool)
|
|
119
|
+
|
|
120
|
+
With Pydantic validation:
|
|
121
|
+
|
|
122
|
+
>>> from pydantic import BaseModel
|
|
123
|
+
>>> class SearchInput(BaseModel):
|
|
124
|
+
... query: str
|
|
125
|
+
... limit: int = 10
|
|
126
|
+
>>> def search_tool(query: str, limit: int = 10):
|
|
127
|
+
... return {"results": [f"Result for {query}"]}
|
|
128
|
+
>>> handler = tool_handler_factory(search_tool, SearchInput)
|
|
129
|
+
|
|
130
|
+
The handler can then be used with OpenAI tool calls:
|
|
131
|
+
|
|
132
|
+
>>> class ToolCall:
|
|
133
|
+
... def __init__(self):
|
|
134
|
+
... self.arguments = '{"query": "test", "limit": 5}'
|
|
135
|
+
... self.name = "search"
|
|
136
|
+
>>> tool_call = ToolCall()
|
|
137
|
+
>>> result = handler(tool_call) # doctest: +SKIP
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def handler(tool_call: Any) -> str:
|
|
141
|
+
"""Handle tool execution with parsing, validation, and serialization.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
tool_call : Any
|
|
146
|
+
Tool call object with 'arguments' and 'name' attributes.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
str
|
|
151
|
+
JSON-formatted result from the tool function.
|
|
152
|
+
|
|
153
|
+
Raises
|
|
154
|
+
------
|
|
155
|
+
ValueError
|
|
156
|
+
If argument parsing fails.
|
|
157
|
+
ValidationError
|
|
158
|
+
If Pydantic validation fails (when input_model is provided).
|
|
159
|
+
"""
|
|
160
|
+
# Extract tool name for error context (required)
|
|
161
|
+
tool_name = getattr(tool_call, "name", "unknown")
|
|
162
|
+
|
|
163
|
+
# Parse arguments with error context
|
|
164
|
+
parsed_args = parse_tool_arguments(tool_call.arguments, tool_name=tool_name)
|
|
165
|
+
|
|
166
|
+
# Validate with Pydantic if model provided
|
|
167
|
+
if input_model is not None:
|
|
168
|
+
validated_input = input_model(**parsed_args)
|
|
169
|
+
# Convert back to dict for function call
|
|
170
|
+
call_kwargs = validated_input.model_dump()
|
|
171
|
+
else:
|
|
172
|
+
call_kwargs = parsed_args
|
|
173
|
+
|
|
174
|
+
# Execute function (sync only - async functions not supported)
|
|
175
|
+
if inspect.iscoroutinefunction(func):
|
|
176
|
+
raise TypeError(
|
|
177
|
+
f"Async functions are not supported by tool_handler_factory. "
|
|
178
|
+
f"Function '{func.__name__}' is async. "
|
|
179
|
+
"Wrap async functions in a synchronous adapter before passing to tool_handler_factory."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
result = func(**call_kwargs)
|
|
183
|
+
|
|
184
|
+
# Serialize result
|
|
185
|
+
return serialize_tool_result(result)
|
|
186
|
+
|
|
187
|
+
return handler
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
__all__ = [
|
|
191
|
+
"serialize_tool_result",
|
|
192
|
+
"tool_handler_factory",
|
|
193
|
+
]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Utility helpers for openai-sdk-helpers.
|
|
2
2
|
|
|
3
3
|
This package provides common utility functions for type coercion, file
|
|
4
|
-
handling, JSON serialization,
|
|
5
|
-
throughout the openai_sdk_helpers package.
|
|
4
|
+
handling, JSON serialization, logging, and OpenAI settings construction.
|
|
5
|
+
These utilities are used throughout the openai_sdk_helpers package.
|
|
6
6
|
|
|
7
7
|
Functions
|
|
8
8
|
---------
|
|
@@ -20,6 +20,8 @@ coerce_jsonable(value)
|
|
|
20
20
|
Convert a value into a JSON-serializable representation.
|
|
21
21
|
log(message, level)
|
|
22
22
|
Log a message with basic configuration.
|
|
23
|
+
build_openai_settings(**kwargs)
|
|
24
|
+
Build OpenAI settings from environment with validation.
|
|
23
25
|
|
|
24
26
|
Classes
|
|
25
27
|
-------
|
|
@@ -33,6 +35,7 @@ from __future__ import annotations
|
|
|
33
35
|
|
|
34
36
|
from .core import (
|
|
35
37
|
JSONSerializable,
|
|
38
|
+
build_openai_settings,
|
|
36
39
|
check_filepath,
|
|
37
40
|
coerce_jsonable,
|
|
38
41
|
coerce_dict,
|
|
@@ -53,4 +56,5 @@ __all__ = [
|
|
|
53
56
|
"JSONSerializable",
|
|
54
57
|
"customJSONEncoder",
|
|
55
58
|
"log",
|
|
59
|
+
"build_openai_settings",
|
|
56
60
|
]
|
openai_sdk_helpers/utils/core.py
CHANGED
|
@@ -143,6 +143,130 @@ def coerce_dict(value: object) -> dict[str, Any]:
|
|
|
143
143
|
raise TypeError("extra_client_kwargs must be a mapping or None")
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
def build_openai_settings(
|
|
147
|
+
api_key: str | None = None,
|
|
148
|
+
org_id: str | None = None,
|
|
149
|
+
project_id: str | None = None,
|
|
150
|
+
base_url: str | None = None,
|
|
151
|
+
default_model: str | None = None,
|
|
152
|
+
timeout: float | str | None = None,
|
|
153
|
+
max_retries: int | str | None = None,
|
|
154
|
+
dotenv_path: Path | None = None,
|
|
155
|
+
**extra_kwargs: Any,
|
|
156
|
+
) -> Any: # Returns OpenAISettings but use Any to avoid circular import
|
|
157
|
+
"""Build OpenAI settings from environment with explicit validation.
|
|
158
|
+
|
|
159
|
+
Convenience function for creating OpenAISettings with validation and
|
|
160
|
+
clear error messages. Reads from environment variables and validates
|
|
161
|
+
required fields, with explicit type coercion for timeout and max_retries.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
api_key : str or None, default None
|
|
166
|
+
API key for OpenAI authentication. If None, reads from OPENAI_API_KEY.
|
|
167
|
+
org_id : str or None, default None
|
|
168
|
+
Organization ID. If None, reads from OPENAI_ORG_ID.
|
|
169
|
+
project_id : str or None, default None
|
|
170
|
+
Project ID. If None, reads from OPENAI_PROJECT_ID.
|
|
171
|
+
base_url : str or None, default None
|
|
172
|
+
Base URL for API requests. If None, reads from OPENAI_BASE_URL.
|
|
173
|
+
default_model : str or None, default None
|
|
174
|
+
Default model name. If None, reads from OPENAI_MODEL.
|
|
175
|
+
timeout : float, str, or None, default None
|
|
176
|
+
Request timeout in seconds. If None, reads from OPENAI_TIMEOUT.
|
|
177
|
+
Can be string that will be parsed to float.
|
|
178
|
+
max_retries : int, str, or None, default None
|
|
179
|
+
Maximum retry attempts. If None, reads from OPENAI_MAX_RETRIES.
|
|
180
|
+
Can be string that will be parsed to int.
|
|
181
|
+
dotenv_path : Path or None, default None
|
|
182
|
+
Path to .env file. If None, searches for .env in current directory.
|
|
183
|
+
**extra_kwargs : Any
|
|
184
|
+
Additional keyword arguments for extra_client_kwargs.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
OpenAISettings
|
|
189
|
+
Configured settings instance.
|
|
190
|
+
|
|
191
|
+
Raises
|
|
192
|
+
------
|
|
193
|
+
ValueError
|
|
194
|
+
If OPENAI_API_KEY is not found in environment or parameters.
|
|
195
|
+
If timeout cannot be parsed as float.
|
|
196
|
+
If max_retries cannot be parsed as int.
|
|
197
|
+
TypeError
|
|
198
|
+
If timeout or max_retries have invalid types.
|
|
199
|
+
|
|
200
|
+
Examples
|
|
201
|
+
--------
|
|
202
|
+
Build from explicit parameters:
|
|
203
|
+
|
|
204
|
+
>>> settings = build_openai_settings(
|
|
205
|
+
... api_key="sk-...",
|
|
206
|
+
... default_model="gpt-4o",
|
|
207
|
+
... timeout=30.0
|
|
208
|
+
... )
|
|
209
|
+
|
|
210
|
+
Build from environment:
|
|
211
|
+
|
|
212
|
+
>>> settings = build_openai_settings() # doctest: +SKIP
|
|
213
|
+
|
|
214
|
+
With custom .env location:
|
|
215
|
+
|
|
216
|
+
>>> settings = build_openai_settings(
|
|
217
|
+
... dotenv_path=Path("/path/to/.env")
|
|
218
|
+
... ) # doctest: +SKIP
|
|
219
|
+
"""
|
|
220
|
+
# Import at runtime to avoid circular import
|
|
221
|
+
from openai_sdk_helpers.config import OpenAISettings
|
|
222
|
+
|
|
223
|
+
# Parse timeout with validation
|
|
224
|
+
parsed_timeout: float | None = None
|
|
225
|
+
if timeout is not None:
|
|
226
|
+
try:
|
|
227
|
+
parsed_timeout = coerce_optional_float(timeout)
|
|
228
|
+
except (ValueError, TypeError) as exc:
|
|
229
|
+
raise ValueError(
|
|
230
|
+
f"Invalid timeout value '{timeout}'. Must be a number or numeric string."
|
|
231
|
+
) from exc
|
|
232
|
+
|
|
233
|
+
# Parse max_retries with validation
|
|
234
|
+
parsed_max_retries: int | None = None
|
|
235
|
+
if max_retries is not None:
|
|
236
|
+
try:
|
|
237
|
+
parsed_max_retries = coerce_optional_int(max_retries)
|
|
238
|
+
except (ValueError, TypeError) as exc:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"Invalid max_retries value '{max_retries}'. "
|
|
241
|
+
"Must be an integer or numeric string."
|
|
242
|
+
) from exc
|
|
243
|
+
|
|
244
|
+
# Build settings using from_env with overrides
|
|
245
|
+
overrides = {}
|
|
246
|
+
if api_key is not None:
|
|
247
|
+
overrides["api_key"] = api_key
|
|
248
|
+
if org_id is not None:
|
|
249
|
+
overrides["org_id"] = org_id
|
|
250
|
+
if project_id is not None:
|
|
251
|
+
overrides["project_id"] = project_id
|
|
252
|
+
if base_url is not None:
|
|
253
|
+
overrides["base_url"] = base_url
|
|
254
|
+
if default_model is not None:
|
|
255
|
+
overrides["default_model"] = default_model
|
|
256
|
+
if parsed_timeout is not None:
|
|
257
|
+
overrides["timeout"] = parsed_timeout
|
|
258
|
+
if parsed_max_retries is not None:
|
|
259
|
+
overrides["max_retries"] = parsed_max_retries
|
|
260
|
+
if extra_kwargs:
|
|
261
|
+
overrides["extra_client_kwargs"] = extra_kwargs
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
return OpenAISettings.from_env(dotenv_path=dotenv_path, **overrides)
|
|
265
|
+
except ValueError as exc:
|
|
266
|
+
# Re-raise with more context but preserve original message
|
|
267
|
+
raise ValueError(f"Failed to build OpenAI settings: {exc}") from exc
|
|
268
|
+
|
|
269
|
+
|
|
146
270
|
T = TypeVar("T")
|
|
147
271
|
_configured_logging = False
|
|
148
272
|
|
|
@@ -465,4 +589,8 @@ __all__ = [
|
|
|
465
589
|
"JSONSerializable",
|
|
466
590
|
"customJSONEncoder",
|
|
467
591
|
"log",
|
|
592
|
+
"coerce_optional_float",
|
|
593
|
+
"coerce_optional_int",
|
|
594
|
+
"coerce_dict",
|
|
595
|
+
"build_openai_settings",
|
|
468
596
|
]
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
openai_sdk_helpers/__init__.py,sha256=
|
|
1
|
+
openai_sdk_helpers/__init__.py,sha256=yFWF1rBF8-mCVPK-_PfiCaBMIVl_NGvJb4wySXWenhg,3765
|
|
2
2
|
openai_sdk_helpers/async_utils.py,sha256=_GPiSDQhWehiZu2S_jB_Xgl0p2qGc5MNu1NN92zz3bg,3726
|
|
3
|
-
openai_sdk_helpers/config.py,sha256=
|
|
3
|
+
openai_sdk_helpers/config.py,sha256=841YGiUu7hvFrB2Bdr6ck4YKCtb2HRI0Uhk6HKpIqAc,7640
|
|
4
4
|
openai_sdk_helpers/context_manager.py,sha256=9z54rjcJ-nAFdEoZHjFdk1YYpeD9bet13MOgn23FzM8,6629
|
|
5
5
|
openai_sdk_helpers/environment.py,sha256=HSPI1h1JUuMxzcTSvr28ktHBvyEJLRzL4bZhNfy59lI,1372
|
|
6
6
|
openai_sdk_helpers/errors.py,sha256=oytqn-6Jg6nPMQOP956ftfkLS0R5c1XBDX-lNstrb3Y,3135
|
|
7
7
|
openai_sdk_helpers/logging_config.py,sha256=fOKBgisOkM0VYDt68pmUSxVWzTeO25_u-El0HOxqEYM,2928
|
|
8
8
|
openai_sdk_helpers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
openai_sdk_helpers/retry.py,sha256=3I5cZOq5X6d7kno3fk1SVDYJ7U2VDHqBS7ZsaXWbU6A,6529
|
|
10
|
+
openai_sdk_helpers/tools.py,sha256=MW6bdJH9Ij16LKNCNNvSthq206T-orTOQTLFzLWxQTo,6043
|
|
10
11
|
openai_sdk_helpers/types.py,sha256=xzldCRfwCZ3rZl18IBmfgA-PVdoZKSWNrlSIhirumSo,1451
|
|
11
12
|
openai_sdk_helpers/validation.py,sha256=fr3zVZ7uEokJJqF3LSIcQm4wV3MvWXcep2ZRpseXZBk,7789
|
|
12
13
|
openai_sdk_helpers/agent/__init__.py,sha256=giowU8jke0z0h7FFUG9V6Vssja8AYwvJMQbiMb3s64k,960
|
|
@@ -26,22 +27,22 @@ openai_sdk_helpers/agent/search/web.py,sha256=8le4xnZ3nllySqWb7rZaOq44ZR8q67c_Wi
|
|
|
26
27
|
openai_sdk_helpers/enums/__init__.py,sha256=aFf79C4JBeLC3kMlJfSpehyjx5uNCtW6eK5rD6ZFfhM,322
|
|
27
28
|
openai_sdk_helpers/enums/base.py,sha256=cNllDtzcgI0_eZYXxFko14yhxwicX6xbeDfz9gFE3qo,2753
|
|
28
29
|
openai_sdk_helpers/prompt/__init__.py,sha256=MOqgKwG9KLqKudoKRlUfLxiSmdOi2aD6hNrWDFqLHkk,418
|
|
29
|
-
openai_sdk_helpers/prompt/base.py,sha256=
|
|
30
|
+
openai_sdk_helpers/prompt/base.py,sha256=FvQNEjIdYDLvGEiR9_dbEn6G47TPMsWm4r26jcLjgus,7569
|
|
30
31
|
openai_sdk_helpers/prompt/summarizer.jinja,sha256=jliSetWDISbql1EkWi1RB8-L_BXUg8JMkRRsPRHuzbY,309
|
|
31
32
|
openai_sdk_helpers/prompt/translator.jinja,sha256=SZhW8ipEzM-9IA4wyS_r2wIMTAclWrilmk1s46njoL0,291
|
|
32
33
|
openai_sdk_helpers/prompt/validator.jinja,sha256=6t8q_IdxFd3mVBGX6SFKNOert1Wo3YpTOji2SNEbbtE,547
|
|
33
|
-
openai_sdk_helpers/response/__init__.py,sha256=
|
|
34
|
+
openai_sdk_helpers/response/__init__.py,sha256=eoQF086o3OZYmVfJWXhSpYlPhQBb-VLDA5hvw7guLEc,1741
|
|
34
35
|
openai_sdk_helpers/response/base.py,sha256=-d-vvY4OId_MU6EAN55bBPEftnskw9Ry8TVQ705f9Xw,27965
|
|
35
|
-
openai_sdk_helpers/response/config.py,sha256=
|
|
36
|
+
openai_sdk_helpers/response/config.py,sha256=WheEWkTxNFHL54_yvFY3M0LclNmajwTiMftSFeAH2eI,10300
|
|
36
37
|
openai_sdk_helpers/response/messages.py,sha256=oVSHpSV_iQxHreCXm--a6MlHg_kkElQi3R2Y8Y7VphA,9134
|
|
37
38
|
openai_sdk_helpers/response/runner.py,sha256=Rf13cQGsR7sN9gA81Y5th1tfH2DCCAwQ6RMs3bVgjnk,4269
|
|
38
|
-
openai_sdk_helpers/response/tool_call.py,sha256=
|
|
39
|
+
openai_sdk_helpers/response/tool_call.py,sha256=VYPvKUR-Ren0Y_nYS4jUSinhTyXKzFwQLxu-d3r_YuM,4506
|
|
39
40
|
openai_sdk_helpers/response/vector_store.py,sha256=MyHUu6P9ueNsd9erbBkyVqq3stLK6qVuehdvmFAHq9E,3074
|
|
40
41
|
openai_sdk_helpers/streamlit_app/__init__.py,sha256=RjJbnBDS5_YmAmxvaa3phB5u9UcXsXDEk_jMlY_pa5Q,793
|
|
41
42
|
openai_sdk_helpers/streamlit_app/app.py,sha256=ID3B4fUQHvv1Cwuuvrlm4nK4d0nWL6uBE40O_T6r7yY,10808
|
|
42
43
|
openai_sdk_helpers/streamlit_app/config.py,sha256=EK6LWACo7YIkDko1oesvupOx56cTuWWnwnXRiu8EYbs,15986
|
|
43
44
|
openai_sdk_helpers/streamlit_app/streamlit_web_search.py,sha256=OrX-kgW_yaHgIsK6wY9gBVLbvDaMFXgkgdhKQDsA8kQ,2506
|
|
44
|
-
openai_sdk_helpers/structure/__init__.py,sha256=
|
|
45
|
+
openai_sdk_helpers/structure/__init__.py,sha256=QUvRdJMbKsumjwJdWq9ihfcOED4ZbJMBQbmA1nmYJVw,3339
|
|
45
46
|
openai_sdk_helpers/structure/agent_blueprint.py,sha256=2W-RBM5G3ZefMcYHqqoV6Y1witcSbMlUpdU1CA9n3tg,9698
|
|
46
47
|
openai_sdk_helpers/structure/base.py,sha256=7RMsCMjQR7u3mksirqd0E6AgCgWEMVRQtgNefwHWPGo,28278
|
|
47
48
|
openai_sdk_helpers/structure/prompt.py,sha256=7DBdLu6WDvXy2RkEBayDiX2Jn8T4-hJuohsOaKEoqJs,1075
|
|
@@ -50,17 +51,19 @@ openai_sdk_helpers/structure/summary.py,sha256=MyZzMuqHP9F8B4rYYxCGJwojy5RavWUkM
|
|
|
50
51
|
openai_sdk_helpers/structure/validation.py,sha256=vsilA3Qs3fjWLeYlnZnMEGj9i_bOJtXc2J3mSIEHncg,2409
|
|
51
52
|
openai_sdk_helpers/structure/vector_search.py,sha256=A0w2AR0r6aIFoYbNkscUAGT7VzTe6WuvxrqUsWT2PMQ,5782
|
|
52
53
|
openai_sdk_helpers/structure/web_search.py,sha256=S8OdllBWqEGXaKf6Alocl89ZuG7BlvXK5ra1Lm7lfjE,4572
|
|
53
|
-
openai_sdk_helpers/structure/plan/__init__.py,sha256=
|
|
54
|
+
openai_sdk_helpers/structure/plan/__init__.py,sha256=IGr0Tk4inN_8o7fT2N02_FTi6U6l2T9_npcQHAlBwKA,1076
|
|
54
55
|
openai_sdk_helpers/structure/plan/enum.py,sha256=seESSwH-IeeW-9BqIMUQyk3qjtchfU3TDhF9HPDB1OM,3079
|
|
55
|
-
openai_sdk_helpers/structure/plan/
|
|
56
|
+
openai_sdk_helpers/structure/plan/helpers.py,sha256=25qXUMPY73y3E6EsE88n9VBwNj2JZkzXy1AaWGsoSLw,5132
|
|
57
|
+
openai_sdk_helpers/structure/plan/plan.py,sha256=LtfwWwZiHGe06nFCXSbT8p3x3w9hhI0wXS7hTeeWXvY,9663
|
|
56
58
|
openai_sdk_helpers/structure/plan/task.py,sha256=2dH8iaLhjC7MKZEW1T_HICaggi1RPyKSPOl9ORmmYdg,4538
|
|
57
|
-
openai_sdk_helpers/
|
|
58
|
-
openai_sdk_helpers/utils/
|
|
59
|
+
openai_sdk_helpers/structure/plan/types.py,sha256=7y9QEVdZreQUXV7n-R4RoNZzw5HeOVbJGWx9QkSfuNY,418
|
|
60
|
+
openai_sdk_helpers/utils/__init__.py,sha256=oNMc8xyOGmXLNIOjwC5EhN8Jjy_S74Vgwzzg41RNb4g,1566
|
|
61
|
+
openai_sdk_helpers/utils/core.py,sha256=Ehm9WePZTl9ypbbKlHNiyRkhI4a5ZhbDwKiSJTNTgz8,17262
|
|
59
62
|
openai_sdk_helpers/vector_storage/__init__.py,sha256=L5LxO09puh9_yBB9IDTvc1CvVkARVkHqYY1KX3inB4c,975
|
|
60
63
|
openai_sdk_helpers/vector_storage/cleanup.py,sha256=ImWIE-9lli-odD8qIARvmeaa0y8ZD4pYYP-kT0O3178,3552
|
|
61
64
|
openai_sdk_helpers/vector_storage/storage.py,sha256=ZiTZnvCY28R-WUQjHdnjQo1xIRbXAM6JL0VhqW9MM9I,21725
|
|
62
65
|
openai_sdk_helpers/vector_storage/types.py,sha256=jTCcOYMeOpZWvcse0z4T3MVs-RBOPC-fqWTBeQrgafU,1639
|
|
63
|
-
openai_sdk_helpers-0.0.
|
|
64
|
-
openai_sdk_helpers-0.0.
|
|
65
|
-
openai_sdk_helpers-0.0.
|
|
66
|
-
openai_sdk_helpers-0.0.
|
|
66
|
+
openai_sdk_helpers-0.1.0.dist-info/METADATA,sha256=R-Lc44fSOhAohVtwOAVW_pg3_GMHfYW5ve3bwZlIETE,18492
|
|
67
|
+
openai_sdk_helpers-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
68
|
+
openai_sdk_helpers-0.1.0.dist-info/licenses/LICENSE,sha256=CUhc1NrE50bs45tcXF7OcTQBKEvkUuLqeOHgrWQ5jaA,1067
|
|
69
|
+
openai_sdk_helpers-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|