feat: voice-service von lehrer nach core verschoben, Pipeline erweitert (voice, BQAS, embedding, night-scheduler)
This commit is contained in:
262
voice-service/api/tasks.py
Normal file
262
voice-service/api/tasks.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user