Skip to content

Commit

Permalink
Merge pull request #2 from jekalmin/add-automation
Browse files Browse the repository at this point in the history
add "add_automation" function, change name from "predefined" to "native"
  • Loading branch information
jekalmin authored Oct 14, 2023
2 parents 017ed77 + 19249c9 commit 0ce560e
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 29 deletions.
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ Options include [OpenAI Conversation](https://www.home-assistant.io/integrations


### Functions

#### Supported function types
- `native`: function that is provided by "extended_openai_conversation".
- Currently supported native functions and parameters are:
- `execute_service`
- `domain`(string): domain to be passed to `hass.services.async_call`
- `service`(string): service to be passed to `hass.services.async_call`
- `service_data`(string): service_data to be passed to `hass.services.async_call`
- `add_automation`
- `automation_config`(string): An automation configuration in a yaml format
- `script`: A list of services that will be called
- `template`: The value to be returned from function.

Below is a default configuration of functions.

```yaml
Expand Down Expand Up @@ -87,13 +100,13 @@ Below is a default configuration of functions.
- service
- service_data
function:
type: predefined
type: native
name: execute_service
```
This is an example of configuration of functions.
#### Example 1.
#### Example 1. Get weather, Add to cart
```yaml
- spec:
name: get_current_weather
Expand Down Expand Up @@ -141,7 +154,7 @@ Then you will be able to let OpenAI call your function.
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <img width="391" alt="스크린샷 2023-10-07 오후 7 56 27" src="https://github.com/jekalmin/extended_openai_conversation/assets/2917984/05e31ea5-daab-4759-b57d-9f5be546bac8"> | <img width="341" alt="스크린샷 2023-10-07 오후 7 54 56" src="https://github.com/jekalmin/extended_openai_conversation/assets/2917984/89060728-4703-4e57-8423-354cdc47f0ee"> |
#### Example 2.
#### Example 2. Send messages to another messenger
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.
Expand All @@ -165,12 +178,35 @@ In order to accomplish "send it to Line" like [example3](https://github.com/jeka
message: "{{ message }}"
```
Supported function types are following:
#### Example 3. Add Automation
Before adding automation, I highly recommend set notification on `automation_registered_via_extended_openai_conversation` event and create separate "Extended OpenAI Assistant" and "Assistant"

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

- `predefined`: Pre-defined function that is provided by "extended_openai_conversation".
- Currently `name: execute_service` is only supported.
- `script`: A list of services that will be called
- `template`: The value to be returned from function.
| Create Assistant | Notify on created |
|----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <img width="830" alt="1" src="https://github.com/jekalmin/extended_openai_conversation/assets/2917984/b7030a46-9a4e-4ea8-a4ed-03d2eb3af0a9"> | <img width="1116" alt="스크린샷 2023-10-13 오후 6 01 40" src="https://github.com/jekalmin/extended_openai_conversation/assets/2917984/7afa3709-1c1d-41d0-8847-70f2102d824f"> |


Copy and paste below configuration into "Functions"

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

## Logging
In order to monitor logs of API requests and responses, add following config to `configuration.yaml` file
Expand Down
12 changes: 5 additions & 7 deletions custom_components/extended_openai_conversation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import openai
from openai import error


from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, MATCH_ALL
Expand All @@ -24,7 +23,6 @@
ServiceNotFound,
)


from homeassistant.helpers import (
config_validation as cv,
intent,
Expand All @@ -47,20 +45,19 @@
DEFAULT_TOP_P,
DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION,
DOMAIN,
SERVICE_RELOAD,
)

from .exceptions import (
EntityNotFound,
EntityNotExposed,
CallServiceError,
FunctionNotFound,
PredefinedNotFound,
NativeNotFound,
)

from .helpers import (
FunctionExecutor,
PredefinedFunctionExecutor,
NativeFunctionExecutor,
ScriptFunctionExecutor,
TemplateFunctionExecutor,
convert_to_template,
Expand All @@ -73,7 +70,8 @@


FUNCTION_EXECUTORS: dict[str, FunctionExecutor] = {
"predefined": PredefinedFunctionExecutor(),
"predefined": NativeFunctionExecutor(),
"native": NativeFunctionExecutor(),
"script": ScriptFunctionExecutor(),
"template": TemplateFunctionExecutor(),
}
Expand Down Expand Up @@ -175,7 +173,7 @@ async def async_process(
CallServiceError,
EntityNotExposed,
FunctionNotFound,
PredefinedNotFound,
NativeNotFound,
) as err:
intent_response = intent.IntentResponse(language=user_input.language)
intent_response.async_set_error(
Expand Down
8 changes: 4 additions & 4 deletions custom_components/extended_openai_conversation/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

DOMAIN = "extended_openai_conversation"
DEFAULT_NAME = "Extended OpenAI Conversation"
SERVICE_RELOAD = "reload"
EVENT_AUTOMATION_REGISTERED = "automation_registered_via_extended_openai_conversation"
CONF_PROMPT = "prompt"
DEFAULT_PROMPT = """This is smart home is controlled by Home Assistant.
Answer the user's question using a list of available devices in one or two sentences, at most three sentences in everyday language.
Answer the user's question using a list of available devices in one or two sentences in everyday language.
A list of available devices in this smart home:
```csv
Expand All @@ -29,7 +29,7 @@
CONF_TEMPERATURE = "temperature"
DEFAULT_TEMPERATURE = 0.5
CONF_MAX_FUNCTION_CALLS_PER_CONVERSATION = "max_function_calls_per_conversation"
DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION = 3
DEFAULT_MAX_FUNCTION_CALLS_PER_CONVERSATION = 1
CONF_FUNCTIONS = "functions"
DEFAULT_CONF_FUNCTIONS = [
{
Expand Down Expand Up @@ -63,6 +63,6 @@
},
},
},
"function": {"type": "predefined", "name": "execute_service"},
"function": {"type": "native", "name": "execute_service"},
}
]
9 changes: 5 additions & 4 deletions custom_components/extended_openai_conversation/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,15 @@ def __str__(self) -> str:
"""Return string representation."""
return f"function '{self.function}' does not exist"

class PredefinedNotFound(HomeAssistantError):
"""When predefined not found."""

class NativeNotFound(HomeAssistantError):
"""When native function not found."""

def __init__(self, name: str) -> None:
"""Initialize error."""
super().__init__(self, f"predefined '{name}' does not exist")
super().__init__(self, f"native function '{name}' does not exist")
self.name = name

def __str__(self) -> str:
"""Return string representation."""
return f"predefined '{self.name}' does not exist"
return f"native function '{self.name}' does not exist"
63 changes: 57 additions & 6 deletions custom_components/extended_openai_conversation/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from abc import ABC, abstractmethod
import logging

import os
import yaml
import time

from homeassistant.components import automation
from homeassistant.components.automation.config import _async_validate_config_item
from homeassistant.const import SERVICE_RELOAD
from homeassistant.config import AUTOMATION_CONFIG_PATH
from homeassistant.components import conversation
from homeassistant.core import HomeAssistant
from homeassistant.helpers import template
Expand All @@ -18,10 +25,10 @@
EntityNotFound,
EntityNotExposed,
CallServiceError,
PredefinedNotFound,
NativeNotFound,
)

from .const import DOMAIN
from .const import DOMAIN, EVENT_AUTOMATION_REGISTERED


_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -65,9 +72,9 @@ async def execute(
"""execute function"""


class PredefinedFunctionExecutor(FunctionExecutor):
class NativeFunctionExecutor(FunctionExecutor):
def __init__(self) -> None:
"""initialize predefined function"""
"""initialize native function"""

async def execute(
self,
Expand All @@ -82,8 +89,12 @@ async def execute(
return await self.execute_service(
hass, function, arguments, user_input, exposed_entities
)
if name == "add_automation":
return await self.add_automation(
hass, function, arguments, user_input, exposed_entities
)

raise PredefinedNotFound(name)
raise NativeNotFound(name)

async def execute_service(
self,
Expand Down Expand Up @@ -129,6 +140,46 @@ async def execute_service(

return str(result)

async def add_automation(
self,
hass: HomeAssistant,
function,
arguments,
user_input: conversation.ConversationInput,
exposed_entities,
) -> str:
automation_config = yaml.safe_load(arguments["automation_config"])
config = {"id": str(round(time.time() * 1000))}
if isinstance(automation_config, list):
config.update(automation_config[0])
if isinstance(automation_config, dict):
config.update(automation_config)

await _async_validate_config_item(hass, config, True, False)

automations = [config]
with open(
os.path.join(hass.config.config_dir, AUTOMATION_CONFIG_PATH),
"r",
encoding="utf-8",
) as f:
current_automations = yaml.safe_load(f.read())

with open(
os.path.join(hass.config.config_dir, AUTOMATION_CONFIG_PATH),
"a" if current_automations else "w",
encoding="utf-8",
) as f:
raw_config = yaml.dump(automations, allow_unicode=True, sort_keys=False)
f.write("\n" + raw_config)

await hass.services.async_call(automation.config.DOMAIN, SERVICE_RELOAD)
hass.bus.async_fire(
EVENT_AUTOMATION_REGISTERED,
{"automation_config": config, "raw_config": raw_config},
)
return "Success"


class ScriptFunctionExecutor(FunctionExecutor):
def __init__(self) -> None:
Expand Down

0 comments on commit 0ce560e

Please sign in to comment.