""" 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"" 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"" 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"" 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"" 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, } )