mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-29 15:03:55 +08:00
Release v2.0.0
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user