Vineetiitg commited on
Commit
9cc0476
·
1 Parent(s): 8123d0b

feat: add LangGraph self-rag workflow for retrieval and answer validation

Browse files
Files changed (1) hide show
  1. app/graph/workflow.py +121 -0
app/graph/workflow.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import List, TypedDict
3
+ from langchain_core.prompts import PromptTemplate
4
+ from langchain_core.documents import Document
5
+ from langchain_ollama import ChatOllama
6
+ from langgraph.graph import START, END, StateGraph
7
+
8
+ from app.core.config import settings
9
+ from app.engine.retriever import get_reranked_retriever
10
+
11
+ class GraphState(TypedDict):
12
+ question: str
13
+ generation: str
14
+ documents: List[Document]
15
+ run_count: int
16
+
17
+ llm = ChatOllama(model=settings.OLLAMA_MODEL, temperature=0, base_url=settings.OLLAMA_BASE_URL)
18
+ llm_json = ChatOllama(model=settings.OLLAMA_MODEL, temperature=0, format="json", base_url=settings.OLLAMA_BASE_URL)
19
+
20
+ def retrieve(state: GraphState):
21
+ print("--- NODE: RETRIEVE DOCS ---")
22
+ question = state["question"]
23
+ run_count = state.get("run_count", 0)
24
+ retriever = get_reranked_retriever()
25
+ documents = retriever.invoke(question)
26
+ return {"documents": documents, "question": question, "run_count": run_count}
27
+
28
+ def grade_documents(state: GraphState):
29
+ print("--- NODE: GRADE DOCUMENT RELEVANCE ---")
30
+ question = state["question"]
31
+ documents = state.get("documents", [])
32
+
33
+ prompt = PromptTemplate(
34
+ template="""You are a strict grader assessing relevance of a retrieved document to a user question.
35
+ Document: \n\n {document} \n\n
36
+ Question: {question} \n
37
+ If the document contains keywords or semantic meaning related to the question, grade it as 'yes'. Otherwise, 'no'.
38
+ Provide a JSON with a single key 'score' and value 'yes' or 'no'.""",
39
+ input_variables=["question", "document"],
40
+ )
41
+ grader = prompt | llm_json
42
+
43
+ filtered_docs = []
44
+ for d in documents:
45
+ result = grader.invoke({"question": question, "document": d.page_content})
46
+ try:
47
+ grade = json.loads(result.content).get("score", "no")
48
+ except:
49
+ grade = "no"
50
+ if grade.lower() == "yes":
51
+ filtered_docs.append(d)
52
+
53
+ return {"documents": filtered_docs}
54
+
55
+ def generate(state: GraphState):
56
+ print("--- NODE: GENERATE ANSWER ---")
57
+ question = state["question"]
58
+ documents = state["documents"]
59
+ run_count = state.get("run_count", 0) + 1
60
+
61
+ context = "\n\n".join(doc.page_content for doc in documents)
62
+ prompt = PromptTemplate(
63
+ template="""You are a Support Docs Copilot. Use the retrieved context to answer the question concisely. If you don't know the answer, say "I don't know".
64
+ Question: {question}
65
+ Context: {context}
66
+ Answer:""",
67
+ input_variables=["question", "context"],
68
+ )
69
+ rag_chain = prompt | llm
70
+ generation = rag_chain.invoke({"context": context, "question": question})
71
+ return {"generation": generation.content, "run_count": run_count}
72
+
73
+ def decide_to_generate(state: GraphState):
74
+ if not state["documents"]:
75
+ print("--- ROUTE: ALL DOCS IRRELEVANT ---")
76
+ return "end"
77
+ print("--- ROUTE: RELEVANT DOCS FOUND ---")
78
+ return "generate"
79
+
80
+ def check_hallucinations(state: GraphState):
81
+ documents = state["documents"]
82
+ generation = state["generation"]
83
+ run_count = state["run_count"]
84
+
85
+ if run_count >= 3:
86
+ print("--- ROUTE: MAX RETRIES REACHED ---")
87
+ return "end"
88
+
89
+ context = "\n\n".join(doc.page_content for doc in documents)
90
+ prompt = PromptTemplate(
91
+ template="""You are evaluating whether a generated answer is fully grounded in the retrieved facts.
92
+ Facts: \n\n {context} \n\n
93
+ Answer: {generation} \n
94
+ If the answer is supported by the facts, return 'yes'. If it contains hallucinations, return 'no'.
95
+ Provide a JSON with a single key 'score' and value 'yes' or 'no'.""",
96
+ input_variables=["context", "generation"],
97
+ )
98
+ grader = prompt | llm_json
99
+
100
+ result = grader.invoke({"context": context, "generation": generation})
101
+ try:
102
+ grade = json.loads(result.content).get("score", "yes")
103
+ except:
104
+ grade = "yes"
105
+
106
+ if grade.lower() == "yes":
107
+ print("--- ROUTE: GROUNDED ---")
108
+ return "end"
109
+ print("--- ROUTE: HALLUCINATION DETECTED ---")
110
+ return "regenerate"
111
+
112
+ def compile_workflow():
113
+ workflow = StateGraph(GraphState)
114
+ workflow.add_node("retrieve", retrieve)
115
+ workflow.add_node("grade_documents", grade_documents)
116
+ workflow.add_node("generate", generate)
117
+ workflow.add_edge(START, "retrieve")
118
+ workflow.add_edge("retrieve", "grade_documents")
119
+ workflow.add_conditional_edges("grade_documents", decide_to_generate, {"generate": "generate", "end": END})
120
+ workflow.add_conditional_edges("generate", check_hallucinations, {"end": END, "regenerate": "generate"})
121
+ return workflow.compile()