diff --git a/backend/bausteine.py b/backend/bausteine.py index 7971556..44b61ba 100644 --- a/backend/bausteine.py +++ b/backend/bausteine.py @@ -11,7 +11,6 @@ die Gesamtliste. """ import asyncio -import json import logging import math import shutil @@ -23,7 +22,7 @@ from config import KONSENS_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER from fsutil import atomic_write_text, atomic_write_json from jsonio import read_json_file as _json_datei from lernen import FRAGETYPEN -from paths import arbeit_dir, bausteine_path, frage_muster_path, grafik_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 pipeline import ( CANCELLED, FAILED, GenContext, _extra, _log, _prompt, _race, _relevanz_schema, _rest_schema, @@ -54,7 +53,6 @@ BAUSTEINE_STEPS = ( "Stufen finden", "Stufen wählen", "Stufen klären", "Relevanz finden", "Relevanz wählen", "Relevanz klären", "Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen", - "Grafik finden", "Grafik prüfen", ) @@ -111,19 +109,6 @@ def lade_frage_muster(topic: str, baustein: str) -> list[dict]: ] -def lade_grafiken(topic: str) -> dict: - """Alle Baustein-Grafiken eines Themas: {Baustein-Titel: {knoten, kanten, richtung}}. - Leere/ungültige Datei → {} (Frontend zeigt Fallback).""" - g = _json_datei(grafik_path(topic)) - return g if isinstance(g, dict) else {} - - -def lade_grafik(topic: str, baustein: str) -> dict | None: - """Grafik eines Bausteins (None = keine → Fallback).""" - g = lade_grafiken(topic).get(baustein) - return g if isinstance(g, dict) and g.get("knoten") else None - - def lade_uebersicht(topic: str) -> list[dict]: """Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen. @@ -164,7 +149,6 @@ def _bausteine_steps(topic: str) -> tuple: "Stufen finden", "Stufen wählen", "Stufen klären", "Relevanz finden", "Relevanz wählen", "Relevanz klären", "Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen", - "Grafik finden", "Grafik prüfen", ) mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest return (("Quelle laden",) if q["type"] == "link" else ()) + mitte @@ -182,7 +166,6 @@ PHASEN = ( ("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")), ("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")), ("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen")), - ("Grafiken", ("Grafik finden", "Grafik prüfen")), ) @@ -223,19 +206,18 @@ def _bausteine_files(topic: str) -> dict: "sub_roh": arbeit / "subbausteine-roh.json", "sidecar": subbausteine_path(topic), "frage_muster": frage_muster_path(topic), - "grafik": grafik_path(topic), } def _alle_slot_dateien(files: dict) -> list[Path]: arbeit = files["arbeit"] # Subbaustein-/Stufen-Slots sind pro Chunk dynamisch — per Glob einsammeln. - dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*")) + list(arbeit.glob("frage-muster-*")) + list(arbeit.glob("grafik-*"))) if arbeit.is_dir() else [] + dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*")) + list(arbeit.glob("frage-muster-*"))) if arbeit.is_dir() else [] return [ *files["recherche"], files["recherche_mapping"], *(p for slots in files["auswahl"].values() for p in slots), *files["mapping"].values(), files["ergaenzung"], - files["sub_roh"], files["sidecar"], files["frage_muster"], files["grafik"], *dyn, + files["sub_roh"], files["sidecar"], files["frage_muster"], *dyn, ] @@ -275,9 +257,6 @@ def _resume_step(topic: str) -> int: # Relevanz fertig; nur noch Frage-Muster offen? if not _frage_muster_komplett(topic): return _step_idx(topic, "Fragen finden") - # Frage-Muster fertig; nur noch Grafiken offen? - if not _grafik_komplett(topic): - return _step_idx(topic, "Grafik finden") return len(_bausteine_steps(topic)) if _sub_roh_schema(_json_datei(files["sub_roh"])) is None: return _step_idx(topic, "Subbausteine finden") @@ -316,7 +295,6 @@ def reset_bausteine(topic: str) -> None: files["final"].unlink(missing_ok=True) files["sidecar"].unlink(missing_ok=True) # liegt im Themen-Root, nicht in arbeit/ files["frage_muster"].unlink(missing_ok=True) # ebenfalls im Themen-Root - files["grafik"].unlink(missing_ok=True) # ebenfalls im Themen-Root quelle_path(topic).unlink(missing_ok=True) shutil.rmtree(quelle_crawl_dir(topic), ignore_errors=True) # gecrawlte Link-Quelle shutil.rmtree(files["arbeit"], ignore_errors=True) @@ -335,9 +313,6 @@ def _reset_ab_phase(topic: str, phase: int) -> None: for p in arbeit.glob(pat): p.unlink(missing_ok=True) - if phase <= 6: # Grafiken - files["grafik"].unlink(missing_ok=True) - glob_del("grafik-*") if phase <= 5: # Fragen files["frage_muster"].unlink(missing_ok=True) glob_del("frage-muster-*") @@ -497,45 +472,6 @@ def _frage_muster_komplett(topic: str) -> bool: return isinstance(_json_datei(frage_muster_path(topic)), dict) -def _grafik_schema(data) -> dict | None: - """{"knoten": [{id, text}], "kanten": [{von, nach, text?}], "richtung"?} → bereinigtes - Grafik-Dict · sonst None. Knoten nicht leer, IDs eindeutig, jede Kante referenziert - existierende Knoten (sonst verworfen). Leere Knotenliste → None.""" - if not isinstance(data, dict) or not isinstance(data.get("knoten"), list): - return None - knoten, ids = [], set() - for k in data["knoten"]: - if not isinstance(k, dict): - return None - kid = str(k.get("id", "")).strip() - text = str(k.get("text", "")).strip() - if not kid or not text or kid in ids: - continue - ids.add(kid) - knoten.append({"id": kid, "text": text}) - if not knoten: - return None - kanten = [] - for e in (data.get("kanten") or []): - if not isinstance(e, dict): - continue - von, nach = str(e.get("von", "")).strip(), str(e.get("nach", "")).strip() - if von not in ids or nach not in ids: - continue # hängende Kante (halluzinierte ID) verwerfen - kante = {"von": von, "nach": nach} - if (t := str(e.get("text", "")).strip()): - kante["text"] = t - kanten.append(kante) - richtung = data.get("richtung") if data.get("richtung") in ("TB", "LR") else "TB" - return {"richtung": richtung, "knoten": knoten, "kanten": kanten} - - -def _grafik_komplett(topic: str) -> bool: - """Grafik-Sidecar existiert (Build gelaufen)? Einzelne leere Bausteine fallen im - Frontend auf den Fallback-Hinweis zurück — daher genügt die Datei.""" - return isinstance(_json_datei(grafik_path(topic)), dict) - - def _read(p: Path) -> str: return p.read_text(encoding="utf-8") if p.exists() else "" @@ -1054,109 +990,6 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict return ergebnis -async def _grafik_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None: - """Block F: zwei Phasen — Finden (1 Generator je Baustein, parallel) erzeugt eine - Graph-Grafik (Knoten/Kanten) als JSON, Prüfen (Kritiker je Baustein) fixt Struktur + - Inhalt; Bausteine ohne Grafik eine Runde nachholen. - → {Baustein-Titel: {knoten, kanten, richtung}} oder None bei Abbruch.""" - topic, is_cancelled = ctx.topic, ctx.is_cancelled - arbeit = files["arbeit"] - beschr = {b["titel"]: b["beschreibung"] for b in lade_uebersicht(topic)} - # Je Baustein die relevanten Subbausteine (relevanz != 'rand') als Graph-Grundlage. - bausteine = [] - for titel, subs in sidecar.items(): - rel = [s["titel"] for s in subs if isinstance(s, dict) and s.get("relevanz") != "rand" and str(s.get("titel", "")).strip()] - if rel: - bausteine.append((titel, rel)) - if not bausteine: - return {} - - def roh_path(c): - return arbeit / f"grafik-c{c}.json" - - def final_path(c): - return arbeit / f"grafik-final-c{c}.json" - - # Phase „Grafik finden": pro Baustein 1 Generator, alle parallel. - async def _finde(c, titel, rel): - fp = roh_path(c) - if _grafik_schema(_json_datei(fp)): - return # Resume - sub_block = "\n".join(f"- {s}" for s in rel) - status, _ = await run_single_slot( - ctx, f"Grafik {c}", - key=f"bausteine-{topic}-grafik-c{c}", - prompt=_prompt("Baustein-Grafik", topic=topic, baustein=titel, - beschreibung=beschr.get(titel, ""), subbausteine=sub_block, - out_path=fp, extra=_extra(instructions)), - role="guide", capabilities="files", - payload=lambda result, p=fp: _grafik_schema(_json_datei(p)), - timeout=_timeout("grafik", len(rel)), - ) - if status == FAILED: - _log(topic, f"Grafik Baustein {c} fehlgeschlagen — keine Grafik (Fallback)") - - set_p(f"Grafik finden ({len(bausteine)} Bausteine)…", step=_step_idx(topic, "Grafik finden")) - await asyncio.gather(*[_finde(c, t, r) for c, (t, r) in enumerate(bausteine, 1)], return_exceptions=True) - if is_cancelled(): - return None - - # Phase „Grafik prüfen": Kritiker je Baustein fixt Struktur/Inhalt; sonst Roh übernehmen. - async def _pruefe(c, titel, rel): - roh = _grafik_schema(_json_datei(roh_path(c))) - fp = final_path(c) - if _grafik_schema(_json_datei(fp)) or not roh: - return - sub_block = "\n".join(f"- {s}" for s in rel) - status, _ = await run_single_slot( - ctx, f"Grafik-Prüfung {c}", - key=f"bausteine-{topic}-grafik-final-c{c}", - prompt=_prompt("Baustein-Grafik-Check", topic=topic, baustein=titel, - subbausteine=sub_block, - grafik=json.dumps(roh, ensure_ascii=False, indent=1), - out_path=fp, extra=_extra(instructions)), - role="judge", capabilities="files", - payload=lambda result, p=fp: _grafik_schema(_json_datei(p)), - timeout=_timeout("grafik_check", len(rel)), - ) - if status == FAILED: - _log(topic, f"Grafik-Prüfung Baustein {c} fehlgeschlagen — Roh-Grafik übernommen") - - set_p(f"Grafik prüfen ({len(bausteine)} Bausteine)…", step=_step_idx(topic, "Grafik prüfen")) - await asyncio.gather(*[_pruefe(c, t, r) for c, (t, r) in enumerate(bausteine, 1)], return_exceptions=True) - if is_cancelled(): - return None - - def _finalisiere(c): - return _grafik_schema(_json_datei(final_path(c))) or _grafik_schema(_json_datei(roh_path(c))) - - ergebnis = {} - for c, (titel, rel) in enumerate(bausteine, 1): - if (g := _finalisiere(c)): - ergebnis[titel] = g - - # Bausteine ohne Grafik eine Runde nachholen. - leer = [(c, t, r) for c, (t, r) in enumerate(bausteine, 1) if t not in ergebnis] - if leer: - _log(topic, f"Grafik: {len(leer)} Baustein(e) ohne Grafik — Nachrunde") - for c, t, r in leer: - roh_path(c).unlink(missing_ok=True) - final_path(c).unlink(missing_ok=True) - await asyncio.gather(*[_finde(c, t, r) for c, t, r in leer], return_exceptions=True) - if is_cancelled(): - return None - await asyncio.gather(*[_pruefe(c, t, r) for c, t, r in leer], return_exceptions=True) - if is_cancelled(): - return None - for c, t, r in leer: - if (g := _finalisiere(c)): - ergebnis[t] = g - rest = [t for c, t, r in leer if t not in ergebnis] - if rest: - _log(topic, f"Grafik: {len(rest)} Baustein(e) bleiben leer (Fallback): {rest[:5]}") - return ergebnis - - async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None) -> None: if topic in _bausteine_progress: return @@ -1453,19 +1286,6 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str = if muster is None: return # Abbruch atomic_write_json(files["frage_muster"], muster, indent=1) - - # Block F: pro Baustein eine Graph-Grafik (Knoten/Kanten) der relevanten - # Subbausteine → eigenes Sidecar. Von allen Guide-Formaten geteilt; das - # Frontend rendert sie mit Auto-Layout (dagre) + KaTeX. - sidecar = _json_datei(files["sidecar"]) - if _sidecar_schema(sidecar) is not None and _relevanz_komplett(sidecar) and _frage_muster_komplett(topic) and not _grafik_komplett(topic): - grafik = await _grafik_block(ctx, set_p, files, sidecar, instructions) - if is_cancelled(): - abgebrochen() - return - if grafik is None: - return # Abbruch - atomic_write_json(files["grafik"], grafik, indent=1) except Exception as e: log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic) _bausteine_errors[topic] = str(e)[:2000] diff --git a/backend/config.py b/backend/config.py index 6f4e665..0590981 100644 --- a/backend/config.py +++ b/backend/config.py @@ -60,10 +60,6 @@ TIMEOUTS = { "frage_muster_check": (300, 10), # Kritiker bereinigt die Muster-Tabelle je Baustein "writer": (600, 120), # pro Section im Chunk "lese_check": (300, 10), # pro Section im Paket - "grafik": (600, 60), # Graph-Grafik je Baustein (Knoten/Kanten), n = Subbausteine - "grafik_check": (300, 15), # Grafik-Prüfung/Fix je Baustein, n = Subbausteine - "roadmap": (600, 0), # Roadmap-Graph aus der Baustein-Übersicht bauen - "roadmap_check": (300, 0), # Judge prüft/fixt den Roadmap-Graph } # Auswahl-Auftrag je Format: (Mindest-Anteil, Maximal-Anteil, Mindestanzahl, Zweck). diff --git a/backend/elements.py b/backend/elements.py index 7e76800..d10a496 100644 --- a/backend/elements.py +++ b/backend/elements.py @@ -71,7 +71,7 @@ def _topic_context(topic: str, limit: int = 12000) -> str: bp = bausteine_path(topic) if bp.exists(): parts.append(bp.read_text(encoding="utf-8")) - for fmt in ("Guide",): # Roadmap ist ein Graph ohne Prosa-Sections → kein Text-Kontext + for fmt in ("Guide", "FullGuide"): # bester verfügbarer Prosa-Guide als Chat-Kontext content = _json_datei(guide_content_path(topic, fmt)) if content: for ch in content.get("chapters", []): diff --git a/backend/guide.py b/backend/guide.py index 4e53bd2..baee7a9 100644 --- a/backend/guide.py +++ b/backend/guide.py @@ -1,4 +1,4 @@ -"""Guide-Generierung als Konsens-Pipeline (Roadmap hat einen eigenen Weg). +"""Guide-Generierung als Konsens-Pipeline. Auswahl: 5 Agenten (min. 3, Grace) → Code-Voting (Mehrheit = Konsens) → Mapping-Agent sortiert Strittiges → Klärungs-Loop (max. KONSENS_MAX_RUNDEN). @@ -24,7 +24,6 @@ import lesbarkeit from database import list_guides, update_guide from fsutil import atomic_write_json from jsonio import read_json_file as _json_datei -from roadmap import _generate_roadmap from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path from pipeline import ( CANCELLED, FAILED, GenContext, _claude_error, _extra, @@ -429,7 +428,7 @@ async def _generate_sections( def _hat_relevanz(num, art): return any(isinstance(s, dict) and s.get("relevanz") == art for s in subs_raw.get(_titel(entries[num]), [])) - # Schritt 0: Auswahl. Deterministisch je Format (Roadmap hat einen eigenen Weg): + # Schritt 0: Auswahl. Deterministisch je Format: # Guide=relevante Bausteine · FullGuide=alle · Rest=Rand-Bausteine. if format_name == "FullGuide": auswahl = list(entries) @@ -528,10 +527,10 @@ async def _generate_sections( # Garantie: jeder gewählte Baustein steht im Plan (gegen weglassende Agenten/Judges). plan = _mit_resten(plan, sel_entries) - # Chunks festlegen — Inhalte, Inhalts-Prüfung, Writer und Lese-Check nutzen - # dieselbe Aufteilung: 1 Agent je ~30 Bausteine (gedeckelt). - total_sections = sum(len(c["nums"]) for c in plan) - chunks = _split_chunks(plan, min(WRITER_MAX, max(1, math.ceil(total_sections / WRITER_SECTIONS)))) + # 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 + # rationiert sein Output-Budget dann nicht über viele Sections → längere, variable Texte. + chunks = [[{"title": ch["title"], "nums": [num]}] for ch in plan for num in ch["nums"]] # Subbausteine je Baustein, je Format gefiltert: Guide=relevant · FullGuide=alle · Rest=Rand. # Subs ohne Relevanz-Feld (Alt-Themen) zählen als „nicht Rand" (→ Guide/FullGuide). if format_name == "FullGuide": @@ -727,10 +726,9 @@ async def _generate_sections( if is_cancelled(): return None if res is None: - if runde == 1: - await _fail(guide_id, "Lese-Prüfung fehlgeschlagen") - return None - _log(topic, f"Lese-Prüfung Runde {runde} fehlgeschlagen — Stand bleibt") + # Nicht hart failen: bei 1 Agent je Baustein darf ein einzelner Check-Ausfall + # nicht den ganzen Guide kippen. Sections sind geschrieben; Lese-Prüfung ist Politur. + _log(topic, f"Lese-Prüfung Runde {runde} ohne vollständiges Ergebnis — Stand bleibt") break probleme_by_num: dict[int, str] = {} @@ -859,25 +857,19 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio for p_alt in guide_slot_dateien(content_path): p_alt.unlink(missing_ok=True) - if format_name == "Roadmap": - graph = await _generate_roadmap(guide_id, topic, instructions, provider, content_path) - if graph is None or is_guide_cancelled(guide_id): - return - content = {"topic": topic, "format": format_name, "graph": graph} - else: - alle = _lade_bausteine(bausteine_path(topic).read_text(encoding="utf-8")) - if not alle: - await _fail(guide_id, "Keine Bausteine gefunden") - return - entries = _eindeutige_titel(alle) - facts = _prompt("Guide-Fakten-Projekt", project=project) if project else _prompt("Guide-Fakten-Thema") - chapters = await _generate_sections( - guide_id, topic, format_name, entries, - facts, instructions, provider, content_path, - ) - if chapters is None or is_guide_cancelled(guide_id): - return - content = {"topic": topic, "format": format_name, "chapters": chapters} + alle = _lade_bausteine(bausteine_path(topic).read_text(encoding="utf-8")) + if not alle: + await _fail(guide_id, "Keine Bausteine gefunden") + return + entries = _eindeutige_titel(alle) + facts = _prompt("Guide-Fakten-Projekt", project=project) if project else _prompt("Guide-Fakten-Thema") + chapters = await _generate_sections( + guide_id, topic, format_name, entries, + facts, instructions, provider, content_path, + ) + if chapters is None or is_guide_cancelled(guide_id): + return + content = {"topic": topic, "format": format_name, "chapters": chapters} atomic_write_json(content_path, content, indent=1) diff --git a/backend/models.py b/backend/models.py index 239e918..adddd7a 100644 --- a/backend/models.py +++ b/backend/models.py @@ -2,7 +2,6 @@ from pydantic import BaseModel, Field from typing import Literal FormatType = Literal[ - "Roadmap", "Guide", "FullGuide", "Rest", @@ -30,7 +29,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=6) # Re-Run ab grober Phase (1 Inventar … 6 Grafiken); None = Resume/Fortsetzen ohne Löschen + 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 class BausteineStep(BaseModel): diff --git a/backend/paths.py b/backend/paths.py index efee04a..50b394f 100644 --- a/backend/paths.py +++ b/backend/paths.py @@ -31,11 +31,6 @@ def frage_muster_path(topic: str) -> Path: return topic_dir(topic) / "frage_muster.json" -def grafik_path(topic: str) -> Path: - """Sidecar: pro Baustein eine Graph-Grafik (Knoten/Kanten), von allen Guides geteilt.""" - return topic_dir(topic) / "grafik.json" - - def quelle_path(topic: str) -> Path: """Persistierte Quellen-Wahl pro Thema: {type, ort, spec}.""" return topic_dir(topic) / "quelle.json" diff --git a/backend/regeln.py b/backend/regeln.py index 71710e8..cb3513f 100644 --- a/backend/regeln.py +++ b/backend/regeln.py @@ -1,10 +1,10 @@ """Lernschulden-Regeln: Progression und Deckel für offene Guides — die EINZIGE Quelle. -Regeln (nur Neu-Erstellungen; Themen, Bausteine, Roadmap unbegrenzt): +Regeln (nur Neu-Erstellungen; Themen + Bausteine unbegrenzt): - Format „Guide": höchstens 3 erstellte, nicht absolvierte Guides - Keine Progression/Vorstufe mehr (nur ein Guide-Format). - Absolviert: ALLE Bausteine (Section-Titel) des neuesten fertigen Guides haben - eine bestandene Prüfung. Roadmap ist read-only (kein Fortschritt, keine Prüfung). + eine bestandene Prüfung. Rest ist read-only (kein Fortschritt, keine Prüfung). Alle Funktionen arbeiten auf einmal geladenen Daten (lade_lernstand) — keine Query-Schleifen mehr pro Guide. """ @@ -18,7 +18,7 @@ from textkit import _norm_titel MAX_OFFENE_GUIDES = 3 # Nur noch EIN Format „Guide" (alle relevanten Bausteine, Prüfung 0–30). Keine Progression, -# keine Vorstufe → der Guide ist immer freischaltbar. „Rest"/Roadmap separat. +# keine Vorstufe → der Guide ist immer freischaltbar. „Rest"/FullGuide separat. VORSTUFE: dict[str, str] = {} FREISCHALT_LEVEL: dict[str, str] = {} FORMATE = ("Guide",) diff --git a/backend/roadmap.py b/backend/roadmap.py deleted file mode 100644 index 089f01a..0000000 --- a/backend/roadmap.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Roadmap-Generierung: ein read-only Übersichts-Graph pro Thema (Advance Organizer). - -Quelle = vorhandene Bausteine (keine Web-Recherche). Zwei Schritte: -Bauen (1 Agent synthetisiert aus der Baustein-Übersicht einen gerichteten Graph aus -Bereichen) und Prüfung (1 Judge fixt Struktur/Knappheit). Schritt-Dateien bleiben liegen → -Abbruch erhält Fortschritt, ▶ setzt am offenen Schritt fort. -""" - -import json -from pathlib import Path - -from bausteine import lade_uebersicht -from config import KONSENS_MAX_RUNDEN -from jsonio import read_json_file as _json_datei -from pipeline import ( - CANCELLED, FAILED, GenContext, _extra, _fail, _log, - _prompt, _set_step, _timeout, is_guide_cancelled, run_single_slot, -) - -ROADMAP_STEPS = ("Bauen", "Prüfung") - -_PRIO = ("grund", "kern", "vertiefung") - - -def _roadmap_schema(data) -> dict | None: - """{"richtung", "knoten":[{id,title,text,prioritaet}], "kanten":[{von,nach,typ}]} → - bereinigtes Graph-Dict · sonst None. IDs eindeutig, Kanten referenzieren echte Knoten.""" - if not isinstance(data, dict) or not isinstance(data.get("knoten"), list): - return None - knoten, ids = [], set() - for k in data["knoten"]: - if not isinstance(k, dict): - return None - kid = str(k.get("id", "")).strip() - title = str(k.get("title", "")).strip() - text = str(k.get("text", "")).strip() - if not kid or not title or kid in ids: - continue - ids.add(kid) - prio = k.get("prioritaet") if k.get("prioritaet") in _PRIO else "kern" - knoten.append({"id": kid, "title": title, "text": text, "prioritaet": prio}) - if not knoten: - return None - kanten = [] - for e in (data.get("kanten") or []): - if not isinstance(e, dict): - continue - von, nach = str(e.get("von", "")).strip(), str(e.get("nach", "")).strip() - if von not in ids or nach not in ids or von == nach: - continue # hängende/Selbst-Kante verwerfen - typ = e.get("typ") if e.get("typ") in ("prereq", "optional") else "prereq" - kanten.append({"von": von, "nach": nach, "typ": typ}) - richtung = data.get("richtung") if data.get("richtung") in ("TB", "LR") else "TB" - return {"richtung": richtung, "knoten": knoten, "kanten": kanten} - - -def _uebersicht_block(topic: str) -> str: - """Baustein-Übersicht (nur Bausteine mit ≥1 relevantem Subbaustein) als Prompt-Text.""" - zeilen = [] - for b in lade_uebersicht(topic): - rel = [s["titel"] for s in b["subbausteine"] if s.get("relevanz") != "rand"] - if not rel: - continue - kopf = f"{b['num']}. {b['titel']}" - if b.get("beschreibung"): - kopf += f" — {b['beschreibung']}" - zeilen.append(kopf) - zeilen += [f" - {t}" for t in rel] - return "\n".join(zeilen) - - -async def _generate_roadmap( - guide_id: str, topic: str, instructions: str, provider: str, content_path: Path, -) -> dict | None: - is_cancelled = lambda: is_guide_cancelled(guide_id) - ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled, guide_id=guide_id) - - uebersicht = _uebersicht_block(topic) - if not uebersicht.strip(): - await _fail(guide_id, "Keine relevanten Bausteine für die Roadmap") - return None - - d, stem = content_path.parent, content_path.stem - bau_path = d / f"{stem}.roadmap-bau.json" - check_path = d / f"{stem}.roadmap-check.json" - - # Schritt 0: Bauen — 1 Agent synthetisiert den Graph. Gültige Datei überspringt. - graph = _roadmap_schema(_json_datei(bau_path)) - if graph is None: - await _set_step(guide_id, 0, "Baue Roadmap…") - status, _ = await run_single_slot( - ctx, "Roadmap-Bauen", - key=f"{guide_id}-roadmap-bau", - prompt=_prompt("Roadmap-Bauen", topic=topic, uebersicht=uebersicht, - out_path=bau_path, extra=_extra(instructions)), - role="guide", capabilities="files", - payload=lambda result: _roadmap_schema(_json_datei(bau_path)), - timeout=_timeout("roadmap"), - ) - if status == CANCELLED: - return None - if status == FAILED: - await _fail(guide_id, "Roadmap-Bau fehlgeschlagen") - return None - graph = _roadmap_schema(_json_datei(bau_path)) - - # Schritt 1: Prüfung — Judge fixt Struktur/Knappheit; Fallback = Bau-Graph. - geprueft = _roadmap_schema(_json_datei(check_path)) - if geprueft is None: - await _set_step(guide_id, 1, "Prüfe Roadmap…") - status, _ = await run_single_slot( - ctx, "Roadmap-Prüfung", - key=f"{guide_id}-roadmap-check", - prompt=_prompt("Roadmap-Check", topic=topic, uebersicht=uebersicht, - graph=json.dumps(graph, ensure_ascii=False, indent=1), - out_path=check_path, extra=_extra(instructions)), - role="judge", capabilities="files", - payload=lambda result: _roadmap_schema(_json_datei(check_path)), - timeout=_timeout("roadmap_check"), - ) - if status == CANCELLED: - return None - if status == FAILED: - _log(topic, "Roadmap-Prüfung fehlgeschlagen — Bau-Graph bleibt") - geprueft = _roadmap_schema(_json_datei(check_path)) - - return geprueft or graph diff --git a/backend/routes.py b/backend/routes.py index 98c2d61..b14709f 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -16,7 +16,7 @@ from database import ( list_baustein_progress, get_baustein_progress, set_offene_frage, set_baustein_score, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten, ) -from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster, lade_grafiken +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 @@ -212,13 +212,6 @@ async def get_frage_muster(topic: str, baustein: str): return {"muster": lade_frage_muster(topic, baustein)} -@router.get("/bausteine/grafiken") -async def get_grafiken(topic: str): - """Alle Baustein-Grafiken eines Themas: {Baustein-Titel: {knoten, kanten, richtung}}. - Einmal je Thema geladen; das Frontend schlägt pro Baustein nach dem Titel nach.""" - return {"grafiken": lade_grafiken(topic)} - - # --- Baustein-Lernen: Chat, Prüfung --- @router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse) @@ -478,7 +471,7 @@ async def list_all(): async def guide_locks(topic: str): """Sperr-Gründe pro Format für den ▶-Button — None = erstellbar.""" guides, progress, levels = await lade_lernstand() - return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("Roadmap", "FullGuide", "Rest", *FORMATE)} + return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("FullGuide", "Rest", *FORMATE)} @router.get("/guides/{guide_id}", response_model=GuideResponse) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 928a215..d3f8249 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,6 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@dagrejs/dagre": "^3.0.0", - "@vue-flow/core": "^1.48.2", "dompurify": "^3.4.7", "highlight.js": "^11.11.1", "katex": "^0.17.0", @@ -481,21 +479,6 @@ "node": ">=6.9.0" } }, - "node_modules/@dagrejs/dagre": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-3.0.0.tgz", - "integrity": "sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==", - "license": "MIT", - "dependencies": { - "@dagrejs/graphlib": "4.0.1" - } - }, - "node_modules/@dagrejs/graphlib": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-4.0.1.tgz", - "integrity": "sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==", - "license": "MIT" - }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -897,12 +880,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", - "license": "MIT" - }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", @@ -920,22 +897,6 @@ "vue": "^3.2.25" } }, - "node_modules/@vue-flow/core": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.2.tgz", - "integrity": "sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==", - "license": "MIT", - "dependencies": { - "@vueuse/core": "^10.5.0", - "d3-drag": "^3.0.0", - "d3-interpolate": "^3.0.1", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0" - }, - "peerDependencies": { - "vue": "^3.3.0" - } - }, "node_modules/@vue/babel-helper-vue-transform-on": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", @@ -1123,94 +1084,6 @@ "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", "license": "MIT" }, - "node_modules/@vueuse/core": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", - "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.11.1", - "@vueuse/shared": "10.11.1", - "vue-demi": ">=0.14.8" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, - "node_modules/@vueuse/metadata": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", - "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", - "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", - "license": "MIT", - "dependencies": { - "vue-demi": ">=0.14.8" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, "node_modules/ansis": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.0.tgz", @@ -1337,111 +1210,6 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1a6a9b4..8cadba0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,8 +9,6 @@ "preview": "vite preview" }, "dependencies": { - "@dagrejs/dagre": "^3.0.0", - "@vue-flow/core": "^1.48.2", "dompurify": "^3.4.7", "highlight.js": "^11.11.1", "katex": "^0.17.0", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2481d46..135fb2f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -181,7 +181,7 @@ async function loadBausteine() { } } -const FORMAT_ORDER = ['Guide', 'FullGuide', 'Roadmap', 'Rest'] +const FORMAT_ORDER = ['Guide', 'FullGuide', 'Rest'] function autoPreview() { const map = doneByFormat.value @@ -202,7 +202,7 @@ function selectTopic(topic) { elementsView.value = false bausteineView.value = false elementOpenId.value = null - ansichtModus.value = localStorage.getItem('ansicht_' + topic) || 'kompakt' + ansichtModus.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'kompakt' localStorage.setItem('lastTopic', topic) loadBausteine() nextTick(autoPreview) diff --git a/frontend/src/api.js b/frontend/src/api.js index 0e3d913..d132d1e 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -94,11 +94,6 @@ export async function fetchFrageMuster(topic, baustein) { return jsonOrThrow(res) } -export async function fetchGrafiken(topic) { - const res = await fetch(`${BASE}/bausteine/grafiken?topic=${encodeURIComponent(topic)}`) - return jsonOrThrow(res) -} - export async function fetchTopicFortschritt(topic) { const res = await fetch(`${BASE}/topics/fortschritt?topic=${encodeURIComponent(topic)}`) return res.json() diff --git a/frontend/src/components/BausteinFokus.vue b/frontend/src/components/BausteinFokus.vue index f2c97e1..641f682 100644 --- a/frontend/src/components/BausteinFokus.vue +++ b/frontend/src/components/BausteinFokus.vue @@ -1,11 +1,9 @@ - - - - diff --git a/frontend/src/components/RoadmapView.vue b/frontend/src/components/RoadmapView.vue deleted file mode 100644 index 16773d3..0000000 --- a/frontend/src/components/RoadmapView.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - - - diff --git a/frontend/src/components/TopicDetail.vue b/frontend/src/components/TopicDetail.vue index aa48c1c..84f0543 100644 --- a/frontend/src/components/TopicDetail.vue +++ b/frontend/src/components/TopicDetail.vue @@ -1,12 +1,10 @@