File size: 26,628 Bytes
8c86b22
 
 
 
 
 
52798f7
8c86b22
 
 
 
 
 
 
 
52798f7
 
8c86b22
 
 
a7b6255
 
 
8c86b22
a7b6255
8c86b22
 
 
a7b6255
8c86b22
 
 
 
 
 
a7b6255
 
8c86b22
a7b6255
 
8c86b22
a7b6255
 
52798f7
 
8c86b22
52798f7
a7b6255
 
 
dce985b
a7b6255
 
 
 
8c86b22
 
a7b6255
 
 
 
52798f7
 
8c86b22
52798f7
8c86b22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52798f7
a7b6255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c86b22
 
 
 
a7b6255
8c86b22
a7b6255
 
 
52798f7
8c86b22
a7b6255
8c86b22
a7b6255
8c86b22
a7b6255
 
 
 
8c86b22
 
 
 
 
a7b6255
 
 
 
8c86b22
 
 
 
a7b6255
 
 
52798f7
a7b6255
8c86b22
a7b6255
8c86b22
 
 
 
a7b6255
 
 
 
 
52798f7
8c86b22
 
 
a7b6255
52798f7
a7b6255
8c86b22
a7b6255
 
 
 
 
 
 
673dfa2
8c86b22
 
 
a7b6255
673dfa2
a7b6255
 
 
8c86b22
a7b6255
8c86b22
a7b6255
 
 
 
 
 
 
 
 
 
8c86b22
 
 
 
 
a7b6255
8c86b22
 
a7b6255
8c86b22
 
a7b6255
 
 
 
8c86b22
a7b6255
 
 
 
8c86b22
 
 
 
 
 
 
a7b6255
8c86b22
 
 
 
 
 
 
 
 
a7b6255
 
 
 
 
 
 
 
 
 
8c86b22
 
a7b6255
 
 
8c86b22
 
 
a7b6255
8c86b22
52798f7
 
8c86b22
52798f7
8c86b22
 
0492949
a7b6255
8c86b22
a7b6255
8c86b22
 
 
 
a7b6255
 
 
 
8c86b22
 
 
 
 
 
a7b6255
8c86b22
 
 
 
 
a7b6255
8c86b22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a7b6255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c86b22
 
 
 
 
a7b6255
8c86b22
 
 
a7b6255
 
 
 
 
 
 
 
8c86b22
 
a7b6255
 
 
 
 
 
 
 
 
 
 
 
52798f7
 
a7b6255
52798f7
8c86b22
a7b6255
 
 
0492949
a7b6255
8c86b22
a7b6255
 
 
8c86b22
a7b6255
 
8c86b22
52798f7
 
a7b6255
52798f7
8c86b22
a7b6255
8c86b22
 
0492949
8c86b22
a7b6255
8c86b22
 
 
 
a7b6255
8c86b22
 
a7b6255
8c86b22
 
a7b6255
 
 
 
8c86b22
 
a7b6255
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# -*- coding: utf-8 -*-
"""
GAIA Benchmark Agent using LangChain, Groq, Tavily, and various tools.
"""

# --- Core Libraries ---
import os
import sys
import subprocess
import time
import importlib
from pathlib import Path
from typing import List, Optional, Dict, Any

# --- Environment & Configuration ---
from dotenv import load_dotenv

# --- LangChain Imports ---
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import BaseTool, tool
# Using Pydantic v2 is recommended if your environment supports it fully
# from pydantic import BaseModel, Field # Pydantic v2
from pydantic import BaseModel, Field # Pydantic v1 compatibility shim
from langchain.memory import ConversationBufferWindowMemory
from langchain.agents import AgentExecutor, create_openai_tools_agent # Keep OpenAI Tools Agent

# --- Tool Specific Imports ---
# Search
from langchain_community.tools.tavily_search import TavilySearchResults
# Web Scraping
import requests
from bs4 import BeautifulSoup
# LLM
from langchain_groq import ChatGroq
# Audio/Video Transcription (Optional)
try: import openai; OPENAI_AVAILABLE = True
except ImportError: OPENAI_AVAILABLE = False
# Excel Reading (Optional)
try: import pandas as pd; PANDAS_AVAILABLE = True
except ImportError: PANDAS_AVAILABLE = False
# YouTube Processing (Optional)
try: from pytube import YouTube, PytubeError; PYTUBE_AVAILABLE = True
except ImportError: PYTUBE_AVAILABLE = False

# ==============================================================================
# 1. CONFIGURATION
# ==============================================================================
load_dotenv()
AGENT_WORKSPACE = Path("./gaia_agent_workspace"); AGENT_WORKSPACE.mkdir(exist_ok=True)
MAX_ITERATIONS = 15; MEMORY_WINDOW_SIZE = 10
GROQ_API_KEY = os.getenv("GROQ_API_KEY"); GROQ_MODEL_NAME = os.getenv("GROQ_MODEL_NAME", "meta-llama/llama-4-maverick-17b-128e-instruct")
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY"); TAVILY_MAX_RESULTS = 3
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY"); WHISPER_MODEL = "whisper-1"
if not GROQ_API_KEY: print("ERROR: GROQ_API_KEY not set."); sys.exit(1)
if not TAVILY_API_KEY: print("Warning: TAVILY_API_KEY not set.")
openai_client = None
if OPENAI_AVAILABLE and OPENAI_API_KEY:
    try: openai_client = openai.OpenAI(api_key=OPENAI_API_KEY); print("OpenAI client initialized.")
    except Exception as e: print(f"Warning: OpenAI client init failed: {e}"); openai_client = None
if not PANDAS_AVAILABLE: print("Info: 'pandas' not installed. Excel tool disabled.")
if not PYTUBE_AVAILABLE: print("Info: 'pytube' not installed. YouTube tool disabled.")

# ==============================================================================
# 2. TOOL DEFINITIONS
# ==============================================================================

# --- Tool Input Schemas (Pydantic Models) ---
class FileWriteArgs(BaseModel):
    relative_path: str = Field(description="Relative path within the agent's workspace where the file should be written.")
    content: str = Field(description="The text content to write into the file.")
class FileReadArgs(BaseModel):
    relative_path: str = Field(description="Relative path within the agent's workspace of the file to read.")
class ListDirectoryArgs(BaseModel):
    relative_path: str = Field(default=".", description="Relative path within the agent's workspace to list contents of. Use '.' for the root.")
class RunPythonCodeArgs(BaseModel):
    code: str = Field(description="The Python code to execute. Use 'print()' to output results. Code runs in isolation.")
class WebScrapeArgs(BaseModel):
    url: str = Field(description="The URL of the webpage to scrape.")
    query: Optional[str] = Field(default=None, description="Optional specific question to answer from the page content.")
class ReadExcelArgs(BaseModel):
    relative_path: str = Field(description="Relative path within the agent's workspace of the Excel file (.xlsx or .xls).")
    sheet_name: Optional[str] = Field(default=None, description="Optional name of the specific sheet to read. Reads the first sheet if not specified.")
    max_rows_preview: int = Field(default=20, description="Maximum number of rows to include in the text preview.")
class TranscribeAudioArgs(BaseModel):
    relative_path: str = Field(description="Relative path within the agent's workspace of the audio file (e.g., .mp3, .wav, .m4a). Max 25MB.")
class TranscribeYouTubeArgs(BaseModel):
    youtube_url: str = Field(description="The URL of the YouTube video to transcribe. Audio will be downloaded temporarily.")

# --- Helper Functions ---
def _resolve_path(relative_path: str) -> Optional[Path]:
    """Resolves a relative path against the workspace and checks bounds."""
    try:
        normalized_relative_path = os.path.normpath(relative_path)
        # Prevent absolute paths or paths trying to escape the workspace
        if os.path.isabs(normalized_relative_path) or ".." in normalized_relative_path.split(os.sep):
             print(f"Error: Invalid path characters or attempt to escape workspace in '{relative_path}'.")
             return None
        full_path = (AGENT_WORKSPACE / normalized_relative_path).resolve()
        if AGENT_WORKSPACE.resolve() in full_path.parents or full_path == AGENT_WORKSPACE.resolve():
             return full_path
        # Check prefix as a fallback, although resolve should handle canonical paths
        if str(full_path).startswith(str(AGENT_WORKSPACE.resolve())):
             print(f"Warning: Path resolution for '{relative_path}' seems complex but within workspace: {full_path}")
             return full_path
        print(f"Error: Path '{relative_path}' resolved to '{full_path}' which is outside the allowed workspace '{AGENT_WORKSPACE.resolve()}'.")
        return None
    except Exception as e:
        print(f"Error resolving path '{relative_path}': {e}")
        return None

def _transcribe_audio(file_path: Path, file_description: str) -> str:
    """Helper to transcribe an audio file using OpenAI Whisper."""
    if not openai_client: return "Error: OpenAI client not available for transcription."
    if not file_path.is_file():
        try: rel_path_str = file_path.relative_to(AGENT_WORKSPACE)
        except ValueError: rel_path_str = file_path
        return f"Error: Audio file not found at '{rel_path_str}'"
    try:
        file_size_mb = file_path.stat().st_size / (1024 * 1024)
        if file_size_mb > 25: return f"Error: Audio file '{file_description}' is too large ({file_size_mb:.2f} MB). Max 25 MB."
        print(f"Transcribing audio: {file_description}...")
        with open(file_path, "rb") as audio_file_handle: transcript = openai_client.audio.transcriptions.create(model=WHISPER_MODEL, file=audio_file_handle, response_format="text")
        print("Transcription complete.")
        if isinstance(transcript, str): max_len = 10000; transcript = transcript[:max_len] + ("\n... [Transcription truncated]" if len(transcript) > max_len else ""); return f"Transcription of '{file_description}':\n{transcript}"
        else: return f"Transcription of '{file_description}' succeeded, but format was unexpected: {type(transcript)}"
    except openai.APIError as e: return f"OpenAI API Error during transcription of '{file_description}': {e}"
    except Exception as e: return f"Error transcribing '{file_description}': {e}"

# --- Tool Implementations ---
@tool("write_file", args_schema=FileWriteArgs)
def write_file(relative_path: str, content: str) -> str:
    """Writes text content to a file within the agent's workspace. Creates parent directories if needed."""
    full_path = _resolve_path(relative_path);
    if not full_path: return f"Error: Invalid or disallowed path '{relative_path}'."
    try: full_path.parent.mkdir(parents=True, exist_ok=True); open(full_path, 'w', encoding='utf-8').write(content); return f"Successfully wrote to file: {relative_path}"
    except Exception as e: return f"Error writing file '{relative_path}': {e}"

@tool("read_file", args_schema=FileReadArgs)
def read_file(relative_path: str) -> str:
    """Reads the text content of a file from the agent's workspace. Limited read size."""
    full_path = _resolve_path(relative_path);
    if not full_path: return f"Error: Invalid or disallowed path '{relative_path}'."
    if not full_path.is_file(): return f"Error: File not found at '{relative_path}'"
    try:
        with open(full_path, 'r', encoding='utf-8') as f: content = f.read(10000); content += "\n... [File truncated due to length]" if len(f.read(1)) > 0 else ""
        return content
    except Exception as e: return f"Error reading file '{relative_path}': {e}"

@tool("list_directory", args_schema=ListDirectoryArgs)
def list_directory(relative_path: str = ".") -> str:
    """Lists the contents (files and directories) of a specified directory within the agent's workspace."""
    target_path = _resolve_path(relative_path);
    if not target_path: return f"Error: Invalid or disallowed path '{relative_path}'."
    if not target_path.is_dir(): return f"Error: '{relative_path}' is not a valid directory."
    try: items = [f.name + ('/' if f.is_dir() else '') for f in target_path.iterdir()]; items.sort(); return f"Contents of '{relative_path}':\n" + "\n".join(items) if items else f"Directory '{relative_path}' is empty."
    except Exception as e: return f"Error listing directory '{relative_path}': {e}"

@tool("run_python_code", args_schema=RunPythonCodeArgs)
def run_python_code(code: str) -> str:
    """Executes Python code in a subprocess and returns the stdout/stderr. Use print() for output. WARNING: Executes arbitrary code."""
    print(f"Executing Python code:\n```python\n{code}\n```")
    try:
        process = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=30, cwd=AGENT_WORKSPACE, check=False)
        output, error = process.stdout, process.stderr
        result = "Execution successful.\n" if process.returncode == 0 else f"Execution failed (Return Code: {process.returncode}).\n"
        if output: max_output = 2000; output = output[:max_output] + ("\n... [Output truncated]" if len(output) > max_output else ""); result += f"Output:\n{output}\n"
        if error: max_error = 1000; error = error[:max_error] + ("\n... [Error truncated]" if len(error) > max_error else ""); result += f"Error Output:\n{error}\n"
        if not output and not error: result += "No output produced." if process.returncode == 0 else "No output or error message produced despite non-zero exit code."
        return result.strip()
    except subprocess.TimeoutExpired: return "Error: Code execution timed out after 30 seconds."
    except Exception as e: return f"Error executing Python code: {e}"

@tool("scrape_webpage", args_schema=WebScrapeArgs)
def scrape_webpage(url: str, query: Optional[str] = None) -> str:
    """Scrapes text content from a given URL using BeautifulSoup. If a query is provided, returns content for the agent to answer it."""
    print(f"Attempting to scrape URL: {url}")
    try:
        space_id = os.getenv("SPACE_ID", "YOUR_SPACE_ID")
        headers = {'User-Agent': f'Mozilla/5.0 (compatible; GAIA-Agent/1.0; +https://huggingface.co/spaces/{space_id})'}
        response = requests.get(url, headers=headers, timeout=20); response.raise_for_status()
        content_type = response.headers.get('content-type', '').lower()
        if 'text/html' not in content_type: return f"Error: Content type of URL {url} is '{content_type}', not HTML. Cannot scrape."
        soup = BeautifulSoup(response.text, 'html.parser')
        for tag in soup(["script", "style", "nav", "footer", "aside", "header", "form", "button", "iframe", "noscript"]): tag.decompose()
        text_content = soup.get_text(separator='\n', strip=True); text_content = '\n'.join(line for line in text_content.splitlines() if line.strip())
        if not text_content: return f"Could not extract meaningful text content from {url} after cleaning."
        max_chars = 10000; text_content = text_content[:max_chars] + ("\n... [Content truncated]" if len(text_content) > max_chars else "")
        print(f"Scraping successful for {url}. Content length (approx): {len(text_content)}")
        if query: return f"Use the following content from {url} to answer the query '{query}':\n\n{text_content}"
        else: return f"Content scraped from {url}:\n\n{text_content}"
    except requests.exceptions.Timeout: return f"Error: Timeout occurred while trying to fetch URL {url}"
    except requests.exceptions.RequestException as e: return f"Error fetching or reading URL {url}: {e}"
    except Exception as e: return f"Error scraping URL {url}: {e}"

if PANDAS_AVAILABLE:
    @tool("read_excel_file", args_schema=ReadExcelArgs)
    def read_excel_file(relative_path: str, sheet_name: Optional[str] = None, max_rows_preview: int = 20) -> str:
        """Reads data from an Excel file (.xlsx or .xls) within the workspace and returns a text preview."""
        full_path = _resolve_path(relative_path);
        if not full_path: return f"Error: Invalid or disallowed path '{relative_path}'."
        if not full_path.is_file(): return f"Error: Excel file not found at '{relative_path}'"
        print(f"Reading Excel file: {relative_path}")
        try:
            excel_file = pd.ExcelFile(full_path)
            if not excel_file.sheet_names: return f"Error: Excel file '{relative_path}' contains no sheets."
            sheet_to_read = sheet_name if sheet_name and sheet_name in excel_file.sheet_names else excel_file.sheet_names[0]
            if sheet_name and sheet_name not in excel_file.sheet_names: print(f"Warning: Sheet '{sheet_name}' not found, reading first sheet '{sheet_to_read}' instead.")
            print(f"Reading sheet '{sheet_to_read}' from {relative_path}")
            df = pd.read_excel(full_path, sheet_name=sheet_to_read)
            if df.empty: return f"Sheet '{sheet_to_read}' in '{relative_path}' is empty."
            output = f"Preview of sheet '{sheet_to_read}' from '{relative_path}' ({df.shape[0]} rows, {df.shape[1]} columns):\n"
            output += df.to_string(max_rows=max_rows_preview, max_cols=15, line_width=120)
            max_output_len = 5000; output = output[:max_output_len] + ("\n... [Output truncated due to length]" if len(output) > max_output_len else "")
            return output
        except Exception as e: return f"Error reading Excel file '{relative_path}': {e}"

if OPENAI_AVAILABLE and openai_client:
    @tool("transcribe_audio_file", args_schema=TranscribeAudioArgs)
    def transcribe_audio_file(relative_path: str) -> str:
        """Transcribes audio content from a file in the workspace using OpenAI Whisper (max 25MB)."""
        full_path = _resolve_path(relative_path);
        if not full_path: return f"Error: Invalid or disallowed path '{relative_path}'."
        return _transcribe_audio(full_path, relative_path)

if PYTUBE_AVAILABLE and OPENAI_AVAILABLE and openai_client:
    @tool("transcribe_youtube_video", args_schema=TranscribeYouTubeArgs)
    def transcribe_youtube_video(youtube_url: str) -> str:
        """Downloads audio from a YouTube URL, transcribes it using OpenAI Whisper, and returns the text."""
        temp_audio_path = None
        try:
            print(f"Processing YouTube URL: {youtube_url}"); yt = YouTube(youtube_url, use_oauth=False, allow_oauth_cache=False)
            print("Fetching available streams...")
            audio_stream = yt.streams.filter(only_audio=True, subtype='webm').order_by('abr').desc().first() or \
                           yt.streams.filter(only_audio=True, subtype='mp4').order_by('abr').desc().first() or \
                           yt.streams.get_audio_only()
            if not audio_stream: return f"Error: No suitable audio stream found for YouTube video: {youtube_url}"
            print(f"Selected audio stream: Itag {audio_stream.itag}, ABR {audio_stream.abr}")
            try: video_id = yt.video_id
            except: video_id = f"vid_{int(time.time())}"
            temp_filename = f"temp_youtube_{video_id}.{audio_stream.subtype or 'mp4'}"
            temp_audio_path = AGENT_WORKSPACE / temp_filename
            print(f"Downloading audio to: {temp_audio_path}...")
            audio_stream.download(output_path=AGENT_WORKSPACE, filename=temp_filename); print("Download complete.")
            result = _transcribe_audio(temp_audio_path, f"YouTube video '{yt.title}'"); return result
        except PytubeError as e: return f"Error processing YouTube video {youtube_url} (PytubeError): {e}"
        except Exception as e: return f"Unexpected error during YouTube transcription {youtube_url}: {e}"
        finally:
            if temp_audio_path and temp_audio_path.exists():
                try: temp_audio_path.unlink(); print(f"Cleaned up temporary file: {temp_audio_path}")
                except Exception as e: print(f"Warning: Failed to delete temp file {temp_audio_path}: {e}")

# ==============================================================================
# 3. AGENT SETUP
# ==============================================================================

# --- Initialize LLM ---
try:
    llm = ChatGroq(temperature=0, model_name=GROQ_MODEL_NAME, groq_api_key=GROQ_API_KEY)
    print(f"Using Groq LLM: {GROQ_MODEL_NAME}")
except Exception as e: print(f"FATAL: Error initializing Groq LLM: {e}"); sys.exit(1)

# --- Assemble Available Tools ---
available_tools = []
if TAVILY_API_KEY:
    try: available_tools.append(TavilySearchResults(max_results=TAVILY_MAX_RESULTS, api_key=TAVILY_API_KEY))
    except Exception as e: print(f"Warning: Failed to initialize Tavily Search tool: {e}. Tool disabled.")
else: print("Warning: Tavily Search tool disabled (API key missing).")
available_tools.extend([write_file, read_file, list_directory, run_python_code, scrape_webpage])
if PANDAS_AVAILABLE: available_tools.append(read_excel_file)
if OPENAI_AVAILABLE and openai_client: available_tools.append(transcribe_audio_file)
if PYTUBE_AVAILABLE and OPENAI_AVAILABLE and openai_client: available_tools.append(transcribe_youtube_video)
print(f"Agent initialized with tools: {[tool.name for tool in available_tools]}")

# --- Define System Prompt ---
# Contains {tools} and {agent_workspace} placeholders.
SYSTEM_PROMPT_TEMPLATE = """You are a highly capable AI assistant designed to solve complex problems step-by-step, mimicking human-like reasoning and actions. Your goal is to accurately answer the user's request based on the GAIA benchmark philosophy.

**Workspace:** You have access to a local workspace directory: '{agent_workspace}'. You can ONLY interact with files inside this directory using the provided tools. Always use relative paths for file operations.

**Available Tools:** You have access to the following tools:
{tools}

**Reasoning Process:**
1.  **Understand:** Analyze the request. Identify objectives, constraints, and required information (text, web search, file content, Excel data, audio/video transcription, calculations).
2.  **Plan:** Break down the problem into logical steps. Choose the *most appropriate* tool for each step.
3.  **Execute:** Perform actions step-by-step using ONE tool at a time. Provide valid arguments for the chosen tool.
4.  **Observe:** Analyze the results (observations) from each tool execution. Note errors or unexpected output.
5.  **Reflect & Adjust:** If a step fails or results are insufficient, analyze the error, refine your plan, and try a different approach or tool. If a file isn't found, consider using `list_directory`. If web search results aren't specific enough, refine your query. If scraping fails, the site might be dynamic or blocking; note this limitation.
6.  **Synthesize:** Once all necessary information is gathered and actions performed, combine the findings to formulate the final answer.
7.  **Final Answer:** Provide ONLY the final answer in the precise format requested by the task. Do not include explanations, commentary, or conversational text unless explicitly asked for. If the task requires creating a file, use `write_file` and state the relative path if needed as the final answer.

**Important Guidelines:**
*   Think step-by-step. Be methodical.
*   Use file/audio/excel tools ONLY for the designated workspace: {agent_workspace}. Use relative paths.
*   Check file existence with `list_directory` before attempting to read if unsure.
*   Use `read_excel_file` for `.xlsx` or `.xls` files.
*   Use `transcribe_audio_file` for local audio files (e.g., .mp3, .wav). Max 25MB.
*   Use `transcribe_youtube_video` for YouTube URLs. Max 25MB audio download.
*   Use `run_python_code` for calculations or data manipulation not covered by other tools. Use `print()` for output.
*   Use `tavily_search_results_json` for web searches. Use `scrape_webpage` to get content from a specific URL found in search or given in the prompt.
*   Adhere strictly to the requested final answer format.
"""

# --- Create Prompt Template ---
# Pre-format the system prompt string fully before creating the template
try:
    # Format the tool descriptions manually using the render_text_description utility
    from langchain.tools.render import render_text_description
    tool_descriptions = render_text_description(available_tools)

    # Format the entire system prompt string
    formatted_system_prompt = SYSTEM_PROMPT_TEMPLATE.format(
        agent_workspace=str(AGENT_WORKSPACE.resolve()),
        tools=tool_descriptions
    )

    # Create the template from the fully formatted string
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", formatted_system_prompt), # Use the pre-formatted string
            MessagesPlaceholder(variable_name="chat_history"),
            ("human", "{input}"),
            MessagesPlaceholder(variable_name="agent_scratchpad"), # Still needed by the agent type
        ]
    )

except Exception as e:
    print(f"FATAL: Error creating ChatPromptTemplate: {e}")
    sys.exit(1)


# --- Setup Memory ---
memory = ConversationBufferWindowMemory(
    k=MEMORY_WINDOW_SIZE,
    memory_key="chat_history",
    return_messages=True
)

# --- Create Agent ---
# Using create_openai_tools_agent
try:
    agent = create_openai_tools_agent(llm, available_tools, prompt)
except Exception as e:
    print(f"FATAL: Error creating agent with create_openai_tools_agent: {e}")
    import traceback
    traceback.print_exc()
    sys.exit(1)

# --- Create Agent Executor ---
try:
    agent_executor = AgentExecutor(
        agent=agent,
        tools=available_tools,
        memory=memory,
        verbose=True,
        max_iterations=MAX_ITERATIONS,
        handle_parsing_errors=True,
    )
except Exception as e:
    print(f"FATAL: Error creating AgentExecutor: {e}")
    sys.exit(1)

# ==============================================================================
# 4. EXECUTION FUNCTION (Exported for app.py)
# ==============================================================================
def run_gaia_task(task_description: str):
    """Runs the GAIA agent on a given task description. This is the main entry point."""
    print("\n" + "="*50 + f"\n🚀 Running GAIA Task\n📝 Task: {task_description[:150]}...\n📍 Workspace: {AGENT_WORKSPACE.resolve()}\n🛠️ Tools: {[tool.name for tool in available_tools]}\n" + "="*50 + "\n")
    memory.clear() # Reset memory for the task
    try:
        if 'agent_executor' not in globals() or agent_executor is None: return "Error: Agent Executor not initialized."
        result = agent_executor.invoke({"input": task_description})
        final_output = result.get('output', 'Agent finished but produced no output.')
        print("\n" + "="*50 + f"\n✅ Agent Execution Finished\n🏁 Final Output:\n{final_output}\n" + "="*50 + "\n")
        return str(final_output)
    except Exception as e:
        print(f"\n{'='*50}\n❌ Agent Execution Error during task run\nAn error occurred: {e}\n{'='*50}\n")
        import traceback; traceback.print_exc() # Print full traceback for debugging
        return f"Agent failed with error: {e}"

# ==============================================================================
# 5. EXAMPLE USAGE (Local Testing)
# ==============================================================================
if __name__ == "__main__":
    print("\n" + "*"*30 + " LOCAL TEST RUN " + "*"*30)
    print("--- Setting up example files (if needed) ---")
    if PANDAS_AVAILABLE:
        try:
            dummy_excel_path = AGENT_WORKSPACE / "sample_data.xlsx"
            if not dummy_excel_path.exists(): pd.DataFrame({'ID': [1, 2, 3], 'Product': ['Widget', 'Gadget', 'Thingamajig']}).to_excel(dummy_excel_path, index=False); print(f"Created dummy Excel: {dummy_excel_path}")
        except Exception as e: print(f"Could not create dummy Excel: {e}")
    try:
        dummy_text_path = AGENT_WORKSPACE / "numbers.txt"
        if not dummy_text_path.exists():
            with open(dummy_text_path, "w") as f: f.write("15\n-3\n42.5\n100\n"); print(f"Created dummy text file: {dummy_text_path}")
    except Exception as e: print(f"Could not create dummy text file: {e}")
    dummy_audio_path = AGENT_WORKSPACE / "sample_audio.mp3"
    if not dummy_audio_path.exists() and OPENAI_AVAILABLE and openai_client: print(f"INFO: To test audio transcription, place an MP3 file at: {dummy_audio_path}")
    print("--- Example setup complete ---")

    example_tasks = [
        {"id": "local_excel_read", "description": "Read the file 'sample_data.xlsx' in the workspace. What is the 'Product' where 'ID' is 2? Final answer should be just the product name."},
        {"id": "local_python_sum", "description": "Read the numbers from 'numbers.txt' in the workspace (one per line). Calculate their sum using python code. Write the sum into 'sum_result.txt'. Final answer should be the relative path 'sum_result.txt'."},
        {"id": "local_search_scrape_write", "description": "Search the web for the official website of the Python Software Foundation. Scrape the main title from the homepage of that website. Write the title into 'psf_title.txt'. Final answer is 'psf_title.txt'."},
    ]

    if example_tasks:
        task_to_run = example_tasks[0] # Change index to test different tasks
        print(f"\n>>> Running local test task: {task_to_run['id']} <<<")
        final_answer = run_gaia_task(task_to_run['description'])
        print(f">>> Local test task {task_to_run['id']} completed. Agent Output: {final_answer} <<<")
    else: print("No example tasks defined for local testing.")
    print("\n" + "*"*30 + " LOCAL TEST RUN COMPLETE " + "*"*30)