From f4b081b83f28014c07a63b3a1b39e5d3f75398fc Mon Sep 17 00:00:00 2001
From: Yitong Li
Date: Mon, 2 Feb 2026 15:32:12 +0800
Subject: [PATCH 01/22] feat: add vision support for image recognition in
Telegram
---
nanobot/agent/context.py | 70 ++++++++++++++++++++++++++++++++++------
nanobot/agent/loop.py | 3 +-
2 files changed, 63 insertions(+), 10 deletions(-)
diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py
index aaba890..32585f5 100644
--- a/nanobot/agent/context.py
+++ b/nanobot/agent/context.py
@@ -1,8 +1,12 @@
"""Context builder for assembling agent prompts."""
+import base64
+import mimetypes
from pathlib import Path
from typing import Any
+from loguru import logger
+
from nanobot.agent.memory import MemoryStore
from nanobot.agent.skills import SkillsLoader
@@ -114,32 +118,80 @@ When remembering something, write to {workspace_path}/memory/MEMORY.md"""
self,
history: list[dict[str, Any]],
current_message: str,
- skill_names: list[str] | None = None
+ skill_names: list[str] | None = None,
+ media: list[str] | None = None,
) -> list[dict[str, Any]]:
"""
Build the complete message list for an LLM call.
-
+
Args:
history: Previous conversation messages.
current_message: The new user message.
skill_names: Optional skills to include.
-
+ media: Optional list of local file paths for images/media.
+
Returns:
List of messages including system prompt.
"""
messages = []
-
+
# System prompt
system_prompt = self.build_system_prompt(skill_names)
messages.append({"role": "system", "content": system_prompt})
-
+
# History
messages.extend(history)
-
- # Current message
- messages.append({"role": "user", "content": current_message})
-
+
+ # Current message (with optional image attachments)
+ user_content = self._build_user_content(current_message, media)
+ messages.append({"role": "user", "content": user_content})
+
return messages
+
+ def _build_user_content(
+ self, text: str, media: list[str] | None
+ ) -> str | list[dict[str, Any]]:
+ """
+ Build user message content, optionally with images.
+
+ Returns a plain string if no media, or a multimodal content list
+ with base64-encoded images.
+ """
+ if not media:
+ return text
+
+ content: list[dict[str, Any]] = []
+
+ for path in media:
+ encoded = self._encode_image(path)
+ if encoded:
+ content.append(encoded)
+
+ if not content:
+ return text
+
+ content.append({"type": "text", "text": text})
+ return content
+
+ @staticmethod
+ def _encode_image(file_path: str) -> dict[str, Any] | None:
+ """Encode a local image file to a base64 data URL for the LLM."""
+ path = Path(file_path)
+ if not path.is_file():
+ logger.warning(f"Media file not found: {file_path}")
+ return None
+
+ mime, _ = mimetypes.guess_type(file_path)
+ if not mime or not mime.startswith("image/"):
+ logger.warning(f"Unsupported media type for {file_path}: {mime}")
+ return None
+
+ data = path.read_bytes()
+ b64 = base64.b64encode(data).decode("utf-8")
+ return {
+ "type": "image_url",
+ "image_url": {"url": f"data:{mime};base64,{b64}"},
+ }
def add_tool_result(
self,
diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py
index 6fe2cfd..3925a44 100644
--- a/nanobot/agent/loop.py
+++ b/nanobot/agent/loop.py
@@ -152,7 +152,8 @@ class AgentLoop:
# Build initial messages (use get_history for LLM-formatted messages)
messages = self.context.build_messages(
history=session.get_history(),
- current_message=msg.content
+ current_message=msg.content,
+ media=msg.media if msg.media else None,
)
# Agent loop
From b7be3a6f9a016f3741261fd14bcdad2b7a7c1640 Mon Sep 17 00:00:00 2001
From: Peter van Eijk
Date: Mon, 2 Feb 2026 15:26:17 +0700
Subject: [PATCH 02/22] Add uv as install method
---
README.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/README.md b/README.md
index 1d69635..cd55335 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,14 @@ cd nanobot
pip install -e .
```
+**Install with uv**
+
+```bash
+uv venv
+source .venv/bin/activate
+uv pip install nanobot-ai
+```
+
## π Quick Start
> [!TIP]
From 20227f1f04ea195ec681477f03905f4954cc9f9a Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 2 Feb 2026 08:55:21 +0000
Subject: [PATCH 03/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 04/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 05/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 06/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 07/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 08/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 09/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 10/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 11/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 ac390253c3b3f75ba0425bb86074837228d6be69 Mon Sep 17 00:00:00 2001
From: Re-bin
Date: Mon, 2 Feb 2026 13:29:38 +0000
Subject: [PATCH 12/22] simplify vision support code
---
nanobot/agent/context.py | 55 ++++++++++------------------------------
1 file changed, 13 insertions(+), 42 deletions(-)
diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py
index 32585f5..f70103d 100644
--- a/nanobot/agent/context.py
+++ b/nanobot/agent/context.py
@@ -5,8 +5,6 @@ import mimetypes
from pathlib import Path
from typing import Any
-from loguru import logger
-
from nanobot.agent.memory import MemoryStore
from nanobot.agent.skills import SkillsLoader
@@ -148,50 +146,23 @@ When remembering something, write to {workspace_path}/memory/MEMORY.md"""
return messages
- def _build_user_content(
- self, text: str, media: list[str] | None
- ) -> str | list[dict[str, Any]]:
- """
- Build user message content, optionally with images.
-
- Returns a plain string if no media, or a multimodal content list
- with base64-encoded images.
- """
+ def _build_user_content(self, text: str, media: list[str] | None) -> str | list[dict[str, Any]]:
+ """Build user message content with optional base64-encoded images."""
if not media:
return text
-
- content: list[dict[str, Any]] = []
-
+
+ images = []
for path in media:
- encoded = self._encode_image(path)
- if encoded:
- content.append(encoded)
-
- if not content:
+ p = Path(path)
+ mime, _ = mimetypes.guess_type(path)
+ if not p.is_file() or not mime or not mime.startswith("image/"):
+ continue
+ b64 = base64.b64encode(p.read_bytes()).decode()
+ images.append({"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}})
+
+ if not images:
return text
-
- content.append({"type": "text", "text": text})
- return content
-
- @staticmethod
- def _encode_image(file_path: str) -> dict[str, Any] | None:
- """Encode a local image file to a base64 data URL for the LLM."""
- path = Path(file_path)
- if not path.is_file():
- logger.warning(f"Media file not found: {file_path}")
- return None
-
- mime, _ = mimetypes.guess_type(file_path)
- if not mime or not mime.startswith("image/"):
- logger.warning(f"Unsupported media type for {file_path}: {mime}")
- return None
-
- data = path.read_bytes()
- b64 = base64.b64encode(data).decode("utf-8")
- return {
- "type": "image_url",
- "image_url": {"url": f"data:{mime};base64,{b64}"},
- }
+ return images + [{"type": "text", "text": text}]
def add_tool_result(
self,
From 30d6e4b4b6462838d8dbcc76a8f6b04967cfd189 Mon Sep 17 00:00:00 2001
From: Re-bin
Date: Mon, 2 Feb 2026 16:18:04 +0000
Subject: [PATCH 13/22] feat: enhance scheduled reminders
---
nanobot/cli/commands.py | 2 ++
workspace/AGENTS.md | 10 ++++++++++
2 files changed, 12 insertions(+)
diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py
index d847710..d293564 100644
--- a/nanobot/cli/commands.py
+++ b/nanobot/cli/commands.py
@@ -506,6 +506,7 @@ def cron_add(
at: str = typer.Option(None, "--at", help="Run once at time (ISO format)"),
deliver: bool = typer.Option(False, "--deliver", "-d", help="Deliver response to channel"),
to: str = typer.Option(None, "--to", help="Recipient for delivery"),
+ channel: str = typer.Option(None, "--channel", help="Channel for delivery (e.g. 'telegram', 'whatsapp')"),
):
"""Add a scheduled job."""
from nanobot.config.loader import get_data_dir
@@ -534,6 +535,7 @@ def cron_add(
message=message,
deliver=deliver,
to=to,
+ channel=channel,
)
console.print(f"[green]β[/green] Added job '{job.name}' ({job.id})")
diff --git a/workspace/AGENTS.md b/workspace/AGENTS.md
index 0e5a055..a99a7b4 100644
--- a/workspace/AGENTS.md
+++ b/workspace/AGENTS.md
@@ -22,6 +22,16 @@ You have access to:
- Use `memory/` directory for daily notes
- Use `MEMORY.md` for long-term information
+## Scheduled Reminders
+
+When user asks for a reminder at a specific time, use `exec` to run:
+```
+nanobot cron add --name "reminder" --message "Your message" --at "YYYY-MM-DDTHH:MM:SS" --deliver --to "USER_ID" --channel "CHANNEL"
+```
+Get USER_ID and CHANNEL from the current session (e.g., `8281248569` and `telegram` from `telegram:8281248569`).
+
+**Do NOT just write reminders to MEMORY.md** β that won't trigger actual notifications.
+
## Heartbeat Tasks
`HEARTBEAT.md` is checked every 30 minutes. You can manage periodic tasks by editing this file:
From 2466d9e1dc40405ad996f75fbf181e172866ce10 Mon Sep 17 00:00:00 2001
From: Cheng Wang
Date: Tue, 3 Feb 2026 00:37:55 +0800
Subject: [PATCH 14/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 1af404c4d90501708338efb6f741ba45c93822be Mon Sep 17 00:00:00 2001
From: tlguszz1010
Date: Tue, 3 Feb 2026 14:08:36 +0900
Subject: [PATCH 15/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 16/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 17/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 18/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 19/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 20/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 21/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 22/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