Build a Personal AI Agent with Telegram
Build a local AI agent with Gemma 4 and connect it to Telegram via tool calling.
Introduction
30 Second Summary
Think about how you ask ChatGPT or Claude a question and it decides whether to search the web, run code, or just answer directly. That decision-making ability is what separates a basic chatbot from an AI agent.
In this project, you will build your own personal AI agent that runs entirely on your machine and connects to Telegram so you can message it from your phone. Your agent will autonomously decide when to use tools like a calculator or clock to answer your questions.
What You'll Build
A personal AI agent powered by Gemma 4 through Ollama that responds to Telegram messages using the agentic loop pattern (think, act, observe) to decide when and how to use tools.
By the end of this project, you'll have:
- Built an agentic loop that lets your AI decide when to use tools vs. answer directly.
- Connected your agent to Telegram so you can message it from anywhere on your phone.
- Defined real tool functions (calculator, datetime, system info) that your agent calls autonomously.
- Secret Mission: Add web search so your agent can look things up on the internet.
Are there any prerequisites?
This project assumes you already have Python, VS Code, and Ollama installed with the Gemma 4 model pulled. You also need a Telegram account (free to create on your phone).
If you have 32GB of RAM you can run Gemma 4 comfortably. The project costs nothing because everything runs locally on your machine.
Set Up Your Project
Your AI agent needs a home. Before you can teach it to think and use tools, you need a project folder with the right Python packages installed and a connection to Telegram.
In this step, you'll create a workspace, install the three libraries that power your agent, and register a brand-new bot on Telegram that you'll later wire up to your agent's brain.
In this step, get ready to:
- Create a project folder and Python virtual environment.
- Install the ollama, python-telegram-bot, and python-dotenv packages.
- Create a Telegram bot and save its token in a .env file.
Create a project folder and virtual environment
Every Python project needs its own isolated environment so packages don't conflict with other projects on your machine. You'll create a folder on your Desktop and set up a virtual environment inside it.
- In VS Code, open a terminal by pressing Ctrl+` (macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir telegram-agent
cd telegram-agent
- Open this folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the telegram-agent folder, and click Open.
- If VS Code asks whether you trust the authors of the files in this folder, click Yes, I trust the authors.
- Open a new terminal in VS Code by pressing Ctrl+` (macOS and Windows).
Now create and activate your virtual environment. The commands differ by operating system.
macOS
- Create and activate a virtual environment by running these commands:
python3 -m venv .venv
source .venv/bin/activate
Windows
- Create and activate a virtual environment by running these commands:
python -m venv .venv
.venv\Scripts\activate
- Confirm the virtual environment is active by checking your terminal prompt. You should see (.venv) at the beginning of the line.
Don't see (.venv) in your prompt?
On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try the activate command again.
On macOS, make sure you typed source before the path. Without it, the activation script won't run in your current shell.
Help me activate my Python virtual environment.
Install the required Python packages
Your agent needs three packages: one to talk to Ollama, one to connect to Telegram, and one to load environment variables securely.
- Install all three packages by running this command:
pip install ollama python-telegram-bot python-dotenv
What do these packages do?
- ollama lets your Python code talk to the Ollama server running Gemma 4 on your machine.
- python-telegram-bot is the official-style library for building Telegram bots in Python. It handles all the API communication.
- python-dotenv loads secrets from a .env file so you never hardcode tokens in your source code.
- Wait for the installation to finish. You should see a Successfully installed message listing all three packages and their dependencies.
Seeing a permissions error or install failure?
Make sure your virtual environment is active. You should see (.venv) at the start of your terminal prompt. If not, re-run the activation command from the previous substep.
If you see a network error, check your internet connection and try again.
Help me fix my pip install error.
Create your Telegram bot via BotFather
Every Telegram bot is registered through BotFather. It will generate a unique token that your Python code uses to control the bot.
- Open Telegram on your phone or desktop.
- Search for @BotFather and open the chat.
- Send the message /newbot to start creating a new bot.
- BotFather will ask for a display name. Type any name you like (e.g. My AI Agent).
- BotFather will then ask for a username. This must end in bot (e.g. myaiagent_bot). The username cannot be changed later, so choose carefully.
- BotFather will reply with a message containing your bot token. It looks like a string of numbers, a colon, then letters and numbers. Copy this token.
What is a bot token?
The token is your bot's password. Anyone with the token can control your bot. You'll store it in a .env file that stays on your machine and never gets shared.
Now save the token in your project.
- In VS Code, create a new file by clicking File then New File.
- Save the file immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it .env in the telegram-agent folder.
- Add the following line to your .env file, replacing your-bot-token-here with the actual token you copied from BotFather:
TELEGRAM_BOT_TOKEN=your-bot-token-here
- Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
Why use a .env file?
Storing secrets in a .env file keeps them out of your source code. If you ever share your project or push it to GitHub, the token stays private as long as you add .env to your .gitignore.
- Confirm your project structure by running this in your terminal:
ls -a
- You should see .env and .venv in the output.
Don't see your .env file?
Files starting with a dot are hidden by default. The -a flag in ls -a shows hidden files. If .env isn't listed, check that you saved the file in the telegram-agent folder and not somewhere else.
Help me find my .env file.
✔️ Awesome, I've got everything!
Great. Make sure you've saved your .env file with your real bot token pasted in.
ⓧ I'd like to double check the full code
Here is what your .env file should look like (with your actual token replacing the placeholder):
TELEGRAM_BOT_TOKEN=your-bot-token-here
Your workspace is locked and loaded. Next up, you'll build the tools your agent will use to answer questions about time, math, and your system.
Build Your Agent's Tools
Your project is set up with all packages installed and your Telegram bot token safely stored. Now it's time to give your agent actual capabilities.
An LLM on its own can only generate text. To make it an agent, you give it tools that let it interact with the real world. In this step, you'll build three tools: one for checking the time, one for doing math, and one for reading system information.
In this step, get ready to:
- Define three tool functions in agent.py.
- Test the tools work independently.
Create agent.py and define your tool functions
Tools in an AI agent are regular functions with one special requirement: they need detailed docstrings. Ollama reads the docstrings to automatically generate the JSON schema that tells Gemma 4 what each tool does and what arguments it accepts.
- In VS Code, create a new file called agent.py in your project folder.
- Add the imports and your first tool function by pasting this code:
import asyncio
from datetime import datetime
import platform
import ast
import operator
from ollama import AsyncClient
def get_current_datetime() -> str:
"""Get the current date and time.
Returns:
str: The current date and time in a readable format.
"""
now = datetime.now()
return now.strftime("%A, %B %d, %Y at %I:%M %p")
What does this code do?
The imports pull in everything your agent will need: asyncio for async operations, datetime for time checks, platform for system info, and ast + operator for safe math evaluation.
The AsyncClient from Ollama is what you'll use later to talk to Gemma 4. The get_current_datetime function returns a human-readable date and time string.
Notice the Google-style docstring with a Returns: section. Ollama uses this docstring to auto-generate the tool schema that Gemma 4 reads. Without it, the model won't know how to call your tool.
- Save agent.py.
You won't see any output yet. The calculator tool is next.
- Add the calculate tool below your get_current_datetime function:
def calculate(expression: str) -> str:
"""Calculate a mathematical expression safely.
Args:
expression: A mathematical expression to evaluate, e.g. '2 + 2' or '15 * 7'.
Returns:
str: The result of the calculation.
"""
allowed_operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
ast.FloorDiv: operator.floordiv,
ast.USub: operator.neg,
}
def safe_eval(node):
if isinstance(node, ast.Constant):
if isinstance(node.value, (int, float)):
return node.value
raise ValueError(f"Unsupported constant: {node.value}")
elif isinstance(node, ast.BinOp):
left = safe_eval(node.left)
right = safe_eval(node.right)
op_type = type(node.op)
if op_type in allowed_operators:
return allowed_operators[op_type](left, right)
raise ValueError(f"Unsupported operator: {op_type.__name__}")
elif isinstance(node, ast.UnaryOp):
operand = safe_eval(node.operand)
op_type = type(node.op)
if op_type in allowed_operators:
return allowed_operators[op_type](operand)
raise ValueError(f"Unsupported unary operator: {op_type.__name__}")
else:
raise ValueError(f"Unsupported expression type: {type(node).__name__}")
try:
tree = ast.parse(expression, mode="eval")
result = safe_eval(tree.body)
return str(result)
except Exception as e:
return f"Error: {e}"
Why not just use eval()?
Python's built-in eval() executes arbitrary code. If your agent passed user input to eval(), someone could run malicious commands on your machine.
This calculator uses ast.parse() to convert the expression into a syntax tree, then walks the tree with a whitelist of allowed operations. Only numbers and basic math operators (add, subtract, multiply, divide, power, modulo, floor divide, negate) get through.
- Save agent.py.
One more tool to go.
- Add the get_system_info tool below the calculate function:
def get_system_info() -> str:
"""Get information about the current system including OS, Python version, and machine type.
Returns:
str: A summary of the current system information.
"""
info = (
f"OS: {platform.system()} {platform.release()}\n"
f"Machine: {platform.machine()}\n"
f"Python: {platform.python_version()}\n"
f"Processor: {platform.processor()}"
)
return info
What does this code do?
This function uses Python's platform module to gather details about the machine running your agent. It returns the OS name, machine architecture, Python version, and processor type as a formatted string.
Your agent will call this when someone asks a question like "What computer am I running?" or "What's my Python version?".
- Save agent.py.
Seeing a SyntaxError when you save?
Check that each function's docstring uses triple double quotes (""") and that the f" strings in get_system_info have matching quotes. Also confirm the indentation inside safe_eval uses 8 spaces (two levels of indent), since it's a nested function.
help me fix a SyntaxError in my agent.py file
✔️ Awesome, I've got everything!
Great. Double check you've saved agent.py with all three functions.
ⓧ I'd like to double check the full code
Here's what agent.py should look like so far:
import asyncio
from datetime import datetime
import platform
import ast
import operator
from ollama import AsyncClient
def get_current_datetime() -> str:
"""Get the current date and time.
Returns:
str: The current date and time in a readable format.
"""
now = datetime.now()
return now.strftime("%A, %B %d, %Y at %I:%M %p")
def calculate(expression: str) -> str:
"""Calculate a mathematical expression safely.
Args:
expression: A mathematical expression to evaluate, e.g. '2 + 2' or '15 * 7'.
Returns:
str: The result of the calculation.
"""
allowed_operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
ast.FloorDiv: operator.floordiv,
ast.USub: operator.neg,
}
def safe_eval(node):
if isinstance(node, ast.Constant):
if isinstance(node.value, (int, float)):
return node.value
raise ValueError(f"Unsupported constant: {node.value}")
elif isinstance(node, ast.BinOp):
left = safe_eval(node.left)
right = safe_eval(node.right)
op_type = type(node.op)
if op_type in allowed_operators:
return allowed_operators[op_type](left, right)
raise ValueError(f"Unsupported operator: {op_type.__name__}")
elif isinstance(node, ast.UnaryOp):
operand = safe_eval(node.operand)
op_type = type(node.op)
if op_type in allowed_operators:
return allowed_operators[op_type](operand)
raise ValueError(f"Unsupported unary operator: {op_type.__name__}")
else:
raise ValueError(f"Unsupported expression type: {type(node).__name__}")
try:
tree = ast.parse(expression, mode="eval")
result = safe_eval(tree.body)
return str(result)
except Exception as e:
return f"Error: {e}"
def get_system_info() -> str:
"""Get information about the current system including OS, Python version, and machine type.
Returns:
str: A summary of the current system information.
"""
info = (
f"OS: {platform.system()} {platform.release()}\n"
f"Machine: {platform.machine()}\n"
f"Python: {platform.python_version()}\n"
f"Processor: {platform.processor()}"
)
return info
Test the tools independently
Before wiring these tools into the agentic loop, let's confirm they each work on their own.
- Test the datetime tool by running this command in your terminal:
python -c "from agent import get_current_datetime; print(get_current_datetime())"
You should see today's date and time printed in a format like Tuesday, May 27, 2025 at 02:30 PM.
- Test the calculator tool by running:
python -c "from agent import calculate; print(calculate('247 * 38'))"
You should see 9386 printed.
- Test the system info tool by running:
python -c "from agent import get_system_info; print(get_system_info())"
You should see your operating system, machine type, Python version, and processor listed.
Seeing ModuleNotFoundError or ImportError?
Make sure your virtual environment is activated. You should see (.venv) at the start of your terminal prompt. Also confirm you're running the command from inside your project folder (the one containing agent.py).
help me fix an import error when testing my agent tools
All three tools are working. Next up, you'll build the agentic loop that lets Gemma 4 decide when to call these tools.
Build the Agentic Loop
In the previous step, you built three tools that your agent can use. Now comes the exciting part: building the brain that decides when and how to use them.
The agentic loop is the core pattern behind AI agents like Claude Code and OpenClaw. It follows a simple cycle: think, act, observe. The model thinks about what to do, acts by calling a tool, observes the result, and repeats until it has a complete answer.
In this step, get ready to:
- Register your tool functions so Gemma 4 can discover and call them.
- Build the run_agent function that implements the think-act-observe loop.
- Test the agent locally to confirm it uses tools correctly.
Register your tools and system prompt
Your three tool functions exist, but Gemma 4 doesn't know about them yet. You need a registry that maps function names to the actual functions, and a system prompt that tells the model what it can do.
- In agent.py, scroll to the bottom of the file (below the get_system_info function) and add the following code:
TOOLS = [get_current_datetime, calculate, get_system_info]
TOOL_MAP = {fn.__name__: fn for fn in TOOLS}
SYSTEM_PROMPT = (
"You are a helpful personal AI assistant. You have access to tools that let you "
"check the current date and time, perform calculations, and get system information. "
"Use these tools when they would help answer the user's question. "
"When you don't need a tool, just respond directly."
)
What does this code do?
- TOOLS is a list of your three Python functions. The Ollama SDK reads each function's docstring and automatically generates the JSON schema that Gemma 4 needs to understand what the tool does.
- TOOL_MAP is a dictionary that maps each function's name (as a string) to the function itself. When Gemma 4 says "call calculate", this map lets you look up and execute the right function.
- SYSTEM_PROMPT instructs Gemma 4 on its role and what tools are available. It also tells the model to respond directly when no tool is needed.
- Save agent.py.
Why pass Python functions directly?
Most tool-calling examples require you to manually write JSON schemas describing each tool. The Ollama Python SDK skips this by reading your function's type hints and Google-style docstrings to auto-generate the schema. That's why the docstrings on your tool functions are so detailed.
Implement the agentic loop
Now for the core of your agent. The run_agent function sends the user's message to Gemma 4 along with the tools list. If the model decides to call a tool, your code executes it and feeds the result back. This loop continues until the model generates a final text response.
- In agent.py, add the following function below the SYSTEM_PROMPT you just added:
async def run_agent(user_message: str) -> str:
"""Run the agentic loop: send a message to Gemma 4, handle tool calls, return final response."""
client = AsyncClient()
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
]
max_iterations = 5
iteration = 0
while iteration < max_iterations:
iteration += 1
response = await client.chat(
model="gemma4",
messages=messages,
tools=TOOLS,
)
if not response.message.tool_calls:
return response.message.content or "I wasn't able to generate a response."
messages.append(response.message)
for tool_call in response.message.tool_calls:
function_name = tool_call.function.name
arguments = tool_call.function.arguments
if function_name in TOOL_MAP:
result = TOOL_MAP[function_name](**arguments)
else:
result = f"Error: Unknown tool '{function_name}'"
messages.append(
{"role": "tool", "content": str(result), "tool_name": function_name}
)
return "I reached the maximum number of tool calls. Here's what I found so far."
How does the agentic loop work?
This function implements the think-act-observe cycle that powers every AI agent:
- Think: client.chat() sends the conversation (system prompt + user message + any tool results so far) to Gemma 4. The model decides whether to call a tool or respond directly.
- Act: If response.message.tool_calls is not empty, the model wants to use a tool. Your code looks up the function in TOOL_MAP and executes it with the arguments Gemma 4 provided.
- Observe: The tool result is appended to the messages list with role: "tool". On the next loop iteration, Gemma 4 sees this result and can either call another tool or generate a final answer.
- max_iterations prevents infinite loops. If the model keeps calling tools without producing a final answer, the function exits after 5 rounds.
When the model has all the information it needs, it responds without any tool calls. The function detects this and returns the text response to the user.
- Save agent.py.
Seeing a syntax error?
- Make sure run_agent is defined at the module level (no extra indentation), directly below SYSTEM_PROMPT.
- Check that you have from ollama import AsyncClient at the top of your file from the previous step.
- Verify you have a blank line between SYSTEM_PROMPT and the async def run_agent line.
Help me debug a syntax error in my agent.py file
Test the agent in your terminal
Before connecting the agent to Telegram, you should test it locally. You will add a test block that sends several prompts to your agent and prints the responses.
- At the very bottom of agent.py, add this test block:
if __name__ == "__main__":
async def test():
print("Testing agent...")
print()
test_prompts = [
"What time is it right now?",
"What is 247 multiplied by 38?",
"What operating system am I running?",
"Hello! How are you?",
]
for prompt in test_prompts:
print(f"User: {prompt}")
response = await run_agent(prompt)
print(f"Agent: {response}")
print()
asyncio.run(test())
What does this test block do?
- if __name__ == "__main__" ensures this code only runs when you execute agent.py directly (not when it's imported by another file like bot.py later).
- The four test prompts each exercise a different path: the first three should trigger tool calls (datetime, calculate, system info), and the last one should get a direct response without tools.
- asyncio.run(test()) starts the async event loop so the await calls inside run_agent can execute.
- Save agent.py.
- Run your agent by executing this command in your terminal:
python agent.py
You should see output for each test prompt. The agent should return the current date and time, calculate 9386 (247 * 38), report your OS details, and give a friendly greeting without calling any tools.
First run takes a moment
The first time you run this, Ollama needs to load the Gemma 4 model into memory. This can take 10-30 seconds depending on your machine. Subsequent runs will be faster because the model stays loaded.
Not seeing any output?
- Make sure Ollama is running. Open a separate terminal and run ollama list to verify gemma4 is available.
- Check that your virtual environment is activated. You should see (.venv) at the start of your terminal prompt.
- If you see a connection error, Ollama may not be running. Start it with ollama serve in a separate terminal window.
Help me figure out why my agent isn't returning any output
✔️ Awesome, I've got everything!
Great. Double check you have saved agent.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete agent.py file at this point:
import asyncio
from datetime import datetime
import platform
import ast
import operator
from ollama import AsyncClient
def get_current_datetime() -> str:
"""Get the current date and time.
Returns:
str: The current date and time in a readable format.
"""
now = datetime.now()
return now.strftime("%A, %B %d, %Y at %I:%M %p")
def calculate(expression: str) -> str:
"""Calculate a mathematical expression safely.
Args:
expression: A mathematical expression to evaluate, e.g. '2 + 2' or '15 * 7'.
Returns:
str: The result of the calculation.
"""
allowed_operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
ast.FloorDiv: operator.floordiv,
ast.USub: operator.neg,
}
def safe_eval(node):
if isinstance(node, ast.Constant):
if isinstance(node.value, (int, float)):
return node.value
raise ValueError(f"Unsupported constant: {node.value}")
elif isinstance(node, ast.BinOp):
left = safe_eval(node.left)
right = safe_eval(node.right)
op_type = type(node.op)
if op_type in allowed_operators:
return allowed_operators[op_type](left, right)
raise ValueError(f"Unsupported operator: {op_type.__name__}")
elif isinstance(node, ast.UnaryOp):
operand = safe_eval(node.operand)
op_type = type(node.op)
if op_type in allowed_operators:
return allowed_operators[op_type](operand)
raise ValueError(f"Unsupported unary operator: {op_type.__name__}")
else:
raise ValueError(f"Unsupported expression type: {type(node).__name__}")
try:
tree = ast.parse(expression, mode="eval")
result = safe_eval(tree.body)
return str(result)
except Exception as e:
return f"Error: {e}"
def get_system_info() -> str:
"""Get information about the current system including OS, Python version, and machine type.
Returns:
str: A summary of the current system information.
"""
info = (
f"OS: {platform.system()} {platform.release()}\n"
f"Machine: {platform.machine()}\n"
f"Python: {platform.python_version()}\n"
f"Processor: {platform.processor()}"
)
return info
TOOLS = [get_current_datetime, calculate, get_system_info]
TOOL_MAP = {fn.__name__: fn for fn in TOOLS}
SYSTEM_PROMPT = (
"You are a helpful personal AI assistant. You have access to tools that let you "
"check the current date and time, perform calculations, and get system information. "
"Use these tools when they would help answer the user's question. "
"When you don't need a tool, just respond directly."
)
async def run_agent(user_message: str) -> str:
"""Run the agentic loop: send a message to Gemma 4, handle tool calls, return final response."""
client = AsyncClient()
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
]
max_iterations = 5
iteration = 0
while iteration < max_iterations:
iteration += 1
response = await client.chat(
model="gemma4",
messages=messages,
tools=TOOLS,
)
if not response.message.tool_calls:
return response.message.content or "I wasn't able to generate a response."
messages.append(response.message)
for tool_call in response.message.tool_calls:
function_name = tool_call.function.name
arguments = tool_call.function.arguments
if function_name in TOOL_MAP:
result = TOOL_MAP[function_name](**arguments)
else:
result = f"Error: Unknown tool '{function_name}'"
messages.append(
{"role": "tool", "content": str(result), "tool_name": function_name}
)
return "I reached the maximum number of tool calls. Here's what I found so far."
if __name__ == "__main__":
async def test():
print("Testing agent...")
print()
test_prompts = [
"What time is it right now?",
"What is 247 multiplied by 38?",
"What operating system am I running?",
"Hello! How are you?",
]
for prompt in test_prompts:
print(f"User: {prompt}")
response = await run_agent(prompt)
print(f"Agent: {response}")
print()
asyncio.run(test())
Your agent is thinking, acting, and observing all on its own. Next up, you will connect it to Telegram so you can chat with it from your phone.
Connect Your Agent to Telegram
Your agent can think, call tools, and generate responses. Right now it only works in your terminal.
In this step, you will create a bot.py file that connects your agent to Telegram. Every message you send from your phone will flow through the Telegram Bot API to your local machine, trigger the agentic loop, and send a response back.
In this step, get ready to:
- Create bot.py with imports, environment config, and a /start command handler.
- Wire up the message handler to call your run_agent function and return responses to Telegram.
- Run the bot and test it from your phone.
Create the bot file with imports and the /start handler
The python-telegram-bot library handles all the network communication with Telegram's servers. You just define handler functions that respond to commands and messages.
- Create a new file called bot.py in your project folder.
- Add the imports and environment loading by pasting this code:
import asyncio
import os
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import (
ApplicationBuilder,
CommandHandler,
MessageHandler,
ContextTypes,
filters,
)
from agent import run_agent
# Load environment variables from .env
load_dotenv()
# Get the bot token from environment
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
What does this code do?
The telegram imports bring in the bot framework. Update represents an incoming message from a user. ApplicationBuilder constructs your bot application.
The from agent import run_agent line imports the agentic loop you built in the previous step. This is where the actual AI thinking happens.
- Save bot.py.
- Below the TOKEN line, add the /start command handler by pasting this code:
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle the /start command."""
welcome_message = (
"\ud83d\udc4b Hello! I'm your personal AI agent powered by Gemma 4.\n\n"
"I can help you with:\n"
"\ud83d\udd50 Checking the current date and time\n"
"\ud83e\uddee Doing math calculations\n"
"\ud83d\udcbb Getting system information\n\n"
"Just send me a message and I'll figure out the best way to help!"
)
await update.message.reply_text(welcome_message)
What does this code do?
When someone sends /start to your bot (which happens automatically the first time they message it), this function runs and sends back a welcome message listing what the bot can do.
The update parameter contains the incoming message data. The context parameter holds bot-level data (you won't need it here, but Telegram requires it).
- Save bot.py.
Seeing a red underline on the telegram imports?
Make sure your virtual environment is activated. VS Code might be using your system Python instead of the one with python-telegram-bot installed.
Check the bottom-right of VS Code for the Python interpreter. It should point to your .venv folder.
Help me fix my Python imports in VS Code.
Wire up the message handler to call your agent
The /start handler covers the first-time greeting. Now you need a handler for every regular message the user sends. This is where your agent gets called.
- Below the start_command function, add the message handler by pasting this code:
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle incoming text messages by running the agent."""
# Get the user's message text
user_message = update.message.text
# Send a "thinking" placeholder so the user knows something is happening
thinking_message = await update.message.reply_text("\ud83e\udd14 Thinking...")
try:
# Run the agentic loop with the user's message
response = await run_agent(user_message)
# Replace the thinking message with the agent's response
await thinking_message.edit_text(response)
except Exception as e:
await thinking_message.edit_text(f"\u274c Sorry, something went wrong: {e}")
What does this code do?
This is the bridge between Telegram and your agent. When a user sends any text message, this function:
- Immediately sends a "Thinking..." reply so the user sees instant feedback.
- Calls run_agent(user_message) to trigger the full agentic loop (think, act, observe).
- Edits the thinking message to show the final response once the agent finishes.
- Catches any errors and shows them to the user instead of crashing silently.
- Save bot.py.
- Below the handle_message function, add the main() function that ties everything together:
def main() -> None:
"""Start the Telegram bot."""
# Check that the token exists before trying to start
if not TOKEN:
print("Error: TELEGRAM_BOT_TOKEN not found in .env file!")
print("Please add your bot token to the .env file.")
return
# Build the Telegram application with your bot token
app = ApplicationBuilder().token(TOKEN).build()
# Register the /start command handler
app.add_handler(CommandHandler("start", start_command))
# Register the message handler for all text that isn't a command
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
print("\ud83e\udd16 Bot is running! Send a message to your bot on Telegram.")
print("Press Ctrl+C to stop.")
# Start polling Telegram for updates
app.run_polling()
if __name__ == "__main__":
main()
What does this code do?
The main() function is the entry point for your bot. It builds the application using ApplicationBuilder and registers two handlers.
- CommandHandler("start", start_command) routes /start messages to your welcome function.
- MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message) routes all other text messages to your agent handler.
- app.run_polling() keeps checking Telegram for new messages in a loop.
- Save bot.py.
Not sure where to paste the code?
Each chunk goes directly below the previous one. Your file from top to bottom should be: imports, then start_command, then handle_message, then main().
Help me figure out the correct order for my bot.py functions.
✔️ Awesome, I've got everything!
Great. Double check you have saved bot.py before moving on.
ⓧ I'd like to double check the full code
import asyncio
import os
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import (
ApplicationBuilder,
CommandHandler,
MessageHandler,
ContextTypes,
filters,
)
from agent import run_agent
load_dotenv()
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle the /start command."""
welcome_message = (
"\ud83d\udc4b Hello! I'm your personal AI agent powered by Gemma 4.\n\n"
"I can help you with:\n"
"\ud83d\udd50 Checking the current date and time\n"
"\ud83e\uddee Doing math calculations\n"
"\ud83d\udcbb Getting system information\n\n"
"Just send me a message and I'll figure out the best way to help!"
)
await update.message.reply_text(welcome_message)
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle incoming text messages by running the agent."""
user_message = update.message.text
thinking_message = await update.message.reply_text("\ud83e\udd14 Thinking...")
try:
response = await run_agent(user_message)
await thinking_message.edit_text(response)
except Exception as e:
await thinking_message.edit_text(f"\u274c Sorry, something went wrong: {e}")
def main() -> None:
"""Start the Telegram bot."""
if not TOKEN:
print("Error: TELEGRAM_BOT_TOKEN not found in .env file!")
print("Please add your bot token to the .env file.")
return
app = ApplicationBuilder().token(TOKEN).build()
app.add_handler(CommandHandler("start", start_command))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
print("\ud83e\udd16 Bot is running! Send a message to your bot on Telegram.")
print("Press Ctrl+C to stop.")
app.run_polling()
if __name__ == "__main__":
main()
Run the bot and test it from your phone
Your bot is ready to go live. When you run bot.py, it starts polling Telegram for messages and routing them to your agent.
- Make sure Ollama is running on your machine. If it is not running, open it from your applications.
- Start your bot by running this command in your terminal:
python bot.py
- You should see the message: Bot is running! Send a message to your bot on Telegram. in your terminal.
Seeing TELEGRAM_BOT_TOKEN not found?
This means your .env file is either missing or does not contain the TELEGRAM_BOT_TOKEN variable. Check that your .env file is in the same folder as bot.py and that it contains your token from BotFather.
Help me fix my .env file so my bot can find the token.
- Open Telegram on your phone.
- Search for the bot name you created with BotFather.
- Tap Start or send /start to your bot.
You should see the welcome message listing what your bot can do.
- Send a test message like Hello! to your bot.
You should see a "Thinking..." message appear, then get replaced with your agent's response after a few seconds.
Bot not responding to your messages?
Check your terminal for error messages. The most common issue is Ollama not running in the background. Open Ollama before retrying.
If you see a connection error, make sure your Gemma 4 model is still available by running ollama list in a separate terminal.
Help me debug why my Telegram bot isn't responding.
Your agent is now live on Telegram. In the next step, you will put it through a full test suite to make sure each tool works correctly from end to end.
Test Your AI Agent End-to-End
Your bot is live on Telegram and responding to messages. Now it's time to verify that the full agentic loop works correctly for every tool.
Each message you send triggers the complete cycle: Gemma 4 receives your question, decides whether a tool is needed, executes the tool if so, feeds the result back into the conversation, and returns a final human-readable response through Telegram.
In this step, get ready to:
- Test each of the three tools (datetime, calculator, system info) through Telegram.
- Verify the agent responds to conversational messages without using tools.
- Confirm multi-step reasoning works with questions that require multiple tool calls.
Send messages to test each tool
Your agent has three tools available. You need to confirm that Gemma 4 correctly identifies when each tool is needed and returns meaningful results.
- Switch to your Telegram conversation with your bot.
- Send the message What time is it right now? to test the get_current_datetime tool.
- Wait for the response. You should see the current date and time in a readable format.
- Send the message What is 247 multiplied by 38? to test the calculate tool.
- The agent should respond with 9386 (or a sentence containing that number).
- Send the message What operating system am I running? to test the get_system_info tool.
- You should see details about your OS, machine type, Python version, and processor.
What is happening behind the scenes?
When you ask "What time is it?", Gemma 4 reads the message and the available tools list. It recognizes that get_current_datetime is relevant and returns a tool_calls response instead of a text answer.
Your run_agent function catches this, executes the tool, appends the result back into the messages list, and calls Gemma 4 again. The model then uses the tool output to compose a final natural-language response.
Not getting a response?
Make sure Ollama is still running in the background. If it stopped, open a new terminal and run ollama serve to restart it.
If you see a timeout error, Gemma 4 may be loading into memory for the first time. Wait 30 seconds and try again.
Check the terminal where your bot is running for any error messages. Help me debug my Telegram bot not responding
Test a conversational response
A well-built agent knows when NOT to use a tool. If you send a simple greeting, the model should respond directly without calling any tools.
- Send the message Hello! How are you? to your bot on Telegram.
- The agent should reply with a friendly conversational response. No tool is needed for a greeting.
How does the agent decide?
Gemma 4 sees both the user message and the list of available tools. When it determines that none of the tools are relevant, it skips tool calling entirely and returns a plain text response.
In your code, this is the if not response.message.tool_calls: check. When there are no tool calls, the loop exits immediately and returns the model's text content.
You should see a natural conversational reply with no delay for tool execution.
Verify multi-step reasoning
The real power of the agentic loop is that it can iterate multiple times. When a question requires more than one piece of information, the model calls multiple tools across separate loop iterations.
- Send a message that requires multiple tools, like What time is it, and what is 1024 divided by 16? to your bot.
- The agent should respond with both the current time and the calculation result (64) in a single message.
What is multi-step tool calling?
When Gemma 4 sees a question that needs multiple tools, it may return multiple entries in tool_calls at once, or it may call one tool at a time across iterations. Either way, your while iteration < max_iterations: loop handles it.
Each iteration executes the requested tools, appends their results, and sends everything back to the model. The loop only exits when the model returns a response with no more tool calls.
- Try one more test. Send Tell me about my system and what time it is there. to trigger both get_system_info and get_current_datetime.
- You should see a combined response mentioning your OS details and the current time.
Agent only answering part of the question?
Sometimes the model answers one part and skips the other. Try rephrasing your message to be more explicit. For example, First tell me the time, then calculate 1024 / 16 gives clearer instructions.
If the model consistently ignores tool calls, make sure your SYSTEM_PROMPT in agent.py matches the one from the previous step exactly.
Help me figure out why my agent isn't using multiple tools
Your AI agent is fully operational. It receives messages through Telegram, thinks about which tools to use, executes them, and delivers polished responses back to your phone. This is the exact agentic loop pattern that powers production AI agents. Next up, the Secret Mission challenges you to give your agent a brand new capability.
Secret mission
Add Web Search
Your agent can check the time, do math, and get system info. But what if you could ask it about current events, look up facts, or research topics? In this secret mission, you'll add a real web search tool so your agent can find information on the internet and bring it back to you.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Project folder on your Desktop (agent.py, bot.py, .env, virtual environment).
- Python packages installed in the virtual environment (ollama, python-telegram-bot, python-dotenv, duckduckgo-search).
- Telegram bot created via BotFather.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep messaging your bot.
- Your bot will continue to work whenever you run bot.py from inside your virtual environment.
- Your Telegram bot token remains active in .env.
- Ollama continues to serve Gemma 4 locally whenever it is running.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files stay on disk so you can pick up where you left off.
- Stop the bot by pressing Ctrl+C in the terminal where bot.py is running.
- Stop Ollama if you want to free memory. Close the Ollama app or stop the background process.
- Deactivate your virtual environment by running:
deactivate
When you're ready to resume, activate the virtual environment again and run bot.py.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Stop the bot by pressing Ctrl+C in the terminal if it is still running.
- Delete your project folder by running:
macOS
cd ~/Desktop
rm -rf telegram-ai-agent
Windows
cd ~/Desktop
rmdir /s /q telegram-ai-agent
- Delete your Telegram bot. Open @BotFather in Telegram, send /deletebot, select your bot from the list, and confirm the deletion.
- Revoke your bot token for safety. If you prefer to revoke the token without deleting the bot, open @BotFather, send /revoke, and select your bot.
Nice Work!
Nice Work!
You just built the same kind of AI agent that powers tools like OpenClaw and Claude Code. From scratch, running entirely on your own machine.
You've learned how to:
- Built a working AI agent with the agentic loop pattern (think, act, observe) that autonomously decides when to use tools.
- Implemented tool calling with Gemma 4 through Ollama's native function calling, passing Python functions directly as tools.
- Connected your agent to Telegram so you can chat with it from your phone using the Telegram Bot API and async Python.
- Added web search to your agent so it can look up real information from the internet on demand.
Ready to quiz yourself?