Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f27b003
feat(model): /model picker, live Ollama discovery, persistent choice
Smilez1985 May 8, 2026
40ae47f
docs(changelog): /model picker + live Ollama discovery + persistence
Smilez1985 May 8, 2026
ff4d67d
fix(prompts): respect BOT_LANGUAGE in system prompt
Smilez1985 May 8, 2026
f6d1cba
fix(onboarding): auto-complete when IDENTITY.md is already populated
Smilez1985 May 8, 2026
0f0a8c1
fix(display): respect BOT_LANGUAGE in error_screen SAY: text
Smilez1985 May 8, 2026
95d1f0c
docs(changelog): respect BOT_LANGUAGE end-to-end (prompt + display + …
Smilez1985 May 8, 2026
c965f22
feat(update): /update command + auto_update.sh with backup & rollback
Smilez1985 May 8, 2026
1d4b529
docs(changelog): /update command and auto_update.sh
Smilez1985 May 8, 2026
e0e941d
Merge branch 'fix/respect-bot-language' into deploy/all-features
Smilez1985 May 8, 2026
ff41a5b
Merge branch 'feat/self-update' into deploy/all-features
Smilez1985 May 8, 2026
ed08ac5
feat(battery): UPS HAT (C) monitoring + /battery command
Smilez1985 May 8, 2026
fe161de
Merge branch 'feat/ups-hat-battery' into deploy/all-features
Smilez1985 May 8, 2026
f13d9d9
feat(display): auto-detect Waveshare 2.13in V4 mono vs B (3-color) va…
Smilez1985 May 8, 2026
288aa3d
fix(heartbeat): reinforce BOT_LANGUAGE pin for the reflection prompt
Smilez1985 May 9, 2026
b5b427f
feat(rag): generic external RAG service integration on deploy
Smilez1985 May 9, 2026
6a39e35
feat(display): low-battery red accent on B variant
Smilez1985 May 9, 2026
4315a39
docs(display-skill): document variant + red-as-accent rule
Smilez1985 May 9, 2026
2fa300d
fix(battery): UPS HAT (C) is 1S not 2S — fix percentage formula
Smilez1985 May 9, 2026
9a3f5c1
fix: restore handlers + main + config after partial branch-checkout d…
Smilez1985 May 9, 2026
00462e4
fix(display): propagate BOT_NAME + move battery to footer centre
Smilez1985 May 9, 2026
140d3bd
fix(auto_mood): don't duplicate header metrics in the footer status text
Smilez1985 May 9, 2026
420539e
fix(heartbeat): pin language at BOTH ends of the reflection prompt
Smilez1985 May 9, 2026
5829c39
feat(mcp): bot as MCP client — minimal SSE client + LLM tools
Smilez1985 May 9, 2026
cd631b0
feat(mcp): auto-register MCP tools as first-class + RAG-aware system …
Smilez1985 May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ coverage.xml
# ===================
rate_limits.json
data/cron_jobs.json
data/active_model.json
backups/
# data/custom_faces.json

# ===================
Expand All @@ -117,3 +119,4 @@ lore/

# Personal commands
.claude/commands/
data/active_model.json
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@

All notable changes to the OpenClawGotchi project will be documented in this file.

## [Unreleased] - 2026-05-09

### Added
- **External RAG service integration via REST**: opt-in connector to any RAG (Retrieval-Augmented Generation) backend that exposes a small documented HTTP contract (see `src/llm/rag_client.py` module docstring). New `/rag` Telegram command (`/rag`, `/rag <query>`, `/rag --top N <query>`). LLM tools `query_rag(query, top_k)` and `persist_to_rag(text, title, tags)`. Env vars: `RAG_API_URL` (empty disables), `RAG_API_KEY`, `RAG_DEFAULT_COLLECTIONS`.
- **Bot can also act as a generic MCP client over SSE.** New module `src/llm/rag_mcp_client.py` is a hand-rolled minimal MCP-over-SSE client (~250 LoC) using only the stdlib + `requests` (already in the venv via litellm). Speaks just enough of the MCP spec to do `initialize` + `tools/list` + `tools/call` against any SSE-transport server — no `mcp[cli]` dependency, so the Pi Zero 2W's RAM budget is respected (the official package pulls in `cryptography`, `pydantic-settings`, `starlette`, `uvicorn`, etc.).
- **Two new LLM tools for MCP**: `mcp_list_tools()` (discover) and `mcp_call_tool(name, arguments)` (invoke). The agent decides which advertised tool to call. Activates only when `RAG_TRANSPORT=mcp` is set in the environment.
- New env var `RAG_TRANSPORT=rest|mcp` (default `rest`). When `mcp` is selected, `RAG_API_URL` is interpreted as the MCP-SSE base URL (e.g. `http://your-rag-host:8766`).
Comment on lines +7 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Document the MCP auto-registration feature.

The changelog describes the two generic tools (mcp_list_tools, mcp_call_tool) but omits the auto-registration capability that is the main point of this PR. According to the PR objectives and test plan, when RAG_TRANSPORT=mcp and the MCP server is reachable at startup, the code discovers advertised tools via tools/list and registers each as a first-class entry in TOOL_MAP, allowing the LLM to call them directly (e.g., rag_search(query=..., top_k=3)) instead of the two-step mcp_list_tools → mcp_call_tool flow.

Additionally, the changelog doesn't mention the "External Memory (MCP)" system prompt section that is emitted when at least one MCP tool is registered, which instructs the agent when and how to use RAG tools.

📝 Suggested addition to document auto-registration

Consider adding a bullet point after line 11:

 - New env var `RAG_TRANSPORT=rest|mcp` (default `rest`). When `mcp` is selected, `RAG_API_URL` is interpreted as the MCP-SSE base URL (e.g. `http://your-rag-host:8766`).
+- **MCP tool auto-registration**: When `RAG_TRANSPORT=mcp` and the MCP server is reachable at startup, discovered tools (via `tools/list`) are automatically registered as first-class entries in `TOOL_MAP`. The LLM can call them directly (e.g., `rag_search(query, top_k)`) instead of using the generic two-step `mcp_list_tools` → `mcp_call_tool` flow. Colliding names are skipped; failures are gracefully logged. A "External Memory (MCP)" system prompt section is injected when registration succeeds, guiding the agent to search RAG before answering questions about preferences, project rules, or past context, and to persist durable lessons.
 - **`/model` Telegram command**: inline-keyboard model picker. Without args it opens buttons for every preset (gemini, glm, ollama). With an argument (`/model glm`) it falls through to the existing `/use` flow. `/use` and `/switch` remain as text aliases.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 7 - 11, The changelog omits documentation of the
MCP auto-registration feature: when RAG_TRANSPORT=mcp and the MCP server is
reachable at startup the code calls tools/list and auto-registers each
discovered tool into TOOL_MAP so they become first-class callable tools (not
just via mcp_list_tools → mcp_call_tool), and it also emits an "External Memory
(MCP)" system prompt when at least one MCP tool is registered; update the Added
section to mention auto-registration of discovered tools into TOOL_MAP, give an
example (e.g., discovered tool becomes rag_search(query..., top_k=...)), and
note the emitted "External Memory (MCP)" system prompt behavior when MCP tools
are present (include references to mcp_list_tools, mcp_call_tool, TOOL_MAP, and
RAG_TRANSPORT).

- **`/model` Telegram command**: inline-keyboard model picker. Without args it opens buttons for every preset (gemini, glm, ollama). With an argument (`/model glm`) it falls through to the existing `/use` flow. `/use` and `/switch` remain as text aliases.
- **Live Ollama discovery**: tapping `🦙 ollama ▸` queries the configured Ollama server (`/api/tags` + `/api/show`), filters by `capabilities.tools`, and only lists tool-capable models. Falls back to all installed models with a warning when none advertise tools. Includes `◂ Back` button and a graceful "could not reach server" state. New env vars: `OLLAMA_MODEL` (default `qwen2.5:14b`) and `OLLAMA_API_BASE` (placeholder default `http://ollama-server:11434`).
- **Persistent model choice**: `/model` and `/use` now write the selection to `data/active_model.json` (gitignored). On startup `LiteLLMConnector` restores it before falling back to `DEFAULT_LITE_PRESET`. Survives `systemctl restart` and reboots.
- **`/update` Telegram command + `scripts/auto_update.sh`**: owner-only command that fetches `origin/main`, fast-forwards if there are new commits, refreshes venv deps when `requirements.txt` changed, and restarts the systemd service. Supports `/update check` for dry-run. Cron-friendly so the bot can also auto-update unattended.
- **Update safety net**: before pulling, the script tarballs `gotchi.db` + `data/` + `.env` to `backups/pre-update-<timestamp>-<sha>.tar.gz` (rolling, keeps last 3 — see `OCG_BACKUP_KEEP`). If the service fails to come back up after the new code is in place, the script auto-rolls-back to the previous commit, reinstalls deps if needed, restarts, and exits with code 4 to flag the failed upgrade. Disable with `OCG_NO_BACKUP=1` / `OCG_NO_ROLLBACK=1`.
- **`gotchi-update` sudoers entry** in `setup.sh`: lets the bot user `systemctl restart gotchi-bot.service` without a password — needed by `/update` and the unattended cron path.
- **UPS HAT (C) battery monitoring** (Waveshare): new `hardware/battery.py` reads bus voltage, current and power from the on-board INA219 over I2C and reports a 0–100 % estimate based on the 1× 18650 voltage curve (3.0 V empty → 4.2 V full). Auto-detects the sensor and gracefully degrades when I2C is disabled or the HAT is absent — every public function returns `None` rather than raising.
- **`/battery` Telegram command**: shows the current reading (`🔋 87 % — 4.05 V, +120 mA (charging, 974 mW)`) or a friendly "no UPS HAT detected" hint with `i2cdetect` instructions.
- **System status line includes battery** (when present): `get_stats_string()` adds a `[BATTERY] …` line, so heartbeat reflections and the bot's self-awareness pick up battery state automatically.
- **Optional dep `smbus2`** added to `requirements.txt` (pure-Python, ~30 KB). Drop the line to disable battery support entirely.

### Changed
- **HTTP timeouts** raised via `Application.builder()` (`read=60`, `write=60`, `connect=30`, `pool=30`). Pi Zero 2W's WiFi can otherwise time out polling Telegram while a long Ollama reply is streaming, surfacing as `httpx.ReadError` / `Timed out`.

### Fixed
- **`BOT_LANGUAGE` was dead code in the system prompt**: defined in `config.py` and exposed via `.env`, but never injected anywhere — heartbeat reflections and the SAY: speech bubble would happily drift into Japanese/Chinese on Qwen-family models because no language was pinned. New `_language_directive()` in `llm/prompts.py` is part of `build_system_context()` and applies to every system prompt path (replies, heartbeat, SAY:). Codes mapped to readable names (`de` → "German (Deutsch)" etc.) for common languages; unknown codes pass through verbatim.
- **`error_screen()` SAY: text now respects `BOT_LANGUAGE`**: previously hardcoded Japanese (`システムエラー発生` etc.), which renders as garbled glyphs for owners who don't read it. Localized into `ja` / `en` / `de` / `ru` / `es` / `fr`. Default (when `BOT_LANGUAGE` is unset) stays Japanese to preserve the original cyberpunk aesthetic; unknown codes fall back to English.
- **Onboarding loop never exited**: `BOOTSTRAP.md` was only deleted when the LLM emitted a magic completion phrase ("onboarding complete", "saved to identity.md", …). Models that update `IDENTITY.md` correctly without that phrase left the bootstrap stale forever and re-triggered onboarding on every restart. `needs_onboarding()` now auto-completes when `IDENTITY.md` mtime > `BOOTSTRAP.md` mtime.

### Notes
- The MCP client uses a sync API throughout — slots into the existing TOOL_MAP dispatcher without async plumbing.
- A single background thread reads the SSE stream and routes JSON-RPC responses by id; one connected client is reused per process via a lazy singleton.
- Graceful degradation: when the MCP server is unreachable or `RAG_TRANSPORT` isn't set, the new tools return informative no-op strings; the bot stays alive.

## [Unreleased] - 2026-04-29

### Added
Expand Down
16 changes: 13 additions & 3 deletions gotchi-skills/display/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,19 @@ Custom faces from `data/custom_faces.json` are merged with defaults on each rend
## Display Info

- **Size:** 250x122 pixels
- **Colors:** Black & white only
- **Refresh:** ~2-3 seconds
- **Ghosting:** Use `--full` to clear
- **Variants:** two physical panels share the same code path (selected via `OCG_DISPLAY_VARIANT`):
- `mono` (default, `epd2in13_V4`): 2-color **black & white**, fast (~2 s) refresh, supports partial updates so face changes feel snappy.
- `b` (`epd2in13b_V4`): 3-color **black + red + white**, full refresh only (~15-20 s per update). The red plane is reserved for system-initiated warning accents — see "Color rule" below.
- **Refresh:** ~2-3 s mono, ~15-20 s on B variant
- **Ghosting:** Use `--full` to clear (mono only — B always full-refreshes)

## Color rule (B variant)

You **cannot** emit a "make this red" command — there is no `RED:` directive in the FACE/SAY/DISPLAY protocol. Red usage is decided by the bot's runtime code, not the LLM.

When you DO see something rendered red on a B-variant panel, it means a system-level **warning** is active (today: low battery, < 20 %). Treat red as a hint to the user, not as an aesthetic.

If you ever extend the protocol with an explicit red channel (e.g. a future `WARN:` directive), the rule remains: **red is an accent, never a background**. Never instruct the bot to "fill the screen red" or "make everything red" — that defeats the warning channel and looks broken.

## Do not

Expand Down
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ Pillow>=9.0.0
# Note: Only needed on actual Pi hardware
# RPi.GPIO
# spidev

# Optional: UPS HAT (C) battery monitoring via INA219 over I2C.
# `battery.py` gracefully degrades if smbus2 is missing or I2C is disabled,
# so removing this line just disables the /battery command.
smbus2>=0.4.0
160 changes: 160 additions & 0 deletions scripts/auto_update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
#
# OpenClawGotchi auto-update
#
# Fetches the configured upstream branch, fast-forwards if there are new
# commits, refreshes the venv's Python deps, restarts the systemd service,
# and rolls back automatically if the service fails to come back up.
#
# Idempotent and safe to run repeatedly (no-op when up-to-date).
#
# User state (.env, data/, .workspace/) is in .gitignore and never touched
# by `git pull`. As an extra safety net, gotchi.db + data/ + .env are
# tarballed to backups/ before each update; the last 3 are kept.
#
# Usage:
# bash scripts/auto_update.sh # update from origin/main
# bash scripts/auto_update.sh --check # exit 0 if updates available, 1 if not
#
# Env overrides:
# OCG_UPDATE_REMOTE (default: origin)
# OCG_UPDATE_BRANCH (default: main)
# OCG_SERVICE (default: gotchi-bot.service)
# OCG_BACKUP_KEEP (default: 3) — number of backups to retain
# OCG_NO_BACKUP=1 — skip the pre-update backup
# OCG_NO_ROLLBACK=1 — skip auto-rollback on service failure
#
# Cron suggestion (weekly, Sunday 04:00):
# 0 4 * * 0 /bin/bash /full/path/openclawgotchi/scripts/auto_update.sh \
# >> /full/path/openclawgotchi/logs/update.log 2>&1
#
# Exit codes:
# 0 success (updated or already up-to-date)
# 1 --check mode and no updates available
# 2 uncommitted changes block the update
# 3 service failed to start AND rollback was skipped/failed
# 4 service failed to start, rollback succeeded — manual review wanted
#
set -e

PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "${PROJECT_DIR}"

REMOTE="${OCG_UPDATE_REMOTE:-origin}"
BRANCH="${OCG_UPDATE_BRANCH:-main}"
SERVICE="${OCG_SERVICE:-gotchi-bot.service}"
VENV_PIP="${PROJECT_DIR}/venv/bin/pip"
BACKUP_DIR="${PROJECT_DIR}/backups"
BACKUP_KEEP="${OCG_BACKUP_KEEP:-3}"

log() { echo "[$(date -Iseconds)] $*"; }

# --- Pre-flight: refuse if tracked files are modified (untracked is fine) ---
DIRTY_TRACKED="$(git status --porcelain | grep -v '^??' || true)"
if [ -n "${DIRTY_TRACKED}" ]; then
log "ERROR: tracked files have uncommitted changes. Commit/stash first."
echo "${DIRTY_TRACKED}"
exit 2
fi

log "Fetching ${REMOTE}/${BRANCH}…"
git fetch --quiet "${REMOTE}" "${BRANCH}"

LOCAL_HEAD="$(git rev-parse HEAD)"
REMOTE_HEAD="$(git rev-parse "${REMOTE}/${BRANCH}")"
AHEAD_BY="$(git rev-list --count "HEAD..${REMOTE}/${BRANCH}")"

if [ "${AHEAD_BY}" = "0" ]; then
log "Up-to-date with ${REMOTE}/${BRANCH} (no new commits behind)."
[ "${1:-}" = "--check" ] && exit 1 || exit 0
fi

log "${AHEAD_BY} new commit(s) on ${REMOTE}/${BRANCH}:"
git --no-pager log --oneline "HEAD..${REMOTE}/${BRANCH}" | head -20

if [ "${1:-}" = "--check" ]; then
exit 0
fi

# --- Backup user state (DB + small JSON state + .env) before pulling ---
BACKUP_FILE=""
if [ "${OCG_NO_BACKUP:-0}" != "1" ]; then
mkdir -p "${BACKUP_DIR}"
TS="$(date +%Y%m%d-%H%M%S)"
BACKUP_FILE="${BACKUP_DIR}/pre-update-${TS}-${LOCAL_HEAD:0:8}.tar.gz"
# Build list of things to back up that actually exist (no errors on first runs).
BACKUP_PATHS=()
[ -f gotchi.db ] && BACKUP_PATHS+=(gotchi.db)
[ -f .env ] && BACKUP_PATHS+=(.env)
[ -d data ] && BACKUP_PATHS+=(data)
if [ "${#BACKUP_PATHS[@]}" -gt 0 ]; then
log "Backing up user state to $(basename "${BACKUP_FILE}")…"
tar -czf "${BACKUP_FILE}" "${BACKUP_PATHS[@]}" 2>/dev/null
# Rolling retention — keep newest N
ls -1t "${BACKUP_DIR}"/pre-update-*.tar.gz 2>/dev/null \
| tail -n +"$((BACKUP_KEEP + 1))" \
| xargs -r rm -f
else
log "No user state to back up yet (skipping)."
BACKUP_FILE=""
fi
fi

# --- Track previous HEAD so we can roll back if the service fails ---
PREVIOUS_HEAD="${LOCAL_HEAD}"
REQS_CHANGED="$(git diff --name-only "HEAD..${REMOTE}/${BRANCH}" -- requirements.txt | head -1)"

log "Pulling ${REMOTE}/${BRANCH} (fast-forward only)…"
git pull --ff-only --quiet "${REMOTE}" "${BRANCH}"

if [ -n "${REQS_CHANGED}" ] && [ -x "${VENV_PIP}" ]; then
log "requirements.txt changed — refreshing venv dependencies…"
"${VENV_PIP}" install --quiet --upgrade -r requirements.txt
fi

# --- Restart and verify ---
restart_service() {
sudo systemctl restart "${SERVICE}"
sleep 4
systemctl is-active --quiet "${SERVICE}"
}

if ! command -v systemctl >/dev/null 2>&1; then
log "systemctl not available, skipping service restart. Now at $(git rev-parse --short HEAD)."
exit 0
fi

log "Restarting ${SERVICE}…"
if restart_service; then
log "OK — ${SERVICE} is active. Now at $(git rev-parse --short HEAD)."
[ -n "${BACKUP_FILE}" ] && log "Pre-update backup: $(basename "${BACKUP_FILE}")"
exit 0
fi

# --- Auto-rollback path ---
log "ERROR — ${SERVICE} failed to come back up after update."
journalctl -u "${SERVICE}" -n 20 --no-pager 2>&1 | sed 's/^/ | /' || true

if [ "${OCG_NO_ROLLBACK:-0}" = "1" ]; then
log "OCG_NO_ROLLBACK=1 — skipping rollback. Manual intervention needed."
exit 3
fi

log "Rolling back to ${PREVIOUS_HEAD:0:8}…"
git reset --hard --quiet "${PREVIOUS_HEAD}"

if [ -n "${REQS_CHANGED}" ] && [ -x "${VENV_PIP}" ]; then
log "Reinstalling old requirements.txt…"
"${VENV_PIP}" install --quiet --upgrade -r requirements.txt
fi

if restart_service; then
log "Rollback succeeded — ${SERVICE} active at ${PREVIOUS_HEAD:0:8}."
log "The new version did not boot. Inspect with: journalctl -u ${SERVICE} -e"
exit 4
else
log "FATAL — rollback also failed to start the service. Manual intervention required."
log "Last 20 service log lines:"
journalctl -u "${SERVICE}" -n 20 --no-pager 2>&1 | sed 's/^/ | /' || true
exit 3
fi
6 changes: 6 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ echo "${USER} ALL=(ALL) NOPASSWD: ${PYTHON_VENV_PATH} ${UI_SCRIPT_PATH}" | sudo
sudo chmod 0440 "$SUDOERS_FILE"
echo " ✅ Display permissions configured (passwordless sudo)"

# Allow the bot user to restart its own service (used by /update + auto_update.sh)
UPDATE_SUDOERS_FILE="/etc/sudoers.d/gotchi-update"
echo "${USER} ALL=(ALL) NOPASSWD: /bin/systemctl restart gotchi-bot.service, /usr/bin/systemctl restart gotchi-bot.service" | sudo tee "$UPDATE_SUDOERS_FILE" > /dev/null
sudo chmod 0440 "$UPDATE_SUDOERS_FILE"
echo " ✅ /update permissions configured (passwordless service restart)"

# ============================================
# OPTIONAL: HARDENING (recommended for Pi Zero)
# ============================================
Expand Down
Loading