mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-21 22:54:00 +08:00
Release v2.0.0-alpha
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
"""The HA Text AI integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict
|
||||
|
||||
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,
|
||||
PLATFORMS,
|
||||
CONF_MODEL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_API_ENDPOINT,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
CONF_API_PROVIDER,
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_OPENAI_ENDPOINT,
|
||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
API_TIMEOUT,
|
||||
SERVICE_ASK_QUESTION,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_GET_HISTORY,
|
||||
SERVICE_SET_SYSTEM_PROMPT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||
vol.Required("instance"): cv.string,
|
||||
vol.Required("question"): cv.string,
|
||||
vol.Optional("system_prompt"): cv.string,
|
||||
vol.Optional("model"): cv.string,
|
||||
vol.Optional("temperature"): cv.positive_float,
|
||||
vol.Optional("max_tokens"): cv.positive_int,
|
||||
vol.Optional("context_messages"): cv.positive_int,
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||
vol.Required("instance"): cv.string,
|
||||
vol.Required("prompt"): cv.string,
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||
vol.Required("instance"): cv.string,
|
||||
vol.Optional("limit"): cv.positive_int,
|
||||
vol.Optional("filter_model"): cv.string,
|
||||
})
|
||||
|
||||
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
|
||||
"""Get coordinator by instance name."""
|
||||
if instance.startswith("sensor."):
|
||||
instance = instance.replace("sensor.ha_text_ai_", "", 1)
|
||||
|
||||
for entry_id, coord in hass.data[DOMAIN].items():
|
||||
if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower():
|
||||
return coord
|
||||
|
||||
raise HomeAssistantError(f"Instance {instance} not found")
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
||||
"""Set up the HA Text AI component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
try:
|
||||
source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg')
|
||||
dest_dir = os.path.join(hass.config.path('www'), 'icons')
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
dest = os.path.join(dest_dir, 'icon.png')
|
||||
if not os.path.exists(dest):
|
||||
shutil.copyfile(source, dest)
|
||||
except Exception as ex:
|
||||
_LOGGER.warning("Failed to copy custom icon: %s", str(ex))
|
||||
|
||||
async def async_ask_question(call: ServiceCall) -> None:
|
||||
"""Handle ask_question service."""
|
||||
try:
|
||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||
await coordinator.async_ask_question(
|
||||
question=call.data["question"],
|
||||
model=call.data.get("model"),
|
||||
temperature=call.data.get("temperature"),
|
||||
max_tokens=call.data.get("max_tokens"),
|
||||
system_prompt=call.data.get("system_prompt"),
|
||||
context_messages=call.data.get("context_messages"),
|
||||
)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error asking question: %s", str(err))
|
||||
raise HomeAssistantError(f"Failed to process question: {str(err)}")
|
||||
|
||||
async def async_clear_history(call: ServiceCall) -> None:
|
||||
"""Handle clear_history service."""
|
||||
try:
|
||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||
await coordinator.async_clear_history()
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error clearing history: %s", str(err))
|
||||
raise HomeAssistantError(f"Failed to clear history: {str(err)}")
|
||||
|
||||
async def async_get_history(call: ServiceCall) -> list:
|
||||
"""Handle get_history service."""
|
||||
try:
|
||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||
return await coordinator.async_get_history(
|
||||
limit=call.data.get("limit"),
|
||||
filter_model=call.data.get("filter_model")
|
||||
)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error getting history: %s", str(err))
|
||||
raise HomeAssistantError(f"Failed to get history: {str(err)}")
|
||||
|
||||
async def async_set_system_prompt(call: ServiceCall) -> None:
|
||||
"""Handle set_system_prompt service."""
|
||||
try:
|
||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||
await coordinator.async_set_system_prompt(call.data["prompt"])
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error setting system prompt: %s", str(err))
|
||||
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_ASK_QUESTION,
|
||||
async_ask_question,
|
||||
schema=SERVICE_SCHEMA_ASK_QUESTION
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
async_clear_history,
|
||||
schema=vol.Schema({vol.Required("instance"): cv.string})
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_HISTORY,
|
||||
async_get_history,
|
||||
schema=SERVICE_SCHEMA_GET_HISTORY
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SYSTEM_PROMPT,
|
||||
async_set_system_prompt,
|
||||
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
|
||||
"""Check API availability for different providers."""
|
||||
try:
|
||||
if provider == API_PROVIDER_ANTHROPIC:
|
||||
check_url = f"{endpoint}/v1/models"
|
||||
else: # OpenAI
|
||||
check_url = f"{endpoint}/models"
|
||||
|
||||
async with timeout(API_TIMEOUT):
|
||||
async with session.get(check_url, headers=headers) as response:
|
||||
if response.status in [200, 404]:
|
||||
return True
|
||||
elif response.status == 401:
|
||||
raise ConfigEntryNotReady("Invalid API key")
|
||||
elif response.status == 429:
|
||||
_LOGGER.warning("Rate limit exceeded during API check")
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error("API check failed with status: %d", response.status)
|
||||
return False
|
||||
except Exception as ex:
|
||||
_LOGGER.error("API check error: %s", str(ex))
|
||||
return False
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up HA Text AI from a config entry."""
|
||||
try:
|
||||
if CONF_API_PROVIDER not in entry.data:
|
||||
_LOGGER.error("API provider not specified")
|
||||
raise ConfigEntryNotReady("API provider is required")
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
api_provider = entry.data.get(CONF_API_PROVIDER)
|
||||
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||
endpoint = entry.data.get(
|
||||
CONF_API_ENDPOINT,
|
||||
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
|
||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||
).rstrip('/')
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
if is_anthropic:
|
||||
headers["x-api-key"] = api_key
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
else:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
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_client = APIClient(
|
||||
session=session,
|
||||
endpoint=endpoint,
|
||||
headers=headers,
|
||||
api_provider=api_provider,
|
||||
model=model,
|
||||
)
|
||||
|
||||
coordinator = HATextAICoordinator(
|
||||
hass=hass,
|
||||
client=api_client,
|
||||
model=model,
|
||||
update_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
||||
instance_name=instance_name,
|
||||
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
||||
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
||||
is_anthropic=is_anthropic,
|
||||
context_messages=entry.data.get(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
DEFAULT_CONTEXT_MESSAGES
|
||||
),
|
||||
)
|
||||
|
||||
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
|
||||
@@ -0,0 +1,180 @@
|
||||
"""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__)
|
||||
|
||||
class APIClient:
|
||||
"""API Client for OpenAI and Anthropic."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: ClientSession,
|
||||
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
|
||||
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,
|
||||
model: str,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create completion using appropriate API."""
|
||||
try:
|
||||
self._validate_parameters(temperature, max_tokens)
|
||||
|
||||
if self.api_provider == 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,
|
||||
}
|
||||
|
||||
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"]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
"""Config flow for HA text AI integration."""
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_API_ENDPOINT,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
CONF_API_PROVIDER,
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDERS,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
DEFAULT_OPENAI_ENDPOINT,
|
||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
MIN_TEMPERATURE,
|
||||
MAX_TEMPERATURE,
|
||||
MIN_MAX_TOKENS,
|
||||
MAX_MAX_TOKENS,
|
||||
MIN_REQUEST_INTERVAL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for HA text AI."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow."""
|
||||
self._errors = {}
|
||||
self._data = {}
|
||||
self._provider = None
|
||||
|
||||
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=API_PROVIDERS,
|
||||
translation_key="api_provider"
|
||||
)
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
self._provider = user_input[CONF_API_PROVIDER]
|
||||
return await self.async_step_provider()
|
||||
|
||||
async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||
"""Handle provider configuration step."""
|
||||
if user_input is None:
|
||||
default_endpoint = (
|
||||
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
|
||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||
)
|
||||
|
||||
suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="provider",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_NAME, default=suggested_name): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
|
||||
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
|
||||
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||
),
|
||||
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
default=DEFAULT_CONTEXT_MESSAGES
|
||||
): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=20)
|
||||
),
|
||||
}),
|
||||
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()
|
||||
|
||||
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."""
|
||||
for entry in self._async_current_entries():
|
||||
if entry.data.get(CONF_NAME) == name:
|
||||
self._errors["name"] = "name_exists"
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
|
||||
"""Validate API connection."""
|
||||
try:
|
||||
session = async_get_clientsession(self.hass)
|
||||
headers = self._get_api_headers(user_input)
|
||||
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
||||
|
||||
check_url = (
|
||||
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
|
||||
else f"{endpoint}/models"
|
||||
)
|
||||
|
||||
async with session.get(check_url, headers=headers) as response:
|
||||
if response.status == 401:
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
elif response.status not in [200, 404]:
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
return True
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error("API validation error: %s", str(err))
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
|
||||
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Get API headers based on provider."""
|
||||
api_key = user_input[CONF_API_KEY]
|
||||
|
||||
if self._provider == API_PROVIDER_ANTHROPIC:
|
||||
return {
|
||||
"x-api-key": api_key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
|
||||
"""Create the config entry."""
|
||||
instance_name = user_input[CONF_NAME]
|
||||
unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=instance_name,
|
||||
data={
|
||||
CONF_API_PROVIDER: self._provider,
|
||||
CONF_NAME: instance_name,
|
||||
**user_input,
|
||||
"unique_id": unique_id,
|
||||
CONF_CONTEXT_MESSAGES: user_input.get(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
DEFAULT_CONTEXT_MESSAGES)
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle options flow."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_MODEL,
|
||||
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||
): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=current_data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
|
||||
): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_REQUEST_INTERVAL,
|
||||
default=current_data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
|
||||
): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
default=current_data.get(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
DEFAULT_CONTEXT_MESSAGES
|
||||
)
|
||||
): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=20)
|
||||
),
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Constants for the HA text AI integration."""
|
||||
from typing import Final
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
# Domain and platforms
|
||||
DOMAIN: Final = "ha_text_ai"
|
||||
PLATFORMS: Final = [Platform.SENSOR]
|
||||
|
||||
# Provider configuration
|
||||
CONF_API_PROVIDER: Final = "api_provider"
|
||||
API_PROVIDER_OPENAI: Final = "openai"
|
||||
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
||||
|
||||
API_PROVIDERS: Final = [
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC
|
||||
]
|
||||
|
||||
# Default endpoints
|
||||
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
||||
|
||||
# Configuration constants
|
||||
CONF_MODEL: Final = "model"
|
||||
CONF_TEMPERATURE: Final = "temperature"
|
||||
CONF_MAX_TOKENS: Final = "max_tokens"
|
||||
CONF_API_ENDPOINT: Final = "api_endpoint"
|
||||
CONF_REQUEST_INTERVAL: Final = "request_interval"
|
||||
CONF_INSTANCE: Final = "instance"
|
||||
CONF_MAX_HISTORY_SIZE: Final = "max_history_size"
|
||||
CONF_IS_ANTHROPIC: Final = "is_anthropic"
|
||||
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
||||
|
||||
# Default values
|
||||
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||
DEFAULT_TEMPERATURE: Final = 0.1
|
||||
DEFAULT_MAX_TOKENS: Final = 1000
|
||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||
DEFAULT_TIMEOUT: Final = 30
|
||||
DEFAULT_MAX_HISTORY: Final = 50
|
||||
DEFAULT_NAME: Final = "HA Text AI"
|
||||
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
||||
|
||||
# Parameter constraints
|
||||
MIN_TEMPERATURE: Final = 0.0
|
||||
MAX_TEMPERATURE: Final = 2.0
|
||||
MIN_MAX_TOKENS: Final = 1
|
||||
MAX_MAX_TOKENS: Final = 4096
|
||||
MIN_REQUEST_INTERVAL: Final = 0.1
|
||||
MAX_REQUEST_INTERVAL: Final = 60.0
|
||||
|
||||
# API constants
|
||||
API_TIMEOUT: Final = 30
|
||||
API_RETRY_COUNT: Final = 3
|
||||
|
||||
# Service names
|
||||
SERVICE_ASK_QUESTION: Final = "ask_question"
|
||||
SERVICE_CLEAR_HISTORY: Final = "clear_history"
|
||||
SERVICE_GET_HISTORY: Final = "get_history"
|
||||
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
|
||||
|
||||
# Attribute keys
|
||||
ATTR_QUESTION: Final = "question"
|
||||
ATTR_RESPONSE: Final = "response"
|
||||
ATTR_INSTANCE: Final = "instance"
|
||||
ATTR_MODEL: Final = "model"
|
||||
ATTR_TEMPERATURE: Final = "temperature"
|
||||
ATTR_MAX_TOKENS: Final = "max_tokens"
|
||||
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
|
||||
ATTR_API_STATUS: Final = "api_status"
|
||||
ATTR_ERROR_COUNT: Final = "error_count"
|
||||
ATTR_CONVERSATION_HISTORY: Final = "conversation_history"
|
||||
|
||||
# Sensor attributes
|
||||
ATTR_TOTAL_RESPONSES: Final = "total_responses"
|
||||
ATTR_TOTAL_ERRORS: Final = "total_errors"
|
||||
ATTR_AVG_RESPONSE_TIME: Final = "average_response_time"
|
||||
ATTR_LAST_REQUEST_TIME: Final = "last_request_time"
|
||||
ATTR_LAST_ERROR: Final = "last_error"
|
||||
ATTR_IS_PROCESSING: Final = "is_processing"
|
||||
ATTR_IS_RATE_LIMITED: Final = "is_rate_limited"
|
||||
ATTR_IS_MAINTENANCE: Final = "is_maintenance"
|
||||
ATTR_API_VERSION: Final = "api_version"
|
||||
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
|
||||
ATTR_PERFORMANCE_METRICS: Final = "performance_metrics"
|
||||
ATTR_HISTORY_SIZE: Final = "history_size"
|
||||
ATTR_UPTIME: Final = "uptime"
|
||||
ATTR_API_PROVIDER: Final = "api_provider"
|
||||
ATTR_METRICS: Final = "metrics"
|
||||
ATTR_STATE: Final = "state"
|
||||
ATTR_LAST_RESPONSE: Final = "last_response"
|
||||
ATTR_ERROR: Final = "error"
|
||||
ATTR_TIMESTAMP: Final = "timestamp"
|
||||
|
||||
# Sensor metrics
|
||||
METRIC_TOTAL_TOKENS: Final = "total_tokens"
|
||||
METRIC_PROMPT_TOKENS: Final = "prompt_tokens"
|
||||
METRIC_COMPLETION_TOKENS: Final = "completion_tokens"
|
||||
METRIC_SUCCESSFUL_REQUESTS: Final = "successful_requests"
|
||||
METRIC_FAILED_REQUESTS: Final = "failed_requests"
|
||||
METRIC_AVERAGE_LATENCY: Final = "average_latency"
|
||||
METRIC_MAX_LATENCY: Final = "max_latency"
|
||||
METRIC_MIN_LATENCY: Final = "min_latency"
|
||||
|
||||
# Error messages
|
||||
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
|
||||
ERROR_CANNOT_CONNECT: Final = "cannot_connect"
|
||||
ERROR_UNKNOWN: Final = "unknown_error"
|
||||
ERROR_INVALID_MODEL: Final = "invalid_model"
|
||||
ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
|
||||
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
|
||||
ERROR_API_ERROR: Final = "api_error"
|
||||
ERROR_TIMEOUT: Final = "timeout_error"
|
||||
ERROR_INVALID_INSTANCE: Final = "invalid_instance"
|
||||
ERROR_NAME_EXISTS: Final = "name_exists"
|
||||
|
||||
# Entity attributes
|
||||
ENTITY_ICON: Final = "mdi:robot"
|
||||
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
|
||||
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
|
||||
|
||||
# State attributes
|
||||
STATE_READY: Final = "ready"
|
||||
STATE_PROCESSING: Final = "processing"
|
||||
STATE_ERROR: Final = "error"
|
||||
STATE_INITIALIZING: Final = "initializing"
|
||||
STATE_MAINTENANCE: Final = "maintenance"
|
||||
STATE_RATE_LIMITED: Final = "rate_limited"
|
||||
STATE_DISCONNECTED: Final = "disconnected"
|
||||
|
||||
# Event names
|
||||
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
|
||||
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
|
||||
EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
|
||||
|
||||
# Service schema constants
|
||||
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||
vol.Required(CONF_INSTANCE): cv.string,
|
||||
vol.Required("question"): cv.string,
|
||||
vol.Optional("system_prompt"): cv.string,
|
||||
vol.Optional("model"): cv.string,
|
||||
vol.Optional("temperature"): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional("max_tokens"): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||
),
|
||||
vol.Optional("context_messages"): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=20)
|
||||
)
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||
vol.Required(CONF_INSTANCE): cv.string,
|
||||
vol.Required("prompt"): cv.string
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||
vol.Required(CONF_INSTANCE): cv.string,
|
||||
vol.Optional("limit", default=10): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=100)
|
||||
),
|
||||
vol.Optional("filter_model"): cv.string
|
||||
})
|
||||
|
||||
# Configuration schema
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS),
|
||||
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
|
||||
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||
),
|
||||
vol.Optional(CONF_API_ENDPOINT): cv.string,
|
||||
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
||||
),
|
||||
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=100),
|
||||
),
|
||||
vol.Optional(CONF_CONTEXT_MESSAGES, default=DEFAULT_CONTEXT_MESSAGES): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=20)
|
||||
)
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@@ -0,0 +1,377 @@
|
||||
"""The HA Text AI coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
STATE_READY,
|
||||
STATE_PROCESSING,
|
||||
STATE_ERROR,
|
||||
STATE_RATE_LIMITED,
|
||||
STATE_MAINTENANCE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class HATextAICoordinator(DataUpdateCoordinator):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: Any,
|
||||
model: str,
|
||||
update_interval: int,
|
||||
instance_name: str,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
max_history_size: int = DEFAULT_MAX_HISTORY,
|
||||
context_messages: int = DEFAULT_CONTEXT_MESSAGES,
|
||||
is_anthropic: bool = False,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
self.instance_name = instance_name
|
||||
self.hass = hass
|
||||
self.client = client
|
||||
self.model = model
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self.max_history_size = max_history_size
|
||||
self.is_anthropic = is_anthropic
|
||||
|
||||
# Initialize with default state
|
||||
self._initial_state = {
|
||||
"state": STATE_READY,
|
||||
"metrics": {
|
||||
"total_tokens": 0,
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"successful_requests": 0,
|
||||
"failed_requests": 0,
|
||||
"total_errors": 0,
|
||||
"average_latency": 0,
|
||||
"max_latency": 0,
|
||||
"min_latency": float('inf'),
|
||||
},
|
||||
"last_response": {
|
||||
"timestamp": dt_util.utcnow().isoformat(),
|
||||
"question": "",
|
||||
"response": "",
|
||||
"model": model,
|
||||
"instance": instance_name,
|
||||
"error": None
|
||||
},
|
||||
"is_processing": False,
|
||||
"is_rate_limited": False,
|
||||
"is_maintenance": False,
|
||||
"endpoint_status": "ready",
|
||||
"uptime": 0,
|
||||
"system_prompt": None,
|
||||
"history_size": 0,
|
||||
"conversation_history": [],
|
||||
}
|
||||
|
||||
update_interval_td = timedelta(seconds=update_interval)
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=instance_name,
|
||||
update_interval=update_interval_td,
|
||||
)
|
||||
|
||||
# Register instance
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
self.hass.data[DOMAIN][instance_name] = self
|
||||
self.context_messages = context_messages
|
||||
|
||||
self._system_prompt = None
|
||||
self._conversation_history = []
|
||||
self._performance_metrics = self._initial_state["metrics"].copy()
|
||||
self._is_processing = False
|
||||
self._is_rate_limited = False
|
||||
self._is_maintenance = False
|
||||
self.endpoint_status = "ready"
|
||||
self.last_response = self._initial_state["last_response"].copy()
|
||||
self._start_time = dt_util.utcnow()
|
||||
|
||||
_LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}")
|
||||
|
||||
async def _async_update_data(self) -> Dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
try:
|
||||
current_state = self._get_current_state()
|
||||
_LOGGER.debug(f"Updating data for {self.instance_name}, current state: {current_state}")
|
||||
|
||||
data = {
|
||||
"state": current_state,
|
||||
"metrics": self._performance_metrics,
|
||||
"last_response": self.last_response,
|
||||
"is_processing": self._is_processing,
|
||||
"is_rate_limited": self._is_rate_limited,
|
||||
"is_maintenance": self._is_maintenance,
|
||||
"endpoint_status": self.endpoint_status,
|
||||
"uptime": (dt_util.utcnow() - self._start_time).total_seconds(),
|
||||
"system_prompt": self._system_prompt,
|
||||
"history_size": len(self._conversation_history),
|
||||
"conversation_history": self._conversation_history,
|
||||
}
|
||||
|
||||
# Validate data
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("Invalid data format")
|
||||
|
||||
_LOGGER.debug(f"Updated data for {self.instance_name}: {data}")
|
||||
return data
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error(f"Error updating data for {self.instance_name}: {err}")
|
||||
return self._initial_state
|
||||
|
||||
async def async_update_ha_state(self) -> None:
|
||||
"""Update Home Assistant state."""
|
||||
try:
|
||||
_LOGGER.debug(f"Requesting state update for {self.instance_name}")
|
||||
await self.async_request_refresh()
|
||||
|
||||
# Force update of all entities
|
||||
for entity_id in self.hass.states.async_entity_ids():
|
||||
if entity_id.startswith(f"sensor.ha_text_ai_{self.instance_name}"):
|
||||
self.hass.states.async_set(entity_id, self._get_current_state())
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error(f"Error updating HA state for {self.instance_name}: {err}")
|
||||
|
||||
def _get_current_state(self) -> str:
|
||||
"""Get current state based on internal flags."""
|
||||
if self._is_processing:
|
||||
return STATE_PROCESSING
|
||||
elif self._is_rate_limited:
|
||||
return STATE_RATE_LIMITED
|
||||
elif self._is_maintenance:
|
||||
return STATE_MAINTENANCE
|
||||
elif self.last_response.get("error"):
|
||||
return STATE_ERROR
|
||||
return STATE_READY
|
||||
|
||||
def _calculate_context_tokens(self, messages: List[Dict[str, str]]) -> int:
|
||||
try:
|
||||
if self.is_anthropic and hasattr(self.client, 'count_tokens'):
|
||||
return sum(self.client.count_tokens(msg['content']) for msg in messages)
|
||||
|
||||
return sum(len(msg['content']) // 4 for msg in messages)
|
||||
except Exception as e:
|
||||
_LOGGER.warning(f"Error calculating context tokens: {e}")
|
||||
return 0
|
||||
|
||||
async def async_ask_question(
|
||||
self,
|
||||
question: str,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
context_messages: Optional[int] = None,
|
||||
) -> dict:
|
||||
"""Process a question with optional parameters."""
|
||||
return await self.async_process_question(
|
||||
question, model, temperature, max_tokens, system_prompt, context_messages
|
||||
)
|
||||
|
||||
async def async_process_question(
|
||||
self,
|
||||
question: str,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
context_messages: Optional[int] = None,
|
||||
) -> dict:
|
||||
temp_context_messages = context_messages or self.context_messages
|
||||
|
||||
if not question:
|
||||
raise ValueError("Question cannot be empty")
|
||||
|
||||
_LOGGER.debug(f"Processing question for instance {self.instance_name}")
|
||||
|
||||
try:
|
||||
self._is_processing = True
|
||||
await self.async_update_ha_state()
|
||||
|
||||
temp_model = model or self.model
|
||||
temp_temperature = temperature or self.temperature
|
||||
temp_max_tokens = max_tokens or self.max_tokens
|
||||
temp_system_prompt = system_prompt or self._system_prompt
|
||||
|
||||
start_time = dt_util.utcnow()
|
||||
|
||||
messages = []
|
||||
if temp_system_prompt:
|
||||
if self.is_anthropic:
|
||||
system_content = f"\n\nHuman: {temp_system_prompt}\n\nAssistant: I understand and will follow these instructions."
|
||||
messages.append({"role": "user", "content": system_content})
|
||||
else:
|
||||
messages.append({"role": "system", "content": temp_system_prompt})
|
||||
|
||||
# Add conversation history
|
||||
context_history = self._conversation_history[-temp_context_messages:]
|
||||
for entry in context_history:
|
||||
messages.append({"role": "user", "content": entry["question"]})
|
||||
messages.append({"role": "assistant", "content": entry["response"]})
|
||||
|
||||
messages.append({"role": "user", "content": question})
|
||||
|
||||
kwargs = {
|
||||
"model": temp_model,
|
||||
"temperature": temp_temperature,
|
||||
"max_tokens": temp_max_tokens,
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
response = await self.async_process_message(question, **kwargs)
|
||||
|
||||
# Update metrics
|
||||
end_time = dt_util.utcnow()
|
||||
latency = (end_time - start_time).total_seconds()
|
||||
self._update_metrics(latency, response)
|
||||
|
||||
# Update history
|
||||
self._update_history(question, response)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as err:
|
||||
self._handle_error(err)
|
||||
raise HomeAssistantError(f"Failed to process question: {err}")
|
||||
|
||||
finally:
|
||||
self._is_processing = False
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_process_message(self, question: str, **kwargs) -> dict:
|
||||
"""Process message using the AI client."""
|
||||
try:
|
||||
if self.is_anthropic:
|
||||
response = await self._process_anthropic_message(question, **kwargs)
|
||||
else:
|
||||
response = await self._process_openai_message(question, **kwargs)
|
||||
|
||||
self.last_response = {
|
||||
"timestamp": dt_util.utcnow().isoformat(),
|
||||
"question": question,
|
||||
"response": response["content"],
|
||||
"model": kwargs.get("model", self.model),
|
||||
"instance": self.instance_name,
|
||||
"error": None
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
except Exception as err:
|
||||
self._handle_error(err)
|
||||
raise
|
||||
|
||||
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
||||
"""Process message using Anthropic API."""
|
||||
response = await self.client.messages.create(
|
||||
model=kwargs["model"],
|
||||
max_tokens=kwargs["max_tokens"],
|
||||
messages=kwargs["messages"],
|
||||
temperature=kwargs["temperature"],
|
||||
)
|
||||
return {
|
||||
"content": response.content[0].text,
|
||||
"tokens": {
|
||||
"prompt": response.usage.input_tokens,
|
||||
"completion": response.usage.output_tokens,
|
||||
"total": response.usage.input_tokens + response.usage.output_tokens
|
||||
}
|
||||
}
|
||||
|
||||
async def _process_openai_message(self, question: str, **kwargs) -> dict:
|
||||
"""Process message using OpenAI API."""
|
||||
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."""
|
||||
metrics = self._performance_metrics
|
||||
tokens = response.get("tokens", {})
|
||||
|
||||
metrics["total_tokens"] += tokens.get("total", 0)
|
||||
metrics["prompt_tokens"] += tokens.get("prompt", 0)
|
||||
metrics["completion_tokens"] += tokens.get("completion", 0)
|
||||
metrics["successful_requests"] += 1
|
||||
|
||||
metrics["average_latency"] = (
|
||||
(metrics["average_latency"] * (metrics["successful_requests"] - 1) + latency)
|
||||
/ metrics["successful_requests"]
|
||||
)
|
||||
metrics["max_latency"] = max(metrics["max_latency"], latency)
|
||||
metrics["min_latency"] = min(metrics["min_latency"], latency)
|
||||
|
||||
def _update_history(self, question: str, response: dict) -> None:
|
||||
"""Update conversation history."""
|
||||
self._conversation_history.append({
|
||||
"timestamp": dt_util.utcnow().isoformat(),
|
||||
"question": question,
|
||||
"response": response["content"]
|
||||
})
|
||||
|
||||
while len(self._conversation_history) > self.max_history_size:
|
||||
self._conversation_history.pop(0)
|
||||
|
||||
def _handle_error(self, error: Exception) -> None:
|
||||
"""Handle error and update metrics."""
|
||||
self._performance_metrics["total_errors"] += 1
|
||||
self._performance_metrics["failed_requests"] += 1
|
||||
|
||||
self.last_response = {
|
||||
"timestamp": dt_util.utcnow().isoformat(),
|
||||
"question": "",
|
||||
"response": "",
|
||||
"model": self.model,
|
||||
"instance": self.instance_name,
|
||||
"error": str(error)
|
||||
}
|
||||
|
||||
async def async_clear_history(self) -> None:
|
||||
"""Clear conversation history."""
|
||||
self._conversation_history = []
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_get_history(self) -> List[Dict[str, str]]:
|
||||
"""Get conversation history."""
|
||||
return self._conversation_history
|
||||
|
||||
async def async_set_system_prompt(self, prompt: str) -> None:
|
||||
"""Set system prompt."""
|
||||
self._system_prompt = prompt
|
||||
await self.async_update_ha_state()
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"domain": "ha_text_ai",
|
||||
"name": "HA Text AI",
|
||||
"after_dependencies": ["http"],
|
||||
"bluetooth": [],
|
||||
"codeowners": ["@smkrv"],
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"documentation": "https://github.com/smkrv/ha-text-ai",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
|
||||
"loggers": ["custom_components.ha_text_ai"],
|
||||
"mqtt": [],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"openai>=1.12.0",
|
||||
"anthropic>=0.8.0",
|
||||
"aiohttp>=3.8.0",
|
||||
"async-timeout>=4.0.0",
|
||||
"certifi>=2024.2.2"
|
||||
],
|
||||
"single_config_entry": false,
|
||||
"ssdp": [],
|
||||
"usb": [],
|
||||
"version": "2.0.0-alpha",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
"""Sensor platform for HA Text AI."""
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, Dict
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
CONF_API_PROVIDER,
|
||||
ATTR_TOTAL_RESPONSES,
|
||||
ATTR_TOTAL_ERRORS,
|
||||
ATTR_AVG_RESPONSE_TIME,
|
||||
ATTR_LAST_REQUEST_TIME,
|
||||
ATTR_LAST_ERROR,
|
||||
ATTR_IS_PROCESSING,
|
||||
ATTR_IS_RATE_LIMITED,
|
||||
ATTR_IS_MAINTENANCE,
|
||||
ATTR_API_VERSION,
|
||||
ATTR_ENDPOINT_STATUS,
|
||||
ATTR_PERFORMANCE_METRICS,
|
||||
ATTR_HISTORY_SIZE,
|
||||
ATTR_UPTIME,
|
||||
ATTR_API_PROVIDER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SYSTEM_PROMPT,
|
||||
ATTR_API_STATUS,
|
||||
ATTR_RESPONSE,
|
||||
ATTR_QUESTION,
|
||||
ATTR_CONVERSATION_HISTORY,
|
||||
METRIC_TOTAL_TOKENS,
|
||||
METRIC_PROMPT_TOKENS,
|
||||
METRIC_COMPLETION_TOKENS,
|
||||
METRIC_SUCCESSFUL_REQUESTS,
|
||||
METRIC_FAILED_REQUESTS,
|
||||
METRIC_AVERAGE_LATENCY,
|
||||
METRIC_MAX_LATENCY,
|
||||
METRIC_MIN_LATENCY,
|
||||
STATE_READY,
|
||||
STATE_PROCESSING,
|
||||
STATE_ERROR,
|
||||
STATE_INITIALIZING,
|
||||
STATE_MAINTENANCE,
|
||||
STATE_RATE_LIMITED,
|
||||
STATE_DISCONNECTED,
|
||||
ENTITY_ICON,
|
||||
ENTITY_ICON_ERROR,
|
||||
ENTITY_ICON_PROCESSING,
|
||||
)
|
||||
|
||||
from .coordinator import HATextAICoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HA Text AI sensor."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
instance_name = coordinator.instance_name
|
||||
|
||||
_LOGGER.debug(f"Setting up sensor with instance: {instance_name}")
|
||||
|
||||
sensor = HATextAISensor(coordinator, entry)
|
||||
async_add_entities([sensor], True)
|
||||
|
||||
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
||||
"""HA Text AI Sensor."""
|
||||
|
||||
coordinator: HATextAICoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HATextAICoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._config_entry = config_entry
|
||||
self._instance_name = coordinator.instance_name
|
||||
self._conversation_history = []
|
||||
self._system_prompt = None
|
||||
|
||||
self._attr_name = f"HA Text AI {self._instance_name}"
|
||||
self.entity_id = f"sensor.ha_text_ai_{slugify(self._instance_name)}"
|
||||
self._attr_unique_id = f"{config_entry.entry_id}"
|
||||
|
||||
self.entity_description = SensorEntityDescription(
|
||||
key=f"ha_text_ai_{self._instance_name}",
|
||||
entity_registry_enabled_default=True,
|
||||
)
|
||||
|
||||
self._current_state = STATE_INITIALIZING
|
||||
self._error_count = 0
|
||||
self._last_error = None
|
||||
self._last_update = None
|
||||
self._is_processing = False
|
||||
self._last_response = {}
|
||||
self._metrics = {}
|
||||
|
||||
model = config_entry.data.get(CONF_MODEL, "Unknown")
|
||||
api_provider = config_entry.data.get(CONF_API_PROVIDER, "Unknown")
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
name=self._attr_name, # Используем имя сенсора
|
||||
manufacturer="Community",
|
||||
model=f"{model} ({api_provider} provider)",
|
||||
sw_version="1.0.0",
|
||||
)
|
||||
|
||||
_LOGGER.debug(f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}")
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return (
|
||||
self.coordinator.last_update_success
|
||||
and self.coordinator.data is not None
|
||||
and self._current_state != STATE_DISCONNECTED
|
||||
)
|
||||
|
||||
def _sanitize_value(self, value: Any) -> Any:
|
||||
"""Sanitize values for JSON serialization."""
|
||||
if isinstance(value, float):
|
||||
if math.isinf(value) or math.isnan(value):
|
||||
return None
|
||||
return value
|
||||
|
||||
def _sanitize_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Sanitize all attributes for JSON serialization."""
|
||||
return {
|
||||
key: self._sanitize_value(value)
|
||||
for key, value in attributes.items()
|
||||
if value is not None
|
||||
}
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the native value of the sensor."""
|
||||
if not self.coordinator.last_update_success or not self.coordinator.data:
|
||||
self._current_state = STATE_DISCONNECTED
|
||||
return self._current_state
|
||||
|
||||
status = self.coordinator.data.get("state", STATE_READY)
|
||||
self._current_state = status
|
||||
return status
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon based on the current state."""
|
||||
if self._current_state == STATE_ERROR:
|
||||
return ENTITY_ICON_ERROR
|
||||
elif self._current_state == STATE_PROCESSING:
|
||||
return ENTITY_ICON_PROCESSING
|
||||
return ENTITY_ICON
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Dict[str, Any]:
|
||||
"""Return entity specific state attributes."""
|
||||
if not self.coordinator.data:
|
||||
return {}
|
||||
|
||||
try:
|
||||
data = self.coordinator.data
|
||||
attributes = {
|
||||
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
|
||||
ATTR_API_PROVIDER: self._config_entry.data.get(CONF_API_PROVIDER, "Unknown"),
|
||||
ATTR_API_STATUS: self._current_state,
|
||||
ATTR_TOTAL_ERRORS: self._error_count,
|
||||
ATTR_LAST_ERROR: self._last_error,
|
||||
"instance_name": self._instance_name,
|
||||
ATTR_SYSTEM_PROMPT: data.get("system_prompt"),
|
||||
ATTR_IS_PROCESSING: data.get("is_processing", False),
|
||||
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
|
||||
ATTR_IS_MAINTENANCE: data.get("is_maintenance", False),
|
||||
ATTR_ENDPOINT_STATUS: data.get("endpoint_status", "unknown"),
|
||||
ATTR_UPTIME: data.get("uptime", 0),
|
||||
ATTR_HISTORY_SIZE: data.get("history_size", 0),
|
||||
ATTR_CONVERSATION_HISTORY: data.get("conversation_history", []),
|
||||
}
|
||||
|
||||
# Add metrics
|
||||
metrics = data.get("metrics", {})
|
||||
if isinstance(metrics, dict):
|
||||
self._metrics = metrics
|
||||
attributes.update({
|
||||
METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0),
|
||||
METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0),
|
||||
METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0),
|
||||
METRIC_SUCCESSFUL_REQUESTS: metrics.get("successful_requests", 0),
|
||||
METRIC_FAILED_REQUESTS: metrics.get("failed_requests", 0),
|
||||
METRIC_AVERAGE_LATENCY: metrics.get("average_latency", 0),
|
||||
METRIC_MAX_LATENCY: metrics.get("max_latency", 0),
|
||||
METRIC_MIN_LATENCY: metrics.get("min_latency", float("inf")),
|
||||
})
|
||||
|
||||
# Add last response
|
||||
last_response = data.get("last_response", {})
|
||||
if isinstance(last_response, dict):
|
||||
self._last_response = last_response
|
||||
attributes.update({
|
||||
ATTR_RESPONSE: last_response.get("response", ""),
|
||||
ATTR_QUESTION: last_response.get("question", ""),
|
||||
"last_model": last_response.get("model", ""),
|
||||
"last_timestamp": last_response.get("timestamp", ""),
|
||||
"last_error": last_response.get("error"),
|
||||
})
|
||||
|
||||
# Add performance metrics if available
|
||||
if ATTR_PERFORMANCE_METRICS in data:
|
||||
attributes[ATTR_PERFORMANCE_METRICS] = data[ATTR_PERFORMANCE_METRICS]
|
||||
|
||||
# Add API version if available
|
||||
if ATTR_API_VERSION in data:
|
||||
attributes[ATTR_API_VERSION] = data[ATTR_API_VERSION]
|
||||
|
||||
return self._sanitize_attributes(attributes)
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error preparing attributes: %s", err, exc_info=True)
|
||||
return {}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self._handle_coordinator_update()
|
||||
_LOGGER.debug(f"Entity {self.entity_id} added to Home Assistant")
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
data = self.coordinator.data
|
||||
if not self.coordinator.last_update_success or not data:
|
||||
self._current_state = STATE_DISCONNECTED
|
||||
_LOGGER.warning(f"No data available for {self.entity_id}")
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._is_processing = data.get("is_processing", False)
|
||||
|
||||
# Update conversation history and system prompt
|
||||
self._conversation_history = data.get("conversation_history", [])
|
||||
self._system_prompt = data.get("system_prompt")
|
||||
|
||||
# Update state based on conditions
|
||||
if self._is_processing:
|
||||
self._current_state = STATE_PROCESSING
|
||||
elif data.get("is_rate_limited"):
|
||||
self._current_state = STATE_RATE_LIMITED
|
||||
elif data.get("is_maintenance"):
|
||||
self._current_state = STATE_MAINTENANCE
|
||||
elif data.get("error"):
|
||||
self._current_state = STATE_ERROR
|
||||
self._last_error = data["error"]
|
||||
self._error_count += 1
|
||||
else:
|
||||
self._current_state = data.get("state", STATE_READY)
|
||||
|
||||
# Update last update timestamp
|
||||
self._last_update = dt_util.utcnow()
|
||||
|
||||
_LOGGER.debug(
|
||||
f"Updated {self.entity_id} state to: {self._current_state} "
|
||||
f"(available: {self.available})"
|
||||
)
|
||||
|
||||
except Exception as err:
|
||||
self._current_state = STATE_ERROR
|
||||
self._last_error = str(err)
|
||||
self._error_count += 1
|
||||
_LOGGER.error(
|
||||
"Error handling update for %s: %s",
|
||||
self.entity_id,
|
||||
err,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
@@ -0,0 +1,75 @@
|
||||
ask_question:
|
||||
name: Ask Question
|
||||
description: >-
|
||||
Send a question to the AI model and receive a detailed response.
|
||||
The response will be stored in the conversation history and can be retrieved later.
|
||||
fields:
|
||||
instance:
|
||||
name: Instance
|
||||
description: Name of the HA Text AI instance to use
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: ha_text_ai
|
||||
domain: sensor
|
||||
|
||||
question:
|
||||
name: Question
|
||||
description: Your question or prompt for the AI assistant
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
type: text
|
||||
|
||||
system_prompt:
|
||||
name: System Prompt
|
||||
description: Optional system prompt to set context for this specific question
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
|
||||
context_messages:
|
||||
name: Context Messages
|
||||
description: Number of previous messages to include in context (1-20)
|
||||
required: false
|
||||
default: 5
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 20
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
model:
|
||||
name: Model
|
||||
description: "Select AI model to use (optional, overrides default setting)"
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
multiline: false
|
||||
|
||||
temperature:
|
||||
name: Temperature
|
||||
description: Controls response creativity (0.0-2.0)
|
||||
required: false
|
||||
default: 0.7
|
||||
selector:
|
||||
number:
|
||||
min: 0.0
|
||||
max: 2.0
|
||||
step: 0.1
|
||||
mode: slider
|
||||
|
||||
max_tokens:
|
||||
name: Max Tokens
|
||||
description: Maximum length of the response (1-4096 tokens)
|
||||
required: false
|
||||
default: 1000
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 4096
|
||||
step: 1
|
||||
mode: box
|
||||
@@ -0,0 +1,261 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"provider": {
|
||||
"title": "KI-Anbieter auswählen",
|
||||
"description": "Wählen Sie den KI-Dienst-Anbieter für diese Instanz",
|
||||
"data": {
|
||||
"api_provider": "API-Anbieter",
|
||||
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "HA Text AI Instanz konfigurieren",
|
||||
"description": "Richten Sie eine neue KI-Assistenten-Instanz mit Ihrem ausgewählten Anbieter ein",
|
||||
"data": {
|
||||
"name": "Instanzname (z.B. 'GPT Assistent', 'Claude Helfer')",
|
||||
"api_key": "API-Schlüssel für Authentifizierung",
|
||||
"model": "Zu verwendendes KI-Modell",
|
||||
"temperature": "Antwort-Kreativität (0-2, niedriger = fokussierter)",
|
||||
"max_tokens": "Maximale Antwortlänge (1-4096 Token)",
|
||||
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
||||
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "Eine Instanz mit diesem Namen existiert bereits",
|
||||
"invalid_name": "Ungültiger Instanzname",
|
||||
"invalid_auth": "Authentifizierung fehlgeschlagen - überprüfen Sie Ihren API-Schlüssel",
|
||||
"invalid_api_key": "Ungültiger API-Schlüssel - bitte überprüfen Sie Ihre Anmeldedaten",
|
||||
"cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen",
|
||||
"invalid_model": "Ausgewähltes Modell ist nicht verfügbar",
|
||||
"rate_limit": "Anfragelimit überschritten",
|
||||
"context_length": "Kontextlänge überschritten",
|
||||
"rate_limit_exceeded": "API-Anfragelimit überschritten",
|
||||
"maintenance": "Dienst ist in Wartung",
|
||||
"invalid_response": "Ungültige API-Antwort erhalten",
|
||||
"api_error": "API-Dienst-Fehler aufgetreten",
|
||||
"timeout": "Anfrage-Zeitüberschreitung",
|
||||
"invalid_instance": "Ungültige Instanz angegeben",
|
||||
"unknown": "Unerwarteter Fehler aufgetreten"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Instanzeinstellungen aktualisieren",
|
||||
"description": "Einstellungen für diese KI-Assistenten-Instanz ändern",
|
||||
"data": {
|
||||
"model": "KI-Modell",
|
||||
"temperature": "Antwort-Kreativität (0-2)",
|
||||
"max_tokens": "Maximale Antwortlänge (1-4096)",
|
||||
"request_interval": "Minimales Anfragen-Intervall (0,1-60 Sekunden)",
|
||||
"context_messages": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Frage stellen (HA Text AI)",
|
||||
"description": "Senden Sie eine Frage an das KI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird in der Gesprächshistorie gespeichert und kann später abgerufen werden.",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instanz",
|
||||
"description": "Name der zu verwendenden HA Text AI Instanz"
|
||||
},
|
||||
"question": {
|
||||
"name": "Frage",
|
||||
"description": "Ihre Frage oder Eingabeaufforderung für den KI-Assistenten"
|
||||
},
|
||||
"context_messages": {
|
||||
"name": "Kontextnachrichten",
|
||||
"description": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "Systemaufforderung",
|
||||
"description": "Optionale Systemaufforderung zur Kontexteinstellung für diese spezifische Frage"
|
||||
},
|
||||
"model": {
|
||||
"name": "Modell",
|
||||
"description": "Wählen Sie das zu verwendende KI-Modell (optional, überschreibt Standardeinstellung)"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatur",
|
||||
"description": "Steuert die Antwort-Kreativität (0,0-2,0)"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max. Token",
|
||||
"description": "Maximale Länge der Antwort (1-4096 Token)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Verlauf löschen",
|
||||
"description": "Alle gespeicherten Fragen und Antworten aus dem Gesprächsverlauf löschen",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instanz",
|
||||
"description": "Name der HA Text AI Instanz, für die der Verlauf gelöscht werden soll"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_history": {
|
||||
"name": "Verlauf abrufen",
|
||||
"description": "Gesprächsverlauf mit optionaler Filterung und Sortierung abrufen",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instanz",
|
||||
"description": "Name der HA Text AI Instanz, aus der der Verlauf abgerufen werden soll"
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": "Anzahl der zurückzugebenden Gespräche (1-100)"
|
||||
},
|
||||
"filter_model": {
|
||||
"name": "Modell filtern",
|
||||
"description": "Gespräche nach bestimmtem KI-Modell filtern"
|
||||
},
|
||||
"start_date": {
|
||||
"name": "Startdatum",
|
||||
"description": "Gespräche ab diesem Datum/Zeitpunkt filtern"
|
||||
},
|
||||
"include_metadata": {
|
||||
"name": "Metadaten einbeziehen",
|
||||
"description": "Zusätzliche Informationen wie verwendete Token, Antwortzeit usw. einbeziehen"
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "Sortierreihenfolge",
|
||||
"description": "Sortierreihenfolge der Ergebnisse (neueste oder älteste zuerst)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_system_prompt": {
|
||||
"name": "Systemaufforderung festlegen",
|
||||
"description": "Standardmäßige Systemverhaltensinstruktionen für alle zukünftigen Gespräche festlegen",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instanz",
|
||||
"description": "Name der HA Text AI Instanz, für die die Systemaufforderung festgelegt werden soll"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "Systemaufforderung",
|
||||
"description": "Anweisungen, die definieren, wie sich die KI verhalten und antworten soll"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ha_text_ai": {
|
||||
"name": "{name}",
|
||||
"state": {
|
||||
"ready": "Bereit",
|
||||
"processing": "Verarbeitung",
|
||||
"error": "Fehler",
|
||||
"disconnected": "Getrennt",
|
||||
"rate_limited": "Anfragelimit",
|
||||
"maintenance": "Wartung",
|
||||
"initializing": "Initialisierung",
|
||||
"retrying": "Wiederholung",
|
||||
"queued": "In Warteschlange"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
"name": "Letzte Frage"
|
||||
},
|
||||
"response": {
|
||||
"name": "Letzte Antwort"
|
||||
},
|
||||
"model": {
|
||||
"name": "Aktuelles Modell"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatur"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max. Token"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "Systemaufforderung"
|
||||
},
|
||||
"response_time": {
|
||||
"name": "Letzte Antwortzeit"
|
||||
},
|
||||
"total_responses": {
|
||||
"name": "Gesamte Antworten"
|
||||
},
|
||||
"error_count": {
|
||||
"name": "Fehleranzahl"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "Letzter Fehler"
|
||||
},
|
||||
"api_status": {
|
||||
"name": "API-Status"
|
||||
},
|
||||
"tokens_used": {
|
||||
"name": "Insgesamt verwendete Token"
|
||||
},
|
||||
"average_response_time": {
|
||||
"name": "Durchschnittliche Antwortzeit"
|
||||
},
|
||||
"last_request_time": {
|
||||
"name": "Letzte Anforderungszeit"
|
||||
},
|
||||
"is_processing": {
|
||||
"name": "Verarbeitungsstatus"
|
||||
},
|
||||
"is_rate_limited": {
|
||||
"name": "Status Anfragelimit"
|
||||
},
|
||||
"is_maintenance": {
|
||||
"name": "Wartungsstatus"
|
||||
},
|
||||
"api_version": {
|
||||
"name": "API-Version"
|
||||
},
|
||||
"endpoint_status": {
|
||||
"name": "Endpunktstatus"
|
||||
},
|
||||
"performance_metrics": {
|
||||
"name": "Leistungsmetriken"
|
||||
},
|
||||
"history_size": {
|
||||
"name": "Verlaufsgröße"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "Betriebszeit"
|
||||
},
|
||||
"total_tokens": {
|
||||
"name": "Gesamte Token"
|
||||
},
|
||||
"prompt_tokens": {
|
||||
"name": "Prompt-Token"
|
||||
},
|
||||
"completion_tokens": {
|
||||
"name": "Abschluss-Token"
|
||||
},
|
||||
"successful_requests": {
|
||||
"name": "Erfolgreiche Anfragen"
|
||||
},
|
||||
"failed_requests": {
|
||||
"name": "Fehlgeschlagene Anfragen"
|
||||
},
|
||||
"average_latency": {
|
||||
"name": "Durchschnittliche Latenz"
|
||||
},
|
||||
"max_latency": {
|
||||
"name": "Maximale Latenz"
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "Minimale Latenz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"provider": {
|
||||
"title": "Select AI Provider",
|
||||
"description": "Choose which AI service provider to use for this instance",
|
||||
"data": {
|
||||
"api_provider": "API Provider",
|
||||
"context_messages": "Number of context messages to retain (1-20)"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "Configure HA Text AI Instance",
|
||||
"description": "Set up a new AI assistant instance with your selected provider",
|
||||
"data": {
|
||||
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
|
||||
"api_key": "API key for authentication",
|
||||
"model": "AI model to use",
|
||||
"temperature": "Response creativity (0-2, lower = more focused)",
|
||||
"max_tokens": "Maximum response length (1-4096 tokens)",
|
||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||
"context_messages": "Number of context messages to retain (1-20)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "An instance with this name already exists",
|
||||
"invalid_name": "Invalid instance name",
|
||||
"invalid_auth": "Authentication failed - check your API key",
|
||||
"invalid_api_key": "Invalid API key - please verify your credentials",
|
||||
"cannot_connect": "Failed to connect to API service",
|
||||
"invalid_model": "Selected model is not available",
|
||||
"rate_limit": "Rate limit exceeded",
|
||||
"context_length": "Context length exceeded",
|
||||
"rate_limit_exceeded": "API rate limit exceeded",
|
||||
"maintenance": "Service is under maintenance",
|
||||
"invalid_response": "Invalid API response received",
|
||||
"api_error": "API service error occurred",
|
||||
"timeout": "Request timed out",
|
||||
"invalid_instance": "Invalid instance specified",
|
||||
"unknown": "Unexpected error occurred"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Update Instance Settings",
|
||||
"description": "Modify settings for this AI assistant instance",
|
||||
"data": {
|
||||
"model": "AI model",
|
||||
"temperature": "Response creativity (0-2)",
|
||||
"max_tokens": "Maximum response length (1-4096)",
|
||||
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
||||
"context_messages": "Number of previous messages to include in context (1-20)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Ask Question (HA Text AI)",
|
||||
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
"description": "Name of the HA Text AI instance to use"
|
||||
},
|
||||
"question": {
|
||||
"name": "Question",
|
||||
"description": "Your question or prompt for the AI assistant"
|
||||
},
|
||||
"context_messages": {
|
||||
"name": "Context Messages",
|
||||
"description": "Number of previous messages to include in context (1-20)"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "System Prompt",
|
||||
"description": "Optional system prompt to set context for this specific question"
|
||||
},
|
||||
"model": {
|
||||
"name": "Model",
|
||||
"description": "Select AI model to use (optional, overrides default setting)"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature",
|
||||
"description": "Controls response creativity (0.0-2.0)"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens",
|
||||
"description": "Maximum length of the response (1-4096 tokens)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Clear History",
|
||||
"description": "Delete all stored questions and responses from the conversation history",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
"description": "Name of the HA Text AI instance to clear history for"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_history": {
|
||||
"name": "Get History",
|
||||
"description": "Retrieve conversation history with optional filtering and sorting",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
"description": "Name of the HA Text AI instance to get history from"
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": "Number of conversations to return (1-100)"
|
||||
},
|
||||
"filter_model": {
|
||||
"name": "Filter Model",
|
||||
"description": "Filter conversations by specific AI model"
|
||||
},
|
||||
"start_date": {
|
||||
"name": "Start Date",
|
||||
"description": "Filter conversations starting from this date/time"
|
||||
},
|
||||
"include_metadata": {
|
||||
"name": "Include Metadata",
|
||||
"description": "Include additional information like tokens used, response time, etc."
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "Sort Order",
|
||||
"description": "Sort order for results (newest or oldest first)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_system_prompt": {
|
||||
"name": "Set System Prompt",
|
||||
"description": "Set default system behavior instructions for all future conversations",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
"description": "Name of the HA Text AI instance to set system prompt for"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "System Prompt",
|
||||
"description": "Instructions that define how the AI should behave and respond"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ha_text_ai": {
|
||||
"name": "{name}",
|
||||
"state": {
|
||||
"ready": "Ready",
|
||||
"processing": "Processing",
|
||||
"error": "Error",
|
||||
"disconnected": "Disconnected",
|
||||
"rate_limited": "Rate Limited",
|
||||
"maintenance": "Maintenance",
|
||||
"initializing": "Initializing",
|
||||
"retrying": "Retrying",
|
||||
"queued": "Queued"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
"name": "Last Question"
|
||||
},
|
||||
"response": {
|
||||
"name": "Last Response"
|
||||
},
|
||||
"model": {
|
||||
"name": "Current Model"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "System Prompt"
|
||||
},
|
||||
"response_time": {
|
||||
"name": "Last Response Time"
|
||||
},
|
||||
"total_responses": {
|
||||
"name": "Total Responses"
|
||||
},
|
||||
"error_count": {
|
||||
"name": "Error Count"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "Last Error"
|
||||
},
|
||||
"api_status": {
|
||||
"name": "API Status"
|
||||
},
|
||||
"tokens_used": {
|
||||
"name": "Total Tokens Used"
|
||||
},
|
||||
"average_response_time": {
|
||||
"name": "Average Response Time"
|
||||
},
|
||||
"last_request_time": {
|
||||
"name": "Last Request Time"
|
||||
},
|
||||
"is_processing": {
|
||||
"name": "Processing Status"
|
||||
},
|
||||
"is_rate_limited": {
|
||||
"name": "Rate Limited Status"
|
||||
},
|
||||
"is_maintenance": {
|
||||
"name": "Maintenance Status"
|
||||
},
|
||||
"api_version": {
|
||||
"name": "API Version"
|
||||
},
|
||||
"endpoint_status": {
|
||||
"name": "Endpoint Status"
|
||||
},
|
||||
"performance_metrics": {
|
||||
"name": "Performance Metrics"
|
||||
},
|
||||
"history_size": {
|
||||
"name": "History Size"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "Uptime"
|
||||
},
|
||||
"total_tokens": {
|
||||
"name": "Total Tokens"
|
||||
},
|
||||
"prompt_tokens": {
|
||||
"name": "Prompt Tokens"
|
||||
},
|
||||
"completion_tokens": {
|
||||
"name": "Completion Tokens"
|
||||
},
|
||||
"successful_requests": {
|
||||
"name": "Successful Requests"
|
||||
},
|
||||
"failed_requests": {
|
||||
"name": "Failed Requests"
|
||||
},
|
||||
"average_latency": {
|
||||
"name": "Average Latency"
|
||||
},
|
||||
"max_latency": {
|
||||
"name": "Maximum Latency"
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "Minimum Latency"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"provider": {
|
||||
"title": "Выбор провайдера ИИ",
|
||||
"description": "Выберите сервис ИИ для этого экземпляра",
|
||||
"data": {
|
||||
"api_provider": "Провайдер API",
|
||||
"context_messages": "Количество сообщений в контексте (1-20)"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "Настройка экземпляра HA Text AI",
|
||||
"description": "Настройте нового помощника ИИ с выбранным провайдером",
|
||||
"data": {
|
||||
"name": "Название экземпляра (например, 'GPT Помощник', 'Claude Ассистент')",
|
||||
"api_key": "API-ключ для аутентификации",
|
||||
"model": "Модель ИИ для использования",
|
||||
"temperature": "Креативность ответов (0-2, меньше = более сфокусированно)",
|
||||
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
|
||||
"api_endpoint": "Пользовательский URL-адрес API (необязательно)",
|
||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "Экземпляр с таким именем уже существует",
|
||||
"invalid_name": "Некорректное имя экземпляра",
|
||||
"invalid_auth": "Ошибка аутентификации - проверьте API-ключ",
|
||||
"invalid_api_key": "Неверный API-ключ - проверьте учетные данные",
|
||||
"cannot_connect": "Не удалось подключиться к сервису API",
|
||||
"invalid_model": "Выбранная модель недоступна",
|
||||
"rate_limit": "Превышен лимит запросов",
|
||||
"context_length": "Превышена длина контекста",
|
||||
"rate_limit_exceeded": "Превышен лимит API",
|
||||
"maintenance": "Сервис на техническом обслуживании",
|
||||
"invalid_response": "Получен некорректный ответ API",
|
||||
"api_error": "Произошла ошибка сервиса API",
|
||||
"timeout": "Время ожидания истекло",
|
||||
"invalid_instance": "Указан неверный экземпляр",
|
||||
"unknown": "Произошла непредвиденная ошибка"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Обновление настроек экземпляра",
|
||||
"description": "Измените настройки для этого помощника ИИ",
|
||||
"data": {
|
||||
"model": "Модель ИИ",
|
||||
"temperature": "Креативность ответов (0-2)",
|
||||
"max_tokens": "Максимальная длина ответа (1-4096)",
|
||||
"request_interval": "Минимальный интервал запросов (0.1-60 секунд)",
|
||||
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Задать вопрос (HA Text AI)",
|
||||
"description": "Отправьте вопрос модели ИИ и получите подробный ответ. Ответ будет сохранен в истории беседы и может быть извлечен позже.",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Экземпляр",
|
||||
"description": "Название экземпляра HA Text AI для использования"
|
||||
},
|
||||
"question": {
|
||||
"name": "Вопрос",
|
||||
"description": "Ваш вопрос или запрос помощнику ИИ"
|
||||
},
|
||||
"context_messages": {
|
||||
"name": "Контекстные сообщения",
|
||||
"description": "Количество предыдущих сообщений для включения в контекст (1-20)"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "Системный промпт",
|
||||
"description": "Необязательный системный промпт для установки контекста для этого конкретного вопроса"
|
||||
},
|
||||
"model": {
|
||||
"name": "Модель",
|
||||
"description": "Выберите модель ИИ (необязательно, переопределяет настройки по умолчанию)"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Температура",
|
||||
"description": "Управляет креативностью ответа (0.0-2.0)"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Максимальное количество токенов",
|
||||
"description": "Максимальная длина ответа (1-4096 токенов)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Очистить историю",
|
||||
"description": "Удалить все сохраненные вопросы и ответы из истории беседы",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Экземпляр",
|
||||
"description": "Название экземпляра HA Text AI для очистки истории"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_history": {
|
||||
"name": "Получить историю",
|
||||
"description": "Получить историю беседы с дополнительной фильтрацией и сортировкой",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Экземпляр",
|
||||
"description": "Название экземпляра HA Text AI для получения истории"
|
||||
},
|
||||
"limit": {
|
||||
"name": "Лимит",
|
||||
"description": "Количество возвращаемых бесед (1-100)"
|
||||
},
|
||||
"filter_model": {
|
||||
"name": "Фильтр модели",
|
||||
"description": "Фильтрация бесед по конкретной модели ИИ"
|
||||
},
|
||||
"start_date": {
|
||||
"name": "Начальная дата",
|
||||
"description": "Фильтрация бесед, начиная с указанной даты/времени"
|
||||
},
|
||||
"include_metadata": {
|
||||
"name": "Включить метаданные",
|
||||
"description": "Включить дополнительную информацию, например, использованные токены, время ответа и т.д."
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "Порядок сортировки",
|
||||
"description": "Порядок результатов (сначала новые или старые)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_system_prompt": {
|
||||
"name": "Установить системный промпт",
|
||||
"description": "Установить инструкции по умолчанию для поведения ИИ во всех будущих разговорах",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Экземпляр",
|
||||
"description": "Название экземпляра HA Text AI для установки системного промпта"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "Системный промпт",
|
||||
"description": "Инструкции, определяющие, как ИИ должен вести себя и отвечать"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ha_text_ai": {
|
||||
"name": "{name}",
|
||||
"state": {
|
||||
"ready": "Готов",
|
||||
"processing": "Обработка",
|
||||
"error": "Ошибка",
|
||||
"disconnected": "Отключен",
|
||||
"rate_limited": "Лимит запросов",
|
||||
"maintenance": "Техническое обслуживание",
|
||||
"initializing": "Инициализация",
|
||||
"retrying": "Повторная попытка",
|
||||
"queued": "В очереди"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
"name": "Последний вопрос"
|
||||
},
|
||||
"response": {
|
||||
"name": "Последний ответ"
|
||||
},
|
||||
"model": {
|
||||
"name": "Текущая модель"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Температура"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Максимальное количество токенов"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "Системный промпт"
|
||||
},
|
||||
"response_time": {
|
||||
"name": "Время последнего ответа"
|
||||
},
|
||||
"total_responses": {
|
||||
"name": "Всего ответов"
|
||||
},
|
||||
"error_count": {
|
||||
"name": "Количество ошибок"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "Последняя ошибка"
|
||||
},
|
||||
"api_status": {
|
||||
"name": "Статус API"
|
||||
},
|
||||
"tokens_used": {
|
||||
"name": "Всего использовано токенов"
|
||||
},
|
||||
"average_response_time": {
|
||||
"name": "Среднее время ответа"
|
||||
},
|
||||
"last_request_time": {
|
||||
"name": "Время последнего запроса"
|
||||
},
|
||||
"is_processing": {
|
||||
"name": "Статус обработки"
|
||||
},
|
||||
"is_rate_limited": {
|
||||
"name": "Статус лимита запросов"
|
||||
},
|
||||
"is_maintenance": {
|
||||
"name": "Статус обслуживания"
|
||||
},
|
||||
"api_version": {
|
||||
"name": "Версия API"
|
||||
},
|
||||
"endpoint_status": {
|
||||
"name": "Статус эндпоинта"
|
||||
},
|
||||
"performance_metrics": {
|
||||
"name": "Показатели производительности"
|
||||
},
|
||||
"history_size": {
|
||||
"name": "Размер истории"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "Время работы"
|
||||
},
|
||||
"total_tokens": {
|
||||
"name": "Всего токенов"
|
||||
},
|
||||
"prompt_tokens": {
|
||||
"name": "Токены промпта"
|
||||
},
|
||||
"completion_tokens": {
|
||||
"name": "Токены завершения"
|
||||
},
|
||||
"successful_requests": {
|
||||
"name": "Успешные запросы"
|
||||
},
|
||||
"failed_requests": {
|
||||
"name": "Неудачные запросы"
|
||||
},
|
||||
"average_latency": {
|
||||
"name": "Средняя задержка"
|
||||
},
|
||||
"max_latency": {
|
||||
"name": "Максимальная задержка"
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "Минимальная задержка"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user