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.
@@ -10,28 +10,19 @@ from openai.types.responses.response_text_config_param import ResponseTextConfig
10
10
  from ..config import OpenAISettings
11
11
  from ..structure.base import BaseStructure
12
12
  from ..response.base import BaseResponse, ToolHandler
13
+ from ..utils import JSONSerializable
14
+ from ..utils.registry import BaseRegistry
15
+ from ..utils.instructions import resolve_instructions_from_path
13
16
 
14
17
  TIn = TypeVar("TIn", bound="BaseStructure")
15
18
  TOut = TypeVar("TOut", bound="BaseStructure")
16
19
 
17
20
 
18
- class ResponseRegistry:
21
+ class ResponseRegistry(BaseRegistry["ResponseConfiguration"]):
19
22
  """Registry for managing ResponseConfiguration instances.
20
23
 
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.
24
+ Inherits from BaseRegistry to provide centralized storage and retrieval
25
+ of response configurations, enabling reusable response specs across the application.
35
26
 
36
27
  Examples
37
28
  --------
@@ -49,91 +40,7 @@ class ResponseRegistry:
49
40
  'test'
50
41
  """
51
42
 
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()
43
+ pass
137
44
 
138
45
 
139
46
  # Global default registry instance
@@ -158,12 +65,13 @@ def get_default_registry() -> ResponseRegistry:
158
65
 
159
66
 
160
67
  @dataclass(frozen=True, slots=True)
161
- class ResponseConfiguration(Generic[TIn, TOut]):
68
+ class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
162
69
  """
163
70
  Represent an immutable configuration describing input and output structures.
164
71
 
165
72
  Encapsulate all metadata required to define how a request is interpreted and
166
73
  how a response is structured, while enforcing strict type and runtime safety.
74
+ Inherits from JSONSerializable to support serialization to JSON format.
167
75
 
168
76
  Parameters
169
77
  ----------
@@ -181,6 +89,12 @@ class ResponseConfiguration(Generic[TIn, TOut]):
181
89
  Structure class used to format or validate output. Schema is
182
90
  automatically generated from this structure. Must subclass
183
91
  BaseStructure. Default is None.
92
+ system_vector_store : list[str], optional
93
+ Optional list of vector store names to attach as system context.
94
+ Default is None.
95
+ data_path : Path, str, or None, optional
96
+ Optional absolute directory path for storing artifacts. If not provided,
97
+ defaults to get_data_path(class_name). Default is None.
184
98
 
185
99
  Raises
186
100
  ------
@@ -201,6 +115,14 @@ class ResponseConfiguration(Generic[TIn, TOut]):
201
115
  Validate configuration invariants and enforce BaseStructure subclassing.
202
116
  instructions_text
203
117
  Return the resolved instruction content as a string.
118
+ to_json()
119
+ Return a JSON-compatible dict representation (inherited from JSONSerializable).
120
+ to_json_file(filepath)
121
+ Write serialized JSON data to a file path (inherited from JSONSerializable).
122
+ from_json(data)
123
+ Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
124
+ from_json_file(filepath)
125
+ Load an instance from a JSON file (class method, inherited from JSONSerializable).
204
126
 
205
127
  Examples
206
128
  --------
@@ -219,6 +141,8 @@ class ResponseConfiguration(Generic[TIn, TOut]):
219
141
  tools: Optional[list]
220
142
  input_structure: Optional[Type[TIn]]
221
143
  output_structure: Optional[Type[TOut]]
144
+ system_vector_store: Optional[list[str]] = None
145
+ data_path: Optional[Path | str] = None
222
146
 
223
147
  def __post_init__(self) -> None:
224
148
  """
@@ -277,15 +201,7 @@ class ResponseConfiguration(Generic[TIn, TOut]):
277
201
  return self._resolve_instructions()
278
202
 
279
203
  def _resolve_instructions(self) -> str:
280
- if isinstance(self.instructions, Path):
281
- instruction_path = self.instructions.expanduser()
282
- try:
283
- return instruction_path.read_text(encoding="utf-8")
284
- except OSError as exc:
285
- raise ValueError(
286
- f"Unable to read instructions at '{instruction_path}': {exc}"
287
- ) from exc
288
- return self.instructions
204
+ return resolve_instructions_from_path(self.instructions)
289
205
 
290
206
  def gen_response(
291
207
  self,
@@ -328,6 +244,8 @@ class ResponseConfiguration(Generic[TIn, TOut]):
328
244
  instructions=instructions,
329
245
  tools=self.tools,
330
246
  output_structure=self.output_structure,
247
+ system_vector_store=self.system_vector_store,
248
+ data_path=self.data_path,
331
249
  tool_handlers=tool_handlers,
332
250
  openai_settings=openai_settings,
333
251
  )
@@ -50,6 +50,14 @@ class ResponseMessage(JSONSerializable):
50
50
  -------
51
51
  to_openai_format()
52
52
  Return the message content in OpenAI API format.
53
+ to_json()
54
+ Return a JSON-compatible dict representation (inherited from JSONSerializable).
55
+ to_json_file(filepath)
56
+ Write serialized JSON data to a file path (inherited from JSONSerializable).
57
+ from_json(data)
58
+ Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
59
+ from_json_file(filepath)
60
+ Load an instance from a JSON file (class method, inherited from JSONSerializable).
53
61
  """
54
62
 
55
63
  role: str # "user", "assistant", "tool", etc.
@@ -113,6 +121,14 @@ class ResponseMessages(JSONSerializable):
113
121
  Return the most recent tool message or None.
114
122
  get_last_user_message()
115
123
  Return the most recent user message or None.
124
+ to_json()
125
+ Return a JSON-compatible dict representation (inherited from JSONSerializable).
126
+ to_json_file(filepath)
127
+ Write serialized JSON data to a file path (inherited from JSONSerializable).
128
+ from_json(data)
129
+ Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
130
+ from_json_file(filepath)
131
+ Load an instance from a JSON file (class method, inherited from JSONSerializable).
116
132
  """
117
133
 
118
134
  messages: list[ResponseMessage] = field(default_factory=list)
@@ -0,0 +1,35 @@
1
+ """Utilities for resolving instructions from strings or file paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def resolve_instructions_from_path(instructions: str | Path) -> str:
9
+ """Resolve instructions from a string or file path.
10
+
11
+ Parameters
12
+ ----------
13
+ instructions : str or Path
14
+ Either plain-text instructions or a path to a file containing
15
+ instructions.
16
+
17
+ Returns
18
+ -------
19
+ str
20
+ The resolved instruction text.
21
+
22
+ Raises
23
+ ------
24
+ ValueError
25
+ If instructions is a Path that cannot be read.
26
+ """
27
+ if isinstance(instructions, Path):
28
+ instruction_path = instructions.expanduser()
29
+ try:
30
+ return instruction_path.read_text(encoding="utf-8")
31
+ except OSError as exc:
32
+ raise ValueError(
33
+ f"Unable to read instructions at '{instruction_path}': {exc}"
34
+ ) from exc
35
+ return instructions
@@ -3,14 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- from dataclasses import asdict, is_dataclass
6
+ from dataclasses import asdict, fields, is_dataclass
7
7
  from datetime import datetime
8
8
  from enum import Enum
9
9
  from pathlib import Path
10
- from typing import Any
10
+ from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
11
11
 
12
12
  from .path_utils import check_filepath
13
13
 
14
+ T = TypeVar("T", bound="JSONSerializable")
15
+
14
16
 
15
17
  def _to_jsonable(value: Any) -> Any:
16
18
  """Convert common helper types to JSON-serializable forms."""
@@ -65,7 +67,19 @@ class customJSONEncoder(json.JSONEncoder):
65
67
 
66
68
 
67
69
  class JSONSerializable:
68
- """Mixin for classes that can be serialized to JSON."""
70
+ """Mixin for classes that can be serialized to and from JSON.
71
+
72
+ Methods
73
+ -------
74
+ to_json()
75
+ Return a JSON-compatible dict representation.
76
+ to_json_file(filepath)
77
+ Write serialized JSON data to a file path.
78
+ from_json(data)
79
+ Create an instance from a JSON-compatible dict (class method).
80
+ from_json_file(filepath)
81
+ Load an instance from a JSON file (class method).
82
+ """
69
83
 
70
84
  def to_json(self) -> dict[str, Any]:
71
85
  """Return a JSON-compatible dict representation."""
@@ -77,7 +91,18 @@ class JSONSerializable:
77
91
  return _to_jsonable(self.__dict__)
78
92
 
79
93
  def to_json_file(self, filepath: str | Path) -> str:
80
- """Write serialized JSON data to a file path."""
94
+ """Write serialized JSON data to a file path.
95
+
96
+ Parameters
97
+ ----------
98
+ filepath : str or Path
99
+ Path where the JSON file will be written.
100
+
101
+ Returns
102
+ -------
103
+ str
104
+ Absolute path to the written file.
105
+ """
81
106
  target = Path(filepath)
82
107
  check_filepath(fullfilepath=str(target))
83
108
  with open(target, "w", encoding="utf-8") as handle:
@@ -90,6 +115,113 @@ class JSONSerializable:
90
115
  )
91
116
  return str(target)
92
117
 
118
+ @classmethod
119
+ def from_json(cls: type[T], data: dict[str, Any]) -> T:
120
+ """Create an instance from a JSON-compatible dict.
121
+
122
+ For dataclasses, this reconstructs Path objects and passes the
123
+ dict keys directly as constructor arguments.
124
+
125
+ Parameters
126
+ ----------
127
+ data : dict[str, Any]
128
+ JSON-compatible dictionary containing the instance data.
129
+
130
+ Returns
131
+ -------
132
+ T
133
+ New instance of the class.
134
+
135
+ Examples
136
+ --------
137
+ >>> json_data = {"name": "test", "path": "/tmp/data"}
138
+ >>> instance = MyClass.from_json(json_data)
139
+ """
140
+ if is_dataclass(cls):
141
+ # Get resolved field types using get_type_hints
142
+ try:
143
+ field_types = get_type_hints(cls)
144
+ except Exception:
145
+ # Fallback to raw annotations if get_type_hints fails
146
+ field_types = {f.name: f.type for f in fields(cls)}
147
+
148
+ converted_data = {}
149
+
150
+ for key, value in data.items():
151
+ if key in field_types:
152
+ field_type = field_types[key]
153
+
154
+ # Check if this field should be converted to Path
155
+ should_convert_to_path = False
156
+
157
+ if field_type is Path:
158
+ should_convert_to_path = True
159
+ else:
160
+ # Handle Union/Optional types
161
+ origin = get_origin(field_type)
162
+ if origin is Union:
163
+ type_args = get_args(field_type)
164
+ # Only convert to Path if:
165
+ # 1. Path is in the union AND
166
+ # 2. str is NOT in the union (to avoid converting string fields)
167
+ # OR the field name suggests it's a path (contains "path")
168
+ if Path in type_args:
169
+ if str not in type_args:
170
+ # Path-only union (e.g., Union[Path, None])
171
+ should_convert_to_path = True
172
+ elif "path" in key.lower():
173
+ # Field name contains "path", likely meant to be a path
174
+ should_convert_to_path = True
175
+
176
+ # Convert string to Path if needed
177
+ if (
178
+ should_convert_to_path
179
+ and value is not None
180
+ and isinstance(value, str)
181
+ ):
182
+ converted_data[key] = Path(value)
183
+ else:
184
+ converted_data[key] = value
185
+ else:
186
+ converted_data[key] = value
187
+
188
+ return cls(**converted_data) # type: ignore[return-value]
189
+
190
+ # For non-dataclass types, try to instantiate with data as kwargs
191
+ return cls(**data) # type: ignore[return-value]
192
+
193
+ @classmethod
194
+ def from_json_file(cls: type[T], filepath: str | Path) -> T:
195
+ """Load an instance from a JSON file.
196
+
197
+ Parameters
198
+ ----------
199
+ filepath : str or Path
200
+ Path to the JSON file to load.
201
+
202
+ Returns
203
+ -------
204
+ T
205
+ New instance of the class loaded from the file.
206
+
207
+ Raises
208
+ ------
209
+ FileNotFoundError
210
+ If the file does not exist.
211
+
212
+ Examples
213
+ --------
214
+ >>> instance = MyClass.from_json_file("config.json")
215
+ """
216
+ target = Path(filepath)
217
+ if not target.exists():
218
+ raise FileNotFoundError(f"JSON file not found: {target}")
219
+
220
+ with open(target, "r", encoding="utf-8") as handle:
221
+ data = json.load(handle)
222
+
223
+ return cls.from_json(data)
224
+
93
225
 
94
226
  __all__ = [
95
227
  "coerce_jsonable",
@@ -0,0 +1,183 @@
1
+ """Base registry class for managing configuration instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from pathlib import Path
7
+ from typing import Generic, TypeVar
8
+
9
+ from .path_utils import ensure_directory
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ class BaseRegistry(Generic[T]):
15
+ """Base registry for managing configuration instances.
16
+
17
+ Provides centralized storage and retrieval of configurations,
18
+ enabling reusable specs across the application. Configurations
19
+ are stored by name and can be retrieved or listed as needed.
20
+
21
+ Type Parameters
22
+ ---------------
23
+ T
24
+ The configuration type this registry manages.
25
+
26
+ Methods
27
+ -------
28
+ register(config)
29
+ Add a configuration to the registry.
30
+ get(name)
31
+ Retrieve a configuration by name.
32
+ list_names()
33
+ Return all registered configuration names.
34
+ clear()
35
+ Remove all registered configurations.
36
+ save_to_directory(path)
37
+ Export all registered configurations to JSON files.
38
+ load_from_directory(path)
39
+ Load configurations from JSON files in a directory.
40
+ """
41
+
42
+ def __init__(self) -> None:
43
+ """Initialize an empty registry."""
44
+ self._configs: dict[str, T] = {}
45
+
46
+ def register(self, config: T) -> None:
47
+ """Add a configuration to the registry.
48
+
49
+ Parameters
50
+ ----------
51
+ config : T
52
+ Configuration to register. Must have a 'name' attribute.
53
+
54
+ Raises
55
+ ------
56
+ ValueError
57
+ If a configuration with the same name is already registered.
58
+ """
59
+ name = getattr(config, "name")
60
+ if name in self._configs:
61
+ raise ValueError(
62
+ f"Configuration '{name}' is already registered. "
63
+ "Use a unique name or clear the registry first."
64
+ )
65
+ self._configs[name] = config
66
+
67
+ def get(self, name: str) -> T:
68
+ """Retrieve a configuration by name.
69
+
70
+ Parameters
71
+ ----------
72
+ name : str
73
+ Configuration name to look up.
74
+
75
+ Returns
76
+ -------
77
+ T
78
+ The registered configuration.
79
+
80
+ Raises
81
+ ------
82
+ KeyError
83
+ If no configuration with the given name exists.
84
+ """
85
+ if name not in self._configs:
86
+ raise KeyError(
87
+ f"No configuration named '{name}' found. "
88
+ f"Available: {list(self._configs.keys())}"
89
+ )
90
+ return self._configs[name]
91
+
92
+ def list_names(self) -> list[str]:
93
+ """Return all registered configuration names.
94
+
95
+ Returns
96
+ -------
97
+ list[str]
98
+ Sorted list of configuration names.
99
+ """
100
+ return sorted(self._configs.keys())
101
+
102
+ def clear(self) -> None:
103
+ """Remove all registered configurations."""
104
+ self._configs.clear()
105
+
106
+ def save_to_directory(self, path: Path | str) -> None:
107
+ """Export all registered configurations to JSON files in a directory.
108
+
109
+ Serializes each registered configuration to an individual JSON file
110
+ named after the configuration. Creates the directory if it does not exist.
111
+
112
+ Parameters
113
+ ----------
114
+ path : Path or str
115
+ Directory path where JSON files will be saved. Will be created if
116
+ it does not already exist.
117
+
118
+ Raises
119
+ ------
120
+ OSError
121
+ If the directory cannot be created or files cannot be written.
122
+ """
123
+ dir_path = ensure_directory(Path(path))
124
+ config_names = self.list_names()
125
+
126
+ if not config_names:
127
+ return
128
+
129
+ for config_name in config_names:
130
+ config = self.get(config_name)
131
+ filename = f"{config_name}.json"
132
+ filepath = dir_path / filename
133
+ # Call to_json_file on the config
134
+ getattr(config, "to_json_file")(filepath)
135
+
136
+ def load_from_directory(self, path: Path | str, *, config_class: type[T]) -> int:
137
+ """Load all configurations from JSON files in a directory.
138
+
139
+ Scans the directory for JSON files and attempts to load each as a
140
+ configuration. Successfully loaded configurations are registered.
141
+ If a file fails to load, a warning is issued and processing continues
142
+ with the remaining files.
143
+
144
+ Parameters
145
+ ----------
146
+ path : Path or str
147
+ Directory path containing JSON configuration files.
148
+ config_class : type[T]
149
+ The configuration class to use for deserialization.
150
+
151
+ Returns
152
+ -------
153
+ int
154
+ Number of configurations successfully loaded and registered.
155
+
156
+ Raises
157
+ ------
158
+ FileNotFoundError
159
+ If the directory does not exist.
160
+ NotADirectoryError
161
+ If the path is not a directory.
162
+ """
163
+ dir_path = Path(path)
164
+ if not dir_path.exists():
165
+ raise FileNotFoundError(f"Directory not found: {dir_path}")
166
+
167
+ if not dir_path.is_dir():
168
+ raise NotADirectoryError(f"Path is not a directory: {dir_path}")
169
+
170
+ count = 0
171
+ for json_file in sorted(dir_path.glob("*.json")):
172
+ try:
173
+ config = getattr(config_class, "from_json_file")(json_file)
174
+ self.register(config)
175
+ count += 1
176
+ except Exception as exc:
177
+ # Log warning but continue processing other files
178
+ warnings.warn(
179
+ f"Failed to load configuration from {json_file}: {exc}",
180
+ stacklevel=2,
181
+ )
182
+
183
+ return count
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openai-sdk-helpers
3
- Version: 0.1.4
3
+ Version: 0.3.0
4
4
  Summary: Composable helpers for OpenAI SDK agents, prompts, and storage
5
5
  Author: openai-sdk-helpers maintainers
6
6
  License: MIT