Spaces:
Running
Running
# app.py | |
import os | |
import gradio as gr | |
import pandas as pd | |
from bs4 import BeautifulSoup # Keep this if your tools use it | |
import datetime | |
import pytz | |
import math | |
import re | |
import requests | |
from transformers import HfAgent # Your successful import | |
from transformers.tools import Tool # Your successful import | |
from transformers import pipeline # <<< --- MAKE SURE THIS IMPORT IS ADDED / PRESENT | |
import traceback | |
import sys | |
print(f"--- Python version: {sys.version} ---") | |
# print(f"--- Python sys.path (module search paths): {sys.path} ---") # Optional now | |
import transformers | |
from transformers.tools import Tool | |
print(f"--- Expected Transformers Version: 4.36.0 ---") | |
print(f"--- Actual Transformers Version: {transformers.__version__} ---") | |
# print(f"--- Transformers module loaded from: {transformers.__file__} ---") # Optional now | |
# print(f"--- Attributes of 'transformers' module (dir(transformers)): {dir(transformers)} ---") # Optional now | |
try: | |
from transformers import HfAgent # <<< --- THE CORRECT IMPORT! | |
print("--- Successfully imported HfAgent directly from transformers! ---") | |
except ImportError as e: | |
print(f"--- FAILED to import HfAgent directly from transformers: {e} ---") | |
# This should ideally not happen now | |
raise | |
except Exception as e_gen: | |
print(f"--- Some other UNEXPECTED error during HfAgent import: {e_gen} ---") | |
raise | |
print("--- If no errors above, imports were successful. Proceeding with rest of app. ---") | |
# (Keep Constants as is) | |
# --- Constants --- | |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space" | |
# --- Tool Definitions --- | |
def get_current_time_in_timezone(timezone: str) -> str: | |
"""Fetches the current local time in a specified IANA timezone (e.g., 'America/New_York', 'Europe/London', 'UTC'). | |
Args: | |
timezone (str): A string representing a valid IANA timezone name. | |
""" | |
print(f"--- Tool: Executing get_current_time_in_timezone for: {timezone} ---") | |
try: | |
tz = pytz.timezone(timezone) | |
# Added %Z (timezone name) and %z (UTC offset) | |
local_time = datetime.datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S %Z%z") | |
return f"The current local time in {timezone} is: {local_time}" | |
except pytz.exceptions.UnknownTimeZoneError: | |
print(f"Error: Unknown timezone '{timezone}'") | |
return f"Error: Unknown timezone '{timezone}'. Please use a valid IANA timezone name (e.g., 'America/Denver', 'UTC')." | |
except Exception as e: | |
print(f"Error fetching time for timezone '{timezone}': {str(e)}") | |
return f"Error fetching time for timezone '{timezone}': {str(e)}" | |
def web_search(query: str) -> str: | |
""" | |
Performs a web search using DuckDuckGo (via HTML scraping) and returns the text content of the top result snippets. | |
Use this tool to find up-to-date information about events, facts, or topics when the answer isn't already known. | |
Args: | |
query (str): The search query string. | |
Returns: | |
str: A string containing the summarized search results (titles and snippets of top hits), or an error message if the search fails. | |
""" | |
print(f"--- Tool: Executing web_search with query: {query} ---") | |
try: | |
search_url = "https://html.duckduckgo.com/html/" | |
params = {"q": query} | |
headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36'} # Common user agent | |
response = requests.post(search_url, data=params, headers=headers, timeout=15) # Increased timeout | |
response.raise_for_status() # Check for HTTP errors (4xx or 5xx) | |
soup = BeautifulSoup(response.text, 'html.parser') | |
results = soup.find_all('div', class_='result__body') # Find result containers | |
snippets = [] | |
for i, result in enumerate(results[:3]): # Get top 3 results for brevity | |
title_tag = result.find('a', class_='result__a') | |
snippet_tag = result.find('a', class_='result__snippet') | |
title = title_tag.get_text(strip=True) if title_tag else "No Title" | |
snippet = snippet_tag.get_text(strip=True) if snippet_tag else "No Snippet" | |
if snippet != "No Snippet": # Only include results with a snippet | |
snippets.append(f"Result {i+1}: {title} - {snippet}") | |
if not snippets: | |
return "No search results with relevant snippets found." | |
return "\n".join(snippets) | |
except requests.exceptions.Timeout: | |
print(f"Error during web search request: Timeout") | |
return "Error: The web search request timed out." | |
except requests.exceptions.RequestException as e: | |
print(f"Error during web search request: {e}") | |
return f"Error: Could not perform web search. Network issue: {e}" | |
except Exception as e: | |
print(f"Error processing web search results: {e}") | |
return f"Error: Could not process search results. {e}" | |
def safe_calculator(expression: str) -> str: | |
""" | |
Evaluates a simple mathematical expression involving numbers, +, -, *, /, %, parentheses, and the math functions: sqrt, pow. | |
Use this tool *only* for calculations. Do not use it to run other code. | |
Args: | |
expression (str): The mathematical expression string (e.g., "(2 + 3) * 4", "pow(2, 5)", "sqrt(16)"). | |
Returns: | |
str: The numerical result of the calculation or a descriptive error message if the expression is invalid or unsafe. | |
""" | |
print(f"--- Tool: Executing safe_calculator with expression: {expression} ---") | |
try: | |
# Basic check for allowed characters/patterns first | |
# Allows numbers (including scientific notation), operators, parentheses, whitespace, and known function names | |
pattern = r"^[0-9eE\.\+\-\*\/\%\(\)\s]*(sqrt|pow)?[0-9eE\.\+\-\*\/\%\(\)\s\,]*$" | |
if not re.match(pattern, expression): | |
# Fallback simple pattern check (less precise) | |
allowed_chars_pattern = r"^[0-9eE\.\+\-\*\/\%\(\)\s\,sqrtpow]+$" | |
if not re.match(allowed_chars_pattern, expression): | |
raise ValueError(f"Expression '{expression}' contains disallowed characters.") | |
# Define allowed functions/names for eval's context | |
allowed_names = { | |
"sqrt": math.sqrt, | |
"pow": math.pow, | |
# Add other safe math functions if needed e.g. "log": math.log | |
} | |
# Evaluate the expression in a restricted environment | |
# Limited builtins, only allowed names are accessible. | |
result = eval(expression, {"__builtins__": {}}, allowed_names) | |
# Ensure the result is a number before converting to string | |
if not isinstance(result, (int, float)): | |
raise ValueError("Calculation did not produce a numerical result.") | |
return str(result) | |
except Exception as e: | |
# Catch potential errors during eval (SyntaxError, NameError, TypeError etc.) or from the checks | |
print(f"Error during calculation for '{expression}': {e}") | |
return f"Error calculating '{expression}': Invalid expression or calculation error ({e})." | |
# --- Basic Agent Definition --- | |
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------ | |
# --- Agent Definition using HfAgent --- | |
class HfAgentWrapper: | |
def __init__(self): | |
print("Initializing HfAgentWrapper...") | |
model_id_or_path = "bigcode/starcoderbase-1b" # A model compatible with transformers v4.36.0 | |
try: | |
print(f"Strategy: Pre-creating pipeline for model: {model_id_or_path}") | |
hf_auth_token = os.getenv("HF_TOKEN") # Secret should be named HF_TOKEN | |
if not hf_auth_token: | |
print("WARNING: HF_TOKEN secret not found. This may fail if model requires token.") | |
# Starcoderbase is gated, so this is needed. | |
raise ValueError("HF_TOKEN secret is missing and is required for this model.") | |
else: | |
print(f"HF_TOKEN secret found (length: {len(hf_auth_token)}).") | |
# --- Step 1: Create the pipeline object FIRST --- | |
# This allows us to handle errors from pipeline creation directly. | |
llm_pipeline = pipeline( | |
task="text-generation", | |
model=model_id_or_path, | |
token=hf_auth_token | |
# trust_remote_code=True # Not generally needed for starcoder with this version | |
) | |
print("Successfully created LLM pipeline object.") | |
# --- Step 2: Ensure your tools are created WITH proper names --- | |
if not get_current_time_in_timezone.__doc__: raise ValueError("Tool 'get_current_time_in_timezone' is missing a docstring.") | |
if not web_search.__doc__: raise ValueError("Tool 'web_search' is missing a docstring.") | |
if not safe_calculator.__doc__: raise ValueError("Tool 'safe_calculator' is missing a docstring.") | |
time_tool_obj = Tool( | |
name=get_current_time_in_timezone.__name__, # Use the function's name | |
func=get_current_time_in_timezone, | |
description=get_current_time_in_timezone.__doc__ | |
) | |
search_tool_obj = Tool( | |
name=web_search.__name__, # Use the function's name | |
func=web_search, | |
description=web_search.__doc__ | |
) | |
calculator_tool_obj = Tool( | |
name=safe_calculator.__name__, # Use the function's name | |
func=safe_calculator, | |
description=safe_calculator.__doc__ | |
) | |
self.actual_tools_for_agent = [time_tool_obj, search_tool_obj, calculator_tool_obj] | |
print(f"Prepared Tool objects with names: {[tool.name for tool in self.actual_tools_for_agent]}") | |
# --- Step 3: Pass the PRE-INITIALIZED pipeline object to HfAgent --- | |
print("Initializing HfAgent with the pre-created pipeline...") | |
self.agent = HfAgent( | |
llm_pipeline, # Pass the pipeline object directly as the first argument | |
additional_tools=self.actual_tools_for_agent | |
) | |
print("HfAgent successfully instantiated with pre-initialized pipeline.") | |
except Exception as e: | |
print(f"CRITICAL ERROR: Failed to initialize HfAgent or Pipeline: {e}") | |
print("Full traceback of HfAgent/Pipeline initialization error:") | |
traceback.print_exc() | |
raise RuntimeError(f"HfAgent/Pipeline initialization failed: {e}") from e | |
# The __call__ method remains the same | |
def __call__(self, question: str) -> str: | |
print(f"\n--- HfAgentWrapper received question (first 100 chars): {question[:100]}... ---") | |
try: | |
answer = self.agent.run(question) | |
print(f"--- HfAgentWrapper generated answer (first 100 chars): {str(answer)[:100]}... ---") | |
return str(answer) | |
except Exception as e: | |
print(f"ERROR: HfAgent execution failed for question '{question[:50]}...': {e}") | |
print("Full traceback of HfAgent execution error:") | |
traceback.print_exc() | |
return f"Agent Error: Failed to process the question. Details: {e}" | |
def run_and_submit_all( profile: gr.OAuthProfile | None): | |
""" | |
Fetches all questions, runs the BasicAgent on them, submits all answers, | |
and displays the results. | |
""" | |
# --- Determine HF Space Runtime URL and Repo URL --- | |
space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code | |
if profile: | |
username= f"{profile.username}" | |
print(f"User logged in: {username}") | |
else: | |
print("User not logged in.") | |
return "Please Login to Hugging Face with the button.", None | |
api_url = DEFAULT_API_URL | |
questions_url = f"{api_url}/questions" | |
submit_url = f"{api_url}/submit" | |
# 1. Instantiate Agent ( modify this part to create your agent) | |
try: | |
agent = HfAgentWrapper() | |
except Exception as e: | |
print(f"Error instantiating agent: {e}") | |
return f"Error initializing agent: {e}", None | |
# In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public) | |
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" | |
print(agent_code) | |
# 2. Fetch Questions | |
print(f"Fetching questions from: {questions_url}") | |
try: | |
response = requests.get(questions_url, timeout=15) | |
response.raise_for_status() | |
questions_data = response.json() | |
if not questions_data: | |
print("Fetched questions list is empty.") | |
return "Fetched questions list is empty or invalid format.", None | |
print(f"Fetched {len(questions_data)} questions.") | |
except requests.exceptions.RequestException as e: | |
print(f"Error fetching questions: {e}") | |
return f"Error fetching questions: {e}", None | |
except requests.exceptions.JSONDecodeError as e: | |
print(f"Error decoding JSON response from questions endpoint: {e}") | |
print(f"Response text: {response.text[:500]}") | |
return f"Error decoding server response for questions: {e}", None | |
except Exception as e: | |
print(f"An unexpected error occurred fetching questions: {e}") | |
return f"An unexpected error occurred fetching questions: {e}", None | |
# 3. Run your Agent | |
results_log = [] | |
answers_payload = [] | |
print(f"Running agent on {len(questions_data)} questions...") | |
for item in questions_data: | |
task_id = item.get("task_id") | |
question_text = item.get("question") | |
if not task_id or question_text is None: | |
print(f"Skipping item with missing task_id or question: {item}") | |
continue | |
try: | |
submitted_answer = agent(question_text) | |
answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer}) | |
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer}) | |
except Exception as e: | |
print(f"Error running agent on task {task_id}: {e}") | |
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"}) | |
if not answers_payload: | |
print("Agent did not produce any answers to submit.") | |
return "Agent did not produce any answers to submit.", pd.DataFrame(results_log) | |
# 4. Prepare Submission | |
submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload} | |
status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..." | |
print(status_update) | |
# 5. Submit | |
print(f"Submitting {len(answers_payload)} answers to: {submit_url}") | |
try: | |
response = requests.post(submit_url, json=submission_data, timeout=60) | |
response.raise_for_status() | |
result_data = response.json() | |
final_status = ( | |
f"Submission Successful!\n" | |
f"User: {result_data.get('username')}\n" | |
f"Overall Score: {result_data.get('score', 'N/A')}% " | |
f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n" | |
f"Message: {result_data.get('message', 'No message received.')}" | |
) | |
print("Submission successful.") | |
results_df = pd.DataFrame(results_log) | |
return final_status, results_df | |
except requests.exceptions.HTTPError as e: | |
error_detail = f"Server responded with status {e.response.status_code}." | |
try: | |
error_json = e.response.json() | |
error_detail += f" Detail: {error_json.get('detail', e.response.text)}" | |
except requests.exceptions.JSONDecodeError: | |
error_detail += f" Response: {e.response.text[:500]}" | |
status_message = f"Submission Failed: {error_detail}" | |
print(status_message) | |
results_df = pd.DataFrame(results_log) | |
return status_message, results_df | |
except requests.exceptions.Timeout: | |
status_message = "Submission Failed: The request timed out." | |
print(status_message) | |
results_df = pd.DataFrame(results_log) | |
return status_message, results_df | |
except requests.exceptions.RequestException as e: | |
status_message = f"Submission Failed: Network error - {e}" | |
print(status_message) | |
results_df = pd.DataFrame(results_log) | |
return status_message, results_df | |
except Exception as e: | |
status_message = f"An unexpected error occurred during submission: {e}" | |
print(status_message) | |
results_df = pd.DataFrame(results_log) | |
return status_message, results_df | |
# --- Build Gradio Interface using Blocks --- | |
with gr.Blocks() as demo: | |
gr.Markdown("# Basic Agent Evaluation Runner") | |
gr.Markdown( | |
""" | |
**Instructions:** | |
1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ... | |
2. Log in to your Hugging Face account using the button below. This uses your HF username for submission. | |
3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score. | |
--- | |
**Disclaimers:** | |
Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions). | |
This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async. | |
""" | |
) | |
gr.LoginButton() | |
run_button = gr.Button("Run Evaluation & Submit All Answers") | |
status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False) | |
# Removed max_rows=10 from DataFrame constructor | |
results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True) | |
run_button.click( | |
fn=run_and_submit_all, | |
outputs=[status_output, results_table] | |
) | |
if __name__ == "__main__": | |
print("\n" + "-"*30 + " App Starting " + "-"*30) | |
# Check for SPACE_HOST and SPACE_ID at startup for information | |
space_host_startup = os.getenv("SPACE_HOST") | |
space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup | |
if space_host_startup: | |
print(f"✅ SPACE_HOST found: {space_host_startup}") | |
print(f" Runtime URL should be: https://{space_host_startup}.hf.space") | |
else: | |
print("ℹ️ SPACE_HOST environment variable not found (running locally?).") | |
if space_id_startup: # Print repo URLs if SPACE_ID is found | |
print(f"✅ SPACE_ID found: {space_id_startup}") | |
print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}") | |
print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main") | |
else: | |
print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.") | |
print("-"*(60 + len(" App Starting ")) + "\n") | |
print("Launching Gradio Interface for Basic Agent Evaluation...") | |
demo.launch(debug=True, share=False) |