Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -27,10 +27,15 @@ from langchain_community.tools import DuckDuckGoSearchRun
|
|
| 27 |
from langchain_core.tools import tool
|
| 28 |
from langchain_groq import ChatGroq
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# --- Constants ---
|
| 31 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 32 |
-
MAX_TURNS = 20
|
| 33 |
-
MAX_MESSAGE_LENGTH = 8000
|
| 34 |
|
| 35 |
# --- Initialize ASR Pipeline ---
|
| 36 |
asr_pipeline = None
|
|
@@ -50,6 +55,9 @@ except Exception as e:
|
|
| 50 |
print(f"⚠️ Warning: Could not load ASR pipeline globally. Error: {e}")
|
| 51 |
asr_pipeline = None
|
| 52 |
|
|
|
|
|
|
|
|
|
|
| 53 |
# ====================================================
|
| 54 |
# --- Tool Definitions ---
|
| 55 |
|
|
@@ -63,13 +71,10 @@ def search_tool(query: str) -> str:
|
|
| 63 |
try:
|
| 64 |
search = DuckDuckGoSearchRun()
|
| 65 |
result = search.run(query)
|
| 66 |
-
# Truncate if too long
|
| 67 |
if len(result) > MAX_MESSAGE_LENGTH:
|
| 68 |
result = result[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(result)} total chars]"
|
| 69 |
return result
|
| 70 |
except Exception as e:
|
| 71 |
-
tb_str = traceback.format_exc()
|
| 72 |
-
print(f"--- Search Tool FAILED ---\n{tb_str}\n---")
|
| 73 |
return f"Error running search for '{query}': {str(e)}"
|
| 74 |
|
| 75 |
|
|
@@ -94,7 +99,6 @@ def code_interpreter(code: str) -> str:
|
|
| 94 |
if pattern in code_lower:
|
| 95 |
return f"Error: Potentially dangerous operation '{pattern}' is not allowed."
|
| 96 |
|
| 97 |
-
# Check for file writing in code
|
| 98 |
if 'open(' in code_lower and any(mode in code for mode in ["'w'", '"w"', "'a'", '"a"', "'wb'", '"wb"']):
|
| 99 |
return "Error: Writing files is not allowed in code_interpreter. Use write_file tool instead."
|
| 100 |
|
|
@@ -117,7 +121,6 @@ def code_interpreter(code: str) -> str:
|
|
| 117 |
return f"Error in execution:\n{stderr}\n\nStdout (if any):\n{stdout}"
|
| 118 |
|
| 119 |
if stdout:
|
| 120 |
-
# Truncate if too long
|
| 121 |
if len(stdout) > MAX_MESSAGE_LENGTH:
|
| 122 |
stdout = stdout[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(stdout)} total chars]"
|
| 123 |
return f"Success:\n{stdout}"
|
|
@@ -126,9 +129,7 @@ def code_interpreter(code: str) -> str:
|
|
| 126 |
|
| 127 |
except Exception as e:
|
| 128 |
tb_str = traceback.format_exc()
|
| 129 |
-
|
| 130 |
-
error_msg = f"Execution failed:\n{tb_str}\n\n💡 Hints:\n- Check your syntax\n- Ensure you're using print() for output\n- Verify variable names and types"
|
| 131 |
-
return error_msg
|
| 132 |
|
| 133 |
|
| 134 |
@tool
|
|
@@ -143,11 +144,10 @@ def read_file(path: str) -> str:
|
|
| 143 |
script_dir = os.getcwd()
|
| 144 |
safe_path = os.path.normpath(path)
|
| 145 |
|
| 146 |
-
# Try multiple path strategies
|
| 147 |
paths_to_try = [
|
| 148 |
-
os.path.join(script_dir, safe_path),
|
| 149 |
-
safe_path,
|
| 150 |
-
os.path.join(os.getcwd(), os.path.basename(safe_path))
|
| 151 |
]
|
| 152 |
|
| 153 |
full_path = None
|
|
@@ -157,49 +157,32 @@ def read_file(path: str) -> str:
|
|
| 157 |
break
|
| 158 |
|
| 159 |
if not full_path:
|
| 160 |
-
|
| 161 |
-
cwd_files = os.listdir(".")
|
| 162 |
-
except Exception:
|
| 163 |
-
cwd_files = ["(could not list)"]
|
| 164 |
return (f"Error: File not found: '{path}'\n"
|
| 165 |
-
|
| 166 |
-
|
| 167 |
|
| 168 |
print(f"Reading file: {full_path}")
|
| 169 |
-
|
| 170 |
-
# Try to detect file type
|
| 171 |
_, ext = os.path.splitext(full_path)
|
| 172 |
|
| 173 |
try:
|
| 174 |
with open(full_path, 'r', encoding='utf-8') as f:
|
| 175 |
content = f.read()
|
| 176 |
-
|
| 177 |
-
# Truncate if too long
|
| 178 |
if len(content) > MAX_MESSAGE_LENGTH:
|
| 179 |
content = content[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(content)} total chars]"
|
| 180 |
-
|
| 181 |
return content
|
| 182 |
|
| 183 |
except UnicodeDecodeError:
|
| 184 |
-
# Try binary read for non-text files
|
| 185 |
try:
|
| 186 |
with open(full_path, 'rb') as f:
|
| 187 |
binary_content = f.read()
|
| 188 |
return f"File appears to be binary ({len(binary_content)} bytes). Cannot display as text.\nFile type: {ext}\nConsider using audio_transcription_tool for audio files."
|
| 189 |
except Exception as bin_e:
|
| 190 |
return f"Error: Could not read file as text or binary: {str(bin_e)}"
|
| 191 |
-
|
| 192 |
-
except PermissionError:
|
| 193 |
-
return f"Error: Permission denied reading '{full_path}'."
|
| 194 |
-
except IsADirectoryError:
|
| 195 |
-
return f"Error: '{full_path}' is a directory, not a file. Use list_directory to see its contents."
|
| 196 |
except Exception as read_e:
|
| 197 |
-
|
| 198 |
-
return f"Error reading file: {str(read_e)}\n{tb_str}"
|
| 199 |
|
| 200 |
except Exception as e:
|
| 201 |
-
tb_str = traceback.format_exc()
|
| 202 |
-
print(f"--- Read File Tool FAILED ---\n{tb_str}\n---")
|
| 203 |
return f"Unexpected error accessing file '{path}': {str(e)}"
|
| 204 |
|
| 205 |
|
|
@@ -217,7 +200,6 @@ def write_file(path: str, content: str) -> str:
|
|
| 217 |
base_dir = os.getcwd()
|
| 218 |
full_path = os.path.join(base_dir, path)
|
| 219 |
|
| 220 |
-
# Create directories if needed
|
| 221 |
dir_path = os.path.dirname(full_path)
|
| 222 |
if dir_path:
|
| 223 |
os.makedirs(dir_path, exist_ok=True)
|
|
@@ -227,11 +209,8 @@ def write_file(path: str, content: str) -> str:
|
|
| 227 |
|
| 228 |
return f"Successfully wrote {len(content)} characters to '{path}'."
|
| 229 |
|
| 230 |
-
except PermissionError:
|
| 231 |
-
return f"Error: Permission denied writing to '{path}'."
|
| 232 |
except Exception as e:
|
| 233 |
-
|
| 234 |
-
return f"Error writing file '{path}': {str(e)}\n{tb_str}"
|
| 235 |
|
| 236 |
|
| 237 |
@tool
|
|
@@ -254,10 +233,7 @@ def list_directory(path: str = ".") -> str:
|
|
| 254 |
if not items:
|
| 255 |
return f"Directory '{path}' is empty."
|
| 256 |
|
| 257 |
-
|
| 258 |
-
files = []
|
| 259 |
-
directories = []
|
| 260 |
-
|
| 261 |
for item in sorted(items):
|
| 262 |
item_path = os.path.join(full_path, item)
|
| 263 |
if os.path.isdir(item_path):
|
|
@@ -274,11 +250,8 @@ def list_directory(path: str = ".") -> str:
|
|
| 274 |
|
| 275 |
return result
|
| 276 |
|
| 277 |
-
except PermissionError:
|
| 278 |
-
return f"Error: Permission denied listing directory '{path}'."
|
| 279 |
except Exception as e:
|
| 280 |
-
|
| 281 |
-
return f"Error listing directory '{path}': {str(e)}\n{tb_str}"
|
| 282 |
|
| 283 |
|
| 284 |
@tool
|
|
@@ -293,7 +266,6 @@ def audio_transcription_tool(file_path: str) -> str:
|
|
| 293 |
return "Error: ASR pipeline is not available. Audio transcription cannot be performed."
|
| 294 |
|
| 295 |
try:
|
| 296 |
-
# Find file using same strategy as read_file
|
| 297 |
script_dir = os.getcwd()
|
| 298 |
safe_path = os.path.normpath(file_path)
|
| 299 |
|
|
@@ -317,17 +289,15 @@ def audio_transcription_tool(file_path: str) -> str:
|
|
| 317 |
result_text = transcription.get("text", "")
|
| 318 |
|
| 319 |
if not result_text:
|
| 320 |
-
return "Error: Transcription produced no text.
|
| 321 |
|
| 322 |
-
# Truncate if too long
|
| 323 |
if len(result_text) > MAX_MESSAGE_LENGTH:
|
| 324 |
-
result_text = result_text[:MAX_MESSAGE_LENGTH] + f"\n...[truncated
|
| 325 |
|
| 326 |
return f"Transcription:\n{result_text}"
|
| 327 |
|
| 328 |
except Exception as e:
|
| 329 |
-
|
| 330 |
-
return f"Error transcribing '{file_path}': {str(e)}\n{tb_str}"
|
| 331 |
|
| 332 |
|
| 333 |
@tool
|
|
@@ -339,100 +309,96 @@ def get_youtube_transcript(video_url: str) -> str:
|
|
| 339 |
print(f"--- Calling YouTube Transcript: {video_url} ---")
|
| 340 |
|
| 341 |
try:
|
| 342 |
-
# Extract video ID
|
| 343 |
video_id = None
|
| 344 |
if "watch?v=" in video_url:
|
| 345 |
video_id = video_url.split("v=")[1].split("&")[0]
|
| 346 |
elif "youtu.be/" in video_url:
|
| 347 |
video_id = video_url.split("youtu.be/")[1].split("?")[0]
|
| 348 |
-
elif len(video_url) == 11 and video_url.isalnum(): # Direct video ID
|
| 349 |
-
video_id = video_url
|
| 350 |
|
| 351 |
if not video_id:
|
| 352 |
-
return f"Error: Could not extract YouTube video ID from '{video_url}'.
|
| 353 |
-
|
| 354 |
-
print(f"Fetching transcript for video ID: {video_id}")
|
| 355 |
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
|
| 356 |
|
| 357 |
if not transcript_list:
|
| 358 |
-
return "Error: No transcript found for this video.
|
| 359 |
-
|
| 360 |
full_transcript = " ".join([item["text"] for item in transcript_list])
|
| 361 |
|
| 362 |
-
# Truncate if too long
|
| 363 |
if len(full_transcript) > MAX_MESSAGE_LENGTH:
|
| 364 |
-
full_transcript = full_transcript[:MAX_MESSAGE_LENGTH] + f"\n...[truncated
|
| 365 |
|
| 366 |
return f"YouTube Transcript:\n{full_transcript}"
|
| 367 |
|
| 368 |
except Exception as e:
|
| 369 |
-
|
| 370 |
-
return f"Error getting transcript for '{video_url}': {str(e)}\nThis video may not have transcripts available.\n{tb_str}"
|
| 371 |
|
| 372 |
|
|
|
|
| 373 |
@tool
|
| 374 |
-
def
|
| 375 |
-
"""
|
| 376 |
-
|
| 377 |
-
|
|
|
|
| 378 |
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
return f"Error: Invalid URL. Must start with http:// or https://. Got: '{url}'"
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
|
| 384 |
try:
|
|
|
|
| 385 |
headers = {
|
| 386 |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
| 387 |
}
|
| 388 |
-
|
| 389 |
response = requests.get(url, headers=headers, timeout=20)
|
| 390 |
response.raise_for_status()
|
| 391 |
|
| 392 |
-
content_type = response.headers.get('Content-Type', '').lower()
|
| 393 |
-
if 'html' not in content_type:
|
| 394 |
-
return f"Error: URL returned '{content_type}', not HTML. Cannot scrape non-HTML content."
|
| 395 |
-
|
| 396 |
soup = BeautifulSoup(response.text, 'html.parser')
|
| 397 |
-
|
| 398 |
-
# Remove unwanted elements
|
| 399 |
-
for tag in soup(["script", "style", "nav", "footer", "aside", "header",
|
| 400 |
-
"form", "button", "input", "img", "link", "meta"]):
|
| 401 |
tag.extract()
|
| 402 |
|
| 403 |
-
|
| 404 |
-
main_content = (soup.find('main') or
|
| 405 |
-
soup.find('article') or
|
| 406 |
-
soup.find('div', role='main') or
|
| 407 |
-
soup.find('div', class_=lambda x: x and 'content' in x.lower()) or
|
| 408 |
-
soup.body)
|
| 409 |
-
|
| 410 |
if not main_content:
|
| 411 |
-
return "Error: Could not find main content
|
| 412 |
|
| 413 |
text = main_content.get_text(separator='\n', strip=True)
|
| 414 |
-
|
| 415 |
-
# Clean up whitespace
|
| 416 |
-
lines = (line.strip() for line in text.splitlines())
|
| 417 |
-
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
| 418 |
-
text = '\n'.join(chunk for chunk in chunks if chunk)
|
| 419 |
|
| 420 |
if not text:
|
| 421 |
-
return "Error: Scraped content was empty
|
| 422 |
|
| 423 |
-
#
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
|
| 427 |
-
|
|
|
|
|
|
|
| 428 |
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
|
|
|
|
|
|
|
|
|
| 433 |
except Exception as e:
|
| 434 |
tb_str = traceback.format_exc()
|
| 435 |
-
return f"Error scraping {url}: {str(e)}\n{tb_str}"
|
| 436 |
|
| 437 |
|
| 438 |
@tool
|
|
@@ -440,10 +406,6 @@ def final_answer_tool(answer: str) -> str:
|
|
| 440 |
"""
|
| 441 |
Call this tool ONLY when you have the final, definitive answer.
|
| 442 |
The 'answer' must be EXACTLY what was asked for, with no extra text.
|
| 443 |
-
Examples:
|
| 444 |
-
- If asked for a number: "42" (not "The answer is 42")
|
| 445 |
-
- If asked for a list: "apple, banana, cherry"
|
| 446 |
-
- If asked for a name: "John Smith"
|
| 447 |
"""
|
| 448 |
if not isinstance(answer, str):
|
| 449 |
try:
|
|
@@ -461,16 +423,13 @@ def remove_fences_simple(text):
|
|
| 461 |
"""Remove code fences from text."""
|
| 462 |
original_text = text
|
| 463 |
text = text.strip()
|
| 464 |
-
|
| 465 |
if text.startswith("```") and text.endswith("```"):
|
| 466 |
text = text[3:-3].strip()
|
| 467 |
if '\n' in text:
|
| 468 |
first_line, rest = text.split('\n', 1)
|
| 469 |
-
# Remove language identifier
|
| 470 |
if first_line.strip().replace('_','').isalnum() and len(first_line.strip()) < 15:
|
| 471 |
text = rest.strip()
|
| 472 |
return text
|
| 473 |
-
|
| 474 |
return original_text
|
| 475 |
|
| 476 |
|
|
@@ -483,7 +442,7 @@ defined_tools = [
|
|
| 483 |
list_directory,
|
| 484 |
audio_transcription_tool,
|
| 485 |
get_youtube_transcript,
|
| 486 |
-
scrape_web_page
|
| 487 |
final_answer_tool
|
| 488 |
]
|
| 489 |
|
|
@@ -491,59 +450,29 @@ defined_tools = [
|
|
| 491 |
# --- LangGraph Agent State ---
|
| 492 |
class AgentState(TypedDict):
|
| 493 |
messages: Annotated[List[AnyMessage], add_messages]
|
|
|
|
| 494 |
turn: int
|
| 495 |
|
| 496 |
|
| 497 |
# --- Conditional Edge Function ---
|
| 498 |
-
def
|
| 499 |
-
"""
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
return END
|
| 509 |
-
|
| 510 |
-
# 2. Check turn limit
|
| 511 |
-
if current_turn >= MAX_TURNS:
|
| 512 |
-
print(f"--- Condition: Max turns ({MAX_TURNS}) reached. Ending. ---")
|
| 513 |
-
state['messages'].append(
|
| 514 |
-
SystemMessage(content=f"SYSTEM: Maximum turn limit ({MAX_TURNS}) reached. Ending execution.")
|
| 515 |
-
)
|
| 516 |
return END
|
| 517 |
|
| 518 |
-
# 3. Route to tools if tool calls exist
|
| 519 |
-
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
| 520 |
-
print("--- Condition: Tools called, routing to tools node. ---")
|
| 521 |
-
return "tools"
|
| 522 |
-
|
| 523 |
-
# 4. NEW LOOP PREVENTION:
|
| 524 |
-
# Check for consecutive AI messages without tool calls.
|
| 525 |
-
# This catches "thinking" loops or raw answer dribbling (like "58").
|
| 526 |
-
if isinstance(last_message, AIMessage) and not last_message.tool_calls:
|
| 527 |
-
# Check if the message *before* this one was ALSO an AIMessage.
|
| 528 |
-
# We need at least 3 messages total (System, Human, AI-Turn1-Plan)
|
| 529 |
-
# for this check to be valid, so we check len > 2.
|
| 530 |
-
if len(state['messages']) > 2 and isinstance(state['messages'][-2], AIMessage):
|
| 531 |
-
print(f"--- Condition: Detected 2+ consecutive AI messages (Turn {current_turn}). Ending to prevent loop. ---")
|
| 532 |
-
state['messages'].append(
|
| 533 |
-
SystemMessage(content=f"SYSTEM: Agent stuck in a loop (consecutive non-tool-call AI messages). Ending execution.")
|
| 534 |
-
)
|
| 535 |
-
return END
|
| 536 |
-
|
| 537 |
-
# 5. Default: Loop back to agent (e.g., after Turn 1 plan)
|
| 538 |
-
print(f"--- Condition: No tool call (Turn {current_turn}). Continuing to agent. ---")
|
| 539 |
-
return "agent"
|
| 540 |
-
|
| 541 |
|
| 542 |
# ====================================================
|
| 543 |
# --- Basic Agent Class ---
|
| 544 |
class BasicAgent:
|
| 545 |
def __init__(self):
|
| 546 |
-
print("BasicAgent (
|
| 547 |
|
| 548 |
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 549 |
if not GROQ_API_KEY:
|
|
@@ -551,292 +480,283 @@ class BasicAgent:
|
|
| 551 |
|
| 552 |
self.tools = defined_tools
|
| 553 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
# Build tool descriptions
|
| 555 |
tool_desc_list = []
|
| 556 |
for tool in self.tools:
|
| 557 |
-
|
| 558 |
-
desc = (
|
| 559 |
-
f"- {tool.name}: Executes Python code. Use for calculations, data analysis, logic puzzles.\n"
|
| 560 |
-
f" **CRITICAL RULES:**\n"
|
| 561 |
-
f" 1. ALWAYS use print() to output results\n"
|
| 562 |
-
f" 2. Write simple, focused code (one task per execution)\n"
|
| 563 |
-
f" 3. Add comments (#) to explain your logic\n"
|
| 564 |
-
f" Available: pandas as pd"
|
| 565 |
-
)
|
| 566 |
-
else:
|
| 567 |
-
desc = f"- {tool.name}: {tool.description}"
|
| 568 |
tool_desc_list.append(desc)
|
| 569 |
-
|
| 570 |
tool_descriptions = "\n".join(tool_desc_list)
|
| 571 |
|
| 572 |
-
# ==================== SYSTEM PROMPT
|
| 573 |
self.system_prompt = f"""You are a highly intelligent AI assistant for the GAIA benchmark.
|
| 574 |
Your goal: Provide the EXACT answer in the EXACT format requested.
|
| 575 |
|
| 576 |
**PROTOCOL:**
|
| 577 |
|
| 578 |
-
1. **ANALYZE
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
-
|
| 582 |
-
|
| 583 |
-
2. **FIRST TURN - MAKE A PLAN:**
|
| 584 |
-
Your FIRST response MUST be a brief plan (2-3 sentences):
|
| 585 |
-
- What tools you'll use
|
| 586 |
-
- What order you'll use them
|
| 587 |
-
- What format the final answer should be
|
| 588 |
-
DO NOT call tools on your first turn!
|
| 589 |
-
|
| 590 |
-
3. **EXECUTE:**
|
| 591 |
-
- Call ONE tool per turn
|
| 592 |
-
- Wait for the result before planning your next step
|
| 593 |
-
- For ANY calculation or logic: use code_interpreter with print()
|
| 594 |
-
|
| 595 |
-
4. **VERIFY RESULTS:**
|
| 596 |
-
- Check if tool output contains errors
|
| 597 |
-
- If error: plan a different approach
|
| 598 |
-
- If success: decide if you need more info or have the answer
|
| 599 |
-
|
| 600 |
-
5. **FINISH:**
|
| 601 |
-
When you have the answer from a tool output:
|
| 602 |
-
- Call final_answer_tool immediately
|
| 603 |
-
- Provide ONLY the exact answer (no explanations!)
|
| 604 |
|
| 605 |
**CRITICAL RULES:**
|
| 606 |
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
|
| 614 |
-
**
|
| 615 |
-
|
| 616 |
-
- "List the colors" → final_answer("red, blue, green")
|
| 617 |
-
- "Is it true?" → final_answer("Yes") or final_answer("No")
|
| 618 |
-
- "What's the name?" → final_answer("John Smith")
|
| 619 |
|
| 620 |
**TOOLS:**
|
| 621 |
{tool_descriptions}
|
| 622 |
|
| 623 |
-
**REMEMBER:**
|
| 624 |
"""
|
| 625 |
|
| 626 |
-
print("Initializing Groq
|
| 627 |
try:
|
| 628 |
-
|
| 629 |
-
|
|
|
|
| 630 |
groq_api_key=GROQ_API_KEY,
|
| 631 |
-
model_name="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
max_tokens=4096,
|
| 633 |
timeout=60
|
| 634 |
)
|
| 635 |
-
print("✅
|
| 636 |
except Exception as e:
|
| 637 |
print(f"❌ Error initializing Groq: {e}")
|
| 638 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
# --- Agent Node ---
|
| 644 |
-
# --- Agent Node (v3 - Simplified) ---
|
| 645 |
-
def agent_node(state: AgentState):
|
| 646 |
current_turn = state.get('turn', 0) + 1
|
| 647 |
print(f"\n{'='*60}")
|
| 648 |
-
print(f"
|
| 649 |
print('='*60)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
|
| 651 |
-
|
|
|
|
| 652 |
|
| 653 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
max_retries = 3
|
| 655 |
ai_message = None
|
| 656 |
-
|
| 657 |
for attempt in range(max_retries):
|
| 658 |
try:
|
| 659 |
-
|
|
|
|
| 660 |
break
|
| 661 |
except Exception as e:
|
| 662 |
-
print(f"⚠️ LLM attempt {attempt+1}/{max_retries} failed: {e}")
|
| 663 |
if attempt == max_retries - 1:
|
| 664 |
-
|
| 665 |
-
content=f"Error: LLM failed
|
| 666 |
)
|
| 667 |
-
|
| 668 |
-
time.sleep(2 ** attempt) # Exponential backoff
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
| 672 |
-
# --- ROBUST FALLBACK PARSING BLOCK ---
|
| 673 |
-
# (We still need this to catch malformed tool calls)
|
| 674 |
-
|
| 675 |
-
if not ai_message.tool_calls and isinstance(ai_message.content, str) and ai_message.content.strip():
|
| 676 |
-
content = ai_message.content
|
| 677 |
-
tool_name = None
|
| 678 |
-
tool_input = None
|
| 679 |
-
|
| 680 |
-
# 1. Try to parse <function(tool_name)>{json}</function>
|
| 681 |
-
func_match = re.search(
|
| 682 |
-
r"<function\(([^)]+)\)>(\{.*?\})(?:</function>)?",
|
| 683 |
-
content,
|
| 684 |
-
re.DOTALL | re.IGNORECASE
|
| 685 |
-
)
|
| 686 |
-
|
| 687 |
-
if func_match:
|
| 688 |
-
try:
|
| 689 |
-
tool_name = func_match.group(1).strip()
|
| 690 |
-
json_str = func_match.group(2)
|
| 691 |
-
tool_input = json.loads(json_str)
|
| 692 |
-
print(f"🔧 Fallback (Format 1): Parsed tool call for '{tool_name}'")
|
| 693 |
-
except json.JSONDecodeError as e:
|
| 694 |
-
print(f"⚠️ Fallback (Format 1): Failed to parse JSON: {e}")
|
| 695 |
-
tool_name = None
|
| 696 |
-
|
| 697 |
-
# 2. If Format 1 failed, try to parse bare JSON
|
| 698 |
-
if not tool_name:
|
| 699 |
-
json_match = re.search(
|
| 700 |
-
r"```(?:json)?\s*(\{.*?\})\s*```|(\{.*?\})",
|
| 701 |
-
content,
|
| 702 |
-
re.DOTALL | re.IGNORECASE
|
| 703 |
-
)
|
| 704 |
-
if json_match:
|
| 705 |
-
json_str = json_match.group(1) or json_match.group(2)
|
| 706 |
-
try:
|
| 707 |
-
parsed_json = json.loads(json_str)
|
| 708 |
-
if isinstance(parsed_json, dict):
|
| 709 |
-
if "tool" in parsed_json and "tool_input" in parsed_json:
|
| 710 |
-
tool_name = parsed_json.get("tool")
|
| 711 |
-
tool_input = parsed_json.get("tool_input", {})
|
| 712 |
-
elif "code" in parsed_json:
|
| 713 |
-
tool_name = "code_interpreter"
|
| 714 |
-
tool_input = parsed_json
|
| 715 |
-
elif "answer" in parsed_json:
|
| 716 |
-
tool_name = "final_answer_tool"
|
| 717 |
-
tool_input = parsed_json
|
| 718 |
-
|
| 719 |
-
if tool_name:
|
| 720 |
-
print(f"🔧 Fallback (Format 2): Parsed tool call for '{tool_name}'")
|
| 721 |
-
except json.JSONDecodeError as e:
|
| 722 |
-
print(f"⚠️ Fallback (Format 2): Failed to parse JSON: {e}")
|
| 723 |
-
|
| 724 |
-
# --- If any fallback parser succeeded, build the tool call ---
|
| 725 |
-
if tool_name and tool_input is not None and any(t.name == tool_name for t in self.tools):
|
| 726 |
-
print(f"🔧 Fallback SUCCESS: Rebuilding tool call for '{tool_name}'")
|
| 727 |
-
tool_call = ToolCall(
|
| 728 |
-
name=tool_name,
|
| 729 |
-
args=tool_input,
|
| 730 |
-
id=str(uuid.uuid4())
|
| 731 |
-
)
|
| 732 |
-
ai_message.tool_calls = [tool_call]
|
| 733 |
-
ai_message.content = ""
|
| 734 |
-
|
| 735 |
-
elif not tool_name:
|
| 736 |
-
# We still want to log if it's just dribbling text
|
| 737 |
-
print(f"⚠️ Fallback FAILED: Could not parse any tool call from content:\n{content[:200]}...")
|
| 738 |
-
# --- END OF REPLACEMENT BLOCK ---
|
| 739 |
-
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
| 740 |
-
|
| 741 |
|
| 742 |
-
# --- Logging ---
|
| 743 |
if ai_message.tool_calls:
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
elif ai_message.content:
|
| 748 |
-
content_preview = ai_message.content[:300]
|
| 749 |
-
if len(ai_message.content) > 300:
|
| 750 |
-
content_preview += "..."
|
| 751 |
-
print(f"💭 Agent Reasoning:\n{content_preview}")
|
| 752 |
|
| 753 |
-
return {"messages": [ai_message], "
|
| 754 |
-
|
|
|
|
| 755 |
tool_node = ToolNode(self.tools)
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
print("Building
|
| 759 |
graph_builder = StateGraph(AgentState)
|
| 760 |
-
|
|
|
|
|
|
|
| 761 |
graph_builder.add_node("tools", tool_node)
|
| 762 |
|
| 763 |
-
graph_builder.add_edge(START, "
|
| 764 |
-
graph_builder.add_edge("tools", "agent")
|
| 765 |
|
| 766 |
graph_builder.add_conditional_edges(
|
| 767 |
-
"
|
| 768 |
-
|
| 769 |
{
|
| 770 |
-
"
|
| 771 |
-
"agent": "agent",
|
| 772 |
END: END
|
| 773 |
}
|
| 774 |
)
|
| 775 |
|
|
|
|
|
|
|
|
|
|
| 776 |
self.graph = graph_builder.compile()
|
| 777 |
-
print("✅
|
|
|
|
| 778 |
def __call__(self, question: str) -> str:
|
| 779 |
print(f"\n--- Starting Agent Run for Question ---")
|
| 780 |
print(f"Agent received question (first 100 chars): {question[:100]}...")
|
| 781 |
|
| 782 |
-
# Initialize graph input
|
| 783 |
graph_input = {
|
| 784 |
"messages": [
|
| 785 |
SystemMessage(content=self.system_prompt),
|
| 786 |
HumanMessage(content=question)
|
| 787 |
],
|
|
|
|
| 788 |
"turn": 0
|
| 789 |
}
|
| 790 |
|
| 791 |
final_answer = "AGENT FAILED TO PRODUCE ANSWER"
|
| 792 |
try:
|
| 793 |
-
|
| 794 |
-
config = {"recursion_limit": MAX_TURNS + 5} # Allow slightly more graph steps than turns
|
| 795 |
for event in self.graph.stream(graph_input, stream_mode="values", config=config):
|
| 796 |
last_message = event["messages"][-1]
|
| 797 |
|
| 798 |
-
# Check for final answer extraction
|
| 799 |
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
| 800 |
if last_message.tool_calls[0].get("name") == "final_answer_tool":
|
| 801 |
final_answer = last_message.tool_calls[0]['args'].get('answer', "ERROR: FINAL_ANSWER_TOOL CALLED WITHOUT ANSWER")
|
| 802 |
print(f"--- Final Answer Captured from tool call: '{final_answer}' ---")
|
| 803 |
-
# We can break here since the graph condition should lead to END anyway
|
| 804 |
break
|
| 805 |
|
| 806 |
-
# Log other message types (optional but helpful)
|
| 807 |
elif isinstance(last_message, ToolMessage):
|
| 808 |
print(f"Tool Result ({last_message.tool_call_id}): {last_message.content[:500]}...")
|
| 809 |
elif isinstance(last_message, AIMessage) and not last_message.tool_calls:
|
| 810 |
-
|
| 811 |
-
print(f"AI Message (Plan/Thought): {last_message.content[:500]}...")
|
| 812 |
-
# Don't set final_answer here anymore, only final_answer_tool counts
|
| 813 |
|
| 814 |
-
# --- Cleaning step (Keep as is) ---
|
| 815 |
cleaned_answer = str(final_answer).strip()
|
| 816 |
-
# ... (keep existing prefix removal and fence removal logic) ...
|
| 817 |
prefixes_to_remove = ["The answer is:", "Here is the answer:", "Based on the information:", "Final Answer:", "Answer:"]
|
| 818 |
original_cleaned = cleaned_answer
|
| 819 |
for prefix in prefixes_to_remove:
|
| 820 |
if cleaned_answer.lower().startswith(prefix.lower()):
|
| 821 |
potential_answer = cleaned_answer[len(prefix):].strip()
|
| 822 |
if potential_answer: cleaned_answer = potential_answer; break
|
| 823 |
-
|
| 824 |
-
print(f"Warning: Prefix found but not stripped: '{original_cleaned[:100]}...'")
|
| 825 |
-
# Simple fence removal
|
| 826 |
cleaned_answer = remove_fences_simple(cleaned_answer)
|
| 827 |
if cleaned_answer.startswith("`") and cleaned_answer.endswith("`"):
|
| 828 |
-
|
|
|
|
| 829 |
print(f"Agent returning final answer (cleaned): '{cleaned_answer}'")
|
| 830 |
return cleaned_answer
|
|
|
|
| 831 |
except Exception as e:
|
| 832 |
print(f"Error running agent graph: {e}")
|
| 833 |
tb_str = traceback.format_exc()
|
| 834 |
print(tb_str)
|
| 835 |
-
# Check if it was specifically our turn limit message
|
| 836 |
-
if isinstance(e, SystemMessage) and f"maximum turn limit ({MAX_TURNS})" in str(e.content):
|
| 837 |
-
return f"AGENT STOPPED: Reached maximum turn limit ({MAX_TURNS})."
|
| 838 |
return f"AGENT GRAPH ERROR: {e}"
|
| 839 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
# --- (Original Template Code - Mock Questions Version) ---
|
| 841 |
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
| 842 |
"""
|
|
@@ -846,13 +766,12 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
|
|
| 846 |
space_id = os.getenv("SPACE_ID")
|
| 847 |
username = profile.username if profile else "local_test_user"
|
| 848 |
print(f"User: {username}{'' if profile else ' (dummy)'}")
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
agent
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
print("Agent instantiated successfully.")
|
| 856 |
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else "local_run"
|
| 857 |
print(f"Agent code URL: {agent_code}")
|
| 858 |
print("--- USING MOCK QUESTIONS ---")
|
|
@@ -953,12 +872,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
|
|
| 953 |
file_path = item.get("file_path")
|
| 954 |
question_text_with_context = question_text
|
| 955 |
if file_path:
|
| 956 |
-
|
| 957 |
-
potential_path = os.path.join(base_dir, file_path)
|
| 958 |
-
file_context = f"[Attached File (provided): {file_path}]"
|
| 959 |
-
if os.path.exists(potential_path): file_context = f"[Attached File (exists): {file_path}]"
|
| 960 |
-
else: file_context = f"[Attached File (NOT FOUND): {file_path}]"
|
| 961 |
-
question_text_with_context = f"{question_text}\n\n{file_context}"
|
| 962 |
print(f"Q includes file: {file_path}")
|
| 963 |
|
| 964 |
submitted_answer = agent(question_text_with_context)
|
|
@@ -1010,4 +924,4 @@ if __name__ == "__main__":
|
|
| 1010 |
except FileNotFoundError: print("Warning: CWD listing failed.")
|
| 1011 |
print("-"*(60 + len(" App Starting ")) + "\n")
|
| 1012 |
print("Launching Gradio Interface...")
|
| 1013 |
-
demo.queue().launch(debug=True, share=False)
|
|
|
|
| 27 |
from langchain_core.tools import tool
|
| 28 |
from langchain_groq import ChatGroq
|
| 29 |
|
| 30 |
+
# --- RAG Imports ---
|
| 31 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 32 |
+
from langchain_community.vectorstores import FAISS
|
| 33 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 34 |
+
|
| 35 |
# --- Constants ---
|
| 36 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 37 |
+
MAX_TURNS = 20
|
| 38 |
+
MAX_MESSAGE_LENGTH = 8000
|
| 39 |
|
| 40 |
# --- Initialize ASR Pipeline ---
|
| 41 |
asr_pipeline = None
|
|
|
|
| 55 |
print(f"⚠️ Warning: Could not load ASR pipeline globally. Error: {e}")
|
| 56 |
asr_pipeline = None
|
| 57 |
|
| 58 |
+
# Global agent declaration for RAG tool access
|
| 59 |
+
agent = None
|
| 60 |
+
|
| 61 |
# ====================================================
|
| 62 |
# --- Tool Definitions ---
|
| 63 |
|
|
|
|
| 71 |
try:
|
| 72 |
search = DuckDuckGoSearchRun()
|
| 73 |
result = search.run(query)
|
|
|
|
| 74 |
if len(result) > MAX_MESSAGE_LENGTH:
|
| 75 |
result = result[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(result)} total chars]"
|
| 76 |
return result
|
| 77 |
except Exception as e:
|
|
|
|
|
|
|
| 78 |
return f"Error running search for '{query}': {str(e)}"
|
| 79 |
|
| 80 |
|
|
|
|
| 99 |
if pattern in code_lower:
|
| 100 |
return f"Error: Potentially dangerous operation '{pattern}' is not allowed."
|
| 101 |
|
|
|
|
| 102 |
if 'open(' in code_lower and any(mode in code for mode in ["'w'", '"w"', "'a'", '"a"', "'wb'", '"wb"']):
|
| 103 |
return "Error: Writing files is not allowed in code_interpreter. Use write_file tool instead."
|
| 104 |
|
|
|
|
| 121 |
return f"Error in execution:\n{stderr}\n\nStdout (if any):\n{stdout}"
|
| 122 |
|
| 123 |
if stdout:
|
|
|
|
| 124 |
if len(stdout) > MAX_MESSAGE_LENGTH:
|
| 125 |
stdout = stdout[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(stdout)} total chars]"
|
| 126 |
return f"Success:\n{stdout}"
|
|
|
|
| 129 |
|
| 130 |
except Exception as e:
|
| 131 |
tb_str = traceback.format_exc()
|
| 132 |
+
return f"Execution failed:\n{tb_str}"
|
|
|
|
|
|
|
| 133 |
|
| 134 |
|
| 135 |
@tool
|
|
|
|
| 144 |
script_dir = os.getcwd()
|
| 145 |
safe_path = os.path.normpath(path)
|
| 146 |
|
|
|
|
| 147 |
paths_to_try = [
|
| 148 |
+
os.path.join(script_dir, safe_path),
|
| 149 |
+
safe_path,
|
| 150 |
+
os.path.join(os.getcwd(), os.path.basename(safe_path))
|
| 151 |
]
|
| 152 |
|
| 153 |
full_path = None
|
|
|
|
| 157 |
break
|
| 158 |
|
| 159 |
if not full_path:
|
| 160 |
+
cwd_files = os.listdir(".")
|
|
|
|
|
|
|
|
|
|
| 161 |
return (f"Error: File not found: '{path}'\n"
|
| 162 |
+
f"Tried paths:\n" + "\n".join(f" - {p}" for p in paths_to_try) +
|
| 163 |
+
f"\n\nFiles in current directory: {cwd_files}")
|
| 164 |
|
| 165 |
print(f"Reading file: {full_path}")
|
|
|
|
|
|
|
| 166 |
_, ext = os.path.splitext(full_path)
|
| 167 |
|
| 168 |
try:
|
| 169 |
with open(full_path, 'r', encoding='utf-8') as f:
|
| 170 |
content = f.read()
|
|
|
|
|
|
|
| 171 |
if len(content) > MAX_MESSAGE_LENGTH:
|
| 172 |
content = content[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(content)} total chars]"
|
|
|
|
| 173 |
return content
|
| 174 |
|
| 175 |
except UnicodeDecodeError:
|
|
|
|
| 176 |
try:
|
| 177 |
with open(full_path, 'rb') as f:
|
| 178 |
binary_content = f.read()
|
| 179 |
return f"File appears to be binary ({len(binary_content)} bytes). Cannot display as text.\nFile type: {ext}\nConsider using audio_transcription_tool for audio files."
|
| 180 |
except Exception as bin_e:
|
| 181 |
return f"Error: Could not read file as text or binary: {str(bin_e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
except Exception as read_e:
|
| 183 |
+
return f"Error reading file: {str(read_e)}"
|
|
|
|
| 184 |
|
| 185 |
except Exception as e:
|
|
|
|
|
|
|
| 186 |
return f"Unexpected error accessing file '{path}': {str(e)}"
|
| 187 |
|
| 188 |
|
|
|
|
| 200 |
base_dir = os.getcwd()
|
| 201 |
full_path = os.path.join(base_dir, path)
|
| 202 |
|
|
|
|
| 203 |
dir_path = os.path.dirname(full_path)
|
| 204 |
if dir_path:
|
| 205 |
os.makedirs(dir_path, exist_ok=True)
|
|
|
|
| 209 |
|
| 210 |
return f"Successfully wrote {len(content)} characters to '{path}'."
|
| 211 |
|
|
|
|
|
|
|
| 212 |
except Exception as e:
|
| 213 |
+
return f"Error writing file '{path}': {str(e)}"
|
|
|
|
| 214 |
|
| 215 |
|
| 216 |
@tool
|
|
|
|
| 233 |
if not items:
|
| 234 |
return f"Directory '{path}' is empty."
|
| 235 |
|
| 236 |
+
files, directories = [], []
|
|
|
|
|
|
|
|
|
|
| 237 |
for item in sorted(items):
|
| 238 |
item_path = os.path.join(full_path, item)
|
| 239 |
if os.path.isdir(item_path):
|
|
|
|
| 250 |
|
| 251 |
return result
|
| 252 |
|
|
|
|
|
|
|
| 253 |
except Exception as e:
|
| 254 |
+
return f"Error listing directory '{path}': {str(e)}"
|
|
|
|
| 255 |
|
| 256 |
|
| 257 |
@tool
|
|
|
|
| 266 |
return "Error: ASR pipeline is not available. Audio transcription cannot be performed."
|
| 267 |
|
| 268 |
try:
|
|
|
|
| 269 |
script_dir = os.getcwd()
|
| 270 |
safe_path = os.path.normpath(file_path)
|
| 271 |
|
|
|
|
| 289 |
result_text = transcription.get("text", "")
|
| 290 |
|
| 291 |
if not result_text:
|
| 292 |
+
return "Error: Transcription produced no text."
|
| 293 |
|
|
|
|
| 294 |
if len(result_text) > MAX_MESSAGE_LENGTH:
|
| 295 |
+
result_text = result_text[:MAX_MESSAGE_LENGTH] + f"\n...[truncated]"
|
| 296 |
|
| 297 |
return f"Transcription:\n{result_text}"
|
| 298 |
|
| 299 |
except Exception as e:
|
| 300 |
+
return f"Error transcribing '{file_path}': {str(e)}"
|
|
|
|
| 301 |
|
| 302 |
|
| 303 |
@tool
|
|
|
|
| 309 |
print(f"--- Calling YouTube Transcript: {video_url} ---")
|
| 310 |
|
| 311 |
try:
|
|
|
|
| 312 |
video_id = None
|
| 313 |
if "watch?v=" in video_url:
|
| 314 |
video_id = video_url.split("v=")[1].split("&")[0]
|
| 315 |
elif "youtu.be/" in video_url:
|
| 316 |
video_id = video_url.split("youtu.be/")[1].split("?")[0]
|
|
|
|
|
|
|
| 317 |
|
| 318 |
if not video_id:
|
| 319 |
+
return f"Error: Could not extract YouTube video ID from '{video_url}'."
|
| 320 |
+
|
|
|
|
| 321 |
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
|
| 322 |
|
| 323 |
if not transcript_list:
|
| 324 |
+
return "Error: No transcript found for this video."
|
| 325 |
+
|
| 326 |
full_transcript = " ".join([item["text"] for item in transcript_list])
|
| 327 |
|
|
|
|
| 328 |
if len(full_transcript) > MAX_MESSAGE_LENGTH:
|
| 329 |
+
full_transcript = full_transcript[:MAX_MESSAGE_LENGTH] + f"\n...[truncated]"
|
| 330 |
|
| 331 |
return f"YouTube Transcript:\n{full_transcript}"
|
| 332 |
|
| 333 |
except Exception as e:
|
| 334 |
+
return f"Error getting transcript for '{video_url}': {str(e)}"
|
|
|
|
| 335 |
|
| 336 |
|
| 337 |
+
# --- NEW RAG-BASED SCRAPER TOOL ---
|
| 338 |
@tool
|
| 339 |
+
def scrape_and_retrieve(url: str, query: str) -> str:
|
| 340 |
+
"""
|
| 341 |
+
Scrapes a webpage, chunks its content, and performs a RAG (Retrieval-Augmented Generation)
|
| 342 |
+
search to find the most relevant information related to a query.
|
| 343 |
+
Use this to "ask a question" of a webpage.
|
| 344 |
|
| 345 |
+
Args:
|
| 346 |
+
url (str): The URL to scrape (must start with http:// or https://).
|
| 347 |
+
query (str): The specific question to answer or information to find on the page.
|
| 348 |
+
"""
|
| 349 |
+
if not (url.lower().startswith(('http://', 'https://'))):
|
| 350 |
return f"Error: Invalid URL. Must start with http:// or https://. Got: '{url}'"
|
| 351 |
+
if not query:
|
| 352 |
+
return "Error: A query is required to search the page content."
|
| 353 |
+
if not agent or not agent.embeddings or not agent.text_splitter:
|
| 354 |
+
return "Error: RAG components are not initialized. Cannot perform retrieval."
|
| 355 |
+
|
| 356 |
+
print(f"--- Calling RAG Scraper: {url} for query: {query} ---")
|
| 357 |
|
| 358 |
try:
|
| 359 |
+
# 1. Scrape
|
| 360 |
headers = {
|
| 361 |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
| 362 |
}
|
|
|
|
| 363 |
response = requests.get(url, headers=headers, timeout=20)
|
| 364 |
response.raise_for_status()
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
soup = BeautifulSoup(response.text, 'html.parser')
|
| 367 |
+
for tag in soup(["script", "style", "nav", "footer", "aside", "header"]):
|
|
|
|
|
|
|
|
|
|
| 368 |
tag.extract()
|
| 369 |
|
| 370 |
+
main_content = soup.find('main') or soup.find('article') or soup.body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
if not main_content:
|
| 372 |
+
return "Error: Could not find main content on the page."
|
| 373 |
|
| 374 |
text = main_content.get_text(separator='\n', strip=True)
|
| 375 |
+
text = '\n'.join(chunk for chunk in (line.strip() for line in text.splitlines()) if chunk)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
if not text:
|
| 378 |
+
return "Error: Scraped content was empty."
|
| 379 |
|
| 380 |
+
# 2. Split
|
| 381 |
+
docs = agent.text_splitter.create_documents([text])
|
| 382 |
+
if not docs:
|
| 383 |
+
return "Error: Text could not be split into documents."
|
| 384 |
+
|
| 385 |
+
# 3. Embed & Create Vector Store
|
| 386 |
+
db = FAISS.from_documents(docs, agent.embeddings)
|
| 387 |
|
| 388 |
+
# 4. Retrieve
|
| 389 |
+
retriever = db.as_retriever(search_kwargs={"k": 5}) # Get top 5 chunks
|
| 390 |
+
retrieved_docs = retriever.invoke(query)
|
| 391 |
|
| 392 |
+
if not retrieved_docs:
|
| 393 |
+
return "Error: No relevant information found on the page for that query."
|
| 394 |
+
|
| 395 |
+
# 5. Format and Return
|
| 396 |
+
context = "\n\n---\n\n".join([doc.page_content for doc in retrieved_docs])
|
| 397 |
+
return f"Relevant Context from {url} for query '{query}':\n\n{context}"
|
| 398 |
+
|
| 399 |
except Exception as e:
|
| 400 |
tb_str = traceback.format_exc()
|
| 401 |
+
return f"Error scraping or retrieving from {url}: {str(e)}\n{tb_str}"
|
| 402 |
|
| 403 |
|
| 404 |
@tool
|
|
|
|
| 406 |
"""
|
| 407 |
Call this tool ONLY when you have the final, definitive answer.
|
| 408 |
The 'answer' must be EXACTLY what was asked for, with no extra text.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
"""
|
| 410 |
if not isinstance(answer, str):
|
| 411 |
try:
|
|
|
|
| 423 |
"""Remove code fences from text."""
|
| 424 |
original_text = text
|
| 425 |
text = text.strip()
|
|
|
|
| 426 |
if text.startswith("```") and text.endswith("```"):
|
| 427 |
text = text[3:-3].strip()
|
| 428 |
if '\n' in text:
|
| 429 |
first_line, rest = text.split('\n', 1)
|
|
|
|
| 430 |
if first_line.strip().replace('_','').isalnum() and len(first_line.strip()) < 15:
|
| 431 |
text = rest.strip()
|
| 432 |
return text
|
|
|
|
| 433 |
return original_text
|
| 434 |
|
| 435 |
|
|
|
|
| 442 |
list_directory,
|
| 443 |
audio_transcription_tool,
|
| 444 |
get_youtube_transcript,
|
| 445 |
+
scrape_and_retrieve, # Replaced scrape_web_page
|
| 446 |
final_answer_tool
|
| 447 |
]
|
| 448 |
|
|
|
|
| 450 |
# --- LangGraph Agent State ---
|
| 451 |
class AgentState(TypedDict):
|
| 452 |
messages: Annotated[List[AnyMessage], add_messages]
|
| 453 |
+
plan: List[str] # A list of steps to execute
|
| 454 |
turn: int
|
| 455 |
|
| 456 |
|
| 457 |
# --- Conditional Edge Function ---
|
| 458 |
+
def route_from_planner(state: AgentState):
|
| 459 |
+
"""
|
| 460 |
+
Routes to the executor if a plan exists, or ends the graph if the plan is complete.
|
| 461 |
+
"""
|
| 462 |
+
plan = state.get('plan', [])
|
| 463 |
+
if plan:
|
| 464 |
+
print("--- Condition: Plan has steps. Routing to executor. ---")
|
| 465 |
+
return "executor"
|
| 466 |
+
else:
|
| 467 |
+
print("--- Condition: Plan is empty. Ending. ---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
return END
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
|
| 471 |
# ====================================================
|
| 472 |
# --- Basic Agent Class ---
|
| 473 |
class BasicAgent:
|
| 474 |
def __init__(self):
|
| 475 |
+
print("BasicAgent (Planner-Executor) initializing...")
|
| 476 |
|
| 477 |
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 478 |
if not GROQ_API_KEY:
|
|
|
|
| 480 |
|
| 481 |
self.tools = defined_tools
|
| 482 |
|
| 483 |
+
# --- Initialize RAG Components ---
|
| 484 |
+
print("Initializing RAG components...")
|
| 485 |
+
try:
|
| 486 |
+
self.embeddings = HuggingFaceEmbeddings(
|
| 487 |
+
model_name="sentence-transformers/all-MiniLM-L6-v2",
|
| 488 |
+
model_kwargs={'device': 'cpu'}
|
| 489 |
+
)
|
| 490 |
+
self.text_splitter = RecursiveCharacterTextSplitter(
|
| 491 |
+
chunk_size=1000,
|
| 492 |
+
chunk_overlap=200
|
| 493 |
+
)
|
| 494 |
+
print("✅ RAG components initialized.")
|
| 495 |
+
except Exception as e:
|
| 496 |
+
print(f"⚠️ Warning: Could not initialize RAG components. Error: {e}")
|
| 497 |
+
self.embeddings = None
|
| 498 |
+
self.text_splitter = None
|
| 499 |
+
|
| 500 |
# Build tool descriptions
|
| 501 |
tool_desc_list = []
|
| 502 |
for tool in self.tools:
|
| 503 |
+
desc = f"- {tool.name}: {tool.description}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
tool_desc_list.append(desc)
|
|
|
|
| 505 |
tool_descriptions = "\n".join(tool_desc_list)
|
| 506 |
|
| 507 |
+
# ==================== SYSTEM PROMPT V7 (Simplified) ====================
|
| 508 |
self.system_prompt = f"""You are a highly intelligent AI assistant for the GAIA benchmark.
|
| 509 |
Your goal: Provide the EXACT answer in the EXACT format requested.
|
| 510 |
|
| 511 |
**PROTOCOL:**
|
| 512 |
|
| 513 |
+
1. **ANALYZE:** Read the question. What info is needed? What is the answer format?
|
| 514 |
+
2. **ACT:** Call ONE tool to get information.
|
| 515 |
+
3. **EVALUATE:** Look at the tool's output. Do you have the final answer?
|
| 516 |
+
- **If NO:** Go back to Step 2.
|
| 517 |
+
- **If YES:** Call final_answer_tool immediately.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
|
| 519 |
**CRITICAL RULES:**
|
| 520 |
|
| 521 |
+
- **TOOL USE:** You MUST use tools to find the answer. Do NOT use your own knowledge.
|
| 522 |
+
- **FINAL ANSWER:** When you have the answer, use final_answer_tool. The 'answer' argument must be the answer ONLY (e.g., "42", "red, blue, green").
|
| 523 |
+
- **JSON FORMAT:** All tool calls MUST be in this exact JSON format:
|
| 524 |
+
{{ "name": "tool_name", "arguments": {{"key": "value"}} }}
|
| 525 |
+
|
| 526 |
+
**EXAMPLE: CODE INTERPRETER**
|
| 527 |
+
{{ "name": "code_interpreter", "arguments": {{"code": "print(1 + 1)"}} }}
|
| 528 |
+
|
| 529 |
+
**EXAMPLE: FINAL ANSWER**
|
| 530 |
+
{{ "name": "final_answer_tool", "arguments": {{"answer": "28"}} }}
|
| 531 |
|
| 532 |
+
**EXAMPLE: RAG SCRAPER**
|
| 533 |
+
{{ "name": "scrape_and_retrieve", "arguments": {{"url": "https://example.com", "query": "what is X?"}} }}
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
**TOOLS:**
|
| 536 |
{tool_descriptions}
|
| 537 |
|
| 538 |
+
**REMEMBER:** Use tools. Format JSON correctly.
|
| 539 |
"""
|
| 540 |
|
| 541 |
+
print("Initializing Groq LLMs...")
|
| 542 |
try:
|
| 543 |
+
# LLM 1: The Executor (binds to tools)
|
| 544 |
+
self.executor_llm = ChatGroq(
|
| 545 |
+
temperature=0,
|
| 546 |
groq_api_key=GROQ_API_KEY,
|
| 547 |
+
model_name="llama-3.3-70b-versatile",
|
| 548 |
+
max_tokens=4096,
|
| 549 |
+
timeout=60
|
| 550 |
+
).bind_tools(self.tools)
|
| 551 |
+
print("✅ Executor LLM (with tools) initialized.")
|
| 552 |
+
|
| 553 |
+
# LLM 2: The Planner (no tools, just reasoning)
|
| 554 |
+
self.planner_llm = ChatGroq(
|
| 555 |
+
temperature=0,
|
| 556 |
+
groq_api_key=GROQ_API_KEY,
|
| 557 |
+
model_name="llama-3.3-70b-versatile",
|
| 558 |
max_tokens=4096,
|
| 559 |
timeout=60
|
| 560 |
)
|
| 561 |
+
print("✅ Planner LLM (no tools) initialized.")
|
| 562 |
except Exception as e:
|
| 563 |
print(f"❌ Error initializing Groq: {e}")
|
| 564 |
raise
|
| 565 |
+
|
| 566 |
+
# --- Define Planner Prompt ---
|
| 567 |
+
self.planner_prompt = f"""You are a master planner. Your job is to create a step-by-step plan
|
| 568 |
+
to solve the user's request. You will be given the user's question and a history of
|
| 569 |
+
all executed steps and their results.
|
| 570 |
+
|
| 571 |
+
Your system prompt (which you must obey) is:
|
| 572 |
+
{self.system_prompt}
|
| 573 |
+
|
| 574 |
+
Review the chat history.
|
| 575 |
+
- If the last message was a tool result, analyze it.
|
| 576 |
+
- If the original goal is not yet met, create an updated, numbered list of the *next* steps.
|
| 577 |
+
- If the goal IS met, or if the last tool call (like final_answer_tool)
|
| 578 |
+
achieved the goal, you must respond with an empty plan list: []
|
| 579 |
+
|
| 580 |
+
**CRITICAL:**
|
| 581 |
+
- Your plan should be a Python list of strings: ["Step 1", "Step 2"]
|
| 582 |
+
- If the user's request is simple (e.g., "What is 2+2?"), your plan might be a single step.
|
| 583 |
+
- If the goal is complete, return an empty list: []
|
| 584 |
+
|
| 585 |
+
Current Chat History:
|
| 586 |
+
[HISTORY]
|
| 587 |
+
"""
|
| 588 |
|
| 589 |
+
# --- Node 1: The Planner ---
|
| 590 |
+
def planner_node(state: AgentState):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
current_turn = state.get('turn', 0) + 1
|
| 592 |
print(f"\n{'='*60}")
|
| 593 |
+
print(f"PLANNER TURN {current_turn}/{MAX_TURNS}")
|
| 594 |
print('='*60)
|
| 595 |
+
|
| 596 |
+
if current_turn > MAX_TURNS:
|
| 597 |
+
print("--- Condition: Max turns reached. Ending. ---")
|
| 598 |
+
return {"plan": []}
|
| 599 |
+
|
| 600 |
+
# Format history for the prompt
|
| 601 |
+
history_str = "\n".join([msg.pretty_repr() for msg in state['messages']])
|
| 602 |
+
prompt = self.planner_prompt.replace("[HISTORY]", history_str)
|
| 603 |
|
| 604 |
+
# Planner just generates text (the plan)
|
| 605 |
+
plan_str = self.planner_llm.invoke(prompt).content
|
| 606 |
|
| 607 |
+
# Try to parse the plan string into a list
|
| 608 |
+
try:
|
| 609 |
+
match = re.search(r"(\[.*?\])", plan_str, re.DOTALL)
|
| 610 |
+
if match:
|
| 611 |
+
plan_list = json.loads(match.group(1))
|
| 612 |
+
else:
|
| 613 |
+
plan_list = []
|
| 614 |
+
|
| 615 |
+
if not isinstance(plan_list, list):
|
| 616 |
+
plan_list = []
|
| 617 |
+
|
| 618 |
+
except Exception as e:
|
| 619 |
+
print(f"⚠️ Planner Error: Could not parse plan. Defaulting to empty plan. Error: {e}")
|
| 620 |
+
print(f"Raw plan string: {plan_str}")
|
| 621 |
+
plan_list = []
|
| 622 |
+
|
| 623 |
+
print(f"📋 Plan Generated: {plan_list}")
|
| 624 |
+
return {"plan": plan_list, "turn": current_turn}
|
| 625 |
+
|
| 626 |
+
# --- Node 2: The Executor ---
|
| 627 |
+
def executor_node(state: AgentState):
|
| 628 |
+
print(f"\n--- EXECUTOR ---")
|
| 629 |
+
|
| 630 |
+
plan = state['plan']
|
| 631 |
+
current_step = plan[0]
|
| 632 |
+
remaining_plan = plan[1:]
|
| 633 |
+
|
| 634 |
+
print(f"Executing Step: {current_step}")
|
| 635 |
+
|
| 636 |
+
executor_messages = state['messages'] + [
|
| 637 |
+
HumanMessage(
|
| 638 |
+
content=f"My current task is to: {current_step}\n\n"
|
| 639 |
+
"Based on this task and the chat history, "
|
| 640 |
+
"call the ONE most appropriate tool."
|
| 641 |
+
)
|
| 642 |
+
]
|
| 643 |
+
|
| 644 |
max_retries = 3
|
| 645 |
ai_message = None
|
|
|
|
| 646 |
for attempt in range(max_retries):
|
| 647 |
try:
|
| 648 |
+
# Executor calls the tool-bound LLM
|
| 649 |
+
ai_message = self.executor_llm.invoke(executor_messages)
|
| 650 |
break
|
| 651 |
except Exception as e:
|
| 652 |
+
print(f"⚠️ Executor LLM attempt {attempt+1}/{max_retries} failed: {e}")
|
| 653 |
if attempt == max_retries - 1:
|
| 654 |
+
ai_message = AIMessage(
|
| 655 |
+
content=f"Error: Executor LLM failed: {e}"
|
| 656 |
)
|
| 657 |
+
time.sleep(2 ** attempt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
|
|
|
|
| 659 |
if ai_message.tool_calls:
|
| 660 |
+
print(f"🔧 Executor Tool Call: {ai_message.tool_calls[0]['name']}")
|
| 661 |
+
else:
|
| 662 |
+
print("⚠️ Executor: No tool call. Passing reasoning to planner.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
|
| 664 |
+
return {"messages": [ai_message], "plan": remaining_plan}
|
| 665 |
+
|
| 666 |
+
# --- Tool Node ---
|
| 667 |
tool_node = ToolNode(self.tools)
|
| 668 |
+
|
| 669 |
+
# --- Build Graph ---
|
| 670 |
+
print("Building Planner-Executor graph...")
|
| 671 |
graph_builder = StateGraph(AgentState)
|
| 672 |
+
|
| 673 |
+
graph_builder.add_node("planner", planner_node)
|
| 674 |
+
graph_builder.add_node("executor", executor_node)
|
| 675 |
graph_builder.add_node("tools", tool_node)
|
| 676 |
|
| 677 |
+
graph_builder.add_edge(START, "planner")
|
|
|
|
| 678 |
|
| 679 |
graph_builder.add_conditional_edges(
|
| 680 |
+
"planner",
|
| 681 |
+
route_from_planner,
|
| 682 |
{
|
| 683 |
+
"executor": "executor",
|
|
|
|
| 684 |
END: END
|
| 685 |
}
|
| 686 |
)
|
| 687 |
|
| 688 |
+
graph_builder.add_edge("executor", "tools")
|
| 689 |
+
graph_builder.add_edge("tools", "planner") # Loop back to planner
|
| 690 |
+
|
| 691 |
self.graph = graph_builder.compile()
|
| 692 |
+
print("✅ Planner-Executor graph compiled successfully.")
|
| 693 |
+
|
| 694 |
def __call__(self, question: str) -> str:
|
| 695 |
print(f"\n--- Starting Agent Run for Question ---")
|
| 696 |
print(f"Agent received question (first 100 chars): {question[:100]}...")
|
| 697 |
|
| 698 |
+
# Initialize graph input
|
| 699 |
graph_input = {
|
| 700 |
"messages": [
|
| 701 |
SystemMessage(content=self.system_prompt),
|
| 702 |
HumanMessage(content=question)
|
| 703 |
],
|
| 704 |
+
"plan": [], # Start with an empty plan
|
| 705 |
"turn": 0
|
| 706 |
}
|
| 707 |
|
| 708 |
final_answer = "AGENT FAILED TO PRODUCE ANSWER"
|
| 709 |
try:
|
| 710 |
+
config = {"recursion_limit": MAX_TURNS + 5}
|
|
|
|
| 711 |
for event in self.graph.stream(graph_input, stream_mode="values", config=config):
|
| 712 |
last_message = event["messages"][-1]
|
| 713 |
|
|
|
|
| 714 |
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
| 715 |
if last_message.tool_calls[0].get("name") == "final_answer_tool":
|
| 716 |
final_answer = last_message.tool_calls[0]['args'].get('answer', "ERROR: FINAL_ANSWER_TOOL CALLED WITHOUT ANSWER")
|
| 717 |
print(f"--- Final Answer Captured from tool call: '{final_answer}' ---")
|
|
|
|
| 718 |
break
|
| 719 |
|
|
|
|
| 720 |
elif isinstance(last_message, ToolMessage):
|
| 721 |
print(f"Tool Result ({last_message.tool_call_id}): {last_message.content[:500]}...")
|
| 722 |
elif isinstance(last_message, AIMessage) and not last_message.tool_calls:
|
| 723 |
+
print(f"AI Message (Executor): {last_message.content[:500]}...")
|
|
|
|
|
|
|
| 724 |
|
|
|
|
| 725 |
cleaned_answer = str(final_answer).strip()
|
|
|
|
| 726 |
prefixes_to_remove = ["The answer is:", "Here is the answer:", "Based on the information:", "Final Answer:", "Answer:"]
|
| 727 |
original_cleaned = cleaned_answer
|
| 728 |
for prefix in prefixes_to_remove:
|
| 729 |
if cleaned_answer.lower().startswith(prefix.lower()):
|
| 730 |
potential_answer = cleaned_answer[len(prefix):].strip()
|
| 731 |
if potential_answer: cleaned_answer = potential_answer; break
|
| 732 |
+
|
|
|
|
|
|
|
| 733 |
cleaned_answer = remove_fences_simple(cleaned_answer)
|
| 734 |
if cleaned_answer.startswith("`") and cleaned_answer.endswith("`"):
|
| 735 |
+
cleaned_answer = cleaned_answer[1:-1].strip()
|
| 736 |
+
|
| 737 |
print(f"Agent returning final answer (cleaned): '{cleaned_answer}'")
|
| 738 |
return cleaned_answer
|
| 739 |
+
|
| 740 |
except Exception as e:
|
| 741 |
print(f"Error running agent graph: {e}")
|
| 742 |
tb_str = traceback.format_exc()
|
| 743 |
print(tb_str)
|
|
|
|
|
|
|
|
|
|
| 744 |
return f"AGENT GRAPH ERROR: {e}"
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
# ====================================================
|
| 748 |
+
# --- Global Agent Instantiation ---
|
| 749 |
+
|
| 750 |
+
try:
|
| 751 |
+
agent = BasicAgent()
|
| 752 |
+
print("��� Global BasicAgent instantiated successfully.")
|
| 753 |
+
if asr_pipeline is None: print("⚠️ Global ASR Pipeline failed load.")
|
| 754 |
+
except Exception as e:
|
| 755 |
+
print(f"❌ FATAL: Could not instantiate global agent: {e}")
|
| 756 |
+
traceback.print_exc()
|
| 757 |
+
agent = None
|
| 758 |
+
|
| 759 |
+
# ====================================================
|
| 760 |
# --- (Original Template Code - Mock Questions Version) ---
|
| 761 |
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
| 762 |
"""
|
|
|
|
| 766 |
space_id = os.getenv("SPACE_ID")
|
| 767 |
username = profile.username if profile else "local_test_user"
|
| 768 |
print(f"User: {username}{'' if profile else ' (dummy)'}")
|
| 769 |
+
|
| 770 |
+
# Check if global agent initialized
|
| 771 |
+
if not agent:
|
| 772 |
+
return "FATAL ERROR: Global agent failed to initialize. Check logs.", None
|
| 773 |
+
|
| 774 |
+
print("Using globally instantiated agent.")
|
|
|
|
| 775 |
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else "local_run"
|
| 776 |
print(f"Agent code URL: {agent_code}")
|
| 777 |
print("--- USING MOCK QUESTIONS ---")
|
|
|
|
| 872 |
file_path = item.get("file_path")
|
| 873 |
question_text_with_context = question_text
|
| 874 |
if file_path:
|
| 875 |
+
question_text_with_context = f"{question_text}\n\n[Attached File: {file_path}]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 876 |
print(f"Q includes file: {file_path}")
|
| 877 |
|
| 878 |
submitted_answer = agent(question_text_with_context)
|
|
|
|
| 924 |
except FileNotFoundError: print("Warning: CWD listing failed.")
|
| 925 |
print("-"*(60 + len(" App Starting ")) + "\n")
|
| 926 |
print("Launching Gradio Interface...")
|
| 927 |
+
demo.queue().launch(debug=True, share=False)
|