update
This commit is contained in:
@@ -103,13 +103,15 @@ CREATE TABLE IF NOT EXISTS subbausteine (
|
||||
nennungen INTEGER NOT NULL DEFAULT 1,
|
||||
stufe TEXT,
|
||||
relevanz TEXT,
|
||||
fakten TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'kandidat',
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (topic, baustein_norm, sub_norm)
|
||||
)
|
||||
"""
|
||||
|
||||
# Ein Frage-Muster je (Baustein, Subbaustein, Typ).
|
||||
# Ein Frage-Muster je (Baustein, Subbaustein). Schwierigkeit kommt erst bei der Prüfung
|
||||
# aus dem Lerner-Niveau, nicht aus dem Muster — daher kein Typ-Kreuzprodukt mehr.
|
||||
CREATE_FRAGE_MUSTER = """
|
||||
CREATE TABLE IF NOT EXISTS frage_muster (
|
||||
topic TEXT NOT NULL,
|
||||
@@ -117,10 +119,9 @@ CREATE TABLE IF NOT EXISTS frage_muster (
|
||||
sub_norm TEXT NOT NULL,
|
||||
baustein TEXT NOT NULL,
|
||||
sub_titel TEXT NOT NULL,
|
||||
typ TEXT NOT NULL,
|
||||
frage TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (topic, baustein_norm, sub_norm, typ)
|
||||
PRIMARY KEY (topic, baustein_norm, sub_norm)
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -170,6 +171,33 @@ CREATE TABLE IF NOT EXISTS quelle (
|
||||
)
|
||||
"""
|
||||
|
||||
# Gliederung (Kapitel → Baustein-Nummern) je Thema als JSON. In der Bausteine-Phase erzeugt,
|
||||
# vom Guide nur noch gelesen + je Format gefiltert. Format-agnostisch (alle Bausteine).
|
||||
CREATE_GUIDE_GLIEDERUNG = """
|
||||
CREATE TABLE IF NOT EXISTS guide_gliederung (
|
||||
topic TEXT PRIMARY KEY,
|
||||
json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
# Generischer Lern-Artefakt-Layer: je (Baustein, Subbaustein, Typ) ein Artefakt (JSON in `daten`).
|
||||
# typ: karteikarte | beispiel | diagramm. sub_norm='' = Baustein-Ebene (z. B. Diagramm).
|
||||
# In der Bausteine-Phase aus den Fakten erzeugt, vom Frontend präsentiert (Trennung Artefakt/Anzeige).
|
||||
CREATE_SUB_ARTEFAKTE = """
|
||||
CREATE TABLE IF NOT EXISTS sub_artefakte (
|
||||
topic TEXT NOT NULL,
|
||||
baustein_norm TEXT NOT NULL,
|
||||
sub_norm TEXT NOT NULL DEFAULT '',
|
||||
typ TEXT NOT NULL,
|
||||
baustein TEXT NOT NULL DEFAULT '',
|
||||
sub_titel TEXT NOT NULL DEFAULT '',
|
||||
daten TEXT NOT NULL DEFAULT '{}',
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (topic, baustein_norm, sub_norm, typ)
|
||||
)
|
||||
"""
|
||||
|
||||
_db: aiosqlite.Connection | None = None
|
||||
|
||||
|
||||
@@ -199,6 +227,8 @@ async def init_db():
|
||||
await db.execute(CREATE_PIPELINE_STATE)
|
||||
await db.execute(CREATE_GUIDE_CONTENT)
|
||||
await db.execute(CREATE_QUELLE)
|
||||
await db.execute(CREATE_GUIDE_GLIEDERUNG)
|
||||
await db.execute(CREATE_SUB_ARTEFAKTE)
|
||||
try: # Migration für Bestands-DBs ohne step-Spalte
|
||||
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
|
||||
except aiosqlite.OperationalError:
|
||||
@@ -226,6 +256,16 @@ async def init_db():
|
||||
await db.execute(f"ALTER TABLE baustein_progress ADD COLUMN {_spalte} {_typ}")
|
||||
except aiosqlite.OperationalError:
|
||||
pass
|
||||
# Migration: frage_muster ohne typ-Spalte (1 Muster je Sub statt Sub×Typ-Kreuzprodukt).
|
||||
# PK-Änderung → Tabelle einmalig neu bauen. Bestands-Muster gehen verloren (bewusst, kein Mapping).
|
||||
cursor = await db.execute("PRAGMA table_info(frage_muster)")
|
||||
if any(_r[1] == "typ" for _r in await cursor.fetchall()):
|
||||
await db.execute("DROP TABLE frage_muster")
|
||||
await db.execute(CREATE_FRAGE_MUSTER)
|
||||
try: # Migration: subbausteine.fakten (Quell-Fakten je Sub, JSON-Blob) — Extract-once-Grounding.
|
||||
await db.execute("ALTER TABLE subbausteine ADD COLUMN fakten TEXT") # nullable: COALESCE schützt Bestand
|
||||
except aiosqlite.OperationalError:
|
||||
pass
|
||||
# Migration: alte vertiefungen-Tabelle → baustein_texte (Bestand = lange Form, art 'deepdive')
|
||||
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'vertiefungen'")
|
||||
if await cursor.fetchone():
|
||||
@@ -482,6 +522,21 @@ async def set_baustein_score(topic: str, baustein: str, score: int) -> int:
|
||||
return score
|
||||
|
||||
|
||||
async def set_baustein_score_and_streak(topic: str, baustein: str, score: int, streak: int) -> tuple[int, int]:
|
||||
"""Setzt Score + Streak atomar (vom Aufrufer geclampt). Liefert (score, streak)."""
|
||||
db = await get_db()
|
||||
await db.execute(
|
||||
"""INSERT INTO baustein_progress (topic, baustein, gute_antworten, streak, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(topic, baustein) DO UPDATE SET
|
||||
gute_antworten = excluded.gute_antworten, streak = excluded.streak,
|
||||
updated_at = excluded.updated_at""",
|
||||
(topic, baustein, score, streak, _now()),
|
||||
)
|
||||
await db.commit()
|
||||
return score, streak
|
||||
|
||||
|
||||
async def delete_baustein_progress(topic: str, baustein: str) -> None:
|
||||
"""Fortschritt EINES Bausteins zurücksetzen: Zeile löschen (Score/Streak/Flags/offene Frage).
|
||||
Fehlt die Zeile, liefert get_baustein_progress Defaults (0) — also voller Reset."""
|
||||
@@ -574,6 +629,75 @@ async def subs_count_alle() -> dict[tuple[str, str], int]:
|
||||
return {(t, bn): n for t, bn, n in await cursor.fetchall()}
|
||||
|
||||
|
||||
# Sub-Ebene aus den zwei orthogonalen Spalten: rand → 4 (V), sonst Stufe (Lernpfad-Position):
|
||||
# anfaenger/NULL → 1, fortgeschritten → 2, experte → 3. Alte Werte (einfach/mittel/schwer) werden
|
||||
# abwärtskompatibel mitgemappt. Steuert Guide-Ansicht A/F/E/V + cap (freigeschaltete Subs × 25).
|
||||
_EBENE_CASE = """CASE
|
||||
WHEN relevanz = 'rand' THEN 4
|
||||
WHEN stufe IN ('fortgeschritten', 'mittel') THEN 2
|
||||
WHEN stufe IN ('experte', 'schwer') THEN 3
|
||||
ELSE 1
|
||||
END"""
|
||||
|
||||
|
||||
def _leere_ebenen() -> dict[int, int]:
|
||||
return {1: 0, 2: 0, 3: 0, 4: 0}
|
||||
|
||||
|
||||
async def subs_je_ebene(topic: str, baustein: str) -> dict[int, int]:
|
||||
"""Konsens-Subbausteine eines Bausteins je Ebene 1–4. roher Baustein-Titel rein."""
|
||||
from textkit import _norm_titel
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
f"SELECT {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
|
||||
"WHERE topic = ? AND baustein_norm = ? AND status = 'konsens' GROUP BY ebene",
|
||||
(topic, _norm_titel(baustein)),
|
||||
)
|
||||
out = _leere_ebenen()
|
||||
for ebene, n in await cursor.fetchall():
|
||||
out[ebene] = n
|
||||
return out
|
||||
|
||||
|
||||
async def subs_je_ebene_roh(topic: str) -> dict[str, dict[int, int]]:
|
||||
"""Subbausteine je Ebene, gruppiert nach ROHEM Baustein-Titel (= Guide-Section-Titel)."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
f"SELECT baustein, {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
|
||||
"WHERE topic = ? AND status = 'konsens' GROUP BY baustein, ebene",
|
||||
(topic,),
|
||||
)
|
||||
out: dict[str, dict[int, int]] = {}
|
||||
for b, ebene, n in await cursor.fetchall():
|
||||
out.setdefault(b, _leere_ebenen())[ebene] = n
|
||||
return out
|
||||
|
||||
|
||||
async def subs_je_ebene_alle() -> dict[tuple[str, str], dict[int, int]]:
|
||||
"""Subbausteine je Ebene je (topic, baustein_norm) — für die themenweite Stufen-Ableitung."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
f"SELECT topic, baustein_norm, {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
|
||||
"WHERE status = 'konsens' GROUP BY topic, baustein_norm, ebene"
|
||||
)
|
||||
out: dict[tuple[str, str], dict[int, int]] = {}
|
||||
for t, bn, ebene, n in await cursor.fetchall():
|
||||
out.setdefault((t, bn), _leere_ebenen())[ebene] = n
|
||||
return out
|
||||
|
||||
|
||||
async def subs_mit_ebene(topic: str, baustein: str) -> list[dict]:
|
||||
"""Konsens-Subbausteine eines Bausteins mit Titel, sub_norm und Ebene 1–4."""
|
||||
from textkit import _norm_titel
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
f"SELECT sub_titel, sub_norm, {_EBENE_CASE} AS ebene FROM subbausteine "
|
||||
"WHERE topic = ? AND baustein_norm = ? AND status = 'konsens'",
|
||||
(topic, _norm_titel(baustein)),
|
||||
)
|
||||
return [{"titel": t, "norm": sn, "ebene": e} for t, sn, e in await cursor.fetchall()]
|
||||
|
||||
|
||||
async def list_baustein_scores_all() -> list[tuple[str, str, int]]:
|
||||
"""(topic, baustein, gute_antworten) je Baustein — Rohdaten für die Stufen-Ableitung."""
|
||||
db = await get_db()
|
||||
@@ -653,19 +777,21 @@ async def upsert_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baus
|
||||
|
||||
|
||||
async def put_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str,
|
||||
stufe: str | None = None, relevanz: str | None = None, status: str = "konsens") -> None:
|
||||
"""Insert/Update OHNE Nennungszähler (Spiegel aus dem Sidecar). stufe/relevanz nur überschreiben,
|
||||
wenn ein neuer Wert übergeben wird (COALESCE schützt Bestehendes)."""
|
||||
stufe: str | None = None, relevanz: str | None = None,
|
||||
fakten: str | None = None, status: str = "konsens") -> None:
|
||||
"""Insert/Update OHNE Nennungszähler (Spiegel aus dem Sidecar). stufe/relevanz/fakten nur
|
||||
überschreiben, wenn ein neuer Wert übergeben wird (COALESCE schützt Bestehendes)."""
|
||||
db = await get_db()
|
||||
await db.execute(
|
||||
"""INSERT INTO subbausteine (topic, baustein_norm, sub_norm, baustein, sub_titel, nennungen, stufe, relevanz, status, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
|
||||
"""INSERT INTO subbausteine (topic, baustein_norm, sub_norm, baustein, sub_titel, nennungen, stufe, relevanz, fakten, status, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
|
||||
baustein = excluded.baustein, sub_titel = excluded.sub_titel,
|
||||
stufe = COALESCE(excluded.stufe, subbausteine.stufe),
|
||||
relevanz = COALESCE(excluded.relevanz, subbausteine.relevanz),
|
||||
fakten = COALESCE(excluded.fakten, subbausteine.fakten),
|
||||
status = excluded.status, updated_at = excluded.updated_at""",
|
||||
(topic, baustein_norm, sub_norm, baustein, sub_titel, stufe, relevanz, status, _now()),
|
||||
(topic, baustein_norm, sub_norm, baustein, sub_titel, stufe, relevanz, fakten, status, _now()),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@@ -700,14 +826,14 @@ async def delete_subbausteine(topic: str) -> None:
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def upsert_frage_muster(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str, typ: str, frage: str) -> None:
|
||||
async def upsert_frage_muster(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str, frage: str) -> None:
|
||||
db = await get_db()
|
||||
await db.execute(
|
||||
"""INSERT INTO frage_muster (topic, baustein_norm, sub_norm, baustein, sub_titel, typ, frage, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(topic, baustein_norm, sub_norm, typ) DO UPDATE SET
|
||||
frage = excluded.frage, updated_at = excluded.updated_at""",
|
||||
(topic, baustein_norm, sub_norm, baustein, sub_titel, typ, frage, _now()),
|
||||
"""INSERT INTO frage_muster (topic, baustein_norm, sub_norm, baustein, sub_titel, frage, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
|
||||
sub_titel = excluded.sub_titel, frage = excluded.frage, updated_at = excluded.updated_at""",
|
||||
(topic, baustein_norm, sub_norm, baustein, sub_titel, frage, _now()),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@@ -849,10 +975,90 @@ async def delete_guide_content(topic: str, format: str | None = None) -> None:
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def set_gliederung(topic: str, gliederung_json: str) -> None:
|
||||
"""Gliederung (Kapitel→Nummern, JSON) je Thema speichern — Bausteine-Artefakt für den Guide."""
|
||||
db = await get_db()
|
||||
await db.execute(
|
||||
"""INSERT INTO guide_gliederung (topic, json, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(topic) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at""",
|
||||
(topic, gliederung_json, _now()),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_gliederung(topic: str) -> str | None:
|
||||
db = await get_db()
|
||||
cursor = await db.execute("SELECT json FROM guide_gliederung WHERE topic = ?", (topic,))
|
||||
row = await cursor.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
async def delete_gliederung(topic: str) -> None:
|
||||
db = await get_db()
|
||||
await db.execute("DELETE FROM guide_gliederung WHERE topic = ?", (topic,))
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def put_sub_artefakt(topic: str, baustein_norm: str, sub_norm: str, typ: str,
|
||||
daten: str, baustein: str = "", sub_titel: str = "") -> None:
|
||||
"""Ein Lern-Artefakt (karteikarte/beispiel/diagramm) als JSON in `daten` speichern."""
|
||||
db = await get_db()
|
||||
await db.execute(
|
||||
"""INSERT INTO sub_artefakte (topic, baustein_norm, sub_norm, typ, baustein, sub_titel, daten, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(topic, baustein_norm, sub_norm, typ) DO UPDATE SET
|
||||
baustein = excluded.baustein, sub_titel = excluded.sub_titel,
|
||||
daten = excluded.daten, updated_at = excluded.updated_at""",
|
||||
(topic, baustein_norm, sub_norm, typ, baustein, sub_titel, daten, _now()),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_sub_artefakte(topic: str, typ: str | None = None) -> list[dict]:
|
||||
db = await get_db()
|
||||
if typ is None:
|
||||
cursor = await db.execute("SELECT * FROM sub_artefakte WHERE topic = ? ORDER BY rowid", (topic,))
|
||||
else:
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM sub_artefakte WHERE topic = ? AND typ = ? ORDER BY rowid", (topic, typ)
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [_row_to_dict(row, cursor) for row in rows]
|
||||
|
||||
|
||||
async def delete_sub_artefakte(topic: str) -> None:
|
||||
db = await get_db()
|
||||
await db.execute("DELETE FROM sub_artefakte WHERE topic = ?", (topic,))
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_baustein_huerden(topic: str, baustein_norm: str) -> list[str]:
|
||||
"""Typische Irrtümer (huerden) aller Subs eines Bausteins — als Distraktor-Pool fürs Quiz.
|
||||
Liest aus subbausteine.fakten (JSON-Blob); leere/fehlende werden übersprungen."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT fakten FROM subbausteine WHERE topic = ? AND baustein_norm = ?", (topic, baustein_norm)
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
huerden = []
|
||||
for (fakten,) in rows:
|
||||
if not fakten:
|
||||
continue
|
||||
try:
|
||||
fk = json.loads(fakten)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
h = (fk.get("huerden") or "").strip() if isinstance(fk, dict) else ""
|
||||
if h:
|
||||
huerden.append(h)
|
||||
return huerden
|
||||
|
||||
|
||||
async def delete_topic_pipeline(topic: str) -> None:
|
||||
"""Bausteine-Bereich eines Themas verwerfen (Inventar/Subs/Muster/Coverage/State).
|
||||
"""Bausteine-Bereich eines Themas verwerfen (Inventar/Subs/Muster/Coverage/State/Artefakte).
|
||||
NICHT die Themen-Config `quelle` — die wird separat verwaltet (delete_quelle)."""
|
||||
db = await get_db()
|
||||
for tab in ("bausteine", "subbausteine", "frage_muster", "recherche_coverage", "pipeline_state"):
|
||||
for tab in ("bausteine", "subbausteine", "frage_muster", "recherche_coverage",
|
||||
"pipeline_state", "guide_gliederung", "sub_artefakte"):
|
||||
await db.execute(f"DELETE FROM {tab} WHERE topic = ?", (topic,))
|
||||
await db.commit()
|
||||
|
||||
Reference in New Issue
Block a user