Metadata-Version: 2.4
Name: ellements
Version: 0.2.0
Summary: Public Ellements primitives for LLM calls, execution strategies, finite-state linguistic machines, benchmarking, and CLI presentation.
Author-email: Paulo Salem <paulosalem@paulosalem.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/paulosalem/ellements
Project-URL: Documentation, https://github.com/paulosalem/ellements
Project-URL: Repository, https://github.com/paulosalem/ellements
Project-URL: Bug Tracker, https://github.com/paulosalem/ellements/issues
Keywords: llm,ai,prompting,benchmarking,cli,tooling
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: chevron<1,>=0.14.0
Requires-Dist: litellm<2,>=1.0.0
Requires-Dist: pydantic<3,>=2.0.0
Requires-Dist: pyyaml<7,>=6.0
Requires-Dist: rich<15,>=13.0.0
Requires-Dist: tiktoken<1,>=0.7.0
Requires-Dist: typing-extensions<5,>=4.8.0
Provides-Extra: core
Provides-Extra: execution
Requires-Dist: chevron<1,>=0.14.0; extra == "execution"
Provides-Extra: fslm
Provides-Extra: benchmarking
Requires-Dist: lm-eval<0.5,>=0.4.0; extra == "benchmarking"
Provides-Extra: cli
Requires-Dist: rich<15,>=13.0.0; extra == "cli"
Requires-Dist: textual<7,>=0.40.0; extra == "cli"
Provides-Extra: agents
Requires-Dist: openai-agents<1,>=0.0.1; extra == "agents"
Provides-Extra: reporting
Requires-Dist: jinja2<4,>=3.1.0; extra == "reporting"
Requires-Dist: matplotlib<4,>=3.7.0; extra == "reporting"
Requires-Dist: pandas<4,>=2.0.0; extra == "reporting"
Provides-Extra: finance
Requires-Dist: numpy<3,>=1.26.0; extra == "finance"
Requires-Dist: numpy-financial<2,>=1.0.0; extra == "finance"
Requires-Dist: pandas<4,>=2.0.0; extra == "finance"
Requires-Dist: yfinance<1,>=0.2.0; extra == "finance"
Provides-Extra: finance-technical
Requires-Dist: jinja2<4,>=3.1.0; extra == "finance-technical"
Requires-Dist: matplotlib<4,>=3.7.0; extra == "finance-technical"
Requires-Dist: mplfinance<1,>=0.12.10b0; extra == "finance-technical"
Requires-Dist: numpy<3,>=1.26.0; extra == "finance-technical"
Requires-Dist: pandas<4,>=2.0.0; extra == "finance-technical"
Requires-Dist: TA-Lib<1,>=0.6.5; extra == "finance-technical"
Requires-Dist: yfinance<1,>=0.2.0; extra == "finance-technical"
Provides-Extra: finance-quant
Requires-Dist: numpy<3,>=1.26.0; extra == "finance-quant"
Requires-Dist: pandas<4,>=2.0.0; extra == "finance-quant"
Requires-Dist: quantstats<1,>=0.0.62; extra == "finance-quant"
Provides-Extra: domain-specific
Requires-Dist: jinja2<4,>=3.1.0; extra == "domain-specific"
Requires-Dist: matplotlib<4,>=3.7.0; extra == "domain-specific"
Requires-Dist: mplfinance<1,>=0.12.10b0; extra == "domain-specific"
Requires-Dist: numpy<3,>=1.26.0; extra == "domain-specific"
Requires-Dist: numpy-financial<2,>=1.0.0; extra == "domain-specific"
Requires-Dist: pandas<4,>=2.0.0; extra == "domain-specific"
Requires-Dist: quantstats<1,>=0.0.62; extra == "domain-specific"
Requires-Dist: TA-Lib<1,>=0.6.5; extra == "domain-specific"
Requires-Dist: yfinance<1,>=0.2.0; extra == "domain-specific"
Provides-Extra: web-search
Requires-Dist: duckduckgo-search<9,>=6.0.0; extra == "web-search"
Provides-Extra: web-crawl
Requires-Dist: crawl4ai<1,>=0.3.0; extra == "web-crawl"
Requires-Dist: nest-asyncio<2,>=1.6.0; extra == "web-crawl"
Requires-Dist: playwright<2,>=1.40.0; extra == "web-crawl"
Provides-Extra: web-youtube
Requires-Dist: httpx<0.28.0,>=0.24.0; extra == "web-youtube"
Requires-Dist: youtube-search-python<2,>=1.6.0; extra == "web-youtube"
Requires-Dist: youtube-transcript-api<2,>=1.2.4; extra == "web-youtube"
Provides-Extra: web
Requires-Dist: crawl4ai<1,>=0.3.0; extra == "web"
Requires-Dist: duckduckgo-search<9,>=6.0.0; extra == "web"
Requires-Dist: httpx<0.28.0,>=0.24.0; extra == "web"
Requires-Dist: nest-asyncio<2,>=1.6.0; extra == "web"
Requires-Dist: playwright<2,>=1.40.0; extra == "web"
Requires-Dist: youtube-search-python<2,>=1.6.0; extra == "web"
Requires-Dist: youtube-transcript-api<2,>=1.2.4; extra == "web"
Provides-Extra: standard-tools
Requires-Dist: crawl4ai<1,>=0.3.0; extra == "standard-tools"
Requires-Dist: duckduckgo-search<9,>=6.0.0; extra == "standard-tools"
Requires-Dist: httpx<0.28.0,>=0.24.0; extra == "standard-tools"
Requires-Dist: nest-asyncio<2,>=1.6.0; extra == "standard-tools"
Requires-Dist: playwright<2,>=1.40.0; extra == "standard-tools"
Requires-Dist: youtube-search-python<2,>=1.6.0; extra == "standard-tools"
Requires-Dist: youtube-transcript-api<2,>=1.2.4; extra == "standard-tools"
Provides-Extra: all
Requires-Dist: chevron<1,>=0.14.0; extra == "all"
Requires-Dist: crawl4ai<1,>=0.3.0; extra == "all"
Requires-Dist: duckduckgo-search<9,>=6.0.0; extra == "all"
Requires-Dist: httpx<0.28.0,>=0.24.0; extra == "all"
Requires-Dist: jinja2<4,>=3.1.0; extra == "all"
Requires-Dist: lm-eval<0.5,>=0.4.0; extra == "all"
Requires-Dist: matplotlib<4,>=3.7.0; extra == "all"
Requires-Dist: mplfinance<1,>=0.12.10b0; extra == "all"
Requires-Dist: nest-asyncio<2,>=1.6.0; extra == "all"
Requires-Dist: numpy<3,>=1.26.0; extra == "all"
Requires-Dist: numpy-financial<2,>=1.0.0; extra == "all"
Requires-Dist: openai-agents<1,>=0.0.1; extra == "all"
Requires-Dist: pandas<4,>=2.0.0; extra == "all"
Requires-Dist: playwright<2,>=1.40.0; extra == "all"
Requires-Dist: quantstats<1,>=0.0.62; extra == "all"
Requires-Dist: rich<15,>=13.0.0; extra == "all"
Requires-Dist: TA-Lib<1,>=0.6.5; extra == "all"
Requires-Dist: textual<7,>=0.40.0; extra == "all"
Requires-Dist: yfinance<1,>=0.2.0; extra == "all"
Requires-Dist: youtube-search-python<2,>=1.6.0; extra == "all"
Requires-Dist: youtube-transcript-api<2,>=1.2.4; extra == "all"
Provides-Extra: dev
Requires-Dist: black<27,>=23.0.0; extra == "dev"
Requires-Dist: mypy<2,>=1.7.0; extra == "dev"
Requires-Dist: pre-commit<5,>=3.0.0; extra == "dev"
Requires-Dist: pytest<9,>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio<2,>=0.21.0; extra == "dev"
Requires-Dist: ruff<1,>=0.1.0; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="./ellements_logo_transparent_crisp.png" alt="Ellements logo" width="360">
</p>

<h1>Ellements</h1>

<p >
  <strong>A Python toolkit for extreme experimentation with LLM systems.</strong>
</p>

**Ellements** is best understood as a continuously extended set of building
blocks for LLM experimentation: model clients, prompt context, execution
strategies, agent abstractions, benchmarking harnesses, and terminal UI
primitives. The repository is organized into focused source roots, but ships as
a single PyPI package, `ellements`, containing every public `ellements.*`
subpackage.

> [!WARNING]
> **Do not treat this as a normal dependency.** This repository exists to support
> my own projects, experiments, and tooling. Probably nobody other than me should
> depend on it directly.

> [!NOTE]
> Almost all of it is created with AI assistance, and I myself understand only a
> fraction of it at any given time. This can produce fast-paced changes, as well
> as both good and bad surprises. Pull requests are unlikely to be accepted. This
> is published for transparency and my own reuse, not as a community-maintained
> library or a general-purpose package roadmap.

## Why yet another LLM library?

True, there are many other similar libraries. This one, however, is mine. It exists so I can keep extending it,
test ideas, change direction, delete abstractions, and rebuild APIs as I see fit:
not by committee, not by roadmap, not by community consensus.

## Design principles

The practical consequences are the following:

- **One install, focused internals.** `pip install ellements` installs the full
  namespace: `ellements.core`, `ellements.execution`, `ellements.agents`,
  `ellements.benchmarking`, `ellements.cli`, `ellements.domain_specific`,
  `ellements.reporting`, `ellements.standard_tools`, and `ellements.fslm`.
- **Extension before stabilization.** The point is to keep adding mechanisms as
  they become useful in my projects. Stability comes later, if it comes at all.
- **Async-only public API.** No hidden sync wrappers. Callers keep explicit
  control of concurrency.
- **Explicit model selection.** `LLMClient(model=...)` is required. There is no
  hidden default.
- **Structured observability.** Every LLM call emits request, response, and
  error events. `JsonlPromptLogger` writes durable JSON-lines traces.
- **Composable strategy layer.** Single-call, reflection, self-consistency,
  tree-of-thought, and collaborative editing all conform to one `Strategy`
  protocol.
- **Backend-agnostic agents.** `ellements.agents` is an abstraction layer over
  external agent libraries. OpenAI Agents and Claude Agents are adapters; they
  are not the identity of the module.
- **Finite-state linguistic machines.** `ellements.fslm` keeps agentic workflows
  bounded by explicit state graphs while still allowing natural-language guards,
  invariants, actions, and outputs where they are useful. The `L` is deliberate:
  language is part of the control surface, not a decorative interface.

## Install

```bash
pip install ellements
```

Optional extras install integration-specific dependencies. They do **not** split
the package or change which modules ship in the wheel.

```bash
pip install "ellements[agents]"        # declared agent-adapter dependencies
pip install "ellements[benchmarking]"  # lm-eval integration
pip install "ellements[cli]"           # Textual-powered terminal UI helpers
pip install "ellements[fslm]>=0.2.0"   # finite-state linguistic machines
pip install "ellements[finance]"       # Yahoo Finance asset tools
pip install "ellements[web]"           # web search, crawl, and YouTube tools
pip install "ellements[reporting]"     # chart/report export helpers
pip install "ellements[all]"           # all declared optional integrations
```

In particular, the agents module is not OpenAI-specific. It defines shared
builder, controller, runner, and event abstractions over external agent
libraries. The OpenAI adapter has declared optional dependencies; the Claude
adapter is included in the wheel, but its upstream SDK is still evolving, so
install it separately when using that backend.

## What ships

| Import package | Purpose |
| --- | --- |
| `ellements.core` | `LLMClient`, conversations, multimodal inputs, tools, prompt context, templating, observers, caching, rate limiting, budgeting, and config helpers |
| `ellements.execution` | Prompting strategies: `SingleCallStrategy`, `ReflectionStrategy`, `SelfConsistencyStrategy`, `TreeOfThoughtStrategy`, `CollaborativeEditingStrategy` |
| `ellements.agents` | Backend-agnostic layer over external agent libraries: runner, controller, fluent `AgentBuilder`, event surface, and OpenAI/Claude adapters |
| `ellements.benchmarking` | Async benchmark harnesses, model runners, dataset adapters, and comparison helpers |
| `ellements.cli` | Terminal presentation primitives, agent TUI components, and slash-command building blocks |
| `ellements.domain_specific` | Domain tools, currently including finance calculators, Yahoo Finance asset data, valuation, technical indicators, and risk helpers |
| `ellements.reporting` | Chart artifacts, HTML report generation, and multi-format presentation helpers |
| `ellements.standard_tools` | Reusable tool surfaces for terminal execution, web search, web crawl/read, and YouTube search/transcript access |
| `ellements.fslm` | Finite-state linguistic machines: explicit graphs, deterministic kernel, natural-language evaluators, persistence, observers, and `fslm` CLI |

## Quick start

```python
import asyncio

from ellements.core import JsonlPromptLogger, LLMClient


async def main() -> None:
    client = LLMClient(
        model="openai/gpt-5.5",
        observers=[JsonlPromptLogger("./logs")],
    )

    answer = await client.complete("Explain attention in one paragraph.")
    print(answer)


asyncio.run(main())
```

## Run a strategy

```python
from ellements.execution import ReflectionConfig, ReflectionStrategy

strategy = ReflectionStrategy()

result = await strategy.execute(
    prompts={
        "generate": "Draft a haiku about distributed systems.",
        "critique": "Review this draft and return a CritiqueResult:\n\n{{response}}",
        "revise": (
            "Revise the draft using the issues below.\n\n"
            "Draft:\n{{response}}\n\nIssues:\n{{issues}}"
        ),
    },
    client=client,
    config=ReflectionConfig(max_rounds=3),
)

print(result.output)
```

Runtime strategy templates accept both Mustache placeholders such as
`{{response}}` and PromptSpec-style placeholders such as `@{response}`.

## Build an agent

OpenAI is shown here as one concrete adapter; the builder targets the
backend-agnostic `AgentBackend` protocol.

```python
from ellements.agents import AgentBuilder, OpenAIAgentsBackend

agent = (
    AgentBuilder("researcher", backend=OpenAIAgentsBackend())
    .with_model("gpt-4.1")
    .with_instructions("Research carefully, cite evidence, and be concise.")
    .with_tool("search", my_search_tool)
    .build()
)
```

## Use finance and web tools

Finance and web tools expose canonical `ToolRegistry` surfaces, so LLM clients,
agents, and PromptSpec examples can bind the same tools without local adapters.

```bash
pip install "ellements[finance,web]"
playwright install chromium  # required by crawl4ai-backed page crawling
```

```python
from ellements.domain_specific.finance.yahoo_finance import finance_tools
from ellements.standard_tools.web.crawler import web_crawler_tools
from ellements.standard_tools.web.search import web_search_tools

tools = (
    finance_tools()
    .merge(web_search_tools())
    .merge(web_crawler_tools(max_content_tokens=4000))
)

print(sorted(tools))
```

Typical stock-research tools include `search_asset`, `get_asset_quote`,
`get_asset_profile`, `get_financial_metrics`, `search_web`, `search_news`, and
`crawl_url`.

## Packaging model

The repo is modular at the filesystem level:

```text
ellements-core/src/ellements/core
ellements-execution/src/ellements/execution
ellements-agents/src/ellements/agents
ellements-benchmarking/src/ellements/benchmarking
ellements-cli/src/ellements/cli
ellements-domain-specific/src/ellements/domain_specific
ellements-fslm/src/ellements/fslm
ellements-reporting/src/ellements/reporting
ellements-standard-tools/src/ellements/standard_tools
```

Those source roots are discovered into a single wheel,
`ellements-<version>-py3-none-any.whl`. Users install one PyPI project and
import the modules they need:

```python
from ellements.core import LLMClient
from ellements.domain_specific.finance.yahoo_finance import finance_tools
from ellements.execution import TreeOfThoughtStrategy
from ellements.agents import AgentBuilder
from ellements.fslm import FSLMKernel
from ellements.standard_tools.web.search import web_search_tools
```

This gives the repo clean internal boundaries without creating multiple PyPI
entries to publish, document, secure, and maintain.

## Local development

```bash
pip install -e ".[dev,all]"
python -m pytest -q
python -m ruff check .
python -m mypy --strict ellements-core/src ellements-execution/src \
  ellements-agents/src ellements-benchmarking/src ellements-cli/src \
  ellements-domain-specific/src ellements-fslm/src ellements-reporting/src \
  ellements-standard-tools/src
```

## License

MIT, with the project status and maintenance notice in [`LICENSE`](LICENSE).
