update local tool example

This commit is contained in:
Shanghua
2025-11-23 14:52:39 +08:00
parent 09fc0b96a6
commit 0c2618ab7a
7 changed files with 454 additions and 15 deletions
@@ -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/``
+50
View File
@@ -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
```
+33
View File
@@ -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"
}
]
+113
View File
@@ -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()
+113
View File
@@ -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()