diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 7223fa3..d91e052 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -242,6 +242,7 @@ class AnalyzerAgent: result, usage = await call_claude(prompt) analysis = self._parse_response(result) if analysis: + analysis = self._sanitize_sources(analysis) logger.info(f"Erstanalyse abgeschlossen: {len(analysis.get('sources', []))} Quellen referenziert") return analysis, usage except Exception as e: @@ -303,6 +304,8 @@ class AnalyzerAgent: try: result, usage = await call_claude(prompt) analysis = self._parse_response(result) + if analysis: + analysis = self._sanitize_sources(analysis) if analysis and self._all_previous_sources: # Merge: alte Quellen beibehalten, neue hinzufuegen returned_sources = analysis.get("sources", []) @@ -325,6 +328,51 @@ class AnalyzerAgent: logger.error(f"Inkrementelle Analyse-Fehler: {e}") return None, None + def _sanitize_sources(self, analysis: dict) -> dict: + """Entfernt Buchstaben-Suffixe aus Quellennummern (z.B. '1383a' -> 1383). + + Das LLM erzeugt trotz Anweisung gelegentlich Suffix-Nummern. + Diese werden hier auf die Basisnummer normalisiert. + Duplikate werden entfernt, wobei Eintraege mit URL bevorzugt werden. + """ + sources = analysis.get("sources", []) + if not sources: + return analysis + + cleaned = {} + suffix_count = 0 + for s in sources: + nr = s.get("nr", "") + nr_str = str(nr) + # Prüfe auf Buchstaben-Suffix (z.B. "1383a", "1383b") + m = re.match(r"^(\d+)[a-z]$", nr_str) + if m: + base_nr = int(m.group(1)) + suffix_count += 1 + # Nur übernehmen wenn Basisnummer noch nicht existiert oder + # dieser Eintrag eine URL hat und der bisherige nicht + if base_nr not in cleaned: + s_copy = dict(s) + s_copy["nr"] = base_nr + cleaned[base_nr] = s_copy + elif s.get("url") and not cleaned[base_nr].get("url"): + s_copy = dict(s) + s_copy["nr"] = base_nr + cleaned[base_nr] = s_copy + else: + nr_int = int(nr) if isinstance(nr, (int, float)) or (isinstance(nr, str) and nr.isdigit()) else nr + if nr_int not in cleaned: + cleaned[nr_int] = s + elif s.get("url") and not cleaned[nr_int].get("url"): + cleaned[nr_int] = s + + if suffix_count > 0: + logger.info(f"Quellen-Sanitierung: {suffix_count} Buchstaben-Suffixe entfernt") + analysis["sources"] = sorted(cleaned.values(), + key=lambda s: s.get("nr", 0) if isinstance(s.get("nr"), int) else 9999) + + return analysis + def _parse_response(self, response: str) -> dict | None: """Parst die Claude-Antwort als JSON-Objekt mit robustem Fallback.""" # Markdown-Code-Fences entfernen diff --git a/src/static/js/components.js b/src/static/js/components.js index 8ef0507..801f84b 100644 --- a/src/static/js/components.js +++ b/src/static/js/components.js @@ -444,10 +444,17 @@ const UI = { html = html.replace(/<\/ul>(
)+/g, ''); html = html.replace(/(
){2,}/g, '
'); - // Inline-Zitate [1], [2] etc. als klickbare Links rendern + // Inline-Zitate [1], [2], [1383a] etc. als klickbare Links rendern if (sources.length > 0) { html = html.replace(/\[(\d+[a-z]?)\]/g, (match, num) => { - const src = sources.find(s => String(s.nr) === num || Number(s.nr) === Number(num)); + // Exakte Suche (auch mit Buchstaben-Suffix) + let src = sources.find(s => String(s.nr) === num || Number(s.nr) === Number(num)); + // Fallback: Bei Suffix wie "1383a" auf Basisnummer 1383 zurueckfallen + if ((!src || !src.url) && /[a-z]$/.test(num)) { + const baseNum = num.replace(/[a-z]$/, ''); + const baseSrc = sources.find(s => String(s.nr) === baseNum || Number(s.nr) === Number(baseNum)); + if (baseSrc && baseSrc.url) src = baseSrc; + } if (src && src.url) { return `[${num}]`; }