A scripted LLM test double that verifies what your pipeline feeds the model, not just what it does with the response.
Project description
dummybot
A scripted LLM test double that verifies what your pipeline feeds the model — not just what it does with the response.
Most LLM mocking looks like this:
def fake_model(prompt):
return "42"
That proves your code can handle "42". It proves nothing about the prompt: did the retrieved context actually make it in? Did the system instructions survive your template refactor? Did an internal debug blob leak into the model's input? In LLM pipelines, most real bugs are feed bugs — the model was called with the wrong thing — and a return-only fake is blind to all of them.
Dummybot treats the model seam as a contract boundary:
- Trigger-keyed dispatch — each scripted line answers only prompts containing its trigger substring, in script order, with per-line use limits.
- Prompt input assertions — required substrings, forbidden substrings, and any-of groups checked against the actual prompt your pipeline sent.
- Typed faults, never silent drift — an unmatched call, a failed assertion, or a partially-consumed script raises a structured error naming exactly what went wrong (with a prompt digest and the available triggers).
- Completion contract — assert that every expected model call actually happened; the pytest fixture does this automatically at teardown.
- Scripted failures — lines can simulate provider errors (timeouts, refusals) so you can test your error handling deterministically.
- Zero dependencies — pure standard library; pytest is only needed for the optional fixture.
Install
pip install dummybot
Quickstart
from dummybot import Dummybot, PromptAssertion, Script, ScriptLine
script = Script(
name="rag-happy-path",
lines=(
ScriptLine(
line_id="answer",
trigger="USER QUESTION:", # matches as a substring of the prompt
response="Paris",
assertions=(
PromptAssertion.requires("RETRIEVED CONTEXT:", "Eiffel"),
PromptAssertion.forbids("INTERNAL_DEBUG"),
),
),
),
)
bot = Dummybot(script)
# Patch it in wherever your pipeline calls its model provider:
answer = my_rag_pipeline(question="Where is the Eiffel Tower?", model_call=bot)
assert answer == "Paris"
bot.assert_all_expected_calls_consumed() # every expected model call happened
If the pipeline drops the retrieved context on the floor, you don't get a mysterious wrong answer — you get:
DummybotFault: [input_assertion_failed] input_assertion_failed
assertion_label: 'required_substrings'
line_id: 'answer'
missing_required_substrings: ('RETRIEVED CONTEXT:', 'Eiffel')
prompt_digest: 'sha256:…'
The pytest fixture
Installing the package registers a dummybot fixture (a factory). Bots built
through it are automatically completion-checked at test teardown:
def test_pipeline_calls_planner_then_answerer(dummybot):
bot = dummybot({
"name": "two-step",
"lines": [
{"line_id": "plan", "trigger": "PLAN THE TASK", "response": '{"steps": ["look up"]}'},
{"line_id": "answer", "trigger": "FINAL ANSWER", "response": "done",
"assertions": [{"required_substrings": ["steps"]}]},
],
})
run_pipeline(model_call=bot)
# teardown fails the test if either line was never used
Opt out per-bot with dummybot(script, autocheck=False).
Scripts as JSON
Scripts are plain data — keep them next to your tests and load them:
from dummybot import load_script
bot = Dummybot(load_script("tests/scripts/rag_happy_path.json"))
{
"schema": "dummybot_script.v1",
"name": "rag-happy-path",
"lines": [
{
"line_id": "answer",
"trigger": "USER QUESTION:",
"response": "Paris",
"max_uses": 1,
"assertions": [
{"required_substrings": ["RETRIEVED CONTEXT:"], "forbidden_substrings": ["INTERNAL_DEBUG"]}
]
}
]
}
Simulating model failures
script = Script(name="outage", lines=(
ScriptLine(line_id="down", trigger="USER QUESTION:", fault_type="model_unavailable"),
))
bot = Dummybot(script) # raises ScriptedModelError on match
bot = Dummybot(script, # or raise the exception type your seam catches
fault_factory=lambda line, prompt: TimeoutError("scripted outage"))
Shaping the return value for your seam
By default a matched line returns plain text (raw_response if set, else
response — use raw_response to include <think> blocks your seam is
supposed to strip). If your model-call seam expects a richer object, pass a
response_factory:
def ollama_shaped(line, prompt, kwargs):
return {
"response": line.response,
"model": kwargs.get("model", "qwen3:4b"),
"prompt_eval_count": 0,
"eval_count": 0,
}
bot = Dummybot(script, response_factory=ollama_shaped)
Helpers for common response shapes:
from dummybot import json_response, thinking_json_response
json_response({"answer": 42}) # deterministic sorted-key JSON
thinking_json_response("let me think", {"answer": 42}) # <think>…</think> + JSON
Semantics worth knowing
- Matching order: first line in script order whose trigger is in the prompt and that has uses remaining. Exhausted lines fall through to later lines — so two lines with the same trigger model a two-step conversation.
max_usesis both a cap and an expectation:max_uses=2means "matches at most twice, and the completion contract expects exactly two uses."max_uses=0means unlimited and optional.- Failed assertions don't consume the line — fix the feed and the same line is still there.
- Scripted fault lines do consume the line — the call happened; it's
recorded in
bot.callsand satisfies the completion contract. bot.callsrecords every matched call: line id, full prompt, prompt digest, and the kwargs your pipeline passed (model name, temperature, …).
Origin
Dummybot was extracted from the test infrastructure of Tamago, a local-first LLM orchestration runtime, where it replaces the model at the Ollama caller seam so that ~2,800 deterministic tests can pin down not just what the pipeline does with model output, but what every stage feeds the model. The feed-assertion idea earned its keep there — most of the bugs it caught were prompts that silently lost or leaked context.
License
MIT
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 dummybot-0.1.0.tar.gz.
File metadata
- Download URL: dummybot-0.1.0.tar.gz
- Upload date:
- Size: 15.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c757fb3b014a305a35e568a34d03092124f02944546c1cd65dfa510b7c272ea1
|
|
| MD5 |
816c54a8d0efd7312f6d8278ffcfe189
|
|
| BLAKE2b-256 |
06a1a806d64397e1ee658de914dcab6e1317791940e5b8c163c1080db652ea95
|
Provenance
The following attestation bundles were made for dummybot-0.1.0.tar.gz:
Publisher:
release.yml on Sicatho/dummybot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dummybot-0.1.0.tar.gz -
Subject digest:
c757fb3b014a305a35e568a34d03092124f02944546c1cd65dfa510b7c272ea1 - Sigstore transparency entry: 2336250442
- Sigstore integration time:
-
Permalink:
Sicatho/dummybot@361dc84c8ab0512f99fb6d0bfe55ca1b7c04b5ec -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Sicatho
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@361dc84c8ab0512f99fb6d0bfe55ca1b7c04b5ec -
Trigger Event:
release
-
Statement type:
File details
Details for the file dummybot-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dummybot-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c9943ff4b50e85fb37c1bd6eb55096ebf93a785dc1cd8230078713607fca2f82
|
|
| MD5 |
c643dbc1c847598d340d81897af6a97d
|
|
| BLAKE2b-256 |
cd61248a48dec080119b91a7e8c4b9e697d22cf07ef173ece361ed23e603eab1
|
Provenance
The following attestation bundles were made for dummybot-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Sicatho/dummybot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dummybot-0.1.0-py3-none-any.whl -
Subject digest:
c9943ff4b50e85fb37c1bd6eb55096ebf93a785dc1cd8230078713607fca2f82 - Sigstore transparency entry: 2336250524
- Sigstore integration time:
-
Permalink:
Sicatho/dummybot@361dc84c8ab0512f99fb6d0bfe55ca1b7c04b5ec -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Sicatho
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@361dc84c8ab0512f99fb6d0bfe55ca1b7c04b5ec -
Trigger Event:
release
-
Statement type: