Gabe commited on
Commit
26e9610
·
1 Parent(s): 1e6b798
Files changed (1) hide show
  1. app.py +639 -225
app.py CHANGED
@@ -24,220 +24,11 @@ from langgraph.prebuilt import tools_condition
24
  from langchain_huggingface import ChatHuggingFace
25
  from langchain_huggingface import HuggingFaceEndpoint
26
  from langchain_community.tools import DuckDuckGoSearchRun
27
- from langchain_core.tools import tool
28
 
29
  # (Keep Constants as is)
30
  # --- Constants ---
31
- DEFAULT_API_URL = "[https://agents-course-unit4-scoring.hf.space](https://agents-course-unit4-scoring.hf.space)"
32
-
33
- # --- Initialize ASR Pipeline (for Audio Tool) ---
34
- # Load the model once when the app starts for efficiency
35
- try:
36
- asr_pipeline = pipeline(
37
- "automatic-speech-recognition",
38
- model="openai/whisper-base",
39
- torch_dtype=torch.float16, # Use float16 for faster inference
40
- device_map="auto" # Use GPU if available
41
- )
42
- print("✅ ASR (Whisper) pipeline loaded successfully.")
43
- except Exception as e:
44
- print(f"⚠️ Warning: Could not load ASR pipeline. Audio tool will not work. Error: {e}")
45
- asr_pipeline = None
46
-
47
- # --- Tool Definitions ---
48
-
49
- @tool
50
- def search_tool(query: str) -> str:
51
- """Calls DuckDuckGo search and returns the results. Use this for recent information or general web searches."""
52
- print(f"--- Calling Search Tool with query: {query} ---")
53
- try:
54
- search = DuckDuckGoSearchRun()
55
- return search.run(query)
56
- except Exception as e:
57
- return f"Error running search: {e}"
58
-
59
- @tool
60
- def code_interpreter(code: str) -> str:
61
- """
62
- Executes a string of Python code and returns its stdout, stderr, and any error.
63
- Use this for calculations, data manipulation (including pandas on dataframes read from files), list operations, string manipulations, or any other Python operation.
64
- The code runs in a sandboxed environment. 'pandas' (as pd) and 'openpyxl' are available.
65
- Ensure the code is complete and executable. If printing, use print().
66
- """
67
- print(f"--- Calling Code Interpreter with code:\n{code}\n---")
68
- output_stream = io.StringIO()
69
- error_stream = io.StringIO()
70
-
71
- try:
72
- # Use contextlib to redirect stdout and stderr
73
- with contextlib.redirect_stdout(output_stream), contextlib.redirect_stderr(error_stream):
74
- # Execute the code. Provide 'pd' (pandas) in the globals
75
- exec(code, {"pd": pd}, {})
76
-
77
- stdout = output_stream.getvalue()
78
- stderr = error_stream.getvalue()
79
-
80
- if stderr:
81
- return f"Error: {stderr}\nStdout: {stdout}"
82
- if stdout:
83
- return f"Success:\n{stdout}"
84
- return "Success: Code executed without error and produced no stdout."
85
-
86
- except Exception as e:
87
- # Capture any exception during exec
88
- return f"Execution failed with error: {str(e)}"
89
-
90
- @tool
91
- def read_file(path: str) -> str:
92
- """Reads the content of a file at the specified path. Use this to examine files provided in the question."""
93
- print(f"--- Calling Read File Tool at path: {path} ---")
94
- try:
95
- # Try finding the file relative to the app directory first
96
- script_dir = os.path.dirname(__file__)
97
- full_path = os.path.join(script_dir, path)
98
- if not os.path.exists(full_path):
99
- # If not found, try the direct path (might be absolute or relative to cwd)
100
- full_path = path
101
- if not os.path.exists(full_path):
102
- # Try basename for GAIA questions providing just the filename
103
- if os.path.exists(os.path.basename(path)):
104
- full_path = os.path.basename(path)
105
- else:
106
- return f"Error: File not found at '{path}', '{os.path.join(script_dir, path)}', or '{os.path.basename(path)}'"
107
-
108
- with open(full_path, 'r', encoding='utf-8') as f:
109
- return f.read()
110
- except Exception as e:
111
- return f"Error reading file {path}: {str(e)}"
112
-
113
- @tool
114
- def write_file(path: str, content: str) -> str:
115
- """Writes the given content to a file at the specified path. Creates directories if they don't exist."""
116
- print(f"--- Calling Write File Tool at path: {path} ---")
117
- try:
118
- # Ensure the directory exists
119
- full_path = os.path.join(os.path.dirname(__file__), path) # Write relative to script dir
120
- os.makedirs(os.path.dirname(full_path), exist_ok=True)
121
-
122
- with open(full_path, 'w', encoding='utf-8') as f:
123
- f.write(content)
124
- return f"Successfully wrote to file {path} (relative to app)."
125
- except Exception as e:
126
- return f"Error writing to file {path}: {str(e)}"
127
-
128
- @tool
129
- def list_directory(path: str = ".") -> str:
130
- """Lists the contents (files and directories) of a directory at the specified path relative to the app."""
131
- print(f"--- Calling List Directory Tool at path: {path} ---")
132
- try:
133
- full_path = os.path.join(os.path.dirname(__file__), path) # List relative to script dir
134
- files = os.listdir(full_path)
135
- return "\n".join(files) if files else "Directory is empty."
136
- except Exception as e:
137
- return f"Error listing directory {path}: {str(e)}"
138
-
139
- @tool
140
- def audio_transcription_tool(file_path: str) -> str:
141
- """
142
- Transcribes an audio file (like .mp3 or .wav) using Whisper and returns the text content.
143
- Use this for questions involving audio file analysis.
144
- """
145
- print(f"--- Calling Audio Transcription Tool at path: {file_path} ---")
146
- if not asr_pipeline:
147
- return "Error: Audio transcription pipeline is not available."
148
- try:
149
- # Try finding the file relative to the app directory first
150
- script_dir = os.path.dirname(__file__)
151
- full_path = os.path.join(script_dir, file_path)
152
- if not os.path.exists(full_path):
153
- # If not found, try the direct path
154
- full_path = file_path
155
- if not os.path.exists(full_path):
156
- # Try basename for GAIA questions
157
- if os.path.exists(os.path.basename(file_path)):
158
- full_path = os.path.basename(file_path)
159
- else:
160
- return f"Error: Audio file not found at '{file_path}', '{os.path.join(script_dir, file_path)}', or '{os.path.basename(file_path)}'"
161
-
162
- # The pipeline handles file loading
163
- transcription = asr_pipeline(full_path)
164
- print("--- Transcription Complete ---")
165
- return transcription["text"]
166
- except Exception as e:
167
- return f"Error during audio transcription: {str(e)}"
168
-
169
- @tool
170
- def get_youtube_transcript(video_url: str) -> str:
171
- """
172
- Fetches the transcript for a given YouTube video URL. Use this for questions about YouTube video content.
173
- """
174
- print(f"--- Calling YouTube Transcript Tool for URL: {video_url} ---")
175
- try:
176
- # Extract video ID from URL more robustly
177
- video_id = None
178
- if "watch?v=" in video_url:
179
- video_id = video_url.split("v=")[1].split("&")[0]
180
- elif "youtu.be/" in video_url:
181
- video_id = video_url.split("youtu.be/")[1].split("?")[0]
182
-
183
- if not video_id:
184
- return f"Error: Could not extract video ID from URL: {video_url}"
185
-
186
- transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
187
-
188
- # Combine all transcript parts into one string
189
- full_transcript = " ".join([item["text"] for item in transcript_list])
190
- print("--- Transcript Fetched ---")
191
- # Return a limited amount to avoid overwhelming the context
192
- return full_transcript[:8000]
193
- except Exception as e:
194
- return f"Error fetching YouTube transcript: {str(e)}"
195
-
196
- @tool
197
- def scrape_web_page(url: str) -> str:
198
- """
199
- Fetches the primary text content of a given web page URL, removing navigation, footer, scripts, and styles.
200
- Use this when you need the full content of a webpage found via search.
201
- """
202
- print(f"--- Calling Web Scraper Tool for URL: {url} ---")
203
- try:
204
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
205
- response = requests.get(url, headers=headers, timeout=15) # Increased timeout
206
- response.raise_for_status() # Raise an error for bad responses (4xx or 5xx)
207
-
208
- # Check content type to avoid parsing non-HTML
209
- if 'html' not in response.headers.get('Content-Type', '').lower():
210
- return f"Error: URL {url} did not return HTML content."
211
-
212
- soup = BeautifulSoup(response.text, 'html.parser')
213
-
214
- # Remove common non-content tags
215
- for tag in soup(["script", "style", "nav", "footer", "aside", "header", "form"]):
216
- tag.extract()
217
-
218
- # Attempt to find the main content area (heuristics, may not always work)
219
- main_content = soup.find('main') or soup.find('article') or soup.find('div', role='main') or soup.body
220
- if not main_content:
221
- main_content = soup # Fallback to the whole soup if no main area found
222
-
223
- text = main_content.get_text(separator='\n', strip=True)
224
-
225
- # Clean up excessive whitespace
226
- lines = (line.strip() for line in text.splitlines())
227
- chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
228
- text = '\n'.join(chunk for chunk in chunks if chunk)
229
-
230
- print("--- Web Page Scraped ---")
231
- # Limit context size
232
- return text[:8000]
233
-
234
- except requests.exceptions.RequestException as e:
235
- return f"Error fetching web page {url}: {str(e)}"
236
- except Exception as e:
237
- return f"Error scraping web page {url}: {str(e)}"
238
-
239
- # --- End of Tool Definitions ---
240
-
241
 
242
  # --- LangGraph Agent State ---
243
  class AgentState(TypedDict):
@@ -248,27 +39,273 @@ class AgentState(TypedDict):
248
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
249
  class BasicAgent:
250
 
251
- def __init__(self):
252
- print("BasicAgent (LangGraph) initialized.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
- # 1. Get API Token from Space Secrets
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  HUGGINGFACEHUB_API_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN")
256
  if not HUGGINGFACEHUB_API_TOKEN:
257
  raise ValueError("HUGGINGFACEHUB_API_TOKEN secret is not set! Please add it to your Space secrets.")
258
 
259
- # 2. Initialize Tools
 
 
260
  self.tools = [
261
- search_tool,
262
- code_interpreter,
263
- read_file,
264
- write_file,
265
- list_directory,
266
- audio_transcription_tool,
267
- get_youtube_transcript,
268
- scrape_web_page
269
  ]
270
 
271
- # 3. Define the Improved System Prompt
272
  tool_descriptions = "\n".join([f"- {tool.name}: {tool.description}" for tool in self.tools])
273
  self.system_prompt = f"""You are a highly intelligent and meticulous AI assistant built to answer questions from the GAIA benchmark.
274
  Your primary goal is to provide **only the concise, factual, and direct answer** to the user's question, exactly matching the format required by the benchmark (e.g., a name, a number, a specific string format, a comma-separated list).
@@ -290,4 +327,381 @@ You have access to the following tools to gather information and perform actions
290
  "tool_input": {{ "arg_name1": "value1", "arg_name2": "value2", ... }}
291
  }}
292
  ```
293
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  from langchain_huggingface import ChatHuggingFace
25
  from langchain_huggingface import HuggingFaceEndpoint
26
  from langchain_community.tools import DuckDuckGoSearchRun
27
+ from langchain_core.tools import tool, BaseTool
28
 
29
  # (Keep Constants as is)
30
  # --- Constants ---
31
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  # --- LangGraph Agent State ---
34
  class AgentState(TypedDict):
 
39
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
40
  class BasicAgent:
41
 
42
+ # --- Tool Definitions as Methods ---
43
+ # By making tools methods, they can access self.asr_pipeline
44
+
45
+ @tool
46
+ def search_tool(self, query: str) -> str:
47
+ """Calls DuckDuckGo search and returns the results. Use this for recent information or general web searches."""
48
+ print(f"--- Calling Search Tool with query: {query} ---")
49
+ try:
50
+ search = DuckDuckGoSearchRun()
51
+ return search.run(query)
52
+ except Exception as e:
53
+ return f"Error running search: {e}"
54
+
55
+ @tool
56
+ def code_interpreter(self, code: str) -> str:
57
+ """
58
+ Executes a string of Python code and returns its stdout, stderr, and any error.
59
+ Use this for calculations, data manipulation (including pandas on dataframes read from files), list operations, string manipulations, or any other Python operation.
60
+ The code runs in a sandboxed environment. 'pandas' (as pd) and 'openpyxl' are available.
61
+ Ensure the code is complete and executable. If printing, use print().
62
+ """
63
+ print(f"--- Calling Code Interpreter with code:\n{code}\n---")
64
+ output_stream = io.StringIO()
65
+ error_stream = io.StringIO()
66
+
67
+ try:
68
+ # Use contextlib to redirect stdout and stderr
69
+ with contextlib.redirect_stdout(output_stream), contextlib.redirect_stderr(error_stream):
70
+ # Execute the code. Provide 'pd' (pandas) in the globals
71
+ exec(code, {"pd": pd}, {})
72
+
73
+ stdout = output_stream.getvalue()
74
+ stderr = error_stream.getvalue()
75
+
76
+ if stderr:
77
+ return f"Error: {stderr}\nStdout: {stdout}"
78
+ if stdout:
79
+ return f"Success:\n{stdout}"
80
+ return "Success: Code executed without error and produced no stdout."
81
+
82
+ except Exception as e:
83
+ # Capture any exception during exec
84
+ return f"Execution failed with error: {str(e)}"
85
+
86
+ @tool
87
+ def read_file(self, path: str) -> str:
88
+ """Reads the content of a file at the specified path. Use this to examine files provided in the question."""
89
+ print(f"--- Calling Read File Tool at path: {path} ---")
90
+ try:
91
+ # Try finding the file relative to the app directory first
92
+ script_dir = os.path.dirname(os.path.abspath(__file__)) # Use absolute path
93
+ full_path = os.path.join(script_dir, path)
94
+ print(f"Attempting to read relative path: {full_path}")
95
+ if not os.path.exists(full_path):
96
+ # If not found, try the direct path (might be absolute or relative to cwd)
97
+ full_path = path
98
+ print(f"Attempting to read direct path: {full_path}")
99
+ if not os.path.exists(full_path):
100
+ # Try basename for GAIA questions providing just the filename
101
+ base_path = os.path.basename(path)
102
+ print(f"Attempting to read basename path: {base_path}")
103
+ if os.path.exists(base_path):
104
+ full_path = base_path
105
+ else:
106
+ # List files in current and script directory for debugging
107
+ cwd_files = os.listdir(".")
108
+ script_dir_files = os.listdir(script_dir)
109
+ return (f"Error: File not found.\n"
110
+ f"Tried: '{path}', '{os.path.join(script_dir, path)}', '{base_path}'.\n"
111
+ f"Files in current dir (.): {cwd_files}\n"
112
+ f"Files in script dir ({script_dir}): {script_dir_files}")
113
+
114
+ print(f"Reading file: {full_path}")
115
+ with open(full_path, 'r', encoding='utf-8') as f:
116
+ return f.read()
117
+ except Exception as e:
118
+ return f"Error reading file {path}: {str(e)}"
119
+
120
+ @tool
121
+ def write_file(self, path: str, content: str) -> str:
122
+ """Writes the given content to a file at the specified path relative to the app's directory. Creates directories if they don't exist."""
123
+ print(f"--- Calling Write File Tool at path: {path} ---")
124
+ try:
125
+ # Ensure the directory exists
126
+ script_dir = os.path.dirname(os.path.abspath(__file__))
127
+ full_path = os.path.join(script_dir, path) # Write relative to script dir
128
+ print(f"Writing file to: {full_path}")
129
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
130
+
131
+ with open(full_path, 'w', encoding='utf-8') as f:
132
+ f.write(content)
133
+ return f"Successfully wrote to file {path} (relative to app)."
134
+ except Exception as e:
135
+ return f"Error writing to file {path}: {str(e)}"
136
+
137
+ @tool
138
+ def list_directory(self, path: str = ".") -> str:
139
+ """Lists the contents (files and directories) of a directory at the specified path relative to the app's directory."""
140
+ print(f"--- Calling List Directory Tool at path: {path} ---")
141
+ try:
142
+ script_dir = os.path.dirname(os.path.abspath(__file__))
143
+ full_path = os.path.join(script_dir, path) # List relative to script dir
144
+ print(f"Listing directory: {full_path}")
145
+ if not os.path.isdir(full_path):
146
+ return f"Error: '{path}' is not a valid directory relative to the app."
147
+ files = os.listdir(full_path)
148
+ return "\n".join(files) if files else "Directory is empty."
149
+ except Exception as e:
150
+ return f"Error listing directory {path}: {str(e)}"
151
+
152
+ @tool
153
+ def audio_transcription_tool(self, file_path: str) -> str:
154
+ """
155
+ Transcribes an audio file (like .mp3 or .wav) using Whisper and returns the text content.
156
+ Use this for questions involving audio file analysis.
157
+ """
158
+ print(f"--- Calling Audio Transcription Tool at path: {file_path} ---")
159
+ # Access the pipeline via self
160
+ if not self.asr_pipeline:
161
+ return "Error: Audio transcription pipeline is not available."
162
+ try:
163
+ # Try finding the file relative to the app directory first
164
+ script_dir = os.path.dirname(os.path.abspath(__file__))
165
+ full_path = os.path.join(script_dir, file_path)
166
+ print(f"Attempting to transcribe relative path: {full_path}")
167
+ if not os.path.exists(full_path):
168
+ # If not found, try the direct path
169
+ full_path = file_path
170
+ print(f"Attempting to transcribe direct path: {full_path}")
171
+ if not os.path.exists(full_path):
172
+ # Try basename for GAIA questions
173
+ base_path = os.path.basename(file_path)
174
+ print(f"Attempting to transcribe basename path: {base_path}")
175
+ if os.path.exists(base_path):
176
+ full_path = base_path
177
+ else:
178
+ cwd_files = os.listdir(".")
179
+ script_dir_files = os.listdir(script_dir)
180
+ return (f"Error: Audio file not found.\n"
181
+ f"Tried: '{file_path}', '{os.path.join(script_dir, file_path)}', '{base_path}'.\n"
182
+ f"Files in current dir (.): {cwd_files}\n"
183
+ f"Files in script dir ({script_dir}): {script_dir_files}")
184
+
185
+ print(f"Transcribing file: {full_path}")
186
+ # Use self.asr_pipeline
187
+ transcription = self.asr_pipeline(full_path)
188
+ print("--- Transcription Complete ---")
189
+ return transcription["text"]
190
+ except Exception as e:
191
+ return f"Error during audio transcription: {str(e)}"
192
+
193
+ @tool
194
+ def get_youtube_transcript(self, video_url: str) -> str:
195
+ """
196
+ Fetches the transcript for a given YouTube video URL. Use this for questions about YouTube video content.
197
+ """
198
+ print(f"--- Calling YouTube Transcript Tool for URL: {video_url} ---")
199
+ try:
200
+ # Extract video ID from URL more robustly
201
+ video_id = None
202
+ if "watch?v=" in video_url:
203
+ video_id = video_url.split("v=")[1].split("&")[0]
204
+ elif "youtu.be/" in video_url:
205
+ video_id = video_url.split("youtu.be/")[1].split("?")[0]
206
+
207
+ if not video_id:
208
+ return f"Error: Could not extract video ID from URL: {video_url}"
209
+
210
+ transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
211
+
212
+ # Combine all transcript parts into one string
213
+ full_transcript = " ".join([item["text"] for item in transcript_list])
214
+ print("--- Transcript Fetched ---")
215
+ # Return a limited amount to avoid overwhelming the context
216
+ return full_transcript[:8000]
217
+ except Exception as e:
218
+ return f"Error fetching YouTube transcript: {str(e)}"
219
+
220
+ @tool
221
+ def scrape_web_page(self, url: str) -> str:
222
+ """
223
+ Fetches the primary text content of a given web page URL, removing navigation, footer, scripts, and styles.
224
+ Use this when you need the full content of a webpage found via search.
225
+ """
226
+ print(f"--- Calling Web Scraper Tool for URL: {url} ---")
227
+ try:
228
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
229
+ response = requests.get(url, headers=headers, timeout=15) # Increased timeout
230
+ response.raise_for_status() # Raise an error for bad responses (4xx or 5xx)
231
+
232
+ # Check content type to avoid parsing non-HTML
233
+ if 'html' not in response.headers.get('Content-Type', '').lower():
234
+ return f"Error: URL {url} did not return HTML content."
235
+
236
+ soup = BeautifulSoup(response.text, 'html.parser')
237
+
238
+ # Remove common non-content tags
239
+ for tag in soup(["script", "style", "nav", "footer", "aside", "header", "form"]):
240
+ tag.extract()
241
+
242
+ # Attempt to find the main content area (heuristics, may not always work)
243
+ main_content = soup.find('main') or soup.find('article') or soup.find('div', role='main') or soup.body
244
+ if not main_content:
245
+ main_content = soup # Fallback to the whole soup if no main area found
246
+
247
+ text = main_content.get_text(separator='\n', strip=True)
248
+
249
+ # Clean up excessive whitespace
250
+ lines = (line.strip() for line in text.splitlines())
251
+ chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
252
+ text = '\n'.join(chunk for chunk in chunks if chunk)
253
+
254
+ print("--- Web Page Scraped ---")
255
+ # Limit context size
256
+ return text[:8000]
257
+
258
+ except requests.exceptions.RequestException as e:
259
+ return f"Error fetching web page {url}: {str(e)}"
260
+ except Exception as e:
261
+ return f"Error scraping web page {url}: {str(e)}"
262
+
263
+ # --- End of Tool Definitions ---
264
 
265
+
266
+ def __init__(self):
267
+ print("BasicAgent (LangGraph) initializing...")
268
+
269
+ # 1. Initialize ASR Pipeline *inside* init - DELAYED LOADING
270
+ # ==================== MOVED HERE ====================
271
+ self.asr_pipeline = None # Initialize as None first
272
+ try:
273
+ print("Loading ASR (Whisper) pipeline...")
274
+ # Decide device based on availability
275
+ device = "cuda:0" if torch.cuda.is_available() else "cpu"
276
+ print(f"Using device: {device} for ASR.")
277
+ self.asr_pipeline = pipeline(
278
+ "automatic-speech-recognition",
279
+ model="openai/whisper-base",
280
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32, # Use float16 only if CUDA available
281
+ device=device # Explicitly set device
282
+ )
283
+ print("✅ ASR (Whisper) pipeline loaded successfully.")
284
+ except Exception as e:
285
+ print(f"⚠️ Warning: Could not load ASR pipeline. Audio tool will not work. Error: {e}")
286
+ self.asr_pipeline = None # Ensure it's None if loading fails
287
+ # ====================================================
288
+
289
+ # 2. Get API Token from Space Secrets
290
  HUGGINGFACEHUB_API_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN")
291
  if not HUGGINGFACEHUB_API_TOKEN:
292
  raise ValueError("HUGGINGFACEHUB_API_TOKEN secret is not set! Please add it to your Space secrets.")
293
 
294
+ # 3. Collect Tool Methods
295
+ # LangChain tools expect functions or objects with a 'run' method.
296
+ # The @tool decorator makes our methods compatible.
297
  self.tools = [
298
+ self.search_tool, # References the method
299
+ self.code_interpreter,
300
+ self.read_file,
301
+ self.write_file,
302
+ self.list_directory,
303
+ self.audio_transcription_tool,
304
+ self.get_youtube_transcript,
305
+ self.scrape_web_page
306
  ]
307
 
308
+ # 4. Define the Improved System Prompt
309
  tool_descriptions = "\n".join([f"- {tool.name}: {tool.description}" for tool in self.tools])
310
  self.system_prompt = f"""You are a highly intelligent and meticulous AI assistant built to answer questions from the GAIA benchmark.
311
  Your primary goal is to provide **only the concise, factual, and direct answer** to the user's question, exactly matching the format required by the benchmark (e.g., a name, a number, a specific string format, a comma-separated list).
 
327
  "tool_input": {{ "arg_name1": "value1", "arg_name2": "value2", ... }}
328
  }}
329
  ```
330
+ * Replace `tool_name` with the exact name of the tool you want to use.
331
+ * Provide the required arguments within the `tool_input` dictionary. Ensure argument names and value types match the tool description precisely.
332
+ * Do not add any text before or after the JSON tool call block.
333
+
334
+ **REASONING PROCESS:**
335
+ 1. Carefully analyze the user's question to understand the specific information required and the expected answer format. Check if any files are attached (mentioned like `[Attached File: filename.ext]`).
336
+ 2. Break down the problem into logical steps.
337
+ 3. Determine if any tools are necessary. Use `read_file` for attached files, `audio_transcription_tool` for audio, `get_youtube_transcript` for YouTube URLs, `search_tool` for web info, `scrape_web_page` to read content from URLs found via search, and `code_interpreter` for calculations or data processing.
338
+ 4. If a tool is needed, call it using the specified JSON format. Wait for the tool's output.
339
+ 5. Analyze the tool's output. If the answer is found, proceed to step 7.
340
+ 6. If more information or steps are needed, use another tool (step 4) or continue reasoning based on the gathered information. Pay close attention to previous tool results.
341
+ 7. Once you have derived the final, definitive answer that meets the question's requirements, output **ONLY** that answer and nothing else. Stop the process.
342
+ """
343
+
344
+ # 5. Initialize the LLM (Using Qwen Coder)
345
+ print("Initializing LLM Endpoint...")
346
+ llm = HuggingFaceEndpoint(
347
+ repo_id="Qwen/Qwen2.5-Coder-32B-Instruct", # Changed model
348
+ huggingfacehub_api_token=HUGGINGFACEHUB_API_TOKEN,
349
+ max_new_tokens=2048, # Increased token limit for potentially longer reasoning/tool use
350
+ temperature=0.01, # Keep temperature low for factual tasks
351
+ # stop_sequences=["\nObservation:", "\nTool Result:", "\n```"] # Help prevent hallucinating tool calls/results
352
+ )
353
+ chat_llm = ChatHuggingFace(llm=llm)
354
+ print("✅ LLM Endpoint initialized.")
355
+
356
+ # 6. Bind tools to the LLM
357
+ # Ensure the LLM knows how to format calls for the tools
358
+ self.llm_with_tools = chat_llm.bind_tools(self.tools)
359
+ print("✅ Tools bound to LLM.")
360
+
361
+ # 7. Define the Agent Node
362
+ def agent_node(state: AgentState):
363
+ print("--- Running Agent Node ---")
364
+ messages_with_prompt = state["messages"] # We inject in __call__
365
+
366
+ ai_message = self.llm_with_tools.invoke(messages_with_prompt)
367
+ print(f"AI Message Raw: {ai_message}") # Log raw output for debugging
368
+ content_str = ai_message.content if isinstance(ai_message.content, str) else ""
369
+ # Check for tool_calls attribute populated by bind_tools
370
+ if ai_message.tool_calls:
371
+ print(f"AI Message contains tool calls: {ai_message.tool_calls}")
372
+ elif '"tool":' in content_str and '"tool_input":' in content_str:
373
+ # Fallback check if bind_tools didn't populate tool_calls but JSON is present
374
+ print(f"AI Message appears to contain raw tool call JSON.")
375
+ else:
376
+ print(f"AI Message Interpreted Content: {ai_message.pretty_repr()}")
377
+
378
+ return {"messages": [ai_message]}
379
+
380
+ # 8. Define the Tool Node
381
+ # This uses the list of tool methods directly
382
+ tool_node = ToolNode(self.tools)
383
+
384
+ # 9. Create the Graph
385
+ print("Building agent graph...")
386
+ graph_builder = StateGraph(AgentState)
387
+ graph_builder.add_node("agent", agent_node)
388
+ graph_builder.add_node("tools", tool_node)
389
+ graph_builder.add_edge(START, "agent")
390
+ graph_builder.add_conditional_edges(
391
+ "agent",
392
+ tools_condition, # This checks if the AIMessage contains tool_calls
393
+ {
394
+ "tools": "tools", # If tool_calls exist, go to tool node
395
+ "__end__": "__end__", # Otherwise, end the graph
396
+ },
397
+ )
398
+ graph_builder.add_edge("tools", "agent") # Loop back to agent after tools run
399
+
400
+ # 10. Compile the graph and store it
401
+ self.graph = graph_builder.compile()
402
+ print("✅ Graph compiled successfully.")
403
+
404
+ def __call__(self, question: str) -> str:
405
+ print(f"\n--- Starting Agent Run for Question ---")
406
+ print(f"Agent received question (first 100 chars): {question[:100]}...")
407
+
408
+ # Prepare the input for the graph, including the system prompt
409
+ graph_input = {"messages": [
410
+ HumanMessage(content=self.system_prompt + "\n\nUser Question:\n" + question)
411
+ ]}
412
+
413
+ final_answer_content = ""
414
+
415
+ # Stream the graph's execution
416
+ try:
417
+ # Use stream_mode="values" to get the full state at each step
418
+ for event in self.graph.stream(graph_input, stream_mode="values", config={"recursion_limit": 25}): # Increased recursion limit
419
+ # The 'event' dictionary holds the entire AgentState ('messages')
420
+ last_message = event["messages"][-1]
421
+
422
+ # Keep track of the latest AI response that isn't a tool call
423
+ if isinstance(last_message, AIMessage):
424
+ # Check if it has tool calls. If not, it might be the final answer.
425
+ if not last_message.tool_calls and not last_message.invalid_tool_calls:
426
+ if isinstance(last_message.content, str):
427
+ print(f"Potential Final AI Response: {last_message.content[:500]}...")
428
+ final_answer_content = last_message.content
429
+ else:
430
+ print(f"Non-string AI message content: {last_message.content}")
431
+
432
+ elif isinstance(last_message, ToolMessage):
433
+ print(f"Tool Result ({last_message.tool_call_id}): {last_message.content[:500]}...")
434
+
435
+ # --- Add the cleaning step ---
436
+ cleaned_answer = final_answer_content.strip()
437
+
438
+ prefixes_to_remove = [
439
+ "The answer is:", "Here is the answer:", "Based on the information:",
440
+ "Final Answer:", "Answer:"
441
+ ]
442
+ for prefix in prefixes_to_remove:
443
+ # Case-insensitive check
444
+ if cleaned_answer.lower().startswith(prefix.lower()):
445
+ cleaned_answer = cleaned_answer[len(prefix):].strip()
446
+ break
447
+
448
+ looks_like_code = any(kw in cleaned_answer for kw in ["def ", "import ", "print(", "for ", "while ", "if ", "class "]) or cleaned_answer.count('\n') > 3
449
+ if not looks_like_code:
450
+ if cleaned_answer.startswith("```") and cleaned_answer.endswith("```"):
451
+ cleaned_answer = cleaned_answer[3:-3].strip()
452
+ if '\n' in cleaned_answer:
453
+ first_line, rest = cleaned_answer.split('\n', 1)
454
+ if first_line.strip().replace('_','').isalnum() and len(first_line.strip()) < 15:
455
+ cleaned_answer = rest.strip()
456
+ elif cleaned_answer.startswith("`") and cleaned_answer.endswith("`"):
457
+ cleaned_answer = cleaned_answer[1:-1].strip()
458
+
459
+ print(f"Agent returning final answer (cleaned): {cleaned_answer}")
460
+ if not cleaned_answer:
461
+ print("Warning: Agent produced an empty final answer after cleaning. Falling back to raw answer.")
462
+ return final_answer_content.strip() # Fallback
463
+
464
+ return cleaned_answer
465
+
466
+ except Exception as e:
467
+ print(f"Error running agent graph: {e}")
468
+ import traceback
469
+ traceback.print_exc()
470
+ return f"AGENT GRAPH ERROR: {e}"
471
+
472
+
473
+ # --- (Original Template Code Starts Here - NO CHANGES NEEDED BELOW THIS LINE) ---
474
+ # ... (run_and_submit_all function, Gradio interface, __main__ block) ...
475
+ # Note: Ensure the 'run_and_submit_all' function correctly instantiates 'BasicAgent()'
476
+ # The rest of the template code should remain the same.
477
+
478
+ def run_and_submit_all( profile: gr.OAuthProfile | None):
479
+ """
480
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
481
+ and displays the results.
482
+ """
483
+ # --- Determine HF Space Runtime URL and Repo URL ---
484
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
485
+ if profile:
486
+ username= f"{profile.username}"
487
+ print(f"User logged in: {username}")
488
+ else:
489
+ print("User not logged in.")
490
+ return "Please Login to Hugging Face with the button.", None
491
+
492
+ api_url = DEFAULT_API_URL
493
+ questions_url = f"{api_url}/questions"
494
+ submit_url = f"{api_url}/submit"
495
+
496
+ # 1. Instantiate Agent
497
+ print("Instantiating agent...") # Changed log message slightly
498
+ try:
499
+ agent = BasicAgent()
500
+ # Check for ASR pipeline status after init
501
+ if agent.asr_pipeline is None:
502
+ print("⚠️ ASR Pipeline failed to load during agent init. Audio questions will likely fail.")
503
+
504
+ except Exception as e:
505
+ print(f"Error instantiating agent: {e}")
506
+ import traceback
507
+ traceback.print_exc() # Print full traceback for init errors
508
+ return f"Error initializing agent: {e}", None
509
+ print("Agent instantiated successfully.") # Changed log message slightly
510
+
511
+ # Agent code URL
512
+ agent_code = f"[https://huggingface.co/spaces/](https://huggingface.co/spaces/){space_id}/tree/main"
513
+ print(f"Agent code URL: {agent_code}")
514
+
515
+ # 2. Fetch Questions
516
+ print(f"Fetching questions from: {questions_url}")
517
+ try:
518
+ response = requests.get(questions_url, timeout=30) # Increased timeout
519
+ response.raise_for_status()
520
+ questions_data = response.json()
521
+ if not questions_data:
522
+ print("Fetched questions list is empty.")
523
+ return "Fetched questions list is empty or invalid format.", None
524
+ print(f"Fetched {len(questions_data)} questions.")
525
+ except requests.exceptions.RequestException as e:
526
+ print(f"Error fetching questions: {e}")
527
+ return f"Error fetching questions: {e}", None
528
+ except requests.exceptions.JSONDecodeError as e:
529
+ print(f"Error decoding JSON response from questions endpoint: {e}")
530
+ print(f"Response text: {response.text[:500]}")
531
+ return f"Error decoding server response for questions: {e}", None
532
+ except Exception as e:
533
+ print(f"An unexpected error occurred fetching questions: {e}")
534
+ return f"An unexpected error occurred fetching questions: {e}", None
535
+
536
+ # 3. Run your Agent
537
+ results_log = []
538
+ answers_payload = []
539
+ total_questions = len(questions_data)
540
+ print(f"Running agent on {total_questions} questions...")
541
+
542
+ # --- Limit for Testing ---
543
+ # question_limit = 5 # Uncomment and set a number (e.g., 5) to test fewer questions
544
+ # questions_to_run = questions_data[:question_limit]
545
+ # print(f"--- RUNNING WITH QUESTION LIMIT: {question_limit} ---")
546
+ questions_to_run = questions_data # Comment this line out if using the limit above
547
+
548
+ for i, item in enumerate(questions_to_run):
549
+ task_id = item.get("task_id")
550
+ question_text = item.get("question")
551
+ if not task_id or question_text is None:
552
+ print(f"Skipping item {i+1} with missing task_id or question: {item}")
553
+ continue
554
+
555
+ print(f"\n--- Running Task {i+1}/{len(questions_to_run)} (ID: {task_id}) ---")
556
+ try:
557
+ # Add file paths to the question context if present
558
+ # GAIA often includes files like images, audio, excel
559
+ file_path = item.get("file_path")
560
+ if file_path:
561
+ # Construct a potential path within the space if it's just a filename
562
+ potential_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), file_path)
563
+ if os.path.exists(potential_path):
564
+ file_context = f"[Attached File (exists): {file_path}]"
565
+ else:
566
+ # Check if it exists in the current working directory too
567
+ if os.path.exists(file_path):
568
+ file_context = f"[Attached File (exists in cwd): {file_path}]"
569
+ else:
570
+ file_context = f"[Attached File (path provided): {file_path}]" # Agent needs to handle finding it
571
+
572
+ question_text_with_context = f"{question_text}\n\n{file_context}"
573
+ print(f"Question includes file reference: {file_path}")
574
+ else:
575
+ question_text_with_context = question_text
576
+
577
+ submitted_answer = agent(question_text_with_context)
578
+ # Ensure answer is a string, even if agent returns None or other types
579
+ submitted_answer_str = str(submitted_answer) if submitted_answer is not None else ""
580
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer_str})
581
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer_str})
582
+ print(f"--- Task {task_id} Complete ---")
583
+ except Exception as e:
584
+ print(f"FATAL ERROR running agent graph on task {task_id}: {e}")
585
+ import traceback
586
+ traceback.print_exc()
587
+ submitted_answer = f"AGENT CRASH ERROR: {e}"
588
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
589
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
590
+
591
+ if not answers_payload:
592
+ print("Agent did not produce any answers to submit.")
593
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
594
+
595
+ # 4. Prepare Submission
596
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
597
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
598
+ print(status_update)
599
+
600
+ # 5. Submit
601
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
602
+ try:
603
+ response = requests.post(submit_url, json=submission_data, timeout=120) # Increased timeout
604
+ response.raise_for_status()
605
+ result_data = response.json()
606
+ final_status = (
607
+ f"Submission Successful!\n"
608
+ f"User: {result_data.get('username')}\n"
609
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
610
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
611
+ f"Message: {result_data.get('message', 'No message received.')}"
612
+ )
613
+ print("Submission successful.")
614
+ results_df = pd.DataFrame(results_log)
615
+ # Add score details if available
616
+ if 'scores' in result_data:
617
+ scores_dict = {item['task_id']: item['score'] for item in result_data['scores']}
618
+ results_df['Correct'] = results_df['Task ID'].map(lambda x: scores_dict.get(x, None))
619
+ results_df['Correct'] = results_df['Correct'].apply(lambda x: 'Yes' if x == 1 else ('No' if x == 0 else 'N/A'))
620
+
621
+
622
+ return final_status, results_df
623
+ except requests.exceptions.HTTPError as e:
624
+ error_detail = f"Server responded with status {e.response.status_code}."
625
+ try:
626
+ error_json = e.response.json()
627
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
628
+ except requests.exceptions.JSONDecodeError:
629
+ error_detail += f" Response: {e.response.text[:500]}"
630
+ status_message = f"Submission Failed: {error_detail}"
631
+ print(status_message)
632
+ results_df = pd.DataFrame(results_log)
633
+ return status_message, results_df
634
+ except requests.exceptions.Timeout:
635
+ status_message = "Submission Failed: The submission request timed out."
636
+ print(status_message)
637
+ results_df = pd.DataFrame(results_log)
638
+ return status_message, results_df
639
+ except requests.exceptions.RequestException as e:
640
+ status_message = f"Submission Failed: Network error during submission - {e}"
641
+ print(status_message)
642
+ results_df = pd.DataFrame(results_log)
643
+ return status_message, results_df
644
+ except Exception as e:
645
+ status_message = f"An unexpected error occurred during submission processing: {e}"
646
+ print(status_message)
647
+ import traceback
648
+ traceback.print_exc()
649
+ results_df = pd.DataFrame(results_log)
650
+ return status_message, results_df
651
+
652
+ # --- Build Gradio Interface using Blocks ---
653
+ with gr.Blocks() as demo:
654
+ gr.Markdown("# GAIA Agent Evaluation Runner (LangGraph + Qwen)")
655
+ gr.Markdown(
656
+ """
657
+ **Instructions:**
658
+ 1. Log in to your Hugging Face account using the button below.
659
+ 2. Click 'Run Evaluation & Submit All Answers' to fetch questions, run the agent, submit answers, and see the score.
660
+ ---
661
+ **Notes:**
662
+ * The full evaluation can take **several hours**. Use the logs tab to monitor progress.
663
+ * This agent uses `Qwen/Qwen2.5-Coder-32B-Instruct` and multiple tools (search, code, file, audio, youtube, web scrape).
664
+ * Make sure your `HUGGINGFACEHUB_API_TOKEN` secret is set correctly in Settings.
665
+ """
666
+ )
667
+ gr.LoginButton()
668
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
669
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
670
+ results_table = gr.DataFrame(label="Questions, Agent Answers, and Results", wrap=True)
671
+
672
+ run_button.click(
673
+ fn=run_and_submit_all,
674
+ outputs=[status_output, results_table]
675
+ )
676
+
677
+ if __name__ == "__main__":
678
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
679
+
680
+ # Check for SPACE_HOST and SPACE_ID at startup for information
681
+ space_host_startup = os.getenv("SPACE_HOST")
682
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
683
+
684
+ if space_host_startup:
685
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
686
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
687
+ else:
688
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
689
+
690
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
691
+ print(f"✅ SPACE_ID found: {space_id_startup}")
692
+ print(f" Repo URL: [https://huggingface.co/spaces/](https://huggingface.co/spaces/){space_id_startup}")
693
+ print(f" Repo Tree URL: [https://huggingface.co/spaces/](https://huggingface.co/spaces/){space_id_startup}/tree/main")
694
+ else:
695
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
696
+
697
+ # Add detailed path info for debugging file access
698
+ print(f"Script directory (__file__): {os.path.dirname(os.path.abspath(__file__))}")
699
+ print(f"Current working directory (os.getcwd()): {os.getcwd()}")
700
+ print("Files in current working directory:", os.listdir("."))
701
+
702
+
703
+ print("-"*(60 + len(" App Starting ")) + "\n")
704
+ print("Launching Gradio Interface for GAIA Agent Evaluation...")
705
+ # Set queue=True to handle multiple clicks better, though only one run should happen at a time.
706
+ demo.queue().launch(debug=True, share=False)
707
+