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
- 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>
263 lines
7.1 KiB
Python
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,
|
|
}
|