[split-required] Split final 43 files (500-668 LOC) to complete refactoring

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>
This commit is contained in:
Benjamin Admin
2026-04-25 09:41:42 +02:00
parent 451365a312
commit bd4b956e3c
113 changed files with 13790 additions and 14148 deletions

View File

@@ -1,5 +1,5 @@
"""
API Routes für Alerts Agent.
API Routes fuer Alerts Agent.
Endpoints:
- POST /alerts/ingest - Manuell Alerts importieren
@@ -13,12 +13,18 @@ Endpoints:
import os
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from fastapi import APIRouter, HTTPException, Query
from ..models.alert_item import AlertItem, AlertStatus
from ..models.relevance_profile import RelevanceProfile, PriorityItem
from ..processing.relevance_scorer import RelevanceDecision, RelevanceScorer
from .schemas import (
AlertIngestRequest, AlertIngestResponse,
AlertRunRequest, AlertRunResponse,
InboxItem, InboxResponse,
FeedbackRequest, FeedbackResponse,
ProfilePriorityRequest, ProfileUpdateRequest, ProfileResponse,
)
router = APIRouter(prefix="/alerts", tags=["alerts"])
@@ -30,113 +36,13 @@ ALERTS_USE_LLM = os.getenv("ALERTS_USE_LLM", "false").lower() == "true"
# ============================================================================
# In-Memory Storage (später durch DB ersetzen)
# In-Memory Storage (spaeter durch DB ersetzen)
# ============================================================================
_alerts_store: dict[str, AlertItem] = {}
_profile_store: dict[str, RelevanceProfile] = {}
# ============================================================================
# Request/Response Models
# ============================================================================
class AlertIngestRequest(BaseModel):
"""Request für manuelles Alert-Import."""
title: str = Field(..., min_length=1, max_length=500)
url: str = Field(..., min_length=1)
snippet: Optional[str] = Field(default=None, max_length=2000)
topic_label: str = Field(default="Manual Import")
published_at: Optional[datetime] = None
class AlertIngestResponse(BaseModel):
"""Response für Alert-Import."""
id: str
status: str
message: str
class AlertRunRequest(BaseModel):
"""Request für Scoring-Pipeline."""
limit: int = Field(default=50, ge=1, le=200)
skip_scored: bool = Field(default=True)
class AlertRunResponse(BaseModel):
"""Response für Scoring-Pipeline."""
processed: int
keep: int
drop: int
review: int
errors: int
duration_ms: int
class InboxItem(BaseModel):
"""Ein Item in der Inbox."""
id: str
title: str
url: str
snippet: Optional[str]
topic_label: str
published_at: Optional[datetime]
relevance_score: Optional[float]
relevance_decision: Optional[str]
relevance_summary: Optional[str]
status: str
class InboxResponse(BaseModel):
"""Response für Inbox-Abfrage."""
items: list[InboxItem]
total: int
page: int
page_size: int
class FeedbackRequest(BaseModel):
"""Request für Relevanz-Feedback."""
alert_id: str
is_relevant: bool
reason: Optional[str] = None
tags: list[str] = Field(default_factory=list)
class FeedbackResponse(BaseModel):
"""Response für Feedback."""
success: bool
message: str
profile_updated: bool
class ProfilePriorityRequest(BaseModel):
"""Priority für Profile-Update."""
label: str
weight: float = Field(default=0.5, ge=0.0, le=1.0)
keywords: list[str] = Field(default_factory=list)
description: Optional[str] = None
class ProfileUpdateRequest(BaseModel):
"""Request für Profile-Update."""
priorities: Optional[list[ProfilePriorityRequest]] = None
exclusions: Optional[list[str]] = None
policies: Optional[dict] = None
class ProfileResponse(BaseModel):
"""Response für Profile."""
id: str
priorities: list[dict]
exclusions: list[str]
policies: dict
total_scored: int
total_kept: int
total_dropped: int
accuracy_estimate: Optional[float]
# ============================================================================
# Endpoints
# ============================================================================
@@ -146,7 +52,7 @@ async def ingest_alert(request: AlertIngestRequest):
"""
Manuell einen Alert importieren.
Nützlich für Tests oder manuelles Hinzufügen von Artikeln.
Nuetzlich fuer Tests oder manuelles Hinzufuegen von Artikeln.
"""
alert = AlertItem(
title=request.title,
@@ -168,13 +74,13 @@ async def ingest_alert(request: AlertIngestRequest):
@router.post("/run", response_model=AlertRunResponse)
async def run_scoring_pipeline(request: AlertRunRequest):
"""
Scoring-Pipeline für neue Alerts starten.
Scoring-Pipeline fuer neue Alerts starten.
Bewertet alle unbewerteten Alerts und klassifiziert sie
in KEEP, DROP oder REVIEW.
Wenn ALERTS_USE_LLM=true, wird das LLM Gateway für Scoring verwendet.
Sonst wird ein schnelles Keyword-basiertes Scoring durchgeführt.
Wenn ALERTS_USE_LLM=true, wird das LLM Gateway fuer Scoring verwendet.
Sonst wird ein schnelles Keyword-basiertes Scoring durchgefuehrt.
"""
import time
start = time.time()
@@ -193,7 +99,7 @@ async def run_scoring_pipeline(request: AlertRunRequest):
keep = drop = review = errors = 0
# Profil für Scoring laden
# Profil fuer Scoring laden
profile = _profile_store.get("default")
if not profile:
profile = RelevanceProfile.create_default_education_profile()
@@ -201,7 +107,7 @@ async def run_scoring_pipeline(request: AlertRunRequest):
_profile_store["default"] = profile
if ALERTS_USE_LLM and LLM_API_KEY:
# LLM-basiertes Scoring über Gateway
# LLM-basiertes Scoring ueber Gateway
scorer = RelevanceScorer(
gateway_url=LLM_GATEWAY_URL,
api_key=LLM_API_KEY,
@@ -227,12 +133,12 @@ async def run_scoring_pipeline(request: AlertRunRequest):
snippet_lower = (alert.snippet or "").lower()
combined = title_lower + " " + snippet_lower
# Ausschlüsse aus Profil prüfen
# Ausschluesse aus Profil pruefen
if any(excl.lower() in combined for excl in profile.exclusions):
alert.relevance_score = 0.15
alert.relevance_decision = RelevanceDecision.DROP.value
drop += 1
# Prioritäten aus Profil prüfen
# Prioritaeten aus Profil pruefen
elif any(
p.label.lower() in combined or
any(kw.lower() in combined for kw in (p.keywords if hasattr(p, 'keywords') else []))
@@ -285,9 +191,9 @@ async def get_inbox(
# Pagination
total = len(alerts)
start = (page - 1) * page_size
end = start + page_size
page_alerts = alerts[start:end]
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
page_alerts = alerts[start_idx:end_idx]
items = [
InboxItem(
@@ -327,7 +233,7 @@ async def submit_feedback(request: FeedbackRequest):
# Alert Status aktualisieren
alert.status = AlertStatus.REVIEWED
# Profile aktualisieren (Default-Profile für Demo)
# Profile aktualisieren (Default-Profile fuer Demo)
profile = _profile_store.get("default")
if not profile:
profile = RelevanceProfile.create_default_education_profile()
@@ -353,7 +259,7 @@ async def get_profile(user_id: Optional[str] = Query(default=None)):
"""
Relevanz-Profil abrufen.
Ohne user_id wird das Default-Profil zurückgegeben.
Ohne user_id wird das Default-Profil zurueckgegeben.
"""
profile_id = user_id or "default"
profile = _profile_store.get(profile_id)
@@ -385,7 +291,7 @@ async def update_profile(
"""
Relevanz-Profil aktualisieren.
Erlaubt Anpassung von Prioritäten, Ausschlüssen und Policies.
Erlaubt Anpassung von Prioritaeten, Ausschluessen und Policies.
"""
profile_id = user_id or "default"
profile = _profile_store.get(profile_id)
@@ -431,34 +337,24 @@ async def update_profile(
@router.get("/stats")
async def get_stats():
"""
Statistiken über Alerts und Scoring.
Gibt Statistiken im Format zurück, das das Frontend erwartet:
- total_alerts, new_alerts, kept_alerts, review_alerts, dropped_alerts
- total_topics, active_topics, total_rules
Statistiken ueber Alerts und Scoring.
"""
alerts = list(_alerts_store.values())
total = len(alerts)
# Zähle nach Status und Decision
new_alerts = sum(1 for a in alerts if a.status == AlertStatus.NEW)
kept_alerts = sum(1 for a in alerts if a.relevance_decision == "KEEP")
review_alerts = sum(1 for a in alerts if a.relevance_decision == "REVIEW")
dropped_alerts = sum(1 for a in alerts if a.relevance_decision == "DROP")
# Topics und Rules (In-Memory hat diese nicht, aber wir geben 0 zurück)
# Bei DB-Implementierung würden wir hier die Repositories nutzen
total_topics = 0
active_topics = 0
total_rules = 0
# Versuche DB-Statistiken zu laden wenn verfügbar
try:
from alerts_agent.db import get_db
from alerts_agent.db.repository import TopicRepository, RuleRepository
from contextlib import contextmanager
# Versuche eine DB-Session zu bekommen
db_gen = get_db()
db = next(db_gen, None)
if db:
@@ -478,15 +374,12 @@ async def get_stats():
except StopIteration:
pass
except Exception:
# DB nicht verfügbar, nutze In-Memory Defaults
pass
# Berechne Durchschnittsscore
scored_alerts = [a for a in alerts if a.relevance_score is not None]
avg_score = sum(a.relevance_score for a in scored_alerts) / len(scored_alerts) if scored_alerts else 0.0
return {
# Frontend-kompatibles Format
"total_alerts": total,
"new_alerts": new_alerts,
"kept_alerts": kept_alerts,
@@ -496,7 +389,6 @@ async def get_stats():
"active_topics": active_topics,
"total_rules": total_rules,
"avg_score": avg_score,
# Zusätzliche Details (Abwärtskompatibilität)
"by_status": {
"new": new_alerts,
"scored": sum(1 for a in alerts if a.status == AlertStatus.SCORED),