jesusvilela commited on
Commit
3c1bb16
·
verified ·
1 Parent(s): 1bc2237

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +566 -6
app.py CHANGED
@@ -1,25 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  def initialize_agent_and_tools(force_reinit=False):
2
  global AGENT_INSTANCE, TOOLS, LLM_INSTANCE, LANGGRAPH_FLAVOR_AVAILABLE, LG_StateGraph, LG_ToolExecutor_Class, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class, LANGGRAPH_MEMORY_SAVER, google_genai_client
3
  if AGENT_INSTANCE and not force_reinit: logger.info("Agent already initialized."); return
4
  logger.info("Initializing agent and tools...")
5
  if not GOOGLE_API_KEY: raise ValueError("GOOGLE_API_KEY not set for LangChain LLM.")
6
-
7
- # CORRECCIÓN: Usa los .value, no strings
8
- from google.genai.types import HarmCategory, HarmBlockThreshold
9
-
10
  llm_safety_settings_corrected_final = {
11
  HarmCategory.HARM_CATEGORY_HARASSMENT.value: HarmBlockThreshold.BLOCK_NONE.value,
12
  HarmCategory.HARM_CATEGORY_HATE_SPEECH.value: HarmBlockThreshold.BLOCK_NONE.value,
13
  HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT.value: HarmBlockThreshold.BLOCK_NONE.value,
14
  HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT.value: HarmBlockThreshold.BLOCK_NONE.value,
15
  }
16
-
17
  try:
18
  LLM_INSTANCE = ChatGoogleGenerativeAI(
19
  model=GEMINI_MODEL_NAME,
20
  google_api_key=GOOGLE_API_KEY,
21
  temperature=0.0,
22
- safety_settings=llm_safety_settings_corrected_final, # AQUÍ EL CAMBIO
23
  timeout=120,
24
  convert_system_message_to_human=True
25
  )
@@ -62,6 +455,7 @@ def initialize_agent_and_tools(force_reinit=False):
62
  if not LG_ToolExecutor_Class: raise ValueError("LG_ToolExecutor_Class is None for LangGraph.")
63
  tool_executor_instance_lg = LG_ToolExecutor_Class(tools=TOOLS)
64
 
 
65
  def tool_node(state: AgentState):
66
  last_msg = state['messages'][-1] if state.get('messages') and isinstance(state['messages'][-1], AIMessage) else None
67
  if not last_msg or not last_msg.tool_calls: return {"messages": []}
@@ -112,3 +506,169 @@ def initialize_agent_and_tools(force_reinit=False):
112
 
113
  if not AGENT_INSTANCE: raise RuntimeError("CRITICAL: Agent initialization completely failed.")
114
  logger.info(f"Agent init finished. Active agent type: {type(AGENT_INSTANCE).__name__}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Jesus Vilela Jato.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # --- Imports ---
16
+ import os
17
+ import gradio as gr
18
+ import requests
19
+ import pandas as pd
20
+ import json
21
+ import logging
22
+ from typing import Optional, List, Dict, Any, Tuple, Union, Type, TYPE_CHECKING, Annotated
23
+ import hashlib
24
+ from urllib.parse import urlparse
25
+ import mimetypes
26
+ import subprocess # For yt-dlp
27
+ import io # For BytesIO with PIL
28
+
29
+ # --- Global Variables for Startup Status ---
30
+ missing_vars_startup_list_global = []
31
+ agent_pre_init_status_msg_global = "Agent status will be determined at startup."
32
+
33
+ # File Processing Libs
34
+ try: from PyPDF2 import PdfReader; PYPDF2_AVAILABLE = True
35
+ except ImportError: PYPDF2_AVAILABLE = False; print("WARNING: PyPDF2 not found, PDF tool will be disabled.")
36
+ try: from PIL import Image; import pytesseract; PIL_TESSERACT_AVAILABLE = True
37
+ except ImportError: PIL_TESSERACT_AVAILABLE = False; print("WARNING: Pillow or Pytesseract not found, OCR tool will be disabled.")
38
+ try: import whisper; WHISPER_AVAILABLE = True
39
+ except ImportError: WHISPER_AVAILABLE = False; print("WARNING: OpenAI Whisper not found, Audio Transcription tool will be disabled.")
40
+
41
+ # --- google-genai SDK (Unified SDK) ---
42
+ from google import genai as google_genai_sdk # Alias for clarity
43
+ from google.genai.types import HarmCategory, HarmBlockThreshold # ***** CORRECTED IMPORT *****
44
+ # For FileState enum later
45
+ from google.ai import generativelanguage as glm
46
+ # --- End google-genai SDK ---
47
+
48
+
49
+ # LangChain
50
+ from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, ToolMessage
51
+ from langchain.prompts import PromptTemplate
52
+ from langchain.tools import BaseTool, tool as lc_tool_decorator
53
+ from langchain_google_genai import ChatGoogleGenerativeAI
54
+ from langchain.agents import AgentExecutor, create_react_agent
55
+ from langchain_community.tools import DuckDuckGoSearchRun
56
+ from langchain_experimental.tools import PythonREPLTool
57
+
58
+ # LangGraph Conditional Imports
59
+ if TYPE_CHECKING:
60
+ from langgraph.graph import StateGraph as StateGraphAliasedForHinting
61
+ from langgraph.prebuilt import ToolNode as ToolExecutorAliasedForHinting
62
+ from typing_extensions import TypedDict
63
+ from langgraph.checkpoint.base import BaseCheckpointSaver
64
+
65
+ LANGGRAPH_FLAVOR_AVAILABLE = False
66
+ LG_StateGraph: Optional[Type[Any]] = None
67
+ LG_ToolExecutor_Class: Optional[Type[Any]] = None
68
+ LG_END: Optional[Any] = None
69
+ LG_ToolInvocation: Optional[Type[Any]] = None
70
+ add_messages: Optional[Any] = None
71
+ MemorySaver_Class: Optional[Type[Any]] = None
72
+
73
+ AGENT_INSTANCE: Optional[Union[AgentExecutor, Any]] = None
74
+ TOOLS: List[BaseTool] = []
75
+ LLM_INSTANCE: Optional[ChatGoogleGenerativeAI] = None
76
+ LANGGRAPH_MEMORY_SAVER: Optional[Any] = None
77
+
78
+ google_genai_client: Optional[google_genai_sdk.Client] = None # For direct SDK calls
79
+
80
+ try:
81
+ from langgraph.graph import StateGraph, END
82
+ try:
83
+ from langgraph.prebuilt import ToolNode
84
+ LG_ToolExecutor_Class = ToolNode
85
+ print("Using langgraph.prebuilt.ToolNode for LangGraph tool execution.")
86
+ except ImportError:
87
+ try:
88
+ from langgraph.prebuilt import ToolExecutor
89
+ LG_ToolExecutor_Class = ToolExecutor
90
+ print("Using langgraph.prebuilt.ToolExecutor (fallback) for LangGraph tool execution.")
91
+ except ImportError as e_lg_exec_inner:
92
+ print(f"Failed to import ToolNode and ToolExecutor from langgraph.prebuilt: {e_lg_exec_inner}")
93
+ LG_ToolExecutor_Class = None
94
+
95
+ if LG_ToolExecutor_Class is not None:
96
+ from langgraph.prebuilt import ToolInvocation as LGToolInvocationActual
97
+ from langgraph.graph.message import add_messages as lg_add_messages
98
+ from langgraph.checkpoint.memory import MemorySaver as LGMemorySaver
99
+ LANGGRAPH_FLAVOR_AVAILABLE = True
100
+ LG_StateGraph, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = \
101
+ StateGraph, END, LGToolInvocationActual, lg_add_messages, LGMemorySaver
102
+ print("Successfully imported LangGraph components.")
103
+ else:
104
+ LANGGRAPH_FLAVOR_AVAILABLE = False
105
+ LG_StateGraph, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = (None,) * 5
106
+ print(f"WARNING: No suitable LangGraph tool executor (ToolNode/ToolExecutor) found. LangGraph agent will be disabled.")
107
+
108
+ except ImportError as e: # Catch import error for StateGraph, END itself
109
+ LANGGRAPH_FLAVOR_AVAILABLE = False
110
+ LG_StateGraph, LG_ToolExecutor_Class, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = (None,) * 6
111
+ print(f"WARNING: Core LangGraph components (StateGraph, END) not found or import error: {e}. LangGraph agent will be disabled.")
112
+
113
+
114
+ # --- Constants ---
115
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
116
+ GEMINI_MODEL_NAME = "gemini-2.5-pro-preview-05-06"
117
+ GEMINI_FLASH_MULTIMODAL_MODEL_NAME = "gemini-2.0-flash-exp"
118
+ SCORING_API_BASE_URL = os.getenv("SCORING_API_URL", DEFAULT_API_URL)
119
+ MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024
120
+ LOCAL_FILE_STORE_PATH = "./Data"
121
+ os.makedirs(LOCAL_FILE_STORE_PATH, exist_ok=True)
122
+
123
+ # --- Global State ---
124
+ WHISPER_MODEL: Optional[Any] = None
125
+
126
+ # --- Environment Variables & API Keys ---
127
+ GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
128
+ HUGGINGFACE_TOKEN = os.environ.get("HF_TOKEN")
129
+
130
+ # --- Setup Logging ---
131
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(module)s:%(lineno)d - %(message)s')
132
+ logger = logging.getLogger(__name__)
133
+
134
+ # --- Initialize google-genai Client SDK ---
135
+ if GOOGLE_API_KEY:
136
+ try:
137
+ google_genai_client = google_genai_sdk.Client(api_key=GOOGLE_API_KEY) # Using the aliased import
138
+ logger.info("google-genai SDK Client initialized successfully.")
139
+ except Exception as e:
140
+ logger.error(f"Failed to initialize google-genai SDK Client: {e}")
141
+ google_genai_client = None
142
+ else:
143
+ logger.warning("GOOGLE_API_KEY not found. google-genai SDK Client not initialized.")
144
+
145
+ # --- Helper Functions (Unchanged) ---
146
+ def _strip_exact_match_answer(text: Any) -> str:
147
+ # ... (Your original _strip_exact_match_answer function)
148
+ if not isinstance(text, str): text = str(text)
149
+ text_lower_check = text.lower()
150
+ if text_lower_check.startswith("final answer:"):
151
+ text = text[len("final answer:"):].strip()
152
+ text = text.strip()
153
+ if text.startswith("
154
+ ") and text.endswith("
155
+ "):
156
+ if "\n" in text:
157
+ text_content = text.split("\n", 1)[1] if len(text.split("\n", 1)) > 1 else ""
158
+ text = text_content.strip()[:-3].strip() if text_content.strip().endswith("
159
+ ") else text[3:-3].strip()
160
+ else: text = text[3:-3].strip()
161
+ elif text.startswith("`") and text.endswith("`"): text = text[1:-1].strip()
162
+ if (text.startswith('"') and text.endswith('"')) or \
163
+ (text.startswith("'") and text.endswith("'")):
164
+ if len(text) > 1: text = text[1:-1]
165
+ return text.strip()
166
+
167
+ def _is_full_url(url_string: str) -> bool:
168
+ # ... (Your original _is_full_url function)
169
+ try: result = urlparse(url_string); return all([result.scheme, result.netloc])
170
+ except ValueError: return False
171
+
172
+ def _is_youtube_url(url: str) -> bool:
173
+ # ... (Your original _is_youtube_url function)
174
+ parsed_url = urlparse(url)
175
+ return parsed_url.netloc.lower().endswith(("youtube.com", "youtu.be"))
176
+
177
+ def _download_file(file_identifier: str, task_id_for_file: Optional[str] = None) -> str:
178
+ # ... (Your original _download_file function - unchanged) ...
179
+ os.makedirs(LOCAL_FILE_STORE_PATH, exist_ok=True)
180
+ logger.debug(f"Download request: '{file_identifier}', task_id: {task_id_for_file}")
181
+ original_filename = os.path.basename(urlparse(file_identifier).path) if _is_full_url(file_identifier) else os.path.basename(file_identifier)
182
+ if not original_filename or original_filename == '/':
183
+ original_filename = hashlib.md5(file_identifier.encode()).hexdigest()[:12] + ".download"
184
+ prefix = f"{task_id_for_file}_" if task_id_for_file else ""
185
+ sanitized_original_filename = "".join(c if c.isalnum() or c in ['.', '_', '-'] else '_' for c in original_filename)
186
+ tentative_local_path = os.path.join(LOCAL_FILE_STORE_PATH, f"{prefix}{sanitized_original_filename}")
187
+
188
+ if _is_full_url(file_identifier) and _is_youtube_url(file_identifier):
189
+ logger.info(f"YouTube URL: {file_identifier}. Using yt-dlp.")
190
+ yt_file_hash = hashlib.md5(file_identifier.encode()).hexdigest()[:10]
191
+ yt_filename_base = f"youtube_{prefix}{yt_file_hash}"
192
+ target_mp3_path = os.path.join(LOCAL_FILE_STORE_PATH, yt_filename_base + ".mp3")
193
+ if os.path.exists(target_mp3_path) and os.path.getsize(target_mp3_path) > 0:
194
+ logger.info(f"Cached YouTube MP3: {target_mp3_path}"); return target_mp3_path
195
+ temp_output_template = os.path.join(LOCAL_FILE_STORE_PATH, yt_filename_base + "_temp.%(ext)s")
196
+ try:
197
+ command = ['yt-dlp', '--quiet', '--no-warnings', '-x', '--audio-format', 'mp3',
198
+ '--audio-quality', '0', '--max-filesize', str(MAX_FILE_SIZE_BYTES),
199
+ '-o', temp_output_template, file_identifier]
200
+ logger.info(f"yt-dlp command: {' '.join(command)}")
201
+ process = subprocess.run(command, capture_output=True, text=True, timeout=180, check=False)
202
+ downloaded_temp_file = next((os.path.join(LOCAL_FILE_STORE_PATH, f) for f in os.listdir(LOCAL_FILE_STORE_PATH)
203
+ if f.startswith(yt_filename_base + "_temp") and f.endswith(".mp3")), None)
204
+ if process.returncode == 0 and downloaded_temp_file and os.path.exists(downloaded_temp_file):
205
+ os.rename(downloaded_temp_file, target_mp3_path)
206
+ logger.info(f"yt-dlp success: {target_mp3_path}"); return target_mp3_path
207
+ else:
208
+ err_msg = process.stderr.strip() if process.stderr else "Unknown yt-dlp error"
209
+ logger.error(f"yt-dlp failed. RC:{process.returncode}. File:{downloaded_temp_file}. Err:{err_msg[:500]}")
210
+ if downloaded_temp_file and os.path.exists(downloaded_temp_file): os.remove(downloaded_temp_file)
211
+ return f"Error: yt-dlp failed. Msg:{err_msg[:200]}"
212
+ except Exception as e: logger.error(f"yt-dlp exception: {e}", exc_info=True); return f"Error: yt-dlp exception: {str(e)[:200]}"
213
+
214
+ file_url_to_try = file_identifier if _is_full_url(file_identifier) else None
215
+ if not file_url_to_try and task_id_for_file:
216
+ file_url_to_try = f"{SCORING_API_BASE_URL.rstrip('/')}/files/{task_id_for_file}"
217
+ elif not file_url_to_try:
218
+ if os.path.exists(file_identifier): logger.info(f"Using local file: {file_identifier}"); return file_identifier
219
+ return f"Error: Not URL, not local, no task_id for '{file_identifier}'."
220
+
221
+ if os.path.exists(tentative_local_path) and os.path.getsize(tentative_local_path) > 0:
222
+ logger.info(f"Cached file (pre-CD): {tentative_local_path}"); return tentative_local_path
223
+ effective_save_path = tentative_local_path
224
+ try:
225
+ auth_headers = {"Authorization": f"Bearer {HUGGINGFACE_TOKEN}"} if HUGGINGFACE_TOKEN and \
226
+ any(s in file_url_to_try for s in [SCORING_API_BASE_URL, ".hf.space", "huggingface.co"]) else {}
227
+ logger.info(f"Standard download: {file_url_to_try} (Headers: {list(auth_headers.keys())})")
228
+ with requests.get(file_url_to_try, stream=True, headers=auth_headers, timeout=60) as r:
229
+ r.raise_for_status()
230
+ cd_header = r.headers.get('content-disposition')
231
+ filename_from_cd = None
232
+ if cd_header:
233
+ try:
234
+ decoded_cd_header = cd_header.encode('latin-1', 'replace').decode('utf-8', 'replace')
235
+ _, params = requests.utils.parse_header_links(decoded_cd_header) # type: ignore
236
+ for key, val in params.items():
237
+ if key.lower() == 'filename*' and val.lower().startswith("utf-8''"):
238
+ filename_from_cd = requests.utils.unquote(val[len("utf-8''"):]); break
239
+ elif key.lower() == 'filename':
240
+ filename_from_cd = requests.utils.unquote(val)
241
+ if filename_from_cd.startswith('"') and filename_from_cd.endswith('"'): filename_from_cd = filename_from_cd[1:-1]
242
+ break
243
+ except Exception as e_cd: logger.warning(f"CD parse error '{cd_header}': {e_cd}")
244
+ if filename_from_cd:
245
+ sanitized_cd_filename = "".join(c if c.isalnum() or c in ['.', '_', '-'] else '_' for c in filename_from_cd)
246
+ effective_save_path = os.path.join(LOCAL_FILE_STORE_PATH, f"{prefix}{sanitized_cd_filename}")
247
+ logger.info(f"Using CD filename: '{sanitized_cd_filename}'. Path: {effective_save_path}")
248
+
249
+ name_without_ext, current_ext = os.path.splitext(effective_save_path)
250
+ if not current_ext:
251
+ content_type_header = r.headers.get('content-type', '')
252
+ content_type_val = content_type_header.split(';')[0].strip() if content_type_header else ''
253
+ if content_type_val:
254
+ guessed_ext = mimetypes.guess_extension(content_type_val)
255
+ if guessed_ext: effective_save_path += guessed_ext; logger.info(f"Added guessed ext: {guessed_ext}")
256
+
257
+ if effective_save_path != tentative_local_path and os.path.exists(effective_save_path) and os.path.getsize(effective_save_path) > 0:
258
+ logger.info(f"Cached file (CD name): {effective_save_path}"); return effective_save_path
259
+ with open(effective_save_path, "wb") as f_download:
260
+ for chunk in r.iter_content(chunk_size=1024*1024): f_download.write(chunk)
261
+ logger.info(f"File downloaded to {effective_save_path}"); return effective_save_path
262
+ except requests.exceptions.HTTPError as e:
263
+ err_msg = f"HTTP {e.response.status_code} for {file_url_to_try}. Detail: {e.response.text[:100]}"
264
+ logger.error(err_msg, exc_info=False); return f"Error downloading: {err_msg}"
265
+ except Exception as e:
266
+ logger.error(f"Download error for {file_url_to_try}: {e}", exc_info=True); return f"Error: {str(e)[:100]}"
267
+
268
+ # --- Tool Function Definitions ---
269
+ READ_PDF_TOOL_DESC = "Reads text content from a PDF file. Input: JSON '{\"file_identifier\": \"FILENAME_OR_URL\", \"task_id\": \"TASK_ID_IF_GAIA_FILENAME_ONLY\"}'. Returns extracted text."
270
+ @lc_tool_decorator(description=READ_PDF_TOOL_DESC)
271
+ def read_pdf_tool(action_input_json_str: str) -> str:
272
+ # ... (Your original read_pdf_tool logic)
273
+ if not PYPDF2_AVAILABLE: return "Error: PyPDF2 not installed."
274
+ try: data = json.loads(action_input_json_str); file_id, task_id = data.get("file_identifier"), data.get("task_id")
275
+ except Exception as e: return f"Error parsing JSON for read_pdf_tool: {e}. Input: {action_input_json_str}"
276
+ if not file_id: return "Error: 'file_identifier' missing."
277
+ path = _download_file(file_id, task_id)
278
+ if path.startswith("Error:"): return path
279
+ try:
280
+ text_content = "";
281
+ with open(path, "rb") as f_pdf:
282
+ reader = PdfReader(f_pdf)
283
+ if reader.is_encrypted:
284
+ try: reader.decrypt('')
285
+ except: return f"Error: PDF '{path}' encrypted."
286
+ for page_num in range(len(reader.pages)):
287
+ page = reader.pages[page_num]
288
+ text_content += page.extract_text() + "\n\n"
289
+ return text_content[:40000]
290
+ except Exception as e: return f"Error reading PDF '{path}': {e}"
291
+
292
+ OCR_IMAGE_TOOL_DESC = "Extracts text from an image using OCR. Input: JSON '{\"file_identifier\": \"FILENAME_OR_URL\", \"task_id\": \"TASK_ID_IF_GAIA_FILENAME_ONLY\"}'. Returns extracted text."
293
+ @lc_tool_decorator(description=OCR_IMAGE_TOOL_DESC)
294
+ def ocr_image_tool(action_input_json_str: str) -> str:
295
+ # ... (Your original ocr_image_tool logic)
296
+ if not PIL_TESSERACT_AVAILABLE: return "Error: Pillow/Pytesseract not installed."
297
+ try: data = json.loads(action_input_json_str); file_id, task_id = data.get("file_identifier"), data.get("task_id")
298
+ except Exception as e: return f"Error parsing JSON for ocr_image_tool: {e}. Input: {action_input_json_str}"
299
+ if not file_id: return "Error: 'file_identifier' missing."
300
+ path = _download_file(file_id, task_id)
301
+ if path.startswith("Error:"): return path
302
+ try: return pytesseract.image_to_string(Image.open(path))[:40000]
303
+ except Exception as e: return f"Error OCR'ing '{path}': {e}"
304
+
305
+ TRANSCRIBE_AUDIO_TOOL_DESC = "Transcribes speech from an audio file (or YouTube URL) to text. Input: JSON '{\"file_identifier\": \"FILENAME_OR_URL_OR_YOUTUBE_URL\", \"task_id\": \"TASK_ID_IF_GAIA_FILENAME_ONLY\"}'. Returns transcript."
306
+ @lc_tool_decorator(description=TRANSCRIBE_AUDIO_TOOL_DESC)
307
+ def transcribe_audio_tool(action_input_json_str: str) -> str:
308
+ # ... (Your original transcribe_audio_tool logic)
309
+ global WHISPER_MODEL
310
+ if not WHISPER_AVAILABLE: return "Error: Whisper not installed."
311
+ try: data = json.loads(action_input_json_str); file_id, task_id = data.get("file_identifier"), data.get("task_id")
312
+ except Exception as e: return f"Error parsing JSON for transcribe_audio_tool: {e}. Input: {action_input_json_str}"
313
+ if not file_id: return "Error: 'file_identifier' missing."
314
+ if WHISPER_MODEL is None:
315
+ try: WHISPER_MODEL = whisper.load_model("base"); logger.info("Whisper 'base' model loaded.")
316
+ except Exception as e: logger.error(f"Whisper load failed: {e}"); return f"Error: Whisper load: {e}"
317
+ path = _download_file(file_id, task_id)
318
+ if path.startswith("Error:"): return path
319
+ try: result = WHISPER_MODEL.transcribe(path, fp16=False); return result["text"][:40000] # type: ignore
320
+ except Exception as e: logger.error(f"Whisper error on '{path}': {e}", exc_info=True); return f"Error transcribing '{path}': {e}"
321
+
322
+ DIRECT_MULTIMODAL_GEMINI_TOOL_DESC = (
323
+ "Processes an image file (URL or local path) along with a text prompt using a Gemini multimodal model (gemini-2.0-flash-exp) "
324
+ "for tasks like image description, Q&A about the image, or text generation based on the image. "
325
+ "Input: JSON '{\"file_identifier\": \"IMAGE_FILENAME_OR_URL\", \"text_prompt\": \"Your question or instruction.\", \"task_id\": \"TASK_ID\" (optional)}'. "
326
+ "Returns the model's text response."
327
+ )
328
+ @lc_tool_decorator(description=DIRECT_MULTIMODAL_GEMINI_TOOL_DESC)
329
+ def direct_multimodal_gemini_tool(action_input_json_str: str) -> str:
330
+ # ... (Implementation from previous response)
331
+ global google_genai_client
332
+ if not google_genai_client: return "Error: google-genai SDK client not initialized."
333
+ if not PIL_TESSERACT_AVAILABLE : return "Error: Pillow (PIL) library not available."
334
+ try:
335
+ data = json.loads(action_input_json_str)
336
+ file_identifier = data.get("file_identifier")
337
+ text_prompt = data.get("text_prompt", "Describe this image.")
338
+ task_id = data.get("task_id")
339
+ if not file_identifier: return "Error: 'file_identifier' for image missing."
340
+ logger.info(f"Direct Multimodal Tool: Image '{file_identifier}', Prompt '{text_prompt}'")
341
+ local_image_path = _download_file(file_identifier, task_id)
342
+ if local_image_path.startswith("Error:"): return f"Error downloading for Direct MM Tool: {local_image_path}"
343
+ try:
344
+ pil_image = Image.open(local_image_path)
345
+ except Exception as e_img_open: return f"Error opening image {local_image_path}: {str(e_img_open)}"
346
+ # Use the google_genai_client (which is google.genai.Client)
347
+ # For the client SDK, model names often don't need "models/" prefix if it's a tuned model or specific ID.
348
+ # If it's a base model, "models/" is usually required. Let's assume GEMINI_FLASH_MULTIMODAL_MODEL_NAME is a direct ID.
349
+ # However, to be safe with client.models.generate_content, using "models/" is more standard.
350
+ model_id_for_client = f"models/{GEMINI_FLASH_MULTIMODAL_MODEL_NAME}" if not GEMINI_FLASH_MULTIMODAL_MODEL_NAME.startswith("models/") else GEMINI_FLASH_MULTIMODAL_MODEL_NAME
351
+
352
+ response = google_genai_client.models.generate_content(
353
+ model=model_id_for_client,
354
+ contents=[pil_image, text_prompt]
355
+ )
356
+ logger.info(f"Direct Multimodal Tool: Response received from {model_id_for_client} received.")
357
+ return response.text[:40000]
358
+ except json.JSONDecodeError as e_json_mm: return f"Error parsing JSON for Direct MM Tool: {str(e_json_mm)}. Input: {action_input_json_str}"
359
+ except Exception as e_tool_mm:
360
+ logger.error(f"Error in direct_multimodal_gemini_tool: {e_tool_mm}", exc_info=True)
361
+ return f"Error executing Direct Multimodal Tool: {str(e_tool_mm)}"
362
+
363
+ # --- Agent Prompts (Unchanged) ---
364
+ LANGGRAPH_PROMPT_TEMPLATE_STR = """You are a highly intelligent agent for the GAIA benchmark.
365
+ Your goal is to provide an EXACT MATCH final answer. No conversational text, explanations, or markdown unless explicitly part of the answer.
366
+ TOOLS:
367
+ You have access to the following tools. Use them if necessary.
368
+ {tools}
369
+ TOOL USAGE:
370
+ - To use a tool, your response must include a `tool_calls` attribute in the AIMessage. Each tool call should be a dictionary with "name", "args" (a dictionary of arguments), and "id".
371
+ - For file tools ('read_pdf_tool', 'ocr_image_tool', 'transcribe_audio_tool', 'direct_multimodal_gemini_tool'): `args` must contain 'file_identifier' (filename/URL) and 'task_id' (if GAIA file). For 'direct_multimodal_gemini_tool', also include 'text_prompt'.
372
+ - 'web_search': `args` is like '{{"query": "search query"}}'.
373
+ - 'python_repl': `args` is like '{{"command": "python code string"}}'. Use print() for output.
374
+ RESPONSE FORMAT:
375
+ Final AIMessage should contain ONLY the answer in 'content' and NO 'tool_calls'. If using tools, 'content' can be thought process, with 'tool_calls'.
376
+ Begin!
377
+ Current Task Details (including Task ID and any associated files):
378
+ {input}"""
379
+
380
+ REACT_PROMPT_TEMPLATE_STR = """You are a highly intelligent agent for the GAIA benchmark.
381
+ Goal: EXACT MATCH answer. No extra text/markdown.
382
+ Tools: {tools}
383
+ Process: Question -> Thought -> Action (ONE of [{tool_names}]) -> Action Input -> Observation -> Thought ... -> Final Answer: [exact answer]
384
+ Tool Inputs:
385
+ - web_search: Your search query string.
386
+ - python_repl: Python code string. Use print(). For Excel/CSV, use pandas: import pandas as pd; df = pd.read_excel('./Data/TASKID_filename.xlsx'); print(df.head())
387
+ - read_pdf_tool, ocr_image_tool, transcribe_audio_tool: JSON string like '{{"file_identifier": "FILENAME_OR_URL", "task_id": "CURRENT_TASK_ID_IF_FILENAME"}}'.
388
+ - direct_multimodal_gemini_tool: JSON string like '{{"file_identifier": "IMAGE_FILENAME_OR_URL", "text_prompt": "Your prompt for the image.", "task_id": "TASK_ID_IF_GAIA_FILENAME"}}'.
389
+ If tool fails or info missing, Final Answer: N/A. Do NOT use unlisted tools.
390
+ Begin!
391
+ {input}
392
+ Thought:{agent_scratchpad}"""
393
+
394
+
395
+ # --- Agent Initialization and Response Logic ---
396
  def initialize_agent_and_tools(force_reinit=False):
397
  global AGENT_INSTANCE, TOOLS, LLM_INSTANCE, LANGGRAPH_FLAVOR_AVAILABLE, LG_StateGraph, LG_ToolExecutor_Class, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class, LANGGRAPH_MEMORY_SAVER, google_genai_client
398
  if AGENT_INSTANCE and not force_reinit: logger.info("Agent already initialized."); return
399
  logger.info("Initializing agent and tools...")
400
  if not GOOGLE_API_KEY: raise ValueError("GOOGLE_API_KEY not set for LangChain LLM.")
401
+
402
+ # Using INTEGER VALUES for HarmCategory keys and HarmBlockThreshold enum .value for values.
 
 
403
  llm_safety_settings_corrected_final = {
404
  HarmCategory.HARM_CATEGORY_HARASSMENT.value: HarmBlockThreshold.BLOCK_NONE.value,
405
  HarmCategory.HARM_CATEGORY_HATE_SPEECH.value: HarmBlockThreshold.BLOCK_NONE.value,
406
  HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT.value: HarmBlockThreshold.BLOCK_NONE.value,
407
  HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT.value: HarmBlockThreshold.BLOCK_NONE.value,
408
  }
409
+
410
  try:
411
  LLM_INSTANCE = ChatGoogleGenerativeAI(
412
  model=GEMINI_MODEL_NAME,
413
  google_api_key=GOOGLE_API_KEY,
414
  temperature=0.0,
415
+ safety_settings=llm_safety_settings_corrected_final, # USE THE DICTIONARY WITH INT VALUES FOR BOTH
416
  timeout=120,
417
  convert_system_message_to_human=True
418
  )
 
455
  if not LG_ToolExecutor_Class: raise ValueError("LG_ToolExecutor_Class is None for LangGraph.")
456
  tool_executor_instance_lg = LG_ToolExecutor_Class(tools=TOOLS)
457
 
458
+
459
  def tool_node(state: AgentState):
460
  last_msg = state['messages'][-1] if state.get('messages') and isinstance(state['messages'][-1], AIMessage) else None
461
  if not last_msg or not last_msg.tool_calls: return {"messages": []}
 
506
 
507
  if not AGENT_INSTANCE: raise RuntimeError("CRITICAL: Agent initialization completely failed.")
508
  logger.info(f"Agent init finished. Active agent type: {type(AGENT_INSTANCE).__name__}")
509
+
510
+ # --- get_agent_response, construct_prompt_for_agent, run_and_submit_all (Unchanged) ---
511
+ def get_agent_response(prompt: str, task_id: Optional[str]=None, thread_id: Optional[str]=None) -> str:
512
+ # ... (Your original get_agent_response logic - unchanged) ...
513
+ global AGENT_INSTANCE, LLM_INSTANCE
514
+ thread_id_to_use = thread_id or (f"gaia_task_{task_id}" if task_id else hashlib.md5(prompt.encode()).hexdigest()[:8])
515
+ if not AGENT_INSTANCE or not LLM_INSTANCE:
516
+ logger.warning("Agent/LLM not initialized in get_agent_response. Attempting re-initialization.")
517
+ try: initialize_agent_and_tools(force_reinit=True)
518
+ except Exception as e_reinit_get: logger.error(f"Re-initialization failed: {e_reinit_get}"); return f"[ERROR] Agent/LLM re-init failed: {str(e_reinit_get)}"
519
+ if not AGENT_INSTANCE or not LLM_INSTANCE: return "[ERROR] Agent/LLM still None after re-init."
520
+ agent_name_get = type(AGENT_INSTANCE).__name__
521
+ logger.info(f"Agent ({agent_name_get}) processing. Task: {task_id or 'N/A'}. Thread: {thread_id_to_use}.")
522
+ is_langgraph_agent_get = LANGGRAPH_FLAVOR_AVAILABLE and AGENT_INSTANCE and hasattr(AGENT_INSTANCE, 'graph') and hasattr(AGENT_INSTANCE, 'config_schema')
523
+ try:
524
+ if is_langgraph_agent_get:
525
+ logger.debug(f"Using LangGraph agent (Memory: {LANGGRAPH_MEMORY_SAVER is not None}) for thread: {thread_id_to_use}")
526
+ initial_messages_lg_get = []
527
+ input_for_lg_get = {"input": prompt, "messages": initial_messages_lg_get}
528
+ final_state_lg_get = AGENT_INSTANCE.invoke(input_for_lg_get, {"configurable": {"thread_id": thread_id_to_use}}) # type: ignore
529
+ if not final_state_lg_get or 'messages' not in final_state_lg_get or not final_state_lg_get['messages']:
530
+ logger.error("LangGraph: No final state/messages."); return "[ERROR] LangGraph: No final state/messages."
531
+ for message_item_lg_get in reversed(final_state_lg_get['messages']):
532
+ if isinstance(message_item_lg_get, AIMessage) and not message_item_lg_get.tool_calls:
533
+ return str(message_item_lg_get.content)
534
+ logger.warning("LangGraph: No suitable final AIMessage without tool_calls.")
535
+ return str(final_state_lg_get['messages'][-1].content) if final_state_lg_get['messages'] else "[ERROR] LangGraph: Empty messages."
536
+ elif isinstance(AGENT_INSTANCE, AgentExecutor):
537
+ logger.debug("Using ReAct agent.")
538
+ response_react_get = AGENT_INSTANCE.invoke({"input": prompt})
539
+ return str(response_react_get.get("output", "[ERROR] ReAct: No 'output' key."))
540
+ else:
541
+ logger.error(f"Unknown agent type: {agent_name_get}"); return f"[ERROR] Unknown agent type: {agent_name_get}"
542
+ except Exception as e_agent_run_get:
543
+ logger.error(f"Error during agent execution ({agent_name_get}): {e_agent_run_get}", exc_info=True)
544
+ return f"[ERROR] Agent execution failed: {str(e_agent_run_get)[:150]}"
545
+
546
+ def construct_prompt_for_agent(q: Dict[str,Any]) -> str:
547
+ # ... (Your original construct_prompt_for_agent logic - unchanged) ...
548
+ tid,q_str=q.get("task_id","N/A"),q.get("question",""); files=q.get("files",[])
549
+ files_info = ("\nFiles:\n"+"\n".join([f"- {f} (task_id:{tid})"for f in files])) if files else ""
550
+ level = f"\nLevel:{q.get('level')}" if q.get('level') else ""
551
+ return f"Task ID:{tid}{level}{files_info}\n\nQuestion:{q_str}"
552
+
553
+ def run_and_submit_all(profile: Optional[gr.OAuthProfile] = None):
554
+ # ... (Your original run_and_submit_all logic - unchanged) ...
555
+ global AGENT_INSTANCE
556
+ space_id = os.getenv("SPACE_ID")
557
+ username_for_submission = None
558
+ if profile and hasattr(profile, 'username') and profile.username:
559
+ username_for_submission = profile.username
560
+ logger.info(f"Username from OAuth profile: {username_for_submission}")
561
+ else:
562
+ logger.warning("OAuth profile not available or username missing.")
563
+ return "Hugging Face login required. Please use the login button and try again.", None
564
+ if AGENT_INSTANCE is None:
565
+ try: logger.info("Agent not pre-initialized. Initializing for run..."); initialize_agent_and_tools()
566
+ except Exception as e: return f"Agent on-demand initialization failed: {e}", None
567
+ if AGENT_INSTANCE is None: return "Agent is still None after on-demand init.", None
568
+ agent_code_url_run=f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else "local_dev_run"
569
+ questions_url_run,submit_url_run=f"{DEFAULT_API_URL}/questions",f"{DEFAULT_API_URL}/submit"
570
+ auth_headers_run={"Authorization":f"Bearer {HUGGINGFACE_TOKEN}"} if HUGGINGFACE_TOKEN else {}
571
+ try:
572
+ logger.info(f"Fetching questions from {questions_url_run}")
573
+ response_q_run=requests.get(questions_url_run,headers=auth_headers_run,timeout=30);response_q_run.raise_for_status();questions_data_run=response_q_run.json()
574
+ if not questions_data_run or not isinstance(questions_data_run,list):logger.error(f"Invalid questions data: {questions_data_run}");return "Fetched questions_data invalid.",None
575
+ logger.info(f"Fetched {len(questions_data_run)} questions.")
576
+ except Exception as e:logger.error(f"Fetch questions error: {e}",exc_info=True);return f"Fetch questions error:{e}",None
577
+ results_log_run,answers_payload_run=[],[]
578
+ logger.info(f"Running agent on {len(questions_data_run)} questions for user '{username_for_submission}'...")
579
+ for i,item_run in enumerate(questions_data_run):
580
+ task_id_run,question_text_run=item_run.get("task_id"),item_run.get("question")
581
+ if not task_id_run or question_text_run is None:logger.warning(f"Skipping item: {item_run}");continue
582
+ prompt_run=construct_prompt_for_agent(item_run);thread_id_run=f"gaia_batch_task_{task_id_run}"
583
+ logger.info(f"Processing Q {i+1}/{len(questions_data_run)} - Task: {task_id_run}")
584
+ try:
585
+ raw_answer_run=get_agent_response(prompt_run,task_id=task_id_run,thread_id=thread_id_run);submitted_answer_run=_strip_exact_match_answer(raw_answer_run)
586
+ answers_payload_run.append({"task_id":task_id_run,"submitted_answer":submitted_answer_run})
587
+ results_log_run.append({"Task ID":task_id_run,"Question":question_text_run,"Full Agent Prompt":prompt_run,"Raw Agent Output":raw_answer_run,"Submitted Answer":submitted_answer_run})
588
+ except Exception as e:
589
+ logger.error(f"Agent error task {task_id_run}:{e}",exc_info=True);error_answer_run=f"AGENT ERROR:{str(e)[:100]}"
590
+ answers_payload_run.append({"task_id":task_id_run,"submitted_answer":"N/A [AGENT_ERROR]"})
591
+ results_log_run.append({"Task ID":task_id_run,"Question":question_text_run,"Full Agent Prompt":prompt_run,"Raw Agent Output":error_answer_run,"Submitted Answer":"N/A [AGENT_ERROR]"})
592
+ if not answers_payload_run:return "Agent produced no answers.",pd.DataFrame(results_log_run)
593
+ submission_payload_run={"username":username_for_submission.strip(),"agent_code":agent_code_url_run,"answers":answers_payload_run}
594
+ logger.info(f"Submitting {len(answers_payload_run)} answers to {submit_url_run} for user '{username_for_submission}'...")
595
+ submission_headers_run={"Content-Type":"application/json",**auth_headers_run}
596
+ try:
597
+ response_s_run=requests.post(submit_url_run,json=submission_payload_run,headers=submission_headers_run,timeout=120);response_s_run.raise_for_status();submission_result_run=response_s_run.json()
598
+ result_message_run=(f"User:{submission_result_run.get('username',username_for_submission)}\nScore:{submission_result_run.get('score','N/A')}% ({submission_result_run.get('correct_count','?')}/{submission_result_run.get('total_attempted','?')})\nMsg:{submission_result_run.get('message','N/A')}")
599
+ logger.info(f"Submission OK! {result_message_run}");return f"Submission OK!\n{result_message_run}",pd.DataFrame(results_log_run,columns=["Task ID","Question","Full Agent Prompt","Raw Agent Output","Submitted Answer"])
600
+ except requests.exceptions.HTTPError as e:
601
+ error_http_run=f"HTTP {e.response.status_code}. Detail:{e.response.text[:200]}"; logger.error(f"Submit Fail:{error_http_run}",exc_info=True); return f"Submit Fail:{error_http_run}",pd.DataFrame(results_log_run)
602
+ except Exception as e:logger.error(f"Submit Fail unexpected:{e}",exc_info=True);return f"Submit Fail:{str(e)[:100]}",pd.DataFrame(results_log_run)
603
+
604
+ # --- Build Gradio Interface ---
605
+ with gr.Blocks(css=".gradio-container {max-width:1280px !important;margin:auto !important;}",theme=gr.themes.Soft()) as demo:
606
+ gr.Markdown("# GAIA Agent Challenge Runner v7 (OAuth for Username)")
607
+ gr.Markdown(f"""**Instructions:**
608
+ 1. **Login with Hugging Face** using the button below. Your HF username will be used for submission.
609
+ 2. Click 'Run Evaluation & Submit' to process GAIA questions (typically 20).
610
+ 3. **Goal: 30%+ (6/20).** Agent uses Gemini Pro ({GEMINI_MODEL_NAME}) as planner. Tools include Web Search, Python, PDF, OCR, Audio/YouTube, and a new Direct Multimodal tool using Gemini Flash ({GEMINI_FLASH_MULTIMODAL_MODEL_NAME}).
611
+ 4. Ensure `GOOGLE_API_KEY` and `HUGGINGFACE_TOKEN` are Space secrets.
612
+ 5. Check Space logs for details. LangGraph is attempted (ReAct fallback).""")
613
+
614
+ agent_status_display = gr.Markdown("**Agent Status:** Initializing...")
615
+ missing_secrets_display = gr.Markdown("")
616
+
617
+ gr.LoginButton()
618
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
619
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=7, interactive=False)
620
+ results_table = gr.DataFrame(label="Q&A Log", headers=["Task ID","Question","Prompt","Raw","Submitted"], wrap=True) # Removed height
621
+
622
+ run_button.click(fn=run_and_submit_all, outputs=[status_output,results_table], api_name="run_evaluation")
623
+
624
+ def update_ui_on_load_fn_within_context():
625
+ global missing_vars_startup_list_global, agent_pre_init_status_msg_global
626
+ secrets_msg_md = ""
627
+ if missing_vars_startup_list_global:
628
+ secrets_msg_md = f"<font color='red'>**⚠️ Secrets Missing:** {', '.join(missing_vars_startup_list_global)}.</font>"
629
+ env_issues = []
630
+ try: subprocess.run(['yt-dlp','--version'],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
631
+ except: env_issues.append("yt-dlp"); logger.warning("yt-dlp check failed (UI load).")
632
+ try: subprocess.run(['ffmpeg','-version'],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
633
+ except: env_issues.append("ffmpeg"); logger.warning("ffmpeg check failed (UI load).")
634
+ if env_issues: secrets_msg_md += f"<br/><font color='orange'>**Tool Deps Missing:** {', '.join(env_issues)}.</font>"
635
+ current_status_md = agent_pre_init_status_msg_global
636
+ if not LANGGRAPH_FLAVOR_AVAILABLE and "LangGraph" not in current_status_md:
637
+ current_status_md += f" (LangGraph core components not fully loaded: LG_ToolExecutor_Class is {type(LG_ToolExecutor_Class).__name__ if LG_ToolExecutor_Class else 'None'}, ReAct fallback.)"
638
+ elif LANGGRAPH_FLAVOR_AVAILABLE and "LangGraph" not in current_status_md:
639
+ current_status_md += f" (LangGraph ready with {type(LG_ToolExecutor_Class).__name__ if LG_ToolExecutor_Class else 'UnknownExecutor'}.)"
640
+ return { agent_status_display: gr.Markdown(value=current_status_md),
641
+ missing_secrets_display: gr.Markdown(value=secrets_msg_md) }
642
+
643
+ demo.load(update_ui_on_load_fn_within_context, [], [agent_status_display, missing_secrets_display])
644
+
645
+ if __name__ == "__main__":
646
+ logger.info(f"Application starting up (v7 - Corrected GenAI Types Import)...")
647
+ if not PYPDF2_AVAILABLE: logger.warning("PyPDF2 (PDF tool) NOT AVAILABLE.")
648
+ if not PIL_TESSERACT_AVAILABLE: logger.warning("Pillow/Pytesseract (OCR tool) NOT AVAILABLE.")
649
+ if not WHISPER_AVAILABLE: logger.warning("Whisper (Audio tool) NOT AVAILABLE.")
650
+ if LANGGRAPH_FLAVOR_AVAILABLE: logger.info(f"Core LangGraph components (StateGraph, END, {type(LG_ToolExecutor_Class).__name__ if LG_ToolExecutor_Class else 'FailedExecutor'}) loaded.")
651
+ else: logger.warning("Core LangGraph FAILED import or essential component (ToolExecutor/Node) missing. ReAct fallback. Check requirements & Space build logs.")
652
+
653
+ missing_vars_startup_list_global.clear()
654
+ if not GOOGLE_API_KEY: missing_vars_startup_list_global.append("GOOGLE_API_KEY")
655
+ if not HUGGINGFACE_TOKEN: missing_vars_startup_list_global.append("HUGGINGFACE_TOKEN (for GAIA API)")
656
+
657
+ try:
658
+ logger.info("Pre-initializing agent...")
659
+ initialize_agent_and_tools()
660
+ if AGENT_INSTANCE:
661
+ agent_type_name = type(AGENT_INSTANCE).__name__
662
+ agent_pre_init_status_msg_global = f"Agent Pre-initialized: **{agent_type_name}**."
663
+ if LANGGRAPH_FLAVOR_AVAILABLE and ("StateGraph" in agent_type_name or "CompiledGraph" in agent_type_name) :
664
+ lg_executor_display_name = type(LG_ToolExecutor_Class).__name__ if LG_ToolExecutor_Class else "UnknownExecutor"
665
+ agent_pre_init_status_msg_global = f"Agent Pre-initialized: **LangGraph** (Executor: {lg_executor_display_name}, Memory: {LANGGRAPH_MEMORY_SAVER is not None})."
666
+ else: agent_pre_init_status_msg_global = "Agent pre-init FAILED (AGENT_INSTANCE is None)."
667
+ logger.info(agent_pre_init_status_msg_global.replace("**",""))
668
+ except Exception as e:
669
+ agent_pre_init_status_msg_global = f"Agent pre-init CRASHED: {str(e)}"
670
+ logger.critical(f"Agent pre-init CRASHED: {e}", exc_info=True)
671
+
672
+ logger.info(f"Space ID: {os.getenv('SPACE_ID', 'Not Set')}")
673
+ logger.info("Gradio Interface launching...")
674
+ demo.queue().launch(debug=os.getenv("GRADIO_DEBUG","false").lower()=="true", share=False, max_threads=20)