This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/geo-service/api/learning.py
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

290 lines
10 KiB
Python

"""
Learning Nodes API Endpoints
Generates and manages educational content for AOI regions
"""
from fastapi import APIRouter, HTTPException, Path, Query, BackgroundTasks
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from typing import Optional
import structlog
from config import settings
from models.learning_node import LearningNode, LearningNodeRequest, LearningTheme
from services.learning_generator import LearningGeneratorService
logger = structlog.get_logger(__name__)
router = APIRouter()
# Initialize learning generator service
learning_service = LearningGeneratorService()
class GenerateNodesRequest(BaseModel):
"""Request model for generating learning nodes."""
aoi_id: str = Field(..., description="AOI UUID to generate nodes for")
theme: LearningTheme = Field(LearningTheme.TOPOGRAPHIE, description="Learning theme")
difficulty: str = Field("mittel", pattern="^(leicht|mittel|schwer)$", description="Difficulty level")
node_count: int = Field(5, ge=1, le=20, description="Number of nodes to generate")
grade_level: Optional[str] = Field(None, description="Target grade level (e.g., '5-6', '7-8')")
language: str = Field("de", description="Content language")
class GenerateNodesResponse(BaseModel):
"""Response model for generated learning nodes."""
aoi_id: str
theme: str
nodes: list[LearningNode]
total_count: int
generation_model: str
@router.post("/generate", response_model=GenerateNodesResponse)
async def generate_learning_nodes(
request: GenerateNodesRequest,
):
"""
Generate learning nodes for an AOI using LLM.
Uses Ollama with the configured model to generate educational content
based on the AOI's geographic features and selected theme.
Themes:
- topographie: Landforms, elevation, terrain features
- landnutzung: Land use, settlement patterns, agriculture
- orientierung: Navigation, compass, map reading
- geologie: Rock types, geological formations
- hydrologie: Water features, drainage, watersheds
- vegetation: Plant communities, climate zones
"""
try:
nodes = await learning_service.generate_nodes(
aoi_id=request.aoi_id,
theme=request.theme,
difficulty=request.difficulty,
node_count=request.node_count,
grade_level=request.grade_level,
language=request.language,
)
logger.info(
"Learning nodes generated",
aoi_id=request.aoi_id,
theme=request.theme.value,
count=len(nodes),
)
return GenerateNodesResponse(
aoi_id=request.aoi_id,
theme=request.theme.value,
nodes=nodes,
total_count=len(nodes),
generation_model=settings.ollama_model,
)
except FileNotFoundError:
raise HTTPException(
status_code=404,
detail="AOI not found. Please create an AOI first.",
)
except ConnectionError:
raise HTTPException(
status_code=503,
detail="LLM service (Ollama) not available. Please check if Ollama is running.",
)
except Exception as e:
logger.error("Error generating learning nodes", error=str(e))
raise HTTPException(status_code=500, detail="Error generating learning nodes")
@router.get("/templates")
async def get_learning_templates():
"""
Get available learning theme templates.
Returns theme definitions with descriptions, suitable grade levels,
and example questions.
"""
return {
"themes": [
{
"id": "topographie",
"name": "Topographie",
"description": "Landschaftsformen, Höhen und Geländemerkmale",
"icon": "mountain",
"grade_levels": ["5-6", "7-8", "9-10"],
"example_questions": [
"Welche Höhe hat der höchste Punkt in diesem Gebiet?",
"Beschreibe die Hangneigung im nördlichen Bereich.",
"Wo befinden sich Täler und wo Bergrücken?",
],
"keywords": ["Höhe", "Hang", "Tal", "Berg", "Hügel", "Ebene"],
},
{
"id": "landnutzung",
"name": "Landnutzung",
"description": "Siedlungen, Landwirtschaft und Flächennutzung",
"icon": "home",
"grade_levels": ["5-6", "7-8", "9-10", "11-12"],
"example_questions": [
"Welche Arten von Gebäuden sind in diesem Bereich zu finden?",
"Wie viel Prozent der Fläche wird landwirtschaftlich genutzt?",
"Wo verläuft die Grenze zwischen Siedlung und Naturraum?",
],
"keywords": ["Siedlung", "Acker", "Wald", "Industrie", "Straße"],
},
{
"id": "orientierung",
"name": "Orientierung",
"description": "Kartenlesen, Kompass und Navigation",
"icon": "compass",
"grade_levels": ["5-6", "7-8"],
"example_questions": [
"In welcher Himmelsrichtung liegt der See?",
"Wie lang ist der Weg von A nach B?",
"Beschreibe den Weg vom Parkplatz zum Aussichtspunkt.",
],
"keywords": ["Norden", "Süden", "Entfernung", "Maßstab", "Legende"],
},
{
"id": "geologie",
"name": "Geologie",
"description": "Gesteinsarten und geologische Formationen",
"icon": "layers",
"grade_levels": ["7-8", "9-10", "11-12"],
"example_questions": [
"Welches Gestein dominiert in diesem Gebiet?",
"Wie sind die Felsformationen entstanden?",
"Welche Rolle spielt die Geologie für die Landschaft?",
],
"keywords": ["Gestein", "Formation", "Erosion", "Schicht", "Mineral"],
},
{
"id": "hydrologie",
"name": "Hydrologie",
"description": "Gewässer, Einzugsgebiete und Wasserkreislauf",
"icon": "droplet",
"grade_levels": ["5-6", "7-8", "9-10"],
"example_questions": [
"Wohin fließt das Wasser in diesem Gebiet?",
"Welche Gewässerarten sind vorhanden?",
"Wo könnte sich bei Starkregen Wasser sammeln?",
],
"keywords": ["Fluss", "See", "Bach", "Einzugsgebiet", "Quelle"],
},
{
"id": "vegetation",
"name": "Vegetation",
"description": "Pflanzengemeinschaften und Klimazonen",
"icon": "tree",
"grade_levels": ["5-6", "7-8", "9-10", "11-12"],
"example_questions": [
"Welche Waldtypen sind in diesem Gebiet zu finden?",
"Wie beeinflusst die Höhenlage die Vegetation?",
"Welche Pflanzen würdest du hier erwarten?",
],
"keywords": ["Wald", "Wiese", "Laubbaum", "Nadelbaum", "Höhenstufe"],
},
],
"difficulties": [
{"id": "leicht", "name": "Leicht", "description": "Grundlegende Beobachtungen"},
{"id": "mittel", "name": "Mittel", "description": "Verknüpfung von Zusammenhängen"},
{"id": "schwer", "name": "Schwer", "description": "Analyse und Transfer"},
],
"supported_languages": ["de", "en"],
}
@router.get("/{aoi_id}/nodes")
async def get_aoi_learning_nodes(
aoi_id: str = Path(..., description="AOI UUID"),
theme: Optional[LearningTheme] = Query(None, description="Filter by theme"),
):
"""
Get all learning nodes for an AOI.
Returns previously generated nodes, optionally filtered by theme.
"""
nodes = await learning_service.get_nodes_for_aoi(aoi_id, theme)
if nodes is None:
raise HTTPException(status_code=404, detail="AOI not found")
return {
"aoi_id": aoi_id,
"nodes": nodes,
"total_count": len(nodes),
}
@router.put("/{aoi_id}/nodes/{node_id}")
async def update_learning_node(
aoi_id: str = Path(..., description="AOI UUID"),
node_id: str = Path(..., description="Node UUID"),
node_update: LearningNode,
):
"""
Update a learning node (teacher review/edit).
Allows teachers to modify AI-generated content before use.
"""
success = await learning_service.update_node(aoi_id, node_id, node_update)
if not success:
raise HTTPException(status_code=404, detail="Node not found")
logger.info("Learning node updated", aoi_id=aoi_id, node_id=node_id)
return {"message": "Node updated successfully", "node_id": node_id}
@router.delete("/{aoi_id}/nodes/{node_id}")
async def delete_learning_node(
aoi_id: str = Path(..., description="AOI UUID"),
node_id: str = Path(..., description="Node UUID"),
):
"""
Delete a learning node.
"""
success = await learning_service.delete_node(aoi_id, node_id)
if not success:
raise HTTPException(status_code=404, detail="Node not found")
return {"message": "Node deleted successfully", "node_id": node_id}
@router.post("/{aoi_id}/nodes/{node_id}/approve")
async def approve_learning_node(
aoi_id: str = Path(..., description="AOI UUID"),
node_id: str = Path(..., description="Node UUID"),
):
"""
Approve a learning node for student use.
Only approved nodes will be visible to students.
"""
success = await learning_service.approve_node(aoi_id, node_id)
if not success:
raise HTTPException(status_code=404, detail="Node not found")
return {"message": "Node approved", "node_id": node_id}
@router.get("/statistics")
async def get_learning_statistics():
"""
Get statistics about learning node usage.
"""
stats = await learning_service.get_statistics()
return {
"total_nodes_generated": stats.get("total_nodes", 0),
"nodes_by_theme": stats.get("by_theme", {}),
"nodes_by_difficulty": stats.get("by_difficulty", {}),
"average_nodes_per_aoi": stats.get("avg_per_aoi", 0),
"most_popular_theme": stats.get("popular_theme", "topographie"),
}