""" Task Management API Handles TaskOrchestrator task lifecycle Endpoints: - POST /api/v1/tasks # Task erstellen - GET /api/v1/tasks/{id} # Task Status - PUT /api/v1/tasks/{id}/transition # Status aendern - DELETE /api/v1/tasks/{id} # Task loeschen """ import structlog from fastapi import APIRouter, HTTPException, Request from typing import Optional from datetime import datetime from config import settings from models.task import ( Task, TaskCreate, TaskResponse, TaskTransition, TaskState, TaskType, is_valid_transition, ) logger = structlog.get_logger(__name__) router = APIRouter() # In-memory task store (will be replaced with Valkey in production) _tasks: dict[str, Task] = {} async def get_task(task_id: str) -> Task: """Get task by ID or raise 404.""" task = _tasks.get(task_id) if not task: raise HTTPException(status_code=404, detail="Task not found") return task @router.post("", response_model=TaskResponse) async def create_task(request: Request, task_data: TaskCreate): """ Create a new task. The task will be queued for processing by TaskOrchestrator. Intent text is encrypted before storage. """ logger.info( "Creating task", session_id=task_data.session_id[:8], task_type=task_data.type.value, ) # Get encryption service encryption = request.app.state.encryption # Get session to validate and get namespace from api.sessions import _sessions session = _sessions.get(task_data.session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") # Encrypt intent text if encryption is enabled encrypted_intent = task_data.intent_text if settings.encryption_enabled: encrypted_intent = encryption.encrypt_content( task_data.intent_text, session.namespace_id, ) # Encrypt any PII in parameters encrypted_params = {} pii_fields = ["student_name", "class_name", "parent_name", "content"] for key, value in task_data.parameters.items(): if key in pii_fields and settings.encryption_enabled: encrypted_params[key] = encryption.encrypt_content( str(value), session.namespace_id, ) else: encrypted_params[key] = value # Create task task = Task( session_id=task_data.session_id, namespace_id=session.namespace_id, type=task_data.type, intent_text=encrypted_intent, parameters=encrypted_params, ) # Store task _tasks[task.id] = task # Add to session's pending tasks session.pending_tasks.append(task.id) # Queue task for processing orchestrator = request.app.state.orchestrator await orchestrator.queue_task(task) logger.info( "Task created", task_id=task.id[:8], session_id=task_data.session_id[:8], task_type=task_data.type.value, ) return TaskResponse( id=task.id, session_id=task.session_id, type=task.type, state=task.state, created_at=task.created_at, updated_at=task.updated_at, result_available=False, ) @router.get("/{task_id}", response_model=TaskResponse) async def get_task_status(task_id: str): """ Get task status. Returns current state and whether results are available. """ task = await get_task(task_id) return TaskResponse( id=task.id, session_id=task.session_id, type=task.type, state=task.state, created_at=task.created_at, updated_at=task.updated_at, result_available=task.result_ref is not None, error_message=task.error_message, ) @router.put("/{task_id}/transition", response_model=TaskResponse) async def transition_task(task_id: str, transition: TaskTransition): """ Transition task to a new state. Only valid transitions are allowed according to the state machine. """ task = await get_task(task_id) # Validate transition if not is_valid_transition(task.state, transition.new_state): raise HTTPException( status_code=400, detail=f"Invalid transition from {task.state.value} to {transition.new_state.value}" ) logger.info( "Transitioning task", task_id=task_id[:8], from_state=task.state.value, to_state=transition.new_state.value, reason=transition.reason, ) # Apply transition task.transition_to(transition.new_state, transition.reason) # If approved, execute the task if transition.new_state == TaskState.APPROVED: from services.task_orchestrator import TaskOrchestrator orchestrator = TaskOrchestrator() await orchestrator.execute_task(task) return TaskResponse( id=task.id, session_id=task.session_id, type=task.type, state=task.state, created_at=task.created_at, updated_at=task.updated_at, result_available=task.result_ref is not None, error_message=task.error_message, ) @router.delete("/{task_id}") async def delete_task(task_id: str): """ Delete a task. Only allowed for tasks in DRAFT, COMPLETED, or EXPIRED state. """ task = await get_task(task_id) # Check if deletion is allowed if task.state not in [TaskState.DRAFT, TaskState.COMPLETED, TaskState.EXPIRED, TaskState.REJECTED]: raise HTTPException( status_code=400, detail=f"Cannot delete task in {task.state.value} state" ) logger.info( "Deleting task", task_id=task_id[:8], state=task.state.value, ) # Remove from session's pending tasks from api.sessions import _sessions session = _sessions.get(task.session_id) if session and task_id in session.pending_tasks: session.pending_tasks.remove(task_id) # Delete task del _tasks[task_id] return {"status": "deleted", "task_id": task_id} @router.get("/{task_id}/result") async def get_task_result(task_id: str, request: Request): """ Get task result. Result is decrypted using the session's namespace key. Only available for completed tasks. """ task = await get_task(task_id) if task.state != TaskState.COMPLETED: raise HTTPException( status_code=400, detail=f"Task is in {task.state.value} state, not completed" ) if not task.result_ref: raise HTTPException( status_code=404, detail="No result available for this task" ) # Get encryption service to decrypt result encryption = request.app.state.encryption # Decrypt result reference if settings.encryption_enabled: result = encryption.decrypt_content( task.result_ref, task.namespace_id, ) else: result = task.result_ref return { "task_id": task_id, "type": task.type.value, "result": result, "completed_at": task.completed_at.isoformat() if task.completed_at else None, }