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 standardizeschatandchat_streaminterfaces across OpenAI, Anthropic, Azure, and custom endpoints. - Channels (
nanobot/channels/base.py): Adapters for chat platforms that normalize external messages intoInboundMessageevents and transmitOutboundMessageresponses. - 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:
- Inbound Receipt: The
WebhookChannelreceives a POST request and calls_handle_message, which publishes anInboundMessageto theMessageBus(nanobot/bus/queue.py). - Context Assembly: The
AgentLoop(nanobot/agent/loop.py) consumes the event, retrieves conversation history, and builds the prompt payload. - LLM Invocation: The loop calls
RemoteLLMProvider.chat, passing registered tools from theToolRegistry. - Tool Execution: If the LLM returns
tool_calls, theAgentRunner(nanobot/agent/runner.py) dispatches execution throughToolRegistry.executeand feeds results back to the LLM. - Outbound Delivery: The final response is wrapped in an
OutboundMessageand routed back through the channel'ssendmethod.
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_messagefor inbound andsendfor outbound communication. - Extend
LLMProvider(nanobot/providers/base.py) to connect custom LLM endpoints; implementchatand optionallychat_streamwith properLLMResponseformatting. - Extend
Toolto expose custom capabilities; register instances withToolRegistry(nanobot/agent/tools/registry.py) for automatic schema generation and execution. - Leverage auto-discovery by placing modules in
nanobot/channels/ornanobot/providers/; the framework scans these packages viapkgutilat 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →