asaporta commited on
Commit
23bc5d8
·
verified ·
1 Parent(s): 293ebe0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +411 -0
app.py CHANGED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import pathlib
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import Union, Optional
7
+
8
+ import openai
9
+ import requests
10
+ import gradio as gr
11
+ import pandas as pd
12
+ from tabulate import tabulate
13
+ from smolagents import OpenAIServerModel,DuckDuckGoSearchTool,CodeAgent, WikipediaSearchTool
14
+ from smolagents.tools import PipelineTool, Tool
15
+
16
+
17
+ # (Keep Constants as is)
18
+ # --- Constants ---
19
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
20
+
21
+
22
+ class SpeechToTextTool(PipelineTool):
23
+ """
24
+ Transcribes an audio file to text using the OpenAI Whisper API.
25
+ Only local file paths are supported.
26
+ """
27
+
28
+ default_checkpoint = "openai/whisper-1" # purely informational here
29
+ description = (
30
+ "This tool sends an audio file to OpenAI Whisper and returns the "
31
+ "transcribed text."
32
+ )
33
+ name = "transcriber"
34
+ inputs = {
35
+ "audio": {
36
+ "type": "string",
37
+ "description": "Absolute or relative path to a local audio file.",
38
+ }
39
+ }
40
+ output_type = "string"
41
+
42
+ # ──────────────────────────────────────────────────────────────────────────
43
+ # Public interface
44
+ # ──────────────────────────────────────────────────────────────────────────
45
+ def __call__(self, audio: str) -> str:
46
+ """
47
+ Convenience wrapper so the tool can be used like a regular function:
48
+ text = SpeechToTextTool()(path_to_audio)
49
+ """
50
+ return self._transcribe(audio)
51
+
52
+ # ──────────────────────────────────────────────────────────────────────────
53
+ # Internal helpers
54
+ # ──────────────────────────────────────────────────────────────────────────
55
+ @staticmethod
56
+ def _transcribe(audio_path: str) -> str:
57
+ # ----- validation ----------------------------------------------------
58
+ if not isinstance(audio_path, str):
59
+ raise TypeError(
60
+ "Parameter 'audio' must be a string containing the file path."
61
+ )
62
+ path = Path(audio_path).expanduser().resolve()
63
+ if not path.is_file():
64
+ raise FileNotFoundError(f"No such audio file: {path}")
65
+
66
+ # ----- API call ------------------------------------------------------
67
+ with path.open("rb") as fp:
68
+ response = openai.audio.transcriptions.create(
69
+ file=fp,
70
+ model="whisper-1", # currently the only Whisper model
71
+ response_format="text", # returns plain text instead of JSON
72
+ )
73
+
74
+ # For response_format="text", `response` is already the raw transcript
75
+ return response
76
+
77
+
78
+ class ExcelToTextTool(Tool):
79
+ """Render an Excel worksheet as Markdown text."""
80
+
81
+ # ------------------------------------------------------------------
82
+ # Required smol‑agents metadata
83
+ # ------------------------------------------------------------------
84
+ name = "excel_to_text"
85
+ description = (
86
+ "Read an Excel file and return a Markdown table of the requested sheet. "
87
+ "Accepts either the sheet name or the zero-based index."
88
+ )
89
+
90
+ inputs = {
91
+ "excel_path": {
92
+ "type": "string",
93
+ "description": "Path to the Excel file (.xlsx / .xls).",
94
+ },
95
+ "sheet_name": {
96
+ "type": "string",
97
+ "description": (
98
+ "Worksheet name or zero-based index *as a string* (optional; default first sheet)."
99
+ ),
100
+ "nullable": True,
101
+ },
102
+ }
103
+
104
+ output_type = "string"
105
+
106
+ def forward(
107
+ self,
108
+ excel_path: str,
109
+ sheet_name: Optional[str] = None,
110
+ ) -> str:
111
+ """Load *excel_path* and return the sheet as a Markdown table."""
112
+
113
+ path = pathlib.Path(excel_path).expanduser().resolve()
114
+ if not path.exists():
115
+ return f"Error: Excel file not found at {path}"
116
+
117
+ try:
118
+ # Interpret sheet identifier -----------------------------------
119
+ sheet: Union[str, int]
120
+ if sheet_name is None or sheet_name == "":
121
+ sheet = 0 # first sheet
122
+ else:
123
+ # If the user passed a numeric string (e.g. "1"), cast to int
124
+ sheet = int(sheet_name) if sheet_name.isdigit() else sheet_name
125
+
126
+ # Load worksheet ----------------------------------------------
127
+ df = pd.read_excel(path, sheet_name=sheet)
128
+
129
+ # Render to Markdown; fall back to tabulate if needed ---------
130
+ if hasattr(pd.DataFrame, "to_markdown"):
131
+ return df.to_markdown(index=False)
132
+
133
+ return tabulate(df, headers="keys", tablefmt="github", showindex=False)
134
+
135
+ except Exception as exc: # pylint: disable=broad-except
136
+ return f"Error reading Excel file: {exc}"
137
+
138
+
139
+ def download_file_if_any(base_api_url: str, task_id: str) -> str | None:
140
+ """
141
+ Try GET /files/{task_id}.
142
+ • On HTTP 200 → save to a temp dir and return local path.
143
+ • On 404 → return None.
144
+ • On other errors → raise so caller can log / handle.
145
+ """
146
+ url = f"{base_api_url}/files/{task_id}"
147
+ try:
148
+ resp = requests.get(url, timeout=30)
149
+ if resp.status_code == 404:
150
+ return None # no file
151
+ resp.raise_for_status() # raise on 4xx/5xx ≠ 404
152
+ except requests.exceptions.HTTPError as e:
153
+ # propagate non-404 errors (403, 500, …)
154
+ raise e
155
+
156
+ # ▸ Save bytes to a named file inside the system temp dir
157
+ # Try to keep original extension from Content-Disposition if present.
158
+ cdisp = resp.headers.get("content-disposition", "")
159
+ filename = task_id # default base name
160
+ if "filename=" in cdisp:
161
+ m = re.search(r'filename="([^"]+)"', cdisp)
162
+ if m:
163
+ filename = m.group(1) # keep provided name
164
+
165
+ tmp_dir = Path(tempfile.gettempdir()) / "gaia_files"
166
+ tmp_dir.mkdir(exist_ok=True)
167
+ file_path = tmp_dir / filename
168
+ with open(file_path, "wb") as f:
169
+ f.write(resp.content)
170
+ return str(file_path)
171
+
172
+
173
+ class BasicAgent:
174
+ """Basic Agent for the evaluation task."""
175
+
176
+ def __init__(self):
177
+ self.agent = CodeAgent(
178
+ model=OpenAIServerModel(model_id="gpt-4o"),
179
+ tools=[
180
+ DuckDuckGoSearchTool(),
181
+ WikipediaSearchTool(),
182
+ SpeechToTextTool(),
183
+ ExcelToTextTool(),
184
+ ],
185
+ add_base_tools=True,
186
+ additional_authorized_imports=["pandas", "numpy", "csv", "subprocess"],
187
+ )
188
+
189
+ print("BasicAgent initialized.")
190
+
191
+ def __call__(self, question: str) -> str:
192
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
193
+ fixed_answer = self.agent.run(question)
194
+ print(f"Agent returning answer: {fixed_answer}")
195
+ return fixed_answer
196
+
197
+
198
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
199
+ """
200
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
201
+ and displays the results.
202
+ """
203
+ # --- Determine HF Space Runtime URL and Repo URL ---
204
+ space_id = os.getenv("SPACE_ID")
205
+
206
+ if profile:
207
+ username = f"{profile.username}"
208
+ print(f"User logged in: {username}")
209
+ else:
210
+ print("User not logged in.")
211
+ return "Please Login to Hugging Face with the button.", None
212
+
213
+ api_url = DEFAULT_API_URL
214
+ questions_url = f"{api_url}/questions"
215
+ submit_url = f"{api_url}/submit"
216
+
217
+ try:
218
+ agent = BasicAgent()
219
+ except Exception as e: # pylint: disable=broad-except
220
+ print(f"Error instantiating agent: {e}")
221
+ return f"Error initializing agent: {e}", None
222
+ # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
223
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
224
+ print(agent_code)
225
+
226
+ # 2. Fetch Questions
227
+ print(f"Fetching questions from: {questions_url}")
228
+ try:
229
+ response = requests.get(questions_url, timeout=15)
230
+ response.raise_for_status()
231
+ questions_data = response.json()
232
+ if not questions_data:
233
+ print("Fetched questions list is empty.")
234
+ return "Fetched questions list is empty or invalid format.", None
235
+ print(f"Fetched {len(questions_data)} questions.")
236
+ except requests.exceptions.JSONDecodeError as e:
237
+ print(f"Error decoding JSON response from questions endpoint: {e}")
238
+ print(f"Response text: {response.text[:500]}")
239
+ return f"Error decoding server response for questions: {e}", None
240
+ except requests.exceptions.RequestException as e:
241
+ print(f"Error fetching questions: {e}")
242
+ return f"Error fetching questions: {e}", None
243
+ except Exception as e: # pylint: disable=broad-except
244
+ print(f"An unexpected error occurred fetching questions: {e}")
245
+ return f"An unexpected error occurred fetching questions: {e}", None
246
+
247
+ # 3. Run your Agent
248
+ results_log = []
249
+ answers_payload = []
250
+ print(f"Running agent on {len(questions_data)} questions...")
251
+ for item in questions_data:
252
+ task_id = item.get("task_id")
253
+ question_text = item.get("question")
254
+
255
+ # ----------fetch any attached file ----------
256
+ try:
257
+ file_path = download_file_if_any(api_url, task_id)
258
+ except Exception as e: # pylint: disable=broad-except
259
+ file_path = None
260
+ print(f"[file fetch error] {task_id}: {e}")
261
+
262
+ # ---------- Build the prompt sent to the agent ----------
263
+ if file_path:
264
+ q_for_agent = (
265
+ f"{question_text}\n\n"
266
+ f"---\n"
267
+ f"A file was downloaded for this task and saved locally at:\n"
268
+ f"{file_path}\n"
269
+ f"---\n\n"
270
+ )
271
+ else:
272
+ q_for_agent = question_text
273
+
274
+ if not task_id or question_text is None:
275
+ print(f"Skipping item with missing task_id or question: {item}")
276
+ continue
277
+ try:
278
+ submitted_answer = agent(q_for_agent)
279
+ answers_payload.append(
280
+ {"task_id": task_id, "submitted_answer": submitted_answer}
281
+ )
282
+ results_log.append(
283
+ {
284
+ "Task ID": task_id,
285
+ "Question": question_text,
286
+ "Submitted Answer": submitted_answer,
287
+ }
288
+ )
289
+ except Exception as e: # pylint: disable=broad-except
290
+ print(f"Error running agent on task {task_id}: {e}")
291
+ results_log.append(
292
+ {
293
+ "Task ID": task_id,
294
+ "Question": question_text,
295
+ "Submitted Answer": f"AGENT ERROR: {e}",
296
+ }
297
+ )
298
+
299
+ if not answers_payload:
300
+ print("Agent did not produce any answers to submit.")
301
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
302
+
303
+ # 4. Prepare Submission
304
+ submission_data = {
305
+ "username": username.strip(),
306
+ "agent_code": agent_code,
307
+ "answers": answers_payload,
308
+ }
309
+
310
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
311
+ print(status_update)
312
+
313
+ # 5. Submit
314
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
315
+ try:
316
+ response = requests.post(submit_url, json=submission_data, timeout=60)
317
+ response.raise_for_status()
318
+ result_data = response.json()
319
+ final_status = (
320
+ f"Submission Successful!\n"
321
+ f"User: {result_data.get('username')}\n"
322
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
323
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
324
+ f"Message: {result_data.get('message', 'No message received.')}"
325
+ )
326
+ print("Submission successful.")
327
+ results_df = pd.DataFrame(results_log)
328
+ return final_status, results_df
329
+ except requests.exceptions.HTTPError as e:
330
+ error_detail = f"Server responded with status {e.response.status_code}."
331
+ try:
332
+ error_json = e.response.json()
333
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
334
+ except requests.exceptions.JSONDecodeError:
335
+ error_detail += f" Response: {e.response.text[:500]}"
336
+ status_message = f"Submission Failed: {error_detail}"
337
+ print(status_message)
338
+ results_df = pd.DataFrame(results_log)
339
+ return status_message, results_df
340
+ except requests.exceptions.Timeout:
341
+ status_message = "Submission Failed: The request timed out."
342
+ print(status_message)
343
+ results_df = pd.DataFrame(results_log)
344
+ return status_message, results_df
345
+ except requests.exceptions.RequestException as e:
346
+ status_message = f"Submission Failed: Network error - {e}"
347
+ print(status_message)
348
+ results_df = pd.DataFrame(results_log)
349
+ return status_message, results_df
350
+ except Exception as e: # pylint: disable=broad-except
351
+ status_message = f"An unexpected error occurred during submission: {e}"
352
+ print(status_message)
353
+ results_df = pd.DataFrame(results_log)
354
+ return status_message, results_df
355
+
356
+
357
+ # --- Build Gradio Interface using Blocks ---
358
+ with gr.Blocks() as demo:
359
+ gr.Markdown("# Basic Agent Evaluation Runner")
360
+ gr.Markdown(
361
+ """
362
+ **Instructions:**
363
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
364
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
365
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
366
+ ---
367
+ **Disclaimers:**
368
+ Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
369
+ This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
370
+ """
371
+ )
372
+
373
+ gr.LoginButton()
374
+
375
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
376
+
377
+ status_output = gr.Textbox(
378
+ label="Run Status / Submission Result", lines=5, interactive=False
379
+ )
380
+ # Removed max_rows=10 from DataFrame constructor
381
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
382
+
383
+ run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
384
+
385
+ if __name__ == "__main__":
386
+ print("\n" + "-" * 30 + " App Starting " + "-" * 30)
387
+ # Check for SPACE_HOST and SPACE_ID at startup for information
388
+ space_host_startup = os.getenv("SPACE_HOST")
389
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
390
+
391
+ if space_host_startup:
392
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
393
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
394
+ else:
395
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
396
+
397
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
398
+ print(f"✅ SPACE_ID found: {space_id_startup}")
399
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
400
+ print(
401
+ f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main"
402
+ )
403
+ else:
404
+ print(
405
+ "ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined."
406
+ )
407
+
408
+ print("-" * (60 + len(" App Starting ")) + "\n")
409
+
410
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
411
+ demo.launch(debug=True, share=False)