update
This commit is contained in:
2
Makefile
2
Makefile
@@ -12,7 +12,7 @@ auth:
|
|||||||
@echo "Verzeichnisse angelegt und auf uid 1000 chowned."
|
@echo "Verzeichnisse angelegt und auf uid 1000 chowned."
|
||||||
|
|
||||||
install:
|
install:
|
||||||
pip install --break-system-packages fastapi uvicorn[standard] aiosqlite uv playwright transformers
|
pip install --break-system-packages fastapi uvicorn[standard] aiosqlite uv playwright transformers trafilatura
|
||||||
pip install --break-system-packages torch --index-url https://download.pytorch.org/whl/cpu
|
pip install --break-system-packages torch --index-url https://download.pytorch.org/whl/cpu
|
||||||
python3 -m playwright install chromium
|
python3 -m playwright install chromium
|
||||||
@echo "Falls Chromium OS-Libs fehlen: 'sudo python3 -m playwright install-deps chromium' einmalig ausführen."
|
@echo "Falls Chromium OS-Libs fehlen: 'sudo python3 -m playwright install-deps chromium' einmalig ausführen."
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ from pathlib import Path
|
|||||||
|
|
||||||
import database as db
|
import database as db
|
||||||
from agents import kill_process, cancel_scope, clear_scope, run_agent
|
from agents import kill_process, cancel_scope, clear_scope, run_agent
|
||||||
from config import KONSENS_GRACE, RECHERCHE_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER, CRAWL_KEEP_PATTERNS, CRAWL_NOISE_PATTERNS, CRAWL_MIN_CHARS
|
from config import KONSENS_GRACE, RECHERCHE_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER, CRAWL_KEEP_PATTERNS, CRAWL_NOISE_PATTERNS, CRAWL_MIN_CHARS, QUELLE_RELEVANZ_CHUNK, QUELLE_RELEVANZ_SNIPPET
|
||||||
from fsutil import atomic_write_text, atomic_write_json
|
from fsutil import atomic_write_text, atomic_write_json
|
||||||
from jsonio import read_json_file as _json_datei
|
from jsonio import read_json_file as _json_datei
|
||||||
from paths import arbeit_dir, bausteine_path, frage_muster_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
from paths import arbeit_dir, bausteine_path, frage_muster_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
||||||
from crawl import crawl
|
from crawl import crawl
|
||||||
from pipeline import (
|
from pipeline import (
|
||||||
CANCELLED, FAILED, GenContext, _extra, _gather_fortschritt, _log, _prompt, _race, _relevanz_schema,
|
CANCELLED, FAILED, GenContext, _extra, _gather_fortschritt, _janein_schema, _log, _prompt, _race,
|
||||||
_runde_schema, _semaphore, _str_liste, _stufen_schema, _timeout, run_single_slot,
|
_relevanz_schema, _runde_schema, _semaphore, _str_liste, _stufen_schema, _timeout, run_single_slot,
|
||||||
)
|
)
|
||||||
from textkit import (
|
from textkit import (
|
||||||
_eindeutige_titel, _lade_bausteine, _norm_titel, _parse_auswahl, _parse_subbausteine, _titel,
|
_eindeutige_titel, _lade_bausteine, _norm_titel, _parse_auswahl, _parse_subbausteine, _titel,
|
||||||
@@ -57,6 +57,8 @@ KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); dar
|
|||||||
FRAGE_CHUNK_SUBS = 50 # Ziel-Summe relevanter Subs je Chunk
|
FRAGE_CHUNK_SUBS = 50 # Ziel-Summe relevanter Subs je Chunk
|
||||||
FAKTEN_CHUNK_SUBS = 25 # Fakten-Extraktion: kleinere Chunks (Fakten sind umfangreicher als Muster)
|
FAKTEN_CHUNK_SUBS = 25 # Fakten-Extraktion: kleinere Chunks (Fakten sind umfangreicher als Muster)
|
||||||
FAKTEN_CHECK_PANEL = 3 # Judges je Chunk im Fakten-Check (Mehrheit beanstandet)
|
FAKTEN_CHECK_PANEL = 3 # Judges je Chunk im Fakten-Check (Mehrheit beanstandet)
|
||||||
|
KONSOLIDIERUNG_PANEL = 3 # Mapping-Judges je Chunk (Panel → Reconcile statt Einzel-Judge)
|
||||||
|
SUBBAUSTEIN_PANEL = 3 # Source-Judges in der Subbaustein-Klärung (Mehrheit statt Einzel-Judge)
|
||||||
|
|
||||||
log = logging.getLogger("creator.bausteine")
|
log = logging.getLogger("creator.bausteine")
|
||||||
|
|
||||||
@@ -678,7 +680,9 @@ async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict
|
|||||||
bl.append(f"<!-- baustein: {titel_by_num[num]} -->\n" + "\n".join(f"- {s}" for s in subs))
|
bl.append(f"<!-- baustein: {titel_by_num[num]} -->\n" + "\n".join(f"- {s}" for s in subs))
|
||||||
if not bl:
|
if not bl:
|
||||||
return ""
|
return ""
|
||||||
return "\n\nBEREITS GEFUNDEN — bestätige diese Subbausteine erneut UND ergänze fehlende:\n" + "\n".join(bl)
|
# Bekanntes NICHT erneut auflisten lassen (sonst bläht Re-Bestätigung den Mention-Count auf,
|
||||||
|
# Self-Bias/Echo) — nur Fehlendes ergänzen. Der Zähler bleibt so ein ehrliches Konsens-Signal.
|
||||||
|
return ("\n\nBEREITS ERFASST — liste diese NICHT erneut. Finde nur, was FEHLT:\n" + "\n".join(bl))
|
||||||
|
|
||||||
# Phase „Subbausteine finden": je Paket Loop bis 0 neue Subs / Zeit-Kappe.
|
# Phase „Subbausteine finden": je Paket Loop bis 0 neue Subs / Zeit-Kappe.
|
||||||
async def _finde(c, chunk):
|
async def _finde(c, chunk):
|
||||||
@@ -741,30 +745,74 @@ async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict
|
|||||||
await db.set_subbaustein_felder(topic, norm_by_num[num], s["sub_norm"],
|
await db.set_subbaustein_felder(topic, norm_by_num[num], s["sub_norm"],
|
||||||
status=("konsens" if s["nennungen"] >= 2 else "verworfen"))
|
status=("konsens" if s["nennungen"] >= 2 else "verworfen"))
|
||||||
|
|
||||||
# Phase „Subbausteine klären": Judge je Paket bereinigt die Konsens-Liste.
|
# Phase „Subbausteine klären": Source-Panel (SUBBAUSTEIN_PANEL Judges) prüft Konsens + Unsicher (1×)
|
||||||
|
# gegen die Quelle; Code-Mehrheit je Sub. Externes, mehrstimmiges Gate gegen Einzel-Judge-Bias + Echo.
|
||||||
async def _klaere(c, chunk):
|
async def _klaere(c, chunk):
|
||||||
fp = arbeit / f"subbaustein-final-c{c}.md"
|
fp = arbeit / f"subbaustein-final-c{c}.md"
|
||||||
if _parse_subbausteine(_read(fp)):
|
if _parse_subbausteine(_read(fp)):
|
||||||
return
|
return
|
||||||
bloecke, hat = [], False
|
bloecke, hat = [], False
|
||||||
|
konsens_by_num: dict[int, list[str]] = {}
|
||||||
for num in chunk:
|
for num in chunk:
|
||||||
subs = [s["sub_titel"] for s in await db.list_subbausteine(topic, norm_by_num[num]) if s["status"] == "konsens"]
|
rows = await db.list_subbausteine(topic, norm_by_num[num])
|
||||||
zeilen = "\n".join(f"- {s}" for s in subs) if subs else "- (keiner)"
|
kon = [s["sub_titel"] for s in rows if s["status"] == "konsens"]
|
||||||
bloecke.append(f"BAUSTEIN: {titel_by_num[num]}\nKonsens:\n{zeilen}")
|
uns = [s["sub_titel"] for s in rows if s["status"] != "konsens" and s["nennungen"] == 1]
|
||||||
if subs:
|
konsens_by_num[num] = kon
|
||||||
hat = True
|
if not kon and not uns:
|
||||||
|
continue
|
||||||
|
hat = True
|
||||||
|
k_zeilen = "\n".join(f"- {s}" for s in kon) if kon else "- (keiner)"
|
||||||
|
u_zeilen = "\n".join(f"- {s}" for s in uns) if uns else "- (keiner)"
|
||||||
|
bloecke.append(f"BAUSTEIN: {titel_by_num[num]}\nKonsens (≥2 Finder):\n{k_zeilen}\nUnsicher (1× — streng gegen Quelle prüfen):\n{u_zeilen}")
|
||||||
if not hat:
|
if not hat:
|
||||||
return
|
return
|
||||||
status, _ = await run_single_slot(
|
|
||||||
ctx, f"Subbaustein-Klärung {c}",
|
chunk_idx = _titel_index({num: titel_by_num[num] for num in chunk})
|
||||||
key=f"bausteine-{topic}-subbaustein-final-c{c}",
|
paths = [arbeit / f"subbaustein-final-c{c}-j{j}.md" for j in range(1, SUBBAUSTEIN_PANEL + 1)]
|
||||||
prompt=_prompt("Subbaustein-Mapping", topic=topic, source=source, bausteine="\n\n".join(bloecke), out_path=fp, extra=_extra(instructions)),
|
offen = [(j, p) for j, p in enumerate(paths, 1) if _parse_subbausteine(_read(p)) is None]
|
||||||
role="judge", capabilities=caps,
|
for _, p in offen:
|
||||||
payload=lambda result, p=fp: _parse_subbausteine(_read(p)) or None,
|
p.unlink(missing_ok=True)
|
||||||
timeout=_timeout("subbaustein_check", len(chunk)),
|
if offen:
|
||||||
)
|
slots = [{
|
||||||
if status == FAILED:
|
"key": f"bausteine-{topic}-subbaustein-final-c{c}-j{j}",
|
||||||
|
"prompt": _prompt("Subbaustein-Mapping", topic=topic, source=source, bausteine="\n\n".join(bloecke), out_path=p, extra=_extra(instructions)),
|
||||||
|
"role": "judge", "capabilities": caps,
|
||||||
|
"payload": (lambda result, p=p: _parse_subbausteine(_read(p)) or None),
|
||||||
|
} for j, p in offen]
|
||||||
|
vorhanden = SUBBAUSTEIN_PANEL - len(offen)
|
||||||
|
await _race(topic, f"Subbaustein-Klärung {c}", slots, max(1, 2 - vorhanden),
|
||||||
|
_timeout("subbaustein_check", len(chunk)), provider, cancelled=is_cancelled, grace=KONSENS_GRACE)
|
||||||
|
if is_cancelled():
|
||||||
|
return
|
||||||
|
outs = [d for p in paths if (d := _parse_subbausteine(_read(p)))]
|
||||||
|
if not outs: # Panel komplett gescheitert → Konsens übernehmen (wie bisher der Fallback)
|
||||||
_log(topic, f"Subbaustein-Klärung Paket {c} fehlgeschlagen — Konsens übernommen")
|
_log(topic, f"Subbaustein-Klärung Paket {c} fehlgeschlagen — Konsens übernommen")
|
||||||
|
text = "\n\n".join(f"<!-- baustein: {titel_by_num[num]} -->\n" + "\n".join(f"- {s}" for s in konsens_by_num[num])
|
||||||
|
for num in chunk if konsens_by_num[num])
|
||||||
|
atomic_write_text(fp, text)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Code-Mehrheit je Baustein/Sub-Norm: behalten wenn Mehrheit der Judges ihn führt (Tie → behalten).
|
||||||
|
bloecke_out = []
|
||||||
|
for num in chunk:
|
||||||
|
votes: dict[str, int] = {}
|
||||||
|
form: dict[str, str] = {}
|
||||||
|
for d in outs:
|
||||||
|
seen = set()
|
||||||
|
for marker, subs in d.items():
|
||||||
|
if _titel_aufloesen(chunk_idx, marker) != num:
|
||||||
|
continue
|
||||||
|
for sub in subs:
|
||||||
|
sn = _norm_titel(sub)
|
||||||
|
if not sn or sn in seen:
|
||||||
|
continue
|
||||||
|
seen.add(sn)
|
||||||
|
form.setdefault(sn, sub)
|
||||||
|
votes[sn] = votes.get(sn, 0) + 1
|
||||||
|
kept = [form[sn] for sn in form if votes[sn] * 2 >= len(outs)]
|
||||||
|
if kept:
|
||||||
|
bloecke_out.append(f"<!-- baustein: {titel_by_num[num]} -->\n" + "\n".join(f"- {s}" for s in kept))
|
||||||
|
atomic_write_text(fp, "\n\n".join(bloecke_out))
|
||||||
|
|
||||||
await _gather_fortschritt([_klaere(c, chunk) for c, chunk in enumerate(chunks, 1)], n, _melde_p(set_p, topic, "Subbausteine klären"))
|
await _gather_fortschritt([_klaere(c, chunk) for c, chunk in enumerate(chunks, 1)], n, _melde_p(set_p, topic, "Subbausteine klären"))
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
@@ -806,6 +854,9 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
|
|||||||
→ {Baustein-Titel: [{titel, stufe}, …]} oder None."""
|
→ {Baustein-Titel: [{titel, stufe}, …]} oder None."""
|
||||||
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"]
|
||||||
|
# Kernpunkte je Sub als knapper Kontext (fundiertere Einstufung; Klassifikation braucht wenig).
|
||||||
|
fakten_map = _json_datei(files["fakten"])
|
||||||
|
fakten_map = fakten_map if isinstance(fakten_map, dict) else {}
|
||||||
items = [(titel, sub) for titel, subs in roh.items() for sub in subs] # globale id = index+1
|
items = [(titel, sub) for titel, subs in roh.items() for sub in subs] # globale id = index+1
|
||||||
if not items:
|
if not items:
|
||||||
return {titel: [] for titel in roh}
|
return {titel: [] for titel in roh}
|
||||||
@@ -843,6 +894,8 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
|
|||||||
enum_zeilen.append(f"\nBAUSTEIN: {b}")
|
enum_zeilen.append(f"\nBAUSTEIN: {b}")
|
||||||
cur_b = b
|
cur_b = b
|
||||||
enum_zeilen.append(f"{k}. {sub}")
|
enum_zeilen.append(f"{k}. {sub}")
|
||||||
|
if (kz := _kern_zeile(fakten_map.get(b, {}).get(_norm_titel(sub)))):
|
||||||
|
enum_zeilen.append(f" {kz}")
|
||||||
enum = "\n".join(enum_zeilen).strip()
|
enum = "\n".join(enum_zeilen).strip()
|
||||||
offen = [(i, p) for i, p in enumerate(paths, 1) if not _stufen_schema(_json_datei(p), local_set)]
|
offen = [(i, p) for i, p in enumerate(paths, 1) if not _stufen_schema(_json_datei(p), local_set)]
|
||||||
slots = [{
|
slots = [{
|
||||||
@@ -978,10 +1031,18 @@ def _fakten_check_schema(data) -> list[tuple[str, bool]] | None:
|
|||||||
for p in pr if isinstance(p, dict) and (sn := _norm_titel(str(p.get("subbaustein", ""))))]
|
for p in pr if isinstance(p, dict) and (sn := _norm_titel(str(p.get("subbaustein", ""))))]
|
||||||
|
|
||||||
|
|
||||||
|
def _kern_zeile(fk) -> str:
|
||||||
|
"""Knappe Kernpunkt-Zeile für Klassifikation (Stufe/Relevanz) — weniger Kontext genügt dort.
|
||||||
|
Leer, wenn keine Fakten/Kernpunkte (Altbestand)."""
|
||||||
|
if not isinstance(fk, dict) or not fk.get("kernpunkte"):
|
||||||
|
return ""
|
||||||
|
return "Kern: " + " · ".join(str(k) for k in fk["kernpunkte"])
|
||||||
|
|
||||||
|
|
||||||
def _fakten_zeilen(fk: dict) -> str:
|
def _fakten_zeilen(fk: dict) -> str:
|
||||||
z = []
|
z = []
|
||||||
if fk.get("kernpunkte"):
|
if fk.get("kernpunkte"):
|
||||||
z.append("Kernpunkte: " + " · ".join(fk["kernpunkte"]))
|
z.append("Kernpunkte: " + " · ".join(str(k) for k in fk["kernpunkte"]))
|
||||||
if fk.get("voraussetzungen"):
|
if fk.get("voraussetzungen"):
|
||||||
z.append("Voraussetzung: " + fk["voraussetzungen"])
|
z.append("Voraussetzung: " + fk["voraussetzungen"])
|
||||||
if fk.get("huerden"):
|
if fk.get("huerden"):
|
||||||
@@ -1015,6 +1076,7 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
|
|||||||
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"
|
||||||
|
def erg_path(ci): return arbeit / f"fakten-erg-c{ci}.json"
|
||||||
def chk_path(ci, j): return arbeit / f"fakten-check-c{ci}-j{j}.json"
|
def chk_path(ci, j): return arbeit / f"fakten-check-c{ci}-j{j}.json"
|
||||||
def fix_path(ci): return arbeit / f"fakten-fix-c{ci}.json"
|
def fix_path(ci): return arbeit / f"fakten-fix-c{ci}.json"
|
||||||
def ctitel(idxs): return [bausteine[i][0] for i in idxs]
|
def ctitel(idxs): return [bausteine[i][0] for i in idxs]
|
||||||
@@ -1037,6 +1099,33 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
|
|||||||
out.setdefault(bt, {})[_norm_titel(sub)] = {"sub": sub, **{k: e[k] for k in _FAKTEN_FELDER}}
|
out.setdefault(bt, {})[_norm_titel(sub)] = {"sub": sub, **{k: e[k] for k in _FAKTEN_FELDER}}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
# Roh-Fakten + Completeness-Ergänzungen vereinigen (Recall): nur Subs, die in roh existieren.
|
||||||
|
def _chunk_fakten(ci):
|
||||||
|
roh = roh_map(ci, roh_path(ci))
|
||||||
|
erg = roh_map(ci, erg_path(ci)) if erg_path(ci).exists() else {}
|
||||||
|
if not erg:
|
||||||
|
return roh
|
||||||
|
for bt, fm in roh.items():
|
||||||
|
ebt = erg.get(bt, {})
|
||||||
|
for sn, fk in fm.items():
|
||||||
|
ek = ebt.get(sn)
|
||||||
|
if not ek:
|
||||||
|
continue
|
||||||
|
seen = {str(k).strip().casefold() for k in fk.get("kernpunkte", [])}
|
||||||
|
for k in ek.get("kernpunkte", []):
|
||||||
|
if str(k).strip().casefold() not in seen:
|
||||||
|
seen.add(str(k).strip().casefold())
|
||||||
|
fk["kernpunkte"].append(k)
|
||||||
|
seent = {bf["text"].strip().casefold() for bf in fk.get("belegte_fakten", [])}
|
||||||
|
for bf in ek.get("belegte_fakten", []):
|
||||||
|
if bf["text"].strip().casefold() not in seent:
|
||||||
|
seent.add(bf["text"].strip().casefold())
|
||||||
|
fk["belegte_fakten"].append(bf)
|
||||||
|
for f in ("voraussetzungen", "huerden", "beispiel_idee"):
|
||||||
|
if not fk.get(f) and ek.get(f):
|
||||||
|
fk[f] = ek[f]
|
||||||
|
return roh
|
||||||
|
|
||||||
# Phase „Fakten finden": 1 Generator je Chunk.
|
# Phase „Fakten finden": 1 Generator je Chunk.
|
||||||
async def _finde(ci, idxs):
|
async def _finde(ci, idxs):
|
||||||
fp = roh_path(ci)
|
fp = roh_path(ci)
|
||||||
@@ -1058,10 +1147,38 @@ 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 ergänzen" (Recall): ein gezielter Gap-Hunt je Chunk sucht source-belegte Fakten, die
|
||||||
|
# der Single-Pass übersah. Best-effort — schlägt nie fehl (keine erg-Datei → Merge nutzt nur roh).
|
||||||
|
async def _ergaenze(ci, idxs):
|
||||||
|
ep = erg_path(ci)
|
||||||
|
if _fakten_schema(_json_datei(ep)):
|
||||||
|
return
|
||||||
|
per = roh_map(ci, roh_path(ci))
|
||||||
|
if not per:
|
||||||
|
return
|
||||||
|
block = "\n\n".join(
|
||||||
|
f"BAUSTEIN: {bt}\nSUBBAUSTEINE (mit bereits erfassten Fakten):\n" + "\n".join(
|
||||||
|
f"- {fk['sub']}\n Erfasst: " + ("; ".join(
|
||||||
|
list(fk.get("kernpunkte", [])) + [bf["text"] for bf in fk.get("belegte_fakten", [])]) or "(nichts)")
|
||||||
|
for fk in fm.values())
|
||||||
|
for bt, fm in per.items())
|
||||||
|
subs_total = sum(len(bausteine[i][1]) for i in idxs)
|
||||||
|
await run_single_slot(
|
||||||
|
ctx, f"Fakten ergänzen {ci}", key=f"bausteine-{topic}-fakten-erg-c{ci}",
|
||||||
|
prompt=_prompt("Fakten-Ergaenzung", topic=topic, source=source, bausteine=block, out_path=ep, extra=_extra(instructions)),
|
||||||
|
role="guide", capabilities=caps,
|
||||||
|
payload=lambda result, p=ep: _fakten_schema(_json_datei(p)),
|
||||||
|
timeout=_timeout("inhalt", subs_total))
|
||||||
|
|
||||||
|
set_p("Fakten ergänzen…", step=_step_idx(topic, "Fakten finden"))
|
||||||
|
await _gather_fortschritt([_ergaenze(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, "Fakten finden"))
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
|
||||||
# Phase „Fakten prüfen": FAKTEN_CHECK_PANEL Judges je Chunk. Zwei Mehrheits-Mengen:
|
# Phase „Fakten prüfen": FAKTEN_CHECK_PANEL Judges je Chunk. Zwei Mehrheits-Mengen:
|
||||||
# beanstandet (Fakt ungenau → korrigieren) und verwerfen (Sub nicht belegbar → entfernen).
|
# 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 = _chunk_fakten(ci) # roh + Ergänzungen → Panel verifiziert die Vereinigung
|
||||||
if not per:
|
if not per:
|
||||||
return ci, set(), 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())
|
||||||
@@ -1132,7 +1249,7 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
|
|||||||
ergebnis: dict[str, dict] = {}
|
ergebnis: dict[str, dict] = {}
|
||||||
verworfen_map: dict[str, set] = {}
|
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 = _chunk_fakten(ci) # roh + Ergänzungen (Recall); Fix überschreibt nur Korrigierte
|
||||||
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())
|
verw = verwerfen.get(ci, set())
|
||||||
for bt, fm in per.items():
|
for bt, fm in per.items():
|
||||||
@@ -1153,7 +1270,7 @@ async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, in
|
|||||||
→ {gid: relevanz} oder None bei Abbruch/Recherche-Fehler. Default bei Lücke/Streit: 'relevant'."""
|
→ {gid: relevanz} oder None bei Abbruch/Recherche-Fehler. Default bei Lücke/Streit: 'relevant'."""
|
||||||
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"]
|
||||||
items = [(titel, sub["titel"]) for titel, subs in sidecar.items() for sub in subs] # globale id = index+1
|
items = [(titel, sub["titel"], sub.get("fakten")) for titel, subs in sidecar.items() for sub in subs] # globale id = index+1
|
||||||
if not items:
|
if not items:
|
||||||
return {}
|
return {}
|
||||||
chunks = _chunk_nums(list(range(len(items))), _n_chunks(len(items), STUFE_CHUNK))
|
chunks = _chunk_nums(list(range(len(items))), _n_chunks(len(items), STUFE_CHUNK))
|
||||||
@@ -1172,7 +1289,12 @@ async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, in
|
|||||||
vorhanden = sum(1 for p in paths if _relevanz_schema(_json_datei(p), local_set))
|
vorhanden = sum(1 for p in paths if _relevanz_schema(_json_datei(p), local_set))
|
||||||
if vorhanden >= 2:
|
if vorhanden >= 2:
|
||||||
return True
|
return True
|
||||||
enum = "\n".join(f"{k}. [{items[j][0]}] {items[j][1]}" for k, j in enumerate(item_idxs, 1))
|
enum_zeilen = []
|
||||||
|
for k, j in enumerate(item_idxs, 1):
|
||||||
|
enum_zeilen.append(f"{k}. [{items[j][0]}] {items[j][1]}")
|
||||||
|
if (kz := _kern_zeile(items[j][2])):
|
||||||
|
enum_zeilen.append(f" {kz}")
|
||||||
|
enum = "\n".join(enum_zeilen)
|
||||||
offen = [(i, p) for i, p in enumerate(paths, 1) if not _relevanz_schema(_json_datei(p), local_set)]
|
offen = [(i, p) for i, p in enumerate(paths, 1) if not _relevanz_schema(_json_datei(p), local_set)]
|
||||||
slots = [{
|
slots = [{
|
||||||
"key": f"bausteine-{topic}-relevanz-c{c}-{i}",
|
"key": f"bausteine-{topic}-relevanz-c{c}-{i}",
|
||||||
@@ -1280,9 +1402,16 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
|||||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||||
arbeit = files["arbeit"]
|
arbeit = files["arbeit"]
|
||||||
# ALLE Subbausteine (auch rand) bekommen ein Muster — Rand ist in der FGuide-Ebene prüfbar.
|
# ALLE Subbausteine (auch rand) bekommen ein Muster — Rand ist in der FGuide-Ebene prüfbar.
|
||||||
|
# fakten_by: voller Fakten-Kontext je Sub (Generierung profitiert davon — bessere Fragen).
|
||||||
bausteine = []
|
bausteine = []
|
||||||
|
fakten_by: dict[tuple, dict] = {}
|
||||||
for titel, subs in sidecar.items():
|
for titel, subs in sidecar.items():
|
||||||
alle = [s["titel"] for s in subs if isinstance(s, dict) and str(s.get("titel", "")).strip()]
|
alle = []
|
||||||
|
for s in subs:
|
||||||
|
if isinstance(s, dict) and (st := str(s.get("titel", "")).strip()):
|
||||||
|
alle.append(st)
|
||||||
|
if isinstance(s.get("fakten"), dict):
|
||||||
|
fakten_by[(titel, _norm_titel(st))] = s["fakten"]
|
||||||
if alle:
|
if alle:
|
||||||
bausteine.append((titel, alle))
|
bausteine.append((titel, alle))
|
||||||
if not bausteine:
|
if not bausteine:
|
||||||
@@ -1303,8 +1432,14 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
|||||||
fp = roh_path(ci)
|
fp = roh_path(ci)
|
||||||
if _frage_muster_chunk_schema(_json_datei(fp)):
|
if _frage_muster_chunk_schema(_json_datei(fp)):
|
||||||
return # Resume
|
return # Resume
|
||||||
|
def _sub_zeile(bi, s):
|
||||||
|
zeile = f"- {s}"
|
||||||
|
fk = fakten_by.get((bausteine[bi][0], _norm_titel(s)))
|
||||||
|
if fk and (ft := _fakten_zeilen(fk)):
|
||||||
|
zeile += "\n" + "\n".join(" " + l for l in ft.split("\n"))
|
||||||
|
return zeile
|
||||||
block = "\n\n".join(
|
block = "\n\n".join(
|
||||||
f"BAUSTEIN: {bausteine[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(f"- {s}" for s in bausteine[i][1])
|
f"BAUSTEIN: {bausteine[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(_sub_zeile(i, s) for s in bausteine[i][1])
|
||||||
for i in idxs
|
for i in idxs
|
||||||
)
|
)
|
||||||
subs_total = sum(len(bausteine[i][1]) for i in idxs)
|
subs_total = sum(len(bausteine[i][1]) for i in idxs)
|
||||||
@@ -1499,7 +1634,89 @@ def _sichte_regeln(ordner, pages: list[str]) -> tuple[list[str], list[str]]:
|
|||||||
return content, noise
|
return content, noise
|
||||||
|
|
||||||
|
|
||||||
async def _quelle_aufbereiten(ctx: GenContext, set_p, files: dict, q: dict, ordner) -> bool:
|
def _seite_snippet(ordner, fn: str) -> tuple[str, str]:
|
||||||
|
"""(url, snippet) einer Crawl-Seite für das Relevanz-Gate. url aus der QUELLE:-Zeile;
|
||||||
|
snippet = Body-Auszug (Navigations-Boilerplate steht vorn — der Prompt ignoriert es).
|
||||||
|
URL ist das Primärsignal (sprechender Slug), der Snippet stützt nur."""
|
||||||
|
zeilen = _read(Path(ordner) / fn).splitlines()
|
||||||
|
url = zeilen[0][len("QUELLE:"):].strip() if zeilen and zeilen[0].startswith("QUELLE:") else ""
|
||||||
|
body = "\n".join(zeilen[1:]).strip()
|
||||||
|
snippet = " ".join(body.split())[:QUELLE_RELEVANZ_SNIPPET]
|
||||||
|
return (url or fn), snippet
|
||||||
|
|
||||||
|
|
||||||
|
async def _relevanz_sichtung(ctx: GenContext, set_p, files: dict, ordner, content: list[str], spec: str, instructions: str) -> tuple[list[str], list[str]]:
|
||||||
|
"""LLM-Themen-Gate nach dem Regel-Filter: jede Content-Seite ja/nein gegen die Spec.
|
||||||
|
Off-topic (anderes Fachgebiet) → raus. Muster wie `_relevanz_block`: kleine Pakete, 3 Rater
|
||||||
|
(`fast`), 2-von-3-Konsens. KONSERVATIV: nur bei klarer „nein"-Mehrheit droppen; Streit/Lücke/
|
||||||
|
Race-Fehler → behalten. SAFETY: würde das Gate ≥80 % (oder alles) droppen, bleibt alles
|
||||||
|
(Spec-Mismatch/Bug soll die Quelle nicht leeren). → (behalten, raus) als Dateinamen."""
|
||||||
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
pages = sorted(content)
|
||||||
|
if not pages:
|
||||||
|
return content, []
|
||||||
|
items = [_seite_snippet(ordner, fn) for fn in pages] # Index deckt sich mit `pages`
|
||||||
|
chunks = _chunk_nums(list(range(len(pages))), _n_chunks(len(pages), QUELLE_RELEVANZ_CHUNK))
|
||||||
|
n = len(chunks)
|
||||||
|
|
||||||
|
def rater_paths(c):
|
||||||
|
return [arbeit / f"quelle-relevanz-c{c}-{i}.json" for i in (1, 2, 3)]
|
||||||
|
|
||||||
|
def lset(idxs):
|
||||||
|
return set(range(1, len(idxs) + 1))
|
||||||
|
|
||||||
|
async def _rate(c, idxs):
|
||||||
|
local_set = lset(idxs)
|
||||||
|
paths = rater_paths(c)
|
||||||
|
vorhanden = sum(1 for p in paths if _janein_schema(_json_datei(p), local_set))
|
||||||
|
if vorhanden >= 2:
|
||||||
|
return True
|
||||||
|
enum_zeilen = []
|
||||||
|
for k, j in enumerate(idxs, 1):
|
||||||
|
url, snip = items[j]
|
||||||
|
enum_zeilen.append(f"{k}. {url}")
|
||||||
|
if snip:
|
||||||
|
enum_zeilen.append(f" {snip}")
|
||||||
|
enum = "\n".join(enum_zeilen)
|
||||||
|
offen = [(i, p) for i, p in enumerate(paths, 1) if not _janein_schema(_json_datei(p), local_set)]
|
||||||
|
slots = [{
|
||||||
|
"key": f"bausteine-{topic}-quelle-relevanz-c{c}-{i}",
|
||||||
|
"prompt": _prompt("Quelle-Relevanz", topic=topic, spec=spec, seiten=enum, out_path=p, extra=_extra(instructions)),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result, p=p, ids=local_set: _janein_schema(_json_datei(p), ids)),
|
||||||
|
} for i, p in offen]
|
||||||
|
neu = await _race(topic, f"Relevanz-Sichtung Paket {c}", slots, 2 - vorhanden, _timeout("relevanz", len(idxs)), provider, cancelled=is_cancelled, grace=KONSENS_GRACE)
|
||||||
|
return not is_cancelled() and neu is not None
|
||||||
|
|
||||||
|
_qidx = _step_idx(topic, "Quelle aufbereiten") # Gate läuft im Quelle-Schritt (kein eigener Step)
|
||||||
|
set_p(f"Prüfe Relevanz zur Spec ({n} Pakete)…", step=_qidx)
|
||||||
|
async def _melde_sichtung(d, t):
|
||||||
|
set_p(f"Prüfe Relevanz zur Spec {d}/{t}…", step=_qidx)
|
||||||
|
await _gather_fortschritt([_rate(c, idxs) for c, idxs in enumerate(chunks, 1)], n, _melde_sichtung)
|
||||||
|
if is_cancelled():
|
||||||
|
return content, [] # Abbruch → nichts droppen (Caller bricht ab)
|
||||||
|
|
||||||
|
# Vote je Seite: nur eine klare „nein"-Mehrheit (≥2 und mehr als „ja") wirft raus.
|
||||||
|
raus: list[str] = []
|
||||||
|
for c, idxs in enumerate(chunks, 1):
|
||||||
|
local_set = lset(idxs)
|
||||||
|
rater = [d for p in rater_paths(c) if (d := _janein_schema(_json_datei(p), local_set))]
|
||||||
|
for k in range(1, len(idxs) + 1):
|
||||||
|
stimmen = [d[k] for d in rater if k in d]
|
||||||
|
nein, ja = stimmen.count("nein"), stimmen.count("ja")
|
||||||
|
if nein >= 2 and nein > ja:
|
||||||
|
raus.append(pages[idxs[k - 1]])
|
||||||
|
|
||||||
|
if raus and len(raus) >= max(1, int(len(pages) * 0.8)):
|
||||||
|
_log(topic, f"Relevanz-Sichtung: würde {len(raus)}/{len(pages)} droppen — verworfen (Spec-Mismatch?), alles behalten")
|
||||||
|
return content, []
|
||||||
|
raus_set = set(raus)
|
||||||
|
behalten = [fn for fn in pages if fn not in raus_set]
|
||||||
|
return behalten, raus
|
||||||
|
|
||||||
|
|
||||||
|
async def _quelle_aufbereiten(ctx: GenContext, set_p, files: dict, q: dict, ordner, instructions: str) -> bool:
|
||||||
"""Schritt „Quelle aufbereiten": Crawl (link) + PDF-Konvert + Content/Noise-Sichtung.
|
"""Schritt „Quelle aufbereiten": Crawl (link) + PDF-Konvert + Content/Noise-Sichtung.
|
||||||
Persistiert die Sichtung in der Coverage-Tabelle (inhalt). → True (ok) / False (Abbruch/Fehler).
|
Persistiert die Sichtung in der Coverage-Tabelle (inhalt). → True (ok) / False (Abbruch/Fehler).
|
||||||
thema: nichts. projekt/uni: nur PDFs (kuratierter Ordner, keine Sichtung)."""
|
thema: nichts. projekt/uni: nur PDFs (kuratierter Ordner, keine Sichtung)."""
|
||||||
@@ -1525,8 +1742,15 @@ async def _quelle_aufbereiten(ctx: GenContext, set_p, files: dict, q: dict, ordn
|
|||||||
set_p("Sichte Seiten…", step=_step_idx(topic, "Quelle aufbereiten"))
|
set_p("Sichte Seiten…", step=_step_idx(topic, "Quelle aufbereiten"))
|
||||||
await db.delete_coverage(topic)
|
await db.delete_coverage(topic)
|
||||||
content, noise = _sichte_regeln(ordner, pages) # deterministischer Regel-Filter
|
content, noise = _sichte_regeln(ordner, pages) # deterministischer Regel-Filter
|
||||||
|
if q.get("spec") and content: # Themen-Gate: trennt das Fachgebiet (Regeln können das nicht)
|
||||||
|
content, raus = await _relevanz_sichtung(ctx, set_p, files, ordner, content, q["spec"], instructions)
|
||||||
|
if is_cancelled():
|
||||||
|
return False
|
||||||
|
if raus:
|
||||||
|
noise = sorted(set(noise) | set(raus))
|
||||||
|
_log(topic, f"LLM-Relevanz: {len(raus)} Seiten off-topic → Noise")
|
||||||
await db.mark_inhalt(topic, sorted(content), sorted(noise))
|
await db.mark_inhalt(topic, sorted(content), sorted(noise))
|
||||||
_log(topic, f"Sichtung: {len(content)} Content / {len(noise)} Noise von {len(pages)} (Regeln)")
|
_log(topic, f"Sichtung: {len(content)} Content / {len(noise)} Noise von {len(pages)} (Regeln + LLM-Gate)")
|
||||||
await db.set_step_status(topic, "Quelle aufbereiten", "fertig")
|
await db.set_step_status(topic, "Quelle aufbereiten", "fertig")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1673,8 +1897,10 @@ async def _recherche_batch(ctx: GenContext, set_p, files: dict, q: dict, ordner,
|
|||||||
|
|
||||||
|
|
||||||
async def _konsolidiere(ctx: GenContext, set_p, files: dict) -> bool:
|
async def _konsolidiere(ctx: GenContext, set_p, files: dict) -> bool:
|
||||||
"""Judge mergt Kandidaten semantisch + teilt in Konsens (≥2)/Rest (1×); Status in DB."""
|
"""Panel (KONSOLIDIERUNG_PANEL Judges) mergt Kandidaten semantisch; ein Reconcile-Judge führt die
|
||||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
Panel-Ausgaben zur finalen Konsens (≥2)/Rest (1×)-Liste zusammen. Status in DB.
|
||||||
|
Panel statt Einzel-Judge: ein einzelner Judge ist bias-anfällig (Position/Verbosity) und instabil."""
|
||||||
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
if await db.get_step_status(topic, "Konsolidierung") == "fertig":
|
if await db.get_step_status(topic, "Konsolidierung") == "fertig":
|
||||||
return True
|
return True
|
||||||
set_p("Konsolidiere Recherche…", step=_step_idx(topic, "Konsolidierung"))
|
set_p("Konsolidiere Recherche…", step=_step_idx(topic, "Konsolidierung"))
|
||||||
@@ -1684,27 +1910,81 @@ async def _konsolidiere(ctx: GenContext, set_p, files: dict) -> bool:
|
|||||||
return False
|
return False
|
||||||
arbeit = files["arbeit"]
|
arbeit = files["arbeit"]
|
||||||
chunks = _chunk_nums(kandidaten, max(1, math.ceil(len(kandidaten) / KONSOLIDIERUNG_CHUNK)))
|
chunks = _chunk_nums(kandidaten, max(1, math.ceil(len(kandidaten) / KONSOLIDIERUNG_CHUNK)))
|
||||||
|
|
||||||
|
async def _map_panel(c: int, eintraege: str, anzahl: int):
|
||||||
|
"""3 Mapping-Judges über `eintraege` → Reconcile-Judge → (konsens, rest). None bei Abbruch/Fehler."""
|
||||||
|
paths = [arbeit / f"konsolidierung-c{c}-j{j}.json" for j in range(1, KONSOLIDIERUNG_PANEL + 1)]
|
||||||
|
offen = [(j, p) for j, p in enumerate(paths, 1) if _mapping_schema(_json_datei(p)) is None]
|
||||||
|
for _, p in offen:
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
if offen:
|
||||||
|
slots = [{
|
||||||
|
"key": f"bausteine-{topic}-konsolidierung-c{c}-j{j}",
|
||||||
|
"prompt": _prompt("Bausteine-Recherche-Mapping", topic=topic, n=RECHERCHE_READERS, eintraege=eintraege, out_path=p),
|
||||||
|
"role": "judge", "capabilities": "files",
|
||||||
|
"payload": (lambda result, p=p: _mapping_schema(_json_datei(p))),
|
||||||
|
} for j, p in offen]
|
||||||
|
vorhanden = KONSOLIDIERUNG_PANEL - len(offen)
|
||||||
|
await _race(topic, f"Konsolidierung {c}", slots, max(1, 2 - vorhanden),
|
||||||
|
_timeout("recherche_mapping", anzahl), provider, cancelled=is_cancelled, grace=KONSENS_GRACE)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
outs = [m for p in paths if (m := _mapping_schema(_json_datei(p)))]
|
||||||
|
if not outs:
|
||||||
|
return None
|
||||||
|
# Union der Panel-Titel; je Titel zählen, wie viele Judges ihn als Konsens führen.
|
||||||
|
kvotes: dict[str, int] = {}
|
||||||
|
form: dict[str, str] = {} # norm → Anzeigetitel (erstes Vorkommen)
|
||||||
|
order: list[str] = []
|
||||||
|
for kk, rr in outs:
|
||||||
|
for t in kk + rr:
|
||||||
|
nt = _norm_titel(_titel(t))
|
||||||
|
if not nt:
|
||||||
|
continue
|
||||||
|
if nt not in form:
|
||||||
|
form[nt] = t
|
||||||
|
order.append(nt)
|
||||||
|
kvotes.setdefault(nt, 0)
|
||||||
|
for t in kk:
|
||||||
|
nt = _norm_titel(_titel(t))
|
||||||
|
if nt:
|
||||||
|
kvotes[nt] = kvotes.get(nt, 0) + 1
|
||||||
|
# Reconcile: ein Merge-Judge über die Union, annotiert mit Judge-Stimmen ("k× genannt").
|
||||||
|
rp = arbeit / f"konsolidierung-c{c}-reconcile.json"
|
||||||
|
recon = _mapping_schema(_json_datei(rp))
|
||||||
|
if recon is None:
|
||||||
|
rp.unlink(missing_ok=True)
|
||||||
|
eintraege_r = "\n".join(f"{i}. {form[nt]} ({max(1, kvotes[nt])}× genannt)" for i, nt in enumerate(order, 1))
|
||||||
|
status, recon = await run_single_slot(
|
||||||
|
ctx, f"Konsolidierung Reconcile {c}",
|
||||||
|
key=f"bausteine-{topic}-konsolidierung-c{c}-reconcile",
|
||||||
|
prompt=_prompt("Bausteine-Recherche-Mapping", topic=topic, n=KONSOLIDIERUNG_PANEL, eintraege=eintraege_r, out_path=rp),
|
||||||
|
role="judge", capabilities="files",
|
||||||
|
payload=lambda result, p=rp: _mapping_schema(_json_datei(p)),
|
||||||
|
timeout=_timeout("recherche_mapping", len(order)),
|
||||||
|
)
|
||||||
|
if status == CANCELLED:
|
||||||
|
return None
|
||||||
|
recon = recon if status != FAILED else None
|
||||||
|
if recon:
|
||||||
|
return recon
|
||||||
|
# Fallback (Reconcile gescheitert): Code-Mehrheit — Konsens, wenn Mehrheit der Judges Konsens sagt.
|
||||||
|
konsens = [form[nt] for nt in order if kvotes[nt] * 2 >= len(outs) and kvotes[nt] > 0]
|
||||||
|
kset = {_norm_titel(_titel(t)) for t in konsens}
|
||||||
|
return konsens, [form[nt] for nt in order if nt not in kset]
|
||||||
|
|
||||||
konsens, rest = [], []
|
konsens, rest = [], []
|
||||||
for c, chunk in enumerate(chunks, 1):
|
for c, chunk in enumerate(chunks, 1):
|
||||||
fp = arbeit / f"konsolidierung-c{c}.json"
|
|
||||||
fp.unlink(missing_ok=True)
|
|
||||||
eintraege = "\n".join(
|
eintraege = "\n".join(
|
||||||
f"{i}. {b['titel']} — {b['beschreibung']} ({b['nennungen']}× genannt)" for i, b in enumerate(chunk, 1)
|
f"{i}. {b['titel']} — {b['beschreibung']} ({b['nennungen']}× genannt)" for i, b in enumerate(chunk, 1)
|
||||||
)
|
)
|
||||||
status, mapping = await run_single_slot(
|
res = await _map_panel(c, eintraege, len(chunk))
|
||||||
ctx, f"Konsolidierung {c}",
|
if res is None:
|
||||||
key=f"bausteine-{topic}-konsolidierung-c{c}",
|
if is_cancelled():
|
||||||
prompt=_prompt("Bausteine-Recherche-Mapping", topic=topic, n=RECHERCHE_READERS, eintraege=eintraege, out_path=fp),
|
return False
|
||||||
role="judge", capabilities="files",
|
|
||||||
payload=lambda result, p=fp: _mapping_schema(_json_datei(p)),
|
|
||||||
timeout=_timeout("recherche_mapping", len(chunk)),
|
|
||||||
)
|
|
||||||
if status == CANCELLED:
|
|
||||||
return False
|
|
||||||
if status == FAILED:
|
|
||||||
_bausteine_errors[topic] = "Recherche-Mapping fehlgeschlagen"
|
_bausteine_errors[topic] = "Recherche-Mapping fehlgeschlagen"
|
||||||
return False
|
return False
|
||||||
k, r = mapping
|
k, r = res
|
||||||
konsens += k
|
konsens += k
|
||||||
rest += r
|
rest += r
|
||||||
# Bei mehreren Chunks: ein globaler Merge-Pass über die vereinten Konsens-Einträge,
|
# Bei mehreren Chunks: ein globaler Merge-Pass über die vereinten Konsens-Einträge,
|
||||||
@@ -1737,39 +2017,51 @@ async def _konsolidiere(ctx: GenContext, set_p, files: dict) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def _klaere_inventar(ctx: GenContext, set_p, files: dict) -> bool:
|
async def _klaere_inventar(ctx: GenContext, set_p, files: dict) -> bool:
|
||||||
"""1 Judge entscheidet über den Rest (1×-Genannte): aufnehmen → Konsens, sonst verworfen."""
|
"""Panel (KONSOLIDIERUNG_PANEL Judges) entscheidet über den Rest (1×-Genannte): Mehrheit `aufnehmen`
|
||||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
→ Konsens, sonst verworfen. Panel statt Einzel-Judge — der Rest-Schnitt ist der schärfste Eingriff;
|
||||||
|
ein einzelner Judge ist hier zu instabil. Konservativer Tie → behalten (nie ein Konzept verlieren)."""
|
||||||
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
if await db.get_step_status(topic, "Klärung") == "fertig":
|
if await db.get_step_status(topic, "Klärung") == "fertig":
|
||||||
return True
|
return True
|
||||||
set_p("Klärung läuft…", step=_step_idx(topic, "Klärung"))
|
set_p("Klärung läuft…", step=_step_idx(topic, "Klärung"))
|
||||||
rest_rows = await db.list_bausteine(topic, status="rest")
|
rest_rows = await db.list_bausteine(topic, status="rest")
|
||||||
if rest_rows:
|
if rest_rows:
|
||||||
konsens = [b["titel"] for b in await db.list_bausteine(topic, status="konsens")]
|
konsens = [b["titel"] for b in await db.list_bausteine(topic, status="konsens")]
|
||||||
fp = files["arbeit"] / "klaerung.json"
|
arbeit = files["arbeit"]
|
||||||
fp.unlink(missing_ok=True)
|
paths = [arbeit / f"klaerung-j{j}.json" for j in range(1, KONSOLIDIERUNG_PANEL + 1)]
|
||||||
status, ergebnis = await run_single_slot(
|
offen = [(j, p) for j, p in enumerate(paths, 1) if _runde_schema(_json_datei(p), final=True) is None]
|
||||||
ctx, "Klärung",
|
for _, p in offen:
|
||||||
key=f"bausteine-{topic}-klaerung",
|
p.unlink(missing_ok=True)
|
||||||
prompt=_prompt(
|
if offen:
|
||||||
"Bausteine-Klaerung", topic=topic,
|
slots = [{
|
||||||
konsens="\n".join(f"- {t}" for t in konsens) or "(noch leer)",
|
"key": f"bausteine-{topic}-klaerung-j{j}",
|
||||||
rest="\n".join(f"- {b['titel']}" for b in rest_rows),
|
"prompt": _prompt(
|
||||||
final="\n- Entscheide JEDEN Eintrag. `rest` MUSS leer sein.",
|
"Bausteine-Klaerung", topic=topic,
|
||||||
out_path=fp,
|
konsens="\n".join(f"- {t}" for t in konsens) or "(noch leer)",
|
||||||
),
|
rest="\n".join(f"- {b['titel']}" for b in rest_rows),
|
||||||
role="judge", capabilities="files",
|
final="\n- Entscheide JEDEN Eintrag. `rest` MUSS leer sein.",
|
||||||
payload=lambda result, p=fp: _runde_schema(_json_datei(p), final=True),
|
out_path=p,
|
||||||
timeout=_timeout("auswahl_mapping", len(rest_rows)),
|
),
|
||||||
)
|
"role": "judge", "capabilities": "files",
|
||||||
if status == CANCELLED:
|
"payload": (lambda result, p=p: _runde_schema(_json_datei(p), final=True)),
|
||||||
|
} for j, p in offen]
|
||||||
|
vorhanden = KONSOLIDIERUNG_PANEL - len(offen)
|
||||||
|
await _race(topic, "Klärung", slots, max(1, 2 - vorhanden),
|
||||||
|
_timeout("auswahl_mapping", len(rest_rows)), provider, cancelled=is_cancelled, grace=KONSENS_GRACE)
|
||||||
|
if is_cancelled():
|
||||||
return False
|
return False
|
||||||
if status == FAILED:
|
outs = [r for p in paths if (r := _runde_schema(_json_datei(p), final=True))]
|
||||||
|
if not outs:
|
||||||
_bausteine_errors[topic] = "Klärung fehlgeschlagen"
|
_bausteine_errors[topic] = "Klärung fehlgeschlagen"
|
||||||
return False
|
return False
|
||||||
aufnehmen, _ = ergebnis
|
# Mehrheit je Rest-Eintrag (per Norm-Titel). Tie → behalten (votes*2 >= n).
|
||||||
auf_norm = {_norm_titel(_titel(t)) for t in aufnehmen}
|
votes: dict[str, int] = {}
|
||||||
|
for aufnehmen, _ in outs:
|
||||||
|
for nt in {_norm_titel(_titel(t)) for t in aufnehmen}:
|
||||||
|
votes[nt] = votes.get(nt, 0) + 1
|
||||||
for b in rest_rows:
|
for b in rest_rows:
|
||||||
await db.set_baustein_status(topic, b["titel_norm"], "konsens" if b["titel_norm"] in auf_norm else "verworfen")
|
auf = votes.get(b["titel_norm"], 0) * 2 >= len(outs)
|
||||||
|
await db.set_baustein_status(topic, b["titel_norm"], "konsens" if auf else "verworfen")
|
||||||
await db.set_step_status(topic, "Klärung", "fertig")
|
await db.set_step_status(topic, "Klärung", "fertig")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1808,14 +2100,99 @@ def _gliederung_schema(data, valid: set[int]):
|
|||||||
return {"kapitel": out}
|
return {"kapitel": out}
|
||||||
|
|
||||||
|
|
||||||
|
def _prereq_schema(data, valid: set[int]) -> dict[int, list[int]]:
|
||||||
|
"""{"prereqs": {"3": [1, 7]}} → {num: [prereq-nums]} · nur Nummern aus `valid`, ohne Selbstkante.
|
||||||
|
Ungültig/leer → {} (Best-effort: dann Original-Reihenfolge)."""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("prereqs"), dict):
|
||||||
|
return {}
|
||||||
|
out: dict[int, list[int]] = {}
|
||||||
|
for k, v in data["prereqs"].items():
|
||||||
|
try:
|
||||||
|
num = int(k)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if num not in valid or not isinstance(v, list):
|
||||||
|
continue
|
||||||
|
pres = []
|
||||||
|
for p in v:
|
||||||
|
try:
|
||||||
|
p = int(p)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if p in valid and p != num and p not in pres:
|
||||||
|
pres.append(p)
|
||||||
|
if pres:
|
||||||
|
out[num] = pres
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _topo_order(nums: list[int], edges: dict[int, list[int]]) -> list[int]:
|
||||||
|
"""Kahn-Topo-Sort: Voraussetzungen zuerst. `edges[num]` = Nummern, die VOR num kommen müssen.
|
||||||
|
Stabiler Tie-Break (Original-Reihenfolge von `nums`); Zyklen werden gebrochen (nie Deadlock)."""
|
||||||
|
pos = {n: i for i, n in enumerate(nums)}
|
||||||
|
# Resteingangsgrad nur über gültige Knoten; Selbst-/Fremdkanten ignoriert.
|
||||||
|
pre = {n: [p for p in edges.get(n, []) if p in pos and p != n] for n in nums}
|
||||||
|
fertig: list[int] = []
|
||||||
|
erledigt: set[int] = set()
|
||||||
|
rest = list(nums)
|
||||||
|
while rest:
|
||||||
|
bereit = [n for n in rest if all(p in erledigt for p in pre[n])]
|
||||||
|
if not bereit: # Zyklus → den in Original-Reihenfolge frühesten Rest-Knoten erzwingen
|
||||||
|
bereit = [min(rest, key=lambda n: pos[n])]
|
||||||
|
nxt = min(bereit, key=lambda n: pos[n]) # stabil: kleinste Original-Position zuerst
|
||||||
|
fertig.append(nxt)
|
||||||
|
erledigt.add(nxt)
|
||||||
|
rest.remove(nxt)
|
||||||
|
return fertig
|
||||||
|
|
||||||
|
|
||||||
|
async def _lernreihenfolge(ctx: GenContext, set_p, files: dict, entries: dict, valid: set[int], instructions: str) -> dict:
|
||||||
|
"""entries (num→titel) in Lernreihenfolge bringen: LLM extrahiert Prereq-Kanten aus den
|
||||||
|
extrahierten `voraussetzungen`, Code löst per Topo-Sort. Best-effort → sonst entries unverändert."""
|
||||||
|
if len(entries) < 3:
|
||||||
|
return entries
|
||||||
|
topic = ctx.topic
|
||||||
|
fakten_map = _json_datei(files["fakten"])
|
||||||
|
fakten_map = fakten_map if isinstance(fakten_map, dict) else {}
|
||||||
|
|
||||||
|
def _hint(titel):
|
||||||
|
fm = fakten_map.get(titel) or {}
|
||||||
|
vs = [v for fk in fm.values() if isinstance(fk, dict) and (v := str(fk.get("voraussetzungen", "")).strip())]
|
||||||
|
return " · ".join(dict.fromkeys(vs))
|
||||||
|
|
||||||
|
pp = files["arbeit"] / "gliederung-prereqs.json"
|
||||||
|
|
||||||
|
def _payload(result, p=pp):
|
||||||
|
d = _json_datei(p)
|
||||||
|
return d if isinstance(d, dict) and "prereqs" in d else None
|
||||||
|
|
||||||
|
vorhanden = _json_datei(pp)
|
||||||
|
if not (isinstance(vorhanden, dict) and "prereqs" in vorhanden):
|
||||||
|
zeilen = [f"{n}. {t}" + (f"\n braucht vorher: {h}" if (h := _hint(t)) else "") for n, t in entries.items()]
|
||||||
|
set_p("Gliederung — Lernreihenfolge…", step=_step_idx(topic, "Gliederung"))
|
||||||
|
await run_single_slot(
|
||||||
|
ctx, "Gliederung-Voraussetzungen", key=f"bausteine-{topic}-gliederung-prereqs",
|
||||||
|
prompt=_prompt("Gliederung-Voraussetzungen", topic=topic, bausteine="\n".join(zeilen), out_path=pp, extra=_extra(instructions)),
|
||||||
|
role="guide", capabilities="files", payload=_payload, timeout=_timeout("plan", len(entries)))
|
||||||
|
edges = _prereq_schema(_json_datei(pp), valid)
|
||||||
|
if not edges:
|
||||||
|
return entries # keine/ungültige Kanten → Original-Reihenfolge (kein Regress)
|
||||||
|
ordered = _topo_order(list(entries), edges)
|
||||||
|
return {n: entries[n] for n in ordered}
|
||||||
|
|
||||||
|
|
||||||
async def _gliederung_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str) -> dict:
|
async def _gliederung_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str) -> dict:
|
||||||
"""Format-agnostische Gliederung über ALLE Bausteine — 3 Vorschläge → Judge merged.
|
"""Format-agnostische Gliederung über ALLE Bausteine — 3 Vorschläge → Judge merged.
|
||||||
Bricht nie ab: 0 gültige → ein Kapitel mit allem; fehlende Bausteine landen in „Weitere".
|
Bricht nie ab: 0 gültige → ein Kapitel mit allem; fehlende Bausteine landen in „Weitere".
|
||||||
→ {"kapitel":[{titel,nummern}]} (auch in files["gliederung"])."""
|
→ {"kapitel":[{titel,nummern}]} (auch in files["gliederung"])."""
|
||||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||||
valid = set(entries)
|
valid = set(entries)
|
||||||
liste = "\n".join(f"{n}. {t}" for n, t in entries.items())
|
|
||||||
step = _step_idx(topic, "Gliederung")
|
step = _step_idx(topic, "Gliederung")
|
||||||
|
|
||||||
|
# Lernreihenfolge gründen (LLM-Modulo): LLM extrahiert Prereq-Kanten aus den extrahierten
|
||||||
|
# `voraussetzungen`, Code löst per Topo-Sort. Best-effort → sonst Original-Reihenfolge.
|
||||||
|
entries = await _lernreihenfolge(ctx, set_p, files, entries, valid, instructions)
|
||||||
|
liste = "\n".join(f"{n}. {t}" for n, t in entries.items())
|
||||||
set_p("Gliederung — Vorschläge…", step=step)
|
set_p("Gliederung — Vorschläge…", step=step)
|
||||||
|
|
||||||
async def _vorschlag(i, path):
|
async def _vorschlag(i, path):
|
||||||
@@ -1894,6 +2271,26 @@ def _beispiel_schema(data):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _beispiel_check_schema(data):
|
||||||
|
"""Worked-Example-Check → {"ok": true} → set() (alles korrekt); {"probleme":[{"index":N}]} →
|
||||||
|
{N, …} (1-basierte beanstandete Indizes); None bei kaputt."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
if data.get("ok") is True:
|
||||||
|
return set()
|
||||||
|
pr = data.get("probleme")
|
||||||
|
if not isinstance(pr, list):
|
||||||
|
return None
|
||||||
|
out: set[int] = set()
|
||||||
|
for p in pr:
|
||||||
|
if isinstance(p, dict):
|
||||||
|
try:
|
||||||
|
out.add(int(p.get("index")))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
_ARTEFAKT_SCHEMA = {"karteikarte": _karten_schema, "beispiel": _beispiel_schema}
|
_ARTEFAKT_SCHEMA = {"karteikarte": _karten_schema, "beispiel": _beispiel_schema}
|
||||||
_ARTEFAKT_PROMPT = {"karteikarte": "Artefakt-Karteikarte", "beispiel": "Artefakt-Beispiel"}
|
_ARTEFAKT_PROMPT = {"karteikarte": "Artefakt-Karteikarte", "beispiel": "Artefakt-Beispiel"}
|
||||||
_ARTEFAKT_SCHRITT = {"karteikarte": "Karteikarten", "beispiel": "Beispiele"}
|
_ARTEFAKT_SCHRITT = {"karteikarte": "Karteikarten", "beispiel": "Beispiele"}
|
||||||
@@ -1907,9 +2304,11 @@ def _artefakte_komplett(files: dict) -> bool:
|
|||||||
|
|
||||||
async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
||||||
"""Lern-Artefakte je Typ aus den gespeicherten Fakten erzeugen — ein Generierungs-Durchlauf
|
"""Lern-Artefakte je Typ aus den gespeicherten Fakten erzeugen — ein Generierungs-Durchlauf
|
||||||
je Typ über Chunks, keine Verifikation (Bonus-Material). → {typ: [eintraege]} (auch in files)."""
|
je Typ über Chunks. Worked Examples werden gegen die Fakten verifiziert (falsche verworfen);
|
||||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
Karteikarten sind risikoarm und bleiben ungeprüft. → {typ: [eintraege]} (auch in files)."""
|
||||||
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
arbeit = files["arbeit"]
|
arbeit = files["arbeit"]
|
||||||
|
caps = "files"
|
||||||
# Bausteine mit Subs + Fakten-Zeilen als Input-Block (extract-once aus den Fakten).
|
# Bausteine mit Subs + Fakten-Zeilen als Input-Block (extract-once aus den Fakten).
|
||||||
bausteine = []
|
bausteine = []
|
||||||
for btitel, subs in sidecar.items():
|
for btitel, subs in sidecar.items():
|
||||||
@@ -1935,6 +2334,36 @@ async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
|
|||||||
def block_text(idxs):
|
def block_text(idxs):
|
||||||
return "\n\n".join(f"BAUSTEIN: {bausteine[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(bausteine[i][1]) for i in idxs)
|
return "\n\n".join(f"BAUSTEIN: {bausteine[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(bausteine[i][1]) for i in idxs)
|
||||||
|
|
||||||
|
# Worked Examples gegen die Fakten prüfen (Panel-Mehrheit) — falsche verwerfen. CoT-Schritte sind
|
||||||
|
# fehleranfällig; ein falsches Beispiel prägt ein fehlerhaftes Schema ein → kein Beispiel > falsches.
|
||||||
|
async def _pruefe_beispiele(ci, idxs, items):
|
||||||
|
if is_cancelled() or not items:
|
||||||
|
return items
|
||||||
|
def cpath(j): return arbeit / f"artefakt-beispiel-check-c{ci}-j{j}.json"
|
||||||
|
beispiele_txt = "\n\n".join(
|
||||||
|
f"{k}. PROBLEM: {e['problem']}\n SCHRITTE: " + " | ".join(e.get("schritte", []))
|
||||||
|
+ (f"\n ERGEBNIS: {e['ergebnis']}" if e.get("ergebnis") else "")
|
||||||
|
for k, e in enumerate(items, 1))
|
||||||
|
offen = [j for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if _beispiel_check_schema(_json_datei(cpath(j))) is None]
|
||||||
|
if offen:
|
||||||
|
await asyncio.gather(*[
|
||||||
|
run_agent(f"bausteine-{topic}-artefakt-beispiel-check-c{ci}-j{j}",
|
||||||
|
_prompt("Artefakt-Beispiel-Check", topic=topic, fakten=block_text(idxs), beispiele=beispiele_txt, out_path=cpath(j), extra=_extra(instructions)),
|
||||||
|
_timeout("inhalt_check", len(items)), provider=provider, role="judge", capabilities=caps)
|
||||||
|
for j in offen], return_exceptions=True)
|
||||||
|
outs = [s for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if (s := _beispiel_check_schema(_json_datei(cpath(j)))) is not None]
|
||||||
|
if not outs:
|
||||||
|
return items # keine Prüfung möglich → behalten (Best-effort)
|
||||||
|
votes: dict[int, int] = {}
|
||||||
|
for s in outs:
|
||||||
|
for idx in s:
|
||||||
|
votes[idx] = votes.get(idx, 0) + 1
|
||||||
|
schwelle = len(outs) / 2
|
||||||
|
raus = {idx for idx, v in votes.items() if v > schwelle} # Mehrheit (≥2 von 3) beanstandet → raus
|
||||||
|
if raus:
|
||||||
|
_log(topic, f"Worked-Example-Check Chunk {ci}: {len(raus)}/{len(items)} verworfen")
|
||||||
|
return [e for k, e in enumerate(items, 1) if k not in raus]
|
||||||
|
|
||||||
ergebnis: dict[str, list] = {}
|
ergebnis: dict[str, list] = {}
|
||||||
for typ in ARTEFAKT_TYPEN:
|
for typ in ARTEFAKT_TYPEN:
|
||||||
schema = _ARTEFAKT_SCHEMA[typ]
|
schema = _ARTEFAKT_SCHEMA[typ]
|
||||||
@@ -1957,7 +2386,10 @@ async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
|
|||||||
return None
|
return None
|
||||||
eintraege: list = []
|
eintraege: list = []
|
||||||
for ci in range(len(chunks)):
|
for ci in range(len(chunks)):
|
||||||
eintraege += schema(_json_datei(apath(ci))) or []
|
chunk_items = schema(_json_datei(apath(ci))) or []
|
||||||
|
if typ == "beispiel" and chunk_items:
|
||||||
|
chunk_items = await _pruefe_beispiele(ci, chunks[ci], chunk_items)
|
||||||
|
eintraege += chunk_items
|
||||||
ergebnis[typ] = eintraege
|
ergebnis[typ] = eintraege
|
||||||
atomic_write_json(files["artefakte"], ergebnis, indent=1)
|
atomic_write_json(files["artefakte"], ergebnis, indent=1)
|
||||||
return ergebnis
|
return ergebnis
|
||||||
@@ -2069,7 +2501,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
_reset_ab_phase(topic, label)
|
_reset_ab_phase(topic, label)
|
||||||
await _reset_db_ab_phase(topic, label)
|
await _reset_db_ab_phase(topic, label)
|
||||||
# Schritt „Quelle aufbereiten": Crawl (link) + PDFs + Content/Noise-Sichtung.
|
# Schritt „Quelle aufbereiten": Crawl (link) + PDFs + Content/Noise-Sichtung.
|
||||||
if not await _quelle_aufbereiten(ctx, set_p, files, q, ordner):
|
if not await _quelle_aufbereiten(ctx, set_p, files, q, ordner, instructions):
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
abgebrochen()
|
abgebrochen()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ CRAWL_NOISE_PATTERNS = [ # eindeutig the
|
|||||||
]
|
]
|
||||||
CRAWL_MIN_CHARS = 400 # zu wenig Text → raus
|
CRAWL_MIN_CHARS = 400 # zu wenig Text → raus
|
||||||
|
|
||||||
|
# LLM-Themen-Relevanz-Gate (nach dem Regel-Filter): je Content-Seite ja/nein gegen die Spec.
|
||||||
|
# Trennt das Fachgebiet (z.B. Backend vs Frontend), was die globalen CRAWL_*-Regeln nicht können.
|
||||||
|
QUELLE_RELEVANZ_CHUNK = 12 # Seiten je Rater-Paket (klein, da je Seite ein Snippet mitgeht)
|
||||||
|
QUELLE_RELEVANZ_SNIPPET = 800 # Body-Zeichen je Seite im Prompt (URL ist Primärsignal)
|
||||||
|
|
||||||
# Timeouts pro Agenten-Schritt: (Basis-Sekunden, Sekunden pro Baustein/Section).
|
# Timeouts pro Agenten-Schritt: (Basis-Sekunden, Sekunden pro Baustein/Section).
|
||||||
# Gilt für alle Provider gleich — wer zu langsam ist, wird neu gestartet bzw. überholt.
|
# Gilt für alle Provider gleich — wer zu langsam ist, wird neu gestartet bzw. überholt.
|
||||||
TIMEOUTS = {
|
TIMEOUTS = {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ log = logging.getLogger("creator.crawl")
|
|||||||
MAX_TIEFE = 3
|
MAX_TIEFE = 3
|
||||||
MAX_SEITEN = 500
|
MAX_SEITEN = 500
|
||||||
SEITE_TIMEOUT = 30 # Sekunden pro Seite (Render bzw. PDF-Download)
|
SEITE_TIMEOUT = 30 # Sekunden pro Seite (Render bzw. PDF-Download)
|
||||||
|
CRAWL_SETTLE_MS = 3000 # gedeckelter Settle nach domcontentloaded (SPA-Render); kein 30s-networkidle-Hang
|
||||||
MAX_BYTES = 10_000_000 # 10 MB Deckel pro PDF
|
MAX_BYTES = 10_000_000 # 10 MB Deckel pro PDF
|
||||||
_UA = "Mozilla/5.0 (creator-lernbot)"
|
_UA = "Mozilla/5.0 (creator-lernbot)"
|
||||||
|
|
||||||
@@ -46,6 +47,37 @@ def _is_pdf(url: str) -> bool:
|
|||||||
return url.lower().split("?")[0].rstrip("/").endswith(".pdf")
|
return url.lower().split("?")[0].rstrip("/").endswith(".pdf")
|
||||||
|
|
||||||
|
|
||||||
|
def _scope_prefix(start_url: str) -> str:
|
||||||
|
"""Erstes nicht-leeres Pfad-Segment der Start-URL als Crawl-Scope, z.B.
|
||||||
|
`/learn/path/x` → `/learn`. Ohne Pfad-Segment → `""` (ganze Domain, kein Regress)."""
|
||||||
|
seg = [s for s in urlparse(start_url).path.split("/") if s]
|
||||||
|
return f"/{seg[0]}" if seg else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _in_scope(url: str, prefix: str) -> bool:
|
||||||
|
"""Segment-genauer Prefix-Match (kein `/learn` ⊃ `/learning-x`). Leerer Prefix → alles erlaubt."""
|
||||||
|
if not prefix:
|
||||||
|
return True
|
||||||
|
p = urlparse(url).path
|
||||||
|
return p == prefix or p.startswith(prefix + "/")
|
||||||
|
|
||||||
|
|
||||||
|
def _seiten_text(page) -> str:
|
||||||
|
"""Haupttext der gerenderten Seite — Nav/Footer/Boilerplate per trafilatura entfernt.
|
||||||
|
Fallback auf den rohen Body-Text, wenn die Extraktion leer/zu kurz ausfällt (Nicht-Artikel-Seiten)."""
|
||||||
|
try:
|
||||||
|
from trafilatura import extract # lazy: Backend startet auch ohne das Paket
|
||||||
|
text = extract(page.content(), include_comments=False, include_tables=True) or ""
|
||||||
|
except Exception:
|
||||||
|
text = ""
|
||||||
|
if len(text.strip()) >= 200:
|
||||||
|
return text.strip()
|
||||||
|
try:
|
||||||
|
return page.inner_text("body").strip()
|
||||||
|
except Exception:
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten: int = MAX_SEITEN, cancelled=None) -> int:
|
def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten: int = MAX_SEITEN, cancelled=None) -> int:
|
||||||
"""Crawlt ab start_url (nur gleiche Domain), rendert JS und legt Seiten/PDFs in `ziel` ab.
|
"""Crawlt ab start_url (nur gleiche Domain), rendert JS und legt Seiten/PDFs in `ziel` ab.
|
||||||
|
|
||||||
@@ -58,6 +90,7 @@ def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten:
|
|||||||
|
|
||||||
ziel.mkdir(parents=True, exist_ok=True)
|
ziel.mkdir(parents=True, exist_ok=True)
|
||||||
domain = urlparse(start_url).netloc
|
domain = urlparse(start_url).netloc
|
||||||
|
prefix = _scope_prefix(start_url) # nur Links unter diesem Pfad-Segment folgen
|
||||||
gesehen: set[str] = set()
|
gesehen: set[str] = set()
|
||||||
queue: list[tuple[str, int]] = [(urldefrag(start_url)[0], 0)]
|
queue: list[tuple[str, int]] = [(urldefrag(start_url)[0], 0)]
|
||||||
gespeichert = 0
|
gespeichert = 0
|
||||||
@@ -85,13 +118,14 @@ def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
page.goto(url, wait_until="networkidle", timeout=SEITE_TIMEOUT * 1000)
|
page.goto(url, wait_until="domcontentloaded", timeout=SEITE_TIMEOUT * 1000)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug("crawl: goto unvollständig %s: %s", url, e) # trotzdem versuchen, Inhalt zu lesen
|
log.debug("crawl: goto unvollständig %s: %s", url, e) # trotzdem versuchen, Inhalt zu lesen
|
||||||
try:
|
try:
|
||||||
text = page.inner_text("body").strip()
|
page.wait_for_load_state("networkidle", timeout=CRAWL_SETTLE_MS)
|
||||||
except Exception:
|
except Exception:
|
||||||
text = ""
|
pass # SPA mit Dauer-Traffic erreicht nie idle → nach Settle weiter, kein 30s-Hang
|
||||||
|
text = _seiten_text(page) # Haupttext, Nav/Footer entfernt (Fallback: roher Body)
|
||||||
if text:
|
if text:
|
||||||
atomic_write_text(ziel / _name(url, ".txt"), f"QUELLE: {url}\n\n{text}")
|
atomic_write_text(ziel / _name(url, ".txt"), f"QUELLE: {url}\n\n{text}")
|
||||||
gespeichert += 1
|
gespeichert += 1
|
||||||
@@ -103,7 +137,8 @@ def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten:
|
|||||||
for href in hrefs:
|
for href in hrefs:
|
||||||
nxt = urldefrag(href)[0]
|
nxt = urldefrag(href)[0]
|
||||||
if (nxt.startswith(("http://", "https://"))
|
if (nxt.startswith(("http://", "https://"))
|
||||||
and urlparse(nxt).netloc == domain and nxt not in gesehen):
|
and urlparse(nxt).netloc == domain and _in_scope(nxt, prefix)
|
||||||
|
and nxt not in gesehen):
|
||||||
queue.append((nxt, tiefe + 1))
|
queue.append((nxt, tiefe + 1))
|
||||||
finally:
|
finally:
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|||||||
@@ -346,11 +346,13 @@ def _vermeide_block(vermeide: list[str] | None) -> str:
|
|||||||
|
|
||||||
# Lerner-Niveau (aus dem Score abgeleitet) → Adressaten-Rolle für die Frage. So entsteht die
|
# Lerner-Niveau (aus dem Score abgeleitet) → Adressaten-Rolle für die Frage. So entsteht die
|
||||||
# Schwierigkeit: nicht „extra schwer machen", sondern „für einen Anfänger/Experten fragen".
|
# Schwierigkeit: nicht „extra schwer machen", sondern „für einen Anfänger/Experten fragen".
|
||||||
|
# Je Stufe: Adressaten-Rolle + kognitive Anforderung (Bloom) + „frag so"-Cue. Ohne explizite Stufe nimmt
|
||||||
|
# das Modell den leichten Pfad (bloßer Abruf) — die Cues heben höhere Niveaus auf Anwenden/Analysieren/Transfer.
|
||||||
NIVEAU_ROLLE = {
|
NIVEAU_ROLLE = {
|
||||||
"anfaenger": "Der Lerner ist ANFÄNGER. Frage nach dem Grundverständnis — das Kernkonzept, einfach und direkt.",
|
"anfaenger": "Der Lerner ist ANFÄNGER. Kognitiv: ERINNERN/VERSTEHEN. Frage nach dem Grundverständnis — das Kernkonzept, einfach und direkt.",
|
||||||
"fortgeschritten": "Der Lerner ist FORTGESCHRITTEN. Frage etwas tiefer — Zusammenhänge und die typische Anwendung.",
|
"fortgeschritten": "Der Lerner ist FORTGESCHRITTEN. Kognitiv: ANWENDEN. Stelle eine kleine konkrete Situation und lass das Konzept darauf anwenden — frage nicht bloß die Definition ab.",
|
||||||
"experte": "Der Lerner ist EXPERTE. Frage anspruchsvoll — Sonderfälle, Abgrenzungen, typische Tücken.",
|
"experte": "Der Lerner ist EXPERTE. Kognitiv: ANALYSIEREN. Lass abgrenzen/vergleichen, einen Sonderfall einordnen oder eine typische Tücke (Hürde) aufdecken — nicht das Lehrbuch-Wissen abfragen.",
|
||||||
"meister": "Der Lerner ist auf MEISTER-Niveau. Frage sehr anspruchsvoll — Edge-Cases, präzise Begründung, Transfer auf Neues.",
|
"meister": "Der Lerner ist auf MEISTER-Niveau. Kognitiv: BEWERTEN/TRANSFER. Lass das Konzept auf ein NEUES Problem übertragen, eine Entscheidung begründen oder einen Trade-off abwägen.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,32 @@ def _relevanz_schema(data, ids: set[int] | None = None):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_JANEIN = ("ja", "nein")
|
||||||
|
|
||||||
|
|
||||||
|
def _janein_schema(data, ids: set[int] | None = None):
|
||||||
|
"""{"relevant": {"1": "ja", …}} → {id: ja/nein} · sonst None.
|
||||||
|
|
||||||
|
Binäres ja/nein — das Themen-Relevanz-Gate der Sichtung. Wie `_relevanz_schema`:
|
||||||
|
sind `ids` gegeben, müssen mindestens diese abgedeckt sein (Extras erlaubt).
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("relevant"), dict) or not data["relevant"]:
|
||||||
|
return None
|
||||||
|
out: dict[int, str] = {}
|
||||||
|
for k, v in data["relevant"].items():
|
||||||
|
try:
|
||||||
|
num = int(k)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
wert = str(v).strip().casefold()
|
||||||
|
if wert not in _JANEIN:
|
||||||
|
return None
|
||||||
|
out[num] = wert
|
||||||
|
if ids is not None and not ids <= set(out):
|
||||||
|
return None
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
_MAX_RESTARTS = 2
|
_MAX_RESTARTS = 2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ fastapi
|
|||||||
uvicorn[standard]
|
uvicorn[standard]
|
||||||
aiosqlite
|
aiosqlite
|
||||||
playwright
|
playwright
|
||||||
|
trafilatura
|
||||||
transformers
|
transformers
|
||||||
# torch NICHT hier listen — sonst zieht pip die CUDA-Variante (~2,5 GB).
|
# torch NICHT hier listen — sonst zieht pip die CUDA-Variante (~2,5 GB).
|
||||||
# Es wird separat als CPU-Build installiert (Dockerfile + Makefile-Target `install`).
|
# Es wird separat als CPU-Build installiert (Dockerfile + Makefile-Target `install`).
|
||||||
|
|||||||
25
templates/Prompt/Artefakt-Beispiel-Check.md
Normal file
25
templates/Prompt/Artefakt-Beispiel-Check.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Du bist Korrektheits-Prüfer für durchgerechnete Beispiele (Worked Examples) zum Thema "{topic}". Ein anderer Agent hat sie erzeugt — sie sollen dem Lerner eine **korrekte** Lösung vormachen. Ein falsches Beispiel prägt einen falschen Lösungsweg ein. Finde die fehlerhaften.
|
||||||
|
|
||||||
|
GRUNDWAHRHEIT — die belegten Fakten (nur hieran messen):
|
||||||
|
{fakten}
|
||||||
|
|
||||||
|
ZU PRÜFENDE BEISPIELE (nummeriert; PROBLEM / SCHRITTE / ERGEBNIS):
|
||||||
|
{beispiele}
|
||||||
|
|
||||||
|
Beanstande ein Beispiel, wenn EINES davon zutrifft:
|
||||||
|
- **Rechenfehler** in einem Schritt (Zahl, Umformung, Einheit).
|
||||||
|
- **Falsche Folgerung:** ein Schritt folgt nicht aus dem vorigen; das ERGEBNIS folgt nicht aus den Schritten.
|
||||||
|
- **Nicht gedeckt:** ein Schritt/Wert widerspricht den Fakten oder erfindet etwas, das dort nicht steht.
|
||||||
|
- **Unvollständig/irreführend:** der Weg ließe den Lerner einen falschen Schluss ziehen.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Miss NUR an den Fakten + an interner Logik. Rechne selbst nach.
|
||||||
|
- **Konservativ:** beanstande nur, was **klar** falsch ist. Im Zweifel behalten.
|
||||||
|
- Gib die 1-basierte Nummer (`index`) jedes fehlerhaften Beispiels an.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path} — eines von beiden:
|
||||||
|
{{"ok": true}}
|
||||||
|
{{"probleme": [{{"index": 2}}, {{"index": 5}}]}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
|
{extra}
|
||||||
@@ -9,6 +9,7 @@ EINZELN GENANNTE EINTRÄGE (jeweils entscheiden):
|
|||||||
Regeln:
|
Regeln:
|
||||||
- Fachlich valider, eigenständiger Baustein und KEINE Dublette/Variante des Inventars → `aufnehmen`.
|
- Fachlich valider, eigenständiger Baustein und KEINE Dublette/Variante des Inventars → `aufnehmen`.
|
||||||
- Erfunden, zu vage, oder Dublette/Variante eines Inventar-Bausteins → verwerfen (in keine Liste).
|
- Erfunden, zu vage, oder Dublette/Variante eines Inventar-Bausteins → verwerfen (in keine Liste).
|
||||||
|
- Doppelt ein Eintrag inhaltlich einen ANDEREN einzeln genannten Eintrag (gleiche Sache, andere Worte) → nur EINEN aufnehmen, den anderen verwerfen.
|
||||||
- Übernimm aufgenommene Einträge WÖRTLICH ("Titel — Kurzbeschreibung"), nicht umformulieren.{final}
|
- Übernimm aufgenommene Einträge WÖRTLICH ("Titel — Kurzbeschreibung"), nicht umformulieren.{final}
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|||||||
24
templates/Prompt/Fakten-Ergaenzung.md
Normal file
24
templates/Prompt/Fakten-Ergaenzung.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Du füllst **Lücken**. Für jeden Subbaustein des Themas "{topic}" sind unten die bereits erfassten Fakten gelistet. Ein erster Durchlauf hat sie extrahiert — dabei fällt erfahrungsgemäß Belegbares durch. Deine Aufgabe: **nur das Fehlende** aus der Quelle nachtragen.
|
||||||
|
|
||||||
|
{source}
|
||||||
|
|
||||||
|
SUBBAUSTEINE MIT BEREITS ERFASSTEN FAKTEN:
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Gib pro Subbaustein **nur NEUE** Punkte aus, die in der Quelle belegt sind und oben **noch fehlen**.
|
||||||
|
- **Wiederhole nichts** aus „Erfasst" — auch nicht umformuliert. Steht es schon da → weglassen.
|
||||||
|
- **Nur Belegbares.** Mit Quelldatei/Skript: sinngemäß-genau zitieren + Stelle angeben. Erfinde keine Werte, rechne nichts selbst aus. Ohne Quelldatei (reines Thema): nur gesichertes Standardwissen.
|
||||||
|
- **HARTE TRENNUNG:** `belegte_fakten` = nur Belegbares. Ein selbst gerechnetes/erdachtes Beispiel gehört in `beispiel_idee`, NIE in `belegte_fakten`.
|
||||||
|
- `kernpunkte` nur ergänzen, wenn ein **wesentlicher** Aspekt fehlt — keine Dopplungen, keine Trivialitäten.
|
||||||
|
- **Findest du für einen Subbaustein nichts Neues → lass ihn weg.** Findest du gar nichts → leere Liste. Lieber nichts als Erfundenes.
|
||||||
|
|
||||||
|
Schreibe NUR die NEUEN Fakten als EIN JSON in die Datei {out_path}, GENAU in diesem Format:
|
||||||
|
{{"fakten": [
|
||||||
|
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Subbaustein-Titel>",
|
||||||
|
"kernpunkte": ["…"], "voraussetzungen": "…", "huerden": "…",
|
||||||
|
"belegte_fakten": [{{"text": "…", "quelle": "…"}}], "beispiel_idee": "…"}}
|
||||||
|
]}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
|
{extra}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
Du baust den Fragen-Vorrat für eine Lern-Prüfung zum Thema "{topic}". Erzeuge **Frage-Muster** — **genau eine** konkrete Beispielfrage **pro Subbaustein**. Aus jedem Muster wird später bei der Prüfung eine leicht abgewandelte Frage formuliert — passend zum Niveau des Lerners (Anfänger bis Experte). Die Schwierigkeit steckt also NICHT im Muster, sondern kommt erst bei der Prüfung. Frag deshalb nach dem **Kernkonzept** des Subbausteins, neutral formuliert.
|
Du baust den Fragen-Vorrat für eine Lern-Prüfung zum Thema "{topic}". Erzeuge **Frage-Muster** — **genau eine** konkrete Beispielfrage **pro Subbaustein**. Aus jedem Muster wird später bei der Prüfung eine leicht abgewandelte Frage formuliert — passend zum Niveau des Lerners (Anfänger bis Experte). Die Schwierigkeit steckt also NICHT im Muster, sondern kommt erst bei der Prüfung. Frag deshalb nach dem **Kernkonzept** des Subbausteins, neutral formuliert.
|
||||||
|
|
||||||
Du bearbeitest **mehrere Bausteine**. Jeder Baustein hat seine eigenen Subbausteine:
|
Du bearbeitest **mehrere Bausteine**. Jeder Baustein hat seine eigenen Subbausteine — viele mit Kernpunkten, belegten Fakten und einem Beispiel:
|
||||||
{bausteine}
|
{bausteine}
|
||||||
|
|
||||||
AUFGABE (für **jeden** der Bausteine oben):
|
AUFGABE (für **jeden** der Bausteine oben):
|
||||||
- Erzeuge für **jeden** seiner Subbausteine **genau ein** Muster — eine konkrete, knappe Frage zum Kernkonzept dieses Subbausteins.
|
- Erzeuge für **jeden** seiner Subbausteine **genau ein** Muster — eine konkrete, knappe Frage zum Kernkonzept dieses Subbausteins.
|
||||||
- Jedes Muster ist EINE konkrete Frage (1–2 Sätze, genau ein Fragezeichen, eine Sache) — kein Mehrteiler, keine Aufzählung.
|
- Jedes Muster ist EINE konkrete Frage (1–2 Sätze, genau ein Fragezeichen, eine Sache) — kein Mehrteiler, keine Aufzählung.
|
||||||
- Die Frage muss aus dem Baustein-Wissen beantwortbar sein. Erfinde keine Zusatzannahmen.
|
- Die Frage muss aus dem Baustein-Wissen beantwortbar sein. Erfinde keine Zusatzannahmen.
|
||||||
- WICHTIG: Du siehst nur Titel, NICHT den späteren Guide-Text. Frag deshalb nur nach dem KERNKONZEPT des Subbausteins. Setze NICHTS voraus, was evtl. nicht im Guide steht: keine „gezeigten" Code-Ausschnitte/Beispiele/Diagramme, keine konkreten Werte/Listen/Zahlen.
|
- Stütze die Frage auf die gelieferten **Kernpunkte und belegten Fakten** (die verlässliche Grundlage). Triff das KERNKONZEPT des Subbausteins, nicht ein auswendig zu lernendes Detail. Setze NICHTS voraus, was nicht in den Fakten steht: keine „gezeigten" Code-Ausschnitte/Diagramme, keine konkreten Werte/Listen/Zahlen aus einem späteren Guide-Text.
|
||||||
- Die Frage trifft den **Kern** des Subbausteins — den zentralen Punkt, den man verstanden haben muss. Nicht zu eng auf ein Detail, nicht zu breit über den ganzen Baustein.
|
- Die Frage trifft den **Kern** des Subbausteins — den zentralen Punkt, den man verstanden haben muss. Nicht zu eng auf ein Detail, nicht zu breit über den ganzen Baustein.
|
||||||
- `baustein` ist exakt einer der Baustein-Titel oben. `subbaustein` ist exakt einer der Subbaustein-Titel des jeweiligen Bausteins.
|
- `baustein` ist exakt einer der Baustein-Titel oben. `subbaustein` ist exakt einer der Subbaustein-Titel des jeweiligen Bausteins.
|
||||||
|
|
||||||
|
|||||||
17
templates/Prompt/Gliederung-Voraussetzungen.md
Normal file
17
templates/Prompt/Gliederung-Voraussetzungen.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Ermittle die **Lern-Voraussetzungen** zwischen den Bausteinen des Themas "{topic}". Daraus wird die Reihenfolge des Guides berechnet.
|
||||||
|
|
||||||
|
BAUSTEINE (Nummer, Titel; manche nennen unter „braucht vorher" die Voraussetzungen ihrer Inhalte):
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
AUFGABE:
|
||||||
|
- Für **jeden** Baustein: Welche **anderen Bausteine aus der Liste** muss man **vorher** verstanden haben?
|
||||||
|
- Nur **echte fachliche Voraussetzungen** — „B baut direkt auf A auf", nicht „passt thematisch dazu".
|
||||||
|
- Nutze die „braucht vorher"-Hinweise, ordne sie den passenden Baustein-Nummern zu.
|
||||||
|
- Nummern **nur aus der Liste**. **Keine Selbstbezüge.** Im Zweifel **leer** — lieber keine Kante als eine erfundene.
|
||||||
|
- Halte es sparsam: die meisten Bausteine haben 0–2 direkte Voraussetzungen, nicht mehr.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}, GENAU in diesem Format (Schlüssel = Baustein-Nummer, Wert = Liste der Voraussetzungs-Nummern):
|
||||||
|
{{"prereqs": {{"3": [1, 7], "5": []}}}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
|
{extra}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{n} Agenten haben die feste Baustein-Auswahl für {format_name} zum Thema "{topic}" unabhängig gegliedert ({zweck}). Führe die Vorschläge zu EINER kohärenten Gliederung zusammen.
|
{n} Agenten haben die feste Baustein-Auswahl für {format_name} zum Thema "{topic}" unabhängig gegliedert ({zweck}). Führe die Vorschläge zu EINER kohärenten Gliederung zusammen.
|
||||||
|
|
||||||
GEWÄHLTE BAUSTEINE (jeder mit seiner NUMMER — die Auswahl steht fest):
|
GEWÄHLTE BAUSTEINE (jeder mit seiner NUMMER — die Auswahl steht fest; die Liste steht in **Lernreihenfolge**, Voraussetzungen zuerst — halte sie grob ein):
|
||||||
{bausteine}
|
{bausteine}
|
||||||
|
|
||||||
DIE VORSCHLÄGE (je Kapitel die Baustein-Nummern):
|
DIE VORSCHLÄGE (je Kapitel die Baustein-Nummern):
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ Plane die Gliederung eines Lern-Guides zum Thema "{topic}".
|
|||||||
BAUSTEINE (jeder mit seiner NUMMER — die Liste steht fest, du ordnest nur):
|
BAUSTEINE (jeder mit seiner NUMMER — die Liste steht fest, du ordnest nur):
|
||||||
{bausteine}
|
{bausteine}
|
||||||
|
|
||||||
|
Die Liste steht bereits in **Lernreihenfolge** (Voraussetzungen zuerst, per Abhängigkeits-Sortierung). Halte sie **grob** ein; gruppiere benachbarte verwandte Bausteine zu Kapiteln.
|
||||||
|
|
||||||
REIHENFOLGE — vom Bekannten zum Unbekannten:
|
REIHENFOLGE — vom Bekannten zum Unbekannten:
|
||||||
- Kapitel 1 führt zum schnellsten sichtbaren Ergebnis (erster Erfolg), nicht zur Theorie.
|
- Kapitel 1 führt zum schnellsten sichtbaren Ergebnis (erster Erfolg), nicht zur Theorie.
|
||||||
- Konkretes vor Abstraktem, Einfaches vor Komplexem, Voraussetzungen vor dem, was auf ihnen aufbaut.
|
- Konkretes vor Abstraktem, Einfaches vor Komplexem, Voraussetzungen vor dem, was auf ihnen aufbaut.
|
||||||
|
|||||||
23
templates/Prompt/Quelle-Relevanz.md
Normal file
23
templates/Prompt/Quelle-Relevanz.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Du sichtest gecrawlte Seiten für das Lern-Thema "{topic}". Entscheide je Seite: **ja** (gehört zum Thema) oder **nein** (themenfremd). Die Auswahl steuert, welche Seiten überhaupt gelesen werden — themenfremde Seiten blähen das Inventar mit Fremdstoff auf.
|
||||||
|
|
||||||
|
LERNZIEL / SPEC (das ist der Maßstab):
|
||||||
|
{spec}
|
||||||
|
|
||||||
|
SEITEN (Nummer, dann die URL, darunter ein Inhalts-Auszug):
|
||||||
|
{seiten}
|
||||||
|
|
||||||
|
So entscheidest du:
|
||||||
|
- **ja**: Die Seite behandelt genau das Fachgebiet der Spec. Kern-, Standard- oder Vertiefungsstoff dazu. Auch Randthemen des RICHTIGEN Gebiets sind `ja`.
|
||||||
|
- **nein**: Anderes Fachgebiet als die Spec. Beispiel: Spec ist Backend-Entwicklung, die Seite behandelt Frontend, Storefront, Theming, Payment-Bedienung oder einen ganz anderen Rollen-/Produktbereich. Auch reine Marketing-, Community-, Event- oder Konto-Seiten ohne Lernstoff sind `nein`.
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
- Urteile auf URL **und** Inhalts-Auszug zusammen. Der URL-Slug ist oft am aussagekräftigsten.
|
||||||
|
- **Ignoriere das Navigations-Menü / Boilerplate** im Auszug (Login, „Sign up", Menüpunkte, Footer) — das steht auf jeder Seite und sagt nichts über das Thema.
|
||||||
|
- **Im Zweifel `ja`** — lieber eine Seite zu viel lesen als Wichtiges ausschließen. Sage nur `nein`, wenn die Seite klar in ein anderes Fachgebiet gehört.
|
||||||
|
- Bewerte jede Seite einzeln. Erfinde nichts dazu.
|
||||||
|
{extra}
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format (für JEDE Nummer genau ein Wert; kein weiterer Text):
|
||||||
|
{{"relevant": {{"1": "ja", "2": "nein", "3": "ja"}}}}
|
||||||
@@ -8,7 +8,7 @@ Relevanz:
|
|||||||
- **rand**: Randnotiz, Nischen-/Spezialfall, „nice to know", Deprecated/Exotik, rein historische Details, konkrete Zahlenbeispiele, bloße Querverweise. NICHT rand: ein zentrales Theorem/eine fundamentale Eigenschaft. Nur für den Komplett-Guide.
|
- **rand**: Randnotiz, Nischen-/Spezialfall, „nice to know", Deprecated/Exotik, rein historische Details, konkrete Zahlenbeispiele, bloße Querverweise. NICHT rand: ein zentrales Theorem/eine fundamentale Eigenschaft. Nur für den Komplett-Guide.
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
- Wäge die Stimmen ab, entscheide fachlich. Im Zweifel **relevant**.
|
- Wäge die Stimmen ab, entscheide am Kriterium **Kern vs Rand SEINES Bausteins**. `rand` ist eine echte Kategorie — markiere Randständiges bewusst so. Nur echte Kernkonzepte/zentrale Sätze sind nie `rand`.
|
||||||
- Für JEDE strittige Nummer genau einen Wert.
|
- Für JEDE strittige Nummer genau einen Wert.
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
Bewerte jeden Subbaustein des Themas "{topic}" nach Relevanz: relevant oder rand. Die Relevanz steuert, ob ein Punkt in die fokussierten Guides kommt oder nur in den Komplett-Guide.
|
Bewerte jeden Subbaustein des Themas "{topic}" nach Relevanz: relevant oder rand. Die Relevanz steuert, ob ein Punkt in die fokussierten Guides kommt oder nur in den Komplett-Guide.
|
||||||
|
|
||||||
SUBBAUSTEINE (Nummer in Klammern der zugehörige Baustein):
|
SUBBAUSTEINE (Nummer in Klammern der zugehörige Baustein; viele nennen ihre Kernpunkte mit „Kern:"):
|
||||||
{subbausteine}
|
{subbausteine}
|
||||||
|
|
||||||
|
Nutze die Kernpunkte zur fundierten Bewertung — urteile auf dem Inhalt, nicht nur dem Titel.
|
||||||
|
|
||||||
Relevanz:
|
Relevanz:
|
||||||
- **relevant**: Kern-/Standardwissen, das das Thema TRÄGT — zentrale Konzepte, Definitionen, Eigenschaften, gängige Praxis. Bei praktischen Themen: was man im Gebrauch braucht. Bei theoretischen Themen: zentrale Sätze/Eigenschaften, AUCH ohne praktischen Gebrauch. Was man wirklich kennen sollte.
|
- **relevant**: Kern-/Standardwissen, das das Thema TRÄGT — zentrale Konzepte, Definitionen, Eigenschaften, gängige Praxis. Bei praktischen Themen: was man im Gebrauch braucht. Bei theoretischen Themen: zentrale Sätze/Eigenschaften, AUCH ohne praktischen Gebrauch. Was man wirklich kennen sollte.
|
||||||
- **rand**: Randnotiz, Spezial-/Nischenfall, „nice to know", Deprecated/Exotik — PLUS rein historische Details (wer/wann), konkrete Zahlenbeispiele eines allgemeinen Prinzips, bloße Querverweise. NICHT rand: ein zentrales Theorem oder eine fundamentale Eigenschaft, nur weil sie „theoretisch" wirkt. Gehört nur in einen Komplett-Guide.
|
- **rand**: Randnotiz, Spezial-/Nischenfall, „nice to know", Deprecated/Exotik — PLUS rein historische Details (wer/wann), konkrete Zahlenbeispiele eines allgemeinen Prinzips, bloße Querverweise. NICHT rand: ein zentrales Theorem oder eine fundamentale Eigenschaft, nur weil sie „theoretisch" wirkt. Gehört nur in einen Komplett-Guide.
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
- Bewerte jeden Punkt einzeln, gemessen an SEINEM Baustein und am Thema.
|
- Diese Subbausteine gehören **alle schon** zum Thema — die Frage ist nicht *ob*, sondern *wie zentral*. Bewerte **Kern vs Rand SEINES Bausteins**, einzeln.
|
||||||
- Im Zweifel **relevant** — lieber zu viel als versehentlich Wichtiges weglassen.
|
- `rand` ist eine **echte Kategorie**, kein Rest. Markiere Randständiges (nice-to-know, Spezial-/Nischenfälle, Detail-/Zahlenbeispiele, historische Notizen, Querverweise) **bewusst** als `rand`.
|
||||||
|
- Stufe nicht aus Vorsicht hoch. Nur **echte Kernkonzepte / zentrale Sätze** sind nie `rand`.
|
||||||
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
|
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Stufe = Lernpfad-Position (WANN man den Punkt braucht), NICHT Schwierigkeit:
|
|||||||
Regeln:
|
Regeln:
|
||||||
- Ein einfaches Nischen-Detail ist `experte`; ein fundamentales komplexes Konzept ist `anfaenger`.
|
- Ein einfaches Nischen-Detail ist `experte`; ein fundamentales komplexes Konzept ist `anfaenger`.
|
||||||
- Wäge die Stimmen ab, entscheide fachlich nach Lernpfad-Position.
|
- Wäge die Stimmen ab, entscheide fachlich nach Lernpfad-Position.
|
||||||
|
- **`fortgeschritten` ist keine Verlegenheits-Wahl.** Wähle die Mitte nicht, weil die Stimmen streuen — entscheide am Kriterium „Wann braucht man das?". `anfaenger`/`experte` klar treffen, wo sie zutreffen.
|
||||||
- Für JEDE strittige Nummer genau eine Stufe.
|
- Für JEDE strittige Nummer genau eine Stufe.
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
Ordne jeden Subbaustein des Themas "{topic}" seiner **Lernpfad-Position** zu: anfaenger, fortgeschritten oder experte. Die Stufe steuert, wann ein Lerner den Punkt sieht (Anfänger sehen nur „anfaenger", Fortgeschrittene mehr).
|
Ordne jeden Subbaustein des Themas "{topic}" seiner **Lernpfad-Position** zu: anfaenger, fortgeschritten oder experte. Die Stufe steuert, wann ein Lerner den Punkt sieht (Anfänger sehen nur „anfaenger", Fortgeschrittene mehr).
|
||||||
|
|
||||||
SUBBAUSTEINE (nach Baustein gruppiert):
|
SUBBAUSTEINE (nach Baustein gruppiert; viele nennen ihre Kernpunkte mit „Kern:"):
|
||||||
{subbausteine}
|
{subbausteine}
|
||||||
|
|
||||||
|
Nutze die Kernpunkte zur fundierten Einstufung — urteile auf dem Inhalt, nicht nur dem Titel.
|
||||||
|
|
||||||
ENTSCHEIDEND — die Stufe misst, WANN man den Punkt im Lernpfad braucht, NICHT wie schwer er zu verstehen ist:
|
ENTSCHEIDEND — die Stufe misst, WANN man den Punkt im Lernpfad braucht, NICHT wie schwer er zu verstehen ist:
|
||||||
- **anfaenger**: Fundament. Was man als Erstes/unbedingt braucht, um den Baustein überhaupt zu verstehen. Voraussetzung für den Rest. Auch wenn das Konzept gedanklich anspruchsvoll ist — ist es das Fundament, gehört es hierher.
|
- **anfaenger**: Fundament. Was man als Erstes/unbedingt braucht, um den Baustein überhaupt zu verstehen. Voraussetzung für den Rest. Auch wenn das Konzept gedanklich anspruchsvoll ist — ist es das Fundament, gehört es hierher.
|
||||||
- **fortgeschritten**: baut auf dem Fundament auf. Gängige Varianten, typische Anwendung, der nächste Schritt.
|
- **fortgeschritten**: baut auf dem Fundament auf. Gängige Varianten, typische Anwendung, der nächste Schritt.
|
||||||
@@ -18,6 +20,12 @@ Regeln:
|
|||||||
- Nicht jeder Baustein braucht einen `anfaenger`-Punkt. Manche Themen setzen Vorwissen voraus und sind ganz `fortgeschritten`/`experte` — das ist gewollt. Stufe ehrlich nach Lernpfad-Position, ohne künstlichen Einstieg zu erfinden.
|
- Nicht jeder Baustein braucht einen `anfaenger`-Punkt. Manche Themen setzen Vorwissen voraus und sind ganz `fortgeschritten`/`experte` — das ist gewollt. Stufe ehrlich nach Lernpfad-Position, ohne künstlichen Einstieg zu erfinden.
|
||||||
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
|
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
|
||||||
|
|
||||||
|
NICHT zur Mitte flüchten:
|
||||||
|
- **`fortgeschritten` ist keine Verlegenheits-Wahl.** Wähle die Mitte nicht, weil du unsicher bist.
|
||||||
|
- Entscheide jede Stufe an der Frage **„Wann im Lernpfad braucht man das?"** — nicht nach der sichersten Mittel-Option.
|
||||||
|
- **Differenziere innerhalb des Bausteins:** Was ist Fundament (anfaenger), was Aufbau (fortgeschritten), was Feinheit (experte)? Nicht alles ist Mitte.
|
||||||
|
- Triff `anfaenger` und `experte` klar, wo sie zutreffen — kein Bonus für die Mitte.
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
Format (für JEDE Nummer genau eine Stufe; kein weiterer Text):
|
Format (für JEDE Nummer genau eine Stufe; kein weiterer Text):
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ Unten je Baustein des Themas "{topic}" die gefundenen Subbausteine. Gib je Baust
|
|||||||
|
|
||||||
{source}
|
{source}
|
||||||
|
|
||||||
GEFUNDENE SUBBAUSTEINE:
|
GEFUNDENE SUBBAUSTEINE (je Baustein zwei Gruppen):
|
||||||
{bausteine}
|
{bausteine}
|
||||||
|
|
||||||
|
Die Gruppen:
|
||||||
|
- **Konsens (≥2 Finder):** mehrfach unabhängig gefunden — standardmäßig behalten. Nur Erfundenes/Dubletten raus.
|
||||||
|
- **Unsicher (1×):** nur von einem Finder genannt — **streng prüfen**. Nimm einen Unsicher-Eintrag NUR auf, wenn er **klar in der Quelle belegt UND ein eigenständiger Punkt** ist. Im Zweifel weglassen.
|
||||||
|
|
||||||
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.
|
- **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.)
|
||||||
|
|||||||
Reference in New Issue
Block a user