Files
breakpilot-lehrer/klausur-service/docs/BYOEH-Architecture.md
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

23 KiB

BYOEH (Bring-Your-Own-Expectation-Horizon) - Architecture Documentation

Overview

The BYOEH module enables teachers to upload their own Erwartungshorizonte (expectation horizons/grading rubrics) and use them for RAG-assisted grading suggestions. Key design principles:

  • Tenant Isolation: Each teacher/school has an isolated namespace
  • No Training Guarantee: EH content is only used for RAG, never for model training
  • Operator Blindness: Client-side encryption ensures Breakpilot cannot view plaintext
  • Rights Confirmation: Required legal acknowledgment at upload time

Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│                         klausur-service (Port 8086)                      │
├─────────────────────────────────────────────────────────────────────────┤
│  ┌────────────────────┐    ┌─────────────────────────────────────────┐  │
│  │   BYOEH REST API   │    │           BYOEH Service Layer           │  │
│  │                    │    │                                         │  │
│  │ POST /api/v1/eh    │───▶│ - Upload Wizard Logic                   │  │
│  │ GET /api/v1/eh     │    │ - Rights Confirmation                   │  │
│  │ DELETE /api/v1/eh  │    │ - Chunking Pipeline                     │  │
│  │ POST /rag-query    │    │ - Encryption Service                    │  │
│  └────────────────────┘    └────────────────────┬────────────────────┘  │
└─────────────────────────────────────────────────┼────────────────────────┘
                                                  │
          ┌───────────────────────────────────────┼───────────────────────┐
          │                                       │                       │
          ▼                                       ▼                       ▼
┌──────────────────────┐   ┌──────────────────────────┐   ┌──────────────────────┐
│     PostgreSQL       │   │        Qdrant            │   │  Encrypted Storage   │
│   (Metadata + Audit) │   │   (Vector Search)        │   │   /app/eh-uploads/   │
│                      │   │                          │   │                      │
│ In-Memory Storage:   │   │ Collection: bp_eh        │   │ {tenant}/{eh_id}/    │
│ - erwartungshorizonte│   │ - tenant_id (filter)     │   │   encrypted.bin      │
│ - eh_chunks          │   │ - eh_id                  │   │   salt.txt           │
│ - eh_key_shares      │   │ - embedding[1536]        │   │                      │
│ - eh_klausur_links   │   │ - encrypted_content      │   └──────────────────────┘
│ - eh_audit_log       │   │                          │
└──────────────────────┘   └──────────────────────────┘

Data Flow

1. Upload Flow

Browser                          Backend                        Storage
   │                               │                               │
   │ 1. User selects PDF          │                               │
   │ 2. User enters passphrase    │                               │
   │ 3. PBKDF2 key derivation     │                               │
   │ 4. AES-256-GCM encryption    │                               │
   │ 5. SHA-256 key hash          │                               │
   │                               │                               │
   │──────────────────────────────▶│                               │
   │ POST /api/v1/eh/upload        │                               │
   │ (encrypted blob + key_hash)   │                               │
   │                               │──────────────────────────────▶│
   │                               │ Store encrypted.bin + salt    │
   │                               │◀──────────────────────────────│
   │                               │                               │
   │                               │ Save metadata to DB           │
   │◀──────────────────────────────│                               │
   │ Return EH record              │                               │

2. Indexing Flow (RAG Preparation)

Browser                          Backend                        Qdrant
   │                               │                               │
   │──────────────────────────────▶│                               │
   │ POST /api/v1/eh/{id}/index    │                               │
   │ (passphrase for decryption)   │                               │
   │                               │                               │
   │                               │ 1. Verify key hash            │
   │                               │ 2. Decrypt content            │
   │                               │ 3. Extract text (PDF)         │
   │                               │ 4. Chunk text                 │
   │                               │ 5. Generate embeddings        │
   │                               │ 6. Re-encrypt each chunk      │
   │                               │──────────────────────────────▶│
   │                               │ Index vectors + encrypted     │
   │                               │ chunks with tenant filter     │
   │◀──────────────────────────────│                               │
   │ Return chunk count            │                               │

3. RAG Query Flow

Browser                          Backend                        Qdrant
   │                               │                               │
   │──────────────────────────────▶│                               │
   │ POST /api/v1/eh/rag-query     │                               │
   │ (query + passphrase)          │                               │
   │                               │                               │
   │                               │ 1. Generate query embedding   │
   │                               │──────────────────────────────▶│
   │                               │ 2. Semantic search            │
   │                               │    (tenant-filtered)          │
   │                               │◀──────────────────────────────│
   │                               │ 3. Decrypt matched chunks     │
   │◀──────────────────────────────│                               │
   │ Return decrypted context      │                               │

Security Architecture

Client-Side Encryption

┌─────────────────────────────────────────────────────────────────┐
│                    Browser (Client-Side)                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. User enters passphrase (NEVER sent to server)               │
│     │                                                           │
│     ▼                                                           │
│  2. Key Derivation: PBKDF2-SHA256(passphrase, salt, 100k iter)  │
│     │                                                           │
│     ▼                                                           │
│  3. Encryption: AES-256-GCM(key, iv, file_content)              │
│     │                                                           │
│     ▼                                                           │
│  4. Key-Hash: SHA-256(derived_key) → server verification only   │
│     │                                                           │
│     ▼                                                           │
│  5. Upload: encrypted_blob + key_hash + salt (NOT key!)         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Security Guarantees

Guarantee Implementation
No Training training_allowed: false on all Qdrant points
Operator Blindness Passphrase never leaves browser; server only sees key hash
Tenant Isolation Every query filtered by tenant_id
Audit Trail All actions logged with timestamps

Key Sharing System

The key sharing system enables first examiners to grant access to their EH to second examiners and supervisors.

Share Flow

First Examiner                   Backend                    Second Examiner
      │                             │                             │
      │ 1. Encrypt passphrase for   │                             │
      │    recipient (client-side)  │                             │
      │                             │                             │
      │─────────────────────────────▶                             │
      │ POST /eh/{id}/share         │                             │
      │ (encrypted_passphrase, role)│                             │
      │                             │                             │
      │                             │ Store EHKeyShare            │
      │◀─────────────────────────────                             │
      │                             │                             │
      │                             │                             │
      │                             │◀────────────────────────────│
      │                             │ GET /eh/shared-with-me      │
      │                             │                             │
      │                             │─────────────────────────────▶
      │                             │ Return shared EH list       │
      │                             │                             │
      │                             │◀────────────────────────────│
      │                             │ RAG query with decrypted    │
      │                             │ passphrase                  │

Data Structures

@dataclass
class EHKeyShare:
    id: str
    eh_id: str
    user_id: str                    # Recipient
    encrypted_passphrase: str       # Client-encrypted for recipient
    passphrase_hint: str            # Optional hint
    granted_by: str                 # Grantor user ID
    granted_at: datetime
    role: str                       # second_examiner, third_examiner, supervisor
    klausur_id: Optional[str]       # Link to specific Klausur
    active: bool

@dataclass
class EHKlausurLink:
    id: str
    eh_id: str
    klausur_id: str
    linked_by: str
    linked_at: datetime

API Endpoints

Core EH Endpoints

Method Endpoint Description
POST /api/v1/eh/upload Upload encrypted EH
GET /api/v1/eh List user's EH
GET /api/v1/eh/{id} Get single EH
DELETE /api/v1/eh/{id} Soft delete EH
POST /api/v1/eh/{id}/index Index EH for RAG
POST /api/v1/eh/rag-query Query EH content

Key Sharing Endpoints

Method Endpoint Description
POST /api/v1/eh/{id}/share Share EH with examiner
GET /api/v1/eh/{id}/shares List shares (owner)
DELETE /api/v1/eh/{id}/shares/{shareId} Revoke share
GET /api/v1/eh/shared-with-me List EH shared with user

Klausur Integration Endpoints

Method Endpoint Description
POST /api/v1/eh/{id}/link-klausur Link EH to Klausur
DELETE /api/v1/eh/{id}/link-klausur/{klausurId} Unlink EH
GET /api/v1/klausuren/{id}/linked-eh Get linked EH for Klausur

Audit & Admin Endpoints

Method Endpoint Description
GET /api/v1/eh/audit-log Get audit log
GET /api/v1/eh/rights-text Get rights confirmation text
GET /api/v1/eh/qdrant-status Get Qdrant status (admin)

Frontend Components

EHUploadWizard

5-step wizard for uploading Erwartungshorizonte:

  1. File Selection - Choose PDF file
  2. Metadata - Title, Subject, Niveau, Year
  3. Rights Confirmation - Legal acknowledgment
  4. Encryption - Set passphrase (2x confirmation)
  5. Summary - Review and upload

Integration Points

  • KorrekturPage: Shows EH prompt after first student upload
  • GutachtenGeneration: Uses RAG context from linked EH
  • Sidebar Badge: Shows linked EH count

File Structure

klausur-service/
├── backend/
│   ├── main.py              # API endpoints + data structures
│   ├── qdrant_service.py    # Vector database operations
│   ├── eh_pipeline.py       # Chunking, embedding, encryption
│   └── requirements.txt     # Python dependencies
├── frontend/
│   └── src/
│       ├── components/
│       │   └── EHUploadWizard.tsx
│       ├── services/
│       │   ├── api.ts       # API client
│       │   └── encryption.ts # Client-side crypto
│       ├── pages/
│       │   └── KorrekturPage.tsx # EH integration
│       └── styles/
│           └── eh-wizard.css
└── docs/
    ├── BYOEH-Architecture.md
    └── BYOEH-Developer-Guide.md

Configuration

Environment Variables

QDRANT_URL=http://qdrant:6333
OPENAI_API_KEY=sk-...              # For embeddings
BYOEH_ENCRYPTION_ENABLED=true
EH_UPLOAD_DIR=/app/eh-uploads

Docker Services

# docker-compose.yml
services:
  qdrant:
    image: qdrant/qdrant:v1.7.4
    ports:
      - "6333:6333"
    volumes:
      - qdrant_data:/qdrant/storage

Audit Events

Action Description
upload EH uploaded
index EH indexed for RAG
rag_query RAG query executed
delete EH soft deleted
share EH shared with examiner
revoke_share Share revoked
link_klausur EH linked to Klausur
unlink_klausur EH unlinked from Klausur

RBAC Extensions for Zeugnis System

The RBAC system has been extended to support the Zeugnis (Certificate) workflow. This enables role-based access control for certificate generation, approval, and management.

class Role(str, Enum):
    # Existing exam roles
    ERSTPRUEFER = "erstpruefer"          # First examiner
    ZWEITPRUEFER = "zweitpruefer"        # Second examiner
    DRITTPRUEFER = "drittpruefer"        # Third examiner
    FACHVORSITZ = "fachvorsitz"          # Subject chair

    # Certificate workflow roles
    FACHLEHRER = "fachlehrer"            # Subject teacher - enters grades
    KLASSENLEHRER = "klassenlehrer"      # Class teacher - approves grades
    ZEUGNISBEAUFTRAGTER = "zeugnisbeauftragter"  # Certificate coordinator
    SCHULLEITUNG = "schulleitung"        # Principal - final sign-off
    SEKRETARIAT = "sekretariat"          # Secretary - printing

Certificate Resource Types

class ResourceType(str, Enum):
    # Existing types
    KLAUSUR = "klausur"
    ERWARTUNGSHORIZONT = "erwartungshorizont"

    # Certificate types
    ZEUGNIS = "zeugnis"                  # Final certificate
    ZEUGNIS_VORLAGE = "zeugnis_vorlage"  # Certificate template
    ZEUGNIS_ENTWURF = "zeugnis_entwurf"  # Draft certificate
    FACHNOTE = "fachnote"                # Subject grade
    KOPFNOTE = "kopfnote"                # Head grade (Arbeits-/Sozialverhalten)
    BEMERKUNG = "bemerkung"              # Certificate remarks
    STATISTIK = "statistik"              # Class/subject statistics
    NOTENSPIEGEL = "notenspiegel"        # Grade distribution

VerfahrenType Extension

class VerfahrenType(str, Enum):
    # Exam types
    ABITUR = "abitur"
    KLAUSUR = "klausur"
    NACHSCHREIBKLAUSUR = "nachschreibklausur"

    # Certificate types (NEW)
    HALBJAHRESZEUGNIS = "halbjahreszeugnis"    # Mid-year certificate
    JAHRESZEUGNIS = "jahreszeugnis"            # End-of-year certificate
    ABSCHLUSSZEUGNIS = "abschlusszeugnis"      # Graduation certificate
    ABGANGSZEUGNIS = "abgangszeugnis"          # Leaving certificate

    @classmethod
    def is_certificate_type(cls, verfahren: "VerfahrenType") -> bool:
        """Check if this is a certificate type (not an exam)."""
        cert_types = {
            cls.HALBJAHRESZEUGNIS,
            cls.JAHRESZEUGNIS,
            cls.ABSCHLUSSZEUGNIS,
            cls.ABGANGSZEUGNIS
        }
        return verfahren in cert_types

Certificate Workflow Permissions

┌───────────────────┐    ┌───────────────────┐    ┌───────────────────────┐
│   FACHLEHRER      │───▶│   KLASSENLEHRER   │───▶│  ZEUGNISBEAUFTRAGTER  │
│                   │    │                   │    │                       │
│ FACHNOTE: CRUD    │    │ ZEUGNIS: CRU      │    │ ZEUGNIS: RU           │
│ ZEUGNIS_ENTWURF:R │    │ ZEUGNIS_ENTWURF:  │    │ ZEUGNIS_VORLAGE: RUU  │
│                   │    │   CRUD            │    │                       │
└───────────────────┘    └───────────────────┘    └───────────────────────┘
                                                            │
                                                            ▼
                         ┌───────────────────┐    ┌───────────────────────┐
                         │    SEKRETARIAT    │◀───│     SCHULLEITUNG      │
                         │                   │    │                       │
                         │ ZEUGNIS: RD       │    │ ZEUGNIS: R/SIGN/LOCK  │
                         │ (Print & Archive) │    │ (Final Approval)      │
                         └───────────────────┘    └───────────────────────┘

DEFAULT_PERMISSIONS for Certificate Roles

DEFAULT_PERMISSIONS = {
    # ... existing roles ...

    Role.KLASSENLEHRER: {
        ResourceType.KLASSE: {Action.READ, Action.UPDATE},
        ResourceType.SCHUELER: {Action.READ, Action.CREATE, Action.UPDATE},
        ResourceType.FACHNOTE: {Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE},
        ResourceType.ZEUGNIS: {Action.CREATE, Action.READ, Action.UPDATE},
        ResourceType.ZEUGNIS_ENTWURF: {Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE},
        ResourceType.ZEUGNIS_VORLAGE: {Action.READ},
        ResourceType.KOPFNOTE: {Action.CREATE, Action.READ, Action.UPDATE},
        ResourceType.BEMERKUNG: {Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE},
        ResourceType.STATISTIK: {Action.READ},
        ResourceType.NOTENSPIEGEL: {Action.READ},
    },

    Role.ZEUGNISBEAUFTRAGTER: {
        ResourceType.KLASSE: {Action.READ},
        ResourceType.ZEUGNIS: {Action.READ, Action.UPDATE},
        ResourceType.ZEUGNIS_ENTWURF: {Action.READ, Action.UPDATE},
        ResourceType.ZEUGNIS_VORLAGE: {Action.READ, Action.UPDATE, Action.UPLOAD},
        ResourceType.STATISTIK: {Action.READ},
        ResourceType.NOTENSPIEGEL: {Action.READ},
    },

    Role.SEKRETARIAT: {
        ResourceType.ZEUGNIS: {Action.READ, Action.DOWNLOAD},
        ResourceType.ZEUGNIS_VORLAGE: {Action.READ},
    },

    Role.SCHULLEITUNG: {
        ResourceType.ZEUGNIS: {Action.READ, Action.SIGN_OFF, Action.LOCK},
        ResourceType.ZEUGNIS_ENTWURF: {Action.READ, Action.UPDATE},
        ResourceType.ZEUGNIS_VORLAGE: {Action.READ, Action.UPDATE},
        ResourceType.STATISTIK: {Action.READ},
        ResourceType.NOTENSPIEGEL: {Action.READ},
    },

    Role.FACHLEHRER: {
        ResourceType.FACHNOTE: {Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE},
        ResourceType.ZEUGNIS: {Action.READ, Action.UPDATE},
        ResourceType.ZEUGNIS_ENTWURF: {Action.READ, Action.UPDATE},
        ResourceType.STATISTIK: {Action.READ},
        ResourceType.NOTENSPIEGEL: {Action.READ},
    },
}

See Also

For complete Zeugnis system documentation including:

  • Full workflow diagrams
  • Statistics API endpoints
  • Frontend components
  • Seed data generator

See: docs/architecture/zeugnis-system.md