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>
365 lines
9.8 KiB
Python
365 lines
9.8 KiB
Python
"""
|
|
Claude Service
|
|
Integration mit Claude API für Content-Generierung
|
|
"""
|
|
|
|
import os
|
|
from typing import List, Dict, Any, Optional
|
|
from anthropic import Anthropic
|
|
|
|
|
|
class ClaudeService:
|
|
"""Claude API Service"""
|
|
|
|
def __init__(self):
|
|
self.api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
self.client = Anthropic(api_key=self.api_key) if self.api_key else None
|
|
self.model = "claude-sonnet-4-5-20251101" # Latest model
|
|
|
|
def is_configured(self) -> bool:
|
|
"""Check if API key is configured"""
|
|
return self.client is not None
|
|
|
|
async def generate_content(
|
|
self,
|
|
prompt: str,
|
|
system_prompt: Optional[str] = None,
|
|
max_tokens: int = 4000,
|
|
temperature: float = 1.0
|
|
) -> str:
|
|
"""
|
|
Generate content with Claude
|
|
|
|
Args:
|
|
prompt: User prompt
|
|
system_prompt: System prompt (optional)
|
|
max_tokens: Maximum tokens to generate
|
|
temperature: Sampling temperature
|
|
|
|
Returns:
|
|
Generated text
|
|
"""
|
|
if not self.client:
|
|
raise ValueError("Claude API not configured. Set ANTHROPIC_API_KEY environment variable.")
|
|
|
|
messages = [{"role": "user", "content": prompt}]
|
|
|
|
kwargs = {
|
|
"model": self.model,
|
|
"max_tokens": max_tokens,
|
|
"temperature": temperature,
|
|
"messages": messages
|
|
}
|
|
|
|
if system_prompt:
|
|
kwargs["system"] = system_prompt
|
|
|
|
response = self.client.messages.create(**kwargs)
|
|
return response.content[0].text
|
|
|
|
async def generate_quiz_questions(
|
|
self,
|
|
topic: str,
|
|
materials: List[Dict[str, Any]],
|
|
target_grade: str,
|
|
num_questions: int = 10
|
|
) -> List[Dict[str, Any]]:
|
|
"""Generate Quiz questions"""
|
|
|
|
material_text = self._format_materials(materials)
|
|
|
|
prompt = f"""Erstelle {num_questions} Multiple-Choice-Fragen zum Thema "{topic}" für Klassenstufe {target_grade}.
|
|
|
|
Materialien:
|
|
{material_text}
|
|
|
|
Erstelle Fragen die:
|
|
1. Das Verständnis testen
|
|
2. Auf den Materialien basieren
|
|
3. Altersgerecht sind
|
|
4. 4 Antwortmöglichkeiten haben (1 richtig, 3 falsch)
|
|
|
|
Formatiere die Ausgabe als JSON-Array:
|
|
[
|
|
{{
|
|
"question": "Frage text?",
|
|
"options": ["Option A", "Option B", "Option C", "Option D"],
|
|
"correct_answer": 0,
|
|
"explanation": "Erklärung warum die Antwort richtig ist"
|
|
}}
|
|
]
|
|
|
|
Nur das JSON-Array zurückgeben, keine zusätzlichen Texte."""
|
|
|
|
response = await self.generate_content(
|
|
prompt=prompt,
|
|
system_prompt="Du bist ein pädagogischer Experte der Quizfragen erstellt."
|
|
)
|
|
|
|
# Parse JSON
|
|
import json
|
|
try:
|
|
questions = json.loads(response)
|
|
return questions
|
|
except json.JSONDecodeError:
|
|
# Try to extract JSON from response
|
|
import re
|
|
json_match = re.search(r'\[.*\]', response, re.DOTALL)
|
|
if json_match:
|
|
questions = json.loads(json_match.group())
|
|
return questions
|
|
raise ValueError("Could not parse quiz questions from Claude response")
|
|
|
|
async def generate_flashcards(
|
|
self,
|
|
topic: str,
|
|
materials: List[Dict[str, Any]],
|
|
target_grade: str,
|
|
num_cards: int = 15
|
|
) -> List[Dict[str, str]]:
|
|
"""Generate Flashcards"""
|
|
|
|
material_text = self._format_materials(materials)
|
|
|
|
prompt = f"""Erstelle {num_cards} Lernkarten (Flashcards) zum Thema "{topic}" für Klassenstufe {target_grade}.
|
|
|
|
Materialien:
|
|
{material_text}
|
|
|
|
Erstelle Karten die:
|
|
1. Wichtige Begriffe und Konzepte abdecken
|
|
2. Kurz und prägnant sind
|
|
3. Zum Wiederholen geeignet sind
|
|
|
|
Formatiere die Ausgabe als JSON-Array:
|
|
[
|
|
{{
|
|
"front": "Begriff oder Frage",
|
|
"back": "Definition oder Antwort"
|
|
}}
|
|
]
|
|
|
|
Nur das JSON-Array zurückgeben."""
|
|
|
|
response = await self.generate_content(
|
|
prompt=prompt,
|
|
system_prompt="Du bist ein Experte für Lernkarten-Design."
|
|
)
|
|
|
|
import json
|
|
import re
|
|
json_match = re.search(r'\[.*\]', response, re.DOTALL)
|
|
if json_match:
|
|
return json.loads(json_match.group())
|
|
return json.loads(response)
|
|
|
|
async def generate_fill_blanks_text(
|
|
self,
|
|
topic: str,
|
|
materials: List[Dict[str, Any]],
|
|
target_grade: str
|
|
) -> Dict[str, Any]:
|
|
"""Generate Fill-in-the-Blanks exercise"""
|
|
|
|
material_text = self._format_materials(materials)
|
|
|
|
prompt = f"""Erstelle einen Lückentext zum Thema "{topic}" für Klassenstufe {target_grade}.
|
|
|
|
Materialien:
|
|
{material_text}
|
|
|
|
Erstelle einen Text mit 10-15 Lücken. Markiere Lücken mit *Wort*.
|
|
|
|
Formatiere als JSON:
|
|
{{
|
|
"title": "Titel des Lückentexts",
|
|
"text": "Der Text mit *Lücken* markiert...",
|
|
"hints": "Hilfreiche Hinweise"
|
|
}}
|
|
|
|
Nur JSON zurückgeben."""
|
|
|
|
response = await self.generate_content(
|
|
prompt=prompt,
|
|
system_prompt="Du bist ein Experte für Lückentexte."
|
|
)
|
|
|
|
import json
|
|
import re
|
|
json_match = re.search(r'\{.*\}', response, re.DOTALL)
|
|
if json_match:
|
|
return json.loads(json_match.group())
|
|
return json.loads(response)
|
|
|
|
async def generate_drag_drop_exercise(
|
|
self,
|
|
topic: str,
|
|
materials: List[Dict[str, Any]],
|
|
target_grade: str
|
|
) -> Dict[str, Any]:
|
|
"""Generate Drag-and-Drop exercise"""
|
|
|
|
material_text = self._format_materials(materials)
|
|
|
|
prompt = f"""Erstelle eine Drag-and-Drop Zuordnungsaufgabe zum Thema "{topic}" für Klassenstufe {target_grade}.
|
|
|
|
Materialien:
|
|
{material_text}
|
|
|
|
Erstelle 3-4 Kategorien (Zonen) und 8-12 Elemente zum Zuordnen.
|
|
|
|
Formatiere als JSON:
|
|
{{
|
|
"title": "Titel der Aufgabe",
|
|
"question": "Aufgabenstellung",
|
|
"zones": [
|
|
{{ "id": 1, "name": "Kategorie 1" }},
|
|
{{ "id": 2, "name": "Kategorie 2" }}
|
|
],
|
|
"draggables": [
|
|
{{ "id": 1, "text": "Element 1", "correctZoneId": 1 }},
|
|
{{ "id": 2, "text": "Element 2", "correctZoneId": 2 }}
|
|
]
|
|
}}
|
|
|
|
Nur JSON zurückgeben."""
|
|
|
|
response = await self.generate_content(
|
|
prompt=prompt,
|
|
system_prompt="Du bist ein Experte für interaktive Lernaufgaben."
|
|
)
|
|
|
|
import json
|
|
import re
|
|
json_match = re.search(r'\{.*\}', response, re.DOTALL)
|
|
if json_match:
|
|
return json.loads(json_match.group())
|
|
return json.loads(response)
|
|
|
|
async def generate_memory_pairs(
|
|
self,
|
|
topic: str,
|
|
materials: List[Dict[str, Any]],
|
|
target_grade: str,
|
|
num_pairs: int = 8
|
|
) -> List[Dict[str, str]]:
|
|
"""Generate Memory Game pairs"""
|
|
|
|
material_text = self._format_materials(materials)
|
|
|
|
prompt = f"""Erstelle {num_pairs} Memory-Paare zum Thema "{topic}" für Klassenstufe {target_grade}.
|
|
|
|
Materialien:
|
|
{material_text}
|
|
|
|
Jedes Paar besteht aus zwei zusammengehörigen Begriffen/Konzepten.
|
|
|
|
Formatiere als JSON-Array:
|
|
[
|
|
{{ "card1": "Begriff 1", "card2": "Zugehöriger Begriff" }}
|
|
]
|
|
|
|
Nur JSON zurückgeben."""
|
|
|
|
response = await self.generate_content(
|
|
prompt=prompt,
|
|
system_prompt="Du bist ein Experte für Memory-Spiele."
|
|
)
|
|
|
|
import json
|
|
import re
|
|
json_match = re.search(r'\[.*\]', response, re.DOTALL)
|
|
if json_match:
|
|
return json.loads(json_match.group())
|
|
return json.loads(response)
|
|
|
|
async def generate_timeline_events(
|
|
self,
|
|
topic: str,
|
|
materials: List[Dict[str, Any]],
|
|
target_grade: str
|
|
) -> List[Dict[str, Any]]:
|
|
"""Generate Timeline events"""
|
|
|
|
material_text = self._format_materials(materials)
|
|
|
|
prompt = f"""Erstelle eine Timeline mit 5-8 Ereignissen zum Thema "{topic}" für Klassenstufe {target_grade}.
|
|
|
|
Materialien:
|
|
{material_text}
|
|
|
|
Formatiere als JSON-Array:
|
|
[
|
|
{{
|
|
"year": "Jahr oder Zeitpunkt",
|
|
"title": "Ereignis Titel",
|
|
"description": "Kurze Beschreibung"
|
|
}}
|
|
]
|
|
|
|
Nur JSON zurückgeben."""
|
|
|
|
response = await self.generate_content(
|
|
prompt=prompt,
|
|
system_prompt="Du bist ein Experte für chronologische Darstellungen."
|
|
)
|
|
|
|
import json
|
|
import re
|
|
json_match = re.search(r'\[.*\]', response, re.DOTALL)
|
|
if json_match:
|
|
return json.loads(json_match.group())
|
|
return json.loads(response)
|
|
|
|
async def generate_presentation_slides(
|
|
self,
|
|
topic: str,
|
|
materials: List[Dict[str, Any]],
|
|
target_grade: str,
|
|
num_slides: int = 5
|
|
) -> List[Dict[str, str]]:
|
|
"""Generate Presentation slides"""
|
|
|
|
material_text = self._format_materials(materials)
|
|
|
|
prompt = f"""Erstelle {num_slides} Präsentationsfolien zum Thema "{topic}" für Klassenstufe {target_grade}.
|
|
|
|
Materialien:
|
|
{material_text}
|
|
|
|
Formatiere als JSON-Array:
|
|
[
|
|
{{
|
|
"title": "Folien Titel",
|
|
"content": "Folien Inhalt (2-4 Sätze)",
|
|
"backgroundColor": "#ffffff"
|
|
}}
|
|
]
|
|
|
|
Nur JSON zurückgeben."""
|
|
|
|
response = await self.generate_content(
|
|
prompt=prompt,
|
|
system_prompt="Du bist ein Experte für Präsentationen."
|
|
)
|
|
|
|
import json
|
|
import re
|
|
json_match = re.search(r'\[.*\]', response, re.DOTALL)
|
|
if json_match:
|
|
return json.loads(json_match.group())
|
|
return json.loads(response)
|
|
|
|
def _format_materials(self, materials: List[Dict[str, Any]]) -> str:
|
|
"""Format materials for prompt"""
|
|
if not materials:
|
|
return "Keine Materialien vorhanden."
|
|
|
|
formatted = []
|
|
for i, material in enumerate(materials, 1):
|
|
formatted.append(f"Material {i} ({material.get('type', 'unknown')}):")
|
|
formatted.append(material.get('content', '')[:2000]) # Limit content
|
|
formatted.append("")
|
|
|
|
return "\n".join(formatted)
|