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.
@@ -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
  ]
@@ -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
- else:
148
- env_file_values = dotenv_values()
149
-
150
- timeout_raw = (
151
- overrides.get("timeout")
152
- or env_file_values.get("OPENAI_TIMEOUT")
153
- or os.getenv("OPENAI_TIMEOUT")
154
- )
155
- max_retries_raw = (
156
- overrides.get("max_retries")
157
- or env_file_values.get("OPENAI_MAX_RETRIES")
158
- or os.getenv("OPENAI_MAX_RETRIES")
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": overrides.get("api_key")
163
- or env_file_values.get("OPENAI_API_KEY")
164
- or os.getenv("OPENAI_API_KEY"),
165
- "org_id": overrides.get("org_id")
166
- or env_file_values.get("OPENAI_ORG_ID")
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
- template_path_text = template_path_.read_text()
168
- template = Template(template_path_text)
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
- raise ValueError(f"Invalid JSON arguments: {arguments}") from exc
137
+ # Build informative error message with context
138
+ payload_preview = (
139
+ arguments[:100] + "..." if len(arguments) > 100 else arguments
140
+ )
141
+ raise ValueError(
142
+ f"Failed to parse tool arguments for tool '{tool_name}'. "
143
+ f"Raw payload: {payload_preview}"
144
+ ) from exc
@@ -86,6 +86,9 @@ __all__ = [
86
86
  "AgentEnum",
87
87
  "TaskStructure",
88
88
  "PlanStructure",
89
+ "create_plan",
90
+ "execute_task",
91
+ "execute_plan",
89
92
  "PromptStructure",
90
93
  "SummaryTopic",
91
94
  "SummaryStructure",
@@ -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, Callable, Coroutine, cast
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: Mapping[
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 : Mapping[AgentEnum | str, Callable[..., Any]]
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 agent_registry:
157
+ if callable_key not in normalized_registry:
154
158
  raise KeyError(f"No agent registered for '{callable_key}'.")
155
159
 
156
- agent_callable = agent_registry[callable_key]
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: Callable[..., object | Coroutine[Any, Any, object]],
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 : Callable[..., Any]
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, and logging. These utilities are used
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
  ]
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openai-sdk-helpers
3
- Version: 0.0.9
3
+ Version: 0.1.0
4
4
  Summary: Composable helpers for OpenAI SDK agents, prompts, and storage
5
5
  Author: openai-sdk-helpers maintainers
6
6
  License: MIT
@@ -1,12 +1,13 @@
1
- openai_sdk_helpers/__init__.py,sha256=rXQ5KjONsOf1aN31NKXfljN7FlEvxg8s29Z6LkIG3fw,3231
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=ChJmSfi71T3LyNYhsNpJF_bho1a3d5vCGIRD0IwLh9o,7662
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=4VJFmJozVmKQ04dqm8jYSiOijcQbT0CSAll2NE8zF68,5734
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=BbKIigsEVQGbxRSBp9FSLPM9OYsEndKHZpGHonnu8P4,1599
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=kSsZtDT9JYK8KgF3OaFeeP2rhGtSsFlw535HJoPqpUk,6523
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=218mWOj-QGW3IwVsgnSurSRWRcdUiFT-5C44Zn_m4pQ,3943
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=YI15qqUr2hezDJoShUfhEfRWcO2iEe9CQEBM5tAQQB4,3280
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=0kLN6n3wTAFuOvCOWiDrreWpv6RX4ycD4HNhVqF4pTs,667
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/plan.py,sha256=Rc8vwY2RmDrRRhVtiOv0BXjoRKn3T1RGY0Pi3wRvLjk,9602
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/utils/__init__.py,sha256=L4U5uaBIt0nIV-GhFc5Kotx9h69SryoCmfJL9u23IJU,1388
58
- openai_sdk_helpers/utils/core.py,sha256=XvQePx0CDkcwkENAatcPwF9H_b4N5nedlIX3viW2RtM,12684
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.9.dist-info/METADATA,sha256=lKUWNHXa5c7maQW4p7pQA0uM0cPSyvNf_TMce5huHlo,18492
64
- openai_sdk_helpers-0.0.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
65
- openai_sdk_helpers-0.0.9.dist-info/licenses/LICENSE,sha256=CUhc1NrE50bs45tcXF7OcTQBKEvkUuLqeOHgrWQ5jaA,1067
66
- openai_sdk_helpers-0.0.9.dist-info/RECORD,,
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,,