Build a Telegram Bot with Local AI (Ollama + Python Tutorial)
Want to go deeper than this article?
Free account unlocks the first chapter of all 25 courses — RAG, agents, MCP, voice AI, MLOps, real GitHub repos.
Ollama’s running. Here’s what to build with it. Go from “ollama run” to RAG apps, agents, and fine-tuned models — structured and hands-on. First chapter free.
Published on April 23, 2026 • 18 min read
Telegram is the most practical front-end for a personal AI assistant, and the reason is boring: it already runs on every device you own. Phone, tablet, desktop, web, and the ten-year-old laptop in the closet. You do not have to build a UI, ship an app, or maintain a web front-end. You only have to make it talk to a model you control instead of OpenAI's API.
This guide builds that bot in layers. It starts as roughly 30 lines that pipe messages to Ollama, then adds voice notes (Whisper transcription), photos (LLaVA vision), long documents (RAG), and character-by-character streaming so it feels like ChatGPT — with the entire pipeline on hardware you own, behind no public ports, and no API key going to anyone.
Everything below is copy-paste code with the reasoning behind each choice spelled out. By the end you will have a private AI assistant in your pocket that costs nothing per message and never sends a prompt to a third-party model provider.
Quick Start: Working Bot in 7 Minutes
# 1. Already have Ollama? Skip to step 2.
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3.1:8b
# 2. Get a Telegram bot token
# Open Telegram, message @BotFather, send /newbot, follow prompts.
# Copy the token (looks like 1234567890:ABCdefGhi-JklMnoPqRstUvWxyZ)
# 3. Install Python deps
pip install python-telegram-bot ollama python-dotenv
# 4. Run the bot
export TELEGRAM_TOKEN="1234567890:ABC..."
python telegram_bot.py
The minimal bot is 40 lines (full code below). Send your bot /start from your Telegram account, then ask it anything. By the end of this guide you will have streaming responses, voice and photo support, RAG over your own docs, and a production deployment that auto-restarts.
Reading articles is good. Building is better.
Free account = the first chapter of all 25 courses, with a per-chapter AI tutor. No card.
Table of Contents
- Why Build a Personal Telegram AI Bot
- Architecture Overview
- Hardware & Hosting Options
- Get Your Bot Token from BotFather
- Minimal Bot — Full Code
- Streaming Responses
- Voice Notes with Whisper
- Photo Understanding with LLaVA
- Adding RAG over Personal Docs
- Allowlist & Security
- Production Deployment
- Pitfalls
- FAQs
Why Build a Personal Telegram AI Bot
Three reasons, in order of how much they actually matter:
1. Telegram is the only chat UI that runs everywhere. iOS, Android, Mac, Windows, Linux, web, and a tablet you do not bother updating. One bot, every device, no per-platform app to maintain.
2. Long polling means no public ingress. Telegram bots can use long polling — your bot script connects out to Telegram's servers and asks for new messages. No port forwarding, no SSL certificate, no exposed home IP. From a security standpoint this is dramatically simpler than running your own web app.
3. Telegram's UX matches an AI assistant naturally. Threading via reply, voice notes, image attachments, file uploads, persistent history. You get all of it for free without writing any UI code.
Compared to a Slack/Discord bot (covered separately in build a local AI Slack & Discord bot), Telegram wins for personal and family use. The team-bot guide wins for workplace use.
Architecture Overview
[Telegram app] ──► Telegram MTProto servers ◄── [Your bot script] ──► [Ollama]
│
├──► [Whisper for voice]
│
└──► [ChromaDB for RAG]
Three things to know:
- No inbound connections to your machine. The bot connects out, polls Telegram, fetches new messages, sends replies. Same direction as a browser checking email.
- Ollama, Whisper, and ChromaDB all run on the same host (or your home network). Anywhere on private network is fine.
- Telegram never sees your model or its responses raw — it sees them as bot messages going to your account. Telegram could theoretically log them, so for truly sensitive content use Telegram's "Secret Chats" — but those don't support bots. Most users find Telegram's privacy posture acceptable for personal AI; if you need true zero-knowledge, build the same bot for Signal instead.
Have the whole stack running before your coffee goes cold
Ten Compose files that come up with one command — instead of an afternoon of debugging YAML and CUDA flags.
Hardware & Hosting Options
| Use Case | Hardware | Monthly Cost |
|---|---|---|
| Just you | Old laptop at home, port-forward not needed | $0 |
| You + family | Mini PC (Intel NUC, Beelink) at home | $0 + electricity |
| Always-on cloud | Hetzner GEX44 (RTX 4000 Ada) | €184 |
| Always-on budget | OVH RISE-1 + ollama on CPU | €17 |
| Burstable | Vast.ai GPU on-demand | $0.20-0.40/hr |
For personal use, the home setup wins on every metric, and you can predict whether a given box is fast enough before you buy anything. Local decoding is memory-bandwidth bound: the model reads its entire weight set once per generated token, and at Q4_K_M quantization that is roughly 0.6 GB per billion parameters. A 7B model therefore moves about 4.2 GB per token, so the arithmetic upper bound is simply memory bandwidth divided by 4.2 GB.
- Older mini PC on CPU — dual-channel DDR4-2666 tops out near 42 GB/s, giving an arithmetic ceiling of about 10 tokens/sec for a 7B model. Real CPU inference lands below that ceiling because prompt processing and sampling also compete for the same bandwidth, but it is fast enough to read comfortably for one or two people.
- Used RTX 3060 12 GB — 360 GB/s of VRAM bandwidth puts the same 7B model's ceiling near 85 tokens/sec, roughly 8x the CPU box, which is what makes a GPU worth adding once more than one person is using the bot.
These are ceilings from the arithmetic, not measurements — treat them as "this hardware cannot possibly be faster than X", and expect real output to be meaningfully lower.
For a deeper hosting walkthrough, see Ollama production deployment.
Get Your Bot Token from BotFather
- Open Telegram → search for
@BotFather→ start chat - Send
/newbot - Pick a display name (e.g., "My Local AI")
- Pick a username ending in
bot(e.g.,mylocalai_bot) - BotFather replies with a token like
1234567890:ABCdefGhi-JklMnoPqRstUvWxyZ
Save that token. Anyone with it can impersonate your bot, so treat it like a password. Put it in an .env file, never in git.
While you're with BotFather, configure these for nicer UX:
/setdescription— text shown when users open chat with bot/setabouttext— short bio in user info popup/setcommands— type-ahead command list:
start - Start the bot
help - Show available commands
reset - Clear conversation history
voice - Voice note transcription mode
img - Send a photo for analysis
Minimal Bot — Full Code
The 40-line version that already works:
# telegram_bot.py
import os
import logging
import asyncio
from telegram import Update
from telegram.ext import (
Application, CommandHandler, MessageHandler,
ContextTypes, filters
)
import ollama
logging.basicConfig(level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
log = logging.getLogger("telegram_bot")
OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
MODEL = os.environ.get("OLLAMA_MODEL", "llama3.1:8b")
client = ollama.AsyncClient(host=OLLAMA_HOST)
# Per-chat conversation history (production: use Redis/SQLite)
HISTORY: dict[int, list[dict]] = {}
SYSTEM_PROMPT = (
"You are a helpful, concise personal assistant on Telegram. "
"Use markdown sparingly. Keep responses under 4000 characters."
)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Hi! I'm your local AI. Send any message and I'll respond.\n"
"Commands: /reset to clear history, /help for more."
)
async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Send text — I'll reply.\n"
"Send a voice note — I'll transcribe + respond.\n"
"Send a photo — I'll describe what's in it.\n"
"/reset — clear our conversation history\n"
)
async def reset(update: Update, context: ContextTypes.DEFAULT_TYPE):
HISTORY.pop(update.effective_chat.id, None)
await update.message.reply_text("History cleared.")
async def chat(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
history = HISTORY.setdefault(chat_id, [])
user_msg = update.message.text
log.info(f"[{chat_id}] user: {user_msg[:80]}")
# Show typing indicator
await context.bot.send_chat_action(chat_id, "typing")
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(history[-10:])
messages.append({"role": "user", "content": user_msg})
try:
resp = await client.chat(model=MODEL, messages=messages,
options={"temperature": 0.5, "num_predict": 800})
answer = resp["message"]["content"].strip()
history.append({"role": "user", "content": user_msg})
history.append({"role": "assistant", "content": answer})
# Telegram caps message at 4096 chars
for i in range(0, len(answer), 4000):
await update.message.reply_text(answer[i:i+4000])
except Exception as e:
log.exception("ollama error")
await update.message.reply_text(f"Backend error: {e}")
def main():
token = os.environ["TELEGRAM_TOKEN"]
app = Application.builder().token(token).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_cmd))
app.add_handler(CommandHandler("reset", reset))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, chat))
log.info("Bot starting...")
app.run_polling()
if __name__ == "__main__":
main()
Run it with python telegram_bot.py. Open your bot in Telegram, send /start, then ask anything. The very first message is always the slowest because Ollama has to load the model into memory; once the model is resident, reply latency is dominated by generation speed, which the bandwidth arithmetic above bounds for your hardware.
Streaming Responses
The minimal bot above waits for the full response then sends it. ChatGPT-like streaming feels much better. The pattern:
async def chat_streaming(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
history = HISTORY.setdefault(chat_id, [])
user_msg = update.message.text
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(history[-10:])
messages.append({"role": "user", "content": user_msg})
# Send placeholder we will edit
sent = await update.message.reply_text("…")
full = ""
last_edit = 0
EDIT_EVERY = 25 # tokens
async for part in await client.chat(model=MODEL, messages=messages, stream=True):
chunk = part["message"]["content"]
full += chunk
if len(full) - last_edit >= EDIT_EVERY:
try:
await context.bot.edit_message_text(
chat_id=chat_id, message_id=sent.message_id, text=full
)
last_edit = len(full)
except Exception:
pass # Telegram throws if text unchanged or rate-limited
# Final edit with complete text
try:
await context.bot.edit_message_text(
chat_id=chat_id, message_id=sent.message_id, text=full
)
except Exception:
pass
history.append({"role": "user", "content": user_msg})
history.append({"role": "assistant", "content": full})
Replace the chat handler with chat_streaming. Two things to know:
- Telegram rate-limits message edits to ~30/minute per chat. Edit every 25 tokens (not every token) to stay under the limit.
- Use
parse_mode=Nonewhile streaming because partial markdown breaks. After the final edit, you can re-edit withparse_mode="Markdown"if you want formatting.
Voice Notes with Whisper
This is the killer feature for mobile use — speak instead of type.
Step 1: Install whisper.cpp
# Mac
brew install whisper-cpp
# Linux
git clone https://github.com/ggerganov/whisper.cpp
cd whisper.cpp && make
bash ./models/download-ggml-model.sh base.en
Step 2: Add Voice Handler
import subprocess
import tempfile
WHISPER_BIN = "/usr/local/bin/whisper-cli" # adjust path
WHISPER_MODEL = "/path/to/whisper.cpp/models/ggml-base.en.bin"
async def voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
voice_file = await update.message.voice.get_file()
with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as f:
await voice_file.download_to_drive(f.name)
ogg_path = f.name
# Convert ogg to wav (whisper.cpp needs 16kHz wav)
wav_path = ogg_path.replace(".ogg", ".wav")
subprocess.run([
"ffmpeg", "-y", "-i", ogg_path,
"-ar", "16000", "-ac", "1", wav_path
], check=True, capture_output=True)
# Transcribe
result = subprocess.run([
WHISPER_BIN, "-m", WHISPER_MODEL, "-f", wav_path, "-otxt", "-of", wav_path[:-4]
], check=True, capture_output=True)
with open(wav_path[:-4] + ".txt") as f:
transcript = f.read().strip()
# Cleanup
os.unlink(ogg_path); os.unlink(wav_path); os.unlink(wav_path[:-4] + ".txt")
# Show transcript, then respond as if it were a text message
await update.message.reply_text(f"_Transcribed:_ {transcript}", parse_mode="Markdown")
update.message.text = transcript # forge text and route to chat handler
await chat_streaming(update, context)
# Register
app.add_handler(MessageHandler(filters.VOICE, voice))
base.en is the smallest genuinely useful English model, and on modern CPUs it transcribes faster than real time, so the transcription step is rarely the bottleneck — the model load on the very first call is. whisper.cpp ships a bench example if you want a real number for your own machine rather than a guess. The bot first echoes the transcript (so you can verify it) then responds to it.
Photo Understanding with LLaVA
Drop a photo into Telegram and have your bot describe it.
ollama pull llava:13b
async def photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
photo_file = await update.message.photo[-1].get_file() # highest resolution
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
await photo_file.download_to_drive(f.name)
img_path = f.name
caption = update.message.caption or "Describe this image in detail."
await context.bot.send_chat_action(chat_id, "typing")
resp = await client.chat(
model="llava:13b",
messages=[{"role": "user", "content": caption, "images": [img_path]}]
)
answer = resp["message"]["content"].strip()
await update.message.reply_text(answer)
os.unlink(img_path)
app.add_handler(MessageHandler(filters.PHOTO, photo))
LLaVA 13B handles most everyday images well: receipts, screenshots, photos of whiteboards, food labels in foreign languages. For a true OCR workflow (turning a photo of a document into text + Q&A), see the RAG section below combined with this handler.
Adding RAG over Personal Docs
The most useful upgrade for a personal Telegram bot: it knows everything about your notes, recipes, runbooks, or whatever folder you point it at.
Step 1: Run ChromaDB
docker run -d -p 8000:8000 -v chroma:/chroma/chroma \
--name chroma chromadb/chroma:latest
Step 2: Index Your Files
# index.py — run once, then re-run when files change
import os, glob, chromadb, ollama
ollama_client = ollama.Client(host="http://localhost:11434")
chroma = chromadb.HttpClient(host="localhost", port=8000)
coll = chroma.get_or_create_collection(name="personal")
def chunk(text, size=800, overlap=100):
return [text[i:i+size] for i in range(0, len(text), size-overlap)]
def embed(text):
return ollama_client.embeddings(model="nomic-embed-text", prompt=text)["embedding"]
for path in glob.glob("./mynotes/**/*.md", recursive=True):
text = open(path).read()
for i, ch in enumerate(chunk(text)):
coll.upsert(ids=[f"{path}:{i}"], documents=[ch], embeddings=[embed(ch)],
metadatas=[{"source": path}])
print("Indexed.")
Step 3: RAG-Enabled Chat
async def chat_rag(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_msg = update.message.text
q_emb = ollama_client.embeddings(model="nomic-embed-text", prompt=user_msg)["embedding"]
results = coll.query(query_embeddings=[q_emb], n_results=5)
context_text = "\n\n---\n\n".join(results["documents"][0])
aug_system = (
SYSTEM_PROMPT + "\n\n"
"Use the following context from the user's personal notes to answer. "
"If not in context, use general knowledge but say so.\n\n"
f"CONTEXT:\n{context_text}"
)
# ... rest same as chat handler with aug_system instead of SYSTEM_PROMPT
Now @bot what was that pasta sauce I made last summer? finds the actual recipe in your notes folder. For deeper RAG tuning (chunk size, embedding choice, hybrid search), see RAG local setup guide.
Allowlist & Security
Anyone who finds your bot's username can message it. If you do not lock it down, strangers will eat your tokens (and read whatever your RAG returns).
Allowlist by User ID
Find your Telegram user ID by messaging @userinfobot. Then:
ALLOWED_USERS = {123456789, 987654321} # your IDs
async def auth_check(update: Update) -> bool:
user_id = update.effective_user.id
if user_id not in ALLOWED_USERS:
await update.message.reply_text(
"This bot is private. Contact the owner."
)
log.warning(f"Denied user {user_id} ({update.effective_user.username})")
return False
return True
# At top of every handler:
if not await auth_check(update):
return
Rate Limiting
import time
from collections import defaultdict, deque
USER_REQS: dict[int, deque] = defaultdict(deque)
def rate_ok(user_id: int, max_n=20, window=60) -> bool:
now = time.time()
q = USER_REQS[user_id]
while q and q[0] < now - window:
q.popleft()
if len(q) >= max_n:
return False
q.append(now)
return True
Don't Log Sensitive Content
Replace log.info(f"user: {user_msg[:80]}") with log.info(f"user message len={len(user_msg)}") once you start using RAG over personal data. Logs are a side channel.
Production Deployment
systemd
# /etc/systemd/system/tgbot.service
[Unit]
Description=Local AI Telegram bot
After=network.target ollama.service
[Service]
Type=simple
User=botuser
WorkingDirectory=/opt/tgbot
EnvironmentFile=/opt/tgbot/.env
ExecStart=/opt/tgbot/venv/bin/python telegram_bot.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now tgbot
journalctl -u tgbot -f
Docker Alternative
FROM python:3.12-slim
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "telegram_bot.py"]
docker build -t tgbot .
docker run -d --restart always --env-file .env --name tgbot --network host tgbot
Persistence
In-memory HISTORY: dict works fine for personal use. For multi-user or long-running bots, use SQLite:
import sqlite3, json
conn = sqlite3.connect("history.db")
conn.execute("CREATE TABLE IF NOT EXISTS h (chat_id INT PRIMARY KEY, history TEXT)")
Persisting saves you when systemd restarts and means the bot remembers conversations across deploys.
Monitoring
from prometheus_client import Counter, Histogram, start_http_server
REQ = Counter("tgbot_requests_total", "Total requests", ["kind"])
LAT = Histogram("tgbot_latency_seconds", "Latency seconds")
start_http_server(9101)
For full Prometheus + Grafana setup, see Ollama monitoring guide.
Pitfalls
- Forgetting to allowlist. Telegram bot usernames are globally searchable, so a live bot is discoverable by anyone whether or not you share the link. Add the allowlist before your first session, not after.
- Telegram message length cap (4096 chars). Always loop
for i in range(0, len(text), 4000). Otherwise long responses 500-error. - Stream edits getting rate-limited. Edit every 25-50 tokens, not every token. Telegram allows ~30 edits/minute per chat.
- Voice transcription latency on first run. Whisper loads the model lazily; the first transcription takes 5-10x longer. "Warm up" Whisper at startup with a dummy file.
- LLaVA model unloading. Ollama unloads models after 5 minutes by default. For mixed text+vision bots, set
OLLAMA_KEEP_ALIVE=24handOLLAMA_MAX_LOADED_MODELS=2. - Using the python-telegram-bot v13 syntax. v20+ is async. Many StackOverflow answers are stale. Stick to the official v20+ docs.
- Storing the bot token in code. Use
.envandpython-dotenv. Add.envto.gitignore.
Wrap-Up
Telegram + Ollama is the cleanest "personal AI in your pocket" setup that exists in 2026. It costs nothing once installed, runs on hardware you already own, and gives you the universal interface (works on every device) plus the modalities you actually need (text, voice, photo, document Q&A) without writing a single line of UI code.
The reason this pattern sticks is that it removes the two frictions that normally kill a self-hosted assistant. There is no app to install on anyone else's device, and there is no monthly bill that makes you ration your usage. Voice transcription in particular turns it into something non-technical people in a household will actually use, because talking to a phone is easier than typing on one.
Set it up over a weekend and the Monday version of this is summarizing your inbox while the coffee brews, voice-noting meeting prep on the bus, and querying your RAG-indexed notes folder for the API key you wrote down six months ago — none of which ever leaves hardware you control.
Looking for the workplace version? Build a local AI Slack & Discord bot covers team chat. For deeper integration patterns, see Ollama function calling and tool use.
Ollama’s running. Here’s what to build with it.
Go from “ollama run” to RAG apps, agents, and fine-tuned models — structured and hands-on. First chapter free.
Stop piecing Ollama together from blog posts
Ollama Mastery is 15 chapters end to end — install, model choice, Modelfiles, GPU offload, the API, and the 20 errors that actually happen. Plus 24 more courses.
Liked this? 25 full AI courses are waiting.
From fundamentals to RAG, agents, MCP servers, voice AI, and production deployment with real GitHub repos. First chapter free, every course.
Build Real AI on Your Machine
RAG, agents, NLP, vision, and MLOps - chapters across 25 courses that take you from reading about AI to building AI.
Want structured AI education?
25 courses, 519+ chapters, from $9. Understand AI, don't just use it.
Continue Your Local AI Journey
- PILLARBest Ollama Models 2026: 15 Ranked (Coding, Reasoning, Chat)
- AI on Steam Deck: Run Local LLMs with Ollama on SteamOS
- Air-Gapped AI Deployment: Install Ollama With No Internet
- Best Free Local AI Models to Run With Ollama (No API Key)
- Best Ollama Embedding Models Compared for Local RAG
- Best Ollama Models for 8GB RAM 2026: 12 Tested Local Picks
- Best Ollama Models for AI Agents 2026: Ranked by Tool Use
- Best Ollama Models for Tool Calling: BFCL Ranked (2026)
- Best Uncensored Local LLMs: Abliterated Ollama Models
- Build a Local AI Slack & Discord Bot with Ollama + Python
Comments (0)
No comments yet. Be the first to share your thoughts!