Skip to content

Commit

Permalink
Add plugin generator script (#228)
Browse files Browse the repository at this point in the history
* feat: add plugin creation script

This adds a script to generate new GOAT SDK plugins with:
- Project structure and configuration
- Parameter definitions
- Service implementation with Tool decorators
- Plugin class with chain support
- Documentation and examples

Co-Authored-By: Agus Armellini Fischer <[email protected]>

* refactor: move plugin generator to scripts directory

Co-Authored-By: Agus Armellini Fischer <[email protected]>

* docs: add plugin generator documentation to README

Co-Authored-By: Agus Armellini Fischer <[email protected]>

* Update README.md

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Agus Armellini Fischer <[email protected]>
Co-authored-by: Agus <[email protected]>
  • Loading branch information
3 people authored Jan 15, 2025
1 parent da70aca commit 90a9478
Show file tree
Hide file tree
Showing 2 changed files with 333 additions and 0 deletions.
43 changes: 43 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,47 @@

[Docs](https://ohmygoat.dev) | [Examples](https://github.com/goat-sdk/goat/tree/main/typescript/examples) | [Discord](https://discord.gg/goat-sdk)

## Plugin Development

GOAT SDK supports easy plugin development through our plugin generator script. This tool helps you quickly scaffold new plugins with the correct structure and boilerplate code.

### Creating a New Plugin

To create a new plugin, use the plugin generator script from the Python directory:

```bash
cd python
python scripts/create_plugin.py <plugin-name> [--evm]
```

Options:
- `<plugin-name>`: Name of your plugin (e.g., 'my-token', 'my-service')
- `--evm`: Optional flag to indicate if the plugin is EVM-compatible

Examples:
1. Create an EVM-compatible plugin:
```bash
python scripts/create_plugin.py my-token --evm
```

2. Create a chain-agnostic plugin:
```bash
python scripts/create_plugin.py my-service
```

### Generated Structure

The script generates a complete plugin package with the following structure:

```
src/plugins/<plugin-name>/
├── pyproject.toml # Project configuration and dependencies
├── README.md # Plugin documentation
└── goat_plugins/<plugin-name>/
├── __init__.py # Plugin initialization and configuration
├── parameters.py # Pydantic parameter models
└── service.py # Service implementation with Tool decorators
```
For more information about plugin development, check out our [documentation](https://ohmygoat.dev).


290 changes: 290 additions & 0 deletions python/scripts/create_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
#!/usr/bin/env python3

import os
import argparse
from pathlib import Path
from typing import Optional

def create_project_toml(plugin_dir: Path, plugin_name: str, is_evm: bool) -> None:
"""Create the pyproject.toml file for the plugin."""
# Base dependencies
dependencies = '''python = "^3.10"
goat-sdk = "^0.1.0"'''

# Add EVM dependency if needed
if is_evm:
dependencies += '\ngoat-sdk-wallet-evm = "^0.1.0"'

# Dev dependencies
dev_dependencies = '''ruff = "^0.8.6"
goat-sdk = { path = "../../goat-sdk", develop = true }'''

# Add EVM dev dependency if needed
if is_evm:
dev_dependencies += '\ngoat-sdk-wallet-evm = { path = "../../wallets/evm", develop = true }'

toml_content = f'''[tool.poetry]
name = "goat-sdk-plugin-{plugin_name}"
version = "0.1.0"
description = "Goat plugin for {plugin_name}"
authors = ["Your Name <[email protected]>"]
readme = "README.md"
keywords = ["goat", "sdk", "agents", "ai", "{plugin_name}"]
homepage = "https://ohmygoat.dev/"
repository = "https://github.com/goat-sdk/goat"
packages = [
{{ include = "goat_plugins/{plugin_name}" }},
]
[tool.poetry.dependencies]
{dependencies}
[tool.poetry.group.test.dependencies]
pytest = "^8.3.4"
pytest-asyncio = "^0.25.0"
[tool.poetry.urls]
"Bug Tracker" = "https://github.com/goat-sdk/goat/issues"
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]
pythonpath = "src"
asyncio_default_fixture_loop_scope = "function"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.group.dev.dependencies]
{dev_dependencies}
[tool.ruff]
line-length = 120
target-version = "py312"
'''

with open(plugin_dir / "pyproject.toml", "w") as f:
f.write(toml_content)

def create_parameters_file(goat_plugins_dir: Path, plugin_name: str) -> None:
"""Create the parameters.py file with example parameters in the goat_plugins directory."""
parameters_content = '''from pydantic import BaseModel, Field
from typing import Optional
class ExampleQueryParameters(BaseModel):
query: str = Field(
description="An example query parameter (e.g., 'search term', 'identifier')"
)
limit: Optional[int] = Field(
None,
description="Maximum number of results to return. Defaults to no limit.",
)
include_metadata: bool = Field(
default=False,
description="Include additional metadata in the response"
)
class ExampleActionParameters(BaseModel):
target_id: str = Field(
description="The ID of the target to perform action on"
)
action_type: str = Field(
description="The type of action to perform (e.g., 'create', 'update', 'delete')"
)
parameters: Optional[dict] = Field(
None,
description="Optional parameters for the action"
)
'''

with open(goat_plugins_dir / "parameters.py", "w") as f:
f.write(parameters_content)

def create_service_file(goat_plugins_dir: Path, plugin_name: str, is_evm: bool) -> None:
"""Create the service.py file with an empty tool."""
# Start with common imports
service_content = '''from goat.decorators.tool import Tool
from .parameters import ExampleQueryParameters, ExampleActionParameters
'''

# Add EVM-specific imports if needed
if is_evm:
service_content += '''from goat_wallets.evm import EVMWalletClient
'''

# Create the service class
class_name = f"{plugin_name.title()}Service"
service_content += f'''
class {class_name}:
def __init__(self, api_key: str):
self.api_key = api_key
@Tool({{
"description": "Example query tool that demonstrates parameter usage",
"parameters_schema": ExampleQueryParameters
}})
async def example_query(self{", wallet_client: EVMWalletClient" if is_evm else ""}, parameters: dict):
"""An example query method that shows how to use parameters."""
try:
# Example implementation
query = parameters["query"]
limit = parameters.get("limit")
include_metadata = parameters.get("include_metadata", False)
# Placeholder for actual implementation
return {{"status": "success", "query": query, "limit": limit, "metadata_included": include_metadata}}
except Exception as error:
raise Exception(f"Failed to execute query: {{error}}")
@Tool({{
"description": "Example action tool that demonstrates parameter usage",
"parameters_schema": ExampleActionParameters
}})
async def example_action(self{", wallet_client: EVMWalletClient" if is_evm else ""}, parameters: dict):
"""An example action method that shows how to use parameters."""
try:
# Example implementation
target_id = parameters["target_id"]
action_type = parameters["action_type"]
action_params = parameters.get("parameters", {{}})
# Placeholder for actual implementation
return {{"status": "success", "action": action_type, "target": target_id, "params": action_params}}
except Exception as error:
raise Exception(f"Failed to execute action: {{error}}")
'''

with open(goat_plugins_dir / "service.py", "w") as f:
f.write(service_content)

def create_init_file(goat_plugins_dir: Path, plugin_name: str, is_evm: bool) -> None:
"""Create the __init__.py file with plugin class."""
# Convert hyphens to underscores for class names
class_base = plugin_name.replace("-", "_").title()
class_name = f"{class_base}Service"
plugin_class = f"{class_base}Plugin"
options_class = f"{class_base}PluginOptions"

init_content = '''from dataclasses import dataclass
from goat.classes.plugin_base import PluginBase
from .service import {class_name}
@dataclass
class {options_class}:
"""Options for the {plugin_class}."""
api_key: str # API key for external service integration
class {plugin_class}(PluginBase):
def __init__(self, options: {options_class}):
super().__init__("{plugin_name}", [{class_name}(options.api_key)])
def supports_chain(self, chain) -> bool:
return {supports_chain}
def {plugin_name}(options: {options_class}) -> {plugin_class}:
return {plugin_class}(options)
'''.format(
class_name=class_name,
plugin_class=plugin_class,
options_class=options_class,
plugin_name=plugin_name,
supports_chain="chain['type'] == 'evm'" if is_evm else "True"
)

with open(goat_plugins_dir / "__init__.py", "w") as f:
f.write(init_content)

def main():
"""Main function to create a new GOAT plugin.
This script generates a new plugin package for the GOAT SDK with a standardized structure.
The plugin name should use hyphens for spaces (e.g., 'my-plugin') and be lowercase.
Class names will automatically convert hyphens to underscores for valid Python syntax.
Examples:
# Create an EVM-compatible plugin
python create_plugin.py my-token --evm
# Create a chain-agnostic plugin
python create_plugin.py my-service
The script will create:
1. pyproject.toml with proper dependencies
2. goat_plugins directory with:
- parameters.py: Example Pydantic models
- service.py: Service class with Tool decorators
- __init__.py: Plugin class and initialization
3. README.md with usage instructions
"""
parser = argparse.ArgumentParser(description="Create a new GOAT plugin.")
parser.add_argument("name", help="Plugin name, e.g. 'myplugin'")
parser.add_argument("--evm", action="store_true", help="Indicate if plugin is for EVM")
args = parser.parse_args()

plugin_name = args.name.lower() # Normalize to lowercase
is_evm = args.evm

# Create base plugin directory (relative to python root)
plugin_dir = Path(__file__).parent.parent / "src" / "plugins" / plugin_name
plugin_dir.mkdir(parents=True, exist_ok=True)

# Create all required files
create_project_toml(plugin_dir, plugin_name, is_evm)

# Create goat_plugins directory and its contents
goat_plugins_dir = plugin_dir / "goat_plugins" / plugin_name
goat_plugins_dir.mkdir(parents=True, exist_ok=True)

# Create all plugin files
create_parameters_file(goat_plugins_dir, plugin_name)
create_service_file(goat_plugins_dir, plugin_name, is_evm)
create_init_file(goat_plugins_dir, plugin_name, is_evm)

# Create README.md
readme_content = f"""# {plugin_name} Plugin for GOAT SDK
A plugin for the GOAT SDK that provides {plugin_name} functionality.
## Installation
```bash
poetry add goat-sdk-plugin-{plugin_name}
```
## Usage
```python
from goat_plugins.{plugin_name} import {plugin_name}, {plugin_name.title()}PluginOptions
# Initialize the plugin
options = {plugin_name.title()}PluginOptions(
api_key="your-api-key"
)
plugin = {plugin_name}(options)
```
## Features
- Example query functionality
- Example action functionality
- {'EVM chain support' if is_evm else 'Chain-agnostic support'}
## License
This project is licensed under the terms of the MIT license.
"""
with open(plugin_dir / "README.md", "w") as f:
f.write(readme_content)

print(f"Plugin '{plugin_name}' created successfully in {plugin_dir}")

if __name__ == "__main__":
main()

0 comments on commit 90a9478

Please sign in to comment.