Files
breakpilot-lehrer/backend-lehrer/alerts_agent/db/models.py
Benjamin Admin 451365a312 [split-required] Split remaining 500-680 LOC files (final batch)
website (17 pages + 3 components):
- multiplayer/wizard, middleware/wizard+test-wizard, communication
- builds/wizard, staff-search, voice, sbom/wizard
- foerderantrag, mail/tasks, tools/communication, sbom
- compliance/evidence, uni-crawler, brandbook (already done)
- CollectionsTab, IngestionTab, RiskHeatmap

backend-lehrer (5 files):
- letters_api (641 → 2), certificates_api (636 → 2)
- alerts_agent/db/models (636 → 3)
- llm_gateway/communication_service (614 → 2)
- game/database already done in prior batch

klausur-service (2 files):
- hybrid_vocab_extractor (664 → 2)
- klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2)

voice-service (3 files):
- bqas/rag_judge (618 → 3), runner (529 → 2)
- enhanced_task_orchestrator (519 → 2)

studio-v2 (6 files):
- korrektur/[klausurId] (578 → 4), fairness (569 → 2)
- AlertsWizard (552 → 2), OnboardingWizard (513 → 2)
- korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:56:45 +02:00

252 lines
10 KiB
Python

"""
SQLAlchemy Database Models fuer Alerts Agent.
Split into:
- enums.py: All enum definitions
- models.py (this file): Core ORM models (Topic, Item, Rule, Profile)
- models_dual_mode.py: Dual-mode system (Template, Source, Subscription, Digest)
All symbols are re-exported here for backward compatibility.
"""
from datetime import datetime
from sqlalchemy import (
Column, String, Integer, Float, DateTime, JSON,
Boolean, Text, Enum as SQLEnum, ForeignKey, Index
)
from sqlalchemy.orm import relationship
import uuid
from classroom_engine.database import Base
# Re-export all enums
from .enums import (
AlertSourceEnum,
AlertStatusEnum,
RelevanceDecisionEnum,
FeedTypeEnum,
RuleActionEnum,
ImportanceLevelEnum,
AlertModeEnum,
MigrationModeEnum,
DigestStatusEnum,
UserRoleEnum,
)
# Re-export dual-mode models
from .models_dual_mode import (
AlertTemplateDB,
AlertSourceDB,
UserAlertSubscriptionDB,
AlertDigestDB,
)
class AlertTopicDB(Base):
"""
Alert Topic / Feed-Quelle.
Repraesentiert eine Google Alert-Konfiguration oder einen RSS-Feed.
"""
__tablename__ = 'alert_topics'
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(String(36), nullable=True, index=True)
name = Column(String(255), nullable=False)
description = Column(Text, default="")
feed_url = Column(String(2000), nullable=True)
feed_type = Column(SQLEnum(FeedTypeEnum), default=FeedTypeEnum.RSS, nullable=False)
is_active = Column(Boolean, default=True, index=True)
fetch_interval_minutes = Column(Integer, default=60)
last_fetched_at = Column(DateTime, nullable=True)
last_fetch_error = Column(Text, nullable=True)
total_items_fetched = Column(Integer, default=0)
items_kept = Column(Integer, default=0)
items_dropped = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
alerts = relationship("AlertItemDB", back_populates="topic", cascade="all, delete-orphan")
rules = relationship("AlertRuleDB", back_populates="topic", cascade="all, delete-orphan")
def __repr__(self):
return f"<AlertTopic {self.name} ({self.feed_type.value})>"
class AlertItemDB(Base):
"""
Einzelner Alert-Eintrag.
Entspricht einem Artikel/Link aus Google Alerts oder RSS.
"""
__tablename__ = 'alert_items'
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
topic_id = Column(String(36), ForeignKey('alert_topics.id', ondelete='CASCADE'), nullable=False, index=True)
title = Column(Text, nullable=False)
url = Column(String(2000), nullable=False)
snippet = Column(Text, default="")
article_text = Column(Text, nullable=True)
lang = Column(String(10), default="de")
published_at = Column(DateTime, nullable=True, index=True)
fetched_at = Column(DateTime, default=datetime.utcnow, index=True)
processed_at = Column(DateTime, nullable=True)
source = Column(SQLEnum(AlertSourceEnum), default=AlertSourceEnum.GOOGLE_ALERTS_RSS, nullable=False)
url_hash = Column(String(64), unique=True, nullable=False, index=True)
content_hash = Column(String(64), nullable=True)
canonical_url = Column(String(2000), nullable=True)
status = Column(SQLEnum(AlertStatusEnum), default=AlertStatusEnum.NEW, nullable=False, index=True)
cluster_id = Column(String(36), nullable=True)
relevance_score = Column(Float, nullable=True)
relevance_decision = Column(SQLEnum(RelevanceDecisionEnum), nullable=True, index=True)
relevance_reasons = Column(JSON, default=list)
relevance_summary = Column(Text, nullable=True)
scored_by_model = Column(String(100), nullable=True)
scored_at = Column(DateTime, nullable=True)
user_marked_relevant = Column(Boolean, nullable=True)
user_tags = Column(JSON, default=list)
user_notes = Column(Text, nullable=True)
# Guided Mode Fields
importance_level = Column(SQLEnum(ImportanceLevelEnum), nullable=True, index=True)
why_relevant = Column(Text, nullable=True)
next_steps = Column(JSON, default=list)
action_deadline = Column(DateTime, nullable=True)
source_name = Column(String(255), nullable=True)
source_credibility = Column(String(50), default="official")
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
topic = relationship("AlertTopicDB", back_populates="alerts")
__table_args__ = (
Index('ix_alert_items_topic_status', 'topic_id', 'status'),
Index('ix_alert_items_topic_decision', 'topic_id', 'relevance_decision'),
)
def __repr__(self):
return f"<AlertItem {self.id[:8]}: {self.title[:50]}... ({self.status.value})>"
class AlertRuleDB(Base):
"""Filterregel fuer Alerts."""
__tablename__ = 'alert_rules'
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
topic_id = Column(String(36), ForeignKey('alert_topics.id', ondelete='CASCADE'), nullable=True, index=True)
user_id = Column(String(36), nullable=True, index=True)
name = Column(String(255), nullable=False)
description = Column(Text, default="")
conditions = Column(JSON, nullable=False, default=list)
action_type = Column(SQLEnum(RuleActionEnum), default=RuleActionEnum.KEEP, nullable=False)
action_config = Column(JSON, default=dict)
priority = Column(Integer, default=0, index=True)
is_active = Column(Boolean, default=True, index=True)
match_count = Column(Integer, default=0)
last_matched_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
topic = relationship("AlertTopicDB", back_populates="rules")
def __repr__(self):
return f"<AlertRule {self.name} ({self.action_type.value})>"
class AlertProfileDB(Base):
"""Nutzer-Profil fuer Relevanz-Scoring."""
__tablename__ = 'alert_profiles'
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(String(36), unique=True, nullable=True, index=True)
name = Column(String(255), default="Default")
priorities = Column(JSON, default=list)
exclusions = Column(JSON, default=list)
positive_examples = Column(JSON, default=list)
negative_examples = Column(JSON, default=list)
policies = Column(JSON, default=dict)
total_scored = Column(Integer, default=0)
total_kept = Column(Integer, default=0)
total_dropped = Column(Integer, default=0)
accuracy_estimate = Column(Float, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<AlertProfile {self.name} (user={self.user_id})>"
def get_prompt_context(self) -> str:
"""Generiere Kontext fuer LLM-Prompt."""
lines = ["## Relevanzprofil des Nutzers\n"]
if self.priorities:
lines.append("### Prioritaeten (Themen von Interesse):")
for p in self.priorities:
weight = p.get("weight", 0.5)
weight_label = "Sehr wichtig" if weight > 0.7 else "Wichtig" if weight > 0.4 else "Interessant"
lines.append(f"- **{p.get('label', 'Unbenannt')}** ({weight_label})")
if p.get("description"):
lines.append(f" {p['description']}")
if p.get("keywords"):
lines.append(f" Keywords: {', '.join(p['keywords'])}")
lines.append("")
if self.exclusions:
lines.append("### Ausschluesse (ignorieren):")
lines.append(f"Themen mit diesen Keywords: {', '.join(self.exclusions)}")
lines.append("")
if self.positive_examples:
lines.append("### Beispiele fuer relevante Alerts:")
for ex in self.positive_examples[-5:]:
lines.append(f"- \"{ex.get('title', '')}\"")
if ex.get("reason"):
lines.append(f" Grund: {ex['reason']}")
lines.append("")
if self.negative_examples:
lines.append("### Beispiele fuer irrelevante Alerts:")
for ex in self.negative_examples[-5:]:
lines.append(f"- \"{ex.get('title', '')}\"")
if ex.get("reason"):
lines.append(f" Grund: {ex['reason']}")
lines.append("")
if self.policies:
lines.append("### Zusaetzliche Regeln:")
for key, value in self.policies.items():
lines.append(f"- {key}: {value}")
return "\n".join(lines)
@classmethod
def create_default_education_profile(cls) -> "AlertProfileDB":
"""Erstelle ein Standard-Profil fuer Bildungsthemen."""
return cls(
name="Bildung Default",
priorities=[
{
"label": "Inklusion", "weight": 0.9,
"keywords": ["inklusiv", "Foerderbedarf", "Behinderung", "Barrierefreiheit"],
"description": "Inklusive Bildung, Foerderschulen, Nachteilsausgleich"
},
{
"label": "Datenschutz Schule", "weight": 0.85,
"keywords": ["DSGVO", "Schuelerfotos", "Einwilligung", "personenbezogene Daten"],
"description": "DSGVO in Schulen, Datenschutz bei Klassenfotos"
},
{
"label": "Schulrecht Bayern", "weight": 0.8,
"keywords": ["BayEUG", "Schulordnung", "Kultusministerium", "Bayern"],
"description": "Bayerisches Schulrecht, Verordnungen"
},
{
"label": "Digitalisierung Schule", "weight": 0.7,
"keywords": ["DigitalPakt", "Tablet-Klasse", "Lernplattform"],
"description": "Digitale Medien im Unterricht"
},
],
exclusions=["Stellenanzeige", "Praktikum gesucht", "Werbung", "Pressemitteilung"],
policies={
"prefer_german_sources": True,
"max_age_days": 30,
"min_content_length": 100,
}
)