Release v2.0.0

This commit is contained in:
SMKRV
2024-11-22 01:34:47 +03:00
parent 9341b02f4b
commit 53b15fa74c
8 changed files with 235 additions and 649 deletions
+27 -31
View File
@@ -24,13 +24,15 @@ from .const import (
CONF_MAX_TOKENS,
CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL,
CONF_API_PROVIDER,
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC,
DEFAULT_MODEL,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS,
DEFAULT_API_ENDPOINT,
DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT,
DEFAULT_REQUEST_INTERVAL,
API_VERSION,
API_MODELS_PATH,
API_TIMEOUT,
API_RETRY_COUNT,
API_BACKOFF_FACTOR,
@@ -131,27 +133,17 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
return True
async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: bool = False) -> bool:
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
"""Check API availability for different providers."""
try:
# Определяем, является ли это VSE GPT endpoint
is_vsegpt = "vsegpt" in endpoint.lower()
if is_vsegpt:
# Для VSE GPT используем специальный endpoint
check_url = f"{endpoint.rstrip('/')}/v1/models"
headers["Authorization"] = f"Bearer {headers.get('x-api-key', '')}"
elif is_anthropic:
if provider == API_PROVIDER_ANTHROPIC:
check_url = f"{endpoint}/v1/models"
else:
check_url = f"{endpoint}/{API_VERSION}/{API_MODELS_PATH}"
else: # OpenAI
check_url = f"{endpoint}/models"
async with timeout(API_TIMEOUT):
async with session.get(check_url, headers=headers) as response:
if response.status == 200:
return True
elif response.status == 404 and is_vsegpt:
# VSE GPT может возвращать 404 для /models endpoint
if response.status in [200, 404]:
return True
elif response.status == 401:
raise ConfigEntryNotReady("Invalid API key")
@@ -168,35 +160,39 @@ async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: b
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA Text AI from a config entry."""
try:
# Проверка наличия провайдера
if CONF_API_PROVIDER not in entry.data:
_LOGGER.error("API provider not specified")
raise ConfigEntryNotReady("API provider is required")
session = aiohttp_client.async_get_clientsession(hass)
# Determine API type based on model and endpoint
# Получаем провайдера из конфигурации
api_provider = entry.data.get(CONF_API_PROVIDER)
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
endpoint = entry.data.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT).rstrip('/')
endpoint = entry.data.get(CONF_API_ENDPOINT,
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT).rstrip('/')
api_key = entry.data[CONF_API_KEY]
is_vsegpt = "vsegpt" in endpoint.lower()
is_anthropic = any(m in model.lower() for m in ["claude", "anthropic"]) or is_vsegpt
# Определяем параметры подключения в зависимости от провайдера
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
# Configure headers based on API type
# Конфигурация headers
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
if is_vsegpt:
headers["Authorization"] = f"Bearer {api_key}"
if is_anthropic:
headers["anthropic-version"] = "2023-06-01"
elif is_anthropic:
if is_anthropic:
headers["x-api-key"] = api_key
headers["anthropic-version"] = "2023-06-01"
else:
else: # OpenAI
headers["Authorization"] = f"Bearer {api_key}"
# Check API with retries
# Проверка API с повторами
for attempt in range(API_RETRY_COUNT):
if await async_check_api(session, endpoint, headers, is_anthropic):
if await async_check_api(session, endpoint, headers, api_provider):
break
if attempt < API_RETRY_COUNT - 1:
delay = API_BACKOFF_FACTOR * (2 ** attempt)
+150 -363
View File
@@ -1,14 +1,10 @@
"""Config flow for HA text AI integration."""
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, Optional
import voluptuous as vol
import asyncio
import aiohttp
from async_timeout import timeout
from urllib.parse import urlparse, urljoin
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -20,403 +16,194 @@ from .const import (
CONF_MAX_TOKENS,
CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL,
CONF_API_PROVIDER,
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC,
API_PROVIDERS,
DEFAULT_MODEL,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS,
DEFAULT_API_ENDPOINT,
DEFAULT_REQUEST_INTERVAL,
DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT,
MIN_TEMPERATURE,
MAX_TEMPERATURE,
MIN_MAX_TOKENS,
MAX_MAX_TOKENS,
MIN_REQUEST_INTERVAL,
API_VERSION,
API_MODELS_PATH,
ERROR_INVALID_API_KEY,
ERROR_CANNOT_CONNECT,
ERROR_UNKNOWN,
ERROR_INVALID_MODEL,
ERROR_RATE_LIMIT,
ERROR_API_ERROR,
ERROR_TIMEOUT,
)
import logging
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema({
vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Optional(CONF_TEMPERATURE, default=str(DEFAULT_TEMPERATURE)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=str(DEFAULT_MAX_TOKENS)): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): str,
vol.Optional(CONF_REQUEST_INTERVAL, default=str(DEFAULT_REQUEST_INTERVAL)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
})
async def async_step_user(
self,
user_input: Optional[Dict[str, Any]] = None
) -> FlowResult:
"""Handle the initial step."""
errors: Dict[str, str] = {}
if user_input is not None:
try:
_LOGGER.debug("Received user input: %s", user_input)
# Преобразование строковых значений в числовые
try:
temperature = float(user_input[CONF_TEMPERATURE])
max_tokens = int(user_input[CONF_MAX_TOKENS])
request_interval = float(user_input[CONF_REQUEST_INTERVAL])
# Обновляем значения в user_input
validated_input = {
CONF_API_KEY: user_input[CONF_API_KEY],
CONF_MODEL: user_input[CONF_MODEL],
CONF_TEMPERATURE: temperature,
CONF_MAX_TOKENS: max_tokens,
CONF_API_ENDPOINT: user_input[CONF_API_ENDPOINT],
CONF_REQUEST_INTERVAL: request_interval
}
_LOGGER.debug("Converted values: %s", validated_input)
# Проверка диапазонов
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
_LOGGER.error("Invalid temperature: %f", temperature)
errors["base"] = "invalid_temperature"
elif not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
_LOGGER.error("Invalid max_tokens: %d", max_tokens)
errors["base"] = "invalid_max_tokens"
elif request_interval < MIN_REQUEST_INTERVAL:
_LOGGER.error("Invalid request_interval: %f", request_interval)
errors["base"] = "invalid_request_interval"
else:
# Проверка URL
endpoint = validated_input[CONF_API_ENDPOINT]
try:
result = urlparse(endpoint)
if not all([result.scheme, result.netloc]):
errors["base"] = "invalid_url_format"
else:
is_valid, error_code, available_models = await validate_api_connection(
self.hass,
validated_input[CONF_API_KEY],
endpoint,
validated_input[CONF_MODEL]
)
if is_valid:
await self.async_set_unique_id(validated_input[CONF_API_KEY])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="HA Text AI",
data=validated_input
)
errors["base"] = error_code
if error_code == ERROR_INVALID_MODEL:
_LOGGER.warning(
"Selected model %s not found in available models: %s",
validated_input[CONF_MODEL],
", ".join(available_models)
)
except Exception as e:
_LOGGER.error("URL parsing error: %s", str(e))
errors["base"] = "invalid_url_format"
except ValueError as ve:
_LOGGER.error("Value conversion error: %s", str(ve))
errors["base"] = "invalid_input_format"
except Exception as err:
_LOGGER.error("Unexpected error: %s", str(err))
errors["base"] = "unknown"
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={
"default_model": DEFAULT_MODEL,
"default_endpoint": DEFAULT_API_ENDPOINT,
}
)
async def validate_api_connection(
hass,
api_key: str,
endpoint: str,
model: str,
retry_count: int = 3,
retry_delay: float = 1.0
) -> Tuple[bool, str, list]:
"""Validate API connection with retry logic."""
session = async_get_clientsession(hass)
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
if "vsegpt" in endpoint.lower():
headers["Authorization"] = f"Bearer {api_key}"
base_url = endpoint.rstrip('/')
if not base_url.endswith("/v1"):
base_url = f"{base_url}/v1"
models_url = f"{base_url}/models"
_LOGGER.debug("Using VSE GPT endpoint: %s", models_url)
supported_models = [
"anthropic/claude-3-5-haiku",
"anthropic/claude-3.5-sonnet",
]
if model in supported_models:
return True, "", supported_models
elif any(m in model.lower() for m in ["claude", "anthropic"]):
headers["x-api-key"] = api_key
headers["anthropic-version"] = "2023-06-01"
base_url = endpoint.rstrip('/')
if not base_url.endswith("/v1"):
base_url = f"{base_url}/v1"
models_url = f"{base_url}/models"
_LOGGER.debug("Using Anthropic endpoint: %s", models_url)
else:
headers["Authorization"] = f"Bearer {api_key}"
base_url = endpoint.rstrip('/')
if not base_url.endswith(f"/{API_VERSION}"):
base_url = f"{base_url}/{API_VERSION}"
models_url = f"{base_url}/{API_MODELS_PATH}"
_LOGGER.debug("Using OpenAI endpoint: %s", models_url)
for attempt in range(retry_count):
try:
async with timeout(10):
if "vsegpt" in endpoint.lower():
test_url = f"{base_url}/models"
async with session.get(test_url, headers=headers) as response:
if response.status == 404:
if model.startswith("anthropic/"):
return True, "", [model]
elif response.status == 401:
return False, ERROR_INVALID_API_KEY, []
elif response.status == 429:
return False, ERROR_RATE_LIMIT, []
return True, "", [model]
async with session.get(models_url, headers=headers) as response:
_LOGGER.debug("API response status: %s", response.status)
if response.status == 200:
data = await response.json()
if any(m in model.lower() for m in ["claude", "anthropic"]):
model_ids = [m["id"] for m in data.get("models", [])]
if model.startswith("anthropic/"):
model = model.split("/")[1]
if model in model_ids or any(m.endswith(model) for m in model_ids):
return True, "", model_ids
else: # OpenAI format
model_ids = [m["id"] for m in data.get("data", [])]
if model in model_ids:
return True, "", model_ids
_LOGGER.warning(
"Model %s not found in available models: %s",
model,
", ".join(model_ids)
)
return False, ERROR_INVALID_MODEL, model_ids
elif response.status == 401:
_LOGGER.error("Authentication failed")
return False, ERROR_INVALID_API_KEY, []
elif response.status == 429:
_LOGGER.error("Rate limit exceeded")
if attempt < retry_count - 1:
await asyncio.sleep(retry_delay * (2 ** attempt))
continue
return False, ERROR_RATE_LIMIT, []
else:
response_text = await response.text()
_LOGGER.error(
"API error: %s - %s",
response.status,
response_text
)
if attempt < retry_count - 1:
await asyncio.sleep(retry_delay)
continue
return False, ERROR_API_ERROR, []
except asyncio.TimeoutError:
_LOGGER.warning(
"Timeout during API validation (attempt %d/%d)",
attempt + 1,
retry_count
)
if attempt < retry_count - 1:
await asyncio.sleep(retry_delay)
continue
return False, ERROR_TIMEOUT, []
except aiohttp.ClientError as err:
_LOGGER.error("Connection error: %s", str(err))
return False, ERROR_CANNOT_CONNECT, []
except Exception as err:
_LOGGER.exception("Unexpected error during validation: %s", str(err))
return False, ERROR_UNKNOWN, []
return False, ERROR_UNKNOWN, []
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HA text AI."""
VERSION = 1
async def async_step_user(
self,
user_input: Optional[Dict[str, Any]] = None
) -> FlowResult:
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
"""Handle the initial step."""
errors: Dict[str, str] = {}
if user_input is None:
return self._show_provider_selection()
if user_input is not None:
try:
# Валидация значений через схему
validated_input = STEP_USER_DATA_SCHEMA(user_input)
if "provider" in user_input:
return self._show_provider_config(user_input["provider"])
endpoint = validated_input[CONF_API_ENDPOINT]
try:
result = urlparse(endpoint)
if not all([result.scheme, result.netloc]):
errors["base"] = "invalid_url_format"
else:
is_valid, error_code, available_models = await validate_api_connection(
self.hass,
validated_input[CONF_API_KEY],
endpoint,
validated_input[CONF_MODEL]
)
return await self._process_configuration(user_input)
if is_valid:
await self.async_set_unique_id(validated_input[CONF_API_KEY])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="HA Text AI",
data=validated_input
)
errors["base"] = error_code
if error_code == ERROR_INVALID_MODEL:
_LOGGER.warning(
"Selected model %s not found in available models: %s",
validated_input[CONF_MODEL],
", ".join(available_models)
)
except Exception as e:
_LOGGER.error("URL parsing error: %s", str(e))
errors["base"] = "invalid_url_format"
def _show_provider_selection(self):
"""Show provider selection screen."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required("provider"): vol.In(API_PROVIDERS)
}),
description_placeholders={"providers": ", ".join(API_PROVIDERS)}
)
except vol.Invalid as err:
_LOGGER.error("Validation error: %s", str(err))
errors["base"] = "invalid_input"
except Exception as err:
_LOGGER.error("Unexpected error: %s", str(err))
errors["base"] = "unknown"
def _show_provider_config(self, provider):
"""Show configuration screen for selected provider."""
default_endpoint = DEFAULT_OPENAI_ENDPOINT if provider == API_PROVIDER_OPENAI else DEFAULT_ANTHROPIC_ENDPOINT
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={
"default_model": DEFAULT_MODEL,
"default_endpoint": DEFAULT_API_ENDPOINT,
}
data_schema=vol.Schema({
vol.Required(CONF_API_PROVIDER): vol.In([provider]),
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Optional(CONF_API_ENDPOINT, default=default_endpoint): str,
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
}),
description_placeholders={"provider": provider}
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
async def _process_configuration(self, user_input):
"""Validate and process user configuration."""
errors = {}
try:
session = async_get_clientsession(self.hass)
# Minimal API key validation
headers = {
"Authorization": f"Bearer {user_input[CONF_API_KEY]}",
"Content-Type": "application/json"
}
# Adjust headers and endpoint based on provider
if user_input[CONF_API_PROVIDER] == API_PROVIDER_ANTHROPIC:
headers = {
"x-api-key": user_input[CONF_API_KEY],
"anthropic-version": "2023-06-01"
}
# Basic connection test
try:
async with session.get(
f"{user_input[CONF_API_ENDPOINT]}/models",
headers=headers
) as response:
if response.status not in [200, 404]:
errors["base"] = "cannot_connect"
except Exception as e:
_LOGGER.error(f"Connection test failed: {e}")
errors["base"] = "cannot_connect"
if not errors:
# Use a unique ID based on the API key
await self.async_set_unique_id(user_input[CONF_API_KEY])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"HA Text AI ({user_input[CONF_API_PROVIDER]})",
data=user_input
)
except Exception as e:
_LOGGER.error(f"Unexpected error: {e}")
errors["base"] = "unknown"
# Return to provider config, preserving previous input
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_API_PROVIDER): vol.In([user_input[CONF_API_PROVIDER]]),
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_MODEL, default=user_input.get(CONF_MODEL, DEFAULT_MODEL)): str,
vol.Optional(CONF_API_ENDPOINT, default=user_input.get(CONF_API_ENDPOINT,
DEFAULT_OPENAI_ENDPOINT if user_input[CONF_API_PROVIDER] == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT)): str,
vol.Optional(CONF_TEMPERATURE, default=user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_REQUEST_INTERVAL, default=user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
}),
description_placeholders={"provider": user_input[CONF_API_PROVIDER]},
errors=errors
)
@staticmethod
@callback
def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for HA text AI."""
"""Options flow handler."""
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: Optional[Dict[str, Any]] = None
) -> FlowResult:
"""Handle options flow."""
async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
"""Manage the options."""
if user_input is not None:
try:
# Validate using schema
options_schema = self._get_options_schema()
validated_input = options_schema(user_input)
return self.async_create_entry(title="", data=validated_input)
except vol.Invalid as err:
_LOGGER.error("Options validation error: %s", str(err))
return self.async_show_form(
step_id="init",
data_schema=self._get_options_schema(),
errors={"base": "invalid_input"}
)
return self.async_create_entry(title="", data=user_input)
current_data = self.config_entry.data
return self.async_show_form(
step_id="init",
data_schema=self._get_options_schema(),
data_schema=vol.Schema({
vol.Optional(
CONF_TEMPERATURE,
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(
CONF_MAX_TOKENS,
default=current_data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(
CONF_REQUEST_INTERVAL,
default=current_data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
})
)
def _get_options_schema(self) -> vol.Schema:
"""Get options schema."""
return vol.Schema({
vol.Optional(
CONF_TEMPERATURE,
default=self.config_entry.options.get(
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
)
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(
CONF_MAX_TOKENS,
default=self.config_entry.options.get(
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
)
): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(
CONF_REQUEST_INTERVAL,
default=self.config_entry.options.get(
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
)
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
)
})
+24 -40
View File
@@ -8,6 +8,20 @@ from homeassistant.helpers import config_validation as cv
DOMAIN: Final = "ha_text_ai"
PLATFORMS: Final = [Platform.SENSOR]
# New constants for providers
CONF_API_PROVIDER: Final = "api_provider"
API_PROVIDER_OPENAI: Final = "openai"
API_PROVIDER_ANTHROPIC: Final = "anthropic"
API_PROVIDERS: Final = [
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC
]
# Default endpoints
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com/v1"
# Configuration constants
CONF_MODEL: Final = "model"
CONF_TEMPERATURE: Final = "temperature"
@@ -15,29 +29,13 @@ CONF_MAX_TOKENS: Final = "max_tokens"
CONF_API_ENDPOINT: Final = "api_endpoint"
CONF_REQUEST_INTERVAL: Final = "request_interval"
# Model constants
SUPPORTED_MODELS: Final = [
"o1-preview",
"o1-mini",
"gpt-4o-mini",
"gpt-4o",
"claude-3-5-haiku",
"claude-3.5-sonnet",
"claude-3-haiku",
"anthropic/claude-3-5-haiku",
"anthropic/claude-3.5-sonnet",
]
# Default values
DEFAULT_MODEL: Final = "gpt-4o-mini"
DEFAULT_TEMPERATURE: Final = 0.1
DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com"
DEFAULT_API_ENDPOINT: Final = DEFAULT_OPENAI_ENDPOINT
DEFAULT_REQUEST_INTERVAL: Final = 1.0
DEFAULT_TIMEOUT: Final = 30
DEFAULT_QUEUE_SIZE: Final = 100
DEFAULT_HISTORY_LIMIT: Final = 50
DEFAULT_RETRY_COUNT: Final = 3
# Parameter constraints
MIN_TEMPERATURE: Final = 0.0
@@ -45,19 +43,11 @@ MAX_TEMPERATURE: Final = 2.0
MIN_MAX_TOKENS: Final = 1
MAX_MAX_TOKENS: Final = 4096
MIN_REQUEST_INTERVAL: Final = 0.1
MIN_TIMEOUT: Final = 5
MAX_TIMEOUT: Final = 120
MAX_PROMPT_LENGTH: Final = 1000
MAX_HISTORY_LIMIT: Final = 100
# API constants
API_VERSION: Final = "v1"
API_MODELS_PATH: Final = "models"
API_CHAT_PATH: Final = "chat/completions"
API_TIMEOUT: Final = 30
API_RETRY_COUNT: Final = 3
API_BACKOFF_FACTOR: Final = 1.5
API_MAX_RETRIES: Final = 3
# History constants
HISTORY_FILTER_MODEL: Final = "filter_model"
@@ -195,7 +185,7 @@ EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Required("question"): cv.string,
vol.Optional("system_prompt"): cv.string,
vol.Optional("model"): vol.In(SUPPORTED_MODELS),
vol.Optional("model"): cv.string,
vol.Optional("temperature"): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
@@ -207,7 +197,13 @@ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Optional("priority"): cv.boolean,
})
SERVICE_SCHEMA_GET_HISTORY = {
# set_system_prompt
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required("prompt"): cv.string,
})
# get_history
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Optional("limit", default=10): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
@@ -215,16 +211,4 @@ SERVICE_SCHEMA_GET_HISTORY = {
vol.Optional("filter_model"): vol.In(SUPPORTED_MODELS),
vol.Optional("start_date"): cv.datetime,
vol.Optional("include_metadata"): cv.boolean,
}
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = {
vol.Required("prompt"): cv.string,
}
# VSE GPT specific constants
VSE_GPT_ENDPOINT = "https://api.vsegpt.ru"
VSE_GPT_API_VERSION = "2023-06-01"
VSE_GPT_MODELS = [
"anthropic/claude-3-5-haiku",
"anthropic/claude-3.5-sonnet",
]
})
+1 -9
View File
@@ -22,7 +22,6 @@ from .const import (
QUEUE_MAX_SIZE,
MAX_RETRIES,
RETRY_DELAY,
SUPPORTED_MODELS,
)
_LOGGER = logging.getLogger(__name__)
@@ -41,13 +40,11 @@ class HATextAICoordinator(DataUpdateCoordinator):
session: Optional[Any] = None,
is_anthropic: bool = False,
) -> None:
request_interval = float(request_interval)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=request_interval),
update_interval=timedelta(seconds=float(request_interval)),
)
"""Initialize coordinator."""
super().__init__(
@@ -684,13 +681,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
}
}
async def validate_model(self, model: str) -> bool:
"""Validate if model is supported."""
return model in SUPPORTED_MODELS
async def estimate_tokens(self, text: str) -> int:
"""Estimate token count for text."""
# Простая оценка: примерно 4 символа на токен
return len(text) // 4
def get_rate_limit_info(self) -> Dict[str, Any]:
+2 -1
View File
@@ -23,5 +23,6 @@
"ssdp": [],
"usb": [],
"version": "2.0.0",
"zeroconf": []
"zeroconf": [],
"supported_api_providers": ["openai", "anthropic"]
}
+4 -2
View File
@@ -17,6 +17,7 @@ from homeassistant.util import dt as dt_util
from .const import (
DOMAIN,
CONF_API_PROVIDER, # Новый импорт
ATTR_QUESTION,
ATTR_RESPONSE,
ATTR_LAST_UPDATED,
@@ -80,11 +81,13 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
self._error_count = 0
self._last_error = None
self._state = STATE_INITIALIZING
# Добавляем провайдера в device_info
self._attr_device_info = {
"identifiers": {(DOMAIN, self._attr_unique_id)},
"name": "HA Text AI",
"manufacturer": "Community",
"model": coordinator.model,
"model": f"{coordinator.model} ({self._config_entry.data.get(CONF_API_PROVIDER, 'Unknown')} provider)",
"sw_version": coordinator.api_version,
}
@@ -216,7 +219,6 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
else:
self._state = STATE_DISCONNECTED
# Обновляем счетчик ошибок только если статус изменился на ошибку
if self._state == STATE_ERROR:
self._error_count += 1
+13 -122
View File
@@ -3,24 +3,11 @@ ask_question:
description: >-
Send a question to the AI model and receive a detailed response.
The response will be stored in the conversation history and can be retrieved later.
Response time may vary based on model selection and server load.
Supports various AI models with different capabilities and pricing.
fields:
question:
name: Question
description: >-
Your question or prompt for the AI assistant. Be specific and clear for better results.
You can ask about home automation, technical advice, or general questions.
For complex queries, consider breaking them into smaller parts.
The system will maintain conversation context for follow-up questions.
description: Your question or prompt for the AI assistant.
required: true
example: |
What automations would you recommend for a smart kitchen?
Consider energy efficiency, convenience, and integration with:
- Smart lighting
- Appliance control
- Temperature monitoring
- Voice commands
selector:
text:
multiline: true
@@ -28,43 +15,15 @@ ask_question:
model:
name: Model
description: "Select an AI model to use (optional, overrides default setting)."
description: "Select AI model to use (optional, overrides default setting)."
required: false
example: "gpt-4o-mini"
default: "gpt-4o-mini"
selector:
select:
custom_value: true
options:
- label: "o1-preview"
value: "o1-preview"
- label: "o1-mini"
value: "o1-mini"
- label: "gpt-4o-mini"
value: "gpt-4o-mini"
- label: "gpt-4o"
value: "gpt-4o"
- label: "claude-3-5-haiku"
value: "claude-3-5-haiku"
- label: "claude-3.5-sonnet"
value: "claude-3.5-sonnet"
- label: "claude-3-haiku"
value: "claude-3-haiku"
- label: "anthropic/claude-3-5-haiku"
value: "anthropic/claude-3-5-haiku"
- label: "anthropic/claude-3.5-sonnet"
value: "anthropic/claude-3.5-sonnet"
mode: dropdown
text:
multiline: false
temperature:
name: Temperature
description: >-
Controls response creativity (0.0-2.0):
0.0-0.3: Focused, consistent responses (best for technical/factual queries)
0.4-0.7: Balanced responses (recommended for most uses)
0.8-2.0: More creative, varied responses (best for brainstorming)
Note: Higher values may produce less predictable results.
description: Controls response creativity (0.0-2.0).
required: false
default: 0.7
selector:
@@ -73,17 +32,10 @@ ask_question:
max: 2.0
step: 0.1
mode: slider
unit_of_measurement: ""
max_tokens:
name: Max Tokens
description: >-
Maximum length of the response (1-4096 tokens).
Recommended ranges:
- Short (256-512): Quick answers, status updates
- Medium (512-1024): Detailed explanations
- Long (1024-4096): Complex analysis
Note: Token limits vary by model. Actual length may be shorter.
description: Maximum length of the response (1-4096 tokens).
required: false
default: 1000
selector:
@@ -95,40 +47,24 @@ ask_question:
system_prompt:
name: System Prompt
description: >-
Optional system prompt to set context for this specific question.
This will temporarily override the default system prompt.
Use this to specify expertise areas, response format, or special instructions.
description: Optional system prompt to set context for this specific question.
required: false
example: "You are a home automation expert focused on energy efficiency and security"
selector:
text:
multiline: true
clear_history:
name: Clear History
description: >-
Delete all stored questions and responses from the conversation history.
This action cannot be undone. Consider using 'get_history' first if you need to backup the data.
System prompt settings and configuration will be preserved.
description: Delete all stored questions and responses from the conversation history.
fields: {}
get_history:
name: Get History
description: >-
Retrieve recent conversation history, including:
- Questions and responses
- Timestamps and response times
- Models used and token counts
- Temperature and other settings
Results are ordered from newest to oldest.
description: Retrieve recent conversation history.
fields:
limit:
name: Limit
description: >-
Number of most recent conversations to return (1-100).
Higher values return more history but may take longer to process.
Default: 10 conversations
description: Number of most recent conversations to return (1-100).
required: false
default: 10
selector:
@@ -138,66 +74,21 @@ get_history:
step: 1
mode: box
filter_model:
name: Filter by Model
description: >-
Only return conversations using a specific AI model.
Leave empty to show all models.
required: false
selector:
select:
custom_value: true
options:
- label: "All Models"
value: ""
- label: "GPT-3.5 Turbo"
value: "gpt-3.5-turbo"
- label: "GPT-4"
value: "gpt-4"
- label: "GPT-4 Turbo"
value: "gpt-4-1106-preview"
- label: "Claude-3 Sonnet"
value: "claude-3-sonnet"
- label: "Claude-3 Opus"
value: "claude-3-opus"
mode: dropdown
start_date:
name: Start Date
description: >-
Optional start date for filtering history.
Format: YYYY-MM-DD
description: Optional start date for filtering history.
required: false
selector:
datetime:
set_system_prompt:
name: Set System Prompt
description: >-
Set default system behavior instructions for all future conversations.
This defines how the AI should behave and respond to questions.
The prompt will be used for all models unless overridden per question.
description: Set default system behavior instructions for all future conversations.
fields:
prompt:
name: System Prompt
description: >-
Instructions that define how the AI should behave and respond.
Be specific about:
- Desired expertise and knowledge areas
- Response tone and style
- Output format preferences
- Special handling instructions
Maximum length: 1000 characters
description: Instructions that define how the AI should behave and respond.
required: true
example: |
You are a home automation expert assistant. Focus on:
1. Practical and efficient solutions
2. Energy-saving recommendations
3. Integration with popular smart home platforms
4. Security and privacy considerations
Provide detailed but concise responses with clear steps when applicable.
Format complex responses with bullet points or numbered lists.
Include warnings about potential risks or limitations.
selector:
text:
multiline: true
@@ -3,15 +3,15 @@
"step": {
"user": {
"title": "Set up HA Text AI",
"description": "Configure your AI integration for various providers (OpenAI, Anthropic, etc.). Different models have different capabilities and pricing.",
"description": "Configure your AI integration with OpenAI or Anthropic providers.",
"data": {
"api_provider": "Select API Provider",
"api_key": "API key for authentication (required)",
"model": "AI model to use (e.g., gpt-3.5-turbo, gpt-4, claude-3-sonnet, claude-3-opus)",
"model": "AI model to use (provider-specific)",
"temperature": "Response creativity (0-2, lower = more focused and consistent)",
"max_tokens": "Maximum response length (1-4096 tokens)",
"api_endpoint": "Custom API endpoint URL (default varies by provider)",
"request_interval": "Minimum time between requests in seconds (min: 0.1)",
"system_prompt": "Default instructions for AI behavior and expertise"
"api_endpoint": "Custom API endpoint URL (optional)",
"request_interval": "Minimum time between requests in seconds (min: 0.1)"
}
}
},
@@ -19,13 +19,11 @@
"invalid_auth": "Authentication failed - check your API key",
"invalid_api_key": "Invalid API key - please verify your credentials",
"cannot_connect": "Connection failed - check endpoint and network status",
"invalid_model": "Model unavailable or not supported by the API",
"invalid_model": "Model unavailable or not supported by the selected provider",
"rate_limit": "Rate limit exceeded - please reduce request frequency",
"context_length": "Input exceeds maximum context length for model",
"api_error": "API service error - check provider status",
"timeout": "Request timeout - server not responding",
"queue_full": "Request queue full - try again later",
"invalid_prompt": "Invalid system prompt format or length",
"invalid_url_format": "Invalid API endpoint URL format",
"invalid_input": "Invalid configuration parameters",
"unknown": "Unexpected error - check logs for details"
@@ -35,16 +33,12 @@
"step": {
"init": {
"title": "HA Text AI Settings",
"description": "Adjust your AI integration parameters and behavior",
"description": "Adjust your AI integration parameters",
"data": {
"model": "Select AI model for responses (capabilities vary)",
"temperature": "Response creativity (0-2, affects variation)",
"model": "Select AI model (provider-specific)",
"temperature": "Response creativity (0-2)",
"max_tokens": "Maximum response length in tokens",
"request_interval": "Minimum seconds between requests (rate limiting)",
"system_prompt": "Default AI behavior and expertise instructions",
"timeout": "Request timeout in seconds (default: 30)",
"retry_count": "Number of retry attempts for failed requests",
"queue_size": "Maximum pending requests in queue"
"request_interval": "Minimum seconds between requests"
}
}
}
@@ -52,86 +46,25 @@
"services": {
"ask_question": {
"name": "Ask Question",
"description": "Send a question to the AI model and get a detailed response. Supports context awareness for follow-up questions.",
"description": "Send a question to the AI model and get a detailed response.",
"fields": {
"question": {
"name": "Question",
"description": "Your question or prompt for the AI model. Be specific for better results."
},
"system_prompt": {
"name": "System Prompt",
"description": "Optional behavior instructions for this specific question"
"description": "Your question or prompt for the AI model"
},
"model": {
"name": "Model",
"description": "Optional specific AI model for this request (overrides default)"
"description": "Optional specific AI model for this request"
},
"temperature": {
"name": "Temperature",
"description": "Optional creativity setting (0-2, affects response variation)"
"description": "Optional creativity setting (0-2)"
},
"max_tokens": {
"name": "Max Tokens",
"description": "Optional maximum response length (1-4096 tokens)"
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Delete all stored conversation history, responses, and metadata"
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history with detailed metadata including timestamps",
"fields": {
"limit": {
"name": "Limit",
"description": "Maximum number of conversations to return (1-100)"
},
"filter_model": {
"name": "Filter Model",
"description": "Show only responses from a specific model"
},
"start_date": {
"name": "Start Date",
"description": "Filter conversations from this date (YYYY-MM-DD)"
}
}
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Update default AI behavior and expertise instructions",
"fields": {
"prompt": {
"name": "System Prompt",
"description": "Instructions for AI behavior, expertise, and response style"
}
}
}
},
"entity": {
"sensor": {
"status": {
"name": "AI Status",
"state": {
"ready": "Ready for requests",
"processing": "Processing request",
"error": "Error occurred",
"disconnected": "API disconnected",
"rate_limited": "Rate limit reached",
"initializing": "Starting up",
"retrying": "Retrying request",
"queued": "Request queued"
}
},
"last_response": {
"name": "Last Response",
"state": {
"success": "Success",
"error": "Error",
"timeout": "Timeout"
}
}
}
}
}