lvvignesh2122 commited on
Commit
be6b61f
·
1 Parent(s): 88cc76a

feat: Update RAG implementation to V2 with agentic graph and enhanced frontend

Browse files
Files changed (7) hide show
  1. .gitignore +2 -0
  2. agentic_rag_v2_graph.py +354 -0
  3. frontend/index.html +5 -2
  4. llm_utils.py +33 -0
  5. main.py +12 -29
  6. rag_store.py +16 -7
  7. verify_rag.py +43 -0
.gitignore CHANGED
@@ -19,3 +19,5 @@ data/
19
  # OS / editor
20
  .vscode/
21
  .DS_Store
 
 
 
19
  # OS / editor
20
  .vscode/
21
  .DS_Store
22
+ verify_log.txt
23
+ verify_out.txt
agentic_rag_v2_graph.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, List, Optional, Annotated
2
+ import google.generativeai as genai
3
+ from langgraph.graph import StateGraph, END
4
+ from langgraph.checkpoint.memory import MemorySaver
5
+ from langgraph.graph.message import add_messages
6
+ from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
7
+ import time
8
+ import random
9
+
10
+ from rag_store import search_knowledge
11
+ from eval_logger import log_eval
12
+ from llm_utils import generate_with_retry
13
+
14
+ MODEL_NAME = "gemini-2.5-flash"
15
+ MAX_RETRIES = 2
16
+
17
+
18
+
19
+
20
+ def format_history(messages: List[BaseMessage]) -> str:
21
+ history_str = ""
22
+ for msg in messages:
23
+ role = "User" if isinstance(msg, HumanMessage) else "Assistant"
24
+ history_str += f"{role}: {msg.content}\n"
25
+ return history_str
26
+
27
+ # ===============================
28
+ # STATE
29
+ # ===============================
30
+ class AgentState(TypedDict):
31
+ messages: Annotated[List[BaseMessage], add_messages]
32
+ query: str
33
+ refined_query: str
34
+ decision: str
35
+ retrieved_chunks: List[dict]
36
+ retrieval_quality: str
37
+ retries: int
38
+ answer: Optional[str]
39
+ confidence: float
40
+ answer_known: bool
41
+
42
+
43
+ # ===============================
44
+ # LLM DECISION NODE (PLANNER)
45
+ # ===============================
46
+ def llm_decision_node(state: AgentState) -> AgentState:
47
+ history = format_history(state.get("messages", []))
48
+ prompt = f"""
49
+ You are an AI agent deciding whether a question requires document retrieval.
50
+ Answer ONLY one word:
51
+ - use_rag
52
+ - no_rag
53
+
54
+ Conversation History:
55
+ {history}
56
+
57
+ Current Question:
58
+ {state["query"]}
59
+ """
60
+ model = genai.GenerativeModel(MODEL_NAME)
61
+ resp = generate_with_retry(model, prompt)
62
+
63
+ decision = "use_rag"
64
+ if resp and "no_rag" in resp.text.lower():
65
+ decision = "no_rag"
66
+
67
+ return {**state, "decision": decision}
68
+
69
+
70
+ # ===============================
71
+ # RETRIEVAL NODE (TOOL)
72
+ # ===============================
73
+ def retrieve_node(state: AgentState) -> AgentState:
74
+ q = state["refined_query"] or state["query"]
75
+ chunks = search_knowledge(q)
76
+ return {**state, "retrieved_chunks": chunks}
77
+
78
+
79
+ # ===============================
80
+ # GRADE DOCUMENTS NODE (GRADER)
81
+ # ===============================
82
+ def grade_documents_node(state: AgentState) -> AgentState:
83
+ """
84
+ Determines whether the retrieved documents are relevant to the question.
85
+ """
86
+ query = state["query"]
87
+ retrieved_docs = state["retrieved_chunks"]
88
+
89
+ filtered_docs = []
90
+ for doc in retrieved_docs:
91
+ prompt = f"""
92
+ You are a grader assessing relevance of a retrieved document to a user question.
93
+
94
+ Retrieved document:
95
+ {doc['text']}
96
+
97
+ User question:
98
+ {query}
99
+
100
+ If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant.
101
+ Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.
102
+
103
+ Answer ONLY 'yes' or 'no'.
104
+ """
105
+ model = genai.GenerativeModel(MODEL_NAME)
106
+ resp = generate_with_retry(model, prompt)
107
+ score = resp.text.strip().lower() if resp else "no"
108
+
109
+ if "yes" in score:
110
+ filtered_docs.append(doc)
111
+
112
+ return {**state, "retrieved_chunks": filtered_docs}
113
+
114
+
115
+ # ===============================
116
+ # RETRIEVAL EVALUATION (CRITIC)
117
+ # ===============================
118
+ def evaluate_retrieval_node(state: AgentState) -> AgentState:
119
+ if not state["retrieved_chunks"]:
120
+ return {**state, "retrieval_quality": "bad"}
121
+
122
+ context_sample = "\n".join(c["text"][:200] for c in state["retrieved_chunks"][:3])
123
+
124
+ prompt = f"""
125
+ Evaluate whether the following retrieved context is sufficient
126
+ to answer the question.
127
+
128
+ Answer ONLY one word:
129
+ - good
130
+ - bad
131
+
132
+ Question:
133
+ {state["query"]}
134
+
135
+ Context:
136
+ {context_sample}
137
+ """
138
+
139
+ model = genai.GenerativeModel(MODEL_NAME)
140
+ resp = generate_with_retry(model, prompt)
141
+
142
+ quality = "bad"
143
+ if resp and "good" in resp.text.lower():
144
+ quality = "good"
145
+
146
+ return {**state, "retrieval_quality": quality}
147
+
148
+
149
+ # ===============================
150
+ # QUERY REFINEMENT (SELF-CORRECTION)
151
+ # ===============================
152
+ def refine_query_node(state: AgentState) -> AgentState:
153
+ history = format_history(state.get("messages", []))
154
+ prompt = f"""
155
+ Rewrite the following question to improve document retrieval.
156
+ Be concise and factual.
157
+
158
+ Conversation History:
159
+ {history}
160
+
161
+ Original question:
162
+ {state["query"]}
163
+ """
164
+
165
+ model = genai.GenerativeModel(MODEL_NAME)
166
+ resp = generate_with_retry(model, prompt)
167
+
168
+ refined = resp.text.strip() if resp else state["query"]
169
+
170
+ return {
171
+ **state,
172
+ "refined_query": refined,
173
+ "retries": state["retries"] + 1
174
+ }
175
+
176
+
177
+ # ===============================
178
+ # ANSWER WITH RAG (HIGH CONF)
179
+ # ===============================
180
+ def answer_with_rag_node(state: AgentState) -> AgentState:
181
+ context = "\n\n".join(c["text"] for c in state["retrieved_chunks"])
182
+ history = format_history(state.get("messages", []))
183
+
184
+ prompt = f"""
185
+ Answer using ONLY the context below.
186
+ If the answer is not present, say "I don't know".
187
+
188
+ Context:
189
+ {context}
190
+
191
+ Conversation History:
192
+ {history}
193
+
194
+ Question:
195
+ {state["query"]}
196
+ """
197
+
198
+ model = genai.GenerativeModel(MODEL_NAME)
199
+ resp = generate_with_retry(model, prompt)
200
+ answer_text = resp.text if resp else "Error generating answer due to quota limits."
201
+
202
+ answer_known = "i don't know" not in answer_text.lower()
203
+ confidence = min(0.95, 0.6 + (0.1 * len(state["retrieved_chunks"])))
204
+
205
+ log_eval(
206
+ query=state["query"],
207
+ retrieved_count=len(state["retrieved_chunks"]),
208
+ confidence=confidence,
209
+ answer_known=answer_known
210
+ )
211
+
212
+ # Append interaction to memory
213
+ new_messages = [
214
+ HumanMessage(content=state["query"]),
215
+ AIMessage(content=answer_text)
216
+ ]
217
+
218
+ return {
219
+ **state,
220
+ "messages": new_messages,
221
+ "answer": answer_text,
222
+ "confidence": confidence,
223
+ "answer_known": answer_known
224
+ }
225
+
226
+
227
+ # ===============================
228
+ # ANSWER WITHOUT RAG
229
+ # ===============================
230
+ def answer_direct_node(state: AgentState) -> AgentState:
231
+ history = format_history(state.get("messages", []))
232
+ prompt = f"""
233
+ Conversation History:
234
+ {history}
235
+
236
+ Answer clearly and concisely:
237
+ {state['query']}
238
+ """
239
+
240
+ model = genai.GenerativeModel(MODEL_NAME)
241
+ resp = generate_with_retry(model, prompt)
242
+ answer_text = resp.text if resp else "Error generating answer due to quota limits."
243
+
244
+ log_eval(
245
+ query=state["query"],
246
+ retrieved_count=0,
247
+ confidence=0.4,
248
+ answer_known=True
249
+ )
250
+
251
+ # Append interaction to memory
252
+ new_messages = [
253
+ HumanMessage(content=state["query"]),
254
+ AIMessage(content=answer_text)
255
+ ]
256
+
257
+ return {
258
+ **state,
259
+ "messages": new_messages,
260
+ "answer": answer_text,
261
+ "confidence": 0.4,
262
+ "answer_known": True
263
+ }
264
+
265
+
266
+ # ===============================
267
+ # NO ANSWER
268
+ # ===============================
269
+ def no_answer_node(state: AgentState) -> AgentState:
270
+ log_eval(
271
+ query=state["query"],
272
+ retrieved_count=0,
273
+ confidence=0.0,
274
+ answer_known=False
275
+ )
276
+
277
+ answer_text = "I don't know based on the provided documents."
278
+
279
+ # Append interaction to memory
280
+ new_messages = [
281
+ HumanMessage(content=state["query"]),
282
+ AIMessage(content=answer_text)
283
+ ]
284
+
285
+ return {
286
+ **state,
287
+ "messages": new_messages,
288
+ "answer": answer_text,
289
+ "confidence": 0.0,
290
+ "answer_known": False
291
+ }
292
+
293
+
294
+ # ===============================
295
+ # GRAPH BUILDER
296
+ # ===============================
297
+ def build_agentic_rag_v2_graph():
298
+ graph = StateGraph(AgentState)
299
+ memory = MemorySaver()
300
+
301
+ graph.add_node("decide", llm_decision_node)
302
+ graph.add_node("retrieve", retrieve_node)
303
+ graph.add_node("grade", grade_documents_node)
304
+ graph.add_node("evaluate", evaluate_retrieval_node)
305
+ graph.add_node("refine", refine_query_node)
306
+ graph.add_node("answer_rag", answer_with_rag_node)
307
+ graph.add_node("answer_direct", answer_direct_node)
308
+ graph.add_node("no_answer", no_answer_node)
309
+
310
+ graph.set_entry_point("decide")
311
+
312
+ graph.add_conditional_edges(
313
+ "decide",
314
+ lambda s: s["decision"],
315
+ {
316
+ "use_rag": "retrieve",
317
+ "no_rag": "answer_direct"
318
+ }
319
+ )
320
+
321
+ graph.add_edge("retrieve", "grade")
322
+
323
+ def check_relevance(state):
324
+ if not state["retrieved_chunks"]:
325
+ if state["retries"] >= MAX_RETRIES:
326
+ return "no_answer"
327
+ return "rewrite"
328
+ return "evaluate"
329
+
330
+ graph.add_conditional_edges(
331
+ "grade",
332
+ check_relevance,
333
+ {
334
+ "rewrite": "refine",
335
+ "evaluate": "evaluate",
336
+ "no_answer": "no_answer"
337
+ }
338
+ )
339
+
340
+ graph.add_conditional_edges(
341
+ "evaluate",
342
+ lambda s: "retry" if s["retrieval_quality"] == "bad" and s["retries"] < MAX_RETRIES else "answer",
343
+ {
344
+ "retry": "refine",
345
+ "answer": "answer_rag"
346
+ }
347
+ )
348
+
349
+ graph.add_edge("refine", "retrieve")
350
+ graph.add_edge("answer_rag", END)
351
+ graph.add_edge("answer_direct", END)
352
+ graph.add_edge("no_answer", END)
353
+
354
+ return graph.compile(checkpointer=memory)
frontend/index.html CHANGED
@@ -264,6 +264,8 @@
264
 
265
  <script>
266
  const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
 
 
267
 
268
  async function upload() {
269
  const fileInput = document.getElementById("files");
@@ -375,9 +377,10 @@
375
  }
376
 
377
  function newChat() {
378
- document.getElementById("question").value = "";
379
  document.getElementById("answerBox").style.display = "none";
380
  document.getElementById("answerBox").innerHTML = "";
 
 
381
  }
382
 
383
  function clearHistory() {
@@ -402,7 +405,7 @@
402
  const res = await fetch("/ask", {
403
  method: "POST",
404
  headers: { "Content-Type": "application/json" },
405
- body: JSON.stringify({ prompt: q })
406
  });
407
 
408
  const data = await res.json();
 
264
 
265
  <script>
266
  const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
267
+ let threadId = sessionStorage.getItem("rag_thread_id") || crypto.randomUUID();
268
+ sessionStorage.setItem("rag_thread_id", threadId);
269
 
270
  async function upload() {
271
  const fileInput = document.getElementById("files");
 
377
  }
378
 
379
  function newChat() {
 
380
  document.getElementById("answerBox").style.display = "none";
381
  document.getElementById("answerBox").innerHTML = "";
382
+ threadId = crypto.randomUUID();
383
+ sessionStorage.setItem("rag_thread_id", threadId);
384
  }
385
 
386
  function clearHistory() {
 
405
  const res = await fetch("/ask", {
406
  method: "POST",
407
  headers: { "Content-Type": "application/json" },
408
+ body: JSON.stringify({ prompt: q, thread_id: threadId })
409
  });
410
 
411
  const data = await res.json();
llm_utils.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import random
3
+ import google.generativeai as genai
4
+ from google.api_core import exceptions
5
+
6
+ def generate_with_retry(model, prompt, retries=3, base_delay=2):
7
+ """
8
+ Generates content using the Gemini model with exponential backoff for rate limits.
9
+ """
10
+ for i in range(retries):
11
+ try:
12
+ return model.generate_content(prompt)
13
+ except Exception as e:
14
+ # Check for Rate Limit (429) or Quota Exceeded (ResourceExhausted)
15
+ is_quota_error = (
16
+ "429" in str(e)
17
+ or "quota" in str(e).lower()
18
+ or isinstance(e, exceptions.ResourceExhausted)
19
+ )
20
+
21
+ if is_quota_error:
22
+ if i < retries - 1:
23
+ sleep_time = base_delay * (2 ** i) + random.uniform(0, 1)
24
+ print(f"⚠️ Quota exceeded. Retrying in {sleep_time:.2f}s... (Attempt {i+1}/{retries})")
25
+ time.sleep(sleep_time)
26
+ continue
27
+ else:
28
+ print(f"❌ Quota exceeded after {retries} attempts.")
29
+ # We can re-raise or return None depending on preference.
30
+ # Re-raising allows the caller to handle the failure (e.g. return 503 Service Unavailable)
31
+ # identifying strictly as quota error might be useful.
32
+ raise e
33
+ return None
main.py CHANGED
@@ -11,6 +11,7 @@ import google.generativeai as genai
11
  from rag_store import ingest_documents, get_all_chunks, clear_database
12
  from analytics import get_analytics
13
  from agentic_rag_v2_graph import build_agentic_rag_v2_graph
 
14
 
15
  # =========================================================
16
  # ENV + MODEL
@@ -39,18 +40,7 @@ app.mount("/frontend", StaticFiles(directory="frontend"), name="frontend")
39
  # =========================================================
40
  # SECURITY
41
  # =========================================================
42
- from fastapi import Request, HTTPException, Depends
43
- from fastapi.security import APIKeyCookie
44
 
45
- ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "secret")
46
- COOKIE_NAME = "rag_auth"
47
-
48
- api_key_cookie = APIKeyCookie(name=COOKIE_NAME, auto_error=False)
49
-
50
- async def verify_admin(cookie: str = Depends(api_key_cookie)):
51
- if cookie != ADMIN_PASSWORD:
52
- raise HTTPException(status_code=401, detail="Unauthorized")
53
- return cookie
54
 
55
  # =========================================================
56
  # STATE
@@ -63,39 +53,28 @@ answer_cache: dict[str, tuple[float, dict]] = {}
63
  # =========================================================
64
  class PromptRequest(BaseModel):
65
  prompt: str
 
 
66
 
67
- class LoginRequest(BaseModel):
68
- password: str
69
 
70
  # =========================================================
71
  # ROUTES
72
  # =========================================================
73
- @app.post("/login")
74
- def login(data: LoginRequest):
75
- if data.password != ADMIN_PASSWORD:
76
- raise HTTPException(status_code=401, detail="Invalid password")
77
-
78
- response = JSONResponse(content={"message": "Logged in"})
79
- response.set_cookie(key=COOKIE_NAME, value=data.password, httponly=True)
80
- return response
81
 
82
- @app.get("/me")
83
- def me(user: str = Depends(verify_admin)):
84
- return {"status": "authenticated"}
85
 
86
  @app.get("/", response_class=HTMLResponse)
87
  def serve_ui():
88
  with open("frontend/index.html", "r", encoding="utf-8") as f:
89
  return f.read()
90
 
91
- @app.get("/analytics", dependencies=[Depends(verify_admin)])
92
  def analytics():
93
  return get_analytics()
94
 
95
  # ---------------------------------------------------------
96
  # UPLOAD
97
  # ---------------------------------------------------------
98
- @app.post("/upload", dependencies=[Depends(verify_admin)])
99
  async def upload(files: list[UploadFile] = File(...)):
100
  for file in files:
101
  ext = file.filename.split(".")[-1].lower()
@@ -144,12 +123,15 @@ async def ask(data: PromptRequest):
144
  context = "\n\n".join(c["text"] for c in chunks)
145
 
146
  model = genai.GenerativeModel(MODEL_NAME)
147
- resp = model.generate_content(
 
148
  f"Summarize the following content clearly:\n\n{context}"
149
  )
 
 
150
 
151
  response = {
152
- "answer": resp.text,
153
  "confidence": 0.95,
154
  "citations": []
155
  }
@@ -161,6 +143,7 @@ async def ask(data: PromptRequest):
161
  # 🟩 AGENTIC RAG (LLM + EVALUATION)
162
  # ==========================
163
  result = agentic_graph.invoke({
 
164
  "query": query,
165
  "refined_query": "",
166
  "decision": "",
@@ -170,7 +153,7 @@ async def ask(data: PromptRequest):
170
  "answer": None,
171
  "confidence": 0.0,
172
  "answer_known": False
173
- })
174
 
175
  response = {
176
  "answer": result["answer"],
 
11
  from rag_store import ingest_documents, get_all_chunks, clear_database
12
  from analytics import get_analytics
13
  from agentic_rag_v2_graph import build_agentic_rag_v2_graph
14
+ from llm_utils import generate_with_retry
15
 
16
  # =========================================================
17
  # ENV + MODEL
 
40
  # =========================================================
41
  # SECURITY
42
  # =========================================================
 
 
43
 
 
 
 
 
 
 
 
 
 
44
 
45
  # =========================================================
46
  # STATE
 
53
  # =========================================================
54
  class PromptRequest(BaseModel):
55
  prompt: str
56
+ thread_id: str = "default"
57
+
58
 
 
 
59
 
60
  # =========================================================
61
  # ROUTES
62
  # =========================================================
 
 
 
 
 
 
 
 
63
 
 
 
 
64
 
65
  @app.get("/", response_class=HTMLResponse)
66
  def serve_ui():
67
  with open("frontend/index.html", "r", encoding="utf-8") as f:
68
  return f.read()
69
 
70
+ @app.get("/analytics")
71
  def analytics():
72
  return get_analytics()
73
 
74
  # ---------------------------------------------------------
75
  # UPLOAD
76
  # ---------------------------------------------------------
77
+ @app.post("/upload")
78
  async def upload(files: list[UploadFile] = File(...)):
79
  for file in files:
80
  ext = file.filename.split(".")[-1].lower()
 
123
  context = "\n\n".join(c["text"] for c in chunks)
124
 
125
  model = genai.GenerativeModel(MODEL_NAME)
126
+ resp = generate_with_retry(
127
+ model,
128
  f"Summarize the following content clearly:\n\n{context}"
129
  )
130
+
131
+ answer_text = resp.text if resp else "Error generating summary due to quota limits."
132
 
133
  response = {
134
+ "answer": answer_text,
135
  "confidence": 0.95,
136
  "citations": []
137
  }
 
143
  # 🟩 AGENTIC RAG (LLM + EVALUATION)
144
  # ==========================
145
  result = agentic_graph.invoke({
146
+ "messages": [],
147
  "query": query,
148
  "refined_query": "",
149
  "decision": "",
 
153
  "answer": None,
154
  "confidence": 0.0,
155
  "answer_known": False
156
+ }, config={"configurable": {"thread_id": data.thread_id}})
157
 
158
  response = {
159
  "answer": result["answer"],
rag_store.py CHANGED
@@ -90,13 +90,22 @@ def ingest_documents(files):
90
 
91
  for file in files:
92
  if file.filename.endswith(".pdf"):
93
- reader = PdfReader(file.file)
94
- for i, page in enumerate(reader.pages):
95
- text = page.extract_text()
96
- if text:
97
- for chunk in chunk_text(text):
98
- texts.append(chunk)
99
- meta.append({"source": file.filename, "page": i + 1})
 
 
 
 
 
 
 
 
 
100
 
101
  elif file.filename.endswith(".txt"):
102
  content = file.file.read().decode("utf-8", errors="ignore")
 
90
 
91
  for file in files:
92
  if file.filename.endswith(".pdf"):
93
+ # Save temp file for pymupdf4llm
94
+ import tempfile
95
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
96
+ tmp.write(file.file.read())
97
+ tmp_path = tmp.name
98
+
99
+ try:
100
+ # Use pymupdf4llm to extract markdown with tables
101
+ import pymupdf4llm
102
+ md_text = pymupdf4llm.to_markdown(tmp_path)
103
+
104
+ for chunk in chunk_text(md_text):
105
+ texts.append(chunk)
106
+ meta.append({"source": file.filename, "page": "N/A"}) # pymupdf4llm merges pages by default
107
+ finally:
108
+ os.remove(tmp_path)
109
 
110
  elif file.filename.endswith(".txt"):
111
  content = file.file.read().decode("utf-8", errors="ignore")
verify_rag.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from agentic_rag_v2_graph import build_agentic_rag_v2_graph
3
+
4
+ async def main():
5
+ graph = build_agentic_rag_v2_graph()
6
+ thread_id = "test-thread-1"
7
+ config = {"configurable": {"thread_id": thread_id}}
8
+
9
+ print("--- Turn 1 ---")
10
+ inputs = {
11
+ "messages": [], # Initialize
12
+ "query": "My name is Alice.",
13
+ "refined_query": "",
14
+ "decision": "",
15
+ "retrieved_chunks": [],
16
+ "retrieval_quality": "",
17
+ "retries": 0,
18
+ "answer": None,
19
+ "confidence": 0.0,
20
+ "answer_known": False
21
+ }
22
+
23
+ result = await graph.ainvoke(inputs, config=config)
24
+ print(f"Answer 1: {result['answer']}")
25
+
26
+ print("\n--- Turn 2 ---")
27
+ inputs["query"] = "What is my name?"
28
+ # We don't need to pass 'messages' again as it should be loaded from memory,
29
+ # but the graph definition expects it in TypedDict.
30
+ # We can pass empty list, it will be merged/ignored depending on implementation?
31
+ # Actually, MemorySaver loads the state. The input 'messages' is merged.
32
+ # Since we defined 'add_messages', passing empty list is fine (no new messages to add yet).
33
+ inputs["messages"] = []
34
+
35
+ result = await graph.ainvoke(inputs, config=config)
36
+ print(f"Answer 2: {result['answer']}")
37
+
38
+ if __name__ == "__main__":
39
+ try:
40
+ asyncio.run(main())
41
+ except Exception as e:
42
+ import traceback
43
+ traceback.print_exc()