Files
breakpilot-lehrer/backend-lehrer/alerts_agent/api/digests.py
Benjamin Admin bd4b956e3c [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>
2026-04-25 09:41:42 +02:00

247 lines
7.9 KiB
Python

"""
API Routes fuer Alert Digests (Wochenzusammenfassungen).
Endpoints:
- GET /digests - Liste aller Digests fuer den User
- GET /digests/{id} - Digest-Details
- GET /digests/{id}/pdf - PDF-Download
- POST /digests/generate - Digest manuell generieren
- POST /digests/{id}/send-email - Digest per E-Mail versenden
"""
import io
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session as DBSession
from ..db.database import get_db
from ..db.models import (
AlertDigestDB, UserAlertSubscriptionDB, DigestStatusEnum
)
from ..processing.digest_generator import DigestGenerator
from .digests_models import (
DigestDetail,
DigestListResponse,
GenerateDigestRequest,
GenerateDigestResponse,
SendEmailRequest,
SendEmailResponse,
digest_to_list_item,
digest_to_detail,
)
from .digests_email import generate_pdf_from_html, send_digest_by_email
router = APIRouter(prefix="/digests", tags=["digests"])
def get_user_id_from_request() -> str:
"""Extrahiert User-ID aus Request. TODO: JWT-Token auswerten."""
return "demo-user"
# ============================================================================
# Endpoints
# ============================================================================
@router.get("", response_model=DigestListResponse)
async def list_digests(
limit: int = Query(10, ge=1, le=50),
offset: int = Query(0, ge=0),
db: DBSession = Depends(get_db)
):
"""Liste alle Digests des aktuellen Users."""
user_id = get_user_id_from_request()
query = db.query(AlertDigestDB).filter(
AlertDigestDB.user_id == user_id
).order_by(AlertDigestDB.created_at.desc())
total = query.count()
digests = query.offset(offset).limit(limit).all()
return DigestListResponse(
digests=[digest_to_list_item(d) for d in digests],
total=total
)
@router.get("/latest", response_model=DigestDetail)
async def get_latest_digest(db: DBSession = Depends(get_db)):
"""Hole den neuesten Digest des Users."""
user_id = get_user_id_from_request()
digest = db.query(AlertDigestDB).filter(
AlertDigestDB.user_id == user_id
).order_by(AlertDigestDB.created_at.desc()).first()
if not digest:
raise HTTPException(status_code=404, detail="Kein Digest vorhanden")
return digest_to_detail(digest)
@router.get("/{digest_id}", response_model=DigestDetail)
async def get_digest(digest_id: str, db: DBSession = Depends(get_db)):
"""Hole Details eines spezifischen Digests."""
user_id = get_user_id_from_request()
digest = db.query(AlertDigestDB).filter(
AlertDigestDB.id == digest_id,
AlertDigestDB.user_id == user_id
).first()
if not digest:
raise HTTPException(status_code=404, detail="Digest nicht gefunden")
return digest_to_detail(digest)
@router.get("/{digest_id}/pdf")
async def get_digest_pdf(digest_id: str, db: DBSession = Depends(get_db)):
"""Generiere und lade PDF-Version des Digests herunter."""
user_id = get_user_id_from_request()
digest = db.query(AlertDigestDB).filter(
AlertDigestDB.id == digest_id,
AlertDigestDB.user_id == user_id
).first()
if not digest:
raise HTTPException(status_code=404, detail="Digest nicht gefunden")
if not digest.summary_html:
raise HTTPException(status_code=400, detail="Digest hat keinen Inhalt")
try:
pdf_bytes = await generate_pdf_from_html(digest.summary_html)
except Exception as e:
raise HTTPException(status_code=500, detail=f"PDF-Generierung fehlgeschlagen: {str(e)}")
filename = f"wochenbericht_{digest.period_start.strftime('%Y%m%d')}_{digest.period_end.strftime('%Y%m%d')}.pdf"
return StreamingResponse(
io.BytesIO(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.get("/latest/pdf")
async def get_latest_digest_pdf(db: DBSession = Depends(get_db)):
"""PDF des neuesten Digests herunterladen."""
user_id = get_user_id_from_request()
digest = db.query(AlertDigestDB).filter(
AlertDigestDB.user_id == user_id
).order_by(AlertDigestDB.created_at.desc()).first()
if not digest:
raise HTTPException(status_code=404, detail="Kein Digest vorhanden")
if not digest.summary_html:
raise HTTPException(status_code=400, detail="Digest hat keinen Inhalt")
try:
pdf_bytes = await generate_pdf_from_html(digest.summary_html)
except Exception as e:
raise HTTPException(status_code=500, detail=f"PDF-Generierung fehlgeschlagen: {str(e)}")
filename = f"wochenbericht_{digest.period_start.strftime('%Y%m%d')}_{digest.period_end.strftime('%Y%m%d')}.pdf"
return StreamingResponse(
io.BytesIO(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.post("/generate", response_model=GenerateDigestResponse)
async def generate_digest(
request: GenerateDigestRequest = None,
db: DBSession = Depends(get_db)
):
"""Generiere einen neuen Digest manuell."""
user_id = get_user_id_from_request()
weeks_back = request.weeks_back if request else 1
now = datetime.utcnow()
period_end = now - timedelta(days=now.weekday())
period_start = period_end - timedelta(weeks=weeks_back)
existing = db.query(AlertDigestDB).filter(
AlertDigestDB.user_id == user_id,
AlertDigestDB.period_start >= period_start - timedelta(days=1),
AlertDigestDB.period_end <= period_end + timedelta(days=1)
).first()
if existing and not (request and request.force_regenerate):
return GenerateDigestResponse(
status="exists", digest_id=existing.id,
message="Digest fuer diesen Zeitraum existiert bereits"
)
generator = DigestGenerator(db)
try:
digest = await generator.generate_weekly_digest(user_id, weeks_back)
if digest:
return GenerateDigestResponse(
status="success", digest_id=digest.id,
message="Digest erfolgreich generiert"
)
else:
return GenerateDigestResponse(
status="empty", digest_id=None,
message="Keine Alerts fuer diesen Zeitraum vorhanden"
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Fehler bei Digest-Generierung: {str(e)}")
@router.post("/{digest_id}/send-email", response_model=SendEmailResponse)
async def send_digest_email(
digest_id: str,
request: SendEmailRequest = None,
db: DBSession = Depends(get_db)
):
"""Versende Digest per E-Mail."""
user_id = get_user_id_from_request()
digest = db.query(AlertDigestDB).filter(
AlertDigestDB.id == digest_id,
AlertDigestDB.user_id == user_id
).first()
if not digest:
raise HTTPException(status_code=404, detail="Digest nicht gefunden")
email = None
if request and request.email:
email = request.email
else:
subscription = db.query(UserAlertSubscriptionDB).filter(
UserAlertSubscriptionDB.id == digest.subscription_id
).first()
if subscription:
email = subscription.notification_email
if not email:
raise HTTPException(status_code=400, detail="Keine E-Mail-Adresse angegeben")
try:
await send_digest_by_email(digest, email)
digest.status = DigestStatusEnum.SENT
digest.sent_at = datetime.utcnow()
db.commit()
return SendEmailResponse(
status="success", sent_to=email,
message="E-Mail erfolgreich versendet"
)
except Exception as e:
digest.status = DigestStatusEnum.FAILED
db.commit()
raise HTTPException(status_code=500, detail=f"E-Mail-Versand fehlgeschlagen: {str(e)}")