import os from pydantic import BaseModel, Field, model_validator # type: ignore from typing import List, Optional, Generator, ClassVar, Any, Dict from datetime import datetime from typing import ( Any, ) from typing_extensions import Annotated from bs4 import BeautifulSoup # type: ignore from geopy.geocoders import Nominatim # type: ignore import pytz # type: ignore import requests import yfinance as yf # type: ignore import logging # %% def WeatherForecast(city, state, country="USA"): """ Get weather information from weather.gov based on city, state, and country. Args: city (str): City name state (str): State name or abbreviation country (str): Country name (defaults to "USA" as weather.gov is for US locations) Returns: dict: Weather forecast information """ # Step 1: Get coordinates for the location using geocoding location = f"{city}, {state}, {country}" coordinates = get_coordinates(location) if not coordinates: return {"error": f"Could not find coordinates for {location}"} # Step 2: Get the forecast grid endpoint for the coordinates grid_endpoint = get_grid_endpoint(coordinates) if not grid_endpoint: return {"error": f"Could not find weather grid for coordinates {coordinates}"} # Step 3: Get the forecast data from the grid endpoint forecast = get_forecast(grid_endpoint) if not forecast["location"]: forecast["location"] = location return forecast def get_coordinates(location): """Convert a location string to latitude and longitude using Nominatim geocoder.""" try: # Create a geocoder with a meaningful user agent geolocator = Nominatim(user_agent="weather_app_example") # Get the location location_data = geolocator.geocode(location) if location_data: return { "latitude": location_data.latitude, "longitude": location_data.longitude, } else: print(f"Location not found: {location}") return None except Exception as e: print(f"Error getting coordinates: {e}") return None def get_grid_endpoint(coordinates): """Get the grid endpoint from weather.gov based on coordinates.""" try: lat = coordinates["latitude"] lon = coordinates["longitude"] # Define headers for the API request headers = { "User-Agent": "WeatherAppExample/1.0 (your_email@example.com)", "Accept": "application/geo+json", } # Make the request to get the grid endpoint url = f"https://api.weather.gov/points/{lat},{lon}" response = requests.get(url, headers=headers) if response.status_code == 200: data = response.json() return data["properties"]["forecast"] else: print(f"Error getting grid: {response.status_code} - {response.text}") return None except Exception as e: print(f"Error in get_grid_endpoint: {e}") return None # Weather related function def get_forecast(grid_endpoint): """Get the forecast data from the grid endpoint.""" try: # Define headers for the API request headers = { "User-Agent": "WeatherAppExample/1.0 (your_email@example.com)", "Accept": "application/geo+json", } # Make the request to get the forecast response = requests.get(grid_endpoint, headers=headers) if response.status_code == 200: data = response.json() # Extract the relevant forecast information periods = data["properties"]["periods"] # Process the forecast data into a simpler format forecast = { "location": data["properties"] .get("relativeLocation", {}) .get("properties", {}), "updated": data["properties"].get("updated", ""), "periods": [], } for period in periods: forecast["periods"].append( { "name": period.get("name", ""), "temperature": period.get("temperature", ""), "temperatureUnit": period.get("temperatureUnit", ""), "windSpeed": period.get("windSpeed", ""), "windDirection": period.get("windDirection", ""), "shortForecast": period.get("shortForecast", ""), "detailedForecast": period.get("detailedForecast", ""), } ) return forecast else: print(f"Error getting forecast: {response.status_code} - {response.text}") return {"error": f"API Error: {response.status_code}"} except Exception as e: print(f"Error in get_forecast: {e}") return {"error": f"Exception: {str(e)}"} # Example usage # def do_weather(): # city = input("Enter city: ") # state = input("Enter state: ") # country = input("Enter country (default USA): ") or "USA" # print(f"Getting weather for {city}, {state}, {country}...") # weather_data = WeatherForecast(city, state, country) # if "error" in weather_data: # print(f"Error: {weather_data['error']}") # else: # print("\nWeather Forecast:") # print(f"Location: {weather_data.get('location', {}).get('city', city)}, {weather_data.get('location', {}).get('state', state)}") # print(f"Last Updated: {weather_data.get('updated', 'N/A')}") # print("\nForecast Periods:") # for period in weather_data.get("periods", []): # print(f"\n{period['name']}:") # print(f" Temperature: {period['temperature']}{period['temperatureUnit']}") # print(f" Wind: {period['windSpeed']} {period['windDirection']}") # print(f" Forecast: {period['shortForecast']}") # print(f" Details: {period['detailedForecast']}") # %% def TickerValue(ticker_symbols): api_key = os.getenv("TWELVEDATA_API_KEY", "") if not api_key: return {"error": f"Error fetching data: No API key for TwelveData"} results = [] for ticker_symbol in ticker_symbols.split(","): ticker_symbol = ticker_symbol.strip() if ticker_symbol == "": continue url = ( f"https://api.twelvedata.com/price?symbol={ticker_symbol}&apikey={api_key}" ) response = requests.get(url) data = response.json() if "price" in data: logging.info(f"TwelveData: {ticker_symbol} {data}") results.append({"symbol": ticker_symbol, "price": float(data["price"])}) else: logging.error(f"TwelveData: {data}") results.append({"symbol": ticker_symbol, "price": "Unavailable"}) return results[0] if len(results) == 1 else results # Stock related function def yfTickerValue(ticker_symbols): """ Look up the current price of a stock using its ticker symbol. Args: ticker_symbol (str): The stock ticker symbol (e.g., 'AAPL' for Apple) Returns: dict: Current stock information including price """ results = [] for ticker_symbol in ticker_symbols.split(","): ticker_symbol = ticker_symbol.strip() if ticker_symbol == "": continue # Create a Ticker object try: logging.info(f"Looking up {ticker_symbol}") ticker = yf.Ticker(ticker_symbol) # Get the latest market data ticker_data = ticker.history(period="1d") if ticker_data.empty: results.append({"error": f"No data found for ticker {ticker_symbol}"}) continue # Get the latest closing price latest_price = ticker_data["Close"].iloc[-1] # Get some additional info results.append({"symbol": ticker_symbol, "price": latest_price}) except Exception as e: import traceback logging.error(f"Error fetching data for {ticker_symbol}: {e}") logging.error(traceback.format_exc()) results.append( {"error": f"Error fetching data for {ticker_symbol}: {str(e)}"} ) return results[0] if len(results) == 1 else results # %% def DateTime(timezone="America/Los_Angeles"): """ Returns the current date and time in the specified timezone in ISO 8601 format. Args: timezone (str): Timezone name (e.g., "UTC", "America/New_York", "Europe/London") Default is "America/Los_Angeles" Returns: str: Current date and time with timezone in the format YYYY-MM-DDTHH:MM:SS+HH:MM """ try: if timezone == "system" or timezone == "" or not timezone: timezone = "America/Los_Angeles" # Get current UTC time (timezone-aware) local_tz = pytz.timezone("America/Los_Angeles") local_now = datetime.now(tz=local_tz) # Convert to target timezone target_tz = pytz.timezone(timezone) target_time = local_now.astimezone(target_tz) return target_time.isoformat() except Exception as e: return {"error": f"Invalid timezone {timezone}: {str(e)}"} async def GenerateImage(llm, model: str, prompt: str): return { "image_id": "image-a830a83-bd831" } async def AnalyzeSite(llm, model: str, url: str, question: str): """ Fetches content from a URL, extracts the text, and uses Ollama to summarize it. Args: url (str): The URL of the website to summarize Returns: str: A summary of the website content """ try: # Fetch the webpage headers = { "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" } logging.info(f"Fetching {url}") response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() logging.info(f"{url} returned. Processing...") # Parse the HTML soup = BeautifulSoup(response.text, "html.parser") # Remove script and style elements for script in soup(["script", "style"]): script.extract() # Get text content text = soup.get_text(separator=" ", strip=True) # Clean up text (remove extra whitespace) lines = (line.strip() for line in text.splitlines()) chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) text = " ".join(chunk for chunk in chunks if chunk) # Limit text length if needed (Ollama may have token limits) max_chars = 100000 if len(text) > max_chars: text = text[:max_chars] + "..." # Create Ollama client # logging.info(f"Requesting summary of: {text}") # Generate summary using Ollama prompt = f"CONTENTS:\n\n{text}\n\n{question}" response = llm.generate( model=model, system="You are given the contents of {url}. Answer the question about the contents", prompt=prompt, ) # logging.info(response["response"]) return { "source": "summarizer-llm", "content": response["response"], "metadata": DateTime(), } except requests.exceptions.RequestException as e: logging.error(f"Error fetching the URL: {e}") return f"Error fetching the URL: {str(e)}" except Exception as e: logging.error(f"Error processing the website content: {e}") return f"Error processing the website content: {str(e)}" # %% class Function(BaseModel): name: str description: str parameters: Dict[str, Any] returns: Optional[Dict[str, Any]] = {} class Tool(BaseModel): type: str function: Function tools : List[Tool] = [ # Tool.model_validate({ # "type": "function", # "function": { # "name": "GenerateImage", # "description": """\ # CRITICAL INSTRUCTIONS FOR IMAGE GENERATION: # 1. Call this tool when users request images, drawings, or visual content # 2. This tool returns an image_id (e.g., "img_abc123") # 3. MANDATORY: You must respond with EXACTLY this format: # 4. FORBIDDEN: DO NOT use markdown image syntax ![](url) # 5. FORBIDDEN: DO NOT create fake URLs or file paths # 6. FORBIDDEN: DO NOT use any other image embedding format # CORRECT EXAMPLE: # User: "Draw a cat" # Tool returns: {"image_id": "img_xyz789"} # Your response: "Here's your cat image: " # WRONG EXAMPLES (DO NOT DO THIS): # - ![](https://example.com/...) # - ![Cat image](any_url) # - # The format is the ONLY way to display images in this system. # """, # "parameters": { # "type": "object", # "properties": { # "prompt": { # "type": "string", # "description": "Detailed image description including style, colors, subject, composition" # } # }, # "required": ["prompt"] # }, # "returns": { # "type": "object", # "properties": { # "image_id": { # "type": "string", # "description": "Unique identifier for the generated image. Use this EXACTLY in " # } # } # } # } # }), Tool.model_validate({ "type": "function", "function": { "name": "TickerValue", "description": "Get the current stock price of one or more ticker symbols. Returns an array of objects with 'symbol' and 'price' fields. Call this whenever you need to know the latest value of stock ticker symbols, for example when a user asks 'How much is Intel trading at?' or 'What are the prices of AAPL and MSFT?'", "parameters": { "type": "object", "properties": { "ticker": { "type": "string", "description": "The company stock ticker symbol. For multiple tickers, provide a comma-separated list (e.g., 'AAPL,MSFT,GOOGL').", }, }, "required": ["ticker"], "additionalProperties": False, }, }, }), Tool.model_validate({ "type": "function", "function": { "name": "AnalyzeSite", "description": "Downloads the requested site and asks a second LLM agent to answer the question based on the site content. For example if the user says 'What are the top headlines on cnn.com?' you would use AnalyzeSite to get the answer. Only use this if the user asks about a specific site or company.", "parameters": { "type": "object", "properties": { "url": { "type": "string", "description": "The website URL to download and process", }, "question": { "type": "string", "description": "The question to ask the second LLM about the content", }, }, "required": ["url", "question"], "additionalProperties": False, }, "returns": { "type": "object", "properties": { "source": { "type": "string", "description": "Identifier for the source LLM", }, "content": { "type": "string", "description": "The complete response from the second LLM", }, "metadata": { "type": "object", "description": "Additional information about the response", }, }, }, }, }), Tool.model_validate({ "type": "function", "function": { "name": "DateTime", "description": "Get the current date and time in a specified timezone. For example if a user asks 'What time is it in Poland?' you would pass the Warsaw timezone to DateTime.", "parameters": { "type": "object", "properties": { "timezone": { "type": "string", "description": "Timezone name (e.g., 'UTC', 'America/New_York', 'Europe/London', 'America/Los_Angeles'). Default is 'America/Los_Angeles'.", } }, "required": [], }, }, }), Tool.model_validate({ "type": "function", "function": { "name": "WeatherForecast", "description": "Get the full weather forecast as structured data for a given CITY and STATE location in the United States. For example, if the user asks 'What is the weather in Portland?' or 'What is the forecast for tomorrow?' use the provided data to answer the question.", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "City to find the weather forecast (e.g., 'Portland', 'Seattle').", "minLength": 2, }, "state": { "type": "string", "description": "State to find the weather forecast (e.g., 'OR', 'WA').", "minLength": 2, }, }, "required": ["city", "state"], "additionalProperties": False, }, }, }), ] class ToolEntry(BaseModel): enabled: bool = True tool: Tool def llm_tools(tools: List[ToolEntry]) -> List[Dict[str, Any]]: return [entry.tool.model_dump(mode='json') for entry in tools if entry.enabled == True] def all_tools() -> List[ToolEntry]: return [ToolEntry(tool=tool) for tool in tools] def enabled_tools(tools: List[ToolEntry]) -> List[ToolEntry]: return [ToolEntry(tool=entry.tool) for entry in tools if entry.enabled == True] tool_functions = ["DateTime", "WeatherForecast", "TickerValue", "AnalyzeSite", "GenerateImage"] __all__ = ["ToolEntry", "all_tools", "llm_tools", "enabled_tools", "tool_functions"] # __all__.extend(__tool_functions__) # type: ignore