gabejavitt commited on
Commit
6428172
·
verified ·
1 Parent(s): f58c066

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +268 -354
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 # Increased from 15 for complex questions
33
- MAX_MESSAGE_LENGTH = 8000 # Truncate long outputs
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
- print(f"--- Code Interpreter FAILED ---\n{tb_str}\n---")
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), # Relative to CWD
149
- safe_path, # Direct/absolute
150
- os.path.join(os.getcwd(), os.path.basename(safe_path)) # Basename in CWD
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
- try:
161
- cwd_files = os.listdir(".")
162
- except Exception:
163
- cwd_files = ["(could not list)"]
164
  return (f"Error: File not found: '{path}'\n"
165
- f"Tried paths:\n" + "\n".join(f" - {p}" for p in paths_to_try) +
166
- f"\n\nFiles in current directory: {cwd_files}")
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
- tb_str = traceback.format_exc()
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
- tb_str = traceback.format_exc()
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
- # Separate files and directories
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
- tb_str = traceback.format_exc()
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. The audio file may be empty or corrupted."
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, original length unknown]"
325
 
326
  return f"Transcription:\n{result_text}"
327
 
328
  except Exception as e:
329
- tb_str = traceback.format_exc()
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}'. Provide a valid YouTube 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. It may not have captions available."
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, {len(full_transcript)} total chars]"
365
 
366
  return f"YouTube Transcript:\n{full_transcript}"
367
 
368
  except Exception as e:
369
- tb_str = traceback.format_exc()
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 scrape_web_page(url: str) -> str:
375
- """Fetches and extracts the main text content from a webpage."""
376
- if not isinstance(url, str) or not url.strip():
377
- return "Error: Invalid input. 'url' must be a non-empty string."
 
378
 
379
- if not url.lower().startswith(('http://', 'https://')):
 
 
 
 
380
  return f"Error: Invalid URL. Must start with http:// or https://. Got: '{url}'"
381
-
382
- print(f"--- Calling Web Scraper: {url} ---")
 
 
 
 
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
- # Try to find main content area
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 area on the page."
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 after cleaning."
422
 
423
- # Truncate if too long
424
- if len(text) > MAX_MESSAGE_LENGTH:
425
- text = text[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(text)} total chars]"
 
 
 
 
426
 
427
- return f"Content from {url}:\n\n{text}"
 
 
428
 
429
- except requests.exceptions.Timeout:
430
- return f"Error: Request to {url} timed out after 20 seconds."
431
- except requests.exceptions.RequestException as req_e:
432
- return f"Error fetching URL {url}: {str(req_e)}"
 
 
 
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 should_continue(state: AgentState):
499
- """Decide whether to continue, call tools, or end."""
500
- last_message = state['messages'][-1]
501
- current_turn = state.get('turn', 0)
502
-
503
- # 1. Check for final_answer_tool
504
- if isinstance(last_message, AIMessage) and last_message.tool_calls:
505
- for tool_call in last_message.tool_calls:
506
- if tool_call.get("name") == "final_answer_tool":
507
- print("--- Condition: final_answer_tool called, ending. ---")
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 (LangGraph) initializing...")
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
- if tool.name == 'code_interpreter':
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 V5 ====================
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 QUESTION:**
579
- - What information is needed?
580
- - What format should the answer be? (number, list, yes/no, name, etc.)
581
- - Are there any files attached?
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
- NEVER guess or use training data for the final answer
608
- NEVER call multiple tools in one turn
609
- NEVER add explanations to final_answer_tool
610
- ALWAYS use code_interpreter for calculations/logic
611
- ✅ ALWAYS match the requested answer format exactly
612
- ALWAYS base your answer on tool outputs, not memory
 
 
 
 
613
 
614
- **ANSWER FORMAT EXAMPLES:**
615
- - "What is 5+5?" final_answer("10")
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:** One tool per turn. Base everything on tool outputs. Match the format exactly.
624
  """
625
 
626
- print("Initializing Groq LLM...")
627
  try:
628
- chat_llm = ChatGroq(
629
- temperature=0, # Maximum determinism
 
630
  groq_api_key=GROQ_API_KEY,
631
- model_name="openai/gpt-oss-120b", # Best reasoning model
 
 
 
 
 
 
 
 
 
 
632
  max_tokens=4096,
633
  timeout=60
634
  )
635
- print("✅ Groq LLM initialized with llama-3.3-70b-versatile")
636
  except Exception as e:
637
  print(f"❌ Error initializing Groq: {e}")
638
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
- self.llm_with_tools = chat_llm.bind_tools(self.tools)
641
- print("✅ Tools bound to LLM")
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"AGENT TURN {current_turn}/{MAX_TURNS}")
649
  print('='*60)
 
 
 
 
 
 
 
 
650
 
651
- messages_to_send = state["messages"]
 
652
 
653
- # Retry logic with exponential backoff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  max_retries = 3
655
  ai_message = None
656
-
657
  for attempt in range(max_retries):
658
  try:
659
- ai_message = self.llm_with_tools.invoke(messages_to_send)
 
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
- error_msg = AIMessage(
665
- content=f"Error: LLM failed after {max_retries} attempts: {str(e)}"
666
  )
667
- return {"messages": [error_msg], "turn": current_turn}
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
- for tc in ai_message.tool_calls:
745
- print(f"🔧 Tool Call: {tc.get('name')}")
746
- print(f" Args: {tc.get('args', {})}")
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], "turn": current_turn}
754
- # --- Tool Node ---
 
755
  tool_node = ToolNode(self.tools)
756
-
757
- # --- Build Graph ---
758
- print("Building agent graph...")
759
  graph_builder = StateGraph(AgentState)
760
- graph_builder.add_node("agent", agent_node)
 
 
761
  graph_builder.add_node("tools", tool_node)
762
 
763
- graph_builder.add_edge(START, "agent")
764
- graph_builder.add_edge("tools", "agent")
765
 
766
  graph_builder.add_conditional_edges(
767
- "agent",
768
- should_continue,
769
  {
770
- "tools": "tools",
771
- "agent": "agent",
772
  END: END
773
  }
774
  )
775
 
 
 
 
776
  self.graph = graph_builder.compile()
777
- print("✅ Graph compiled successfully")
 
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 with turn counter
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
- # Add config for recursion limit (LangGraph default is 25, but our turn limit is softer)
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
- # This is now expected (the "plan" or "think" step)
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
- if cleaned_answer == original_cleaned and any(cleaned_answer.lower().startswith(p.lower()) for p in prefixes_to_remove):
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
- cleaned_answer = cleaned_answer[1:-1].strip()
 
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
- submit_url = f"{DEFAULT_API_URL}/submit"
850
- print("Instantiating agent...")
851
- try:
852
- agent = BasicAgent()
853
- if asr_pipeline is None: print("⚠️ Global ASR Pipeline failed load.")
854
- except Exception as e: print(f"Error instantiating agent: {e}"); import traceback; traceback.print_exc(); return f"Error initializing agent: {e}", None
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
- base_dir = os.getcwd()
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)