feat(tools): Telegram gateway for soul chat + setup docs
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Neuron Telegram Gateway — Setup
|
||||
|
||||
The Telegram gateway lets you chat with your Neuron soul via Telegram. Plain messages go to the soul; commands give access to memory and status.
|
||||
|
||||
## 1. Create a bot via @BotFather
|
||||
|
||||
1. Open Telegram and search for **@BotFather**
|
||||
2. Send `/newbot`
|
||||
3. Pick a name (e.g. "Neuron")
|
||||
4. Pick a username (must end in `bot`, e.g. `myneuron_bot`)
|
||||
5. BotFather replies with your **HTTP API token** — looks like `7123456789:ABCdef...`
|
||||
6. Optionally set a description: `/setdescription` → select your bot → type a description
|
||||
|
||||
## 2. Store the token in the macOS Keychain
|
||||
|
||||
Never put the token in a plist, `.env`, or any file that might be committed.
|
||||
|
||||
```bash
|
||||
security add-generic-password \
|
||||
-s neuron-telegram-bot \
|
||||
-a neuron \
|
||||
-w '<paste token here>'
|
||||
```
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
security find-generic-password -s neuron-telegram-bot -a neuron -w
|
||||
```
|
||||
|
||||
## 3. Load the LaunchAgent
|
||||
|
||||
```bash
|
||||
launchctl load ~/Library/LaunchAgents/ai.neuron.telegram-gateway.plist
|
||||
```
|
||||
|
||||
Check it started:
|
||||
```bash
|
||||
launchctl list | grep telegram
|
||||
tail -f ~/.neuron/logs/telegram-gateway.out.log
|
||||
```
|
||||
|
||||
## 4. Test
|
||||
|
||||
Send your bot a message in Telegram. It should reply using your soul's voice.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `<any text>` | Forwarded to the soul → responds in its voice |
|
||||
| `/memory <query>` | Searches soul memories, returns top 3 |
|
||||
| `/remember <text>` | Stores text as a memory node |
|
||||
| `/status` | Reports whether the soul is reachable |
|
||||
|
||||
## Unload / stop
|
||||
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/ai.neuron.telegram-gateway.plist
|
||||
```
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
- **"token not found"** — re-run step 2 above
|
||||
- **"Soul is resting"** — the soul daemon at `http://localhost:7770` is not running; start it with `launchctl load ~/Library/LaunchAgents/ai.neuron.engram.plist` (or whichever plist runs the soul)
|
||||
- **Logs**: `~/.neuron/logs/telegram-gateway.out.log` and `telegram-gateway.err.log`
|
||||
- **Test gateway script directly**:
|
||||
```bash
|
||||
TELEGRAM_BOT_TOKEN=<token> ~/Development/neuron-technologies/neuron/tools/telegram-gateway.sh
|
||||
```
|
||||
|
||||
## Soul API endpoints used
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `POST /api/chat` | Forward messages to the soul |
|
||||
| `POST /api/neuron/recall` | Search memories |
|
||||
| `POST /api/neuron/memory` | Store conversation as a memory node |
|
||||
Executable
+191
@@ -0,0 +1,191 @@
|
||||
#!/bin/bash
|
||||
# Neuron Telegram Gateway
|
||||
# Polls Telegram for new messages, forwards to the soul at localhost:7770, sends responses back.
|
||||
# Supports plain text chat + commands: /memory, /remember, /status
|
||||
#
|
||||
# Token resolution order:
|
||||
# 1. $TELEGRAM_BOT_TOKEN env var
|
||||
# 2. macOS Keychain: security find-generic-password -s neuron-telegram-bot -a neuron -w
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TOKEN="${TELEGRAM_BOT_TOKEN:-$(security find-generic-password -s neuron-telegram-bot -a neuron -w 2>/dev/null || true)}"
|
||||
SOUL_URL="http://localhost:7770"
|
||||
OFFSET=0
|
||||
POLL_TIMEOUT=30
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "ERROR: No Telegram bot token. Set TELEGRAM_BOT_TOKEN or store in keychain." >&2
|
||||
echo "See: ~/Development/neuron-technologies/neuron/docs/telegram-bot-setup.md" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TG="https://api.telegram.org/bot${TOKEN}"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||
|
||||
# Send a Telegram message back to a chat
|
||||
send_message() {
|
||||
local chat_id="$1"
|
||||
local text="$2"
|
||||
curl -s -X POST "${TG}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --argjson cid "$chat_id" --arg t "$text" \
|
||||
'{chat_id: $cid, text: $t, parse_mode: "Markdown"}')" \
|
||||
> /dev/null
|
||||
}
|
||||
|
||||
# Store a memory in the soul
|
||||
store_memory() {
|
||||
local content="$1"
|
||||
local label="${2:-telegram:conversation}"
|
||||
curl -s -X POST "${SOUL_URL}/api/neuron/memory" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg c "$content" --arg l "$label" \
|
||||
'{content: $c, label: $l}')" \
|
||||
> /dev/null
|
||||
}
|
||||
|
||||
# Chat with the soul; echoes the response text
|
||||
soul_chat() {
|
||||
local message="$1"
|
||||
local from="${2:-unknown}"
|
||||
local response
|
||||
response=$(curl -s -X POST "${SOUL_URL}/api/chat" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg m "$message" --arg f "$from" \
|
||||
'{message: $m, from: $f}')" 2>/dev/null)
|
||||
# Extract .response — fall back to raw body on parse failure
|
||||
jq -r '.response // empty' <<< "$response" 2>/dev/null || echo "$response"
|
||||
}
|
||||
|
||||
# Search soul memories; echoes formatted results
|
||||
soul_recall() {
|
||||
local query="$1"
|
||||
local limit="${2:-3}"
|
||||
local raw
|
||||
raw=$(curl -s -X POST "${SOUL_URL}/api/neuron/recall" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg q "$query" --argjson l "$limit" \
|
||||
'{query: $q, limit: $l}')" 2>/dev/null)
|
||||
# Format top results as a numbered list (truncate long nodes to 300 chars)
|
||||
jq -r 'if type == "array" then
|
||||
to_entries | .[:3] | map(
|
||||
(.index + 1 | tostring) + ". " + (.value.content | .[0:300] | gsub("\n";" "))
|
||||
) | join("\n\n")
|
||||
else
|
||||
"No results found."
|
||||
end' <<< "$raw" 2>/dev/null || echo "No results found."
|
||||
}
|
||||
|
||||
# Check if soul is reachable
|
||||
soul_health() {
|
||||
curl -s --max-time 3 "${SOUL_URL}/" > /dev/null 2>&1 && echo "up" || echo "down"
|
||||
}
|
||||
|
||||
handle_update() {
|
||||
local update="$1"
|
||||
local chat_id msg_text from_name update_id
|
||||
|
||||
update_id=$(jq -r '.update_id' <<< "$update")
|
||||
chat_id=$(jq -r '.message.chat.id // empty' <<< "$update")
|
||||
msg_text=$(jq -r '.message.text // empty' <<< "$update")
|
||||
from_name=$(jq -r '.message.from.first_name // "stranger"' <<< "$update")
|
||||
|
||||
# Skip non-message updates (inline queries, etc.)
|
||||
if [[ -z "$chat_id" || -z "$msg_text" ]]; then
|
||||
OFFSET=$((update_id + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
log "[$update_id] from=$from_name chat=$chat_id text=${msg_text:0:60}"
|
||||
|
||||
# Route by command prefix
|
||||
if [[ "$msg_text" == /status* ]]; then
|
||||
local health
|
||||
health=$(soul_health)
|
||||
if [[ "$health" == "up" ]]; then
|
||||
send_message "$chat_id" "Soul is *online* at ${SOUL_URL} ✓"
|
||||
else
|
||||
send_message "$chat_id" "Soul appears to be *offline* (${SOUL_URL} unreachable)."
|
||||
fi
|
||||
|
||||
elif [[ "$msg_text" == /memory* ]]; then
|
||||
local query="${msg_text#/memory}"
|
||||
query="${query# }"
|
||||
if [[ -z "$query" ]]; then
|
||||
send_message "$chat_id" "Usage: /memory <query>"
|
||||
else
|
||||
local results
|
||||
results=$(soul_recall "$query" 3)
|
||||
if [[ -n "$results" ]]; then
|
||||
send_message "$chat_id" "*Memories matching \"${query}\":*
|
||||
|
||||
${results}"
|
||||
else
|
||||
send_message "$chat_id" "No memories found for \"${query}\"."
|
||||
fi
|
||||
fi
|
||||
|
||||
elif [[ "$msg_text" == /remember* ]]; then
|
||||
local content="${msg_text#/remember}"
|
||||
content="${content# }"
|
||||
if [[ -z "$content" ]]; then
|
||||
send_message "$chat_id" "Usage: /remember <text to store>"
|
||||
else
|
||||
store_memory "Telegram (${from_name}): ${content}" "telegram:explicit"
|
||||
send_message "$chat_id" "Stored: _${content}_"
|
||||
fi
|
||||
|
||||
else
|
||||
# Plain text — forward to soul chat
|
||||
local soul_response
|
||||
soul_response=$(soul_chat "$msg_text" "$from_name" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$soul_response" ]]; then
|
||||
soul_response="Neuron is resting — try again in a moment."
|
||||
fi
|
||||
|
||||
send_message "$chat_id" "$soul_response"
|
||||
|
||||
# Capture conversation as a memory (fire-and-forget)
|
||||
store_memory "Telegram conversation with ${from_name}: [user] ${msg_text} [soul] ${soul_response}" \
|
||||
"telegram:conversation" &
|
||||
fi
|
||||
|
||||
OFFSET=$((update_id + 1))
|
||||
}
|
||||
|
||||
log "Neuron Telegram gateway starting (soul=${SOUL_URL}, poll_timeout=${POLL_TIMEOUT}s)"
|
||||
|
||||
while true; do
|
||||
# Long-poll for updates
|
||||
UPDATES=$(curl -s --max-time $((POLL_TIMEOUT + 5)) \
|
||||
"${TG}/getUpdates?offset=${OFFSET}&timeout=${POLL_TIMEOUT}" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$UPDATES" ]]; then
|
||||
log "WARN: Empty response from Telegram; retrying in 5s"
|
||||
sleep 5
|
||||
continue
|
||||
fi
|
||||
|
||||
OK=$(jq -r '.ok // false' <<< "$UPDATES" 2>/dev/null)
|
||||
if [[ "$OK" != "true" ]]; then
|
||||
DESC=$(jq -r '.description // "unknown error"' <<< "$UPDATES" 2>/dev/null)
|
||||
log "WARN: Telegram API error: ${DESC}; retrying in 10s"
|
||||
sleep 10
|
||||
continue
|
||||
fi
|
||||
|
||||
# Iterate over each update
|
||||
COUNT=$(jq '.result | length' <<< "$UPDATES" 2>/dev/null || echo 0)
|
||||
if [[ "$COUNT" -gt 0 ]]; then
|
||||
for i in $(seq 0 $((COUNT - 1))); do
|
||||
update=$(jq ".result[$i]" <<< "$UPDATES")
|
||||
handle_update "$update"
|
||||
done
|
||||
fi
|
||||
|
||||
# Avoid hammering the API if something is very wrong
|
||||
sleep 1
|
||||
done
|
||||
Reference in New Issue
Block a user