llm-behave
Behavioral testing for LLM applications. A pytest plugin.
No LLM judge. No API cost. Offline models. Works with any provider.
def test_support_bot():
output = my_support_bot("I want a refund for order 1234")
assert_behavior(output) \
.mentions("refund policy") \
.tone("empathetic") \
.not_mentions("competitor")
Why llm-behave?
Most LLM testing tools either:
- Use another LLM to judge the output (expensive, slow, circular)
- Only do exact string matching (misses semantic meaning)
- Don't support multi-turn conversations at all
llm-behave uses small offline transformer models (80MB, runs on CPU) to understand meaning — no API calls, no cost, no internet required during tests.
Install
# Core (tool call assertions, structure tests — no ML deps)
pip install llm-behave
# Full (semantic assertions: mentions, tone, intent, drift)
pip install llm-behave[semantic]
Features at a glance
| Feature | What it does |
|---|---|
mentions() |
Semantic similarity — "money back" matches "refund" |
not_mentions() |
Assert a topic is NOT brought up |
tone() |
Detect empathetic / professional / rude / helpful etc. |
intent() |
Does the response intend to help? refuse? apologize? |
calls_tool() |
Assert which tool the LLM called |
contradicts() |
Assert the output contradicts a reference statement (NLI) |
grounded_in() |
Assert every claim is entailed by the retrieved context (offline RAG eval) |
no_hallucination() |
Assert the output adds no claims unsupported by the context |
cites() |
Assert the output references a given source id / URL / marker |
returns_valid_json() |
Assert the output parses as JSON (tolerates ```json fences) |
matches_schema() |
Validate output JSON against a Pydantic model |
has_keys() |
Assert the output JSON object contains required keys |
ConversationTest |
Multi-turn testing with memory, contradiction detection |
DriftTest |
Save baseline behavior, detect regressions in CI |
Usage
Basic assertions
from llm_behave import assert_behavior
output = my_llm("I want a refund")
# Semantic match — not exact string
assert_behavior(output).mentions("refund policy")
# Tone detection
assert_behavior(output).tone("empathetic")
assert_behavior(output).tone("professional", threshold=0.6)
# Intent
assert_behavior(output).intent("offering to help the customer")
# Negative assertions
assert_behavior(output).not_mentions("competitor")
# Fluent chaining
assert_behavior(output) \
.mentions("refund") \
.tone("empathetic") \
.not_mentions("competitor")
When mentions(), not_mentions(), or intent() fail, the error shows the top-3 closest sentences with their similarity scores — so you can see whether it was a near-miss (0.41) or nowhere close (0.05), instead of guessing from a single number.
Contradiction assertions
Assert that an output contradicts a previous statement — useful for detecting policy reversals across conversation turns.
# Turn 1 said refunds are available. Does turn 3 contradict that?
assert_behavior(turn_3_response).contradicts("Refunds are always available within 30 days.")
RAG / grounding assertions
Verify a RAG answer is actually supported by the retrieved context — fully offline, no judge LLM, no API cost. Uses the same NLI model as contradicts().
context = retriever.get_docs("refund policy") # your retrieved chunks
answer = my_rag_app("How long do I have to return something?")
# Every claim in the answer must be entailed by the context
assert_behavior(answer).grounded_in(context)
# No claim may go beyond the context (catches hallucinations)
assert_behavior(answer).no_hallucination(context)
# The answer must cite the expected source
assert_behavior(answer).cites("doc-42")
On failure, no_hallucination() lists the specific unsupported sentences and their entailment scores, so a green check is trustworthy.
Structured-output assertions
For LLMs that emit JSON. Pure Python — no model download needed.
from pydantic import BaseModel
class Order(BaseModel):
order_id: str
total: float
shipped: bool
output = my_llm("Return the order as JSON")
assert_behavior(output).returns_valid_json()
assert_behavior(output).has_keys(["order_id", "total"])
assert_behavior(output).matches_schema(Order)
Tool call assertions
text, tool_calls = my_llm.chat_with_tools(messages, tools=my_tools)
assert_behavior(text, tool_calls) \
.calls_tool("lookup_order") \
.mentions("order")
Multi-turn conversation testing
from llm_behave import ConversationTest, MockProvider
conv = ConversationTest(agent=my_agent)
conv.say("Hi, my name is Alex")
conv.say("I placed order #5678 last week")
response = conv.say("When will it arrive?")
# Does it remember context from earlier turns?
assert response.recalls("order")
assert response.recalls("Alex")
# Is tone consistent across the whole conversation?
assert response.consistent_tone_across_turns(threshold=0.6)
Drift detection (for CI)
Catch silent regressions when you update your model or prompts.
from llm_behave import DriftTest
# First run: save baseline
@DriftTest.baseline(save_as="support_refund_flow")
def get_baseline_output():
return my_llm("I need a refund")
# Every CI run: compare against baseline
result = DriftTest.compare("support_refund_flow", current_output)
assert result.passed, f"Behavior drift detected: {result.details}"
pytest fixtures (auto-registered)
# These fixtures are available in any test file automatically
def test_with_mock(mock_provider, assert_llm):
provider = mock_provider(responses=["I'll help with your refund right away."])
output = provider.chat([{"role": "user", "content": "refund please"}])
assert_llm(output).mentions("refund").tone("helpful")
def test_conversation(conversation):
conv = conversation(responses=["Hello!", "Sure, I can help with that."])
conv.say("Hi")
response = conv.say("I need help")
assert "help" in response.text.lower()
Providers
Built-in adapters for all major LLM providers:
from llm_behave.providers.openai_adapter import OpenAIProvider
from llm_behave.providers.anthropic_adapter import AnthropicProvider
from llm_behave.providers.ollama_adapter import OllamaProvider
from llm_behave import MockProvider # for tests, no API calls
# All providers have the same interface
provider = OpenAIProvider(model="gpt-4o-mini")
provider = AnthropicProvider(model="claude-haiku-4-5")
provider = OllamaProvider(model="llama3")
output = provider.chat([{"role": "user", "content": "Hello"}])
text, tool_calls = provider.chat_with_tools(messages, tools=my_tools)
Bring your own provider by subclassing LLMProvider:
from llm_behave.providers.base import LLMProvider
class MyProvider(LLMProvider):
def chat(self, messages, **kwargs):
...
def chat_with_tools(self, messages, tools, **kwargs):
...
How it works
llm-behave uses all-MiniLM-L6-v2 — an 80MB sentence-transformer model that runs fully offline on CPU.
mentions()/not_mentions()— splits text into sentences, computes max cosine similarity between any sentence and your concepttone()— batch-encodes input text against example sentences for each tone, returns max similarityintent()— semantic similarity between output and your intent descriptioncontradicts()— NLI (Natural Language Inference) model detects if the output contradicts a reference statementcontradicts_turn()— same NLI model, applied across conversation turnsgrounded_in()/no_hallucination()— the same NLI model, run the other direction: each output sentence must be entailed by some context sentence, else it's flagged as ungroundedreturns_valid_json()/matches_schema()/has_keys()— pure Python + Pydantic, no model needed
Models load lazily on first use and are cached for the rest of the test session. Import time stays fast.
Performance
Measured after model warmup (model loads once per test session):
| Assertion | Time |
|---|---|
mentions() |
~32ms |
tone() |
~40ms |
intent() |
~32ms |
| 4-assertion chain | ~350ms |
pytest markers
import pytest
@pytest.mark.behavioral
def test_refund_flow():
...
@pytest.mark.drift
def test_no_regression():
...
Run only behavioral tests:
pytest -m behavioral
pytest -m drift
Full install options
pip install llm-behave # core only
pip install llm-behave[semantic] # + sentence-transformers + torch
pip install llm-behave[openai] # + openai SDK
pip install llm-behave[anthropic] # + anthropic SDK
pip install llm-behave[ollama] # + ollama SDK
pip install llm-behave[all] # everything
Requirements
- Python 3.10+
- pytest 7.0+
- For semantic assertions:
pip install llm-behave[semantic]
License
MIT — free to use in personal and commercial projects.
Author
Built by Swanand Potnis — Pune, India.
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 llm_behave-0.2.0.tar.gz.
File metadata
- Download URL: llm_behave-0.2.0.tar.gz
- Upload date:
- Size: 35.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f9e5c7f71ae6523dbc36f8e86e2f0b49877225131a61b62763cda771c509c9e
|
|
| MD5 |
93fc47a2f6f8e6e5eb7641df567dd4f1
|
|
| BLAKE2b-256 |
7178bfd1fd5f77aef7047bd1f868b248d2dc05f9a96580ec751bcda2b2d1e303
|
File details
Details for the file llm_behave-0.2.0-py3-none-any.whl.
File metadata
- Download URL: llm_behave-0.2.0-py3-none-any.whl
- Upload date:
- Size: 25.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc44f74bc1050de315e45c6628dfeb346ddf465a044ade96a5b71fc12ed6f8b6
|
|
| MD5 |
5089276b7a833780e392f797b3bf179d
|
|
| BLAKE2b-256 |
1b8348ce9e0c751a0d751cdf3ea8a45ec0b8917c97c9b6799b56b87d1640d7ae
|