update
This commit is contained in:
@@ -48,7 +48,7 @@ RECHERCHE_THEMA_AGENTEN = 5 # Web-Modus (Quelle „thema", kein Crawl-Ordner)
|
||||
RECHERCHE_KAPPE = 1800 # Sicherheits-Deckel je Batch-Agent
|
||||
SICHTUNG_CHUNK = 80 # Crawl-Seiten je Sichtungs-Agent (Content/Noise-Triage)
|
||||
SUBBAUSTEIN_KAPPE = 900 # Subbaustein-Finde-Loop je Chunk (15 min)
|
||||
KONSOLIDIERUNG_CHUNK = 150 # Konsolidierung ab so vielen Kandidaten chunken
|
||||
KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); darüber chunked + Merge-Pass
|
||||
|
||||
log = logging.getLogger("creator.bausteine")
|
||||
|
||||
@@ -180,7 +180,7 @@ def _bausteine_steps(topic: str) -> tuple:
|
||||
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
|
||||
)
|
||||
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
|
||||
return (("Quelle laden",) if q["type"] == "link" else ()) + mitte
|
||||
return (("Quelle aufbereiten",) if q["type"] == "link" else ()) + mitte
|
||||
|
||||
|
||||
def _step_idx(topic: str, name: str) -> int:
|
||||
@@ -198,7 +198,7 @@ def _melde_p(set_p, topic: str, schritt: str):
|
||||
# Grobe Anzeige-Phasen: bündeln die Feinschritte (intern bleibt alles feingranular).
|
||||
# Sonderschritte (Quelle laden, Ergänzung) gehören zur Phase „Inventar".
|
||||
PHASEN = (
|
||||
("Inventar", ("Quelle laden", "Recherche", "Konsolidierung", "Klärung", "Ergänzung")),
|
||||
("Inventar", ("Quelle aufbereiten", "Recherche", "Konsolidierung", "Klärung", "Ergänzung")),
|
||||
("Subbausteine", ("Subbausteine finden", "Subbausteine wählen", "Subbausteine klären")),
|
||||
("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")),
|
||||
("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")),
|
||||
@@ -273,7 +273,7 @@ def _resume_step(topic: str) -> int:
|
||||
files = _bausteine_files(topic)
|
||||
q = lade_quelle(topic)
|
||||
if q["type"] == "link" and not _crawl_fertig(topic):
|
||||
return _step_idx(topic, "Quelle laden")
|
||||
return _step_idx(topic, "Quelle aufbereiten")
|
||||
if not files["final"].exists(): # Inventar (DB-Loop) noch offen
|
||||
return _step_idx(topic, "Recherche")
|
||||
if q["type"] == "projekt" and not files["ergaenzung"].exists():
|
||||
@@ -1267,6 +1267,39 @@ async def _sichte_inhalt(ctx: GenContext, set_p, files: dict, ordner, pages: lis
|
||||
return content or set(pages) # alles als Noise markiert? → fail-open, alle behalten
|
||||
|
||||
|
||||
async def _quelle_aufbereiten(ctx: GenContext, set_p, files: dict, q: dict, ordner) -> bool:
|
||||
"""Schritt „Quelle aufbereiten": Crawl (link) + PDF-Konvert + Content/Noise-Sichtung.
|
||||
Persistiert die Sichtung in der Coverage-Tabelle (inhalt). → True (ok) / False (Abbruch/Fehler).
|
||||
thema: nichts. projekt/uni: nur PDFs (kuratierter Ordner, keine Sichtung)."""
|
||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||
if not ordner:
|
||||
return True # thema → keine Quelle aufzubereiten
|
||||
if q["type"] != "link":
|
||||
await asyncio.to_thread(_pdfs_konvertieren, ordner) # projekt/uni: nur PDFs, keine Sichtung
|
||||
return True
|
||||
if await db.get_step_status(topic, "Quelle aufbereiten") == "fertig":
|
||||
return True
|
||||
if not _crawl_fertig(topic):
|
||||
set_p("Quelle laden (Crawl)…", step=_step_idx(topic, "Quelle aufbereiten"))
|
||||
n = await asyncio.to_thread(crawl, q["ort"], ordner, cancelled=is_cancelled)
|
||||
if is_cancelled():
|
||||
return False
|
||||
if not n:
|
||||
_bausteine_errors[topic] = "Crawl ergab keine Inhalte — Link/Domain prüfen"
|
||||
return False
|
||||
await asyncio.to_thread(_pdfs_konvertieren, ordner)
|
||||
pages = sorted(set(_crawl_index(ordner).values()))
|
||||
if pages:
|
||||
await db.delete_coverage(topic)
|
||||
content = await _sichte_inhalt(ctx, set_p, files, ordner, pages)
|
||||
if is_cancelled():
|
||||
return False
|
||||
noise = [p for p in pages if p not in content]
|
||||
await db.mark_inhalt(topic, sorted(content), noise)
|
||||
await db.set_step_status(topic, "Quelle aufbereiten", "fertig")
|
||||
return True
|
||||
|
||||
|
||||
async def _recherche_batch(ctx: GenContext, set_p, files: dict, q: dict, ordner, instructions: str) -> bool:
|
||||
"""Befüllt DB-Tabelle `bausteine` mit Kandidaten (+ Nennungszähler). FESTE Datei-Batches:
|
||||
jede Crawl-Seite wird genau einem Batch zugeteilt und von RECHERCHE_READERS Agenten gelesen
|
||||
@@ -1276,8 +1309,7 @@ async def _recherche_batch(ctx: GenContext, set_p, files: dict, q: dict, ordner,
|
||||
if await db.get_step_status(topic, "Recherche") == "fertig":
|
||||
return True
|
||||
arbeit = files["arbeit"]
|
||||
await db.delete_bausteine(topic)
|
||||
await db.delete_coverage(topic)
|
||||
await db.delete_bausteine(topic) # Coverage/inhalt gehört der Sichtung — NICHT löschen
|
||||
await db.set_step_status(topic, "Recherche", "laufend")
|
||||
|
||||
async def _ingest(text: str) -> None:
|
||||
@@ -1293,7 +1325,9 @@ async def _recherche_batch(ctx: GenContext, set_p, files: dict, q: dict, ordner,
|
||||
quelle = [teile[2]] if len(teile) >= 3 and teile[2] else []
|
||||
await db.upsert_baustein(topic, norm, titel, besch, quelle)
|
||||
|
||||
pages = sorted(set(_crawl_index(ordner).values()))
|
||||
pages = await db.list_content(topic) # von der Sichtung als Content markierte Seiten
|
||||
if not pages and ordner:
|
||||
pages = sorted(set(_crawl_index(ordner).values())) # Fallback (projekt/uni: keine Sichtung)
|
||||
|
||||
if not pages:
|
||||
# Quelle „thema" (oder kein Crawl): freie Web-Recherche, eine Runde.
|
||||
@@ -1320,14 +1354,9 @@ async def _recherche_batch(ctx: GenContext, set_p, files: dict, q: dict, ordner,
|
||||
await db.set_step_status(topic, "Recherche", "fertig")
|
||||
return True
|
||||
|
||||
# Content/Noise-Triage: Noise (Events/Clubs/Profile/…) raus, nur Content batchen.
|
||||
content = await _sichte_inhalt(ctx, set_p, files, ordner, pages)
|
||||
if is_cancelled():
|
||||
return False
|
||||
pages = sorted(content)
|
||||
|
||||
# Crawl: feste Batches, je Batch RECHERCHE_READERS Reader, die GENAU diese Dateien lesen.
|
||||
batches = _chunk_nums(pages, max(1, math.ceil(len(pages) / RECHERCHE_BATCH)))
|
||||
# Content-Seiten stehen schon fest (Sichtung im Schritt „Quelle aufbereiten").
|
||||
# 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)))
|
||||
|
||||
async def _lese_batch(bi: int, batch: list[str]) -> bool:
|
||||
liste = "\n".join(f"- {p}" for p in batch)
|
||||
@@ -1399,6 +1428,25 @@ async def _konsolidiere(ctx: GenContext, set_p, files: dict) -> bool:
|
||||
k, r = mapping
|
||||
konsens += k
|
||||
rest += r
|
||||
# Bei mehreren Chunks: ein globaler Merge-Pass über die vereinten Konsens-Einträge,
|
||||
# damit Dubletten über Chunk-Grenzen (DAL×4, PHPUnit×5 …) verschmelzen.
|
||||
if len(chunks) > 1 and konsens:
|
||||
fp = arbeit / "konsolidierung-merge.json"
|
||||
fp.unlink(missing_ok=True)
|
||||
eintraege = "\n".join(f"{i}. {t} (2× genannt)" for i, t in enumerate(konsens, 1))
|
||||
status, mapping = await run_single_slot(
|
||||
ctx, "Konsolidierung Merge",
|
||||
key=f"bausteine-{topic}-konsolidierung-merge",
|
||||
prompt=_prompt("Bausteine-Recherche-Mapping", topic=topic, n=RECHERCHE_READERS, eintraege=eintraege, out_path=fp),
|
||||
role="judge", capabilities="files",
|
||||
payload=lambda result, p=fp: _mapping_schema(_json_datei(p)),
|
||||
timeout=_timeout("recherche_mapping", len(konsens)),
|
||||
)
|
||||
if status == CANCELLED:
|
||||
return False
|
||||
if status != FAILED and mapping:
|
||||
konsens, r2 = mapping
|
||||
rest += r2 # vom Merge zurückgestufte Einträge in den Rest
|
||||
# Judge-Ausgabe ist maßgeblich → Inventar in der DB neu setzen.
|
||||
await db.delete_bausteine(topic)
|
||||
for t in konsens:
|
||||
@@ -1488,7 +1536,7 @@ async def _reset_db_ab_phase(topic: str, ab_phase: int) -> None:
|
||||
if ab_phase <= 1:
|
||||
await db.delete_bausteine(topic)
|
||||
await db.delete_coverage(topic)
|
||||
await db.delete_pipeline_state(topic, ["Recherche", "Konsolidierung", "Klärung"])
|
||||
await db.delete_pipeline_state(topic, ["Quelle aufbereiten", "Recherche", "Konsolidierung", "Klärung"])
|
||||
if ab_phase <= 2:
|
||||
await db.delete_subbausteine(topic)
|
||||
if ab_phase <= 5:
|
||||
@@ -1528,18 +1576,11 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
if ab_phase is not None:
|
||||
_reset_ab_phase(topic, ab_phase)
|
||||
await _reset_db_ab_phase(topic, ab_phase)
|
||||
# Link-Quelle: erst crawlen (gleiche Domain, begrenzt) → wird zur Ordner-Quelle.
|
||||
if q["type"] == "link" and not _crawl_fertig(topic):
|
||||
set_p("Quelle laden (Crawl)…", step=_step_idx(topic, "Quelle laden"))
|
||||
n = await asyncio.to_thread(crawl, q["ort"], ordner, cancelled=is_cancelled)
|
||||
# Schritt „Quelle aufbereiten": Crawl (link) + PDFs + Content/Noise-Sichtung.
|
||||
if not await _quelle_aufbereiten(ctx, set_p, files, q, ordner):
|
||||
if is_cancelled():
|
||||
abgebrochen()
|
||||
return
|
||||
if not n:
|
||||
_bausteine_errors[topic] = "Crawl ergab keine Inhalte — Link/Domain prüfen"
|
||||
return
|
||||
if ordner:
|
||||
await asyncio.to_thread(_pdfs_konvertieren, ordner)
|
||||
return
|
||||
# „Neu erstellen": NUR wenn wirklich alles fertig ist (bausteine.md UND
|
||||
# Sidecar) → kompletter Frischstart. Liegt bausteine.md ohne Sidecar vor,
|
||||
# ist das ein Teil-Stand (Block B/C offen) → Resume, nicht wischen.
|
||||
|
||||
@@ -124,12 +124,13 @@ CREATE TABLE IF NOT EXISTS frage_muster (
|
||||
)
|
||||
"""
|
||||
|
||||
# Crawl-Seiten-Abdeckung: treibt den Recherche-Loop (welche Quelle wurde zitiert?).
|
||||
# Crawl-Seiten je Thema: inhalt (1=Content/0=Noise, von der Sichtung) + gelesen (von der Recherche).
|
||||
CREATE_RECHERCHE_COVERAGE = """
|
||||
CREATE TABLE IF NOT EXISTS recherche_coverage (
|
||||
topic TEXT NOT NULL,
|
||||
quelle TEXT NOT NULL,
|
||||
gelesen INTEGER NOT NULL DEFAULT 0,
|
||||
inhalt INTEGER,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (topic, quelle)
|
||||
)
|
||||
@@ -202,6 +203,10 @@ async def init_db():
|
||||
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
|
||||
except aiosqlite.OperationalError:
|
||||
pass
|
||||
try: # Migration: recherche_coverage.inhalt (Content/Noise aus der Sichtung)
|
||||
await db.execute("ALTER TABLE recherche_coverage ADD COLUMN inhalt INTEGER")
|
||||
except aiosqlite.OperationalError:
|
||||
pass
|
||||
try: # Migration für Bestands-DBs ohne verstanden-Spalte (Mastery-Stufe)
|
||||
await db.execute("ALTER TABLE baustein_progress ADD COLUMN verstanden TEXT")
|
||||
except aiosqlite.OperationalError:
|
||||
@@ -739,6 +744,30 @@ async def list_coverage(topic: str) -> dict[str, int]:
|
||||
return {q: g for q, g in rows}
|
||||
|
||||
|
||||
async def mark_inhalt(topic: str, content: list[str], noise: list[str]) -> None:
|
||||
"""Sichtungs-Ergebnis je Crawl-Seite ablegen: inhalt=1 (Content) bzw. 0 (Noise)."""
|
||||
db = await get_db()
|
||||
now = _now()
|
||||
rows = [(topic, q, 1, now) for q in content] + [(topic, q, 0, now) for q in noise]
|
||||
if not rows:
|
||||
return
|
||||
await db.executemany(
|
||||
"""INSERT INTO recherche_coverage (topic, quelle, inhalt, updated_at) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(topic, quelle) DO UPDATE SET inhalt = excluded.inhalt, updated_at = excluded.updated_at""",
|
||||
rows,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def list_content(topic: str) -> list[str]:
|
||||
"""Crawl-Seiten, die die Sichtung als Content markiert hat (inhalt=1)."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT quelle FROM recherche_coverage WHERE topic = ? AND inhalt = 1 ORDER BY quelle", (topic,)
|
||||
)
|
||||
return [r[0] for r in await cursor.fetchall()]
|
||||
|
||||
|
||||
async def delete_coverage(topic: str) -> None:
|
||||
db = await get_db()
|
||||
await db.execute("DELETE FROM recherche_coverage WHERE topic = ?", (topic,))
|
||||
|
||||
@@ -4,8 +4,12 @@ SEITEN (Dateiname · URL · Auszug):
|
||||
{seiten}
|
||||
|
||||
Regeln:
|
||||
- `noise` = eindeutig kein Lerninhalt. Typisch: Veranstaltungs-/Termin-Seiten, Mitglieder-/Profil-Seiten, Vereins-/Community-Seiten, Podcast-/Video-Verzeichnisse, reine Marketing-/Preis-/Werbeseiten, Login/Impressum, leere Navigations- oder Übersichtsseiten ohne Substanz.
|
||||
- Im ZWEIFEL NICHT markieren. Alles, was fachlichen Stoff enthalten könnte (Konzepte, Anleitungen, Beispiele, Definitionen), bleibt Content.
|
||||
- `noise` = eindeutig kein FACHLICHER Lerninhalt zum Thema. Markiere insbesondere:
|
||||
- Veranstaltungs-/Termin-, Mitglieder-/Profil-, Vereins-/Community-, Podcast-/Video-Verzeichnisse, Login/Impressum/Karriere.
|
||||
- **Marketing/Vertrieb:** Positionierung, ICP/Zielgruppe, Editionen/Preise/Pläne, Plattform-/Produkt-Werbung ohne How-to.
|
||||
- **Meta/Navigation:** Übersichts-/Index-/Landing-Seiten ohne eigenen Stoff, Lernpfad-/Kurs-Intros, Voraussetzungs-/„Was du lernst"-Seiten.
|
||||
- **Fremde Tracks:** Seiten eines ANDEREN Lern-Pfads/Tracks oder Levels als dieses Thema (z.B. Frontend statt Backend, „Intermediate"/„Advanced" wenn das Thema „Essentials" ist).
|
||||
- Im ZWEIFEL bei ECHTER Technik NICHT markieren: konkrete Konzepte, Anleitungen, Code, Konfiguration, Verfahren bleiben Content — auch Deployment/PaaS/Betrieb.
|
||||
- Nutze NUR die exakten Dateinamen aus der Liste oben.
|
||||
|
||||
Schreibe NUR die JSON-Datei nach {out_path} (kein weiterer Text):
|
||||
|
||||
Reference in New Issue
Block a user