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.
@@ -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
+ }