From 7576d2f31d74023fd5440dc264fa2a39a7d53678 Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Fri, 17 Nov 2023 00:57:21 -0700 Subject: [PATCH 01/20] Attach the user's name to the user message payload if known --- .../extended_openai_conversation/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/custom_components/extended_openai_conversation/__init__.py b/custom_components/extended_openai_conversation/__init__.py index 16f03d2..640a3b3 100644 --- a/custom_components/extended_openai_conversation/__init__.py +++ b/custom_components/extended_openai_conversation/__init__.py @@ -11,7 +11,7 @@ from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, MATCH_ALL +from homeassistant.const import CONF_API_KEY, MATCH_ALL, ATTR_NAME from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse from homeassistant.util import ulid from homeassistant.components.homeassistant.exposed_entities import async_should_expose @@ -150,8 +150,12 @@ async def async_process( response=intent_response, conversation_id=conversation_id ) messages = [{"role": "system", "content": prompt}] + user_message = {"role": "user", "content": user_input.text} + user = await self.hass.auth.async_get_user(user_input.context.user_id) + if user is not None and user.name is not None: + user_message[ATTR_NAME] = user.name - messages.append({"role": "user", "content": user_input.text}) + messages.append(user_message) try: response = await self.query(user_input, messages, exposed_entities, 0) From 31f4e7dacb44c31b7819c7175be6393c49b823bc Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Sun, 19 Nov 2023 21:06:15 -0700 Subject: [PATCH 02/20] Configuration option to attach username to the message payload --- README.md | 2 ++ .../extended_openai_conversation/__init__.py | 9 ++++++--- .../extended_openai_conversation/config_flow.py | 11 +++++++++++ .../extended_openai_conversation/const.py | 2 ++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 68e6c1c..0410585 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Derived from [OpenAI Conversation](https://www.home-assistant.io/integrations/op - Ability to call service of Home Assistant - Ability to create automation - Ability to get data from API or web page +- Option to pass the current user's name to OpenAI via the user message context ## How it works 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. @@ -55,6 +56,7 @@ https://github.com/jekalmin/extended_openai_conversation/assets/2917984/64ba656e By clicking a button from Edit Assist, Options can be customized.
Options include [OpenAI Conversation](https://www.home-assistant.io/integrations/openai_conversation/) options and two new options. +- `Attach Username`: Pass the active user's name (if applicaple) to OpenAI via rhe message payload. Currently, this only applies to conversations through the UI or REST API. - `Maximum Function Calls Per Conversation`: limit the number of function calls in a single conversation. (Sometimes function is called over and over again, possibly running into infinite loop) diff --git a/custom_components/extended_openai_conversation/__init__.py b/custom_components/extended_openai_conversation/__init__.py index 640a3b3..a9ed633 100644 --- a/custom_components/extended_openai_conversation/__init__.py +++ b/custom_components/extended_openai_conversation/__init__.py @@ -30,6 +30,7 @@ ) from .const import ( + CONF_ATTACH_USERNAME, CONF_CHAT_MODEL, CONF_MAX_TOKENS, CONF_PROMPT, @@ -38,6 +39,7 @@ CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION, CONF_FUNCTIONS, CONF_BASE_URL, + DEFAULT_ATTACH_USERNAME, DEFAULT_CHAT_MODEL, DEFAULT_MAX_TOKENS, DEFAULT_PROMPT, @@ -151,9 +153,10 @@ async def async_process( ) messages = [{"role": "system", "content": prompt}] user_message = {"role": "user", "content": user_input.text} - user = await self.hass.auth.async_get_user(user_input.context.user_id) - if user is not None and user.name is not None: - user_message[ATTR_NAME] = user.name + if self.entry.options.get(CONF_ATTACH_USERNAME, DEFAULT_ATTACH_USERNAME): + user = await self.hass.auth.async_get_user(user_input.context.user_id) + if user is not None and user.name is not None: + user_message[ATTR_NAME] = user.name messages.append(user_message) diff --git a/custom_components/extended_openai_conversation/config_flow.py b/custom_components/extended_openai_conversation/config_flow.py index 6a45bd8..97c41f9 100644 --- a/custom_components/extended_openai_conversation/config_flow.py +++ b/custom_components/extended_openai_conversation/config_flow.py @@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( + BooleanSelector, NumberSelector, NumberSelectorConfig, TemplateSelector, @@ -24,6 +25,7 @@ from .helpers import validate_authentication from .const import ( + CONF_ATTACH_USERNAME, CONF_CHAT_MODEL, CONF_MAX_TOKENS, CONF_PROMPT, @@ -32,6 +34,7 @@ CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION, CONF_FUNCTIONS, CONF_BASE_URL, + DEFAULT_ATTACH_USERNAME, DEFAULT_CHAT_MODEL, DEFAULT_MAX_TOKENS, DEFAULT_PROMPT, @@ -65,6 +68,7 @@ CONF_TOP_P: DEFAULT_TOP_P, CONF_TEMPERATURE: DEFAULT_TEMPERATURE, CONF_FUNCTIONS: DEFAULT_CONF_FUNCTIONS_STR, + CONF_ATTACH_USERNAME: DEFAULT_ATTACH_USERNAME, } ) @@ -194,4 +198,11 @@ def openai_config_option_schema(options: MappingProxyType[str, Any]) -> dict: description={"suggested_value": options.get(CONF_FUNCTIONS)}, default=DEFAULT_CONF_FUNCTIONS_STR, ): TemplateSelector(), + vol.Optional( + CONF_ATTACH_USERNAME, + description={ + "suggested_value": options[CONF_ATTACH_USERNAME] + }, + default=DEFAULT_ATTACH_USERNAME, + ): BooleanSelector(), } diff --git a/custom_components/extended_openai_conversation/const.py b/custom_components/extended_openai_conversation/const.py index 358bf62..3203189 100644 --- a/custom_components/extended_openai_conversation/const.py +++ b/custom_components/extended_openai_conversation/const.py @@ -77,3 +77,5 @@ ] CONF_BASE_URL = "base_url" DEFAULT_CONF_BASE_URL = "https://api.openai.com/v1" +CONF_ATTACH_USERNAME = "attach_username" +DEFAULT_ATTACH_USERNAME = False From e2236b93744a8248edeb1a9ae7698857fb622403 Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Wed, 22 Nov 2023 09:43:04 -0700 Subject: [PATCH 03/20] Update custom_components/extended_openai_conversation/config_flow.py Co-authored-by: jekalmin --- custom_components/extended_openai_conversation/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/extended_openai_conversation/config_flow.py b/custom_components/extended_openai_conversation/config_flow.py index 97c41f9..94e17d3 100644 --- a/custom_components/extended_openai_conversation/config_flow.py +++ b/custom_components/extended_openai_conversation/config_flow.py @@ -201,7 +201,7 @@ def openai_config_option_schema(options: MappingProxyType[str, Any]) -> dict: vol.Optional( CONF_ATTACH_USERNAME, description={ - "suggested_value": options[CONF_ATTACH_USERNAME] + "suggested_value": options.get(CONF_ATTACH_USERNAME) }, default=DEFAULT_ATTACH_USERNAME, ): BooleanSelector(), From 28beb3196038cf0c18a456474f1cb636bbaf6897 Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Wed, 22 Nov 2023 20:21:28 -0700 Subject: [PATCH 04/20] Add to strings and translations --- custom_components/extended_openai_conversation/strings.json | 3 ++- .../extended_openai_conversation/translations/en.json | 3 ++- .../extended_openai_conversation/translations/ko.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/custom_components/extended_openai_conversation/strings.json b/custom_components/extended_openai_conversation/strings.json index 3d0052b..4db21d6 100644 --- a/custom_components/extended_openai_conversation/strings.json +++ b/custom_components/extended_openai_conversation/strings.json @@ -25,7 +25,8 @@ "temperature": "Temperature", "top_p": "Top P", "max_function_calls_per_conversation": "Maximum function calls per conversation", - "functions": "Functions" + "functions": "Functions", + "attach_username": "Attach Username to Message" } } } diff --git a/custom_components/extended_openai_conversation/translations/en.json b/custom_components/extended_openai_conversation/translations/en.json index bd38ec0..7f0a07e 100644 --- a/custom_components/extended_openai_conversation/translations/en.json +++ b/custom_components/extended_openai_conversation/translations/en.json @@ -25,7 +25,8 @@ "temperature": "Temperature", "top_p": "Top P", "max_function_calls_per_conversation": "Maximum function calls per conversation", - "functions": "Functions" + "functions": "Functions", + "attach_username": "Attach Username to Message" } } } diff --git a/custom_components/extended_openai_conversation/translations/ko.json b/custom_components/extended_openai_conversation/translations/ko.json index bd38ec0..7f0a07e 100644 --- a/custom_components/extended_openai_conversation/translations/ko.json +++ b/custom_components/extended_openai_conversation/translations/ko.json @@ -25,7 +25,8 @@ "temperature": "Temperature", "top_p": "Top P", "max_function_calls_per_conversation": "Maximum function calls per conversation", - "functions": "Functions" + "functions": "Functions", + "attach_username": "Attach Username to Message" } } } From 08b3dd07524490fe293009fcab6d81465570f81b Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Mon, 4 Dec 2023 23:08:20 -0700 Subject: [PATCH 05/20] Update README.md Spelling issues --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0410585..cdcd8ac 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ https://github.com/jekalmin/extended_openai_conversation/assets/2917984/64ba656e By clicking a button from Edit Assist, Options can be customized.
Options include [OpenAI Conversation](https://www.home-assistant.io/integrations/openai_conversation/) options and two new options. -- `Attach Username`: Pass the active user's name (if applicaple) to OpenAI via rhe message payload. Currently, this only applies to conversations through the UI or REST API. +- `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. - `Maximum Function Calls Per Conversation`: limit the number of function calls in a single conversation. (Sometimes function is called over and over again, possibly running into infinite loop) From 4c4afcb7ce5605b64258d2c59ee861b6d60638b0 Mon Sep 17 00:00:00 2001 From: Scags Date: Sat, 2 Dec 2023 14:25:23 -0500 Subject: [PATCH 06/20] Update README.md --- examples/function/shopping_list/README.md | 45 ++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/examples/function/shopping_list/README.md b/examples/function/shopping_list/README.md index 4d216ef..2b92508 100644 --- a/examples/function/shopping_list/README.md +++ b/examples/function/shopping_list/README.md @@ -1,25 +1,60 @@ ## Objective +## Prompt +### add the following to prompts and fill in your list entity ids +```For lists: Ensure you are differentiating and using one of the following as the list parameter: todo.shopping_list for modifying the "shopping list" and todo.to_do for modification to the "to-do list" ``` + + ## Function ### add_item_to_shopping_cart ```yaml - spec: - name: add_item_to_shopping_cart - description: Add item to shopping cart + name: add_item_to_list + description: Add item to a list + parameters: + type: object + properties: + item: + type: string + description: The item to be added to the list + list: + type: string + description: the entity id of the list to update + required: + - item + - list + function: + type: script + sequence: + - service: todo.add_item + data: + item: '{{item}}' + target: + entity_id: '{{list}}' +- spec: + name: remove_item_from_list + description: Check an item off a list parameters: type: object properties: item: type: string - description: The item to be added to cart + description: The item to be removed from the list + list: + type: string + description: the entity id of the list to update required: - item + - list function: type: script sequence: - - service: shopping_list.add_item + - service: todo.update_item data: - name: '{{item}}' + item: '{{item}}' + status: 'completed' + target: + entity_id: '{{list}}' ``` From 5139d9ddba642395e3cdd2871be0ac740b9b18ee Mon Sep 17 00:00:00 2001 From: Scags Date: Sun, 3 Dec 2023 07:20:48 -0500 Subject: [PATCH 07/20] Update examples/function/shopping_list/README.md Yup that does the trick! Co-authored-by: jekalmin --- examples/function/shopping_list/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/function/shopping_list/README.md b/examples/function/shopping_list/README.md index 2b92508..249bc22 100644 --- a/examples/function/shopping_list/README.md +++ b/examples/function/shopping_list/README.md @@ -22,6 +22,8 @@ list: type: string description: the entity id of the list to update + enum: + - todo.shopping_list required: - item - list From 5fa3b2feb1ac0f19b9d217085501fba961d96c38 Mon Sep 17 00:00:00 2001 From: Scags Date: Sun, 3 Dec 2023 21:00:44 -0500 Subject: [PATCH 08/20] Update README.md --- examples/function/shopping_list/README.md | 32 +++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/examples/function/shopping_list/README.md b/examples/function/shopping_list/README.md index 249bc22..38477e6 100644 --- a/examples/function/shopping_list/README.md +++ b/examples/function/shopping_list/README.md @@ -1,11 +1,6 @@ ## Objective -## Prompt -### add the following to prompts and fill in your list entity ids -```For lists: Ensure you are differentiating and using one of the following as the list parameter: todo.shopping_list for modifying the "shopping list" and todo.to_do for modification to the "to-do list" ``` - - ## Function ### add_item_to_shopping_cart @@ -24,6 +19,7 @@ description: the entity id of the list to update enum: - todo.shopping_list + - todo.to_do required: - item - list @@ -47,6 +43,9 @@ list: type: string description: the entity id of the list to update + enum: + - todo.shopping_list + - todo.to_do required: - item - list @@ -59,4 +58,27 @@ status: 'completed' target: entity_id: '{{list}}' +- spec: + name: get_items_from_list + description: Read back items from a list + parameters: + type: object + properties: + list: + type: string + description: the entity id of the list to update + enum: + - todo.shopping_list + - todo.to_do + required: + - list + function: + type: script + sequence: + - service: todo.get_items + data: + status: 'needs_action' + target: + entity_id: '{{list}}' + response_variable: _function_result ``` From ef4d1407637bca76cb71dcc3475bcf40384c5f53 Mon Sep 17 00:00:00 2001 From: jekalmin Date: Sun, 3 Dec 2023 21:13:19 +0900 Subject: [PATCH 09/20] [#35] add logging, fix exception messages when failed to load functions or parse arguments --- .../extended_openai_conversation/__init__.py | 22 ++++++------- .../exceptions.py | 31 +++++++++++++++++++ .../extended_openai_conversation/helpers.py | 3 +- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/custom_components/extended_openai_conversation/__init__.py b/custom_components/extended_openai_conversation/__init__.py index a9ed633..274a479 100644 --- a/custom_components/extended_openai_conversation/__init__.py +++ b/custom_components/extended_openai_conversation/__init__.py @@ -56,6 +56,8 @@ CallServiceError, FunctionNotFound, NativeNotFound, + FunctionLoadFailed, + ParseArgumentsFailed, ) from .helpers import ( @@ -171,14 +173,8 @@ async def async_process( return conversation.ConversationResult( response=intent_response, conversation_id=conversation_id ) - except ( - EntityNotFound, - ServiceNotFound, - CallServiceError, - EntityNotExposed, - FunctionNotFound, - NativeNotFound, - ) as err: + except HomeAssistantError as err: + _LOGGER.error(err, exc_info=err) intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, @@ -242,9 +238,8 @@ def get_functions(self): for function in setting["function"].values(): convert_to_template(function, hass=self.hass) return result - except Exception as e: - _LOGGER.error("Failed to load functions", e) - return [] + except: + raise FunctionLoadFailed() async def query( self, @@ -323,7 +318,10 @@ async def execute_function( function, ): function_executor = FUNCTION_EXECUTORS[function["function"]["type"]] - arguments = json.loads(message["function_call"]["arguments"]) + try: + arguments = json.loads(message["function_call"]["arguments"]) + except json.decoder.JSONDecodeError: + raise ParseArgumentsFailed(message["function_call"]["arguments"]) result = await function_executor.execute( self.hass, function["function"], arguments, user_input, exposed_entities diff --git a/custom_components/extended_openai_conversation/exceptions.py b/custom_components/extended_openai_conversation/exceptions.py index 27c5ab9..f2a11dc 100644 --- a/custom_components/extended_openai_conversation/exceptions.py +++ b/custom_components/extended_openai_conversation/exceptions.py @@ -70,3 +70,34 @@ def __init__(self, name: str) -> None: def __str__(self) -> str: """Return string representation.""" return f"native function '{self.name}' does not exist" + + +class FunctionLoadFailed(HomeAssistantError): + """When function load failed.""" + + def __init__(self) -> None: + """Initialize error.""" + super().__init__( + self, + "failed to load functions. Verify functions are valid in a yaml format", + ) + + def __str__(self) -> str: + """Return string representation.""" + return "failed to load functions. Verify functions are valid in a yaml format" + + +class ParseArgumentsFailed(HomeAssistantError): + """When parse arguments failed.""" + + def __init__(self, arguments: str) -> None: + """Initialize error.""" + super().__init__( + self, + f"failed to parse arguments `{arguments}`. Increase maximum token to avoid the issue.", + ) + self.arguments = arguments + + def __str__(self) -> str: + """Return string representation.""" + return f"failed to parse arguments `{self.arguments}`. Increase maximum token to avoid the issue." diff --git a/custom_components/extended_openai_conversation/helpers.py b/custom_components/extended_openai_conversation/helpers.py index 2878738..ef79a31 100644 --- a/custom_components/extended_openai_conversation/helpers.py +++ b/custom_components/extended_openai_conversation/helpers.py @@ -486,8 +486,9 @@ async def execute( q = Template(query, hass).async_render(template_arguments) _LOGGER.info("Rendered query: %s", q) + with sqlite3.connect(db_url, uri=True) as conn: - cursor = conn.execute(q) + cursor = conn.cursor().execute(q) names = [description[0] for description in cursor.description] if function.get("single") is True: From 4d9f35ad17ba8f8243042434bab70124e10c6c1c Mon Sep 17 00:00:00 2001 From: jekalmin Date: Mon, 11 Dec 2023 21:31:17 +0900 Subject: [PATCH 10/20] [#38] Add schema validation for functions, script function bug fixed --- README.md | 1 - .../extended_openai_conversation/__init__.py | 13 ++++-- .../exceptions.py | 16 +++++++ .../extended_openai_conversation/helpers.py | 45 +++++++++++++++++-- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cdcd8ac..56bf1e6 100644 --- a/README.md +++ b/README.md @@ -367,7 +367,6 @@ Scrape version from webpage, "https://www.home-assistant.io" Unlike [scrape](https://www.home-assistant.io/integrations/scrape/), "value_template" is added at root level in which scraped data from sensors are passed. ```yaml -scrape: - spec: name: get_ha_version description: Use this function to get Home Assistant version diff --git a/custom_components/extended_openai_conversation/__init__.py b/custom_components/extended_openai_conversation/__init__.py index 274a479..7e9c9fc 100644 --- a/custom_components/extended_openai_conversation/__init__.py +++ b/custom_components/extended_openai_conversation/__init__.py @@ -58,6 +58,7 @@ NativeNotFound, FunctionLoadFailed, ParseArgumentsFailed, + InvalidFunction, ) from .helpers import ( @@ -235,9 +236,13 @@ def get_functions(self): result = yaml.safe_load(function) if function else DEFAULT_CONF_FUNCTIONS if result: for setting in result: - for function in setting["function"].values(): - convert_to_template(function, hass=self.hass) + function_executor = FUNCTION_EXECUTORS[setting["function"]["type"]] + setting["function"] = function_executor.to_arguments( + setting["function"] + ) return result + except InvalidFunction as e: + raise e except: raise FunctionLoadFailed() @@ -320,8 +325,8 @@ async def execute_function( function_executor = FUNCTION_EXECUTORS[function["function"]["type"]] try: arguments = json.loads(message["function_call"]["arguments"]) - except json.decoder.JSONDecodeError: - raise ParseArgumentsFailed(message["function_call"]["arguments"]) + except json.decoder.JSONDecodeError as err: + raise ParseArgumentsFailed(message["function_call"]["arguments"]) from err result = await function_executor.execute( self.hass, function["function"], arguments, user_input, exposed_entities diff --git a/custom_components/extended_openai_conversation/exceptions.py b/custom_components/extended_openai_conversation/exceptions.py index f2a11dc..4c513e3 100644 --- a/custom_components/extended_openai_conversation/exceptions.py +++ b/custom_components/extended_openai_conversation/exceptions.py @@ -101,3 +101,19 @@ def __init__(self, arguments: str) -> None: def __str__(self) -> str: """Return string representation.""" return f"failed to parse arguments `{self.arguments}`. Increase maximum token to avoid the issue." + + +class InvalidFunction(HomeAssistantError): + """When function validation failed.""" + + def __init__(self, function_name: str) -> None: + """Initialize error.""" + super().__init__( + self, + f"failed to validate function `{function_name}`", + ) + self.function_name = function_name + + def __str__(self) -> str: + """Return string representation.""" + return f"failed to validate function `{self.function_name}` ({self.__cause__})" diff --git a/custom_components/extended_openai_conversation/helpers.py b/custom_components/extended_openai_conversation/helpers.py index ef79a31..e92469b 100644 --- a/custom_components/extended_openai_conversation/helpers.py +++ b/custom_components/extended_openai_conversation/helpers.py @@ -4,12 +4,14 @@ import yaml import time import sqlite3 +import voluptuous as vol from bs4 import BeautifulSoup from typing import Any from homeassistant.helpers.aiohttp_client import async_get_clientsession from openai.error import AuthenticationError from urllib import parse +from homeassistant.components.script.config import SCRIPT_ENTITY_SCHEMA from homeassistant.components import automation, rest, scrape from homeassistant.components.automation.config import _async_validate_config_item from homeassistant.const import ( @@ -26,6 +28,7 @@ from homeassistant.config import AUTOMATION_CONFIG_PATH from homeassistant.components import conversation, recorder from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.template import Template from homeassistant.helpers.script import ( Script, @@ -42,6 +45,7 @@ EntityNotExposed, CallServiceError, NativeNotFound, + InvalidFunction, ) from .const import DOMAIN, EVENT_AUTOMATION_REGISTERED, DEFAULT_CONF_BASE_URL @@ -119,8 +123,20 @@ async def validate_authentication( class FunctionExecutor(ABC): - def __init__(self) -> None: + def __init__(self, data_schema=vol.Schema({})) -> None: """initialize function executor""" + self.data_schema = data_schema.extend({vol.Required("type"): str}) + + def to_arguments(self, arguments): + """to_arguments function""" + try: + return self.data_schema(arguments) + except vol.error.Error as e: + function_type = next( + (key for key, value in FUNCTION_EXECUTORS.items() if value == self), + None, + ) + raise InvalidFunction(function_type) from e @abstractmethod async def execute( @@ -137,6 +153,7 @@ async def execute( class NativeFunctionExecutor(FunctionExecutor): def __init__(self) -> None: """initialize native function""" + super().__init__(vol.Schema({vol.Required("name"): str})) async def execute( self, @@ -246,6 +263,7 @@ async def add_automation( class ScriptFunctionExecutor(FunctionExecutor): def __init__(self) -> None: """initialize script function""" + super().__init__(SCRIPT_ENTITY_SCHEMA) async def execute( self, @@ -273,6 +291,7 @@ async def execute( class TemplateFunctionExecutor(FunctionExecutor): def __init__(self) -> None: """initialize template function""" + super().__init__(vol.Schema({vol.Required("value_template"): cv.template})) async def execute( self, @@ -282,7 +301,7 @@ async def execute( user_input: conversation.ConversationInput, exposed_entities, ): - return Template(function["value_template"], hass).async_render( + return function["value_template"].async_render( arguments, parse_result=False, ) @@ -291,6 +310,11 @@ async def execute( class RestFunctionExecutor(FunctionExecutor): def __init__(self) -> None: """initialize Rest function""" + super().__init__( + vol.Schema(rest.RESOURCE_SCHEMA).extend( + {vol.Optional("value_template"): cv.template} + ) + ) async def execute( self, @@ -318,6 +342,9 @@ async def execute( class ScrapeFunctionExecutor(FunctionExecutor): def __init__(self) -> None: """initialize Scrape function""" + super().__init__( + scrape.COMBINED_SCHEMA.extend({vol.Optional("value_template"): cv.template}) + ) async def execute( self, @@ -339,13 +366,13 @@ async def execute( new_arguments = dict(arguments) for sensor_config in config["sensor"]: - name: str = sensor_config.get(CONF_NAME) + name: Template = sensor_config.get(CONF_NAME) value = self._async_update_from_rest_data( coordinator.data, sensor_config, arguments ) new_arguments["value"] = value if name: - new_arguments[name] = value + new_arguments[name.async_render()] = value result = new_arguments["value"] value_template = config.get(CONF_VALUE_TEMPLATE) @@ -402,6 +429,7 @@ def _extract_value(self, data: BeautifulSoup, sensor_config: dict[str, Any]) -> class CompositeFunctionExecutor(FunctionExecutor): def __init__(self) -> None: """initialize composite function""" + super().__init__(vol.Schema({vol.Required("sequence"): cv.ensure_list})) async def execute( self, @@ -430,6 +458,15 @@ async def execute( class SqliteFunctionExecutor(FunctionExecutor): def __init__(self) -> None: """initialize sqlite function""" + super().__init__( + vol.Schema( + { + vol.Optional("query"): str, + vol.Optional("db_url"): str, + vol.Optional("single"): bool, + } + ) + ) def is_exposed(self, entity_id, exposed_entities) -> bool: return any( From 9df27267bb3ffcd0e0f209c45762868ac76bfcb0 Mon Sep 17 00:00:00 2001 From: jekalmin Date: Mon, 11 Dec 2023 22:18:44 +0900 Subject: [PATCH 11/20] explicitly set response_format to "text" --- custom_components/extended_openai_conversation/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/extended_openai_conversation/__init__.py b/custom_components/extended_openai_conversation/__init__.py index 7e9c9fc..425d54e 100644 --- a/custom_components/extended_openai_conversation/__init__.py +++ b/custom_components/extended_openai_conversation/__init__.py @@ -265,6 +265,7 @@ async def query( DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION, ): function_call = "none" + response_format = {"type": "text"} _LOGGER.info("Prompt for %s: %s", model, messages) @@ -279,6 +280,7 @@ async def query( user=user_input.conversation_id, functions=functions, function_call=function_call, + response_format=response_format, ) _LOGGER.info("Response %s", response) From e1b4c1c0e67499120f65c5d2c8ec4ef8551a12b7 Mon Sep 17 00:00:00 2001 From: jekalmin Date: Thu, 14 Dec 2023 00:51:10 +0900 Subject: [PATCH 12/20] [#49] add OpenAIError log --- custom_components/extended_openai_conversation/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/extended_openai_conversation/__init__.py b/custom_components/extended_openai_conversation/__init__.py index 425d54e..db0d437 100644 --- a/custom_components/extended_openai_conversation/__init__.py +++ b/custom_components/extended_openai_conversation/__init__.py @@ -166,6 +166,7 @@ async def async_process( try: response = await self.query(user_input, messages, exposed_entities, 0) except error.OpenAIError as err: + _LOGGER.error(err) intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, From 28f67beaa27997587a6b1ae8a3701f810b969f3d Mon Sep 17 00:00:00 2001 From: jekalmin Date: Thu, 14 Dec 2023 23:34:52 +0900 Subject: [PATCH 13/20] add history function --- .../extended_openai_conversation/helpers.py | 87 +++++++++++++++++-- 1 file changed, 80 insertions(+), 7 deletions(-) diff --git a/custom_components/extended_openai_conversation/helpers.py b/custom_components/extended_openai_conversation/helpers.py index e92469b..ac46e8a 100644 --- a/custom_components/extended_openai_conversation/helpers.py +++ b/custom_components/extended_openai_conversation/helpers.py @@ -11,8 +11,12 @@ from openai.error import AuthenticationError from urllib import parse +from datetime import timedelta +import homeassistant.util.dt as dt_util +from homeassistant.components.recorder import get_instance, history as hist +from homeassistant.components.recorder.util import session_scope from homeassistant.components.script.config import SCRIPT_ENTITY_SCHEMA -from homeassistant.components import automation, rest, scrape +from homeassistant.components import automation, rest, scrape, history from homeassistant.components.automation.config import _async_validate_config_item from homeassistant.const import ( SERVICE_RELOAD, @@ -27,7 +31,7 @@ ) from homeassistant.config import AUTOMATION_CONFIG_PATH from homeassistant.components import conversation, recorder -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from homeassistant.helpers import config_validation as cv from homeassistant.helpers.template import Template from homeassistant.helpers.script import ( @@ -138,6 +142,13 @@ def to_arguments(self, arguments): ) raise InvalidFunction(function_type) from e + def validate_entity_ids(self, hass: HomeAssistant, entity_ids, exposed_entities): + if any(hass.states.get(entity_id) is None for entity_id in entity_ids): + raise EntityNotFound(entity_ids) + exposed_entity_ids = map(lambda e: e["entity_id"], exposed_entities) + if not set(entity_ids).issubset(exposed_entity_ids): + raise EntityNotExposed(entity_ids) + @abstractmethod async def execute( self, @@ -200,11 +211,7 @@ async def execute_service( raise CallServiceError(domain, service, service_data) if not hass.services.has_service(domain, service): raise ServiceNotFound(domain, service) - if any(hass.states.get(entity) is None for entity in entity_id): - raise EntityNotFound(entity_id) - exposed_entity_ids = map(lambda e: e["entity_id"], exposed_entities) - if not set(entity_id).issubset(exposed_entity_ids): - raise EntityNotExposed(entity_id) + self.validate_entity_ids(hass, entity_id, exposed_entities) try: await hass.services.async_call( @@ -539,6 +546,71 @@ async def execute( return result +class HistoryFunctionExecutor(FunctionExecutor): + def __init__(self) -> None: + """initialize history function""" + super().__init__() + self.view = history.HistoryPeriodView() + self.one_day = timedelta(days=1) + # super().__init__(vol.Schema({vol.Required("sequence"): cv.ensure_list})) + + def as_utc(self, value: str, default_value, parse_error_message: str): + if value is None: + return default_value + + parsed_datetime = dt_util.parse_datetime(value) + if parsed_datetime is None: + raise HomeAssistantError(parse_error_message) + + return dt_util.as_utc(parsed_datetime) + + def as_dict(self, state: State | dict[str, Any]): + if isinstance(state, State): + return state.as_dict() + return state + + async def execute( + self, + hass: HomeAssistant, + function, + arguments, + user_input: conversation.ConversationInput, + exposed_entities, + ): + start_time = arguments.get("start_time") + end_time = arguments.get("end_time") + entity_ids = arguments.get("entity_ids", []) + include_start_time_state = arguments.get("include_start_time_state", True) + significant_changes_only = arguments.get("significant_changes_only", True) + minimal_response = arguments.get("minimal_response", True) + no_attributes = arguments.get("no_attributes", True) + + now = dt_util.utcnow() + start_time = self.as_utc(start_time, now - self.one_day, "start_time not valid") + end_time = self.as_utc( + end_time, start_time + self.one_day, "end_time not valid" + ) + + self.validate_entity_ids(hass, entity_ids, exposed_entities) + + with session_scope(hass=hass, read_only=True) as session: + result = await get_instance(hass).async_add_executor_job( + hist.get_significant_states_with_session, + hass, + session, + start_time, + end_time, + entity_ids, + None, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + ) + + return [self.as_dict(item) for sublist in result.values() for item in sublist] + + FUNCTION_EXECUTORS: dict[str, FunctionExecutor] = { "predefined": NativeFunctionExecutor(), "native": NativeFunctionExecutor(), @@ -548,4 +620,5 @@ async def execute( "scrape": ScrapeFunctionExecutor(), "composite": CompositeFunctionExecutor(), "sqlite": SqliteFunctionExecutor(), + "history": HistoryFunctionExecutor(), } From 08522ef1a195016a1dcbb2f49c338d706e6dc948 Mon Sep 17 00:00:00 2001 From: jekalmin Date: Sat, 16 Dec 2023 01:11:07 +0900 Subject: [PATCH 14/20] fix composite function schema --- .../extended_openai_conversation/helpers.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/custom_components/extended_openai_conversation/helpers.py b/custom_components/extended_openai_conversation/helpers.py index ac46e8a..457b737 100644 --- a/custom_components/extended_openai_conversation/helpers.py +++ b/custom_components/extended_openai_conversation/helpers.py @@ -436,7 +436,25 @@ def _extract_value(self, data: BeautifulSoup, sensor_config: dict[str, Any]) -> class CompositeFunctionExecutor(FunctionExecutor): def __init__(self) -> None: """initialize composite function""" - super().__init__(vol.Schema({vol.Required("sequence"): cv.ensure_list})) + super().__init__( + vol.Schema( + { + vol.Required("sequence"): vol.All( + cv.ensure_list, [self.function_schema] + ) + } + ) + ) + + def function_schema(self, value: Any) -> dict: + """Validate a composite function schema.""" + if not isinstance(value, dict): + raise vol.Invalid("expected dictionary") + + composite_schema = {vol.Optional("response_variable"): str} + return FUNCTION_EXECUTORS[value["type"]].data_schema.extend(composite_schema)( + value + ) async def execute( self, From 4c591ea049f0bc4bee181624592067d8d7f439d6 Mon Sep 17 00:00:00 2001 From: jekalmin Date: Sat, 16 Dec 2023 02:09:38 +0900 Subject: [PATCH 15/20] fix history function result --- custom_components/extended_openai_conversation/helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/extended_openai_conversation/helpers.py b/custom_components/extended_openai_conversation/helpers.py index 457b737..b5365df 100644 --- a/custom_components/extended_openai_conversation/helpers.py +++ b/custom_components/extended_openai_conversation/helpers.py @@ -570,7 +570,6 @@ def __init__(self) -> None: super().__init__() self.view = history.HistoryPeriodView() self.one_day = timedelta(days=1) - # super().__init__(vol.Schema({vol.Required("sequence"): cv.ensure_list})) def as_utc(self, value: str, default_value, parse_error_message: str): if value is None: @@ -626,7 +625,7 @@ async def execute( no_attributes, ) - return [self.as_dict(item) for sublist in result.values() for item in sublist] + return [[self.as_dict(item) for item in sublist] for sublist in result.values()] FUNCTION_EXECUTORS: dict[str, FunctionExecutor] = { From fe7747c37de2d63f944ff17b24584609b131928a Mon Sep 17 00:00:00 2001 From: jekalmin Date: Sat, 16 Dec 2023 13:32:19 +0900 Subject: [PATCH 16/20] move history function to native function --- .../extended_openai_conversation/__init__.py | 10 +- .../extended_openai_conversation/helpers.py | 151 +++++++++--------- examples/function/history/README.md | 48 ++++++ examples/function/shopping_list/README.md | 10 +- 4 files changed, 142 insertions(+), 77 deletions(-) create mode 100644 examples/function/history/README.md diff --git a/custom_components/extended_openai_conversation/__init__.py b/custom_components/extended_openai_conversation/__init__.py index db0d437..7fbcf57 100644 --- a/custom_components/extended_openai_conversation/__init__.py +++ b/custom_components/extended_openai_conversation/__init__.py @@ -72,6 +72,7 @@ CompositeFunctionExecutor, convert_to_template, validate_authentication, + get_function_executor, ) @@ -237,12 +238,14 @@ def get_functions(self): result = yaml.safe_load(function) if function else DEFAULT_CONF_FUNCTIONS if result: for setting in result: - function_executor = FUNCTION_EXECUTORS[setting["function"]["type"]] + function_executor = get_function_executor( + setting["function"]["type"] + ) setting["function"] = function_executor.to_arguments( setting["function"] ) return result - except InvalidFunction as e: + except (InvalidFunction, FunctionNotFound) as e: raise e except: raise FunctionLoadFailed() @@ -325,7 +328,8 @@ async def execute_function( n_requests, function, ): - function_executor = FUNCTION_EXECUTORS[function["function"]["type"]] + function_executor = get_function_executor(function["function"]["type"]) + try: arguments = json.loads(message["function_call"]["arguments"]) except json.decoder.JSONDecodeError as err: diff --git a/custom_components/extended_openai_conversation/helpers.py b/custom_components/extended_openai_conversation/helpers.py index b5365df..55f7610 100644 --- a/custom_components/extended_openai_conversation/helpers.py +++ b/custom_components/extended_openai_conversation/helpers.py @@ -13,10 +13,15 @@ from datetime import timedelta import homeassistant.util.dt as dt_util -from homeassistant.components.recorder import get_instance, history as hist -from homeassistant.components.recorder.util import session_scope from homeassistant.components.script.config import SCRIPT_ENTITY_SCHEMA -from homeassistant.components import automation, rest, scrape, history +from homeassistant.components import ( + automation, + rest, + scrape, + history, + conversation, + recorder, +) from homeassistant.components.automation.config import _async_validate_config_item from homeassistant.const import ( SERVICE_RELOAD, @@ -30,7 +35,6 @@ CONF_ATTRIBUTE, ) from homeassistant.config import AUTOMATION_CONFIG_PATH -from homeassistant.components import conversation, recorder from homeassistant.core import HomeAssistant, State from homeassistant.helpers import config_validation as cv from homeassistant.helpers.template import Template @@ -50,6 +54,7 @@ CallServiceError, NativeNotFound, InvalidFunction, + FunctionNotFound, ) from .const import DOMAIN, EVENT_AUTOMATION_REGISTERED, DEFAULT_CONF_BASE_URL @@ -58,6 +63,13 @@ _LOGGER = logging.getLogger(__name__) +def get_function_executor(value: str): + function_executor = FUNCTION_EXECUTORS.get(value) + if function_executor is None: + raise FunctionNotFound(value) + return function_executor + + def convert_to_template( settings, template_keys=["data", "event_data", "target", "service"], @@ -183,6 +195,10 @@ async def execute( return await self.add_automation( hass, function, arguments, user_input, exposed_entities ) + if name == "get_history": + return await self.get_history( + hass, function, arguments, user_input, exposed_entities + ) raise NativeNotFound(name) @@ -266,6 +282,61 @@ async def add_automation( ) return "Success" + async def get_history( + self, + hass: HomeAssistant, + function, + arguments, + user_input: conversation.ConversationInput, + exposed_entities, + ): + start_time = arguments.get("start_time") + end_time = arguments.get("end_time") + entity_ids = arguments.get("entity_ids", []) + include_start_time_state = arguments.get("include_start_time_state", True) + significant_changes_only = arguments.get("significant_changes_only", True) + minimal_response = arguments.get("minimal_response", True) + no_attributes = arguments.get("no_attributes", True) + + now = dt_util.utcnow() + one_day = timedelta(days=1) + start_time = self.as_utc(start_time, now - one_day, "start_time not valid") + end_time = self.as_utc(end_time, start_time + one_day, "end_time not valid") + + self.validate_entity_ids(hass, entity_ids, exposed_entities) + + with recorder.util.session_scope(hass=hass, read_only=True) as session: + result = await recorder.get_instance(hass).async_add_executor_job( + recorder.history.get_significant_states_with_session, + hass, + session, + start_time, + end_time, + entity_ids, + None, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + ) + + return [[self.as_dict(item) for item in sublist] for sublist in result.values()] + + def as_utc(self, value: str, default_value, parse_error_message: str): + if value is None: + return default_value + + parsed_datetime = dt_util.parse_datetime(value) + if parsed_datetime is None: + raise HomeAssistantError(parse_error_message) + + return dt_util.as_utc(parsed_datetime) + + def as_dict(self, state: State | dict[str, Any]): + if isinstance(state, State): + return state.as_dict() + return state + class ScriptFunctionExecutor(FunctionExecutor): def __init__(self) -> None: @@ -452,9 +523,9 @@ def function_schema(self, value: Any) -> dict: raise vol.Invalid("expected dictionary") composite_schema = {vol.Optional("response_variable"): str} - return FUNCTION_EXECUTORS[value["type"]].data_schema.extend(composite_schema)( - value - ) + function_executor = get_function_executor(value["type"]) + + return function_executor.data_schema.extend(composite_schema)(value) async def execute( self, @@ -564,72 +635,7 @@ async def execute( return result -class HistoryFunctionExecutor(FunctionExecutor): - def __init__(self) -> None: - """initialize history function""" - super().__init__() - self.view = history.HistoryPeriodView() - self.one_day = timedelta(days=1) - - def as_utc(self, value: str, default_value, parse_error_message: str): - if value is None: - return default_value - - parsed_datetime = dt_util.parse_datetime(value) - if parsed_datetime is None: - raise HomeAssistantError(parse_error_message) - - return dt_util.as_utc(parsed_datetime) - - def as_dict(self, state: State | dict[str, Any]): - if isinstance(state, State): - return state.as_dict() - return state - - async def execute( - self, - hass: HomeAssistant, - function, - arguments, - user_input: conversation.ConversationInput, - exposed_entities, - ): - start_time = arguments.get("start_time") - end_time = arguments.get("end_time") - entity_ids = arguments.get("entity_ids", []) - include_start_time_state = arguments.get("include_start_time_state", True) - significant_changes_only = arguments.get("significant_changes_only", True) - minimal_response = arguments.get("minimal_response", True) - no_attributes = arguments.get("no_attributes", True) - - now = dt_util.utcnow() - start_time = self.as_utc(start_time, now - self.one_day, "start_time not valid") - end_time = self.as_utc( - end_time, start_time + self.one_day, "end_time not valid" - ) - - self.validate_entity_ids(hass, entity_ids, exposed_entities) - - with session_scope(hass=hass, read_only=True) as session: - result = await get_instance(hass).async_add_executor_job( - hist.get_significant_states_with_session, - hass, - session, - start_time, - end_time, - entity_ids, - None, - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - ) - - return [[self.as_dict(item) for item in sublist] for sublist in result.values()] - - FUNCTION_EXECUTORS: dict[str, FunctionExecutor] = { - "predefined": NativeFunctionExecutor(), "native": NativeFunctionExecutor(), "script": ScriptFunctionExecutor(), "template": TemplateFunctionExecutor(), @@ -637,5 +643,4 @@ async def execute( "scrape": ScrapeFunctionExecutor(), "composite": CompositeFunctionExecutor(), "sqlite": SqliteFunctionExecutor(), - "history": HistoryFunctionExecutor(), } diff --git a/examples/function/history/README.md b/examples/function/history/README.md new file mode 100644 index 0000000..a1e3fe6 --- /dev/null +++ b/examples/function/history/README.md @@ -0,0 +1,48 @@ +## Objective + + + + +## Function + +### get_history +```yaml +- spec: + name: get_history + description: Retrieve historical data of specified entities. + parameters: + type: object + properties: + entity_ids: + type: array + items: + type: string + description: The entity id to filter. + start_time: + type: string + description: Start of the history period in "%Y-%m-%dT%H:%M:%S%z". + end_time: + type: string + description: End of the history period in "%Y-%m-%dT%H:%M:%S%z". + required: + - entity_ids + function: + type: composite + sequence: + - type: native + name: get_history + response_variable: history_result + - type: template + value_template: >- + {% set ns = namespace(result = [], list = []) %} + {% for item_list in history_result %} + {% set ns.list = [] %} + {% for item in item_list %} + {% set last_changed = item.last_changed | as_timestamp | timestamp_local if item.last_changed else None %} + {% set new_item = dict(item, last_changed=last_changed) %} + {% set ns.list = ns.list + [new_item] %} + {% endfor %} + {% set ns.result = ns.result + [ns.list] %} + {% endfor %} + {{ ns.result }} +``` diff --git a/examples/function/shopping_list/README.md b/examples/function/shopping_list/README.md index 38477e6..d2211ce 100644 --- a/examples/function/shopping_list/README.md +++ b/examples/function/shopping_list/README.md @@ -3,7 +3,7 @@ ## Function -### add_item_to_shopping_cart +### add_item_to_list ```yaml - spec: name: add_item_to_list @@ -31,6 +31,10 @@ item: '{{item}}' target: entity_id: '{{list}}' +``` + +### remove_item_from_list +```yaml - spec: name: remove_item_from_list description: Check an item off a list @@ -58,6 +62,10 @@ status: 'completed' target: entity_id: '{{list}}' +``` + +### get_items_from_list +```yaml - spec: name: get_items_from_list description: Read back items from a list From dbf3cbc5df4e8489de09ad8bee1e9d2df84ebdbd Mon Sep 17 00:00:00 2001 From: jekalmin Date: Sun, 17 Dec 2023 20:12:35 +0900 Subject: [PATCH 17/20] add history example in README --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 56bf1e6..09a9f35 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Derived from [OpenAI Conversation](https://www.home-assistant.io/integrations/op ## Additional Features - Ability to call service of Home Assistant - Ability to create automation -- Ability to get data from API or web page +- Ability to get data from external API or web page +- Ability to retrieve state history of entities - Option to pass the current user's name to OpenAI via the user message context ## How it works @@ -81,6 +82,13 @@ Options include [OpenAI Conversation](https://www.home-assistant.io/integrations - `service_data`(string): service_data to be passed to `hass.services.async_call` - `add_automation` - `automation_config`(string): An automation configuration in a yaml format + - `get_history` + - `entity_ids`(list): a list of entity ids to filter + - `start_time`(string): defaults to 1 day before the time of the request. It determines the beginning of the period + - `end_time`(string): the end of the period in URL encoded format (defaults to 1 day) + - `minimal_response`(boolean): only return last_changed and state for states other than the first and last state (defaults to true) + - `no_attributes`(boolean): skip returning attributes from the database (defaults to true) + - `significant_changes_only`(boolean): only return significant state changes (defaults to true) - `script`: A list of services that will be called - `template`: The value to be returned from function. - `rest`: Getting data from REST API endpoint. @@ -360,6 +368,52 @@ Copy and paste below configuration into "Functions" 스크린샷 2023-10-31 오후 9 32 27 +#### 3-2. Get History +Get state history of entities + +```yaml +- spec: + name: get_history + description: Retrieve historical data of specified entities. + parameters: + type: object + properties: + entity_ids: + type: array + items: + type: string + description: The entity id to filter. + start_time: + type: string + description: Start of the history period in "%Y-%m-%dT%H:%M:%S%z". + end_time: + type: string + description: End of the history period in "%Y-%m-%dT%H:%M:%S%z". + required: + - entity_ids + function: + type: composite + sequence: + - type: native + name: get_history + response_variable: history_result + - type: template + value_template: >- + {% set ns = namespace(result = [], list = []) %} + {% for item_list in history_result %} + {% set ns.list = [] %} + {% for item in item_list %} + {% set last_changed = item.last_changed | as_timestamp | timestamp_local if item.last_changed else None %} + {% set new_item = dict(item, last_changed=last_changed) %} + {% set ns.list = ns.list + [new_item] %} + {% endfor %} + {% set ns.result = ns.result + [ns.list] %} + {% endfor %} + {{ ns.result }} +``` + + + ### 4. scrape #### 4-1. Get current HA version Scrape version from webpage, "https://www.home-assistant.io" From 4a7383bcb056b4c21863cc9dcf703aa8f79a6ee1 Mon Sep 17 00:00:00 2001 From: jekalmin Date: Sun, 17 Dec 2023 20:33:43 +0900 Subject: [PATCH 18/20] raise FunctionNotFound error when type not found --- custom_components/extended_openai_conversation/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/extended_openai_conversation/helpers.py b/custom_components/extended_openai_conversation/helpers.py index 55f7610..9da483d 100644 --- a/custom_components/extended_openai_conversation/helpers.py +++ b/custom_components/extended_openai_conversation/helpers.py @@ -539,7 +539,7 @@ async def execute( sequence = config["sequence"] for executor_config in sequence: - function_executor = FUNCTION_EXECUTORS[executor_config["type"]] + function_executor = get_function_executor(executor_config["type"]) result = await function_executor.execute( hass, executor_config, arguments, user_input, exposed_entities ) From 8d4c54a7cb8bb70e30bec0db599fcae6d9606c3d Mon Sep 17 00:00:00 2001 From: jekalmin Date: Mon, 18 Dec 2023 10:28:32 +0900 Subject: [PATCH 19/20] bump to 0.0.9 --- custom_components/extended_openai_conversation/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/extended_openai_conversation/manifest.json b/custom_components/extended_openai_conversation/manifest.json index 9bf8e17..b8f5b97 100644 --- a/custom_components/extended_openai_conversation/manifest.json +++ b/custom_components/extended_openai_conversation/manifest.json @@ -14,5 +14,5 @@ "requirements": [ "openai==0.27.2" ], - "version": "0.0.8" + "version": "0.0.9" } \ No newline at end of file From 46aeb469149939684daec569e19b84f4f30c9c01 Mon Sep 17 00:00:00 2001 From: jekalmin Date: Tue, 19 Dec 2023 21:55:09 +0900 Subject: [PATCH 20/20] add get_attributes function usage --- examples/function/attributes/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 examples/function/attributes/README.md diff --git a/examples/function/attributes/README.md b/examples/function/attributes/README.md new file mode 100644 index 0000000..7dd018d --- /dev/null +++ b/examples/function/attributes/README.md @@ -0,0 +1,25 @@ +## Objective +- Get attributes of entity + + + + +## Function + +### get_attributes +```yaml +- spec: + name: get_attributes + description: Get attributes of any home assistant entity + parameters: + type: object + properties: + entity_id: + type: string + description: entity_id + required: + - entity_id + function: + type: template + value_template: "{{states[entity_id]}}" +``` \ No newline at end of file