[Custom Component] extended_openai_conversation: Let's control entities via ChatGPT

Overview

This is an attempt to control entities via Open AI.
I have created a custom component called extended_openai_conversation.

As you may assume, it is derived from openai_conversation with a couple of new features added.

Additional Features

  • Ability to call service of Home Assistant
  • Ability to create automation
  • Ability to get data from API or web page

How it works

Extended OpenAI Conversation uses OpenAI API’s feature of function calling to call service of Home Assistant.

Since “gpt-3.5-turbo” model already knows how to call service of Home Assistant in general, you just have to let model know what devices you have by exposing entities

Installation

  1. Install via HACS or by copying extended_openai_conversation folder into <config directory>/custom_components

  2. Restart Home Assistant

  3. Go to Settings > Devices & Services.

  4. In the bottom right corner, select the Add Integration button.

  5. Follow the instructions on screen to complete the setup (API Key is required).

  6. Go to Settings > Voice Assistants.

  7. Click to edit Assistant (named “Home Assistant” by default).

  8. Select “Extended OpenAI Conversation” from “Conversation agent” tab.

    guide image ![스크린샷 2023-10-07 오후 6 15 29](upload://5oo98rYHkF5pnR9N3UEuJBNpLPN.png)
  9. After installed, you need to expose entities from “http://{your-home-assistant}/config/voice-assistants/expose”.

Examples

1. Add to shopping cart

2. Send messages to messenger

3. Add Automation

4. Play Netflix on TV

5. Play Youtube on TV

Configuration

Options

By clicking a button from Edit Assist, Options can be customized.

Options include OpenAI Conversation options and two new options.

  • Maximum Function Calls Per Conversation: limit the number of function calls in a single conversation.
    (Sometimes function is called over and over again, possibly running into infinite loop)
  • Functions: A list of mappings of function spec to function.
    • spec: Function which would be passed to functions of chat API.
    • function: function that will be called.

Functions

Supported function types

  • native: built-in function provided by “extended_openai_conversation”.
    • Currently supported native functions and parameters are:
      • execute_service
        • domain(string): domain to be passed to hass.services.async_call
        • service(string): service to be passed to hass.services.async_call
        • service_data(string): service_data to be passed to hass.services.async_call
      • add_automation
        • automation_config(string): An automation configuration in a yaml format
  • script: A list of services that will be called
  • template: The value to be returned from function.
  • rest: Getting data from REST API endpoint.
  • scrape: Scraping information from website
  • composite: A sequence of functions to execute.

Below is a default configuration of functions.

- spec:
    name: execute_services
    description: Use this function to execute service of devices in Home Assistant.
    parameters:
      type: object
      properties:
        list:
          type: array
          items:
            type: object
            properties:
              domain:
                type: string
                description: The domain of the service
              service:
                type: string
                description: The service to be called
              service_data:
                type: object
                description: The service data object to indicate what to control.
                properties:
                  entity_id:
                    type: string
                    description: The entity_id retrieved from available devices. It must start with domain, followed by dot character.
                required:
                - entity_id
            required:
            - domain
            - service
            - service_data
  function:
    type: native
    name: execute_service

Function Usage

This is an example of configuration of functions.

Copy and paste below yaml configuration into “Functions”.

Then you will be able to let OpenAI call your function.

1. template

1-1. Get current weather

- spec:
    name: get_current_weather
    description: Get the current weather in a given location
    parameters:
      type: object
      properties:
        location:
          type: string
          description: The city and state, e.g. San Francisco, CA
        unit:
          type: string
          enum:
          - celcius
          - farenheit
      required:
      - location
  function:
    type: template
    value_template: The temperature in {{ location }} is 25 {{unit}}

2. script

2-1. Add item to shopping cart

- spec:
    name: add_item_to_shopping_cart
    description: Add item to shopping cart
    parameters:
      type: object
      properties:
        item:
          type: string
          description: The item to be added to cart
      required:
      - item
  function:
    type: script
    sequence:
    - service: shopping_list.add_item
      data:
        name: '{{item}}'

2-2. Send messages to another messenger

In order to accomplish “send it to Line” like example3, register a notify function like below.

- spec:
    name: send_message_to_line
    description: Use this function to send message to Line.
    parameters:
      type: object
      properties:
        message:
          type: string
          description: message you want to send
      required:
      - message
  function:
    type: script
    sequence:
    - service: script.notify_all
      data:
        message: "{{ message }}"

2-3. Get events from calendar

In order to pass result of calling service to OpenAI, set response variable to _function_result.

- spec:
    name: get_events
    description: Use this function to get list of calendar events.
    parameters:
      type: object
      properties:
        start_date_time:
          type: string
          description: The start date time in '%Y-%m-%dT%H:%M:%S%z' format
        end_date_time:
          type: string
          description: The end date time in '%Y-%m-%dT%H:%M:%S%z' format
      required:
      - start_date_time
      - end_date_time
  function:
    type: script
    sequence:
    - service: calendar.list_events
      data:
        start_date_time: "{{start_date_time}}"
        end_date_time: "{{end_date_time}}"
      target:
        entity_id: calendar.test
      response_variable: _function_result

2-4. Play Youtube on TV

- spec:
    name: play_youtube
    description: Use this function to play Youtube.
    parameters:
      type: object
      properties:
        video_id:
          type: string
          description: The video id.
      required:
      - video_id
  function:
    type: script
    sequence:
    - service: webostv.command
      data:
        entity_id: media_player.{YOUR_WEBOSTV}
        command: system.launcher/launch
        payload:
          id: youtube.leanback.v4
          contentId: "{{video_id}}"
    - delay:
        hours: 0
        minutes: 0
        seconds: 10
        milliseconds: 0
    - service: webostv.button
      data:
        entity_id: media_player.{YOUR_WEBOSTV}
        button: ENTER

2-5. Play Netflix on TV

- spec:
    name: play_netflix
    description: Use this function to play Netflix.
    parameters:
      type: object
      properties:
        video_id:
          type: string
          description: The video id.
      required:
      - video_id
  function:
    type: script
    sequence:
    - service: webostv.command
      data:
        entity_id: media_player.{YOUR_WEBOSTV}
        command: system.launcher/launch
        payload:
          id: netflix
          contentId: "m=https://www.netflix.com/watch/{{video_id}}"

3. native

3-1. Add automation

Before adding automation, I highly recommend set notification on automation_registered_via_extended_openai_conversation event and create separate “Extended OpenAI Assistant” and “Assistant”

(Automation can be added even if conversation fails because of failure to get response message, not automation)

Copy and paste below configuration into “Functions”

For English

- spec:
    name: add_automation
    description: Use this function to add an automation in Home Assistant.
    parameters:
      type: object
      properties:
        automation_config:
          type: string
          description: A configuration for automation in a valid yaml format. Next line character should be \n. Use devices from the list.
      required:
      - automation_config
  function:
    type: native
    name: add_automation

For Korean

- spec:
    name: add_automation
    description: Use this function to add an automation in Home Assistant.
    parameters:
      type: object
      properties:
        automation_config:
          type: string
          description: A configuration for automation in a valid yaml format. Next line character should be \\n, not \n. Use devices from the list.
      required:
      - automation_config
  function:
    type: native
    name: add_automation

4. scrape

4-1. Get current HA version

Scrape version from webpage, “https://www.home-assistant.io

Unlike scrape, “value_template” is added at root level in which scraped data from sensors are passed.

scrape:
- spec:
    name: get_ha_version
    description: Use this function to get Home Assistant version
    parameters:
      type: object
      properties:
        dummy:
          type: string
          description: Nothing
  function:
    type: scrape
    resource: https://www.home-assistant.io
    value_template: "version: {{version}}, release_date: {{release_date}}"
    sensor:
      - name: version
        select: ".current-version h1"
        value_template: '{{ value.split(":")[1] }}'
      - name: release_date
        select: ".release-date"
        value_template: '{{ value.lower() }}'

5. rest

5-1. Get friend names

- spec:
    name: get_friend_names
    description: Use this function to get friend_names
    parameters:
      type: object
      properties:
        dummy:
          type: string
          description: Nothing.
  function:
    type: rest
    resource: https://jsonplaceholder.typicode.com/users
    value_template: '{{value_json | map(attribute="name") | list }}'

6. composite

6-1. Search Youtube Music

When using ytube_music_player, after ytube_music_player.search service is called, result is stored in attribute of sensor.ytube_music_player_extra entity.

- spec:
    name: search_music
    description: Use this function to search music
    parameters:
      type: object
      properties:
        query:
          type: string
          description: The query
      required:
      - query
  function:
    type: composite
    sequence:
    - type: script
      sequence:
      - service: ytube_music_player.search
        data:
          entity_id: media_player.ytube_music_player
          query: "{{ query }}"
    - type: template
      value_template: >-
        media_content_type,media_content_id,title
        {% for media in state_attr('sensor.ytube_music_player_extra', 'search') -%}
          {{media.type}},{{media.id}},{{media.title}}
        {% endfor%}

Practical Usage

See more practical examples.

Logging

In order to monitor logs of API requests and responses, add following config to configuration.yaml file

logger:
  logs:
    custom_components.extended_openai_conversation: info
11 Likes

This works great. I tried and it works well with proper prompt and functions. Using it like smarter Google Assistant.

2 Likes

I’m trying this out at the moment. At the last update to the year of the voice I was happy to see Assist be able to control and read states from entities, and it was fun to play around with OpenAI conversation and give Assist a personality, but I was disappointed that we couldn’t combine the two. This bridges the gap and restricts which devices are exposed somewhat.

It does have a bit of weirdness. Occasionally if asking for which lights are on, it will make a service call to do so, but also turn on certain lights. I haven’t been able to narrow down exactly what’s happening here. On the plus side, you can still ask Assist what the json response was, but I’m not always 100% sure it’s not hallucinating. Some tweaking to be done for sure, but an excellent use case of function calls. Very nicely done.

I have a few M5Stack Atom Echoes set up with wake word triggers and it also works pretty well most of the time.

1 Like

This happens to me too when I set model to gpt-3.5-turbo-1106. The default prompt is tested on gpt-3.5-turbo model, and it doesn’t have this issue. Even if I tell gpt-3.5-turbo-1106 not to do this, it doesn’t work.

Oh that’s odd. I’m on gpt-3.5-turbo (which currently points to gpt-3.5-turbo-0613). I’m wondering whether it has to do with the naming of some of my devices, but haven’t been able to consistently reproduce when it does this. I’ll have a tinker and see.

Hey @jekalmin, I love the component. I wanted to share with you that this general functions is working very well:

- spec:
    name: get_attributes
    description: Get attributes of any home assistant entity
    parameters:
      type: object
      properties:
        entity_id:
          type: string
          description: entity_id
      required:
      - entity_id
  function:
    type: template
    value_template: "{{states[entity_id]}}"

You can later for example expose the weather entity and query the chatbot to give you forecast for the upcoming days.

5 Likes

I have used this as function. Can I replace this with yours?

- spec:
    name: get_entity_details
    description: Use this function to get detailed information of specific entity
    parameters:
      type: object
      properties:
        entity_id:
          type: string
          description: Entity ID
      required:
      - entity_id
  function:
    type: template
    value_template: >-
      {% if states[entity_id] %}
      {% set entity_state = states[entity_id].state %}
      {% set attributes = states[entity_id].attributes %}
      ```csv
      key,value
      state,{{ entity_state | replace(",", " ") }}
      {% for key, value in attributes.items() -%}
      {{ key | replace(",", " ") }},{{ value | string | replace(",", " ") }}
      {% endfor -%}
      ```
      {% else %}
      No entity found with the given entity_id,
      {% endif %}

Yes. It should work @sayanova. I haven’t tested this extensively, but it works fine for most of my entities, like: calendar, weather and even some custom components like sensor.feedparser.

1 Like

Omg. This is really simple and concise way to access attributes in entity.

I would love to put this in examples and README of the component.

Thank you very much for sharing the function!
Also, feel free to contribute not only to code but also to examples of prompts or functions.
I would like to collect useful functions like this, so everyone can access them whenever they need.

2 Likes

Is it possible to integrate the custom intent_scripts with Extended OpenAI Conversation?

When I try to use them I receive this response, “I’m sorry, but I don’t have any information on …”.

No, it directly calls service rather than calls service via intent. Could you explain more about your case? You should be able to do the same with supported functions.

Ah thanks for that bit of advice, I didn’t realize this would be possible with the functions. In hindsight that seems pretty obvious. I looked through the docs pretty extensively and I think I have a rough idea of what needs to be done, but I’m not entirely sure I have the technical ability to put it all together.

Use case: I would like to ask “has <my dog> been fed” and receive a response based on the state of a contact sensor. I created a template sensor which determines if he’s been fed, and use the automation below (not an intent script, I forgot I changed it to an automation as that worked better) which responds to tell me if he’s been fed and at what time.

trigger:
  - platform: conversation
    command:
      - Has rudy been fed
      - Is rudy fed
      - rudy fed
      - was rudy fed
      - when was rudy fed
    id: voiceAssistant
- service: tts.google_translate_say

action:
....
                      - service: tts.google_translate_say
                        data:
                          cache: false
                          entity_id: media_player.kitchen_entry
                          language: en-ie
                          message: >
                            {% set feedingTime = (as_timestamp(
                            states.binary_sensor.rudy_foody_contact.last_updated)
                            | timestamp_custom(' %H:%M', true | int)) -%} Yes,
                            Rudy had dinner at {{feedingTime}}

It looks like the easiest way to integrate this with EOC is call a service to trigger the automation, like this:

  function:
    type: native
    name: execute_service
      - service: automation.trigger
        target:
          entity_id: automation.rudy_foody

I’m confused on how to code the “spec”. How can I trigger this function to run when EOC hears one of the commands from my trigger?

I made search_music function with Music Assistant service, ‘mass.search’. It provide spotify uri with Spotify provider, so you can use this function to play music.

- spec:
    name: search_music
    description: search for music.
    parameters:
      type: object
      properties:
        query:
          type: string
          description: search query
        media_type:
          type: array
          optional: true
          items: 
            type: string
            enum: ["artist", "album", "playlist", "track", "radio"]
          description: Types of media to search
        artist:
          type: string
          optional: true
          description: Specify this when you search from a specific artist
        album:
          type: string
          optional: true
          description: Specify this when you search from a specific album
      required:
      - query
  function:
    type: script
    sequence:
    - service: mass.search
      data: >
        {
          "name": "{{ query }}",
          {% if media_type is defined and media_type %}
          "media_type": {{ media_type | tojson }},
          {% else %}
          "media_type": ["artist", "album", "playlist", "track", "radio"],
          {% endif %}
          {% if artist is defined and artist %}
          "artist": "{{ artist }}",
          {% endif %}
          {% if album is defined and album %}
          "album": "{{ album }}",
          {% endif %}
          "limit": 1
        }
      response_variable: _function_result
1 Like

I made search_google function to make Assist more clever.

- spec:
    name: search_google
    description: Search Google using the Custom Search API.
    parameters:
      type: object
      properties:
        query:
          type: string
          description: The search query.
      required:
      - query
  function:
    type: rest
    resource_template: "https://www.googleapis.com/customsearch/v1?key=[GOOGLE_API_KEY]&cx=[GOOGLE_PROGRAMMING_SEARCH_ENGINE]:omuauf_lfve&q={{ query }}&num=3"
    value_template: >-
      {% if value_json.items %}
      ```csv
      title,link
      {% for item in value_json.items %}
      "{{ item.title | replace(',', ' ') }}","{{ item.link }}"
      {% endfor %}
      ```
      {% else %}
      No results found,
      {% endif %}

It makes you search google and retrieve top 3 search result as csv, title and link as column. Enjoy!

1 Like

@francesc0

Understood!

Firstly, since you want to retrieve the state of entity, you don’t have to register any functions.

You can achieve it simply by exposing entity “binary_sensor.rudy_foody_contact”.
Then ask “has Rudy been fed”. There might be two cases.
1. gpt answers based on state of “binary_sensor.rudy_foody_contact” entity. (without feeding time)
2. gpt can’t find entity about rudy fed.

If #2 is the case, try to set aliases on “binary_sensor.rudy_foody_contact” like “rudy fed” and try again.

Secondly, if you want gpt to know “last_updated” of “binary_sensor.rudy_foody_contact” entity, you have to register one of two functions.

1. get_attributes

Since last_updated property is in attributes, let gpt know current attributes of the entity.

- spec:
    name: get_attributes
    description: Get attributes of any home assistant entity
    parameters:
      type: object
      properties:
        entity_id:
          type: string
          description: entity_id
      required:
      - entity_id
  function:
    type: template
    value_template: "{{states[entity_id]}}"

2. query_histories_from_db

If you want gpt to know the past history, add below function. Then you will be able to ask “when was rudy fed yesterday”

- spec:
    name: query_histories_from_db
    description: >-
      Use this function to query histories from Home Assistant SQLite database.
      Example:
        Question: When did bedroom light turn on?
        Answer: SELECT datetime(s.last_updated_ts, 'unixepoch', 'localtime') last_updated_ts FROM states s INNER JOIN states_meta sm ON s.metadata_id = sm.metadata_id INNER JOIN states old ON s.old_state_id = old.state_id WHERE sm.entity_id = 'light.bedroom' AND s.state = 'on' AND s.state != old.state ORDER BY s.last_updated_ts DESC LIMIT 1
        Question: Was livingroom light on at 9 am?
        Answer: SELECT datetime(s.last_updated_ts, 'unixepoch', 'localtime') last_updated, s.state FROM states s INNER JOIN states_meta sm ON s.metadata_id = sm.metadata_id INNER JOIN states old ON s.old_state_id = old.state_id WHERE sm.entity_id = 'switch.livingroom' AND s.state != old.state AND datetime(s.last_updated_ts, 'unixepoch', 'localtime') < '2023-11-17 08:00:00' ORDER BY s.last_updated_ts DESC LIMIT 1
    parameters:
      type: object
      properties:
        query:
          type: string
          description: A fully formed SQL query.
  function:
    type: sqlite
    query: >-
      {%- if is_exposed_entity_in_query(query) -%}
        {{ query }}
      {%- else -%}
        {{ raise("entity_id should be exposed.") }}
      {%- endif -%}
1 Like

@sayanova

Thanks for contributing functions!
If you don’t mind, can I share these functions in examples?

Yes, of course! :grinning:

1 Like

@jekalmin
It’s really special, exactly what I was missing…

I want to play music with a Squeezebox player (Logitech Media Server)
I want to be able to send a command to play an album I want,

Would appreciate help
Thanks

Hi,
if i right, i had to add this function in the openai parameters, below the execute_services function?
When i try this i get this error:

@stban1983 Have you installed Music Assistant (BETA) addon and hass Music Assistant integration, set music provider? I see mass.search not found.