llm-behave 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_behave-0.1.0/.github/workflows/publish.yml +28 -0
- llm_behave-0.1.0/.gitignore +54 -0
- llm_behave-0.1.0/CHANGELOG.md +30 -0
- llm_behave-0.1.0/LICENSE +21 -0
- llm_behave-0.1.0/PKG-INFO +307 -0
- llm_behave-0.1.0/README.md +260 -0
- llm_behave-0.1.0/llm_assert/__init__.py +17 -0
- llm_behave-0.1.0/llm_assert/core/__init__.py +0 -0
- llm_behave-0.1.0/llm_assert/core/assertions.py +223 -0
- llm_behave-0.1.0/llm_assert/core/conversation.py +156 -0
- llm_behave-0.1.0/llm_assert/core/drift.py +143 -0
- llm_behave-0.1.0/llm_assert/engines/__init__.py +0 -0
- llm_behave-0.1.0/llm_assert/engines/contradiction.py +69 -0
- llm_behave-0.1.0/llm_assert/engines/semantic.py +100 -0
- llm_behave-0.1.0/llm_assert/engines/tone.py +149 -0
- llm_behave-0.1.0/llm_assert/plugin/__init__.py +0 -0
- llm_behave-0.1.0/llm_assert/plugin/fixtures.py +5 -0
- llm_behave-0.1.0/llm_assert/plugin/pytest_plugin.py +48 -0
- llm_behave-0.1.0/llm_assert/providers/__init__.py +0 -0
- llm_behave-0.1.0/llm_assert/providers/anthropic_adapter.py +65 -0
- llm_behave-0.1.0/llm_assert/providers/base.py +79 -0
- llm_behave-0.1.0/llm_assert/providers/ollama_adapter.py +57 -0
- llm_behave-0.1.0/llm_assert/providers/openai_adapter.py +57 -0
- llm_behave-0.1.0/pyproject.toml +78 -0
- llm_behave-0.1.0/tests/__init__.py +0 -0
- llm_behave-0.1.0/tests/test_assertions.py +109 -0
- llm_behave-0.1.0/tests/test_conversation.py +80 -0
- llm_behave-0.1.0/tests/test_drift.py +92 -0
- llm_behave-0.1.0/tests/test_e2e.py +267 -0
- llm_behave-0.1.0/tests/test_harden.py +373 -0
- llm_behave-0.1.0/tests/test_intent.py +36 -0
- llm_behave-0.1.0/tests/test_mentions.py +72 -0
- llm_behave-0.1.0/tests/test_multi_turn.py +106 -0
- llm_behave-0.1.0/tests/test_providers.py +41 -0
- llm_behave-0.1.0/tests/test_semantic_engine.py +62 -0
- llm_behave-0.1.0/tests/test_tone.py +74 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Set up Python
|
|
14
|
+
uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
|
|
18
|
+
- name: Install build tools
|
|
19
|
+
run: pip install build twine hatchling
|
|
20
|
+
|
|
21
|
+
- name: Build package
|
|
22
|
+
run: python -m build --no-isolation
|
|
23
|
+
|
|
24
|
+
- name: Upload to PyPI
|
|
25
|
+
run: twine upload dist/*
|
|
26
|
+
env:
|
|
27
|
+
TWINE_USERNAME: __token__
|
|
28
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.egg
|
|
9
|
+
.eggs/
|
|
10
|
+
|
|
11
|
+
# Virtual environment
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# Testing
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
htmlcov/
|
|
19
|
+
coverage.xml
|
|
20
|
+
.coverage
|
|
21
|
+
*.cover
|
|
22
|
+
|
|
23
|
+
# llm-assert baselines (user-specific, not committed)
|
|
24
|
+
.llm_assert_baselines/
|
|
25
|
+
|
|
26
|
+
# ML models cache
|
|
27
|
+
.models_cache/
|
|
28
|
+
|
|
29
|
+
# IDE
|
|
30
|
+
.idea/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
|
|
34
|
+
# OS
|
|
35
|
+
.DS_Store
|
|
36
|
+
Thumbs.db
|
|
37
|
+
|
|
38
|
+
# Reference docs (not part of the library)
|
|
39
|
+
*.docx
|
|
40
|
+
*.pdf
|
|
41
|
+
rought.txt
|
|
42
|
+
|
|
43
|
+
# Environment
|
|
44
|
+
.env
|
|
45
|
+
.env.local
|
|
46
|
+
|
|
47
|
+
# Internal / private files (not for public GitHub)
|
|
48
|
+
CLAUDE.md
|
|
49
|
+
ideas.txt
|
|
50
|
+
PROJECT_LOG.txt
|
|
51
|
+
PLAIN_ENGLISH_GUIDE.txt
|
|
52
|
+
launch/
|
|
53
|
+
.claudeignore
|
|
54
|
+
.vscode/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to llm-assert will be documented here.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] — 2026-03-12
|
|
6
|
+
|
|
7
|
+
First public release.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `assert_behavior(text)` — fluent assertion interface for LLM outputs
|
|
11
|
+
- `.mentions(concept)` — semantic similarity using sentence-level max comparison
|
|
12
|
+
- `.not_mentions(concept)` — assert a topic is absent
|
|
13
|
+
- `.tone(label)` — detect empathetic / professional / rude / helpful / formal / casual etc.
|
|
14
|
+
- `.intent(description)` — match output intent against a natural language description
|
|
15
|
+
- `.calls_tool(name)` — assert a specific tool was called (supports OpenAI + Anthropic formats)
|
|
16
|
+
- `ConversationTest` — multi-turn conversation harness with full history tracking
|
|
17
|
+
- `.recalls(concept)` — assert response references a concept from earlier in the conversation
|
|
18
|
+
- `.contradicts_turn(n)` — NLI-based contradiction detection across turns
|
|
19
|
+
- `.consistent_tone_across_turns()` — tone consistency check across all assistant turns
|
|
20
|
+
- `DriftTest` — baseline save and compare for CI regression detection
|
|
21
|
+
- `@DriftTest.baseline()` — decorator to save test output as baseline
|
|
22
|
+
- `MockProvider` — built-in mock for testing without real LLM API calls
|
|
23
|
+
- `OpenAIProvider` — adapter for OpenAI chat completions
|
|
24
|
+
- `AnthropicProvider` — adapter for Anthropic messages API
|
|
25
|
+
- `OllamaProvider` — adapter for Ollama local models
|
|
26
|
+
- pytest fixtures: `assert_llm`, `mock_provider`, `conversation`
|
|
27
|
+
- pytest markers: `behavioral`, `drift`
|
|
28
|
+
- Offline ML engine: `all-MiniLM-L6-v2` (80MB, CPU, no internet needed)
|
|
29
|
+
- Batch encoding for tone detection (~4x faster than sequential)
|
|
30
|
+
- Lazy model loading — fast import, model loads on first assertion
|
llm_behave-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Swanand Potnis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-behave
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Behavioral testing for LLM applications. pytest plugin with semantic assertions, multi-turn conversation testing, and drift detection. No LLM judge needed.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Swanand33/llm-behave
|
|
6
|
+
Project-URL: Repository, https://github.com/Swanand33/llm-behave
|
|
7
|
+
Project-URL: Issues, https://github.com/Swanand33/llm-behave/issues
|
|
8
|
+
Author-email: Swanand Potnis <swanandpotnis500@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,anthropic,behavioral-testing,langchain,llm,openai,pytest,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: numpy>=1.24
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pytest>=7.0
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: anthropic>=0.20; extra == 'all'
|
|
28
|
+
Requires-Dist: ollama>=0.1; extra == 'all'
|
|
29
|
+
Requires-Dist: openai>=1.0; extra == 'all'
|
|
30
|
+
Requires-Dist: sentence-transformers>=2.2; extra == 'all'
|
|
31
|
+
Requires-Dist: torch>=2.0; extra == 'all'
|
|
32
|
+
Provides-Extra: anthropic
|
|
33
|
+
Requires-Dist: anthropic>=0.20; extra == 'anthropic'
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-html>=3.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
39
|
+
Provides-Extra: ollama
|
|
40
|
+
Requires-Dist: ollama>=0.1; extra == 'ollama'
|
|
41
|
+
Provides-Extra: openai
|
|
42
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
43
|
+
Provides-Extra: semantic
|
|
44
|
+
Requires-Dist: sentence-transformers>=2.2; extra == 'semantic'
|
|
45
|
+
Requires-Dist: torch>=2.0; extra == 'semantic'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# llm-behave
|
|
49
|
+
|
|
50
|
+
**Behavioral testing for LLM applications. A pytest plugin.**
|
|
51
|
+
|
|
52
|
+
No LLM judge. No API cost. Offline models. Works with any provider.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
def test_support_bot():
|
|
56
|
+
output = my_support_bot("I want a refund for order 1234")
|
|
57
|
+
|
|
58
|
+
assert_behavior(output) \
|
|
59
|
+
.mentions("refund policy") \
|
|
60
|
+
.tone("empathetic") \
|
|
61
|
+
.not_mentions("competitor")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Why llm-behave?
|
|
67
|
+
|
|
68
|
+
Most LLM testing tools either:
|
|
69
|
+
- Use another LLM to judge the output (expensive, slow, circular)
|
|
70
|
+
- Only do exact string matching (misses semantic meaning)
|
|
71
|
+
- Don't support multi-turn conversations at all
|
|
72
|
+
|
|
73
|
+
**llm-behave** uses small offline transformer models (80MB, runs on CPU) to understand meaning — no API calls, no cost, no internet required during tests.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Core (tool call assertions, structure tests — no ML deps)
|
|
81
|
+
pip install llm-behave
|
|
82
|
+
|
|
83
|
+
# Full (semantic assertions: mentions, tone, intent, drift)
|
|
84
|
+
pip install llm-behave[semantic]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Features at a glance
|
|
90
|
+
|
|
91
|
+
| Feature | What it does |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `mentions()` | Semantic similarity — "money back" matches "refund" |
|
|
94
|
+
| `not_mentions()` | Assert a topic is NOT brought up |
|
|
95
|
+
| `tone()` | Detect empathetic / professional / rude / helpful etc. |
|
|
96
|
+
| `intent()` | Does the response intend to help? refuse? apologize? |
|
|
97
|
+
| `calls_tool()` | Assert which tool the LLM called |
|
|
98
|
+
| `ConversationTest` | Multi-turn testing with memory, contradiction detection |
|
|
99
|
+
| `DriftTest` | Save baseline behavior, detect regressions in CI |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
### Basic assertions
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from llm_assert import assert_behavior
|
|
109
|
+
|
|
110
|
+
output = my_llm("I want a refund")
|
|
111
|
+
|
|
112
|
+
# Semantic match — not exact string
|
|
113
|
+
assert_behavior(output).mentions("refund policy")
|
|
114
|
+
|
|
115
|
+
# Tone detection
|
|
116
|
+
assert_behavior(output).tone("empathetic")
|
|
117
|
+
assert_behavior(output).tone("professional", threshold=0.6)
|
|
118
|
+
|
|
119
|
+
# Intent
|
|
120
|
+
assert_behavior(output).intent("offering to help the customer")
|
|
121
|
+
|
|
122
|
+
# Negative assertions
|
|
123
|
+
assert_behavior(output).not_mentions("competitor")
|
|
124
|
+
|
|
125
|
+
# Fluent chaining
|
|
126
|
+
assert_behavior(output) \
|
|
127
|
+
.mentions("refund") \
|
|
128
|
+
.tone("empathetic") \
|
|
129
|
+
.not_mentions("competitor")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Tool call assertions
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
text, tool_calls = my_llm.chat_with_tools(messages, tools=my_tools)
|
|
136
|
+
|
|
137
|
+
assert_behavior(text, tool_calls) \
|
|
138
|
+
.calls_tool("lookup_order") \
|
|
139
|
+
.mentions("order")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Multi-turn conversation testing
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from llm_assert import ConversationTest, MockProvider
|
|
146
|
+
|
|
147
|
+
conv = ConversationTest(agent=my_agent)
|
|
148
|
+
|
|
149
|
+
conv.say("Hi, my name is Swanand")
|
|
150
|
+
conv.say("I placed order #5678 last week")
|
|
151
|
+
response = conv.say("When will it arrive?")
|
|
152
|
+
|
|
153
|
+
# Does it remember context from earlier turns?
|
|
154
|
+
assert response.recalls("order")
|
|
155
|
+
assert response.recalls("Swanand")
|
|
156
|
+
|
|
157
|
+
# Is tone consistent across the whole conversation?
|
|
158
|
+
assert response.consistent_tone_across_turns(threshold=0.6)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Drift detection (for CI)
|
|
162
|
+
|
|
163
|
+
Catch silent regressions when you update your model or prompts.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from llm_assert import DriftTest
|
|
167
|
+
|
|
168
|
+
# First run: save baseline
|
|
169
|
+
@DriftTest.baseline(save_as="support_refund_flow")
|
|
170
|
+
def get_baseline_output():
|
|
171
|
+
return my_llm("I need a refund")
|
|
172
|
+
|
|
173
|
+
# Every CI run: compare against baseline
|
|
174
|
+
result = DriftTest.compare("support_refund_flow", current_output)
|
|
175
|
+
assert result.passed, f"Behavior drift detected: {result.details}"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### pytest fixtures (auto-registered)
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# These fixtures are available in any test file automatically
|
|
182
|
+
|
|
183
|
+
def test_with_mock(mock_provider, assert_llm):
|
|
184
|
+
provider = mock_provider(responses=["I'll help with your refund right away."])
|
|
185
|
+
output = provider.chat([{"role": "user", "content": "refund please"}])
|
|
186
|
+
assert_llm(output).mentions("refund").tone("helpful")
|
|
187
|
+
|
|
188
|
+
def test_conversation(conversation):
|
|
189
|
+
conv = conversation(responses=["Hello!", "Sure, I can help with that."])
|
|
190
|
+
conv.say("Hi")
|
|
191
|
+
response = conv.say("I need help")
|
|
192
|
+
assert "help" in response.text.lower()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Providers
|
|
198
|
+
|
|
199
|
+
Built-in adapters for all major LLM providers:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from llm_assert.providers.openai_adapter import OpenAIProvider
|
|
203
|
+
from llm_assert.providers.anthropic_adapter import AnthropicProvider
|
|
204
|
+
from llm_assert.providers.ollama_adapter import OllamaProvider
|
|
205
|
+
from llm_assert import MockProvider # for tests, no API calls
|
|
206
|
+
|
|
207
|
+
# All providers have the same interface
|
|
208
|
+
provider = OpenAIProvider(model="gpt-4o-mini")
|
|
209
|
+
provider = AnthropicProvider(model="claude-haiku-4-5-20251001")
|
|
210
|
+
provider = OllamaProvider(model="llama3")
|
|
211
|
+
|
|
212
|
+
output = provider.chat([{"role": "user", "content": "Hello"}])
|
|
213
|
+
text, tool_calls = provider.chat_with_tools(messages, tools=my_tools)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Bring your own provider by subclassing `LLMProvider`:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from llm_assert.providers.base import LLMProvider
|
|
220
|
+
|
|
221
|
+
class MyProvider(LLMProvider):
|
|
222
|
+
def chat(self, messages, **kwargs):
|
|
223
|
+
...
|
|
224
|
+
def chat_with_tools(self, messages, tools, **kwargs):
|
|
225
|
+
...
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## How it works
|
|
231
|
+
|
|
232
|
+
llm-behave uses [`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) — an 80MB sentence-transformer model that runs fully offline on CPU.
|
|
233
|
+
|
|
234
|
+
- **`mentions()` / `not_mentions()`** — splits text into sentences, computes max cosine similarity between any sentence and your concept
|
|
235
|
+
- **`tone()`** — batch-encodes input text against example sentences for each tone, returns max similarity
|
|
236
|
+
- **`intent()`** — semantic similarity between output and your intent description
|
|
237
|
+
- **`contradicts_turn()`** — NLI (Natural Language Inference) model detects logical contradictions
|
|
238
|
+
|
|
239
|
+
Models load **lazily** on first use and are cached for the rest of the test session. Import time stays fast.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Performance
|
|
244
|
+
|
|
245
|
+
Measured after model warmup (model loads once per test session):
|
|
246
|
+
|
|
247
|
+
| Assertion | Time |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `mentions()` | ~32ms |
|
|
250
|
+
| `tone()` | ~40ms |
|
|
251
|
+
| `intent()` | ~32ms |
|
|
252
|
+
| 4-assertion chain | ~350ms |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## pytest markers
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
import pytest
|
|
260
|
+
|
|
261
|
+
@pytest.mark.behavioral
|
|
262
|
+
def test_refund_flow():
|
|
263
|
+
...
|
|
264
|
+
|
|
265
|
+
@pytest.mark.drift
|
|
266
|
+
def test_no_regression():
|
|
267
|
+
...
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Run only behavioral tests:
|
|
271
|
+
```bash
|
|
272
|
+
pytest -m behavioral
|
|
273
|
+
pytest -m drift
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Full install options
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
pip install llm-behave # core only
|
|
282
|
+
pip install llm-behave[semantic] # + sentence-transformers + torch
|
|
283
|
+
pip install llm-behave[openai] # + openai SDK
|
|
284
|
+
pip install llm-behave[anthropic] # + anthropic SDK
|
|
285
|
+
pip install llm-behave[ollama] # + ollama SDK
|
|
286
|
+
pip install llm-behave[all] # everything
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Requirements
|
|
292
|
+
|
|
293
|
+
- Python 3.10+
|
|
294
|
+
- pytest 7.0+
|
|
295
|
+
- For semantic assertions: `pip install llm-behave[semantic]`
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
MIT — free to use in personal and commercial projects.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Author
|
|
306
|
+
|
|
307
|
+
Built by [Swanand Potnis](https://github.com/Swanand33) — Pune, India.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# llm-behave
|
|
2
|
+
|
|
3
|
+
**Behavioral testing for LLM applications. A pytest plugin.**
|
|
4
|
+
|
|
5
|
+
No LLM judge. No API cost. Offline models. Works with any provider.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
def test_support_bot():
|
|
9
|
+
output = my_support_bot("I want a refund for order 1234")
|
|
10
|
+
|
|
11
|
+
assert_behavior(output) \
|
|
12
|
+
.mentions("refund policy") \
|
|
13
|
+
.tone("empathetic") \
|
|
14
|
+
.not_mentions("competitor")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why llm-behave?
|
|
20
|
+
|
|
21
|
+
Most LLM testing tools either:
|
|
22
|
+
- Use another LLM to judge the output (expensive, slow, circular)
|
|
23
|
+
- Only do exact string matching (misses semantic meaning)
|
|
24
|
+
- Don't support multi-turn conversations at all
|
|
25
|
+
|
|
26
|
+
**llm-behave** uses small offline transformer models (80MB, runs on CPU) to understand meaning — no API calls, no cost, no internet required during tests.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Core (tool call assertions, structure tests — no ML deps)
|
|
34
|
+
pip install llm-behave
|
|
35
|
+
|
|
36
|
+
# Full (semantic assertions: mentions, tone, intent, drift)
|
|
37
|
+
pip install llm-behave[semantic]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Features at a glance
|
|
43
|
+
|
|
44
|
+
| Feature | What it does |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `mentions()` | Semantic similarity — "money back" matches "refund" |
|
|
47
|
+
| `not_mentions()` | Assert a topic is NOT brought up |
|
|
48
|
+
| `tone()` | Detect empathetic / professional / rude / helpful etc. |
|
|
49
|
+
| `intent()` | Does the response intend to help? refuse? apologize? |
|
|
50
|
+
| `calls_tool()` | Assert which tool the LLM called |
|
|
51
|
+
| `ConversationTest` | Multi-turn testing with memory, contradiction detection |
|
|
52
|
+
| `DriftTest` | Save baseline behavior, detect regressions in CI |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
### Basic assertions
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from llm_assert import assert_behavior
|
|
62
|
+
|
|
63
|
+
output = my_llm("I want a refund")
|
|
64
|
+
|
|
65
|
+
# Semantic match — not exact string
|
|
66
|
+
assert_behavior(output).mentions("refund policy")
|
|
67
|
+
|
|
68
|
+
# Tone detection
|
|
69
|
+
assert_behavior(output).tone("empathetic")
|
|
70
|
+
assert_behavior(output).tone("professional", threshold=0.6)
|
|
71
|
+
|
|
72
|
+
# Intent
|
|
73
|
+
assert_behavior(output).intent("offering to help the customer")
|
|
74
|
+
|
|
75
|
+
# Negative assertions
|
|
76
|
+
assert_behavior(output).not_mentions("competitor")
|
|
77
|
+
|
|
78
|
+
# Fluent chaining
|
|
79
|
+
assert_behavior(output) \
|
|
80
|
+
.mentions("refund") \
|
|
81
|
+
.tone("empathetic") \
|
|
82
|
+
.not_mentions("competitor")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Tool call assertions
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
text, tool_calls = my_llm.chat_with_tools(messages, tools=my_tools)
|
|
89
|
+
|
|
90
|
+
assert_behavior(text, tool_calls) \
|
|
91
|
+
.calls_tool("lookup_order") \
|
|
92
|
+
.mentions("order")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Multi-turn conversation testing
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from llm_assert import ConversationTest, MockProvider
|
|
99
|
+
|
|
100
|
+
conv = ConversationTest(agent=my_agent)
|
|
101
|
+
|
|
102
|
+
conv.say("Hi, my name is Swanand")
|
|
103
|
+
conv.say("I placed order #5678 last week")
|
|
104
|
+
response = conv.say("When will it arrive?")
|
|
105
|
+
|
|
106
|
+
# Does it remember context from earlier turns?
|
|
107
|
+
assert response.recalls("order")
|
|
108
|
+
assert response.recalls("Swanand")
|
|
109
|
+
|
|
110
|
+
# Is tone consistent across the whole conversation?
|
|
111
|
+
assert response.consistent_tone_across_turns(threshold=0.6)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Drift detection (for CI)
|
|
115
|
+
|
|
116
|
+
Catch silent regressions when you update your model or prompts.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from llm_assert import DriftTest
|
|
120
|
+
|
|
121
|
+
# First run: save baseline
|
|
122
|
+
@DriftTest.baseline(save_as="support_refund_flow")
|
|
123
|
+
def get_baseline_output():
|
|
124
|
+
return my_llm("I need a refund")
|
|
125
|
+
|
|
126
|
+
# Every CI run: compare against baseline
|
|
127
|
+
result = DriftTest.compare("support_refund_flow", current_output)
|
|
128
|
+
assert result.passed, f"Behavior drift detected: {result.details}"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### pytest fixtures (auto-registered)
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# These fixtures are available in any test file automatically
|
|
135
|
+
|
|
136
|
+
def test_with_mock(mock_provider, assert_llm):
|
|
137
|
+
provider = mock_provider(responses=["I'll help with your refund right away."])
|
|
138
|
+
output = provider.chat([{"role": "user", "content": "refund please"}])
|
|
139
|
+
assert_llm(output).mentions("refund").tone("helpful")
|
|
140
|
+
|
|
141
|
+
def test_conversation(conversation):
|
|
142
|
+
conv = conversation(responses=["Hello!", "Sure, I can help with that."])
|
|
143
|
+
conv.say("Hi")
|
|
144
|
+
response = conv.say("I need help")
|
|
145
|
+
assert "help" in response.text.lower()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Providers
|
|
151
|
+
|
|
152
|
+
Built-in adapters for all major LLM providers:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from llm_assert.providers.openai_adapter import OpenAIProvider
|
|
156
|
+
from llm_assert.providers.anthropic_adapter import AnthropicProvider
|
|
157
|
+
from llm_assert.providers.ollama_adapter import OllamaProvider
|
|
158
|
+
from llm_assert import MockProvider # for tests, no API calls
|
|
159
|
+
|
|
160
|
+
# All providers have the same interface
|
|
161
|
+
provider = OpenAIProvider(model="gpt-4o-mini")
|
|
162
|
+
provider = AnthropicProvider(model="claude-haiku-4-5-20251001")
|
|
163
|
+
provider = OllamaProvider(model="llama3")
|
|
164
|
+
|
|
165
|
+
output = provider.chat([{"role": "user", "content": "Hello"}])
|
|
166
|
+
text, tool_calls = provider.chat_with_tools(messages, tools=my_tools)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Bring your own provider by subclassing `LLMProvider`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from llm_assert.providers.base import LLMProvider
|
|
173
|
+
|
|
174
|
+
class MyProvider(LLMProvider):
|
|
175
|
+
def chat(self, messages, **kwargs):
|
|
176
|
+
...
|
|
177
|
+
def chat_with_tools(self, messages, tools, **kwargs):
|
|
178
|
+
...
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## How it works
|
|
184
|
+
|
|
185
|
+
llm-behave uses [`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) — an 80MB sentence-transformer model that runs fully offline on CPU.
|
|
186
|
+
|
|
187
|
+
- **`mentions()` / `not_mentions()`** — splits text into sentences, computes max cosine similarity between any sentence and your concept
|
|
188
|
+
- **`tone()`** — batch-encodes input text against example sentences for each tone, returns max similarity
|
|
189
|
+
- **`intent()`** — semantic similarity between output and your intent description
|
|
190
|
+
- **`contradicts_turn()`** — NLI (Natural Language Inference) model detects logical contradictions
|
|
191
|
+
|
|
192
|
+
Models load **lazily** on first use and are cached for the rest of the test session. Import time stays fast.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Performance
|
|
197
|
+
|
|
198
|
+
Measured after model warmup (model loads once per test session):
|
|
199
|
+
|
|
200
|
+
| Assertion | Time |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `mentions()` | ~32ms |
|
|
203
|
+
| `tone()` | ~40ms |
|
|
204
|
+
| `intent()` | ~32ms |
|
|
205
|
+
| 4-assertion chain | ~350ms |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## pytest markers
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
import pytest
|
|
213
|
+
|
|
214
|
+
@pytest.mark.behavioral
|
|
215
|
+
def test_refund_flow():
|
|
216
|
+
...
|
|
217
|
+
|
|
218
|
+
@pytest.mark.drift
|
|
219
|
+
def test_no_regression():
|
|
220
|
+
...
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Run only behavioral tests:
|
|
224
|
+
```bash
|
|
225
|
+
pytest -m behavioral
|
|
226
|
+
pytest -m drift
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Full install options
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
pip install llm-behave # core only
|
|
235
|
+
pip install llm-behave[semantic] # + sentence-transformers + torch
|
|
236
|
+
pip install llm-behave[openai] # + openai SDK
|
|
237
|
+
pip install llm-behave[anthropic] # + anthropic SDK
|
|
238
|
+
pip install llm-behave[ollama] # + ollama SDK
|
|
239
|
+
pip install llm-behave[all] # everything
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Requirements
|
|
245
|
+
|
|
246
|
+
- Python 3.10+
|
|
247
|
+
- pytest 7.0+
|
|
248
|
+
- For semantic assertions: `pip install llm-behave[semantic]`
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT — free to use in personal and commercial projects.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Author
|
|
259
|
+
|
|
260
|
+
Built by [Swanand Potnis](https://github.com/Swanand33) — Pune, India.
|