Speculate is a BDD-style framework for validating LLM behavior, offering rich CLI reporting and support for multiple backends like Ollama, OpenAI, and more.
Project description
speculate
speculate is a lightweight, Docker-ready behavior-driven testing framework for Large Language Models (LLMs).
It helps you write clear, structured specs for how your LLM should behave, then validates actual responses.
Built by Donavan White — powered with Pydantic validation, rich CLI output, and first-class support for Ollama.
✨ Features
- BDD-style scenarios with step chaining: define multiple
.prompt(...).expect_*(...)steps. - System prompt support: global or per-scenario.
- Multi-shot control:
multi_shot(True)(default): steps chain with conversation history per run.multi_shot(False): only the first step runs, no history passed.
- Runs-per-test and accuracy thresholds:
.runs(N)to repeat each scenario..require_accuracy(0.9)to enforce pass rates.
- Seed management:
- Provider-level default seed.
- Scenario override with
.seed(1234). - Randomize seeds per run with
.randomize_seed(True).
- Raw output capture:
.dump_raw(mode="never|fail|always", to_dir="raw_outputs", file_format="txt|json").
- Expectation types:
- Exact match, substring, regex, not equal, not contains, Pydantic schema.
- Rich CLI output with per-run tables, accuracy bars, and suite summary.
- Dockerized runner: reproducible, isolated environment.
- Ollama integration: works with
/api/chatand falls back to/api/generate.
📂 Project Structure
speculate/
├─ __init__.py
├─ core.py # Scenario runner & summary (rich output)
├─ cli.py # CLI entrypoint
├─ providers/
│ ├─ __init__.py
│ └─ ollama_provider.py # Provider wrapper for Ollama API
scenarios/ # Example scenarios
models/ # Example Pydantic models
requirements.txt
Dockerfile
docker-compose.yml
🚀 Quick Start
- Ensure Ollama is running on your host:
ollama serve
ollama run mistral # or another model, e.g. llama3.1, qwen2.5
- Build and run tests in Docker:
docker compose build
docker compose up
Or run directly via the CLI:
python -m speculate.cli scenarios/
After packaging & install, you’ll get:
speculate scenarios/
⚙️ Configuration
Environment variables:
-
OLLAMA_BASE_URL
Default:http://host.docker.internal:11434
Use the Ollama server base URL. -
OLLAMA_API_STYLE
Options:chat→ force/api/chatgenerate→ force/api/generateopenai→ proxy-compatible/v1/chat/completions- (default) auto: try
/api/chat, fall back to/api/generate.
-
OLLAMA_TIMEOUT_S
Timeout in seconds (default 120). -
LLM_DEFAULT_SEED
Provider default seed. -
LLM_RUNS_PER_TEST
Default runs per scenario. -
LLM_ACCURACY_THRESHOLD
Suite-wide accuracy requirement. -
LLM_RAW_DUMP
never|fail|always. -
LLM_RAW_DUMP_DIR
Directory to save raw outputs.
🧪 Writing Tests
Scenarios go in scenarios/*.py. Each test chains prompts and expectations.
from speculate import Scenario
from speculate.providers import OllamaProvider
from models.greeting import GreetingResponse
provider = OllamaProvider(model="mistral")
# JSON schema validation
Scenario("test_json_greeting", provider)\
.set_system_prompt("Always respond in JSON.")\
.prompt("Return greeting='Hello' and name='Donavan'.")\
.expect_schema(GreetingResponse, greeting="Hello", name="Donavan")\
.runs(3)\
.require_accuracy(0.9)\
.dump_raw(mode="fail", to_dir="raw_outputs", file_format="json")\
.run()
# Contains expectation, random seed per run
Scenario("test_contains_name", provider)\
.randomize_seed(True)\
.prompt("Say hello to Donavan in one short sentence.")\
.expect_contains("Donavan")\
.runs(5)\
.run()
# Multi-step chained prompts
Scenario("test_multi_step", provider)\
.set_system_prompt("Answer tersely.")\
.prompt("Explain why the sky is blue.")\
.expect_contains("Rayleigh")\
.prompt("Is that wavelength-dependent?")\
.expect_contains("yes")\
.runs(2)\
.run()
# Single-shot: ignores second step
Scenario("test_single_shot", provider)\
.multi_shot(False)\
.prompt("THIS runs")\
.expect_contains("THIS")\
.prompt("THIS will be ignored")\
.expect_contains("ignored")\
.runs(3)\
.run()
Pydantic schema example:
# models/greeting.py
from pydantic import BaseModel
class GreetingResponse(BaseModel):
greeting: str
name: str
📊 Output Example
Each scenario prints:
- Header panel (scenario, mode, seed info, prompt preview).
- Per-run table (run #, PASS/FAIL badge, seed, details).
- Accuracy bar + threshold info.
- Suite summary at the bottom.
🛠 Extending
- Add other providers by implementing
generate(prompt, system_prompt, history, seed, **kwargs). - Export
RESULTSfromspeculate.coreas JSON or JUnit for CI pipelines. - Build fixtures for common prompts or expectations.
- Use
.expect_not_equal(...)and.expect_not_contains(...)for negative checks.
📦 Packaging
- Editable install for development:
pip install -e .
speculate scenarios/
📜 License
MIT License © 2025 Donavan White (digi-cazter)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file speculate_llm-0.1.0.tar.gz.
File metadata
- Download URL: speculate_llm-0.1.0.tar.gz
- Upload date:
- Size: 135.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b45f95113355e8e4bcfd45a196f3c2a3c6b411e3d725079e7b3b3a782c4682da
|
|
| MD5 |
6e9a70d9c1b1ed4f8618588546bfa342
|
|
| BLAKE2b-256 |
5fde845cf55af5e26bc77994037708ca34b7cb203aefcd9e88d6157c442c1069
|
File details
Details for the file speculate_llm-0.1.0-py3-none-any.whl.
File metadata
- Download URL: speculate_llm-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa953529771735d4d3046daa94802d6c49eae1bc9ce4a32c9e649e5ce2fc8e9d
|
|
| MD5 |
e2f9c4252c68544bad6a9a3d2141725e
|
|
| BLAKE2b-256 |
ac34e313a0df03b50ef1aaae86353e69da76681df6f8598906e54d505421e968
|