mirror of
https://github.com/mims-harvard/ToolUniverse.git
synced 2026-09-19 07:31:47 +08:00
update local tool example
This commit is contained in:
@@ -6,6 +6,9 @@ This guide covers how to contribute local Python tools to the ToolUniverse repos
|
||||
.. note::
|
||||
**Key Difference**: Contributing to the repository requires additional steps compared to using tools locally. The most critical step is modifying ``__init__.py`` in 4 specific locations.
|
||||
|
||||
.. note::
|
||||
**For Local Development Only**: If you just want to use a tool locally without contributing to the repository, see the single-file example in ``examples/my_new_tool/single_file_example.py``. This approach doesn't require modifying any core ToolUniverse files (``__init__.py``, ``default_config.py``, or ``data/`` directory). The complete working examples are available in ``examples/my_new_tool/`` directory with a README explaining both approaches.
|
||||
|
||||
Quick Overview
|
||||
--------------
|
||||
|
||||
@@ -14,7 +17,7 @@ Quick Overview
|
||||
1. **Environment Setup** - Fork, clone, install dependencies
|
||||
2. **Create Tool File** - Python class in ``src/tooluniverse/``
|
||||
3. **Register Tool** - Use ``@register_tool('Type')`` decorator
|
||||
4. **Create Config** - JSON file in ``data/xxx_tools.json``
|
||||
4. **Create Config** - JSON file in ``data/my_new_tool_tools.json``
|
||||
5. **🔑 Modify __init__.py** - Add tool in 4 locations (critical!)
|
||||
6. **Write Tests** - >90% coverage required
|
||||
7. **Code Quality** - Pre-commit hooks (automatic)
|
||||
@@ -47,7 +50,7 @@ Step 1: Environment Setup
|
||||
Step 2: Create Tool File
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create your tool file in ``src/tooluniverse/xxx_tool.py``:
|
||||
Create your tool file in ``src/tooluniverse/my_new_tool.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -82,7 +85,7 @@ The ``@register_tool('MyNewTool')`` decorator registers your tool class. Note th
|
||||
Step 4: Create Configuration File
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create or edit ``src/tooluniverse/data/xxx_tools.json``:
|
||||
Create or edit ``src/tooluniverse/data/my_new_tool_tools.json``:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
@@ -230,18 +233,49 @@ Add comprehensive docstrings to your tool class:
|
||||
Step 9: Create Examples
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create ``examples/my_new_tool_example.py``:
|
||||
Create ``examples/my_new_tool/my_new_tool_example.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Example usage of MyNewTool."""
|
||||
"""Example usage of MyNewTool.
|
||||
|
||||
This example follows the documentation pattern for contributing tools to the
|
||||
repository. It demonstrates the multi-file structure:
|
||||
- my_new_tool.py: Tool class definition
|
||||
- my_new_tool_tools.json: Tool configuration
|
||||
- my_new_tool_example.py: Example usage
|
||||
|
||||
Note: In a real contribution, these files would be placed in:
|
||||
- src/tooluniverse/my_new_tool.py
|
||||
- src/tooluniverse/data/my_new_tool_tools.json
|
||||
- examples/my_new_tool_example.py
|
||||
|
||||
And you would need to modify __init__.py in 4 locations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add src to path to ensure tooluniverse can be imported
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src'))
|
||||
# Add current directory to path to import the tool class
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import the tool class to register it
|
||||
from my_new_tool import MyNewTool # noqa: E402, F401
|
||||
|
||||
from tooluniverse import ToolUniverse # noqa: E402
|
||||
|
||||
|
||||
from tooluniverse import ToolUniverse
|
||||
|
||||
def main():
|
||||
# Initialize ToolUniverse
|
||||
tu = ToolUniverse()
|
||||
tu.load_tools()
|
||||
|
||||
# Load tools with the config file
|
||||
# In a real contribution, this would be in default_tool_files
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(current_dir, 'my_new_tool_tools.json')
|
||||
tu.load_tools(tool_config_files={"my_new_tool": config_path})
|
||||
|
||||
# Use the tool
|
||||
result = tu.run({
|
||||
@@ -259,10 +293,14 @@ Create ``examples/my_new_tool_example.py``:
|
||||
"arguments": {"input": text}
|
||||
})
|
||||
print(f"'{text}' -> '{result.get('result', 'ERROR')}'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
**Note**: A complete working example can be found in ``examples/my_new_tool/`` directory,
|
||||
which includes both the multi-file structure (for contributions) and a single-file
|
||||
example (for local development). See ``examples/my_new_tool/README.md`` for details.
|
||||
|
||||
Step 10: Submit Pull Request
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -273,10 +311,10 @@ Step 10: Submit Pull Request
|
||||
|
||||
# Add all files
|
||||
git add src/tooluniverse/my_new_tool.py
|
||||
git add src/tooluniverse/data/xxx_tools.json
|
||||
git add src/tooluniverse/data/my_new_tool_tools.json
|
||||
git add src/tooluniverse/__init__.py
|
||||
git add tests/unit/test_my_new_tool.py
|
||||
git add examples/my_new_tool_example.py
|
||||
git add examples/my_new_tool/my_new_tool_example.py
|
||||
|
||||
# Commit with descriptive message
|
||||
git commit -m "feat: add MyNewTool for text processing
|
||||
@@ -303,14 +341,14 @@ Step 10: Submit Pull Request
|
||||
- ✅ **Tool Implementation**: Complete MyNewTool class
|
||||
- ✅ **Testing**: Unit tests with >95% coverage
|
||||
- ✅ **Documentation**: Comprehensive docstrings and examples
|
||||
- ✅ **Configuration**: JSON config in data/xxx_tools.json
|
||||
- ✅ **Configuration**: JSON config in data/my_new_tool_tools.json
|
||||
- ✅ **Integration**: Modified __init__.py in 4 locations
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
pytest tests/unit/test_my_new_tool.py --cov=tooluniverse
|
||||
python examples/my_new_tool_example.py
|
||||
python examples/my_new_tool/my_new_tool_example.py
|
||||
```
|
||||
|
||||
## Checklist
|
||||
@@ -329,8 +367,9 @@ Common Mistakes
|
||||
- Solution: Check all 4 locations in __init__.py
|
||||
|
||||
**❌ Config in wrong place**
|
||||
- Don't put config in ``@register_tool()`` decorator
|
||||
- Put it in ``data/xxx_tools.json`` instead
|
||||
- Don't put config in ``@register_tool()`` decorator (for contributions)
|
||||
- Put it in ``data/my_new_tool_tools.json`` instead
|
||||
- Note: For local development only, you CAN put config in the decorator (see ``examples/my_new_tool/single_file_example.py``)
|
||||
|
||||
**❌ Wrong file location**
|
||||
- Tool file must be in ``src/tooluniverse/``
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# MyNewTool Examples
|
||||
|
||||
This directory contains two examples demonstrating how to add a local tool to ToolUniverse:
|
||||
|
||||
## 1. Single File Example (`single_file_example.py`)
|
||||
|
||||
**Use case**: Quick local development, no modifications to core files needed.
|
||||
|
||||
- All code in one file (tool definition + config + usage)
|
||||
- Config is in the `@register_tool` decorator
|
||||
- No separate JSON file needed
|
||||
- No modifications to `__init__.py`, `default_config.py`, or `data/` directory
|
||||
|
||||
**Best for**: Local testing, prototyping, personal projects
|
||||
|
||||
## 2. Multi-File Example (Documentation Pattern)
|
||||
|
||||
**Use case**: Contributing a tool to the ToolUniverse repository.
|
||||
|
||||
Files:
|
||||
- `my_new_tool.py`: Tool class definition (no config in decorator)
|
||||
- `my_new_tool_tools.json`: Tool configuration file
|
||||
- `my_new_tool_example.py`: Example usage code
|
||||
|
||||
This matches the structure shown in:
|
||||
`docs/expand_tooluniverse/contributing/local_tools.rst`
|
||||
|
||||
**Note**: In a real contribution, these files would be placed in:
|
||||
- `src/tooluniverse/my_new_tool.py`
|
||||
- `src/tooluniverse/data/my_new_tool_tools.json`
|
||||
- `examples/my_new_tool_example.py`
|
||||
|
||||
And you would need to modify `__init__.py` in 4 locations as described in the documentation.
|
||||
|
||||
**Best for**: Contributing tools to the repository
|
||||
|
||||
## Running the Examples
|
||||
|
||||
### Single File Example:
|
||||
```bash
|
||||
cd examples/my_new_tool
|
||||
python single_file_example.py
|
||||
```
|
||||
|
||||
### Multi-File Example:
|
||||
```bash
|
||||
cd examples/my_new_tool
|
||||
python my_new_tool_example.py
|
||||
```
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tool definition for MyNewTool.
|
||||
|
||||
This file demonstrates the tool class definition following the documentation
|
||||
pattern for contributing tools to the repository.
|
||||
|
||||
Note: For contributions, the config is NOT in the decorator - it goes in a
|
||||
separate JSON file (see my_new_tool_tools.json).
|
||||
"""
|
||||
|
||||
from tooluniverse.tool_registry import register_tool
|
||||
from tooluniverse.base_tool import BaseTool
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
@register_tool('MyNewTool') # Note: No config here for contributions
|
||||
class MyNewTool(BaseTool):
|
||||
"""My new tool for ToolUniverse."""
|
||||
|
||||
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute the tool."""
|
||||
# Your tool logic here
|
||||
input_value = arguments.get('input', '')
|
||||
return {
|
||||
"result": input_value.upper(),
|
||||
"success": True
|
||||
}
|
||||
|
||||
def validate_input(self, **kwargs) -> None:
|
||||
"""Validate input parameters."""
|
||||
input_val = kwargs.get('input')
|
||||
if not input_val:
|
||||
raise ValueError("Input is required")
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Example usage of MyNewTool.
|
||||
|
||||
This example follows the documentation pattern for contributing tools to the
|
||||
repository. It demonstrates the multi-file structure:
|
||||
- my_new_tool.py: Tool class definition
|
||||
- my_new_tool_tools.json: Tool configuration
|
||||
- my_new_tool_example.py: Example usage
|
||||
|
||||
This matches the structure shown in:
|
||||
docs/expand_tooluniverse/contributing/local_tools.rst
|
||||
|
||||
Note: In a real contribution, these files would be placed in:
|
||||
- src/tooluniverse/my_new_tool.py
|
||||
- src/tooluniverse/data/my_new_tool_tools.json
|
||||
- examples/my_new_tool_example.py
|
||||
|
||||
And you would need to modify __init__.py in 4 locations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add src to path to ensure tooluniverse can be imported
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src'))
|
||||
# Add current directory to path to import the tool class
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import the tool class to register it
|
||||
from my_new_tool import MyNewTool # noqa: E402, F401
|
||||
|
||||
from tooluniverse import ToolUniverse # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
# Initialize ToolUniverse
|
||||
tu = ToolUniverse()
|
||||
|
||||
# Load tools with the config file
|
||||
# In a real contribution, this would be in default_tool_files
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(current_dir, 'my_new_tool_tools.json')
|
||||
tu.load_tools(tool_config_files={"my_new_tool": config_path})
|
||||
|
||||
# Use the tool
|
||||
result = tu.run({
|
||||
"name": "my_new_tool",
|
||||
"arguments": {"input": "hello world"}
|
||||
})
|
||||
|
||||
print(f"Result: {result}")
|
||||
|
||||
# Test with different inputs
|
||||
test_inputs = ["hello", "world", "python"]
|
||||
for text in test_inputs:
|
||||
result = tu.run({
|
||||
"name": "my_new_tool",
|
||||
"arguments": {"input": text}
|
||||
})
|
||||
print(f"'{text}' -> '{result.get('result', 'ERROR')}'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"name": "my_new_tool",
|
||||
"type": "MyNewTool",
|
||||
"description": "Convert text to uppercase",
|
||||
"parameter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "string",
|
||||
"description": "Text to convert to uppercase"
|
||||
}
|
||||
},
|
||||
"required": ["input"]
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"description": "Convert text to uppercase",
|
||||
"arguments": {"input": "hello world"}
|
||||
}
|
||||
],
|
||||
"tags": ["text", "utility"],
|
||||
"author": "Your Name <your.email@example.com>",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Example: Adding and Using a Local Tool (Without Contributing)
|
||||
|
||||
This example demonstrates how to create and use a local tool WITHOUT
|
||||
modifying the core ToolUniverse files (default_config.py, __init__.py,
|
||||
data/ directory).
|
||||
|
||||
DIFFERENCE from documentation example:
|
||||
- Documentation (docs/expand_tooluniverse/contributing/local_tools.rst):
|
||||
Shows how to CONTRIBUTE a tool to the repository (requires modifying
|
||||
__init__.py, default_config.py, and adding files to data/ directory)
|
||||
|
||||
- This example:
|
||||
Shows how to use a tool LOCALLY in your own project (all code in one
|
||||
file, no modifications to core ToolUniverse files needed)
|
||||
|
||||
To use a local tool:
|
||||
1. Create your tool class with @register_tool decorator and config
|
||||
2. Import the tool class to register it (config auto-loaded)
|
||||
3. Call tu.load_tools() - the tool will be available automatically
|
||||
|
||||
All code is in this single file for easy reference.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Import ToolUniverse components (after sys.path is set)
|
||||
from tooluniverse.tool_registry import register_tool # noqa: E402
|
||||
from tooluniverse.base_tool import BaseTool # noqa: E402
|
||||
from tooluniverse import ToolUniverse # noqa: E402
|
||||
from typing import Dict, Any # noqa: E402
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL DEFINITION
|
||||
# ============================================================================
|
||||
|
||||
@register_tool('MyNewTool', config={
|
||||
"name": "my_new_tool",
|
||||
"type": "MyNewTool",
|
||||
"description": "Convert text to uppercase",
|
||||
"parameter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "string",
|
||||
"description": "Text to convert to uppercase"
|
||||
}
|
||||
},
|
||||
"required": ["input"]
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"description": "Convert text to uppercase",
|
||||
"arguments": {"input": "hello world"}
|
||||
}
|
||||
],
|
||||
"tags": ["text", "utility"],
|
||||
"author": "ToolUniverse Contributor",
|
||||
"version": "1.0.0"
|
||||
})
|
||||
class MyNewTool(BaseTool):
|
||||
"""My new tool for ToolUniverse."""
|
||||
|
||||
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute the tool."""
|
||||
# Your tool logic here
|
||||
input_value = arguments.get('input', '')
|
||||
return {
|
||||
"result": input_value.upper(),
|
||||
"success": True
|
||||
}
|
||||
|
||||
def validate_input(self, **kwargs) -> None:
|
||||
"""Validate input parameters."""
|
||||
input_val = kwargs.get('input')
|
||||
if not input_val:
|
||||
raise ValueError("Input is required")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# EXAMPLE USAGE
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
# Initialize ToolUniverse
|
||||
tu = ToolUniverse()
|
||||
|
||||
# Load tools - the tool config is automatically loaded from the decorator
|
||||
# No need for a separate JSON file or tool_config_files parameter
|
||||
tu.load_tools()
|
||||
|
||||
# Use the tool
|
||||
result = tu.run({
|
||||
"name": "my_new_tool",
|
||||
"arguments": {"input": "hello world"}
|
||||
})
|
||||
|
||||
print(f"Result: {result}")
|
||||
|
||||
# Test with different inputs
|
||||
test_inputs = ["hello", "world", "python"]
|
||||
for text in test_inputs:
|
||||
result = tu.run({
|
||||
"name": "my_new_tool",
|
||||
"arguments": {"input": text}
|
||||
})
|
||||
print(f"'{text}' -> '{result.get('result', 'ERROR')}'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Example: Adding and Using a Local Tool (Without Contributing)
|
||||
|
||||
This example demonstrates how to create and use a local tool WITHOUT
|
||||
modifying the core ToolUniverse files (default_config.py, __init__.py,
|
||||
data/ directory).
|
||||
|
||||
DIFFERENCE from documentation example:
|
||||
- Documentation (docs/expand_tooluniverse/contributing/local_tools.rst):
|
||||
Shows how to CONTRIBUTE a tool to the repository (requires modifying
|
||||
__init__.py, default_config.py, and adding files to data/ directory)
|
||||
|
||||
- This example:
|
||||
Shows how to use a tool LOCALLY in your own project (all code in one
|
||||
file, no modifications to core ToolUniverse files needed)
|
||||
|
||||
To use a local tool:
|
||||
1. Create your tool class with @register_tool decorator and config
|
||||
2. Import the tool class to register it (config auto-loaded)
|
||||
3. Call tu.load_tools() - the tool will be available automatically
|
||||
|
||||
All code is in this single file for easy reference.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# Import ToolUniverse components (after sys.path is set)
|
||||
from tooluniverse.tool_registry import register_tool # noqa: E402
|
||||
from tooluniverse.base_tool import BaseTool # noqa: E402
|
||||
from tooluniverse import ToolUniverse # noqa: E402
|
||||
from typing import Dict, Any # noqa: E402
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL DEFINITION
|
||||
# ============================================================================
|
||||
|
||||
@register_tool('MyNewTool', config={
|
||||
"name": "my_new_tool",
|
||||
"type": "MyNewTool",
|
||||
"description": "Convert text to uppercase",
|
||||
"parameter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "string",
|
||||
"description": "Text to convert to uppercase"
|
||||
}
|
||||
},
|
||||
"required": ["input"]
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"description": "Convert text to uppercase",
|
||||
"arguments": {"input": "hello world"}
|
||||
}
|
||||
],
|
||||
"tags": ["text", "utility"],
|
||||
"author": "ToolUniverse Contributor",
|
||||
"version": "1.0.0"
|
||||
})
|
||||
class MyNewTool(BaseTool):
|
||||
"""My new tool for ToolUniverse."""
|
||||
|
||||
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute the tool."""
|
||||
# Your tool logic here
|
||||
input_value = arguments.get('input', '')
|
||||
return {
|
||||
"result": input_value.upper(),
|
||||
"success": True
|
||||
}
|
||||
|
||||
def validate_input(self, **kwargs) -> None:
|
||||
"""Validate input parameters."""
|
||||
input_val = kwargs.get('input')
|
||||
if not input_val:
|
||||
raise ValueError("Input is required")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# EXAMPLE USAGE
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
# Initialize ToolUniverse
|
||||
tu = ToolUniverse()
|
||||
|
||||
# Load tools - the tool config is automatically loaded from the decorator
|
||||
# No need for a separate JSON file or tool_config_files parameter
|
||||
tu.load_tools()
|
||||
|
||||
# Use the tool
|
||||
result = tu.run({
|
||||
"name": "my_new_tool",
|
||||
"arguments": {"input": "hello world"}
|
||||
})
|
||||
|
||||
print(f"Result: {result}")
|
||||
|
||||
# Test with different inputs
|
||||
test_inputs = ["hello", "world", "python"]
|
||||
for text in test_inputs:
|
||||
result = tu.run({
|
||||
"name": "my_new_tool",
|
||||
"arguments": {"input": text}
|
||||
})
|
||||
print(f"'{text}' -> '{result.get('result', 'ERROR')}'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user