ellements 0.2.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.
Files changed (130) hide show
  1. ellements/__init__.py +57 -0
  2. ellements/agents/__init__.py +45 -0
  3. ellements/agents/backend.py +100 -0
  4. ellements/agents/builder.py +303 -0
  5. ellements/agents/claude_backend.py +200 -0
  6. ellements/agents/controller.py +237 -0
  7. ellements/agents/openai_backend.py +187 -0
  8. ellements/agents/py.typed +0 -0
  9. ellements/agents/runner.py +358 -0
  10. ellements/agents/tools.py +30 -0
  11. ellements/benchmarking/__init__.py +52 -0
  12. ellements/benchmarking/harness.py +342 -0
  13. ellements/benchmarking/py.typed +0 -0
  14. ellements/benchmarking/results.py +173 -0
  15. ellements/cli/__init__.py +80 -0
  16. ellements/cli/adapters.py +73 -0
  17. ellements/cli/agent_tui.py +1178 -0
  18. ellements/cli/components.py +411 -0
  19. ellements/cli/printer.py +229 -0
  20. ellements/cli/py.typed +0 -0
  21. ellements/core/__init__.py +112 -0
  22. ellements/core/async_utils.py +42 -0
  23. ellements/core/budgeting/__init__.py +43 -0
  24. ellements/core/budgeting/client.py +276 -0
  25. ellements/core/budgeting/protocol.py +51 -0
  26. ellements/core/budgeting/trackers.py +177 -0
  27. ellements/core/caching/__init__.py +51 -0
  28. ellements/core/caching/cache.py +73 -0
  29. ellements/core/caching/client.py +300 -0
  30. ellements/core/caching/disk.py +128 -0
  31. ellements/core/caching/keys.py +97 -0
  32. ellements/core/caching/memory.py +104 -0
  33. ellements/core/chunking.py +262 -0
  34. ellements/core/config.py +180 -0
  35. ellements/core/exceptions.py +145 -0
  36. ellements/core/llm/__init__.py +46 -0
  37. ellements/core/llm/client.py +1124 -0
  38. ellements/core/llm/images.py +226 -0
  39. ellements/core/llm/messages.py +202 -0
  40. ellements/core/llm/model_params.py +66 -0
  41. ellements/core/llm/protocol.py +146 -0
  42. ellements/core/llm/requests.py +100 -0
  43. ellements/core/llm/structured.py +91 -0
  44. ellements/core/llm/wrapper.py +79 -0
  45. ellements/core/observability/__init__.py +39 -0
  46. ellements/core/observability/events.py +169 -0
  47. ellements/core/observability/jsonl_logger.py +244 -0
  48. ellements/core/observability/markdown_formatter.py +197 -0
  49. ellements/core/observability/observer.py +56 -0
  50. ellements/core/prompting/__init__.py +14 -0
  51. ellements/core/prompting/context.py +185 -0
  52. ellements/core/prompting/guideline.py +133 -0
  53. ellements/core/prompting/persona.py +267 -0
  54. ellements/core/prompting/sources.py +92 -0
  55. ellements/core/py.typed +0 -0
  56. ellements/core/rate_limit/__init__.py +44 -0
  57. ellements/core/rate_limit/bucket.py +85 -0
  58. ellements/core/rate_limit/client.py +216 -0
  59. ellements/core/rate_limit/protocol.py +27 -0
  60. ellements/core/templating.py +126 -0
  61. ellements/core/tools/__init__.py +51 -0
  62. ellements/core/tools/dialects.py +136 -0
  63. ellements/core/tools/executor.py +48 -0
  64. ellements/core/tools/protocol.py +36 -0
  65. ellements/core/tools/records.py +28 -0
  66. ellements/core/tools/registry.py +205 -0
  67. ellements/core/tools/schemas.py +80 -0
  68. ellements/core/tools/simple.py +119 -0
  69. ellements/core/tools/spec.py +33 -0
  70. ellements/domain_specific/__init__.py +7 -0
  71. ellements/domain_specific/finance/__init__.py +188 -0
  72. ellements/domain_specific/finance/calculations.py +837 -0
  73. ellements/domain_specific/finance/charts.py +426 -0
  74. ellements/domain_specific/finance/portfolio.py +129 -0
  75. ellements/domain_specific/finance/quant_analysis.py +279 -0
  76. ellements/domain_specific/finance/risk.py +362 -0
  77. ellements/domain_specific/finance/technical_indicators.py +241 -0
  78. ellements/domain_specific/finance/tools.py +483 -0
  79. ellements/domain_specific/finance/valuation.py +312 -0
  80. ellements/domain_specific/finance/yahoo_finance.py +1523 -0
  81. ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
  82. ellements/domain_specific/py.typed +0 -0
  83. ellements/execution/__init__.py +56 -0
  84. ellements/execution/callbacks.py +149 -0
  85. ellements/execution/catalog.py +70 -0
  86. ellements/execution/collaborative.py +191 -0
  87. ellements/execution/config.py +135 -0
  88. ellements/execution/py.typed +0 -0
  89. ellements/execution/reflection.py +156 -0
  90. ellements/execution/self_consistency.py +189 -0
  91. ellements/execution/single_call.py +48 -0
  92. ellements/execution/strategies.py +237 -0
  93. ellements/execution/tree_of_thought.py +541 -0
  94. ellements/fslm/__init__.py +108 -0
  95. ellements/fslm/builtins.py +103 -0
  96. ellements/fslm/cli.py +199 -0
  97. ellements/fslm/context.py +50 -0
  98. ellements/fslm/definition.py +163 -0
  99. ellements/fslm/det.py +173 -0
  100. ellements/fslm/dsl.py +458 -0
  101. ellements/fslm/errors.py +38 -0
  102. ellements/fslm/evaluators.py +141 -0
  103. ellements/fslm/kernel.py +603 -0
  104. ellements/fslm/loading.py +123 -0
  105. ellements/fslm/models.py +623 -0
  106. ellements/fslm/nl.py +87 -0
  107. ellements/fslm/observers.py +107 -0
  108. ellements/fslm/persistence.py +72 -0
  109. ellements/fslm/py.typed +0 -0
  110. ellements/fslm/rendering.py +551 -0
  111. ellements/fslm/visualization.py +25 -0
  112. ellements/reporting/__init__.py +12 -0
  113. ellements/reporting/charts.py +364 -0
  114. ellements/reporting/html_generation.py +132 -0
  115. ellements/reporting/py.typed +0 -0
  116. ellements/reporting/visualization.py +73 -0
  117. ellements/standard_tools/__init__.py +22 -0
  118. ellements/standard_tools/py.typed +0 -0
  119. ellements/standard_tools/terminal.py +205 -0
  120. ellements/standard_tools/web/__init__.py +37 -0
  121. ellements/standard_tools/web/browser_viewer.py +214 -0
  122. ellements/standard_tools/web/crawler.py +454 -0
  123. ellements/standard_tools/web/search.py +399 -0
  124. ellements/standard_tools/web/youtube.py +1297 -0
  125. ellements-0.2.0.dist-info/METADATA +368 -0
  126. ellements-0.2.0.dist-info/RECORD +130 -0
  127. ellements-0.2.0.dist-info/WHEEL +5 -0
  128. ellements-0.2.0.dist-info/entry_points.txt +2 -0
  129. ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
  130. ellements-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,200 @@
1
+ """Anthropic Claude Agents backend implementing :class:`AgentBackend`.
2
+
3
+ This backend wraps the ``claude_agents`` (or ``anthropic-agents``) SDK
4
+ when installed. The shape mirrors :class:`OpenAIAgentsBackend` so
5
+ higher-level code never branches on backend identity.
6
+
7
+ If the Anthropic SDK is not installed, instantiating
8
+ :class:`ClaudeAgentsBackend` raises :class:`ImportError` — the same
9
+ contract as :class:`OpenAIAgentsBackend`.
10
+
11
+ Note:
12
+ The Claude Agents SDK is still evolving; this adapter targets the
13
+ interface published as of writing (``claude_agents.Agent`` +
14
+ ``claude_agents.Runner`` with ``run``/``run_streamed``). When that
15
+ interface changes, only this file needs to follow.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import AsyncIterator
21
+ from typing import Any
22
+
23
+ from ellements.core import ToolRegistry, ToolSpec
24
+ from ellements.core.tools import bind_json_invoker
25
+
26
+ from .backend import AgentEvent
27
+
28
+
29
+ class ClaudeAgentsBackend:
30
+ """Backend that adapts Anthropic's Claude Agents SDK to ellements."""
31
+
32
+ name: str = "claude_agents"
33
+
34
+ def __init__(self) -> None:
35
+ Agent, Runner = _load_sdk()
36
+ self._Agent = Agent
37
+ self._Runner = Runner
38
+
39
+ # ── Agent construction ─────────────────────────────────────────
40
+
41
+ def create_agent(
42
+ self,
43
+ *,
44
+ name: str,
45
+ instructions: str,
46
+ tools: ToolRegistry,
47
+ model: str,
48
+ ) -> Any:
49
+ return self._Agent(
50
+ name=name,
51
+ instructions=instructions,
52
+ tools=[_adapt_spec_for_claude(spec) for spec in tools.values()],
53
+ model=model,
54
+ )
55
+
56
+ # ── Session construction ───────────────────────────────────────
57
+
58
+ def create_session(
59
+ self, session_id: str | None = None, **kwargs: Any
60
+ ) -> Any | None:
61
+ """Claude Agents has no native session shape; always returns ``None``."""
62
+ return None
63
+
64
+ # ── Run (non-streaming) ────────────────────────────────────────
65
+
66
+ async def run(
67
+ self,
68
+ agent: Any,
69
+ task: str,
70
+ *,
71
+ max_turns: int = 10,
72
+ session: Any | None = None,
73
+ **kwargs: Any,
74
+ ) -> Any:
75
+ return await self._Runner.run(
76
+ agent, task, max_turns=max_turns, session=session, **kwargs
77
+ )
78
+
79
+ # ── Run (streaming) ────────────────────────────────────────────
80
+
81
+ def stream_run(
82
+ self,
83
+ agent: Any,
84
+ task: str,
85
+ *,
86
+ max_turns: int = 10,
87
+ session: Any | None = None,
88
+ **kwargs: Any,
89
+ ) -> _ClaudeAgentStream:
90
+ streaming_result = self._Runner.run_streamed(
91
+ agent, task, max_turns=max_turns, session=session, **kwargs
92
+ )
93
+ return _ClaudeAgentStream(streaming_result)
94
+
95
+
96
+ def _load_sdk() -> tuple[Any, Any]:
97
+ try:
98
+ from claude_agents import Agent, Runner
99
+ except ImportError as exc:
100
+ raise ImportError(
101
+ "Claude Agents SDK not installed. "
102
+ "Install the appropriate Anthropic agents package "
103
+ "(currently 'claude_agents')."
104
+ ) from exc
105
+ return Agent, Runner
106
+
107
+
108
+ class _ClaudeAgentStream:
109
+ """:class:`AgentStream` impl translating Claude SDK events to AgentEvent.
110
+
111
+ Translation rules mirror :class:`_OpenAIAgentStream` so consumers
112
+ receive the same uniform event vocabulary.
113
+ """
114
+
115
+ def __init__(self, streaming_result: Any) -> None:
116
+ self._streaming_result = streaming_result
117
+
118
+ def __aiter__(self) -> AsyncIterator[AgentEvent]:
119
+ return self._iter()
120
+
121
+ async def _iter(self) -> AsyncIterator[AgentEvent]:
122
+ async for raw in self._streaming_result.stream_events():
123
+ event = _translate_event(raw)
124
+ if event is not None:
125
+ yield event
126
+
127
+ @property
128
+ def result(self) -> Any:
129
+ return self._streaming_result
130
+
131
+
132
+ def _translate_event(raw: Any) -> AgentEvent | None:
133
+ event_type = getattr(raw, "type", None) or getattr(raw, "kind", None)
134
+ if event_type in {None, "raw", "raw_response"}:
135
+ return None
136
+ if event_type in {"agent_active", "agent_updated"}:
137
+ return AgentEvent(
138
+ type="agent_active",
139
+ payload={"name": getattr(raw, "name", "Agent")},
140
+ )
141
+ if event_type in {"tool_call", "tool_use"}:
142
+ return AgentEvent(
143
+ type="tool_call",
144
+ payload={
145
+ "name": getattr(raw, "tool_name", None)
146
+ or getattr(raw, "name", "unknown"),
147
+ "arguments": getattr(raw, "arguments", None)
148
+ or getattr(raw, "input", "{}"),
149
+ },
150
+ )
151
+ if event_type in {"tool_output", "tool_result"}:
152
+ return AgentEvent(
153
+ type="tool_output",
154
+ payload={
155
+ "output": getattr(raw, "output", None)
156
+ or getattr(raw, "result", None)
157
+ },
158
+ )
159
+ if event_type in {"message", "text", "completion"}:
160
+ text = getattr(raw, "text", None) or getattr(raw, "content", "")
161
+ if not text:
162
+ return None
163
+ return AgentEvent(
164
+ type="message",
165
+ payload={
166
+ "text": str(text),
167
+ "final": bool(getattr(raw, "final", False)),
168
+ },
169
+ )
170
+ return None
171
+
172
+
173
+ class _ClaudeToolView:
174
+ """Surface a :class:`ToolSpec` as the SimpleTool-like attributes the SDK reads.
175
+
176
+ The Claude Agents SDK consumes objects exposing ``name``,
177
+ ``description``, ``params_json_schema``, and ``on_invoke_tool``. We
178
+ construct a thin adapter from each :class:`ToolSpec` rather than
179
+ forcing the SDK to learn ellements' canonical type.
180
+ """
181
+
182
+ __slots__ = ("name", "description", "params_json_schema", "_invoke", "on_invoke_tool")
183
+
184
+ def __init__(self, spec: ToolSpec) -> None:
185
+ self.name = spec.name
186
+ self.description = spec.description
187
+ self.params_json_schema = spec.params_json_schema
188
+ self._invoke = spec.invoke
189
+ self.on_invoke_tool = bind_json_invoker(spec.invoke)
190
+
191
+ async def invoke(self, **kwargs: Any) -> Any:
192
+ return await self._invoke(**kwargs)
193
+
194
+
195
+ def _adapt_spec_for_claude(spec: ToolSpec) -> Any:
196
+ """Adapt a canonical :class:`ToolSpec` to the Claude Agents SDK shape."""
197
+ return _ClaudeToolView(spec)
198
+
199
+
200
+ __all__ = ["ClaudeAgentsBackend"]
@@ -0,0 +1,237 @@
1
+ """Backend-agnostic agent controller for testable agent behavior.
2
+
3
+ Controllers encapsulate agent lifecycle, conversation state (via a
4
+ backend-provided session object), persona/guideline composition, and
5
+ statistics tracking — separately from any presentation layer (TUI,
6
+ HTTP, etc.). They operate purely through the :class:`AgentBackend`
7
+ Protocol, so the same controller works against any backend that
8
+ implements it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import inspect
14
+ import uuid
15
+ from abc import ABC, abstractmethod
16
+ from collections.abc import Awaitable, Callable
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from ellements.core import (
22
+ GuidelineLibrary,
23
+ PersonaLibrary,
24
+ ToolRegistry,
25
+ )
26
+ from ellements.core.prompting import PromptContext
27
+
28
+ from .backend import AgentBackend
29
+ from .runner import AgentRunResult, AgentRunStats, run_agent_with_progress
30
+
31
+ ProgressCallback = Callable[[str], Awaitable[None] | None]
32
+ StatsCallback = Callable[[AgentRunStats], Awaitable[None] | None]
33
+
34
+
35
+ @dataclass
36
+ class ControllerConfig:
37
+ """Static configuration for an :class:`AgentController`."""
38
+
39
+ model: str
40
+ max_turns: int = 200
41
+ show_activity_log: bool = True
42
+ persona_folder: Path | None = None
43
+ guideline_folder: Path | None = None
44
+ default_persona_id: str | None = None
45
+ default_guideline_id: str | None = None
46
+ session_storage_path: Path | None = None
47
+
48
+
49
+ class AgentController(ABC):
50
+ """Base class encapsulating the run-loop and state for one agent.
51
+
52
+ Subclasses implement :meth:`_build_agent` and
53
+ :meth:`_get_fallback_instructions`. The controller can be shared
54
+ across UIs (CLI, TUI, HTTP), tests, or batch jobs because it
55
+ exposes no presentation concerns.
56
+ """
57
+
58
+ def __init__(
59
+ self, backend: AgentBackend, config: ControllerConfig
60
+ ) -> None:
61
+ self.backend = backend
62
+ self.config = config
63
+ self.prompt_context = self._build_prompt_context(config)
64
+
65
+ self._session = self._create_session()
66
+ self._conversation_history: list[dict[str, Any]] = []
67
+ self._session_stats = AgentRunStats()
68
+
69
+ @staticmethod
70
+ def _build_prompt_context(config: ControllerConfig) -> PromptContext:
71
+ """Wire the configured persona / guideline folders into a fresh context.
72
+
73
+ Extracted from ``__init__`` so the assembly is described once,
74
+ in one place, instead of inline with the other initialization
75
+ bookkeeping. Subclasses can override this hook if they want to
76
+ substitute different libraries or pre-populate additional
77
+ slots.
78
+ """
79
+ context = PromptContext()
80
+ if config.persona_folder is not None:
81
+ context.attach_persona_library(
82
+ PersonaLibrary(config.persona_folder),
83
+ default_persona=config.default_persona_id,
84
+ fallback_to_first=True,
85
+ )
86
+ if config.guideline_folder is not None:
87
+ context.attach_guideline_library(
88
+ GuidelineLibrary(config.guideline_folder),
89
+ default_guideline=config.default_guideline_id,
90
+ )
91
+ return context
92
+
93
+ # ── Subclass hooks ─────────────────────────────────────────────
94
+
95
+ @abstractmethod
96
+ def _build_agent(self) -> Any:
97
+ """Build and return the runtime-native agent instance."""
98
+
99
+ @abstractmethod
100
+ def _get_fallback_instructions(self) -> str:
101
+ """Return fallback instructions used when :meth:`_build_agent` fails."""
102
+
103
+ # ── Public surface ─────────────────────────────────────────────
104
+
105
+ async def run(
106
+ self,
107
+ query: str,
108
+ *,
109
+ progress_callback: ProgressCallback | None = None,
110
+ stats_callback: StatsCallback | None = None,
111
+ ) -> AgentRunResult:
112
+ """Run one query through the agent and capture stats."""
113
+ try:
114
+ agent = self._build_agent()
115
+ except Exception as exc: # noqa: BLE001
116
+ if progress_callback is not None:
117
+ _maybe_await = progress_callback(
118
+ f"⚠️ Build error: {exc}. Using fallback agent.\n"
119
+ )
120
+ if inspect.isawaitable(_maybe_await):
121
+ await _maybe_await
122
+ agent = self._build_fallback_agent()
123
+
124
+ prepared_query = self._prepare_query(query)
125
+ result = await run_agent_with_progress(
126
+ self.backend,
127
+ agent,
128
+ prepared_query,
129
+ progress_callback=progress_callback,
130
+ stats_callback=stats_callback,
131
+ max_turns=self.config.max_turns,
132
+ session=self._session,
133
+ show_activity_log=self.config.show_activity_log,
134
+ )
135
+
136
+ self._conversation_history.append(
137
+ {
138
+ "query": query,
139
+ "response": result.final_output,
140
+ "stats": result.stats.to_dict(),
141
+ }
142
+ )
143
+ self._session_stats.merge(result.stats)
144
+ return result
145
+
146
+ async def reset_conversation(self) -> None:
147
+ """Clear the underlying session and reset accumulated stats."""
148
+ clear = getattr(self._session, "clear_session", None)
149
+ if clear is not None:
150
+ clear_result = clear()
151
+ if hasattr(clear_result, "__await__"):
152
+ await clear_result
153
+ self._conversation_history = []
154
+ self._session_stats = AgentRunStats()
155
+
156
+ # ── Persona / guideline pass-throughs ──────────────────────────
157
+
158
+ def set_persona_by_id(self, persona_id: str) -> None:
159
+ self.prompt_context.set_persona_by_id(persona_id)
160
+
161
+ def set_persona_from_data(self, persona_data: dict[str, Any]) -> None:
162
+ self.prompt_context.set_persona_from_data(persona_data)
163
+
164
+ def set_persona_from_path(self, path: Path | str) -> None:
165
+ self.prompt_context.set_persona_from_path(path)
166
+
167
+ def set_guideline_by_id(self, guideline_id: str) -> None:
168
+ self.prompt_context.set_guideline_by_id(guideline_id)
169
+
170
+ def set_guideline_from_text(self, text: str) -> None:
171
+ self.prompt_context.set_guideline_from_text(text)
172
+
173
+ def set_guideline_from_path(self, path: Path | str) -> None:
174
+ self.prompt_context.set_guideline_from_path(path)
175
+
176
+ def clear_guideline(self) -> None:
177
+ self.prompt_context.clear_guideline()
178
+
179
+ @property
180
+ def current_persona_id(self) -> str | None:
181
+ return self.prompt_context.current_persona_id
182
+
183
+ @property
184
+ def current_guideline_id(self) -> str | None:
185
+ return self.prompt_context.current_guideline_id
186
+
187
+ @property
188
+ def persona_library(self) -> PersonaLibrary | None:
189
+ return self.prompt_context.persona_library
190
+
191
+ @property
192
+ def guideline_library(self) -> GuidelineLibrary | None:
193
+ return self.prompt_context.guideline_library
194
+
195
+ def get_current_persona_text(self) -> str | None:
196
+ return self.prompt_context.get_persona_text()
197
+
198
+ def get_current_guideline_text(self) -> str | None:
199
+ return self.prompt_context.get_guideline_text()
200
+
201
+ # ── Stats / history ────────────────────────────────────────────
202
+
203
+ def get_session_stats(self) -> AgentRunStats:
204
+ return self._session_stats
205
+
206
+ def get_conversation_history(self) -> list[dict[str, Any]]:
207
+ return list(self._conversation_history)
208
+
209
+ def get_session(self) -> Any:
210
+ return self._session
211
+
212
+ # ── Internals ──────────────────────────────────────────────────
213
+
214
+ def _prepare_query(self, query: str) -> str:
215
+ return self.prompt_context.apply_guideline(query)
216
+
217
+ def _build_fallback_agent(self) -> Any:
218
+ return self.backend.create_agent(
219
+ name=self.__class__.__name__,
220
+ instructions=self._get_fallback_instructions(),
221
+ tools=ToolRegistry(),
222
+ model=self.config.model,
223
+ )
224
+
225
+ def _create_session(self) -> Any:
226
+ """Create a per-controller session via the backend.
227
+
228
+ The session shape is backend-specific; the controller never
229
+ introspects it directly.
230
+ """
231
+ session_id = f"controller_{uuid.uuid4().hex}"
232
+ return self.backend.create_session(
233
+ session_id, storage_path=self.config.session_storage_path
234
+ )
235
+
236
+
237
+ __all__ = ["AgentController", "ControllerConfig"]
@@ -0,0 +1,187 @@
1
+ """OpenAI Agents SDK backend implementing :class:`AgentBackend`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from collections.abc import AsyncIterator
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from ellements.core import ToolRegistry, ToolSpec
11
+ from ellements.core.tools import bind_json_invoker
12
+
13
+ from .backend import AgentEvent
14
+
15
+
16
+ class OpenAIAgentsBackend:
17
+ """Backend that adapts the ``openai-agents`` SDK to ellements."""
18
+
19
+ name: str = "openai_agents"
20
+
21
+ def __init__(self) -> None:
22
+ try:
23
+ from agents import Agent, Runner
24
+
25
+ self._Agent = Agent
26
+ self._Runner = Runner
27
+ except ImportError as exc:
28
+ raise ImportError(
29
+ "openai-agents package not installed. "
30
+ "Install with: pip install 'ellements[agents]'"
31
+ ) from exc
32
+
33
+ # ── Agent construction ─────────────────────────────────────────
34
+
35
+ def create_agent(
36
+ self,
37
+ *,
38
+ name: str,
39
+ instructions: str,
40
+ tools: ToolRegistry,
41
+ model: str,
42
+ ) -> Any:
43
+ return self._Agent(
44
+ name=name,
45
+ instructions=instructions,
46
+ tools=[_adapt_spec_for_openai(spec) for spec in tools.values()],
47
+ model=model,
48
+ )
49
+
50
+ # ── Session construction ───────────────────────────────────────
51
+
52
+ def create_session(
53
+ self,
54
+ session_id: str | None = None,
55
+ *,
56
+ storage_path: Path | str | None = None,
57
+ ) -> Any | None:
58
+ """Create an ``openai-agents`` ``SQLiteSession`` instance.
59
+
60
+ Returns ``None`` if the SDK does not expose ``SQLiteSession``.
61
+ """
62
+ try:
63
+ from agents import SQLiteSession
64
+ except ImportError:
65
+ return None
66
+ sid = session_id or f"openai_agents_{uuid.uuid4().hex}"
67
+ if storage_path is None:
68
+ return SQLiteSession(sid)
69
+ return SQLiteSession(sid, str(storage_path))
70
+
71
+ # ── Run (non-streaming) ────────────────────────────────────────
72
+
73
+ async def run(
74
+ self,
75
+ agent: Any,
76
+ task: str,
77
+ *,
78
+ max_turns: int = 10,
79
+ session: Any | None = None,
80
+ **kwargs: Any,
81
+ ) -> Any:
82
+ return await self._Runner.run(
83
+ agent, task, max_turns=max_turns, session=session, **kwargs
84
+ )
85
+
86
+ # ── Run (streaming) ────────────────────────────────────────────
87
+
88
+ def stream_run(
89
+ self,
90
+ agent: Any,
91
+ task: str,
92
+ *,
93
+ max_turns: int = 10,
94
+ session: Any | None = None,
95
+ **kwargs: Any,
96
+ ) -> _OpenAIAgentStream:
97
+ streaming_result = self._Runner.run_streamed(
98
+ agent, task, max_turns=max_turns, session=session, **kwargs
99
+ )
100
+ return _OpenAIAgentStream(streaming_result)
101
+
102
+
103
+ class _OpenAIAgentStream:
104
+ """:class:`AgentStream` impl that translates SDK events to AgentEvent."""
105
+
106
+ def __init__(self, streaming_result: Any) -> None:
107
+ self._streaming_result = streaming_result
108
+
109
+ def __aiter__(self) -> AsyncIterator[AgentEvent]:
110
+ return self._iter()
111
+
112
+ async def _iter(self) -> AsyncIterator[AgentEvent]:
113
+ async for raw in self._streaming_result.stream_events():
114
+ event = _translate_event(raw)
115
+ if event is not None:
116
+ yield event
117
+
118
+ @property
119
+ def result(self) -> Any:
120
+ return self._streaming_result
121
+
122
+
123
+ def _translate_event(raw: Any) -> AgentEvent | None:
124
+ """Map an OpenAI Agents SDK event into a uniform :class:`AgentEvent`."""
125
+ event_type = getattr(raw, "type", None)
126
+ if event_type == "raw_response_event":
127
+ return None
128
+ if event_type == "agent_updated_stream_event":
129
+ new_agent = getattr(raw, "new_agent", None)
130
+ return AgentEvent(
131
+ type="agent_active",
132
+ payload={"name": getattr(new_agent, "name", "Agent")},
133
+ )
134
+ if event_type != "run_item_stream_event":
135
+ return None
136
+ item = getattr(raw, "item", None)
137
+ if item is None:
138
+ return None
139
+ item_type = getattr(item, "type", None)
140
+ if item_type == "tool_call_item":
141
+ raw_item = getattr(item, "raw_item", None) or item
142
+ return AgentEvent(
143
+ type="tool_call",
144
+ payload={
145
+ "name": getattr(raw_item, "name", "unknown"),
146
+ "arguments": getattr(raw_item, "arguments", "{}"),
147
+ },
148
+ )
149
+ if item_type == "tool_call_output_item":
150
+ return AgentEvent(
151
+ type="tool_output",
152
+ payload={"output": getattr(item, "output", None)},
153
+ )
154
+ if item_type == "message_output_item":
155
+ raw_item = getattr(item, "raw_item", None)
156
+ text = _extract_message_text(raw_item)
157
+ if not text:
158
+ return None
159
+ return AgentEvent(
160
+ type="message",
161
+ payload={"text": text, "final": False},
162
+ )
163
+ return None
164
+
165
+
166
+ def _extract_message_text(raw_item: Any) -> str:
167
+ if raw_item is None or not hasattr(raw_item, "content"):
168
+ return ""
169
+ content_items = raw_item.content or []
170
+ return "\n".join(
171
+ getattr(c, "text", "") for c in content_items if hasattr(c, "text")
172
+ )
173
+
174
+
175
+ def _adapt_spec_for_openai(spec: ToolSpec) -> Any:
176
+ """Adapt a canonical :class:`ToolSpec` to the OpenAI Agents SDK shape."""
177
+ from agents.tool import FunctionTool
178
+
179
+ return FunctionTool(
180
+ name=spec.name,
181
+ description=spec.description,
182
+ params_json_schema=spec.params_json_schema,
183
+ on_invoke_tool=bind_json_invoker(spec.invoke),
184
+ )
185
+
186
+
187
+ __all__ = ["OpenAIAgentsBackend"]
File without changes