├── .github └── workflows │ ├── hassfest.yaml │ └── validate.yaml ├── README.md ├── custom_components └── extended_openai_conversation │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── exceptions.py │ ├── helpers.py │ ├── manifest.json │ ├── services.py │ ├── services.yaml │ ├── strings.json │ └── translations │ ├── de.json │ ├── en.json │ ├── fr.json │ ├── hu.json │ ├── it.json │ ├── ko.json │ ├── nl.json │ ├── pl.json │ └── pt-BR.json ├── examples ├── component_function │ ├── 17track │ │ ├── README.ko.md │ │ └── README.md │ ├── grocy │ │ └── README.md │ ├── o365 │ │ └── README.md │ └── ytube_music_player │ │ └── README.md ├── function │ ├── area │ │ └── README.md │ ├── attributes │ │ └── README.md │ ├── automation │ │ ├── README.ko.md │ │ └── README.md │ ├── calendar │ │ └── README.md │ ├── camera_image_query │ │ └── README.md │ ├── energy │ │ └── README.md │ ├── google_search │ │ └── README.md │ ├── history │ │ └── README.md │ ├── kakao_bus │ │ └── README.md │ ├── netflix │ │ └── README.md │ ├── notify │ │ └── README.md │ ├── plex │ │ └── README.md │ ├── say_tts │ │ └── README.md │ ├── shopping_list │ │ └── README.md │ ├── sqlite │ │ └── README.md │ ├── user_id_to_user │ │ └── README.md │ ├── weather │ │ └── README.md │ └── youtube │ │ └── README.md └── prompt │ ├── annoying │ └── README.md │ ├── area │ └── README.md │ ├── default │ └── README.md │ └── with_attributes │ └── README.md └── hacs.json /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v4" 14 | - uses: "home-assistant/actions/hassfest@master" -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | validate-hacs: 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - uses: "actions/checkout@v3" 15 | - name: HACS validation 16 | uses: "hacs/action@main" 17 | with: 18 | category: "integration" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extended OpenAI Conversation 2 | This is custom component of Home Assistant. 3 | 4 | Derived from [OpenAI Conversation](https://www.home-assistant.io/integrations/openai_conversation/) with some new features such as call-service. 5 | 6 | ## Additional Features 7 | - Ability to call service of Home Assistant 8 | - Ability to create automation 9 | - Ability to get data from external API or web page 10 | - Ability to retrieve state history of entities 11 | - Option to pass the current user's name to OpenAI via the user message context 12 | 13 | ## How it works 14 | Extended OpenAI Conversation uses OpenAI API's feature of [function calling](https://platform.openai.com/docs/guides/function-calling) to call service of Home Assistant. 15 | 16 | Since OpenAI models already know how to call service of Home Assistant in general, you just have to let model know what devices you have by [exposing entities](https://github.com/jekalmin/extended_openai_conversation#preparation) 17 | 18 | ## Installation 19 | 1. Install via registering as a custom repository of HACS or by copying `extended_openai_conversation` folder into `/custom_components` 20 | 2. Restart Home Assistant 21 | 3. Go to Settings > Devices & Services. 22 | 4. In the bottom right corner, select the Add Integration button. 23 | 5. Follow the instructions on screen to complete the setup (API Key is required). 24 | - [Generating an API Key](https://www.home-assistant.io/integrations/openai_conversation/#generate-an-api-key) 25 | - Specify "Base Url" if using OpenAI compatible servers like Azure OpenAI (also with APIM), LocalAI, otherwise leave as it is. 26 | 6. Go to Settings > [Voice Assistants](https://my.home-assistant.io/redirect/voice_assistants/). 27 | 7. Click to edit Assistant (named "Home Assistant" by default). 28 | 8. Select "Extended OpenAI Conversation" from "Conversation agent" tab. 29 |
30 | 31 | guide image 32 | 스크린샷 2023-10-07 오후 6 15 29 33 | 34 |
35 | 36 | ## Preparation 37 | After installed, you need to expose entities from "http://{your-home-assistant}/config/voice-assistants/expose". 38 | 39 | ## Examples 40 | ### 1. Turn on single entity 41 | https://github.com/jekalmin/extended_openai_conversation/assets/2917984/938dee95-8907-44fd-9fb8-dc8cd559fea2 42 | 43 | ### 2. Turn on multiple entities 44 | https://github.com/jekalmin/extended_openai_conversation/assets/2917984/528f5965-94a7-4cbe-908a-e24f7bbb0a93 45 | 46 | ### 3. Hook with custom notify function 47 | https://github.com/jekalmin/extended_openai_conversation/assets/2917984/4a575ee7-0188-41eb-b2db-6eab61499a99 48 | 49 | ### 4. Add automation 50 | https://github.com/jekalmin/extended_openai_conversation/assets/2917984/04b93aa6-085e-450a-a554-34c1ed1fbb36 51 | 52 | ### 5. Play Netflix 53 | https://github.com/jekalmin/extended_openai_conversation/assets/2917984/64ba656e-3ae7-4003-9956-da71efaf06dc 54 | 55 | ## Configuration 56 | ### Options 57 | By clicking a button from Edit Assist, Options can be customized.
58 | Options include [OpenAI Conversation](https://www.home-assistant.io/integrations/openai_conversation/) options and two new options. 59 | 60 | - `Attach Username`: Pass the active user's name (if applicable) to OpenAI via the message payload. Currently, this only applies to conversations through the UI or REST API. 61 | 62 | - `Maximum Function Calls Per Conversation`: limit the number of function calls in a single conversation. 63 | (Sometimes function is called over and over again, possibly running into infinite loop) 64 | - `Functions`: A list of mappings of function spec to function. 65 | - `spec`: Function which would be passed to [functions](https://platform.openai.com/docs/api-reference/chat/create#chat-create-functions) of [chat API](https://platform.openai.com/docs/api-reference/chat/create). 66 | - `function`: function that will be called. 67 | 68 | 69 | | Edit Assist | Options | 70 | |----------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 71 | | 1 | 스크린샷 2023-10-10 오후 10 53 57 | 72 | 73 | 74 | ### Functions 75 | 76 | #### Supported function types 77 | - `native`: built-in function provided by "extended_openai_conversation". 78 | - Currently supported native functions and parameters are: 79 | - `execute_service` 80 | - `domain`(string): domain to be passed to `hass.services.async_call` 81 | - `service`(string): service to be passed to `hass.services.async_call` 82 | - `service_data`(object): service_data to be passed to `hass.services.async_call`. 83 | - `entity_id`(string): target entity 84 | - `device_id`(string): target device 85 | - `area_id`(string): target area 86 | - `add_automation` 87 | - `automation_config`(string): An automation configuration in a yaml format 88 | - `get_history` 89 | - `entity_ids`(list): a list of entity ids to filter 90 | - `start_time`(string): defaults to 1 day before the time of the request. It determines the beginning of the period 91 | - `end_time`(string): the end of the period in URL encoded format (defaults to 1 day) 92 | - `minimal_response`(boolean): only return last_changed and state for states other than the first and last state (defaults to true) 93 | - `no_attributes`(boolean): skip returning attributes from the database (defaults to true) 94 | - `significant_changes_only`(boolean): only return significant state changes (defaults to true) 95 | - `script`: A list of services that will be called 96 | - `template`: The value to be returned from function. 97 | - `rest`: Getting data from REST API endpoint. 98 | - `scrape`: Scraping information from website 99 | - `composite`: A sequence of functions to execute. 100 | 101 | Below is a default configuration of functions. 102 | 103 | ```yaml 104 | - spec: 105 | name: execute_services 106 | description: Use this function to execute service of devices in Home Assistant. 107 | parameters: 108 | type: object 109 | properties: 110 | list: 111 | type: array 112 | items: 113 | type: object 114 | properties: 115 | domain: 116 | type: string 117 | description: The domain of the service 118 | service: 119 | type: string 120 | description: The service to be called 121 | service_data: 122 | type: object 123 | description: The service data object to indicate what to control. 124 | properties: 125 | entity_id: 126 | type: string 127 | description: The entity_id retrieved from available devices. It must start with domain, followed by dot character. 128 | required: 129 | - entity_id 130 | required: 131 | - domain 132 | - service 133 | - service_data 134 | function: 135 | type: native 136 | name: execute_service 137 | ``` 138 | 139 | ## Function Usage 140 | This is an example of configuration of functions. 141 | 142 | Copy and paste below yaml configuration into "Functions".
143 | Then you will be able to let OpenAI call your function. 144 | 145 | ### 1. template 146 | #### 1-1. Get current weather 147 | 148 | For real world example, see [weather](https://github.com/jekalmin/extended_openai_conversation/tree/main/examples/function/weather).
149 | This is just an example from [OpenAI documentation](https://platform.openai.com/docs/guides/function-calling/common-use-cases) 150 | 151 | ```yaml 152 | - spec: 153 | name: get_current_weather 154 | description: Get the current weather in a given location 155 | parameters: 156 | type: object 157 | properties: 158 | location: 159 | type: string 160 | description: The city and state, e.g. San Francisco, CA 161 | unit: 162 | type: string 163 | enum: 164 | - celcius 165 | - farenheit 166 | required: 167 | - location 168 | function: 169 | type: template 170 | value_template: The temperature in {{ location }} is 25 {{unit}} 171 | ``` 172 | 173 | 스크린샷 2023-10-07 오후 7 56 27 174 | 175 | ### 2. script 176 | #### 2-1. Add item to shopping cart 177 | ```yaml 178 | - spec: 179 | name: add_item_to_shopping_cart 180 | description: Add item to shopping cart 181 | parameters: 182 | type: object 183 | properties: 184 | item: 185 | type: string 186 | description: The item to be added to cart 187 | required: 188 | - item 189 | function: 190 | type: script 191 | sequence: 192 | - service: shopping_list.add_item 193 | data: 194 | name: '{{item}}' 195 | ``` 196 | 197 | 스크린샷 2023-10-07 오후 7 54 56 198 | 199 | #### 2-2. Send messages to another messenger 200 | 201 | In order to accomplish "send it to Line" like [example3](https://github.com/jekalmin/extended_openai_conversation#3-hook-with-custom-notify-function), register a notify function like below. 202 | 203 | ```yaml 204 | - spec: 205 | name: send_message_to_line 206 | description: Use this function to send message to Line. 207 | parameters: 208 | type: object 209 | properties: 210 | message: 211 | type: string 212 | description: message you want to send 213 | required: 214 | - message 215 | function: 216 | type: script 217 | sequence: 218 | - service: script.notify_all 219 | data: 220 | message: "{{ message }}" 221 | ``` 222 | 223 | 224 | 225 | #### 2-3. Get events from calendar 226 | 227 | In order to pass result of calling service to OpenAI, set response variable to `_function_result`. 228 | 229 | ```yaml 230 | - spec: 231 | name: get_events 232 | description: Use this function to get list of calendar events. 233 | parameters: 234 | type: object 235 | properties: 236 | start_date_time: 237 | type: string 238 | description: The start date time in '%Y-%m-%dT%H:%M:%S%z' format 239 | end_date_time: 240 | type: string 241 | description: The end date time in '%Y-%m-%dT%H:%M:%S%z' format 242 | required: 243 | - start_date_time 244 | - end_date_time 245 | function: 246 | type: script 247 | sequence: 248 | - service: calendar.get_events 249 | data: 250 | start_date_time: "{{start_date_time}}" 251 | end_date_time: "{{end_date_time}}" 252 | target: 253 | entity_id: 254 | - calendar.[YourCalendarHere] 255 | - calendar.[MoreCalendarsArePossible] 256 | response_variable: _function_result 257 | ``` 258 | 259 | 스크린샷 2023-10-31 오후 9 04 56 260 | 261 | #### 2-4. Play Youtube on TV 262 | 263 | ```yaml 264 | - spec: 265 | name: play_youtube 266 | description: Use this function to play Youtube. 267 | parameters: 268 | type: object 269 | properties: 270 | video_id: 271 | type: string 272 | description: The video id. 273 | required: 274 | - video_id 275 | function: 276 | type: script 277 | sequence: 278 | - service: webostv.command 279 | data: 280 | entity_id: media_player.{YOUR_WEBOSTV} 281 | command: system.launcher/launch 282 | payload: 283 | id: youtube.leanback.v4 284 | contentId: "{{video_id}}" 285 | - delay: 286 | hours: 0 287 | minutes: 0 288 | seconds: 10 289 | milliseconds: 0 290 | - service: webostv.button 291 | data: 292 | entity_id: media_player.{YOUR_WEBOSTV} 293 | button: ENTER 294 | ``` 295 | 296 | 297 | 298 | #### 2-5. Play Netflix on TV 299 | 300 | ```yaml 301 | - spec: 302 | name: play_netflix 303 | description: Use this function to play Netflix. 304 | parameters: 305 | type: object 306 | properties: 307 | video_id: 308 | type: string 309 | description: The video id. 310 | required: 311 | - video_id 312 | function: 313 | type: script 314 | sequence: 315 | - service: webostv.command 316 | data: 317 | entity_id: media_player.{YOUR_WEBOSTV} 318 | command: system.launcher/launch 319 | payload: 320 | id: netflix 321 | contentId: "m=https://www.netflix.com/watch/{{video_id}}" 322 | ``` 323 | 324 | 325 | 326 | ### 3. native 327 | 328 | #### 3-1. Add automation 329 | 330 | Before adding automation, I highly recommend set notification on `automation_registered_via_extended_openai_conversation` event and create separate "Extended OpenAI Assistant" and "Assistant" 331 | 332 | (Automation can be added even if conversation fails because of failure to get response message, not automation) 333 | 334 | | Create Assistant | Notify on created | 335 | |----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 336 | | 1 | 스크린샷 2023-10-13 오후 6 01 40 | 337 | 338 | 339 | Copy and paste below configuration into "Functions" 340 | 341 | **For English** 342 | ```yaml 343 | - spec: 344 | name: add_automation 345 | description: Use this function to add an automation in Home Assistant. 346 | parameters: 347 | type: object 348 | properties: 349 | automation_config: 350 | type: string 351 | description: A configuration for automation in a valid yaml format. Next line character should be \n. Use devices from the list. 352 | required: 353 | - automation_config 354 | function: 355 | type: native 356 | name: add_automation 357 | ``` 358 | 359 | **For Korean** 360 | ```yaml 361 | - spec: 362 | name: add_automation 363 | description: Use this function to add an automation in Home Assistant. 364 | parameters: 365 | type: object 366 | properties: 367 | automation_config: 368 | type: string 369 | description: A configuration for automation in a valid yaml format. Next line character should be \\n, not \n. Use devices from the list. 370 | required: 371 | - automation_config 372 | function: 373 | type: native 374 | name: add_automation 375 | ``` 376 | 377 | 스크린샷 2023-10-31 오후 9 32 27 378 | 379 | #### 3-2. Get History 380 | Get state history of entities 381 | 382 | ```yaml 383 | - spec: 384 | name: get_history 385 | description: Retrieve historical data of specified entities. 386 | parameters: 387 | type: object 388 | properties: 389 | entity_ids: 390 | type: array 391 | items: 392 | type: string 393 | description: The entity id to filter. 394 | start_time: 395 | type: string 396 | description: Start of the history period in "%Y-%m-%dT%H:%M:%S%z". 397 | end_time: 398 | type: string 399 | description: End of the history period in "%Y-%m-%dT%H:%M:%S%z". 400 | required: 401 | - entity_ids 402 | function: 403 | type: composite 404 | sequence: 405 | - type: native 406 | name: get_history 407 | response_variable: history_result 408 | - type: template 409 | value_template: >- 410 | {% set ns = namespace(result = [], list = []) %} 411 | {% for item_list in history_result %} 412 | {% set ns.list = [] %} 413 | {% for item in item_list %} 414 | {% set last_changed = item.last_changed | as_timestamp | timestamp_local if item.last_changed else None %} 415 | {% set new_item = dict(item, last_changed=last_changed) %} 416 | {% set ns.list = ns.list + [new_item] %} 417 | {% endfor %} 418 | {% set ns.result = ns.result + [ns.list] %} 419 | {% endfor %} 420 | {{ ns.result }} 421 | ``` 422 | 423 | 424 | 425 | ### 4. scrape 426 | #### 4-1. Get current HA version 427 | Scrape version from webpage, "https://www.home-assistant.io" 428 | 429 | Unlike [scrape](https://www.home-assistant.io/integrations/scrape/), "value_template" is added at root level in which scraped data from sensors are passed. 430 | 431 | ```yaml 432 | - spec: 433 | name: get_ha_version 434 | description: Use this function to get Home Assistant version 435 | parameters: 436 | type: object 437 | properties: 438 | dummy: 439 | type: string 440 | description: Nothing 441 | function: 442 | type: scrape 443 | resource: https://www.home-assistant.io 444 | value_template: "version: {{version}}, release_date: {{release_date}}" 445 | sensor: 446 | - name: version 447 | select: ".current-version h1" 448 | value_template: '{{ value.split(":")[1] }}' 449 | - name: release_date 450 | select: ".release-date" 451 | value_template: '{{ value.lower() }}' 452 | ``` 453 | 454 | 스크린샷 2023-10-31 오후 9 46 07 455 | 456 | ### 5. rest 457 | #### 5-1. Get friend names 458 | - Sample URL: https://jsonplaceholder.typicode.com/users 459 | ```yaml 460 | - spec: 461 | name: get_friend_names 462 | description: Use this function to get friend_names 463 | parameters: 464 | type: object 465 | properties: 466 | dummy: 467 | type: string 468 | description: Nothing. 469 | function: 470 | type: rest 471 | resource: https://jsonplaceholder.typicode.com/users 472 | value_template: '{{value_json | map(attribute="name") | list }}' 473 | ``` 474 | 475 | 스크린샷 2023-10-31 오후 9 48 36 476 | 477 | 478 | ### 6. composite 479 | #### 6-1. Search Youtube Music 480 | When using [ytube_music_player](https://github.com/KoljaWindeler/ytube_music_player), after `ytube_music_player.search` service is called, result is stored in attribute of `sensor.ytube_music_player_extra` entity.
481 | 482 | 483 | ```yaml 484 | - spec: 485 | name: search_music 486 | description: Use this function to search music 487 | parameters: 488 | type: object 489 | properties: 490 | query: 491 | type: string 492 | description: The query 493 | required: 494 | - query 495 | function: 496 | type: composite 497 | sequence: 498 | - type: script 499 | sequence: 500 | - service: ytube_music_player.search 501 | data: 502 | entity_id: media_player.ytube_music_player 503 | query: "{{ query }}" 504 | - type: template 505 | value_template: >- 506 | media_content_type,media_content_id,title 507 | {% for media in state_attr('sensor.ytube_music_player_extra', 'search') -%} 508 | {{media.type}},{{media.id}},{{media.title}} 509 | {% endfor%} 510 | ``` 511 | 512 | 스크린샷 2023-11-02 오후 8 40 36 513 | 514 | ### 7. sqlite 515 | #### 7-1. Let model generate a query 516 | - Without examples, a query tries to fetch data only from "states" table like below 517 | > Question: When did bedroom light turn on?
518 | Query(generated by gpt): SELECT * FROM states WHERE entity_id = 'input_boolean.livingroom_light_2' AND state = 'on' ORDER BY last_changed DESC LIMIT 1 519 | - Since "entity_id" is stored in "states_meta" table, we need to give examples of question and query. 520 | - Not secured, but flexible way 521 | 522 | ```yaml 523 | - spec: 524 | name: query_histories_from_db 525 | description: >- 526 | Use this function to query histories from Home Assistant SQLite database. 527 | Example: 528 | Question: When did bedroom light turn on? 529 | 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 530 | Question: Was livingroom light on at 9 am? 531 | 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 532 | parameters: 533 | type: object 534 | properties: 535 | query: 536 | type: string 537 | description: A fully formed SQL query. 538 | function: 539 | type: sqlite 540 | ``` 541 | 542 | Get last changed date time of state | Get state at specific time 543 | --|-- 544 | 스크린샷 2023-11-19 오후 5 32 56 |스크린샷 2023-11-19 오후 5 32 30 545 | 546 | 547 | **FAQ** 548 | 1. Can gpt modify or delete data? 549 | > No, since connection is created in a read only mode, data are only used for fetching. 550 | 2. Can gpt query data that are not exposed in database? 551 | > Yes, it is hard to validate whether a query is only using exposed entities. 552 | 3. Query uses UTC time. Is there any way to adjust timezone? 553 | > Yes. Set "TZ" environment variable to your [region](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (eg. `Asia/Seoul`).
554 | Or use plus/minus hours to adjust instead of 'localtime' (eg. `datetime(s.last_updated_ts, 'unixepoch', '+9 hours')`). 555 | 556 | 557 | #### 7-2. Let model generate a query (with minimum validation) 558 | - If need to check at least "entity_id" of exposed entities is present in a query, use "is_exposed_entity_in_query" in combination with "raise". 559 | - Not secured enough, but flexible way 560 | ```yaml 561 | - spec: 562 | name: query_histories_from_db 563 | description: >- 564 | Use this function to query histories from Home Assistant SQLite database. 565 | Example: 566 | Question: When did bedroom light turn on? 567 | 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 568 | Question: Was livingroom light on at 9 am? 569 | 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 570 | parameters: 571 | type: object 572 | properties: 573 | query: 574 | type: string 575 | description: A fully formed SQL query. 576 | function: 577 | type: sqlite 578 | query: >- 579 | {%- if is_exposed_entity_in_query(query) -%} 580 | {{ query }} 581 | {%- else -%} 582 | {{ raise("entity_id should be exposed.") }} 583 | {%- endif -%} 584 | ``` 585 | 586 | #### 7-3. Defined SQL manually 587 | - Use a user defined query, which is verified. And model passes a requested entity to get data from database. 588 | - Secured, but less flexible way 589 | ```yaml 590 | - spec: 591 | name: get_last_updated_time_of_entity 592 | description: > 593 | Use this function to get last updated time of entity 594 | parameters: 595 | type: object 596 | properties: 597 | entity_id: 598 | type: string 599 | description: The target entity 600 | function: 601 | type: sqlite 602 | query: >- 603 | {%- if is_exposed(entity_id) -%} 604 | SELECT datetime(s.last_updated_ts, 'unixepoch', 'localtime') as last_updated_ts 605 | FROM states s 606 | INNER JOIN states_meta sm ON s.metadata_id = sm.metadata_id 607 | INNER JOIN states old ON s.old_state_id = old.state_id 608 | WHERE sm.entity_id = '{{entity_id}}' AND s.state != old.state ORDER BY s.last_updated_ts DESC LIMIT 1 609 | {%- else -%} 610 | {{ raise("entity_id should be exposed.") }} 611 | {%- endif -%} 612 | ``` 613 | 614 | ## Practical Usage 615 | See more practical [examples](https://github.com/jekalmin/extended_openai_conversation/tree/main/examples). 616 | 617 | ## Logging 618 | In order to monitor logs of API requests and responses, add following config to `configuration.yaml` file 619 | 620 | ```yaml 621 | logger: 622 | logs: 623 | custom_components.extended_openai_conversation: info 624 | ``` 625 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/__init__.py: -------------------------------------------------------------------------------- 1 | """The OpenAI Conversation integration.""" 2 | from __future__ import annotations 3 | 4 | import json 5 | import logging 6 | from typing import Literal 7 | 8 | from openai import AsyncAzureOpenAI, AsyncOpenAI 9 | from openai._exceptions import AuthenticationError, OpenAIError 10 | from openai.types.chat.chat_completion import ( 11 | ChatCompletion, 12 | ChatCompletionMessage, 13 | Choice, 14 | ) 15 | import yaml 16 | 17 | from homeassistant.components import conversation 18 | from homeassistant.components.homeassistant.exposed_entities import async_should_expose 19 | from homeassistant.config_entries import ConfigEntry 20 | from homeassistant.const import ATTR_NAME, CONF_API_KEY, MATCH_ALL 21 | from homeassistant.core import HomeAssistant 22 | from homeassistant.exceptions import ( 23 | ConfigEntryNotReady, 24 | HomeAssistantError, 25 | TemplateError, 26 | ) 27 | from homeassistant.helpers import ( 28 | config_validation as cv, 29 | entity_registry as er, 30 | intent, 31 | template, 32 | ) 33 | from homeassistant.helpers.httpx_client import get_async_client 34 | from homeassistant.helpers.typing import ConfigType 35 | from homeassistant.util import ulid 36 | 37 | from .const import ( 38 | CONF_API_VERSION, 39 | CONF_ATTACH_USERNAME, 40 | CONF_BASE_URL, 41 | CONF_CHAT_MODEL, 42 | CONF_CONTEXT_THRESHOLD, 43 | CONF_CONTEXT_TRUNCATE_STRATEGY, 44 | CONF_FUNCTIONS, 45 | CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION, 46 | CONF_MAX_TOKENS, 47 | CONF_ORGANIZATION, 48 | CONF_PROMPT, 49 | CONF_SKIP_AUTHENTICATION, 50 | CONF_TEMPERATURE, 51 | CONF_TOP_P, 52 | CONF_USE_TOOLS, 53 | DEFAULT_ATTACH_USERNAME, 54 | DEFAULT_CHAT_MODEL, 55 | DEFAULT_CONF_FUNCTIONS, 56 | DEFAULT_CONTEXT_THRESHOLD, 57 | DEFAULT_CONTEXT_TRUNCATE_STRATEGY, 58 | DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION, 59 | DEFAULT_MAX_TOKENS, 60 | DEFAULT_PROMPT, 61 | DEFAULT_SKIP_AUTHENTICATION, 62 | DEFAULT_TEMPERATURE, 63 | DEFAULT_TOP_P, 64 | DEFAULT_USE_TOOLS, 65 | DOMAIN, 66 | EVENT_CONVERSATION_FINISHED, 67 | ) 68 | from .exceptions import ( 69 | FunctionLoadFailed, 70 | FunctionNotFound, 71 | InvalidFunction, 72 | ParseArgumentsFailed, 73 | TokenLengthExceededError, 74 | ) 75 | from .helpers import ( 76 | get_function_executor, 77 | is_azure, 78 | validate_authentication, 79 | ) 80 | from .services import async_setup_services 81 | 82 | _LOGGER = logging.getLogger(__name__) 83 | 84 | CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) 85 | 86 | 87 | # hass.data key for agent. 88 | DATA_AGENT = "agent" 89 | 90 | 91 | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 92 | """Set up OpenAI Conversation.""" 93 | await async_setup_services(hass, config) 94 | return True 95 | 96 | 97 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 98 | """Set up OpenAI Conversation from a config entry.""" 99 | 100 | try: 101 | await validate_authentication( 102 | hass=hass, 103 | api_key=entry.data[CONF_API_KEY], 104 | base_url=entry.data.get(CONF_BASE_URL), 105 | api_version=entry.data.get(CONF_API_VERSION), 106 | organization=entry.data.get(CONF_ORGANIZATION), 107 | skip_authentication=entry.data.get( 108 | CONF_SKIP_AUTHENTICATION, DEFAULT_SKIP_AUTHENTICATION 109 | ), 110 | ) 111 | except AuthenticationError as err: 112 | _LOGGER.error("Invalid API key: %s", err) 113 | return False 114 | except OpenAIError as err: 115 | raise ConfigEntryNotReady(err) from err 116 | 117 | agent = OpenAIAgent(hass, entry) 118 | 119 | data = hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) 120 | data[CONF_API_KEY] = entry.data[CONF_API_KEY] 121 | data[DATA_AGENT] = agent 122 | 123 | conversation.async_set_agent(hass, entry, agent) 124 | return True 125 | 126 | 127 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 128 | """Unload OpenAI.""" 129 | hass.data[DOMAIN].pop(entry.entry_id) 130 | conversation.async_unset_agent(hass, entry) 131 | return True 132 | 133 | 134 | class OpenAIAgent(conversation.AbstractConversationAgent): 135 | """OpenAI conversation agent.""" 136 | 137 | def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: 138 | """Initialize the agent.""" 139 | self.hass = hass 140 | self.entry = entry 141 | self.history: dict[str, list[dict]] = {} 142 | base_url = entry.data.get(CONF_BASE_URL) 143 | if is_azure(base_url): 144 | self.client = AsyncAzureOpenAI( 145 | api_key=entry.data[CONF_API_KEY], 146 | azure_endpoint=base_url, 147 | api_version=entry.data.get(CONF_API_VERSION), 148 | organization=entry.data.get(CONF_ORGANIZATION), 149 | http_client=get_async_client(hass), 150 | ) 151 | else: 152 | self.client = AsyncOpenAI( 153 | api_key=entry.data[CONF_API_KEY], 154 | base_url=base_url, 155 | organization=entry.data.get(CONF_ORGANIZATION), 156 | http_client=get_async_client(hass), 157 | ) 158 | 159 | @property 160 | def supported_languages(self) -> list[str] | Literal["*"]: 161 | """Return a list of supported languages.""" 162 | return MATCH_ALL 163 | 164 | async def async_process( 165 | self, user_input: conversation.ConversationInput 166 | ) -> conversation.ConversationResult: 167 | exposed_entities = self.get_exposed_entities() 168 | 169 | if user_input.conversation_id in self.history: 170 | conversation_id = user_input.conversation_id 171 | messages = self.history[conversation_id] 172 | else: 173 | conversation_id = ulid.ulid() 174 | user_input.conversation_id = conversation_id 175 | try: 176 | system_message = self._generate_system_message( 177 | exposed_entities, user_input 178 | ) 179 | except TemplateError as err: 180 | _LOGGER.error("Error rendering prompt: %s", err) 181 | intent_response = intent.IntentResponse(language=user_input.language) 182 | intent_response.async_set_error( 183 | intent.IntentResponseErrorCode.UNKNOWN, 184 | f"Sorry, I had a problem with my template: {err}", 185 | ) 186 | return conversation.ConversationResult( 187 | response=intent_response, conversation_id=conversation_id 188 | ) 189 | messages = [system_message] 190 | user_message = {"role": "user", "content": user_input.text} 191 | if self.entry.options.get(CONF_ATTACH_USERNAME, DEFAULT_ATTACH_USERNAME): 192 | user = user_input.context.user_id 193 | if user is not None: 194 | user_message[ATTR_NAME] = user 195 | 196 | messages.append(user_message) 197 | 198 | try: 199 | query_response = await self.query(user_input, messages, exposed_entities, 0) 200 | except OpenAIError as err: 201 | _LOGGER.error(err) 202 | intent_response = intent.IntentResponse(language=user_input.language) 203 | intent_response.async_set_error( 204 | intent.IntentResponseErrorCode.UNKNOWN, 205 | f"Sorry, I had a problem talking to OpenAI: {err}", 206 | ) 207 | return conversation.ConversationResult( 208 | response=intent_response, conversation_id=conversation_id 209 | ) 210 | except HomeAssistantError as err: 211 | _LOGGER.error(err, exc_info=err) 212 | intent_response = intent.IntentResponse(language=user_input.language) 213 | intent_response.async_set_error( 214 | intent.IntentResponseErrorCode.UNKNOWN, 215 | f"Something went wrong: {err}", 216 | ) 217 | return conversation.ConversationResult( 218 | response=intent_response, conversation_id=conversation_id 219 | ) 220 | 221 | messages.append(query_response.message.model_dump(exclude_none=True)) 222 | self.history[conversation_id] = messages 223 | 224 | self.hass.bus.async_fire( 225 | EVENT_CONVERSATION_FINISHED, 226 | { 227 | "response": query_response.response.model_dump(), 228 | "user_input": user_input, 229 | "messages": messages, 230 | }, 231 | ) 232 | 233 | intent_response = intent.IntentResponse(language=user_input.language) 234 | intent_response.async_set_speech(query_response.message.content) 235 | return conversation.ConversationResult( 236 | response=intent_response, conversation_id=conversation_id 237 | ) 238 | 239 | def _generate_system_message( 240 | self, exposed_entities, user_input: conversation.ConversationInput 241 | ): 242 | raw_prompt = self.entry.options.get(CONF_PROMPT, DEFAULT_PROMPT) 243 | prompt = self._async_generate_prompt(raw_prompt, exposed_entities, user_input) 244 | return {"role": "system", "content": prompt} 245 | 246 | def _async_generate_prompt( 247 | self, 248 | raw_prompt: str, 249 | exposed_entities, 250 | user_input: conversation.ConversationInput, 251 | ) -> str: 252 | """Generate a prompt for the user.""" 253 | return template.Template(raw_prompt, self.hass).async_render( 254 | { 255 | "ha_name": self.hass.config.location_name, 256 | "exposed_entities": exposed_entities, 257 | "current_device_id": user_input.device_id, 258 | }, 259 | parse_result=False, 260 | ) 261 | 262 | def get_exposed_entities(self): 263 | states = [ 264 | state 265 | for state in self.hass.states.async_all() 266 | if async_should_expose(self.hass, conversation.DOMAIN, state.entity_id) 267 | ] 268 | entity_registry = er.async_get(self.hass) 269 | exposed_entities = [] 270 | for state in states: 271 | entity_id = state.entity_id 272 | entity = entity_registry.async_get(entity_id) 273 | 274 | aliases = [] 275 | if entity and entity.aliases: 276 | aliases = entity.aliases 277 | 278 | exposed_entities.append( 279 | { 280 | "entity_id": entity_id, 281 | "name": state.name, 282 | "state": self.hass.states.get(entity_id).state, 283 | "aliases": aliases, 284 | } 285 | ) 286 | return exposed_entities 287 | 288 | def get_functions(self): 289 | try: 290 | function = self.entry.options.get(CONF_FUNCTIONS) 291 | result = yaml.safe_load(function) if function else DEFAULT_CONF_FUNCTIONS 292 | if result: 293 | for setting in result: 294 | function_executor = get_function_executor( 295 | setting["function"]["type"] 296 | ) 297 | setting["function"] = function_executor.to_arguments( 298 | setting["function"] 299 | ) 300 | return result 301 | except (InvalidFunction, FunctionNotFound) as e: 302 | raise e 303 | except: 304 | raise FunctionLoadFailed() 305 | 306 | async def truncate_message_history( 307 | self, messages, exposed_entities, user_input: conversation.ConversationInput 308 | ): 309 | """Truncate message history.""" 310 | strategy = self.entry.options.get( 311 | CONF_CONTEXT_TRUNCATE_STRATEGY, DEFAULT_CONTEXT_TRUNCATE_STRATEGY 312 | ) 313 | 314 | if strategy == "clear": 315 | last_user_message_index = None 316 | for i in reversed(range(len(messages))): 317 | if messages[i]["role"] == "user": 318 | last_user_message_index = i 319 | break 320 | 321 | if last_user_message_index is not None: 322 | del messages[1:last_user_message_index] 323 | # refresh system prompt when all messages are deleted 324 | messages[0] = self._generate_system_message( 325 | exposed_entities, user_input 326 | ) 327 | 328 | async def query( 329 | self, 330 | user_input: conversation.ConversationInput, 331 | messages, 332 | exposed_entities, 333 | n_requests, 334 | ) -> OpenAIQueryResponse: 335 | """Process a sentence.""" 336 | model = self.entry.options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL) 337 | max_tokens = self.entry.options.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS) 338 | top_p = self.entry.options.get(CONF_TOP_P, DEFAULT_TOP_P) 339 | temperature = self.entry.options.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE) 340 | use_tools = self.entry.options.get(CONF_USE_TOOLS, DEFAULT_USE_TOOLS) 341 | context_threshold = self.entry.options.get( 342 | CONF_CONTEXT_THRESHOLD, DEFAULT_CONTEXT_THRESHOLD 343 | ) 344 | functions = list(map(lambda s: s["spec"], self.get_functions())) 345 | function_call = "auto" 346 | if n_requests == self.entry.options.get( 347 | CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION, 348 | DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION, 349 | ): 350 | function_call = "none" 351 | 352 | tool_kwargs = {"functions": functions, "function_call": function_call} 353 | if use_tools: 354 | tool_kwargs = { 355 | "tools": [{"type": "function", "function": func} for func in functions], 356 | "tool_choice": function_call, 357 | } 358 | 359 | if len(functions) == 0: 360 | tool_kwargs = {} 361 | 362 | _LOGGER.info("Prompt for %s: %s", model, json.dumps(messages)) 363 | 364 | response: ChatCompletion = await self.client.chat.completions.create( 365 | model=model, 366 | messages=messages, 367 | max_tokens=max_tokens, 368 | top_p=top_p, 369 | temperature=temperature, 370 | user=user_input.conversation_id, 371 | **tool_kwargs, 372 | ) 373 | 374 | _LOGGER.info("Response %s", json.dumps(response.model_dump(exclude_none=True))) 375 | 376 | if response.usage.total_tokens > context_threshold: 377 | await self.truncate_message_history(messages, exposed_entities, user_input) 378 | 379 | choice: Choice = response.choices[0] 380 | message = choice.message 381 | 382 | if choice.finish_reason == "function_call": 383 | return await self.execute_function_call( 384 | user_input, messages, message, exposed_entities, n_requests + 1 385 | ) 386 | if choice.finish_reason == "tool_calls": 387 | return await self.execute_tool_calls( 388 | user_input, messages, message, exposed_entities, n_requests + 1 389 | ) 390 | if choice.finish_reason == "length": 391 | raise TokenLengthExceededError(response.usage.completion_tokens) 392 | 393 | return OpenAIQueryResponse(response=response, message=message) 394 | 395 | async def execute_function_call( 396 | self, 397 | user_input: conversation.ConversationInput, 398 | messages, 399 | message: ChatCompletionMessage, 400 | exposed_entities, 401 | n_requests, 402 | ) -> OpenAIQueryResponse: 403 | function_name = message.function_call.name 404 | function = next( 405 | (s for s in self.get_functions() if s["spec"]["name"] == function_name), 406 | None, 407 | ) 408 | if function is not None: 409 | return await self.execute_function( 410 | user_input, 411 | messages, 412 | message, 413 | exposed_entities, 414 | n_requests, 415 | function, 416 | ) 417 | raise FunctionNotFound(function_name) 418 | 419 | async def execute_function( 420 | self, 421 | user_input: conversation.ConversationInput, 422 | messages, 423 | message: ChatCompletionMessage, 424 | exposed_entities, 425 | n_requests, 426 | function, 427 | ) -> OpenAIQueryResponse: 428 | function_executor = get_function_executor(function["function"]["type"]) 429 | 430 | try: 431 | arguments = json.loads(message.function_call.arguments) 432 | except json.decoder.JSONDecodeError as err: 433 | raise ParseArgumentsFailed(message.function_call.arguments) from err 434 | 435 | result = await function_executor.execute( 436 | self.hass, function["function"], arguments, user_input, exposed_entities 437 | ) 438 | 439 | messages.append( 440 | { 441 | "role": "function", 442 | "name": message.function_call.name, 443 | "content": str(result), 444 | } 445 | ) 446 | return await self.query(user_input, messages, exposed_entities, n_requests) 447 | 448 | async def execute_tool_calls( 449 | self, 450 | user_input: conversation.ConversationInput, 451 | messages, 452 | message: ChatCompletionMessage, 453 | exposed_entities, 454 | n_requests, 455 | ) -> OpenAIQueryResponse: 456 | messages.append(message.model_dump(exclude_none=True)) 457 | for tool in message.tool_calls: 458 | function_name = tool.function.name 459 | function = next( 460 | (s for s in self.get_functions() if s["spec"]["name"] == function_name), 461 | None, 462 | ) 463 | if function is not None: 464 | result = await self.execute_tool_function( 465 | user_input, 466 | tool, 467 | exposed_entities, 468 | function, 469 | ) 470 | 471 | messages.append( 472 | { 473 | "tool_call_id": tool.id, 474 | "role": "tool", 475 | "name": function_name, 476 | "content": str(result), 477 | } 478 | ) 479 | else: 480 | raise FunctionNotFound(function_name) 481 | return await self.query(user_input, messages, exposed_entities, n_requests) 482 | 483 | async def execute_tool_function( 484 | self, 485 | user_input: conversation.ConversationInput, 486 | tool, 487 | exposed_entities, 488 | function, 489 | ) -> OpenAIQueryResponse: 490 | function_executor = get_function_executor(function["function"]["type"]) 491 | 492 | try: 493 | arguments = json.loads(tool.function.arguments) 494 | except json.decoder.JSONDecodeError as err: 495 | raise ParseArgumentsFailed(tool.function.arguments) from err 496 | 497 | result = await function_executor.execute( 498 | self.hass, function["function"], arguments, user_input, exposed_entities 499 | ) 500 | return result 501 | 502 | 503 | class OpenAIQueryResponse: 504 | """OpenAI query response value object.""" 505 | 506 | def __init__( 507 | self, response: ChatCompletion, message: ChatCompletionMessage 508 | ) -> None: 509 | """Initialize OpenAI query response value object.""" 510 | self.response = response 511 | self.message = message 512 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for OpenAI Conversation integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | import types 6 | from types import MappingProxyType 7 | from typing import Any 8 | 9 | from openai._exceptions import APIConnectionError, AuthenticationError 10 | import voluptuous as vol 11 | import yaml 12 | 13 | from homeassistant import config_entries 14 | from homeassistant.const import CONF_API_KEY, CONF_NAME 15 | from homeassistant.core import HomeAssistant 16 | from homeassistant.data_entry_flow import FlowResult 17 | from homeassistant.helpers.selector import ( 18 | BooleanSelector, 19 | NumberSelector, 20 | NumberSelectorConfig, 21 | SelectOptionDict, 22 | SelectSelector, 23 | SelectSelectorConfig, 24 | SelectSelectorMode, 25 | TemplateSelector, 26 | ) 27 | 28 | from .const import ( 29 | CONF_API_VERSION, 30 | CONF_ATTACH_USERNAME, 31 | CONF_BASE_URL, 32 | CONF_CHAT_MODEL, 33 | CONF_CONTEXT_THRESHOLD, 34 | CONF_CONTEXT_TRUNCATE_STRATEGY, 35 | CONF_FUNCTIONS, 36 | CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION, 37 | CONF_MAX_TOKENS, 38 | CONF_ORGANIZATION, 39 | CONF_PROMPT, 40 | CONF_SKIP_AUTHENTICATION, 41 | CONF_TEMPERATURE, 42 | CONF_TOP_P, 43 | CONF_USE_TOOLS, 44 | CONTEXT_TRUNCATE_STRATEGIES, 45 | DEFAULT_ATTACH_USERNAME, 46 | DEFAULT_CHAT_MODEL, 47 | DEFAULT_CONF_BASE_URL, 48 | DEFAULT_CONF_FUNCTIONS, 49 | DEFAULT_CONTEXT_THRESHOLD, 50 | DEFAULT_CONTEXT_TRUNCATE_STRATEGY, 51 | DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION, 52 | DEFAULT_MAX_TOKENS, 53 | DEFAULT_NAME, 54 | DEFAULT_PROMPT, 55 | DEFAULT_SKIP_AUTHENTICATION, 56 | DEFAULT_TEMPERATURE, 57 | DEFAULT_TOP_P, 58 | DEFAULT_USE_TOOLS, 59 | DOMAIN, 60 | ) 61 | from .helpers import validate_authentication 62 | 63 | _LOGGER = logging.getLogger(__name__) 64 | 65 | STEP_USER_DATA_SCHEMA = vol.Schema( 66 | { 67 | vol.Optional(CONF_NAME): str, 68 | vol.Required(CONF_API_KEY): str, 69 | vol.Optional(CONF_BASE_URL, default=DEFAULT_CONF_BASE_URL): str, 70 | vol.Optional(CONF_API_VERSION): str, 71 | vol.Optional(CONF_ORGANIZATION): str, 72 | vol.Optional( 73 | CONF_SKIP_AUTHENTICATION, default=DEFAULT_SKIP_AUTHENTICATION 74 | ): bool, 75 | } 76 | ) 77 | 78 | DEFAULT_CONF_FUNCTIONS_STR = yaml.dump(DEFAULT_CONF_FUNCTIONS, sort_keys=False) 79 | 80 | DEFAULT_OPTIONS = types.MappingProxyType( 81 | { 82 | CONF_PROMPT: DEFAULT_PROMPT, 83 | CONF_CHAT_MODEL: DEFAULT_CHAT_MODEL, 84 | CONF_MAX_TOKENS: DEFAULT_MAX_TOKENS, 85 | CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION: DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION, 86 | CONF_TOP_P: DEFAULT_TOP_P, 87 | CONF_TEMPERATURE: DEFAULT_TEMPERATURE, 88 | CONF_FUNCTIONS: DEFAULT_CONF_FUNCTIONS_STR, 89 | CONF_ATTACH_USERNAME: DEFAULT_ATTACH_USERNAME, 90 | CONF_USE_TOOLS: DEFAULT_USE_TOOLS, 91 | CONF_CONTEXT_THRESHOLD: DEFAULT_CONTEXT_THRESHOLD, 92 | CONF_CONTEXT_TRUNCATE_STRATEGY: DEFAULT_CONTEXT_TRUNCATE_STRATEGY, 93 | } 94 | ) 95 | 96 | 97 | async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: 98 | """Validate the user input allows us to connect. 99 | 100 | Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. 101 | """ 102 | api_key = data[CONF_API_KEY] 103 | base_url = data.get(CONF_BASE_URL) 104 | api_version = data.get(CONF_API_VERSION) 105 | organization = data.get(CONF_ORGANIZATION) 106 | skip_authentication = data.get(CONF_SKIP_AUTHENTICATION) 107 | 108 | if base_url == DEFAULT_CONF_BASE_URL: 109 | # Do not set base_url if using OpenAI for case of OpenAI's base_url change 110 | base_url = None 111 | data.pop(CONF_BASE_URL) 112 | 113 | await validate_authentication( 114 | hass=hass, 115 | api_key=api_key, 116 | base_url=base_url, 117 | api_version=api_version, 118 | organization=organization, 119 | skip_authentication=skip_authentication, 120 | ) 121 | 122 | 123 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 124 | """Handle a config flow for OpenAI Conversation.""" 125 | 126 | VERSION = 1 127 | 128 | async def async_step_user( 129 | self, user_input: dict[str, Any] | None = None 130 | ) -> FlowResult: 131 | """Handle the initial step.""" 132 | if user_input is None: 133 | return self.async_show_form( 134 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA 135 | ) 136 | 137 | errors = {} 138 | 139 | try: 140 | await validate_input(self.hass, user_input) 141 | except APIConnectionError: 142 | errors["base"] = "cannot_connect" 143 | except AuthenticationError: 144 | errors["base"] = "invalid_auth" 145 | except Exception: # pylint: disable=broad-except 146 | _LOGGER.exception("Unexpected exception") 147 | errors["base"] = "unknown" 148 | else: 149 | return self.async_create_entry( 150 | title=user_input.get(CONF_NAME, DEFAULT_NAME), data=user_input 151 | ) 152 | 153 | return self.async_show_form( 154 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors 155 | ) 156 | 157 | @staticmethod 158 | def async_get_options_flow( 159 | config_entry: config_entries.ConfigEntry, 160 | ) -> config_entries.OptionsFlow: 161 | """Create the options flow.""" 162 | return OptionsFlow(config_entry) 163 | 164 | 165 | class OptionsFlow(config_entries.OptionsFlow): 166 | """OpenAI config flow options handler.""" 167 | 168 | def __init__(self, config_entry: config_entries.ConfigEntry) -> None: 169 | """Initialize options flow.""" 170 | self.config_entry = config_entry 171 | 172 | async def async_step_init( 173 | self, user_input: dict[str, Any] | None = None 174 | ) -> FlowResult: 175 | """Manage the options.""" 176 | if user_input is not None: 177 | return self.async_create_entry( 178 | title=user_input.get(CONF_NAME, DEFAULT_NAME), data=user_input 179 | ) 180 | schema = self.openai_config_option_schema(self.config_entry.options) 181 | return self.async_show_form( 182 | step_id="init", 183 | data_schema=vol.Schema(schema), 184 | ) 185 | 186 | def openai_config_option_schema(self, options: MappingProxyType[str, Any]) -> dict: 187 | """Return a schema for OpenAI completion options.""" 188 | if not options: 189 | options = DEFAULT_OPTIONS 190 | 191 | return { 192 | vol.Optional( 193 | CONF_PROMPT, 194 | description={"suggested_value": options[CONF_PROMPT]}, 195 | default=DEFAULT_PROMPT, 196 | ): TemplateSelector(), 197 | vol.Optional( 198 | CONF_CHAT_MODEL, 199 | description={ 200 | # New key in HA 2023.4 201 | "suggested_value": options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL) 202 | }, 203 | default=DEFAULT_CHAT_MODEL, 204 | ): str, 205 | vol.Optional( 206 | CONF_MAX_TOKENS, 207 | description={"suggested_value": options[CONF_MAX_TOKENS]}, 208 | default=DEFAULT_MAX_TOKENS, 209 | ): int, 210 | vol.Optional( 211 | CONF_TOP_P, 212 | description={"suggested_value": options[CONF_TOP_P]}, 213 | default=DEFAULT_TOP_P, 214 | ): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)), 215 | vol.Optional( 216 | CONF_TEMPERATURE, 217 | description={"suggested_value": options[CONF_TEMPERATURE]}, 218 | default=DEFAULT_TEMPERATURE, 219 | ): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)), 220 | vol.Optional( 221 | CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION, 222 | description={ 223 | "suggested_value": options[CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION] 224 | }, 225 | default=DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION, 226 | ): int, 227 | vol.Optional( 228 | CONF_FUNCTIONS, 229 | description={"suggested_value": options.get(CONF_FUNCTIONS)}, 230 | default=DEFAULT_CONF_FUNCTIONS_STR, 231 | ): TemplateSelector(), 232 | vol.Optional( 233 | CONF_ATTACH_USERNAME, 234 | description={"suggested_value": options.get(CONF_ATTACH_USERNAME)}, 235 | default=DEFAULT_ATTACH_USERNAME, 236 | ): BooleanSelector(), 237 | vol.Optional( 238 | CONF_USE_TOOLS, 239 | description={"suggested_value": options.get(CONF_USE_TOOLS)}, 240 | default=DEFAULT_USE_TOOLS, 241 | ): BooleanSelector(), 242 | vol.Optional( 243 | CONF_CONTEXT_THRESHOLD, 244 | description={"suggested_value": options.get(CONF_CONTEXT_THRESHOLD)}, 245 | default=DEFAULT_CONTEXT_THRESHOLD, 246 | ): int, 247 | vol.Optional( 248 | CONF_CONTEXT_TRUNCATE_STRATEGY, 249 | description={ 250 | "suggested_value": options.get(CONF_CONTEXT_TRUNCATE_STRATEGY) 251 | }, 252 | default=DEFAULT_CONTEXT_TRUNCATE_STRATEGY, 253 | ): SelectSelector( 254 | SelectSelectorConfig( 255 | options=[ 256 | SelectOptionDict(value=strategy["key"], label=strategy["label"]) 257 | for strategy in CONTEXT_TRUNCATE_STRATEGIES 258 | ], 259 | mode=SelectSelectorMode.DROPDOWN, 260 | ) 261 | ), 262 | } 263 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Extended DeepSeek Conversation integration.""" 2 | 3 | DOMAIN = "extended_deepseek_conversation" 4 | DEFAULT_NAME = "Extended DeepSeek Conversation" 5 | CONF_ORGANIZATION = "organization" 6 | CONF_BASE_URL = "base_url" 7 | DEFAULT_CONF_BASE_URL = "https://api.deepseek.com/v1" 8 | CONF_API_VERSION = "api_version" 9 | CONF_SKIP_AUTHENTICATION = "skip_authentication" 10 | DEFAULT_SKIP_AUTHENTICATION = False 11 | 12 | EVENT_AUTOMATION_REGISTERED = "automation_registered_via_extended_deepseek_conversation" 13 | EVENT_CONVERSATION_FINISHED = "extended_deepseek_conversation.conversation.finished" 14 | 15 | CONF_PROMPT = "prompt" 16 | DEFAULT_PROMPT = """I want you to act as smart home manager of Home Assistant. 17 | I will provide information of smart home along with a question, you will truthfully make correction or answer using information provided in one sentence in everyday language. 18 | 19 | Current Time: {{now()}} 20 | 21 | Available Devices: 22 | ```csv 23 | entity_id,name,state,aliases 24 | {% for entity in exposed_entities -%} 25 | {{ entity.entity_id }},{{ entity.name }},{{ entity.state }},{{entity.aliases | join('/')}} 26 | {% endfor -%} 27 | ``` 28 | 29 | The current state of devices is provided in available devices. 30 | Use execute_services function only for requested action, not for current states. 31 | Do not execute service without user's confirmation. 32 | Do not restate or appreciate what user says, rather make a quick inquiry. 33 | """ 34 | CONF_CHAT_MODEL = "chat_model" 35 | DEFAULT_CHAT_MODEL = "deepseek-chat" 36 | CONF_MAX_TOKENS = "max_tokens" 37 | DEFAULT_MAX_TOKENS = 1024 38 | CONF_TOP_P = "top_p" 39 | DEFAULT_TOP_P = 1 40 | CONF_TEMPERATURE = "temperature" 41 | DEFAULT_TEMPERATURE = 0.5 42 | CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION = "max_function_calls_per_conversation" 43 | DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION = 1 44 | CONF_FUNCTIONS = "functions" 45 | DEFAULT_CONF_FUNCTIONS = [ 46 | { 47 | "spec": { 48 | "name": "execute_services", 49 | "description": "Use this function to execute service of devices in Home Assistant.", 50 | "parameters": { 51 | "type": "object", 52 | "properties": { 53 | "list": { 54 | "type": "array", 55 | "items": { 56 | "type": "object", 57 | "properties": { 58 | "domain": { 59 | "type": "string", 60 | "description": "The domain of the service", 61 | }, 62 | "service": { 63 | "type": "string", 64 | "description": "The service to be called", 65 | }, 66 | "service_data": { 67 | "type": "object", 68 | "description": "The service data object to indicate what to control.", 69 | "properties": { 70 | "entity_id": { 71 | "type": "string", 72 | "description": "The entity_id retrieved from available devices. It must start with domain, followed by dot character.", 73 | } 74 | }, 75 | "required": ["entity_id"], 76 | }, 77 | }, 78 | "required": ["domain", "service", "service_data"], 79 | }, 80 | } 81 | }, 82 | }, 83 | }, 84 | "function": {"type": "native", "name": "execute_service"}, 85 | } 86 | ] 87 | CONF_ATTACH_USERNAME = "attach_username" 88 | DEFAULT_ATTACH_USERNAME = False 89 | CONF_USE_TOOLS = "use_tools" 90 | DEFAULT_USE_TOOLS = False 91 | CONF_CONTEXT_THRESHOLD = "context_threshold" 92 | DEFAULT_CONTEXT_THRESHOLD = 13000 93 | CONTEXT_TRUNCATE_STRATEGIES = [{"key": "clear", "label": "Clear All Messages"}] 94 | CONF_CONTEXT_TRUNCATE_STRATEGY = "context_truncate_strategy" 95 | DEFAULT_CONTEXT_TRUNCATE_STRATEGY = CONTEXT_TRUNCATE_STRATEGIES[0]["key"] 96 | 97 | SERVICE_QUERY_IMAGE = "query_image" 98 | 99 | CONF_PAYLOAD_TEMPLATE = "payload_template" 100 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/exceptions.py: -------------------------------------------------------------------------------- 1 | """The exceptions used by Extended OpenAI Conversation.""" 2 | from homeassistant.exceptions import HomeAssistantError 3 | 4 | 5 | class EntityNotFound(HomeAssistantError): 6 | """When referenced entity not found.""" 7 | 8 | def __init__(self, entity_id: str) -> None: 9 | """Initialize error.""" 10 | super().__init__(self, f"entity {entity_id} not found") 11 | self.entity_id = entity_id 12 | 13 | def __str__(self) -> str: 14 | """Return string representation.""" 15 | return f"Unable to find entity {self.entity_id}" 16 | 17 | 18 | class EntityNotExposed(HomeAssistantError): 19 | """When referenced entity not exposed.""" 20 | 21 | def __init__(self, entity_id: str) -> None: 22 | """Initialize error.""" 23 | super().__init__(self, f"entity {entity_id} not exposed") 24 | self.entity_id = entity_id 25 | 26 | def __str__(self) -> str: 27 | """Return string representation.""" 28 | return f"entity {self.entity_id} is not exposed" 29 | 30 | 31 | class CallServiceError(HomeAssistantError): 32 | """Error during service calling""" 33 | 34 | def __init__(self, domain: str, service: str, data: object) -> None: 35 | """Initialize error.""" 36 | super().__init__( 37 | self, 38 | f"unable to call service {domain}.{service} with data {data}. One of 'entity_id', 'area_id', or 'device_id' is required", 39 | ) 40 | self.domain = domain 41 | self.service = service 42 | self.data = data 43 | 44 | def __str__(self) -> str: 45 | """Return string representation.""" 46 | return f"unable to call service {self.domain}.{self.service} with data {self.data}. One of 'entity_id', 'area_id', or 'device_id' is required" 47 | 48 | 49 | class FunctionNotFound(HomeAssistantError): 50 | """When referenced function not found.""" 51 | 52 | def __init__(self, function: str) -> None: 53 | """Initialize error.""" 54 | super().__init__(self, f"function '{function}' does not exist") 55 | self.function = function 56 | 57 | def __str__(self) -> str: 58 | """Return string representation.""" 59 | return f"function '{self.function}' does not exist" 60 | 61 | 62 | class NativeNotFound(HomeAssistantError): 63 | """When native function not found.""" 64 | 65 | def __init__(self, name: str) -> None: 66 | """Initialize error.""" 67 | super().__init__(self, f"native function '{name}' does not exist") 68 | self.name = name 69 | 70 | def __str__(self) -> str: 71 | """Return string representation.""" 72 | return f"native function '{self.name}' does not exist" 73 | 74 | 75 | class FunctionLoadFailed(HomeAssistantError): 76 | """When function load failed.""" 77 | 78 | def __init__(self) -> None: 79 | """Initialize error.""" 80 | super().__init__( 81 | self, 82 | "failed to load functions. Verify functions are valid in a yaml format", 83 | ) 84 | 85 | def __str__(self) -> str: 86 | """Return string representation.""" 87 | return "failed to load functions. Verify functions are valid in a yaml format" 88 | 89 | 90 | class ParseArgumentsFailed(HomeAssistantError): 91 | """When parse arguments failed.""" 92 | 93 | def __init__(self, arguments: str) -> None: 94 | """Initialize error.""" 95 | super().__init__( 96 | self, 97 | f"failed to parse arguments `{arguments}`. Increase maximum token to avoid the issue.", 98 | ) 99 | self.arguments = arguments 100 | 101 | def __str__(self) -> str: 102 | """Return string representation.""" 103 | return f"failed to parse arguments `{self.arguments}`. Increase maximum token to avoid the issue." 104 | 105 | 106 | class TokenLengthExceededError(HomeAssistantError): 107 | """When openai return 'length' as 'finish_reason'.""" 108 | 109 | def __init__(self, token: int) -> None: 110 | """Initialize error.""" 111 | super().__init__( 112 | self, 113 | f"token length(`{token}`) exceeded. Increase maximum token to avoid the issue.", 114 | ) 115 | self.token = token 116 | 117 | def __str__(self) -> str: 118 | """Return string representation.""" 119 | return f"token length(`{self.token}`) exceeded. Increase maximum token to avoid the issue." 120 | 121 | 122 | class InvalidFunction(HomeAssistantError): 123 | """When function validation failed.""" 124 | 125 | def __init__(self, function_name: str) -> None: 126 | """Initialize error.""" 127 | super().__init__( 128 | self, 129 | f"failed to validate function `{function_name}`", 130 | ) 131 | self.function_name = function_name 132 | 133 | def __str__(self) -> str: 134 | """Return string representation.""" 135 | return f"failed to validate function `{self.function_name}` ({self.__cause__})" 136 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/helpers.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from datetime import timedelta 3 | from functools import partial 4 | import logging 5 | import os 6 | import re 7 | import sqlite3 8 | import time 9 | from typing import Any 10 | from urllib import parse 11 | 12 | from bs4 import BeautifulSoup 13 | from openai import AsyncAzureOpenAI, AsyncOpenAI 14 | import voluptuous as vol 15 | import yaml 16 | 17 | from homeassistant.components import ( 18 | automation, 19 | conversation, 20 | energy, 21 | recorder, 22 | rest, 23 | scrape, 24 | ) 25 | from homeassistant.components.automation.config import _async_validate_config_item 26 | from homeassistant.components.script.config import SCRIPT_ENTITY_SCHEMA 27 | from homeassistant.config import AUTOMATION_CONFIG_PATH 28 | from homeassistant.const import ( 29 | CONF_ATTRIBUTE, 30 | CONF_METHOD, 31 | CONF_NAME, 32 | CONF_PAYLOAD, 33 | CONF_RESOURCE, 34 | CONF_RESOURCE_TEMPLATE, 35 | CONF_TIMEOUT, 36 | CONF_VALUE_TEMPLATE, 37 | CONF_VERIFY_SSL, 38 | SERVICE_RELOAD, 39 | ) 40 | from homeassistant.core import HomeAssistant, State 41 | from homeassistant.exceptions import HomeAssistantError, ServiceNotFound 42 | from homeassistant.helpers import config_validation as cv 43 | from homeassistant.helpers.httpx_client import get_async_client 44 | from homeassistant.helpers.script import Script 45 | from homeassistant.helpers.template import Template 46 | import homeassistant.util.dt as dt_util 47 | 48 | from .const import CONF_PAYLOAD_TEMPLATE, DOMAIN, EVENT_AUTOMATION_REGISTERED 49 | from .exceptions import ( 50 | CallServiceError, 51 | EntityNotExposed, 52 | EntityNotFound, 53 | FunctionNotFound, 54 | InvalidFunction, 55 | NativeNotFound, 56 | ) 57 | 58 | _LOGGER = logging.getLogger(__name__) 59 | 60 | 61 | AZURE_DOMAIN_PATTERN = r"\.(openai\.azure\.com|azure-api\.net)" 62 | 63 | 64 | def get_function_executor(value: str): 65 | function_executor = FUNCTION_EXECUTORS.get(value) 66 | if function_executor is None: 67 | raise FunctionNotFound(value) 68 | return function_executor 69 | 70 | 71 | def is_azure(base_url: str): 72 | if base_url and re.search(AZURE_DOMAIN_PATTERN, base_url): 73 | return True 74 | return False 75 | 76 | 77 | def convert_to_template( 78 | settings, 79 | template_keys=["data", "event_data", "target", "service"], 80 | hass: HomeAssistant | None = None, 81 | ): 82 | _convert_to_template(settings, template_keys, hass, []) 83 | 84 | 85 | def _convert_to_template(settings, template_keys, hass, parents: list[str]): 86 | if isinstance(settings, dict): 87 | for key, value in settings.items(): 88 | if isinstance(value, str) and ( 89 | key in template_keys or set(parents).intersection(template_keys) 90 | ): 91 | settings[key] = Template(value, hass) 92 | if isinstance(value, dict): 93 | parents.append(key) 94 | _convert_to_template(value, template_keys, hass, parents) 95 | parents.pop() 96 | if isinstance(value, list): 97 | parents.append(key) 98 | for item in value: 99 | _convert_to_template(item, template_keys, hass, parents) 100 | parents.pop() 101 | if isinstance(settings, list): 102 | for setting in settings: 103 | _convert_to_template(setting, template_keys, hass, parents) 104 | 105 | 106 | def _get_rest_data(hass, rest_config, arguments): 107 | rest_config.setdefault(CONF_METHOD, rest.const.DEFAULT_METHOD) 108 | rest_config.setdefault(CONF_VERIFY_SSL, rest.const.DEFAULT_VERIFY_SSL) 109 | rest_config.setdefault(CONF_TIMEOUT, rest.data.DEFAULT_TIMEOUT) 110 | rest_config.setdefault(rest.const.CONF_ENCODING, rest.const.DEFAULT_ENCODING) 111 | 112 | resource_template: Template | None = rest_config.get(CONF_RESOURCE_TEMPLATE) 113 | if resource_template is not None: 114 | rest_config.pop(CONF_RESOURCE_TEMPLATE) 115 | rest_config[CONF_RESOURCE] = resource_template.async_render( 116 | arguments, parse_result=False 117 | ) 118 | 119 | payload_template: Template | None = rest_config.get(CONF_PAYLOAD_TEMPLATE) 120 | if payload_template is not None: 121 | rest_config.pop(CONF_PAYLOAD_TEMPLATE) 122 | rest_config[CONF_PAYLOAD] = payload_template.async_render( 123 | arguments, parse_result=False 124 | ) 125 | 126 | return rest.create_rest_data_from_config(hass, rest_config) 127 | 128 | 129 | async def validate_authentication( 130 | hass: HomeAssistant, 131 | api_key: str, 132 | base_url: str, 133 | api_version: str, 134 | organization: str = None, 135 | skip_authentication=False, 136 | ) -> None: 137 | if skip_authentication: 138 | return 139 | 140 | if is_azure(base_url): 141 | client = AsyncAzureOpenAI( 142 | api_key=api_key, 143 | azure_endpoint=base_url, 144 | api_version=api_version, 145 | organization=organization, 146 | http_client=get_async_client(hass), 147 | ) 148 | else: 149 | client = AsyncOpenAI( 150 | api_key=api_key, 151 | base_url=base_url, 152 | organization=organization, 153 | http_client=get_async_client(hass), 154 | ) 155 | 156 | await hass.async_add_executor_job(partial(client.models.list, timeout=10)) 157 | 158 | 159 | class FunctionExecutor(ABC): 160 | def __init__(self, data_schema=vol.Schema({})) -> None: 161 | """initialize function executor""" 162 | self.data_schema = data_schema.extend({vol.Required("type"): str}) 163 | 164 | def to_arguments(self, arguments): 165 | """to_arguments function""" 166 | try: 167 | return self.data_schema(arguments) 168 | except vol.error.Error as e: 169 | function_type = next( 170 | (key for key, value in FUNCTION_EXECUTORS.items() if value == self), 171 | None, 172 | ) 173 | raise InvalidFunction(function_type) from e 174 | 175 | def validate_entity_ids(self, hass: HomeAssistant, entity_ids, exposed_entities): 176 | if any(hass.states.get(entity_id) is None for entity_id in entity_ids): 177 | raise EntityNotFound(entity_ids) 178 | exposed_entity_ids = map(lambda e: e["entity_id"], exposed_entities) 179 | if not set(entity_ids).issubset(exposed_entity_ids): 180 | raise EntityNotExposed(entity_ids) 181 | 182 | @abstractmethod 183 | async def execute( 184 | self, 185 | hass: HomeAssistant, 186 | function, 187 | arguments, 188 | user_input: conversation.ConversationInput, 189 | exposed_entities, 190 | ): 191 | """execute function""" 192 | 193 | 194 | class NativeFunctionExecutor(FunctionExecutor): 195 | def __init__(self) -> None: 196 | """initialize native function""" 197 | super().__init__(vol.Schema({vol.Required("name"): str})) 198 | 199 | async def execute( 200 | self, 201 | hass: HomeAssistant, 202 | function, 203 | arguments, 204 | user_input: conversation.ConversationInput, 205 | exposed_entities, 206 | ): 207 | name = function["name"] 208 | if name == "execute_service": 209 | return await self.execute_service( 210 | hass, function, arguments, user_input, exposed_entities 211 | ) 212 | if name == "execute_service_single": 213 | return await self.execute_service_single( 214 | hass, function, arguments, user_input, exposed_entities 215 | ) 216 | if name == "add_automation": 217 | return await self.add_automation( 218 | hass, function, arguments, user_input, exposed_entities 219 | ) 220 | if name == "get_history": 221 | return await self.get_history( 222 | hass, function, arguments, user_input, exposed_entities 223 | ) 224 | if name == "get_energy": 225 | return await self.get_energy( 226 | hass, function, arguments, user_input, exposed_entities 227 | ) 228 | if name == "get_statistics": 229 | return await self.get_statistics( 230 | hass, function, arguments, user_input, exposed_entities 231 | ) 232 | if name == "get_user_from_user_id": 233 | return await self.get_user_from_user_id( 234 | hass, function, arguments, user_input, exposed_entities 235 | ) 236 | 237 | raise NativeNotFound(name) 238 | 239 | async def execute_service_single( 240 | self, 241 | hass: HomeAssistant, 242 | function, 243 | service_argument, 244 | user_input: conversation.ConversationInput, 245 | exposed_entities, 246 | ): 247 | domain = service_argument["domain"] 248 | service = service_argument["service"] 249 | service_data = service_argument.get( 250 | "service_data", service_argument.get("data", {}) 251 | ) 252 | entity_id = service_data.get("entity_id", service_argument.get("entity_id")) 253 | area_id = service_data.get("area_id") 254 | device_id = service_data.get("device_id") 255 | 256 | if isinstance(entity_id, str): 257 | entity_id = [e.strip() for e in entity_id.split(",")] 258 | service_data["entity_id"] = entity_id 259 | 260 | if entity_id is None and area_id is None and device_id is None: 261 | raise CallServiceError(domain, service, service_data) 262 | if not hass.services.has_service(domain, service): 263 | raise ServiceNotFound(domain, service) 264 | self.validate_entity_ids(hass, entity_id or [], exposed_entities) 265 | 266 | try: 267 | await hass.services.async_call( 268 | domain=domain, 269 | service=service, 270 | service_data=service_data, 271 | ) 272 | return {"success": True} 273 | except HomeAssistantError as e: 274 | _LOGGER.error(e) 275 | return {"error": str(e)} 276 | 277 | async def execute_service( 278 | self, 279 | hass: HomeAssistant, 280 | function, 281 | arguments, 282 | user_input: conversation.ConversationInput, 283 | exposed_entities, 284 | ): 285 | result = [] 286 | for service_argument in arguments.get("list", []): 287 | result.append( 288 | await self.execute_service_single( 289 | hass, function, service_argument, user_input, exposed_entities 290 | ) 291 | ) 292 | return result 293 | 294 | async def add_automation( 295 | self, 296 | hass: HomeAssistant, 297 | function, 298 | arguments, 299 | user_input: conversation.ConversationInput, 300 | exposed_entities, 301 | ): 302 | automation_config = yaml.safe_load(arguments["automation_config"]) 303 | config = {"id": str(round(time.time() * 1000))} 304 | if isinstance(automation_config, list): 305 | config.update(automation_config[0]) 306 | if isinstance(automation_config, dict): 307 | config.update(automation_config) 308 | 309 | await _async_validate_config_item(hass, config, True, False) 310 | 311 | automations = [config] 312 | with open( 313 | os.path.join(hass.config.config_dir, AUTOMATION_CONFIG_PATH), 314 | "r", 315 | encoding="utf-8", 316 | ) as f: 317 | current_automations = yaml.safe_load(f.read()) 318 | 319 | with open( 320 | os.path.join(hass.config.config_dir, AUTOMATION_CONFIG_PATH), 321 | "a" if current_automations else "w", 322 | encoding="utf-8", 323 | ) as f: 324 | raw_config = yaml.dump(automations, allow_unicode=True, sort_keys=False) 325 | f.write("\n" + raw_config) 326 | 327 | await hass.services.async_call(automation.config.DOMAIN, SERVICE_RELOAD) 328 | hass.bus.async_fire( 329 | EVENT_AUTOMATION_REGISTERED, 330 | {"automation_config": config, "raw_config": raw_config}, 331 | ) 332 | return "Success" 333 | 334 | async def get_history( 335 | self, 336 | hass: HomeAssistant, 337 | function, 338 | arguments, 339 | user_input: conversation.ConversationInput, 340 | exposed_entities, 341 | ): 342 | start_time = arguments.get("start_time") 343 | end_time = arguments.get("end_time") 344 | entity_ids = arguments.get("entity_ids", []) 345 | include_start_time_state = arguments.get("include_start_time_state", True) 346 | significant_changes_only = arguments.get("significant_changes_only", True) 347 | minimal_response = arguments.get("minimal_response", True) 348 | no_attributes = arguments.get("no_attributes", True) 349 | 350 | now = dt_util.utcnow() 351 | one_day = timedelta(days=1) 352 | start_time = self.as_utc(start_time, now - one_day, "start_time not valid") 353 | end_time = self.as_utc(end_time, start_time + one_day, "end_time not valid") 354 | 355 | self.validate_entity_ids(hass, entity_ids, exposed_entities) 356 | 357 | with recorder.util.session_scope(hass=hass, read_only=True) as session: 358 | result = await recorder.get_instance(hass).async_add_executor_job( 359 | recorder.history.get_significant_states_with_session, 360 | hass, 361 | session, 362 | start_time, 363 | end_time, 364 | entity_ids, 365 | None, 366 | include_start_time_state, 367 | significant_changes_only, 368 | minimal_response, 369 | no_attributes, 370 | ) 371 | 372 | return [[self.as_dict(item) for item in sublist] for sublist in result.values()] 373 | 374 | async def get_energy( 375 | self, 376 | hass: HomeAssistant, 377 | function, 378 | arguments, 379 | user_input: conversation.ConversationInput, 380 | exposed_entities, 381 | ): 382 | energy_manager: energy.data.EnergyManager = await energy.async_get_manager(hass) 383 | return energy_manager.data 384 | 385 | async def get_user_from_user_id( 386 | self, 387 | hass: HomeAssistant, 388 | function, 389 | arguments, 390 | user_input: conversation.ConversationInput, 391 | exposed_entities, 392 | ): 393 | user = await hass.auth.async_get_user(user_input.context.user_id) 394 | return {'name': user.name if user and hasattr(user, 'name') else 'Unknown'} 395 | 396 | async def get_statistics( 397 | self, 398 | hass: HomeAssistant, 399 | function, 400 | arguments, 401 | user_input: conversation.ConversationInput, 402 | exposed_entities, 403 | ): 404 | statistic_ids = arguments.get("statistic_ids", []) 405 | start_time = dt_util.as_utc(dt_util.parse_datetime(arguments["start_time"])) 406 | end_time = dt_util.as_utc(dt_util.parse_datetime(arguments["end_time"])) 407 | 408 | return await recorder.get_instance(hass).async_add_executor_job( 409 | recorder.statistics.statistics_during_period, 410 | hass, 411 | start_time, 412 | end_time, 413 | statistic_ids, 414 | arguments.get("period", "day"), 415 | arguments.get("units"), 416 | arguments.get("types", {"change"}), 417 | ) 418 | 419 | def as_utc(self, value: str, default_value, parse_error_message: str): 420 | if value is None: 421 | return default_value 422 | 423 | parsed_datetime = dt_util.parse_datetime(value) 424 | if parsed_datetime is None: 425 | raise HomeAssistantError(parse_error_message) 426 | 427 | return dt_util.as_utc(parsed_datetime) 428 | 429 | def as_dict(self, state: State | dict[str, Any]): 430 | if isinstance(state, State): 431 | return state.as_dict() 432 | return state 433 | 434 | 435 | class ScriptFunctionExecutor(FunctionExecutor): 436 | def __init__(self) -> None: 437 | """initialize script function""" 438 | super().__init__(SCRIPT_ENTITY_SCHEMA) 439 | 440 | async def execute( 441 | self, 442 | hass: HomeAssistant, 443 | function, 444 | arguments, 445 | user_input: conversation.ConversationInput, 446 | exposed_entities, 447 | ): 448 | script = Script( 449 | hass, 450 | function["sequence"], 451 | "extended_deepseek_conversation", 452 | DOMAIN, 453 | running_description="[extended_deepseek_conversation] function", 454 | logger=_LOGGER, 455 | ) 456 | 457 | result = await script.async_run( 458 | run_variables=arguments, context=user_input.context 459 | ) 460 | return result.variables.get("_function_result", "Success") 461 | 462 | 463 | class TemplateFunctionExecutor(FunctionExecutor): 464 | def __init__(self) -> None: 465 | """initialize template function""" 466 | super().__init__( 467 | vol.Schema( 468 | { 469 | vol.Required("value_template"): cv.template, 470 | vol.Optional("parse_result"): bool, 471 | } 472 | ) 473 | ) 474 | 475 | async def execute( 476 | self, 477 | hass: HomeAssistant, 478 | function, 479 | arguments, 480 | user_input: conversation.ConversationInput, 481 | exposed_entities, 482 | ): 483 | return function["value_template"].async_render( 484 | arguments, 485 | parse_result=function.get("parse_result", False), 486 | ) 487 | 488 | 489 | class RestFunctionExecutor(FunctionExecutor): 490 | def __init__(self) -> None: 491 | """initialize Rest function""" 492 | super().__init__( 493 | vol.Schema(rest.RESOURCE_SCHEMA).extend( 494 | { 495 | vol.Optional("value_template"): cv.template, 496 | vol.Optional("payload_template"): cv.template, 497 | } 498 | ) 499 | ) 500 | 501 | async def execute( 502 | self, 503 | hass: HomeAssistant, 504 | function, 505 | arguments, 506 | user_input: conversation.ConversationInput, 507 | exposed_entities, 508 | ): 509 | config = function 510 | rest_data = _get_rest_data(hass, config, arguments) 511 | 512 | await rest_data.async_update() 513 | value = rest_data.data_without_xml() 514 | value_template = config.get(CONF_VALUE_TEMPLATE) 515 | 516 | if value is not None and value_template is not None: 517 | value = value_template.async_render_with_possible_json_value( 518 | value, None, arguments 519 | ) 520 | 521 | return value 522 | 523 | 524 | class ScrapeFunctionExecutor(FunctionExecutor): 525 | def __init__(self) -> None: 526 | """initialize Scrape function""" 527 | super().__init__( 528 | scrape.COMBINED_SCHEMA.extend( 529 | { 530 | vol.Optional("value_template"): cv.template, 531 | vol.Optional("payload_template"): cv.template, 532 | } 533 | ) 534 | ) 535 | 536 | async def execute( 537 | self, 538 | hass: HomeAssistant, 539 | function, 540 | arguments, 541 | user_input: conversation.ConversationInput, 542 | exposed_entities, 543 | ): 544 | config = function 545 | rest_data = _get_rest_data(hass, config, arguments) 546 | coordinator = scrape.coordinator.ScrapeCoordinator( 547 | hass, 548 | rest_data, 549 | scrape.const.DEFAULT_SCAN_INTERVAL, 550 | ) 551 | await coordinator.async_config_entry_first_refresh() 552 | 553 | new_arguments = dict(arguments) 554 | 555 | for sensor_config in config["sensor"]: 556 | name: Template = sensor_config.get(CONF_NAME) 557 | value = self._async_update_from_rest_data( 558 | coordinator.data, sensor_config, arguments 559 | ) 560 | new_arguments["value"] = value 561 | if name: 562 | new_arguments[name.async_render()] = value 563 | 564 | result = new_arguments["value"] 565 | value_template = config.get(CONF_VALUE_TEMPLATE) 566 | 567 | if value_template is not None: 568 | result = value_template.async_render_with_possible_json_value( 569 | result, None, new_arguments 570 | ) 571 | 572 | return result 573 | 574 | def _async_update_from_rest_data( 575 | self, 576 | data: BeautifulSoup, 577 | sensor_config: dict[str, Any], 578 | arguments: dict[str, Any], 579 | ) -> None: 580 | """Update state from the rest data.""" 581 | value = self._extract_value(data, sensor_config) 582 | value_template = sensor_config.get(CONF_VALUE_TEMPLATE) 583 | 584 | if value_template is not None: 585 | value = value_template.async_render_with_possible_json_value( 586 | value, None, arguments 587 | ) 588 | 589 | return value 590 | 591 | def _extract_value(self, data: BeautifulSoup, sensor_config: dict[str, Any]) -> Any: 592 | """Parse the html extraction in the executor.""" 593 | value: str | list[str] | None 594 | select = sensor_config[scrape.const.CONF_SELECT] 595 | index = sensor_config.get(scrape.const.CONF_INDEX, 0) 596 | attr = sensor_config.get(CONF_ATTRIBUTE) 597 | try: 598 | if attr is not None: 599 | value = data.select(select)[index][attr] 600 | else: 601 | tag = data.select(select)[index] 602 | if tag.name in ("style", "script", "template"): 603 | value = tag.string 604 | else: 605 | value = tag.text 606 | except IndexError: 607 | _LOGGER.warning("Index '%s' not found", index) 608 | value = None 609 | except KeyError: 610 | _LOGGER.warning("Attribute '%s' not found", attr) 611 | value = None 612 | _LOGGER.debug("Parsed value: %s", value) 613 | return value 614 | 615 | 616 | class CompositeFunctionExecutor(FunctionExecutor): 617 | def __init__(self) -> None: 618 | """initialize composite function""" 619 | super().__init__( 620 | vol.Schema( 621 | { 622 | vol.Required("sequence"): vol.All( 623 | cv.ensure_list, [self.function_schema] 624 | ) 625 | } 626 | ) 627 | ) 628 | 629 | def function_schema(self, value: Any) -> dict: 630 | """Validate a composite function schema.""" 631 | if not isinstance(value, dict): 632 | raise vol.Invalid("expected dictionary") 633 | 634 | composite_schema = {vol.Optional("response_variable"): str} 635 | function_executor = get_function_executor(value["type"]) 636 | 637 | return function_executor.data_schema.extend(composite_schema)(value) 638 | 639 | async def execute( 640 | self, 641 | hass: HomeAssistant, 642 | function, 643 | arguments, 644 | user_input: conversation.ConversationInput, 645 | exposed_entities, 646 | ): 647 | config = function 648 | sequence = config["sequence"] 649 | 650 | for executor_config in sequence: 651 | function_executor = get_function_executor(executor_config["type"]) 652 | result = await function_executor.execute( 653 | hass, executor_config, arguments, user_input, exposed_entities 654 | ) 655 | 656 | response_variable = executor_config.get("response_variable") 657 | if response_variable: 658 | arguments[response_variable] = result 659 | 660 | return result 661 | 662 | 663 | class SqliteFunctionExecutor(FunctionExecutor): 664 | def __init__(self) -> None: 665 | """initialize sqlite function""" 666 | super().__init__( 667 | vol.Schema( 668 | { 669 | vol.Optional("query"): str, 670 | vol.Optional("db_url"): str, 671 | vol.Optional("single"): bool, 672 | } 673 | ) 674 | ) 675 | 676 | def is_exposed(self, entity_id, exposed_entities) -> bool: 677 | return any( 678 | exposed_entity["entity_id"] == entity_id 679 | for exposed_entity in exposed_entities 680 | ) 681 | 682 | def is_exposed_entity_in_query(self, query: str, exposed_entities) -> bool: 683 | exposed_entity_ids = list( 684 | map(lambda e: f"'{e['entity_id']}'", exposed_entities) 685 | ) 686 | return any( 687 | exposed_entity_id in query for exposed_entity_id in exposed_entity_ids 688 | ) 689 | 690 | def raise_error(self, msg="Unexpected error occurred."): 691 | raise HomeAssistantError(msg) 692 | 693 | def get_default_db_url(self, hass: HomeAssistant) -> str: 694 | db_file_path = os.path.join(hass.config.config_dir, recorder.DEFAULT_DB_FILE) 695 | return f"file:{db_file_path}?mode=ro" 696 | 697 | def set_url_read_only(self, url: str) -> str: 698 | scheme, netloc, path, query_string, fragment = parse.urlsplit(url) 699 | query_params = parse.parse_qs(query_string) 700 | 701 | query_params["mode"] = ["ro"] 702 | new_query_string = parse.urlencode(query_params, doseq=True) 703 | 704 | return parse.urlunsplit((scheme, netloc, path, new_query_string, fragment)) 705 | 706 | async def execute( 707 | self, 708 | hass: HomeAssistant, 709 | function, 710 | arguments, 711 | user_input: conversation.ConversationInput, 712 | exposed_entities, 713 | ): 714 | db_url = self.set_url_read_only( 715 | function.get("db_url", self.get_default_db_url(hass)) 716 | ) 717 | query = function.get("query", "{{query}}") 718 | 719 | template_arguments = { 720 | "is_exposed": lambda e: self.is_exposed(e, exposed_entities), 721 | "is_exposed_entity_in_query": lambda q: self.is_exposed_entity_in_query( 722 | q, exposed_entities 723 | ), 724 | "exposed_entities": exposed_entities, 725 | "raise": self.raise_error, 726 | } 727 | template_arguments.update(arguments) 728 | 729 | q = Template(query, hass).async_render(template_arguments) 730 | _LOGGER.info("Rendered query: %s", q) 731 | 732 | with sqlite3.connect(db_url, uri=True) as conn: 733 | cursor = conn.cursor().execute(q) 734 | names = [description[0] for description in cursor.description] 735 | 736 | if function.get("single") is True: 737 | row = cursor.fetchone() 738 | return {name: val for name, val in zip(names, row)} 739 | 740 | rows = cursor.fetchall() 741 | result = [] 742 | for row in rows: 743 | result.append({name: val for name, val in zip(names, row)}) 744 | return result 745 | 746 | 747 | FUNCTION_EXECUTORS: dict[str, FunctionExecutor] = { 748 | "native": NativeFunctionExecutor(), 749 | "script": ScriptFunctionExecutor(), 750 | "template": TemplateFunctionExecutor(), 751 | "rest": RestFunctionExecutor(), 752 | "scrape": ScrapeFunctionExecutor(), 753 | "composite": CompositeFunctionExecutor(), 754 | "sqlite": SqliteFunctionExecutor(), 755 | } 756 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "extended_deepseek_conversation", 3 | "name": "Extended DeepSeek Conversation", 4 | "codeowners": [ 5 | "@itsRhen" 6 | ], 7 | "config_flow": true, 8 | "dependencies": [ 9 | "conversation", 10 | "energy", 11 | "history", 12 | "recorder", 13 | "rest", 14 | "scrape" 15 | ], 16 | "documentation": "https://github.com/ItsRhen/extended_deepseek_conversation", 17 | "integration_type": "service", 18 | "iot_class": "cloud_polling", 19 | "issue_tracker": "https://github.com/ItsRhen/extended_deepseek_conversation/issues", 20 | "requirements": [ 21 | "openai~=1.3.8" 22 | ], 23 | "version": "1.0.4" 24 | } -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/services.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import mimetypes 4 | from pathlib import Path 5 | from urllib.parse import urlparse 6 | 7 | from openai import AsyncOpenAI 8 | from openai._exceptions import OpenAIError 9 | from openai.types.chat.chat_completion_content_part_image_param import ( 10 | ChatCompletionContentPartImageParam, 11 | ) 12 | import voluptuous as vol 13 | 14 | from homeassistant.core import ( 15 | HomeAssistant, 16 | ServiceCall, 17 | ServiceResponse, 18 | SupportsResponse, 19 | ) 20 | from homeassistant.exceptions import HomeAssistantError 21 | from homeassistant.helpers import config_validation as cv, selector 22 | from homeassistant.helpers.typing import ConfigType 23 | 24 | from .const import DOMAIN, SERVICE_QUERY_IMAGE 25 | 26 | QUERY_IMAGE_SCHEMA = vol.Schema( 27 | { 28 | vol.Required("config_entry"): selector.ConfigEntrySelector( 29 | { 30 | "integration": DOMAIN, 31 | } 32 | ), 33 | vol.Required("model", default="deepseek-chat"): cv.string, 34 | vol.Required("prompt"): cv.string, 35 | vol.Required("images"): vol.All(cv.ensure_list, [{"url": cv.string}]), 36 | vol.Optional("max_tokens", default=1024): cv.positive_int, 37 | } 38 | ) 39 | 40 | _LOGGER = logging.getLogger(__package__) 41 | 42 | 43 | async def async_setup_services(hass: HomeAssistant, config: ConfigType) -> None: 44 | """Set up services for the extended openai conversation component.""" 45 | 46 | async def query_image(call: ServiceCall) -> ServiceResponse: 47 | """Query an image.""" 48 | try: 49 | model = call.data["model"] 50 | images = [ 51 | {"type": "image_url", "image_url": to_image_param(hass, image)} 52 | for image in call.data["images"] 53 | ] 54 | 55 | messages = [ 56 | { 57 | "role": "user", 58 | "content": [{"type": "text", "text": call.data["prompt"]}] + images, 59 | } 60 | ] 61 | _LOGGER.info("Prompt for %s: %s", model, messages) 62 | 63 | response = await AsyncOpenAI( 64 | api_key=hass.data[DOMAIN][call.data["config_entry"]]["api_key"] 65 | ).chat.completions.create( 66 | model=model, 67 | messages=messages, 68 | max_tokens=call.data["max_tokens"], 69 | ) 70 | response_dict = response.model_dump() 71 | _LOGGER.info("Response %s", response_dict) 72 | except OpenAIError as err: 73 | raise HomeAssistantError(f"Error generating image: {err}") from err 74 | 75 | return response_dict 76 | 77 | hass.services.async_register( 78 | DOMAIN, 79 | SERVICE_QUERY_IMAGE, 80 | query_image, 81 | schema=QUERY_IMAGE_SCHEMA, 82 | supports_response=SupportsResponse.ONLY, 83 | ) 84 | 85 | 86 | def to_image_param(hass: HomeAssistant, image) -> ChatCompletionContentPartImageParam: 87 | """Convert url to base64 encoded image if local.""" 88 | url = image["url"] 89 | 90 | if urlparse(url).scheme in cv.EXTERNAL_URL_PROTOCOL_SCHEMA_LIST: 91 | return image 92 | 93 | if not hass.config.is_allowed_path(url): 94 | raise HomeAssistantError( 95 | f"Cannot read `{url}`, no access to path; " 96 | "`allowlist_external_dirs` may need to be adjusted in " 97 | "`configuration.yaml`" 98 | ) 99 | if not Path(url).exists(): 100 | raise HomeAssistantError(f"`{url}` does not exist") 101 | mime_type, _ = mimetypes.guess_type(url) 102 | if mime_type is None or not mime_type.startswith("image"): 103 | raise HomeAssistantError(f"`{url}` is not an image") 104 | 105 | image["url"] = f"data:{mime_type};base64,{encode_image(url)}" 106 | return image 107 | 108 | 109 | def encode_image(image_path): 110 | """Convert to base64 encoded image.""" 111 | with open(image_path, "rb") as image_file: 112 | return base64.b64encode(image_file.read()).decode("utf-8") 113 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/services.yaml: -------------------------------------------------------------------------------- 1 | query_image: 2 | fields: 3 | config_entry: 4 | required: true 5 | selector: 6 | config_entry: 7 | integration: extended_deepseek_conversation 8 | model: 9 | example: deepseek-chat 10 | selector: 11 | text: 12 | prompt: 13 | example: "What’s in this image?" 14 | required: true 15 | selector: 16 | text: 17 | multiline: true 18 | images: 19 | example: '{"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"}' 20 | required: true 21 | default: [] 22 | selector: 23 | object: 24 | max_tokens: 25 | example: 300 26 | default: 300 27 | selector: 28 | number: 29 | min: 1 30 | mode: box 31 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "name": "[%key:common::config_flow::data::name%]", 7 | "api_key": "[%key:common::config_flow::data::api_key%]", 8 | "base_url": "[%key:common::config_flow::data::base_url%]", 9 | "api_version": "[%key:common::config_flow::data::api_version%]", 10 | "organization": "[%key:common::config_flow::data::organization%]", 11 | "skip_authentication": "[%key:common::config_flow::data::skip_authentication%]" 12 | } 13 | } 14 | }, 15 | "error": { 16 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 17 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 18 | "unknown": "[%key:common::config_flow::error::unknown%]" 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "prompt": "Prompt Template", 26 | "model": "Completion Model", 27 | "max_tokens": "Maximum tokens to return in response", 28 | "temperature": "Temperature", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Maximum function calls per conversation", 31 | "functions": "Functions", 32 | "attach_username": "Attach Username to Message", 33 | "use_tools": "Use Tools", 34 | "context_threshold": "Context Threshold", 35 | "context_truncate_strategy": "Context truncation strategy when exceeded threshold" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Query image", 43 | "description": "Take in images and answer questions about them", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Config Entry", 47 | "description": "The config entry to use for this service" 48 | }, 49 | "model": { 50 | "name": "Model", 51 | "description": "The model", 52 | "example": "deepseek-chat" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "The text to ask about image", 57 | "example": "What’s in this image?" 58 | }, 59 | "images": { 60 | "name": "Images", 61 | "description": "A list of images that would be asked", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "The maximum tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Fehler beim Verbindungsaufbau", 5 | "invalid_auth": "Authentifizierung fehlgeschlagen", 6 | "unknown": "Unbekannter Fehler" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Name", 12 | "api_key": "API Key", 13 | "base_url": "Base Url", 14 | "api_version": "Api Version", 15 | "organization": "Organization", 16 | "skip_authentication": "Authentifizierung überspringen" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "max_tokens": "Maximale Anzahl an Tokens, die in einer Antwort zurückgegeben werden", 26 | "model": "Completion Model", 27 | "prompt": "Prompt Vorlage", 28 | "temperature": "Temperatur", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Maximale Anzahl an Funktionsaufrufen pro Konversation", 31 | "functions": "Funktionen", 32 | "attach_username": "Benutzernamen mitgeben", 33 | "use_tools": "Use Tools", 34 | "context_threshold": "Context Threshold", 35 | "context_truncate_strategy": "Context truncation strategy when exceeded threshold" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Query image", 43 | "description": "Take in images and answer questions about them", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Config Entry", 47 | "description": "The config entry to use for this service" 48 | }, 49 | "model": { 50 | "name": "Model", 51 | "description": "The model", 52 | "example": "gpt-4-vision-preview" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "The text to ask about image", 57 | "example": "What’s in this image?" 58 | }, 59 | "images": { 60 | "name": "Images", 61 | "description": "A list of images that would be asked", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "The maximum tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Failed to connect", 5 | "invalid_auth": "Invalid authentication", 6 | "unknown": "Unexpected error" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Name", 12 | "api_key": "API Key", 13 | "base_url": "Base Url", 14 | "api_version": "Api Version", 15 | "organization": "Organization", 16 | "skip_authentication": "Skip Authentication" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "max_tokens": "Maximum tokens to return in response", 26 | "model": "Completion Model", 27 | "prompt": "Prompt Template", 28 | "temperature": "Temperature", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Maximum function calls per conversation", 31 | "functions": "Functions", 32 | "attach_username": "Attach Username to Message", 33 | "use_tools": "Use Tools", 34 | "context_threshold": "Context Threshold", 35 | "context_truncate_strategy": "Context truncation strategy when exceeded threshold" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Query image", 43 | "description": "Take in images and answer questions about them", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Config Entry", 47 | "description": "The config entry to use for this service" 48 | }, 49 | "model": { 50 | "name": "Model", 51 | "description": "The model", 52 | "example": "deepseek-chat" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "The text to ask about image", 57 | "example": "What’s in this image?" 58 | }, 59 | "images": { 60 | "name": "Images", 61 | "description": "A list of images that would be asked", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "The maximum tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Échec de la connexion", 5 | "invalid_auth": "Authentification invalide", 6 | "unknown": "Erreur inconnue" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Nom", 12 | "api_key": "Clé d'API", 13 | "base_url": "Base de l'URL", 14 | "api_version": "Version de l'API", 15 | "organization": "Organization", 16 | "skip_authentication": "Ignorer l'authentification" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "max_tokens": "Nombre maximum de jetons facturé pour la réponse", 26 | "model": "Modèle d'IA", 27 | "prompt": "Prompt", 28 | "temperature": "Température", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Nombre maximal d'appels de fonction par conversation", 31 | "functions": "Fonctions", 32 | "attach_username": "Joindre le nom d'utilisateur au message", 33 | "use_tools": "Use Tools", 34 | "context_threshold": "Context Threshold", 35 | "context_truncate_strategy": "Context truncation strategy when exceeded threshold" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Query image", 43 | "description": "Take in images and answer questions about them", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Config Entry", 47 | "description": "The config entry to use for this service" 48 | }, 49 | "model": { 50 | "name": "Model", 51 | "description": "The model", 52 | "example": "gpt-4-vision-preview" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "The text to ask about image", 57 | "example": "What’s in this image?" 58 | }, 59 | "images": { 60 | "name": "Images", 61 | "description": "A list of images that would be asked", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "The maximum tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Nem sikerült csatlakozni", 5 | "invalid_auth": "Azonosítás sikertelen", 6 | "unknown": "Váratlan hiba" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Név", 12 | "api_key": "API Kulcs", 13 | "base_url": "Base Url", 14 | "api_version": "API Verzió", 15 | "organization": "Organization", 16 | "skip_authentication": "Azonosítás átugrása" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "max_tokens": "A válaszként viszaküldhető maximum tokenek száma", 26 | "model": "Model", 27 | "prompt": "Kiindulási szöveg sablon", 28 | "temperature": "Hőmérséklet", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Beszélgetésenkénti maximum funkcióhívások száma", 31 | "functions": "Funkciók", 32 | "attach_username": "Felhasználónév hozzácsatolása az üzenethez", 33 | "use_tools": "Use Tools", 34 | "context_threshold": "Context Threshold", 35 | "context_truncate_strategy": "Context truncation strategy when exceeded threshold" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Query image", 43 | "description": "Take in images and answer questions about them", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Config Entry", 47 | "description": "The config entry to use for this service" 48 | }, 49 | "model": { 50 | "name": "Model", 51 | "description": "The model", 52 | "example": "gpt-4-vision-preview" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "The text to ask about image", 57 | "example": "What’s in this image?" 58 | }, 59 | "images": { 60 | "name": "Images", 61 | "description": "A list of images that would be asked", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "The maximum tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Connessione non riuscita", 5 | "invalid_auth": "Autenticazione non valida", 6 | "unknown": "Errore imprevisto" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Nome", 12 | "api_key": "Chiave API", 13 | "base_url": "URL di base", 14 | "api_version": "Versione API", 15 | "skip_authentication": "Ignora autenticazione" 16 | } 17 | } 18 | } 19 | }, 20 | "options": { 21 | "step": { 22 | "init": { 23 | "data": { 24 | "max_tokens": "Numero massimo token da restituire nella risposta", 25 | "model": "Modello di completamento", 26 | "prompt": "Modello di prompt", 27 | "temperature": "Temperatura", 28 | "top_p": "Top P", 29 | "max_function_calls_per_conversation": "Numero massimo di chiamate di funzioni per conversazione", 30 | "functions": "Funzioni", 31 | "attach_username": "Allega nome utente al messaggio", 32 | "use_tools": "Utilizza strumenti", 33 | "context_threshold": "Soglia di contesto", 34 | "context_truncate_strategy": "Strategia di troncamento del contesto quando superata la soglia" 35 | } 36 | } 37 | } 38 | }, 39 | "services": { 40 | "query_image": { 41 | "name": "Interrogazione immagine", 42 | "description": "Prendi le immagini e rispondi alle domande su di esse", 43 | "fields": { 44 | "config_entry": { 45 | "name": "Voce di configurazione", 46 | "description": "La voce di configurazione da utilizzare per questo servizio" 47 | }, 48 | "model": { 49 | "name": "Modello", 50 | "description": "Il modello", 51 | "example": "gpt-4-vision-preview" 52 | }, 53 | "prompt": { 54 | "name": "Prompt", 55 | "description": "Il testo da chiedere riguardo all'immagine", 56 | "example": "Cosa c'è in questa immagine?" 57 | }, 58 | "images": { 59 | "name": "Immagini", 60 | "description": "Un elenco delle immagini richieste", 61 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 62 | }, 63 | "max_tokens": { 64 | "name": "Max Token", 65 | "description": "I token massimi", 66 | "example": "300" 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Failed to connect", 5 | "invalid_auth": "Invalid authentication", 6 | "unknown": "Unexpected error" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Name", 12 | "api_key": "API Key", 13 | "base_url": "Base Url", 14 | "api_version": "Api Version", 15 | "organization": "Organization", 16 | "skip_authentication": "Skip Authentication" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "max_tokens": "Maximum tokens to return in response", 26 | "model": "Completion Model", 27 | "prompt": "Prompt Template", 28 | "temperature": "Temperature", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Maximum function calls per conversation", 31 | "functions": "Functions", 32 | "attach_username": "Attach Username to Message", 33 | "use_tools": "Use Tools", 34 | "context_threshold": "Context Threshold", 35 | "context_truncate_strategy": "Context truncation strategy when exceeded threshold" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Query image", 43 | "description": "Take in images and answer questions about them", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Config Entry", 47 | "description": "The config entry to use for this service" 48 | }, 49 | "model": { 50 | "name": "Model", 51 | "description": "The model", 52 | "example": "gpt-4-vision-preview" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "The text to ask about image", 57 | "example": "What’s in this image?" 58 | }, 59 | "images": { 60 | "name": "Images", 61 | "description": "A list of images that would be asked", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "The maximum tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Verbinden mislukt", 5 | "invalid_auth": "Ongeldige authenticatie", 6 | "unknown": "Onverwachte fout" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Naam", 12 | "api_key": "API Sleutel", 13 | "base_url": "Basis URL", 14 | "api_version": "API Version", 15 | "organization": "Organization", 16 | "skip_authentication": "Authenticatie overslaan" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "max_tokens": "Maximale aantal tokens dat mag worden gegenereerd", 26 | "model": "Completion Model", 27 | "prompt": "Prompt Sjabloon", 28 | "temperature": "Temperatuur", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Maximale keren functies mogen worden aangeroepen per conversatie", 31 | "functions": "Functies", 32 | "attach_username": "Gebruikersnaam aan bericht toevoegen", 33 | "use_tools": "Use Tools", 34 | "context_threshold": "Context Threshold", 35 | "context_truncate_strategy": "Context truncation strategy when exceeded threshold" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Query image", 43 | "description": "Take in images and answer questions about them", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Config Entry", 47 | "description": "The config entry to use for this service" 48 | }, 49 | "model": { 50 | "name": "Model", 51 | "description": "The model", 52 | "example": "gpt-4-vision-preview" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "The text to ask about image", 57 | "example": "What’s in this image?" 58 | }, 59 | "images": { 60 | "name": "Images", 61 | "description": "A list of images that would be asked", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "The maximum tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Nie udało się połączyć", 5 | "invalid_auth": "Błąd authentykacji", 6 | "unknown": "Nieznany błąd" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Imię", 12 | "api_key": "Klucz API", 13 | "base_url": "Bazowy URL", 14 | "api_version": "Wersja API", 15 | "organization": "Organization", 16 | "skip_authentication": "Pomiń authentykację" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "max_tokens": "Maksymalna ilość tokenów w odpowiedzi", 26 | "model": "Model", 27 | "prompt": "Prompt", 28 | "temperature": "Temperatura", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Maksymalna ilość wywołań funkcji na rozmowę", 31 | "functions": "Funkcje", 32 | "attach_username": "Dodaj nazwę użytkownika do wiadomości", 33 | "use_tools": "Use Tools", 34 | "context_threshold": "Context Threshold", 35 | "context_truncate_strategy": "Context truncation strategy when exceeded threshold" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Query image", 43 | "description": "Take in images and answer questions about them", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Config Entry", 47 | "description": "The config entry to use for this service" 48 | }, 49 | "model": { 50 | "name": "Model", 51 | "description": "The model", 52 | "example": "gpt-4-vision-preview" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "The text to ask about image", 57 | "example": "What’s in this image?" 58 | }, 59 | "images": { 60 | "name": "Images", 61 | "description": "A list of images that would be asked", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "The maximum tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /custom_components/extended_openai_conversation/translations/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": { 4 | "cannot_connect": "Não é possível conectar", 5 | "invalid_auth": "Autenticação inválida", 6 | "unknown": "Erro desconhecido" 7 | }, 8 | "step": { 9 | "user": { 10 | "data": { 11 | "name": "Nome", 12 | "api_key": "Chave API", 13 | "base_url": "Base Url", 14 | "api_version": "Versão da API", 15 | "organization": "Organização", 16 | "skip_authentication": "Pular autenticação" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "max_tokens": "Número máximo de tokens da resposta", 26 | "model": "Modelo da Conclusão", 27 | "prompt": "Template do Prompt", 28 | "temperature": "Temperatura", 29 | "top_p": "Top P", 30 | "max_function_calls_per_conversation": "Quantidade máxima de chamadas por conversação", 31 | "functions": "Funções", 32 | "attach_username": "Anexar nome do usuário na mensagem", 33 | "use_tools": "Use ferramentas", 34 | "context_threshold": "Limite do contexto", 35 | "context_truncate_strategy": "Estratégia de truncamento de contexto quando o limite é excedido" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "query_image": { 42 | "name": "Consultar imagem", 43 | "description": "Receba imagens e responda perguntas sobre elas", 44 | "fields": { 45 | "config_entry": { 46 | "name": "Registro de configuração", 47 | "description": "O registro de configuração para utilizar neste serviço" 48 | }, 49 | "model": { 50 | "name": "Modelo", 51 | "description": "Especificar modelo", 52 | "example": "gpt-4-vision-preview" 53 | }, 54 | "prompt": { 55 | "name": "Prompt", 56 | "description": "O texto para fazer a pergunta sobre a imagem", 57 | "example": "O que tem nesta imagem?" 58 | }, 59 | "images": { 60 | "name": "Imagens", 61 | "description": "Uma lista de imagens que serão analisadas", 62 | "example": "{\"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"}" 63 | }, 64 | "max_tokens": { 65 | "name": "Max Tokens", 66 | "description": "Quantidade máxima de tokens", 67 | "example": "300" 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /examples/component_function/17track/README.ko.md: -------------------------------------------------------------------------------- 1 | ## Requirement 2 | Assume using [17track](https://www.home-assistant.io/integrations/seventeentrack) 3 | 4 | ## Function 5 | 6 | ### get_incoming_packages 7 | 8 | ```yaml 9 | - spec: 10 | name: get_incoming_packages 11 | description: Use this function to retrieve information about incoming packages. 12 | parameters: 13 | type: object 14 | properties: {} 15 | function: 16 | type: template 17 | value_template: >- 18 | {% set ns = namespace(current_status=None) %} 19 | {% set statuses = { 20 | 'expired': states('sensor.17track_packages_expired')|int, 21 | 'undelivered': states('sensor.17track_packages_undelivered')|int, 22 | 'delivered': states('sensor.17track_packages_delivered')|int, 23 | 'ready_to_be_picked_up': states('sensor.17track_packages_ready_to_be_picked_up')|int, 24 | 'returned': states('sensor.17track_packages_returned')|int, 25 | 'in_transit': states('sensor.17track_packages_in_transit')|int, 26 | 'not_found': states('sensor.17track_packages_not_found')|int 27 | } %} 28 | {% set priority_order = ['expired', 'undelivered', 'delivered', 'ready_to_be_picked_up', 'returned', 'in_transit', 'not_found'] %} 29 | {% set friendly_status = { 30 | 'expired': '만료', 31 | 'undelivered': '미배송', 32 | 'delivered': '배송 완료', 33 | 'ready_to_be_picked_up': '배송 출발', 34 | 'returned': '반송', 35 | 'in_transit': '배송 중', 36 | 'not_found': '찾을 수 없음' 37 | } %} 38 | ```csv 39 | package status,package name,details 40 | {%- for status in priority_order %} 41 | {%- set current_status = status %} 42 | {%- set current_package_count = statuses[current_status] %} 43 | {%- if current_package_count > 0 %} 44 | {%- set package_details = state_attr('sensor.17track_packages_' + current_status, 'packages') %} 45 | {%- for package in package_details %} 46 | {{ friendly_status[current_status] }},{{ package.friendly_name }},{{ package.info_text | replace(",", ";") }} 47 | {%- endfor %} 48 | {%- endif %} 49 | {%- endfor -%} 50 | ``` 51 | ``` -------------------------------------------------------------------------------- /examples/component_function/17track/README.md: -------------------------------------------------------------------------------- 1 | ## Requirement 2 | Assume using [17track](https://www.home-assistant.io/integrations/seventeentrack) 3 | 4 | ## Function 5 | 6 | ### get_incoming_packages 7 | 8 | ```yaml 9 | - spec: 10 | name: get_incoming_packages 11 | description: Use this function to retrieve information about incoming packages. 12 | parameters: 13 | type: object 14 | properties: {} 15 | function: 16 | type: template 17 | value_template: >- 18 | {% set ns = namespace(current_status=None) %} 19 | {% set statuses = { 20 | 'expired': states('sensor.17track_packages_expired')|int, 21 | 'undelivered': states('sensor.17track_packages_undelivered')|int, 22 | 'delivered': states('sensor.17track_packages_delivered')|int, 23 | 'ready_to_be_picked_up': states('sensor.17track_packages_ready_to_be_picked_up')|int, 24 | 'returned': states('sensor.17track_packages_returned')|int, 25 | 'in_transit': states('sensor.17track_packages_in_transit')|int, 26 | 'not_found': states('sensor.17track_packages_not_found')|int 27 | } %} 28 | {% set priority_order = ['expired', 'undelivered', 'delivered', 'ready_to_be_picked_up', 'returned', 'in_transit', 'not_found'] %} 29 | ```csv 30 | package status,package name,details 31 | {%- for status in priority_order %} 32 | {%- set current_status = status %} 33 | {%- set current_package_count = statuses[current_status] %} 34 | {%- if current_package_count > 0 %} 35 | {%- set package_details = state_attr('sensor.17track_packages_' + current_status, 'packages') %} 36 | {%- for package in package_details %} 37 | {{ current_status }},{{ package.friendly_name }},{{ package.info_text | replace(",", ";") }} 38 | {%- endfor %} 39 | {%- endif %} 40 | {%- endfor -%} 41 | ``` 42 | ``` -------------------------------------------------------------------------------- /examples/component_function/grocy/README.md: -------------------------------------------------------------------------------- 1 | ## Requirement 2 | Assume using [grocy](https://github.com/custom-components/grocy) 3 | 4 | ## Function 5 | ### get_today_chore 6 | ```yaml 7 | - spec: 8 | name: get_today_chore 9 | description: Use this function to retrieve a list of chores due for today or before. 10 | parameters: 11 | type: object 12 | properties: {} 13 | function: 14 | type: template 15 | value_template: >- 16 | {% set now_date = now().date() %} 17 | {% set chores_data = state_attr('sensor.grocy_chores', 'chores') %} 18 | 19 | {% set overdue_chores = chores_data | selectattr('next_estimated_execution_time', 'string') | map(attribute='next_estimated_execution_time') | map('regex_replace', 'T\\d{2}:\\d{2}:\\d{2}', '') | select('lt', now_date | string) | list %} 20 | {% set chores_due_today = chores_data | selectattr('next_estimated_execution_time', 'string') | map(attribute='next_estimated_execution_time') | map('regex_replace', 'T\\d{2}:\\d{2}:\\d{2}', '') | select('equalto', now_date | string) | list %} 21 | {% set combined_chores = overdue_chores + chores_due_today %} 22 | 23 | ```csv 24 | name,last_tracked_time,next_estimated_execution_time 25 | {% for chore in combined_chores %} 26 | {{ chore['name'] | replace(",", " ") }},{{ chore['last_tracked_time'] }},{{ chore['next_estimated_execution_time'] }} 27 | {% endfor -%} 28 | ``` 29 | ``` 30 | 31 | ### execute_chore 32 | 33 | ```yaml 34 | - spec: 35 | name: execute_chore 36 | description: Use this function to execute a chore in Home Assistant. 37 | parameters: 38 | type: object 39 | properties: 40 | chore_id: 41 | type: string 42 | description: The ID of the chore to be executed. 43 | required: 44 | - chore_id 45 | function: 46 | type: script 47 | sequence: 48 | - service: script.execute_chore 49 | data: 50 | chore_id: "{{ chore_id }}" 51 | ``` 52 | 53 | ```yaml 54 | script: 55 | execute_chore: 56 | alias: "Execute Chore" 57 | sequence: 58 | - service: grocy.execute_chore 59 | data: 60 | chore_id: "{{ chore_id }}" 61 | ``` 62 | 63 | ### get_inventory_stock 64 | ```yaml 65 | - spec: 66 | name: get_inventory_stock 67 | description: Use this function to retrieve the inventory entries data. 68 | parameters: 69 | type: object 70 | properties: {} 71 | function: 72 | type: template 73 | value_template: >- 74 | {% set data = states['sensor.grocy_stock'].attributes['products'] | list %} 75 | ```csv 76 | name,id,product_group_id,available_amount,amount_aggregated,amount_opened,amount_opened_aggregated,is_aggregated_amount,best_before_date 77 | {% for product in data -%} 78 | {{ product['name'] }},{{ product['id'] }},{{ product['product_group_id'] }}, 79 | {%- if product['available_amount'] == 0 -%} 80 | {{ product['amount_aggregated'] }}, 81 | {%- else -%} 82 | {{ product['available_amount'] }}, 83 | {%- endif -%} 84 | {{ product['amount_aggregated'] }},{{ product['amount_opened'] }},{{ product['amount_opened_aggregated'] }},{{ product['is_aggregated_amount'] }},{{ product['best_before_date'] }} 85 | {% endfor -%} 86 | ``` 87 | ``` -------------------------------------------------------------------------------- /examples/component_function/o365/README.md: -------------------------------------------------------------------------------- 1 | ## Requirement 2 | Assume using [o365](https://github.com/PTST/O365-HomeAssistant) 3 | 4 | ## Function 5 | ### get_email_inbox 6 | ```yaml 7 | - spec: 8 | name: get_email_inbox 9 | description: Use this function to retrieve the list of emails from the inbox. 10 | parameters: 11 | type: object 12 | properties: {} 13 | function: 14 | type: template 15 | value_template: >- 16 | {% set data = states['sensor.inbox'].attributes['data'] | list %} 17 | ```csv 18 | subject,received,to,sender,has_attachments,importance,is_read,body 19 | {% for email in data -%} 20 | "{{ email['subject'] }}","{{ email['received'] }}","{{ email['to'] | join(', ') }}","{{ email['sender'] }}",{{ email['has_attachments'] }},{{ email['importance'] }},{{ email['is_read'] }},"{{ email['body'] | replace('\n', ' ') | replace('"', '\\"') }}" 21 | {% endfor -%} 22 | ``` 23 | ``` -------------------------------------------------------------------------------- /examples/component_function/ytube_music_player/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | ## Requirement 4 | Assume using [ytube_music_player](https://github.com/KoljaWindeler/ytube_music_player) 5 | 6 | ## Function 7 | 8 | ### search_music 9 | ```yaml 10 | - spec: 11 | name: search_music 12 | description: Use this function to search music 13 | parameters: 14 | type: object 15 | properties: 16 | query: 17 | type: string 18 | description: The query 19 | required: 20 | - query 21 | function: 22 | type: composite 23 | sequence: 24 | - type: script 25 | sequence: 26 | - service: ytube_music_player.search 27 | data: 28 | entity_id: media_player.ytube_music_player 29 | query: "{{ query }}" 30 | - type: template 31 | value_template: >- 32 | media_content_type,media_content_id,title 33 | {% for media in state_attr('sensor.ytube_music_player_extra', 'search') -%} 34 | {{media.type}},{{media.id}},{{media.title}} 35 | {% endfor%} 36 | ``` -------------------------------------------------------------------------------- /examples/function/area/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Objective 4 | - Call service via area_id 5 | 6 | 7 | 8 | 9 | ## Function 10 | 11 | ### execute_services 12 | ```yaml 13 | - spec: 14 | name: execute_services 15 | description: Execute service of devices in Home Assistant. 16 | parameters: 17 | type: object 18 | properties: 19 | list: 20 | type: array 21 | items: 22 | type: object 23 | properties: 24 | domain: 25 | type: string 26 | description: The domain of the service. 27 | service: 28 | type: string 29 | description: The service to be called 30 | service_data: 31 | type: object 32 | description: The service data object to indicate what to control. 33 | properties: 34 | entity_id: 35 | type: array 36 | items: 37 | type: string 38 | description: The entity_id retrieved from available devices. It must start with domain, followed by dot character. 39 | area_id: 40 | type: array 41 | items: 42 | type: string 43 | description: The id retrieved from areas. You can specify only area_id without entity_id to act on all entities in that area 44 | required: 45 | - domain 46 | - service 47 | - service_data 48 | function: 49 | type: native 50 | name: execute_service 51 | ``` -------------------------------------------------------------------------------- /examples/function/attributes/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | - Get attributes of entity 3 | 4 | 5 | 6 | 7 | ## Function 8 | 9 | ### get_attributes 10 | ```yaml 11 | - spec: 12 | name: get_attributes 13 | description: Get attributes of any home assistant entity 14 | parameters: 15 | type: object 16 | properties: 17 | entity_id: 18 | type: string 19 | description: entity_id 20 | required: 21 | - entity_id 22 | function: 23 | type: template 24 | value_template: "{{states[entity_id]}}" 25 | ``` -------------------------------------------------------------------------------- /examples/function/automation/README.ko.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | 4 | 5 | ## Notice 6 | 7 | Before adding automation, I highly recommend set notification on `automation_registered_via_extended_openai_conversation` event and create separate "Extended OpenAI Assistant" and "Assistant" 8 | 9 | (Automation can be added even if conversation fails because of failure to get response message, not automation) 10 | 11 | | Create Assistant | Notify on created | 12 | |----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 13 | | 1 | 스크린샷 2023-10-13 오후 6 01 40 | 14 | 15 | 16 | Copy and paste below configuration into "Functions" 17 | 18 | ## Function 19 | ### add_automation 20 | ```yaml 21 | - spec: 22 | name: add_automation 23 | description: Use this function to add an automation in Home Assistant. 24 | parameters: 25 | type: object 26 | properties: 27 | automation_config: 28 | type: string 29 | description: A configuration for automation in a valid yaml format. Next line character should be \\n, not \n. Use devices from the list. 30 | required: 31 | - automation_config 32 | function: 33 | type: native 34 | name: add_automation 35 | ``` 36 | 37 | -------------------------------------------------------------------------------- /examples/function/automation/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | 4 | 5 | ## Notice 6 | 7 | Before adding automation, I highly recommend set notification on `automation_registered_via_extended_openai_conversation` event and create separate "Extended OpenAI Assistant" and "Assistant" 8 | 9 | (Automation can be added even if conversation fails because of failure to get response message, not automation) 10 | 11 | | Create Assistant | Notify on created | 12 | |----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 13 | | 1 | 스크린샷 2023-10-13 오후 6 01 40 | 14 | 15 | 16 | Copy and paste below configuration into "Functions" 17 | 18 | ## Function 19 | ### add_automation 20 | ```yaml 21 | - spec: 22 | name: add_automation 23 | description: Use this function to add an automation in Home Assistant. 24 | parameters: 25 | type: object 26 | properties: 27 | automation_config: 28 | type: string 29 | description: A configuration for automation in a valid yaml format. Next line character should be \n. Use devices from the list. 30 | required: 31 | - automation_config 32 | function: 33 | type: native 34 | name: add_automation 35 | ``` -------------------------------------------------------------------------------- /examples/function/calendar/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 스크린샷 2023-10-31 오후 9 04 56 3 | 4 | ## Function 5 | ### 1. get_events 6 | ```yaml 7 | - spec: 8 | name: get_events 9 | description: Use this function to get list of calendar events. 10 | parameters: 11 | type: object 12 | properties: 13 | start_date_time: 14 | type: string 15 | description: The start date time in '%Y-%m-%dT%H:%M:%S%z' format 16 | end_date_time: 17 | type: string 18 | description: The end date time in '%Y-%m-%dT%H:%M:%S%z' format 19 | required: 20 | - start_date_time 21 | - end_date_time 22 | function: 23 | type: script 24 | sequence: 25 | - service: calendar.get_events 26 | data: 27 | start_date_time: "{{start_date_time}}" 28 | end_date_time: "{{end_date_time}}" 29 | target: 30 | entity_id: 31 | - calendar.[YourCalendarHere] 32 | - calendar.[MoreCalendarsArePossible] 33 | response_variable: _function_result 34 | ``` 35 | 36 | ### 2. create_event 37 | ```yaml 38 | - spec: 39 | name: create_event 40 | description: Adds a new calendar event. 41 | parameters: 42 | type: object 43 | properties: 44 | summary: 45 | type: string 46 | description: Defines the short summary or subject for the event. 47 | description: 48 | type: string 49 | description: A more complete description of the event than the one provided by the summary. 50 | start_date_time: 51 | type: string 52 | description: The date and time the event should start. 53 | end_date_time: 54 | type: string 55 | description: The date and time the event should end. 56 | location: 57 | type: string 58 | description: The location 59 | required: 60 | - summary 61 | function: 62 | type: script 63 | sequence: 64 | - service: calendar.create_event 65 | data: 66 | summary: "{{summary}}" 67 | description: "{{description}}" 68 | start_date_time: "{{start_date_time}}" 69 | end_date_time: "{{end_date_time}}" 70 | location: "{{location}}" 71 | target: 72 | entity_id: calendar.[YourCalendarHere] 73 | ``` -------------------------------------------------------------------------------- /examples/function/camera_image_query/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | This example provides three functions: 4 | 5 | - query_image: generic query image 6 | - camera_snapshot: generic take camera snapshot and store in /media 7 | - camera_query: a combined take camera snapshot and query image 8 | 9 | Use the first two functions instead of the combined camera_query 10 | function for more flexiblity as they could be used independantly. You 11 | may need to grant file system access via home assistant configuration 12 | of allowlist_external_dirs to /media or your choosen directory. 13 | 14 | ## Function 15 | 16 | ### query_image 17 | ```yaml 18 | - spec: 19 | name: query_image 20 | description: Get description of items or scene from an image 21 | parameters: 22 | type: object 23 | properties: 24 | url: 25 | type: string 26 | description: path or url for image 27 | required: 28 | - url 29 | function: 30 | type: composite 31 | sequence: 32 | - type: script 33 | sequence: 34 | - service: extended_openai_conversation.query_image 35 | data: 36 | model: gpt-4-vision-preview 37 | prompt: What's in this image? 38 | images: 39 | - url: "{{url}}" 40 | max_tokens: 500 41 | config_entry: YOUR_CONFIG_ENTRY 42 | response_variable: _function_result 43 | response_variable: image_result 44 | - type: template 45 | value_template: "{{image_result.choices[0].message.content}}" 46 | ``` 47 | 48 | ### camera_snapshot 49 | ```yaml 50 | - spec: 51 | name: camera_snapshot 52 | description: Generate an image from a camera 53 | parameters: 54 | type: object 55 | properties: 56 | entity_id: 57 | type: string 58 | description: Camera entity 59 | filename: 60 | type: string 61 | description: full path and name of file to generate. Please name it as /media/camera_entity_latest.jpg 62 | required: 63 | - item 64 | function: 65 | type: script 66 | sequence: 67 | - service: camera.snapshot 68 | target: 69 | entity_id: "{{entity_id}}" 70 | data: 71 | filename: '{{filename}}' 72 | ``` 73 | 74 | ### camera_query 75 | ```yaml 76 | - spec: 77 | name: camera_query 78 | description: Get a description of items or scene from a camera 79 | parameters: 80 | type: object 81 | properties: 82 | entity_id: 83 | type: string 84 | description: Camera entity 85 | filename: 86 | type: string 87 | description: full path and name of file to generate. Please name it as /media/camera_entity_latest.jpg 88 | required: 89 | - item 90 | function: 91 | type: composite 92 | sequence: 93 | - type: script 94 | sequence: 95 | - service: camera.snapshot 96 | target: 97 | entity_id: "{{entity_id}}" 98 | data: 99 | filename: '{{filename}}' 100 | - service: extended_openai_conversation.query_image 101 | data: 102 | model: gpt-4-vision-preview 103 | prompt: What's in this image? 104 | images: 105 | - url: "{{filename}}" 106 | max_tokens: 500 107 | config_entry: YOUR_CONFIG_ENTRY 108 | response_variable: _function_result 109 | response_variable: image_result 110 | - type: template 111 | value_template: "{{image_result.choices[0].message.content}}" 112 | ``` 113 | -------------------------------------------------------------------------------- /examples/function/energy/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | - Get energy statistics 3 | 4 | 5 | 6 | 7 | ## Function 8 | 9 | ### get_energy_statistic_ids 10 | ```yaml 11 | - spec: 12 | name: get_energy_statistic_ids 13 | description: Get statistics 14 | parameters: 15 | type: object 16 | properties: 17 | dummy: 18 | type: string 19 | description: Nothing 20 | function: 21 | type: composite 22 | sequence: 23 | - type: native 24 | name: get_energy 25 | response_variable: result 26 | - type: template 27 | value_template: "{{result.device_consumption | map(attribute='stat_consumption') | list}}" 28 | ``` 29 | ### get_statistics 30 | ```yaml 31 | - spec: 32 | name: get_statistics 33 | description: Get statistics 34 | parameters: 35 | type: object 36 | properties: 37 | start_time: 38 | type: string 39 | description: The start datetime 40 | end_time: 41 | type: string 42 | description: The end datetime 43 | statistic_ids: 44 | type: array 45 | items: 46 | type: string 47 | description: The statistic ids 48 | period: 49 | type: string 50 | description: The period 51 | enum: 52 | - day 53 | - week 54 | - month 55 | required: 56 | - start_time 57 | - end_time 58 | - statistic_ids 59 | - period 60 | function: 61 | type: native 62 | name: get_statistics 63 | ``` -------------------------------------------------------------------------------- /examples/function/google_search/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | - Search from Google 3 | 4 | ## Prerequisite 5 | Needs Google API Key 6 | 7 | ## Function 8 | 9 | ### search_google 10 | 11 | ```yaml 12 | - spec: 13 | name: search_google 14 | description: Search Google using the Custom Search API. 15 | parameters: 16 | type: object 17 | properties: 18 | query: 19 | type: string 20 | description: The search query. 21 | required: 22 | - query 23 | function: 24 | type: rest 25 | resource_template: "https://www.googleapis.com/customsearch/v1?key=[GOOGLE_API_KEY]&cx=[GOOGLE_PROGRAMMING_SEARCH_ENGINE]:omuauf_lfve&q={{ query }}&num=3" 26 | value_template: >- 27 | {% if value_json.items %} 28 | ```csv 29 | title,link 30 | {% for item in value_json.items %} 31 | "{{ item.title | replace(',', ' ') }}","{{ item.link }}" 32 | {% endfor %} 33 | ``` 34 | {% else %} 35 | No results found, 36 | {% endif %} 37 | ``` -------------------------------------------------------------------------------- /examples/function/history/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | 4 | 5 | 6 | ## Function 7 | 8 | ### get_history 9 | ```yaml 10 | - spec: 11 | name: get_history 12 | description: Retrieve historical data of specified entities. 13 | parameters: 14 | type: object 15 | properties: 16 | entity_ids: 17 | type: array 18 | items: 19 | type: string 20 | description: The entity id to filter. 21 | start_time: 22 | type: string 23 | description: Start of the history period in "%Y-%m-%dT%H:%M:%S%z". 24 | end_time: 25 | type: string 26 | description: End of the history period in "%Y-%m-%dT%H:%M:%S%z". 27 | required: 28 | - entity_ids 29 | function: 30 | type: composite 31 | sequence: 32 | - type: native 33 | name: get_history 34 | response_variable: history_result 35 | - type: template 36 | value_template: >- 37 | {% set ns = namespace(result = [], list = []) %} 38 | {% for item_list in history_result %} 39 | {% set ns.list = [] %} 40 | {% for item in item_list %} 41 | {% set last_changed = item.last_changed | as_timestamp | timestamp_local if item.last_changed else None %} 42 | {% set new_item = dict(item, last_changed=last_changed) %} 43 | {% set ns.list = ns.list + [new_item] %} 44 | {% endfor %} 45 | {% set ns.result = ns.result + [ns.list] %} 46 | {% endfor %} 47 | {{ ns.result }} 48 | ``` 49 | -------------------------------------------------------------------------------- /examples/function/kakao_bus/README.md: -------------------------------------------------------------------------------- 1 | ## Function 2 | 3 | ### get_bus_info (scrape ver.) 4 | ```yaml 5 | - spec: 6 | name: get_bus_info 7 | description: Use this function to get bus information 8 | parameters: 9 | type: object 10 | properties: 11 | dummy: 12 | type: string 13 | description: Nothing 14 | function: 15 | type: scrape 16 | resource: https://m.map.kakao.com/actions/busStationInfo?busStopId=BS219668 17 | value_template: "remain time: {{[next | trim, next_of_next | trim]}}" 18 | sensor: 19 | - name: next 20 | select: "li[data-id='1100061486'] span.txt_situation" 21 | index: 0 22 | - name: next_of_next 23 | select: "li[data-id='1100061486'] span.txt_situation" 24 | index: 1 25 | ``` 26 | 27 | ### get_bus_info (rest ver.) 28 | ```yaml 29 | - spec: 30 | name: get_bus_info 31 | description: Use this function to get bus information 32 | parameters: 33 | type: object 34 | properties: 35 | dummy: 36 | type: string 37 | description: Nothing. 38 | function: 39 | type: rest 40 | resource: https://m.map.kakao.com/actions/busesInBusStopJson?busStopId=BS219668 41 | value_template: '{{value_json["busesList"] | selectattr("id", "==", "1100061486") | map(attribute="vehicleStateMessage") | list }}' 42 | ``` -------------------------------------------------------------------------------- /examples/function/netflix/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | https://github.com/jekalmin/extended_openai_conversation/assets/2917984/64ba656e-3ae7-4003-9956-da71efaf06dc 3 | 4 | ## Prompt 5 | Add following text in your prompt 6 | ```` 7 | Netflix Video: 8 | ```csv 9 | video_id,title 10 | 81040344,Squid Game 11 | ``` 12 | ```` 13 | ## Function 14 | 15 | ### play_netflix 16 | #### webostv 17 | ```yaml 18 | - spec: 19 | name: play_netflix 20 | description: Use this function to play Netflix. 21 | parameters: 22 | type: object 23 | properties: 24 | video_id: 25 | type: string 26 | description: The video id. 27 | required: 28 | - video_id 29 | function: 30 | type: script 31 | sequence: 32 | - service: webostv.command 33 | data: 34 | entity_id: media_player.{YOUR_WEBOSTV} 35 | command: system.launcher/launch 36 | payload: 37 | id: netflix 38 | contentId: "m=https://www.netflix.com/watch/{{video_id}}" 39 | ``` -------------------------------------------------------------------------------- /examples/function/notify/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | 4 | ## Function 5 | 6 | ### send_message_to_messenger 7 | ```yaml 8 | - spec: 9 | name: send_message_to_messenger 10 | description: Use this function to send message to messenger. 11 | parameters: 12 | type: object 13 | properties: 14 | message: 15 | type: string 16 | description: message you want to send 17 | required: 18 | - message 19 | function: 20 | type: script 21 | sequence: 22 | - service: notify.{YOUR_MESSENGER} 23 | data: 24 | message: "{{ message }}" 25 | ``` -------------------------------------------------------------------------------- /examples/function/plex/README.md: -------------------------------------------------------------------------------- 1 | ## Function 2 | 3 | ### search_plex 4 | ```yaml 5 | - spec: 6 | name: search_plex 7 | description: Use this function to search for media in Plex. 8 | parameters: 9 | type: object 10 | properties: 11 | query: 12 | type: string 13 | description: The search query to look up media on Plex. 14 | required: 15 | - query 16 | - token 17 | function: 18 | type: rest 19 | resource_template: "https://YOUR.PLEX.SERVER.TLD/search?query={{query}}&X-Plex-Token=YOURPLEXTOKEN" 20 | value_template: >- 21 | ```csv 22 | title,year,director,type,key 23 | {% for metadata in value_json["MediaContainer"]["Metadata"] %} 24 | {{ metadata["title"]|replace(",", " ") }}, 25 | {{ metadata["year"] }}, 26 | {{ metadata["Director"][0]["tag"] if metadata["Director"] else "N/A" }}, 27 | {{ metadata["type"] }}, 28 | {{ metadata["key"] }} 29 | {% endfor -%} 30 | ``` 31 | ``` 32 | 33 | ### play_plex_media_in_apple_tv 34 | 35 | ```yaml 36 | - spec: 37 | name: play_plex_media_in_apple_tv 38 | description: Use this function to play Plex media on an Apple TV. 39 | parameters: 40 | type: object 41 | properties: 42 | key: 43 | type: string 44 | description: The key of the media in Plex. 45 | entity_id: 46 | type: string 47 | description: The entity ID of the Apple TV in Home Assistant. 48 | type: 49 | type: string 50 | enum: 51 | - movie 52 | - show 53 | - episode 54 | required: 55 | - key 56 | - entity_id 57 | - type 58 | function: 59 | type: script 60 | sequence: 61 | - service: script.play_plex_media_on_apple_tv 62 | data: 63 | kind: "{{ kind }}" 64 | content_id: "{{ content_id }}" 65 | player: "{{ player }}" 66 | ``` 67 | 68 | ```yaml 69 | script: 70 | play_plex_media_on_apple_tv: 71 | alias: "Play Plex Media on Apple TV" 72 | sequence: 73 | - service: media_player.play_media 74 | data_template: 75 | media_content_type: "{{ type }}" 76 | media_content_id: "plex://MYSERVERID/{{ key }}" 77 | target: 78 | entity_id: "{{ entity_id }}" 79 | ``` -------------------------------------------------------------------------------- /examples/function/say_tts/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | - say_tts will say a message on any text to speech device 4 | 5 | ## Function 6 | 7 | ### say_tts 8 | ```yaml 9 | - spec: 10 | name: say_tts 11 | description: Say message on a text to speech device 12 | parameters: 13 | type: object 14 | properties: 15 | message: 16 | type: string 17 | description: message you want to say 18 | device: 19 | type: string 20 | description: entity_id of media_player tts device 21 | required: 22 | - message 23 | - device 24 | function: 25 | type: script 26 | sequence: 27 | - service: tts.cloud_say 28 | data: 29 | entity_id: "{{device}}" 30 | message: "{{message}}" 31 | ``` 32 | -------------------------------------------------------------------------------- /examples/function/shopping_list/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | 4 | ## Function 5 | 6 | ### add_item_to_list 7 | ```yaml 8 | - spec: 9 | name: add_item_to_list 10 | description: Add item to a list 11 | parameters: 12 | type: object 13 | properties: 14 | item: 15 | type: string 16 | description: The item to be added to the list 17 | list: 18 | type: string 19 | description: the entity id of the list to update 20 | enum: 21 | - todo.shopping_list 22 | - todo.to_do 23 | required: 24 | - item 25 | - list 26 | function: 27 | type: script 28 | sequence: 29 | - service: todo.add_item 30 | data: 31 | item: '{{item}}' 32 | target: 33 | entity_id: '{{list}}' 34 | ``` 35 | 36 | ### remove_item_from_list 37 | ```yaml 38 | - spec: 39 | name: remove_item_from_list 40 | description: Check an item off a list 41 | parameters: 42 | type: object 43 | properties: 44 | item: 45 | type: string 46 | description: The item to be removed from the list 47 | list: 48 | type: string 49 | description: the entity id of the list to update 50 | enum: 51 | - todo.shopping_list 52 | - todo.to_do 53 | required: 54 | - item 55 | - list 56 | function: 57 | type: script 58 | sequence: 59 | - service: todo.update_item 60 | data: 61 | item: '{{item}}' 62 | status: 'completed' 63 | target: 64 | entity_id: '{{list}}' 65 | ``` 66 | 67 | ### get_items_from_list 68 | ```yaml 69 | - spec: 70 | name: get_items_from_list 71 | description: Read back items from a list 72 | parameters: 73 | type: object 74 | properties: 75 | list: 76 | type: string 77 | description: the entity id of the list to update 78 | enum: 79 | - todo.shopping_list 80 | - todo.to_do 81 | required: 82 | - list 83 | function: 84 | type: script 85 | sequence: 86 | - service: todo.get_items 87 | data: 88 | status: 'needs_action' 89 | target: 90 | entity_id: '{{list}}' 91 | response_variable: _function_result 92 | ``` 93 | -------------------------------------------------------------------------------- /examples/function/sqlite/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | - Query anything from database. 3 | 4 | ## Function 5 | 6 | ### 1. SQL generated by LLM 7 | - Try with few examples and find one that best fits for your case. 8 | - Tweak name and description to find better result of query 9 | 10 | #### simple (with no validation) 11 | ```yaml 12 | - spec: 13 | name: query_histories_from_db 14 | description: >- 15 | Use this function to query histories from Home Assistant SQLite database. 16 | Example: 17 | Question: How long was livingroom light on in Nov 15? 18 | Answer: SELECT datetime(s.last_updated_ts, 'unixepoch', 'localtime') last_updated, s.state, old.state as prev_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') BETWEEN '2023-11-15 00:00:00' AND '2023-11-15 23:59:59' 19 | parameters: 20 | type: object 21 | properties: 22 | query: 23 | type: string 24 | description: A fully formed SQL query. 25 | function: 26 | type: sqlite 27 | ``` 28 | 29 | ```yaml 30 | - spec: 31 | name: query_histories_from_db 32 | description: >- 33 | Use this function to query histories from Home Assistant SQLite database. 34 | Example: 35 | Question: When did bedroom light turn on? 36 | 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 37 | Question: Was livingroom light on at 9 am? 38 | 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 39 | parameters: 40 | type: object 41 | properties: 42 | query: 43 | type: string 44 | description: A fully formed SQL query. 45 | function: 46 | type: sqlite 47 | ``` 48 | 49 | #### with minimum validation (still not enough) 50 | ```yaml 51 | - spec: 52 | name: query_histories_from_db 53 | description: >- 54 | Use this function to query histories from Home Assistant SQLite database. 55 | Example: 56 | Question: How long was livingroom light on in Nov 15? 57 | Answer: SELECT datetime(s.last_updated_ts, 'unixepoch', 'localtime') last_updated, s.state, old.state as prev_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') BETWEEN '2023-11-15 00:00:00' AND '2023-11-15 23:59:59' 58 | parameters: 59 | type: object 60 | properties: 61 | query: 62 | type: string 63 | description: A fully formed SQL query. 64 | function: 65 | type: sqlite 66 | query: >- 67 | {%- if is_exposed_entity_in_query(query) -%} 68 | {{ query }} 69 | {%- else -%} 70 | {{ raise("entity_id should be exposed.") }} 71 | {%- endif -%} 72 | ``` 73 | 74 | ```yaml 75 | - spec: 76 | name: query_histories_from_db 77 | description: >- 78 | Use this function to query histories from Home Assistant SQLite database. 79 | Example: 80 | Question: When did bedroom light turn on? 81 | 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 82 | Question: Was livingroom light on at 9 am? 83 | 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 84 | parameters: 85 | type: object 86 | properties: 87 | query: 88 | type: string 89 | description: A fully formed SQL query. 90 | function: 91 | type: sqlite 92 | query: >- 93 | {%- if is_exposed_entity_in_query(query) -%} 94 | {{ query }} 95 | {%- else -%} 96 | {{ raise("entity_id should be exposed.") }} 97 | {%- endif -%} 98 | ``` 99 | 100 | 101 | ### 2. Defined SQL manually 102 | 103 | #### 2-1. get_state_at_time 104 | 105 | 106 | 107 | ```yaml 108 | - spec: 109 | name: get_state_at_time 110 | description: > 111 | Use this function to get state at time 112 | parameters: 113 | type: object 114 | properties: 115 | entity_id: 116 | type: string 117 | description: The target entity 118 | datetime: 119 | type: string 120 | description: The datetime in '%Y-%m-%d %H:%M:%S' format 121 | required: 122 | - entity_id 123 | - datetime 124 | - limit 125 | function: 126 | type: sqlite 127 | query: >- 128 | {%- if is_exposed(entity_id) -%} 129 | SELECT datetime(s.last_updated_ts, 'unixepoch', 'localtime') as state_updated_at, s.state 130 | FROM states s 131 | INNER JOIN states_meta sm ON s.metadata_id = sm.metadata_id 132 | INNER JOIN states old ON s.old_state_id = old.state_id 133 | WHERE sm.entity_id = '{{entity_id}}' 134 | AND s.state != old.state 135 | AND datetime(s.last_updated_ts, 'unixepoch', 'localtime') < '{{datetime}}' 136 | ORDER BY s.last_updated_ts DESC 137 | LIMIT 1 138 | {%- else -%} 139 | {{ raise("entity_id should be exposed.") }} 140 | {%- endif -%} 141 | ``` 142 | 143 | 144 | #### 2-2. get_states_between 145 | 146 | 147 | ```yaml 148 | - spec: 149 | name: get_states_between 150 | description: > 151 | Use this function to get non-numeric states between two dates. 152 | parameters: 153 | type: object 154 | properties: 155 | entity_id: 156 | type: string 157 | description: The target entity 158 | state: 159 | type: string 160 | description: The state 161 | state_operator: 162 | type: string 163 | description: The state operator 164 | enum: 165 | - ">" 166 | - "<" 167 | - "=" 168 | - ">=" 169 | - "<=" 170 | start_datetime: 171 | type: string 172 | description: The start datetime in '%Y-%m-%d %H:%M:%S' format 173 | end_datetime: 174 | type: string 175 | description: The end datetime in '%Y-%m-%d %H:%M:%S' format 176 | order: 177 | type: string 178 | description: The order of datetime, defaults to desc 179 | enum: 180 | - asc 181 | - desc 182 | page: 183 | type: integer 184 | description: The page number 185 | limit: 186 | type: integer 187 | description: The page size defaults to 10 188 | required: 189 | - entity_id 190 | - start_datetime 191 | - end_datetime 192 | - order 193 | - page 194 | - limit 195 | function: 196 | type: composite 197 | sequence: 198 | - type: sqlite 199 | query: >- 200 | {%- if is_exposed(entity_id) -%} 201 | SELECT datetime(s.last_updated_ts, 'unixepoch', 'localtime') as updated_at, s.state 202 | FROM states s 203 | INNER JOIN states_meta sm ON s.metadata_id = sm.metadata_id 204 | INNER JOIN states old ON s.old_state_id = old.state_id 205 | WHERE sm.entity_id = '{{entity_id}}' 206 | AND s.state != old.state 207 | AND (('{{state | default('')}}' = '') OR (s.state {{state_operator | default('=')}} '{{state | default('')}}')) 208 | AND datetime(s.last_updated_ts, 'unixepoch', 'localtime') >= '{{start_datetime}}' 209 | AND datetime(s.last_updated_ts, 'unixepoch', 'localtime') < '{{end_datetime}}' 210 | ORDER BY s.last_updated_ts {{order}} 211 | LIMIT {{(page-1) * limit}}, {{limit}} 212 | {%- else -%} 213 | {{ raise("entity_id should be exposed.") }} 214 | {%- endif -%} 215 | response_variable: data 216 | - type: sqlite 217 | single: true 218 | query: >- 219 | SELECT count(*) as count 220 | FROM states s 221 | INNER JOIN states_meta sm ON s.metadata_id = sm.metadata_id 222 | INNER JOIN states old ON s.old_state_id = old.state_id 223 | WHERE sm.entity_id = '{{entity_id}}' 224 | AND s.state != old.state 225 | AND (('{{state | default('')}}' = '') OR (s.state {{state_operator | default('=')}} '{{state | default('')}}')) 226 | AND datetime(s.last_updated_ts, 'unixepoch', 'localtime') >= '{{start_datetime}}' 227 | AND datetime(s.last_updated_ts, 'unixepoch', 'localtime') < '{{end_datetime}}' 228 | response_variable: total 229 | - type: template 230 | value_template: '{"data": {{data}}, "total": {{total.count}}}' 231 | ``` 232 | 233 | #### 2-3. get_total_time_of_entity_state 234 | 235 | 236 | 237 | ```yaml 238 | - spec: 239 | name: get_total_time_of_entity_state 240 | description: > 241 | Use this function to get total time of state of entity between two dates 242 | parameters: 243 | type: object 244 | properties: 245 | entity_id: 246 | type: string 247 | description: The target entity 248 | state: 249 | type: string 250 | description: The non-numeric target state 251 | start_datetime: 252 | type: string 253 | description: The start datetime in '%Y-%m-%d %H:%M:%S' format 254 | end_datetime: 255 | type: string 256 | description: The end datetime in '%Y-%m-%d %H:%M:%S' format 257 | required: 258 | - entity_id 259 | - state 260 | - start_datetime 261 | - end_datetime 262 | function: 263 | type: composite 264 | sequence: 265 | - type: sqlite 266 | query: >- 267 | {%- if is_exposed(entity_id) -%} 268 | WITH stat_data AS ( 269 | WITH lead_data AS ( 270 | SELECT datetime(old.last_updated_ts, 'unixepoch', 'localtime') AS prev_last_updated, 271 | old.state AS prev_state, 272 | datetime(s.last_updated_ts, 'unixepoch', 'localtime') AS last_updated, 273 | s.state, 274 | COALESCE(LEAD(datetime(s.last_updated_ts, 'unixepoch', 'localtime')) OVER (ORDER BY s.last_updated), '{{end_datetime}}') AS lead_last_updated, 275 | LEAD(s.state) OVER (ORDER BY s.last_updated) AS lead_state 276 | FROM states s 277 | INNER JOIN states_meta sm ON s.metadata_id = sm.metadata_id 278 | INNER JOIN states old ON s.old_state_id = old.state_id 279 | WHERE sm.entity_id = '{{entity_id}}' 280 | AND s.state != old.state 281 | AND datetime(s.last_updated_ts, 'unixepoch', 'localtime') BETWEEN '{{start_datetime}}' AND '{{end_datetime}}' 282 | ) 283 | SELECT max(prev_last_updated, '{{start_datetime}}') AS prev_last_updated, 284 | prev_state, 285 | last_updated AS last_updated, 286 | state 287 | FROM lead_data 288 | WHERE last_updated = (SELECT MIN(last_updated) FROM lead_data) 289 | 290 | UNION ALL 291 | 292 | SELECT last_updated AS prev_last_updated, state AS prev_state, min(lead_last_updated, strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime')) AS last_updated, lead_state AS state 293 | FROM lead_data 294 | ) 295 | SELECT SUM(CASE WHEN prev_state = '{{state}}' THEN cast(strftime('%s', last_updated, 'utc') as real) - cast(strftime('%s', prev_last_updated, 'utc') as real) ELSE 0 END) AS total_time_in_sec FROM stat_data 296 | {%- else -%} 297 | {{ raise("entity_id should be exposed.") }} 298 | {%- endif -%} 299 | response_variable: result 300 | - type: template 301 | value_template: >- 302 | {%- if result and result[0] and result[0].total_time_in_sec -%} 303 | {%- set duration = result[0].total_time_in_sec | int -%} 304 | 305 | {%- set days = (duration // 86400) | int -%} 306 | {%- set hours = ((duration % 86400) // 3600) | int -%} 307 | {%- set minutes = ((duration % 3600) // 60) | int -%} 308 | {%- set remaining_seconds = (duration % 60) | int -%} 309 | 310 | {{ "{0}d ".format(days) if days > 0 else "" }}{{ "{0}h ".format(hours) if hours > 0 else "" }}{{ "{0}m ".format(minutes) if minutes > 0 else "" }}{{ "{0}s".format(remaining_seconds) if remaining_seconds > 0 else "" }} 311 | {%- else -%} 312 | unkown 313 | {%- endif -%} 314 | ``` -------------------------------------------------------------------------------- /examples/function/user_id_to_user/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | - Map user_id to friendly user name string 3 | 4 | When the option to pass the current user to OpenAI via the user 5 | message context is enabled, we actually pass the user_id rather than a 6 | friendly user name as OpenAI has limitations on characters it accepts. 7 | This function can be used to resolve the user's name, without 8 | limitation on acceptable characters. 9 | 10 | ## Function 11 | 12 | ### get_user_from_user_id 13 | ```yaml 14 | - spec: 15 | name: get_user_from_user_id 16 | description: Retrieve users name from the supplied user id hash 17 | parameters: 18 | type: object 19 | properties: 20 | user_id: 21 | type: string 22 | description: user_id 23 | required: 24 | - user_id 25 | function: 26 | type: native 27 | name: get_user_from_user_id 28 | ``` -------------------------------------------------------------------------------- /examples/function/weather/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | - Get current weather and forecasts 3 | 4 | 5 | 6 | 7 | ## Prerequisite 8 | Expose `weather.xxxxx` entity 9 | 10 | ## Function 11 | 12 | ### get_attributes 13 | ```yaml 14 | - spec: 15 | name: get_attributes 16 | description: Get attributes of any home assistant entity 17 | parameters: 18 | type: object 19 | properties: 20 | entity_id: 21 | type: string 22 | description: entity_id 23 | required: 24 | - entity_id 25 | function: 26 | type: template 27 | value_template: "{{states[entity_id]}}" 28 | ``` -------------------------------------------------------------------------------- /examples/function/youtube/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | 4 | ## Prompt 5 | Add following text in your prompt 6 | ```` 7 | Youtube Channels: 8 | ```csv 9 | channel_id,channel_name 10 | UCLkAepWjdylmXSltofFvsYQ,BANGTANTV 11 | ``` 12 | ```` 13 | 14 | ## Function 15 | ### play_youtube 16 | #### webostv 17 | ```yaml 18 | - spec: 19 | name: play_youtube 20 | description: Use this function to play Youtube. 21 | parameters: 22 | type: object 23 | properties: 24 | video_id: 25 | type: string 26 | description: The video id. 27 | required: 28 | - video_id 29 | function: 30 | type: script 31 | sequence: 32 | - service: webostv.command 33 | data: 34 | entity_id: media_player.{YOUR_WEBOSTV} 35 | command: system.launcher/launch 36 | payload: 37 | id: youtube.leanback.v4 38 | contentId: "{{video_id}}" 39 | - delay: 40 | hours: 0 41 | minutes: 0 42 | seconds: 10 43 | milliseconds: 0 44 | - service: webostv.button 45 | data: 46 | entity_id: media_player.{YOUR_WEBOSTV} 47 | button: ENTER 48 | ``` 49 | #### Apple TV 50 | ```yaml 51 | - spec: 52 | name: play_youtube_on_apple_tv 53 | description: Use this function to play YouTube content on a specified Apple TV. 54 | parameters: 55 | type: object 56 | properties: 57 | kind: 58 | type: string 59 | enum: 60 | - video 61 | - channel 62 | - playlist 63 | description: The type of YouTube content. 64 | content_id: 65 | type: string 66 | description: ID of the YouTube content (can be videoId, channelId, or playlistId). 67 | entity_id: 68 | type: string 69 | description: entity_id of Apple TV. 70 | required: 71 | - kind 72 | - content_id 73 | - entity_id 74 | function: 75 | type: script 76 | sequence: 77 | - service: script.play_youtube_on_apple_tv 78 | data: 79 | kind: "{{ kind }}" 80 | content_id: "{{ content_id }}" 81 | player: "{{ player }}" 82 | ``` 83 | 84 | ```yaml 85 | script: 86 | play_youtube_on_apple_tv: 87 | alias: "Play YouTube on Apple TV" 88 | sequence: 89 | - service: media_player.play_media 90 | data_template: 91 | media_content_type: url 92 | media_content_id: >- 93 | {% if kind == 'video' %} 94 | youtube://www.youtube.com/watch?v={{content_id}} 95 | {% elif kind == 'channel' %} 96 | youtube://www.youtube.com/channel/{{content_id}} 97 | {% else %} 98 | youtube://www.youtube.com/playlist?list={{content_id}} 99 | {% endif %} 100 | target: 101 | entity_id: "{{ entity_id }}" 102 | ``` 103 | 104 | #### Android TV 105 | ```yaml 106 | - spec: 107 | name: play_youtube_on_android_tv 108 | description: Use this function to play YouTube content on a specified Android TV. 109 | parameters: 110 | type: object 111 | properties: 112 | kind: 113 | type: string 114 | enum: 115 | - video 116 | - channel 117 | - playlist 118 | description: The type of YouTube content. 119 | content_id: 120 | type: string 121 | description: ID of the YouTube content (can be videoId, channelId, or playlistId). 122 | player: 123 | type: string 124 | description: media_player entity. 125 | required: 126 | - kind 127 | - content_id 128 | - player 129 | function: 130 | type: script 131 | sequence: 132 | - service: script.play_youtube_on_android_tv 133 | data: 134 | kind: "{{ kind }}" 135 | content_id: "{{ content_id }}" 136 | player: "{{ player }}" 137 | ``` 138 | 139 | ```yaml 140 | script: 141 | play_youtube_on_android_tv: 142 | alias: "Play YouTube on Android TV" 143 | sequence: 144 | - service: remote.turn_on 145 | data: 146 | activity: >- 147 | {% if kind == 'video' %} 148 | https://www.youtube.com/watch?v={{content_id}} 149 | {% elif kind == 'channel' %} 150 | https://www.youtube.com/channel/{{content_id}} 151 | {% else %} {# playlist kind #} 152 | https://www.youtube.com/playlist?list={{content_id}} 153 | {% endif %} 154 | target: 155 | entity_id: "{{ player }}" 156 | ``` 157 | 158 | ### get_recent_youtube 159 | ```yaml 160 | - spec: 161 | name: get_recent_youtube_videos 162 | description: Use this function to get recent videos of youtube. 163 | parameters: 164 | type: object 165 | properties: 166 | channel_id: 167 | type: string 168 | description: The channel id of Youtube 169 | required: 170 | - channel_id 171 | function: 172 | type: rest 173 | resource_template: "https://www.youtube.com/feeds/videos.xml?channel_id={{channel_id}}" 174 | value_template: >- 175 | ```csv 176 | video_id,title 177 | {% for item in value_json["feed"]["entry"] %} 178 | {{item["yt:videoId"]}},{{item["title"][0:10]}} 179 | {% endfor -%} 180 | ``` 181 | ``` 182 | 183 | ### search_youtube 184 | - Replace "YOUROWNSUPERSECRETYOUTUBEAPIV3KEY" with your API Key 185 | 186 | ```yaml 187 | - spec: 188 | name: search_youtube 189 | description: Use this function to search for YouTube videos, channels, or playlists based on a query. 190 | parameters: 191 | type: object 192 | properties: 193 | query: 194 | type: string 195 | description: The search query to look up on YouTube. 196 | type: 197 | type: string 198 | enum: 199 | - video 200 | - channel 201 | - playlist 202 | default: video 203 | description: The type of content to search for on YouTube. 204 | required: 205 | - query 206 | - type 207 | function: 208 | type: rest 209 | resource_template: "https://www.googleapis.com/youtube/v3/search?part=snippet&q={{query}}&type={{type}}&key={YOUROWNSUPERSECRETYOUTUBEAPIV3KEY}" 210 | value_template: >- 211 | ```csv 212 | kind,id,title 213 | {% for item in value_json["items"] %} 214 | {{item["id"]["kind"]|replace("youtube#", "")}},{{item["id"][type + "Id"]}},{{item["snippet"]["title"]|replace(",", " ")|truncate(50, True, "...")}} 215 | {% endfor -%} 216 | ``` 217 | ``` 218 | -------------------------------------------------------------------------------- /examples/prompt/annoying/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | - Just for fun 3 | 4 | ## Prompt 5 | 6 | ````yaml 7 | You are the most annoying assistant of Home Assistant 8 | Always answer in a rude manner using a list of available devices. 9 | A list of available devices in this smart home: 10 | 11 | ```csv 12 | entity_id,name,state,aliases 13 | {% for entity in exposed_entities -%} 14 | {{ entity.entity_id }},{{ entity.name }},{{entity.state}},{{entity.aliases | join('/')}} 15 | {% endfor -%} 16 | ``` 17 | 18 | If user asks for devices that are not available, do not have to answer. 19 | ```` 20 | -------------------------------------------------------------------------------- /examples/prompt/area/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | - Let gpt know about area information, so [execute_services](https://github.com/jekalmin/extended_openai_conversation/tree/v1.0.2/examples/function/area#execute_services) can be called using `area_id` 3 | - Use area awareness feature like [Year of Voice Chapter 5](https://www.home-assistant.io/blog/2023/12/13/year-of-the-voice-chapter-5/#area-awareness) 4 | 5 | ## How to use area awareness? 6 | 1. Assign area to your ESP-S3-BOX or Atom echo. 7 | 2. Copy and paste prompt below. 8 | 3. Ask "turn on light", "turn off light" 9 | 10 | 11 | ## Prompt 12 | 13 | ### 1. List areas and entities separately 14 | ````yaml 15 | I want you to act as smart home manager of Home Assistant. 16 | I will provide information of smart home along with a question, you will truthfully make correction or answer using information provided in one sentence in everyday language. 17 | 18 | Current Time: {{now()}} 19 | Current Area: {{area_id(current_device_id)}} 20 | 21 | Available Devices: 22 | ```csv 23 | entity_id,name,state,area_id,aliases 24 | {% for entity in exposed_entities -%} 25 | {{ entity.entity_id }},{{ entity.name }},{{ entity.state }},{{area_id(entity.entity_id)}},{{entity.aliases | join('/')}} 26 | {% endfor -%} 27 | ``` 28 | 29 | Areas: 30 | ```csv 31 | area_id,name 32 | {% for area_id in areas() -%} 33 | {{area_id}},{{area_name(area_id)}} 34 | {% endfor -%} 35 | ``` 36 | 37 | 38 | The current state of devices is provided in available devices. 39 | Use execute_services function only for requested action, not for current states. 40 | Do not execute service without user's confirmation. 41 | Do not restate or appreciate what user says, rather make a quick inquiry. 42 | Make decisions based on current area first. 43 | ```` 44 | 45 | ### 2. Categorize entities by areas 46 | ````yaml 47 | I want you to act as smart home manager of Home Assistant. 48 | I will provide information of smart home along with a question, you will truthfully make correction or answer using information provided in one sentence in everyday language. 49 | 50 | Current Time: {{now()}} 51 | Current Area: {{area_name(current_device_id)}} 52 | 53 | An overview of the areas and the available devices: 54 | {%- set area_entities = namespace(mapping={}) %} 55 | {%- for entity in exposed_entities %} 56 | {%- set current_area_id = area_id(entity.entity_id) or "etc" %} 57 | {%- set entities = (area_entities.mapping.get(current_area_id) or []) + [entity] %} 58 | {%- set area_entities.mapping = dict(area_entities.mapping, **{current_area_id: entities}) -%} 59 | {%- endfor %} 60 | 61 | {%- for current_area_id, entities in area_entities.mapping.items() %} 62 | 63 | {%- if current_area_id == "etc" %} 64 | Etc: 65 | {%- else %} 66 | {{area_name(current_area_id)}}({{current_area_id}}): 67 | {%- endif %} 68 | ```csv 69 | entity_id,name,state,aliases 70 | {%- for entity in entities %} 71 | {{ entity.entity_id }},{{ entity.name }},{{ entity.state }},{{entity.aliases | join('/')}} 72 | {%- endfor %} 73 | ``` 74 | {%- endfor %} 75 | 76 | The current state of devices is provided in available devices. 77 | Use execute_services function only for requested action, not for current states. 78 | Do not execute service without user's confirmation. 79 | Do not restate or appreciate what user says, rather make a quick inquiry. 80 | Make decisions based on current area first. 81 | ```` -------------------------------------------------------------------------------- /examples/prompt/default/README.md: -------------------------------------------------------------------------------- 1 | ## Prompt 2 | 3 | ````yaml 4 | I want you to act as smart home manager of Home Assistant. 5 | I will provide information of smart home along with a question, you will truthfully make correction or answer using information provided in one sentence in everyday language. 6 | 7 | Current Time: {{now()}} 8 | 9 | Available Devices: 10 | ```csv 11 | entity_id,name,state,aliases 12 | {% for entity in exposed_entities -%} 13 | {{ entity.entity_id }},{{ entity.name }},{{ entity.state }},{{entity.aliases | join('/')}} 14 | {% endfor -%} 15 | ``` 16 | 17 | The current state of devices is provided in available devices. 18 | Use execute_services function only for requested action, not for current states. 19 | Do not execute service without user's confirmation. 20 | Do not restate or appreciate what user says, rather make a quick inquiry. 21 | ```` -------------------------------------------------------------------------------- /examples/prompt/with_attributes/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | Add attributes of entities that are configured in `customize_glob_exposed_attributes`. 3 | It is similar to [customize_glob](https://www.home-assistant.io/docs/configuration/customizing-devices/) of Home Assistant. 4 | It uses regular expression as a pattern. 5 | 6 | If value is true, attribute is included. If false, attribute is excluded.
7 | If value is not boolean, the value is included, not value of attribute. 8 | 9 | 10 | ## Prompt 11 | 12 | ````yaml 13 | {%- set customize_glob_exposed_attributes = { 14 | ".*": { 15 | "friendly_name": true, 16 | }, 17 | "timer\..*": { 18 | "duration": true, 19 | }, 20 | "sun.sun": { 21 | "next_dawn": true, 22 | "next_midnight": true, 23 | }, 24 | "media_player.YOUR_WEBOS_TV": { 25 | "source_list": ["Netflix","YouTube","wavve"], 26 | "source": true, 27 | }, 28 | } %} 29 | 30 | {%- macro get_exposed_attributes(entity_id) -%} 31 | {%- set ns = namespace(exposed_attributes = {}, result = {}) %} 32 | {%- for pattern, attributes in customize_glob_exposed_attributes.items() -%} 33 | {%- if entity_id | regex_match(pattern) -%} 34 | {%- set ns.exposed_attributes = dict(ns.exposed_attributes, **attributes) -%} 35 | {%- endif -%} 36 | {%- endfor -%} 37 | {%- for attribute_key, should_include in ns.exposed_attributes.items() -%} 38 | {%- if should_include and state_attr(entity_id, attribute_key) != None -%} 39 | {%- set temp = {attribute_key: state_attr(entity_id, attribute_key)} if should_include is boolean else {attribute_key: should_include} -%} 40 | {%- set ns.result = dict(ns.result, **temp) -%} 41 | {%- endif -%} 42 | {%- endfor -%} 43 | {%- set result = ns.result | to_json if ns.result!={} else None -%} 44 | {{"'" + result + "'" if result != None else ''}} 45 | {%- endmacro -%} 46 | 47 | I want you to act as smart home manager of Home Assistant. 48 | I will provide information of smart home along with a question, you will truthfully make correction or answer using information provided in one sentence in everyday language. 49 | 50 | Current Time: {{now()}} 51 | 52 | Available Devices: 53 | ```csv 54 | entity_id,name,state,aliases,attributes 55 | {% for entity in exposed_entities -%} 56 | {{ entity.entity_id }},{{ entity.name }},{{ entity.state }},{{entity.aliases | join('/')}},{{get_exposed_attributes(entity.entity_id)}} 57 | {% endfor -%} 58 | ``` 59 | 60 | The current state of devices is provided in available devices. 61 | Use execute_services function only for requested action, not for current states. 62 | Do not execute service without user's confirmation. 63 | Do not restate or appreciate what user says, rather make a quick inquiry. 64 | ```` -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Extended Deepseek Conversation", 3 | "render_readme": true, 4 | "homeassistant": "2024.1.0b0" 5 | } --------------------------------------------------------------------------------