Integrating nanobot with External Services: A Complete Guide to Custom Channels, Providers, and Tools

Integrating nanobot with external services requires implementing thin adapter layers—either a BaseChannel subclass for inbound chat platforms, an LLMProvider subclass for custom language models, or a Tool subclass for executable capabilities—that automatically plug into the framework's message bus and agent loop.

nanobot is a modular AI-agent framework developed by HKUDS that enables rapid integration with external services ranging from proprietary LLM endpoints to internal business tools. Its architecture decouples transport mechanisms from agent logic through a unified message bus, allowing developers to connect custom channels, providers, and tools without modifying core orchestration code. This guide demonstrates the precise integration points defined in the source code and provides runnable implementations for production deployments.

Core Architecture for External Integration

The framework exposes six primary extension points for external service integration:

  • Message Bus (nanobot/bus/queue.py): Decouples inbound and outbound events from concrete implementations using an async publish-subscribe model.
  • Agent Loop (nanobot/agent/loop.py): Orchestrates conversation turns, managing context windows, retry logic, and streaming responses.
  • Agent Runner (nanobot/agent/runner.py): Handles low-level LLM interaction, including tool-call extraction and parallel execution.
  • Providers (nanobot/providers/base.py): Abstract base class for LLM backends that standardizes chat and chat_stream interfaces across OpenAI, Anthropic, Azure, and custom endpoints.
  • Channels (nanobot/channels/base.py): Adapters for chat platforms that normalize external messages into InboundMessage events and transmit OutboundMessage responses.
  • Tools Registry (nanobot/agent/tools/registry.py): Dynamically registers, validates, and executes custom tools that the LLM can invoke via function calling.

All components are discovered automatically at startup through pkgutil scanning of the nanobot.channels and nanobot.providers packages.

Implementing Custom Channel Adapters

To receive messages from an external service, subclass BaseChannel and implement the start, stop, and send methods. The base class located at nanobot/channels/base.py provides _handle_message for standardizing incoming payloads and handles pairing, streaming, and permission checks automatically.


# my_channel.py

from nanobot.channels.base import BaseChannel
from nanobot.bus.events import OutboundMessage
from aiohttp import web

class WebhookChannel(BaseChannel):
    name = "webhook"
    display_name = "Custom Webhook"

    async def start(self):
        async def handler(request):
            data = await request.json()
            await self._handle_message(
                sender_id=str(data["user_id"]),
                chat_id=str(data["channel_id"]),
                content=data.get("text", ""),
                media=data.get("files"),
                metadata={"source": "webhook"},
            )
            return web.Response(text="ok")

        app = web.Application()
        app.router.add_post("/nanobot", handler)
        self._runner = web.AppRunner(app)
        await self._runner.setup()
        site = web.TCPSite(self._runner, "0.0.0.0", 8080)
        await site.start()
        self._running = True

    async def stop(self):
        await self._runner.cleanup()
        self._running = False

    async def send(self, msg: OutboundMessage):
        # Implement webhook callback to external service

        pass

Register the channel by importing it in nanobot/channels/__init__.py to enable automatic discovery:


# nanobot/channels/__init__.py

from .my_channel import WebhookChannel  # noqa: F401

Connecting External LLM Providers

To integrate a proprietary or self-hosted LLM, subclass LLMProvider from nanobot/providers/base.py and implement the chat method with the following signature:

async def chat(
    self,
    messages,
    tools=None,
    model=None,
    max_tokens=4096,
    temperature=0.7,
    reasoning_effort=None,
    tool_choice=None
) -> LLMResponse

The implementation must return an LLMResponse object containing content, tool_calls, and finish_reason. Streaming support is optional via chat_stream.


# my_provider.py

import aiohttp
from nanobot.providers.base import LLMProvider, LLMResponse

class RemoteLLMProvider(LLMProvider):
    async def chat(self, messages, tools=None, model=None,
                   max_tokens=4096, temperature=0.7,
                   reasoning_effort=None, tool_choice=None) -> LLMResponse:
        payload = {
            "messages": messages,
            "model": model or self.get_default_model(),
            "max_tokens": max_tokens,
            "temperature": temperature,
        }
        async with aiohttp.ClientSession() as session:
            async with session.post(
                "https://api.example.com/v1/chat", json=payload
            ) as resp:
                data = await resp.json()
        
        return LLMResponse(
            content=data.get("choices", [{}])[0].get("message", {}).get("content"),
            tool_calls=[],
            finish_reason=data.get("choices", [{}])[0].get("finish_reason", "stop"),
        )

    def get_default_model(self) -> str:
        return "example-model"

Registering Custom Tools

Tools extend the agent's capabilities by exposing executable functions to the LLM. Create a subclass of Tool from nanobot/agent/tools/base.py, define the JSON schema in parameters, and implement execute.


# my_tool.py

from nanobot.agent.tools.base import Tool, ToolResult

class EchoTool(Tool):
    name = "echo"
    description = "Return the supplied text unchanged."
    parameters = {
        "type": "object",
        "properties": {
            "text": {
                "type": "string",
                "description": "Text to echo"
            }
        },
        "required": ["text"],
    }

    async def execute(self, text: str) -> ToolResult:
        return ToolResult.success({"echo": text})

Register the tool with the global registry at startup. The registry located at nanobot/agent/tools/registry.py handles schema validation, argument coercion, and error reporting.


# startup.py

from nanobot.agent.tools.registry import ToolRegistry
from my_tool import EchoTool

registry = ToolRegistry()
registry.register(EchoTool())

Runtime Integration Flow

When nanobot gateway starts, it automatically wires all discovered components through the message bus. The execution flow for an inbound message follows this sequence:

  1. Inbound Receipt: The WebhookChannel receives a POST request and calls _handle_message, which publishes an InboundMessage to the MessageBus (nanobot/bus/queue.py).
  2. Context Assembly: The AgentLoop (nanobot/agent/loop.py) consumes the event, retrieves conversation history, and builds the prompt payload.
  3. LLM Invocation: The loop calls RemoteLLMProvider.chat, passing registered tools from the ToolRegistry.
  4. Tool Execution: If the LLM returns tool_calls, the AgentRunner (nanobot/agent/runner.py) dispatches execution through ToolRegistry.execute and feeds results back to the LLM.
  5. Outbound Delivery: The final response is wrapped in an OutboundMessage and routed back through the channel's send method.

This architecture ensures that integrating nanobot with external services requires only implementing the transport-specific logic in channels and providers, while the framework handles retries, streaming, permissions, and tool orchestration automatically.

Summary

  • Extend BaseChannel (nanobot/channels/base.py) to integrate chat platforms or webhooks; implement _handle_message for inbound and send for outbound communication.
  • Extend LLMProvider (nanobot/providers/base.py) to connect custom LLM endpoints; implement chat and optionally chat_stream with proper LLMResponse formatting.
  • Extend Tool to expose custom capabilities; register instances with ToolRegistry (nanobot/agent/tools/registry.py) for automatic schema generation and execution.
  • Leverage auto-discovery by placing modules in nanobot/channels/ or nanobot/providers/; the framework scans these packages via pkgutil at startup.
  • Utilize the message bus (nanobot/bus/queue.py) for decoupled communication between channels, the agent loop, and providers, ensuring robust error handling and streaming support.

Frequently Asked Questions

How does nanobot discover custom channels and providers?

nanobot uses Python's pkgutil module to scan the nanobot.channels and nanobot.providers packages at startup. By placing your subclass modules within these directories and ensuring they are importable in the package __init__.py, the framework automatically instantiates and manages your adapters without explicit configuration.

What methods must a custom LLM provider implement?

According to nanobot/providers/base.py, you must implement async def chat(...) returning an LLMResponse. The method signature must accept messages, tools, model, max_tokens, temperature, reasoning_effort, and tool_choice. Optional streaming support requires implementing async def chat_stream(...) yielding response chunks.

How does nanobot handle tool execution errors?

The ToolRegistry (nanobot/agent/tools/registry.py) catches exceptions during Tool.execute() and wraps them in a ToolResult with error details. These are formatted into the conversation context as function results, allowing the LLM to receive structured error messages and potentially retry or notify the user.

Can I expose nanobot through an OpenAI-compatible HTTP API?

Yes. The framework includes nanobot/api/server.py, which exposes an OpenAI-compatible REST API at /v1/chat/completions. Additionally, the WebUI components in nanobot/webui/* provide a React-based interface that communicates with the gateway via WebSocket, enabling browser-based interactions without custom channel development.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →