Skip to content

Commit 12ca16a

Browse files
Merge pull request #80 from jeroenterheerdt/main
Making this work in 2025
2 parents 7c6b96b + 31f9f70 commit 12ca16a

File tree

11 files changed

+773
-260
lines changed

11 files changed

+773
-260
lines changed

README.md

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,72 +21,44 @@ This custom integration adds a conversation agent powered by [Azure OpenAI](http
2121

2222
# What It Does
2323

24-
This conversation agent is unable to control your house. It can only query information that has been provided by Home Assistant. To be able to answer questions about your house, Home Assistant will need to provide OpenAI with the details of your house, which include areas, devices and their states.
24+
This is equivalent to the built-in [OpenAI Conversation integration](https://www.home-assistant.io/integrations/openai_conversation/). The difference is that it uses the OpenAI algorithms available through Azure. Other than that the goal is to keep the differences to a minimum. You can use this conversation integration with Assistants in Home Assistant to control you house. They have all the capabilities the built-in OpenAI Conversation integration has.
2525

2626
# Limitations
2727

28-
- Supported [Azure OpenAI API Versions](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/reference#completions):
29-
- 2023-06-01-preview
30-
- 2023-05-15
31-
- 2023-03-15-preview
32-
- 2022-12-01
33-
34-
- **Home Assistant versions** supported: Due to the breaking changes introduced by [Home Assistant Core](https://github.com/home-assistant/core/releases) for custom assistants, the Azure OpenAI Conversation integration is compatible with the following Home Assistant versions:
35-
<center>
36-
37-
| Azure OpenAI Conversation Version | Home Assistant Version |
38-
| --------------------------------- | ---------------------- |
39-
| 0.x.y | 2023.4.x |
40-
| 1.x.y | 2023.5+ |
41-
</center>
42-
43-
## AI Models supported:
44-
45-
- gpt-35-turbo
46-
- gpt-4
47-
- gpt-4-32k
28+
- **Home Assistant versions** required: 2023.5+
4829

4930
# Installation and Configuration
50-
51-
1. Ensure that the conversation integration is enabled, it allows you to converse with Home Assistant, add the line below to your `configuration.yaml`:
52-
```yaml
53-
conversation:
54-
```
55-
2. Download and install the integration from HACS: [Azure OpenAI Conversation](https://my.home-assistant.io/redirect/hacs_repository/?owner=joselcaguilar&repository=azure-openai-ha&category=integration)
56-
3. Restart your Home Assistant instance
57-
4. Go to [Settings -> Devices & Services -> Add Integration -> Azure OpenAI Conversation](https://my.home-assistant.io/redirect/config_flow_start/?domain=azure_openai_conversation)
31+
1. Download and install the integration from HACS: [Azure OpenAI Conversation](https://my.home-assistant.io/redirect/hacs_repository/?owner=joselcaguilar&repository=azure-openai-ha&category=integration).
32+
2. Restart your Home Assistant instance
33+
3. Go to [Settings -> Devices & Services -> Add Integration -> Azure OpenAI Conversation](https://my.home-assistant.io/redirect/config_flow_start/?domain=azure_openai_conversation)
34+
4. To have a conversation, made sure to deploy a chat completion model (like gpt-35-turbo or gpt-4o) in Azure.
35+
5. If you want to generate images using the available `generate_image` service, make sure to deploy the `dall-e-3` model as well.
5836
5. Type your `API Key`, `API Base` and `API Version` used following the example below and hit submit:
5937
> - API Key: 1234567890abcdef1234567890abcdef <br>
6038
> - API Base: https://iotlabopenai.openai.azure.com/ <br>
6139
> - API Version: 2023-03-15-preview <br>
62-
63-
#### **<u>Home Assistant 2023.5+ users</u>**
64-
The custom assistant needs to be added from [Settings -> Voice assistants](https://my.home-assistant.io/redirect/voice_assistants/). Once it's added, you can select the custom assistant as favorite to be used by default:
65-
66-
<p align="center">
67-
<img src="https://raw.githubusercontent.com/joselcaguilar/azure-openai-ha/main/.attachments/customAssistant.png" alt="Voice Assistants" width=90%></img>
68-
</p>
40+
6. Configure your assistant to use the Azure OpenAI Conversation.
6941

7042
# Options
7143

7244
Options for Azure OpenAI Conversation can be set via the user interface, by taking the following steps:
7345

7446
1. Browse to your Home Assistant instance.
7547
2. In the sidebar click on [Settings -> Devices & Services](https://my.home-assistant.io/redirect/integrations/).
76-
3. If multiple instances of OpenAI Conversation are configured, choose the instance you want to modify and click on "Configure".
48+
3. FInd the Azure Open AI Conversation integration and click 'Options'
7749

78-
Options available:
79-
- **Prompt Template:**
50+
Options available (same as built-in OpenAI conversation):
51+
- **Prompt:**
8052
The starting text for the AI language model to generate new text from. This text can include information about your Home Assistant instance, devices, and areas and is written using [Home Assistant Templating](https://www.home-assistant.io/docs/configuration/templating).
8153

82-
- **Completion Model:** The name of the GPT language model deployed for text generation (i.e.- `my-gpt35-model`). You can find more details on the available models in the [Azure OpenAI Documentation](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/concepts/models#finding-what-models-are-available).
54+
- **Model:** The name of the GPT language model deployed for text generation (i.e.- `my-gpt35-model`). You can find more details on the available models in the [Azure OpenAI Documentation](https://learn.microsoft.com/azure/cognitive-services/openai/concepts/models#finding-what-models-are-available). If you are having issues using an assistant that uses this integration please check this model is the model you actually deployed.
8355

8456
- Maximum Tokens to Return in Response
85-
The maximum number of words or "tokens" that the AI model should generate in its completion of the prompt. For more information, see the [Azure OpenAI Completion Documentation](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/overview#tokens).
57+
The maximum number of words or "tokens" that the AI model should generate in its completion of the prompt. For more information, see the [Azure OpenAI Completion Documentation](https://learn.microsoft.com/azure/cognitive-services/openai/overview#tokens).
8658

87-
- **Temperature:** A value that determines the level of creativity and risk-taking the model should use when generating text. A higher temperature means the model is more likely to generate unexpected results, while a lower temperature results in more deterministic results. See the [Azure OpenAI Completion Documentation](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/completions) for more information.
59+
- **Temperature:** A value that determines the level of creativity and risk-taking the model should use when generating text. A higher temperature means the model is more likely to generate unexpected results, while a lower temperature results in more deterministic results. See the [Azure OpenAI Completion Documentation](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/completions) for more information.
8860

89-
- **Top P:** An alternative to temperature, top_p determines the proportion of the most likely word choices the model should consider when generating text. A higher top_p means the model will only consider the most likely words, while a lower top_p means a wider range of words, including less likely ones, will be considered. For more information, see the [Azure OpenAI Completion Documentation](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/completions).
61+
- **Top P:** An alternative to temperature, top_p determines the proportion of the most likely word choices the model should consider when generating text. A higher top_p means the model will only consider the most likely words, while a lower top_p means a wider range of words, including less likely ones, will be considered. For more information, see the [Azure OpenAI Completion Documentation](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/completions).
9062

9163
# Changelog
9264

@@ -102,12 +74,6 @@ More languages can be added [here](./custom_components/azure_openai_conversation
10274

10375
Languages available:
10476
- English
105-
- Spanish
106-
- Dutch (credits to: [@jobvk](https://github.com/jobvk))
107-
- French (credits to: [@jobvk](https://github.com/jobvk))
108-
- German (credits to: [@jobvk](https://github.com/jobvk))
109-
- Portuguese (credits to: [@ViPeR5000](https://github.com/ViPeR5000))
110-
- Chinese (credits to: [@weiting-tw](https://github.com/weiting-tw))
11177

11278
## Documentation
11379

Lines changed: 101 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,121 @@
1-
"""The OpenAI Conversation integration."""
2-
from __future__ import annotations
1+
"""The Azure OpenAI Conversation integration."""
32

4-
from functools import partial
5-
import logging
6-
from typing import Literal
3+
from __future__ import annotations
74

85
import openai
9-
from openai import error
6+
import voluptuous as vol
107

11-
from homeassistant.components import conversation
128
from homeassistant.config_entries import ConfigEntry
13-
from homeassistant.const import CONF_API_KEY, MATCH_ALL
14-
from homeassistant.core import HomeAssistant
15-
from homeassistant.exceptions import ConfigEntryNotReady, TemplateError
16-
from homeassistant.helpers import intent, template
17-
from homeassistant.util import ulid
18-
19-
from .const import (
20-
CONF_CHAT_MODEL,
21-
CONF_MAX_TOKENS,
22-
CONF_PROMPT,
23-
CONF_TEMPERATURE,
24-
CONF_TOP_P,
25-
DEFAULT_CHAT_MODEL,
26-
DEFAULT_MAX_TOKENS,
27-
DEFAULT_PROMPT,
28-
DEFAULT_TEMPERATURE,
29-
DEFAULT_TOP_P,
30-
CONF_API_BASE,
31-
CONF_API_VERSION,
9+
from homeassistant.const import CONF_API_KEY, Platform
10+
from homeassistant.core import (
11+
HomeAssistant,
12+
ServiceCall,
13+
ServiceResponse,
14+
SupportsResponse,
3215
)
16+
from homeassistant.exceptions import (
17+
ConfigEntryNotReady,
18+
HomeAssistantError,
19+
ServiceValidationError,
20+
)
21+
from homeassistant.helpers import config_validation as cv, selector
22+
from homeassistant.helpers.httpx_client import get_async_client
23+
from homeassistant.helpers.typing import ConfigType
3324

34-
_LOGGER = logging.getLogger(__name__)
25+
from .const import CONF_API_BASE, CONF_API_VERSION, DOMAIN, LOGGER
3526

27+
SERVICE_GENERATE_IMAGE = "generate_image"
28+
PLATFORMS = (Platform.CONVERSATION,)
29+
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
30+
type OpenAIConfigEntry = ConfigEntry[openai.AsyncClient]
3631

37-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
38-
"""Set up OpenAI Conversation from a config entry."""
39-
openai.api_key = entry.data[CONF_API_KEY]
40-
openai.api_type = "azure"
41-
openai.api_base = entry.data[CONF_API_BASE]
42-
openai.api_version = entry.data[CONF_API_VERSION]
4332

44-
try:
45-
await hass.async_add_executor_job(
46-
partial(openai.Model.list, request_timeout=10)
47-
)
48-
except error.AuthenticationError as err:
49-
_LOGGER.error("Invalid API key: %s", err)
50-
return False
51-
except error.OpenAIError as err:
52-
raise ConfigEntryNotReady(err) from err
33+
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
34+
"""Set up Azure OpenAI Conversation."""
5335

54-
conversation.async_set_agent(hass, entry, OpenAIAgent(hass, entry))
55-
return True
36+
async def render_image(call: ServiceCall) -> ServiceResponse:
37+
"""Render an image with dall-e."""
38+
entry_id = call.data["config_entry"]
39+
entry = hass.config_entries.async_get_entry(entry_id)
40+
41+
if entry is None or entry.domain != DOMAIN:
42+
raise ServiceValidationError(
43+
translation_domain=DOMAIN,
44+
translation_key="invalid_config_entry",
45+
translation_placeholders={"config_entry": entry_id},
46+
)
5647

48+
client: openai.AsyncAzureOpenAI = entry.runtime_data
5749

58-
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
59-
"""Unload OpenAI."""
60-
openai.api_key = None
61-
conversation.async_unset_agent(hass, entry)
50+
try:
51+
response = await client.images.generate(
52+
model="dall-e-3",
53+
prompt=call.data["prompt"],
54+
size=call.data["size"],
55+
quality=call.data["quality"],
56+
style=call.data["style"],
57+
response_format="url",
58+
n=1,
59+
)
60+
except openai.OpenAIError as err:
61+
raise HomeAssistantError(f"Error generating image: {err}") from err
62+
63+
return response.data[0].model_dump(exclude={"b64_json"})
64+
65+
hass.services.async_register(
66+
DOMAIN,
67+
SERVICE_GENERATE_IMAGE,
68+
render_image,
69+
schema=vol.Schema(
70+
{
71+
vol.Required("config_entry"): selector.ConfigEntrySelector(
72+
{
73+
"integration": DOMAIN,
74+
}
75+
),
76+
vol.Required("prompt"): cv.string,
77+
vol.Optional("size", default="1024x1024"): vol.In(
78+
("1024x1024", "1024x1792", "1792x1024")
79+
),
80+
vol.Optional("quality", default="standard"): vol.In(("standard", "hd")),
81+
vol.Optional("style", default="vivid"): vol.In(("vivid", "natural")),
82+
}
83+
),
84+
supports_response=SupportsResponse.ONLY,
85+
)
6286
return True
6387

6488

65-
class OpenAIAgent(conversation.AbstractConversationAgent):
66-
"""OpenAI conversation agent."""
67-
68-
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
69-
"""Initialize the agent."""
70-
self.hass = hass
71-
self.entry = entry
72-
self.history: dict[str, list[dict]] = {}
73-
74-
@property
75-
def attribution(self):
76-
"""Return the attribution."""
77-
return {
78-
"name": "Powered by Azure OpenAI",
79-
"url": "https://azure.microsoft.com/products/cognitive-services/openai-service",
80-
}
81-
82-
@property
83-
def supported_languages(self) -> list[str] | Literal["*"]:
84-
"""Return a list of supported languages."""
85-
return MATCH_ALL
86-
87-
async def async_process(
88-
self, user_input: conversation.ConversationInput
89-
) -> conversation.ConversationResult:
90-
"""Process a sentence."""
91-
raw_prompt = self.entry.options.get(CONF_PROMPT, DEFAULT_PROMPT)
92-
model = self.entry.options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL)
93-
max_tokens = self.entry.options.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
94-
top_p = self.entry.options.get(CONF_TOP_P, DEFAULT_TOP_P)
95-
temperature = self.entry.options.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
96-
97-
if user_input.conversation_id in self.history:
98-
conversation_id = user_input.conversation_id
99-
messages = self.history[conversation_id]
100-
else:
101-
conversation_id = ulid.ulid()
102-
try:
103-
prompt = self._async_generate_prompt(raw_prompt)
104-
except TemplateError as err:
105-
_LOGGER.error("Error rendering prompt: %s", err)
106-
intent_response = intent.IntentResponse(language=user_input.language)
107-
intent_response.async_set_error(
108-
intent.IntentResponseErrorCode.UNKNOWN,
109-
f"Sorry, I had a problem with my template: {err}",
110-
)
111-
return conversation.ConversationResult(
112-
response=intent_response, conversation_id=conversation_id
113-
)
114-
messages = [{"role": "system", "content": prompt}]
115-
116-
messages.append({"role": "user", "content": user_input.text})
117-
118-
_LOGGER.debug("Prompt for %s: %s", model, messages)
89+
async def async_setup_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bool:
90+
"""Set up Azure OpenAI Conversation from a config entry."""
11991

120-
try:
121-
result = await openai.ChatCompletion.acreate(
122-
engine=model,
123-
messages=messages,
124-
max_tokens=max_tokens,
125-
top_p=top_p,
126-
temperature=temperature,
127-
user=conversation_id,
128-
)
129-
except error.OpenAIError as err:
130-
intent_response = intent.IntentResponse(language=user_input.language)
131-
intent_response.async_set_error(
132-
intent.IntentResponseErrorCode.UNKNOWN,
133-
f"Sorry, I had a problem talking to OpenAI: {err}",
134-
)
135-
return conversation.ConversationResult(
136-
response=intent_response, conversation_id=conversation_id
137-
)
92+
client = openai.AsyncAzureOpenAI(
93+
azure_endpoint=entry.data[CONF_API_BASE],
94+
api_version=entry.data[CONF_API_VERSION],
95+
api_key=entry.data[CONF_API_KEY],
96+
http_client=get_async_client(hass),
97+
)
13898

139-
_LOGGER.debug("Response %s", result)
140-
response = result["choices"][0]["message"]
141-
messages.append(response)
142-
self.history[conversation_id] = messages
99+
# Cache current platform data which gets added to each request (caching done by library)
100+
_ = await hass.async_add_executor_job(client.platform_headers)
143101

144-
intent_response = intent.IntentResponse(language=user_input.language)
145-
intent_response.async_set_speech(response["content"])
146-
return conversation.ConversationResult(
147-
response=intent_response, conversation_id=conversation_id
148-
)
102+
try:
103+
await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list)
104+
except openai.AuthenticationError as err:
105+
LOGGER.error("Invalid API key: %s", err)
106+
return False
107+
except openai.OpenAIError as err:
108+
raise ConfigEntryNotReady(err) from err
149109

150-
def _async_generate_prompt(self, raw_prompt: str) -> str:
151-
"""Generate a prompt for the user."""
152-
return template.Template(raw_prompt, self.hass).async_render(
153-
{
154-
"ha_name": self.hass.config.location_name,
155-
},
156-
parse_result=False,
157-
)
110+
entry.runtime_data = client
111+
112+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
113+
114+
return True
115+
116+
117+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
118+
"""Unload Azure OpenAI."""
119+
120+
"""Unload OpenAI."""
121+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

0 commit comments

Comments
 (0)