Spaces:
Sleeping
Sleeping
| """ARIA RAG engine. | |
| Loads markdown knowledge files, chunks them by `##` heading (then by | |
| ~300-token windows with overlap), embeds with all-MiniLM-L6-v2, and serves | |
| cosine-similarity search via a FAISS inner-product index over normalized | |
| vectors. The index is built once at startup — the knowledge base is small, | |
| so this takes seconds. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| from dataclasses import dataclass | |
| KNOWLEDGE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "knowledge") | |
| EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2" | |
| CHUNK_WORDS = 220 # ~300 tokens | |
| OVERLAP_WORDS = 40 # ~50 tokens | |
| TOP_K = 4 | |
| class Chunk: | |
| source: str # filename, e.g. "genie.md" | |
| heading: str # nearest ## heading ("" for file preamble) | |
| text: str # chunk text with heading prepended | |
| def _split_sections(markdown: str) -> list[tuple[str, str]]: | |
| """Split a markdown document into (heading, body) pairs on `##` headings.""" | |
| parts = re.split(r"(?m)^##\s+", markdown) | |
| sections: list[tuple[str, str]] = [] | |
| preamble = parts[0].strip() | |
| if preamble: | |
| # Strip the top-level `# Title` line but keep any intro text. | |
| preamble_body = re.sub(r"(?m)^#\s+.*$", "", preamble).strip() | |
| title = "" | |
| m = re.search(r"(?m)^#\s+(.*)$", preamble) | |
| if m: | |
| title = m.group(1).strip() | |
| if preamble_body: | |
| sections.append((title, preamble_body)) | |
| for part in parts[1:]: | |
| lines = part.splitlines() | |
| heading = lines[0].strip() if lines else "" | |
| body = "\n".join(lines[1:]).strip() | |
| if body: | |
| sections.append((heading, body)) | |
| return sections | |
| def _windows(words: list[str], size: int, overlap: int): | |
| """Yield overlapping word windows of `size` with `overlap` words shared.""" | |
| step = max(size - overlap, 1) | |
| start = 0 | |
| while True: | |
| yield words[start:start + size] | |
| if start + size >= len(words): | |
| break | |
| start += step | |
| def load_chunks() -> list[Chunk]: | |
| """Read every .md file in knowledge/ and return its chunks.""" | |
| chunks: list[Chunk] = [] | |
| for name in sorted(os.listdir(KNOWLEDGE_DIR)): | |
| if not name.endswith(".md"): | |
| continue | |
| with open(os.path.join(KNOWLEDGE_DIR, name), encoding="utf-8") as f: | |
| text = f.read() | |
| for heading, body in _split_sections(text): | |
| words = body.split() | |
| for piece in _windows(words, CHUNK_WORDS, OVERLAP_WORDS): | |
| if not piece: | |
| continue | |
| chunk_text = " ".join(piece) | |
| if heading: | |
| chunk_text = f"{heading}\n{chunk_text}" | |
| chunks.append(Chunk(source=name, heading=heading, text=chunk_text)) | |
| return chunks | |
| class RagIndex: | |
| """In-memory FAISS index over the knowledge chunks (cosine similarity).""" | |
| def __init__(self) -> None: | |
| # Heavy imports kept local so chunking can be unit-tested without them. | |
| import faiss | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| self._np = np | |
| self.chunks = load_chunks() | |
| if not self.chunks: | |
| raise RuntimeError(f"No knowledge chunks found in {KNOWLEDGE_DIR}") | |
| self.model = SentenceTransformer(EMBED_MODEL, device="cpu") | |
| vectors = self.model.encode( | |
| [c.text for c in self.chunks], | |
| normalize_embeddings=True, | |
| show_progress_bar=False, | |
| ) | |
| vectors = np.asarray(vectors, dtype="float32") | |
| self.index = faiss.IndexFlatIP(vectors.shape[1]) # cosine via normalized IP | |
| self.index.add(vectors) | |
| def search(self, query: str, k: int = TOP_K) -> list[Chunk]: | |
| q = self.model.encode([query], normalize_embeddings=True) | |
| q = self._np.asarray(q, dtype="float32") | |
| _, idx = self.index.search(q, min(k, len(self.chunks))) | |
| return [self.chunks[i] for i in idx[0] if i != -1] | |