kenqia commited on
Commit
da4480d
·
verified ·
1 Parent(s): 8f75113

Update tools.py

Browse files
Files changed (1) hide show
  1. tools.py +303 -41
tools.py CHANGED
@@ -1,61 +1,323 @@
 
 
 
 
 
 
 
 
1
  import pandas as pd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def answer_excel_question(self, question: str, file_path: str) -> str:
4
- df = pd.read_excel(file_path)
5
 
6
- prompt = f"""
7
- Question:
8
- {question}
9
-
10
- Excel data:
11
- {df.to_csv(index=False)}
12
-
13
- Answer directly. Return only the final answer.
 
 
 
 
 
 
 
 
14
  """
 
 
 
 
 
15
 
16
- response = self.model.invoke([
17
- SystemMessage(content="You are a precise data analysis assistant."),
18
- HumanMessage(content=prompt)
19
- ])
20
 
21
- return response.content.strip()
 
 
 
 
 
 
22
 
 
23
 
24
- import subprocess
25
 
26
- def answer_python_question(self, question: str, file_path: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  try:
28
  result = subprocess.run(
29
- ["python", file_path],
 
30
  capture_output=True,
31
  text=True,
32
  timeout=10,
33
  )
34
- output = result.stdout.strip()
35
- if output:
36
- return output.splitlines()[-1].strip()
37
- return result.stderr.strip()
 
 
 
 
 
 
 
 
 
 
 
38
  except Exception as e:
39
- return f""
40
 
41
 
42
- from youtube_transcript_api import YouTubeTranscriptApi
43
- from urllib.parse import urlparse, parse_qs
44
-
45
- def extract_youtube_id(self, url: str) -> str | None:
46
- parsed = urlparse(url)
47
- if parsed.hostname in ["www.youtube.com", "youtube.com"]:
48
- return parse_qs(parsed.query).get("v", [None])[0]
49
- if parsed.hostname == "youtu.be":
50
- return parsed.path.lstrip("/")
51
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- def get_youtube_transcript(self, question: str) -> str:
54
- import re
55
- urls = re.findall(r"https?://\S+", question)
56
  for url in urls:
57
- video_id = self.extract_youtube_id(url)
58
- if video_id:
59
- transcript = YouTubeTranscriptApi.get_transcript(video_id)
60
- return " ".join([x["text"] for x in transcript])
61
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import requests
9
  import pandas as pd
10
+ from langchain_core.tools import tool
11
+ from youtube_transcript_api import YouTubeTranscriptApi
12
+
13
+
14
+ DEFAULT_API_URL = os.getenv(
15
+ "AGENT_COURSE_API_URL",
16
+ "https://agents-course-unit4-scoring.hf.space",
17
+ )
18
+
19
+ CACHE_DIR = Path("/tmp/agent_course_files")
20
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
21
+
22
+
23
+ def _safe_filename(name: str) -> str:
24
+ """Make a filename safe for local storage."""
25
+ return re.sub(r"[^a-zA-Z0-9._-]+", "_", name).strip("_") or "file"
26
+
27
+
28
+ def _filename_from_headers(task_id: str, response: requests.Response) -> str:
29
+ """Infer filename from Content-Disposition or fallback to task_id."""
30
+ content_disposition = response.headers.get("content-disposition", "")
31
+
32
+ match = re.search(r'filename="?([^";]+)"?', content_disposition)
33
+ if match:
34
+ return _safe_filename(match.group(1))
35
+
36
+ content_type = response.headers.get("content-type", "").lower()
37
+
38
+ ext = ""
39
+ if "spreadsheet" in content_type or "excel" in content_type:
40
+ ext = ".xlsx"
41
+ elif "csv" in content_type:
42
+ ext = ".csv"
43
+ elif "python" in content_type or "text" in content_type:
44
+ ext = ".txt"
45
+ elif "audio" in content_type:
46
+ ext = ".mp3"
47
+ elif "image/png" in content_type:
48
+ ext = ".png"
49
+ elif "image/jpeg" in content_type:
50
+ ext = ".jpg"
51
+
52
+ return _safe_filename(task_id) + ext
53
+
54
+
55
+ def _download_file_by_task_id(task_id: str) -> Optional[Path]:
56
+ """Download attached task file from the official scoring API."""
57
+ if not task_id:
58
+ return None
59
+
60
+ url = f"{DEFAULT_API_URL}/files/{task_id}"
61
+
62
+ try:
63
+ response = requests.get(url, timeout=30)
64
+
65
+ if response.status_code == 404:
66
+ return None
67
+
68
+ response.raise_for_status()
69
+
70
+ filename = _filename_from_headers(task_id, response)
71
+ file_path = CACHE_DIR / filename
72
+
73
+ file_path.write_bytes(response.content)
74
+ return file_path
75
 
76
+ except Exception:
77
+ return None
78
 
79
+
80
+ def _resolve_file(task_id: str = "", file_path: str = "") -> Optional[Path]:
81
+ """Resolve either a local file path or a task_id into a real local Path."""
82
+ if file_path:
83
+ path = Path(file_path)
84
+ if path.exists():
85
+ return path
86
+
87
+ if task_id:
88
+ return _download_file_by_task_id(task_id)
89
+
90
+ return None
91
+
92
+
93
+ @tool
94
+ def download_task_file(task_id: str) -> str:
95
  """
96
+ Download the attached file for a Hugging Face Agent Course task_id.
97
+ Use this when a question mentions an attached file, image, audio, spreadsheet, or code file.
98
+ Returns the local file path and basic file information.
99
+ """
100
+ path = _download_file_by_task_id(task_id)
101
 
102
+ if path is None:
103
+ return f"No attached file found for task_id={task_id}."
 
 
104
 
105
+ info = {
106
+ "task_id": task_id,
107
+ "file_path": str(path),
108
+ "filename": path.name,
109
+ "suffix": path.suffix,
110
+ "size_bytes": path.stat().st_size,
111
+ }
112
 
113
+ return json.dumps(info, ensure_ascii=False)
114
 
 
115
 
116
+ @tool
117
+ def read_attached_text_file(task_id: str = "", file_path: str = "", max_chars: int = 12000) -> str:
118
+ """
119
+ Read the text content of an attached file.
120
+ Use this for .txt, .py, .csv, .md, .json, or other plain-text attachments.
121
+ Provide either task_id or file_path.
122
+ """
123
+ path = _resolve_file(task_id=task_id, file_path=file_path)
124
+
125
+ if path is None:
126
+ return "No file could be resolved from the given task_id or file_path."
127
+
128
+ try:
129
+ text = path.read_text(encoding="utf-8", errors="replace")
130
+ return text[:max_chars]
131
+ except Exception as e:
132
+ return f"Failed to read file {path}: {e}"
133
+
134
+
135
+ @tool
136
+ def answer_python_question(task_id: str = "", file_path: str = "") -> str:
137
+ """
138
+ Execute an attached Python file and return its final output.
139
+ Use this when the question asks for the final numeric output from attached Python code.
140
+ Provide either task_id or file_path.
141
+ """
142
+ path = _resolve_file(task_id=task_id, file_path=file_path)
143
+
144
+ if path is None:
145
+ return "No Python file could be resolved from the given task_id or file_path."
146
+
147
  try:
148
  result = subprocess.run(
149
+ ["python", str(path)],
150
+ cwd=str(path.parent),
151
  capture_output=True,
152
  text=True,
153
  timeout=10,
154
  )
155
+
156
+ stdout = result.stdout.strip()
157
+ stderr = result.stderr.strip()
158
+
159
+ if stdout:
160
+ lines = [line.strip() for line in stdout.splitlines() if line.strip()]
161
+ return lines[-1] if lines else stdout
162
+
163
+ if stderr:
164
+ return f"Python execution produced no stdout. stderr:\n{stderr[-2000:]}"
165
+
166
+ return "Python execution finished with no output."
167
+
168
+ except subprocess.TimeoutExpired:
169
+ return "Python execution timed out."
170
  except Exception as e:
171
+ return f"Failed to execute Python file: {e}"
172
 
173
 
174
+ @tool
175
+ def answer_excel_question(task_id: str = "", file_path: str = "", question: str = "") -> str:
176
+ """
177
+ Read an attached Excel or CSV file and return compact spreadsheet data for answering the question.
178
+ Use this for questions about attached .xlsx, .xls, or .csv files.
179
+ Provide task_id when available. Also include the original question.
180
+ """
181
+ path = _resolve_file(task_id=task_id, file_path=file_path)
182
+
183
+ if path is None:
184
+ return "No spreadsheet file could be resolved from the given task_id or file_path."
185
+
186
+ try:
187
+ suffix = path.suffix.lower()
188
+
189
+ if suffix == ".csv":
190
+ sheets = {"sheet1": pd.read_csv(path)}
191
+ else:
192
+ sheets = pd.read_excel(path, sheet_name=None)
193
+
194
+ outputs = []
195
+
196
+ for sheet_name, df in sheets.items():
197
+ df = df.copy()
198
+
199
+ outputs.append(f"Sheet: {sheet_name}")
200
+ outputs.append(f"Shape: {df.shape[0]} rows x {df.shape[1]} columns")
201
+ outputs.append(f"Columns: {list(df.columns)}")
202
+
203
+ preview_csv = df.head(80).to_csv(index=False)
204
+ outputs.append("Preview CSV:")
205
+ outputs.append(preview_csv)
206
+
207
+ numeric_summary = df.select_dtypes(include="number").sum(numeric_only=True)
208
+ if not numeric_summary.empty:
209
+ outputs.append("Numeric column sums:")
210
+ outputs.append(numeric_summary.to_string())
211
+
212
+ # Heuristic helper for common “food excluding drinks” sales question.
213
+ q = question.lower()
214
+ if "food" in q and ("drink" in q or "drinks" in q):
215
+ possible_category_cols = [
216
+ col for col in df.columns
217
+ if any(key in str(col).lower() for key in ["category", "type", "item", "name", "menu"])
218
+ ]
219
+ possible_value_cols = [
220
+ col for col in df.columns
221
+ if any(key in str(col).lower() for key in ["sales", "revenue", "amount", "total", "usd", "price"])
222
+ ]
223
+
224
+ if possible_category_cols and possible_value_cols:
225
+ category_col = possible_category_cols[0]
226
+ value_col = possible_value_cols[0]
227
+
228
+ values = (
229
+ df[value_col]
230
+ .astype(str)
231
+ .str.replace(r"[$,]", "", regex=True)
232
+ )
233
+ values = pd.to_numeric(values, errors="coerce").fillna(0)
234
+
235
+ drink_pattern = r"drink|drinks|beverage|soda|coffee|tea|juice|water|milkshake|shake|smoothie"
236
+ food_mask = ~df[category_col].astype(str).str.lower().str.contains(
237
+ drink_pattern,
238
+ regex=True,
239
+ na=False,
240
+ )
241
+
242
+ total_food_sales = values[food_mask].sum()
243
+ outputs.append(
244
+ f"Heuristic food-not-drinks total using category column "
245
+ f"'{category_col}' and value column '{value_col}': "
246
+ f"${total_food_sales:.2f}"
247
+ )
248
+
249
+ return "\n\n".join(outputs)[:20000]
250
+
251
+ except Exception as e:
252
+ return f"Failed to read spreadsheet {path}: {e}"
253
+
254
+
255
+ def _extract_youtube_id(text: str) -> Optional[str]:
256
+ """Extract YouTube video ID from a URL or text containing a URL."""
257
+ urls = re.findall(r"https?://[^\s)]+", text)
258
 
 
 
 
259
  for url in urls:
260
+ if "youtube.com/watch" in url:
261
+ match = re.search(r"[?&]v=([^&\s]+)", url)
262
+ if match:
263
+ return match.group(1)
264
+
265
+ if "youtu.be/" in url:
266
+ match = re.search(r"youtu\.be/([^?&\s]+)", url)
267
+ if match:
268
+ return match.group(1)
269
+
270
+ return None
271
+
272
+
273
+ def _transcript_to_text(transcript) -> str:
274
+ """Convert transcript result to plain text, compatible with old/new youtube-transcript-api."""
275
+ pieces = []
276
+
277
+ for item in transcript:
278
+ if isinstance(item, dict):
279
+ text = item.get("text", "")
280
+ else:
281
+ text = getattr(item, "text", "")
282
+
283
+ if text:
284
+ pieces.append(text.replace("\n", " ").strip())
285
+
286
+ return " ".join(pieces)
287
+
288
+
289
+ @tool
290
+ def get_youtube_transcript(url_or_question: str) -> str:
291
+ """
292
+ Get the transcript text of a YouTube video from a URL or a question containing a YouTube URL.
293
+ Use this when the question asks what someone says in a YouTube video.
294
+ This may not help for purely visual questions about video frames.
295
+ """
296
+ video_id = _extract_youtube_id(url_or_question)
297
+
298
+ if not video_id:
299
+ return "No YouTube video ID found."
300
+
301
+ try:
302
+ # Compatibility with older youtube-transcript-api versions.
303
+ try:
304
+ transcript = YouTubeTranscriptApi.get_transcript(
305
+ video_id,
306
+ languages=["en", "en-US", "en-GB"],
307
+ )
308
+ except AttributeError:
309
+ api = YouTubeTranscriptApi()
310
+ transcript = api.fetch(
311
+ video_id,
312
+ languages=["en", "en-US", "en-GB"],
313
+ )
314
+
315
+ text = _transcript_to_text(transcript)
316
+
317
+ if not text:
318
+ return "Transcript was found but is empty."
319
+
320
+ return text[:20000]
321
+
322
+ except Exception as e:
323
+ return f"Failed to fetch YouTube transcript for video_id={video_id}: {e}"