Spaces:
Running
Running
| import hashlib | |
| import re | |
| import time | |
| from typing import Optional | |
| import polars as pl | |
| from src.config import Config | |
| class Chunker: | |
| def __init__(self, config: Config): | |
| self.config = config | |
| def chunk(self, pages_df: pl.DataFrame) -> pl.DataFrame: | |
| if pages_df.is_empty(): | |
| return pl.DataFrame( | |
| schema={ | |
| "chunk_id": pl.Utf8, | |
| "url": pl.Utf8, | |
| "title": pl.Utf8, | |
| "heading_path": pl.Utf8, | |
| "text": pl.Utf8, | |
| "chunk_index": pl.Int32, | |
| "timestamp": pl.Float64, | |
| } | |
| ) | |
| all_chunks = [] | |
| for row in pages_df.iter_rows(named=True): | |
| markdown = row.get("markdown", "") or "" | |
| chunks = self._split_markdown(markdown) | |
| current_heading = row.get("title", "") | |
| for i, chunk_text in enumerate(chunks): | |
| if not chunk_text.strip(): | |
| continue | |
| chunk_id = hashlib.md5( | |
| f"{row['url']}_{i}_{chunk_text[:50]}".encode() | |
| ).hexdigest() | |
| all_chunks.append({ | |
| "chunk_id": chunk_id, | |
| "url": row["url"], | |
| "title": row.get("title", ""), | |
| "heading_path": current_heading, | |
| "text": chunk_text.strip(), | |
| "chunk_index": i, | |
| "timestamp": row.get("timestamp", time.time()), | |
| }) | |
| if not all_chunks: | |
| return pl.DataFrame( | |
| schema={ | |
| "chunk_id": pl.Utf8, | |
| "url": pl.Utf8, | |
| "title": pl.Utf8, | |
| "heading_path": pl.Utf8, | |
| "text": pl.Utf8, | |
| "chunk_index": pl.Int32, | |
| "timestamp": pl.Float64, | |
| } | |
| ) | |
| return pl.DataFrame(all_chunks) | |
| def _split_markdown(self, text: str) -> list[str]: | |
| if not text: | |
| return [] | |
| sections = re.split(r"(^#+\s+.*$)", text, flags=re.MULTILINE) | |
| merged_sections = [] | |
| current = "" | |
| for part in sections: | |
| if re.match(r"^#+\s+", part): | |
| if current.strip(): | |
| merged_sections.append(current.strip()) | |
| current = part | |
| else: | |
| current += part | |
| if current.strip(): | |
| merged_sections.append(current.strip()) | |
| chunks = [] | |
| for section in merged_sections: | |
| chunks.extend(self._split_section(section)) | |
| return chunks or [text] | |
| def _split_section(self, section: str) -> list[str]: | |
| if len(section) <= self.config.chunk_max_chars: | |
| return [section] | |
| paragraphs = re.split(r"\n\s*\n", section) | |
| chunks = [] | |
| current = "" | |
| for para in paragraphs: | |
| if len(current) + len(para) + 1 <= self.config.chunk_max_chars: | |
| current = (current + "\n\n" + para).strip() if current else para | |
| else: | |
| if current: | |
| chunks.append(current) | |
| current = para | |
| if len(current) > self.config.chunk_max_chars: | |
| for i in range(0, len(current), self.config.chunk_max_chars - self.config.chunk_overlap): | |
| sub = current[i: i + self.config.chunk_max_chars] | |
| if len(sub) > self.config.chunk_max_chars // 2: | |
| chunks.append(sub) | |
| if current: | |
| chunks.append(current) | |
| return chunks | |