feat: add multi-provider support,

config improvements
This commit is contained in:
SMKRV
2024-11-19 18:53:51 +03:00
parent d6144be7ed
commit df3d79c20c
8 changed files with 345 additions and 59 deletions
+133 -13
View File
@@ -1,15 +1,15 @@
"""The HA Text AI integration."""
import logging
from typing import Any
from typing import Any, Dict, Optional
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers import config_validation as cv
from .coordinator import HATextAICoordinator
from .coordinator import HATextAICoordinator
from .const import (
DOMAIN,
@@ -55,19 +55,134 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
except Exception as refresh_ex:
_LOGGER.error("Failed to refresh coordinator: %s", str(refresh_ex))
return False
raise ConfigEntryNotReady from refresh_ex
if not coordinator.last_update_success:
_LOGGER.error("Failed to communicate with OpenAI API")
return False
raise ConfigEntryNotReady("Failed to communicate with OpenAI API")
hass.data[DOMAIN][entry.entry_id] = coordinator
try:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as setup_ex:
_LOGGER.error("Failed to setup platforms: %s", str(setup_ex))
return False
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def async_ask_question(call: ServiceCall) -> None:
"""Handle the ask_question service call."""
question = call.data.get("question", "")
if not question:
_LOGGER.error("No question provided in service call")
return
# Собираем все опциональные параметры
request_params = {}
# Обработка system_prompt
system_prompt = call.data.get("system_prompt")
if system_prompt is not None:
request_params["system_prompt"] = system_prompt
# Обработка model
model = call.data.get("model")
if model is not None:
request_params["model"] = model
# Обработка temperature
temperature = call.data.get("temperature")
if temperature is not None:
try:
request_params["temperature"] = float(temperature)
except ValueError:
_LOGGER.error("Invalid temperature value: %s", temperature)
return
# Обработка max_tokens
max_tokens = call.data.get("max_tokens")
if max_tokens is not None:
try:
request_params["max_tokens"] = int(max_tokens)
except ValueError:
_LOGGER.error("Invalid max_tokens value: %s", max_tokens)
return
try:
await coordinator.async_ask_question(question, **request_params)
except Exception as err:
_LOGGER.error("Error asking question: %s", str(err))
async def async_clear_history(call: ServiceCall) -> None:
"""Handle the clear_history service call."""
try:
coordinator._responses.clear()
await coordinator.async_refresh()
_LOGGER.info("History cleared successfully")
except Exception as err:
_LOGGER.error("Error clearing history: %s", str(err))
async def async_get_history(call: ServiceCall) -> dict:
"""Handle the get_history service call."""
try:
limit = call.data.get("limit", 10)
filter_model = call.data.get("filter_model", "")
responses = coordinator._responses
# Применяем фильтрацию по модели
if filter_model:
filtered_responses = {
k: v for k, v in responses.items()
if v.get("model") == filter_model
}
else:
filtered_responses = responses.copy()
# Сортируем по времени и ограничиваем количество
sorted_responses = dict(
sorted(
filtered_responses.items(),
key=lambda x: x[1]["timestamp"],
reverse=True
)[:limit]
)
return sorted_responses
except Exception as err:
_LOGGER.error("Error getting history: %s", str(err))
return {}
async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle the set_system_prompt service call."""
prompt = call.data.get("prompt", "")
if prompt:
try:
coordinator.system_prompt = prompt
_LOGGER.info("System prompt updated successfully")
except Exception as err:
_LOGGER.error("Error setting system prompt: %s", str(err))
else:
_LOGGER.error("No prompt provided in service call")
# Регистрация сервисов
hass.services.async_register(
DOMAIN,
"ask_question",
async_ask_question
)
hass.services.async_register(
DOMAIN,
"clear_history",
async_clear_history
)
hass.services.async_register(
DOMAIN,
"get_history",
async_get_history
)
hass.services.async_register(
DOMAIN,
"set_system_prompt",
async_set_system_prompt
)
_LOGGER.info(
"Successfully set up HA Text AI with model: %s",
@@ -78,7 +193,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except Exception as ex:
_LOGGER.exception("Unexpected error setting up entry: %s", str(ex))
return False
raise ConfigEntryNotReady from ex
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
@@ -86,6 +201,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if entry.entry_id not in hass.data.get(DOMAIN, {}):
return True
# Удаляем все сервисы при выгрузке интеграции
services = ["ask_question", "clear_history", "get_history", "set_system_prompt"]
for service in services:
hass.services.async_remove(DOMAIN, service)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator = hass.data[DOMAIN].pop(entry.entry_id)
@@ -103,4 +223,4 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)
except Exception as ex:
_LOGGER.exception("Error reloading entry: %s", str(ex))
_LOGGER.exception("Error reloading entry: %s", str(ex)) # убрано лишнее двоеточие
+54 -13
View File
@@ -71,14 +71,27 @@ class HATextAICoordinator(DataUpdateCoordinator):
try:
async with async_timeout.timeout(30):
question = await self._question_queue.get()
question_data = await self._question_queue.get()
question = question_data["question"]
params = question_data["params"]
try:
response_content = await self._make_api_call(question)
response_content = await self._make_api_call(
question,
model=params.get("model"),
temperature=params.get("temperature"),
max_tokens=params.get("max_tokens"),
system_prompt=params.get("system_prompt")
)
self._responses[question] = {
"question": question,
"response": response_content,
"error": None,
"timestamp": self.hass.loop.time()
"timestamp": self.hass.loop.time(),
"model": params.get("model", self.model),
"temperature": params.get("temperature", self.temperature),
"max_tokens": params.get("max_tokens", self.max_tokens)
}
self._error_count = 0
self._is_ready = True
@@ -113,7 +126,10 @@ class HATextAICoordinator(DataUpdateCoordinator):
"question": question,
"response": None,
"error": error_msg,
"timestamp": self.hass.loop.time()
"timestamp": self.hass.loop.time(),
"model": self.model,
"temperature": self.temperature,
"max_tokens": self.max_tokens
}
_LOGGER.error("API error (%s): %s", type(error).__name__, error_msg)
@@ -136,19 +152,27 @@ class HATextAICoordinator(DataUpdateCoordinator):
except Exception as err:
_LOGGER.error("Error clearing question queue: %s", err)
async def _make_api_call(self, question: str) -> str:
async def _make_api_call(
self,
question: str,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None
) -> str:
"""Make API call to OpenAI."""
try:
messages = []
if self.system_prompt:
messages.append({"role": "system", "content": self.system_prompt})
current_system_prompt = system_prompt if system_prompt is not None else self.system_prompt
if current_system_prompt:
messages.append({"role": "system", "content": current_system_prompt})
messages.append({"role": "user", "content": question})
completion = await self.client.chat.completions.create(
model=self.model,
model=model or self.model,
messages=messages,
temperature=self.temperature,
max_tokens=self.max_tokens,
temperature=temperature if temperature is not None else self.temperature,
max_tokens=max_tokens if max_tokens is not None else self.max_tokens,
)
return completion.choices[0].message.content
@@ -156,13 +180,30 @@ class HATextAICoordinator(DataUpdateCoordinator):
_LOGGER.error("Error in API call: %s", err)
raise
async def async_ask_question(self, question: str) -> None:
"""Add question to queue."""
async def async_ask_question(
self,
question: str,
system_prompt: Optional[str] = None,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None
) -> None:
"""Add question to queue with optional parameters."""
if not self._is_ready and self._error_count >= self._MAX_ERRORS:
_LOGGER.warning("Coordinator is not ready due to previous errors")
return
await self._question_queue.put(question)
question_data = {
"question": question,
"params": {
"system_prompt": system_prompt,
"model": model,
"temperature": temperature,
"max_tokens": max_tokens
}
}
await self._question_queue.put(question_data)
await self.async_refresh()
async def async_shutdown(self) -> None:
+1 -1
View File
@@ -9,6 +9,6 @@
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"requirements": ["openai>=1.0.0"],
"ssdp": [],
"version": "1.0.10",
"version": "1.1.0",
"zeroconf": []
}
+25 -5
View File
@@ -117,21 +117,35 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
return attributes
try:
history = list(self.coordinator.data.items())
# Получаем историю ответов
history = list(self.coordinator._responses.items())
if history:
last_question, last_data = history[-1]
# Handle different response formats
# Обработка формата ответа
if isinstance(last_data, dict):
last_response = last_data.get("response", "")
last_updated = last_data.get("timestamp", self.coordinator.last_update_success_time)
response_time = last_data.get("response_time")
# Добавляем новые метаданные
model = last_data.get("model", self.coordinator.model)
temperature = last_data.get("temperature", self.coordinator.temperature)
max_tokens = last_data.get("max_tokens", self.coordinator.max_tokens)
error = last_data.get("error")
if error:
self._last_error = error
self._state = STATE_ERROR
else:
last_response = str(last_data)
last_updated = self.coordinator.last_update_success_time
response_time = None
model = self.coordinator.model
temperature = self.coordinator.temperature
max_tokens = self.coordinator.max_tokens
# Convert timestamp to local time if needed
# Конвертация времени в локальный формат
if isinstance(last_updated, datetime):
last_updated = dt_util.as_local(last_updated)
@@ -140,6 +154,9 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
ATTR_RESPONSE: last_response,
ATTR_LAST_UPDATED: last_updated,
ATTR_TOTAL_RESPONSES: len(history),
ATTR_MODEL: model,
ATTR_TEMPERATURE: temperature,
ATTR_MAX_TOKENS: max_tokens,
})
if response_time is not None:
@@ -169,7 +186,10 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
"""Handle updated data from the coordinator."""
try:
if self.coordinator.data:
self._state = STATE_READY
if self.coordinator._is_ready:
self._state = STATE_READY
else:
self._state = STATE_DISCONNECTED
else:
self._state = STATE_DISCONNECTED
except Exception as err:
@@ -178,4 +198,4 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
self._last_error = str(err)
self._state = STATE_ERROR
self.async_write_ha_state()
self.async_write_ha_state() # Исправлено лишнее двоеточие
+23 -2
View File
@@ -28,6 +28,7 @@ ask_question:
name: Model
description: >-
Select an AI model to use (optional, overrides default setting).
You can choose from predefined models or enter your own model identifier.
Different models have different capabilities and token limits.
Note: More capable models may have longer response times and higher API costs.
required: false
@@ -35,6 +36,7 @@ ask_question:
default: "gpt-3.5-turbo"
selector:
select:
custom_value: true
options:
- label: "GPT-3.5 Turbo (Fast & Efficient)"
value: "gpt-3.5-turbo"
@@ -46,6 +48,8 @@ ask_question:
value: "gpt-4-32k"
- label: "GPT-4 Turbo (Latest)"
value: "gpt-4-1106-preview"
- label: "Claude-3 Sonnet"
value: "claude-3-sonnet"
mode: dropdown
temperature:
@@ -84,6 +88,17 @@ ask_question:
step: 1
mode: box
system_prompt:
name: System Prompt
description: >-
Optional system prompt to set context for this specific question.
This will temporarily override the default system prompt.
required: false
example: "You are a home automation expert focused on energy efficiency"
selector:
text:
multiline: true
clear_history:
name: Clear History
description: >-
@@ -121,6 +136,7 @@ get_history:
required: false
selector:
select:
custom_value: true
options:
- label: "All Models"
value: ""
@@ -128,10 +144,16 @@ get_history:
value: "gpt-3.5-turbo"
- label: "GPT-4"
value: "gpt-4"
- label: "Claude-3 Sonnet"
value: "claude-3-sonnet"
mode: dropdown
set_system_prompt:
fields:
name: Set System Prompt
description: >-
Set default system behavior instructions for all future conversations.
This defines how the AI should behave and respond to questions.
fields:
prompt:
name: System Prompt
description: >-
@@ -151,4 +173,3 @@ set_system_prompt:
selector:
text:
multiline: true
type: text
@@ -3,16 +3,24 @@
"step": {
"user": {
"title": "Set up HA Text AI",
"description": "Configure your OpenAI integration",
"description": "Configure your AI integration (OpenAI, Anthropic, etc.)",
"data": {
"api_key": "Your OpenAI API key",
"model": "AI model to use for responses",
"api_key": "Your API key",
"model": "AI model to use for responses (e.g., gpt-3.5-turbo, claude-3-sonnet)",
"temperature": "Temperature for response generation (0-2)",
"max_tokens": "Maximum tokens in response (1-4096)",
"api_endpoint": "API endpoint URL",
"request_interval": "Minimum time between API requests (seconds)"
"api_endpoint": "API endpoint URL (optional, for custom endpoints)",
"request_interval": "Minimum time between API requests (seconds)",
"system_prompt": "Default system prompt for all conversations"
}
}
},
"error": {
"invalid_api_key": "Invalid API key - please check your credentials",
"cannot_connect": "Failed to connect to API - check endpoint and network",
"invalid_model": "Selected model is not available or invalid",
"rate_limit": "API rate limit exceeded - please wait",
"unknown": "Unexpected error occurred - check logs for details"
}
},
"options": {
@@ -20,9 +28,11 @@
"init": {
"title": "HA Text AI Options",
"data": {
"model": "AI model selection",
"temperature": "Response temperature (0-2)",
"max_tokens": "Maximum response length",
"request_interval": "Time between requests"
"request_interval": "Time between requests (seconds)",
"system_prompt": "Default system instructions"
}
}
}
@@ -30,25 +40,57 @@
"services": {
"ask_question": {
"name": "Ask Question",
"description": "Send a question to the AI model",
"fields": {
"description": "Send a question to the AI model and receive a response",
"fields": {
"question": {
"name": "Question",
"description": "Your question for the AI"
"description": "Your question or prompt for the AI"
},
"system_prompt": {
"name": "System Prompt",
"description": "Optional system prompt to override default for this question"
},
"model": {
"name": "Model",
"description": "Optional AI model to use for this question"
},
"temperature": {
"name": "Temperature",
"description": "Optional temperature setting for this question (0-2)"
},
"max_tokens": {
"name": "Max Tokens",
"description": "Optional maximum token limit for this response"
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Clear conversation history"
"description": "Delete all stored conversation history"
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history"
"description": "Retrieve conversation history with metadata",
"fields": {
"limit": {
"name": "Limit",
"description": "Maximum number of conversations to return"
},
"filter_model": {
"name": "Filter Model",
"description": "Optional filter to show only specific model responses"
}
}
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Set system behavior instructions"
"description": "Set default system behavior instructions",
"fields": {
"prompt": {
"name": "System Prompt",
"description": "Instructions defining AI behavior and response style"
}
}
}
}
}
@@ -3,16 +3,24 @@
"step": {
"user": {
"title": "Настройка HA Text AI",
"description": "Настройка интеграции с OpenAI",
"description": "Настройка интеграции с AI-сервисами (OpenAI, Anthropic и др.)",
"data": {
"api_key": "Ваш ключ API OpenAI",
"model": "Модель ИИ для генерации ответов",
"api_key": "Ваш API-ключ",
"model": "Модель AI для ответов (например, gpt-3.5-turbo, claude-3-sonnet)",
"temperature": "Температура генерации ответов (0-2)",
"max_tokens": "Максимальное количество токенов в ответе (1-4096)",
"api_endpoint": "URL конечной точки API",
"request_interval": "Минимальное время между запросами к API (секунды)"
"api_endpoint": "URL конечной точки API (необязательно, для пользовательских endpoint)",
"request_interval": "Минимальный интервал между запросами к API (секунды)",
"system_prompt": "Системная инструкция по умолчанию для всех диалогов"
}
}
},
"error": {
"invalid_api_key": "Недействительный API-ключ - проверьте учетные данные",
"cannot_connect": "Не удалось подключиться к API - проверьте endpoint и сеть",
"invalid_model": "Выбранная модель недоступна или некорректна",
"rate_limit": "Превышен лимит запросов API - пожалуйста, подождите",
"unknown": "Произошла непредвиденная ошибка - проверьте логи"
}
},
"options": {
@@ -20,9 +28,11 @@
"init": {
"title": "Настройки HA Text AI",
"data": {
"model": "Выбор модели AI",
"temperature": "Температура ответов (0-2)",
"max_tokens": "Максимальная длина ответа",
"request_interval": "Время между запросами"
"request_interval": "Интервал между запросами (секунды)",
"system_prompt": "Системные инструкции по умолчанию"
}
}
}
@@ -30,25 +40,57 @@
"services": {
"ask_question": {
"name": "Задать вопрос",
"description": "Отправить вопрос модели ИИ",
"description": "Отправить вопрос модели AI и получить ответ",
"fields": {
"question": {
"name": "Вопрос",
"description": "Ваш вопрос для ИИ"
"description": "Ваш вопрос или запрос для AI"
},
"system_prompt": {
"name": "Системная инструкция",
"description": "Необязательная системная инструкция, заменяющая инструкцию по умолчанию для этого вопроса"
},
"model": {
"name": "Модель",
"description": "Необязательная модель AI для этого вопроса"
},
"temperature": {
"name": "Температура",
"description": "Необязательная настройка температуры для этого вопроса (0-2)"
},
"max_tokens": {
"name": "Макс. токенов",
"description": "Необязательное ограничение количества токенов для этого ответа"
}
}
},
"clear_history": {
"name": "Очистить историю",
"description": "Очистить историю разговора"
"description": "Удалить всю сохраненную историю диалогов"
},
"get_history": {
"name": "Получить историю",
"description": "Получить историю разговора"
"description": "Получить историю диалогов с метаданными",
"fields": {
"limit": {
"name": "Лимит",
"description": "Максимальное количество диалогов для возврата"
},
"filter_model": {
"name": "Фильтр по модели",
"description": "Необязательный фильтр для показа ответов только от определенной модели"
}
}
},
"set_system_prompt": {
"name": "Установить системный промпт",
"description": "Установить инструкции поведения системы"
"name": "Установить системную инструкцию",
"description": "Задать системные инструкции по умолчанию",
"fields": {
"prompt": {
"name": "Системная инструкция",
"description": "Инструкции, определяющие поведение AI и стиль ответов"
}
}
}
}
}
+1 -1
View File
@@ -4,6 +4,6 @@
"domains": ["sensor"],
"homeassistant": "2024.11.0",
"icon": "mdi:brain",
"version": "1.0.10",
"version": "1.1.0",
"documentation": "https://github.com/smkrv/ha-text-ai"
}