openai-sdk-helpers 0.0.5__py3-none-any.whl → 0.0.7__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 (49) hide show
  1. openai_sdk_helpers/__init__.py +62 -0
  2. openai_sdk_helpers/agent/__init__.py +31 -0
  3. openai_sdk_helpers/agent/base.py +330 -0
  4. openai_sdk_helpers/agent/config.py +66 -0
  5. openai_sdk_helpers/agent/project_manager.py +511 -0
  6. openai_sdk_helpers/agent/prompt_utils.py +9 -0
  7. openai_sdk_helpers/agent/runner.py +215 -0
  8. openai_sdk_helpers/agent/summarizer.py +85 -0
  9. openai_sdk_helpers/agent/translator.py +139 -0
  10. openai_sdk_helpers/agent/utils.py +47 -0
  11. openai_sdk_helpers/agent/validation.py +97 -0
  12. openai_sdk_helpers/agent/vector_search.py +462 -0
  13. openai_sdk_helpers/agent/web_search.py +404 -0
  14. openai_sdk_helpers/config.py +199 -0
  15. openai_sdk_helpers/enums/__init__.py +7 -0
  16. openai_sdk_helpers/enums/base.py +29 -0
  17. openai_sdk_helpers/environment.py +27 -0
  18. openai_sdk_helpers/prompt/__init__.py +77 -0
  19. openai_sdk_helpers/py.typed +0 -0
  20. openai_sdk_helpers/response/__init__.py +20 -0
  21. openai_sdk_helpers/response/base.py +505 -0
  22. openai_sdk_helpers/response/messages.py +211 -0
  23. openai_sdk_helpers/response/runner.py +104 -0
  24. openai_sdk_helpers/response/tool_call.py +70 -0
  25. openai_sdk_helpers/response/vector_store.py +84 -0
  26. openai_sdk_helpers/structure/__init__.py +43 -0
  27. openai_sdk_helpers/structure/agent_blueprint.py +224 -0
  28. openai_sdk_helpers/structure/base.py +713 -0
  29. openai_sdk_helpers/structure/plan/__init__.py +13 -0
  30. openai_sdk_helpers/structure/plan/enum.py +64 -0
  31. openai_sdk_helpers/structure/plan/plan.py +253 -0
  32. openai_sdk_helpers/structure/plan/task.py +122 -0
  33. openai_sdk_helpers/structure/prompt.py +24 -0
  34. openai_sdk_helpers/structure/responses.py +132 -0
  35. openai_sdk_helpers/structure/summary.py +65 -0
  36. openai_sdk_helpers/structure/validation.py +47 -0
  37. openai_sdk_helpers/structure/vector_search.py +86 -0
  38. openai_sdk_helpers/structure/web_search.py +46 -0
  39. openai_sdk_helpers/utils/__init__.py +25 -0
  40. openai_sdk_helpers/utils/core.py +300 -0
  41. openai_sdk_helpers/vector_storage/__init__.py +15 -0
  42. openai_sdk_helpers/vector_storage/cleanup.py +91 -0
  43. openai_sdk_helpers/vector_storage/storage.py +564 -0
  44. openai_sdk_helpers/vector_storage/types.py +58 -0
  45. {openai_sdk_helpers-0.0.5.dist-info → openai_sdk_helpers-0.0.7.dist-info}/METADATA +6 -3
  46. openai_sdk_helpers-0.0.7.dist-info/RECORD +51 -0
  47. openai_sdk_helpers-0.0.5.dist-info/RECORD +0 -7
  48. {openai_sdk_helpers-0.0.5.dist-info → openai_sdk_helpers-0.0.7.dist-info}/WHEEL +0 -0
  49. {openai_sdk_helpers-0.0.5.dist-info → openai_sdk_helpers-0.0.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,511 @@
1
+ """Generic project manager for coordinating agent plans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ import logging
8
+ import threading
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+
14
+ from ..structure import TaskStructure, PlanStructure, PromptStructure
15
+ from ..environment import DATETIME_FMT
16
+ from ..utils import JSONSerializable, log
17
+ from .base import AgentBase
18
+ from .config import AgentConfig
19
+ from ..structure.plan.enum import AgentEnum
20
+
21
+ PromptFn = Callable[[str], PromptStructure]
22
+ BuildPlanFn = Callable[[str], PlanStructure]
23
+ ExecutePlanFn = Callable[[PlanStructure], List[str]]
24
+ SummarizeFn = Callable[[List[str]], str]
25
+
26
+
27
+ class ProjectManager(AgentBase, JSONSerializable):
28
+ """Coordinate agent plans while persisting project state and outputs.
29
+
30
+ Methods
31
+ -------
32
+ build_prompt(prompt)
33
+ Summarize the prompt into a concise brief.
34
+ build_plan()
35
+ Create a list of ``TaskStructure`` entries for the project.
36
+ execute_plan()
37
+ Run each task sequentially while tracking status and timing.
38
+ summarize_plan(results)
39
+ Summarize a collection of result strings.
40
+ run_plan(prompt)
41
+ Execute the prompt-to-summary workflow end to end.
42
+ file_path
43
+ Path to the JSON artifact for the current run.
44
+ to_dict()
45
+ Return a JSON-serializable snapshot of stored project data.
46
+ save()
47
+ Persist the stored project data to a JSON file.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ prompt_fn: PromptFn,
53
+ build_plan_fn: BuildPlanFn,
54
+ execute_plan_fn: ExecutePlanFn,
55
+ summarize_fn: SummarizeFn,
56
+ module_data_path: Path,
57
+ module_name: str,
58
+ config: Optional[AgentConfig] = None,
59
+ prompt_dir: Optional[Path] = None,
60
+ default_model: Optional[str] = None,
61
+ ) -> None:
62
+ """Initialize the project manager with injected workflow helpers.
63
+
64
+ Parameters
65
+ ----------
66
+ prompt_fn
67
+ Callable that generates a prompt brief from the input string.
68
+ build_plan_fn
69
+ Callable that generates a plan from the prompt brief.
70
+ execute_plan_fn
71
+ Callable that executes a plan and returns results.
72
+ summarize_fn
73
+ Callable that summarizes a list of result strings.
74
+ module_data_path
75
+ Base path for persisting project artifacts.
76
+ module_name
77
+ Name of the parent module for data organization.
78
+ config
79
+ Optional agent configuration describing prompts and metadata.
80
+ prompt_dir
81
+ Optional directory holding prompt templates.
82
+ default_model
83
+ Optional fallback model identifier.
84
+
85
+ Returns
86
+ -------
87
+ None
88
+ """
89
+ if config is None:
90
+ config = AgentConfig(
91
+ name="project_manager",
92
+ description="Coordinates agents for planning and summarization.",
93
+ )
94
+ super().__init__(
95
+ config=config, prompt_dir=prompt_dir, default_model=default_model
96
+ )
97
+ self._prompt_fn = prompt_fn
98
+ self._build_plan_fn = build_plan_fn
99
+ self._execute_plan_fn = execute_plan_fn
100
+ self._summarize_fn = summarize_fn
101
+ self._module_data_path = Path(module_data_path)
102
+ self._module_name = module_name
103
+
104
+ self.prompt: Optional[str] = None
105
+ self.brief: Optional[PromptStructure] = None
106
+ self.plan: PlanStructure = PlanStructure()
107
+ self.summary: Optional[str] = None
108
+ self.start_date: Optional[datetime] = None
109
+ self.end_date: Optional[datetime] = None
110
+
111
+ def build_prompt(self, prompt: str) -> None:
112
+ """Return a concise brief for the project.
113
+
114
+ Parameters
115
+ ----------
116
+ prompt : str
117
+ The core request or goal for the project.
118
+
119
+ Returns
120
+ -------
121
+ None
122
+ """
123
+ log("build_prompt", level=logging.INFO)
124
+ self.start_date = datetime.now(timezone.utc)
125
+ self.prompt = prompt
126
+ self.brief = self._prompt_fn(prompt)
127
+ self.save()
128
+
129
+ def build_plan(self) -> None:
130
+ """Generate and store a structured plan based on the current brief.
131
+
132
+ Raises
133
+ ------
134
+ ValueError
135
+ If called before :meth:`build_prompt`.
136
+
137
+ Returns
138
+ -------
139
+ None
140
+ """
141
+ log("build_plan", level=logging.INFO)
142
+ if not self.brief:
143
+ raise ValueError("Brief is required before building a plan.")
144
+
145
+ plan = self._build_plan_fn(self.brief.prompt)
146
+ if isinstance(plan, PlanStructure):
147
+ self.plan = plan
148
+ self.save()
149
+
150
+ def execute_plan(self) -> List[str]:
151
+ """Run each task, updating status, timestamps, and recorded results.
152
+
153
+ Returns
154
+ -------
155
+ list[str]
156
+ Flattened list of results from all executed tasks.
157
+ """
158
+ log("execute_plan", level=logging.INFO)
159
+ if not self.plan:
160
+ log("No tasks to execute.", level=logging.WARNING)
161
+ return []
162
+
163
+ compiled_results = self._execute_plan_fn(self.plan)
164
+ self.save()
165
+ return compiled_results
166
+
167
+ def summarize_plan(self, results: Optional[List[str]] = None) -> str:
168
+ """Summarize a collection of task outputs.
169
+
170
+ Parameters
171
+ ----------
172
+ results : list[str], optional
173
+ List of string outputs gathered from task execution. Defaults to
174
+ ``None``, which uses the stored plan task results if available.
175
+
176
+ Returns
177
+ -------
178
+ str
179
+ Concise summary derived from the provided results.
180
+ """
181
+ log("summarize_plan", level=logging.INFO)
182
+
183
+ if results is None:
184
+ results = []
185
+ if self.plan and self.plan.tasks:
186
+ for task in self.plan.tasks:
187
+ results.extend(task.results or [])
188
+
189
+ if not results:
190
+ self.summary = ""
191
+ return self.summary
192
+
193
+ self.summary = self._summarize_fn(results)
194
+ self.end_date = datetime.now(timezone.utc)
195
+ self.save()
196
+ return self.summary
197
+
198
+ def run_plan(self, prompt: str) -> None:
199
+ """Execute the full workflow for the provided prompt.
200
+
201
+ Parameters
202
+ ----------
203
+ prompt : str
204
+ The request or question to analyze and summarize.
205
+
206
+ Returns
207
+ -------
208
+ None
209
+ """
210
+ self.build_prompt(prompt)
211
+ self.build_plan()
212
+ results = self.execute_plan()
213
+ self.summarize_plan(results)
214
+
215
+ @property
216
+ def file_path(self) -> Path:
217
+ """Return the path where the project snapshot will be stored.
218
+
219
+ Returns
220
+ -------
221
+ Path
222
+ Location of the JSON artifact for the current run.
223
+ """
224
+ if not self.start_date:
225
+ self.start_date = datetime.now(timezone.utc)
226
+ start_date_str = self.start_date.strftime(DATETIME_FMT)
227
+ return self._module_data_path / self._module_name / f"{start_date_str}.json"
228
+
229
+ def save(self) -> Path:
230
+ """Persist the current project state to disk.
231
+
232
+ Returns
233
+ -------
234
+ Path
235
+ Path to the saved JSON artifact.
236
+ """
237
+ self.to_json_file(self.file_path)
238
+ return self.file_path
239
+
240
+ @staticmethod
241
+ def _run_task(
242
+ task: TaskStructure,
243
+ agent_callable: Callable[..., Any],
244
+ aggregated_context: List[str],
245
+ ) -> Any:
246
+ """Execute a single task and return the raw result.
247
+
248
+ Parameters
249
+ ----------
250
+ task : TaskStructure
251
+ Task definition containing the callable and inputs.
252
+ agent_callable : Callable[..., Any]
253
+ Callable that executes the task prompt and returns a result.
254
+ aggregated_context : list[str]
255
+ Context combined from the task and prior task outputs.
256
+
257
+ Returns
258
+ -------
259
+ Any
260
+ Raw output from the underlying callable.
261
+ """
262
+ task_type = ProjectManager._normalize_task_type(task.task_type)
263
+ prompt_with_context = task.prompt
264
+ if aggregated_context and task_type not in {"WebAgentSearch", "VectorSearch"}:
265
+ context_block = "\n".join(aggregated_context)
266
+ prompt_with_context = f"{task.prompt}\n\nContext:\n{context_block}"
267
+
268
+ try:
269
+ if task_type == "summarizer":
270
+ summary_chunks: List[str] = [task.prompt] + aggregated_context
271
+ output = agent_callable(summary_chunks)
272
+ elif task_type in {"WebAgentSearch", "VectorSearch"}:
273
+ output = agent_callable(task.prompt)
274
+ else:
275
+ output = agent_callable(
276
+ prompt_with_context,
277
+ context=aggregated_context,
278
+ )
279
+ except TypeError:
280
+ output = agent_callable(prompt_with_context)
281
+ except Exception as exc: # pragma: no cover - defensive guard
282
+ log(
283
+ f"Task '{task.task_type}' encountered an error: {exc}",
284
+ level=logging.ERROR,
285
+ )
286
+ return f"Task error: {exc}"
287
+ return ProjectManager._resolve_result(output)
288
+
289
+ @staticmethod
290
+ def _run_task_in_thread(
291
+ task: TaskStructure,
292
+ agent_callable: Callable[..., Any],
293
+ aggregated_context: List[str],
294
+ ) -> Any:
295
+ """Execute a task in a background thread to avoid event-loop conflicts.
296
+
297
+ Parameters
298
+ ----------
299
+ task : TaskStructure
300
+ Task definition containing the callable and inputs.
301
+ agent_callable : Callable[..., Any]
302
+ Callable that executes the task prompt and returns a result.
303
+ aggregated_context : list[str]
304
+ Context combined from the task and prior task outputs.
305
+
306
+ Returns
307
+ -------
308
+ Any
309
+ Resolved output from the underlying callable.
310
+ """
311
+ result_container: Dict[str, Any] = {"result": None, "error": None}
312
+
313
+ def _runner() -> None:
314
+ try:
315
+ result_container["result"] = ProjectManager._run_task(
316
+ task,
317
+ agent_callable=agent_callable,
318
+ aggregated_context=aggregated_context,
319
+ )
320
+ except Exception as exc: # pragma: no cover - defensive guard
321
+ result_container["error"] = exc
322
+
323
+ thread = threading.Thread(target=_runner)
324
+ thread.start()
325
+ thread.join()
326
+ if result_container["error"] is not None:
327
+ raise result_container["error"]
328
+ return result_container["result"]
329
+
330
+ @staticmethod
331
+ def _resolve_result(result: Any) -> Any:
332
+ """Return awaited results when the callable is asynchronous.
333
+
334
+ Parameters
335
+ ----------
336
+ result : Any
337
+ Potentially awaitable output from a task callable.
338
+
339
+ Returns
340
+ -------
341
+ Any
342
+ Resolved output, awaited when necessary.
343
+ """
344
+ if not inspect.isawaitable(result):
345
+ return result
346
+
347
+ if isinstance(result, (asyncio.Future, asyncio.Task)):
348
+ if result.done():
349
+ return result.result()
350
+
351
+ try:
352
+ owning_loop = result.get_loop()
353
+ except AttributeError: # pragma: no cover - defensive guard
354
+ owning_loop = None
355
+ if owning_loop is not None and owning_loop.is_running():
356
+ try:
357
+ current_loop = asyncio.get_running_loop()
358
+ except RuntimeError:
359
+ current_loop = None
360
+ if current_loop is not None and current_loop is owning_loop:
361
+ raise RuntimeError(
362
+ "Cannot resolve a pending task from its owning running event loop; "
363
+ "await the task instead."
364
+ )
365
+ return asyncio.run_coroutine_threadsafe(
366
+ ProjectManager._await_wrapper(result), owning_loop
367
+ ).result()
368
+
369
+ awaitable: asyncio.Future[Any] | asyncio.Task[Any] | Any = result
370
+ coroutine = (
371
+ awaitable
372
+ if inspect.iscoroutine(awaitable)
373
+ else ProjectManager._await_wrapper(awaitable)
374
+ )
375
+
376
+ try:
377
+ loop = asyncio.get_running_loop()
378
+ except RuntimeError:
379
+ return asyncio.run(coroutine)
380
+
381
+ if loop.is_running():
382
+ resolved_result: Any = None
383
+
384
+ def _run_in_thread() -> None:
385
+ nonlocal resolved_result
386
+ resolved_result = asyncio.run(coroutine)
387
+
388
+ thread = threading.Thread(target=_run_in_thread, daemon=True)
389
+ thread.start()
390
+ thread.join()
391
+ return resolved_result
392
+
393
+ return loop.run_until_complete(coroutine)
394
+
395
+ @staticmethod
396
+ async def _await_wrapper(awaitable: Any) -> Any:
397
+ """Await a generic awaitable and return its result.
398
+
399
+ Parameters
400
+ ----------
401
+ awaitable : Any
402
+ Awaitable object to resolve.
403
+
404
+ Returns
405
+ -------
406
+ Any
407
+ Result of the awaited object.
408
+ """
409
+ return await awaitable
410
+
411
+ @staticmethod
412
+ def _normalize_results(result: Any) -> List[str]:
413
+ """Convert agent outputs into a list of strings.
414
+
415
+ Parameters
416
+ ----------
417
+ result : Any
418
+ Raw output from a task execution.
419
+
420
+ Returns
421
+ -------
422
+ list[str]
423
+ Normalized string values representing the output.
424
+ """
425
+ if result is None:
426
+ return []
427
+ if isinstance(result, list):
428
+ return [str(item) for item in result]
429
+ return [str(result)]
430
+
431
+ def _persist_task_results(self, task: TaskStructure) -> Path:
432
+ """Write task context and results to disk for future analysis.
433
+
434
+ Parameters
435
+ ----------
436
+ task : TaskStructure
437
+ Task definition containing the callable and inputs.
438
+
439
+ Returns
440
+ -------
441
+ Path
442
+ Location where the task artifact was saved.
443
+ """
444
+ run_dir = self._get_run_directory()
445
+ task_label = self._task_label(task)
446
+ file_path = run_dir / f"{task_label}.json"
447
+ task.to_json_file(str(file_path))
448
+ return file_path
449
+
450
+ def _get_run_directory(self) -> Path:
451
+ """Return (and create) the directory used to persist task artifacts.
452
+
453
+ Returns
454
+ -------
455
+ Path
456
+ Directory where task outputs are stored for the run.
457
+ """
458
+ if not hasattr(self, "_run_directory"):
459
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
460
+ self._run_directory = (
461
+ self._module_data_path
462
+ / Path(self._module_name)
463
+ / "project_manager"
464
+ / timestamp
465
+ )
466
+ self._run_directory.mkdir(parents=True, exist_ok=True)
467
+ return self._run_directory
468
+
469
+ @staticmethod
470
+ def _task_label(task: TaskStructure) -> str:
471
+ """Generate a filesystem-safe label for the task.
472
+
473
+ Parameters
474
+ ----------
475
+ task : TaskStructure
476
+ Task definition containing the callable and inputs.
477
+
478
+ Returns
479
+ -------
480
+ str
481
+ Lowercase label safe for filesystem usage.
482
+ """
483
+ task_type = ProjectManager._normalize_task_type(task.task_type)
484
+ base = (task_type or "task").replace(" ", "_").lower()
485
+ return f"{base}_{task_type}"
486
+
487
+ @staticmethod
488
+ def _normalize_task_type(task_type: AgentEnum | str) -> str:
489
+ """Return the normalized task type string.
490
+
491
+ Parameters
492
+ ----------
493
+ task_type : AgentEnum or str
494
+ Task classification to normalize.
495
+
496
+ Returns
497
+ -------
498
+ str
499
+ String representation of the task type.
500
+ """
501
+ if isinstance(task_type, AgentEnum):
502
+ return task_type.value
503
+ if task_type in AgentEnum.__members__:
504
+ return AgentEnum.__members__[task_type].value
505
+ try:
506
+ return AgentEnum(task_type).value
507
+ except ValueError:
508
+ return str(task_type)
509
+
510
+
511
+ __all__ = ["ProjectManager"]
@@ -0,0 +1,9 @@
1
+ """Shared helpers for locating bundled prompt templates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ DEFAULT_PROMPT_DIR = Path(__file__).resolve().parent.parent / "prompt"
8
+
9
+ __all__ = ["DEFAULT_PROMPT_DIR"]