GT5557 commited on
Commit
e1bf07d
·
verified ·
1 Parent(s): aabeef8

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +155 -155
agent.py CHANGED
@@ -1,7 +1,10 @@
 
 
 
 
 
1
  import os
2
  import re
3
- import time
4
- import json
5
  import tempfile
6
  import subprocess
7
  from typing import Literal
@@ -23,16 +26,15 @@ from bs4 import BeautifulSoup
23
 
24
  load_dotenv()
25
 
26
- # ============================================================
27
  # CONFIG
28
- # ============================================================
29
 
30
- MAX_TOOL_LOOPS = 3
31
- QUESTION_TIMEOUT_SECONDS = 85
32
 
33
- # ============================================================
34
  # TOOLS
35
- # ============================================================
36
 
37
  @tool
38
  def add(a: float, b: float) -> float:
@@ -42,7 +44,7 @@ def add(a: float, b: float) -> float:
42
 
43
  @tool
44
  def subtract(a: float, b: float) -> float:
45
- """Subtract b from a."""
46
  return a - b
47
 
48
 
@@ -54,96 +56,103 @@ def multiply(a: float, b: float) -> float:
54
 
55
  @tool
56
  def divide(a: float, b: float) -> str:
57
- """Divide a by b."""
58
  if b == 0:
59
- return "division by zero"
60
  return str(a / b)
61
 
62
 
63
  @tool
64
  def reverse_text(text: str) -> str:
65
- """Reverse text character by character."""
66
  return text[::-1]
67
 
68
 
69
  @tool
70
  def wiki_search(query: str) -> str:
71
- """Search Wikipedia."""
72
  try:
73
  docs = WikipediaLoader(query=query, load_max_docs=2).load()
 
74
  if not docs:
75
  return "No results"
76
 
77
  out = []
 
78
  for d in docs:
79
  out.append(
80
- f"TITLE: {d.metadata.get('title','')}\n"
81
- f"{d.page_content[:2200]}"
82
  )
83
- return "\n\n---\n\n".join(out)
84
 
85
- except Exception as e:
86
- return f"wiki error: {e}"
 
 
87
 
88
 
89
  @tool
90
  def web_search(query: str) -> str:
91
- """Search web using DuckDuckGo."""
92
  try:
93
- return DuckDuckGoSearchRun(max_results=5).run(query)
94
- except Exception as e:
95
- return f"web error: {e}"
96
 
97
 
98
  @tool
99
  def fetch_page(url: str) -> str:
100
  """Fetch webpage text."""
101
  try:
102
- headers = {"User-Agent": "Mozilla/5.0"}
103
- r = requests.get(url, timeout=15, headers=headers)
 
 
 
 
104
  r.raise_for_status()
105
 
106
  soup = BeautifulSoup(r.text, "html.parser")
107
 
108
- for t in soup(["script", "style", "nav", "footer", "header"]):
109
  t.decompose()
110
 
111
- txt = soup.get_text(separator="\n", strip=True)
112
- return txt[:8000]
 
 
113
 
114
- except Exception as e:
115
- return f"fetch error: {e}"
 
 
116
 
117
 
118
  @tool
119
  def run_python(code: str) -> str:
120
- """Execute Python safely."""
121
  try:
122
  with tempfile.NamedTemporaryFile(
123
- mode="w", suffix=".py", delete=False
 
 
124
  ) as f:
125
  f.write(code)
126
  fname = f.name
127
 
128
- result = subprocess.run(
129
  ["python3", fname],
130
  capture_output=True,
131
  text=True,
132
- timeout=18
133
  )
134
 
135
- out = result.stdout.strip()
136
- err = result.stderr.strip()
137
-
138
- if err:
139
- out += f"\nSTDERR:\n{err}"
140
 
141
- return out if out else "(no output)"
142
 
143
- except subprocess.TimeoutExpired:
144
- return "python timeout"
145
- except Exception as e:
146
- return f"python error: {e}"
147
 
148
 
149
  TOOLS = [
@@ -158,11 +167,11 @@ TOOLS = [
158
  run_python,
159
  ]
160
 
161
- # ============================================================
162
- # MODEL LAYER
163
- # ============================================================
164
 
165
- def build_model(model_name: str):
166
  return ChatGroq(
167
  model=model_name,
168
  api_key=os.getenv("GROQ_API_KEY"),
@@ -170,198 +179,189 @@ def build_model(model_name: str):
170
  )
171
 
172
 
173
- MODEL_8B = build_model("llama-3.1-8b-instant")
174
- MODEL_QWEN = build_model("qwen/qwen3-32b")
175
-
176
- # optional
177
- MODEL_70B = build_model("llama-3.3-70b-versatile")
178
 
 
 
 
 
179
 
180
- # ============================================================
181
  # ROUTER
182
- # ============================================================
183
 
184
  def classify_question(question: str) -> Literal["simple", "medium", "hard"]:
185
  q = question.lower()
186
 
187
  if any(x in q for x in [
188
- "youtube.com", "watch?v=", "video", "audio",
189
- "image", "xlsx", "excel", "pdf",
190
- "attached file", "chess", "board position"
 
191
  ]):
192
  return "hard"
193
 
194
  if any(x in q for x in [
195
- "calculate", "reverse", "opposite",
196
- "who is", "what is", "where is",
197
- "how many", "first name", "surname"
 
198
  ]):
199
  return "simple"
200
 
201
  return "medium"
202
 
203
 
204
- def choose_model(question: str):
205
  level = classify_question(question)
206
 
207
  if level == "simple":
208
- return MODEL_8B
209
 
210
  if level == "medium":
211
- return MODEL_QWEN
212
-
213
- # hard
214
- try:
215
- return MODEL_70B
216
- except Exception:
217
- return MODEL_QWEN
218
 
 
219
 
220
- # ============================================================
221
- # SYSTEM PROMPT
222
- # ============================================================
223
 
224
  SYSTEM_PROMPT = """
225
- You are an exact-answer benchmark agent.
226
-
227
- STRICT RULES:
228
-
229
- 1. Use tools whenever facts, names, dates, counts, URLs, files or code are involved.
230
- 2. If URL appears -> use fetch_page first.
231
- 3. If code appears -> use run_python.
232
- 4. If text seems reversed -> use reverse_text.
233
- 5. For factual lookup: wiki_search first, then web_search if needed.
234
- 6. Be concise.
235
- 7. Never hallucinate.
236
- 8. If unsure, search again once.
237
 
238
- FINAL OUTPUT MUST END EXACTLY AS:
 
 
 
 
 
 
 
239
 
240
- FINAL ANSWER: answer_here
241
 
242
- Rules:
243
- - no explanation after final answer
244
- - plain text only
245
- - list answers comma-separated
246
  """
247
 
 
 
 
248
 
249
- # ============================================================
250
- # FALLBACK INVOKER
251
- # ============================================================
 
 
 
252
 
253
- def invoke_with_escalation(messages, question: str):
254
- primary = choose_model(question)
255
-
256
- order = [primary, MODEL_QWEN, MODEL_8B]
257
-
258
- # keep unique order
259
- unique = []
260
  seen = set()
261
 
262
- for m in order:
263
  key = str(m)
264
- if key not in seen:
265
- unique.append(m)
266
- seen.add(key)
267
 
268
- last_err = None
 
269
 
270
- for model in unique:
271
- try:
272
- return model.bind_tools(TOOLS).invoke(messages)
273
 
274
- except Exception as e:
275
- last_err = e
276
- time.sleep(1)
277
-
278
- raise RuntimeError(f"all models failed: {last_err}")
279
 
 
280
 
281
- # ============================================================
282
- # GRAPH STATE
283
- # ============================================================
284
 
285
  def assistant(state: MessagesState):
286
  msgs = state["messages"]
287
 
288
- user_question = ""
 
289
  for m in reversed(msgs):
290
  if hasattr(m, "content"):
291
- user_question = str(m.content)
292
  break
293
 
294
  messages = [SystemMessage(content=SYSTEM_PROMPT)] + msgs
295
 
296
- result = invoke_with_escalation(messages, user_question)
297
 
298
  return {"messages": [result]}
299
 
300
 
301
  def build_graph():
302
- graph = StateGraph(MessagesState)
303
-
304
- graph.add_node("assistant", assistant)
305
- graph.add_node("tools", ToolNode(TOOLS))
306
-
307
- graph.add_edge(START, "assistant")
308
- graph.add_conditional_edges("assistant", tools_condition)
309
- graph.add_edge("tools", "assistant")
310
 
311
- return graph.compile()
 
312
 
 
 
 
313
 
314
- agent = build_graph()
315
 
316
- # ============================================================
317
  # ANSWER EXTRACTION
318
- # ============================================================
319
 
320
  def extract_final_answer(output: dict) -> str:
321
  msgs = output.get("messages", [])
322
 
323
  for m in reversed(msgs):
324
  content = getattr(m, "content", "")
 
325
  if isinstance(content, str):
326
  match = re.search(
327
  r"FINAL ANSWER:\s*(.+)",
328
  content,
329
  re.I | re.S
330
  )
 
331
  if match:
332
- return match.group(1).strip()
 
333
 
334
- return "N/A"
335
 
 
336
 
337
- # ============================================================
338
- # PUBLIC SOLVER
339
- # ============================================================
340
 
341
- def solve_question(question: str) -> str:
342
- start = time.time()
343
 
344
- try:
345
- result = agent.invoke(
346
- {"messages": [("user", question)]},
347
- {"recursion_limit": MAX_TOOL_LOOPS * 2}
348
- )
349
 
350
- answer = extract_final_answer(result)
 
351
 
352
- if time.time() - start > QUESTION_TIMEOUT_SECONDS:
353
- return "N/A"
354
 
355
- return answer
356
 
357
- except Exception:
358
- return "N/A"
 
 
 
 
359
 
 
 
 
360
 
361
- # ============================================================
362
- # TEST
363
- # ============================================================
 
364
 
365
- if __name__ == "__main__":
366
- q = "Write the opposite of left."
367
- print(solve_question(q))
 
1
+ # ===========================
2
+ # FINAL agent.py
3
+ # Tool Tracking Enabled
4
+ # ===========================
5
+
6
  import os
7
  import re
 
 
8
  import tempfile
9
  import subprocess
10
  from typing import Literal
 
26
 
27
  load_dotenv()
28
 
29
+ # ==========================================================
30
  # CONFIG
31
+ # ==========================================================
32
 
33
+ MAX_TOOL_LOOPS = 2
 
34
 
35
+ # ==========================================================
36
  # TOOLS
37
+ # ==========================================================
38
 
39
  @tool
40
  def add(a: float, b: float) -> float:
 
44
 
45
  @tool
46
  def subtract(a: float, b: float) -> float:
47
+ """Subtract two numbers."""
48
  return a - b
49
 
50
 
 
56
 
57
  @tool
58
  def divide(a: float, b: float) -> str:
59
+ """Divide two numbers."""
60
  if b == 0:
61
+ return "N/A"
62
  return str(a / b)
63
 
64
 
65
  @tool
66
  def reverse_text(text: str) -> str:
67
+ """Reverse text."""
68
  return text[::-1]
69
 
70
 
71
  @tool
72
  def wiki_search(query: str) -> str:
73
+ """Wikipedia search."""
74
  try:
75
  docs = WikipediaLoader(query=query, load_max_docs=2).load()
76
+
77
  if not docs:
78
  return "No results"
79
 
80
  out = []
81
+
82
  for d in docs:
83
  out.append(
84
+ f"{d.metadata.get('title','')}\n"
85
+ f"{d.page_content[:1800]}"
86
  )
 
87
 
88
+ return "\n\n".join(out)
89
+
90
+ except Exception:
91
+ return "N/A"
92
 
93
 
94
  @tool
95
  def web_search(query: str) -> str:
96
+ """DuckDuckGo search."""
97
  try:
98
+ return DuckDuckGoSearchRun(max_results=4).run(query)
99
+ except Exception:
100
+ return "N/A"
101
 
102
 
103
  @tool
104
  def fetch_page(url: str) -> str:
105
  """Fetch webpage text."""
106
  try:
107
+ r = requests.get(
108
+ url,
109
+ timeout=12,
110
+ headers={"User-Agent": "Mozilla/5.0"}
111
+ )
112
+
113
  r.raise_for_status()
114
 
115
  soup = BeautifulSoup(r.text, "html.parser")
116
 
117
+ for t in soup(["script", "style", "nav", "footer"]):
118
  t.decompose()
119
 
120
+ txt = soup.get_text(
121
+ separator="\n",
122
+ strip=True
123
+ )
124
 
125
+ return txt[:7000]
126
+
127
+ except Exception:
128
+ return "N/A"
129
 
130
 
131
  @tool
132
  def run_python(code: str) -> str:
133
+ """Run Python code."""
134
  try:
135
  with tempfile.NamedTemporaryFile(
136
+ mode="w",
137
+ suffix=".py",
138
+ delete=False
139
  ) as f:
140
  f.write(code)
141
  fname = f.name
142
 
143
+ p = subprocess.run(
144
  ["python3", fname],
145
  capture_output=True,
146
  text=True,
147
+ timeout=15
148
  )
149
 
150
+ out = p.stdout.strip()
 
 
 
 
151
 
152
+ return out if out else "N/A"
153
 
154
+ except Exception:
155
+ return "N/A"
 
 
156
 
157
 
158
  TOOLS = [
 
167
  run_python,
168
  ]
169
 
170
+ # ==========================================================
171
+ # MODELS
172
+ # ==========================================================
173
 
174
+ def llm(model_name):
175
  return ChatGroq(
176
  model=model_name,
177
  api_key=os.getenv("GROQ_API_KEY"),
 
179
  )
180
 
181
 
182
+ MODEL_FAST = llm("llama-3.1-8b-instant")
183
+ MODEL_SMART = llm("qwen/qwen3-32b")
 
 
 
184
 
185
+ try:
186
+ MODEL_BIG = llm("llama-3.3-70b-versatile")
187
+ except Exception:
188
+ MODEL_BIG = MODEL_SMART
189
 
190
+ # ==========================================================
191
  # ROUTER
192
+ # ==========================================================
193
 
194
  def classify_question(question: str) -> Literal["simple", "medium", "hard"]:
195
  q = question.lower()
196
 
197
  if any(x in q for x in [
198
+ "youtube", "video", "audio",
199
+ "image", "xlsx", "excel",
200
+ "pdf", "attached file",
201
+ "chess", "position"
202
  ]):
203
  return "hard"
204
 
205
  if any(x in q for x in [
206
+ "what is", "who is",
207
+ "surname", "first name",
208
+ "how many", "reverse",
209
+ "opposite"
210
  ]):
211
  return "simple"
212
 
213
  return "medium"
214
 
215
 
216
+ def pick_model(question: str):
217
  level = classify_question(question)
218
 
219
  if level == "simple":
220
+ return MODEL_FAST
221
 
222
  if level == "medium":
223
+ return MODEL_SMART
 
 
 
 
 
 
224
 
225
+ return MODEL_BIG
226
 
227
+ # ==========================================================
228
+ # PROMPT
229
+ # ==========================================================
230
 
231
  SYSTEM_PROMPT = """
232
+ You solve benchmark tasks.
 
 
 
 
 
 
 
 
 
 
 
233
 
234
+ RULES:
235
+ 1. Exact answer only.
236
+ 2. Never explain.
237
+ 3. Use tools when needed.
238
+ 4. If asked first name / surname, return only requested part.
239
+ 5. If list requested, give comma separated list only.
240
+ 6. Do not over-answer.
241
+ 7. If unsure, search once then answer.
242
 
243
+ Always end with:
244
 
245
+ FINAL ANSWER: value
 
 
 
246
  """
247
 
248
+ # ==========================================================
249
+ # INVOKER
250
+ # ==========================================================
251
 
252
+ def invoke(messages, question):
253
+ models = [
254
+ pick_model(question),
255
+ MODEL_SMART,
256
+ MODEL_FAST
257
+ ]
258
 
 
 
 
 
 
 
 
259
  seen = set()
260
 
261
+ for m in models:
262
  key = str(m)
 
 
 
263
 
264
+ if key in seen:
265
+ continue
266
 
267
+ seen.add(key)
 
 
268
 
269
+ try:
270
+ return m.bind_tools(TOOLS).invoke(messages)
271
+ except Exception:
272
+ continue
 
273
 
274
+ raise RuntimeError("All models failed")
275
 
276
+ # ==========================================================
277
+ # GRAPH NODE
278
+ # ==========================================================
279
 
280
  def assistant(state: MessagesState):
281
  msgs = state["messages"]
282
 
283
+ question = ""
284
+
285
  for m in reversed(msgs):
286
  if hasattr(m, "content"):
287
+ question = str(m.content)
288
  break
289
 
290
  messages = [SystemMessage(content=SYSTEM_PROMPT)] + msgs
291
 
292
+ result = invoke(messages, question)
293
 
294
  return {"messages": [result]}
295
 
296
 
297
  def build_graph():
298
+ g = StateGraph(MessagesState)
 
 
 
 
 
 
 
299
 
300
+ g.add_node("assistant", assistant)
301
+ g.add_node("tools", ToolNode(TOOLS))
302
 
303
+ g.add_edge(START, "assistant")
304
+ g.add_conditional_edges("assistant", tools_condition)
305
+ g.add_edge("tools", "assistant")
306
 
307
+ return g.compile()
308
 
309
+ # ==========================================================
310
  # ANSWER EXTRACTION
311
+ # ==========================================================
312
 
313
  def extract_final_answer(output: dict) -> str:
314
  msgs = output.get("messages", [])
315
 
316
  for m in reversed(msgs):
317
  content = getattr(m, "content", "")
318
+
319
  if isinstance(content, str):
320
  match = re.search(
321
  r"FINAL ANSWER:\s*(.+)",
322
  content,
323
  re.I | re.S
324
  )
325
+
326
  if match:
327
+ ans = match.group(1).strip()
328
+ ans = ans.replace("\n", " ").strip()
329
 
330
+ return ans[:250]
331
 
332
+ return "N/A"
333
 
334
+ # ==========================================================
335
+ # TOOL TRACKING
336
+ # ==========================================================
337
 
338
+ def extract_tools_used(output: dict):
339
+ msgs = output.get("messages", [])
340
 
341
+ tools = []
 
 
 
 
342
 
343
+ for m in msgs:
344
+ name = getattr(m, "name", None)
345
 
346
+ if name:
347
+ tools.append(str(name))
348
 
349
+ tc = getattr(m, "tool_calls", None)
350
 
351
+ if tc:
352
+ for item in tc:
353
+ if isinstance(item, dict):
354
+ nm = item.get("name")
355
+ if nm:
356
+ tools.append(nm)
357
 
358
+ # deduplicate
359
+ seen = set()
360
+ final = []
361
 
362
+ for t in tools:
363
+ if t not in seen:
364
+ final.append(t)
365
+ seen.add(t)
366
 
367
+ return final