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