# Are you wanting to create your own AG2 agent type?
# You're in the right place.
agent = MyFantasticAgent()

We’ll be getting into the AG2 code-base, it’s useful to understand how AG2 works under the hood, see this section for the rundown.

Creating a new agent in AG2 is easy and there are two main approaches you can take.

  1. Create and/or use tools as the agent’s functionality, keeping the core ConversableAgent functionality pretty much unchanged.
  2. Create a reply function as the method for the agent to carry out its internal workflow.

Tools-based agents

Pros:

  • Easier to implement
  • Can work well with a small distinct set of tools
  • Easy to add another tool
  • If you create tools, they can be used by other agents as well

Cons:

  • Too many tools may confuse the LLM
  • Multiple tool calls can be triggered from one LLM response, this may produce undesired results if the sequence isn’t right
  • Your agent must have an associated LLM as it needs to recommend tool calls

Reply-based agents

Pros:

  • Most flexibility over how the agent works

Cons:

  • More code required, more error handling, more tests
  • If using a tool or two is possible, this may be more complexity than necessary
  • Functionality encapsulated within the agent, can’t be easily used by other agents

How a tool-based agent is created

Let’s take a look at a tool-based agent, the DiscordAgent. You can read more about how it’s used here.

All agents should be based on ConversableAgent, this makes them usable in all orchestrations.

The DiscordAgent uses two tools to send and retrieve messages for a Discord channel. In addition to ConversableAgent’s parameters, it also takes in the authentication and channel details, as well as a boolean to indicate whether writing instructions should be appended to the agent’s system message.

Let’s look at the code for DiscordAgent with annotations added (current code here):

@export_module("autogen.agents.experimental") # Indicates where this appears in the API Reference documentation, autogen > agents > experimental > DiscordAgent
class DiscordAgent(ConversableAgent): # Built on the ConversableAgent class
    """An agent that can send messages and retrieve messages on Discord."""
    # Ensure there's a docstring for the agent for documentation

    DEFAULT_SYSTEM_MESSAGE = (
        "You are a helpful AI assistant that communicates through Discord. "
        "Remember that Discord uses Markdown for formatting and has a character limit. "
        "Keep messages clear and concise, and consider using appropriate formatting when helpful."
    )

    def __init__(
        self,
        bot_token: str, # Discord specific parameter
        channel_name: str, # Discord specific parameter
        guild_name: str, # Discord specific parameter
        system_message: Optional[Union[str, list[str]]] = None, # We provide the ability to override the system message
        has_writing_instructions: bool = True, # Flag to indicate whether writing instructions are added to the system message
        **kwargs: Any,
    ) -> None:
        """Initialize the DiscordAgent.

        Args:
            llm_config (dict[str, Any]): The LLM configuration.
            bot_token (str): Discord bot token
            channel_name (str): Channel name where messages will be sent / retrieved
            guild_name (str): Guild (server) name where the channel is located
            has_writing_instructions (bool): Whether to add writing instructions to the system message. Defaults to True.
        """ # Follow this docstring format

        # We set the system message to the passed in value or a default value
        system_message = kwargs.pop("system_message", self.DEFAULT_SYSTEM_MESSAGE)

        # Our two tools, one for sending and one for retrieving
        self._send_tool = DiscordSendTool(bot_token=bot_token, channel_name=channel_name, guild_name=guild_name)
        self._retrieve_tool = DiscordRetrieveTool(bot_token=bot_token, channel_name=channel_name, guild_name=guild_name)

        # Add formatting instructions to the system message
        if has_writing_instructions:
            formatting_instructions = (
                "\nFormat guidelines for Discord:\n"
                "1. Max message length: 2000 characters\n"
                "2. Supports Markdown formatting\n"
                "3. Can use ** for bold, * for italic, ``` for code blocks\n"
                "4. Consider using appropriate emojis when suitable\n"
            )

            if isinstance(system_message, str):
                system_message = system_message + formatting_instructions
            elif isinstance(system_message, list):
                system_message = system_message + [formatting_instructions]

        # Initialize our base ConversableAgent
        super().__init__(system_message=system_message, **kwargs)

        # Register the two tools with the agent for LLM recommendations (tool execution needs to be handled separately)
        self.register_for_llm()(self._send_tool)
        self.register_for_llm()(self._retrieve_tool)

Agent’s Tool

Let’s look at one of the tools that the agent is using, DiscordSendTool, to see how a Tool is created (annotations added):

(See the Creating a Tool page for a more detailed guide on creating tools)

# Import from 3rd party packages are handled with this context manager
with optional_import_block():
    from discord import Client, Intents, utils

# Some constants
MAX_MESSAGE_LENGTH = 2000
MAX_BATCH_RETRIEVE_MESSAGES = 100

# Denote that this requires a 3rd party package, with "discord" being the namespace
# Our AG2 'extra' is called "commsagent_discord"
@require_optional_import(["discord"], "commsagent-discord")
@export_module("autogen.tools.experimental") # Where this appears in the API Reference documentation
class DiscordSendTool(Tool): # Built on the Tool class
    """Sends a message to a Discord channel."""
    # Ensure there's a docstring for the tool for documentation

    def __init__(self, *, bot_token: str, channel_name: str, guild_name: str) -> None:
        """
        Initialize the DiscordSendTool.

        Args:
            bot_token: The bot token to use for sending messages.
            channel_name: The name of the channel to send messages to.
            guild_name: The name of the guild for the channel.
        """

        # Function that sends the message, uses dependency injection for bot token / channel / guild
        async def discord_send_message(
            message: Annotated[str, "Message to send to the channel."],
            # Dependency Injection used to protect information from LLMs
            # These following three parameters won't be used in tool calling but
            # will be injected in when the tool is executed
            bot_token: Annotated[str, Depends(on(bot_token))],
            guild_name: Annotated[str, Depends(on(guild_name))],
            channel_name: Annotated[str, Depends(on(channel_name))],
        ) -> Any:
            """
            Sends a message to a Discord channel.

            Args:
                message: The message to send to the channel.
                bot_token: The bot token to use for Discord. (uses dependency injection)
                guild_name: The name of the server. (uses dependency injection)
                channel_name: The name of the channel. (uses dependency injection)
            """
            ... # code for the sending of a message in here

        # Initialise the base Tool class with the LLM description and the function to call
        super().__init__(
            name="discord_send",
            description="Sends a message to a Discord channel.",
            func_or_tool=discord_send_message, # This function gets called when the tool is executed
        )

An important aspect of this tool is the use of AG2’s dependency injection functionality. This protects the bot_token, guild_name, and channel_name, from the tool definition for the LLMs, so the LLM will not see the attributes or values. See Tools with Secrets for guidance.

And, that’s all that’s needed to create a brand new agent powered by tools.

How a reply-based agent is created

If tools aren’t a viable option for your agent consider a reply-based agent.

It’s important to understand how ConversableAgent generates a reply, see this page for more details.

Let’s take a look at a basic agent that tells the time utilising a reply function.

from typing import Any, Optional, Union
__all__ = ["TimeAgent"]

# Where our agent appears in the documentation
# autogen > agents > contrib > TimeAgent
@export_module("autogen.agents.contrib")
class TimeAgent(ConversableAgent): # Built on the ConversableAgent class
    """This agent outputs the date and time."""
    # Ensure there's a docstring for the agent for documentation

    def __init__(
        self,
        system_message: Optional[Union[str, list]] = None, # This is how we can use ConversableAgent's parameters
        *args,
        date_time_format: str = "%Y-%m-%d %H:%M:%S", # This is a parameter that is unique to this agent
        **kwargs: Any,
    ) -> None:
        """Initialize the TimeAgent.

        Args:
            date_time_format (str): The format in which the date and time should be returned.
        """ # Follow this docstring format

        # This agent doesn't use an LLM so we don't need this, but it's here as an example
        # of how to take in and use ConversableAgent parameters.
        system_message = system_message or (
            "You are a calendar agent that returns the date and time. "
            "Please provide a date and time format for the responses."
        )

        # Store the date and time format on the agent
        # Prefixed with an underscore to indicate it's a private variable
        self._date_time_format = date_time_format

        # Initialise the base class, passing through the system_message parameter
        super().__init__(*args, system_message=system_message, **kwargs)

        # Our reply function.
        # This one is simple, but yours will be more complex and
        # may even contain another AG2 workflow inside it
        def get_date_time_reply(
            agent: ConversableAgent,
            messages: Optional[list[dict[str, Any]]] = None,
            sender: Optional[Agent] = None,
            config: Optional[OpenAIWrapper] = None,
        ) -> tuple[bool, dict[str, Any]]:

            from datetime import datetime
            now = datetime.now()

            # Format the date and time as a string (e.g., "2025-02-25 14:30:00")
            current_date_time = now.strftime(self._date_time_format)

            # Final reply, with the date/time as the message
            return True, {"content": f"Tick, tock, the current date/time is {current_date_time}."}

        # Register our reply function with the agent
        # Removing all other reply functions so only this one will be used
        self.register_reply(
            trigger=[Agent, None],
            reply_func=get_date_time_reply,
            remove_other_reply_funcs=True
        )

When we use this TimeAgent in an AG2 workflow it will always return the date and time:

Bob (to time_agent):

Hi Time Agent!

--------------------------------------------------------------------------------
time_agent (to Bob):

Tick, tock, the current date/time is 2025-02-25 14:05:24.

--------------------------------------------------------------------------------

Where to put your code

Decide on a folder name that matches your agent name, use underscores to separate words, e.g. deep_research.

Create your agent code in a folder under autogen/agents/contrib/.

Put the tests for the agent in a folder under test/agents/contrib/.

If you are creating tools, put them in a folder under autogen/tools/contrib/.

For tools tests, put them in a folder under test/tools/contrib.

Documentation

As a way for other developers to learn about and understand how to use your agent, it is recommended to create a Jupyter notebook that:

  • Explains what the agent is
  • How to install AG2 for the agent (e.g. with extras)
  • Has sample codes, simple to advanced
  • Notes on capabilities and limitations

As an example, here’s the notebook for the Discord, Slack, and Telegram tools.

3rd party packages

If your agent, or their tools, require a 3rd party package to be installed, add an extra in the pyproject.toml file, for example:

twilio = [
    "fastapi>=0.115.0,<1",
    "uvicorn>=0.30.6,<1",
    "twilio>=9.3.2,<10>"
]

Put the current version of the packages as the minimum version and the next major version for the less than value.

Changes to pyproject.toml cover pyautogen, autogen, and ag2 packages because they propagate automatically to setup_ag2.py and setup_autogen.py.

Tests

It’s critical that tests are created for each piece of functionality within your agent or tool.

See this test file for the WebSurferAgent as an example.

See this documentation for how to run tests locally and coverage.

Create a Pull Request

We’re excited to review and test your agent and tool! Create your Pull Request (PR) here.

Set the PR as a Draft PR if you’re not ready for it to be merged into the AG2 repository.

See our Contributor Guide for more guidance.

Help me get started…

Two basic agents and a tool are available in the agents and tools contrib namespaces that you can look at and use as a starting point for your own agents and tools.