From b05afe1085fbb3e21e45791d20ff95b83d6d19cc Mon Sep 17 00:00:00 2001 From: SMKRV Date: Tue, 26 Nov 2024 02:08:55 +0300 Subject: [PATCH] Release v2.0.0-alpha --- ha_text_ai/__init__.py | 295 ------------------------- ha_text_ai/api_client.py | 180 --------------- ha_text_ai/config_flow.py | 247 --------------------- ha_text_ai/const.py | 201 ----------------- ha_text_ai/coordinator.py | 377 -------------------------------- ha_text_ai/manifest.json | 28 --- ha_text_ai/sensor.py | 294 ------------------------- ha_text_ai/services.yaml | 75 ------- ha_text_ai/translations/de.json | 261 ---------------------- ha_text_ai/translations/en.json | 260 ---------------------- ha_text_ai/translations/ru.json | 260 ---------------------- 11 files changed, 2478 deletions(-) delete mode 100644 ha_text_ai/__init__.py delete mode 100644 ha_text_ai/api_client.py delete mode 100644 ha_text_ai/config_flow.py delete mode 100644 ha_text_ai/const.py delete mode 100644 ha_text_ai/coordinator.py delete mode 100644 ha_text_ai/manifest.json delete mode 100644 ha_text_ai/sensor.py delete mode 100644 ha_text_ai/services.yaml delete mode 100644 ha_text_ai/translations/de.json delete mode 100644 ha_text_ai/translations/en.json delete mode 100644 ha_text_ai/translations/ru.json diff --git a/ha_text_ai/__init__.py b/ha_text_ai/__init__.py deleted file mode 100644 index 5c639e0..0000000 --- a/ha_text_ai/__init__.py +++ /dev/null @@ -1,295 +0,0 @@ -"""The HA Text AI integration.""" -from __future__ import annotations - -import logging -import os -import shutil -from datetime import datetime, timedelta -from typing import Any, Dict - -import voluptuous as vol -from async_timeout import timeout - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import aiohttp_client - -from .coordinator import HATextAICoordinator -from .api_client import APIClient -from .const import ( - DOMAIN, - PLATFORMS, - CONF_MODEL, - CONF_TEMPERATURE, - CONF_MAX_TOKENS, - CONF_API_ENDPOINT, - CONF_REQUEST_INTERVAL, - CONF_API_PROVIDER, - CONF_CONTEXT_MESSAGES, - API_PROVIDER_OPENAI, - API_PROVIDER_ANTHROPIC, - DEFAULT_MODEL, - DEFAULT_TEMPERATURE, - DEFAULT_MAX_TOKENS, - DEFAULT_OPENAI_ENDPOINT, - DEFAULT_ANTHROPIC_ENDPOINT, - DEFAULT_REQUEST_INTERVAL, - DEFAULT_CONTEXT_MESSAGES, - API_TIMEOUT, - SERVICE_ASK_QUESTION, - SERVICE_CLEAR_HISTORY, - SERVICE_GET_HISTORY, - SERVICE_SET_SYSTEM_PROMPT, -) - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) - -SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({ - vol.Required("instance"): cv.string, - vol.Required("question"): cv.string, - vol.Optional("system_prompt"): cv.string, - vol.Optional("model"): cv.string, - vol.Optional("temperature"): cv.positive_float, - vol.Optional("max_tokens"): cv.positive_int, - vol.Optional("context_messages"): 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, -}) - -def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator: - """Get coordinator by instance name.""" - if instance.startswith("sensor."): - instance = instance.replace("sensor.ha_text_ai_", "", 1) - - for entry_id, coord in hass.data[DOMAIN].items(): - if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower(): - return coord - - raise HomeAssistantError(f"Instance {instance} not found") - -async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: - """Set up the HA Text AI component.""" - hass.data.setdefault(DOMAIN, {}) - - try: - source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg') - dest_dir = os.path.join(hass.config.path('www'), 'icons') - os.makedirs(dest_dir, exist_ok=True) - dest = os.path.join(dest_dir, 'icon.png') - if not os.path.exists(dest): - shutil.copyfile(source, dest) - except Exception as ex: - _LOGGER.warning("Failed to copy custom icon: %s", str(ex)) - - async def async_ask_question(call: ServiceCall) -> None: - """Handle ask_question service.""" - try: - coordinator = get_coordinator_by_instance(hass, call.data["instance"]) - await coordinator.async_ask_question( - question=call.data["question"], - model=call.data.get("model"), - temperature=call.data.get("temperature"), - max_tokens=call.data.get("max_tokens"), - system_prompt=call.data.get("system_prompt"), - context_messages=call.data.get("context_messages"), - ) - except Exception as err: - _LOGGER.error("Error asking question: %s", str(err)) - raise HomeAssistantError(f"Failed to process question: {str(err)}") - - async def async_clear_history(call: ServiceCall) -> None: - """Handle clear_history service.""" - try: - coordinator = get_coordinator_by_instance(hass, call.data["instance"]) - await coordinator.async_clear_history() - except Exception as err: - _LOGGER.error("Error clearing history: %s", str(err)) - raise HomeAssistantError(f"Failed to clear history: {str(err)}") - - async def async_get_history(call: ServiceCall) -> list: - """Handle get_history service.""" - try: - coordinator = get_coordinator_by_instance(hass, call.data["instance"]) - return await coordinator.async_get_history( - limit=call.data.get("limit"), - filter_model=call.data.get("filter_model") - ) - except Exception as err: - _LOGGER.error("Error getting history: %s", str(err)) - raise HomeAssistantError(f"Failed to get history: {str(err)}") - - async def async_set_system_prompt(call: ServiceCall) -> None: - """Handle set_system_prompt service.""" - try: - coordinator = get_coordinator_by_instance(hass, call.data["instance"]) - await coordinator.async_set_system_prompt(call.data["prompt"]) - except Exception as err: - _LOGGER.error("Error setting system prompt: %s", str(err)) - raise HomeAssistantError(f"Failed to set system prompt: {str(err)}") - - hass.services.async_register( - DOMAIN, - SERVICE_ASK_QUESTION, - async_ask_question, - schema=SERVICE_SCHEMA_ASK_QUESTION - ) - - hass.services.async_register( - DOMAIN, - SERVICE_CLEAR_HISTORY, - async_clear_history, - schema=vol.Schema({vol.Required("instance"): cv.string}) - ) - - hass.services.async_register( - DOMAIN, - SERVICE_GET_HISTORY, - async_get_history, - schema=SERVICE_SCHEMA_GET_HISTORY - ) - - hass.services.async_register( - DOMAIN, - SERVICE_SET_SYSTEM_PROMPT, - async_set_system_prompt, - schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT - ) - - return True - -async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool: - """Check API availability for different providers.""" - try: - if provider == API_PROVIDER_ANTHROPIC: - check_url = f"{endpoint}/v1/models" - 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 in [200, 404]: - return True - elif response.status == 401: - raise ConfigEntryNotReady("Invalid API key") - elif response.status == 429: - _LOGGER.warning("Rate limit exceeded during API check") - return False - else: - _LOGGER.error("API check failed with status: %d", response.status) - return False - except Exception as ex: - _LOGGER.error("API check error: %s", str(ex)) - return False - -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) - api_provider = entry.data.get(CONF_API_PROVIDER) - model = entry.data.get(CONF_MODEL, DEFAULT_MODEL) - 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] - instance_name = entry.data.get(CONF_NAME, entry.entry_id) - is_anthropic = api_provider == API_PROVIDER_ANTHROPIC - - headers = { - "Content-Type": "application/json", - "Accept": "application/json" - } - - if is_anthropic: - headers["x-api-key"] = api_key - headers["anthropic-version"] = "2023-06-01" - else: - headers["Authorization"] = f"Bearer {api_key}" - - if not await async_check_api(session, endpoint, headers, api_provider): - raise ConfigEntryNotReady("API connection failed") - - _LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint) - - api_client = APIClient( - session=session, - endpoint=endpoint, - headers=headers, - api_provider=api_provider, - model=model, - ) - - coordinator = HATextAICoordinator( - hass=hass, - client=api_client, - model=model, - update_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL), - instance_name=instance_name, - max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS), - temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE), - is_anthropic=is_anthropic, - context_messages=entry.data.get( - CONF_CONTEXT_MESSAGES, - DEFAULT_CONTEXT_MESSAGES - ), - ) - - coordinator.data = coordinator._initial_state.copy() - _LOGGER.debug(f"Initial state set for coordinator {instance_name}") - - await coordinator.async_config_entry_first_refresh() - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - _LOGGER.info( - "Successfully set up %s instance '%s' with model %s", - api_provider, - instance_name, - model - ) - - return True - - except Exception as ex: - _LOGGER.exception("Setup error: %s", str(ex)) - raise ConfigEntryNotReady(f"Setup error: {str(ex)}") from ex - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - try: - if entry.entry_id in hass.data[DOMAIN]: - coordinator = hass.data[DOMAIN][entry.entry_id] - - if hasattr(coordinator.client, 'shutdown'): - await coordinator.client.shutdown() - - await coordinator.async_shutdown() - hass.data[DOMAIN].pop(entry.entry_id) - - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - except Exception as ex: - _LOGGER.exception("Error unloading entry: %s", str(ex)) - return False diff --git a/ha_text_ai/api_client.py b/ha_text_ai/api_client.py deleted file mode 100644 index 727af26..0000000 --- a/ha_text_ai/api_client.py +++ /dev/null @@ -1,180 +0,0 @@ -"""API Client for HA Text AI.""" -import logging -import asyncio -from typing import Any, Dict, List, Optional -from aiohttp import ClientSession, ClientTimeout -from async_timeout import timeout - -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from .const import ( - API_TIMEOUT, - API_RETRY_COUNT, - API_PROVIDER_ANTHROPIC, - MIN_TEMPERATURE, - MAX_TEMPERATURE, - MIN_MAX_TOKENS, - MAX_MAX_TOKENS, -) - -_LOGGER = logging.getLogger(__name__) - -class APIClient: - """API Client for OpenAI and Anthropic.""" - - def __init__( - self, - session: ClientSession, - endpoint: str, - headers: Dict[str, str], - api_provider: str, - model: str, - ) -> None: - """Initialize API client.""" - self.session = session - self.endpoint = endpoint - self.headers = headers - self.api_provider = api_provider - self.model = model - self.timeout = ClientTimeout(total=API_TIMEOUT) - - def _validate_parameters( - self, - temperature: float, - max_tokens: int, - ) -> None: - """Validate API parameters.""" - if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE: - raise ValueError( - f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}" - ) - if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS: - raise ValueError( - f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}" - ) - - async def _make_request( - self, - url: str, - payload: Dict[str, Any], - ) -> Dict[str, Any]: - """Make API request with retry logic.""" - for attempt in range(API_RETRY_COUNT): - try: - async with timeout(API_TIMEOUT): - async with self.session.post( - url, - json=payload, - headers=self.headers, - timeout=self.timeout - ) as response: - if response.status != 200: - error_data = await response.json() - raise HomeAssistantError(f"API error: {error_data}") - return await response.json() - except asyncio.TimeoutError: - if attempt == API_RETRY_COUNT - 1: - raise HomeAssistantError("API request timed out") - await asyncio.sleep(1 * (attempt + 1)) - except Exception as e: - if attempt == API_RETRY_COUNT - 1: - raise - _LOGGER.warning("API request failed, retrying: %s", str(e)) - await asyncio.sleep(1 * (attempt + 1)) - - async def create( - self, - model: str, - messages: List[Dict[str, str]], - temperature: float, - max_tokens: int, - ) -> Dict[str, Any]: - """Create completion using appropriate API.""" - try: - self._validate_parameters(temperature, max_tokens) - - if self.api_provider == API_PROVIDER_ANTHROPIC: - return await self._create_anthropic_completion( - model, messages, temperature, max_tokens - ) - else: - return await self._create_openai_completion( - model, messages, temperature, max_tokens - ) - except Exception as e: - _LOGGER.error("API request failed: %s", str(e)) - raise HomeAssistantError(f"API request failed: {str(e)}") - - async def _create_openai_completion( - self, - model: str, - messages: List[Dict[str, str]], - temperature: float, - max_tokens: int, - ) -> Dict[str, Any]: - """Create completion using OpenAI API.""" - url = f"{self.endpoint}/chat/completions" - payload = { - "model": model, - "messages": messages, - "temperature": temperature, - "max_tokens": max_tokens, - } - - data = await self._make_request(url, payload) - return { - "choices": [ - { - "message": { - "content": data["choices"][0]["message"]["content"] - } - } - ], - "usage": { - "prompt_tokens": data["usage"]["prompt_tokens"], - "completion_tokens": data["usage"]["completion_tokens"], - "total_tokens": data["usage"]["total_tokens"] - } - } - - async def _create_anthropic_completion( - self, - model: str, - messages: List[Dict[str, str]], - temperature: float, - max_tokens: int, - ) -> Dict[str, Any]: - """Create completion using Anthropic API.""" - url = f"{self.endpoint}/v1/messages" - - # Convert messages to Anthropic format - system_prompt = next( - (msg["content"] for msg in messages if msg["role"] == "system"), - None - ) - conversation = [msg for msg in messages if msg["role"] != "system"] - - payload = { - "model": model, - "messages": conversation, - "max_tokens": max_tokens, - "temperature": temperature, - } - if system_prompt: - payload["system"] = system_prompt - - data = await self._make_request(url, payload) - return { - "choices": [ - { - "message": { - "content": data["content"][0]["text"] - } - } - ], - "usage": { - "prompt_tokens": data["usage"]["input_tokens"], - "completion_tokens": data["usage"]["output_tokens"], - "total_tokens": data["usage"]["input_tokens"] + data["usage"]["output_tokens"] - } - } diff --git a/ha_text_ai/config_flow.py b/ha_text_ai/config_flow.py deleted file mode 100644 index b87f3d4..0000000 --- a/ha_text_ai/config_flow.py +++ /dev/null @@ -1,247 +0,0 @@ -"""Config flow for HA text AI integration.""" -import logging -from typing import Any, Dict, Optional - -import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers import selector - -from .const import ( - DOMAIN, - CONF_MODEL, - CONF_TEMPERATURE, - CONF_MAX_TOKENS, - CONF_API_ENDPOINT, - CONF_REQUEST_INTERVAL, - CONF_API_PROVIDER, - CONF_CONTEXT_MESSAGES, - API_PROVIDER_OPENAI, - API_PROVIDER_ANTHROPIC, - API_PROVIDERS, - DEFAULT_MODEL, - DEFAULT_TEMPERATURE, - DEFAULT_MAX_TOKENS, - DEFAULT_REQUEST_INTERVAL, - DEFAULT_OPENAI_ENDPOINT, - DEFAULT_ANTHROPIC_ENDPOINT, - DEFAULT_CONTEXT_MESSAGES, - MIN_TEMPERATURE, - MAX_TEMPERATURE, - MIN_MAX_TOKENS, - MAX_MAX_TOKENS, - MIN_REQUEST_INTERVAL, -) - -_LOGGER = logging.getLogger(__name__) - -class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for HA text AI.""" - - VERSION = 1 - - def __init__(self) -> None: - """Initialize flow.""" - self._errors = {} - self._data = {} - self._provider = None - - async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult: - """Handle the initial step.""" - if user_input is None: - 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" - ) - ), - }) - ) - - self._provider = user_input[CONF_API_PROVIDER] - return await self.async_step_provider() - - 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 - ) - - suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}" - - 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) - ), - vol.Optional( - CONF_CONTEXT_MESSAGES, - default=DEFAULT_CONTEXT_MESSAGES - ): vol.All( - vol.Coerce(int), - vol.Range(min=1, max=20) - ), - }), - 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() - - 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: - session = async_get_clientsession(self.hass) - headers = self._get_api_headers(user_input) - endpoint = user_input[CONF_API_ENDPOINT].rstrip('/') - - check_url = ( - f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC - else f"{endpoint}/models" - ) - - async with session.get(check_url, headers=headers) as response: - if response.status == 401: - self._errors["base"] = "invalid_auth" - return False - elif response.status not in [200, 404]: - self._errors["base"] = "cannot_connect" - return False - return True - - except Exception as err: - _LOGGER.error("API validation error: %s", str(err)) - self._errors["base"] = "cannot_connect" - return False - - def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]: - """Get API headers based on provider.""" - api_key = user_input[CONF_API_KEY] - - if self._provider == API_PROVIDER_ANTHROPIC: - return { - "x-api-key": api_key, - "anthropic-version": "2023-06-01", - "Content-Type": "application/json" - } - return { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json" - } - - async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult: - """Create the config entry.""" - instance_name = user_input[CONF_NAME] - unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_") - - return self.async_create_entry( - title=instance_name, - data={ - CONF_API_PROVIDER: self._provider, - CONF_NAME: instance_name, - **user_input, - "unique_id": unique_id, - CONF_CONTEXT_MESSAGES: user_input.get( - CONF_CONTEXT_MESSAGES, - DEFAULT_CONTEXT_MESSAGES) - } - ) - - @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): - """Handle options flow.""" - - 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: - """Manage the options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - current_data = {**self.config_entry.data, **self.config_entry.options} - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema({ - vol.Optional( - CONF_MODEL, - default=current_data.get(CONF_MODEL, DEFAULT_MODEL) - ): str, - 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) - ), - vol.Optional( - CONF_CONTEXT_MESSAGES, - default=current_data.get( - CONF_CONTEXT_MESSAGES, - DEFAULT_CONTEXT_MESSAGES - ) - ): vol.All( - vol.Coerce(int), - vol.Range(min=1, max=20) - ), - }) - ) diff --git a/ha_text_ai/const.py b/ha_text_ai/const.py deleted file mode 100644 index ccd41f2..0000000 --- a/ha_text_ai/const.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Constants for the HA text AI integration.""" -from typing import Final -import voluptuous as vol -from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME -from homeassistant.helpers import config_validation as cv - -# Domain and platforms -DOMAIN: Final = "ha_text_ai" -PLATFORMS: Final = [Platform.SENSOR] - -# Provider configuration -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" - -# Configuration constants -CONF_MODEL: Final = "model" -CONF_TEMPERATURE: Final = "temperature" -CONF_MAX_TOKENS: Final = "max_tokens" -CONF_API_ENDPOINT: Final = "api_endpoint" -CONF_REQUEST_INTERVAL: Final = "request_interval" -CONF_INSTANCE: Final = "instance" -CONF_MAX_HISTORY_SIZE: Final = "max_history_size" -CONF_IS_ANTHROPIC: Final = "is_anthropic" -CONF_CONTEXT_MESSAGES: Final = "context_messages" - -# Default values -DEFAULT_MODEL: Final = "gpt-4o-mini" -DEFAULT_TEMPERATURE: Final = 0.1 -DEFAULT_MAX_TOKENS: Final = 1000 -DEFAULT_REQUEST_INTERVAL: Final = 1.0 -DEFAULT_TIMEOUT: Final = 30 -DEFAULT_MAX_HISTORY: Final = 50 -DEFAULT_NAME: Final = "HA Text AI" -DEFAULT_CONTEXT_MESSAGES: Final = 5 - -# Parameter constraints -MIN_TEMPERATURE: Final = 0.0 -MAX_TEMPERATURE: Final = 2.0 -MIN_MAX_TOKENS: Final = 1 -MAX_MAX_TOKENS: Final = 4096 -MIN_REQUEST_INTERVAL: Final = 0.1 -MAX_REQUEST_INTERVAL: Final = 60.0 - -# API constants -API_TIMEOUT: Final = 30 -API_RETRY_COUNT: Final = 3 - -# Service names -SERVICE_ASK_QUESTION: Final = "ask_question" -SERVICE_CLEAR_HISTORY: Final = "clear_history" -SERVICE_GET_HISTORY: Final = "get_history" -SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt" - -# Attribute keys -ATTR_QUESTION: Final = "question" -ATTR_RESPONSE: Final = "response" -ATTR_INSTANCE: Final = "instance" -ATTR_MODEL: Final = "model" -ATTR_TEMPERATURE: Final = "temperature" -ATTR_MAX_TOKENS: Final = "max_tokens" -ATTR_SYSTEM_PROMPT: Final = "system_prompt" -ATTR_API_STATUS: Final = "api_status" -ATTR_ERROR_COUNT: Final = "error_count" -ATTR_CONVERSATION_HISTORY: Final = "conversation_history" - -# Sensor attributes -ATTR_TOTAL_RESPONSES: Final = "total_responses" -ATTR_TOTAL_ERRORS: Final = "total_errors" -ATTR_AVG_RESPONSE_TIME: Final = "average_response_time" -ATTR_LAST_REQUEST_TIME: Final = "last_request_time" -ATTR_LAST_ERROR: Final = "last_error" -ATTR_IS_PROCESSING: Final = "is_processing" -ATTR_IS_RATE_LIMITED: Final = "is_rate_limited" -ATTR_IS_MAINTENANCE: Final = "is_maintenance" -ATTR_API_VERSION: Final = "api_version" -ATTR_ENDPOINT_STATUS: Final = "endpoint_status" -ATTR_PERFORMANCE_METRICS: Final = "performance_metrics" -ATTR_HISTORY_SIZE: Final = "history_size" -ATTR_UPTIME: Final = "uptime" -ATTR_API_PROVIDER: Final = "api_provider" -ATTR_METRICS: Final = "metrics" -ATTR_STATE: Final = "state" -ATTR_LAST_RESPONSE: Final = "last_response" -ATTR_ERROR: Final = "error" -ATTR_TIMESTAMP: Final = "timestamp" - -# Sensor metrics -METRIC_TOTAL_TOKENS: Final = "total_tokens" -METRIC_PROMPT_TOKENS: Final = "prompt_tokens" -METRIC_COMPLETION_TOKENS: Final = "completion_tokens" -METRIC_SUCCESSFUL_REQUESTS: Final = "successful_requests" -METRIC_FAILED_REQUESTS: Final = "failed_requests" -METRIC_AVERAGE_LATENCY: Final = "average_latency" -METRIC_MAX_LATENCY: Final = "max_latency" -METRIC_MIN_LATENCY: Final = "min_latency" - -# Error messages -ERROR_INVALID_API_KEY: Final = "invalid_api_key" -ERROR_CANNOT_CONNECT: Final = "cannot_connect" -ERROR_UNKNOWN: Final = "unknown_error" -ERROR_INVALID_MODEL: Final = "invalid_model" -ERROR_RATE_LIMIT: Final = "rate_limit_exceeded" -ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded" -ERROR_API_ERROR: Final = "api_error" -ERROR_TIMEOUT: Final = "timeout_error" -ERROR_INVALID_INSTANCE: Final = "invalid_instance" -ERROR_NAME_EXISTS: Final = "name_exists" - -# 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" -STATE_INITIALIZING: Final = "initializing" -STATE_MAINTENANCE: Final = "maintenance" -STATE_RATE_LIMITED: Final = "rate_limited" -STATE_DISCONNECTED: Final = "disconnected" - -# Event names -EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received" -EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred" -EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed" - -# Service schema constants -SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({ - vol.Required(CONF_INSTANCE): cv.string, - vol.Required("question"): cv.string, - vol.Optional("system_prompt"): cv.string, - vol.Optional("model"): cv.string, - vol.Optional("temperature"): vol.All( - vol.Coerce(float), - vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE) - ), - vol.Optional("max_tokens"): vol.All( - vol.Coerce(int), - vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS) - ), - vol.Optional("context_messages"): vol.All( - vol.Coerce(int), - vol.Range(min=1, max=20) - ) -}) - -SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({ - vol.Required(CONF_INSTANCE): cv.string, - vol.Required("prompt"): cv.string -}) - -SERVICE_SCHEMA_GET_HISTORY = vol.Schema({ - vol.Required(CONF_INSTANCE): cv.string, - vol.Optional("limit", default=10): vol.All( - vol.Coerce(int), - vol.Range(min=1, max=100) - ), - vol.Optional("filter_model"): cv.string -}) - -# Configuration schema -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_NAME): 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_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_API_ENDPOINT): cv.string, - vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All( - vol.Coerce(float), - vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL) - ), - vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All( - vol.Coerce(int), - vol.Range(min=1, max=100), - ), - vol.Optional(CONF_CONTEXT_MESSAGES, default=DEFAULT_CONTEXT_MESSAGES): vol.All( - vol.Coerce(int), - vol.Range(min=1, max=20) - ) - }) -}, extra=vol.ALLOW_EXTRA) diff --git a/ha_text_ai/coordinator.py b/ha_text_ai/coordinator.py deleted file mode 100644 index 9344e5f..0000000 --- a/ha_text_ai/coordinator.py +++ /dev/null @@ -1,377 +0,0 @@ -"""The HA Text AI coordinator.""" -from __future__ import annotations - -import logging -from datetime import datetime, timedelta -from typing import Any, Dict, List, Optional - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util import dt as dt_util -from homeassistant.exceptions import HomeAssistantError - -from .const import ( - DOMAIN, - STATE_READY, - STATE_PROCESSING, - STATE_ERROR, - STATE_RATE_LIMITED, - STATE_MAINTENANCE, - DEFAULT_MAX_TOKENS, - DEFAULT_TEMPERATURE, - DEFAULT_MAX_HISTORY, - DEFAULT_CONTEXT_MESSAGES, -) - -_LOGGER = logging.getLogger(__name__) - -class HATextAICoordinator(DataUpdateCoordinator): - def __init__( - self, - hass: HomeAssistant, - client: Any, - model: str, - update_interval: int, - instance_name: str, - max_tokens: int = DEFAULT_MAX_TOKENS, - temperature: float = DEFAULT_TEMPERATURE, - max_history_size: int = DEFAULT_MAX_HISTORY, - context_messages: int = DEFAULT_CONTEXT_MESSAGES, - is_anthropic: bool = False, - ) -> None: - """Initialize coordinator.""" - self.instance_name = instance_name - self.hass = hass - self.client = client - self.model = model - self.temperature = temperature - self.max_tokens = max_tokens - self.max_history_size = max_history_size - self.is_anthropic = is_anthropic - - # Initialize with default state - self._initial_state = { - "state": STATE_READY, - "metrics": { - "total_tokens": 0, - "prompt_tokens": 0, - "completion_tokens": 0, - "successful_requests": 0, - "failed_requests": 0, - "total_errors": 0, - "average_latency": 0, - "max_latency": 0, - "min_latency": float('inf'), - }, - "last_response": { - "timestamp": dt_util.utcnow().isoformat(), - "question": "", - "response": "", - "model": model, - "instance": instance_name, - "error": None - }, - "is_processing": False, - "is_rate_limited": False, - "is_maintenance": False, - "endpoint_status": "ready", - "uptime": 0, - "system_prompt": None, - "history_size": 0, - "conversation_history": [], - } - - update_interval_td = timedelta(seconds=update_interval) - - super().__init__( - hass, - _LOGGER, - name=instance_name, - update_interval=update_interval_td, - ) - - # Register instance - self.hass.data.setdefault(DOMAIN, {}) - self.hass.data[DOMAIN][instance_name] = self - self.context_messages = context_messages - - self._system_prompt = None - self._conversation_history = [] - self._performance_metrics = self._initial_state["metrics"].copy() - self._is_processing = False - self._is_rate_limited = False - self._is_maintenance = False - self.endpoint_status = "ready" - self.last_response = self._initial_state["last_response"].copy() - self._start_time = dt_util.utcnow() - - _LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}") - - async def _async_update_data(self) -> Dict[str, Any]: - """Update data via library.""" - try: - current_state = self._get_current_state() - _LOGGER.debug(f"Updating data for {self.instance_name}, current state: {current_state}") - - data = { - "state": current_state, - "metrics": self._performance_metrics, - "last_response": self.last_response, - "is_processing": self._is_processing, - "is_rate_limited": self._is_rate_limited, - "is_maintenance": self._is_maintenance, - "endpoint_status": self.endpoint_status, - "uptime": (dt_util.utcnow() - self._start_time).total_seconds(), - "system_prompt": self._system_prompt, - "history_size": len(self._conversation_history), - "conversation_history": self._conversation_history, - } - - # Validate data - if not isinstance(data, dict): - raise ValueError("Invalid data format") - - _LOGGER.debug(f"Updated data for {self.instance_name}: {data}") - return data - - except Exception as err: - _LOGGER.error(f"Error updating data for {self.instance_name}: {err}") - return self._initial_state - - async def async_update_ha_state(self) -> None: - """Update Home Assistant state.""" - try: - _LOGGER.debug(f"Requesting state update for {self.instance_name}") - await self.async_request_refresh() - - # Force update of all entities - for entity_id in self.hass.states.async_entity_ids(): - if entity_id.startswith(f"sensor.ha_text_ai_{self.instance_name}"): - self.hass.states.async_set(entity_id, self._get_current_state()) - - except Exception as err: - _LOGGER.error(f"Error updating HA state for {self.instance_name}: {err}") - - def _get_current_state(self) -> str: - """Get current state based on internal flags.""" - if self._is_processing: - return STATE_PROCESSING - elif self._is_rate_limited: - return STATE_RATE_LIMITED - elif self._is_maintenance: - return STATE_MAINTENANCE - elif self.last_response.get("error"): - return STATE_ERROR - return STATE_READY - - def _calculate_context_tokens(self, messages: List[Dict[str, str]]) -> int: - try: - if self.is_anthropic and hasattr(self.client, 'count_tokens'): - return sum(self.client.count_tokens(msg['content']) for msg in messages) - - return sum(len(msg['content']) // 4 for msg in messages) - except Exception as e: - _LOGGER.warning(f"Error calculating context tokens: {e}") - return 0 - - async def async_ask_question( - self, - question: str, - model: Optional[str] = None, - temperature: Optional[float] = None, - max_tokens: Optional[int] = None, - system_prompt: Optional[str] = None, - context_messages: Optional[int] = None, - ) -> dict: - """Process a question with optional parameters.""" - return await self.async_process_question( - question, model, temperature, max_tokens, system_prompt, context_messages - ) - - async def async_process_question( - self, - question: str, - model: Optional[str] = None, - temperature: Optional[float] = None, - max_tokens: Optional[int] = None, - system_prompt: Optional[str] = None, - context_messages: Optional[int] = None, - ) -> dict: - temp_context_messages = context_messages or self.context_messages - - if not question: - raise ValueError("Question cannot be empty") - - _LOGGER.debug(f"Processing question for instance {self.instance_name}") - - try: - self._is_processing = True - await self.async_update_ha_state() - - 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 - - start_time = dt_util.utcnow() - - messages = [] - if temp_system_prompt: - if self.is_anthropic: - system_content = f"\n\nHuman: {temp_system_prompt}\n\nAssistant: I understand and will follow these instructions." - messages.append({"role": "user", "content": system_content}) - else: - messages.append({"role": "system", "content": temp_system_prompt}) - - # Add conversation history - context_history = self._conversation_history[-temp_context_messages:] - for entry in context_history: - messages.append({"role": "user", "content": entry["question"]}) - messages.append({"role": "assistant", "content": entry["response"]}) - - messages.append({"role": "user", "content": question}) - - kwargs = { - "model": temp_model, - "temperature": temp_temperature, - "max_tokens": temp_max_tokens, - "messages": messages, - } - - response = await self.async_process_message(question, **kwargs) - - # Update metrics - end_time = dt_util.utcnow() - latency = (end_time - start_time).total_seconds() - self._update_metrics(latency, response) - - # Update history - self._update_history(question, response) - - return response - - except Exception as err: - self._handle_error(err) - raise HomeAssistantError(f"Failed to process question: {err}") - - finally: - self._is_processing = False - await self.async_update_ha_state() - - async def async_process_message(self, question: str, **kwargs) -> dict: - """Process message using the AI client.""" - try: - if self.is_anthropic: - response = await self._process_anthropic_message(question, **kwargs) - else: - response = await self._process_openai_message(question, **kwargs) - - self.last_response = { - "timestamp": dt_util.utcnow().isoformat(), - "question": question, - "response": response["content"], - "model": kwargs.get("model", self.model), - "instance": self.instance_name, - "error": None - } - - return response - - except Exception as err: - self._handle_error(err) - raise - - async def _process_anthropic_message(self, question: str, **kwargs) -> dict: - """Process message using Anthropic API.""" - response = await self.client.messages.create( - model=kwargs["model"], - max_tokens=kwargs["max_tokens"], - messages=kwargs["messages"], - temperature=kwargs["temperature"], - ) - return { - "content": response.content[0].text, - "tokens": { - "prompt": response.usage.input_tokens, - "completion": response.usage.output_tokens, - "total": response.usage.input_tokens + response.usage.output_tokens - } - } - - async def _process_openai_message(self, question: str, **kwargs) -> dict: - """Process message using OpenAI API.""" - try: - response = await self.client.create( - model=kwargs["model"], - messages=kwargs["messages"], - temperature=kwargs["temperature"], - max_tokens=kwargs["max_tokens"], - ) - - return { - "content": response["choices"][0]["message"]["content"], - "tokens": { - "prompt": response["usage"]["prompt_tokens"], - "completion": response["usage"]["completion_tokens"], - "total": response["usage"]["total_tokens"] - } - } - except Exception as e: - _LOGGER.error(f"Error in OpenAI API call: {str(e)}") - raise - - def _update_metrics(self, latency: float, response: dict) -> None: - """Update performance metrics.""" - metrics = self._performance_metrics - tokens = response.get("tokens", {}) - - metrics["total_tokens"] += tokens.get("total", 0) - metrics["prompt_tokens"] += tokens.get("prompt", 0) - metrics["completion_tokens"] += tokens.get("completion", 0) - metrics["successful_requests"] += 1 - - metrics["average_latency"] = ( - (metrics["average_latency"] * (metrics["successful_requests"] - 1) + latency) - / metrics["successful_requests"] - ) - metrics["max_latency"] = max(metrics["max_latency"], latency) - metrics["min_latency"] = min(metrics["min_latency"], latency) - - def _update_history(self, question: str, response: dict) -> None: - """Update conversation history.""" - self._conversation_history.append({ - "timestamp": dt_util.utcnow().isoformat(), - "question": question, - "response": response["content"] - }) - - while len(self._conversation_history) > self.max_history_size: - self._conversation_history.pop(0) - - def _handle_error(self, error: Exception) -> None: - """Handle error and update metrics.""" - self._performance_metrics["total_errors"] += 1 - self._performance_metrics["failed_requests"] += 1 - - self.last_response = { - "timestamp": dt_util.utcnow().isoformat(), - "question": "", - "response": "", - "model": self.model, - "instance": self.instance_name, - "error": str(error) - } - - async def async_clear_history(self) -> None: - """Clear conversation history.""" - self._conversation_history = [] - await self.async_update_ha_state() - - async def async_get_history(self) -> List[Dict[str, str]]: - """Get conversation history.""" - return self._conversation_history - - async def async_set_system_prompt(self, prompt: str) -> None: - """Set system prompt.""" - self._system_prompt = prompt - await self.async_update_ha_state() diff --git a/ha_text_ai/manifest.json b/ha_text_ai/manifest.json deleted file mode 100644 index c37d467..0000000 --- a/ha_text_ai/manifest.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "domain": "ha_text_ai", - "name": "HA Text AI", - "after_dependencies": ["http"], - "bluetooth": [], - "codeowners": ["@smkrv"], - "config_flow": true, - "dependencies": [], - "documentation": "https://github.com/smkrv/ha-text-ai", - "integration_type": "service", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/smkrv/ha-text-ai/issues", - "loggers": ["custom_components.ha_text_ai"], - "mqtt": [], - "quality_scale": "silver", - "requirements": [ - "openai>=1.12.0", - "anthropic>=0.8.0", - "aiohttp>=3.8.0", - "async-timeout>=4.0.0", - "certifi>=2024.2.2" - ], - "single_config_entry": false, - "ssdp": [], - "usb": [], - "version": "2.0.0-alpha", - "zeroconf": [] -} diff --git a/ha_text_ai/sensor.py b/ha_text_ai/sensor.py deleted file mode 100644 index e18fd2f..0000000 --- a/ha_text_ai/sensor.py +++ /dev/null @@ -1,294 +0,0 @@ -"""Sensor platform for HA Text AI.""" -import logging -import math -from typing import Any, Dict - -from homeassistant.components.sensor import ( - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import dt as dt_util -from homeassistant.util import slugify - -from .const import ( - DOMAIN, - CONF_MODEL, - CONF_API_PROVIDER, - ATTR_TOTAL_RESPONSES, - ATTR_TOTAL_ERRORS, - ATTR_AVG_RESPONSE_TIME, - ATTR_LAST_REQUEST_TIME, - ATTR_LAST_ERROR, - ATTR_IS_PROCESSING, - ATTR_IS_RATE_LIMITED, - ATTR_IS_MAINTENANCE, - ATTR_API_VERSION, - ATTR_ENDPOINT_STATUS, - ATTR_PERFORMANCE_METRICS, - ATTR_HISTORY_SIZE, - ATTR_UPTIME, - ATTR_API_PROVIDER, - ATTR_MODEL, - ATTR_SYSTEM_PROMPT, - ATTR_API_STATUS, - ATTR_RESPONSE, - ATTR_QUESTION, - ATTR_CONVERSATION_HISTORY, - METRIC_TOTAL_TOKENS, - METRIC_PROMPT_TOKENS, - METRIC_COMPLETION_TOKENS, - METRIC_SUCCESSFUL_REQUESTS, - METRIC_FAILED_REQUESTS, - METRIC_AVERAGE_LATENCY, - METRIC_MAX_LATENCY, - METRIC_MIN_LATENCY, - STATE_READY, - STATE_PROCESSING, - STATE_ERROR, - STATE_INITIALIZING, - STATE_MAINTENANCE, - STATE_RATE_LIMITED, - STATE_DISCONNECTED, - ENTITY_ICON, - ENTITY_ICON_ERROR, - ENTITY_ICON_PROCESSING, -) - -from .coordinator import HATextAICoordinator - -_LOGGER = logging.getLogger(__name__) - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the HA Text AI sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - instance_name = coordinator.instance_name - - _LOGGER.debug(f"Setting up sensor with instance: {instance_name}") - - sensor = HATextAISensor(coordinator, entry) - async_add_entities([sensor], True) - -class HATextAISensor(CoordinatorEntity, SensorEntity): - """HA Text AI Sensor.""" - - coordinator: HATextAICoordinator - - def __init__( - self, - coordinator: HATextAICoordinator, - config_entry: ConfigEntry, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) - - self._config_entry = config_entry - self._instance_name = coordinator.instance_name - self._conversation_history = [] - self._system_prompt = None - - self._attr_name = f"HA Text AI {self._instance_name}" - self.entity_id = f"sensor.ha_text_ai_{slugify(self._instance_name)}" - self._attr_unique_id = f"{config_entry.entry_id}" - - self.entity_description = SensorEntityDescription( - key=f"ha_text_ai_{self._instance_name}", - entity_registry_enabled_default=True, - ) - - self._current_state = STATE_INITIALIZING - self._error_count = 0 - self._last_error = None - self._last_update = None - self._is_processing = False - self._last_response = {} - self._metrics = {} - - model = config_entry.data.get(CONF_MODEL, "Unknown") - api_provider = config_entry.data.get(CONF_API_PROVIDER, "Unknown") - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, - name=self._attr_name, # Используем имя сенсора - manufacturer="Community", - model=f"{model} ({api_provider} provider)", - sw_version="1.0.0", - ) - - _LOGGER.debug(f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}") - - @property - def available(self) -> bool: - """Return if entity is available.""" - return ( - self.coordinator.last_update_success - and self.coordinator.data is not None - and self._current_state != STATE_DISCONNECTED - ) - - def _sanitize_value(self, value: Any) -> Any: - """Sanitize values for JSON serialization.""" - if isinstance(value, float): - if math.isinf(value) or math.isnan(value): - return None - return value - - def _sanitize_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]: - """Sanitize all attributes for JSON serialization.""" - return { - key: self._sanitize_value(value) - for key, value in attributes.items() - if value is not None - } - - @property - def native_value(self) -> StateType: - """Return the native value of the sensor.""" - if not self.coordinator.last_update_success or not self.coordinator.data: - self._current_state = STATE_DISCONNECTED - return self._current_state - - status = self.coordinator.data.get("state", STATE_READY) - self._current_state = status - return status - - @property - def icon(self) -> str: - """Return the icon based on the current state.""" - if self._current_state == STATE_ERROR: - return ENTITY_ICON_ERROR - elif self._current_state == STATE_PROCESSING: - return ENTITY_ICON_PROCESSING - return ENTITY_ICON - - @property - def extra_state_attributes(self) -> Dict[str, Any]: - """Return entity specific state attributes.""" - if not self.coordinator.data: - return {} - - try: - data = self.coordinator.data - attributes = { - ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"), - ATTR_API_PROVIDER: self._config_entry.data.get(CONF_API_PROVIDER, "Unknown"), - ATTR_API_STATUS: self._current_state, - ATTR_TOTAL_ERRORS: self._error_count, - ATTR_LAST_ERROR: self._last_error, - "instance_name": self._instance_name, - ATTR_SYSTEM_PROMPT: data.get("system_prompt"), - ATTR_IS_PROCESSING: data.get("is_processing", False), - ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False), - ATTR_IS_MAINTENANCE: data.get("is_maintenance", False), - ATTR_ENDPOINT_STATUS: data.get("endpoint_status", "unknown"), - ATTR_UPTIME: data.get("uptime", 0), - ATTR_HISTORY_SIZE: data.get("history_size", 0), - ATTR_CONVERSATION_HISTORY: data.get("conversation_history", []), - } - - # Add metrics - metrics = data.get("metrics", {}) - if isinstance(metrics, dict): - self._metrics = metrics - attributes.update({ - METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0), - METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0), - METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0), - METRIC_SUCCESSFUL_REQUESTS: metrics.get("successful_requests", 0), - METRIC_FAILED_REQUESTS: metrics.get("failed_requests", 0), - METRIC_AVERAGE_LATENCY: metrics.get("average_latency", 0), - METRIC_MAX_LATENCY: metrics.get("max_latency", 0), - METRIC_MIN_LATENCY: metrics.get("min_latency", float("inf")), - }) - - # Add last response - last_response = data.get("last_response", {}) - if isinstance(last_response, dict): - self._last_response = last_response - attributes.update({ - ATTR_RESPONSE: last_response.get("response", ""), - ATTR_QUESTION: last_response.get("question", ""), - "last_model": last_response.get("model", ""), - "last_timestamp": last_response.get("timestamp", ""), - "last_error": last_response.get("error"), - }) - - # Add performance metrics if available - if ATTR_PERFORMANCE_METRICS in data: - attributes[ATTR_PERFORMANCE_METRICS] = data[ATTR_PERFORMANCE_METRICS] - - # Add API version if available - if ATTR_API_VERSION in data: - attributes[ATTR_API_VERSION] = data[ATTR_API_VERSION] - - return self._sanitize_attributes(attributes) - - except Exception as err: - _LOGGER.error("Error preparing attributes: %s", err, exc_info=True) - return {} - - async def async_added_to_hass(self) -> None: - """When entity is added to hass.""" - await super().async_added_to_hass() - self._handle_coordinator_update() - _LOGGER.debug(f"Entity {self.entity_id} added to Home Assistant") - - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - try: - data = self.coordinator.data - if not self.coordinator.last_update_success or not data: - self._current_state = STATE_DISCONNECTED - _LOGGER.warning(f"No data available for {self.entity_id}") - self.async_write_ha_state() - return - - self._is_processing = data.get("is_processing", False) - - # Update conversation history and system prompt - self._conversation_history = data.get("conversation_history", []) - self._system_prompt = data.get("system_prompt") - - # Update state based on conditions - if self._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 - elif data.get("error"): - self._current_state = STATE_ERROR - self._last_error = data["error"] - self._error_count += 1 - else: - self._current_state = data.get("state", STATE_READY) - - # Update last update timestamp - self._last_update = dt_util.utcnow() - - _LOGGER.debug( - f"Updated {self.entity_id} state to: {self._current_state} " - f"(available: {self.available})" - ) - - except Exception as err: - self._current_state = STATE_ERROR - self._last_error = str(err) - self._error_count += 1 - _LOGGER.error( - "Error handling update for %s: %s", - self.entity_id, - err, - exc_info=True - ) - - self.async_write_ha_state() diff --git a/ha_text_ai/services.yaml b/ha_text_ai/services.yaml deleted file mode 100644 index b69c734..0000000 --- a/ha_text_ai/services.yaml +++ /dev/null @@ -1,75 +0,0 @@ -ask_question: - name: 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. - 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: - name: Question - description: Your question or prompt for the AI assistant - required: true - selector: - text: - multiline: true - type: text - - system_prompt: - name: System Prompt - description: Optional system prompt to set context for this specific question - required: false - selector: - text: - multiline: true - - context_messages: - name: Context Messages - description: Number of previous messages to include in context (1-20) - required: false - default: 5 - selector: - number: - min: 1 - max: 20 - step: 1 - mode: box - - model: - name: Model - description: "Select AI model to use (optional, overrides default setting)" - required: false - selector: - text: - multiline: false - - temperature: - name: Temperature - description: Controls response creativity (0.0-2.0) - required: false - default: 0.7 - selector: - number: - min: 0.0 - max: 2.0 - step: 0.1 - mode: slider - - max_tokens: - name: Max Tokens - description: Maximum length of the response (1-4096 tokens) - required: false - default: 1000 - selector: - number: - min: 1 - max: 4096 - step: 1 - mode: box diff --git a/ha_text_ai/translations/de.json b/ha_text_ai/translations/de.json deleted file mode 100644 index 68b954f..0000000 --- a/ha_text_ai/translations/de.json +++ /dev/null @@ -1,261 +0,0 @@ -{ - "config": { - "step": { - "provider": { - "title": "KI-Anbieter auswählen", - "description": "Wählen Sie den KI-Dienst-Anbieter für diese Instanz", - "data": { - "api_provider": "API-Anbieter", - "context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)" - } - }, - "user": { - "title": "HA Text AI Instanz konfigurieren", - "description": "Richten Sie eine neue KI-Assistenten-Instanz mit Ihrem ausgewählten Anbieter ein", - "data": { - "name": "Instanzname (z.B. 'GPT Assistent', 'Claude Helfer')", - "api_key": "API-Schlüssel für Authentifizierung", - "model": "Zu verwendendes KI-Modell", - "temperature": "Antwort-Kreativität (0-2, niedriger = fokussierter)", - "max_tokens": "Maximale Antwortlänge (1-4096 Token)", - "api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)", - "request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)", - "context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)" - } - } - }, - "error": { - "name_exists": "Eine Instanz mit diesem Namen existiert bereits", - "invalid_name": "Ungültiger Instanzname", - "invalid_auth": "Authentifizierung fehlgeschlagen - überprüfen Sie Ihren API-Schlüssel", - "invalid_api_key": "Ungültiger API-Schlüssel - bitte überprüfen Sie Ihre Anmeldedaten", - "cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen", - "invalid_model": "Ausgewähltes Modell ist nicht verfügbar", - "rate_limit": "Anfragelimit überschritten", - "context_length": "Kontextlänge überschritten", - "rate_limit_exceeded": "API-Anfragelimit überschritten", - "maintenance": "Dienst ist in Wartung", - "invalid_response": "Ungültige API-Antwort erhalten", - "api_error": "API-Dienst-Fehler aufgetreten", - "timeout": "Anfrage-Zeitüberschreitung", - "invalid_instance": "Ungültige Instanz angegeben", - "unknown": "Unerwarteter Fehler aufgetreten" - } - }, - "options": { - "step": { - "init": { - "title": "Instanzeinstellungen aktualisieren", - "description": "Einstellungen für diese KI-Assistenten-Instanz ändern", - "data": { - "model": "KI-Modell", - "temperature": "Antwort-Kreativität (0-2)", - "max_tokens": "Maximale Antwortlänge (1-4096)", - "request_interval": "Minimales Anfragen-Intervall (0,1-60 Sekunden)", - "context_messages": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)" - - } - } - } - }, - "services": { - "ask_question": { - "name": "Frage stellen (HA Text AI)", - "description": "Senden Sie eine Frage an das KI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird in der Gesprächshistorie gespeichert und kann später abgerufen werden.", - "fields": { - "instance": { - "name": "Instanz", - "description": "Name der zu verwendenden HA Text AI Instanz" - }, - "question": { - "name": "Frage", - "description": "Ihre Frage oder Eingabeaufforderung für den KI-Assistenten" - }, - "context_messages": { - "name": "Kontextnachrichten", - "description": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)" - }, - "system_prompt": { - "name": "Systemaufforderung", - "description": "Optionale Systemaufforderung zur Kontexteinstellung für diese spezifische Frage" - }, - "model": { - "name": "Modell", - "description": "Wählen Sie das zu verwendende KI-Modell (optional, überschreibt Standardeinstellung)" - }, - "temperature": { - "name": "Temperatur", - "description": "Steuert die Antwort-Kreativität (0,0-2,0)" - }, - "max_tokens": { - "name": "Max. Token", - "description": "Maximale Länge der Antwort (1-4096 Token)" - } - } - }, - "clear_history": { - "name": "Verlauf löschen", - "description": "Alle gespeicherten Fragen und Antworten aus dem Gesprächsverlauf löschen", - "fields": { - "instance": { - "name": "Instanz", - "description": "Name der HA Text AI Instanz, für die der Verlauf gelöscht werden soll" - } - } - }, - "get_history": { - "name": "Verlauf abrufen", - "description": "Gesprächsverlauf mit optionaler Filterung und Sortierung abrufen", - "fields": { - "instance": { - "name": "Instanz", - "description": "Name der HA Text AI Instanz, aus der der Verlauf abgerufen werden soll" - }, - "limit": { - "name": "Limit", - "description": "Anzahl der zurückzugebenden Gespräche (1-100)" - }, - "filter_model": { - "name": "Modell filtern", - "description": "Gespräche nach bestimmtem KI-Modell filtern" - }, - "start_date": { - "name": "Startdatum", - "description": "Gespräche ab diesem Datum/Zeitpunkt filtern" - }, - "include_metadata": { - "name": "Metadaten einbeziehen", - "description": "Zusätzliche Informationen wie verwendete Token, Antwortzeit usw. einbeziehen" - }, - "sort_order": { - "name": "Sortierreihenfolge", - "description": "Sortierreihenfolge der Ergebnisse (neueste oder älteste zuerst)" - } - } - }, - "set_system_prompt": { - "name": "Systemaufforderung festlegen", - "description": "Standardmäßige Systemverhaltensinstruktionen für alle zukünftigen Gespräche festlegen", - "fields": { - "instance": { - "name": "Instanz", - "description": "Name der HA Text AI Instanz, für die die Systemaufforderung festgelegt werden soll" - }, - "prompt": { - "name": "Systemaufforderung", - "description": "Anweisungen, die definieren, wie sich die KI verhalten und antworten soll" - } - } - } - }, - "entity": { - "sensor": { - "ha_text_ai": { - "name": "{name}", - "state": { - "ready": "Bereit", - "processing": "Verarbeitung", - "error": "Fehler", - "disconnected": "Getrennt", - "rate_limited": "Anfragelimit", - "maintenance": "Wartung", - "initializing": "Initialisierung", - "retrying": "Wiederholung", - "queued": "In Warteschlange" - }, - "state_attributes": { - "question": { - "name": "Letzte Frage" - }, - "response": { - "name": "Letzte Antwort" - }, - "model": { - "name": "Aktuelles Modell" - }, - "temperature": { - "name": "Temperatur" - }, - "max_tokens": { - "name": "Max. Token" - }, - "system_prompt": { - "name": "Systemaufforderung" - }, - "response_time": { - "name": "Letzte Antwortzeit" - }, - "total_responses": { - "name": "Gesamte Antworten" - }, - "error_count": { - "name": "Fehleranzahl" - }, - "last_error": { - "name": "Letzter Fehler" - }, - "api_status": { - "name": "API-Status" - }, - "tokens_used": { - "name": "Insgesamt verwendete Token" - }, - "average_response_time": { - "name": "Durchschnittliche Antwortzeit" - }, - "last_request_time": { - "name": "Letzte Anforderungszeit" - }, - "is_processing": { - "name": "Verarbeitungsstatus" - }, - "is_rate_limited": { - "name": "Status Anfragelimit" - }, - "is_maintenance": { - "name": "Wartungsstatus" - }, - "api_version": { - "name": "API-Version" - }, - "endpoint_status": { - "name": "Endpunktstatus" - }, - "performance_metrics": { - "name": "Leistungsmetriken" - }, - "history_size": { - "name": "Verlaufsgröße" - }, - "uptime": { - "name": "Betriebszeit" - }, - "total_tokens": { - "name": "Gesamte Token" - }, - "prompt_tokens": { - "name": "Prompt-Token" - }, - "completion_tokens": { - "name": "Abschluss-Token" - }, - "successful_requests": { - "name": "Erfolgreiche Anfragen" - }, - "failed_requests": { - "name": "Fehlgeschlagene Anfragen" - }, - "average_latency": { - "name": "Durchschnittliche Latenz" - }, - "max_latency": { - "name": "Maximale Latenz" - }, - "min_latency": { - "name": "Minimale Latenz" - } - } - } - } - } -} diff --git a/ha_text_ai/translations/en.json b/ha_text_ai/translations/en.json deleted file mode 100644 index bd1b825..0000000 --- a/ha_text_ai/translations/en.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "config": { - "step": { - "provider": { - "title": "Select AI Provider", - "description": "Choose which AI service provider to use for this instance", - "data": { - "api_provider": "API Provider", - "context_messages": "Number of context messages to retain (1-20)" - } - }, - "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)", - "api_endpoint": "Custom API endpoint URL (optional)", - "request_interval": "Minimum time between requests (0.1-60 seconds)", - "context_messages": "Number of context messages to retain (1-20)" - } - } - }, - "error": { - "name_exists": "An instance with this name already exists", - "invalid_name": "Invalid instance name", - "invalid_auth": "Authentication failed - check your API key", - "invalid_api_key": "Invalid API key - please verify your credentials", - "cannot_connect": "Failed to connect to API service", - "invalid_model": "Selected model is not available", - "rate_limit": "Rate limit exceeded", - "context_length": "Context length exceeded", - "rate_limit_exceeded": "API rate limit exceeded", - "maintenance": "Service is under maintenance", - "invalid_response": "Invalid API response received", - "api_error": "API service error occurred", - "timeout": "Request timed out", - "invalid_instance": "Invalid instance specified", - "unknown": "Unexpected error occurred" - } - }, - "options": { - "step": { - "init": { - "title": "Update Instance Settings", - "description": "Modify settings for this AI assistant instance", - "data": { - "model": "AI model", - "temperature": "Response creativity (0-2)", - "max_tokens": "Maximum response length (1-4096)", - "request_interval": "Minimum request interval (0.1-60 seconds)", - "context_messages": "Number of previous messages to include in context (1-20)" - } - } - } - }, - "services": { - "ask_question": { - "name": "Ask Question (HA Text AI)", - "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.", - "fields": { - "instance": { - "name": "Instance", - "description": "Name of the HA Text AI instance to use" - }, - "question": { - "name": "Question", - "description": "Your question or prompt for the AI assistant" - }, - "context_messages": { - "name": "Context Messages", - "description": "Number of previous messages to include in context (1-20)" - }, - "system_prompt": { - "name": "System Prompt", - "description": "Optional system prompt to set context for this specific question" - }, - "model": { - "name": "Model", - "description": "Select AI model to use (optional, overrides default setting)" - }, - "temperature": { - "name": "Temperature", - "description": "Controls response creativity (0.0-2.0)" - }, - "max_tokens": { - "name": "Max Tokens", - "description": "Maximum length of the response (1-4096 tokens)" - } - } - }, - "clear_history": { - "name": "Clear History", - "description": "Delete all stored questions and responses from the conversation history", - "fields": { - "instance": { - "name": "Instance", - "description": "Name of the HA Text AI instance to clear history for" - } - } - }, - "get_history": { - "name": "Get History", - "description": "Retrieve conversation history with optional filtering and sorting", - "fields": { - "instance": { - "name": "Instance", - "description": "Name of the HA Text AI instance to get history from" - }, - "limit": { - "name": "Limit", - "description": "Number of conversations to return (1-100)" - }, - "filter_model": { - "name": "Filter Model", - "description": "Filter conversations by specific AI model" - }, - "start_date": { - "name": "Start Date", - "description": "Filter conversations starting from this date/time" - }, - "include_metadata": { - "name": "Include Metadata", - "description": "Include additional information like tokens used, response time, etc." - }, - "sort_order": { - "name": "Sort Order", - "description": "Sort order for results (newest or oldest first)" - } - } - }, - "set_system_prompt": { - "name": "Set System Prompt", - "description": "Set default system behavior instructions for all future conversations", - "fields": { - "instance": { - "name": "Instance", - "description": "Name of the HA Text AI instance to set system prompt for" - }, - "prompt": { - "name": "System Prompt", - "description": "Instructions that define how the AI should behave and respond" - } - } - } - }, - "entity": { - "sensor": { - "ha_text_ai": { - "name": "{name}", - "state": { - "ready": "Ready", - "processing": "Processing", - "error": "Error", - "disconnected": "Disconnected", - "rate_limited": "Rate Limited", - "maintenance": "Maintenance", - "initializing": "Initializing", - "retrying": "Retrying", - "queued": "Queued" - }, - "state_attributes": { - "question": { - "name": "Last Question" - }, - "response": { - "name": "Last Response" - }, - "model": { - "name": "Current Model" - }, - "temperature": { - "name": "Temperature" - }, - "max_tokens": { - "name": "Max Tokens" - }, - "system_prompt": { - "name": "System Prompt" - }, - "response_time": { - "name": "Last Response Time" - }, - "total_responses": { - "name": "Total Responses" - }, - "error_count": { - "name": "Error Count" - }, - "last_error": { - "name": "Last Error" - }, - "api_status": { - "name": "API Status" - }, - "tokens_used": { - "name": "Total Tokens Used" - }, - "average_response_time": { - "name": "Average Response Time" - }, - "last_request_time": { - "name": "Last Request Time" - }, - "is_processing": { - "name": "Processing Status" - }, - "is_rate_limited": { - "name": "Rate Limited Status" - }, - "is_maintenance": { - "name": "Maintenance Status" - }, - "api_version": { - "name": "API Version" - }, - "endpoint_status": { - "name": "Endpoint Status" - }, - "performance_metrics": { - "name": "Performance Metrics" - }, - "history_size": { - "name": "History Size" - }, - "uptime": { - "name": "Uptime" - }, - "total_tokens": { - "name": "Total Tokens" - }, - "prompt_tokens": { - "name": "Prompt Tokens" - }, - "completion_tokens": { - "name": "Completion Tokens" - }, - "successful_requests": { - "name": "Successful Requests" - }, - "failed_requests": { - "name": "Failed Requests" - }, - "average_latency": { - "name": "Average Latency" - }, - "max_latency": { - "name": "Maximum Latency" - }, - "min_latency": { - "name": "Minimum Latency" - } - } - } - } - } -} diff --git a/ha_text_ai/translations/ru.json b/ha_text_ai/translations/ru.json deleted file mode 100644 index 30ca1ea..0000000 --- a/ha_text_ai/translations/ru.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "config": { - "step": { - "provider": { - "title": "Выбор провайдера ИИ", - "description": "Выберите сервис ИИ для этого экземпляра", - "data": { - "api_provider": "Провайдер API", - "context_messages": "Количество сообщений в контексте (1-20)" - } - }, - "user": { - "title": "Настройка экземпляра HA Text AI", - "description": "Настройте нового помощника ИИ с выбранным провайдером", - "data": { - "name": "Название экземпляра (например, 'GPT Помощник', 'Claude Ассистент')", - "api_key": "API-ключ для аутентификации", - "model": "Модель ИИ для использования", - "temperature": "Креативность ответов (0-2, меньше = более сфокусированно)", - "max_tokens": "Максимальная длина ответа (1-4096 токенов)", - "api_endpoint": "Пользовательский URL-адрес API (необязательно)", - "request_interval": "Минимальный интервал между запросами (0.1-60 секунд)", - "context_messages": "Количество сохраняемых контекстных сообщений (1-20)" - } - } - }, - "error": { - "name_exists": "Экземпляр с таким именем уже существует", - "invalid_name": "Некорректное имя экземпляра", - "invalid_auth": "Ошибка аутентификации - проверьте API-ключ", - "invalid_api_key": "Неверный API-ключ - проверьте учетные данные", - "cannot_connect": "Не удалось подключиться к сервису API", - "invalid_model": "Выбранная модель недоступна", - "rate_limit": "Превышен лимит запросов", - "context_length": "Превышена длина контекста", - "rate_limit_exceeded": "Превышен лимит API", - "maintenance": "Сервис на техническом обслуживании", - "invalid_response": "Получен некорректный ответ API", - "api_error": "Произошла ошибка сервиса API", - "timeout": "Время ожидания истекло", - "invalid_instance": "Указан неверный экземпляр", - "unknown": "Произошла непредвиденная ошибка" - } - }, - "options": { - "step": { - "init": { - "title": "Обновление настроек экземпляра", - "description": "Измените настройки для этого помощника ИИ", - "data": { - "model": "Модель ИИ", - "temperature": "Креативность ответов (0-2)", - "max_tokens": "Максимальная длина ответа (1-4096)", - "request_interval": "Минимальный интервал запросов (0.1-60 секунд)", - "context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)" - } - } - } - }, - "services": { - "ask_question": { - "name": "Задать вопрос (HA Text AI)", - "description": "Отправьте вопрос модели ИИ и получите подробный ответ. Ответ будет сохранен в истории беседы и может быть извлечен позже.", - "fields": { - "instance": { - "name": "Экземпляр", - "description": "Название экземпляра HA Text AI для использования" - }, - "question": { - "name": "Вопрос", - "description": "Ваш вопрос или запрос помощнику ИИ" - }, - "context_messages": { - "name": "Контекстные сообщения", - "description": "Количество предыдущих сообщений для включения в контекст (1-20)" - }, - "system_prompt": { - "name": "Системный промпт", - "description": "Необязательный системный промпт для установки контекста для этого конкретного вопроса" - }, - "model": { - "name": "Модель", - "description": "Выберите модель ИИ (необязательно, переопределяет настройки по умолчанию)" - }, - "temperature": { - "name": "Температура", - "description": "Управляет креативностью ответа (0.0-2.0)" - }, - "max_tokens": { - "name": "Максимальное количество токенов", - "description": "Максимальная длина ответа (1-4096 токенов)" - } - } - }, - "clear_history": { - "name": "Очистить историю", - "description": "Удалить все сохраненные вопросы и ответы из истории беседы", - "fields": { - "instance": { - "name": "Экземпляр", - "description": "Название экземпляра HA Text AI для очистки истории" - } - } - }, - "get_history": { - "name": "Получить историю", - "description": "Получить историю беседы с дополнительной фильтрацией и сортировкой", - "fields": { - "instance": { - "name": "Экземпляр", - "description": "Название экземпляра HA Text AI для получения истории" - }, - "limit": { - "name": "Лимит", - "description": "Количество возвращаемых бесед (1-100)" - }, - "filter_model": { - "name": "Фильтр модели", - "description": "Фильтрация бесед по конкретной модели ИИ" - }, - "start_date": { - "name": "Начальная дата", - "description": "Фильтрация бесед, начиная с указанной даты/времени" - }, - "include_metadata": { - "name": "Включить метаданные", - "description": "Включить дополнительную информацию, например, использованные токены, время ответа и т.д." - }, - "sort_order": { - "name": "Порядок сортировки", - "description": "Порядок результатов (сначала новые или старые)" - } - } - }, - "set_system_prompt": { - "name": "Установить системный промпт", - "description": "Установить инструкции по умолчанию для поведения ИИ во всех будущих разговорах", - "fields": { - "instance": { - "name": "Экземпляр", - "description": "Название экземпляра HA Text AI для установки системного промпта" - }, - "prompt": { - "name": "Системный промпт", - "description": "Инструкции, определяющие, как ИИ должен вести себя и отвечать" - } - } - } - }, - "entity": { - "sensor": { - "ha_text_ai": { - "name": "{name}", - "state": { - "ready": "Готов", - "processing": "Обработка", - "error": "Ошибка", - "disconnected": "Отключен", - "rate_limited": "Лимит запросов", - "maintenance": "Техническое обслуживание", - "initializing": "Инициализация", - "retrying": "Повторная попытка", - "queued": "В очереди" - }, - "state_attributes": { - "question": { - "name": "Последний вопрос" - }, - "response": { - "name": "Последний ответ" - }, - "model": { - "name": "Текущая модель" - }, - "temperature": { - "name": "Температура" - }, - "max_tokens": { - "name": "Максимальное количество токенов" - }, - "system_prompt": { - "name": "Системный промпт" - }, - "response_time": { - "name": "Время последнего ответа" - }, - "total_responses": { - "name": "Всего ответов" - }, - "error_count": { - "name": "Количество ошибок" - }, - "last_error": { - "name": "Последняя ошибка" - }, - "api_status": { - "name": "Статус API" - }, - "tokens_used": { - "name": "Всего использовано токенов" - }, - "average_response_time": { - "name": "Среднее время ответа" - }, - "last_request_time": { - "name": "Время последнего запроса" - }, - "is_processing": { - "name": "Статус обработки" - }, - "is_rate_limited": { - "name": "Статус лимита запросов" - }, - "is_maintenance": { - "name": "Статус обслуживания" - }, - "api_version": { - "name": "Версия API" - }, - "endpoint_status": { - "name": "Статус эндпоинта" - }, - "performance_metrics": { - "name": "Показатели производительности" - }, - "history_size": { - "name": "Размер истории" - }, - "uptime": { - "name": "Время работы" - }, - "total_tokens": { - "name": "Всего токенов" - }, - "prompt_tokens": { - "name": "Токены промпта" - }, - "completion_tokens": { - "name": "Токены завершения" - }, - "successful_requests": { - "name": "Успешные запросы" - }, - "failed_requests": { - "name": "Неудачные запросы" - }, - "average_latency": { - "name": "Средняя задержка" - }, - "max_latency": { - "name": "Максимальная задержка" - }, - "min_latency": { - "name": "Минимальная задержка" - } - } - } - } - } -}