""" Result Container Parser (Phase 1) Parsed strukturierte LLM-Antworten (Markdown-Sektionen). Konzept-Basis: konzept_workflow_engine_konsolidated.md (Sektion 8.2) Anforderungsanalyse: anforderungsanalyse_umsetzungsplan.md (Sektion 4.1) Erwartet Format (Markdown-Sektionen): ``` ## Analyse [Hauptinhalt] ## Entscheidungsfragen - Relevanz: ja - Priorität: hoch ## Begründung [Optional: Plausibilisierung] ``` Output: ```python { "analysis_core": str, "decision_signals": Dict[str, str], # {"relevanz": "ja", "prioritaet": "hoch"} "reasoning_anchors": Optional[str] } ``` """ import re from typing import Dict, Optional, List, Tuple def parse_result_container(llm_output: str) -> Dict: """ Parsed strukturierte LLM-Antwort in Ergebniscontainer. Extrahiert drei Bereiche: 1. **Analysekern** (## Analyse) 2. **Entscheidungsanteil** (## Entscheidungsfragen) 3. **Begründungsanker** (## Begründung, optional) Args: llm_output: Vollständiger LLM-Output als String Returns: Dict mit drei Bereichen: { "analysis_core": str, "decision_signals": Dict[str, str], "reasoning_anchors": Optional[str], "parsing_status": str # "complete", "partial", "failed" } Raises: ValueError: Bei komplett fehlgeschlagenem Parsing """ # Extrahiere Sektionen analysis_core = extract_section(llm_output, "Analyse") decision_section = extract_section(llm_output, "Entscheidungsfragen") reasoning_anchors = extract_section(llm_output, "Begründung") # Parse Entscheidungsfragen decision_signals = {} if decision_section: decision_signals = parse_decision_questions(decision_section) # Determine parsing status if analysis_core and decision_signals: parsing_status = "complete" elif analysis_core or decision_signals: parsing_status = "partial" else: parsing_status = "failed" # Fallback: Wenn keine Strukturierung erkannt, verwende gesamten Output als Analysekern if parsing_status == "failed": analysis_core = llm_output parsing_status = "fallback" return { "analysis_core": analysis_core or "", "decision_signals": decision_signals, "reasoning_anchors": reasoning_anchors, "parsing_status": parsing_status } def extract_section(text: str, section_name: str) -> Optional[str]: """ Extrahiert eine Markdown-Sektion aus dem Text. Sucht nach: - `## Section_Name` (Überschrift) - Alles bis zur nächsten `##` Überschrift Args: text: Volltext section_name: Name der Sektion (z.B. "Analyse", "Entscheidungsfragen") Returns: Sektionsinhalt (ohne Überschrift) oder None wenn nicht gefunden """ # Pattern: ## Section_Name (mit optionalem Whitespace) # Captured content bis zur nächsten ## oder Ende pattern = rf'##\s*{re.escape(section_name)}\s*\n(.*?)(?=\n##|\Z)' match = re.search(pattern, text, re.DOTALL | re.IGNORECASE) if match: content = match.group(1).strip() return content if content else None return None def parse_decision_questions(section_text: str) -> Dict[str, str]: """ Parsed Entscheidungsfragen aus Sektion. Erwartet Format: ``` - Relevanz: ja - Priorität: hoch - Selektion: nein ``` Alternativen: ``` Relevanz: ja Priorität: hoch ``` oder: ``` - **Relevanz**: ja - **Priorität**: hoch ``` Args: section_text: Text der Entscheidungsfragen-Sektion Returns: Dict mit {frage_typ: antwort} Beispiel: {"relevanz": "ja", "prioritaet": "hoch"} """ signals = {} # Pattern: Matcht verschiedene Formatvariationen # Gruppe 1: Fragetyp (z.B. "Relevanz", "**Priorität**") # Gruppe 2: Antwort (z.B. "ja", "hoch") patterns = [ r'-\s*\*?\*?(\w+)\*?\*?\s*:\s*\[?([^\]\n]+)\]?', # "- **Relevanz**: [ja]" r'^\s*\*?\*?(\w+)\*?\*?\s*:\s*\[?([^\]\n]+)\]?', # "Relevanz: ja" (ohne -) ] for pattern in patterns: matches = re.finditer(pattern, section_text, re.MULTILINE | re.IGNORECASE) for match in matches: question_type = match.group(1).strip().lower() answer = match.group(2).strip() # Entferne Klammern und Whitespace answer = answer.strip('[]()').strip() signals[question_type] = answer return signals def validate_decision_signal( signal_value: str, answer_spectrum: List[str] ) -> Tuple[str, str]: """ Validiert ein Entscheidungssignal gegen Antwortspektrum. Gibt zurück: - (normalized_value, status) Status: - "valid": Antwort exakt im Spektrum - "normalized": Antwort wurde normalisiert (z.B. "Ja" → "ja") - "invalid": Antwort außerhalb des Spektrums Args: signal_value: Rohe Antwort vom LLM answer_spectrum: Erlaubte Werte (z.B. ["ja", "nein", "unklar"]) Returns: Tuple (normalisierter_wert, status) """ # Exakte Übereinstimmung (case-sensitive) if signal_value in answer_spectrum: return signal_value, "valid" # Case-insensitive Matching signal_lower = signal_value.lower() for allowed in answer_spectrum: if signal_lower == allowed.lower(): return allowed, "normalized" # Keine Übereinstimmung return signal_value, "invalid" def extract_analysis_core_fallback(llm_output: str) -> str: """ Fallback-Methode: Extrahiert Analysekern wenn keine Strukturierung erkannt. Versucht intelligent zu identifizieren was der Hauptinhalt ist: 1. Wenn Text vor "## Entscheidungsfragen" existiert: Verwende das 2. Sonst: Verwende gesamten Text Args: llm_output: Vollständiger LLM-Output Returns: Best-Guess für Analysekern """ # Suche nach "## Entscheidungsfragen" Marker pattern = r'##\s*Entscheidungsfragen' match = re.search(pattern, llm_output, re.IGNORECASE) if match: # Text vor dem Marker ist wahrscheinlich die Analyse return llm_output[:match.start()].strip() # Fallback: Gesamter Text return llm_output.strip() def parse_result_container_robust( llm_output: str, expected_questions: Optional[List[str]] = None ) -> Dict: """ Robuste Variante des Parsings mit zusätzlicher Validierung. Args: llm_output: Vollständiger LLM-Output expected_questions: Optionale Liste erwarteter Fragetypen (z.B. ["relevanz", "prioritaet"]) Returns: Ergebniscontainer mit zusätzlichem "warnings"-Feld """ result = parse_result_container(llm_output) warnings = [] # Prüfe ob erwartete Fragen vorhanden if expected_questions: found_questions = set(result['decision_signals'].keys()) expected_set = set(expected_questions) missing = expected_set - found_questions if missing: warnings.append(f"Fehlende Entscheidungsfragen: {', '.join(missing)}") unexpected = found_questions - expected_set if unexpected: warnings.append(f"Unerwartete Entscheidungsfragen: {', '.join(unexpected)}") # Prüfe Parsing-Status if result['parsing_status'] == "partial": warnings.append("Parsing unvollständig: Einige Sektionen fehlen") elif result['parsing_status'] == "fallback": warnings.append("Keine Strukturierung erkannt, Fallback-Parsing verwendet") result['warnings'] = warnings return result