Metadata-Version: 2.5
Name: plural
Version: 0.9.0
Summary: Unified LLM routing with first-class traces, environments, and benchmarks.
Project-URL: Homepage, https://github.com/lastlabs-ai/plural
Project-URL: Documentation, https://lastlabs-ai.github.io/plural/
Project-URL: Repository, https://github.com/lastlabs-ai/plural
Project-URL: Issues, https://github.com/lastlabs-ai/plural/issues
Project-URL: Changelog, https://github.com/lastlabs-ai/plural/blob/main/CHANGELOG.md
Author: Plural
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: ai,benchmarks,environments,llm,openrouter,routing,tracing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.7
Requires-Dist: pyyaml<7,>=6.0
Requires-Dist: tomli<3,>=2.0; python_version < '3.11'
Requires-Dist: typer<1,>=0.12
Provides-Extra: all
Requires-Dist: daytona<1,>=0.207; extra == 'all'
Requires-Dist: keyring<26,>=25; extra == 'all'
Requires-Dist: opentelemetry-api<2,>=1.27; extra == 'all'
Requires-Dist: opentelemetry-sdk<2,>=1.27; extra == 'all'
Provides-Extra: daytona
Requires-Dist: daytona<1,>=0.207; extra == 'daytona'
Provides-Extra: keyring
Requires-Dist: keyring<26,>=25; extra == 'keyring'
Provides-Extra: otel
Requires-Dist: opentelemetry-api<2,>=1.27; extra == 'otel'
Requires-Dist: opentelemetry-sdk<2,>=1.27; extra == 'otel'
Description-Content-Type: text/markdown

# Plural

**Unified LLM routing with first-class traces, environments, and benchmarks.**

Plural is for builders putting AI into products. Start with an OpenRouter-style multi-provider router. Keep going with the thing OpenRouter does not give you: a single `Trace` object shared by production traffic and RL-style environments — so you can understand prompts, build datasets, run benchmarks, and eventually autoroute to the best model for *your* data.

```mermaid
flowchart LR
  App[Your app] --> Client[Plural]
  Env[Environment] --> Client
  Client --> Trace[Trace]
  Trace --> TraceDataset[TraceDataset / Dataset]
  TraceDataset --> Training[Training / export]
  TaskDataset[TaskDataset] --> Bench[Benchmark]
  Env --> Bench
```

## Install

```bash
pip install plural
# optional OpenTelemetry exporter
pip install "plural[otel]"
# optional OS keyring / Daytona sandbox provider
pip install "plural[keyring]" "plural[daytona]"
```

## Quickstart

Set `PLURAL_API_KEY`, then use the client like the OpenAI SDK:

```bash
export PLURAL_API_KEY=plural-...
```

```python
from plural import Client, Message

client = Client()
assert client.is_authenticated()
response = client.chat(
    model="openai/gpt-4o-mini",
    messages=[Message(role="user", content="Hello from plural")],
    models=["anthropic/claude-sonnet-4"],  # optional fallbacks
)
print(response.text)
client.close()
# Traces → .plural/traces.jsonl
```

Create or update a hosted environment, trace, or benchmark with
`client.create(x)` and `client.update(x)`. Environments, agent templates, and
benchmarks are addressed by project-unique slug, so you do not need the
hosted id. Create templates with `client.agents.templates.create(...)`. A
project API key already knows the project; an account key needs `project=` or
`PLURAL_PROJECT`.

Optional BYOK (pass your own upstream keys explicitly):

```python
import os
from plural import Client

client = Client(providers={"openai": os.environ["OPENAI_API_KEY"]})
```

## Four pillars

| Pillar | What you get |
| --- | --- |
| **Router** | Sync/async chat + streaming, fallbacks, retries, cost accounting, model catalog |
| **Tracing** | JSONL / SQLite / OTel sinks, redaction, sampling, late labels, attempt history |
| **Environments** | Versioned `Environment` subclass + native actions + scorers → one episode Trace |
| **Benchmarks** | Environment × models → markdown/JSON report with win rates |

## Package execution foundation (0.9)

Plural includes a local-first CLI and immutable execution domain. Environments
own native actions, schemas, guardrails, runtime, and resources. Harnesses are
stamped and capability-restricted. Agents split into templates and instances.

```bash
plural env init environment --name support
plural env action list --environment environment
plural env capabilities --environment environment
plural harness init harness --name support-loop
plural env harness stamp harness --environment environment
plural benchmark init benchmark.yaml --environment environment
plural agent template init agent.yaml --model openai/gpt-4o-mini \
  --environment environment
plural job init job.yaml --environment environment \
  --benchmark benchmark.yaml --agent agent.yaml
plural run job.yaml --dry-run
```

An Environment owns tasks, instructions, native actions, runtime, and
verification. An `AgentTemplate` is Model + Environment, optionally plus a
stamped harness. A Job expands templates × selected task IDs × `n_attempts`
into stable Trials; retries keep the same Trial identity. Locks, logs,
artifacts, receipts, and results persist under `.plural/jobs`.

Execution providers are unsafe local subprocesses (explicit opt-in), hardened
Docker containers, optional Daytona sandboxes, and entry-point plugins.
Unsatisfiable environment requirements fail before any sandbox is created.
Receipts are currently `self_reported` and package signatures are not verified.
Local runs make no hosted writes unless `--sync` is passed; completed results
can be replayed with `plural job upload`. See the
[CLI docs](https://lastlabs-ai.github.io/plural/cli/), [security
boundaries](https://lastlabs-ai.github.io/plural/operations/security/), and
[known limitations](https://lastlabs-ai.github.io/plural/reference/limitations/).

## Environments in 30 seconds

```python
from plural import Environment, TaskData

env = Environment(name="support-triage", version="0.1.0")

@env.action
def lookup_order(order_id: str) -> dict:
    """Look up an order."""
    return {"status": "shipped"}

@env.scorer(weight=1.0)
def ok(rollout) -> float:
    return 1.0 if "shipped" in (rollout.response.text or "").lower() else 0.0

@env.tasks
def tasks():
    yield TaskData(task_id="1", input="Where is order A?")

rollout = env.rollout(next(env.iter_tasks()), client, model="openai/gpt-4o-mini")
print(rollout.trace.outcome)
```

Action environments use the built-in dispatch. For scalar or custom text
actions, override `apply_action()` and return `ActionResult`; `step()` remains
framework-owned so lifecycle and trace invariants are always recorded.

## Docs

Follow the beginner-to-advanced [documentation](https://lastlabs-ai.github.io/plural/):

- [Install and authenticate](docs/getting-started/setup.md)
- [First offline evaluation](docs/quickstart.md)
- [Practical Python walkthrough](docs/tutorials/sdk-walkthrough.md)
- [Complete CLI walkthrough](docs/tutorials/cli-walkthrough.md)
- [Fetch and update Plural Intel objects](docs/guides/push-to-plural.md)
- [All SDK and CLI features](docs/reference/feature-map.md)

Detailed lifecycle, security, package schemas, and generated command/API
references remain available for advanced integrations.

## Development

```bash
uv sync --group dev --group docs
uv run python scripts/generate_trace_schema.py --check
uv run python scripts/generate_package_schemas.py --check
uv run python scripts/generate_cli_reference.py --check
uv run pytest -m "not live"
uv run ruff check .
uv run mypy src/plural
uv run mkdocs build --strict
```

## License

Apache-2.0
