Spaces:
Sleeping
Sleeping
Gabe
commited on
Commit
·
26e9610
1
Parent(s):
1e6b798
fix error
Browse files
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 = "
|
| 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 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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 |
+
|