Using OpenAI ChatGPT Assistants API to Build a Landscape Assistant

After a few years of owning my home, I’m now at the point where I’ve become obsessed with taking care of my yard. I’ve also been wanting to start a flower and vegetable garden. Given the fact that I have a hard time remembering how much water I’ve consumed in a given day, I was terrified that I wouldn’t be able to care for all of my new plant friends correctly.

Enter the beta OpenAI/ChatGPT assistants API. If you haven’t heard about the assistants API just yet, I think it’s pretty awesome in my opinion.

Gardening with OpenAI/ChatGPT assistants API in Ruby

You can create different assistants and give them an instruction set up front so you don’t have to build up conversation context every time, or start the conversation with some huge prompt. My assistant knows everything it needs to know about my property before I ever start a conversation with it. In my instructions I’ve told it all about my property and garden. Things like elevation, latitude, longitude, timezone, etc are super helpful.

After giving it some initial instructions about my property, I started asking it questions like:

  • “what plants should I plant to create a bee and butterfly sanctuaries?”
  • “what vegetables would grow well here in raised beds?”

I purchased and planted based on its recommendations. I then added all the plants I planted and how they are planted to the assistants instructions. For example, in the ground, a container, or a raised bed.

Now that it has all this context about my property and plants upfront I can just ask simple questions like “what other plants should I add in the bee and butterfly sanctuary?”, and it will tell me other plants that will complement what I already have.

I can also ask for watering recommendations and care instructions. It does a pretty good job of offering care recommendations and tips. But what if it could take the weather or other real time date it isn’t trained on into consideration?

This is where function calling comes into play, and it’s pretty amazing.

OpenAI/ChatGPT Function Calling in Ruby

You can describe a function like get_weather and list out all of its parameters. In my example, this function is describing the parameters needed to get the weather timeline from tomorrow.io’s weather API.

{
"name": "get_weather",
"description": "Determine weather in my location",
"parameters": {
  "type": "object",
  "properties": {
    "location": {
      "type": "string",
      "description": "Latitude and longitude separated by a comma"
    },
    "units": {
      "type": "string",
      "description": "Units to display measurements in",
      "enum": [
        "imperial",
        "metric"
      ]
    },
    "fields": {
      "type": "array",
      "description": "Fields is an array of stings for the API to return in it's response",
      "items": {
        "type": "string",
        "enum": [
          "temperature",
          "humidity",
          "windSpeed",
          "windDirection",
          "weatherCode",
          "cloudCover",
          "cloudBase",
          "cloudCeiling",
          "precipitationType",
          "precipitationProbability",
          "dewPoint",
          "temperatureApparent",
          "windGust"
        ]
      }
    },
    "timesteps": {
      "type": "array",
      "description": "array of timestep strings. The timesteps can be current, 1d or 1h. Current for realtime, 1h for hourly, 1d for daily",
      "items": {
        "type": "string",
        "enum": [
          "1h",
          "1d",
          "current"
        ]
      }
    },
    "startTime": {
      "type": "string",
      "description": "Start time can be 'now', 'nowPlusXm/h/d', 'nowMinusXm/h/d' (defaults to now)."
    },
    "endTime": {
      "type": "string",
      "description": "Start time can be 'now', 'nowPlusXm/h/d', 'nowMinusXm/h/d' (defaults to now). Can't be more than 5 days in the future."
    }
  },
  "required": [
    "location",
    "fields"
  ]
}

I have not only told it about how to get the weather, but I’ve also added an Ecowitt weather station and some soil moisture sensors to my garden, so it can get all of that data from the Ecowitt API too.

Now when I send a message like “Do I need to water the plants today?”, the assistants API will send a special event that needs to be handled before it will send a response and will pass all of the functions it wants to call with the necessary parameters for each event.

It’s then up to me to handle implementing and calling these functions in my code passing the results back to the assistant. The assistant will then parse the newly acquired weather data I provided it to make useful recommendations about how to take care of my plants.

It’s so good at picking and sending the correct arguments I can just say something like “is it going to rain between now and 6pm?” and it can send the correct timestep “1h” along with the correct start and end times to the function call 🤯 lol. Here is the code I’m using to handle the function calls:

    def stream_response
      proc do |chunk, _bytesize|
        if chunk["object"] == "thread.message.delta"
          sse.write({text: chunk.dig("delta", "content", 0, "text", "value")})
        elsif chunk.dig("status") == "requires_action"
          handle_tool_calls(chunk)
        end
      end
    end

    def handle_tool_calls(chunk)
      tools_to_call = chunk.dig("required_action", "submit_tool_outputs", "tool_calls")
      my_tool_outputs = tools_to_call.map do |tool|
        function_name = tool.dig("function", "name")
        arguments = tool.dig("function", "arguments")
        Async do
          tool_output = tools.public_send(function_name, arguments)
          {tool_call_id: tool["id"], output: tool_output}
        end
      end.map(&:wait)

      client.runs.submit_tool_outputs(
        thread_id: thread_id,
        run_id: chunk.dig("id"),
        parameters: {
          tool_outputs: my_tool_outputs,
          stream: stream_response
        }
      )
    ensure
      Faraday.default_connection.close
    end

    def tools
      @tools ||= Tools.new
    end

Here is an example function from my Assistants::Tools class

    def get_weather(arguments)
      weather_api_key = Rails.application.credentials.tomorrow_io.dig("api_key")
      url = "https://api.tomorrow.io/v4/timelines?apikey=#{weather_api_key}"
      connection = Faraday.new(url) do |builder|
        builder.adapter :async_http
      end
      headers = {
        "content-type": "application/json"
      }
      Async do
        response = connection.post(url, arguments, headers)
        response.body
      end.wait
    end

Example of the paramaters passed to the get_weather function when I ask “Do I need to take care of anything?”

get weather called
{
  "location": "37.21234,-83.7988",
  "units": "imperial",
  "fields": ["temperature", "humidity", "windSpeed", "precipitationProbability", "weatherCode"],
  "timesteps": ["current"], "startTime": "now"
}

The assistant will call all of the functions it wants to use in parallel. I call the functions asynchronously with async-http-faraday and async. That way, I can call multiple APIs at once vs calling one at a time and waiting for it’s response before calling the next one.

An important note is if the assistant calls multiple functions, you have to pass the results of all functions back to it in one call and then it will return the response.

You aren’t limited to calling APIs and feeding the assistant more data either. I’ve added another function for saving the conversation so I can come back to them. Just tell the assistant to save the conversation if that’s what you want. For example, I have a function that calls a Rails model to persist the conversation title and thread id to the database, and then broadcast a turbo stream to update the UI.

Here you can see how you can ask it simple questions without mentioning any functions and it uses the functions you have defined as it sees fit:

Alt

This has got me thinking about all of the cool possibilities that this technology unlocks. Next steps for me will be building a rain water harvesting system, and then giving the assistant the ability to irrigate the garden automagically with stored rain water when it needs it.

Want to stay in the loop on what we are up to with AI and Ruby? Sign up for our weekly newsletter.