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>
469 lines
23 KiB
Markdown
469 lines
23 KiB
Markdown
# 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
|
|
|
|
```python
|
|
@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
|
|
|
|
```env
|
|
QDRANT_URL=http://qdrant:6333
|
|
OPENAI_API_KEY=sk-... # For embeddings
|
|
BYOEH_ENCRYPTION_ENABLED=true
|
|
EH_UPLOAD_DIR=/app/eh-uploads
|
|
```
|
|
|
|
### Docker Services
|
|
|
|
```yaml
|
|
# 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.
|
|
|
|
### Certificate-Related Roles
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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](../../docs/architecture/zeugnis-system.md)
|