From 4f50cfac3caaf8872e8e2266d5c86af3d49c3885 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Fri, 27 Mar 2026 13:06:24 -0400 Subject: [PATCH] Add multi-bot Docker setup and improve MCP/tool reliability Document and add multi-bot Docker workflows with env layering scripts, and update agent/tool configuration handling to make MCP/email/calendar behavior more robust for day-to-day operations. Made-with: Cursor --- DEVELOPMENT_WITH_DOCKER.md | 113 ++++++ DOCKER_MULTI_BOT_GUIDE.md | 614 ++++++++++++++++++++++++++++++ ENV_FILES_GUIDE.md | 241 ++++++++++++ MULTI_BOT_CONFIG_MANAGEMENT.md | 222 +++++++++++ MULTI_BOT_SETUP.md | 261 +++++++++++++ QUICK_REFERENCE.md | 106 ++++++ README.md | 11 + SETUP_SUMMARY.md | 116 ++++++ VERIFY_DOCKER_SETUP.md | 125 ++++++ create-bot-configs.sh | 65 ++++ docker-compose.multi.dev.yml | 80 ++++ docker-compose.multi.env.yml | 75 ++++ docker-compose.multi.yml | 61 +++ env-files-setup.sh | 70 ++++ multi-bot-setup.sh | 49 +++ nanobot/agent/context.py | 6 +- nanobot/agent/tools/calendar.py | 39 +- nanobot/agent/tools/email.py | 22 +- nanobot/agent/tools/filesystem.py | 2 +- nanobot/agent/tools/mcp.py | 24 +- nanobot/config/schema.py | 2 +- update-multi-configs.sh | 104 +++++ 22 files changed, 2398 insertions(+), 10 deletions(-) create mode 100644 DEVELOPMENT_WITH_DOCKER.md create mode 100644 DOCKER_MULTI_BOT_GUIDE.md create mode 100644 ENV_FILES_GUIDE.md create mode 100644 MULTI_BOT_CONFIG_MANAGEMENT.md create mode 100644 MULTI_BOT_SETUP.md create mode 100644 QUICK_REFERENCE.md create mode 100644 SETUP_SUMMARY.md create mode 100644 VERIFY_DOCKER_SETUP.md create mode 100755 create-bot-configs.sh create mode 100644 docker-compose.multi.dev.yml create mode 100644 docker-compose.multi.env.yml create mode 100644 docker-compose.multi.yml create mode 100755 env-files-setup.sh create mode 100755 multi-bot-setup.sh create mode 100755 update-multi-configs.sh diff --git a/DEVELOPMENT_WITH_DOCKER.md b/DEVELOPMENT_WITH_DOCKER.md new file mode 100644 index 0000000..acc1e70 --- /dev/null +++ b/DEVELOPMENT_WITH_DOCKER.md @@ -0,0 +1,113 @@ +# Developing Nanobot with Docker + +## Current Setup (Production) + +**`docker-compose.multi.env.yml`** - Production mode: +- Code is **copied** into Docker image during build +- Changes to source code **NOT** picked up automatically +- Need to rebuild image: `docker compose -f docker-compose.multi.env.yml build` + +## Development Setup + +**`docker-compose.multi.dev.yml`** - Development mode: +- Source code is **mounted** as volume +- Changes to `nanobot/` directory **picked up automatically** +- Just restart container (no rebuild needed) + +## How It Works + +### Production Mode (Current) +```bash +# 1. Build image (copies code) +docker compose -f docker-compose.multi.env.yml build + +# 2. Run container +docker compose -f docker-compose.multi.env.yml up -d + +# 3. Make code changes... +# 4. Changes NOT visible - need to rebuild: +docker compose -f docker-compose.multi.env.yml build +docker compose -f docker-compose.multi.env.yml up -d --force-recreate +``` + +### Development Mode (Recommended for Development) +```bash +# 1. Build image once (for dependencies) +docker compose -f docker-compose.multi.dev.yml build + +# 2. Run container +docker compose -f docker-compose.multi.dev.yml up -d + +# 3. Make code changes in venv... +# 4. Changes visible immediately - just restart: +docker compose -f docker-compose.multi.dev.yml restart nanobot-user1 +``` + +## Workflow + +### Option 1: Develop in Venv, Test in Docker (Recommended) + +```bash +# Terminal 1: Develop in venv +source venv/bin/activate +# Edit code, test locally if needed +nano nanobot/channels/telegram.py + +# Terminal 2: Run Docker in dev mode +docker compose -f docker-compose.multi.dev.yml up -d nanobot-user1 + +# After making changes, restart container: +docker compose -f docker-compose.multi.dev.yml restart nanobot-user1 + +# Watch logs: +docker logs -f nanobot-user1-dev +``` + +### Option 2: Rebuild After Changes (Current) + +```bash +# Make changes in venv +source venv/bin/activate +nano nanobot/channels/telegram.py + +# Rebuild Docker image +docker compose -f docker-compose.multi.env.yml build nanobot-user1 + +# Recreate container +docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1 +``` + +## Important Notes + +### Development Mode (`docker-compose.multi.dev.yml`) +- ✅ Changes picked up automatically +- ✅ Faster iteration (no rebuild needed) +- ⚠️ Mounts source code (may have slight performance impact) +- ⚠️ Python needs to reload modules (restart container) + +### Production Mode (`docker-compose.multi.env.yml`) +- ✅ Code baked into image (more stable) +- ✅ No performance impact from mounts +- ❌ Need rebuild for every change +- ✅ Better for production deployments + +## Quick Reference + +```bash +# Development workflow +docker compose -f docker-compose.multi.dev.yml up -d nanobot-user1 +# ... make changes ... +docker compose -f docker-compose.multi.dev.yml restart nanobot-user1 + +# Production workflow +docker compose -f docker-compose.multi.env.yml build nanobot-user1 +docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1 +``` + +## Which Should You Use? + +- **Developing code**: Use `docker-compose.multi.dev.yml` +- **Production/staging**: Use `docker-compose.multi.env.yml` +- **Quick testing**: Use venv directly on host + + diff --git a/DOCKER_MULTI_BOT_GUIDE.md b/DOCKER_MULTI_BOT_GUIDE.md new file mode 100644 index 0000000..2361421 --- /dev/null +++ b/DOCKER_MULTI_BOT_GUIDE.md @@ -0,0 +1,614 @@ +# Docker Multi-Bot Setup Guide + +Complete guide for running multiple nanobot instances with Docker, each with its own Telegram bot. + +## 📋 Table of Contents + +1. [Quick Start](#quick-start) +2. [Architecture Overview](#architecture-overview) +3. [Setup Instructions](#setup-instructions) +4. [Running Bots](#running-bots) +5. [Configuration Management](#configuration-management) +6. [Development Workflow](#development-workflow) +7. [Troubleshooting](#troubleshooting) +8. [Reference](#reference) + +--- + +## Quick Start + +### Start Only User1 Bot +```bash +docker compose -f docker-compose.multi.env.yml up -d nanobot-user1 +``` + +### Start All Bots +```bash +docker compose -f docker-compose.multi.env.yml up -d +``` + +### Stop All Bots +```bash +docker compose -f docker-compose.multi.env.yml down +``` + +### Stop Specific Bot +```bash +docker compose -f docker-compose.multi.env.yml stop nanobot-user1 +``` + +### View Logs +```bash +docker logs -f nanobot-user1 +``` + +--- + +## Architecture Overview + +### How It Works + +Each bot runs in its own Docker container with: +- **Separate config directory**: `~/.nanobot-user1`, `~/.nanobot-user2`, etc. +- **Shared env file**: `.env.shared` (common settings) +- **Bot-specific env file**: `.env.user1`, `.env.user2`, etc. (overrides) +- **Isolated environment**: No conflicts between bots + +### File Structure + +``` +nanobot/ +├── .env.shared # Shared settings (API keys, model, etc.) +├── .env.user1 # Bot 1 overrides +├── .env.user2 # Bot 2 overrides +├── .env.user3 # Bot 3 overrides +├── docker-compose.multi.env.yml # Production compose file +├── docker-compose.multi.dev.yml # Development compose file +│ +└── ~/.nanobot-user1/ + └── config.json # Bot 1 channel config (Telegram token, allowFrom) +└── ~/.nanobot-user2/ + └── config.json # Bot 2 channel config +└── ~/.nanobot-user3/ + └── config.json # Bot 3 channel config +``` + +### Configuration Loading + +1. **Docker Compose** loads environment files: + - First: `.env.shared` (shared settings) + - Second: `.env.userX` (bot-specific overrides) + - Later files override earlier ones + +2. **Container** mounts config directory: + - Host: `~/.nanobot-user1` → Container: `/root/.nanobot` + +3. **Nanobot** loads: + - Environment variables (from Docker env files) + - Config file: `/root/.nanobot/config.json` (mounted from host) + +--- + +## Setup Instructions + +### Step 1: Create Environment Files + +Run the setup script: +```bash +./env-files-setup.sh +``` + +This creates: +- `.env.shared` - Shared settings +- `.env.user1`, `.env.user2`, `.env.user3` - Bot-specific overrides + +### Step 2: Edit `.env.shared` + +Add your shared settings: +```bash +nano .env.shared +``` + +Example: +```bash +# Provider Settings +NANOBOT_PROVIDERS__CUSTOM__API_KEY=no-key +NANOBOT_PROVIDERS__CUSTOM__API_BASE=http://172.17.0.1:11434/v1 + +# Agent Settings +NANOBOT_AGENTS__DEFAULTS__MODEL=llama3.1:8b +NANOBOT_AGENTS__DEFAULTS__WORKSPACE=/mnt/data/nanobot +NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7 +``` + +**Important**: Use `172.17.0.1` (Docker bridge gateway) instead of `localhost` so containers can reach Ollama on the host. + +### Step 3: Edit Bot-Specific Env Files + +Edit `.env.user1`, `.env.user2`, etc. with bot-specific settings: +```bash +nano .env.user1 +``` + +Example: +```bash +# Telegram Bot Token +NANOBOT_CHANNELS__TELEGRAM__ENABLED=true +NANOBOT_CHANNELS__TELEGRAM__TOKEN=your_bot_token_here + +# Email Credentials (if different per bot) +NANOBOT_CHANNELS__EMAIL__IMAP_USERNAME=bot1@example.com +NANOBOT_CHANNELS__EMAIL__IMAP_PASSWORD=password +``` + +### Step 4: Create Config Files + +Run the config creation script: +```bash +./create-bot-configs.sh +``` + +Or manually create `~/.nanobot-user1/config.json`: +```json +{ + "channels": { + "telegram": { + "enabled": true, + "allowFrom": ["YOUR_TELEGRAM_USERNAME"] + }, + "email": { + "enabled": true, + "allowFrom": ["email@example.com"] + } + } +} +``` + +**Note**: `allowFrom` arrays must be in `config.json`, NOT in env files. + +### Step 5: Build Docker Image + +```bash +docker compose -f docker-compose.multi.env.yml build +``` + +--- + +## Running Bots + +### Start Commands + +```bash +# Start only user1 +docker compose -f docker-compose.multi.env.yml up -d nanobot-user1 + +# Start only user2 +docker compose -f docker-compose.multi.env.yml up -d nanobot-user2 + +# Start only user3 +docker compose -f docker-compose.multi.env.yml up -d nanobot-user3 + +# Start all bots +docker compose -f docker-compose.multi.env.yml up -d +``` + +### Stop Commands + +```bash +# Stop only user1 +docker compose -f docker-compose.multi.env.yml stop nanobot-user1 + +# Stop only user2 +docker compose -f docker-compose.multi.env.yml stop nanobot-user2 + +# Stop only user3 +docker compose -f docker-compose.multi.env.yml stop nanobot-user3 + +# Stop all bots +docker compose -f docker-compose.multi.env.yml down +``` + +### Restart Commands + +```bash +# Restart only user1 +docker compose -f docker-compose.multi.env.yml restart nanobot-user1 + +# Restart all bots +docker compose -f docker-compose.multi.env.yml restart +``` + +### Status Commands + +```bash +# Check what's running +docker compose -f docker-compose.multi.env.yml ps + +# View logs for user1 +docker logs -f nanobot-user1 + +# View logs for all bots +docker compose -f docker-compose.multi.env.yml logs -f + +# View logs for specific bot +docker compose -f docker-compose.multi.env.yml logs -f nanobot-user1 +``` + +### Remove Commands + +```bash +# Stop and remove user1 container +docker compose -f docker-compose.multi.env.yml stop nanobot-user1 +docker rm nanobot-user1 + +# Stop and remove all containers +docker compose -f docker-compose.multi.env.yml down + +# Remove containers and volumes (keeps config files) +docker compose -f docker-compose.multi.env.yml down -v +``` + +--- + +## Configuration Management + +### Updating Shared Settings + +Edit `.env.shared`: +```bash +nano .env.shared +``` + +Restart affected containers: +```bash +docker compose -f docker-compose.multi.env.yml restart +``` + +### Updating Bot-Specific Settings + +Edit `.env.userX`: +```bash +nano .env.user1 +``` + +Restart that specific bot: +```bash +docker compose -f docker-compose.multi.env.yml restart nanobot-user1 +``` + +### Updating Config Files + +Edit config file: +```bash +nano ~/.nanobot-user1/config.json +``` + +Restart container: +```bash +docker restart nanobot-user1 +``` + +### Adding Email to allowFrom + +```bash +# Add email to user1's allowFrom +jq '.channels.email.allowFrom += ["newemail@example.com"]' \ + ~/.nanobot-user1/config.json > ~/.nanobot-user1/config.json.tmp && \ + mv ~/.nanobot-user1/config.json.tmp ~/.nanobot-user1/config.json + +# Restart container +docker restart nanobot-user1 +``` + +### Environment Variable Format + +Nanobot uses Pydantic's `BaseSettings` with: +- Prefix: `NANOBOT_` +- Nested delimiter: `__` (double underscore) + +Examples: +- `NANOBOT_PROVIDERS__CUSTOM__API_KEY` → `providers.custom.apiKey` +- `NANOBOT_CHANNELS__TELEGRAM__TOKEN` → `channels.telegram.token` +- `NANOBOT_AGENTS__DEFAULTS__MODEL` → `agents.defaults.model` + +--- + +## Development Workflow + +### Option 1: Development Mode (Recommended) + +Use `docker-compose.multi.dev.yml` which mounts source code: + +```bash +# Start in development mode +docker compose -f docker-compose.multi.dev.yml up -d nanobot-user1 + +# Make changes in venv +source venv/bin/activate +nano nanobot/channels/telegram.py + +# Restart container (picks up changes) +docker compose -f docker-compose.multi.dev.yml restart nanobot-user1 + +# View logs +docker logs -f nanobot-user1-dev +``` + +### Option 2: Rebuild After Changes + +```bash +# Make changes +source venv/bin/activate +nano nanobot/channels/telegram.py + +# Rebuild image +docker compose -f docker-compose.multi.env.yml build nanobot-user1 + +# Recreate container +docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1 +``` + +### Option 3: Run on Host (Not Docker) + +```bash +# Activate venv +source venv/bin/activate + +# Run directly +nanobot gateway +``` + +**Note**: Host command uses: +- Config: `~/.nanobot/config.json` (original) +- .env: `.env` in current directory (NOT `.env.shared`) + +--- + +## Troubleshooting + +### Bot Not Responding + +1. **Check if container is running**: + ```bash + docker ps | grep nanobot-user1 + ``` + +2. **Check logs**: + ```bash + docker logs nanobot-user1 --tail 50 + ``` + +3. **Verify Telegram token**: + ```bash + grep TELEGRAM__TOKEN .env.user1 + ``` + +4. **Check allowFrom**: + ```bash + cat ~/.nanobot-user1/config.json | jq '.channels.telegram.allowFrom' + ``` + +### Connection Error (Ollama) + +**Problem**: "Connection error" when bot tries to use LLM + +**Solution**: +1. Check Ollama is running: + ```bash + curl http://localhost:11434/api/tags + ``` + +2. Verify API_BASE in `.env.shared`: + ```bash + grep API_BASE .env.shared + ``` + Should be: `http://172.17.0.1:11434/v1` (NOT `localhost`) + +3. Restart container: + ```bash + docker restart nanobot-user1 + ``` + +### Config Not Loading + +1. **Check volume mount**: + ```bash + docker inspect nanobot-user1 | grep -A 5 Mounts + ``` + +2. **Verify config exists**: + ```bash + ls -lh ~/.nanobot-user1/config.json + ``` + +3. **Check inside container**: + ```bash + docker exec nanobot-user1 cat /root/.nanobot/config.json + ``` + +### Environment Variables Not Applied + +1. **Check env files are loaded**: + ```bash + docker exec nanobot-user1 env | grep NANOBOT + ``` + +2. **Recreate container** (env files loaded at creation): + ```bash + docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1 + ``` + +### Port Already in Use + +If port conflicts: +```bash +# Check what's using the port +sudo lsof -i :18790 + +# Stop conflicting container +docker stop + +# Or change port in docker-compose.multi.env.yml +``` + +--- + +## Reference + +### Docker Compose Files + +| File | Purpose | Use Case | Env Loading | +|------|---------|----------|-------------| +| `docker-compose.yml` | Single-bot baseline | One gateway + optional CLI, simplest setup | No `env_file`; uses mounted `~/.nanobot/config.json` and container environment | +| `docker-compose.multi.yml` | Multi-bot baseline | Multiple bots with separate config dirs, minimal env indirection | No `env_file`; each bot uses mounted `~/.nanobot-userX/config.json` and container environment | +| `docker-compose.multi.env.yml` | Multi-bot production | Stable multi-bot deployments with shared + per-bot overrides | Loads `.env.shared` first, then `.env.userX` (later file overrides earlier) | +| `docker-compose.multi.dev.yml` | Multi-bot development | Active code development with source mounted into containers | Same env behavior as `multi.env` (`.env.shared` + `.env.userX`) | + +### Container Names + +- `nanobot-user1` - Bot 1 container (production) +- `nanobot-user1-dev` - Bot 1 container (development) +- `nanobot-user2` - Bot 2 container +- `nanobot-user3` - Bot 3 container + +### Ports + +- User 1: `18790` (host) → `18790` (container) +- User 2: `18791` (host) → `18790` (container) +- User 3: `18792` (host) → `18790` (container) + +### Config Locations + +| Location | Used By | Purpose | +|----------|---------|---------| +| `~/.nanobot/config.json` | Host `nanobot gateway` | Original config (not used by Docker) | +| `~/.nanobot-user1/config.json` | Docker user1 | Bot 1 config (mounted into container) | +| `~/.nanobot-user2/config.json` | Docker user2 | Bot 2 config | +| `~/.nanobot-user3/config.json` | Docker user3 | Bot 3 config | + +### Environment Files + +| File | Loaded By | Purpose | +|------|-----------|---------| +| `.env.shared` | All Docker containers | Shared settings (API keys, model, etc.) | +| `.env.user1` | Docker user1 only | Bot 1 specific overrides | +| `.env.user2` | Docker user2 only | Bot 2 specific overrides | +| `.env.user3` | Docker user3 only | Bot 3 specific overrides | +| `.env` | Host `nanobot gateway` | Host environment (not used by Docker) | + +### Quick Command Reference + +```bash +# === START === +docker compose -f docker-compose.multi.env.yml up -d nanobot-user1 # Start user1 +docker compose -f docker-compose.multi.env.yml up -d # Start all + +# === STOP === +docker compose -f docker-compose.multi.env.yml stop nanobot-user1 # Stop user1 +docker compose -f docker-compose.multi.env.yml down # Stop all + +# === RESTART === +docker compose -f docker-compose.multi.env.yml restart nanobot-user1 # Restart user1 +docker compose -f docker-compose.multi.env.yml restart # Restart all + +# === LOGS === +docker logs -f nanobot-user1 # View logs +docker compose -f docker-compose.multi.env.yml logs -f nanobot-user1 # View logs + +# === STATUS === +docker compose -f docker-compose.multi.env.yml ps # List containers +docker ps | grep nanobot # List containers + +# === REBUILD === +docker compose -f docker-compose.multi.env.yml build nanobot-user1 # Rebuild user1 +docker compose -f docker-compose.multi.env.yml build # Rebuild all + +# === RECREATE (after config changes) === +docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1 +``` + +--- + +## Common Tasks + +### Add a New Bot (User4) + +1. Create config directory: + ```bash + mkdir -p ~/.nanobot-user4 + ``` + +2. Create config file: + ```bash + cat > ~/.nanobot-user4/config.json << 'EOF' + { + "channels": { + "telegram": { + "enabled": true, + "allowFrom": ["USERNAME"] + } + } + } + EOF + ``` + +3. Create env file: + ```bash + cp .env.user1 .env.user4 + nano .env.user4 # Edit with bot-specific settings + ``` + +4. Add to `docker-compose.multi.env.yml`: + ```yaml + nanobot-user4: + # ... (copy from nanobot-user1, change port to 18793) + ``` + +5. Start: + ```bash + docker compose -f docker-compose.multi.env.yml up -d nanobot-user4 + ``` + +### Update Ollama API Base + +If Ollama IP changes: +```bash +# Edit .env.shared +sed -i 's|http://172.17.0.1:11434|http://NEW_IP:11434|g' .env.shared + +# Restart all containers +docker compose -f docker-compose.multi.env.yml restart +``` + +### Backup Configurations + +```bash +# Backup all configs +tar -czf nanobot-configs-backup-$(date +%Y%m%d).tar.gz \ + ~/.nanobot-user* \ + .env.shared .env.user* + +# Restore +tar -xzf nanobot-configs-backup-YYYYMMDD.tar.gz +``` + +--- + +## Notes + +- **Original config** (`~/.nanobot/config.json`) is NOT used by Docker containers +- **Host venv** is NOT needed to run Docker commands (Docker builds its own environment) +- **Environment variables** override config file settings +- **Arrays** (like `allowFrom`) must be in `config.json`, NOT in env files +- **Ollama API_BASE** must use `172.17.0.1` (Docker bridge) not `localhost` + +--- + +## See Also + +- `ENV_FILES_GUIDE.md` - Detailed env file management +- `DEVELOPMENT_WITH_DOCKER.md` - Development workflow details +- `SETUP_SUMMARY.md` - Initial setup summary +- `VERIFY_DOCKER_SETUP.md` - Verification checklist + + diff --git a/ENV_FILES_GUIDE.md b/ENV_FILES_GUIDE.md new file mode 100644 index 0000000..3dcd73e --- /dev/null +++ b/ENV_FILES_GUIDE.md @@ -0,0 +1,241 @@ +# Using Separate Env Files Per Container + +This guide shows you how to use separate `.env` files for each bot container, making it easy to manage both shared and bot-specific settings. + +## How It Works + +Docker Compose loads `env_file` entries in order. Later files override earlier ones: + +1. **`.env.shared`** - Loaded first, contains common settings +2. **`.env.user1`, `.env.user2`, `.env.user3`** - Loaded after, can override shared settings + +This means: +- ✅ Shared settings go in `.env.shared` (update once) +- ✅ Bot-specific overrides go in `.env.user1`, `.env.user2`, etc. (only if needed) +- ✅ Easy to edit (plain text files, no JSON) + +## Quick Setup + +### Step 1: Create Env Files + +Run the setup script: + +```bash +chmod +x env-files-setup.sh +./env-files-setup.sh +``` + +This creates: +- `.env.shared` - Shared settings for all bots +- `.env.user1` - Overrides for bot 1 (optional) +- `.env.user2` - Overrides for bot 2 (optional) +- `.env.user3` - Overrides for bot 3 (optional) + +### Step 2: Edit `.env.shared` + +Edit `.env.shared` with your shared settings: + +```bash +# Shared configuration for all nanobot instances +NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-your-actual-key +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5 +NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7 +NANOBOT_AGENTS__DEFAULTS__MAX_TOKENS=8192 +``` + +### Step 3: Edit Bot-Specific Files (Optional) + +If a bot needs different settings, edit its `.env.userX` file: + +**`.env.user1`** (example - override model for bot 1): +```bash +# Override model for this bot only +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-sonnet-4 +``` + +**`.env.user2`** (example - override temperature): +```bash +# Override temperature for this bot only +NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.9 +``` + +**`.env.user3`** (example - leave empty to use all shared settings): +```bash +# This bot uses all settings from .env.shared +# No overrides needed +``` + +### Step 4: Create Minimal Config Files + +Each bot still needs a minimal `config.json` with bot-specific channel settings: + +**`~/.nanobot-user1/config.json`**: +```json +{ + "channels": { + "telegram": { + "enabled": true, + "token": "BOT_TOKEN_FOR_USER1", + "allowFrom": ["USER1_TELEGRAM_ID"] + } + } +} +``` + +**`~/.nanobot-user2/config.json`**: +```json +{ + "channels": { + "telegram": { + "enabled": true, + "token": "BOT_TOKEN_FOR_USER2", + "allowFrom": ["USER2_TELEGRAM_ID"] + } + } +} +``` + +### Step 5: Run with Docker Compose + +```bash +docker compose -f docker-compose.multi.env.yml up -d +``` + +## File Structure + +``` +nanobot/ +├── .env.shared ← Shared settings (API keys, model, etc.) +├── .env.user1 ← Bot 1 overrides (optional) +├── .env.user2 ← Bot 2 overrides (optional) +├── .env.user3 ← Bot 3 overrides (optional) +├── docker-compose.multi.env.yml +│ +└── ~/.nanobot-user1/ + └── config.json ← Bot 1 channel config (Telegram token, user ID) +└── ~/.nanobot-user2/ + └── config.json ← Bot 2 channel config +└── ~/.nanobot-user3/ + └── config.json ← Bot 3 channel config +``` + +## Examples + +### Example 1: All Bots Use Same Settings + +**`.env.shared`**: +```bash +NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5 +``` + +**`.env.user1`**, **`.env.user2`**, **`.env.user3`**: +```bash +# Empty - all bots use shared settings +``` + +### Example 2: One Bot Uses Different Model + +**`.env.shared`**: +```bash +NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5 +``` + +**`.env.user1`**: +```bash +# Bot 1 uses a different model +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-sonnet-4 +``` + +**`.env.user2`**, **`.env.user3`**: +```bash +# Empty - use shared model +``` + +### Example 3: One Bot Uses Different API Key + +**`.env.shared`**: +```bash +NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-shared-key +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5 +``` + +**`.env.user2`**: +```bash +# Bot 2 uses its own API key +NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-user2-key +``` + +## Updating Settings + +### Update Shared Settings + +Edit `.env.shared` and restart all containers: + +```bash +# Edit shared settings +nano .env.shared + +# Restart all bots +docker compose -f docker-compose.multi.env.yml restart +``` + +### Update Bot-Specific Settings + +Edit the specific `.env.userX` file and restart that bot: + +```bash +# Edit bot 1's settings +nano .env.user1 + +# Restart only bot 1 +docker restart nanobot-user1 +``` + +## Environment Variable Format + +Nanobot uses Pydantic's `BaseSettings` with: +- Prefix: `NANOBOT_` +- Nested delimiter: `__` (double underscore) + +Examples: +- `NANOBOT_PROVIDERS__OPENROUTER__API_KEY` → `providers.openrouter.apiKey` +- `NANOBOT_AGENTS__DEFAULTS__MODEL` → `agents.defaults.model` +- `NANOBOT_AGENTS__DEFAULTS__TEMPERATURE` → `agents.defaults.temperature` + +## Advantages + +✅ **Easy to edit** - Plain text files, no JSON syntax +✅ **Clear separation** - Shared vs bot-specific settings +✅ **Flexible** - Override only what you need +✅ **Version control friendly** - Can commit `.env.shared`, ignore `.env.userX` if they contain secrets +✅ **No config.json editing** - Only edit env files for most changes + +## Troubleshooting + +### Settings Not Applied + +1. Check if env files exist: + ```bash + ls -la .env.* + ``` + +2. Check what's loaded in container: + ```bash + docker exec nanobot-user1 env | grep NANOBOT + ``` + +3. Restart container after changes: + ```bash + docker restart nanobot-user1 + ``` + +### Override Not Working + +Remember: Later files override earlier ones. If `.env.user1` has a setting but it's not applied, check: +- Is the variable name correct? (use `__` not `.`) +- Did you restart the container? +- Is the setting in `.env.shared` overriding it? (remove from `.env.shared` if you want bot-specific only) + + diff --git a/MULTI_BOT_CONFIG_MANAGEMENT.md b/MULTI_BOT_CONFIG_MANAGEMENT.md new file mode 100644 index 0000000..1a5081c --- /dev/null +++ b/MULTI_BOT_CONFIG_MANAGEMENT.md @@ -0,0 +1,222 @@ +# Managing Multiple Bot Configs Efficiently + +Yes, with Docker you'll need to manage multiple config files, but here are strategies to make it easier: + +## The Challenge + +When you have 3 bots, you have 3 config files: +- `~/.nanobot-user1/config.json` +- `~/.nanobot-user2/config.json` +- `~/.nanobot-user3/config.json` + +If you need to change something like: +- API key (shared across all bots) +- Model settings (might be shared) +- Tool configurations (might be shared) + +You'd normally need to edit all 3 files. + +## Solution 1: Use Environment Variables for Shared Settings + +Nanobot supports environment variables! You can set shared settings via environment variables in Docker. + +### Update docker-compose.multi.yml + +```yaml +services: + nanobot-user1: + # ... existing config ... + environment: + # Shared settings - set once, applies to all + NANOBOT_PROVIDERS__OPENROUTER__API_KEY: "sk-or-v1-xxx" + NANOBOT_AGENTS__DEFAULTS__MODEL: "anthropic/claude-opus-4-5" + NANOBOT_AGENTS__DEFAULTS__TEMPERATURE: "0.7" + # Bot-specific settings still in config.json + volumes: + - ~/.nanobot-user1:/root/.nanobot + + nanobot-user2: + # ... same environment variables ... + environment: + NANOBOT_PROVIDERS__OPENROUTER__API_KEY: "sk-or-v1-xxx" + NANOBOT_AGENTS__DEFAULTS__MODEL: "anthropic/claude-opus-4-5" + NANOBOT_AGENTS__DEFAULTS__TEMPERATURE: "0.7" + volumes: + - ~/.nanobot-user2:/root/.nanobot +``` + +**Better yet**, use a shared `.env` file: + +### Create `.env.shared`: + +```bash +# Shared settings for all bots +NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5 +NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7 +NANOBOT_AGENTS__DEFAULTS__MAX_TOKENS=8192 +``` + +### Update docker-compose.multi.yml: + +```yaml +services: + nanobot-user1: + env_file: + - .env.shared # Load shared settings + volumes: + - ~/.nanobot-user1:/root/.nanobot + # ... rest of config ... + + nanobot-user2: + env_file: + - .env.shared # Same shared settings + volumes: + - ~/.nanobot-user2:/root/.nanobot + # ... rest of config ... +``` + +Now you only update `.env.shared` for shared settings! + +## Solution 2: Minimal Configs + Environment Variables + +Keep only bot-specific settings in config files: + +**`~/.nanobot-user1/config.json`** (minimal): +```json +{ + "channels": { + "telegram": { + "enabled": true, + "token": "BOT_TOKEN_1", + "allowFrom": ["USER_ID_1"] + } + } +} +``` + +**`~/.nanobot-user2/config.json`** (minimal): +```json +{ + "channels": { + "telegram": { + "enabled": true, + "token": "BOT_TOKEN_2", + "allowFrom": ["USER_ID_2"] + } + } +} +``` + +Everything else comes from `.env.shared`! + +## Solution 3: Use the Update Script + +I've created `update-multi-configs.sh` to batch-update configs: + +```bash +# Update API key in all configs +./update-multi-configs.sh update-api-key openrouter "sk-or-v1-new-key" + +# Update model in all configs +./update-multi-configs.sh update-model "anthropic/claude-opus-4-5" + +# Update any setting +./update-multi-configs.sh update-setting "agents.defaults.temperature" "0.8" +``` + +Requires `jq` to be installed: +```bash +sudo apt install jq # Linux +brew install jq # macOS +``` + +## Solution 4: Base Config Template + +Create a template and only override what's different: + +**`~/.nanobot-base/config.json`** (template): +```json +{ + "providers": { + "openrouter": { + "apiKey": "sk-or-v1-xxx" + } + }, + "agents": { + "defaults": { + "model": "anthropic/claude-opus-4-5", + "temperature": 0.7 + } + }, + "channels": { + "telegram": { + "enabled": true, + "token": "REPLACE_WITH_BOT_TOKEN", + "allowFrom": ["REPLACE_WITH_USER_ID"] + } + } +} +``` + +Then create bot-specific configs by copying and modifying: + +```bash +cp ~/.nanobot-base/config.json ~/.nanobot-user1/config.json +# Edit only the telegram token and user ID + +cp ~/.nanobot-base/config.json ~/.nanobot-user2/config.json +# Edit only the telegram token and user ID +``` + +## Recommended Approach + +**Use Solution 1 (Environment Variables)** - it's the cleanest: + +1. Create `.env.shared` with all shared settings +2. Keep minimal configs with only bot-specific settings (Telegram tokens/user IDs) +3. Update `.env.shared` when you need to change shared settings +4. Restart containers to apply changes + +This way: +- ✅ Shared settings: Update once in `.env.shared` +- ✅ Bot-specific settings: Only in each config.json +- ✅ Easy to manage and maintain + +## Example: Complete Setup + +**`.env.shared`**: +```bash +NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5 +NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7 +NANOBOT_AGENTS__DEFAULTS__MAX_TOKENS=8192 +``` + +**`~/.nanobot-user1/config.json`**: +```json +{ + "channels": { + "telegram": { + "enabled": true, + "token": "1234567890:ABC...", + "allowFrom": ["123456789"] + } + } +} +``` + +**`docker-compose.multi.yml`**: +```yaml +services: + nanobot-user1: + env_file: + - .env.shared + volumes: + - ~/.nanobot-user1:/root/.nanobot + # ... rest ... +``` + +Now when you need to change the API key or model, just edit `.env.shared` and restart! + + diff --git a/MULTI_BOT_SETUP.md b/MULTI_BOT_SETUP.md new file mode 100644 index 0000000..e60cd42 --- /dev/null +++ b/MULTI_BOT_SETUP.md @@ -0,0 +1,261 @@ +# Running Multiple Nanobot Gateways with Docker + +This guide shows you how to run multiple nanobot gateway instances, each with its own Telegram bot. + +## Quick Start + +### Step 1: Setup Config Directories + +Run the setup script: + +```bash +./multi-bot-setup.sh +``` + +Or manually: + +```bash +# Create directories +mkdir -p ~/.nanobot-user1 +mkdir -p ~/.nanobot-user2 +mkdir -p ~/.nanobot-user3 + +# Copy your base config (if you have one) +cp ~/.nanobot/config.json ~/.nanobot-user1/config.json +cp ~/.nanobot/config.json ~/.nanobot-user2/config.json +cp ~/.nanobot/config.json ~/.nanobot-user3/config.json +``` + +### Step 2: Configure Each Bot + +Edit each config file with different Telegram bot tokens: + +**`~/.nanobot-user1/config.json`:** +```json +{ + "providers": { + "openrouter": { + "apiKey": "sk-or-v1-xxx" + } + }, + "agents": { + "defaults": { + "model": "anthropic/claude-opus-4-5" + } + }, + "channels": { + "telegram": { + "enabled": true, + "token": "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz", + "allowFrom": ["123456789"] + } + } +} +``` + +**`~/.nanobot-user2/config.json`:** +```json +{ + "providers": { + "openrouter": { + "apiKey": "sk-or-v1-xxx" + } + }, + "agents": { + "defaults": { + "model": "anthropic/claude-opus-4-5" + } + }, + "channels": { + "telegram": { + "enabled": true, + "token": "9876543210:XYZabcDEFghiJKLmnopQRSuvwx", + "allowFrom": ["987654321"] + } + } +} +``` + +**`~/.nanobot-user3/config.json`:** +```json +{ + "providers": { + "openrouter": { + "apiKey": "sk-or-v1-xxx" + } + }, + "agents": { + "defaults": { + "model": "anthropic/claude-opus-4-5" + } + }, + "channels": { + "telegram": { + "enabled": true, + "token": "5551234567:LMNopqRSTuvwXYZabcdefGHIjkl", + "allowFrom": ["555123456"] + } + } +} +``` + +### Step 3: Run with Docker Compose (Recommended) + +```bash +# Build the image (first time only) +docker compose -f docker-compose.multi.yml build + +# Start all bots +docker compose -f docker-compose.multi.yml up -d + +# View logs +docker compose -f docker-compose.multi.yml logs -f + +# Stop all bots +docker compose -f docker-compose.multi.yml down + +# Start/stop individual bots +docker compose -f docker-compose.multi.yml start nanobot-user1 +docker compose -f docker-compose.multi.yml stop nanobot-user2 +``` + +### Step 4: Run with Docker Run Commands (Alternative) + +If you prefer `docker run` commands: + +```bash +# Build the image first +docker build -t nanobot . + +# Run bot 1 +docker run -d \ + --name nanobot-user1 \ + -v ~/.nanobot-user1:/root/.nanobot \ + -p 18790:18790 \ + --restart unless-stopped \ + nanobot gateway + +# Run bot 2 +docker run -d \ + --name nanobot-user2 \ + -v ~/.nanobot-user2:/root/.nanobot \ + -p 18791:18790 \ + --restart unless-stopped \ + nanobot gateway + +# Run bot 3 +docker run -d \ + --name nanobot-user3 \ + -v ~/.nanobot-user3:/root/.nanobot \ + -p 18792:18790 \ + --restart unless-stopped \ + nanobot gateway +``` + +## Managing Containers + +### View Logs + +```bash +# All bots +docker compose -f docker-compose.multi.yml logs -f + +# Specific bot +docker logs -f nanobot-user1 + +# Or with docker-compose +docker compose -f docker-compose.multi.yml logs -f nanobot-user1 +``` + +### Stop/Start Containers + +```bash +# Stop all +docker compose -f docker-compose.multi.yml down + +# Start all +docker compose -f docker-compose.multi.yml up -d + +# Restart specific bot +docker restart nanobot-user1 + +# Stop specific bot +docker stop nanobot-user1 +docker start nanobot-user1 +``` + +### Check Status + +```bash +# List all running containers +docker ps | grep nanobot + +# Check logs for errors +docker logs nanobot-user1 --tail 50 +``` + +## Port Mapping + +Each bot uses a different host port: +- **User 1**: Port `18790` → Container port `18790` +- **User 2**: Port `18791` → Container port `18790` +- **User 3**: Port `18792` → Container port `18790` + +The gateway port inside the container is always `18790`, but mapped to different host ports to avoid conflicts. + +## Creating Telegram Bots + +For each user, create a separate bot: + +1. Open Telegram, search `@BotFather` +2. Send `/newbot` +3. Follow prompts to create a bot +4. Copy the token +5. Add it to the respective config file + +## Troubleshooting + +### Bot not responding + +```bash +# Check if container is running +docker ps | grep nanobot-user1 + +# Check logs for errors +docker logs nanobot-user1 + +# Verify config is correct +cat ~/.nanobot-user1/config.json | jq '.channels.telegram' +``` + +### Port already in use + +If you get port conflicts, change the port mappings in `docker-compose.multi.yml`: + +```yaml +ports: + - "18890:18790" # Change 18790 to 18890 +``` + +### Config not loading + +Make sure the volume mount is correct: +```bash +# Verify config exists +ls -la ~/.nanobot-user1/config.json + +# Check if it's readable +docker exec nanobot-user1 cat /root/.nanobot/config.json +``` + +## Adding More Bots + +To add more bots: + +1. Create new directory: `mkdir -p ~/.nanobot-user4` +2. Copy config: `cp ~/.nanobot-user1/config.json ~/.nanobot-user4/config.json` +3. Edit config with new bot token +4. Add new service to `docker-compose.multi.yml` (copy existing service, change name and port) +5. Run: `docker compose -f docker-compose.multi.yml up -d nanobot-user4` + + diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md new file mode 100644 index 0000000..cdefeea --- /dev/null +++ b/QUICK_REFERENCE.md @@ -0,0 +1,106 @@ +# Quick Reference Card + +## 🚀 Most Common Commands + +### Start/Stop + +```bash +# Start user1 only +docker compose -f docker-compose.multi.env.yml up -d nanobot-user1 + +# Start all bots +docker compose -f docker-compose.multi.env.yml up -d + +# Stop user1 only +docker compose -f docker-compose.multi.env.yml stop nanobot-user1 + +# Stop all bots +docker compose -f docker-compose.multi.env.yml down + +# Restart user1 +docker compose -f docker-compose.multi.env.yml restart nanobot-user1 +``` + +### Logs + +```bash +# View logs (follow) +docker logs -f nanobot-user1 + +# View last 50 lines +docker logs --tail 50 nanobot-user1 +``` + +### Status + +```bash +# Check what's running +docker compose -f docker-compose.multi.env.yml ps + +# Check specific container +docker ps | grep nanobot-user1 +``` + +### Configuration + +```bash +# Edit shared settings +nano .env.shared +docker compose -f docker-compose.multi.env.yml restart + +# Edit bot-specific settings +nano .env.user1 +docker restart nanobot-user1 + +# Edit config file +nano ~/.nanobot-user1/config.json +docker restart nanobot-user1 +``` + +### Development + +```bash +# Use dev mode (mounts source code) +docker compose -f docker-compose.multi.dev.yml up -d nanobot-user1 + +# After code changes +docker compose -f docker-compose.multi.dev.yml restart nanobot-user1 +``` + +## 📁 File Locations + +| What | Where | +|------|-------| +| Shared settings | `.env.shared` | +| Bot 1 settings | `.env.user1` | +| Bot 1 config | `~/.nanobot-user1/config.json` | +| Production compose | `docker-compose.multi.env.yml` | +| Dev compose | `docker-compose.multi.dev.yml` | + +## 🧭 Which Compose File? + +| File | Best For | Uses env files? | +|------|----------|-----------------| +| `docker-compose.yml` | Single bot (`nanobot-gateway` + optional `nanobot-cli`) | No | +| `docker-compose.multi.yml` | Multi-bot with per-user config directories | No | +| `docker-compose.multi.env.yml` | Multi-bot with shared + per-user env overrides (recommended) | Yes: `.env.shared` then `.env.userX` | +| `docker-compose.multi.dev.yml` | Same as above, but with source code mounted for development | Yes: `.env.shared` then `.env.userX` | + +## 🔧 Troubleshooting + +```bash +# Bot not responding? +docker logs nanobot-user1 --tail 50 + +# Connection error? +grep API_BASE .env.shared # Should be 172.17.0.1:11434 + +# Config not loading? +docker exec nanobot-user1 cat /root/.nanobot/config.json +``` + +## 📖 Full Documentation + +See `DOCKER_MULTI_BOT_GUIDE.md` for complete guide. + + diff --git a/README.md b/README.md index a474367..cbaac12 100644 --- a/README.md +++ b/README.md @@ -742,6 +742,8 @@ That's it! Environment variables, model prefixing, config matching, and `nanobot nanobot supports [MCP](https://modelcontextprotocol.io/) — connect external tool servers and use them as native agent tools. +For a full Gmail MCP walkthrough (config + OAuth + verification), see [`docs/gmail_mcp_setup.md`](docs/gmail_mcp_setup.md). + Add MCP servers to your `config.json`: ```json @@ -827,6 +829,15 @@ vim ~/.nanobot/config.json # add API keys docker compose up -d nanobot-gateway # start gateway ``` +#### Which Compose File To Use? + +| File | Scenario | Notes | +|------|----------|-------| +| `docker-compose.yml` | Single-bot local usage | One gateway + optional CLI, mounts `~/.nanobot` | +| `docker-compose.multi.yml` | Multi-bot with separate per-user config directories | No `env_file`; use `~/.nanobot-userX/config.json` per bot | +| `docker-compose.multi.env.yml` | Multi-bot with shared and per-user environment overrides | Loads `.env.shared` then `.env.userX` (recommended for multi-bot ops) | +| `docker-compose.multi.dev.yml` | Multi-bot development | Same env layering as `multi.env`, plus source mount for live code iteration | + ```bash docker compose run --rm nanobot-cli agent -m "Hello!" # run CLI docker compose logs -f nanobot-gateway # view logs diff --git a/SETUP_SUMMARY.md b/SETUP_SUMMARY.md new file mode 100644 index 0000000..9a2277d --- /dev/null +++ b/SETUP_SUMMARY.md @@ -0,0 +1,116 @@ +# Multi-Bot Docker Setup Summary + +## ✅ Setup Complete + +### Files Created + +1. **Environment Files:** + - `.env.shared` - Shared settings (providers, agents, tools, gateway) + - `.env.user1` - Bot 1 specific settings (Telegram token, email credentials) + - `.env.user2` - Bot 2 specific settings (placeholder) + - `.env.user3` - Bot 3 specific settings (placeholder) + +2. **Config Files:** + - `~/.nanobot-user1/config.json` - Bot 1 channel config (allowFrom arrays) + - `~/.nanobot-user2/config.json` - Bot 2 channel config (placeholder) + - `~/.nanobot-user3/config.json` - Bot 3 channel config (placeholder) + +3. **Docker Compose:** + - `docker-compose.multi.env.yml` - Multi-bot Docker Compose configuration + +## 🔍 How It Works + +### Configuration Loading Order + +1. **Docker Compose loads environment files:** + - First: `.env.shared` (shared settings) + - Second: `.env.user1` (bot-specific overrides) + - Later files override earlier ones ✅ + +2. **Container starts with volume mount:** + - Host: `~/.nanobot-user1` + - Container: `/root/.nanobot` + - This maps your config directory into the container ✅ + +3. **Nanobot loads configuration:** + - **Environment variables**: Automatically loaded by Pydantic `BaseSettings` + - Format: `NANOBOT_CHANNELS__TELEGRAM__TOKEN=...` + - These come from Docker environment (loaded from `.env.shared` + `.env.user1`) + + - **Config file**: Loaded from `/root/.nanobot/config.json` + - Inside container: `/root/.nanobot/config.json` + - On host: `~/.nanobot-user1/config.json` (mounted) + - Contains: `allowFrom` arrays (can't be in env vars) + +### Important: Original Config is NOT Used + +**Your original config** (`~/.nanobot/config.json`) is **NOT** used by Docker containers. + +Each container uses: +- Its own env files (`.env.shared` + `.env.userX`) +- Its own config directory (`~/.nanobot-userX`) + +This means: +- ✅ Original config stays untouched +- ✅ Each bot has isolated configuration +- ✅ No conflicts between bots + +## 📋 Current Configuration + +### Bot 1 (nanobot-user1) + +**Environment Variables** (from `.env.shared` + `.env.user1`): +- Provider: Custom/Ollama (`http://localhost:11434/v1`) +- Model: `llama3.1:8b` +- Workspace: `/mnt/data/nanobot` +- Telegram: Enabled with token +- Email: Enabled with credentials + +**Config File** (`~/.nanobot-user1/config.json`): +- Telegram `allowFrom`: `["TADec2023"]` +- Email `allowFrom`: `["adayear2025@gmail.com"]` + +## 🚀 Running the Bots + +```bash +# Build Docker image (first time) +docker compose -f docker-compose.multi.env.yml build + +# Start all bots +docker compose -f docker-compose.multi.env.yml up -d + +# View logs +docker compose -f docker-compose.multi.env.yml logs -f + +# Stop all bots +docker compose -f docker-compose.multi.env.yml down + +# Restart specific bot +docker restart nanobot-user1 +``` + +## ✅ Verification Checklist + +- [x] `.env.shared` created with shared settings +- [x] `.env.user1` created with bot-specific settings +- [x] `ALLOW_FROM` removed from env files (arrays belong in config.json) +- [x] Config directories created (`~/.nanobot-user1`, etc.) +- [x] Config files created with `allowFrom` arrays +- [x] Docker Compose file configured correctly +- [x] Volume mounts map host configs to container paths + +## 🎯 Key Points + +1. **Environment Variables** → For simple key-value settings (tokens, API keys, models) +2. **Config Files** → For complex settings (arrays like `allowFrom`) +3. **Docker Mounts** → Each container gets its own config directory +4. **Original Config** → Not used by Docker containers (stays safe) + +## 📝 Next Steps + +1. Update `.env.user2` and `.env.user3` with bot-specific settings +2. Update `~/.nanobot-user2/config.json` and `~/.nanobot-user3/config.json` with actual user IDs/emails +3. Run `docker compose -f docker-compose.multi.env.yml up -d` to start all bots +4. Check logs to verify each bot is using correct configuration + + diff --git a/VERIFY_DOCKER_SETUP.md b/VERIFY_DOCKER_SETUP.md new file mode 100644 index 0000000..0864b8c --- /dev/null +++ b/VERIFY_DOCKER_SETUP.md @@ -0,0 +1,125 @@ +# Verifying Docker Multi-Bot Setup + +## ✅ Configuration Check + +### 1. Environment Files + +**`.env.shared`** - Contains: +- ✅ Provider settings (custom/Ollama) +- ✅ Agent defaults (model, workspace, temperature, etc.) +- ✅ Tool settings +- ✅ Gateway settings +- ✅ Email IMAP/SMTP settings (shared) + +**`.env.user1`** - Contains: +- ✅ Telegram token (bot-specific) +- ✅ Email credentials (bot-specific override) +- ⚠️ **Issue**: `ALLOW_FROM` should NOT be in env files (arrays don't work in env vars) + +### 2. Config Files + +**`~/.nanobot-user1/config.json`** - Contains: +- ✅ Telegram `allowFrom` array (correct location) +- ✅ Email `allowFrom` array (correct location) + +### 3. Docker Compose + +**`docker-compose.multi.env.yml`** - Correctly configured: +- ✅ Loads `.env.shared` first +- ✅ Loads `.env.user1` second (overrides shared) +- ✅ Mounts `~/.nanobot-user1:/root/.nanobot` (maps host config to container) + +## 🔍 How Nanobot Loads Config in Docker + +1. **Environment Variables** (from `.env.shared` and `.env.user1`): + - Loaded by Docker Compose into container environment + - Nanobot's Pydantic `BaseSettings` reads them automatically + - Format: `NANOBOT_CHANNELS__TELEGRAM__TOKEN=...` + +2. **Config File** (`config.json`): + - Path inside container: `/root/.nanobot/config.json` + - Maps to host: `~/.nanobot-user1/config.json` + - Loaded by `load_config()` which calls `get_config_path()` + - `get_config_path()` returns `Path.home() / ".nanobot" / "config.json"` + - In Docker, `Path.home()` = `/root`, so it reads `/root/.nanobot/config.json` + - This is mounted from `~/.nanobot-user1/config.json` on host ✅ + +## ⚠️ Issues Found + +### Issue 1: ALLOW_FROM in env file +**Problem**: `.env.user1` has: +```bash +NANOBOT_CHANNELS__TELEGRAM__ALLOW_FROM=["TADec2023"] +NANOBOT_CHANNELS__EMAIL__ALLOW_FROM=["adayear2025@gmail.com"] +``` + +**Why it's wrong**: Environment variables can't handle JSON arrays. These will be treated as strings, not arrays. + +**Fix**: Remove these from `.env.user1` - they're already correctly in `config.json`: +```json +{ + "channels": { + "telegram": { + "allowFrom": ["TADec2023"] + }, + "email": { + "allowFrom": ["adayear2025@gmail.com"] + } + } +} +``` + +### Issue 2: Duplicate email settings +**Problem**: Email IMAP/SMTP settings are in both `.env.shared` and `.env.user1` + +**Recommendation**: +- If all bots use same email account → Keep only in `.env.shared` +- If each bot uses different email → Keep only in `.env.userX` files + +## ✅ Verification Steps + +1. **Check env files don't have ALLOW_FROM**: + ```bash + grep ALLOW_FROM .env.user1 + # Should return nothing or be removed + ``` + +2. **Check config files have allowFrom**: + ```bash + cat ~/.nanobot-user1/config.json | jq '.channels.telegram.allowFrom' + # Should show: ["TADec2023"] + ``` + +3. **Verify Docker mounts**: + ```bash + docker compose -f docker-compose.multi.env.yml config | grep -A 5 "nanobot-user1" + # Should show volume mount: ~/.nanobot-user1:/root/.nanobot + ``` + +4. **Test config loading in container**: + ```bash + docker run --rm -v ~/.nanobot-user1:/root/.nanobot \ + -e NANOBOT_CHANNELS__TELEGRAM__TOKEN=test \ + nanobot gateway --help + ``` + +## 🎯 Summary + +**What's Correct:** +- ✅ Docker Compose loads env files correctly +- ✅ Config files are in correct locations +- ✅ Volume mounts map host configs to container paths +- ✅ Nanobot will read from `/root/.nanobot/config.json` inside container + +**What Needs Fixing:** +- ⚠️ Remove `ALLOW_FROM` from `.env.user1` (keep only in config.json) +- ⚠️ Decide: Email settings in `.env.shared` OR `.env.userX` (not both) + +**How It Works:** +1. Docker Compose loads `.env.shared` → sets environment variables +2. Docker Compose loads `.env.user1` → overrides with bot-specific vars +3. Container starts → mounts `~/.nanobot-user1` to `/root/.nanobot` +4. Nanobot starts → reads env vars (from Docker) + config.json (from mount) +5. Result: Bot uses combined settings from env vars + config.json ✅ + + diff --git a/create-bot-configs.sh b/create-bot-configs.sh new file mode 100755 index 0000000..7df03c6 --- /dev/null +++ b/create-bot-configs.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Create minimal config.json files for each bot +# These only contain channel-specific settings that can't be in env vars + +set -e + +echo "Creating bot config files..." + +# Bot 1 config +cat > ~/.nanobot-user1/config.json << 'EOF' +{ + "channels": { + "telegram": { + "enabled": true, + "allowFrom": ["TADec2023"] + }, + "email": { + "enabled": true, + "allowFrom": ["adayear2025@gmail.com"] + } + } +} +EOF + +# Bot 2 config (placeholder - update with actual values) +cat > ~/.nanobot-user2/config.json << 'EOF' +{ + "channels": { + "telegram": { + "enabled": true, + "allowFrom": [] + }, + "email": { + "enabled": true, + "allowFrom": [] + } + } +} +EOF + +# Bot 3 config (placeholder - update with actual values) +cat > ~/.nanobot-user3/config.json << 'EOF' +{ + "channels": { + "telegram": { + "enabled": true, + "allowFrom": [] + }, + "email": { + "enabled": true, + "allowFrom": [] + } + } +} +EOF + +echo "✓ Created config files:" +echo " - ~/.nanobot-user1/config.json" +echo " - ~/.nanobot-user2/config.json" +echo " - ~/.nanobot-user3/config.json" +echo "" +echo "Note: Telegram tokens come from .env.userX files" +echo " Update allowFrom arrays with actual user IDs/emails" + + diff --git a/docker-compose.multi.dev.yml b/docker-compose.multi.dev.yml new file mode 100644 index 0000000..41ef6f9 --- /dev/null +++ b/docker-compose.multi.dev.yml @@ -0,0 +1,80 @@ +# Development version - mounts source code for live updates +# Use this when developing nanobot code +# Changes to nanobot/ directory will be picked up automatically (may need container restart) + +services: + nanobot-user1: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user1-dev + command: ["gateway"] + restart: unless-stopped + env_file: + - .env.shared + - .env.user1 + volumes: + - ~/.nanobot-user1:/root/.nanobot + # Mount source code for development (changes picked up immediately) + - ./nanobot:/app/nanobot:ro # Read-only mount (safer) + # Or use this for read-write (if you edit inside container): + # - ./nanobot:/app/nanobot + ports: + - "18790:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + + nanobot-user2: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user2-dev + command: ["gateway"] + restart: unless-stopped + env_file: + - .env.shared + - .env.user2 + volumes: + - ~/.nanobot-user2:/root/.nanobot + - ./nanobot:/app/nanobot:ro + ports: + - "18791:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + + nanobot-user3: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user3-dev + command: ["gateway"] + restart: unless-stopped + env_file: + - .env.shared + - .env.user3 + volumes: + - ~/.nanobot-user3:/root/.nanobot + - ./nanobot:/app/nanobot:ro + ports: + - "18792:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + diff --git a/docker-compose.multi.env.yml b/docker-compose.multi.env.yml new file mode 100644 index 0000000..9c6d47c --- /dev/null +++ b/docker-compose.multi.env.yml @@ -0,0 +1,75 @@ +# Using separate env files per container: +# - .env.shared: Common settings (API keys, model, etc.) - loaded first +# - .env.user1, .env.user2, .env.user3: Bot-specific overrides - loaded after +# Later files override earlier ones, so bot-specific settings take precedence + +services: + nanobot-user1: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user1 + command: ["gateway"] + restart: unless-stopped + env_file: + - .env.shared # Shared settings (loaded first) + - .env.user1 # Bot-specific overrides (loaded second, overrides shared) + volumes: + - ~/.nanobot-user1:/root/.nanobot + ports: + - "18790:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + + nanobot-user2: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user2 + command: ["gateway"] + restart: unless-stopped + env_file: + - .env.shared # Shared settings (loaded first) + - .env.user2 # Bot-specific overrides (loaded second, overrides shared) + volumes: + - ~/.nanobot-user2:/root/.nanobot + ports: + - "18791:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + + nanobot-user3: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user3 + command: ["gateway"] + restart: unless-stopped + env_file: + - .env.shared # Shared settings (loaded first) + - .env.user3 # Bot-specific overrides (loaded second, overrides shared) + volumes: + - ~/.nanobot-user3:/root/.nanobot + ports: + - "18792:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + diff --git a/docker-compose.multi.yml b/docker-compose.multi.yml new file mode 100644 index 0000000..914758d --- /dev/null +++ b/docker-compose.multi.yml @@ -0,0 +1,61 @@ +services: + nanobot-user1: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user1 + command: ["gateway"] + restart: unless-stopped + volumes: + - ~/.nanobot-user1:/root/.nanobot + ports: + - "18790:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + + nanobot-user2: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user2 + command: ["gateway"] + restart: unless-stopped + volumes: + - ~/.nanobot-user2:/root/.nanobot + ports: + - "18791:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + + nanobot-user3: + build: + context: . + dockerfile: Dockerfile + container_name: nanobot-user3 + command: ["gateway"] + restart: unless-stopped + volumes: + - ~/.nanobot-user3:/root/.nanobot + ports: + - "18792:18790" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + diff --git a/env-files-setup.sh b/env-files-setup.sh new file mode 100755 index 0000000..dee7d6c --- /dev/null +++ b/env-files-setup.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Setup script to create env files for multi-bot setup + +set -e + +echo "Setting up environment files for multi-bot configuration..." +echo "" + +# Create .env.shared if it doesn't exist +if [ ! -f .env.shared ]; then + cat > .env.shared << 'EOF' +# Shared configuration for all nanobot instances +# These settings apply to all bots unless overridden in .env.user1, .env.user2, etc. + +# LLM Provider API Key (shared across all bots) +NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx + +# Default Model (shared across all bots) +NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5 + +# Agent Settings (shared) +NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7 +NANOBOT_AGENTS__DEFAULTS__MAX_TOKENS=8192 +NANOBOT_AGENTS__DEFAULTS__MAX_TOOL_ITERATIONS=20 +NANOBOT_AGENTS__DEFAULTS__MEMORY_WINDOW=50 + +# Tool Settings (shared) +NANOBOT_TOOLS__RESTRICT_TO_WORKSPACE=true + +# Gateway Settings (shared) +NANOBOT_GATEWAY__PORT=18790 +NANOBOT_GATEWAY__HOST=0.0.0.0 +EOF + echo "✓ Created .env.shared" +else + echo "⚠ .env.shared already exists, skipping..." +fi + +# Create bot-specific env files +for i in 1 2 3; do + env_file=".env.user${i}" + if [ ! -f "$env_file" ]; then + cat > "$env_file" << EOF +# Bot-specific configuration for user${i} +# These settings override .env.shared for this bot only +# Leave empty or comment out to use shared settings + +# Example: Override model for this bot +# NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-sonnet-4 + +# Example: Override temperature for this bot +# NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.9 + +# Example: Use different API key for this bot +# NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-different-key +EOF + echo "✓ Created $env_file" + else + echo "⚠ $env_file already exists, skipping..." + fi +done + +echo "" +echo "Done! Next steps:" +echo "1. Edit .env.shared and add your API keys and shared settings" +echo "2. Edit .env.user1, .env.user2, .env.user3 if you need bot-specific overrides" +echo "3. Create minimal config.json files in ~/.nanobot-user1, ~/.nanobot-user2, etc." +echo "4. Run: docker compose -f docker-compose.multi.env.yml up -d" + + diff --git a/multi-bot-setup.sh b/multi-bot-setup.sh new file mode 100755 index 0000000..41c09ca --- /dev/null +++ b/multi-bot-setup.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Setup script for multiple nanobot instances + +# Create directories for each bot +mkdir -p ~/.nanobot-user1 +mkdir -p ~/.nanobot-user2 +mkdir -p ~/.nanobot-user3 + +# Copy base config if it exists +if [ -f ~/.nanobot/config.json ]; then + cp ~/.nanobot/config.json ~/.nanobot-user1/config.json + cp ~/.nanobot/config.json ~/.nanobot-user2/config.json + cp ~/.nanobot/config.json ~/.nanobot-user3/config.json + echo "✓ Copied base config to all directories" +else + echo "⚠ Base config not found. Creating minimal configs..." + # Create minimal configs + cat > ~/.nanobot-user1/config.json <" content as their message and write a direct, helpful reply. Do NOT call read_emails just because the message is an email; only call read_emails if the human explicitly asks you to inspect or search the mailbox (e.g. "check my inbox", "find an email", etc.). + +**Tool failures:** If a tool result says authentication failed, API access failed, or includes a tag like [NO_CALENDAR_DATA], you have no real data from that integration—never fabricate meetings, mailbox contents, or file contents. Say access failed and what to fix. For other tool errors (e.g. missing parameters), correct the call or explain the error without inventing facts.""" def _load_bootstrap_files(self) -> str: """Load all bootstrap files from workspace.""" diff --git a/nanobot/agent/tools/calendar.py b/nanobot/agent/tools/calendar.py index a17ed23..4943952 100644 --- a/nanobot/agent/tools/calendar.py +++ b/nanobot/agent/tools/calendar.py @@ -6,17 +6,27 @@ from datetime import datetime, timedelta from pathlib import Path from typing import Any +from google.auth.exceptions import RefreshError from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.errors import HttpError +from loguru import logger from nanobot.agent.tools.base import Tool # Scopes required for Google Calendar API SCOPES = ["https://www.googleapis.com/auth/calendar"] +# Appended to failures so the model does not invent meetings when the API/auth did not succeed. +_NO_CALENDAR_DATA_FOR_MODEL = ( + "\n\n[NO_CALENDAR_DATA] Google Calendar was not read successfully. " + "You MUST NOT infer, guess, or invent meetings, times, locations, or titles. " + "Tell the user only that calendar could not be accessed and what they should fix " + "(e.g. credentials_file, OAuth / calendar_token.json)." +) + class CalendarTool(Tool): """Tool to interact with Google Calendar.""" @@ -38,7 +48,9 @@ class CalendarTool(Tool): "3. IMMEDIATELY call calendar(action='delete_events', event_ids=[...]) with the extracted IDs\n" "NEVER use placeholder values like 'ID1', '[get event ID...]', or 'list_events'.\n" "NEVER explain what you will do - just execute the tools immediately.\n" - "When you call this tool, the system will execute it automatically. Do not show JSON in your response - just call the tool." + "When you call this tool, the system will execute it automatically. Do not show JSON in your response - just call the tool.\n\n" + "If the tool result contains [NO_CALENDAR_DATA] or starts with 'Error:' and describes auth/API failure, " + "you have zero reliable calendar information—report the problem only; never make up events." ) def __init__(self, calendar_config: Any = None): @@ -50,6 +62,7 @@ class CalendarTool(Tool): """ self._calendar_config = calendar_config self._service = None + self._calendar_refresh_rejected = False @property def config(self) -> Any: @@ -73,6 +86,7 @@ class CalendarTool(Tool): def _get_credentials(self) -> Credentials | None: """Get valid user credentials from storage or OAuth flow.""" + self._calendar_refresh_rejected = False config = self.config if not config.enabled: @@ -106,6 +120,10 @@ class CalendarTool(Tool): if creds and creds.expired and creds.refresh_token: try: creds.refresh(Request()) + except RefreshError as e: + self._calendar_refresh_rejected = True + logger.warning("Google Calendar OAuth refresh failed: {}", e) + creds = None except Exception: creds = None @@ -579,15 +597,26 @@ class CalendarTool(Tool): config = self.config if not config.enabled: - return "Error: Calendar is not enabled. Set NANOBOT_TOOLS__CALENDAR__ENABLED=true" + return ( + "Error: Calendar is not enabled. Set NANOBOT_TOOLS__CALENDAR__ENABLED=true" + + _NO_CALENDAR_DATA_FOR_MODEL + ) service = self._get_service() if not service: - return ( + msg = ( "Error: Could not authenticate with Google Calendar. " "Please ensure credentials_file is configured and valid. " "You may need to run OAuth flow once to authorize access." ) + if self._calendar_refresh_rejected: + msg += ( + " Google rejected the saved refresh token (expired or revoked). " + "Re-authorize on a machine with a browser and replace ~/.nanobot/calendar_token.json on the host, " + "then restart the container. If the token file is mounted :ro, refreshes cannot be persisted—" + "use rw or re-auth on the host when that happens." + ) + return msg + _NO_CALENDAR_DATA_FOR_MODEL calendar_id = config.calendar_id or "primary" @@ -679,9 +708,9 @@ class CalendarTool(Tool): else: return f"Error: Unknown action '{action}'. Use 'list_events', 'create_event', 'delete_event', 'delete_events', 'update_event', or 'check_availability'" except HttpError as e: - return f"Error accessing Google Calendar API: {e}" + return f"Error accessing Google Calendar API: {e}" + _NO_CALENDAR_DATA_FOR_MODEL except Exception as e: - return f"Error: {str(e)}" + return f"Error: {str(e)}" + _NO_CALENDAR_DATA_FOR_MODEL async def _list_events( self, service: Any, calendar_id: str, max_results: int, time_min: str | None diff --git a/nanobot/agent/tools/email.py b/nanobot/agent/tools/email.py index 9a7d3b2..f73344d 100644 --- a/nanobot/agent/tools/email.py +++ b/nanobot/agent/tools/email.py @@ -17,7 +17,27 @@ class EmailTool(Tool): """Read emails from configured IMAP mailbox.""" name = "read_emails" - description = "USE THIS TOOL FOR ALL EMAIL QUERIES. When user asks about emails, latest email, email sender, inbox, attachments, etc., you MUST call read_emails(). DO NOT use mcp_gmail_mcp_read_email for emails received via email channel - use read_emails instead. DO NOT use exec() with mail/tail/awk commands. DO NOT use read_file() on /var/mail or memory files. DO NOT try alternative methods. This is the ONLY way to read emails from IMAP - it connects to IMAP and fetches real-time data. For 'latest email' or 'last email received' queries, use limit=1. When user asks to download attachments, use download_attachments=true. When user asks to find emails with a specific attachment (e.g., 'find email with attachment Rubiks'), use attachment_name='Rubiks'. CRITICAL: When user asks for specific fields like 'From and Subject' or 'sender and subject', extract and return ONLY those fields from the tool output. Do NOT summarize or analyze the email body content unless the user specifically asks for it. If user asks 'give me the from and subject', respond with just: 'From: [email] Subject: [subject]'. Parameters: limit (1-50, default 10, use 1 for latest), unread_only (bool, default false), mark_seen (bool, default false), download_attachments (bool, default false - set to true to download all attachments to workspace), attachment_name (string, optional - filter emails by attachment filename, case-insensitive partial match). Returns formatted email list with sender, subject, date, attachments (if any), downloaded file paths (if downloaded), and body." + description = ( + "USE THIS TOOL FOR OWNER-INITIATED MAILBOX QUERIES. When the user explicitly asks about their inbox " + "(e.g. latest email, email sender, unread emails, search inbox, attachments, etc.), you SHOULD call " + "read_emails(). DO NOT use mcp_gmail_mcp_read_email for emails received via the email channel - use " + "read_emails instead. DO NOT use exec() with mail/tail/awk commands. DO NOT use read_file() on /var/mail " + "or memory files. DO NOT try alternative methods. This is the ONLY way to read emails from IMAP - it connects " + "to IMAP and fetches real-time data. For 'latest email' or 'last email received' queries, use limit=1. " + "When the user asks to download attachments, use download_attachments=true. When the user asks to find " + "emails with a specific attachment (e.g., 'find email with attachment Rubiks'), use attachment_name='Rubiks'. " + "CRITICAL: When the user asks for specific fields like 'From and Subject' or 'sender and subject', extract " + "and return ONLY those fields from the tool output. Do NOT summarize or analyze the email body content unless " + "the user specifically asks for it. If the user asks 'give me the from and subject', respond with just: " + "'From: [email] Subject: [subject]'. VERY IMPORTANT: When replying on the email channel to a single incoming " + "email (content typically starts with 'Email received. From: ... Subject: ...'), treat that content as the " + "user's message and compose a direct reply. Do NOT call read_emails in that case unless the human explicitly " + "asks you to inspect or search the mailbox. Parameters: limit (1-50, default 10, use 1 for latest), " + "unread_only (bool, default false), mark_seen (bool, default false), download_attachments (bool, default false " + "- set to true to download all attachments to workspace), attachment_name (string, optional - filter emails by " + "attachment filename, case-insensitive partial match). Returns formatted email list with sender, subject, date, " + "attachments (if any), downloaded file paths (if downloaded), and body." + ) def __init__(self, email_config: Any = None): """ diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index 9519aaa..bf451d9 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -115,7 +115,7 @@ class WriteFileTool(Tool): @property def description(self) -> str: - return "Write content to a file at the given path. Creates parent directories if needed." + return "Write content to a file at the given path. Creates parent directories if needed. IMPORTANT: Always provide both 'path' and 'content' parameters. If no full path is specified, use the workspace directory (/mnt/data/nanobot/workspace/)." @property def parameters(self) -> dict[str, Any]: diff --git a/nanobot/agent/tools/mcp.py b/nanobot/agent/tools/mcp.py index 1c8eac4..d01c724 100644 --- a/nanobot/agent/tools/mcp.py +++ b/nanobot/agent/tools/mcp.py @@ -33,6 +33,7 @@ class MCPToolWrapper(Tool): async def execute(self, **kwargs: Any) -> str: from mcp import types + import json result = await self._session.call_tool(self._original_name, arguments=kwargs) parts = [] for block in result.content: @@ -40,7 +41,28 @@ class MCPToolWrapper(Tool): parts.append(block.text) else: parts.append(str(block)) - return "\n".join(parts) or "(no output)" + output = "\n".join(parts) + + # For empty results from search/list operations, provide clearer feedback + if not output or output.strip() == "": + # Check if this is a search/list operation (common patterns) + if "search" in self._original_name.lower() or "list" in self._original_name.lower(): + if "unread" in str(kwargs).lower() or "is:unread" in str(kwargs).lower(): + return "No unread emails found." + return "No results found." + + # Try to parse JSON to check for empty arrays/lists + try: + parsed = json.loads(output) + if isinstance(parsed, list) and len(parsed) == 0: + if "search" in self._original_name.lower() or "list" in self._original_name.lower(): + if "unread" in str(kwargs).lower() or "is:unread" in str(kwargs).lower(): + return "No unread emails found." + return "No results found." + except (json.JSONDecodeError, ValueError): + pass # Not JSON, continue with original output + + return output or "(no output)" async def connect_mcp_servers( diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index a6fd43c..46d8d0a 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -276,7 +276,7 @@ class ToolsConfig(Base): web: WebToolsConfig = Field(default_factory=WebToolsConfig) exec: ExecToolConfig = Field(default_factory=ExecToolConfig) calendar: CalendarConfig = Field(default_factory=CalendarConfig) - restrict_to_workspace: bool = False # If true, restrict all tool access to workspace directory + restrict_to_workspace: bool = True # If true, restrict all tool access to workspace directory mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict) diff --git a/update-multi-configs.sh b/update-multi-configs.sh new file mode 100755 index 0000000..14c41be --- /dev/null +++ b/update-multi-configs.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Script to update shared settings across all bot configs + +set -e + +CONFIG_DIRS=( + ~/.nanobot-user1 + ~/.nanobot-user2 + ~/.nanobot-user3 +) + +# Function to update a specific key in all configs +update_config_key() { + local key_path="$1" + local new_value="$2" + + echo "Updating $key_path to $new_value in all configs..." + + for dir in "${CONFIG_DIRS[@]}"; do + config_file="$dir/config.json" + if [ -f "$config_file" ]; then + # Use jq to update the config + if command -v jq &> /dev/null; then + # Convert key_path like "providers.openrouter.apiKey" to jq path + jq_path=$(echo "$key_path" | sed 's/\./"."/g' | sed 's/^/./' | sed 's/\.$//') + jq "$jq_path = \"$new_value\"" "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file" + echo " ✓ Updated $config_file" + else + echo " ⚠ jq not found, skipping $config_file (install jq for automatic updates)" + fi + else + echo " ⚠ Config not found: $config_file" + fi + done +} + +# Function to show usage +usage() { + cat << EOF +Usage: $0 [args] + +Commands: + update-api-key Update API key for a provider (e.g., openrouter) + update-model Update default model + update-setting Update any setting using dot notation + +Examples: + $0 update-api-key openrouter "sk-or-v1-xxx" + $0 update-model "anthropic/claude-opus-4-5" + $0 update-setting "agents.defaults.temperature" "0.8" + +Note: Requires 'jq' to be installed for automatic updates. + Install with: sudo apt install jq (or brew install jq on macOS) +EOF +} + +# Main script +if [ $# -eq 0 ]; then + usage + exit 1 +fi + +case "$1" in + update-api-key) + if [ $# -ne 3 ]; then + echo "Error: update-api-key requires provider name and API key" + usage + exit 1 + fi + provider="$2" + api_key="$3" + update_config_key "providers.$provider.apiKey" "$api_key" + ;; + update-model) + if [ $# -ne 2 ]; then + echo "Error: update-model requires model name" + usage + exit 1 + fi + model="$2" + update_config_key "agents.defaults.model" "$model" + ;; + update-setting) + if [ $# -ne 3 ]; then + echo "Error: update-setting requires key path and value" + usage + exit 1 + fi + key_path="$2" + value="$3" + update_config_key "$key_path" "$value" + ;; + *) + echo "Unknown command: $1" + usage + exit 1 + ;; +esac + +echo "" +echo "Done! Restart containers to apply changes:" +echo " docker compose -f docker-compose.multi.yml restart" + +