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

> Integrate nanobot with external services by building custom channels providers and tools Explore this guide to seamlessly connect your existing platforms and LLMs to the nanobot framework

- Repository: [✨Data Intelligence Lab@HKU✨/nanobot](https://github.com/HKUDS/nanobot)
- Tags: how-to-guide
- Published: 2026-07-22

---

**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`](https://github.com/HKUDS/nanobot/blob/main/nanobot/bus/queue.py)): Decouples inbound and outbound events from concrete implementations using an async publish-subscribe model.
- **Agent Loop** ([`nanobot/agent/loop.py`](https://github.com/HKUDS/nanobot/blob/main/nanobot/agent/loop.py)): Orchestrates conversation turns, managing context windows, retry logic, and streaming responses.
- **Agent Runner** ([`nanobot/agent/runner.py`](https://github.com/HKUDS/nanobot/blob/main/nanobot/agent/runner.py)): Handles low-level LLM interaction, including tool-call extraction and parallel execution.
- **Providers** ([`nanobot/providers/base.py`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/nanobot/channels/base.py) provides `_handle_message` for standardizing incoming payloads and handles pairing, streaming, and permission checks automatically.

```python

# 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`](https://github.com/HKUDS/nanobot/blob/main/nanobot/channels/__init__.py) to enable automatic discovery:

```python

# 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`](https://github.com/HKUDS/nanobot/blob/main/nanobot/providers/base.py) and implement the `chat` method with the following signature:

```python
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`.

```python

# 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`](https://github.com/HKUDS/nanobot/blob/main/nanobot/agent/tools/base.py), define the JSON schema in `parameters`, and implement `execute`.

```python

# 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`](https://github.com/HKUDS/nanobot/blob/main/nanobot/agent/tools/registry.py) handles schema validation, argument coercion, and error reporting.

```python

# 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`](https://github.com/HKUDS/nanobot/blob/main/nanobot/bus/queue.py)).
2. **Context Assembly**: The `AgentLoop` ([`nanobot/agent/loop.py`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/__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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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`](https://github.com/HKUDS/nanobot/blob/main/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.