openai-sdk-helpers 0.1.4__py3-none-any.whl → 0.3.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.
@@ -1,68 +1,375 @@
1
- """Configuration helpers for ``AgentBase``."""
1
+ """Configuration helpers for ``BaseAgent``."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, List, Optional, Type
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Optional
6
8
 
9
+ from agents import Agent, Handoff, InputGuardrail, OutputGuardrail, Session
7
10
  from agents.model_settings import ModelSettings
8
- from pydantic import BaseModel, ConfigDict, Field
9
11
 
10
- from ..structure import BaseStructure
12
+ from ..utils import JSONSerializable
13
+ from ..utils.registry import BaseRegistry
14
+ from ..utils.instructions import resolve_instructions_from_path
11
15
 
12
16
 
13
- class AgentConfig(BaseStructure):
14
- """Configuration required to build an AgentBase.
17
+ class AgentConfigurationRegistry(BaseRegistry["AgentConfiguration"]):
18
+ """Registry for managing AgentConfiguration instances.
19
+
20
+ Inherits from BaseRegistry to provide centralized storage and retrieval
21
+ of agent configurations, enabling reusable agent specs across the application.
22
+
23
+ Examples
24
+ --------
25
+ >>> registry = AgentConfigurationRegistry()
26
+ >>> config = AgentConfiguration(
27
+ ... name="test_agent",
28
+ ... model="gpt-4o-mini",
29
+ ... instructions="Test instructions"
30
+ ... )
31
+ >>> registry.register(config)
32
+ >>> retrieved = registry.get("test_agent")
33
+ >>> retrieved.name
34
+ 'test_agent'
35
+ """
36
+
37
+ def load_from_directory(
38
+ self,
39
+ path: Path | str,
40
+ *,
41
+ config_class: type["AgentConfiguration"] | None = None,
42
+ ) -> int:
43
+ """Load all agent configurations from JSON files in a directory.
44
+
45
+ Parameters
46
+ ----------
47
+ path : Path or str
48
+ Directory path containing JSON configuration files.
49
+ config_class : type[AgentConfiguration], optional
50
+ The configuration class to use for deserialization.
51
+ Defaults to AgentConfiguration.
52
+
53
+ Returns
54
+ -------
55
+ int
56
+ Number of configurations successfully loaded and registered.
57
+
58
+ Raises
59
+ ------
60
+ FileNotFoundError
61
+ If the directory does not exist.
62
+ NotADirectoryError
63
+ If the path is not a directory.
64
+
65
+ Examples
66
+ --------
67
+ >>> registry = AgentConfigurationRegistry()
68
+ >>> count = registry.load_from_directory("./agents")
69
+ >>> print(f"Loaded {count} configurations")
70
+ """
71
+ if config_class is None:
72
+ config_class = AgentConfiguration
73
+ return super().load_from_directory(path, config_class=config_class)
74
+
75
+
76
+ # Global default registry instance
77
+ _default_registry = AgentConfigurationRegistry()
78
+
79
+
80
+ def get_default_registry() -> AgentConfigurationRegistry:
81
+ """Return the global default registry instance.
82
+
83
+ Returns
84
+ -------
85
+ AgentConfigurationRegistry
86
+ Singleton registry for application-wide configuration storage.
87
+
88
+ Examples
89
+ --------
90
+ >>> registry = get_default_registry()
91
+ >>> config = AgentConfiguration(
92
+ ... name="test", model="gpt-4o-mini", instructions="Test instructions"
93
+ ... )
94
+ >>> registry.register(config)
95
+ """
96
+ return _default_registry
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class AgentConfiguration(JSONSerializable):
101
+ """Immutable configuration for building a BaseAgent.
102
+
103
+ Encapsulates all metadata required to define an agent including its
104
+ instructions, tools, model settings, handoffs, guardrails, and session
105
+ management. Inherits from JSONSerializable to support serialization.
106
+
107
+ This dataclass is frozen (immutable) to ensure thread-safety and
108
+ enable use as dictionary keys. All list-type fields use None as the
109
+ default value rather than mutable defaults like [] to avoid issues
110
+ with shared state across instances.
111
+
112
+ Parameters
113
+ ----------
114
+ name : str
115
+ Unique identifier for the agent. Must be a non-empty string.
116
+ instructions : str or Path
117
+ Plain text instructions or a path to a Jinja template file whose
118
+ contents are loaded at runtime. Required field.
119
+ description : str, optional
120
+ Short description of the agent's purpose. Default is None.
121
+ model : str, optional
122
+ Model identifier to use (e.g., "gpt-4o-mini"). Default is None.
123
+ template_path : str or Path, optional
124
+ Path to the Jinja template (absolute or relative to prompt_dir).
125
+ This takes precedence over instructions if both are provided.
126
+ Default is None.
127
+ input_type : type, optional
128
+ Type describing the agent input, commonly a Pydantic model.
129
+ Default is None.
130
+ output_type : type, optional
131
+ Type describing the agent output, commonly a Pydantic model or
132
+ builtin like str. Default is None.
133
+ tools : list, optional
134
+ Tool definitions available to the agent. Default is None.
135
+ model_settings : ModelSettings, optional
136
+ Additional model configuration settings. Default is None.
137
+ handoffs : list[Agent or Handoff], optional
138
+ List of agents or handoff configurations that this agent can
139
+ delegate to for specific tasks. Default is None.
140
+ input_guardrails : list[InputGuardrail], optional
141
+ List of guardrails to validate agent inputs before processing.
142
+ Default is None.
143
+ output_guardrails : list[OutputGuardrail], optional
144
+ List of guardrails to validate agent outputs before returning.
145
+ Default is None.
146
+ session : Session, optional
147
+ Session configuration for automatically maintaining conversation
148
+ history across agent runs. Default is None.
15
149
 
16
150
  Methods
17
151
  -------
18
- print()
19
- Return a human-readable representation of the configuration.
152
+ __post_init__()
153
+ Validate configuration invariants after initialization.
154
+ instructions_text
155
+ Return the resolved instruction content as a string.
156
+ create_agent(run_context_wrapper, prompt_dir, default_model)
157
+ Create a BaseAgent instance from this configuration.
158
+ replace(**changes)
159
+ Create a new AgentConfiguration with specified fields replaced.
160
+ to_json()
161
+ Return a JSON-compatible dict (inherited from JSONSerializable).
162
+ to_json_file(filepath)
163
+ Write serialized JSON data to a file (inherited from JSONSerializable).
164
+ from_json(data)
165
+ Create an instance from a JSON-compatible dict (inherited from JSONSerializable).
166
+ from_json_file(filepath)
167
+ Load an instance from a JSON file (inherited from JSONSerializable).
168
+
169
+ Examples
170
+ --------
171
+ >>> config = AgentConfiguration(
172
+ ... name="summarizer",
173
+ ... description="Summarizes text",
174
+ ... model="gpt-4o-mini"
175
+ ... )
176
+ >>> config.name
177
+ 'summarizer'
20
178
  """
21
179
 
22
- model_config = ConfigDict(arbitrary_types_allowed=True)
23
-
24
- name: str = Field(title="Agent Name", description="Unique name for the agent")
25
- description: Optional[str] = Field(
26
- default=None, title="Description", description="Short description of the agent"
27
- )
28
- model: Optional[str] = Field(
29
- default=None, title="Model", description="Model identifier to use"
30
- )
31
- template_path: Optional[str] = Field(
32
- default=None,
33
- title="Template Path",
34
- description="Path to the Jinja template (absolute or relative to prompt_dir)",
35
- )
36
- input_type: Optional[Type[BaseModel]] = Field(
37
- default=None,
38
- title="Input Type",
39
- description="Pydantic model describing the agent input",
40
- )
41
- output_type: Optional[Type[Any]] = Field(
42
- default=None,
43
- title="Output Type",
44
- description="Type describing the agent output; commonly a Pydantic model or builtin like ``str``",
45
- )
46
- tools: Optional[List[Any]] = Field(
47
- default=None,
48
- title="Tools",
49
- description="Tools available to the agent",
50
- )
51
- model_settings: Optional[ModelSettings] = Field(
52
- default=None, title="Model Settings", description="Additional model settings"
53
- )
54
-
55
- def print(self) -> str:
56
- """Return a human-readable representation.
180
+ name: str
181
+ instructions: str | Path
182
+ description: Optional[str] = None
183
+ model: Optional[str] = None
184
+ template_path: Optional[str | Path] = None
185
+ input_type: Optional[type] = None
186
+ output_type: Optional[type] = None
187
+ tools: Optional[list] = None
188
+ model_settings: Optional[ModelSettings] = None
189
+ handoffs: Optional[list[Agent | Handoff]] = None
190
+ input_guardrails: Optional[list[InputGuardrail]] = None
191
+ output_guardrails: Optional[list[OutputGuardrail]] = None
192
+ session: Optional[Session] = None
193
+
194
+ def __post_init__(self) -> None:
195
+ """Validate configuration invariants after initialization.
196
+
197
+ Ensures that the name is a non-empty string and that instructions
198
+ are properly formatted.
199
+
200
+ Raises
201
+ ------
202
+ TypeError
203
+ If name is not a non-empty string.
204
+ If instructions is not a string or Path.
205
+ ValueError
206
+ If instructions is an empty string.
207
+ FileNotFoundError
208
+ If instructions is a Path that doesn't point to a readable file.
209
+ """
210
+ if not self.name or not isinstance(self.name, str):
211
+ raise TypeError("AgentConfiguration.name must be a non-empty str")
212
+
213
+ # Validate instructions (required field, like in Response module)
214
+ instructions_value = self.instructions
215
+ if isinstance(instructions_value, str):
216
+ if not instructions_value.strip():
217
+ raise ValueError(
218
+ "AgentConfiguration.instructions must be a non-empty str"
219
+ )
220
+ elif isinstance(instructions_value, Path):
221
+ instruction_path = instructions_value.expanduser()
222
+ if not instruction_path.is_file():
223
+ raise FileNotFoundError(
224
+ f"Instruction template not found: {instruction_path}"
225
+ )
226
+ else:
227
+ raise TypeError("AgentConfiguration.instructions must be a str or Path")
228
+
229
+ if self.template_path is not None and isinstance(self.template_path, Path):
230
+ # Validate template_path if it's a Path object
231
+ template = self.template_path.expanduser()
232
+ if not template.exists():
233
+ # We don't raise here because template_path might be relative
234
+ # and resolved later with prompt_dir
235
+ pass
236
+
237
+ @property
238
+ def instructions_text(self) -> str:
239
+ """Return the resolved instruction text.
57
240
 
58
241
  Returns
59
242
  -------
60
243
  str
61
- The agent's name.
244
+ Plain-text instructions, loading template files when necessary.
245
+ """
246
+ return self._resolve_instructions()
247
+
248
+ def _resolve_instructions(self) -> str:
249
+ """Resolve instructions from string or file path."""
250
+ return resolve_instructions_from_path(self.instructions)
251
+
252
+ def create_agent(
253
+ self,
254
+ run_context_wrapper: Any = None,
255
+ prompt_dir: Path | None = None,
256
+ default_model: str | None = None,
257
+ ) -> Any:
258
+ """Create a BaseAgent instance from this configuration.
259
+
260
+ This is a convenience method that delegates to BaseAgent.from_configuration().
261
+
262
+ Parameters
263
+ ----------
264
+ run_context_wrapper : RunContextWrapper or None, default=None
265
+ Optional wrapper providing runtime context for prompt rendering.
266
+ prompt_dir : Path or None, default=None
267
+ Optional directory holding prompt templates.
268
+ default_model : str or None, default=None
269
+ Optional fallback model identifier if config doesn't specify one.
270
+
271
+ Returns
272
+ -------
273
+ BaseAgent
274
+ Configured agent instance ready for execution.
275
+
276
+ Examples
277
+ --------
278
+ >>> config = AgentConfiguration(
279
+ ... name="helper", model="gpt-4o-mini", instructions="Help the user"
280
+ ... )
281
+ >>> agent = config.create_agent()
282
+ >>> result = agent.run_sync("Hello!")
283
+ """
284
+ # Import here to avoid circular dependency
285
+ from .base import BaseAgent
286
+
287
+ return BaseAgent.from_configuration(
288
+ config=self,
289
+ run_context_wrapper=run_context_wrapper,
290
+ prompt_dir=prompt_dir,
291
+ default_model=default_model,
292
+ )
293
+
294
+ def replace(self, **changes: Any) -> AgentConfiguration:
295
+ """Create a new AgentConfiguration with specified fields replaced.
296
+
297
+ Since AgentConfiguration is frozen (immutable), this method creates a new
298
+ instance with the specified changes applied. This is useful for
299
+ creating variations of a configuration.
300
+
301
+ Parameters
302
+ ----------
303
+ **changes : Any
304
+ Keyword arguments specifying fields to change and their new values.
305
+
306
+ Returns
307
+ -------
308
+ AgentConfiguration
309
+ New configuration instance with changes applied.
310
+
311
+ Examples
312
+ --------
313
+ >>> config = AgentConfiguration(
314
+ ... name="agent1", model="gpt-4o-mini", instructions="Agent instructions"
315
+ ... )
316
+ >>> config2 = config.replace(name="agent2", description="Modified")
317
+ >>> config2.name
318
+ 'agent2'
319
+ >>> config2.model
320
+ 'gpt-4o-mini'
62
321
  """
63
- return self.name
322
+ from dataclasses import replace
323
+
324
+ return replace(self, **changes)
325
+
326
+ @classmethod
327
+ def from_json(cls, data: dict[str, Any]) -> AgentConfiguration:
328
+ """Create an AgentConfiguration from JSON data.
329
+
330
+ Overrides the default JSONSerializable.from_json to properly handle
331
+ the instructions field, converting string paths that look like file
332
+ paths back to Path objects for proper file loading.
333
+
334
+ Parameters
335
+ ----------
336
+ data : dict[str, Any]
337
+ Dictionary containing the configuration data.
338
+
339
+ Returns
340
+ -------
341
+ AgentConfiguration
342
+ New configuration instance.
343
+
344
+ Notes
345
+ -----
346
+ This method attempts to preserve the original type of the instructions
347
+ field. If instructions is a string that represents an existing file path,
348
+ it will be converted to a Path object to ensure proper file loading
349
+ behavior is maintained across JSON round-trips.
350
+ """
351
+ # Make a copy to avoid modifying the input
352
+ data = data.copy()
353
+
354
+ # Handle instructions field: if it's a string path to an existing file,
355
+ # convert it back to Path for proper file loading
356
+ if "instructions" in data and data["instructions"] is not None:
357
+ instructions_value = data["instructions"]
358
+ if isinstance(instructions_value, str):
359
+ # Check if it looks like a file path and the file exists
360
+ # This preserves the intended behavior for file-based instructions
361
+ try:
362
+ potential_path = Path(instructions_value)
363
+ # Only convert to Path if it's an existing file
364
+ # This way, plain text instructions stay as strings
365
+ if potential_path.exists() and potential_path.is_file():
366
+ data["instructions"] = potential_path
367
+ except (OSError, ValueError):
368
+ # If path parsing fails, keep it as a string (likely plain text)
369
+ pass
64
370
 
371
+ # Use the parent class method for the rest
372
+ return super(AgentConfiguration, cls).from_json(data)
65
373
 
66
- __all__ = ["AgentConfig"]
67
374
 
68
- AgentConfig.model_rebuild()
375
+ __all__ = ["AgentConfiguration", "AgentConfigurationRegistry", "get_default_registry"]
@@ -13,8 +13,8 @@ from typing import Any, Callable, Dict, List, Optional
13
13
 
14
14
  from ..structure import TaskStructure, PlanStructure, PromptStructure
15
15
  from ..utils import JSONSerializable, ensure_directory, log
16
- from .base import AgentBase
17
- from .config import AgentConfig
16
+ from .base import BaseAgent
17
+ from .config import AgentConfiguration
18
18
  from ..structure.plan.enum import AgentEnum
19
19
 
20
20
  PromptFn = Callable[[str], PromptStructure]
@@ -23,7 +23,7 @@ ExecutePlanFn = Callable[[PlanStructure], List[str]]
23
23
  SummarizeFn = Callable[[List[str]], str]
24
24
 
25
25
 
26
- class CoordinatorAgent(AgentBase, JSONSerializable):
26
+ class CoordinatorAgent(BaseAgent, JSONSerializable):
27
27
  """Coordinate agent plans while persisting project state and outputs.
28
28
 
29
29
  Methods
@@ -44,6 +44,14 @@ class CoordinatorAgent(AgentBase, JSONSerializable):
44
44
  Return a JSON-serializable snapshot of stored project data.
45
45
  save()
46
46
  Persist the stored project data to a JSON file.
47
+ to_json()
48
+ Return a JSON-compatible dict representation (inherited from JSONSerializable).
49
+ to_json_file(filepath)
50
+ Write serialized JSON data to a file path (inherited from JSONSerializable).
51
+ from_json(data)
52
+ Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
53
+ from_json_file(filepath)
54
+ Load an instance from a JSON file (class method, inherited from JSONSerializable).
47
55
  """
48
56
 
49
57
  def __init__(
@@ -55,7 +63,7 @@ class CoordinatorAgent(AgentBase, JSONSerializable):
55
63
  summarize_fn: SummarizeFn,
56
64
  module_data_path: Path,
57
65
  name: str,
58
- config: Optional[AgentConfig] = None,
66
+ config: Optional[AgentConfiguration] = None,
59
67
  prompt_dir: Optional[Path] = None,
60
68
  default_model: Optional[str] = None,
61
69
  ) -> None:
@@ -75,7 +83,7 @@ class CoordinatorAgent(AgentBase, JSONSerializable):
75
83
  Base path for persisting project artifacts.
76
84
  name : str
77
85
  Name of the parent module for data organization.
78
- config : AgentConfig or None, default=None
86
+ config : AgentConfiguration or None, default=None
79
87
  Optional agent configuration describing prompts and metadata.
80
88
  prompt_dir : Path or None, default=None
81
89
  Optional directory holding prompt templates.
@@ -83,8 +91,9 @@ class CoordinatorAgent(AgentBase, JSONSerializable):
83
91
  Optional fallback model identifier.
84
92
  """
85
93
  if config is None:
86
- config = AgentConfig(
94
+ config = AgentConfiguration(
87
95
  name="coordinator_agent",
96
+ instructions="Coordinate agents for planning and summarization.",
88
97
  description="Coordinates agents for planning and summarization.",
89
98
  )
90
99
  super().__init__(
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  from typing import Any, Dict, Optional
11
11
 
12
- from agents import Agent, RunResult, RunResultStreaming, Runner
12
+ from agents import Agent, RunResult, RunResultStreaming, Runner, Session
13
13
 
14
14
  from openai_sdk_helpers.utils.async_utils import run_coroutine_with_fallback
15
15
 
@@ -20,6 +20,7 @@ async def run_async(
20
20
  *,
21
21
  context: Optional[Dict[str, Any]] = None,
22
22
  output_type: Optional[Any] = None,
23
+ session: Optional[Session] = None,
23
24
  ) -> Any:
24
25
  """Run an Agent asynchronously.
25
26
 
@@ -33,6 +34,8 @@ async def run_async(
33
34
  Optional context dictionary passed to the agent.
34
35
  output_type : type or None, default=None
35
36
  Optional type used to cast the final output.
37
+ session : Session or None, default=None
38
+ Optional session for maintaining conversation history.
36
39
 
37
40
  Returns
38
41
  -------
@@ -49,7 +52,7 @@ async def run_async(
49
52
  ... return result
50
53
  >>> asyncio.run(example()) # doctest: +SKIP
51
54
  """
52
- result = await Runner.run(agent, input, context=context)
55
+ result = await Runner.run(agent, input, context=context, session=session)
53
56
  if output_type is not None:
54
57
  return result.final_output_as(output_type)
55
58
  return result
@@ -61,6 +64,7 @@ def run_sync(
61
64
  *,
62
65
  context: Optional[Dict[str, Any]] = None,
63
66
  output_type: Optional[Any] = None,
67
+ session: Optional[Session] = None,
64
68
  ) -> Any:
65
69
  """Run an Agent synchronously.
66
70
 
@@ -78,6 +82,8 @@ def run_sync(
78
82
  Optional context dictionary passed to the agent.
79
83
  output_type : type or None, default=None
80
84
  Optional type used to cast the final output.
85
+ session : Session or None, default=None
86
+ Optional session for maintaining conversation history.
81
87
 
82
88
  Returns
83
89
  -------
@@ -95,7 +101,7 @@ def run_sync(
95
101
  >>> agent = Agent(name="test", instructions="test", model="gpt-4o-mini")
96
102
  >>> result = run_sync(agent, "What is 2+2?") # doctest: +SKIP
97
103
  """
98
- coro = Runner.run(agent, input, context=context)
104
+ coro = Runner.run(agent, input, context=context, session=session)
99
105
  result: RunResult = run_coroutine_with_fallback(coro)
100
106
  if output_type is not None:
101
107
  return result.final_output_as(output_type)
@@ -108,6 +114,7 @@ def run_streamed(
108
114
  *,
109
115
  context: Optional[Dict[str, Any]] = None,
110
116
  output_type: Optional[Any] = None,
117
+ session: Optional[Session] = None,
111
118
  ) -> RunResultStreaming:
112
119
  """Stream agent execution results.
113
120
 
@@ -121,6 +128,8 @@ def run_streamed(
121
128
  Optional context dictionary passed to the agent.
122
129
  output_type : type or None, default=None
123
130
  Optional type used to cast the final output.
131
+ session : Session or None, default=None
132
+ Optional session for maintaining conversation history.
124
133
 
125
134
  Returns
126
135
  -------
@@ -135,7 +144,7 @@ def run_streamed(
135
144
  >>> for chunk in result.stream_text(): # doctest: +SKIP
136
145
  ... print(chunk, end="")
137
146
  """
138
- result = Runner.run_streamed(agent, input, context=context)
147
+ result = Runner.run_streamed(agent, input, context=context, session=session)
139
148
  if output_type is not None:
140
149
  return result.final_output_as(output_type)
141
150
  return result