Release v2.0.0

This commit is contained in:
SMKRV
2024-11-24 23:14:48 +03:00
parent d05b39d8ae
commit b61429b52d
3 changed files with 207 additions and 38 deletions
+22 -12
View File
@@ -18,6 +18,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .coordinator import HATextAICoordinator from .coordinator import HATextAICoordinator
from .api_client import APIClient
from .const import ( from .const import (
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
@@ -68,11 +69,9 @@ SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator: def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
"""Get coordinator by instance name.""" """Get coordinator by instance name."""
# Убираем префикс "sensor." если он есть
if instance.startswith("sensor."): if instance.startswith("sensor."):
instance = instance.replace("sensor.ha_text_ai_", "", 1) instance = instance.replace("sensor.ha_text_ai_", "", 1)
# Ищем координатор по instance_name
for entry_id, coord in hass.data[DOMAIN].items(): for entry_id, coord in hass.data[DOMAIN].items():
if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower(): if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower():
return coord 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): if not await async_check_api(session, endpoint, headers, api_provider):
raise ConfigEntryNotReady("API connection failed") raise ConfigEntryNotReady("API connection failed")
_LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
# Создаем API клиент # Создаем API клиент
api_client = { api_client = APIClient(
"session": session, session=session,
"endpoint": endpoint, endpoint=endpoint,
"headers": headers, headers=headers,
"api_provider": api_provider, api_provider=api_provider,
"model": model, model=model,
} )
coordinator = HATextAICoordinator( coordinator = HATextAICoordinator(
hass=hass, hass=hass,
@@ -245,15 +246,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
is_anthropic=is_anthropic, is_anthropic=is_anthropic,
) )
# Инициализация координатора
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
# Сохраняем координатор
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
# Загружаем платформы
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 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 return True
except Exception as ex: except Exception as ex:
@@ -265,6 +271,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
if entry.entry_id in hass.data[DOMAIN]: if entry.entry_id in hass.data[DOMAIN]:
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = hass.data[DOMAIN][entry.entry_id]
if hasattr(coordinator.client, 'shutdown'):
await coordinator.client.shutdown()
await coordinator.async_shutdown() await coordinator.async_shutdown()
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
+128
View File
@@ -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"]
}
}
+56 -25
View File
@@ -1,20 +1,46 @@
"""DataUpdateCoordinator for HA Text AI.""" """The HA Text AI integration."""
from datetime import timedelta from __future__ import annotations
import logging 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 import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError from async_timeout import timeout
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 ( from .const import (
DOMAIN, DOMAIN,
STATE_READY, PLATFORMS,
STATE_PROCESSING, CONF_MODEL,
STATE_ERROR, CONF_TEMPERATURE,
STATE_RATE_LIMITED, CONF_MAX_TOKENS,
STATE_MAINTENANCE, 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__)
@@ -239,20 +265,25 @@ class HATextAICoordinator(DataUpdateCoordinator):
async def _process_openai_message(self, question: str, **kwargs) -> dict: async def _process_openai_message(self, question: str, **kwargs) -> dict:
"""Process message using OpenAI API.""" """Process message using OpenAI API."""
response = await self.client.chat.completions.create( try:
model=kwargs["model"], response = await self.client.create(
messages=kwargs["messages"], model=kwargs["model"],
temperature=kwargs["temperature"], messages=kwargs["messages"],
max_tokens=kwargs["max_tokens"], temperature=kwargs["temperature"],
) max_tokens=kwargs["max_tokens"],
return { )
"content": response.choices[0].message.content,
"tokens": { return {
"prompt": response.usage.prompt_tokens, "content": response["choices"][0]["message"]["content"],
"completion": response.usage.completion_tokens, "tokens": {
"total": response.usage.total_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: def _update_metrics(self, latency: float, response: dict) -> None:
"""Update performance metrics.""" """Update performance metrics."""