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,100 @@
|
|
|
1
|
+
"""Shared LiteLLM request normalization helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import litellm
|
|
10
|
+
|
|
11
|
+
from .model_params import filter_parameters, get_unsupported_parameters
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class CompletionRequest:
|
|
16
|
+
"""Normalized chat-completion request settings."""
|
|
17
|
+
|
|
18
|
+
model: str
|
|
19
|
+
params: dict[str, Any]
|
|
20
|
+
logged_max_tokens: int | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configure_litellm_globals(config: Mapping[str, Any]) -> None:
|
|
24
|
+
"""Apply top-level LiteLLM configuration from a client config mapping."""
|
|
25
|
+
if "api_key" in config:
|
|
26
|
+
litellm.api_key = config["api_key"]
|
|
27
|
+
if "base_url" in config:
|
|
28
|
+
litellm.api_base = config["base_url"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_model_name(
|
|
32
|
+
model: str,
|
|
33
|
+
*,
|
|
34
|
+
use_responses_api: bool = False,
|
|
35
|
+
) -> tuple[str, bool]:
|
|
36
|
+
"""Resolve provider-prefixed model names for special GPT-5 routing."""
|
|
37
|
+
is_gpt5 = "gpt-5" in model.lower()
|
|
38
|
+
target_model = model
|
|
39
|
+
|
|
40
|
+
if is_gpt5 and not model.startswith("openai/"):
|
|
41
|
+
if use_responses_api:
|
|
42
|
+
target_model = f"openai/responses/{model}"
|
|
43
|
+
else:
|
|
44
|
+
target_model = f"openai/{model}"
|
|
45
|
+
|
|
46
|
+
return target_model, is_gpt5
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def prepare_completion_request(
|
|
50
|
+
*,
|
|
51
|
+
model: str,
|
|
52
|
+
temperature: float | None,
|
|
53
|
+
max_tokens: int | None,
|
|
54
|
+
use_responses_api: bool = False,
|
|
55
|
+
extra_params: Mapping[str, Any] | None = None,
|
|
56
|
+
default_reasoning_effort: str | None = "medium",
|
|
57
|
+
) -> CompletionRequest:
|
|
58
|
+
"""Normalize model routing and provider parameters for a chat completion."""
|
|
59
|
+
params: dict[str, Any] = dict(extra_params or {})
|
|
60
|
+
target_model, is_gpt5 = resolve_model_name(
|
|
61
|
+
model,
|
|
62
|
+
use_responses_api=use_responses_api,
|
|
63
|
+
)
|
|
64
|
+
unsupported = get_unsupported_parameters(target_model)
|
|
65
|
+
logged_max_tokens = max_tokens
|
|
66
|
+
target_max_tokens = max_tokens
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
is_gpt5
|
|
70
|
+
and use_responses_api
|
|
71
|
+
and default_reasoning_effort
|
|
72
|
+
and "reasoning_effort" not in params
|
|
73
|
+
):
|
|
74
|
+
params["reasoning_effort"] = default_reasoning_effort
|
|
75
|
+
|
|
76
|
+
if is_gpt5 and target_max_tokens is not None:
|
|
77
|
+
if use_responses_api:
|
|
78
|
+
params["max_output_tokens"] = target_max_tokens
|
|
79
|
+
else:
|
|
80
|
+
params["max_completion_tokens"] = target_max_tokens
|
|
81
|
+
target_max_tokens = None
|
|
82
|
+
|
|
83
|
+
if temperature is not None and "temperature" not in unsupported:
|
|
84
|
+
params["temperature"] = temperature
|
|
85
|
+
if target_max_tokens is not None:
|
|
86
|
+
params["max_tokens"] = target_max_tokens
|
|
87
|
+
|
|
88
|
+
return CompletionRequest(
|
|
89
|
+
model=target_model,
|
|
90
|
+
params=filter_parameters(target_model, **params),
|
|
91
|
+
logged_max_tokens=logged_max_tokens,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_response_text(response: Any) -> str:
|
|
96
|
+
"""Read text content from a LiteLLM response object."""
|
|
97
|
+
content = response.choices[0].message.content
|
|
98
|
+
if content is None:
|
|
99
|
+
return ""
|
|
100
|
+
return str(content)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Structured-output helpers built on litellm's native ``response_format``.
|
|
2
|
+
|
|
3
|
+
Modern litellm supports passing a Pydantic ``BaseModel`` subclass to the
|
|
4
|
+
``response_format`` parameter of ``litellm.acompletion``. Providers that
|
|
5
|
+
support JSON-mode (OpenAI, Anthropic, Mistral, Gemini, …) will return a
|
|
6
|
+
JSON document validating against the schema; providers that don't will
|
|
7
|
+
raise an exception, which we surface as
|
|
8
|
+
:class:`StructuredOutputUnsupportedError`.
|
|
9
|
+
|
|
10
|
+
This module deliberately does *not* re-implement schema-to-prompt
|
|
11
|
+
conversion. Hand-rolled "please respond with JSON like this…" prompts
|
|
12
|
+
are inferior — they don't constrain generation, only post-hoc parsing.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
from typing import TypeVar
|
|
20
|
+
|
|
21
|
+
import litellm
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
24
|
+
|
|
25
|
+
from ..exceptions import LLMError, StructuredOutputUnsupportedError
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T", bound=BaseModel)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def supports_structured_output(model: str) -> bool:
|
|
31
|
+
"""Return whether *model* supports litellm's native ``response_format``."""
|
|
32
|
+
try:
|
|
33
|
+
return bool(litellm.supports_response_schema(model=model))
|
|
34
|
+
except Exception:
|
|
35
|
+
# Older litellm versions or unknown models — be conservative and
|
|
36
|
+
# fall through to the attempt, which will raise if unsupported.
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ensure_structured_support(model: str) -> None:
|
|
41
|
+
"""Hard-fail when *model* cannot produce native structured outputs."""
|
|
42
|
+
if not supports_structured_output(model):
|
|
43
|
+
raise StructuredOutputUnsupportedError(
|
|
44
|
+
f"Model {model!r} does not support native structured outputs. "
|
|
45
|
+
"Choose a model with JSON-mode / response_format support."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_json_payload(content: str) -> str:
|
|
50
|
+
"""Extract a JSON document from a response that may include code fences."""
|
|
51
|
+
stripped = content.strip()
|
|
52
|
+
if "```json" in stripped:
|
|
53
|
+
match = re.search(r"```json\s*\n(.*?)\n```", stripped, re.DOTALL)
|
|
54
|
+
if match:
|
|
55
|
+
return match.group(1).strip()
|
|
56
|
+
if "```" in stripped:
|
|
57
|
+
match = re.search(r"```\s*\n(.*?)\n```", stripped, re.DOTALL)
|
|
58
|
+
if match:
|
|
59
|
+
return match.group(1).strip()
|
|
60
|
+
return stripped
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_structured_content(
|
|
64
|
+
content: str,
|
|
65
|
+
response_model: type[T],
|
|
66
|
+
*,
|
|
67
|
+
error_context: str = "Failed to parse structured response",
|
|
68
|
+
) -> T:
|
|
69
|
+
"""Parse JSON *content* into an instance of *response_model*.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
LLMError: If *content* is not valid JSON or does not validate
|
|
73
|
+
against the model's schema.
|
|
74
|
+
"""
|
|
75
|
+
payload = extract_json_payload(content)
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(payload)
|
|
78
|
+
return response_model.model_validate(data)
|
|
79
|
+
except (json.JSONDecodeError, PydanticValidationError, ValueError) as exc:
|
|
80
|
+
preview = payload[:200] if len(payload) > 200 else payload
|
|
81
|
+
raise LLMError(
|
|
82
|
+
f"{error_context}: {exc}\nContent preview: {preview}"
|
|
83
|
+
) from exc
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = [
|
|
87
|
+
"ensure_structured_support",
|
|
88
|
+
"extract_json_payload",
|
|
89
|
+
"parse_structured_content",
|
|
90
|
+
"supports_structured_output",
|
|
91
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Shared base class for LLM-client wrappers.
|
|
2
|
+
|
|
3
|
+
The composition layers in :mod:`ellements.core.caching`,
|
|
4
|
+
:mod:`ellements.core.rate_limit`, and :mod:`ellements.core.budgeting`
|
|
5
|
+
all decorate a :class:`LLMClientProtocol`. They share two boring
|
|
6
|
+
pieces: storing the wrapped client and proxying the ``model``
|
|
7
|
+
attribute. This module centralizes those, plus a default
|
|
8
|
+
:meth:`continue_conversation` passthrough.
|
|
9
|
+
|
|
10
|
+
The base intentionally does **not** provide default passthroughs for
|
|
11
|
+
:meth:`complete`, :meth:`complete_structured`, :meth:`complete_with_tools`,
|
|
12
|
+
:meth:`stream`, :meth:`loglikelihood`, or :meth:`generate_image`.
|
|
13
|
+
A half-decorated wrapper that silently forwards methods it forgot to
|
|
14
|
+
override would defeat the purpose of the wrapper. Subclasses must
|
|
15
|
+
implement every method they care about, and the type checker will flag
|
|
16
|
+
any that are missing from :class:`LLMClientProtocol`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .messages import Conversation
|
|
25
|
+
from .protocol import LLMClientProtocol
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LLMClientWrapper:
|
|
29
|
+
"""Storage + minimal proxying for LLM-client decorators.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
inner: The wrapped client. Must satisfy
|
|
33
|
+
:class:`~ellements.core.llm.LLMClientProtocol`.
|
|
34
|
+
|
|
35
|
+
Subclasses are expected to expose the same methods as
|
|
36
|
+
:class:`LLMClientProtocol`, overriding the ones they intercept
|
|
37
|
+
and letting :meth:`continue_conversation` use the default
|
|
38
|
+
passthrough provided here (or override it too if they want to
|
|
39
|
+
charge/throttle multi-turn calls).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, *, inner: LLMClientProtocol) -> None:
|
|
43
|
+
self._inner = inner
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def inner(self) -> LLMClientProtocol:
|
|
47
|
+
"""The wrapped client (read-only access for debugging/inspection)."""
|
|
48
|
+
return self._inner
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def model(self) -> str:
|
|
52
|
+
"""Proxy the wrapped client's model identifier."""
|
|
53
|
+
return self._inner.model
|
|
54
|
+
|
|
55
|
+
async def continue_conversation(
|
|
56
|
+
self,
|
|
57
|
+
conversation: Conversation,
|
|
58
|
+
user_message: str,
|
|
59
|
+
*,
|
|
60
|
+
temperature: float = 0.7,
|
|
61
|
+
max_tokens: int | None = None,
|
|
62
|
+
**kwargs: Any,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""Forward to the wrapped client unchanged.
|
|
65
|
+
|
|
66
|
+
Subclasses may override to intercept multi-turn calls (e.g. to
|
|
67
|
+
charge the budget once per turn, or to count the turn toward a
|
|
68
|
+
rate limit).
|
|
69
|
+
"""
|
|
70
|
+
return await self._inner.continue_conversation(
|
|
71
|
+
conversation,
|
|
72
|
+
user_message,
|
|
73
|
+
temperature=temperature,
|
|
74
|
+
max_tokens=max_tokens,
|
|
75
|
+
**kwargs,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["LLMClientWrapper"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Observability primitives for ellements.
|
|
2
|
+
|
|
3
|
+
This subpackage centralises everything related to *seeing what
|
|
4
|
+
happened* inside an ellements run:
|
|
5
|
+
|
|
6
|
+
- :mod:`.events` — the event vocabulary
|
|
7
|
+
(:class:`LLMRequestEvent`, :class:`LLMResponseEvent`,
|
|
8
|
+
:class:`LLMErrorEvent`, :class:`AgentEvent`).
|
|
9
|
+
- :mod:`.observer` — the :class:`LLMObserver` Protocol.
|
|
10
|
+
- :mod:`.jsonl_logger` — :class:`JsonlPromptLogger`, the canonical
|
|
11
|
+
observer implementation, and its :class:`LoggedCall` record schema.
|
|
12
|
+
- :mod:`.markdown_formatter` — :func:`format_log_markdown`, a
|
|
13
|
+
human-readable view over any ``.jsonl`` log file.
|
|
14
|
+
|
|
15
|
+
This ``__init__`` re-exports the full public surface so callers can
|
|
16
|
+
``from ellements.core.observability import …`` without reaching into
|
|
17
|
+
specific submodules.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .events import (
|
|
21
|
+
AgentEvent,
|
|
22
|
+
LLMErrorEvent,
|
|
23
|
+
LLMRequestEvent,
|
|
24
|
+
LLMResponseEvent,
|
|
25
|
+
)
|
|
26
|
+
from .jsonl_logger import JsonlPromptLogger, LoggedCall
|
|
27
|
+
from .markdown_formatter import format_log_markdown
|
|
28
|
+
from .observer import LLMObserver
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"AgentEvent",
|
|
32
|
+
"JsonlPromptLogger",
|
|
33
|
+
"LLMErrorEvent",
|
|
34
|
+
"LLMObserver",
|
|
35
|
+
"LLMRequestEvent",
|
|
36
|
+
"LLMResponseEvent",
|
|
37
|
+
"LoggedCall",
|
|
38
|
+
"format_log_markdown",
|
|
39
|
+
]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Event records emitted across ellements layers.
|
|
2
|
+
|
|
3
|
+
This module owns the **vocabulary** observers consume. We keep it
|
|
4
|
+
deliberately small and immutable-by-default so that every observer
|
|
5
|
+
implementation knows the exact shape of every field, with no surprises
|
|
6
|
+
from subclassing.
|
|
7
|
+
|
|
8
|
+
Two families of events live here:
|
|
9
|
+
|
|
10
|
+
LLM call events (consumed by :class:`LLMObserver`)
|
|
11
|
+
Every method on :class:`~ellements.core.LLMClient` (``complete``,
|
|
12
|
+
``complete_structured``, ``complete_with_tools``, ``stream``,
|
|
13
|
+
``generate_image``, ``loglikelihood``) fires the same three events:
|
|
14
|
+
:class:`LLMRequestEvent` before the call leaves the client,
|
|
15
|
+
:class:`LLMResponseEvent` on success, :class:`LLMErrorEvent` on
|
|
16
|
+
failure. The triple shares a ``call_id`` so observers can correlate
|
|
17
|
+
them.
|
|
18
|
+
|
|
19
|
+
Agent run events (consumed by streaming agent backends)
|
|
20
|
+
:class:`AgentEvent` is the uniform shape yielded by
|
|
21
|
+
:meth:`~ellements.agents.AgentBackend.stream_run`. Backends
|
|
22
|
+
translate their native event streams (OpenAI Agents SDK,
|
|
23
|
+
Claude Agents SDK, etc.) onto this small vocabulary so UIs can
|
|
24
|
+
consume them without backend-specific code.
|
|
25
|
+
|
|
26
|
+
Co-locating both families here makes ellements' observability surface
|
|
27
|
+
discoverable from a single place. Layers that need only one family
|
|
28
|
+
import only what they need (events are independent).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# LLM call events
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(slots=True)
|
|
42
|
+
class LLMRequestEvent:
|
|
43
|
+
"""Event fired before an LLM call leaves the client.
|
|
44
|
+
|
|
45
|
+
Observers receive this event before the network request hits the
|
|
46
|
+
provider, which makes it the right place to record the exact
|
|
47
|
+
payload (messages, parameters, available tools) that will be sent.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
call_id: UUID that identifies this call across the
|
|
51
|
+
``request``/``response``/``error`` triple.
|
|
52
|
+
method: The :class:`LLMClient` method name that initiated this
|
|
53
|
+
call (e.g. ``"complete"``, ``"stream"``).
|
|
54
|
+
model: The model identifier the call is targeting.
|
|
55
|
+
messages: The serialized message list as sent to the provider.
|
|
56
|
+
temperature: The temperature parameter, if specified.
|
|
57
|
+
max_tokens: The max-tokens parameter, if specified.
|
|
58
|
+
tools: Provider-formatted tool definitions, if any.
|
|
59
|
+
extra_params: Additional provider-specific parameters.
|
|
60
|
+
metadata: Free-form, observer-supplied annotation channel.
|
|
61
|
+
Observers may carry context (e.g. workstream id) here from
|
|
62
|
+
request through response/error.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
call_id: str
|
|
66
|
+
method: str
|
|
67
|
+
model: str
|
|
68
|
+
messages: list[dict[str, Any]]
|
|
69
|
+
temperature: float | None
|
|
70
|
+
max_tokens: int | None
|
|
71
|
+
tools: list[dict[str, Any]] = field(default_factory=list)
|
|
72
|
+
extra_params: dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True)
|
|
77
|
+
class LLMResponseEvent:
|
|
78
|
+
"""Event fired after a successful LLM call returns.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
call_id: Matches the originating :class:`LLMRequestEvent`.
|
|
82
|
+
method: Same as on the request event.
|
|
83
|
+
model: Same as on the request event.
|
|
84
|
+
response: The final assistant text. For streaming calls this
|
|
85
|
+
is the concatenated content.
|
|
86
|
+
duration_ms: Wall-clock latency in milliseconds.
|
|
87
|
+
tool_calls: Tool calls the model issued during this turn,
|
|
88
|
+
already merged with any tool-call results the client
|
|
89
|
+
looped back internally.
|
|
90
|
+
usage: Provider-reported token usage, when available.
|
|
91
|
+
metadata: Free-form, observer-supplied annotation channel.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
call_id: str
|
|
95
|
+
method: str
|
|
96
|
+
model: str
|
|
97
|
+
response: str
|
|
98
|
+
duration_ms: int
|
|
99
|
+
tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
100
|
+
usage: dict[str, Any] | None = None
|
|
101
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class LLMErrorEvent:
|
|
106
|
+
"""Event fired when an LLM call raises (after any retries).
|
|
107
|
+
|
|
108
|
+
Attributes:
|
|
109
|
+
call_id: Matches the originating :class:`LLMRequestEvent`.
|
|
110
|
+
method: Same as on the request event.
|
|
111
|
+
model: Same as on the request event.
|
|
112
|
+
error: The actual exception instance that ended the call.
|
|
113
|
+
Observers should not assume the exception is JSON-safe;
|
|
114
|
+
persistent loggers usually record ``type(error).__name__``
|
|
115
|
+
and ``str(error)`` instead of the live object.
|
|
116
|
+
duration_ms: Wall-clock time spent before the failure.
|
|
117
|
+
metadata: Free-form, observer-supplied annotation channel.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
call_id: str
|
|
121
|
+
method: str
|
|
122
|
+
model: str
|
|
123
|
+
error: BaseException
|
|
124
|
+
duration_ms: int
|
|
125
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Agent run events
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(slots=True)
|
|
134
|
+
class AgentEvent:
|
|
135
|
+
"""One incremental event surfaced by streaming agent runs.
|
|
136
|
+
|
|
137
|
+
Backends map their native event types onto a small,
|
|
138
|
+
backend-agnostic vocabulary so UIs can consume agent streams
|
|
139
|
+
without knowing which SDK powers the backend:
|
|
140
|
+
|
|
141
|
+
- ``"agent_active"``: a new sub-agent took the floor.
|
|
142
|
+
Payload: ``{"name": str}``.
|
|
143
|
+
- ``"tool_call"``: the agent invoked a tool.
|
|
144
|
+
Payload: ``{"name": str, "arguments": Mapping[str, Any] | str}``.
|
|
145
|
+
- ``"tool_output"``: a tool produced an output.
|
|
146
|
+
Payload: ``{"output": Any}``.
|
|
147
|
+
- ``"message"``: the agent produced a (possibly partial) message.
|
|
148
|
+
Payload: ``{"text": str, "final": bool}``.
|
|
149
|
+
|
|
150
|
+
Backends MAY surface additional event types under
|
|
151
|
+
backend-namespaced keys (e.g. ``"openai.raw_response"``).
|
|
152
|
+
Consumers should ignore unknown types.
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
type: One of the type strings above (or a backend-namespaced
|
|
156
|
+
extension).
|
|
157
|
+
payload: Type-specific payload as documented above.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
type: str
|
|
161
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = [
|
|
165
|
+
"AgentEvent",
|
|
166
|
+
"LLMErrorEvent",
|
|
167
|
+
"LLMRequestEvent",
|
|
168
|
+
"LLMResponseEvent",
|
|
169
|
+
]
|