├── tests ├── __init__.py ├── conftest.py └── test_base.py ├── .gitignore ├── praisonai_tools ├── tools │ ├── __init__.py │ ├── decorator.py │ └── base.py └── __init__.py ├── .github └── workflows │ ├── python-package.yml │ └── python-publish.yml ├── LICENSE ├── pyproject.toml ├── test_integration.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for PraisonAI Tools.""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Pytest configuration and fixtures for PraisonAI Tools tests.""" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .DS_Store 3 | .pytest_cache 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # Distribution / packaging 9 | .Python 10 | build/ 11 | dist/ 12 | *.egg-info/ 13 | 14 | # Virtual environments 15 | .venv/ 16 | venv/ 17 | 18 | # IDE 19 | .vscode/ 20 | .idea/ 21 | 22 | # Testing 23 | .pytest_cache/ 24 | .coverage 25 | htmlcov/ 26 | 27 | # Lock files 28 | poetry.lock 29 | uv.lock 30 | 31 | # Database 32 | *.db 33 | db/ 34 | chroma.sqlite3 35 | 36 | # OS 37 | .DS_Store 38 | praisonai 39 | .cache 40 | __pycache__ 41 | test/ 42 | .env 43 | assets/* 44 | .idea 45 | .DS_Store 46 | .pytest_cache 47 | praisonAI.egg-info 48 | flagged 49 | test.yaml 50 | db 51 | -------------------------------------------------------------------------------- /praisonai_tools/tools/__init__.py: -------------------------------------------------------------------------------- 1 | """Tools package for PraisonAI Tools. 2 | 3 | This module provides base classes for creating custom tools. 4 | 5 | Note: Common tools (Tavily, Exa, You.com, DuckDuckGo, Wikipedia, etc.) are 6 | already built into praisonaiagents. This package is for creating CUSTOM tools. 7 | """ 8 | 9 | from praisonai_tools.tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool 10 | from praisonai_tools.tools.decorator import tool, FunctionTool, is_tool, get_tool_schema 11 | 12 | __all__ = [ 13 | # Base classes 14 | "BaseTool", 15 | "ToolResult", 16 | "ToolValidationError", 17 | "validate_tool", 18 | 19 | # Decorator 20 | "tool", 21 | "FunctionTool", 22 | "is_tool", 23 | "get_tool_schema", 24 | ] 25 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "develop" ] 9 | pull_request: 10 | branches: [ "develop" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.10", "3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mervin Praison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@v1.9.0 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "praisonai-tools" 3 | version = "0.1.1" 4 | description = "Base classes for creating custom tools for PraisonAI Agents" 5 | authors = [ 6 | {name = "Mervin Praison", email = "mervin@praison.ai"} 7 | ] 8 | readme = "README.md" 9 | license = {text = "MIT"} 10 | requires-python = ">=3.10" 11 | keywords = ["ai", "agents", "tools", "praisonai", "custom-tools", "plugin"] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Topic :: Software Development :: Libraries :: Python Modules", 21 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 22 | ] 23 | dependencies = [] 24 | 25 | [project.optional-dependencies] 26 | dev = [ 27 | "pytest>=8.0.0", 28 | "pytest-asyncio>=0.23.0", 29 | ] 30 | 31 | [project.urls] 32 | Homepage = "https://docs.praison.ai" 33 | Repository = "https://github.com/MervinPraison/PraisonAI-Tools" 34 | Documentation = "https://docs.praison.ai/tools" 35 | Issues = "https://github.com/MervinPraison/PraisonAI-Tools/issues" 36 | 37 | [build-system] 38 | requires = ["hatchling>=1.0.0"] 39 | build-backend = "hatchling.build" 40 | 41 | [tool.hatch.build.targets.wheel] 42 | packages = ["praisonai_tools"] 43 | 44 | [tool.pytest.ini_options] 45 | testpaths = ["tests"] 46 | asyncio_mode = "auto" -------------------------------------------------------------------------------- /praisonai_tools/__init__.py: -------------------------------------------------------------------------------- 1 | """PraisonAI Tools - Base classes for creating custom tools for AI agents. 2 | 3 | This package provides the foundation for creating custom tools that work with 4 | PraisonAI Agents. Use BaseTool or the @tool decorator to create your own tools. 5 | 6 | Note: Common tools (Tavily, Exa, You.com, DuckDuckGo, Wikipedia, etc.) are 7 | already built into praisonaiagents. This package is for creating CUSTOM tools. 8 | 9 | Usage: 10 | from praisonai_tools import BaseTool, tool 11 | 12 | # Method 1: Using @tool decorator 13 | @tool 14 | def my_search(query: str) -> str: 15 | '''Search for something.''' 16 | return f"Results for {query}" 17 | 18 | # Method 2: Subclassing BaseTool 19 | class MyTool(BaseTool): 20 | name = "my_tool" 21 | description = "Does something useful" 22 | 23 | def run(self, query: str) -> str: 24 | return f"Result for {query}" 25 | 26 | # Use with PraisonAI Agents 27 | from praisonaiagents import Agent 28 | agent = Agent(tools=[my_search, MyTool()]) 29 | """ 30 | 31 | from praisonai_tools.tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool 32 | from praisonai_tools.tools.decorator import tool, FunctionTool, is_tool, get_tool_schema 33 | 34 | __version__ = "0.1.0" 35 | __author__ = "Mervin Praison" 36 | 37 | __all__ = [ 38 | # Base classes for custom tools 39 | "BaseTool", 40 | "ToolResult", 41 | "ToolValidationError", 42 | "validate_tool", 43 | 44 | # Decorator for function-based tools 45 | "tool", 46 | "FunctionTool", 47 | "is_tool", 48 | "get_tool_schema", 49 | ] -------------------------------------------------------------------------------- /test_integration.py: -------------------------------------------------------------------------------- 1 | """Integration test: praisonai-tools custom tools with praisonaiagents built-in tools. 2 | 3 | This test verifies: 4 | 1. Built-in tools from praisonaiagents work (tavily_search) 5 | 2. Custom tools from praisonai-tools work (@tool decorator) 6 | 3. Both can be combined in an Agent 7 | 8 | Usage: 9 | export OPENAI_API_KEY=your_openai_key 10 | export TAVILY_API_KEY=your_tavily_key 11 | python test_integration.py 12 | """ 13 | 14 | import os 15 | import sys 16 | 17 | print("=" * 60) 18 | print("PraisonAI Tools Integration Test") 19 | print("=" * 60) 20 | 21 | # Test 1: Verify praisonai-tools imports work 22 | print("\n[Test 1] Importing praisonai-tools...") 23 | try: 24 | from praisonai_tools import BaseTool, tool, FunctionTool, is_tool, get_tool_schema 25 | print("✅ praisonai-tools imports successful") 26 | except ImportError as e: 27 | print(f"❌ Failed to import praisonai-tools: {e}") 28 | sys.exit(1) 29 | 30 | # Test 2: Verify praisonaiagents imports work 31 | print("\n[Test 2] Importing praisonaiagents...") 32 | try: 33 | from praisonaiagents import Agent 34 | from praisonaiagents.tools import tavily_search 35 | print("✅ praisonaiagents imports successful") 36 | except ImportError as e: 37 | print(f"❌ Failed to import praisonaiagents: {e}") 38 | sys.exit(1) 39 | 40 | # Test 3: Create custom tool with @tool decorator 41 | print("\n[Test 3] Creating custom tool with @tool decorator...") 42 | 43 | @tool 44 | def my_calculator(expression: str) -> str: 45 | """Calculate a mathematical expression. 46 | 47 | Args: 48 | expression: Math expression to evaluate (e.g., '2 + 2') 49 | """ 50 | try: 51 | result = eval(expression) 52 | return f"Result: {result}" 53 | except Exception as e: 54 | return f"Error: {e}" 55 | 56 | print(f"✅ Custom tool created: {my_calculator.name}") 57 | print(f" Description: {my_calculator.description}") 58 | print(f" Is tool: {is_tool(my_calculator)}") 59 | 60 | # Test 4: Get schema from custom tool 61 | print("\n[Test 4] Getting OpenAI schema from custom tool...") 62 | schema = get_tool_schema(my_calculator) 63 | print(f"✅ Schema generated:") 64 | print(f" Function name: {schema['function']['name']}") 65 | print(f" Parameters: {list(schema['function']['parameters']['properties'].keys())}") 66 | 67 | # Test 5: Execute custom tool directly 68 | print("\n[Test 5] Executing custom tool directly...") 69 | result = my_calculator(expression="10 * 5 + 2") 70 | print(f"✅ Tool execution result: {result}") 71 | 72 | # Test 6: Verify built-in tavily_search tool 73 | print("\n[Test 6] Checking built-in tavily_search tool...") 74 | print(f"✅ tavily_search function: {tavily_search.__name__}") 75 | 76 | # Test 7: Test tavily_search with real API call (if key available) 77 | tavily_key = os.environ.get("TAVILY_API_KEY") 78 | if tavily_key: 79 | print("\n[Test 7] Testing tavily_search with real API call...") 80 | try: 81 | result = tavily_search("Python programming", max_results=2) 82 | if "error" not in result: 83 | print(f"✅ Tavily search successful!") 84 | print(f" Found {len(result.get('results', []))} results") 85 | for r in result.get('results', [])[:2]: 86 | print(f" - {r.get('title', 'No title')[:50]}...") 87 | else: 88 | print(f"⚠️ Tavily returned error: {result.get('error')}") 89 | except Exception as e: 90 | print(f"❌ Tavily search failed: {e}") 91 | else: 92 | print("\n[Test 7] Skipped - TAVILY_API_KEY not set") 93 | 94 | # Test 8: Create Agent with combined tools (without calling LLM) 95 | print("\n[Test 8] Creating Agent with combined tools...") 96 | openai_key = os.environ.get("OPENAI_API_KEY") 97 | if openai_key: 98 | try: 99 | agent = Agent( 100 | instructions="You are a helpful assistant", 101 | tools=[tavily_search, my_calculator], 102 | verbose=False 103 | ) 104 | print(f"✅ Agent created with {len(agent.tools)} tools:") 105 | for t in agent.tools: 106 | name = getattr(t, '__name__', getattr(t, 'name', str(t))) 107 | print(f" - {name}") 108 | except Exception as e: 109 | print(f"❌ Agent creation failed: {e}") 110 | else: 111 | print("⚠️ Skipped Agent creation - OPENAI_API_KEY not set") 112 | print(" To test full integration, set OPENAI_API_KEY") 113 | 114 | print("\n" + "=" * 60) 115 | print("Integration test completed!") 116 | print("=" * 60) 117 | -------------------------------------------------------------------------------- /praisonai_tools/tools/decorator.py: -------------------------------------------------------------------------------- 1 | """Tool decorator for converting functions into tools. 2 | 3 | This module provides the @tool decorator for easily creating tools from functions. 4 | 5 | Usage: 6 | from praisonai_tools import tool 7 | 8 | @tool 9 | def search(query: str) -> list: 10 | '''Search the web for information.''' 11 | return [...] 12 | 13 | # Or with explicit parameters: 14 | @tool(name="web_search", description="Search the internet") 15 | def search(query: str, max_results: int = 5) -> list: 16 | return [...] 17 | """ 18 | 19 | import inspect 20 | import functools 21 | import logging 22 | from typing import Any, Callable, Dict, Optional, Union, get_type_hints 23 | 24 | from praisonai_tools.tools.base import BaseTool 25 | 26 | 27 | class FunctionTool(BaseTool): 28 | """A BaseTool wrapper for plain functions. 29 | 30 | Created automatically by the @tool decorator. 31 | """ 32 | 33 | def __init__( 34 | self, 35 | func: Callable, 36 | name: Optional[str] = None, 37 | description: Optional[str] = None, 38 | version: str = "1.0.0" 39 | ): 40 | self._func = func 41 | self.name = name or func.__name__ 42 | self.description = description or func.__doc__ or f"Tool: {self.name}" 43 | self.version = version 44 | 45 | # Generate schema from the original function 46 | self.parameters = self._generate_schema_from_func(func) 47 | 48 | # Copy function metadata 49 | functools.update_wrapper(self, func) 50 | 51 | def _generate_schema_from_func(self, func: Callable) -> Dict[str, Any]: 52 | """Generate JSON Schema from the wrapped function's signature.""" 53 | schema = { 54 | "type": "object", 55 | "properties": {}, 56 | "required": [] 57 | } 58 | 59 | try: 60 | sig = inspect.signature(func) 61 | hints = get_type_hints(func) if hasattr(func, '__annotations__') else {} 62 | 63 | for param_name, param in sig.parameters.items(): 64 | if param_name in ('self', 'cls'): 65 | continue 66 | 67 | param_type = hints.get(param_name, Any) 68 | json_type = BaseTool._python_type_to_json(param_type) 69 | 70 | schema["properties"][param_name] = {"type": json_type} 71 | 72 | if param.default is inspect.Parameter.empty: 73 | schema["required"].append(param_name) 74 | except Exception as e: 75 | logging.debug(f"Could not generate schema for {func.__name__}: {e}") 76 | 77 | return schema 78 | 79 | def run(self, **kwargs) -> Any: 80 | """Execute the wrapped function.""" 81 | return self._func(**kwargs) 82 | 83 | def __call__(self, *args, **kwargs) -> Any: 84 | """Allow calling with positional args like the original function.""" 85 | return self._func(*args, **kwargs) 86 | 87 | 88 | def tool( 89 | func: Optional[Callable] = None, 90 | *, 91 | name: Optional[str] = None, 92 | description: Optional[str] = None, 93 | version: str = "1.0.0" 94 | ) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]: 95 | """Decorator to convert a function into a tool. 96 | 97 | Can be used with or without arguments: 98 | 99 | @tool 100 | def my_func(x: str) -> str: 101 | '''Does something.''' 102 | return x 103 | 104 | @tool(name="custom_name", description="Custom description") 105 | def my_func(x: str) -> str: 106 | return x 107 | 108 | Args: 109 | func: The function to wrap (when used without parentheses) 110 | name: Override the tool name (default: function name) 111 | description: Override description (default: function docstring) 112 | version: Tool version (default: "1.0.0") 113 | 114 | Returns: 115 | FunctionTool instance that wraps the function 116 | """ 117 | def decorator(fn: Callable) -> FunctionTool: 118 | return FunctionTool( 119 | func=fn, 120 | name=name, 121 | description=description, 122 | version=version 123 | ) 124 | 125 | if func is not None: 126 | return decorator(func) 127 | else: 128 | return decorator 129 | 130 | 131 | def is_tool(obj: Any) -> bool: 132 | """Check if an object is a tool (BaseTool instance or decorated function).""" 133 | if isinstance(obj, BaseTool): 134 | return True 135 | if isinstance(obj, FunctionTool): 136 | return True 137 | if hasattr(obj, 'run') and hasattr(obj, 'name'): 138 | return True 139 | return False 140 | 141 | 142 | def get_tool_schema(obj: Any) -> Optional[Dict[str, Any]]: 143 | """Get OpenAI-compatible schema for any tool-like object.""" 144 | if isinstance(obj, BaseTool): 145 | return obj.get_schema() 146 | 147 | if callable(obj): 148 | return _schema_from_function(obj) 149 | 150 | return None 151 | 152 | 153 | def _schema_from_function(func: Callable) -> Dict[str, Any]: 154 | """Generate OpenAI function schema from a plain function.""" 155 | name = getattr(func, '__name__', 'unknown') 156 | description = func.__doc__ or f"Function: {name}" 157 | 158 | properties = {} 159 | required = [] 160 | 161 | try: 162 | sig = inspect.signature(func) 163 | hints = get_type_hints(func) if hasattr(func, '__annotations__') else {} 164 | 165 | for param_name, param in sig.parameters.items(): 166 | if param_name == 'self': 167 | continue 168 | 169 | param_type = hints.get(param_name, Any) 170 | json_type = BaseTool._python_type_to_json(param_type) 171 | 172 | properties[param_name] = {"type": json_type} 173 | 174 | if param.default is inspect.Parameter.empty: 175 | required.append(param_name) 176 | except Exception as e: 177 | logging.debug(f"Could not generate schema for {name}: {e}") 178 | 179 | return { 180 | "type": "function", 181 | "function": { 182 | "name": name, 183 | "description": description.strip(), 184 | "parameters": { 185 | "type": "object", 186 | "properties": properties, 187 | "required": required 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | """Tests for base tool classes.""" 2 | 3 | import pytest 4 | from praisonai_tools.tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool 5 | from praisonai_tools.tools.decorator import tool, FunctionTool, is_tool, get_tool_schema 6 | 7 | 8 | class TestToolResult: 9 | """Tests for ToolResult class.""" 10 | 11 | def test_successful_result(self): 12 | """Test creating a successful result.""" 13 | result = ToolResult(output="test output", success=True) 14 | assert result.success is True 15 | assert result.output == "test output" 16 | assert result.error is None 17 | assert str(result) == "test output" 18 | 19 | def test_failed_result(self): 20 | """Test creating a failed result.""" 21 | result = ToolResult(output=None, success=False, error="Something went wrong") 22 | assert result.success is False 23 | assert result.output is None 24 | assert result.error == "Something went wrong" 25 | assert "Error:" in str(result) 26 | 27 | def test_result_with_metadata(self): 28 | """Test result with metadata.""" 29 | result = ToolResult( 30 | output="data", 31 | success=True, 32 | metadata={"duration": 1.5, "source": "test"} 33 | ) 34 | assert result.metadata["duration"] == 1.5 35 | assert result.metadata["source"] == "test" 36 | 37 | def test_to_dict(self): 38 | """Test converting result to dictionary.""" 39 | result = ToolResult(output="test", success=True, metadata={"key": "value"}) 40 | d = result.to_dict() 41 | assert d["output"] == "test" 42 | assert d["success"] is True 43 | assert d["metadata"]["key"] == "value" 44 | 45 | 46 | class TestBaseTool: 47 | """Tests for BaseTool class.""" 48 | 49 | def test_concrete_tool_creation(self): 50 | """Test creating a concrete tool.""" 51 | class MyTool(BaseTool): 52 | name = "my_tool" 53 | description = "A test tool" 54 | 55 | def run(self, query: str) -> str: 56 | return f"Result: {query}" 57 | 58 | tool = MyTool() 59 | assert tool.name == "my_tool" 60 | assert tool.description == "A test tool" 61 | assert tool.version == "1.0.0" 62 | 63 | def test_tool_execution(self): 64 | """Test executing a tool.""" 65 | class EchoTool(BaseTool): 66 | name = "echo" 67 | description = "Echoes input" 68 | 69 | def run(self, message: str) -> str: 70 | return message 71 | 72 | tool = EchoTool() 73 | result = tool.run(message="Hello") 74 | assert result == "Hello" 75 | 76 | # Test callable 77 | result = tool(message="World") 78 | assert result == "World" 79 | 80 | def test_safe_run_success(self): 81 | """Test safe_run with successful execution.""" 82 | class SafeTool(BaseTool): 83 | name = "safe" 84 | description = "Safe tool" 85 | 86 | def run(self, x: int) -> int: 87 | return x * 2 88 | 89 | tool = SafeTool() 90 | result = tool.safe_run(x=5) 91 | assert result.success is True 92 | assert result.output == 10 93 | 94 | def test_safe_run_failure(self): 95 | """Test safe_run with failed execution.""" 96 | class FailingTool(BaseTool): 97 | name = "failing" 98 | description = "Failing tool" 99 | 100 | def run(self, x: int) -> int: 101 | raise ValueError("Intentional error") 102 | 103 | tool = FailingTool() 104 | result = tool.safe_run(x=5) 105 | assert result.success is False 106 | assert "Intentional error" in result.error 107 | 108 | def test_get_schema(self): 109 | """Test getting OpenAI-compatible schema.""" 110 | class SchemaTool(BaseTool): 111 | name = "schema_tool" 112 | description = "Tool with schema" 113 | 114 | def run(self, query: str, limit: int = 10) -> dict: 115 | return {} 116 | 117 | tool = SchemaTool() 118 | schema = tool.get_schema() 119 | 120 | assert schema["type"] == "function" 121 | assert schema["function"]["name"] == "schema_tool" 122 | assert schema["function"]["description"] == "Tool with schema" 123 | assert "query" in schema["function"]["parameters"]["properties"] 124 | 125 | def test_auto_name_from_class(self): 126 | """Test automatic name generation from class name.""" 127 | class AutoNameTool(BaseTool): 128 | description = "Auto named" 129 | 130 | def run(self) -> str: 131 | return "ok" 132 | 133 | tool = AutoNameTool() 134 | assert tool.name == "autoname" 135 | 136 | def test_validation(self): 137 | """Test tool validation.""" 138 | class ValidTool(BaseTool): 139 | name = "valid" 140 | description = "Valid tool" 141 | 142 | def run(self) -> str: 143 | return "ok" 144 | 145 | tool = ValidTool() 146 | assert tool.validate() is True 147 | 148 | 149 | class TestToolDecorator: 150 | """Tests for @tool decorator.""" 151 | 152 | def test_simple_decorator(self): 153 | """Test simple @tool decorator.""" 154 | @tool 155 | def greet(name: str) -> str: 156 | """Greet someone.""" 157 | return f"Hello, {name}!" 158 | 159 | assert isinstance(greet, FunctionTool) 160 | assert greet.name == "greet" 161 | assert "Greet someone" in greet.description 162 | 163 | result = greet(name="World") 164 | assert result == "Hello, World!" 165 | 166 | def test_decorator_with_args(self): 167 | """Test @tool decorator with arguments.""" 168 | @tool(name="custom_greet", description="Custom greeting") 169 | def greet(name: str) -> str: 170 | return f"Hi, {name}!" 171 | 172 | assert greet.name == "custom_greet" 173 | assert greet.description == "Custom greeting" 174 | 175 | def test_decorator_preserves_signature(self): 176 | """Test that decorator preserves function signature.""" 177 | @tool 178 | def add(a: int, b: int = 0) -> int: 179 | """Add two numbers.""" 180 | return a + b 181 | 182 | schema = add.get_schema() 183 | params = schema["function"]["parameters"] 184 | 185 | assert "a" in params["properties"] 186 | assert "b" in params["properties"] 187 | assert "a" in params["required"] 188 | assert "b" not in params["required"] # Has default value 189 | 190 | def test_is_tool(self): 191 | """Test is_tool function.""" 192 | @tool 193 | def my_tool() -> str: 194 | """A tool.""" 195 | return "ok" 196 | 197 | assert is_tool(my_tool) is True 198 | assert is_tool(lambda: None) is False 199 | 200 | def test_get_tool_schema(self): 201 | """Test get_tool_schema function.""" 202 | @tool 203 | def search(query: str) -> list: 204 | """Search for something.""" 205 | return [] 206 | 207 | schema = get_tool_schema(search) 208 | assert schema is not None 209 | assert schema["function"]["name"] == "search" 210 | 211 | 212 | class TestValidateTool: 213 | """Tests for validate_tool function.""" 214 | 215 | def test_validate_base_tool(self): 216 | """Test validating a BaseTool instance.""" 217 | class ValidTool(BaseTool): 218 | name = "valid" 219 | description = "Valid" 220 | 221 | def run(self) -> str: 222 | return "ok" 223 | 224 | tool = ValidTool() 225 | assert validate_tool(tool) is True 226 | 227 | def test_validate_callable(self): 228 | """Test validating a callable.""" 229 | def my_func(): 230 | pass 231 | 232 | assert validate_tool(my_func) is True 233 | 234 | def test_validate_invalid(self): 235 | """Test validating an invalid object.""" 236 | with pytest.raises(ToolValidationError): 237 | validate_tool("not a tool") 238 | -------------------------------------------------------------------------------- /praisonai_tools/tools/base.py: -------------------------------------------------------------------------------- 1 | """Base classes for PraisonAI Tools. 2 | 3 | This module provides the foundation for creating tools that can be used by agents. 4 | External developers can create plugins by subclassing BaseTool. 5 | 6 | Usage: 7 | from praisonai_tools import BaseTool 8 | 9 | class MyTool(BaseTool): 10 | name = "my_tool" 11 | description = "Does something useful" 12 | 13 | def run(self, query: str) -> str: 14 | return f"Result for {query}" 15 | """ 16 | 17 | from abc import ABC, abstractmethod 18 | from typing import Any, Dict, Optional, Type, get_type_hints 19 | import inspect 20 | import logging 21 | 22 | 23 | class ToolValidationError(Exception): 24 | """Raised when a tool fails validation.""" 25 | pass 26 | 27 | 28 | class ToolResult: 29 | """Wrapper for tool execution results.""" 30 | 31 | def __init__( 32 | self, 33 | output: Any, 34 | success: bool = True, 35 | error: Optional[str] = None, 36 | metadata: Optional[Dict[str, Any]] = None 37 | ): 38 | self.output = output 39 | self.success = success 40 | self.error = error 41 | self.metadata = metadata or {} 42 | 43 | def __str__(self) -> str: 44 | if self.success: 45 | return str(self.output) 46 | return f"Error: {self.error}" 47 | 48 | def __repr__(self) -> str: 49 | return f"ToolResult(success={self.success}, output={self.output!r})" 50 | 51 | def to_dict(self) -> Dict[str, Any]: 52 | return { 53 | "output": self.output, 54 | "success": self.success, 55 | "error": self.error, 56 | "metadata": self.metadata 57 | } 58 | 59 | 60 | class BaseTool(ABC): 61 | """Abstract base class for all PraisonAI tools. 62 | 63 | Subclass this to create custom tools that can be: 64 | - Used directly by agents 65 | - Distributed as pip-installable plugins 66 | - Auto-discovered via entry_points 67 | 68 | Attributes: 69 | name: Unique identifier for the tool 70 | description: Human-readable description (used by LLM) 71 | version: Tool version string (default: "1.0.0") 72 | parameters: JSON Schema for parameters (auto-generated if not provided) 73 | 74 | Example: 75 | class WeatherTool(BaseTool): 76 | name = "get_weather" 77 | description = "Get current weather for a location" 78 | 79 | def run(self, location: str, units: str = "celsius") -> dict: 80 | # Implementation here 81 | return {"temp": 22, "condition": "sunny"} 82 | """ 83 | 84 | # Required class attributes (must be overridden) 85 | name: str = "" 86 | description: str = "" 87 | 88 | # Optional class attributes 89 | version: str = "1.0.0" 90 | parameters: Optional[Dict[str, Any]] = None # JSON Schema, auto-generated if None 91 | 92 | def __init__(self): 93 | """Initialize the tool and validate configuration.""" 94 | if not self.name: 95 | # Use class name as default 96 | self.name = self.__class__.__name__.lower().replace("tool", "") 97 | 98 | if not self.description: 99 | # Use docstring as default 100 | self.description = self.__class__.__doc__ or f"Tool: {self.name}" 101 | 102 | # Auto-generate parameters schema if not provided 103 | if self.parameters is None: 104 | self.parameters = self._generate_parameters_schema() 105 | 106 | def _generate_parameters_schema(self) -> Dict[str, Any]: 107 | """Generate JSON Schema from run() method signature.""" 108 | schema = { 109 | "type": "object", 110 | "properties": {}, 111 | "required": [] 112 | } 113 | 114 | try: 115 | sig = inspect.signature(self.run) 116 | hints = get_type_hints(self.run) if hasattr(self.run, '__annotations__') else {} 117 | 118 | for param_name, param in sig.parameters.items(): 119 | if param_name == 'self': 120 | continue 121 | 122 | # Get type hint 123 | param_type = hints.get(param_name, Any) 124 | json_type = self._python_type_to_json(param_type) 125 | 126 | # Build property schema 127 | prop_schema = {"type": json_type} 128 | 129 | schema["properties"][param_name] = prop_schema 130 | 131 | # Check if required (no default value) 132 | if param.default is inspect.Parameter.empty: 133 | schema["required"].append(param_name) 134 | except Exception as e: 135 | logging.debug(f"Could not generate schema for {self.name}: {e}") 136 | 137 | return schema 138 | 139 | @staticmethod 140 | def _python_type_to_json(python_type: Type) -> str: 141 | """Convert Python type to JSON Schema type.""" 142 | type_map = { 143 | str: "string", 144 | int: "integer", 145 | float: "number", 146 | bool: "boolean", 147 | list: "array", 148 | dict: "object", 149 | type(None): "null" 150 | } 151 | 152 | # Handle Optional, Union, etc. 153 | origin = getattr(python_type, '__origin__', None) 154 | if origin is not None: 155 | if origin is list: 156 | return "array" 157 | if origin is dict: 158 | return "object" 159 | 160 | return type_map.get(python_type, "string") 161 | 162 | @abstractmethod 163 | def run(self, **kwargs) -> Any: 164 | """Execute the tool with given arguments. 165 | 166 | This method must be implemented by subclasses. 167 | 168 | Args: 169 | **kwargs: Tool-specific arguments 170 | 171 | Returns: 172 | Tool output (any type, will be converted to string for LLM) 173 | """ 174 | pass 175 | 176 | def __call__(self, **kwargs) -> Any: 177 | """Allow tool to be called directly like a function.""" 178 | return self.run(**kwargs) 179 | 180 | def safe_run(self, **kwargs) -> ToolResult: 181 | """Execute tool with error handling, returning ToolResult.""" 182 | try: 183 | output = self.run(**kwargs) 184 | return ToolResult(output=output, success=True) 185 | except Exception as e: 186 | logging.error(f"Tool {self.name} failed: {e}") 187 | return ToolResult( 188 | output=None, 189 | success=False, 190 | error=str(e) 191 | ) 192 | 193 | def get_schema(self) -> Dict[str, Any]: 194 | """Get OpenAI-compatible function schema for this tool.""" 195 | return { 196 | "type": "function", 197 | "function": { 198 | "name": self.name, 199 | "description": self.description, 200 | "parameters": self.parameters 201 | } 202 | } 203 | 204 | def __str__(self) -> str: 205 | return f"{self.__class__.__name__}(name='{self.name}')" 206 | 207 | def __repr__(self) -> str: 208 | return f"{self.__class__.__name__}(name='{self.name}', description='{self.description[:50]}...')" 209 | 210 | def validate(self) -> bool: 211 | """Validate the tool configuration. 212 | 213 | Raises: 214 | ToolValidationError: If validation fails 215 | 216 | Returns: 217 | True if validation passes 218 | """ 219 | errors = [] 220 | 221 | if not self.name or not isinstance(self.name, str): 222 | errors.append("Tool must have a non-empty string 'name'") 223 | 224 | if not self.description or not isinstance(self.description, str): 225 | errors.append("Tool must have a non-empty string 'description'") 226 | 227 | if getattr(self.run, '__isabstractmethod__', False): 228 | errors.append("Tool must implement the 'run()' method") 229 | 230 | if self.parameters: 231 | if not isinstance(self.parameters, dict): 232 | errors.append("'parameters' must be a dictionary") 233 | elif "type" not in self.parameters: 234 | errors.append("'parameters' must have a 'type' field") 235 | 236 | if errors: 237 | raise ToolValidationError(f"Tool '{self.name}' validation failed: {'; '.join(errors)}") 238 | 239 | return True 240 | 241 | 242 | def validate_tool(tool: Any) -> bool: 243 | """Validate any tool-like object. 244 | 245 | Args: 246 | tool: Object to validate (BaseTool, callable, etc.) 247 | 248 | Returns: 249 | True if valid 250 | 251 | Raises: 252 | ToolValidationError: If validation fails 253 | """ 254 | if isinstance(tool, BaseTool): 255 | return tool.validate() 256 | 257 | if callable(tool): 258 | name = getattr(tool, '__name__', None) or getattr(tool, 'name', None) 259 | if not name: 260 | raise ToolValidationError("Callable tool must have a __name__ or name attribute") 261 | return True 262 | 263 | raise ToolValidationError(f"Invalid tool type: {type(tool)}") 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PraisonAI Tools 2 | 3 | Base classes for creating **custom tools** for [PraisonAI Agents](https://github.com/MervinPraison/PraisonAI). 4 | 5 | [![PyPI version](https://badge.fury.io/py/praisonai-tools.svg)](https://badge.fury.io/py/praisonai-tools) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) 8 | 9 | ## What is this package? 10 | 11 | This package provides **base classes** (`BaseTool`, `@tool` decorator) for creating custom tools that work with PraisonAI Agents. 12 | 13 | > **Note:** Common tools like Tavily, Exa, You.com, DuckDuckGo, Wikipedia, arXiv, and many more are **already built into `praisonaiagents`**. You don't need this package for those - just use them directly from `praisonaiagents.tools`. 14 | 15 | ## Installation 16 | 17 | ```bash 18 | pip install praisonai-tools 19 | ``` 20 | 21 | --- 22 | 23 | # All Ways to Add Tools to PraisonAI Agents 24 | 25 | PraisonAI Agents supports **8 different ways** to add tools. Choose the method that best fits your use case: 26 | 27 | | Method | Best For | Complexity | 28 | |--------|----------|------------| 29 | | [1. Plain Python Functions](#1-plain-python-functions) | Quick custom tools | ⭐ Easy | 30 | | [2. Built-in Tools](#2-built-in-tools) | Common operations | ⭐ Easy | 31 | | [3. @tool Decorator](#3-tool-decorator) | Custom tools with metadata | ⭐ Easy | 32 | | [4. BaseTool Class](#4-basetool-class) | Complex tools with state | ⭐⭐ Medium | 33 | | [5. Pydantic Class with run()](#5-pydantic-class-with-run) | Validated tools | ⭐⭐ Medium | 34 | | [6. LangChain Tools](#6-langchain-tools) | LangChain ecosystem | ⭐⭐ Medium | 35 | | [7. CrewAI Tools](#7-crewai-tools) | CrewAI ecosystem | ⭐⭐ Medium | 36 | | [8. MCP Tools](#8-mcp-model-context-protocol-tools) | External services | ⭐⭐⭐ Advanced | 37 | 38 | --- 39 | 40 | ## 1. Plain Python Functions 41 | 42 | The simplest way - just write a function with type hints and docstring: 43 | 44 | ```python 45 | from praisonaiagents import Agent 46 | 47 | def search_web(query: str, max_results: int = 5) -> list: 48 | """Search the web for information. 49 | 50 | Args: 51 | query: The search query 52 | max_results: Maximum number of results to return 53 | """ 54 | # Your implementation 55 | return [{"title": "Result 1", "url": "https://..."}] 56 | 57 | def calculate(expression: str) -> float: 58 | """Evaluate a mathematical expression.""" 59 | return eval(expression) 60 | 61 | # Just pass the functions! 62 | agent = Agent( 63 | instructions="You are a helpful assistant", 64 | tools=[search_web, calculate] 65 | ) 66 | 67 | agent.start("Search for Python tutorials and calculate 15 * 7") 68 | ``` 69 | 70 | **How it works:** PraisonAI automatically: 71 | - Extracts function name as tool name 72 | - Uses docstring as description 73 | - Generates JSON schema from type hints 74 | - Parses Args section for parameter descriptions 75 | 76 | --- 77 | 78 | ## 2. Built-in Tools 79 | 80 | Use pre-built tools from `praisonaiagents.tools`: 81 | 82 | ```python 83 | from praisonaiagents import Agent 84 | from praisonaiagents.tools import ( 85 | # Search 86 | tavily_search, 87 | exa_search, 88 | internet_search, # DuckDuckGo 89 | 90 | # Wikipedia 91 | wiki_search, 92 | wiki_summary, 93 | 94 | # News 95 | get_article, 96 | get_trending_topics, 97 | 98 | # Files 99 | read_file, 100 | write_file, 101 | 102 | # Code 103 | execute_code, 104 | 105 | # And many more... 106 | ) 107 | 108 | agent = Agent( 109 | instructions="You are a research assistant", 110 | tools=[tavily_search, wiki_search, read_file] 111 | ) 112 | 113 | agent.start("Search for AI news and save a summary to a file") 114 | ``` 115 | 116 | ### Available Built-in Tools 117 | 118 | | Category | Tools | 119 | |----------|-------| 120 | | **Search** | `tavily_search`, `exa_search`, `ydc_search`, `internet_search`, `searxng_search` | 121 | | **Wikipedia** | `wiki_search`, `wiki_summary`, `wiki_page`, `wiki_random` | 122 | | **arXiv** | `search_arxiv`, `get_arxiv_paper`, `get_papers_by_author`, `get_papers_by_category` | 123 | | **News** | `get_article`, `get_news_sources`, `get_articles_from_source`, `get_trending_topics` | 124 | | **Web Crawling** | `crawl4ai`, `scrape_page`, `extract_links`, `extract_text` | 125 | | **Files** | `read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `delete_file` | 126 | | **Data** | `read_csv`, `write_csv`, `read_json`, `write_json`, `read_excel`, `read_yaml` | 127 | | **Code** | `execute_code`, `analyze_code`, `format_code`, `lint_code` | 128 | | **Shell** | `execute_command`, `list_processes`, `kill_process`, `get_system_info` | 129 | | **Calculator** | `evaluate`, `solve_equation`, `convert_units`, `calculate_statistics` | 130 | | **Finance** | `get_stock_price`, `get_stock_info`, `get_historical_data` | 131 | | **Database** | `query` (DuckDB), `insert_document`, `find_documents` (MongoDB) | 132 | 133 | --- 134 | 135 | ## 3. @tool Decorator 136 | 137 | Use the `@tool` decorator for custom tools with metadata: 138 | 139 | ```python 140 | from praisonaiagents import Agent 141 | from praisonai_tools import tool 142 | 143 | @tool 144 | def get_weather(location: str, units: str = "celsius") -> dict: 145 | """Get current weather for a location. 146 | 147 | Args: 148 | location: City name or coordinates 149 | units: Temperature units (celsius/fahrenheit) 150 | """ 151 | # Your implementation 152 | return {"temp": 22, "condition": "sunny", "location": location} 153 | 154 | @tool(name="custom_search", description="Search with custom parameters") 155 | def my_search(query: str, limit: int = 10) -> list: 156 | """Custom search implementation.""" 157 | return [{"result": query}] 158 | 159 | agent = Agent( 160 | instructions="You are a weather assistant", 161 | tools=[get_weather, my_search] 162 | ) 163 | 164 | agent.start("What's the weather in London?") 165 | ``` 166 | 167 | --- 168 | 169 | ## 4. BaseTool Class 170 | 171 | For complex tools with state, validation, or multiple methods: 172 | 173 | ```python 174 | from praisonaiagents import Agent 175 | from praisonai_tools import BaseTool 176 | 177 | class DatabaseTool(BaseTool): 178 | name = "database_query" 179 | description = "Query a database and return results" 180 | 181 | def __init__(self, connection_string: str): 182 | self.connection_string = connection_string 183 | self._connection = None 184 | super().__init__() 185 | 186 | def run(self, query: str, limit: int = 100) -> list: 187 | """Execute a database query. 188 | 189 | Args: 190 | query: SQL query to execute 191 | limit: Maximum rows to return 192 | """ 193 | # Your implementation 194 | return [{"id": 1, "name": "Example"}] 195 | 196 | # Create instance with configuration 197 | db_tool = DatabaseTool(connection_string="postgresql://...") 198 | 199 | agent = Agent( 200 | instructions="You are a data analyst", 201 | tools=[db_tool] 202 | ) 203 | 204 | agent.start("Query the users table for active users") 205 | ``` 206 | 207 | --- 208 | 209 | ## 5. Pydantic Class with run() 210 | 211 | Use Pydantic for validated tools with type checking: 212 | 213 | ```python 214 | from praisonaiagents import Agent 215 | from pydantic import BaseModel, Field 216 | from typing import Optional 217 | import requests 218 | 219 | class APISearchTool(BaseModel): 220 | """Search tool using an external API.""" 221 | 222 | api_url: str = "https://api.example.com/search" 223 | api_key: Optional[str] = None 224 | max_results: int = Field(default=10, ge=1, le=100) 225 | 226 | def run(self, query: str) -> dict: 227 | """Execute search query. 228 | 229 | Args: 230 | query: Search query string 231 | """ 232 | headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {} 233 | response = requests.get( 234 | self.api_url, 235 | params={"q": query, "limit": self.max_results}, 236 | headers=headers 237 | ) 238 | return response.json() 239 | 240 | # Pass the class (not instance) - PraisonAI will instantiate it 241 | agent = Agent( 242 | instructions="You are a search assistant", 243 | tools=[APISearchTool] 244 | ) 245 | 246 | agent.start("Search for machine learning tutorials") 247 | ``` 248 | 249 | --- 250 | 251 | ## 6. LangChain Tools 252 | 253 | Use any LangChain tool directly: 254 | 255 | ```python 256 | from praisonaiagents import Agent 257 | from langchain_community.tools import YouTubeSearchTool, DuckDuckGoSearchRun 258 | from langchain_community.utilities import WikipediaAPIWrapper 259 | 260 | # Method 1: Pass LangChain tool classes directly 261 | agent = Agent( 262 | instructions="You are a research assistant", 263 | tools=[YouTubeSearchTool, WikipediaAPIWrapper] 264 | ) 265 | 266 | agent.start("Find YouTube videos about Python and search Wikipedia for its history") 267 | ``` 268 | 269 | ### Wrapping LangChain Tools 270 | 271 | For more control, wrap LangChain tools in functions: 272 | 273 | ```python 274 | from praisonaiagents import Agent 275 | from langchain_community.tools import YouTubeSearchTool 276 | from langchain_community.utilities import WikipediaAPIWrapper 277 | 278 | def youtube_search(query: str, max_results: int = 5) -> str: 279 | """Search YouTube for videos. 280 | 281 | Args: 282 | query: Search query 283 | max_results: Number of results 284 | """ 285 | yt = YouTubeSearchTool() 286 | return yt.run(f"{query}, {max_results}") 287 | 288 | def wikipedia_search(query: str) -> str: 289 | """Search Wikipedia for information. 290 | 291 | Args: 292 | query: Search query 293 | """ 294 | wiki = WikipediaAPIWrapper() 295 | return wiki.run(query) 296 | 297 | agent = Agent( 298 | instructions="You are a research assistant", 299 | tools=[youtube_search, wikipedia_search] 300 | ) 301 | 302 | agent.start("Find videos about AI and get Wikipedia info on machine learning") 303 | ``` 304 | 305 | ### Using LangChain Toolkits 306 | 307 | ```python 308 | from praisonaiagents import Agent 309 | from langchain_agentql.tools import ExtractWebDataTool 310 | 311 | def extract_web_data(url: str, query: str) -> dict: 312 | """Extract structured data from a webpage. 313 | 314 | Args: 315 | url: URL to extract from 316 | query: What data to extract 317 | """ 318 | tool = ExtractWebDataTool() 319 | return tool.invoke({"url": url, "prompt": query}) 320 | 321 | agent = Agent( 322 | instructions="You are a web scraping assistant", 323 | tools=[extract_web_data] 324 | ) 325 | 326 | agent.start("Extract product names and prices from https://example.com/products") 327 | ``` 328 | 329 | --- 330 | 331 | ## 7. CrewAI Tools 332 | 333 | Use CrewAI tools (classes with `_run` method): 334 | 335 | ```python 336 | from praisonaiagents import Agent 337 | 338 | # CrewAI-style tool class 339 | class CrewAISearchTool: 340 | """A CrewAI-compatible search tool.""" 341 | 342 | name = "web_search" 343 | description = "Search the web for information" 344 | 345 | def _run(self, query: str) -> str: 346 | """Execute the search. 347 | 348 | Args: 349 | query: Search query 350 | """ 351 | # Your implementation 352 | return f"Results for: {query}" 353 | 354 | # Pass the class - PraisonAI detects _run method 355 | agent = Agent( 356 | instructions="You are a search assistant", 357 | tools=[CrewAISearchTool] 358 | ) 359 | 360 | agent.start("Search for latest tech news") 361 | ``` 362 | 363 | --- 364 | 365 | ## 8. MCP (Model Context Protocol) Tools 366 | 367 | Use external tools via MCP servers: 368 | 369 | ### Filesystem MCP 370 | 371 | ```python 372 | from praisonaiagents import Agent, MCP 373 | 374 | agent = Agent( 375 | instructions="You are a file manager assistant", 376 | tools=MCP("npx -y @modelcontextprotocol/server-filesystem", 377 | args=["/Users/username/Documents"]) 378 | ) 379 | 380 | agent.start("List all Python files in the Documents folder") 381 | ``` 382 | 383 | ### Time MCP 384 | 385 | ```python 386 | from praisonaiagents import Agent, MCP 387 | 388 | agent = Agent( 389 | instructions="You are a time assistant", 390 | tools=MCP("python -m mcp_server_time --local-timezone=America/New_York") 391 | ) 392 | 393 | agent.start("What time is it in New York? Convert to UTC.") 394 | ``` 395 | 396 | ### GitHub MCP 397 | 398 | ```python 399 | from praisonaiagents import Agent, MCP 400 | import os 401 | 402 | agent = Agent( 403 | instructions="You are a GitHub assistant", 404 | tools=MCP("npx -y @modelcontextprotocol/server-github", 405 | env={"GITHUB_TOKEN": os.environ["GITHUB_TOKEN"]}) 406 | ) 407 | 408 | agent.start("List my recent repositories") 409 | ``` 410 | 411 | ### Multiple MCP Servers 412 | 413 | ```python 414 | from praisonaiagents import Agent, MCP 415 | 416 | agent = Agent( 417 | instructions="You are a multi-capable assistant", 418 | tools=[ 419 | MCP("npx -y @modelcontextprotocol/server-filesystem", args=["/tmp"]), 420 | MCP("python -m mcp_server_time"), 421 | my_custom_function, # Can mix with other tool types! 422 | ] 423 | ) 424 | 425 | agent.start("Create a file with the current timestamp") 426 | ``` 427 | 428 | --- 429 | 430 | ## Combining Multiple Tool Types 431 | 432 | You can mix and match all tool types: 433 | 434 | ```python 435 | from praisonaiagents import Agent, MCP 436 | from praisonaiagents.tools import tavily_search, wiki_search 437 | from praisonai_tools import tool, BaseTool 438 | from langchain_community.tools import YouTubeSearchTool 439 | 440 | # Plain function 441 | def calculate(expression: str) -> float: 442 | """Calculate a math expression.""" 443 | return eval(expression) 444 | 445 | # @tool decorator 446 | @tool 447 | def format_output(data: dict) -> str: 448 | """Format data as a nice string.""" 449 | return str(data) 450 | 451 | # BaseTool class 452 | class CustomTool(BaseTool): 453 | name = "custom" 454 | description = "A custom tool" 455 | def run(self, input: str) -> str: 456 | return f"Processed: {input}" 457 | 458 | # Combine everything! 459 | agent = Agent( 460 | instructions="You are a super assistant with many capabilities", 461 | tools=[ 462 | # Built-in tools 463 | tavily_search, 464 | wiki_search, 465 | 466 | # Plain function 467 | calculate, 468 | 469 | # Decorated function 470 | format_output, 471 | 472 | # BaseTool instance 473 | CustomTool(), 474 | 475 | # LangChain tool 476 | YouTubeSearchTool, 477 | 478 | # MCP server 479 | MCP("python -m mcp_server_time"), 480 | ] 481 | ) 482 | 483 | agent.start("Search for AI news, find related YouTube videos, and calculate 2^10") 484 | ``` 485 | 486 | --- 487 | 488 | ## Creating Distributable Tool Packages 489 | 490 | To create a pip-installable tool package: 491 | 492 | ### 1. Create your tool module 493 | 494 | ```python 495 | # my_tools/weather.py 496 | from praisonai_tools import BaseTool 497 | 498 | class WeatherTool(BaseTool): 499 | name = "get_weather" 500 | description = "Get weather for a location" 501 | 502 | def run(self, location: str) -> dict: 503 | # Implementation 504 | return {"temp": 22, "location": location} 505 | 506 | # Convenience instance 507 | weather_tool = WeatherTool() 508 | ``` 509 | 510 | ### 2. Create pyproject.toml 511 | 512 | ```toml 513 | [project] 514 | name = "my-weather-tools" 515 | version = "0.1.0" 516 | dependencies = ["praisonai-tools"] 517 | 518 | [project.entry-points."praisonaiagents.tools"] 519 | weather = "my_tools.weather:WeatherTool" 520 | ``` 521 | 522 | ### 3. Users can then: 523 | 524 | ```python 525 | from praisonaiagents import Agent 526 | from my_tools.weather import weather_tool 527 | 528 | agent = Agent( 529 | instructions="Weather assistant", 530 | tools=[weather_tool] 531 | ) 532 | ``` 533 | 534 | --- 535 | 536 | ## Contributing 537 | 538 | We welcome contributions! To add a new tool: 539 | 540 | 1. Fork this repository 541 | 2. Create your tool using `BaseTool` or `@tool` decorator 542 | 3. Add tests in `tests/` 543 | 4. Submit a pull request 544 | 545 | --- 546 | 547 | ## Testing 548 | 549 | ```bash 550 | pip install praisonai-tools[dev] 551 | pytest tests/test_base.py -v 552 | ``` 553 | 554 | --- 555 | 556 | ## License 557 | 558 | MIT License - see [LICENSE](LICENSE) for details. 559 | 560 | ## Author 561 | 562 | **Mervin Praison** - [GitHub](https://github.com/MervinPraison) 563 | 564 | ## Links 565 | 566 | - [PraisonAI Documentation](https://docs.praison.ai) 567 | - [PraisonAI Agents](https://github.com/MervinPraison/PraisonAI) 568 | - [PyPI](https://pypi.org/project/praisonai-tools/) 569 | - [GitHub](https://github.com/MervinPraison/PraisonAI-Tools) 570 | - [Issues](https://github.com/MervinPraison/PraisonAI-Tools/issues) --------------------------------------------------------------------------------