lezaf commited on
Commit
448903c
·
1 Parent(s): d3b88d9

Add agent implementation

Browse files
Files changed (7) hide show
  1. .gitignore +5 -1
  2. agent.py +187 -0
  3. app.py +94 -14
  4. requirements.txt +0 -0
  5. subset_task_ids.txt +11 -0
  6. system_prompt.txt +56 -5
  7. tools.py +267 -0
.gitignore CHANGED
@@ -1 +1,5 @@
1
- venv/
 
 
 
 
 
1
+ .venv/
2
+ .env
3
+ # Python cache files
4
+ __pycache__/
5
+ .dist/
agent.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from io import BytesIO
2
+ import os
3
+ import getpass
4
+ import requests
5
+ from dotenv import load_dotenv
6
+ from langgraph.graph import StateGraph, MessagesState, START
7
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
8
+ from langchain_openai import ChatOpenAI
9
+ from langchain_core.messages import HumanMessage, SystemMessage
10
+ from langgraph.prebuilt import ToolNode, tools_condition
11
+ from langchain_google_genai import ChatGoogleGenerativeAI
12
+ from langfuse.langchain import CallbackHandler
13
+
14
+ from tools import *
15
+
16
+
17
+ load_dotenv(override=True)
18
+
19
+ PROVIDER="google"
20
+
21
+ langfuse_handler = CallbackHandler()
22
+
23
+ tools = [
24
+ # add_numbers,
25
+ add_numbers_in_list,
26
+ web_search,
27
+ # wikipedia_search,
28
+ arxiv_search,
29
+ check_commutativity,
30
+ extract_sales_data_from_excel,
31
+ extract_transcript_from_youtube
32
+ ]
33
+
34
+ # --------------- Define the agent structure ---------------- #
35
+ def build_agent(provider: str = "hf"):
36
+ print(f"Building agent with provider: {provider}")
37
+ if provider == "hf":
38
+ llm = HuggingFaceEndpoint(
39
+ repo_id="mistralai/Mixtral-8x7B-Instruct-v0.1",
40
+ task="text-generation",
41
+ temperature=0.0,
42
+ provider="hf-inference"
43
+ )
44
+
45
+ llm = ChatHuggingFace(llm=llm)
46
+
47
+ elif provider == "google":
48
+ # Google Gemini
49
+ llm = ChatGoogleGenerativeAI(
50
+ model="gemini-2.0-flash",
51
+ # temperature=0,
52
+ max_tokens=512,
53
+ # timeout=None,
54
+ max_retries=2,
55
+ )
56
+
57
+ elif provider == "openai":
58
+ llm = ChatOpenAI(
59
+ model="gpt-3.5-turbo", # or "gpt-3.5-turbo"
60
+ temperature=0,
61
+ api_key=os.getenv("OPENAI_API_KEY"),
62
+ max_tokens=512
63
+ )
64
+ else:
65
+ raise ValueError(f"Unsupported provider: {provider}")
66
+
67
+ # Bind the tools to the LLM
68
+ llm_with_tools = llm.bind_tools(tools)
69
+
70
+ # load the system prompt from the file
71
+ with open("system_prompt.txt", "r", encoding="utf-8") as f:
72
+ system_prompt = f.read()
73
+
74
+ # Create system message with the system prompt
75
+ sys_msg = SystemMessage(content=system_prompt)
76
+
77
+ # --------------- Define nodes ---------------- #
78
+ def assistant(state: MessagesState):
79
+ """Node for the assistant to respond to user input."""
80
+ # return {"messages": [llm_with_tools.invoke(state["messages"])]}
81
+
82
+ response = llm_with_tools.invoke([sys_msg] + state["messages"])
83
+ return {"messages": [response]}
84
+
85
+
86
+ tool_node = ToolNode(tools=tools)
87
+
88
+ # --------------- Build the state graph ---------------- #
89
+ graph_builder = StateGraph(MessagesState)
90
+
91
+ graph_builder.add_node("assistant", assistant)
92
+ graph_builder.add_node("tools", tool_node)
93
+
94
+ graph_builder.add_conditional_edges(
95
+ "assistant",
96
+ tools_condition,
97
+ )
98
+ graph_builder.add_edge("tools", "assistant")
99
+ graph_builder.add_edge(START, "assistant")
100
+
101
+ return graph_builder.compile()
102
+
103
+
104
+ if __name__ == "__main__":
105
+ print("\n" + "-"*30 + " Agent Starting " + "-"*30)
106
+ agent = build_agent(provider=PROVIDER) # Change to "hf" for HuggingFace
107
+ print("Agent built successfully.")
108
+ print("-"*70)
109
+
110
+ # Get questions
111
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
112
+ api_url = DEFAULT_API_URL
113
+ questions_url = f"{api_url}/questions"
114
+ files_url = f"{api_url}/files/" # Needs task_id
115
+
116
+ # 2. Fetch Questions
117
+ print(f"Fetching questions from: {questions_url}")
118
+ try:
119
+ response = requests.get(questions_url, timeout=15)
120
+ response.raise_for_status()
121
+ questions_data = response.json()
122
+ if not questions_data:
123
+ print("Fetched questions list is empty.")
124
+ print(f"Fetched {len(questions_data)} questions.")
125
+ except Exception as e:
126
+ print(f"An unexpected error occurred fetching questions: {e}")
127
+
128
+ # 3. Get specific question by task_id
129
+ task_id = "cca530fc-4052-43b2-b130-b30968d8aa44" # Chess image
130
+ # task_id = "6f37996b-2ac7-44b0-8e68-6d28256631b4" # Commutativity check
131
+ # task_id = "2d83110e-a098-4ebb-9987-066c06fa42d0" # Reverse text example
132
+ # task_id = "f918266a-b3e0-4914-865d-4faa564f1aef" # Code example
133
+ # task_id = "7bd855d8-463d-4ed5-93ca-5fe35145f733" # Excel file (passed)
134
+ # task_id = "cabe07ed-9eca-40ea-8ead-410ef5e83f91" # Louvrier
135
+ # task_id = "305ac316-eef6-4446-960a-92d80d542f82" # Poland film (FAIL)
136
+ # task_id = "3f57289b-8c60-48be-bd80-01f8099ca449" # at bats (PASS)
137
+ # task_id = "bda648d7-d618-4883-88f4-3466eabd860e" # Vietnamese (FAIL)
138
+ # task_id = "cf106601-ab4f-4af9-b045-5295fe67b37d" # Olympics
139
+ # task_id = "a0c07678-e491-4bbc-8f0b-07405144218f"
140
+ # task_id = "3cef3a44-215e-4aed-8e3b-b1e3f08063b7" # grocery list
141
+ # task_id = "8e867cd7-cff9-4e6c-867a-ff5ddc2550be" # Sosa albums
142
+ # task_id = "4fc2f1ae-8625-45b5-ab34-ad4433bc21f8" # Dinosaur
143
+ # task_id = "840bfca7-4f7b-481a-8794-c560c340185d" # Carolyn Collins Petersen (FAIL)
144
+ # task_id = "5a0c1adf-205e-4841-a666-7c3ef95def9d" # Malko competition (PASS)
145
+
146
+ # get question with task_id
147
+ q_data = next((item for item in questions_data if item["task_id"] == task_id), None)
148
+
149
+ content = [
150
+ {"type": "text", "text": q_data["question"]}
151
+ ]
152
+
153
+ if q_data["file_name"] != "":
154
+ file_url = f"{files_url}{task_id}"
155
+
156
+ if q_data["file_name"].endswith((".png", ".jpg", ".jpeg")):
157
+ content.append({"type": "image_url", "image_url": {"url": file_url}})
158
+
159
+ elif q_data["file_name"].endswith((".py")):
160
+ # For code files, we can just send the text content
161
+ try:
162
+ response = requests.get(file_url, timeout=15)
163
+ response.raise_for_status()
164
+ code_content = response.text
165
+
166
+ content.append({"type": "text", "text": code_content})
167
+ except Exception as e:
168
+ print(f"Error fetching code file: {e}")
169
+
170
+ elif q_data["file_name"].endswith((".xlsx", ".xls")):
171
+ content.append({"type": "text", "text": "Excel file url: " + file_url})
172
+
173
+ human_msg = HumanMessage(content=content)
174
+
175
+ human_msg.pretty_print()
176
+
177
+ try:
178
+ result = agent.invoke(
179
+ {"messages": [human_msg]},
180
+ config={"callbacks": [langfuse_handler]}
181
+ )
182
+
183
+ for message in result["messages"]:
184
+ message.pretty_print()
185
+ # Result already printed inside assistant() node
186
+ except Exception as e:
187
+ print(f"Error: {e}")
app.py CHANGED
@@ -1,27 +1,99 @@
 
 
 
 
 
 
 
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 ---
9
  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 ---
@@ -34,13 +106,9 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
34
  print("User not logged in.")
35
  return "Please Login to Hugging Face with the button.", None
36
 
37
- api_url = DEFAULT_API_URL
38
- questions_url = f"{api_url}/questions"
39
- submit_url = f"{api_url}/submit"
40
-
41
  # 1. Instantiate Agent ( modify this part to create your agent)
42
  try:
43
- agent = BasicAgent()
44
  except Exception as e:
45
  print(f"Error instantiating agent: {e}")
46
  return f"Error initializing agent: {e}", None
@@ -79,10 +147,22 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
79
  if not task_id or question_text is None:
80
  print(f"Skipping item with missing task_id or question: {item}")
81
  continue
 
 
 
 
 
 
 
 
 
82
  try:
83
- submitted_answer = agent(question_text)
 
84
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
85
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
 
 
86
  except Exception as e:
87
  print(f"Error running agent on task {task_id}: {e}")
88
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
 
1
+ """
2
+ NOTE:
3
+ - The agent only runs on a subset of tasks defined in `subset_task_ids.txt` to avoid unnecessary token usage
4
+ for questions that the agent cannot handle right now.
5
+ - There is a 30 sec delay after each question is answered to avoid rate limiting issues.
6
+ """
7
+
8
  import os
9
  import gradio as gr
10
  import requests
11
  import inspect
12
  import pandas as pd
13
+ import time
14
+ from agent import build_agent
15
+ from langchain_core.messages import HumanMessage
16
+ from langfuse.langchain import CallbackHandler
17
+
18
+ langfuse_handler = CallbackHandler()
19
 
20
  # (Keep Constants as is)
21
  # --- Constants ---
22
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
23
+ questions_url = f"{DEFAULT_API_URL}/questions"
24
+ submit_url = f"{DEFAULT_API_URL}/submit"
25
+ files_url = f"{DEFAULT_API_URL}/files/" # Needs task_id
26
 
27
  # --- Basic Agent Definition ---
28
+ class SuperAgent:
 
29
  def __init__(self):
30
+ print("SuperAgent initialized.")
31
+ self.agent = build_agent(provider="google") # Change to "hf" for HuggingFace
32
+
33
+ def __call__(self, data: str) -> str:
34
+ """
35
+ Args:
36
+ data (str): A string containing the question to be answered.
37
+ Schema: {
38
+ task_id: str,
39
+ question: str,
40
+ file_name: str,
41
+ }
42
+ """
43
+ # Quick validation of input data (TODO: Use pydantic for schema)
44
+ if not data.get("question") or not data.get("task_id") or not data.get("file_name"):
45
+ raise ValueError("Input data must contain 'question', 'task_id', and 'file_name'.")
46
+
47
+ task_id, question, file_name = data["task_id"], data["question"], data["file_name"]
48
+
49
  print(f"Agent received question (first 50 chars): {question[:50]}...")
50
+
51
+ # Build HumanMessage
52
+ content = [
53
+ {"type": "text", "text": question}
54
+ ]
55
+
56
+ if file_name != "":
57
+ file_url = f"{files_url}{task_id}"
58
+
59
+ if file_name.endswith((".png", ".jpg", ".jpeg")):
60
+ content.append({"type": "image_url", "image_url": {"url": file_url}})
61
+
62
+ elif file_name.endswith((".py")):
63
+ # For code files, we can just send the text content
64
+ try:
65
+ response = requests.get(file_url, timeout=15)
66
+ response.raise_for_status()
67
+ code_content = response.text
68
+
69
+ content.append({"type": "text", "text": code_content})
70
+ except Exception as e:
71
+ print(f"Error fetching code file: {e}")
72
+
73
+ elif file_name.endswith((".xlsx", ".xls")):
74
+ content.append({"type": "text", "text": "Excel file url: " + file_url})
75
+
76
+ human_msg = HumanMessage(content=content)
77
+
78
+ try:
79
+ answer = self.agent.invoke(
80
+ {"messages": [human_msg]},
81
+ config={"callbacks": [langfuse_handler]}
82
+ )
83
+
84
+ # for message in answer["messages"]:
85
+ # message.pretty_print()
86
+ # Result already printed inside assistant() node
87
+ except Exception as e:
88
+ print(f"Error: {e}")
89
+
90
+ return answer["messages"][-1].content
91
+
92
+
93
 
94
  def run_and_submit_all( profile: gr.OAuthProfile | None):
95
  """
96
+ Fetches all questions, runs the SuperAgent on them, submits all answers,
97
  and displays the results.
98
  """
99
  # --- Determine HF Space Runtime URL and Repo URL ---
 
106
  print("User not logged in.")
107
  return "Please Login to Hugging Face with the button.", None
108
 
 
 
 
 
109
  # 1. Instantiate Agent ( modify this part to create your agent)
110
  try:
111
+ agent = SuperAgent()
112
  except Exception as e:
113
  print(f"Error instantiating agent: {e}")
114
  return f"Error initializing agent: {e}", None
 
147
  if not task_id or question_text is None:
148
  print(f"Skipping item with missing task_id or question: {item}")
149
  continue
150
+
151
+ # Only run on subset of tasks that is capable of being run so that
152
+ # token usage is not wasted on tasks that the agent cannot handle.
153
+ with open("subset_task_ids.txt", "r") as f:
154
+ subset_task_ids = [line.strip() for line in f if line.strip()]
155
+
156
+ if task_id not in subset_task_ids:
157
+ continue
158
+
159
  try:
160
+ submitted_answer = agent(item)
161
+
162
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
163
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
164
+
165
+ time.sleep(30) # Sleep to avoid rate limiting issues
166
  except Exception as e:
167
  print(f"Error running agent on task {task_id}: {e}")
168
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ
 
subset_task_ids.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 8e867cd7-cff9-4e6c-867a-ff5ddc2550be
2
+ 2d83110e-a098-4ebb-9987-066c06fa42d0
3
+ cca530fc-4052-43b2-b130-b30968d8aa44
4
+ 4fc2f1ae-8625-45b5-ab34-ad4433bc21f8
5
+ 6f37996b-2ac7-44b0-8e68-6d28256631b4
6
+ 9d191bce-651d-4746-be2d-7ef8ecadb9c2
7
+ cabe07ed-9eca-40ea-8ead-410ef5e83f91
8
+ f918266a-b3e0-4914-865d-4faa564f1aef
9
+ 3f57289b-8c60-48be-bd80-01f8099ca449
10
+ 7bd855d8-463d-4ed5-93ca-5fe35145f733
11
+ 5a0c1adf-205e-4841-a666-7c3ef95def9d
system_prompt.txt CHANGED
@@ -1,8 +1,59 @@
1
- You are a general AI assistant.
2
- I will ask you a question.
3
- Report your thoughts, and finish your answer with the following template: [YOUR_FINAL_ANSWER].
4
  For YOUR_FINAL_ANSWER follow strictly the instructions below:
5
  * YOUR_FINAL_ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
6
- * 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.
 
7
  * 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.
8
- * 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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are a general AI assistant. I will ask you a question and I want an answer in the following template: YOUR_FINAL_ANSWER.
2
+
 
3
  For YOUR_FINAL_ANSWER follow strictly the instructions below:
4
  * YOUR_FINAL_ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
5
+ * If you are asked for a number, don't use comma to write your number neither use units such as: [$, meters (m), centimeters (cm), oz] or any other unit of measurement
6
+ or percent sign unless specified otherwise.
7
  * 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.
8
+ * 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.
9
+
10
+ You are provided with tools that you can use to answer questions accurately. If you cannot answer the question directly, examine the list of available tools and
11
+ choose the suitable tool for your case. You may need to use more than one tool to conclude to an answer.
12
+
13
+ Below are some Question/Answer examples. "Q" is what you get from user, "[P]" is the internal planning and processing you make and "A" is the output to the user.
14
+
15
+ Do not restate or explain the answer. Do not prefix the answer with "A:", "Answer:", or any other text. Only output the final value requested.
16
+
17
+ Example 1:
18
+
19
+ Q: What is the height of statue of liberty?
20
+ [P]: I should use web_search tool.
21
+ [P]: web_search("height of statue of liberty")
22
+ [P]: The result of web_search is "The height of the statue of liberty is 93 m"
23
+ A: 93
24
+
25
+ Example 2:
26
+
27
+ Q: What is the circumference of earth in miles?
28
+ [P]: I should use web_search tool.
29
+ [P]: web_search("circumference of earth in miles")
30
+ [P]: The result of web_search is "The circumference of earth is 24,901 miles"
31
+ A: 24901 miles
32
+
33
+ Example 3:
34
+
35
+ Q: What is the capital of France?
36
+ [P]: This is a factual question I know.
37
+ A: Paris
38
+
39
+ Example 4:
40
+
41
+ Q: What is the total cost with two decimal places of the items in the table, excluding drinks?
42
+ Table:
43
+ | Burgers | Salads | Soda | Ice Cream |
44
+ | 10.0 | 5.0 | 3.0 | 4.0 |
45
+ [P]: Soda is a drink. The rest are food.
46
+ [P]: I should use add_numbers_in_list([10.0, 5.0, 4.0])
47
+ [P]: The result is 19.0
48
+ A: 19.00
49
+
50
+ Example 5:
51
+
52
+ Q: What was the name of the director that won the Oscar in 2009?
53
+ A: Boyle
54
+
55
+ IMPORTANT: Never report to the user the strategy you followed to conclude to the answer. Always report the final answer as a string, number, or whatever is asked in the question.
56
+
57
+ If the question involves summing or totaling numeric values from a list or data source, always use the add_numbers_in_list tool.
58
+ Do not attempt to manually perform or display the addition; instead, pass the numeric list to the tool and use its output directly as the final answer.
59
+ Never display intermediate math like “X + Y + Z = …” unless specifically requested. Only show the final answer after using the tool.
tools.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import requests
3
+ from io import BytesIO
4
+ from io import StringIO
5
+ from langchain_core.tools import tool
6
+ from langchain_community.retrievers import WikipediaRetriever
7
+ from langchain_community.document_loaders import ArxivLoader
8
+ from langchain_community.retrievers import BM25Retriever
9
+ from langchain_core.documents import Document
10
+ from duckduckgo_search import DDGS
11
+ from markitdown import MarkItDown
12
+
13
+ # --------------- Math Tools ---------------- #
14
+ @tool
15
+ def add_numbers(a: int, b: int) -> int:
16
+ """Add two numbers.
17
+
18
+ Args:
19
+ a (int): The first number.
20
+ b (int): The second number.
21
+ """
22
+ return a + b
23
+
24
+ @tool
25
+ def add_numbers_in_list(numbers: list[float]) -> float:
26
+ """Add all numbers in a list.
27
+ Always use this tool for summing numerical values, instead of doing math directly in the response.
28
+
29
+ Args:
30
+ numbers (list[float]): A list of numbers to add.
31
+ """
32
+ return sum(numbers)
33
+
34
+ # @tool
35
+ # def web_search(query: str) -> str:
36
+ # """Perform a web search using DuckDuckGo.
37
+
38
+ # Args:
39
+ # query (str): The search query.
40
+
41
+ # Returns:
42
+ # str: The search results.
43
+ # """
44
+ # search_tool = DuckDuckGoSearchRun()
45
+ # return search_tool.invoke(query)
46
+
47
+ @tool
48
+ def web_search(query: str) -> str:
49
+ """
50
+ Perform a web search using DuckDuckGo. Visit the top ranked page,
51
+ apply chunking in page results, perform similarity search, and return
52
+ the top results content.
53
+
54
+ Args:
55
+ query (str): The search query.
56
+ Returns:
57
+ Document: The top results from the ranking, in langchain_core.documents.Document
58
+ objects having fields 'page_content' with the chunk content and 'metadata'.
59
+ """
60
+ def _chunk_text(text, chunk_size_words=1000, overlap_words=100):
61
+ """
62
+ Split text into chunks of specified size with overlap.
63
+ Args:
64
+ text (str): The text to be chunked.
65
+ chunk_size (int): The size of each chunk.
66
+ overlap (int): The number of overlapping characters between chunks.
67
+ Returns:
68
+ list: A list of text chunks.
69
+ """
70
+ words = text.split()
71
+ chunks = []
72
+ for i in range(0, len(words), chunk_size_words - overlap_words):
73
+ chunk = " ".join(words[i:i + chunk_size_words])
74
+ chunks.append(chunk)
75
+ return chunks
76
+
77
+ # STEP 1: Find the most relevant webpage
78
+ results = DDGS().text(query, max_results=1)
79
+ top_rank_page = results[0] if results else None
80
+ if not top_rank_page:
81
+ return "No relevant results found for the query."
82
+
83
+ # STEP 2: Extract the content of the webpage
84
+ md = MarkItDown(enable_plugins=True)
85
+ md_result = md.convert(top_rank_page['href'])
86
+
87
+ page_content = md_result.text_content
88
+
89
+ # STEP 3: Apply chunking
90
+ chunks = _chunk_text(page_content)
91
+
92
+ # STEP 4: Apply ranking in chunks
93
+ list_of_docs = [
94
+ Document(page_content = chunk, metadata = {"source": top_rank_page['href'], "title": top_rank_page['title']})
95
+ for chunk in chunks
96
+ ]
97
+
98
+ retriever = BM25Retriever.from_documents(list_of_docs)
99
+ matched = retriever.invoke(query)
100
+
101
+ return matched[0]
102
+
103
+ # TODO:
104
+ # Maybe don't return the summary, but the full document?
105
+ @tool
106
+ def wikipedia_search(query: str) -> str:
107
+ """
108
+ Search Wikipedia for a given query and return a summary of the top result.
109
+
110
+ Args:
111
+ query (str): The search term.
112
+
113
+ Returns:
114
+ str: A summary of the most relevant Wikipedia entry.
115
+ """
116
+ wikipedia_retriever = WikipediaRetriever(load_max_docs=1)
117
+
118
+ documents = wikipedia_retriever.get_relevant_documents(query)
119
+ if not documents:
120
+ return "No relevant Wikipedia articles found."
121
+
122
+ formatted_search_docs = "\n\n---\n\n".join(
123
+ [
124
+ f'<Document source="{doc.metadata["source"]}" title="{doc.metadata.get("title", "")}"/>\n{doc.metadata["summary"]}\n</Document>'
125
+ for doc in documents
126
+ ])
127
+
128
+ # Return the content of the top document
129
+ return formatted_search_docs
130
+
131
+ @tool
132
+ def arxiv_search(query: str) -> str:
133
+ """
134
+ Search Arxiv for academic papers based on a query and return summaries of top results.
135
+
136
+ Args:
137
+ query (str): The search query for Arxiv.
138
+
139
+ Returns:
140
+ str: Summary of the top few relevant papers from Arxiv.
141
+ """
142
+ try:
143
+ loader = ArxivLoader(query=query, load_max_docs=2)
144
+ documents = loader.load()
145
+
146
+ if not documents:
147
+ return "No relevant papers found on Arxiv."
148
+
149
+ # Format and return top paper summaries
150
+ results = []
151
+ for doc in documents:
152
+ title = doc.metadata.get("Title", "No Title")
153
+ published = doc.metadata.get("Published", "Unknown date")
154
+ url = doc.metadata.get("entry_id", "No URL")
155
+ summary = doc.page_content[:500] # limit summary length
156
+
157
+ results.append(f"Title: {title}\nPublished: {published}\nURL: {url}\nSummary: {summary}\n")
158
+
159
+ return "\n---\n".join(results)
160
+
161
+ except Exception as e:
162
+ return f"An error occurred while searching Arxiv: {str(e)}"
163
+
164
+ @tool
165
+ def check_commutativity(table_str: str) -> str:
166
+ """
167
+ Given a binary operation table (in markdown format), returns the subset of elements
168
+ involved in counter-examples to commutativity, sorted alphabetically.
169
+
170
+ Args:
171
+ table_str (str): Markdown table defining the operation * on a finite set.
172
+
173
+ Returns:
174
+ str: Comma-separated list of elements in the counter-example set, alphabetically sorted.
175
+ """
176
+ # Read the table using pandas
177
+ df = pd.read_csv(StringIO(table_str), sep="|", skipinitialspace=True, engine='python')
178
+
179
+ # Drop empty columns due to leading/trailing pipes
180
+ df = df.dropna(axis=1, how="all")
181
+ df.columns = [c.strip() for c in df.columns]
182
+ df = df.dropna(axis=0, how="all")
183
+
184
+ # Extract header and values
185
+ elements = df.columns[1:]
186
+ df.index = df[df.columns[0]]
187
+ df = df.drop(df.columns[0], axis=1)
188
+
189
+ # Check commutativity: a*b == b*a
190
+ counterexample_elements = set()
191
+ for x in elements:
192
+ for y in elements:
193
+ if df.loc[x, y] != df.loc[y, x]:
194
+ counterexample_elements.add(x)
195
+ counterexample_elements.add(y)
196
+
197
+ return ", ".join(sorted(counterexample_elements))
198
+
199
+ @tool
200
+ def extract_sales_data_from_excel(url: str) -> str:
201
+ """
202
+ Downloads and extracts sales data from an Excel file at the given URL.
203
+ Returns the contents of the first sheet as a markdown-formatted string.
204
+ """
205
+ try:
206
+ response = requests.get(url)
207
+ response.raise_for_status()
208
+
209
+ excel_file = BytesIO(response.content)
210
+ df = pd.read_excel(excel_file)
211
+
212
+ # Optional: Remove unnamed columns often created by Excel
213
+ df = df.loc[:, ~df.columns.str.contains('^Unnamed')]
214
+
215
+ # Convert all numeric columns to float
216
+ for col in df.select_dtypes(include=["number"]).columns:
217
+ df[col] = df[col].astype(float)
218
+
219
+ return df.to_string(index=False)
220
+
221
+ except Exception as e:
222
+ return f"Failed to process Excel file from URL: {str(e)}"
223
+
224
+ @tool
225
+ def extract_transcript_from_youtube(url: str) -> str:
226
+ """
227
+ Extracts the transcript from a YouTube video given its URL.
228
+
229
+ Args:
230
+ url (str): The YouTube video URL.
231
+ Returns:
232
+ str: The transcript of the video, or an error message if extraction fails.
233
+ """
234
+ transcript_str = "### Transcript"
235
+ md = MarkItDown(enable_plugins=True)
236
+
237
+ try:
238
+ result = md.convert(url)
239
+ except Exception as e:
240
+ return f"Failed to extract transcript from YouTube video: {str(e)}"
241
+
242
+ parts = result.text_content.split(transcript_str)
243
+ if len(parts) < 2:
244
+ return result.text_content
245
+
246
+ transcript = transcript_str + "\n" + parts[1]
247
+ return transcript.strip()
248
+
249
+ # @tool
250
+ # def extract_transcript_from_audio(url: str) -> str:
251
+ # """
252
+ # Extracts the transcript from an audio file given its URL.
253
+ # Supported formats: mp3, wav.
254
+
255
+ # Args:
256
+ # url (str): The URL of the audio file.
257
+ # Returns:
258
+ # str: The transcript of the audio file, or an error message if extraction fails.
259
+ # """
260
+ # md = MarkItDown(enable_plugins=True)
261
+
262
+ # try:
263
+ # result = md.convert(url)
264
+ # except Exception as e:
265
+ # return f"Failed to extract transcript from audio: {str(e)}"
266
+
267
+ # return result.text_content