diff --git a/backend/bausteine.py b/backend/bausteine.py index 66ad311..586a4b7 100644 --- a/backend/bausteine.py +++ b/backend/bausteine.py @@ -198,7 +198,8 @@ 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 aufbereiten", "Recherche", "Konsolidierung", "Klärung", "Ergänzung")), + ("Quelle", ("Quelle aufbereiten",)), + ("Inventar", ("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")), @@ -330,24 +331,30 @@ def reset_bausteine(topic: str) -> None: _bausteine_errors.pop(topic, None) -def _reset_ab_phase(topic: str, phase: int) -> None: - """Artefakte AB der groben Phase löschen (1 Inventar … 5 Fragen), frühere behalten. - Danach baut der normale Resume-Flow die fehlenden Phasen neu. Kumulativ: Re-Run ab P - löscht P…5. quelle.json + Crawl bleiben immer (Quellen-Wahl erhalten).""" +def _phase_idx(label: str) -> int: + """Index der groben Phase in der kanonischen Reihenfolge (Quelle=0 … Fragen=5).""" + order = [l for l, _ in PHASEN] + return order.index(label) if label in order else 1 + + +def _reset_ab_phase(topic: str, label: str) -> None: + """Datei-Artefakte AB der groben Phase `label` löschen (Quelle/Inventar … Fragen), frühere + behalten. Kumulativ. quelle.json + Crawl (.done) bleiben immer (re-crawl nur bei Voll-Reset).""" files = _bausteine_files(topic) arbeit = files["arbeit"] + idx = _phase_idx(label) def glob_del(pat: str) -> None: if arbeit.is_dir(): for p in arbeit.glob(pat): p.unlink(missing_ok=True) - if phase <= 5: # Fragen + if idx <= 5: # Fragen files["frage_muster"].unlink(missing_ok=True) glob_del("frage-muster-*") - if phase <= 4: # Relevanz + if idx <= 4: # Relevanz glob_del("relevanz-*") - if phase <= 3: # Stufen + Relevanz teilen die Sidecar → ab Stufen ganz neu + if idx <= 3: # Stufen + Relevanz teilen die Sidecar → ab Stufen ganz neu files["sidecar"].unlink(missing_ok=True) glob_del("stufe-*") else: # ab Relevanz: Stufen behalten, nur Relevanz-Felder strippen @@ -358,10 +365,10 @@ def _reset_ab_phase(topic: str, phase: int) -> None: if isinstance(s, dict): s.pop("relevanz", None) atomic_write_json(files["sidecar"], sc, indent=1) - if phase <= 2: # Subbausteine + if idx <= 2: # Subbausteine files["sub_roh"].unlink(missing_ok=True) glob_del("subbaustein-*") - if phase <= 1: # Inventar = kompletter Frischstart (alle Zwischendateien + bausteine.md) + if idx <= 1: # Inventar (und Quelle) = Inventar-Dateien + bausteine.md weg for p_alt in _alle_slot_dateien(files): p_alt.unlink(missing_ok=True) files["final"].unlink(missing_ok=True) @@ -1531,16 +1538,19 @@ async def _mirror_frage_muster_db(topic: str, muster: dict) -> None: await db.upsert_frage_muster(topic, bnorm, sn, btitel, sub, typ, frage) -async def _reset_db_ab_phase(topic: str, ab_phase: int) -> None: - """DB-Inhalt der Phasen ≥ ab_phase verwerfen (Inventar=1 … Fragen=5).""" - if ab_phase <= 1: - await db.delete_bausteine(topic) - await db.delete_coverage(topic) - 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: +async def _reset_db_ab_phase(topic: str, label: str) -> None: + """DB-Inhalt der Phasen ≥ `label` verwerfen (kanonische Reihenfolge Quelle…Fragen).""" + idx = _phase_idx(label) + if idx <= 5: # Fragen await db.delete_frage_muster(topic) + if idx <= 2: # Subbausteine (Stufen/Relevanz greifen über Sidecar→Mirror) + await db.delete_subbausteine(topic) + if idx <= 1: # Inventar: Inventar + Recherche-Schritte — Sichtung bleibt + await db.delete_bausteine(topic) + await db.delete_pipeline_state(topic, ["Recherche", "Konsolidierung", "Klärung"]) + if idx <= 0: # Quelle: Sichtung neu (Coverage/inhalt + Schritt) + await db.delete_coverage(topic) + await db.delete_pipeline_state(topic, ["Quelle aufbereiten"]) async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None) -> None: @@ -1574,8 +1584,10 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str = # Re-Run ab gewählter Phase: Artefakte ab dort löschen; der Frischstart-Block # unten wird übersprungen (er würde bei erhaltener Sidecar sonst alles wischen). if ab_phase is not None: - _reset_ab_phase(topic, ab_phase) - await _reset_db_ab_phase(topic, ab_phase) + phasen = _phasen(topic) + label = phasen[ab_phase - 1][0] if 1 <= ab_phase <= len(phasen) else "Inventar" + _reset_ab_phase(topic, label) + await _reset_db_ab_phase(topic, label) # Schritt „Quelle aufbereiten": Crawl (link) + PDFs + Content/Noise-Sichtung. if not await _quelle_aufbereiten(ctx, set_p, files, q, ordner): if is_cancelled(): diff --git a/backend/models.py b/backend/models.py index 0ce3e45..84991c0 100644 --- a/backend/models.py +++ b/backend/models.py @@ -30,7 +30,7 @@ class BausteineCreateRequest(BaseModel): provider: ProviderType = "claude" source_type: SourceType = "thema" source_ort: str = Field(default="", max_length=2000) - ab_phase: int | None = Field(default=None, ge=1, le=5) # Re-Run ab grober Phase (1 Inventar … 5 Fragen); None = Resume/Fortsetzen ohne Löschen + ab_phase: int | None = Field(default=None, ge=1, le=6) # Re-Run ab grober Phase (Position in _phasen(topic), 1-based); None = Resume/Fortsetzen ohne Löschen class BausteineStep(BaseModel):