lvvignesh2122 commited on
Commit
58a1fee
Β·
1 Parent(s): 4e0f514

Implement Hybrid Agent architecture: Gemini 2.5 Lite (Router) + Gemini 3 Flash (Responder)

Browse files
agentic_rag_v2_graph.py CHANGED
@@ -14,7 +14,8 @@ from llm_utils import generate_with_retry
14
  from sql_db import query_database
15
 
16
  # Config
17
- MODEL_NAME = "gemini-2.5-flash"
 
18
  MAX_RETRIES = 2
19
 
20
  # ===============================
@@ -79,7 +80,7 @@ def text_to_sql_tool(query: str):
79
  - Output ONLY the SQL query. No markdown.
80
  - Do NOT use Markdown formatting.
81
  """
82
- model = genai.GenerativeModel(MODEL_NAME)
83
  resp = generate_with_retry(model, prompt)
84
  sql_query = resp.text.strip().replace("```sql", "").replace("```", "").strip() if resp else ""
85
 
@@ -121,7 +122,7 @@ def supervisor_node(state: AgentState):
121
  # Heuristic: If we already searched SQL and got results, maybe go to responder or PDF
122
  # But for now, let LLM decide based on history.
123
 
124
- model = genai.GenerativeModel(MODEL_NAME)
125
  resp = generate_with_retry(model, prompt)
126
  decision = resp.text.strip().lower() if resp else "responder"
127
 
@@ -153,7 +154,38 @@ def researcher_sql_node(state: AgentState):
153
  current_outputs = state.get("tool_outputs", []) + results
154
  return {**state, "tool_outputs": current_outputs}
155
 
156
- # ... (Verifier is unchanged) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  # 6. RESPONDER
159
  def responder_node(state: AgentState):
@@ -183,7 +215,7 @@ def responder_node(state: AgentState):
183
  Answer the user query. If you used SQL, summarize the data insights.
184
  """
185
 
186
- model = genai.GenerativeModel(MODEL_NAME)
187
  resp = generate_with_retry(model, prompt)
188
  answer = resp.text if resp else "I could not generate an answer."
189
 
 
14
  from sql_db import query_database
15
 
16
  # Config
17
+ MODEL_FAST = "gemini-2.5-flash-lite"
18
+ MODEL_SMART = "gemini-3-flash-preview"
19
  MAX_RETRIES = 2
20
 
21
  # ===============================
 
80
  - Output ONLY the SQL query. No markdown.
81
  - Do NOT use Markdown formatting.
82
  """
83
+ model = genai.GenerativeModel(MODEL_SMART)
84
  resp = generate_with_retry(model, prompt)
85
  sql_query = resp.text.strip().replace("```sql", "").replace("```", "").strip() if resp else ""
86
 
 
122
  # Heuristic: If we already searched SQL and got results, maybe go to responder or PDF
123
  # But for now, let LLM decide based on history.
124
 
125
+ model = genai.GenerativeModel(MODEL_FAST)
126
  resp = generate_with_retry(model, prompt)
127
  decision = resp.text.strip().lower() if resp else "responder"
128
 
 
154
  current_outputs = state.get("tool_outputs", []) + results
155
  return {**state, "tool_outputs": current_outputs}
156
 
157
+ # 5. VERIFIER
158
+ def verifier_node(state: AgentState):
159
+ """Verifies the quality of gathered information."""
160
+ query = state["query"]
161
+ tools_out = state.get("tool_outputs", [])
162
+
163
+ # Simple verification logic
164
+ context = ""
165
+ for t in tools_out:
166
+ context += f"\n[{t['source'].upper()}]: {t['content']}..."
167
+
168
+ prompt = f"""
169
+ You are a Verifier Agent.
170
+ User Query: "{query}"
171
+
172
+ Gathered Info:
173
+ {context}
174
+
175
+ Task:
176
+ Analyze the gathered information.
177
+ - Is it relevant to the query?
178
+ - Are there conflicts?
179
+ - What key details are present?
180
+
181
+ Provide concise verification notes for the Final Responder.
182
+ """
183
+
184
+ model = genai.GenerativeModel(MODEL_SMART)
185
+ resp = generate_with_retry(model, prompt)
186
+ notes = resp.text if resp else "Verification completed."
187
+
188
+ return {**state, "verification_notes": notes}
189
 
190
  # 6. RESPONDER
191
  def responder_node(state: AgentState):
 
215
  Answer the user query. If you used SQL, summarize the data insights.
216
  """
217
 
218
+ model = genai.GenerativeModel(MODEL_SMART)
219
  resp = generate_with_retry(model, prompt)
220
  answer = resp.text if resp else "I could not generate an answer."
221
 
llm_utils.py CHANGED
@@ -11,7 +11,7 @@ class DummyResponse:
11
  def text(self):
12
  return self._text
13
 
14
- def generate_with_retry(model, prompt, retries=3, base_delay=2):
15
  """
16
  Generates content using the Gemini model with exponential backoff for rate limits.
17
  Returns a dummy response if all retries fail, preventing app crashes.
 
11
  def text(self):
12
  return self._text
13
 
14
+ def generate_with_retry(model, prompt, retries=5, base_delay=4):
15
  """
16
  Generates content using the Gemini model with exponential backoff for rate limits.
17
  Returns a dummy response if all retries fail, preventing app crashes.
main.py CHANGED
@@ -21,7 +21,7 @@ load_dotenv()
21
  genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
22
 
23
  MOCK_MODE = False # Refactor complete - enabling real agent
24
- MODEL_NAME = "gemini-2.5-flash"
25
  MAX_FILE_SIZE = 50 * 1024 * 1024
26
  CACHE_TTL = 300
27
 
@@ -79,8 +79,12 @@ def analytics():
79
  @app.post("/upload")
80
  async def upload(files: list[UploadFile] = File(...)):
81
  for file in files:
82
- ext = file.filename.split(".")[-1].lower()
 
 
 
83
  if ext not in ["pdf", "txt"]:
 
84
  return JSONResponse(
85
  status_code=400,
86
  content={"error": "Only PDF and TXT files allowed"}
 
21
  genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
22
 
23
  MOCK_MODE = False # Refactor complete - enabling real agent
24
+ MODEL_NAME = "gemini-3-flash-preview"
25
  MAX_FILE_SIZE = 50 * 1024 * 1024
26
  CACHE_TTL = 300
27
 
 
79
  @app.post("/upload")
80
  async def upload(files: list[UploadFile] = File(...)):
81
  for file in files:
82
+ filename = file.filename
83
+ ext = filename.split(".")[-1].lower() if "." in filename else ""
84
+ print(f"πŸ” DEBUG: Uploading '{filename}' (Ext: {ext})")
85
+
86
  if ext not in ["pdf", "txt"]:
87
+ print(f"❌ REJECTED: Invalid extension '{ext}'")
88
  return JSONResponse(
89
  status_code=400,
90
  content={"error": "Only PDF and TXT files allowed"}
rag_eval_logs.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
rag_store.py CHANGED
@@ -99,11 +99,17 @@ def ingest_documents(files):
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
 
 
99
  try:
100
  # Use pymupdf4llm to extract markdown with tables
101
  import pymupdf4llm
102
+ # Get list of dicts: [{'text': '...', 'metadata': {'page': 1, ...}}]
103
+ pages_data = pymupdf4llm.to_markdown(tmp_path, page_chunks=True)
104
 
105
+ for page_obj in pages_data:
106
+ p_text = page_obj["text"]
107
+ p_num = page_obj["metadata"].get("page", "N/A")
108
+
109
+ # Chunk within the page to preserve page context
110
+ for chunk in chunk_text(p_text):
111
+ texts.append(chunk)
112
+ meta.append({"source": file.filename, "page": p_num})
113
  finally:
114
  os.remove(tmp_path)
115
 
students.db ADDED
Binary file (12.3 kB). View file
 
verify_graph_flow.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+ import os
4
+
5
+ # Set dummy key if not present to avoid init errors
6
+ if "GEMINI_API_KEY" not in os.environ:
7
+ os.environ["GEMINI_API_KEY"] = "dummy_key"
8
+ if "TAVILY_API_KEY" not in os.environ:
9
+ os.environ["TAVILY_API_KEY"] = "dummy_key"
10
+
11
+ from agentic_rag_v2_graph import build_agentic_rag_v2_graph
12
+
13
+ class TestRagGraph(unittest.TestCase):
14
+ @patch('agentic_rag_v2_graph.genai.GenerativeModel')
15
+ @patch('agentic_rag_v2_graph.TavilyClient')
16
+ def test_web_search_flow(self, mock_tavily, mock_genai):
17
+ print("\n\n=== πŸ§ͺ STARTING DRY RUN GRAPH TEST ===")
18
+ print("Goal: Verify 'research_web' -> 'verifier' -> 'responder' flow without API calls.\n")
19
+
20
+ # === πŸ”§ DEMO CONFIGURATION (EDIT HERE) ===
21
+ # Change these values to simulate different questions!
22
+ DEMO_QUERY = "Who is the father of the computer?"
23
+ EXPECTED_WEB_CONTENT = "Charles Babbage is considered by many as the father of the computer."
24
+ VERIFIER_NOTE = "βœ… VERIFIED: Search results confirm Charles Babbage invented the Analytical Engine."
25
+ FINAL_ANSWER = "Charles Babbage is the father of the computer."
26
+ # =========================================
27
+
28
+ # --- Setup Mocks ---
29
+ mock_model = MagicMock()
30
+ mock_genai.return_value = mock_model
31
+
32
+ # Helper to create dummy response object
33
+ def create_response(text):
34
+ r = MagicMock()
35
+ r.text = text
36
+ return r
37
+
38
+ # Sequence of LLM outputs (Order matters!):
39
+ # 1. Supervisor: "research_web"
40
+ # 2. Verifier: "The info looks consistent."
41
+ # 3. Supervisor: "responder"
42
+ # 4. Responder: "The Answer."
43
+
44
+ mock_model.generate_content.side_effect = [
45
+ create_response("research_web"),
46
+ create_response(VERIFIER_NOTE),
47
+ create_response("responder"),
48
+ create_response(FINAL_ANSWER)
49
+ ]
50
+
51
+ # Mock Web Search Tool
52
+ mock_tavily_instance = MagicMock()
53
+ mock_tavily.return_value = mock_tavily_instance
54
+ mock_tavily_instance.get_search_context.return_value = EXPECTED_WEB_CONTENT
55
+
56
+ # --- Build Graph ---
57
+ print("πŸ› οΈ Building Graph...")
58
+ try:
59
+ graph = build_agentic_rag_v2_graph()
60
+ print("βœ… Graph built successfully.")
61
+ except Exception as e:
62
+ self.fail(f"❌ Graph build failed: {e}")
63
+
64
+ # --- Run Graph ---
65
+ initial_state = {
66
+ "messages": [],
67
+ "query": DEMO_QUERY,
68
+ "final_answer": "",
69
+ "next_node": "",
70
+ "current_tool": "",
71
+ "tool_outputs": [],
72
+ "verification_notes": "",
73
+ "retries": 0
74
+ }
75
+
76
+ print("\nπŸƒ Invoking Graph (Mocked LLM)...")
77
+ result = graph.invoke(initial_state, config={"configurable": {"thread_id": "test_dry_run"}})
78
+
79
+ # --- Assertions ---
80
+ print("\n\n=== πŸ“Š TEST RESULT ANALYSIS ===")
81
+ print(f"Final Answer: {result['final_answer']}")
82
+ print(f"Verification Notes: {result['verification_notes']}")
83
+
84
+ self.assertIn("VERIFIED", result['verification_notes'], "❌ verifier_node did not populate verification_notes!")
85
+ self.assertIn(FINAL_ANSWER, result['final_answer'], "❌ Responder did not fail gracefully.")
86
+
87
+ print("\nβœ… SUCCESS: The Graph followed the correct path: Supervisor -> Web -> Verifier -> Supervisor -> Responder")
88
+ print("βœ… SUCCESS: 'verifier_node' executed and produced notes.")
89
+
90
+ if __name__ == "__main__":
91
+ unittest.main()
verify_rag.py CHANGED
@@ -1,4 +1,7 @@
1
  import asyncio
 
 
 
2
  from agentic_rag_v2_graph import build_agentic_rag_v2_graph
3
 
4
  async def main():
@@ -21,7 +24,7 @@ async def main():
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?"
@@ -33,7 +36,7 @@ async def main():
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:
 
1
  import asyncio
2
+ import os
3
+ from dotenv import load_dotenv
4
+ load_dotenv()
5
  from agentic_rag_v2_graph import build_agentic_rag_v2_graph
6
 
7
  async def main():
 
24
  }
25
 
26
  result = await graph.ainvoke(inputs, config=config)
27
+ print(f"Answer 1: {result['final_answer']}")
28
 
29
  print("\n--- Turn 2 ---")
30
  inputs["query"] = "What is my name?"
 
36
  inputs["messages"] = []
37
 
38
  result = await graph.ainvoke(inputs, config=config)
39
+ print(f"Answer 2: {result['final_answer']}")
40
 
41
  if __name__ == "__main__":
42
  try: