JPM34 commited on
Commit
9505e38
·
1 Parent(s): d68b105

Added tools

Browse files
Files changed (7) hide show
  1. tools_audio.py +42 -0
  2. tools_browser.py +79 -0
  3. tools_code.py +410 -0
  4. tools_doc.py +190 -0
  5. tools_img.py +331 -0
  6. tools_maths.py +83 -0
  7. tools_video.py +278 -0
tools_audio.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Audio Transcription Tool
2
+ from google import genai
3
+ from google.genai import types
4
+ from langchain_core.tools import tool
5
+
6
+
7
+ @tool
8
+ def transcribe_audio(audio_file_path: str, mime_type: str) -> str:
9
+ """Transcribes an audio file using Gemini's audio capabilities.
10
+ Args:
11
+ audio_file_path (str): the path to the audio file to transcribe.
12
+ mime_type (str): the mime type of the audio file.
13
+ Returns:
14
+ str: The transcript of the audio file.
15
+ """
16
+ try:
17
+ # Initialize the model
18
+ client = genai.Client(api_key=os.getenv("GEMINI_KEY"))
19
+ model = "models/gemini-1.5-flash-8b"
20
+
21
+ # Read and encode the audio file
22
+ with open(audio_file_path, "rb") as audio_file:
23
+ audio_data = audio_file.read()
24
+
25
+ # Create the content with audio data
26
+ contents = types.Content(
27
+ parts=[
28
+ types.Part.from_bytes(
29
+ data=audio_data,
30
+ mime_type=mime_type,
31
+ ),
32
+ types.Part(text="Please transcribe this audio file."),
33
+ ]
34
+ )
35
+
36
+ # Generate transcription
37
+ response = client.models.generate_content(
38
+ model=model, contents=contents
39
+ )
40
+ return response.text
41
+ except Exception as e:
42
+ return f"Error transcribing audio: {str(e)}"
tools_browser.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bs4 import BeautifulSoup
2
+
3
+ from langchain_community.document_loaders import WikipediaLoader
4
+ from langchain_community.document_loaders import ArxivLoader
5
+ from langchain_community.tools.tavily_search import TavilySearchResults
6
+
7
+ from langchain_core.tools import tool
8
+ # from smolagents import tool
9
+
10
+ from playwright.sync_api import sync_playwright
11
+
12
+
13
+ @tool
14
+ def wiki_search(query: str) -> str:
15
+ """Search Wikipedia for a query and return maximum 2 results.
16
+ Args:
17
+ query: The search query."""
18
+ search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
19
+ formatted_search_docs = "\n\n---\n\n".join(
20
+ [
21
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
22
+ for doc in search_docs
23
+ ]
24
+ )
25
+ return {"wiki_results": formatted_search_docs}
26
+
27
+
28
+ @tool
29
+ def web_search(query: str) -> str:
30
+ """Search Tavily for a query and return maximum 3 results.
31
+ Args:
32
+ query: The search query."""
33
+ search_docs = TavilySearchResults(max_results=3).invoke(query=query)
34
+ formatted_search_docs = "\n\n---\n\n".join(
35
+ [
36
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
37
+ for doc in search_docs
38
+ ]
39
+ )
40
+ return {"web_results": formatted_search_docs}
41
+
42
+
43
+ @tool
44
+ def arxiv_search(query: str) -> str:
45
+ """Search Arxiv for a query and return maximum 3 result.
46
+ Args:
47
+ query: The search query."""
48
+ search_docs = ArxivLoader(query=query, load_max_docs=3).load()
49
+ formatted_search_docs = "\n\n---\n\n".join(
50
+ [
51
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
52
+ for doc in search_docs
53
+ ]
54
+ )
55
+ return {"arxiv_results": formatted_search_docs}
56
+
57
+
58
+ @tool
59
+ def website_scrape(url: str, question: str) -> str:
60
+ """Scrapes a website and returns the text.
61
+ Args:
62
+ url (str): the URL to the website to scrape.
63
+ Returns:
64
+ str: The text of the website.
65
+ """
66
+
67
+ with sync_playwright() as p:
68
+ browser = p.chromium.launch(headless=True)
69
+ page = browser.new_page()
70
+ page.goto(url)
71
+ html_content = page.content()
72
+ browser.close()
73
+
74
+ soup = BeautifulSoup(html_content, "html.parser")
75
+
76
+ # Extract text from the website
77
+ text = soup.get_text()
78
+
79
+ return text
tools_code.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import contextlib
3
+ import io
4
+ import os
5
+ import subprocess
6
+ import sqlite3
7
+ import tempfile
8
+ import traceback
9
+ import uuid
10
+
11
+ from langchain_core.tools import tool
12
+ from typing import Dict, Any
13
+
14
+ # Imports for Python Code interpreter
15
+ import numpy as np
16
+ import pandas as pd
17
+ import matplotlib.pyplot as plt
18
+ from PIL import Image
19
+
20
+
21
+ class CodeInterpreter:
22
+ def __init__(
23
+ self,
24
+ allowed_modules=None,
25
+ max_execution_time=30,
26
+ working_directory=None,
27
+ ):
28
+ """Initialize the code interpreter with safety measures."""
29
+ self.allowed_modules = allowed_modules or [
30
+ "numpy",
31
+ "pandas",
32
+ "matplotlib",
33
+ "scipy",
34
+ "sklearn",
35
+ "math",
36
+ "random",
37
+ "statistics",
38
+ "datetime",
39
+ "collections",
40
+ "itertools",
41
+ "functools",
42
+ "operator",
43
+ "re",
44
+ "json",
45
+ "sympy",
46
+ "networkx",
47
+ "nltk",
48
+ "PIL",
49
+ "pytesseract",
50
+ "cmath",
51
+ "uuid",
52
+ "tempfile",
53
+ "requests",
54
+ "urllib",
55
+ ]
56
+ self.max_execution_time = max_execution_time
57
+ self.working_directory = working_directory or os.path.join(os.getcwd())
58
+ if not os.path.exists(self.working_directory):
59
+ os.makedirs(self.working_directory)
60
+
61
+ self.globals = {
62
+ "__builtins__": __builtins__,
63
+ "np": np,
64
+ "pd": pd,
65
+ "plt": plt,
66
+ "Image": Image,
67
+ }
68
+ self.temp_sqlite_db = os.path.join(
69
+ tempfile.gettempdir(), "code_exec.db"
70
+ )
71
+
72
+ def execute_code(
73
+ self, code: str, language: str = "python"
74
+ ) -> Dict[str, Any]:
75
+ """Execute the provided code in the selected programming language."""
76
+ language = language.lower()
77
+ execution_id = str(uuid.uuid4())
78
+
79
+ result = {
80
+ "execution_id": execution_id,
81
+ "status": "error",
82
+ "stdout": "",
83
+ "stderr": "",
84
+ "result": None,
85
+ "plots": [],
86
+ "dataframes": [],
87
+ }
88
+
89
+ try:
90
+ if language == "python":
91
+ return self._execute_python(code, execution_id)
92
+ elif language == "bash":
93
+ return self._execute_bash(code, execution_id)
94
+ elif language == "sql":
95
+ return self._execute_sql(code, execution_id)
96
+ elif language == "c":
97
+ return self._execute_c(code, execution_id)
98
+ elif language == "java":
99
+ return self._execute_java(code, execution_id)
100
+ else:
101
+ result["stderr"] = f"Unsupported language: {language}"
102
+ except Exception as e:
103
+ result["stderr"] = str(e)
104
+
105
+ return result
106
+
107
+ def _execute_python(self, code: str, execution_id: str) -> dict:
108
+ output_buffer = io.StringIO()
109
+ error_buffer = io.StringIO()
110
+ result = {
111
+ "execution_id": execution_id,
112
+ "status": "error",
113
+ "stdout": "",
114
+ "stderr": "",
115
+ "result": None,
116
+ "plots": [],
117
+ "dataframes": [],
118
+ }
119
+
120
+ try:
121
+ exec_dir = os.path.join(self.working_directory, execution_id)
122
+ os.makedirs(exec_dir, exist_ok=True)
123
+ plt.switch_backend("Agg")
124
+
125
+ with (
126
+ contextlib.redirect_stdout(output_buffer),
127
+ contextlib.redirect_stderr(error_buffer),
128
+ ):
129
+ exec_result = exec(code, self.globals)
130
+
131
+ if plt.get_fignums():
132
+ for i, fig_num in enumerate(plt.get_fignums()):
133
+ fig = plt.figure(fig_num)
134
+ img_path = os.path.join(exec_dir, f"plot_{i}.png")
135
+ fig.savefig(img_path)
136
+ with open(img_path, "rb") as img_file:
137
+ img_data = base64.b64encode(
138
+ img_file.read()
139
+ ).decode("utf-8")
140
+ result["plots"].append(
141
+ {"figure_number": fig_num, "data": img_data}
142
+ )
143
+
144
+ for var_name, var_value in self.globals.items():
145
+ if (
146
+ isinstance(var_value, pd.DataFrame)
147
+ and len(var_value) > 0
148
+ ):
149
+ result["dataframes"].append(
150
+ {
151
+ "name": var_name,
152
+ "head": var_value.head().to_dict(),
153
+ "shape": var_value.shape,
154
+ "dtypes": str(var_value.dtypes),
155
+ }
156
+ )
157
+
158
+ result["status"] = "success"
159
+ result["stdout"] = output_buffer.getvalue()
160
+ result["result"] = exec_result
161
+
162
+ except Exception as e:
163
+ result["status"] = "error"
164
+ result["stderr"] = (
165
+ f"{error_buffer.getvalue()}\n{traceback.format_exc()}"
166
+ )
167
+
168
+ return result
169
+
170
+ def _execute_bash(self, code: str, execution_id: str) -> dict:
171
+ try:
172
+ completed = subprocess.run(
173
+ code,
174
+ shell=True,
175
+ capture_output=True,
176
+ text=True,
177
+ timeout=self.max_execution_time,
178
+ )
179
+ return {
180
+ "execution_id": execution_id,
181
+ "status": "success" if completed.returncode == 0 else "error",
182
+ "stdout": completed.stdout,
183
+ "stderr": completed.stderr,
184
+ "result": None,
185
+ "plots": [],
186
+ "dataframes": [],
187
+ }
188
+ except subprocess.TimeoutExpired:
189
+ return {
190
+ "execution_id": execution_id,
191
+ "status": "error",
192
+ "stdout": "",
193
+ "stderr": "Execution timed out.",
194
+ "result": None,
195
+ "plots": [],
196
+ "dataframes": [],
197
+ }
198
+
199
+ def _execute_sql(self, code: str, execution_id: str) -> dict:
200
+ result = {
201
+ "execution_id": execution_id,
202
+ "status": "error",
203
+ "stdout": "",
204
+ "stderr": "",
205
+ "result": None,
206
+ "plots": [],
207
+ "dataframes": [],
208
+ }
209
+ try:
210
+ conn = sqlite3.connect(self.temp_sqlite_db)
211
+ cur = conn.cursor()
212
+ cur.execute(code)
213
+ if code.strip().lower().startswith("select"):
214
+ columns = [description[0] for description in cur.description]
215
+ rows = cur.fetchall()
216
+ df = pd.DataFrame(rows, columns=columns)
217
+ result["dataframes"].append(
218
+ {
219
+ "name": "query_result",
220
+ "head": df.head().to_dict(),
221
+ "shape": df.shape,
222
+ "dtypes": str(df.dtypes),
223
+ }
224
+ )
225
+ else:
226
+ conn.commit()
227
+
228
+ result["status"] = "success"
229
+ result["stdout"] = "Query executed successfully."
230
+
231
+ except Exception as e:
232
+ result["stderr"] = str(e)
233
+ finally:
234
+ conn.close()
235
+
236
+ return result
237
+
238
+ def _execute_c(self, code: str, execution_id: str) -> dict:
239
+ temp_dir = tempfile.mkdtemp()
240
+ source_path = os.path.join(temp_dir, "program.c")
241
+ binary_path = os.path.join(temp_dir, "program")
242
+
243
+ try:
244
+ with open(source_path, "w") as f:
245
+ f.write(code)
246
+
247
+ compile_proc = subprocess.run(
248
+ ["gcc", source_path, "-o", binary_path],
249
+ capture_output=True,
250
+ text=True,
251
+ timeout=self.max_execution_time,
252
+ )
253
+ if compile_proc.returncode != 0:
254
+ return {
255
+ "execution_id": execution_id,
256
+ "status": "error",
257
+ "stdout": compile_proc.stdout,
258
+ "stderr": compile_proc.stderr,
259
+ "result": None,
260
+ "plots": [],
261
+ "dataframes": [],
262
+ }
263
+
264
+ run_proc = subprocess.run(
265
+ [binary_path],
266
+ capture_output=True,
267
+ text=True,
268
+ timeout=self.max_execution_time,
269
+ )
270
+ return {
271
+ "execution_id": execution_id,
272
+ "status": "success" if run_proc.returncode == 0 else "error",
273
+ "stdout": run_proc.stdout,
274
+ "stderr": run_proc.stderr,
275
+ "result": None,
276
+ "plots": [],
277
+ "dataframes": [],
278
+ }
279
+ except Exception as e:
280
+ return {
281
+ "execution_id": execution_id,
282
+ "status": "error",
283
+ "stdout": "",
284
+ "stderr": str(e),
285
+ "result": None,
286
+ "plots": [],
287
+ "dataframes": [],
288
+ }
289
+
290
+ def _execute_java(self, code: str, execution_id: str) -> dict:
291
+ temp_dir = tempfile.mkdtemp()
292
+ source_path = os.path.join(temp_dir, "Main.java")
293
+
294
+ try:
295
+ with open(source_path, "w") as f:
296
+ f.write(code)
297
+
298
+ compile_proc = subprocess.run(
299
+ ["javac", source_path],
300
+ capture_output=True,
301
+ text=True,
302
+ timeout=self.max_execution_time,
303
+ )
304
+ if compile_proc.returncode != 0:
305
+ return {
306
+ "execution_id": execution_id,
307
+ "status": "error",
308
+ "stdout": compile_proc.stdout,
309
+ "stderr": compile_proc.stderr,
310
+ "result": None,
311
+ "plots": [],
312
+ "dataframes": [],
313
+ }
314
+
315
+ run_proc = subprocess.run(
316
+ ["java", "-cp", temp_dir, "Main"],
317
+ capture_output=True,
318
+ text=True,
319
+ timeout=self.max_execution_time,
320
+ )
321
+ return {
322
+ "execution_id": execution_id,
323
+ "status": "success" if run_proc.returncode == 0 else "error",
324
+ "stdout": run_proc.stdout,
325
+ "stderr": run_proc.stderr,
326
+ "result": None,
327
+ "plots": [],
328
+ "dataframes": [],
329
+ }
330
+ except Exception as e:
331
+ return {
332
+ "execution_id": execution_id,
333
+ "status": "error",
334
+ "stdout": "",
335
+ "stderr": str(e),
336
+ "result": None,
337
+ "plots": [],
338
+ "dataframes": [],
339
+ }
340
+
341
+
342
+ @tool
343
+ def execute_code_multilang(code: str, language: str = "python") -> str:
344
+ """Execute code in multiple languages (Python, Bash, SQL, C, Java) and return results.
345
+ Args:
346
+ code (str): The source code to execute.
347
+ language (str): The language of the code. Supported: "python", "bash", "sql", "c", "java".
348
+ Returns:
349
+ A string summarizing the execution results (stdout, stderr, errors, plots, dataframes if any).
350
+ """
351
+ interpreter_instance = CodeInterpreter()
352
+ supported_languages = ["python", "bash", "sql", "c", "java"]
353
+ language = language.lower()
354
+
355
+ if language not in supported_languages:
356
+ return f"❌ Unsupported language: {language}. Supported languages are: {', '.join(supported_languages)}"
357
+
358
+ result = interpreter_instance.execute_code(code, language=language)
359
+
360
+ response = []
361
+
362
+ if result["status"] == "success":
363
+ response.append(
364
+ f"✅ Code executed successfully in **{language.upper()}**"
365
+ )
366
+
367
+ if result.get("stdout"):
368
+ response.append(
369
+ "\n**Standard Output:**\n```\n"
370
+ + result["stdout"].strip()
371
+ + "\n```"
372
+ )
373
+
374
+ if result.get("stderr"):
375
+ response.append(
376
+ "\n**Standard Error (if any):**\n```\n"
377
+ + result["stderr"].strip()
378
+ + "\n```"
379
+ )
380
+
381
+ if result.get("result") is not None:
382
+ response.append(
383
+ "\n**Execution Result:**\n```\n"
384
+ + str(result["result"]).strip()
385
+ + "\n```"
386
+ )
387
+
388
+ if result.get("dataframes"):
389
+ for df_info in result["dataframes"]:
390
+ response.append(
391
+ f"\n**DataFrame `{df_info['name']}` (Shape: {df_info['shape']})**"
392
+ )
393
+ df_preview = pd.DataFrame(df_info["head"])
394
+ response.append(
395
+ "First 5 rows:\n```\n" + str(df_preview) + "\n```"
396
+ )
397
+
398
+ if result.get("plots"):
399
+ response.append(
400
+ f"\n**Generated {len(result['plots'])} plot(s)** (Image data returned separately)"
401
+ )
402
+
403
+ else:
404
+ response.append(f"❌ Code execution failed in **{language.upper()}**")
405
+ if result.get("stderr"):
406
+ response.append(
407
+ "\n**Error Log:**\n```\n" + result["stderr"].strip() + "\n```"
408
+ )
409
+
410
+ return "\n".join(response)
tools_doc.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import tempfile
4
+
5
+ from langchain_core.tools import tool
6
+
7
+ # from smolagents import tool
8
+ from typing import Optional
9
+ from urllib.parse import urlparse
10
+
11
+
12
+ @tool
13
+ def save_and_read_file(content: str, filename: Optional[str] = None) -> str:
14
+ """
15
+ Save content to a temporary file and return the path.
16
+ Useful for processing files from the GAIA API.
17
+
18
+ Args:
19
+ content: The content to save to the file
20
+ filename: Optional filename, will generate a random name if not provided
21
+
22
+ Returns:
23
+ Path to the saved file
24
+ """
25
+ temp_dir = tempfile.gettempdir()
26
+ if filename is None:
27
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
28
+ filepath = temp_file.name
29
+ else:
30
+ filepath = os.path.join(temp_dir, filename)
31
+
32
+ # Write content to the file
33
+ with open(filepath, "w") as f:
34
+ f.write(content)
35
+
36
+ return f"File saved to {filepath}. You can read this file to process its contents."
37
+
38
+
39
+ @tool
40
+ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
41
+ """
42
+ Download a file from a URL and save it to a temporary location.
43
+
44
+ Args:
45
+ url: The URL to download from
46
+ filename: Optional filename, will generate one based on URL if not provided
47
+
48
+ Returns:
49
+ Path to the downloaded file
50
+ """
51
+ try:
52
+ # Parse URL to get filename if not provided
53
+ if not filename:
54
+ path = urlparse(url).path
55
+ filename = os.path.basename(path)
56
+ if not filename:
57
+ # Generate a random name if we couldn't extract one
58
+ import uuid
59
+
60
+ filename = f"downloaded_{uuid.uuid4().hex[:8]}"
61
+
62
+ # Create temporary file
63
+ temp_dir = tempfile.gettempdir()
64
+ filepath = os.path.join(temp_dir, filename)
65
+
66
+ # Download the file
67
+ response = requests.get(url, stream=True)
68
+ response.raise_for_status()
69
+
70
+ # Save the file
71
+ with open(filepath, "wb") as f:
72
+ for chunk in response.iter_content(chunk_size=8192):
73
+ f.write(chunk)
74
+
75
+ return f"File downloaded to {filepath}. You can now process this file."
76
+ except Exception as e:
77
+ return f"Error downloading file: {str(e)}"
78
+
79
+
80
+ @tool
81
+ def extract_text_from_image(image_path: str) -> str:
82
+ """
83
+ Extract text from an image using pytesseract (if available).
84
+
85
+ Args:
86
+ image_path: Path to the image file
87
+
88
+ Returns:
89
+ Extracted text or error message
90
+ """
91
+ try:
92
+ # Try to import pytesseract
93
+ import pytesseract
94
+ from PIL import Image
95
+
96
+ # Open the image
97
+ image = Image.open(image_path)
98
+
99
+ # Extract text
100
+ text = pytesseract.image_to_string(image)
101
+
102
+ return f"Extracted text from image:\n\n{text}"
103
+ except ImportError:
104
+ return "Error: pytesseract is not installed. Please install it with 'pip install pytesseract' and ensure Tesseract OCR is installed on your system."
105
+ except Exception as e:
106
+ return f"Error extracting text from image: {str(e)}"
107
+
108
+
109
+ @tool
110
+ def analyze_csv_file(file_path: str, query: str) -> str:
111
+ """
112
+ Analyze a CSV file using pandas and answer a question about it.
113
+
114
+ Args:
115
+ file_path: Path to the CSV file
116
+ query: Question about the data
117
+
118
+ Returns:
119
+ Analysis result or error message
120
+ """
121
+ try:
122
+ import pandas as pd
123
+
124
+ # Read the CSV file
125
+ df = pd.read_csv(file_path)
126
+
127
+ # Run various analyses based on the query
128
+ result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
129
+ result += f"Columns: {', '.join(df.columns)}\n\n"
130
+
131
+ # Add summary statistics
132
+ result += "Summary statistics:\n"
133
+ result += str(df.describe())
134
+
135
+ return result
136
+ except ImportError:
137
+ return "Error: pandas is not installed. Please install it with 'pip install pandas'."
138
+ except Exception as e:
139
+ return f"Error analyzing CSV file: {str(e)}"
140
+
141
+
142
+ @tool
143
+ def analyze_excel_file(file_path: str, query: str) -> str:
144
+ """
145
+ Analyze an Excel file using pandas and answer a question about it.
146
+
147
+ Args:
148
+ file_path: Path to the Excel file
149
+ query: Question about the data
150
+
151
+ Returns:
152
+ Analysis result or error message
153
+ """
154
+ try:
155
+ import pandas as pd
156
+
157
+ # Read the Excel file
158
+ df = pd.read_excel(file_path)
159
+
160
+ # Run various analyses based on the query
161
+ result = f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
162
+ result += f"Columns: {', '.join(df.columns)}\n\n"
163
+
164
+ # Add summary statistics
165
+ result += "Summary statistics:\n"
166
+ result += str(df.describe())
167
+
168
+ return result
169
+ except ImportError:
170
+ return "Error: pandas and openpyxl are not installed. Please install them with 'pip install pandas openpyxl'."
171
+ except Exception as e:
172
+ return f"Error analyzing Excel file: {str(e)}"
173
+
174
+
175
+ @tool
176
+ def read_file(filepath: str) -> str:
177
+ """Reads the content of a text file.
178
+ Args:
179
+ filepath (str): the path to the file to read.
180
+ Returns:
181
+ str: The content of the file.
182
+ """
183
+ try:
184
+ with open(filepath, "r", encoding="utf-8") as file:
185
+ content = file.read()
186
+ return content
187
+ except FileNotFoundError:
188
+ return f"File not found: {filepath}"
189
+ except IOError as e:
190
+ return f"Error reading file: {str(e)}"
tools_img.py ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import numpy as np
5
+ import uuid
6
+
7
+ from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter
8
+ from langchain_core.tools import tool
9
+
10
+ # from smolagents import tool
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ @tool
15
+ def analyze_image(image_base64: str) -> Dict[str, Any]:
16
+ """
17
+ Analyze basic properties of an image (size, mode, color analysis, thumbnail preview).
18
+ Args:
19
+ image_base64 (str): Base64 encoded image string
20
+ Returns:
21
+ Dictionary with analysis result
22
+ """
23
+ try:
24
+ img = decode_image(image_base64)
25
+ width, height = img.size
26
+ mode = img.mode
27
+
28
+ if mode in ("RGB", "RGBA"):
29
+ arr = np.array(img)
30
+ avg_colors = arr.mean(axis=(0, 1))
31
+ dominant = ["Red", "Green", "Blue"][np.argmax(avg_colors[:3])]
32
+ brightness = avg_colors.mean()
33
+ color_analysis = {
34
+ "average_rgb": avg_colors.tolist(),
35
+ "brightness": brightness,
36
+ "dominant_color": dominant,
37
+ }
38
+ else:
39
+ color_analysis = {"note": f"No color analysis for mode {mode}"}
40
+
41
+ thumbnail = img.copy()
42
+ thumbnail.thumbnail((100, 100))
43
+ thumb_path = save_image(thumbnail, "thumbnails")
44
+ thumbnail_base64 = encode_image(thumb_path)
45
+
46
+ return {
47
+ "dimensions": (width, height),
48
+ "mode": mode,
49
+ "color_analysis": color_analysis,
50
+ "thumbnail": thumbnail_base64,
51
+ }
52
+ except Exception as e:
53
+ return {"error": str(e)}
54
+
55
+
56
+ @tool
57
+ def transform_image(
58
+ image_base64: str, operation: str, params: Optional[Dict[str, Any]] = None
59
+ ) -> Dict[str, Any]:
60
+ """
61
+ Apply transformations: resize, rotate, crop, flip, brightness, contrast, blur, sharpen, grayscale.
62
+ Args:
63
+ image_base64 (str): Base64 encoded input image
64
+ operation (str): Transformation operation
65
+ params (Dict[str, Any], optional): Parameters for the operation
66
+ Returns:
67
+ Dictionary with transformed image (base64)
68
+ """
69
+ try:
70
+ img = decode_image(image_base64)
71
+ params = params or {}
72
+
73
+ if operation == "resize":
74
+ img = img.resize(
75
+ (
76
+ params.get("width", img.width // 2),
77
+ params.get("height", img.height // 2),
78
+ )
79
+ )
80
+ elif operation == "rotate":
81
+ img = img.rotate(params.get("angle", 90), expand=True)
82
+ elif operation == "crop":
83
+ img = img.crop(
84
+ (
85
+ params.get("left", 0),
86
+ params.get("top", 0),
87
+ params.get("right", img.width),
88
+ params.get("bottom", img.height),
89
+ )
90
+ )
91
+ elif operation == "flip":
92
+ if params.get("direction", "horizontal") == "horizontal":
93
+ img = img.transpose(Image.FLIP_LEFT_RIGHT)
94
+ else:
95
+ img = img.transpose(Image.FLIP_TOP_BOTTOM)
96
+ elif operation == "adjust_brightness":
97
+ img = ImageEnhance.Brightness(img).enhance(
98
+ params.get("factor", 1.5)
99
+ )
100
+ elif operation == "adjust_contrast":
101
+ img = ImageEnhance.Contrast(img).enhance(params.get("factor", 1.5))
102
+ elif operation == "blur":
103
+ img = img.filter(ImageFilter.GaussianBlur(params.get("radius", 2)))
104
+ elif operation == "sharpen":
105
+ img = img.filter(ImageFilter.SHARPEN)
106
+ elif operation == "grayscale":
107
+ img = img.convert("L")
108
+ else:
109
+ return {"error": f"Unknown operation: {operation}"}
110
+
111
+ result_path = save_image(img)
112
+ result_base64 = encode_image(result_path)
113
+ return {"transformed_image": result_base64}
114
+
115
+ except Exception as e:
116
+ return {"error": str(e)}
117
+
118
+
119
+ @tool
120
+ def draw_on_image(
121
+ image_base64: str, drawing_type: str, params: Dict[str, Any]
122
+ ) -> Dict[str, Any]:
123
+ """
124
+ Draw shapes (rectangle, circle, line) or text onto an image.
125
+ Args:
126
+ image_base64 (str): Base64 encoded input image
127
+ drawing_type (str): Drawing type
128
+ params (Dict[str, Any]): Drawing parameters
129
+ Returns:
130
+ Dictionary with result image (base64)
131
+ """
132
+ try:
133
+ img = decode_image(image_base64)
134
+ draw = ImageDraw.Draw(img)
135
+ color = params.get("color", "red")
136
+
137
+ if drawing_type == "rectangle":
138
+ draw.rectangle(
139
+ [
140
+ params["left"],
141
+ params["top"],
142
+ params["right"],
143
+ params["bottom"],
144
+ ],
145
+ outline=color,
146
+ width=params.get("width", 2),
147
+ )
148
+ elif drawing_type == "circle":
149
+ x, y, r = params["x"], params["y"], params["radius"]
150
+ draw.ellipse(
151
+ (x - r, y - r, x + r, y + r),
152
+ outline=color,
153
+ width=params.get("width", 2),
154
+ )
155
+ elif drawing_type == "line":
156
+ draw.line(
157
+ (
158
+ params["start_x"],
159
+ params["start_y"],
160
+ params["end_x"],
161
+ params["end_y"],
162
+ ),
163
+ fill=color,
164
+ width=params.get("width", 2),
165
+ )
166
+ elif drawing_type == "text":
167
+ font_size = params.get("font_size", 20)
168
+ try:
169
+ font = ImageFont.truetype("arial.ttf", font_size)
170
+ except IOError:
171
+ font = ImageFont.load_default()
172
+ draw.text(
173
+ (params["x"], params["y"]),
174
+ params.get("text", "Text"),
175
+ fill=color,
176
+ font=font,
177
+ )
178
+ else:
179
+ return {"error": f"Unknown drawing type: {drawing_type}"}
180
+
181
+ result_path = save_image(img)
182
+ result_base64 = encode_image(result_path)
183
+ return {"result_image": result_base64}
184
+
185
+ except Exception as e:
186
+ return {"error": str(e)}
187
+
188
+
189
+ @tool
190
+ def generate_simple_image(
191
+ image_type: str,
192
+ width: int = 500,
193
+ height: int = 500,
194
+ params: Optional[Dict[str, Any]] = None,
195
+ ) -> Dict[str, Any]:
196
+ """
197
+ Generate a simple image (gradient, noise, pattern, chart).
198
+ Args:
199
+ image_type (str): Type of image
200
+ width (int), height (int)
201
+ params (Dict[str, Any], optional): Specific parameters
202
+ Returns:
203
+ Dictionary with generated image (base64)
204
+ """
205
+ try:
206
+ params = params or {}
207
+
208
+ if image_type == "gradient":
209
+ direction = params.get("direction", "horizontal")
210
+ start_color = params.get("start_color", (255, 0, 0))
211
+ end_color = params.get("end_color", (0, 0, 255))
212
+
213
+ img = Image.new("RGB", (width, height))
214
+ draw = ImageDraw.Draw(img)
215
+
216
+ if direction == "horizontal":
217
+ for x in range(width):
218
+ r = int(
219
+ start_color[0]
220
+ + (end_color[0] - start_color[0]) * x / width
221
+ )
222
+ g = int(
223
+ start_color[1]
224
+ + (end_color[1] - start_color[1]) * x / width
225
+ )
226
+ b = int(
227
+ start_color[2]
228
+ + (end_color[2] - start_color[2]) * x / width
229
+ )
230
+ draw.line([(x, 0), (x, height)], fill=(r, g, b))
231
+ else:
232
+ for y in range(height):
233
+ r = int(
234
+ start_color[0]
235
+ + (end_color[0] - start_color[0]) * y / height
236
+ )
237
+ g = int(
238
+ start_color[1]
239
+ + (end_color[1] - start_color[1]) * y / height
240
+ )
241
+ b = int(
242
+ start_color[2]
243
+ + (end_color[2] - start_color[2]) * y / height
244
+ )
245
+ draw.line([(0, y), (width, y)], fill=(r, g, b))
246
+
247
+ elif image_type == "noise":
248
+ noise_array = np.random.randint(
249
+ 0, 256, (height, width, 3), dtype=np.uint8
250
+ )
251
+ img = Image.fromarray(noise_array, "RGB")
252
+
253
+ else:
254
+ return {"error": f"Unsupported image_type {image_type}"}
255
+
256
+ result_path = save_image(img)
257
+ result_base64 = encode_image(result_path)
258
+ return {"generated_image": result_base64}
259
+
260
+ except Exception as e:
261
+ return {"error": str(e)}
262
+
263
+
264
+ @tool
265
+ def combine_images(
266
+ images_base64: List[str],
267
+ operation: str,
268
+ params: Optional[Dict[str, Any]] = None,
269
+ ) -> Dict[str, Any]:
270
+ """
271
+ Combine multiple images (collage, stack, blend).
272
+ Args:
273
+ images_base64 (List[str]): List of base64 images
274
+ operation (str): Combination type
275
+ params (Dict[str, Any], optional)
276
+ Returns:
277
+ Dictionary with combined image (base64)
278
+ """
279
+ try:
280
+ images = [decode_image(b64) for b64 in images_base64]
281
+ params = params or {}
282
+
283
+ if operation == "stack":
284
+ direction = params.get("direction", "horizontal")
285
+ if direction == "horizontal":
286
+ total_width = sum(img.width for img in images)
287
+ max_height = max(img.height for img in images)
288
+ new_img = Image.new("RGB", (total_width, max_height))
289
+ x = 0
290
+ for img in images:
291
+ new_img.paste(img, (x, 0))
292
+ x += img.width
293
+ else:
294
+ max_width = max(img.width for img in images)
295
+ total_height = sum(img.height for img in images)
296
+ new_img = Image.new("RGB", (max_width, total_height))
297
+ y = 0
298
+ for img in images:
299
+ new_img.paste(img, (0, y))
300
+ y += img.height
301
+ else:
302
+ return {"error": f"Unsupported combination operation {operation}"}
303
+
304
+ result_path = save_image(new_img)
305
+ result_base64 = encode_image(result_path)
306
+ return {"combined_image": result_base64}
307
+
308
+ except Exception as e:
309
+ return {"error": str(e)}
310
+
311
+
312
+ # Helper functions for image processing
313
+ def encode_image(image_path: str) -> str:
314
+ """Convert an image file to base64 string."""
315
+ with open(image_path, "rb") as image_file:
316
+ return base64.b64encode(image_file.read()).decode("utf-8")
317
+
318
+
319
+ def decode_image(base64_string: str) -> Image.Image:
320
+ """Convert a base64 string to a PIL Image."""
321
+ image_data = base64.b64decode(base64_string)
322
+ return Image.open(io.BytesIO(image_data))
323
+
324
+
325
+ def save_image(image: Image.Image, directory: str = "image_outputs") -> str:
326
+ """Save a PIL Image to disk and return the path."""
327
+ os.makedirs(directory, exist_ok=True)
328
+ image_id = str(uuid.uuid4())
329
+ image_path = os.path.join(directory, f"{image_id}.png")
330
+ image.save(image_path)
331
+ return image_path
tools_maths.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cmath
2
+
3
+ from langchain_core.tools import tool
4
+ # from smolagents import tool
5
+
6
+
7
+ @tool
8
+ def multiply(a: int, b: int) -> int:
9
+ """Multiply two numbers.
10
+ Args:
11
+ a: first int
12
+ b: second int
13
+ """
14
+ return a * b
15
+
16
+
17
+ @tool
18
+ def add(a: int, b: int) -> int:
19
+ """Add two numbers.
20
+
21
+ Args:
22
+ a: first int
23
+ b: second int
24
+ """
25
+ return a + b
26
+
27
+
28
+ @tool
29
+ def subtract(a: int, b: int) -> int:
30
+ """Subtract two numbers.
31
+
32
+ Args:
33
+ a: first int
34
+ b: second int
35
+ """
36
+ return a - b
37
+
38
+
39
+ @tool
40
+ def divide(a: int, b: int) -> int:
41
+ """Divide two numbers.
42
+
43
+ Args:
44
+ a: first int
45
+ b: second int
46
+ """
47
+ if b == 0:
48
+ raise ValueError("Cannot divide by zero.")
49
+ return a / b
50
+
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
+
63
+ @tool
64
+ def power(a: float, b: float) -> float:
65
+ """
66
+ Get the power of two numbers.
67
+ Args:
68
+ a (float): the first number
69
+ b (float): the second number
70
+ """
71
+ return a**b
72
+
73
+
74
+ @tool
75
+ def square_root(a: float) -> float | complex:
76
+ """
77
+ Get the square root of a number.
78
+ Args:
79
+ a (float): the number to get the square root of
80
+ """
81
+ if a >= 0:
82
+ return a**0.5
83
+ return cmath.sqrt(a)
tools_video.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import imageio
2
+ import os
3
+ import re
4
+ import yt_dlp
5
+
6
+ from datetime import timedelta
7
+ from google import genai
8
+ from google.genai import types
9
+
10
+ from langchain_core.tools import tool
11
+ from typing import List, Optional
12
+ from youtube_transcript_api import YouTubeTranscriptApi
13
+
14
+
15
+ # YouTube Video Review Tool
16
+ @tool
17
+ def review_youtube_video(url: str, question: str) -> str:
18
+ """Reviews a YouTube video and answers a specific question about that video.
19
+ Args:
20
+ url (str): the URL to the YouTube video.
21
+ question (str): The question you are asking about the video
22
+ Returns:
23
+ str: The answer to the question
24
+ """
25
+ try:
26
+ client = genai.Client(api_key=os.getenv("GEMINI_KEY"))
27
+ model = "models/gemini-1.5-flash-8b"
28
+
29
+ response = client.models.generate_content(
30
+ model=model,
31
+ contents=types.Content(
32
+ parts=[
33
+ types.Part(file_data=types.FileData(file_uri=url)),
34
+ types.Part(text=question),
35
+ ]
36
+ ),
37
+ )
38
+ return response.text
39
+ except Exception as e:
40
+ return f"Error asking {model} about video: {str(e)}"
41
+
42
+
43
+ @tool
44
+ def use_vision_model(
45
+ question: str, image_paths: List[str], mime_type: str
46
+ ) -> str:
47
+ """Use a Vision Model to answer a question about a set of images.
48
+ Args:
49
+ question (str): The question you are asking about the images.
50
+ image_paths (List[str]): The paths to the images to use for the question.
51
+ mime_type (str): The mime type of the image.
52
+ Returns:
53
+ str: The answer to the question
54
+ """
55
+ try:
56
+ client = genai.Client(api_key=os.getenv("GEMINI_KEY"))
57
+ model = "models/gemini-2.0-flash-001"
58
+
59
+ # Prepare the content parts
60
+ parts = []
61
+ for image_path in image_paths:
62
+ with open(image_path, "rb") as f:
63
+ image_bytes = f.read()
64
+
65
+ response = []
66
+
67
+ for chunk in client.models.generate_content_stream(
68
+ model=model,
69
+ contents=[
70
+ question,
71
+ types.Part.from_bytes(data=image_bytes, mime_type=mime_type),
72
+ ],
73
+ ):
74
+ response.append(chunk.text)
75
+
76
+ return " ".join(response)
77
+
78
+ except Exception as e:
79
+ return f"Error using vision model: {str(e)}"
80
+
81
+
82
+ # YouTube Frames to Images Tool
83
+ @tool
84
+ def video_frames_to_images(
85
+ url: str,
86
+ folder_name: str,
87
+ sample_interval_seconds: int = 5,
88
+ ) -> List[str]:
89
+ """Extracts frames from a video at specified intervals and saves them as images.
90
+ Args:
91
+ url (str): the URL to the video.
92
+ folder_name (str): the name of the folder to save the images to.
93
+ sample_interval_seconds (int): the interval between frames to sample.
94
+ Returns:
95
+ List[str]: A list of paths to the saved image files.
96
+ """
97
+ # Create a subdirectory for the frames
98
+ frames_dir = os.path.join(folder_name, "frames")
99
+ os.makedirs(frames_dir, exist_ok=True)
100
+
101
+ ydl_opts = {
102
+ "format": "bestvideo[height<=1080]+bestaudio/best[height<=1080]/best",
103
+ "outtmpl": os.path.join(folder_name, "video.%(ext)s"),
104
+ "quiet": True,
105
+ "noplaylist": True,
106
+ "merge_output_format": "mp4",
107
+ "force_ipv4": True,
108
+ }
109
+
110
+ try:
111
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
112
+ info = ydl.extract_info(url, download=True)
113
+ video_path = next(
114
+ (
115
+ os.path.join(folder_name, f)
116
+ for f in os.listdir(folder_name)
117
+ if f.endswith(".mp4")
118
+ ),
119
+ None,
120
+ )
121
+
122
+ if not video_path:
123
+ raise RuntimeError("Failed to download video as mp4")
124
+
125
+ reader = imageio.get_reader(video_path)
126
+ metadata = reader.get_meta_data()
127
+ fps = metadata.get("fps")
128
+
129
+ if fps is None:
130
+ reader.close()
131
+ raise RuntimeError(
132
+ "Unable to determine FPS from video metadata"
133
+ )
134
+
135
+ frame_interval = int(fps * sample_interval_seconds)
136
+ image_paths: List[str] = []
137
+
138
+ for idx, frame in enumerate(reader):
139
+ if idx % frame_interval == 0:
140
+ # Save frame as image
141
+ image_path = os.path.join(
142
+ frames_dir, f"frame_{idx:06d}.jpg"
143
+ )
144
+ imageio.imwrite(image_path, frame)
145
+ image_paths.append(image_path)
146
+
147
+ reader.close()
148
+ return image_paths
149
+
150
+ except Exception as e:
151
+ raise RuntimeError(f"Error processing video frames: {str(e)}") from e
152
+
153
+
154
+ @tool
155
+ def transcribe_youtube(url: str) -> str:
156
+ """Transcribes a YouTube video using YouTube Transcript API or Gemini as fallback.
157
+ Args:
158
+ url (str): the URL to the YouTube video.
159
+ Returns:
160
+ str: The transcript of the YouTube video.
161
+ """
162
+ try:
163
+ # First try using YouTube Transcript API
164
+ video_id = _extract_video_id(url)
165
+ if not video_id:
166
+ raise ValueError(f"Invalid YouTube URL: {url}")
167
+
168
+ try:
169
+ # Try to get transcript in English
170
+ transcript_chunks = YouTubeTranscriptApi.get_transcript(
171
+ video_id, languages=["en"]
172
+ )
173
+ # Combine all chunks into a single transcript with timestamps
174
+ transcript = ""
175
+ for chunk in transcript_chunks:
176
+ timestamp = str(timedelta(seconds=int(chunk["start"])))
177
+ transcript += f"[{timestamp}] {chunk['text']}\n"
178
+ return transcript
179
+
180
+ except Exception as transcript_error:
181
+ print(
182
+ f"Failed to get transcript using YouTube API: {str(transcript_error)}"
183
+ )
184
+ print("Falling back to Gemini-based transcription...")
185
+
186
+ # Fallback to Gemini-based transcription
187
+ with tempfile.TemporaryDirectory() as tmpdir:
188
+ # Download audio from YouTube
189
+ ydl_opts = {
190
+ "format": "bestaudio/best",
191
+ "outtmpl": os.path.join(tmpdir, "audio.%(ext)s"),
192
+ "quiet": True,
193
+ "noplaylist": True,
194
+ "postprocessors": [
195
+ {
196
+ "key": "FFmpegExtractAudio",
197
+ "preferredcodec": "wav",
198
+ "preferredquality": "192",
199
+ }
200
+ ],
201
+ }
202
+
203
+ try:
204
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
205
+ info = ydl.extract_info(url, download=True)
206
+ audio_path = next(
207
+ (
208
+ os.path.join(tmpdir, f)
209
+ for f in os.listdir(tmpdir)
210
+ if f.endswith(".wav")
211
+ ),
212
+ None,
213
+ )
214
+
215
+ if not audio_path:
216
+ raise RuntimeError(
217
+ "Failed to download audio"
218
+ ) from transcript_error
219
+
220
+ # Use Gemini to transcribe the audio
221
+ client = genai.Client(api_key=os.getenv("GEMINI_KEY"))
222
+ model = "models/gemini-1.5-flash-8b"
223
+
224
+ # Read the audio file
225
+ with open(audio_path, "rb") as audio_file:
226
+ audio_data = audio_file.read()
227
+
228
+ # Create the content with audio data
229
+ contents = types.Content(
230
+ parts=[
231
+ types.Part(
232
+ file_data=types.FileData(
233
+ mime_type="audio/wav",
234
+ data=audio_data,
235
+ )
236
+ ),
237
+ types.Part(
238
+ text="Please transcribe this audio file. Include timestamps if possible."
239
+ ),
240
+ ]
241
+ )
242
+
243
+ # Generate transcription
244
+ response = client.models.generate_content(
245
+ model=model, contents=contents
246
+ )
247
+ return response.text
248
+
249
+ except yt_dlp.utils.DownloadError as e:
250
+ raise RuntimeError(
251
+ f"Error downloading YouTube video: {str(e)}"
252
+ ) from transcript_error
253
+ except Exception as e:
254
+ raise RuntimeError(
255
+ f"Error processing YouTube video: {str(e)}"
256
+ ) from transcript_error
257
+
258
+ except Exception as e:
259
+ raise RuntimeError(f"Error in YouTube transcription: {str(e)}") from e
260
+
261
+
262
+ def _extract_video_id(url: str) -> Optional[str]:
263
+ """Extract video ID from YouTube URL.
264
+ Args:
265
+ url (str): the URL to the YouTube video.
266
+ Returns:
267
+ str: The video ID of the YouTube video.
268
+ """
269
+ patterns = [
270
+ r"(?:youtube\.com\/watch\?v=|youtube\.com\/embed\/|youtu\.be\/)([^&\n?#]+)",
271
+ r"(?:youtube\.com\/v\/|youtube\.com\/e\/|youtube\.com\/user\/[^\/]+\/|youtube\.com\/[^\/]+\/|youtube\.com\/embed\/|youtu\.be\/)([^&\n?#]+)",
272
+ ]
273
+
274
+ for pattern in patterns:
275
+ match = re.search(pattern, url)
276
+ if match:
277
+ return match.group(1)
278
+ return None