""" 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)