gabejavitt commited on
Commit
9e0417b
Β·
verified Β·
1 Parent(s): 9970fe2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +371 -929
app.py CHANGED
@@ -1,267 +1,305 @@
1
  import os
2
- import gradio as gr
3
- import requests
4
- import inspect
5
- import pandas as pd
6
  import io
7
- import contextlib
8
- import traceback
9
- from typing import TypedDict, Annotated, List, Optional
10
- import torch
11
  import json
12
  import re
13
- import uuid
14
- import time
15
- import ast
 
16
 
17
- # --- Pydantic Import ---
 
 
18
  from pydantic import BaseModel, Field
19
 
20
- # --- Multimodal & Web Tool Imports ---
21
  from transformers import pipeline
22
  from youtube_transcript_api import YouTubeTranscriptApi
23
  from bs4 import BeautifulSoup
 
24
 
25
- # --- LangChain & LangGraph Imports ---
26
  from langgraph.graph.message import add_messages
27
- from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, ToolMessage, SystemMessage, ToolCall
28
  from langgraph.prebuilt import ToolNode
29
  from langgraph.graph import START, END, StateGraph
30
- from langchain_community.tools import DuckDuckGoSearchRun
31
  from langchain_core.tools import tool
32
- from langchain_google_genai import ChatGoogleGenerativeAI # <-- For Gemini
33
 
34
- # --- RAG Imports ---
35
  from langchain_text_splitters import RecursiveCharacterTextSplitter
36
  from langchain_community.vectorstores import FAISS
37
  from langchain_community.embeddings import HuggingFaceEmbeddings
 
38
 
39
- # --- Constants ---
40
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
41
- MAX_TURNS = 20
42
- MAX_MESSAGE_LENGTH = 8000
43
-
44
- # --- Initialize ASR Pipeline ---
45
- asr_pipeline = None
46
- try:
47
- print("Loading ASR (Whisper) pipeline globally...")
48
- device = 0 if torch.cuda.is_available() else -1
49
- device_name = "cuda:0" if device == 0 else "cpu"
50
- print(f"Attempting to use device: {device_name} for ASR.")
51
- asr_pipeline = pipeline(
52
- "automatic-speech-recognition",
53
- model="openai/whisper-base",
54
- torch_dtype=torch.float16 if device == 0 else torch.float32,
55
- device=device
56
- )
57
- print("βœ… ASR (Whisper) pipeline loaded successfully.")
58
- except Exception as e:
59
- print(f"⚠️ Warning: Could not load ASR pipeline globally. Error: {e}")
60
- asr_pipeline = None
61
-
62
- # Global agent declaration for RAG tool access
63
- agent = None
64
-
65
- # ====================================================
66
- # --- Tool Definitions (Unchanged) ---
67
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  class SearchInput(BaseModel):
69
  query: str = Field(description="The search query.")
70
 
71
  @tool(args_schema=SearchInput)
72
  def search_tool(query: str) -> str:
73
- """Calls DuckDuckGo search and returns the results. Use this for recent information or general web searches."""
74
- if not isinstance(query, str) or not query.strip():
75
- return "Error: Invalid input. 'query' must be a non-empty string."
76
 
77
- print(f"--- Calling Search Tool with query: {query} ---")
78
  try:
79
  search = DuckDuckGoSearchRun()
80
  result = search.run(query)
81
- if len(result) > MAX_MESSAGE_LENGTH:
82
- result = result[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(result)} total chars]"
83
- return result
84
  except Exception as e:
85
- return f"Error running search for '{query}': {str(e)}"
86
-
87
 
 
88
  class CodeInput(BaseModel):
89
- code: str = Field(description="The Python code to execute, which must include a print() statement for output.")
90
 
91
  @tool(args_schema=CodeInput)
92
  def code_interpreter(code: str) -> str:
93
  """
94
- Executes a string of Python code and returns its stdout, stderr, and any error.
95
- ...
96
- CRITICAL RULES:
97
- 1. ALWAYS use print() to output your final answer.
98
- 2. Write simple, focused code. One task per execution.
99
- 3. Add comments (#) to explain your logic.
100
- 4. SCOPE RULE: Import all necessary libraries (e.g., 'from collections import Counter') *inside* any function you define.
101
- Available: pandas as pd, basic Python libraries.
102
  """
103
  if not isinstance(code, str):
104
- return "Error: Invalid input. 'code' must be a string."
105
 
106
- # Basic safety checks
107
- dangerous_patterns = ['__import__', 'eval(', 'compile(', 'subprocess', 'os.system']
108
- code_lower = code.lower()
109
- for pattern in dangerous_patterns:
110
- if pattern in code_lower:
111
- return f"Error: Potentially dangerous operation '{pattern}' is not allowed."
112
 
113
- if 'open(' in code_lower and any(mode in code for mode in ["'w'", '"w"', "'a'", '"a"', "'wb'", '"wb"']):
114
- return "Error: Writing files is not allowed in code_interpreter. Use write_file tool instead."
115
 
116
- print(f"--- Calling Code Interpreter ---\nCode:\n{code}\n---")
117
  output_stream = io.StringIO()
118
  error_stream = io.StringIO()
119
 
120
  try:
121
- with contextlib.redirect_stdout(output_stream), contextlib.redirect_stderr(error_stream):
 
 
122
  safe_globals = {
123
  "pd": pd,
124
  "__builtins__": __builtins__
125
  }
126
  exec(code, safe_globals, {})
127
-
128
  stdout = output_stream.getvalue()
129
  stderr = error_stream.getvalue()
130
 
131
  if stderr:
132
- return f"Error in execution:\n{stderr}\n\nStdout (if any):\n{stdout}"
133
 
134
  if stdout:
135
- if len(stdout) > MAX_MESSAGE_LENGTH:
136
- stdout = stdout[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(stdout)} total chars]"
137
- return f"Success:\n{stdout}"
138
 
139
- return "Success: Code executed without error but produced no output.\n⚠️ Remember to use print() to output your results!"
140
-
141
- except Exception as e:
142
- tb_str = traceback.format_exc()
143
- return f"Execution failed:\n{tb_str}"
144
-
145
 
 
146
  class ReadFileInput(BaseModel):
147
- path: str = Field(description="The path to the file to read.")
148
 
149
  @tool(args_schema=ReadFileInput)
150
  def read_file(path: str) -> str:
151
- """Reads the content of a file at the specified path. Use this to examine uploaded files or files you've created."""
152
- if not isinstance(path, str) or not path.strip():
153
- return "Error: Invalid input. 'path' must be a non-empty string."
 
 
154
 
155
- print(f"--- Calling Read File Tool: {path} ---")
 
 
 
 
156
 
157
  try:
158
- script_dir = os.getcwd()
159
- safe_path = os.path.normpath(path)
160
-
161
- paths_to_try = [
162
- os.path.join(script_dir, safe_path),
163
- safe_path,
164
- os.path.join(os.getcwd(), os.path.basename(safe_path))
165
- ]
166
-
167
- full_path = None
168
- for attempt_path in paths_to_try:
169
- if os.path.exists(attempt_path):
170
- full_path = attempt_path
171
- break
172
-
173
- if not full_path:
174
- cwd_files = os.listdir(".")
175
- return (f"Error: File not found: '{path}'\n"
176
- f"Tried paths:\n" + "\n".join(f" - {p}" for p in paths_to_try) +
177
- f"\n\nFiles in current directory: {cwd_files}")
178
-
179
- print(f"Reading file: {full_path}")
180
- _, ext = os.path.splitext(full_path)
181
-
182
- try:
183
- with open(full_path, 'r', encoding='utf-8') as f:
184
- content = f.read()
185
- if len(content) > MAX_MESSAGE_LENGTH:
186
- content = content[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(content)} total chars]"
187
- return content
188
-
189
- except UnicodeDecodeError:
190
- try:
191
- with open(full_path, 'rb') as f:
192
- binary_content = f.read()
193
- 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."
194
- except Exception as bin_e:
195
- return f"Error: Could not read file as text or binary: {str(bin_e)}"
196
- except Exception as read_e:
197
- return f"Error reading file: {str(read_e)}"
198
-
199
  except Exception as e:
200
- return f"Unexpected error accessing file '{path}': {str(e)}"
201
-
202
 
203
  class WriteFileInput(BaseModel):
204
- path: str = Field(description="The path of the file to write to.")
205
- content: str = Field(description="The content to write into the file.")
206
 
207
  @tool(args_schema=WriteFileInput)
208
  def write_file(path: str, content: str) -> str:
209
- """Writes content to a file at the specified path. Creates directories if needed."""
210
- if not isinstance(path, str) or not path.strip():
211
- return "Error: Invalid input. 'path' must be a non-empty string."
212
  if not isinstance(content, str):
213
- return "Error: Invalid input. 'content' must be a string."
214
 
215
- print(f"--- Calling Write File Tool: {path} ---")
216
 
217
  try:
218
- base_dir = os.getcwd()
219
- full_path = os.path.join(base_dir, path)
220
-
221
- dir_path = os.path.dirname(full_path)
222
- if dir_path:
223
- os.makedirs(dir_path, exist_ok=True)
224
-
225
- with open(full_path, 'w', encoding='utf-8') as f:
226
- f.write(content)
227
-
228
- return f"Successfully wrote {len(content)} characters to '{path}'."
229
-
230
  except Exception as e:
231
- return f"Error writing file '{path}': {str(e)}"
232
-
233
 
234
  class ListDirInput(BaseModel):
235
- path: str = Field(description="The directory path to list.", default=".")
236
 
237
  @tool(args_schema=ListDirInput)
238
  def list_directory(path: str = ".") -> str:
239
- """Lists the contents of a directory. Useful for finding available files."""
240
- if not isinstance(path, str):
241
- return "Error: Invalid input. 'path' must be a string."
242
-
243
- print(f"--- Calling List Directory Tool: {path} ---")
244
 
245
  try:
246
- base_dir = os.getcwd()
247
- full_path = os.path.join(base_dir, path) if path != "." else base_dir
248
 
249
- if not os.path.isdir(full_path):
250
- return f"Error: '{path}' is not a valid directory."
251
 
252
- items = os.listdir(full_path)
253
 
254
  if not items:
255
- return f"Directory '{path}' is empty."
 
 
 
256
 
257
- files, directories = [], []
258
- for item in sorted(items):
259
- item_path = os.path.join(full_path, item)
260
- if os.path.isdir(item_path):
261
- directories.append(f"πŸ“ {item}/")
262
  else:
263
- size = os.path.getsize(item_path)
264
- files.append(f"πŸ“„ {item} ({size} bytes)")
265
 
266
  result = f"Contents of '{path}':\n\n"
267
  if directories:
@@ -270,72 +308,48 @@ def list_directory(path: str = ".") -> str:
270
  result += "Files:\n" + "\n".join(files)
271
 
272
  return result
273
-
274
  except Exception as e:
275
- return f"Error listing directory '{path}': {str(e)}"
276
-
277
 
 
278
  class AudioInput(BaseModel):
279
- file_path: str = Field(description="The file path of the audio to transcribe.")
280
 
281
  @tool(args_schema=AudioInput)
282
  def audio_transcription_tool(file_path: str) -> str:
283
- """Transcribes an audio file (mp3, wav, etc.) to text using Whisper."""
284
- if not isinstance(file_path, str) or not file_path.strip():
285
- return "Error: Invalid input. 'file_path' must be a non-empty string."
286
 
287
- print(f"--- Calling Audio Transcription: {file_path} ---")
288
 
289
- if asr_pipeline is None:
290
- return "Error: ASR pipeline is not available. Audio transcription cannot be performed."
 
291
 
292
  try:
293
- script_dir = os.getcwd()
294
- safe_path = os.path.normpath(file_path)
295
-
296
- paths_to_try = [
297
- os.path.join(script_dir, safe_path),
298
- safe_path,
299
- os.path.join(os.getcwd(), os.path.basename(safe_path))
300
- ]
301
-
302
- full_path = None
303
- for attempt_path in paths_to_try:
304
- if os.path.exists(attempt_path):
305
- full_path = attempt_path
306
- break
307
-
308
- if not full_path:
309
- return f"Error: Audio file not found: '{file_path}'"
310
-
311
- print(f"Transcribing file: {full_path}")
312
- transcription = asr_pipeline(full_path)
313
- result_text = transcription.get("text", "")
314
-
315
- if not result_text:
316
- return "Error: Transcription produced no text."
317
-
318
- if len(result_text) > MAX_MESSAGE_LENGTH:
319
- result_text = result_text[:MAX_MESSAGE_LENGTH] + f"\n...[truncated]"
320
-
321
- return f"Transcription:\n{result_text}"
322
-
323
  except Exception as e:
324
- return f"Error transcribing '{file_path}': {str(e)}"
325
-
326
 
 
327
  class YoutubeInput(BaseModel):
328
- video_url: str = Field(description="The URL of the YouTube video.")
329
 
330
  @tool(args_schema=YoutubeInput)
331
  def get_youtube_transcript(video_url: str) -> str:
332
- """Fetches the transcript/captions for a YouTube video."""
333
- if not isinstance(video_url, str) or not video_url.strip():
334
- return "Error: Invalid input. 'video_url' must be a non-empty string."
335
 
336
- print(f"--- Calling YouTube Transcript: {video_url} ---")
337
 
338
  try:
 
339
  video_id = None
340
  if "watch?v=" in video_url:
341
  video_id = video_url.split("v=")[1].split("&")[0]
@@ -343,775 +357,203 @@ def get_youtube_transcript(video_url: str) -> str:
343
  video_id = video_url.split("youtu.be/")[1].split("?")[0]
344
 
345
  if not video_id:
346
- return f"Error: Could not extract YouTube video ID from '{video_url}'."
347
-
348
  transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
349
 
350
  if not transcript_list:
351
- return "Error: No transcript found for this video."
352
-
353
- full_transcript = " ".join([item["text"] for item in transcript_list])
354
-
355
- if len(full_transcript) > MAX_MESSAGE_LENGTH:
356
- full_transcript = full_transcript[:MAX_MESSAGE_LENGTH] + f"\n...[truncated]"
357
-
358
- return f"YouTube Transcript:\n{full_transcript}"
359
 
 
 
 
360
  except Exception as e:
361
- return f"Error getting transcript for '{video_url}': {str(e)}"
362
 
363
-
364
- # --- NEW RAG-BASED SCRAPER TOOL ---
365
  class ScrapeInput(BaseModel):
366
- url: str = Field(description="The URL to scrape (must start with http:// or https://).")
367
- query: str = Field(description="The specific question to answer or information to find on the page.")
368
 
369
  @tool(args_schema=ScrapeInput)
370
  def scrape_and_retrieve(url: str, query: str) -> str:
371
  """
372
- Scrapes a webpage, chunks its content, and performs a RAG (Retrieval-Augmented Generation)
373
- search to find the most relevant information related to a query.
374
- Use this to "ask a question" of a webpage.
375
 
376
  Args:
377
- url (str): The URL to scrape (must start with http:// or https://).
378
- query (str): The specific question to answer or information to find on the page.
379
  """
380
- if not (url.lower().startswith(('http://', 'https://'))):
381
- return "Error: Invalid URL. Must start with http:// or https://. Got: '{url}'"
382
  if not query:
383
- return "Error: A query is required to search the page content."
384
- if not agent or not agent.embeddings or not agent.text_splitter:
385
- return "Error: RAG components are not initialized. Cannot perform retrieval."
386
-
387
- print(f"--- Calling RAG Scraper: {url} for query: {query} ---")
 
 
388
 
389
  try:
390
- # 1. Scrape
391
  headers = {
392
- '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'
393
  }
394
  response = requests.get(url, headers=headers, timeout=20)
395
  response.raise_for_status()
396
 
397
  soup = BeautifulSoup(response.text, 'html.parser')
 
 
398
  for tag in soup(["script", "style", "nav", "footer", "aside", "header"]):
399
  tag.extract()
400
 
 
401
  main_content = soup.find('main') or soup.find('article') or soup.body
402
  if not main_content:
403
- return "Error: Could not find main content on the page."
404
 
405
  text = main_content.get_text(separator='\n', strip=True)
406
- text = '\n'.join(chunk for chunk in (line.strip() for line in text.splitlines()) if chunk)
407
 
408
  if not text:
409
- return "Error: Scraped content was empty."
410
 
411
- # 2. Split
412
- docs = agent.text_splitter.create_documents([text])
413
  if not docs:
414
- return "Error: Text could not be split into documents."
415
-
416
- # 3. Embed & Create Vector Store
417
- db = FAISS.from_documents(docs, agent.embeddings)
418
 
419
- # 4. Retrieve
420
- retriever = db.as_retriever(search_kwargs={"k": 5}) # Get top 5 chunks
 
421
  retrieved_docs = retriever.invoke(query)
422
 
423
  if not retrieved_docs:
424
- return "Error: No relevant information found on the page for that query."
425
-
426
- # 5. Format and Return
427
- context = "\n\n---\n\n".join([doc.page_content for doc in retrieved_docs])
428
- return f"Relevant Context from {url} for query '{query}':\n\n{context}"
429
-
430
  except Exception as e:
431
- tb_str = traceback.format_exc()
432
- return f"Error scraping or retrieving from {url}: {str(e)}\n{tb_str}"
433
-
434
 
 
435
  class FinalAnswerInput(BaseModel):
436
- answer: str = Field(description="The final, definitive answer to the question.")
437
 
438
  @tool(args_schema=FinalAnswerInput)
439
  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
  """
444
  if not isinstance(answer, str):
445
- try:
446
- answer = str(answer)
447
- except:
448
- return "Error: Invalid input. 'answer' must be a string."
449
 
450
- print(f"--- FINAL ANSWER TOOL CALLED ---")
451
- print(f"Answer: {answer}")
452
  return answer
453
 
454
-
455
- # --- Helper Function ---
456
- def remove_fences_simple(text):
457
- """Remove code fences from text."""
458
- original_text = text
459
- text = text.strip()
460
- if text.startswith("```") and text.endswith("```"):
461
- text = text[3:-3].strip()
462
- if '\n' in text:
463
- first_line, rest = text.split('\n', 1)
464
- if first_line.strip().replace('_','').isalnum() and len(first_line.strip()) < 15:
465
- text = rest.strip()
466
- return text
467
- return original_text
468
-
469
-
470
- # --- *** ROBUST FALLBACK PARSER *** ---
471
- def parse_tool_call_from_string(content: str, tools: List) -> List[ToolCall]:
472
- """
473
- Parses malformed tool call strings (dribbled) from an LLM response.
474
- """
475
- print(f"Original LLM content for fallback parsing:\n---\n{content}\n---")
476
- tool_name = None
477
- tool_input = None
478
- cleaned_str = None # For storing cleaned string before parsing
479
-
480
- # STRATEGY 1: Try to parse <function(tool_name)>...{json_string}...
481
- # This also handles <function=tool_name>...{json_string}...
482
- func_match = re.search(
483
- r"<function[(=]\s*([^)]+)\s*[)>](.*)", # <-- More robust regex
484
- content,
485
- re.DOTALL | re.IGNORECASE
486
- )
487
 
488
- if func_match:
489
- try:
490
- tool_name = func_match.group(1).strip().replace("'", "").replace('"', '') # Clean tool name
491
- remaining_content = func_match.group(2)
492
-
493
- json_start_index = remaining_content.find('{')
494
- if json_start_index != -1:
495
- json_str = remaining_content[json_start_index:]
496
- # --- Aggressive Cleaning ---
497
- cleaned_str = json_str.strip()
498
- cleaned_str = ''.join(c for c in cleaned_str if c.isprintable() or c in '\n\r\t')
499
- cleaned_str = cleaned_str.strip().rstrip(',')
500
-
501
- tool_input = json.loads(cleaned_str)
502
- print(f"πŸ”§ Fallback (Format 1 - json.loads): Parsed tool call for '{tool_name}'")
503
- else:
504
- print(f"⚠️ Fallback (Format 1): Found <function> but no JSON blob.")
505
- tool_name = None
506
-
507
- except json.JSONDecodeError as e:
508
- print(f"⚠️ Fallback (Format 1): json.loads failed after cleaning: {e}. Trying ast.literal_eval.")
509
- try:
510
- # Secondary attempt with ast.literal_eval
511
- if cleaned_str:
512
- potential_input = ast.literal_eval(cleaned_str)
513
- if isinstance(potential_input, dict):
514
- tool_input = potential_input
515
- print(f"πŸ”§ Fallback (Format 1 - ast.literal_eval): Parsed tool call for '{tool_name}'")
516
- else:
517
- print(f"⚠️ Fallback (Format 1): ast.literal_eval did not produce a dict.")
518
- tool_name = None
519
- else:
520
- tool_name = None
521
-
522
- except (SyntaxError, ValueError) as ast_e:
523
- print(f"⚠️ Fallback (Format 1): ast.literal_eval also failed: {ast_e}")
524
- tool_name = None
525
- except Exception as e_inner:
526
- print(f"⚠️ Fallback (Format 1): Unexpected error during ast.literal_eval: {e_inner}")
527
- tool_name = None
528
-
529
- # ========================================================================
530
- # STRATEGY 2: Try to parse bare JSON (if Strategy 1 failed)
531
- # ========================================================================
532
- if not tool_name:
533
- # First, try to find markdown code fences
534
- fence_match = re.search(r"```(?:json)?\s*(\{.+?\})\s*```", content, re.DOTALL)
535
-
536
- if fence_match:
537
- json_str = fence_match.group(1)
538
- else:
539
- # Use balanced brace extraction for bare JSON
540
- json_str = extract_json_with_balanced_braces(content)
541
 
542
- if json_str:
543
- try:
544
- # Try strict JSON first
545
- parsed_json = json.loads(json_str)
546
- print(f"πŸ”§ Fallback (Format 2 - json.loads): Parsed bare JSON.")
547
- except json.JSONDecodeError as e:
548
- print(f"⚠️ Fallback (Format 2): json.loads failed: {e}")
549
- print(f" Attempting ast.literal_eval...")
550
- try:
551
- parsed_json = ast.literal_eval(json_str)
552
- if isinstance(parsed_json, dict):
553
- print(f"πŸ”§ Fallback (Format 2 - ast.literal_eval): Parsed bare JSON.")
554
- else:
555
- print(f"⚠️ Fallback (Format 2): ast.literal_eval did not produce a dict.")
556
- parsed_json = None
557
- except Exception as ast_e:
558
- print(f"⚠️ Fallback (Format 2): ast.literal_eval also failed: {ast_e}")
559
- parsed_json = None
560
-
561
- # --- If either parser succeeded, try to infer tool name ---
562
- if parsed_json and isinstance(parsed_json, dict):
563
- tool_input = parsed_json
564
-
565
- # ------------------------------------------------
566
- # Infer tool name based on JSON structure
567
- # ORDER MATTERS: Most specific checks first!
568
- # ------------------------------------------------
569
-
570
- # 1. OpenAI/LangChain standard format (name + arguments)
571
- if "name" in parsed_json and "arguments" in parsed_json:
572
- tool_name = parsed_json.get("name")
573
- tool_input = parsed_json.get("arguments", {})
574
- print(f"πŸ”§ Fallback (Format 2): Detected OpenAI-style format for '{tool_name}'")
575
-
576
- # 2. Custom format (tool + tool_input)
577
- elif "tool" in parsed_json and "tool_input" in parsed_json:
578
- tool_name = parsed_json.get("tool")
579
- tool_input = parsed_json.get("tool_input", {})
580
- print(f"πŸ”§ Fallback (Format 2): Detected custom format for '{tool_name}'")
581
-
582
- # 3. Specific tool signatures (most specific first)
583
- elif "url" in parsed_json and "query" in parsed_json:
584
- tool_name = "scrape_and_retrieve"
585
- print(f"πŸ”§ Fallback (Format 2): Inferred 'scrape_and_retrieve'")
586
-
587
- elif "path" in parsed_json and "content" in parsed_json:
588
- tool_name = "write_file"
589
- print(f"πŸ”§ Fallback (Format 2): Inferred 'write_file'")
590
-
591
- elif "video_url" in parsed_json:
592
- tool_name = "get_youtube_transcript"
593
- print(f"πŸ”§ Fallback (Format 2): Inferred 'get_youtube_transcript'")
594
-
595
- elif "file_path" in parsed_json:
596
- tool_name = "audio_transcription_tool"
597
- print(f"πŸ”§ Fallback (Format 2): Inferred 'audio_transcription_tool'")
598
-
599
- elif "code" in parsed_json:
600
- tool_name = "code_interpreter"
601
- print(f"πŸ”§ Fallback (Format 2): Inferred 'code_interpreter'")
602
-
603
- elif "answer" in parsed_json:
604
- tool_name = "final_answer_tool"
605
- print(f"πŸ”§ Fallback (Format 2): Inferred 'final_answer_tool'")
606
-
607
- elif "query" in parsed_json:
608
- tool_name = "search_tool"
609
- print(f"πŸ”§ Fallback (Format 2): Inferred 'search_tool'")
610
-
611
- elif "path" in parsed_json:
612
- tool_name = "read_file"
613
- print(f"πŸ”§ Fallback (Format 2): Inferred 'read_file'")
614
-
615
- else:
616
- print(f"⚠️ Fallback (Format 2): Found JSON but couldn't infer tool name")
617
- print(f" JSON keys: {list(parsed_json.keys())}")
618
-
619
- # ========================================================================
620
- # FINAL VALIDATION: Build and return ToolCall if parsing succeeded
621
- # ========================================================================
622
- if tool_name and tool_input is not None:
623
- if any(t.name == tool_name for t in tools):
624
- tool_call = ToolCall(
625
- name=tool_name,
626
- args=tool_input,
627
- id=str(uuid.uuid4())
628
- )
629
- print(f"βœ… Successfully created tool call: {tool_name}")
630
- return [tool_call]
631
- else:
632
- print(f"❌ Tool '{tool_name}' not found in available tools")
633
- print(f" Available: {[t.name for t in tools]}")
634
-
635
- print("❌ Failed to parse any valid tool call from content")
636
- return []
637
-
638
- # List of all tools
639
- defined_tools = [
640
- search_tool,
641
- code_interpreter,
642
- read_file,
643
- write_file,
644
- list_directory,
645
- audio_transcription_tool,
646
- get_youtube_transcript,
647
- scrape_and_retrieve, # Replaced scrape_web_page
648
- final_answer_tool
649
- ]
650
-
651
-
652
- # --- *** NEW: Reverted AgentState *** ---
653
- class AgentState(TypedDict):
654
- messages: Annotated[List[AnyMessage], add_messages]
655
- turn: int
656
-
657
-
658
- # --- *** NEW: Reverted Conditional Edge Function *** ---
659
- def should_continue(state: AgentState):
660
- """
661
- Decide whether to continue, call tools, or end.
662
- """
663
- last_message = state['messages'][-1]
664
- current_turn = state.get('turn', 0)
665
-
666
- # 1. Check for final_answer_tool
667
- if isinstance(last_message, AIMessage) and last_message.tool_calls:
668
- for tool_call in last_message.tool_calls:
669
- if tool_call.get("name") == "final_answer_tool":
670
- print("--- Condition: final_answer_tool called, ending. ---")
671
- return END
672
-
673
- # 2. Check turn limit
674
- if current_turn >= MAX_TURNS:
675
- print(f"--- Condition: Max turns ({MAX_TURNS}) reached. Ending. ---")
676
- return END
677
-
678
- # 3. Route to tools if tool calls exist
679
- if isinstance(last_message, AIMessage) and last_message.tool_calls:
680
- print("--- Condition: Tools called, routing to tools node. ---")
681
- return "tools"
682
-
683
- # 4. Loop prevention
684
- if len(state['messages']) > 2 and isinstance(last_message, AIMessage) and isinstance(state['messages'][-2], AIMessage):
685
- print(f"--- Condition: Detected 2+ consecutive AI messages (Turn {current_turn}). Ending to prevent loop. ---")
686
- return END
687
-
688
- # 5. Loop back to agent (reasoning/planning step)
689
- print(f"--- Condition: No tool call (Turn {current_turn}). Continuing to agent. ---")
690
- return "agent"
691
-
692
-
693
- # ====================================================
694
- # --- *** NEW: Reverted Basic Agent Class *** ---
695
- class BasicAgent:
696
- def __init__(self):
697
- print("BasicAgent (Single LLM) initializing...")
698
 
699
- # --- *** UPDATED: Check for Google API Key *** ---
700
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
701
- if not GEMINI_API_KEY:
702
- raise ValueError("GEMINI_API_KEY environment variable is not set!")
703
- # --- *** END UPDATE *** ---
704
-
705
- self.tools = defined_tools
 
 
 
 
 
 
 
 
 
706
 
707
- # --- Initialize RAG Components ---
708
- print("Initializing RAG components...")
709
- try:
710
- self.embeddings = HuggingFaceEmbeddings(
711
- model_name="sentence-transformers/all-MiniLM-L6-v2",
712
- model_kwargs={'device': 'cpu'}
713
- )
714
- self.text_splitter = RecursiveCharacterTextSplitter(
715
- chunk_size=1000,
716
- chunk_overlap=200
717
- )
718
- print("βœ… RAG components initialized.")
719
- except Exception as e:
720
- print(f"⚠️ Warning: Could not initialize RAG components. Error: {e}")
721
- self.embeddings = None
722
- self.text_splitter = None
723
 
724
- # Build tool descriptions
725
- tool_desc_list = []
726
- for tool in self.tools:
727
- if tool.args_schema:
728
- schema = tool.args_schema.model_json_schema()
729
- args_desc = []
730
- for prop, details in schema.get('properties', {}).items():
731
- desc = details.get('description', '')
732
- args_desc.append(f" - {prop}: {desc}")
733
- args_str = "\n".join(args_desc)
734
- desc = f"- {tool.name}:\n {tool.description}\n Args:\n{args_str}"
735
- else:
736
- desc = f"- {tool.name}: {tool.description}"
737
- tool_desc_list.append(desc)
738
- tool_descriptions = "\n".join(tool_desc_list)
739
-
740
- # ==================== SYSTEM PROMPT V7 (Simplified) ====================
741
- # This prompt is for a single, powerful agent
742
- self.system_prompt = f"""You are a highly intelligent AI assistant for the GAIA benchmark.
743
- Your goal: Provide the EXACT answer in the EXACT format requested.
744
-
745
- **PROTOCOL:**
746
-
747
- 1. **ANALYZE:** Read the question and history. What is the next logical step?
748
- 2. **ACT:** Call ONE tool to get information or perform a calculation.
749
- 3. **EVALUATE:** Look at the tool's output. Do you have the final answer?
750
- - **If NO:** Go back to Step 1 and decide the *next* step.
751
- - **If YES:** Call final_answer_tool immediately with the answer.
752
-
753
- **CRITICAL RULES:**
754
-
755
- - **TOOL USE:** You MUST use tools to find the answer. Do NOT use your own knowledge.
756
- - **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").
757
- - **JSON FORMAT:** All tool calls MUST be in this exact JSON format:
758
- {{ "name": "tool_name", "arguments": {{"key": "value"}} }}
759
-
760
- **EXAMPLE: CODE INTERPRETER**
761
- {{ "name": "code_interpreter", "arguments": {{"code": "print(1 + 1)"}} }}
762
-
763
- **EXAMPLE: FINAL ANSWER**
764
- {{ "name": "final_answer_tool", "arguments": {{"answer": "28"}} }}
765
-
766
- **TOOLS:**
767
- {tool_descriptions}
768
-
769
- **REMEMBER:** One step at a time. Use tools. Format JSON correctly.
770
- """
771
-
772
- # --- *** UPDATED: Initialize Google Gemini LLM *** ---
773
- print("Initializing Google Gemini LLM...")
774
- try:
775
- # --- Initialize ONE Powerful LLM for all tasks ---
776
- self.planner_llm = ChatGoogleGenerativeAI(
777
- model="gemini-2.0-flash-exp", # ← Same model, different config
778
- google_api_key=GEMINI_API_KEY,
779
- temperature=0,
780
- convert_system_message_to_human=True,
781
- max_output_tokens=1024 # Planner needs less tokens
782
- )
783
- print("βœ… Planner LLM (Gemini 2.0 Flash) initialized.")
784
-
785
- except Exception as e:
786
- print(f"❌ Error initializing Gemini: {e}")
787
- raise
788
- # --- *** END UPDATE *** ---
789
 
790
- # --- Node 1: The Agent ---
791
- def agent_node(state: AgentState):
792
- current_turn = state.get('turn', 0) + 1
793
- print(f"\n{'='*60}")
794
- print(f"AGENT TURN {current_turn}/{MAX_TURNS}")
795
- print('='*60)
796
-
797
- # Note: Max turns is also checked in should_continue, but good to have here
798
- if current_turn > MAX_TURNS:
799
- return {"messages": [SystemMessage(content="Max turns reached.")]}
800
 
801
- max_retries = 3
802
- ai_message = None
803
- for attempt in range(max_retries):
804
- try:
805
- # Call the single, powerful LLM
806
- ai_message = self.llm_with_tools.invoke(state["messages"])
807
- break
808
- except Exception as e:
809
- print(f"⚠️ LLM attempt {attempt+1}/{max_retries} failed: {e}")
810
- if attempt == max_retries - 1:
811
- ai_message = AIMessage(
812
- content=f"Error: LLM failed after {max_retries} attempts: {e}"
813
- )
814
- time.sleep(2 ** attempt)
815
-
816
- # --- Fallback Parsing Logic ---
817
- if not ai_message.tool_calls and isinstance(ai_message.content, str) and ai_message.content.strip():
818
- parsed_tool_calls = parse_tool_call_from_string(ai_message.content, self.tools)
819
- if parsed_tool_calls:
820
- print("πŸ”§ Fallback SUCCESS: Rebuilding tool call(s).")
821
- ai_message.tool_calls = parsed_tool_calls
822
- ai_message.content = "" # Clear the text content
823
- else:
824
- print(f"⚠️ Fallback FAILED: Could not parse any tool call from content:\n{ai_message.content[:200]}...")
825
-
826
- if ai_message.tool_calls:
827
- print(f"πŸ”§ Agent Tool Call: {ai_message.tool_calls[0]['name']}")
828
- else:
829
- print(f"πŸ’­ Agent Reasoning: {ai_message.content[:200]}...")
830
-
831
- return {"messages": [ai_message], "turn": current_turn}
832
 
833
- # --- Tool Node ---
834
- tool_node = ToolNode(self.tools)
835
-
836
- # --- Build Graph ---
837
- print("Building Single-Agent graph...")
838
- graph_builder = StateGraph(AgentState)
839
-
840
- graph_builder.add_node("agent", agent_node)
841
- graph_builder.add_node("tools", tool_node)
842
-
843
- graph_builder.add_edge(START, "agent")
844
-
845
- graph_builder.add_conditional_edges(
846
- "agent",
847
- should_continue, # Use the reverted conditional function
848
- {
849
- "tools": "tools",
850
- "agent": "agent", # For loop prevention
851
- END: END
852
- }
853
- )
854
-
855
- graph_builder.add_edge("tools", "agent") # Loop back to agent
856
-
857
- self.graph = graph_builder.compile()
858
- print("βœ… Single-Agent graph compiled successfully.")
859
-
860
- def __call__(self, question: str) -> str:
861
- print(f"\n--- Starting Agent Run for Question ---")
862
- print(f"Agent received question (first 100 chars): {question[:100]}...")
863
-
864
- # --- Initialize Reverted AgentState (no plan) ---
865
- graph_input = {
866
- "messages": [
867
- SystemMessage(content=self.system_prompt),
868
- HumanMessage(content=question)
869
- ],
870
- "turn": 0
871
- }
872
 
873
- final_answer = "AGENT FAILED TO PRODUCE ANSWER"
874
  try:
875
- config = {"recursion_limit": MAX_TURNS + 5}
876
- for event in self.graph.stream(graph_input, stream_mode="values", config=config):
877
-
878
- if event.get('messages'): # Ensure messages exist
879
- last_message = event["messages"][-1]
880
- else:
881
- continue # Skip if no messages yet
882
-
883
- # Check for final answer extraction
884
- if isinstance(last_message, AIMessage) and last_message.tool_calls:
885
- if last_message.tool_calls[0].get("name") == "final_answer_tool":
886
- final_answer_args = last_message.tool_calls[0].get('args', {})
887
- if 'answer' in final_answer_args:
888
- final_answer = final_answer_args['answer']
889
- print(f"--- Final Answer Captured from tool call: '{final_answer}' ---")
890
- break
891
- else:
892
- print(f"⚠️ Final Answer tool called without 'answer' argument: {final_answer_args}")
893
- final_answer = "ERROR: FINAL_ANSWER_TOOL CALLED WITHOUT ANSWER"
894
- break
895
-
896
- elif isinstance(last_message, ToolMessage):
897
- print(f"Tool Result ({last_message.tool_call_id}): {last_message.content[:500]}...")
898
- elif isinstance(last_message, AIMessage) and not last_message.tool_calls:
899
- print(f"AI Message (Reasoning): {last_message.content[:500]}...")
900
- elif isinstance(last_message, SystemMessage):
901
- print(f"System Message: {last_message.content[:500]}...")
902
-
903
-
904
- # --- Final Answer Cleaning ---
905
- cleaned_answer = str(final_answer).strip()
906
- prefixes_to_remove = ["The answer is:", "Here is the answer:", "Based on the information:", "Final Answer:", "Answer:"]
907
- original_cleaned = cleaned_answer
908
- for prefix in prefixes_to_remove:
909
- if cleaned_answer.lower().startswith(prefix.lower()):
910
- potential_answer = cleaned_answer[len(prefix):].strip()
911
- if potential_answer:
912
- cleaned_answer = potential_answer
913
- break
914
-
915
- cleaned_answer = remove_fences_simple(cleaned_answer)
916
- if cleaned_answer.startswith("`") and cleaned_answer.endswith("`"):
917
- cleaned_answer = cleaned_answer[1:-1].strip()
918
-
919
- print(f"Agent returning final answer (cleaned): '{cleaned_answer}'")
920
- return cleaned_answer
921
-
922
  except Exception as e:
923
- print(f"Error running agent graph: {e}")
924
- tb_str = traceback.format_exc()
925
- print(tb_str)
926
- return f"AGENT GRAPH ERROR: {e}"
927
-
928
-
929
- # ====================================================
930
- # --- Global Agent Instantiation ---
931
-
932
- try:
933
- agent = BasicAgent()
934
- print("βœ… Global BasicAgent instantiated successfully.")
935
- if asr_pipeline is None: print("⚠️ Global ASR Pipeline failed load.")
936
- except Exception as e:
937
- print(f"❌ FATAL: Could not instantiate global agent: {e}")
938
- traceback.print_exc()
939
- agent = None
940
-
941
- # ====================================================
942
- # --- (Original Template Code - Mock Questions Version) ---
943
- def run_and_submit_all( profile: gr.OAuthProfile | None): # Corrected type hint
944
- """
945
- Fetches MOCK questions, runs the BasicAgent on them, simulates submission prep,
946
- and displays the results. DOES NOT SUBMIT.
947
- """
948
- space_id = os.getenv("SPACE_ID")
949
- username = profile.username if profile else "local_test_user"
950
- print(f"User: {username}{'' if profile else ' (dummy)'}")
951
 
952
- # Check if global agent initialized
953
- if not agent:
954
- return "FATAL ERROR: Global agent failed to initialize. Check logs.", None
955
-
956
- print("Using globally instantiated agent.")
957
- agent_code = f"httpsS://huggingface.co/spaces/{space_id}/tree/main" if space_id else "local_run" # Corrected URL
958
- print(f"Agent code URL: {agent_code}")
959
- print("--- USING MOCK QUESTIONS ---")
960
-
961
- # --- MOCK QUESTIONS ---
962
- #
963
- # vvv PASTE YOUR FULL LIST OF 20 MOCK QUESTIONS HERE vvv
964
- #
965
- mock_questions_data = [
966
- {
967
- "task_id": "mock_level1_001",
968
- "question": r"""Here's a fun riddle that I'd like you to try.\n\nAn adventurer exploring an ancient tomb came across a horde of gold coins, all neatly stacked in columns. As he reached to scoop them into his backpack, a mysterious voice filled the room. \"You have fallen for my trap adventurer,\" the voice began, and suddenly the doorway to the chamber was sealed by a heavy rolling disk of stone. The adventurer tried to move the stone disk but was unable to budge the heavy stone. Trapped, he was startled when the voice again spoke. \n\n\"If you solve my riddle, I will reward you with a portion of my riches, but if you are not clever, you will never leave this treasure chamber. Before you are 200 gold coins. I pose a challenge to you, adventurer. Within these stacks of coins, all but 30 are face-up. You must divide the coins into two piles, one is yours, and one is mine. You may place as many coins as you like in either pile. You may flip any coins over, but you may not balance any coins on their edges. For every face-down coin in your pile, you will be rewarded with two gold coins. But be warned, if both piles do not contain the same number of face-down coins, the door will remain sealed for all eternity!\"\n\nThe adventurer smiled, as this would be an easy task. All he had to do was flip over every coin so it was face down, and he would win the entire treasure! As he moved to the columns of coins, however, the light suddenly faded, and he was left in total darkness. The adventurer reached forward and picked up one of the coins, and was shocked when he realized that both sides felt almost the same. Without the light, he was unable to determine which side of the coin was heads and which side was tails. He carefully replaced the coin in its original orientation and tried to think of a way to solve the puzzle. Finally, out of desperation, the adventurer removed 30 coins to create his pile. He then carefully flipped over each coin in his pile, so its orientation was inverted from its original state.\n\n\"I've finished,\" he said, and the lights returned. Looking at the two piles, he noticed that the larger pile contained 14 face-down coins.\n\nWhat was the outcome for the adventurer? If he failed the challenge, please respond with \"The adventurer died.\" Otherwise, please provide the number of coins the adventurer won at the conclusion of the riddle. If the adventurer won any coins, provide your response as the number of coins, with no other text."""
969
- },
970
- {
971
- "task_id": "mock_level1_002",
972
- "question": r"""If you use some of the letters in the given Letter Bank to spell out the sentence "I am a penguin halfway to the moon", which of the remaining unused letters would have to be changed to spell out, "The moon is made of cheese"? Return a comma-separated alphabetized list.\nLetter Bank: {OAMFETIMPECRFSHTDNIWANEPNOFAAIYOOMGUTNAHHLNEHCME}"""
973
- },
974
- {
975
- "task_id": "mock_level1_003",
976
- "question": r"""A data annotator stayed up too late creating test questions to check that a system was working properly and submitted several questions with mathematical errors. On nights when they created 15 test questions, they made 1 error. On nights when they created fewer than 15 questions, they also corrected 3 errors. On nights they created 20 questions, they made 0 errors. On nights when they created 25 or more, they made 4 errors. Over the course of five nights, the worker produced a total of 6 errors. When asked how many nights they created 15 questions, they gave three possible numbers as responses. What are the three numbers, presented in the format x, y, z in ascending order?"""
977
- },
978
- {
979
- "task_id": "mock_level1_004",
980
- "question": r"""Please solve the following crossword:\n\n|1|2|3|4|5|\n|6| | | | |\n|7| | | | |\n|8| | | | |\n|X|9| | | |\n\nI have indicated by numbers where the hints start, so you should replace numbers and spaces by the answers.\nAnd X denotes a black square that isn\u2019t to fill.\n\nACROSS\n- 1 Wooden strips on a bed frame\n- 6 _ Minhaj, Peabody-winning comedian for "Patriot Act"\n- 7 Japanese city of 2.6+ million\n- 8 Stopwatch, e.g.\n- 9 Pain in the neck\n\nDOWN\n- 1 Quick drink of whiskey\n- 2 Eye procedure\n- 3 "Same here," in a three-word phrase\n- 4 Already occupied, as a seat\n- 5 Sarcastically critical commentary. Answer by concatenating the characters you choose to fill the crossword, in row-major order."""
981
- },
982
- {
983
- "task_id": "mock_level1_005",
984
- "question": r"""I wanted to make another batch of cherry melomel. I remember liking the last recipe I tried, but I can't remember it off the top of my head. It was from the Reddit, r/mead. I remember that the user who made it had a really distinct name, I think it was StormBeforeDawn. Could you please look up the recipe for me? I'm not sure if it has been changed, so please make sure that the recipe you review wasn't updated after July 14, 2022. That's the last time I tried the recipe.\n\nWhat I want to know is how many cherries I'm supposed to use. I'm making a 10-gallon batch in two 5-gallon carboys. Please just respond with the integer number of pounds of whole cherries with pits that are supposed to be used for a 10-gallon batch."""
985
- },
986
- {
987
- "task_id": "mock_level1_006",
988
- "question": r"""Verify each of the following ISBN 13 numbers:\n\n1. 9783518188156\n2. 9788476540746\n3. 9788415091004\n4. 9788256014590\n5. 9782046407331\n\nIf any are invalid, correct them by changing the final digit. Then, return the list, comma separated, in the same order as in the question."""
989
- },
990
- {
991
- "task_id": "mock_level1_007",
992
- "question": r"""A porterhouse by any other name is centered around a letter. What does Three Dog Night think about the first natural number that starts with that letter? Give the first line from the lyrics that references it."""
993
- },
994
- {
995
- "task_id": "mock_level1_008",
996
- "question": r"""Bob has genome type Aa, and Linda has genome type Aa. Assuming that a child of theirs also has a child with someone who also has genome type Aa, what is the probability that Bob and Linda's grandchild will have Genome type Aa? Write the answer as a percentage, rounding to the nearest integer if necessary."""
997
- },
998
- {
999
- "task_id": "mock_level1_009",
1000
- "question": r"""An array of candy is set out to choose from including gumballs, candy corn, gumdrops, banana taffy, chocolate chips, and gummy bears. There is one bag of each type of candy. The gumballs come in red, orange, yellow, green, blue, and brown. The candy corn is yellow, white, and orange. The gumdrops are red, green, purple, yellow, and orange. The banana taffy is yellow. The chocolate chips are brown and white. The gummy bears are red, green, yellow, and orange. Five people pass through and each selects one bag. The first selects one with only primary colors. The second selects one with no primary colors. The third selects one with all the primary colors. The fourth selects one that has neither the most nor the least colors of the remaining bags. The fifth selects the one with their favorite color, green. A second bag of the candy the first person chose is added to the remaining bag of candy. Which two candies are in the remaining bag after the addition? Give me them in a comma separated list, in alphabetical order"""
1001
- },
1002
- {
1003
- "task_id": "mock_level1_010",
1004
- "question": r"""In the year 2020, where were koi fish found in the watershed with the id 02040203? Give only the name of the pond, lake, or stream where the fish were found, and not the name of the city or county."""
1005
- },
1006
- {
1007
- "task_id": "mock_level1_011",
1008
- "question": r"""In Sonia Sanchez\u2019s poem \u201cfather\u2019s voice\u201d, what primary colour is evoked by the imagery in the beginning of the tenth stanza? Answer with a capitalized word."""
1009
- },
1010
- {
1011
- "task_id": "mock_level1_012",
1012
- "question": r"""According to Papers with Code, what was the name of the first model to go beyond 70% of accuracy on ImageNet ?"""
1013
- },
1014
- {
1015
- "task_id": "mock_level1_013",
1016
- "question": r"""What is the dimension of the boundary of the tame twindragon rounded to two decimal places?"""
1017
- },
1018
- {
1019
- "task_id": "mock_level1_014",
1020
- "question": r"""In what year was the home village of the subject of British Museum item #Bb,11.118 founded?"""
1021
- },
1022
- {
1023
- "task_id": "mock_level1_015",
1024
- "question": r"""What is the ISSN of the journal that included G. Scott's potato article that mentioned both a fast food restaurant and a Chinese politician in the title in a 2012 issue?"""
1025
- },
1026
- {
1027
- "task_id": "mock_level1_016",
1028
- "question": r"""VNV Nation has a song that shares its title with the nickname of Louis XV. What album was it released with?"""
1029
- },
1030
- {
1031
- "task_id": "mock_level1_017",
1032
- "question": r"""If I combine a Beatle's first name and a type of beer, in what category and year of Nobel Prize do I have a winner? Answer using the format CATEGORY, YEAR."""
1033
- },
1034
- {
1035
- "task_id": "mock_level1_018",
1036
- "question": r"""In the version of NumPy where the numpy.msort function was deprecated, which attribute was added to the numpy.polynomial package's polynomial classes?"""
1037
- },
1038
- {
1039
- "task_id": "mock_level1_019",
1040
- "question": r"""A word meaning dramatic or theatrical forms a species of duck when appended with two letters and then duplicated. What is that word?"""
1041
- },
1042
- {
1043
- "task_id": "mock_level1_020",
1044
- "question": r"""As of August 2023, how many in-text citations on the West African Vodun Wikipedia page reference a source that was cited using Scopus?"""
1045
- }
1046
- ]
1047
 
1048
- questions_data = mock_questions_data
1049
- print(f"Using {len(questions_data)} mock questions.")
1050
-
1051
- results_log, answers_payload = [], []
1052
- print(f"Running agent on {len(questions_data)} mock questions...")
1053
-
1054
- for i, item in enumerate(questions_data):
1055
- task_id, question_text = item.get("task_id"), item.get("question")
1056
- if not task_id or question_text is None: print(f"Skipping mock item {i+1}"); continue
1057
-
1058
- print(f"\n--- Running Mock Task {i+1} (ID: {task_id}) ---")
1059
- try:
1060
- file_path = item.get("file_path")
1061
- question_text_with_context = question_text
1062
- if file_path:
1063
- question_text_with_context = f"{question_text}\n\n[Attached File: {file_path}]"
1064
- print(f"Q includes file: {file_path}")
1065
-
1066
- submitted_answer = agent(question_text_with_context)
1067
- submitted_answer_str = str(submitted_answer) if submitted_answer is not None else ""
1068
- answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer_str})
1069
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer_str})
1070
- print(f"--- Mock Task {task_id} Complete ---")
1071
- except Exception as e:
1072
- print(f"FATAL ERROR on mock task {task_id}: {e}")
1073
- import traceback; traceback.print_exc()
1074
- submitted_answer = f"AGENT CRASH: {e}"
1075
- answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
1076
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
1077
-
1078
- if not answers_payload: return "Agent produced no answers.", pd.DataFrame(results_log)
1079
-
1080
- status_update = f"Finished mock run. Processed {len(answers_payload)} answers for '{username}'."
1081
- print(status_update); print("--- MOCK RUN - SUBMISSION SKIPPED ---")
1082
- final_status = "--- Mock RUN COMPLETE ---\n" + status_update + "\nSubmission SKIPPED." # Corrected typo
1083
- results_df = pd.DataFrame(results_log); results_df['Correct'] = 'N/A (Mock)'
1084
- return final_status, results_df
1085
-
1086
-
1087
- # --- Build Gradio Interface ---
1088
- with gr.Blocks() as demo:
1089
- gr.Markdown("# GAIA Agent - MOCK TEST (Groq Llama3.1)")
1090
- gr.Markdown("""
1091
- **Instructions:** Click 'Run Mock Evaluation'.
1092
- **Notes:** Uses Groq (Llama-3.3-70b Executor). Ensure `GROQ_API_KEY` secret/env var exists. **DOES NOT** fetch official Qs or submit. Check logs for details.
1093
- """)
1094
- gr.LoginButton()
1095
- run_button = gr.Button("Run Mock Evaluation")
1096
- status_output = gr.Textbox(label="Run Status / Mock Result", lines=5, interactive=False)
1097
- results_table = gr.DataFrame(label="Mock Qs, Agent Answers, Results", wrap=True)
1098
- run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
1099
 
1100
  if __name__ == "__main__":
1101
- print("\n" + "-"*30 + " App Starting " + "-"*30)
1102
- space_host_startup = os.getenv("SPACE_ID"); space_id_startup = os.getenv("SPACE_ID") # Corrected variable name
1103
- if space_host_startup: print(f"βœ… SPACE_HOST: {space_host_startup}\n Runtime URL: https://{space_host_startup}.hf.space")
1104
- else: print("ℹ️ No SPACE_HOST (local?).")
1105
- if space_id_startup: print(f"βœ… SPACE_ID: {space_id_startup}\n Repo URL: https://huggingface.co/spaces/{space_id_startup}\n Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
1106
- else: print("ℹ️ No SPACE_ID (local?).")
1107
- try: script_dir = os.path.dirname(os.path.realpath(__file__))
1108
- except NameError: script_dir = os.getcwd()
1109
- print(f"Script directory: {script_dir}")
1110
- print(f"CWD: {os.getcwd()}")
1111
- try: print("Files in CWD:", os.listdir("."))
1112
- except FileNotFoundError: print("Warning: CWD listing failed.")
1113
- print("-"*(60 + len(" App Starting ")) + "\n")
1114
- print("Launching Gradio Interface...")
1115
- demo.queue().launch(debug=True, share=False)
1116
-
1117
-
 
1
  import os
 
 
 
 
2
  import io
 
 
 
 
3
  import json
4
  import re
5
+ import traceback
6
+ import contextlib
7
+ from typing import List, Optional
8
+ from pathlib import Path
9
 
10
+ import gradio as gr
11
+ import pandas as pd
12
+ import torch
13
  from pydantic import BaseModel, Field
14
 
15
+ # Multimodal & Web Tools
16
  from transformers import pipeline
17
  from youtube_transcript_api import YouTubeTranscriptApi
18
  from bs4 import BeautifulSoup
19
+ import requests
20
 
21
+ # LangChain & LangGraph
22
  from langgraph.graph.message import add_messages
23
+ from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
24
  from langgraph.prebuilt import ToolNode
25
  from langgraph.graph import START, END, StateGraph
 
26
  from langchain_core.tools import tool
27
+ from langchain_groq import ChatGroq # <-- Groq integration
28
 
29
+ # RAG
30
  from langchain_text_splitters import RecursiveCharacterTextSplitter
31
  from langchain_community.vectorstores import FAISS
32
  from langchain_community.embeddings import HuggingFaceEmbeddings
33
+ from langchain_community.tools import DuckDuckGoSearchRun
34
 
35
+ # =============================================================================
36
+ # CONFIGURATION
37
+ # =============================================================================
38
+ class Config:
39
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
40
+ MAX_TURNS = 20
41
+ MAX_MESSAGE_LENGTH = 8000
42
+ GROQ_MODEL = "llama-3.3-70b-versatile" # Groq's Llama 70B model
43
+ ASR_MODEL = "openai/whisper-base"
44
+ EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
45
+ CHUNK_SIZE = 1000
46
+ CHUNK_OVERLAP = 200
47
+
48
+ # =============================================================================
49
+ # ASR INITIALIZATION
50
+ # =============================================================================
51
+ class ASRManager:
52
+ """Manages the Automatic Speech Recognition pipeline."""
53
+
54
+ def __init__(self):
55
+ self.pipeline = None
56
+ self._initialize()
57
+
58
+ def _initialize(self):
59
+ """Initialize the ASR pipeline with proper device handling."""
60
+ try:
61
+ print("Initializing ASR (Whisper) pipeline...")
62
+ device = 0 if torch.cuda.is_available() else -1
63
+ device_name = "cuda:0" if device == 0 else "cpu"
64
+ print(f"Using device: {device_name}")
65
+
66
+ self.pipeline = pipeline(
67
+ "automatic-speech-recognition",
68
+ model=Config.ASR_MODEL,
69
+ torch_dtype=torch.float16 if device == 0 else torch.float32,
70
+ device=device
71
+ )
72
+ print("βœ… ASR pipeline loaded successfully")
73
+ except Exception as e:
74
+ print(f"⚠️ Failed to load ASR pipeline: {e}")
75
+ self.pipeline = None
76
+
77
+ def transcribe(self, file_path: str) -> str:
78
+ """Transcribe an audio file."""
79
+ if self.pipeline is None:
80
+ raise RuntimeError("ASR pipeline not available")
81
+
82
+ result = self.pipeline(file_path)
83
+ return result.get("text", "")
84
+
85
+ # Global ASR manager
86
+ asr_manager = ASRManager()
87
+
88
+ # =============================================================================
89
+ # UTILITY FUNCTIONS
90
+ # =============================================================================
91
+ class FileUtils:
92
+ """Utilities for file operations."""
93
+
94
+ @staticmethod
95
+ def find_file(path: str) -> Optional[Path]:
96
+ """Find a file by trying multiple path variations."""
97
+ script_dir = Path.cwd()
98
+ safe_path = Path(path).as_posix()
99
+
100
+ paths_to_try = [
101
+ script_dir / safe_path,
102
+ Path(safe_path),
103
+ script_dir / Path(path).name
104
+ ]
105
+
106
+ for attempt_path in paths_to_try:
107
+ if attempt_path.exists():
108
+ return attempt_path
109
+
110
+ return None
111
+
112
+ @staticmethod
113
+ def truncate_if_needed(content: str, max_length: int = Config.MAX_MESSAGE_LENGTH) -> str:
114
+ """Truncate content if it exceeds max length."""
115
+ if len(content) > max_length:
116
+ return content[:max_length] + f"\n...[truncated, {len(content)} total chars]"
117
+ return content
118
+
119
+ class SecurityValidator:
120
+ """Validates code for security concerns."""
121
+
122
+ DANGEROUS_PATTERNS = ['__import__', 'eval(', 'compile(', 'subprocess', 'os.system']
123
+ WRITE_MODES = ["'w'", '"w"', "'a'", '"a"', "'wb'", '"wb"']
124
+
125
+ @classmethod
126
+ def validate_code(cls, code: str) -> Optional[str]:
127
+ """
128
+ Validate code for security issues.
129
+ Returns error message if dangerous, None if safe.
130
+ """
131
+ code_lower = code.lower()
132
+
133
+ # Check for dangerous operations
134
+ for pattern in cls.DANGEROUS_PATTERNS:
135
+ if pattern in code_lower:
136
+ return f"Potentially dangerous operation '{pattern}' is not allowed"
137
+
138
+ # Check for file writing
139
+ if 'open(' in code_lower and any(mode in code for mode in cls.WRITE_MODES):
140
+ return "Writing files not allowed in code_interpreter. Use write_file tool"
141
+
142
+ return None
143
+
144
+ # =============================================================================
145
+ # TOOL DEFINITIONS
146
+ # =============================================================================
147
+
148
+ # --- Search Tool ---
149
  class SearchInput(BaseModel):
150
  query: str = Field(description="The search query.")
151
 
152
  @tool(args_schema=SearchInput)
153
  def search_tool(query: str) -> str:
154
+ """Search the web using DuckDuckGo for recent information."""
155
+ if not query or not isinstance(query, str):
156
+ return "Error: 'query' must be a non-empty string"
157
 
158
+ print(f"πŸ” Searching: {query}")
159
  try:
160
  search = DuckDuckGoSearchRun()
161
  result = search.run(query)
162
+ return FileUtils.truncate_if_needed(result)
 
 
163
  except Exception as e:
164
+ return f"Search error for '{query}': {str(e)}"
 
165
 
166
+ # --- Code Interpreter Tool ---
167
  class CodeInput(BaseModel):
168
+ code: str = Field(description="Python code to execute (must include print() for output).")
169
 
170
  @tool(args_schema=CodeInput)
171
  def code_interpreter(code: str) -> str:
172
  """
173
+ Execute Python code and return output.
174
+
175
+ RULES:
176
+ 1. ALWAYS use print() for output
177
+ 2. Keep code simple and focused
178
+ 3. Add comments to explain logic
179
+ 4. Import libraries inside functions
180
+ 5. Available: pandas as pd, basic Python libraries
181
  """
182
  if not isinstance(code, str):
183
+ return "Error: 'code' must be a string"
184
 
185
+ # Security validation
186
+ error = SecurityValidator.validate_code(code)
187
+ if error:
188
+ return f"Error: {error}"
 
 
189
 
190
+ print(f"πŸ’» Executing code:\n{code}\n---")
 
191
 
 
192
  output_stream = io.StringIO()
193
  error_stream = io.StringIO()
194
 
195
  try:
196
+ with contextlib.redirect_stdout(output_stream), \
197
+ contextlib.redirect_stderr(error_stream):
198
+
199
  safe_globals = {
200
  "pd": pd,
201
  "__builtins__": __builtins__
202
  }
203
  exec(code, safe_globals, {})
204
+
205
  stdout = output_stream.getvalue()
206
  stderr = error_stream.getvalue()
207
 
208
  if stderr:
209
+ return f"Error:\n{stderr}\n\nOutput:\n{stdout}"
210
 
211
  if stdout:
212
+ return f"Success:\n{FileUtils.truncate_if_needed(stdout)}"
 
 
213
 
214
+ return "Success: Code executed but produced no output.\n⚠️ Use print() to see results!"
215
+
216
+ except Exception:
217
+ return f"Execution failed:\n{traceback.format_exc()}"
 
 
218
 
219
+ # --- File Operations ---
220
  class ReadFileInput(BaseModel):
221
+ path: str = Field(description="Path to the file to read.")
222
 
223
  @tool(args_schema=ReadFileInput)
224
  def read_file(path: str) -> str:
225
+ """Read the content of a file."""
226
+ if not path or not isinstance(path, str):
227
+ return "Error: 'path' must be a non-empty string"
228
+
229
+ print(f"πŸ“– Reading: {path}")
230
 
231
+ file_path = FileUtils.find_file(path)
232
+ if not file_path:
233
+ cwd_files = list(Path.cwd().iterdir())
234
+ return (f"Error: File not found: '{path}'\n"
235
+ f"Files in current directory: {[f.name for f in cwd_files]}")
236
 
237
  try:
238
+ # Try reading as text
239
+ content = file_path.read_text(encoding='utf-8')
240
+ return FileUtils.truncate_if_needed(content)
241
+
242
+ except UnicodeDecodeError:
243
+ # Binary file
244
+ size = file_path.stat().st_size
245
+ ext = file_path.suffix
246
+ return (f"File appears to be binary ({size} bytes). Cannot display as text.\n"
247
+ f"File type: {ext}\n"
248
+ f"Consider using audio_transcription_tool for audio files.")
249
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  except Exception as e:
251
+ return f"Error reading file: {str(e)}"
 
252
 
253
  class WriteFileInput(BaseModel):
254
+ path: str = Field(description="Path to write the file.")
255
+ content: str = Field(description="Content to write.")
256
 
257
  @tool(args_schema=WriteFileInput)
258
  def write_file(path: str, content: str) -> str:
259
+ """Write content to a file."""
260
+ if not path or not isinstance(path, str):
261
+ return "Error: 'path' must be a non-empty string"
262
  if not isinstance(content, str):
263
+ return "Error: 'content' must be a string"
264
 
265
+ print(f"✍️ Writing to: {path}")
266
 
267
  try:
268
+ file_path = Path.cwd() / path
269
+ file_path.parent.mkdir(parents=True, exist_ok=True)
270
+ file_path.write_text(content, encoding='utf-8')
271
+ return f"Successfully wrote {len(content)} characters to '{path}'"
 
 
 
 
 
 
 
 
272
  except Exception as e:
273
+ return f"Error writing file: {str(e)}"
 
274
 
275
  class ListDirInput(BaseModel):
276
+ path: str = Field(description="Directory path to list.", default=".")
277
 
278
  @tool(args_schema=ListDirInput)
279
  def list_directory(path: str = ".") -> str:
280
+ """List the contents of a directory."""
281
+ print(f"πŸ“ Listing: {path}")
 
 
 
282
 
283
  try:
284
+ dir_path = Path.cwd() / path if path != "." else Path.cwd()
 
285
 
286
+ if not dir_path.is_dir():
287
+ return f"Error: '{path}' is not a valid directory"
288
 
289
+ items = sorted(dir_path.iterdir())
290
 
291
  if not items:
292
+ return f"Directory '{path}' is empty"
293
+
294
+ files = []
295
+ directories = []
296
 
297
+ for item in items:
298
+ if item.is_dir():
299
+ directories.append(f"πŸ“ {item.name}/")
 
 
300
  else:
301
+ size = item.stat().st_size
302
+ files.append(f"πŸ“„ {item.name} ({size} bytes)")
303
 
304
  result = f"Contents of '{path}':\n\n"
305
  if directories:
 
308
  result += "Files:\n" + "\n".join(files)
309
 
310
  return result
311
+
312
  except Exception as e:
313
+ return f"Error listing directory: {str(e)}"
 
314
 
315
+ # --- Audio Transcription ---
316
  class AudioInput(BaseModel):
317
+ file_path: str = Field(description="Path to the audio file.")
318
 
319
  @tool(args_schema=AudioInput)
320
  def audio_transcription_tool(file_path: str) -> str:
321
+ """Transcribe an audio file to text using Whisper."""
322
+ if not file_path or not isinstance(file_path, str):
323
+ return "Error: 'file_path' must be a non-empty string"
324
 
325
+ print(f"🎀 Transcribing: {file_path}")
326
 
327
+ audio_path = FileUtils.find_file(file_path)
328
+ if not audio_path:
329
+ return f"Error: Audio file not found: '{file_path}'"
330
 
331
  try:
332
+ text = asr_manager.transcribe(str(audio_path))
333
+ if not text:
334
+ return "Error: Transcription produced no text"
335
+ return f"Transcription:\n{FileUtils.truncate_if_needed(text)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  except Exception as e:
337
+ return f"Error transcribing: {str(e)}"
 
338
 
339
+ # --- YouTube Transcript ---
340
  class YoutubeInput(BaseModel):
341
+ video_url: str = Field(description="YouTube video URL.")
342
 
343
  @tool(args_schema=YoutubeInput)
344
  def get_youtube_transcript(video_url: str) -> str:
345
+ """Fetch transcript/captions for a YouTube video."""
346
+ if not video_url or not isinstance(video_url, str):
347
+ return "Error: 'video_url' must be a non-empty string"
348
 
349
+ print(f"πŸ“Ί Fetching transcript: {video_url}")
350
 
351
  try:
352
+ # Extract video ID
353
  video_id = None
354
  if "watch?v=" in video_url:
355
  video_id = video_url.split("v=")[1].split("&")[0]
 
357
  video_id = video_url.split("youtu.be/")[1].split("?")[0]
358
 
359
  if not video_id:
360
+ return f"Error: Could not extract video ID from '{video_url}'"
361
+
362
  transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
363
 
364
  if not transcript_list:
365
+ return "Error: No transcript found"
 
 
 
 
 
 
 
366
 
367
+ full_transcript = " ".join(item["text"] for item in transcript_list)
368
+ return f"YouTube Transcript:\n{FileUtils.truncate_if_needed(full_transcript)}"
369
+
370
  except Exception as e:
371
+ return f"Error getting transcript: {str(e)}"
372
 
373
+ # --- RAG-Based Web Scraper ---
 
374
  class ScrapeInput(BaseModel):
375
+ url: str = Field(description="URL to scrape (http:// or https://).")
376
+ query: str = Field(description="Question to answer from the page.")
377
 
378
  @tool(args_schema=ScrapeInput)
379
  def scrape_and_retrieve(url: str, query: str) -> str:
380
  """
381
+ Scrape a webpage and use RAG to find relevant information.
 
 
382
 
383
  Args:
384
+ url: The URL to scrape
385
+ query: The specific question to answer
386
  """
387
+ if not url.lower().startswith(('http://', 'https://')):
388
+ return f"Error: Invalid URL. Must start with http:// or https://. Got: '{url}'"
389
  if not query:
390
+ return "Error: A query is required"
391
+
392
+ # Access global agent for RAG components
393
+ if not hasattr(scrape_and_retrieve, 'embeddings'):
394
+ return "Error: RAG components not initialized"
395
+
396
+ print(f"🌐 Scraping: {url} for query: {query}")
397
 
398
  try:
399
+ # Scrape webpage
400
  headers = {
401
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
402
  }
403
  response = requests.get(url, headers=headers, timeout=20)
404
  response.raise_for_status()
405
 
406
  soup = BeautifulSoup(response.text, 'html.parser')
407
+
408
+ # Remove non-content elements
409
  for tag in soup(["script", "style", "nav", "footer", "aside", "header"]):
410
  tag.extract()
411
 
412
+ # Extract main content
413
  main_content = soup.find('main') or soup.find('article') or soup.body
414
  if not main_content:
415
+ return "Error: Could not find main content"
416
 
417
  text = main_content.get_text(separator='\n', strip=True)
418
+ text = '\n'.join(line.strip() for line in text.splitlines() if line.strip())
419
 
420
  if not text:
421
+ return "Error: No content extracted"
422
 
423
+ # Split text into chunks
424
+ docs = scrape_and_retrieve.text_splitter.create_documents([text])
425
  if not docs:
426
+ return "Error: Could not split text into documents"
 
 
 
427
 
428
+ # Create vector store and retrieve
429
+ db = FAISS.from_documents(docs, scrape_and_retrieve.embeddings)
430
+ retriever = db.as_retriever(search_kwargs={"k": 5})
431
  retrieved_docs = retriever.invoke(query)
432
 
433
  if not retrieved_docs:
434
+ return "Error: No relevant information found"
435
+
436
+ # Format results
437
+ context = "\n\n---\n\n".join(doc.page_content for doc in retrieved_docs)
438
+ return f"Relevant Context from {url} for '{query}':\n\n{context}"
439
+
440
  except Exception as e:
441
+ return f"Error scraping {url}: {str(e)}\n{traceback.format_exc()}"
 
 
442
 
443
+ # --- Final Answer Tool ---
444
  class FinalAnswerInput(BaseModel):
445
+ answer: str = Field(description="The final, definitive answer.")
446
 
447
  @tool(args_schema=FinalAnswerInput)
448
  def final_answer_tool(answer: str) -> str:
449
  """
450
+ Call this ONLY when you have the final answer.
451
+ The answer must be EXACTLY what was asked for, with no extra text.
452
  """
453
  if not isinstance(answer, str):
454
+ answer = str(answer)
 
 
 
455
 
456
+ print(f"βœ… FINAL ANSWER: {answer}")
 
457
  return answer
458
 
459
+ # =============================================================================
460
+ # AGENT CLASS
461
+ # =============================================================================
462
+ class GroqAgent:
463
+ """Agent powered by Groq's Llama 70B model."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
 
465
+ def __init__(self, api_key: str):
466
+ self.api_key = api_key
467
+
468
+ # Initialize LLM
469
+ self.llm = ChatGroq(
470
+ api_key=api_key,
471
+ model=Config.GROQ_MODEL,
472
+ temperature=0.1,
473
+ max_tokens=4096
474
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
+ # Initialize RAG components
477
+ self.embeddings = HuggingFaceEmbeddings(
478
+ model_name=Config.EMBEDDING_MODEL
479
+ )
480
+ self.text_splitter = RecursiveCharacterTextSplitter(
481
+ chunk_size=Config.CHUNK_SIZE,
482
+ chunk_overlap=Config.CHUNK_OVERLAP
483
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
+ # Attach RAG components to scraper tool
486
+ scrape_and_retrieve.embeddings = self.embeddings
487
+ scrape_and_retrieve.text_splitter = self.text_splitter
488
+
489
+ # Define tools
490
+ self.tools = [
491
+ search_tool,
492
+ code_interpreter,
493
+ read_file,
494
+ write_file,
495
+ list_directory,
496
+ audio_transcription_tool,
497
+ get_youtube_transcript,
498
+ scrape_and_retrieve,
499
+ final_answer_tool
500
+ ]
501
 
502
+ # Bind tools to LLM
503
+ self.llm_with_tools = self.llm.bind_tools(self.tools)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
+ # Build graph
506
+ self.graph = self._build_graph()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
 
508
+ def _build_graph(self) -> StateGraph:
509
+ """Build the LangGraph state graph."""
510
+ # TODO: Implement graph building logic
511
+ pass
 
 
 
 
 
 
512
 
513
+ def run(self, user_input: str) -> str:
514
+ """Run the agent on user input."""
515
+ # TODO: Implement agent execution logic
516
+ pass
517
+
518
+ # =============================================================================
519
+ # GRADIO INTERFACE
520
+ # =============================================================================
521
+ def create_interface():
522
+ """Create the Gradio interface."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
+ def chat(message, history, groq_api_key):
525
+ if not groq_api_key:
526
+ return "Please provide a Groq API key"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
 
528
  try:
529
+ agent = GroqAgent(api_key=groq_api_key)
530
+ response = agent.run(message)
531
+ return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  except Exception as e:
533
+ return f"Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
+ with gr.Blocks(title="Groq Llama 70B Agent") as demo:
536
+ gr.Markdown("# πŸš€ Groq Llama 70B Agentic Assistant")
537
+ gr.Markdown("Powered by Groq's ultra-fast Llama 70B model")
538
+
539
+ with gr.Row():
540
+ api_key_input = gr.Textbox(
541
+ label="Groq API Key",
542
+ type="password",
543
+ placeholder="Enter your Groq API key..."
544
+ )
545
+
546
+ chatbot = gr.Chatbot(label="Chat", height=500)
547
+ msg = gr.Textbox(
548
+ label="Message",
549
+ placeholder="Ask me anything...",
550
+ lines=2
551
+ )
552
+
553
+ msg.submit(chat, [msg, chatbot, api_key_input], chatbot)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
+ return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
 
557
  if __name__ == "__main__":
558
+ demo = create_interface()
559
+ demo.launch()