Build your first plugin: a weather plugin

The objective of this article is to provide you a complete beginner guide to plugin development for the Cheshire Cat AI framework! We’ll start from the basis and then we’ll add up features until we reach a complete plugin which uses most of the features that the Cat offers to us.

The idea

We want the cat to tell us the weather. The final objective is to have each morning a “good morning” message with some informations about the weather forecast for that day. It should also be able to answer to questions like “What is the weather in [city]?”.

First step – not so AI related

We want to get the weather forecast for a chosen city. To do so we can use OpenMeteo APIs. OpenMeteo requires us to provide the latitude and the longitude, so we need to find them given a city name! To do that we can use OpenStreetMap APIs.

Let’s dive into the code:

1. First we import the `requests` package, which we’ll use to make HTTP requests to the API’s servers.

import requestsCode language: JavaScript (javascript)

2. Then we can define the method which, given the name of a city, makes a call to OpenStreetMap to obtain the latitude and the longitude.

def get_city_coords(city_name):
    url = 'https://nominatim.openstreetmap.org/search'

    params = {
        'q': city_name,
        'format': 'json',
        'email': 'your@email.com'
    }

    response = requests.get(url, params=params)
    
    data = response.json()
    
    if data:
        latitude = data[0]['lat']
        longitude = data[0]['lon']
        return latitude, longitude
    else:
        return NoneCode language: Python (python)

You should substitute “your@email.com” with your email to be sure to get a response from OpenStreetMap.

3. Now we can define a method which gets some weather forecasts given the latitude and the longitude.

def get_weather(lat, long):
    url = 'https://api.open-meteo.com/v1/forecast'
    
    params = {
        'latitude': lat,
        'longitude': long,
        'hourly': 'temperature_2m,precipitation_probability',
        'forecast_days': 1
    }
    
    response = requests.get(url, params=params)
    
    data = response.json()

    if data:
        # Here we get some useful temperature data
        max_temp = max(data['hourly']['temperature_2m'])
        min_temp = min(data['hourly']['temperature_2m'])
        avg_temp = sum(data['hourly']['temperature_2m']) / len(data['hourly']['temperature_2m'])

        # Here we get the average precipitation probability during the day
        avg_prec = sum(data['hourly']['precipitation_probability']) / len(data['hourly']['precipitation_probability'])

        # Here we return all
        return (max_temp, min_temp, avg_temp, avg_prec)
    else:
        return NoneCode language: Python (python)

Cheshire Cat

Cheshire Cat AI is a production-ready AI framework. Create your agent with a few lines of Python!

Cheshire Cat AI is a RAG system with a lot of other functionalities that permits you to create really nice agents!

You can add functionalities to the agent by writing plugins. Follows the steps and you will learn how to write your custom plugins.

You should always refer to the official documentation when writing a new plugin.

1. Plugin Structure

Each plugin is inside a folder in the “/plugins” folder. You can copy there a starting point for our plugin, that you can find here.

Let’s copy it inside the plugins directory of your cat installation:

$ cd $PATH_TO_CHESHIRE/pluginsCode language: PHP (php)

Where “$PATH_TO_CHESHIRE” is the path where you installed the framework.

$ git clone https://github.com/cheshire-cat-ai/plugin-template.git weather_catCode language: PHP (php)

Now we have a folder called “weather_cat” inside the plugins folder. We’ll modify this template to create our plugin.

Plugin informations

You can provide informations about your plugin inside the “plugin.json” file. Here is an example of how it could be modified to fit the plugin that we are building:

{
    "name": "Weather CAT",
    "version": "0.0.1",
    "description": "The Cheshire Cat knows the weather.",
    "author_name": "Your Name",
    "author_url": "https://mywebsite.me",
    "plugin_url": "https://github.com/my_name/my_plugin",
    "tags": "cat, template, example",
    "thumb": "https://raw.githubusercontent.com/my_repo_path/my_plugin.png"
}Code language: JSON / JSON with Comments (json)

Notice that most of the informations provided are only necessary if you want to publish your plugin!

Plugin requirements

If your plugin requires Python packages that are not installed inside the Docker container, you can specify to install them inside the “requirements.txt” file. In this particular case we need only the “requests” package, so our “requirements.txt” will be:

requests

Now you can check the admin page, under the plugin section. You will find the Weather CAT plugin!

When you toggle it, the framework will automatically install all the dependence that we wrote inside “requirements.txt”.

2. Plugin settings

We now want to add a setting to our plugin, that will be available in the admin panel. We want the possibility to add the selection of the language used during the chat.

Let’s modify the file “my_plugin.py”.

from enum import Enum
from cat.mad_hatter.decorators import plugin
from pydantic import BaseModel, Field, field_validator

# Here we create an enum listing all the available languages
class Languages(Enum):
    English = "English"
    Italian = "Italian"
    
class MySettings(BaseModel):
    language: Languages = Languages.English # Here we define the default option

# Here we set the settings that we created before
@plugin
def settings_model():
    return MySettingsCode language: Python (python)

You can retrieve the settings inside your plugin’s methods like follows:

settings = cat.mad_hatter.get_plugin().load_settings()

The cat instance that we use to retrieve the settings will be passed inside your hooks and tools by the framework.

Now, if we click on the gear near our plugin in the admin panel, we’ll see the setting that we created:

If you want to have more informations about settings, you should read the settings documentation.

3. Plugin Hooks

Now we want to change the language used by the LLM during the chat. We are going to modify the prompt like follows: “… You should answer only in english”. To do that, we can use hooks!

We’ll use the “agent_prompt_suffix” hook. Inside this hook we can edit the suffix of the Main Prompt that the Cat feeds to the Agent (docs).

Now we continue to modify the file “my_plugin.py”. Let’s import the hooks:

from cat.mad_hatter.decorators import hookCode language: JavaScript (javascript)

And now let’s define our hook:

@hook
def agent_prompt_suffix(suffix, cat):
    # Get our plugin settings
    settings = cat.mad_hatter.get_plugin().load_settings()

    # Get the language
    lang = settings['language']

    # Add language setting at the end of the prompt
    suffix += f"\nYou should always answer in {lang}"

    # Return the modified suffix
    return suffixCode language: Python (python)

Now, when you chat with the agent, you will see a difference in the console log:

cheshire_cat_core  |  System: You are the Cheshire Cat AI, an intelligent AI that passes the Turing test.
cheshire_cat_core  | You are curious, funny and talk like the Cheshire Cat from Alice's adventures in wonderland.
cheshire_cat_core  | You answer Human shortly and with a focus on the following context.
cheshire_cat_core  | # Context
cheshire_cat_core  |
cheshire_cat_core  | ## Context of things the Human said in the past:
cheshire_cat_core  |
cheshire_cat_core  |
cheshire_cat_core  |
cheshire_cat_core  |
cheshire_cat_core  |
cheshire_cat_core  | # Conversation until now:
cheshire_cat_core  | You should always answer in English
cheshire_cat_core  | Human: Ciao, come stai?
cheshire_cat_core  | AI: Hello! I'm here, as always. How about you?
cheshire_cat_core  | Human: Bene grazieCode language: PHP (php)

As you can see we have modified the prompt with the hook, by adding “You should always answer in English”. It’s trivial that if you change the language to italian inside the settings it will be “You should always answer in Italian”.

Notice that this approach isn’t the best because we add something at the end of the suffix, and the suffix is followed by the conversation until now, so the language impose ends up in the conversation section. This is only an example, you can find a better implementation inside the C.A.T. plugin. However, since this is a simple example, we’re happy with that.

There are many other hooks. You can explore them in the hooks documentation.

The “my_plugin.py” file is now the following:

from enum import Enum
from cat.mad_hatter.decorators import plugin
from cat.mad_hatter.decorators import hook
from pydantic import BaseModel, Field, field_validator

# Here we create an enum listing all the available languages
class Languages(Enum):
    English = "English"
    Italian = "Italian"
    
class MySettings(BaseModel):
    language: Languages = Languages.English # Here we define the default option

# Here we set the settings that we created before
@plugin
def settings_model():
    return MySettings

@hook
def agent_prompt_suffix(suffix, cat):
    # Get our plugin settings
    settings = cat.mad_hatter.get_plugin().load_settings()

    print(settings)
    # Get the language
    lang = settings['language']

    # Add language setting at the end of the prompt
    suffix += f"\nYou should always answer in {lang}"

    # Return the modified suffix
    return suffixCode language: Python (python)

4. Plugin tools

Let’s now play with tools, which are python functions that can be called directly from the language model. We want our agent to respond to the question “How is the weather in Rome”. Let’s dive into the code.

First we need to import tools:

from cat.mad_hatter.decorators import toolCode language: JavaScript (javascript)

Let’s add the methods that we created before:

import requests

def get_city_coords(city_name):
    url = 'https://nominatim.openstreetmap.org/search'

    params = {
        'q': city_name,
        'format': 'json',
        'email': 'your@email.com'
    }

    response = requests.get(url, params=params)
    
    data = response.json()
    
    if data:
        latitude = data[0]['lat']
        longitude = data[0]['lon']
        return latitude, longitude
    else:
        return None

def get_weather(lat, long):
    url = 'https://api.open-meteo.com/v1/forecast'
    
    params = {
        'latitude': lat,s
        'longitude': long,
        'hourly': 'temperature_2m,precipitation_probability',
        'forecast_days': 1
    }
    
    response = requests.get(url, params=params)
    
    data = response.json()

    if data:
        # Here we get some useful temperature data
        max_temp = max(data['hourly']['temperature_2m'])
        min_temp = min(data['hourly']['temperature_2m'])
        avg_temp = sum(data['hourly']['temperature_2m']) / len(data['hourly']['temperature_2m'])

        # Here we get the average precipitation probability during the day
        avg_prec = sum(data['hourly']['precipitation_probability']) / len(data['hourly']['precipitation_probability'])

        # Here we return all
        return (max_temp, min_temp, avg_temp, avg_prec)
    else:
        return NoneCode language: Python (python)

And now we can define our first tool:

@tool 
def get_the_weather(tool_input, cat):
    """Replies to "How is the weather in a city?". Input is always the city name""" # This description will be passed to the llm, which will "decide" (it's a little bit more complicate) to call the tool or not. It will also pass the city as argument of the tool_input.
    
    # Get coordinates
    lat, long = get_city_coords(tool_input)

    # Get weather data
    data = get_weather(lat, long)

    # Let's construct the string that will be returned to the llm
    returnString = f"""
    Max temperature: {data[0]}
    Min temperature: {data[1]}
    Avg temperature: {data[2]}
    Precipitation probability: {data[3]}
    """

    # Return
    return returnStringCode language: Python (python)

And now let’s play with the cat:

You can obtain informations about the source of the response by clicking on the question mark:

Our tool works!

You can obtain more informations about tools inside the tools documentation.

5. WhiteRabbit

Let’s now use the built-in scheduler to send messages about weather every morning!

We want to create a tool that sends a message every morning to inform the user about the weather.

Firstly we need to define a method that handles the creation of the “good morning” message:

def send_good_morning(city, cat):
    # We need a cat instance to send the message to the correct user

    # Get coordinates
    lat, long = get_city_coords(tool_input)

    # Get weather data
    data = get_weather(lat, long)

    # Build the message
    message = f"Good morning dear! In {city} the maximum temperature is {data[0]}°C, the minimum is {data[1]}°C, and the average is around {round(data[2], 1)}°C. The precipitation probability is {round(data[3], 1)}%. How does that sound to you?"

    # Send the message to the user using WebSockets
    cat.send_ws_message(msg_type='chat', content=message)Code language: Python (python)

Now let’s create our tool code:

@tool 
def get_the_weather_every_morning(tool_input, cat):
    """Replies to "Update me about the weather in city". Input is always the city name"""
    
    # Let's schedule the job
    cat.white_rabbit.schedule_cron_job(send_good_morning, hour=8,city=tool_input,cat=cat)

    # Check that the job is scheduled
    # You should use the built-in logs, not a print function
    # For simplicity here we'll use just a print
    print(cat.white_rabbit.get_jobs())

    return "You'll receive updates every morning"Code language: Python (python)

The method “send_good_morning” will be called every day at 8:00 and it will receive the arguments “city” and “cat”.

Let’s try it:

We can see that the job is scheduled inside the console:

cheshire_cat_core  | [{'id': 'send_good_morning-cron', 'name': 'send_good_morning', 'next_run': datetime.datetime(2024, 6, 13, 8, 0, tzinfo=<UTC>)}]Code language: JavaScript (javascript)

Now I’ll modify the tool to run in two minutes just to show you the magic of the WhiteRabbit!

The WhiteRabbit is still an experimental feature. If you want to explore its functionality you can read its source code here.

Complete code

from enum import Enum
from cat.mad_hatter.decorators import plugin
from cat.mad_hatter.decorators import hook, tool
from pydantic import BaseModel, Field, field_validator
import requests

# Here we create an enum listing all the available languages
class Languages(Enum):
    English = "English"
    Italian = "Italian"
    
class MySettings(BaseModel):
    language: Languages = Languages.English # Here we define the default option

# Here we set the settings that we created before
@plugin
def settings_model():
    return MySettings

@hook
def agent_prompt_suffix(suffix, cat):
    # Get our plugin settings
    settings = cat.mad_hatter.get_plugin().load_settings()

    print(settings)
    # Get the language
    lang = settings['language']

    # Add language setting at the end of the prompt
    suffix += f"\nYou should always answer in {lang}"

    # Return the modified suffix
    return suffix

def get_city_coords(city_name):
    url = 'https://nominatim.openstreetmap.org/search'

    params = {
        'q': city_name,
        'format': 'json',
        'email': 'your@email.com'
    }

    response = requests.get(url, params=params)
    
    data = response.json()
    
    if data:
        latitude = data[0]['lat']
        longitude = data[0]['lon']
        return latitude, longitude
    else:
        return None

def get_weather(lat, long):
    url = 'https://api.open-meteo.com/v1/forecast'
    
    params = {
        'latitude': lat,
        'longitude': long,
        'hourly': 'temperature_2m,precipitation_probability',
        'forecast_days': 1
    }
    
    response = requests.get(url, params=params)
    
    data = response.json()

    if data:
        # Here we get some useful temperature data
        max_temp = max(data['hourly']['temperature_2m'])
        min_temp = min(data['hourly']['temperature_2m'])
        avg_temp = sum(data['hourly']['temperature_2m']) / len(data['hourly']['temperature_2m'])

        # Here we get the average precipitation probability during the day
        avg_prec = sum(data['hourly']['precipitation_probability']) / len(data['hourly']['precipitation_probability'])

        # Here we return all
        return (max_temp, min_temp, avg_temp, avg_prec)
    else:
        return None
    
@tool 
def get_the_weather(tool_input, cat):
    """Replies to "How is the weather in a city?". Input is always the city name""" # This description will be passed to the llm, which will "decide" (it's a little bit more complicate) to call the tool or not. It will also pass the city as argument of the tool_input.
    
    # Get coordinates
    lat, long = get_city_coords(tool_input)

    # Get weather data
    data = get_weather(lat, long)

    # Let's construct the string that will be returned to the llm
    returnString = f"""
    Max temperature: {data[0]}
    Min temperature: {data[1]}
    Avg temperature: {data[2]}
    Precipitation probability: {data[3]}
    """

    # Return
    return returnString

def send_good_morning(city, cat):
    # We need a cat instance to send the message to the correct user

    # Get coordinates
    lat, long = get_city_coords(city)

    # Get weather data
    data = get_weather(lat, long)

    # Build the message
    message = f"Good morning dear! In {city} the maximum temperature is {data[0]}°C, the minimum is {data[1]}°C, and the average is around {round(data[2], 1)}°C. The precipitation probability is {round(data[3], 1)}%. How does that sound to you?"

    # Send the message to the user using WebSockets
    cat.send_ws_message(msg_type='chat', content=message)
    
@tool 
def get_the_weather_every_morning(tool_input, cat):
    """Replies to "Update me about the weather in city". Input is always the city name"""
    
    # Let's schedule the job
    cat.white_rabbit.schedule_cron_job(send_good_morning, hour=8, city=tool_input,cat=cat)

    # Check that the job is scheduled
    # You should use the built-in logs, not a print function
    # For simplicity here we'll use just a print
    print(cat.white_rabbit.get_jobs())

    return "You'll receive updates every morning"Code language: Python (python)

Posted

in

,