Release v2.0.0

This commit is contained in:
SMKRV
2024-11-24 02:20:29 +03:00
parent 4f46d077df
commit 2688da5a82
7 changed files with 562 additions and 997 deletions
+70 -99
View File
@@ -40,23 +40,32 @@ from .const import (
SERVICE_CLEAR_HISTORY, SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY, SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT, SERVICE_SET_SYSTEM_PROMPT,
SERVICE_SCHEMA_ASK_QUESTION,
SERVICE_SCHEMA_SET_SYSTEM_PROMPT,
SERVICE_SCHEMA_GET_HISTORY,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@callback # Обновленные схемы сервисов
def get_coordinator_by_id(hass: HomeAssistant, entity_id: str) -> Optional[HATextAICoordinator]: SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
"""Get coordinator by entity ID.""" vol.Required("instance"): cv.string,
if DOMAIN in hass.data: vol.Required("question"): cv.string,
for entry_id, coordinator in hass.data[DOMAIN].items(): vol.Optional("system_prompt"): cv.string,
if coordinator.entity_id == entity_id: vol.Optional("model"): cv.string,
return coordinator vol.Optional("temperature"): cv.positive_float,
return None vol.Optional("max_tokens"): cv.positive_int,
})
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required("instance"): cv.string,
vol.Required("prompt"): cv.string,
})
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Required("instance"): cv.string,
vol.Optional("limit"): cv.positive_int,
vol.Optional("filter_model"): cv.string,
})
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."""
@@ -75,128 +84,93 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
async def async_ask_question(call: ServiceCall) -> None: async def async_ask_question(call: ServiceCall) -> None:
"""Handle ask_question service.""" """Handle ask_question service."""
entity_id = call.target.get("entity_id") instance = call.data["instance"]
if not entity_id: if instance not in hass.data[DOMAIN]:
raise HomeAssistantError("No target entity specified") raise HomeAssistantError(f"Instance {instance} not found")
coordinator = get_coordinator_by_id(hass, entity_id)
if not coordinator:
raise HomeAssistantError(f"No coordinator found for entity {entity_id}")
question = call.data.get("question")
if not question:
raise HomeAssistantError("No question provided")
coordinator = hass.data[DOMAIN][instance]["coordinator"]
try: try:
params = { await coordinator.async_ask_question(
"system_prompt": call.data.get("system_prompt"), question=call.data["question"],
"model": call.data.get("model"), model=call.data.get("model"),
"temperature": call.data.get("temperature"), temperature=call.data.get("temperature"),
"max_tokens": call.data.get("max_tokens"), max_tokens=call.data.get("max_tokens"),
"priority": call.data.get("priority", False) system_prompt=call.data.get("system_prompt"),
} )
await coordinator.async_ask_question(question, **params)
except Exception as err: except Exception as err:
_LOGGER.error("Error asking question: %s", str(err)) _LOGGER.error("Error asking question: %s", str(err))
raise HomeAssistantError(f"Failed to process question: {str(err)}") raise HomeAssistantError(f"Failed to process question: {str(err)}")
async def async_clear_history(call: ServiceCall) -> None: async def async_clear_history(call: ServiceCall) -> None:
"""Handle clear_history service.""" """Handle clear_history service."""
entity_id = call.target.get("entity_id") instance = call.data["instance"]
if not entity_id: if instance not in hass.data[DOMAIN]:
raise HomeAssistantError("No target entity specified") raise HomeAssistantError(f"Instance {instance} not found")
coordinator = get_coordinator_by_id(hass, entity_id)
if not coordinator:
raise HomeAssistantError(f"No coordinator found for entity {entity_id}")
coordinator = hass.data[DOMAIN][instance]["coordinator"]
try: try:
await coordinator.clear_history() await coordinator.async_clear_history()
except Exception as err: except Exception as err:
_LOGGER.error("Error clearing history: %s", str(err)) _LOGGER.error("Error clearing history: %s", str(err))
raise HomeAssistantError(f"Failed to clear history: {str(err)}") raise HomeAssistantError(f"Failed to clear history: {str(err)}")
async def async_get_history(call: ServiceCall) -> None: async def async_get_history(call: ServiceCall) -> None:
"""Handle get_history service.""" """Handle get_history service."""
entity_id = call.target.get("entity_id") instance = call.data["instance"]
if not entity_id: if instance not in hass.data[DOMAIN]:
raise HomeAssistantError("No target entity specified") raise HomeAssistantError(f"Instance {instance} not found")
coordinator = get_coordinator_by_id(hass, entity_id)
if not coordinator:
raise HomeAssistantError(f"No coordinator found for entity {entity_id}")
coordinator = hass.data[DOMAIN][instance]["coordinator"]
try: try:
limit = call.data.get("limit", 10) return await coordinator.async_get_history(
start_date = call.data.get("start_date") limit=call.data.get("limit"),
include_metadata = call.data.get("include_metadata", False) filter_model=call.data.get("filter_model"),
sort_order = call.data.get("sort_order", "desc") instance=instance
history = await coordinator.get_history(
limit=limit,
start_date=start_date,
include_metadata=include_metadata,
sort_order=sort_order
) )
return history
except Exception as err: except Exception as err:
_LOGGER.error("Error getting history: %s", str(err)) _LOGGER.error("Error getting history: %s", str(err))
raise HomeAssistantError(f"Failed to get history: {str(err)}") raise HomeAssistantError(f"Failed to get history: {str(err)}")
async def async_set_system_prompt(call: ServiceCall) -> None: async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle set_system_prompt service.""" """Handle set_system_prompt service."""
entity_id = call.target.get("entity_id") instance = call.data["instance"]
if not entity_id: if instance not in hass.data[DOMAIN]:
raise HomeAssistantError("No target entity specified") raise HomeAssistantError(f"Instance {instance} not found")
coordinator = get_coordinator_by_id(hass, entity_id)
if not coordinator:
raise HomeAssistantError(f"No coordinator found for entity {entity_id}")
prompt = call.data.get("prompt")
if not prompt:
raise HomeAssistantError("No prompt provided")
coordinator = hass.data[DOMAIN][instance]["coordinator"]
try: try:
coordinator.update_system_prompt(prompt) await coordinator.async_set_system_prompt(call.data["prompt"])
except Exception as err: except Exception as err:
_LOGGER.error("Error setting system prompt: %s", str(err)) _LOGGER.error("Error setting system prompt: %s", str(err))
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}") raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
# Base schema with target
base_schema = vol.Schema({
vol.Required("target"): {
vol.Required("entity_id"): cv.entity_id
}
})
# Register services # Register services
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_ASK_QUESTION, SERVICE_ASK_QUESTION,
async_ask_question, async_ask_question,
schema=base_schema.extend(SERVICE_SCHEMA_ASK_QUESTION.schema) schema=SERVICE_SCHEMA_ASK_QUESTION
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_CLEAR_HISTORY, SERVICE_CLEAR_HISTORY,
async_clear_history, async_clear_history,
schema=base_schema schema=vol.Schema({vol.Required("instance"): cv.string})
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_GET_HISTORY, SERVICE_GET_HISTORY,
async_get_history, async_get_history,
schema=base_schema.extend(SERVICE_SCHEMA_GET_HISTORY.schema) schema=SERVICE_SCHEMA_GET_HISTORY
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_SET_SYSTEM_PROMPT, SERVICE_SET_SYSTEM_PROMPT,
async_set_system_prompt, async_set_system_prompt,
schema=base_schema.extend(SERVICE_SCHEMA_SET_SYSTEM_PROMPT.schema) schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
) )
return True return True
@@ -239,6 +213,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT).rstrip('/') else DEFAULT_ANTHROPIC_ENDPOINT).rstrip('/')
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
headers = { headers = {
@@ -256,23 +231,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady("API connection failed") raise ConfigEntryNotReady("API connection failed")
coordinator = HATextAICoordinator( coordinator = HATextAICoordinator(
hass, hass=hass,
api_key=api_key, client=None, # Будет установлен позже
endpoint=endpoint,
model=model, model=model,
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE), update_interval=int(entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)),
instance_name=instance_name,
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS), max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
request_interval=float(entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)), temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
name=entry.title or entry.data.get(CONF_NAME, "HA Text AI"), is_anthropic=is_anthropic,
session=session,
is_anthropic=is_anthropic
) )
await coordinator.async_initialize() # Сохраняем данные интеграции
await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][instance_name] = {
"coordinator": coordinator,
hass.data.setdefault(DOMAIN, {}) "config_entry": entry,
hass.data[DOMAIN][entry.entry_id] = coordinator }
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
@@ -284,15 +257,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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."""
try: try:
coordinator = hass.data[DOMAIN].get(entry.entry_id) instance_name = entry.data.get(CONF_NAME, entry.entry_id)
if coordinator: if instance_name in hass.data[DOMAIN]:
coordinator = hass.data[DOMAIN][instance_name]["coordinator"]
await coordinator.async_shutdown() await coordinator.async_shutdown()
hass.data[DOMAIN].pop(instance_name)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
except Exception as ex: except Exception as ex:
_LOGGER.exception("Error unloading entry: %s", str(ex)) _LOGGER.exception("Error unloading entry: %s", str(ex))
+121 -117
View File
@@ -4,10 +4,11 @@ from typing import Any, Dict, Optional
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import selector
from .const import ( from .const import (
DOMAIN, DOMAIN,
@@ -42,145 +43,144 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize flow.""" """Initialize flow."""
super().__init__() self._errors = {}
self.provider = None self._data = {}
self._provider = None
def _show_provider_selection(self) -> FlowResult:
"""Show the provider selection form."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required("provider"): vol.In(API_PROVIDERS),
}),
)
def _show_provider_config(self, provider: str) -> FlowResult:
"""Show the configuration form for the 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=vol.Schema({
vol.Required("name", default=f"HA Text AI {len(self._async_current_entries()) + 1}"): str,
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)
),
})
)
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.""" """Handle the initial step."""
if user_input is None: if user_input is None:
return self._show_provider_selection() return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
selector.SelectSelectorConfig(
options=API_PROVIDERS,
translation_key="api_provider"
)
),
})
)
if "provider" in user_input: self._provider = user_input[CONF_API_PROVIDER]
self.provider = user_input["provider"] return await self.async_step_provider()
return self._show_provider_config(self.provider)
return await self._process_configuration(user_input) async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
"""Handle provider configuration step."""
if user_input is None:
default_endpoint = (
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT
)
async def _process_configuration(self, user_input): suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}"
"""Validate and process user configuration."""
errors = {}
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
vol.Required(CONF_NAME, default=suggested_name): str,
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Required(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)
),
}),
errors=self._errors
)
# Проверка уникальности имени
instance_name = user_input[CONF_NAME]
await self._async_validate_name(instance_name)
if self._errors:
return await self.async_step_provider()
# Проверка API подключения
if not await self._async_validate_api(user_input):
return await self.async_step_provider()
# Создание записи конфигурации
return await self._create_entry(user_input)
async def _async_validate_name(self, name: str) -> bool:
"""Validate that the name is unique."""
for entry in self._async_current_entries():
if entry.data.get(CONF_NAME) == name:
self._errors["name"] = "name_exists"
return False
return True
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
"""Validate API connection."""
try: try:
# Создаем уникальный идентификатор, включающий имя экземпляра
instance_name = user_input.get("name", f"HA Text AI {len(self._async_current_entries()) + 1}")
unique_id = f"{DOMAIN}_{instance_name}_{user_input[CONF_API_PROVIDER]}"
# Нормализуем уникальный идентификатор
unique_id = unique_id.lower().replace(" ", "_")
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
# Проверяем подключение к API
session = async_get_clientsession(self.hass) session = async_get_clientsession(self.hass)
headers = self._get_api_headers(user_input) headers = self._get_api_headers(user_input)
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
try: check_url = (
async with session.get( f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
f"{user_input[CONF_API_ENDPOINT]}/models", else f"{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: async with session.get(check_url, headers=headers) as response:
return self.async_create_entry( if response.status == 401:
title=instance_name, self._errors["base"] = "invalid_auth"
data={ return False
"name": instance_name, elif response.status not in [200, 404]:
**user_input, self._errors["base"] = "cannot_connect"
"unique_id": unique_id # Сохраняем уникальный идентификатор в данных return False
} return True
)
except Exception as e: except Exception as err:
_LOGGER.error(f"Unexpected error: {e}") _LOGGER.error("API validation error: %s", str(err))
errors["base"] = "unknown" self._errors["base"] = "cannot_connect"
return False
return self._show_configuration_form(user_input, errors) def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
def _get_api_headers(self, user_input):
"""Get API headers based on provider.""" """Get API headers based on provider."""
if user_input[CONF_API_PROVIDER] == API_PROVIDER_ANTHROPIC: api_key = user_input[CONF_API_KEY]
if self._provider == API_PROVIDER_ANTHROPIC:
return { return {
"x-api-key": user_input[CONF_API_KEY], "x-api-key": api_key,
"anthropic-version": "2023-06-01" "anthropic-version": "2023-06-01",
"Content-Type": "application/json"
} }
return { return {
"Authorization": f"Bearer {user_input[CONF_API_KEY]}", "Authorization": f"Bearer {api_key}",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
def _show_configuration_form(self, user_input, errors=None): async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
"""Show the configuration form to edit location data.""" """Create the config entry."""
return self.async_show_form( instance_name = user_input[CONF_NAME]
step_id="user", unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_")
data_schema=vol.Schema({
vol.Required("name", default=user_input.get("name", f"HA Text AI {len(self._async_current_entries()) + 1}")): str, return self.async_create_entry(
vol.Required(CONF_API_PROVIDER): vol.In([user_input[CONF_API_PROVIDER]]), title=instance_name,
vol.Required(CONF_API_KEY): str, data={
vol.Required(CONF_MODEL, default=user_input.get(CONF_MODEL, DEFAULT_MODEL)): str, CONF_API_PROVIDER: self._provider,
vol.Optional(CONF_API_ENDPOINT, default=user_input.get(CONF_API_ENDPOINT, CONF_NAME: instance_name,
DEFAULT_OPENAI_ENDPOINT if user_input[CONF_API_PROVIDER] == API_PROVIDER_OPENAI **user_input,
else DEFAULT_ANTHROPIC_ENDPOINT)): str, "unique_id": unique_id
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)
),
}),
errors=errors or {}
) )
@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)
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow.""" """Handle options flow."""
@@ -194,11 +194,15 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)
current_data = self.config_entry.data current_data = {**self.config_entry.data, **self.config_entry.options}
return self.async_show_form( return self.async_show_form(
step_id="init", step_id="init",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Optional(
CONF_MODEL,
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
): str,
vol.Optional( vol.Optional(
CONF_TEMPERATURE, CONF_TEMPERATURE,
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE) default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
+26 -118
View File
@@ -1,14 +1,12 @@
"""Constants for the HA text AI integration.""" """Constants for the HA text AI integration."""
from typing import Final from typing import Final
import voluptuous as vol import voluptuous as vol
from homeassistant.const import Platform, CONF_API_KEY # Добавлен CONF_API_KEY from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
# Domain and platforms # Domain and platforms
DOMAIN: Final = "ha_text_ai" DOMAIN: Final = "ha_text_ai"
PLATFORMS: Final = [Platform.SENSOR] PLATFORMS: Final = [Platform.SENSOR]
CONF_NAME = "name"
DEFAULT_NAME = "HA Text AI"
# Provider configuration # Provider configuration
CONF_API_PROVIDER: Final = "api_provider" CONF_API_PROVIDER: Final = "api_provider"
@@ -22,7 +20,7 @@ API_PROVIDERS: Final = [
# Default endpoints # Default endpoints
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1" DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com/v1" DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
# Configuration constants # Configuration constants
CONF_MODEL: Final = "model" CONF_MODEL: Final = "model"
@@ -30,12 +28,12 @@ CONF_TEMPERATURE: Final = "temperature"
CONF_MAX_TOKENS: Final = "max_tokens" CONF_MAX_TOKENS: Final = "max_tokens"
CONF_API_ENDPOINT: Final = "api_endpoint" CONF_API_ENDPOINT: Final = "api_endpoint"
CONF_REQUEST_INTERVAL: Final = "request_interval" CONF_REQUEST_INTERVAL: Final = "request_interval"
CONF_INSTANCE: Final = "instance"
# Default values # Default values
DEFAULT_MODEL: Final = "gpt-4o-mini" DEFAULT_MODEL: Final = "gpt-3.5-turbo" # Обновлено на актуальную модель
DEFAULT_TEMPERATURE: Final = 0.1 DEFAULT_TEMPERATURE: Final = 0.7
DEFAULT_MAX_TOKENS: Final = 1000 DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_API_ENDPOINT: Final = DEFAULT_OPENAI_ENDPOINT
DEFAULT_REQUEST_INTERVAL: Final = 1.0 DEFAULT_REQUEST_INTERVAL: Final = 1.0
DEFAULT_TIMEOUT: Final = 30 DEFAULT_TIMEOUT: Final = 30
@@ -48,53 +46,23 @@ MIN_REQUEST_INTERVAL: Final = 0.1
MAX_REQUEST_INTERVAL: Final = 60.0 MAX_REQUEST_INTERVAL: Final = 60.0
# API constants # API constants
API_CHAT_PATH: Final = "chat/completions"
API_TIMEOUT: Final = 30 API_TIMEOUT: Final = 30
API_RETRY_COUNT: Final = 3 API_RETRY_COUNT: Final = 3
# History constants
HISTORY_FILTER_MODEL: Final = "filter_model"
HISTORY_FILTER_DATE: Final = "start_date"
HISTORY_SORT_ORDER: Final = "sort_order"
HISTORY_INCLUDE_METADATA: Final = "include_metadata"
HISTORY_SORT_ASC: Final = "asc"
HISTORY_SORT_DESC: Final = "desc"
HISTORY_MAX_ENTRIES: Final = 100
# Service names # Service names
SERVICE_ASK_QUESTION: Final = "ask_question" SERVICE_ASK_QUESTION: Final = "ask_question"
SERVICE_CLEAR_HISTORY: Final = "clear_history" SERVICE_CLEAR_HISTORY: Final = "clear_history"
SERVICE_GET_HISTORY: Final = "get_history" SERVICE_GET_HISTORY: Final = "get_history"
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt" SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
# Service descriptions
SERVICE_ASK_QUESTION_DESCRIPTION: Final = "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history."
SERVICE_CLEAR_HISTORY_DESCRIPTION: Final = "Delete all stored questions and responses from the conversation history."
SERVICE_GET_HISTORY_DESCRIPTION: Final = "Retrieve recent conversation history with optional filtering and sorting."
SERVICE_SET_SYSTEM_PROMPT_DESCRIPTION: Final = "Set default system behavior instructions for all future conversations."
# Attribute keys # Attribute keys
ATTR_QUESTION: Final = "question" ATTR_QUESTION: Final = "question"
ATTR_RESPONSE: Final = "response" ATTR_RESPONSE: Final = "response"
ATTR_LAST_UPDATED: Final = "last_updated" ATTR_INSTANCE: Final = "instance"
ATTR_MODEL: Final = "model" ATTR_MODEL: Final = "model"
ATTR_TEMPERATURE: Final = "temperature" ATTR_TEMPERATURE: Final = "temperature"
ATTR_MAX_TOKENS: Final = "max_tokens" ATTR_MAX_TOKENS: Final = "max_tokens"
ATTR_TOTAL_RESPONSES: Final = "total_responses"
ATTR_SYSTEM_PROMPT: Final = "system_prompt" ATTR_SYSTEM_PROMPT: Final = "system_prompt"
ATTR_RESPONSE_TIME: Final = "response_time"
ATTR_QUEUE_SIZE: Final = "queue_size"
ATTR_API_STATUS: Final = "api_status"
ATTR_ERROR_COUNT: Final = "error_count"
ATTR_LAST_ERROR: Final = "last_error"
ATTR_API_VERSION: Final = "api_version"
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
ATTR_REQUEST_COUNT: Final = "request_count"
ATTR_TOKENS_USED: Final = "tokens_used"
ATTR_RETRY_COUNT: Final = "retry_count"
ATTR_QUEUE_POSITION: Final = "queue_position"
ATTR_ESTIMATED_WAIT: Final = "estimated_wait"
ATTR_SORT_ORDER: Final = "sort_order"
# Error messages # Error messages
ERROR_INVALID_API_KEY: Final = "invalid_api_key" ERROR_INVALID_API_KEY: Final = "invalid_api_key"
@@ -105,80 +73,12 @@ ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded" ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
ERROR_API_ERROR: Final = "api_error" ERROR_API_ERROR: Final = "api_error"
ERROR_TIMEOUT: Final = "timeout_error" ERROR_TIMEOUT: Final = "timeout_error"
ERROR_QUEUE_FULL: Final = "queue_full" ERROR_INVALID_INSTANCE: Final = "invalid_instance"
ERROR_INVALID_PROMPT: Final = "invalid_prompt" ERROR_NAME_EXISTS: Final = "name_exists"
ERROR_INVALID_PARAMETERS: Final = "invalid_parameters"
ERROR_SERVICE_UNAVAILABLE: Final = "service_unavailable"
ERROR_INVALID_SORT_ORDER: Final = "invalid_sort_order"
# Configuration descriptions
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses"
CONF_TEMPERATURE_DESCRIPTION: Final = "Temperature for response generation (0-2)"
CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)"
CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL"
CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)"
# Entity attributes descriptions
ATTR_QUESTION_DESCRIPTION: Final = "Last question asked"
ATTR_RESPONSE_DESCRIPTION: Final = "Last response received"
ATTR_LAST_UPDATED_DESCRIPTION: Final = "Time of last update"
ATTR_MODEL_DESCRIPTION: Final = "Current AI model in use"
ATTR_TEMPERATURE_DESCRIPTION: Final = "Current temperature setting"
ATTR_MAX_TOKENS_DESCRIPTION: Final = "Current max tokens setting"
ATTR_TOTAL_RESPONSES_DESCRIPTION: Final = "Total number of responses"
ATTR_SYSTEM_PROMPT_DESCRIPTION: Final = "Current system prompt"
ATTR_RESPONSE_TIME_DESCRIPTION: Final = "Time taken for last response"
ATTR_QUEUE_SIZE_DESCRIPTION: Final = "Current size of question queue"
ATTR_API_STATUS_DESCRIPTION: Final = "Current API connection status"
ATTR_ERROR_COUNT_DESCRIPTION: Final = "Total number of errors"
ATTR_LAST_ERROR_DESCRIPTION: Final = "Last error message"
ATTR_API_VERSION_DESCRIPTION: Final = "Current API version"
ATTR_ENDPOINT_STATUS_DESCRIPTION: Final = "Current endpoint status"
ATTR_REQUEST_COUNT_DESCRIPTION: Final = "Total number of API requests"
ATTR_TOKENS_USED_DESCRIPTION: Final = "Total tokens used"
ATTR_SORT_ORDER_DESCRIPTION: Final = "Sort order for history (asc/desc)"
# Entity attributes
ENTITY_NAME: Final = "HA Text AI"
ENTITY_ICON: Final = "mdi:robot"
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
ENTITY_ICON_OFFLINE: Final = "mdi:robot-off"
ENTITY_ICON_QUEUE: Final = "mdi:robot-confused"
# Translation keys
TRANSLATION_KEY_CONFIG: Final = "config"
TRANSLATION_KEY_OPTIONS: Final = "options"
TRANSLATION_KEY_ERROR: Final = "error"
TRANSLATION_KEY_STATE: Final = "state"
TRANSLATION_KEY_SERVICES: Final = "services"
# State attributes
STATE_READY: Final = "ready"
STATE_PROCESSING: Final = "processing"
STATE_ERROR: Final = "error"
STATE_DISCONNECTED: Final = "disconnected"
STATE_RATE_LIMITED: Final = "rate_limited"
STATE_INITIALIZING: Final = "initializing"
STATE_MAINTENANCE: Final = "maintenance"
STATE_RETRYING: Final = "retrying"
STATE_QUEUED: Final = "queued"
STATE_UPDATING: Final = "updating"
# Logging
LOGGER_NAME: Final = "custom_components.ha_text_ai"
LOG_LEVEL_DEFAULT: Final = "INFO"
# Queue constants
QUEUE_TIMEOUT: Final = 5
QUEUE_MAX_SIZE: Final = 100
# Retry constants
MAX_RETRIES: Final = 3
RETRY_DELAY: Final = 1.0
# Service schema constants # Service schema constants
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Required("question"): cv.string, vol.Required("question"): cv.string,
vol.Optional("system_prompt"): cv.string, vol.Optional("system_prompt"): cv.string,
vol.Optional("model"): cv.string, vol.Optional("model"): cv.string,
@@ -193,26 +93,25 @@ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
}) })
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({ SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Required("prompt"): cv.string vol.Required("prompt"): cv.string
}) })
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({ SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Optional("limit", default=10): vol.All( vol.Optional("limit", default=10): vol.All(
vol.Coerce(int), vol.Coerce(int),
vol.Range(min=1, max=HISTORY_MAX_ENTRIES) vol.Range(min=1, max=100)
), ),
vol.Optional("start_date"): cv.datetime, vol.Optional("filter_model"): cv.string
vol.Optional("include_metadata"): cv.boolean,
vol.Optional("sort_order", default=HISTORY_SORT_DESC): vol.In([
HISTORY_SORT_ASC,
HISTORY_SORT_DESC
])
}) })
# Configuration schema # Configuration schema
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS),
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string, vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All( vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Coerce(float),
@@ -226,11 +125,20 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All( vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL) vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
), )
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS)
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# Entity attributes
ENTITY_ICON: Final = "mdi:robot"
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
# State attributes
STATE_READY: Final = "ready"
STATE_PROCESSING: Final = "processing"
STATE_ERROR: Final = "error"
# Event names # Event names
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received" EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred" EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
+150 -533
View File
@@ -1,561 +1,117 @@
"""Data coordinator for HA text AI.""" """Data coordinator for HA text AI."""
import asyncio
import logging import logging
import time import time
import uuid from datetime import datetime
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import httpx
from anthropic import AsyncAnthropic
from openai import APIError, AsyncOpenAI, AuthenticationError, RateLimitError
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import (
DOMAIN,
DEFAULT_TIMEOUT,
MAX_RETRIES,
QUEUE_MAX_SIZE,
RETRY_DELAY,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class HATextAICoordinator(DataUpdateCoordinator): class HATextAICoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API.""" """Class to manage fetching data from API."""
def _validate_params(self, api_key: str, temperature: float, max_tokens: int) -> None:
"""Validate initialization parameters.
Args:
api_key: API key for authentication
temperature: Sampling temperature for text generation
max_tokens: Maximum number of tokens to generate
Raises:
ValueError: If any parameters are invalid
"""
if not api_key:
raise ValueError("API key cannot be empty")
try:
temp = float(temperature)
if not 0 <= temp <= 2:
raise ValueError("Temperature must be between 0 and 2")
except (TypeError, ValueError):
raise ValueError("Temperature must be a number between 0 and 2")
try:
tokens = int(max_tokens)
if tokens < 1:
raise ValueError("Max tokens must be a positive number")
except (TypeError, ValueError):
raise ValueError("Max tokens must be a positive integer")
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
api_key: str, client: Any,
endpoint: str,
model: str, model: str,
temperature: float, update_interval: int,
max_tokens: int, instance_name: str, # Добавляем идентификатор интеграции
request_interval: float, max_tokens: int = 1000,
name: str, temperature: float = 0.7,
session: Optional[Any] = None, max_history_size: int = 50,
is_anthropic: bool = False, is_anthropic: bool = False,
) -> None: ) -> None:
"""Initialize coordinator.""" """Initialize coordinator."""
self._name = name
self._validate_params(api_key, temperature, max_tokens)
self._entity_id = None
self.api_key = api_key
self.endpoint = endpoint
self.model = model
self.temperature = float(temperature)
self.max_tokens = int(max_tokens)
self._question_queue = asyncio.PriorityQueue(maxsize=QUEUE_MAX_SIZE)
self._responses: Dict[str, Any] = {}
self.system_prompt: Optional[str] = None
self._is_ready = False
self._is_processing = False
self._is_rate_limited = False
self._is_maintenance = False
self._error_count = 0
self._MAX_ERRORS = 3
self._request_count = 0
self._tokens_used = 0
self._api_version = "v1" # Keep single definition
self._endpoint_status = "disconnected"
self._last_error = None
self._last_request_time = 0
self._is_anthropic = is_anthropic
self._session = session or aiohttp_client.async_get_clientsession(hass)
self.client = None
self.hass = hass # Store hass reference for _init_client
self._start_time = time.time()
self.last_response = None
# History and metrics
self._history: List[Dict[str, Any]] = []
self._max_history_size = 100
self._performance_metrics: Dict[str, Any] = {
"avg_response_time": 0,
"total_errors": 0,
"success_rate": 100,
"requests_per_minute": 0,
"avg_tokens_per_request": 0,
}
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
name=self._name, name=f"HA Text AI {instance_name}", # Уникальное имя для каждой интеграции
update_interval=timedelta(seconds=float(request_interval)), update_interval=update_interval,
) )
async def _init_client(self): self.instance_name = instance_name # Сохраняем идентификатор
"""Initialize API client with proper SSL context.""" self.client = client
try: self.model = model
if self._is_anthropic: self.max_tokens = max_tokens
self.client = AsyncAnthropic( self.temperature = temperature
api_key=self.api_key self._max_history_size = max_history_size
) self._is_anthropic = is_anthropic
else: # OpenAI self._system_prompt = None
# Create transport in separate thread
transport = await self.hass.async_add_executor_job(
lambda: httpx.AsyncHTTPTransport(retries=3)
)
limits = httpx.Limits(max_keepalive_connections=5, max_connections=10) # Статусы
self._is_processing = False
self._is_rate_limited = False
self._is_maintenance = False
async_client = httpx.AsyncClient( # История и метрики
base_url=self.endpoint, self._history: List[Dict[str, Any]] = []
timeout=httpx.Timeout(30.0), self._request_count = 0
transport=transport, self._error_count = 0
limits=limits, self._last_error = None
follow_redirects=True self._performance_metrics = {
) "total_requests": 0,
"total_errors": 0,
self.client = AsyncOpenAI( "avg_response_time": 0.0,
api_key=self.api_key, "last_request_time": None,
base_url=self.endpoint,
http_client=async_client,
max_retries=3
)
except Exception as e:
self._last_error = str(e)
_LOGGER.error("Error initializing API client: %s", str(e))
raise
async def async_initialize(self) -> None:
"""Initialize coordinator."""
try:
await self._init_client()
self._is_ready = True
self._endpoint_status = "connected"
except Exception as e:
self._last_error = str(e)
self._endpoint_status = "error"
_LOGGER.error("Failed to initialize coordinator: %s", str(e))
raise
async def async_ask(
self,
question: str,
priority: bool = False,
system_prompt: Optional[str] = None,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
) -> str:
"""Add question to queue with priority."""
if not self._is_ready:
raise HomeAssistantError("Coordinator is not ready")
if not question or not isinstance(question, str):
raise ValueError("Invalid question format")
# Validate optional parameters
if temperature is not None and not 0 <= temperature <= 2:
raise ValueError("Temperature must be between 0 and 2")
if max_tokens is not None and not isinstance(max_tokens, int):
raise ValueError("max_tokens must be an integer")
request_id = str(uuid.uuid4())
request_params = {
"id": request_id,
"question": question,
"system_prompt": system_prompt or self.system_prompt,
"model": model or self.model,
"temperature": temperature or self.temperature,
"max_tokens": max_tokens or self.max_tokens,
"timestamp": dt_util.utcnow().isoformat(),
} }
priority_level = 0 if priority else 1 # Версия API и последний ответ
await self._question_queue.put((priority_level, request_id, request_params)) self.api_version = "v1"
self.last_response = None
# Wait for response self.endpoint_status = "initialized"
while request_id not in self._responses:
await asyncio.sleep(0.1)
response = self._responses.pop(request_id)
# Add to history
history_entry = {
"timestamp": request_params["timestamp"],
"question": question,
"response": response,
"model": request_params["model"],
"metadata": {
"temperature": request_params["temperature"],
"max_tokens": request_params["max_tokens"],
"system_prompt": request_params["system_prompt"],
"priority": priority,
}
}
self._add_to_history(history_entry)
return response
async def _process_queue(self) -> None:
"""Process questions from queue."""
while True:
try:
if self._is_rate_limited:
await asyncio.sleep(60)
self._is_rate_limited = False
continue
priority, request_id, params = await self._question_queue.get()
try:
response = await self._make_api_request(params)
self._responses[request_id] = response
self._update_metrics(response)
except Exception as e:
self._last_error = str(e)
_LOGGER.error("Error processing request: %s", str(e))
self._responses[request_id] = f"Error: {str(e)}"
finally:
self._question_queue.task_done()
except Exception as e:
self._last_error = str(e)
_LOGGER.error("Queue processing error: %s", str(e))
await asyncio.sleep(1)
async def _make_api_request(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Make API request with retry logic."""
attempts = 0
while attempts < MAX_RETRIES:
try:
if self._is_anthropic:
response = await self._make_anthropic_call(**params)
else:
response = await self._make_openai_call(**params)
return response
except (AuthenticationError, ValueError) as e:
# Don't retry auth errors
raise
except RateLimitError:
self._is_rate_limited = True
await asyncio.sleep(RETRY_DELAY * (2 ** attempts))
attempts += 1
except Exception as e:
attempts += 1
if attempts >= MAX_RETRIES:
raise
await asyncio.sleep(RETRY_DELAY)
async def _make_anthropic_call(self, **params) -> Dict[str, Any]:
"""Make API call to Anthropic."""
start_time = time.time()
messages = []
if params.get("system_prompt"):
messages.append({"role": "system", "content": params["system_prompt"]})
messages.append({"role": "user", "content": params["question"]})
try:
completion = await self.client.messages.create(
model=params.get("model", self.model),
messages=messages,
temperature=params.get("temperature", self.temperature),
max_tokens=params.get("max_tokens", self.max_tokens),
)
response_time = time.time() - start_time
return {
"response": completion.content[0].text,
"model": completion.model,
"tokens": completion.usage.total_tokens if hasattr(completion, 'usage') else 0,
"response_time": response_time
}
except Exception as e:
self._last_error = str(e)
_LOGGER.error("Anthropic API call error: %s", str(e))
raise
async def _make_openai_call(self, **params) -> Dict[str, Any]:
"""Make API call to OpenAI."""
start_time = time.time()
try:
if not params.get("question"):
raise ValueError("Question cannot be empty")
messages = []
if params.get("system_prompt"):
messages.append({"role": "system", "content": params["system_prompt"]})
messages.append({"role": "user", "content": params["question"]})
api_params = {
"model": params.get("model", self.model),
"messages": messages,
"temperature": params.get("temperature", self.temperature),
"max_tokens": params.get("max_tokens", self.max_tokens),
}
completion = await self.client.chat.completions.create(**api_params)
response_time = time.time() - start_time
if not completion.choices:
raise ValueError("No response choices available")
return {
"response": completion.choices[0].message.content.strip(),
"model": completion.model,
"tokens": completion.usage.total_tokens,
"response_time": response_time
}
except Exception as e:
self._last_error = str(e)
error_msg = f"OpenAI API call failed: {str(e)}"
_LOGGER.error(error_msg, exc_info=True)
raise RuntimeError(error_msg) from e
def _add_to_history(self, entry: Dict[str, Any]) -> None:
"""Add entry to history with size limit."""
self._history.append(entry)
if len(self._history) > self._max_history_size:
self._history.pop(0)
async def get_history(
self,
limit: Optional[int] = None,
start_date: Optional[datetime] = None,
filter_model: Optional[str] = None, # Добавляем параметр
include_metadata: bool = False
) -> List[Dict[str, Any]]:
"""Get conversation history."""
history = self._history
if start_date:
history = [
h for h in history
if datetime.fromisoformat(h["timestamp"]) >= start_date
]
# Добавляем фильтрацию по модели
if filter_model:
history = [h for h in history if h["model"] == filter_model]
if not include_metadata:
history = [{
"timestamp": h["timestamp"],
"question": h["question"],
"response": h["response"],
"model": h["model"]
} for h in history]
if limit and limit > 0:
history = history[-limit:]
return history
def _update_metrics(self, response_data: Dict[str, Any]) -> None:
"""Update performance metrics."""
# Update response time metrics
response_time = response_data.get("response_time", 0)
current_avg = self._performance_metrics["avg_response_time"]
self._performance_metrics["avg_response_time"] = (
(current_avg * self._request_count + response_time) /
(self._request_count + 1)
)
# Update success rate
total_requests = self._request_count + 1
self._performance_metrics["success_rate"] = (
(total_requests - self._performance_metrics["total_errors"]) /
total_requests * 100
)
# Update tokens metrics
tokens = response_data.get("tokens", 0)
self._tokens_used += tokens
self._performance_metrics["avg_tokens_per_request"] = (
self._tokens_used / total_requests
)
# Calculate requests per minute
if self._last_request_time:
time_diff = time.time() - self._last_request_time
if time_diff > 0:
self._performance_metrics["requests_per_minute"] = 60 / time_diff
self._last_request_time = time.time()
self._request_count += 1
async def async_refresh_metrics(self) -> None:
"""Refresh performance metrics."""
try:
self._performance_metrics.update({
"queue_size": self._question_queue.qsize(),
"last_update": dt_util.utcnow().isoformat(),
"uptime": time.time() - self._start_time if hasattr(self, '_start_time') else 0,
"memory_usage": {
"history_size": len(self._history),
"response_cache_size": len(self._responses)
}
})
except Exception as e:
_LOGGER.error("Error refreshing metrics: %s", str(e))
async def export_metrics(self) -> Dict[str, Any]:
"""Export detailed metrics and statistics."""
await self.async_refresh_metrics()
return {
"entity_id": self._entity_id,
"performance": self._performance_metrics,
"requests": {
"total": self._request_count,
"successful": self._request_count - self._performance_metrics["total_errors"],
"failed": self._performance_metrics["total_errors"]
},
"tokens": {
"total_used": self._tokens_used,
"average_per_request": self._performance_metrics["avg_tokens_per_request"]
},
"status": {
"is_ready": self.is_ready,
"endpoint_status": self._endpoint_status,
"error_count": self._error_count,
"last_error": self._last_error
},
"queue": {
"size": self.queue_size,
"is_full": self.is_queue_full
}
}
@property
def is_ready(self) -> bool:
"""Return if coordinator is ready."""
return self._is_ready
@property @property
def is_processing(self) -> bool: def is_processing(self) -> bool:
"""Return if coordinator is processing.""" """Return True if currently processing a request."""
return self._is_processing return self._is_processing
@property @property
def is_rate_limited(self) -> bool: def is_rate_limited(self) -> bool:
"""Return if coordinator is rate limited.""" """Return True if currently rate limited."""
return self._is_rate_limited return self._is_rate_limited
@property @property
def is_maintenance(self) -> bool: def is_maintenance(self) -> bool:
"""Return if coordinator is in maintenance mode.""" """Return True if in maintenance mode."""
return self._is_maintenance return self._is_maintenance
@property async def async_ask_question(
def queue_size(self) -> int: self,
"""Return current queue size.""" question: str,
return self._question_queue.qsize() model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None,
) -> dict:
"""Process a question with optional parameters."""
try:
temp_model = model or self.model
temp_temperature = temperature or self.temperature
temp_max_tokens = max_tokens or self.max_tokens
temp_system_prompt = system_prompt or self._system_prompt
@property messages = []
def is_queue_full(self) -> bool: if temp_system_prompt:
"""Return if queue is full.""" messages.append({"role": "system", "content": temp_system_prompt})
return self.queue_size >= QUEUE_MAX_SIZE messages.append({"role": "user", "content": question})
@property kwargs = {
def last_error(self) -> Optional[str]: "model": temp_model,
"""Return last error message.""" "temperature": temp_temperature,
return self._last_error "max_tokens": temp_max_tokens,
"messages": messages,
}
@property return await self.async_process_message(question, **kwargs)
def endpoint_status(self) -> str:
"""Return endpoint connection status."""
return self._endpoint_status
@property except Exception as err:
def api_version(self) -> str: _LOGGER.error("Error processing question: %s", err)
"""Return the API version.""" raise HomeAssistantError(f"Failed to process question: {err}")
return self._api_version
async def __aenter__(self):
"""Async enter."""
await self.async_start()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async exit."""
await self.async_stop()
async def async_start(self) -> None:
"""Start coordinator operations."""
if not self._is_ready:
await self.async_initialize()
self._start_time = time.time()
self._is_processing = True
asyncio.create_task(self._process_queue())
async def async_stop(self) -> None:
"""Stop coordinator operations."""
self._is_processing = False
self._is_ready = False
# Clear queues and caches
while not self._question_queue.empty():
try:
await self._question_queue.get_nowait()
except asyncio.QueueEmpty:
break
self._responses.clear()
async def async_reset(self) -> None:
"""Reset coordinator state."""
await self.async_stop()
self._error_count = 0
self._request_count = 0
self._tokens_used = 0
self._last_error = None
self._history.clear()
self._performance_metrics = {
"avg_response_time": 0,
"total_errors": 0,
"success_rate": 100,
"requests_per_minute": 0,
"avg_tokens_per_request": 0,
}
await self.async_start()
async def async_clear_history(self) -> None:
"""Clear conversation history."""
self._history.clear()
def set_system_prompt(self, prompt: str) -> None:
"""Set system prompt."""
if not isinstance(prompt, str):
raise ValueError("System prompt must be a string")
self.system_prompt = prompt
async def _async_update_data(self) -> Dict[str, Any]: async def _async_update_data(self) -> Dict[str, Any]:
"""Update data from API.""" """Update data from API."""
@@ -565,17 +121,28 @@ class HATextAICoordinator(DataUpdateCoordinator):
"status": self.endpoint_status, "status": self.endpoint_status,
"metrics": self._performance_metrics, "metrics": self._performance_metrics,
"last_update": dt_util.utcnow().isoformat(), "last_update": dt_util.utcnow().isoformat(),
"last_response": self.last_response, # Добавляем last_response "last_response": self.last_response,
"is_processing": self._is_processing, "is_processing": self._is_processing,
"is_rate_limited": self._is_rate_limited, "is_rate_limited": self._is_rate_limited,
"is_maintenance": self._is_maintenance, "is_maintenance": self._is_maintenance,
"instance": self.instance_name, # Добавляем идентификатор интеграции
} }
except Exception as e: except Exception as e:
self._last_error = str(e) self._last_error = str(e)
self._error_count += 1
_LOGGER.error("Error updating data: %s", str(e)) _LOGGER.error("Error updating data: %s", str(e))
raise HomeAssistantError(f"Error updating data: {str(e)}") raise HomeAssistantError(f"Error updating data: {str(e)}")
async def async_process_message(self, message: str, **kwargs) -> dict: async def async_refresh_metrics(self) -> None:
"""Refresh performance metrics."""
self._performance_metrics.update({
"total_requests": self._request_count,
"total_errors": self._error_count,
"last_request_time": dt_util.utcnow().isoformat(),
"instance": self.instance_name, # Добавляем идентификатор интеграции
})
async def async_process_message(self, message: str, **kwargs) -> dict:
"""Process message and update last_response.""" """Process message and update last_response."""
try: try:
self._is_processing = True self._is_processing = True
@@ -583,36 +150,47 @@ class HATextAICoordinator(DataUpdateCoordinator):
start_time = time.time() start_time = time.time()
messages = kwargs.pop("messages", [{"role": "user", "content": message}])
model = kwargs.pop("model", self.model)
temperature = kwargs.pop("temperature", self.temperature)
max_tokens = kwargs.pop("max_tokens", self.max_tokens)
if self._is_anthropic: if self._is_anthropic:
response = await self.client.messages.create( response = await self.client.messages.create(
model=self.model, model=model,
max_tokens=self.max_tokens, max_tokens=max_tokens,
temperature=self.temperature, temperature=temperature,
messages=[{"role": "user", "content": message}], messages=messages,
**kwargs **kwargs
) )
response_text = response.content[0].text response_text = response.content[0].text
else: else:
response = await self.client.chat.completions.create( response = await self.client.chat.completions.create(
model=self.model, model=model,
temperature=self.temperature, temperature=temperature,
max_tokens=self.max_tokens, max_tokens=max_tokens,
messages=[{"role": "user", "content": message}], messages=messages,
**kwargs **kwargs
) )
response_text = response.choices[0].message.content response_text = response.choices[0].message.content
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
self._request_count += 1 self._request_count += 1
self._performance_metrics["avg_response_time"] = (
(self._performance_metrics["avg_response_time"] * (self._request_count - 1) + elapsed_time) if self._performance_metrics["avg_response_time"] == 0:
/ self._request_count self._performance_metrics["avg_response_time"] = elapsed_time
) else:
self._performance_metrics["avg_response_time"] = (
(self._performance_metrics["avg_response_time"] * (self._request_count - 1) + elapsed_time)
/ self._request_count
)
self.last_response = { self.last_response = {
"timestamp": dt_util.utcnow().isoformat(), "timestamp": dt_util.utcnow().isoformat(),
"question": message, "question": message,
"response": response_text, "response": response_text,
"model": model,
"instance": self.instance_name, # Добавляем идентификатор интеграции
"error": None "error": None
} }
@@ -620,6 +198,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
if len(self._history) > self._max_history_size: if len(self._history) > self._max_history_size:
self._history.pop(0) self._history.pop(0)
self.endpoint_status = "ready"
self._is_processing = False self._is_processing = False
self.async_update_listeners() self.async_update_listeners()
@@ -634,18 +213,56 @@ class HATextAICoordinator(DataUpdateCoordinator):
"timestamp": dt_util.utcnow().isoformat(), "timestamp": dt_util.utcnow().isoformat(),
"question": message, "question": message,
"response": None, "response": None,
"instance": self.instance_name, # Добавляем идентификатор интеграции
"error": str(err) "error": str(err)
} }
self.endpoint_status = "error"
self._is_processing = False self._is_processing = False
self.async_update_listeners() self.async_update_listeners()
raise raise
async def __aenter__(self): async def async_set_system_prompt(self, prompt: str) -> None:
"""Async enter.""" """Set the system prompt."""
await self.async_start() self._system_prompt = prompt
return self _LOGGER.debug("[%s] System prompt updated: %s", self.instance_name, prompt)
async def __aexit__(self, exc_type, exc_val, exc_tb): async def async_clear_history(self) -> None:
"""Async exit.""" """Clear conversation history."""
await self.async_stop() self._history.clear()
_LOGGER.debug("[%s] Conversation history cleared", self.instance_name)
self.async_update_listeners()
async def async_get_history(
self,
limit: Optional[int] = None,
filter_model: Optional[str] = None,
instance: Optional[str] = None # Добавляем фильтр по интеграции
) -> List[Dict[str, Any]]:
"""Get conversation history with optional filtering."""
history = self._history.copy()
if instance and instance != self.instance_name:
return [] # Возвращаем пустой список для чужих интеграций
if filter_model:
history = [h for h in history if h.get("model") == filter_model]
if limit and limit > 0:
history = history[-limit:]
return history
async def async_reset_metrics(self) -> None:
"""Reset all metrics."""
self._request_count = 0
self._error_count = 0
self._performance_metrics = {
"total_requests": 0,
"total_errors": 0,
"avg_response_time": 0.0,
"last_request_time": None,
"instance": self.instance_name, # Добавляем идентификатор интеграции
}
_LOGGER.debug("[%s] Metrics reset", self.instance_name)
self.async_update_listeners()
+38 -33
View File
@@ -57,19 +57,10 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self._config_entry = config_entry self._config_entry = config_entry
# Используем имя из конфигурации
self._name = config_entry.title self._name = config_entry.title
# Создаем уникальный ID используя имя
self._attr_unique_id = f"{config_entry.entry_id}_{slugify(self._name)}" self._attr_unique_id = f"{config_entry.entry_id}_{slugify(self._name)}"
# Создаем entity_id используя имя
self.entity_id = f"sensor.ha_text_ai_{slugify(self._name)}" self.entity_id = f"sensor.ha_text_ai_{slugify(self._name)}"
# Устанавливаем отображаемое имя
self._attr_name = self._name self._attr_name = self._name
self._current_state = STATE_INITIALIZING self._current_state = STATE_INITIALIZING
self._error_count = 0 self._error_count = 0
self._last_error = None self._last_error = None
@@ -80,7 +71,7 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
name=self._name, name=self._name,
manufacturer="Community", manufacturer="Community",
model=f"{coordinator.model} ({self._config_entry.data.get(CONF_API_PROVIDER, 'Unknown')} provider)", model=f"{coordinator.model} ({self._config_entry.data.get(CONF_API_PROVIDER, 'Unknown')} provider)",
sw_version=coordinator._api_version, sw_version=getattr(coordinator, 'api_version', 'v1'),
) )
@property @property
@@ -95,8 +86,14 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
@property @property
def state(self) -> StateType: def state(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self.coordinator.data: try:
return self.coordinator.data.get("last_update", self._current_state) if self.coordinator.data and "last_update" in self.coordinator.data:
timestamp = self.coordinator.data["last_update"]
if isinstance(timestamp, str):
return dt_util.parse_datetime(timestamp)
return timestamp
except Exception as err:
_LOGGER.debug("Error parsing state: %s", err)
return self._current_state return self._current_state
@property @property
@@ -112,17 +109,22 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
if self.coordinator.data: if self.coordinator.data:
data = self.coordinator.data data = self.coordinator.data
if "metrics" in data:
# Обновляем метрики если они есть
if "metrics" in data and isinstance(data["metrics"], dict):
attributes.update({"metrics": data["metrics"]}) attributes.update({"metrics": data["metrics"]})
# Обновляем статус API
if "status" in data: if "status" in data:
attributes[ATTR_API_STATUS] = data["status"] attributes[ATTR_API_STATUS] = data["status"]
if "last_response" in data:
last_response = data["last_response"] # Обновляем информацию о последнем ответе
if last_response: last_response = data.get("last_response", {})
attributes.update({ if isinstance(last_response, dict):
ATTR_RESPONSE: last_response.get("response", ""), attributes.update({
ATTR_QUESTION: last_response.get("question", ""), ATTR_RESPONSE: last_response.get("response", ""),
}) ATTR_QUESTION: last_response.get("question", ""),
})
return attributes return attributes
@@ -130,20 +132,23 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
try: try:
data = self.coordinator.data data = self.coordinator.data
if data: if not data:
if data.get("is_processing"):
self._current_state = STATE_PROCESSING
elif data.get("is_rate_limited"):
self._current_state = STATE_RATE_LIMITED
elif data.get("is_maintenance"):
self._current_state = STATE_MAINTENANCE
else:
self._current_state = STATE_READY
if "metrics" in data:
self._error_count = data["metrics"].get("total_errors", 0)
else:
self._current_state = STATE_DISCONNECTED self._current_state = STATE_DISCONNECTED
return
# Определяем текущее состояние
if data.get("is_processing"):
self._current_state = STATE_PROCESSING
elif data.get("is_rate_limited"):
self._current_state = STATE_RATE_LIMITED
elif data.get("is_maintenance"):
self._current_state = STATE_MAINTENANCE
else:
self._current_state = STATE_READY
# Обновляем счетчик ошибок из метрик
if "metrics" in data and isinstance(data["metrics"], dict):
self._error_count = data["metrics"].get("total_errors", self._error_count)
except Exception as err: except Exception as err:
_LOGGER.error("Error handling update: %s", err, exc_info=True) _LOGGER.error("Error handling update: %s", err, exc_info=True)
+83 -35
View File
@@ -3,23 +3,36 @@ ask_question:
description: >- description: >-
Send a question to the AI model and receive a detailed response. 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. The response will be stored in the conversation history and can be retrieved later.
target:
entity:
domain: sensor
integration: ha_text_ai
fields: fields:
instance:
name: Instance
description: Name of the HA Text AI instance to use
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
question: question:
name: Question name: Question
description: Your question or prompt for the AI assistant. description: Your question or prompt for the AI assistant
required: true required: true
selector: selector:
text: text:
multiline: true multiline: true
type: text type: text
system_prompt:
name: System Prompt
description: Optional system prompt to set context for this specific question
required: false
selector:
text:
multiline: true
model: model:
name: Model name: Model
description: "Select AI model to use (optional, overrides default setting)." description: "Select AI model to use (optional, overrides default setting)"
required: false required: false
selector: selector:
text: text:
@@ -27,7 +40,7 @@ ask_question:
temperature: temperature:
name: Temperature name: Temperature
description: Controls response creativity (0.0-2.0). description: Controls response creativity (0.0-2.0)
required: false required: false
default: 0.7 default: 0.7
selector: selector:
@@ -39,7 +52,7 @@ ask_question:
max_tokens: max_tokens:
name: Max Tokens name: Max Tokens
description: Maximum length of the response (1-4096 tokens). description: Maximum length of the response (1-4096 tokens)
required: false required: false
default: 1000 default: 1000
selector: selector:
@@ -49,34 +62,35 @@ 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.
required: false
selector:
text:
multiline: true
clear_history: clear_history:
name: Clear History name: Clear History
description: Delete all stored questions and responses from the conversation history. description: Delete all stored questions and responses from the conversation history
target: fields:
entity: instance:
domain: sensor name: Instance
integration: ha_text_ai description: Name of the HA Text AI instance to clear history for
fields: {} required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
get_history: get_history:
name: Get History name: Get History
description: Retrieve recent conversation history. description: Retrieve conversation history with optional filtering and sorting
target:
entity:
domain: sensor
integration: ha_text_ai
fields: fields:
instance:
name: Instance
description: Name of the HA Text AI instance to get history from
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
limit: limit:
name: Limit name: Limit
description: Number of most recent conversations to return (1-100). description: Number of conversations to return (1-100)
required: false required: false
default: 10 default: 10
selector: selector:
@@ -86,24 +100,58 @@ get_history:
step: 1 step: 1
mode: box mode: box
filter_model:
name: Filter Model
description: Filter conversations by specific AI model
required: false
selector:
text:
multiline: false
start_date: start_date:
name: Start Date name: Start Date
description: Optional start date for filtering history. description: Filter conversations starting from this date/time
required: false required: false
selector: selector:
datetime: datetime:
include_metadata:
name: Include Metadata
description: Include additional information like tokens used, response time, etc.
required: false
default: false
selector:
boolean:
sort_order:
name: Sort Order
description: Sort order for results (newest or oldest first)
required: false
default: "desc"
selector:
select:
options:
- label: "Newest First"
value: "desc"
- label: "Oldest First"
value: "asc"
set_system_prompt: set_system_prompt:
name: Set System Prompt name: Set System Prompt
description: Set default system behavior instructions for all future conversations. description: Set default system behavior instructions for all future conversations
target:
entity:
domain: sensor
integration: ha_text_ai
fields: fields:
instance:
name: Instance
description: Name of the HA Text AI instance to set system prompt for
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
prompt: prompt:
name: System Prompt name: System Prompt
description: Instructions that define how the AI should behave and respond. description: Instructions that define how the AI should behave and respond
required: true required: true
selector: selector:
text: text:
@@ -1,49 +1,51 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "provider": {
"title": "Set up HA Text AI", "title": "Select AI Provider",
"description": "Configure your AI integration with OpenAI or Anthropic providers.", "description": "Choose which AI service provider to use for this instance",
"data": { "data": {
"api_provider": "Select API Provider", "api_provider": "API Provider"
"api_key": "API key for authentication (required)", }
"model": "AI model to use (provider-specific)", },
"temperature": "Response creativity (0-2, lower = more focused and consistent)", "user": {
"title": "Configure HA Text AI Instance",
"description": "Set up a new AI assistant instance with your selected provider",
"data": {
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
"api_key": "API key for authentication",
"model": "AI model to use",
"temperature": "Response creativity (0-2, lower = more focused)",
"max_tokens": "Maximum response length (1-4096 tokens)", "max_tokens": "Maximum response length (1-4096 tokens)",
"api_endpoint": "Custom API endpoint URL (optional)", "api_endpoint": "Custom API endpoint URL (optional)",
"request_interval": "Minimum time between requests in seconds (0.1-60)", "request_interval": "Minimum time between requests (0.1-60 seconds)"
"name": "Name for this integration instance (e.g., 'GPT Assistant', 'Claude Helper')"
} }
} }
}, },
"error": { "error": {
"invalid_name": "Invalid integration name", "name_exists": "An instance with this name already exists",
"invalid_name": "Invalid instance name",
"invalid_auth": "Authentication failed - check your API key", "invalid_auth": "Authentication failed - check your API key",
"invalid_api_key": "Invalid API key - please verify your credentials", "invalid_api_key": "Invalid API key - please verify your credentials",
"cannot_connect": "Connection failed - check endpoint and network status", "cannot_connect": "Failed to connect to API service",
"invalid_model": "Model unavailable or not supported by the selected provider", "invalid_model": "Selected model is not available",
"rate_limit": "Rate limit exceeded - please reduce request frequency", "rate_limit": "Rate limit exceeded",
"api_error": "API service error - check provider status", "api_error": "API service error occurred",
"timeout": "Request timeout - server not responding", "timeout": "Request timed out",
"queue_full": "Request queue full - try again later", "invalid_instance": "Invalid instance specified",
"invalid_url_format": "Invalid API endpoint URL format", "unknown": "Unexpected error occurred"
"invalid_input": "Invalid configuration parameters",
"invalid_parameters": "Invalid service parameters provided",
"service_unavailable": "Service temporarily unavailable",
"context_length": "Maximum context length exceeded",
"unknown": "Unexpected error - check logs for details"
} }
}, },
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "HA Text AI Settings", "title": "Update Instance Settings",
"description": "Adjust your AI integration parameters", "description": "Modify settings for this AI assistant instance",
"data": { "data": {
"model": "Select AI model (provider-specific)", "model": "AI model",
"temperature": "Response creativity (0-2)", "temperature": "Response creativity (0-2)",
"max_tokens": "Maximum response length in tokens (1-4096)", "max_tokens": "Maximum response length (1-4096)",
"request_interval": "Minimum seconds between requests (0.1-60)" "request_interval": "Minimum request interval (0.1-60 seconds)"
} }
} }
} }
@@ -51,63 +53,85 @@
"services": { "services": {
"ask_question": { "ask_question": {
"name": "Ask Question", "name": "Ask Question",
"description": "Send a question to the AI model and get a detailed response.", "description": "Send a question to the specified AI instance",
"fields": { "fields": {
"instance": {
"name": "Instance",
"description": "The HA Text AI instance to use"
},
"question": { "question": {
"name": "Question", "name": "Question",
"description": "Your question or prompt for the AI model" "description": "Your question or prompt"
}, },
"system_prompt": { "system_prompt": {
"name": "System Prompt", "name": "System Prompt",
"description": "Optional context or instructions for this specific question" "description": "Optional context for this question"
}, },
"model": { "model": {
"name": "Model", "name": "Model",
"description": "Optional specific AI model for this request" "description": "Override default model for this request"
}, },
"temperature": { "temperature": {
"name": "Temperature", "name": "Temperature",
"description": "Optional creativity setting (0-2)" "description": "Override default creativity setting"
}, },
"max_tokens": { "max_tokens": {
"name": "Max Tokens", "name": "Max Tokens",
"description": "Optional maximum response length (1-4096 tokens)" "description": "Override default response length limit"
} }
} }
}, },
"clear_history": { "clear_history": {
"name": "Clear History", "name": "Clear History",
"description": "Delete all stored questions and responses from the conversation history." "description": "Delete conversation history for specified instance",
"fields": {
"instance": {
"name": "Instance",
"description": "The HA Text AI instance to clear history for"
}
}
}, },
"get_history": { "get_history": {
"name": "Get History", "name": "Get History",
"description": "Retrieve recent conversation history with optional filtering.", "description": "Retrieve conversation history for specified instance",
"fields": { "fields": {
"instance": {
"name": "Instance",
"description": "The HA Text AI instance to get history from"
},
"limit": { "limit": {
"name": "Limit", "name": "Limit",
"description": "Maximum number of entries to return (1-100)" "description": "Maximum number of entries (1-100)"
},
"filter_model": {
"name": "Filter Model",
"description": "Filter by specific AI model"
}, },
"start_date": { "start_date": {
"name": "Start Date", "name": "Start Date",
"description": "Optional date to filter history from" "description": "Filter from specific date/time"
}, },
"include_metadata": { "include_metadata": {
"name": "Include Metadata", "name": "Include Metadata",
"description": "Include additional response metadata" "description": "Include additional response information"
}, },
"sort_order": { "sort_order": {
"name": "Sort Order", "name": "Sort Order",
"description": "Order of results (asc/desc)" "description": "Sort order (newest/oldest first)"
} }
} }
}, },
"set_system_prompt": { "set_system_prompt": {
"name": "Set System Prompt", "name": "Set System Prompt",
"description": "Set default system behavior instructions for all future conversations.", "description": "Set default behavior for specified instance",
"fields": { "fields": {
"instance": {
"name": "Instance",
"description": "The HA Text AI instance to configure"
},
"prompt": { "prompt": {
"name": "System Prompt", "name": "System Prompt",
"description": "Instructions that define how the AI should behave" "description": "Default behavior instructions"
} }
} }
} }
@@ -117,16 +141,13 @@
"ha_text_ai": { "ha_text_ai": {
"name": "{name}", "name": "{name}",
"state": { "state": {
"initializing": "Initializing",
"ready": "Ready", "ready": "Ready",
"processing": "Processing", "processing": "Processing",
"error": "Error", "error": "Error",
"disconnected": "Disconnected", "disconnected": "Disconnected",
"rate_limited": "Rate Limited", "rate_limited": "Rate Limited",
"maintenance": "Maintenance",
"retrying": "Retrying", "retrying": "Retrying",
"queued": "Queued", "queued": "Queued"
"updating": "Updating"
}, },
"state_attributes": { "state_attributes": {
"question": { "question": {
@@ -135,11 +156,8 @@
"response": { "response": {
"name": "Last Response" "name": "Last Response"
}, },
"last_updated": {
"name": "Last Updated"
},
"model": { "model": {
"name": "Model" "name": "Current Model"
}, },
"temperature": { "temperature": {
"name": "Temperature" "name": "Temperature"
@@ -147,20 +165,14 @@
"max_tokens": { "max_tokens": {
"name": "Max Tokens" "name": "Max Tokens"
}, },
"total_responses": {
"name": "Total Responses"
},
"system_prompt": { "system_prompt": {
"name": "System Prompt" "name": "System Prompt"
}, },
"response_time": { "response_time": {
"name": "Response Time" "name": "Last Response Time"
}, },
"queue_size": { "total_responses": {
"name": "Queue Size" "name": "Total Responses"
},
"api_status": {
"name": "API Status"
}, },
"error_count": { "error_count": {
"name": "Error Count" "name": "Error Count"
@@ -168,11 +180,11 @@
"last_error": { "last_error": {
"name": "Last Error" "name": "Last Error"
}, },
"tokens_used": { "api_status": {
"name": "Tokens Used" "name": "API Status"
}, },
"request_count": { "tokens_used": {
"name": "Request Count" "name": "Total Tokens Used"
} }
} }
} }