from langchain_community.tools import DuckDuckGoSearchRun import base64 from langchain.tools import Tool import random from langchain.tools import Tool from huggingface_hub import list_models from langchain_community.document_loaders import WikipediaLoader from langchain_community.document_loaders import ArxivLoader from langchain_community.document_loaders.csv_loader import CSVLoader import math from langgraph.prebuilt import tools_condition from langgraph.prebuilt import ToolNode from langchain_core.tools import tool from langchain_core.messages import SystemMessage, HumanMessage, AIMessage import os from typing import Optional, Tuple from huggingface_hub import list_models from pathlib import Path import json from langchain_community.document_loaders import UnstructuredExcelLoader from langchain_tavily import TavilySearch from langchain.schema import Document from langchain_community.document_loaders import YoutubeLoader from langchain_community.document_loaders import AssemblyAIAudioTranscriptLoader from langchain_community.document_loaders.assemblyai import TranscriptFormat import assemblyai as aai import tempfile from typing import Dict, List from langchain_community.document_loaders import GoogleApiClient, GoogleApiYoutubeLoader from langchain_openai import ChatOpenAI def create_credentials_file(api_key: str) -> str: """ Create a temporary credentials file for Google API access. This function creates the necessary credentials structure that Google's API expects. In a production environment, you would typically have a proper service account JSON file, but for this educational example, we'll create a minimal structure. Args: api_key: "The API Key" Returns: Path to the temporary credentials file """ # Create a minimal credentials structure # Note: In production, you would use proper service account credentials credentials_data = { "type": "service_account", "project_id": "your-project-id", "private_key_id": "dummy-key-id", "private_key": "dummy-private-key", "client_email": "tu101@pnw.edu", "client_id": "dummy-client-id", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/dummy%40your-project-id.iam.gserviceaccount.com" } # Create a temporary file to store credentials temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) json.dump(credentials_data, temp_file) temp_file.close() return temp_file.name def extract_video_id_from_url(url: str) -> Optional[str]: """ Extract the video ID from a YouTube URL. This function handles different YouTube URL formats and extracts the unique video identifier that the API needs to fetch the video content. Args: url: YouTube URL in various formats Returns: Video ID string or None if URL is invalid """ import re # Handle different YouTube URL formats patterns = [ r'(?:https?://)?(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]+)', r'(?:https?://)?(?:www\.)?youtu\.be/([a-zA-Z0-9_-]+)', r'(?:https?://)?(?:www\.)?youtube\.com/embed/([a-zA-Z0-9_-]+)', ] for pattern in patterns: match = re.search(pattern, url) if match: return match.group(1) return None def youtube_search_google_api(link: str) -> Dict: """ Search YouTube using Google's official API and extract transcript/captions. This function represents a more robust approach to YouTube content access. Instead of web scraping, we use Google's official API endpoints, which provide reliable access to video metadata and transcript data. Args: link: YouTube URL to process Returns: Dictionary containing video documents or error information """ # Your Google Cloud API key api_key = "AIzaSyA-1ZuYz-PYnnE9PQsWA7vOuJeRz-E2sbU" try: # First, let's validate and extract the video ID video_id = extract_video_id_from_url(link) if not video_id: return { "YT_docs": [{ "text": "Invalid YouTube URL format. Please provide a valid YouTube link.", "metadata": {"error": True, "url": link} }] } # For this implementation, we'll use a simpler approach with the API key # In a full production system, you would use proper service account credentials # Alternative approach using youtube-transcript-api for transcript data # This is often more reliable for transcript access try: from youtube_transcript_api import YouTubeTranscriptApi # Get the transcript for the video transcript_list = YouTubeTranscriptApi.get_transcript(video_id) # Combine all transcript segments into a single text full_transcript = "" for segment in transcript_list: full_transcript += segment['text'] + " " # Create a document-like structure similar to other loaders documents = [{ "text": full_transcript.strip(), "metadata": { "video_id": video_id, "source": link, "title": f"YouTube Video {video_id}", "segments_count": len(transcript_list) } }] return {"YT_docs": documents} except Exception as transcript_error: # If transcript API fails, provide helpful error information error_msg = f"Could not retrieve transcript for video {video_id}. " error_msg += "This video may not have captions available, may be private, " error_msg += "or may have restricted access. " error_msg += f"Original error: {str(transcript_error)}" return { "YT_docs": [{ "text": error_msg, "metadata": {"error": True, "video_id": video_id} }] } except Exception as e: # Comprehensive error handling with debugging information error_msg = f"Error processing YouTube video: {str(e)}" return { "YT_docs": [{ "text": error_msg, "metadata": {"error": True, "url": link} }] } YT_search_tool = Tool( name="YT_search", func=youtube_search_google_api, description="Searches YouTube using Google's official services to extract video transcripts and captions. Handles various YouTube URL formats and provides detailed error information when content is not available." ) def audio_search(path: str) -> str: """Read an audio mp3 file and extract the text to understand it. Args: path: The path to the file. If the file attached is just {name}.mp3, then it is local, and just use {name}.mp3. """ aai.settings.api_key = "945f7d6c00f142158322cd0c13901024" possible_paths = [ path, # Try the path as-is first os.path.join(".", path), # Try current directory os.path.join("/tmp", path), # Try common temp directory os.path.join("uploads", path), # Try uploads directory ] # Find the actual file location actual_path = None for potential_path in possible_paths: if os.path.exists(potential_path): actual_path = potential_path break if actual_path is None: # If we can't find the file, let's provide debugging information current_dir_files = os.listdir(".") error_msg = f"Could not find audio file. Searched in: {possible_paths}. Current directory contains: {current_dir_files[:10]}" # Limit output for readability return {"audio_docs": [{"text": error_msg, "metadata": {"error": True}}]} try: # Use the found path loader = AssemblyAIAudioTranscriptLoader( file_path=actual_path, transcript_format=TranscriptFormat.PARAGRAPHS ) raw_docs = loader.load() if not raw_docs: return {"audio_docs": [{"text": "No audio content could be transcribed from this file.", "metadata": {"warning": True}}]} # Convert each Document into a serializable dict serialized_docs = [ {"text": doc.page_content, "metadata": doc.metadata} for doc in raw_docs ] return {"audio_docs": serialized_docs} except Exception as e: error_msg = f"Error processing audio file at {actual_path}: {str(e)}" return {"audio_docs": [{"text": error_msg, "metadata": {"error": True}}]} audio_search_tool = Tool( name="audio_search", func=audio_search, description="Uses an mp3 audio file path and extract it into paragraphs of text that are searchable." ) def excel_search(excel_path: str ) -> str: """Given the path to an excel file, return the excel file contents as raw text. Args: excel_path: The path to where the excel file is stored.""" loader = UnstructuredExcelLoader(excel_path, mode="elements") raw_excel_docs = loader.load() formatted_search_docs = "\n\n---\n\n".join( [ f'\n{doc.page_content}\n' for doc in raw_excel_docs ]) return {"excel_results": formatted_search_docs} excel_search_tool = Tool( name="excel_search", func=excel_search, description="Searches for and retrieves information in an Excel Document using the given path." ) def wiki_search(query: str) -> str: """Search Wikipedia for a query and return maximum 2 results. Args: query: The search query.""" search_docs = WikipediaLoader(query=query, load_max_docs=2).load() formatted_search_docs = "\n\n---\n\n".join( [ f'\n{doc.page_content}\n' for doc in search_docs ]) return {"wiki_results": formatted_search_docs} wiki_search_tool = Tool( name="wiki_search", func=wiki_search, description="Searches for and retrieves information on Wikipedia using an inputted query." ) def web_search(query: str) -> str: """Search Tavily for a query and return maximum 5 results. Make sure to format the args as query and have the query sound like a sentence, hopefully matching part of the question the search is used for. Args: query: The search query. The query fulfils the argument that is input.""" tavily = TavilySearch(max_results=5, topic="general", include_answer=True, include_image_descriptions=True) raw_results = tavily.invoke(query) # Extract the actual search results from the response dictionary # The 'results' key contains the list of search results we want search_results = raw_results.get('results', []) docs = [] for idx, result in enumerate(search_results): # Each result is a dictionary with keys like 'title', 'content', 'url' # We'll use the content as the main text and include other metadata content = result.get('content', result.get('title', 'No content available')) source_url = result.get('url', f'Tavily result #{idx+1}') docs.append(Document( page_content=content, metadata={"source": source_url, "title": result.get('title', 'No title')} )) formatted_docs = "\n\n---\n\n".join( f'\n' f'{doc.page_content}\n' f'' for doc in docs ) # Fix the return type - should return string, not dictionary return formatted_docs web_search_tool = Tool( name="web_search", func=web_search, description="Searches for and retrieves information online using an inputted query using Tavily. Please use a query that sounds like a sentence and not just keywords." ) def arvix_search(query: str) -> str: """Search Arxiv for a query and return maximum 3 results. Args: query: The search query. Returns: Formatted string containing search results from Arxiv papers. """ try: # Load documents from Arxiv search_docs = ArxivLoader(query=query, load_max_docs=3).load() # Check if we got any results if not search_docs: return "No results found for the query: " + query # Process each document with safe metadata access formatted_docs = [] for doc in search_docs: # Use .get() method for safe dictionary access with fallback values source = doc.metadata.get("source", "Unknown source") page = doc.metadata.get("page", "N/A") # Create the formatted document string formatted_doc = f'\n{doc.page_content[:1000]}\n' formatted_docs.append(formatted_doc) # Join all documents with separators formatted_search_docs = "\n\n---\n\n".join(formatted_docs) # Return the string directly (not wrapped in a dictionary) return formatted_search_docs except Exception as e: # Handle any unexpected errors gracefully return f"Error searching Arxiv: {str(e)}" arvix_search_tool = Tool( name="arvix_search", func=arvix_search, description="Searches for and retrieves information from Arxiv, a database storing scientific papers. Returns formatted text containing paper abstracts and metadata." ) def extract_image_text(img_path: str) -> str: """ Extract text from an image file using a multimodal model. Args: img_path: A local image file path (strings). Returns: A single string containing the concatenated text extracted from each image. """ vision_llm = ChatOpenAI(model="gpt-4o") all_text = "" try: # Read image and encode as base64 with open(img_path, "rb") as image_file: image_bytes = image_file.read() image_base64 = base64.b64encode(image_bytes).decode("utf-8") # Prepare the prompt including the base64 image data message = [ HumanMessage( content=[ { "type": "text", "text": ( "Extract all the text from this image. " "Return only the extracted text, no explanations." ), }, { "type": "image_url", "image_url": { "url": f"data:image/png;base64,{image_base64}" }, }, ] ) ] # Call the vision-capable model response = vision_llm.invoke(message) # Append extracted text all_text += response.content + "\n\n" return all_text.strip() except Exception as e: # You can choose whether to raise or just return an empty string / error message error_msg = f"Error extracting text: {str(e)}" print(error_msg) return "" extract_image_text = Tool( name="extract_image_text", func=extract_image_text, description="Extracts text that might be visible on an image by reading the image. Works with path/url to image." ) def multiply(a: int, b: int) -> int: """Multiply two numbers. Args: a: first int b: second int """ return a * b multiply = Tool( name="multiply", func=multiply, description="Multiplies two integer numbers to get the product." ) def add(a: int, b: int) -> int: """Add two numbers. Args: a: first int b: second int """ return a + b add = Tool( name="add", func=add, description="Adds two integer numbers to get the result." ) def subtract(a: int, b: int) -> int: """Subtract two numbers. Args: a: first int b: second int """ return a - b subtract = Tool( name="subtract", func=subtract, description="Subtracts two integer numbers to get the result." ) def divide(a: int, b: int) -> int: """Divide two numbers. Args: a: first int b: second int """ if b == 0: raise ValueError("Cannot divide by zero.") return a / b divide = Tool( name="divide", func=divide, description="Divides two integer numbers to get the result." ) def modulus(a: int, b: int) -> int: """Get the modulus of two numbers. Args: a: first int b: second int """ return a % b modulus = Tool( name="modulus", func=modulus, description="Calculates the modulus of two integer numbers, a mod b, to get the result." ) def calculate_cargo_travel_time( origin_coords: Tuple[float, float], destination_coords: Tuple[float, float], cruising_speed_kmh: Optional[float] = 750.0, # Average speed for cargo planes ) -> float: """ Calculate the travel time for an object, usually a cargo plane, between two points on Earth using great-circle distance. Args: origin_coords: Tuple of (latitude, longitude) for the starting point destination_coords: Tuple of (latitude, longitude) for the destination cruising_speed_kmh: Optional cruising speed in km/h (defaults to 750 km/h for typical cargo planes). Adjustable depending on object. Returns: float: The estimated travel time in hours Example: >>> # Chicago (41.8781° N, 87.6298° W) to Sydney (33.8688° S, 151.2093° E) >>> result = calculate_cargo_travel_time((41.8781, -87.6298), (-33.8688, 151.2093)) """ def to_radians(degrees: float) -> float: return degrees * (math.pi / 180) # Extract coordinates lat1, lon1 = map(to_radians, origin_coords) lat2, lon2 = map(to_radians, destination_coords) # Earth's radius in kilometers EARTH_RADIUS_KM = 6371.0 # Calculate great-circle distance using the haversine formula dlon = lon2 - lon1 dlat = lat2 - lat1 a = ( math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 ) c = 2 * math.asin(math.sqrt(a)) distance = EARTH_RADIUS_KM * c # Add 10% to account for non-direct routes and air traffic controls actual_distance = distance * 1.1 # Calculate flight time # Add 1 hour for takeoff and landing procedures flight_time = (actual_distance / cruising_speed_kmh) + 1.0 # Format the results return round(flight_time, 2) calculate_cargo_travel_time = Tool( name="calculate_cargo_travel_time", func=calculate_cargo_travel_time, description="Calculates the travel time between two points on the earth based on an object with optionally set speed, usually a cargo plane." ) def get_hub_stats(author: str) -> str: """Fetches the most downloaded model from a specific author on the Hugging Face Hub.""" try: # List models from the specified author, sorted by downloads models = list(list_models(author=author, sort="downloads", direction=-1, limit=1)) if models: model = models[0] return f"The most downloaded model by {author} is {model.id} with {model.downloads:,} downloads." else: return f"No models found for author {author}." except Exception as e: return f"Error fetching models for {author}: {str(e)}" # Initialize the tool hub_stats_tool = Tool( name="get_hub_stats", func=get_hub_stats, description="Fetches the most downloaded model from a specific author on the Hugging Face Hub." )