michaelarutyunov commited on
Commit
8fdadf8
·
verified ·
1 Parent(s): bcac1d0

Create tools.py

Browse files
Files changed (1) hide show
  1. tools.py +719 -0
tools.py ADDED
@@ -0,0 +1,719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import csv
3
+ from langchain_openai import ChatOpenAI
4
+ import openai
5
+ import requests
6
+ import pandas as pd
7
+ import tempfile
8
+ import os
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any, Union
12
+ from dotenv import load_dotenv
13
+ from bs4 import BeautifulSoup
14
+
15
+
16
+ from langchain_core.messages import HumanMessage
17
+ from langchain_core.output_parsers import StrOutputParser
18
+ from langchain.tools import tool
19
+ from langchain_openai import ChatOpenAI
20
+ from langchain_community.document_loaders import YoutubeLoader
21
+ from langchain_community.document_loaders.youtube import TranscriptFormat
22
+ from langchain_community.tools import ArxivQueryRun
23
+ from langchain_community.utilities import ArxivAPIWrapper
24
+ from langchain.agents.agent_types import AgentType
25
+ from langchain_experimental.agents.agent_toolkits import create_pandas_dataframe_agent
26
+
27
+ import wikipedia
28
+ from langchain_tavily import TavilySearch
29
+ import yt_dlp
30
+ from yt_dlp.utils import DownloadError, ExtractorError
31
+ from utils import setup_llm
32
+
33
+ current_dir = Path(__file__).parent.absolute()
34
+ env_path = current_dir / ".env"
35
+
36
+ load_dotenv(dotenv_path=env_path, override=True)
37
+
38
+ @tool
39
+ def read_file(file_path: str) -> str:
40
+ """Read the entire content of a text file specified by its path."""
41
+ try:
42
+ with open(file_path, 'r', encoding='utf-8') as f:
43
+ return f.read()
44
+ except Exception as e:
45
+ return f"read_file tool error: {str(e)}"
46
+
47
+ @tool
48
+ def web_search(query: str) -> str:
49
+ """
50
+ Searches the web and returns a list of the most relevant URLs.
51
+ Use this FIRST for complex queries, metadata questions, or to find the right sources.
52
+ Then follow up with web_content_extract on the most promising URL.
53
+ """
54
+ try:
55
+ tavily_search = TavilySearch(
56
+ max_results=5,
57
+ topic="general",
58
+ search_depth="advanced",
59
+ include_raw_content=False, # Just URLs and snippets
60
+ )
61
+
62
+ results = tavily_search.invoke(query)
63
+ # Format results to show URLs and brief descriptions
64
+ web_search_results = "Search Results:\n"
65
+ for i, result in enumerate(results["results"], 1):
66
+ web_search_results += f"{i}. {result['title']}: {result['url']}\n {result['content'][:150]}...\n\n"
67
+
68
+ return web_search_results
69
+ except Exception as e:
70
+ return f"web_search tool error: {str(e)}"
71
+
72
+ @tool
73
+ def web_content_extract(url: str) -> str:
74
+ """
75
+ Extracts and analyzes specific content from a URL using BeautifulSoup.
76
+ Particularly effective for Wikipedia metadata pages, discussion pages,
77
+ and structured web content.
78
+ Can be used after web_search to get detailed information.
79
+ """
80
+ try:
81
+ import requests
82
+ from bs4 import BeautifulSoup
83
+
84
+ headers = {
85
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
86
+ }
87
+
88
+ response = requests.get(url, headers=headers, timeout=10)
89
+ response.raise_for_status() # Raise exception for 4XX/5XX responses
90
+
91
+ soup = BeautifulSoup(response.text, 'html.parser')
92
+ for element in soup.select('script, style, footer, nav, header'):
93
+ if element:
94
+ element.decompose()
95
+ text = soup.body.get_text(separator='\n', strip=True) if soup.body else soup.get_text(separator='\n', strip=True)
96
+
97
+ # Limit content length for response
98
+ return f"Content extracted from {url}:\n\n{text[:10000]}..." if len(text) > 10000 else text
99
+
100
+ except Exception as e:
101
+ return f"web_content_extract tool error: {str(e)}"
102
+ """
103
+ # not used
104
+ def web_search(query: str) -> str:
105
+
106
+ Searches the web for general information. Optimal for:
107
+ 1. Recent events or time-sensitive information
108
+ 2. Metadata about websites
109
+ 3. Questions combining multiple specific criteria
110
+ 4. Queries that need to search across multiple websites or data sources
111
+
112
+ try:
113
+ tavily_search = TavilySearch(
114
+ max_results=3,
115
+ topic="general",
116
+ search_depth="basic",
117
+ include_raw_content=True
118
+ )
119
+
120
+ # This returns a string representation of the results
121
+ return tavily_search.invoke(query)
122
+ except Exception as e:
123
+ return f"web_search tool error: {str(e)}"
124
+ _summary_
125
+
126
+ Raises:
127
+ ValueError: _description_
128
+ RuntimeError: _description_
129
+
130
+ Returns:
131
+ _type_: _description_
132
+ """
133
+ @tool
134
+ def wikipedia_search(query: str, get_summary: bool = True) -> str:
135
+ """
136
+ Searches Wikipedia for factual information contained within articles.
137
+ Best for:
138
+ 1. Basic encyclopedic facts about topics
139
+ 2. Definitions and explanations of concepts
140
+ 3. Historical information about well-documented subjects
141
+ 4. Classification and categorical information
142
+
143
+ Args:
144
+ query (str): The query to search Wikipedia for
145
+ get_summary (bool): Whether to get the summary of the Wikipedia page instead of the full content
146
+ """
147
+
148
+ try:
149
+ page = wikipedia.page(title=query, auto_suggest=True, redirect=True)
150
+
151
+ # text_content = page.content # excluding images, tables, and other data
152
+ full_content_html = page.html()
153
+
154
+ # parse the html content
155
+ soup = BeautifulSoup(full_content_html, 'html.parser')
156
+ text_content = soup.get_text()
157
+
158
+ if get_summary:
159
+ llm_agent_management = setup_llm()[0]
160
+ message = [
161
+ HumanMessage(content=f"Provide response to the following query: {query}\n\nbased on the following content: {text_content}")
162
+ ]
163
+ response = llm_agent_management.invoke(message)
164
+ return response.content
165
+
166
+ # summary = page.summary
167
+ response = f"Page: {page.title}\nSource: {page.url}\n\n{text_content}"
168
+ if response:
169
+ return StrOutputParser().invoke(response)
170
+ else:
171
+ return "wikipedia_search tool produced empty response"
172
+
173
+ # Basic error handling for wikipedia library issues
174
+ except wikipedia.exceptions.PageError:
175
+ # Use the original query in the error message as 'page' might not be defined
176
+ return f"Wikipedia page for query '{query}' does not match any known pages."
177
+ except wikipedia.exceptions.DisambiguationError as e:
178
+ # Provide options if it's a disambiguation page
179
+ options = "\n - ".join(e.options[:5]) # Show first 5 options
180
+ return f"Ambiguous query '{query}'. Did you mean:\n - {options}\nPlease refine your search."
181
+ except Exception as e:
182
+ return f"wikipedia_search tool error: {str(e)}"
183
+
184
+ @tool
185
+ def arxiv_search(query: str) -> str:
186
+ """
187
+ Search Arxiv for scientific papers
188
+ """
189
+ try:
190
+ arxiv_api = ArxivAPIWrapper(top_k_results=3)
191
+ arxiv_search = ArxivQueryRun(api_wrapper=arxiv_api)
192
+ result = arxiv_search.invoke(query)
193
+ response = result.content + "\n\n"
194
+
195
+ if response:
196
+ return response
197
+ else:
198
+ return "arxiv_search tool produced empty response"
199
+ except Exception as e:
200
+ return f"arxiv_search tool error: {str(e)}"
201
+
202
+ @tool
203
+ def calculator_tool(expression: str) -> str:
204
+ """Evaluate a mathematical expression."""
205
+ try:
206
+ result = eval(expression, {"__builtins__": {}},
207
+ {"abs": abs, "round": round, "max": max, "min": min})
208
+ if result:
209
+ return str(result)
210
+ else:
211
+ return "calculator tool produced empty response"
212
+ except Exception as e:
213
+ return f"calculator tool error: {str(e)}"
214
+
215
+ @tool
216
+ def extract_text_from_image(image_path: str) -> str:
217
+ """Extract text from a locally saved image."""
218
+
219
+ try:
220
+ llm_vision = setup_llm()[3]
221
+ with open(image_path, "rb") as image_file:
222
+ image_bytes = image_file.read()
223
+ image_base64 = base64.b64encode(image_bytes).decode("utf-8")
224
+
225
+ message = [
226
+ HumanMessage(
227
+ content=[
228
+ {
229
+ "type": "text",
230
+ "text": (
231
+ "Extract all the text from this image. "
232
+ "Return only the extracted text, no explanations."
233
+ ),
234
+ },
235
+ {
236
+ "type": "image_url",
237
+ "image_url": {
238
+ "url": f"data:image/png;base64,{image_base64}"
239
+ },
240
+ },
241
+ ]
242
+ )
243
+ ]
244
+
245
+ result = llm_vision.invoke(message)
246
+ response = result.content + "\n\n"
247
+
248
+ if response:
249
+ return response
250
+ else:
251
+ return "extract_text_from_image tool produced empty response"
252
+ except Exception as e:
253
+ return f"extract_text_from_image tool error: {str(e)}"
254
+
255
+ @tool
256
+ def extract_youtube_video(video_url: str) -> str:
257
+ """Extract text from a youtube video."""
258
+ try:
259
+ loader = YoutubeLoader.from_youtube_url(
260
+ video_url,
261
+ transcript_format=TranscriptFormat.TEXT,
262
+ language="en",
263
+ add_video_info=True,
264
+ )
265
+
266
+ result = loader.load()
267
+ response = result[0].page_content + "\n\n"
268
+
269
+ if response:
270
+ return response
271
+ else:
272
+ return "extract_youtube_video tool produced empty response"
273
+ except Exception as e:
274
+ return f"extract_youtube_video tool error: {str(e)}"
275
+
276
+ @tool
277
+ def transcribe_audio(audio_path: str) -> str:
278
+ """Extract text from a locally saved audio file."""
279
+
280
+ try:
281
+ openai_client = setup_llm()[4]
282
+
283
+ with open(audio_path, "rb") as audio_file:
284
+ response = openai_client.audio.transcriptions.create(
285
+ model="gpt-4o-mini-transcribe",
286
+ file=audio_file
287
+ )
288
+
289
+ if response:
290
+ return response.text
291
+ else:
292
+ return "transcribe_audio tool produced empty response"
293
+ except Exception as e:
294
+ return f"transcribe_audio tool error: {str(e)}"
295
+
296
+ @tool
297
+ def query_about_image(query: str, image_url: str) -> str:
298
+ """Ask anything about an image from a URL using a Vision Language Model
299
+ Args:
300
+ query (str): The query about the image
301
+ image_url (str): The URL to the image
302
+ """
303
+
304
+ try:
305
+ openai_client = setup_llm()[4]
306
+ response = openai_client.responses.create(
307
+ model="gpt-4o-mini",
308
+ input=[{
309
+ "role": "user",
310
+ "content": [
311
+ {"type": "input_text", "text": query},
312
+ {
313
+ "type": "input_image",
314
+ "image_url": image_url,
315
+ },
316
+ ],
317
+ }],
318
+ )
319
+
320
+ if response:
321
+ return response.text
322
+ else:
323
+ return "query_about_image tool produced empty response"
324
+ except Exception as e:
325
+ return f"query_about_image tool error: {str(e)}"
326
+
327
+ @tool
328
+ def execute_python_code(code: str) -> Any:
329
+ """Executes Python code safely in a restricted environment."""
330
+
331
+ try:
332
+ # Basic restricted execution
333
+ allowed_modules = {'math', 're', 'json'}
334
+ forbidden_terms = ['import', 'exec', 'eval', 'open', 'os', 'sys', 'subprocess']
335
+
336
+ # Simple check for forbidden terms
337
+ for term in forbidden_terms:
338
+ if term in code:
339
+ return f"Forbidden term used: {term}"
340
+
341
+ # Create a restricted globals dictionary
342
+ restricted_globals = {
343
+ '__builtins__': {
344
+ k: __builtins__[k] for k in __builtins__
345
+ if k not in ['open', 'exec', 'eval', 'compile']
346
+ }
347
+ }
348
+
349
+ # Allowed modules
350
+ for module_name in allowed_modules:
351
+ restricted_globals[module_name] = __import__(module_name)
352
+
353
+ # Execute the code with restricted globals and locals
354
+ local_vars = {}
355
+ exec(code, restricted_globals, local_vars)
356
+
357
+ # Return the local variables after execution
358
+ return local_vars
359
+
360
+ except Exception as e:
361
+ return f"Code execution error: {str(e)}"
362
+ """
363
+ # not used
364
+ def csv_reader(file_path: str) -> str:
365
+
366
+ try:
367
+ df = pd.read_csv(file_path, encoding='utf-8')
368
+
369
+ if df.empty:
370
+ return f"CSV file found at '{file_path}' but it is empty."
371
+
372
+ return df.to_string()
373
+
374
+ except FileNotFoundError:
375
+ return f"csv_reader tool error: File not found at '{file_path}'"
376
+ except pd.errors.EmptyDataError:
377
+ return f"csv_reader tool error: No data found in CSV file '{file_path}'."
378
+ except pd.errors.ParserError as e:
379
+ return f"csv_reader tool error: Error parsing CSV file '{file_path}'. Details: {str(e)}"
380
+ except Exception as e:
381
+ # Catch any other unexpected errors
382
+ import traceback
383
+ tb_str = traceback.format_exc()
384
+ return f"csv_reader tool error: An unexpected error occurred while reading '{file_path}'. Error: {str(e)}\nTraceback:\n{tb_str}"
385
+ """
386
+ @tool
387
+ def chess_board_image_analysis(image_path, order_of_play: str = "black") -> str:
388
+ """Analyze a chess position from an image and order of play (black or white) and return the best move in algebraic notation."""
389
+ import chess
390
+ import base64
391
+ import requests
392
+ import os.path
393
+
394
+ def image_to_chess_json(image_path: str) -> Union[dict, str]:
395
+ try:
396
+ llm_vision = setup_llm()[3]
397
+ with open(image_path, "rb") as image_file:
398
+ image_bytes = image_file.read()
399
+ image_base64 = base64.b64encode(image_bytes).decode("utf-8")
400
+
401
+ message = [
402
+ HumanMessage(
403
+ content=[
404
+ {
405
+ "type": "text",
406
+ "text":
407
+ """Analyze this image of a chessboard and return the position of each figure in the following format:
408
+ {
409
+ "figure_name": "position_on_board"
410
+ }
411
+ Important instructions:
412
+ 1. Each figure on the board is represented by a unique line in the JSON object
413
+ 2. Return ONLY the raw JSON without any formatting, markdown, code blocks, or explanations
414
+ 3. Do not use triple backticks
415
+ 4. Do not include the string "json" before the output
416
+ 5. Just return the plain JSON object directly""",
417
+ },
418
+ {
419
+ "type": "image_url",
420
+ "image_url": {
421
+ "url": f"data:image/png;base64,{image_base64}"
422
+ },
423
+ },
424
+ ]
425
+ )
426
+ ]
427
+
428
+ result = llm_vision.invoke(message)
429
+ response = result.content
430
+
431
+ try:
432
+ position_dict = json.loads(response)
433
+ print(position_dict)
434
+ return position_dict
435
+ except json.JSONDecodeError:
436
+ return f"Error: Could not parse response as JSON: {response}"
437
+ except Exception as e:
438
+ return f"image_to_fen_llm tool error: {str(e)}"
439
+
440
+ def create_fen_from_position(position_dict):
441
+ """Convert a position dictionary to FEN notation."""
442
+ # Initialize an 8x8 empty board
443
+ board = [['' for _ in range(8)] for _ in range(8)]
444
+
445
+ # Map of piece names to FEN characters
446
+ piece_map = {
447
+ 'white_king': 'K', 'white_queen': 'Q',
448
+ 'white_rook': 'R', 'white_bishop': 'B',
449
+ 'white_knight': 'N', 'white_pawn': 'P',
450
+ 'black_king': 'k', 'black_queen': 'q',
451
+ 'black_rook': 'r', 'black_bishop': 'b',
452
+ 'black_knight': 'n', 'black_pawn': 'p'
453
+ }
454
+
455
+ # Place pieces on the board
456
+ for piece, position in position_dict.items():
457
+ if not position or len(position) < 2:
458
+ continue # Skip invalid positions
459
+
460
+ # Convert UCI notation to board indices
461
+ file, rank = position[0], position[1]
462
+ col = ord(file) - ord('a')
463
+ row = 8 - int(rank)
464
+
465
+ # Skip if out of board bounds
466
+ if col < 0 or col > 7 or row < 0 or row > 7:
467
+ continue
468
+
469
+ # Determine the correct piece symbol
470
+ piece_symbol = ''
471
+
472
+ # Direct mapping if piece name exactly matches
473
+ if piece in piece_map:
474
+ piece_symbol = piece_map[piece]
475
+ else:
476
+ # Strip numeric suffix if present (e.g., white_pawn1 -> white_pawn)
477
+ base_name = piece.rstrip('0123456789')
478
+ if base_name.endswith('_'):
479
+ base_name = base_name[:-1]
480
+
481
+ if base_name in piece_map:
482
+ piece_symbol = piece_map[base_name]
483
+ else:
484
+ # Try partial matching by looking for key prefixes
485
+ for key, symbol in piece_map.items():
486
+ if piece.startswith(key):
487
+ piece_symbol = symbol
488
+ break
489
+
490
+ # Place the piece on the board
491
+ if piece_symbol:
492
+ board[row][col] = piece_symbol
493
+
494
+ # Convert board to FEN piece placement notation
495
+ fen_parts = []
496
+ for row in board:
497
+ empty_count = 0
498
+ rank_str = ""
499
+
500
+ for cell in row:
501
+ if cell == '':
502
+ empty_count += 1
503
+ else:
504
+ if empty_count > 0:
505
+ rank_str += str(empty_count)
506
+ empty_count = 0
507
+ rank_str += cell
508
+
509
+ if empty_count > 0:
510
+ rank_str += str(empty_count)
511
+
512
+ fen_parts.append(rank_str)
513
+
514
+ piece_placement = '/'.join(fen_parts)
515
+
516
+ # Since it's black's turn as specified
517
+ active_color = "b" if order_of_play == "black" else "w"
518
+ castling = "KQkq"
519
+ en_passant = "-"
520
+ halfmove_clock = "0"
521
+ fullmove_number = "1"
522
+
523
+ # Construct the complete FEN string
524
+ fen_notation = f"{piece_placement} {active_color} {castling} {en_passant} {halfmove_clock} {fullmove_number}"
525
+
526
+ return fen_notation
527
+
528
+ def get_best_move(fen_notation: str) -> str:
529
+ """Get the best move from Stockfish chess engine API."""
530
+ if not fen_notation:
531
+ return "Error: Invalid FEN notation"
532
+
533
+ try:
534
+ api_url_stockfish = "https://stockfish.online/api/s/v2.php"
535
+ depth = 10
536
+
537
+ import urllib.parse
538
+ encoded_fen = urllib.parse.quote(fen_notation)
539
+ full_url = f"{api_url_stockfish}?fen={encoded_fen}&depth={depth}"
540
+
541
+ # Make GET request with increased timeout
542
+ response = requests.get(full_url, timeout=60)
543
+
544
+
545
+ if response.status_code == 200:
546
+ result = response.json()
547
+ if result.get("success", False):
548
+ # Extract best move from the response
549
+ best_move = result.get("bestmove", "")
550
+
551
+ # The API returns format like "bestmove e2e4 ponder h7h5"
552
+ # We need to extract just the move part
553
+ if " " in best_move:
554
+ best_move = best_move.split(" ")[1] # Get the actual move
555
+
556
+ return best_move
557
+ else:
558
+ # If we got a response but success is False
559
+ return f"Failed to get best move: {result.get('data', 'Unknown error')}"
560
+
561
+ return f"Failed to get best move. Status code: {response.status_code}"
562
+ except Exception as e:
563
+ return f"Error getting best move: {str(e)}"
564
+
565
+ def convert_uci_to_algebraic(fen_notation, uci_move):
566
+ """Convert UCI move notation to algebraic notation."""
567
+ if not fen_notation or not uci_move:
568
+ return "Error: Missing FEN notation or UCI move"
569
+
570
+ try:
571
+ # Create chess board from FEN
572
+ board = chess.Board(fen_notation)
573
+
574
+ # Convert move from UCI to algebraic
575
+ move = chess.Move.from_uci(uci_move)
576
+
577
+ # Verify move is legal
578
+ if move not in board.legal_moves:
579
+ return f"Error: {uci_move} is not a legal move in this position"
580
+
581
+ # Get algebraic notation
582
+ algebraic_move = board.san(move)
583
+ return algebraic_move
584
+ except ValueError as e:
585
+ return f"Error converting move: {str(e)}"
586
+ except Exception as e:
587
+ return f"Unexpected error: {str(e)}"
588
+
589
+ # Main function logic
590
+ try:
591
+ # Get FEN notation from image
592
+ chess_json = image_to_chess_json(image_path)
593
+ if isinstance(chess_json, str) and (chess_json.startswith("Error") or chess_json.startswith("Failed")):
594
+ return chess_json
595
+
596
+ # Get best move in UCI format
597
+ fen_notation = create_fen_from_position(chess_json)
598
+ uci_move = get_best_move(fen_notation)
599
+ if isinstance(uci_move, str) and (uci_move.startswith("Error") or uci_move.startswith("Failed")):
600
+ return uci_move
601
+
602
+ # Convert to algebraic notation
603
+ algebraic_move = convert_uci_to_algebraic(fen_notation, uci_move)
604
+
605
+ # Return the result
606
+ if algebraic_move.startswith("Error"):
607
+ return algebraic_move
608
+ else:
609
+ return f"Best move: {algebraic_move}"
610
+ except Exception as e:
611
+ return f"Chess board analysis failed: {str(e)}"
612
+
613
+ @tool
614
+ def download_youtube_audio(url, task_id):
615
+ """Download the audio from a YouTube video"""
616
+ temp_dir = tempfile.gettempdir()
617
+ output_filename_template = os.path.join(temp_dir, f"{task_id}.%(ext)s")
618
+ downloaded_audio_file_path = os.path.join(temp_dir, f"{task_id}.mp3")
619
+
620
+ ydl_opts = {
621
+ 'format': 'bestaudio/best', # Select best audio quality available
622
+ 'outtmpl': output_filename_template, # Temporary filename pattern
623
+ 'postprocessors': [{
624
+ 'key': 'FFmpegExtractAudio', # Use FFmpeg to extract audio
625
+ 'preferredcodec': 'mp3', # Convert to MP3 format
626
+ 'preferredquality': '192', # Set audio quality (bitrate)
627
+ }],
628
+ 'quiet': True, # Suppress console output
629
+ }
630
+
631
+ try:
632
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
633
+ ydl.download([url])
634
+ if os.path.exists(downloaded_audio_file_path):
635
+ print(f"Successfully downloaded and converted audio to: {downloaded_audio_file_path}")
636
+ return downloaded_audio_file_path
637
+ else:
638
+ print(f"Failed to download audio from {url}")
639
+ return None
640
+
641
+ except DownloadError as e:
642
+ print(f"yt-dlp download error for {url} (Task ID: {task_id}): {e}")
643
+ return None
644
+ except ExtractorError as e:
645
+ print(f"yt-dlp extractor error for {url} (Task ID: {task_id}): {e}")
646
+ return None
647
+ except Exception as e:
648
+ print(f"An unexpected error occurred while processing {url} (Task ID: {task_id}): {e}")
649
+ return None
650
+
651
+ @tool
652
+ def find_phrase_in_text(text, phrase):
653
+ """
654
+ Find a specific phrase in the text and return its segment with the next segment
655
+
656
+ Args:
657
+ text (str): The text to search
658
+ phrase (str): The phrase to look for
659
+
660
+ Returns:
661
+ tuple: (question segment, response segment)
662
+ """
663
+ segments = text['segments']
664
+
665
+ # Convert the question phrase to lowercase for case-insensitive matching
666
+ phrase_lower = phrase.lower()
667
+
668
+ # Find the segment containing the question
669
+ phrase_segment = None
670
+
671
+ for i, segment in enumerate(segments):
672
+ if phrase_lower in segment['text'].lower():
673
+ phrase_segment = segment
674
+ # If we found the question, the response is likely in the next segment
675
+ if i + 1 < len(segments):
676
+ response_segment = segments[i + 1]
677
+ return phrase_segment, response_segment
678
+
679
+ return None, None
680
+
681
+ @tool
682
+ def analyse_tabular_data(table_path, query: str) -> str:
683
+ """
684
+ Analyse a table and return the answer to a question.
685
+ """
686
+ try:
687
+ # Read the table
688
+ file_type = table_path.split(".")[-1]
689
+ reader_map = {
690
+ "csv": pd.read_csv,
691
+ "json": pd.read_json,
692
+ "xlsx": pd.read_excel,
693
+ "xls": pd.read_excel,
694
+ }
695
+
696
+ if file_type not in reader_map:
697
+ return f"Unsupported file type: {file_type}"
698
+
699
+ df = reader_map[file_type](table_path)
700
+ except Exception as e:
701
+ return f"Error reading table: {str(e)}"
702
+
703
+ if df is None:
704
+ print(f"Error: Table is not in a valid format")
705
+ return None
706
+ else:
707
+ try:
708
+ agent = create_pandas_dataframe_agent(
709
+ ChatOpenAI(temperature=0, model="gpt-4.1-mini"),
710
+ df,
711
+ verbose=True,
712
+ agent_type=AgentType.OPENAI_FUNCTIONS,
713
+ allow_dangerous_code=True
714
+ )
715
+ result = agent.invoke({"input": query})
716
+ return str(result)
717
+ except Exception as e:
718
+ return f"Error analysing table: {str(e)}"
719
+