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,180 @@
1
+ """Lightweight, opinionated configuration support for ellements.
2
+
3
+ Goals:
4
+
5
+ * Apps describe their settings as **frozen dataclasses** — strongly
6
+ typed, immutable, easy to introspect, trivially picklable, plays
7
+ well with Pydantic or attrs at the boundary.
8
+ * Loading from disk is **one function call** per supported format
9
+ (TOML, JSON). Both formats accept identical key→value structures, so
10
+ callers can pick whichever suits their tooling.
11
+ * No global "settings singleton". Apps build their config explicitly
12
+ and pass it to whoever needs it — this keeps tests trivial and
13
+ removes hidden coupling.
14
+ * Loaders never inject defaults silently. If the dataclass declares a
15
+ default, that's what callers get when the file omits the field; if
16
+ no default is declared, the loader raises a clear error.
17
+ * Composition is by **plain object construction** (``dataclasses.replace``
18
+ for overlays) rather than a bespoke merge DSL. We provide
19
+ :func:`overlay` as a tiny convenience for the common "base config +
20
+ per-environment overrides" pattern.
21
+
22
+ Example::
23
+
24
+ from dataclasses import dataclass
25
+ from ellements.core.config import load_toml, overlay
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class AppConfig:
29
+ model: str
30
+ temperature: float = 0.7
31
+ retries: int = 3
32
+
33
+ base = load_toml("config/base.toml", AppConfig)
34
+ prod = overlay(base, {"temperature": 0.0})
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import tomllib
41
+ from dataclasses import fields, is_dataclass, replace
42
+ from pathlib import Path
43
+ from typing import Any, TypeVar
44
+
45
+ from .exceptions import EllementsError
46
+
47
+ ConfigT = TypeVar("ConfigT")
48
+
49
+
50
+ class ConfigError(EllementsError):
51
+ """Raised when a config payload cannot be turned into a dataclass."""
52
+
53
+
54
+ def load_toml(path: Path | str, config_class: type[ConfigT]) -> ConfigT:
55
+ """Load *path* as TOML and instantiate *config_class*.
56
+
57
+ Args:
58
+ path: Path to a TOML file. Must be readable.
59
+ config_class: A dataclass type. Its field set defines the
60
+ expected schema. Extra keys in the file raise
61
+ :class:`ConfigError` so typos surface loudly.
62
+
63
+ Returns:
64
+ A fully populated instance of *config_class*.
65
+
66
+ Raises:
67
+ ConfigError: If the file is unreadable, malformed TOML,
68
+ references unknown fields, or omits required fields.
69
+ """
70
+ file_path = Path(path)
71
+ try:
72
+ with file_path.open("rb") as handle:
73
+ data = tomllib.load(handle)
74
+ except FileNotFoundError as exc:
75
+ raise ConfigError(f"Config file not found: {file_path}") from exc
76
+ except tomllib.TOMLDecodeError as exc:
77
+ raise ConfigError(f"Invalid TOML in {file_path}: {exc}") from exc
78
+ return _instantiate(config_class, data, source=str(file_path))
79
+
80
+
81
+ def load_json(path: Path | str, config_class: type[ConfigT]) -> ConfigT:
82
+ """Load *path* as JSON and instantiate *config_class*.
83
+
84
+ Args:
85
+ path: Path to a JSON file containing a single object literal.
86
+ config_class: A dataclass type. Its field set defines the
87
+ expected schema. Extra keys raise :class:`ConfigError`.
88
+
89
+ Returns:
90
+ A fully populated instance of *config_class*.
91
+
92
+ Raises:
93
+ ConfigError: If the file is unreadable, malformed JSON, the
94
+ top-level value is not an object, or required fields are
95
+ missing.
96
+ """
97
+ file_path = Path(path)
98
+ try:
99
+ text = file_path.read_text(encoding="utf-8")
100
+ except FileNotFoundError as exc:
101
+ raise ConfigError(f"Config file not found: {file_path}") from exc
102
+ try:
103
+ data = json.loads(text)
104
+ except json.JSONDecodeError as exc:
105
+ raise ConfigError(f"Invalid JSON in {file_path}: {exc}") from exc
106
+ if not isinstance(data, dict):
107
+ raise ConfigError(
108
+ f"Config file {file_path} must contain a JSON object at the "
109
+ f"top level (got {type(data).__name__})."
110
+ )
111
+ return _instantiate(config_class, data, source=str(file_path))
112
+
113
+
114
+ def overlay(base: ConfigT, changes: dict[str, Any]) -> ConfigT:
115
+ """Return a copy of *base* with *changes* applied.
116
+
117
+ Thin wrapper over :func:`dataclasses.replace` that validates the
118
+ keys in *changes* against the dataclass schema, so callers get a
119
+ crisp error if they typo an override.
120
+
121
+ Args:
122
+ base: An instance of a frozen dataclass.
123
+ changes: A mapping of field name → new value. Every key must
124
+ be an existing field on the dataclass.
125
+
126
+ Returns:
127
+ A new instance with the same type as *base*, sharing all
128
+ unchanged fields and using overridden values for everything
129
+ in *changes*.
130
+
131
+ Raises:
132
+ ConfigError: If *base* is not a dataclass instance, or if
133
+ *changes* contains an unknown field name.
134
+ """
135
+ if not is_dataclass(base) or isinstance(base, type):
136
+ raise ConfigError(
137
+ f"overlay() expects a dataclass instance, got {type(base).__name__}."
138
+ )
139
+ known = {field.name for field in fields(base)}
140
+ unknown = set(changes) - known
141
+ if unknown:
142
+ raise ConfigError(
143
+ f"Unknown config fields for {type(base).__name__}: "
144
+ f"{sorted(unknown)}. Known fields: {sorted(known)}."
145
+ )
146
+ return replace(base, **changes)
147
+
148
+
149
+ def _instantiate(
150
+ config_class: type[ConfigT], data: dict[str, Any], *, source: str
151
+ ) -> ConfigT:
152
+ """Construct *config_class* from *data*, raising on schema mismatches."""
153
+ if not is_dataclass(config_class):
154
+ raise ConfigError(
155
+ f"{config_class.__name__} is not a dataclass; config_class must "
156
+ f"be a (preferably frozen) dataclass."
157
+ )
158
+ known = {field.name for field in fields(config_class)}
159
+ unknown = set(data) - known
160
+ if unknown:
161
+ raise ConfigError(
162
+ f"Unknown config keys in {source} for "
163
+ f"{config_class.__name__}: {sorted(unknown)}. "
164
+ f"Known fields: {sorted(known)}."
165
+ )
166
+ try:
167
+ return config_class(**data)
168
+ except TypeError as exc:
169
+ raise ConfigError(
170
+ f"Failed to instantiate {config_class.__name__} from {source}: "
171
+ f"{exc}"
172
+ ) from exc
173
+
174
+
175
+ __all__ = [
176
+ "ConfigError",
177
+ "load_json",
178
+ "load_toml",
179
+ "overlay",
180
+ ]
@@ -0,0 +1,145 @@
1
+ """Custom exceptions for the ellements library.
2
+
3
+ All ellements exceptions inherit from :class:`EllementsError`, which itself
4
+ inherits from :class:`Exception`. The taxonomy is intentionally small and
5
+ purpose-built — callers can catch the most specific class they care about,
6
+ or :class:`EllementsError` as a single umbrella.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from .tools import ToolCallRecord
15
+
16
+
17
+ class EllementsError(Exception):
18
+ """Base exception for all ellements errors."""
19
+
20
+
21
+ class LLMError(EllementsError):
22
+ """LLM call failed irrecoverably (after any retries)."""
23
+
24
+
25
+ class ConversationError(EllementsError):
26
+ """Conversation state or input was invalid."""
27
+
28
+
29
+ class EllementsValidationError(EllementsError):
30
+ """A validation rule inside the ellements library failed.
31
+
32
+ Distinct from :class:`pydantic.ValidationError`, which is raised by
33
+ Pydantic itself. Use this exception when an ellements component
34
+ rejects an input because it violates a library-defined contract
35
+ (e.g. a strategy expecting a non-empty prompt, a tool refusing a
36
+ malformed argument).
37
+ """
38
+
39
+
40
+ class StructuredOutputUnsupportedError(LLMError):
41
+ """The selected model/provider does not support native structured outputs.
42
+
43
+ Raised by :meth:`LLMClient.complete_structured` when the configured
44
+ model cannot produce a JSON object matching a Pydantic schema via
45
+ litellm's ``response_format=PydanticModel`` mechanism.
46
+ """
47
+
48
+
49
+ class LogprobsUnsupportedError(LLMError):
50
+ """The selected model/provider does not expose token log-probabilities.
51
+
52
+ Raised eagerly at benchmark startup when the requested tasks need
53
+ log-likelihoods (e.g. MMLU, HellaSwag) but the model cannot provide
54
+ them. This is intentionally a hard failure — silently returning
55
+ fake ``0.0`` would invalidate benchmark results.
56
+ """
57
+
58
+
59
+ class MaxToolIterationsError(LLMError):
60
+ """The tool-calling loop hit its iteration cap without converging.
61
+
62
+ The error carries the partial conversation captured up to the cap
63
+ and the list of unresolved tool calls the model produced on the
64
+ final turn, so callers can inspect, log, or resume the loop with
65
+ additional executors.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ message: str,
71
+ *,
72
+ max_iterations: int,
73
+ messages: list[dict[str, Any]],
74
+ tool_calls_made: list[ToolCallRecord],
75
+ unresolved_tool_calls: list[dict[str, Any]],
76
+ ) -> None:
77
+ super().__init__(message)
78
+ self.max_iterations = max_iterations
79
+ self.messages = messages
80
+ self.tool_calls_made = tool_calls_made
81
+ self.unresolved_tool_calls = unresolved_tool_calls
82
+
83
+
84
+ class PromptLibraryError(EllementsError):
85
+ """Base class for errors raised by persona / guideline / prompt libraries.
86
+
87
+ Catch this to handle any failure to resolve a named entry in any
88
+ prompt library — useful when several lookups happen in a row and
89
+ you want a single fallback path.
90
+ """
91
+
92
+
93
+ class PersonaNotFoundError(PromptLibraryError):
94
+ """The requested persona could not be located in the active library."""
95
+
96
+
97
+ class GuidelineNotFoundError(PromptLibraryError):
98
+ """The requested guideline could not be located in the active library."""
99
+
100
+
101
+ class PromptKeyMissingError(PromptLibraryError):
102
+ """A strategy required a named prompt that was not supplied."""
103
+
104
+
105
+ class BudgetExceededError(EllementsError):
106
+ """A cost budget was exhausted before a request could complete.
107
+
108
+ Raised by :class:`ellements.core.budgeting.BudgetedLLMClient` (and
109
+ any :class:`~ellements.core.budgeting.BudgetTrackerProtocol`
110
+ implementation) when a request would push cumulative spend past
111
+ the configured cap.
112
+
113
+ The error carries the budget identifier, the limit that was hit,
114
+ and the amount that was charged so callers can surface useful
115
+ diagnostics or implement graceful fallbacks.
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ message: str,
121
+ *,
122
+ limit: float,
123
+ spent: float,
124
+ attempted: float,
125
+ ) -> None:
126
+ super().__init__(message)
127
+ self.limit = limit
128
+ self.spent = spent
129
+ self.attempted = attempted
130
+
131
+
132
+ __all__ = [
133
+ "BudgetExceededError",
134
+ "ConversationError",
135
+ "EllementsError",
136
+ "EllementsValidationError",
137
+ "GuidelineNotFoundError",
138
+ "LLMError",
139
+ "LogprobsUnsupportedError",
140
+ "MaxToolIterationsError",
141
+ "PersonaNotFoundError",
142
+ "PromptKeyMissingError",
143
+ "PromptLibraryError",
144
+ "StructuredOutputUnsupportedError",
145
+ ]
@@ -0,0 +1,46 @@
1
+ """Internal LLM support package for ellements.core."""
2
+
3
+ from .client import LLMClient
4
+ from .images import (
5
+ GeneratedImage,
6
+ ImageGenerationResponse,
7
+ ImageInput,
8
+ )
9
+ from .messages import (
10
+ Conversation,
11
+ ImageURLPart,
12
+ Message,
13
+ MessageContent,
14
+ MessageInput,
15
+ build_multimodal_user_message,
16
+ normalize_message_input,
17
+ )
18
+ from .model_params import filter_parameters, get_unsupported_parameters
19
+ from .protocol import LLMClientProtocol
20
+ from .structured import (
21
+ ensure_structured_support,
22
+ parse_structured_content,
23
+ supports_structured_output,
24
+ )
25
+ from .wrapper import LLMClientWrapper
26
+
27
+ __all__ = [
28
+ "Conversation",
29
+ "GeneratedImage",
30
+ "ImageGenerationResponse",
31
+ "ImageInput",
32
+ "ImageURLPart",
33
+ "LLMClient",
34
+ "LLMClientProtocol",
35
+ "LLMClientWrapper",
36
+ "Message",
37
+ "MessageContent",
38
+ "MessageInput",
39
+ "build_multimodal_user_message",
40
+ "ensure_structured_support",
41
+ "filter_parameters",
42
+ "get_unsupported_parameters",
43
+ "normalize_message_input",
44
+ "parse_structured_content",
45
+ "supports_structured_output",
46
+ ]