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,205 @@
1
+ """Reusable terminal CLI tool for ellements applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import shlex
8
+ import time
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from ellements.core import ToolSpec
14
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
15
+
16
+
17
+ class TerminalExecutionSpec(BaseModel):
18
+ """Trusted local terminal execution request."""
19
+
20
+ model_config = ConfigDict(extra="forbid")
21
+
22
+ command: str | None = None
23
+ args: list[str] = Field(default_factory=list)
24
+ shell: bool = False
25
+ script: str | None = None
26
+ cwd: str | None = None
27
+ env: dict[str, str] = Field(default_factory=dict)
28
+ stdin: str | None = None
29
+ timeout_seconds: float = Field(default=300.0, gt=0)
30
+ max_output_bytes: int = Field(default=100_000, gt=0)
31
+
32
+ @model_validator(mode="after")
33
+ def _validate_command(self) -> TerminalExecutionSpec:
34
+ if self.shell:
35
+ if not self.script:
36
+ raise ValueError("shell terminal execution requires script")
37
+ elif not self.command:
38
+ raise ValueError("argv terminal execution requires command")
39
+ return self
40
+
41
+
42
+ class TerminalExecutionResult(BaseModel):
43
+ """Captured result of a terminal execution."""
44
+
45
+ model_config = ConfigDict(extra="forbid")
46
+
47
+ command_line: list[str]
48
+ cwd: str | None
49
+ started_at: datetime
50
+ completed_at: datetime
51
+ duration_seconds: float
52
+ exit_code: int | None
53
+ timed_out: bool
54
+ stdout: str
55
+ stderr: str
56
+ stdout_truncated: bool = False
57
+ stderr_truncated: bool = False
58
+
59
+
60
+ async def run_terminal_execution(
61
+ spec: TerminalExecutionSpec,
62
+ ) -> TerminalExecutionResult:
63
+ """Run a local terminal command with bounded output."""
64
+ started_at = datetime.now(UTC)
65
+ start = time.monotonic()
66
+ env = {**os.environ, **spec.env}
67
+ stdin_bytes = spec.stdin.encode("utf-8") if spec.stdin is not None else None
68
+ command_line = (
69
+ [spec.script or ""] if spec.shell else [spec.command or "", *spec.args]
70
+ )
71
+ process = await _start_process(spec, env)
72
+ timed_out = False
73
+ try:
74
+ stdout_bytes, stderr_bytes = await asyncio.wait_for(
75
+ process.communicate(input=stdin_bytes),
76
+ timeout=spec.timeout_seconds,
77
+ )
78
+ except TimeoutError:
79
+ timed_out = True
80
+ process.kill()
81
+ stdout_bytes, stderr_bytes = await process.communicate()
82
+ completed_at = datetime.now(UTC)
83
+ stdout, stdout_truncated = _decode_bounded(stdout_bytes, spec.max_output_bytes)
84
+ stderr, stderr_truncated = _decode_bounded(stderr_bytes, spec.max_output_bytes)
85
+ return TerminalExecutionResult(
86
+ command_line=command_line,
87
+ cwd=spec.cwd,
88
+ started_at=started_at,
89
+ completed_at=completed_at,
90
+ duration_seconds=time.monotonic() - start,
91
+ exit_code=process.returncode,
92
+ timed_out=timed_out,
93
+ stdout=stdout,
94
+ stderr=stderr,
95
+ stdout_truncated=stdout_truncated,
96
+ stderr_truncated=stderr_truncated,
97
+ )
98
+
99
+
100
+ def terminal_cli_tool(
101
+ *,
102
+ name: str = "terminal.run",
103
+ description: str = "Run a local terminal command with bounded output.",
104
+ timeout_seconds: float = 300.0,
105
+ max_output_bytes: int = 100_000,
106
+ ) -> ToolSpec:
107
+ """Return an ellements `ToolSpec` for local CLI execution."""
108
+
109
+ async def invoke(
110
+ command: str | None = None,
111
+ args: list[str] | None = None,
112
+ shell: bool = False,
113
+ script: str | None = None,
114
+ cwd: str | None = None,
115
+ env: dict[str, str] | None = None,
116
+ stdin: str | None = None,
117
+ timeout_seconds: float | None = None,
118
+ max_output_bytes: int | None = None,
119
+ ) -> dict[str, Any]:
120
+ spec = TerminalExecutionSpec(
121
+ command=command,
122
+ args=args or [],
123
+ shell=shell,
124
+ script=script,
125
+ cwd=str(Path(cwd)) if cwd is not None else None,
126
+ env=env or {},
127
+ stdin=stdin,
128
+ timeout_seconds=timeout_seconds or terminal_cli_tool_timeout,
129
+ max_output_bytes=max_output_bytes or terminal_cli_tool_max_output,
130
+ )
131
+ return (await run_terminal_execution(spec)).model_dump(mode="json")
132
+
133
+ terminal_cli_tool_timeout = timeout_seconds
134
+ terminal_cli_tool_max_output = max_output_bytes
135
+ return ToolSpec(
136
+ name=name,
137
+ description=description,
138
+ params_json_schema={
139
+ "type": "object",
140
+ "properties": {
141
+ "command": {"type": "string"},
142
+ "args": {"type": "array", "items": {"type": "string"}},
143
+ "shell": {"type": "boolean", "default": False},
144
+ "script": {"type": "string"},
145
+ "cwd": {"type": "string"},
146
+ "env": {
147
+ "type": "object",
148
+ "additionalProperties": {"type": "string"},
149
+ },
150
+ "stdin": {"type": "string"},
151
+ "timeout_seconds": {"type": "number", "exclusiveMinimum": 0},
152
+ "max_output_bytes": {"type": "integer", "exclusiveMinimum": 0},
153
+ },
154
+ },
155
+ invoke=invoke,
156
+ )
157
+
158
+
159
+ async def _start_process(
160
+ spec: TerminalExecutionSpec,
161
+ env: dict[str, str],
162
+ ) -> asyncio.subprocess.Process:
163
+ if spec.shell:
164
+ assert spec.script is not None
165
+ return await asyncio.create_subprocess_shell(
166
+ spec.script,
167
+ cwd=spec.cwd,
168
+ env=env,
169
+ stdin=asyncio.subprocess.PIPE if spec.stdin is not None else None,
170
+ stdout=asyncio.subprocess.PIPE,
171
+ stderr=asyncio.subprocess.PIPE,
172
+ )
173
+ assert spec.command is not None
174
+ return await asyncio.create_subprocess_exec(
175
+ spec.command,
176
+ *spec.args,
177
+ cwd=spec.cwd,
178
+ env=env,
179
+ stdin=asyncio.subprocess.PIPE if spec.stdin is not None else None,
180
+ stdout=asyncio.subprocess.PIPE,
181
+ stderr=asyncio.subprocess.PIPE,
182
+ )
183
+
184
+
185
+ def _decode_bounded(data: bytes, max_bytes: int) -> tuple[str, bool]:
186
+ truncated = len(data) > max_bytes
187
+ if truncated:
188
+ data = data[:max_bytes]
189
+ return data.decode("utf-8", errors="replace"), truncated
190
+
191
+
192
+ def terminal_command_text(spec: TerminalExecutionSpec) -> str:
193
+ """Return a display-safe command string."""
194
+ if spec.shell:
195
+ return spec.script or ""
196
+ return " ".join(shlex.quote(part) for part in [spec.command or "", *spec.args])
197
+
198
+
199
+ __all__ = [
200
+ "TerminalExecutionResult",
201
+ "TerminalExecutionSpec",
202
+ "run_terminal_execution",
203
+ "terminal_cli_tool",
204
+ "terminal_command_text",
205
+ ]
@@ -0,0 +1,37 @@
1
+ """Web-oriented standard tools for search, crawling, and YouTube access."""
2
+
3
+ from .browser_viewer import (
4
+ BrowserViewer,
5
+ BrowserViewResult,
6
+ LocalArtifactServer,
7
+ is_remote_url,
8
+ )
9
+ from .crawler import CrawlResult, ExtractionResult, WebCrawler, web_crawler_tools
10
+ from .search import SearchResult, SearchResults, WebSearcher, web_search_tools
11
+ from .youtube import (
12
+ RateLimitedYouTubeSearcher,
13
+ VideoMetadata,
14
+ VideoTranscript,
15
+ YouTubeSearcher,
16
+ youtube_search_tools,
17
+ )
18
+
19
+ __all__ = [
20
+ "BrowserViewer",
21
+ "BrowserViewResult",
22
+ "CrawlResult",
23
+ "ExtractionResult",
24
+ "LocalArtifactServer",
25
+ "RateLimitedYouTubeSearcher",
26
+ "SearchResult",
27
+ "SearchResults",
28
+ "VideoMetadata",
29
+ "VideoTranscript",
30
+ "WebCrawler",
31
+ "WebSearcher",
32
+ "YouTubeSearcher",
33
+ "is_remote_url",
34
+ "web_crawler_tools",
35
+ "web_search_tools",
36
+ "youtube_search_tools",
37
+ ]
@@ -0,0 +1,214 @@
1
+ """Browser display helpers for local artifacts and remote URLs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import http.server
7
+ import threading
8
+ import time
9
+ import webbrowser
10
+ from pathlib import Path
11
+ from typing import Literal
12
+ from urllib.parse import quote, urlparse
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ SourceType = Literal["remote-url", "local-file", "local-directory"]
17
+
18
+
19
+ class BrowserViewResult(BaseModel):
20
+ """Structured result for a browser display target."""
21
+
22
+ target: str = Field(description="Original resolved target URL/path.")
23
+ display_url: str = Field(description="URL to navigate a browser to.")
24
+ source_type: SourceType = Field(description="Kind of target being displayed.")
25
+ opened: bool = Field(description="Whether the system browser was opened.")
26
+ serve_root: str | None = Field(
27
+ default=None,
28
+ description="Local directory being served for local targets.",
29
+ )
30
+ host: str | None = Field(default=None, description="Local server host.")
31
+ port: int | None = Field(default=None, description="Local server port.")
32
+ duration: int | None = Field(
33
+ default=None,
34
+ description="Requested local-server lifetime in seconds.",
35
+ )
36
+
37
+
38
+ def is_remote_url(target: str) -> bool:
39
+ """Return whether a target is an HTTP(S) URL."""
40
+ parsed = urlparse(target)
41
+ return parsed.scheme in {"http", "https"}
42
+
43
+
44
+ def _url_path(path: Path) -> str:
45
+ return "/".join(quote(part) for part in path.parts)
46
+
47
+
48
+ def _resolve_local_target(target: str | Path) -> tuple[Path, Path, str]:
49
+ path = Path(target).expanduser().resolve()
50
+ if not path.exists():
51
+ raise FileNotFoundError(f"Local target not found: {path}")
52
+
53
+ if path.is_dir():
54
+ return path, path, ""
55
+ return path, path.parent, _url_path(Path(path.name))
56
+
57
+
58
+ class LocalArtifactServer:
59
+ """Small localhost HTTP server for browser-blocked local files/directories."""
60
+
61
+ def __init__(
62
+ self,
63
+ target: str | Path,
64
+ *,
65
+ host: str = "127.0.0.1",
66
+ port: int = 0,
67
+ ) -> None:
68
+ self.target = target
69
+ self.host = host
70
+ self.port = port
71
+ self.resolved_target, self.serve_root, self._relative_url = (
72
+ _resolve_local_target(target)
73
+ )
74
+ self._server: http.server.ThreadingHTTPServer | None = None
75
+ self._thread: threading.Thread | None = None
76
+
77
+ @property
78
+ def source_type(self) -> SourceType:
79
+ """Return whether the target is a file or directory."""
80
+ return "local-directory" if self.resolved_target.is_dir() else "local-file"
81
+
82
+ @property
83
+ def display_url(self) -> str:
84
+ """Return the browser URL for the served target."""
85
+ if self._server is None:
86
+ raise RuntimeError("Local artifact server has not been started.")
87
+ port = int(self._server.server_address[1])
88
+ display_url = f"http://{self.host}:{port}/"
89
+ if self._relative_url:
90
+ display_url += self._relative_url
91
+ return display_url
92
+
93
+ def start(self) -> LocalArtifactServer:
94
+ """Start serving the target directory in a daemon thread."""
95
+ if self._server is not None:
96
+ return self
97
+
98
+ handler = functools.partial(
99
+ http.server.SimpleHTTPRequestHandler,
100
+ directory=str(self.serve_root),
101
+ )
102
+ self._server = http.server.ThreadingHTTPServer((self.host, self.port), handler)
103
+ self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
104
+ self._thread.start()
105
+ return self
106
+
107
+ def shutdown(self) -> None:
108
+ """Stop the local server if it is running."""
109
+ if self._server is None:
110
+ return
111
+ self._server.shutdown()
112
+ self._server.server_close()
113
+ self._server = None
114
+ self._thread = None
115
+
116
+ def __enter__(self) -> LocalArtifactServer:
117
+ return self.start()
118
+
119
+ def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
120
+ self.shutdown()
121
+
122
+
123
+ class BrowserViewer:
124
+ """Open remote URLs or serve local artifacts for browser display."""
125
+
126
+ def open_remote(
127
+ self, target: str, *, open_browser: bool = True
128
+ ) -> BrowserViewResult:
129
+ """Open a remote HTTP(S) URL or return the browser target."""
130
+ if not is_remote_url(target):
131
+ raise ValueError(f"Not an HTTP(S) URL: {target}")
132
+ if open_browser:
133
+ webbrowser.open(target)
134
+ return BrowserViewResult(
135
+ target=target,
136
+ display_url=target,
137
+ source_type="remote-url",
138
+ opened=open_browser,
139
+ )
140
+
141
+ def serve_local(
142
+ self,
143
+ target: str | Path,
144
+ *,
145
+ host: str = "127.0.0.1",
146
+ port: int = 0,
147
+ open_browser: bool = True,
148
+ duration: int | None = None,
149
+ ) -> tuple[LocalArtifactServer, BrowserViewResult]:
150
+ """Start serving a local file/directory and return the live server."""
151
+ server = LocalArtifactServer(target, host=host, port=port).start()
152
+ if open_browser:
153
+ webbrowser.open(server.display_url)
154
+ result = BrowserViewResult(
155
+ target=str(server.resolved_target),
156
+ display_url=server.display_url,
157
+ source_type=server.source_type,
158
+ opened=open_browser,
159
+ serve_root=str(server.serve_root),
160
+ host=host,
161
+ port=int(server.display_url.split(":", 2)[2].split("/", 1)[0]),
162
+ duration=duration,
163
+ )
164
+ return server, result
165
+
166
+ def serve_for_duration(
167
+ self,
168
+ target: str | Path,
169
+ *,
170
+ host: str = "127.0.0.1",
171
+ port: int = 0,
172
+ duration: int = 300,
173
+ open_browser: bool = True,
174
+ ) -> BrowserViewResult:
175
+ """Serve a local target for a bounded duration; 0 means until interrupted."""
176
+ server, result = self.serve_local(
177
+ target,
178
+ host=host,
179
+ port=port,
180
+ open_browser=open_browser,
181
+ duration=duration,
182
+ )
183
+ try:
184
+ if duration == 0:
185
+ while True:
186
+ time.sleep(3600)
187
+ else:
188
+ time.sleep(duration)
189
+ except KeyboardInterrupt:
190
+ pass
191
+ finally:
192
+ server.shutdown()
193
+ return result
194
+
195
+ def open(
196
+ self,
197
+ target: str | Path,
198
+ *,
199
+ host: str = "127.0.0.1",
200
+ port: int = 0,
201
+ duration: int = 300,
202
+ open_browser: bool = True,
203
+ ) -> BrowserViewResult:
204
+ """Open a remote URL directly or serve a local target for browser display."""
205
+ target_text = str(target)
206
+ if is_remote_url(target_text):
207
+ return self.open_remote(target_text, open_browser=open_browser)
208
+ return self.serve_for_duration(
209
+ target,
210
+ host=host,
211
+ port=port,
212
+ duration=duration,
213
+ open_browser=open_browser,
214
+ )