Upload 16 files
#592
by ansh62949 - opened
- hf_agent_course_final_assignment/.gitattributes +35 -0
- hf_agent_course_final_assignment/.gitignore +10 -0
- hf_agent_course_final_assignment/README.md +27 -0
- hf_agent_course_final_assignment/app.py +192 -0
- hf_agent_course_final_assignment/audio_agent.py +222 -0
- hf_agent_course_final_assignment/evaluation_questions.json +122 -0
- hf_agent_course_final_assignment/excel_agent.py +211 -0
- hf_agent_course_final_assignment/gaia_agent.py +334 -0
- hf_agent_course_final_assignment/image_agent.py +183 -0
- hf_agent_course_final_assignment/packages.txt +1 -0
- hf_agent_course_final_assignment/python_agent.py +52 -0
- hf_agent_course_final_assignment/requirements.txt +29 -0
- hf_agent_course_final_assignment/test_agents.py +93 -0
- hf_agent_course_final_assignment/utils/fetch_files.py +37 -0
- hf_agent_course_final_assignment/web_agent.py +180 -0
- hf_agent_course_final_assignment/youtube_agent.py +216 -0
hf_agent_course_final_assignment/.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
hf_agent_course_final_assignment/.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# enviroment files
|
| 2 |
+
.env
|
| 3 |
+
*.jpg
|
| 4 |
+
*.png
|
| 5 |
+
*.mp3
|
| 6 |
+
*.xlsx
|
| 7 |
+
f918266a-b3e0-4914-865d-4faa564f1aef.py
|
| 8 |
+
|
| 9 |
+
# pycache
|
| 10 |
+
__pycache__/
|
hf_agent_course_final_assignment/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Agent Course Final Assignment
|
| 3 |
+
emoji: 🕵🏻♂️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.25.2
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
hf_oauth: true
|
| 11 |
+
# optional, default duration is 8 hours/480 minutes. Max duration is 30 days/43200 minutes.
|
| 12 |
+
hf_oauth_expiration_minutes: 480
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 16 |
+
|
| 17 |
+
## Question analysis
|
| 18 |
+
|
| 19 |
+
We need following functionalities in our agent:
|
| 20 |
+
- [x] General orchestrator agent: base on the question, recognize what tools are needed to answer the question, call the tools, collect the results, and generate the final answer.
|
| 21 |
+
- [x] youtube video analysis
|
| 22 |
+
- [x] excel manipulation
|
| 23 |
+
- [x] web search (duckduckgo react agent)
|
| 24 |
+
- [x] within web search, we need also to search published papers
|
| 25 |
+
- [x] python exection (done, without llm, only naive code execution)
|
| 26 |
+
- [x] audio analysis (api calling inference client of mp3 transcript + chat model)
|
| 27 |
+
- [x] image analysis (api calling inference client of vlm model)
|
hf_agent_course_final_assignment/app.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import requests
|
| 4 |
+
import inspect
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from gaia_agent import GaiaAgent
|
| 7 |
+
from utils.fetch_files import fetch_files_gaia_validation_hub
|
| 8 |
+
# (Keep Constants as is)
|
| 9 |
+
# --- Constants ---
|
| 10 |
+
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 11 |
+
|
| 12 |
+
# --- Basic Agent Definition ---
|
| 13 |
+
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
|
| 14 |
+
|
| 15 |
+
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
| 16 |
+
"""
|
| 17 |
+
Fetches all questions, runs the BasicAgent on them, submits all answers,
|
| 18 |
+
and displays the results.
|
| 19 |
+
"""
|
| 20 |
+
# --- Determine HF Space Runtime URL and Repo URL ---
|
| 21 |
+
space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
|
| 22 |
+
|
| 23 |
+
if profile:
|
| 24 |
+
username= f"{profile.username}"
|
| 25 |
+
print(f"User logged in: {username}")
|
| 26 |
+
else:
|
| 27 |
+
print("User not logged in.")
|
| 28 |
+
return "Please Login to Hugging Face with the button.", None
|
| 29 |
+
|
| 30 |
+
api_url = DEFAULT_API_URL
|
| 31 |
+
questions_url = f"{api_url}/questions"
|
| 32 |
+
submit_url = f"{api_url}/submit"
|
| 33 |
+
|
| 34 |
+
# 1. Instantiate Agent ( modify this part to create your agent)
|
| 35 |
+
try:
|
| 36 |
+
agent = GaiaAgent()
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"Error instantiating agent: {e}")
|
| 39 |
+
return f"Error initializing agent: {e}", None
|
| 40 |
+
# 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)
|
| 41 |
+
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
|
| 42 |
+
print(agent_code)
|
| 43 |
+
|
| 44 |
+
# 2. Fetch Questions
|
| 45 |
+
print(f"Fetching questions from: {questions_url}")
|
| 46 |
+
try:
|
| 47 |
+
response = requests.get(questions_url, timeout=15)
|
| 48 |
+
response.raise_for_status()
|
| 49 |
+
questions_data = response.json()
|
| 50 |
+
if not questions_data:
|
| 51 |
+
print("Fetched questions list is empty.")
|
| 52 |
+
return "Fetched questions list is empty or invalid format.", None
|
| 53 |
+
print(f"Fetched {len(questions_data)} questions.")
|
| 54 |
+
except requests.exceptions.RequestException as e:
|
| 55 |
+
print(f"Error fetching questions: {e}")
|
| 56 |
+
return f"Error fetching questions: {e}", None
|
| 57 |
+
except requests.exceptions.JSONDecodeError as e:
|
| 58 |
+
print(f"Error decoding JSON response from questions endpoint: {e}")
|
| 59 |
+
print(f"Response text: {response.text[:500]}")
|
| 60 |
+
return f"Error decoding server response for questions: {e}", None
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"An unexpected error occurred fetching questions: {e}")
|
| 63 |
+
return f"An unexpected error occurred fetching questions: {e}", None
|
| 64 |
+
|
| 65 |
+
# 3. Run your Agent
|
| 66 |
+
results_log = []
|
| 67 |
+
answers_payload = []
|
| 68 |
+
print(f"Running agent on {len(questions_data)} questions...")
|
| 69 |
+
for item in questions_data:
|
| 70 |
+
task_id = item.get("task_id")
|
| 71 |
+
question_text = item.get("question")
|
| 72 |
+
file_name = item.get("file_name")
|
| 73 |
+
if file_name != "":
|
| 74 |
+
file_path = fetch_files_gaia_validation_hub(file_name)
|
| 75 |
+
else:
|
| 76 |
+
file_path = ""
|
| 77 |
+
if not task_id or question_text is None:
|
| 78 |
+
print(f"Skipping item with missing task_id or question: {item}")
|
| 79 |
+
continue
|
| 80 |
+
try:
|
| 81 |
+
submitted_answer = agent(question_text, file_path)
|
| 82 |
+
answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
|
| 83 |
+
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f"Error running agent on task {task_id}: {e}")
|
| 86 |
+
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
|
| 87 |
+
|
| 88 |
+
if not answers_payload:
|
| 89 |
+
print("Agent did not produce any answers to submit.")
|
| 90 |
+
return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
|
| 91 |
+
|
| 92 |
+
# 4. Prepare Submission
|
| 93 |
+
submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
|
| 94 |
+
status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
|
| 95 |
+
print(status_update)
|
| 96 |
+
|
| 97 |
+
# 5. Submit
|
| 98 |
+
print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
|
| 99 |
+
try:
|
| 100 |
+
response = requests.post(submit_url, json=submission_data, timeout=60)
|
| 101 |
+
response.raise_for_status()
|
| 102 |
+
result_data = response.json()
|
| 103 |
+
final_status = (
|
| 104 |
+
f"Submission Successful!\n"
|
| 105 |
+
f"User: {result_data.get('username')}\n"
|
| 106 |
+
f"Overall Score: {result_data.get('score', 'N/A')}% "
|
| 107 |
+
f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
|
| 108 |
+
f"Message: {result_data.get('message', 'No message received.')}"
|
| 109 |
+
)
|
| 110 |
+
print("Submission successful.")
|
| 111 |
+
results_df = pd.DataFrame(results_log)
|
| 112 |
+
return final_status, results_df
|
| 113 |
+
except requests.exceptions.HTTPError as e:
|
| 114 |
+
error_detail = f"Server responded with status {e.response.status_code}."
|
| 115 |
+
try:
|
| 116 |
+
error_json = e.response.json()
|
| 117 |
+
error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
|
| 118 |
+
except requests.exceptions.JSONDecodeError:
|
| 119 |
+
error_detail += f" Response: {e.response.text[:500]}"
|
| 120 |
+
status_message = f"Submission Failed: {error_detail}"
|
| 121 |
+
print(status_message)
|
| 122 |
+
results_df = pd.DataFrame(results_log)
|
| 123 |
+
return status_message, results_df
|
| 124 |
+
except requests.exceptions.Timeout:
|
| 125 |
+
status_message = "Submission Failed: The request timed out."
|
| 126 |
+
print(status_message)
|
| 127 |
+
results_df = pd.DataFrame(results_log)
|
| 128 |
+
return status_message, results_df
|
| 129 |
+
except requests.exceptions.RequestException as e:
|
| 130 |
+
status_message = f"Submission Failed: Network error - {e}"
|
| 131 |
+
print(status_message)
|
| 132 |
+
results_df = pd.DataFrame(results_log)
|
| 133 |
+
return status_message, results_df
|
| 134 |
+
except Exception as e:
|
| 135 |
+
status_message = f"An unexpected error occurred during submission: {e}"
|
| 136 |
+
print(status_message)
|
| 137 |
+
results_df = pd.DataFrame(results_log)
|
| 138 |
+
return status_message, results_df
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# --- Build Gradio Interface using Blocks ---
|
| 142 |
+
with gr.Blocks() as demo:
|
| 143 |
+
gr.Markdown("# Basic Agent Evaluation Runner")
|
| 144 |
+
gr.Markdown(
|
| 145 |
+
"""
|
| 146 |
+
**Instructions:**
|
| 147 |
+
1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
|
| 148 |
+
2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
|
| 149 |
+
3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
|
| 150 |
+
---
|
| 151 |
+
**Disclaimers:**
|
| 152 |
+
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).
|
| 153 |
+
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.
|
| 154 |
+
"""
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
gr.LoginButton()
|
| 158 |
+
|
| 159 |
+
run_button = gr.Button("Run Evaluation & Submit All Answers")
|
| 160 |
+
|
| 161 |
+
status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
|
| 162 |
+
# Removed max_rows=10 from DataFrame constructor
|
| 163 |
+
results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
|
| 164 |
+
|
| 165 |
+
run_button.click(
|
| 166 |
+
fn=run_and_submit_all,
|
| 167 |
+
outputs=[status_output, results_table]
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
if __name__ == "__main__":
|
| 171 |
+
print("\n" + "-"*30 + " App Starting " + "-"*30)
|
| 172 |
+
# Check for SPACE_HOST and SPACE_ID at startup for information
|
| 173 |
+
space_host_startup = os.getenv("SPACE_HOST")
|
| 174 |
+
space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
|
| 175 |
+
|
| 176 |
+
if space_host_startup:
|
| 177 |
+
print(f"✅ SPACE_HOST found: {space_host_startup}")
|
| 178 |
+
print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
|
| 179 |
+
else:
|
| 180 |
+
print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
|
| 181 |
+
|
| 182 |
+
if space_id_startup: # Print repo URLs if SPACE_ID is found
|
| 183 |
+
print(f"✅ SPACE_ID found: {space_id_startup}")
|
| 184 |
+
print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
|
| 185 |
+
print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
|
| 186 |
+
else:
|
| 187 |
+
print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
|
| 188 |
+
|
| 189 |
+
print("-"*(60 + len(" App Starting ")) + "\n")
|
| 190 |
+
|
| 191 |
+
print("Launching Gradio Interface for Basic Agent Evaluation...")
|
| 192 |
+
demo.launch(debug=True, share=False)
|
hf_agent_course_final_assignment/audio_agent.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import base64
|
| 5 |
+
import mimetypes
|
| 6 |
+
import argparse
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import List, Dict, Any, Optional
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from huggingface_hub import InferenceClient
|
| 11 |
+
|
| 12 |
+
# Load environment variables (e.g., HF_TOKEN) from a .env file
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# This is the system prompt you provided
|
| 16 |
+
AGENT_SYSTEM_PROMPT = """You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string."""
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class AudioAgent:
|
| 20 |
+
"""
|
| 21 |
+
Encapsulated agent for answering questions about an audio file.
|
| 22 |
+
|
| 23 |
+
This agent works in two stages:
|
| 24 |
+
1. Transcribe the audio file using an ASR model.
|
| 25 |
+
2. Pass the transcription and the user's question to a chat model
|
| 26 |
+
to get the final answer.
|
| 27 |
+
|
| 28 |
+
Usage:
|
| 29 |
+
# Assumes HF_TOKEN is in your environment
|
| 30 |
+
agent = AudioAgent()
|
| 31 |
+
|
| 32 |
+
# Ask a question with an audio file
|
| 33 |
+
question = "What ingredients are in this recipe?"
|
| 34 |
+
audio_path = "/path/to/your/recipe.mp3"
|
| 35 |
+
answer = agent.answer(question, audio_path)
|
| 36 |
+
|
| 37 |
+
# Or use the callable shortcut
|
| 38 |
+
answer = agent(question, audio_path)
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
asr_model_id: Optional[str] = None,
|
| 44 |
+
chat_model_id: Optional[str] = None,
|
| 45 |
+
provider: Optional[str] = "novita",
|
| 46 |
+
temperature: float = 0.2,
|
| 47 |
+
api_key: Optional[str] = None,
|
| 48 |
+
):
|
| 49 |
+
"""
|
| 50 |
+
Initializes the AudioAgent.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
asr_model_id: The repo ID of the ASR (transcription) model.
|
| 54 |
+
chat_model_id: The repo ID of the chat model.
|
| 55 |
+
provider: The provider for the chat InferenceClient.
|
| 56 |
+
temperature: The generation temperature for the chat model.
|
| 57 |
+
api_key: The API key. Defaults to os.environ["HF_TOKEN"].
|
| 58 |
+
"""
|
| 59 |
+
# Model for speech-to-text
|
| 60 |
+
self.asr_model_id = asr_model_id or "openai/whisper-large-v3"
|
| 61 |
+
|
| 62 |
+
# Model for answering the question based on the text
|
| 63 |
+
# Using a standard, high-performance chat model.
|
| 64 |
+
# You can change this to "meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
| 65 |
+
# if you know it also performs well on text-only tasks.
|
| 66 |
+
self.chat_model_id = chat_model_id or "meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
| 67 |
+
|
| 68 |
+
self.temperature = temperature
|
| 69 |
+
|
| 70 |
+
# Get API key from arg or environment
|
| 71 |
+
self.api_key = api_key or os.environ.get("HF_TOKEN")
|
| 72 |
+
if not self.api_key:
|
| 73 |
+
raise ValueError(
|
| 74 |
+
"API key not found. Please set the HF_TOKEN environment variable "
|
| 75 |
+
"or pass it as 'api_key' to the AudioAgent."
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# 1. Client for ASR (using default HF provider)
|
| 79 |
+
# We pass the full model ID to use the standard HF inference API
|
| 80 |
+
self.asr_client = InferenceClient(
|
| 81 |
+
model=self.asr_model_id,
|
| 82 |
+
api_key=self.api_key
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# 2. Client for Chat (using your specified provider)
|
| 86 |
+
# We pass the provider, and specify the model in the create call
|
| 87 |
+
self.chat_client = InferenceClient(
|
| 88 |
+
provider=provider,
|
| 89 |
+
api_key=self.api_key
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# ---- Public API ----
|
| 93 |
+
|
| 94 |
+
def answer(self, question: str, audio_path: str) -> str:
|
| 95 |
+
"""
|
| 96 |
+
Run the agent to answer a question about an audio file.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
question: The text prompt to ask.
|
| 100 |
+
audio_path: The local file path to the audio file (e.g., .mp3).
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
The parsed "Final Answer" string from the chat model.
|
| 104 |
+
"""
|
| 105 |
+
# 1. Transcribe the audio
|
| 106 |
+
try:
|
| 107 |
+
transcription = self._transcribe(audio_path)
|
| 108 |
+
except FileNotFoundError:
|
| 109 |
+
return f"Error: Audio file not found at {audio_path}"
|
| 110 |
+
except Exception as e:
|
| 111 |
+
return f"Error during audio transcription: {e}"
|
| 112 |
+
|
| 113 |
+
# 2. Build the combined prompt for the chat model
|
| 114 |
+
combined_prompt = (
|
| 115 |
+
f"You will be given a user's question and the transcription of an audio file. "
|
| 116 |
+
f"Your task is to answer the question based *only* on the transcription.\n\n"
|
| 117 |
+
f"--- TRANSCRIPTION ---\n"
|
| 118 |
+
f"{transcription}\n"
|
| 119 |
+
f"--- END TRANSCRIPTION ---\n\n"
|
| 120 |
+
f"--- QUESTION ---\n"
|
| 121 |
+
f"{question}\n"
|
| 122 |
+
f"--- END QUESTION ---"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# 3. Build the messages payload
|
| 126 |
+
messages = self._build_chat_messages(combined_prompt)
|
| 127 |
+
|
| 128 |
+
# 4. Call the Chat API
|
| 129 |
+
try:
|
| 130 |
+
completion = self.chat_client.chat.completions.create(
|
| 131 |
+
model=self.chat_model_id,
|
| 132 |
+
messages=messages,
|
| 133 |
+
temperature=self.temperature,
|
| 134 |
+
)
|
| 135 |
+
response_text = completion.choices[0].message.content
|
| 136 |
+
except Exception as e:
|
| 137 |
+
return f"Error calling Chat API: {e}"
|
| 138 |
+
|
| 139 |
+
# 5. Parse and return the final answer
|
| 140 |
+
return self._parse_answer(response_text)
|
| 141 |
+
|
| 142 |
+
def __call__(self, question: str, audio_path: str) -> str:
|
| 143 |
+
"""Callable shortcut for self.answer()."""
|
| 144 |
+
return self.answer(question, audio_path)
|
| 145 |
+
|
| 146 |
+
# ---- Internals ----
|
| 147 |
+
|
| 148 |
+
def _transcribe(self, audio_path: str) -> str:
|
| 149 |
+
"""Transcribes the audio file to text using the ASR client."""
|
| 150 |
+
p = Path(audio_path)
|
| 151 |
+
if not p.exists():
|
| 152 |
+
raise FileNotFoundError(f"No file found at {audio_path}")
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
# Read audio file as bytes
|
| 156 |
+
audio_bytes = p.read_bytes()
|
| 157 |
+
|
| 158 |
+
# Call ASR API
|
| 159 |
+
# This task returns a dict, e.g., {'text': '...'}
|
| 160 |
+
response = self.asr_client.automatic_speech_recognition(audio_bytes)
|
| 161 |
+
|
| 162 |
+
if "text" in response:
|
| 163 |
+
return response["text"]
|
| 164 |
+
else:
|
| 165 |
+
raise ValueError(f"ASR API response did not contain 'text': {response}")
|
| 166 |
+
except Exception as e:
|
| 167 |
+
# Re-raise as a more informative exception
|
| 168 |
+
raise RuntimeError(f"Error during audio transcription: {e}") from e
|
| 169 |
+
|
| 170 |
+
def _build_chat_messages(self, combined_prompt: str) -> List[Dict[str, Any]]:
|
| 171 |
+
"""Constructs the message payload for the chat API."""
|
| 172 |
+
return [
|
| 173 |
+
{
|
| 174 |
+
"role": "system",
|
| 175 |
+
"content": AGENT_SYSTEM_PROMPT,
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"role": "user",
|
| 179 |
+
"content": combined_prompt,
|
| 180 |
+
},
|
| 181 |
+
]
|
| 182 |
+
|
| 183 |
+
def _parse_answer(self, response: str) -> str:
|
| 184 |
+
"""Extracts the text after 'FINAL ANSWER: '."""
|
| 185 |
+
try:
|
| 186 |
+
# Use rpartition for robustness, splits on the *last* occurrence
|
| 187 |
+
_before, _marker, final_answer = response.rpartition("FINAL ANSWER: ")
|
| 188 |
+
|
| 189 |
+
if _marker:
|
| 190 |
+
return final_answer.strip()
|
| 191 |
+
else:
|
| 192 |
+
# If marker isn't found, return the whole response with a warning
|
| 193 |
+
print("Warning: Could not find 'FINAL ANSWER:' marker. Returning full response.")
|
| 194 |
+
return response.strip()
|
| 195 |
+
|
| 196 |
+
except Exception as e:
|
| 197 |
+
print(f"Error parsing response: {e}")
|
| 198 |
+
return response.strip()
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
# ---- CLI Entrypoint ----
|
| 202 |
+
def main():
|
| 203 |
+
"""
|
| 204 |
+
Allows running the agent from the command line.
|
| 205 |
+
|
| 206 |
+
Example:
|
| 207 |
+
python audio_agent.py "What ingredients are listed?" "path/to/my_recipe.mp3"
|
| 208 |
+
"""
|
| 209 |
+
parser = argparse.ArgumentParser(description="Class-based agent for audio questions.")
|
| 210 |
+
parser.add_argument("question", type=str, help="Your question (quoted).")
|
| 211 |
+
parser.add_argument("audio_path", type=str, help="Path to the audio file.")
|
| 212 |
+
args = parser.parse_args()
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
audio_agent = AudioAgent()
|
| 216 |
+
answer = audio_agent.answer(args.question, args.audio_path)
|
| 217 |
+
print(answer)
|
| 218 |
+
except Exception as e:
|
| 219 |
+
print(f"An error occurred: {e}")
|
| 220 |
+
|
| 221 |
+
if __name__ == "__main__":
|
| 222 |
+
main()
|
hf_agent_course_final_assignment/evaluation_questions.json
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"task_id": "8e867cd7-cff9-4e6c-867a-ff5ddc2550be",
|
| 4 |
+
"question": "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia.",
|
| 5 |
+
"Level": "1",
|
| 6 |
+
"file_name": ""
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"task_id": "a1e91b78-d3d8-4675-bb8d-62741b4b68a6",
|
| 10 |
+
"question": "In the video https://www.youtube.com/watch?v=L1vXCYZAYYM, what is the highest number of bird species to be on camera simultaneously?",
|
| 11 |
+
"Level": "1",
|
| 12 |
+
"file_name": ""
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"task_id": "2d83110e-a098-4ebb-9987-066c06fa42d0",
|
| 16 |
+
"question": ".rewsna eht sa \"tfel\" drow eht fo etisoppo eht etirw ,ecnetnes siht dnatsrednu uoy fI",
|
| 17 |
+
"Level": "1",
|
| 18 |
+
"file_name": ""
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"task_id": "cca530fc-4052-43b2-b130-b30968d8aa44",
|
| 22 |
+
"question": "Review the chess position provided in the image. It is black's turn. Provide the correct next move for black which guarantees a win. Please provide your response in algebraic notation.",
|
| 23 |
+
"Level": "1",
|
| 24 |
+
"file_name": "cca530fc-4052-43b2-b130-b30968d8aa44.png"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"task_id": "4fc2f1ae-8625-45b5-ab34-ad4433bc21f8",
|
| 28 |
+
"question": "Who nominated the only Featured Article on English Wikipedia about a dinosaur that was promoted in November 2016?",
|
| 29 |
+
"Level": "1",
|
| 30 |
+
"file_name": ""
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"task_id": "6f37996b-2ac7-44b0-8e68-6d28256631b4",
|
| 34 |
+
"question": "Given this table defining * on the set S = {a, b, c, d, e}\n\n|*|a|b|c|d|e|\n|---|---|---|---|---|---|\n|a|a|b|c|b|d|\n|b|b|c|a|e|c|\n|c|c|a|b|b|a|\n|d|b|e|b|e|d|\n|e|d|b|a|d|c|\n\nprovide the subset of S involved in any possible counter-examples that prove * is not commutative. Provide your answer as a comma separated list of the elements in the set in alphabetical order.",
|
| 35 |
+
"Level": "1",
|
| 36 |
+
"file_name": ""
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"task_id": "9d191bce-651d-4746-be2d-7ef8ecadb9c2",
|
| 40 |
+
"question": "Examine the video at https://www.youtube.com/watch?v=1htKBjuUWec.\n\nWhat does Teal'c say in response to the question \"Isn't that hot?\"",
|
| 41 |
+
"Level": "1",
|
| 42 |
+
"file_name": ""
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"task_id": "cabe07ed-9eca-40ea-8ead-410ef5e83f91",
|
| 46 |
+
"question": "What is the surname of the equine veterinarian mentioned in 1.E Exercises from the chemistry materials licensed by Marisa Alviar-Agnew & Henry Agnew under the CK-12 license in LibreText's Introductory Chemistry materials as compiled 08/21/2023?",
|
| 47 |
+
"Level": "1",
|
| 48 |
+
"file_name": ""
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"task_id": "3cef3a44-215e-4aed-8e3b-b1e3f08063b7",
|
| 52 |
+
"question": "I'm making a grocery list for my mom, but she's a professor of botany and she's a real stickler when it comes to categorizing things. I need to add different foods to different categories on the grocery list, but if I make a mistake, she won't buy anything inserted in the wrong category. Here's the list I have so far:\n\nmilk, eggs, flour, whole bean coffee, Oreos, sweet potatoes, fresh basil, plums, green beans, rice, corn, bell pepper, whole allspice, acorns, broccoli, celery, zucchini, lettuce, peanuts\n\nI need to make headings for the fruits and vegetables. Could you please create a list of just the vegetables from my list? If you could do that, then I can figure out how to categorize the rest of the list into the appropriate categories. But remember that my mom is a real stickler, so make sure that no botanical fruits end up on the vegetable list, or she won't get them when she's at the store. Please alphabetize the list of vegetables, and place each item in a comma separated list.",
|
| 53 |
+
"Level": "1",
|
| 54 |
+
"file_name": ""
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"task_id": "99c9cc74-fdc8-46c6-8f8d-3ce2d3bfeea3",
|
| 58 |
+
"question": "Hi, I'm making a pie but I could use some help with my shopping list. I have everything I need for the crust, but I'm not sure about the filling. I got the recipe from my friend Aditi, but she left it as a voice memo and the speaker on my phone is buzzing so I can't quite make out what she's saying. Could you please listen to the recipe and list all of the ingredients that my friend described? I only want the ingredients for the filling, as I have everything I need to make my favorite pie crust. I've attached the recipe as Strawberry pie.mp3.\n\nIn your response, please only list the ingredients, not any measurements. So if the recipe calls for \"a pinch of salt\" or \"two cups of ripe strawberries\" the ingredients on the list would be \"salt\" and \"ripe strawberries\".\n\nPlease format your response as a comma separated list of ingredients. Also, please alphabetize the ingredients.",
|
| 59 |
+
"Level": "1",
|
| 60 |
+
"file_name": "99c9cc74-fdc8-46c6-8f8d-3ce2d3bfeea3.mp3"
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"task_id": "305ac316-eef6-4446-960a-92d80d542f82",
|
| 64 |
+
"question": "Who did the actor who played Ray in the Polish-language version of Everybody Loves Raymond play in Magda M.? Give only the first name.",
|
| 65 |
+
"Level": "1",
|
| 66 |
+
"file_name": ""
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"task_id": "f918266a-b3e0-4914-865d-4faa564f1aef",
|
| 70 |
+
"question": "What is the final numeric output from the attached Python code?",
|
| 71 |
+
"Level": "1",
|
| 72 |
+
"file_name": "f918266a-b3e0-4914-865d-4faa564f1aef.py"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"task_id": "3f57289b-8c60-48be-bd80-01f8099ca449",
|
| 76 |
+
"question": "How many at bats did the Yankee with the most walks in the 1977 regular season have that same season?",
|
| 77 |
+
"Level": "1",
|
| 78 |
+
"file_name": ""
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"task_id": "1f975693-876d-457b-a649-393859e79bf3",
|
| 82 |
+
"question": "Hi, I was out sick from my classes on Friday, so I'm trying to figure out what I need to study for my Calculus mid-term next week. My friend from class sent me an audio recording of Professor Willowbrook giving out the recommended reading for the test, but my headphones are broken :(\n\nCould you please listen to the recording for me and tell me the page numbers I'm supposed to go over? I've attached a file called Homework.mp3 that has the recording. Please provide just the page numbers as a comma-delimited list. And please provide the list in ascending order.",
|
| 83 |
+
"Level": "1",
|
| 84 |
+
"file_name": "1f975693-876d-457b-a649-393859e79bf3.mp3"
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"task_id": "840bfca7-4f7b-481a-8794-c560c340185d",
|
| 88 |
+
"question": "On June 6, 2023, an article by Carolyn Collins Petersen was published in Universe Today. This article mentions a team that produced a paper about their observations, linked at the bottom of the article. Find this paper. Under what NASA award number was the work performed by R. G. Arendt supported by?",
|
| 89 |
+
"Level": "1",
|
| 90 |
+
"file_name": ""
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"task_id": "bda648d7-d618-4883-88f4-3466eabd860e",
|
| 94 |
+
"question": "Where were the Vietnamese specimens described by Kuznetzov in Nedoshivina's 2010 paper eventually deposited? Just give me the city name without abbreviations.",
|
| 95 |
+
"Level": "1",
|
| 96 |
+
"file_name": ""
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"task_id": "cf106601-ab4f-4af9-b045-5295fe67b37d",
|
| 100 |
+
"question": "What country had the least number of athletes at the 1928 Summer Olympics? If there's a tie for a number of athletes, return the first in alphabetical order. Give the IOC country code as your answer.",
|
| 101 |
+
"Level": "1",
|
| 102 |
+
"file_name": ""
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"task_id": "a0c07678-e491-4bbc-8f0b-07405144218f",
|
| 106 |
+
"question": "Who are the pitchers with the number before and after Taishō Tamai's number as of July 2023? Give them to me in the form Pitcher Before, Pitcher After, use their last names only, in Roman characters.",
|
| 107 |
+
"Level": "1",
|
| 108 |
+
"file_name": ""
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"task_id": "7bd855d8-463d-4ed5-93ca-5fe35145f733",
|
| 112 |
+
"question": "The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places.",
|
| 113 |
+
"Level": "1",
|
| 114 |
+
"file_name": "7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx"
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"task_id": "5a0c1adf-205e-4841-a666-7c3ef95def9d",
|
| 118 |
+
"question": "What is the first name of the only Malko Competition recipient from the 20th Century (after 1977) whose nationality on record is a country that no longer exists?",
|
| 119 |
+
"Level": "1",
|
| 120 |
+
"file_name": ""
|
| 121 |
+
}
|
| 122 |
+
]
|
hf_agent_course_final_assignment/excel_agent.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import argparse
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import List, Dict, Any, Optional
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
from huggingface_hub import InferenceClient
|
| 9 |
+
|
| 10 |
+
# Import pandas for Excel/CSV handling
|
| 11 |
+
try:
|
| 12 |
+
import pandas as pd
|
| 13 |
+
except ImportError:
|
| 14 |
+
print("Error: 'pandas' and 'openpyxl' libraries are required for ExcelAgent.")
|
| 15 |
+
print("Please install them with: pip install pandas openpyxl")
|
| 16 |
+
exit(1)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# Load environment variables (e.g., HF_TOKEN) from a .env file
|
| 20 |
+
load_dotenv()
|
| 21 |
+
|
| 22 |
+
# This is the system prompt you provided
|
| 23 |
+
AGENT_SYSTEM_PROMPT = """You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string."""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ExcelAgent:
|
| 27 |
+
"""
|
| 28 |
+
Encapsulated agent for answering questions about an Excel file.
|
| 29 |
+
|
| 30 |
+
This agent works in two stages:
|
| 31 |
+
1. Read the Excel file and convert its content to a text (CSV) format.
|
| 32 |
+
2. Pass the user's question and the text-based data to a chat model
|
| 33 |
+
to get the final answer.
|
| 34 |
+
|
| 35 |
+
Usage:
|
| 36 |
+
# Assumes HF_TOKEN is in your environment
|
| 37 |
+
agent = ExcelAgent()
|
| 38 |
+
|
| 39 |
+
# Ask a question with an Excel file
|
| 40 |
+
question = "What are the total sales?"
|
| 41 |
+
excel_path = "/path/to/your/data.xlsx"
|
| 42 |
+
answer = agent.answer(question, excel_path)
|
| 43 |
+
|
| 44 |
+
# Or use the callable shortcut
|
| 45 |
+
answer = agent(question, excel_path)
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
chat_model_id: Optional[str] = None,
|
| 51 |
+
provider: Optional[str] = "novita",
|
| 52 |
+
temperature: float = 0.1,
|
| 53 |
+
api_key: Optional[str] = None,
|
| 54 |
+
):
|
| 55 |
+
"""
|
| 56 |
+
Initializes the ExcelAgent.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
chat_model_id: The repo ID of the chat model.
|
| 60 |
+
provider: The provider for the chat InferenceClient.
|
| 61 |
+
temperature: The generation temperature for the chat model.
|
| 62 |
+
api_key: The API key. Defaults to os.environ["HF_TOKEN"].
|
| 63 |
+
"""
|
| 64 |
+
# We use the same powerful model as the ImageAgent
|
| 65 |
+
self.chat_model_id = chat_model_id or "meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
| 66 |
+
self.temperature = temperature
|
| 67 |
+
|
| 68 |
+
# Get API key from arg or environment
|
| 69 |
+
self.api_key = api_key or os.environ.get("HF_TOKEN")
|
| 70 |
+
if not self.api_key:
|
| 71 |
+
raise ValueError(
|
| 72 |
+
"API key not found. Please set the HF_TOKEN environment variable "
|
| 73 |
+
"or pass it as 'api_key' to the ExcelAgent."
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# Client for Chat
|
| 77 |
+
self.chat_client = InferenceClient(
|
| 78 |
+
provider=provider,
|
| 79 |
+
api_key=self.api_key
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# ---- Public API ----
|
| 83 |
+
|
| 84 |
+
def answer(self, question: str, excel_path: str) -> str:
|
| 85 |
+
"""
|
| 86 |
+
Run the agent to answer a question about an Excel file.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
question: The text prompt to ask.
|
| 90 |
+
excel_path: The local file path to the Excel file (.xlsx).
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
The parsed "Final Answer" string from the chat model.
|
| 94 |
+
"""
|
| 95 |
+
# 1. Read Excel and convert to text
|
| 96 |
+
try:
|
| 97 |
+
data_as_text = self._excel_to_text(excel_path)
|
| 98 |
+
except FileNotFoundError:
|
| 99 |
+
return f"Error: Excel file not found at {excel_path}"
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return f"Error during Excel processing: {e}"
|
| 102 |
+
|
| 103 |
+
# 2. Build the combined prompt for the chat model
|
| 104 |
+
combined_prompt = (
|
| 105 |
+
f"You will be given a user's question and the content of an Excel file. "
|
| 106 |
+
f"The Excel data is represented in CSV format. "
|
| 107 |
+
f"Your task is to answer the question based *only* on the provided data.\n\n"
|
| 108 |
+
f"--- DATA (CSV format) ---\n"
|
| 109 |
+
f"{data_as_text}\n"
|
| 110 |
+
f"--- END DATA ---\n\n"
|
| 111 |
+
f"--- QUESTION ---\n"
|
| 112 |
+
f"{question}\n"
|
| 113 |
+
f"--- END QUESTION ---"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# 3. Build the messages payload
|
| 117 |
+
messages = self._build_chat_messages(combined_prompt)
|
| 118 |
+
|
| 119 |
+
# 4. Call the Chat API
|
| 120 |
+
try:
|
| 121 |
+
completion = self.chat_client.chat.completions.create(
|
| 122 |
+
model=self.chat_model_id,
|
| 123 |
+
messages=messages,
|
| 124 |
+
temperature=self.temperature,
|
| 125 |
+
)
|
| 126 |
+
response_text = completion.choices[0].message.content
|
| 127 |
+
except Exception as e:
|
| 128 |
+
return f"Error calling Chat API: {e}"
|
| 129 |
+
|
| 130 |
+
# 5. Parse and return the final answer
|
| 131 |
+
return self._parse_answer(response_text)
|
| 132 |
+
|
| 133 |
+
def __call__(self, question: str, excel_path: str) -> str:
|
| 134 |
+
"""Callable shortcut for self.answer()."""
|
| 135 |
+
return self.answer(question, excel_path)
|
| 136 |
+
|
| 137 |
+
# ---- Internals ----
|
| 138 |
+
|
| 139 |
+
def _excel_to_text(self, excel_path: str) -> str:
|
| 140 |
+
"""Reads an Excel file and converts it to a CSV string."""
|
| 141 |
+
p = Path(excel_path)
|
| 142 |
+
if not p.exists():
|
| 143 |
+
raise FileNotFoundError(f"No file found at {excel_path}")
|
| 144 |
+
|
| 145 |
+
try:
|
| 146 |
+
# Read the Excel file
|
| 147 |
+
# By default, reads the first sheet
|
| 148 |
+
df = pd.read_excel(str(p))
|
| 149 |
+
|
| 150 |
+
# Convert DataFrame to a CSV string
|
| 151 |
+
# index=False to avoid writing row numbers
|
| 152 |
+
csv_string = df.to_csv(index=False)
|
| 153 |
+
|
| 154 |
+
return csv_string
|
| 155 |
+
except Exception as e:
|
| 156 |
+
# Re-raise as a more informative exception
|
| 157 |
+
raise RuntimeError(f"Error reading/converting Excel file: {e}") from e
|
| 158 |
+
|
| 159 |
+
def _build_chat_messages(self, combined_prompt: str) -> List[Dict[str, Any]]:
|
| 160 |
+
"""Constructs the message payload for the chat API."""
|
| 161 |
+
return [
|
| 162 |
+
{
|
| 163 |
+
"role": "system",
|
| 164 |
+
"content": AGENT_SYSTEM_PROMPT,
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"role": "user",
|
| 168 |
+
"content": combined_prompt,
|
| 169 |
+
},
|
| 170 |
+
]
|
| 171 |
+
|
| 172 |
+
def _parse_answer(self, response: str) -> str:
|
| 173 |
+
"""Extracts the text after 'FINAL ANSWER: '."""
|
| 174 |
+
try:
|
| 175 |
+
# Use rpartition for robustness, splits on the *last* occurrence
|
| 176 |
+
_before, _marker, final_answer = response.rpartition("FINAL ANSWER: ")
|
| 177 |
+
|
| 178 |
+
if _marker:
|
| 179 |
+
return final_answer.strip()
|
| 180 |
+
else:
|
| 181 |
+
# If marker isn't found, return the whole response with a warning
|
| 182 |
+
print("Warning: Could not find 'FINAL ANSWER:' marker. Returning full response.")
|
| 183 |
+
return response.strip()
|
| 184 |
+
|
| 185 |
+
except Exception as e:
|
| 186 |
+
print(f"Error parsing response: {e}")
|
| 187 |
+
return response.strip()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
# ---- CLI Entrypoint ----
|
| 191 |
+
def main():
|
| 192 |
+
"""
|
| 193 |
+
Allows running the agent from the command line.
|
| 194 |
+
|
| 195 |
+
Example:
|
| 196 |
+
python excel_agent.py "What are the total sales?" "path/to/my_data.xlsx"
|
| 197 |
+
"""
|
| 198 |
+
parser = argparse.ArgumentParser(description="Class-based agent for Excel questions.")
|
| 199 |
+
parser.add_argument("question", type=str, help="Your question (quoted).")
|
| 200 |
+
parser.add_argument("excel_path", type=str, help="Path to the Excel file.")
|
| 201 |
+
args = parser.parse_args()
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
excel_agent = ExcelAgent()
|
| 205 |
+
answer = excel_agent.answer(args.question, args.excel_path)
|
| 206 |
+
print(answer)
|
| 207 |
+
except Exception as e:
|
| 208 |
+
print(f"An error occurred: {e}")
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
main()
|
hf_agent_course_final_assignment/gaia_agent.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# gaia_agent.py
|
| 2 |
+
#
|
| 3 |
+
# High-level GAIA orchestrator with LLM-based routing.
|
| 4 |
+
#
|
| 5 |
+
# Usage:
|
| 6 |
+
# from gaia_agent import GaiaAgent
|
| 7 |
+
# from utils.fetch_files import fetch_files_gaia_validation_hub
|
| 8 |
+
#
|
| 9 |
+
# gaia_agent = GaiaAgent()
|
| 10 |
+
#
|
| 11 |
+
# # Q without file
|
| 12 |
+
# ans1 = gaia_agent(
|
| 13 |
+
# "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia.",
|
| 14 |
+
# ""
|
| 15 |
+
# )
|
| 16 |
+
#
|
| 17 |
+
# # Q with file (image)
|
| 18 |
+
# img_path = fetch_files_gaia_validation_hub("cca530fc-4052-43b2-b130-b30968d8aa44.png")
|
| 19 |
+
# ans2 = gaia_agent(
|
| 20 |
+
# "Review the chess position provided in the image. It is black's turn...",
|
| 21 |
+
# img_path
|
| 22 |
+
# )
|
| 23 |
+
#
|
| 24 |
+
# # Q with YouTube link
|
| 25 |
+
# ans3 = gaia_agent(
|
| 26 |
+
# "In the video https://www.youtube.com/watch?v=L1vXCYZAYYM, what is the highest number of bird species to be on camera simultaneously?",
|
| 27 |
+
# ""
|
| 28 |
+
# )
|
| 29 |
+
#
|
| 30 |
+
# print(ans1, ans2, ans3)
|
| 31 |
+
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import os
|
| 35 |
+
import re
|
| 36 |
+
import mimetypes
|
| 37 |
+
from typing import Optional, Dict, Any, List
|
| 38 |
+
from urllib.parse import urlparse, parse_qs
|
| 39 |
+
|
| 40 |
+
from dotenv import load_dotenv
|
| 41 |
+
from huggingface_hub import InferenceClient
|
| 42 |
+
|
| 43 |
+
from web_agent import WebAgent
|
| 44 |
+
from image_agent import ImageAgent
|
| 45 |
+
from audio_agent import AudioAgent
|
| 46 |
+
from python_agent import PythonAgent
|
| 47 |
+
from excel_agent import ExcelAgent
|
| 48 |
+
from youtube_agent import YouTubeSubsAgent # YouTubeSubsAgent(question, video_url)
|
| 49 |
+
|
| 50 |
+
load_dotenv()
|
| 51 |
+
|
| 52 |
+
#########################################################
|
| 53 |
+
# 1. Routing model prompt / helper
|
| 54 |
+
#########################################################
|
| 55 |
+
|
| 56 |
+
ROUTER_SYSTEM_PROMPT = """You are a routing controller for a GAIA QA system.
|
| 57 |
+
Your job is to look at a task and decide which specialized tool should answer it.
|
| 58 |
+
|
| 59 |
+
TOOLS:
|
| 60 |
+
- "web": For questions that need factual info from the internet, Wikipedia, sports stats, award numbers, historical rosters, etc. Use this if the answer relies on public info not contained in an attached file or transcript.
|
| 61 |
+
- "youtube": For questions that refer to a YouTube video URL and ask about what was said/heard in that video. The YouTube agent only has access to the video's subtitles.
|
| 62 |
+
- "image": For questions that require analyzing an image file (ex: chessboard, screenshot, diagram). File extensions like .png/.jpg/.jpeg.
|
| 63 |
+
- "audio": For questions that require transcribing or extracting info from an audio file (ex: .mp3, .wav, "listen to the recording and list ingredients").
|
| 64 |
+
- "excel": For questions that require reading a spreadsheet (.xls, .xlsx, .csv) and doing numeric reasoning or lookup in that sheet.
|
| 65 |
+
- "python": For questions that say "What is the final numeric output from the attached Python code?" or otherwise require running code from a .py file.
|
| 66 |
+
- "internal_reasoning": For pure logic, math, text manipulation, categorization, set theory, etc. No web. No external file beyond the text in the question.
|
| 67 |
+
|
| 68 |
+
RESTRICTIONS AND NOTES:
|
| 69 |
+
1. If there is a YouTube link in the question (youtube.com or youtu.be) and the user is asking about what someone says or does in that video, choose "youtube".
|
| 70 |
+
2. If there is an attached file_path and it's an image, choose "image".
|
| 71 |
+
3. If there is an attached file_path and it's audio, choose "audio".
|
| 72 |
+
4. If there is an attached file_path and it's Excel/CSV, choose "excel".
|
| 73 |
+
5. If there is an attached file_path and it's Python code (.py), choose "python".
|
| 74 |
+
6. Otherwise, if answering requires up-to-date or external factual knowledge (e.g. Wikipedia, sports stats, award numbers, who played in some show, etc.), choose "web".
|
| 75 |
+
7. Otherwise choose "internal_reasoning".
|
| 76 |
+
|
| 77 |
+
OUTPUT FORMAT:
|
| 78 |
+
Return ONLY one of the following exact strings:
|
| 79 |
+
web
|
| 80 |
+
youtube
|
| 81 |
+
image
|
| 82 |
+
audio
|
| 83 |
+
excel
|
| 84 |
+
python
|
| 85 |
+
internal_reasoning
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
INTERNAL_REASONING_SYSTEM_PROMPT = """You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string."""
|
| 89 |
+
|
| 90 |
+
class RoutingLLM:
|
| 91 |
+
"""
|
| 92 |
+
A tiny wrapper around the HF InferenceClient that decides the tool name.
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
def __init__(
|
| 96 |
+
self,
|
| 97 |
+
model_id: Optional[str] = None,
|
| 98 |
+
provider: str = "novita",
|
| 99 |
+
temperature: float = 0.0,
|
| 100 |
+
api_key: Optional[str] = None,
|
| 101 |
+
):
|
| 102 |
+
self.model_id = model_id or "meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
| 103 |
+
self.temperature = temperature
|
| 104 |
+
|
| 105 |
+
self.api_key = api_key or os.environ.get("HF_TOKEN")
|
| 106 |
+
if not self.api_key:
|
| 107 |
+
raise ValueError("RoutingLLM: HF_TOKEN not found and api_key not provided.")
|
| 108 |
+
|
| 109 |
+
self.client = InferenceClient(provider=provider, api_key=self.api_key)
|
| 110 |
+
|
| 111 |
+
def route(self, question: str, file_path: Optional[str]) -> str:
|
| 112 |
+
"""
|
| 113 |
+
Ask the LLM which tool to use.
|
| 114 |
+
We also inject lightweight hints (file extension, presence of YT link),
|
| 115 |
+
so the LLM has explicit signals.
|
| 116 |
+
"""
|
| 117 |
+
file_path = file_path or ""
|
| 118 |
+
ext = os.path.splitext(file_path)[1].lower() if file_path else ""
|
| 119 |
+
mime, _ = mimetypes.guess_type(file_path) if file_path else (None, None)
|
| 120 |
+
has_youtube = "youtube.com" in question.lower() or "youtu.be" in question.lower()
|
| 121 |
+
|
| 122 |
+
user_prompt = (
|
| 123 |
+
"Question:\n"
|
| 124 |
+
f"{question}\n\n"
|
| 125 |
+
"file_path:\n"
|
| 126 |
+
f"{file_path}\n"
|
| 127 |
+
f"(extension='{ext}', mime='{mime}', has_youtube={has_youtube})\n\n"
|
| 128 |
+
"Which tool should handle this? Remember: output ONLY one of:\n"
|
| 129 |
+
"web | youtube | image | audio | excel | python | internal_reasoning"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
messages = [
|
| 133 |
+
{"role": "system", "content": ROUTER_SYSTEM_PROMPT},
|
| 134 |
+
{"role": "user", "content": user_prompt},
|
| 135 |
+
]
|
| 136 |
+
|
| 137 |
+
completion = self.client.chat.completions.create(
|
| 138 |
+
model=self.model_id,
|
| 139 |
+
messages=messages,
|
| 140 |
+
temperature=self.temperature,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
raw = completion.choices[0].message.content.strip().lower()
|
| 144 |
+
|
| 145 |
+
# clean it to a known label
|
| 146 |
+
return self._normalize_route_label(raw)
|
| 147 |
+
|
| 148 |
+
def _normalize_route_label(self, raw: str) -> str:
|
| 149 |
+
"""
|
| 150 |
+
Normalize whatever the model said into one of the 7 valid labels,
|
| 151 |
+
with a safe fallback.
|
| 152 |
+
"""
|
| 153 |
+
allowed = {
|
| 154 |
+
"web",
|
| 155 |
+
"youtube",
|
| 156 |
+
"image",
|
| 157 |
+
"audio",
|
| 158 |
+
"excel",
|
| 159 |
+
"python",
|
| 160 |
+
"internal_reasoning",
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
# grab the first token-like word that matches any allowed label
|
| 164 |
+
for word in re.findall(r"[a-z_]+", raw):
|
| 165 |
+
if word in allowed:
|
| 166 |
+
return word
|
| 167 |
+
|
| 168 |
+
# fallback: default to "web" because GAIA often needs retrieval
|
| 169 |
+
return "web"
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
#########################################################
|
| 173 |
+
# 2. GaiaAgent Orchestrator
|
| 174 |
+
#########################################################
|
| 175 |
+
|
| 176 |
+
class GaiaAgent:
|
| 177 |
+
"""
|
| 178 |
+
High-level GAIA orchestrator with an LLM router.
|
| 179 |
+
|
| 180 |
+
Init:
|
| 181 |
+
gaia_agent = GaiaAgent()
|
| 182 |
+
|
| 183 |
+
Call:
|
| 184 |
+
ans = gaia_agent(question, file_path)
|
| 185 |
+
|
| 186 |
+
where:
|
| 187 |
+
- question: str
|
| 188 |
+
- file_path: str or "" (path to local file if provided for that task)
|
| 189 |
+
"""
|
| 190 |
+
|
| 191 |
+
def __init__(
|
| 192 |
+
self,
|
| 193 |
+
chat_model_id: Optional[str] = None,
|
| 194 |
+
router_model_id: Optional[str] = None,
|
| 195 |
+
provider: str = "novita",
|
| 196 |
+
temperature: float = 0.2,
|
| 197 |
+
router_temperature: float = 0.0,
|
| 198 |
+
api_key: Optional[str] = None,
|
| 199 |
+
):
|
| 200 |
+
self.chat_model_id = chat_model_id or "meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
| 201 |
+
self.temperature = temperature
|
| 202 |
+
|
| 203 |
+
# shared key/client
|
| 204 |
+
self.api_key = api_key or os.environ.get("HF_TOKEN")
|
| 205 |
+
if not self.api_key:
|
| 206 |
+
raise ValueError("GaiaAgent: HF_TOKEN not found and api_key not provided.")
|
| 207 |
+
|
| 208 |
+
# core reasoning client (for internal_reasoning branch)
|
| 209 |
+
self.reasoning_client = InferenceClient(
|
| 210 |
+
provider=provider,
|
| 211 |
+
api_key=self.api_key,
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# router (LLM-based intent classifier)
|
| 215 |
+
self.router = RoutingLLM(
|
| 216 |
+
model_id=router_model_id or self.chat_model_id,
|
| 217 |
+
provider=provider,
|
| 218 |
+
temperature=router_temperature,
|
| 219 |
+
api_key=self.api_key,
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
# instantiate sub-agents once
|
| 223 |
+
self.web_agent = WebAgent() # expects question
|
| 224 |
+
self.youtube_agent = YouTubeSubsAgent(
|
| 225 |
+
api_key=self.api_key,
|
| 226 |
+
) # expects (question, video_url)
|
| 227 |
+
self.image_agent = ImageAgent(
|
| 228 |
+
api_key=self.api_key,
|
| 229 |
+
) # expects (question, file_path)
|
| 230 |
+
self.audio_agent = AudioAgent(
|
| 231 |
+
api_key=self.api_key,
|
| 232 |
+
) # expects (question, file_path)
|
| 233 |
+
self.python_agent = PythonAgent() # expects (file_path)
|
| 234 |
+
self.excel_agent = ExcelAgent(
|
| 235 |
+
api_key=self.api_key,
|
| 236 |
+
) # expects (question, file_path)
|
| 237 |
+
|
| 238 |
+
def __call__(self, question: str, file_path: Optional[str] = "") -> str:
|
| 239 |
+
return self.answer(question, file_path)
|
| 240 |
+
|
| 241 |
+
def answer(self, question: str, file_path: Optional[str] = "") -> str:
|
| 242 |
+
"""
|
| 243 |
+
1. Ask router LLM which tool to use.
|
| 244 |
+
2. Dispatch to that tool.
|
| 245 |
+
3. Return the tool's final answer.
|
| 246 |
+
"""
|
| 247 |
+
route = self.router.route(question, file_path)
|
| 248 |
+
|
| 249 |
+
if route == "youtube":
|
| 250 |
+
print("[GaiaAgent] Routing to YouTube agent.")
|
| 251 |
+
video_url = self._extract_youtube_url(question)
|
| 252 |
+
return self.youtube_agent(question, video_url if video_url else "")
|
| 253 |
+
|
| 254 |
+
if route == "image":
|
| 255 |
+
print("[GaiaAgent] Routing to Image agent.")
|
| 256 |
+
return self.image_agent(question, file_path)
|
| 257 |
+
|
| 258 |
+
if route == "audio":
|
| 259 |
+
print("[GaiaAgent] Routing to Audio agent.")
|
| 260 |
+
return self.audio_agent(question, file_path)
|
| 261 |
+
|
| 262 |
+
if route == "excel":
|
| 263 |
+
print("[GaiaAgent] Routing to Excel agent.")
|
| 264 |
+
return self.excel_agent(question, file_path)
|
| 265 |
+
|
| 266 |
+
if route == "python":
|
| 267 |
+
print("[GaiaAgent] Routing to Python agent.")
|
| 268 |
+
return self._run_python_agent(file_path)
|
| 269 |
+
|
| 270 |
+
if route == "web":
|
| 271 |
+
print("[GaiaAgent] Routing to Web agent.")
|
| 272 |
+
return self.web_agent(question)
|
| 273 |
+
|
| 274 |
+
# internal_reasoning or fallback
|
| 275 |
+
print("[GaiaAgent] Routing to Internal Reasoning.")
|
| 276 |
+
return self._internal_reasoning(question)
|
| 277 |
+
|
| 278 |
+
#########################################################
|
| 279 |
+
# internal helpers
|
| 280 |
+
#########################################################
|
| 281 |
+
|
| 282 |
+
def _extract_youtube_url(self, text: str) -> Optional[str]:
|
| 283 |
+
"""
|
| 284 |
+
Pull first YouTube URL from question.
|
| 285 |
+
"""
|
| 286 |
+
m = re.search(r"(https?://[^\s]+youtu[^\s]+)", text)
|
| 287 |
+
if m:
|
| 288 |
+
return m.group(1).strip().rstrip(').,]')
|
| 289 |
+
return None
|
| 290 |
+
|
| 291 |
+
def _run_python_agent(self, file_path: str) -> str:
|
| 292 |
+
"""
|
| 293 |
+
PythonAgent currently is called like python_agent_instance(file_path)
|
| 294 |
+
and returns output as string.
|
| 295 |
+
"""
|
| 296 |
+
return self.python_agent(file_path)
|
| 297 |
+
|
| 298 |
+
def _internal_reasoning(self, question: str) -> str:
|
| 299 |
+
"""
|
| 300 |
+
Use the same GAIA-style 'FINAL ANSWER:' contract,
|
| 301 |
+
but do pure reasoning, no web.
|
| 302 |
+
"""
|
| 303 |
+
reasoning_prompt = (
|
| 304 |
+
"You will be given a question that can be answered using reasoning, "
|
| 305 |
+
"math, logic, text manipulation, categorization, or other internal thinking. "
|
| 306 |
+
"Do not use any outside knowledge beyond what a normal educated human would know; "
|
| 307 |
+
"do not fabricate web data.\n\n"
|
| 308 |
+
"Question:\n"
|
| 309 |
+
f"{question}\n\n"
|
| 310 |
+
"Remember to follow the FINAL ANSWER format exactly."
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
messages = [
|
| 314 |
+
{"role": "system", "content": INTERNAL_REASONING_SYSTEM_PROMPT},
|
| 315 |
+
{"role": "user", "content": reasoning_prompt},
|
| 316 |
+
]
|
| 317 |
+
|
| 318 |
+
completion = self.reasoning_client.chat.completions.create(
|
| 319 |
+
model=self.chat_model_id,
|
| 320 |
+
messages=messages,
|
| 321 |
+
temperature=self.temperature,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
raw = completion.choices[0].message.content
|
| 325 |
+
return self._parse_final_answer(raw)
|
| 326 |
+
|
| 327 |
+
def _parse_final_answer(self, response: str) -> str:
|
| 328 |
+
"""
|
| 329 |
+
Extract text after 'FINAL ANSWER:' to stay consistent with other agents.
|
| 330 |
+
"""
|
| 331 |
+
_before, _marker, final_answer = response.rpartition("FINAL ANSWER: ")
|
| 332 |
+
if _marker:
|
| 333 |
+
return final_answer.strip()
|
| 334 |
+
return response.strip()
|
hf_agent_course_final_assignment/image_agent.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import base64
|
| 5 |
+
import mimetypes
|
| 6 |
+
import argparse
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import List, Dict, Any, Optional
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from huggingface_hub import InferenceClient
|
| 11 |
+
|
| 12 |
+
# Load environment variables (e.g., HF_TOKEN) from a .env file
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# This is the system prompt you provided
|
| 16 |
+
AGENT_SYSTEM_PROMPT = """You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string."""
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ImageAgent:
|
| 20 |
+
"""
|
| 21 |
+
Encapsulated agent for answering questions about an image.
|
| 22 |
+
|
| 23 |
+
Usage:
|
| 24 |
+
# Assumes HF_TOKEN is in your environment
|
| 25 |
+
agent = ImageAgent()
|
| 26 |
+
|
| 27 |
+
# Ask a question with an image
|
| 28 |
+
question = "What is in this image?"
|
| 29 |
+
image_path = "/path/to/your/image.png"
|
| 30 |
+
answer = agent.answer(question, image_path)
|
| 31 |
+
|
| 32 |
+
# Or use the callable shortcut
|
| 33 |
+
answer = agent(question, image_path)
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
model_id: Optional[str] = None,
|
| 39 |
+
provider: Optional[str] = "novita",
|
| 40 |
+
temperature: float = 0.2,
|
| 41 |
+
api_key: Optional[str] = None,
|
| 42 |
+
):
|
| 43 |
+
"""
|
| 44 |
+
Initializes the ImageAgent.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
model_id: The repository ID of the model to use.
|
| 48 |
+
provider: The provider for the InferenceClient.
|
| 49 |
+
temperature: The generation temperature.
|
| 50 |
+
api_key: The API key. Defaults to os.environ["HF_TOKEN"].
|
| 51 |
+
"""
|
| 52 |
+
self.model_id = model_id or "meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
| 53 |
+
self.temperature = temperature
|
| 54 |
+
|
| 55 |
+
# Get API key from arg or environment, raising an error if missing
|
| 56 |
+
self.api_key = api_key or os.environ.get("HF_TOKEN")
|
| 57 |
+
if not self.api_key:
|
| 58 |
+
raise ValueError(
|
| 59 |
+
"API key not found. Please set the HF_TOKEN environment variable "
|
| 60 |
+
"or pass it as 'api_key' to the ImageAgent."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Initialize the client
|
| 64 |
+
self.client = InferenceClient(
|
| 65 |
+
provider=provider,
|
| 66 |
+
api_key=self.api_key,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# ---- Public API ----
|
| 70 |
+
|
| 71 |
+
def answer(self, question: str, image_path: str) -> str:
|
| 72 |
+
"""
|
| 73 |
+
Run the agent to answer a question about an image.
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
question: The text prompt to ask the model.
|
| 77 |
+
image_path: The local file path to the image.
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
The parsed "Final Answer" string from the model.
|
| 81 |
+
"""
|
| 82 |
+
# 1. Convert image to data URL
|
| 83 |
+
try:
|
| 84 |
+
image_data_url = self._to_data_url(image_path)
|
| 85 |
+
except FileNotFoundError:
|
| 86 |
+
return f"Error: Image file not found at {image_path}"
|
| 87 |
+
except Exception as e:
|
| 88 |
+
return f"Error processing image: {e}"
|
| 89 |
+
|
| 90 |
+
# 2. Build the messages payload
|
| 91 |
+
messages = self._build_messages(question, image_data_url)
|
| 92 |
+
|
| 93 |
+
# 3. Call the API
|
| 94 |
+
try:
|
| 95 |
+
completion = self.client.chat.completions.create(
|
| 96 |
+
model=self.model_id,
|
| 97 |
+
messages=messages,
|
| 98 |
+
temperature=self.temperature,
|
| 99 |
+
)
|
| 100 |
+
response_text = completion.choices[0].message.content
|
| 101 |
+
except Exception as e:
|
| 102 |
+
return f"Error calling API: {e}"
|
| 103 |
+
|
| 104 |
+
# 4. Parse and return the final answer
|
| 105 |
+
return self._parse_answer(response_text)
|
| 106 |
+
|
| 107 |
+
def __call__(self, question: str, image_path: str) -> str:
|
| 108 |
+
"""Callable shortcut for self.answer()."""
|
| 109 |
+
return self.answer(question, image_path)
|
| 110 |
+
|
| 111 |
+
# ---- Internals ----
|
| 112 |
+
|
| 113 |
+
def _to_data_url(self, path: str) -> str:
|
| 114 |
+
"""Converts a local file path to a base64 data URL."""
|
| 115 |
+
p = Path(path)
|
| 116 |
+
if not p.exists():
|
| 117 |
+
raise FileNotFoundError(f"No file found at {path}")
|
| 118 |
+
|
| 119 |
+
mime = mimetypes.guess_type(str(p))[0] or "image/png"
|
| 120 |
+
b64 = base64.b64encode(p.read_bytes()).decode("utf-8")
|
| 121 |
+
return f"data:{mime};base64,{b64}"
|
| 122 |
+
|
| 123 |
+
def _build_messages(self, question: str, image_data_url: str) -> List[Dict[str, Any]]:
|
| 124 |
+
"""Constructs the message payload for the API."""
|
| 125 |
+
return [
|
| 126 |
+
{
|
| 127 |
+
"role": "system",
|
| 128 |
+
"content": [
|
| 129 |
+
{"type": "text", "text": AGENT_SYSTEM_PROMPT},
|
| 130 |
+
],
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"role": "user",
|
| 134 |
+
"content": [
|
| 135 |
+
{"type": "text", "text": question},
|
| 136 |
+
{
|
| 137 |
+
"type": "image_url",
|
| 138 |
+
"image_url": {"url": image_data_url},
|
| 139 |
+
},
|
| 140 |
+
],
|
| 141 |
+
},
|
| 142 |
+
]
|
| 143 |
+
|
| 144 |
+
def _parse_answer(self, response: str) -> str:
|
| 145 |
+
"""Extracts the text after 'FINAL ANSWER: '."""
|
| 146 |
+
try:
|
| 147 |
+
# Use rpartition for robustness, splits on the *last* occurrence
|
| 148 |
+
_before, _marker, final_answer = response.rpartition("FINAL ANSWER: ")
|
| 149 |
+
|
| 150 |
+
if _marker:
|
| 151 |
+
return final_answer.strip()
|
| 152 |
+
else:
|
| 153 |
+
# If marker isn't found, return the whole response with a warning
|
| 154 |
+
print("Warning: Could not find 'FINAL ANSWER:' marker. Returning full response.")
|
| 155 |
+
return response.strip()
|
| 156 |
+
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"Error parsing response: {e}")
|
| 159 |
+
return response.strip()
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# ---- CLI Entrypoint ----
|
| 163 |
+
def main():
|
| 164 |
+
"""
|
| 165 |
+
Allows running the agent from the command line.
|
| 166 |
+
|
| 167 |
+
Example:
|
| 168 |
+
python image_agent.py "What move is this?" "path/to/my_image.png"
|
| 169 |
+
"""
|
| 170 |
+
parser = argparse.ArgumentParser(description="Class-based agent for image questions.")
|
| 171 |
+
parser.add_argument("question", type=str, help="Your question (quoted).")
|
| 172 |
+
parser.add_argument("image_path", type=str, help="Path to the image file.")
|
| 173 |
+
args = parser.parse_args()
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
image_agent = ImageAgent()
|
| 177 |
+
answer = image_agent.answer(args.question, args.image_path)
|
| 178 |
+
print(answer)
|
| 179 |
+
except Exception as e:
|
| 180 |
+
print(f"An error occurred: {e}")
|
| 181 |
+
|
| 182 |
+
if __name__ == "__main__":
|
| 183 |
+
main()
|
hf_agent_course_final_assignment/packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
stockfish
|
hf_agent_course_final_assignment/python_agent.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import base64
|
| 5 |
+
import mimetypes
|
| 6 |
+
import argparse
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import List, Dict, Any, Optional
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from huggingface_hub import InferenceClient
|
| 11 |
+
import io
|
| 12 |
+
from contextlib import redirect_stdout
|
| 13 |
+
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
class PythonAgent:
|
| 17 |
+
def __init__(self):
|
| 18 |
+
# print("Python code execution agent initialized.")
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
def __call__(self, file_path: str) -> str:
|
| 22 |
+
return self.execute_python_file(file_path)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def execute_python_file(self, file_path: str) -> str:
|
| 26 |
+
import io
|
| 27 |
+
from contextlib import redirect_stdout
|
| 28 |
+
|
| 29 |
+
with open(file_path, "r") as f_in:
|
| 30 |
+
code_to_exec = f_in.read()
|
| 31 |
+
|
| 32 |
+
# important: real module-like globals with __name__="__main__"
|
| 33 |
+
sandbox_globals = {
|
| 34 |
+
"__name__": "__main__",
|
| 35 |
+
"__file__": file_path,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
buf = io.StringIO()
|
| 39 |
+
with redirect_stdout(buf):
|
| 40 |
+
try:
|
| 41 |
+
exec(code_to_exec, sandbox_globals)
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"An error occurred: {e}")
|
| 44 |
+
|
| 45 |
+
raw_response = buf.getvalue()
|
| 46 |
+
# the response is the last line
|
| 47 |
+
response = raw_response.strip().splitlines()[-1]
|
| 48 |
+
return response
|
| 49 |
+
|
| 50 |
+
# path_python_file = r"downloaded\f918266a-b3e0-4914-865d-4faa564f1aef.py"
|
| 51 |
+
# response = execute_python_file(path_python_file)
|
| 52 |
+
# print(response)
|
hf_agent_course_final_assignment/requirements.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
+
requests>=2.32.0
|
| 3 |
+
pandas>=2.2.2
|
| 4 |
+
|
| 5 |
+
# LangChain stack
|
| 6 |
+
langchain==0.3.27
|
| 7 |
+
langchain-core==0.3.79
|
| 8 |
+
langchain-community==0.3.31
|
| 9 |
+
langchain-huggingface>=0.1.0
|
| 10 |
+
huggingface_hub==0.35.3
|
| 11 |
+
transformers==4.57.1
|
| 12 |
+
pytorch
|
| 13 |
+
|
| 14 |
+
pillow==11.3.0
|
| 15 |
+
ddgs==9.6.1
|
| 16 |
+
beautifulsoup4>=4.12.3
|
| 17 |
+
duckduckgo-search>=6.2.5
|
| 18 |
+
wikipedia>=1.4.0
|
| 19 |
+
youtube-transcript-api==1.2.3
|
| 20 |
+
faster-whisper>=1.0.3
|
| 21 |
+
openpyxl>=3.1.5
|
| 22 |
+
python-dotenv==1.1.1
|
| 23 |
+
bs4>=0.0.2
|
| 24 |
+
lxml>=5.3.0
|
| 25 |
+
|
| 26 |
+
openpyxl==3.1.5
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# accelerate
|
hf_agent_course_final_assignment/test_agents.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from web_agent import WebAgent
|
| 2 |
+
from utils.fetch_files import fetch_files_gaia_validation_hub
|
| 3 |
+
from image_agent import ImageAgent
|
| 4 |
+
from audio_agent import AudioAgent
|
| 5 |
+
from python_agent import PythonAgent
|
| 6 |
+
from excel_agent import ExcelAgent
|
| 7 |
+
from youtube_agent import YouTubeSubsAgent
|
| 8 |
+
from gaia_agent import GaiaAgent
|
| 9 |
+
|
| 10 |
+
# ## testing the web agent alone
|
| 11 |
+
# test_question_web = "What is the surname of the equine veterinarian mentioned in 1.E Exercises from the chemistry materials licensed by Marisa Alviar-Agnew & Henry Agnew under the CK-12 license in LibreText's Introductory Chemistry materials as compiled 08/21/2023?"
|
| 12 |
+
# webagent_instance = WebAgent()
|
| 13 |
+
# webagent_answer = webagent_instance(test_question_web)
|
| 14 |
+
|
| 15 |
+
## testing the image agent
|
| 16 |
+
# test_question_image = "Review the chess position provided in the image. It is black's turn. Provide the correct next move for black which guarantees a win. Please provide your response in algebraic notation."
|
| 17 |
+
# file_name = "cca530fc-4052-43b2-b130-b30968d8aa44.png"
|
| 18 |
+
# img_path = fetch_files_gaia_validation_hub(file_name)
|
| 19 |
+
# print(img_path)
|
| 20 |
+
# print("------- Image Agent ------------------")
|
| 21 |
+
# imageagent_instance = ImageAgent()
|
| 22 |
+
# imageagent_answer = imageagent_instance(test_question_image, img_path)
|
| 23 |
+
# print(imageagent_answer)
|
| 24 |
+
|
| 25 |
+
## testing the audio agent
|
| 26 |
+
# test_question_audio = "Hi, I'm making a pie but I could use some help with my shopping list. I have everything I need for the crust, but I'm not sure about the filling. I got the recipe from my friend Aditi, but she left it as a voice memo and the speaker on my phone is buzzing so I can't quite make out what she's saying. Could you please listen to the recipe and list all of the ingredients that my friend described? I only want the ingredients for the filling, as I have everything I need to make my favorite pie crust. I've attached the recipe as Strawberry pie.mp3.\n\nIn your response, please only list the ingredients, not any measurements. So if the recipe calls for \"a pinch of salt\" or \"two cups of ripe strawberries\" the ingredients on the list would be \"salt\" and \"ripe strawberries\".\n\nPlease format your response as a comma separated list of ingredients. Also, please alphabetize the ingredients."
|
| 27 |
+
# file_name = "99c9cc74-fdc8-46c6-8f8d-3ce2d3bfeea3.mp3"
|
| 28 |
+
# video_path = fetch_files_gaia_validation_hub(file_name)
|
| 29 |
+
# print(video_path)
|
| 30 |
+
|
| 31 |
+
# audioagent_instance = AudioAgent()
|
| 32 |
+
# audioagent_answer = audioagent_instance(test_question_audio, video_path)
|
| 33 |
+
# print(audioagent_answer)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
## Testing the python code execution agent
|
| 37 |
+
# test_question_python = "What is the final numeric output from the attached Python code?"
|
| 38 |
+
# file_name = "f918266a-b3e0-4914-865d-4faa564f1aef.py"
|
| 39 |
+
# script_path = fetch_files_gaia_validation_hub(file_name)
|
| 40 |
+
# print(script_path)
|
| 41 |
+
# pythonagent_instance = PythonAgent()
|
| 42 |
+
# pythonagent_answer = pythonagent_instance(script_path)
|
| 43 |
+
# print(pythonagent_answer)
|
| 44 |
+
|
| 45 |
+
## Testing the excel agent
|
| 46 |
+
# test_question_excel = "The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places."
|
| 47 |
+
# file_name = "7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx"
|
| 48 |
+
# excel_path = fetch_files_gaia_validation_hub(file_name)
|
| 49 |
+
# print(excel_path)
|
| 50 |
+
# excelagent_instance = ExcelAgent()
|
| 51 |
+
# excelagent_answer = excelagent_instance(test_question_excel, excel_path)
|
| 52 |
+
# print(excelagent_answer)
|
| 53 |
+
|
| 54 |
+
## Testing youtube agent
|
| 55 |
+
# youtube_agent = YouTubeSubsAgent()
|
| 56 |
+
# ans = youtube_agent(
|
| 57 |
+
# "What does Teal'c say in response to the question \"Isn't that hot?\"",
|
| 58 |
+
# "https://www.youtube.com/watch?v=1htKBjuUWec"
|
| 59 |
+
# )
|
| 60 |
+
# print(ans)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
gaia_agent = GaiaAgent()
|
| 64 |
+
|
| 65 |
+
# q = "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia."
|
| 66 |
+
# ans = gaia_agent(q, "")
|
| 67 |
+
|
| 68 |
+
# q = "Examine the video at https://www.youtube.com/watch?v=1htKBjuUWec. What does Teal'c say in response to the question \"Isn't that hot?\""
|
| 69 |
+
# ans = gaia_agent(q, "")
|
| 70 |
+
|
| 71 |
+
# file_img = fetch_files_gaia_validation_hub("cca530fc-4052-43b2-b130-b30968d8aa44.png")
|
| 72 |
+
# q = "Review the chess position provided in the image. It is black's turn. Provide the correct next move for black which guarantees a win. Please provide your response in algebraic notation."
|
| 73 |
+
# ans = gaia_agent(q, file_img)
|
| 74 |
+
|
| 75 |
+
# file_audio = fetch_files_gaia_validation_hub("1f975693-876d-457b-a649-393859e79bf3.mp3")
|
| 76 |
+
# q = "Hi, I was out sick from my classes on Friday, so I'm trying to figure out what I need to study for my Calculus mid-term next week. My friend from class sent me an audio recording of Professor Willowbrook giving out the recommended reading for the test, but my headphones are broken :(\n\nCould you please listen to the recording for me and tell me the page numbers I'm supposed to go over? I've attached a file called Homework.mp3 that has the recording. Please provide just the page numbers as a comma-delimited list. And please provide the list in ascending order."
|
| 77 |
+
# ans = gaia_agent(q, file_audio)
|
| 78 |
+
|
| 79 |
+
# file_xlsx = fetch_files_gaia_validation_hub("7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx")
|
| 80 |
+
# q = "The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places."
|
| 81 |
+
# ans = gaia_agent(q, file_xlsx)
|
| 82 |
+
|
| 83 |
+
# file_py = fetch_files_gaia_validation_hub("f918266a-b3e0-4914-865d-4faa564f1aef.py")
|
| 84 |
+
# q = "What is the final numeric output from the attached Python code?"
|
| 85 |
+
# ans = gaia_agent(q, file_py)
|
| 86 |
+
|
| 87 |
+
q = ".rewsna eht sa \"tfel\" drow eht fo etisoppo eht etirw ,ecnetnes siht dnatsrednu uoy fI"
|
| 88 |
+
ans = gaia_agent(q, "")
|
| 89 |
+
|
| 90 |
+
print(ans)
|
| 91 |
+
|
| 92 |
+
# from huggingface_hub import whoami
|
| 93 |
+
# whoami()
|
hf_agent_course_final_assignment/utils/fetch_files.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" Function to fetch files from the GAIA (validation set) in huggingface repo. """
|
| 2 |
+
import os, requests, huggingface_hub
|
| 3 |
+
from huggingface_hub import hf_hub_download
|
| 4 |
+
|
| 5 |
+
def fetch_files_gaia_validation_hub(filename: str):
|
| 6 |
+
"""
|
| 7 |
+
Fetch a file from the GAIA validation set hosted on Hugging Face Hub.
|
| 8 |
+
Args:
|
| 9 |
+
filename (str): The name of the file to fetch (e.g., 'question_1.json').
|
| 10 |
+
Returns:
|
| 11 |
+
str: Local path to the downloaded file.
|
| 12 |
+
"""
|
| 13 |
+
if not filename:
|
| 14 |
+
raise ValueError("Please provide a valid filename.")
|
| 15 |
+
|
| 16 |
+
repo_id = "gaia-benchmark/GAIA"
|
| 17 |
+
repo_type = "dataset"
|
| 18 |
+
path_in_repo = f"2023/validation/{filename}"
|
| 19 |
+
|
| 20 |
+
# If you need a private token: set HF_TOKEN env var or pass token=...
|
| 21 |
+
downloaded_path = hf_hub_download(
|
| 22 |
+
repo_id=repo_id,
|
| 23 |
+
repo_type=repo_type,
|
| 24 |
+
filename=path_in_repo,
|
| 25 |
+
# token=os.getenv("HF_TOKEN"), # uncomment if needed
|
| 26 |
+
force_download=False
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Copy into ./downloaded to keep a consistent local layout
|
| 30 |
+
os.makedirs("downloaded", exist_ok=True)
|
| 31 |
+
local_path = os.path.join("downloaded", filename)
|
| 32 |
+
if os.path.abspath(downloaded_path) != os.path.abspath(local_path):
|
| 33 |
+
# Lightweight copy
|
| 34 |
+
with open(downloaded_path, "rb") as src, open(local_path, "wb") as dst:
|
| 35 |
+
dst.write(src.read())
|
| 36 |
+
|
| 37 |
+
return local_path
|
hf_agent_course_final_assignment/web_agent.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# web_agent.py
|
| 2 |
+
# Minimal web-searching LangChain agent for GAIA-style questions.
|
| 3 |
+
# - Tools: DuckDuckGo search + HTTP GET
|
| 4 |
+
# - Model: defaults to a lightweight reasoning-ready open model (Hugging Face), but you can
|
| 5 |
+
# swap to any chat model supported by LangChain. If you don't set HF token, we'll
|
| 6 |
+
# fall back to a simple reasoning prompt with the same interface.
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import os
|
| 12 |
+
from typing import List, Optional, Dict, Any
|
| 13 |
+
|
| 14 |
+
# LangChain core (v0.2+ / v0.3)
|
| 15 |
+
from langchain.agents import AgentExecutor, create_react_agent
|
| 16 |
+
from langchain_core.prompts import PromptTemplate
|
| 17 |
+
from langchain_core.runnables import RunnableConfig
|
| 18 |
+
from langchain_community.utilities import RequestsWrapper
|
| 19 |
+
from langchain.tools import Tool
|
| 20 |
+
|
| 21 |
+
# Chat model: Hugging Face Endpoint (no key required to run, but highly recommended)
|
| 22 |
+
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
| 23 |
+
|
| 24 |
+
# Community tools
|
| 25 |
+
from langchain_community.tools import DuckDuckGoSearchRun, RequestsGetTool
|
| 26 |
+
|
| 27 |
+
# ---- Prompt (self-contained: no hub dependency) ----
|
| 28 |
+
REACT_PROMPT = """You are a meticulous research assistant for GAIA-style fact questions.
|
| 29 |
+
|
| 30 |
+
You have access to these tools:
|
| 31 |
+
{tools}
|
| 32 |
+
|
| 33 |
+
Use this format:
|
| 34 |
+
|
| 35 |
+
Question: the input question to answer
|
| 36 |
+
Thought: think about what to search or read next
|
| 37 |
+
Action: one of [{tool_names}]
|
| 38 |
+
Action Input: the input to the action
|
| 39 |
+
Observation: the result of the action
|
| 40 |
+
... (repeat Thought/Action/Observation as needed)
|
| 41 |
+
Thought: I can now answer.
|
| 42 |
+
Final Answer: <the shortest exact answer>
|
| 43 |
+
|
| 44 |
+
Follow the rules:
|
| 45 |
+
- Prefer precise quotes from primary/official sources when possible.
|
| 46 |
+
- Keep the final answer SHORT (a few words if possible)
|
| 47 |
+
- If you need multiple pages, open them sequentially.
|
| 48 |
+
- Never repeat the exact same Action Input twice. If an Action returns a login, auth, or error page, change strategy (use search with site: filters, try a different public page, or a cached/mirrored source).
|
| 49 |
+
- If a URL contains "login", "@app/auth", or "returnto=", do NOT open it; search for a public page instead.
|
| 50 |
+
|
| 51 |
+
Question: {input}
|
| 52 |
+
{agent_scratchpad}"""
|
| 53 |
+
|
| 54 |
+
# Final Answer: <the shortest exact answer> — <single best source URL>
|
| 55 |
+
# - Keep the final answer SHORT (a few words if possible) plus one URL.
|
| 56 |
+
class WebAgent:
|
| 57 |
+
"""Encapsulated GAIA-style web agent.
|
| 58 |
+
|
| 59 |
+
Usage:
|
| 60 |
+
web_agent = WebAgent()
|
| 61 |
+
result = web_agent("Who is the CEO of ...?")
|
| 62 |
+
# or
|
| 63 |
+
result = web_agent.answer("...")
|
| 64 |
+
|
| 65 |
+
You can also run as a script:
|
| 66 |
+
python web_agent.py "Your question here"
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def __init__(
|
| 70 |
+
self,
|
| 71 |
+
hf_repo_id: Optional[str] = None,
|
| 72 |
+
max_new_tokens: int = 2048,
|
| 73 |
+
do_sample: bool = False,
|
| 74 |
+
repetition_penalty: float = 1.05,
|
| 75 |
+
max_iterations: int = 20,
|
| 76 |
+
verbose: bool = True,
|
| 77 |
+
user_agent: str = "Mozilla/5.0 (compatible; GAIA/1.0; +https://example.org/bot)",
|
| 78 |
+
):
|
| 79 |
+
self.hf_repo_id = hf_repo_id or os.environ.get("HF_REPO_ID", "Qwen/Qwen2.5-7B-Instruct")
|
| 80 |
+
self.max_new_tokens = max_new_tokens
|
| 81 |
+
self.do_sample = do_sample
|
| 82 |
+
self.repetition_penalty = repetition_penalty
|
| 83 |
+
self.max_iterations = max_iterations
|
| 84 |
+
self.verbose = verbose
|
| 85 |
+
self.user_agent = user_agent
|
| 86 |
+
|
| 87 |
+
# Built lazily so each call gets a fresh per-run "seen URL" set for the guarded GET.
|
| 88 |
+
self._executor: Optional[AgentExecutor] = None
|
| 89 |
+
self._tools: Optional[List[Tool]] = None
|
| 90 |
+
|
| 91 |
+
# ---- Public API ----
|
| 92 |
+
def answer(self, question: str) -> str:
|
| 93 |
+
"""Run the agent and return the model's 'Final Answer' string."""
|
| 94 |
+
executor = self._build_agent() # build fresh each time to reset guarded state
|
| 95 |
+
result: Dict[str, Any] = executor.invoke({"input": question})
|
| 96 |
+
return result["output"]
|
| 97 |
+
|
| 98 |
+
def __call__(self, question: str) -> str:
|
| 99 |
+
return self.answer(question)
|
| 100 |
+
|
| 101 |
+
# ---- Internals ----
|
| 102 |
+
def _build_model(self) -> ChatHuggingFace:
|
| 103 |
+
llm = HuggingFaceEndpoint(
|
| 104 |
+
repo_id=self.hf_repo_id,
|
| 105 |
+
task="text-generation",
|
| 106 |
+
max_new_tokens=self.max_new_tokens,
|
| 107 |
+
do_sample=self.do_sample,
|
| 108 |
+
repetition_penalty=self.repetition_penalty,
|
| 109 |
+
)
|
| 110 |
+
return ChatHuggingFace(llm=llm)
|
| 111 |
+
|
| 112 |
+
def _build_tools(self) -> List[Tool]:
|
| 113 |
+
wrapper = RequestsWrapper(headers={"User-Agent": self.user_agent})
|
| 114 |
+
raw_get = RequestsGetTool(
|
| 115 |
+
name="Requests Get (raw)",
|
| 116 |
+
requests_wrapper=wrapper,
|
| 117 |
+
allow_dangerous_requests=True,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
seen: set[str] = set()
|
| 121 |
+
|
| 122 |
+
def guarded_get(url: str) -> str:
|
| 123 |
+
low = url.lower()
|
| 124 |
+
if any(x in low for x in ["@app/auth", "login?returnto=", "/login"]):
|
| 125 |
+
return ("BLOCKED: This looks like a login/auth wall. "
|
| 126 |
+
"Use search to find a public or cached page instead.")
|
| 127 |
+
if url in seen:
|
| 128 |
+
return (f"BLOCKED: Already fetched {url}. "
|
| 129 |
+
"Choose a different URL or try a different query.")
|
| 130 |
+
seen.add(url)
|
| 131 |
+
return raw_get.run(url)
|
| 132 |
+
|
| 133 |
+
return [
|
| 134 |
+
DuckDuckGoSearchRun(name="DuckDuckGo Search"),
|
| 135 |
+
Tool.from_function(
|
| 136 |
+
name="Requests Get",
|
| 137 |
+
func=guarded_get,
|
| 138 |
+
description="HTTP GET for public pages; prevents repeats and login walls.",
|
| 139 |
+
),
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
def _build_agent(self) -> AgentExecutor:
|
| 143 |
+
model = self._build_model()
|
| 144 |
+
tools = self._build_tools()
|
| 145 |
+
prompt = PromptTemplate.from_template(REACT_PROMPT)
|
| 146 |
+
|
| 147 |
+
agent = create_react_agent(model, tools, prompt)
|
| 148 |
+
executor = AgentExecutor(
|
| 149 |
+
agent=agent,
|
| 150 |
+
tools=tools,
|
| 151 |
+
max_iterations=self.max_iterations,
|
| 152 |
+
handle_parsing_errors=True,
|
| 153 |
+
verbose=self.verbose,
|
| 154 |
+
)
|
| 155 |
+
return executor
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# def main():
|
| 159 |
+
# parser = argparse.ArgumentParser(description="Simple LangChain web agent for GAIA-style questions.")
|
| 160 |
+
# parser.add_argument("question", type=str, help="Your question (quoted).")
|
| 161 |
+
# args = parser.parse_args()
|
| 162 |
+
|
| 163 |
+
# print(answer(args.question))
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# if __name__ == "__main__":
|
| 167 |
+
# main()
|
| 168 |
+
|
| 169 |
+
# ---- CLI Entrypoint ----
|
| 170 |
+
def main():
|
| 171 |
+
parser = argparse.ArgumentParser(description="Class-based LangChain web agent for GAIA-style questions.")
|
| 172 |
+
parser.add_argument("question", type=str, help="Your question (quoted).")
|
| 173 |
+
args = parser.parse_args()
|
| 174 |
+
|
| 175 |
+
web_agent = WebAgent()
|
| 176 |
+
print(web_agent.answer(args.question))
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
if __name__ == "__main__":
|
| 180 |
+
main()
|
hf_agent_course_final_assignment/youtube_agent.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# youtube_subs_agent.py
|
| 2 |
+
# GAIA-style YouTube subtitle-based agent using youtube-transcript-api.
|
| 3 |
+
#
|
| 4 |
+
# Usage:
|
| 5 |
+
# pip install youtube-transcript-api python-dotenv huggingface_hub
|
| 6 |
+
#
|
| 7 |
+
# Example:
|
| 8 |
+
# from youtube_subs_agent import YouTubeSubsAgent
|
| 9 |
+
#
|
| 10 |
+
# agent = YouTubeSubsAgent()
|
| 11 |
+
# answer = agent(
|
| 12 |
+
# "What does Teal'c say in response to the question 'Isn't that hot?'",
|
| 13 |
+
# "https://www.youtube.com/watch?v=1htKBjuUWec"
|
| 14 |
+
# )
|
| 15 |
+
# print(answer)
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import os
|
| 20 |
+
from typing import Optional, List, Dict, Any
|
| 21 |
+
from urllib.parse import urlparse, parse_qs
|
| 22 |
+
|
| 23 |
+
from dotenv import load_dotenv
|
| 24 |
+
from youtube_transcript_api import (
|
| 25 |
+
YouTubeTranscriptApi,
|
| 26 |
+
TranscriptsDisabled,
|
| 27 |
+
NoTranscriptFound,
|
| 28 |
+
VideoUnavailable,
|
| 29 |
+
)
|
| 30 |
+
from huggingface_hub import InferenceClient
|
| 31 |
+
|
| 32 |
+
load_dotenv()
|
| 33 |
+
|
| 34 |
+
AGENT_SYSTEM_PROMPT = """You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string."""
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class YouTubeSubsAgent:
|
| 38 |
+
"""
|
| 39 |
+
GAIA-style agent for answering questions about YouTube videos using subtitles.
|
| 40 |
+
|
| 41 |
+
Usage:
|
| 42 |
+
agent = YouTubeSubsAgent()
|
| 43 |
+
answer = agent(
|
| 44 |
+
"What does Teal'c say in response to the question 'Isn't that hot?'",
|
| 45 |
+
"https://www.youtube.com/watch?v=1htKBjuUWec"
|
| 46 |
+
)
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(
|
| 50 |
+
self,
|
| 51 |
+
chat_model_id: Optional[str] = None,
|
| 52 |
+
provider: str = "novita",
|
| 53 |
+
temperature: float = 0.2,
|
| 54 |
+
api_key: Optional[str] = None,
|
| 55 |
+
transcript_char_limit: int = 12000,
|
| 56 |
+
languages: Optional[List[str]] = None,
|
| 57 |
+
):
|
| 58 |
+
"""
|
| 59 |
+
languages: priority list for transcript languages.
|
| 60 |
+
default = English-first fallback.
|
| 61 |
+
"""
|
| 62 |
+
self.chat_model_id = chat_model_id or "meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
| 63 |
+
self.temperature = temperature
|
| 64 |
+
self.transcript_char_limit = transcript_char_limit
|
| 65 |
+
self.languages = languages or ["en", "en-US", "en-GB"]
|
| 66 |
+
|
| 67 |
+
self.api_key = api_key or os.environ.get("HF_TOKEN")
|
| 68 |
+
if not self.api_key:
|
| 69 |
+
raise ValueError(
|
| 70 |
+
"API key not found. Please set the HF_TOKEN environment variable or pass api_key=."
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
self.client = InferenceClient(provider=provider, api_key=self.api_key)
|
| 74 |
+
|
| 75 |
+
# ---- Public API ----
|
| 76 |
+
|
| 77 |
+
def answer(self, question: str, video_url: str) -> str:
|
| 78 |
+
"""Main call method: question + YouTube URL."""
|
| 79 |
+
transcript_text = self._fetch_transcript_text(video_url)
|
| 80 |
+
prompt = self._build_reasoning_prompt(question, transcript_text)
|
| 81 |
+
response = self._run_chat(prompt)
|
| 82 |
+
return self._parse_final_answer(response)
|
| 83 |
+
|
| 84 |
+
def __call__(self, question: str, video_url: str) -> str:
|
| 85 |
+
"""Shortcut for answer()."""
|
| 86 |
+
return self.answer(question, video_url)
|
| 87 |
+
|
| 88 |
+
# ---- Internals ----
|
| 89 |
+
|
| 90 |
+
def _extract_video_id(self, url: str) -> str:
|
| 91 |
+
"""
|
| 92 |
+
Extracts the video ID from common YouTube URL formats.
|
| 93 |
+
Falls back to returning the input if it looks like a bare ID already.
|
| 94 |
+
"""
|
| 95 |
+
parsed = urlparse(url)
|
| 96 |
+
|
| 97 |
+
# Short link: https://youtu.be/VIDEOID
|
| 98 |
+
if parsed.hostname in ("youtu.be",):
|
| 99 |
+
# path is like "/VIDEOID"
|
| 100 |
+
return parsed.path.strip("/")
|
| 101 |
+
|
| 102 |
+
# Standard watch link: https://www.youtube.com/watch?v=VIDEOID
|
| 103 |
+
if parsed.hostname in ("www.youtube.com", "youtube.com", "m.youtube.com"):
|
| 104 |
+
qs = parse_qs(parsed.query)
|
| 105 |
+
if "v" in qs:
|
| 106 |
+
return qs["v"][0]
|
| 107 |
+
|
| 108 |
+
# Fallback: assume caller already passed just the ID
|
| 109 |
+
return url
|
| 110 |
+
|
| 111 |
+
def _fetch_transcript_text(self, video_url: str) -> str:
|
| 112 |
+
"""
|
| 113 |
+
Fetch transcript with youtube-transcript-api, format it as
|
| 114 |
+
MM:SS text
|
| 115 |
+
one line per snippet, and truncate if needed.
|
| 116 |
+
"""
|
| 117 |
+
video_id = self._extract_video_id(video_url)
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
fetched = YouTubeTranscriptApi().fetch(
|
| 121 |
+
video_id,
|
| 122 |
+
languages=self.languages,
|
| 123 |
+
)
|
| 124 |
+
except (TranscriptsDisabled, NoTranscriptFound, VideoUnavailable) as e:
|
| 125 |
+
print(f"Warning: no transcript available for this video ({e}).")
|
| 126 |
+
return ""
|
| 127 |
+
|
| 128 |
+
# IMPORTANT FIX:
|
| 129 |
+
# 'fetched' is a FetchedTranscript, which is iterable, but each element
|
| 130 |
+
# is a FetchedTranscriptSnippet (attributes, not dicts).
|
| 131 |
+
# Easiest: convert to raw dicts first.
|
| 132 |
+
raw_snippets = fetched.to_raw_data()
|
| 133 |
+
|
| 134 |
+
def format_ts(seconds_float: float) -> str:
|
| 135 |
+
seconds_int = int(seconds_float)
|
| 136 |
+
mm = seconds_int // 60
|
| 137 |
+
ss = seconds_int % 60
|
| 138 |
+
return f"{mm:02d}:{ss:02d}"
|
| 139 |
+
|
| 140 |
+
lines: List[str] = []
|
| 141 |
+
for snip in raw_snippets:
|
| 142 |
+
start_ts = format_ts(snip["start"])
|
| 143 |
+
text = snip["text"]
|
| 144 |
+
# Collapse internal newlines that YouTube sometimes puts in a single snippet
|
| 145 |
+
text = text.replace("\n", " ").strip()
|
| 146 |
+
if text:
|
| 147 |
+
lines.append(f"{start_ts} {text}")
|
| 148 |
+
|
| 149 |
+
transcript_text = "\n".join(lines)
|
| 150 |
+
|
| 151 |
+
if len(transcript_text) > self.transcript_char_limit:
|
| 152 |
+
transcript_text = transcript_text[: self.transcript_char_limit] + "\n[truncated]"
|
| 153 |
+
|
| 154 |
+
return transcript_text
|
| 155 |
+
|
| 156 |
+
def _build_reasoning_prompt(self, question: str, transcript_text: str) -> str:
|
| 157 |
+
"""
|
| 158 |
+
Create the reasoning prompt for the LLM.
|
| 159 |
+
"""
|
| 160 |
+
return (
|
| 161 |
+
"You will be given a question about a YouTube video's dialogue and its transcript.\n"
|
| 162 |
+
"Answer ONLY using what is said in the transcript. If the user asks for an exact quote, "
|
| 163 |
+
"repeat the line(s) exactly as spoken.\n\n"
|
| 164 |
+
"----- QUESTION -----\n"
|
| 165 |
+
f"{question}\n"
|
| 166 |
+
"----- END QUESTION -----\n\n"
|
| 167 |
+
"----- TRANSCRIPT -----\n"
|
| 168 |
+
f"{transcript_text if transcript_text else '[no transcript available]'}\n"
|
| 169 |
+
"----- END TRANSCRIPT -----\n"
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
def _run_chat(self, prompt: str) -> str:
|
| 173 |
+
"""
|
| 174 |
+
Send the reasoning prompt to the chat model.
|
| 175 |
+
"""
|
| 176 |
+
messages: List[Dict[str, Any]] = [
|
| 177 |
+
{"role": "system", "content": AGENT_SYSTEM_PROMPT},
|
| 178 |
+
{"role": "user", "content": prompt},
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
+
completion = self.client.chat.completions.create(
|
| 182 |
+
model=self.chat_model_id,
|
| 183 |
+
messages=messages,
|
| 184 |
+
temperature=self.temperature,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
return completion.choices[0].message.content
|
| 188 |
+
|
| 189 |
+
def _parse_final_answer(self, response: str) -> str:
|
| 190 |
+
"""
|
| 191 |
+
Extract 'FINAL ANSWER: ...' just like in your other agents.
|
| 192 |
+
"""
|
| 193 |
+
_before, _marker, final_answer = response.rpartition("FINAL ANSWER: ")
|
| 194 |
+
if _marker:
|
| 195 |
+
return final_answer.strip()
|
| 196 |
+
else:
|
| 197 |
+
return response.strip()
|
| 198 |
+
|
| 199 |
+
# ---- CLI Entrypoint ----
|
| 200 |
+
def main():
|
| 201 |
+
import argparse
|
| 202 |
+
parser = argparse.ArgumentParser(description="GAIA-style YouTube subtitles agent (youtube-transcript-api).")
|
| 203 |
+
parser.add_argument("question", type=str, help='Your question (quoted).')
|
| 204 |
+
parser.add_argument("video_url", type=str, help="YouTube video URL.")
|
| 205 |
+
args = parser.parse_args()
|
| 206 |
+
|
| 207 |
+
try:
|
| 208 |
+
agent = YouTubeSubsAgent()
|
| 209 |
+
answer = agent(args.question, args.video_url)
|
| 210 |
+
print(answer)
|
| 211 |
+
except Exception as e:
|
| 212 |
+
print(f"An error occurred: {e}")
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
main()
|