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!
+
+---
+
+
+ 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 @@
+
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. π€
----
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):