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.
- ellements/__init__.py +57 -0
- ellements/agents/__init__.py +45 -0
- ellements/agents/backend.py +100 -0
- ellements/agents/builder.py +303 -0
- ellements/agents/claude_backend.py +200 -0
- ellements/agents/controller.py +237 -0
- ellements/agents/openai_backend.py +187 -0
- ellements/agents/py.typed +0 -0
- ellements/agents/runner.py +358 -0
- ellements/agents/tools.py +30 -0
- ellements/benchmarking/__init__.py +52 -0
- ellements/benchmarking/harness.py +342 -0
- ellements/benchmarking/py.typed +0 -0
- ellements/benchmarking/results.py +173 -0
- ellements/cli/__init__.py +80 -0
- ellements/cli/adapters.py +73 -0
- ellements/cli/agent_tui.py +1178 -0
- ellements/cli/components.py +411 -0
- ellements/cli/printer.py +229 -0
- ellements/cli/py.typed +0 -0
- ellements/core/__init__.py +112 -0
- ellements/core/async_utils.py +42 -0
- ellements/core/budgeting/__init__.py +43 -0
- ellements/core/budgeting/client.py +276 -0
- ellements/core/budgeting/protocol.py +51 -0
- ellements/core/budgeting/trackers.py +177 -0
- ellements/core/caching/__init__.py +51 -0
- ellements/core/caching/cache.py +73 -0
- ellements/core/caching/client.py +300 -0
- ellements/core/caching/disk.py +128 -0
- ellements/core/caching/keys.py +97 -0
- ellements/core/caching/memory.py +104 -0
- ellements/core/chunking.py +262 -0
- ellements/core/config.py +180 -0
- ellements/core/exceptions.py +145 -0
- ellements/core/llm/__init__.py +46 -0
- ellements/core/llm/client.py +1124 -0
- ellements/core/llm/images.py +226 -0
- ellements/core/llm/messages.py +202 -0
- ellements/core/llm/model_params.py +66 -0
- ellements/core/llm/protocol.py +146 -0
- ellements/core/llm/requests.py +100 -0
- ellements/core/llm/structured.py +91 -0
- ellements/core/llm/wrapper.py +79 -0
- ellements/core/observability/__init__.py +39 -0
- ellements/core/observability/events.py +169 -0
- ellements/core/observability/jsonl_logger.py +244 -0
- ellements/core/observability/markdown_formatter.py +197 -0
- ellements/core/observability/observer.py +56 -0
- ellements/core/prompting/__init__.py +14 -0
- ellements/core/prompting/context.py +185 -0
- ellements/core/prompting/guideline.py +133 -0
- ellements/core/prompting/persona.py +267 -0
- ellements/core/prompting/sources.py +92 -0
- ellements/core/py.typed +0 -0
- ellements/core/rate_limit/__init__.py +44 -0
- ellements/core/rate_limit/bucket.py +85 -0
- ellements/core/rate_limit/client.py +216 -0
- ellements/core/rate_limit/protocol.py +27 -0
- ellements/core/templating.py +126 -0
- ellements/core/tools/__init__.py +51 -0
- ellements/core/tools/dialects.py +136 -0
- ellements/core/tools/executor.py +48 -0
- ellements/core/tools/protocol.py +36 -0
- ellements/core/tools/records.py +28 -0
- ellements/core/tools/registry.py +205 -0
- ellements/core/tools/schemas.py +80 -0
- ellements/core/tools/simple.py +119 -0
- ellements/core/tools/spec.py +33 -0
- ellements/domain_specific/__init__.py +7 -0
- ellements/domain_specific/finance/__init__.py +188 -0
- ellements/domain_specific/finance/calculations.py +837 -0
- ellements/domain_specific/finance/charts.py +426 -0
- ellements/domain_specific/finance/portfolio.py +129 -0
- ellements/domain_specific/finance/quant_analysis.py +279 -0
- ellements/domain_specific/finance/risk.py +362 -0
- ellements/domain_specific/finance/technical_indicators.py +241 -0
- ellements/domain_specific/finance/tools.py +483 -0
- ellements/domain_specific/finance/valuation.py +312 -0
- ellements/domain_specific/finance/yahoo_finance.py +1523 -0
- ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
- ellements/domain_specific/py.typed +0 -0
- ellements/execution/__init__.py +56 -0
- ellements/execution/callbacks.py +149 -0
- ellements/execution/catalog.py +70 -0
- ellements/execution/collaborative.py +191 -0
- ellements/execution/config.py +135 -0
- ellements/execution/py.typed +0 -0
- ellements/execution/reflection.py +156 -0
- ellements/execution/self_consistency.py +189 -0
- ellements/execution/single_call.py +48 -0
- ellements/execution/strategies.py +237 -0
- ellements/execution/tree_of_thought.py +541 -0
- ellements/fslm/__init__.py +108 -0
- ellements/fslm/builtins.py +103 -0
- ellements/fslm/cli.py +199 -0
- ellements/fslm/context.py +50 -0
- ellements/fslm/definition.py +163 -0
- ellements/fslm/det.py +173 -0
- ellements/fslm/dsl.py +458 -0
- ellements/fslm/errors.py +38 -0
- ellements/fslm/evaluators.py +141 -0
- ellements/fslm/kernel.py +603 -0
- ellements/fslm/loading.py +123 -0
- ellements/fslm/models.py +623 -0
- ellements/fslm/nl.py +87 -0
- ellements/fslm/observers.py +107 -0
- ellements/fslm/persistence.py +72 -0
- ellements/fslm/py.typed +0 -0
- ellements/fslm/rendering.py +551 -0
- ellements/fslm/visualization.py +25 -0
- ellements/reporting/__init__.py +12 -0
- ellements/reporting/charts.py +364 -0
- ellements/reporting/html_generation.py +132 -0
- ellements/reporting/py.typed +0 -0
- ellements/reporting/visualization.py +73 -0
- ellements/standard_tools/__init__.py +22 -0
- ellements/standard_tools/py.typed +0 -0
- ellements/standard_tools/terminal.py +205 -0
- ellements/standard_tools/web/__init__.py +37 -0
- ellements/standard_tools/web/browser_viewer.py +214 -0
- ellements/standard_tools/web/crawler.py +454 -0
- ellements/standard_tools/web/search.py +399 -0
- ellements/standard_tools/web/youtube.py +1297 -0
- ellements-0.2.0.dist-info/METADATA +368 -0
- ellements-0.2.0.dist-info/RECORD +130 -0
- ellements-0.2.0.dist-info/WHEEL +5 -0
- ellements-0.2.0.dist-info/entry_points.txt +2 -0
- ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
- 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
|