akif07's picture
Clean initial commit without database or cache files
a7cba55
Raw
History Blame Contribute Delete
7.44 kB
"""
The Cyclic RAG Workflow (LangGraph), matching the reference diagram exactly:
1. Query Understanding -> 2. Retrieve -> 3. Generate -> 4. Critique & Evaluate
▲ │
└──── 5. Refine Query ◄── "No" ┘
│ "Yes"
Final Answer
`MemorySaver` gives each conversation (keyed by thread_id) durable short-term
memory across turns, matching the "MemorySaver / Conversation history" box.
"""
from __future__ import annotations
import os
import logging
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, StateGraph
from core.critique import critique_answer
from core.planner import refine_query, understand_query
from core.responder import generate_answer
from core.retriever import rerank, retrieve
from core.state import RAGState
logger = logging.getLogger("core.graph")
MAX_REFINE_ITERATIONS = int(os.getenv("MAX_REFINE_ITERATIONS", "2"))
def _history_as_text(state: RAGState) -> str:
messages = state.get("messages", [])
lines = []
for m in messages[-6:]: # last 3 turns
role = getattr(m, "type", getattr(m, "role", "user"))
content = getattr(m, "content", str(m))
lines.append(f"{role}: {content}")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Node 1: Query Understanding
# ---------------------------------------------------------------------------
def node_query_understanding(state: RAGState) -> dict:
history = _history_as_text(state)
standalone = understand_query(state["raw_query"], history)
logger.info("Node 1: Raw Query = %r | Standalone Query = %r", state["raw_query"], standalone)
return {
"standalone_query": standalone,
"refine_count": 0,
"max_refine_iterations": state.get("max_refine_iterations", MAX_REFINE_ITERATIONS),
"trace": state.get("trace", []) + [{"node": "query_understanding", "output": standalone}],
}
# ---------------------------------------------------------------------------
# Node 2: Retrieve (+ semantic re-rank -> true data vs noisy data)
# ---------------------------------------------------------------------------
def node_retrieve(state: RAGState) -> dict:
query = state["standalone_query"]
raw_hits = retrieve(query)
reranked = rerank(query, raw_hits)
logger.info("Node 2: Retrieve Query = %r | Raw Hits = %d | Kept After Rerank = %d", query, len(raw_hits), len(reranked))
return {
"retrieved_chunks": raw_hits,
"reranked_chunks": reranked,
"trace": state.get("trace", []) + [
{"node": "retrieve", "raw_hits": len(raw_hits), "kept_after_rerank": len(reranked)}
],
}
# ---------------------------------------------------------------------------
# Node 3: Generate
# ---------------------------------------------------------------------------
def node_generate(state: RAGState) -> dict:
answer = generate_answer(
state["standalone_query"],
state.get("reranked_chunks", []),
feedback=state.get("critique_feedback", ""),
)
logger.info("Node 3: Generated Draft Answer = %r", answer[:150] + "...")
return {
"draft_answer": answer,
"trace": state.get("trace", []) + [{"node": "generate", "answer_preview": answer[:200]}],
}
# ---------------------------------------------------------------------------
# Node 4: Critique & Evaluate
# ---------------------------------------------------------------------------
def node_critique(state: RAGState) -> dict:
result = critique_answer(
state["standalone_query"], state.get("reranked_chunks", []), state["draft_answer"]
)
logger.info("Node 4: Critique Result: is_satisfactory = %s | Feedback = %r", result.is_satisfactory, result.feedback)
return {
"is_satisfactory": result.is_satisfactory,
"critique_feedback": result.feedback,
"trace": state.get("trace", []) + [
{"node": "critique", "is_satisfactory": result.is_satisfactory, "feedback": result.feedback}
],
}
# ---------------------------------------------------------------------------
# Node 5: Refine Query
# ---------------------------------------------------------------------------
def node_refine_query(state: RAGState) -> dict:
refined = refine_query(state["standalone_query"], state.get("critique_feedback", ""))
logger.info("Node 5: Refine Query: original = %r | feedback = %r | refined = %r", state["standalone_query"], state.get("critique_feedback", ""), refined)
return {
"standalone_query": refined,
"refine_count": state.get("refine_count", 0) + 1,
"trace": state.get("trace", []) + [{"node": "refine_query", "refined_query": refined}],
}
# ---------------------------------------------------------------------------
# Terminal node: Final Answer
# ---------------------------------------------------------------------------
def node_final_answer(state: RAGState) -> dict:
logger.info("Node Final Answer: Output = %r", state["draft_answer"][:150] + "...")
return {
"final_answer": state["draft_answer"],
"messages": [{"role": "assistant", "content": state["draft_answer"]}],
}
def _route_after_critique(state: RAGState) -> str:
"""The 'Satisfactory Answer? Yes/No' decision diamond."""
if state.get("is_satisfactory"):
return "generate_final_answer"
if state.get("refine_count", 0) >= state.get("max_refine_iterations", MAX_REFINE_ITERATIONS):
# Stop looping even if not fully satisfactory — avoid infinite cycles / runaway cost.
return "generate_final_answer"
return "refine_query"
def build_graph():
graph = StateGraph(RAGState)
graph.add_node("query_understanding", node_query_understanding)
graph.add_node("retrieve", node_retrieve)
graph.add_node("generate", node_generate)
graph.add_node("critique", node_critique)
graph.add_node("refine_query", node_refine_query)
graph.add_node("generate_final_answer", node_final_answer)
graph.set_entry_point("query_understanding")
graph.add_edge("query_understanding", "retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", "critique")
graph.add_conditional_edges(
"critique",
_route_after_critique,
{"generate_final_answer": "generate_final_answer", "refine_query": "refine_query"},
)
graph.add_edge("refine_query", "retrieve") # cyclic loop back to retrieval
graph.add_edge("generate_final_answer", END)
checkpointer = MemorySaver()
return graph.compile(checkpointer=checkpointer)
_compiled_graph = None
def get_graph():
global _compiled_graph
if _compiled_graph is None:
_compiled_graph = build_graph()
return _compiled_graph
def run_query(raw_query: str, thread_id: str = "default") -> RAGState:
"""Convenience wrapper used by the API layer."""
graph = get_graph()
config = {"configurable": {"thread_id": thread_id}}
initial_state: RAGState = {
"raw_query": raw_query,
"messages": [{"role": "user", "content": raw_query}],
}
result = graph.invoke(initial_state, config=config)
return result