update
This commit is contained in:
@@ -74,6 +74,8 @@ Note = 10 · (1 − gewichtete Befundquote); 10,0 nur bei null offenen Befunden.
|
||||
## Nebenläufigkeit
|
||||
|
||||
Einfache Wellen (`asyncio.gather` + Semaphoren in agents.py), keine Streaming-Engine.
|
||||
Innerhalb einer Ebene laufen unabhängige KETTEN (gen→verify je Chunk/Baustein) parallel;
|
||||
Stage-Barrieren nur, wo ein Schritt global rechnet (Dedup, Level, Ordnung, Konsens).
|
||||
Panels = n parallele Calls + Konsensregel. Hedge: Slot ohne Ergebnis nach
|
||||
max(90 s, timeout/2) bekommt einen Zwilling, erstes valides Ergebnis gewinnt.
|
||||
Live-Board trotzdem: jeder Statuswechsel → WebSocket-Broadcast.
|
||||
|
||||
@@ -248,12 +248,17 @@ def _beispiel_pruefen(inh: dict) -> tuple[bool, dict]:
|
||||
return True, inh
|
||||
|
||||
|
||||
def _formen_gate(topic: str) -> None:
|
||||
def _formen_gate(topic: str, atom_ids: list[int] | None = None) -> None:
|
||||
"""Kandidat-Beispiele durch ihr Form-Gate: Syntax/Struktur ungültig → verworfen
|
||||
(fail-closed für den Inhalt; ein fehlendes Beispiel ist erlaubt, optional)."""
|
||||
for b in db.query(
|
||||
"SELECT ar.* FROM artefakte ar JOIN atome a ON a.id=ar.atom_id"
|
||||
" WHERE a.topic=? AND ar.typ='beispiel' AND ar.status='kandidat'", (topic,)):
|
||||
(fail-closed für den Inhalt; ein fehlendes Beispiel ist erlaubt, optional).
|
||||
atom_ids: optional auf einen Chunk beschränken (Pipeline pro Kette)."""
|
||||
sql = ("SELECT ar.* FROM artefakte ar JOIN atome a ON a.id=ar.atom_id"
|
||||
" WHERE a.topic=? AND ar.typ='beispiel' AND ar.status='kandidat'")
|
||||
params: tuple = (topic,)
|
||||
if atom_ids:
|
||||
sql += f" AND ar.atom_id IN ({','.join('?' * len(atom_ids))})"
|
||||
params += tuple(atom_ids)
|
||||
for b in db.query(sql, params):
|
||||
inh = db.uj(b["inhalt"], {})
|
||||
ok, inh_neu = _beispiel_pruefen(inh)
|
||||
if not ok:
|
||||
@@ -262,13 +267,22 @@ def _formen_gate(topic: str) -> None:
|
||||
db.update("artefakte", "id", b["id"], inhalt=db.j(inh_neu))
|
||||
|
||||
|
||||
async def _kette(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
"""gen→gate→verify EINES Chunks verkettet — der Verify braucht nur die eigenen
|
||||
Kandidaten, nicht die Generierung fremder Chunks (Stage-Barriere kostete
|
||||
Wall-Clock: 1 Nachzügler blockierte alle Verifies)."""
|
||||
ohne = [a for a in chunk if not db.one(
|
||||
"SELECT id FROM artefakte WHERE atom_id=? AND typ='flashcard'"
|
||||
" AND status!='verworfen' LIMIT 1", (a["id"],))]
|
||||
if ohne:
|
||||
await _generieren(ctx, ohne)
|
||||
_formen_gate(ctx.topic, [a["id"] for a in chunk])
|
||||
await _verifizieren(ctx, chunk)
|
||||
|
||||
|
||||
async def bauen(ctx: llm.Kontext) -> None:
|
||||
ctx.ebene = EBENE
|
||||
neu = _chunks(ctx.topic, nur_ohne=True)
|
||||
await llm.alle(_generieren(ctx, c) for c in neu)
|
||||
_formen_gate(ctx.topic) # form-spezifische Syntax/Struktur-Gates vor dem Panel
|
||||
alle = _chunks(ctx.topic, nur_ohne=False)
|
||||
await llm.alle(_verifizieren(ctx, c) for c in alle)
|
||||
await llm.alle(_kette(ctx, c) for c in _chunks(ctx.topic, nur_ohne=False))
|
||||
|
||||
|
||||
def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
|
||||
@@ -137,6 +137,10 @@ def _init_schema(con: sqlite3.Connection) -> None:
|
||||
con.execute(zusatz)
|
||||
except sqlite3.OperationalError:
|
||||
pass # Spalte existiert schon
|
||||
# Status-Marker entfernter Ebenen (diagramme/fehlersuche) zurückrollen — sonst
|
||||
# kennt _ORDNUNG den Status nicht und der Resume fiele fälschlich auf korpus zurück.
|
||||
con.execute("UPDATE topics SET status='struktur_fertig'"
|
||||
" WHERE status IN ('diagramme_fertig', 'fehlersuche_fertig')")
|
||||
con.commit()
|
||||
|
||||
|
||||
|
||||
@@ -560,8 +560,8 @@ async def bauen(ctx: llm.Kontext) -> None:
|
||||
offen = [b for b in _bausteine(ctx.topic) if b["status"] != "fertig"
|
||||
or (db.one("SELECT stage FROM sections WHERE baustein_id=?", (b["id"],)) or
|
||||
{"stage": "writer"})["stage"] != "done"]
|
||||
await llm.alle(_karte(ctx, b) for b in offen)
|
||||
await _kapitel_intros(ctx)
|
||||
# Intros brauchen nur Kapitel+Ziele (keine Sections) → parallel zu den Karten
|
||||
await llm.alle([*(_karte(ctx, b) for b in offen), _kapitel_intros(ctx)])
|
||||
|
||||
|
||||
def kapitel_struktur(topic: str) -> list[dict]:
|
||||
|
||||
@@ -6,6 +6,24 @@ import re
|
||||
|
||||
_FENCE = re.compile(r"```(?:json)?\s*(.*?)```", re.DOTALL)
|
||||
|
||||
# LaTeX in JSON-Strings ("$\in$", "\Sigma") ist ein INVALIDES Escape → json.loads
|
||||
# wirft (kostete bis zu 29 % Parse-Fehler bei Mathe-Stages). Nur ungültige Escapes
|
||||
# verdoppeln: gültige (\" \\ \/ \b \f \n \r \t \uXXXX) bleiben; \u ohne 4
|
||||
# Hex-Ziffern (\underline) zählt als ungültig.
|
||||
_UNGUELTIGES_ESCAPE = re.compile(r'\\(?![\"\\/bfnrt]|u[0-9a-fA-F]{4})')
|
||||
|
||||
|
||||
def _loads(s: str):
|
||||
"""json.loads mit EINER Reparaturstufe für rohe LaTeX-Backslashes. Valides
|
||||
JSON durchläuft unverändert (Reparatur nur im Fehlerfall)."""
|
||||
try:
|
||||
return json.loads(s)
|
||||
except ValueError:
|
||||
try:
|
||||
return json.loads(_UNGUELTIGES_ESCAPE.sub(r"\\\\", s))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def parse(text: str):
|
||||
"""→ Objekt oder None. Nie werfen — der Aufrufer entscheidet über Retry.
|
||||
@@ -54,11 +72,8 @@ def _ein_kandidat(text: str):
|
||||
elif c == end_ch:
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
try:
|
||||
return json.loads(text[start:i + 1])
|
||||
except ValueError:
|
||||
break
|
||||
try:
|
||||
return json.loads(text)
|
||||
except ValueError:
|
||||
return None
|
||||
daten = _loads(text[start:i + 1])
|
||||
if daten is not None:
|
||||
return daten
|
||||
break
|
||||
return _loads(text)
|
||||
|
||||
@@ -134,6 +134,11 @@ async def call(ctx: Kontext, *, stage: str, template: str, werte: dict,
|
||||
status = "ok"
|
||||
return res.text
|
||||
daten = jsonx.parse(res.text)
|
||||
# Modelle lassen bei EINEM Element die Array-Klammern gern weg —
|
||||
# ein valides Einzel-Dict zählt als 1-elementige Liste (sonst
|
||||
# brannte jeder 1-Item-Chunk 3 Neuversuche durch).
|
||||
if erwartet is list and isinstance(daten, dict) and daten:
|
||||
daten = [daten] # leeres {} bleibt „falscher Typ" (= Ausfall)
|
||||
if daten is not None and isinstance(daten, erwartet):
|
||||
status = "ok"
|
||||
return daten
|
||||
|
||||
@@ -8,7 +8,7 @@ const emit = defineEmits(['schliessen'])
|
||||
|
||||
const wurzel = ref(null)
|
||||
const pos = ref(0)
|
||||
const fertig = ref(new Set()) // baustein-ids mit bestandener Abrufprüfung
|
||||
const fertig = ref(new Set()) // baustein-ids mit abgeschlossenem Abruf
|
||||
|
||||
// Flache Screen-Liste aus den Guide-Daten (alle Kapitel/Level in ord):
|
||||
// je Kapitel ein Intro, je Baustein Fundament + Abruf, am Ende ein Abschluss.
|
||||
@@ -58,6 +58,11 @@ function taste(e) {
|
||||
if (e.key === 'ArrowRight') vor()
|
||||
else if (e.key === 'ArrowLeft') zurueck()
|
||||
}
|
||||
async function markFertig(bausteinId) {
|
||||
if (fertig.value.has(bausteinId)) return
|
||||
fertig.value = new Set([...fertig.value, bausteinId])
|
||||
await api.bausteinFertig(props.topic, bausteinId).catch(() => {})
|
||||
}
|
||||
|
||||
// ── Abrufprüfung (Karteikarten, Loop wie Ueben.vue) ─────────────────────────────
|
||||
const stapel = ref([])
|
||||
@@ -67,7 +72,6 @@ const sende = ref(false)
|
||||
async function abrufLaden(bausteinId) {
|
||||
stapel.value = await api.bausteinKarten(props.topic, bausteinId)
|
||||
zeigeAntwort.value = false
|
||||
if (!stapel.value.length) markFertig(bausteinId) // keine Karten → sofort bestanden
|
||||
}
|
||||
async function antworten(richtig) {
|
||||
if (sende.value || !stapel.value.length) return
|
||||
@@ -80,15 +84,10 @@ async function antworten(richtig) {
|
||||
if (!stapel.value.length) markFertig(screen.value.baustein)
|
||||
} finally { sende.value = false }
|
||||
}
|
||||
async function markFertig(bausteinId) {
|
||||
if (fertig.value.has(bausteinId)) return
|
||||
fertig.value = new Set([...fertig.value, bausteinId])
|
||||
await api.bausteinFertig(props.topic, bausteinId).catch(() => {})
|
||||
}
|
||||
|
||||
// Beim Betreten eines Abruf-Screens die Karten des Bausteins laden.
|
||||
watch(screen, (s) => {
|
||||
if (s.typ === 'abruf' && !fertig.value.has(s.baustein)) abrufLaden(s.baustein)
|
||||
if (s.typ === 'abruf') abrufLaden(s.baustein)
|
||||
})
|
||||
|
||||
// ── Fullscreen + Resume ─────────────────────────────────────────────────────────
|
||||
@@ -101,7 +100,7 @@ onMounted(async () => {
|
||||
try { await wurzel.value?.requestFullscreen() } catch { /* CSS-Overlay reicht als Fallback */ }
|
||||
const stand = await api.lernstand(props.topic).catch(() => [])
|
||||
fertig.value = new Set(stand.filter(s => s.status === 'fertig').map(s => s.baustein_id))
|
||||
// Resume: zum Fundament des ersten noch nicht bestandenen Bausteins springen.
|
||||
// Resume: zum Fundament des ersten noch nicht erledigten Bausteins springen.
|
||||
const idx = screens.value.findIndex(s => s.typ === 'fundament' && !fertig.value.has(s.baustein))
|
||||
pos.value = idx >= 0 ? idx : screens.value.length - 1
|
||||
})
|
||||
@@ -149,12 +148,12 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="lm-rest">noch {{ stapel.length }} · Box {{ stapel[0].box }}</div>
|
||||
</div>
|
||||
<div v-else class="lm-bestanden">✓ Abruf bestanden — weiter mit →</div>
|
||||
<div v-else class="lm-bestanden">✓ Abruf durch — weiter mit →</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="lm-screen lm-ende">
|
||||
<h1>Fundament abgeschlossen 🎉</h1>
|
||||
<p class="lm-introtext">Schritt 2 (Fehlersuche) und Schritt 3 (Aufgaben) folgen.</p>
|
||||
<p class="lm-introtext">Schritt 2 (Anwendung) und Schritt 3 (eigene Aufgaben) folgen.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -366,6 +366,18 @@ def test_split_writer_delimited_und_fallback():
|
||||
assert guide._split_writer("nur text")[1] == "nur text"
|
||||
|
||||
|
||||
def test_jsonx_repariert_latex_escapes():
|
||||
# rohes LaTeX im JSON-String (\i = ungültiges Escape) → Reparatur parst
|
||||
assert jsonx.parse('[{"a": "$\\in \\Sigma$"}]') == [{"a": "$\\in \\Sigma$"}]
|
||||
# \u ohne 4 Hex-Ziffern (\underline) zählt als ungültig → repariert
|
||||
assert jsonx.parse('{"m": "\\underline{x}"}') == {"m": "\\underline{x}"}
|
||||
# gültige Escapes bleiben unberührt (Reparatur nur im Fehlerfall)
|
||||
assert jsonx.parse('{"x": "a\\nb"}') == {"x": "a\nb"}
|
||||
assert jsonx.parse('{"u": "\\u00e4"}') == {"u": "ä"}
|
||||
# echter Müll bleibt None
|
||||
assert jsonx.parse("kein json hier") is None
|
||||
|
||||
|
||||
def test_writer_saeubern_terminator():
|
||||
import guide
|
||||
# geleakter `=====`-Terminator (Setext-H1 unter Bullet) + </s> raus
|
||||
|
||||
Reference in New Issue
Block a user