driftshield-mini 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.
- driftshield_mini-0.1.0/LICENSE +21 -0
- driftshield_mini-0.1.0/PKG-INFO +250 -0
- driftshield_mini-0.1.0/README.md +215 -0
- driftshield_mini-0.1.0/driftshield/__init__.py +27 -0
- driftshield_mini-0.1.0/driftshield/alerts/__init__.py +169 -0
- driftshield_mini-0.1.0/driftshield/baseline/__init__.py +107 -0
- driftshield_mini-0.1.0/driftshield/cli.py +191 -0
- driftshield_mini-0.1.0/driftshield/crewai.py +95 -0
- driftshield_mini-0.1.0/driftshield/detectors/__init__.py +13 -0
- driftshield_mini-0.1.0/driftshield/detectors/action_loop.py +142 -0
- driftshield_mini-0.1.0/driftshield/detectors/base.py +32 -0
- driftshield_mini-0.1.0/driftshield/detectors/goal_drift.py +158 -0
- driftshield_mini-0.1.0/driftshield/detectors/resource_spike.py +202 -0
- driftshield_mini-0.1.0/driftshield/models.py +164 -0
- driftshield_mini-0.1.0/driftshield/monitor.py +253 -0
- driftshield_mini-0.1.0/driftshield/storage/__init__.py +284 -0
- driftshield_mini-0.1.0/driftshield_mini.egg-info/PKG-INFO +250 -0
- driftshield_mini-0.1.0/driftshield_mini.egg-info/SOURCES.txt +23 -0
- driftshield_mini-0.1.0/driftshield_mini.egg-info/dependency_links.txt +1 -0
- driftshield_mini-0.1.0/driftshield_mini.egg-info/entry_points.txt +2 -0
- driftshield_mini-0.1.0/driftshield_mini.egg-info/requires.txt +18 -0
- driftshield_mini-0.1.0/driftshield_mini.egg-info/top_level.txt +1 -0
- driftshield_mini-0.1.0/pyproject.toml +51 -0
- driftshield_mini-0.1.0/setup.cfg +4 -0
- driftshield_mini-0.1.0/tests/test_standalone.py +249 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DriftShield
|
|
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,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: driftshield-mini
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Real-time behavioural drift detection for agentic AI systems
|
|
5
|
+
Author: DriftShield
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/driftshield/driftshield-mini
|
|
8
|
+
Project-URL: Issues, https://github.com/driftshield/driftshield-mini/issues
|
|
9
|
+
Keywords: ai,agents,monitoring,drift-detection,langchain,crewai
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: sentence-transformers>=2.2.0
|
|
20
|
+
Requires-Dist: scikit-learn>=1.3.0
|
|
21
|
+
Requires-Dist: numpy>=1.24.0
|
|
22
|
+
Requires-Dist: click>=8.1.0
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Provides-Extra: langchain
|
|
26
|
+
Requires-Dist: langchain>=0.1.0; extra == "langchain"
|
|
27
|
+
Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
|
|
28
|
+
Provides-Extra: crewai
|
|
29
|
+
Requires-Dist: crewai>=0.1.0; extra == "crewai"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# DriftShield
|
|
37
|
+
|
|
38
|
+
Your LangChain agent just called the same API 47 times. Your CrewAI crew burned £200 in tokens overnight. Your research agent started writing marketing copy instead of financial summaries.
|
|
39
|
+
|
|
40
|
+
You didn't find out until morning.
|
|
41
|
+
|
|
42
|
+
**DriftShield catches this stuff in real-time.** It wraps your existing agent, watches what it does, and pings you on Slack or Discord the moment something goes sideways. No dashboard. No cloud. No account to create. Just a Python library that runs alongside your agent.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## What it actually does
|
|
47
|
+
|
|
48
|
+
DriftShield monitors three things:
|
|
49
|
+
|
|
50
|
+
**Loop detection**: Is your agent calling the same tool over and over? Or stuck in a cycle like `search → format → search → format`? DriftShield spots the pattern and alerts you before it eats your budget.
|
|
51
|
+
|
|
52
|
+
**Goal drift**: Is your agent still doing what you asked it to? DriftShield uses local embeddings (runs on your CPU, no API calls) to measure how far the agent's output has drifted from its original objective.
|
|
53
|
+
|
|
54
|
+
**Resource spikes**: Is this run burning way more tokens or taking way longer than usual? DriftShield learns what "normal" looks like for your agent, then flags when things go abnormal.
|
|
55
|
+
|
|
56
|
+
Everything stays on your machine. Traces go to a local SQLite file. Embeddings run on your CPU. The only thing that leaves your machine is the alert you choose to send to Slack/Discord.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Get started
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
pip install driftshield
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### LangChain
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from driftshield import DriftMonitor
|
|
70
|
+
|
|
71
|
+
monitor = DriftMonitor(
|
|
72
|
+
agent_id="logistics-v2",
|
|
73
|
+
alert_webhook="https://hooks.slack.com/...",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
agent = monitor.wrap(existing_agent)
|
|
77
|
+
result = agent.invoke({"input": "optimise route for order #4821"})
|
|
78
|
+
# DriftShield is now watching. That's it.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### CrewAI
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from driftshield.crewai import DriftCrew
|
|
85
|
+
|
|
86
|
+
crew = DriftCrew(
|
|
87
|
+
crew=existing_crew,
|
|
88
|
+
agent_id="research-team-v1",
|
|
89
|
+
alert_webhook="https://discord.com/api/webhooks/...",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
result = crew.kickoff()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Works with any LLM
|
|
96
|
+
|
|
97
|
+
OpenAI, Anthropic, Groq, Ollama, local models doesn't matter. DriftShield only sees the traces (tool calls, token counts, outputs), not the model internals. Swap providers whenever you want.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## How calibration works
|
|
102
|
+
|
|
103
|
+
For the first 30 runs (configurable), DriftShield quietly observes your agent and builds a baseline average tokens per run, typical tool sequences, normal execution time. No alerts during this phase.
|
|
104
|
+
|
|
105
|
+
After that, it knows what "normal" looks like and starts flagging deviations. You can inspect the baseline anytime:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
driftshield baseline my-agent
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> **Tip:** If 30 runs feels like a lot, you can lower `calibration_runs` or use a preset template. DriftShield still catches obvious problems (like 50 identical tool calls) even without a baseline, using absolute safety limits.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## What an alert looks like
|
|
116
|
+
|
|
117
|
+
When drift hits your Slack/Discord, you get:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"agent_id": "logistics-v2",
|
|
122
|
+
"detector": "action_loop",
|
|
123
|
+
"severity": "HIGH",
|
|
124
|
+
"message": "Action loop: search_inventory called 6x in 45s",
|
|
125
|
+
"suggested_action": "Check search_inventory input/output for stale data or error loops",
|
|
126
|
+
"context": {
|
|
127
|
+
"tool_name": "search_inventory",
|
|
128
|
+
"repeat_count": 6,
|
|
129
|
+
"recent_actions": ["search_inventory", "search_inventory", "search_inventory", "..."]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Not just "something's wrong" — it tells you what happened, which detector caught it, and what to check first.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## CLI
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# What went wrong in the last 24 hours?
|
|
142
|
+
driftshield alerts --last 24h
|
|
143
|
+
|
|
144
|
+
# Show me exactly what my agent did on its last run
|
|
145
|
+
driftshield traces logistics-v2 --run latest
|
|
146
|
+
|
|
147
|
+
# What does "normal" look like for this agent?
|
|
148
|
+
driftshield baseline logistics-v2
|
|
149
|
+
|
|
150
|
+
# List recent runs
|
|
151
|
+
driftshield runs logistics-v2
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
Everything's tuneable. Defaults are sensible, but you can adjust:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
monitor = DriftMonitor(
|
|
162
|
+
agent_id="my-agent",
|
|
163
|
+
alert_webhook="https://hooks.slack.com/...",
|
|
164
|
+
goal_description="Summarise financial reports",
|
|
165
|
+
calibration_runs=30, # runs before baseline kicks in
|
|
166
|
+
loop_window=20, # how many recent actions to check
|
|
167
|
+
loop_max_repeats=4, # repeated calls before flagging
|
|
168
|
+
similarity_threshold=0.5, # goal drift sensitivity (lower = stricter)
|
|
169
|
+
spike_multiplier=2.5, # how many std devs = a spike
|
|
170
|
+
min_alert_severity="MED", # ignore LOW severity events
|
|
171
|
+
alert_cooldown=60.0, # don't spam the same alert
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Custom reactions
|
|
178
|
+
|
|
179
|
+
DriftShield alerts you by default, but you can also react programmatically:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
def handle_drift(event):
|
|
183
|
+
if event.severity.value == "CRITICAL":
|
|
184
|
+
agent.stop() # kill the run
|
|
185
|
+
page_oncall() # wake someone up
|
|
186
|
+
|
|
187
|
+
monitor.on_drift(handle_drift)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## What this isn't
|
|
193
|
+
|
|
194
|
+
I want to be upfront about scope. DriftShield is **v0.1**, built by one person.
|
|
195
|
+
|
|
196
|
+
- **Not a full observability platform.** No web dashboard, no hosted backend, no team features. If you need that, look at LangSmith, Langfuse, or Arize.
|
|
197
|
+
- **Not a guardrail system.** It detects drift after the fact and alerts you. It doesn't block actions before they happen (that's on the roadmap).
|
|
198
|
+
- **Not production-hardened yet.** It works, it's tested, but it hasn't been battle-tested by thousands of users. Expect rough edges.
|
|
199
|
+
|
|
200
|
+
What it IS: the smallest, simplest tool that does one thing well — tells you when your agent is going off the rails, fast, with zero setup overhead.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Roadmap
|
|
205
|
+
|
|
206
|
+
- **v0.2** — Auto-correction hooks (retry, context trim, kill run). Preset baseline templates so you get value from run 1.
|
|
207
|
+
- **v0.3** — Better multi-agent support. Predictive drift (catch it before it happens).
|
|
208
|
+
- **v1.0** — Dashboard, team features, historical analytics. But only if people actually want it.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Built with
|
|
213
|
+
|
|
214
|
+
- Python 3.10+
|
|
215
|
+
- SQLite (zero config)
|
|
216
|
+
- sentence-transformers (local CPU embeddings)
|
|
217
|
+
- scikit-learn (basic stats)
|
|
218
|
+
- httpx (webhooks)
|
|
219
|
+
- click + rich (CLI)
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Contributing
|
|
224
|
+
|
|
225
|
+
This is early. If you're running agents in production and hit a case DriftShield missed (or flagged incorrectly), please open an issue. Your real-world edge cases are the most valuable thing you can give this project right now.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
git clone https://github.com/YOUR_USERNAME/driftshield.git
|
|
229
|
+
cd driftshield
|
|
230
|
+
python -m venv .venv
|
|
231
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
232
|
+
pip install -e ".[dev]"
|
|
233
|
+
python -m pytest tests/ -v
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Why I built this
|
|
239
|
+
|
|
240
|
+
I kept reading the same story: dev builds agent, agent works great in testing, agent goes haywire in production at 2am, dev wakes up to a hefty API bill and a Slack full of confused users. The big observability platforms exist but they're heavy on dashboards, accounts, pricing tiers, cloud dependencies. Most solo devs and small teams just want to know when their agent is broken. That's it.
|
|
241
|
+
|
|
242
|
+
So I built the smallest thing that solves that problem.
|
|
243
|
+
|
|
244
|
+
If you try it and it helps (or doesn't), I genuinely want to hear about it.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT - do whatever you want with it.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# DriftShield
|
|
2
|
+
|
|
3
|
+
Your LangChain agent just called the same API 47 times. Your CrewAI crew burned £200 in tokens overnight. Your research agent started writing marketing copy instead of financial summaries.
|
|
4
|
+
|
|
5
|
+
You didn't find out until morning.
|
|
6
|
+
|
|
7
|
+
**DriftShield catches this stuff in real-time.** It wraps your existing agent, watches what it does, and pings you on Slack or Discord the moment something goes sideways. No dashboard. No cloud. No account to create. Just a Python library that runs alongside your agent.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What it actually does
|
|
12
|
+
|
|
13
|
+
DriftShield monitors three things:
|
|
14
|
+
|
|
15
|
+
**Loop detection**: Is your agent calling the same tool over and over? Or stuck in a cycle like `search → format → search → format`? DriftShield spots the pattern and alerts you before it eats your budget.
|
|
16
|
+
|
|
17
|
+
**Goal drift**: Is your agent still doing what you asked it to? DriftShield uses local embeddings (runs on your CPU, no API calls) to measure how far the agent's output has drifted from its original objective.
|
|
18
|
+
|
|
19
|
+
**Resource spikes**: Is this run burning way more tokens or taking way longer than usual? DriftShield learns what "normal" looks like for your agent, then flags when things go abnormal.
|
|
20
|
+
|
|
21
|
+
Everything stays on your machine. Traces go to a local SQLite file. Embeddings run on your CPU. The only thing that leaves your machine is the alert you choose to send to Slack/Discord.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Get started
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
pip install driftshield
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### LangChain
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from driftshield import DriftMonitor
|
|
35
|
+
|
|
36
|
+
monitor = DriftMonitor(
|
|
37
|
+
agent_id="logistics-v2",
|
|
38
|
+
alert_webhook="https://hooks.slack.com/...",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
agent = monitor.wrap(existing_agent)
|
|
42
|
+
result = agent.invoke({"input": "optimise route for order #4821"})
|
|
43
|
+
# DriftShield is now watching. That's it.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### CrewAI
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from driftshield.crewai import DriftCrew
|
|
50
|
+
|
|
51
|
+
crew = DriftCrew(
|
|
52
|
+
crew=existing_crew,
|
|
53
|
+
agent_id="research-team-v1",
|
|
54
|
+
alert_webhook="https://discord.com/api/webhooks/...",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
result = crew.kickoff()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Works with any LLM
|
|
61
|
+
|
|
62
|
+
OpenAI, Anthropic, Groq, Ollama, local models doesn't matter. DriftShield only sees the traces (tool calls, token counts, outputs), not the model internals. Swap providers whenever you want.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## How calibration works
|
|
67
|
+
|
|
68
|
+
For the first 30 runs (configurable), DriftShield quietly observes your agent and builds a baseline average tokens per run, typical tool sequences, normal execution time. No alerts during this phase.
|
|
69
|
+
|
|
70
|
+
After that, it knows what "normal" looks like and starts flagging deviations. You can inspect the baseline anytime:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
driftshield baseline my-agent
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> **Tip:** If 30 runs feels like a lot, you can lower `calibration_runs` or use a preset template. DriftShield still catches obvious problems (like 50 identical tool calls) even without a baseline, using absolute safety limits.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## What an alert looks like
|
|
81
|
+
|
|
82
|
+
When drift hits your Slack/Discord, you get:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"agent_id": "logistics-v2",
|
|
87
|
+
"detector": "action_loop",
|
|
88
|
+
"severity": "HIGH",
|
|
89
|
+
"message": "Action loop: search_inventory called 6x in 45s",
|
|
90
|
+
"suggested_action": "Check search_inventory input/output for stale data or error loops",
|
|
91
|
+
"context": {
|
|
92
|
+
"tool_name": "search_inventory",
|
|
93
|
+
"repeat_count": 6,
|
|
94
|
+
"recent_actions": ["search_inventory", "search_inventory", "search_inventory", "..."]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Not just "something's wrong" — it tells you what happened, which detector caught it, and what to check first.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## CLI
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# What went wrong in the last 24 hours?
|
|
107
|
+
driftshield alerts --last 24h
|
|
108
|
+
|
|
109
|
+
# Show me exactly what my agent did on its last run
|
|
110
|
+
driftshield traces logistics-v2 --run latest
|
|
111
|
+
|
|
112
|
+
# What does "normal" look like for this agent?
|
|
113
|
+
driftshield baseline logistics-v2
|
|
114
|
+
|
|
115
|
+
# List recent runs
|
|
116
|
+
driftshield runs logistics-v2
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
Everything's tuneable. Defaults are sensible, but you can adjust:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
monitor = DriftMonitor(
|
|
127
|
+
agent_id="my-agent",
|
|
128
|
+
alert_webhook="https://hooks.slack.com/...",
|
|
129
|
+
goal_description="Summarise financial reports",
|
|
130
|
+
calibration_runs=30, # runs before baseline kicks in
|
|
131
|
+
loop_window=20, # how many recent actions to check
|
|
132
|
+
loop_max_repeats=4, # repeated calls before flagging
|
|
133
|
+
similarity_threshold=0.5, # goal drift sensitivity (lower = stricter)
|
|
134
|
+
spike_multiplier=2.5, # how many std devs = a spike
|
|
135
|
+
min_alert_severity="MED", # ignore LOW severity events
|
|
136
|
+
alert_cooldown=60.0, # don't spam the same alert
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Custom reactions
|
|
143
|
+
|
|
144
|
+
DriftShield alerts you by default, but you can also react programmatically:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
def handle_drift(event):
|
|
148
|
+
if event.severity.value == "CRITICAL":
|
|
149
|
+
agent.stop() # kill the run
|
|
150
|
+
page_oncall() # wake someone up
|
|
151
|
+
|
|
152
|
+
monitor.on_drift(handle_drift)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## What this isn't
|
|
158
|
+
|
|
159
|
+
I want to be upfront about scope. DriftShield is **v0.1**, built by one person.
|
|
160
|
+
|
|
161
|
+
- **Not a full observability platform.** No web dashboard, no hosted backend, no team features. If you need that, look at LangSmith, Langfuse, or Arize.
|
|
162
|
+
- **Not a guardrail system.** It detects drift after the fact and alerts you. It doesn't block actions before they happen (that's on the roadmap).
|
|
163
|
+
- **Not production-hardened yet.** It works, it's tested, but it hasn't been battle-tested by thousands of users. Expect rough edges.
|
|
164
|
+
|
|
165
|
+
What it IS: the smallest, simplest tool that does one thing well — tells you when your agent is going off the rails, fast, with zero setup overhead.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Roadmap
|
|
170
|
+
|
|
171
|
+
- **v0.2** — Auto-correction hooks (retry, context trim, kill run). Preset baseline templates so you get value from run 1.
|
|
172
|
+
- **v0.3** — Better multi-agent support. Predictive drift (catch it before it happens).
|
|
173
|
+
- **v1.0** — Dashboard, team features, historical analytics. But only if people actually want it.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Built with
|
|
178
|
+
|
|
179
|
+
- Python 3.10+
|
|
180
|
+
- SQLite (zero config)
|
|
181
|
+
- sentence-transformers (local CPU embeddings)
|
|
182
|
+
- scikit-learn (basic stats)
|
|
183
|
+
- httpx (webhooks)
|
|
184
|
+
- click + rich (CLI)
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Contributing
|
|
189
|
+
|
|
190
|
+
This is early. If you're running agents in production and hit a case DriftShield missed (or flagged incorrectly), please open an issue. Your real-world edge cases are the most valuable thing you can give this project right now.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
git clone https://github.com/YOUR_USERNAME/driftshield.git
|
|
194
|
+
cd driftshield
|
|
195
|
+
python -m venv .venv
|
|
196
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
197
|
+
pip install -e ".[dev]"
|
|
198
|
+
python -m pytest tests/ -v
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Why I built this
|
|
204
|
+
|
|
205
|
+
I kept reading the same story: dev builds agent, agent works great in testing, agent goes haywire in production at 2am, dev wakes up to a hefty API bill and a Slack full of confused users. The big observability platforms exist but they're heavy on dashboards, accounts, pricing tiers, cloud dependencies. Most solo devs and small teams just want to know when their agent is broken. That's it.
|
|
206
|
+
|
|
207
|
+
So I built the smallest thing that solves that problem.
|
|
208
|
+
|
|
209
|
+
If you try it and it helps (or doesn't), I genuinely want to hear about it.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT - do whatever you want with it.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DriftShield — Real-time behavioural drift detection for agentic AI systems.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from driftshield import DriftMonitor
|
|
6
|
+
|
|
7
|
+
monitor = DriftMonitor(
|
|
8
|
+
agent_id="my-agent",
|
|
9
|
+
alert_webhook="https://hooks.slack.com/...",
|
|
10
|
+
)
|
|
11
|
+
agent = monitor.wrap(existing_agent)
|
|
12
|
+
result = agent.invoke({"input": "do the thing"})
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from driftshield.models import BaselineStats, DetectorType, DriftEvent, Severity, TraceEvent
|
|
16
|
+
from driftshield.monitor import DriftMonitor
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DriftMonitor",
|
|
22
|
+
"TraceEvent",
|
|
23
|
+
"DriftEvent",
|
|
24
|
+
"BaselineStats",
|
|
25
|
+
"DetectorType",
|
|
26
|
+
"Severity",
|
|
27
|
+
]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Alert dispatchers — Slack, Discord, and generic webhook support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from driftshield.models import DriftEvent
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Severity → color mapping
|
|
16
|
+
SEVERITY_COLORS = {
|
|
17
|
+
"LOW": "#36a64f", # green
|
|
18
|
+
"MED": "#daa520", # amber
|
|
19
|
+
"HIGH": "#ff6600", # orange
|
|
20
|
+
"CRITICAL": "#ff0000", # red
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
SEVERITY_EMOJI = {
|
|
24
|
+
"LOW": "🟢",
|
|
25
|
+
"MED": "🟡",
|
|
26
|
+
"HIGH": "🟠",
|
|
27
|
+
"CRITICAL": "🔴",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AlertDispatcher:
|
|
32
|
+
"""Dispatches drift alerts via webhook (Slack, Discord, or generic)."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
webhook_url: str | None = None,
|
|
37
|
+
min_severity: str = "MEDIUM",
|
|
38
|
+
cooldown_seconds: float = 60.0,
|
|
39
|
+
):
|
|
40
|
+
self.webhook_url = webhook_url
|
|
41
|
+
self.min_severity = min_severity
|
|
42
|
+
self.cooldown_seconds = cooldown_seconds
|
|
43
|
+
self._last_alert: dict[str, float] = {} # agent_id+detector → timestamp
|
|
44
|
+
|
|
45
|
+
def should_alert(self, event: DriftEvent) -> bool:
|
|
46
|
+
"""Check if alert should fire (severity + cooldown)."""
|
|
47
|
+
if not self.webhook_url:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
severity_order = ["LOW", "MED", "HIGH", "CRITICAL"]
|
|
51
|
+
min_idx = severity_order.index(self.min_severity) if self.min_severity in severity_order else 1
|
|
52
|
+
event_idx = severity_order.index(event.severity.value) if event.severity.value in severity_order else 0
|
|
53
|
+
|
|
54
|
+
if event_idx < min_idx:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
# Cooldown: don't spam the same detector for the same agent
|
|
58
|
+
key = f"{event.agent_id}:{event.detector.value}"
|
|
59
|
+
now = time.time()
|
|
60
|
+
if key in self._last_alert and (now - self._last_alert[key]) < self.cooldown_seconds:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
self._last_alert[key] = now
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
async def send_async(self, event: DriftEvent) -> bool:
|
|
67
|
+
"""Send alert via async HTTP (preferred)."""
|
|
68
|
+
if not self.should_alert(event):
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
import httpx
|
|
73
|
+
|
|
74
|
+
payload = self._build_payload(event)
|
|
75
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
76
|
+
resp = await client.post(self.webhook_url, json=payload)
|
|
77
|
+
resp.raise_for_status()
|
|
78
|
+
logger.info(f"Alert sent for {event.agent_id}: {event.message}")
|
|
79
|
+
return True
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning(f"Failed to send alert: {e}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def send_sync(self, event: DriftEvent) -> bool:
|
|
85
|
+
"""Send alert via sync HTTP (fallback)."""
|
|
86
|
+
if not self.should_alert(event):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
import httpx
|
|
91
|
+
|
|
92
|
+
payload = self._build_payload(event)
|
|
93
|
+
with httpx.Client(timeout=10.0) as client:
|
|
94
|
+
resp = client.post(self.webhook_url, json=payload)
|
|
95
|
+
resp.raise_for_status()
|
|
96
|
+
logger.info(f"Alert sent for {event.agent_id}: {event.message}")
|
|
97
|
+
return True
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.warning(f"Failed to send alert: {e}")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def _build_payload(self, event: DriftEvent) -> dict[str, Any]:
|
|
103
|
+
"""Build webhook payload — auto-detects Slack vs Discord vs generic."""
|
|
104
|
+
if not self.webhook_url:
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
if "hooks.slack.com" in self.webhook_url:
|
|
108
|
+
return self._slack_payload(event)
|
|
109
|
+
elif "discord.com" in self.webhook_url:
|
|
110
|
+
return self._discord_payload(event)
|
|
111
|
+
else:
|
|
112
|
+
return self._generic_payload(event)
|
|
113
|
+
|
|
114
|
+
def _slack_payload(self, event: DriftEvent) -> dict[str, Any]:
|
|
115
|
+
emoji = SEVERITY_EMOJI.get(event.severity.value, "⚠️")
|
|
116
|
+
color = SEVERITY_COLORS.get(event.severity.value, "#daa520")
|
|
117
|
+
ts = datetime.fromtimestamp(event.timestamp, tz=timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"attachments": [
|
|
121
|
+
{
|
|
122
|
+
"color": color,
|
|
123
|
+
"blocks": [
|
|
124
|
+
{
|
|
125
|
+
"type": "section",
|
|
126
|
+
"text": {
|
|
127
|
+
"type": "mrkdwn",
|
|
128
|
+
"text": (
|
|
129
|
+
f"{emoji} *[{event.severity.value}] DriftShield Alert*\n"
|
|
130
|
+
f"*Agent:* `{event.agent_id}`\n"
|
|
131
|
+
f"*Detector:* {event.detector.value}\n"
|
|
132
|
+
f"*Time:* {ts}\n\n"
|
|
133
|
+
f"{event.message}\n\n"
|
|
134
|
+
f"💡 *Suggested action:* {event.suggested_action}"
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def _discord_payload(self, event: DriftEvent) -> dict[str, Any]:
|
|
144
|
+
emoji = SEVERITY_EMOJI.get(event.severity.value, "⚠️")
|
|
145
|
+
color_hex = SEVERITY_COLORS.get(event.severity.value, "#daa520")
|
|
146
|
+
color_int = int(color_hex.lstrip("#"), 16)
|
|
147
|
+
ts = datetime.fromtimestamp(event.timestamp, tz=timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"embeds": [
|
|
151
|
+
{
|
|
152
|
+
"title": f"{emoji} [{event.severity.value}] DriftShield Alert",
|
|
153
|
+
"color": color_int,
|
|
154
|
+
"fields": [
|
|
155
|
+
{"name": "Agent", "value": f"`{event.agent_id}`", "inline": True},
|
|
156
|
+
{"name": "Detector", "value": event.detector.value, "inline": True},
|
|
157
|
+
{"name": "Time", "value": ts, "inline": True},
|
|
158
|
+
{"name": "Details", "value": event.message, "inline": False},
|
|
159
|
+
{"name": "💡 Suggested Action", "value": event.suggested_action, "inline": False},
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def _generic_payload(self, event: DriftEvent) -> dict[str, Any]:
|
|
166
|
+
return {
|
|
167
|
+
"source": "driftshield",
|
|
168
|
+
"event": event.to_dict(),
|
|
169
|
+
}
|