This commit is contained in:
team3
2026-07-03 12:50:32 +02:00
parent 9754cbcfae
commit 91b0d00aa1
27 changed files with 203 additions and 580 deletions

View File

@@ -276,24 +276,6 @@ def _blocks_files(topic: str) -> dict:
}
def _all_slot_files(files: dict) -> list[Path]:
work_dir = files["arbeit"]
# Subblock/levels slots are dynamic per chunk — collect via glob.
dyn = (list(work_dir.glob("subblock-*")) + list(work_dir.glob("facts-*")) + list(work_dir.glob("level-*")) + list(work_dir.glob("relevance-*"))
+ list(work_dir.glob("question-pattern-*")) + list(work_dir.glob("outline-*")) + list(work_dir.glob("artifact-*"))
+ list(work_dir.glob("research-*")) + list(work_dir.glob("consolidation-*"))
+ list(work_dir.glob("clarification*")) + list(work_dir.glob("dedup-*"))
+ list(work_dir.glob("inventar-filter*"))
+ list(work_dir.glob("gruppierung-*")) + list(work_dir.glob("inventar-gruppierung*"))) if work_dir.is_dir() else []
return [
*files["research"], files["research_mapping"],
*(p for slots in files["selection"].values() for p in slots),
*files["mapping"].values(), files["ergaenzung"],
files["sub_roh"], files["sidecar"], files["question_pattern"],
files["facts"], files["outline"], files["artefakte"], *dyn,
]
def cancel_blocks(topic: str) -> bool:
if topic not in _blocks_progress:
return False
@@ -655,54 +637,6 @@ def _file_payload(path: Path):
return text if _parse_selection(text) else None
def _mapping_schema(data):
"""{"blocks": [str, ≥1], "rest": [str]} → (blocks, rest) · otherwise None."""
if not isinstance(data, dict):
return None
blocks = _str_list(data.get("blocks"))
rest = _str_list(data.get("rest"))
if not blocks or rest is None:
return None
return blocks, rest
def _sub_raw_schema(data):
"""{block title: [subblock, …]} → dict · otherwise None (intermediate state of block B)."""
if not isinstance(data, dict) or not data:
return None
out: dict[str, list[str]] = {}
for k, v in data.items():
subs = _str_list(v) if isinstance(v, list) else None
if not isinstance(k, str) or not k.strip() or not subs:
return None
out[k] = subs
return out
def _sidecar_schema(data):
"""{block title: [{title, level}, …]} → dict · otherwise None (sidecar with levels)."""
if not isinstance(data, dict) or not data:
return None
for v in data.values():
if not isinstance(v, list) or not v:
return None
for s in v:
if not isinstance(s, dict) or not str(s.get("title", "")).strip() or s.get("level") not in _LEVELS:
return None
return data
def _relevance_complete(data) -> bool:
"""Does every subblock in the sidecar carry a valid relevance (relevant/peripheral)?"""
if not isinstance(data, dict) or not data:
return False
return all(
isinstance(s, dict) and s.get("relevance") in ("relevant", "peripheral")
for v in data.values() if isinstance(v, list)
for s in v
)
def _question_pattern_chunk_schema(data) -> list[dict] | None:
@@ -725,12 +659,6 @@ def _question_pattern_chunk_schema(data) -> list[dict] | None:
return out or None
def _question_pattern_complete(topic: str) -> bool:
"""Does the question-pattern sidecar exist (build ran)? Individual empty blocks
fall back to live generation at exam time — so the file is enough."""
return isinstance(_json_file(question_pattern_path(topic)), dict)
def _read(p: Path) -> str:
return p.read_text(encoding="utf-8") if p.exists() else ""
@@ -1457,12 +1385,6 @@ def _facts_lines(fk: dict) -> str:
return "\n".join(z)
def _facts_complete(files: dict) -> bool:
"""Does the facts map exist (block done)? {block: {sub_norm: {...}}}."""
d = _json_file(files["facts"])
return isinstance(d, dict) and bool(d)
async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, instructions: str, ns: str = "", lbl: str = "", sources: list[str] | None = None) -> tuple | None:
"""Block: per sub extract source facts (find) → verify (check) → correct/discard (fix).
Extract-once grounding: the result feeds level/relevance/questions/guide.
@@ -2043,18 +1965,6 @@ def _crawl_index(folder) -> dict[str, str]:
async def _set_inventory(topic: str, record: str, status: str) -> None:
"""Write an inventory entry ('title — description') with status to the DB."""
title = _title(record)
norm = _norm_title(title)
if not norm:
return
split_parts = [t.strip() for t in record.split("")]
desc = split_parts[1] if len(split_parts) >= 2 else ""
await db.upsert_block(topic, norm, title, desc)
await db.set_block_status(topic, norm, status)
def _triage_rules(folder, pages: list[str]) -> tuple[list[str], list[str]]:
"""Deterministic content/noise filter (config.CRAWL_*). Substring match (lowercase) against
URL + filename. Order: keep > noise > min_chars > keep. → (content, noise)."""
@@ -2324,6 +2234,22 @@ _CANON_CATALOGUE = re.compile(
r'|kapitel|chapter|abschnitt|section)\s*\d+(?:\.\d+)*\b', re.I)
_PAREN_GROUP = re.compile(r'^\s*(.*?)\s*\(([^()]{2,60})\)\s*$')
def _title_variants(title: str) -> set[str]:
"""Acronym/expansion variants of a "X (Y)" title — normalized outer part and paren
content. „Satisfiability Problem (SAT)"{'satisfiability problem', 'sat'}: matched
against another card's norm/key this makes acronym↔expansion pairs dedup CANDIDATES
(measured: title cosine 'sat' vs the long form is 0.53, far below the floor).
Titles without exactly one trailing paren group → empty set."""
from textkit import _norm_title
m = _PAREN_GROUP.match(title or "")
if not m:
return set()
return {v for v in (_norm_title(m.group(1)), _norm_title(m.group(2))) if v}
def _canonical_key(title: str) -> str:
"""Order-independent canonical key of a title (scaffolding stripped, relation operators normalized).
Two titles with the same key denote the same entity with ~100% precision (ER blocking). Empty string
@@ -2622,12 +2548,6 @@ _GROUP_STANDALONE = re.compile(
# --- Outline (blocks artifact: chapter structure, only read by the guide) ---
def _outline_complete(files: dict) -> bool:
"""Is the outline present (chapter list exists)?"""
d = _json_file(files["outline"])
return isinstance(d, dict) and isinstance(d.get("chapters"), list) and bool(d.get("chapters"))
def _outline_review_schema(data, valid: set[int], n_chapters: int, n_blocks: int):
"""{"moves": {"<blocknr>": <chapter-idx>}} → {nr: idx} (may be {}) · None if broken/invalid.
A mass rewrite (more than a third of all blocks) is rejected — the reviewer's job is
@@ -2898,12 +2818,6 @@ _ARTEFACT_PROMPT = {"flashcard": "Artifact-Flashcard", "example": "Artifact-Exam
_ARTEFACT_STEP = {"flashcard": "Flashcards", "example": "Examples"}
def _artefacts_complete(files: dict) -> bool:
"""Artifact map present (all types generated)? Values may be empty (content-aware)."""
d = _json_file(files["artefakte"])
return isinstance(d, dict) and all(t in d for t in ARTEFACT_TYPES)
async def _artefacts_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str, ns: str = "", lbl: str = "") -> dict | None:
"""Generate learning artefacts per type from the stored facts — one generation pass
per type over chunks. Worked examples are verified against the facts (wrong ones discarded);
@@ -3009,20 +2923,6 @@ async def _artefacts_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
return outcome
async def _mirror_artefacts_db(topic: str, sidecar: dict, artefacts: dict) -> None:
"""Mirror artefacts into the DB. Flashcard/example per sub (sub_norm)."""
await db.delete_sub_artefakte(topic)
btitle_list = list(sidecar.keys())
for type in ARTEFACT_TYPES:
for e in artefacts.get(type, []):
bt = _match_sub(e.get("block", ""), btitle_list)
bnorm, sn = _norm_title(bt), _norm_title(e.get("subblock", ""))
if not bnorm or not sn:
continue
data = json.dumps({k: v for k, v in e.items() if k not in ("block", "subblock")}, ensure_ascii=False)
await db.put_sub_artifact(topic, bnorm, sn, type, data, bt, e.get("subblock", ""))
async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None:
"""Mirror the sidecar {block title: [{title, level, relevance}]} into the DB table subblocks."""
for btitle, subs in sidecar.items():
@@ -3042,24 +2942,6 @@ async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None:
facts=facts, status="consensus")
async def _mirror_question_pattern_db(topic: str, pattern: dict) -> None:
"""Mirror question patterns {block title: [{subblock, question}]} into the DB table question_pattern."""
await db.delete_question_pattern(topic)
for btitle, eintraege in pattern.items():
bnorm = _norm_title(btitle)
if not bnorm or not isinstance(eintraege, list):
continue
for e in eintraege:
if not isinstance(e, dict):
continue
sub = str(e.get("subblock", "")).strip()
sn = _norm_title(sub)
question = str(e.get("question", "")).strip()
if not (sn and question):
continue
await db.upsert_question_pattern(topic, bnorm, sn, btitle, sub, question)
async def generate_blocks(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER,