Files
breakpilot-lehrer/backend-lehrer/alerts_agent/actions/slack_action.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

199 lines
5.9 KiB
Python

"""
Slack Action für Alerts Agent.
Sendet Slack-Nachrichten für Alerts via Incoming Webhooks.
"""
import logging
from typing import Dict, Any, List
import httpx
from .base import ActionHandler, ActionResult, ActionType, AlertContext
logger = logging.getLogger(__name__)
class SlackAction(ActionHandler):
"""
Slack-Benachrichtigungen für Alerts via Incoming Webhooks.
Konfiguration:
- webhook_url: Slack Incoming Webhook URL
- channel: Optional - Channel überschreiben
- username: Optional - Bot-Username (default: BreakPilot Alerts)
- icon_emoji: Optional - Bot-Icon (default: :bell:)
"""
@property
def action_type(self) -> ActionType:
return ActionType.SLACK
def get_required_config_fields(self) -> List[str]:
return ["webhook_url"]
def validate_config(self, config: Dict[str, Any]) -> bool:
url = config.get("webhook_url", "")
return "hooks.slack.com" in url or url.startswith("https://")
async def execute(
self,
context: AlertContext,
config: Dict[str, Any],
) -> ActionResult:
"""
Sendet eine Slack-Nachricht.
Args:
context: Alert-Kontext
config: Slack-Konfiguration (webhook_url, channel, etc.)
Returns:
ActionResult
"""
try:
webhook_url = config.get("webhook_url")
# Slack-Payload mit Block Kit
payload = self._build_slack_payload(context, config)
# Request senden
async with httpx.AsyncClient(timeout=30) as client:
response = await client.post(
webhook_url,
json=payload,
headers={"Content-Type": "application/json"},
)
# Slack gibt "ok" als Text zurück bei Erfolg
success = response.status_code == 200 and response.text == "ok"
return ActionResult(
success=success,
action_type=self.action_type,
message="Slack-Nachricht gesendet" if success else "Slack-Fehler",
details={
"status_code": response.status_code,
"response": response.text[:100],
},
error=None if success else response.text,
)
except Exception as e:
logger.error(f"Slack action error: {e}")
return ActionResult(
success=False,
action_type=self.action_type,
message="Slack-Fehler",
error=str(e),
)
def _build_slack_payload(
self,
context: AlertContext,
config: Dict[str, Any],
) -> Dict[str, Any]:
"""
Erstellt den Slack-Payload mit Block Kit.
Verwendet Rich-Formatting für bessere Darstellung.
"""
# Basis-Payload
payload = {
"username": config.get("username", "BreakPilot Alerts"),
"icon_emoji": config.get("icon_emoji", ":bell:"),
}
# Channel überschreiben wenn angegeben
if config.get("channel"):
payload["channel"] = config["channel"]
# Block Kit Blocks
blocks = [
# Header
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"📰 {context.topic_name}",
"emoji": True,
}
},
# Alert-Titel als Link
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*<{context.url}|{context.title}>*",
}
},
]
# Snippet wenn vorhanden
if context.snippet:
snippet = context.snippet[:200]
if len(context.snippet) > 200:
snippet += "..."
blocks.append({
"type": "section",
"text": {
"type": "plain_text",
"text": snippet,
"emoji": False,
}
})
# Kontext-Felder (Score, Decision, Rule)
fields = []
if context.relevance_score is not None:
score_emoji = "🟢" if context.relevance_score >= 0.7 else "🟡" if context.relevance_score >= 0.4 else "🔴"
fields.append({
"type": "mrkdwn",
"text": f"*Score:* {score_emoji} {context.relevance_score:.0%}",
})
if context.relevance_decision:
decision_emoji = {"KEEP": "", "DROP": "", "REVIEW": "👀"}.get(context.relevance_decision, "")
fields.append({
"type": "mrkdwn",
"text": f"*Decision:* {decision_emoji} {context.relevance_decision}",
})
if context.matched_rule:
fields.append({
"type": "mrkdwn",
"text": f"*Regel:* {context.matched_rule}",
})
if context.tags:
fields.append({
"type": "mrkdwn",
"text": f"*Tags:* {', '.join(context.tags)}",
})
if fields:
blocks.append({
"type": "section",
"fields": fields[:10], # Max 10 Felder
})
# Divider
blocks.append({"type": "divider"})
# Actions (Link zur Inbox)
blocks.append({
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"<{config.get('dashboard_url', 'http://localhost:8000/studio#alerts')}|Zur Alerts Inbox> | Gesendet von BreakPilot",
}
]
})
payload["blocks"] = blocks
# Fallback-Text für Notifications
payload["text"] = f"Neuer Alert: {context.title}"
return payload