Splitting & Refactoring - Umsetzung¶
🔧 Status: Bereit zur Umsetzung
Dieses Dokument enthält die konkreten Implementierungsschritte. Siehe Progress für den aktuellen Stand.
Konzept: Splitting & Refactoring Konzept Erstellt: 2025-11-28 Autor: Claude Code
Übersicht¶
Dieses Dokument beschreibt die schrittweise Umsetzung des Splittings. Jede Phase enthält konkreten Code und Anleitungen.
Implementierungs-Reihenfolge¶
- Phase 1: Backend Models (
tables.py) - Phase 2: Backend Routes (
judge_routes.py) - Phase 3: Frontend Judge (
JudgeSession.vue) - Phase 4: Backend Routes (oncoco, RAG)
- Phase 5: Frontend OnCoCo & Admin
- Phase 6: Restliche Dateien
Phase 1: Backend Models (tables.py)¶
Ziel¶
app/db/tables.py (1.260 Zeilen) → 8 Model-Dateien (je ~150 Zeilen)
Schritt 1.1: Ordnerstruktur erstellen¶
Schritt 1.2: Base-Konfiguration¶
Datei: app/db/models/__init__.py
"""
LLARS Database Models
Alle Models sind hier re-exportiert für Backwards Compatibility.
Import: from app.db.models import User, Permission, JudgeSession
"""
from app.db.models.user import User, UserGroup, UserRole
from app.db.models.permission import Permission, Role, RolePermission, UserPermission, PermissionAuditLog
from app.db.models.judge import JudgeSession, JudgeComparison, JudgeEvaluation
from app.db.models.rag import RAGCollection, RAGDocument, RAGDocumentChunk
from app.db.models.chatbot import Chatbot, ChatbotCollection, ChatbotConversation, ChatbotMessage
from app.db.models.oncoco import OnCoCoAnalysis, OnCoCoSentenceLabel
from app.db.models.pillar import PillarThread, PillarStatistics
from app.db.models.scenario import RatingScenario, MailRating, EmailThread, Message
__all__ = [
# User
'User', 'UserGroup', 'UserRole',
# Permission
'Permission', 'Role', 'RolePermission', 'UserPermission', 'PermissionAuditLog',
# Judge
'JudgeSession', 'JudgeComparison', 'JudgeEvaluation',
# RAG
'RAGCollection', 'RAGDocument', 'RAGDocumentChunk',
# Chatbot
'Chatbot', 'ChatbotCollection', 'ChatbotConversation', 'ChatbotMessage',
# OnCoCo
'OnCoCoAnalysis', 'OnCoCoSentenceLabel',
# Pillar
'PillarThread', 'PillarStatistics',
# Scenario
'RatingScenario', 'MailRating', 'EmailThread', 'Message',
]
Schritt 1.3: User Models¶
Datei: app/db/models/user.py
"""User-related database models."""
from datetime import datetime
from app.db.db import db
class User(db.Model):
"""User model for authentication and authorization."""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(255), unique=True, nullable=False)
email = db.Column(db.String(255), unique=True, nullable=True)
display_name = db.Column(db.String(255), nullable=True)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
# Relationships
groups = db.relationship('UserGroup', secondary='user_group_members', back_populates='users')
roles = db.relationship('UserRole', back_populates='user')
permissions = db.relationship('UserPermission', back_populates='user')
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'display_name': self.display_name,
'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None,
}
class UserGroup(db.Model):
"""Group model for organizing users."""
__tablename__ = 'user_groups'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(255), unique=True, nullable=False)
description = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
users = db.relationship('User', secondary='user_group_members', back_populates='groups')
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
}
# Association table for User-Group many-to-many
user_group_members = db.Table(
'user_group_members',
db.Column('user_id', db.Integer, db.ForeignKey('users.id'), primary_key=True),
db.Column('group_id', db.Integer, db.ForeignKey('user_groups.id'), primary_key=True),
)
class UserRole(db.Model):
"""Maps users to roles."""
__tablename__ = 'user_roles'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), nullable=False)
assigned_at = db.Column(db.DateTime, default=datetime.utcnow)
assigned_by = db.Column(db.String(255), nullable=True)
user = db.relationship('User', back_populates='roles')
role = db.relationship('Role', back_populates='user_roles')
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'role_id': self.role_id,
'assigned_at': self.assigned_at.isoformat() if self.assigned_at else None,
}
Schritt 1.4: Permission Models¶
Datei: app/db/models/permission.py
"""Permission and Role database models."""
from datetime import datetime
from app.db.db import db
class Permission(db.Model):
"""Individual permission definition."""
__tablename__ = 'permissions'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
key = db.Column(db.String(255), unique=True, nullable=False)
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=True)
category = db.Column(db.String(100), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'key': self.key,
'name': self.name,
'description': self.description,
'category': self.category,
}
class Role(db.Model):
"""Role definition with associated permissions."""
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(255), unique=True, nullable=False)
description = db.Column(db.Text, nullable=True)
is_system = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
permissions = db.relationship('RolePermission', back_populates='role')
user_roles = db.relationship('UserRole', back_populates='role')
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'is_system': self.is_system,
}
class RolePermission(db.Model):
"""Maps roles to permissions."""
__tablename__ = 'role_permissions'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), nullable=False)
permission_id = db.Column(db.Integer, db.ForeignKey('permissions.id'), nullable=False)
role = db.relationship('Role', back_populates='permissions')
permission = db.relationship('Permission')
class UserPermission(db.Model):
"""Direct user permission overrides."""
__tablename__ = 'user_permissions'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
permission_id = db.Column(db.Integer, db.ForeignKey('permissions.id'), nullable=False)
granted = db.Column(db.Boolean, default=True)
assigned_at = db.Column(db.DateTime, default=datetime.utcnow)
assigned_by = db.Column(db.String(255), nullable=True)
user = db.relationship('User', back_populates='permissions')
permission = db.relationship('Permission')
class PermissionAuditLog(db.Model):
"""Audit trail for permission changes."""
__tablename__ = 'permission_audit_log'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
actor = db.Column(db.String(255), nullable=False)
action = db.Column(db.String(100), nullable=False)
target_user = db.Column(db.String(255), nullable=True)
target_role = db.Column(db.String(255), nullable=True)
permission_key = db.Column(db.String(255), nullable=True)
details = db.Column(db.JSON, nullable=True)
def to_dict(self):
return {
'id': self.id,
'timestamp': self.timestamp.isoformat() if self.timestamp else None,
'actor': self.actor,
'action': self.action,
'target_user': self.target_user,
'target_role': self.target_role,
'permission_key': self.permission_key,
'details': self.details,
}
Schritt 1.5: Weitere Model-Dateien¶
Die weiteren Model-Dateien (judge.py, rag.py, chatbot.py, oncoco.py, pillar.py, scenario.py) folgen dem gleichen Muster. Jede Datei enthält die zugehörigen Models aus tables.py.
Schritt 1.6: tables.py aktualisieren (Backwards Compatibility)¶
Datei: app/db/tables.py
"""
DEPRECATED: Use app.db.models instead.
This file is kept for backwards compatibility.
All models are re-exported from app.db.models.
"""
import warnings
warnings.warn(
"Importing from app.db.tables is deprecated. Use app.db.models instead.",
DeprecationWarning,
stacklevel=2
)
# Re-export all models for backwards compatibility
from app.db.models import *
Schritt 1.7: Validierung¶
# Prüfen ob alle Imports funktionieren
docker compose exec backend-flask-service python -c "from app.db.models import User, Permission, JudgeSession; print('OK')"
# Prüfen ob alte Imports noch funktionieren
docker compose exec backend-flask-service python -c "from app.db.tables import User; print('OK')"
# Tests ausführen
docker compose exec backend-flask-service pytest app/tests/ -v
Phase 2: Backend Routes (judge_routes.py)¶
Ziel¶
app/routes/judge/judge_routes.py (2.596 Zeilen) → 6 Route-Dateien (je ~400 Zeilen)
Schritt 2.1: Neue Dateien erstellen¶
touch app/routes/judge/session_routes.py
touch app/routes/judge/comparison_routes.py
touch app/routes/judge/evaluation_routes.py
touch app/routes/judge/kia_sync_routes.py
touch app/routes/judge/statistics_routes.py
touch app/routes/judge/stream_routes.py
Schritt 2.2: Blueprint-Struktur¶
Datei: app/routes/judge/__init__.py
"""
Judge Routes Module
Stellt alle Judge-bezogenen API-Endpoints bereit.
"""
from flask import Blueprint
# Haupt-Blueprint
judge_blueprint = Blueprint('judge', __name__, url_prefix='/api/judge')
# Sub-Blueprints importieren und registrieren
from app.routes.judge.session_routes import session_bp
from app.routes.judge.comparison_routes import comparison_bp
from app.routes.judge.evaluation_routes import evaluation_bp
from app.routes.judge.kia_sync_routes import kia_sync_bp
from app.routes.judge.statistics_routes import statistics_bp
from app.routes.judge.stream_routes import stream_bp
judge_blueprint.register_blueprint(session_bp)
judge_blueprint.register_blueprint(comparison_bp)
judge_blueprint.register_blueprint(evaluation_bp)
judge_blueprint.register_blueprint(kia_sync_bp)
judge_blueprint.register_blueprint(statistics_bp)
judge_blueprint.register_blueprint(stream_bp)
__all__ = ['judge_blueprint']
Schritt 2.3: Session Routes¶
Datei: app/routes/judge/session_routes.py
"""Session management routes for LLM-as-Judge."""
from flask import Blueprint, request, jsonify
from app.decorators.permission_decorator import require_permission
session_bp = Blueprint('judge_sessions', __name__)
@session_bp.route('/sessions', methods=['GET'])
@require_permission('feature:comparison:view')
def get_sessions():
"""List all judge sessions."""
# Implementation aus judge_routes.py verschieben
pass
@session_bp.route('/sessions', methods=['POST'])
@require_permission('feature:comparison:edit')
def create_session():
"""Create a new judge session."""
pass
@session_bp.route('/sessions/<int:session_id>', methods=['GET'])
@require_permission('feature:comparison:view')
def get_session(session_id):
"""Get a specific session by ID."""
pass
@session_bp.route('/sessions/<int:session_id>', methods=['DELETE'])
@require_permission('feature:comparison:edit')
def delete_session(session_id):
"""Delete a session."""
pass
@session_bp.route('/sessions/<int:session_id>/start', methods=['POST'])
@require_permission('feature:comparison:edit')
def start_session(session_id):
"""Start a session."""
pass
@session_bp.route('/sessions/<int:session_id>/pause', methods=['POST'])
@require_permission('feature:comparison:edit')
def pause_session(session_id):
"""Pause a running session."""
pass
Schritt 2.4-2.8: Weitere Route-Dateien¶
Analog zu session_routes.py werden die anderen Route-Dateien erstellt.
Phase 3: Frontend Judge (JudgeSession.vue)¶
Ziel¶
JudgeSession.vue (4.191 Zeilen) → 8+ Komponenten (je ~300 Zeilen)
Schritt 3.1: Ordnerstruktur¶
mkdir -p llars-frontend/src/components/Judge/JudgeSession
mkdir -p llars-frontend/src/components/Judge/JudgeSession/composables
Schritt 3.2: Composables extrahieren¶
Datei: llars-frontend/src/components/Judge/JudgeSession/composables/useSessionSocket.js
/**
* Socket.IO connection management for Judge Sessions
*/
import { ref, onMounted, onUnmounted } from 'vue'
import { getSocket } from '@/services/socketService'
export function useSessionSocket(sessionId) {
const socket = ref(null)
const isConnected = ref(false)
const connectionError = ref(null)
const connect = () => {
socket.value = getSocket()
socket.value.on('connect', () => {
isConnected.value = true
socket.value.emit('judge:join_session', { session_id: sessionId.value })
})
socket.value.on('disconnect', () => {
isConnected.value = false
})
socket.value.on('connect_error', (error) => {
connectionError.value = error.message
})
}
const disconnect = () => {
if (socket.value) {
socket.value.emit('judge:leave_session', { session_id: sessionId.value })
socket.value.disconnect()
}
}
onMounted(() => connect())
onUnmounted(() => disconnect())
return {
socket,
isConnected,
connectionError,
connect,
disconnect,
}
}
Schritt 3.3: Sub-Komponenten erstellen¶
Datei: llars-frontend/src/components/Judge/JudgeSession/SessionHeader.vue
<template>
<v-card class="mb-4">
<v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-gavel</v-icon>
<span>{{ session?.name || 'Judge Session' }}</span>
<v-spacer />
<v-chip :color="statusColor" class="ml-2">
{{ session?.status }}
</v-chip>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="3">
<div class="text-h4">{{ progress.completed }}</div>
<div class="text-caption">Abgeschlossen</div>
</v-col>
<v-col cols="3">
<div class="text-h4">{{ progress.total }}</div>
<div class="text-caption">Gesamt</div>
</v-col>
<v-col cols="3">
<div class="text-h4">{{ progress.queued }}</div>
<div class="text-caption">In Queue</div>
</v-col>
<v-col cols="3">
<div class="text-h4">{{ activeWorkers }}</div>
<div class="text-caption">Aktive Worker</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
session: Object,
progress: Object,
activeWorkers: Number,
})
const statusColor = computed(() => {
const colors = {
created: 'grey',
running: 'success',
paused: 'warning',
completed: 'info',
failed: 'error',
}
return colors[props.session?.status] || 'grey'
})
</script>
Schritt 3.4: Haupt-Komponente refaktorieren¶
Datei: llars-frontend/src/components/Judge/JudgeSession/JudgeSession.vue
<template>
<v-container fluid>
<!-- Header -->
<SessionHeader
:session="session"
:progress="progress"
:active-workers="activeWorkers"
/>
<!-- Controls -->
<SessionControls
:session="session"
:is-running="isRunning"
@start="startSession"
@pause="pauseSession"
@stop="stopSession"
/>
<!-- Worker Grid -->
<WorkerGrid
:workers="workers"
:comparisons="activeComparisons"
/>
<!-- Queue -->
<ComparisonQueue
:queue="queue"
:loading="loadingQueue"
/>
</v-container>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useSessionSocket } from './composables/useSessionSocket'
import { useSessionState } from './composables/useSessionState'
import SessionHeader from './SessionHeader.vue'
import SessionControls from './SessionControls.vue'
import WorkerGrid from './WorkerGrid.vue'
import ComparisonQueue from './ComparisonQueue.vue'
const route = useRoute()
const sessionId = computed(() => route.params.id)
// Socket connection
const { socket, isConnected } = useSessionSocket(sessionId)
// Session state
const {
session,
workers,
queue,
progress,
loadSession,
startSession,
pauseSession,
} = useSessionState(sessionId, socket)
// Computed
const isRunning = computed(() => session.value?.status === 'running')
const activeWorkers = computed(() => workers.value.filter(w => w.active).length)
onMounted(() => {
loadSession()
})
</script>
Validierung nach jeder Phase¶
Checkliste¶
# 1. Syntax-Check
docker compose exec backend-flask-service python -m py_compile app/routes/judge/__init__.py
# 2. Import-Check
docker compose exec backend-flask-service python -c "from app.routes.judge import judge_blueprint; print('OK')"
# 3. Tests
docker compose exec backend-flask-service pytest app/tests/ -v --tb=short
# 4. Lint
docker compose exec backend-flask-service flake8 app/routes/judge/
# 5. Frontend Build
cd llars-frontend && npm run build
# 6. Anwendung starten und testen
./start_llars.sh
Rollback-Strategie¶
Falls etwas schief geht:
# Git-Status prüfen
git status
# Änderungen verwerfen
git checkout -- app/routes/judge/
# Oder: Letzten Commit rückgängig
git revert HEAD
Nächste Schritte nach Abschluss¶
- Alte Dateien mit Deprecation-Warnings versehen
- Dokumentation aktualisieren (CLAUDE.md)
- Team über neue Struktur informieren
- In 2-3 Monaten: Alte Re-Exports entfernen