Telegram bots are one of the most practical ways to deploy an AI assistant — your users already have Telegram, there’s no app to install, and the bot API is straightforward. This guide builds a fully functional AI Telegram bot using python-telegram-bot and the Claude API. You’ll get commands, inline keyboards, per-user conversation history, error handling, and a systemd service for production.
If you want a simpler bot using just the Claude API without the python-telegram-bot wrapper, see Building a Telegram Bot with Claude API (Python, 2026).
Prerequisites
- Python 3.10+
- A Telegram account
- Anthropic API key (set as
ANTHROPIC_API_KEY) - A Telegram Bot Token from @BotFather
Step 1: Create a Bot with BotFather
Open Telegram and search for @BotFather. Send /newbot, follow the prompts, and you’ll receive a token like 7123456789:AAF.... Save it — you’ll use it as TELEGRAM_BOT_TOKEN.
Step 2: Install Dependencies
pip install python-telegram-bot anthropicThe python-telegram-bot library (v20+) uses Python’s asyncio under the hood. anthropic is the official Claude SDK.
Step 3: Basic Bot Structure
Here’s the minimal bot skeleton — we’ll expand each section:
import os
import anthropic
from telegram import Update
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
filters,
ContextTypes,
)
TELEGRAM_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
claude = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text(
"Hi! I'm an AI assistant powered by Claude.\n"
"Send me any message and I'll reply intelligently.\n\n"
"Commands: /start /help /clear"
)
async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text(
"Available commands:\n"
"/start — welcome message\n"
"/help — show this help\n"
"/clear — reset conversation history\n\n"
"Just type anything to chat with AI."
)
def main() -> None:
app = Application.builder().token(TELEGRAM_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_cmd))
print("Bot is running...")
app.run_polling()
if __name__ == "__main__":
main()Step 4: Add Conversation History
Claude’s API is stateless — you must pass the full message history on every request. Store it per chat ID using context.chat_data:
SYSTEM_PROMPT = (
"You are a helpful AI assistant inside a Telegram bot. "
"Be concise — Telegram messages are short. "
"Use plain text (no Markdown unless asked)."
)
MAX_HISTORY = 20 # keep last 20 turns to stay within token limits
def get_history(context: ContextTypes.DEFAULT_TYPE) -> list:
return context.chat_data.setdefault("history", [])
def trim_history(history: list) -> None:
if len(history) > MAX_HISTORY:
del history[: len(history) - MAX_HISTORY]
async def chat(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
user_text = update.message.text
history = get_history(context)
history.append({"role": "user", "content": user_text})
trim_history(history)
try:
response = claude.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=history,
)
answer = response.content[0].text
except anthropic.APIError as e:
answer = f"API error: {e}"
history.append({"role": "assistant", "content": answer})
await update.message.reply_text(answer)Register the handler in main():
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, chat))Step 5: /clear Command
Let users reset the conversation:
async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
context.chat_data["history"] = []
await update.message.reply_text("Conversation cleared. Starting fresh!")Step 6: Custom Commands — /summarize and /translate
Add task-specific commands that inject extra instructions:
async def summarize(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Summarize the text sent after /summarize."""
args = context.args
if not args:
await update.message.reply_text("Usage: /summarize <text to summarize>")
return
text = " ".join(args)
response = claude.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[
{
"role": "user",
"content": f"Summarize this in 3 bullet points:\n\n{text}",
}
],
)
await update.message.reply_text(response.content[0].text)
async def translate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Translate: /translate <lang> <text>."""
args = context.args
if len(args) < 2:
await update.message.reply_text("Usage: /translate <language> <text>")
return
lang = args[0]
text = " ".join(args[1:])
response = claude.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[
{
"role": "user",
"content": f"Translate to {lang}: {text}",
}
],
)
await update.message.reply_text(response.content[0].text)Step 7: Inline Keyboards
Inline keyboards add clickable buttons under messages. Here’s a mode-selector that changes the bot’s behavior:
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import CallbackQueryHandler
MODES = {
"concise": "Be extremely concise — one or two sentences max.",
"detailed": "Provide thorough, detailed answers with examples.",
"friendly": "Be casual and friendly, use simple language.",
}
async def mode(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[
InlineKeyboardButton("Concise", callback_data="mode_concise"),
InlineKeyboardButton("Detailed", callback_data="mode_detailed"),
InlineKeyboardButton("Friendly", callback_data="mode_friendly"),
]
]
await update.message.reply_text(
"Choose response mode:", reply_markup=InlineKeyboardMarkup(keyboard)
)
async def mode_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer()
selected = query.data.replace("mode_", "")
context.chat_data["mode"] = selected
await query.edit_message_text(f"Mode set to: {selected}")
# In chat(), pull the mode:
# mode_key = context.chat_data.get("mode", "concise")
# system = SYSTEM_PROMPT + " " + MODES[mode_key]Register the callback handler:
app.add_handler(CommandHandler("mode", mode))
app.add_handler(CallbackQueryHandler(mode_callback, pattern=r"^mode_"))Step 8: Typing Indicator
Show a typing indicator while Claude is processing — this improves UX significantly for longer responses:
from telegram.constants import ChatAction
async def chat(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.chat.send_action(ChatAction.TYPING)
# ... rest of the handlerStep 9: Complete Bot Script
Here’s the full bot in one file:
import os
import anthropic
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.constants import ChatAction
from telegram.ext import (
Application,
CallbackQueryHandler,
CommandHandler,
ContextTypes,
MessageHandler,
filters,
)
TELEGRAM_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
SYSTEM_PROMPT = (
"You are a helpful AI assistant inside a Telegram bot. "
"Be concise — Telegram messages are short."
)
MAX_HISTORY = 20
MODES = {
"concise": "Keep all replies under three sentences.",
"detailed": "Provide thorough answers with examples.",
"friendly": "Be casual and friendly.",
}
claude = anthropic.Anthropic()
def get_history(ctx):
return ctx.chat_data.setdefault("history", [])
def trim_history(history):
if len(history) > MAX_HISTORY:
del history[: len(history) - MAX_HISTORY]
async def start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Hi! I'm an AI assistant.\n/help — commands\n/clear — reset history\n/mode — change style"
)
async def help_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"/start — welcome\n/help — this message\n/clear — reset history\n"
"/mode — choose response style\n/summarize <text>\n/translate <lang> <text>"
)
async def clear(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
ctx.chat_data["history"] = []
await update.message.reply_text("History cleared.")
async def mode(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
keyboard = [[
InlineKeyboardButton("Concise", callback_data="mode_concise"),
InlineKeyboardButton("Detailed", callback_data="mode_detailed"),
InlineKeyboardButton("Friendly", callback_data="mode_friendly"),
]]
await update.message.reply_text("Choose mode:", reply_markup=InlineKeyboardMarkup(keyboard))
async def mode_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
selected = query.data.replace("mode_", "")
ctx.chat_data["mode"] = selected
await query.edit_message_text(f"Mode: {selected}")
async def summarize(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not ctx.args:
await update.message.reply_text("Usage: /summarize <text>")
return
text = " ".join(ctx.args)
await update.message.chat.send_action(ChatAction.TYPING)
resp = claude.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{"role": "user", "content": f"Summarize in 3 bullet points:\n{text}"}],
)
await update.message.reply_text(resp.content[0].text)
async def translate(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
args = ctx.args
if len(args) < 2:
await update.message.reply_text("Usage: /translate <lang> <text>")
return
lang, text = args[0], " ".join(args[1:])
await update.message.chat.send_action(ChatAction.TYPING)
resp = claude.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{"role": "user", "content": f"Translate to {lang}: {text}"}],
)
await update.message.reply_text(resp.content[0].text)
async def chat(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.chat.send_action(ChatAction.TYPING)
history = get_history(ctx)
mode_key = ctx.chat_data.get("mode", "concise")
system = SYSTEM_PROMPT + " " + MODES[mode_key]
history.append({"role": "user", "content": update.message.text})
trim_history(history)
try:
resp = claude.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=system,
messages=history,
)
answer = resp.content[0].text
except anthropic.APIError as e:
answer = f"Error: {e}"
history.append({"role": "assistant", "content": answer})
await update.message.reply_text(answer)
def main():
app = Application.builder().token(TELEGRAM_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_cmd))
app.add_handler(CommandHandler("clear", clear))
app.add_handler(CommandHandler("mode", mode))
app.add_handler(CommandHandler("summarize", summarize))
app.add_handler(CommandHandler("translate", translate))
app.add_handler(CallbackQueryHandler(mode_callback, pattern=r"^mode_"))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, chat))
print("Bot running. Press Ctrl+C to stop.")
app.run_polling()
if __name__ == "__main__":
main()Step 10: Deploy with systemd
Run the bot as a persistent background service on Linux:
# /etc/systemd/system/telegram-ai-bot.service
[Unit]
Description=Telegram AI Bot
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/telegram-bot
ExecStart=/home/ubuntu/telegram-bot/venv/bin/python bot.py
Environment=TELEGRAM_BOT_TOKEN=7123456789:AAF...
Environment=ANTHROPIC_API_KEY=sk-ant-...
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable telegram-ai-bot
sudo systemctl start telegram-ai-bot
sudo systemctl status telegram-ai-botRate Limiting
Telegram has no built-in rate limit per user, but Claude’s API does. Add a simple per-user rate limiter:
import time
from collections import defaultdict
_last_request: dict[int, float] = defaultdict(float)
MIN_INTERVAL = 2.0 # seconds between requests per user
async def chat(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
uid = update.effective_user.id
now = time.time()
if now - _last_request[uid] < MIN_INTERVAL:
await update.message.reply_text("Please wait a moment before sending another message.")
return
_last_request[uid] = now
# ... rest of handlerSummary
You’ve built a production-ready AI Telegram bot with:
- Per-chat conversation history stored in
context.chat_data - Custom commands:
/summarize,/translate - Inline keyboards for switching response modes
- Typing indicator for better UX
- systemd service for production deployment
- Simple per-user rate limiting
For a simpler approach using raw HTTP requests to the Telegram Bot API, see Building a Telegram Bot with Claude API.
Subscribe to my newsletter — practical guides on Claude API, AI agents, RAG, and automation.