""" 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"), }