GT5557 commited on
Commit
d28c250
·
verified ·
1 Parent(s): 63f5fb3

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +218 -154
agent.py CHANGED
@@ -1,11 +1,11 @@
1
  # ==========================================================
2
- # UPDATED agent.py
3
- # Observability + Adaptive Search + Better Recursion Control
4
  # ==========================================================
5
 
6
  import os
7
  import re
8
- import time
9
  import tempfile
10
  import subprocess
11
  from typing import Literal
@@ -15,25 +15,28 @@ from dotenv import load_dotenv
15
  from langgraph.graph import START, StateGraph, MessagesState
16
  from langgraph.prebuilt import ToolNode, tools_condition
17
 
18
- from langchain_groq import ChatGroq
19
  from langchain_core.tools import tool
20
  from langchain_core.messages import SystemMessage
21
 
 
 
 
22
  from langchain_community.document_loaders import WikipediaLoader
23
  from langchain_community.tools import DuckDuckGoSearchRun
24
 
25
  import requests
 
26
  from bs4 import BeautifulSoup
27
 
28
  load_dotenv()
29
 
30
  # ==========================================================
31
- # GLOBAL TRACE
32
  # ==========================================================
33
 
34
  LAST_MODEL_USED = "N/A"
35
- LAST_MODEL_FALLBACK = "No"
36
- LAST_MODEL_ERROR = "None"
37
 
38
  # ==========================================================
39
  # TOOLS
@@ -43,21 +46,17 @@ LAST_MODEL_ERROR = "None"
43
  def wiki_search(query: str) -> str:
44
  """Search Wikipedia."""
45
  try:
46
- docs = WikipediaLoader(
47
- query=query,
48
- load_max_docs=2
49
- ).load()
50
 
51
  if not docs:
52
  return "No results"
53
 
54
  return "\n\n".join(
55
- d.page_content[:1800]
56
- for d in docs
57
  )
58
 
59
- except Exception as e:
60
- return f"N/A {e}"
61
 
62
 
63
  @tool
@@ -67,13 +66,14 @@ def web_search(query: str) -> str:
67
  return DuckDuckGoSearchRun(
68
  max_results=4
69
  ).run(query)
70
- except Exception as e:
71
- return f"N/A {e}"
 
72
 
73
 
74
  @tool
75
  def fetch_page(url: str) -> str:
76
- """Fetch page text."""
77
  try:
78
  r = requests.get(
79
  url,
@@ -94,10 +94,12 @@ def fetch_page(url: str) -> str:
94
  ):
95
  t.decompose()
96
 
97
- return soup.get_text(
98
  separator="\n",
99
  strip=True
100
- )[:7000]
 
 
101
 
102
  except Exception:
103
  return "N/A"
@@ -105,7 +107,7 @@ def fetch_page(url: str) -> str:
105
 
106
  @tool
107
  def run_python(code: str) -> str:
108
- """Run Python."""
109
  try:
110
  with tempfile.NamedTemporaryFile(
111
  mode="w",
@@ -113,13 +115,13 @@ def run_python(code: str) -> str:
113
  delete=False
114
  ) as f:
115
  f.write(code)
116
- fname = f.name
117
 
118
  p = subprocess.run(
119
- ["python3", fname],
120
  capture_output=True,
121
  text=True,
122
- timeout=15
123
  )
124
 
125
  out = p.stdout.strip()
@@ -130,53 +132,111 @@ def run_python(code: str) -> str:
130
  return "N/A"
131
 
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  @tool
134
  def reverse_text(text: str) -> str:
135
  """Reverse text."""
136
  return text[::-1]
137
 
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  TOOLS = [
140
  wiki_search,
141
  web_search,
142
  fetch_page,
143
  run_python,
144
- reverse_text
 
 
 
 
 
 
145
  ]
146
 
147
  # ==========================================================
148
  # MODELS
149
  # ==========================================================
150
 
151
- def llm(name):
152
  return ChatGroq(
153
- model=name,
154
  api_key=os.getenv("GROQ_API_KEY"),
155
  temperature=0
156
  )
157
 
158
 
159
- MODEL_FAST = llm("llama-3.1-8b-instant")
160
- MODEL_SMART = llm("qwen/qwen3-32b")
161
-
162
- try:
163
- MODEL_BIG = llm(
164
- "llama-3.3-70b-versatile"
165
  )
166
- except Exception:
167
- MODEL_BIG = MODEL_SMART
 
 
 
 
168
 
169
  # ==========================================================
170
- # ROUTER
171
  # ==========================================================
172
 
173
  def classify_question(
174
  question: str
175
- ) -> Literal[
176
- "simple",
177
- "medium",
178
- "hard"
179
- ]:
180
  q = question.lower()
181
 
182
  if any(x in q for x in [
@@ -186,132 +246,160 @@ def classify_question(
186
  "image",
187
  "excel",
188
  "xlsx",
 
189
  "pdf",
 
190
  "chess",
191
- "attached"
192
  ]):
193
  return "hard"
194
 
195
  if any(x in q for x in [
196
- "how many",
197
- "who is",
198
  "what is",
 
 
199
  "surname",
200
  "first name",
201
- "reverse"
202
  ]):
203
  return "simple"
204
 
205
  return "medium"
206
 
 
 
 
207
 
208
- def preferred_search(question: str):
209
- q = question.lower()
210
-
211
- if any(x in q for x in [
212
- "paper",
213
- "article",
214
- "competition",
215
- "recipient",
216
- "licensed",
217
- "2023",
218
- "july",
219
- "pdf"
220
- ]):
221
- return "web"
222
-
223
- return "wiki"
224
-
225
-
226
- def pick_model(question: str):
227
  level = classify_question(question)
228
 
229
  if level == "simple":
230
- return MODEL_FAST
 
 
 
231
 
232
  if level == "medium":
233
- return MODEL_SMART
234
-
235
- return MODEL_BIG
 
 
 
 
 
 
 
 
236
 
237
  # ==========================================================
238
- # PROMPT
239
  # ==========================================================
240
 
241
- def make_prompt(question: str):
242
- search_pref = preferred_search(question)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
- return f"""
245
- You solve benchmark tasks.
246
 
247
- Preferred search tool:
248
- {search_pref}_search
 
 
249
 
250
- Rules:
251
- 1. Exact answer only.
252
- 2. Never explain.
253
- 3. If asked first name / surname,
254
- return only requested part.
255
- 4. Use at most 2 tool calls.
256
- 5. If unsure, short best guess.
257
- 6. No extra words.
258
 
259
- Always end with:
 
260
 
261
- FINAL ANSWER: value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  """
263
 
264
  # ==========================================================
265
- # INVOKE
266
  # ==========================================================
267
 
268
  def invoke(messages, question):
269
  global LAST_MODEL_USED
270
- global LAST_MODEL_FALLBACK
271
- global LAST_MODEL_ERROR
272
-
273
- LAST_MODEL_FALLBACK = "No"
274
- LAST_MODEL_ERROR = "None"
275
-
276
- models = [
277
- pick_model(question),
278
- MODEL_SMART,
279
- MODEL_FAST
280
- ]
281
 
282
- used = set()
 
 
283
 
284
  first = True
285
 
286
- for m in models:
287
- key = str(m)
288
-
289
- if key in used:
290
- continue
291
-
292
- used.add(key)
293
-
294
  try:
295
- LAST_MODEL_USED = key
296
 
297
  if not first:
298
- LAST_MODEL_FALLBACK = "Yes"
299
 
300
  first = False
301
 
302
- return m.bind_tools(
303
  TOOLS
304
  ).invoke(messages)
305
 
306
  except Exception as e:
307
- LAST_MODEL_ERROR = str(e)
308
 
309
- raise RuntimeError(
310
- "All models failed"
311
- )
312
 
313
  # ==========================================================
314
- # GRAPH
315
  # ==========================================================
316
 
317
  def assistant(state: MessagesState):
@@ -324,7 +412,7 @@ def assistant(state: MessagesState):
324
  question = str(m.content)
325
  break
326
 
327
- prompt = make_prompt(question)
328
 
329
  messages = [
330
  SystemMessage(content=prompt)
@@ -371,23 +459,14 @@ def build_graph():
371
  return g.compile()
372
 
373
  # ==========================================================
374
- # EXTRACTION
375
  # ==========================================================
376
 
377
- def extract_final_answer(
378
- output: dict
379
- ):
380
- msgs = output.get(
381
- "messages",
382
- []
383
- )
384
 
385
  for m in reversed(msgs):
386
- txt = getattr(
387
- m,
388
- "content",
389
- ""
390
- )
391
 
392
  if isinstance(txt, str):
393
  match = re.search(
@@ -397,45 +476,30 @@ def extract_final_answer(
397
  )
398
 
399
  if match:
400
- return match.group(
401
- 1
402
- ).strip()[:250]
 
403
 
404
  return "N/A"
405
 
406
 
407
- def extract_tools_used(
408
- output: dict
409
- ):
410
- msgs = output.get(
411
- "messages",
412
- []
413
- )
414
 
415
  tools = []
416
 
417
  for m in msgs:
418
- nm = getattr(
419
- m,
420
- "name",
421
- None
422
- )
423
 
424
  if nm:
425
  tools.append(str(nm))
426
 
427
- tc = getattr(
428
- m,
429
- "tool_calls",
430
- None
431
- )
432
 
433
  if tc:
434
  for x in tc:
435
- if isinstance(
436
- x,
437
- dict
438
- ):
439
  n = x.get("name")
440
 
441
  if n:
@@ -455,6 +519,6 @@ def extract_tools_used(
455
  def get_last_trace():
456
  return {
457
  "model": LAST_MODEL_USED,
458
- "fallback": LAST_MODEL_FALLBACK,
459
- "model_error": LAST_MODEL_ERROR
460
  }
 
1
  # ==========================================================
2
+ # FINAL agent.py
3
+ # GAIA Optimized | Multi-Provider | Strong Prompt | Tools
4
  # ==========================================================
5
 
6
  import os
7
  import re
8
+ import io
9
  import tempfile
10
  import subprocess
11
  from typing import Literal
 
15
  from langgraph.graph import START, StateGraph, MessagesState
16
  from langgraph.prebuilt import ToolNode, tools_condition
17
 
 
18
  from langchain_core.tools import tool
19
  from langchain_core.messages import SystemMessage
20
 
21
+ from langchain_groq import ChatGroq
22
+ from langchain_google_genai import ChatGoogleGenerativeAI
23
+
24
  from langchain_community.document_loaders import WikipediaLoader
25
  from langchain_community.tools import DuckDuckGoSearchRun
26
 
27
  import requests
28
+ import pandas as pd
29
  from bs4 import BeautifulSoup
30
 
31
  load_dotenv()
32
 
33
  # ==========================================================
34
+ # TRACE VARIABLES
35
  # ==========================================================
36
 
37
  LAST_MODEL_USED = "N/A"
38
+ LAST_FALLBACK = "No"
39
+ LAST_ERROR = "None"
40
 
41
  # ==========================================================
42
  # TOOLS
 
46
  def wiki_search(query: str) -> str:
47
  """Search Wikipedia."""
48
  try:
49
+ docs = WikipediaLoader(query=query, load_max_docs=2).load()
 
 
 
50
 
51
  if not docs:
52
  return "No results"
53
 
54
  return "\n\n".join(
55
+ d.page_content[:1800] for d in docs
 
56
  )
57
 
58
+ except Exception:
59
+ return "N/A"
60
 
61
 
62
  @tool
 
66
  return DuckDuckGoSearchRun(
67
  max_results=4
68
  ).run(query)
69
+
70
+ except Exception:
71
+ return "N/A"
72
 
73
 
74
  @tool
75
  def fetch_page(url: str) -> str:
76
+ """Fetch webpage text."""
77
  try:
78
  r = requests.get(
79
  url,
 
94
  ):
95
  t.decompose()
96
 
97
+ text = soup.get_text(
98
  separator="\n",
99
  strip=True
100
+ )
101
+
102
+ return text[:7000]
103
 
104
  except Exception:
105
  return "N/A"
 
107
 
108
  @tool
109
  def run_python(code: str) -> str:
110
+ """Execute Python code."""
111
  try:
112
  with tempfile.NamedTemporaryFile(
113
  mode="w",
 
115
  delete=False
116
  ) as f:
117
  f.write(code)
118
+ path = f.name
119
 
120
  p = subprocess.run(
121
+ ["python3", path],
122
  capture_output=True,
123
  text=True,
124
+ timeout=20
125
  )
126
 
127
  out = p.stdout.strip()
 
132
  return "N/A"
133
 
134
 
135
+ @tool
136
+ def add(a: float, b: float) -> str:
137
+ """Add numbers."""
138
+ return str(a + b)
139
+
140
+
141
+ @tool
142
+ def subtract(a: float, b: float) -> str:
143
+ """Subtract numbers."""
144
+ return str(a - b)
145
+
146
+
147
+ @tool
148
+ def multiply(a: float, b: float) -> str:
149
+ """Multiply numbers."""
150
+ return str(a * b)
151
+
152
+
153
+ @tool
154
+ def divide(a: float, b: float) -> str:
155
+ """Divide numbers."""
156
+ if b == 0:
157
+ return "N/A"
158
+ return str(a / b)
159
+
160
+
161
  @tool
162
  def reverse_text(text: str) -> str:
163
  """Reverse text."""
164
  return text[::-1]
165
 
166
 
167
+ @tool
168
+ def read_excel(url: str) -> str:
169
+ """Read Excel file from URL."""
170
+ try:
171
+ r = requests.get(url, timeout=20)
172
+ r.raise_for_status()
173
+
174
+ bio = io.BytesIO(r.content)
175
+
176
+ df = pd.read_excel(bio)
177
+
178
+ return df.head(25).to_csv(index=False)[:7000]
179
+
180
+ except Exception:
181
+ return "N/A"
182
+
183
+
184
+ @tool
185
+ def youtube_transcript(url: str) -> str:
186
+ """
187
+ Placeholder transcript tool.
188
+ Use web_search if transcript unavailable.
189
+ """
190
+ return "Transcript unavailable"
191
+
192
+
193
  TOOLS = [
194
  wiki_search,
195
  web_search,
196
  fetch_page,
197
  run_python,
198
+ add,
199
+ subtract,
200
+ multiply,
201
+ divide,
202
+ reverse_text,
203
+ read_excel,
204
+ youtube_transcript,
205
  ]
206
 
207
  # ==========================================================
208
  # MODELS
209
  # ==========================================================
210
 
211
+ def groq(model_name):
212
  return ChatGroq(
213
+ model=model_name,
214
  api_key=os.getenv("GROQ_API_KEY"),
215
  temperature=0
216
  )
217
 
218
 
219
+ def gemini(model_name):
220
+ return ChatGoogleGenerativeAI(
221
+ model=model_name,
222
+ google_api_key=os.getenv("GOOGLE_API_KEY"),
223
+ temperature=0
 
224
  )
225
+
226
+
227
+ MODEL_FAST = groq("llama-3.1-8b-instant")
228
+ MODEL_MED = groq("qwen/qwen3-32b")
229
+ MODEL_HARD = gemini("gemini-3.1-flash-lite")
230
+ MODEL_REASON = gemini("gemma-4-26b")
231
 
232
  # ==========================================================
233
+ # QUESTION CLASSIFIER
234
  # ==========================================================
235
 
236
  def classify_question(
237
  question: str
238
+ ) -> Literal["simple", "medium", "hard"]:
239
+
 
 
 
240
  q = question.lower()
241
 
242
  if any(x in q for x in [
 
246
  "image",
247
  "excel",
248
  "xlsx",
249
+ "csv",
250
  "pdf",
251
+ "attached",
252
  "chess",
 
253
  ]):
254
  return "hard"
255
 
256
  if any(x in q for x in [
 
 
257
  "what is",
258
+ "who is",
259
+ "how many",
260
  "surname",
261
  "first name",
262
+ "reverse",
263
  ]):
264
  return "simple"
265
 
266
  return "medium"
267
 
268
+ # ==========================================================
269
+ # MODEL ROUTING
270
+ # ==========================================================
271
 
272
+ def choose_models(question: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  level = classify_question(question)
274
 
275
  if level == "simple":
276
+ return [
277
+ ("llama-3.1-8b", MODEL_FAST),
278
+ ("qwen3-32b", MODEL_MED),
279
+ ]
280
 
281
  if level == "medium":
282
+ return [
283
+ ("qwen3-32b", MODEL_MED),
284
+ ("gemma-4-26b", MODEL_REASON),
285
+ ("gemini-3.1-lite", MODEL_HARD),
286
+ ]
287
+
288
+ return [
289
+ ("gemini-3.1-lite", MODEL_HARD),
290
+ ("gemma-4-26b", MODEL_REASON),
291
+ ("qwen3-32b", MODEL_MED),
292
+ ]
293
 
294
  # ==========================================================
295
+ # STRONG SYSTEM PROMPT
296
  # ==========================================================
297
 
298
+ SYSTEM_PROMPT = """
299
+ You are a precise AI assistant for GAIA benchmark evaluation.
300
+ Your objective is to maximize exact-match score.
301
+
302
+ AVAILABLE TOOLS:
303
+ wiki_search, web_search, fetch_page, run_python,
304
+ add, subtract, multiply, divide, reverse_text,
305
+ read_excel, youtube_transcript
306
+
307
+ DECISION RULES:
308
+
309
+ 1. Use tools for factual questions requiring external knowledge:
310
+ names, dates, rankings, counts, locations, webpages, files.
311
+
312
+ 2. Do NOT use tools for trivial math, simple logic,
313
+ direct text transforms, or obvious reasoning.
314
+
315
+ 3. If a URL is present -> use fetch_page first.
316
+
317
+ 4. If code / python file is present -> use run_python.
318
+
319
+ 5. If reversed / encoded text -> decode first.
320
+
321
+ 6. For math -> use math tools or direct arithmetic.
322
+
323
+ 7. If spreadsheet / xlsx / csv appears -> use read_excel.
324
+
325
+ 8. For YouTube/video/audio -> use youtube_transcript first,
326
+ otherwise web_search.
327
+
328
+ 9. Use at most 2 search attempts total.
329
+ Do not loop endlessly.
330
 
331
+ ACCURACY RULES:
 
332
 
333
+ 10. Read qualifiers carefully:
334
+ only, least, highest, lowest, included,
335
+ before, after, between, as of date,
336
+ first alphabetically.
337
 
338
+ 11. If asked first name -> first name only.
339
+ If asked surname -> surname only.
 
 
 
 
 
 
340
 
341
+ 12. If asked for list:
342
+ return only valid items, comma-separated.
343
 
344
+ 13. Do not over-answer.
345
+ Do not include related but unasked facts.
346
+
347
+ 14. If uncertain, give the most likely concise answer.
348
+ Do not apologize.
349
+
350
+ OUTPUT RULES:
351
+
352
+ 15. Final output must be exactly one clean line:
353
+
354
+ FINAL ANSWER: <answer>
355
+
356
+ 16. Use digits for numbers unless explicitly asked otherwise.
357
+
358
+ 17. No quotes, bullets, markdown, commentary, or explanation.
359
+ """
360
+
361
+ def build_prompt(question: str):
362
+ return f"""{SYSTEM_PROMPT}
363
+
364
+ QUESTION:
365
+ {question}
366
  """
367
 
368
  # ==========================================================
369
+ # INVOKER
370
  # ==========================================================
371
 
372
  def invoke(messages, question):
373
  global LAST_MODEL_USED
374
+ global LAST_FALLBACK
375
+ global LAST_ERROR
 
 
 
 
 
 
 
 
 
376
 
377
+ LAST_MODEL_USED = "N/A"
378
+ LAST_FALLBACK = "No"
379
+ LAST_ERROR = "None"
380
 
381
  first = True
382
 
383
+ for name, model in choose_models(question):
 
 
 
 
 
 
 
384
  try:
385
+ LAST_MODEL_USED = name
386
 
387
  if not first:
388
+ LAST_FALLBACK = "Yes"
389
 
390
  first = False
391
 
392
+ return model.bind_tools(
393
  TOOLS
394
  ).invoke(messages)
395
 
396
  except Exception as e:
397
+ LAST_ERROR = str(e)
398
 
399
+ raise RuntimeError("All models failed")
 
 
400
 
401
  # ==========================================================
402
+ # GRAPH NODE
403
  # ==========================================================
404
 
405
  def assistant(state: MessagesState):
 
412
  question = str(m.content)
413
  break
414
 
415
+ prompt = build_prompt(question)
416
 
417
  messages = [
418
  SystemMessage(content=prompt)
 
459
  return g.compile()
460
 
461
  # ==========================================================
462
+ # HELPERS
463
  # ==========================================================
464
 
465
+ def extract_final_answer(output: dict):
466
+ msgs = output.get("messages", [])
 
 
 
 
 
467
 
468
  for m in reversed(msgs):
469
+ txt = getattr(m, "content", "")
 
 
 
 
470
 
471
  if isinstance(txt, str):
472
  match = re.search(
 
476
  )
477
 
478
  if match:
479
+ ans = match.group(1).strip()
480
+ ans = ans.replace("\n", " ")
481
+
482
+ return ans[:250]
483
 
484
  return "N/A"
485
 
486
 
487
+ def extract_tools_used(output: dict):
488
+ msgs = output.get("messages", [])
 
 
 
 
 
489
 
490
  tools = []
491
 
492
  for m in msgs:
493
+ nm = getattr(m, "name", None)
 
 
 
 
494
 
495
  if nm:
496
  tools.append(str(nm))
497
 
498
+ tc = getattr(m, "tool_calls", None)
 
 
 
 
499
 
500
  if tc:
501
  for x in tc:
502
+ if isinstance(x, dict):
 
 
 
503
  n = x.get("name")
504
 
505
  if n:
 
519
  def get_last_trace():
520
  return {
521
  "model": LAST_MODEL_USED,
522
+ "fallback": LAST_FALLBACK,
523
+ "model_error": LAST_ERROR,
524
  }