Compare commits

...
15 Commits
Author SHA1 Message Date
SMKRV 9a7635c2ae Release v1.1.0 2024-11-19 18:55:14 +03:00
SMKRV df3d79c20c feat: add multi-provider support,
config improvements
2024-11-19 18:53:51 +03:00
SMKRV d6144be7ed v.1.0.10 2024-11-19 17:49:48 +03:00
SMKRV 9afbb904b3 __init__.py:
added: from .coordinator import HATextAICoordinator
2024-11-19 17:48:00 +03:00
SMKRV 93558b2444 Version update 2024-11-19 17:34:27 +03:00
SMKRV 9f93f1ee18 Main changes:
Removed the validate_endpoint function
Optimized the validate_api_connection function
Simplified API connection verification
Preserved all error handling and retry logic
Improved exception handling
The integration should now correctly verify the
OpenAI API connection without false endpoint_not_available errors.
2024-11-19 17:33:27 +03:00
SMKRV 30a9b53ba1 Markdown changes 2024-11-19 17:18:23 +03:00
SMKRV 5175970d55 structure.md added 2024-11-19 17:16:30 +03:00
SMKRV f6bfbd4a07 Release v1.0.8 2024-11-19 17:02:38 +03:00
SMKRV 4ddb0dc977 Translation fixes 2024-11-19 16:59:42 +03:00
SMKRV 24dc4ac4d4 Hotfix 2024-11-19 16:53:27 +03:00
SMKRV 976f4f16a3 translation fixes 2024-11-19 16:40:56 +03:00
SMKRV 2bdef2b494 Main changes:
Created global SSL_CONTEXT at module level
Removed blocking create_default_context calls from async functions
Optimized aiohttp.ClientSession handling:
Using single connector with SSL context
Session is created once for all requests in validate_api_connection
Improved resource management:
Automatic session closure using context managers
More efficient connection handling
These changes should eliminate the blocking call warning and improve
overall code performance.
2024-11-19 16:33:30 +03:00
SMKRV 42324a793b bufix 2024-11-19 16:25:44 +03:00
SMKRV 30aa894634 Release v1.0.6 2024-11-19 15:10:06 +03:00
11 changed files with 488 additions and 481 deletions
+156 -19
View File
@@ -1,18 +1,35 @@
"""The HA Text AI integration.""" """The HA Text AI integration."""
import logging import logging
from typing import Any from typing import Any, Dict, Optional
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, PLATFORMS
from .coordinator import HATextAICoordinator from .coordinator import HATextAICoordinator
from .const import (
DOMAIN,
PLATFORMS,
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,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the HA Text AI component.""" """Set up the HA Text AI component."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
@@ -26,11 +43,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = HATextAICoordinator( coordinator = HATextAICoordinator(
hass, hass,
api_key=entry.data[CONF_API_KEY], api_key=entry.data[CONF_API_KEY],
endpoint=entry.data.get("api_endpoint", "https://api.openai.com/v1"), endpoint=entry.data.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT),
model=entry.data.get("model", "gpt-3.5-turbo"), model=entry.data.get(CONF_MODEL, DEFAULT_MODEL),
temperature=entry.data.get("temperature", 0.7), temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
max_tokens=entry.data.get("max_tokens", 1000), max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
request_interval=entry.data.get("request_interval", 1.0), request_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
session=session, session=session,
) )
@@ -38,30 +55,145 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
except Exception as refresh_ex: except Exception as refresh_ex:
_LOGGER.error("Failed to refresh coordinator: %s", str(refresh_ex)) _LOGGER.error("Failed to refresh coordinator: %s", str(refresh_ex))
return False raise ConfigEntryNotReady from refresh_ex
if not coordinator.last_update_success: if not coordinator.last_update_success:
_LOGGER.error("Failed to communicate with OpenAI API") raise ConfigEntryNotReady("Failed to communicate with OpenAI API")
return False
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
try: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as setup_ex: async def async_ask_question(call: ServiceCall) -> None:
_LOGGER.error("Failed to setup platforms: %s", str(setup_ex)) """Handle the ask_question service call."""
return False question = call.data.get("question", "")
if not question:
_LOGGER.error("No question provided in service call")
return
# Собираем все опциональные параметры
request_params = {}
# Обработка system_prompt
system_prompt = call.data.get("system_prompt")
if system_prompt is not None:
request_params["system_prompt"] = system_prompt
# Обработка model
model = call.data.get("model")
if model is not None:
request_params["model"] = model
# Обработка temperature
temperature = call.data.get("temperature")
if temperature is not None:
try:
request_params["temperature"] = float(temperature)
except ValueError:
_LOGGER.error("Invalid temperature value: %s", temperature)
return
# Обработка max_tokens
max_tokens = call.data.get("max_tokens")
if max_tokens is not None:
try:
request_params["max_tokens"] = int(max_tokens)
except ValueError:
_LOGGER.error("Invalid max_tokens value: %s", max_tokens)
return
try:
await coordinator.async_ask_question(question, **request_params)
except Exception as err:
_LOGGER.error("Error asking question: %s", str(err))
async def async_clear_history(call: ServiceCall) -> None:
"""Handle the clear_history service call."""
try:
coordinator._responses.clear()
await coordinator.async_refresh()
_LOGGER.info("History cleared successfully")
except Exception as err:
_LOGGER.error("Error clearing history: %s", str(err))
async def async_get_history(call: ServiceCall) -> dict:
"""Handle the get_history service call."""
try:
limit = call.data.get("limit", 10)
filter_model = call.data.get("filter_model", "")
responses = coordinator._responses
# Применяем фильтрацию по модели
if filter_model:
filtered_responses = {
k: v for k, v in responses.items()
if v.get("model") == filter_model
}
else:
filtered_responses = responses.copy()
# Сортируем по времени и ограничиваем количество
sorted_responses = dict(
sorted(
filtered_responses.items(),
key=lambda x: x[1]["timestamp"],
reverse=True
)[:limit]
)
return sorted_responses
except Exception as err:
_LOGGER.error("Error getting history: %s", str(err))
return {}
async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle the set_system_prompt service call."""
prompt = call.data.get("prompt", "")
if prompt:
try:
coordinator.system_prompt = prompt
_LOGGER.info("System prompt updated successfully")
except Exception as err:
_LOGGER.error("Error setting system prompt: %s", str(err))
else:
_LOGGER.error("No prompt provided in service call")
# Регистрация сервисов
hass.services.async_register(
DOMAIN,
"ask_question",
async_ask_question
)
hass.services.async_register(
DOMAIN,
"clear_history",
async_clear_history
)
hass.services.async_register(
DOMAIN,
"get_history",
async_get_history
)
hass.services.async_register(
DOMAIN,
"set_system_prompt",
async_set_system_prompt
)
_LOGGER.info( _LOGGER.info(
"Successfully set up HA Text AI with model: %s", "Successfully set up HA Text AI with model: %s",
entry.data.get("model", "gpt-3.5-turbo") entry.data.get(CONF_MODEL, DEFAULT_MODEL)
) )
return True return True
except Exception as ex: except Exception as ex:
_LOGGER.exception("Unexpected error setting up entry: %s", str(ex)) _LOGGER.exception("Unexpected error setting up entry: %s", str(ex))
return False raise ConfigEntryNotReady from ex
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
@@ -69,6 +201,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if entry.entry_id not in hass.data.get(DOMAIN, {}): if entry.entry_id not in hass.data.get(DOMAIN, {}):
return True return True
# Удаляем все сервисы при выгрузке интеграции
services = ["ask_question", "clear_history", "get_history", "set_system_prompt"]
for service in services:
hass.services.async_remove(DOMAIN, service)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: if unload_ok:
coordinator = hass.data[DOMAIN].pop(entry.entry_id) coordinator = hass.data[DOMAIN].pop(entry.entry_id)
@@ -86,4 +223,4 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
await async_unload_entry(hass, entry) await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry) await async_setup_entry(hass, entry)
except Exception as ex: except Exception as ex:
_LOGGER.exception("Error reloading entry: %s", str(ex)) _LOGGER.exception("Error reloading entry: %s", str(ex)) # убрано лишнее двоеточие
+34 -55
View File
@@ -32,6 +32,9 @@ from .const import (
import logging import logging
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Create SSL context at module level
SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
STEP_USER_DATA_SCHEMA = vol.Schema({ STEP_USER_DATA_SCHEMA = vol.Schema({
vol.Required(CONF_API_KEY): str, vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str, vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str,
@@ -40,50 +43,25 @@ STEP_USER_DATA_SCHEMA = vol.Schema({
default=DEFAULT_TEMPERATURE default=DEFAULT_TEMPERATURE
): vol.All( ): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=0, max=2), vol.Range(min=0, max=2)
msg="Temperature must be between 0 and 2"
), ),
vol.Optional( vol.Optional(
CONF_MAX_TOKENS, CONF_MAX_TOKENS,
default=DEFAULT_MAX_TOKENS default=DEFAULT_MAX_TOKENS
): vol.All( ): vol.All(
vol.Coerce(int), vol.Coerce(int),
vol.Range(min=1, max=4096), vol.Range(min=1, max=4096)
msg="Max tokens must be between 1 and 4096"
),
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): vol.All(
str,
vol.Url(), # Заменено с URL на Url
msg="Must be a valid URL"
), ),
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): str,
vol.Optional( vol.Optional(
CONF_REQUEST_INTERVAL, CONF_REQUEST_INTERVAL,
default=DEFAULT_REQUEST_INTERVAL default=DEFAULT_REQUEST_INTERVAL
): vol.All( ): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=0.1), vol.Range(min=0.1)
msg="Request interval must be at least 0.1 seconds"
), ),
}) })
async def validate_endpoint(endpoint: str) -> Tuple[bool, str]:
"""Validate API endpoint accessibility."""
try:
parsed_url = urlparse(endpoint)
if parsed_url.scheme not in ('http', 'https'):
return False, "invalid_endpoint_scheme"
ssl_context = ssl.create_default_context(cafile=certifi.where())
async with timeout(5):
async with aiohttp.ClientSession() as session:
async with session.get(endpoint, ssl=ssl_context) as response:
if response.status != 200:
return False, "endpoint_not_available"
return True, ""
except Exception as e:
_LOGGER.error("Error validating endpoint: %s", str(e))
return False, "endpoint_error"
async def validate_api_connection( async def validate_api_connection(
api_key: str, api_key: str,
endpoint: str, endpoint: str,
@@ -91,33 +69,17 @@ async def validate_api_connection(
retry_count: int = 3, retry_count: int = 3,
retry_delay: float = 1.0 retry_delay: float = 1.0
) -> Tuple[bool, str, list]: ) -> Tuple[bool, str, list]:
"""Validate API connection with improved retry logic.""" """Validate API connection with retry logic."""
ssl_context = ssl.create_default_context(cafile=certifi.where())
# Validate endpoint first
endpoint_valid, endpoint_error = await validate_endpoint(endpoint)
if not endpoint_valid:
return False, endpoint_error, []
for attempt in range(retry_count): for attempt in range(retry_count):
try: try:
async with timeout(10): async with timeout(10):
client = AsyncOpenAI( client = AsyncOpenAI(
api_key=api_key, api_key=api_key,
base_url=endpoint, base_url=endpoint,
http_client=aiohttp.ClientSession(
connector=aiohttp.TCPConnector(
ssl=ssl_context,
enable_cleanup_closed=True
)
)
) )
try: models = await client.models.list()
models = await client.models.list() model_ids = [model.id for model in models.data]
model_ids = [model.id for model in models.data]
finally:
await client.http_client.close()
if model not in model_ids: if model not in model_ids:
_LOGGER.warning( _LOGGER.warning(
@@ -172,12 +134,32 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
# Validate URL format
endpoint = user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT)
try:
result = urlparse(endpoint)
if not all([result.scheme, result.netloc]):
errors["base"] = "invalid_url_format"
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors
)
except Exception as e:
_LOGGER.error("URL parsing error: %s", str(e))
errors["base"] = "invalid_url_format"
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors
)
# Validate input data # Validate input data
user_input = STEP_USER_DATA_SCHEMA(user_input) user_input = STEP_USER_DATA_SCHEMA(user_input)
is_valid, error_code, available_models = await validate_api_connection( is_valid, error_code, available_models = await validate_api_connection(
user_input[CONF_API_KEY], user_input[CONF_API_KEY],
user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT), endpoint,
user_input[CONF_MODEL] user_input[CONF_MODEL]
) )
@@ -244,8 +226,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
description={"suggested_value": DEFAULT_TEMPERATURE}, description={"suggested_value": DEFAULT_TEMPERATURE},
): vol.All( ): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=0, max=2), vol.Range(min=0, max=2)
msg="Temperature must be between 0 and 2"
), ),
vol.Optional( vol.Optional(
CONF_MAX_TOKENS, CONF_MAX_TOKENS,
@@ -255,8 +236,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
description={"suggested_value": DEFAULT_MAX_TOKENS}, description={"suggested_value": DEFAULT_MAX_TOKENS},
): vol.All( ): vol.All(
vol.Coerce(int), vol.Coerce(int),
vol.Range(min=1, max=4096), vol.Range(min=1, max=4096)
msg="Max tokens must be between 1 and 4096"
), ),
vol.Optional( vol.Optional(
CONF_REQUEST_INTERVAL, CONF_REQUEST_INTERVAL,
@@ -266,8 +246,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
description={"suggested_value": DEFAULT_REQUEST_INTERVAL}, description={"suggested_value": DEFAULT_REQUEST_INTERVAL},
): vol.All( ): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=0.1), vol.Range(min=0.1)
msg="Request interval must be at least 0.1 seconds"
), ),
}) })
+54 -13
View File
@@ -71,14 +71,27 @@ class HATextAICoordinator(DataUpdateCoordinator):
try: try:
async with async_timeout.timeout(30): async with async_timeout.timeout(30):
question = await self._question_queue.get() question_data = await self._question_queue.get()
question = question_data["question"]
params = question_data["params"]
try: try:
response_content = await self._make_api_call(question) response_content = await self._make_api_call(
question,
model=params.get("model"),
temperature=params.get("temperature"),
max_tokens=params.get("max_tokens"),
system_prompt=params.get("system_prompt")
)
self._responses[question] = { self._responses[question] = {
"question": question, "question": question,
"response": response_content, "response": response_content,
"error": None, "error": None,
"timestamp": self.hass.loop.time() "timestamp": self.hass.loop.time(),
"model": params.get("model", self.model),
"temperature": params.get("temperature", self.temperature),
"max_tokens": params.get("max_tokens", self.max_tokens)
} }
self._error_count = 0 self._error_count = 0
self._is_ready = True self._is_ready = True
@@ -113,7 +126,10 @@ class HATextAICoordinator(DataUpdateCoordinator):
"question": question, "question": question,
"response": None, "response": None,
"error": error_msg, "error": error_msg,
"timestamp": self.hass.loop.time() "timestamp": self.hass.loop.time(),
"model": self.model,
"temperature": self.temperature,
"max_tokens": self.max_tokens
} }
_LOGGER.error("API error (%s): %s", type(error).__name__, error_msg) _LOGGER.error("API error (%s): %s", type(error).__name__, error_msg)
@@ -136,19 +152,27 @@ class HATextAICoordinator(DataUpdateCoordinator):
except Exception as err: except Exception as err:
_LOGGER.error("Error clearing question queue: %s", err) _LOGGER.error("Error clearing question queue: %s", err)
async def _make_api_call(self, question: str) -> str: async def _make_api_call(
self,
question: str,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None
) -> str:
"""Make API call to OpenAI.""" """Make API call to OpenAI."""
try: try:
messages = [] messages = []
if self.system_prompt: current_system_prompt = system_prompt if system_prompt is not None else self.system_prompt
messages.append({"role": "system", "content": self.system_prompt}) if current_system_prompt:
messages.append({"role": "system", "content": current_system_prompt})
messages.append({"role": "user", "content": question}) messages.append({"role": "user", "content": question})
completion = await self.client.chat.completions.create( completion = await self.client.chat.completions.create(
model=self.model, model=model or self.model,
messages=messages, messages=messages,
temperature=self.temperature, temperature=temperature if temperature is not None else self.temperature,
max_tokens=self.max_tokens, max_tokens=max_tokens if max_tokens is not None else self.max_tokens,
) )
return completion.choices[0].message.content return completion.choices[0].message.content
@@ -156,13 +180,30 @@ class HATextAICoordinator(DataUpdateCoordinator):
_LOGGER.error("Error in API call: %s", err) _LOGGER.error("Error in API call: %s", err)
raise raise
async def async_ask_question(self, question: str) -> None: async def async_ask_question(
"""Add question to queue.""" self,
question: str,
system_prompt: Optional[str] = None,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None
) -> None:
"""Add question to queue with optional parameters."""
if not self._is_ready and self._error_count >= self._MAX_ERRORS: if not self._is_ready and self._error_count >= self._MAX_ERRORS:
_LOGGER.warning("Coordinator is not ready due to previous errors") _LOGGER.warning("Coordinator is not ready due to previous errors")
return return
await self._question_queue.put(question) question_data = {
"question": question,
"params": {
"system_prompt": system_prompt,
"model": model,
"temperature": temperature,
"max_tokens": max_tokens
}
}
await self._question_queue.put(question_data)
await self.async_refresh() await self.async_refresh()
async def async_shutdown(self) -> None: async def async_shutdown(self) -> None:
+1 -1
View File
@@ -9,6 +9,6 @@
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues", "issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"requirements": ["openai>=1.0.0"], "requirements": ["openai>=1.0.0"],
"ssdp": [], "ssdp": [],
"version": "1.0.5", "version": "1.1.0",
"zeroconf": [] "zeroconf": []
} }
+25 -5
View File
@@ -117,21 +117,35 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
return attributes return attributes
try: try:
history = list(self.coordinator.data.items()) # Получаем историю ответов
history = list(self.coordinator._responses.items())
if history: if history:
last_question, last_data = history[-1] last_question, last_data = history[-1]
# Handle different response formats # Обработка формата ответа
if isinstance(last_data, dict): if isinstance(last_data, dict):
last_response = last_data.get("response", "") last_response = last_data.get("response", "")
last_updated = last_data.get("timestamp", self.coordinator.last_update_success_time) last_updated = last_data.get("timestamp", self.coordinator.last_update_success_time)
response_time = last_data.get("response_time") response_time = last_data.get("response_time")
# Добавляем новые метаданные
model = last_data.get("model", self.coordinator.model)
temperature = last_data.get("temperature", self.coordinator.temperature)
max_tokens = last_data.get("max_tokens", self.coordinator.max_tokens)
error = last_data.get("error")
if error:
self._last_error = error
self._state = STATE_ERROR
else: else:
last_response = str(last_data) last_response = str(last_data)
last_updated = self.coordinator.last_update_success_time last_updated = self.coordinator.last_update_success_time
response_time = None response_time = None
model = self.coordinator.model
temperature = self.coordinator.temperature
max_tokens = self.coordinator.max_tokens
# Convert timestamp to local time if needed # Конвертация времени в локальный формат
if isinstance(last_updated, datetime): if isinstance(last_updated, datetime):
last_updated = dt_util.as_local(last_updated) last_updated = dt_util.as_local(last_updated)
@@ -140,6 +154,9 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
ATTR_RESPONSE: last_response, ATTR_RESPONSE: last_response,
ATTR_LAST_UPDATED: last_updated, ATTR_LAST_UPDATED: last_updated,
ATTR_TOTAL_RESPONSES: len(history), ATTR_TOTAL_RESPONSES: len(history),
ATTR_MODEL: model,
ATTR_TEMPERATURE: temperature,
ATTR_MAX_TOKENS: max_tokens,
}) })
if response_time is not None: if response_time is not None:
@@ -169,7 +186,10 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
try: try:
if self.coordinator.data: if self.coordinator.data:
self._state = STATE_READY if self.coordinator._is_ready:
self._state = STATE_READY
else:
self._state = STATE_DISCONNECTED
else: else:
self._state = STATE_DISCONNECTED self._state = STATE_DISCONNECTED
except Exception as err: except Exception as err:
@@ -178,4 +198,4 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
self._last_error = str(err) self._last_error = str(err)
self._state = STATE_ERROR self._state = STATE_ERROR
self.async_write_ha_state() self.async_write_ha_state() # Исправлено лишнее двоеточие
+20 -15
View File
@@ -28,6 +28,7 @@ ask_question:
name: Model name: Model
description: >- description: >-
Select an AI model to use (optional, overrides default setting). Select an AI model to use (optional, overrides default setting).
You can choose from predefined models or enter your own model identifier.
Different models have different capabilities and token limits. Different models have different capabilities and token limits.
Note: More capable models may have longer response times and higher API costs. Note: More capable models may have longer response times and higher API costs.
required: false required: false
@@ -35,6 +36,7 @@ ask_question:
default: "gpt-3.5-turbo" default: "gpt-3.5-turbo"
selector: selector:
select: select:
custom_value: true
options: options:
- label: "GPT-3.5 Turbo (Fast & Efficient)" - label: "GPT-3.5 Turbo (Fast & Efficient)"
value: "gpt-3.5-turbo" value: "gpt-3.5-turbo"
@@ -46,6 +48,8 @@ ask_question:
value: "gpt-4-32k" value: "gpt-4-32k"
- label: "GPT-4 Turbo (Latest)" - label: "GPT-4 Turbo (Latest)"
value: "gpt-4-1106-preview" value: "gpt-4-1106-preview"
- label: "Claude-3 Sonnet"
value: "claude-3-sonnet"
mode: dropdown mode: dropdown
temperature: temperature:
@@ -84,6 +88,17 @@ ask_question:
step: 1 step: 1
mode: box mode: box
system_prompt:
name: System Prompt
description: >-
Optional system prompt to set context for this specific question.
This will temporarily override the default system prompt.
required: false
example: "You are a home automation expert focused on energy efficiency"
selector:
text:
multiline: true
clear_history: clear_history:
name: Clear History name: Clear History
description: >- description: >-
@@ -121,6 +136,7 @@ get_history:
required: false required: false
selector: selector:
select: select:
custom_value: true
options: options:
- label: "All Models" - label: "All Models"
value: "" value: ""
@@ -128,14 +144,15 @@ get_history:
value: "gpt-3.5-turbo" value: "gpt-3.5-turbo"
- label: "GPT-4" - label: "GPT-4"
value: "gpt-4" value: "gpt-4"
- label: "Claude-3 Sonnet"
value: "claude-3-sonnet"
mode: dropdown mode: dropdown
set_system_prompt: set_system_prompt:
name: Set System Prompt name: Set System Prompt
description: >- description: >-
Configure the AI's behavior by setting a system prompt. Set default system behavior instructions for all future conversations.
This affects how the AI interprets and responds to all future questions. This defines how the AI should behave and respond to questions.
The prompt will persist until changed or cleared.
fields: fields:
prompt: prompt:
name: System Prompt name: System Prompt
@@ -156,15 +173,3 @@ set_system_prompt:
selector: selector:
text: text:
multiline: true multiline: true
type: text
max_length: 1000
clear_prompt:
name: Clear Existing Prompt
description: >-
Set to true to remove the current system prompt before applying the new one.
This ensures no conflicting instructions remain.
required: false
default: false
selector:
boolean: {}
+90 -186
View File
@@ -1,192 +1,96 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "user": {
"title": "Set up HA Text AI", "title": "Set up HA Text AI",
"description": "Configure your OpenAI integration for smart home interactions. You'll need an OpenAI API key from platform.openai.com to proceed.", "description": "Configure your AI integration (OpenAI, Anthropic, etc.)",
"data": { "data": {
"api_key": { "api_key": "Your API key",
"name": "OpenAI API Key", "model": "AI model to use for responses (e.g., gpt-3.5-turbo, claude-3-sonnet)",
"description": "Your OpenAI API key from platform.openai.com. Keep this secure and never share it." "temperature": "Temperature for response generation (0-2)",
}, "max_tokens": "Maximum tokens in response (1-4096)",
"model": { "api_endpoint": "API endpoint URL (optional, for custom endpoints)",
"name": "AI Model", "request_interval": "Minimum time between API requests (seconds)",
"description": "Select the AI model to use. GPT-3.5-Turbo is recommended for most uses as it offers the best balance of capabilities and cost." "system_prompt": "Default system prompt for all conversations"
}, }
"temperature": { }
"name": "Temperature", },
"description": "Controls response creativity (0-2). Low values (0.1-0.3) for focused responses, high values (0.8-2.0) for creative ones." "error": {
}, "invalid_api_key": "Invalid API key - please check your credentials",
"max_tokens": { "cannot_connect": "Failed to connect to API - check endpoint and network",
"name": "Max Tokens", "invalid_model": "Selected model is not available or invalid",
"description": "Maximum length of responses. Higher values allow longer responses but consume more API tokens. Recommended: 512-1024." "rate_limit": "API rate limit exceeded - please wait",
}, "unknown": "Unexpected error occurred - check logs for details"
"api_endpoint": {
"name": "API Endpoint",
"description": "OpenAI API endpoint URL. Leave default unless using a custom endpoint or proxy."
},
"request_interval": {
"name": "Request Interval",
"description": "Minimum time between API requests in seconds. Increase if experiencing rate limits."
}
} }
}
}, },
"error": { "options": {
"invalid_auth": "Invalid API key. Please check your OpenAI API key and try again.", "step": {
"cannot_connect": "Failed to connect to API. Please check your internet connection and API endpoint.", "init": {
"unknown": "Unexpected error occurred. Please check the logs for more details.", "title": "HA Text AI Options",
"already_exists": "This API key is already configured in another integration.", "data": {
"invalid_model": "Selected model is not available. Please choose a different model.", "model": "AI model selection",
"rate_limit": "API rate limit exceeded. Please try again later or increase the request interval.", "temperature": "Response temperature (0-2)",
"context_length": "Input too long for selected model. Try reducing max tokens or using a model with larger context.", "max_tokens": "Maximum response length",
"api_error": "OpenAI API error. Please check the logs for details.", "request_interval": "Time between requests (seconds)",
"timeout": "API response timeout. Request took too long to complete.", "system_prompt": "Default system instructions"
"queue_full": "Request queue is full. Please try again later." }
}
}
}, },
"abort": { "services": {
"already_configured": "This OpenAI integration is already configured", "ask_question": {
"auth_failed": "Authentication failed. Please verify your API key.", "name": "Ask Question",
"invalid_endpoint": "Invalid API endpoint URL provided" "description": "Send a question to the AI model and receive a response",
"fields": {
"question": {
"name": "Question",
"description": "Your question or prompt for the AI"
},
"system_prompt": {
"name": "System Prompt",
"description": "Optional system prompt to override default for this question"
},
"model": {
"name": "Model",
"description": "Optional AI model to use for this question"
},
"temperature": {
"name": "Temperature",
"description": "Optional temperature setting for this question (0-2)"
},
"max_tokens": {
"name": "Max Tokens",
"description": "Optional maximum token limit for this response"
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Delete all stored conversation history"
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history with metadata",
"fields": {
"limit": {
"name": "Limit",
"description": "Maximum number of conversations to return"
},
"filter_model": {
"name": "Filter Model",
"description": "Optional filter to show only specific model responses"
}
}
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Set default system behavior instructions",
"fields": {
"prompt": {
"name": "System Prompt",
"description": "Instructions defining AI behavior and response style"
}
}
}
} }
},
"options": {
"step": {
"init": {
"title": "HA Text AI Options",
"description": "Adjust your OpenAI integration settings. Changes will apply to future requests only.",
"data": {
"temperature": {
"name": "Temperature",
"description": "Controls response creativity (0-2). Low values for focused responses, high for creative ones."
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum length of responses. Higher values allow longer responses but consume more API tokens."
},
"request_interval": {
"name": "Request Interval",
"description": "Minimum time between API requests in seconds. Increase if experiencing rate limits."
}
}
}
}
},
"entity": {
"sensor": {
"last_response": {
"name": "Last Response",
"state_attributes": {
"last_updated": {
"name": "Last Updated",
"description": "Timestamp of the last AI response"
},
"question": {
"name": "Last Question",
"description": "Most recent question asked"
},
"response": {
"name": "AI Response",
"description": "Latest response from the AI"
},
"model": {
"name": "Current Model",
"description": "AI model currently in use"
},
"temperature": {
"name": "Temperature Setting",
"description": "Current temperature parameter"
},
"max_tokens": {
"name": "Max Tokens Setting",
"description": "Current maximum tokens limit"
},
"total_responses": {
"name": "Total Responses",
"description": "Number of responses since last reset"
},
"system_prompt": {
"name": "System Prompt",
"description": "Current system instructions for the AI"
},
"response_time": {
"name": "Response Time",
"description": "Time taken to generate last response"
},
"queue_size": {
"name": "Queue Size",
"description": "Current size of request queue"
},
"api_status": {
"name": "API Status",
"description": "Current API connection status"
},
"error_count": {
"name": "Error Count",
"description": "Number of errors since last reset"
},
"last_error": {
"name": "Last Error",
"description": "Description of the last error encountered"
}
}
}
}
},
"services": {
"ask_question": {
"name": "Ask Question",
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in conversation history.",
"fields": {
"question": {
"name": "Question",
"description": "Your question or prompt for the AI. Be specific for better results."
},
"model": {
"name": "Model",
"description": "AI model to use (optional, overrides default settings)."
},
"temperature": {
"name": "Temperature",
"description": "Response creativity level (0-2, optional)."
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum response length (optional)."
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Delete all stored conversation history. This action cannot be undone."
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history, including questions, responses, and timestamps.",
"fields": {
"limit": {
"name": "Limit",
"description": "Number of recent conversations to return (default 10)."
},
"filter_model": {
"name": "Filter by Model",
"description": "Retrieve only conversations using a specific AI model."
}
}
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Configure AI behavior by setting a system prompt.",
"fields": {
"prompt": {
"name": "Prompt",
"description": "Instructions defining AI behavior and response style."
},
"clear_prompt": {
"name": "Clear Prompt",
"description": "Remove current system prompt before setting new one."
}
}
}
}
} }
+90 -186
View File
@@ -1,192 +1,96 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "user": {
"title": "Настройка HA Text AI", "title": "Настройка HA Text AI",
"description": "Настройте интеграцию OpenAI для умного дома. Требуется API ключ OpenAI. Подробнее о получении ключа на platform.openai.com", "description": "Настройка интеграции с AI-сервисами (OpenAI, Anthropic и др.)",
"data": { "data": {
"api_key": { "api_key": "Ваш API-ключ",
"name": "API ключ OpenAI", "model": "Модель AI для ответов (например, gpt-3.5-turbo, claude-3-sonnet)",
"description": "Ваш API ключ с platform.openai.com. Храните его в безопасности." "temperature": "Температура генерации ответов (0-2)",
}, "max_tokens": "Максимальное количество токенов в ответе (1-4096)",
"model": { "api_endpoint": "URL конечной точки API (необязательно, для пользовательских endpoint)",
"name": "AI Модель", "request_interval": "Минимальный интервал между запросами к API (секунды)",
"description": "Выберите модель AI. GPT-3.5-Turbo рекомендуется для большинства задач как оптимальное сочетание возможностей и стоимости." "system_prompt": "Системная инструкция по умолчанию для всех диалогов"
}, }
"temperature": { }
"name": "Температура", },
"description": "Контролирует креативность ответов (0-2). Низкие значения (0.1-0.3) для точных ответов, высокие (0.8-2.0) для творческих." "error": {
}, "invalid_api_key": "Недействительный API-ключ - проверьте учетные данные",
"max_tokens": { "cannot_connect": "Не удалось подключиться к API - проверьте endpoint и сеть",
"name": "Максимум токенов", "invalid_model": "Выбранная модель недоступна или некорректна",
"description": "Максимальная длина ответов. Больше токенов = длиннее ответы, но выше расход API токенов. Рекомендуется: 512-1024." "rate_limit": "Превышен лимит запросов API - пожалуйста, подождите",
}, "unknown": "Произошла непредвиденная ошибка - проверьте логи"
"api_endpoint": {
"name": "API Endpoint",
"description": "URL API OpenAI. Оставьте значение по умолчанию, если не используете собственный endpoint."
},
"request_interval": {
"name": "Интервал запросов",
"description": "Минимальное время между API запросами в секундах. Увеличьте при превышении лимитов запросов."
}
} }
}
}, },
"error": { "options": {
"invalid_auth": "Неверный API ключ. Проверьте ключ OpenAI и попробуйте снова.", "step": {
"cannot_connect": "Не удалось подключиться к API. Проверьте подключение к интернету и endpoint.", "init": {
"unknown": "Неожиданная ошибка. Проверьте логи для подробностей.", "title": "Настройки HA Text AI",
"already_exists": "Этот API ключ уже используется в другой интеграции.", "data": {
"invalid_model": "Выбранная модель недоступна. Выберите другую модель.", "model": "Выбор модели AI",
"rate_limit": "Превышен лимит API запросов. Попробуйте позже или увеличьте интервал запросов.", "temperature": "Температура ответов (0-2)",
"context_length": "Входные данные слишком длинные для выбранной модели. Уменьшите max_tokens или используйте модель с большим контекстом.", "max_tokens": "Максимальная длина ответа",
"api_error": "Ошибка API OpenAI. Проверьте логи для подробностей.", "request_interval": "Интервал между запросами (секунды)",
"timeout": "Превышено время ожидания ответа от API.", "system_prompt": "Системные инструкции по умолчанию"
"queue_full": "Очередь запросов переполнена. Попробуйте позже." }
}
}
}, },
"abort": { "services": {
"already_configured": "Эта интеграция OpenAI уже настроена", "ask_question": {
"auth_failed": "Ошибка аутентификации. Проверьте API ключ.", "name": "Задать вопрос",
"invalid_endpoint": "Указан неверный URL API endpoint" "description": "Отправить вопрос модели AI и получить ответ",
"fields": {
"question": {
"name": "Вопрос",
"description": "Ваш вопрос или запрос для AI"
},
"system_prompt": {
"name": "Системная инструкция",
"description": "Необязательная системная инструкция, заменяющая инструкцию по умолчанию для этого вопроса"
},
"model": {
"name": "Модель",
"description": "Необязательная модель AI для этого вопроса"
},
"temperature": {
"name": "Температура",
"description": "Необязательная настройка температуры для этого вопроса (0-2)"
},
"max_tokens": {
"name": "Макс. токенов",
"description": "Необязательное ограничение количества токенов для этого ответа"
}
}
},
"clear_history": {
"name": "Очистить историю",
"description": "Удалить всю сохраненную историю диалогов"
},
"get_history": {
"name": "Получить историю",
"description": "Получить историю диалогов с метаданными",
"fields": {
"limit": {
"name": "Лимит",
"description": "Максимальное количество диалогов для возврата"
},
"filter_model": {
"name": "Фильтр по модели",
"description": "Необязательный фильтр для показа ответов только от определенной модели"
}
}
},
"set_system_prompt": {
"name": "Установить системную инструкцию",
"description": "Задать системные инструкции по умолчанию",
"fields": {
"prompt": {
"name": "Системная инструкция",
"description": "Инструкции, определяющие поведение AI и стиль ответов"
}
}
}
} }
},
"options": {
"step": {
"init": {
"title": "Настройки HA Text AI",
"description": "Измените настройки интеграции OpenAI. Изменения применятся к будущим запросам.",
"data": {
"temperature": {
"name": "Температура",
"description": "Контролирует креативность ответов (0-2). Низкие значения для точных ответов, высокие для творческих."
},
"max_tokens": {
"name": "Максимум токенов",
"description": "Максимальная длина ответов. Больше токенов = длиннее ответы, но выше расход API токенов."
},
"request_interval": {
"name": "Интервал запросов",
"description": "Минимальное время между API запросами в секундах. Увеличьте при превышении лимитов."
}
}
}
}
},
"entity": {
"sensor": {
"last_response": {
"name": "Последний ответ",
"state_attributes": {
"last_updated": {
"name": "Последнее обновление",
"description": "Время последнего ответа AI"
},
"question": {
"name": "Последний вопрос",
"description": "Последний заданный вопрос"
},
"response": {
"name": "Ответ AI",
"description": "Последний ответ от AI"
},
"model": {
"name": "Текущая модель",
"description": "Используемая модель AI"
},
"temperature": {
"name": "Настройка температуры",
"description": "Текущий параметр температуры"
},
"max_tokens": {
"name": "Лимит токенов",
"description": "Текущий лимит максимальных токенов"
},
"total_responses": {
"name": "Всего ответов",
"description": "Количество ответов с последнего сброса"
},
"system_prompt": {
"name": "Системный промпт",
"description": "Текущие системные инструкции для AI"
},
"response_time": {
"name": "Время ответа",
"description": "Время генерации последнего ответа"
},
"queue_size": {
"name": "Размер очереди",
"description": "Текущий размер очереди запросов"
},
"api_status": {
"name": "Статус API",
"description": "Текущий статус подключения к API"
},
"error_count": {
"name": "Счётчик ошибок",
"description": "Количество ошибок с последнего сброса"
},
"last_error": {
"name": "Последняя ошибка",
"description": "Описание последней возникшей ошибки"
}
}
}
}
},
"services": {
"ask_question": {
"name": "Задать вопрос",
"description": "Отправить вопрос модели AI и получить подробный ответ. Ответ сохраняется в истории.",
"fields": {
"question": {
"name": "Вопрос",
"description": "Ваш вопрос или запрос для AI. Будьте конкретны для лучших результатов."
},
"model": {
"name": "Модель",
"description": "Модель AI для использования (необязательно, переопределяет настройки по умолчанию)."
},
"temperature": {
"name": "Температура",
"description": "Уровень креативности ответа (0-2, необязательно)."
},
"max_tokens": {
"name": "Максимум токенов",
"description": "Максимальная длина ответа (необязательно)."
}
}
},
"clear_history": {
"name": "Очистить историю",
"description": "Удалить всю историю разговоров. Это действие нельзя отменить."
},
"get_history": {
"name": "Получить историю",
"description": "Получить историю разговоров, включая вопросы, ответы и временные метки.",
"fields": {
"limit": {
"name": "Лимит",
"description": "Количество последних разговоров для получения (по умолчанию 10)."
},
"filter_model": {
"name": "Фильтр по модели",
"description": "Получить только разговоры с определённой моделью AI."
}
}
},
"set_system_prompt": {
"name": "Установить системный промпт",
"description": "Настроить поведение AI, установив системный промпт.",
"fields": {
"prompt": {
"name": "Промпт",
"description": "Инструкции, определяющие поведение и стиль ответов AI."
},
"clear_prompt": {
"name": "Очистить промпт",
"description": "Удалить текущий системный промпт перед установкой нового."
}
}
}
}
} }
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -4,6 +4,6 @@
"domains": ["sensor"], "domains": ["sensor"],
"homeassistant": "2024.11.0", "homeassistant": "2024.11.0",
"icon": "mdi:brain", "icon": "mdi:brain",
"version": "1.0.5", "version": "1.1.0",
"documentation": "https://github.com/smkrv/ha-text-ai" "documentation": "https://github.com/smkrv/ha-text-ai"
} }
+17
View File
@@ -0,0 +1,17 @@
```
ha-text-ai/
├── custom_components/
│ └── ha_text_ai/
│ ├── __init__.py
│ ├── config_flow.py
│ ├── coordinator.py
│ ├── manifest.json
│ ├── sensor.py
│ ├── services.yaml
│ └── const.py
└── strings/
├── en.json
└── ru.json
```