gmf_forge_ai_shared_core.tools.builtin_tools
Built-in tools for search, calculation, and API calls.
1"""Built-in tools for search, calculation, and API calls.""" 2 3from gmf_forge_ai_shared_core.tools.builtin_tools.search_tool import SearchTool 4from gmf_forge_ai_shared_core.tools.builtin_tools.calculation_tool import CalculationTool 5from gmf_forge_ai_shared_core.tools.builtin_tools.api_tool import APITool 6 7__all__ = [ 8 "SearchTool", 9 "CalculationTool", 10 "APITool", 11]
27class SearchTool: 28 """ 29 Search tool for web and document search. 30 31 Provides a unified interface for searching across different sources: 32 - Web search (via search APIs) 33 - Document search (local or vector store) 34 - Custom search endpoints 35 36 Example: 37 >>> tool = SearchTool() 38 >>> 39 >>> # Search the web 40 >>> results = await tool.search( 41 ... query="What is RAG?", 42 ... max_results=5 43 ... ) 44 >>> 45 >>> for result in results: 46 ... print(f"{result.title}: {result.snippet}") 47 """ 48 49 def __init__( 50 self, 51 api_key: Optional[str] = None, 52 search_endpoint: Optional[str] = None, 53 timeout: int = 30 54 ): 55 """ 56 Initialize the search tool. 57 58 Args: 59 api_key: Optional API key for search service 60 search_endpoint: Optional custom search endpoint URL 61 timeout: Request timeout in seconds (default: 30) 62 """ 63 self.api_key = api_key 64 self.search_endpoint = search_endpoint 65 self.timeout = timeout 66 self._client: Optional[httpx.AsyncClient] = None 67 68 async def _get_client(self) -> httpx.AsyncClient: 69 """Get or create HTTP client.""" 70 if self._client is None: 71 self._client = httpx.AsyncClient(timeout=self.timeout) 72 return self._client 73 74 async def search( 75 self, 76 query: str, 77 max_results: int = 10, 78 search_type: str = "web", 79 filters: Optional[Dict[str, Any]] = None 80 ) -> List[SearchResult]: 81 """ 82 Perform a search query. 83 84 Args: 85 query: Search query string 86 max_results: Maximum number of results to return 87 search_type: Type of search ("web", "documents", "hybrid") 88 filters: Optional filters to apply to search 89 90 Returns: 91 List of SearchResult objects 92 93 Example: 94 >>> results = await tool.search( 95 ... query="machine learning tutorials", 96 ... max_results=5, 97 ... filters={"language": "en", "date_range": "1y"} 98 ... ) 99 """ 100 if not query or not query.strip(): 101 return [] 102 103 if search_type == "web": 104 return await self._web_search(query, max_results, filters) 105 elif search_type == "documents": 106 return await self._document_search(query, max_results, filters) 107 elif search_type == "hybrid": 108 # Combine web and document search 109 web_results = await self._web_search(query, max_results // 2, filters) 110 doc_results = await self._document_search(query, max_results // 2, filters) 111 return web_results + doc_results 112 else: 113 raise ValueError(f"Unknown search type: {search_type}") 114 115 async def _web_search( 116 self, 117 query: str, 118 max_results: int, 119 filters: Optional[Dict[str, Any]] 120 ) -> List[SearchResult]: 121 """ 122 Perform web search. 123 124 This is a placeholder implementation. In production, integrate with: 125 - Bing Search API 126 - Google Custom Search API 127 - Azure Cognitive Search 128 - Brave Search API 129 """ 130 # Placeholder: Return mock results 131 # TODO: Integrate with actual search API 132 results = [ 133 SearchResult( 134 title=f"Result {i+1} for: {query}", 135 url=f"https://example.com/result{i+1}", 136 snippet=f"This is a sample search result snippet for query: {query}", 137 score=1.0 - (i * 0.1), 138 metadata={"source": "web", "rank": i+1} 139 ) 140 for i in range(min(max_results, 3)) 141 ] 142 return results 143 144 async def _document_search( 145 self, 146 query: str, 147 max_results: int, 148 filters: Optional[Dict[str, Any]] 149 ) -> List[SearchResult]: 150 """ 151 Perform document search. 152 153 This is a placeholder implementation. In production, integrate with: 154 - Azure AI Search 155 - Pinecone 156 - Weaviate 157 - Local vector database 158 """ 159 # Placeholder: Return mock results 160 # TODO: Integrate with vector store 161 results = [ 162 SearchResult( 163 title=f"Document {i+1}", 164 url=f"document://{i+1}", 165 snippet=f"Document content related to: {query}", 166 score=0.9 - (i * 0.1), 167 metadata={"source": "documents", "rank": i+1} 168 ) 169 for i in range(min(max_results, 2)) 170 ] 171 return results 172 173 async def close(self) -> None: 174 """Close the HTTP client.""" 175 if self._client: 176 await self._client.aclose() 177 self._client = None 178 179 def __del__(self): 180 """Cleanup on deletion.""" 181 if self._client: 182 import asyncio 183 try: 184 loop = asyncio.get_event_loop() 185 if loop.is_running(): 186 loop.create_task(self.close()) 187 else: 188 loop.run_until_complete(self.close()) 189 except Exception: 190 pass # Ignore errors during cleanup
Search tool for web and document search.
Provides a unified interface for searching across different sources:
- Web search (via search APIs)
- Document search (local or vector store)
- Custom search endpoints
Example:
tool = SearchTool()
Search the web
results = await tool.search( ... query="What is RAG?", ... max_results=5 ... )
for result in results: ... print(f"{result.title}: {result.snippet}")
49 def __init__( 50 self, 51 api_key: Optional[str] = None, 52 search_endpoint: Optional[str] = None, 53 timeout: int = 30 54 ): 55 """ 56 Initialize the search tool. 57 58 Args: 59 api_key: Optional API key for search service 60 search_endpoint: Optional custom search endpoint URL 61 timeout: Request timeout in seconds (default: 30) 62 """ 63 self.api_key = api_key 64 self.search_endpoint = search_endpoint 65 self.timeout = timeout 66 self._client: Optional[httpx.AsyncClient] = None
Initialize the search tool.
Args: api_key: Optional API key for search service search_endpoint: Optional custom search endpoint URL timeout: Request timeout in seconds (default: 30)
74 async def search( 75 self, 76 query: str, 77 max_results: int = 10, 78 search_type: str = "web", 79 filters: Optional[Dict[str, Any]] = None 80 ) -> List[SearchResult]: 81 """ 82 Perform a search query. 83 84 Args: 85 query: Search query string 86 max_results: Maximum number of results to return 87 search_type: Type of search ("web", "documents", "hybrid") 88 filters: Optional filters to apply to search 89 90 Returns: 91 List of SearchResult objects 92 93 Example: 94 >>> results = await tool.search( 95 ... query="machine learning tutorials", 96 ... max_results=5, 97 ... filters={"language": "en", "date_range": "1y"} 98 ... ) 99 """ 100 if not query or not query.strip(): 101 return [] 102 103 if search_type == "web": 104 return await self._web_search(query, max_results, filters) 105 elif search_type == "documents": 106 return await self._document_search(query, max_results, filters) 107 elif search_type == "hybrid": 108 # Combine web and document search 109 web_results = await self._web_search(query, max_results // 2, filters) 110 doc_results = await self._document_search(query, max_results // 2, filters) 111 return web_results + doc_results 112 else: 113 raise ValueError(f"Unknown search type: {search_type}")
Perform a search query.
Args: query: Search query string max_results: Maximum number of results to return search_type: Type of search ("web", "documents", "hybrid") filters: Optional filters to apply to search
Returns: List of SearchResult objects
Example:
results = await tool.search( ... query="machine learning tutorials", ... max_results=5, ... filters={"language": "en", "date_range": "1y"} ... )
15class CalculationTool: 16 """ 17 Safe mathematical calculation tool. 18 19 Provides sandboxed evaluation of mathematical expressions with support for: 20 - Basic arithmetic (+, -, *, /, //, %, **) 21 - Common math functions (sin, cos, sqrt, log, etc.) 22 - Safe expression parsing (no code execution) 23 24 Example: 25 >>> calc = CalculationTool() 26 >>> 27 >>> result = calc.calculate("2 + 2 * 3") 28 >>> print(result) # 8 29 >>> 30 >>> result = calc.calculate("sqrt(16) + log(100, 10)") 31 >>> print(result) # 6.0 32 >>> 33 >>> result = calc.calculate("sin(pi / 2)") 34 >>> print(result) # 1.0 35 """ 36 37 # Allowed operators 38 _OPERATORS = { 39 ast.Add: operator.add, 40 ast.Sub: operator.sub, 41 ast.Mult: operator.mul, 42 ast.Div: operator.truediv, 43 ast.FloorDiv: operator.floordiv, 44 ast.Mod: operator.mod, 45 ast.Pow: operator.pow, 46 ast.USub: operator.neg, 47 ast.UAdd: operator.pos, 48 } 49 50 # Allowed functions from math module 51 _FUNCTIONS = { 52 # Trigonometric 53 'sin': math.sin, 54 'cos': math.cos, 55 'tan': math.tan, 56 'asin': math.asin, 57 'acos': math.acos, 58 'atan': math.atan, 59 'atan2': math.atan2, 60 61 # Hyperbolic 62 'sinh': math.sinh, 63 'cosh': math.cosh, 64 'tanh': math.tanh, 65 66 # Exponential and logarithmic 67 'exp': math.exp, 68 'log': math.log, 69 'log10': math.log10, 70 'log2': math.log2, 71 'sqrt': math.sqrt, 72 73 # Power and rounding 74 'pow': pow, 75 'abs': abs, 76 'round': round, 77 'ceil': math.ceil, 78 'floor': math.floor, 79 80 # Other 81 'factorial': math.factorial, 82 'gcd': math.gcd, 83 'degrees': math.degrees, 84 'radians': math.radians, 85 } 86 87 # Math constants 88 _CONSTANTS = { 89 'pi': math.pi, 90 'e': math.e, 91 'tau': math.tau, 92 'inf': math.inf, 93 'nan': math.nan, 94 } 95 96 def __init__(self, precision: int = 10): 97 """ 98 Initialize the calculation tool. 99 100 Args: 101 precision: Number of decimal places for results (default: 10) 102 """ 103 self.precision = precision 104 105 def calculate(self, expression: str) -> Union[float, int]: 106 """ 107 Evaluate a mathematical expression safely. 108 109 Args: 110 expression: Mathematical expression as a string 111 112 Returns: 113 Calculated result as float or int 114 115 Raises: 116 ValueError: If expression is invalid or contains unsafe operations 117 ZeroDivisionError: If division by zero occurs 118 119 Example: 120 >>> calc = CalculationTool() 121 >>> calc.calculate("(5 + 3) * 2") 122 16 123 >>> calc.calculate("sqrt(144)") 124 12.0 125 >>> calc.calculate("2 ** 10") 126 1024 127 """ 128 if not expression or not isinstance(expression, str): 129 raise ValueError("Expression must be a non-empty string") 130 131 try: 132 # Parse the expression into an AST 133 node = ast.parse(expression, mode='eval').body 134 135 # Evaluate the AST 136 result = self._eval_node(node) 137 138 # Round to precision 139 if isinstance(result, float): 140 return round(result, self.precision) 141 return result 142 143 except SyntaxError as e: 144 raise ValueError(f"Invalid expression syntax: {e}") 145 except Exception as e: 146 raise ValueError(f"Calculation error: {e}") 147 148 def _eval_node(self, node: ast.AST) -> Union[float, int]: 149 """ 150 Recursively evaluate an AST node. 151 152 Args: 153 node: AST node to evaluate 154 155 Returns: 156 Evaluated result 157 158 Raises: 159 ValueError: If node type is not allowed 160 """ 161 if isinstance(node, ast.Constant): 162 # Python 3.8+ uses ast.Constant 163 return node.value 164 165 elif isinstance(node, ast.Num): 166 # Fallback for older Python versions 167 return node.n 168 169 elif isinstance(node, ast.BinOp): 170 # Binary operation (e.g., a + b) 171 left = self._eval_node(node.left) 172 right = self._eval_node(node.right) 173 op_type = type(node.op) 174 175 if op_type not in self._OPERATORS: 176 raise ValueError(f"Operator {op_type.__name__} not allowed") 177 178 return self._OPERATORS[op_type](left, right) 179 180 elif isinstance(node, ast.UnaryOp): 181 # Unary operation (e.g., -a, +a) 182 operand = self._eval_node(node.operand) 183 op_type = type(node.op) 184 185 if op_type not in self._OPERATORS: 186 raise ValueError(f"Operator {op_type.__name__} not allowed") 187 188 return self._OPERATORS[op_type](operand) 189 190 elif isinstance(node, ast.Call): 191 # Function call (e.g., sqrt(4)) 192 func_name = node.func.id if isinstance(node.func, ast.Name) else None 193 194 if func_name not in self._FUNCTIONS: 195 raise ValueError(f"Function '{func_name}' not allowed") 196 197 func = self._FUNCTIONS[func_name] 198 args = [self._eval_node(arg) for arg in node.args] 199 200 return func(*args) 201 202 elif isinstance(node, ast.Name): 203 # Variable/constant (e.g., pi, e) 204 if node.id not in self._CONSTANTS: 205 raise ValueError(f"Variable '{node.id}' not allowed") 206 207 return self._CONSTANTS[node.id] 208 209 else: 210 raise ValueError(f"Node type {type(node).__name__} not allowed") 211 212 def evaluate_multiple(self, expressions: list[str]) -> Dict[str, Union[float, int, str]]: 213 """ 214 Evaluate multiple expressions and return results. 215 216 Args: 217 expressions: List of expression strings 218 219 Returns: 220 Dictionary mapping expressions to their results or error messages 221 222 Example: 223 >>> calc = CalculationTool() 224 >>> results = calc.evaluate_multiple([ 225 ... "2 + 2", 226 ... "sqrt(16)", 227 ... "1 / 0" # Will error 228 ... ]) 229 >>> print(results) 230 {'2 + 2': 4, 'sqrt(16)': 4.0, '1 / 0': 'Error: ...'} 231 """ 232 results = {} 233 for expr in expressions: 234 try: 235 results[expr] = self.calculate(expr) 236 except Exception as e: 237 results[expr] = f"Error: {str(e)}" 238 return results
Safe mathematical calculation tool.
Provides sandboxed evaluation of mathematical expressions with support for:
- Basic arithmetic (+, -, *, /, //, %, **)
- Common math functions (sin, cos, sqrt, log, etc.)
- Safe expression parsing (no code execution)
Example:
calc = CalculationTool()
result = calc.calculate("2 + 2 * 3") print(result) # 8
result = calc.calculate("sqrt(16) + log(100, 10)") print(result) # 6.0
result = calc.calculate("sin(pi / 2)") print(result) # 1.0
96 def __init__(self, precision: int = 10): 97 """ 98 Initialize the calculation tool. 99 100 Args: 101 precision: Number of decimal places for results (default: 10) 102 """ 103 self.precision = precision
Initialize the calculation tool.
Args: precision: Number of decimal places for results (default: 10)
105 def calculate(self, expression: str) -> Union[float, int]: 106 """ 107 Evaluate a mathematical expression safely. 108 109 Args: 110 expression: Mathematical expression as a string 111 112 Returns: 113 Calculated result as float or int 114 115 Raises: 116 ValueError: If expression is invalid or contains unsafe operations 117 ZeroDivisionError: If division by zero occurs 118 119 Example: 120 >>> calc = CalculationTool() 121 >>> calc.calculate("(5 + 3) * 2") 122 16 123 >>> calc.calculate("sqrt(144)") 124 12.0 125 >>> calc.calculate("2 ** 10") 126 1024 127 """ 128 if not expression or not isinstance(expression, str): 129 raise ValueError("Expression must be a non-empty string") 130 131 try: 132 # Parse the expression into an AST 133 node = ast.parse(expression, mode='eval').body 134 135 # Evaluate the AST 136 result = self._eval_node(node) 137 138 # Round to precision 139 if isinstance(result, float): 140 return round(result, self.precision) 141 return result 142 143 except SyntaxError as e: 144 raise ValueError(f"Invalid expression syntax: {e}") 145 except Exception as e: 146 raise ValueError(f"Calculation error: {e}")
Evaluate a mathematical expression safely.
Args: expression: Mathematical expression as a string
Returns: Calculated result as float or int
Raises: ValueError: If expression is invalid or contains unsafe operations ZeroDivisionError: If division by zero occurs
Example:
calc = CalculationTool() calc.calculate("(5 + 3) * 2") 16 calc.calculate("sqrt(144)") 12.0 calc.calculate("2 ** 10") 1024
212 def evaluate_multiple(self, expressions: list[str]) -> Dict[str, Union[float, int, str]]: 213 """ 214 Evaluate multiple expressions and return results. 215 216 Args: 217 expressions: List of expression strings 218 219 Returns: 220 Dictionary mapping expressions to their results or error messages 221 222 Example: 223 >>> calc = CalculationTool() 224 >>> results = calc.evaluate_multiple([ 225 ... "2 + 2", 226 ... "sqrt(16)", 227 ... "1 / 0" # Will error 228 ... ]) 229 >>> print(results) 230 {'2 + 2': 4, 'sqrt(16)': 4.0, '1 / 0': 'Error: ...'} 231 """ 232 results = {} 233 for expr in expressions: 234 try: 235 results[expr] = self.calculate(expr) 236 except Exception as e: 237 results[expr] = f"Error: {str(e)}" 238 return results
Evaluate multiple expressions and return results.
Args: expressions: List of expression strings
Returns: Dictionary mapping expressions to their results or error messages
Example:
calc = CalculationTool() results = calc.evaluate_multiple([ ... "2 + 2", ... "sqrt(16)", ... "1 / 0" # Will error ... ]) print(results) {'2 + 2': 4, 'sqrt(16)': 4.0, '1 / 0': 'Error: ...'}
24class APITool: 25 """ 26 Tool for making REST API calls. 27 28 Supports all standard HTTP methods (GET, POST, PUT, DELETE, PATCH, etc.) 29 with authentication, headers, and request body support. 30 31 Example: 32 >>> tool = APITool() 33 >>> 34 >>> # Simple GET request 35 >>> response = await tool.get("https://api.example.com/data") 36 >>> print(response.body) 37 >>> 38 >>> # POST with JSON body 39 >>> response = await tool.post( 40 ... "https://api.example.com/users", 41 ... json={"name": "John", "email": "john@example.com"} 42 ... ) 43 >>> 44 >>> # With authentication 45 >>> response = await tool.get( 46 ... "https://api.example.com/secure", 47 ... headers={"Authorization": "Bearer token123"} 48 ... ) 49 """ 50 51 def __init__( 52 self, 53 base_url: Optional[str] = None, 54 default_headers: Optional[Dict[str, str]] = None, 55 timeout: int = 30, 56 follow_redirects: bool = True 57 ): 58 """ 59 Initialize the API tool. 60 61 Args: 62 base_url: Optional base URL to prepend to all requests 63 default_headers: Default headers to include in all requests 64 timeout: Request timeout in seconds (default: 30) 65 follow_redirects: Whether to follow redirects (default: True) 66 """ 67 self.base_url = base_url.rstrip('/') if base_url else None 68 self.default_headers = default_headers or {} 69 self.timeout = timeout 70 self.follow_redirects = follow_redirects 71 self._client: Optional[httpx.AsyncClient] = None 72 73 async def _get_client(self) -> httpx.AsyncClient: 74 """Get or create HTTP client.""" 75 if self._client is None: 76 self._client = httpx.AsyncClient( 77 timeout=self.timeout, 78 follow_redirects=self.follow_redirects 79 ) 80 return self._client 81 82 def _build_url(self, path: str) -> str: 83 """Build full URL from path.""" 84 if self.base_url and not path.startswith(('http://', 'https://')): 85 return f"{self.base_url}/{path.lstrip('/')}" 86 return path 87 88 def _merge_headers(self, headers: Optional[Dict[str, str]]) -> Dict[str, str]: 89 """Merge default headers with request-specific headers.""" 90 merged = self.default_headers.copy() 91 if headers: 92 merged.update(headers) 93 return merged 94 95 async def request( 96 self, 97 method: str, 98 url: str, 99 headers: Optional[Dict[str, str]] = None, 100 params: Optional[Dict[str, Any]] = None, 101 json: Optional[Dict[str, Any]] = None, 102 data: Optional[Union[Dict[str, Any], str]] = None, 103 **kwargs: Any 104 ) -> APIResponse: 105 """ 106 Make an HTTP request. 107 108 Args: 109 method: HTTP method (GET, POST, PUT, DELETE, etc.) 110 url: URL or path (combined with base_url if set) 111 headers: Optional request headers 112 params: Optional query parameters 113 json: Optional JSON body 114 data: Optional form data or raw body 115 **kwargs: Additional arguments passed to httpx 116 117 Returns: 118 APIResponse object with status, body, and headers 119 120 Example: 121 >>> response = await tool.request( 122 ... "POST", 123 ... "/api/endpoint", 124 ... json={"key": "value"}, 125 ... headers={"X-Custom": "header"} 126 ... ) 127 """ 128 client = await self._get_client() 129 full_url = self._build_url(url) 130 merged_headers = self._merge_headers(headers) 131 132 try: 133 response = await client.request( 134 method=method.upper(), 135 url=full_url, 136 headers=merged_headers, 137 params=params, 138 json=json, 139 data=data, 140 **kwargs 141 ) 142 143 # Try to parse response as JSON, fallback to text 144 try: 145 body = response.json() 146 except Exception: 147 body = response.text 148 149 return APIResponse( 150 status_code=response.status_code, 151 body=body, 152 headers=dict(response.headers), 153 success=response.is_success, 154 error=None if response.is_success else response.text 155 ) 156 157 except httpx.TimeoutException as e: 158 return APIResponse( 159 status_code=408, 160 body=None, 161 success=False, 162 error=f"Request timeout: {str(e)}" 163 ) 164 except httpx.RequestError as e: 165 return APIResponse( 166 status_code=0, 167 body=None, 168 success=False, 169 error=f"Request error: {str(e)}" 170 ) 171 except Exception as e: 172 return APIResponse( 173 status_code=0, 174 body=None, 175 success=False, 176 error=f"Unexpected error: {str(e)}" 177 ) 178 179 async def get( 180 self, 181 url: str, 182 params: Optional[Dict[str, Any]] = None, 183 headers: Optional[Dict[str, str]] = None, 184 **kwargs: Any 185 ) -> APIResponse: 186 """ 187 Make a GET request. 188 189 Args: 190 url: URL or path 191 params: Optional query parameters 192 headers: Optional headers 193 **kwargs: Additional arguments 194 195 Returns: 196 APIResponse object 197 """ 198 return await self.request("GET", url, params=params, headers=headers, **kwargs) 199 200 async def post( 201 self, 202 url: str, 203 json: Optional[Dict[str, Any]] = None, 204 data: Optional[Union[Dict[str, Any], str]] = None, 205 headers: Optional[Dict[str, str]] = None, 206 **kwargs: Any 207 ) -> APIResponse: 208 """ 209 Make a POST request. 210 211 Args: 212 url: URL or path 213 json: Optional JSON body 214 data: Optional form data 215 headers: Optional headers 216 **kwargs: Additional arguments 217 218 Returns: 219 APIResponse object 220 """ 221 return await self.request("POST", url, json=json, data=data, headers=headers, **kwargs) 222 223 async def put( 224 self, 225 url: str, 226 json: Optional[Dict[str, Any]] = None, 227 data: Optional[Union[Dict[str, Any], str]] = None, 228 headers: Optional[Dict[str, str]] = None, 229 **kwargs: Any 230 ) -> APIResponse: 231 """ 232 Make a PUT request. 233 234 Args: 235 url: URL or path 236 json: Optional JSON body 237 data: Optional form data 238 headers: Optional headers 239 **kwargs: Additional arguments 240 241 Returns: 242 APIResponse object 243 """ 244 return await self.request("PUT", url, json=json, data=data, headers=headers, **kwargs) 245 246 async def delete( 247 self, 248 url: str, 249 headers: Optional[Dict[str, str]] = None, 250 **kwargs: Any 251 ) -> APIResponse: 252 """ 253 Make a DELETE request. 254 255 Args: 256 url: URL or path 257 headers: Optional headers 258 **kwargs: Additional arguments 259 260 Returns: 261 APIResponse object 262 """ 263 return await self.request("DELETE", url, headers=headers, **kwargs) 264 265 async def patch( 266 self, 267 url: str, 268 json: Optional[Dict[str, Any]] = None, 269 data: Optional[Union[Dict[str, Any], str]] = None, 270 headers: Optional[Dict[str, str]] = None, 271 **kwargs: Any 272 ) -> APIResponse: 273 """ 274 Make a PATCH request. 275 276 Args: 277 url: URL or path 278 json: Optional JSON body 279 data: Optional form data 280 headers: Optional headers 281 **kwargs: Additional arguments 282 283 Returns: 284 APIResponse object 285 """ 286 return await self.request("PATCH", url, json=json, data=data, headers=headers, **kwargs) 287 288 async def close(self) -> None: 289 """Close the HTTP client.""" 290 if self._client: 291 await self._client.aclose() 292 self._client = None 293 294 def __del__(self): 295 """Cleanup on deletion.""" 296 if self._client: 297 import asyncio 298 try: 299 loop = asyncio.get_event_loop() 300 if loop.is_running(): 301 loop.create_task(self.close()) 302 else: 303 loop.run_until_complete(self.close()) 304 except Exception: 305 pass # Ignore errors during cleanup
Tool for making REST API calls.
Supports all standard HTTP methods (GET, POST, PUT, DELETE, PATCH, etc.) with authentication, headers, and request body support.
Example:
tool = APITool()
Simple GET request
response = await tool.get("https://api.example.com/data") print(response.body)
POST with JSON body
response = await tool.post( ... "https://api.example.com/users", ... json={"name": "John", "email": "john@example.com"} ... )
With authentication
response = await tool.get( ... "https://api.example.com/secure", ... headers={"Authorization": "Bearer token123"} ... )
51 def __init__( 52 self, 53 base_url: Optional[str] = None, 54 default_headers: Optional[Dict[str, str]] = None, 55 timeout: int = 30, 56 follow_redirects: bool = True 57 ): 58 """ 59 Initialize the API tool. 60 61 Args: 62 base_url: Optional base URL to prepend to all requests 63 default_headers: Default headers to include in all requests 64 timeout: Request timeout in seconds (default: 30) 65 follow_redirects: Whether to follow redirects (default: True) 66 """ 67 self.base_url = base_url.rstrip('/') if base_url else None 68 self.default_headers = default_headers or {} 69 self.timeout = timeout 70 self.follow_redirects = follow_redirects 71 self._client: Optional[httpx.AsyncClient] = None
Initialize the API tool.
Args: base_url: Optional base URL to prepend to all requests default_headers: Default headers to include in all requests timeout: Request timeout in seconds (default: 30) follow_redirects: Whether to follow redirects (default: True)
95 async def request( 96 self, 97 method: str, 98 url: str, 99 headers: Optional[Dict[str, str]] = None, 100 params: Optional[Dict[str, Any]] = None, 101 json: Optional[Dict[str, Any]] = None, 102 data: Optional[Union[Dict[str, Any], str]] = None, 103 **kwargs: Any 104 ) -> APIResponse: 105 """ 106 Make an HTTP request. 107 108 Args: 109 method: HTTP method (GET, POST, PUT, DELETE, etc.) 110 url: URL or path (combined with base_url if set) 111 headers: Optional request headers 112 params: Optional query parameters 113 json: Optional JSON body 114 data: Optional form data or raw body 115 **kwargs: Additional arguments passed to httpx 116 117 Returns: 118 APIResponse object with status, body, and headers 119 120 Example: 121 >>> response = await tool.request( 122 ... "POST", 123 ... "/api/endpoint", 124 ... json={"key": "value"}, 125 ... headers={"X-Custom": "header"} 126 ... ) 127 """ 128 client = await self._get_client() 129 full_url = self._build_url(url) 130 merged_headers = self._merge_headers(headers) 131 132 try: 133 response = await client.request( 134 method=method.upper(), 135 url=full_url, 136 headers=merged_headers, 137 params=params, 138 json=json, 139 data=data, 140 **kwargs 141 ) 142 143 # Try to parse response as JSON, fallback to text 144 try: 145 body = response.json() 146 except Exception: 147 body = response.text 148 149 return APIResponse( 150 status_code=response.status_code, 151 body=body, 152 headers=dict(response.headers), 153 success=response.is_success, 154 error=None if response.is_success else response.text 155 ) 156 157 except httpx.TimeoutException as e: 158 return APIResponse( 159 status_code=408, 160 body=None, 161 success=False, 162 error=f"Request timeout: {str(e)}" 163 ) 164 except httpx.RequestError as e: 165 return APIResponse( 166 status_code=0, 167 body=None, 168 success=False, 169 error=f"Request error: {str(e)}" 170 ) 171 except Exception as e: 172 return APIResponse( 173 status_code=0, 174 body=None, 175 success=False, 176 error=f"Unexpected error: {str(e)}" 177 )
Make an HTTP request.
Args: method: HTTP method (GET, POST, PUT, DELETE, etc.) url: URL or path (combined with base_url if set) headers: Optional request headers params: Optional query parameters json: Optional JSON body data: Optional form data or raw body **kwargs: Additional arguments passed to httpx
Returns: APIResponse object with status, body, and headers
Example:
response = await tool.request( ... "POST", ... "/api/endpoint", ... json={"key": "value"}, ... headers={"X-Custom": "header"} ... )
179 async def get( 180 self, 181 url: str, 182 params: Optional[Dict[str, Any]] = None, 183 headers: Optional[Dict[str, str]] = None, 184 **kwargs: Any 185 ) -> APIResponse: 186 """ 187 Make a GET request. 188 189 Args: 190 url: URL or path 191 params: Optional query parameters 192 headers: Optional headers 193 **kwargs: Additional arguments 194 195 Returns: 196 APIResponse object 197 """ 198 return await self.request("GET", url, params=params, headers=headers, **kwargs)
Make a GET request.
Args: url: URL or path params: Optional query parameters headers: Optional headers **kwargs: Additional arguments
Returns: APIResponse object
200 async def post( 201 self, 202 url: str, 203 json: Optional[Dict[str, Any]] = None, 204 data: Optional[Union[Dict[str, Any], str]] = None, 205 headers: Optional[Dict[str, str]] = None, 206 **kwargs: Any 207 ) -> APIResponse: 208 """ 209 Make a POST request. 210 211 Args: 212 url: URL or path 213 json: Optional JSON body 214 data: Optional form data 215 headers: Optional headers 216 **kwargs: Additional arguments 217 218 Returns: 219 APIResponse object 220 """ 221 return await self.request("POST", url, json=json, data=data, headers=headers, **kwargs)
Make a POST request.
Args: url: URL or path json: Optional JSON body data: Optional form data headers: Optional headers **kwargs: Additional arguments
Returns: APIResponse object
223 async def put( 224 self, 225 url: str, 226 json: Optional[Dict[str, Any]] = None, 227 data: Optional[Union[Dict[str, Any], str]] = None, 228 headers: Optional[Dict[str, str]] = None, 229 **kwargs: Any 230 ) -> APIResponse: 231 """ 232 Make a PUT request. 233 234 Args: 235 url: URL or path 236 json: Optional JSON body 237 data: Optional form data 238 headers: Optional headers 239 **kwargs: Additional arguments 240 241 Returns: 242 APIResponse object 243 """ 244 return await self.request("PUT", url, json=json, data=data, headers=headers, **kwargs)
Make a PUT request.
Args: url: URL or path json: Optional JSON body data: Optional form data headers: Optional headers **kwargs: Additional arguments
Returns: APIResponse object
246 async def delete( 247 self, 248 url: str, 249 headers: Optional[Dict[str, str]] = None, 250 **kwargs: Any 251 ) -> APIResponse: 252 """ 253 Make a DELETE request. 254 255 Args: 256 url: URL or path 257 headers: Optional headers 258 **kwargs: Additional arguments 259 260 Returns: 261 APIResponse object 262 """ 263 return await self.request("DELETE", url, headers=headers, **kwargs)
Make a DELETE request.
Args: url: URL or path headers: Optional headers **kwargs: Additional arguments
Returns: APIResponse object
265 async def patch( 266 self, 267 url: str, 268 json: Optional[Dict[str, Any]] = None, 269 data: Optional[Union[Dict[str, Any], str]] = None, 270 headers: Optional[Dict[str, str]] = None, 271 **kwargs: Any 272 ) -> APIResponse: 273 """ 274 Make a PATCH request. 275 276 Args: 277 url: URL or path 278 json: Optional JSON body 279 data: Optional form data 280 headers: Optional headers 281 **kwargs: Additional arguments 282 283 Returns: 284 APIResponse object 285 """ 286 return await self.request("PATCH", url, json=json, data=data, headers=headers, **kwargs)
Make a PATCH request.
Args: url: URL or path json: Optional JSON body data: Optional form data headers: Optional headers **kwargs: Additional arguments
Returns: APIResponse object