Spaces:
Sleeping
Sleeping
restructuring
Browse files- agent.py +47 -62
- app.py +4 -5
- app_template.py +0 -1
- cocolabelmap.py +1 -2
- fullreq.txt +309 -0
- langtools.py +20 -14
- requirements.txt +15 -8
- setup_actions.ipynb +30 -20
- tools copy.py +55 -61
- tools.py +326 -241
- tools_beta.py +222 -228
- utils.py +11 -10
agent.py
CHANGED
|
@@ -1,46 +1,39 @@
|
|
| 1 |
-
from dotenv import load_dotenv
|
| 2 |
import os
|
| 3 |
-
from typing import
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# Import custom tools
|
| 10 |
from Final_Assignment_Template.tools import (
|
| 11 |
-
|
| 12 |
-
WikipediaSearchTool,
|
| 13 |
-
VisitWebpageTool,
|
| 14 |
-
TranscribeAudioTool,
|
| 15 |
-
TranscibeVideoFileTool,
|
| 16 |
-
BraveWebSearchTool,
|
| 17 |
-
DescribeImageTool,
|
| 18 |
ArxivSearchTool,
|
| 19 |
DownloadFileFromLinkTool,
|
| 20 |
DuckDuckGoSearchTool,
|
| 21 |
-
AddDocumentToVectorStoreTool,
|
| 22 |
QueryVectorStoreTool,
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
# Import utility functions
|
| 27 |
-
from utils import
|
| 28 |
-
|
| 29 |
-
# Import SmolaAgents tools
|
| 30 |
-
from smolagents.default_tools import (
|
| 31 |
-
PythonInterpreterTool,
|
| 32 |
-
FinalAnswerTool
|
| 33 |
-
)
|
| 34 |
|
| 35 |
-
# Import
|
| 36 |
-
from smolagents import OpenAIServerModel, LiteLLMModel, CodeAgent, HfApiModel
|
| 37 |
|
| 38 |
|
| 39 |
class BoomBot:
|
| 40 |
def __init__(self, provider="deepinfra"):
|
| 41 |
"""
|
| 42 |
Initialize the BoomBot with the specified provider.
|
| 43 |
-
|
| 44 |
Args:
|
| 45 |
provider (str): The model provider to use (e.g., "groq", "qwen", "gemma", "anthropic", "deepinfra", "meta")
|
| 46 |
"""
|
|
@@ -48,11 +41,11 @@ class BoomBot:
|
|
| 48 |
self.provider = provider
|
| 49 |
self.model = self._initialize_model()
|
| 50 |
self.agent = self._create_agent()
|
| 51 |
-
|
| 52 |
def _initialize_model(self):
|
| 53 |
"""
|
| 54 |
Initialize the appropriate model based on the provider.
|
| 55 |
-
|
| 56 |
Returns:
|
| 57 |
The initialized model object
|
| 58 |
"""
|
|
@@ -60,10 +53,10 @@ class BoomBot:
|
|
| 60 |
qwen_model = "ollama_chat/qwen3:8b"
|
| 61 |
return LiteLLMModel(
|
| 62 |
model_id=qwen_model,
|
| 63 |
-
device=
|
| 64 |
num_ctx=32768,
|
| 65 |
temperature=0.6,
|
| 66 |
-
top_p=0.95
|
| 67 |
)
|
| 68 |
elif self.provider == "gemma":
|
| 69 |
gemma_model = "ollama_chat/gemma3:12b-it-qat"
|
|
@@ -71,18 +64,14 @@ class BoomBot:
|
|
| 71 |
model_id=gemma_model,
|
| 72 |
num_ctx=65536,
|
| 73 |
temperature=1.0,
|
| 74 |
-
device=
|
| 75 |
top_k=64,
|
| 76 |
top_p=0.95,
|
| 77 |
-
min_p=0.0
|
| 78 |
)
|
| 79 |
elif self.provider == "anthropic":
|
| 80 |
model_id = "anthropic/claude-3-5-sonnet-latest"
|
| 81 |
-
return LiteLLMModel(
|
| 82 |
-
model_id=model_id,
|
| 83 |
-
temperature=0.6,
|
| 84 |
-
max_tokens=8192
|
| 85 |
-
)
|
| 86 |
elif self.provider == "deepinfra":
|
| 87 |
deepinfra_model = "Qwen/Qwen3-235B-A22B"
|
| 88 |
return OpenAIServerModel(
|
|
@@ -91,7 +80,7 @@ class BoomBot:
|
|
| 91 |
api_key=os.environ["DEEPINFRA_API_KEY"],
|
| 92 |
flatten_messages_as_text=True,
|
| 93 |
max_tokens=8192,
|
| 94 |
-
temperature=0.1
|
| 95 |
)
|
| 96 |
elif self.provider == "meta":
|
| 97 |
meta_model = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
|
|
@@ -101,23 +90,19 @@ class BoomBot:
|
|
| 101 |
api_key=os.environ["DEEPINFRA_API_KEY"],
|
| 102 |
flatten_messages_as_text=True,
|
| 103 |
max_tokens=8192,
|
| 104 |
-
temperature=0.7
|
| 105 |
)
|
| 106 |
elif self.provider == "groq":
|
| 107 |
# Default to use groq's claude-3-opus or llama-3
|
| 108 |
model_id = "claude-3-opus-20240229"
|
| 109 |
-
return LiteLLMModel(
|
| 110 |
-
model_id=model_id,
|
| 111 |
-
temperature=0.7,
|
| 112 |
-
max_tokens=8192
|
| 113 |
-
)
|
| 114 |
else:
|
| 115 |
raise ValueError(f"Unsupported provider: {self.provider}")
|
| 116 |
-
|
| 117 |
def _create_agent(self):
|
| 118 |
"""
|
| 119 |
Create and configure the agent with all necessary tools.
|
| 120 |
-
|
| 121 |
Returns:
|
| 122 |
The configured CodeAgent
|
| 123 |
"""
|
|
@@ -132,11 +117,11 @@ class BoomBot:
|
|
| 132 |
arxiv_search = ArxivSearchTool()
|
| 133 |
add_doc_vectorstore = AddDocumentToVectorStoreTool()
|
| 134 |
retrieve_doc_vectorstore = QueryVectorStoreTool()
|
| 135 |
-
|
| 136 |
# SmolaAgents default tools
|
| 137 |
python_interpreter = PythonInterpreterTool()
|
| 138 |
final_answer = FinalAnswerTool()
|
| 139 |
-
|
| 140 |
# Combine all tools
|
| 141 |
agent_tools = [
|
| 142 |
web_searcher,
|
|
@@ -150,9 +135,9 @@ class BoomBot:
|
|
| 150 |
add_doc_vectorstore,
|
| 151 |
retrieve_doc_vectorstore,
|
| 152 |
python_interpreter,
|
| 153 |
-
final_answer
|
| 154 |
]
|
| 155 |
-
|
| 156 |
# Additional imports for the Python interpreter
|
| 157 |
additional_imports = [
|
| 158 |
"json",
|
|
@@ -178,7 +163,7 @@ class BoomBot:
|
|
| 178 |
"itertools",
|
| 179 |
"functools",
|
| 180 |
]
|
| 181 |
-
|
| 182 |
# Create the agent
|
| 183 |
agent = CodeAgent(
|
| 184 |
tools=agent_tools,
|
|
@@ -186,19 +171,19 @@ class BoomBot:
|
|
| 186 |
model=self.model,
|
| 187 |
add_base_tools=False,
|
| 188 |
stream_outputs=True,
|
| 189 |
-
additional_authorized_imports=additional_imports
|
| 190 |
)
|
| 191 |
-
|
| 192 |
# Modify the system prompt
|
| 193 |
modified_prompt = replace_tool_mentions(agent.system_prompt)
|
| 194 |
agent.system_prompt = modified_prompt + self._get_system_prompt()
|
| 195 |
-
|
| 196 |
return agent
|
| 197 |
-
|
| 198 |
def _get_system_prompt(self):
|
| 199 |
"""
|
| 200 |
Return the system prompt for the agent.
|
| 201 |
-
|
| 202 |
Returns:
|
| 203 |
str: The system prompt
|
| 204 |
"""
|
|
@@ -260,32 +245,32 @@ class BoomBot:
|
|
| 260 |
- List → comma-separated, one space (e.g., 2, 3, 4)
|
| 261 |
- Conclude with: FINAL ANSWER: <your_answer>
|
| 262 |
"""
|
| 263 |
-
|
| 264 |
def run(self, question: str, task_id: str, to_download: Bool) -> str:
|
| 265 |
"""
|
| 266 |
Run the agent with the given question, task_id, and download flag.
|
| 267 |
-
|
| 268 |
Args:
|
| 269 |
question (str): The question or task for the agent to process
|
| 270 |
task_id (str): A unique identifier for the task
|
| 271 |
to_download (Bool): Flag indicating whether to download resources
|
| 272 |
-
|
| 273 |
Returns:
|
| 274 |
str: The agent's response
|
| 275 |
"""
|
| 276 |
print(f"BoomBot running with question (first 50 chars): {question[:50]}...")
|
| 277 |
-
|
| 278 |
# Configure any task-specific settings based on the parameters
|
| 279 |
if to_download:
|
| 280 |
# You could set up specific agent configurations here for download tasks
|
| 281 |
pass
|
| 282 |
-
|
| 283 |
# Run the agent with the given question
|
| 284 |
result = self.agent.generate_response(question)
|
| 285 |
-
|
| 286 |
# Extract the final answer from the result
|
| 287 |
final_answer = extract_final_answer(result)
|
| 288 |
-
|
| 289 |
return final_answer
|
| 290 |
|
| 291 |
|
|
@@ -293,4 +278,4 @@ class BoomBot:
|
|
| 293 |
# if __name__ == "__main__":
|
| 294 |
# agent = BasicAgent()
|
| 295 |
# response = agent("What is the current population of Tokyo?", "population_query", True)
|
| 296 |
-
# print(f"Response: {response}")
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
+
from typing import Bool
|
| 3 |
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
# Import models from SmolaAgents
|
| 7 |
+
from smolagents import CodeAgent, LiteLLMModel, OpenAIServerModel
|
| 8 |
+
|
| 9 |
+
# Import SmolaAgents tools
|
| 10 |
+
from smolagents.default_tools import FinalAnswerTool, PythonInterpreterTool
|
| 11 |
|
| 12 |
# Import custom tools
|
| 13 |
from Final_Assignment_Template.tools import (
|
| 14 |
+
AddDocumentToVectorStoreTool,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
ArxivSearchTool,
|
| 16 |
DownloadFileFromLinkTool,
|
| 17 |
DuckDuckGoSearchTool,
|
|
|
|
| 18 |
QueryVectorStoreTool,
|
| 19 |
+
ReadFileContentTool,
|
| 20 |
+
TranscibeVideoFileTool,
|
| 21 |
+
TranscribeAudioTool,
|
| 22 |
+
VisitWebpageTool,
|
| 23 |
+
WikipediaSearchTool,
|
| 24 |
)
|
| 25 |
|
| 26 |
# Import utility functions
|
| 27 |
+
from utils import extract_final_answer, replace_tool_mentions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
# Import tools from LangChain
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
class BoomBot:
|
| 33 |
def __init__(self, provider="deepinfra"):
|
| 34 |
"""
|
| 35 |
Initialize the BoomBot with the specified provider.
|
| 36 |
+
|
| 37 |
Args:
|
| 38 |
provider (str): The model provider to use (e.g., "groq", "qwen", "gemma", "anthropic", "deepinfra", "meta")
|
| 39 |
"""
|
|
|
|
| 41 |
self.provider = provider
|
| 42 |
self.model = self._initialize_model()
|
| 43 |
self.agent = self._create_agent()
|
| 44 |
+
|
| 45 |
def _initialize_model(self):
|
| 46 |
"""
|
| 47 |
Initialize the appropriate model based on the provider.
|
| 48 |
+
|
| 49 |
Returns:
|
| 50 |
The initialized model object
|
| 51 |
"""
|
|
|
|
| 53 |
qwen_model = "ollama_chat/qwen3:8b"
|
| 54 |
return LiteLLMModel(
|
| 55 |
model_id=qwen_model,
|
| 56 |
+
device="cuda",
|
| 57 |
num_ctx=32768,
|
| 58 |
temperature=0.6,
|
| 59 |
+
top_p=0.95,
|
| 60 |
)
|
| 61 |
elif self.provider == "gemma":
|
| 62 |
gemma_model = "ollama_chat/gemma3:12b-it-qat"
|
|
|
|
| 64 |
model_id=gemma_model,
|
| 65 |
num_ctx=65536,
|
| 66 |
temperature=1.0,
|
| 67 |
+
device="cuda",
|
| 68 |
top_k=64,
|
| 69 |
top_p=0.95,
|
| 70 |
+
min_p=0.0,
|
| 71 |
)
|
| 72 |
elif self.provider == "anthropic":
|
| 73 |
model_id = "anthropic/claude-3-5-sonnet-latest"
|
| 74 |
+
return LiteLLMModel(model_id=model_id, temperature=0.6, max_tokens=8192)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
elif self.provider == "deepinfra":
|
| 76 |
deepinfra_model = "Qwen/Qwen3-235B-A22B"
|
| 77 |
return OpenAIServerModel(
|
|
|
|
| 80 |
api_key=os.environ["DEEPINFRA_API_KEY"],
|
| 81 |
flatten_messages_as_text=True,
|
| 82 |
max_tokens=8192,
|
| 83 |
+
temperature=0.1,
|
| 84 |
)
|
| 85 |
elif self.provider == "meta":
|
| 86 |
meta_model = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
|
|
|
|
| 90 |
api_key=os.environ["DEEPINFRA_API_KEY"],
|
| 91 |
flatten_messages_as_text=True,
|
| 92 |
max_tokens=8192,
|
| 93 |
+
temperature=0.7,
|
| 94 |
)
|
| 95 |
elif self.provider == "groq":
|
| 96 |
# Default to use groq's claude-3-opus or llama-3
|
| 97 |
model_id = "claude-3-opus-20240229"
|
| 98 |
+
return LiteLLMModel(model_id=model_id, temperature=0.7, max_tokens=8192)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
else:
|
| 100 |
raise ValueError(f"Unsupported provider: {self.provider}")
|
| 101 |
+
|
| 102 |
def _create_agent(self):
|
| 103 |
"""
|
| 104 |
Create and configure the agent with all necessary tools.
|
| 105 |
+
|
| 106 |
Returns:
|
| 107 |
The configured CodeAgent
|
| 108 |
"""
|
|
|
|
| 117 |
arxiv_search = ArxivSearchTool()
|
| 118 |
add_doc_vectorstore = AddDocumentToVectorStoreTool()
|
| 119 |
retrieve_doc_vectorstore = QueryVectorStoreTool()
|
| 120 |
+
|
| 121 |
# SmolaAgents default tools
|
| 122 |
python_interpreter = PythonInterpreterTool()
|
| 123 |
final_answer = FinalAnswerTool()
|
| 124 |
+
|
| 125 |
# Combine all tools
|
| 126 |
agent_tools = [
|
| 127 |
web_searcher,
|
|
|
|
| 135 |
add_doc_vectorstore,
|
| 136 |
retrieve_doc_vectorstore,
|
| 137 |
python_interpreter,
|
| 138 |
+
final_answer,
|
| 139 |
]
|
| 140 |
+
|
| 141 |
# Additional imports for the Python interpreter
|
| 142 |
additional_imports = [
|
| 143 |
"json",
|
|
|
|
| 163 |
"itertools",
|
| 164 |
"functools",
|
| 165 |
]
|
| 166 |
+
|
| 167 |
# Create the agent
|
| 168 |
agent = CodeAgent(
|
| 169 |
tools=agent_tools,
|
|
|
|
| 171 |
model=self.model,
|
| 172 |
add_base_tools=False,
|
| 173 |
stream_outputs=True,
|
| 174 |
+
additional_authorized_imports=additional_imports,
|
| 175 |
)
|
| 176 |
+
|
| 177 |
# Modify the system prompt
|
| 178 |
modified_prompt = replace_tool_mentions(agent.system_prompt)
|
| 179 |
agent.system_prompt = modified_prompt + self._get_system_prompt()
|
| 180 |
+
|
| 181 |
return agent
|
| 182 |
+
|
| 183 |
def _get_system_prompt(self):
|
| 184 |
"""
|
| 185 |
Return the system prompt for the agent.
|
| 186 |
+
|
| 187 |
Returns:
|
| 188 |
str: The system prompt
|
| 189 |
"""
|
|
|
|
| 245 |
- List → comma-separated, one space (e.g., 2, 3, 4)
|
| 246 |
- Conclude with: FINAL ANSWER: <your_answer>
|
| 247 |
"""
|
| 248 |
+
|
| 249 |
def run(self, question: str, task_id: str, to_download: Bool) -> str:
|
| 250 |
"""
|
| 251 |
Run the agent with the given question, task_id, and download flag.
|
| 252 |
+
|
| 253 |
Args:
|
| 254 |
question (str): The question or task for the agent to process
|
| 255 |
task_id (str): A unique identifier for the task
|
| 256 |
to_download (Bool): Flag indicating whether to download resources
|
| 257 |
+
|
| 258 |
Returns:
|
| 259 |
str: The agent's response
|
| 260 |
"""
|
| 261 |
print(f"BoomBot running with question (first 50 chars): {question[:50]}...")
|
| 262 |
+
|
| 263 |
# Configure any task-specific settings based on the parameters
|
| 264 |
if to_download:
|
| 265 |
# You could set up specific agent configurations here for download tasks
|
| 266 |
pass
|
| 267 |
+
|
| 268 |
# Run the agent with the given question
|
| 269 |
result = self.agent.generate_response(question)
|
| 270 |
+
|
| 271 |
# Extract the final answer from the result
|
| 272 |
final_answer = extract_final_answer(result)
|
| 273 |
+
|
| 274 |
return final_answer
|
| 275 |
|
| 276 |
|
|
|
|
| 278 |
# if __name__ == "__main__":
|
| 279 |
# agent = BasicAgent()
|
| 280 |
# response = agent("What is the current population of Tokyo?", "population_query", True)
|
| 281 |
+
# print(f"Response: {response}")
|
app.py
CHANGED
|
@@ -4,8 +4,7 @@ import os
|
|
| 4 |
import gradio as gr
|
| 5 |
import pandas as pd
|
| 6 |
import requests
|
| 7 |
-
from
|
| 8 |
-
from traitlets import Bool # type: ignore
|
| 9 |
|
| 10 |
from agent import BoomBot
|
| 11 |
|
|
@@ -15,16 +14,16 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
|
| 15 |
|
| 16 |
|
| 17 |
# --- Basic Agent Definition --
|
| 18 |
-
# --- Basic Agent Definition ---
|
| 19 |
class BasicAgent:
|
| 20 |
def __init__(self):
|
| 21 |
print("BasicAgent initialized.")
|
| 22 |
self.agent = BoomBot(provider="groq")
|
| 23 |
-
|
| 24 |
def __call__(self, question: str, task_id: str, to_download: Bool) -> str:
|
| 25 |
print(f"Agent received question (first 50 chars): {question[:50]}...")
|
| 26 |
return self.agent.run(question, task_id, to_download)
|
| 27 |
|
|
|
|
| 28 |
def run_and_submit_all(profile: gr.OAuthProfile | None):
|
| 29 |
"""
|
| 30 |
Fetches all questions, runs the BasicAgent on them, submits all answers,
|
|
@@ -93,7 +92,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 93 |
print(f"Skipping item with missing task_id or question: {item}")
|
| 94 |
continue
|
| 95 |
try:
|
| 96 |
-
submitted_answer = agent(question_text, task_id, to_download
|
| 97 |
answers_payload.append(
|
| 98 |
{"task_id": task_id, "submitted_answer": submitted_answer}
|
| 99 |
)
|
|
|
|
| 4 |
import gradio as gr
|
| 5 |
import pandas as pd
|
| 6 |
import requests
|
| 7 |
+
from traitlets import Bool # type: ignore
|
|
|
|
| 8 |
|
| 9 |
from agent import BoomBot
|
| 10 |
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
# --- Basic Agent Definition --
|
|
|
|
| 17 |
class BasicAgent:
|
| 18 |
def __init__(self):
|
| 19 |
print("BasicAgent initialized.")
|
| 20 |
self.agent = BoomBot(provider="groq")
|
| 21 |
+
|
| 22 |
def __call__(self, question: str, task_id: str, to_download: Bool) -> str:
|
| 23 |
print(f"Agent received question (first 50 chars): {question[:50]}...")
|
| 24 |
return self.agent.run(question, task_id, to_download)
|
| 25 |
|
| 26 |
+
|
| 27 |
def run_and_submit_all(profile: gr.OAuthProfile | None):
|
| 28 |
"""
|
| 29 |
Fetches all questions, runs the BasicAgent on them, submits all answers,
|
|
|
|
| 92 |
print(f"Skipping item with missing task_id or question: {item}")
|
| 93 |
continue
|
| 94 |
try:
|
| 95 |
+
submitted_answer = agent(question_text, task_id, to_download=to_download)
|
| 96 |
answers_payload.append(
|
| 97 |
{"task_id": task_id, "submitted_answer": submitted_answer}
|
| 98 |
)
|
app_template.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
import inspect
|
| 2 |
import os
|
| 3 |
|
| 4 |
import gradio as gr
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
import gradio as gr
|
cocolabelmap.py
CHANGED
|
@@ -181,6 +181,5 @@ LABEL_MAP = {
|
|
| 181 |
179: "waterdrops",
|
| 182 |
180: "window",
|
| 183 |
181: "window",
|
| 184 |
-
182: "wood"
|
| 185 |
}
|
| 186 |
-
|
|
|
|
| 181 |
179: "waterdrops",
|
| 182 |
180: "window",
|
| 183 |
181: "window",
|
| 184 |
+
182: "wood",
|
| 185 |
}
|
|
|
fullreq.txt
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
accelerate==1.6.0
|
| 2 |
+
aiofiles==23.2.1
|
| 3 |
+
aiohappyeyeballs==2.4.4
|
| 4 |
+
aiohttp==3.11.18
|
| 5 |
+
aiosignal==1.3.2
|
| 6 |
+
alabaster==1.0.0
|
| 7 |
+
altair==5.5.0
|
| 8 |
+
annotated-types==0.7.0
|
| 9 |
+
anyio==4.8.0
|
| 10 |
+
arrow==1.3.0
|
| 11 |
+
arxiv==2.2.0
|
| 12 |
+
asgiref==3.8.1
|
| 13 |
+
astroid==3.3.9
|
| 14 |
+
asttokens==3.0.0
|
| 15 |
+
attrs==25.1.0
|
| 16 |
+
babel==2.17.0
|
| 17 |
+
backoff==2.2.1
|
| 18 |
+
bcrypt==4.2.1
|
| 19 |
+
beautifulsoup4==4.12.3
|
| 20 |
+
bibtexparser==1.4.3
|
| 21 |
+
black==25.1.0
|
| 22 |
+
blinker==1.9.0
|
| 23 |
+
bs4==0.0.2
|
| 24 |
+
build==1.2.2.post1
|
| 25 |
+
cachetools==5.5.1
|
| 26 |
+
certifi==2025.1.31
|
| 27 |
+
chardet==5.2.0
|
| 28 |
+
charset-normalizer==3.4.1
|
| 29 |
+
chess==1.11.2
|
| 30 |
+
Chroma==0.2.0
|
| 31 |
+
chroma-hnswlib==0.7.6
|
| 32 |
+
chromadb==1.0.9
|
| 33 |
+
click==8.1.8
|
| 34 |
+
colorama==0.4.6
|
| 35 |
+
coloredlogs==15.0.1
|
| 36 |
+
comm==0.2.2
|
| 37 |
+
contourpy==1.3.2
|
| 38 |
+
cssselect==1.3.0
|
| 39 |
+
cycler==0.12.1
|
| 40 |
+
dataclasses-json==0.6.7
|
| 41 |
+
datasets==3.5.1
|
| 42 |
+
debugpy==1.8.13
|
| 43 |
+
decorator==5.2.1
|
| 44 |
+
Deprecated==1.2.18
|
| 45 |
+
dill==0.3.8
|
| 46 |
+
diskcache==5.6.3
|
| 47 |
+
distro==1.9.0
|
| 48 |
+
docutils==0.21.2
|
| 49 |
+
duckduckgo_search==8.0.2
|
| 50 |
+
durationpy==0.9
|
| 51 |
+
et_xmlfile==2.0.0
|
| 52 |
+
executing==2.2.0
|
| 53 |
+
fake-useragent==2.2.0
|
| 54 |
+
fastapi==0.115.9
|
| 55 |
+
feedparser==6.0.11
|
| 56 |
+
ffmpy==0.5.0
|
| 57 |
+
filelock==3.17.0
|
| 58 |
+
flake8==7.2.0
|
| 59 |
+
flatbuffers==25.1.24
|
| 60 |
+
fonttools==4.58.0
|
| 61 |
+
free_proxy==1.1.3
|
| 62 |
+
frozenlist==1.5.0
|
| 63 |
+
fsspec==2024.12.0
|
| 64 |
+
gguf==0.16.3
|
| 65 |
+
git-filter-repo==2.47.0
|
| 66 |
+
gitdb==4.0.12
|
| 67 |
+
GitPython==3.1.44
|
| 68 |
+
google-auth==2.38.0
|
| 69 |
+
google_search_results==2.4.2
|
| 70 |
+
googleapis-common-protos==1.66.0
|
| 71 |
+
gradio==5.14.0
|
| 72 |
+
gradio_client==1.7.0
|
| 73 |
+
greenlet==3.1.1
|
| 74 |
+
grpcio==1.70.0
|
| 75 |
+
h11==0.14.0
|
| 76 |
+
hf-xet==1.1.0
|
| 77 |
+
httpcore==1.0.7
|
| 78 |
+
httptools==0.6.4
|
| 79 |
+
httpx==0.27.2
|
| 80 |
+
httpx-sse==0.4.0
|
| 81 |
+
huggingface-hub==0.31.1
|
| 82 |
+
humanfriendly==10.0
|
| 83 |
+
idna==3.10
|
| 84 |
+
imagesize==1.4.1
|
| 85 |
+
impact-factor==1.1.2
|
| 86 |
+
importlib_metadata==8.5.0
|
| 87 |
+
importlib_resources==6.5.2
|
| 88 |
+
iniconfig==2.1.0
|
| 89 |
+
ipykernel==6.29.5
|
| 90 |
+
ipython==9.1.0
|
| 91 |
+
ipython_pygments_lexers==1.1.1
|
| 92 |
+
ipywidgets==8.1.7
|
| 93 |
+
isort==6.0.1
|
| 94 |
+
jedi==0.19.2
|
| 95 |
+
Jinja2==3.1.5
|
| 96 |
+
jiter==0.8.2
|
| 97 |
+
joblib==1.4.2
|
| 98 |
+
jsonpatch==1.33
|
| 99 |
+
jsonpointer==3.0.0
|
| 100 |
+
jsonschema==4.23.0
|
| 101 |
+
jsonschema-specifications==2024.10.1
|
| 102 |
+
jupyter_client==8.6.3
|
| 103 |
+
jupyter_core==5.7.2
|
| 104 |
+
jupyterlab_widgets==3.0.15
|
| 105 |
+
kiwisolver==1.4.8
|
| 106 |
+
kubernetes==32.0.0
|
| 107 |
+
langchain==0.3.25
|
| 108 |
+
langchain-chroma==0.2.1
|
| 109 |
+
langchain-community==0.3.16
|
| 110 |
+
langchain-core==0.3.59
|
| 111 |
+
langchain-huggingface==0.1.2
|
| 112 |
+
langchain-tavily==0.1.6
|
| 113 |
+
langchain-text-splitters==0.3.8
|
| 114 |
+
langsmith==0.3.3
|
| 115 |
+
litellm==1.59.10
|
| 116 |
+
llama_cpp_python==0.3.9
|
| 117 |
+
lxml==5.3.0
|
| 118 |
+
lxml_html_clean==0.4.2
|
| 119 |
+
markdown-it-py==3.0.0
|
| 120 |
+
markdownify==0.14.1
|
| 121 |
+
MarkupSafe==2.1.5
|
| 122 |
+
marshmallow==3.26.0
|
| 123 |
+
matplotlib==3.10.3
|
| 124 |
+
matplotlib-inline==0.1.7
|
| 125 |
+
matplotlib-venn==1.1.2
|
| 126 |
+
mccabe==0.7.0
|
| 127 |
+
mdurl==0.1.2
|
| 128 |
+
mmh3==5.1.0
|
| 129 |
+
monotonic==1.6
|
| 130 |
+
mpmath==1.3.0
|
| 131 |
+
multidict==6.1.0
|
| 132 |
+
multiprocess==0.70.16
|
| 133 |
+
mypy==1.15.0
|
| 134 |
+
mypy-extensions==1.0.0
|
| 135 |
+
narwhals==1.24.1
|
| 136 |
+
nest-asyncio==1.6.0
|
| 137 |
+
networkx==3.4.2
|
| 138 |
+
numpy==1.26.4
|
| 139 |
+
nvidia-cublas-cu12==12.6.4.1
|
| 140 |
+
nvidia-cuda-cupti-cu12==12.6.80
|
| 141 |
+
nvidia-cuda-nvrtc-cu12==12.6.77
|
| 142 |
+
nvidia-cuda-runtime-cu12==12.6.77
|
| 143 |
+
nvidia-cudnn-cu12==9.5.1.17
|
| 144 |
+
nvidia-cufft-cu12==11.3.0.4
|
| 145 |
+
nvidia-cufile-cu12==1.11.1.6
|
| 146 |
+
nvidia-curand-cu12==10.3.7.77
|
| 147 |
+
nvidia-cusolver-cu12==11.7.1.2
|
| 148 |
+
nvidia-cusparse-cu12==12.5.4.2
|
| 149 |
+
nvidia-cusparselt-cu12==0.6.3
|
| 150 |
+
nvidia-nccl-cu12==2.26.2
|
| 151 |
+
nvidia-nvjitlink-cu12==12.6.85
|
| 152 |
+
nvidia-nvtx-cu12==12.6.77
|
| 153 |
+
oauthlib==3.2.2
|
| 154 |
+
ollama==0.4.8
|
| 155 |
+
onnxruntime==1.20.1
|
| 156 |
+
openai==1.60.2
|
| 157 |
+
opencv-python==4.11.0.86
|
| 158 |
+
openpyxl==3.1.5
|
| 159 |
+
opentelemetry-api==1.29.0
|
| 160 |
+
opentelemetry-exporter-otlp-proto-common==1.29.0
|
| 161 |
+
opentelemetry-exporter-otlp-proto-grpc==1.29.0
|
| 162 |
+
opentelemetry-instrumentation==0.50b0
|
| 163 |
+
opentelemetry-instrumentation-asgi==0.50b0
|
| 164 |
+
opentelemetry-instrumentation-fastapi==0.50b0
|
| 165 |
+
opentelemetry-proto==1.29.0
|
| 166 |
+
opentelemetry-sdk==1.29.0
|
| 167 |
+
opentelemetry-semantic-conventions==0.50b0
|
| 168 |
+
opentelemetry-util-http==0.50b0
|
| 169 |
+
orjson==3.10.15
|
| 170 |
+
outcome==1.3.0.post0
|
| 171 |
+
overrides==7.7.0
|
| 172 |
+
packaging==24.2
|
| 173 |
+
pandas==2.2.3
|
| 174 |
+
paperscraper==0.3.0
|
| 175 |
+
parso==0.8.4
|
| 176 |
+
pathspec==0.12.1
|
| 177 |
+
pexpect==4.9.0
|
| 178 |
+
pillow==11.1.0
|
| 179 |
+
platformdirs==4.3.7
|
| 180 |
+
pluggy==1.6.0
|
| 181 |
+
posthog==3.11.0
|
| 182 |
+
prettytable==3.16.0
|
| 183 |
+
primp==0.15.0
|
| 184 |
+
prompt_toolkit==3.0.50
|
| 185 |
+
propcache==0.2.1
|
| 186 |
+
protobuf==5.29.3
|
| 187 |
+
psutil==7.0.0
|
| 188 |
+
ptyprocess==0.7.0
|
| 189 |
+
pure_eval==0.2.3
|
| 190 |
+
pyarrow==19.0.0
|
| 191 |
+
pyasn1==0.6.1
|
| 192 |
+
pyasn1_modules==0.4.1
|
| 193 |
+
pycodestyle==2.13.0
|
| 194 |
+
pydantic==2.10.6
|
| 195 |
+
pydantic-settings==2.7.1
|
| 196 |
+
pydantic_core==2.27.2
|
| 197 |
+
pydeck==0.9.1
|
| 198 |
+
pydub==0.25.1
|
| 199 |
+
pyflakes==3.3.2
|
| 200 |
+
Pygments==2.19.1
|
| 201 |
+
pylint==3.3.6
|
| 202 |
+
pymed_paperscraper==1.0.4
|
| 203 |
+
PyMuPDF==1.25.5
|
| 204 |
+
pyparsing==3.2.3
|
| 205 |
+
pypdf==5.2.0
|
| 206 |
+
PyPika==0.48.9
|
| 207 |
+
pyproject_hooks==1.2.0
|
| 208 |
+
pyreadline3==3.5.4
|
| 209 |
+
PySocks==1.7.1
|
| 210 |
+
pytesseract==0.3.13
|
| 211 |
+
pytest==8.3.5
|
| 212 |
+
python-dateutil==2.9.0.post0
|
| 213 |
+
python-dotenv==1.0.1
|
| 214 |
+
python-multipart==0.0.20
|
| 215 |
+
pytz==2025.1
|
| 216 |
+
PyYAML==6.0.2
|
| 217 |
+
pyzmq==26.4.0
|
| 218 |
+
rank-bm25==0.2.2
|
| 219 |
+
RapidFuzz==3.13.0
|
| 220 |
+
readability-lxml==0.8.4.1
|
| 221 |
+
referencing==0.36.2
|
| 222 |
+
regex==2024.11.6
|
| 223 |
+
requests==2.32.3
|
| 224 |
+
requests-file==2.1.0
|
| 225 |
+
requests-oauthlib==2.0.0
|
| 226 |
+
requests-toolbelt==1.0.0
|
| 227 |
+
rich==13.9.4
|
| 228 |
+
roman-numerals-py==3.1.0
|
| 229 |
+
rpds-py==0.22.3
|
| 230 |
+
rsa==4.9
|
| 231 |
+
ruff==0.9.4
|
| 232 |
+
safehttpx==0.1.6
|
| 233 |
+
safetensors==0.5.2
|
| 234 |
+
scholarly==1.7.11
|
| 235 |
+
scikit-learn==1.6.1
|
| 236 |
+
scipy==1.15.1
|
| 237 |
+
seaborn==0.13.2
|
| 238 |
+
selenium==4.32.0
|
| 239 |
+
semantic-version==2.10.0
|
| 240 |
+
semanticscholar==0.10.0
|
| 241 |
+
sentence-transformers==3.4.1
|
| 242 |
+
sentencepiece==0.2.0
|
| 243 |
+
sgmllib3k==1.0.0
|
| 244 |
+
shellingham==1.5.4
|
| 245 |
+
simple-loggers==1.0.5
|
| 246 |
+
six==1.17.0
|
| 247 |
+
smmap==5.0.2
|
| 248 |
+
smolagents==1.15.0
|
| 249 |
+
sniffio==1.3.1
|
| 250 |
+
snowballstemmer==3.0.1
|
| 251 |
+
sortedcontainers==2.4.0
|
| 252 |
+
soupsieve==2.6
|
| 253 |
+
SpeechRecognition==3.14.3
|
| 254 |
+
Sphinx==8.2.3
|
| 255 |
+
sphinx-rtd-theme==3.0.2
|
| 256 |
+
sphinxcontrib-applehelp==2.0.0
|
| 257 |
+
sphinxcontrib-devhelp==2.0.0
|
| 258 |
+
sphinxcontrib-htmlhelp==2.1.0
|
| 259 |
+
sphinxcontrib-jquery==4.1
|
| 260 |
+
sphinxcontrib-jsmath==1.0.1
|
| 261 |
+
sphinxcontrib-qthelp==2.0.0
|
| 262 |
+
sphinxcontrib-serializinghtml==2.0.0
|
| 263 |
+
sql-manager==1.0.5
|
| 264 |
+
SQLAlchemy==2.0.37
|
| 265 |
+
stack-data==0.6.3
|
| 266 |
+
starlette==0.45.3
|
| 267 |
+
streamlit==1.41.1
|
| 268 |
+
sympy==1.14.0
|
| 269 |
+
tenacity==9.0.0
|
| 270 |
+
thefuzz==0.22.1
|
| 271 |
+
threadpoolctl==3.5.0
|
| 272 |
+
tiktoken==0.8.0
|
| 273 |
+
tldextract==5.3.0
|
| 274 |
+
tokenizers==0.21.0
|
| 275 |
+
toml==0.10.2
|
| 276 |
+
tomlkit==0.13.2
|
| 277 |
+
torch==2.7.0
|
| 278 |
+
torchvision==0.22.0
|
| 279 |
+
tornado==6.4.2
|
| 280 |
+
tqdm==4.67.1
|
| 281 |
+
traitlets==5.14.3
|
| 282 |
+
transformers==4.51.3
|
| 283 |
+
trio==0.30.0
|
| 284 |
+
trio-websocket==0.12.2
|
| 285 |
+
triton==3.3.0
|
| 286 |
+
typer==0.15.1
|
| 287 |
+
types-python-dateutil==2.9.0.20250516
|
| 288 |
+
typing-inspect==0.9.0
|
| 289 |
+
typing_extensions==4.12.2
|
| 290 |
+
tzdata==2025.1
|
| 291 |
+
urllib3==2.3.0
|
| 292 |
+
uvicorn==0.34.0
|
| 293 |
+
uvloop==0.21.0
|
| 294 |
+
watchdog==6.0.0
|
| 295 |
+
watchfiles==1.0.4
|
| 296 |
+
wcwidth==0.2.13
|
| 297 |
+
webrequests==1.0.8
|
| 298 |
+
websocket-client==1.8.0
|
| 299 |
+
websockets==14.2
|
| 300 |
+
whisper==1.1.10
|
| 301 |
+
widgetsnbextension==4.0.14
|
| 302 |
+
wikipedia==1.4.0
|
| 303 |
+
Wikipedia-API==0.8.1
|
| 304 |
+
wrapt==1.17.2
|
| 305 |
+
wsproto==1.2.0
|
| 306 |
+
xxhash==3.5.0
|
| 307 |
+
yarl==1.18.3
|
| 308 |
+
zipp==3.21.0
|
| 309 |
+
zstandard==0.23.0
|
langtools.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
| 1 |
-
import os
|
| 2 |
from dotenv import load_dotenv
|
|
|
|
| 3 |
from langchain_community.tools.tavily_search import TavilySearchResults
|
| 4 |
-
from langchain_community.document_loaders import WikipediaLoader
|
| 5 |
-
from langchain_community.document_loaders import ArxivLoader
|
| 6 |
from langchain_core.tools import tool
|
| 7 |
|
| 8 |
-
|
| 9 |
load_dotenv()
|
| 10 |
|
|
|
|
| 11 |
@tool
|
| 12 |
def multiply(a: int, b: int) -> int:
|
| 13 |
"""Multiply two numbers.
|
|
@@ -17,30 +15,33 @@ def multiply(a: int, b: int) -> int:
|
|
| 17 |
"""
|
| 18 |
return a * b
|
| 19 |
|
|
|
|
| 20 |
@tool
|
| 21 |
def add(a: int, b: int) -> int:
|
| 22 |
"""Add two numbers.
|
| 23 |
-
|
| 24 |
Args:
|
| 25 |
a: first int
|
| 26 |
b: second int
|
| 27 |
"""
|
| 28 |
return a + b
|
| 29 |
|
|
|
|
| 30 |
@tool
|
| 31 |
def subtract(a: int, b: int) -> int:
|
| 32 |
"""Subtract two numbers.
|
| 33 |
-
|
| 34 |
Args:
|
| 35 |
a: first int
|
| 36 |
b: second int
|
| 37 |
"""
|
| 38 |
return a - b
|
| 39 |
|
|
|
|
| 40 |
@tool
|
| 41 |
def divide(a: int, b: int) -> int:
|
| 42 |
"""Divide two numbers.
|
| 43 |
-
|
| 44 |
Args:
|
| 45 |
a: first int
|
| 46 |
b: second int
|
|
@@ -49,20 +50,22 @@ def divide(a: int, b: int) -> int:
|
|
| 49 |
raise ValueError("Cannot divide by zero.")
|
| 50 |
return a / b
|
| 51 |
|
|
|
|
| 52 |
@tool
|
| 53 |
def modulus(a: int, b: int) -> int:
|
| 54 |
"""Get the modulus of two numbers.
|
| 55 |
-
|
| 56 |
Args:
|
| 57 |
a: first int
|
| 58 |
b: second int
|
| 59 |
"""
|
| 60 |
return a % b
|
| 61 |
|
|
|
|
| 62 |
@tool
|
| 63 |
def wiki_search(query: str) -> str:
|
| 64 |
"""Tool to search Wikipedia for a query about a known or historical person or subject and return maximum 2 results.
|
| 65 |
-
|
| 66 |
Args:
|
| 67 |
query: The search query."""
|
| 68 |
search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
|
|
@@ -70,13 +73,15 @@ def wiki_search(query: str) -> str:
|
|
| 70 |
[
|
| 71 |
f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
|
| 72 |
for doc in search_docs
|
| 73 |
-
]
|
|
|
|
| 74 |
return {"wiki_results": formatted_search_docs}
|
| 75 |
|
|
|
|
| 76 |
@tool
|
| 77 |
def web_search(query: str) -> str:
|
| 78 |
"""Search Tavily for a query and return maximum 3 results.
|
| 79 |
-
|
| 80 |
Args:
|
| 81 |
query: The search query."""
|
| 82 |
search_docs = TavilySearchResults(max_results=3).invoke(query=query)
|
|
@@ -84,13 +89,15 @@ def web_search(query: str) -> str:
|
|
| 84 |
[
|
| 85 |
f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
|
| 86 |
for doc in search_docs
|
| 87 |
-
]
|
|
|
|
| 88 |
return {"web_results": formatted_search_docs}
|
| 89 |
|
|
|
|
| 90 |
@tool
|
| 91 |
def arvix_search(query: str) -> str:
|
| 92 |
"""Tool to search Arxiv for a query about a research paper or article and return maximum 3 results.
|
| 93 |
-
|
| 94 |
Args:
|
| 95 |
query: The search query."""
|
| 96 |
search_docs = ArxivLoader(query=query, load_max_docs=3).load()
|
|
@@ -101,4 +108,3 @@ def arvix_search(query: str) -> str:
|
|
| 101 |
# ])
|
| 102 |
# return {"arvix_results": formatted_search_docs}
|
| 103 |
return search_docs
|
| 104 |
-
|
|
|
|
|
|
|
| 1 |
from dotenv import load_dotenv
|
| 2 |
+
from langchain_community.document_loaders import ArxivLoader, WikipediaLoader
|
| 3 |
from langchain_community.tools.tavily_search import TavilySearchResults
|
|
|
|
|
|
|
| 4 |
from langchain_core.tools import tool
|
| 5 |
|
|
|
|
| 6 |
load_dotenv()
|
| 7 |
|
| 8 |
+
|
| 9 |
@tool
|
| 10 |
def multiply(a: int, b: int) -> int:
|
| 11 |
"""Multiply two numbers.
|
|
|
|
| 15 |
"""
|
| 16 |
return a * b
|
| 17 |
|
| 18 |
+
|
| 19 |
@tool
|
| 20 |
def add(a: int, b: int) -> int:
|
| 21 |
"""Add two numbers.
|
| 22 |
+
|
| 23 |
Args:
|
| 24 |
a: first int
|
| 25 |
b: second int
|
| 26 |
"""
|
| 27 |
return a + b
|
| 28 |
|
| 29 |
+
|
| 30 |
@tool
|
| 31 |
def subtract(a: int, b: int) -> int:
|
| 32 |
"""Subtract two numbers.
|
| 33 |
+
|
| 34 |
Args:
|
| 35 |
a: first int
|
| 36 |
b: second int
|
| 37 |
"""
|
| 38 |
return a - b
|
| 39 |
|
| 40 |
+
|
| 41 |
@tool
|
| 42 |
def divide(a: int, b: int) -> int:
|
| 43 |
"""Divide two numbers.
|
| 44 |
+
|
| 45 |
Args:
|
| 46 |
a: first int
|
| 47 |
b: second int
|
|
|
|
| 50 |
raise ValueError("Cannot divide by zero.")
|
| 51 |
return a / b
|
| 52 |
|
| 53 |
+
|
| 54 |
@tool
|
| 55 |
def modulus(a: int, b: int) -> int:
|
| 56 |
"""Get the modulus of two numbers.
|
| 57 |
+
|
| 58 |
Args:
|
| 59 |
a: first int
|
| 60 |
b: second int
|
| 61 |
"""
|
| 62 |
return a % b
|
| 63 |
|
| 64 |
+
|
| 65 |
@tool
|
| 66 |
def wiki_search(query: str) -> str:
|
| 67 |
"""Tool to search Wikipedia for a query about a known or historical person or subject and return maximum 2 results.
|
| 68 |
+
|
| 69 |
Args:
|
| 70 |
query: The search query."""
|
| 71 |
search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
|
|
|
|
| 73 |
[
|
| 74 |
f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
|
| 75 |
for doc in search_docs
|
| 76 |
+
]
|
| 77 |
+
)
|
| 78 |
return {"wiki_results": formatted_search_docs}
|
| 79 |
|
| 80 |
+
|
| 81 |
@tool
|
| 82 |
def web_search(query: str) -> str:
|
| 83 |
"""Search Tavily for a query and return maximum 3 results.
|
| 84 |
+
|
| 85 |
Args:
|
| 86 |
query: The search query."""
|
| 87 |
search_docs = TavilySearchResults(max_results=3).invoke(query=query)
|
|
|
|
| 89 |
[
|
| 90 |
f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
|
| 91 |
for doc in search_docs
|
| 92 |
+
]
|
| 93 |
+
)
|
| 94 |
return {"web_results": formatted_search_docs}
|
| 95 |
|
| 96 |
+
|
| 97 |
@tool
|
| 98 |
def arvix_search(query: str) -> str:
|
| 99 |
"""Tool to search Arxiv for a query about a research paper or article and return maximum 3 results.
|
| 100 |
+
|
| 101 |
Args:
|
| 102 |
query: The search query."""
|
| 103 |
search_docs = ArxivLoader(query=query, load_max_docs=3).load()
|
|
|
|
| 108 |
# ])
|
| 109 |
# return {"arvix_results": formatted_search_docs}
|
| 110 |
return search_docs
|
|
|
requirements.txt
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
gradio
|
| 2 |
-
|
| 3 |
langchain
|
|
|
|
| 4 |
langchain-community
|
| 5 |
langchain-core
|
| 6 |
-
langchain-google-genai
|
| 7 |
-
langchain-huggingface
|
| 8 |
langchain-groq
|
|
|
|
|
|
|
| 9 |
langchain-tavily
|
| 10 |
-
langchain-chroma
|
| 11 |
langgraph
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
| 15 |
python-dotenv
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
beautifulsoup4
|
| 2 |
+
chromadb
|
| 3 |
+
duckduckgo_search
|
| 4 |
gradio
|
| 5 |
+
huggingface_hub
|
| 6 |
langchain
|
| 7 |
+
langchain-chroma
|
| 8 |
langchain-community
|
| 9 |
langchain-core
|
|
|
|
|
|
|
| 10 |
langchain-groq
|
| 11 |
+
langchain-huggingface
|
| 12 |
+
langchain-google-genai
|
| 13 |
langchain-tavily
|
|
|
|
| 14 |
langgraph
|
| 15 |
+
markdownify
|
| 16 |
+
pandas
|
| 17 |
+
protobuf==3.20.*
|
| 18 |
+
PyMuPDF
|
| 19 |
python-dotenv
|
| 20 |
+
requests
|
| 21 |
+
sentence-transformers
|
| 22 |
+
smolagents
|
| 23 |
+
traitlets
|
setup_actions.ipynb
CHANGED
|
@@ -16,9 +16,10 @@
|
|
| 16 |
}
|
| 17 |
],
|
| 18 |
"source": [
|
| 19 |
-
"from dotenv import load_dotenv\n",
|
| 20 |
"import os\n",
|
| 21 |
"\n",
|
|
|
|
|
|
|
| 22 |
"load_dotenv()\n",
|
| 23 |
"token = os.getenv(\"HUGGINGFACE_TOKEN\")\n",
|
| 24 |
"\n",
|
|
@@ -265,7 +266,13 @@
|
|
| 265 |
"source": [
|
| 266 |
"from datasets import load_dataset\n",
|
| 267 |
"\n",
|
| 268 |
-
"dataset = load_dataset(\"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
]
|
| 270 |
},
|
| 271 |
{
|
|
@@ -286,7 +293,9 @@
|
|
| 286 |
}
|
| 287 |
],
|
| 288 |
"source": [
|
| 289 |
-
"os.listdir(
|
|
|
|
|
|
|
| 290 |
]
|
| 291 |
},
|
| 292 |
{
|
|
@@ -307,11 +316,17 @@
|
|
| 307 |
"source": [
|
| 308 |
"from datasets import load_dataset\n",
|
| 309 |
"from langchain.embeddings import HuggingFaceEmbeddings\n",
|
| 310 |
-
"from langchain.vectorstores import Chroma\n",
|
| 311 |
"from langchain.schema import Document\n",
|
|
|
|
| 312 |
"\n",
|
| 313 |
"# Load the GAIA validation dataset\n",
|
| 314 |
-
"dataset = load_dataset(\
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
"# Prepare the embeddings model\n",
|
| 316 |
"embeddings = HuggingFaceEmbeddings(model_name=\"all-MiniLM-L6-v2\")\n",
|
| 317 |
"\n",
|
|
@@ -320,29 +335,24 @@
|
|
| 320 |
"for entry in dataset:\n",
|
| 321 |
" question = entry[\"Question\"]\n",
|
| 322 |
" answer = entry[\"Final answer\"]\n",
|
| 323 |
-
"
|
| 324 |
" # Create a document with both the question and the answer as metadata\n",
|
| 325 |
" metadata = {\n",
|
| 326 |
" \"task_id\": entry[\"task_id\"],\n",
|
| 327 |
" \"steps\": entry[\"Annotator Metadata\"][\"Steps\"],\n",
|
| 328 |
" \"tools\": entry[\"Annotator Metadata\"][\"Tools\"],\n",
|
| 329 |
-
" \"answer\": answer
|
| 330 |
" }\n",
|
| 331 |
-
"
|
| 332 |
" # Add the question to the list of documents\n",
|
| 333 |
-
" documents.append(\n",
|
| 334 |
-
" Document(\n",
|
| 335 |
-
" page_content=question,\n",
|
| 336 |
-
" metadata=metadata\n",
|
| 337 |
-
" )\n",
|
| 338 |
-
" )\n",
|
| 339 |
"\n",
|
| 340 |
"# Insert the documents into Chroma\n",
|
| 341 |
"vectorstore = Chroma.from_documents(\n",
|
| 342 |
" documents=documents,\n",
|
| 343 |
" embedding=embeddings,\n",
|
| 344 |
" collection_name=\"gaia_validation\",\n",
|
| 345 |
-
" persist_directory=\"./chroma_store\"
|
| 346 |
")\n",
|
| 347 |
"\n",
|
| 348 |
"# Persist the data for future use\n",
|
|
@@ -376,19 +386,19 @@
|
|
| 376 |
"for entry in dataset:\n",
|
| 377 |
" # Access the tools used (they are stored in the 'Tools' field of 'Annotator Metadata')\n",
|
| 378 |
" tools = entry[\"Annotator Metadata\"][\"Tools\"]\n",
|
| 379 |
-
"
|
| 380 |
" # Split the tools into a list (since they are stored as a string, we split by line breaks)\n",
|
| 381 |
-
" tools_list = tools.split(
|
| 382 |
-
"
|
| 383 |
" # Add each tool to the set (set automatically ensures uniqueness)\n",
|
| 384 |
" for tool in tools_list:\n",
|
| 385 |
" unique_tools.add(tool.strip()) # Remove any extra spaces or newlines\n",
|
| 386 |
"\n",
|
| 387 |
"# Convert the set of unique tools to a dictionary under the key 'tools'\n",
|
| 388 |
-
"tools_dict = {
|
| 389 |
"\n",
|
| 390 |
"# Print the unique tools to get a sense of what was used\n",
|
| 391 |
-
"print(tools_dict)
|
| 392 |
]
|
| 393 |
},
|
| 394 |
{
|
|
|
|
| 16 |
}
|
| 17 |
],
|
| 18 |
"source": [
|
|
|
|
| 19 |
"import os\n",
|
| 20 |
"\n",
|
| 21 |
+
"from dotenv import load_dotenv\n",
|
| 22 |
+
"\n",
|
| 23 |
"load_dotenv()\n",
|
| 24 |
"token = os.getenv(\"HUGGINGFACE_TOKEN\")\n",
|
| 25 |
"\n",
|
|
|
|
| 266 |
"source": [
|
| 267 |
"from datasets import load_dataset\n",
|
| 268 |
"\n",
|
| 269 |
+
"dataset = load_dataset(\n",
|
| 270 |
+
" \"gaia-benchmark/GAIA\",\n",
|
| 271 |
+
" name=\"2023_level1\",\n",
|
| 272 |
+
" split=\"validation\",\n",
|
| 273 |
+
" trust_remote_code=True,\n",
|
| 274 |
+
" cache_dir=\"ragdata\",\n",
|
| 275 |
+
")"
|
| 276 |
]
|
| 277 |
},
|
| 278 |
{
|
|
|
|
| 293 |
}
|
| 294 |
],
|
| 295 |
"source": [
|
| 296 |
+
"os.listdir(\n",
|
| 297 |
+
" r\"ragdata/gaia-benchmark___gaia/2023_level1/0.0.1/ec492fe4320ee795b1aed6bb46229c5f693226b0f1316347501c24b4baeee005\"\n",
|
| 298 |
+
")"
|
| 299 |
]
|
| 300 |
},
|
| 301 |
{
|
|
|
|
| 316 |
"source": [
|
| 317 |
"from datasets import load_dataset\n",
|
| 318 |
"from langchain.embeddings import HuggingFaceEmbeddings\n",
|
|
|
|
| 319 |
"from langchain.schema import Document\n",
|
| 320 |
+
"from langchain.vectorstores import Chroma\n",
|
| 321 |
"\n",
|
| 322 |
"# Load the GAIA validation dataset\n",
|
| 323 |
+
"dataset = load_dataset(\n",
|
| 324 |
+
" \"gaia-benchmark/GAIA\",\n",
|
| 325 |
+
" name=\"2023_level1\",\n",
|
| 326 |
+
" split=\"validation\",\n",
|
| 327 |
+
" trust_remote_code=True,\n",
|
| 328 |
+
" cache_dir=\"ragdata\",\n",
|
| 329 |
+
")\n",
|
| 330 |
"# Prepare the embeddings model\n",
|
| 331 |
"embeddings = HuggingFaceEmbeddings(model_name=\"all-MiniLM-L6-v2\")\n",
|
| 332 |
"\n",
|
|
|
|
| 335 |
"for entry in dataset:\n",
|
| 336 |
" question = entry[\"Question\"]\n",
|
| 337 |
" answer = entry[\"Final answer\"]\n",
|
| 338 |
+
"\n",
|
| 339 |
" # Create a document with both the question and the answer as metadata\n",
|
| 340 |
" metadata = {\n",
|
| 341 |
" \"task_id\": entry[\"task_id\"],\n",
|
| 342 |
" \"steps\": entry[\"Annotator Metadata\"][\"Steps\"],\n",
|
| 343 |
" \"tools\": entry[\"Annotator Metadata\"][\"Tools\"],\n",
|
| 344 |
+
" \"answer\": answer,\n",
|
| 345 |
" }\n",
|
| 346 |
+
"\n",
|
| 347 |
" # Add the question to the list of documents\n",
|
| 348 |
+
" documents.append(Document(page_content=question, metadata=metadata))\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
"\n",
|
| 350 |
"# Insert the documents into Chroma\n",
|
| 351 |
"vectorstore = Chroma.from_documents(\n",
|
| 352 |
" documents=documents,\n",
|
| 353 |
" embedding=embeddings,\n",
|
| 354 |
" collection_name=\"gaia_validation\",\n",
|
| 355 |
+
" persist_directory=\"./chroma_store\",\n",
|
| 356 |
")\n",
|
| 357 |
"\n",
|
| 358 |
"# Persist the data for future use\n",
|
|
|
|
| 386 |
"for entry in dataset:\n",
|
| 387 |
" # Access the tools used (they are stored in the 'Tools' field of 'Annotator Metadata')\n",
|
| 388 |
" tools = entry[\"Annotator Metadata\"][\"Tools\"]\n",
|
| 389 |
+
"\n",
|
| 390 |
" # Split the tools into a list (since they are stored as a string, we split by line breaks)\n",
|
| 391 |
+
" tools_list = tools.split(\"\\n\")\n",
|
| 392 |
+
"\n",
|
| 393 |
" # Add each tool to the set (set automatically ensures uniqueness)\n",
|
| 394 |
" for tool in tools_list:\n",
|
| 395 |
" unique_tools.add(tool.strip()) # Remove any extra spaces or newlines\n",
|
| 396 |
"\n",
|
| 397 |
"# Convert the set of unique tools to a dictionary under the key 'tools'\n",
|
| 398 |
+
"tools_dict = {\"tools\": list(unique_tools)}\n",
|
| 399 |
"\n",
|
| 400 |
"# Print the unique tools to get a sense of what was used\n",
|
| 401 |
+
"print(tools_dict)"
|
| 402 |
]
|
| 403 |
},
|
| 404 |
{
|
tools copy.py
CHANGED
|
@@ -1,18 +1,13 @@
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
import tempfile
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
-
import pandas as pd
|
| 7 |
import fitz # PyMuPDF
|
| 8 |
-
|
| 9 |
-
from smolagents import Tool
|
| 10 |
-
from smolagents import Tool
|
| 11 |
import requests
|
| 12 |
-
import traceback
|
| 13 |
-
from langchain_community.retrievers import BM25Retriever
|
| 14 |
from smolagents import Tool
|
| 15 |
-
|
| 16 |
|
| 17 |
class DownloadFileFromTaskTool(Tool):
|
| 18 |
name = "download_file_from_task"
|
|
@@ -20,15 +15,12 @@ class DownloadFileFromTaskTool(Tool):
|
|
| 20 |
Use this when question requires information from a mentioned file, before reading a file."""
|
| 21 |
|
| 22 |
inputs = {
|
| 23 |
-
"task_id": {
|
| 24 |
-
"type": "string",
|
| 25 |
-
"description": "The GAIA task ID (REQUIRED)."
|
| 26 |
-
},
|
| 27 |
"filename": {
|
| 28 |
"type": "string",
|
| 29 |
"description": "Optional custom filename to save the file as (e.g., 'data.xlsx').",
|
| 30 |
-
"nullable": True
|
| 31 |
-
}
|
| 32 |
}
|
| 33 |
output_type = "string"
|
| 34 |
|
|
@@ -65,15 +57,13 @@ class DownloadFileFromTaskTool(Tool):
|
|
| 65 |
except Exception as e:
|
| 66 |
return f"❌ Error: {e}"
|
| 67 |
|
|
|
|
| 68 |
class ReadFileContentTool(Tool):
|
| 69 |
name = "read_file_content"
|
| 70 |
description = """Reads and returns the content of a file. Use after downloading a file using `download_file_from_task`."""
|
| 71 |
|
| 72 |
inputs = {
|
| 73 |
-
"file_path": {
|
| 74 |
-
"type": "string",
|
| 75 |
-
"description": "Full path to a file to read."
|
| 76 |
-
}
|
| 77 |
}
|
| 78 |
output_type = "string"
|
| 79 |
|
|
@@ -124,15 +114,16 @@ class ReadFileContentTool(Tool):
|
|
| 124 |
except Exception as e:
|
| 125 |
return f"❌ Could not read {file_path}: {e}"
|
| 126 |
|
|
|
|
| 127 |
class GetWikipediaInfoTool(Tool):
|
| 128 |
name = "get_wikipedia_info"
|
| 129 |
description = """Fetches a short summary about a topic from Wikipedia.
|
| 130 |
Use this when a user asks for background information, an explanation, or context on a well-known subject."""
|
| 131 |
-
|
| 132 |
inputs = {
|
| 133 |
"topic": {
|
| 134 |
"type": "string",
|
| 135 |
-
"description": "The topic to search for on Wikipedia."
|
| 136 |
}
|
| 137 |
}
|
| 138 |
output_type = "string"
|
|
@@ -145,10 +136,10 @@ Use this when a user asks for background information, an explanation, or context
|
|
| 145 |
search_response.raise_for_status()
|
| 146 |
search_data = search_response.json()
|
| 147 |
|
| 148 |
-
if not search_data.get(
|
| 149 |
return f"No Wikipedia info for '{topic}'."
|
| 150 |
|
| 151 |
-
page_id = search_data[
|
| 152 |
|
| 153 |
content_url = (
|
| 154 |
f"https://en.wikipedia.org/w/api.php?action=query&prop=extracts&"
|
|
@@ -158,7 +149,7 @@ Use this when a user asks for background information, an explanation, or context
|
|
| 158 |
content_response.raise_for_status()
|
| 159 |
content_data = content_response.json()
|
| 160 |
|
| 161 |
-
extract = content_data[
|
| 162 |
if len(extract) > 1500:
|
| 163 |
extract = extract[:1500] + "..."
|
| 164 |
|
|
@@ -171,6 +162,7 @@ Use this when a user asks for background information, an explanation, or context
|
|
| 171 |
traceback.print_exc()
|
| 172 |
return f"Error wiki: {e}"
|
| 173 |
|
|
|
|
| 174 |
class VisitWebpageTool(Tool):
|
| 175 |
name = "visit_webpage"
|
| 176 |
description = """
|
|
@@ -181,23 +173,24 @@ class VisitWebpageTool(Tool):
|
|
| 181 |
inputs = {
|
| 182 |
"url": {
|
| 183 |
"type": "string",
|
| 184 |
-
"description": "The full URL of the webpage to visit."
|
| 185 |
}
|
| 186 |
}
|
| 187 |
output_type = "string"
|
| 188 |
|
| 189 |
def forward(self, url: str) -> str:
|
| 190 |
try:
|
|
|
|
|
|
|
| 191 |
import requests
|
| 192 |
from bs4 import BeautifulSoup
|
| 193 |
-
import json
|
| 194 |
|
| 195 |
response = requests.get(url, timeout=10)
|
| 196 |
response.raise_for_status()
|
| 197 |
soup = BeautifulSoup(response.text, "html.parser")
|
| 198 |
|
| 199 |
def clean(text):
|
| 200 |
-
return
|
| 201 |
|
| 202 |
def extract_tables(soup):
|
| 203 |
tables_data = []
|
|
@@ -254,57 +247,57 @@ class VisitWebpageTool(Tool):
|
|
| 254 |
|
| 255 |
except Exception as e:
|
| 256 |
return f"❌ Failed to fetch or parse webpage: {str(e)}"
|
| 257 |
-
|
|
|
|
| 258 |
class TranscribeAudioTool(Tool):
|
| 259 |
name = "transcribe_audio"
|
| 260 |
-
description =
|
|
|
|
|
|
|
| 261 |
|
| 262 |
-
inputs = {
|
| 263 |
-
"file_path": {
|
| 264 |
-
"type": "string",
|
| 265 |
-
"description": "Path to an audio file."
|
| 266 |
-
}
|
| 267 |
-
}
|
| 268 |
output_type = "string"
|
| 269 |
|
| 270 |
def forward(self, file_path: str) -> str:
|
| 271 |
try:
|
| 272 |
-
import speech_recognition as sr
|
| 273 |
-
from pydub import AudioSegment
|
| 274 |
import os
|
| 275 |
import tempfile
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
| 277 |
# Initialize recognizer
|
| 278 |
recognizer = sr.Recognizer()
|
| 279 |
-
|
| 280 |
# Convert to WAV if not already (needed for speech_recognition)
|
| 281 |
file_ext = os.path.splitext(file_path)[1].lower()
|
| 282 |
-
|
| 283 |
-
if file_ext !=
|
| 284 |
# Create temp WAV file
|
| 285 |
-
temp_wav = tempfile.NamedTemporaryFile(suffix=
|
| 286 |
-
|
| 287 |
# Convert to WAV using pydub
|
| 288 |
audio = AudioSegment.from_file(file_path)
|
| 289 |
audio.export(temp_wav, format="wav")
|
| 290 |
audio_path = temp_wav
|
| 291 |
else:
|
| 292 |
audio_path = file_path
|
| 293 |
-
|
| 294 |
# Transcribe audio using Google's speech recognition
|
| 295 |
with sr.AudioFile(audio_path) as source:
|
| 296 |
audio_data = recognizer.record(source)
|
| 297 |
transcript = recognizer.recognize_google(audio_data)
|
| 298 |
-
|
| 299 |
# Clean up temp file if created
|
| 300 |
-
if file_ext !=
|
| 301 |
os.remove(temp_wav)
|
| 302 |
-
|
| 303 |
return transcript.strip()
|
| 304 |
-
|
| 305 |
except Exception as e:
|
| 306 |
return f"❌ Transcription failed: {str(e)}"
|
| 307 |
|
|
|
|
| 308 |
class TranscibeVideoFileTool(Tool):
|
| 309 |
name = "transcribe_video"
|
| 310 |
description = """Transcribes speech from a video file. Use this to understand video lectures, tutorials, or visual demos."""
|
|
@@ -312,41 +305,42 @@ class TranscibeVideoFileTool(Tool):
|
|
| 312 |
inputs = {
|
| 313 |
"file_path": {
|
| 314 |
"type": "string",
|
| 315 |
-
"description": "Path to the video file (e.g., .mp4, .mov)."
|
| 316 |
}
|
| 317 |
}
|
| 318 |
output_type = "string"
|
| 319 |
|
| 320 |
def forward(self, file_path: str) -> str:
|
| 321 |
try:
|
| 322 |
-
import moviepy.editor as mp
|
| 323 |
-
import speech_recognition as sr
|
| 324 |
import os
|
| 325 |
import tempfile
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
| 327 |
# Extract audio from video
|
| 328 |
video = mp.VideoFileClip(file_path)
|
| 329 |
-
|
| 330 |
# Create temporary audio file
|
| 331 |
-
temp_audio = tempfile.NamedTemporaryFile(suffix=
|
| 332 |
-
|
| 333 |
# Extract audio to WAV format (required for speech_recognition)
|
| 334 |
video.audio.write_audiofile(temp_audio, verbose=False, logger=None)
|
| 335 |
video.close()
|
| 336 |
-
|
| 337 |
# Initialize recognizer
|
| 338 |
recognizer = sr.Recognizer()
|
| 339 |
-
|
| 340 |
# Transcribe audio
|
| 341 |
with sr.AudioFile(temp_audio) as source:
|
| 342 |
audio_data = recognizer.record(source)
|
| 343 |
transcript = recognizer.recognize_google(audio_data)
|
| 344 |
-
|
| 345 |
# Clean up temp file
|
| 346 |
if os.path.exists(temp_audio):
|
| 347 |
os.remove(temp_audio)
|
| 348 |
-
|
| 349 |
return transcript.strip()
|
| 350 |
-
|
| 351 |
except Exception as e:
|
| 352 |
-
return f"❌ Video processing failed: {str(e)}"
|
|
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
import tempfile
|
| 4 |
+
import traceback
|
| 5 |
+
|
|
|
|
| 6 |
import fitz # PyMuPDF
|
| 7 |
+
import pandas as pd
|
|
|
|
|
|
|
| 8 |
import requests
|
|
|
|
|
|
|
| 9 |
from smolagents import Tool
|
| 10 |
+
|
| 11 |
|
| 12 |
class DownloadFileFromTaskTool(Tool):
|
| 13 |
name = "download_file_from_task"
|
|
|
|
| 15 |
Use this when question requires information from a mentioned file, before reading a file."""
|
| 16 |
|
| 17 |
inputs = {
|
| 18 |
+
"task_id": {"type": "string", "description": "The GAIA task ID (REQUIRED)."},
|
|
|
|
|
|
|
|
|
|
| 19 |
"filename": {
|
| 20 |
"type": "string",
|
| 21 |
"description": "Optional custom filename to save the file as (e.g., 'data.xlsx').",
|
| 22 |
+
"nullable": True,
|
| 23 |
+
},
|
| 24 |
}
|
| 25 |
output_type = "string"
|
| 26 |
|
|
|
|
| 57 |
except Exception as e:
|
| 58 |
return f"❌ Error: {e}"
|
| 59 |
|
| 60 |
+
|
| 61 |
class ReadFileContentTool(Tool):
|
| 62 |
name = "read_file_content"
|
| 63 |
description = """Reads and returns the content of a file. Use after downloading a file using `download_file_from_task`."""
|
| 64 |
|
| 65 |
inputs = {
|
| 66 |
+
"file_path": {"type": "string", "description": "Full path to a file to read."}
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
output_type = "string"
|
| 69 |
|
|
|
|
| 114 |
except Exception as e:
|
| 115 |
return f"❌ Could not read {file_path}: {e}"
|
| 116 |
|
| 117 |
+
|
| 118 |
class GetWikipediaInfoTool(Tool):
|
| 119 |
name = "get_wikipedia_info"
|
| 120 |
description = """Fetches a short summary about a topic from Wikipedia.
|
| 121 |
Use this when a user asks for background information, an explanation, or context on a well-known subject."""
|
| 122 |
+
|
| 123 |
inputs = {
|
| 124 |
"topic": {
|
| 125 |
"type": "string",
|
| 126 |
+
"description": "The topic to search for on Wikipedia.",
|
| 127 |
}
|
| 128 |
}
|
| 129 |
output_type = "string"
|
|
|
|
| 136 |
search_response.raise_for_status()
|
| 137 |
search_data = search_response.json()
|
| 138 |
|
| 139 |
+
if not search_data.get("query", {}).get("search", []):
|
| 140 |
return f"No Wikipedia info for '{topic}'."
|
| 141 |
|
| 142 |
+
page_id = search_data["query"]["search"][0]["pageid"]
|
| 143 |
|
| 144 |
content_url = (
|
| 145 |
f"https://en.wikipedia.org/w/api.php?action=query&prop=extracts&"
|
|
|
|
| 149 |
content_response.raise_for_status()
|
| 150 |
content_data = content_response.json()
|
| 151 |
|
| 152 |
+
extract = content_data["query"]["pages"][str(page_id)]["extract"]
|
| 153 |
if len(extract) > 1500:
|
| 154 |
extract = extract[:1500] + "..."
|
| 155 |
|
|
|
|
| 162 |
traceback.print_exc()
|
| 163 |
return f"Error wiki: {e}"
|
| 164 |
|
| 165 |
+
|
| 166 |
class VisitWebpageTool(Tool):
|
| 167 |
name = "visit_webpage"
|
| 168 |
description = """
|
|
|
|
| 173 |
inputs = {
|
| 174 |
"url": {
|
| 175 |
"type": "string",
|
| 176 |
+
"description": "The full URL of the webpage to visit.",
|
| 177 |
}
|
| 178 |
}
|
| 179 |
output_type = "string"
|
| 180 |
|
| 181 |
def forward(self, url: str) -> str:
|
| 182 |
try:
|
| 183 |
+
import json
|
| 184 |
+
|
| 185 |
import requests
|
| 186 |
from bs4 import BeautifulSoup
|
|
|
|
| 187 |
|
| 188 |
response = requests.get(url, timeout=10)
|
| 189 |
response.raise_for_status()
|
| 190 |
soup = BeautifulSoup(response.text, "html.parser")
|
| 191 |
|
| 192 |
def clean(text):
|
| 193 |
+
return " ".join(text.strip().split())
|
| 194 |
|
| 195 |
def extract_tables(soup):
|
| 196 |
tables_data = []
|
|
|
|
| 247 |
|
| 248 |
except Exception as e:
|
| 249 |
return f"❌ Failed to fetch or parse webpage: {str(e)}"
|
| 250 |
+
|
| 251 |
+
|
| 252 |
class TranscribeAudioTool(Tool):
|
| 253 |
name = "transcribe_audio"
|
| 254 |
+
description = (
|
| 255 |
+
"""Transcribes spoken audio (e.g. voice memos, lectures) into plain text."""
|
| 256 |
+
)
|
| 257 |
|
| 258 |
+
inputs = {"file_path": {"type": "string", "description": "Path to an audio file."}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
output_type = "string"
|
| 260 |
|
| 261 |
def forward(self, file_path: str) -> str:
|
| 262 |
try:
|
|
|
|
|
|
|
| 263 |
import os
|
| 264 |
import tempfile
|
| 265 |
+
|
| 266 |
+
import speech_recognition as sr
|
| 267 |
+
from pydub import AudioSegment
|
| 268 |
+
|
| 269 |
# Initialize recognizer
|
| 270 |
recognizer = sr.Recognizer()
|
| 271 |
+
|
| 272 |
# Convert to WAV if not already (needed for speech_recognition)
|
| 273 |
file_ext = os.path.splitext(file_path)[1].lower()
|
| 274 |
+
|
| 275 |
+
if file_ext != ".wav":
|
| 276 |
# Create temp WAV file
|
| 277 |
+
temp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
| 278 |
+
|
| 279 |
# Convert to WAV using pydub
|
| 280 |
audio = AudioSegment.from_file(file_path)
|
| 281 |
audio.export(temp_wav, format="wav")
|
| 282 |
audio_path = temp_wav
|
| 283 |
else:
|
| 284 |
audio_path = file_path
|
| 285 |
+
|
| 286 |
# Transcribe audio using Google's speech recognition
|
| 287 |
with sr.AudioFile(audio_path) as source:
|
| 288 |
audio_data = recognizer.record(source)
|
| 289 |
transcript = recognizer.recognize_google(audio_data)
|
| 290 |
+
|
| 291 |
# Clean up temp file if created
|
| 292 |
+
if file_ext != ".wav" and os.path.exists(temp_wav):
|
| 293 |
os.remove(temp_wav)
|
| 294 |
+
|
| 295 |
return transcript.strip()
|
| 296 |
+
|
| 297 |
except Exception as e:
|
| 298 |
return f"❌ Transcription failed: {str(e)}"
|
| 299 |
|
| 300 |
+
|
| 301 |
class TranscibeVideoFileTool(Tool):
|
| 302 |
name = "transcribe_video"
|
| 303 |
description = """Transcribes speech from a video file. Use this to understand video lectures, tutorials, or visual demos."""
|
|
|
|
| 305 |
inputs = {
|
| 306 |
"file_path": {
|
| 307 |
"type": "string",
|
| 308 |
+
"description": "Path to the video file (e.g., .mp4, .mov).",
|
| 309 |
}
|
| 310 |
}
|
| 311 |
output_type = "string"
|
| 312 |
|
| 313 |
def forward(self, file_path: str) -> str:
|
| 314 |
try:
|
|
|
|
|
|
|
| 315 |
import os
|
| 316 |
import tempfile
|
| 317 |
+
|
| 318 |
+
import moviepy.editor as mp
|
| 319 |
+
import speech_recognition as sr
|
| 320 |
+
|
| 321 |
# Extract audio from video
|
| 322 |
video = mp.VideoFileClip(file_path)
|
| 323 |
+
|
| 324 |
# Create temporary audio file
|
| 325 |
+
temp_audio = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
| 326 |
+
|
| 327 |
# Extract audio to WAV format (required for speech_recognition)
|
| 328 |
video.audio.write_audiofile(temp_audio, verbose=False, logger=None)
|
| 329 |
video.close()
|
| 330 |
+
|
| 331 |
# Initialize recognizer
|
| 332 |
recognizer = sr.Recognizer()
|
| 333 |
+
|
| 334 |
# Transcribe audio
|
| 335 |
with sr.AudioFile(temp_audio) as source:
|
| 336 |
audio_data = recognizer.record(source)
|
| 337 |
transcript = recognizer.recognize_google(audio_data)
|
| 338 |
+
|
| 339 |
# Clean up temp file
|
| 340 |
if os.path.exists(temp_audio):
|
| 341 |
os.remove(temp_audio)
|
| 342 |
+
|
| 343 |
return transcript.strip()
|
| 344 |
+
|
| 345 |
except Exception as e:
|
| 346 |
+
return f"❌ Video processing failed: {str(e)}"
|
tools.py
CHANGED
|
@@ -1,71 +1,41 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import re
|
| 3 |
-
import tempfile
|
| 4 |
-
import mimetypes
|
| 5 |
-
import requests
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import fitz # PyMuPDF
|
| 8 |
-
from urllib.parse import unquote
|
| 9 |
-
from smolagents import Tool
|
| 10 |
-
import requests
|
| 11 |
-
import traceback
|
| 12 |
-
import math
|
| 13 |
-
from langchain_community.tools import BraveSearch
|
| 14 |
-
from typing import List, Dict
|
| 15 |
-
import json
|
| 16 |
-
import html
|
| 17 |
-
import requests, cv2, numpy as np, os
|
| 18 |
import html
|
| 19 |
import json
|
| 20 |
-
import requests
|
| 21 |
-
from bs4 import BeautifulSoup
|
| 22 |
-
from langchain_community.document_loaders import ArxivLoader
|
| 23 |
-
import arxiv
|
| 24 |
-
from smolagents import tool
|
| 25 |
-
|
| 26 |
-
from smolagents.tools import Tool
|
| 27 |
-
import requests
|
| 28 |
-
import os
|
| 29 |
import mimetypes
|
|
|
|
|
|
|
|
|
|
| 30 |
import traceback
|
|
|
|
|
|
|
| 31 |
from urllib.parse import urlparse
|
| 32 |
|
| 33 |
-
import
|
| 34 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
from duckduckgo_search import DDGS
|
| 36 |
from duckduckgo_search.exceptions import (
|
|
|
|
| 37 |
DuckDuckGoSearchException,
|
| 38 |
RatelimitException,
|
| 39 |
TimeoutException,
|
| 40 |
-
ConversationLimitException,
|
| 41 |
)
|
| 42 |
-
|
| 43 |
-
from smolagents.tools import Tool
|
| 44 |
-
import chromadb
|
| 45 |
-
from pathlib import Path
|
| 46 |
-
import traceback
|
| 47 |
-
import json
|
| 48 |
-
import os
|
| 49 |
from langchain.document_loaders import (
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
)
|
| 52 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 53 |
-
|
| 54 |
-
import chromadb.utils.embedding_functions as embedding_functions
|
| 55 |
-
|
| 56 |
-
import os
|
| 57 |
-
import pandas as pd
|
| 58 |
-
import fitz # PyMuPDF
|
| 59 |
-
from markdownify import markdownify
|
| 60 |
-
from bs4 import BeautifulSoup
|
| 61 |
-
import re
|
| 62 |
-
from smolagents.utils import truncate_content
|
| 63 |
-
import requests
|
| 64 |
-
from bs4 import BeautifulSoup
|
| 65 |
from markdownify import markdownify
|
| 66 |
-
import
|
| 67 |
from smolagents.utils import truncate_content
|
| 68 |
|
|
|
|
| 69 |
class ReadFileContentTool(Tool):
|
| 70 |
name = "read_file_content"
|
| 71 |
description = """Reads local files in various formats (text, CSV, Excel, PDF, HTML, etc.) and returns their content as readable text. Automatically detects and processes the appropriate file format."""
|
|
@@ -73,7 +43,7 @@ class ReadFileContentTool(Tool):
|
|
| 73 |
inputs = {
|
| 74 |
"file_path": {
|
| 75 |
"type": "string",
|
| 76 |
-
"description": "The full path to the file from which the content should be read."
|
| 77 |
}
|
| 78 |
}
|
| 79 |
output_type = "string"
|
|
@@ -91,17 +61,23 @@ class ReadFileContentTool(Tool):
|
|
| 91 |
|
| 92 |
elif ext == ".csv":
|
| 93 |
df = pd.read_csv(file_path)
|
| 94 |
-
return truncate_content(
|
|
|
|
|
|
|
| 95 |
|
| 96 |
elif ext in [".xlsx", ".xls"]:
|
| 97 |
df = pd.read_excel(file_path)
|
| 98 |
-
return truncate_content(
|
|
|
|
|
|
|
| 99 |
|
| 100 |
elif ext == ".pdf":
|
| 101 |
doc = fitz.open(file_path)
|
| 102 |
text = "".join([page.get_text() for page in doc])
|
| 103 |
doc.close()
|
| 104 |
-
return truncate_content(
|
|
|
|
|
|
|
| 105 |
|
| 106 |
elif ext == ".json":
|
| 107 |
with open(file_path, "r", encoding="utf-8") as f:
|
|
@@ -135,6 +111,7 @@ class ReadFileContentTool(Tool):
|
|
| 135 |
except Exception as e:
|
| 136 |
return f"❌ Could not read {file_path}: {e}"
|
| 137 |
|
|
|
|
| 138 |
class WikipediaSearchTool(Tool):
|
| 139 |
name = "wikipedia_search"
|
| 140 |
description = """Searches Wikipedia for a specific topic and returns a concise summary. Useful for background information on subjects, concepts, historical events, or scientific topics."""
|
|
@@ -142,7 +119,7 @@ class WikipediaSearchTool(Tool):
|
|
| 142 |
inputs = {
|
| 143 |
"query": {
|
| 144 |
"type": "string",
|
| 145 |
-
"description": "The query or subject to search for on Wikipedia."
|
| 146 |
}
|
| 147 |
}
|
| 148 |
output_type = "string"
|
|
@@ -155,10 +132,10 @@ class WikipediaSearchTool(Tool):
|
|
| 155 |
search_response.raise_for_status()
|
| 156 |
search_data = search_response.json()
|
| 157 |
|
| 158 |
-
if not search_data.get(
|
| 159 |
return f"No Wikipedia info for '{query}'."
|
| 160 |
|
| 161 |
-
page_id = search_data[
|
| 162 |
|
| 163 |
content_link = (
|
| 164 |
f"https://en.wikipedia.org/w/api.php?action=query&prop=extracts&"
|
|
@@ -168,7 +145,7 @@ class WikipediaSearchTool(Tool):
|
|
| 168 |
content_response.raise_for_status()
|
| 169 |
content_data = content_response.json()
|
| 170 |
|
| 171 |
-
extract = content_data[
|
| 172 |
if len(extract) > 1500:
|
| 173 |
extract = extract[:1500] + "..."
|
| 174 |
|
|
@@ -181,11 +158,10 @@ class WikipediaSearchTool(Tool):
|
|
| 181 |
traceback.print_exc()
|
| 182 |
return f"Error wiki: {e}"
|
| 183 |
|
|
|
|
| 184 |
class VisitWebpageTool(Tool):
|
| 185 |
name = "visit_webpage"
|
| 186 |
-
description =
|
| 187 |
-
"Loads a webpage from a URL and converts its content to markdown format. Use this to browse websites, extract information, or identify downloadable resources from a specific web address."
|
| 188 |
-
)
|
| 189 |
inputs = {
|
| 190 |
"url": {
|
| 191 |
"type": "string",
|
|
@@ -201,7 +177,6 @@ class VisitWebpageTool(Tool):
|
|
| 201 |
import requests
|
| 202 |
from markdownify import markdownify
|
| 203 |
from requests.exceptions import RequestException
|
| 204 |
-
|
| 205 |
from smolagents.utils import truncate_content
|
| 206 |
except ImportError as e:
|
| 207 |
raise ImportError(
|
|
@@ -220,7 +195,8 @@ class VisitWebpageTool(Tool):
|
|
| 220 |
return f"Error fetching the webpage: {str(e)}"
|
| 221 |
except Exception as e:
|
| 222 |
return f"An unexpected error occurred: {str(e)}"
|
| 223 |
-
|
|
|
|
| 224 |
class TranscribeAudioTool(Tool):
|
| 225 |
name = "transcribe_audio"
|
| 226 |
description = """Converts spoken content in audio files to text. Handles various audio formats and produces a transcript of the spoken content for analysis."""
|
|
@@ -228,54 +204,57 @@ class TranscribeAudioTool(Tool):
|
|
| 228 |
inputs = {
|
| 229 |
"file_path": {
|
| 230 |
"type": "string",
|
| 231 |
-
"description": "The full path to the audio file that needs to be transcribed."
|
| 232 |
}
|
| 233 |
}
|
| 234 |
output_type = "string"
|
| 235 |
|
| 236 |
-
|
| 237 |
def forward(self, file_path: str) -> str:
|
| 238 |
try:
|
| 239 |
-
import speech_recognition as sr
|
| 240 |
-
from pydub import AudioSegment
|
| 241 |
import os
|
| 242 |
import tempfile
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
| 244 |
# Verify file exists
|
| 245 |
if not os.path.exists(file_path):
|
| 246 |
-
return
|
| 247 |
-
|
|
|
|
|
|
|
| 248 |
# Initialize recognizer
|
| 249 |
recognizer = sr.Recognizer()
|
| 250 |
-
|
| 251 |
# Convert to WAV if not already (needed for speech_recognition)
|
| 252 |
file_ext = os.path.splitext(file_path)[1].lower()
|
| 253 |
-
|
| 254 |
-
if file_ext !=
|
| 255 |
# Create temp WAV file
|
| 256 |
-
temp_wav = tempfile.NamedTemporaryFile(suffix=
|
| 257 |
-
|
| 258 |
# Convert to WAV using pydub
|
| 259 |
audio = AudioSegment.from_file(file_path)
|
| 260 |
audio.export(temp_wav, format="wav")
|
| 261 |
audio_path = temp_wav
|
| 262 |
else:
|
| 263 |
audio_path = file_path
|
| 264 |
-
|
| 265 |
# Transcribe audio using Google's speech recognition
|
| 266 |
with sr.AudioFile(audio_path) as source:
|
| 267 |
audio_data = recognizer.record(source)
|
| 268 |
transcript = recognizer.recognize_google(audio_data)
|
| 269 |
-
|
| 270 |
# Clean up temp file if created
|
| 271 |
-
if file_ext !=
|
| 272 |
os.remove(temp_wav)
|
| 273 |
-
|
| 274 |
return transcript.strip()
|
| 275 |
-
|
| 276 |
except Exception as e:
|
| 277 |
return f"❌ Transcription failed: {str(e)}"
|
| 278 |
|
|
|
|
| 279 |
class TranscibeVideoFileTool(Tool):
|
| 280 |
name = "transcribe_video"
|
| 281 |
description = """Extracts and transcribes speech from video files. Converts the audio portion of videos into readable text for analysis or reference."""
|
|
@@ -283,7 +262,7 @@ class TranscibeVideoFileTool(Tool):
|
|
| 283 |
inputs = {
|
| 284 |
"file_path": {
|
| 285 |
"type": "string",
|
| 286 |
-
"description": "The full path to the video file that needs to be transcribed."
|
| 287 |
}
|
| 288 |
}
|
| 289 |
output_type = "string"
|
|
@@ -292,40 +271,44 @@ class TranscibeVideoFileTool(Tool):
|
|
| 292 |
try:
|
| 293 |
# Verify file exists
|
| 294 |
if not os.path.exists(file_path):
|
| 295 |
-
return
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
import os
|
| 300 |
import tempfile
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
| 302 |
# Extract audio from video
|
| 303 |
video = mp.VideoFileClip(file_path)
|
| 304 |
-
|
| 305 |
# Create temporary audio file
|
| 306 |
-
temp_audio = tempfile.NamedTemporaryFile(suffix=
|
| 307 |
-
|
| 308 |
# Extract audio to WAV format (required for speech_recognition)
|
| 309 |
video.audio.write_audiofile(temp_audio, verbose=False, logger=None)
|
| 310 |
video.close()
|
| 311 |
-
|
| 312 |
# Initialize recognizer
|
| 313 |
recognizer = sr.Recognizer()
|
| 314 |
-
|
| 315 |
# Transcribe audio
|
| 316 |
with sr.AudioFile(temp_audio) as source:
|
| 317 |
audio_data = recognizer.record(source)
|
| 318 |
transcript = recognizer.recognize_google(audio_data)
|
| 319 |
-
|
| 320 |
# Clean up temp file
|
| 321 |
if os.path.exists(temp_audio):
|
| 322 |
os.remove(temp_audio)
|
| 323 |
-
|
| 324 |
return transcript.strip()
|
| 325 |
-
|
| 326 |
except Exception as e:
|
| 327 |
return f"❌ Video processing failed: {str(e)}"
|
| 328 |
-
|
|
|
|
| 329 |
class BraveWebSearchTool(Tool):
|
| 330 |
name = "web_search"
|
| 331 |
description = """Performs web searches and returns content from top results. Provides real-time information from across the internet including current events, facts, and website content relevant to your query."""
|
|
@@ -333,7 +316,7 @@ class BraveWebSearchTool(Tool):
|
|
| 333 |
inputs = {
|
| 334 |
"query": {
|
| 335 |
"type": "string",
|
| 336 |
-
"description": "A web search query string (e.g., a question or query)."
|
| 337 |
}
|
| 338 |
}
|
| 339 |
output_type = "string"
|
|
@@ -366,10 +349,14 @@ class BraveWebSearchTool(Tool):
|
|
| 366 |
def forward(self, query: str) -> str:
|
| 367 |
try:
|
| 368 |
results_json = self.tool.run(query)
|
| 369 |
-
results =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
output_parts = []
|
| 372 |
-
for i, r in enumerate(results[:self.count], start=1):
|
| 373 |
title = html.unescape(r.get("title", "").strip())
|
| 374 |
link = r.get("link", "").strip()
|
| 375 |
|
|
@@ -388,6 +375,7 @@ class BraveWebSearchTool(Tool):
|
|
| 388 |
except Exception as e:
|
| 389 |
return f"Search failed: {str(e)}"
|
| 390 |
|
|
|
|
| 391 |
class DescribeImageTool(Tool):
|
| 392 |
name = "describe_image"
|
| 393 |
description = """Analyzes images and generates detailed text descriptions. Identifies objects, scenes, text, and visual elements within the image to provide context or understanding."""
|
|
@@ -395,23 +383,27 @@ class DescribeImageTool(Tool):
|
|
| 395 |
inputs = {
|
| 396 |
"image_path": {
|
| 397 |
"type": "string",
|
| 398 |
-
"description": "The full path to the image file to describe."
|
| 399 |
}
|
| 400 |
}
|
| 401 |
output_type = "string"
|
| 402 |
|
| 403 |
def forward(self, image_path: str) -> str:
|
| 404 |
import os
|
|
|
|
| 405 |
from PIL import Image
|
| 406 |
-
import
|
| 407 |
-
from transformers import BlipProcessor, BlipForConditionalGeneration
|
| 408 |
|
| 409 |
if not os.path.exists(image_path):
|
| 410 |
return f"❌ Image file does not exist: {image_path}"
|
| 411 |
|
| 412 |
try:
|
| 413 |
-
processor = BlipProcessor.from_pretrained(
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
|
| 416 |
image = Image.open(image_path).convert("RGB")
|
| 417 |
inputs = processor(images=image, return_tensors="pt")
|
|
@@ -422,24 +414,37 @@ class DescribeImageTool(Tool):
|
|
| 422 |
except Exception as e:
|
| 423 |
return f"❌ Failed to describe image: {e}"
|
| 424 |
|
|
|
|
| 425 |
class DownloadFileFromLinkTool(Tool):
|
| 426 |
name = "download_file_from_link"
|
| 427 |
description = "Downloads files from a URL and saves them locally. Supports various formats including PDFs, documents, images, and data files. Returns the local file path for further processing."
|
| 428 |
|
| 429 |
inputs = {
|
| 430 |
-
"link": {
|
| 431 |
-
"type": "string",
|
| 432 |
-
"description": "The URL to download the file from."
|
| 433 |
-
},
|
| 434 |
"file_name": {
|
| 435 |
"type": "string",
|
| 436 |
"description": "Desired name of the saved file, without extension.",
|
| 437 |
-
"nullable": True
|
| 438 |
-
}
|
| 439 |
}
|
| 440 |
|
| 441 |
output_type = "string"
|
| 442 |
-
SUPPORTED_EXTENSIONS = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
|
| 444 |
def forward(self, link: str, file_name: str = "taskfile") -> str:
|
| 445 |
print(f"⬇️ Downloading file from: {link}")
|
|
@@ -452,7 +457,9 @@ class DownloadFileFromLinkTool(Tool):
|
|
| 452 |
return f"❌ Error: Request failed - {e}"
|
| 453 |
|
| 454 |
if response.status_code != 200:
|
| 455 |
-
return
|
|
|
|
|
|
|
| 456 |
|
| 457 |
# Step 1: Try extracting extension from provided filename
|
| 458 |
base_name, provided_ext = os.path.splitext(file_name)
|
|
@@ -463,7 +470,9 @@ class DownloadFileFromLinkTool(Tool):
|
|
| 463 |
ext = provided_ext
|
| 464 |
else:
|
| 465 |
# Step 3: Try to infer from Content-Type
|
| 466 |
-
content_type =
|
|
|
|
|
|
|
| 467 |
guessed_ext = mimetypes.guess_extension(content_type or "") or ""
|
| 468 |
|
| 469 |
# Step 4: If mimetype returned .bin or nothing useful, try to fallback to URL
|
|
@@ -489,6 +498,7 @@ class DownloadFileFromLinkTool(Tool):
|
|
| 489 |
|
| 490 |
return file_path
|
| 491 |
|
|
|
|
| 492 |
class DuckDuckGoSearchTool(Tool):
|
| 493 |
name = "web_search"
|
| 494 |
description = """Performs web searches and returns content from top results. Provides real-time information from across the internet including current events, facts, and website content relevant to your query."""
|
|
@@ -496,7 +506,7 @@ class DuckDuckGoSearchTool(Tool):
|
|
| 496 |
inputs = {
|
| 497 |
"query": {
|
| 498 |
"type": "string",
|
| 499 |
-
"description": "The search query to run on DuckDuckGo"
|
| 500 |
},
|
| 501 |
}
|
| 502 |
output_type = "string"
|
|
@@ -507,7 +517,9 @@ class DuckDuckGoSearchTool(Tool):
|
|
| 507 |
|
| 508 |
def forward(self, query: str) -> str:
|
| 509 |
self._configure()
|
| 510 |
-
print(
|
|
|
|
|
|
|
| 511 |
|
| 512 |
top_results = 5
|
| 513 |
|
|
@@ -532,7 +544,7 @@ class DuckDuckGoSearchTool(Tool):
|
|
| 532 |
title = res.get("title", "N/A")
|
| 533 |
url = res.get("href", "N/A")
|
| 534 |
snippet = res.get("body", "N/A")
|
| 535 |
-
|
| 536 |
output_lines.append(
|
| 537 |
f"Result {idx}:\n"
|
| 538 |
f"Title: {title}\n"
|
|
@@ -545,9 +557,16 @@ class DuckDuckGoSearchTool(Tool):
|
|
| 545 |
print(f"-> Tool Result (DuckDuckGo): {output[:1500]}...")
|
| 546 |
return output
|
| 547 |
|
| 548 |
-
except (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
retries += 1
|
| 550 |
-
print(
|
|
|
|
|
|
|
| 551 |
traceback.print_exc()
|
| 552 |
time.sleep(retry_sleep)
|
| 553 |
|
|
@@ -557,12 +576,22 @@ class DuckDuckGoSearchTool(Tool):
|
|
| 557 |
return f"Unhandled exception during DuckDuckGo search: {e}"
|
| 558 |
|
| 559 |
return f"❌ Failed to retrieve results after {max_retries} retries."
|
| 560 |
-
|
|
|
|
| 561 |
huggingface_ef = embedding_functions.HuggingFaceEmbeddingFunction(
|
| 562 |
-
api_key=os.environ["HF_TOKEN"],
|
| 563 |
-
model_name="sentence-transformers/all-mpnet-base-v2"
|
| 564 |
)
|
| 565 |
-
SUPPORTED_EXTENSIONS = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
| 567 |
class AddDocumentToVectorStoreTool(Tool):
|
| 568 |
name = "add_document_to_vector_store"
|
|
@@ -599,16 +628,21 @@ class AddDocumentToVectorStoreTool(Tool):
|
|
| 599 |
return f"Unsupported or missing file: {file_path}"
|
| 600 |
|
| 601 |
docs = self._load_file(path)
|
| 602 |
-
text_splitter = RecursiveCharacterTextSplitter(
|
|
|
|
|
|
|
| 603 |
split_docs = text_splitter.split_documents(docs)
|
| 604 |
|
| 605 |
-
client = chromadb.Client(
|
| 606 |
-
|
| 607 |
-
|
|
|
|
|
|
|
| 608 |
|
| 609 |
-
collection = client.get_or_create_collection(
|
| 610 |
-
|
| 611 |
-
|
|
|
|
| 612 |
|
| 613 |
texts = [doc.page_content for doc in split_docs]
|
| 614 |
metadatas = [doc.metadata for doc in split_docs]
|
|
@@ -616,7 +650,7 @@ class AddDocumentToVectorStoreTool(Tool):
|
|
| 616 |
collection.add(
|
| 617 |
documents=texts,
|
| 618 |
metadatas=metadatas,
|
| 619 |
-
ids=[f"{path.stem}_{i}" for i in range(len(texts))]
|
| 620 |
)
|
| 621 |
|
| 622 |
return f"✅ Successfully added {len(texts)} chunks from '{file_path}' to collection '{collection_name}'."
|
|
@@ -625,7 +659,8 @@ class AddDocumentToVectorStoreTool(Tool):
|
|
| 625 |
print(f"❌ Error in add_to_vector_store: {e}")
|
| 626 |
traceback.print_exc()
|
| 627 |
return f"Error: {e}"
|
| 628 |
-
|
|
|
|
| 629 |
class QueryVectorStoreTool(Tool):
|
| 630 |
name = "query_downloaded_documents"
|
| 631 |
description = "Performs semantic searches across your downloaded documents. Use detailed queries to find specific information, concepts, or answers from your collected resources."
|
|
@@ -638,8 +673,8 @@ class QueryVectorStoreTool(Tool):
|
|
| 638 |
"top_k": {
|
| 639 |
"type": "integer",
|
| 640 |
"description": "Number of top results to retrieve. Usually between 3 and 30",
|
| 641 |
-
"nullable": True
|
| 642 |
-
}
|
| 643 |
}
|
| 644 |
output_type = "string"
|
| 645 |
|
|
@@ -653,9 +688,11 @@ class QueryVectorStoreTool(Tool):
|
|
| 653 |
|
| 654 |
print(f"🔎 Querying vector store '{collection_name}' with: '{query}'")
|
| 655 |
try:
|
| 656 |
-
client = chromadb.Client(
|
| 657 |
-
|
| 658 |
-
|
|
|
|
|
|
|
| 659 |
collection = client.get_collection(name=collection_name)
|
| 660 |
|
| 661 |
results = collection.query(
|
|
@@ -668,9 +705,7 @@ class QueryVectorStoreTool(Tool):
|
|
| 668 |
doc = results["documents"][0][i]
|
| 669 |
metadata = results["metadatas"][0][i]
|
| 670 |
formatted.append(
|
| 671 |
-
f"Result {i+1}:\n"
|
| 672 |
-
f"Content: {doc}\n"
|
| 673 |
-
f"Metadata: {metadata}\n"
|
| 674 |
)
|
| 675 |
|
| 676 |
return "\n".join(formatted) or "No relevant documents found."
|
|
@@ -680,15 +715,16 @@ class QueryVectorStoreTool(Tool):
|
|
| 680 |
traceback.print_exc()
|
| 681 |
return f"Error querying vector store: {e}"
|
| 682 |
|
|
|
|
| 683 |
@tool
|
| 684 |
def image_question_answering(image_path: str, prompt: str) -> str:
|
| 685 |
"""
|
| 686 |
Analyzes images and answers specific questions about their content. Can identify objects, read text, describe scenes, or interpret visual information based on your questions.
|
| 687 |
-
|
| 688 |
Args:
|
| 689 |
image_path: The path to the image file
|
| 690 |
prompt: The question to ask about the image
|
| 691 |
-
|
| 692 |
Returns:
|
| 693 |
A string answer generated by the local Ollama model
|
| 694 |
"""
|
|
@@ -703,15 +739,15 @@ def image_question_answering(image_path: str, prompt: str) -> str:
|
|
| 703 |
|
| 704 |
# Send the image and prompt to Ollama's local model
|
| 705 |
response = chat(
|
| 706 |
-
model=
|
| 707 |
messages=[
|
| 708 |
{
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
},
|
| 713 |
],
|
| 714 |
-
options={
|
| 715 |
)
|
| 716 |
|
| 717 |
return response.message.content.strip()
|
|
@@ -719,9 +755,7 @@ def image_question_answering(image_path: str, prompt: str) -> str:
|
|
| 719 |
|
| 720 |
class VisitWebpageTool(Tool):
|
| 721 |
name = "visit_webpage"
|
| 722 |
-
description =
|
| 723 |
-
"Loads a webpage from a URL and converts its content to markdown format. Use this to browse websites, extract information, or identify downloadable resources from a specific web address."
|
| 724 |
-
)
|
| 725 |
inputs = {
|
| 726 |
"url": {
|
| 727 |
"type": "string",
|
|
@@ -732,53 +766,51 @@ class VisitWebpageTool(Tool):
|
|
| 732 |
|
| 733 |
def forward(self, url: str) -> str:
|
| 734 |
try:
|
| 735 |
-
import re
|
| 736 |
from urllib.parse import urlparse
|
| 737 |
|
| 738 |
import requests
|
| 739 |
from bs4 import BeautifulSoup
|
| 740 |
from markdownify import markdownify
|
| 741 |
from requests.exceptions import RequestException
|
| 742 |
-
|
| 743 |
from smolagents.utils import truncate_content
|
| 744 |
except ImportError as e:
|
| 745 |
raise ImportError(
|
| 746 |
"You must install packages `markdownify`, `requests`, and `beautifulsoup4` to run this tool: for instance run `pip install markdownify requests beautifulsoup4`."
|
| 747 |
) from e
|
| 748 |
-
|
| 749 |
try:
|
| 750 |
# Get the webpage content
|
| 751 |
headers = {
|
| 752 |
-
|
| 753 |
}
|
| 754 |
response = requests.get(url, headers=headers, timeout=20)
|
| 755 |
response.raise_for_status()
|
| 756 |
-
|
| 757 |
# Parse the HTML with BeautifulSoup
|
| 758 |
-
soup = BeautifulSoup(response.text,
|
| 759 |
-
|
| 760 |
# Extract domain name for context
|
| 761 |
domain = urlparse(url).netloc
|
| 762 |
-
|
| 763 |
# Remove common clutter elements
|
| 764 |
self._remove_clutter(soup)
|
| 765 |
-
|
| 766 |
# Try to identify and prioritize main content
|
| 767 |
main_content = self._extract_main_content(soup)
|
| 768 |
-
|
| 769 |
if main_content:
|
| 770 |
# Convert the cleaned HTML to markdown
|
| 771 |
markdown_content = markdownify(str(main_content)).strip()
|
| 772 |
else:
|
| 773 |
# Fallback to full page content if main content extraction fails
|
| 774 |
markdown_content = markdownify(str(soup)).strip()
|
| 775 |
-
|
| 776 |
# Post-process the markdown content
|
| 777 |
markdown_content = self._clean_markdown(markdown_content)
|
| 778 |
-
|
| 779 |
# Add source information
|
| 780 |
result = f"Content from {domain}:\n\n{markdown_content}"
|
| 781 |
-
|
| 782 |
return truncate_content(result, 40000)
|
| 783 |
|
| 784 |
except requests.exceptions.Timeout:
|
|
@@ -787,47 +819,75 @@ class VisitWebpageTool(Tool):
|
|
| 787 |
return f"Error fetching the webpage: {str(e)}"
|
| 788 |
except Exception as e:
|
| 789 |
return f"An unexpected error occurred: {str(e)}"
|
| 790 |
-
|
| 791 |
def _remove_clutter(self, soup):
|
| 792 |
"""Remove common elements that clutter web pages."""
|
| 793 |
# Common non-content elements to remove
|
| 794 |
clutter_selectors = [
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
]
|
| 802 |
-
|
| 803 |
for selector in clutter_selectors:
|
| 804 |
for element in soup.select(selector):
|
| 805 |
element.decompose()
|
| 806 |
-
|
| 807 |
# Remove hidden elements
|
| 808 |
-
for hidden in soup.select(
|
|
|
|
|
|
|
| 809 |
hidden.decompose()
|
| 810 |
-
|
| 811 |
def _extract_main_content(self, soup):
|
| 812 |
"""Try to identify and extract the main content of the page."""
|
| 813 |
# Priority order for common main content containers
|
| 814 |
main_content_selectors = [
|
| 815 |
-
|
| 816 |
-
'[role="main"]',
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
]
|
| 830 |
-
|
| 831 |
# Try to find the main content container
|
| 832 |
for selector in main_content_selectors:
|
| 833 |
main_content = soup.select(selector)
|
|
@@ -836,9 +896,9 @@ class VisitWebpageTool(Tool):
|
|
| 836 |
if len(main_content) > 1:
|
| 837 |
return max(main_content, key=lambda x: len(x.get_text()))
|
| 838 |
return main_content[0]
|
| 839 |
-
|
| 840 |
# If no main content container found, look for the largest text block
|
| 841 |
-
paragraphs = soup.find_all(
|
| 842 |
if paragraphs:
|
| 843 |
# Find the parent that contains the most paragraphs
|
| 844 |
parents = {}
|
|
@@ -847,88 +907,106 @@ class VisitWebpageTool(Tool):
|
|
| 847 |
if p.parent not in parents:
|
| 848 |
parents[p.parent] = 0
|
| 849 |
parents[p.parent] += 1
|
| 850 |
-
|
| 851 |
if parents:
|
| 852 |
# Return the parent with the most paragraphs
|
| 853 |
return max(parents.items(), key=lambda x: x[1])[0]
|
| 854 |
-
|
| 855 |
# Return None if we can't identify main content
|
| 856 |
return None
|
| 857 |
-
|
| 858 |
def _clean_markdown(self, content):
|
| 859 |
"""Clean up the markdown content."""
|
| 860 |
# Normalize whitespace
|
| 861 |
-
content = re.sub(r
|
| 862 |
-
|
| 863 |
# Remove consecutive duplicate links
|
| 864 |
-
content = re.sub(r
|
| 865 |
-
|
| 866 |
# Remove very short lines that are likely menu items
|
| 867 |
-
lines = content.split(
|
| 868 |
filtered_lines = []
|
| 869 |
-
|
| 870 |
# Skip consecutive short lines (likely menus)
|
| 871 |
short_line_threshold = 40 # characters
|
| 872 |
consecutive_short_lines = 0
|
| 873 |
max_consecutive_short_lines = 3
|
| 874 |
-
|
| 875 |
for line in lines:
|
| 876 |
stripped_line = line.strip()
|
| 877 |
-
if len(
|
|
|
|
|
|
|
| 878 |
consecutive_short_lines += 1
|
| 879 |
if consecutive_short_lines > max_consecutive_short_lines:
|
| 880 |
continue
|
| 881 |
else:
|
| 882 |
consecutive_short_lines = 0
|
| 883 |
-
|
| 884 |
filtered_lines.append(line)
|
| 885 |
-
|
| 886 |
-
content =
|
| 887 |
-
|
| 888 |
# Remove duplicate headers
|
| 889 |
seen_headers = set()
|
| 890 |
-
lines = content.split(
|
| 891 |
filtered_lines = []
|
| 892 |
-
|
| 893 |
for line in lines:
|
| 894 |
-
if line.startswith(
|
| 895 |
header_text = line.strip()
|
| 896 |
if header_text in seen_headers:
|
| 897 |
continue
|
| 898 |
seen_headers.add(header_text)
|
| 899 |
filtered_lines.append(line)
|
| 900 |
-
|
| 901 |
-
content =
|
| 902 |
-
|
| 903 |
# Remove lines containing common footer patterns
|
| 904 |
footer_patterns = [
|
| 905 |
-
r
|
| 906 |
-
r
|
| 907 |
-
r
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 908 |
]
|
| 909 |
-
|
| 910 |
-
footer_pattern =
|
| 911 |
-
lines = content.split(
|
| 912 |
filtered_lines = []
|
| 913 |
-
|
| 914 |
for line in lines:
|
| 915 |
if not re.search(footer_pattern, line.lower()):
|
| 916 |
filtered_lines.append(line)
|
| 917 |
-
|
| 918 |
-
content =
|
| 919 |
-
|
| 920 |
return content
|
| 921 |
-
|
| 922 |
|
| 923 |
class ArxivSearchTool(Tool):
|
| 924 |
name = "arxiv_search"
|
| 925 |
description = """Searches arXiv for academic papers and returns structured information including titles, authors, publication dates, abstracts, and download links."""
|
| 926 |
|
| 927 |
-
|
| 928 |
inputs = {
|
| 929 |
-
"query":
|
| 930 |
-
|
| 931 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 932 |
}
|
| 933 |
|
| 934 |
output_type = "string"
|
|
@@ -962,15 +1040,17 @@ class ArxivSearchTool(Tool):
|
|
| 962 |
f"Summary : {p['abstract'][:500]}{'...' if len(p['abstract'])>500 else ''}",
|
| 963 |
f"Entry ID : {p['entry_link']}",
|
| 964 |
f"Download link: {p['download_link']}",
|
| 965 |
-
""
|
| 966 |
]
|
| 967 |
|
| 968 |
return "\n".join(output_lines).strip()
|
| 969 |
-
|
|
|
|
|
|
|
| 970 |
|
| 971 |
import requests
|
| 972 |
from bs4 import BeautifulSoup
|
| 973 |
-
|
| 974 |
|
| 975 |
def fetch_and_parse_arxiv(url: str) -> List[Dict[str, str]]:
|
| 976 |
"""
|
|
@@ -999,7 +1079,9 @@ def fetch_and_parse_arxiv(url: str) -> List[Dict[str, str]]:
|
|
| 999 |
|
| 1000 |
# Abstract
|
| 1001 |
ab = li.find("span", class_="abstract-full")
|
| 1002 |
-
abstract =
|
|
|
|
|
|
|
| 1003 |
|
| 1004 |
# Published date
|
| 1005 |
d = li.find("p", class_="is-size-7")
|
|
@@ -1017,24 +1099,27 @@ def fetch_and_parse_arxiv(url: str) -> List[Dict[str, str]]:
|
|
| 1017 |
doi = a_tag["href"]
|
| 1018 |
break
|
| 1019 |
|
| 1020 |
-
results.append(
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1028 |
|
| 1029 |
return results
|
| 1030 |
|
|
|
|
| 1031 |
from urllib.parse import quote_plus
|
| 1032 |
|
|
|
|
| 1033 |
def build_arxiv_url(
|
| 1034 |
-
query: str,
|
| 1035 |
-
from_date: str = None,
|
| 1036 |
-
to_date: str = None,
|
| 1037 |
-
size: int = 50
|
| 1038 |
) -> str:
|
| 1039 |
"""
|
| 1040 |
Build an arXiv advanced-search URL matching the exact segment order:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import html
|
| 2 |
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import mimetypes
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import time
|
| 7 |
import traceback
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Dict, List
|
| 10 |
from urllib.parse import urlparse
|
| 11 |
|
| 12 |
+
import chromadb
|
| 13 |
+
import chromadb.utils.embedding_functions as embedding_functions
|
| 14 |
+
import fitz # PyMuPDF
|
| 15 |
+
import pandas as pd
|
| 16 |
+
import requests
|
| 17 |
+
from bs4 import BeautifulSoup
|
| 18 |
from duckduckgo_search import DDGS
|
| 19 |
from duckduckgo_search.exceptions import (
|
| 20 |
+
ConversationLimitException,
|
| 21 |
DuckDuckGoSearchException,
|
| 22 |
RatelimitException,
|
| 23 |
TimeoutException,
|
|
|
|
| 24 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
from langchain.document_loaders import (
|
| 26 |
+
BSHTMLLoader,
|
| 27 |
+
JSONLoader,
|
| 28 |
+
PyPDFLoader,
|
| 29 |
+
TextLoader,
|
| 30 |
+
UnstructuredFileLoader,
|
| 31 |
)
|
| 32 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 33 |
+
from langchain_community.tools import BraveSearch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
from markdownify import markdownify
|
| 35 |
+
from smolagents import Tool, tool
|
| 36 |
from smolagents.utils import truncate_content
|
| 37 |
|
| 38 |
+
|
| 39 |
class ReadFileContentTool(Tool):
|
| 40 |
name = "read_file_content"
|
| 41 |
description = """Reads local files in various formats (text, CSV, Excel, PDF, HTML, etc.) and returns their content as readable text. Automatically detects and processes the appropriate file format."""
|
|
|
|
| 43 |
inputs = {
|
| 44 |
"file_path": {
|
| 45 |
"type": "string",
|
| 46 |
+
"description": "The full path to the file from which the content should be read.",
|
| 47 |
}
|
| 48 |
}
|
| 49 |
output_type = "string"
|
|
|
|
| 61 |
|
| 62 |
elif ext == ".csv":
|
| 63 |
df = pd.read_csv(file_path)
|
| 64 |
+
return truncate_content(
|
| 65 |
+
f"CSV Content:\n{df.to_string(index=False)}\n\nColumn names: {', '.join(df.columns)}"
|
| 66 |
+
)
|
| 67 |
|
| 68 |
elif ext in [".xlsx", ".xls"]:
|
| 69 |
df = pd.read_excel(file_path)
|
| 70 |
+
return truncate_content(
|
| 71 |
+
f"Excel Content:\n{df.to_string(index=False)}\n\nColumn names: {', '.join(df.columns)}"
|
| 72 |
+
)
|
| 73 |
|
| 74 |
elif ext == ".pdf":
|
| 75 |
doc = fitz.open(file_path)
|
| 76 |
text = "".join([page.get_text() for page in doc])
|
| 77 |
doc.close()
|
| 78 |
+
return truncate_content(
|
| 79 |
+
text.strip() or "⚠️ PDF contains no readable text."
|
| 80 |
+
)
|
| 81 |
|
| 82 |
elif ext == ".json":
|
| 83 |
with open(file_path, "r", encoding="utf-8") as f:
|
|
|
|
| 111 |
except Exception as e:
|
| 112 |
return f"❌ Could not read {file_path}: {e}"
|
| 113 |
|
| 114 |
+
|
| 115 |
class WikipediaSearchTool(Tool):
|
| 116 |
name = "wikipedia_search"
|
| 117 |
description = """Searches Wikipedia for a specific topic and returns a concise summary. Useful for background information on subjects, concepts, historical events, or scientific topics."""
|
|
|
|
| 119 |
inputs = {
|
| 120 |
"query": {
|
| 121 |
"type": "string",
|
| 122 |
+
"description": "The query or subject to search for on Wikipedia.",
|
| 123 |
}
|
| 124 |
}
|
| 125 |
output_type = "string"
|
|
|
|
| 132 |
search_response.raise_for_status()
|
| 133 |
search_data = search_response.json()
|
| 134 |
|
| 135 |
+
if not search_data.get("query", {}).get("search", []):
|
| 136 |
return f"No Wikipedia info for '{query}'."
|
| 137 |
|
| 138 |
+
page_id = search_data["query"]["search"][0]["pageid"]
|
| 139 |
|
| 140 |
content_link = (
|
| 141 |
f"https://en.wikipedia.org/w/api.php?action=query&prop=extracts&"
|
|
|
|
| 145 |
content_response.raise_for_status()
|
| 146 |
content_data = content_response.json()
|
| 147 |
|
| 148 |
+
extract = content_data["query"]["pages"][str(page_id)]["extract"]
|
| 149 |
if len(extract) > 1500:
|
| 150 |
extract = extract[:1500] + "..."
|
| 151 |
|
|
|
|
| 158 |
traceback.print_exc()
|
| 159 |
return f"Error wiki: {e}"
|
| 160 |
|
| 161 |
+
|
| 162 |
class VisitWebpageTool(Tool):
|
| 163 |
name = "visit_webpage"
|
| 164 |
+
description = "Loads a webpage from a URL and converts its content to markdown format. Use this to browse websites, extract information, or identify downloadable resources from a specific web address."
|
|
|
|
|
|
|
| 165 |
inputs = {
|
| 166 |
"url": {
|
| 167 |
"type": "string",
|
|
|
|
| 177 |
import requests
|
| 178 |
from markdownify import markdownify
|
| 179 |
from requests.exceptions import RequestException
|
|
|
|
| 180 |
from smolagents.utils import truncate_content
|
| 181 |
except ImportError as e:
|
| 182 |
raise ImportError(
|
|
|
|
| 195 |
return f"Error fetching the webpage: {str(e)}"
|
| 196 |
except Exception as e:
|
| 197 |
return f"An unexpected error occurred: {str(e)}"
|
| 198 |
+
|
| 199 |
+
|
| 200 |
class TranscribeAudioTool(Tool):
|
| 201 |
name = "transcribe_audio"
|
| 202 |
description = """Converts spoken content in audio files to text. Handles various audio formats and produces a transcript of the spoken content for analysis."""
|
|
|
|
| 204 |
inputs = {
|
| 205 |
"file_path": {
|
| 206 |
"type": "string",
|
| 207 |
+
"description": "The full path to the audio file that needs to be transcribed.",
|
| 208 |
}
|
| 209 |
}
|
| 210 |
output_type = "string"
|
| 211 |
|
|
|
|
| 212 |
def forward(self, file_path: str) -> str:
|
| 213 |
try:
|
|
|
|
|
|
|
| 214 |
import os
|
| 215 |
import tempfile
|
| 216 |
+
|
| 217 |
+
import speech_recognition as sr
|
| 218 |
+
from pydub import AudioSegment
|
| 219 |
+
|
| 220 |
# Verify file exists
|
| 221 |
if not os.path.exists(file_path):
|
| 222 |
+
return (
|
| 223 |
+
f"❌ Audio file not found at: {file_path}. Download the file first."
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
# Initialize recognizer
|
| 227 |
recognizer = sr.Recognizer()
|
| 228 |
+
|
| 229 |
# Convert to WAV if not already (needed for speech_recognition)
|
| 230 |
file_ext = os.path.splitext(file_path)[1].lower()
|
| 231 |
+
|
| 232 |
+
if file_ext != ".wav":
|
| 233 |
# Create temp WAV file
|
| 234 |
+
temp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
| 235 |
+
|
| 236 |
# Convert to WAV using pydub
|
| 237 |
audio = AudioSegment.from_file(file_path)
|
| 238 |
audio.export(temp_wav, format="wav")
|
| 239 |
audio_path = temp_wav
|
| 240 |
else:
|
| 241 |
audio_path = file_path
|
| 242 |
+
|
| 243 |
# Transcribe audio using Google's speech recognition
|
| 244 |
with sr.AudioFile(audio_path) as source:
|
| 245 |
audio_data = recognizer.record(source)
|
| 246 |
transcript = recognizer.recognize_google(audio_data)
|
| 247 |
+
|
| 248 |
# Clean up temp file if created
|
| 249 |
+
if file_ext != ".wav" and os.path.exists(temp_wav):
|
| 250 |
os.remove(temp_wav)
|
| 251 |
+
|
| 252 |
return transcript.strip()
|
| 253 |
+
|
| 254 |
except Exception as e:
|
| 255 |
return f"❌ Transcription failed: {str(e)}"
|
| 256 |
|
| 257 |
+
|
| 258 |
class TranscibeVideoFileTool(Tool):
|
| 259 |
name = "transcribe_video"
|
| 260 |
description = """Extracts and transcribes speech from video files. Converts the audio portion of videos into readable text for analysis or reference."""
|
|
|
|
| 262 |
inputs = {
|
| 263 |
"file_path": {
|
| 264 |
"type": "string",
|
| 265 |
+
"description": "The full path to the video file that needs to be transcribed.",
|
| 266 |
}
|
| 267 |
}
|
| 268 |
output_type = "string"
|
|
|
|
| 271 |
try:
|
| 272 |
# Verify file exists
|
| 273 |
if not os.path.exists(file_path):
|
| 274 |
+
return (
|
| 275 |
+
f"❌ Video file not found at: {file_path}. Download the file first."
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
import os
|
| 279 |
import tempfile
|
| 280 |
+
|
| 281 |
+
import moviepy.editor as mp
|
| 282 |
+
import speech_recognition as sr
|
| 283 |
+
|
| 284 |
# Extract audio from video
|
| 285 |
video = mp.VideoFileClip(file_path)
|
| 286 |
+
|
| 287 |
# Create temporary audio file
|
| 288 |
+
temp_audio = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
| 289 |
+
|
| 290 |
# Extract audio to WAV format (required for speech_recognition)
|
| 291 |
video.audio.write_audiofile(temp_audio, verbose=False, logger=None)
|
| 292 |
video.close()
|
| 293 |
+
|
| 294 |
# Initialize recognizer
|
| 295 |
recognizer = sr.Recognizer()
|
| 296 |
+
|
| 297 |
# Transcribe audio
|
| 298 |
with sr.AudioFile(temp_audio) as source:
|
| 299 |
audio_data = recognizer.record(source)
|
| 300 |
transcript = recognizer.recognize_google(audio_data)
|
| 301 |
+
|
| 302 |
# Clean up temp file
|
| 303 |
if os.path.exists(temp_audio):
|
| 304 |
os.remove(temp_audio)
|
| 305 |
+
|
| 306 |
return transcript.strip()
|
| 307 |
+
|
| 308 |
except Exception as e:
|
| 309 |
return f"❌ Video processing failed: {str(e)}"
|
| 310 |
+
|
| 311 |
+
|
| 312 |
class BraveWebSearchTool(Tool):
|
| 313 |
name = "web_search"
|
| 314 |
description = """Performs web searches and returns content from top results. Provides real-time information from across the internet including current events, facts, and website content relevant to your query."""
|
|
|
|
| 316 |
inputs = {
|
| 317 |
"query": {
|
| 318 |
"type": "string",
|
| 319 |
+
"description": "A web search query string (e.g., a question or query).",
|
| 320 |
}
|
| 321 |
}
|
| 322 |
output_type = "string"
|
|
|
|
| 349 |
def forward(self, query: str) -> str:
|
| 350 |
try:
|
| 351 |
results_json = self.tool.run(query)
|
| 352 |
+
results = (
|
| 353 |
+
json.loads(results_json)
|
| 354 |
+
if isinstance(results_json, str)
|
| 355 |
+
else results_json
|
| 356 |
+
)
|
| 357 |
|
| 358 |
output_parts = []
|
| 359 |
+
for i, r in enumerate(results[: self.count], start=1):
|
| 360 |
title = html.unescape(r.get("title", "").strip())
|
| 361 |
link = r.get("link", "").strip()
|
| 362 |
|
|
|
|
| 375 |
except Exception as e:
|
| 376 |
return f"Search failed: {str(e)}"
|
| 377 |
|
| 378 |
+
|
| 379 |
class DescribeImageTool(Tool):
|
| 380 |
name = "describe_image"
|
| 381 |
description = """Analyzes images and generates detailed text descriptions. Identifies objects, scenes, text, and visual elements within the image to provide context or understanding."""
|
|
|
|
| 383 |
inputs = {
|
| 384 |
"image_path": {
|
| 385 |
"type": "string",
|
| 386 |
+
"description": "The full path to the image file to describe.",
|
| 387 |
}
|
| 388 |
}
|
| 389 |
output_type = "string"
|
| 390 |
|
| 391 |
def forward(self, image_path: str) -> str:
|
| 392 |
import os
|
| 393 |
+
|
| 394 |
from PIL import Image
|
| 395 |
+
from transformers import BlipForConditionalGeneration, BlipProcessor
|
|
|
|
| 396 |
|
| 397 |
if not os.path.exists(image_path):
|
| 398 |
return f"❌ Image file does not exist: {image_path}"
|
| 399 |
|
| 400 |
try:
|
| 401 |
+
processor = BlipProcessor.from_pretrained(
|
| 402 |
+
"Salesforce/blip-image-captioning-base", use_fast=True
|
| 403 |
+
)
|
| 404 |
+
model = BlipForConditionalGeneration.from_pretrained(
|
| 405 |
+
"Salesforce/blip-image-captioning-base"
|
| 406 |
+
)
|
| 407 |
|
| 408 |
image = Image.open(image_path).convert("RGB")
|
| 409 |
inputs = processor(images=image, return_tensors="pt")
|
|
|
|
| 414 |
except Exception as e:
|
| 415 |
return f"❌ Failed to describe image: {e}"
|
| 416 |
|
| 417 |
+
|
| 418 |
class DownloadFileFromLinkTool(Tool):
|
| 419 |
name = "download_file_from_link"
|
| 420 |
description = "Downloads files from a URL and saves them locally. Supports various formats including PDFs, documents, images, and data files. Returns the local file path for further processing."
|
| 421 |
|
| 422 |
inputs = {
|
| 423 |
+
"link": {"type": "string", "description": "The URL to download the file from."},
|
|
|
|
|
|
|
|
|
|
| 424 |
"file_name": {
|
| 425 |
"type": "string",
|
| 426 |
"description": "Desired name of the saved file, without extension.",
|
| 427 |
+
"nullable": True,
|
| 428 |
+
},
|
| 429 |
}
|
| 430 |
|
| 431 |
output_type = "string"
|
| 432 |
+
SUPPORTED_EXTENSIONS = {
|
| 433 |
+
".xlsx",
|
| 434 |
+
".pdf",
|
| 435 |
+
".txt",
|
| 436 |
+
".csv",
|
| 437 |
+
".json",
|
| 438 |
+
".xml",
|
| 439 |
+
".html",
|
| 440 |
+
".jpg",
|
| 441 |
+
".jpeg",
|
| 442 |
+
".png",
|
| 443 |
+
".mp4",
|
| 444 |
+
".mp3",
|
| 445 |
+
".wav",
|
| 446 |
+
".zip",
|
| 447 |
+
}
|
| 448 |
|
| 449 |
def forward(self, link: str, file_name: str = "taskfile") -> str:
|
| 450 |
print(f"⬇️ Downloading file from: {link}")
|
|
|
|
| 457 |
return f"❌ Error: Request failed - {e}"
|
| 458 |
|
| 459 |
if response.status_code != 200:
|
| 460 |
+
return (
|
| 461 |
+
f"❌ Error: Unable to fetch file. Status code: {response.status_code}"
|
| 462 |
+
)
|
| 463 |
|
| 464 |
# Step 1: Try extracting extension from provided filename
|
| 465 |
base_name, provided_ext = os.path.splitext(file_name)
|
|
|
|
| 470 |
ext = provided_ext
|
| 471 |
else:
|
| 472 |
# Step 3: Try to infer from Content-Type
|
| 473 |
+
content_type = (
|
| 474 |
+
response.headers.get("Content-Type", "").split(";")[0].strip()
|
| 475 |
+
)
|
| 476 |
guessed_ext = mimetypes.guess_extension(content_type or "") or ""
|
| 477 |
|
| 478 |
# Step 4: If mimetype returned .bin or nothing useful, try to fallback to URL
|
|
|
|
| 498 |
|
| 499 |
return file_path
|
| 500 |
|
| 501 |
+
|
| 502 |
class DuckDuckGoSearchTool(Tool):
|
| 503 |
name = "web_search"
|
| 504 |
description = """Performs web searches and returns content from top results. Provides real-time information from across the internet including current events, facts, and website content relevant to your query."""
|
|
|
|
| 506 |
inputs = {
|
| 507 |
"query": {
|
| 508 |
"type": "string",
|
| 509 |
+
"description": "The search query to run on DuckDuckGo",
|
| 510 |
},
|
| 511 |
}
|
| 512 |
output_type = "string"
|
|
|
|
| 517 |
|
| 518 |
def forward(self, query: str) -> str:
|
| 519 |
self._configure()
|
| 520 |
+
print(
|
| 521 |
+
f"EXECUTING TOOL: duckduckgo_search(query='{query}', top_results={top_results})"
|
| 522 |
+
)
|
| 523 |
|
| 524 |
top_results = 5
|
| 525 |
|
|
|
|
| 544 |
title = res.get("title", "N/A")
|
| 545 |
url = res.get("href", "N/A")
|
| 546 |
snippet = res.get("body", "N/A")
|
| 547 |
+
|
| 548 |
output_lines.append(
|
| 549 |
f"Result {idx}:\n"
|
| 550 |
f"Title: {title}\n"
|
|
|
|
| 557 |
print(f"-> Tool Result (DuckDuckGo): {output[:1500]}...")
|
| 558 |
return output
|
| 559 |
|
| 560 |
+
except (
|
| 561 |
+
DuckDuckGoSearchException,
|
| 562 |
+
TimeoutException,
|
| 563 |
+
RatelimitException,
|
| 564 |
+
ConversationLimitException,
|
| 565 |
+
) as e:
|
| 566 |
retries += 1
|
| 567 |
+
print(
|
| 568 |
+
f"⚠️ DuckDuckGo Exception (Attempt {retries}/{max_retries}): {type(e).__name__}: {e}"
|
| 569 |
+
)
|
| 570 |
traceback.print_exc()
|
| 571 |
time.sleep(retry_sleep)
|
| 572 |
|
|
|
|
| 576 |
return f"Unhandled exception during DuckDuckGo search: {e}"
|
| 577 |
|
| 578 |
return f"❌ Failed to retrieve results after {max_retries} retries."
|
| 579 |
+
|
| 580 |
+
|
| 581 |
huggingface_ef = embedding_functions.HuggingFaceEmbeddingFunction(
|
| 582 |
+
api_key=os.environ["HF_TOKEN"], model_name="sentence-transformers/all-mpnet-base-v2"
|
|
|
|
| 583 |
)
|
| 584 |
+
SUPPORTED_EXTENSIONS = [
|
| 585 |
+
".txt",
|
| 586 |
+
".md",
|
| 587 |
+
".py",
|
| 588 |
+
".pdf",
|
| 589 |
+
".json",
|
| 590 |
+
".jsonl",
|
| 591 |
+
".html",
|
| 592 |
+
".htm",
|
| 593 |
+
]
|
| 594 |
+
|
| 595 |
|
| 596 |
class AddDocumentToVectorStoreTool(Tool):
|
| 597 |
name = "add_document_to_vector_store"
|
|
|
|
| 628 |
return f"Unsupported or missing file: {file_path}"
|
| 629 |
|
| 630 |
docs = self._load_file(path)
|
| 631 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 632 |
+
chunk_size=500, chunk_overlap=50
|
| 633 |
+
)
|
| 634 |
split_docs = text_splitter.split_documents(docs)
|
| 635 |
|
| 636 |
+
client = chromadb.Client(
|
| 637 |
+
chromadb.config.Settings(
|
| 638 |
+
persist_directory="./chroma_store",
|
| 639 |
+
)
|
| 640 |
+
)
|
| 641 |
|
| 642 |
+
collection = client.get_or_create_collection(
|
| 643 |
+
name=collection_name,
|
| 644 |
+
configuration={"embedding_function": huggingface_ef},
|
| 645 |
+
)
|
| 646 |
|
| 647 |
texts = [doc.page_content for doc in split_docs]
|
| 648 |
metadatas = [doc.metadata for doc in split_docs]
|
|
|
|
| 650 |
collection.add(
|
| 651 |
documents=texts,
|
| 652 |
metadatas=metadatas,
|
| 653 |
+
ids=[f"{path.stem}_{i}" for i in range(len(texts))],
|
| 654 |
)
|
| 655 |
|
| 656 |
return f"✅ Successfully added {len(texts)} chunks from '{file_path}' to collection '{collection_name}'."
|
|
|
|
| 659 |
print(f"❌ Error in add_to_vector_store: {e}")
|
| 660 |
traceback.print_exc()
|
| 661 |
return f"Error: {e}"
|
| 662 |
+
|
| 663 |
+
|
| 664 |
class QueryVectorStoreTool(Tool):
|
| 665 |
name = "query_downloaded_documents"
|
| 666 |
description = "Performs semantic searches across your downloaded documents. Use detailed queries to find specific information, concepts, or answers from your collected resources."
|
|
|
|
| 673 |
"top_k": {
|
| 674 |
"type": "integer",
|
| 675 |
"description": "Number of top results to retrieve. Usually between 3 and 30",
|
| 676 |
+
"nullable": True,
|
| 677 |
+
},
|
| 678 |
}
|
| 679 |
output_type = "string"
|
| 680 |
|
|
|
|
| 688 |
|
| 689 |
print(f"🔎 Querying vector store '{collection_name}' with: '{query}'")
|
| 690 |
try:
|
| 691 |
+
client = chromadb.Client(
|
| 692 |
+
chromadb.config.Settings(
|
| 693 |
+
persist_directory="./chroma_store",
|
| 694 |
+
)
|
| 695 |
+
)
|
| 696 |
collection = client.get_collection(name=collection_name)
|
| 697 |
|
| 698 |
results = collection.query(
|
|
|
|
| 705 |
doc = results["documents"][0][i]
|
| 706 |
metadata = results["metadatas"][0][i]
|
| 707 |
formatted.append(
|
| 708 |
+
f"Result {i+1}:\n" f"Content: {doc}\n" f"Metadata: {metadata}\n"
|
|
|
|
|
|
|
| 709 |
)
|
| 710 |
|
| 711 |
return "\n".join(formatted) or "No relevant documents found."
|
|
|
|
| 715 |
traceback.print_exc()
|
| 716 |
return f"Error querying vector store: {e}"
|
| 717 |
|
| 718 |
+
|
| 719 |
@tool
|
| 720 |
def image_question_answering(image_path: str, prompt: str) -> str:
|
| 721 |
"""
|
| 722 |
Analyzes images and answers specific questions about their content. Can identify objects, read text, describe scenes, or interpret visual information based on your questions.
|
| 723 |
+
|
| 724 |
Args:
|
| 725 |
image_path: The path to the image file
|
| 726 |
prompt: The question to ask about the image
|
| 727 |
+
|
| 728 |
Returns:
|
| 729 |
A string answer generated by the local Ollama model
|
| 730 |
"""
|
|
|
|
| 739 |
|
| 740 |
# Send the image and prompt to Ollama's local model
|
| 741 |
response = chat(
|
| 742 |
+
model="llava", # Assuming your model is named 'lava'
|
| 743 |
messages=[
|
| 744 |
{
|
| 745 |
+
"role": "user",
|
| 746 |
+
"content": prompt,
|
| 747 |
+
"images": [path],
|
| 748 |
},
|
| 749 |
],
|
| 750 |
+
options={"temperature": 0.2}, # Slight randomness for naturalness
|
| 751 |
)
|
| 752 |
|
| 753 |
return response.message.content.strip()
|
|
|
|
| 755 |
|
| 756 |
class VisitWebpageTool(Tool):
|
| 757 |
name = "visit_webpage"
|
| 758 |
+
description = "Loads a webpage from a URL and converts its content to markdown format. Use this to browse websites, extract information, or identify downloadable resources from a specific web address."
|
|
|
|
|
|
|
| 759 |
inputs = {
|
| 760 |
"url": {
|
| 761 |
"type": "string",
|
|
|
|
| 766 |
|
| 767 |
def forward(self, url: str) -> str:
|
| 768 |
try:
|
|
|
|
| 769 |
from urllib.parse import urlparse
|
| 770 |
|
| 771 |
import requests
|
| 772 |
from bs4 import BeautifulSoup
|
| 773 |
from markdownify import markdownify
|
| 774 |
from requests.exceptions import RequestException
|
|
|
|
| 775 |
from smolagents.utils import truncate_content
|
| 776 |
except ImportError as e:
|
| 777 |
raise ImportError(
|
| 778 |
"You must install packages `markdownify`, `requests`, and `beautifulsoup4` to run this tool: for instance run `pip install markdownify requests beautifulsoup4`."
|
| 779 |
) from e
|
| 780 |
+
|
| 781 |
try:
|
| 782 |
# Get the webpage content
|
| 783 |
headers = {
|
| 784 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
| 785 |
}
|
| 786 |
response = requests.get(url, headers=headers, timeout=20)
|
| 787 |
response.raise_for_status()
|
| 788 |
+
|
| 789 |
# Parse the HTML with BeautifulSoup
|
| 790 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 791 |
+
|
| 792 |
# Extract domain name for context
|
| 793 |
domain = urlparse(url).netloc
|
| 794 |
+
|
| 795 |
# Remove common clutter elements
|
| 796 |
self._remove_clutter(soup)
|
| 797 |
+
|
| 798 |
# Try to identify and prioritize main content
|
| 799 |
main_content = self._extract_main_content(soup)
|
| 800 |
+
|
| 801 |
if main_content:
|
| 802 |
# Convert the cleaned HTML to markdown
|
| 803 |
markdown_content = markdownify(str(main_content)).strip()
|
| 804 |
else:
|
| 805 |
# Fallback to full page content if main content extraction fails
|
| 806 |
markdown_content = markdownify(str(soup)).strip()
|
| 807 |
+
|
| 808 |
# Post-process the markdown content
|
| 809 |
markdown_content = self._clean_markdown(markdown_content)
|
| 810 |
+
|
| 811 |
# Add source information
|
| 812 |
result = f"Content from {domain}:\n\n{markdown_content}"
|
| 813 |
+
|
| 814 |
return truncate_content(result, 40000)
|
| 815 |
|
| 816 |
except requests.exceptions.Timeout:
|
|
|
|
| 819 |
return f"Error fetching the webpage: {str(e)}"
|
| 820 |
except Exception as e:
|
| 821 |
return f"An unexpected error occurred: {str(e)}"
|
| 822 |
+
|
| 823 |
def _remove_clutter(self, soup):
|
| 824 |
"""Remove common elements that clutter web pages."""
|
| 825 |
# Common non-content elements to remove
|
| 826 |
clutter_selectors = [
|
| 827 |
+
"header",
|
| 828 |
+
"footer",
|
| 829 |
+
"nav",
|
| 830 |
+
".nav",
|
| 831 |
+
".navigation",
|
| 832 |
+
".menu",
|
| 833 |
+
".sidebar",
|
| 834 |
+
".footer",
|
| 835 |
+
".header",
|
| 836 |
+
"#footer",
|
| 837 |
+
"#header",
|
| 838 |
+
"#nav",
|
| 839 |
+
"#sidebar",
|
| 840 |
+
".widget",
|
| 841 |
+
".cookie",
|
| 842 |
+
".cookies",
|
| 843 |
+
".ad",
|
| 844 |
+
".ads",
|
| 845 |
+
".advertisement",
|
| 846 |
+
"script",
|
| 847 |
+
"style",
|
| 848 |
+
"noscript",
|
| 849 |
+
"iframe",
|
| 850 |
+
".social",
|
| 851 |
+
".share",
|
| 852 |
+
".comment",
|
| 853 |
+
".comments",
|
| 854 |
+
".subscription",
|
| 855 |
+
".newsletter",
|
| 856 |
+
'[role="banner"]',
|
| 857 |
+
'[role="navigation"]',
|
| 858 |
+
'[role="complementary"]',
|
| 859 |
]
|
| 860 |
+
|
| 861 |
for selector in clutter_selectors:
|
| 862 |
for element in soup.select(selector):
|
| 863 |
element.decompose()
|
| 864 |
+
|
| 865 |
# Remove hidden elements
|
| 866 |
+
for hidden in soup.select(
|
| 867 |
+
'[style*="display: none"], [style*="display:none"], [style*="visibility: hidden"], [style*="visibility:hidden"], [hidden]'
|
| 868 |
+
):
|
| 869 |
hidden.decompose()
|
| 870 |
+
|
| 871 |
def _extract_main_content(self, soup):
|
| 872 |
"""Try to identify and extract the main content of the page."""
|
| 873 |
# Priority order for common main content containers
|
| 874 |
main_content_selectors = [
|
| 875 |
+
"main",
|
| 876 |
+
'[role="main"]',
|
| 877 |
+
"article",
|
| 878 |
+
".content",
|
| 879 |
+
".main-content",
|
| 880 |
+
".post-content",
|
| 881 |
+
"#content",
|
| 882 |
+
"#main",
|
| 883 |
+
"#main-content",
|
| 884 |
+
".article",
|
| 885 |
+
".post",
|
| 886 |
+
".entry",
|
| 887 |
+
".page-content",
|
| 888 |
+
".entry-content",
|
| 889 |
]
|
| 890 |
+
|
| 891 |
# Try to find the main content container
|
| 892 |
for selector in main_content_selectors:
|
| 893 |
main_content = soup.select(selector)
|
|
|
|
| 896 |
if len(main_content) > 1:
|
| 897 |
return max(main_content, key=lambda x: len(x.get_text()))
|
| 898 |
return main_content[0]
|
| 899 |
+
|
| 900 |
# If no main content container found, look for the largest text block
|
| 901 |
+
paragraphs = soup.find_all("p")
|
| 902 |
if paragraphs:
|
| 903 |
# Find the parent that contains the most paragraphs
|
| 904 |
parents = {}
|
|
|
|
| 907 |
if p.parent not in parents:
|
| 908 |
parents[p.parent] = 0
|
| 909 |
parents[p.parent] += 1
|
| 910 |
+
|
| 911 |
if parents:
|
| 912 |
# Return the parent with the most paragraphs
|
| 913 |
return max(parents.items(), key=lambda x: x[1])[0]
|
| 914 |
+
|
| 915 |
# Return None if we can't identify main content
|
| 916 |
return None
|
| 917 |
+
|
| 918 |
def _clean_markdown(self, content):
|
| 919 |
"""Clean up the markdown content."""
|
| 920 |
# Normalize whitespace
|
| 921 |
+
content = re.sub(r"\n{3,}", "\n\n", content)
|
| 922 |
+
|
| 923 |
# Remove consecutive duplicate links
|
| 924 |
+
content = re.sub(r"(\[.*?\]\(.*?\))\s*\1+", r"\1", content)
|
| 925 |
+
|
| 926 |
# Remove very short lines that are likely menu items
|
| 927 |
+
lines = content.split("\n")
|
| 928 |
filtered_lines = []
|
| 929 |
+
|
| 930 |
# Skip consecutive short lines (likely menus)
|
| 931 |
short_line_threshold = 40 # characters
|
| 932 |
consecutive_short_lines = 0
|
| 933 |
max_consecutive_short_lines = 3
|
| 934 |
+
|
| 935 |
for line in lines:
|
| 936 |
stripped_line = line.strip()
|
| 937 |
+
if len(
|
| 938 |
+
stripped_line
|
| 939 |
+
) < short_line_threshold and not stripped_line.startswith("#"):
|
| 940 |
consecutive_short_lines += 1
|
| 941 |
if consecutive_short_lines > max_consecutive_short_lines:
|
| 942 |
continue
|
| 943 |
else:
|
| 944 |
consecutive_short_lines = 0
|
| 945 |
+
|
| 946 |
filtered_lines.append(line)
|
| 947 |
+
|
| 948 |
+
content = "\n".join(filtered_lines)
|
| 949 |
+
|
| 950 |
# Remove duplicate headers
|
| 951 |
seen_headers = set()
|
| 952 |
+
lines = content.split("\n")
|
| 953 |
filtered_lines = []
|
| 954 |
+
|
| 955 |
for line in lines:
|
| 956 |
+
if line.startswith("#"):
|
| 957 |
header_text = line.strip()
|
| 958 |
if header_text in seen_headers:
|
| 959 |
continue
|
| 960 |
seen_headers.add(header_text)
|
| 961 |
filtered_lines.append(line)
|
| 962 |
+
|
| 963 |
+
content = "\n".join(filtered_lines)
|
| 964 |
+
|
| 965 |
# Remove lines containing common footer patterns
|
| 966 |
footer_patterns = [
|
| 967 |
+
r"^copyright",
|
| 968 |
+
r"^©",
|
| 969 |
+
r"^all rights reserved",
|
| 970 |
+
r"^terms",
|
| 971 |
+
r"^privacy policy",
|
| 972 |
+
r"^contact us",
|
| 973 |
+
r"^follow us",
|
| 974 |
+
r"^social media",
|
| 975 |
+
r"^disclaimer",
|
| 976 |
]
|
| 977 |
+
|
| 978 |
+
footer_pattern = "|".join(footer_patterns)
|
| 979 |
+
lines = content.split("\n")
|
| 980 |
filtered_lines = []
|
| 981 |
+
|
| 982 |
for line in lines:
|
| 983 |
if not re.search(footer_pattern, line.lower()):
|
| 984 |
filtered_lines.append(line)
|
| 985 |
+
|
| 986 |
+
content = "\n".join(filtered_lines)
|
| 987 |
+
|
| 988 |
return content
|
| 989 |
+
|
| 990 |
|
| 991 |
class ArxivSearchTool(Tool):
|
| 992 |
name = "arxiv_search"
|
| 993 |
description = """Searches arXiv for academic papers and returns structured information including titles, authors, publication dates, abstracts, and download links."""
|
| 994 |
|
|
|
|
| 995 |
inputs = {
|
| 996 |
+
"query": {
|
| 997 |
+
"type": "string",
|
| 998 |
+
"description": "A research-related query (e.g., 'AI regulation')",
|
| 999 |
+
},
|
| 1000 |
+
"from_date": {
|
| 1001 |
+
"type": "string",
|
| 1002 |
+
"description": "Optional search start date in format (YYYY or YYYY-MM or YYYY-MM-DD) (e.g., '2022-06' or '2022' or '2022-04-12')",
|
| 1003 |
+
"nullable": True,
|
| 1004 |
+
},
|
| 1005 |
+
"to_date": {
|
| 1006 |
+
"type": "string",
|
| 1007 |
+
"description": "Optional search end date in (YYYY or YYYY-MM or YYYY-MM-DD) (e.g., '2022-06' or '2022' or '2022-04-12')",
|
| 1008 |
+
"nullable": True,
|
| 1009 |
+
},
|
| 1010 |
}
|
| 1011 |
|
| 1012 |
output_type = "string"
|
|
|
|
| 1040 |
f"Summary : {p['abstract'][:500]}{'...' if len(p['abstract'])>500 else ''}",
|
| 1041 |
f"Entry ID : {p['entry_link']}",
|
| 1042 |
f"Download link: {p['download_link']}",
|
| 1043 |
+
"",
|
| 1044 |
]
|
| 1045 |
|
| 1046 |
return "\n".join(output_lines).strip()
|
| 1047 |
+
|
| 1048 |
+
|
| 1049 |
+
from typing import Dict, List
|
| 1050 |
|
| 1051 |
import requests
|
| 1052 |
from bs4 import BeautifulSoup
|
| 1053 |
+
|
| 1054 |
|
| 1055 |
def fetch_and_parse_arxiv(url: str) -> List[Dict[str, str]]:
|
| 1056 |
"""
|
|
|
|
| 1079 |
|
| 1080 |
# Abstract
|
| 1081 |
ab = li.find("span", class_="abstract-full")
|
| 1082 |
+
abstract = (
|
| 1083 |
+
ab.get_text(strip=True).replace("Abstract:", "").strip() if ab else ""
|
| 1084 |
+
)
|
| 1085 |
|
| 1086 |
# Published date
|
| 1087 |
d = li.find("p", class_="is-size-7")
|
|
|
|
| 1099 |
doi = a_tag["href"]
|
| 1100 |
break
|
| 1101 |
|
| 1102 |
+
results.append(
|
| 1103 |
+
{
|
| 1104 |
+
"title": title,
|
| 1105 |
+
"authors": authors,
|
| 1106 |
+
"published": published,
|
| 1107 |
+
"abstract": abstract,
|
| 1108 |
+
"entry_link": entry_link,
|
| 1109 |
+
"download_link": (
|
| 1110 |
+
entry_link.replace("abs", "pdf") if "abs" in entry_link else "N/A"
|
| 1111 |
+
),
|
| 1112 |
+
}
|
| 1113 |
+
)
|
| 1114 |
|
| 1115 |
return results
|
| 1116 |
|
| 1117 |
+
|
| 1118 |
from urllib.parse import quote_plus
|
| 1119 |
|
| 1120 |
+
|
| 1121 |
def build_arxiv_url(
|
| 1122 |
+
query: str, from_date: str = None, to_date: str = None, size: int = 50
|
|
|
|
|
|
|
|
|
|
| 1123 |
) -> str:
|
| 1124 |
"""
|
| 1125 |
Build an arXiv advanced-search URL matching the exact segment order:
|
tools_beta.py
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
-
import
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
-
import pandas as pd
|
| 7 |
import fitz # PyMuPDF
|
| 8 |
-
from urllib.parse import unquote
|
| 9 |
-
from smolagents import Tool
|
| 10 |
-
from smolagents import Tool
|
| 11 |
import requests
|
| 12 |
-
import traceback
|
| 13 |
from langchain_community.retrievers import BM25Retriever
|
| 14 |
from smolagents import Tool
|
| 15 |
-
import math
|
| 16 |
-
|
| 17 |
-
import subprocess
|
| 18 |
-
import sys
|
| 19 |
-
import os
|
| 20 |
-
import re
|
| 21 |
-
|
| 22 |
|
| 23 |
|
| 24 |
class DetectVisualElementsTool(Tool):
|
|
@@ -28,204 +17,204 @@ class DetectVisualElementsTool(Tool):
|
|
| 28 |
inputs = {
|
| 29 |
"image_path": {
|
| 30 |
"type": "string",
|
| 31 |
-
"description": "The full path to the image file to analyze."
|
| 32 |
}
|
| 33 |
}
|
| 34 |
output_type = "string"
|
| 35 |
|
| 36 |
def forward(self, image_path: str) -> list:
|
| 37 |
import os
|
| 38 |
-
|
| 39 |
import torch
|
| 40 |
-
import torchvision.transforms as T
|
| 41 |
import torchvision.models.detection as models
|
|
|
|
|
|
|
| 42 |
|
| 43 |
label_map = {
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
|
| 230 |
if not os.path.exists(image_path):
|
| 231 |
return [f"❌ Image file does not exist: {image_path}"]
|
|
@@ -246,7 +235,7 @@ class DetectVisualElementsTool(Tool):
|
|
| 246 |
if score > 0.8:
|
| 247 |
print(str(label_id.item()))
|
| 248 |
labels_list.append(label_map.get(label_id.item()))
|
| 249 |
-
|
| 250 |
labels = ",".join(labels_list)
|
| 251 |
|
| 252 |
return labels or ["⚠️ No confident visual elements detected."]
|
|
@@ -263,19 +252,17 @@ class ChessPositionSolverTool(Tool):
|
|
| 263 |
"url": {
|
| 264 |
"type": "string",
|
| 265 |
"description": "Optional. URL pointing to an image of a chessboard position.",
|
| 266 |
-
"nullable": True
|
| 267 |
},
|
| 268 |
"file_path": {
|
| 269 |
"type": "string",
|
| 270 |
"description": "Optional. Local file path to an image of a chessboard position.",
|
| 271 |
-
"nullable": True
|
| 272 |
-
}
|
| 273 |
}
|
| 274 |
|
| 275 |
output_type = "string"
|
| 276 |
|
| 277 |
-
|
| 278 |
-
|
| 279 |
def forward(self, url: str = None, file_path: str = None) -> str:
|
| 280 |
if not url and not file_path:
|
| 281 |
return "❌ Please provide either a URL or a local file path to the chessboard image."
|
|
@@ -303,7 +290,10 @@ class ChessPositionSolverTool(Tool):
|
|
| 303 |
|
| 304 |
board = chess.Board(fen)
|
| 305 |
|
| 306 |
-
STOCKFISH_PATH = os.getenv(
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
# Step 3 - Analyze with Stockfish
|
| 309 |
engine = chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH)
|
|
@@ -327,17 +317,19 @@ def patch_pyproject(path):
|
|
| 327 |
|
| 328 |
with open(pyproject_path, "w", encoding="utf-8") as f:
|
| 329 |
for line in lines:
|
| 330 |
-
if re.match(r
|
| 331 |
f.write('python = ">=3.8,<3.12"\n')
|
| 332 |
else:
|
| 333 |
f.write(line)
|
| 334 |
|
|
|
|
| 335 |
def install_chesscog():
|
| 336 |
TARGET_DIR = "chesscog"
|
| 337 |
REPO_URL = "https://github.com/georg-wolflein/chesscog.git"
|
| 338 |
|
| 339 |
try:
|
| 340 |
-
|
|
|
|
| 341 |
print("✅ chesscog already installed.")
|
| 342 |
# return
|
| 343 |
except ImportError:
|
|
@@ -348,9 +340,12 @@ def install_chesscog():
|
|
| 348 |
|
| 349 |
patch_pyproject(TARGET_DIR)
|
| 350 |
|
| 351 |
-
subprocess.run(
|
|
|
|
|
|
|
| 352 |
print("✅ chesscog installed successfully.")
|
| 353 |
|
|
|
|
| 354 |
class RetrieverTool(Tool):
|
| 355 |
name = "retriever"
|
| 356 |
description = "Retrieves the most similar known question to the query."
|
|
@@ -374,6 +369,7 @@ class RetrieverTool(Tool):
|
|
| 374 |
else:
|
| 375 |
return "No similar question found."
|
| 376 |
|
|
|
|
| 377 |
class CalculatorTool(Tool):
|
| 378 |
name = "calculator"
|
| 379 |
description = """Performs basic mathematical calculations (e.g., addition, subtraction, multiplication, division, exponentiation, square root).
|
|
@@ -382,7 +378,7 @@ Use this tool whenever math is required, especially for numeric reasoning."""
|
|
| 382 |
inputs = {
|
| 383 |
"expression": {
|
| 384 |
"type": "string",
|
| 385 |
-
"description": "A basic math expression, e.g., '5 + 3 * 2', 'sqrt(49)', '2 ** 3'. No variables or natural language."
|
| 386 |
}
|
| 387 |
}
|
| 388 |
output_type = "string"
|
|
@@ -390,8 +386,7 @@ Use this tool whenever math is required, especially for numeric reasoning."""
|
|
| 390 |
def forward(self, expression: str) -> str:
|
| 391 |
try:
|
| 392 |
allowed_names = {
|
| 393 |
-
k: v for k, v in math.__dict__.items()
|
| 394 |
-
if not k.startswith("__")
|
| 395 |
}
|
| 396 |
allowed_names.update({"abs": abs, "round": round})
|
| 397 |
result = eval(expression, {"__builtins__": {}}, allowed_names)
|
|
@@ -399,6 +394,7 @@ Use this tool whenever math is required, especially for numeric reasoning."""
|
|
| 399 |
except Exception as e:
|
| 400 |
return f"Error: Invalid math expression. ({e})"
|
| 401 |
|
|
|
|
| 402 |
class AnalyzeChessImageTool(Tool):
|
| 403 |
name = "analyze_chess_image"
|
| 404 |
description = """Extracts the board state from a chessboard image and returns the best move for black (in algebraic notation)."""
|
|
@@ -406,7 +402,7 @@ class AnalyzeChessImageTool(Tool):
|
|
| 406 |
inputs = {
|
| 407 |
"file_path": {
|
| 408 |
"type": "string",
|
| 409 |
-
"description": "Path to the image file of the chess board."
|
| 410 |
}
|
| 411 |
}
|
| 412 |
output_type = "string"
|
|
@@ -431,7 +427,6 @@ class AnalyzeChessImageTool(Tool):
|
|
| 431 |
return f"❌ Chess analysis failed: {e}"
|
| 432 |
|
| 433 |
|
| 434 |
-
|
| 435 |
class ExecutePythonCodeTool(Tool):
|
| 436 |
name = "execute_python_code"
|
| 437 |
description = """Executes a provided Python code snippet in a controlled, sandboxed environment.
|
|
@@ -440,7 +435,7 @@ class ExecutePythonCodeTool(Tool):
|
|
| 440 |
inputs = {
|
| 441 |
"code": {
|
| 442 |
"type": "string",
|
| 443 |
-
"description": "A valid Python code block that needs to be executed. It should be a string containing executable Python code."
|
| 444 |
}
|
| 445 |
}
|
| 446 |
output_type = "string"
|
|
@@ -485,8 +480,8 @@ class ExecutePythonCodeTool(Tool):
|
|
| 485 |
exec(code, restricted_globals, exec_locals)
|
| 486 |
|
| 487 |
# If the code produces a result, we return that as output
|
| 488 |
-
if
|
| 489 |
-
return str(exec_locals[
|
| 490 |
else:
|
| 491 |
return "❌ The code did not produce a result."
|
| 492 |
|
|
@@ -494,7 +489,6 @@ class ExecutePythonCodeTool(Tool):
|
|
| 494 |
return f"❌ Error executing code: {str(e)}"
|
| 495 |
|
| 496 |
|
| 497 |
-
|
| 498 |
class ArxivSearchTool(Tool):
|
| 499 |
name = "arxiv_search"
|
| 500 |
description = """Searches arXiv for academic papers and returns structured information including titles, authors, publication dates, and abstracts. Ideal for finding scientific research on specific topics."""
|
|
@@ -502,7 +496,7 @@ class ArxivSearchTool(Tool):
|
|
| 502 |
inputs = {
|
| 503 |
"query": {
|
| 504 |
"type": "string",
|
| 505 |
-
"description": "A research-related query string (e.g., 'Superstring Cosmology')"
|
| 506 |
}
|
| 507 |
}
|
| 508 |
output_type = "string"
|
|
@@ -512,9 +506,7 @@ class ArxivSearchTool(Tool):
|
|
| 512 |
|
| 513 |
try:
|
| 514 |
search_docs = ArxivLoader(
|
| 515 |
-
query=query,
|
| 516 |
-
load_max_docs=max_results,
|
| 517 |
-
load_all_available_meta=True
|
| 518 |
).load()
|
| 519 |
except Exception as e:
|
| 520 |
return f"❌ Arxiv search failed: {e}"
|
|
@@ -539,7 +531,9 @@ class ArxivSearchTool(Tool):
|
|
| 539 |
# output_lines.append(f"Journal Ref : {meta.get('journal_ref', '[N/A]')}")
|
| 540 |
# output_lines.append(f"Primary Cat. : {meta.get('primary_category', '[N/A]')}")
|
| 541 |
# output_lines.append(f"Categories : {', '.join(meta.get('categories', [])) or '[N/A]'}")
|
| 542 |
-
output_lines.append(
|
|
|
|
|
|
|
| 543 |
|
| 544 |
if content:
|
| 545 |
preview = content[:30] + ("..." if len(content) > 30 else "")
|
|
@@ -547,4 +541,4 @@ class ArxivSearchTool(Tool):
|
|
| 547 |
|
| 548 |
output_lines.append("") # spacing between results
|
| 549 |
|
| 550 |
-
return "\n".join(output_lines).strip()
|
|
|
|
| 1 |
+
import math
|
| 2 |
import os
|
| 3 |
import re
|
| 4 |
+
import subprocess
|
| 5 |
+
import sys
|
| 6 |
+
|
|
|
|
| 7 |
import fitz # PyMuPDF
|
|
|
|
|
|
|
|
|
|
| 8 |
import requests
|
|
|
|
| 9 |
from langchain_community.retrievers import BM25Retriever
|
| 10 |
from smolagents import Tool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
class DetectVisualElementsTool(Tool):
|
|
|
|
| 17 |
inputs = {
|
| 18 |
"image_path": {
|
| 19 |
"type": "string",
|
| 20 |
+
"description": "The full path to the image file to analyze.",
|
| 21 |
}
|
| 22 |
}
|
| 23 |
output_type = "string"
|
| 24 |
|
| 25 |
def forward(self, image_path: str) -> list:
|
| 26 |
import os
|
| 27 |
+
|
| 28 |
import torch
|
|
|
|
| 29 |
import torchvision.models.detection as models
|
| 30 |
+
import torchvision.transforms as T
|
| 31 |
+
from PIL import Image
|
| 32 |
|
| 33 |
label_map = {
|
| 34 |
+
0: "unlabeled",
|
| 35 |
+
1: "person",
|
| 36 |
+
2: "bicycle",
|
| 37 |
+
3: "car",
|
| 38 |
+
4: "motorcycle",
|
| 39 |
+
5: "airplane",
|
| 40 |
+
6: "bus",
|
| 41 |
+
7: "train",
|
| 42 |
+
8: "truck",
|
| 43 |
+
9: "boat",
|
| 44 |
+
10: "traffic",
|
| 45 |
+
11: "fire",
|
| 46 |
+
12: "street",
|
| 47 |
+
13: "stop",
|
| 48 |
+
14: "parking",
|
| 49 |
+
15: "bench",
|
| 50 |
+
16: "bird",
|
| 51 |
+
17: "cat",
|
| 52 |
+
18: "dog",
|
| 53 |
+
19: "horse",
|
| 54 |
+
20: "sheep",
|
| 55 |
+
21: "cow",
|
| 56 |
+
22: "elephant",
|
| 57 |
+
23: "bear",
|
| 58 |
+
24: "zebra",
|
| 59 |
+
25: "giraffe",
|
| 60 |
+
26: "hat",
|
| 61 |
+
27: "backpack",
|
| 62 |
+
28: "umbrella",
|
| 63 |
+
29: "shoe",
|
| 64 |
+
30: "eye",
|
| 65 |
+
31: "handbag",
|
| 66 |
+
32: "tie",
|
| 67 |
+
33: "suitcase",
|
| 68 |
+
34: "frisbee",
|
| 69 |
+
35: "skis",
|
| 70 |
+
36: "snowboard",
|
| 71 |
+
37: "sports",
|
| 72 |
+
38: "kite",
|
| 73 |
+
39: "baseball",
|
| 74 |
+
40: "baseball",
|
| 75 |
+
41: "skateboard",
|
| 76 |
+
42: "surfboard",
|
| 77 |
+
43: "tennis",
|
| 78 |
+
44: "bottle",
|
| 79 |
+
45: "plate",
|
| 80 |
+
46: "wine",
|
| 81 |
+
47: "cup",
|
| 82 |
+
48: "fork",
|
| 83 |
+
49: "knife",
|
| 84 |
+
50: "spoon",
|
| 85 |
+
51: "bowl",
|
| 86 |
+
52: "banana",
|
| 87 |
+
53: "apple",
|
| 88 |
+
54: "sandwich",
|
| 89 |
+
55: "orange",
|
| 90 |
+
56: "broccoli",
|
| 91 |
+
57: "carrot",
|
| 92 |
+
58: "hot",
|
| 93 |
+
59: "pizza",
|
| 94 |
+
60: "donut",
|
| 95 |
+
61: "cake",
|
| 96 |
+
62: "chair",
|
| 97 |
+
63: "couch",
|
| 98 |
+
64: "potted",
|
| 99 |
+
65: "bed",
|
| 100 |
+
66: "mirror",
|
| 101 |
+
67: "dining",
|
| 102 |
+
68: "window",
|
| 103 |
+
69: "desk",
|
| 104 |
+
70: "toilet",
|
| 105 |
+
71: "door",
|
| 106 |
+
72: "tv",
|
| 107 |
+
73: "laptop",
|
| 108 |
+
74: "mouse",
|
| 109 |
+
75: "remote",
|
| 110 |
+
76: "keyboard",
|
| 111 |
+
77: "cell",
|
| 112 |
+
78: "microwave",
|
| 113 |
+
79: "oven",
|
| 114 |
+
80: "toaster",
|
| 115 |
+
81: "sink",
|
| 116 |
+
82: "refrigerator",
|
| 117 |
+
83: "blender",
|
| 118 |
+
84: "book",
|
| 119 |
+
85: "clock",
|
| 120 |
+
86: "vase",
|
| 121 |
+
87: "scissors",
|
| 122 |
+
88: "teddy",
|
| 123 |
+
89: "hair",
|
| 124 |
+
90: "toothbrush",
|
| 125 |
+
91: "hair",
|
| 126 |
+
92: "banner",
|
| 127 |
+
93: "blanket",
|
| 128 |
+
94: "branch",
|
| 129 |
+
95: "bridge",
|
| 130 |
+
96: "building",
|
| 131 |
+
97: "bush",
|
| 132 |
+
98: "cabinet",
|
| 133 |
+
99: "cage",
|
| 134 |
+
100: "cardboard",
|
| 135 |
+
101: "carpet",
|
| 136 |
+
102: "ceiling",
|
| 137 |
+
103: "ceiling",
|
| 138 |
+
104: "cloth",
|
| 139 |
+
105: "clothes",
|
| 140 |
+
106: "clouds",
|
| 141 |
+
107: "counter",
|
| 142 |
+
108: "cupboard",
|
| 143 |
+
109: "curtain",
|
| 144 |
+
110: "desk",
|
| 145 |
+
111: "dirt",
|
| 146 |
+
112: "door",
|
| 147 |
+
113: "fence",
|
| 148 |
+
114: "floor",
|
| 149 |
+
115: "floor",
|
| 150 |
+
116: "floor",
|
| 151 |
+
117: "floor",
|
| 152 |
+
118: "floor",
|
| 153 |
+
119: "flower",
|
| 154 |
+
120: "fog",
|
| 155 |
+
121: "food",
|
| 156 |
+
122: "fruit",
|
| 157 |
+
123: "furniture",
|
| 158 |
+
124: "grass",
|
| 159 |
+
125: "gravel",
|
| 160 |
+
126: "ground",
|
| 161 |
+
127: "hill",
|
| 162 |
+
128: "house",
|
| 163 |
+
129: "leaves",
|
| 164 |
+
130: "light",
|
| 165 |
+
131: "mat",
|
| 166 |
+
132: "metal",
|
| 167 |
+
133: "mirror",
|
| 168 |
+
134: "moss",
|
| 169 |
+
135: "mountain",
|
| 170 |
+
136: "mud",
|
| 171 |
+
137: "napkin",
|
| 172 |
+
138: "net",
|
| 173 |
+
139: "paper",
|
| 174 |
+
140: "pavement",
|
| 175 |
+
141: "pillow",
|
| 176 |
+
142: "plant",
|
| 177 |
+
143: "plastic",
|
| 178 |
+
144: "platform",
|
| 179 |
+
145: "playingfield",
|
| 180 |
+
146: "railing",
|
| 181 |
+
147: "railroad",
|
| 182 |
+
148: "river",
|
| 183 |
+
149: "road",
|
| 184 |
+
150: "rock",
|
| 185 |
+
151: "roof",
|
| 186 |
+
152: "rug",
|
| 187 |
+
153: "salad",
|
| 188 |
+
154: "sand",
|
| 189 |
+
155: "sea",
|
| 190 |
+
156: "shelf",
|
| 191 |
+
157: "sky",
|
| 192 |
+
158: "skyscraper",
|
| 193 |
+
159: "snow",
|
| 194 |
+
160: "solid",
|
| 195 |
+
161: "stairs",
|
| 196 |
+
162: "stone",
|
| 197 |
+
163: "straw",
|
| 198 |
+
164: "structural",
|
| 199 |
+
165: "table",
|
| 200 |
+
166: "tent",
|
| 201 |
+
167: "textile",
|
| 202 |
+
168: "towel",
|
| 203 |
+
169: "tree",
|
| 204 |
+
170: "vegetable",
|
| 205 |
+
171: "wall",
|
| 206 |
+
172: "wall",
|
| 207 |
+
173: "wall",
|
| 208 |
+
174: "wall",
|
| 209 |
+
175: "wall",
|
| 210 |
+
176: "wall",
|
| 211 |
+
177: "wall",
|
| 212 |
+
178: "water",
|
| 213 |
+
179: "waterdrops",
|
| 214 |
+
180: "window",
|
| 215 |
+
181: "window",
|
| 216 |
+
182: "wood",
|
| 217 |
+
}
|
|
|
|
| 218 |
|
| 219 |
if not os.path.exists(image_path):
|
| 220 |
return [f"❌ Image file does not exist: {image_path}"]
|
|
|
|
| 235 |
if score > 0.8:
|
| 236 |
print(str(label_id.item()))
|
| 237 |
labels_list.append(label_map.get(label_id.item()))
|
| 238 |
+
|
| 239 |
labels = ",".join(labels_list)
|
| 240 |
|
| 241 |
return labels or ["⚠️ No confident visual elements detected."]
|
|
|
|
| 252 |
"url": {
|
| 253 |
"type": "string",
|
| 254 |
"description": "Optional. URL pointing to an image of a chessboard position.",
|
| 255 |
+
"nullable": True,
|
| 256 |
},
|
| 257 |
"file_path": {
|
| 258 |
"type": "string",
|
| 259 |
"description": "Optional. Local file path to an image of a chessboard position.",
|
| 260 |
+
"nullable": True,
|
| 261 |
+
},
|
| 262 |
}
|
| 263 |
|
| 264 |
output_type = "string"
|
| 265 |
|
|
|
|
|
|
|
| 266 |
def forward(self, url: str = None, file_path: str = None) -> str:
|
| 267 |
if not url and not file_path:
|
| 268 |
return "❌ Please provide either a URL or a local file path to the chessboard image."
|
|
|
|
| 290 |
|
| 291 |
board = chess.Board(fen)
|
| 292 |
|
| 293 |
+
STOCKFISH_PATH = os.getenv(
|
| 294 |
+
"STOCKFISH_PATH",
|
| 295 |
+
"/home/boom/Desktop/repos/boombot/engines/stockfish-ubuntu-x86-64-bmi2",
|
| 296 |
+
) # Ensure Stockfish is available
|
| 297 |
|
| 298 |
# Step 3 - Analyze with Stockfish
|
| 299 |
engine = chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH)
|
|
|
|
| 317 |
|
| 318 |
with open(pyproject_path, "w", encoding="utf-8") as f:
|
| 319 |
for line in lines:
|
| 320 |
+
if re.match(r"\s*python\s*=", line):
|
| 321 |
f.write('python = ">=3.8,<3.12"\n')
|
| 322 |
else:
|
| 323 |
f.write(line)
|
| 324 |
|
| 325 |
+
|
| 326 |
def install_chesscog():
|
| 327 |
TARGET_DIR = "chesscog"
|
| 328 |
REPO_URL = "https://github.com/georg-wolflein/chesscog.git"
|
| 329 |
|
| 330 |
try:
|
| 331 |
+
pass
|
| 332 |
+
|
| 333 |
print("✅ chesscog already installed.")
|
| 334 |
# return
|
| 335 |
except ImportError:
|
|
|
|
| 340 |
|
| 341 |
patch_pyproject(TARGET_DIR)
|
| 342 |
|
| 343 |
+
subprocess.run(
|
| 344 |
+
[sys.executable, "-m", "pip", "install", f"./{TARGET_DIR}"], check=True
|
| 345 |
+
)
|
| 346 |
print("✅ chesscog installed successfully.")
|
| 347 |
|
| 348 |
+
|
| 349 |
class RetrieverTool(Tool):
|
| 350 |
name = "retriever"
|
| 351 |
description = "Retrieves the most similar known question to the query."
|
|
|
|
| 369 |
else:
|
| 370 |
return "No similar question found."
|
| 371 |
|
| 372 |
+
|
| 373 |
class CalculatorTool(Tool):
|
| 374 |
name = "calculator"
|
| 375 |
description = """Performs basic mathematical calculations (e.g., addition, subtraction, multiplication, division, exponentiation, square root).
|
|
|
|
| 378 |
inputs = {
|
| 379 |
"expression": {
|
| 380 |
"type": "string",
|
| 381 |
+
"description": "A basic math expression, e.g., '5 + 3 * 2', 'sqrt(49)', '2 ** 3'. No variables or natural language.",
|
| 382 |
}
|
| 383 |
}
|
| 384 |
output_type = "string"
|
|
|
|
| 386 |
def forward(self, expression: str) -> str:
|
| 387 |
try:
|
| 388 |
allowed_names = {
|
| 389 |
+
k: v for k, v in math.__dict__.items() if not k.startswith("__")
|
|
|
|
| 390 |
}
|
| 391 |
allowed_names.update({"abs": abs, "round": round})
|
| 392 |
result = eval(expression, {"__builtins__": {}}, allowed_names)
|
|
|
|
| 394 |
except Exception as e:
|
| 395 |
return f"Error: Invalid math expression. ({e})"
|
| 396 |
|
| 397 |
+
|
| 398 |
class AnalyzeChessImageTool(Tool):
|
| 399 |
name = "analyze_chess_image"
|
| 400 |
description = """Extracts the board state from a chessboard image and returns the best move for black (in algebraic notation)."""
|
|
|
|
| 402 |
inputs = {
|
| 403 |
"file_path": {
|
| 404 |
"type": "string",
|
| 405 |
+
"description": "Path to the image file of the chess board.",
|
| 406 |
}
|
| 407 |
}
|
| 408 |
output_type = "string"
|
|
|
|
| 427 |
return f"❌ Chess analysis failed: {e}"
|
| 428 |
|
| 429 |
|
|
|
|
| 430 |
class ExecutePythonCodeTool(Tool):
|
| 431 |
name = "execute_python_code"
|
| 432 |
description = """Executes a provided Python code snippet in a controlled, sandboxed environment.
|
|
|
|
| 435 |
inputs = {
|
| 436 |
"code": {
|
| 437 |
"type": "string",
|
| 438 |
+
"description": "A valid Python code block that needs to be executed. It should be a string containing executable Python code.",
|
| 439 |
}
|
| 440 |
}
|
| 441 |
output_type = "string"
|
|
|
|
| 480 |
exec(code, restricted_globals, exec_locals)
|
| 481 |
|
| 482 |
# If the code produces a result, we return that as output
|
| 483 |
+
if "result" in exec_locals:
|
| 484 |
+
return str(exec_locals["result"])
|
| 485 |
else:
|
| 486 |
return "❌ The code did not produce a result."
|
| 487 |
|
|
|
|
| 489 |
return f"❌ Error executing code: {str(e)}"
|
| 490 |
|
| 491 |
|
|
|
|
| 492 |
class ArxivSearchTool(Tool):
|
| 493 |
name = "arxiv_search"
|
| 494 |
description = """Searches arXiv for academic papers and returns structured information including titles, authors, publication dates, and abstracts. Ideal for finding scientific research on specific topics."""
|
|
|
|
| 496 |
inputs = {
|
| 497 |
"query": {
|
| 498 |
"type": "string",
|
| 499 |
+
"description": "A research-related query string (e.g., 'Superstring Cosmology')",
|
| 500 |
}
|
| 501 |
}
|
| 502 |
output_type = "string"
|
|
|
|
| 506 |
|
| 507 |
try:
|
| 508 |
search_docs = ArxivLoader(
|
| 509 |
+
query=query, load_max_docs=max_results, load_all_available_meta=True
|
|
|
|
|
|
|
| 510 |
).load()
|
| 511 |
except Exception as e:
|
| 512 |
return f"❌ Arxiv search failed: {e}"
|
|
|
|
| 531 |
# output_lines.append(f"Journal Ref : {meta.get('journal_ref', '[N/A]')}")
|
| 532 |
# output_lines.append(f"Primary Cat. : {meta.get('primary_category', '[N/A]')}")
|
| 533 |
# output_lines.append(f"Categories : {', '.join(meta.get('categories', [])) or '[N/A]'}")
|
| 534 |
+
output_lines.append(
|
| 535 |
+
f"Links : {', '.join(meta.get('links', [])) or '[N/A]'}"
|
| 536 |
+
)
|
| 537 |
|
| 538 |
if content:
|
| 539 |
preview = content[:30] + ("..." if len(content) > 30 else "")
|
|
|
|
| 541 |
|
| 542 |
output_lines.append("") # spacing between results
|
| 543 |
|
| 544 |
+
return "\n".join(output_lines).strip()
|
utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import re
|
| 2 |
|
|
|
|
| 3 |
def extract_final_answer(output: str) -> str:
|
| 4 |
"""
|
| 5 |
Extracts the text after 'FINAL ANSWER:' in the model's output.
|
|
@@ -9,27 +10,27 @@ def extract_final_answer(output: str) -> str:
|
|
| 9 |
output = str(output)
|
| 10 |
marker = "FINAL ANSWER:"
|
| 11 |
lower_output = output.lower()
|
| 12 |
-
|
| 13 |
if marker.lower() in lower_output:
|
| 14 |
# Find actual case version in original output (for safety)
|
| 15 |
idx = lower_output.rfind(marker.lower())
|
| 16 |
-
raw_answer = output[idx + len(marker):].strip()
|
| 17 |
-
|
| 18 |
# Normalize comma-separated lists: ensure single space after commas
|
| 19 |
-
cleaned_answer = re.sub(r
|
| 20 |
return cleaned_answer
|
| 21 |
-
|
| 22 |
return output
|
| 23 |
|
| 24 |
|
| 25 |
def replace_tool_mentions(prompt: str) -> str:
|
| 26 |
# Replace tool mentions in backticks: `search` -> `web_search`, `wiki` -> `wikipedia_search`
|
| 27 |
-
prompt = re.sub(r
|
| 28 |
-
prompt = re.sub(r
|
| 29 |
|
| 30 |
# Replace function calls: search(...) -> web_search(...), wiki(...) -> wikipedia_search(...)
|
| 31 |
# This ensures we only catch function calls (not words like arxiv_search)
|
| 32 |
-
prompt = re.sub(r
|
| 33 |
-
prompt = re.sub(r
|
| 34 |
|
| 35 |
-
return prompt
|
|
|
|
| 1 |
import re
|
| 2 |
|
| 3 |
+
|
| 4 |
def extract_final_answer(output: str) -> str:
|
| 5 |
"""
|
| 6 |
Extracts the text after 'FINAL ANSWER:' in the model's output.
|
|
|
|
| 10 |
output = str(output)
|
| 11 |
marker = "FINAL ANSWER:"
|
| 12 |
lower_output = output.lower()
|
| 13 |
+
|
| 14 |
if marker.lower() in lower_output:
|
| 15 |
# Find actual case version in original output (for safety)
|
| 16 |
idx = lower_output.rfind(marker.lower())
|
| 17 |
+
raw_answer = output[idx + len(marker) :].strip()
|
| 18 |
+
|
| 19 |
# Normalize comma-separated lists: ensure single space after commas
|
| 20 |
+
cleaned_answer = re.sub(r",\s*", ", ", raw_answer)
|
| 21 |
return cleaned_answer
|
| 22 |
+
|
| 23 |
return output
|
| 24 |
|
| 25 |
|
| 26 |
def replace_tool_mentions(prompt: str) -> str:
|
| 27 |
# Replace tool mentions in backticks: `search` -> `web_search`, `wiki` -> `wikipedia_search`
|
| 28 |
+
prompt = re.sub(r"(?<!\w)`search`(?!\w)", "`web_search`", prompt)
|
| 29 |
+
prompt = re.sub(r"(?<!\w)`wiki`(?!\w)", "`wikipedia_search`", prompt)
|
| 30 |
|
| 31 |
# Replace function calls: search(...) -> web_search(...), wiki(...) -> wikipedia_search(...)
|
| 32 |
# This ensures we only catch function calls (not words like arxiv_search)
|
| 33 |
+
prompt = re.sub(r"(?<!\w)(?<!_)search\(", "web_search(", prompt)
|
| 34 |
+
prompt = re.sub(r"(?<!\w)(?<!_)wiki\(", "wikipedia_search(", prompt)
|
| 35 |
|
| 36 |
+
return prompt
|