Files
breakpilot-lehrer/backend-lehrer/generators/mindmap_generator.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

381 lines
11 KiB
Python

"""
Mindmap Generator - Erstellt Mindmaps aus Quelltexten.
Generiert:
- Hierarchische Struktur aus Text
- Hauptthema mit Unterthemen
- Verbindungen und Beziehungen
"""
import logging
import json
import re
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class MindmapNode:
"""Ein Knoten in der Mindmap."""
id: str
label: str
level: int = 0
children: List['MindmapNode'] = field(default_factory=list)
color: Optional[str] = None
icon: Optional[str] = None
notes: Optional[str] = None
@dataclass
class Mindmap:
"""Eine komplette Mindmap."""
root: MindmapNode
title: str
topic: Optional[str] = None
total_nodes: int = 0
class MindmapGenerator:
"""
Generiert Mindmaps aus Quelltexten.
Extrahiert:
- Hauptthema als Zentrum
- Unterthemen als Äste
- Details als Blätter
"""
def __init__(self, llm_client=None):
"""
Initialisiert den Generator.
Args:
llm_client: Optional - LLM-Client für intelligente Generierung
"""
self.llm_client = llm_client
logger.info("MindmapGenerator initialized")
# Farben für verschiedene Ebenen
self.level_colors = [
"#6C1B1B", # Weinrot (Zentrum)
"#3b82f6", # Blau
"#22c55e", # Grün
"#f59e0b", # Orange
"#8b5cf6", # Violett
]
def generate(
self,
source_text: str,
title: Optional[str] = None,
max_depth: int = 3,
topic: Optional[str] = None
) -> Mindmap:
"""
Generiert eine Mindmap aus einem Quelltext.
Args:
source_text: Der Ausgangstext
title: Optionaler Titel (sonst automatisch ermittelt)
max_depth: Maximale Tiefe der Hierarchie
topic: Optionales Thema
Returns:
Mindmap-Objekt
"""
logger.info(f"Generating mindmap (max_depth: {max_depth})")
if not source_text or len(source_text.strip()) < 50:
logger.warning("Source text too short")
return self._empty_mindmap(title or "Mindmap")
if self.llm_client:
return self._generate_with_llm(source_text, title, max_depth, topic)
else:
return self._generate_automatic(source_text, title, max_depth, topic)
def _generate_with_llm(
self,
source_text: str,
title: Optional[str],
max_depth: int,
topic: Optional[str]
) -> Mindmap:
"""Generiert Mindmap mit LLM."""
prompt = f"""
Erstelle eine Mindmap-Struktur auf Deutsch basierend auf folgendem Text.
{f'Titel: {title}' if title else 'Ermittle einen passenden Titel.'}
Maximale Tiefe: {max_depth} Ebenen
{f'Thema: {topic}' if topic else ''}
Text:
{source_text}
Erstelle eine hierarchische Struktur mit:
- Einem zentralen Hauptthema
- 3-5 Hauptästen (Unterthemen)
- Jeweils 2-4 Details pro Ast
Antworte im JSON-Format:
{{
"title": "Hauptthema",
"branches": [
{{
"label": "Unterthema 1",
"children": [
{{"label": "Detail 1.1"}},
{{"label": "Detail 1.2"}}
]
}},
{{
"label": "Unterthema 2",
"children": [
{{"label": "Detail 2.1"}}
]
}}
]
}}
"""
try:
response = self.llm_client.generate(prompt)
data = json.loads(response)
return self._create_mindmap_from_llm(data, topic)
except Exception as e:
logger.error(f"Error generating with LLM: {e}")
return self._generate_automatic(source_text, title, max_depth, topic)
def _generate_automatic(
self,
source_text: str,
title: Optional[str],
max_depth: int,
topic: Optional[str]
) -> Mindmap:
"""Generiert Mindmap automatisch ohne LLM."""
# Extrahiere Struktur aus Text
sections = self._extract_sections(source_text)
# Bestimme Titel
if not title:
# Erste Zeile oder erstes Substantiv
first_line = source_text.split('\n')[0].strip()
title = first_line[:50] if first_line else "Mindmap"
# Erstelle Root-Knoten
node_counter = [0]
root = self._create_node(title, 0, node_counter)
# Füge Hauptäste hinzu
for section_title, section_content in sections[:5]: # Max 5 Hauptäste
branch = self._create_node(section_title, 1, node_counter)
branch.color = self.level_colors[1 % len(self.level_colors)]
# Füge Details hinzu
details = self._extract_details(section_content)
for detail in details[:4]: # Max 4 Details pro Ast
if max_depth >= 2:
leaf = self._create_node(detail, 2, node_counter)
leaf.color = self.level_colors[2 % len(self.level_colors)]
branch.children.append(leaf)
root.children.append(branch)
return Mindmap(
root=root,
title=title,
topic=topic,
total_nodes=node_counter[0]
)
def _extract_sections(self, text: str) -> List[tuple]:
"""Extrahiert Abschnitte aus dem Text."""
sections = []
# Versuche Überschriften zu finden
lines = text.split('\n')
current_section = None
current_content = []
for line in lines:
line = line.strip()
if not line:
continue
# Erkenne potenzielle Überschriften
if (line.endswith(':') or
line.isupper() or
len(line) < 50 and line[0].isupper() and '.' not in line):
# Speichere vorherige Section
if current_section:
sections.append((current_section, '\n'.join(current_content)))
current_section = line.rstrip(':')
current_content = []
else:
current_content.append(line)
# Letzte Section
if current_section:
sections.append((current_section, '\n'.join(current_content)))
# Falls keine Sections gefunden, erstelle aus Sätzen
if not sections:
sentences = re.split(r'[.!?]+', text)
for i, sentence in enumerate(sentences[:5]):
sentence = sentence.strip()
if len(sentence) > 10:
# Kürze auf max 30 Zeichen für Label
label = sentence[:30] + '...' if len(sentence) > 30 else sentence
sections.append((label, sentence))
return sections
def _extract_details(self, content: str) -> List[str]:
"""Extrahiert Details aus Abschnittsinhalt."""
details = []
# Aufzählungen
bullet_pattern = r'[-•*]\s*(.+)'
bullets = re.findall(bullet_pattern, content)
details.extend(bullets)
# Nummerierte Listen
num_pattern = r'\d+[.)]\s*(.+)'
numbered = re.findall(num_pattern, content)
details.extend(numbered)
# Falls keine Listen, nehme Sätze
if not details:
sentences = re.split(r'[.!?]+', content)
for sentence in sentences:
sentence = sentence.strip()
if len(sentence) > 5:
label = sentence[:40] + '...' if len(sentence) > 40 else sentence
details.append(label)
return details
def _create_node(
self,
label: str,
level: int,
counter: List[int]
) -> MindmapNode:
"""Erstellt einen neuen Knoten."""
counter[0] += 1
return MindmapNode(
id=f"node_{counter[0]}",
label=label,
level=level,
children=[],
color=self.level_colors[level % len(self.level_colors)]
)
def _create_mindmap_from_llm(
self,
data: Dict[str, Any],
topic: Optional[str]
) -> Mindmap:
"""Erstellt Mindmap aus LLM-Antwort."""
node_counter = [0]
title = data.get("title", "Mindmap")
root = self._create_node(title, 0, node_counter)
for branch_data in data.get("branches", []):
branch = self._create_node(branch_data.get("label", ""), 1, node_counter)
branch.color = self.level_colors[1 % len(self.level_colors)]
for child_data in branch_data.get("children", []):
child = self._create_node(child_data.get("label", ""), 2, node_counter)
child.color = self.level_colors[2 % len(self.level_colors)]
branch.children.append(child)
root.children.append(branch)
return Mindmap(
root=root,
title=title,
topic=topic,
total_nodes=node_counter[0]
)
def _empty_mindmap(self, title: str) -> Mindmap:
"""Erstellt leere Mindmap bei Fehler."""
root = MindmapNode(
id="root",
label=title,
level=0,
color=self.level_colors[0]
)
return Mindmap(root=root, title=title, total_nodes=1)
def to_dict(self, mindmap: Mindmap) -> Dict[str, Any]:
"""Konvertiert Mindmap zu Dictionary-Format."""
def node_to_dict(node: MindmapNode) -> Dict[str, Any]:
return {
"id": node.id,
"label": node.label,
"level": node.level,
"color": node.color,
"icon": node.icon,
"notes": node.notes,
"children": [node_to_dict(child) for child in node.children]
}
return {
"title": mindmap.title,
"topic": mindmap.topic,
"total_nodes": mindmap.total_nodes,
"root": node_to_dict(mindmap.root)
}
def to_mermaid(self, mindmap: Mindmap) -> str:
"""
Konvertiert Mindmap zu Mermaid-Format für Visualisierung.
Args:
mindmap: Mindmap-Objekt
Returns:
Mermaid-Diagramm als String
"""
lines = ["mindmap"]
lines.append(f" root(({mindmap.title}))")
def add_node(node: MindmapNode, indent: int):
for child in node.children:
prefix = " " * (indent + 1)
if child.children:
lines.append(f"{prefix}{child.label}")
else:
lines.append(f"{prefix}){child.label}(")
add_node(child, indent + 1)
add_node(mindmap.root, 1)
return "\n".join(lines)
def to_json_tree(self, mindmap: Mindmap) -> Dict[str, Any]:
"""
Konvertiert Mindmap zu JSON-Tree-Format für JS-Bibliotheken.
Args:
mindmap: Mindmap-Objekt
Returns:
JSON-Tree-Format für d3.js, vis.js etc.
"""
def node_to_tree(node: MindmapNode) -> Dict[str, Any]:
result = {
"name": node.label,
"id": node.id,
"color": node.color
}
if node.children:
result["children"] = [node_to_tree(c) for c in node.children]
return result
return node_to_tree(mindmap.root)