Angshul's picture
Upload 21 files
198bd6d verified
Raw History Blame Contribute Delete
3.61 kB
from __future__ import annotations
from typing import Any, Literal, TypedDict
from .core import BayesianIndex, ExtractiveGroundedGenerator, RetrievalHit
class RAGState(TypedDict, total=False):
query: str
hits: list[RetrievalHit]
decision: Literal["generate", "abstain"]
answer: str
citations: list[str]
abstained: bool
class BayesianRAGGraph:
"""Actual LangGraph orchestration around the Bayesian retriever.
Graph topology:
START -> retrieve -> assess -> {generate | abstain} -> END
"""
def __init__(self, index: BayesianIndex, generator: Any | None = None, k: int = 4):
self.index = index
self.generator = generator or ExtractiveGroundedGenerator()
self.k = k
self.graph = self._build_graph()
def _retrieve(self, state: RAGState) -> dict[str, Any]:
return {"hits": self.index.search(state["query"], k=self.k)}
def _assess(self, state: RAGState) -> dict[str, Any]:
decision: Literal["generate", "abstain"] = "abstain" if self.index.should_abstain(state.get("hits", [])) else "generate"
return {"decision": decision}
@staticmethod
def _route(state: RAGState) -> str:
return state.get("decision", "abstain")
def _generate(self, state: RAGState) -> dict[str, Any]:
hits = state.get("hits", [])
if hasattr(self.generator, "generate"):
answer, citations = self.generator.generate(state["query"], hits)
else:
context = "\n".join(f"[{h.document.id}] {h.document.text}" for h in hits)
prompt = (
"Answer only from the supplied context. Cite document ids in square brackets. "
"If unsupported, say you do not know.\n\n"
f"Context:\n{context}\n\nQuestion: {state['query']}"
)
resp = self.generator.invoke(prompt)
answer = getattr(resp, "content", str(resp))
citations = [h.document.id for h in hits if f"[{h.document.id}]" in answer]
return {"answer": answer, "citations": citations, "abstained": False}
@staticmethod
def _abstain(state: RAGState) -> dict[str, Any]:
return {
"answer": ExtractiveGroundedGenerator.abstain_text,
"citations": [],
"abstained": True,
}
def _build_graph(self):
try:
from langgraph.graph import StateGraph, START, END
except ImportError as exc:
raise ImportError("bayesian-langgraph-rag requires langgraph>=1.2,<2.0") from exc
builder = StateGraph(RAGState)
builder.add_node("retrieve", self._retrieve)
builder.add_node("assess", self._assess)
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",
self._route,
{"generate": "generate", "abstain": "abstain"},
)
builder.add_edge("generate", END)
builder.add_edge("abstain", END)
return builder.compile()
def invoke(self, query: str) -> dict[str, Any]:
state = self.graph.invoke({"query": query})
hits = state.get("hits", [])
return {
"question": query,
"answer": state.get("answer", ExtractiveGroundedGenerator.abstain_text),
"citations": state.get("citations", []),
"abstained": state.get("abstained", True),
"hits": [h.as_dict() for h in hits],
}