[split-required] Split remaining 500-680 LOC files (final batch)

website (17 pages + 3 components):
- multiplayer/wizard, middleware/wizard+test-wizard, communication
- builds/wizard, staff-search, voice, sbom/wizard
- foerderantrag, mail/tasks, tools/communication, sbom
- compliance/evidence, uni-crawler, brandbook (already done)
- CollectionsTab, IngestionTab, RiskHeatmap

backend-lehrer (5 files):
- letters_api (641 → 2), certificates_api (636 → 2)
- alerts_agent/db/models (636 → 3)
- llm_gateway/communication_service (614 → 2)
- game/database already done in prior batch

klausur-service (2 files):
- hybrid_vocab_extractor (664 → 2)
- klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2)

voice-service (3 files):
- bqas/rag_judge (618 → 3), runner (529 → 2)
- enhanced_task_orchestrator (519 → 2)

studio-v2 (6 files):
- korrektur/[klausurId] (578 → 4), fairness (569 → 2)
- AlertsWizard (552 → 2), OnboardingWizard (513 → 2)
- korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-25 08:56:45 +02:00
parent b4613e26f3
commit 451365a312
115 changed files with 10694 additions and 13839 deletions

View File

@@ -6,6 +6,10 @@ Extends the existing TaskOrchestrator with Multi-Agent support:
- Message bus integration for inter-agent communication
- Quality judge integration via BQAS
- Heartbeat-based liveness
Split into:
- enhanced_orchestrator_session.py: Session lifecycle (create/end/recover)
- enhanced_task_orchestrator.py (this file): Main orchestrator class
"""
import structlog
@@ -27,6 +31,12 @@ from brain.context_manager import ContextManager, MessageRole
from orchestrator.message_bus import MessageBus, AgentMessage, MessagePriority
from orchestrator.task_router import TaskRouter, RoutingStrategy
from services.enhanced_orchestrator_session import (
create_session as _create_session,
end_session as _end_session,
recover_session as _recover_session,
)
logger = structlog.get_logger(__name__)
@@ -47,50 +57,25 @@ class EnhancedTaskOrchestrator(TaskOrchestrator):
db_pool=None,
namespace: str = "breakpilot"
):
"""
Initialize the enhanced orchestrator.
Args:
redis_client: Async Redis/Valkey client
db_pool: Async PostgreSQL connection pool
namespace: Namespace for isolation
"""
super().__init__()
# Initialize agent-core components
self.session_manager = SessionManager(
redis_client=redis_client,
db_pool=db_pool,
namespace=namespace
redis_client=redis_client, db_pool=db_pool, namespace=namespace
)
self.memory_store = MemoryStore(
redis_client=redis_client,
db_pool=db_pool,
namespace=namespace
redis_client=redis_client, db_pool=db_pool, namespace=namespace
)
self.context_manager = ContextManager(
redis_client=redis_client,
db_pool=db_pool,
namespace=namespace
redis_client=redis_client, db_pool=db_pool, namespace=namespace
)
self.message_bus = MessageBus(
redis_client=redis_client,
db_pool=db_pool,
namespace=namespace
redis_client=redis_client, db_pool=db_pool, namespace=namespace
)
self.heartbeat = HeartbeatMonitor(
timeout_seconds=30,
check_interval_seconds=5,
max_missed_beats=3
timeout_seconds=30, check_interval_seconds=5, max_missed_beats=3
)
self.task_router = TaskRouter()
# Track active sessions by voice session ID
self._voice_sessions: Dict[str, AgentSession] = {}
self._heartbeat_clients: Dict[str, HeartbeatClient] = {}
@@ -100,231 +85,98 @@ class EnhancedTaskOrchestrator(TaskOrchestrator):
"""Starts the enhanced orchestrator"""
await self.message_bus.start()
await self.heartbeat.start_monitoring()
# Subscribe to messages directed at this orchestrator
await self.message_bus.subscribe(
"voice-orchestrator",
self._handle_agent_message
)
await self.message_bus.subscribe("voice-orchestrator", self._handle_agent_message)
logger.info("Enhanced TaskOrchestrator started")
async def stop(self) -> None:
"""Stops the enhanced orchestrator"""
# Stop all heartbeat clients
for client in self._heartbeat_clients.values():
await client.stop()
self._heartbeat_clients.clear()
await self.heartbeat.stop_monitoring()
await self.message_bus.stop()
logger.info("Enhanced TaskOrchestrator stopped")
async def create_session(
self,
voice_session_id: str,
user_id: str = "",
self, voice_session_id: str, user_id: str = "",
metadata: Optional[Dict[str, Any]] = None
) -> AgentSession:
"""
Creates a new agent session for a voice session.
Args:
voice_session_id: The voice session ID
user_id: Optional user ID
metadata: Additional metadata
Returns:
The created AgentSession
"""
# Create session via session manager
session = await self.session_manager.create_session(
agent_type="voice-orchestrator",
user_id=user_id,
context={"voice_session_id": voice_session_id},
metadata=metadata
return await _create_session(
self.session_manager, self.context_manager, self.heartbeat,
self._voice_sessions, self._heartbeat_clients,
voice_session_id, user_id, metadata, self._get_system_prompt(),
)
# Create conversation context
self.context_manager.create_context(
session_id=session.session_id,
system_prompt=self._get_system_prompt(),
max_messages=50
)
# Start heartbeat for this session
heartbeat_client = HeartbeatClient(
session_id=session.session_id,
monitor=self.heartbeat,
interval_seconds=10
)
await heartbeat_client.start()
# Register heartbeat for monitoring
self.heartbeat.register(session.session_id, "voice-orchestrator")
# Store references
self._voice_sessions[voice_session_id] = session
self._heartbeat_clients[session.session_id] = heartbeat_client
logger.info(
"Created agent session",
session_id=session.session_id[:8],
voice_session_id=voice_session_id
)
return session
async def get_session(
self,
voice_session_id: str
) -> Optional[AgentSession]:
"""Gets the agent session for a voice session"""
async def get_session(self, voice_session_id: str) -> Optional[AgentSession]:
return self._voice_sessions.get(voice_session_id)
async def end_session(self, voice_session_id: str) -> None:
"""
Ends an agent session.
Args:
voice_session_id: The voice session ID
"""
session = self._voice_sessions.get(voice_session_id)
if not session:
return
# Stop heartbeat
if session.session_id in self._heartbeat_clients:
await self._heartbeat_clients[session.session_id].stop()
del self._heartbeat_clients[session.session_id]
# Unregister from heartbeat monitor
self.heartbeat.unregister(session.session_id)
# Mark session as completed
session.complete()
await self.session_manager.update_session(session)
# Clean up
del self._voice_sessions[voice_session_id]
logger.info(
"Ended agent session",
session_id=session.session_id[:8],
duration_seconds=session.get_duration().total_seconds()
await _end_session(
self.session_manager, self.heartbeat,
self._voice_sessions, self._heartbeat_clients, voice_session_id,
)
async def queue_task(self, task: Task) -> None:
"""
Queue a task with session checkpointing.
Extends parent to add checkpoint for recovery.
"""
# Get session for this task
"""Queue a task with session checkpointing."""
session = self._voice_sessions.get(task.session_id)
if session:
# Checkpoint before queueing
session.checkpoint("task_queued", {
"task_id": task.id,
"task_type": task.type.value,
"task_id": task.id, "task_type": task.type.value,
"parameters": task.parameters
})
await self.session_manager.update_session(session)
# Call parent implementation
await super().queue_task(task)
async def process_task(self, task: Task) -> None:
"""
Process a task with enhanced routing and quality checks.
Extends parent to:
- Route complex tasks to specialized agents
- Run quality checks via BQAS
- Store results in memory for learning
"""
"""Process a task with enhanced routing and quality checks."""
session = self._voice_sessions.get(task.session_id)
if session:
session.checkpoint("task_processing", {
"task_id": task.id
})
session.checkpoint("task_processing", {"task_id": task.id})
# Check if this task should be routed to a specialized agent
if self._needs_specialized_agent(task):
await self._route_to_agent(task, session)
else:
# Use parent implementation for simple tasks
await super().process_task(task)
# Run quality check on result
if task.result_ref and self._needs_quality_check(task):
await self._run_quality_check(task, session)
# Store in memory for learning
if task.state == TaskState.READY and task.result_ref:
await self._store_task_result(task)
if session:
session.checkpoint("task_completed", {
"task_id": task.id,
"state": task.state.value
"task_id": task.id, "state": task.state.value
})
await self.session_manager.update_session(session)
def _needs_specialized_agent(self, task: Task) -> bool:
"""Check if task needs routing to a specialized agent"""
from models.task import TaskType
# Tasks that benefit from specialized agents
specialized_types = [
TaskType.PARENT_LETTER, # Could use grader for tone
TaskType.FEEDBACK_SUGGEST, # Quality judge for appropriateness
]
return task.type in specialized_types
return task.type in [TaskType.PARENT_LETTER, TaskType.FEEDBACK_SUGGEST]
def _needs_quality_check(self, task: Task) -> bool:
"""Check if task result needs quality validation"""
from models.task import TaskType
# Tasks that generate content should be checked
content_types = [
TaskType.PARENT_LETTER,
TaskType.CLASS_MESSAGE,
TaskType.FEEDBACK_SUGGEST,
TaskType.WORKSHEET_GENERATE,
return task.type in [
TaskType.PARENT_LETTER, TaskType.CLASS_MESSAGE,
TaskType.FEEDBACK_SUGGEST, TaskType.WORKSHEET_GENERATE,
]
return task.type in content_types
async def _route_to_agent(
self,
task: Task,
session: Optional[AgentSession]
) -> None:
async def _route_to_agent(self, task: Task, session: Optional[AgentSession]) -> None:
"""Routes a task to a specialized agent"""
# Determine target agent
intent = f"task_{task.type.value}"
routing_result = await self.task_router.route(
intent=intent,
context={"task": task.parameters},
intent=intent, context={"task": task.parameters},
strategy=RoutingStrategy.LEAST_LOADED
)
if not routing_result.success:
# Fall back to local processing
logger.warning(
"No agent available for task, using local processing",
task_id=task.id[:8],
reason=routing_result.reason
task_id=task.id[:8], reason=routing_result.reason
)
await super().process_task(task)
return
# Send to agent via message bus
try:
response = await self.message_bus.request(
AgentMessage(
@@ -332,8 +184,7 @@ class EnhancedTaskOrchestrator(TaskOrchestrator):
receiver=routing_result.agent_id,
message_type=f"process_{task.type.value}",
payload={
"task_id": task.id,
"task_type": task.type.value,
"task_id": task.id, "task_type": task.type.value,
"parameters": task.parameters,
"session_id": session.session_id if session else None
},
@@ -341,179 +192,78 @@ class EnhancedTaskOrchestrator(TaskOrchestrator):
),
timeout=30.0
)
task.result_ref = response.get("result", "")
task.transition_to(TaskState.READY, "agent_processed")
except asyncio.TimeoutError:
logger.error(
"Agent timeout, falling back to local",
task_id=task.id[:8],
agent=routing_result.agent_id
task_id=task.id[:8], agent=routing_result.agent_id
)
await super().process_task(task)
async def _run_quality_check(
self,
task: Task,
session: Optional[AgentSession]
) -> None:
async def _run_quality_check(self, task: Task, session: Optional[AgentSession]) -> None:
"""Runs quality check on task result via quality judge"""
try:
response = await self.message_bus.request(
AgentMessage(
sender="voice-orchestrator",
receiver="quality-judge",
sender="voice-orchestrator", receiver="quality-judge",
message_type="evaluate_response",
payload={
"task_id": task.id,
"task_type": task.type.value,
"response": task.result_ref,
"context": task.parameters
"task_id": task.id, "task_type": task.type.value,
"response": task.result_ref, "context": task.parameters
},
priority=MessagePriority.NORMAL
),
timeout=10.0
)
quality_score = response.get("composite_score", 0)
if quality_score < 60:
# Mark for review
task.error_message = f"Quality check failed: {quality_score}"
logger.warning(
"Task failed quality check",
task_id=task.id[:8],
score=quality_score
)
logger.warning("Task failed quality check", task_id=task.id[:8], score=quality_score)
except asyncio.TimeoutError:
# Quality check timeout is non-fatal
logger.warning(
"Quality check timeout",
task_id=task.id[:8]
)
logger.warning("Quality check timeout", task_id=task.id[:8])
async def _store_task_result(self, task: Task) -> None:
"""Stores task result in memory for learning"""
await self.memory_store.remember(
key=f"task:{task.type.value}:{task.id}",
value={
"result": task.result_ref,
"parameters": task.parameters,
"result": task.result_ref, "parameters": task.parameters,
"completed_at": datetime.utcnow().isoformat()
},
agent_id="voice-orchestrator",
ttl_days=30
agent_id="voice-orchestrator", ttl_days=30
)
async def _handle_agent_message(
self,
message: AgentMessage
) -> Optional[Dict[str, Any]]:
async def _handle_agent_message(self, message: AgentMessage) -> Optional[Dict[str, Any]]:
"""Handles incoming messages from other agents"""
logger.debug(
"Received agent message",
sender=message.sender,
type=message.message_type
)
logger.debug("Received agent message", sender=message.sender, type=message.message_type)
if message.message_type == "task_status_update":
# Handle task status updates
task_id = message.payload.get("task_id")
if task_id in self._tasks:
task = self._tasks[task_id]
new_state = message.payload.get("state")
if new_state:
task.transition_to(TaskState(new_state), "agent_update")
return None
def _get_system_prompt(self) -> str:
"""Returns the system prompt for the voice assistant"""
return """Du bist ein hilfreicher Assistent für Lehrer in der Breakpilot-App.
return """Du bist ein hilfreicher Assistent fuer Lehrer in der Breakpilot-App.
Deine Aufgaben:
- Hilf beim Erstellen von Arbeitsblättern
- Unterstütze bei der Korrektur
- Hilf beim Erstellen von Arbeitsblaettern
- Unterstuetze bei der Korrektur
- Erstelle Elternbriefe und Klassennachrichten
- Dokumentiere Beobachtungen und Erinnerungen
Halte dich kurz und präzise. Nutze einfache, klare Sprache.
Halte dich kurz und praezise. Nutze einfache, klare Sprache.
Bei Unklarheiten frage nach."""
# Recovery methods
async def recover_session(
self,
voice_session_id: str,
session_id: str
self, voice_session_id: str, session_id: str
) -> Optional[AgentSession]:
"""
Recovers a session from checkpoint.
Args:
voice_session_id: The voice session ID
session_id: The agent session ID to recover
Returns:
The recovered session or None
"""
session = await self.session_manager.get_session(session_id)
if not session:
logger.warning(
"Session not found for recovery",
session_id=session_id
)
return None
if session.state != SessionState.ACTIVE:
logger.warning(
"Session not active for recovery",
session_id=session_id,
state=session.state.value
)
return None
# Resume session
session.resume()
# Restore heartbeat
heartbeat_client = HeartbeatClient(
session_id=session.session_id,
monitor=self.heartbeat,
interval_seconds=10
return await _recover_session(
self.session_manager, self.heartbeat,
self._voice_sessions, self._heartbeat_clients,
self._tasks, self.process_task,
voice_session_id, session_id,
)
await heartbeat_client.start()
self.heartbeat.register(session.session_id, "voice-orchestrator")
# Store references
self._voice_sessions[voice_session_id] = session
self._heartbeat_clients[session.session_id] = heartbeat_client
# Recover pending tasks from checkpoints
await self._recover_pending_tasks(session)
logger.info(
"Recovered session",
session_id=session.session_id[:8],
checkpoints=len(session.checkpoints)
)
return session
async def _recover_pending_tasks(self, session: AgentSession) -> None:
"""Recovers pending tasks from session checkpoints"""
for checkpoint in reversed(session.checkpoints):
if checkpoint.name == "task_queued":
task_id = checkpoint.data.get("task_id")
if task_id and task_id in self._tasks:
task = self._tasks[task_id]
if task.state == TaskState.QUEUED:
# Re-process queued task
await self.process_task(task)
logger.info(
"Recovered pending task",
task_id=task_id[:8]
)