mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-30 13:13:56 +08:00
Release v2.0.0
This commit is contained in:
@@ -40,23 +40,32 @@ from .const import (
|
|||||||
SERVICE_CLEAR_HISTORY,
|
SERVICE_CLEAR_HISTORY,
|
||||||
SERVICE_GET_HISTORY,
|
SERVICE_GET_HISTORY,
|
||||||
SERVICE_SET_SYSTEM_PROMPT,
|
SERVICE_SET_SYSTEM_PROMPT,
|
||||||
SERVICE_SCHEMA_ASK_QUESTION,
|
|
||||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT,
|
|
||||||
SERVICE_SCHEMA_GET_HISTORY,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
@callback
|
# Обновленные схемы сервисов
|
||||||
def get_coordinator_by_id(hass: HomeAssistant, entity_id: str) -> Optional[HATextAICoordinator]:
|
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||||
"""Get coordinator by entity ID."""
|
vol.Required("instance"): cv.string,
|
||||||
if DOMAIN in hass.data:
|
vol.Required("question"): cv.string,
|
||||||
for entry_id, coordinator in hass.data[DOMAIN].items():
|
vol.Optional("system_prompt"): cv.string,
|
||||||
if coordinator.entity_id == entity_id:
|
vol.Optional("model"): cv.string,
|
||||||
return coordinator
|
vol.Optional("temperature"): cv.positive_float,
|
||||||
return None
|
vol.Optional("max_tokens"): 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,
|
||||||
|
})
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
||||||
"""Set up the HA Text AI component."""
|
"""Set up the HA Text AI component."""
|
||||||
@@ -75,128 +84,93 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
|||||||
|
|
||||||
async def async_ask_question(call: ServiceCall) -> None:
|
async def async_ask_question(call: ServiceCall) -> None:
|
||||||
"""Handle ask_question service."""
|
"""Handle ask_question service."""
|
||||||
entity_id = call.target.get("entity_id")
|
instance = call.data["instance"]
|
||||||
if not entity_id:
|
if instance not in hass.data[DOMAIN]:
|
||||||
raise HomeAssistantError("No target entity specified")
|
raise HomeAssistantError(f"Instance {instance} not found")
|
||||||
|
|
||||||
coordinator = get_coordinator_by_id(hass, entity_id)
|
|
||||||
if not coordinator:
|
|
||||||
raise HomeAssistantError(f"No coordinator found for entity {entity_id}")
|
|
||||||
|
|
||||||
question = call.data.get("question")
|
|
||||||
if not question:
|
|
||||||
raise HomeAssistantError("No question provided")
|
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][instance]["coordinator"]
|
||||||
try:
|
try:
|
||||||
params = {
|
await coordinator.async_ask_question(
|
||||||
"system_prompt": call.data.get("system_prompt"),
|
question=call.data["question"],
|
||||||
"model": call.data.get("model"),
|
model=call.data.get("model"),
|
||||||
"temperature": call.data.get("temperature"),
|
temperature=call.data.get("temperature"),
|
||||||
"max_tokens": call.data.get("max_tokens"),
|
max_tokens=call.data.get("max_tokens"),
|
||||||
"priority": call.data.get("priority", False)
|
system_prompt=call.data.get("system_prompt"),
|
||||||
}
|
)
|
||||||
await coordinator.async_ask_question(question, **params)
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error asking question: %s", str(err))
|
_LOGGER.error("Error asking question: %s", str(err))
|
||||||
raise HomeAssistantError(f"Failed to process question: {str(err)}")
|
raise HomeAssistantError(f"Failed to process question: {str(err)}")
|
||||||
|
|
||||||
async def async_clear_history(call: ServiceCall) -> None:
|
async def async_clear_history(call: ServiceCall) -> None:
|
||||||
"""Handle clear_history service."""
|
"""Handle clear_history service."""
|
||||||
entity_id = call.target.get("entity_id")
|
instance = call.data["instance"]
|
||||||
if not entity_id:
|
if instance not in hass.data[DOMAIN]:
|
||||||
raise HomeAssistantError("No target entity specified")
|
raise HomeAssistantError(f"Instance {instance} not found")
|
||||||
|
|
||||||
coordinator = get_coordinator_by_id(hass, entity_id)
|
|
||||||
if not coordinator:
|
|
||||||
raise HomeAssistantError(f"No coordinator found for entity {entity_id}")
|
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][instance]["coordinator"]
|
||||||
try:
|
try:
|
||||||
await coordinator.clear_history()
|
await coordinator.async_clear_history()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error clearing history: %s", str(err))
|
_LOGGER.error("Error clearing history: %s", str(err))
|
||||||
raise HomeAssistantError(f"Failed to clear history: {str(err)}")
|
raise HomeAssistantError(f"Failed to clear history: {str(err)}")
|
||||||
|
|
||||||
async def async_get_history(call: ServiceCall) -> None:
|
async def async_get_history(call: ServiceCall) -> None:
|
||||||
"""Handle get_history service."""
|
"""Handle get_history service."""
|
||||||
entity_id = call.target.get("entity_id")
|
instance = call.data["instance"]
|
||||||
if not entity_id:
|
if instance not in hass.data[DOMAIN]:
|
||||||
raise HomeAssistantError("No target entity specified")
|
raise HomeAssistantError(f"Instance {instance} not found")
|
||||||
|
|
||||||
coordinator = get_coordinator_by_id(hass, entity_id)
|
|
||||||
if not coordinator:
|
|
||||||
raise HomeAssistantError(f"No coordinator found for entity {entity_id}")
|
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][instance]["coordinator"]
|
||||||
try:
|
try:
|
||||||
limit = call.data.get("limit", 10)
|
return await coordinator.async_get_history(
|
||||||
start_date = call.data.get("start_date")
|
limit=call.data.get("limit"),
|
||||||
include_metadata = call.data.get("include_metadata", False)
|
filter_model=call.data.get("filter_model"),
|
||||||
sort_order = call.data.get("sort_order", "desc")
|
instance=instance
|
||||||
|
|
||||||
history = await coordinator.get_history(
|
|
||||||
limit=limit,
|
|
||||||
start_date=start_date,
|
|
||||||
include_metadata=include_metadata,
|
|
||||||
sort_order=sort_order
|
|
||||||
)
|
)
|
||||||
return history
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error getting history: %s", str(err))
|
_LOGGER.error("Error getting history: %s", str(err))
|
||||||
raise HomeAssistantError(f"Failed to get history: {str(err)}")
|
raise HomeAssistantError(f"Failed to get history: {str(err)}")
|
||||||
|
|
||||||
async def async_set_system_prompt(call: ServiceCall) -> None:
|
async def async_set_system_prompt(call: ServiceCall) -> None:
|
||||||
"""Handle set_system_prompt service."""
|
"""Handle set_system_prompt service."""
|
||||||
entity_id = call.target.get("entity_id")
|
instance = call.data["instance"]
|
||||||
if not entity_id:
|
if instance not in hass.data[DOMAIN]:
|
||||||
raise HomeAssistantError("No target entity specified")
|
raise HomeAssistantError(f"Instance {instance} not found")
|
||||||
|
|
||||||
coordinator = get_coordinator_by_id(hass, entity_id)
|
|
||||||
if not coordinator:
|
|
||||||
raise HomeAssistantError(f"No coordinator found for entity {entity_id}")
|
|
||||||
|
|
||||||
prompt = call.data.get("prompt")
|
|
||||||
if not prompt:
|
|
||||||
raise HomeAssistantError("No prompt provided")
|
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][instance]["coordinator"]
|
||||||
try:
|
try:
|
||||||
coordinator.update_system_prompt(prompt)
|
await coordinator.async_set_system_prompt(call.data["prompt"])
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error setting system prompt: %s", str(err))
|
_LOGGER.error("Error setting system prompt: %s", str(err))
|
||||||
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
|
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
|
||||||
|
|
||||||
# Base schema with target
|
|
||||||
base_schema = vol.Schema({
|
|
||||||
vol.Required("target"): {
|
|
||||||
vol.Required("entity_id"): cv.entity_id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Register services
|
# Register services
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ASK_QUESTION,
|
SERVICE_ASK_QUESTION,
|
||||||
async_ask_question,
|
async_ask_question,
|
||||||
schema=base_schema.extend(SERVICE_SCHEMA_ASK_QUESTION.schema)
|
schema=SERVICE_SCHEMA_ASK_QUESTION
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_CLEAR_HISTORY,
|
SERVICE_CLEAR_HISTORY,
|
||||||
async_clear_history,
|
async_clear_history,
|
||||||
schema=base_schema
|
schema=vol.Schema({vol.Required("instance"): cv.string})
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_GET_HISTORY,
|
SERVICE_GET_HISTORY,
|
||||||
async_get_history,
|
async_get_history,
|
||||||
schema=base_schema.extend(SERVICE_SCHEMA_GET_HISTORY.schema)
|
schema=SERVICE_SCHEMA_GET_HISTORY
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SET_SYSTEM_PROMPT,
|
SERVICE_SET_SYSTEM_PROMPT,
|
||||||
async_set_system_prompt,
|
async_set_system_prompt,
|
||||||
schema=base_schema.extend(SERVICE_SCHEMA_SET_SYSTEM_PROMPT.schema)
|
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -239,6 +213,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
|
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
|
||||||
else DEFAULT_ANTHROPIC_ENDPOINT).rstrip('/')
|
else DEFAULT_ANTHROPIC_ENDPOINT).rstrip('/')
|
||||||
api_key = entry.data[CONF_API_KEY]
|
api_key = entry.data[CONF_API_KEY]
|
||||||
|
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||||
|
|
||||||
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
|
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
|
||||||
headers = {
|
headers = {
|
||||||
@@ -256,23 +231,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
raise ConfigEntryNotReady("API connection failed")
|
raise ConfigEntryNotReady("API connection failed")
|
||||||
|
|
||||||
coordinator = HATextAICoordinator(
|
coordinator = HATextAICoordinator(
|
||||||
hass,
|
hass=hass,
|
||||||
api_key=api_key,
|
client=None, # Будет установлен позже
|
||||||
endpoint=endpoint,
|
|
||||||
model=model,
|
model=model,
|
||||||
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
update_interval=int(entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)),
|
||||||
|
instance_name=instance_name,
|
||||||
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
||||||
request_interval=float(entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)),
|
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
||||||
name=entry.title or entry.data.get(CONF_NAME, "HA Text AI"),
|
is_anthropic=is_anthropic,
|
||||||
session=session,
|
|
||||||
is_anthropic=is_anthropic
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await coordinator.async_initialize()
|
# Сохраняем данные интеграции
|
||||||
await coordinator.async_config_entry_first_refresh()
|
hass.data[DOMAIN][instance_name] = {
|
||||||
|
"coordinator": coordinator,
|
||||||
hass.data.setdefault(DOMAIN, {})
|
"config_entry": entry,
|
||||||
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)
|
||||||
return True
|
return True
|
||||||
@@ -284,15 +257,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
try:
|
try:
|
||||||
coordinator = hass.data[DOMAIN].get(entry.entry_id)
|
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||||
if coordinator:
|
if instance_name in hass.data[DOMAIN]:
|
||||||
|
coordinator = hass.data[DOMAIN][instance_name]["coordinator"]
|
||||||
await coordinator.async_shutdown()
|
await coordinator.async_shutdown()
|
||||||
|
hass.data[DOMAIN].pop(instance_name)
|
||||||
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
_LOGGER.exception("Error unloading entry: %s", str(ex))
|
_LOGGER.exception("Error unloading entry: %s", str(ex))
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ from typing import Any, Dict, Optional
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers import selector
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -42,145 +43,144 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize flow."""
|
"""Initialize flow."""
|
||||||
super().__init__()
|
self._errors = {}
|
||||||
self.provider = None
|
self._data = {}
|
||||||
|
self._provider = None
|
||||||
def _show_provider_selection(self) -> FlowResult:
|
|
||||||
"""Show the provider selection form."""
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
vol.Required("provider"): vol.In(API_PROVIDERS),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _show_provider_config(self, provider: str) -> FlowResult:
|
|
||||||
"""Show the configuration form for the selected provider."""
|
|
||||||
default_endpoint = (
|
|
||||||
DEFAULT_OPENAI_ENDPOINT if provider == API_PROVIDER_OPENAI
|
|
||||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
vol.Required("name", default=f"HA Text AI {len(self._async_current_entries()) + 1}"): str,
|
|
||||||
vol.Required(CONF_API_PROVIDER): vol.In([provider]),
|
|
||||||
vol.Required(CONF_API_KEY): str,
|
|
||||||
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
|
|
||||||
vol.Optional(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)
|
|
||||||
),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self._show_provider_selection()
|
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"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
if "provider" in user_input:
|
self._provider = user_input[CONF_API_PROVIDER]
|
||||||
self.provider = user_input["provider"]
|
return await self.async_step_provider()
|
||||||
return self._show_provider_config(self.provider)
|
|
||||||
|
|
||||||
return await self._process_configuration(user_input)
|
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
|
||||||
|
)
|
||||||
|
|
||||||
async def _process_configuration(self, user_input):
|
suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}"
|
||||||
"""Validate and process user configuration."""
|
|
||||||
errors = {}
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Проверка API подключения
|
||||||
|
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:
|
try:
|
||||||
# Создаем уникальный идентификатор, включающий имя экземпляра
|
|
||||||
instance_name = user_input.get("name", f"HA Text AI {len(self._async_current_entries()) + 1}")
|
|
||||||
unique_id = f"{DOMAIN}_{instance_name}_{user_input[CONF_API_PROVIDER]}"
|
|
||||||
|
|
||||||
# Нормализуем уникальный идентификатор
|
|
||||||
unique_id = unique_id.lower().replace(" ", "_")
|
|
||||||
|
|
||||||
await self.async_set_unique_id(unique_id)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
# Проверяем подключение к API
|
|
||||||
session = async_get_clientsession(self.hass)
|
session = async_get_clientsession(self.hass)
|
||||||
headers = self._get_api_headers(user_input)
|
headers = self._get_api_headers(user_input)
|
||||||
|
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
||||||
|
|
||||||
try:
|
check_url = (
|
||||||
async with session.get(
|
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
|
||||||
f"{user_input[CONF_API_ENDPOINT]}/models",
|
else f"{endpoint}/models"
|
||||||
headers=headers
|
)
|
||||||
) as response:
|
|
||||||
if response.status not in [200, 404]:
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except Exception as e:
|
|
||||||
_LOGGER.error(f"Connection test failed: {e}")
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
|
|
||||||
if not errors:
|
async with session.get(check_url, headers=headers) as response:
|
||||||
return self.async_create_entry(
|
if response.status == 401:
|
||||||
title=instance_name,
|
self._errors["base"] = "invalid_auth"
|
||||||
data={
|
return False
|
||||||
"name": instance_name,
|
elif response.status not in [200, 404]:
|
||||||
**user_input,
|
self._errors["base"] = "cannot_connect"
|
||||||
"unique_id": unique_id # Сохраняем уникальный идентификатор в данных
|
return False
|
||||||
}
|
return True
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
_LOGGER.error(f"Unexpected error: {e}")
|
_LOGGER.error("API validation error: %s", str(err))
|
||||||
errors["base"] = "unknown"
|
self._errors["base"] = "cannot_connect"
|
||||||
|
return False
|
||||||
|
|
||||||
return self._show_configuration_form(user_input, errors)
|
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
|
||||||
def _get_api_headers(self, user_input):
|
|
||||||
"""Get API headers based on provider."""
|
"""Get API headers based on provider."""
|
||||||
if user_input[CONF_API_PROVIDER] == API_PROVIDER_ANTHROPIC:
|
api_key = user_input[CONF_API_KEY]
|
||||||
|
|
||||||
|
if self._provider == API_PROVIDER_ANTHROPIC:
|
||||||
return {
|
return {
|
||||||
"x-api-key": user_input[CONF_API_KEY],
|
"x-api-key": api_key,
|
||||||
"anthropic-version": "2023-06-01"
|
"anthropic-version": "2023-06-01",
|
||||||
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"Authorization": f"Bearer {user_input[CONF_API_KEY]}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
|
|
||||||
def _show_configuration_form(self, user_input, errors=None):
|
async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
|
||||||
"""Show the configuration form to edit location data."""
|
"""Create the config entry."""
|
||||||
return self.async_show_form(
|
instance_name = user_input[CONF_NAME]
|
||||||
step_id="user",
|
unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_")
|
||||||
data_schema=vol.Schema({
|
|
||||||
vol.Required("name", default=user_input.get("name", f"HA Text AI {len(self._async_current_entries()) + 1}")): str,
|
return self.async_create_entry(
|
||||||
vol.Required(CONF_API_PROVIDER): vol.In([user_input[CONF_API_PROVIDER]]),
|
title=instance_name,
|
||||||
vol.Required(CONF_API_KEY): str,
|
data={
|
||||||
vol.Required(CONF_MODEL, default=user_input.get(CONF_MODEL, DEFAULT_MODEL)): str,
|
CONF_API_PROVIDER: self._provider,
|
||||||
vol.Optional(CONF_API_ENDPOINT, default=user_input.get(CONF_API_ENDPOINT,
|
CONF_NAME: instance_name,
|
||||||
DEFAULT_OPENAI_ENDPOINT if user_input[CONF_API_PROVIDER] == API_PROVIDER_OPENAI
|
**user_input,
|
||||||
else DEFAULT_ANTHROPIC_ENDPOINT)): str,
|
"unique_id": unique_id
|
||||||
vol.Optional(CONF_TEMPERATURE, default=user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
}
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
|
||||||
),
|
|
||||||
vol.Optional(CONF_MAX_TOKENS, default=user_input.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=user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
errors=errors or {}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@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):
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Handle options flow."""
|
"""Handle options flow."""
|
||||||
@@ -194,11 +194,15 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
current_data = self.config_entry.data
|
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MODEL,
|
||||||
|
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||||
|
): str,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
"""Constants for the HA text AI integration."""
|
"""Constants for the HA text AI integration."""
|
||||||
from typing import Final
|
from typing import Final
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant.const import Platform, CONF_API_KEY # Добавлен CONF_API_KEY
|
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
# Domain and platforms
|
# Domain and platforms
|
||||||
DOMAIN: Final = "ha_text_ai"
|
DOMAIN: Final = "ha_text_ai"
|
||||||
PLATFORMS: Final = [Platform.SENSOR]
|
PLATFORMS: Final = [Platform.SENSOR]
|
||||||
CONF_NAME = "name"
|
|
||||||
DEFAULT_NAME = "HA Text AI"
|
|
||||||
|
|
||||||
# Provider configuration
|
# Provider configuration
|
||||||
CONF_API_PROVIDER: Final = "api_provider"
|
CONF_API_PROVIDER: Final = "api_provider"
|
||||||
@@ -22,7 +20,7 @@ API_PROVIDERS: Final = [
|
|||||||
|
|
||||||
# Default endpoints
|
# Default endpoints
|
||||||
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||||
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com/v1"
|
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
||||||
|
|
||||||
# Configuration constants
|
# Configuration constants
|
||||||
CONF_MODEL: Final = "model"
|
CONF_MODEL: Final = "model"
|
||||||
@@ -30,12 +28,12 @@ CONF_TEMPERATURE: Final = "temperature"
|
|||||||
CONF_MAX_TOKENS: Final = "max_tokens"
|
CONF_MAX_TOKENS: Final = "max_tokens"
|
||||||
CONF_API_ENDPOINT: Final = "api_endpoint"
|
CONF_API_ENDPOINT: Final = "api_endpoint"
|
||||||
CONF_REQUEST_INTERVAL: Final = "request_interval"
|
CONF_REQUEST_INTERVAL: Final = "request_interval"
|
||||||
|
CONF_INSTANCE: Final = "instance"
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
DEFAULT_MODEL: Final = "gpt-3.5-turbo" # Обновлено на актуальную модель
|
||||||
DEFAULT_TEMPERATURE: Final = 0.1
|
DEFAULT_TEMPERATURE: Final = 0.7
|
||||||
DEFAULT_MAX_TOKENS: Final = 1000
|
DEFAULT_MAX_TOKENS: Final = 1000
|
||||||
DEFAULT_API_ENDPOINT: Final = DEFAULT_OPENAI_ENDPOINT
|
|
||||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||||
DEFAULT_TIMEOUT: Final = 30
|
DEFAULT_TIMEOUT: Final = 30
|
||||||
|
|
||||||
@@ -48,53 +46,23 @@ MIN_REQUEST_INTERVAL: Final = 0.1
|
|||||||
MAX_REQUEST_INTERVAL: Final = 60.0
|
MAX_REQUEST_INTERVAL: Final = 60.0
|
||||||
|
|
||||||
# API constants
|
# API constants
|
||||||
API_CHAT_PATH: Final = "chat/completions"
|
|
||||||
API_TIMEOUT: Final = 30
|
API_TIMEOUT: Final = 30
|
||||||
API_RETRY_COUNT: Final = 3
|
API_RETRY_COUNT: Final = 3
|
||||||
|
|
||||||
# History constants
|
|
||||||
HISTORY_FILTER_MODEL: Final = "filter_model"
|
|
||||||
HISTORY_FILTER_DATE: Final = "start_date"
|
|
||||||
HISTORY_SORT_ORDER: Final = "sort_order"
|
|
||||||
HISTORY_INCLUDE_METADATA: Final = "include_metadata"
|
|
||||||
HISTORY_SORT_ASC: Final = "asc"
|
|
||||||
HISTORY_SORT_DESC: Final = "desc"
|
|
||||||
HISTORY_MAX_ENTRIES: Final = 100
|
|
||||||
|
|
||||||
# Service names
|
# Service names
|
||||||
SERVICE_ASK_QUESTION: Final = "ask_question"
|
SERVICE_ASK_QUESTION: Final = "ask_question"
|
||||||
SERVICE_CLEAR_HISTORY: Final = "clear_history"
|
SERVICE_CLEAR_HISTORY: Final = "clear_history"
|
||||||
SERVICE_GET_HISTORY: Final = "get_history"
|
SERVICE_GET_HISTORY: Final = "get_history"
|
||||||
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
|
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
|
||||||
|
|
||||||
# Service descriptions
|
|
||||||
SERVICE_ASK_QUESTION_DESCRIPTION: Final = "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history."
|
|
||||||
SERVICE_CLEAR_HISTORY_DESCRIPTION: Final = "Delete all stored questions and responses from the conversation history."
|
|
||||||
SERVICE_GET_HISTORY_DESCRIPTION: Final = "Retrieve recent conversation history with optional filtering and sorting."
|
|
||||||
SERVICE_SET_SYSTEM_PROMPT_DESCRIPTION: Final = "Set default system behavior instructions for all future conversations."
|
|
||||||
|
|
||||||
# Attribute keys
|
# Attribute keys
|
||||||
ATTR_QUESTION: Final = "question"
|
ATTR_QUESTION: Final = "question"
|
||||||
ATTR_RESPONSE: Final = "response"
|
ATTR_RESPONSE: Final = "response"
|
||||||
ATTR_LAST_UPDATED: Final = "last_updated"
|
ATTR_INSTANCE: Final = "instance"
|
||||||
ATTR_MODEL: Final = "model"
|
ATTR_MODEL: Final = "model"
|
||||||
ATTR_TEMPERATURE: Final = "temperature"
|
ATTR_TEMPERATURE: Final = "temperature"
|
||||||
ATTR_MAX_TOKENS: Final = "max_tokens"
|
ATTR_MAX_TOKENS: Final = "max_tokens"
|
||||||
ATTR_TOTAL_RESPONSES: Final = "total_responses"
|
|
||||||
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
|
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
|
||||||
ATTR_RESPONSE_TIME: Final = "response_time"
|
|
||||||
ATTR_QUEUE_SIZE: Final = "queue_size"
|
|
||||||
ATTR_API_STATUS: Final = "api_status"
|
|
||||||
ATTR_ERROR_COUNT: Final = "error_count"
|
|
||||||
ATTR_LAST_ERROR: Final = "last_error"
|
|
||||||
ATTR_API_VERSION: Final = "api_version"
|
|
||||||
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
|
|
||||||
ATTR_REQUEST_COUNT: Final = "request_count"
|
|
||||||
ATTR_TOKENS_USED: Final = "tokens_used"
|
|
||||||
ATTR_RETRY_COUNT: Final = "retry_count"
|
|
||||||
ATTR_QUEUE_POSITION: Final = "queue_position"
|
|
||||||
ATTR_ESTIMATED_WAIT: Final = "estimated_wait"
|
|
||||||
ATTR_SORT_ORDER: Final = "sort_order"
|
|
||||||
|
|
||||||
# Error messages
|
# Error messages
|
||||||
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
|
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
|
||||||
@@ -105,80 +73,12 @@ ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
|
|||||||
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
|
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
|
||||||
ERROR_API_ERROR: Final = "api_error"
|
ERROR_API_ERROR: Final = "api_error"
|
||||||
ERROR_TIMEOUT: Final = "timeout_error"
|
ERROR_TIMEOUT: Final = "timeout_error"
|
||||||
ERROR_QUEUE_FULL: Final = "queue_full"
|
ERROR_INVALID_INSTANCE: Final = "invalid_instance"
|
||||||
ERROR_INVALID_PROMPT: Final = "invalid_prompt"
|
ERROR_NAME_EXISTS: Final = "name_exists"
|
||||||
ERROR_INVALID_PARAMETERS: Final = "invalid_parameters"
|
|
||||||
ERROR_SERVICE_UNAVAILABLE: Final = "service_unavailable"
|
|
||||||
ERROR_INVALID_SORT_ORDER: Final = "invalid_sort_order"
|
|
||||||
|
|
||||||
# Configuration descriptions
|
|
||||||
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses"
|
|
||||||
CONF_TEMPERATURE_DESCRIPTION: Final = "Temperature for response generation (0-2)"
|
|
||||||
CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)"
|
|
||||||
CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL"
|
|
||||||
CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)"
|
|
||||||
|
|
||||||
# Entity attributes descriptions
|
|
||||||
ATTR_QUESTION_DESCRIPTION: Final = "Last question asked"
|
|
||||||
ATTR_RESPONSE_DESCRIPTION: Final = "Last response received"
|
|
||||||
ATTR_LAST_UPDATED_DESCRIPTION: Final = "Time of last update"
|
|
||||||
ATTR_MODEL_DESCRIPTION: Final = "Current AI model in use"
|
|
||||||
ATTR_TEMPERATURE_DESCRIPTION: Final = "Current temperature setting"
|
|
||||||
ATTR_MAX_TOKENS_DESCRIPTION: Final = "Current max tokens setting"
|
|
||||||
ATTR_TOTAL_RESPONSES_DESCRIPTION: Final = "Total number of responses"
|
|
||||||
ATTR_SYSTEM_PROMPT_DESCRIPTION: Final = "Current system prompt"
|
|
||||||
ATTR_RESPONSE_TIME_DESCRIPTION: Final = "Time taken for last response"
|
|
||||||
ATTR_QUEUE_SIZE_DESCRIPTION: Final = "Current size of question queue"
|
|
||||||
ATTR_API_STATUS_DESCRIPTION: Final = "Current API connection status"
|
|
||||||
ATTR_ERROR_COUNT_DESCRIPTION: Final = "Total number of errors"
|
|
||||||
ATTR_LAST_ERROR_DESCRIPTION: Final = "Last error message"
|
|
||||||
ATTR_API_VERSION_DESCRIPTION: Final = "Current API version"
|
|
||||||
ATTR_ENDPOINT_STATUS_DESCRIPTION: Final = "Current endpoint status"
|
|
||||||
ATTR_REQUEST_COUNT_DESCRIPTION: Final = "Total number of API requests"
|
|
||||||
ATTR_TOKENS_USED_DESCRIPTION: Final = "Total tokens used"
|
|
||||||
ATTR_SORT_ORDER_DESCRIPTION: Final = "Sort order for history (asc/desc)"
|
|
||||||
|
|
||||||
# Entity attributes
|
|
||||||
ENTITY_NAME: Final = "HA Text AI"
|
|
||||||
ENTITY_ICON: Final = "mdi:robot"
|
|
||||||
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
|
|
||||||
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
|
|
||||||
ENTITY_ICON_OFFLINE: Final = "mdi:robot-off"
|
|
||||||
ENTITY_ICON_QUEUE: Final = "mdi:robot-confused"
|
|
||||||
|
|
||||||
# Translation keys
|
|
||||||
TRANSLATION_KEY_CONFIG: Final = "config"
|
|
||||||
TRANSLATION_KEY_OPTIONS: Final = "options"
|
|
||||||
TRANSLATION_KEY_ERROR: Final = "error"
|
|
||||||
TRANSLATION_KEY_STATE: Final = "state"
|
|
||||||
TRANSLATION_KEY_SERVICES: Final = "services"
|
|
||||||
|
|
||||||
# State attributes
|
|
||||||
STATE_READY: Final = "ready"
|
|
||||||
STATE_PROCESSING: Final = "processing"
|
|
||||||
STATE_ERROR: Final = "error"
|
|
||||||
STATE_DISCONNECTED: Final = "disconnected"
|
|
||||||
STATE_RATE_LIMITED: Final = "rate_limited"
|
|
||||||
STATE_INITIALIZING: Final = "initializing"
|
|
||||||
STATE_MAINTENANCE: Final = "maintenance"
|
|
||||||
STATE_RETRYING: Final = "retrying"
|
|
||||||
STATE_QUEUED: Final = "queued"
|
|
||||||
STATE_UPDATING: Final = "updating"
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOGGER_NAME: Final = "custom_components.ha_text_ai"
|
|
||||||
LOG_LEVEL_DEFAULT: Final = "INFO"
|
|
||||||
|
|
||||||
# Queue constants
|
|
||||||
QUEUE_TIMEOUT: Final = 5
|
|
||||||
QUEUE_MAX_SIZE: Final = 100
|
|
||||||
|
|
||||||
# Retry constants
|
|
||||||
MAX_RETRIES: Final = 3
|
|
||||||
RETRY_DELAY: Final = 1.0
|
|
||||||
|
|
||||||
# Service schema constants
|
# Service schema constants
|
||||||
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
vol.Required("question"): cv.string,
|
vol.Required("question"): cv.string,
|
||||||
vol.Optional("system_prompt"): cv.string,
|
vol.Optional("system_prompt"): cv.string,
|
||||||
vol.Optional("model"): cv.string,
|
vol.Optional("model"): cv.string,
|
||||||
@@ -193,26 +93,25 @@ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
vol.Required("prompt"): cv.string
|
vol.Required("prompt"): cv.string
|
||||||
})
|
})
|
||||||
|
|
||||||
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
vol.Optional("limit", default=10): vol.All(
|
vol.Optional("limit", default=10): vol.All(
|
||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
vol.Range(min=1, max=HISTORY_MAX_ENTRIES)
|
vol.Range(min=1, max=100)
|
||||||
),
|
),
|
||||||
vol.Optional("start_date"): cv.datetime,
|
vol.Optional("filter_model"): cv.string
|
||||||
vol.Optional("include_metadata"): cv.boolean,
|
|
||||||
vol.Optional("sort_order", default=HISTORY_SORT_DESC): vol.In([
|
|
||||||
HISTORY_SORT_ASC,
|
|
||||||
HISTORY_SORT_DESC
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Configuration schema
|
# Configuration schema
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Required(CONF_API_KEY): 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_MODEL, default=DEFAULT_MODEL): cv.string,
|
||||||
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||||
vol.Coerce(float),
|
vol.Coerce(float),
|
||||||
@@ -226,11 +125,20 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
||||||
vol.Coerce(float),
|
vol.Coerce(float),
|
||||||
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
||||||
),
|
)
|
||||||
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS)
|
|
||||||
})
|
})
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
# Event names
|
# Event names
|
||||||
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
|
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
|
||||||
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
|
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
|
||||||
|
|||||||
@@ -1,561 +1,117 @@
|
|||||||
"""Data coordinator for HA text AI."""
|
"""Data coordinator for HA text AI."""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import uuid
|
from datetime import datetime
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import httpx
|
|
||||||
from anthropic import AsyncAnthropic
|
|
||||||
from openai import APIError, AsyncOpenAI, AuthenticationError, RateLimitError
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import aiohttp_client
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import (
|
|
||||||
DOMAIN,
|
|
||||||
DEFAULT_TIMEOUT,
|
|
||||||
MAX_RETRIES,
|
|
||||||
QUEUE_MAX_SIZE,
|
|
||||||
RETRY_DELAY,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
class HATextAICoordinator(DataUpdateCoordinator):
|
class HATextAICoordinator(DataUpdateCoordinator):
|
||||||
"""Class to manage fetching data from the API."""
|
"""Class to manage fetching data from API."""
|
||||||
|
|
||||||
def _validate_params(self, api_key: str, temperature: float, max_tokens: int) -> None:
|
|
||||||
"""Validate initialization parameters.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api_key: API key for authentication
|
|
||||||
temperature: Sampling temperature for text generation
|
|
||||||
max_tokens: Maximum number of tokens to generate
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If any parameters are invalid
|
|
||||||
"""
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError("API key cannot be empty")
|
|
||||||
|
|
||||||
try:
|
|
||||||
temp = float(temperature)
|
|
||||||
if not 0 <= temp <= 2:
|
|
||||||
raise ValueError("Temperature must be between 0 and 2")
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
raise ValueError("Temperature must be a number between 0 and 2")
|
|
||||||
|
|
||||||
try:
|
|
||||||
tokens = int(max_tokens)
|
|
||||||
if tokens < 1:
|
|
||||||
raise ValueError("Max tokens must be a positive number")
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
raise ValueError("Max tokens must be a positive integer")
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
api_key: str,
|
client: Any,
|
||||||
endpoint: str,
|
|
||||||
model: str,
|
model: str,
|
||||||
temperature: float,
|
update_interval: int,
|
||||||
max_tokens: int,
|
instance_name: str, # Добавляем идентификатор интеграции
|
||||||
request_interval: float,
|
max_tokens: int = 1000,
|
||||||
name: str,
|
temperature: float = 0.7,
|
||||||
session: Optional[Any] = None,
|
max_history_size: int = 50,
|
||||||
is_anthropic: bool = False,
|
is_anthropic: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
self._name = name
|
|
||||||
self._validate_params(api_key, temperature, max_tokens)
|
|
||||||
self._entity_id = None
|
|
||||||
self.api_key = api_key
|
|
||||||
self.endpoint = endpoint
|
|
||||||
self.model = model
|
|
||||||
self.temperature = float(temperature)
|
|
||||||
self.max_tokens = int(max_tokens)
|
|
||||||
self._question_queue = asyncio.PriorityQueue(maxsize=QUEUE_MAX_SIZE)
|
|
||||||
self._responses: Dict[str, Any] = {}
|
|
||||||
self.system_prompt: Optional[str] = None
|
|
||||||
self._is_ready = False
|
|
||||||
self._is_processing = False
|
|
||||||
self._is_rate_limited = False
|
|
||||||
self._is_maintenance = False
|
|
||||||
self._error_count = 0
|
|
||||||
self._MAX_ERRORS = 3
|
|
||||||
self._request_count = 0
|
|
||||||
self._tokens_used = 0
|
|
||||||
self._api_version = "v1" # Keep single definition
|
|
||||||
self._endpoint_status = "disconnected"
|
|
||||||
self._last_error = None
|
|
||||||
self._last_request_time = 0
|
|
||||||
self._is_anthropic = is_anthropic
|
|
||||||
self._session = session or aiohttp_client.async_get_clientsession(hass)
|
|
||||||
self.client = None
|
|
||||||
self.hass = hass # Store hass reference for _init_client
|
|
||||||
self._start_time = time.time()
|
|
||||||
self.last_response = None
|
|
||||||
|
|
||||||
# History and metrics
|
|
||||||
self._history: List[Dict[str, Any]] = []
|
|
||||||
self._max_history_size = 100
|
|
||||||
self._performance_metrics: Dict[str, Any] = {
|
|
||||||
"avg_response_time": 0,
|
|
||||||
"total_errors": 0,
|
|
||||||
"success_rate": 100,
|
|
||||||
"requests_per_minute": 0,
|
|
||||||
"avg_tokens_per_request": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=self._name,
|
name=f"HA Text AI {instance_name}", # Уникальное имя для каждой интеграции
|
||||||
update_interval=timedelta(seconds=float(request_interval)),
|
update_interval=update_interval,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _init_client(self):
|
self.instance_name = instance_name # Сохраняем идентификатор
|
||||||
"""Initialize API client with proper SSL context."""
|
self.client = client
|
||||||
try:
|
self.model = model
|
||||||
if self._is_anthropic:
|
self.max_tokens = max_tokens
|
||||||
self.client = AsyncAnthropic(
|
self.temperature = temperature
|
||||||
api_key=self.api_key
|
self._max_history_size = max_history_size
|
||||||
)
|
self._is_anthropic = is_anthropic
|
||||||
else: # OpenAI
|
self._system_prompt = None
|
||||||
# Create transport in separate thread
|
|
||||||
transport = await self.hass.async_add_executor_job(
|
|
||||||
lambda: httpx.AsyncHTTPTransport(retries=3)
|
|
||||||
)
|
|
||||||
|
|
||||||
limits = httpx.Limits(max_keepalive_connections=5, max_connections=10)
|
# Статусы
|
||||||
|
self._is_processing = False
|
||||||
|
self._is_rate_limited = False
|
||||||
|
self._is_maintenance = False
|
||||||
|
|
||||||
async_client = httpx.AsyncClient(
|
# История и метрики
|
||||||
base_url=self.endpoint,
|
self._history: List[Dict[str, Any]] = []
|
||||||
timeout=httpx.Timeout(30.0),
|
self._request_count = 0
|
||||||
transport=transport,
|
self._error_count = 0
|
||||||
limits=limits,
|
self._last_error = None
|
||||||
follow_redirects=True
|
self._performance_metrics = {
|
||||||
)
|
"total_requests": 0,
|
||||||
|
"total_errors": 0,
|
||||||
self.client = AsyncOpenAI(
|
"avg_response_time": 0.0,
|
||||||
api_key=self.api_key,
|
"last_request_time": None,
|
||||||
base_url=self.endpoint,
|
|
||||||
http_client=async_client,
|
|
||||||
max_retries=3
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self._last_error = str(e)
|
|
||||||
_LOGGER.error("Error initializing API client: %s", str(e))
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def async_initialize(self) -> None:
|
|
||||||
"""Initialize coordinator."""
|
|
||||||
try:
|
|
||||||
await self._init_client()
|
|
||||||
self._is_ready = True
|
|
||||||
self._endpoint_status = "connected"
|
|
||||||
except Exception as e:
|
|
||||||
self._last_error = str(e)
|
|
||||||
self._endpoint_status = "error"
|
|
||||||
_LOGGER.error("Failed to initialize coordinator: %s", str(e))
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def async_ask(
|
|
||||||
self,
|
|
||||||
question: str,
|
|
||||||
priority: bool = False,
|
|
||||||
system_prompt: Optional[str] = None,
|
|
||||||
model: Optional[str] = None,
|
|
||||||
temperature: Optional[float] = None,
|
|
||||||
max_tokens: Optional[int] = None,
|
|
||||||
) -> str:
|
|
||||||
"""Add question to queue with priority."""
|
|
||||||
if not self._is_ready:
|
|
||||||
raise HomeAssistantError("Coordinator is not ready")
|
|
||||||
|
|
||||||
if not question or not isinstance(question, str):
|
|
||||||
raise ValueError("Invalid question format")
|
|
||||||
|
|
||||||
# Validate optional parameters
|
|
||||||
if temperature is not None and not 0 <= temperature <= 2:
|
|
||||||
raise ValueError("Temperature must be between 0 and 2")
|
|
||||||
if max_tokens is not None and not isinstance(max_tokens, int):
|
|
||||||
raise ValueError("max_tokens must be an integer")
|
|
||||||
|
|
||||||
request_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
request_params = {
|
|
||||||
"id": request_id,
|
|
||||||
"question": question,
|
|
||||||
"system_prompt": system_prompt or self.system_prompt,
|
|
||||||
"model": model or self.model,
|
|
||||||
"temperature": temperature or self.temperature,
|
|
||||||
"max_tokens": max_tokens or self.max_tokens,
|
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
priority_level = 0 if priority else 1
|
# Версия API и последний ответ
|
||||||
await self._question_queue.put((priority_level, request_id, request_params))
|
self.api_version = "v1"
|
||||||
|
self.last_response = None
|
||||||
# Wait for response
|
self.endpoint_status = "initialized"
|
||||||
while request_id not in self._responses:
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
response = self._responses.pop(request_id)
|
|
||||||
|
|
||||||
# Add to history
|
|
||||||
history_entry = {
|
|
||||||
"timestamp": request_params["timestamp"],
|
|
||||||
"question": question,
|
|
||||||
"response": response,
|
|
||||||
"model": request_params["model"],
|
|
||||||
"metadata": {
|
|
||||||
"temperature": request_params["temperature"],
|
|
||||||
"max_tokens": request_params["max_tokens"],
|
|
||||||
"system_prompt": request_params["system_prompt"],
|
|
||||||
"priority": priority,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self._add_to_history(history_entry)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
async def _process_queue(self) -> None:
|
|
||||||
"""Process questions from queue."""
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
if self._is_rate_limited:
|
|
||||||
await asyncio.sleep(60)
|
|
||||||
self._is_rate_limited = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
priority, request_id, params = await self._question_queue.get()
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = await self._make_api_request(params)
|
|
||||||
self._responses[request_id] = response
|
|
||||||
self._update_metrics(response)
|
|
||||||
except Exception as e:
|
|
||||||
self._last_error = str(e)
|
|
||||||
_LOGGER.error("Error processing request: %s", str(e))
|
|
||||||
self._responses[request_id] = f"Error: {str(e)}"
|
|
||||||
finally:
|
|
||||||
self._question_queue.task_done()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._last_error = str(e)
|
|
||||||
_LOGGER.error("Queue processing error: %s", str(e))
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
async def _make_api_request(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""Make API request with retry logic."""
|
|
||||||
attempts = 0
|
|
||||||
while attempts < MAX_RETRIES:
|
|
||||||
try:
|
|
||||||
if self._is_anthropic:
|
|
||||||
response = await self._make_anthropic_call(**params)
|
|
||||||
else:
|
|
||||||
response = await self._make_openai_call(**params)
|
|
||||||
return response
|
|
||||||
except (AuthenticationError, ValueError) as e:
|
|
||||||
# Don't retry auth errors
|
|
||||||
raise
|
|
||||||
except RateLimitError:
|
|
||||||
self._is_rate_limited = True
|
|
||||||
await asyncio.sleep(RETRY_DELAY * (2 ** attempts))
|
|
||||||
attempts += 1
|
|
||||||
except Exception as e:
|
|
||||||
attempts += 1
|
|
||||||
if attempts >= MAX_RETRIES:
|
|
||||||
raise
|
|
||||||
await asyncio.sleep(RETRY_DELAY)
|
|
||||||
|
|
||||||
async def _make_anthropic_call(self, **params) -> Dict[str, Any]:
|
|
||||||
"""Make API call to Anthropic."""
|
|
||||||
start_time = time.time()
|
|
||||||
messages = []
|
|
||||||
if params.get("system_prompt"):
|
|
||||||
messages.append({"role": "system", "content": params["system_prompt"]})
|
|
||||||
messages.append({"role": "user", "content": params["question"]})
|
|
||||||
|
|
||||||
try:
|
|
||||||
completion = await self.client.messages.create(
|
|
||||||
model=params.get("model", self.model),
|
|
||||||
messages=messages,
|
|
||||||
temperature=params.get("temperature", self.temperature),
|
|
||||||
max_tokens=params.get("max_tokens", self.max_tokens),
|
|
||||||
)
|
|
||||||
|
|
||||||
response_time = time.time() - start_time
|
|
||||||
|
|
||||||
return {
|
|
||||||
"response": completion.content[0].text,
|
|
||||||
"model": completion.model,
|
|
||||||
"tokens": completion.usage.total_tokens if hasattr(completion, 'usage') else 0,
|
|
||||||
"response_time": response_time
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
self._last_error = str(e)
|
|
||||||
_LOGGER.error("Anthropic API call error: %s", str(e))
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def _make_openai_call(self, **params) -> Dict[str, Any]:
|
|
||||||
"""Make API call to OpenAI."""
|
|
||||||
start_time = time.time()
|
|
||||||
try:
|
|
||||||
if not params.get("question"):
|
|
||||||
raise ValueError("Question cannot be empty")
|
|
||||||
|
|
||||||
messages = []
|
|
||||||
if params.get("system_prompt"):
|
|
||||||
messages.append({"role": "system", "content": params["system_prompt"]})
|
|
||||||
messages.append({"role": "user", "content": params["question"]})
|
|
||||||
|
|
||||||
api_params = {
|
|
||||||
"model": params.get("model", self.model),
|
|
||||||
"messages": messages,
|
|
||||||
"temperature": params.get("temperature", self.temperature),
|
|
||||||
"max_tokens": params.get("max_tokens", self.max_tokens),
|
|
||||||
}
|
|
||||||
|
|
||||||
completion = await self.client.chat.completions.create(**api_params)
|
|
||||||
response_time = time.time() - start_time
|
|
||||||
|
|
||||||
if not completion.choices:
|
|
||||||
raise ValueError("No response choices available")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"response": completion.choices[0].message.content.strip(),
|
|
||||||
"model": completion.model,
|
|
||||||
"tokens": completion.usage.total_tokens,
|
|
||||||
"response_time": response_time
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._last_error = str(e)
|
|
||||||
error_msg = f"OpenAI API call failed: {str(e)}"
|
|
||||||
_LOGGER.error(error_msg, exc_info=True)
|
|
||||||
raise RuntimeError(error_msg) from e
|
|
||||||
|
|
||||||
def _add_to_history(self, entry: Dict[str, Any]) -> None:
|
|
||||||
"""Add entry to history with size limit."""
|
|
||||||
self._history.append(entry)
|
|
||||||
if len(self._history) > self._max_history_size:
|
|
||||||
self._history.pop(0)
|
|
||||||
|
|
||||||
async def get_history(
|
|
||||||
self,
|
|
||||||
limit: Optional[int] = None,
|
|
||||||
start_date: Optional[datetime] = None,
|
|
||||||
filter_model: Optional[str] = None, # Добавляем параметр
|
|
||||||
include_metadata: bool = False
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""Get conversation history."""
|
|
||||||
history = self._history
|
|
||||||
|
|
||||||
if start_date:
|
|
||||||
history = [
|
|
||||||
h for h in history
|
|
||||||
if datetime.fromisoformat(h["timestamp"]) >= start_date
|
|
||||||
]
|
|
||||||
|
|
||||||
# Добавляем фильтрацию по модели
|
|
||||||
if filter_model:
|
|
||||||
history = [h for h in history if h["model"] == filter_model]
|
|
||||||
|
|
||||||
if not include_metadata:
|
|
||||||
history = [{
|
|
||||||
"timestamp": h["timestamp"],
|
|
||||||
"question": h["question"],
|
|
||||||
"response": h["response"],
|
|
||||||
"model": h["model"]
|
|
||||||
} for h in history]
|
|
||||||
|
|
||||||
if limit and limit > 0:
|
|
||||||
history = history[-limit:]
|
|
||||||
|
|
||||||
return history
|
|
||||||
|
|
||||||
def _update_metrics(self, response_data: Dict[str, Any]) -> None:
|
|
||||||
"""Update performance metrics."""
|
|
||||||
# Update response time metrics
|
|
||||||
response_time = response_data.get("response_time", 0)
|
|
||||||
current_avg = self._performance_metrics["avg_response_time"]
|
|
||||||
self._performance_metrics["avg_response_time"] = (
|
|
||||||
(current_avg * self._request_count + response_time) /
|
|
||||||
(self._request_count + 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update success rate
|
|
||||||
total_requests = self._request_count + 1
|
|
||||||
self._performance_metrics["success_rate"] = (
|
|
||||||
(total_requests - self._performance_metrics["total_errors"]) /
|
|
||||||
total_requests * 100
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update tokens metrics
|
|
||||||
tokens = response_data.get("tokens", 0)
|
|
||||||
self._tokens_used += tokens
|
|
||||||
self._performance_metrics["avg_tokens_per_request"] = (
|
|
||||||
self._tokens_used / total_requests
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate requests per minute
|
|
||||||
if self._last_request_time:
|
|
||||||
time_diff = time.time() - self._last_request_time
|
|
||||||
if time_diff > 0:
|
|
||||||
self._performance_metrics["requests_per_minute"] = 60 / time_diff
|
|
||||||
|
|
||||||
self._last_request_time = time.time()
|
|
||||||
self._request_count += 1
|
|
||||||
|
|
||||||
async def async_refresh_metrics(self) -> None:
|
|
||||||
"""Refresh performance metrics."""
|
|
||||||
try:
|
|
||||||
self._performance_metrics.update({
|
|
||||||
"queue_size": self._question_queue.qsize(),
|
|
||||||
"last_update": dt_util.utcnow().isoformat(),
|
|
||||||
"uptime": time.time() - self._start_time if hasattr(self, '_start_time') else 0,
|
|
||||||
"memory_usage": {
|
|
||||||
"history_size": len(self._history),
|
|
||||||
"response_cache_size": len(self._responses)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
_LOGGER.error("Error refreshing metrics: %s", str(e))
|
|
||||||
|
|
||||||
async def export_metrics(self) -> Dict[str, Any]:
|
|
||||||
"""Export detailed metrics and statistics."""
|
|
||||||
await self.async_refresh_metrics()
|
|
||||||
return {
|
|
||||||
"entity_id": self._entity_id,
|
|
||||||
"performance": self._performance_metrics,
|
|
||||||
"requests": {
|
|
||||||
"total": self._request_count,
|
|
||||||
"successful": self._request_count - self._performance_metrics["total_errors"],
|
|
||||||
"failed": self._performance_metrics["total_errors"]
|
|
||||||
},
|
|
||||||
"tokens": {
|
|
||||||
"total_used": self._tokens_used,
|
|
||||||
"average_per_request": self._performance_metrics["avg_tokens_per_request"]
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"is_ready": self.is_ready,
|
|
||||||
"endpoint_status": self._endpoint_status,
|
|
||||||
"error_count": self._error_count,
|
|
||||||
"last_error": self._last_error
|
|
||||||
},
|
|
||||||
"queue": {
|
|
||||||
"size": self.queue_size,
|
|
||||||
"is_full": self.is_queue_full
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@property
|
|
||||||
def is_ready(self) -> bool:
|
|
||||||
"""Return if coordinator is ready."""
|
|
||||||
return self._is_ready
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_processing(self) -> bool:
|
def is_processing(self) -> bool:
|
||||||
"""Return if coordinator is processing."""
|
"""Return True if currently processing a request."""
|
||||||
return self._is_processing
|
return self._is_processing
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_rate_limited(self) -> bool:
|
def is_rate_limited(self) -> bool:
|
||||||
"""Return if coordinator is rate limited."""
|
"""Return True if currently rate limited."""
|
||||||
return self._is_rate_limited
|
return self._is_rate_limited
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_maintenance(self) -> bool:
|
def is_maintenance(self) -> bool:
|
||||||
"""Return if coordinator is in maintenance mode."""
|
"""Return True if in maintenance mode."""
|
||||||
return self._is_maintenance
|
return self._is_maintenance
|
||||||
|
|
||||||
@property
|
async def async_ask_question(
|
||||||
def queue_size(self) -> int:
|
self,
|
||||||
"""Return current queue size."""
|
question: str,
|
||||||
return self._question_queue.qsize()
|
model: Optional[str] = None,
|
||||||
|
temperature: Optional[float] = None,
|
||||||
|
max_tokens: Optional[int] = None,
|
||||||
|
system_prompt: Optional[str] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Process a question with optional parameters."""
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
|
||||||
@property
|
messages = []
|
||||||
def is_queue_full(self) -> bool:
|
if temp_system_prompt:
|
||||||
"""Return if queue is full."""
|
messages.append({"role": "system", "content": temp_system_prompt})
|
||||||
return self.queue_size >= QUEUE_MAX_SIZE
|
messages.append({"role": "user", "content": question})
|
||||||
|
|
||||||
@property
|
kwargs = {
|
||||||
def last_error(self) -> Optional[str]:
|
"model": temp_model,
|
||||||
"""Return last error message."""
|
"temperature": temp_temperature,
|
||||||
return self._last_error
|
"max_tokens": temp_max_tokens,
|
||||||
|
"messages": messages,
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
return await self.async_process_message(question, **kwargs)
|
||||||
def endpoint_status(self) -> str:
|
|
||||||
"""Return endpoint connection status."""
|
|
||||||
return self._endpoint_status
|
|
||||||
|
|
||||||
@property
|
except Exception as err:
|
||||||
def api_version(self) -> str:
|
_LOGGER.error("Error processing question: %s", err)
|
||||||
"""Return the API version."""
|
raise HomeAssistantError(f"Failed to process question: {err}")
|
||||||
return self._api_version
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
"""Async enter."""
|
|
||||||
await self.async_start()
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
"""Async exit."""
|
|
||||||
await self.async_stop()
|
|
||||||
|
|
||||||
async def async_start(self) -> None:
|
|
||||||
"""Start coordinator operations."""
|
|
||||||
if not self._is_ready:
|
|
||||||
await self.async_initialize()
|
|
||||||
|
|
||||||
self._start_time = time.time()
|
|
||||||
self._is_processing = True
|
|
||||||
asyncio.create_task(self._process_queue())
|
|
||||||
|
|
||||||
async def async_stop(self) -> None:
|
|
||||||
"""Stop coordinator operations."""
|
|
||||||
self._is_processing = False
|
|
||||||
self._is_ready = False
|
|
||||||
# Clear queues and caches
|
|
||||||
while not self._question_queue.empty():
|
|
||||||
try:
|
|
||||||
await self._question_queue.get_nowait()
|
|
||||||
except asyncio.QueueEmpty:
|
|
||||||
break
|
|
||||||
self._responses.clear()
|
|
||||||
|
|
||||||
async def async_reset(self) -> None:
|
|
||||||
"""Reset coordinator state."""
|
|
||||||
await self.async_stop()
|
|
||||||
self._error_count = 0
|
|
||||||
self._request_count = 0
|
|
||||||
self._tokens_used = 0
|
|
||||||
self._last_error = None
|
|
||||||
self._history.clear()
|
|
||||||
self._performance_metrics = {
|
|
||||||
"avg_response_time": 0,
|
|
||||||
"total_errors": 0,
|
|
||||||
"success_rate": 100,
|
|
||||||
"requests_per_minute": 0,
|
|
||||||
"avg_tokens_per_request": 0,
|
|
||||||
}
|
|
||||||
await self.async_start()
|
|
||||||
|
|
||||||
async def async_clear_history(self) -> None:
|
|
||||||
"""Clear conversation history."""
|
|
||||||
self._history.clear()
|
|
||||||
|
|
||||||
def set_system_prompt(self, prompt: str) -> None:
|
|
||||||
"""Set system prompt."""
|
|
||||||
if not isinstance(prompt, str):
|
|
||||||
raise ValueError("System prompt must be a string")
|
|
||||||
self.system_prompt = prompt
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> Dict[str, Any]:
|
async def _async_update_data(self) -> Dict[str, Any]:
|
||||||
"""Update data from API."""
|
"""Update data from API."""
|
||||||
@@ -565,16 +121,27 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"status": self.endpoint_status,
|
"status": self.endpoint_status,
|
||||||
"metrics": self._performance_metrics,
|
"metrics": self._performance_metrics,
|
||||||
"last_update": dt_util.utcnow().isoformat(),
|
"last_update": dt_util.utcnow().isoformat(),
|
||||||
"last_response": self.last_response, # Добавляем last_response
|
"last_response": self.last_response,
|
||||||
"is_processing": self._is_processing,
|
"is_processing": self._is_processing,
|
||||||
"is_rate_limited": self._is_rate_limited,
|
"is_rate_limited": self._is_rate_limited,
|
||||||
"is_maintenance": self._is_maintenance,
|
"is_maintenance": self._is_maintenance,
|
||||||
|
"instance": self.instance_name, # Добавляем идентификатор интеграции
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._last_error = str(e)
|
self._last_error = str(e)
|
||||||
|
self._error_count += 1
|
||||||
_LOGGER.error("Error updating data: %s", str(e))
|
_LOGGER.error("Error updating data: %s", str(e))
|
||||||
raise HomeAssistantError(f"Error updating data: {str(e)}")
|
raise HomeAssistantError(f"Error updating data: {str(e)}")
|
||||||
|
|
||||||
|
async def async_refresh_metrics(self) -> None:
|
||||||
|
"""Refresh performance metrics."""
|
||||||
|
self._performance_metrics.update({
|
||||||
|
"total_requests": self._request_count,
|
||||||
|
"total_errors": self._error_count,
|
||||||
|
"last_request_time": dt_util.utcnow().isoformat(),
|
||||||
|
"instance": self.instance_name, # Добавляем идентификатор интеграции
|
||||||
|
})
|
||||||
|
|
||||||
async def async_process_message(self, message: str, **kwargs) -> dict:
|
async def async_process_message(self, message: str, **kwargs) -> dict:
|
||||||
"""Process message and update last_response."""
|
"""Process message and update last_response."""
|
||||||
try:
|
try:
|
||||||
@@ -583,36 +150,47 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
|
messages = kwargs.pop("messages", [{"role": "user", "content": message}])
|
||||||
|
model = kwargs.pop("model", self.model)
|
||||||
|
temperature = kwargs.pop("temperature", self.temperature)
|
||||||
|
max_tokens = kwargs.pop("max_tokens", self.max_tokens)
|
||||||
|
|
||||||
if self._is_anthropic:
|
if self._is_anthropic:
|
||||||
response = await self.client.messages.create(
|
response = await self.client.messages.create(
|
||||||
model=self.model,
|
model=model,
|
||||||
max_tokens=self.max_tokens,
|
max_tokens=max_tokens,
|
||||||
temperature=self.temperature,
|
temperature=temperature,
|
||||||
messages=[{"role": "user", "content": message}],
|
messages=messages,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
response_text = response.content[0].text
|
response_text = response.content[0].text
|
||||||
else:
|
else:
|
||||||
response = await self.client.chat.completions.create(
|
response = await self.client.chat.completions.create(
|
||||||
model=self.model,
|
model=model,
|
||||||
temperature=self.temperature,
|
temperature=temperature,
|
||||||
max_tokens=self.max_tokens,
|
max_tokens=max_tokens,
|
||||||
messages=[{"role": "user", "content": message}],
|
messages=messages,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
response_text = response.choices[0].message.content
|
response_text = response.choices[0].message.content
|
||||||
|
|
||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
self._request_count += 1
|
self._request_count += 1
|
||||||
self._performance_metrics["avg_response_time"] = (
|
|
||||||
(self._performance_metrics["avg_response_time"] * (self._request_count - 1) + elapsed_time)
|
if self._performance_metrics["avg_response_time"] == 0:
|
||||||
/ self._request_count
|
self._performance_metrics["avg_response_time"] = elapsed_time
|
||||||
)
|
else:
|
||||||
|
self._performance_metrics["avg_response_time"] = (
|
||||||
|
(self._performance_metrics["avg_response_time"] * (self._request_count - 1) + elapsed_time)
|
||||||
|
/ self._request_count
|
||||||
|
)
|
||||||
|
|
||||||
self.last_response = {
|
self.last_response = {
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
"question": message,
|
"question": message,
|
||||||
"response": response_text,
|
"response": response_text,
|
||||||
|
"model": model,
|
||||||
|
"instance": self.instance_name, # Добавляем идентификатор интеграции
|
||||||
"error": None
|
"error": None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,6 +198,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
if len(self._history) > self._max_history_size:
|
if len(self._history) > self._max_history_size:
|
||||||
self._history.pop(0)
|
self._history.pop(0)
|
||||||
|
|
||||||
|
self.endpoint_status = "ready"
|
||||||
self._is_processing = False
|
self._is_processing = False
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
|
|
||||||
@@ -634,18 +213,56 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
"question": message,
|
"question": message,
|
||||||
"response": None,
|
"response": None,
|
||||||
|
"instance": self.instance_name, # Добавляем идентификатор интеграции
|
||||||
"error": str(err)
|
"error": str(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.endpoint_status = "error"
|
||||||
self._is_processing = False
|
self._is_processing = False
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def async_set_system_prompt(self, prompt: str) -> None:
|
||||||
"""Async enter."""
|
"""Set the system prompt."""
|
||||||
await self.async_start()
|
self._system_prompt = prompt
|
||||||
return self
|
_LOGGER.debug("[%s] System prompt updated: %s", self.instance_name, prompt)
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
async def async_clear_history(self) -> None:
|
||||||
"""Async exit."""
|
"""Clear conversation history."""
|
||||||
await self.async_stop()
|
self._history.clear()
|
||||||
|
_LOGGER.debug("[%s] Conversation history cleared", self.instance_name)
|
||||||
|
self.async_update_listeners()
|
||||||
|
|
||||||
|
async def async_get_history(
|
||||||
|
self,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
filter_model: Optional[str] = None,
|
||||||
|
instance: Optional[str] = None # Добавляем фильтр по интеграции
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Get conversation history with optional filtering."""
|
||||||
|
history = self._history.copy()
|
||||||
|
|
||||||
|
if instance and instance != self.instance_name:
|
||||||
|
return [] # Возвращаем пустой список для чужих интеграций
|
||||||
|
|
||||||
|
if filter_model:
|
||||||
|
history = [h for h in history if h.get("model") == filter_model]
|
||||||
|
|
||||||
|
if limit and limit > 0:
|
||||||
|
history = history[-limit:]
|
||||||
|
|
||||||
|
return history
|
||||||
|
|
||||||
|
async def async_reset_metrics(self) -> None:
|
||||||
|
"""Reset all metrics."""
|
||||||
|
self._request_count = 0
|
||||||
|
self._error_count = 0
|
||||||
|
self._performance_metrics = {
|
||||||
|
"total_requests": 0,
|
||||||
|
"total_errors": 0,
|
||||||
|
"avg_response_time": 0.0,
|
||||||
|
"last_request_time": None,
|
||||||
|
"instance": self.instance_name, # Добавляем идентификатор интеграции
|
||||||
|
}
|
||||||
|
_LOGGER.debug("[%s] Metrics reset", self.instance_name)
|
||||||
|
self.async_update_listeners()
|
||||||
|
|||||||
@@ -57,19 +57,10 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
|
|
||||||
# Используем имя из конфигурации
|
|
||||||
self._name = config_entry.title
|
self._name = config_entry.title
|
||||||
|
|
||||||
# Создаем уникальный ID используя имя
|
|
||||||
self._attr_unique_id = f"{config_entry.entry_id}_{slugify(self._name)}"
|
self._attr_unique_id = f"{config_entry.entry_id}_{slugify(self._name)}"
|
||||||
|
|
||||||
# Создаем entity_id используя имя
|
|
||||||
self.entity_id = f"sensor.ha_text_ai_{slugify(self._name)}"
|
self.entity_id = f"sensor.ha_text_ai_{slugify(self._name)}"
|
||||||
|
|
||||||
# Устанавливаем отображаемое имя
|
|
||||||
self._attr_name = self._name
|
self._attr_name = self._name
|
||||||
|
|
||||||
self._current_state = STATE_INITIALIZING
|
self._current_state = STATE_INITIALIZING
|
||||||
self._error_count = 0
|
self._error_count = 0
|
||||||
self._last_error = None
|
self._last_error = None
|
||||||
@@ -80,7 +71,7 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
name=self._name,
|
name=self._name,
|
||||||
manufacturer="Community",
|
manufacturer="Community",
|
||||||
model=f"{coordinator.model} ({self._config_entry.data.get(CONF_API_PROVIDER, 'Unknown')} provider)",
|
model=f"{coordinator.model} ({self._config_entry.data.get(CONF_API_PROVIDER, 'Unknown')} provider)",
|
||||||
sw_version=coordinator._api_version,
|
sw_version=getattr(coordinator, 'api_version', 'v1'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -95,8 +86,14 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
@property
|
@property
|
||||||
def state(self) -> StateType:
|
def state(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
if self.coordinator.data:
|
try:
|
||||||
return self.coordinator.data.get("last_update", self._current_state)
|
if self.coordinator.data and "last_update" in self.coordinator.data:
|
||||||
|
timestamp = self.coordinator.data["last_update"]
|
||||||
|
if isinstance(timestamp, str):
|
||||||
|
return dt_util.parse_datetime(timestamp)
|
||||||
|
return timestamp
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.debug("Error parsing state: %s", err)
|
||||||
return self._current_state
|
return self._current_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -112,17 +109,22 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
|
|
||||||
if self.coordinator.data:
|
if self.coordinator.data:
|
||||||
data = self.coordinator.data
|
data = self.coordinator.data
|
||||||
if "metrics" in data:
|
|
||||||
|
# Обновляем метрики если они есть
|
||||||
|
if "metrics" in data and isinstance(data["metrics"], dict):
|
||||||
attributes.update({"metrics": data["metrics"]})
|
attributes.update({"metrics": data["metrics"]})
|
||||||
|
|
||||||
|
# Обновляем статус API
|
||||||
if "status" in data:
|
if "status" in data:
|
||||||
attributes[ATTR_API_STATUS] = data["status"]
|
attributes[ATTR_API_STATUS] = data["status"]
|
||||||
if "last_response" in data:
|
|
||||||
last_response = data["last_response"]
|
# Обновляем информацию о последнем ответе
|
||||||
if last_response:
|
last_response = data.get("last_response", {})
|
||||||
attributes.update({
|
if isinstance(last_response, dict):
|
||||||
ATTR_RESPONSE: last_response.get("response", ""),
|
attributes.update({
|
||||||
ATTR_QUESTION: last_response.get("question", ""),
|
ATTR_RESPONSE: last_response.get("response", ""),
|
||||||
})
|
ATTR_QUESTION: last_response.get("question", ""),
|
||||||
|
})
|
||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
@@ -130,20 +132,23 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
try:
|
try:
|
||||||
data = self.coordinator.data
|
data = self.coordinator.data
|
||||||
if data:
|
if not data:
|
||||||
if data.get("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
|
|
||||||
else:
|
|
||||||
self._current_state = STATE_READY
|
|
||||||
|
|
||||||
if "metrics" in data:
|
|
||||||
self._error_count = data["metrics"].get("total_errors", 0)
|
|
||||||
else:
|
|
||||||
self._current_state = STATE_DISCONNECTED
|
self._current_state = STATE_DISCONNECTED
|
||||||
|
return
|
||||||
|
|
||||||
|
# Определяем текущее состояние
|
||||||
|
if data.get("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
|
||||||
|
else:
|
||||||
|
self._current_state = STATE_READY
|
||||||
|
|
||||||
|
# Обновляем счетчик ошибок из метрик
|
||||||
|
if "metrics" in data and isinstance(data["metrics"], dict):
|
||||||
|
self._error_count = data["metrics"].get("total_errors", self._error_count)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error handling update: %s", err, exc_info=True)
|
_LOGGER.error("Error handling update: %s", err, exc_info=True)
|
||||||
|
|||||||
@@ -3,23 +3,36 @@ ask_question:
|
|||||||
description: >-
|
description: >-
|
||||||
Send a question to the AI model and receive a detailed response.
|
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.
|
The response will be stored in the conversation history and can be retrieved later.
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: sensor
|
|
||||||
integration: ha_text_ai
|
|
||||||
fields:
|
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:
|
question:
|
||||||
name: Question
|
name: Question
|
||||||
description: Your question or prompt for the AI assistant.
|
description: Your question or prompt for the AI assistant
|
||||||
required: true
|
required: true
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
multiline: true
|
multiline: true
|
||||||
type: text
|
type: text
|
||||||
|
|
||||||
|
system_prompt:
|
||||||
|
name: System Prompt
|
||||||
|
description: Optional system prompt to set context for this specific question
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiline: true
|
||||||
|
|
||||||
model:
|
model:
|
||||||
name: Model
|
name: Model
|
||||||
description: "Select AI model to use (optional, overrides default setting)."
|
description: "Select AI model to use (optional, overrides default setting)"
|
||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
@@ -27,7 +40,7 @@ ask_question:
|
|||||||
|
|
||||||
temperature:
|
temperature:
|
||||||
name: Temperature
|
name: Temperature
|
||||||
description: Controls response creativity (0.0-2.0).
|
description: Controls response creativity (0.0-2.0)
|
||||||
required: false
|
required: false
|
||||||
default: 0.7
|
default: 0.7
|
||||||
selector:
|
selector:
|
||||||
@@ -39,7 +52,7 @@ ask_question:
|
|||||||
|
|
||||||
max_tokens:
|
max_tokens:
|
||||||
name: Max Tokens
|
name: Max Tokens
|
||||||
description: Maximum length of the response (1-4096 tokens).
|
description: Maximum length of the response (1-4096 tokens)
|
||||||
required: false
|
required: false
|
||||||
default: 1000
|
default: 1000
|
||||||
selector:
|
selector:
|
||||||
@@ -49,34 +62,35 @@ ask_question:
|
|||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
system_prompt:
|
|
||||||
name: System Prompt
|
|
||||||
description: Optional system prompt to set context for this specific question.
|
|
||||||
required: false
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
multiline: true
|
|
||||||
|
|
||||||
clear_history:
|
clear_history:
|
||||||
name: Clear History
|
name: Clear History
|
||||||
description: Delete all stored questions and responses from the conversation history.
|
description: Delete all stored questions and responses from the conversation history
|
||||||
target:
|
fields:
|
||||||
entity:
|
instance:
|
||||||
domain: sensor
|
name: Instance
|
||||||
integration: ha_text_ai
|
description: Name of the HA Text AI instance to clear history for
|
||||||
fields: {}
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
get_history:
|
get_history:
|
||||||
name: Get History
|
name: Get History
|
||||||
description: Retrieve recent conversation history.
|
description: Retrieve conversation history with optional filtering and sorting
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: sensor
|
|
||||||
integration: ha_text_ai
|
|
||||||
fields:
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to get history from
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
limit:
|
limit:
|
||||||
name: Limit
|
name: Limit
|
||||||
description: Number of most recent conversations to return (1-100).
|
description: Number of conversations to return (1-100)
|
||||||
required: false
|
required: false
|
||||||
default: 10
|
default: 10
|
||||||
selector:
|
selector:
|
||||||
@@ -86,24 +100,58 @@ get_history:
|
|||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
|
filter_model:
|
||||||
|
name: Filter Model
|
||||||
|
description: Filter conversations by specific AI model
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiline: false
|
||||||
|
|
||||||
start_date:
|
start_date:
|
||||||
name: Start Date
|
name: Start Date
|
||||||
description: Optional start date for filtering history.
|
description: Filter conversations starting from this date/time
|
||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
datetime:
|
datetime:
|
||||||
|
|
||||||
|
include_metadata:
|
||||||
|
name: Include Metadata
|
||||||
|
description: Include additional information like tokens used, response time, etc.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
|
||||||
|
sort_order:
|
||||||
|
name: Sort Order
|
||||||
|
description: Sort order for results (newest or oldest first)
|
||||||
|
required: false
|
||||||
|
default: "desc"
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: "Newest First"
|
||||||
|
value: "desc"
|
||||||
|
- label: "Oldest First"
|
||||||
|
value: "asc"
|
||||||
|
|
||||||
set_system_prompt:
|
set_system_prompt:
|
||||||
name: Set System Prompt
|
name: Set System Prompt
|
||||||
description: Set default system behavior instructions for all future conversations.
|
description: Set default system behavior instructions for all future conversations
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: sensor
|
|
||||||
integration: ha_text_ai
|
|
||||||
fields:
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to set system prompt for
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
prompt:
|
prompt:
|
||||||
name: System Prompt
|
name: System Prompt
|
||||||
description: Instructions that define how the AI should behave and respond.
|
description: Instructions that define how the AI should behave and respond
|
||||||
required: true
|
required: true
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
|||||||
@@ -1,49 +1,51 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"provider": {
|
||||||
"title": "Set up HA Text AI",
|
"title": "Select AI Provider",
|
||||||
"description": "Configure your AI integration with OpenAI or Anthropic providers.",
|
"description": "Choose which AI service provider to use for this instance",
|
||||||
"data": {
|
"data": {
|
||||||
"api_provider": "Select API Provider",
|
"api_provider": "API Provider"
|
||||||
"api_key": "API key for authentication (required)",
|
}
|
||||||
"model": "AI model to use (provider-specific)",
|
},
|
||||||
"temperature": "Response creativity (0-2, lower = more focused and consistent)",
|
"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)",
|
"max_tokens": "Maximum response length (1-4096 tokens)",
|
||||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||||
"request_interval": "Minimum time between requests in seconds (0.1-60)",
|
"request_interval": "Minimum time between requests (0.1-60 seconds)"
|
||||||
"name": "Name for this integration instance (e.g., 'GPT Assistant', 'Claude Helper')"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"invalid_name": "Invalid integration name",
|
"name_exists": "An instance with this name already exists",
|
||||||
|
"invalid_name": "Invalid instance name",
|
||||||
"invalid_auth": "Authentication failed - check your API key",
|
"invalid_auth": "Authentication failed - check your API key",
|
||||||
"invalid_api_key": "Invalid API key - please verify your credentials",
|
"invalid_api_key": "Invalid API key - please verify your credentials",
|
||||||
"cannot_connect": "Connection failed - check endpoint and network status",
|
"cannot_connect": "Failed to connect to API service",
|
||||||
"invalid_model": "Model unavailable or not supported by the selected provider",
|
"invalid_model": "Selected model is not available",
|
||||||
"rate_limit": "Rate limit exceeded - please reduce request frequency",
|
"rate_limit": "Rate limit exceeded",
|
||||||
"api_error": "API service error - check provider status",
|
"api_error": "API service error occurred",
|
||||||
"timeout": "Request timeout - server not responding",
|
"timeout": "Request timed out",
|
||||||
"queue_full": "Request queue full - try again later",
|
"invalid_instance": "Invalid instance specified",
|
||||||
"invalid_url_format": "Invalid API endpoint URL format",
|
"unknown": "Unexpected error occurred"
|
||||||
"invalid_input": "Invalid configuration parameters",
|
|
||||||
"invalid_parameters": "Invalid service parameters provided",
|
|
||||||
"service_unavailable": "Service temporarily unavailable",
|
|
||||||
"context_length": "Maximum context length exceeded",
|
|
||||||
"unknown": "Unexpected error - check logs for details"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "HA Text AI Settings",
|
"title": "Update Instance Settings",
|
||||||
"description": "Adjust your AI integration parameters",
|
"description": "Modify settings for this AI assistant instance",
|
||||||
"data": {
|
"data": {
|
||||||
"model": "Select AI model (provider-specific)",
|
"model": "AI model",
|
||||||
"temperature": "Response creativity (0-2)",
|
"temperature": "Response creativity (0-2)",
|
||||||
"max_tokens": "Maximum response length in tokens (1-4096)",
|
"max_tokens": "Maximum response length (1-4096)",
|
||||||
"request_interval": "Minimum seconds between requests (0.1-60)"
|
"request_interval": "Minimum request interval (0.1-60 seconds)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,63 +53,85 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Ask Question",
|
"name": "Ask Question",
|
||||||
"description": "Send a question to the AI model and get a detailed response.",
|
"description": "Send a question to the specified AI instance",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "The HA Text AI instance to use"
|
||||||
|
},
|
||||||
"question": {
|
"question": {
|
||||||
"name": "Question",
|
"name": "Question",
|
||||||
"description": "Your question or prompt for the AI model"
|
"description": "Your question or prompt"
|
||||||
},
|
},
|
||||||
"system_prompt": {
|
"system_prompt": {
|
||||||
"name": "System Prompt",
|
"name": "System Prompt",
|
||||||
"description": "Optional context or instructions for this specific question"
|
"description": "Optional context for this question"
|
||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"name": "Model",
|
"name": "Model",
|
||||||
"description": "Optional specific AI model for this request"
|
"description": "Override default model for this request"
|
||||||
},
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"name": "Temperature",
|
"name": "Temperature",
|
||||||
"description": "Optional creativity setting (0-2)"
|
"description": "Override default creativity setting"
|
||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Max Tokens",
|
"name": "Max Tokens",
|
||||||
"description": "Optional maximum response length (1-4096 tokens)"
|
"description": "Override default response length limit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clear_history": {
|
"clear_history": {
|
||||||
"name": "Clear History",
|
"name": "Clear History",
|
||||||
"description": "Delete all stored questions and responses from the conversation history."
|
"description": "Delete conversation history for specified instance",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "The HA Text AI instance to clear history for"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"get_history": {
|
"get_history": {
|
||||||
"name": "Get History",
|
"name": "Get History",
|
||||||
"description": "Retrieve recent conversation history with optional filtering.",
|
"description": "Retrieve conversation history for specified instance",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "The HA Text AI instance to get history from"
|
||||||
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "Limit",
|
"name": "Limit",
|
||||||
"description": "Maximum number of entries to return (1-100)"
|
"description": "Maximum number of entries (1-100)"
|
||||||
|
},
|
||||||
|
"filter_model": {
|
||||||
|
"name": "Filter Model",
|
||||||
|
"description": "Filter by specific AI model"
|
||||||
},
|
},
|
||||||
"start_date": {
|
"start_date": {
|
||||||
"name": "Start Date",
|
"name": "Start Date",
|
||||||
"description": "Optional date to filter history from"
|
"description": "Filter from specific date/time"
|
||||||
},
|
},
|
||||||
"include_metadata": {
|
"include_metadata": {
|
||||||
"name": "Include Metadata",
|
"name": "Include Metadata",
|
||||||
"description": "Include additional response metadata"
|
"description": "Include additional response information"
|
||||||
},
|
},
|
||||||
"sort_order": {
|
"sort_order": {
|
||||||
"name": "Sort Order",
|
"name": "Sort Order",
|
||||||
"description": "Order of results (asc/desc)"
|
"description": "Sort order (newest/oldest first)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"set_system_prompt": {
|
"set_system_prompt": {
|
||||||
"name": "Set System Prompt",
|
"name": "Set System Prompt",
|
||||||
"description": "Set default system behavior instructions for all future conversations.",
|
"description": "Set default behavior for specified instance",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "The HA Text AI instance to configure"
|
||||||
|
},
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"name": "System Prompt",
|
"name": "System Prompt",
|
||||||
"description": "Instructions that define how the AI should behave"
|
"description": "Default behavior instructions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,16 +141,13 @@
|
|||||||
"ha_text_ai": {
|
"ha_text_ai": {
|
||||||
"name": "{name}",
|
"name": "{name}",
|
||||||
"state": {
|
"state": {
|
||||||
"initializing": "Initializing",
|
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
"processing": "Processing",
|
"processing": "Processing",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"disconnected": "Disconnected",
|
"disconnected": "Disconnected",
|
||||||
"rate_limited": "Rate Limited",
|
"rate_limited": "Rate Limited",
|
||||||
"maintenance": "Maintenance",
|
|
||||||
"retrying": "Retrying",
|
"retrying": "Retrying",
|
||||||
"queued": "Queued",
|
"queued": "Queued"
|
||||||
"updating": "Updating"
|
|
||||||
},
|
},
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"question": {
|
"question": {
|
||||||
@@ -135,11 +156,8 @@
|
|||||||
"response": {
|
"response": {
|
||||||
"name": "Last Response"
|
"name": "Last Response"
|
||||||
},
|
},
|
||||||
"last_updated": {
|
|
||||||
"name": "Last Updated"
|
|
||||||
},
|
|
||||||
"model": {
|
"model": {
|
||||||
"name": "Model"
|
"name": "Current Model"
|
||||||
},
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"name": "Temperature"
|
"name": "Temperature"
|
||||||
@@ -147,20 +165,14 @@
|
|||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Max Tokens"
|
"name": "Max Tokens"
|
||||||
},
|
},
|
||||||
"total_responses": {
|
|
||||||
"name": "Total Responses"
|
|
||||||
},
|
|
||||||
"system_prompt": {
|
"system_prompt": {
|
||||||
"name": "System Prompt"
|
"name": "System Prompt"
|
||||||
},
|
},
|
||||||
"response_time": {
|
"response_time": {
|
||||||
"name": "Response Time"
|
"name": "Last Response Time"
|
||||||
},
|
},
|
||||||
"queue_size": {
|
"total_responses": {
|
||||||
"name": "Queue Size"
|
"name": "Total Responses"
|
||||||
},
|
|
||||||
"api_status": {
|
|
||||||
"name": "API Status"
|
|
||||||
},
|
},
|
||||||
"error_count": {
|
"error_count": {
|
||||||
"name": "Error Count"
|
"name": "Error Count"
|
||||||
@@ -168,11 +180,11 @@
|
|||||||
"last_error": {
|
"last_error": {
|
||||||
"name": "Last Error"
|
"name": "Last Error"
|
||||||
},
|
},
|
||||||
"tokens_used": {
|
"api_status": {
|
||||||
"name": "Tokens Used"
|
"name": "API Status"
|
||||||
},
|
},
|
||||||
"request_count": {
|
"tokens_used": {
|
||||||
"name": "Request Count"
|
"name": "Total Tokens Used"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user