Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,8 +14,10 @@ from typing import List, Dict, TypedDict, Annotated
|
|
| 14 |
from tenacity import retry, stop_after_attempt, wait_exponential
|
| 15 |
import io
|
| 16 |
import sys
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
# Set recursion limit for Python
|
| 19 |
sys.setrecursionlimit(1500)
|
| 20 |
|
| 21 |
# --- Setup Logging ---
|
|
@@ -46,6 +48,23 @@ class RateLimitedDuckDuckGoSearch(DuckDuckGoSearchRun):
|
|
| 46 |
logging.warning(f"All retries failed for query '{query}'. Falling back to reasoning.")
|
| 47 |
return "Web search failed; using reasoning."
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
# --- Answer Formatting ---
|
| 50 |
def format_answer(answer: str, question: str) -> str:
|
| 51 |
"""Post-process answers to match GAIA's exact-match requirements."""
|
|
@@ -95,7 +114,7 @@ class AgentState(TypedDict):
|
|
| 95 |
messages: Annotated[List, add_messages]
|
| 96 |
question: str
|
| 97 |
task_id: str
|
| 98 |
-
file_data: pd.DataFrame | None
|
| 99 |
search_result: str
|
| 100 |
answer: str
|
| 101 |
attempts: int
|
|
@@ -117,7 +136,7 @@ class GaiaAgent:
|
|
| 117 |
Analyze the question to determine the required action:
|
| 118 |
|
| 119 |
- List tasks (keywords: "list," "comma-separated," "categorize"): Action: reason, Input: none
|
| 120 |
-
- File tasks (keywords: "attached," "Excel," "spreadsheet", ".mp3"): Action: fetch_file, Input: none
|
| 121 |
- Search tasks (keywords: "who," "when," "historical," "competition", "wikipedia"): Action: search, Input: specific query
|
| 122 |
- Number tasks (keywords: "how many," "count"): Action: search, Input: specific query
|
| 123 |
- Fallback: Action: reason, Input: none
|
|
@@ -146,11 +165,11 @@ class GaiaAgent:
|
|
| 146 |
Steps:
|
| 147 |
1. Analyze question format (list, USD, name, number).
|
| 148 |
2. Use data:
|
| 149 |
-
- File: Analyze DataFrame
|
| 150 |
- Search: Extract names, numbers, codes.
|
| 151 |
3. Reason:
|
| 152 |
- Lists: Filter, alphabetize.
|
| 153 |
-
- Files: Sum columns,
|
| 154 |
- Search: Parse for key info.
|
| 155 |
4. Format:
|
| 156 |
- Lists: 'item1, item2' (alphabetized).
|
|
@@ -162,7 +181,8 @@ class GaiaAgent:
|
|
| 162 |
Specific instructions:
|
| 163 |
- Mercedes Sosa studio albums (2000-2009): Misa Criolla (2000), Acústico (2002), Corazón Libre (2005), Cantora 1 (2009), Cantora 2 (2009). Answer: 5
|
| 164 |
- Vegetable lists: Only non-botanical fruits (broccoli, celery, fresh basil, green beans, lettuce, sweet potatoes).
|
| 165 |
-
- .mp3 files:
|
|
|
|
| 166 |
|
| 167 |
Examples:
|
| 168 |
- Q: List of vegetables...
|
|
@@ -183,17 +203,17 @@ class GaiaAgent:
|
|
| 183 |
Validate the answer:
|
| 184 |
|
| 185 |
- Check format (list, USD, name, number).
|
| 186 |
-
- Verify relevance.
|
| 187 |
- Return 'valid' or 'invalid: [reason]'.
|
| 188 |
- Numbers: Ensure non-negative for counts.
|
| 189 |
-
- Allow reasonable answers
|
| 190 |
|
| 191 |
Question: {question}
|
| 192 |
Answer: {answer}
|
| 193 |
""")
|
| 194 |
self.graph = self._build_graph()
|
| 195 |
|
| 196 |
-
async def fetch_file(self, task_id: str) -> pd.DataFrame | None:
|
| 197 |
@retry(stop=stop_after_attempt(6), wait=wait_exponential(multiplier=1, min=2, max=5))
|
| 198 |
def _fetch():
|
| 199 |
file_url = f"{self.api_url}/files/{task_id}"
|
|
@@ -203,8 +223,14 @@ class GaiaAgent:
|
|
| 203 |
content = io.BytesIO(response.content)
|
| 204 |
if file_url.endswith('.xlsx') or file_url.endswith('.xls'):
|
| 205 |
df = pd.read_excel(content)
|
| 206 |
-
logging.info(f"
|
| 207 |
return df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
logging.warning(f"Unsupported file type for task {task_id}")
|
| 209 |
return None
|
| 210 |
|
|
@@ -216,7 +242,7 @@ class GaiaAgent:
|
|
| 216 |
|
| 217 |
def _build_graph(self):
|
| 218 |
workflow = StateGraph(AgentState)
|
| 219 |
-
#
|
| 220 |
workflow.config = {"recursion_limit": 50}
|
| 221 |
|
| 222 |
def classify(state: AgentState) -> AgentState:
|
|
@@ -233,7 +259,7 @@ class GaiaAgent:
|
|
| 233 |
task_id=state["task_id"],
|
| 234 |
attempts=state["attempts"]
|
| 235 |
))
|
| 236 |
-
response_content = str(response.content)
|
| 237 |
action = response_content.split("Action: ")[1].split("\n")[0].strip() if "Action: " in response_content else "reason"
|
| 238 |
action_input = response_content.split("Input: ")[1].strip() if "Input: " in response_content else "none"
|
| 239 |
except Exception as e:
|
|
@@ -247,7 +273,7 @@ class GaiaAgent:
|
|
| 247 |
|
| 248 |
async def fetch_file_node(state: AgentState) -> AgentState:
|
| 249 |
logging.info(f"Fetching file for task {state['task_id']}")
|
| 250 |
-
if state["file_data"] is None and state["attempts"] <
|
| 251 |
try:
|
| 252 |
state["file_data"] = await asyncio.wait_for(self.fetch_file(state["task_id"]), timeout=30)
|
| 253 |
state["messages"].append(f"Fetched file: {state['file_data'] is not None}")
|
|
@@ -260,7 +286,7 @@ class GaiaAgent:
|
|
| 260 |
|
| 261 |
async def search_node(state: AgentState) -> AgentState:
|
| 262 |
logging.info(f"Searching for task {state['task_id']}")
|
| 263 |
-
if state["attempts"] <
|
| 264 |
action_input = state["action_input"]
|
| 265 |
if action_input != "none":
|
| 266 |
try:
|
|
@@ -284,7 +310,14 @@ class GaiaAgent:
|
|
| 284 |
|
| 285 |
def generate_answer(state: AgentState) -> AgentState:
|
| 286 |
logging.info(f"Generating answer for task {state['task_id']}")
|
| 287 |
-
file_data_str =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
try:
|
| 289 |
response = self.llm.invoke(self.answer_prompt.format(
|
| 290 |
question=state["question"],
|
|
@@ -311,9 +344,8 @@ class GaiaAgent:
|
|
| 311 |
logging.error(f"Critique error: {str(e)}")
|
| 312 |
critique = "valid" # Avoid looping on critique failure
|
| 313 |
state["messages"].append(f"Critique: {critique}")
|
| 314 |
-
# Stricter exit condition to prevent infinite loops
|
| 315 |
state["attempts"] += 1
|
| 316 |
-
if "invalid" in critique.lower() and state["attempts"] <
|
| 317 |
state["action"] = "reason"
|
| 318 |
state["action_input"] = "none"
|
| 319 |
else:
|
|
@@ -327,7 +359,7 @@ class GaiaAgent:
|
|
| 327 |
workflow.add_node("critique", critique_answer)
|
| 328 |
|
| 329 |
def route_action(state: AgentState) -> str:
|
| 330 |
-
if state["attempts"] >=
|
| 331 |
return END
|
| 332 |
return state["action"] if state["action"] in ["fetch_file", "search", "reason"] else "generate_answer"
|
| 333 |
|
|
|
|
| 14 |
from tenacity import retry, stop_after_attempt, wait_exponential
|
| 15 |
import io
|
| 16 |
import sys
|
| 17 |
+
import speech_recognition as sr
|
| 18 |
+
import tempfile
|
| 19 |
|
| 20 |
+
# Set recursion limit for Python
|
| 21 |
sys.setrecursionlimit(1500)
|
| 22 |
|
| 23 |
# --- Setup Logging ---
|
|
|
|
| 48 |
logging.warning(f"All retries failed for query '{query}'. Falling back to reasoning.")
|
| 49 |
return "Web search failed; using reasoning."
|
| 50 |
|
| 51 |
+
# --- Audio Transcription ---
|
| 52 |
+
def transcribe_audio(file_content: bytes) -> str:
|
| 53 |
+
try:
|
| 54 |
+
recognizer = sr.Recognizer()
|
| 55 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
|
| 56 |
+
temp_file.write(file_content)
|
| 57 |
+
temp_file_path = temp_file.name
|
| 58 |
+
with sr.AudioFile(temp_file_path) as source:
|
| 59 |
+
audio = recognizer.record(source)
|
| 60 |
+
text = recognizer.recognize_google(audio)
|
| 61 |
+
os.unlink(temp_file_path)
|
| 62 |
+
logging.info(f"Transcribed audio: {text[:200]}...")
|
| 63 |
+
return text
|
| 64 |
+
except Exception as e:
|
| 65 |
+
logging.error(f"Audio transcription failed: {str(e)}")
|
| 66 |
+
return "ERROR: Audio transcription failed"
|
| 67 |
+
|
| 68 |
# --- Answer Formatting ---
|
| 69 |
def format_answer(answer: str, question: str) -> str:
|
| 70 |
"""Post-process answers to match GAIA's exact-match requirements."""
|
|
|
|
| 114 |
messages: Annotated[List, add_messages]
|
| 115 |
question: str
|
| 116 |
task_id: str
|
| 117 |
+
file_data: pd.DataFrame | bytes | None
|
| 118 |
search_result: str
|
| 119 |
answer: str
|
| 120 |
attempts: int
|
|
|
|
| 136 |
Analyze the question to determine the required action:
|
| 137 |
|
| 138 |
- List tasks (keywords: "list," "comma-separated," "categorize"): Action: reason, Input: none
|
| 139 |
+
- File tasks (keywords: "attached," "Excel," "spreadsheet", ".mp3", "image"): Action: fetch_file, Input: none
|
| 140 |
- Search tasks (keywords: "who," "when," "historical," "competition", "wikipedia"): Action: search, Input: specific query
|
| 141 |
- Number tasks (keywords: "how many," "count"): Action: search, Input: specific query
|
| 142 |
- Fallback: Action: reason, Input: none
|
|
|
|
| 165 |
Steps:
|
| 166 |
1. Analyze question format (list, USD, name, number).
|
| 167 |
2. Use data:
|
| 168 |
+
- File: Analyze DataFrame, audio transcript, or image description.
|
| 169 |
- Search: Extract names, numbers, codes.
|
| 170 |
3. Reason:
|
| 171 |
- Lists: Filter, alphabetize.
|
| 172 |
+
- Files: Sum columns, extract text, or describe visuals.
|
| 173 |
- Search: Parse for key info.
|
| 174 |
4. Format:
|
| 175 |
- Lists: 'item1, item2' (alphabetized).
|
|
|
|
| 181 |
Specific instructions:
|
| 182 |
- Mercedes Sosa studio albums (2000-2009): Misa Criolla (2000), Acústico (2002), Corazón Libre (2005), Cantora 1 (2009), Cantora 2 (2009). Answer: 5
|
| 183 |
- Vegetable lists: Only non-botanical fruits (broccoli, celery, fresh basil, green beans, lettuce, sweet potatoes).
|
| 184 |
+
- .mp3 files: Transcribe audio and extract relevant info.
|
| 185 |
+
- Image-based (e.g., chess): Describe image or return error if no image provided.
|
| 186 |
|
| 187 |
Examples:
|
| 188 |
- Q: List of vegetables...
|
|
|
|
| 203 |
Validate the answer:
|
| 204 |
|
| 205 |
- Check format (list, USD, name, number).
|
| 206 |
+
- Verify relevance to question.
|
| 207 |
- Return 'valid' or 'invalid: [reason]'.
|
| 208 |
- Numbers: Ensure non-negative for counts.
|
| 209 |
+
- Allow reasonable answers to avoid excessive retries.
|
| 210 |
|
| 211 |
Question: {question}
|
| 212 |
Answer: {answer}
|
| 213 |
""")
|
| 214 |
self.graph = self._build_graph()
|
| 215 |
|
| 216 |
+
async def fetch_file(self, task_id: str) -> pd.DataFrame | bytes | None:
|
| 217 |
@retry(stop=stop_after_attempt(6), wait=wait_exponential(multiplier=1, min=2, max=5))
|
| 218 |
def _fetch():
|
| 219 |
file_url = f"{self.api_url}/files/{task_id}"
|
|
|
|
| 223 |
content = io.BytesIO(response.content)
|
| 224 |
if file_url.endswith('.xlsx') or file_url.endswith('.xls'):
|
| 225 |
df = pd.read_excel(content)
|
| 226 |
+
logging.info(f"Excel file fetched. Shape: {df.shape}, Columns: {list(df.columns)}")
|
| 227 |
return df
|
| 228 |
+
elif file_url.endswith('.mp3'):
|
| 229 |
+
logging.info("Audio file fetched.")
|
| 230 |
+
return response.content
|
| 231 |
+
elif file_url.endswith('.png') or file_url.endswith('.jpg'):
|
| 232 |
+
logging.info("Image file fetched.")
|
| 233 |
+
return response.content
|
| 234 |
logging.warning(f"Unsupported file type for task {task_id}")
|
| 235 |
return None
|
| 236 |
|
|
|
|
| 242 |
|
| 243 |
def _build_graph(self):
|
| 244 |
workflow = StateGraph(AgentState)
|
| 245 |
+
# Explicitly set recursion limit
|
| 246 |
workflow.config = {"recursion_limit": 50}
|
| 247 |
|
| 248 |
def classify(state: AgentState) -> AgentState:
|
|
|
|
| 259 |
task_id=state["task_id"],
|
| 260 |
attempts=state["attempts"]
|
| 261 |
))
|
| 262 |
+
response_content = str(response.content).strip()
|
| 263 |
action = response_content.split("Action: ")[1].split("\n")[0].strip() if "Action: " in response_content else "reason"
|
| 264 |
action_input = response_content.split("Input: ")[1].strip() if "Input: " in response_content else "none"
|
| 265 |
except Exception as e:
|
|
|
|
| 273 |
|
| 274 |
async def fetch_file_node(state: AgentState) -> AgentState:
|
| 275 |
logging.info(f"Fetching file for task {state['task_id']}")
|
| 276 |
+
if state["file_data"] is None and state["attempts"] < 2:
|
| 277 |
try:
|
| 278 |
state["file_data"] = await asyncio.wait_for(self.fetch_file(state["task_id"]), timeout=30)
|
| 279 |
state["messages"].append(f"Fetched file: {state['file_data'] is not None}")
|
|
|
|
| 286 |
|
| 287 |
async def search_node(state: AgentState) -> AgentState:
|
| 288 |
logging.info(f"Searching for task {state['task_id']}")
|
| 289 |
+
if state["attempts"] < 2:
|
| 290 |
action_input = state["action_input"]
|
| 291 |
if action_input != "none":
|
| 292 |
try:
|
|
|
|
| 310 |
|
| 311 |
def generate_answer(state: AgentState) -> AgentState:
|
| 312 |
logging.info(f"Generating answer for task {state['task_id']}")
|
| 313 |
+
file_data_str = "none"
|
| 314 |
+
if isinstance(state["file_data"], pd.DataFrame):
|
| 315 |
+
file_data_str = state["file_data"].to_dict()
|
| 316 |
+
elif isinstance(state["file_data"], bytes):
|
| 317 |
+
if "mp3" in state["question"].lower():
|
| 318 |
+
file_data_str = transcribe_audio(state["file_data"])
|
| 319 |
+
else:
|
| 320 |
+
file_data_str = "Image data; cannot process directly."
|
| 321 |
try:
|
| 322 |
response = self.llm.invoke(self.answer_prompt.format(
|
| 323 |
question=state["question"],
|
|
|
|
| 344 |
logging.error(f"Critique error: {str(e)}")
|
| 345 |
critique = "valid" # Avoid looping on critique failure
|
| 346 |
state["messages"].append(f"Critique: {critique}")
|
|
|
|
| 347 |
state["attempts"] += 1
|
| 348 |
+
if "invalid" in critique.lower() and state["attempts"] < 1: # Reduced retries to 1
|
| 349 |
state["action"] = "reason"
|
| 350 |
state["action_input"] = "none"
|
| 351 |
else:
|
|
|
|
| 359 |
workflow.add_node("critique", critique_answer)
|
| 360 |
|
| 361 |
def route_action(state: AgentState) -> str:
|
| 362 |
+
if state["attempts"] >= 2 or state["action"] is None:
|
| 363 |
return END
|
| 364 |
return state["action"] if state["action"] in ["fetch_file", "search", "reason"] else "generate_answer"
|
| 365 |
|