Files
breakpilot-lehrer/voice-service/api/tasks.py
Benjamin Admin 9912997187
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 25s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m55s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 18s
refactor: Jitsi/Matrix/Voice von Core übernommen, Camunda/BPMN gelöscht, Kommunikation-Nav
- Voice-Service von Core nach Lehrer verschoben (bp-lehrer-voice-service)
- 4 Jitsi-Services + 2 Synapse-Services in docker-compose.yml aufgenommen
- Camunda komplett gelöscht: workflow pages, workflow-config.ts, bpmn-js deps
- CAMUNDA_URL aus backend-lehrer environment entfernt
- Sidebar: Kategorie "Compliance SDK" + "Katalogverwaltung" entfernt
- Sidebar: Neue Kategorie "Kommunikation" mit Video & Chat, Voice Service, Alerts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:01:47 +01:00

263 lines
7.1 KiB
Python

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