Skip to content

Bot Manager Guide

Guide to deploying and managing multiple bot instances with BotManager.

Overview

Multi-tenant bot deployment allows a single application to serve multiple clients, each with their own bot configuration and isolated conversations.

Key Concepts:

  • BotManager: Manages bot instances with caching and lifecycle control
  • Bot ID: Unique identifier for each bot configuration
  • Client ID: Tenant identifier within BotContext
  • Conversation ID: Unique conversation identifier
graph TD
    A[BotManager] --> B[support-bot]
    A --> C[sales-bot]
    A --> D[custom-bot]

    B --> E[client-A]
    B --> F[client-B]
    C --> G[client-C]
    C --> H[client-D]
    D --> I[client-E]

    style A fill:#4CAF50
    style B fill:#2196F3
    style C fill:#2196F3
    style D fill:#2196F3

BotManager

Basic Usage

from dataknobs_bots import BotManager, BotContext

# Create manager
manager = BotManager()

# Create or get a bot with inline configuration
bot = await manager.get_or_create("support-bot", config={
    "llm": {"provider": "openai", "model": "gpt-4o"},
    "conversation_storage": {"backend": "memory"},
    "system_prompt": "You are a helpful customer support assistant."
})

# Use the bot
context = BotContext(
    conversation_id="conv-123",
    client_id="client-A",
    user_id="user-456"
)
response = await bot.chat("Hello, I need help", context)

# Get the same bot instance (cached)
same_bot = await manager.get_or_create("support-bot")
assert same_bot is bot  # Same instance

Configuration Loaders

BotManager supports pluggable configuration loading for dynamic bot creation.

Function-based Loader

import yaml

def load_bot_config(bot_id: str) -> dict:
    """Load bot configuration from YAML files."""
    with open(f"configs/{bot_id}.yaml") as f:
        return yaml.safe_load(f)

# Create manager with loader
manager = BotManager(config_loader=load_bot_config)

# Bot will be created using loaded config
bot = await manager.get_or_create("support-bot")

Async Function Loader

async def load_config_from_db(bot_id: str) -> dict:
    """Load configuration from database."""
    async with db.acquire() as conn:
        row = await conn.fetchone(
            "SELECT config FROM bot_configs WHERE bot_id = $1",
            bot_id
        )
        return row["config"]

manager = BotManager(config_loader=load_config_from_db)

Class-based Loader

class ConfigLoader:
    """Configuration loader with caching."""

    def __init__(self, config_dir: str):
        self.config_dir = config_dir
        self._cache = {}

    def load(self, bot_id: str) -> dict:
        if bot_id not in self._cache:
            with open(f"{self.config_dir}/{bot_id}.yaml") as f:
                self._cache[bot_id] = yaml.safe_load(f)
        return self._cache[bot_id]

loader = ConfigLoader("./configs")
manager = BotManager(config_loader=loader)

Bot Lifecycle

# List active bots
active_bots = manager.list_bots()
print(f"Active bots: {active_bots}")

# Get bot count
count = manager.get_bot_count()
print(f"Total bots: {count}")

# Get bot without creating
bot = await manager.get("support-bot")
if bot is None:
    print("Bot not yet created")

# Remove a bot
removed = await manager.remove("support-bot")
print(f"Bot removed: {removed}")

# Reload bot with fresh config (requires config_loader)
bot = await manager.reload("support-bot")

# Clear all bots
await manager.clear_all()

FastAPI Integration

Dependency Injection

The api module provides FastAPI integration with singleton management.

from fastapi import FastAPI
from dataknobs_bots.api import (
    init_bot_manager,
    get_bot_manager,
    BotManagerDep,
)

app = FastAPI()

@app.on_event("startup")
async def startup():
    # Initialize the singleton with a config loader
    init_bot_manager(config_loader=load_config)

@app.post("/chat/{bot_id}")
async def chat(
    bot_id: str,
    message: str,
    manager: BotManagerDep,  # Injected dependency
):
    bot = await manager.get_or_create(bot_id)
    context = BotContext(
        conversation_id="...",
        client_id="..."
    )
    return await bot.chat(message, context)

Exception Handling

Built-in exceptions provide consistent API error responses.

from dataknobs_bots.api import (
    APIError,
    BotNotFoundError,
    BotCreationError,
    ConversationNotFoundError,
    ValidationError,
    ConfigurationError,
    RateLimitError,
    register_exception_handlers,
)

app = FastAPI()

# Register all exception handlers
register_exception_handlers(app)

@app.get("/bots/{bot_id}")
async def get_bot(bot_id: str, manager: BotManagerDep):
    bot = await manager.get(bot_id)
    if not bot:
        raise BotNotFoundError(bot_id)
    return {"bot_id": bot_id, "status": "active"}

Error Response Format:

{
    "error": "BotNotFoundError",
    "message": "Bot with ID 'unknown-bot' not found",
    "detail": {"bot_id": "unknown-bot"},
    "timestamp": "2024-12-08T10:30:00+00:00"
}

Complete FastAPI Example

from fastapi import FastAPI
from pydantic import BaseModel
from dataknobs_bots import BotContext
from dataknobs_bots.api import (
    init_bot_manager,
    reset_bot_manager,
    BotManagerDep,
    BotNotFoundError,
    register_exception_handlers,
)

app = FastAPI(title="Multi-Tenant Bot API")

# Register exception handlers
register_exception_handlers(app)


class ChatRequest(BaseModel):
    message: str
    conversation_id: str
    client_id: str
    user_id: str | None = None


class ChatResponse(BaseModel):
    response: str
    conversation_id: str


def load_config(bot_id: str) -> dict:
    """Load bot configuration."""
    configs = {
        "support": {
            "llm": {"provider": "openai", "model": "gpt-4o"},
            "conversation_storage": {"backend": "memory"},
            "system_prompt": "You are a customer support assistant.",
        },
        "sales": {
            "llm": {"provider": "openai", "model": "gpt-4o"},
            "conversation_storage": {"backend": "memory"},
            "system_prompt": "You are a sales assistant.",
        },
    }
    if bot_id not in configs:
        raise ValueError(f"Unknown bot: {bot_id}")
    return configs[bot_id]


@app.on_event("startup")
async def startup():
    init_bot_manager(config_loader=load_config)


@app.on_event("shutdown")
async def shutdown():
    reset_bot_manager()


@app.post("/bots/{bot_id}/chat", response_model=ChatResponse)
async def chat(
    bot_id: str,
    request: ChatRequest,
    manager: BotManagerDep,
):
    """Chat with a bot."""
    bot = await manager.get_or_create(bot_id)

    context = BotContext(
        conversation_id=request.conversation_id,
        client_id=request.client_id,
        user_id=request.user_id,
    )

    response = await bot.chat(request.message, context)

    return ChatResponse(
        response=response,
        conversation_id=request.conversation_id,
    )


@app.get("/bots")
async def list_bots(manager: BotManagerDep):
    """List active bots."""
    return {
        "bots": manager.list_bots(),
        "count": manager.get_bot_count(),
    }


@app.delete("/bots/{bot_id}")
async def remove_bot(bot_id: str, manager: BotManagerDep):
    """Remove a bot instance."""
    removed = await manager.remove(bot_id)
    if not removed:
        raise BotNotFoundError(bot_id)
    return {"removed": True, "bot_id": bot_id}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Patterns

Pattern 1: Per-Client Bot Configuration

async def get_bot_for_client(manager: BotManager, client_id: str) -> DynaBot:
    """Each client gets their own bot configuration."""
    bot_id = f"bot-{client_id}"

    # Check if bot exists
    bot = await manager.get(bot_id)
    if bot:
        return bot

    # Load client-specific config
    config = await load_client_config(client_id)
    return await manager.get_or_create(bot_id, config=config)

Pattern 2: Shared Bot with Client Context

async def chat_with_shared_bot(
    manager: BotManager,
    client_id: str,
    user_id: str,
    message: str,
) -> str:
    """All clients share the same bot but have isolated conversations."""
    # Single shared bot
    bot = await manager.get_or_create("shared-bot", config=shared_config)

    # Client isolation through context
    context = BotContext(
        conversation_id=f"{client_id}-{user_id}",
        client_id=client_id,
        user_id=user_id,
    )

    return await bot.chat(message, context)

Pattern 3: Bot Pools by Type

class BotPool:
    """Manage pools of bots by type."""

    def __init__(self):
        self.managers = {
            "support": BotManager(config_loader=support_loader),
            "sales": BotManager(config_loader=sales_loader),
            "general": BotManager(config_loader=general_loader),
        }

    async def get_bot(self, bot_type: str, bot_id: str) -> DynaBot:
        manager = self.managers.get(bot_type)
        if not manager:
            raise ValueError(f"Unknown bot type: {bot_type}")
        return await manager.get_or_create(bot_id)

Best Practices

1. Configuration Management

  • Store configurations in version control
  • Use environment variables for secrets
  • Validate configurations at startup

2. Resource Management

  • Set reasonable cache limits for bot instances
  • Implement bot eviction for unused instances
  • Monitor memory usage

3. Error Handling

  • Use specific exception types
  • Log errors with context
  • Return consistent error responses

4. Security

  • Validate client IDs
  • Implement authentication
  • Rate limit requests

5. Monitoring

  • Track bot creation/destruction
  • Monitor conversation counts
  • Alert on errors