Spaces:
Running
Running
Claude
Fix synopsis extraction: read the Sphinx definition line, not the signature head
f1d3985 unverified | """Chunk snapshot pages by heading into OKF units. | |
| A chunk = one coherent doc section: its heading path, its prose, and any code | |
| blocks that live under it. API pages also carry their [source] GitHub link as | |
| metadata. Units are emitted as OKF files (YAML frontmatter + markdown body) β | |
| the human/agent-readable knowledge snapshot β which the embed step then loads | |
| into Neon. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| import yaml | |
| HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$", re.MULTILINE) | |
| GITHUB_SOURCE_RE = re.compile(r"https://github\.com/pytorch/[\w.-]+/blob/\S+") | |
| # Sphinx headerlinks survive HTMLβmarkdown as e.g. [ΒΆ](#sgd "Permalink...") β | |
| # they carry the TRUE anchor, and must not leak into titles/heading paths | |
| HEADERLINK_RE = re.compile(r"\[[^\]]*\]\(#([^)\s\"]+)[^)]*\)") | |
| # A section bigger than this is cut into parts at natural seams. Aligned with | |
| # index/embed.MAX_EMBED_CHARS: anything past it was invisible to the dense | |
| # vector anyway (embedded blind, silently). Each part inherits the section's | |
| # heading_path, so indexed_text() prepends the same symbol+heading "synopsis" | |
| # to every part β the serialized-story recap β and all parts share the | |
| # section's URL+anchor, so citations stay exact. | |
| CHUNK_TARGET_CHARS = 2000 | |
| _FENCE_RE = re.compile(r"```.*?```", re.DOTALL) | |
| class Section: | |
| heading_path: list[str] | |
| title: str | |
| text: str | |
| anchor: str = "" | |
| source_link: str = field(default="") | |
| def slugify(title: str) -> str: | |
| """Sphinx-style anchor slug: lowercase, alphanumerics, hyphens.""" | |
| return re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") | |
| def clean_heading(raw_title: str) -> tuple[str, str]: | |
| """(clean title, anchor): take the real Sphinx anchor from the headerlink | |
| when present, strip the link markup from the title, unescape markdown.""" | |
| match = HEADERLINK_RE.search(raw_title) | |
| title = HEADERLINK_RE.sub("", raw_title).replace("\\_", "_").strip() | |
| anchor = match.group(1) if match else slugify(title) | |
| return title, anchor | |
| def split_by_heading(markdown: str) -> list[Section]: | |
| """Split a page into sections at every heading; preamble becomes section 0.""" | |
| matches = list(HEADING_RE.finditer(markdown)) | |
| sections: list[Section] = [] | |
| stack: list[tuple[int, str]] = [] # (level, title) breadcrumbs | |
| preamble = markdown[: matches[0].start()].strip() if matches else markdown.strip() | |
| if preamble: | |
| sections.append(Section(heading_path=[], title="", text=preamble)) | |
| for i, match in enumerate(matches): | |
| level = len(match.group(1)) | |
| title, anchor = clean_heading(match.group(2).strip()) | |
| end = matches[i + 1].start() if i + 1 < len(matches) else len(markdown) | |
| text = markdown[match.end() : end].strip() | |
| while stack and stack[-1][0] >= level: | |
| stack.pop() | |
| stack.append((level, title)) | |
| source = GITHUB_SOURCE_RE.search(text) | |
| sections.append( | |
| Section( | |
| heading_path=[t for _, t in stack], | |
| title=title, | |
| text=text, | |
| anchor=anchor, | |
| source_link=source.group(0) if source else "", | |
| ) | |
| ) | |
| return sections | |
| def _atoms(text: str) -> list[str]: | |
| """Paragraphs of the text, with fenced code blocks kept whole. | |
| A naive split on blank lines would cut inside ``` fences (code often has | |
| blank lines); half a code block is noise to embed. Fences are atoms. | |
| """ | |
| atoms: list[str] = [] | |
| last = 0 | |
| for match in _FENCE_RE.finditer(text): | |
| atoms += [p for p in text[last : match.start()].split("\n\n") if p.strip()] | |
| atoms.append(match.group(0)) | |
| last = match.end() | |
| atoms += [p for p in text[last:].split("\n\n") if p.strip()] | |
| return atoms | |
| def _hard_split(atom: str, limit: int) -> list[str]: | |
| """Last resort for one atom bigger than the whole budget: cut at line ends.""" | |
| parts: list[str] = [] | |
| current = "" | |
| for line in atom.splitlines(keepends=True): | |
| while len(line) > limit: # a single enormous line β slice it | |
| parts.append(line[:limit]) | |
| line = line[limit:] | |
| if current and len(current) + len(line) > limit: | |
| parts.append(current) | |
| current = line | |
| else: | |
| current += line | |
| if current.strip(): | |
| parts.append(current) | |
| return [p.strip("\n") for p in parts if p.strip()] | |
| def split_oversized(text: str, limit: int | None = None) -> list[str]: | |
| """Cut oversized section text into parts at natural seams. | |
| Greedy paragraph packing: whole paragraphs (and whole code fences) are | |
| accumulated until the next one would overflow the limit; only an atom | |
| that alone exceeds the limit is cut inside (at line boundaries). | |
| """ | |
| if limit is None: | |
| limit = CHUNK_TARGET_CHARS | |
| if len(text) <= limit: | |
| return [text] | |
| pieces: list[str] = [] | |
| for atom in _atoms(text): | |
| pieces.extend(_hard_split(atom, limit) if len(atom) > limit else [atom]) | |
| parts: list[str] = [] | |
| current = "" | |
| for piece in pieces: | |
| if current and len(current) + 2 + len(piece) > limit: | |
| parts.append(current) | |
| current = piece | |
| else: | |
| current = f"{current}\n\n{piece}" if current else piece | |
| if current: | |
| parts.append(current) | |
| return parts | |
| def page_kind(url: str) -> str: | |
| if "/tutorials/" in url: | |
| return "tutorial" | |
| if "/generated/" in url or "/docs/" in url: | |
| return "api" | |
| return "guide" | |
| # a signature-shaped paragraph: `class torch.nn.Linear(...)`, `torch.add(...)` | |
| _SIGNATURE_START_RE = re.compile(r"^(class\s+|@)?[\w.]+\(") | |
| _SENTENCE_RE = re.compile(r"(.+?[.!?])(?:\s|$)") | |
| _MD_LINK_RE = re.compile(r"\[([^\]]*)\]\([^)]*\)") # [text](url) β text | |
| SYNOPSIS_MAX_CHARS = 240 | |
| def _plain(text: str) -> str: | |
| """Markdown β comparable prose: links keep their text, emphasis dropped.""" | |
| return " ".join(_MD_LINK_RE.sub(r"\1", text).replace("*", "").replace("\\_", "_").split()) | |
| def page_synopsis(body: str, limit: int = SYNOPSIS_MAX_CHARS) -> str: | |
| """The page's own first prose sentence β the docstring summary a human wrote. | |
| Reference pages are signature/parameter-shaped, so their embedding sits far | |
| from descriptive questions even though the description sentence ("This | |
| criterion computes the cross entropy loss between input logits and target") | |
| usually exists β buried. Extracting it deterministically (no LLM, no quota, | |
| no hallucination) lets indexed_text() lead every chunk of the page with it. | |
| Shape gotcha this handles: markdownify renders Sphinx's signature <dl> as | |
| ONE paragraph β `*class* torch.nn.X(...)[[source]](github)` and then the | |
| description on a `: ...` definition line. The description after the `:` | |
| marker is the synopsis; naively taking the paragraph head yields the | |
| signature + a github URL, which merely duplicates the chunk's own opening. | |
| """ | |
| text = _FENCE_RE.sub("", body) | |
| for para in text.split("\n\n"): | |
| # a definition-list description line (`: prose`) beats the paragraph | |
| # head β that's where Sphinx puts the docstring summary | |
| desc_lines = [ln.lstrip()[1:] for ln in para.splitlines() if ln.lstrip().startswith(": ")] | |
| flat = _plain(" ".join(desc_lines) if desc_lines else para) | |
| if not flat or flat.startswith("#"): | |
| continue | |
| if _SIGNATURE_START_RE.match(flat): | |
| continue | |
| if len(flat.split()) < 5: # crumbs like "Parameters" or a bare type | |
| continue | |
| match = _SENTENCE_RE.match(flat) | |
| return (match.group(1) if match else flat)[:limit] | |
| return "" | |
| def chunk_page(meta: dict, body: str) -> list[dict]: | |
| """One snapshot page β list of OKF-unit dicts (frontmatter fields + content). | |
| Oversized sections are cut into parts (split_oversized); every part keeps | |
| the section's heading_path (its embedded "synopsis") and URL+anchor (its | |
| citation), and carries a `part` ordinal that makes its identity unique. | |
| """ | |
| units = [] | |
| kind = page_kind(meta["url"]) | |
| # api pages only: tutorials/guides already retrieve well, and their first | |
| # sentence is prose anyway β the synopsis is the terse pages' rescue line | |
| synopsis = page_synopsis(body) if kind == "api" else "" | |
| for section in split_by_heading(body): | |
| if not section.text: | |
| continue | |
| for part, content in enumerate(split_oversized(section.text)): | |
| units.append( | |
| { | |
| "url": meta["url"], | |
| "anchor": section.anchor, | |
| "page_title": meta.get("title", ""), | |
| "heading_path": section.heading_path, | |
| "library": meta.get("library", ""), | |
| "kind": kind, | |
| "source_link": section.source_link, | |
| "content_hash": meta.get("content_hash", ""), | |
| "part": part, | |
| "synopsis": synopsis, | |
| "content": content, | |
| } | |
| ) | |
| return units | |
| def write_units(units: list[dict], out_dir: Path) -> list[Path]: | |
| """Write each unit as an OKF file: YAML frontmatter over the section body.""" | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| paths: list[Path] = [] | |
| for i, unit in enumerate(units): | |
| frontmatter = {k: v for k, v in unit.items() if k != "content"} | |
| # always suffix with the enumerate index: two sections on one page can | |
| # slugify to the same anchor (e.g. two "Parameters" headings) and would | |
| # otherwise clobber each other on disk, silently losing a section | |
| stem = slugify(f"{Path(unit['url']).stem}-{unit['anchor'] or 'sec'}-{i}") | |
| path = out_dir / f"{stem}.md" | |
| frontmatter_yaml = yaml.safe_dump(frontmatter, sort_keys=True) | |
| path.write_text(f"---\n{frontmatter_yaml}---\n\n{unit['content']}\n", encoding="utf-8") | |
| paths.append(path) | |
| return paths | |