anggelomos commited on
Commit
bb41dbe
·
1 Parent(s): 81917a3

Initial version of the agent WIP

Browse files
.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
- Fetches all questions, runs the BasicAgent on them, submits all answers,
25
- and displays the results.
 
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