GT5557 commited on
Commit
a5ef8ca
·
verified ·
1 Parent(s): f404bed

Upload 3 files

Browse files
Files changed (3) hide show
  1. agent.py +195 -7
  2. app.py +13 -6
  3. requirements.txt +2 -1
agent.py CHANGED
@@ -4,9 +4,12 @@
4
 
5
  import os
6
  import re
7
- import time
 
8
  import tempfile
9
  import subprocess
 
 
10
 
11
  from dotenv import load_dotenv
12
 
@@ -18,10 +21,16 @@ from langchain_core.tools import tool
18
  from langchain_core.messages import SystemMessage
19
 
20
  from langchain_community.document_loaders import WikipediaLoader
21
- from langchain_community.tools import DuckDuckGoSearchRun
22
 
23
  import requests
24
  from bs4 import BeautifulSoup
 
 
 
 
 
 
25
 
26
  load_dotenv()
27
 
@@ -51,9 +60,12 @@ def wiki_search(query: str) -> str:
51
 
52
  @tool
53
  def web_search(query: str) -> str:
54
- """Search the web via DuckDuckGo for current events, recent data, or specific facts."""
55
  try:
56
- result = DuckDuckGoSearchRun(max_results=5).run(query)
 
 
 
57
  return result if result else "No results found. Try a different query."
58
  except Exception as e:
59
  return f"Web search unavailable: {type(e).__name__}. Try wiki_search instead."
@@ -74,15 +86,180 @@ def fetch_page(url: str) -> str:
74
  return f"Could not fetch page: {type(e).__name__}."
75
 
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  @tool
78
  def run_python(code: str) -> str:
79
  """Execute Python code and return stdout. Use for calculations, counting, data processing."""
 
80
  try:
81
  with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
82
  f.write(code)
83
  fname = f.name
84
  p = subprocess.run(
85
- ["python3", fname],
86
  capture_output=True,
87
  text=True,
88
  timeout=20
@@ -98,6 +275,12 @@ def run_python(code: str) -> str:
98
  return "Script timed out after 20 seconds."
99
  except Exception as e:
100
  return f"Execution failed: {type(e).__name__}."
 
 
 
 
 
 
101
 
102
 
103
  @tool
@@ -106,7 +289,7 @@ def reverse_text(text: str) -> str:
106
  return text[::-1]
107
 
108
 
109
- TOOLS = [wiki_search, web_search, fetch_page, run_python, reverse_text]
110
 
111
  # ==========================================================
112
  # MODELS — primary + ordered fallback chain
@@ -117,6 +300,9 @@ def _llm(name: str) -> ChatGroq:
117
  model=name,
118
  api_key=os.getenv("GROQ_API_KEY"),
119
  temperature=0,
 
 
 
120
  )
121
 
122
 
@@ -141,9 +327,11 @@ Produce the exact correct answer — nothing more, nothing less.
141
  - Use wiki_search for historical facts, biographies, science, geography.
142
  - Use web_search for recent events, specific articles, prices, or anything time-sensitive.
143
  - Use fetch_page when a URL is provided or a search result points to a relevant page.
 
 
144
  - Use run_python for any arithmetic, counting, sorting, or data transformation.
145
  - Use reverse_text only when asked to reverse a string.
146
- - You may use up to 5 tool calls. Stop as soon as you have a confident answer.
147
 
148
  ## Answer format rules
149
  1. Output the raw value only — no explanation, no preamble.
 
4
 
5
  import os
6
  import re
7
+ import sys
8
+ import json
9
  import tempfile
10
  import subprocess
11
+ from pathlib import Path
12
+ from urllib.parse import urlparse
13
 
14
  from dotenv import load_dotenv
15
 
 
21
  from langchain_core.messages import SystemMessage
22
 
23
  from langchain_community.document_loaders import WikipediaLoader
24
+ from langchain_community.tools import DuckDuckGoSearchResults
25
 
26
  import requests
27
  from bs4 import BeautifulSoup
28
+ import pandas as pd
29
+
30
+ try:
31
+ from youtube_transcript_api import YouTubeTranscriptApi
32
+ except Exception:
33
+ YouTubeTranscriptApi = None
34
 
35
  load_dotenv()
36
 
 
60
 
61
  @tool
62
  def web_search(query: str) -> str:
63
+ """Search the web and return compact title/url/snippet results."""
64
  try:
65
+ result = DuckDuckGoSearchResults(
66
+ max_results=5,
67
+ output_format="list",
68
+ ).run(query)
69
  return result if result else "No results found. Try a different query."
70
  except Exception as e:
71
  return f"Web search unavailable: {type(e).__name__}. Try wiki_search instead."
 
86
  return f"Could not fetch page: {type(e).__name__}."
87
 
88
 
89
+ def _youtube_video_id(text: str) -> str | None:
90
+ patterns = [
91
+ r"(?:v=|youtu\.be/|shorts/|embed/)([A-Za-z0-9_-]{11})",
92
+ r"^([A-Za-z0-9_-]{11})$",
93
+ ]
94
+ for pattern in patterns:
95
+ match = re.search(pattern, text)
96
+ if match:
97
+ return match.group(1)
98
+ return None
99
+
100
+
101
+ @tool
102
+ def youtube_transcript(video_url_or_id: str) -> str:
103
+ """Get available YouTube captions/transcript for a video URL or video id."""
104
+ if YouTubeTranscriptApi is None:
105
+ return "YouTube transcript library is unavailable."
106
+ video_id = _youtube_video_id(video_url_or_id)
107
+ if not video_id:
108
+ return "Could not identify a YouTube video id."
109
+ try:
110
+ rows = YouTubeTranscriptApi.get_transcript(video_id, languages=["en", "en-US", "en-GB"])
111
+ except Exception:
112
+ try:
113
+ rows = YouTubeTranscriptApi.get_transcript(video_id)
114
+ except Exception as e:
115
+ return f"Transcript unavailable: {type(e).__name__}. Use web_search for quotes or descriptions."
116
+ lines = []
117
+ for row in rows[:220]:
118
+ start = int(float(row.get("start", 0)))
119
+ text = " ".join(str(row.get("text", "")).split())
120
+ if text:
121
+ lines.append(f"{start}s: {text}")
122
+ return "\n".join(lines)[:7000] if lines else "Transcript is empty."
123
+
124
+
125
+ def _download_to_temp(source: str) -> tuple[Path | None, str]:
126
+ if re.fullmatch(r"[0-9a-fA-F-]{36}", source.strip()):
127
+ source = f"https://agents-course-unit4-scoring.hf.space/files/{source.strip()}"
128
+ if not source.startswith(("http://", "https://")):
129
+ return None, "Input must be a URL or benchmark task_id."
130
+
131
+ try:
132
+ r = requests.get(source, timeout=25, headers={"User-Agent": "Mozilla/5.0"})
133
+ if r.status_code == 404:
134
+ return None, "No attached file found for this task."
135
+ r.raise_for_status()
136
+ except Exception as e:
137
+ return None, f"Download failed: {type(e).__name__}."
138
+
139
+ parsed = urlparse(source)
140
+ suffix = Path(parsed.path).suffix
141
+ content_type = r.headers.get("content-type", "").lower()
142
+ if not suffix:
143
+ if "spreadsheet" in content_type or "excel" in content_type:
144
+ suffix = ".xlsx"
145
+ elif "csv" in content_type:
146
+ suffix = ".csv"
147
+ elif "pdf" in content_type:
148
+ suffix = ".pdf"
149
+ elif "python" in content_type or "text" in content_type:
150
+ suffix = ".txt"
151
+ elif "image" in content_type:
152
+ suffix = "." + content_type.split("/")[-1].split(";")[0]
153
+ else:
154
+ cd = r.headers.get("content-disposition", "")
155
+ match = re.search(r'filename="?([^";]+)', cd)
156
+ suffix = Path(match.group(1)).suffix if match else ".bin"
157
+
158
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as f:
159
+ f.write(r.content)
160
+ return Path(f.name), f"Downloaded {len(r.content)} bytes as {suffix or '.bin'}."
161
+
162
+
163
+ def _read_text_file(path: Path) -> str:
164
+ for enc in ("utf-8", "latin-1"):
165
+ try:
166
+ return path.read_text(encoding=enc)[:9000]
167
+ except Exception:
168
+ continue
169
+ return "Could not decode text file."
170
+
171
+
172
+ @tool
173
+ def inspect_file(source: str) -> str:
174
+ """Download and inspect an attached benchmark file or URL. Input can be a task_id or URL."""
175
+ path, status = _download_to_temp(source)
176
+ if path is None:
177
+ return status
178
+
179
+ try:
180
+ suffix = path.suffix.lower()
181
+
182
+ if suffix in {".py", ".txt", ".md", ".json", ".html", ".csv"}:
183
+ if suffix == ".csv":
184
+ df = pd.read_csv(path)
185
+ return _summarize_dataframe(df, "csv")
186
+ text = _read_text_file(path)
187
+ if suffix == ".py":
188
+ run = subprocess.run(
189
+ [sys.executable, str(path)],
190
+ capture_output=True,
191
+ text=True,
192
+ timeout=20,
193
+ )
194
+ stdout = run.stdout.strip()
195
+ stderr = run.stderr.strip()
196
+ return f"{status}\nPython stdout:\n{stdout[:3000] or '(empty)'}\nPython stderr:\n{stderr[:1000] or '(empty)'}\n\nCode preview:\n{text[:4000]}"
197
+ return f"{status}\n{text}"
198
+
199
+ if suffix in {".xlsx", ".xls"}:
200
+ xl = pd.ExcelFile(path)
201
+ parts = [status, f"Workbook sheets: {', '.join(xl.sheet_names)}"]
202
+ for sheet in xl.sheet_names[:4]:
203
+ df = xl.parse(sheet)
204
+ parts.append(_summarize_dataframe(df, sheet))
205
+ return "\n\n".join(parts)[:9000]
206
+
207
+ if suffix == ".pdf":
208
+ try:
209
+ from pypdf import PdfReader
210
+ reader = PdfReader(str(path))
211
+ text = "\n".join((p.extract_text() or "") for p in reader.pages[:8])
212
+ return f"{status}\nPDF pages: {len(reader.pages)}\n{text[:8500] or 'No extractable text.'}"
213
+ except Exception as e:
214
+ return f"{status}\nPDF extraction unavailable: {type(e).__name__}."
215
+
216
+ if suffix in {".png", ".jpg", ".jpeg", ".webp", ".gif"}:
217
+ return f"{status}\nImage file detected. If the question requires visual reasoning, use any visible description in the question and web_search; this runtime has no vision model."
218
+
219
+ if suffix in {".mp3", ".wav", ".m4a", ".ogg", ".flac"}:
220
+ return f"{status}\nAudio file detected. This runtime has no local speech-to-text model; use web_search if the recording is from public material."
221
+
222
+ return f"{status}\nUnsupported file type: {suffix or 'unknown'}."
223
+ except Exception as e:
224
+ return f"{status}\nInspection failed: {type(e).__name__}: {e}"
225
+ finally:
226
+ try:
227
+ path.unlink(missing_ok=True)
228
+ except Exception:
229
+ pass
230
+
231
+
232
+ def _summarize_dataframe(df: pd.DataFrame, name: str) -> str:
233
+ rows, cols = df.shape
234
+ df = df.dropna(how="all")
235
+ preview = df.head(12).to_string(index=False)
236
+ numeric = df.select_dtypes(include="number")
237
+ sums = numeric.sum(numeric_only=True).to_dict()
238
+ sums_text = json.dumps({str(k): round(float(v), 4) for k, v in sums.items()}, ensure_ascii=True)
239
+ cols_text = ", ".join(map(str, df.columns))
240
+ food_hint = ""
241
+ lower_cols = {str(c).lower(): c for c in df.columns}
242
+ category_col = next((c for key, c in lower_cols.items() if any(x in key for x in ["category", "type", "item type"])), None)
243
+ sales_col = next((c for key, c in lower_cols.items() if any(x in key for x in ["sales", "revenue", "amount", "total"])), None)
244
+ if category_col is not None and sales_col is not None:
245
+ try:
246
+ grouped = df.groupby(category_col)[sales_col].sum(numeric_only=True).to_dict()
247
+ food_hint = "\nGrouped sums: " + json.dumps({str(k): round(float(v), 2) for k, v in grouped.items()}, ensure_ascii=True)
248
+ except Exception:
249
+ pass
250
+ return f"Sheet/table {name}: {rows} rows x {cols} cols\nColumns: {cols_text}\nNumeric sums: {sums_text}{food_hint}\nPreview:\n{preview}"
251
+
252
+
253
  @tool
254
  def run_python(code: str) -> str:
255
  """Execute Python code and return stdout. Use for calculations, counting, data processing."""
256
+ fname = None
257
  try:
258
  with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
259
  f.write(code)
260
  fname = f.name
261
  p = subprocess.run(
262
+ [sys.executable, fname],
263
  capture_output=True,
264
  text=True,
265
  timeout=20
 
275
  return "Script timed out after 20 seconds."
276
  except Exception as e:
277
  return f"Execution failed: {type(e).__name__}."
278
+ finally:
279
+ if fname:
280
+ try:
281
+ Path(fname).unlink(missing_ok=True)
282
+ except Exception:
283
+ pass
284
 
285
 
286
  @tool
 
289
  return text[::-1]
290
 
291
 
292
+ TOOLS = [wiki_search, web_search, fetch_page, youtube_transcript, inspect_file, run_python, reverse_text]
293
 
294
  # ==========================================================
295
  # MODELS — primary + ordered fallback chain
 
300
  model=name,
301
  api_key=os.getenv("GROQ_API_KEY"),
302
  temperature=0,
303
+ max_tokens=384,
304
+ timeout=45,
305
+ max_retries=1,
306
  )
307
 
308
 
 
327
  - Use wiki_search for historical facts, biographies, science, geography.
328
  - Use web_search for recent events, specific articles, prices, or anything time-sensitive.
329
  - Use fetch_page when a URL is provided or a search result points to a relevant page.
330
+ - Use youtube_transcript first for YouTube questions.
331
+ - Use inspect_file whenever the question says attached file, attached image, spreadsheet, audio, Python code, or provides a task file URL.
332
  - Use run_python for any arithmetic, counting, sorting, or data transformation.
333
  - Use reverse_text only when asked to reverse a string.
334
+ - Prefer tools over guessing. Use compact searches. Stop as soon as you have a confident answer.
335
 
336
  ## Answer format rules
337
  1. Output the raw value only — no explanation, no preamble.
app.py CHANGED
@@ -29,7 +29,7 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
29
 
30
  # Single values — no per-difficulty branching
31
  TIMEOUT_SECONDS = 120
32
- RECURSION_LIMIT = 12
33
  PAUSE_SECONDS = 1.5
34
 
35
  # ==========================================================
@@ -78,9 +78,16 @@ class BenchmarkAgent:
78
  self.graph = build_graph()
79
  log("Agent ready.")
80
 
81
- def __call__(self, question: str) -> tuple[str, list, dict]:
 
 
 
 
 
 
 
82
  result = self.graph.invoke(
83
- {"messages": [HumanMessage(content=question)]},
84
  {"recursion_limit": RECURSION_LIMIT},
85
  )
86
  answer = extract_final_answer(result) or "N/A"
@@ -164,7 +171,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
164
 
165
  try:
166
  def solve():
167
- return agent(question)
168
 
169
  result, did_timeout = run_with_timeout(solve, TIMEOUT_SECONDS)
170
  elapsed = round(time.time() - start, 1)
@@ -260,8 +267,8 @@ with gr.Blocks() as demo:
260
  gr.Markdown(
261
  """
262
  **Model**: `qwen/qwen3-32b` (primary) → `llama-3.3-70b-versatile` → `llama-3.1-8b-instant`
263
- **Routing**: All questions use the same path no classification.
264
- **Timeout**: 60 s per question · **Recursion limit**: 12
265
  """
266
  )
267
 
 
29
 
30
  # Single values — no per-difficulty branching
31
  TIMEOUT_SECONDS = 120
32
+ RECURSION_LIMIT = 20
33
  PAUSE_SECONDS = 1.5
34
 
35
  # ==========================================================
 
78
  self.graph = build_graph()
79
  log("Agent ready.")
80
 
81
+ def __call__(self, question: str, task_id: str = "") -> tuple[str, list, dict]:
82
+ enriched_question = question
83
+ if task_id:
84
+ enriched_question = (
85
+ f"Task ID: {task_id}\n"
86
+ f"Attached file URL, if any: {DEFAULT_API_URL}/files/{task_id}\n\n"
87
+ f"Question: {question}"
88
+ )
89
  result = self.graph.invoke(
90
+ {"messages": [HumanMessage(content=enriched_question)]},
91
  {"recursion_limit": RECURSION_LIMIT},
92
  )
93
  answer = extract_final_answer(result) or "N/A"
 
171
 
172
  try:
173
  def solve():
174
+ return agent(question, task_id)
175
 
176
  result, did_timeout = run_with_timeout(solve, TIMEOUT_SECONDS)
177
  elapsed = round(time.time() - start, 1)
 
267
  gr.Markdown(
268
  """
269
  **Model**: `qwen/qwen3-32b` (primary) → `llama-3.3-70b-versatile` → `llama-3.1-8b-instant`
270
+ **Tools**: structured search, page fetch, YouTube transcripts, task-file inspection, Python execution
271
+ **Timeout**: 120 s per question · **Recursion limit**: 20
272
  """
273
  )
274
 
requirements.txt CHANGED
@@ -10,6 +10,7 @@ groq
10
  # Data / file processing
11
  pandas
12
  openpyxl # required by pandas for .xlsx read/write
 
13
 
14
  # Web tools
15
  requests
@@ -26,4 +27,4 @@ youtube-transcript-api
26
  gradio
27
 
28
  # Env vars
29
- python-dotenv
 
10
  # Data / file processing
11
  pandas
12
  openpyxl # required by pandas for .xlsx read/write
13
+ pypdf # lightweight PDF text extraction
14
 
15
  # Web tools
16
  requests
 
27
  gradio
28
 
29
  # Env vars
30
+ python-dotenv