this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(bot): add Telegram bot with Letta integration

Implements src/bot.ts (bead assistant-pqh.2, assistant-pqh.4)

Bot features:
- Telegraf-based Telegram bot
- /start command with welcome message
- /help command listing capabilities
- Text message handling with typing indicator

Letta integration:
- Creates one agent per Telegram user (in-memory mapping)
- Agents use Claude Opus 4.5 via anthropic-proxy
- OpenAI embeddings for memory
- ADHD support persona in memory blocks
- Non-streaming message flow

Deployment modes:
- handleUpdate() for webhook mode (production)
- startPolling() for polling mode (development)
- Graceful shutdown on SIGINT/SIGTERM

Error handling:
- Catches and logs all errors
- User-friendly error messages
- Never crashes on message failures

Dependencies:
- telegraf@^4.16.3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice 125e2dfc 89695acf

+306 -1
+35
bun.lock
··· 8 8 "@ai-sdk/anthropic": "^2.0.54", 9 9 "@letta-ai/letta-client": "^1.3.3", 10 10 "ai": "^5.0.110", 11 + "telegraf": "^4.16.3", 11 12 }, 12 13 "devDependencies": { 13 14 "@types/bun": "latest", ··· 29 30 "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], 30 31 31 32 "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 33 + 34 + "@telegraf/types": ["@telegraf/types@7.1.0", "", {}, "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw=="], 32 35 33 36 "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 34 37 ··· 51 54 "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251211.1", "", { "os": "win32", "cpu": "x64" }, "sha512-OUB0nNmzZeCl0KjxeG7R+Ey1gq8iaVoJJRJpwKiTj6Ws5voKOb6PxNoM2jMNqJV3R/d3PXfR7Y39/IINioa/CQ=="], 52 55 53 56 "@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], 57 + 58 + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], 54 59 55 60 "ai": ["ai@5.0.110", "", { "dependencies": { "@ai-sdk/gateway": "2.0.19", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZBq+5bvef4e5qoIG4U6NJ1UpCPWGjuaWERHXbHu2T2ND3c02nJ2zlnjm+N6zAAplQPxwqm7Sb16mrRX5uQNWtQ=="], 56 61 62 + "buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="], 63 + 64 + "buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="], 65 + 66 + "buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="], 67 + 57 68 "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 58 69 70 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 71 + 72 + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], 73 + 59 74 "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], 60 75 61 76 "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], 62 77 78 + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], 79 + 80 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 81 + 82 + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], 83 + 84 + "p-timeout": ["p-timeout@4.1.0", "", {}, "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw=="], 85 + 86 + "safe-compare": ["safe-compare@1.1.4", "", { "dependencies": { "buffer-alloc": "^1.2.0" } }, "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ=="], 87 + 88 + "sandwich-stream": ["sandwich-stream@2.0.2", "", {}, "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ=="], 89 + 90 + "telegraf": ["telegraf@4.16.3", "", { "dependencies": { "@telegraf/types": "^7.1.0", "abort-controller": "^3.0.0", "debug": "^4.3.4", "mri": "^1.2.0", "node-fetch": "^2.7.0", "p-timeout": "^4.1.0", "safe-compare": "^1.1.4", "sandwich-stream": "^2.0.2" }, "bin": { "telegraf": "lib/cli.mjs" } }, "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w=="], 91 + 92 + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], 93 + 63 94 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 95 + 96 + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], 97 + 98 + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], 64 99 65 100 "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], 66 101 }
+2 -1
package.json
··· 10 10 "dependencies": { 11 11 "@ai-sdk/anthropic": "^2.0.54", 12 12 "@letta-ai/letta-client": "^1.3.3", 13 - "ai": "^5.0.110" 13 + "ai": "^5.0.110", 14 + "telegraf": "^4.16.3" 14 15 } 15 16 }
+269
src/bot.ts
··· 1 + /** 2 + * Telegram bot implementation using Telegraf 3 + * 4 + * Handles: 5 + * - Message processing and routing to Letta 6 + * - Bot commands (/start, /help) 7 + * - User-specific agent management 8 + * - Error handling and user-friendly responses 9 + */ 10 + 11 + import { Telegraf, type Context } from "telegraf"; 12 + import type { Update } from "telegraf/types"; 13 + import { config } from "./config"; 14 + import { getLettaClient } from "./letta"; 15 + 16 + /** 17 + * Create Telegraf bot instance 18 + */ 19 + export const bot = new Telegraf(config.TELEGRAM_BOT_TOKEN); 20 + 21 + /** 22 + * In-memory map of Telegram user ID -> Letta agent ID 23 + * In production, this would be stored in a database 24 + */ 25 + const userAgentMap = new Map<number, string>(); 26 + 27 + /** 28 + * Get or create a Letta agent for the given Telegram user 29 + * 30 + * For M1, this is a simple implementation that creates one agent per user. 31 + * Later milestones will add more sophisticated agent management. 32 + * 33 + * @param userId - Telegram user ID 34 + * @param username - Telegram username (for logging/debugging) 35 + * @returns Agent ID 36 + */ 37 + async function getOrCreateAgentForUser( 38 + userId: number, 39 + username?: string 40 + ): Promise<string> { 41 + // Check if we already have an agent for this user 42 + const existingAgentId = userAgentMap.get(userId); 43 + if (existingAgentId) { 44 + console.log(`Using existing agent ${existingAgentId} for user ${userId}`); 45 + return existingAgentId; 46 + } 47 + 48 + // Create a new agent for this user 49 + const client = getLettaClient(); 50 + 51 + try { 52 + console.log(`Creating new agent for user ${userId} (${username || "unknown"})`); 53 + 54 + // Create agent with basic configuration 55 + // For M1, we'll use a simple configuration. Later milestones will customize this. 56 + // Note: The model string should be in the format "provider/model-name" 57 + // The Letta server should be configured with the anthropic-proxy as a provider 58 + const agentState = await client.agents.create({ 59 + name: `user-${userId}-${username || "unknown"}`, 60 + description: `ADHD support agent for Telegram user ${userId}`, 61 + // Using Claude Opus 4.5 (should be configured in Letta server) 62 + model: "anthropic/claude-opus-4-5-20251101", 63 + embedding: "openai/text-embedding-ada-002", 64 + // Memory blocks with system prompt for ADHD support 65 + memory_blocks: [ 66 + { 67 + label: "persona", 68 + value: `You are a helpful ADHD support assistant. You help users with: 69 + - Task management and breaking down complex tasks 70 + - Time management and scheduling 71 + - Reducing overwhelm and executive dysfunction 72 + - Building habits and routines 73 + - Managing distractions 74 + 75 + Be supportive, understanding, and practical. Keep responses concise and actionable.`, 76 + }, 77 + ], 78 + }); 79 + 80 + console.log(`Created agent ${agentState.id} for user ${userId}`); 81 + 82 + // Store the mapping 83 + userAgentMap.set(userId, agentState.id); 84 + 85 + return agentState.id; 86 + } catch (error: any) { 87 + console.error(`Failed to create agent for user ${userId}:`, error); 88 + throw new Error( 89 + `Failed to create agent: ${error.message || "Unknown error"}` 90 + ); 91 + } 92 + } 93 + 94 + /** 95 + * Send a message to a Letta agent and get the response 96 + * 97 + * @param agentId - Letta agent ID 98 + * @param message - User message text 99 + * @returns Agent response text 100 + */ 101 + async function sendMessageToAgent( 102 + agentId: string, 103 + message: string 104 + ): Promise<string> { 105 + const client = getLettaClient(); 106 + 107 + try { 108 + // Send message to agent (non-streaming mode for simplicity in M1) 109 + const response = await client.agents.messages.create(agentId, { 110 + input: message, 111 + streaming: false, 112 + }); 113 + 114 + // Extract the assistant's response from the messages 115 + // The response contains an array of messages, we want the assistant's reply 116 + const assistantMessages = response.messages.filter( 117 + (msg) => msg.message_type === "assistant_message" 118 + ); 119 + 120 + if (assistantMessages.length === 0) { 121 + console.warn("No assistant message in response:", response); 122 + return "I'm sorry, I didn't generate a response. Please try again."; 123 + } 124 + 125 + // Get the last assistant message 126 + const lastMessage = assistantMessages[assistantMessages.length - 1]; 127 + 128 + // Extract text content from the message 129 + // AssistantMessage has a 'content' field which can be a string or array 130 + if (typeof lastMessage.content === "string") { 131 + return lastMessage.content; 132 + } 133 + 134 + // If content is an array, join text parts 135 + if (Array.isArray(lastMessage.content)) { 136 + const textParts = lastMessage.content 137 + .filter((part: any) => part.type === "text") 138 + .map((part: any) => part.text); 139 + return textParts.join("\n") || "I'm sorry, I couldn't process that message."; 140 + } 141 + 142 + console.warn("Unexpected message format:", lastMessage); 143 + return "I'm sorry, I received an unexpected response format."; 144 + } catch (error: any) { 145 + console.error(`Failed to send message to agent ${agentId}:`, error); 146 + throw new Error( 147 + `Failed to get response from agent: ${error.message || "Unknown error"}` 148 + ); 149 + } 150 + } 151 + 152 + /** 153 + * Handle /start command 154 + */ 155 + bot.command("start", async (ctx: Context) => { 156 + const welcomeMessage = `Welcome to the ADHD Support Agent! 157 + 158 + I'm here to help you with: 159 + - Task management and breaking down complex tasks 160 + - Time management and scheduling 161 + - Reducing overwhelm and executive dysfunction 162 + - Building habits and routines 163 + - Managing distractions 164 + 165 + Just send me a message and I'll do my best to help! 166 + 167 + Use /help to see available commands.`; 168 + 169 + await ctx.reply(welcomeMessage); 170 + }); 171 + 172 + /** 173 + * Handle /help command 174 + */ 175 + bot.command("help", async (ctx: Context) => { 176 + const helpMessage = `Available commands: 177 + 178 + /start - Show welcome message 179 + /help - Show this help message 180 + 181 + Just send me a regular message to chat! I'll remember our conversation and help you with ADHD-related challenges.`; 182 + 183 + await ctx.reply(helpMessage); 184 + }); 185 + 186 + /** 187 + * Handle text messages 188 + */ 189 + bot.on("message", async (ctx: Context) => { 190 + // Only handle text messages (ignore photos, videos, etc. for M1) 191 + if (!ctx.message || !("text" in ctx.message)) { 192 + return; 193 + } 194 + 195 + const messageText = ctx.message.text; 196 + const userId = ctx.from.id; 197 + const username = ctx.from.username; 198 + 199 + // Skip if it's a command (already handled by command handlers) 200 + if (messageText.startsWith("/")) { 201 + return; 202 + } 203 + 204 + try { 205 + // Show typing indicator while processing 206 + await ctx.sendChatAction("typing"); 207 + 208 + // Get or create agent for this user 209 + const agentId = await getOrCreateAgentForUser(userId, username); 210 + 211 + // Send message to agent and get response 212 + const response = await sendMessageToAgent(agentId, messageText); 213 + 214 + // Reply to user 215 + await ctx.reply(response); 216 + } catch (error: any) { 217 + console.error("Error handling message:", error); 218 + 219 + // Send user-friendly error message 220 + const errorMessage = 221 + "I'm sorry, I encountered an error processing your message. Please try again later."; 222 + 223 + await ctx.reply(errorMessage).catch((replyError) => { 224 + console.error("Failed to send error message to user:", replyError); 225 + }); 226 + } 227 + }); 228 + 229 + /** 230 + * Export function to handle Telegram updates (for webhook mode) 231 + * 232 + * @param update - Telegram Update object 233 + */ 234 + export async function handleUpdate(update: Update): Promise<void> { 235 + try { 236 + await bot.handleUpdate(update); 237 + } catch (error) { 238 + console.error("Error in handleUpdate:", error); 239 + throw error; 240 + } 241 + } 242 + 243 + /** 244 + * Start polling mode (for development) 245 + * 246 + * Only used when TELEGRAM_WEBHOOK_URL is empty. 247 + * Should NOT be called in production webhook mode. 248 + */ 249 + export async function startPolling(): Promise<void> { 250 + console.log("Starting Telegram bot in polling mode..."); 251 + 252 + try { 253 + await bot.launch(); 254 + console.log("Bot is running in polling mode"); 255 + 256 + // Enable graceful stop 257 + process.once("SIGINT", () => { 258 + console.log("SIGINT received, stopping bot..."); 259 + bot.stop("SIGINT"); 260 + }); 261 + process.once("SIGTERM", () => { 262 + console.log("SIGTERM received, stopping bot..."); 263 + bot.stop("SIGTERM"); 264 + }); 265 + } catch (error) { 266 + console.error("Failed to start polling:", error); 267 + throw error; 268 + } 269 + }