Getting Started with AgentUp Plugins
This guide will walk you through creating your first AgentUp plugin.
In just 5 minutes, you'll have a working plugin with secure capabilities that integrate seamlessly with AI function calling.
Prerequisites
- AgentUp 2.0+ installed:
uv add agentuporpip install agentup - Python 3.11 or higher
Important: Plugin Development Workflow
AgentUp plugins are standalone Python packages that can be created anywhere on your system:
# You can create plugins in any directory
cd ~/my-projects/ # Or any directory you prefer
agentup plugin init time-plugin
# This creates a new plugin directory with the modern decorator system
cd time-plugin/
The plugin development workflow is independent of any specific agent project, allowing you to: - Develop plugins separately from specific agents - Share plugins across multiple agents - Publish plugins securely via trusted publishing - Manage with standard Python tools (uv, pip, poetry)
New Decorator-Based System
AgentUp introduces a simple, intuitive decorator system
- @capability decorator: Define plugin capabilities with a single decorator
- Type safety: Full typing support and IDE integration
- Trusted publishing: Built-in security with cryptographic verification
Step 1: Create Your Plugin
Let's create a plugin that provides time and date information:
# Run this from any directory where you want to create the plugin
agentup plugin init time-plugin
# Note: By default, this creates an AI-enabled plugin. Use --template direct for a basic plugin
This creates a new directory with everything you need to get started:
time-plugin/
├── .gitignore
├── pyproject.toml
├── README.md
├── src/
│ └── time_plugin/
│ ├── __init__.py
│ └── plugin.py
├── static/
│ └── logo.png
└── tests/
└── test_time_plugin.py
The pyproject.toml now includes trusted publishing configuration for secure distribution.
Step 2: Examine the Generated Code
Open src/time_plugin/plugin.py:
"""
Time Plugin plugin for AgentUp with AI capabilities.
A plugin that provides Time Plugin functionality
"""
from typing import Dict, Any
from agent.plugins.base import Plugin
from agent.plugins.decorators import capability
from agent.plugins.models import CapabilityContext
class TimePlugin(Plugin):
"""AI-enabled plugin class for Time Plugin."""
def __init__(self):
"""Initialize the plugin."""
super().__init__()
self.name = "time_plugin"
self.version = "1.0.0"
self.llm_service = None
async def initialize(self, config: Dict[str, Any], services: Dict[str, Any]):
"""Initialize plugin with configuration and services."""
self.config = config
# Store LLM service for AI operations
self.llm_service = services.get("llm")
# Setup other services as needed
if "http_client" in services:
self.http_client = services["http_client"]
def _get_parameters(self, context: CapabilityContext) -> dict[str, Any]:
"""Extract parameters from context, checking multiple locations for compatibility."""
params = context.metadata.get("parameters", {})
if not params and context.task and context.task.metadata:
params = context.task.metadata
return params
@capability(
id="time_plugin",
name="Time Plugin",
description="A plugin that provides Time Plugin functionality",
scopes=["time-plugin:use", "ai:function"],
ai_function=True,
ai_parameters={
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The input to process with Time Plugin"
}
},
"required": ["input"]
},
# A2A AgentSkill metadata (optional - add examples for better AI understanding)
examples=[
# Add examples of how to use this capability
# e.g., "Get the current time",
# "What's today's date?",
]
)
async def time_plugin(self, context: CapabilityContext) -> Dict[str, Any]:
"""Execute the time plugin capability."""
try:
# Extract parameters for AI functions
params = self._get_parameters(context)
input_text = params.get("input", "")
# If no input in parameters, try to extract from context
if not input_text:
input_text = self._extract_task_content(context)
# Log the start of processing
self.logger.info("Starting capability execution",
capability_id="time_plugin",
input_length=len(input_text) if input_text else 0)
# AI-powered processing
if self.llm_service and input_text:
# Example: Use LLM to process time-related queries
prompt = f"""Process the following time/date related query:
{input_text}
Provide the current time and date information as requested."""
try:
# Call LLM service - adapt to your LLM service interface
if hasattr(self.llm_service, 'generate'):
response = await self.llm_service.generate(prompt, max_tokens=200)
elif hasattr(self.llm_service, 'chat'):
response = await self.llm_service.chat(
[{"role": "user", "content": prompt}],
max_tokens=200
)
else:
# Fallback if LLM service interface is unknown
import datetime
now = datetime.datetime.now()
response = f"Current time: {now.strftime('%I:%M %p')}, Date: {now.strftime('%A, %B %d, %Y')}"
result = response.strip() if hasattr(response, 'strip') else str(response)
except Exception as llm_error:
self.logger.warning("LLM processing failed, using fallback", error=str(llm_error))
import datetime
now = datetime.datetime.now()
result = f"Current time: {now.strftime('%I:%M %p')}, Date: {now.strftime('%A, %B %d, %Y')}"
else:
# Fallback when no LLM service or no input
import datetime
now = datetime.datetime.now()
result = f"Current time: {now.strftime('%I:%M %p')}, Date: {now.strftime('%A, %B %d, %Y')}"
# Log successful completion
self.logger.info("Capability execution completed",
capability_id="time_plugin",
result_length=len(result) if result else 0)
return {
"success": True,
"content": result,
"metadata": {
"capability": "time_plugin",
"ai_powered": bool(self.llm_service),
"input_length": len(input_text) if input_text else 0
}
}
except Exception as e:
# Log the error with structured data
self.logger.error("Error in capability execution",
capability_id="time_plugin",
error=str(e),
exc_info=True)
return {
"success": False,
"error": str(e),
"content": f"Error in Time Plugin: {str(e)}"
}
Let's break down the key aspects of the simplified template:
- Single @capability decorator: One main capability that can handle various inputs
- _get_parameters() helper: Extracts parameters from context reliably (pattern from production plugins)
- Simplified AI integration: Direct LLM service usage with fallback logic
- Clean structure: Essential methods only - no complex processing modes
- Flexible input handling: Works with both AI function parameters and direct context
- Production patterns: Based on real working plugins like agentup-systools
Step 3: Extending Your Plugin
To add more capabilities to your plugin, simply add more @capability decorated methods:
@capability(
id="get_timezone_info",
name="Get Timezone Information",
description="Get timezone information for a location",
scopes=["time:read"],
ai_function=True,
ai_parameters={
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "Timezone name (optional)",
"default": "local"
},
"include_weekday": {
"type": "boolean",
"description": "Include day of the week",
"default": True
}
}
}
)
async def get_datetime(self, timezone: str = "local", include_weekday: bool = True, **kwargs) -> Dict[str, Any]:
"""Get current date and time together."""
try:
now = datetime.datetime.now()
if include_weekday:
datetime_str = now.strftime("%A, %B %d, %Y at %I:%M %p")
else:
datetime_str = now.strftime("%B %d, %Y at %I:%M %p")
if timezone != "local":
datetime_str += f" ({timezone})"
return {
"success": True,
"content": f"Current date and time: {datetime_str}",
"metadata": {
"timestamp": now.isoformat(),
"timezone": timezone,
"include_weekday": include_weekday
}
}
except Exception as e:
return {
"success": False,
"error": str(e),
"content": f"Error getting datetime: {e}"
}
Step 4: Understanding Automatic Routing
With the new decorator system, routing is handled automatically:
- AI Function Calling: When
ai_function=True, the LLM can directly call your capabilities - Scope-Based Security: Each capability declares required permissions via
scopes - Automatic Discovery: The plugin registry discovers all @capability decorated methods
- Parameter Validation: AI function parameters are automatically validated
No manual routing logic needed! The system intelligently matches user requests to appropriate capabilities based on:
- Function descriptions and parameters
- LLM semantic understanding
- Permission scopes
- Plugin trust levels
Step 5: Install and Test Your Plugin
# Install your plugin in development mode
cd time-plugin
uv add -e . # or pip install -e .
# Verify it's installed and trusted
agentup plugin list
You should see your plugin listed with trust information:
📦 Installed AgentUp Plugins (1)
================================================================================
Name Version Trust Publisher
--------------------------------------------------------------------------------
time-plugin 1.0.0 🟡 community agentup-community
--------------------------------------------------------------------------------
Summary: 🟡 1 community
Step 6: Test in an Agent
Create a simple test agent or use an existing one:
Your plugin is automatically discovered! Just enable it in agentup.yml:
# agentup.yml
name: "Test Agent"
version: "0.1.0"
description: "A test agent for plugin development"
plugins:
- plugin_id: time_plugin
name: Time Plugin
description: Provides time and date information
enabled: true
capabilities:
- capability_id: get_time
enabled: true
required_scopes: ["time:read"]
- capability_id: get_date
enabled: true
required_scopes: ["time:read"]
- capability_id: get_datetime
enabled: true
required_scopes: ["time:read"]
Start the agent:
Now test your plugin by sending requests:
# In another terminal - test AI function calling
curl -s -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_KEY" \
-d '{
"message": "What time is it in 24-hour format?",
"conversation_id": "test-001"
}'
Response:
{
"conversation_id": "test-001",
"message_id": "msg-789",
"response": "Current time: 14:25",
"function_calls": [
{
"function": "get_current_time",
"parameters": {"format": "24hour"},
"result": {
"success": true,
"content": "Current time: 14:25",
"metadata": {
"timestamp": "2025-07-31T14:25:33.123456",
"format": "24hour"
}
}
}
],
"plugin_trust": {
"plugin_id": "time_plugin",
"trust_level": "community",
"publisher": "agentup-community",
"trusted_publishing": false
}
}
Step 7: Understanding AI Function Integration
With the new decorator system, AI functions are automatically generated from your @capability decorated methods when ai_function=True:
Automatic AI Function Generation:
- Method signature becomes function parameters
- ai_parameters provides OpenAI function schema
- Return values are automatically formatted
- Type hints provide additional validation
Example AI function call:
# This capability:
@capability(
id="get_time",
ai_function=True,
ai_parameters={...}
)
async def get_current_time(self, format: str = "12hour", **kwargs):
# Your implementation
# Becomes this AI function:
{
"name": "get_current_time",
"description": "Get the current time in various formats",
"parameters": {
"type": "object",
"properties": {
"format": {
"type": "string",
"enum": ["12hour", "24hour"],
"default": "12hour"
}
}
}
}
The LLM can directly call your method with proper parameters and receive structured responses.
Step 8: Test AI Functions
With an AI-enabled agent, your functions are automatically available:
# Create an AI-enabled agent
agentup init ai-test-agent
cd ai-test-agent
# Configure your AI provider
export OPENAI_API_KEY="your-key-here"
# or export ANTHROPIC_API_KEY="your-key-here"
Update agentup.yml to configure AI and plugin settings:
ai_provider:
provider: openai # or anthropic
model: gpt-4o-mini
api_key: ${OPENAI_API_KEY}
plugins:
- plugin_id: time_plugin
enabled: true
default_scopes: ["time:read"]
Start the agent:
Test AI function calling:
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-H "X-API-Key: dev-key-123" \
-d '{
"message": "What time is it in 24-hour format?"
}'
The LLM will:
1. Understand the request semantically
2. Call your get_current_time function with format: "24hour"
3. Return the formatted response to the user
Step 9: Add Tests
Your plugin already has a test file. Let's add tests for the decorator system:
"""Tests for Time Plugin."""
import pytest
import datetime
from time_plugin.plugin import TimePlugin
@pytest.mark.asyncio
async def test_plugin_capabilities_discovery():
"""Test that capabilities are automatically discovered."""
plugin = TimePlugin()
# Capabilities should be auto-discovered from @capability decorators
assert len(plugin._capabilities) == 3
capability_ids = [cap.id for cap in plugin._capabilities.values()]
assert "get_time" in capability_ids
assert "get_date" in capability_ids
assert "get_datetime" in capability_ids
@pytest.mark.asyncio
async def test_get_time_capability():
"""Test the get_time capability."""
plugin = TimePlugin()
# Test 12-hour format
result = await plugin.get_current_time(format="12hour")
assert result["success"] is True
assert "Current time:" in result["content"]
assert "AM" in result["content"] or "PM" in result["content"]
assert result["metadata"]["format"] == "12hour"
# Test 24-hour format
result = await plugin.get_current_time(format="24hour")
assert result["success"] is True
assert "Current time:" in result["content"]
assert ":" in result["content"]
assert result["metadata"]["format"] == "24hour"
@pytest.mark.asyncio
async def test_get_date_capability():
"""Test the get_date capability."""
plugin = TimePlugin()
# Test long format
result = await plugin.get_current_date(format="long")
assert result["success"] is True
assert "Current date:" in result["content"]
assert result["metadata"]["format"] == "long"
# Test ISO format
result = await plugin.get_current_date(format="iso")
assert result["success"] is True
assert "-" in result["content"] # ISO format has dashes
assert result["metadata"]["format"] == "iso"
@pytest.mark.asyncio
async def test_get_datetime_capability():
"""Test the get_datetime capability."""
plugin = TimePlugin()
result = await plugin.get_datetime(timezone="local", include_weekday=True)
assert result["success"] is True
assert "Current date and time:" in result["content"]
assert "at" in result["content"] # Should contain "at" separator
assert result["metadata"]["timezone"] == "local"
assert result["metadata"]["include_weekday"] is True
def test_ai_function_schemas():
"""Test that AI function schemas are properly generated."""
plugin = TimePlugin()
# Get capability with AI function enabled
time_capability = None
for cap in plugin._capabilities.values():
if cap.id == "get_time":
time_capability = cap
break
assert time_capability is not None
assert time_capability.ai_function is True
assert "properties" in time_capability.ai_parameters
assert "format" in time_capability.ai_parameters["properties"]
def test_scopes_configuration():
"""Test that scopes are properly configured."""
plugin = TimePlugin()
for capability in plugin._capabilities.values():
assert "time:read" in capability.scopes
# Verify security isolation
assert "admin" not in capability.scopes
assert "write" not in capability.scopes[0] # No write permissions
Run your tests:
Step 10: Secure Publishing with Trusted Publishing
Your plugin is ready for secure distribution! The generated pyproject.toml includes trusted publishing configuration:
[project.entry-points."agentup.plugins"]
time_plugin = "time_plugin.plugin:TimePlugin"
# Trusted publishing configuration
[tool.agentup.trusted-publishing]
publisher = "your-github-username"
repository = "your-username/time-plugin"
workflow = "publish.yml"
trust_level = "community"
[tool.agentup.plugin]
capabilities = ["time:current", "date:current"]
scopes = ["time:read"]
min_agentup_version = "2.0.0"
Secure Publishing with GitHub Actions:
# Set up trusted publishing (one-time setup)
# 1. Configure PyPI trusted publisher at https://pypi.org/manage/account/publishing/
# 2. Add your GitHub repo: your-username/time-plugin
# 3. Set workflow: publish.yml
# Build and publish via GitHub Actions
git add .
git commit -m "Release v1.0.0"
git tag v1.0.0
git push origin main --tags
# GitHub Actions will automatically:
# - Run security scans
# - Validate plugin structure
# - Build and publish to PyPI
# - Create cryptographic attestations
Installation with Trust Verification:
# Users can install with trust verification
agentup plugin install time-plugin --require-trusted
# Or verify after installation
agentup plugin verify time-plugin
The trusted publishing system provides: - ✅ Cryptographic verification - ✅ Publisher identity validation - ✅ Automatic security scanning - ✅ Tamper-proof distribution
Troubleshooting
Plugin not loading?
- Check agentup plugin list to see if it's discovered
- Verify your entry point in pyproject.toml points to the correct class
- Make sure you installed with uv add -e . or pip install -e .
- Check that your plugin class inherits from Plugin
Capabilities not discovered?
- Ensure methods are decorated with @capability
- Verify the plugin class calls super().__init__()
- Check that capability IDs are unique
- Import the decorators: from agent.plugins.decorators import capability
AI functions not working?
- Set ai_function=True in the @capability decorator
- Verify ai_parameters follows OpenAI function schema format
- Ensure method signatures match the parameters schema
- Check agent has AI provider configured
Security/scope errors?
- Verify required scopes match capability declarations
- Check agent configuration grants necessary permissions
- Use agentup plugin status to see trust information
- Ensure plugin is from trusted publisher if required
Trust verification failing?
- Check publisher configuration in pyproject.toml
- Verify GitHub repository settings for trusted publishing
- Use agentup plugin verify <plugin-name> for detailed info
- Ensure PyPI trusted publisher is properly configured
Congratulations! You've built your first AgentUp plugin using the modern decorator system. The new architecture provides: - ✅ Simple, intuitive development - ✅ Automatic capability discovery - ✅ Built-in security and trust verification - ✅ Seamless AI function integration - ✅ Production-ready packaging and distribution