Macmill commited on
Commit
86a2b95
·
verified ·
1 Parent(s): 1da8686

Update final_agent.py

Browse files
Files changed (1) hide show
  1. final_agent.py +86 -173
final_agent.py CHANGED
@@ -20,56 +20,55 @@ from langgraph.graph.message import add_messages
20
  from langgraph.prebuilt import ToolNode, tools_condition
21
  from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
22
  from langchain_core.tools import tool
23
- from langchain_groq import ChatGroq
24
- from langchain_google_genai import ChatGoogleGenerativeAI
25
  from langchain_community.tools.tavily_search import TavilySearchResults
26
 
27
  # ==============================================================================
28
  # Environment Setup & LLM
29
  # ==============================================================================
30
  load_dotenv()
31
- gemini_api_key = os.getenv("GEMINI_API_KEY")
32
  tavily_api_key = os.getenv("TAVILY_API_KEY")
33
- groq_api_key = os,getenv("GROQ_API_KEY")
34
 
35
  # --- Optional: Tesseract Path ---
36
  # If Tesseract OCR is not in your system's PATH environment variable,
37
  # uncomment the following line and set the correct path to tesseract.exe
38
  # try:
39
  # pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' # Example path for Windows
40
- # except NameError: pass # Handles case where pytesseract might not be imported yet if PIL fails first
41
  # except Exception as e: print(f"Warning: Could not set tesseract_cmd path: {e}")
42
 
43
 
44
  # --- Validate API Keys ---
45
- if not gemini_api_key:
46
- raise ValueError("GEMINI_API_KEY not found in environment variables.")
47
  if not tavily_api_key:
48
- raise ValueError("TAVILY_API_KEY not found. Required for Tavily search tool.")
49
-
50
- # --- Initialize LLM ---
51
- # Using the model specified in the user's code block
52
- # llm = ChatGoogleGenerativeAI(
53
- # model="gemini-2.0-flash-lite", # As per user's last provided code
54
- # google_api_key=gemini_api_key,
55
- # temperature=0.1 # Low temperature for factual tasks
56
- # )
57
- llm = ChatGroq(
58
- model="gemma2-9b-it",
59
- api_key=groq_api_key,
60
- temperature = 0.1
61
- )
62
- # print(f"LLM Initialized: {llm.model}")
 
 
63
 
64
  # ==============================================================================
65
  # State Definition
66
  # ==============================================================================
67
  class AgentState(TypedDict):
68
  """Defines the structure of the information the agent tracks during its run."""
69
- input_question: str # The original question from the benchmark
70
- messages: Annotated[List[BaseMessage], add_messages] # History of interactions (Human, AI, Tool)
71
- error: Optional[str] # Stores any error message encountered
72
- iterations: int # Counter for agent steps to prevent loops
73
 
74
  # ==============================================================================
75
  # Tools Definitions
@@ -91,22 +90,14 @@ def web_browser(url: str) -> str:
91
  response = requests.get(url, headers=headers, timeout=20)
92
  response.raise_for_status()
93
  response.encoding = response.apparent_encoding or 'utf-8'
94
- # Configure html2text
95
- h = html2text.HTML2Text(bodywidth=0)
96
- h.ignore_links = True
97
- h.ignore_images = True
98
- # Convert HTML to text
99
  clean_text = h.handle(response.text)
100
- # Limit content length
101
  max_length = 6000
102
- if len(clean_text) > max_length:
103
- return clean_text[:max_length] + "\n\n... [Content Truncated]"
104
  cleaned_and_stripped = clean_text.strip()
105
  return cleaned_and_stripped if cleaned_and_stripped else f"Error: No meaningful content via html2text for {url}."
106
- except requests.exceptions.RequestException as e:
107
- return f"Error: Network request failed for URL: {url}. Reason: {e}"
108
- except Exception as e:
109
- return f"Error: Unexpected error processing URL with html2text: {url}. Reason: {str(e)}"
110
 
111
  # --- File Download Tool ---
112
  @tool
@@ -114,43 +105,30 @@ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
114
  """Downloads a file from a URL to a temporary directory. Input: file URL. Returns: path to downloaded file or error."""
115
  print(f"--- [Tool] Downloading file from: {url} ---")
116
  try:
117
- # Generate filename if needed
118
  if not filename:
119
  try: path = urlparse(url).path; filename = os.path.basename(path) if path else None
120
  except Exception: filename = None
121
  if not filename: import uuid; filename = f"downloaded_{uuid.uuid4().hex[:8]}"
122
- # Define save path
123
  temp_dir = tempfile.gettempdir(); filepath = os.path.join(temp_dir, filename)
124
- # Download file
125
  response = requests.get(url, stream=True, timeout=30); response.raise_for_status()
126
  with open(filepath, 'wb') as f:
127
  for chunk in response.iter_content(chunk_size=8192): f.write(chunk)
128
  print(f"--- [Tool] File downloaded to: {filepath} ---")
129
  return f"File downloaded to {filepath}. Use appropriate tools (e.g., analyze_csv_file) to process it."
130
- except requests.exceptions.RequestException as e:
131
- return f"Error downloading file: Network issue for {url}. Reason: {e}"
132
- except Exception as e:
133
- return f"Error downloading file: Unexpected error for {url}. Reason: {str(e)}"
134
 
135
  # --- CSV Analysis Tool ---
136
  @tool
137
  def analyze_csv_file(file_path: str) -> str:
138
  """Analyzes a CSV file at the given path using pandas. Returns a summary of content or error."""
139
  print(f"--- [Tool] Analyzing CSV: {file_path} ---")
140
- # GAIA might provide relative paths, ensure they work or adjust logic if needed
141
  if not os.path.exists(file_path): return f"Error: CSV file not found at path: {file_path}"
142
  try:
143
- df = pd.read_csv(file_path)
144
- # Generate summary string
145
- summary = f"CSV Analysis Report for {os.path.basename(file_path)}:\n"
146
- summary += f"- Shape: {df.shape[0]} rows, {df.shape[1]} columns\n"
147
- summary += f"- Columns: {', '.join(df.columns)}\n"
148
- summary += f"\nFirst 5 rows:\n{df.head().to_string()}\n"
149
  numeric_cols = df.select_dtypes(include=['number'])
150
- if not numeric_cols.empty:
151
- summary += f"\nBasic Stats (Numeric):\n{numeric_cols.describe().to_string()}"
152
- else:
153
- summary += "\nNo numeric columns for stats."
154
  return summary
155
  except ImportError: return "Error: 'pandas' required but not installed."
156
  except Exception as e: return f"Error analyzing CSV {file_path}: {str(e)}"
@@ -162,17 +140,10 @@ def analyze_excel_file(file_path: str) -> str:
162
  print(f"--- [Tool] Analyzing Excel: {file_path} ---")
163
  if not os.path.exists(file_path): return f"Error: Excel file not found at path: {file_path}"
164
  try:
165
- df = pd.read_excel(file_path, engine='openpyxl')
166
- # Generate summary string
167
- summary = f"Excel Analysis Report for {os.path.basename(file_path)} (First Sheet):\n"
168
- summary += f"- Shape: {df.shape[0]} rows, {df.shape[1]} columns\n"
169
- summary += f"- Columns: {', '.join(df.columns)}\n"
170
- summary += f"\nFirst 5 rows:\n{df.head().to_string()}\n"
171
  numeric_cols = df.select_dtypes(include=['number'])
172
- if not numeric_cols.empty:
173
- summary += f"\nBasic Stats (Numeric):\n{numeric_cols.describe().to_string()}"
174
- else:
175
- summary += "\nNo numeric columns for stats."
176
  return summary
177
  except ImportError: return "Error: 'pandas' and 'openpyxl' required but not installed."
178
  except Exception as e: return f"Error analyzing Excel {file_path}: {str(e)}"
@@ -184,10 +155,8 @@ def extract_text_from_image(file_path: str) -> str:
184
  print(f"--- [Tool] Extracting text from image: {file_path} ---")
185
  if not os.path.exists(file_path): return f"Error: Image file not found at path: {file_path}"
186
  try:
187
- # Need to explicitly handle potential empty string from pytesseract
188
  text = pytesseract.image_to_string(Image.open(file_path))
189
  text_stripped = text.strip()
190
- # Return a clear message if no text found, otherwise return extracted text
191
  return f"Extracted text from image '{os.path.basename(file_path)}':\n{text_stripped}" if text_stripped else "No text found in image."
192
  except ImportError: return "Error: 'Pillow' or 'pytesseract' required but not installed."
193
  except pytesseract.TesseractNotFoundError: return "Error: Tesseract OCR not installed or not in PATH."
@@ -195,24 +164,14 @@ def extract_text_from_image(file_path: str) -> str:
195
 
196
  # --- Basic Math Tools ---
197
  @tool
198
- def add(a: float, b: float) -> float:
199
- """Adds two numbers (a + b). Handles float inputs."""
200
- print(f"--- [Tool] Calculating: {a} + {b} ---")
201
- return a + b
202
  @tool
203
- def subtract(a: float, b: float) -> float:
204
- """Subtracts the second number from the first (a - b). Handles float inputs."""
205
- print(f"--- [Tool] Calculating: {a} - {b} ---")
206
- return a - b
207
  @tool
208
- def multiply(a: float, b: float) -> float:
209
- """Multiplies two numbers (a * b). Handles float inputs."""
210
- print(f"--- [Tool] Calculating: {a} * {b} ---")
211
- return a * b
212
  @tool
213
  def divide(a: float, b: float) -> float | str:
214
- """Divides the first number by the second (a / b). Handles float inputs and division by zero."""
215
- print(f"--- [Tool] Calculating: {a} / {b} ---")
216
  if b == 0: return "Error: Cannot divide by zero."
217
  return a / b
218
 
@@ -221,6 +180,9 @@ tools = [ search_tool, web_browser, download_file_from_url, analyze_csv_file,
221
  analyze_excel_file, extract_text_from_image, add, subtract, multiply, divide ]
222
 
223
  # --- Bind tools to the LLM ---
 
 
 
224
  llm_with_tools = llm.bind_tools(tools)
225
  print(f"Agent initialized with {len(tools)} tools.")
226
 
@@ -239,21 +201,18 @@ def call_agent_node(state: AgentState) -> dict:
239
  print(f"Warning: Reached max iterations ({MAX_ITERATIONS}). Stopping.")
240
  return {"error": f"Max iterations ({MAX_ITERATIONS}) reached."}
241
  try:
242
- # Call the LLM
 
 
243
  response = llm_with_tools.invoke(state['messages'])
244
- print("--- [Node] AI Response/Action ---")
245
- response.pretty_print() # Log the LLM's thoughts and actions
246
- # Return the response message and incremented iteration count
247
  return {"messages": [response], "iterations": current_iterations + 1}
248
  except Exception as e:
249
- error_message = f"LLM invocation failed: {str(e)}"
250
- print(f"--- [Node] ERROR: {error_message} ---")
251
- traceback.print_exc() # Print full traceback for debugging LLM errors
252
- # Return an error message and set error state
253
  return {"messages": [AIMessage(content=f"Sorry, I encountered an error: {error_message}")], "error": error_message, "iterations": current_iterations + 1}
254
 
255
  # --- Tool Node ---
256
- # Use the prebuilt ToolNode to handle execution of the bound tools
257
  tool_node = ToolNode(tools)
258
 
259
  # ==============================================================================
@@ -261,37 +220,20 @@ tool_node = ToolNode(tools)
261
  # ==============================================================================
262
  print("Building agent graph...")
263
  builder = StateGraph(AgentState)
264
-
265
- # Add the agent and tool nodes
266
  builder.add_node("agent", call_agent_node)
267
  builder.add_node("tools", tool_node)
268
-
269
- # Set the entry point
270
  builder.add_edge(START, "agent")
 
 
271
 
272
- # Define the conditional logic after the agent node runs
273
- builder.add_conditional_edges(
274
- "agent",
275
- tools_condition, # Built-in function checks if the last message has tool_calls
276
- {
277
- "tools": "tools", # If tool calls exist, route to the tools node
278
- END: END # If no tool calls, the agent is done, route to END
279
- }
280
- )
281
-
282
- # Define the edge after the tools node runs
283
- builder.add_edge("tools", "agent") # Always return to the agent node to process tool results
284
-
285
- # Compile the graph into a runnable object
286
- # NOTE: This compilation happens when the script is imported by app.py
287
  try:
288
  graph = builder.compile()
289
  print("GAIA agent graph compiled successfully.")
290
  except Exception as e:
291
  print(f"ERROR: Failed to compile LangGraph graph: {e}")
292
  traceback.print_exc()
293
- # Raise or handle appropriately - app might fail to start if graph doesn't compile
294
- raise
295
 
296
  # ==============================================================================
297
  # Main Execution Function for GAIA Benchmark <<<< WRAPPER FUNCTION >>>>
@@ -300,40 +242,33 @@ def answer_gaia_task(question: str, file_path: Optional[str] = None) -> str:
300
  """
301
  Runs the compiled GAIA agent graph for a given question and optional file path.
302
  This is the main entry point expected by the benchmark runner.
303
-
304
- Args:
305
- question: The question text from the GAIA benchmark.
306
- file_path: Optional path to a file associated with the question.
307
-
308
- Returns:
309
- A string containing the final answer extracted by the agent, or an error message.
310
  """
311
- # Ensure the compiled graph is available
312
- if 'graph' not in globals():
313
- return "Error: Agent graph was not compiled successfully."
314
 
315
  print(f"\n{'='*20} Running Agent for GAIA Task {'='*20}")
316
  print(f"Question: {question}")
317
- file_context_info = ""
318
- if file_path:
319
- print(f"Associated File Path: {file_path}")
320
- file_context_info = f"An associated file is provided at path: '{file_path}'. Your tools should use this path if they require a file path not explicitly mentioned in the question."
321
 
322
- # Define the initial prompt sent to the agent
323
- prompt_content = f"""Your task is to accurately answer the following question based *only* on information obtained using your tools (web search, web browser, file download, csv/excel analysis, image OCR, math).
324
 
325
  {file_context_info}
326
 
327
  Follow these steps methodically:
328
- 1. Analyze the question to understand required information and tools needed.
329
- 2. If external files are mentioned (e.g., 'data.csv', 'image.png'), use the appropriate analysis tool directly on the provided file path/name. Assume files are accessible in the current directory unless a URL or the separate file path is given.
330
- 3. If a URL is given for a file, use 'download_file_from_url' first, then analyze the downloaded file using its returned path.
331
- 4. If web information is needed, use 'web_search' then 'web_browser' on relevant URLs.
332
- 5. If calculations are needed, use the math tools.
333
- 6. Synthesize the information gathered from tools to arrive at the final answer.
334
- 7. **CRITICAL:** Your final output MUST contain ONLY the precise numerical or text answer requested by the question. Do NOT include explanations, reasoning steps, units unless explicitly asked for, context, apologies, or any introductory phrases like "The final answer is...". Just the required answer string or number itself.
335
-
336
- Question: {question}
 
 
 
337
  """
338
 
339
  # Create the initial state for the graph run
@@ -355,15 +290,17 @@ Question: {question}
355
  if final_state.get("error"):
356
  print(f"--- Agent stopped due to ERROR: {final_state['error']} ---")
357
  final_answer = f"Error: {final_state['error']}"
358
- # Check if the last message is an AIMessage and capture its content
359
  elif final_state.get('messages') and isinstance(final_state['messages'][-1], AIMessage):
360
- # Extract content from the last AI message - relies on prompt working
361
  potential_answer = final_state['messages'][-1].content
 
 
 
 
 
362
  print(f"--- Final Answer (from AI): {potential_answer} ---")
363
  final_answer = potential_answer
364
  else:
365
  print("--- Could not determine final answer (last message not AI or missing). Check logs. ---")
366
- # Log final state details for debugging
367
  print(f"Final State: Error={final_state.get('error')}, Iterations={final_state.get('iterations')}")
368
 
369
  except Exception as e:
@@ -380,57 +317,33 @@ Question: {question}
380
  # Local Testing Block (Optional)
381
  # ==============================================================================
382
  # This block allows you to test the agent by running final_agent.py directly.
383
- # It will not run when the script is imported by app.py in the Space.
384
  if __name__ == "__main__":
385
  print("\n--- Running Local Test ---")
386
- # --- Define Test Question ---
387
  test_question = "What is the result of multiplying the number of rows (excluding the header) in 'data.csv' by the number found after the phrase 'total items:' in 'image.png'?"
388
-
389
- # --- Create Dummy Files for Local Test ---
390
  print("Creating dummy files for local test...")
391
  dummy_files_created = True
392
  try:
393
- # Dummy CSV with 3 data rows + header
394
- with open("data.csv", "w") as f:
395
- f.write("Header1,Header2\nRow1Val1,Row1Val2\nRow2Val1,Row2Val2\nRow3Val1,Row3Val2")
396
-
397
- # Dummy Image containing the required text
398
  try:
399
- img = Image.new('RGB', (300, 50), color = (255, 255, 255)) # White background
400
- from PIL import ImageDraw, ImageFont # Import drawing tools locally
401
  draw = ImageDraw.Draw(img)
402
- # Use a basic font if specific ones aren't found
403
  try: font = ImageFont.truetype("arial.ttf", 15)
404
  except IOError: font = ImageFont.load_default()
405
- draw.text((10,10), "Some random info... total items: 7 ... more text", fill=(0,0,0), font=font) # Black text
406
  img.save("image.png")
407
  print("Dummy data.csv and image.png created successfully.")
408
- except ImportError:
409
- print("Pillow/ImageDraw/ImageFont not installed. Cannot create dummy image file.")
410
- dummy_files_created = False
411
- except Exception as img_e:
412
- print(f"Error creating dummy image: {img_e}")
413
- dummy_files_created = False
414
-
415
- except Exception as file_e:
416
- print(f"Error creating dummy files: {file_e}")
417
- dummy_files_created = False
418
- # ---------------------------------------------
419
-
420
- # --- Run the Test ---
421
  if dummy_files_created:
422
- # Call the main function, simulating how the benchmark runner would call it.
423
- # For this specific question, file_path argument is None as paths are in the question text.
424
  result = answer_gaia_task(question=test_question, file_path=None)
425
-
426
  print(f"\n--- Local Test Result ---")
427
- # Expected answer for dummy files: 3 data rows * 7 = 21
428
  print(f"Returned Answer: {result}")
429
  print(f"Expected Answer (for dummy files): 21")
430
- else:
431
- print("Skipping test execution due to issues creating dummy files.")
432
 
433
- # --- Clean up Dummy Files ---
434
  print("\nCleaning up dummy files...")
435
  for dummy_file in ["data.csv", "image.png"]:
436
  if os.path.exists(dummy_file):
 
20
  from langgraph.prebuilt import ToolNode, tools_condition
21
  from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
22
  from langchain_core.tools import tool
23
+ from langchain_groq import ChatGroq # Using Groq
 
24
  from langchain_community.tools.tavily_search import TavilySearchResults
25
 
26
  # ==============================================================================
27
  # Environment Setup & LLM
28
  # ==============================================================================
29
  load_dotenv()
30
+ # Removed Gemini Key handling
31
  tavily_api_key = os.getenv("TAVILY_API_KEY")
32
+ groq_api_key = os.getenv("GROQ_API_KEY")
33
 
34
  # --- Optional: Tesseract Path ---
35
  # If Tesseract OCR is not in your system's PATH environment variable,
36
  # uncomment the following line and set the correct path to tesseract.exe
37
  # try:
38
  # pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' # Example path for Windows
39
+ # except NameError: pass
40
  # except Exception as e: print(f"Warning: Could not set tesseract_cmd path: {e}")
41
 
42
 
43
  # --- Validate API Keys ---
 
 
44
  if not tavily_api_key:
45
+ raise ValueError("TAVILY_API_KEY not found in Space secrets. Required for search.")
46
+ if not groq_api_key:
47
+ raise ValueError("GROQ_API_KEY not found in Space secrets. Required for Groq LLM.")
48
+
49
+ # --- Initialize LLM (Using Groq) ---
50
+ try:
51
+ llm = ChatGroq(
52
+ model="llama3-70b-8192", # Powerful model available on Groq, good for reasoning
53
+ # model="gemma2-9b-it", # Alternative if Llama3 causes issues
54
+ api_key=groq_api_key,
55
+ temperature=0.1 # Low temperature for factual tasks
56
+ )
57
+ print(f"LLM Initialized: Groq - {llm.model_name}")
58
+ except Exception as e:
59
+ print(f"ERROR initializing Groq LLM: {e}")
60
+ traceback.print_exc()
61
+ raise # Stop if LLM fails to init
62
 
63
  # ==============================================================================
64
  # State Definition
65
  # ==============================================================================
66
  class AgentState(TypedDict):
67
  """Defines the structure of the information the agent tracks during its run."""
68
+ input_question: str
69
+ messages: Annotated[List[BaseMessage], add_messages]
70
+ error: Optional[str]
71
+ iterations: int
72
 
73
  # ==============================================================================
74
  # Tools Definitions
 
90
  response = requests.get(url, headers=headers, timeout=20)
91
  response.raise_for_status()
92
  response.encoding = response.apparent_encoding or 'utf-8'
93
+ h = html2text.HTML2Text(bodywidth=0); h.ignore_links = True; h.ignore_images = True
 
 
 
 
94
  clean_text = h.handle(response.text)
 
95
  max_length = 6000
96
+ if len(clean_text) > max_length: return clean_text[:max_length] + "\n\n... [Content Truncated]"
 
97
  cleaned_and_stripped = clean_text.strip()
98
  return cleaned_and_stripped if cleaned_and_stripped else f"Error: No meaningful content via html2text for {url}."
99
+ except requests.exceptions.RequestException as e: return f"Error: Network request failed for URL: {url}. Reason: {e}"
100
+ except Exception as e: return f"Error: Unexpected error processing URL with html2text: {url}. Reason: {str(e)}"
 
 
101
 
102
  # --- File Download Tool ---
103
  @tool
 
105
  """Downloads a file from a URL to a temporary directory. Input: file URL. Returns: path to downloaded file or error."""
106
  print(f"--- [Tool] Downloading file from: {url} ---")
107
  try:
 
108
  if not filename:
109
  try: path = urlparse(url).path; filename = os.path.basename(path) if path else None
110
  except Exception: filename = None
111
  if not filename: import uuid; filename = f"downloaded_{uuid.uuid4().hex[:8]}"
 
112
  temp_dir = tempfile.gettempdir(); filepath = os.path.join(temp_dir, filename)
 
113
  response = requests.get(url, stream=True, timeout=30); response.raise_for_status()
114
  with open(filepath, 'wb') as f:
115
  for chunk in response.iter_content(chunk_size=8192): f.write(chunk)
116
  print(f"--- [Tool] File downloaded to: {filepath} ---")
117
  return f"File downloaded to {filepath}. Use appropriate tools (e.g., analyze_csv_file) to process it."
118
+ except requests.exceptions.RequestException as e: return f"Error downloading file: Network issue for {url}. Reason: {e}"
119
+ except Exception as e: return f"Error downloading file: Unexpected error for {url}. Reason: {str(e)}"
 
 
120
 
121
  # --- CSV Analysis Tool ---
122
  @tool
123
  def analyze_csv_file(file_path: str) -> str:
124
  """Analyzes a CSV file at the given path using pandas. Returns a summary of content or error."""
125
  print(f"--- [Tool] Analyzing CSV: {file_path} ---")
 
126
  if not os.path.exists(file_path): return f"Error: CSV file not found at path: {file_path}"
127
  try:
128
+ df = pd.read_csv(file_path); summary = f"CSV Analysis Report for {os.path.basename(file_path)}:\n- Shape: {df.shape[0]} rows, {df.shape[1]} columns\n- Columns: {', '.join(df.columns)}\n\nFirst 5 rows:\n{df.head().to_string()}\n"
 
 
 
 
 
129
  numeric_cols = df.select_dtypes(include=['number'])
130
+ if not numeric_cols.empty: summary += f"\nBasic Stats (Numeric):\n{numeric_cols.describe().to_string()}"
131
+ else: summary += "\nNo numeric columns for stats."
 
 
132
  return summary
133
  except ImportError: return "Error: 'pandas' required but not installed."
134
  except Exception as e: return f"Error analyzing CSV {file_path}: {str(e)}"
 
140
  print(f"--- [Tool] Analyzing Excel: {file_path} ---")
141
  if not os.path.exists(file_path): return f"Error: Excel file not found at path: {file_path}"
142
  try:
143
+ df = pd.read_excel(file_path, engine='openpyxl'); summary = f"Excel Analysis Report for {os.path.basename(file_path)} (First Sheet):\n- Shape: {df.shape[0]} rows, {df.shape[1]} columns\n- Columns: {', '.join(df.columns)}\n\nFirst 5 rows:\n{df.head().to_string()}\n"
 
 
 
 
 
144
  numeric_cols = df.select_dtypes(include=['number'])
145
+ if not numeric_cols.empty: summary += f"\nBasic Stats (Numeric):\n{numeric_cols.describe().to_string()}"
146
+ else: summary += "\nNo numeric columns for stats."
 
 
147
  return summary
148
  except ImportError: return "Error: 'pandas' and 'openpyxl' required but not installed."
149
  except Exception as e: return f"Error analyzing Excel {file_path}: {str(e)}"
 
155
  print(f"--- [Tool] Extracting text from image: {file_path} ---")
156
  if not os.path.exists(file_path): return f"Error: Image file not found at path: {file_path}"
157
  try:
 
158
  text = pytesseract.image_to_string(Image.open(file_path))
159
  text_stripped = text.strip()
 
160
  return f"Extracted text from image '{os.path.basename(file_path)}':\n{text_stripped}" if text_stripped else "No text found in image."
161
  except ImportError: return "Error: 'Pillow' or 'pytesseract' required but not installed."
162
  except pytesseract.TesseractNotFoundError: return "Error: Tesseract OCR not installed or not in PATH."
 
164
 
165
  # --- Basic Math Tools ---
166
  @tool
167
+ def add(a: float, b: float) -> float: """Adds two numbers (a + b)."""
 
 
 
168
  @tool
169
+ def subtract(a: float, b: float) -> float: """Subtracts the second number from the first (a - b)."""
 
 
 
170
  @tool
171
+ def multiply(a: float, b: float) -> float: """Multiplies two numbers (a * b)."""
 
 
 
172
  @tool
173
  def divide(a: float, b: float) -> float | str:
174
+ """Divides the first number by the second (a / b). Handles division by zero."""
 
175
  if b == 0: return "Error: Cannot divide by zero."
176
  return a / b
177
 
 
180
  analyze_excel_file, extract_text_from_image, add, subtract, multiply, divide ]
181
 
182
  # --- Bind tools to the LLM ---
183
+ # Ensure LLM is initialized before binding
184
+ if 'llm' not in globals():
185
+ raise RuntimeError("LLM was not initialized successfully before binding tools.")
186
  llm_with_tools = llm.bind_tools(tools)
187
  print(f"Agent initialized with {len(tools)} tools.")
188
 
 
201
  print(f"Warning: Reached max iterations ({MAX_ITERATIONS}). Stopping.")
202
  return {"error": f"Max iterations ({MAX_ITERATIONS}) reached."}
203
  try:
204
+ # Ensure LLM is bound with tools before invoking
205
+ if 'llm_with_tools' not in globals():
206
+ return {"error": "LLM tools not bound."}
207
  response = llm_with_tools.invoke(state['messages'])
208
+ print("--- [Node] AI Response/Action ---"); response.pretty_print()
 
 
209
  return {"messages": [response], "iterations": current_iterations + 1}
210
  except Exception as e:
211
+ error_message = f"LLM invocation failed: {str(e)}"; print(f"--- [Node] ERROR: {error_message} ---")
212
+ traceback.print_exc()
 
 
213
  return {"messages": [AIMessage(content=f"Sorry, I encountered an error: {error_message}")], "error": error_message, "iterations": current_iterations + 1}
214
 
215
  # --- Tool Node ---
 
216
  tool_node = ToolNode(tools)
217
 
218
  # ==============================================================================
 
220
  # ==============================================================================
221
  print("Building agent graph...")
222
  builder = StateGraph(AgentState)
 
 
223
  builder.add_node("agent", call_agent_node)
224
  builder.add_node("tools", tool_node)
 
 
225
  builder.add_edge(START, "agent")
226
+ builder.add_conditional_edges("agent", tools_condition, {"tools": "tools", END: END})
227
+ builder.add_edge("tools", "agent")
228
 
229
+ # Compile the graph globally so it's ready for the function call
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  try:
231
  graph = builder.compile()
232
  print("GAIA agent graph compiled successfully.")
233
  except Exception as e:
234
  print(f"ERROR: Failed to compile LangGraph graph: {e}")
235
  traceback.print_exc()
236
+ graph = None # Set graph to None if compilation fails
 
237
 
238
  # ==============================================================================
239
  # Main Execution Function for GAIA Benchmark <<<< WRAPPER FUNCTION >>>>
 
242
  """
243
  Runs the compiled GAIA agent graph for a given question and optional file path.
244
  This is the main entry point expected by the benchmark runner.
 
 
 
 
 
 
 
245
  """
246
+ # Check if graph compilation was successful
247
+ if graph is None:
248
+ return "Error: Agent graph was not compiled successfully during setup."
249
 
250
  print(f"\n{'='*20} Running Agent for GAIA Task {'='*20}")
251
  print(f"Question: {question}")
252
+ file_context_info = f"An associated file is provided at path: '{file_path}'. Use this path if relevant." if file_path else ""
 
 
 
253
 
254
+ # Define the initial prompt sent to the agent, incorporating strict formatting rules
255
+ prompt_content = f"""You are a precise AI assistant answering a specific question based *only* on information obtained using your tools.
256
 
257
  {file_context_info}
258
 
259
  Follow these steps methodically:
260
+ 1. Analyze the question: {question}
261
+ 2. Use tools (web_search, web_browser, download_file_from_url, analyze_csv_file, analyze_excel_file, extract_text_from_image, add, subtract, multiply, divide) ONLY if necessary to gather the specific information required. Assume local file paths mentioned in the question (like 'data.csv') are accessible.
262
+ 3. Synthesize the final answer from the gathered information.
263
+
264
+ **CRITICAL OUTPUT FORMATTING RULES:**
265
+ * Your final response MUST be ONLY the answer, without any other text, explanations, or introductions.
266
+ * **Numbers:** Do not use commas (e.g., 1000). Do not include units ($ , %) unless explicitly asked for.
267
+ * **Strings:** Do not use articles (a, an, the) unless part of a required proper noun. Do not use abbreviations (e.g., write "Saint Petersburg") unless the abbreviation is the answer. Write digits as numerals (5).
268
+ * **Lists:** If a list is required, provide it as comma-separated values (e.g., apple,banana,cherry). Apply number/string rules to elements.
269
+ * If you cannot find the answer using the tools, output only the exact phrase: Information not found
270
+
271
+ Provide ONLY the final answer according to these rules.
272
  """
273
 
274
  # Create the initial state for the graph run
 
290
  if final_state.get("error"):
291
  print(f"--- Agent stopped due to ERROR: {final_state['error']} ---")
292
  final_answer = f"Error: {final_state['error']}"
 
293
  elif final_state.get('messages') and isinstance(final_state['messages'][-1], AIMessage):
 
294
  potential_answer = final_state['messages'][-1].content
295
+ # Basic cleanup for potential quotes added by LLM
296
+ if isinstance(potential_answer, str):
297
+ if (potential_answer.startswith('"') and potential_answer.endswith('"')) or \
298
+ (potential_answer.startswith("'") and potential_answer.endswith("'")):
299
+ potential_answer = potential_answer[1:-1].strip()
300
  print(f"--- Final Answer (from AI): {potential_answer} ---")
301
  final_answer = potential_answer
302
  else:
303
  print("--- Could not determine final answer (last message not AI or missing). Check logs. ---")
 
304
  print(f"Final State: Error={final_state.get('error')}, Iterations={final_state.get('iterations')}")
305
 
306
  except Exception as e:
 
317
  # Local Testing Block (Optional)
318
  # ==============================================================================
319
  # This block allows you to test the agent by running final_agent.py directly.
 
320
  if __name__ == "__main__":
321
  print("\n--- Running Local Test ---")
 
322
  test_question = "What is the result of multiplying the number of rows (excluding the header) in 'data.csv' by the number found after the phrase 'total items:' in 'image.png'?"
 
 
323
  print("Creating dummy files for local test...")
324
  dummy_files_created = True
325
  try:
326
+ with open("data.csv", "w") as f: f.write("Header1,Header2\nRow1Val1,Row1Val2\nRow2Val1,Row2Val2\nRow3Val1,Row3Val2")
 
 
 
 
327
  try:
328
+ img = Image.new('RGB', (300, 50), color = (255, 255, 255))
329
+ from PIL import ImageDraw, ImageFont
330
  draw = ImageDraw.Draw(img)
 
331
  try: font = ImageFont.truetype("arial.ttf", 15)
332
  except IOError: font = ImageFont.load_default()
333
+ draw.text((10,10), "Some random info... total items: 7 ... more text", fill=(0,0,0), font=font)
334
  img.save("image.png")
335
  print("Dummy data.csv and image.png created successfully.")
336
+ except ImportError: print("Pillow/ImageDraw/ImageFont not installed. Cannot create dummy image."); dummy_files_created = False
337
+ except Exception as img_e: print(f"Error creating dummy image: {img_e}"); dummy_files_created = False
338
+ except Exception as file_e: print(f"Error creating dummy files: {file_e}"); dummy_files_created = False
339
+
 
 
 
 
 
 
 
 
 
340
  if dummy_files_created:
 
 
341
  result = answer_gaia_task(question=test_question, file_path=None)
 
342
  print(f"\n--- Local Test Result ---")
 
343
  print(f"Returned Answer: {result}")
344
  print(f"Expected Answer (for dummy files): 21")
345
+ else: print("Skipping test execution due to issues creating dummy files.")
 
346
 
 
347
  print("\nCleaning up dummy files...")
348
  for dummy_file in ["data.csv", "image.png"]:
349
  if os.path.exists(dummy_file):