Spaces:
Runtime error
Runtime error
Commit ·
bb41dbe
1
Parent(s): 81917a3
Initial version of the agent WIP
Browse files- .gitignore +28 -0
- agent.py +135 -0
- agent_state.py +7 -0
- agent_tools.py +196 -0
- app.py +6 -14
- check_models.py +16 -0
- file_download_tool.py +87 -0
- file_processor_tool.py +185 -0
- logging_config.py +100 -0
- multimodal_demo.py +53 -0
- multimodal_download_tool.py +151 -0
- neighborhood_agent_graph.png +0 -0
- poetry.lock +0 -0
- pyproject.toml +32 -0
- requirements.txt +0 -2
.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
build/
|
| 7 |
+
dist/
|
| 8 |
+
*.egg-info/
|
| 9 |
+
|
| 10 |
+
# Virtual environments
|
| 11 |
+
.env
|
| 12 |
+
.venv
|
| 13 |
+
env/
|
| 14 |
+
venv/
|
| 15 |
+
|
| 16 |
+
# VSCode
|
| 17 |
+
.vscode/
|
| 18 |
+
*.code-workspace
|
| 19 |
+
|
| 20 |
+
# OS files
|
| 21 |
+
.DS_Store
|
| 22 |
+
Thumbs.db
|
| 23 |
+
|
| 24 |
+
# Logs
|
| 25 |
+
*.log
|
| 26 |
+
|
| 27 |
+
# Sandbox folders
|
| 28 |
+
/playground
|
agent.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import TypedDict, Annotated
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
import base64
|
| 5 |
+
import mimetypes
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from langgraph.graph.message import add_messages
|
| 9 |
+
from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage
|
| 10 |
+
from langgraph.graph import START, StateGraph
|
| 11 |
+
from langgraph.prebuilt import ToolNode
|
| 12 |
+
from langgraph.prebuilt import tools_condition
|
| 13 |
+
from langchain_community.tools import DuckDuckGoSearchRun
|
| 14 |
+
import requests
|
| 15 |
+
|
| 16 |
+
from agent_state import AgentState
|
| 17 |
+
from agent_tools import download_file, query_spreadsheet, read_media_file
|
| 18 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 19 |
+
|
| 20 |
+
load_dotenv()
|
| 21 |
+
|
| 22 |
+
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 23 |
+
questions_url = f"{DEFAULT_API_URL}/random-question"
|
| 24 |
+
|
| 25 |
+
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
|
| 26 |
+
search_tool = DuckDuckGoSearchRun()
|
| 27 |
+
tools = [search_tool, download_file, query_spreadsheet, read_media_file]
|
| 28 |
+
llm_with_tools = llm.bind_tools(tools, parallel_tool_calls=False)
|
| 29 |
+
|
| 30 |
+
TOOLS_DESCRIPTION = """
|
| 31 |
+
tool name: search_tool
|
| 32 |
+
description: A tool to search the web for information.
|
| 33 |
+
input: A string with the query to search for.
|
| 34 |
+
output: A string with the search results.
|
| 35 |
+
|
| 36 |
+
tool name: download_file
|
| 37 |
+
description: A tool to download a file from a URL.
|
| 38 |
+
input: A string with the URL of the file to download.
|
| 39 |
+
output: A dictionary with the path to the downloaded file and a dictionary with metadata about the file.
|
| 40 |
+
|
| 41 |
+
tool name: query_spreadsheet
|
| 42 |
+
description: A tool to query a spreadsheet file.
|
| 43 |
+
input: A string with the path to the spreadsheet file and a string with the pandas query to execute, the dataframe is called "df". For example: "df['Burguers'].sum()"
|
| 44 |
+
output: A string with the result of the pandas query.
|
| 45 |
+
|
| 46 |
+
tool name: read_media_file
|
| 47 |
+
description: A tool to read a media file (image or audio).
|
| 48 |
+
input: A string with the path to the media file.
|
| 49 |
+
output: The file content in the format to be used.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
ASSISTANT_PROMPT = f"""
|
| 53 |
+
You are a precise, expert-level answering agent designed to solve a wide range of factual, perceptual, and reasoning-based questions. These questions may involve:
|
| 54 |
+
|
| 55 |
+
* **Information retrieval** from online sources (e.g., Wikipedia)
|
| 56 |
+
* **Analysis of structured data**, such as tables or spreadsheets
|
| 57 |
+
* **Understanding and interpreting images, videos, and audio**
|
| 58 |
+
* **Logical reasoning**, including math, code, and puzzles
|
| 59 |
+
* **Linguistic manipulation**, such as reversing, translating, or categorizing
|
| 60 |
+
|
| 61 |
+
You have access to the following tools to help you solve the questions:
|
| 62 |
+
|
| 63 |
+
{TOOLS_DESCRIPTION}
|
| 64 |
+
|
| 65 |
+
Read the question carefully, think step by step and formulate a plan to answer the question.
|
| 66 |
+
Then follow the plan to answer the question.
|
| 67 |
+
If necessary, use the tools to answer the question.
|
| 68 |
+
|
| 69 |
+
The questions come in the following format:
|
| 70 |
+
|
| 71 |
+
question: question text
|
| 72 |
+
question_file_url (optional): path to the url to download the file required to answer the question
|
| 73 |
+
|
| 74 |
+
You must follow these strict response rules:
|
| 75 |
+
|
| 76 |
+
1. **Only provide the final answer**, exactly in the format requested by the question.
|
| 77 |
+
2. **Do not explain, comment, or add phrases** like “The answer is” or “Here’s what I found.”
|
| 78 |
+
3. If a question requires a list, a single word, a number, or a specific notation (e.g., algebraic chess move), **match the format exactly**.
|
| 79 |
+
4. **Always prioritize accuracy, formatting, and precision**, since your answers will be evaluated automatically.
|
| 80 |
+
5. Use your tools only when required to complete the task with confidence.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def assistant(state: AgentState) -> AgentState:
|
| 85 |
+
sys_msg = SystemMessage(content=ASSISTANT_PROMPT)
|
| 86 |
+
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
graph = StateGraph(AgentState)
|
| 90 |
+
|
| 91 |
+
graph.add_node("assistant", assistant)
|
| 92 |
+
graph.add_node("tools", ToolNode(tools))
|
| 93 |
+
|
| 94 |
+
graph.add_edge(START, "assistant")
|
| 95 |
+
graph.add_conditional_edges(
|
| 96 |
+
"assistant",
|
| 97 |
+
# If the latest message requires a tool, route to tools, otherwise, provide a direct response
|
| 98 |
+
tools_condition,
|
| 99 |
+
)
|
| 100 |
+
graph.add_edge("tools", "assistant")
|
| 101 |
+
neighborhood_assistant = graph.compile()
|
| 102 |
+
|
| 103 |
+
neighborhood_assistant.get_graph().draw_mermaid_png(
|
| 104 |
+
output_file_path="neighborhood_agent_graph.png"
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
# ===== EXAMPLES OF USAGE =====
|
| 108 |
+
|
| 109 |
+
# Get a random question
|
| 110 |
+
# response = requests.get(questions_url, timeout=5)
|
| 111 |
+
# response.raise_for_status()
|
| 112 |
+
# raw_question_data = response.json()
|
| 113 |
+
|
| 114 |
+
# question_data = {
|
| 115 |
+
# "question": raw_question_data["question"],
|
| 116 |
+
# "question_file_url": f"{DEFAULT_API_URL}/files/{raw_question_data['task_id']}"
|
| 117 |
+
# }
|
| 118 |
+
|
| 119 |
+
# Test question
|
| 120 |
+
question_data = {
|
| 121 |
+
"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.",
|
| 122 |
+
"question_file_url": f"{DEFAULT_API_URL}/files/1f975693-876d-457b-a649-393859e79bf3"
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
# Process the question with the agent
|
| 126 |
+
|
| 127 |
+
user_input = [HumanMessage(json.dumps(question_data))]
|
| 128 |
+
response = neighborhood_assistant.invoke({"messages": user_input})
|
| 129 |
+
final_answer = response['messages'][-1].content
|
| 130 |
+
|
| 131 |
+
print(response)
|
| 132 |
+
print("🤖 Question:")
|
| 133 |
+
print(question_data["question"])
|
| 134 |
+
print("🤖 Agent's Response:")
|
| 135 |
+
print(final_answer)
|
agent_state.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated, TypedDict
|
| 2 |
+
from langgraph.graph.message import add_messages
|
| 3 |
+
from langchain_core.messages import AnyMessage
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class AgentState(TypedDict):
|
| 7 |
+
messages: Annotated[list[AnyMessage], add_messages]
|
agent_tools.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import mimetypes
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from urllib.parse import urlparse
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import requests
|
| 10 |
+
from langchain_core.tools import tool
|
| 11 |
+
|
| 12 |
+
from logging_config import get_logger
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
logger = get_logger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@tool
|
| 20 |
+
def download_file(url: str) -> dict:
|
| 21 |
+
"""Download a file from a URL.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
url: The URL of the file to download.
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
dict: A dictionary containing the path to the downloaded file and a dictionary with metadata about the file.
|
| 28 |
+
"""
|
| 29 |
+
logger.info(f"Downloading file from {url}")
|
| 30 |
+
response = requests.get(url, stream=True)
|
| 31 |
+
response.raise_for_status()
|
| 32 |
+
|
| 33 |
+
parsed = urlparse(url)
|
| 34 |
+
base = os.path.basename(parsed.path)
|
| 35 |
+
file_name, file_extension = os.path.splitext(base)
|
| 36 |
+
file_extension = file_extension.lower()
|
| 37 |
+
|
| 38 |
+
# Si no hay extensión en URL, intentar con Content-Disposition
|
| 39 |
+
if not file_extension:
|
| 40 |
+
cd = response.headers.get('content-disposition', '')
|
| 41 |
+
if cd:
|
| 42 |
+
match = re.search(r'filename\*?=(?:UTF-8\'\')?"?([^\";]+)"?', cd)
|
| 43 |
+
if match:
|
| 44 |
+
fname = os.path.basename(match.group(1))
|
| 45 |
+
name2, ext2 = os.path.splitext(fname)
|
| 46 |
+
if ext2:
|
| 47 |
+
file_name, file_extension = name2, ext2.lower()
|
| 48 |
+
|
| 49 |
+
# Si aún sin extensión, dejar ext vacía
|
| 50 |
+
if not file_name:
|
| 51 |
+
file_name = "downloaded_file"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
filename = f"{file_name}{file_extension}"
|
| 55 |
+
full_path = os.path.join("agent_files", filename)
|
| 56 |
+
|
| 57 |
+
size = 0
|
| 58 |
+
os.makedirs("agent_files", exist_ok=True)
|
| 59 |
+
with open(full_path, "wb") as f:
|
| 60 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 61 |
+
if chunk:
|
| 62 |
+
size += len(chunk)
|
| 63 |
+
f.write(chunk)
|
| 64 |
+
|
| 65 |
+
metadata = {"file_name": file_name,
|
| 66 |
+
"file_extension": file_extension,
|
| 67 |
+
"size_bytes": size,
|
| 68 |
+
"file_path": os.path.abspath(full_path)}
|
| 69 |
+
|
| 70 |
+
if file_extension in (".csv", ".xls", ".xlsx", ".xlsm", ".xlsb", ".ods"):
|
| 71 |
+
try:
|
| 72 |
+
df = pd.read_excel(full_path) if file_extension != ".csv" else pd.read_csv(full_path)
|
| 73 |
+
metadata["type"] = "table"
|
| 74 |
+
metadata["num_rows"], metadata["num_columns"] = df.shape
|
| 75 |
+
metadata["columns"] = [
|
| 76 |
+
{"name": col, "dtype": str(df[col].dtype)} for col in df.columns
|
| 77 |
+
]
|
| 78 |
+
if df.shape[0] >= 1:
|
| 79 |
+
first_row = df.iloc[0].to_dict()
|
| 80 |
+
metadata["first_row"] = first_row
|
| 81 |
+
except Exception:
|
| 82 |
+
pass
|
| 83 |
+
|
| 84 |
+
logger.info(f"File downloaded: {os.path.abspath(full_path)}")
|
| 85 |
+
return {"file_path": os.path.abspath(full_path), "metadata": metadata}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@tool
|
| 89 |
+
def query_spreadsheet(file_path: str, pandas_query: str) -> str:
|
| 90 |
+
"""Execute a pandas query on a spreadsheet file.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
file_path: The path to the spreadsheet file.
|
| 94 |
+
pandas_code: The pandas code to execute.
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
str: The result of the pandas code execution.
|
| 98 |
+
"""
|
| 99 |
+
logger.info(f"Querying spreadsheet: {file_path} with code: {pandas_query}")
|
| 100 |
+
if file_path.endswith(".csv"):
|
| 101 |
+
df = pd.read_csv(file_path)
|
| 102 |
+
elif file_path.endswith((".xls", ".xlsx")):
|
| 103 |
+
df = pd.read_excel(file_path)
|
| 104 |
+
else:
|
| 105 |
+
raise ValueError("Formato no soportado")
|
| 106 |
+
|
| 107 |
+
# Ejecutar el código generado por el LLM
|
| 108 |
+
local_vars = {"df": df}
|
| 109 |
+
try:
|
| 110 |
+
exec("result = " + pandas_query, {}, local_vars)
|
| 111 |
+
result = local_vars["result"]
|
| 112 |
+
logger.info(f"Spreadsheet query result: {result}")
|
| 113 |
+
return result.to_string(index=False) if hasattr(result, "to_string") else str(result)
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.error(f"Error executing pandas query: {e}")
|
| 116 |
+
return f"Error executing pandas query: {e}"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@tool
|
| 120 |
+
def read_media_file(file_path: str) -> list[dict]:
|
| 121 |
+
"""Read a media file (image or audio).
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
file_path: Path to the image or audio file
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
list[dict]: A list of dictionaries with multimodal content.
|
| 128 |
+
"""
|
| 129 |
+
logger.info(f"Reading media file: {file_path}")
|
| 130 |
+
if not os.path.exists(file_path):
|
| 131 |
+
raise FileNotFoundError(f"File not found: {file_path}")
|
| 132 |
+
|
| 133 |
+
# Get MIME type to determine if it's image or audio
|
| 134 |
+
mime_type, _ = mimetypes.guess_type(file_path)
|
| 135 |
+
|
| 136 |
+
if mime_type and mime_type.startswith('image/'):
|
| 137 |
+
return [_encode_image(file_path)]
|
| 138 |
+
elif mime_type and mime_type.startswith('audio/'):
|
| 139 |
+
return [_encode_audio(file_path)]
|
| 140 |
+
else:
|
| 141 |
+
raise ValueError(f"Unsupported file type: {mime_type}")
|
| 142 |
+
|
| 143 |
+
def _encode_image(image_path: str) -> dict:
|
| 144 |
+
"""Encode an image file to base64 format for Gemini model. Supports: PNG, JPEG, WEBP, HEIC, HEIF"""
|
| 145 |
+
logger.info(f"Encoding image file: {image_path}")
|
| 146 |
+
path = Path(image_path)
|
| 147 |
+
if not path.exists():
|
| 148 |
+
raise FileNotFoundError(f"Image file not found: {image_path}")
|
| 149 |
+
|
| 150 |
+
# Get MIME type
|
| 151 |
+
mime_type, _ = mimetypes.guess_type(image_path)
|
| 152 |
+
if not mime_type or not mime_type.startswith('image/'):
|
| 153 |
+
raise ValueError(f"Unsupported image format: {mime_type}")
|
| 154 |
+
|
| 155 |
+
# Read and encode image
|
| 156 |
+
with open(image_path, "rb") as image_file:
|
| 157 |
+
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
| 158 |
+
|
| 159 |
+
logger.info(f"Image encoded: {image_path}")
|
| 160 |
+
return {
|
| 161 |
+
"type": "image_url",
|
| 162 |
+
"image_url": {
|
| 163 |
+
"url": f"data:{mime_type};base64,{encoded_string}"
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
def _encode_audio(audio_path: str) -> dict:
|
| 168 |
+
"""Encode an audio file to base64 format for Gemini model. Supports: MP3, MPEG, MP4, MPG, AVI, WMV, MPEGPS, FLV"""
|
| 169 |
+
logger.info(f"Encoding audio file: {audio_path}")
|
| 170 |
+
path = Path(audio_path)
|
| 171 |
+
if not path.exists():
|
| 172 |
+
raise FileNotFoundError(f"Audio file not found: {audio_path}")
|
| 173 |
+
|
| 174 |
+
# Get MIME type
|
| 175 |
+
mime_type, _ = mimetypes.guess_type(audio_path)
|
| 176 |
+
if not mime_type or not mime_type.startswith('audio/'):
|
| 177 |
+
# Handle common audio formats that might not be detected
|
| 178 |
+
if audio_path.lower().endswith('.mp3'):
|
| 179 |
+
mime_type = 'audio/mpeg'
|
| 180 |
+
elif audio_path.lower().endswith('.wav'):
|
| 181 |
+
mime_type = 'audio/wav'
|
| 182 |
+
elif audio_path.lower().endswith('.m4a'):
|
| 183 |
+
mime_type = 'audio/mp4'
|
| 184 |
+
else:
|
| 185 |
+
raise ValueError(f"Unsupported audio format: {audio_path}")
|
| 186 |
+
|
| 187 |
+
# Read and encode audio
|
| 188 |
+
with open(audio_path, "rb") as audio_file:
|
| 189 |
+
encoded_string = base64.b64encode(audio_file.read()).decode('utf-8')
|
| 190 |
+
|
| 191 |
+
logger.info(f"Audio encoded: {audio_path}")
|
| 192 |
+
return {
|
| 193 |
+
"type": "media",
|
| 194 |
+
"media_type": mime_type,
|
| 195 |
+
"data": encoded_string
|
| 196 |
+
}
|
app.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
import os
|
| 2 |
import gradio as gr
|
| 3 |
import requests
|
| 4 |
-
import inspect
|
| 5 |
import pandas as pd
|
|
|
|
| 6 |
|
| 7 |
# (Keep Constants as is)
|
| 8 |
# --- Constants ---
|
|
@@ -10,19 +10,11 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
|
| 10 |
|
| 11 |
# --- Basic Agent Definition ---
|
| 12 |
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
|
| 13 |
-
class BasicAgent:
|
| 14 |
-
def __init__(self):
|
| 15 |
-
print("BasicAgent initialized.")
|
| 16 |
-
def __call__(self, question: str) -> str:
|
| 17 |
-
print(f"Agent received question (first 50 chars): {question[:50]}...")
|
| 18 |
-
fixed_answer = "This is a default answer."
|
| 19 |
-
print(f"Agent returning fixed answer: {fixed_answer}")
|
| 20 |
-
return fixed_answer
|
| 21 |
-
|
| 22 |
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
| 23 |
-
"""
|
| 24 |
-
|
| 25 |
-
|
|
|
|
| 26 |
"""
|
| 27 |
# --- Determine HF Space Runtime URL and Repo URL ---
|
| 28 |
space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
|
|
@@ -193,4 +185,4 @@ if __name__ == "__main__":
|
|
| 193 |
print("-"*(60 + len(" App Starting ")) + "\n")
|
| 194 |
|
| 195 |
print("Launching Gradio Interface for Basic Agent Evaluation...")
|
| 196 |
-
demo.launch(debug=True, share=False)
|
|
|
|
| 1 |
import os
|
| 2 |
import gradio as gr
|
| 3 |
import requests
|
|
|
|
| 4 |
import pandas as pd
|
| 5 |
+
from agent import BasicAgent
|
| 6 |
|
| 7 |
# (Keep Constants as is)
|
| 8 |
# --- Constants ---
|
|
|
|
| 10 |
|
| 11 |
# --- Basic Agent Definition ---
|
| 12 |
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
| 14 |
+
"""Fetches all questions, runs the BasicAgent on them, submits all answers, and displays the results.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
profile: The profile of the user who is logged in.
|
| 18 |
"""
|
| 19 |
# --- Determine HF Space Runtime URL and Repo URL ---
|
| 20 |
space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
|
|
|
|
| 185 |
print("-"*(60 + len(" App Starting ")) + "\n")
|
| 186 |
|
| 187 |
print("Launching Gradio Interface for Basic Agent Evaluation...")
|
| 188 |
+
demo.launch(debug=True, share=False)
|
check_models.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
# Configure the API key
|
| 8 |
+
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
|
| 9 |
+
|
| 10 |
+
print("Available Gemini models:")
|
| 11 |
+
for model in genai.list_models():
|
| 12 |
+
if 'gemini' in model.name.lower():
|
| 13 |
+
print(f"- {model.name}")
|
| 14 |
+
print(f" Display name: {model.display_name}")
|
| 15 |
+
print(f" Description: {model.description}")
|
| 16 |
+
print()
|
file_download_tool.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import os
|
| 3 |
+
from urllib.parse import urlparse
|
| 4 |
+
from langchain.tools import Tool
|
| 5 |
+
import tempfile
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def download_file(url: str) -> str:
|
| 10 |
+
"""Download a file from a URL and save it locally.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
url: The URL of the file to download.
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
str: The local file path where the file was saved, or an error message.
|
| 17 |
+
"""
|
| 18 |
+
try:
|
| 19 |
+
# Validate URL
|
| 20 |
+
parsed_url = urlparse(url)
|
| 21 |
+
if not parsed_url.scheme or not parsed_url.netloc:
|
| 22 |
+
return f"Error: Invalid URL format: {url}"
|
| 23 |
+
|
| 24 |
+
# Send GET request to download the file
|
| 25 |
+
response = requests.get(url, stream=True, timeout=30)
|
| 26 |
+
response.raise_for_status()
|
| 27 |
+
|
| 28 |
+
# Get filename from URL or Content-Disposition header
|
| 29 |
+
filename = None
|
| 30 |
+
if 'Content-Disposition' in response.headers:
|
| 31 |
+
content_disposition = response.headers['Content-Disposition']
|
| 32 |
+
if 'filename=' in content_disposition:
|
| 33 |
+
filename = content_disposition.split('filename=')[1].strip('"')
|
| 34 |
+
|
| 35 |
+
if not filename:
|
| 36 |
+
# Extract filename from URL
|
| 37 |
+
filename = os.path.basename(parsed_url.path)
|
| 38 |
+
if not filename or '.' not in filename:
|
| 39 |
+
# If no filename, create one based on content type
|
| 40 |
+
content_type = response.headers.get('content-type', '')
|
| 41 |
+
if 'json' in content_type:
|
| 42 |
+
filename = 'downloaded_file.json'
|
| 43 |
+
elif 'csv' in content_type:
|
| 44 |
+
filename = 'downloaded_file.csv'
|
| 45 |
+
elif 'xml' in content_type:
|
| 46 |
+
filename = 'downloaded_file.xml'
|
| 47 |
+
elif 'pdf' in content_type:
|
| 48 |
+
filename = 'downloaded_file.pdf'
|
| 49 |
+
else:
|
| 50 |
+
filename = 'downloaded_file.txt'
|
| 51 |
+
|
| 52 |
+
# Create downloads directory if it doesn't exist
|
| 53 |
+
downloads_dir = Path("downloads")
|
| 54 |
+
downloads_dir.mkdir(exist_ok=True)
|
| 55 |
+
|
| 56 |
+
# Save file to downloads directory
|
| 57 |
+
file_path = downloads_dir / filename
|
| 58 |
+
|
| 59 |
+
# Handle duplicate filenames
|
| 60 |
+
counter = 1
|
| 61 |
+
original_path = file_path
|
| 62 |
+
while file_path.exists():
|
| 63 |
+
stem = original_path.stem
|
| 64 |
+
suffix = original_path.suffix
|
| 65 |
+
file_path = downloads_dir / f"{stem}_{counter}{suffix}"
|
| 66 |
+
counter += 1
|
| 67 |
+
|
| 68 |
+
# Write file content
|
| 69 |
+
with open(file_path, 'wb') as f:
|
| 70 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 71 |
+
f.write(chunk)
|
| 72 |
+
|
| 73 |
+
file_size = file_path.stat().st_size
|
| 74 |
+
return f"File successfully downloaded to: {file_path}\nFile size: {file_size} bytes\nContent type: {response.headers.get('content-type', 'unknown')}"
|
| 75 |
+
|
| 76 |
+
except requests.exceptions.RequestException as e:
|
| 77 |
+
return f"Error downloading file: {str(e)}"
|
| 78 |
+
except Exception as e:
|
| 79 |
+
return f"Unexpected error: {str(e)}"
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# Create the tool
|
| 83 |
+
file_download_tool = Tool(
|
| 84 |
+
name="file_download_tool",
|
| 85 |
+
description="Use this tool to download files from URLs. Provide the URL of the file you want to download.",
|
| 86 |
+
func=download_file
|
| 87 |
+
)
|
file_processor_tool.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import json
|
| 3 |
+
import csv
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from langchain.tools import Tool
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def process_file(file_path: str, query: str = None) -> str:
|
| 9 |
+
"""Process and analyze a file, optionally with a specific query.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
file_path: The path to the file to process.
|
| 13 |
+
query: Optional query to filter or search within the file content.
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
str: Analysis results or file content based on the query.
|
| 17 |
+
"""
|
| 18 |
+
try:
|
| 19 |
+
file_path = Path(file_path)
|
| 20 |
+
|
| 21 |
+
if not file_path.exists():
|
| 22 |
+
return f"Error: File not found at {file_path}"
|
| 23 |
+
|
| 24 |
+
file_extension = file_path.suffix.lower()
|
| 25 |
+
|
| 26 |
+
# Handle different file types
|
| 27 |
+
if file_extension in ['.csv']:
|
| 28 |
+
return _process_csv(file_path, query)
|
| 29 |
+
elif file_extension in ['.json']:
|
| 30 |
+
return _process_json(file_path, query)
|
| 31 |
+
elif file_extension in ['.xlsx', '.xls']:
|
| 32 |
+
return _process_excel(file_path, query)
|
| 33 |
+
elif file_extension in ['.txt', '.md']:
|
| 34 |
+
return _process_text(file_path, query)
|
| 35 |
+
else:
|
| 36 |
+
# Try to process as text for unknown formats
|
| 37 |
+
return _process_text(file_path, query)
|
| 38 |
+
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return f"Error processing file: {str(e)}"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _process_csv(file_path: Path, query: str = None) -> str:
|
| 44 |
+
"""Process CSV files."""
|
| 45 |
+
try:
|
| 46 |
+
df = pd.read_csv(file_path)
|
| 47 |
+
|
| 48 |
+
result = f"CSV File Analysis for {file_path.name}:\n"
|
| 49 |
+
result += f"- Rows: {len(df)}\n"
|
| 50 |
+
result += f"- Columns: {len(df.columns)}\n"
|
| 51 |
+
result += f"- Column names: {', '.join(df.columns.tolist())}\n\n"
|
| 52 |
+
|
| 53 |
+
if query:
|
| 54 |
+
# Search for query in the dataframe
|
| 55 |
+
query_lower = query.lower()
|
| 56 |
+
matching_rows = df[df.astype(str).apply(lambda x: x.str.lower().str.contains(query_lower, na=False)).any(axis=1)]
|
| 57 |
+
|
| 58 |
+
if not matching_rows.empty:
|
| 59 |
+
result += f"Rows matching '{query}':\n"
|
| 60 |
+
result += matching_rows.to_string(index=False)
|
| 61 |
+
else:
|
| 62 |
+
result += f"No rows found matching '{query}'"
|
| 63 |
+
else:
|
| 64 |
+
# Show first few rows and basic statistics
|
| 65 |
+
result += "First 5 rows:\n"
|
| 66 |
+
result += df.head().to_string(index=False)
|
| 67 |
+
|
| 68 |
+
if len(df) > 5:
|
| 69 |
+
result += f"\n\n... and {len(df) - 5} more rows"
|
| 70 |
+
|
| 71 |
+
return result
|
| 72 |
+
|
| 73 |
+
except Exception as e:
|
| 74 |
+
return f"Error processing CSV: {str(e)}"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _process_json(file_path: Path, query: str = None) -> str:
|
| 78 |
+
"""Process JSON files."""
|
| 79 |
+
try:
|
| 80 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 81 |
+
data = json.load(f)
|
| 82 |
+
|
| 83 |
+
result = f"JSON File Analysis for {file_path.name}:\n"
|
| 84 |
+
|
| 85 |
+
if isinstance(data, dict):
|
| 86 |
+
result += f"- Type: Dictionary with {len(data)} keys\n"
|
| 87 |
+
result += f"- Keys: {', '.join(data.keys())}\n\n"
|
| 88 |
+
elif isinstance(data, list):
|
| 89 |
+
result += f"- Type: List with {len(data)} items\n\n"
|
| 90 |
+
|
| 91 |
+
if query:
|
| 92 |
+
# Search for query in JSON content
|
| 93 |
+
json_str = json.dumps(data, indent=2).lower()
|
| 94 |
+
if query.lower() in json_str:
|
| 95 |
+
result += f"Found '{query}' in the JSON content.\n"
|
| 96 |
+
# Show relevant parts (simplified)
|
| 97 |
+
result += f"JSON content preview:\n{json.dumps(data, indent=2)[:1000]}..."
|
| 98 |
+
else:
|
| 99 |
+
result += f"'{query}' not found in JSON content."
|
| 100 |
+
else:
|
| 101 |
+
# Show JSON structure
|
| 102 |
+
result += f"JSON content preview:\n{json.dumps(data, indent=2)[:1000]}"
|
| 103 |
+
if len(json.dumps(data)) > 1000:
|
| 104 |
+
result += "..."
|
| 105 |
+
|
| 106 |
+
return result
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
return f"Error processing JSON: {str(e)}"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _process_excel(file_path: Path, query: str = None) -> str:
|
| 113 |
+
"""Process Excel files."""
|
| 114 |
+
try:
|
| 115 |
+
# Read the first sheet
|
| 116 |
+
df = pd.read_excel(file_path)
|
| 117 |
+
|
| 118 |
+
result = f"Excel File Analysis for {file_path.name}:\n"
|
| 119 |
+
result += f"- Rows: {len(df)}\n"
|
| 120 |
+
result += f"- Columns: {len(df.columns)}\n"
|
| 121 |
+
result += f"- Column names: {', '.join(df.columns.tolist())}\n\n"
|
| 122 |
+
|
| 123 |
+
if query:
|
| 124 |
+
# Search for query in the dataframe
|
| 125 |
+
query_lower = query.lower()
|
| 126 |
+
matching_rows = df[df.astype(str).apply(lambda x: x.str.lower().str.contains(query_lower, na=False)).any(axis=1)]
|
| 127 |
+
|
| 128 |
+
if not matching_rows.empty:
|
| 129 |
+
result += f"Rows matching '{query}':\n"
|
| 130 |
+
result += matching_rows.to_string(index=False)
|
| 131 |
+
else:
|
| 132 |
+
result += f"No rows found matching '{query}'"
|
| 133 |
+
else:
|
| 134 |
+
# Show first few rows
|
| 135 |
+
result += "First 5 rows:\n"
|
| 136 |
+
result += df.head().to_string(index=False)
|
| 137 |
+
|
| 138 |
+
if len(df) > 5:
|
| 139 |
+
result += f"\n\n... and {len(df) - 5} more rows"
|
| 140 |
+
|
| 141 |
+
return result
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
return f"Error processing Excel: {str(e)}"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _process_text(file_path: Path, query: str = None) -> str:
|
| 148 |
+
"""Process text files."""
|
| 149 |
+
try:
|
| 150 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 151 |
+
content = f.read()
|
| 152 |
+
|
| 153 |
+
result = f"Text File Analysis for {file_path.name}:\n"
|
| 154 |
+
result += f"- File size: {len(content)} characters\n"
|
| 155 |
+
result += f"- Lines: {len(content.splitlines())}\n\n"
|
| 156 |
+
|
| 157 |
+
if query:
|
| 158 |
+
lines_with_query = [line for line in content.splitlines() if query.lower() in line.lower()]
|
| 159 |
+
if lines_with_query:
|
| 160 |
+
result += f"Lines containing '{query}':\n"
|
| 161 |
+
for i, line in enumerate(lines_with_query[:10]): # Limit to first 10 matches
|
| 162 |
+
result += f"{i+1}: {line}\n"
|
| 163 |
+
if len(lines_with_query) > 10:
|
| 164 |
+
result += f"... and {len(lines_with_query) - 10} more matches"
|
| 165 |
+
else:
|
| 166 |
+
result += f"No lines found containing '{query}'"
|
| 167 |
+
else:
|
| 168 |
+
# Show first 1000 characters
|
| 169 |
+
result += "Content preview:\n"
|
| 170 |
+
result += content[:1000]
|
| 171 |
+
if len(content) > 1000:
|
| 172 |
+
result += "..."
|
| 173 |
+
|
| 174 |
+
return result
|
| 175 |
+
|
| 176 |
+
except Exception as e:
|
| 177 |
+
return f"Error processing text file: {str(e)}"
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# Create the tool
|
| 181 |
+
file_processor_tool = Tool(
|
| 182 |
+
name="file_processor_tool",
|
| 183 |
+
description="Use this tool to process and analyze downloaded files. Supports CSV, JSON, Excel, and text files. You can provide a query to search within the file content.",
|
| 184 |
+
func=lambda file_path_and_query: process_file(*file_path_and_query.split(' | ')) if ' | ' in file_path_and_query else process_file(file_path_and_query)
|
| 185 |
+
)
|
logging_config.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import logging.config
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def setup_logging(
|
| 8 |
+
log_level: str = "INFO",
|
| 9 |
+
log_file: Optional[str] = None,
|
| 10 |
+
log_format: Optional[str] = None
|
| 11 |
+
) -> None:
|
| 12 |
+
"""
|
| 13 |
+
Set up logging configuration for the Pokemon agent project.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
| 17 |
+
log_file: Optional file path to write logs to
|
| 18 |
+
log_format: Optional custom log format
|
| 19 |
+
"""
|
| 20 |
+
if log_format is None:
|
| 21 |
+
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 22 |
+
|
| 23 |
+
# Create logs directory if logging to file
|
| 24 |
+
if log_file:
|
| 25 |
+
log_path = Path(log_file)
|
| 26 |
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 27 |
+
|
| 28 |
+
# Configure logging
|
| 29 |
+
config = {
|
| 30 |
+
"version": 1,
|
| 31 |
+
"disable_existing_loggers": False,
|
| 32 |
+
"formatters": {
|
| 33 |
+
"standard": {
|
| 34 |
+
"format": log_format,
|
| 35 |
+
"datefmt": "%Y-%m-%d %H:%M:%S"
|
| 36 |
+
},
|
| 37 |
+
"detailed": {
|
| 38 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s",
|
| 39 |
+
"datefmt": "%Y-%m-%d %H:%M:%S"
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
"handlers": {
|
| 43 |
+
"console": {
|
| 44 |
+
"class": "logging.StreamHandler",
|
| 45 |
+
"level": log_level,
|
| 46 |
+
"formatter": "standard",
|
| 47 |
+
"stream": "ext://sys.stdout"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"loggers": {
|
| 51 |
+
"pokemon_agent": {
|
| 52 |
+
"level": log_level,
|
| 53 |
+
"handlers": ["console"],
|
| 54 |
+
"propagate": False
|
| 55 |
+
},
|
| 56 |
+
"langgraph": {
|
| 57 |
+
"level": "WARNING",
|
| 58 |
+
"handlers": ["console"],
|
| 59 |
+
"propagate": False
|
| 60 |
+
},
|
| 61 |
+
"langchain": {
|
| 62 |
+
"level": "WARNING",
|
| 63 |
+
"handlers": ["console"],
|
| 64 |
+
"propagate": False
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
"root": {
|
| 68 |
+
"level": log_level,
|
| 69 |
+
"handlers": ["console"]
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# Add file handler if log_file is specified
|
| 74 |
+
if log_file:
|
| 75 |
+
config["handlers"]["file"] = {
|
| 76 |
+
"class": "logging.handlers.RotatingFileHandler",
|
| 77 |
+
"level": log_level,
|
| 78 |
+
"formatter": "detailed",
|
| 79 |
+
"filename": log_file,
|
| 80 |
+
"maxBytes": 10485760, # 10MB
|
| 81 |
+
"backupCount": 5
|
| 82 |
+
}
|
| 83 |
+
# Add file handler to all loggers
|
| 84 |
+
config["loggers"]["pokemon_agent"]["handlers"].append("file")
|
| 85 |
+
config["root"]["handlers"].append("file")
|
| 86 |
+
|
| 87 |
+
logging.config.dictConfig(config)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def get_logger(name: str) -> logging.Logger:
|
| 91 |
+
"""
|
| 92 |
+
Get a logger instance for a specific module.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
name: Logger name (typically __name__)
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
Configured logger instance
|
| 99 |
+
"""
|
| 100 |
+
return logging.getLogger(f"pokemon_agent.{name}")
|
multimodal_demo.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Demonstration of the multimodal download capabilities.
|
| 3 |
+
|
| 4 |
+
This shows how the agent can download and analyze images/audio files from URLs.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from agent import BasicAgent
|
| 8 |
+
from langchain_core.messages import HumanMessage
|
| 9 |
+
|
| 10 |
+
def test_multimodal_download():
|
| 11 |
+
"""Test the multimodal download functionality."""
|
| 12 |
+
|
| 13 |
+
# Initialize the agent
|
| 14 |
+
agent = BasicAgent()
|
| 15 |
+
|
| 16 |
+
print("🤖 Testing Multimodal Download Agent")
|
| 17 |
+
print("=" * 50)
|
| 18 |
+
|
| 19 |
+
# Example 1: Image analysis
|
| 20 |
+
print("\n📸 Example 1: Analyzing an image from URL")
|
| 21 |
+
print("-" * 30)
|
| 22 |
+
|
| 23 |
+
# Example with a sample image URL
|
| 24 |
+
image_question = """
|
| 25 |
+
Can you download and analyze this image: https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Vd-Orig.svg/256px-Vd-Orig.svg.png
|
| 26 |
+
What do you see in this image?
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
response = agent(image_question)
|
| 31 |
+
print(f"Agent Response: {response}")
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f"Error: {e}")
|
| 34 |
+
|
| 35 |
+
print("\n" + "=" * 50)
|
| 36 |
+
|
| 37 |
+
# Example 2: Different type of question
|
| 38 |
+
print("\n🏠 Example 2: Neighborhood query")
|
| 39 |
+
print("-" * 30)
|
| 40 |
+
|
| 41 |
+
neighborhood_question = "Who lives in apartment 302?"
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
response = agent(neighborhood_question)
|
| 45 |
+
print(f"Agent Response: {response}")
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"Error: {e}")
|
| 48 |
+
|
| 49 |
+
print("\n" + "=" * 50)
|
| 50 |
+
print("✅ Demo completed!")
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
test_multimodal_download()
|
multimodal_download_tool.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import os
|
| 3 |
+
from urllib.parse import urlparse
|
| 4 |
+
from langchain.tools import Tool
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import base64
|
| 7 |
+
import mimetypes
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def download_and_process_media(url: str, media_type: str = "auto") -> str:
|
| 11 |
+
"""Download media file (image/audio) from URL and prepare it for multimodal processing.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
url: The URL of the media file to download.
|
| 15 |
+
media_type: Type of media - "image", "audio", or "auto" to detect automatically.
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
str: Information about the downloaded file and instructions for the agent.
|
| 19 |
+
"""
|
| 20 |
+
try:
|
| 21 |
+
# Validate URL
|
| 22 |
+
parsed_url = urlparse(url)
|
| 23 |
+
if not parsed_url.scheme or not parsed_url.netloc:
|
| 24 |
+
return f"Error: Invalid URL format: {url}"
|
| 25 |
+
|
| 26 |
+
# Send GET request to download the file
|
| 27 |
+
response = requests.get(url, stream=True, timeout=30)
|
| 28 |
+
response.raise_for_status()
|
| 29 |
+
|
| 30 |
+
# Get content type and determine file type
|
| 31 |
+
content_type = response.headers.get('content-type', '').lower()
|
| 32 |
+
|
| 33 |
+
# Determine if it's image or audio
|
| 34 |
+
is_image = any(img_type in content_type for img_type in ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif'])
|
| 35 |
+
is_audio = any(audio_type in content_type for audio_type in ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/mp4', 'audio/m4a'])
|
| 36 |
+
|
| 37 |
+
if media_type == "auto":
|
| 38 |
+
if is_image:
|
| 39 |
+
media_type = "image"
|
| 40 |
+
elif is_audio:
|
| 41 |
+
media_type = "audio"
|
| 42 |
+
else:
|
| 43 |
+
return f"Error: Unable to determine media type from content-type: {content_type}. Please specify 'image' or 'audio'."
|
| 44 |
+
|
| 45 |
+
# Get filename from URL or create one based on content type
|
| 46 |
+
filename = os.path.basename(parsed_url.path)
|
| 47 |
+
if not filename or '.' not in filename:
|
| 48 |
+
if media_type == "image":
|
| 49 |
+
if 'jpeg' in content_type or 'jpg' in content_type:
|
| 50 |
+
filename = 'downloaded_image.jpg'
|
| 51 |
+
elif 'png' in content_type:
|
| 52 |
+
filename = 'downloaded_image.png'
|
| 53 |
+
elif 'webp' in content_type:
|
| 54 |
+
filename = 'downloaded_image.webp'
|
| 55 |
+
else:
|
| 56 |
+
filename = 'downloaded_image.jpg'
|
| 57 |
+
elif media_type == "audio":
|
| 58 |
+
if 'mpeg' in content_type or 'mp3' in content_type:
|
| 59 |
+
filename = 'downloaded_audio.mp3'
|
| 60 |
+
elif 'wav' in content_type:
|
| 61 |
+
filename = 'downloaded_audio.wav'
|
| 62 |
+
elif 'mp4' in content_type:
|
| 63 |
+
filename = 'downloaded_audio.mp4'
|
| 64 |
+
else:
|
| 65 |
+
filename = 'downloaded_audio.mp3'
|
| 66 |
+
|
| 67 |
+
# Create downloads directory if it doesn't exist
|
| 68 |
+
downloads_dir = Path("downloads")
|
| 69 |
+
downloads_dir.mkdir(exist_ok=True)
|
| 70 |
+
|
| 71 |
+
# Save file to downloads directory
|
| 72 |
+
file_path = downloads_dir / filename
|
| 73 |
+
|
| 74 |
+
# Handle duplicate filenames
|
| 75 |
+
counter = 1
|
| 76 |
+
original_path = file_path
|
| 77 |
+
while file_path.exists():
|
| 78 |
+
stem = original_path.stem
|
| 79 |
+
suffix = original_path.suffix
|
| 80 |
+
file_path = downloads_dir / f"{stem}_{counter}{suffix}"
|
| 81 |
+
counter += 1
|
| 82 |
+
|
| 83 |
+
# Write file content
|
| 84 |
+
with open(file_path, 'wb') as f:
|
| 85 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 86 |
+
f.write(chunk)
|
| 87 |
+
|
| 88 |
+
file_size = file_path.stat().st_size
|
| 89 |
+
|
| 90 |
+
# Prepare the response with file information
|
| 91 |
+
result = f"Successfully downloaded {media_type} file:\n"
|
| 92 |
+
result += f"- File path: {file_path}\n"
|
| 93 |
+
result += f"- File size: {file_size} bytes\n"
|
| 94 |
+
result += f"- Content type: {content_type}\n"
|
| 95 |
+
result += f"- Media type: {media_type}\n\n"
|
| 96 |
+
|
| 97 |
+
if media_type == "image":
|
| 98 |
+
result += f"DOWNLOADED_IMAGE_PATH:{file_path}"
|
| 99 |
+
elif media_type == "audio":
|
| 100 |
+
result += f"DOWNLOADED_AUDIO_PATH:{file_path}"
|
| 101 |
+
|
| 102 |
+
return result
|
| 103 |
+
|
| 104 |
+
except requests.exceptions.RequestException as e:
|
| 105 |
+
return f"Error downloading media file: {str(e)}"
|
| 106 |
+
except Exception as e:
|
| 107 |
+
return f"Unexpected error: {str(e)}"
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def analyze_downloaded_media(file_path: str, query: str = "") -> str:
|
| 111 |
+
"""Analyze a downloaded media file using multimodal capabilities.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
file_path: Path to the downloaded media file.
|
| 115 |
+
query: Optional query about the media content.
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
str: Special marker for the agent to process this with multimodal capabilities.
|
| 119 |
+
"""
|
| 120 |
+
try:
|
| 121 |
+
file_path = Path(file_path)
|
| 122 |
+
|
| 123 |
+
if not file_path.exists():
|
| 124 |
+
return f"Error: File not found at {file_path}"
|
| 125 |
+
|
| 126 |
+
# Get MIME type to determine if it's image or audio
|
| 127 |
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
| 128 |
+
|
| 129 |
+
if mime_type and mime_type.startswith('image/'):
|
| 130 |
+
return f"ANALYZE_IMAGE:{file_path}|{query}"
|
| 131 |
+
elif mime_type and mime_type.startswith('audio/'):
|
| 132 |
+
return f"ANALYZE_AUDIO:{file_path}|{query}"
|
| 133 |
+
else:
|
| 134 |
+
return f"Error: Unsupported media type for file: {file_path}"
|
| 135 |
+
|
| 136 |
+
except Exception as e:
|
| 137 |
+
return f"Error preparing media analysis: {str(e)}"
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# Create the tools
|
| 141 |
+
multimodal_download_tool = Tool(
|
| 142 |
+
name="multimodal_download_tool",
|
| 143 |
+
description="Download image or audio files from URLs for multimodal analysis. Specify media_type as 'image', 'audio', or 'auto' to detect automatically. Use format: 'URL|media_type' or just 'URL' for auto-detection.",
|
| 144 |
+
func=lambda url_and_type: download_and_process_media(*url_and_type.split('|')) if '|' in url_and_type else download_and_process_media(url_and_type)
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
media_analysis_tool = Tool(
|
| 148 |
+
name="media_analysis_tool",
|
| 149 |
+
description="Analyze downloaded image or audio files with optional query. Use format: 'file_path|query' or just 'file_path'.",
|
| 150 |
+
func=lambda path_and_query: analyze_downloaded_media(*path_and_query.split('|', 1)) if '|' in path_and_query else analyze_downloaded_media(path_and_query)
|
| 151 |
+
)
|
neighborhood_agent_graph.png
ADDED
|
poetry.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pyproject.toml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "final-assignment-template"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "My attempt to the final assignment of hugging face's agents course"
|
| 5 |
+
authors = [
|
| 6 |
+
{name = "Angelo Mosquera",email = "anggelomos@outlook.com"}
|
| 7 |
+
]
|
| 8 |
+
readme = "README.md"
|
| 9 |
+
requires-python = ">=3.10,<4.0"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"gradio (>=5.38.2,<6.0.0)",
|
| 12 |
+
"requests (>=2.32.4,<3.0.0)",
|
| 13 |
+
"python-dotenv (>=1.1.1,<2.0.0)",
|
| 14 |
+
"langchain-community (>=0.3.27,<0.4.0)",
|
| 15 |
+
"langgraph (>=0.5.4,<0.6.0)",
|
| 16 |
+
"rank-bm25 (>=0.2.2,<0.3.0)",
|
| 17 |
+
"langchain-google-genai (>=2.1.8,<3.0.0)",
|
| 18 |
+
"pandas (>=2.0.0,<3.0.0)",
|
| 19 |
+
"openpyxl (>=3.0.0,<4.0.0)",
|
| 20 |
+
"duckduckgo-search (>=8.1.1,<9.0.0)",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
[tool.poetry]
|
| 24 |
+
package-mode = false
|
| 25 |
+
|
| 26 |
+
[tool.poetry.group.dev.dependencies]
|
| 27 |
+
flake8 = "^7.3.0"
|
| 28 |
+
ruff = "^0.12.5"
|
| 29 |
+
|
| 30 |
+
[build-system]
|
| 31 |
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
| 32 |
+
build-backend = "poetry.core.masonry.api"
|
requirements.txt
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
gradio
|
| 2 |
-
requests
|
|
|
|
|
|
|
|
|