Tutorials

Building an AI-powered script to manage DigitalOcean

April 16, 2024 by luc juggery

In this post we will illustrate the usage of OpenAI and GPTScript to run a recurring task on Digital Ocean. We will check every minute if a given URL is reachable and send the resulting http status code to a webhook.

I’ve created a video that also demonstrates the process.

<iframe width=”560″ height=”315″ src=”https://www.youtube.com/embed/fqzqcZDBm0E?si=hNID7dudcaf4fMxM” title=”YouTube video player” frameborder=”0″ allow=”accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share” referrerpolicy=”strict-origin-when-cross-origin” allowfullscreen></iframe>

High level Instructions

First we create the task.gpt text file containing the instructions we want GPTScript to send to the LLM. These instructions are the following ones:

Create a task which verifies every minute if the website WEBSITE_URL is reachable and sends the status code to the following HTTP POST request:
- URL is https://webhooks.app/data
- Authorization bearer is TOKEN
- A json payload must be returned, containing a "message" property with the value of the WEBSITE_URL and the status code returned

There are 2 parts in these instructions:

  • first we ask to check if a given url (WEBSITE_URL) is available
  • then we ask to send the status code to a webhook and provide its characteristics

Note: the webhook URL https://webhooks.app is a simple and free endpoint used for demo purposes. It allows to inspect the content of the payloads sent from various systems

Both WEBSITE_URL and TOKEN are placeholders that we want to set dynamically. In order to do so we change task.gpt so it:

  • defines and url and a token as string arguments
  • uses the ${} format to reference these values dynamically
args: url: URL of the website to check
args: token: webhook token

Create a task which verifies every minute if the website ${url} is reachable and sends the status code to the following HTTP POST request:
- URL is https://webhooks.app/data
- Authorization bearer is ${token}
- A json payload must be returned, containing a "message" property with the value of the ${url} and the status code returned

Let’s try to run this first version of the script as-is.

But first we get a personal token from https://webhooks.app (9326bdf257548c4277e8e395a2a772 in this example)

webhook-token.png

Next we run the script using the following command which provides:

  • the url to check: vote.votingapp.xyz which is a demo application
  • the token to send the status code to
$ gptscript task.gpt --token 9326bdf257548c4277e8e395a2a772 --url https://vote.votingapp.xyz

We get an output similar to:

OUTPUT:

```python
import requests
import time

def check_website_status(url, token):
    while True:
        try:
            response = requests.get(url)
            status_code = response.status_code
        except requests.exceptions.RequestException:
            status_code = "Error"

        payload = {
            "message": f"{url} {status_code}"
        }
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        }

        requests.post("https://webhooks.app/data", json=payload, headers=headers)
        time.sleep(60)

check_website_status("https://vote.votingapp.xyz", "9326bdf257548c4277e8e395a2a772")

As we can see, from our instructions the model understands it needs to generate some code. It returned a Python script which checks the status of our website every 60 seconds and send the status code

This python script would probably work fine but the main goal we want to achieve is a better automation of the whole thing. In order to do so we will define additional tools that the LLM can use in its replies.

Defining tools

we will consider the following hierarchy of tools in this example:

tools-hierarchy.png

First, we define create-regular-task which is a high-level tool. From 2 arguments, the command to run and its associated schedule, this tool relies on 2 other tools to create a virtual machine and to create a crontab entry into that one.

tools: create-vm, create-crontab-entry
name: create-regular-task
description: Manage the creation of a crontab on a remove VM
args: command: command to be run in a crontab without the schedule part
args: schedule: schedule to be used in a crontab

Perform the actions in the following order:

1. Create a virtual machine on DigitalOcean, wait for its IP to be returned and save it in ./vm.ip
2. Create a crontab entry for command ${command} and schedule ${schedule} in the VM which IP address is in file ./vm.ip

The tools referenced in create-regular-task are create-vm and create-crontab-entry:

Below is the definition of create-vm which is in charge of creating a VM on DigitalOcean and saving its IP address to a file:

tools: sys.exec
name: create-vm
description: create a virtual machine on DigitalOcean

You are an operator which can use the doctl command line tool to interact with DigitalOcean infrastructure

Perform the actions in this exact order:

1. Get the ID of the ssh-key named gptscript and save it in ./key.id
2. Create a Virtual Machine in the new-york datacenter named cron making sure to provide the id from ./key.id as the ssh-key of the new droplet
3. Wait for the VM to be up and running and save its IP address in ./vm.ip

Below is the definition of create-crontab-entry with is in charge of sshing into the VM created by create-vm, creating a shell script with the command to run, and making sure this script is running every minute via the VM’s crontab:

tools: sys.exec, sys.write
name: create-crontab-entry
description: Create a crontab entry in a remote VM
args: command: command to run in a crontab on the remote VM
args: schedule: schedule for the crontab

Perform the step in this exact order taking into account that if you need to call a ssh command you must use user root and the IP address which value is in ./vm.ip

1. Create a bash file containing the ${command} to run without the schedule part, make it executable, and make sure the components are correctly escaped first.
2. Send this file to the remote VM via ssh saving it to /tmp/cron.sh on the remove VM
3. Create a crontab entry calling /tmp/cron.sh file for the schedule specified in the ${command}

Both create-vm and create-crontab-entry relies on base tools (sys.exec and sys.exec + sys.write respectively) making it possible to call local functions and to create files.

Now that everything is in place we can test our set of actions.

The whole thing in action

The final content of task.gpt is the following one:

tools: create-regular-task
args: url: URL of the website to check
args: token: webhook token

Create a task which verifies every minute if the website ${url} is reachable and sends the status code to the following HTTP POST request:
- URL is https://webhooks.app/data
- Authorization bearer is ${token}
- A json payload must be returned, containing a "message" property with the value of the ${url} and the status code returned

---
tools: create-vm, create-crontab-entry
name: create-regular-task
description: Manage the creation of a crontab on a remote VM
args: command: command to be run in a crontab without the schedule part
args: schedule: schedule to be used in a crontab

Perform the actions in the following order:

1. Create a virtual machine on DigitalOcean, wait for its IP to be returned and save it in ./vm.ip
2. Create a crontab entry for command ${command} and schedule ${schedule} in the VM which IP address is in file ./vm.ip

---
tools: sys.exec, sys.write
name: create-crontab-entry
description: Create a crontab entry in a remote VM
args: command: command to run in a crontab on the remote VM
args: schedule: schedule for the crontab

Perform the step in this exact order taking into account that if you need to call a ssh command you must use user root and the IP address which value is in ./vm.ip

1. Create a bash file containing the ${command} to run without the schedule part, make it executable, and make sure the components are correctly escaped first.
2. Send this file to the remote VM via ssh saving it to /tmp/cron.sh on the remove VM
3. Create a crontab entry calling /tmp/cron.sh file for the schedule specified in the ${command}

---
tools: sys.exec
name: create-vm
description: create a virtual machine on DigitalOcean

You are an operator which can use the doctl command line tool to interact with DigitalOcean infrastructure

Perform the actions in this exact order:

1. Get the ID of the ssh-key named gptscript and save it in ./key.id
2. Create a Virtual Machine in the new-york datacenter named cron making sure to provide the id from ./key.id as the ssh-key of the new droplet
3. Wait for the VM to be up and running and save its IP address in ./vm.ip

Before running the test, we need:

  • a token to send the HTTP status code to the webhook
  • a DigitalOcean personal access token
  • a ssh key
  • Webhook token: we will use the same token as in the first part

  • Creation of a DO personal access token:

First we log into our DigitalOcean account, then use the API menu at the bottom left and click “Generate New Token”. From the modal we give the token a name (gptscript in this example) and select both Read and Write scopes:

do-pat.png

Once created we export the value of that token in the DIGITALOCEAN_ACCESS_TOKEN environment variable

  • Creation of a SSH key:

First we create a ssh key on your local machine using the following command:

ssh-keygen -f /tmp/do_gptscript

This creates both do_gptscript (private key) and do_gpscript.pub (public key) in the /tmp directory

Next, from the DigitalOcean control panel, we navigate to the Settings menu at the bottom left and then go to the Security tab. From there we copy the public key (/tmp/do_gptscript.pub) and name it gptscript

do-ssh-key.png

We’re all set and can now run the whole example:

gptscript --cache=false ./task.gpt --url https://vote.votingapp.xyz --token 9326bdf257548c4277e8e395a2a772

The output is similar to the following one (full output is provided for reference):

10:25:13 started  [main] [input=--url https://vote.votingapp.xyz --token 9326bdf257548c4277e8e395a2a772]
10:25:13 sent     [main]
         content  [1] content | Waiting for model response...
         content  [1] content | tool call create-regular-task -> {"defaultPromptParameter":"Create a task which verifies every minute if the website https://vote.votingapp.xyz is reachable and sends the status code to the following HTTP POST request:n- URL is https://webhooks.app/datan- Authorization bearer is 9326bdf257548c4277e8e395a2a772n- A json payload must be returned, containing a "message" property with the value of the https://vote.votingapp.xyz and the status code returned"}
10:25:19 started  [create-regular-task(2)] [input={"defaultPromptParameter":"Create a task which verifies every minute if the website https://vote.votingapp.xyz is reachable and sends the status code to the following HTTP POST request:n- URL is https://webhooks.app/datan- Authorization bearer is 9326bdf257548c4277e8e395a2a772n- A json payload must be returned, containing a "message" property with the value of the https://vote.votingapp.xyz and the status code returned"}]
10:25:19 sent     [create-regular-task(2)]
         content  [2] content | Waiting for model response...
         content  [2] content | tool call create-vm -> {"defaultPromptParameter":"Create a virtual machine on DigitalOcean. ...
10:25:21 started  [create-regular-task(3)->create-vm(3)] [input={"defaultPromptParameter":"Create a virtual machine on DigitalOcean."}]
10:25:21 sent     [create-regular-task(3)->create-vm(3)]
         content  [3] content | Waiting for model response...
         content  [3] content | tool call exec -> {"command":"doctl compute ssh-key list --format ID,Name --no-header | gre ...
10:25:23 started  [create-regular-task(4)->create-vm(4)->sys.exec(4)] [input={"command":"doctl compute ssh-key list --format ID,Name --no-header | grep gptscript | awk '{print $1}' > ./key.id"}]
10:25:23 sent     [create-regular-task(4)->create-vm(4)->sys.exec(4)]
10:25:24 ended    [create-regular-task(4)->create-vm(4)->sys.exec(4)]
10:25:24 continue [create-regular-task(3)->create-vm(3)]
10:25:24 sent     [create-regular-task(3)->create-vm(3)]
         content  [3] content | Waiting for model response...
         content  [3] content | tool call exec -> {"command":"doctl compute droplet create cron --region nyc1 --image ubunt ...
10:25:27 started  [create-regular-task(5)->create-vm(5)->sys.exec(5)] [input={"command":"doctl compute droplet create cron --region nyc1 --image ubuntu-20-04-x64 --size s-1vcpu-1gb --ssh-keys $(cat ./key.id) --wait --format PublicIPv4 --no-header > ./vm.ip"}]
10:25:27 sent     [create-regular-task(5)->create-vm(5)->sys.exec(5)]
10:26:01 ended    [create-regular-task(5)->create-vm(5)->sys.exec(5)]
10:26:01 continue [create-regular-task(3)->create-vm(3)]
10:26:02 sent     [create-regular-task(3)->create-vm(3)]
         content  [3] content | Waiting for model response...         content  [3] content | The virtual machine has been created on DigitalOcean, and its IP address has been saved in `./vm.ip` ...
10:26:03 ended    [create-regular-task(3)->create-vm(3)]
10:26:03 continue [create-regular-task(2)]
10:26:03 sent     [create-regular-task(2)]
         content  [2] content | Waiting for model response...
         content  [2] content | tool call create-crontab-entry -> {"command":"* * * * * curl -o /dev/null -s -w "%{http_co ...
10:26:10 started  [create-regular-task(6)->create-crontab-entry(6)] [input={"command":"* * * * * curl -o /dev/null -s -w "%{http_code}" https://vote.votingapp.xyz | xargs -I {} curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer 9326bdf257548c4277e8e395a2a772" -d '{"message": "https://vote.votingapp.xyz status code: {}"}' https://webhooks.app/data","schedule":"* * * * *"}]
10:26:10 sent     [create-regular-task(6)->create-crontab-entry(6)]
         content  [6] content | Waiting for model response...
         content  [6] content | tool call write -> {"content":"curl -o /dev/null -s -w "%{http_code}" https://vote.voting ...
10:26:14 started  [create-regular-task(7)->create-crontab-entry(7)->sys.write(7)] [input={"content":"curl -o /dev/null -s -w "%{http_code}" https://vote.votingapp.xyz | xargs -I {} curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer 9326bdf257548c4277e8e395a2a772" -d '{"message": "https://vote.votingapp.xyz status code: {}"}' https://webhooks.app/data","filename":"cron.sh"}]
10:26:14 sent     [create-regular-task(7)->create-crontab-entry(7)->sys.write(7)]
10:26:14 ended    [create-regular-task(7)->create-crontab-entry(7)->sys.write(7)]
10:26:14 continue [create-regular-task(6)->create-crontab-entry(6)]
10:26:14 sent     [create-regular-task(6)->create-crontab-entry(6)]
         content  [6] content | Waiting for model response...
         content  [6] content | tool call exec -> {"command":"chmod +x cron.sh"}
10:26:15 started  [create-regular-task(8)->create-crontab-entry(8)->sys.exec(8)] [input={"command":"chmod +x cron.sh"}]
10:26:15 sent     [create-regular-task(8)->create-crontab-entry(8)->sys.exec(8)]
10:26:15 ended    [create-regular-task(8)->create-crontab-entry(8)->sys.exec(8)]
10:26:15 continue [create-regular-task(6)->create-crontab-entry(6)]
10:26:16 sent     [create-regular-task(6)->create-crontab-entry(6)]
         content  [6] content | Waiting for model response...
         content  [6] content | tool call exec -> {"command":"cat ./vm.ip"}
10:26:17 started  [create-regular-task(9)->create-crontab-entry(9)->sys.exec(9)] [input={"command":"cat ./vm.ip"}]
10:26:17 sent     [create-regular-task(9)->create-crontab-entry(9)->sys.exec(9)]
10:26:17 ended    [create-regular-task(9)->create-crontab-entry(9)->sys.exec(9)]
10:26:17 continue [create-regular-task(6)->create-crontab-entry(6)]
10:26:18 sent     [create-regular-task(6)->create-crontab-entry(6)]
         content  [6] content | Waiting for model response...         content  [6] content | tool call exec -> {"command": "scp cron.sh root@159.65.218.18:/tmp/cron.sh"}
         content  [6] content | tool call exec -> {"command": "ssh root@159.65.218.18 'echo "* * * * * /tmp/cron.sh" | cr ...
10:26:21 started  [create-regular-task(10)->create-crontab-entry(10)->sys.exec(10)] [input={"command": "scp cron.sh root@159.65.218.18:/tmp/cron.sh"}]
10:26:21 started  [create-regular-task(11)->create-crontab-entry(11)->sys.exec(11)] [input={"command": "ssh root@159.65.218.18 'echo "* * * * * /tmp/cron.sh" | crontab -'"}]
10:26:21 sent     [create-regular-task(10)->create-crontab-entry(10)->sys.exec(10)]
10:26:21 sent     [create-regular-task(11)->create-crontab-entry(11)->sys.exec(11)]
10:26:27 ended    [create-regular-task(10)->create-crontab-entry(10)->sys.exec(10)]
10:26:27 ended    [create-regular-task(11)->create-crontab-entry(11)->sys.exec(11)]
10:26:27 continue [create-regular-task(6)->create-crontab-entry(6)]
10:26:28 sent     [create-regular-task(6)->create-crontab-entry(6)]
         content  [6] content | Waiting for model response...         content  [6] content | The bash file has been created, made executable, and sent to the remote VM at /tmp/cron.sh. Addition ...
10:26:30 ended    [create-regular-task(6)->create-crontab-entry(6)]
10:26:30 continue [create-regular-task(2)]
10:26:30 sent     [create-regular-task(2)]
         content  [2] content | Waiting for model response...         content  [2] content | The task has been successfully completed. A crontab entry was created on the remote VM to verify eve ...
10:26:33 ended    [create-regular-task(2)]
10:26:33 continue [main]
10:26:33 sent     [main]
         content  [1] content | Waiting for model response...         content  [1] content | The task has been successfully set up to verify every minute if the website https://vote.votingapp.xyz is reachable and to send the status code to the specified HTTP POST request.
10:26:37 ended    [main]

INPUT:

--url https://vote.votingapp.xyz --token 9326bdf257548c4277e8e395a2a772

OUTPUT:

The task has been successfully set up to verify every minute if the website https://vote.votingapp.xyz is reachable and to send the status code to the specified HTTP POST request.


We can then verify that a new VM has been created in our DigitalOcean control panel:

do-cron-vm.png

We can ssh into that VM and verify that a crontab entry has been created:

root@cron:~# crontab -l
* * * * * /tmp/cron.sh

We can see /tmp/cron.sh is executed every minute as requested.

If we have a closer look at its content, we can see a one liner has been generated to perform the actions :

  • a first cURL command is used to get the status code returned when trying to access the URL https://vote.votingapp.xyz
  • the result of the command is piped into another cURL in charge of sending the status code to a webhook
root@cron:~# cat /tmp/cron.sh
curl -o /dev/null -s -w "%{http_code}" https://vote.votingapp.xyz | xargs -I {} curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer 9326bdf257548c4277e8e395a2a772" -d '{"message": "https://vote.votingapp.xyz status code: {}"}' https://webhooks.app/data

We can see the result from https://webhooks.add/dashboard where a new message is received every minute

result.png

Once we are done, we need to delete the DigitalOcean VM from the control panel (note: we could also define another tool to make sure the cleanup is done)

Key takeaways

This blog post illustrates a use case for GPTScript in defining a regular task. Through the definition of specific tools which GPTScript provides to the LLM we made possible the interactions with the API of a cloud provider and the configuration of a virtual machine. If you haven’t already, visit GPTScript.ai to download and install GPTScript.

Related Articles