| … | … | @@ -0,0 +1,100 @@ |
| 1 | """ | |
| 2 | Example demonstrating the use of web tools with permission system in OllamaTools. | |
| 3 | ||
| 4 | This script shows how to securely use the web tools with the permission system. | |
| 5 | """ | |
| 6 | ||
| 7 | from ollamatools.tools.web_tools import fetch_url, get_weather, search_web | |
| 8 | from ollamatools.security import PermissionManager | |
| 9 | import json | |
| 10 | ||
| 11 | def main(): | |
| 12 | """Run the secure web tools example.""" | |
| 13 | print("OllamaTools Secure Web Tools Example") | |
| 14 | print("===================================") | |
| 15 | ||
| 16 | # Create a permission manager with prompting enabled | |
| 17 | permission_manager = PermissionManager(prompt_enabled=True) | |
| 18 | print("\nCreated permission manager with prompting enabled.") | |
| 19 | print("You will be prompted for permission before each web request.") | |
| 20 | ||
| 21 | # Example 1: Basic fetch_url with permission | |
| 22 | print("\n1. Secure URL Fetch") | |
| 23 | print("-----------------") | |
| 24 | try: | |
| 25 | print("Attempting to fetch https://httpbin.org/get...") | |
| 26 | result = fetch_url( | |
| 27 | "https://httpbin.org/get", | |
| 28 | permission_manager=permission_manager | |
| 29 | ) | |
| 30 | print(f"Request successful! Status: {result['status']}") | |
| 31 | print(f"Response content type: {result['headers'].get('Content-Type', 'Unknown')}") | |
| 32 | print(f"Response URL: {result['url']}") | |
| 33 | except PermissionError as e: | |
| 34 | print(f"Permission denied: {e}") | |
| 35 | except Exception as e: | |
| 36 | print(f"Error: {e}") | |
| 37 | ||
| 38 | # Example 2: fetch_url to a different domain | |
| 39 | print("\n2. Fetch from a Different Domain") | |
| 40 | print("------------------------------") | |
| 41 | try: | |
| 42 | print("Attempting to fetch https://example.com...") | |
| 43 | result = fetch_url( | |
| 44 | "https://example.com", | |
| 45 | permission_manager=permission_manager | |
| 46 | ) | |
| 47 | print(f"Request successful! Status: {result['status']}") | |
| 48 | print(f"Response content type: {result['headers'].get('Content-Type', 'Unknown')}") | |
| 49 | print(f"Response content length: {len(str(result['content']))} characters") | |
| 50 | except PermissionError as e: | |
| 51 | print(f"Permission denied: {e}") | |
| 52 | except Exception as e: | |
| 53 | print(f"Error: {e}") | |
| 54 | ||
| 55 | # Example 3: Search web with permission | |
| 56 | print("\n3. Secure Web Search") | |
| 57 | print("-----------------") | |
| 58 | try: | |
| 59 | print("Attempting to search for 'Python programming'...") | |
| 60 | results = search_web( | |
| 61 | "Python programming", | |
| 62 | num_results=3, | |
| 63 | permission_manager=permission_manager | |
| 64 | ) | |
| 65 | print(f"Search successful! Found {len(results)} results") | |
| 66 | for i, result in enumerate(results[:2]): # Show first 2 results | |
| 67 | print(f"\nResult {i+1}:") | |
| 68 | print(f" Title: {result['title']}") | |
| 69 | print(f" URL: {result['url']}") | |
| 70 | except PermissionError as e: | |
| 71 | print(f"Permission denied: {e}") | |
| 72 | except Exception as e: | |
| 73 | print(f"Error: {e}") | |
| 74 | ||
| 75 | # Example 4: POST request with permission | |
| 76 | print("\n4. Secure POST Request") | |
| 77 | print("--------------------") | |
| 78 | try: | |
| 79 | print("Attempting to make a POST request to https://httpbin.org/post...") | |
| 80 | data = {"name": "OllamaTools", "purpose": "security_demo"} | |
| 81 | result = fetch_url( | |
| 82 | "https://httpbin.org/post", | |
| 83 | method="POST", | |
| 84 | data=data, | |
| 85 | permission_manager=permission_manager | |
| 86 | ) | |
| 87 | print(f"Request successful! Status: {result['status']}") | |
| 88 | print(f"Server received our data: {json.dumps(result['content'].get('json', {}))}") | |
| 89 | except PermissionError as e: | |
| 90 | print(f"Permission denied: {e}") | |
| 91 | except Exception as e: | |
| 92 | print(f"Error: {e}") | |
| 93 | ||
| 94 | print("\nExample completed. The permission system allows users to control which") | |
| 95 | print("web requests are allowed, providing an additional layer of security.") | |
| 96 | print("This can be especially important when functions are automatically triggered") | |
| 97 | print("by an AI model.") | |
| 98 | ||
| 99 | if __name__ == "__main__": | |
| 100 | main() |
| … | … | @@ -0,0 +1,75 @@ |
| 1 | """ | |
| 2 | Example demonstrating the use of web tools in OllamaTools. | |
| 3 | ||
| 4 | This script shows how to make web requests using the fetch_url function. | |
| 5 | """ | |
| 6 | ||
| 7 | from ollamatools.tools.web_tools import fetch_url | |
| 8 | from ollamatools.security import PermissionManager | |
| 9 | import json | |
| 10 | ||
| 11 | def main(): | |
| 12 | """Run the example.""" | |
| 13 | print("OllamaTools Web Request Example") | |
| 14 | print("===============================") | |
| 15 | ||
| 16 | # Create a permission manager | |
| 17 | permission_manager = PermissionManager(prompt_enabled=True) | |
| 18 | ||
| 19 | print("\n1. Simple GET request") | |
| 20 | try: | |
| 21 | result = fetch_url( | |
| 22 | "https://httpbin.org/get", | |
| 23 | permission_manager=permission_manager | |
| 24 | ) | |
| 25 | print(f"Status: {result['status']}") | |
| 26 | print(f"Response content type: {result['headers'].get('Content-Type', 'Unknown')}") | |
| 27 | print(f"Response snippet: {json.dumps(result['content'], indent=2)[:200]}...") | |
| 28 | except Exception as e: | |
| 29 | print(f"Error: {e}") | |
| 30 | ||
| 31 | print("\n2. POST request with data") | |
| 32 | try: | |
| 33 | post_data = {"name": "OllamaTools", "version": "0.1.0"} | |
| 34 | result = fetch_url( | |
| 35 | "https://httpbin.org/post", | |
| 36 | method="POST", | |
| 37 | data=post_data, | |
| 38 | permission_manager=permission_manager | |
| 39 | ) | |
| 40 | print(f"Status: {result['status']}") | |
| 41 | print(f"Response content type: {result['headers'].get('Content-Type', 'Unknown')}") | |
| 42 | print(f"Response snippet: {json.dumps(result['content'], indent=2)[:200]}...") | |
| 43 | except Exception as e: | |
| 44 | print(f"Error: {e}") | |
| 45 | ||
| 46 | print("\n3. GET request with parameters") | |
| 47 | try: | |
| 48 | params = {"query": "ollama", "limit": "5"} | |
| 49 | result = fetch_url( | |
| 50 | "https://httpbin.org/get", | |
| 51 | params=params, | |
| 52 | permission_manager=permission_manager | |
| 53 | ) | |
| 54 | print(f"Status: {result['status']}") | |
| 55 | print(f"Response content type: {result['headers'].get('Content-Type', 'Unknown')}") | |
| 56 | print(f"Response snippet: {json.dumps(result['content'], indent=2)[:200]}...") | |
| 57 | except Exception as e: | |
| 58 | print(f"Error: {e}") | |
| 59 | ||
| 60 | print("\n4. Request with custom headers") | |
| 61 | try: | |
| 62 | headers = {"X-API-Key": "demo-key", "User-Agent": "OllamaTools/0.1.0"} | |
| 63 | result = fetch_url( | |
| 64 | "https://httpbin.org/headers", | |
| 65 | headers=headers, | |
| 66 | permission_manager=permission_manager | |
| 67 | ) | |
| 68 | print(f"Status: {result['status']}") | |
| 69 | print(f"Response content type: {result['headers'].get('Content-Type', 'Unknown')}") | |
| 70 | print(f"Response snippet: {json.dumps(result['content'], indent=2)[:200]}...") | |
| 71 | except Exception as e: | |
| 72 | print(f"Error: {e}") | |
| 73 | ||
| 74 | if __name__ == "__main__": | |
| 75 | main() |
| … | … | @@ -0,0 +1,105 @@ |
| 1 | """ | |
| 2 | Examples demonstrating the use of advanced web tools in OllamaTools. | |
| 3 | ||
| 4 | This script shows how to use the get_weather and search_web functions. | |
| 5 | """ | |
| 6 | ||
| 7 | from ollamatools.tools.web_tools import fetch_url, get_weather, search_web | |
| 8 | from ollamatools.security import PermissionManager | |
| 9 | import json | |
| 10 | ||
| 11 | def main(): | |
| 12 | """Run the example.""" | |
| 13 | print("OllamaTools Advanced Web Tools Example") | |
| 14 | print("=====================================") | |
| 15 | ||
| 16 | # Create a permission manager | |
| 17 | permission_manager = PermissionManager(prompt_enabled=True) | |
| 18 | ||
| 19 | # Example 1: Get Weather | |
| 20 | print("\n1. Weather Information") | |
| 21 | print("---------------------") | |
| 22 | ||
| 23 | try: | |
| 24 | # Replace 'YOUR_API_KEY' with an actual OpenWeatherMap API key | |
| 25 | api_key = "YOUR_API_KEY" | |
| 26 | ||
| 27 | # Skip this example if no API key is provided | |
| 28 | if api_key == "YOUR_API_KEY": | |
| 29 | print("Skipping weather example - Please add your OpenWeatherMap API key to use this example.") | |
| 30 | else: | |
| 31 | print("Getting weather for London...") | |
| 32 | weather_info = get_weather( | |
| 33 | "London", | |
| 34 | days=3, | |
| 35 | units="metric", | |
| 36 | api_key=api_key, | |
| 37 | permission_manager=permission_manager | |
| 38 | ) | |
| 39 | ||
| 40 | # Display current weather | |
| 41 | print(f"Current weather in {weather_info['location']['name']}, {weather_info['location']['country']}:") | |
| 42 | print(f" Temperature: {weather_info['current']['temperature']}°C") | |
| 43 | print(f" Description: {weather_info['current']['description']}") | |
| 44 | print(f" Humidity: {weather_info['current']['humidity']}%") | |
| 45 | ||
| 46 | # Display forecast summary | |
| 47 | print("\nForecast:") | |
| 48 | for i, item in enumerate(weather_info['forecast'][:3]): # Show first 3 forecast items | |
| 49 | print(f" Item {i+1}: {item['temperature']}°C, {item['description']}") | |
| 50 | ||
| 51 | except Exception as e: | |
| 52 | print(f"Error getting weather: {e}") | |
| 53 | ||
| 54 | # Example 2: Web Search | |
| 55 | print("\n2. Web Search") | |
| 56 | print("------------") | |
| 57 | ||
| 58 | try: | |
| 59 | # Replace 'YOUR_API_KEY' with an actual Search API key if you have one | |
| 60 | # This example will use the fallback search method if no API key is provided | |
| 61 | api_key = "YOUR_API_KEY" | |
| 62 | ||
| 63 | print("Searching for 'Python programming'...") | |
| 64 | search_results = search_web( | |
| 65 | "Python programming", | |
| 66 | num_results=3, | |
| 67 | api_key=api_key if api_key != "YOUR_API_KEY" else None, | |
| 68 | permission_manager=permission_manager | |
| 69 | ) | |
| 70 | ||
| 71 | # Display search results | |
| 72 | print(f"Found {len(search_results)} results:") | |
| 73 | for i, result in enumerate(search_results): | |
| 74 | print(f"\nResult {i+1}:") | |
| 75 | print(f" Title: {result['title']}") | |
| 76 | print(f" URL: {result['url']}") | |
| 77 | print(f" Snippet: {result['snippet'][:100]}...") | |
| 78 | ||
| 79 | except Exception as e: | |
| 80 | print(f"Error searching web: {e}") | |
| 81 | ||
| 82 | # Example 3: Combined Usage | |
| 83 | print("\n3. Combined Usage Example") | |
| 84 | print("-----------------------") | |
| 85 | ||
| 86 | try: | |
| 87 | print("This example would fetch weather data for a city from a search result.") | |
| 88 | print("(Skipped in this demo to avoid API key requirements)") | |
| 89 | ||
| 90 | # In a real implementation, you might do something like: | |
| 91 | # 1. Search for "best cities to visit in Europe" | |
| 92 | # 2. Extract a city from the results | |
| 93 | # 3. Get weather for that city | |
| 94 | ||
| 95 | print("\nPseudo-code:") | |
| 96 | print(" results = search_web('best cities to visit in Europe')") | |
| 97 | print(" city = extract_city_from_results(results)") | |
| 98 | print(" weather = get_weather(city)") | |
| 99 | print(" print(f'Weather in {city}: {weather['current']['temperature']}°C')") | |
| 100 | ||
| 101 | except Exception as e: | |
| 102 | print(f"Error in combined example: {e}") | |
| 103 | ||
| 104 | if __name__ == "__main__": | |
| 105 | main() |
| … | … | @@ -1,23 +1,28 @@ |
| 1 | 1 | """ |
| 2 | OllamaTools function libraries. | |
| 2 | Tools package for OllamaTools. | |
| 3 | 3 | |
| 4 | This package contains various function libraries that can be used | |
| 5 | with the OllamaAgent for semantic function detection and execution. | |
| 4 | This package provides various tools for the OllamaTools library, including: | |
| 5 | - System tools: Execute commands and file operations | |
| 6 | - Web tools: Make web requests and process responses | |
| 7 | - Documentation tools: Generate and process documentation | |
| 6 | 8 | """ |
| 7 | 9 | |
| 8 | 10 | # Import and expose system tools |
| 9 | from ollamatools.tools.system_tools import ( | |
| 10 | execute_command, | |
| 11 | read_file, | |
| 12 | write_file, | |
| 13 | find_files | |
| 14 | ) | |
| 11 | from .system_tools import execute_command, read_file, write_file, find_files | |
| 15 | 12 | |
| 16 | # Define which functions should be exposed at the package level | |
| 13 | # Import and expose web tools | |
| 14 | from .web_tools import fetch_url, get_weather, search_web | |
| 15 | ||
| 16 | # Export all tools | |
| 17 | 17 | __all__ = [ |
| 18 | "execute_command", | |
| 19 | "read_file", | |
| 20 | "write_file", | |
| 21 | "find_files" | |
| 18 | # System tools | |
| 19 | 'execute_command', | |
| 20 | 'read_file', | |
| 21 | 'write_file', | |
| 22 | 'find_files', | |
| 23 | ||
| 24 | # Web tools | |
| 25 | 'fetch_url', | |
| 26 | 'get_weather', | |
| 27 | 'search_web', | |
| 22 | 28 | ] |
| 23 |
| … | … | @@ -1,11 +1,19 @@ |
| 1 | 1 | """ |
| 2 | Web tools for OllamaTools. | |
| 2 | Web tools module for OllamaTools. | |
| 3 | 3 | |
| 4 | This module contains functions for making web requests and processing responses. | |
| 4 | This module provides functions for making web requests and processing responses, | |
| 5 | including weather information fetching and web search functionality. | |
| 5 | 6 | """ |
| 6 | 7 | |
| 7 | from typing import Optional, Dict, Any | |
| 8 | import requests | |
| 9 | from typing import Dict, Any, Optional, Union, List | |
| 10 | import logging | |
| 11 | import json | |
| 12 | from urllib.parse import urlparse, quote | |
| 13 | from bs4 import BeautifulSoup | |
| 8 | 14 | |
| 15 | # Get logger | |
| 16 | logger = logging.getLogger(__name__) | |
| 9 | 17 | |
| 10 | 18 | def fetch_url( |
| 11 | 19 | url: str, |
| … | … | @@ -13,25 +21,358 @@ def fetch_url( |
| 13 | 21 | params: Optional[Dict[str, str]] = None, |
| 14 | 22 | headers: Optional[Dict[str, str]] = None, |
| 15 | 23 | data: Optional[Dict[str, Any]] = None, |
| 16 | timeout: int = 30 | |
| 24 | timeout: int = 30, | |
| 25 | permission_manager = None | |
| 17 | 26 | ) -> Dict[str, Any]: |
| 18 | 27 | """ |
| 19 | 28 | Fetch a URL with specified parameters. |
| 20 | 29 | |
| 21 | 30 | Args: |
| 22 | 31 | url: URL to fetch |
| 23 | method: HTTP method | |
| 32 | method: HTTP method (GET, POST, PUT, DELETE, etc.) | |
| 24 | 33 | params: URL parameters |
| 25 | 34 | headers: HTTP headers |
| 26 | 35 | data: Request data |
| 27 | 36 | timeout: Request timeout in seconds |
| 37 | permission_manager: Optional permission manager to request permission | |
| 28 | 38 | |
| 29 | 39 | Returns: |
| 30 | 40 | dict: Response with status, headers, and content |
| 41 | ||
| 42 | Raises: | |
| 43 | ValueError: If URL or method is invalid | |
| 44 | requests.RequestException: If the request fails | |
| 45 | PermissionError: If permission is denied | |
| 46 | """ | |
| 47 | # Validate URL | |
| 48 | if not url: | |
| 49 | raise ValueError("URL cannot be empty") | |
| 50 | ||
| 51 | # Basic URL validation | |
| 52 | parsed_url = urlparse(url) | |
| 53 | if not parsed_url.scheme or not parsed_url.netloc: | |
| 54 | raise ValueError(f"Invalid URL: {url}") | |
| 55 | ||
| 56 | # Validate method | |
| 57 | valid_methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] | |
| 58 | method = method.upper() | |
| 59 | if method not in valid_methods: | |
| 60 | raise ValueError(f"Invalid HTTP method: {method}. Must be one of {valid_methods}") | |
| 61 | ||
| 62 | # Request permission if permission manager is provided | |
| 63 | if permission_manager: | |
| 64 | args = { | |
| 65 | "url": url, | |
| 66 | "method": method, | |
| 67 | "params": params, | |
| 68 | "headers": headers, | |
| 69 | "data": data | |
| 70 | } | |
| 71 | ||
| 72 | if not permission_manager.request_permission("fetch_url", args): | |
| 73 | logger.warning(f"Permission denied for web request to {url}") | |
| 74 | raise PermissionError(f"Permission denied for web request to {url}") | |
| 75 | ||
| 76 | logger.info(f"Making {method} request to {url}") | |
| 77 | ||
| 78 | try: | |
| 79 | response = requests.request( | |
| 80 | method=method, | |
| 81 | url=url, | |
| 82 | params=params, | |
| 83 | headers=headers, | |
| 84 | json=data if method in ['POST', 'PUT', 'PATCH'] else None, | |
| 85 | timeout=timeout | |
| 86 | ) | |
| 87 | ||
| 88 | # Try to parse response as JSON, fall back to text | |
| 89 | content_type = response.headers.get('Content-Type', '') | |
| 90 | if 'application/json' in content_type: | |
| 91 | try: | |
| 92 | content = response.json() | |
| 93 | except ValueError: | |
| 94 | content = response.text | |
| 95 | else: | |
| 96 | content = response.text | |
| 97 | ||
| 98 | result = { | |
| 99 | "status": response.status_code, | |
| 100 | "headers": dict(response.headers), | |
| 101 | "content": content, | |
| 102 | "url": response.url | |
| 103 | } | |
| 104 | ||
| 105 | logger.debug(f"Request to {url} completed with status {response.status_code}") | |
| 106 | return result | |
| 107 | ||
| 108 | except requests.RequestException as e: | |
| 109 | logger.error(f"Request to {url} failed: {str(e)}") | |
| 110 | raise | |
| 111 | ||
| 112 | ||
| 113 | def get_weather( | |
| 114 | location: str, | |
| 115 | days: int = 3, | |
| 116 | units: str = 'metric', | |
| 117 | api_key: Optional[str] = None, | |
| 118 | permission_manager = None | |
| 119 | ) -> Dict[str, Any]: | |
| 120 | """ | |
| 121 | Get weather information for a location. | |
| 122 | ||
| 123 | Args: | |
| 124 | location: Location name or coordinates | |
| 125 | days: Number of forecast days (1-7) | |
| 126 | units: Temperature units ('metric' or 'imperial') | |
| 127 | api_key: OpenWeatherMap API key (if None, uses config) | |
| 128 | permission_manager: Optional permission manager to request permission | |
| 129 | ||
| 130 | Returns: | |
| 131 | dict: Weather information including current conditions and forecast | |
| 132 | ||
| 133 | Raises: | |
| 134 | ValueError: If parameters are invalid | |
| 135 | requests.RequestException: If the API request fails | |
| 136 | PermissionError: If permission is denied | |
| 137 | """ | |
| 138 | # Validate parameters | |
| 139 | if not location: | |
| 140 | raise ValueError("Location cannot be empty") | |
| 141 | ||
| 142 | if days < 1 or days > 7: | |
| 143 | raise ValueError("Days must be between 1 and 7") | |
| 144 | ||
| 145 | valid_units = ['metric', 'imperial'] | |
| 146 | if units not in valid_units: | |
| 147 | raise ValueError(f"Units must be one of {valid_units}") | |
| 148 | ||
| 149 | # Request permission if permission manager is provided | |
| 150 | if permission_manager: | |
| 151 | args = { | |
| 152 | "location": location, | |
| 153 | "days": days, | |
| 154 | "units": units | |
| 155 | } | |
| 156 | ||
| 157 | if not permission_manager.request_permission("get_weather", args): | |
| 158 | logger.warning(f"Permission denied for weather request for {location}") | |
| 159 | raise PermissionError(f"Permission denied for weather request for {location}") | |
| 160 | ||
| 161 | # Check if API key is provided | |
| 162 | if api_key is None: | |
| 163 | # In a real implementation, this would get the API key from the config | |
| 164 | # For now, we'll use a placeholder message | |
| 165 | logger.error("No API key provided for weather API") | |
| 166 | raise ValueError("Weather API key is required. Please provide an API key or set it in the configuration.") | |
| 167 | ||
| 168 | logger.info(f"Fetching weather for {location}") | |
| 169 | ||
| 170 | try: | |
| 171 | # Base URL for OpenWeatherMap API | |
| 172 | base_url = "https://api.openweathermap.org/data/2.5" | |
| 173 | ||
| 174 | # Get current weather | |
| 175 | current_url = f"{base_url}/weather" | |
| 176 | params = { | |
| 177 | "q": location, | |
| 178 | "appid": api_key, | |
| 179 | "units": units | |
| 180 | } | |
| 181 | ||
| 182 | current_response = requests.get(current_url, params=params, timeout=10) | |
| 183 | current_response.raise_for_status() | |
| 184 | current_data = current_response.json() | |
| 185 | ||
| 186 | # Get forecast | |
| 187 | forecast_url = f"{base_url}/forecast" | |
| 188 | params = { | |
| 189 | "q": location, | |
| 190 | "appid": api_key, | |
| 191 | "units": units, | |
| 192 | "cnt": days * 8 # 8 forecasts per day (every 3 hours) | |
| 193 | } | |
| 194 | ||
| 195 | forecast_response = requests.get(forecast_url, params=params, timeout=10) | |
| 196 | forecast_response.raise_for_status() | |
| 197 | forecast_data = forecast_response.json() | |
| 198 | ||
| 199 | # Format the response | |
| 200 | weather_info = { | |
| 201 | "location": { | |
| 202 | "name": current_data.get("name", ""), | |
| 203 | "country": current_data.get("sys", {}).get("country", ""), | |
| 204 | "coordinates": current_data.get("coord", {}) | |
| 205 | }, | |
| 206 | "current": { | |
| 207 | "temperature": current_data.get("main", {}).get("temp"), | |
| 208 | "feels_like": current_data.get("main", {}).get("feels_like"), | |
| 209 | "humidity": current_data.get("main", {}).get("humidity"), | |
| 210 | "pressure": current_data.get("main", {}).get("pressure"), | |
| 211 | "wind_speed": current_data.get("wind", {}).get("speed"), | |
| 212 | "wind_direction": current_data.get("wind", {}).get("deg"), | |
| 213 | "description": current_data.get("weather", [{}])[0].get("description", ""), | |
| 214 | "icon": current_data.get("weather", [{}])[0].get("icon", "") | |
| 215 | }, | |
| 216 | "forecast": [] | |
| 217 | } | |
| 218 | ||
| 219 | # Process forecast data | |
| 220 | for item in forecast_data.get("list", [])[:days * 8]: | |
| 221 | forecast_item = { | |
| 222 | "timestamp": item.get("dt"), | |
| 223 | "temperature": item.get("main", {}).get("temp"), | |
| 224 | "feels_like": item.get("main", {}).get("feels_like"), | |
| 225 | "humidity": item.get("main", {}).get("humidity"), | |
| 226 | "pressure": item.get("main", {}).get("pressure"), | |
| 227 | "wind_speed": item.get("wind", {}).get("speed"), | |
| 228 | "wind_direction": item.get("wind", {}).get("deg"), | |
| 229 | "description": item.get("weather", [{}])[0].get("description", ""), | |
| 230 | "icon": item.get("weather", [{}])[0].get("icon", "") | |
| 231 | } | |
| 232 | weather_info["forecast"].append(forecast_item) | |
| 233 | ||
| 234 | return weather_info | |
| 235 | ||
| 236 | except requests.RequestException as e: | |
| 237 | logger.error(f"Weather API request failed: {str(e)}") | |
| 238 | raise | |
| 239 | ||
| 240 | ||
| 241 | def search_web( | |
| 242 | query: str, | |
| 243 | num_results: int = 5, | |
| 244 | api_key: Optional[str] = None, | |
| 245 | permission_manager = None | |
| 246 | ) -> List[Dict[str, str]]: | |
| 247 | """ | |
| 248 | Search the web for a query. | |
| 249 | ||
| 250 | Args: | |
| 251 | query: Search query | |
| 252 | num_results: Number of results to return (1-10) | |
| 253 | api_key: Search API key (if None, uses config) | |
| 254 | permission_manager: Optional permission manager to request permission | |
| 255 | ||
| 256 | Returns: | |
| 257 | List[dict]: Search results with title, URL, and snippet | |
| 258 | ||
| 259 | Raises: | |
| 260 | ValueError: If parameters are invalid | |
| 261 | requests.RequestException: If the API request fails | |
| 262 | PermissionError: If permission is denied | |
| 263 | """ | |
| 264 | # Validate parameters | |
| 265 | if not query: | |
| 266 | raise ValueError("Query cannot be empty") | |
| 267 | ||
| 268 | if num_results < 1 or num_results > 10: | |
| 269 | raise ValueError("Number of results must be between 1 and 10") | |
| 270 | ||
| 271 | # Request permission if permission manager is provided | |
| 272 | if permission_manager: | |
| 273 | args = { | |
| 274 | "query": query, | |
| 275 | "num_results": num_results | |
| 276 | } | |
| 277 | ||
| 278 | if not permission_manager.request_permission("search_web", args): | |
| 279 | logger.warning(f"Permission denied for web search: {query}") | |
| 280 | raise PermissionError(f"Permission denied for web search: {query}") | |
| 281 | ||
| 282 | # Check if API key is provided | |
| 283 | if api_key is None: | |
| 284 | # In a real implementation, this would get the API key from the config | |
| 285 | # For now, we'll use a fallback to a simple search method that doesn't require API key | |
| 286 | logger.warning("No API key provided for search API, using fallback method") | |
| 287 | return _fallback_search(query, num_results) | |
| 288 | ||
| 289 | logger.info(f"Searching the web for: {query}") | |
| 290 | ||
| 291 | try: | |
| 292 | # Example using the Serpapi (Google Search API) | |
| 293 | # In a real implementation, you would use a proper search API | |
| 294 | url = "https://serpapi.com/search" | |
| 295 | params = { | |
| 296 | "q": query, | |
| 297 | "api_key": api_key, | |
| 298 | "num": num_results | |
| 299 | } | |
| 300 | ||
| 301 | response = requests.get(url, params=params, timeout=15) | |
| 302 | response.raise_for_status() | |
| 303 | ||
| 304 | data = response.json() | |
| 305 | results = [] | |
| 306 | ||
| 307 | # Process search results | |
| 308 | for item in data.get("organic_results", [])[:num_results]: | |
| 309 | result = { | |
| 310 | "title": item.get("title", ""), | |
| 311 | "url": item.get("link", ""), | |
| 312 | "snippet": item.get("snippet", "") | |
| 313 | } | |
| 314 | results.append(result) | |
| 315 | ||
| 316 | return results | |
| 317 | ||
| 318 | except requests.RequestException as e: | |
| 319 | logger.error(f"Search API request failed: {str(e)}") | |
| 320 | ||
| 321 | # Fall back to the simple search method if API request fails | |
| 322 | logger.info("Falling back to simple search method") | |
| 323 | return _fallback_search(query, num_results) | |
| 324 | ||
| 325 | ||
| 326 | def _fallback_search(query: str, num_results: int = 5) -> List[Dict[str, str]]: | |
| 327 | """ | |
| 328 | Fallback search method using DuckDuckGo HTML scraping when API is not available. | |
| 329 | ||
| 330 | Note: This is a fallback method and not recommended for production use. | |
| 331 | It may break if the search engine changes its HTML structure. | |
| 332 | ||
| 333 | Args: | |
| 334 | query: Search query | |
| 335 | num_results: Number of results to return | |
| 336 | ||
| 337 | Returns: | |
| 338 | List[dict]: Search results with title, URL, and snippet | |
| 31 | 339 | """ |
| 32 | # Placeholder implementation | |
| 33 | return { | |
| 34 | "status": 200, | |
| 35 | "headers": {}, | |
| 36 | "content": "URL fetch placeholder" | |
| 37 | } | |
| 340 | results = [] | |
| 341 | encoded_query = quote(query) | |
| 342 | url = f"https://html.duckduckgo.com/html/?q={encoded_query}" | |
| 343 | ||
| 344 | try: | |
| 345 | headers = { | |
| 346 | "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" | |
| 347 | } | |
| 348 | response = requests.get(url, headers=headers, timeout=15) | |
| 349 | response.raise_for_status() | |
| 350 | ||
| 351 | soup = BeautifulSoup(response.text, 'html.parser') | |
| 352 | ||
| 353 | # Find search results | |
| 354 | result_elements = soup.select('.result') | |
| 355 | ||
| 356 | for element in result_elements[:num_results]: | |
| 357 | title_element = element.select_one('.result__title') | |
| 358 | link_element = element.select_one('.result__url') | |
| 359 | snippet_element = element.select_one('.result__snippet') | |
| 360 | ||
| 361 | if title_element and link_element: | |
| 362 | title = title_element.get_text(strip=True) | |
| 363 | # The actual URL is in the href attribute of the <a> tag | |
| 364 | link = title_element.find('a').get('href', '') | |
| 365 | snippet = snippet_element.get_text(strip=True) if snippet_element else "" | |
| 366 | ||
| 367 | results.append({ | |
| 368 | "title": title, | |
| 369 | "url": link, | |
| 370 | "snippet": snippet | |
| 371 | }) | |
| 372 | ||
| 373 | return results | |
| 374 | ||
| 375 | except (requests.RequestException, Exception) as e: | |
| 376 | logger.error(f"Fallback search failed: {str(e)}") | |
| 377 | # Return empty results if even the fallback fails | |
| 378 | return [] |
| … | … | @@ -1,7 +1,13 @@ |
| 1 | ollama-python>=0.4.0 | |
| 1 | # Core dependencies | |
| 2 | 2 | requests>=2.28.0 |
| 3 | 3 | beautifulsoup4>=4.11.0 |
| 4 | 4 | pydantic>=1.10.0 |
| 5 | 5 | markdown>=3.4.0 |
| 6 | 6 | typing-extensions>=4.4.0 |
| 7 | ||
| 8 | # Test dependencies | |
| 7 | 9 | pytest>=7.0.0 |
| 10 | ||
| 11 | # Ollama client dependency - uncomment and use the correct one based on your system | |
| 12 | # ollama>=0.4.0 | |
| 13 | # ollama-python>=0.1.2 |
| … | … | @@ -1,5 +1,5 @@ |
| 1 | 1 | """ |
| 2 | Setup script for OllamaTools. | |
| 2 | Setup script for the OllamaTools package. | |
| 3 | 3 | """ |
| 4 | 4 | |
| 5 | 5 | from setuptools import setup, find_packages |
| … | … | @@ -9,36 +9,28 @@ setup( |
| 9 | 9 | version="0.1.0", |
| 10 | 10 | packages=find_packages(), |
| 11 | 11 | install_requires=[ |
| 12 | "ollama-python>=0.4.0", | |
| 13 | 12 | "requests>=2.28.0", |
| 14 | 13 | "beautifulsoup4>=4.11.0", |
| 15 | 14 | "pydantic>=1.10.0", |
| 16 | 15 | "markdown>=3.4.0", |
| 17 | 16 | "typing-extensions>=4.4.0", |
| 17 | # Ollama client will be added later once we determine which package to use | |
| 18 | 18 | ], |
| 19 | 19 | extras_require={ |
| 20 | 20 | "dev": [ |
| 21 | 21 | "pytest>=7.0.0", |
| 22 | "pytest-cov>=2.12.1", | |
| 23 | "black>=22.3.0", | |
| 24 | "isort>=5.10.1", | |
| 25 | "flake8>=4.0.1", | |
| 26 | 22 | ], |
| 27 | 23 | }, |
| 28 | python_requires=">=3.8", | |
| 29 | author="Your Name", | |
| 30 | author_email="your.email@example.com", | |
| 24 | author="OllamaTools Team", | |
| 25 | author_email="example@example.com", | |
| 31 | 26 | description="Function Library for Ollama LLMs", |
| 32 | 27 | long_description=open("README.md").read(), |
| 33 | 28 | long_description_content_type="text/markdown", |
| 34 | url="https://github.com/yourusername/ollamatools", | |
| 29 | url="https://github.com/example/ollamatools", | |
| 35 | 30 | classifiers=[ |
| 36 | "Development Status :: 3 - Alpha", | |
| 37 | "Intended Audience :: Developers", | |
| 38 | "License :: OSI Approved :: MIT License", | |
| 39 | 31 | "Programming Language :: Python :: 3", |
| 40 | "Programming Language :: Python :: 3.8", | |
| 41 | "Programming Language :: Python :: 3.9", | |
| 42 | "Programming Language :: Python :: 3.10", | |
| 32 | "License :: OSI Approved :: MIT License", | |
| 33 | "Operating System :: OS Independent", | |
| 43 | 34 | ], |
| 35 | python_requires=">=3.8", | |
| 44 | 36 | ) |
| … | … | @@ -0,0 +1,115 @@ |
| 1 | """ | |
| 2 | Simple script to manually test the web tools functionality. | |
| 3 | """ | |
| 4 | ||
| 5 | import sys | |
| 6 | import os | |
| 7 | from unittest.mock import MagicMock | |
| 8 | ||
| 9 | # Ensure the project directory is in the Python path | |
| 10 | project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| 11 | if project_root not in sys.path: | |
| 12 | sys.path.insert(0, project_root) | |
| 13 | ||
| 14 | # Import the web tools | |
| 15 | from ollamatools.tools.web_tools import fetch_url, get_weather, search_web | |
| 16 | ||
| 17 | def test_fetch_url(): | |
| 18 | """Test the fetch_url function.""" | |
| 19 | print("\nTesting fetch_url...") | |
| 20 | ||
| 21 | # Test a simple GET request | |
| 22 | try: | |
| 23 | result = fetch_url("https://httpbin.org/get") | |
| 24 | print(f"Status: {result['status']}") | |
| 25 | print(f"Content type: {result['headers'].get('Content-Type')}") | |
| 26 | print("Test passed!") | |
| 27 | except Exception as e: | |
| 28 | print(f"Test failed: {e}") | |
| 29 | ||
| 30 | def test_search_web(): | |
| 31 | """Test the search_web function.""" | |
| 32 | print("\nTesting search_web...") | |
| 33 | ||
| 34 | # Test web search with fallback method (no API key) | |
| 35 | try: | |
| 36 | results = search_web("Python programming language") | |
| 37 | print(f"Found {len(results)} results") | |
| 38 | if results: | |
| 39 | print(f"First result title: {results[0]['title']}") | |
| 40 | print("Test passed!") | |
| 41 | except Exception as e: | |
| 42 | print(f"Test failed: {e}") | |
| 43 | ||
| 44 | def test_get_weather_mock(): | |
| 45 | """Test the get_weather function with mocked permission manager.""" | |
| 46 | print("\nTesting get_weather permission handling...") | |
| 47 | ||
| 48 | # Create a mock permission manager | |
| 49 | mock_permission_manager = MagicMock() | |
| 50 | ||
| 51 | # Test 1: Permission granted | |
| 52 | mock_permission_manager.request_permission.return_value = True | |
| 53 | ||
| 54 | try: | |
| 55 | # This should still fail because we don't have a valid API key, | |
| 56 | # but it should get past the permission check | |
| 57 | try: | |
| 58 | get_weather( | |
| 59 | "London", | |
| 60 | api_key="fake_api_key", | |
| 61 | permission_manager=mock_permission_manager | |
| 62 | ) | |
| 63 | except ValueError as e: | |
| 64 | if "API key" in str(e): | |
| 65 | print("Permission check passed, failed with API key error as expected") | |
| 66 | else: | |
| 67 | raise | |
| 68 | ||
| 69 | # Test that permission was requested | |
| 70 | if mock_permission_manager.request_permission.called: | |
| 71 | print("Permission request called correctly") | |
| 72 | else: | |
| 73 | print("Permission request was not called") | |
| 74 | except Exception as e: | |
| 75 | print(f"Test failed: {e}") | |
| 76 | ||
| 77 | # Test 2: Permission denied | |
| 78 | mock_permission_manager.request_permission.return_value = False | |
| 79 | mock_permission_manager.request_permission.reset_mock() | |
| 80 | ||
| 81 | try: | |
| 82 | try: | |
| 83 | get_weather( | |
| 84 | "London", | |
| 85 | api_key="fake_api_key", | |
| 86 | permission_manager=mock_permission_manager | |
| 87 | ) | |
| 88 | print("Test failed: Expected PermissionError but no exception was raised") | |
| 89 | except PermissionError: | |
| 90 | print("Permission denied test passed: PermissionError was raised") | |
| 91 | except Exception as e: | |
| 92 | print(f"Test failed: Expected PermissionError but got {type(e).__name__}: {e}") | |
| 93 | ||
| 94 | # Test that permission was requested | |
| 95 | if mock_permission_manager.request_permission.called: | |
| 96 | print("Permission request called correctly") | |
| 97 | else: | |
| 98 | print("Permission request was not called") | |
| 99 | except Exception as e: | |
| 100 | print(f"Test failed: {e}") | |
| 101 | ||
| 102 | ||
| 103 | def main(): | |
| 104 | """Run all tests.""" | |
| 105 | print("Manual testing of web tools") | |
| 106 | print("==========================") | |
| 107 | ||
| 108 | test_fetch_url() | |
| 109 | test_search_web() | |
| 110 | test_get_weather_mock() | |
| 111 | ||
| 112 | print("\nAll tests completed.") | |
| 113 | ||
| 114 | if __name__ == "__main__": | |
| 115 | main() |
| … | … | @@ -1,7 +1,4 @@ |
| 1 | 1 | """ |
| 2 | Utility functions for OllamaTools. | |
| 3 | ||
| 4 | This package contains various utility functions used by the OllamaTools library. | |
| 2 | Test package for OllamaTools. | |
| 5 | 3 | """ |
| 6 | ||
| 7 | from ollamatools.utils.logger import get_logger, enable_debug_logging, disable_logging, get_ollamatools_logger | |
| 4 | # Intentionally left empty |
| … | … | @@ -0,0 +1,127 @@ |
| 1 | """ | |
| 2 | Tests for web tools module. | |
| 3 | """ | |
| 4 | ||
| 5 | import pytest | |
| 6 | from unittest.mock import MagicMock, patch | |
| 7 | import requests | |
| 8 | from ollamatools.tools.web_tools import fetch_url | |
| 9 | ||
| 10 | # Test successful GET request | |
| 11 | @patch('requests.request') | |
| 12 | def test_successful_get_request(mock_request): | |
| 13 | # Setup mock response | |
| 14 | mock_response = MagicMock() | |
| 15 | mock_response.status_code = 200 | |
| 16 | mock_response.headers = {'Content-Type': 'application/json'} | |
| 17 | mock_response.json.return_value = {'key': 'value'} | |
| 18 | mock_response.url = 'https://example.com' | |
| 19 | mock_request.return_value = mock_response | |
| 20 | ||
| 21 | # Call function | |
| 22 | result = fetch_url('https://example.com') | |
| 23 | ||
| 24 | # Check result | |
| 25 | assert result['status'] == 200 | |
| 26 | assert result['content'] == {'key': 'value'} | |
| 27 | assert 'headers' in result | |
| 28 | assert result['url'] == 'https://example.com' | |
| 29 | ||
| 30 | # Check mock was called correctly | |
| 31 | mock_request.assert_called_once_with( | |
| 32 | method='GET', | |
| 33 | url='https://example.com', | |
| 34 | params=None, | |
| 35 | headers=None, | |
| 36 | json=None, | |
| 37 | timeout=30 | |
| 38 | ) | |
| 39 | ||
| 40 | # Test successful POST request with data | |
| 41 | @patch('requests.request') | |
| 42 | def test_post_request_with_data(mock_request): | |
| 43 | # Setup mock response | |
| 44 | mock_response = MagicMock() | |
| 45 | mock_response.status_code = 201 | |
| 46 | mock_response.headers = {'Content-Type': 'application/json'} | |
| 47 | mock_response.json.return_value = {'id': '123'} | |
| 48 | mock_response.url = 'https://example.com/api' | |
| 49 | mock_request.return_value = mock_response | |
| 50 | ||
| 51 | # Test data | |
| 52 | data = {'name': 'test', 'value': 42} | |
| 53 | ||
| 54 | # Call function | |
| 55 | result = fetch_url( | |
| 56 | 'https://example.com/api', | |
| 57 | method='POST', | |
| 58 | data=data | |
| 59 | ) | |
| 60 | ||
| 61 | # Check result | |
| 62 | assert result['status'] == 201 | |
| 63 | assert result['content'] == {'id': '123'} | |
| 64 | ||
| 65 | # Check mock was called correctly | |
| 66 | mock_request.assert_called_once_with( | |
| 67 | method='POST', | |
| 68 | url='https://example.com/api', | |
| 69 | params=None, | |
| 70 | headers=None, | |
| 71 | json=data, | |
| 72 | timeout=30 | |
| 73 | ) | |
| 74 | ||
| 75 | # Test request with invalid URL | |
| 76 | def test_invalid_url(): | |
| 77 | with pytest.raises(ValueError): | |
| 78 | fetch_url('') | |
| 79 | ||
| 80 | with pytest.raises(ValueError): | |
| 81 | fetch_url('not-a-url') | |
| 82 | ||
| 83 | # Test request with invalid method | |
| 84 | def test_invalid_method(): | |
| 85 | with pytest.raises(ValueError): | |
| 86 | fetch_url('https://example.com', method='INVALID') | |
| 87 | ||
| 88 | # Test request timeout | |
| 89 | @patch('requests.request') | |
| 90 | def test_request_timeout(mock_request): | |
| 91 | mock_request.side_effect = requests.exceptions.Timeout("Request timed out") | |
| 92 | ||
| 93 | with pytest.raises(requests.exceptions.Timeout): | |
| 94 | fetch_url('https://example.com', timeout=5) | |
| 95 | ||
| 96 | # Test permission denied | |
| 97 | def test_permission_denied(): | |
| 98 | # Setup mock permission manager | |
| 99 | mock_permission_manager = MagicMock() | |
| 100 | mock_permission_manager.request_permission.return_value = False | |
| 101 | ||
| 102 | with pytest.raises(PermissionError): | |
| 103 | fetch_url( | |
| 104 | 'https://example.com', | |
| 105 | permission_manager=mock_permission_manager | |
| 106 | ) | |
| 107 | ||
| 108 | # Check permission was requested | |
| 109 | mock_permission_manager.request_permission.assert_called_once() | |
| 110 | ||
| 111 | # Test handling non-JSON content | |
| 112 | @patch('requests.request') | |
| 113 | def test_non_json_content(mock_request): | |
| 114 | # Setup mock response | |
| 115 | mock_response = MagicMock() | |
| 116 | mock_response.status_code = 200 | |
| 117 | mock_response.headers = {'Content-Type': 'text/html'} | |
| 118 | mock_response.text = '<html><body>Hello World</body></html>' | |
| 119 | mock_response.url = 'https://example.com' | |
| 120 | mock_request.return_value = mock_response | |
| 121 | ||
| 122 | # Call function | |
| 123 | result = fetch_url('https://example.com') | |
| 124 | ||
| 125 | # Check result | |
| 126 | assert result['status'] == 200 | |
| 127 | assert result['content'] == '<html><body>Hello World</body></html>' |