update
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user