Build a Calendar Memory Agent

Build an AI agent that remembers your schedule using Grok and Gradio.

Introduction

30 Second Summary

We all forget important dates. Birthdays, project deadlines, dentist appointments. Imagine having a personal assistant that never forgets anything you tell it about your schedule.

In this project, you will build a calendar memory agent powered by the Grok API that remembers your events, deadlines, and dates across conversations. You will deploy it as a live chatbot that anyone can access through a public URL.

What You'll Build

A Python agent that uses Grok's function calling to autonomously save and retrieve calendar events from a persistent JSON memory file, wrapped in a Gradio chat interface and deployed to Hugging Face Spaces.

By the end of this project, you'll have:

  • A persistent memory system that stores calendar events, deadlines, and reminders in a local JSON file.
  • A function calling agent that autonomously decides when to save new events or look up existing ones using Grok's client-side tool system.
  • A live deployed chatbot on Hugging Face Spaces with a shareable public URL that anyone can visit.
  • Secret Mission: Add natural language date queries so the agent can answer questions like "What do I have next week?" or "Show me everything in July."

Are there any prerequisites?

You should have basic Python knowledge (variables, functions, imports) and familiarity with APIs and environment variables. No prior experience with Grok, Gradio, or Hugging Face is needed.

Set Up Your Environment

Before you can build a calendar memory agent, you need a few tools in place. Python runs the agent logic, the Groq SDK connects you to the Groq API for fast LLM inference, and Gradio gives the agent a web interface.

In this step you will install Python, grab the two key packages, and create a Groq account with an API key so the agent can talk to Llama 3.3.

In this step, get ready to:
  • Install and verify Python 3.10+.
  • Install the groq and Gradio packages.
  • Create a Groq account and set your API key as an environment variable.
Install and verify Python

Gradio requires Python 3.10 or higher. Let's check whether you already have a compatible version installed.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Python version by running this command:

macOS

python3 --version

Windows

python --version

You should see a version number printed in your terminal.

✔️ I see version 3.10 or higher

Your Python version is compatible. Move on to the next substep.

ⓧ I see an older version

You have Python installed, but the version is too old for this project. Download the latest Python 3 installer from python.org/downloads and run it.

  • On Windows, make sure to check Add Python to PATH before clicking Install Now.
  • On macOS, the installer from python.org installs alongside any existing version. After installing, run python3 --version again to confirm you see 3.10 or higher.

ⓧ Command not found

Python is not installed yet. Download the latest Python 3 installer from python.org/downloads and run it.

  • On Windows, check Add Python to PATH during installation.
  • On macOS, the official installer will set up python3 for you automatically.
  • After installing, close and reopen your terminal, then rerun the version command to confirm.
Create your project folder and install packages

You need a dedicated folder for the agent's files and two Python packages: the Groq SDK (for talking to Llama 3.3) and Gradio (for the chat UI).

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir calendar-agent
cd calendar-agent
  • Confirm the folder exists by running:
ls

You should see the calendar-agent folder listed (on macOS/Linux) or an empty directory listing since you just created it and moved inside.

  • Install the required packages by running:

macOS

pip3 install groq==1.2.0 gradio==6.16.0

Windows

pip install groq==1.2.0 gradio==6.16.0

What does this command do?

This installs two packages at exact versions. groq==1.2.0 is the official Python SDK for the Groq API, which gives you access to fast inference on open-source models like Llama 3.3.

gradio==6.16.0 provides a ready-made chat interface you can launch with a few lines of Python. Pinning exact versions ensures your code works the same way every time.

The install may take a minute as it downloads dependencies. When it finishes, you should see a success message in your terminal.

Seeing a permissions error or command not found?

  • If pip3 or pip is not found, try python3 -m pip install (macOS) or python -m pip install (Windows) instead.
  • If you get a permissions error on macOS, your system Python may be restricting global installs. Create a virtual environment first with python3 -m venv venv then activate it with source venv/bin/activate before rerunning the install command.

Still stuck? Help me troubleshoot my pip install

Set up your Groq account and API key

The agent uses the Groq API to run Llama 3.3, which understands user messages and decides when to save or retrieve events. To access this API, you need a Groq account and an API key.

  • Open console.groq.com in your browser.
  • Click Sign Up and create a free account using email, Google, or GitHub.
  • Once signed in, navigate to API Keys in the left sidebar (or go directly to console.groq.com/keys).
  • Click Create API Key.
  • Give it a descriptive name like calendar-agent and confirm.

Copy your key immediately

The key is shown only once. Groq does not store the secret value, so if you close the dialog without copying, you will need to create a new one.

Your key starts with gsk_ followed by a long alphanumeric string. Paste it somewhere safe for now.

Now set the key as an environment variable so the SDK can find it automatically.

  • In your terminal (still inside calendar-agent), set the environment variable by running:

macOS

export GROQ_API_KEY="your-api-key-here"

Windows

set GROQ_API_KEY=your-api-key-here
  • Replace your-api-key-here with the key you just copied (the one starting with gsk_).

Why an environment variable?

Storing secrets in environment variables keeps them out of your code files. The Groq SDK automatically reads the GROQ_API_KEY variable, so you never paste the key directly into Python.

This variable lasts only for the current terminal session. If you close the terminal, you will need to set it again.

🙋‍♀️ Getting an authentication error later?

  • Make sure there are no extra spaces around the key value.
  • On macOS, confirm the variable is set by running echo $GROQ_API_KEY. You should see your key printed.
  • On Windows, run echo %GROQ_API_KEY% to verify.

Still stuck? Help me set my GROQ_API_KEY environment variable

Your environment is ready to go. Next up, you will build the memory storage module that lets the agent remember calendar events across conversations.

Build the Memory Storage Module

Your environment is set up and ready to go. Now it's time to give your agent a brain.

In this step, you'll build a memory module that stores calendar events in a local JSON file. This is the foundation that lets your agent remember dates, deadlines, and meetings across conversations.

In this step, get ready to:
  • Create a memory storage file with JSON-based read and write functions.
  • Define a calendar event schema with unique IDs, dates, and categories.
  • Build search and retrieval functions that filter events by date range and keyword.
Create the memory file with storage helpers

Your agent needs a place to persist information between conversations. A local JSON file is perfect for this. It's human-readable, easy to debug, and teaches the same read-write-search pattern used in production memory systems.

  • Create a new file called memory.py in your project folder.
  • Add the following imports, file constant, and two helper functions by pasting this code:
import json
import os
import uuid
from datetime import datetime, timedelta


MEMORY_FILE = "calendar_memory.json"


def load_events():
    """Load all events from the JSON memory file."""
    if not os.path.exists(MEMORY_FILE):
        return []
    with open(MEMORY_FILE, "r") as f:
        return json.load(f)


def save_events(events):
    """Save all events to the JSON memory file."""
    with open(MEMORY_FILE, "w") as f:
        json.dump(events, f, indent=2)

What does this code do?

MEMORY_FILE sets the filename where all events will be stored. Every function in this module reads from or writes to this single file.

load_events() checks if the JSON file exists before reading it. If there's no file yet, it returns an empty list so the rest of your code doesn't crash.

save_events() writes the full event list back to disk with pretty-printing (indent=2) so you can inspect it easily.

You won't see a change yet. The next chunk builds on these helpers to create and store events.

Define the event schema

Every calendar event needs a consistent structure so the agent can store and retrieve information reliably. You'll define this schema inside an add_event function that builds a new event dictionary each time it's called.

  • In memory.py, add the following function below save_events:
def add_event(date, description, category="event"):
    """Add a new calendar event to memory."""
    events = load_events()
    new_event = {
        "id": str(uuid.uuid4()),
        "date": date,
        "description": description,
        "category": category,
        "created_at": datetime.now().isoformat()
    }
    events.append(new_event)
    save_events(events)
    return new_event

What does this code do?

add_event() creates a new event with five fields:

  • id: a unique identifier generated with uuid.uuid4() so each event can be individually referenced.
  • date: the event date in YYYY-MM-DD format for easy sorting and comparison.
  • description: a brief text description of the event.
  • category: one of deadline, meeting, reminder, or event (defaults to event).
  • created_at: an ISO timestamp recording when the event was added to memory.

The function loads existing events, appends the new one, saves everything back to disk, and returns the new event dictionary.

  • Save memory.py.
  • Test the function by running this command in your terminal:
python -c "from memory import add_event; print(add_event('2026-06-15', 'Dentist appointment', 'reminder'))"
  • You should see a dictionary printed with all five fields, including a generated id and created_at timestamp.
  • Confirm that a calendar_memory.json file now exists in your project folder by running:
ls
  • You should see memory.py and calendar_memory.json in the output.

Don't see the event printed?

  • Make sure you saved memory.py before running the command.
  • Check that your terminal is in the same folder as memory.py. Run ls to confirm the file is listed.
  • If you see a ModuleNotFoundError, your terminal's working directory doesn't match your project folder.

Why isn't my add_event function working?

Build the search and retrieval functions

Storing events is only half the picture. Your agent also needs to search through memory to answer questions like "What's on my schedule next week?" or "Do I have any meetings with keyword X?"

  • In memory.py, add the following function below add_event:
def search_events(start_date=None, end_date=None, keyword=None):
    """Search events by date range and/or keyword."""
    events = load_events()
    results = []

    for event in events:
        match = True

        if start_date:
            if event["date"] < start_date:
                match = False

        if end_date:
            if event["date"] > end_date:
                match = False

        if keyword:
            if keyword.lower() not in event["description"].lower():
                match = False

        if match:
            results.append(event)

    results.sort(key=lambda e: e["date"])
    return results

What does this code do?

search_events() accepts three optional filters: start_date, end_date, and keyword. It loops through every stored event and applies each filter that was provided.

String comparison works for dates because YYYY-MM-DD format sorts chronologically as plain text. The keyword search is case-insensitive so "dentist" matches "Dentist appointment".

Results are sorted by date before returning so the agent can present events in order.

You won't see a change yet. The next function uses search_events to power a convenience helper for upcoming events.

  • Add the final function below search_events in memory.py:
def get_upcoming_events(days=7):
    """Get events in the next N days for context assembly."""
    today = datetime.now().strftime("%Y-%m-%d")
    end = (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d")
    return search_events(start_date=today, end_date=end)

What does this code do?

get_upcoming_events() is a convenience wrapper that calculates today's date and a future date (7 days ahead by default), then passes them to search_events.

Later, your agent will call this before every conversation to load upcoming events into the system prompt. This gives the LLM passive awareness of the user's schedule without needing an explicit tool call.

  • Save memory.py.
  • Test the search by running this command:
python -c "from memory import search_events; print(search_events(keyword='dentist'))"
  • You should see a list containing the dentist appointment event you added earlier.

Seeing an empty list?

  • Check that calendar_memory.json exists in your folder. If you deleted it or switched directories, run the add_event test command again first.
  • Make sure the keyword matches the description you used. The search is case-insensitive but must match a substring of the description.

Why is my search_events returning empty results?

✔️ Awesome, I've got everything!

Great. Double check you've saved memory.py and move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete memory.py file. Make sure yours matches exactly:

import json
import os
import uuid
from datetime import datetime, timedelta


MEMORY_FILE = "calendar_memory.json"


def load_events():
    """Load all events from the JSON memory file."""
    if not os.path.exists(MEMORY_FILE):
        return []
    with open(MEMORY_FILE, "r") as f:
        return json.load(f)


def save_events(events):
    """Save all events to the JSON memory file."""
    with open(MEMORY_FILE, "w") as f:
        json.dump(events, f, indent=2)


def add_event(date, description, category="event"):
    """Add a new calendar event to memory."""
    events = load_events()
    new_event = {
        "id": str(uuid.uuid4()),
        "date": date,
        "description": description,
        "category": category,
        "created_at": datetime.now().isoformat()
    }
    events.append(new_event)
    save_events(events)
    return new_event


def search_events(start_date=None, end_date=None, keyword=None):
    """Search events by date range and/or keyword."""
    events = load_events()
    results = []

    for event in events:
        match = True

        if start_date:
            if event["date"] < start_date:
                match = False

        if end_date:
            if event["date"] > end_date:
                match = False

        if keyword:
            if keyword.lower() not in event["description"].lower():
                match = False

        if match:
            results.append(event)

    results.sort(key=lambda e: e["date"])
    return results


def get_upcoming_events(days=7):
    """Get events in the next N days for context assembly."""
    today = datetime.now().strftime("%Y-%m-%d")
    end = (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d")
    return search_events(start_date=today, end_date=end)

Why JSON instead of a database?

A local JSON file avoids database setup overhead and keeps the project focused on the memory pattern itself. It's human-readable, so you can open calendar_memory.json at any time to inspect exactly what your agent remembers.

The read-write-search pattern you built here is the same one used in production systems backed by databases like Redis or PostgreSQL. Swapping storage later means changing only load_events and save_events.

Your memory module is complete. Next up, you'll connect it to the Grok API so the LLM can autonomously decide when to save and retrieve events.

Define Tools and Connect to Grok

Define Tools and Connect to Groq

Your memory module can store and search calendar events. Now you need to give the LLM the ability to use it autonomously.

Function calling (also called tool use) lets you define actions that Llama 3.3 can invoke on its own. You define the tools with names, descriptions, and parameter schemas. The model decides when to call them based on the conversation.

In this step, get ready to:
  • Build a system prompt that injects upcoming events as context.
  • Define two tools that let the model save and retrieve events.
  • Write a tool-calling loop that sends messages to Groq, executes tool calls, and returns the final answer.
Create app.py and build the system prompt

The agent needs context about the user's schedule before every conversation turn. A context assembly function loads upcoming events and injects them into the system prompt so the model is always aware of what's coming up.

  • Create a new file called app.py in your project folder (the same folder where memory.py lives).
  • Add the imports and the build_system_prompt() function by pasting this code:
import os
import json

import gradio as gr
from groq import Groq

from memory import add_event, search_events, get_upcoming_events


def build_system_prompt():
    """Assemble the system prompt with upcoming events context."""
    upcoming = get_upcoming_events(days=7)

    base_prompt = (
        "You are a helpful calendar memory agent. You remember the user's "
        "important dates, deadlines, meetings, and events. You can save new "
        "events and retrieve existing ones.\n\n"
        "When the user mentions a date, deadline, or event, use the save_event "
        "tool to store it. When they ask about their schedule, use the "
        "get_events tool to look it up.\n\n"
        "Always confirm what you saved or found. Be concise and helpful."
    )

    if upcoming:
        events_text = "\n".join(
            f"- {e['date']}: {e['description']} ({e['category']})"
            for e in upcoming
        )
        base_prompt += (
            f"\n\nUpcoming events (next 7 days):\n{events_text}"
        )
    else:
        base_prompt += "\n\nNo upcoming events in the next 7 days."

    return base_prompt

What does this code do?

The imports pull in the groq client and your memory functions from memory.py. The Groq SDK uses an OpenAI-compatible interface, so the patterns here will feel familiar if you have used the OpenAI Python library.

The build_system_prompt() function loads events happening in the next 7 days and appends them to the base system prompt. This gives the model passive awareness of the user's schedule without requiring a tool call every time.

  • Save app.py.
  • You won't see a visible change yet. The next chunk adds the tool definitions that this prompt references.
Define the agent's tools

Tools are custom functions that you define as JSON Schema objects and pass to the model. The model reads these schemas and decides when to call each tool based on the conversation.

  • In app.py, add the define_tools() function below build_system_prompt() by pasting:
def define_tools():
    """Define the tools available to the model."""
    tools = [
        {
            "type": "function",
            "function": {
                "name": "save_event",
                "description": "Save a new calendar event, deadline, or reminder to memory.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "date": {
                            "type": "string",
                            "description": "The date of the event in YYYY-MM-DD format"
                        },
                        "description": {
                            "type": "string",
                            "description": "A brief description of the event"
                        },
                        "category": {
                            "type": "string",
                            "enum": ["deadline", "meeting", "reminder", "event"],
                            "description": "The category of the event"
                        }
                    },
                    "required": ["date", "description"]
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "get_events",
                "description": "Retrieve calendar events from memory by date range or keyword.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "start_date": {
                            "type": "string",
                            "description": "Start date in YYYY-MM-DD format (optional)"
                        },
                        "end_date": {
                            "type": "string",
                            "description": "End date in YYYY-MM-DD format (optional)"
                        },
                        "keyword": {
                            "type": "string",
                            "description": "Keyword to search in event descriptions (optional)"
                        }
                    }
                }
            }
        }
    ]
    return tools

What does this code do?

  • save_event tells the model it can store a new event with a date, description, and optional category (deadline, meeting, reminder, or event).
  • get_events tells the model it can look up events by date range or keyword search.
  • Each tool is a dictionary with type set to function and a nested function object containing the name, description, and JSON Schema parameters. This is the standard OpenAI-compatible format that Groq uses.
  • Save app.py.
  • You won't see output yet. The next function wires these tool definitions to your actual memory functions.

Now add the handle_tool_call() function that executes tool calls when Grok requests them.

  • Below define_tools(), paste the following function:
def handle_tool_call(tool_call):
    """Execute a client-side tool call and return the result."""
    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    if name == "save_event":
        event = add_event(
            date=args["date"],
            description=args["description"],
            category=args.get("category", "event")
        )
        return json.dumps({
            "status": "saved",
            "event": event
        })

    elif name == "get_events":
        events = search_events(
            start_date=args.get("start_date"),
            end_date=args.get("end_date"),
            keyword=args.get("keyword")
        )
        return json.dumps({
            "status": "found",
            "count": len(events),
            "events": events
        })

    return json.dumps({"error": f"Unknown tool: {name}"})

What does this code do?

When Grok decides to call a tool, it sends back a tool call object containing the function name and arguments as a JSON string. This function reads the name, parses the arguments, and routes to the correct memory function.

The result is returned as a JSON string that gets sent back to the model so it can formulate a human-readable response.

  • Save app.py.
  • You won't see output yet. The next chunk creates the main conversation loop that ties everything together.

Seeing a syntax error?

Make sure each function is separated by a blank line from the one above. Python requires consistent indentation. Check that none of the code is nested inside another function.

help me fix a syntax error in my app.py tool definitions

Write the tool-calling loop

The conversation loop is the core of the agent. It sends the user's message to Grok, checks if the model wants to call any tools, executes them locally, feeds results back, and repeats until Grok produces a final text response.

  • Below handle_tool_call(), paste the chat_with_agent() function:
def chat_with_agent(message, history):
    """Process a user message through the Groq agent with tool calling."""
    client = Groq(api_key=os.getenv("GROQ_API_KEY"))
    tools = define_tools()
    system_prompt = build_system_prompt()

    # Build the messages array with system prompt and user message
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": message}
    ]

    while True:
        response = client.chat.completions.create(
            model="llama-3.3-70b-versatile",
            messages=messages,
            tools=tools,
        )

        choice = response.choices[0]

        # If the model wants to call tools, execute them
        if choice.finish_reason == "tool_calls":
            # Append the assistant's message (with tool_calls) to history
            messages.append(choice.message)

            # Execute each tool call and append results
            for tc in choice.message.tool_calls:
                result = handle_tool_call(tc)
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "name": tc.function.name,
                    "content": result,
                })
        else:
            # No tool calls — return the final text response
            return choice.message.content

What does this code do?

  • client.chat.completions.create() sends the conversation to Groq's llama-3.3-70b-versatile model with the available tools.
  • The while True loop checks if finish_reason is tool_calls. If it is, the model wants to call one or more tools before producing a final answer.
  • For each tool call, the loop executes it locally with handle_tool_call() and appends the result as a tool message with the matching tool_call_id. This tells the model which result corresponds to which call.
  • Once the model produces a final response with no tool calls, the loop returns the text content.

💡 Why a message array loop?

The Groq API uses an OpenAI-compatible message format. Each turn of the conversation is a dict with a role (system, user, assistant, or tool). When the model calls a tool, you append both the assistant's tool-call message and your tool result, then send the full array back. This keeps the entire context visible to the model across multiple tool calls.

  • Save app.py.
  • Verify the file has no syntax errors by running:
python -c "from app import chat_with_agent; print('Import successful')"

You should see Import successful printed in your terminal.

Seeing an ImportError or ModuleNotFoundError?

  • Make sure you are running the command from the same folder where app.py and memory.py live.
  • Check that groq is installed in the same Python environment you are using. Run pip show groq to confirm.
  • Verify the GROQ_API_KEY environment variable is set in your current terminal session.

help me fix an import error in my calendar agent app.py

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py and the import test passed.

ⓧ I'd like to double check the full code

Here is the complete app.py at this point:

import os
import json

import gradio as gr
from groq import Groq

from memory import add_event, search_events, get_upcoming_events


def build_system_prompt():
    """Assemble the system prompt with upcoming events context."""
    upcoming = get_upcoming_events(days=7)

    base_prompt = (
        "You are a helpful calendar memory agent. You remember the user's "
        "important dates, deadlines, meetings, and events. You can save new "
        "events and retrieve existing ones.\n\n"
        "When the user mentions a date, deadline, or event, use the save_event "
        "tool to store it. When they ask about their schedule, use the "
        "get_events tool to look it up.\n\n"
        "Always confirm what you saved or found. Be concise and helpful."
    )

    if upcoming:
        events_text = "\n".join(
            f"- {e['date']}: {e['description']} ({e['category']})"
            for e in upcoming
        )
        base_prompt += (
            f"\n\nUpcoming events (next 7 days):\n{events_text}"
        )
    else:
        base_prompt += "\n\nNo upcoming events in the next 7 days."

    return base_prompt


def define_tools():
    """Define the tools available to the model."""
    tools = [
        {
            "type": "function",
            "function": {
                "name": "save_event",
                "description": "Save a new calendar event, deadline, or reminder to memory.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "date": {
                            "type": "string",
                            "description": "The date of the event in YYYY-MM-DD format"
                        },
                        "description": {
                            "type": "string",
                            "description": "A brief description of the event"
                        },
                        "category": {
                            "type": "string",
                            "enum": ["deadline", "meeting", "reminder", "event"],
                            "description": "The category of the event"
                        }
                    },
                    "required": ["date", "description"]
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "get_events",
                "description": "Retrieve calendar events from memory by date range or keyword.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "start_date": {
                            "type": "string",
                            "description": "Start date in YYYY-MM-DD format (optional)"
                        },
                        "end_date": {
                            "type": "string",
                            "description": "End date in YYYY-MM-DD format (optional)"
                        },
                        "keyword": {
                            "type": "string",
                            "description": "Keyword to search in event descriptions (optional)"
                        }
                    }
                }
            }
        }
    ]
    return tools


def handle_tool_call(tool_call):
    """Execute a tool call and return the result."""
    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    if name == "save_event":
        event = add_event(
            date=args["date"],
            description=args["description"],
            category=args.get("category", "event")
        )
        return json.dumps({
            "status": "saved",
            "event": event
        })

    elif name == "get_events":
        events = search_events(
            start_date=args.get("start_date"),
            end_date=args.get("end_date"),
            keyword=args.get("keyword")
        )
        return json.dumps({
            "status": "found",
            "count": len(events),
            "events": events
        })

    return json.dumps({"error": f"Unknown tool: {name}"})


def chat_with_agent(message, history):
    """Process a user message through the Groq agent with tool calling."""
    client = Groq(api_key=os.getenv("GROQ_API_KEY"))
    tools = define_tools()
    system_prompt = build_system_prompt()

    # Build the messages array with system prompt and user message
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": message}
    ]

    while True:
        response = client.chat.completions.create(
            model="llama-3.3-70b-versatile",
            messages=messages,
            tools=tools,
        )

        choice = response.choices[0]

        # If the model wants to call tools, execute them
        if choice.finish_reason == "tool_calls":
            # Append the assistant's message (with tool_calls) to history
            messages.append(choice.message)

            # Execute each tool call and append results
            for tc in choice.message.tool_calls:
                result = handle_tool_call(tc)
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "name": tc.function.name,
                    "content": result,
                })
        else:
            # No tool calls — return the final text response
            return choice.message.content

Your agent brain is wired up. Next, you'll wrap it in a Gradio chat interface and test the full conversation flow locally.

Add Gradio ChatInterface and Test Locally

Your agent can remember events, search memory, and call tools autonomously. But right now there is no way for a user to actually talk to it.

In this step, you will wire up a Gradio ChatInterface that gives your agent a polished web-based chat UI. You will also test the full memory loop to confirm events persist across page refreshes.

In this step, get ready to:
  • Review how the context assembler keeps the agent aware of upcoming events.
  • Add a Gradio ChatInterface wrapping the agent function.
  • Test the full save-refresh-recall memory loop locally.
Review the context assembly pattern

Before each user message reaches Grok, your agent needs awareness of what is already on the schedule. Without this, the LLM would have no idea what events exist and would have to call a tool every single turn just to check.

  • In app.py, scroll to the build_system_prompt() function you added in the previous step.

What does build_system_prompt() do?

This function loads all events happening in the next 7 days using get_upcoming_events() from your memory module. It then formats them as bullet points and appends them to the system prompt.

The result is that Grok already knows your upcoming schedule before it even reads the user's message. This is the "context assembly before inference" pattern used in production memory systems.

Add the Gradio ChatInterface

Gradio's gr.ChatInterface component turns any Python function into a full chat UI with message input, history display, and example prompts. It expects a function that accepts a message string and a history list, which is exactly what chat_with_agent already does.

  • In app.py, scroll to the very bottom of the file (below the chat_with_agent function) and add this code:
demo = gr.ChatInterface(
    fn=chat_with_agent,
    title="Calendar Memory Agent",
    description="I remember your dates, deadlines, and events. Tell me about upcoming events or ask what's on your schedule!",
    examples=[
        "I have a dentist appointment on 2026-06-15",
        "What events do I have coming up?",
        "My project deadline is 2026-06-20",
    ],
)

if __name__ == "__main__":
    demo.launch()

What does this code do?

gr.ChatInterface wraps your chat_with_agent function in a web-based chat UI. The fn parameter points to the function that handles each message.

The examples list adds clickable prompt buttons to the UI so users can quickly test the agent. The if __name__ == "__main__" guard ensures the app only launches when you run the file directly.

  • Save app.py.

Seeing a syntax error when you save?

Make sure the demo = gr.ChatInterface(...) block sits outside the chat_with_agent function (no indentation). It should be at the same level as the function definitions above it.

help me fix a syntax error in my Gradio ChatInterface setup

Launch your agent and test the memory loop

The real test of a memory agent is persistence. You will save an event, refresh the page to clear the chat history, and then ask the agent about the event. If it remembers, your JSON memory file is working.

  • Run your agent locally by running this command:
python app.py

You should see output indicating the Gradio app is running at http://127.0.0.1:7860. Open that URL in your browser.

  • In the chat input, type a message like I have a dentist appointment on 2026-06-15 and press Enter.
  • Wait for the agent to respond confirming it saved the event.
  • Refresh the browser page to clear the chat history.
  • Type What events do I have coming up? and press Enter.

The agent should recall your dentist appointment even though you refreshed. This proves the JSON memory file is persisting data independently of the chat session.

Where is the memory stored?

Check your project folder. You should see a new calendar_memory.json file that was created when the agent saved your first event. Open it to see the structured event data with its date, description, and category.

🙋‍♀️ Agent not responding or showing an error?

  • Confirm your GROQ_API_KEY environment variable is set in the same terminal session where you ran the command.
  • Check that your Groq API key is valid by visiting console.groq.com/keys. The agent needs API access to generate responses.
  • If you see a connection error, make sure you are connected to the internet.

help me debug why my calendar agent is not responding

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py and confirmed the agent can remember events across page refreshes.

ⓧ I'd like to double check the full code

import os
import json

import gradio as gr
from groq import Groq

from memory import add_event, search_events, get_upcoming_events


def build_system_prompt():
    """Assemble the system prompt with upcoming events context."""
    upcoming = get_upcoming_events(days=7)

    base_prompt = (
        "You are a helpful calendar memory agent. You remember the user's "
        "important dates, deadlines, meetings, and events. You can save new "
        "events and retrieve existing ones.\n\n"
        "When the user mentions a date, deadline, or event, use the save_event "
        "tool to store it. When they ask about their schedule, use the "
        "get_events tool to look it up.\n\n"
        "Always confirm what you saved or found. Be concise and helpful."
    )

    if upcoming:
        events_text = "\n".join(
            f"- {e['date']}: {e['description']} ({e['category']})"
            for e in upcoming
        )
        base_prompt += (
            f"\n\nUpcoming events (next 7 days):\n{events_text}"
        )
    else:
        base_prompt += "\n\nNo upcoming events in the next 7 days."

    return base_prompt


def define_tools():
    """Define the tools available to the model."""
    tools = [
        {
            "type": "function",
            "function": {
                "name": "save_event",
                "description": "Save a new calendar event, deadline, or reminder to memory.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "date": {
                            "type": "string",
                            "description": "The date of the event in YYYY-MM-DD format"
                        },
                        "description": {
                            "type": "string",
                            "description": "A brief description of the event"
                        },
                        "category": {
                            "type": "string",
                            "enum": ["deadline", "meeting", "reminder", "event"],
                            "description": "The category of the event"
                        }
                    },
                    "required": ["date", "description"]
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "get_events",
                "description": "Retrieve calendar events from memory by date range or keyword.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "start_date": {
                            "type": "string",
                            "description": "Start date in YYYY-MM-DD format (optional)"
                        },
                        "end_date": {
                            "type": "string",
                            "description": "End date in YYYY-MM-DD format (optional)"
                        },
                        "keyword": {
                            "type": "string",
                            "description": "Keyword to search in event descriptions (optional)"
                        }
                    }
                }
            }
        }
    ]
    return tools


def handle_tool_call(tool_call):
    """Execute a tool call and return the result."""
    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    if name == "save_event":
        event = add_event(
            date=args["date"],
            description=args["description"],
            category=args.get("category", "event")
        )
        return json.dumps({
            "status": "saved",
            "event": event
        })

    elif name == "get_events":
        events = search_events(
            start_date=args.get("start_date"),
            end_date=args.get("end_date"),
            keyword=args.get("keyword")
        )
        return json.dumps({
            "status": "found",
            "count": len(events),
            "events": events
        })

    return json.dumps({"error": f"Unknown tool: {name}"})


def chat_with_agent(message, history):
    """Process a user message through the Groq agent with tool calling."""
    client = Groq(api_key=os.getenv("GROQ_API_KEY"))
    tools = define_tools()
    system_prompt = build_system_prompt()

    # Build the messages array with system prompt and user message
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": message}
    ]

    while True:
        response = client.chat.completions.create(
            model="llama-3.3-70b-versatile",
            messages=messages,
            tools=tools,
        )

        choice = response.choices[0]

        # If the model wants to call tools, execute them
        if choice.finish_reason == "tool_calls":
            # Append the assistant's message (with tool_calls) to history
            messages.append(choice.message)

            # Execute each tool call and append results
            for tc in choice.message.tool_calls:
                result = handle_tool_call(tc)
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "name": tc.function.name,
                    "content": result,
                })
        else:
            # No tool calls — return the final text response
            return choice.message.content


demo = gr.ChatInterface(
    fn=chat_with_agent,
    title="Calendar Memory Agent",
    description="I remember your dates, deadlines, and events. Tell me about upcoming events or ask what's on your schedule!",
    examples=[
        "I have a dentist appointment on 2026-06-15",
        "What events do I have coming up?",
        "My project deadline is 2026-06-20",
    ],
)

if __name__ == "__main__":
    demo.launch()

Your calendar agent is alive and remembering events locally. Next up, you will deploy it to Hugging Face Spaces so anyone can access it from a public URL.

Deploy to Hugging Face Spaces

Your calendar memory agent works locally. Now it is time to put it on the internet so anyone with the link can use it.

Hugging Face Spaces is a free hosting platform for ML apps. You upload your Python files to a Space, it installs your dependencies, and serves your Gradio app at a public URL. No Docker, no server config, no domain setup.

In this step, get ready to:
  • Create a free Hugging Face account and a new Gradio Space.
  • Add a requirements.txt and README.md for the Space configuration.
  • Upload your project files, configure your API key as a Secret, and verify the live chatbot.
Create your Hugging Face account and Space

Hugging Face Spaces are git repositories that automatically build and deploy your app every time you push a new file.

  • Open your browser and go to huggingface.co.
  • Click Sign Up in the top right corner and create a free account.
  • Once signed in, go to huggingface.co/new-space to create a new Space.
  • Enter a name for your Space (for example, calendar-memory-agent).
  • Under SDK, select Gradio.
  • Leave visibility set to Public.
  • Click Create Space.

What is a Hugging Face Space?

A Space is a hosted git repository that automatically builds and serves your app. When you select Gradio as the SDK, Hugging Face pre-installs Gradio in the environment and runs your app.py file on startup.

You only need to list additional dependencies (like groq) in a requirements.txt file.

Create the deployment files

Your Space needs two extra files that were not required locally: a requirements.txt for dependencies and a README.md with YAML frontmatter that configures how the Space builds.

  • Create a new file called requirements.txt in your project folder with the following content:
groq==1.2.0

Why only groq?

Gradio is pre-installed on Hugging Face Spaces, so you do not need to list it here. You only list packages that are not already part of the Space environment.

  • Create a new file called README.md in your project folder with the following content:
---
title: Calendar Memory Agent
emoji: 📅
colorFrom: blue
colorTo: purple
sdk: gradio
sdk_version: 6.16.0
app_file: app.py
pinned: false
---

# Calendar Memory Agent

An AI agent powered by Grok that remembers your calendar events, deadlines, and important dates across conversations.

What does the YAML frontmatter do?

The block between the --- lines tells Hugging Face how to configure your Space. sdk: gradio tells it to use the Gradio runtime. sdk_version: 6.16.0 pins the exact Gradio version. app_file: app.py tells it which file to run.

  • Confirm you now have four files ready to upload: app.py, memory.py, requirements.txt, and README.md.

✔️ Awesome, I've got everything!

Great. Make sure all four files are saved and ready to upload.

ⓧ I'd like to double check the full code

Here is the full content of each new file.

xai-sdk==1.12.2
---
title: Calendar Memory Agent
emoji: 📅
colorFrom: blue
colorTo: purple
sdk: gradio
sdk_version: 6.16.0
app_file: app.py
pinned: false
---

# Calendar Memory Agent

An AI agent powered by Grok that remembers your calendar events, deadlines, and important dates across conversations.
import os
import json

import gradio as gr
from xai_sdk import Client
from xai_sdk.chat import user, system, tool, tool_result
from xai_sdk.tools import get_tool_call_type

from memory import add_event, search_events, get_upcoming_events


def build_system_prompt():
    """Assemble the system prompt with upcoming events context."""
    upcoming = get_upcoming_events(days=7)

    base_prompt = (
        "You are a helpful calendar memory agent. You remember the user's "
        "important dates, deadlines, meetings, and events. You can save new "
        "events and retrieve existing ones.\n\n"
        "When the user mentions a date, deadline, or event, use the save_event "
        "tool to store it. When they ask about their schedule, use the "
        "get_events tool to look it up.\n\n"
        "Always confirm what you saved or found. Be concise and helpful."
    )

    if upcoming:
        events_text = "\n".join(
            f"- {e['date']}: {e['description']} ({e['category']})"
            for e in upcoming
        )
        base_prompt += (
            f"\n\nUpcoming events (next 7 days):\n{events_text}"
        )
    else:
        base_prompt += "\n\nNo upcoming events in the next 7 days."

    return base_prompt


def define_tools():
    """Define the client-side tools for the agent."""
    tools = [
        tool(
            name="save_event",
            description="Save a new calendar event, deadline, or reminder to memory.",
            parameters={
                "type": "object",
                "properties": {
                    "date": {
                        "type": "string",
                        "description": "The date of the event in YYYY-MM-DD format"
                    },
                    "description": {
                        "type": "string",
                        "description": "A brief description of the event"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["deadline", "meeting", "reminder", "event"],
                        "description": "The category of the event"
                    }
                },
                "required": ["date", "description"]
            }
        ),
        tool(
            name="get_events",
            description="Retrieve calendar events from memory by date range or keyword.",
            parameters={
                "type": "object",
                "properties": {
                    "start_date": {
                        "type": "string",
                        "description": "Start date in YYYY-MM-DD format (optional)"
                    },
                    "end_date": {
                        "type": "string",
                        "description": "End date in YYYY-MM-DD format (optional)"
                    },
                    "keyword": {
                        "type": "string",
                        "description": "Keyword to search in event descriptions (optional)"
                    }
                }
            }
        )
    ]
    return tools


def handle_tool_call(tool_call):
    """Execute a client-side tool call and return the result."""
    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    if name == "save_event":
        event = add_event(
            date=args["date"],
            description=args["description"],
            category=args.get("category", "event")
        )
        return json.dumps({
            "status": "saved",
            "event": event
        })

    elif name == "get_events":
        events = search_events(
            start_date=args.get("start_date"),
            end_date=args.get("end_date"),
            keyword=args.get("keyword")
        )
        return json.dumps({
            "status": "found",
            "count": len(events),
            "events": events
        })

    return json.dumps({"error": f"Unknown tool: {name}"})


def chat_with_agent(message, history):
    """Process a user message through the Grok agent with tool calling."""
    client = Client(api_key=os.getenv("XAI_API_KEY"))
    tools = define_tools()
    system_prompt = build_system_prompt()

    chat = client.chat.create(
        model="grok-4.3",
        tools=tools,
    )
    chat.append(system(system_prompt))
    chat.append(user(message))

    while True:
        client_side_tool_calls = []
        for response, chunk in chat.stream():
            for tc in chunk.tool_calls:
                if get_tool_call_type(tc) == "client_side_tool":
                    client_side_tool_calls.append(tc)

        if not client_side_tool_calls:
            break

        chat = client.chat.create(
            model="grok-4.3",
            tools=tools,
            previous_response_id=response.id,
            store_messages=True,
        )

        for tc in client_side_tool_calls:
            result = handle_tool_call(tc)
            chat.append(tool_result(result))

    return response.content


demo = gr.ChatInterface(
    fn=chat_with_agent,
    title="Calendar Memory Agent",
    description="I remember your dates, deadlines, and events. Tell me about upcoming events or ask what's on your schedule!",
    examples=[
        "I have a dentist appointment on 2026-06-15",
        "What events do I have coming up?",
        "My project deadline is 2026-06-20",
    ],
)

if __name__ == "__main__":
    demo.launch()
import json
import os
import uuid
from datetime import datetime, timedelta


MEMORY_FILE = "calendar_memory.json"


def load_events():
    """Load all events from the JSON memory file."""
    if not os.path.exists(MEMORY_FILE):
        return []
    with open(MEMORY_FILE, "r") as f:
        return json.load(f)


def save_events(events):
    """Save all events to the JSON memory file."""
    with open(MEMORY_FILE, "w") as f:
        json.dump(events, f, indent=2)


def add_event(date, description, category="event"):
    """Add a new calendar event to memory."""
    events = load_events()
    new_event = {
        "id": str(uuid.uuid4()),
        "date": date,
        "description": description,
        "category": category,
        "created_at": datetime.now().isoformat()
    }
    events.append(new_event)
    save_events(events)
    return new_event


def search_events(start_date=None, end_date=None, keyword=None):
    """Search events by date range and/or keyword."""
    events = load_events()
    results = []

    for event in events:
        match = True

        if start_date:
            if event["date"] < start_date:
                match = False

        if end_date:
            if event["date"] > end_date:
                match = False

        if keyword:
            if keyword.lower() not in event["description"].lower():
                match = False

        if match:
            results.append(event)

    results.sort(key=lambda e: e["date"])
    return results


def get_upcoming_events(days=7):
    """Get events in the next N days for context assembly."""
    today = datetime.now().strftime("%Y-%m-%d")
    end = (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d")
    return search_events(start_date=today, end_date=end)
Upload files and add your API key Secret

Your Space is an empty repository right now. You need to upload your four files and add your Groq API key as a Secret so the deployed app can authenticate with the Groq API.

  • In your Space page on Hugging Face, click the Files tab.
  • Click + Add file and select Upload files.
  • Drag and drop (or browse for) all four files: app.py, memory.py, requirements.txt, and README.md.
  • Click Commit changes to upload them to your Space.

The Space will try to build immediately, but it will fail because it cannot find your API key yet. Next, you will add it as a Secret.

  • Click the Settings tab at the top of your Space page.
  • Scroll down to the Variables and secrets section.
  • Click New secret.
  • Enter GROQ_API_KEY as the name.
  • Paste your Groq API key as the value (the same key you used locally).
  • Click Save.

How do Secrets work on Hugging Face Spaces?

Secrets are exposed to your app as environment variables at runtime. Your code already reads os.getenv("GROQ_API_KEY"), so it picks up the Secret automatically. Secret values are private and cannot be read back from the Settings page once saved.

Adding or updating a Secret triggers a Space restart. Your Space will now rebuild with access to the API key.

  • Switch back to the App tab and wait for the build to complete. This usually takes 1-2 minutes.
  • Once the build finishes, you should see the Calendar Memory Agent chat interface loaded in your browser.
  • Test it by typing a message like I have a dentist appointment on 2026-06-15 and pressing Enter.
  • Verify the agent confirms it saved the event.

Space stuck on Building or showing an error?

Check the build logs by clicking the three-dot menu on your Space page and selecting the logs option. Common issues include:

  • A typo in requirements.txt (make sure it says xai-sdk==1.12.2 exactly).
  • Missing or incorrectly named README.md YAML fields (copy the exact content from above).
  • The XAI_API_KEY Secret name does not match what os.getenv("XAI_API_KEY") expects. The name must be an exact character match.

Still stuck? Help me debug my Hugging Face Space deployment. The build is failing or the app is not loading.

💡 Your memory resets on Space rebuilds

Hugging Face Spaces use non-persistent disk storage. If your Space rebuilds or restarts, the calendar_memory.json file is lost. This is fine for demonstrating the memory pattern. A production version would use a database instead.

Secret mission

Add Natural Language Date Queries

Your agent can save and retrieve events, but users have to think in exact date ranges. What if you could just ask 'What do I have next week?' and get a natural answer? In this mission, you'll add date parsing intelligence so the agent handles relative time expressions like a human assistant would.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Your Hugging Face Space is on the free tier, so hosting costs nothing. However, every chat interaction on your public Space consumes Grok API tokens charged to your xAI account.

Cost warning

While your Space is live, anyone with the public URL can send messages that consume your Grok API credits. If you are not actively using the agent, pause or delete the Space to prevent unexpected charges.

Resources you used:

  • Hugging Face Space (Calendar Memory Agent).
  • xAI API key (set as XAI_API_KEY locally and as a Hugging Face Space Secret).
  • Local project folder on your Desktop containing memory.py, app.py, requirements.txt, README.md, and calendar_memory.json.
  • Python packages xai-sdk and gradio installed via pip.

Keep everything running

No action needed. Choose this if you are still actively building or want the chatbot to stay live at its public URL.

  • Your Hugging Face Space remains accessible to anyone with the link.
  • Your local project files stay on your Desktop.
  • Your xAI API key stays active and will be charged for any usage on the Space.
  • The xai-sdk and gradio packages remain installed in your Python environment.

Pause - I'll come back to this later

Pause your Space to stop it from serving requests. This prevents any API token usage while you are away.

  • Open your Space on Hugging Face.
  • Click Settings in the top-right area of the Space page.
  • Scroll down and click the option to pause your Space. You can restart it any time you want to resume.
  • Your local files and installed packages remain untouched for when you return.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each item below to fully clean up.

  • Delete your Hugging Face Space: open the Space, click Settings, scroll to the bottom of the page, and use the delete option to permanently remove the Space.
  • Delete your xAI API key: go to accounts.x.ai, navigate to the API Keys page, and delete the key you created for this project.
  • Remove the local environment variable. On macOS/Linux, remove the export XAI_API_KEY=... line from your shell profile file. On Windows, run:
setx XAI_API_KEY ""
  • Delete your local project folder by running this command in your terminal:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

On Windows, use this command instead:

cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
  • Uninstall the Python packages by running:
pip uninstall xai-sdk gradio -y

Nice Work!

Nice Work!

You just built and deployed a calendar memory agent powered by the Grok API. Your agent can remember dates, deadlines, and events across conversations, all accessible through a live public URL.

You've learned how to:

  • Built a persistent JSON-based memory system and connected it to the Grok API (grok-4.3) using client-side function calling to give the LLM autonomous read/write access to stored events.
  • Created a Gradio ChatInterface with a context assembly pattern that injects upcoming events into the system prompt before every turn.
  • Deployed the agent to Hugging Face Spaces with a live public URL anyone can access, using Secrets for secure API key management.
  • Secret Mission: Added natural language date queries so the agent can answer questions like "What do I have next week?" using the query_schedule tool and parse_relative_date function.

Ready to quiz yourself?