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
ellements/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Curated root API and namespace bootstrap for the public Ellements repo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from pkgutil import extend_path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
__path__ = extend_path(__path__, __name__)
|
|
12
|
+
|
|
13
|
+
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
14
|
+
_SOURCE_ROOTS = [
|
|
15
|
+
"ellements-agents/src/ellements",
|
|
16
|
+
"ellements-benchmarking/src/ellements",
|
|
17
|
+
"ellements-cli/src/ellements",
|
|
18
|
+
"ellements-core/src/ellements",
|
|
19
|
+
"ellements-domain-specific/src/ellements",
|
|
20
|
+
"ellements-execution/src/ellements",
|
|
21
|
+
"ellements-fslm/src/ellements",
|
|
22
|
+
"ellements-reporting/src/ellements",
|
|
23
|
+
"ellements-standard-tools/src/ellements",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
for relative_root in _SOURCE_ROOTS:
|
|
27
|
+
candidate = _REPO_ROOT / relative_root
|
|
28
|
+
if candidate.is_dir():
|
|
29
|
+
candidate_str = str(candidate)
|
|
30
|
+
if candidate_str not in __path__:
|
|
31
|
+
__path__.append(candidate_str)
|
|
32
|
+
|
|
33
|
+
_ROOT_EXPORTS: dict[str, tuple[str, str]] = {
|
|
34
|
+
"Conversation": ("ellements.core", "Conversation"),
|
|
35
|
+
"ImageInput": ("ellements.core", "ImageInput"),
|
|
36
|
+
"LLMClient": ("ellements.core", "LLMClient"),
|
|
37
|
+
"SimpleTool": ("ellements.core", "SimpleTool"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
__version__ = version("ellements")
|
|
42
|
+
except PackageNotFoundError:
|
|
43
|
+
__version__ = "0.2.0"
|
|
44
|
+
|
|
45
|
+
__all__ = [*sorted(_ROOT_EXPORTS), "__version__"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def __getattr__(name: str) -> Any:
|
|
49
|
+
"""Resolve curated public root exports lazily."""
|
|
50
|
+
if name not in _ROOT_EXPORTS:
|
|
51
|
+
raise AttributeError(f"module 'ellements' has no attribute {name!r}")
|
|
52
|
+
module_name, attr_name = _ROOT_EXPORTS[name]
|
|
53
|
+
return getattr(import_module(module_name), attr_name)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def __dir__() -> list[str]:
|
|
57
|
+
return sorted(set(globals()) | set(_ROOT_EXPORTS))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Agent framework integration for ellements.
|
|
2
|
+
|
|
3
|
+
The package is built around the :class:`AgentBackend` Protocol so any
|
|
4
|
+
agent runtime (OpenAI Agents SDK, Anthropic Claude Agents, future
|
|
5
|
+
others) can be wired in without changing higher-level code. Two
|
|
6
|
+
concrete backends ship with the package:
|
|
7
|
+
|
|
8
|
+
- :class:`OpenAIAgentsBackend`
|
|
9
|
+
- :class:`ClaudeAgentsBackend`
|
|
10
|
+
|
|
11
|
+
Higher-level helpers (:class:`AgentBuilder`, :class:`AgentController`,
|
|
12
|
+
:class:`AgentRunStats`, :class:`AgentRunResult`,
|
|
13
|
+
:func:`run_agent_with_progress`) are backend-agnostic and operate
|
|
14
|
+
through the protocol.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .backend import AgentBackend, AgentEvent
|
|
18
|
+
from .builder import AgentBuilder, check_openai_api_key, load_prompt_file
|
|
19
|
+
from .claude_backend import ClaudeAgentsBackend
|
|
20
|
+
from .controller import AgentController, ControllerConfig
|
|
21
|
+
from .openai_backend import OpenAIAgentsBackend
|
|
22
|
+
from .runner import (
|
|
23
|
+
DEFAULT_MAX_TURNS,
|
|
24
|
+
AgentRunResult,
|
|
25
|
+
AgentRunStats,
|
|
26
|
+
run_agent_with_progress,
|
|
27
|
+
)
|
|
28
|
+
from .tools import create_tool_from_method
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"AgentBackend",
|
|
32
|
+
"AgentBuilder",
|
|
33
|
+
"AgentController",
|
|
34
|
+
"AgentEvent",
|
|
35
|
+
"AgentRunResult",
|
|
36
|
+
"AgentRunStats",
|
|
37
|
+
"ClaudeAgentsBackend",
|
|
38
|
+
"ControllerConfig",
|
|
39
|
+
"DEFAULT_MAX_TURNS",
|
|
40
|
+
"OpenAIAgentsBackend",
|
|
41
|
+
"check_openai_api_key",
|
|
42
|
+
"create_tool_from_method",
|
|
43
|
+
"load_prompt_file",
|
|
44
|
+
"run_agent_with_progress",
|
|
45
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""The :class:`AgentBackend` Protocol — the single integration point.
|
|
2
|
+
|
|
3
|
+
Any agent framework that satisfies this Protocol can be plugged into
|
|
4
|
+
:class:`~ellements.agents.AgentBuilder`, :class:`AgentController`, and
|
|
5
|
+
:func:`run_agent_with_progress` without further changes.
|
|
6
|
+
|
|
7
|
+
:class:`AgentEvent` — the uniform vocabulary backends use to surface
|
|
8
|
+
streaming events — lives in :mod:`ellements.core.observability.events`
|
|
9
|
+
alongside the other observability primitives. It is re-exported from
|
|
10
|
+
this module for backend authors who only deal with the agent surface.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import AsyncIterator
|
|
16
|
+
from typing import Any, Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
from ellements.core import ToolRegistry
|
|
19
|
+
from ellements.core.observability import AgentEvent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class AgentBackend(Protocol):
|
|
24
|
+
"""Adapts an agent runtime to ellements' uniform agent surface."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
"""Stable backend identifier, e.g. ``"openai_agents"``."""
|
|
28
|
+
|
|
29
|
+
def create_agent(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
name: str,
|
|
33
|
+
instructions: str,
|
|
34
|
+
tools: ToolRegistry,
|
|
35
|
+
model: str,
|
|
36
|
+
) -> Any:
|
|
37
|
+
"""Construct a runtime-native agent instance.
|
|
38
|
+
|
|
39
|
+
*tools* is the canonical :class:`~ellements.core.ToolRegistry`;
|
|
40
|
+
each backend adapts the contained :class:`~ellements.core.ToolSpec`
|
|
41
|
+
instances to its native tool type.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def create_session(
|
|
45
|
+
self, session_id: str | None = None, **kwargs: Any
|
|
46
|
+
) -> Any | None:
|
|
47
|
+
"""Construct a runtime-native session object, or ``None``.
|
|
48
|
+
|
|
49
|
+
Backends that don't support sessions return ``None``. Backends
|
|
50
|
+
that support sessions but were given ``None`` for *session_id*
|
|
51
|
+
should also return ``None`` (caller wants no session).
|
|
52
|
+
|
|
53
|
+
Extra backend-specific options (e.g. ``storage_path`` for
|
|
54
|
+
``OpenAIAgentsBackend``) are passed via ``**kwargs``; backends
|
|
55
|
+
ignore options they don't recognize.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
async def run(
|
|
59
|
+
self,
|
|
60
|
+
agent: Any,
|
|
61
|
+
task: str,
|
|
62
|
+
*,
|
|
63
|
+
max_turns: int = 10,
|
|
64
|
+
session: Any | None = None,
|
|
65
|
+
**kwargs: Any,
|
|
66
|
+
) -> Any:
|
|
67
|
+
"""Run *agent* on *task* and return its native result object.
|
|
68
|
+
|
|
69
|
+
Implementations should propagate :class:`asyncio.CancelledError`
|
|
70
|
+
unchanged.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def stream_run(
|
|
74
|
+
self,
|
|
75
|
+
agent: Any,
|
|
76
|
+
task: str,
|
|
77
|
+
*,
|
|
78
|
+
max_turns: int = 10,
|
|
79
|
+
session: Any | None = None,
|
|
80
|
+
**kwargs: Any,
|
|
81
|
+
) -> AgentStream:
|
|
82
|
+
"""Run *agent* on *task* and return a streaming handle.
|
|
83
|
+
|
|
84
|
+
Use :class:`AgentStream` as both an async iterator (over
|
|
85
|
+
:class:`AgentEvent`) and a final-result holder accessible via
|
|
86
|
+
:attr:`AgentStream.result` after iteration completes.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AgentStream(Protocol):
|
|
91
|
+
"""Combined async-iterator + final-result handle for streaming runs."""
|
|
92
|
+
|
|
93
|
+
def __aiter__(self) -> AsyncIterator[AgentEvent]: ...
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def result(self) -> Any:
|
|
97
|
+
"""The native runtime result, populated once streaming completes."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = ["AgentBackend", "AgentEvent", "AgentStream"]
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Fluent builder for assembling agents on top of any :class:`AgentBackend`.
|
|
2
|
+
|
|
3
|
+
The builder unifies three concerns:
|
|
4
|
+
|
|
5
|
+
1. **Instructions** — direct text, a file, or a rendered template.
|
|
6
|
+
2. **Persona + Guideline** — composed through
|
|
7
|
+
:class:`ellements.core.PromptContext` + :class:`PersonaLibrary` /
|
|
8
|
+
:class:`GuidelineLibrary`.
|
|
9
|
+
3. **Tools** — accumulated in a :class:`~ellements.core.ToolRegistry`
|
|
10
|
+
and forwarded to the backend, which adapts them to its native
|
|
11
|
+
tool format.
|
|
12
|
+
|
|
13
|
+
There are **no** alias methods, **no** ``run_sync`` flavours, **no**
|
|
14
|
+
``.mustache.md`` legacy branch, and **no** lazy circular-import hacks.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from collections.abc import Callable, Mapping
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from ellements.core import (
|
|
25
|
+
GuidelineLibrary,
|
|
26
|
+
PersonaLibrary,
|
|
27
|
+
ToolRegistry,
|
|
28
|
+
)
|
|
29
|
+
from ellements.core.prompting import PromptContext
|
|
30
|
+
from ellements.core.templating import render_prompt_template
|
|
31
|
+
|
|
32
|
+
from .backend import AgentBackend
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AgentBuilder:
|
|
36
|
+
"""Compose an agent backend with tools, instructions, and prompt context.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> from ellements.agents import AgentBuilder, OpenAIAgentsBackend
|
|
40
|
+
>>> agent = (
|
|
41
|
+
... AgentBuilder("YouTubeExpert", backend=OpenAIAgentsBackend())
|
|
42
|
+
... .with_instructions("Find the best YouTube videos")
|
|
43
|
+
... .with_model("gpt-4o-mini")
|
|
44
|
+
... .with_tools(youtube_search_tools())
|
|
45
|
+
... .build()
|
|
46
|
+
... )
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, name: str, *, backend: AgentBackend) -> None:
|
|
50
|
+
self.name = name
|
|
51
|
+
self.backend = backend
|
|
52
|
+
self.model: str | None = None
|
|
53
|
+
self.tools = ToolRegistry()
|
|
54
|
+
self.instructions: str | None = None
|
|
55
|
+
self.instruction_template: str | None = None
|
|
56
|
+
self.template_renderer: Callable[..., str] | None = None
|
|
57
|
+
self.prompt_context = PromptContext()
|
|
58
|
+
|
|
59
|
+
# ── Identity / model ───────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def with_model(self, model: str) -> AgentBuilder:
|
|
62
|
+
self.model = model
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
# ── Tools ──────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def with_tools(self, tools: ToolRegistry | Mapping[str, Any]) -> AgentBuilder:
|
|
68
|
+
"""Bulk-add tools from a :class:`ToolRegistry` or ``{name: callable}`` mapping."""
|
|
69
|
+
for name, tool in tools.items():
|
|
70
|
+
self.tools.register(tool, name=name)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def with_tool(self, name: str, tool: Any) -> AgentBuilder:
|
|
74
|
+
"""Add a single tool under *name*."""
|
|
75
|
+
self.tools.register(tool, name=name)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
# ── Instructions ───────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
def with_instructions(self, instructions: str) -> AgentBuilder:
|
|
81
|
+
"""Set inline instructions text."""
|
|
82
|
+
self.instructions = instructions
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def with_instructions_from_file(
|
|
86
|
+
self, path: Path | str, *, fallback: str | None = None
|
|
87
|
+
) -> AgentBuilder:
|
|
88
|
+
"""Load instructions from *path*.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
FileNotFoundError: When the file is missing and no
|
|
92
|
+
``fallback`` is provided.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
self.instructions = Path(path).read_text(encoding="utf-8")
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
if fallback is None:
|
|
98
|
+
raise
|
|
99
|
+
self.instructions = fallback
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def with_instructions_template(
|
|
103
|
+
self,
|
|
104
|
+
template_name: str,
|
|
105
|
+
*,
|
|
106
|
+
renderer: Callable[..., str] | None = None,
|
|
107
|
+
) -> AgentBuilder:
|
|
108
|
+
"""Render instructions from a Mustache template.
|
|
109
|
+
|
|
110
|
+
For split templates the builder loads
|
|
111
|
+
``{template_name}.system.mustache.md`` for instructions and
|
|
112
|
+
``{template_name}.user.mustache.md`` (when present) for the
|
|
113
|
+
guideline message returned by
|
|
114
|
+
:meth:`get_guideline_user_message`.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
template_name: Template basename. May be qualified with a
|
|
118
|
+
package-relative path (e.g. ``"writing/poem"``).
|
|
119
|
+
renderer: Override the default ``render_prompt_template``
|
|
120
|
+
helper (which loads from this package's ``prompts/``).
|
|
121
|
+
"""
|
|
122
|
+
self.instruction_template = template_name
|
|
123
|
+
self.template_renderer = renderer or _default_template_renderer
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
# ── Persona ────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
def with_persona(self, persona_folder: Path | str) -> AgentBuilder:
|
|
129
|
+
"""Attach a :class:`PersonaLibrary` loaded from *persona_folder*."""
|
|
130
|
+
self.prompt_context.attach_persona_library(
|
|
131
|
+
PersonaLibrary(persona_folder), fallback_to_first=True
|
|
132
|
+
)
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def with_persona_id(self, persona_id: str) -> AgentBuilder:
|
|
136
|
+
"""Select an active persona by its registered ID."""
|
|
137
|
+
if self.prompt_context.persona_library is None:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"Persona library not attached. Call .with_persona(folder) first."
|
|
140
|
+
)
|
|
141
|
+
self.prompt_context.set_persona_by_id(persona_id)
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
def with_persona_data(self, persona_data: dict[str, Any]) -> AgentBuilder:
|
|
145
|
+
"""Activate a persona from inline data (no library required)."""
|
|
146
|
+
self.prompt_context.set_persona_from_data(persona_data)
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
def with_persona_from_path(self, path: Path | str) -> AgentBuilder:
|
|
150
|
+
"""Activate a persona from an arbitrary file path."""
|
|
151
|
+
self.prompt_context.set_persona_from_path(path)
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
# ── Guideline ──────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
def with_guideline(self, guideline_folder: Path | str) -> AgentBuilder:
|
|
157
|
+
"""Attach a :class:`GuidelineLibrary` loaded from *guideline_folder*."""
|
|
158
|
+
self.prompt_context.attach_guideline_library(
|
|
159
|
+
GuidelineLibrary(guideline_folder)
|
|
160
|
+
)
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
def with_guideline_id(self, guideline_id: str) -> AgentBuilder:
|
|
164
|
+
"""Select an active guideline by its registered ID."""
|
|
165
|
+
if self.prompt_context.guideline_library is None:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
"Guideline library not attached. Call .with_guideline(folder) first."
|
|
168
|
+
)
|
|
169
|
+
self.prompt_context.set_guideline_by_id(guideline_id)
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
def with_guideline_text(self, text: str) -> AgentBuilder:
|
|
173
|
+
"""Activate a guideline from inline text."""
|
|
174
|
+
self.prompt_context.set_guideline_from_text(text)
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def with_guideline_from_path(self, path: Path | str) -> AgentBuilder:
|
|
178
|
+
"""Activate a guideline from an arbitrary file path."""
|
|
179
|
+
self.prompt_context.set_guideline_from_path(path)
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def clear_guideline(self) -> None:
|
|
183
|
+
"""Deactivate the currently selected guideline."""
|
|
184
|
+
self.prompt_context.clear_guideline()
|
|
185
|
+
|
|
186
|
+
# ── Convenience read-out ───────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def current_persona_id(self) -> str | None:
|
|
190
|
+
return self.prompt_context.current_persona_id
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def current_guideline_id(self) -> str | None:
|
|
194
|
+
return self.prompt_context.current_guideline_id
|
|
195
|
+
|
|
196
|
+
def get_guideline_user_message(self) -> str | None:
|
|
197
|
+
"""Return the rendered guideline user-message, if available.
|
|
198
|
+
|
|
199
|
+
Only meaningful when both a guideline is active and the
|
|
200
|
+
instructions are template-driven with a sibling
|
|
201
|
+
``{template}.user.mustache.md`` file.
|
|
202
|
+
"""
|
|
203
|
+
guideline_text = self.prompt_context.get_guideline_text()
|
|
204
|
+
if not guideline_text or self.instruction_template is None:
|
|
205
|
+
return None
|
|
206
|
+
if self.template_renderer is None:
|
|
207
|
+
return None
|
|
208
|
+
user_template = f"{self.instruction_template}.user.mustache.md"
|
|
209
|
+
try:
|
|
210
|
+
rendered = self.template_renderer(
|
|
211
|
+
user_template, guideline=guideline_text
|
|
212
|
+
)
|
|
213
|
+
except FileNotFoundError:
|
|
214
|
+
return None
|
|
215
|
+
return rendered.strip() or None
|
|
216
|
+
|
|
217
|
+
# ── Build ──────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
def build(self) -> Any:
|
|
220
|
+
"""Materialize the agent through the configured backend."""
|
|
221
|
+
if self.model is None:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
"Model must be set before building. Call .with_model(...)"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
instructions = self._resolve_instructions()
|
|
227
|
+
return self.backend.create_agent(
|
|
228
|
+
name=self.name,
|
|
229
|
+
instructions=instructions,
|
|
230
|
+
tools=self.tools,
|
|
231
|
+
model=self.model,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
async def run(self, task: str, *, max_turns: int = 10, **kwargs: Any) -> Any:
|
|
235
|
+
"""Build and run the agent on *task* using the backend."""
|
|
236
|
+
agent = self.build()
|
|
237
|
+
return await self.backend.run(
|
|
238
|
+
agent, task, max_turns=max_turns, **kwargs
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# ── Internals ──────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
def _resolve_instructions(self) -> str:
|
|
244
|
+
if self.instruction_template is not None:
|
|
245
|
+
if self.template_renderer is None:
|
|
246
|
+
raise ValueError("Template renderer is not set")
|
|
247
|
+
persona_text = self.prompt_context.get_persona_text()
|
|
248
|
+
if persona_text is None:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"Template-mode instructions require an active persona. "
|
|
251
|
+
"Call .with_persona(...) and select a persona first."
|
|
252
|
+
)
|
|
253
|
+
system_template = f"{self.instruction_template}.system.mustache.md"
|
|
254
|
+
return self.template_renderer(
|
|
255
|
+
system_template, persona=persona_text, guideline=""
|
|
256
|
+
)
|
|
257
|
+
if self.instructions is not None:
|
|
258
|
+
return self.instructions
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"Instructions are not set. Use .with_instructions(...) "
|
|
261
|
+
"or .with_instructions_template(...)."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ── Module-level helpers ───────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _default_template_renderer(template_name: str, /, **context: Any) -> str:
|
|
269
|
+
"""Default template renderer rooted at this package's ``prompts/``."""
|
|
270
|
+
return render_prompt_template(
|
|
271
|
+
template_name,
|
|
272
|
+
package="ellements.agents",
|
|
273
|
+
resource_root="prompts",
|
|
274
|
+
**context,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def check_openai_api_key() -> bool:
|
|
279
|
+
"""Return whether ``OPENAI_API_KEY`` is set; print a hint when not."""
|
|
280
|
+
if os.getenv("OPENAI_API_KEY"):
|
|
281
|
+
return True
|
|
282
|
+
print("Error: OPENAI_API_KEY environment variable not set.")
|
|
283
|
+
print(" export OPENAI_API_KEY='your-api-key'")
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def load_prompt_file(
|
|
288
|
+
path: Path | str, *, fallback: str | None = None
|
|
289
|
+
) -> str:
|
|
290
|
+
"""Load a prompt file with an optional fallback string."""
|
|
291
|
+
try:
|
|
292
|
+
return Path(path).read_text(encoding="utf-8")
|
|
293
|
+
except FileNotFoundError:
|
|
294
|
+
if fallback is None:
|
|
295
|
+
raise
|
|
296
|
+
return fallback
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
__all__ = [
|
|
300
|
+
"AgentBuilder",
|
|
301
|
+
"check_openai_api_key",
|
|
302
|
+
"load_prompt_file",
|
|
303
|
+
]
|