mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 07:03:58 +08:00
Release v2.0.0
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
})
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"ssdp": [],
|
||||
"usb": [],
|
||||
"version": "2.0.0",
|
||||
"zeroconf": []
|
||||
"zeroconf": [],
|
||||
"supported_api_providers": ["openai", "anthropic"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user