diff --git a/app/core/chunk_payload.py b/app/core/chunk_payload.py index be5d3ae..d7ed9c2 100644 --- a/app/core/chunk_payload.py +++ b/app/core/chunk_payload.py @@ -1,19 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -app/core/chunk_payload.py — Mindnet V2 (compat) +app/core/chunk_payload.py — Mindnet V2 (compat) -Ziele (ohne Bruch zur lauffähigen v1-Basis): -- Akzeptiert `file_path=` (Alias zu path_arg) -- Verarbeitet Chunks sowohl als `dict` **als auch** als Objekt (z. B. Dataclass `Chunk`) -- Schreibt v1-kompatible Felder: - * `id` (Alias von `chunk_id` – **wichtig** für app/core/edges.py v1) - * `neighbors: {prev, next}` wird **berechnet** (Sequenz), falls nicht vorhanden -- Denormalisiert optional `tags` der Note auf Chunks -- Fügt Nummern-Aliase hinzu: `ord`, `chunk_num`, `Chunk_Nummer` +Ziele (unveränderte v1-Basis, weniger Duplikate): +- **Kanonicum:** `index` +- **Standard‑Alias (v2):** `ord` (abschaltbar über ENV MINDNET_CHUNK_INCLUDE_ORD=0) +- **Optionale Aliase:** gesteuert über ENV MINDNET_CHUNK_INDEX_ALIASES + (z. B. "chunk_num,Chunk_Nummer" oder "Chunk_Number"). Standard: kein zusätzlicher Alias. +- Verarbeitet Chunks als Dict **oder** Objekt (Dataclass) und setzt immer `id` (= `chunk_id`) +- Berechnet `neighbors.prev/next`, falls nicht vorhanden +- Denormalisiert Note‑`tags` auf Chunks +- Akzeptiert `file_path=` als Alias zu `path_arg` -Wichtig: -- `edge_defaults` gehören zur *Note* (Typ-Regeln), nicht pro Chunk. Werden hier **nicht** gespiegelt. +ENV: +- MINDNET_CHUNK_INCLUDE_ORD: "1" (Default) | "0" +- MINDNET_CHUNK_INDEX_ALIASES: CSV‑Liste zulässiger Namen: chunk_num,Chunk_Nummer,Chunk_Number + +Hinweis: `edge_defaults` sind Note‑Regeln (nicht pro Chunk). """ from __future__ import annotations @@ -30,12 +34,10 @@ from app.core.chunker import assemble_chunks def _as_dict(obj): if isinstance(obj, dict): return obj - # Objekt → (teilweise) Dict-Ansicht via Attribute d = {} for k in ("index","ord","chunk_index","text","window","id","chunk_id","neighbors","note_id","type","title"): if hasattr(obj, k): d[k] = getattr(obj, k) - # Fallback: bestehe nicht auf Vollständigkeit return d def _coalesce(*vals): @@ -64,7 +66,6 @@ def _iter_chunks(note: Dict[str, Any], chunk_profile: str, fulltext: str) -> Lis note_id = note.get("id") or (note.get("frontmatter") or {}).get("id") ntype = (note.get("frontmatter") or {}).get("type") or note.get("type") or "note" raw_list = assemble_chunks(note_id, fulltext, ntype) - # Normalisiere auf Dicts (unter Bewahrung vorhandener Keys) out: List[Dict[str, Any]] = [] for c in raw_list: out.append(_as_dict(c) if not isinstance(c, dict) else c) @@ -81,10 +82,6 @@ def make_chunk_payloads( note_text: Optional[str] = None, types_cfg: Optional[dict] = None, ) -> List[Dict[str, Any]]: - """ - Erzeugt Chunk-Payloads im v1-kompatiblen Format (plus V2-Aliase). - """ - # ---- Note-Kontext ---- n = note if isinstance(note, dict) else {"frontmatter": {}} fm = n.get("frontmatter") or {} note_type = str(fm.get("type") or n.get("type") or "note") @@ -104,7 +101,7 @@ def make_chunk_payloads( note_id = n.get("note_id") or n.get("id") or fm.get("id") title = n.get("title") or fm.get("title") or "" - # Pfadauflösung: file_path > note['path'] > path_arg + # Pfad (file_path > note['path'] > path_arg) path = file_path or n.get("path") or path_arg if isinstance(path, pathlib.Path): path = str(path) @@ -114,14 +111,17 @@ def make_chunk_payloads( tags = fm.get("tags") or fm.get("keywords") or n.get("tags") tags_list = _ensure_list(tags) if tags else [] - # ---- Chunks besorgen ---- + # Chunks holen fulltext = note_text if isinstance(note_text, str) else _text_from_note(n) raw_chunks = chunks_from_chunker if isinstance(chunks_from_chunker, list) else _iter_chunks(n, chunk_profile, fulltext) + include_ord = (os.environ.get("MINDNET_CHUNK_INCLUDE_ORD", "1") != "0") + alias_csv = os.environ.get("MINDNET_CHUNK_INDEX_ALIASES", "").strip() + extra_aliases = [a.strip() for a in alias_csv.split(",") if a.strip()] if alias_csv else [] + payloads: List[Dict[str, Any]] = [] for c in raw_chunks: cdict = c if isinstance(c, dict) else _as_dict(c) - # Index/Basisdaten robust ermitteln idx = _coalesce(cdict.get("index"), cdict.get("ord"), cdict.get("chunk_index"), len(payloads)) try: idx = int(idx) @@ -132,43 +132,43 @@ def make_chunk_payloads( if not isinstance(text, str): text = str(text or "") - # deterministische ID (kompatibel & stabil) + # deterministische ID key = f"{note_id}|{idx}" h = hashlib.sha1(key.encode("utf-8")).hexdigest()[:12] if note_id else hashlib.sha1(f"{path}|{idx}".encode("utf-8")).hexdigest()[:12] chunk_id = cdict.get("chunk_id") or cdict.get("id") or (f"{note_id}-{idx:03d}-{h}" if note_id else h) payload = { - # v1 Kernfelder (+Erweiterungen) - "id": chunk_id, # <— WICHTIG: v1 edges.py erwartet 'id' - "chunk_id": chunk_id, # v2-Alias - "index": idx, - "ord": idx, # v2-Alias - "chunk_num": idx, - "Chunk_Nummer": idx, + "id": chunk_id, # v1 erwartet 'id' + "chunk_id": chunk_id, + "index": idx, # Kanonisch "note_id": note_id, "type": note_type, "title": title, "path": path, "text": text, - "window": text, # falls der Chunker bereits ein Fenster liefert, bleibt es identisch + "window": text, "retriever_weight": retriever_weight, "chunk_profile": chunk_profile, } + if include_ord: + payload["ord"] = idx # v2‑Standard, abschaltbar + for alias in extra_aliases: + # nur whitelisted Namen zulassen + if alias in ("chunk_num","Chunk_Nummer","Chunk_Number"): + payload[alias] = idx - # Bestehende neighbors vom Chunk übernehmen (falls vorhanden) nb = cdict.get("neighbors") if isinstance(nb, dict): prev_id = nb.get("prev"); next_id = nb.get("next") payload["neighbors"] = {"prev": prev_id, "next": next_id} - # Tags spiegeln + if tags_list: payload["tags"] = tags_list - # JSON-Roundtrip als Validierung json.loads(json.dumps(payload, ensure_ascii=False)) payloads.append(payload) - # Nachgelagert: neighbors berechnen, falls fehlend + # neighbors berechnen, falls fehlend for i, p in enumerate(payloads): nb = p.get("neighbors") or {} prev_id = nb.get("prev")