From 553ddad6511f10af84203b922b798f071850e62c Mon Sep 17 00:00:00 2001 From: Aditya Kumar Yadav Date: Wed, 1 Jul 2026 11:42:20 +0530 Subject: [PATCH 1/3] Feat #512: Slack Integration for Smart Alerts --- agentwatch/alerting/engine.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/agentwatch/alerting/engine.py b/agentwatch/alerting/engine.py index 2ed8ed41..2ff84299 100644 --- a/agentwatch/alerting/engine.py +++ b/agentwatch/alerting/engine.py @@ -58,6 +58,24 @@ async def alert_abuse(self, event: AbuseEvent) -> dict[str, bool]: ) return sent + + async def alert_smart_alert(self, alert_text: str, dashboard_link: str) -> dict[str, bool]: + """Surface a Smart Alert generated by the Human-in-the-Loop ambient integration.""" + payload = { + "slack": { + "text": "Smart Alert Intervention Required", + "blocks": [ + {"type": "section", "text": {"type": "mrkdwn", "text": f"*Smart Alert: Intervention Required*"}}, + {"type": "section", "text": {"type": "mrkdwn", "text": alert_text}}, + {"type": "section", "text": {"type": "mrkdwn", "text": f"<{dashboard_link}|Review on Dashboard>"}} + ] + } + } + sent = {"slack": False, "pagerduty": False} + if self._config.slack_webhook_url: + sent["slack"] = await self._post(self._config.slack_webhook_url, payload["slack"]) + return sent + def _build_abuse_payload(self, event: AbuseEvent) -> dict[str, dict[str, Any]]: summary = ( f"AgentWatch entitlement abuse: {event.subject} used on " From 9e9c14baefe072d165a6637f6d2d358b245f06a1 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Yadav Date: Mon, 6 Jul 2026 07:33:38 +0530 Subject: [PATCH 2/3] Fix #525 (PR 525): Remove extraneous f prefix in alert_smart_alert and format engine.py --- agentwatch/alerting/engine.py | 16 ++++++++++++---- tests/test_multiagent.py | 5 ++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/agentwatch/alerting/engine.py b/agentwatch/alerting/engine.py index 2ff84299..a3544f9a 100644 --- a/agentwatch/alerting/engine.py +++ b/agentwatch/alerting/engine.py @@ -58,17 +58,25 @@ async def alert_abuse(self, event: AbuseEvent) -> dict[str, bool]: ) return sent - async def alert_smart_alert(self, alert_text: str, dashboard_link: str) -> dict[str, bool]: """Surface a Smart Alert generated by the Human-in-the-Loop ambient integration.""" payload = { "slack": { "text": "Smart Alert Intervention Required", "blocks": [ - {"type": "section", "text": {"type": "mrkdwn", "text": f"*Smart Alert: Intervention Required*"}}, + { + "type": "section", + "text": {"type": "mrkdwn", "text": "*Smart Alert: Intervention Required*"}, + }, {"type": "section", "text": {"type": "mrkdwn", "text": alert_text}}, - {"type": "section", "text": {"type": "mrkdwn", "text": f"<{dashboard_link}|Review on Dashboard>"}} - ] + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"<{dashboard_link}|Review on Dashboard>", + }, + }, + ], } } sent = {"slack": False, "pagerduty": False} diff --git a/tests/test_multiagent.py b/tests/test_multiagent.py index 0d7ab281..c78cd3ec 100644 --- a/tests/test_multiagent.py +++ b/tests/test_multiagent.py @@ -10,10 +10,10 @@ from agentwatch.orchestration.dag import InterAgentDAG from agentwatch.orchestration.deadlock import DeadlockDetector from agentwatch.orchestration.propagation import trace_propagation +from agentwatch.orchestration.race_condition import AccessType, RaceConditionDetector from agentwatch.orchestration.shapley import shapley_attribution from agentwatch.orchestration.spawning import SpawningTracker, SpawnLimitExceeded from agentwatch.orchestration.trust import InterAgentTrust -from agentwatch.orchestration.race_condition import AccessType, RaceConditionDetector # ───────────────────────────────────────────── # MAG-001 — Inter-Agent DAG @@ -92,6 +92,7 @@ def test_low_trust_influences_high_trust(): def _race_ts(offset_seconds: float): from datetime import datetime, timedelta + base = datetime(2026, 1, 1, 0, 0, 0) return base + timedelta(seconds=offset_seconds) @@ -180,8 +181,6 @@ def test_race_invalid_window_raises(): det.record_access("agent-a", "db:orders", AccessType.WRITE, _race_ts(10), _race_ts(5)) - - # ───────────────────────────────────────────── # MAG-004 — Propagation # ───────────────────────────────────────────── From cb098d71d54578353cf5c7d36300204236f61558 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Yadav Date: Sun, 12 Jul 2026 14:17:06 +0530 Subject: [PATCH 3/3] Implement generic alert_custom, use it for alert_smart_alert, and add tests --- agentwatch/alerting/engine.py | 70 ++++++++++++++++++-------- tests/test_alerting.py | 95 +++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 20 deletions(-) diff --git a/agentwatch/alerting/engine.py b/agentwatch/alerting/engine.py index a3544f9a..a0d61a9a 100644 --- a/agentwatch/alerting/engine.py +++ b/agentwatch/alerting/engine.py @@ -58,32 +58,62 @@ async def alert_abuse(self, event: AbuseEvent) -> dict[str, bool]: ) return sent - async def alert_smart_alert(self, alert_text: str, dashboard_link: str) -> dict[str, bool]: - """Surface a Smart Alert generated by the Human-in-the-Loop ambient integration.""" - payload = { - "slack": { - "text": "Smart Alert Intervention Required", - "blocks": [ - { - "type": "section", - "text": {"type": "mrkdwn", "text": "*Smart Alert: Intervention Required*"}, - }, - {"type": "section", "text": {"type": "mrkdwn", "text": alert_text}}, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"<{dashboard_link}|Review on Dashboard>", - }, + async def alert_custom( + self, + title: str, + text: str, + link: str | None = None, + ) -> dict[str, bool]: + """Surface a custom alert to Slack and PagerDuty channels.""" + slack_blocks = [ + { + "type": "section", + "text": {"type": "mrkdwn", "text": f"*{title}*"}, + }, + {"type": "section", "text": {"type": "mrkdwn", "text": text}}, + ] + if link: + slack_blocks.append( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"<{link}|Review on Dashboard>", }, - ], - } + } + ) + + slack_payload = { + "text": title, + "blocks": slack_blocks, } + + # PagerDuty integration v2 payload + pd_payload = { + "routing_key": self._config.pagerduty_routing_key or "", + "event_action": "trigger", + "payload": { + "summary": f"{title}: {text}", + "source": "agentwatch", + "severity": "warning", + }, + } + sent = {"slack": False, "pagerduty": False} if self._config.slack_webhook_url: - sent["slack"] = await self._post(self._config.slack_webhook_url, payload["slack"]) + sent["slack"] = await self._post(self._config.slack_webhook_url, slack_payload) + if self._config.pagerduty_webhook_url: + sent["pagerduty"] = await self._post(self._config.pagerduty_webhook_url, pd_payload) return sent + async def alert_smart_alert(self, alert_text: str, dashboard_link: str) -> dict[str, bool]: + """Surface a Smart Alert generated by the Human-in-the-Loop ambient integration.""" + return await self.alert_custom( + title="Smart Alert: Intervention Required", + text=alert_text, + link=dashboard_link, + ) + def _build_abuse_payload(self, event: AbuseEvent) -> dict[str, dict[str, Any]]: summary = ( f"AgentWatch entitlement abuse: {event.subject} used on " diff --git a/tests/test_alerting.py b/tests/test_alerting.py index de476e62..afa42c8b 100644 --- a/tests/test_alerting.py +++ b/tests/test_alerting.py @@ -71,3 +71,98 @@ async def mock_post(*args, **kwargs): sent = asyncio.run(engine.alert_event(event)) assert sent["slack"] is False assert calls == 3 + + +def test_alert_custom_slack_and_pagerduty(monkeypatch): + slack_payloads = [] + pd_payloads = [] + + class MockResponse: + def raise_for_status(self): + pass + + async def mock_post(client, url, content, headers): + import json + data = json.loads(content.decode("utf-8")) + if "slack.com" in url: + slack_payloads.append(data) + elif "pagerduty.com" in url: + pd_payloads.append(data) + return MockResponse() + + monkeypatch.setattr(httpx.AsyncClient, "post", mock_post) + + engine = AlertingEngine( + AlertingConfig( + slack_webhook_url="https://hooks.slack.com/services/TTEST1234/BTEST1234/abcdefghijklmn", + pagerduty_webhook_url="https://events.pagerduty.com/v2/enqueue", + pagerduty_routing_key="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + ) + + sent = asyncio.run( + engine.alert_custom( + title="Custom Title", + text="Custom Text Detail", + link="https://dashboard.example.com", + ) + ) + + assert sent["slack"] is True + assert sent["pagerduty"] is True + + assert len(slack_payloads) == 1 + assert slack_payloads[0]["text"] == "Custom Title" + assert slack_payloads[0]["blocks"][1]["text"]["text"] == "Custom Text Detail" + + assert len(pd_payloads) == 1 + assert pd_payloads[0]["payload"]["summary"] == "Custom Title: Custom Text Detail" + assert pd_payloads[0]["routing_key"] == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + +def test_alert_custom_no_webhooks_configured(): + engine = AlertingEngine(AlertingConfig()) + sent = asyncio.run( + engine.alert_custom( + title="Custom Title", + text="Custom Text Detail", + ) + ) + assert sent["slack"] is False + assert sent["pagerduty"] is False + + +def test_alert_smart_alert_delivery(monkeypatch): + slack_payloads = [] + + class MockResponse: + def raise_for_status(self): + pass + + async def mock_post(client, url, content, headers): + import json + data = json.loads(content.decode("utf-8")) + slack_payloads.append(data) + return MockResponse() + + monkeypatch.setattr(httpx.AsyncClient, "post", mock_post) + + engine = AlertingEngine( + AlertingConfig( + slack_webhook_url="https://hooks.slack.com/services/TTEST1234/BTEST1234/abcdefghijklmn", + ) + ) + + sent = asyncio.run( + engine.alert_smart_alert( + alert_text="Smart alert detail", + dashboard_link="https://dashboard.example.com/smart", + ) + ) + + assert sent["slack"] is True + assert len(slack_payloads) == 1 + assert slack_payloads[0]["text"] == "Smart Alert: Intervention Required" + assert slack_payloads[0]["blocks"][1]["text"]["text"] == "Smart alert detail" + assert "https://dashboard.example.com/smart" in slack_payloads[0]["blocks"][2]["text"]["text"] +