This commit is contained in:
team3
2026-06-22 15:23:39 +02:00
parent 0951337a29
commit 36b7aabdb0
5 changed files with 69 additions and 23 deletions

View File

@@ -22,7 +22,7 @@ from config import (
)
import lesbarkeit
from database import list_guides, update_guide
from fsutil import atomic_write_json
from fsutil import atomic_write_json, atomic_write_text
from jsonio import read_json_file as _json_datei
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
from pipeline import (
@@ -101,6 +101,35 @@ def guide_slot_dateien(content_path: Path) -> list[Path]:
return [p for p in content_path.parent.glob(f"{content_path.stem}.*") if p != content_path]
def _fertig_path(content_path: Path) -> Path:
return content_path.parent / f"{content_path.stem}.fertig"
def guide_fertig_step(content_path: Path) -> int:
"""Höchster VOLL abgeschlossener Schritt-Index (Marker je Thema+Format). -1 = keiner.
Existiert die Content-Datei, sind alle Schritte fertig."""
if content_path.exists():
return len(GUIDE_STEPS) - 1
try:
return int(_fertig_path(content_path).read_text(encoding="utf-8").strip())
except (OSError, ValueError):
return -1
def _set_fertig(content_path: Path, step: int) -> None:
"""Marker auf `step` setzen — monoton (nur erhöhen), außer beim Re-Run-Reset (force)."""
if step > guide_fertig_step(content_path):
atomic_write_text(_fertig_path(content_path), str(step))
def _reset_fertig(content_path: Path, step: int) -> None:
"""Marker hart auf `step` setzen (für Re-Run ab Schritt; step kann sinken)."""
if step < 0:
_fertig_path(content_path).unlink(missing_ok=True)
else:
atomic_write_text(_fertig_path(content_path), str(step))
# Slot-Datei-Globs je Schritt (Index = GUIDE_STEPS). Stem-verankert, kollisionsfrei.
_STEP_GLOBS = (
("auswahl-*", "auswahl-mapping-*"), # 0 Auswahl (deterministisch → meist leer)
@@ -121,6 +150,7 @@ def _reset_guide_ab_step(content_path: Path, step: int) -> None:
for pat in globs:
for p in d.glob(f"{stem}.{pat}"):
p.unlink(missing_ok=True)
_reset_fertig(content_path, step - 1) # Schritte < step gelten als fertig
def _resolve_auswahl(data, entries: dict[int, str], k_min: int, k_max: int) -> list[int] | None:
@@ -552,6 +582,7 @@ async def _generate_sections(
# Garantie: jeder gewählte Baustein steht im Plan (gegen weglassende Agenten/Judges).
plan = _mit_resten(plan, sel_entries)
_set_fertig(content_path, 1) # Gliederung steht
# Chunks festlegen: 1 Chunk = 1 Baustein. So läuft JEDER Section-Schritt (Inhalte,
# Inhalts-Prüfung, Writer, Lese-Check) mit genau einem Agent je Baustein. Der Writer
@@ -605,6 +636,8 @@ async def _generate_sections(
if not inhalt_by_num:
await _fail(guide_id, "Keine Inhalte identifiziert")
return None
if all(p.exists() for p in inhalt_paths):
_set_fertig(content_path, 2) # Inhalte vollständig
inhalt_chunk_nums = [[num for ch in chunk for num in ch["nums"] if num in inhalt_by_num] for chunk in chunks]
@@ -670,6 +703,8 @@ async def _generate_sections(
if num in probleme_by_num and sec["md"].strip():
inhalt_by_num[num] = sec["md"]
_set_fertig(content_path, 3) # Inhalts-Check durch
# Schritt 4: Schreiben — Writer formuliert die geprüften Inhalte aus (Resume).
def inhalte_text(chunk) -> str:
nums = [num for ch in chunk for num in ch["nums"] if num in inhalt_by_num]
@@ -718,6 +753,8 @@ async def _generate_sections(
if not by_num:
await _fail(guide_id, "Keine Sections in der Writer-Ausgabe gefunden")
return None
if all(p.exists() for p in paths):
_set_fertig(content_path, 4) # Schreiben vollständig
# Schritt 3: Lese-Prüfungs-Loop — Check pro Writer-Paket, Fix nur für
# beanstandete Sections; Folgerunden prüfen NUR die ersetzten Sections.
@@ -821,6 +858,7 @@ async def _generate_sections(
_log(topic, f"Lese-Prüfung: 1 Runde — Überarbeitung bleibt ungeprüft")
break
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
_set_fertig(content_path, 5) # Lese-Prüfung durch
# Prüfbar = Format hat Prüfung UND Baustein hat ≥1 relevanten Subbaustein.
# Guide ist immer prüfbar (auch ohne Relevanz-Daten, Fallback = alles).

View File

@@ -19,7 +19,7 @@ from database import (
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
from lernen import NOETIG, MASTERY, MEISTERN, NIVEAUS, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, score_berechnen, floor_aus_meilensteinen, _zufall_malus
from guide import generate_guide, guide_slot_dateien
from guide import generate_guide, guide_slot_dateien, guide_fertig_step
from pipeline import cancel_guide
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
from models import (
@@ -474,6 +474,13 @@ async def guide_locks(topic: str):
return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("FullGuide", "Rest", *FORMATE)}
@router.get("/guides/steps")
async def guide_steps(topic: str):
"""Höchster voll abgeschlossener Schritt-Index je Format (artefakt-basiert, -1 = keiner).
Treibt die klickbaren Schritt-Kugeln (wie die Bausteine-Phasen)."""
return {fmt: guide_fertig_step(guide_content_path(topic, fmt)) for fmt in ("Guide", "FullGuide", "Rest")}
@router.get("/guides/{guide_id}", response_model=GuideResponse)
async def get_one(guide_id: str):
guide = await get_guide(guide_id)