Text
Text processing helpers with explicit, inspectable behavior.
"""Text processing helpers with explicit, inspectable behavior."""
import re
from typing import Dict, Iterable, List, Mapping, Sequence, Set, Tuple
from .models import Chunk, Document
TOKEN_RE = re.compile(r"[a-z0-9]+(?:[-'][a-z0-9]+)?", re.IGNORECASE)
SENTENCE_RE = re.compile(r"(?<=[.!?])\s+")
STOPWORDS: Set[str] = {
"a",
"an",
"and",
"are",
"as",
"at",
"be",
"between",
"by",
"did",
"do",
"does",
"for",
"from",
"how",
"in",
"into",
"is",
"it",
"of",
"on",
"or",
"original",
"that",
"the",
"their",
"this",
"to",
"was",
"were",
"what",
"when",
"which",
"who",
"with",
}
DEFAULT_SYNONYM_GROUPS: Tuple[Tuple[str, ...], ...] = (
("retrieve", "retrieval", "retriever", "search", "lookup", "fetch"),
("generate", "generation", "generator", "decode", "decoder"),
("document", "documents", "passage", "passages", "chunk", "chunks", "context"),
("graph", "network", "linked", "relation", "relations"),
("question", "query", "queries"),
("evaluate", "evaluation", "metric", "metrics", "benchmark"),
("adaptive", "dynamic", "conditional", "route", "routing"),
("combine", "fusion", "fuse", "aggregate", "aggregation"),
)
def tokenize(text: str) -> List[str]:
"""Lowercase alphanumeric tokenization used by every reference component."""
return [match.group(0).lower() for match in TOKEN_RE.finditer(text)]
def content_terms(text: str) -> List[str]:
"""Tokens after removing a small, visible stop-word list."""
return [token for token in tokenize(text) if token not in STOPWORDS]
def split_sentences(text: str) -> List[str]:
"""Split prose into non-empty sentences without an external NLP model."""
return [part.strip() for part in SENTENCE_RE.split(text.strip()) if part.strip()]
def synonym_map(groups: Sequence[Sequence[str]] = DEFAULT_SYNONYM_GROUPS) -> Dict[str, str]:
"""Map synonyms to the first term of each declared group."""
mapping: Dict[str, str] = {}
for group in groups:
if not group:
continue
canonical = group[0].lower()
for term in group:
mapping[term.lower()] = canonical
return mapping
def semantic_terms(text: str, mapping: Mapping[str, str]) -> List[str]:
"""Canonicalized terms for the dependency-free semantic proxy."""
return [mapping.get(token, token) for token in content_terms(text)]
def chunk_document(document: Document, chunk_size: int = 120, overlap: int = 20) -> List[Chunk]:
"""Split a document into token windows.
Token offsets, source identity, and metadata are preserved so that a final
citation can be traced back to the original document. The function uses
whitespace reconstruction for portability; production systems should keep
exact character offsets as well.
"""
if chunk_size <= 0:
raise ValueError("chunk_size must be positive")
if overlap < 0 or overlap >= chunk_size:
raise ValueError("overlap must satisfy 0 <= overlap < chunk_size")
matches = list(TOKEN_RE.finditer(document.text))
if not matches:
return []
chunks: List[Chunk] = []
step = chunk_size - overlap
for start in range(0, len(matches), step):
end = min(start + chunk_size, len(matches))
chunk_id = f"{document.id}::c{len(chunks):03d}"
char_start = matches[start].start()
char_end = matches[end - 1].end()
chunks.append(
Chunk(
id=chunk_id,
document_id=document.id,
text=document.text[char_start:char_end],
start_token=start,
end_token=end,
title=document.title,
source=document.source,
metadata=document.metadata,
)
)
if end == len(matches):
break
return chunks
def chunk_documents(
documents: Iterable[Document], chunk_size: int = 120, overlap: int = 20
) -> List[Chunk]:
"""Chunk a document collection in stable input order."""
chunks: List[Chunk] = []
for document in documents:
chunks.extend(chunk_document(document, chunk_size=chunk_size, overlap=overlap))
return chunks
def jaccard(left: Iterable[str], right: Iterable[str]) -> float:
"""Set Jaccard similarity with well-defined empty-set behavior."""
left_set, right_set = set(left), set(right)
union = left_set | right_set
if not union:
return 1.0
return len(left_set & right_set) / len(union)
def ordered_unique(values: Iterable[str]) -> Tuple[str, ...]:
"""Return unique values while retaining first occurrence order."""
seen: Set[str] = set()
output: List[str] = []
for value in values:
if value not in seen:
output.append(value)
seen.add(value)
return tuple(output)