Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
| 14 |
-
import
|
| 15 |
-
import
|
|
|
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
from pydantic import BaseModel, Field
|
| 19 |
|
| 20 |
-
#
|
| 21 |
from transformers import pipeline
|
| 22 |
from youtube_transcript_api import YouTubeTranscriptApi
|
| 23 |
from bs4 import BeautifulSoup
|
|
|
|
| 24 |
|
| 25 |
-
#
|
| 26 |
from langgraph.graph.message import add_messages
|
| 27 |
-
from langchain_core.messages import
|
| 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
|
| 33 |
|
| 34 |
-
#
|
| 35 |
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 36 |
from langchain_community.vectorstores import FAISS
|
| 37 |
from langchain_community.embeddings import HuggingFaceEmbeddings
|
|
|
|
| 38 |
|
| 39 |
-
#
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 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 |
-
"""
|
| 74 |
-
if not
|
| 75 |
-
return "Error:
|
| 76 |
|
| 77 |
-
print(f"
|
| 78 |
try:
|
| 79 |
search = DuckDuckGoSearchRun()
|
| 80 |
result = search.run(query)
|
| 81 |
-
|
| 82 |
-
result = result[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(result)} total chars]"
|
| 83 |
-
return result
|
| 84 |
except Exception as e:
|
| 85 |
-
return f"
|
| 86 |
-
|
| 87 |
|
|
|
|
| 88 |
class CodeInput(BaseModel):
|
| 89 |
-
code: str = Field(description="
|
| 90 |
|
| 91 |
@tool(args_schema=CodeInput)
|
| 92 |
def code_interpreter(code: str) -> str:
|
| 93 |
"""
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
1. ALWAYS use print()
|
| 98 |
-
2.
|
| 99 |
-
3. Add comments
|
| 100 |
-
4.
|
| 101 |
-
Available: pandas as pd, basic Python libraries
|
| 102 |
"""
|
| 103 |
if not isinstance(code, str):
|
| 104 |
-
return "Error:
|
| 105 |
|
| 106 |
-
#
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
if pattern in code_lower:
|
| 111 |
-
return f"Error: Potentially dangerous operation '{pattern}' is not allowed."
|
| 112 |
|
| 113 |
-
|
| 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),
|
|
|
|
|
|
|
| 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
|
| 133 |
|
| 134 |
if stdout:
|
| 135 |
-
|
| 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
|
| 140 |
-
|
| 141 |
-
except Exception
|
| 142 |
-
|
| 143 |
-
return f"Execution failed:\n{tb_str}"
|
| 144 |
-
|
| 145 |
|
|
|
|
| 146 |
class ReadFileInput(BaseModel):
|
| 147 |
-
path: str = Field(description="
|
| 148 |
|
| 149 |
@tool(args_schema=ReadFileInput)
|
| 150 |
def read_file(path: str) -> str:
|
| 151 |
-
"""
|
| 152 |
-
if not
|
| 153 |
-
return "Error:
|
|
|
|
|
|
|
| 154 |
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
try:
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 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"
|
| 201 |
-
|
| 202 |
|
| 203 |
class WriteFileInput(BaseModel):
|
| 204 |
-
path: str = Field(description="
|
| 205 |
-
content: str = Field(description="
|
| 206 |
|
| 207 |
@tool(args_schema=WriteFileInput)
|
| 208 |
def write_file(path: str, content: str) -> str:
|
| 209 |
-
"""
|
| 210 |
-
if not
|
| 211 |
-
return "Error:
|
| 212 |
if not isinstance(content, str):
|
| 213 |
-
return "Error:
|
| 214 |
|
| 215 |
-
print(f"
|
| 216 |
|
| 217 |
try:
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 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
|
| 232 |
-
|
| 233 |
|
| 234 |
class ListDirInput(BaseModel):
|
| 235 |
-
path: str = Field(description="
|
| 236 |
|
| 237 |
@tool(args_schema=ListDirInput)
|
| 238 |
def list_directory(path: str = ".") -> str:
|
| 239 |
-
"""
|
| 240 |
-
|
| 241 |
-
return "Error: Invalid input. 'path' must be a string."
|
| 242 |
-
|
| 243 |
-
print(f"--- Calling List Directory Tool: {path} ---")
|
| 244 |
|
| 245 |
try:
|
| 246 |
-
|
| 247 |
-
full_path = os.path.join(base_dir, path) if path != "." else base_dir
|
| 248 |
|
| 249 |
-
if not
|
| 250 |
-
return f"Error: '{path}' is not a valid directory
|
| 251 |
|
| 252 |
-
items =
|
| 253 |
|
| 254 |
if not items:
|
| 255 |
-
return f"Directory '{path}' is empty
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
if os.path.isdir(item_path):
|
| 261 |
-
directories.append(f"π {item}/")
|
| 262 |
else:
|
| 263 |
-
size =
|
| 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
|
| 276 |
-
|
| 277 |
|
|
|
|
| 278 |
class AudioInput(BaseModel):
|
| 279 |
-
file_path: str = Field(description="
|
| 280 |
|
| 281 |
@tool(args_schema=AudioInput)
|
| 282 |
def audio_transcription_tool(file_path: str) -> str:
|
| 283 |
-
"""
|
| 284 |
-
if not
|
| 285 |
-
return "Error:
|
| 286 |
|
| 287 |
-
print(f"
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
|
|
|
| 291 |
|
| 292 |
try:
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 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
|
| 325 |
-
|
| 326 |
|
|
|
|
| 327 |
class YoutubeInput(BaseModel):
|
| 328 |
-
video_url: str = Field(description="
|
| 329 |
|
| 330 |
@tool(args_schema=YoutubeInput)
|
| 331 |
def get_youtube_transcript(video_url: str) -> str:
|
| 332 |
-
"""
|
| 333 |
-
if not
|
| 334 |
-
return "Error:
|
| 335 |
|
| 336 |
-
print(f"
|
| 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
|
| 347 |
-
|
| 348 |
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
|
| 349 |
|
| 350 |
if not transcript_list:
|
| 351 |
-
return "Error: No transcript found
|
| 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
|
| 362 |
|
| 363 |
-
|
| 364 |
-
# --- NEW RAG-BASED SCRAPER TOOL ---
|
| 365 |
class ScrapeInput(BaseModel):
|
| 366 |
-
url: str = Field(description="
|
| 367 |
-
query: str = Field(description="
|
| 368 |
|
| 369 |
@tool(args_schema=ScrapeInput)
|
| 370 |
def scrape_and_retrieve(url: str, query: str) -> str:
|
| 371 |
"""
|
| 372 |
-
|
| 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
|
| 378 |
-
query
|
| 379 |
"""
|
| 380 |
-
if not
|
| 381 |
-
return "Error: Invalid URL. Must start with http:// or https://. Got: '{url}'"
|
| 382 |
if not query:
|
| 383 |
-
return "Error: A query is required
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
| 388 |
|
| 389 |
try:
|
| 390 |
-
#
|
| 391 |
headers = {
|
| 392 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/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
|
| 404 |
|
| 405 |
text = main_content.get_text(separator='\n', strip=True)
|
| 406 |
-
text = '\n'.join(
|
| 407 |
|
| 408 |
if not text:
|
| 409 |
-
return "Error:
|
| 410 |
|
| 411 |
-
#
|
| 412 |
-
docs =
|
| 413 |
if not docs:
|
| 414 |
-
return "Error:
|
| 415 |
-
|
| 416 |
-
# 3. Embed & Create Vector Store
|
| 417 |
-
db = FAISS.from_documents(docs, agent.embeddings)
|
| 418 |
|
| 419 |
-
#
|
| 420 |
-
|
|
|
|
| 421 |
retrieved_docs = retriever.invoke(query)
|
| 422 |
|
| 423 |
if not retrieved_docs:
|
| 424 |
-
return "Error: No relevant information found
|
| 425 |
-
|
| 426 |
-
#
|
| 427 |
-
context = "\n\n---\n\n".join(
|
| 428 |
-
return f"Relevant Context from {url} for
|
| 429 |
-
|
| 430 |
except Exception as e:
|
| 431 |
-
|
| 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
|
| 437 |
|
| 438 |
@tool(args_schema=FinalAnswerInput)
|
| 439 |
def final_answer_tool(answer: str) -> str:
|
| 440 |
"""
|
| 441 |
-
Call this
|
| 442 |
-
The
|
| 443 |
"""
|
| 444 |
if not isinstance(answer, str):
|
| 445 |
-
|
| 446 |
-
answer = str(answer)
|
| 447 |
-
except:
|
| 448 |
-
return "Error: Invalid input. 'answer' must be a string."
|
| 449 |
|
| 450 |
-
print(f"
|
| 451 |
-
print(f"Answer: {answer}")
|
| 452 |
return answer
|
| 453 |
|
| 454 |
-
|
| 455 |
-
#
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 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 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 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 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 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 |
-
#
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
#
|
| 704 |
-
|
| 705 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
|
| 707 |
-
#
|
| 708 |
-
|
| 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
|
| 725 |
-
|
| 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 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 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 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 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 |
-
|
| 834 |
-
|
| 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 |
-
|
| 876 |
-
|
| 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 |
-
|
| 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 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1102 |
-
|
| 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|