A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
218 lines
7.0 KiB
Python
218 lines
7.0 KiB
Python
"""
|
|
Task Models - Clawdbot State Machine
|
|
Task lifecycle management with encrypted references
|
|
|
|
State Machine:
|
|
DRAFT -> QUEUED -> RUNNING -> READY
|
|
|
|
|
+-----------+----------+
|
|
| |
|
|
APPROVED REJECTED
|
|
| |
|
|
COMPLETED DRAFT (revision)
|
|
|
|
Any State -> EXPIRED (TTL)
|
|
Any State -> PAUSED (User Interrupt)
|
|
"""
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Optional, Dict, Any, List
|
|
from pydantic import BaseModel, Field
|
|
import uuid
|
|
|
|
|
|
class TaskState(str, Enum):
|
|
"""Task state machine states."""
|
|
DRAFT = "draft"
|
|
QUEUED = "queued"
|
|
RUNNING = "running"
|
|
READY = "ready"
|
|
APPROVED = "approved"
|
|
REJECTED = "rejected"
|
|
COMPLETED = "completed"
|
|
EXPIRED = "expired"
|
|
PAUSED = "paused"
|
|
|
|
|
|
class TaskType(str, Enum):
|
|
"""Task types for Breakpilot integration."""
|
|
# Gruppe 1: Kurze Notizen
|
|
STUDENT_OBSERVATION = "student_observation"
|
|
REMINDER = "reminder"
|
|
HOMEWORK_CHECK = "homework_check"
|
|
CONFERENCE_TOPIC = "conference_topic"
|
|
CORRECTION_NOTE = "correction_note"
|
|
|
|
# Gruppe 2: Arbeitsblatt-Generierung
|
|
WORKSHEET_GENERATE = "worksheet_generate"
|
|
WORKSHEET_DIFFERENTIATE = "worksheet_differentiate"
|
|
|
|
# Gruppe 3: Situatives Arbeiten
|
|
QUICK_ACTIVITY = "quick_activity"
|
|
QUIZ_GENERATE = "quiz_generate"
|
|
PARENT_LETTER = "parent_letter"
|
|
CLASS_MESSAGE = "class_message"
|
|
|
|
# Gruppe 4: Canvas-Editor
|
|
CANVAS_EDIT = "canvas_edit"
|
|
CANVAS_LAYOUT = "canvas_layout"
|
|
|
|
# Gruppe 5: Korrektur-Assistenz
|
|
OPERATOR_CHECKLIST = "operator_checklist"
|
|
EH_PASSAGE = "eh_passage"
|
|
FEEDBACK_SUGGEST = "feedback_suggest"
|
|
|
|
# Gruppe 6: Follow-up
|
|
REMINDER_SCHEDULE = "reminder_schedule"
|
|
TASK_SUMMARY = "task_summary"
|
|
|
|
|
|
class Task(BaseModel):
|
|
"""
|
|
Task entity for Clawdbot orchestration.
|
|
Stored in Valkey with TTL.
|
|
"""
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
session_id: str = Field(..., description="Parent session ID")
|
|
namespace_id: str = Field(..., description="Teacher namespace ID")
|
|
|
|
# Task definition
|
|
type: TaskType
|
|
state: TaskState = Field(default=TaskState.DRAFT)
|
|
intent_text: str = Field(..., description="Original voice command (encrypted ref)")
|
|
|
|
# Task parameters (no PII, only references)
|
|
parameters: Dict[str, Any] = Field(default_factory=dict)
|
|
# Example parameters:
|
|
# - student_ref: encrypted reference to student
|
|
# - class_ref: encrypted reference to class
|
|
# - content_type: "worksheet", "quiz", etc.
|
|
# - source_ref: encrypted reference to source document
|
|
|
|
# Execution state
|
|
result_ref: Optional[str] = Field(default=None, description="Encrypted result reference")
|
|
error_message: Optional[str] = Field(default=None)
|
|
|
|
# Timestamps
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
completed_at: Optional[datetime] = Field(default=None)
|
|
expires_at: Optional[datetime] = Field(default=None)
|
|
|
|
# Audit trail (no PII)
|
|
state_history: List[Dict[str, Any]] = Field(default_factory=list)
|
|
|
|
def transition_to(self, new_state: TaskState, reason: Optional[str] = None):
|
|
"""Transition to a new state with history tracking."""
|
|
old_state = self.state
|
|
self.state = new_state
|
|
self.updated_at = datetime.utcnow()
|
|
|
|
# Add to history (no PII in reason)
|
|
self.state_history.append({
|
|
"from": old_state.value,
|
|
"to": new_state.value,
|
|
"timestamp": self.updated_at.isoformat(),
|
|
"reason": reason,
|
|
})
|
|
|
|
if new_state in [TaskState.COMPLETED, TaskState.EXPIRED]:
|
|
self.completed_at = self.updated_at
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"id": "task-xyz789",
|
|
"session_id": "session-abc123",
|
|
"namespace_id": "teacher-ns-456",
|
|
"type": "student_observation",
|
|
"state": "ready",
|
|
"intent_text": "encrypted:abc123...",
|
|
"parameters": {
|
|
"student_ref": "encrypted:student-max-123",
|
|
"observation_type": "behavior",
|
|
},
|
|
"created_at": "2026-01-26T10:30:00Z",
|
|
"updated_at": "2026-01-26T10:30:05Z",
|
|
}
|
|
}
|
|
|
|
|
|
class TaskCreate(BaseModel):
|
|
"""Request to create a new task."""
|
|
session_id: str
|
|
type: TaskType
|
|
intent_text: str = Field(..., description="Voice command text")
|
|
parameters: Dict[str, Any] = Field(default_factory=dict)
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"session_id": "session-abc123",
|
|
"type": "student_observation",
|
|
"intent_text": "Notiz zu Max: heute wiederholt gestoert",
|
|
"parameters": {
|
|
"student_name": "Max", # Will be encrypted
|
|
"observation": "wiederholt gestoert",
|
|
},
|
|
}
|
|
}
|
|
|
|
|
|
class TaskResponse(BaseModel):
|
|
"""Task response for API."""
|
|
id: str
|
|
session_id: str
|
|
type: TaskType
|
|
state: TaskState
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
result_available: bool = Field(default=False)
|
|
error_message: Optional[str] = Field(default=None)
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"id": "task-xyz789",
|
|
"session_id": "session-abc123",
|
|
"type": "student_observation",
|
|
"state": "completed",
|
|
"created_at": "2026-01-26T10:30:00Z",
|
|
"updated_at": "2026-01-26T10:30:10Z",
|
|
"result_available": True,
|
|
}
|
|
}
|
|
|
|
|
|
class TaskTransition(BaseModel):
|
|
"""Request to transition task state."""
|
|
new_state: TaskState
|
|
reason: Optional[str] = Field(default=None, description="Transition reason (no PII)")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"new_state": "approved",
|
|
"reason": "user_confirmed",
|
|
}
|
|
}
|
|
|
|
|
|
# Valid state transitions
|
|
VALID_TRANSITIONS: Dict[TaskState, List[TaskState]] = {
|
|
TaskState.DRAFT: [TaskState.QUEUED, TaskState.EXPIRED, TaskState.PAUSED],
|
|
TaskState.QUEUED: [TaskState.RUNNING, TaskState.EXPIRED, TaskState.PAUSED],
|
|
TaskState.RUNNING: [TaskState.READY, TaskState.EXPIRED, TaskState.PAUSED],
|
|
TaskState.READY: [TaskState.APPROVED, TaskState.REJECTED, TaskState.EXPIRED, TaskState.PAUSED],
|
|
TaskState.APPROVED: [TaskState.COMPLETED, TaskState.EXPIRED],
|
|
TaskState.REJECTED: [TaskState.DRAFT, TaskState.EXPIRED],
|
|
TaskState.PAUSED: [TaskState.DRAFT, TaskState.QUEUED, TaskState.EXPIRED],
|
|
TaskState.COMPLETED: [], # Terminal state
|
|
TaskState.EXPIRED: [], # Terminal state
|
|
}
|
|
|
|
|
|
def is_valid_transition(from_state: TaskState, to_state: TaskState) -> bool:
|
|
"""Check if a state transition is valid."""
|
|
return to_state in VALID_TRANSITIONS.get(from_state, [])
|