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>
247 lines
7.9 KiB
Python
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)}")
|