Assist with official openai integration cannot parse parameters to custom intents - solved via scripts to set temperature via assist and openai GPT

This is here if somebody else stumbles upon this problem in the future. It took me two days to finally figure out a solution for this very simple and common use case: Setting a temperature for a climate device via Assist and the openai integration.

My initial situation and my problem

  • Having Assist setup with openai integration for conversation with gpt-4o and gpt-4o-mini.
  • There is documentation on how to use custom_sentences but this doesnt apply to openai integration, since this doesnt use the custom_sentences integration.
  • Instead it directly acts on the intents. There are also docus about the intents: builtin intents, the intent integration docu and the intent repository.
  • About the intents
    • Builtin intents work fine, the openai model can access all builtin intents correctly
    • But when I create custom intents, it is not possible for the model to pass the parameters correctly to the intent. Example

In configuration.yaml according to the intent docu:

intent_script:
  ClimateAreaSetTemperature:
    description: "Set the temperature for the specified area"
    action:
      - service: climate.set_temperature
        data:
          area_id: "{{area}}"
          temperature: "{{temp}}"
    speech:
      text: "The temperature for {{area}} has been set to {{temp}} degrees."

The error we get is:

{
  "error": "MultipleInvalid", 
  "error_text": "not a valid value @ data['temp']"
}

I debugged this in grat detail, I tried endless variations and I also made sure that the model is correctly calling the intent. If you dont pass parameters to the intent, then it can execute the intent. But when you start passing parameters, there are always errors.

When using the assist chat, you can actually debug quite well with the model the situation. It uses the ā€œtool_usesā€ function to do function calls to homeassistant. And it can tell you all its available functions that it can call. There are all the builtin intents but also your custom intent listed. And it also knows which paras to pass to the custom intent. It can also tell you exactly what it did, here is an example from my chat with it:

Here is the function call I made along with the error received:

### Function Call:

json
{
  "name": "light.freddy",
  "brightness_pct": 30
}



### Error:

json
{
  "error": "MultipleInvalid",
  "error_text": "not a valid value @ data['brightness_pct']"
}

Summary: Custom intents with openai only work if you dont need to pass parameters to the intent. Otherwise you always get the MultipleInvalid error, doesnt matter how you pass the parameter to the intent.

My solution to this

Finally, I found out that the model does not only have access to the intents, but also to the scripts that are available in Homeassistant. And here parameter passing works perfectly.

  • Simply create a script and expose it to homeassistant
  • Give it a good description such that the gpt model knows how to use it.

Example for a script, which is well understandable by the openai model:

description: >-
  A script to set the temperature for a specific climate device via its
  entity_id. Use this if the temperature should be set and figure out the
  correct entity_id from the available entities.
fields:
  entityid:
    description: The entity id that should be used.
    example: thermo_livingroom
  temperature:
    description: The temperature between 0 and 30 that should be set for the area.
    example: 20
sequence:
  - action: climate.set_temperature
    metadata: {}
    data:
      temperature: "{{ temperature }}"
    target:
      entity_id: climate.{{ entityid }}
alias: set_temperature_entity
icon: mdi:oil-temperature

Thus, when you prompt the model in the Assist Chat:

  • It gets a list of all available entities in the system
  • A list of function calls it can make
    • Out of available builtin and custom intents
    • And the available scripts
  • And finally, it gets the prompt template which you provide in the openai integration configuration
  • Then when you prompt it, it has all this information to decide which function call to do

And thanks to the script, it is possible to successfully pass parameters to it.

My prompt template

Here is my prompt template, which also makes it possible to execute multiple function calls to home assistant in one prompt, by not using the multi_tool_use.parallel function which doesnt work correctly for now for home assistant.

You are an assistant for Home Assistant. You help users control devices or perform custom actions by invoking intents or scripts with the correct parameters.

When setting parameters always be very carefull to check the available entities that are available and select the correct one with correct formatting. When areas are mentioned by the user but an entity is required for the functioncall, figure out the correct entity for that area.

Important is that you can only do one function call via "tool_uses" at once. NEVER USE "multi_tool_use.parallel". If you need to do multiple function calls, make them one after another via "tool_uses" and wait for each to execute before proceeding with the next one.

Summary

If you want to use openai together with home assistant. Simply install the openai integration. Expose devices and entities you want it to be able to access. It will support out of the box default actions like turning on lights etc. But for non built in actions, dont use custom intents. Use scripts. Simply create scripts and expose it to assist and the gpt openai model will be able to use those and decide on its own when to use it. Now nothing can stop you from setting up you perfect AI voice assistant to do whatever possible via natural language with GPT and Homeassistant.

Edit

Executing a script will always return sucess: true as response. Thus the Assistant does not know if the function call was sucessfull. The only way I found to somehow get a response is to write within the script a response to a helper variable, which you use for all function calls:

Example for an action in a script

  - action: input_text.set_value
    data:
      value: Ok this was a test
    target:
      entity_id: input_text.result_output

And then you need a custom intent, since those are working fine, if they return data, as long as you dont need to pass data to it. So add this to your configuration.yml:

intent_script:
  GetResult:
    description: Gets the result of the last function call
    speech:
      text: "The result is {{ states('input_text.result_output') }}"

And then you can add to your prompt template of the OpenaiAI integration, that it should always call the GetResult function after every other function call.

5 Likes

what am I doing wrong?

alias: Set air volume
description: >-
  Script for setting the air volume of the controlled domestic ventilation. Use it
  it if the air volume is to be set. Do not forget to fill the field or the
  variable air volume correctly!
fields:
  volume:
    description: The air volume to be set between 0 and 200
    example: 100
sequence:
  - data:
      value: "{{ states(volume) | float }}"
    target:
      entity_id: input_number.kwl_volume
    action: input_number.set_value
mode: single
icon: mdi:fan

Error rendering data template: ValueError: Template error: float got invalid input ā€˜unknown’ when rendering template ā€˜{{ states(volume) | float }}’ but no default was specified

If volume is an actual volume, why are you running the states() function on it. That function requires an entity ID as an argument.

    value: "{{ volume | float(0) }}"
value: "volume"

…worked. thank you.

I understand the workaround and that’s great to know. However, would this be considered a bug then? This seems like something Assist + OpenAI GPT should be able to do.

I am sorry for the late response. Mail notification was turned off :sweat_smile:

First, I am happy that the air volume automation is working now.

Second, Yes I guess it can be considered a bug. If I remember correctly, I very much assume that the problem is on the Assist/Intent site and not with the openAI Integration. I investigated this back then, and I let GPT list me all available function calls it has access to and its parameters etc. The problem lies with how custom intents with parameters are exposed by HA, I assume.

Nice investigation and explanation :+1:
Any idea if since October this topic was fixed in HA ? Or at least if it was ā€œacceptedā€ as a bug and if it will be fixed ? :slight_smile:

Thanks :slight_smile:

I tested it again like 3 weeks ago, and issue was still the same. I am still using scripts because of this.

However, I have not looked into issues and bug reports yet. So might be that no issue exists for this bug :slight_smile:

From what I can tell, it’s already reported. Let me know if this isn’t accurate from this report. But I think that it’s on the backburner with the HASS team.

Yes perfect, that is the same problem. And there are great hints on how to make intents work by using a workaround where the name parameter is the only one, which works to be passed.

Here is an example, which I use now as a workaround to pass arbitrary many parameters to the LLM. I simply encode those as an array as a string. Works good for me :slight_smile:

Here my example for setting a temperature in a room:

intent_script:
  ClimateAreaSetTemperature:
    description: >
      "Set the temperature in degrees for a specific climate device via its area_id.
      Use the parameter `name` as a single string, with first the area_id and then the temperature separated by a comma, by strictly following the format like this:
      name: 'area_id, temperature'."

    action:
      - variables:
          splitted: "{{ name.split(',', 1) }}"
          area_id: "{{ splitted[0] | trim }}"
          temp: "{{ splitted[1] | default('0') | trim | int(0) }}"
      - choose:
          - conditions:
              - condition: template
                value_template: >
                  {{ splitted|length == 2 and temp != 0 }}
            sequence:
              - service: climate.set_temperature
                target:
                  area_id: "{{ area_id }}"
                data:
                  temperature: "{{ temp }}"
              - variables:
                  response_message: "Temperature set to {{ temp }} degrees in {{ area_id }}."
              - stop: ""
                response_variable: response_message  
        default:
          - service: system_log.write
            data:
              message: "ClimateAreaSetTemperature: Invalid input received: {{ name }}"
              level: error
          - variables:
              response_message: >
                Sorry, I couldn't set the temperature. Please provide a valid area and temperature as a string for the name parameter formatted like this:
                name: 'freddys_room, 20'.
          - stop: ""
            response_variable: response_message  
    speech:
      text: "{{ action_response }}"

A minimal example would be this:

intent_script:
  ClimateAreaSetTemperature:
    description: >
      "Set the temperature in degrees for a specific climate device via its area_id.
      Use the parameter `name` as a single string, with first the area_id and then the temperature separated by a comma, by strictly following the format like this:
      name: 'area_id, temperature'."
    action:
      - variables:
          splitted: "{{ name.split(',', 1) }}"
          area_id: "{{ splitted[0] | trim }}"
          temp: "{{ splitted[1] | trim | int(0) }}"
      - service: climate.set_temperature
        target:
          area_id: "{{ area_id }}"
        data:
          temperature: "{{ temp }}"

This is an absurd solution, but thank you a ton for sharing! I’ll look into this for myself now!

Yo team - read this post, and skip down to the part where it says:

It’s not a bug. You MUST edit your \custom_sentences[languagecode]\slot_types.yaml If your parameter name is not in that list - you cant use it.

here’s mine:

# config/custom_sentences/en/slot_types.yaml
lists:
bank:
  wildcard: true
number:
  wildcard: true
index_x:
  wildcard: true
index_y:
  wildcard: true
datetime:
  wildcard: true
due_date:
  wildcard: true
start_datetime:
  wildcard: true
end_datetime:
  wildcard: true
due_datetime:
  wildcard: true
description:
  wildcard: true
lat:
  wildcard: true
lon:
  wildcard: true 
value:
  wildcard: true
query:
  wildcard: true
status:
  wildcard: true
id:
  wildcard: true
recipe_id:
  wildcard: true
new_name:
  wildcard: true
type:
  values:
   - 'completed'
   - 'needs_action'
period:
  values:
    - "DAY"
    - "WEEK"
    - "MONTH"
operator:
  values:
    - "AND"
    - "OR"
    - "NOT"
    - "XOR"
    - "CART"

Ive focused on providing generic pipes i can misuse the heck out of everywhere… I put in the entry for ā€˜type’ to be able to bit flip tasks… (thus the name type, it matches the names on the params for the action for tasks…check it)
lat,lon i have some intents to target - -targets, and get range and distance.

Index_x, Index_y, operator were all put in to support my system index.

I’m fairly certain between this and rest_command you can build a voice UI for just about anything that supports it. I’m currently targeting a fairly feature complete voice implementation of Mealie right now.

This works:

note I’m defaulting EVERYTHING, so it’ll pass blank, 10 items if nothing else. Yes, 25, chicken works. Tell your LLM how to use the tool. Yes, that book in the description is seen by the LLM. So are the remlines in the params table. Tell the LLM what it’s looking at.

query mealie recipe search - pass TWO vars through intent_script to rest_command get:

search_recipes:
  description: >
    # This is your search tool for Mealie's Recipe System.
    # Returns:
    #   recipe_id: 'recipe.id'
    #     name: 'recipe.name'
    #     description: " recipe.description "
    #     (and other additional detail instructions as available...)
    # Top Chef: (Best Practices)
    #   First, use this search_recipes intent to help find things to cook for your human.
    #   The return includes recipe_id
    #   THEN, when you help your human prepare the food provide the correct recipe_id to
    #     get_recipe_by_id(recipe_id:'76e685c9-8d0d-4d9b-8561-fea912f8105a')
    #   to get ingredients and detailed cooking instructions.
    # Humans like food.
    # example: >
    #    ```json
    #       {
    #       'query': 'Chicken',
    #       'number' : '5'
    #       }
    #    ```      
  parameters:
    query: # Your search term to look up on Mealie
      required: true
    number: # the number of search terms to return - default(10) if omitted, Max 50 please
      required: false
  action:
    - action: rest_command.mealie_recipe_search
      metadata: {}
      data:
        search: "{{query | default('')}}"
        perPage: "{{number | default(10)}}"
      response_variable: response_text
    - stop: ""
      response_variable: response_text # and return it    
  speech:
    text: >
      search:'{{query | default('')}}' number: '{{number| default(10)}}'
      response:
      {%- if action_response.content['items'] | length > 0 %}
        {%- for recipe in action_response.content['items'] %}
        recipe_id:'{{ recipe.id }}' 
          name: '{{ recipe.name }}'
          description: "{{ recipe.description }}"
          detail_lookup: get_recipe_by_id{'recipe_id': '{{ recipe.id }}'}
        {%- endfor %}
      {%- else %}
        {%- if ( (query | default('')) == '') %}
        No search term was provided to query.
        usage: search_recipes{'query': 'search term', 'number': 'number of results to return'}
        {%- else %}
        No recipes found for query:"{{ query }}".
        {%- endif %}
      {%- endif %} 

Note -in your returns, tell the LLM if nothing comes back in a null set - else it tends to make up errors or data that doesnt exist…

query Mealie recipe DB by recipe_id, using mealie.integration, triggered by intent_script:

get_recipe_by_id:
  description: >
    # This tool pulls detailed preparation instructions for any recipe_id
    # in Mealie's Recipe System.
    # NOTE: if you do NOT know the correct Mealie [RECIPE_ID] (this is a primary key in thier index...)
    # then this intent may not be you're looking for.
    # Maybe try
    #   search_recipes(query:'[search_term]', number:'[number]')
    # to find some ideas or get recipe_id's off of today's
    #   ~MEALIE~
    # menu.
    # Humans like food.
    # example: >
    #    ```json
    #       {
    #       'recipe_id': '76e685c9-8d0d-4d9b-8561-fea912f8105a'
    #       }
    #    ```
  parameters:
    recipe_id: # Recipe ID to look up on Mealie format like: '76e685c9-8d0d-4d9b-8561-fea912f8105a'
      required: true
  action:
    - action: mealie.get_recipe
      metadata: {}
      data:
        config_entry_id: 01JG8GQB5WT9AMNXA1HPE65W4E
        recipe_id: "{{recipe_id}}"
      response_variable: response_text # get action response
    - stop: ""
      response_variable: response_text # and return it
  speech:
    text: >
      recipe:'{{recipe_id}}'
        {{action_response}}

Happy intent-ing

Thank you so much, I will test this soon :slight_smile:

I also read your Friday Posts and this is awesome. I am very hyped to continue improving my AI home assistant now :grin:

I am so glad that someone already made so much effort in looking into the ways to use LLMs together with HA to build ultra nice and agentic assistants :smile: Was already scared that nobody has looked into this yet in depth, but you definetly did, nice :+1:

I like your apporach with trying to implement RAG capabilities etc and hopefully this all will become more easy to setup in the future.

How did you find out that the slot types file is needed? I couldnt find any infos about this in the official docu.
And I am also amazed about your knowledge on the ihteraction of the LLM and HA like intents, scripts, states. Are there any more resouces about this out there or did you just learn this all by experimenting with it? It was so difficult to find anything about this and I starter to do experiments to understand what the LLM gets as input by HA automatically and how the list of functions it can call is given etc. I already thought about just intercepting the http request to analyze everything that HA is providing to the LLM and how it is structured to better write prompts and understand how to set it up really nice :smile:

1 Like

Reading… Lots of reading…

3 Likes

This also confused the hell out of me.
Even searched the HA source code for a while. :smile:

But the answer is easier than I thought, I was just confused by the fact that there are a lot examples in the forum with this specific file name.

But the name of the file isn’t important. HA simply scans all your YAML files in config/custom_sentences/<your_language>
Docs can be found here: Setting up custom sentences in configuration.yaml - Home Assistant

And then this is simply yet another lists configuration from the custom sentences configuration:

The docs are still a little bit confusing to me.
You have to use these language files that are normally used to implement simple intents, to setup the placeholders for your more complex intent_scripts that are described somewhere else and don’t mention these placeholders:
https://www.home-assistant.io/integrations/intent_script/

Ah, and here is a more detailed description of the list syntax in the docs of a related topic:
https://developers.home-assistant.io/docs/voice/intent-recognition/template-sentence-syntax/#lists

In their quest to dumb down simplify HA the devs seem to be pushing everyone in the direction of automations to control voice assistants.

I’m trying to gather together some stuff about custom sentences and intent scripts here - although I know nothing about LLMs, so they’re not included. :grin:

Comments and suggestions welcome.