cgoncalves commited on
Commit
d303e2f
·
1 Parent(s): 7a5d389

Add new tools and functionalities for audio transcription, code execution, document handling, image processing, and mathematical operations

Browse files

- Updated requirements.txt to include new dependencies for langchain and various tools.
- Created system_prompt.txt to define assistant behavior and response format.
- Implemented audiotools for audio transcription using OpenAI Whisper.
- Developed codetools for executing code in multiple programming languages with safety measures.
- Added documenttools for file handling, including reading, writing, and downloading files.
- Introduced imagetools for image analysis, transformation, and drawing functionalities.
- Created mathtools for basic arithmetic operations.
- Implemented searchtools for querying Wikipedia, Arxiv, and YouTube transcripts.

.env.example ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LangChain and Agent API Keys - Copy this file to .env and fill in your keys
2
+
3
+ # Google API Key (if using Google-based models like Gemini directly)
4
+ GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
5
+
6
+ # Tavily API Key (for Tavily search tool)
7
+ TAVILY_API_KEY="YOUR_TAVILY_API_KEY"
8
+
9
+ # OpenAI API Key (often used for various models or services, e.g., embeddings, other LLMs)
10
+ # OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
11
+
.gitignore ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### Example user template template
2
+ ### Example user template
3
+
4
+ # IntelliJ project files
5
+ .idea
6
+ *.iml
7
+ out
8
+ gen
9
+ ### Python template
10
+ # Byte-compiled / optimized / DLL files
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+
15
+ # C extensions
16
+ *.so
17
+
18
+ # Distribution / packaging
19
+ .Python
20
+ build/
21
+ develop-eggs/
22
+ dist/
23
+ downloads/
24
+ eggs/
25
+ .eggs/
26
+ lib/
27
+ lib64/
28
+ parts/
29
+ sdist/
30
+ var/
31
+ wheels/
32
+ share/python-wheels/
33
+ *.egg-info/
34
+ .installed.cfg
35
+ *.egg
36
+ MANIFEST
37
+
38
+ # PyInstaller
39
+ # Usually these files are written by a python script from a template
40
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
41
+ *.manifest
42
+ *.spec
43
+
44
+ # Installer logs
45
+ pip-log.txt
46
+ pip-delete-this-directory.txt
47
+
48
+ # Unit test / coverage reports
49
+ htmlcov/
50
+ .tox/
51
+ .nox/
52
+ .coverage
53
+ .coverage.*
54
+ .cache
55
+ nosetests.xml
56
+ coverage.xml
57
+ *.cover
58
+ *.py,cover
59
+ .hypothesis/
60
+ .pytest_cache/
61
+ cover/
62
+
63
+ # Translations
64
+ *.mo
65
+ *.pot
66
+
67
+ # Django stuff:
68
+ *.log
69
+ local_settings.py
70
+ db.sqlite3
71
+ db.sqlite3-journal
72
+
73
+ # Flask stuff:
74
+ instance/
75
+ .webassets-cache
76
+
77
+ # Scrapy stuff:
78
+ .scrapy
79
+
80
+ # Sphinx documentation
81
+ docs/_build/
82
+
83
+ # PyBuilder
84
+ .pybuilder/
85
+ target/
86
+
87
+ # Jupyter Notebook
88
+ .ipynb_checkpoints
89
+
90
+ # IPython
91
+ profile_default/
92
+ ipython_config.py
93
+
94
+ # pyenv
95
+ # For a library or package, you might want to ignore these files since the code is
96
+ # intended to run in multiple environments; otherwise, check them in:
97
+ # .python-version
98
+
99
+ # pipenv
100
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
101
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
102
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
103
+ # install all needed dependencies.
104
+ #Pipfile.lock
105
+
106
+ # poetry
107
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111
+ #poetry.lock
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ #pdm.lock
116
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
117
+ # in version control.
118
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
119
+ .pdm.toml
120
+ .pdm-python
121
+ .pdm-build/
122
+
123
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
124
+ __pypackages__/
125
+
126
+ # Celery stuff
127
+ celerybeat-schedule
128
+ celerybeat.pid
129
+
130
+ # SageMath parsed files
131
+ *.sage.py
132
+
133
+ # Environments
134
+ .env
135
+ .venv
136
+ env/
137
+ venv/
138
+ ENV/
139
+ env.bak/
140
+ venv.bak/
141
+
142
+ # Spyder project settings
143
+ .spyderproject
144
+ .spyproject
145
+
146
+ # Rope project settings
147
+ .ropeproject
148
+
149
+ # mkdocs documentation
150
+ /site
151
+
152
+ # mypy
153
+ .mypy_cache/
154
+ .dmypy.json
155
+ dmypy.json
156
+
157
+ # Pyre type checker
158
+ .pyre/
159
+
160
+ # pytype static type analyzer
161
+ .pytype/
162
+
163
+ # Cython debug symbols
164
+ cython_debug/
165
+
166
+ # PyCharm
167
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
168
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
169
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
170
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
171
+ #.idea/
172
+
README.md CHANGED
@@ -12,4 +12,4 @@ hf_oauth: true
12
  hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
12
  hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
+ Check out the configuration reference at <https://huggingface.co/docs/hub/spaces-config-reference>
agents/__init__.py ADDED
File without changes
agents/agent.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ from langgraph.graph import START, StateGraph, MessagesState
3
+ from langgraph.prebuilt import tools_condition
4
+ from langgraph.prebuilt import ToolNode
5
+ from langchain_core.messages import SystemMessage
6
+ from tools.searchtools import wiki_search, web_search, arxiv_search, get_youtube_transcript
7
+ from tools.mathtools import multiply, add, subtract, divide, modulus, power, square_root
8
+ from tools.codetools import execute_code_multilang
9
+ from tools.documenttools import create_file_with_content, read_file_content, download_file_from_url, extract_text_from_image, analyze_csv_file, analyze_excel_file
10
+ from tools.imagetools import analyze_image, transform_image, draw_on_image, generate_simple_image, combine_images
11
+ from tools.audiotools import transcribe_audio
12
+ from langchain_google_genai import ChatGoogleGenerativeAI
13
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
14
+
15
+ load_dotenv()
16
+
17
+ # load the system prompt from the file
18
+ with open("system_prompt.txt", "r", encoding="utf-8") as f:
19
+ system_prompt = f.read()
20
+
21
+ # System message
22
+ sys_msg = SystemMessage(content=system_prompt)
23
+
24
+
25
+ tools = [
26
+ web_search,
27
+ wiki_search,
28
+ arxiv_search,
29
+ get_youtube_transcript,
30
+ multiply,
31
+ add,
32
+ subtract,
33
+ divide,
34
+ modulus,
35
+ power,
36
+ square_root,
37
+ create_file_with_content,
38
+ read_file_content,
39
+ download_file_from_url,
40
+ extract_text_from_image,
41
+ analyze_csv_file,
42
+ analyze_excel_file,
43
+ execute_code_multilang,
44
+ analyze_image,
45
+ transform_image,
46
+ draw_on_image,
47
+ generate_simple_image,
48
+ combine_images,
49
+ transcribe_audio,
50
+ ]
51
+
52
+
53
+ # Build graph function
54
+ def build_graph():
55
+ """Build the graph"""
56
+ # Load environment variables from .env file
57
+ llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0)
58
+
59
+ # Bind tools to LLM
60
+ llm_with_tools = llm.bind_tools(tools)
61
+
62
+ # Node
63
+ def assistant(state: MessagesState):
64
+ """Assistant node"""
65
+ # Prepend system message to the current messages
66
+ # Ensure sys_msg is only added if not already present or if it's the first turn
67
+ current_messages = state["messages"]
68
+ if not current_messages or current_messages[0].type != "system":
69
+ # Or, if you want to ensure it's always the first message for each LLM call in this node:
70
+ # updated_messages = [sys_msg] + [m for m in current_messages if m.type != "system"]
71
+ # For simplicity, let's assume we add it if it's not the very first message overall.
72
+ # A more robust check might be needed depending on multi-turn conversation flow.
73
+ updated_messages = [sys_msg] + current_messages
74
+ else:
75
+ updated_messages = current_messages
76
+ return {"messages": [llm_with_tools.invoke(updated_messages)]}
77
+
78
+ builder = StateGraph(MessagesState)
79
+ builder.add_node("assistant", assistant)
80
+ builder.add_node("tools", ToolNode(tools))
81
+ builder.add_edge(START, "assistant")
82
+ builder.add_conditional_edges(
83
+ "assistant",
84
+ tools_condition,
85
+ )
86
+ builder.add_edge("tools", "assistant")
87
+
88
+ # Compile graph
89
+ return builder.compile()
api_integration.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from typing import List, Dict, Any
3
+
4
+ class GAIAApiClient:
5
+ def __init__(self, api_url="https://agents-course-unit4-scoring.hf.space"):
6
+ self.api_url = api_url
7
+ self.questions_url = f"{api_url}/questions"
8
+ self.submit_url = f"{api_url}/submit"
9
+ self.files_url = f"{api_url}/files"
10
+
11
+ def get_questions(self) -> List[Dict[str, Any]]:
12
+ """Fetch all evaluation questions"""
13
+ response = requests.get(self.questions_url)
14
+ response.raise_for_status()
15
+ return response.json()
16
+
17
+ def get_random_question(self) -> Dict[str, Any]:
18
+ """Fetch a single random question"""
19
+ response = requests.get(f"{self.api_url}/random-question")
20
+ response.raise_for_status()
21
+ return response.json()
22
+
23
+ def get_file(self, task_id: str) -> bytes:
24
+ """Download a file for a specific task"""
25
+ response = requests.get(f"{self.files_url}/{task_id}")
26
+ response.raise_for_status()
27
+ return response.content
28
+
29
+ def submit_answers(self, username: str, agent_code: str, answers: List[Dict[str, Any]]) -> Dict[str, Any]:
30
+ """Submit agent answers and get score"""
31
+ data = {
32
+ "username": username,
33
+ "agent_code": agent_code,
34
+ "answers": answers
35
+ }
36
+ response = requests.post(self.submit_url, json=data)
37
+ response.raise_for_status()
38
+ return response.json()
app.py CHANGED
@@ -1,37 +1,63 @@
 
1
  import os
 
2
  import gradio as gr
3
  import requests
4
- import inspect
5
  import pandas as pd
6
- from smolagents import CodeAgent, DuckDuckGoSearchTool, OpenAIServerModel
 
 
 
 
 
 
 
7
  # (Keep Constants as is)
8
  # --- Constants ---
9
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
10
 
11
-
12
  # --- Basic Agent Definition ---
13
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
 
 
14
  class BasicAgent:
15
- def __init__(self, openai_key):
16
- self.openai_key = openai_key
17
  print("BasicAgent initialized.")
18
- # Initialize the model
19
- #model = HfApiModel()
20
- model = OpenAIServerModel(model_id="gpt-4.1", api_key=self.openai_key)
21
- # Initialize the search tool
22
- search_tool = DuckDuckGoSearchTool()
23
- # Initialize Agent
24
- self.agent = CodeAgent(
25
- model = model,
26
- tools=[search_tool]
27
- )
28
- def __call__(self, question: str) -> str:
29
- print(f"Agent received question (first 50 chars): {question[:50]}...")
30
- fixed_answer =self.agent.run(question)
31
- print(f"Agent returning fixed answer: {fixed_answer}")
32
- return fixed_answer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- def run_and_submit_all(profile: gr.OAuthProfile | None, openai_key: str):
 
35
  """
36
  Fetches all questions, runs the BasicAgent on them, submits all answers,
37
  and displays the results.
@@ -47,12 +73,13 @@ def run_and_submit_all(profile: gr.OAuthProfile | None, openai_key: str):
47
  return "Please Login to Hugging Face with the button.", None
48
 
49
  api_url = DEFAULT_API_URL
50
- questions_url = f"{api_url}/questions"
51
  submit_url = f"{api_url}/submit"
52
 
 
 
53
  # 1. Instantiate Agent ( modify this part to create your agent)
54
  try:
55
- agent = BasicAgent(openai_key)
56
  except Exception as e:
57
  print(f"Error instantiating agent: {e}")
58
  return f"Error initializing agent: {e}", None
@@ -61,26 +88,21 @@ def run_and_submit_all(profile: gr.OAuthProfile | None, openai_key: str):
61
  print(agent_code)
62
 
63
  # 2. Fetch Questions
64
- print(f"Fetching questions from: {questions_url}")
65
  try:
66
- response = requests.get(questions_url, timeout=15)
67
- response.raise_for_status()
68
- questions_data = response.json()
69
  if not questions_data:
70
- print("Fetched questions list is empty.")
71
- return "Fetched questions list is empty or invalid format.", None
72
  print(f"Fetched {len(questions_data)} questions.")
73
  except requests.exceptions.RequestException as e:
74
- print(f"Error fetching questions: {e}")
75
  return f"Error fetching questions: {e}", None
76
- except requests.exceptions.JSONDecodeError as e:
77
- print(f"Error decoding JSON response from questions endpoint: {e}")
78
- print(f"Response text: {response.text[:500]}")
79
- return f"Error decoding server response for questions: {e}", None
80
  except Exception as e:
81
- print(f"An unexpected error occurred fetching questions: {e}")
82
  return f"An unexpected error occurred fetching questions: {e}", None
83
 
 
84
  # 3. Run your Agent
85
  results_log = []
86
  answers_payload = []
@@ -88,16 +110,79 @@ def run_and_submit_all(profile: gr.OAuthProfile | None, openai_key: str):
88
  for item in questions_data:
89
  task_id = item.get("task_id")
90
  question_text = item.get("question")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  if not task_id or question_text is None:
92
  print(f"Skipping item with missing task_id or question: {item}")
93
  continue
 
94
  try:
95
- submitted_answer = agent(question_text)
 
 
96
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
97
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
98
  except Exception as e:
99
  print(f"Error running agent on task {task_id}: {e}")
100
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
101
 
102
  if not answers_payload:
103
  print("Agent did not produce any answers to submit.")
@@ -160,27 +245,24 @@ with gr.Blocks() as demo:
160
  **Instructions:**
161
  1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
162
  2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
163
- 3. Enter your OpenAI key below (if required by your agent).
164
- 4. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
165
  ---
166
  **Disclaimers:**
167
- Once clicking on the "submit" button, it can take quite some time (this is the time for the agent to go through all the questions).
168
- This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance, for the delay process of the submit button, a solution could be to cache the answers and submit in a separate action or even to answer the questions in async.
169
  """
170
  )
171
 
172
  gr.LoginButton()
173
 
174
- openai_key_box = gr.Textbox(label="OpenAI API Key", type="password", placeholder="sk-...", lines=1)
175
-
176
  run_button = gr.Button("Run Evaluation & Submit All Answers")
177
 
178
  status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
 
179
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
180
 
181
  run_button.click(
182
  fn=run_and_submit_all,
183
- inputs=[openai_key_box],
184
  outputs=[status_output, results_table]
185
  )
186
 
 
1
+ """ Basic Agent Evaluation Runner"""
2
  import os
3
+ import inspect
4
  import gradio as gr
5
  import requests
 
6
  import pandas as pd
7
+ from langchain_core.messages import HumanMessage
8
+ from agents.agent import build_graph
9
+ from api_integration import GAIAApiClient
10
+ import tempfile
11
+ import mimetypes # Added for MIME type detection
12
+ import base64 # Added for base64 encoding images
13
+
14
+
15
  # (Keep Constants as is)
16
  # --- Constants ---
17
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
18
 
 
19
  # --- Basic Agent Definition ---
20
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
21
+
22
+
23
  class BasicAgent:
24
+ """A langgraph agent."""
25
+ def __init__(self):
26
  print("BasicAgent initialized.")
27
+ self.graph = build_graph()
28
+
29
+ def __call__(self, messages: list) -> str: # Modified to accept a list of messages
30
+ print(f"Agent received messages: {messages}")
31
+ # Ensure messages are in the correct format for the graph
32
+ processed_messages = self.graph.invoke({"messages": messages})
33
+ # The final answer should be in the 'content' of the last message
34
+ raw_answer = processed_messages['messages'][-1].content
35
+
36
+ # Attempt to find "FINAL ANSWER:" and extract text after it
37
+ final_answer_marker = "FINAL ANSWER:"
38
+ marker_index = raw_answer.rfind(final_answer_marker) # Use rfind to get the last occurrence
39
+
40
+ if marker_index != -1:
41
+ # Extract the text after "FINAL ANSWER: "
42
+ extracted_answer = raw_answer[marker_index + len(final_answer_marker):].strip()
43
+ # If there's a newline after the extracted answer, take only the first line
44
+ # This handles cases where the LLM might add extra explanations after the marker on a new line
45
+ first_line_of_extracted_answer = extracted_answer.split('\\n')[0].strip()
46
+ if first_line_of_extracted_answer: # Ensure it's not empty after stripping
47
+ print(f"Extracted answer: {first_line_of_extracted_answer}")
48
+ return first_line_of_extracted_answer
49
+ else: # If the first line is empty, it might be that the answer is just the marker itself (unlikely but handle)
50
+ print(f"Warning: Extracted answer after '{final_answer_marker}' is empty. Returning raw answer part after marker if any, or full raw answer.")
51
+ # Fallback to extracted_answer if first_line was empty but extracted_answer was not
52
+ return extracted_answer if extracted_answer else raw_answer
53
+
54
+
55
+ # Fallback if "FINAL ANSWER:" is not found or extraction results in empty string
56
+ print(f"Warning: '{final_answer_marker}' not found in agent's output or extraction failed. Returning raw answer: {raw_answer}")
57
+ return raw_answer
58
 
59
+
60
+ def run_and_submit_all( profile: gr.OAuthProfile | None):
61
  """
62
  Fetches all questions, runs the BasicAgent on them, submits all answers,
63
  and displays the results.
 
73
  return "Please Login to Hugging Face with the button.", None
74
 
75
  api_url = DEFAULT_API_URL
 
76
  submit_url = f"{api_url}/submit"
77
 
78
+ gaia_client = GAIAApiClient(api_url=api_url)
79
+
80
  # 1. Instantiate Agent ( modify this part to create your agent)
81
  try:
82
+ agent = BasicAgent()
83
  except Exception as e:
84
  print(f"Error instantiating agent: {e}")
85
  return f"Error initializing agent: {e}", None
 
88
  print(agent_code)
89
 
90
  # 2. Fetch Questions
91
+ print(f"Fetching questions using GAIAApiClient from: {api_url}")
92
  try:
93
+ questions_data = gaia_client.get_questions()
 
 
94
  if not questions_data:
95
+ print("Fetched questions list is empty.")
96
+ return "Fetched questions list is empty or invalid format.", None
97
  print(f"Fetched {len(questions_data)} questions.")
98
  except requests.exceptions.RequestException as e:
99
+ print(f"Error fetching questions via GAIAApiClient: {e}")
100
  return f"Error fetching questions: {e}", None
 
 
 
 
101
  except Exception as e:
102
+ print(f"An unexpected error occurred fetching questions via GAIAApiClient: {e}")
103
  return f"An unexpected error occurred fetching questions: {e}", None
104
 
105
+
106
  # 3. Run your Agent
107
  results_log = []
108
  answers_payload = []
 
110
  for item in questions_data:
111
  task_id = item.get("task_id")
112
  question_text = item.get("question")
113
+ original_file_name = item.get("file_name")
114
+
115
+ content_parts = [{"type": "text", "text": question_text}]
116
+ downloaded_file_path_for_log = None # For logging purposes
117
+
118
+ if task_id and original_file_name:
119
+ print(f"Question {task_id} has an associated file: {original_file_name}. Attempting to download.")
120
+ try:
121
+ file_bytes = gaia_client.get_file(task_id)
122
+ if file_bytes:
123
+ temp_dir = tempfile.gettempdir()
124
+ safe_original_filename = "".join(c if c.isalnum() or c in ('.', '_', '-') else '_' for c in original_file_name)
125
+ temp_file_name = f"task_{task_id}_{safe_original_filename}"
126
+ downloaded_file_path = os.path.join(temp_dir, temp_file_name)
127
+ downloaded_file_path_for_log = downloaded_file_path
128
+
129
+ with open(downloaded_file_path, "wb") as f_out:
130
+ f_out.write(file_bytes)
131
+ print(f"File for task {task_id} downloaded to: {downloaded_file_path}")
132
+
133
+ # Determine MIME type and construct message part
134
+ mime_type, _ = mimetypes.guess_type(downloaded_file_path)
135
+ if mime_type and mime_type.startswith("image/"):
136
+ base64_image = base64.b64encode(file_bytes).decode('utf-8')
137
+ content_parts.append({
138
+ "type": "image_url",
139
+ "image_url": {
140
+ "url": f"data:{mime_type};base64,{base64_image}"
141
+ }
142
+ })
143
+ current_question_for_log = f"{question_text}\n\n[System Note: Image file {original_file_name} ({mime_type}) was processed and included directly in the message.]"
144
+ # elif mime_type and mime_type.startswith("audio/"):
145
+ # # For audio, tools might expect a path or raw bytes.
146
+ # # For now, let's add a note with the path, assuming tools can handle it.
147
+ # # This part might need adjustment based on specific audio tool capabilities.
148
+ # content_parts.append({
149
+ # "type": "text", # Or a custom type if LangGraph/tools support it
150
+ # "text": f"[System Note: An audio file '{original_file_name}' is available at: {downloaded_file_path}]"
151
+ # })
152
+ # current_question_for_log = f"{question_text}\n\n[System Note: Audio file {original_file_name} available at {downloaded_file_path}]"
153
+ else: # For other file types (text, csv, py, etc.)
154
+ # Add a system note with the file path. Tools will need to be able
155
+ # to read the file from this path.
156
+ content_parts.append({
157
+ "type": "text",
158
+ "text": f"[System Note: An associated file '{original_file_name}' ({mime_type if mime_type else 'unknown type'}) has been downloaded. It is available at: {downloaded_file_path}]"
159
+ })
160
+ current_question_for_log = f"{question_text}\n\n[System Note: File {original_file_name} ({mime_type if mime_type else 'unknown type'}) available at {downloaded_file_path}]"
161
+
162
+ else:
163
+ print(f"Warning: File indicated for task {task_id} ('{original_file_name}'), but download returned no content.")
164
+ content_parts.append({"type": "text", "text": f"[System Note: A file ('{original_file_name}') was indicated for this question, but the download attempt returned no content.]"})
165
+ current_question_for_log = f"{question_text}\n\n[System Note: File {original_file_name} download returned no content.]"
166
+ except Exception as e_file:
167
+ print(f"Error downloading or processing file '{original_file_name}' for task {task_id}: {e_file}")
168
+ content_parts.append({"type": "text", "text": f"[System Note: An error occurred while trying to download/process the associated file ('{original_file_name}') for this question: {e_file}]"})
169
+ current_question_for_log = f"{question_text}\n\n[System Note: Error with file {original_file_name}: {e_file}]"
170
+ else:
171
+ current_question_for_log = question_text # No file associated
172
+
173
  if not task_id or question_text is None:
174
  print(f"Skipping item with missing task_id or question: {item}")
175
  continue
176
+
177
  try:
178
+ # The agent now expects a list of content parts
179
+ human_message = HumanMessage(content=content_parts)
180
+ submitted_answer = agent([human_message]) # Pass as a list of messages
181
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
182
+ results_log.append({"Task ID": task_id, "Question": current_question_for_log, "File Path": downloaded_file_path_for_log if downloaded_file_path_for_log else "N/A", "Submitted Answer": submitted_answer})
183
  except Exception as e:
184
  print(f"Error running agent on task {task_id}: {e}")
185
+ results_log.append({"Task ID": task_id, "Question": current_question_for_log, "File Path": downloaded_file_path_for_log if downloaded_file_path_for_log else "N/A", "Submitted Answer": f"AGENT ERROR: {e}"})
186
 
187
  if not answers_payload:
188
  print("Agent did not produce any answers to submit.")
 
245
  **Instructions:**
246
  1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
247
  2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
248
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
 
249
  ---
250
  **Disclaimers:**
251
+ Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
252
+ This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
253
  """
254
  )
255
 
256
  gr.LoginButton()
257
 
 
 
258
  run_button = gr.Button("Run Evaluation & Submit All Answers")
259
 
260
  status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
261
+ # Removed max_rows=10 from DataFrame constructor
262
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
263
 
264
  run_button.click(
265
  fn=run_and_submit_all,
 
266
  outputs=[status_output, results_table]
267
  )
268
 
requirements.txt CHANGED
@@ -1,4 +1,24 @@
1
  gradio
 
2
  requests
3
- smolagents
4
- smolagents[openai]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  gradio
2
+ gradio[oauth]
3
  requests
4
+ langchain
5
+ langchain-community
6
+ langchain-core
7
+ langchain-google-genai
8
+ langchain-huggingface
9
+ langchain-groq
10
+ langchain-tavily
11
+ langchain-chroma
12
+ langgraph
13
+ huggingface_hub
14
+ arxiv
15
+ pymupdf
16
+ wikipedia
17
+ pgvector
18
+ python-dotenv
19
+ pytesseract
20
+ matplotlib
21
+ openai-whisper
22
+ openpyxl
23
+ youtube-transcript-api
24
+ pytube
system_prompt.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ You are a helpful assistant tasked with answering questions using a set of tools.
2
+ Now, I will ask you a question. Report your thoughts, and finish your answer with the following template:
3
+ FINAL ANSWER: [YOUR FINAL ANSWER].
4
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, Apply the rules above for each element (number or string), ensure there is exactly one space after each comma.
5
+ Your answer should only start with "FINAL ANSWER: ", then follows with the answer.
tools/__init__.py ADDED
File without changes
tools/audiotools.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ import os
3
+ import whisper
4
+
5
+ @tool
6
+ def transcribe_audio(file_path: str) -> str:
7
+ """
8
+ Transcribes an audio file using OpenAI Whisper and returns the transcribed text.
9
+
10
+ Args:
11
+ file_path (str): Path to the audio file.
12
+ """
13
+ if not os.path.exists(file_path):
14
+ return f"Error: Audio file {file_path} not found."
15
+
16
+ try:
17
+ # Attempt transcription with Whisper
18
+ # Using "base" model for a balance of speed and accuracy.
19
+ # Other models: "tiny", "small", "medium", "large", "large-v2", "large-v3"
20
+ # Consider making the model choice configurable if needed.
21
+ model = whisper.load_model("base")
22
+ result = model.transcribe(file_path, fp16=False) # fp16=False can improve compatibility/stability on some systems
23
+ transcription = result["text"]
24
+
25
+ if transcription.strip(): # Check if transcription is not empty or just whitespace
26
+ return f"Audio transcription: {transcription}"
27
+ else:
28
+ return "Audio transcribed, but no text was detected."
29
+
30
+ except Exception as e_whisper:
31
+ # Catching a general exception, but more specific ones can be added
32
+ # (e.g., for model loading errors, unsupported file formats by Whisper)
33
+ return f"Error during audio transcription: {str(e_whisper)}"
tools/codetools.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ import os
3
+ import io
4
+ import sys
5
+ import uuid
6
+ import base64
7
+ import traceback
8
+ import contextlib
9
+ import tempfile
10
+ import subprocess
11
+ import sqlite3
12
+ from typing import Dict, List, Any, Optional, Union
13
+ import numpy as np
14
+ import pandas as pd
15
+ import matplotlib.pyplot as plt
16
+ from PIL import Image
17
+
18
+ class CodeInterpreter:
19
+ def __init__(self, allowed_modules=None, max_execution_time=30, working_directory=None):
20
+ """Initialize the code interpreter with safety measures."""
21
+ self.allowed_modules = allowed_modules or [
22
+ "numpy", "pandas", "matplotlib", "scipy", "sklearn",
23
+ "math", "random", "statistics", "datetime", "collections",
24
+ "itertools", "functools", "operator", "re", "json",
25
+ "sympy", "networkx", "nltk", "PIL", "pytesseract",
26
+ "cmath", "uuid", "tempfile", "requests", "urllib", "os", "io", "sys", "base64", "traceback", "contextlib", "sqlite3"
27
+ ]
28
+ self.max_execution_time = max_execution_time
29
+ self.working_directory = working_directory or os.path.join(os.getcwd())
30
+ if not os.path.exists(self.working_directory):
31
+ os.makedirs(self.working_directory)
32
+
33
+ self.globals = {
34
+ "__builtins__": __builtins__,
35
+ "np": np,
36
+ "pd": pd,
37
+ "plt": plt,
38
+ "Image": Image,
39
+ }
40
+ self.temp_sqlite_db = os.path.join(tempfile.gettempdir(), "code_exec.db")
41
+
42
+ def execute_code(self, code: str, language: str = "python", file_path: Optional[str] = None) -> Dict[str, Any]:
43
+ """Execute the provided code or code from a file in the selected programming language."""
44
+ language = language.lower()
45
+ execution_id = str(uuid.uuid4())
46
+
47
+ result = {
48
+ "execution_id": execution_id,
49
+ "status": "error",
50
+ "stdout": "",
51
+ "stderr": "",
52
+ "result": None,
53
+ "plots": [],
54
+ "dataframes": []
55
+ }
56
+
57
+ current_code = code
58
+ if file_path:
59
+ if not os.path.exists(file_path):
60
+ result["stderr"] = f"Error: File not found at {file_path}"
61
+ return result
62
+ if not os.path.isfile(file_path):
63
+ result["stderr"] = f"Error: Path {file_path} is not a file."
64
+ return result
65
+ try:
66
+ with open(file_path, "r", encoding='utf-8') as f:
67
+ current_code = f.read()
68
+ if not current_code.strip() and code.strip(): # If file is empty but code arg has content
69
+ # This case might be ambiguous. Prioritize file content if path is given.
70
+ # If file is truly empty, and code arg was also meant to be empty, it will proceed.
71
+ # If code arg had content and file was empty, it implies user might want to run content of code arg.
72
+ # For now, if file_path is provided, its content (even if empty) takes precedence.
73
+ # If the intention is to run `code` when `file_path` is empty, the caller should not provide `file_path`.
74
+ pass # current_code is already empty string from file
75
+ elif not current_code.strip() and not code.strip():
76
+ result["stderr"] = "Error: Both provided code string and file content are empty."
77
+ return result
78
+
79
+ except Exception as e:
80
+ result["stderr"] = f"Error reading file {file_path}: {str(e)}"
81
+ return result
82
+ elif not code.strip(): # No file_path and code string is empty
83
+ result["stderr"] = "Error: No code provided either as a string or a file path."
84
+ return result
85
+
86
+ try:
87
+ if language == "python":
88
+ return self._execute_python(current_code, execution_id)
89
+ elif language == "bash":
90
+ return self._execute_bash(current_code, execution_id)
91
+ elif language == "sql":
92
+ return self._execute_sql(current_code, execution_id)
93
+ elif language == "c":
94
+ return self._execute_c(current_code, execution_id)
95
+ elif language == "java":
96
+ return self._execute_java(current_code, execution_id)
97
+ else:
98
+ result["stderr"] = f"Unsupported language: {language}"
99
+ except Exception as e:
100
+ result["stderr"] = str(e)
101
+
102
+ return result
103
+
104
+ def _execute_python(self, code: str, execution_id: str) -> dict:
105
+ output_buffer = io.StringIO()
106
+ error_buffer = io.StringIO()
107
+ result = {
108
+ "execution_id": execution_id,
109
+ "status": "error",
110
+ "stdout": "",
111
+ "stderr": "",
112
+ "result": None,
113
+ "plots": [],
114
+ "dataframes": []
115
+ }
116
+
117
+ try:
118
+ exec_dir = os.path.join(self.working_directory, execution_id)
119
+ os.makedirs(exec_dir, exist_ok=True)
120
+ plt.switch_backend('Agg')
121
+
122
+ with contextlib.redirect_stdout(output_buffer), contextlib.redirect_stderr(error_buffer):
123
+ exec_result = exec(code, self.globals)
124
+
125
+ if plt.get_fignums():
126
+ for i, fig_num in enumerate(plt.get_fignums()):
127
+ fig = plt.figure(fig_num)
128
+ img_path = os.path.join(exec_dir, f"plot_{i}.png")
129
+ fig.savefig(img_path)
130
+ with open(img_path, "rb") as img_file:
131
+ img_data = base64.b64encode(img_file.read()).decode('utf-8')
132
+ result["plots"].append({
133
+ "figure_number": fig_num,
134
+ "data": img_data
135
+ })
136
+
137
+ for var_name, var_value in self.globals.items():
138
+ if isinstance(var_value, pd.DataFrame) and len(var_value) > 0:
139
+ result["dataframes"].append({
140
+ "name": var_name,
141
+ "head": var_value.head().to_dict(),
142
+ "shape": var_value.shape,
143
+ "dtypes": str(var_value.dtypes)
144
+ })
145
+
146
+ result["status"] = "success"
147
+ result["stdout"] = output_buffer.getvalue()
148
+ result["result"] = exec_result
149
+
150
+ except Exception as e:
151
+ result["status"] = "error"
152
+ result["stderr"] = f"{error_buffer.getvalue()}\n{traceback.format_exc()}"
153
+
154
+ return result
155
+
156
+ def _execute_bash(self, code: str, execution_id: str) -> dict:
157
+ try:
158
+ completed = subprocess.run(
159
+ code, shell=True, capture_output=True, text=True, timeout=self.max_execution_time
160
+ )
161
+ return {
162
+ "execution_id": execution_id,
163
+ "status": "success" if completed.returncode == 0 else "error",
164
+ "stdout": completed.stdout,
165
+ "stderr": completed.stderr,
166
+ "result": None,
167
+ "plots": [],
168
+ "dataframes": []
169
+ }
170
+ except subprocess.TimeoutExpired:
171
+ return {
172
+ "execution_id": execution_id,
173
+ "status": "error",
174
+ "stdout": "",
175
+ "stderr": "Execution timed out.",
176
+ "result": None,
177
+ "plots": [],
178
+ "dataframes": []
179
+ }
180
+
181
+ def _execute_sql(self, code: str, execution_id: str) -> dict:
182
+ result = {
183
+ "execution_id": execution_id,
184
+ "status": "error",
185
+ "stdout": "",
186
+ "stderr": "",
187
+ "result": None,
188
+ "plots": [],
189
+ "dataframes": []
190
+ }
191
+ try:
192
+ conn = sqlite3.connect(self.temp_sqlite_db)
193
+ cur = conn.cursor()
194
+ cur.execute(code)
195
+ if code.strip().lower().startswith("select"):
196
+ columns = [description[0] for description in cur.description]
197
+ rows = cur.fetchall()
198
+ df = pd.DataFrame(rows, columns=columns)
199
+ result["dataframes"].append({
200
+ "name": "query_result",
201
+ "head": df.head().to_dict(),
202
+ "shape": df.shape,
203
+ "dtypes": str(df.dtypes)
204
+ })
205
+ else:
206
+ conn.commit()
207
+
208
+ result["status"] = "success"
209
+ result["stdout"] = "Query executed successfully."
210
+
211
+ except Exception as e:
212
+ result["stderr"] = str(e)
213
+ finally:
214
+ conn.close()
215
+
216
+ return result
217
+
218
+ def _execute_c(self, code: str, execution_id: str) -> dict:
219
+ temp_dir = tempfile.mkdtemp()
220
+ source_path = os.path.join(temp_dir, "program.c")
221
+ binary_path = os.path.join(temp_dir, "program")
222
+
223
+ try:
224
+ with open(source_path, "w") as f:
225
+ f.write(code)
226
+
227
+ compile_proc = subprocess.run(
228
+ ["gcc", source_path, "-o", binary_path],
229
+ capture_output=True, text=True, timeout=self.max_execution_time
230
+ )
231
+ if compile_proc.returncode != 0:
232
+ return {
233
+ "execution_id": execution_id,
234
+ "status": "error",
235
+ "stdout": compile_proc.stdout,
236
+ "stderr": compile_proc.stderr,
237
+ "result": None,
238
+ "plots": [],
239
+ "dataframes": []
240
+ }
241
+
242
+ run_proc = subprocess.run(
243
+ [binary_path],
244
+ capture_output=True, text=True, timeout=self.max_execution_time
245
+ )
246
+ return {
247
+ "execution_id": execution_id,
248
+ "status": "success" if run_proc.returncode == 0 else "error",
249
+ "stdout": run_proc.stdout,
250
+ "stderr": run_proc.stderr,
251
+ "result": None,
252
+ "plots": [],
253
+ "dataframes": []
254
+ }
255
+ except Exception as e:
256
+ return {
257
+ "execution_id": execution_id,
258
+ "status": "error",
259
+ "stdout": "",
260
+ "stderr": str(e),
261
+ "result": None,
262
+ "plots": [],
263
+ "dataframes": []
264
+ }
265
+
266
+ def _execute_java(self, code: str, execution_id: str) -> dict:
267
+ temp_dir = tempfile.mkdtemp()
268
+ source_path = os.path.join(temp_dir, "Main.java")
269
+
270
+ try:
271
+ with open(source_path, "w") as f:
272
+ f.write(code)
273
+
274
+ compile_proc = subprocess.run(
275
+ ["javac", source_path],
276
+ capture_output=True, text=True, timeout=self.max_execution_time
277
+ )
278
+ if compile_proc.returncode != 0:
279
+ return {
280
+ "execution_id": execution_id,
281
+ "status": "error",
282
+ "stdout": compile_proc.stdout,
283
+ "stderr": compile_proc.stderr,
284
+ "result": None,
285
+ "plots": [],
286
+ "dataframes": []
287
+ }
288
+
289
+ run_proc = subprocess.run(
290
+ ["java", "-cp", temp_dir, "Main"],
291
+ capture_output=True, text=True, timeout=self.max_execution_time
292
+ )
293
+ return {
294
+ "execution_id": execution_id,
295
+ "status": "success" if run_proc.returncode == 0 else "error",
296
+ "stdout": run_proc.stdout,
297
+ "stderr": run_proc.stderr,
298
+ "result": None,
299
+ "plots": [],
300
+ "dataframes": []
301
+ }
302
+ except Exception as e:
303
+ return {
304
+ "execution_id": execution_id,
305
+ "status": "error",
306
+ "stdout": "",
307
+ "stderr": str(e),
308
+ "result": None,
309
+ "plots": [],
310
+ "dataframes": []
311
+ }
312
+
313
+
314
+ interpreter_instance = CodeInterpreter()
315
+
316
+ @tool
317
+ def execute_code_multilang(code: str, language: str = "python", file_path: Optional[str] = None) -> Dict[str, Any]:
318
+ """
319
+ Executes code in various languages (Python, Bash, SQL, C, Java) using a sandboxed interpreter.
320
+ Can execute code provided as a string or from a specified file path.
321
+ If file_path is provided, the content of the file will be executed.
322
+ If both code string and file_path are provided, the content of the file at file_path takes precedence.
323
+
324
+ Args:
325
+ code (str): The code string to execute. Ignored if file_path is provided and valid.
326
+ language (str, optional): The programming language. Defaults to "python".
327
+ Supported: "python", "bash", "sql", "c", "java".
328
+ file_path (Optional[str], optional): Absolute path to a file containing the code to execute.
329
+ If provided, its content overrides the 'code' argument.
330
+
331
+ Returns:
332
+ Dict[str, Any]: A dictionary containing execution results, including status, stdout, stderr,
333
+ plots (for Python), and dataframes (for Python and SQL).
334
+ """
335
+ interpreter = CodeInterpreter()
336
+ return interpreter.execute_code(code=code, language=language, file_path=file_path)
tools/documenttools.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ from typing import List, Dict, Any, Optional
3
+ import tempfile
4
+ from urllib.parse import urlparse
5
+ import os
6
+ import uuid
7
+ import requests
8
+ from PIL import Image
9
+ import pytesseract
10
+ import pandas as pd
11
+
12
+ @tool
13
+ def create_file_with_content(content: str, filename: Optional[str] = None) -> str:
14
+ """
15
+ Save content to a new file in a temporary directory and return the absolute file path.
16
+ Args:
17
+ content (str): The content to save to the file.
18
+ filename (str, optional): The desired name of the file. If not provided, a random unique name will be generated.
19
+ """
20
+ temp_dir = tempfile.gettempdir()
21
+ if filename is None:
22
+ # Generate a unique filename to avoid collisions if no name is provided
23
+ filename = f"file_{uuid.uuid4().hex[:8]}.txt" # Default to .txt if no extension in name
24
+
25
+ filepath = os.path.join(temp_dir, filename)
26
+
27
+ try:
28
+ with open(filepath, "w", encoding='utf-8') as f:
29
+ f.write(content)
30
+ return filepath
31
+ except Exception as e:
32
+ return f"Error creating file {filepath}: {str(e)}"
33
+
34
+
35
+ @tool
36
+ def read_file_content(file_path: str) -> str:
37
+ """
38
+ Read the content of a specified file and return it as a string.
39
+ Args:
40
+ file_path (str): The absolute path to the file to be read.
41
+ """
42
+ if not os.path.exists(file_path):
43
+ return f"Error: File not found at {file_path}"
44
+ if not os.path.isfile(file_path):
45
+ return f"Error: Path {file_path} is not a file."
46
+
47
+ try:
48
+ with open(file_path, "r", encoding='utf-8') as f:
49
+ content = f.read()
50
+ return content
51
+ except Exception as e:
52
+ return f"Error reading file {file_path}: {str(e)}"
53
+
54
+
55
+ @tool
56
+ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
57
+ """
58
+ Download a file from a URL and save it to a temporary location.
59
+ Args:
60
+ url (str): the URL of the file to download.
61
+ filename (str, optional): the name of the file. If not provided, a random name file will be created.
62
+ """
63
+ try:
64
+ print(f"Attempting to download file from {url}")
65
+
66
+ # Parse URL to get filename if not provided
67
+ if not filename:
68
+ path = urlparse(url).path
69
+ filename = os.path.basename(path)
70
+ if not filename:
71
+ filename = f"downloaded_{uuid.uuid4().hex[:8]}"
72
+
73
+ print(f"Will save as {filename}")
74
+
75
+ # Create temporary file
76
+ temp_dir = tempfile.gettempdir()
77
+ filepath = os.path.join(temp_dir, filename)
78
+
79
+ # Download the file with timeout and proper headers
80
+ headers = {
81
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
82
+ }
83
+
84
+ response = requests.get(url, stream=True, headers=headers, timeout=30)
85
+ status_code = response.status_code
86
+ print(f"Download request status code: {status_code}")
87
+
88
+ response.raise_for_status()
89
+
90
+ # Get content type for debugging
91
+ content_type = response.headers.get('Content-Type', 'unknown')
92
+ content_length = response.headers.get('Content-Length', 'unknown')
93
+ print(f"Content type: {content_type}, Content length: {content_length}")
94
+
95
+ # Save the file
96
+ with open(filepath, "wb") as f:
97
+ for chunk in response.iter_content(chunk_size=8192):
98
+ if chunk: # filter out keep-alive new chunks
99
+ f.write(chunk)
100
+
101
+ # Verify file was downloaded successfully
102
+ if os.path.exists(filepath) and os.path.getsize(filepath) > 0:
103
+ print(f"File successfully downloaded to {filepath} ({os.path.getsize(filepath)} bytes)")
104
+ return filepath
105
+ else:
106
+ print(f"File download may have failed. File size: {os.path.getsize(filepath) if os.path.exists(filepath) else 'file does not exist'}")
107
+ return ""
108
+
109
+ except requests.exceptions.Timeout:
110
+ print(f"Timeout error downloading file from {url}")
111
+ return ""
112
+ except requests.exceptions.HTTPError as e:
113
+ print(f"HTTP error downloading file: {e}")
114
+ return ""
115
+ except requests.exceptions.RequestException as e:
116
+ print(f"Request error downloading file: {e}")
117
+ return ""
118
+ except Exception as e:
119
+ print(f"Unexpected error downloading file: {str(e)}")
120
+ return ""
121
+
122
+
123
+ @tool
124
+ def extract_text_from_image(image_path: str) -> str:
125
+ """
126
+ Extract text from an image using OCR library pytesseract (if available).
127
+ Args:
128
+ image_path (str): the path to the image file.
129
+ """
130
+ try:
131
+ # Open the image
132
+ image = Image.open(image_path)
133
+
134
+ # Extract text from the image
135
+ text = pytesseract.image_to_string(image)
136
+
137
+ return f"Extracted text from image:\n\n{text}"
138
+ except Exception as e:
139
+ return f"Error extracting text from image: {str(e)}"
140
+
141
+
142
+ @tool
143
+ def analyze_csv_file(file_path: str, query: str) -> str:
144
+ """
145
+ Reads a CSV file using pandas and returns a summary of its structure and content.
146
+ The summary includes column names, data types, the first 5 rows, and descriptive statistics.
147
+ Use this information to understand the data.
148
+ For specific calculations or data manipulations based on the 'query' (e.g., summing columns, filtering rows, complex aggregations),
149
+ you should use the 'execute_code_multilang' tool with Python pandas code that operates on the file_path.
150
+ The 'query' argument here is for context and will be included in the summary.
151
+ Args:
152
+ file_path (str): The absolute path to the CSV file.
153
+ query (str): The user's question about the data; use this to plan subsequent steps.
154
+ """
155
+ try:
156
+ # Read the CSV file
157
+ df = pd.read_csv(file_path)
158
+
159
+ result = f"CSV File Analysis for: {os.path.basename(file_path)}\n"
160
+ result += f"Query: {query}\n\n"
161
+ result += f"File loaded with {len(df)} rows and {len(df.columns)} columns.\n"
162
+ result += f"Columns: {', '.join(df.columns)}\n\n"
163
+
164
+ result += "First 5 rows:\n"
165
+ result += df.head().to_string() + "\n\n"
166
+
167
+ result += "Data types:\n"
168
+ result += df.dtypes.to_string() + "\n\n"
169
+
170
+ result += "Summary statistics (for numerical columns):\n"
171
+ result += df.describe(include='number').to_string() + "\n\n"
172
+
173
+ result += "Summary statistics (for object/categorical columns):\n"
174
+ result += df.describe(include='object').to_string() + "\n"
175
+
176
+ return result
177
+
178
+ except Exception as e:
179
+ return f"Error analyzing CSV file {file_path}: {str(e)}"
180
+
181
+
182
+ @tool
183
+ def analyze_excel_file(file_path: str, query: str) -> str:
184
+ """
185
+ Reads an Excel file using pandas and returns a summary of its structure and content.
186
+ The summary includes sheet names, column names, data types, the first 5 rows (of the first sheet), and descriptive statistics.
187
+ It defaults to analyzing the first sheet.
188
+ Use this information to understand the data.
189
+ For specific calculations or data manipulations based on the 'query' (e.g., summing columns, filtering rows, complex aggregations),
190
+ you should use the 'execute_code_multilang' tool with Python pandas code that operates on the file_path (and specifies a sheet if not the first).
191
+ The 'query' argument here is for context and will be included in the summary.
192
+ Args:
193
+ file_path (str): The absolute path to the Excel file.
194
+ query (str): The user's question about the data; use this to plan subsequent steps.
195
+ """
196
+ try:
197
+ # Read the Excel file
198
+ # To handle multiple sheets, pandas reads the first sheet by default.
199
+ # For more specific sheet analysis, the tool would need a sheet_name parameter.
200
+ xls = pd.ExcelFile(file_path)
201
+ sheet_names = xls.sheet_names
202
+
203
+ result = f"Excel File Analysis for: {os.path.basename(file_path)}\n"
204
+ result += f"Query: {query}\n"
205
+ result += f"Available sheets: {', '.join(sheet_names)}\n\n"
206
+
207
+ if not sheet_names:
208
+ return f"Error: No sheets found in Excel file {file_path}"
209
+
210
+ # Analyze the first sheet by default
211
+ sheet_to_analyze = sheet_names[0]
212
+ df = pd.read_excel(file_path, sheet_name=sheet_to_analyze)
213
+
214
+ result += f"Analyzing sheet: '{sheet_to_analyze}'\n"
215
+ result += f"Sheet loaded with {len(df)} rows and {len(df.columns)} columns.\n"
216
+ result += f"Columns: {', '.join(df.columns)}\n\n"
217
+
218
+ result += "First 5 rows:\n"
219
+ result += df.head().to_string() + "\n\n"
220
+
221
+ result += "Data types:\n"
222
+ result += df.dtypes.to_string() + "\n\n"
223
+
224
+ result += "Summary statistics (for numerical columns):\n"
225
+ result += df.describe(include='number').to_string() + "\n\n"
226
+
227
+ result += "Summary statistics (for object/categorical columns):\n"
228
+ result += df.describe(include='object').to_string() + "\n"
229
+
230
+ return result
231
+
232
+ except Exception as e:
233
+ return f"Error analyzing Excel file {file_path}: {str(e)}"
tools/imagetools.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ import os
3
+ import io
4
+ import base64
5
+ import uuid
6
+ from PIL import Image
7
+ from typing import List, Dict, Any, Optional
8
+ import numpy as np
9
+ from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter
10
+
11
+ # Helper functions for image processing
12
+ def encode_image(image_path: str) -> str:
13
+ """Convert an image file to base64 string."""
14
+ with open(image_path, "rb") as image_file:
15
+ return base64.b64encode(image_file.read()).decode("utf-8")
16
+
17
+
18
+ def decode_image(base64_string: str) -> Image.Image:
19
+ """Convert a base64 string to a PIL Image."""
20
+ image_data = base64.b64decode(base64_string)
21
+ return Image.open(io.BytesIO(image_data))
22
+
23
+
24
+ def save_image(image: Image.Image, directory: str = "image_outputs") -> str:
25
+ """Save a PIL Image to disk and return the path."""
26
+ os.makedirs(directory, exist_ok=True)
27
+ image_id = str(uuid.uuid4())
28
+ image_path = os.path.join(directory, f"{image_id}.png")
29
+ image.save(image_path)
30
+ return image_path
31
+
32
+
33
+ @tool
34
+ def analyze_image(image_input: str) -> str:
35
+ """
36
+ Analyze an image and provide a detailed description.
37
+
38
+ Args:
39
+ image_input (str): Either a file path to an image or a base64 encoded image string
40
+
41
+ Returns:
42
+ A string description of the image
43
+ """
44
+ try:
45
+ # Check if input is a file path
46
+ if os.path.exists(image_input):
47
+ print(f"Processing image from file path: {image_input}")
48
+ img = Image.open(image_input)
49
+ else:
50
+ # Try to decode as base64
51
+ try:
52
+ print("Input not a file path, trying base64 decoding")
53
+ # Add padding if necessary
54
+ missing_padding = len(image_input) % 4
55
+ if missing_padding != 0:
56
+ image_input += '=' * (4 - missing_padding)
57
+ image_data = base64.b64decode(image_input)
58
+ img = Image.open(io.BytesIO(image_data))
59
+ except Exception as base64_error:
60
+ return f"Error: Could not process image. Not a valid file path or base64 string: {str(base64_error)}"
61
+
62
+ # Get basic image properties
63
+ width, height = img.size
64
+ mode = img.mode
65
+ format = getattr(img, 'format', 'Unknown')
66
+
67
+ # Basic image analysis
68
+ description = "Image analysis:\n"
69
+ description += f"- Dimensions: {width}x{height} pixels\n"
70
+ description += f"- Color mode: {mode}\n"
71
+ description += f"- Format: {format}\n"
72
+
73
+ # More advanced analysis based on image content
74
+ if mode in ("RGB", "RGBA"):
75
+ # Sample colors from different regions
76
+ regions = [
77
+ ("top-left", (width//4, height//4)),
78
+ ("top-right", (width*3//4, height//4)),
79
+ ("center", (width//2, height//2)),
80
+ ("bottom-left", (width//4, height*3//4)),
81
+ ("bottom-right", (width*3//4, height*3//4))
82
+ ]
83
+
84
+ description += "\nColor sampling:\n"
85
+ for region_name, (x, y) in regions:
86
+ pixel = img.getpixel((x, y))
87
+ if len(pixel) >= 3:
88
+ r, g, b = pixel[:3]
89
+ description += f"- {region_name}: RGB({r},{g},{b})\n"
90
+
91
+ # Analyze overall brightness
92
+ try:
93
+ if mode in ("RGB", "RGBA", "L"):
94
+ # Convert to numpy array for faster processing
95
+ arr = np.array(img)
96
+ if mode == "L":
97
+ brightness = arr.mean()
98
+ description += f"\nOverall brightness: {brightness:.1f}/255 "
99
+ if brightness < 85:
100
+ description += "(quite dark)"
101
+ elif brightness < 170:
102
+ description += "(medium brightness)"
103
+ else:
104
+ description += "(quite bright)"
105
+ else:
106
+ # For RGB/RGBA
107
+ if arr.shape[2] >= 3:
108
+ avg_colors = arr[:,:,:3].mean(axis=(0, 1))
109
+ brightness = avg_colors.mean()
110
+ description += f"\nOverall brightness: {brightness:.1f}/255 "
111
+ if brightness < 85:
112
+ description += "(quite dark)"
113
+ elif brightness < 170:
114
+ description += "(medium brightness)"
115
+ else:
116
+ description += "(quite bright)"
117
+
118
+ # Determine dominant color
119
+ r, g, b = avg_colors
120
+ if max(avg_colors) == r:
121
+ description += "\nDominant color channel: Red"
122
+ elif max(avg_colors) == g:
123
+ description += "\nDominant color channel: Green"
124
+ else:
125
+ description += "\nDominant color channel: Blue"
126
+ except Exception as analysis_error:
127
+ description += f"\nError during color analysis: {str(analysis_error)}"
128
+
129
+ return description
130
+
131
+ except Exception as e:
132
+ return f"Error analyzing image: {str(e)}"
133
+
134
+
135
+ @tool
136
+ def transform_image(
137
+ image_base64: str, operation: str, params: Optional[Dict[str, Any]] = None
138
+ ) -> Dict[str, Any]:
139
+ """
140
+ Apply transformations: resize, rotate, crop, flip, brightness, contrast, blur, sharpen, grayscale.
141
+ Args:
142
+ image_base64 (str): Base64 encoded input image
143
+ operation (str): Transformation operation
144
+ params (Dict[str, Any], optional): Parameters for the operation
145
+ Returns:
146
+ Dictionary with transformed image (base64)
147
+ """
148
+ try:
149
+ img = decode_image(image_base64)
150
+ params = params or {}
151
+
152
+ if operation == "resize":
153
+ img = img.resize(
154
+ (
155
+ params.get("width", img.width // 2),
156
+ params.get("height", img.height // 2),
157
+ )
158
+ )
159
+ elif operation == "rotate":
160
+ img = img.rotate(params.get("angle", 90), expand=True)
161
+ elif operation == "crop":
162
+ img = img.crop(
163
+ (
164
+ params.get("left", 0),
165
+ params.get("top", 0),
166
+ params.get("right", img.width),
167
+ params.get("bottom", img.height),
168
+ )
169
+ )
170
+ elif operation == "flip":
171
+ if params.get("direction", "horizontal") == "horizontal":
172
+ img = img.transpose(Image.FLIP_LEFT_RIGHT)
173
+ else:
174
+ img = img.transpose(Image.FLIP_TOP_BOTTOM)
175
+ elif operation == "adjust_brightness":
176
+ img = ImageEnhance.Brightness(img).enhance(params.get("factor", 1.5))
177
+ elif operation == "adjust_contrast":
178
+ img = ImageEnhance.Contrast(img).enhance(params.get("factor", 1.5))
179
+ elif operation == "blur":
180
+ img = img.filter(ImageFilter.GaussianBlur(params.get("radius", 2)))
181
+ elif operation == "sharpen":
182
+ img = img.filter(ImageFilter.SHARPEN)
183
+ elif operation == "grayscale":
184
+ img = img.convert("L")
185
+ else:
186
+ return {"error": f"Unknown operation: {operation}"}
187
+
188
+ result_path = save_image(img)
189
+ result_base64 = encode_image(result_path)
190
+ return {"transformed_image": result_base64}
191
+
192
+ except Exception as e:
193
+ return {"error": str(e)}
194
+
195
+
196
+ @tool
197
+ def draw_on_image(
198
+ image_base64: str, drawing_type: str, params: Dict[str, Any]
199
+ ) -> Dict[str, Any]:
200
+ """
201
+ Draw shapes (rectangle, circle, line) or text onto an image.
202
+ Args:
203
+ image_base64 (str): Base64 encoded input image
204
+ drawing_type (str): Drawing type
205
+ params (Dict[str, Any]): Drawing parameters
206
+ Returns:
207
+ Dictionary with result image (base64)
208
+ """
209
+ try:
210
+ img = decode_image(image_base64)
211
+ draw = ImageDraw.Draw(img)
212
+ color = params.get("color", "red")
213
+
214
+ if drawing_type == "rectangle":
215
+ draw.rectangle(
216
+ [params["left"], params["top"], params["right"], params["bottom"]],
217
+ outline=color,
218
+ width=params.get("width", 2),
219
+ )
220
+ elif drawing_type == "circle":
221
+ x, y, r = params["x"], params["y"], params["radius"]
222
+ draw.ellipse(
223
+ (x - r, y - r, x + r, y + r),
224
+ outline=color,
225
+ width=params.get("width", 2),
226
+ )
227
+ elif drawing_type == "line":
228
+ draw.line(
229
+ (
230
+ params["start_x"],
231
+ params["start_y"],
232
+ params["end_x"],
233
+ params["end_y"],
234
+ ),
235
+ fill=color,
236
+ width=params.get("width", 2),
237
+ )
238
+ elif drawing_type == "text":
239
+ font_size = params.get("font_size", 20)
240
+ try:
241
+ font = ImageFont.truetype("arial.ttf", font_size)
242
+ except IOError:
243
+ font = ImageFont.load_default()
244
+ draw.text(
245
+ (params["x"], params["y"]),
246
+ params.get("text", "Text"),
247
+ fill=color,
248
+ font=font,
249
+ )
250
+ else:
251
+ return {"error": f"Unknown drawing type: {drawing_type}"}
252
+
253
+ result_path = save_image(img)
254
+ result_base64 = encode_image(result_path)
255
+ return {"result_image": result_base64}
256
+
257
+ except Exception as e:
258
+ return {"error": str(e)}
259
+
260
+
261
+ @tool
262
+ def generate_simple_image(
263
+ image_type: str,
264
+ width: int = 500,
265
+ height: int = 500,
266
+ params: Optional[Dict[str, Any]] = None,
267
+ ) -> Dict[str, Any]:
268
+ """
269
+ Generate a simple image (gradient, noise, pattern, chart).
270
+ Args:
271
+ image_type (str): Type of image
272
+ width (int), height (int)
273
+ params (Dict[str, Any], optional): Specific parameters
274
+ Returns:
275
+ Dictionary with generated image (base64)
276
+ """
277
+ try:
278
+ params = params or {}
279
+
280
+ if image_type == "gradient":
281
+ direction = params.get("direction", "horizontal")
282
+ start_color = params.get("start_color", (255, 0, 0))
283
+ end_color = params.get("end_color", (0, 0, 255))
284
+
285
+ img = Image.new("RGB", (width, height))
286
+ draw = ImageDraw.Draw(img)
287
+
288
+ if direction == "horizontal":
289
+ for x in range(width):
290
+ r = int(
291
+ start_color[0] + (end_color[0] - start_color[0]) * x / width
292
+ )
293
+ g = int(
294
+ start_color[1] + (end_color[1] - start_color[1]) * x / width
295
+ )
296
+ b = int(
297
+ start_color[2] + (end_color[2] - start_color[2]) * x / width
298
+ )
299
+ draw.line([(x, 0), (x, height)], fill=(r, g, b))
300
+ else:
301
+ for y in range(height):
302
+ r = int(
303
+ start_color[0] + (end_color[0] - start_color[0]) * y / height
304
+ )
305
+ g = int(
306
+ start_color[1] + (end_color[1] - start_color[1]) * y / height
307
+ )
308
+ b = int(
309
+ start_color[2] + (end_color[2] - start_color[2]) * y / height
310
+ )
311
+ draw.line([(0, y), (width, y)], fill=(r, g, b))
312
+
313
+ elif image_type == "noise":
314
+ noise_array = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8)
315
+ img = Image.fromarray(noise_array, "RGB")
316
+
317
+ else:
318
+ return {"error": f"Unsupported image_type {image_type}"}
319
+
320
+ result_path = save_image(img)
321
+ result_base64 = encode_image(result_path)
322
+ return {"generated_image": result_base64}
323
+
324
+ except Exception as e:
325
+ return {"error": str(e)}
326
+
327
+
328
+ @tool
329
+ def combine_images(
330
+ images_base64: List[str], operation: str, params: Optional[Dict[str, Any]] = None
331
+ ) -> Dict[str, Any]:
332
+ """
333
+ Combine multiple images (collage, stack, blend).
334
+ Args:
335
+ images_base64 (List[str]): List of base64 images
336
+ operation (str): Combination type
337
+ params (Dict[str, Any], optional)
338
+ Returns:
339
+ Dictionary with combined image (base64)
340
+ """
341
+ try:
342
+ images = [decode_image(b64) for b64 in images_base64]
343
+ params = params or {}
344
+
345
+ if operation == "stack":
346
+ direction = params.get("direction", "horizontal")
347
+ if direction == "horizontal":
348
+ total_width = sum(img.width for img in images)
349
+ max_height = max(img.height for img in images)
350
+ new_img = Image.new("RGB", (total_width, max_height))
351
+ x = 0
352
+ for img in images:
353
+ new_img.paste(img, (x, 0))
354
+ x += img.width
355
+ else:
356
+ max_width = max(img.width for img in images)
357
+ total_height = sum(img.height for img in images)
358
+ new_img = Image.new("RGB", (max_width, total_height))
359
+ y = 0
360
+ for img in images:
361
+ new_img.paste(img, (0, y))
362
+ y += img.height
363
+ else:
364
+ return {"error": f"Unsupported combination operation {operation}"}
365
+
366
+ result_path = save_image(new_img)
367
+ result_base64 = encode_image(result_path)
368
+ return {"combined_image": result_base64}
369
+
370
+ except Exception as e:
371
+ return {"error": str(e)}
tools/mathtools.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cmath
2
+ from langchain_core.tools import tool
3
+
4
+
5
+ @tool
6
+ def multiply(a: float, b: float) -> float:
7
+ """
8
+ Multiplies two numbers.
9
+ Args:
10
+ a (float): the first number
11
+ b (float): the second number
12
+ """
13
+ return a * b
14
+
15
+
16
+ @tool
17
+ def add(a: float, b: float) -> float:
18
+ """
19
+ Adds two numbers.
20
+ Args:
21
+ a (float): the first number
22
+ b (float): the second number
23
+ """
24
+ return a + b
25
+
26
+
27
+ @tool
28
+ def subtract(a: float, b: float) -> int:
29
+ """
30
+ Subtracts two numbers.
31
+ Args:
32
+ a (float): the first number
33
+ b (float): the second number
34
+ """
35
+ return a - b
36
+
37
+
38
+ @tool
39
+ def divide(a: float, b: float) -> float:
40
+ """
41
+ Divides two numbers.
42
+ Args:
43
+ a (float): the first float number
44
+ b (float): the second float number
45
+ """
46
+ if b == 0:
47
+ raise ValueError("Cannot divided by zero.")
48
+ return a / b
49
+
50
+
51
+ @tool
52
+ def modulus(a: int, b: int) -> int:
53
+ """
54
+ Get the modulus of two numbers.
55
+ Args:
56
+ a (int): the first number
57
+ b (int): the second number
58
+ """
59
+ return a % b
60
+
61
+
62
+ @tool
63
+ def power(a: float, b: float) -> float:
64
+ """
65
+ Get the power of two numbers.
66
+ Args:
67
+ a (float): the first number
68
+ b (float): the second number
69
+ """
70
+ return a**b
71
+
72
+
73
+ @tool
74
+ def square_root(a: float) -> float | complex:
75
+ """
76
+ Get the square root of a number.
77
+ Args:
78
+ a (float): the number to get the square root of
79
+ """
80
+ if a >= 0:
81
+ return a**0.5
82
+ return cmath.sqrt(a)
tools/searchtools.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ from langchain_community.tools.tavily_search import TavilySearchResults
3
+ from langchain_community.document_loaders import WikipediaLoader
4
+ from langchain_community.document_loaders import ArxivLoader
5
+ from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound # Added
6
+ import os
7
+
8
+ @tool
9
+ def wiki_search(query: str) -> str:
10
+ """Search Wikipedia for a query and return maximum 2 results.
11
+ Args:
12
+ query: The search query."""
13
+ search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
14
+ formatted_search_docs = "\n\n---\n\n".join(
15
+ [
16
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
17
+ for doc in search_docs
18
+ ]
19
+ )
20
+ return {"wiki_results": formatted_search_docs}
21
+
22
+
23
+ @tool
24
+ def web_search(query: str) -> str:
25
+ """Search Tavily for a query and return maximum 3 results.
26
+ Args:
27
+ query: The search query."""
28
+ search_docs = TavilySearchResults(max_results=3).invoke({"query": query})
29
+ formatted_search_docs = "\n\n---\n\n".join(
30
+ [
31
+ f'<Document source="{doc.get("url", "")}">\n{doc.get("content", doc.get("snippet", ""))}\n</Document>'
32
+ for doc in search_docs
33
+ ]
34
+ )
35
+ return {"web_results": formatted_search_docs}
36
+
37
+
38
+ @tool
39
+ def arxiv_search(query: str) -> str:
40
+ """Search Arxiv for a query and return maximum 3 result.
41
+ Args:
42
+ query: The search query."""
43
+ search_docs = ArxivLoader(query=query, load_max_docs=3).load()
44
+ formatted_search_docs = "\n\n---\n\n".join(
45
+ [
46
+ f'<Document source="{doc.metadata.get("source", "N/A")}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
47
+ for doc in search_docs
48
+ ]
49
+ )
50
+ return {"arxiv_results": formatted_search_docs}
51
+
52
+
53
+ @tool
54
+ def get_youtube_transcript(youtube_url: str) -> str:
55
+ """Fetches the transcript for a given YouTube video URL using youtube-transcript-api directly.
56
+ If the video has no transcript, it will return an error message. Then use web_search to find the transcript.
57
+ Args:
58
+ youtube_url: The URL of the YouTube video."""
59
+ try:
60
+ video_id = None
61
+ if "watch?v=" in youtube_url:
62
+ video_id = youtube_url.split("watch?v=")[1].split("&")[0]
63
+ elif "youtu.be/" in youtube_url:
64
+ video_id = youtube_url.split("youtu.be/")[1].split("?")[0]
65
+
66
+ if not video_id:
67
+ return "Error: Could not parse YouTube video ID from URL."
68
+
69
+ transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
70
+
71
+ transcript = None
72
+ try:
73
+ # Try fetching English first if available, then any manual, then any generated
74
+ transcript = transcript_list.find_manually_created_transcript(['en'])
75
+ except NoTranscriptFound:
76
+ try:
77
+ transcript = transcript_list.find_generated_transcript(['en'])
78
+ except NoTranscriptFound:
79
+ # If English not found, try any manual transcript
80
+ try:
81
+ transcript = transcript_list.find_manually_created_transcript(transcript_list.languages)
82
+ except NoTranscriptFound:
83
+ # Finally, try any generated transcript
84
+ try:
85
+ transcript = transcript_list.find_generated_transcript(transcript_list.languages)
86
+ except NoTranscriptFound:
87
+ return "Error: No manual or auto-generated transcripts found for this video in any language."
88
+
89
+ fetched_transcript = transcript.fetch()
90
+
91
+ if not fetched_transcript:
92
+ return "Could not retrieve transcript for the video. The video might not have transcripts available."
93
+
94
+ # Changed item['text'] to item.text to handle cases where items are objects
95
+ full_transcript = " ".join([item.text for item in fetched_transcript])
96
+
97
+ # Returning the transcript text directly, wrapped in a dictionary similar to other tools
98
+ return {"youtube_transcript": full_transcript}
99
+
100
+ except TranscriptsDisabled:
101
+ return "Error: Transcripts are disabled for this video."
102
+ except NoTranscriptFound:
103
+ return "Error: No transcripts found for this video (this should have been caught earlier, but good fallback)."
104
+ except Exception as e:
105
+ # Catching potential network errors or other API issues specifically
106
+ if "HTTP Error 403" in str(e) or "Too Many Requests" in str(e):
107
+ return f"Error: YouTube API request failed, possibly due to rate limiting or access restrictions: {str(e)}"
108
+ return f"Error fetching YouTube transcript using youtube-transcript-api: {str(e)}"