This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/alerts_agent/api/rules.py
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

474 lines
14 KiB
Python

"""
Rules API Routes für Alerts Agent.
CRUD-Operationen für Alert-Regeln.
"""
from typing import List, Optional, Dict, Any
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session as DBSession
from alerts_agent.db import get_db
from alerts_agent.db.repository import RuleRepository
from alerts_agent.db.models import RuleActionEnum
router = APIRouter(prefix="/rules", tags=["alerts"])
# =============================================================================
# PYDANTIC MODELS
# =============================================================================
class RuleConditionModel(BaseModel):
"""Model für eine Regel-Bedingung."""
field: str = Field(..., description="Feld zum Prüfen (title, snippet, url, source, relevance_score)")
operator: str = Field(..., alias="op", description="Operator (contains, not_contains, equals, regex, gt, lt, in)")
value: Any = Field(..., description="Vergleichswert (String, Zahl, oder Liste)")
class Config:
populate_by_name = True
class RuleCreate(BaseModel):
"""Request-Model für Regel-Erstellung."""
name: str = Field(..., min_length=1, max_length=255)
description: str = Field(default="", max_length=2000)
conditions: List[RuleConditionModel] = Field(default_factory=list)
action_type: str = Field(default="keep", description="Aktion: keep, drop, tag, email, webhook, slack")
action_config: Dict[str, Any] = Field(default_factory=dict)
topic_id: Optional[str] = Field(default=None, description="Optional: Nur für bestimmtes Topic")
priority: int = Field(default=0, ge=0, le=1000, description="Priorität (höher = wird zuerst evaluiert)")
is_active: bool = Field(default=True)
class RuleUpdate(BaseModel):
"""Request-Model für Regel-Update."""
name: Optional[str] = Field(default=None, min_length=1, max_length=255)
description: Optional[str] = Field(default=None, max_length=2000)
conditions: Optional[List[RuleConditionModel]] = None
action_type: Optional[str] = None
action_config: Optional[Dict[str, Any]] = None
topic_id: Optional[str] = None
priority: Optional[int] = Field(default=None, ge=0, le=1000)
is_active: Optional[bool] = None
class RuleResponse(BaseModel):
"""Response-Model für Regel."""
id: str
name: str
description: str
conditions: List[Dict[str, Any]]
action_type: str
action_config: Dict[str, Any]
topic_id: Optional[str]
priority: int
is_active: bool
match_count: int
last_matched_at: Optional[datetime]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class RuleListResponse(BaseModel):
"""Response-Model für Regel-Liste."""
rules: List[RuleResponse]
total: int
class RuleTestRequest(BaseModel):
"""Request-Model für Regel-Test."""
title: str = Field(default="Test Title")
snippet: str = Field(default="Test snippet content")
url: str = Field(default="https://example.com/test")
source: str = Field(default="rss_feed")
relevance_score: Optional[float] = Field(default=None)
class RuleTestResponse(BaseModel):
"""Response-Model für Regel-Test."""
rule_id: str
rule_name: str
matched: bool
action: str
conditions_met: List[str]
# =============================================================================
# API ENDPOINTS
# =============================================================================
@router.post("", response_model=RuleResponse, status_code=201)
async def create_rule(
rule: RuleCreate,
db: DBSession = Depends(get_db),
) -> RuleResponse:
"""
Erstellt eine neue Regel.
Regeln werden nach Priorität evaluiert. Höhere Priorität = wird zuerst geprüft.
"""
repo = RuleRepository(db)
# Conditions zu Dict konvertieren
conditions = [
{"field": c.field, "op": c.operator, "value": c.value}
for c in rule.conditions
]
created = repo.create(
name=rule.name,
description=rule.description,
conditions=conditions,
action_type=rule.action_type,
action_config=rule.action_config,
topic_id=rule.topic_id,
priority=rule.priority,
)
if not rule.is_active:
repo.update(created.id, is_active=False)
created = repo.get_by_id(created.id)
return _to_rule_response(created)
@router.get("", response_model=RuleListResponse)
async def list_rules(
is_active: Optional[bool] = None,
topic_id: Optional[str] = None,
db: DBSession = Depends(get_db),
) -> RuleListResponse:
"""
Listet alle Regeln auf.
Regeln sind nach Priorität sortiert (höchste zuerst).
"""
repo = RuleRepository(db)
if is_active is True:
rules = repo.get_active()
else:
rules = repo.get_all()
# Topic-Filter
if topic_id:
rules = [r for r in rules if r.topic_id == topic_id or r.topic_id is None]
return RuleListResponse(
rules=[_to_rule_response(r) for r in rules],
total=len(rules),
)
@router.get("/{rule_id}", response_model=RuleResponse)
async def get_rule(
rule_id: str,
db: DBSession = Depends(get_db),
) -> RuleResponse:
"""
Ruft eine Regel nach ID ab.
"""
repo = RuleRepository(db)
rule = repo.get_by_id(rule_id)
if not rule:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
return _to_rule_response(rule)
@router.put("/{rule_id}", response_model=RuleResponse)
async def update_rule(
rule_id: str,
updates: RuleUpdate,
db: DBSession = Depends(get_db),
) -> RuleResponse:
"""
Aktualisiert eine Regel.
"""
repo = RuleRepository(db)
# Nur übergebene Werte updaten
update_dict = {}
if updates.name is not None:
update_dict["name"] = updates.name
if updates.description is not None:
update_dict["description"] = updates.description
if updates.conditions is not None:
update_dict["conditions"] = [
{"field": c.field, "op": c.operator, "value": c.value}
for c in updates.conditions
]
if updates.action_type is not None:
update_dict["action_type"] = updates.action_type
if updates.action_config is not None:
update_dict["action_config"] = updates.action_config
if updates.topic_id is not None:
update_dict["topic_id"] = updates.topic_id
if updates.priority is not None:
update_dict["priority"] = updates.priority
if updates.is_active is not None:
update_dict["is_active"] = updates.is_active
if not update_dict:
raise HTTPException(status_code=400, detail="Keine Updates angegeben")
updated = repo.update(rule_id, **update_dict)
if not updated:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
return _to_rule_response(updated)
@router.delete("/{rule_id}", status_code=204)
async def delete_rule(
rule_id: str,
db: DBSession = Depends(get_db),
):
"""
Löscht eine Regel.
"""
repo = RuleRepository(db)
success = repo.delete(rule_id)
if not success:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
return None
@router.post("/{rule_id}/activate", response_model=RuleResponse)
async def activate_rule(
rule_id: str,
db: DBSession = Depends(get_db),
) -> RuleResponse:
"""
Aktiviert eine Regel.
"""
repo = RuleRepository(db)
updated = repo.update(rule_id, is_active=True)
if not updated:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
return _to_rule_response(updated)
@router.post("/{rule_id}/deactivate", response_model=RuleResponse)
async def deactivate_rule(
rule_id: str,
db: DBSession = Depends(get_db),
) -> RuleResponse:
"""
Deaktiviert eine Regel.
"""
repo = RuleRepository(db)
updated = repo.update(rule_id, is_active=False)
if not updated:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
return _to_rule_response(updated)
@router.post("/{rule_id}/test", response_model=RuleTestResponse)
async def test_rule(
rule_id: str,
test_data: RuleTestRequest,
db: DBSession = Depends(get_db),
) -> RuleTestResponse:
"""
Testet eine Regel gegen Testdaten.
Nützlich um Regeln vor der Aktivierung zu testen.
"""
from alerts_agent.processing.rule_engine import evaluate_rule
from alerts_agent.db.models import AlertItemDB, AlertSourceEnum, AlertStatusEnum
repo = RuleRepository(db)
rule = repo.get_by_id(rule_id)
if not rule:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
# Mock-Alert für Test erstellen
mock_alert = AlertItemDB(
id="test-alert",
topic_id="test-topic",
title=test_data.title,
snippet=test_data.snippet,
url=test_data.url,
url_hash="test-hash",
source=AlertSourceEnum(test_data.source) if test_data.source else AlertSourceEnum.RSS_FEED,
status=AlertStatusEnum.NEW,
relevance_score=test_data.relevance_score,
)
# Regel evaluieren
match = evaluate_rule(mock_alert, rule)
return RuleTestResponse(
rule_id=match.rule_id,
rule_name=match.rule_name,
matched=match.matched,
action=match.action.value,
conditions_met=match.conditions_met,
)
@router.post("/test-all", response_model=List[RuleTestResponse])
async def test_all_rules(
test_data: RuleTestRequest,
db: DBSession = Depends(get_db),
) -> List[RuleTestResponse]:
"""
Testet alle aktiven Regeln gegen Testdaten.
Zeigt welche Regeln matchen würden.
"""
from alerts_agent.processing.rule_engine import evaluate_rules_for_alert, evaluate_rule
from alerts_agent.db.models import AlertItemDB, AlertSourceEnum, AlertStatusEnum
repo = RuleRepository(db)
rules = repo.get_active()
# Mock-Alert für Test erstellen
mock_alert = AlertItemDB(
id="test-alert",
topic_id="test-topic",
title=test_data.title,
snippet=test_data.snippet,
url=test_data.url,
url_hash="test-hash",
source=AlertSourceEnum(test_data.source) if test_data.source else AlertSourceEnum.RSS_FEED,
status=AlertStatusEnum.NEW,
relevance_score=test_data.relevance_score,
)
results = []
for rule in rules:
match = evaluate_rule(mock_alert, rule)
results.append(RuleTestResponse(
rule_id=match.rule_id,
rule_name=match.rule_name,
matched=match.matched,
action=match.action.value,
conditions_met=match.conditions_met,
))
return results
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
def _to_rule_response(rule) -> RuleResponse:
"""Konvertiert ein Rule-DB-Objekt zu RuleResponse."""
return RuleResponse(
id=rule.id,
name=rule.name,
description=rule.description or "",
conditions=rule.conditions or [],
action_type=rule.action_type.value if rule.action_type else "keep",
action_config=rule.action_config or {},
topic_id=rule.topic_id,
priority=rule.priority,
is_active=rule.is_active,
match_count=rule.match_count,
last_matched_at=rule.last_matched_at,
created_at=rule.created_at,
updated_at=rule.updated_at,
)
# =============================================================================
# PRESET RULES
# =============================================================================
PRESET_RULES = {
"exclude_jobs": {
"name": "Stellenanzeigen ausschließen",
"description": "Filtert Stellenanzeigen und Job-Postings",
"conditions": [
{"field": "title", "op": "in", "value": ["Stellenanzeige", "Job", "Karriere", "Praktikum", "Werkstudent", "Ausbildung", "Referendariat"]}
],
"action_type": "drop",
"priority": 100,
},
"exclude_ads": {
"name": "Werbung ausschließen",
"description": "Filtert Werbung und Pressemitteilungen",
"conditions": [
{"field": "title", "op": "in", "value": ["Werbung", "Anzeige", "Pressemitteilung", "PR:", "Sponsored"]}
],
"action_type": "drop",
"priority": 100,
},
"keep_inklusion": {
"name": "Inklusion behalten",
"description": "Behält Artikel zum Thema Inklusion",
"conditions": [
{"field": "title", "op": "in", "value": ["Inklusion", "inklusiv", "Förderbedarf", "Förderschule", "Nachteilsausgleich"]}
],
"action_type": "keep",
"priority": 50,
},
"keep_datenschutz": {
"name": "Datenschutz behalten",
"description": "Behält Artikel zum Thema Datenschutz in Schulen",
"conditions": [
{"field": "title", "op": "in", "value": ["DSGVO", "Datenschutz", "Schülerfotos", "personenbezogen"]}
],
"action_type": "keep",
"priority": 50,
},
}
@router.get("/presets/list")
async def list_preset_rules() -> Dict[str, Any]:
"""
Listet verfügbare Regel-Vorlagen auf.
"""
return {
"presets": [
{"id": key, **value}
for key, value in PRESET_RULES.items()
]
}
@router.post("/presets/{preset_id}/apply", response_model=RuleResponse)
async def apply_preset_rule(
preset_id: str,
db: DBSession = Depends(get_db),
) -> RuleResponse:
"""
Wendet eine Regel-Vorlage an (erstellt die Regel).
"""
if preset_id not in PRESET_RULES:
raise HTTPException(status_code=404, detail="Preset nicht gefunden")
preset = PRESET_RULES[preset_id]
repo = RuleRepository(db)
created = repo.create(
name=preset["name"],
description=preset.get("description", ""),
conditions=preset["conditions"],
action_type=preset["action_type"],
priority=preset.get("priority", 0),
)
return _to_rule_response(created)