update
This commit is contained in:
@@ -14,6 +14,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
@@ -46,6 +47,9 @@ RECHERCHE_BATCH = 20 # Crawl-Seiten je Batch
|
|||||||
RECHERCHE_READERS = 2 # Reader-Agenten je Batch (Konsens ≥2 innerhalb des Batches)
|
RECHERCHE_READERS = 2 # Reader-Agenten je Batch (Konsens ≥2 innerhalb des Batches)
|
||||||
RECHERCHE_THEMA_AGENTEN = 5 # Web-Modus (Quelle „thema", kein Crawl-Ordner)
|
RECHERCHE_THEMA_AGENTEN = 5 # Web-Modus (Quelle „thema", kein Crawl-Ordner)
|
||||||
RECHERCHE_KAPPE = 1800 # Sicherheits-Deckel je Batch-Agent
|
RECHERCHE_KAPPE = 1800 # Sicherheits-Deckel je Batch-Agent
|
||||||
|
# uni/projekt: Skript-Text in Abschnitte ~dieser Größe chunken (gegen Lost-in-the-Middle bei
|
||||||
|
# großen Dokumenten). ~12k Zeichen ≈ 3k Token → sicher unter der Recall-Abfall-Schwelle.
|
||||||
|
RECHERCHE_ABSCHNITT_ZEICHEN = 12000
|
||||||
# Sichtung (Content/Noise) ist jetzt ein deterministischer Regel-Filter (config.CRAWL_*).
|
# Sichtung (Content/Noise) ist jetzt ein deterministischer Regel-Filter (config.CRAWL_*).
|
||||||
SUBBAUSTEIN_KAPPE = 900 # Subbaustein-Finde-Loop je Chunk (15 min)
|
SUBBAUSTEIN_KAPPE = 900 # Subbaustein-Finde-Loop je Chunk (15 min)
|
||||||
KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); darüber chunked + Merge-Pass
|
KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); darüber chunked + Merge-Pass
|
||||||
@@ -478,8 +482,47 @@ def _pdfs_konvertieren(project: Path) -> None:
|
|||||||
_QUELLE_TEMPLATE = {"projekt": "Bausteine-Quelle-Projekt", "uni": "Bausteine-Quelle-Uni", "link": "Bausteine-Quelle-Link"}
|
_QUELLE_TEMPLATE = {"projekt": "Bausteine-Quelle-Projekt", "uni": "Bausteine-Quelle-Uni", "link": "Bausteine-Quelle-Link"}
|
||||||
|
|
||||||
|
|
||||||
def _build_recherche_prompt(topic: str, out_path: Path, instructions: str, typ: str, ordner: Path | None, fokus: str = "") -> str:
|
def _text_abschnitte(text: str, ziel: int = RECHERCHE_ABSCHNITT_ZEICHEN) -> list[str]:
|
||||||
if typ in _QUELLE_TEMPLATE:
|
"""Text an Absatz-/Zeilengrenzen in Abschnitte ~`ziel` Zeichen splitten (gegen Lost-in-the-Middle
|
||||||
|
bei großen Dokumenten). Kleiner Text bleibt EIN Abschnitt. Inhalt bleibt vollständig — nur
|
||||||
|
Trenn-Whitespace fällt weg."""
|
||||||
|
text = text.strip()
|
||||||
|
if len(text) <= ziel:
|
||||||
|
return [text] if text else []
|
||||||
|
abschnitte: list[str] = []
|
||||||
|
buf = ""
|
||||||
|
|
||||||
|
def flush():
|
||||||
|
nonlocal buf
|
||||||
|
if buf.strip():
|
||||||
|
abschnitte.append(buf.strip())
|
||||||
|
buf = ""
|
||||||
|
|
||||||
|
for block in re.split(r"\n\s*\n", text): # an Absatz-Grenzen
|
||||||
|
block = block.strip()
|
||||||
|
if not block:
|
||||||
|
continue
|
||||||
|
if len(block) > ziel: # einzelner Riesen-Absatz → hart an Zeilen schneiden
|
||||||
|
flush()
|
||||||
|
for zeile in block.split("\n"):
|
||||||
|
if buf and len(buf) + len(zeile) + 1 > ziel:
|
||||||
|
flush()
|
||||||
|
buf += zeile + "\n"
|
||||||
|
flush()
|
||||||
|
elif buf and len(buf) + len(block) + 2 > ziel:
|
||||||
|
flush()
|
||||||
|
buf = block
|
||||||
|
else:
|
||||||
|
buf = (buf + "\n\n" + block) if buf else block
|
||||||
|
flush()
|
||||||
|
return abschnitte
|
||||||
|
|
||||||
|
|
||||||
|
def _build_recherche_prompt(topic: str, out_path: Path, instructions: str, typ: str, ordner: Path | None, fokus: str = "", abschnitt: str = "") -> str:
|
||||||
|
if abschnitt:
|
||||||
|
# Abschnitt-Modus (uni/projekt): Text direkt im Prompt → kleiner Kontext, kein Datei-Lesen.
|
||||||
|
source = abschnitt
|
||||||
|
elif typ in _QUELLE_TEMPLATE:
|
||||||
source = _prompt(_QUELLE_TEMPLATE[typ], project=ordner)
|
source = _prompt(_QUELLE_TEMPLATE[typ], project=ordner)
|
||||||
else:
|
else:
|
||||||
source = _prompt("Bausteine-Quelle-Thema", topic=topic)
|
source = _prompt("Bausteine-Quelle-Thema", topic=topic)
|
||||||
@@ -615,7 +658,11 @@ async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict
|
|||||||
→ {Baustein-Titel: [Subbaustein, …]} (Konsens) oder None. Befüllt DB-Tabelle `subbausteine`."""
|
→ {Baustein-Titel: [Subbaustein, …]} (Konsens) oder None. Befüllt DB-Tabelle `subbausteine`."""
|
||||||
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
arbeit = files["arbeit"]
|
arbeit = files["arbeit"]
|
||||||
caps = "files" if quelle_ordner(topic) else "full"
|
ordner = quelle_ordner(topic)
|
||||||
|
caps = "files" if ordner else "full"
|
||||||
|
# Quelle für die Beleg-Prüfung im Klär-Schritt (verwirft erfundene/unbelegbare Subs).
|
||||||
|
_typ = lade_quelle(topic).get("type", "thema")
|
||||||
|
source = _prompt(_QUELLE_TEMPLATE[_typ], project=ordner) if _typ in _QUELLE_TEMPLATE else _prompt("Bausteine-Quelle-Thema", topic=topic)
|
||||||
nums = list(entries)
|
nums = list(entries)
|
||||||
chunks = _chunk_nums(nums, _n_chunks(len(nums)))
|
chunks = _chunk_nums(nums, _n_chunks(len(nums)))
|
||||||
n = len(chunks)
|
n = len(chunks)
|
||||||
@@ -711,8 +758,8 @@ async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict
|
|||||||
status, _ = await run_single_slot(
|
status, _ = await run_single_slot(
|
||||||
ctx, f"Subbaustein-Klärung {c}",
|
ctx, f"Subbaustein-Klärung {c}",
|
||||||
key=f"bausteine-{topic}-subbaustein-final-c{c}",
|
key=f"bausteine-{topic}-subbaustein-final-c{c}",
|
||||||
prompt=_prompt("Subbaustein-Mapping", topic=topic, bausteine="\n\n".join(bloecke), out_path=fp, extra=_extra(instructions)),
|
prompt=_prompt("Subbaustein-Mapping", topic=topic, source=source, bausteine="\n\n".join(bloecke), out_path=fp, extra=_extra(instructions)),
|
||||||
role="judge", capabilities="files",
|
role="judge", capabilities=caps,
|
||||||
payload=lambda result, p=fp: _parse_subbausteine(_read(p)) or None,
|
payload=lambda result, p=fp: _parse_subbausteine(_read(p)) or None,
|
||||||
timeout=_timeout("subbaustein_check", len(chunk)),
|
timeout=_timeout("subbaustein_check", len(chunk)),
|
||||||
)
|
)
|
||||||
@@ -917,8 +964,9 @@ def _fakten_schema(data) -> list[dict] | None:
|
|||||||
return out or None
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
def _fakten_check_schema(data) -> list[str] | None:
|
def _fakten_check_schema(data) -> list[tuple[str, bool]] | None:
|
||||||
"""Fakten-Check → beanstandete sub_norms · {ok:true}→[] · None bei ungültig."""
|
"""Fakten-Check → [(sub_norm, verwerfen)] je Beanstandung · {ok:true}→[] · None bei ungültig.
|
||||||
|
verwerfen=True: Sub inhaltlich nicht belegbar (entfernen). verwerfen=False: nur Fakt korrigieren."""
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
return None
|
return None
|
||||||
if data.get("ok") is True:
|
if data.get("ok") is True:
|
||||||
@@ -926,7 +974,8 @@ def _fakten_check_schema(data) -> list[str] | None:
|
|||||||
pr = data.get("probleme")
|
pr = data.get("probleme")
|
||||||
if not isinstance(pr, list):
|
if not isinstance(pr, list):
|
||||||
return None
|
return None
|
||||||
return [sn for p in pr if isinstance(p, dict) and (sn := _norm_titel(str(p.get("subbaustein", ""))))]
|
return [(sn, bool(p.get("verwerfen")))
|
||||||
|
for p in pr if isinstance(p, dict) and (sn := _norm_titel(str(p.get("subbaustein", ""))))]
|
||||||
|
|
||||||
|
|
||||||
def _fakten_zeilen(fk: dict) -> str:
|
def _fakten_zeilen(fk: dict) -> str:
|
||||||
@@ -950,10 +999,11 @@ def _fakten_komplett(files: dict) -> bool:
|
|||||||
return isinstance(d, dict) and bool(d)
|
return isinstance(d, dict) and bool(d)
|
||||||
|
|
||||||
|
|
||||||
async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, instructions: str) -> dict | None:
|
async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, instructions: str) -> tuple | None:
|
||||||
"""Block: je Sub Quell-Fakten extrahieren (finden) → verifizieren (prüfen) → korrigieren (fix).
|
"""Block: je Sub Quell-Fakten extrahieren (finden) → verifizieren (prüfen) → korrigieren/verwerfen (fix).
|
||||||
Extract-once-Grounding: das Ergebnis nährt Stufe/Relevanz/Fragen/Guide.
|
Extract-once-Grounding: das Ergebnis nährt Stufe/Relevanz/Fragen/Guide.
|
||||||
→ {Baustein-Titel: {sub_norm: fakten-dict}} oder None bei Abbruch/Fehler."""
|
→ (fakten_map, verworfen_map) — fakten_map {Baustein: {sub_norm: fakten}}, verworfen_map
|
||||||
|
{Baustein: {sub_norm}} (unbelegbare Subs zum Entfernen) — oder None bei Abbruch/Fehler."""
|
||||||
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
arbeit = files["arbeit"]
|
arbeit = files["arbeit"]
|
||||||
caps = "files" if ordner else "full"
|
caps = "files" if ordner else "full"
|
||||||
@@ -961,7 +1011,7 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
|
|||||||
source = _prompt(_QUELLE_TEMPLATE[typ], project=ordner) if typ in _QUELLE_TEMPLATE else _prompt("Bausteine-Quelle-Thema", topic=topic)
|
source = _prompt(_QUELLE_TEMPLATE[typ], project=ordner) if typ in _QUELLE_TEMPLATE else _prompt("Bausteine-Quelle-Thema", topic=topic)
|
||||||
bausteine = [(titel, [str(s).strip() for s in subs if str(s).strip()]) for titel, subs in roh.items() if subs]
|
bausteine = [(titel, [str(s).strip() for s in subs if str(s).strip()]) for titel, subs in roh.items() if subs]
|
||||||
if not bausteine:
|
if not bausteine:
|
||||||
return {}
|
return {}, {}
|
||||||
chunks = _lpt_chunks([len(subs) for _, subs in bausteine], FAKTEN_CHUNK_SUBS)
|
chunks = _lpt_chunks([len(subs) for _, subs in bausteine], FAKTEN_CHUNK_SUBS)
|
||||||
|
|
||||||
def roh_path(ci): return arbeit / f"fakten-c{ci}.json"
|
def roh_path(ci): return arbeit / f"fakten-c{ci}.json"
|
||||||
@@ -1008,11 +1058,12 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
|
|||||||
_bausteine_errors[topic] = "Fakten-Extraktion fehlgeschlagen"
|
_bausteine_errors[topic] = "Fakten-Extraktion fehlgeschlagen"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Phase „Fakten prüfen": FAKTEN_CHECK_PANEL Judges je Chunk → beanstandete sub_norms (Mehrheit).
|
# Phase „Fakten prüfen": FAKTEN_CHECK_PANEL Judges je Chunk. Zwei Mehrheits-Mengen:
|
||||||
|
# beanstandet (Fakt ungenau → korrigieren) und verwerfen (Sub nicht belegbar → entfernen).
|
||||||
async def _pruefe(ci, idxs):
|
async def _pruefe(ci, idxs):
|
||||||
per = roh_map(ci, roh_path(ci))
|
per = roh_map(ci, roh_path(ci))
|
||||||
if not per:
|
if not per:
|
||||||
return ci, set()
|
return ci, set(), set()
|
||||||
fakten_text = "\n\n".join(f"SUBBAUSTEIN: {fk['sub']}\n{_fakten_zeilen(fk)}" for fm in per.values() for fk in fm.values())
|
fakten_text = "\n\n".join(f"SUBBAUSTEIN: {fk['sub']}\n{_fakten_zeilen(fk)}" for fm in per.values() for fk in fm.values())
|
||||||
offen = [j for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if _fakten_check_schema(_json_datei(chk_path(ci, j))) is None]
|
offen = [j for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if _fakten_check_schema(_json_datei(chk_path(ci, j))) is None]
|
||||||
await asyncio.gather(*[
|
await asyncio.gather(*[
|
||||||
@@ -1021,29 +1072,45 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
|
|||||||
_timeout("inhalt_check", len(per)), provider=provider, role="judge", capabilities=caps)
|
_timeout("inhalt_check", len(per)), provider=provider, role="judge", capabilities=caps)
|
||||||
for j in offen], return_exceptions=True)
|
for j in offen], return_exceptions=True)
|
||||||
outs = [s for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if (s := _fakten_check_schema(_json_datei(chk_path(ci, j)))) is not None]
|
outs = [s for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if (s := _fakten_check_schema(_json_datei(chk_path(ci, j)))) is not None]
|
||||||
votes: dict[str, int] = {}
|
bvotes: dict[str, int] = {}
|
||||||
for s in outs:
|
vvotes: dict[str, int] = {}
|
||||||
for sn in set(s):
|
for s in outs: # s = [(sub_norm, verwerfen)] eines Judges
|
||||||
votes[sn] = votes.get(sn, 0) + 1
|
gb, gv = set(), set()
|
||||||
|
for sn, verw in s:
|
||||||
|
if sn not in gb:
|
||||||
|
gb.add(sn); bvotes[sn] = bvotes.get(sn, 0) + 1
|
||||||
|
if verw and sn not in gv:
|
||||||
|
gv.add(sn); vvotes[sn] = vvotes.get(sn, 0) + 1
|
||||||
schwelle = len(outs) / 2 if outs else 99
|
schwelle = len(outs) / 2 if outs else 99
|
||||||
return ci, {sn for sn, v in votes.items() if v > schwelle}
|
beanstandet = {sn for sn, v in bvotes.items() if v > schwelle}
|
||||||
|
# Verwerfen ist irreversibel → strenger als Beanstanden: Mehrheit UND ≥2 zustimmende Judges
|
||||||
|
# (verhindert Löschung durch eine Einzelstimme, wenn das Panel degradiert ist).
|
||||||
|
verwerfen = {sn for sn, v in vvotes.items() if v > schwelle and v >= 2}
|
||||||
|
return ci, beanstandet, verwerfen
|
||||||
|
|
||||||
pruef = await _gather_fortschritt([_pruefe(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, "Fakten prüfen"))
|
pruef = await _gather_fortschritt([_pruefe(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, "Fakten prüfen"))
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
return None
|
return None
|
||||||
beanstandet = {ci: subs for r in pruef if isinstance(r, tuple) for ci, subs in [r]}
|
beanstandet: dict[int, set] = {}
|
||||||
|
verwerfen: dict[int, set] = {}
|
||||||
|
for r in pruef:
|
||||||
|
if isinstance(r, tuple) and len(r) == 3:
|
||||||
|
ci, b, v = r
|
||||||
|
beanstandet[ci] = b
|
||||||
|
verwerfen[ci] = v
|
||||||
|
|
||||||
# Phase „Fakten fix": je Chunk mit beanstandeten Subs → diese neu extrahieren (frische Quelle).
|
# Phase „Fakten fix": nur KORRIGIERBARE (beanstandet ohne verwerfen) neu extrahieren.
|
||||||
n_problem = sum(len(s) for s in beanstandet.values())
|
korrigieren = {ci: (beanstandet.get(ci, set()) - verwerfen.get(ci, set())) for ci in beanstandet}
|
||||||
|
n_problem = sum(len(s) for s in korrigieren.values())
|
||||||
if n_problem:
|
if n_problem:
|
||||||
set_p(f"Fakten korrigieren ({n_problem})…", step=_step_idx(topic, "Fakten fix"))
|
set_p(f"Fakten korrigieren ({n_problem})…", step=_step_idx(topic, "Fakten fix"))
|
||||||
async def _fix(ci):
|
async def _fix(ci):
|
||||||
subs_norm = beanstandet.get(ci, set())
|
subs_norm = korrigieren.get(ci, set())
|
||||||
if not subs_norm or _fakten_schema(_json_datei(fix_path(ci))):
|
if not subs_norm or _fakten_schema(_json_datei(fix_path(ci))):
|
||||||
return
|
return
|
||||||
idxs = chunks[ci]
|
idxs = chunks[ci]
|
||||||
rel_by = {bausteine[i][0]: bausteine[i][1] for i in idxs}
|
rel_by = {bausteine[i][0]: bausteine[i][1] for i in idxs}
|
||||||
# nur die beanstandeten Subs als Block
|
# nur die korrigierbaren Subs als Block
|
||||||
ziel = []
|
ziel = []
|
||||||
for bt, subs in rel_by.items():
|
for bt, subs in rel_by.items():
|
||||||
betroffen = [s for s in subs if _norm_titel(s) in subs_norm]
|
betroffen = [s for s in subs if _norm_titel(s) in subs_norm]
|
||||||
@@ -1057,20 +1124,27 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
|
|||||||
role="guide", capabilities=caps,
|
role="guide", capabilities=caps,
|
||||||
payload=lambda result, p=fix_path(ci): _fakten_schema(_json_datei(p)),
|
payload=lambda result, p=fix_path(ci): _fakten_schema(_json_datei(p)),
|
||||||
timeout=_timeout("inhalt", len(subs_norm)))
|
timeout=_timeout("inhalt", len(subs_norm)))
|
||||||
await _gather_fortschritt([_fix(ci) for ci in beanstandet], len(beanstandet), _melde_p(set_p, topic, "Fakten fix"))
|
await _gather_fortschritt([_fix(ci) for ci in korrigieren], len(korrigieren), _melde_p(set_p, topic, "Fakten fix"))
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Zusammensetzen: roh + Fix-Overrides für beanstandete (Vollständigkeits-Guard: fehlt der Fix, bleibt roh).
|
# Zusammensetzen: roh + Fix-Overrides für korrigierte. Verworfene Subs raus (+ je Baustein melden).
|
||||||
ergebnis: dict[str, dict] = {}
|
ergebnis: dict[str, dict] = {}
|
||||||
|
verworfen_map: dict[str, set] = {}
|
||||||
for ci in range(len(chunks)):
|
for ci in range(len(chunks)):
|
||||||
per = roh_map(ci, roh_path(ci))
|
per = roh_map(ci, roh_path(ci))
|
||||||
fix = roh_map(ci, fix_path(ci)) if fix_path(ci).exists() else {}
|
fix = roh_map(ci, fix_path(ci)) if fix_path(ci).exists() else {}
|
||||||
|
verw = verwerfen.get(ci, set())
|
||||||
for bt, fm in per.items():
|
for bt, fm in per.items():
|
||||||
for sn, fk in fm.items():
|
for sn, fk in fm.items():
|
||||||
gewinner = fix.get(bt, {}).get(sn, fk) if sn in beanstandet.get(ci, set()) else fk
|
if sn in verw:
|
||||||
|
verworfen_map.setdefault(bt, set()).add(sn)
|
||||||
|
continue
|
||||||
|
gewinner = fix.get(bt, {}).get(sn, fk) if sn in korrigieren.get(ci, set()) else fk
|
||||||
ergebnis.setdefault(bt, {})[sn] = {k: gewinner[k] for k in _FAKTEN_FELDER}
|
ergebnis.setdefault(bt, {})[sn] = {k: gewinner[k] for k in _FAKTEN_FELDER}
|
||||||
return ergebnis
|
if verworfen_map:
|
||||||
|
_log(topic, f"Fakten-Check verwirft {sum(len(s) for s in verworfen_map.values())} unbelegbare Subbausteine")
|
||||||
|
return ergebnis, verworfen_map
|
||||||
|
|
||||||
|
|
||||||
async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
||||||
@@ -1511,7 +1585,55 @@ async def _recherche_batch(ctx: GenContext, set_p, files: dict, q: dict, ordner,
|
|||||||
await db.set_step_status(topic, "Recherche", "fertig")
|
await db.set_step_status(topic, "Recherche", "fertig")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Content-Seiten stehen schon fest (Sichtung im Schritt „Quelle aufbereiten").
|
# uni/projekt: kuratierte, oft GROSSE Dateien (Skript). Statt alle am Stück zu lesen
|
||||||
|
# (Lost-in-the-Middle), in Abschnitte chunken und JEDEN gründlich von 2 Readern lesen —
|
||||||
|
# Text direkt im Prompt (kleiner Kontext), Nennungen akkumulieren zu Konsens.
|
||||||
|
if q["type"] in ("uni", "projekt"):
|
||||||
|
eintraege: list[tuple[str, str]] = [] # (dateiname, abschnitt-text)
|
||||||
|
for fn in sorted(pages):
|
||||||
|
for absch in _text_abschnitte(_read(ordner / fn)):
|
||||||
|
eintraege.append((fn, absch))
|
||||||
|
if not eintraege:
|
||||||
|
_bausteine_errors[topic] = "Recherche: Quelle leer"
|
||||||
|
return False
|
||||||
|
set_p(f"Recherche ({len(eintraege)} Abschnitte)…", step=_step_idx(topic, "Recherche"))
|
||||||
|
|
||||||
|
async def _lese_abschnitt(ei: int, fn: str, absch: str) -> None:
|
||||||
|
block = (f"ARBEITE AUSSCHLIESSLICH MIT DIESEM TEXTABSCHNITT (Quelle: {fn}). Lies ihn "
|
||||||
|
f"VOLLSTÄNDIG, überspringe nichts. Notiere `{fn}` als Quelle jedes Bausteins. "
|
||||||
|
f"Suche NICHT im Web — nur dieser Abschnitt zählt.\n\n-----\n{absch}\n-----")
|
||||||
|
paths = [arbeit / f"recherche-a{ei}-{i}.md" for i in range(1, RECHERCHE_READERS + 1)]
|
||||||
|
for p in paths:
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
if is_cancelled():
|
||||||
|
return
|
||||||
|
slots = [{
|
||||||
|
"key": f"bausteine-{topic}-recherche-a{ei}-{i}",
|
||||||
|
"prompt": _build_recherche_prompt(topic, p, instructions, q["type"], ordner, abschnitt=block),
|
||||||
|
"role": "quick", "capabilities": "files",
|
||||||
|
"payload": (lambda result, p=p: _file_payload(p)),
|
||||||
|
} for i, p in enumerate(paths, 1)]
|
||||||
|
# Quorum 2: beide Reader pro Abschnitt sollen durch (mehr Augen = mehr Konzepte +
|
||||||
|
# echter Konsens); nach Timeout fällt _race auf das Vorhandene zurück.
|
||||||
|
texte = await _race(topic, f"Recherche Abschnitt {ei}", slots, 2, _timeout("recherche", 1),
|
||||||
|
provider, cancelled=is_cancelled, grace=RECHERCHE_GRACE)
|
||||||
|
for text in (texte or []):
|
||||||
|
await _ingest(text)
|
||||||
|
|
||||||
|
await _gather_fortschritt([_lese_abschnitt(ei, fn, a) for ei, (fn, a) in enumerate(eintraege, 1)],
|
||||||
|
len(eintraege), _melde_p(set_p, topic, "Recherche"))
|
||||||
|
if is_cancelled():
|
||||||
|
return False
|
||||||
|
await db.mark_quellen_gelesen(topic, sorted(pages))
|
||||||
|
gesamt = len(await db.list_bausteine(topic))
|
||||||
|
_log(topic, f"Recherche (uni/projekt): {gesamt} Kandidaten aus {len(eintraege)} Abschnitten ({len(pages)} Dateien)")
|
||||||
|
if not gesamt:
|
||||||
|
_bausteine_errors[topic] = "Recherche fehlgeschlagen (keine Bausteine)"
|
||||||
|
return False
|
||||||
|
await db.set_step_status(topic, "Recherche", "fertig")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Crawl/Link: viele kleine Content-Seiten (Sichtung im Schritt „Quelle aufbereiten").
|
||||||
# Feste Batches, je Batch RECHERCHE_READERS Reader, die GENAU diese Dateien lesen.
|
# Feste Batches, je Batch RECHERCHE_READERS Reader, die GENAU diese Dateien lesen.
|
||||||
batches = _chunk_nums(sorted(pages), max(1, math.ceil(len(pages) / RECHERCHE_BATCH)))
|
batches = _chunk_nums(sorted(pages), max(1, math.ceil(len(pages) / RECHERCHE_BATCH)))
|
||||||
|
|
||||||
@@ -2040,12 +2162,21 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
# Fakten je Sub (VOR der Stufe): Quell-Fakten extrahieren + verifizieren → fakten.json.
|
# Fakten je Sub (VOR der Stufe): Quell-Fakten extrahieren + verifizieren → fakten.json.
|
||||||
# Extract-once-Grounding — Stufe/Relevanz/Fragen/Guide nähren sich daraus.
|
# Extract-once-Grounding — Stufe/Relevanz/Fragen/Guide nähren sich daraus.
|
||||||
if not _fakten_komplett(files):
|
if not _fakten_komplett(files):
|
||||||
fakten_map = await _fakten_block(ctx, set_p, files, roh, q, ordner, instructions)
|
res = await _fakten_block(ctx, set_p, files, roh, q, ordner, instructions)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
abgebrochen()
|
abgebrochen()
|
||||||
return
|
return
|
||||||
if fakten_map is None:
|
if res is None:
|
||||||
return # Fehler ist gesetzt
|
return # Fehler ist gesetzt
|
||||||
|
fakten_map, verworfen = res
|
||||||
|
# Verworfene (unbelegbare) Subs aus roh streichen — ZUERST (Resume-robust), dann
|
||||||
|
# fakten.json. So sehen Stufen/Relevanz/Gliederung/Fragen/Guide sie nicht mehr.
|
||||||
|
if verworfen:
|
||||||
|
for bt, sns in verworfen.items():
|
||||||
|
if bt in roh:
|
||||||
|
roh[bt] = [s for s in roh[bt] if _norm_titel(s) not in sns]
|
||||||
|
roh = {bt: subs for bt, subs in roh.items() if subs} # leere Bausteine raus (_sub_roh_schema verlangt ≥1)
|
||||||
|
atomic_write_json(files["sub_roh"], roh, indent=1)
|
||||||
atomic_write_json(files["fakten"], fakten_map, indent=1)
|
atomic_write_json(files["fakten"], fakten_map, indent=1)
|
||||||
sidecar = await _stufen_block(ctx, set_p, files, roh, instructions)
|
sidecar = await _stufen_block(ctx, set_p, files, roh, instructions)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ Prüfe je Subbaustein:
|
|||||||
|
|
||||||
Notiere nur ECHTE Mängel (falscher Fakt, falsche Quelle, Beispiel als Fakt getarnt). Geschmacksfragen nicht.
|
Notiere nur ECHTE Mängel (falscher Fakt, falsche Quelle, Beispiel als Fakt getarnt). Geschmacksfragen nicht.
|
||||||
|
|
||||||
|
**Verwerfen vs. korrigieren** — entscheide je Mangel:
|
||||||
|
- `verwerfen: true` — der Subbaustein ist inhaltlich **nicht belegbar**: eine erfundene Aussage, eine im Material nicht auffindbare Schranke/Formel/Behauptung, oder fachlich schlicht falsch. Der Subbaustein wird dann ENTFERNT. Sei sicher — im Zweifel `false`.
|
||||||
|
- `verwerfen: false` — der Kern stimmt, nur ein Fakt/Wert/eine Quelle ist ungenau und korrigierbar.
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
Format — alles in Ordnung:
|
Format — alles in Ordnung:
|
||||||
{{"ok": true}}
|
{{"ok": true}}
|
||||||
Sonst (Subbaustein-Titel EXAKT wie oben):
|
Sonst (Subbaustein-Titel EXAKT wie oben):
|
||||||
{{"probleme": [{{"subbaustein": "<exakter Subbaustein-Titel>", "problem": "…"}}]}}
|
{{"probleme": [{{"subbaustein": "<exakter Subbaustein-Titel>", "problem": "…", "verwerfen": true}}]}}
|
||||||
{extra}
|
{extra}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
Unten je Baustein des Themas "{topic}" die gefundenen Subbausteine. Gib je Baustein die **bereinigte** finale Liste zurück.
|
Unten je Baustein des Themas "{topic}" die gefundenen Subbausteine. Gib je Baustein die **bereinigte** finale Liste zurück.
|
||||||
|
|
||||||
|
{source}
|
||||||
|
|
||||||
|
GEFUNDENE SUBBAUSTEINE:
|
||||||
{bausteine}
|
{bausteine}
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
|
- **Beleg-Prüfung (wichtig):** Prüfe jeden Subbaustein gegen die Quelle. **Verwirf, was im Material NICHT belegbar oder klar erfunden ist** — erfundene Schranken, Formeln, Werte oder Behauptungen, die so nicht in der Quelle stehen. Mit Quelldatei: gegen die Datei prüfen. Ohne Quelle (reines Thema): nur gesichertes Standardwissen behalten, Zweifelhaftes/Falsches streichen.
|
||||||
- **Dubletten zusammenführen:** Subbausteine, die denselben Punkt mit anderen Worten sagen, sind EINER. Behalte den klarsten, streiche die Umformulierungen. (z.B. „Broadcast Mode verfügbar" und „Broadcast-Modus aktivieren" → einer.)
|
- **Dubletten zusammenführen:** Subbausteine, die denselben Punkt mit anderen Worten sagen, sind EINER. Behalte den klarsten, streiche die Umformulierungen. (z.B. „Broadcast Mode verfügbar" und „Broadcast-Modus aktivieren" → einer.)
|
||||||
- Behalte alle fachlich **DISTINKTEN** Teilpunkte vollständig — nichts Wesentliches weglassen.
|
- Behalte alle fachlich **DISTINKTEN** und belegbaren Teilpunkte vollständig — nichts Wesentliches weglassen.
|
||||||
- Verwirf, was zu fein granular, am Rand des Themas oder fachlich zweifelhaft ist.
|
- Verwirf, was zu fein granular, am Rand des Themas oder fachlich zweifelhaft ist.
|
||||||
- Jeder Punkt atomar (eine Aussage). Behaltene Punkte WÖRTLICH übernehmen, nicht umformulieren, nichts erfinden.
|
- Jeder Punkt atomar (eine Aussage). Behaltene Punkte WÖRTLICH übernehmen, nicht umformulieren, nichts erfinden.
|
||||||
- Die Anzahl folgt der Schwierigkeit: lieber wenige distinkte als viele redundante Punkte.
|
- Die Anzahl folgt der Schwierigkeit: lieber wenige distinkte als viele redundante Punkte.
|
||||||
|
|||||||
Reference in New Issue
Block a user