From 20227f1f04ea195ec681477f03905f4954cc9f9a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 08:55:21 +0000 Subject: [PATCH 01/22] feat: add Dockerfile with uv-based installation Add a Dockerfile using the official uv Python image (python3.12-bookworm-slim) for fast dependency installation. Includes Node.js 20 for the WhatsApp bridge, dependency layer caching, and exposes the gateway port (18790). https://claude.ai/code/session_011C1h1NERqqZp4ht3Pqpwkc --- .dockerignore | 13 +++++++++++++ Dockerfile | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..020b9ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +*.egg-info +dist/ +build/ +.git +.env +.assets +node_modules/ +bridge/dist/ +workspace/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..21a502a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim + +# Install Node.js 20 for the WhatsApp bridge +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl ca-certificates gnupg && \ + mkdir -p /etc/apt/keyrings && \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends nodejs && \ + apt-get purge -y gnupg && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Python dependencies first (cached layer) +COPY pyproject.toml README.md LICENSE ./ +RUN mkdir -p nanobot && touch nanobot/__init__.py && \ + uv pip install --system --no-cache . && \ + rm -rf nanobot + +# Copy the full source and install +COPY nanobot/ nanobot/ +COPY bridge/ bridge/ +RUN uv pip install --system --no-cache . + +# Build the WhatsApp bridge +WORKDIR /app/bridge +RUN npm install && npm run build +WORKDIR /app + +# Create config directory +RUN mkdir -p /root/.nanobot + +# Gateway default port +EXPOSE 18790 + +ENTRYPOINT ["nanobot"] +CMD ["gateway"] From 6df4a56586d5411389f985d63b1b4b17b6d5960b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 08:59:57 +0000 Subject: [PATCH 02/22] test: add script to verify Dockerfile builds and nanobot status works Builds the image, runs onboard + status inside the container, and validates that the expected output fields (Config, Workspace, Model, API keys) are present. https://claude.ai/code/session_011C1h1NERqqZp4ht3Pqpwkc --- test_docker.sh | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100755 test_docker.sh diff --git a/test_docker.sh b/test_docker.sh new file mode 100755 index 0000000..a90e080 --- /dev/null +++ b/test_docker.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME="nanobot-test" + +echo "=== Building Docker image ===" +docker build -t "$IMAGE_NAME" . + +echo "" +echo "=== Running 'nanobot onboard' ===" +docker run --name nanobot-test-run "$IMAGE_NAME" onboard + +echo "" +echo "=== Running 'nanobot status' ===" +STATUS_OUTPUT=$(docker commit nanobot-test-run nanobot-test-onboarded > /dev/null && \ + docker run --rm nanobot-test-onboarded status 2>&1) || true + +echo "$STATUS_OUTPUT" + +echo "" +echo "=== Validating output ===" +PASS=true + +check() { + if echo "$STATUS_OUTPUT" | grep -q "$1"; then + echo " PASS: found '$1'" + else + echo " FAIL: missing '$1'" + PASS=false + fi +} + +check "nanobot Status" +check "Config:" +check "Workspace:" +check "Model:" +check "OpenRouter API:" +check "Anthropic API:" +check "OpenAI API:" + +echo "" +if $PASS; then + echo "=== All checks passed ===" +else + echo "=== Some checks FAILED ===" + exit 1 +fi + +# Cleanup +echo "" +echo "=== Cleanup ===" +docker rm -f nanobot-test-run 2>/dev/null || true +docker rmi -f nanobot-test-onboarded 2>/dev/null || true +docker rmi -f "$IMAGE_NAME" 2>/dev/null || true +echo "Done." From fa25856d8cefda9f0e8559d71006f72bdcb614ed Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 09:07:23 +0000 Subject: [PATCH 03/22] fix: create stub bridge/ dir in dependency caching layer Hatchling's force-include requires bridge/ to exist at build time. The dependency caching step now stubs both nanobot/ and bridge/. https://claude.ai/code/session_011C1h1NERqqZp4ht3Pqpwkc --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 21a502a..5244f1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,9 +16,9 @@ WORKDIR /app # Install Python dependencies first (cached layer) COPY pyproject.toml README.md LICENSE ./ -RUN mkdir -p nanobot && touch nanobot/__init__.py && \ +RUN mkdir -p nanobot bridge && touch nanobot/__init__.py && \ uv pip install --system --no-cache . && \ - rm -rf nanobot + rm -rf nanobot bridge # Copy the full source and install COPY nanobot/ nanobot/ From f7e8e73c5414c1e1bdb600da56ec1bc168b3b65b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 09:17:24 +0000 Subject: [PATCH 04/22] fix: add git to Dockerfile for npm bridge dependency install A bridge npm dependency requires git to be present at install time. https://claude.ai/code/session_011C1h1NERqqZp4ht3Pqpwkc --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5244f1a..4287944 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim # Install Node.js 20 for the WhatsApp bridge RUN apt-get update && \ - apt-get install -y --no-install-recommends curl ca-certificates gnupg && \ + apt-get install -y --no-install-recommends curl ca-certificates gnupg git && \ mkdir -p /etc/apt/keyrings && \ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \ From 7fced16e4c73b08e2cb632542735de9ec63fd6d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 09:22:13 +0000 Subject: [PATCH 05/22] docs: add Docker build/run instructions to README https://claude.ai/code/session_011C1h1NERqqZp4ht3Pqpwkc --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 167ae22..71d425b 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,16 @@ nanobot agent -m "What is 2+2?" That's it! You have a working AI assistant in 2 minutes. +## 🐳 Docker + +```bash +docker build -t nanobot . +docker run --rm nanobot onboard +docker run -v ~/.nanobot:/root/.nanobot -p 18790:18790 nanobot +``` + +Mount `~/.nanobot` so your config and workspace persist across runs. Edit `~/.nanobot/config.json` on the host to add API keys, then restart the container. + ## πŸ–₯️ Local Models (vLLM) Run nanobot with your own local models using vLLM or any OpenAI-compatible server. From eaf494ea31fc84b860efb3e2f0d96c5d6a1e940e Mon Sep 17 00:00:00 2001 From: Manus AI Date: Mon, 2 Feb 2026 04:30:15 -0500 Subject: [PATCH 06/22] docs: add uv installation instructions (fixes #5) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 167ae22..ab1f947 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,12 @@ ## πŸ“¦ Install +**Install with [uv](https://github.com/astral-sh/uv)** (recommended for speed) + +```bash +uv tool install nanobot-ai +``` + **Install from PyPi** ```bash From 42f62c0c1aeb4bae98949b670bd698bc40e62c11 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Mon, 2 Feb 2026 04:33:26 -0500 Subject: [PATCH 07/22] feat: add voice transcription support with groq (fixes #13) --- README.md | 1 + bridge/src/whatsapp.ts | 5 +++ nanobot/channels/manager.py | 4 ++ nanobot/channels/telegram.py | 20 ++++++++- nanobot/channels/whatsapp.py | 5 +++ nanobot/config/schema.py | 4 +- nanobot/providers/litellm_provider.py | 2 + nanobot/providers/transcription.py | 65 +++++++++++++++++++++++++++ 8 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 nanobot/providers/transcription.py diff --git a/README.md b/README.md index ab1f947..ec73b51 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,7 @@ nanobot/ ## πŸ—ΊοΈ Roadmap +- [x] **Voice Transcription** β€” Support for Groq Whisper (Issue #13) - [ ] **Multi-modal** β€” See and hear (images, voice, video) - [ ] **Long-term memory** β€” Never forget important context - [ ] **Better reasoning** β€” Multi-step planning and reflection diff --git a/bridge/src/whatsapp.ts b/bridge/src/whatsapp.ts index 4185632..a3a82fc 100644 --- a/bridge/src/whatsapp.ts +++ b/bridge/src/whatsapp.ts @@ -160,6 +160,11 @@ export class WhatsAppClient { return `[Document] ${message.documentMessage.caption}`; } + // Voice/Audio message + if (message.audioMessage) { + return `[Voice Message]`; + } + return null; } diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index 04abf5f..c32aa3d 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -36,6 +36,8 @@ class ChannelManager: if self.config.channels.telegram.enabled: try: from nanobot.channels.telegram import TelegramChannel + # Inject parent config for access to providers + self.config.channels.telegram.parent = self.config self.channels["telegram"] = TelegramChannel( self.config.channels.telegram, self.bus ) @@ -47,6 +49,8 @@ class ChannelManager: if self.config.channels.whatsapp.enabled: try: from nanobot.channels.whatsapp import WhatsAppChannel + # Inject parent config for access to providers + self.config.channels.whatsapp.parent = self.config self.channels["whatsapp"] = WhatsAppChannel( self.config.channels.whatsapp, self.bus ) diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 840c250..dc2f77c 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -247,7 +247,25 @@ class TelegramChannel(BaseChannel): await file.download_to_drive(str(file_path)) media_paths.append(str(file_path)) - content_parts.append(f"[{media_type}: {file_path}]") + + # Handle voice transcription + if media_type == "voice" or media_type == "audio": + from nanobot.providers.transcription import GroqTranscriptionProvider + # Try to get Groq API key from config + groq_key = None + if hasattr(self.config, 'parent') and hasattr(self.config.parent, 'providers'): + groq_key = self.config.parent.providers.groq.api_key + + transcriber = GroqTranscriptionProvider(api_key=groq_key) + transcription = await transcriber.transcribe(file_path) + if transcription: + logger.info(f"Transcribed {media_type}: {transcription[:50]}...") + content_parts.append(f"[transcription: {transcription}]") + else: + content_parts.append(f"[{media_type}: {file_path}]") + else: + content_parts.append(f"[{media_type}: {file_path}]") + logger.debug(f"Downloaded {media_type} to {file_path}") except Exception as e: logger.error(f"Failed to download media: {e}") diff --git a/nanobot/channels/whatsapp.py b/nanobot/channels/whatsapp.py index efbd3e1..c14a6c3 100644 --- a/nanobot/channels/whatsapp.py +++ b/nanobot/channels/whatsapp.py @@ -107,6 +107,11 @@ class WhatsAppChannel(BaseChannel): # Extract just the phone number as chat_id chat_id = sender.split("@")[0] if "@" in sender else sender + # Handle voice transcription if it's a voice message + if content == "[Voice Message]": + logger.info(f"Voice message received from {chat_id}, but direct download from bridge is not yet supported.") + content = "[Voice Message: Transcription not available for WhatsApp yet]" + await self._handle_message( sender_id=chat_id, chat_id=sender, # Use full JID for replies diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index e30fbb2..ee245f1 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -50,6 +50,7 @@ class ProvidersConfig(BaseModel): anthropic: ProviderConfig = Field(default_factory=ProviderConfig) openai: ProviderConfig = Field(default_factory=ProviderConfig) openrouter: ProviderConfig = Field(default_factory=ProviderConfig) + groq: ProviderConfig = Field(default_factory=ProviderConfig) vllm: ProviderConfig = Field(default_factory=ProviderConfig) @@ -89,11 +90,12 @@ class Config(BaseSettings): return Path(self.agents.defaults.workspace).expanduser() def get_api_key(self) -> str | None: - """Get API key in priority order: OpenRouter > Anthropic > OpenAI > vLLM.""" + """Get API key in priority order: OpenRouter > Anthropic > OpenAI > Groq > vLLM.""" return ( self.providers.openrouter.api_key or self.providers.anthropic.api_key or self.providers.openai.api_key or + self.providers.groq.api_key or self.providers.vllm.api_key or None ) diff --git a/nanobot/providers/litellm_provider.py b/nanobot/providers/litellm_provider.py index 4e7305b..f8e8456 100644 --- a/nanobot/providers/litellm_provider.py +++ b/nanobot/providers/litellm_provider.py @@ -47,6 +47,8 @@ class LiteLLMProvider(LLMProvider): os.environ.setdefault("ANTHROPIC_API_KEY", api_key) elif "openai" in default_model or "gpt" in default_model: os.environ.setdefault("OPENAI_API_KEY", api_key) + elif "groq" in default_model: + os.environ.setdefault("GROQ_API_KEY", api_key) if api_base: litellm.api_base = api_base diff --git a/nanobot/providers/transcription.py b/nanobot/providers/transcription.py new file mode 100644 index 0000000..8ce909b --- /dev/null +++ b/nanobot/providers/transcription.py @@ -0,0 +1,65 @@ +"""Voice transcription provider using Groq.""" + +import os +from pathlib import Path +from typing import Any + +import httpx +from loguru import logger + + +class GroqTranscriptionProvider: + """ + Voice transcription provider using Groq's Whisper API. + + Groq offers extremely fast transcription with a generous free tier. + """ + + def __init__(self, api_key: str | None = None): + self.api_key = api_key or os.environ.get("GROQ_API_KEY") + self.api_url = "https://api.groq.com/openai/v1/audio/transcriptions" + + async def transcribe(self, file_path: str | Path) -> str: + """ + Transcribe an audio file using Groq. + + Args: + file_path: Path to the audio file. + + Returns: + Transcribed text. + """ + if not self.api_key: + logger.warning("Groq API key not configured for transcription") + return "" + + path = Path(file_path) + if not path.exists(): + logger.error(f"Audio file not found: {file_path}") + return "" + + try: + async with httpx.AsyncClient() as client: + with open(path, "rb") as f: + files = { + "file": (path.name, f), + "model": (None, "whisper-large-v3"), + } + headers = { + "Authorization": f"Bearer {self.api_key}", + } + + response = await client.post( + self.api_url, + headers=headers, + files=files, + timeout=60.0 + ) + + response.raise_for_status() + data = response.json() + return data.get("text", "") + + except Exception as e: + logger.error(f"Groq transcription error: {e}") + return "" From ae1830acddf6efe997bfac48af5b227caf80d0cd Mon Sep 17 00:00:00 2001 From: Peter van Eijk Date: Mon, 2 Feb 2026 16:36:22 +0700 Subject: [PATCH 08/22] feat: change default command to status --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4287944..8132747 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,4 +37,4 @@ RUN mkdir -p /root/.nanobot EXPOSE 18790 ENTRYPOINT ["nanobot"] -CMD ["gateway"] +CMD ["status"] From 5c49bbc0b2ad235c7a04ae489e096e6dfce768d5 Mon Sep 17 00:00:00 2001 From: JunghwanNA <70629228+shaun0927@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:31:49 +0900 Subject: [PATCH 09/22] feat: add Amazon Bedrock support Skip API key validation for bedrock/ model prefix since AWS Bedrock uses IAM credentials instead of API keys. Fixes #20 Co-Authored-By: Claude Opus 4.5 --- nanobot/cli/commands.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 8dcc460..6ded59b 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -178,11 +178,13 @@ def gateway( # Create components bus = MessageBus() - # Create provider (supports OpenRouter, Anthropic, OpenAI) + # Create provider (supports OpenRouter, Anthropic, OpenAI, Bedrock) api_key = config.get_api_key() api_base = config.get_api_base() - - if not api_key: + model = config.agents.defaults.model + is_bedrock = model.startswith("bedrock/") + + if not api_key and not is_bedrock: console.print("[red]Error: No API key configured.[/red]") console.print("Set one in ~/.nanobot/config.json under providers.openrouter.apiKey") raise typer.Exit(1) @@ -289,11 +291,13 @@ def agent( api_key = config.get_api_key() api_base = config.get_api_base() - - if not api_key: + model = config.agents.defaults.model + is_bedrock = model.startswith("bedrock/") + + if not api_key and not is_bedrock: console.print("[red]Error: No API key configured.[/red]") raise typer.Exit(1) - + bus = MessageBus() provider = LiteLLMProvider( api_key=api_key, From ea849650efee5c9df32834c7dd79284c6b147eb7 Mon Sep 17 00:00:00 2001 From: Cheng Wang Date: Mon, 2 Feb 2026 19:34:22 +0800 Subject: [PATCH 10/22] feat: improve web_fetch URL validation and security Add URL validation and redirect limits to web_fetch tool to prevent potential security issues: - Add _validate_url() function to validate URLs before fetching - Only allow http:// and https:// schemes (prevent file://, ftp://, etc.) - Verify URL has valid scheme and domain - Return descriptive error messages for invalid URLs - Limit HTTP redirects to 5 (down from default 20) to prevent DoS attacks - Add MAX_REDIRECTS constant for easy configuration - Explicitly configure httpx.AsyncClient with max_redirects parameter - Improve error handling with JSON error responses for validation failures This addresses security concerns identified in code review where web_fetch had no URL validation or redirect limits, potentially allowing: - Unsafe URL schemes (file://, etc.) - Redirect-based DoS attacks - Invalid URL formats causing unclear errors --- nanobot/agent/tools/web.py | 46 ++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/nanobot/agent/tools/web.py b/nanobot/agent/tools/web.py index c9d989c..ad72604 100644 --- a/nanobot/agent/tools/web.py +++ b/nanobot/agent/tools/web.py @@ -5,6 +5,7 @@ import json import os import re from typing import Any +from urllib.parse import urlparse import httpx @@ -12,6 +13,7 @@ from nanobot.agent.tools.base import Tool # Shared constants USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36" +MAX_REDIRECTS = 5 # Limit redirects to prevent DoS attacks def _strip_tags(text: str) -> str: @@ -28,6 +30,33 @@ def _normalize(text: str) -> str: return re.sub(r'\n{3,}', '\n\n', text).strip() +def _validate_url(url: str) -> tuple[bool, str]: + """ + Validate URL for security. + + Returns: + (is_valid, error_message): Tuple of validation result and error message if invalid. + """ + try: + parsed = urlparse(url) + + # Check if scheme exists + if not parsed.scheme: + return False, "URL must include a scheme (http:// or https://)" + + # Only allow http and https schemes + if parsed.scheme.lower() not in ('http', 'https'): + return False, f"Invalid URL scheme '{parsed.scheme}'. Only http:// and https:// are allowed" + + # Check if netloc (domain) exists + if not parsed.netloc: + return False, "URL must include a valid domain" + + return True, "" + except Exception as e: + return False, f"Invalid URL format: {str(e)}" + + class WebSearchTool(Tool): """Search the web using Brave Search API.""" @@ -95,12 +124,21 @@ class WebFetchTool(Tool): async def execute(self, url: str, extractMode: str = "markdown", maxChars: int | None = None, **kwargs: Any) -> str: from readability import Document - + max_chars = maxChars or self.max_chars - + + # Validate URL before fetching + is_valid, error_msg = _validate_url(url) + if not is_valid: + return json.dumps({"error": f"URL validation failed: {error_msg}", "url": url}) + try: - async with httpx.AsyncClient() as client: - r = await client.get(url, headers={"User-Agent": USER_AGENT}, follow_redirects=True, timeout=30.0) + async with httpx.AsyncClient( + follow_redirects=True, + max_redirects=MAX_REDIRECTS, + timeout=30.0 + ) as client: + r = await client.get(url, headers={"User-Agent": USER_AGENT}) r.raise_for_status() ctype = r.headers.get("content-type", "") From 3ba0191cef221828713a144b8c9357a9e105cccb Mon Sep 17 00:00:00 2001 From: Cheng Wang Date: Mon, 2 Feb 2026 19:47:42 +0800 Subject: [PATCH 11/22] fix: correct heartbeat token matching logic The HEARTBEAT_OK_TOKEN comparison was broken because the token itself ("HEARTBEAT_OK" with underscore) was being compared against a response string that had underscores removed. This made the condition always fail, preventing the heartbeat service from recognizing "no tasks" responses. Now both sides of the comparison remove underscores consistently, allowing proper matching of the HEARTBEAT_OK token. --- nanobot/heartbeat/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanobot/heartbeat/service.py b/nanobot/heartbeat/service.py index 4cb469e..221ed27 100644 --- a/nanobot/heartbeat/service.py +++ b/nanobot/heartbeat/service.py @@ -115,7 +115,7 @@ class HeartbeatService: response = await self.on_heartbeat(HEARTBEAT_PROMPT) # Check if agent said "nothing to do" - if HEARTBEAT_OK_TOKEN in response.upper().replace("_", ""): + if HEARTBEAT_OK_TOKEN.replace("_", "") in response.upper().replace("_", ""): logger.info("Heartbeat: OK (no action needed)") else: logger.info(f"Heartbeat: completed task") From 2466d9e1dc40405ad996f75fbf181e172866ce10 Mon Sep 17 00:00:00 2001 From: Cheng Wang Date: Tue, 3 Feb 2026 00:37:55 +0800 Subject: [PATCH 12/22] fix: add Telegram channel to channels status command Previously, the `nanobot channels status` command only displayed WhatsApp channel status, completely omitting Telegram despite it being fully implemented in the codebase. Changes: - Added Telegram channel status display - Renamed "Bridge URL" column to "Configuration" for better generality - Show Telegram token (first 10 chars) or "not configured" message - Added comments to distinguish WhatsApp and Telegram sections Fixes the issue where users couldn't see Telegram channel status via CLI, even though the feature was working correctly. --- nanobot/cli/commands.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index d293564..3bddb62 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -348,21 +348,31 @@ app.add_typer(channels_app, name="channels") def channels_status(): """Show channel status.""" from nanobot.config.loader import load_config - + config = load_config() - + table = Table(title="Channel Status") table.add_column("Channel", style="cyan") table.add_column("Enabled", style="green") - table.add_column("Bridge URL", style="yellow") - + table.add_column("Configuration", style="yellow") + + # WhatsApp wa = config.channels.whatsapp table.add_row( "WhatsApp", "βœ“" if wa.enabled else "βœ—", wa.bridge_url ) - + + # Telegram + tg = config.channels.telegram + tg_config = f"token: {tg.token[:10]}..." if tg.token else "[dim]not configured[/dim]" + table.add_row( + "Telegram", + "βœ“" if tg.enabled else "βœ—", + tg_config + ) + console.print(table) From cd2025207248ecc1383ded126533dc398e4b5549 Mon Sep 17 00:00:00 2001 From: Cheng Wang Date: Tue, 3 Feb 2026 00:45:52 +0800 Subject: [PATCH 13/22] fix: status command now respects workspace from config The status command was ignoring the workspace setting from the configuration file and always displaying the default path (~/.nanobot/workspace). This fix loads the config first and uses config.workspace_path when available, falling back to the default only when no config exists. This brings the status command in line with other commands that correctly use config.workspace_path. --- nanobot/cli/commands.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index d293564..6caa0a7 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -609,17 +609,23 @@ def status(): """Show nanobot status.""" from nanobot.config.loader import load_config, get_config_path from nanobot.utils.helpers import get_workspace_path - + config_path = get_config_path() - workspace = get_workspace_path() - - console.print(f"{__logo__} nanobot Status\n") - - console.print(f"Config: {config_path} {'[green]βœ“[/green]' if config_path.exists() else '[red]βœ—[/red]'}") - console.print(f"Workspace: {workspace} {'[green]βœ“[/green]' if workspace.exists() else '[red]βœ—[/red]'}") - + + # Load config first to get the correct workspace path if config_path.exists(): config = load_config() + workspace = config.workspace_path + else: + config = None + workspace = get_workspace_path() + + console.print(f"{__logo__} nanobot Status\n") + + console.print(f"Config: {config_path} {'[green]βœ“[/green]' if config_path.exists() else '[red]βœ—[/red]'}") + console.print(f"Workspace: {workspace} {'[green]βœ“[/green]' if workspace.exists() else '[red]βœ—[/red]'}") + + if config is not None: console.print(f"Model: {config.agents.defaults.model}") # Check API keys From 1af404c4d90501708338efb6f741ba45c93822be Mon Sep 17 00:00:00 2001 From: tlguszz1010 Date: Tue, 3 Feb 2026 14:08:36 +0900 Subject: [PATCH 14/22] docs: update news date from 2025 to 2026 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 358d23e..f7706d7 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## πŸ“’ News -- **2025-02-01** πŸŽ‰ nanobot launched! Welcome to try 🐈 nanobot! +- **2026-02-01** πŸŽ‰ nanobot launched! Welcome to try 🐈 nanobot! ## Key Features of nanobot: From 8989adc9aecd409309c6f472b1022d1eada9d58d Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Feb 2026 06:36:58 +0000 Subject: [PATCH 15/22] refactor: use explicit dependency injection for groq_api_key --- nanobot/channels/manager.py | 8 +++----- nanobot/channels/telegram.py | 10 +++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index c32aa3d..73c3334 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -36,10 +36,10 @@ class ChannelManager: if self.config.channels.telegram.enabled: try: from nanobot.channels.telegram import TelegramChannel - # Inject parent config for access to providers - self.config.channels.telegram.parent = self.config self.channels["telegram"] = TelegramChannel( - self.config.channels.telegram, self.bus + self.config.channels.telegram, + self.bus, + groq_api_key=self.config.providers.groq.api_key, ) logger.info("Telegram channel enabled") except ImportError as e: @@ -49,8 +49,6 @@ class ChannelManager: if self.config.channels.whatsapp.enabled: try: from nanobot.channels.whatsapp import WhatsAppChannel - # Inject parent config for access to providers - self.config.channels.whatsapp.parent = self.config self.channels["whatsapp"] = WhatsAppChannel( self.config.channels.whatsapp, self.bus ) diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 75b9299..23e1de0 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -85,9 +85,10 @@ class TelegramChannel(BaseChannel): name = "telegram" - def __init__(self, config: TelegramConfig, bus: MessageBus): + def __init__(self, config: TelegramConfig, bus: MessageBus, groq_api_key: str = ""): super().__init__(config, bus) self.config: TelegramConfig = config + self.groq_api_key = groq_api_key self._app: Application | None = None self._chat_ids: dict[str, int] = {} # Map sender_id to chat_id for replies @@ -253,12 +254,7 @@ class TelegramChannel(BaseChannel): # Handle voice transcription if media_type == "voice" or media_type == "audio": from nanobot.providers.transcription import GroqTranscriptionProvider - # Try to get Groq API key from config - groq_key = None - if hasattr(self.config, 'parent') and hasattr(self.config.parent, 'providers'): - groq_key = self.config.parent.providers.groq.api_key - - transcriber = GroqTranscriptionProvider(api_key=groq_key) + transcriber = GroqTranscriptionProvider(api_key=self.groq_api_key) transcription = await transcriber.transcribe(file_path) if transcription: logger.info(f"Transcribed {media_type}: {transcription[:50]}...") From 99339c7be93cdcc3cdee5dd0bdf48645bbcb12c7 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Feb 2026 07:17:47 +0000 Subject: [PATCH 16/22] docs: improve README with provider info and Docker examples --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3440fdc..55c6091 100644 --- a/README.md +++ b/README.md @@ -130,16 +130,6 @@ nanobot agent -m "What is 2+2?" That's it! You have a working AI assistant in 2 minutes. -## 🐳 Docker - -```bash -docker build -t nanobot . -docker run --rm nanobot onboard -docker run -v ~/.nanobot:/root/.nanobot -p 18790:18790 nanobot -``` - -Mount `~/.nanobot` so your config and workspace persist across runs. Edit `~/.nanobot/config.json` on the host to add API keys, then restart the container. - ## πŸ–₯️ Local Models (vLLM) Run nanobot with your own local models using vLLM or any OpenAI-compatible server. @@ -257,6 +247,20 @@ nanobot gateway ## βš™οΈ Configuration +Config file: `~/.nanobot/config.json` + +### Providers + +| Provider | Purpose | Get API Key | +|----------|---------|-------------| +| `openrouter` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) | +| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) | +| `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) | +| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) | +| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) | + +> **Note**: Groq provides free voice transcription via Whisper. If configured, Telegram voice messages will be automatically transcribed. +
Full config example @@ -270,6 +274,9 @@ nanobot gateway "providers": { "openrouter": { "apiKey": "sk-or-v1-xxx" + }, + "groq": { + "apiKey": "gsk_xxx" } }, "channels": { @@ -323,6 +330,30 @@ nanobot cron remove
+## 🐳 Docker + +Build and run nanobot in a container: + +```bash +# Build the image +docker build -t nanobot . + +# Initialize config (first time only) +docker run -v ~/.nanobot:/root/.nanobot --rm nanobot onboard + +# Edit config on host to add API keys +vim ~/.nanobot/config.json + +# Run gateway (connects to Telegram/WhatsApp) +docker run -v ~/.nanobot:/root/.nanobot -p 18790:18790 nanobot gateway + +# Or run a single command +docker run -v ~/.nanobot:/root/.nanobot --rm nanobot agent -m "Hello!" +docker run -v ~/.nanobot:/root/.nanobot --rm nanobot status +``` + +> **Tip**: Mount `~/.nanobot` so your config and workspace persist across container restarts. + ## πŸ“ Project Structure ``` From 73a3934cc59de9a616271f6a121855709635406a Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Feb 2026 07:21:46 +0000 Subject: [PATCH 17/22] docs: unify note/tip format to GitHub Alerts --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 55c6091..cd6fc94 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,9 @@ Config file: `~/.nanobot/config.json` ### Providers +> [!NOTE] +> Groq provides free voice transcription via Whisper. If configured, Telegram voice messages will be automatically transcribed. + | Provider | Purpose | Get API Key | |----------|---------|-------------| | `openrouter` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) | @@ -259,7 +262,6 @@ Config file: `~/.nanobot/config.json` | `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) | | `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) | -> **Note**: Groq provides free voice transcription via Whisper. If configured, Telegram voice messages will be automatically transcribed.
Full config example @@ -332,6 +334,9 @@ nanobot cron remove ## 🐳 Docker +> [!TIP] +> The `-v ~/.nanobot:/root/.nanobot` flag mounts your local config directory into the container, so your config and workspace persist across container restarts. + Build and run nanobot in a container: ```bash @@ -352,8 +357,6 @@ docker run -v ~/.nanobot:/root/.nanobot --rm nanobot agent -m "Hello!" docker run -v ~/.nanobot:/root/.nanobot --rm nanobot status ``` -> **Tip**: Mount `~/.nanobot` so your config and workspace persist across container restarts. - ## πŸ“ Project Structure ``` From a4269593fc1e5f11cc94e541bde17cafeadbd248 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Feb 2026 07:24:59 +0000 Subject: [PATCH 18/22] docs: improve install methods --- README.md | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index cd6fc94..046860d 100644 --- a/README.md +++ b/README.md @@ -60,19 +60,7 @@ ## πŸ“¦ Install -**Install with [uv](https://github.com/astral-sh/uv)** (recommended for speed) - -```bash -uv tool install nanobot-ai -``` - -**Install from PyPi** - -```bash -pip install nanobot-ai -``` - -**Install from source** (recommended for development) +**Install from source** (latest features, recommended for development) ```bash git clone https://github.com/HKUDS/nanobot.git @@ -80,12 +68,16 @@ cd nanobot pip install -e . ``` -**Install with uv** +**Install with [uv](https://github.com/astral-sh/uv)** (stable, fast) ```bash -uv venv -source .venv/bin/activate -uv pip install nanobot-ai +uv tool install nanobot-ai +``` + +**Install from PyPI** (stable) + +```bash +pip install nanobot-ai ``` ## πŸš€ Quick Start From c3b32afbbbab07a1f77ae7a76368606545d916bb Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Feb 2026 11:53:21 +0000 Subject: [PATCH 19/22] docs: improve README with disclaimer --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f87e702..ddc5ccd 100644 --- a/README.md +++ b/README.md @@ -408,3 +408,9 @@ PRs welcome! The codebase is intentionally small and readable. πŸ€— Thanks for visiting ✨ nanobot!

Views

+ +--- + +

+ nanobot is for educational, research, and technical exchange purposes only +

From 6b7eebc46dc0334525510ba0cb13a26a6fc0b8ee Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Feb 2026 12:42:06 +0000 Subject: [PATCH 20/22] docs: add discord community --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ddc5ccd..93789d5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ License Feishu WeChat + Discord

From d9d744d536aab152a57827a2931b894badde8479 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Feb 2026 12:44:55 +0000 Subject: [PATCH 21/22] docs: optimize the structure --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 93789d5..f4b1df2 100644 --- a/README.md +++ b/README.md @@ -391,7 +391,6 @@ PRs welcome! The codebase is intentionally small and readable. πŸ€— ---- ## ⭐ Star History @@ -410,7 +409,6 @@ PRs welcome! The codebase is intentionally small and readable. πŸ€— Views

----

nanobot is for educational, research, and technical exchange purposes only From 1a784fca1e8df195d0f1cb8ee3364bfdce9ac263 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Feb 2026 17:13:30 +0000 Subject: [PATCH 22/22] refactor: simplify _validate_url function --- nanobot/agent/tools/web.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/nanobot/agent/tools/web.py b/nanobot/agent/tools/web.py index ad72604..9de1d3c 100644 --- a/nanobot/agent/tools/web.py +++ b/nanobot/agent/tools/web.py @@ -31,30 +31,16 @@ def _normalize(text: str) -> str: def _validate_url(url: str) -> tuple[bool, str]: - """ - Validate URL for security. - - Returns: - (is_valid, error_message): Tuple of validation result and error message if invalid. - """ + """Validate URL: must be http(s) with valid domain.""" try: - parsed = urlparse(url) - - # Check if scheme exists - if not parsed.scheme: - return False, "URL must include a scheme (http:// or https://)" - - # Only allow http and https schemes - if parsed.scheme.lower() not in ('http', 'https'): - return False, f"Invalid URL scheme '{parsed.scheme}'. Only http:// and https:// are allowed" - - # Check if netloc (domain) exists - if not parsed.netloc: - return False, "URL must include a valid domain" - + p = urlparse(url) + if p.scheme not in ('http', 'https'): + return False, f"Only http/https allowed, got '{p.scheme or 'none'}'" + if not p.netloc: + return False, "Missing domain" return True, "" except Exception as e: - return False, f"Invalid URL format: {str(e)}" + return False, str(e) class WebSearchTool(Tool):