openai-sdk-helpers 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. openai_sdk_helpers/__init__.py +34 -0
  2. openai_sdk_helpers/agent/__init__.py +23 -0
  3. openai_sdk_helpers/agent/base.py +432 -0
  4. openai_sdk_helpers/agent/config.py +66 -0
  5. openai_sdk_helpers/agent/project_manager.py +416 -0
  6. openai_sdk_helpers/agent/runner.py +117 -0
  7. openai_sdk_helpers/agent/utils.py +47 -0
  8. openai_sdk_helpers/agent/vector_search.py +418 -0
  9. openai_sdk_helpers/agent/web_search.py +404 -0
  10. openai_sdk_helpers/config.py +141 -0
  11. openai_sdk_helpers/enums/__init__.py +7 -0
  12. openai_sdk_helpers/enums/base.py +17 -0
  13. openai_sdk_helpers/environment.py +27 -0
  14. openai_sdk_helpers/prompt/__init__.py +77 -0
  15. openai_sdk_helpers/response/__init__.py +16 -0
  16. openai_sdk_helpers/response/base.py +477 -0
  17. openai_sdk_helpers/response/messages.py +211 -0
  18. openai_sdk_helpers/response/runner.py +42 -0
  19. openai_sdk_helpers/response/tool_call.py +70 -0
  20. openai_sdk_helpers/structure/__init__.py +57 -0
  21. openai_sdk_helpers/structure/base.py +591 -0
  22. openai_sdk_helpers/structure/plan/__init__.py +13 -0
  23. openai_sdk_helpers/structure/plan/enum.py +48 -0
  24. openai_sdk_helpers/structure/plan/plan.py +104 -0
  25. openai_sdk_helpers/structure/plan/task.py +122 -0
  26. openai_sdk_helpers/structure/prompt.py +24 -0
  27. openai_sdk_helpers/structure/responses.py +148 -0
  28. openai_sdk_helpers/structure/summary.py +65 -0
  29. openai_sdk_helpers/structure/vector_search.py +82 -0
  30. openai_sdk_helpers/structure/web_search.py +46 -0
  31. openai_sdk_helpers/utils/__init__.py +13 -0
  32. openai_sdk_helpers/utils/core.py +208 -0
  33. openai_sdk_helpers/vector_storage/__init__.py +15 -0
  34. openai_sdk_helpers/vector_storage/cleanup.py +91 -0
  35. openai_sdk_helpers/vector_storage/storage.py +501 -0
  36. openai_sdk_helpers/vector_storage/types.py +58 -0
  37. openai_sdk_helpers-0.0.2.dist-info/METADATA +137 -0
  38. openai_sdk_helpers-0.0.2.dist-info/RECORD +40 -0
  39. openai_sdk_helpers-0.0.2.dist-info/WHEEL +4 -0
  40. openai_sdk_helpers-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,591 @@
1
+ """Base structure definitions for shared agent structures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Standard library imports
6
+ import inspect
7
+ import json
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from collections.abc import Mapping, Sequence
13
+ from typing import (
14
+ Any,
15
+ ClassVar,
16
+ Dict,
17
+ List,
18
+ Optional,
19
+ Type,
20
+ TypeVar,
21
+ Union,
22
+ get_args,
23
+ get_origin,
24
+ cast,
25
+ )
26
+
27
+ # Third-party imports
28
+ from pydantic import BaseModel, ConfigDict, Field
29
+
30
+ # Internal imports
31
+ from dataclasses import dataclass
32
+ from ..utils import check_filepath, customJSONEncoder, log
33
+
34
+ T = TypeVar("T", bound="BaseStructure")
35
+ DEFAULT_DATA_PATH: Path | None = None
36
+
37
+
38
+ class BaseStructure(BaseModel):
39
+ """Base class for defining structured output formats for OpenAI Assistants.
40
+
41
+ This class provides Pydantic-based schema definition and serialization
42
+ helpers that support structured output formatting.
43
+
44
+ Methods
45
+ -------
46
+ get_prompt(add_enum_values)
47
+ Format structured prompt lines into a single output string.
48
+ get_input_prompt_list(add_enum_values)
49
+ Build a structured prompt including inherited fields.
50
+ get_schema(force_required)
51
+ Generate a JSON schema for the structure.
52
+ save_schema_to_file(force_required)
53
+ Persist the schema to disk within the application data path.
54
+ to_json()
55
+ Serialize the structure to a JSON-compatible dictionary.
56
+ to_json_file(filepath)
57
+ Write the serialized payload to ``filepath``.
58
+ from_raw_input(data)
59
+ Construct an instance from raw assistant tool-call arguments.
60
+ format_output(label, value)
61
+ Format a label/value pair for console output.
62
+ schema_overrides()
63
+ Produce ``Field`` overrides for dynamic schema customisation.
64
+ print()
65
+ Return a string representation of the structure.
66
+ console_print()
67
+ Print the string representation to stdout.
68
+ """
69
+
70
+ model_config = ConfigDict(
71
+ title="OutputStructure", use_enum_values=False, strict=True, extra="forbid"
72
+ )
73
+ DATA_PATH: ClassVar[Path | None] = DEFAULT_DATA_PATH
74
+ """Optional location for saving schema files."""
75
+
76
+ @classmethod
77
+ def get_prompt(cls, add_enum_values: bool = True) -> str:
78
+ """Format structured prompt lines into a single output string.
79
+
80
+ Parameters
81
+ ----------
82
+ add_enum_values : bool, default=True
83
+ Whether enum choices should be included in the prompt lines.
84
+
85
+ Returns
86
+ -------
87
+ str
88
+ Formatted prompt ready for display.
89
+ """
90
+ prompt_lines = cls.get_input_prompt_list(add_enum_values)
91
+ if not prompt_lines:
92
+ return "No structured prompt available."
93
+ return "# Output Format\n" + "\n".join(prompt_lines)
94
+
95
+ @classmethod
96
+ def _get_all_fields(cls) -> dict[Any, Any]:
97
+ """Collect all fields, including inherited ones, from the class hierarchy.
98
+
99
+ Returns
100
+ -------
101
+ dict[Any, Any]
102
+ Mapping of field names to model fields.
103
+ """
104
+ fields = {}
105
+ for base in reversed(cls.__mro__): # Traverse inheritance tree
106
+ if issubclass(base, BaseModel) and hasattr(base, "model_fields"):
107
+ fields.update(base.model_fields) # Merge fields from parent
108
+ return fields
109
+
110
+ @classmethod
111
+ def _get_field_prompt(
112
+ cls, field_name: str, field, add_enum_values: bool = True
113
+ ) -> str:
114
+ """Return a formatted prompt line for a single field.
115
+
116
+ Parameters
117
+ ----------
118
+ field_name : str
119
+ Name of the field being processed.
120
+ field
121
+ Pydantic ``ModelField`` instance.
122
+ add_enum_values : bool, default=True
123
+ Whether enum choices should be included.
124
+
125
+ Returns
126
+ -------
127
+ str
128
+ Single line describing the field for inclusion in the prompt.
129
+ """
130
+ title = field.title or field_name.capitalize()
131
+ description = field.description or f"Provide relevant {field_name}."
132
+ type_hint = field.annotation
133
+
134
+ # Check for enums or list of enums
135
+ enum_cls = cls._extract_enum_class(type_hint)
136
+ if enum_cls:
137
+ enum_choices_str = "\n\t\t• ".join(f"{e.name}: {e.value}" for e in enum_cls)
138
+ if add_enum_values:
139
+ enum_prompt = f" \n\t Choose from: \n\t\t• {enum_choices_str}"
140
+ else:
141
+ enum_prompt = ""
142
+
143
+ return f"- **{title}**: {description}{enum_prompt}"
144
+
145
+ # Otherwise check normal types
146
+ type_mapping = {
147
+ str: f"- **{title}**: {description}",
148
+ bool: f"- **{title}**: {description} Specify if the {title} is true or false.",
149
+ int: f"- **{title}**: {description} Provide the relevant integer value for {title}.",
150
+ float: f"- **{title}**: {description} Provide the relevant float value for {title}.",
151
+ }
152
+
153
+ return type_mapping.get(
154
+ type_hint, f"- **{title}**: Provide the relevant {title}."
155
+ )
156
+
157
+ @classmethod
158
+ def get_input_prompt_list(cls, add_enum_values: bool = True) -> list[str]:
159
+ """Dynamically build a structured prompt including inherited fields.
160
+
161
+ Parameters
162
+ ----------
163
+ add_enum_values : bool, default=True
164
+ Whether enumeration values should be included.
165
+
166
+ Returns
167
+ -------
168
+ list[str]
169
+ Prompt lines describing each field.
170
+ """
171
+ prompt_lines = []
172
+ all_fields = cls._get_all_fields()
173
+ for field_name, field in all_fields.items():
174
+ prompt_lines.append(
175
+ cls._get_field_prompt(field_name, field, add_enum_values)
176
+ )
177
+ return prompt_lines
178
+
179
+ @classmethod
180
+ def get_schema(cls, force_required: bool = False) -> dict[str, Any]:
181
+ """Generate a JSON schema for the class.
182
+
183
+ Required fields remain driven by Pydantic's schema generation defaults.
184
+
185
+ Parameters
186
+ ----------
187
+ force_required : bool, default=False
188
+ When ``True``, mark all object properties as required.
189
+
190
+ Returns
191
+ -------
192
+ dict[str, Any]
193
+ JSON schema describing the structure.
194
+ """
195
+ schema = cls.model_json_schema()
196
+
197
+ def clean_refs(obj):
198
+ if isinstance(obj, dict):
199
+ if "$ref" in obj:
200
+ for key in list(obj.keys()):
201
+ if key != "$ref":
202
+ obj.pop(key, None)
203
+ for v in obj.values():
204
+ clean_refs(v)
205
+ elif isinstance(obj, list):
206
+ for item in obj:
207
+ clean_refs(item)
208
+ return obj
209
+
210
+ cleaned_schema = cast(Dict[str, Any], clean_refs(schema))
211
+
212
+ if force_required:
213
+ return cls.apply_required_fields(cleaned_schema)
214
+
215
+ return cleaned_schema
216
+
217
+ @staticmethod
218
+ def apply_required_fields(schema: dict[str, Any]) -> dict[str, Any]:
219
+ """Force all object properties to be marked as required.
220
+
221
+ This mirrors the previous behavior that treated every field as required
222
+ in downstream JSON schemas.
223
+
224
+ Parameters
225
+ ----------
226
+ schema : dict[str, Any]
227
+ JSON schema to update in place.
228
+
229
+ Returns
230
+ -------
231
+ dict[str, Any]
232
+ Updated schema with required fields set for all objects.
233
+ """
234
+
235
+ def add_required_fields(target: dict[str, Any]) -> None:
236
+ """Ensure every object declares its required properties."""
237
+ properties = target.get("properties")
238
+ if isinstance(properties, dict) and properties:
239
+ target["required"] = sorted(properties.keys())
240
+ for value in target.values():
241
+ if isinstance(value, dict):
242
+ add_required_fields(value)
243
+ elif isinstance(value, list):
244
+ for item in value:
245
+ if isinstance(item, dict):
246
+ add_required_fields(item)
247
+
248
+ add_required_fields(schema)
249
+ return schema
250
+
251
+ @classmethod
252
+ def save_schema_to_file(cls, force_required: bool = False) -> Path:
253
+ """
254
+ Save the generated JSON schema to a file.
255
+
256
+ The schema is generated using :meth:`get_schema` and saved in the
257
+ application's data path.
258
+
259
+ Parameters
260
+ ----------
261
+ force_required : bool, default=False
262
+ When ``True``, mark all object properties as required.
263
+
264
+ Returns
265
+ -------
266
+ Path
267
+ Path to the saved schema file.
268
+ """
269
+ schema = cls.get_schema(force_required=force_required)
270
+ if cls.DATA_PATH is None:
271
+ raise RuntimeError(
272
+ "DATA_PATH is not set. Set BaseStructure.DATA_PATH before saving."
273
+ )
274
+ file_path = cls.DATA_PATH / f"{cls.__name__}_schema.json"
275
+ check_filepath(file_path)
276
+ with file_path.open("w", encoding="utf-8") as file_handle:
277
+ json.dump(schema, file_handle, indent=2, ensure_ascii=False)
278
+ return file_path
279
+
280
+ def to_json(self) -> Dict[str, Any]:
281
+ """
282
+ Serialize the Pydantic model instance to a JSON-compatible dictionary.
283
+
284
+ Enum members are converted to their values. Lists and nested dictionaries
285
+ are recursively processed.
286
+
287
+ Returns
288
+ -------
289
+ dict[str, Any]
290
+ Model instance serialized as a dictionary.
291
+ """
292
+
293
+ def convert(obj: Any) -> Any:
294
+ if isinstance(obj, Enum):
295
+ return obj.value
296
+ if isinstance(obj, BaseStructure):
297
+ return obj.to_json()
298
+ if isinstance(obj, Mapping):
299
+ return {str(k): convert(v) for k, v in obj.items()}
300
+ if isinstance(obj, Sequence) and not isinstance(
301
+ obj, (str, bytes, bytearray)
302
+ ):
303
+ return [convert(item) for item in obj]
304
+ return obj
305
+
306
+ payload = convert(self.model_dump())
307
+
308
+ def is_list_field(field) -> bool:
309
+ annotation = getattr(field, "annotation", None)
310
+ if annotation is None:
311
+ return False
312
+
313
+ origins_to_match = {list, List, Sequence, tuple, set}
314
+
315
+ origin = get_origin(annotation)
316
+ if origin in origins_to_match or annotation in origins_to_match:
317
+ return True
318
+
319
+ if origin is Union:
320
+ return any(
321
+ get_origin(arg) in origins_to_match or arg in origins_to_match
322
+ for arg in get_args(annotation)
323
+ )
324
+ return False
325
+
326
+ for name, field in self.model_fields.items():
327
+ if name not in payload:
328
+ continue
329
+ if not is_list_field(field):
330
+ continue
331
+ value = payload[name]
332
+ if value is None:
333
+ continue
334
+ if isinstance(value, (str, bytes, bytearray)):
335
+ payload[name] = [value]
336
+ elif not isinstance(value, list):
337
+ payload[name] = [value]
338
+
339
+ return payload
340
+
341
+ def to_json_file(self, filepath: str) -> str:
342
+ """Write :meth:`to_json` output to ``filepath``.
343
+
344
+ Parameters
345
+ ----------
346
+ filepath : str
347
+ Destination path for the JSON file.
348
+
349
+ Returns
350
+ -------
351
+ str
352
+ Path to the written file.
353
+ """
354
+ check_filepath(fullfilepath=filepath)
355
+ with open(file=filepath, mode="w", encoding="utf-8") as f:
356
+ json.dump(
357
+ self.to_json(), f, ensure_ascii=False, indent=4, cls=customJSONEncoder
358
+ )
359
+ return filepath
360
+
361
+ @classmethod
362
+ def _extract_enum_class(cls, field_type: Any) -> Optional[Type[Enum]]:
363
+ """
364
+ Extract an Enum class from a field's type annotation.
365
+
366
+ Handles direct Enum types, List[Enum], and Optional[Enum] (via Union).
367
+
368
+ Parameters
369
+ ----------
370
+ field_type
371
+ Type annotation of a field.
372
+
373
+ Returns
374
+ -------
375
+ type[Enum] or None
376
+ Enum class if found, otherwise ``None``.
377
+ """
378
+ origin = get_origin(field_type)
379
+ args = get_args(field_type)
380
+
381
+ if inspect.isclass(field_type) and issubclass(field_type, Enum):
382
+ return field_type
383
+ elif (
384
+ origin in {list, List}
385
+ and args
386
+ and inspect.isclass(args[0])
387
+ and issubclass(args[0], Enum)
388
+ ):
389
+ return args[0]
390
+ elif origin is Union:
391
+ for arg in args:
392
+ enum_cls = cls._extract_enum_class(arg)
393
+ if enum_cls:
394
+ return enum_cls
395
+ return None
396
+
397
+ @classmethod
398
+ def _build_enum_field_mapping(cls) -> dict[str, Type[Enum]]:
399
+ """
400
+ Build a mapping from field names to their Enum classes.
401
+
402
+ This is used by `from_raw_input` to correctly process enum values.
403
+
404
+ Returns
405
+ -------
406
+ dict[str, type[Enum]]
407
+ Mapping of field names to Enum types.
408
+ """
409
+ mapping: dict[str, Type[Enum]] = {}
410
+
411
+ for name, model_field in cls.model_fields.items():
412
+ field_type = model_field.annotation
413
+ enum_cls = cls._extract_enum_class(field_type)
414
+
415
+ if enum_cls is not None:
416
+ mapping[name] = enum_cls
417
+
418
+ return mapping
419
+
420
+ @classmethod
421
+ def from_raw_input(cls: Type[T], data: dict) -> T:
422
+ """
423
+ Construct an instance of the class from a dictionary of raw input data.
424
+
425
+ This method is particularly useful for converting data received from an
426
+ OpenAI Assistant (e.g., tool call arguments) into a Pydantic model.
427
+ It handles the conversion of string values to Enum members for fields
428
+ typed as Enum or List[Enum]. Warnings are logged for invalid enum values.
429
+
430
+ Parameters
431
+ ----------
432
+ data : dict
433
+ Raw input data payload.
434
+
435
+ Returns
436
+ -------
437
+ T
438
+ Instance populated with the processed data.
439
+ """
440
+ mapping = cls._build_enum_field_mapping()
441
+ clean_data = data.copy()
442
+
443
+ for field, enum_cls in mapping.items():
444
+ raw_value = clean_data.get(field)
445
+
446
+ if raw_value is None:
447
+ continue
448
+
449
+ # List of enum values
450
+ if isinstance(raw_value, list):
451
+ converted = []
452
+ for v in raw_value:
453
+ if isinstance(v, enum_cls):
454
+ converted.append(v)
455
+ elif isinstance(v, str):
456
+ # Check if it's a valid value
457
+ if v in enum_cls._value2member_map_:
458
+ converted.append(enum_cls(v))
459
+ # Check if it's a valid name
460
+ elif v in enum_cls.__members__:
461
+ converted.append(enum_cls.__members__[v])
462
+ else:
463
+ log(
464
+ f"[{cls.__name__}] Skipping invalid value for '{field}': '{v}' not in {enum_cls.__name__}",
465
+ level=logging.WARNING,
466
+ )
467
+ clean_data[field] = converted
468
+
469
+ # Single enum value
470
+ elif (
471
+ isinstance(raw_value, str) and raw_value in enum_cls._value2member_map_
472
+ ):
473
+ clean_data[field] = enum_cls(raw_value)
474
+
475
+ elif isinstance(raw_value, enum_cls):
476
+ # already the correct type
477
+ continue
478
+
479
+ else:
480
+ log(
481
+ message=f"[{cls.__name__}] Invalid value for '{field}': '{raw_value}' not in {enum_cls.__name__}",
482
+ level=logging.WARNING,
483
+ )
484
+ clean_data[field] = None
485
+
486
+ return cls(**clean_data)
487
+
488
+ @staticmethod
489
+ def format_output(label: str, value: Any) -> str:
490
+ """
491
+ Format a label and value for string output.
492
+
493
+ Handles None values and lists appropriately.
494
+
495
+ Parameters
496
+ ----------
497
+ label : str
498
+ Label describing the value.
499
+ value : Any
500
+ Value to format for display.
501
+
502
+ Returns
503
+ -------
504
+ str
505
+ Formatted string (for example ``"- Label: Value"``).
506
+ """
507
+ if not value:
508
+ return f"- {label}: None"
509
+ if isinstance(value, list):
510
+ return f"- {label}: {', '.join(str(v) for v in value)}"
511
+ return f"- {label}: {str(value)}"
512
+
513
+ @classmethod
514
+ def schema_overrides(cls) -> Dict[str, Any]:
515
+ """
516
+ Generate Pydantic ``Field`` overrides.
517
+
518
+ Returns
519
+ -------
520
+ dict[str, Any]
521
+ Mapping of field names to ``Field`` overrides.
522
+ """
523
+ return {}
524
+
525
+ def print(self) -> str:
526
+ """
527
+ Generate a string representation of the structure.
528
+
529
+ Returns
530
+ -------
531
+ str
532
+ Formatted string for the ``logic`` field.
533
+ """
534
+ return "\n".join(
535
+ [
536
+ BaseStructure.format_output(field, value)
537
+ for field, value in self.model_dump().items()
538
+ ]
539
+ )
540
+
541
+ def console_print(self) -> None:
542
+ """Output the result of :meth:`print` to stdout.
543
+
544
+ Returns
545
+ -------
546
+ None
547
+ """
548
+ print(self.print())
549
+
550
+
551
+ @dataclass(frozen=True)
552
+ class SchemaOptions:
553
+ """Options for schema generation helpers.
554
+
555
+ Parameters
556
+ ----------
557
+ force_required : bool, default=False
558
+ When ``True``, mark all object properties as required.
559
+ """
560
+
561
+ force_required: bool = False
562
+
563
+ def to_kwargs(self) -> dict[str, Any]:
564
+ """Return keyword arguments for schema helper calls.
565
+
566
+ Returns
567
+ -------
568
+ dict[str, Any]
569
+ Keyword arguments for schema helper methods.
570
+ """
571
+ return {"force_required": self.force_required}
572
+
573
+
574
+ def spec_field(name: str, **overrides: Any) -> Any:
575
+ """Return a Pydantic ``Field`` configured with a default title.
576
+
577
+ Parameters
578
+ ----------
579
+ name : str
580
+ Name of the field to use as the default title.
581
+ **overrides
582
+ Keyword arguments forwarded to ``pydantic.Field``.
583
+
584
+ Returns
585
+ -------
586
+ Any
587
+ Pydantic ``Field`` configured with a default title.
588
+ """
589
+ field_kwargs: Dict[str, Any] = {"title": name.replace("_", " ").title()}
590
+ field_kwargs.update(overrides)
591
+ return Field(**field_kwargs)
@@ -0,0 +1,13 @@
1
+ """Structured output models for agent tasks and plans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .enum import AgentEnum
6
+ from .plan import PlanStructure
7
+ from .task import AgentTaskStructure
8
+
9
+ __all__ = [
10
+ "AgentEnum",
11
+ "PlanStructure",
12
+ "AgentTaskStructure",
13
+ ]
@@ -0,0 +1,48 @@
1
+ """Agent task enumeration definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ...enums.base import CrosswalkJSONEnum
8
+
9
+
10
+ class AgentEnum(CrosswalkJSONEnum):
11
+ """Auto-generated enumeration for AgentEnum.
12
+
13
+ Methods
14
+ -------
15
+ CROSSWALK()
16
+ Return the raw crosswalk data for this enum.
17
+ """
18
+
19
+ WEB_SEARCH = "WebAgentSearch"
20
+ VECTOR_SEARCH = "VectorSearch"
21
+ DATA_ANALYST = "DataAnalyst"
22
+
23
+ @classmethod
24
+ def CROSSWALK(cls) -> dict[str, dict[str, Any]]:
25
+ """Return the raw crosswalk data for this enum.
26
+
27
+ Returns
28
+ -------
29
+ dict[str, dict[str, Any]]
30
+ Crosswalk mapping keyed by enum member.
31
+
32
+ Raises
33
+ ------
34
+ None
35
+
36
+ Examples
37
+ --------
38
+ >>> AgentEnum.CROSSWALK()["WEB_SEARCH"]["value"]
39
+ 'WebAgentSearch'
40
+ """
41
+ return {
42
+ "WEB_SEARCH": {"value": "WebAgentSearch"},
43
+ "VECTOR_SEARCH": {"value": "VectorSearch"},
44
+ "DATA_ANALYST": {"value": "DataAnalyst"},
45
+ }
46
+
47
+
48
+ __all__ = ["AgentEnum"]