Release v2.0.0

This commit is contained in:
SMKRV
2024-11-25 02:03:29 +03:00
parent 39833b333f
commit 351a8b18dd
3 changed files with 101 additions and 60 deletions
+19 -38
View File
@@ -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
+36 -9
View File
@@ -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
+46 -13
View File
@@ -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)