mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-25 16:44:01 +08:00
new version
This commit is contained in:
@@ -1,156 +1,150 @@
|
||||
"""The HA Text AI Integration."""
|
||||
from __future__ import annotations
|
||||
"""The HA text AI integration."""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components import input_text
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
SERVICE_ASK_QUESTION,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_GET_HISTORY,
|
||||
SERVICE_SET_SYSTEM_PROMPT,
|
||||
CONF_MODEL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_API_ENDPOINT,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
)
|
||||
from .coordinator import HATextAICoordinator
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_API_BASE,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
DEFAULT_API_BASE,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
TEXT_HELPER_PREFIX,
|
||||
TEXT_HELPER_MAX_LENGTH,
|
||||
)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = []
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the HA Text AI component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up HA Text AI from a config entry."""
|
||||
client = AsyncOpenAI(
|
||||
api_key=entry.data[CONF_API_KEY],
|
||||
base_url=entry.data.get(CONF_API_BASE, DEFAULT_API_BASE)
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
"client": client,
|
||||
CONF_REQUEST_INTERVAL: entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
||||
"queue": [],
|
||||
"processing": False,
|
||||
}
|
||||
|
||||
async def create_text_helper(name: str) -> str:
|
||||
"""Create a Text Helper if it doesn't exist."""
|
||||
object_id = f"{TEXT_HELPER_PREFIX}{name}"
|
||||
|
||||
entity_id = f"input_text.{object_id}"
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
if entity_registry.async_get(entity_id) is None:
|
||||
await hass.services.async_call(
|
||||
"input_text",
|
||||
"create",
|
||||
{
|
||||
"name": f"AI Response {name}",
|
||||
"id": object_id,
|
||||
"max": TEXT_HELPER_MAX_LENGTH,
|
||||
"initial": "",
|
||||
},
|
||||
)
|
||||
|
||||
return entity_id
|
||||
|
||||
async def handle_text_ai_call(call: ServiceCall) -> None:
|
||||
"""Handle the text AI service call."""
|
||||
entry_id = list(hass.data[DOMAIN].keys())[0] # Используем первую настроенную интеграцию
|
||||
|
||||
response_id = call.data.get("response_id", "default")
|
||||
entity_id = await create_text_helper(response_id)
|
||||
|
||||
request_data = {
|
||||
"prompt": call.data["prompt"],
|
||||
"model": call.data.get("model", "gpt-3.5-turbo"),
|
||||
"temperature": call.data.get("temperature", 0.7),
|
||||
"max_tokens": call.data.get("max_tokens", 150),
|
||||
"top_p": call.data.get("top_p", 1.0),
|
||||
"frequency_penalty": call.data.get("frequency_penalty", 0.0),
|
||||
"presence_penalty": call.data.get("presence_penalty", 0.0),
|
||||
"entity_id": entity_id,
|
||||
}
|
||||
|
||||
hass.data[DOMAIN][entry_id]["queue"].append(request_data)
|
||||
|
||||
if not hass.data[DOMAIN][entry_id]["processing"]:
|
||||
asyncio.create_task(process_queue(hass, entry_id))
|
||||
|
||||
async def process_queue(hass: HomeAssistant, entry_id: str) -> None:
|
||||
"""Process the queue of requests."""
|
||||
if hass.data[DOMAIN][entry_id]["processing"]:
|
||||
return
|
||||
|
||||
hass.data[DOMAIN][entry_id]["processing"] = True
|
||||
client = hass.data[DOMAIN][entry_id]["client"]
|
||||
|
||||
while hass.data[DOMAIN][entry_id]["queue"]:
|
||||
request = hass.data[DOMAIN][entry_id]["queue"].pop(0)
|
||||
|
||||
try:
|
||||
response = await client.chat.completions.create(
|
||||
model=request["model"],
|
||||
messages=[{"role": "user", "content": request["prompt"]}],
|
||||
temperature=request["temperature"],
|
||||
max_tokens=request["max_tokens"],
|
||||
top_p=request["top_p"],
|
||||
frequency_penalty=request["frequency_penalty"],
|
||||
presence_penalty=request["presence_penalty"],
|
||||
)
|
||||
|
||||
response_text = response.choices[0].message.content
|
||||
await hass.services.async_call(
|
||||
"input_text",
|
||||
"set_value",
|
||||
{
|
||||
"entity_id": request["entity_id"],
|
||||
"value": response_text
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error("Error processing request: %s", str(e))
|
||||
|
||||
await asyncio.sleep(hass.data[DOMAIN][entry_id][CONF_REQUEST_INTERVAL])
|
||||
|
||||
hass.data[DOMAIN][entry_id]["processing"] = False
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"text_ai_call",
|
||||
handle_text_ai_call,
|
||||
schema=vol.Schema({
|
||||
vol.Required("prompt"): cv.string,
|
||||
vol.Optional("response_id", default="default"): cv.string,
|
||||
vol.Optional("model", default="gpt-3.5-turbo"): cv.string,
|
||||
vol.Optional("temperature", default=0.7): vol.Coerce(float),
|
||||
vol.Optional("max_tokens", default=150): vol.Coerce(int),
|
||||
vol.Optional("top_p", default=1.0): vol.Coerce(float),
|
||||
vol.Optional("frequency_penalty", default=0.0): vol.Coerce(float),
|
||||
vol.Optional("presence_penalty", default=0.0): vol.Coerce(float),
|
||||
})
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the HA text AI component from configuration.yaml."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up HA text AI from a config entry."""
|
||||
coordinator = HATextAICoordinator(
|
||||
hass,
|
||||
api_key=entry.data[CONF_API_KEY],
|
||||
endpoint=entry.data.get(CONF_API_ENDPOINT),
|
||||
model=entry.data.get(CONF_MODEL),
|
||||
temperature=entry.data.get(CONF_TEMPERATURE),
|
||||
max_tokens=entry.data.get(CONF_MAX_TOKENS),
|
||||
request_interval=entry.data.get(CONF_REQUEST_INTERVAL),
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def async_ask_question(call: ServiceCall) -> None:
|
||||
"""Handle the ask_question service call."""
|
||||
question = call.data["question"]
|
||||
model = call.data.get("model", coordinator.model)
|
||||
temperature = call.data.get("temperature", coordinator.temperature)
|
||||
max_tokens = call.data.get("max_tokens", coordinator.max_tokens)
|
||||
|
||||
# Temporarily update parameters if they were overridden
|
||||
original_model = coordinator.model
|
||||
original_temperature = coordinator.temperature
|
||||
original_max_tokens = coordinator.max_tokens
|
||||
|
||||
try:
|
||||
coordinator.model = model
|
||||
coordinator.temperature = temperature
|
||||
coordinator.max_tokens = max_tokens
|
||||
await coordinator.async_ask_question(question)
|
||||
finally:
|
||||
# Restore original parameters
|
||||
coordinator.model = original_model
|
||||
coordinator.temperature = original_temperature
|
||||
coordinator.max_tokens = original_max_tokens
|
||||
|
||||
async def async_clear_history(call: ServiceCall) -> None:
|
||||
"""Handle the clear_history service call."""
|
||||
coordinator._responses.clear()
|
||||
await coordinator.async_refresh()
|
||||
|
||||
async def async_get_history(call: ServiceCall) -> None:
|
||||
"""Handle the get_history service call."""
|
||||
limit = call.data.get("limit", 10)
|
||||
history = list(coordinator._responses.items())[-limit:]
|
||||
return {
|
||||
"history": [
|
||||
{"question": q, "response": r} for q, r in history
|
||||
]
|
||||
}
|
||||
|
||||
async def async_set_system_prompt(call: ServiceCall) -> None:
|
||||
"""Handle the set_system_prompt service call."""
|
||||
prompt = call.data["prompt"]
|
||||
coordinator.system_prompt = prompt
|
||||
|
||||
# Register all services
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_ASK_QUESTION,
|
||||
async_ask_question,
|
||||
schema=vol.Schema({
|
||||
vol.Required("question"): cv.string,
|
||||
vol.Optional("model"): cv.string,
|
||||
vol.Optional("temperature"): vol.Coerce(float),
|
||||
vol.Optional("max_tokens"): vol.Coerce(int),
|
||||
})
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
async_clear_history,
|
||||
schema=vol.Schema({})
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_HISTORY,
|
||||
async_get_history,
|
||||
schema=vol.Schema({
|
||||
vol.Optional("limit"): vol.Coerce(int),
|
||||
})
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SYSTEM_PROMPT,
|
||||
async_set_system_prompt,
|
||||
schema=vol.Schema({
|
||||
vol.Required("prompt"): cv.string,
|
||||
})
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# Unregister services
|
||||
for service in [
|
||||
SERVICE_ASK_QUESTION,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_GET_HISTORY,
|
||||
SERVICE_SET_SYSTEM_PROMPT
|
||||
]:
|
||||
hass.services.async_remove(DOMAIN, service)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -1,130 +1,89 @@
|
||||
"""Config flow for HA Text AI Integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
#### `config_flow.py`
|
||||
```python
|
||||
"""Config flow for HA text AI integration."""
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
|
||||
import voluptuous as vol
|
||||
from openai import AsyncOpenAI
|
||||
from openai import OpenAIError, AuthenticationError, APIConnectionError
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_API_ENDPOINT,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_API_ENDPOINT,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for HA text AI."""
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_API_KEY,
|
||||
CONF_API_BASE,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
DEFAULT_API_BASE,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
)
|
||||
VERSION = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
class TextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for HA Text AI Integration."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="HA text AI", data=user_input)
|
||||
|
||||
VERSION = 1
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("api_key"): str,
|
||||
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str,
|
||||
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.Coerce(int),
|
||||
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): str,
|
||||
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.Coerce(float),
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self._test_api_key(user_input[CONF_API_KEY], user_input.get(CONF_API_BASE))
|
||||
return self.async_create_entry(
|
||||
title="HA Text AI",
|
||||
data=user_input,
|
||||
)
|
||||
except ApiKeyError:
|
||||
errors["base"] = "invalid_api_key"
|
||||
except ApiConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle options flow for HA text AI."""
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_API_BASE, default=DEFAULT_API_BASE): str,
|
||||
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): int,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
@staticmethod
|
||||
async def _test_api_key(api_key: str, api_base: str | None) -> None:
|
||||
"""Test if the API key is valid."""
|
||||
try:
|
||||
client = AsyncOpenAI(
|
||||
api_key=api_key,
|
||||
base_url=api_base if api_base else DEFAULT_API_BASE
|
||||
)
|
||||
|
||||
models = await client.models.list()
|
||||
if not models.data:
|
||||
raise ApiKeyError
|
||||
|
||||
except AuthenticationError as err:
|
||||
raise ApiKeyError from err
|
||||
except APIConnectionError as err:
|
||||
raise ApiConnectionError from err
|
||||
except OpenAIError as err:
|
||||
if getattr(err, 'status_code', None) == 401:
|
||||
raise ApiKeyError from err
|
||||
raise ApiConnectionError from err
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle options flow."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> TextAIOptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return TextAIOptionsFlow(config_entry)
|
||||
|
||||
|
||||
class TextAIOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Handle options flow for HA Text AI Integration."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_REQUEST_INTERVAL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
|
||||
),
|
||||
): int,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ApiKeyError(HomeAssistantError):
|
||||
"""Error to indicate there is an invalid API key."""
|
||||
|
||||
|
||||
class ApiConnectionError(HomeAssistantError):
|
||||
"""Error to indicate there is a connection error."""
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
|
||||
),
|
||||
): vol.Coerce(float),
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
|
||||
),
|
||||
): vol.Coerce(int),
|
||||
vol.Optional(
|
||||
CONF_REQUEST_INTERVAL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
|
||||
),
|
||||
): vol.Coerce(float),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
"""Constants for the HA Text AI Integration."""
|
||||
DOMAIN = "ha_text_ai"
|
||||
"""Constants for the HA text AI integration."""
|
||||
from homeassistant.const import Platform
|
||||
|
||||
CONF_API_KEY = "api_key"
|
||||
CONF_API_BASE = "api_base"
|
||||
CONF_REQUEST_INTERVAL = "request_interval"
|
||||
DOMAIN = "ha_text_ai"
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
DEFAULT_API_BASE = "https://api.openai.com/v1"
|
||||
DEFAULT_REQUEST_INTERVAL = 5 # 5 seconds
|
||||
# Configuration
|
||||
CONF_MODEL = "model"
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
CONF_API_ENDPOINT = "api_endpoint"
|
||||
CONF_REQUEST_INTERVAL = "request_interval"
|
||||
|
||||
# Text Helper related
|
||||
TEXT_HELPER_MAX_LENGTH = 65536
|
||||
TEXT_HELPER_PREFIX = "text_ai_response_"
|
||||
# Defaults
|
||||
DEFAULT_MODEL = "gpt-3.5-turbo"
|
||||
DEFAULT_TEMPERATURE = 0.7
|
||||
DEFAULT_MAX_TOKENS = 1000
|
||||
DEFAULT_API_ENDPOINT = "https://api.openai.com/v1"
|
||||
DEFAULT_REQUEST_INTERVAL = 1.0
|
||||
|
||||
# Attributes
|
||||
ATTR_QUESTION = "question"
|
||||
ATTR_RESPONSE = "response"
|
||||
ATTR_LAST_UPDATED = "last_updated"
|
||||
|
||||
# Services
|
||||
SERVICE_ASK_QUESTION = "ask_question
|
||||
SERVICE_CLEAR_HISTORY = "clear_history"
|
||||
SERVICE_GET_HISTORY = "get_history"
|
||||
SERVICE_SET_SYSTEM_PROMPT = "set_system_prompt"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Data coordinator for HA text AI."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict
|
||||
|
||||
import openai
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class HATextAICoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching data from the API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api_key: str,
|
||||
endpoint: str,
|
||||
model: str,
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
request_interval: float,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=request_interval),
|
||||
)
|
||||
|
||||
self.api_key = api_key
|
||||
self.endpoint = endpoint
|
||||
self.model = model
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self._question_queue = asyncio.Queue()
|
||||
self._responses: Dict[str, Any] = {}
|
||||
|
||||
openai.api_key = self.api_key
|
||||
if endpoint != "https://api.openai.com/v1":
|
||||
openai.api_base = endpoint
|
||||
|
||||
async def _async_update_data(self) -> Dict[str, Any]:
|
||||
"""Update data via OpenAI API."""
|
||||
if self._question_queue.empty():
|
||||
return self._responses
|
||||
|
||||
try:
|
||||
question = await self._question_queue.get()
|
||||
response = await self.hass.async_add_executor_job(
|
||||
self._make_api_call, question
|
||||
)
|
||||
self._responses[question] = response
|
||||
return self._responses
|
||||
|
||||
except openai.error.AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error communicating with API: %s", err)
|
||||
return self._responses
|
||||
|
||||
def _make_api_call(self, question: str) -> str:
|
||||
"""Make API call to OpenAI."""
|
||||
try:
|
||||
completion = openai.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[{"role": "user", "content": question}],
|
||||
temperature=self.temperature,
|
||||
max_tokens=self.max_tokens,
|
||||
)
|
||||
return completion.choices[0].message.content
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error in API call: %s", err)
|
||||
raise
|
||||
|
||||
async def async_ask_question(self, question: str) -> None:
|
||||
"""Add question to queue."""
|
||||
await self._question_queue.put(question)
|
||||
await self.async_refresh()
|
||||
@@ -1,17 +1,15 @@
|
||||
{
|
||||
"domain": "ha_text_ai",
|
||||
"name": "HA Text AI",
|
||||
"version": "1.0.0",
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/smkrv/ha-text-ai",
|
||||
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
|
||||
"requirements": ["openai>=1.0.0"],
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
"homekit": {},
|
||||
"dependencies": [],
|
||||
"codeowners": ["@smkrv"],
|
||||
"iot_class": "cloud_polling",
|
||||
"min_ha_version": "2023.8.0",
|
||||
"icon": "images/icon.svg"
|
||||
{
|
||||
"domain": "ha_text_ai",
|
||||
"name": "HA text AI",
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/smkrv/ha_text_ai",
|
||||
"issue_tracker": "https://github.com/smkrv/ha_text_ai/issues",
|
||||
"requirements": ["openai>=1.0.0"],
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
"homekit": {},
|
||||
"dependencies": [],
|
||||
"codeowners": ["@smkrv"],
|
||||
"version": "1.0.1",
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Sensor platform for HA text AI."""
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, ATTR_QUESTION, ATTR_RESPONSE, ATTR_LAST_UPDATED
|
||||
from .coordinator import HATextAICoordinator
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HA text AI sensor."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([HATextAISensor(coordinator, entry)], True)
|
||||
|
||||
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
||||
"""HA text AI Sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HATextAICoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._config_entry = config_entry
|
||||
self._attr_unique_id = f"{config_entry.entry_id}"
|
||||
self._attr_name = "HA text AI"
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return entity specific state attributes."""
|
||||
return {
|
||||
ATTR_QUESTION: list(self.coordinator.data.keys())[-1] if self.coordinator.data else None,
|
||||
ATTR_RESPONSE: list(self.coordinator.data.values())[-1] if self.coordinator.data else None,
|
||||
ATTR_LAST_UPDATED: self.coordinator.last_update_success_time,
|
||||
}
|
||||
@@ -1,34 +1,38 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Service is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_api_key": "Invalid API key",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"api_base": "API Base URL",
|
||||
"request_interval": "Request interval (seconds)"
|
||||
},
|
||||
"description": "Enter your OpenAI API credentials",
|
||||
"title": "OpenAI API"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"request_interval": "Request interval (seconds)"
|
||||
},
|
||||
"description": "Configure HA Text AI options",
|
||||
"title": "HA Text AI Options"
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up HA text AI",
|
||||
"description": "Set up your OpenAI integration",
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"model": "Model",
|
||||
"temperature": "Temperature",
|
||||
"max_tokens": "Max Tokens",
|
||||
"api_endpoint": "API Endpoint",
|
||||
"request_interval": "Request Interval (seconds)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "API key is invalid.",
|
||||
"cannot_connect": "Failed to connect to API.",
|
||||
"unknown": "Unexpected error occurred."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "HA text AI Options",
|
||||
"data": {
|
||||
"temperature": "Temperature",
|
||||
"max_tokens": "Max Tokens",
|
||||
"request_interval": "Request Interval (seconds)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Сервис уже настроен"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Ошибка подключения",
|
||||
"invalid_api_key": "Неверный API ключ",
|
||||
"unknown": "Неожиданная ошибка"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API ключ",
|
||||
"api_base": "Базовый URL API",
|
||||
"request_interval": "Интервал запросов (секунды)"
|
||||
},
|
||||
"description": "Введите учетные данные OpenAI API",
|
||||
"title": "OpenAI API"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"request_interval": "Интервал запросов (секунды)"
|
||||
},
|
||||
"description": "Настройка параметров HA Text AI",
|
||||
"title": "Настройки HA Text AI"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user