[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:
@@ -16,11 +16,9 @@ Unterstützt:
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
from enum import Enum
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
||||
from pydantic import BaseModel, Field
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from generators import (
|
||||
MultipleChoiceGenerator,
|
||||
@@ -28,9 +26,22 @@ from generators import (
|
||||
MindmapGenerator,
|
||||
QuizGenerator
|
||||
)
|
||||
from generators.mc_generator import Difficulty
|
||||
from generators.cloze_generator import ClozeType
|
||||
from generators.quiz_generator import QuizType
|
||||
|
||||
from worksheets_models import (
|
||||
ContentType,
|
||||
GenerateRequest,
|
||||
MCGenerateRequest,
|
||||
ClozeGenerateRequest,
|
||||
MindmapGenerateRequest,
|
||||
QuizGenerateRequest,
|
||||
BatchGenerateRequest,
|
||||
WorksheetContent,
|
||||
GenerateResponse,
|
||||
BatchGenerateResponse,
|
||||
parse_difficulty,
|
||||
parse_cloze_type,
|
||||
parse_quiz_types,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,89 +51,6 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Pydantic Models
|
||||
# ============================================================================
|
||||
|
||||
class ContentType(str, Enum):
|
||||
"""Verfügbare Content-Typen."""
|
||||
MULTIPLE_CHOICE = "multiple_choice"
|
||||
CLOZE = "cloze"
|
||||
MINDMAP = "mindmap"
|
||||
QUIZ = "quiz"
|
||||
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
"""Basis-Request für Generierung."""
|
||||
source_text: str = Field(..., min_length=50, description="Quelltext für Generierung")
|
||||
topic: Optional[str] = Field(None, description="Thema/Titel")
|
||||
subject: Optional[str] = Field(None, description="Fach")
|
||||
grade_level: Optional[str] = Field(None, description="Klassenstufe")
|
||||
|
||||
|
||||
class MCGenerateRequest(GenerateRequest):
|
||||
"""Request für Multiple-Choice-Generierung."""
|
||||
num_questions: int = Field(5, ge=1, le=20, description="Anzahl Fragen")
|
||||
difficulty: str = Field("medium", description="easy, medium, hard")
|
||||
|
||||
|
||||
class ClozeGenerateRequest(GenerateRequest):
|
||||
"""Request für Lückentext-Generierung."""
|
||||
num_gaps: int = Field(5, ge=1, le=15, description="Anzahl Lücken")
|
||||
difficulty: str = Field("medium", description="easy, medium, hard")
|
||||
cloze_type: str = Field("fill_in", description="fill_in, drag_drop, dropdown")
|
||||
|
||||
|
||||
class MindmapGenerateRequest(GenerateRequest):
|
||||
"""Request für Mindmap-Generierung."""
|
||||
max_depth: int = Field(3, ge=2, le=5, description="Maximale Tiefe")
|
||||
|
||||
|
||||
class QuizGenerateRequest(GenerateRequest):
|
||||
"""Request für Quiz-Generierung."""
|
||||
quiz_types: List[str] = Field(
|
||||
["true_false", "matching"],
|
||||
description="Typen: true_false, matching, sorting, open_ended"
|
||||
)
|
||||
num_items: int = Field(5, ge=1, le=10, description="Items pro Typ")
|
||||
difficulty: str = Field("medium", description="easy, medium, hard")
|
||||
|
||||
|
||||
class BatchGenerateRequest(BaseModel):
|
||||
"""Request für Batch-Generierung mehrerer Content-Typen."""
|
||||
source_text: str = Field(..., min_length=50)
|
||||
content_types: List[str] = Field(..., description="Liste von Content-Typen")
|
||||
topic: Optional[str] = None
|
||||
subject: Optional[str] = None
|
||||
grade_level: Optional[str] = None
|
||||
difficulty: str = "medium"
|
||||
|
||||
|
||||
class WorksheetContent(BaseModel):
|
||||
"""Generierter Content."""
|
||||
id: str
|
||||
content_type: str
|
||||
data: Dict[str, Any]
|
||||
h5p_format: Optional[Dict[str, Any]] = None
|
||||
created_at: datetime
|
||||
topic: Optional[str] = None
|
||||
difficulty: Optional[str] = None
|
||||
|
||||
|
||||
class GenerateResponse(BaseModel):
|
||||
"""Response mit generiertem Content."""
|
||||
success: bool
|
||||
content: Optional[WorksheetContent] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class BatchGenerateResponse(BaseModel):
|
||||
"""Response für Batch-Generierung."""
|
||||
success: bool
|
||||
contents: List[WorksheetContent] = []
|
||||
errors: List[str] = []
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# In-Memory Storage (später durch DB ersetzen)
|
||||
# ============================================================================
|
||||
@@ -134,49 +62,12 @@ _generated_content: Dict[str, WorksheetContent] = {}
|
||||
# Generator Instances
|
||||
# ============================================================================
|
||||
|
||||
# Generatoren ohne LLM-Client (automatische Generierung)
|
||||
# In Produktion würde hier der LLM-Client injiziert
|
||||
mc_generator = MultipleChoiceGenerator()
|
||||
cloze_generator = ClozeGenerator()
|
||||
mindmap_generator = MindmapGenerator()
|
||||
quiz_generator = QuizGenerator()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
def _parse_difficulty(difficulty_str: str) -> Difficulty:
|
||||
"""Konvertiert String zu Difficulty Enum."""
|
||||
mapping = {
|
||||
"easy": Difficulty.EASY,
|
||||
"medium": Difficulty.MEDIUM,
|
||||
"hard": Difficulty.HARD
|
||||
}
|
||||
return mapping.get(difficulty_str.lower(), Difficulty.MEDIUM)
|
||||
|
||||
|
||||
def _parse_cloze_type(type_str: str) -> ClozeType:
|
||||
"""Konvertiert String zu ClozeType Enum."""
|
||||
mapping = {
|
||||
"fill_in": ClozeType.FILL_IN,
|
||||
"drag_drop": ClozeType.DRAG_DROP,
|
||||
"dropdown": ClozeType.DROPDOWN
|
||||
}
|
||||
return mapping.get(type_str.lower(), ClozeType.FILL_IN)
|
||||
|
||||
|
||||
def _parse_quiz_types(type_strs: List[str]) -> List[QuizType]:
|
||||
"""Konvertiert String-Liste zu QuizType Enums."""
|
||||
mapping = {
|
||||
"true_false": QuizType.TRUE_FALSE,
|
||||
"matching": QuizType.MATCHING,
|
||||
"sorting": QuizType.SORTING,
|
||||
"open_ended": QuizType.OPEN_ENDED
|
||||
}
|
||||
return [mapping.get(t.lower(), QuizType.TRUE_FALSE) for t in type_strs]
|
||||
|
||||
|
||||
def _store_content(content: WorksheetContent) -> None:
|
||||
"""Speichert generierten Content."""
|
||||
_generated_content[content.id] = content
|
||||
@@ -188,15 +79,9 @@ def _store_content(content: WorksheetContent) -> None:
|
||||
|
||||
@router.post("/generate/multiple-choice", response_model=GenerateResponse)
|
||||
async def generate_multiple_choice(request: MCGenerateRequest):
|
||||
"""
|
||||
Generiert Multiple-Choice-Fragen aus Quelltext.
|
||||
|
||||
- **source_text**: Text mit mind. 50 Zeichen
|
||||
- **num_questions**: Anzahl Fragen (1-20)
|
||||
- **difficulty**: easy, medium, hard
|
||||
"""
|
||||
"""Generiert Multiple-Choice-Fragen aus Quelltext."""
|
||||
try:
|
||||
difficulty = _parse_difficulty(request.difficulty)
|
||||
difficulty = parse_difficulty(request.difficulty)
|
||||
|
||||
questions = mc_generator.generate(
|
||||
source_text=request.source_text,
|
||||
@@ -212,7 +97,6 @@ async def generate_multiple_choice(request: MCGenerateRequest):
|
||||
error="Keine Fragen generiert. Text möglicherweise zu kurz."
|
||||
)
|
||||
|
||||
# Konvertiere zu Dict
|
||||
questions_dict = mc_generator.to_dict(questions)
|
||||
h5p_format = mc_generator.to_h5p_format(questions)
|
||||
|
||||
@@ -227,7 +111,6 @@ async def generate_multiple_choice(request: MCGenerateRequest):
|
||||
)
|
||||
|
||||
_store_content(content)
|
||||
|
||||
return GenerateResponse(success=True, content=content)
|
||||
|
||||
except Exception as e:
|
||||
@@ -237,15 +120,9 @@ async def generate_multiple_choice(request: MCGenerateRequest):
|
||||
|
||||
@router.post("/generate/cloze", response_model=GenerateResponse)
|
||||
async def generate_cloze(request: ClozeGenerateRequest):
|
||||
"""
|
||||
Generiert Lückentext aus Quelltext.
|
||||
|
||||
- **source_text**: Text mit mind. 50 Zeichen
|
||||
- **num_gaps**: Anzahl Lücken (1-15)
|
||||
- **cloze_type**: fill_in, drag_drop, dropdown
|
||||
"""
|
||||
"""Generiert Lückentext aus Quelltext."""
|
||||
try:
|
||||
cloze_type = _parse_cloze_type(request.cloze_type)
|
||||
cloze_type = parse_cloze_type(request.cloze_type)
|
||||
|
||||
cloze = cloze_generator.generate(
|
||||
source_text=request.source_text,
|
||||
@@ -275,7 +152,6 @@ async def generate_cloze(request: ClozeGenerateRequest):
|
||||
)
|
||||
|
||||
_store_content(content)
|
||||
|
||||
return GenerateResponse(success=True, content=content)
|
||||
|
||||
except Exception as e:
|
||||
@@ -285,12 +161,7 @@ async def generate_cloze(request: ClozeGenerateRequest):
|
||||
|
||||
@router.post("/generate/mindmap", response_model=GenerateResponse)
|
||||
async def generate_mindmap(request: MindmapGenerateRequest):
|
||||
"""
|
||||
Generiert Mindmap aus Quelltext.
|
||||
|
||||
- **source_text**: Text mit mind. 50 Zeichen
|
||||
- **max_depth**: Maximale Tiefe (2-5)
|
||||
"""
|
||||
"""Generiert Mindmap aus Quelltext."""
|
||||
try:
|
||||
mindmap = mindmap_generator.generate(
|
||||
source_text=request.source_text,
|
||||
@@ -317,14 +188,13 @@ async def generate_mindmap(request: MindmapGenerateRequest):
|
||||
"mermaid": mermaid,
|
||||
"json_tree": json_tree
|
||||
},
|
||||
h5p_format=None, # Mindmaps haben kein H5P-Format
|
||||
h5p_format=None,
|
||||
created_at=datetime.utcnow(),
|
||||
topic=request.topic,
|
||||
difficulty=None
|
||||
)
|
||||
|
||||
_store_content(content)
|
||||
|
||||
return GenerateResponse(success=True, content=content)
|
||||
|
||||
except Exception as e:
|
||||
@@ -334,17 +204,10 @@ async def generate_mindmap(request: MindmapGenerateRequest):
|
||||
|
||||
@router.post("/generate/quiz", response_model=GenerateResponse)
|
||||
async def generate_quiz(request: QuizGenerateRequest):
|
||||
"""
|
||||
Generiert Quiz mit verschiedenen Fragetypen.
|
||||
|
||||
- **source_text**: Text mit mind. 50 Zeichen
|
||||
- **quiz_types**: Liste von true_false, matching, sorting, open_ended
|
||||
- **num_items**: Items pro Typ (1-10)
|
||||
"""
|
||||
"""Generiert Quiz mit verschiedenen Fragetypen."""
|
||||
try:
|
||||
quiz_types = _parse_quiz_types(request.quiz_types)
|
||||
quiz_types = parse_quiz_types(request.quiz_types)
|
||||
|
||||
# Generate quiz for each type and combine results
|
||||
all_questions = []
|
||||
quizzes = []
|
||||
|
||||
@@ -365,7 +228,6 @@ async def generate_quiz(request: QuizGenerateRequest):
|
||||
error="Quiz konnte nicht generiert werden. Text möglicherweise zu kurz."
|
||||
)
|
||||
|
||||
# Combine all quizzes into a single dict
|
||||
combined_quiz_dict = {
|
||||
"quiz_types": [qt.value for qt in quiz_types],
|
||||
"title": f"Combined Quiz - {request.topic or 'Various Topics'}",
|
||||
@@ -374,12 +236,10 @@ async def generate_quiz(request: QuizGenerateRequest):
|
||||
"questions": []
|
||||
}
|
||||
|
||||
# Add questions from each quiz
|
||||
for quiz in quizzes:
|
||||
quiz_dict = quiz_generator.to_dict(quiz)
|
||||
combined_quiz_dict["questions"].extend(quiz_dict.get("questions", []))
|
||||
|
||||
# Use first quiz's H5P format as base (or empty if none)
|
||||
h5p_format = quiz_generator.to_h5p_format(quizzes[0]) if quizzes else {}
|
||||
|
||||
content = WorksheetContent(
|
||||
@@ -393,7 +253,6 @@ async def generate_quiz(request: QuizGenerateRequest):
|
||||
)
|
||||
|
||||
_store_content(content)
|
||||
|
||||
return GenerateResponse(success=True, content=content)
|
||||
|
||||
except Exception as e:
|
||||
@@ -403,22 +262,10 @@ async def generate_quiz(request: QuizGenerateRequest):
|
||||
|
||||
@router.post("/generate/batch", response_model=BatchGenerateResponse)
|
||||
async def generate_batch(request: BatchGenerateRequest):
|
||||
"""
|
||||
Generiert mehrere Content-Typen aus einem Quelltext.
|
||||
|
||||
Ideal für die Erstellung kompletter Arbeitsblätter mit
|
||||
verschiedenen Übungstypen.
|
||||
"""
|
||||
"""Generiert mehrere Content-Typen aus einem Quelltext."""
|
||||
contents = []
|
||||
errors = []
|
||||
|
||||
type_mapping = {
|
||||
"multiple_choice": MCGenerateRequest,
|
||||
"cloze": ClozeGenerateRequest,
|
||||
"mindmap": MindmapGenerateRequest,
|
||||
"quiz": QuizGenerateRequest
|
||||
}
|
||||
|
||||
for content_type in request.content_types:
|
||||
try:
|
||||
if content_type == "multiple_choice":
|
||||
|
||||
Reference in New Issue
Block a user