Angshul's picture
Upload 10 files
7b4d34e verified
Raw History Blame Contribute Delete
4.68 kB
from __future__ import annotations
from typing import Any, TypedDict
from .core import DocumentRecord, ExtractiveGroundedGenerator, FuzzyEvidence, FuzzyIndex
class RAGState(TypedDict, total=False):
question: str
k: int
expanded: bool
evidence: list[dict[str, Any]]
confidence: float
margin: float
reason: str
route: str
answer: str
citations: list[str]
def _serialize_hits(hits: list[FuzzyEvidence]) -> list[dict[str, Any]]:
return [
{
"id": h.document.id,
"text": h.document.text,
"title": h.document.title,
"source": h.document.source,
"metadata": dict(h.document.metadata),
"raw_relevance": h.raw_relevance,
"fuzzy_score": h.fuzzy_score,
"authority": h.authority,
"freshness": h.freshness,
"reliability": h.reliability,
}
for h in hits
]
def _deserialize_hits(items: list[dict[str, Any]]) -> list[FuzzyEvidence]:
hits: list[FuzzyEvidence] = []
for x in items:
d = DocumentRecord(x["id"], x["text"], x.get("title", ""), x.get("source", ""), x.get("metadata", {}))
hits.append(FuzzyEvidence(d, float(x["raw_relevance"]), float(x["fuzzy_score"]), float(x["authority"]), float(x["freshness"]), float(x["reliability"]), {}))
return hits
class FuzzyRAGGraph:
"""LangGraph orchestration with fuzzy generate/expand/abstain routing."""
def __init__(self, index: FuzzyIndex, initial_k: int = 4, generator=None):
self.index = index
self.initial_k = initial_k
self.generator = ExtractiveGroundedGenerator()
self.external_generator = generator
self.graph = self._build()
def _build(self):
try:
from langgraph.graph import END, START, StateGraph
except Exception as exc: # pragma: no cover
raise RuntimeError("langgraph is required to build FuzzyRAGGraph") from exc
builder = StateGraph(RAGState)
builder.add_node("retrieve", self._retrieve)
builder.add_node("assess", self._assess)
builder.add_node("expand", self._expand)
builder.add_node("generate", self._generate)
builder.add_node("abstain", self._abstain)
builder.add_edge(START, "retrieve")
builder.add_edge("retrieve", "assess")
builder.add_conditional_edges(
"assess",
lambda s: s["route"],
{"generate": "generate", "expand": "expand", "abstain": "abstain"},
)
builder.add_edge("expand", "retrieve")
builder.add_edge("generate", END)
builder.add_edge("abstain", END)
return builder.compile()
def _retrieve(self, state: RAGState) -> dict[str, Any]:
k = int(state.get("k", self.initial_k))
pool = self.index.config.extended_pool if state.get("expanded", False) else self.index.config.candidate_pool
hits = self.index.search(state["question"], k=k, candidate_pool=pool)
return {"evidence": _serialize_hits(hits)}
def _assess(self, state: RAGState) -> dict[str, Any]:
hits = _deserialize_hits(state.get("evidence", []))
d = self.index.decision(hits)
route = d["route"]
# Only one retrieval expansion is permitted. If ambiguity survives the
# wider search, the graph abstains rather than cycling or bluffing.
if route == "expand" and state.get("expanded", False):
route = "abstain"
reason = "ambiguous_after_expansion"
else:
reason = d["reason"]
return {"route": route, "confidence": float(d["confidence"]), "margin": float(d["margin"]), "reason": reason}
def _expand(self, state: RAGState) -> dict[str, Any]:
return {"k": min(self.index.config.extended_pool, max(int(state.get("k", self.initial_k)) * 2, self.initial_k + 2)), "expanded": True}
def _generate(self, state: RAGState) -> dict[str, Any]:
hits = _deserialize_hits(state.get("evidence", []))
if self.external_generator is None:
answer, citations = self.generator.generate(state["question"], hits)
else:
answer = str(self.external_generator(state["question"], hits))
citations = [hits[0].document.id] if hits else []
return {"answer": answer, "citations": citations}
def _abstain(self, state: RAGState) -> dict[str, Any]:
return {"answer": self.generator.abstain_text, "citations": []}
def invoke(self, question: str) -> dict[str, Any]:
out = self.graph.invoke({"question": question, "k": self.initial_k, "expanded": False})
return out