diff --git a/custom_components/ha_text_ai/__init__.py b/custom_components/ha_text_ai/__init__.py index 4b99b12..0330122 100644 --- a/custom_components/ha_text_ai/__init__.py +++ b/custom_components/ha_text_ai/__init__.py @@ -43,8 +43,6 @@ from .const import ( SERVICE_SET_SYSTEM_PROMPT, ) -DOMAIN = "ha_text_ai" - _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -228,7 +226,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint) - # Создаем API клиент api_client = APIClient( session=session, endpoint=endpoint, diff --git a/custom_components/ha_text_ai/api_client.py b/custom_components/ha_text_ai/api_client.py index eb4806c..727af26 100644 --- a/custom_components/ha_text_ai/api_client.py +++ b/custom_components/ha_text_ai/api_client.py @@ -1,9 +1,21 @@ """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__) @@ -12,7 +24,7 @@ class APIClient: def __init__( self, - session: Any, + session: ClientSession, endpoint: str, headers: Dict[str, str], api_provider: str, @@ -24,6 +36,51 @@ class APIClient: 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, @@ -34,7 +91,9 @@ class APIClient: ) -> Dict[str, Any]: """Create completion using appropriate API.""" try: - if self.api_provider == "anthropic": + self._validate_parameters(temperature, max_tokens) + + if self.api_provider == API_PROVIDER_ANTHROPIC: return await self._create_anthropic_completion( model, messages, temperature, max_tokens ) @@ -62,26 +121,21 @@ class APIClient: "max_tokens": max_tokens, } - async with self.session.post(url, json=payload, headers=self.headers) as response: - if response.status != 200: - error_data = await response.json() - raise HomeAssistantError(f"OpenAI API error: {error_data}") - - data = await response.json() - return { - "choices": [ - { - "message": { - "content": data["choices"][0]["message"]["content"] - } + 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"] } + ], + "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, @@ -94,7 +148,10 @@ class APIClient: 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) + 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 = { @@ -106,23 +163,18 @@ class APIClient: if system_prompt: payload["system"] = system_prompt - async with self.session.post(url, json=payload, headers=self.headers) as response: - if response.status != 200: - error_data = await response.json() - raise HomeAssistantError(f"Anthropic API error: {error_data}") - - data = await response.json() - return { - "choices": [ - { - "message": { - "content": data["content"][0]["text"] - } + 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"] } + ], + "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/custom_components/ha_text_ai/coordinator.py b/custom_components/ha_text_ai/coordinator.py index 8516a98..f1dc2fb 100644 --- a/custom_components/ha_text_ai/coordinator.py +++ b/custom_components/ha_text_ai/coordinator.py @@ -1,49 +1,24 @@ -"""The HA Text AI integration.""" +"""The HA Text AI coordinator.""" from __future__ import annotations import logging -import os -import shutil from datetime import datetime, timedelta -from typing import Any, Dict +from typing import Any, Dict, List, Optional -import voluptuous as vol -from async_timeout import timeout +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util -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, - API_PROVIDER_OPENAI, - API_PROVIDER_ANTHROPIC, - DEFAULT_MODEL, - DEFAULT_TEMPERATURE, - DEFAULT_MAX_TOKENS, - DEFAULT_OPENAI_ENDPOINT, - DEFAULT_ANTHROPIC_ENDPOINT, - DEFAULT_REQUEST_INTERVAL, - API_TIMEOUT, - SERVICE_ASK_QUESTION, - SERVICE_CLEAR_HISTORY, - SERVICE_GET_HISTORY, - SERVICE_SET_SYSTEM_PROMPT, + STATE_READY, + STATE_PROCESSING, + STATE_ERROR, + STATE_RATE_LIMITED, + STATE_MAINTENANCE, ) -_LOGGER = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) class HATextAICoordinator(DataUpdateCoordinator): """Class to manage fetching data from the API."""