Download src/fuzzy_langgraph/core.py from Angshul/Fuzzy-LangGraph: direct link, hf CLI and curl.
- Browser
- Download file 13.3 kB
-
https://huggingface.co/Angshul/Fuzzy-LangGraph/resolve/main/src/fuzzy_langgraph/core.py
- Command line
-
hf download hf://Angshul/Fuzzy-LangGraph/src/fuzzy_langgraph/core.py
-
curl -L -o core.py https://huggingface.co/Angshul/Fuzzy-LangGraph/resolve/main/src/fuzzy_langgraph/core.py
13.3 kB
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Any, Iterable, Sequence | |
| import math | |
| import re | |
| import numpy as np | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| class DocumentRecord: | |
| id: str | |
| text: str | |
| title: str = "" | |
| source: str = "" | |
| metadata: dict[str, Any] = field(default_factory=dict) | |
| class FuzzyRetrievalConfig: | |
| candidate_pool: int = 10 | |
| answer_threshold: float = 0.60 | |
| ambiguity_margin: float = 0.015 | |
| min_raw_relevance: float = 0.14 | |
| expand_threshold: float = 0.48 | |
| extended_pool: int = 16 | |
| class FuzzyEvidence: | |
| document: DocumentRecord | |
| raw_relevance: float | |
| fuzzy_score: float | |
| authority: float | |
| freshness: float | |
| reliability: float | |
| memberships: dict[str, float] | |
| class FuzzyEvidenceController: | |
| """Small dependency-free Sugeno fuzzy controller for RAG evidence quality. | |
| Inputs are normalized to [0, 1]: lexical/semantic relevance, source authority, | |
| freshness, and source reliability. The rules intentionally make relevance a | |
| gate: highly trusted but irrelevant evidence must not become high confidence. | |
| """ | |
| def _clip(x: float) -> float: | |
| return max(0.0, min(1.0, float(x))) | |
| def _left(cls, x: float, full_until: float, zero_at: float) -> float: | |
| x = cls._clip(x) | |
| if x <= full_until: | |
| return 1.0 | |
| if x >= zero_at: | |
| return 0.0 | |
| return (zero_at - x) / (zero_at - full_until) | |
| def _right(cls, x: float, zero_until: float, full_at: float) -> float: | |
| x = cls._clip(x) | |
| if x <= zero_until: | |
| return 0.0 | |
| if x >= full_at: | |
| return 1.0 | |
| return (x - zero_until) / (full_at - zero_until) | |
| def _triangle(cls, x: float, left: float, peak: float, right: float) -> float: | |
| x = cls._clip(x) | |
| if x <= left or x >= right: | |
| return 0.0 | |
| if x == peak: | |
| return 1.0 | |
| if x < peak: | |
| return (x - left) / (peak - left) | |
| return (right - x) / (right - peak) | |
| def memberships(self, relevance: float, authority: float, freshness: float, reliability: float) -> dict[str, float]: | |
| r = self._clip(relevance) | |
| a = self._clip(authority) | |
| f = self._clip(freshness) | |
| q = self._clip(reliability) | |
| return { | |
| "rel_low": self._left(r, 0.05, 0.30), | |
| "rel_med": self._triangle(r, 0.12, 0.42, 0.70), | |
| "rel_high": self._right(r, 0.42, 0.72), | |
| "auth_low": self._left(a, 0.0, 0.55), | |
| "auth_med": self._triangle(a, 0.35, 0.62, 0.84), | |
| "auth_high": self._right(a, 0.68, 0.90), | |
| "fresh_low": self._left(f, 0.0, 0.42), | |
| "fresh_med": self._triangle(f, 0.28, 0.58, 0.82), | |
| "fresh_high": self._right(f, 0.68, 0.90), | |
| "reliability_low": self._left(q, 0.0, 0.50), | |
| "reliability_high": self._right(q, 0.58, 0.88), | |
| } | |
| def score(self, relevance: float, authority: float, freshness: float, reliability: float) -> tuple[float, dict[str, float]]: | |
| m = self.memberships(relevance, authority, freshness, reliability) | |
| # First-order Sugeno rules. Consequents are confidence-like evidence values. | |
| rules: list[tuple[float, float]] = [ | |
| (min(m["rel_high"], m["auth_high"], m["fresh_high"], m["reliability_high"]), 0.98), | |
| (min(m["rel_med"], m["auth_high"], m["fresh_high"], m["reliability_high"]), 0.90), | |
| (min(m["rel_high"], m["auth_high"]), 0.92), | |
| (min(m["rel_med"], m["auth_high"]), 0.84), | |
| (min(m["rel_high"], m["auth_med"], m["fresh_high"]), 0.74), | |
| (min(m["rel_med"], m["auth_med"]), 0.54), | |
| (min(m["rel_high"], m["fresh_low"]), 0.28), | |
| (min(m["rel_high"], m["auth_low"]), 0.24), | |
| (min(m["rel_med"], m["auth_low"]), 0.20), | |
| (min(m["rel_high"], m["reliability_low"]), 0.26), | |
| (m["rel_low"], 0.06), | |
| ] | |
| denom = sum(w for w, _ in rules) | |
| if denom <= 1e-12: | |
| # No fuzzy rule fired strongly; relevance still gates the fallback. | |
| base = 0.10 + 0.55 * self._clip(relevance) | |
| else: | |
| base = sum(w * z for w, z in rules) / denom | |
| # Relevance is already a fuzzy antecedent and is also enforced by the | |
| # hard minimum-relevance guard in the final decision. | |
| value = self._clip(base) | |
| return value, m | |
| class FuzzyIndex: | |
| def __init__(self, documents: Sequence[DocumentRecord] | None = None, config: FuzzyRetrievalConfig | None = None): | |
| self.config = config or FuzzyRetrievalConfig() | |
| self.controller = FuzzyEvidenceController() | |
| self.documents: list[DocumentRecord] = [] | |
| self._vectorizer: TfidfVectorizer | None = None | |
| self._matrix = None | |
| if documents: | |
| self.fit(documents) | |
| def _trust(doc: DocumentRecord) -> tuple[float, float, float]: | |
| md = doc.metadata or {} | |
| authority = float(md.get("authority", 0.50)) | |
| freshness = float(md.get("freshness", 0.50)) | |
| if "reliability" in md: | |
| reliability = float(md["reliability"]) | |
| elif "reliability_alpha" in md or "reliability_beta" in md: | |
| a = float(md.get("reliability_alpha", 1.0)) | |
| b = float(md.get("reliability_beta", 1.0)) | |
| reliability = a / max(a + b, 1e-12) | |
| else: | |
| reliability = 0.50 | |
| return tuple(max(0.0, min(1.0, x)) for x in (authority, freshness, reliability)) | |
| def fit(self, documents: Sequence[DocumentRecord]) -> "FuzzyIndex": | |
| self.documents = list(documents) | |
| corpus = [f"{d.title}. {d.text}".strip() for d in self.documents] | |
| self._vectorizer = TfidfVectorizer(ngram_range=(1, 2), stop_words="english", sublinear_tf=True) | |
| self._matrix = self._vectorizer.fit_transform(corpus) if corpus else None | |
| return self | |
| def add(self, documents: Iterable[DocumentRecord]) -> "FuzzyIndex": | |
| return self.fit([*self.documents, *list(documents)]) | |
| def search(self, query: str, k: int = 4, candidate_pool: int | None = None) -> list[FuzzyEvidence]: | |
| if not self.documents or self._vectorizer is None or self._matrix is None: | |
| return [] | |
| q = self._vectorizer.transform([query]) | |
| sims = cosine_similarity(q, self._matrix)[0] | |
| pool = min(candidate_pool or self.config.candidate_pool, len(self.documents)) | |
| candidate_ids = np.argsort(-sims, kind="stable")[:pool] | |
| hits: list[FuzzyEvidence] = [] | |
| for i in candidate_ids: | |
| doc = self.documents[int(i)] | |
| relevance = float(sims[int(i)]) | |
| authority, freshness, reliability = self._trust(doc) | |
| fuzzy, memberships = self.controller.score(relevance, authority, freshness, reliability) | |
| hits.append(FuzzyEvidence(doc, relevance, fuzzy, authority, freshness, reliability, memberships)) | |
| hits.sort(key=lambda h: (h.fuzzy_score, h.raw_relevance, h.authority, h.freshness), reverse=True) | |
| return hits[: max(1, k)] | |
| def decision(self, hits: Sequence[FuzzyEvidence]) -> dict[str, Any]: | |
| if not hits: | |
| return {"route": "abstain", "confidence": 0.0, "margin": 0.0, "reason": "no_evidence"} | |
| top = hits[0] | |
| second = hits[1] if len(hits) > 1 else None | |
| if second is None: | |
| margin = top.fuzzy_score | |
| else: | |
| # Fuzzy confidence can intentionally plateau for equally trustworthy | |
| # sources. A small relevance component breaks benign ties while exact | |
| # equal-authority/equal-relevance contradictions remain ambiguous. | |
| margin = 0.80 * (top.fuzzy_score - second.fuzzy_score) + 0.20 * (top.raw_relevance - second.raw_relevance) | |
| if top.raw_relevance < self.config.min_raw_relevance: | |
| return {"route": "abstain", "confidence": top.fuzzy_score, "margin": margin, "reason": "low_relevance"} | |
| trusted_enough = top.authority >= 0.60 or top.reliability >= 0.75 | |
| if top.fuzzy_score >= self.config.answer_threshold and margin >= self.config.ambiguity_margin and trusted_enough: | |
| return {"route": "generate", "confidence": top.fuzzy_score, "margin": margin, "reason": "strong_evidence"} | |
| if top.fuzzy_score >= self.config.expand_threshold: | |
| return {"route": "expand", "confidence": top.fuzzy_score, "margin": margin, "reason": "borderline_or_ambiguous"} | |
| return {"route": "abstain", "confidence": top.fuzzy_score, "margin": margin, "reason": "weak_evidence"} | |
| def should_abstain(self, hits: Sequence[FuzzyEvidence]) -> bool: | |
| return self.decision(hits)["route"] != "generate" | |
| class PlainTfidfIndex: | |
| """Baseline retriever intentionally unaware of source quality or uncertainty.""" | |
| def __init__(self, documents: Sequence[DocumentRecord]): | |
| self.documents = list(documents) | |
| corpus = [f"{d.title}. {d.text}".strip() for d in self.documents] | |
| self.vectorizer = TfidfVectorizer(ngram_range=(1, 2), stop_words="english", sublinear_tf=True) | |
| self.matrix = self.vectorizer.fit_transform(corpus) | |
| def search(self, query: str, k: int = 4) -> list[FuzzyEvidence]: | |
| q = self.vectorizer.transform([query]) | |
| sims = cosine_similarity(q, self.matrix)[0] | |
| ids = np.argsort(-sims, kind="stable")[: min(k, len(self.documents))] | |
| out = [] | |
| for i in ids: | |
| d = self.documents[int(i)] | |
| s = float(sims[int(i)]) | |
| out.append(FuzzyEvidence(d, s, s, 0.5, 0.5, 0.5, {})) | |
| return out | |
| class ExtractiveGroundedGenerator: | |
| """Offline answer synthesizer used for reproducible RAG evaluation. | |
| It selects the query-most-relevant sentence from the accepted evidence and | |
| returns it with an explicit citation. Production users can replace this with | |
| any LangChain Runnable/LLM without changing fuzzy retrieval or routing. | |
| """ | |
| abstain_text = "Insufficient reliable evidence to answer." | |
| def _sentences(text: str) -> list[str]: | |
| parts = re.split(r"(?<=[.!?])\s+", text.strip()) | |
| return [p.strip() for p in parts if p.strip()] | |
| def generate(self, question: str, hits: Sequence[FuzzyEvidence]) -> tuple[str, list[str]]: | |
| if not hits: | |
| return self.abstain_text, [] | |
| top = hits[0] | |
| sentences = self._sentences(top.document.text) or [top.document.text] | |
| if len(sentences) == 1: | |
| best = sentences[0] | |
| else: | |
| vec = TfidfVectorizer(ngram_range=(1, 2), stop_words="english") | |
| X = vec.fit_transform([question, *sentences]) | |
| scores = cosine_similarity(X[0:1], X[1:])[0] | |
| best = sentences[int(np.argmax(scores))] | |
| return f"{best} [{top.document.id}]", [top.document.id] | |
| class FuzzyRAGEngine: | |
| def __init__(self, documents: Sequence[DocumentRecord] | None = None, config: FuzzyRetrievalConfig | None = None): | |
| self.index = FuzzyIndex(documents or [], config) | |
| self.generator = ExtractiveGroundedGenerator() | |
| def ingest(self, documents: Sequence[DocumentRecord], replace: bool = True) -> int: | |
| if replace: | |
| self.index.fit(documents) | |
| else: | |
| self.index.add(documents) | |
| return len(self.index.documents) | |
| def query(self, question: str, k: int = 4) -> dict[str, Any]: | |
| hits = self.index.search(question, k=k) | |
| decision = self.index.decision(hits) | |
| expanded = False | |
| if decision["route"] == "expand": | |
| expanded = True | |
| hits = self.index.search(question, k=max(k * 2, 6), candidate_pool=self.index.config.extended_pool) | |
| decision = self.index.decision(hits) | |
| if decision["route"] == "expand": | |
| decision = {**decision, "route": "abstain", "reason": "ambiguous_after_expansion"} | |
| if decision["route"] == "generate": | |
| answer, citations = self.generator.generate(question, hits) | |
| else: | |
| answer, citations = self.generator.abstain_text, [] | |
| return { | |
| "question": question, | |
| "answer": answer, | |
| "citations": citations, | |
| "confidence": round(float(decision["confidence"]), 6), | |
| "margin": round(float(decision["margin"]), 6), | |
| "decision": decision["route"], | |
| "reason": decision["reason"], | |
| "expanded": expanded, | |
| "evidence": [ | |
| { | |
| "id": h.document.id, | |
| "title": h.document.title, | |
| "source": h.document.source, | |
| "raw_relevance": round(h.raw_relevance, 6), | |
| "fuzzy_score": round(h.fuzzy_score, 6), | |
| "authority": round(h.authority, 6), | |
| "freshness": round(h.freshness, 6), | |
| "reliability": round(h.reliability, 6), | |
| } | |
| for h in hits | |
| ], | |
| } | |