Build an AI Task Agent with Groq
Build a CLI AI agent that manages tasks and sends Telegram notifications.
Introduction
30 Second Summary
Everyone has a to-do list that gets ignored. What if you could just talk to an AI assistant in plain English and have it actually manage your tasks for you?
In this project, you will build a personal AI agent that understands natural language commands and manages your daily tasks. You will use Groq's free API for the AI brain, SQLite for local data storage, and Telegram for mobile notifications.
What You'll Build
A CLI-based task assistant powered by Groq's Llama 3.1 model that manages tasks through natural conversation, persists data in a local SQLite database, and sends briefing notifications to your phone via a Telegram bot.
By the end of this project, you'll have:
- A working tool-calling agent loop that lets an LLM decide which function to run based on your natural language input.
- A SQLite database that stores your tasks locally with full create, read, update, and query operations.
- A Telegram bot that sends you notifications when you request a daily briefing of your tasks.
- Secret Mission: Add smart task prioritization that uses the LLM to analyze and rank your tasks by deadlines and importance.
Are there any prerequisites?
This project assumes intermediate Python experience. You will also need a free Groq account (no credit card required) and a Telegram account for notifications.
Before We Start
Before We Start...
Set Up Your Environment and API Keys
Your AI task agent needs three things to work: a Python project with the right packages, a connection to Groq's LLM for its brain, and a Telegram bot for sending you notifications.
In this step, you will create your project folder, install packages, and grab your API keys from both services. By the end, you will have a verified connection to Groq and a Telegram bot ready to message you.
In this step, get ready to:
- Install Python packages and set up a virtual environment.
- Create a Groq account and generate an API key.
- Create a Telegram bot and retrieve your chat ID.
Install Python packages
Your agent depends on a few external Python packages. Before installing them, you need a project folder and a virtual environment to keep everything isolated from your system Python.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Antigravity and press Enter to open Google Antigravity IDE.
- Open the integrated 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 task-agent
cd task-agent
- Confirm you are in the right folder by running:
pwd
You should see a path ending in Desktop/task-agent. The folder is empty for now.
Folder not created?
Make sure you ran cd ~/Desktop first. If your Desktop path is different, run pwd to check where you are.
Help me figure out why my folder wasn't created on the Desktop.
A virtual environment keeps your project's packages separate from your system Python. This prevents version conflicts between projects.
- Create a virtual environment by running this command:
python3 -m venv venv
- Activate the virtual environment by running the command for your operating system:
macOS
source venv/bin/activate
This command also works on Linux.
Windows
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try the activation command again.
On macOS/Linux, double check you typed source before the path. Without it, the activation script will not run.
Help me activate my Python virtual environment.
- Install the three packages your project needs by running:
pip install groq python-dotenv requests
What are these packages?
- groq is the official Python SDK for the Groq API. It gives your agent access to the Llama 3.1 8B model.
- python-dotenv loads environment variables from a .env file so you never hard-code API keys in your source code.
- requests is a lightweight HTTP library. You will use it to send Telegram notifications with a single POST request.
You should see Successfully installed followed by package names in your terminal output.
Seeing a pip error?
Make sure your virtual environment is active. You should see (venv) in your prompt. If not, re-run the activation command above.
The groq SDK requires Python 3.10 or higher. Run python3 --version to check your version.
Help me fix my pip install error.
Get your Groq API key
Groq provides free access to large language models with extremely fast inference. Your agent will send natural language messages to Groq's API, and Groq returns the LLM's response including any tool calls.
Why Groq?
Groq's free tier gives you 14,400 requests per day with the llama-3.1-8b-instant model. No credit card is required.
Running models locally on an 8GB machine would consume most of your RAM. Groq handles the heavy lifting in the cloud, so your agent stays fast and your machine stays free.
- Open your browser and go to console.groq.com.
- Click Sign Up and create a free account using your email or Google sign-in.
- After signing in, navigate to API Keys in the left sidebar, or go directly to console.groq.com/keys.
- Click Create API Key.
- Enter task-agent as the key name.
- Click Submit.
- Copy the API key immediately. Groq only shows it once.
Now you need a safe place to store this key. A .env file keeps secrets out of your source code.
- Open the task-agent folder in Antigravity IDE by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the task-agent folder, then click Open.
- Create a new file by clicking the new file icon in the Explorer sidebar.
- Name the file .env.
- Paste the following into .env, replacing your-groq-api-key-here with the API key you just copied:
GROQ_API_KEY=your-groq-api-key-here
TELEGRAM_BOT_TOKEN=your-telegram-bot-token-here
TELEGRAM_CHAT_ID=your-telegram-chat-id-here
What is this file doing?
The .env file stores your API keys as environment variables. Your Python code reads these values at runtime instead of having them hard-coded in your source files.
You will fill in the Telegram values in the next substep. Leave them as placeholders for now.
- Save .env.
Next, your project needs a configuration file that loads these environment variables into Python. Every file in your project can then import values from one central place.
- Create a new file in the Explorer sidebar called config.py.
- Paste the following code into config.py:
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Groq API settings
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
GROQ_MODEL = "llama-3.1-8b-instant"
# Telegram settings
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID")
# Database path
DB_PATH = "tasks.db"
What does this code do?
- load_dotenv() reads the .env file and loads its values into your system's environment variables.
- os.environ.get() pulls each value out by name. If a value is missing, it returns None instead of crashing.
- GROQ_MODEL is set to llama-3.1-8b-instant, the model ID for Groq's free-tier Llama 3.1 8B model.
- DB_PATH defines where your SQLite database file will be stored.
- Save config.py.
Time to verify that your Groq connection actually works. You will create a quick test script that sends a simple message to the LLM and prints the response.
- Create a new file called test_groq.py in the Explorer sidebar.
- Paste the following code into test_groq.py:
from groq import Groq
from config import GROQ_API_KEY, GROQ_MODEL
# Create a Groq client with your API key
client = Groq(api_key=GROQ_API_KEY)
# Send a simple test message to the LLM
response = client.chat.completions.create(
model=GROQ_MODEL,
messages=[{"role": "user", "content": "Say hello in one sentence."}]
)
# Print the LLM's response
print(response.choices[0].message.content)
What does this test do?
This script imports your API key and model name from config.py, creates a Groq client, and sends a one-line prompt. If everything is configured correctly, you will see a short greeting printed in your terminal.
- Save test_groq.py.
- Run the test script in your integrated terminal by running:
python3 test_groq.py
You should see a short greeting like "Hello! How can I assist you today?" printed in your terminal. The exact wording will vary each time.
Seeing an authentication error?
Open .env and check that your GROQ_API_KEY value has no extra spaces or quotes around it. The line should look like GROQ_API_KEY=gsk_abc123... with no quotation marks.
If you see ModuleNotFoundError, make sure your virtual environment is active. You should see (venv) in your terminal prompt.
Help me fix my Groq API connection error.
Create your Telegram bot
Your agent sends Telegram notifications for features like the daily briefing. To make that work, you need a Telegram bot (your agent's messenger) and your personal chat ID (the address it sends messages to).
Why Telegram for notifications?
The Telegram Bot API is completely free, works on both mobile and desktop, and takes just a few minutes to set up. Your agent only needs to send one-way messages, so a simple HTTP POST request is all it takes.
- Open the Telegram app on your phone or desktop.
- Search for @BotFather in the Telegram search bar and open the chat.
- Send the message /newbot to start creating your bot.
- BotFather will ask for a display name. Type Task Agent and send it.
- BotFather will then ask for a username. This must end in bot and be globally unique. Try something like mytaskagent_bot or add numbers to make it unique.
- BotFather will reply with a message containing your bot token. It looks like 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11. Copy this token.
Now you need your personal chat ID. This tells Telegram which conversation to deliver messages to.
- In Telegram, search for the bot username you just created and open the chat.
- Send any message to your bot. Something like hello is fine.
- Open your browser and visit this URL, replacing YOUR_BOT_TOKEN with the token BotFather gave you:
https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates
What should I see?
Your browser will show a JSON response. Look for the "chat":{"id": field inside the "message" object. The number after "id": is your chat ID. It will be a number like 987654321.
- Copy your chat ID number from the JSON response.
Seeing an empty result array?
If the JSON response shows "result":[], you need to send a message to your bot first. Go back to Telegram, find your bot, send it a message, then refresh the browser page.
Also check that you included the word bot before your token in the URL. The URL format is bot followed immediately by the token, with no space.
Help me find my Telegram chat ID.
Now update your .env file with the Telegram values.
- In .env, find the line that says:
TELEGRAM_BOT_TOKEN=your-telegram-bot-token-here
- Replace your-telegram-bot-token-here with the bot token BotFather gave you.
- Find the line that says:
TELEGRAM_CHAT_ID=your-telegram-chat-id-here
- Replace your-telegram-chat-id-here with the chat ID number you copied from the JSON response.
- Save .env.
✔️ Awesome, I've got everything!
Double check you have saved both .env and config.py.
ⓧ I'd like to double check the full code
Here is the full .env file. Your actual values will replace the placeholders:
GROQ_API_KEY=your-groq-api-key-here
TELEGRAM_BOT_TOKEN=your-telegram-bot-token-here
TELEGRAM_CHAT_ID=your-telegram-chat-id-here
Here is the full config.py:
import os
from dotenv import load_dotenv
load_dotenv()
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
GROQ_MODEL = "llama-3.1-8b-instant"
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID")
DB_PATH = "tasks.db"
Your environment is fully set up. You have a virtual environment with all your packages, a working Groq connection, and a Telegram bot waiting for messages. Next up, you will build the SQLite database that stores your tasks.
Build the SQLite Task Database
Your environment is ready to go. You have Python packages installed, API keys stored in .env, and a Telegram bot waiting for messages.
Every task your agent creates needs to live somewhere persistent. In this step, you'll build a SQLite database layer that stores tasks locally on your machine and gives your agent the functions it needs to manage them.
In this step, get ready to:
- Create a SQLite database with a tasks table to store your to-do items.
- Write CRUD functions to add, list, complete, and summarize tasks.
- Test the full database layer with sample data.
Create the database schema
Your agent needs a structured place to store tasks. SQLite is a lightweight database built into Python's standard library, so there's nothing extra to install.
- Create a new file called database.py in your project folder.
- Add the imports and the init_db() function by copying the following code into the file:
import sqlite3
from datetime import datetime, date
from config import DB_PATH
def init_db():
# Connect to the SQLite database file defined in config.py
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create the tasks table if it doesn't already exist
cursor.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT DEFAULT '',
due_date TEXT,
priority TEXT DEFAULT 'medium',
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
What does this code do?
The init_db() function connects to a SQLite database file and creates a tasks table if one doesn't already exist.
Each task row stores an auto-incrementing id, a required title, an optional description, a due_date, a priority level, a status that defaults to pending, and a created_at timestamp.
You won't see a change yet. The next chunk adds the functions that read and write to this table.
Implement CRUD functions
Your agent needs four operations: adding a task, listing tasks, completing a task, and generating a daily briefing. Each function opens its own database connection, runs a query, and returns a Python dictionary the agent can use.
- In database.py, add the add_task() and list_tasks() functions below init_db() by copying this code:
def add_task(title, description="", due_date=None, priority="medium"):
# Insert a new task and return its ID
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"INSERT INTO tasks (title, description, due_date, priority) VALUES (?, ?, ?, ?)",
(title, description, due_date, priority),
)
conn.commit()
task_id = cursor.lastrowid
conn.close()
return {"id": task_id, "title": title, "status": "created"}
def list_tasks(status="pending"):
# Fetch all tasks matching a given status, ordered by due date
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(
"SELECT id, title, description, due_date, priority, status FROM tasks WHERE status = ? ORDER BY due_date ASC",
(status,),
)
rows = cursor.fetchall()
conn.close()
tasks = [dict(row) for row in rows]
return tasks
What does this code do?
- add_task() inserts a new row into the tasks table using parameterized queries (the ? placeholders) to prevent SQL injection. It returns the new task's ID and title.
- list_tasks() retrieves all tasks matching a given status. Setting conn.row_factory = sqlite3.Row lets you convert each row into a Python dictionary instead of a plain tuple.
You won't see a change yet. The next chunk adds the remaining two functions.
- Below list_tasks(), add the complete_task() and get_daily_briefing() functions, then add the init_db() call at the very bottom of the file:
def complete_task(task_id):
# Mark a task as completed by its ID
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"UPDATE tasks SET status = 'completed' WHERE id = ?", (task_id,)
)
affected = cursor.rowcount
conn.commit()
conn.close()
if affected == 0:
return {"error": f"No task found with id {task_id}"}
return {"id": task_id, "status": "completed"}
def get_daily_briefing():
# Summarize pending tasks and today's completed count
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
today = date.today().isoformat()
# Get all pending tasks ordered by due date
cursor.execute(
"SELECT id, title, due_date, priority FROM tasks WHERE status = 'pending' ORDER BY due_date ASC"
)
pending = [dict(row) for row in cursor.fetchall()]
# Count how many tasks were completed today
cursor.execute(
"SELECT COUNT(*) as count FROM tasks WHERE status = 'completed' AND date(created_at) = ?",
(today,),
)
completed_today = cursor.fetchone()["count"]
conn.close()
return {
"date": today,
"pending_tasks": pending,
"pending_count": len(pending),
"completed_today": completed_today,
}
# Auto-initialize the database when this module is imported
init_db()
What does this code do?
- complete_task() updates a task's status to completed. It checks cursor.rowcount to confirm a row was actually changed. If no task matches the ID, it returns an error dictionary.
- get_daily_briefing() fetches all pending tasks and counts how many were completed today. It returns a summary dictionary your agent will use to generate a human-readable briefing.
- The init_db() call at the bottom runs automatically whenever another file imports database.py. This ensures the tasks table exists before any function tries to use it.
- Save database.py.
Seeing a syntax error?
- Make sure the triple-quoted SQL string in init_db() has matching opening and closing """ marks.
- Check that each function definition starts with def and ends its first line with a colon.
- Verify that init_db() at the bottom of the file is NOT indented. It should be at the top level, outside any function.
Help me debug a syntax error in my database.py file.
✔️ Awesome, I've got everything!
Great. Double check you've saved database.py before moving on.
ⓧ I'd like to double check the full code
import sqlite3
from datetime import datetime, date
from config import DB_PATH
def init_db():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT DEFAULT '',
due_date TEXT,
priority TEXT DEFAULT 'medium',
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
def add_task(title, description="", due_date=None, priority="medium"):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"INSERT INTO tasks (title, description, due_date, priority) VALUES (?, ?, ?, ?)",
(title, description, due_date, priority),
)
conn.commit()
task_id = cursor.lastrowid
conn.close()
return {"id": task_id, "title": title, "status": "created"}
def list_tasks(status="pending"):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(
"SELECT id, title, description, due_date, priority, status FROM tasks WHERE status = ? ORDER BY due_date ASC",
(status,),
)
rows = cursor.fetchall()
conn.close()
tasks = [dict(row) for row in rows]
return tasks
def complete_task(task_id):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"UPDATE tasks SET status = 'completed' WHERE id = ?", (task_id,)
)
affected = cursor.rowcount
conn.commit()
conn.close()
if affected == 0:
return {"error": f"No task found with id {task_id}"}
return {"id": task_id, "status": "completed"}
def get_daily_briefing():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
today = date.today().isoformat()
cursor.execute(
"SELECT id, title, due_date, priority FROM tasks WHERE status = 'pending' ORDER BY due_date ASC"
)
pending = [dict(row) for row in cursor.fetchall()]
cursor.execute(
"SELECT COUNT(*) as count FROM tasks WHERE status = 'completed' AND date(created_at) = ?",
(today,),
)
completed_today = cursor.fetchone()["count"]
conn.close()
return {
"date": today,
"pending_tasks": pending,
"pending_count": len(pending),
"completed_today": completed_today,
}
init_db()
Test the database
Before wiring the database into your agent, you should confirm every function works on its own. A quick test script will add sample tasks, list them, complete one, and pull a daily briefing.
- Test all four database functions by running this command in your terminal:
python3 -c "
from database import add_task, list_tasks, complete_task, get_daily_briefing
print(add_task('Review notes', 'Go over chapter 3', '2026-05-04', 'high'))
print(add_task('Buy groceries'))
print(list_tasks())
print(complete_task(1))
print(get_daily_briefing())
"
What should I see?
Your terminal should print five lines of output, one per function call:
- Two dictionaries with 'status': 'created' confirming both tasks were added.
- A list of two task dictionaries showing their titles, priorities, and pending status.
- A dictionary with 'status': 'completed' for task 1.
- A briefing dictionary with today's date, a pending_count of 1, and a completed_today of 1.
- Confirm that a tasks.db file appeared in your project folder by running:
ls
You should see tasks.db listed alongside config.py, database.py, and your .env file.
Seeing an import error or no output?
- If you see ModuleNotFoundError: No module named 'config', make sure you are running the command from inside your project folder where config.py lives.
- If you see ModuleNotFoundError: No module named 'dotenv', make sure your virtual environment is activated. You should see your environment name in parentheses at the start of your terminal prompt.
- If you run the test a second time, you may see duplicate tasks (IDs 3 and 4 instead of 1 and 2). That is expected because the previous tasks are still in the database.
Help me debug an error when testing my database.py file.
Why SQLite instead of a cloud database?
SQLite stores everything in a single file on your machine. There are no servers to run, no accounts to create, and no network calls to make.
Your task data never leaves your computer. That makes SQLite a great fit for a personal productivity tool where privacy and simplicity matter more than multi-user scale.
Your database layer is solid. Next up, you'll define the tool schemas that tell your AI agent exactly how to call these functions.
Define Your Agent Tools
Your database functions can add, list, complete, and summarize tasks. The next challenge is giving the LLM access to these functions so it can decide which one to call based on what the user types.
In this step, you'll define JSON tool schemas that describe each function to the Groq API. You'll also build a routing function that executes the right database operation when the LLM makes a tool call.
In this step, get ready to:
- Define JSON tool schemas for all four database functions.
- Build a tool execution router that connects LLM tool calls to your local functions.
- Test the tool execution with a simulated tool call.
Create tool schemas
When you send a message to Groq's API with tools enabled, the LLM reads JSON schemas to understand what tools are available. Each schema includes a name, a description that tells the LLM when to use it, and a parameters object defining what arguments it expects.
- Create a new file called tools.py in your project folder and add the following code:
import json
from database import add_task, list_tasks, complete_task, get_daily_briefing
# Define tool schemas that tell the LLM what functions are available
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "add_task",
"description": "Add a new task to the user's task list. Use this when the user wants to create, add, or schedule a new task.",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The title or name of the task",
},
"description": {
"type": "string",
"description": "Optional detailed description of the task",
},
"due_date": {
"type": "string",
"description": "Due date in YYYY-MM-DD format, or null if no deadline",
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Priority level of the task",
},
},
"required": ["title"],
},
},
},
{
"type": "function",
"function": {
"name": "list_tasks",
"description": "List the user's tasks. Use this when the user wants to see their tasks, to-do list, or what they need to do.",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["pending", "completed"],
"description": "Filter tasks by status. Defaults to pending.",
}
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "complete_task",
"description": "Mark a task as completed. Use this when the user says they finished or completed a task.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "integer",
"description": "The ID of the task to mark as completed",
}
},
"required": ["task_id"],
},
},
},
{
"type": "function",
"function": {
"name": "daily_briefing",
"description": "Get a summary of the user's day including pending tasks and progress. Use this when the user asks for a briefing, summary, or overview of their day.",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
]
What does this code do?
Each entry in TOOL_SCHEMAS follows Groq's OpenAI-compatible tool format. The key parts are:
- name identifies which function to call.
- description tells the LLM when to pick this tool. A clear description means the LLM makes better decisions.
- parameters defines what arguments the tool accepts, their types, and which ones are required.
- The add_task tool requires a title but leaves description, due date, and priority optional. The daily_briefing tool takes no parameters at all.
- Save tools.py.
You won't see a visible change yet. The next section adds the execution logic that uses these schemas.
Seeing an import error?
- Make sure database.py is in the same folder as tools.py. Python looks for modules in the current directory.
- Check that your function names in the import line match exactly: add_task, list_tasks, complete_task, get_daily_briefing.
- Help me fix an import error in tools.py
Implement tool execution
The schemas tell the LLM what tools exist. Now you need the code that actually runs the right function when the LLM picks a tool.
- In tools.py, add the following code below the TOOL_SCHEMAS list:
# Map tool names to the actual Python functions from database.py
AVAILABLE_FUNCTIONS = {
"add_task": add_task,
"list_tasks": list_tasks,
"complete_task": complete_task,
"daily_briefing": get_daily_briefing,
}
def execute_tool(tool_call):
# Read the function name and arguments from the LLM's tool call
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
# Check if the tool name is valid
if function_name not in AVAILABLE_FUNCTIONS:
return json.dumps({"error": f"Unknown function: {function_name}"})
# Call the matching database function with the parsed arguments
function_to_call = AVAILABLE_FUNCTIONS[function_name]
result = function_to_call(**function_args)
return json.dumps(result, default=str)
What does this code do?
- AVAILABLE_FUNCTIONS is a dictionary that maps string names (like "add_task") to your actual Python functions from database.py. This is the bridge between what the LLM returns and what your code can run.
- execute_tool() reads the tool call object that Groq returns. It extracts the function name and parses the JSON arguments string into a Python dictionary.
- The **function_args syntax unpacks the dictionary into keyword arguments. So {"title": "Buy milk"} becomes add_task(title="Buy milk").
- The result is converted back to a JSON string so it can be sent to the LLM as context for its final response.
- Save tools.py.
No visible output yet. You'll test everything in the next section.
Syntax error when saving?
- Make sure the AVAILABLE_FUNCTIONS dictionary and execute_tool() function are placed after the closing ] of TOOL_SCHEMAS, not inside it.
- Check that every opening brace { in your schemas has a matching closing brace }. Mismatched brackets are the most common issue with nested JSON-like structures.
- Help me fix a syntax error in tools.py
✔️ Awesome, I've got everything!
Great. Double check you've saved tools.py before moving on.
ⓧ I'd like to double check the full code
import json
from database import add_task, list_tasks, complete_task, get_daily_briefing
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "add_task",
"description": "Add a new task to the user's task list. Use this when the user wants to create, add, or schedule a new task.",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The title or name of the task",
},
"description": {
"type": "string",
"description": "Optional detailed description of the task",
},
"due_date": {
"type": "string",
"description": "Due date in YYYY-MM-DD format, or null if no deadline",
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Priority level of the task",
},
},
"required": ["title"],
},
},
},
{
"type": "function",
"function": {
"name": "list_tasks",
"description": "List the user's tasks. Use this when the user wants to see their tasks, to-do list, or what they need to do.",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["pending", "completed"],
"description": "Filter tasks by status. Defaults to pending.",
}
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "complete_task",
"description": "Mark a task as completed. Use this when the user says they finished or completed a task.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "integer",
"description": "The ID of the task to mark as completed",
}
},
"required": ["task_id"],
},
},
},
{
"type": "function",
"function": {
"name": "daily_briefing",
"description": "Get a summary of the user's day including pending tasks and progress. Use this when the user asks for a briefing, summary, or overview of their day.",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
]
AVAILABLE_FUNCTIONS = {
"add_task": add_task,
"list_tasks": list_tasks,
"complete_task": complete_task,
"daily_briefing": get_daily_briefing,
}
def execute_tool(tool_call):
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
if function_name not in AVAILABLE_FUNCTIONS:
return json.dumps({"error": f"Unknown function: {function_name}"})
function_to_call = AVAILABLE_FUNCTIONS[function_name]
result = function_to_call(**function_args)
return json.dumps(result, default=str)
Test tool execution manually
Before wiring this into the agent loop, you should verify that execute_tool() correctly routes a tool call to the right database function. You can simulate the tool call object that Groq returns using Python's built-in SimpleNamespace.
- Open the Python interactive shell by running:
python3
- Simulate a Groq tool call and test the execution by pasting this code:
from types import SimpleNamespace
from tools import execute_tool
# Create a fake tool call that mimics what Groq returns
mock_call = SimpleNamespace(
function=SimpleNamespace(
name="add_task",
arguments='{"title": "Test task from tool call", "priority": "high"}'
)
)
# Run the tool execution and print the result
result = execute_tool(mock_call)
print(result)
What does this test do?
When Groq's API decides to use a tool, it returns an object with a function.name and function.arguments field. SimpleNamespace lets you create a quick mock object with those same attributes so you can test without making an actual API call.
You should see output like this:
{"id": 3, "title": "Test task from tool call", "status": "created"}
Your task ID might be different
The id value depends on how many tasks you added during testing in the previous step. The important thing is that you see a JSON string with "status": "created".
- Exit the Python shell by running:
exit()
Seeing a ModuleNotFoundError or AttributeError?
- Make sure you're running python3 from the same folder where tools.py and database.py live.
- If you see AttributeError: 'SimpleNamespace' object has no attribute 'function', double check that both SimpleNamespace calls are nested correctly with the inner one assigned to function.
- Help me debug my tool execution test
Why this pattern matters
The pattern you just built is the same one that powers every major agent framework. The LLM never runs code directly. It reads JSON schemas, picks a tool, and returns structured arguments. Your code does the actual execution and feeds results back.
Frameworks like CrewAI and LangGraph abstract this loop away. By building it yourself, you understand exactly what happens under the hood.
Your tools are defined and tested. Next up, you'll build the agent loop that ties everything together. The LLM will read your tool schemas, decide which function to call based on the user's message, and you'll see your agent respond conversationally for the first time.
Build the Agent Loop
Your database stores tasks and your tool definitions tell the LLM what functions are available. Now you need the brain that ties everything together.
In this step, you will build the agent loop. This is the core pattern behind every AI agent framework: send a message to the LLM along with your tool definitions, let the LLM decide which tool to call, execute it locally, feed the result back, and get a conversational response.
In this step, get ready to:
- Write a system prompt that shapes your agent's personality and behavior.
- Implement the tool-calling agent loop using the Groq API.
- Add an interactive CLI so you can chat with your agent in the terminal.
Write the system prompt
The system prompt is the instruction manual your LLM reads before every conversation. It defines the agent's role, lists available tools, and sets rules for how to respond.
Your agent file will import from a notifier.py module that you will build fully in the next step. To prevent an import error, you need to create a placeholder first.
- Create a new file called notifier.py in your project folder and add this placeholder code:
def send_telegram_message(text):
# Placeholder - full Telegram integration comes in the next step
print(f"[Notifier] Telegram not configured yet.")
return False
Why a placeholder?
This stub accepts a text message and returns False without sending anything. Your agent imports this function, so the file needs to exist. You will replace it with full Telegram support in the next step.
- Save notifier.py.
Now it is time to create the main agent file. This is where the system prompt, the Groq client, and your tool-calling loop all come together.
- Create a new file called task_agent.py in your project folder and add the following code:
import json
from groq import Groq
from config import GROQ_API_KEY, GROQ_MODEL
from tools import TOOL_SCHEMAS, execute_tool
from notifier import send_telegram_message
# Initialize the Groq client with your API key
client = Groq(api_key=GROQ_API_KEY)
# System prompt that defines the agent's personality and rules
SYSTEM_PROMPT = """You are a personal task management assistant. You help the user manage their daily tasks, schedule, and productivity.
You have access to these tools:
- add_task: Create new tasks with optional due dates and priorities
- list_tasks: Show pending or completed tasks
- complete_task: Mark tasks as done
- daily_briefing: Get a summary of the day's tasks and progress
Rules:
- Always use the appropriate tool when the user wants to manage tasks
- Be conversational and encouraging
- When listing tasks, format them clearly with IDs so the user can reference them
- For due dates, convert natural language like "tomorrow" to YYYY-MM-DD format
- If unsure about a date, ask the user to clarify
- Keep responses concise but helpful"""
# Safety limit to prevent infinite tool-calling loops
MAX_TOOL_ROUNDS = 5
What does this code do?
- The imports pull in the Groq client, your config values, tool schemas, the tool executor, and the notification function.
- SYSTEM_PROMPT is a multi-line string the LLM reads before every interaction. It lists the available tools and sets behavioral rules like converting natural language dates to YYYY-MM-DD format.
- MAX_TOOL_ROUNDS caps how many times the LLM can call tools in a single turn. This prevents infinite loops if the model keeps requesting tool calls.
What makes a good system prompt?
The system prompt explicitly names each tool and describes when to use it. This helps the LLM make better decisions about which tool to call.
The rules section handles edge cases (like ambiguous dates) so the agent asks for clarification instead of guessing. The better your system prompt, the fewer mistakes your agent makes.
- Save task_agent.py.
You won't see any output yet. The next chunk adds the function that makes the agent think and act.
Implement the agent loop
This is the heart of your AI agent. The loop follows a pattern called tool calling: send a message to the LLM, check if it wants to call a tool, execute the tool locally, feed the result back, and repeat until the LLM is ready to respond conversationally.
- In task_agent.py, add the following function below MAX_TOOL_ROUNDS = 5:
def run_agent(user_message, conversation_history):
# Add the user's message to the conversation
conversation_history.append({"role": "user", "content": user_message})
for _ in range(MAX_TOOL_ROUNDS):
# Send the conversation to Groq with tool definitions
response = client.chat.completions.create(
model=GROQ_MODEL,
messages=conversation_history,
tools=TOOL_SCHEMAS,
tool_choice="auto",
)
# Extract the assistant's response
response_message = response.choices[0].message
conversation_history.append(response_message)
# If the LLM didn't request any tool calls, we're done
if not response_message.tool_calls:
break
# Execute each tool the LLM requested
for tool_call in response_message.tool_calls:
result = execute_tool(tool_call)
# Send a Telegram notification for daily briefings
if tool_call.function.name == "daily_briefing":
briefing_data = json.loads(result)
briefing_text = f"Daily Briefing - {briefing_data['date']}\n"
briefing_text += f"Pending: {briefing_data['pending_count']} tasks\n"
briefing_text += f"Completed today: {briefing_data['completed_today']}"
send_telegram_message(briefing_text)
# Feed the tool result back to the LLM
conversation_history.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call.function.name,
"content": result,
}
)
# Return the LLM's final conversational response
return response_message.content
What does this code do?
- The function appends the user's message to the conversation history, then enters a loop capped at 5 rounds.
- Each round calls client.chat.completions.create() with tool_choice="auto", letting the LLM decide whether to call a tool or respond directly.
- If the response contains tool_calls, the function executes each one locally using execute_tool() from your tools.py.
- For the daily_briefing tool specifically, it formats a summary and calls send_telegram_message(). Right now this prints a placeholder message. The real Telegram delivery comes in the next step.
- Each tool result is appended back as a "role": "tool" message so the LLM knows what happened. Once the LLM stops requesting tools, the loop breaks and returns the conversational response.
Why a loop instead of a single call?
Sometimes the LLM needs multiple tool calls to answer one question. For example, if you say "complete task 3 and show me what's left," the LLM calls complete_task first, then list_tasks in a second round.
The loop lets the agent handle multi-step reasoning naturally. The MAX_TOOL_ROUNDS limit is a safety net so a misbehaving model cannot loop forever.
- Save task_agent.py.
You won't see a change yet. The next chunk adds the CLI so you can actually talk to your agent.
Add the interactive CLI
The agent loop is ready, but you have no way to send it messages yet. An interactive CLI lets you type natural language commands in your terminal and see the agent respond in real time.
- In task_agent.py, add the following code below the run_agent() function:
def main():
print("Task Agent ready. Type your message (or 'quit' to exit).\n")
# Start with the system prompt as the first message in history
conversation_history = [{"role": "system", "content": SYSTEM_PROMPT}]
while True:
try:
user_input = input("You: ").strip()
# Skip empty input
if not user_input:
continue
# Exit on quit or exit
if user_input.lower() in ("quit", "exit"):
print("Goodbye!")
break
# Run the agent and print the response
response = run_agent(user_input, conversation_history)
print(f"\nAgent: {response}\n")
except KeyboardInterrupt:
print("\nGoodbye!")
break
if __name__ == "__main__":
main()
What does this code do?
- main() initializes the conversation history with the system prompt, then enters an infinite input loop.
- It reads your input, skips blank lines, and exits gracefully on quit, exit, or Ctrl+C.
- Each message passes through run_agent(), which handles all the tool-calling logic and returns the final text response.
- The if __name__ == "__main__" guard ensures main() only runs when you execute the file directly, not when another module imports from it.
- Save task_agent.py.
- Run the agent by running this command:
python task_agent.py
You should see Task Agent ready. Type your message (or 'quit' to exit). printed in your terminal.
- Type add a task to buy groceries tomorrow and press Enter.
The agent should respond with a confirmation that the task was created, including a task ID and the due date in YYYY-MM-DD format.
- Try typing what's on my list? to see your tasks listed with IDs.
- Type quit to exit the agent.
Agent not responding or crashing?
- If you see ModuleNotFoundError: No module named 'notifier', make sure you created the placeholder notifier.py file in the same folder as task_agent.py.
- If you see AuthenticationError, double-check that your GROQ_API_KEY in .env is correct and that your virtual environment is activated.
- If the agent responds but does not call any tools, check that TOOL_SCHEMAS in tools.py has the "type": "function" wrapper around each tool definition.
- Still stuck? Help me debug my task agent. I'm running python task_agent.py and here is the error I see.
✔️ Awesome, I've got everything!
Great. Double check you have saved task_agent.py and notifier.py.
ⓧ I'd like to double check the full code
Here is the complete task_agent.py file. Make sure your code matches this exactly.
import json
from groq import Groq
from config import GROQ_API_KEY, GROQ_MODEL
from tools import TOOL_SCHEMAS, execute_tool
from notifier import send_telegram_message
client = Groq(api_key=GROQ_API_KEY)
SYSTEM_PROMPT = """You are a personal task management assistant. You help the user manage their daily tasks, schedule, and productivity.
You have access to these tools:
- add_task: Create new tasks with optional due dates and priorities
- list_tasks: Show pending or completed tasks
- complete_task: Mark tasks as done
- daily_briefing: Get a summary of the day's tasks and progress
Rules:
- Always use the appropriate tool when the user wants to manage tasks
- Be conversational and encouraging
- When listing tasks, format them clearly with IDs so the user can reference them
- For due dates, convert natural language like "tomorrow" to YYYY-MM-DD format
- If unsure about a date, ask the user to clarify
- Keep responses concise but helpful"""
MAX_TOOL_ROUNDS = 5
def run_agent(user_message, conversation_history):
conversation_history.append({"role": "user", "content": user_message})
for _ in range(MAX_TOOL_ROUNDS):
response = client.chat.completions.create(
model=GROQ_MODEL,
messages=conversation_history,
tools=TOOL_SCHEMAS,
tool_choice="auto",
)
response_message = response.choices[0].message
conversation_history.append(response_message)
if not response_message.tool_calls:
break
for tool_call in response_message.tool_calls:
result = execute_tool(tool_call)
if tool_call.function.name == "daily_briefing":
briefing_data = json.loads(result)
briefing_text = f"Daily Briefing - {briefing_data['date']}\n"
briefing_text += f"Pending: {briefing_data['pending_count']} tasks\n"
briefing_text += f"Completed today: {briefing_data['completed_today']}"
send_telegram_message(briefing_text)
conversation_history.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call.function.name,
"content": result,
}
)
return response_message.content
def main():
print("Task Agent ready. Type your message (or 'quit' to exit).\n")
conversation_history = [{"role": "system", "content": SYSTEM_PROMPT}]
while True:
try:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit"):
print("Goodbye!")
break
response = run_agent(user_input, conversation_history)
print(f"\nAgent: {response}\n")
except KeyboardInterrupt:
print("\nGoodbye!")
break
if __name__ == "__main__":
main()
Your agent can understand natural language, pick the right tool, execute it, and respond conversationally. Next up, you will replace that placeholder notifier with a real Telegram integration so the agent can ping your phone.
Add Telegram Notifications
Your agent can understand natural language, pick the right tool, and manage tasks in a local database. The last piece is connecting it to the outside world so it can reach you on your phone.
Telegram's Bot API lets you send messages with a single HTTP POST request. In this step, you'll build the notifier module, review how it plugs into the agent loop, and test the complete flow end-to-end.
In this step, get ready to:
- Build the notifier.py module for sending Telegram messages.
- Review how the agent loop triggers notifications on daily briefings.
- Test the full agent end-to-end with a Telegram notification on your phone.
Build the notifier module
The notifier module is a small wrapper around Telegram's REST API. It takes a text string, sends it to your Telegram chat via an HTTP POST request, and handles errors gracefully.
Your agent already imports from this module. Once the file exists, everything connects.
- Install the requests library (used for HTTP calls) by running this command in your terminal:
pip install requests
Seeing a permissions error?
Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, activate it again with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
Still stuck? Help me fix a pip install error in my virtual environment.
- Create a new file called notifier.py in your project folder, then copy the following code into it:
import requests
from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
def send_telegram_message(text):
# Skip if Telegram credentials aren't configured
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
print("[Notifier] Telegram not configured. Skipping notification.")
return False
# Build the Telegram Bot API endpoint URL
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
payload = {
"chat_id": TELEGRAM_CHAT_ID,
"text": text,
"parse_mode": "Markdown",
}
try:
# Send the message via HTTP POST
response = requests.post(url, json=payload, timeout=10)
if response.status_code == 200:
print("[Notifier] Telegram message sent.")
return True
else:
print(f"[Notifier] Telegram error: {response.status_code}")
return False
except requests.RequestException as e:
print(f"[Notifier] Failed to send: {e}")
return False
What does this code do?
- The function first checks if your bot token and chat ID are set. If either is missing, it prints a skip message and returns False instead of crashing.
- It builds the Telegram API URL using your bot token and sends a POST request with your chat ID and message text.
- parse_mode: "Markdown" tells Telegram to format bold and italic text in your messages.
- The try/except block catches network errors so a failed notification never crashes your agent.
Why use requests instead of python-telegram-bot?
The full python-telegram-bot library is async and designed for building interactive bots with command handlers. Your agent only needs to send one-way notifications.
A single requests.post() call is lighter, simpler, and teaches you the underlying API directly.
- Save notifier.py.
You won't see a change yet. The next substep walks through how the agent uses this module.
✔️ Awesome, I've got everything!
Double check you've saved notifier.py in the same folder as your other project files.
ⓧ I'd like to double check the full code
Here is the full notifier.py file. Make sure yours matches exactly.
import requests
from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
def send_telegram_message(text):
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
print("[Notifier] Telegram not configured. Skipping notification.")
return False
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
payload = {
"chat_id": TELEGRAM_CHAT_ID,
"text": text,
"parse_mode": "Markdown",
}
try:
response = requests.post(url, json=payload, timeout=10)
if response.status_code == 200:
print("[Notifier] Telegram message sent.")
return True
else:
print(f"[Notifier] Telegram error: {response.status_code}")
return False
except requests.RequestException as e:
print(f"[Notifier] Failed to send: {e}")
return False
Review the notification wiring in your agent
When you built task_agent.py in the previous step, the notification wiring was already included. Now that notifier.py exists, that import resolves and everything connects.
- Open task_agent.py and find this block inside the for tool_call in response_message.tool_calls: loop:
if tool_call.function.name == "daily_briefing":
briefing_data = json.loads(result)
briefing_text = f"Daily Briefing - {briefing_data['date']}\n"
briefing_text += f"Pending: {briefing_data['pending_count']} tasks\n"
briefing_text += f"Completed today: {briefing_data['completed_today']}"
send_telegram_message(briefing_text)
What does this wiring do?
Every time the agent loop processes a tool call, it checks if the tool was daily_briefing. If so, it parses the raw result from your database function into a readable string.
That formatted string then gets passed to send_telegram_message(). The notification happens as a side effect inside the tool-calling loop. The LLM never knows about Telegram. It just sees the tool result and composes a conversational reply as usual.
Why not let the LLM decide when to notify?
You could define a separate send_notification tool and let the LLM call it. The downside is that the LLM might forget, or send notifications for things you don't want.
Hard-coding the trigger ("if daily briefing, notify") gives you reliable, predictable behavior. You control the notification logic, not the model.
Test the full flow
Everything is wired up. Time to run the agent and confirm that a daily briefing triggers both a terminal response and a Telegram notification.
- Start the agent by running this command:
python task_agent.py
You should see Task Agent ready. Type your message (or 'quit' to exit). printed in your terminal.
- At the You: prompt, type this message and press **Enter**:
give me my daily briefing
Two things should happen. First, your terminal prints [Notifier] Telegram message sent. followed by the agent's conversational response summarizing your tasks.
- Check your Telegram app on your phone or desktop. You should see a message from your bot with the daily briefing summary.
- Type quit in the terminal to exit the agent.
Not receiving a Telegram notification?
- Check that you sent at least one message to your bot in Telegram before testing. The bot cannot message you until you initiate a conversation with it first.
- Verify your TELEGRAM_CHAT_ID in .env is correct. You can double check by visiting https://api.telegram.org/bot<your-token>/getUpdates in your browser and looking for the chat.id field.
- If you see [Notifier] Telegram not configured. Skipping notification., your bot token or chat ID is missing from .env. Open the file and confirm both values are filled in.
- If you see [Notifier] Telegram error: 401, your bot token is invalid. Go back to @BotFather in Telegram and regenerate the token.
Help me debug why my Telegram bot notification is not working.
Agent crashes on startup with ModuleNotFoundError?
If you see ModuleNotFoundError: No module named 'notifier', make sure notifier.py is saved in the same folder as task_agent.py. Python looks for modules in the current directory.
If you see ModuleNotFoundError: No module named 'requests', run pip install requests again with your virtual environment active.
Help me fix a ModuleNotFoundError when running my task agent.
Your AI task agent is fully operational. You can add tasks with natural language, list and complete them, get a daily briefing, and receive that briefing on your phone via Telegram. Next up, you'll take on the Secret Mission and give your agent the ability to prioritize your tasks intelligently.
Secret mission
Add Smart Task Prioritization
Your agent can add, list, and complete tasks. But when you have a dozen pending items, how do you know what to tackle first? In this mission, you'll add a smart prioritization tool that lets the LLM analyze your entire task list and recommend what to focus on next.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with free APIs, so there are no ongoing costs.
Resources you used:
- Project files: task_agent.py, tools.py, database.py, notifier.py, config.py, and .env.
- SQLite database file: tasks.db.
- Python virtual environment.
- Groq API key (free tier, no charges).
- Telegram bot created via BotFather.
- Python packages: groq, python-dotenv, and requests.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the agent with new tools.
- Your project files and tasks.db database stay on your machine.
- Your Groq API key and Telegram bot remain active and free to use.
- Your virtual environment stays ready for the next time you activate it.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and credentials stay intact for later.
- Stop the agent if it is still running by pressing Ctrl+C in your terminal.
- Deactivate your virtual environment by running:
deactivate
- Your project folder, database, and .env credentials all stay on disk for next time.
- To resume later, navigate to your project folder, activate the virtual environment, and run python task_agent.py.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your code, database, and local credentials.
- Stop the agent if it is still running by pressing Ctrl+C.
- Delete your entire project folder from the Desktop. This removes all Python files, the virtual environment, the .env file, and the tasks.db database.
- To revoke your Groq API key, go to console.groq.com, open API Keys, and delete the key you created.
- To delete your Telegram bot, open Telegram, message @BotFather, and send /deletebot. Select your bot from the list to confirm.
Mission accomplished
That's a Wrap!
Nice work! You just built a fully functional AI task agent from scratch using the Groq API, a local SQLite database, and Telegram notifications.
You've learned how to:
- Built a complete tool-calling agent loop that understands natural language and automatically picks the right tool to call. This is the same foundational pattern used by CrewAI, LangGraph, and every major agent framework.
- Created 4 local tools with proper JSON schemas and persisted your task data in a local SQLite database that never leaves your machine.
- Connected a Telegram bot to send yourself mobile notifications directly from your agent's daily briefing tool.
- Secret Mission: Added smart task prioritization that demonstrates the "tool provides data, LLM provides intelligence" pattern.
Ready to quiz yourself?