"""Text chunking (≈600–900 tokens, overlap 120) that respects headings and paragraphs.""" from __future__ import annotations import re import unicodedata from dataclasses import dataclass, field CHARS_PER_TOKEN = 4 TARGET = 750 * CHARS_PER_TOKEN MAX = 900 * CHARS_PER_TOKEN OVERLAP = 120 * CHARS_PER_TOKEN @dataclass class Chunk: content: str module: str = "" section: str = "" page: str = "" url: str = "" metadata: dict = field(default_factory=dict) def split_paragraphs(text: str) -> list[str]: parts = re.split(r"\n\s*\n", text.strip()) return [p.strip() for p in parts if p.strip()] def chunk_text(text: str, **meta: str) -> list[Chunk]: """Greedy paragraph packing with overlap; oversize paragraphs are hard-split.""" paras = split_paragraphs(text) chunks: list[Chunk] = [] buf = "" for p in paras: if len(p) > MAX: if buf: chunks.append(Chunk(buf.strip(), **meta)) buf = "" for i in range(0, len(p), TARGET - OVERLAP): chunks.append(Chunk(p[i:i + TARGET].strip(), **meta)) continue if len(buf) + len(p) + 2 > TARGET and buf: chunks.append(Chunk(buf.strip(), **meta)) buf = buf[-OVERLAP:] + "\n\n" + p if OVERLAP else p else: buf = f"{buf}\n\n{p}" if buf else p if buf.strip(): chunks.append(Chunk(buf.strip(), **meta)) return [c for c in chunks if len(c.content) > 40] # ------------------------------------------------------------------ tokenisation (BM25) STOPWORDS = set(""" le la les l un une des du de d et ou où mais donc or ni car à au aux en dans par pour sur sous avec sans ce cet cette ces se sa son ses leur leurs mon ma mes ton ta tes notre nos votre vos qui que quoi dont il elle ils elles on nous vous je tu y ne pas plus moins très est sont été être avoir a ont fait faire peut peuvent doit doivent comme si the of and to in is are for """.split()) def strip_accents(s: str) -> str: return "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn") def tokenize(text: str) -> list[str]: text = strip_accents(text.lower()) words = re.findall(r"[a-z0-9]+", text) out = [] for w in words: if w in STOPWORDS or len(w) < 2: continue # light French stemming: plural / feminine endings for suf in ("ements", "ement", "tions", "tion", "ees", "es", "s", "x", "e"): if len(w) > 5 and w.endswith(suf): w = w[: -len(suf)] break out.append(w) return out