diff --git a/custom_components/ha_text_ai/__init__.py b/custom_components/ha_text_ai/__init__.py index 4067724..2abdf3e 100644 --- a/custom_components/ha_text_ai/__init__.py +++ b/custom_components/ha_text_ai/__init__.py @@ -18,6 +18,7 @@ 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, @@ -68,11 +69,9 @@ SERVICE_SCHEMA_GET_HISTORY = vol.Schema({ def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator: """Get coordinator by instance name.""" - # Убираем префикс "sensor." если он есть if instance.startswith("sensor."): instance = instance.replace("sensor.ha_text_ai_", "", 1) - # Ищем координатор по instance_name for entry_id, coord in hass.data[DOMAIN].items(): if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower(): return coord @@ -225,14 +224,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 клиент - api_client = { - "session": session, - "endpoint": endpoint, - "headers": headers, - "api_provider": api_provider, - "model": model, - } + api_client = APIClient( + session=session, + endpoint=endpoint, + headers=headers, + api_provider=api_provider, + model=model, + ) coordinator = HATextAICoordinator( hass=hass, @@ -245,15 +246,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: is_anthropic=is_anthropic, ) - # Инициализация координатора 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: @@ -265,6 +271,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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) diff --git a/custom_components/ha_text_ai/api_client.py b/custom_components/ha_text_ai/api_client.py new file mode 100644 index 0000000..eb4806c --- /dev/null +++ b/custom_components/ha_text_ai/api_client.py @@ -0,0 +1,128 @@ +"""API Client for HA Text AI.""" +import logging +from typing import Any, Dict, List, Optional + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +_LOGGER = logging.getLogger(__name__) + +class APIClient: + """API Client for OpenAI and Anthropic.""" + + def __init__( + self, + session: Any, + 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 + + 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: + if self.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, + } + + 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"] + } + } + ], + "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 + + 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"] + } + } + ], + "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 8dcbb44..8516a98 100644 --- a/custom_components/ha_text_ai/coordinator.py +++ b/custom_components/ha_text_ai/coordinator.py @@ -1,23 +1,49 @@ -"""DataUpdateCoordinator for HA Text AI.""" -from datetime import timedelta +"""The HA Text AI integration.""" +from __future__ import annotations + import logging -from typing import Any, Dict, List, Optional +import os +import shutil +from datetime import datetime, timedelta +from typing import Any, Dict -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util import dt as dt_util +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, - STATE_READY, - STATE_PROCESSING, - STATE_ERROR, - STATE_RATE_LIMITED, - STATE_MAINTENANCE, + 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, ) -_LOGGER = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) class HATextAICoordinator(DataUpdateCoordinator): """Class to manage fetching data from the API.""" @@ -239,20 +265,25 @@ class HATextAICoordinator(DataUpdateCoordinator): async def _process_openai_message(self, question: str, **kwargs) -> dict: """Process message using OpenAI API.""" - response = await self.client.chat.completions.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 + 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."""