From 351a8b18dd9d0095231017875aaa4591b827581b Mon Sep 17 00:00:00 2001 From: SMKRV Date: Mon, 25 Nov 2024 02:03:29 +0300 Subject: [PATCH] Release v2.0.0 --- custom_components/ha_text_ai/__init__.py | 57 +++++++------------- custom_components/ha_text_ai/api_client.py | 45 ++++++++++++---- custom_components/ha_text_ai/config_flow.py | 59 ++++++++++++++++----- 3 files changed, 101 insertions(+), 60 deletions(-) diff --git a/custom_components/ha_text_ai/__init__.py b/custom_components/ha_text_ai/__init__.py index e61708b..1ef796c 100644 --- a/custom_components/ha_text_ai/__init__.py +++ b/custom_components/ha_text_ai/__init__.py @@ -1,4 +1,10 @@ -"""The HA Text AI integration.""" +# __init__.py +"""The HA Text AI integration. + +This module initializes the HA Text AI integration for Home Assistant, handling setup, configuration, +API connection verification, and managing service calls for interacting with AI models. +""" + from __future__ import annotations import logging @@ -45,8 +51,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +# Validate the configuration schema CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) +# Schema for the ask_question service SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({ vol.Required("instance"): cv.string, vol.Required("question"): cv.string, @@ -56,11 +64,13 @@ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({ vol.Optional("max_tokens"): cv.positive_int, }) +# Schema for setting a system prompt SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({ vol.Required("instance"): cv.string, vol.Required("prompt"): cv.string, }) +# Schema for getting history SERVICE_SCHEMA_GET_HISTORY = vol.Schema({ vol.Required("instance"): cv.string, vol.Optional("limit"): cv.positive_int, @@ -82,6 +92,7 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: """Set up the HA Text AI component.""" hass.data.setdefault(DOMAIN, {}) + # Try to copy custom icon to home assistant www directory try: source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg') dest_dir = os.path.join(hass.config.path('www'), 'icons') @@ -137,6 +148,7 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: _LOGGER.error("Error setting system prompt: %s", str(err)) raise HomeAssistantError(f"Failed to set system prompt: {str(err)}") + # Registering services to handle different functionalities hass.services.async_register( DOMAIN, SERVICE_ASK_QUESTION, @@ -194,6 +206,7 @@ async def async_check_api(session, endpoint: str, headers: dict, provider: str) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up HA Text AI from a config entry.""" try: + # Ensure the API provider is specified if CONF_API_PROVIDER not in entry.data: _LOGGER.error("API provider not specified") raise ConfigEntryNotReady("API provider is required") @@ -215,17 +228,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Accept": "application/json" } + # Set appropriate headers based on the provider if is_anthropic: headers["x-api-key"] = api_key headers["anthropic-version"] = "2023-06-01" else: headers["Authorization"] = f"Bearer {api_key}" + # Check the API connection 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) + # Initialize the API client api_client = APIClient( session=session, endpoint=endpoint, @@ -234,6 +250,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=model, ) + # Initialize and set up the coordinator coordinator = HATextAICoordinator( hass=hass, client=api_client, @@ -248,40 +265,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 + await coordinator.async_config_entry_first_refresh diff --git a/custom_components/ha_text_ai/api_client.py b/custom_components/ha_text_ai/api_client.py index 727af26..bb73277 100644 --- a/custom_components/ha_text_ai/api_client.py +++ b/custom_components/ha_text_ai/api_client.py @@ -1,7 +1,13 @@ -"""API Client for HA Text AI.""" +# api_client.py +"""API Client for HA Text AI. + +This file contains the implementation of an API client for the HA Text AI integration compatible with both OpenAI and Anthropic service providers. +The client handles API requests, manages connection sessions, implements retry logic, and validates parameters for requests to ensure the correct functionality of the AI service integration within Home Assistant. +""" + import logging import asyncio -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from aiohttp import ClientSession, ClientTimeout from async_timeout import timeout @@ -20,7 +26,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) class APIClient: - """API Client for OpenAI and Anthropic.""" + """API Client for OpenAI and Anthropic. + + This client handles requests to OpenAI and Anthropic services to create text-based AI completions. + """ def __init__( self, @@ -30,7 +39,10 @@ class APIClient: api_provider: str, model: str, ) -> None: - """Initialize API client.""" + """Initialize API client. + + Initialize with session, endpoint URL, request headers, type of API provider (OpenAI or Anthropic), and the AI model in use. + """ self.session = session self.endpoint = endpoint self.headers = headers @@ -43,7 +55,10 @@ class APIClient: temperature: float, max_tokens: int, ) -> None: - """Validate API parameters.""" + """Validate API parameters. + + Check that the temperature and max_tokens are within their respective allowed ranges. + """ if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE: raise ValueError( f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}" @@ -58,7 +73,10 @@ class APIClient: url: str, payload: Dict[str, Any], ) -> Dict[str, Any]: - """Make API request with retry logic.""" + """Make API request with retry logic. + + Attempt the API request a pre-defined number of times in case of failure, with an exponential backoff strategy for timeouts. + """ for attempt in range(API_RETRY_COUNT): try: async with timeout(API_TIMEOUT): @@ -89,7 +107,10 @@ class APIClient: temperature: float, max_tokens: int, ) -> Dict[str, Any]: - """Create completion using appropriate API.""" + """Create completion using appropriate API. + + Make a completion request using either OpenAI or Anthropic API based on the provider specified during initialization. + """ try: self._validate_parameters(temperature, max_tokens) @@ -112,7 +133,10 @@ class APIClient: temperature: float, max_tokens: int, ) -> Dict[str, Any]: - """Create completion using OpenAI API.""" + """Create completion using OpenAI API. + + Send details of the completion request to the OpenAI API and process the response. + """ url = f"{self.endpoint}/chat/completions" payload = { "model": model, @@ -144,7 +168,10 @@ class APIClient: temperature: float, max_tokens: int, ) -> Dict[str, Any]: - """Create completion using Anthropic API.""" + """Create completion using Anthropic API. + + Adapt messages format to Anthropic's requirement and make the API request to generate text completion. + """ url = f"{self.endpoint}/v1/messages" # Convert messages to Anthropic format diff --git a/custom_components/ha_text_ai/config_flow.py b/custom_components/ha_text_ai/config_flow.py index f5a45c4..61bfa18 100644 --- a/custom_components/ha_text_ai/config_flow.py +++ b/custom_components/ha_text_ai/config_flow.py @@ -1,4 +1,12 @@ -"""Config flow for HA text AI integration.""" +# config_flow.py +"""Config flow for HA text AI integration. + +This module defines a configuration flow for the HA text AI integration in Home Assistant. +The flow guides the user through the setup process, including the selection of API provider, +configuration of API settings, and validation of the entered data. It ensures that the +configuration entries are unique and establish a valid connection to the AI API service. +""" + import logging from typing import Any, Dict, Optional @@ -48,7 +56,10 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._provider = None async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult: - """Handle the initial step.""" + """Handle the initial step. + + This step asks the user to select an API provider. + """ if user_input is None: return self.async_show_form( step_id="user", @@ -66,7 +77,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_provider() async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult: - """Handle provider configuration step.""" + """Handle provider configuration step. + + This step collects additional configuration details + based on the chosen provider like API key, model, etc. + """ if user_input is None: default_endpoint = ( DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI @@ -98,21 +113,21 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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.""" + """Validate that the name is unique. + + Ensure no existing configuration entry has the same name. + """ for entry in self._async_current_entries(): if entry.data.get(CONF_NAME) == name: self._errors["name"] = "name_exists" @@ -120,7 +135,10 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return True async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool: - """Validate API connection.""" + """Validate API connection. + + Test the API connection to ensure the provided API details are correct. + """ try: session = async_get_clientsession(self.hass) headers = self._get_api_headers(user_input) @@ -146,7 +164,10 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]: - """Get API headers based on provider.""" + """Get API headers based on provider. + + Returns appropriate headers for API requests depending on the provider. + """ api_key = user_input[CONF_API_KEY] if self._provider == API_PROVIDER_ANTHROPIC: @@ -161,7 +182,10 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult: - """Create the config entry.""" + """Create the config entry. + + Constructs and adds the configuration entry to Home Assistant. + """ instance_name = user_input[CONF_NAME] unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_") @@ -178,7 +202,10 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow: - """Get the options flow for this handler.""" + """Get the options flow for this handler. + + Provides access to configuration options after initial setup. + """ return OptionsFlowHandler(config_entry) @@ -186,11 +213,17 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options flow.""" def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" + """Initialize options flow. + + Prepare for managing configurable options of the integration. + """ self.config_entry = config_entry async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult: - """Manage the options.""" + """Manage the options. + + Allows modification of configurable parameters post initial setup. + """ if user_input is not None: return self.async_create_entry(title="", data=user_input)