LLMs guess. Bricks computes.
Bricks is a deterministic execution engine for AI agents. Your LLM writes a Python DSL pipeline from pre-tested building blocks. Bricks validates it (AST whitelist), then executes it — same input, same output, every time. No hallucinated math. No format failures. No token burn on repeat runs.
pip install -e ".[playground]"
bricks playground
# OK Bricks Playground running -> http://localhost:8080
# (browser opened · Ctrl+C to stop)The Playground is a local web UI that runs your task through Bricks against embedded scenarios (CRM, tickets, orders) or your own CSV/JSON upload. BYOK for Anthropic / OpenAI; local Claude Code and Ollama need no key.
git clone https://github.com/hemipaska-maker/bricks-ai.git
cd bricks-ai
pip install -e ".[ai,playground]"
bricks playground # open the web UI at http://localhost:8080
# claude_code is the default — no API key needed
# for anthropic / openai keys, see "Bring Your Own LLM" belowHere's what you'll see — real results tested across three Claude models:
| BricksEngine | Raw LLM | |
|---|---|---|
| Correctness | 100% (every model, every seed) | 0-60% (varies by model) |
| Haiku | ✅ active_count: 18, revenue: 3447.50 |
❌ Hallucinated: 15, 3848.50 |
| Sonnet | ✅ Correct structured output | ❌ Wrote an essay instead of JSON |
| 10-run consistency | 10/10 pass | 6/10 pass (4 hallucinated) |
| 20-run reuse | 20/20 pass, 3,327 tokens total | 12/20 pass, 75,880 tokens |
Bricks composed one blueprint, reused it 20 times at zero cost. The raw LLM called the API 20 times and still got 40% wrong.
Bricks works with any model. Blueprints are model-agnostic — compose with one LLM, execute anywhere.
Set the relevant API key for your provider, then pick the model from the playground's model dropdown (bricks playground):
# Anthropic (default)
export ANTHROPIC_API_KEY=sk-ant-...
# OpenAI
export OPENAI_API_KEY=sk-...
# Google Gemini
export GOOGLE_API_KEY=AIza...
# Local with Ollama (free, no API key) — selectable as `ollama/llama3`
# Claude Code Max plan ($0) — selectable as `claudecode`Your task → LLM generates Python DSL → Bricks validates (AST) → Bricks executes deterministically
- You describe what you want in plain English
- The LLM picks from 100 pre-tested bricks and writes a Python DSL pipeline (validated with an AST whitelist before execution)
- Bricks validates the blueprint (types, connections, missing inputs) before anything runs
- Bricks executes it deterministically — same blueprint, any data, identical results
- The blueprint is saved. Next time, zero LLM calls needed.
The LLM is used once, for planning. Execution is pure Python — no LLM in the loop, no token cost, no hallucination risk.
The codebase is split into two packages with a strict one-way dependency:
| Package | What it is | LLM involvement | Dependencies |
|---|---|---|---|
bricks |
The deterministic engine: brick registry, blueprint validation, DAG execution, 100 stdlib bricks, blueprint store | None — pure Python | pydantic, typer, ruamel.yaml, rich |
bricks_ai |
The AI layer: LLM composer, healing, provider adapters, task orchestrator, playground, MCP server | All of it | bricks + optional LLM SDKs |
bricks_ai imports bricks; the engine never imports the AI layer. The boundary
is enforced in CI by import-linter and an engine-only job that installs the base
package — with no LLM SDK present — and runs the engine test suites. A plain
pip install bricks-ai gives you the deterministic engine; the AI extras opt you in.
Correctness. 100% across every model tested. Raw LLMs hallucinate numbers, ignore format instructions, and drift across runs. Bricks doesn't — every brick is typed and tested.
Cost. Compose once, reuse forever. 20 runs cost 3,327 tokens with Bricks vs 75,880 with raw LLM calls. That's a 95.6% reduction. After the first call, every repeat is free.
Determinism. Same blueprint + different data = guaranteed correct output. No randomness, no temperature, no "try again and hope."
Auditability. Blueprints are plain YAML. Every step is named, every input/output is typed. You can inspect, version, share, and review exactly what ran.
Natural-language task execution lives in the AI layer (bricks_ai):
from bricks_ai import Bricks
# One line setup — auto-discovers all installed brick packs
engine = Bricks.default() # reads API key from environment
# Describe what you want in plain English
result = engine.execute(
"filter active customers and count them",
{"data": customers_list}
)
print(result["outputs"]) # {"active_customers": [...], "count": 42}
print(result["cache_hit"]) # True on second call — zero tokens!
print(result["tokens_used"]) # 0 on cache hitAlready have a blueprint? The deterministic engine runs it with zero LLM involvement (and zero LLM packages installed):
from bricks import run_blueprint
result = run_blueprint("blueprints/revenue.yaml", inputs={"values": [1, 2, 3]})
print(result.outputs)Verified output (a deterministic stdlib brick, regenerated on every commit):
>>> from bricks.core.registry import BrickRegistry
>>> reg = BrickRegistry.from_stdlib()
>>> fn, _ = reg.get("count_words_chars")
>>> fn(text="hello world")
{'result': {'words': 2, 'chars': 11}}Write pipelines as Python instead of YAML. The @flow decorator traces the function once and builds a DAG:
from bricks import step, for_each, branch, flow
# 1. Simple step chain
@flow
def clean_pipeline(data):
cleaned = step.clean(text=data)
return step.summarize(text=cleaned)
blueprint = clean_pipeline.to_blueprint() # → BlueprintDefinition
yaml_str = clean_pipeline.to_yaml() # → YAML string
# 2. for_each — map a brick over every item in a list
@flow
def batch_clean(items):
return for_each(items, do=lambda x: step.clean(text=x), on_error="collect")
# 3. branch — conditional routing
@flow
def route_record(record):
return branch(
condition="is_valid",
if_true=lambda: step.enrich(data=record),
if_false=lambda: step.log_invalid(data=record),
)The LLM composer now generates Python DSL code instead of YAML. Generated DSL is validated with an AST whitelist before execution.
Use Bricks as an MCP server in Claude Desktop or any MCP-compatible host:
{
"mcpServers": {
"bricks": {
"command": "bricks",
"args": ["serve"],
"env": {
"ANTHROPIC_API_KEY": "sk-ant-..."
}
}
}
}Save this to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows).
The MCP server exposes:
- Tool:
execute_task— run any task through Bricks with optionalverbosemode for step-by-step tracing - Resources:
bricks://catalog(all available bricks) andbricks://blueprints(cached blueprints) - Prompts: templates for common tasks
- Persistent store: blueprints cache to
~/.bricks/blueprintsby default, surviving server restarts
# From source (recommended for now)
git clone https://github.com/hemipaska-maker/bricks-ai.git
cd bricks-ai
pip install -e ".[ai,playground]"
# Base install = the deterministic engine only (no LLM SDKs):
# 100 stdlib bricks (data, string, math, validation, encoding),
# blueprint validation/execution, the store, and the engine CLI commands.
# ai: LiteLLM — enables `bricks_ai` composition (compose/demo/serve).
# playground: FastAPI web UI + provider SDKs + the `bricks playground run` CLI.On Windows, pip install bricks-ai[ai] can fail with a long-path error because litellm installs files whose paths exceed the Windows default 260-character limit (MAX_PATH).
Symptom:
ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory: '...\...\...'
Fix (recommended): Enable long paths via the registry or Group Policy:
- Open
regedit - Navigate to
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem - Set
LongPathsEnabledto1 - Restart your terminal
Alternative: Install in a short path (e.g. C:\bricks\) to stay under the 260-char limit.
Verify your setup:
bricks check-envThis command checks your Python version, litellm installation, and (on Windows) whether long paths are enabled.
Usage: bricks [OPTIONS] COMMAND [ARGS]...
Commands:
check Validate a blueprint YAML file (lint...
check-env Diagnose the local environment (Python...
compose AI-compose a blueprint from a natural...
demo Interactive 3-act demo: simplicity ->...
dry-run Validate a blueprint without executing...
init Scaffold a new Bricks project in the...
list List all available Bricks in the registry.
new Scaffold new Bricks components.
playground Bricks Playground — web UI and headless...
run Execute a blueprint.
serve Start the Bricks MCP server on stdio...
store Blueprint store management.
Run bricks <command> --help for per-command flags.
Create a bricks-{name} package and publish it to PyPI. Users install it and it auto-registers:
# your-package/pyproject.toml
[project.entry-points."bricks.packs"]
mypack = "bricks_mypack"# bricks_mypack/__init__.py
from bricks.core.brick import brick
from bricks.core.registry import BrickRegistry
@brick(description="Fetch from my API")
def fetch_my_api(endpoint: str) -> dict:
...
def register(registry: BrickRegistry) -> None:
registry.register("fetch_my_api", fetch_my_api, fetch_my_api.__brick_meta__)After pip install bricks-mypack, Bricks.default() discovers and loads it automatically.
See examples/agent.yaml for a full configuration reference and examples/skill.md for how to describe a skill.
| Brick | Tags | Description |
|---|---|---|
absolute_value |
math, arithmetic | Return the absolute value of a number. Returns {result: absolute}. |
add_days |
date, arithmetic | Add a number of days to an ISO 8601 date. Returns {result: new_date}. |
add_hours |
date, arithmetic | Add hours to an ISO 8601 datetime string. Returns {result: new_datetime}. |
base64_decode |
encoding, base64 | Decode a base64 string to UTF-8. Returns {result: decoded}. |
base64_encode |
encoding, base64 | Encode a UTF-8 string to base64. Returns {result: encoded}. |
calculate_aggregates |
data, aggregate, math | Aggregate a numeric field across a list of dicts. Returns {result: aggregated_value}. |
cast_data_types |
data, casting, types | Cast dict values to specified types. Returns {result: cast_dict}. |
ceil_value |
math, rounding | Round value up to nearest integer. Returns {result: ceiling}. |
chunk_list |
list, chunk, split | Split a list into chunks of a given size. Returns {result: chunks}. |
clamp_value |
math, range | Clamp value to [minimum, maximum]. Returns {result: clamped}. |
clean_whitespace |
string, cleaning | Strip leading/trailing whitespace and collapse internal runs. Returns {result: cleaned}. |
compare_values |
validation, comparison | Compare two values using an operator. Returns {result: bool}. |
compute_hash |
security, hash, digest | Compute a hash digest of a string. Returns {result: hex_digest}. |
concatenate_strings |
string, join | Join a list of strings with a separator. Returns {result: joined}. |
convert_case |
string, case, transform | Convert string case. Returns {result: converted}. |
Showing 15 of 100 stdlib bricks. Full list: docs/BRICK_CATALOG.md.
The CRM-pipeline benchmark generates 50 fake customer records and asks: "How many are active? What's their total revenue? What's the average?"
The task prompt (what both engines receive):
Parse the JSON string, filter for status='active', count the active customers, sum their monthly_revenue, and compute the average revenue. Return:
active_count,total_active_revenue,avg_active_revenue.
The data (50 records, looks like this):
{
"customers": [
{"id": 1, "name": "Bob Jones", "email": "bob.jones0@example.com",
"status": "active", "plan": "pro", "monthly_revenue": 109.0,
"signup_date": "2020-07-07"},
{"id": 2, "name": "Carol Smith", "email": "carol.smith1@example.com",
"status": "inactive", "plan": "enterprise", "monthly_revenue": 514.5,
"signup_date": "2021-02-02"},
...48 more records...
]
}What BricksEngine does:
- LLM reads the task and the brick catalog (not the data) → composes a YAML blueprint:
extract_json_from_str → filter_dict_list(status=active) → count_dict_list → calculate_aggregates(sum, avg) - Bricks validates the blueprint (types, connections, missing inputs)
- Bricks executes it deterministically with the 50 records
- Returns:
{"active_count": 18, "total_active_revenue": 3447.50, "avg_active_revenue": 191.53}✅
What RawLLMEngine does:
- LLM receives the task AND the full 50-record JSON
- LLM tries to count, sum, and average in its head
- Returns... it depends on the model:
- Haiku:
{"active_count": 15, "total_active_revenue": 3848.50, ...}❌ (hallucinated numbers) - Sonnet:
"Let me analyze this step by step. First, I'll identify..."❌ (essay instead of JSON) - ClaudeCode: sometimes correct, sometimes wrong — 60% pass rate over 10 runs
- Haiku:
The key insight: Bricks uses the LLM only for planning (which bricks to chain). The actual math runs in deterministic Python. The raw LLM has to do everything — read 50 records, count, sum, divide — in one shot. That's where hallucination happens.
No API key needed. See Bricks in action right in your terminal:
bricks demoThree acts: compose a blueprint, execute it on CRM data, compare Bricks vs raw LLM. Run bricks demo --act 1 for just the first act.
The playground ships preset scenarios (CRM, support tickets, dataset join, custom) selectable from the web UI or runnable headlessly via the CLI:
bricks playground # web UI — pick a scenario from the dropdown
bricks playground run crm_pipeline # headless: input data, blueprint, outputs
bricks playground run crm_pipeline --compare-raw # add a side-by-side raw-LLM run
bricks playground run path/to/your_scenario.yaml # your own scenario, same shape as the bundled YAMLsEach preset runs BricksEngine on the bundled data, prints the composed blueprint, and (optionally) compares against a raw LLM call.
| Preset | What it proves | Bricks | Raw LLM |
|---|---|---|---|
crm_pipeline |
Determinism beats reasoning | ✅ Correct | ❌ Wrong (hallucination or format failure) |
cross_dataset_join |
Multi-table join + grouped aggregation | ✅ total_completed: 18, basic_revenue: 3758.70, … |
❌ Often misses the join shape |
ticket_pipeline |
Generalises to a different domain (support tickets) | ✅ Correct | ❌ Struggles with PII + filtering |
custom_example |
Inline / BYO data — no dataset file required | ✅ available_count: 3, total_value: 2859.75 |
✅ Trivial enough that raw usually gets it |
Tested with ClaudeCode, Claude Haiku, and Claude Sonnet.