How to Build a Telegram Bot with Python and AI (2026)

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 anthropic

The 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 handler

Step 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.target
sudo systemctl daemon-reload
sudo systemctl enable telegram-ai-bot
sudo systemctl start telegram-ai-bot
sudo systemctl status telegram-ai-bot

Rate 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 handler

Summary

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.

Subscribe