Skip to content

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 agentup or pip 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:

# Create a test agent
agentup init time-agent

cd time-agent

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:

agentup run

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:

agentup run

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:

# Using uv (recommended)
uv run pytest tests/ -v

# Or using pytest directly
pytest tests/ -v

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