klausur-service (11 files): - cv_gutter_repair, ocr_pipeline_regression, upload_api - ocr_pipeline_sessions, smart_spell, nru_worksheet_generator - ocr_pipeline_overlays, mail/aggregator, zeugnis_api - cv_syllable_detect, self_rag backend-lehrer (17 files): - classroom_engine/suggestions, generators/quiz_generator - worksheets_api, llm_gateway/comparison, state_engine_api - classroom/models (→ 4 submodules), services/file_processor - alerts_agent/api/wizard+digests+routes, content_generators/pdf - classroom/routes/sessions, llm_gateway/inference - classroom_engine/analytics, auth/keycloak_auth - alerts_agent/processing/rule_engine, ai_processor/print_versions agent-core (5 files): - brain/memory_store, brain/knowledge_graph, brain/context_manager - orchestrator/supervisor, sessions/session_manager admin-lehrer (5 components): - GridOverlay, StepGridReview, DevOpsPipelineSidebar - DataFlowDiagram, sbom/wizard/page website (2 files): - DependencyMap, lehrer/abitur-archiv Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
207 lines
6.9 KiB
Python
207 lines
6.9 KiB
Python
"""
|
|
Rule Engine - Models, Condition Evaluation, and Convenience Functions.
|
|
|
|
Datenmodelle und Evaluierungs-Logik fuer Alert-Regeln.
|
|
"""
|
|
|
|
import re
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import List, Dict, Any, Optional
|
|
from enum import Enum
|
|
|
|
from alerts_agent.db.models import AlertItemDB, AlertRuleDB, RuleActionEnum
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ConditionOperator(str, Enum):
|
|
"""Operatoren für Regel-Bedingungen."""
|
|
CONTAINS = "contains"
|
|
NOT_CONTAINS = "not_contains"
|
|
EQUALS = "equals"
|
|
NOT_EQUALS = "not_equals"
|
|
STARTS_WITH = "starts_with"
|
|
ENDS_WITH = "ends_with"
|
|
REGEX = "regex"
|
|
GREATER_THAN = "gt"
|
|
LESS_THAN = "lt"
|
|
GREATER_EQUAL = "gte"
|
|
LESS_EQUAL = "lte"
|
|
IN_LIST = "in"
|
|
NOT_IN_LIST = "not_in"
|
|
|
|
|
|
@dataclass
|
|
class RuleCondition:
|
|
"""Eine einzelne Regel-Bedingung."""
|
|
field: str
|
|
operator: ConditionOperator
|
|
value: Any
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict) -> "RuleCondition":
|
|
return cls(
|
|
field=data.get("field", ""),
|
|
operator=ConditionOperator(data.get("operator", data.get("op", "contains"))),
|
|
value=data.get("value", ""),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class RuleMatch:
|
|
"""Ergebnis einer Regel-Evaluierung."""
|
|
rule_id: str
|
|
rule_name: str
|
|
matched: bool
|
|
action: RuleActionEnum
|
|
action_config: Dict[str, Any]
|
|
conditions_met: List[str]
|
|
|
|
|
|
def get_field_value(alert: AlertItemDB, field: str) -> Any:
|
|
"""Extrahiert einen Feldwert aus einem Alert."""
|
|
field_map = {
|
|
"title": alert.title,
|
|
"snippet": alert.snippet,
|
|
"url": alert.url,
|
|
"source": alert.source.value if alert.source else "",
|
|
"status": alert.status.value if alert.status else "",
|
|
"relevance_score": alert.relevance_score,
|
|
"relevance_decision": alert.relevance_decision.value if alert.relevance_decision else "",
|
|
"lang": alert.lang,
|
|
"topic_id": alert.topic_id,
|
|
}
|
|
return field_map.get(field)
|
|
|
|
|
|
def evaluate_condition(alert: AlertItemDB, condition: RuleCondition) -> bool:
|
|
"""Evaluiert eine einzelne Bedingung gegen einen Alert."""
|
|
field_value = get_field_value(alert, condition.field)
|
|
if field_value is None:
|
|
return False
|
|
|
|
op = condition.operator
|
|
target = condition.value
|
|
|
|
try:
|
|
if isinstance(field_value, str):
|
|
field_lower = field_value.lower()
|
|
target_lower = str(target).lower() if isinstance(target, str) else target
|
|
|
|
if op == ConditionOperator.CONTAINS:
|
|
return target_lower in field_lower
|
|
elif op == ConditionOperator.NOT_CONTAINS:
|
|
return target_lower not in field_lower
|
|
elif op == ConditionOperator.EQUALS:
|
|
return field_lower == target_lower
|
|
elif op == ConditionOperator.NOT_EQUALS:
|
|
return field_lower != target_lower
|
|
elif op == ConditionOperator.STARTS_WITH:
|
|
return field_lower.startswith(target_lower)
|
|
elif op == ConditionOperator.ENDS_WITH:
|
|
return field_lower.endswith(target_lower)
|
|
elif op == ConditionOperator.REGEX:
|
|
try:
|
|
return bool(re.search(str(target), field_value, re.IGNORECASE))
|
|
except re.error:
|
|
logger.warning(f"Invalid regex pattern: {target}")
|
|
return False
|
|
elif op == ConditionOperator.IN_LIST:
|
|
if isinstance(target, list):
|
|
return any(t.lower() in field_lower for t in target if isinstance(t, str))
|
|
return False
|
|
elif op == ConditionOperator.NOT_IN_LIST:
|
|
if isinstance(target, list):
|
|
return not any(t.lower() in field_lower for t in target if isinstance(t, str))
|
|
return True
|
|
|
|
elif isinstance(field_value, (int, float)):
|
|
target_num = float(target) if target else 0
|
|
if op == ConditionOperator.EQUALS:
|
|
return field_value == target_num
|
|
elif op == ConditionOperator.NOT_EQUALS:
|
|
return field_value != target_num
|
|
elif op == ConditionOperator.GREATER_THAN:
|
|
return field_value > target_num
|
|
elif op == ConditionOperator.LESS_THAN:
|
|
return field_value < target_num
|
|
elif op == ConditionOperator.GREATER_EQUAL:
|
|
return field_value >= target_num
|
|
elif op == ConditionOperator.LESS_EQUAL:
|
|
return field_value <= target_num
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error evaluating condition: {e}")
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
def evaluate_rule(alert: AlertItemDB, rule: AlertRuleDB) -> RuleMatch:
|
|
"""Evaluiert eine Regel gegen einen Alert (AND-Verknüpfung)."""
|
|
conditions = rule.conditions or []
|
|
conditions_met = []
|
|
all_matched = True
|
|
|
|
for cond_dict in conditions:
|
|
condition = RuleCondition.from_dict(cond_dict)
|
|
if evaluate_condition(alert, condition):
|
|
conditions_met.append(f"{condition.field} {condition.operator.value} {condition.value}")
|
|
else:
|
|
all_matched = False
|
|
|
|
if not conditions:
|
|
all_matched = True
|
|
|
|
return RuleMatch(
|
|
rule_id=rule.id, rule_name=rule.name, matched=all_matched,
|
|
action=rule.action_type, action_config=rule.action_config or {},
|
|
conditions_met=conditions_met,
|
|
)
|
|
|
|
|
|
def evaluate_rules_for_alert(alert: AlertItemDB, rules: List[AlertRuleDB]) -> Optional[RuleMatch]:
|
|
"""Evaluiert alle Regeln gegen einen Alert und gibt den ersten Match zurück."""
|
|
for rule in rules:
|
|
if not rule.is_active:
|
|
continue
|
|
if rule.topic_id and rule.topic_id != alert.topic_id:
|
|
continue
|
|
|
|
match = evaluate_rule(alert, rule)
|
|
if match.matched:
|
|
logger.debug(f"Rule '{rule.name}' matched alert '{alert.id[:8]}': {match.conditions_met}")
|
|
return match
|
|
|
|
return None
|
|
|
|
|
|
# Convenience-Funktionen
|
|
|
|
def create_keyword_rule(name: str, keywords: List[str], action: str = "keep", field: str = "title") -> Dict:
|
|
"""Erstellt eine Keyword-basierte Regel."""
|
|
return {
|
|
"name": name,
|
|
"conditions": [{"field": field, "operator": "in", "value": keywords}],
|
|
"action_type": action, "action_config": {},
|
|
}
|
|
|
|
|
|
def create_exclusion_rule(name: str, excluded_terms: List[str], field: str = "title") -> Dict:
|
|
"""Erstellt eine Ausschluss-Regel."""
|
|
return {
|
|
"name": name,
|
|
"conditions": [{"field": field, "operator": "in", "value": excluded_terms}],
|
|
"action_type": "drop", "action_config": {},
|
|
}
|
|
|
|
|
|
def create_score_threshold_rule(name: str, min_score: float, action: str = "keep") -> Dict:
|
|
"""Erstellt eine Score-basierte Regel."""
|
|
return {
|
|
"name": name,
|
|
"conditions": [{"field": "relevance_score", "operator": "gte", "value": min_score}],
|
|
"action_type": action, "action_config": {},
|
|
}
|