diff --git a/.idea/.gitignore b/.gitignore similarity index 58% rename from .idea/.gitignore rename to .gitignore index b2e7db0..b9bee3a 100644 --- a/.idea/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ +__pycache__ .idea # Default ignored files /shelf/ /workspace.xml -# Editor-based HTTP Client requests /httpRequests/ -# Datasource local storage ignored files /dataSources/ /dataSources.local.xml *.iws *.iml -*.ipr \ No newline at end of file +*.ipr diff --git a/README.md b/README.md index 7f9f9d1..d933b3b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,10 @@ [![Github All Releases](https://img.shields.io/github/downloads/gritaro/gigachain/total.svg)](https://github.com/gritaro/gigachain/releases) Компонент реализует диалоговую систему Home Assistant для использования с языковыми моделями, поддерживаемыми фреймворком GigaChain. -В настоящее время поддерживается только интеграция с LMM GigaChat (русскоязычная нейросеть от Сбера) +В настоящее время поддерживаются интеграции с LMM: +* [GigaChat](#GigaChat) (русскоязычная нейросеть от Сбера) +* [YandexGPT](#YandexGPT) +* [OpenAI](#OpenAI) ака ChatGPT (не тестируется) ## Установка Устанавливается как и любая HACS интеграция. @@ -39,7 +42,7 @@ После добавления настройте интеграцию. ## Настройки - +### GigaChat ### Авторизация запросов к GigaChat Для авторизации запросов к GigaChat вам понадобится получить *авторизационные данные* для работы с GigaChat API. @@ -49,10 +52,39 @@ Authorization data -### Конфигурация +### YandexGPT +Быстрый старт + +Создайте сервисный аккаунт с ролью `ai.languageModels.user` +Для создания аккаунта потребуется привязка карты. +Создайте API ключ +Идентификатор каталога (Folder ID) можно узнать пройдя по ссылке + +### OpenAI +Для генерации ключа проследуйте по ссылке https://platform.openai.com/account/api-keys + +## Конфигурация + +* _Темплейт промпта_ (template, Home Assistant `template`) + +Системное сообщение, настраивающее модель и задающее исходное поведение. +Значение по умолчанию является лишь примером, взятым из офицальной интеграции OpenAI Conversation +Рекомендуется его изменить под собственные нужды. + +* _Модель_ (model, `string`) + +Модели генерации текста в рамках выбранной LLM. Каждая модель может иметь свои тарифы. +В настоящее время выбор модели не поддерживается. + +* _Температура_ (temperature, `float`) + +Температура выборки. Значение температуры должно быть не меньше ноля. Чем выше значение, тем более случайным будет ответ модели. При значениях температуры больше двух, набор токенов в ответе модели может отличаться избыточной случайностью. +Значение по умолчанию зависит от выбранной модели + +* Максимум токенов (max_tokens, int) -* Темплейт промпта -* Модель +Максимальное количество токенов, которые будут использованы для создания ответов. +В настоящее время не поддерживается, используются настройки модели по умолчанию. ## Использование в качестве диалоговой системы Создайте и настройте новый голосовой ассистент: diff --git a/custom_components/gigachain/__init__.py b/custom_components/gigachain/__init__.py index 77c5de9..89a2348 100644 --- a/custom_components/gigachain/__init__.py +++ b/custom_components/gigachain/__init__.py @@ -10,11 +10,18 @@ ) from homeassistant.components.conversation import AgentManager, agent from typing import Literal -from langchain_community.chat_models import GigaChat +from langchain_community.chat_models import GigaChat, ChatYandexGPT, ChatOpenAI from langchain.schema import AIMessage, HumanMessage, SystemMessage from homeassistant.util import ulid from .const import ( DOMAIN, + CONF_ENGINE, + CONF_TEMPERATURE, + DEFAULT_CONF_TEMPERATURE, + CONF_CHAT_MODEL, + DEFAULT_CHAT_MODEL, + CONF_CHAT_MODEL, + CONF_FOLDER_ID, CONF_API_KEY, CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL, @@ -25,9 +32,29 @@ LOGGER = logging.getLogger(__name__) +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener.""" + await hass.config_entries.async_reload(entry.entry_id) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Initialize GigaChain.""" - client = GigaChat(credentials=entry.data[CONF_API_KEY], verify_ssl_certs=False) + temperature = entry.options.get(CONF_TEMPERATURE, DEFAULT_CONF_TEMPERATURE) + engine = entry.data.get(CONF_ENGINE) or "gigachat" + entry.async_on_unload(entry.add_update_listener(update_listener)) + if engine == 'gigachat': + client = GigaChat(temperature=temperature, + model='GigaChat:latest', + verbose=True, + credentials=entry.data[CONF_API_KEY], + verify_ssl_certs=False) + elif engine == 'yandexgpt': + client = ChatYandexGPT(temperature=temperature, + api_key=entry.data[CONF_API_KEY], + folder_id = entry.data[CONF_FOLDER_ID]) + else: + client = ChatOpenAI(model="gpt-3.5-turbo", + temperature=temperature, + openai_api_key=entry.data[CONF_API_KEY]) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client conversation.async_set_agent(hass, entry, GigaChatAI(hass, entry)) return True @@ -55,7 +82,6 @@ async def async_process( ) -> agent.ConversationResult: """Process a sentence.""" raw_prompt = self.entry.options.get(CONF_PROMPT, DEFAULT_PROMPT) - model = self.entry.options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL) if user_input.conversation_id in self.history: conversation_id = user_input.conversation_id messages = self.history[conversation_id] @@ -70,8 +96,6 @@ async def async_process( messages.append(HumanMessage(content=user_input.text)) client = self.hass.data[DOMAIN][self.entry.entry_id] - client.model = model - res = client(messages) messages.append(res) self.history[conversation_id] = messages diff --git a/custom_components/gigachain/__pycache__/__init__.cpython-312.pyc b/custom_components/gigachain/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index f53ea50..0000000 Binary files a/custom_components/gigachain/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/custom_components/gigachain/__pycache__/config_flow.cpython-312.pyc b/custom_components/gigachain/__pycache__/config_flow.cpython-312.pyc deleted file mode 100644 index dddf035..0000000 Binary files a/custom_components/gigachain/__pycache__/config_flow.cpython-312.pyc and /dev/null differ diff --git a/custom_components/gigachain/__pycache__/const.cpython-312.pyc b/custom_components/gigachain/__pycache__/const.cpython-312.pyc deleted file mode 100644 index 340af6b..0000000 Binary files a/custom_components/gigachain/__pycache__/const.cpython-312.pyc and /dev/null differ diff --git a/custom_components/gigachain/config_flow.py b/custom_components/gigachain/config_flow.py index 56f6e81..cb51e97 100644 --- a/custom_components/gigachain/config_flow.py +++ b/custom_components/gigachain/config_flow.py @@ -9,26 +9,62 @@ from homeassistant.data_entry_flow import FlowResult import types from types import MappingProxyType +from homeassistant.helpers import selector from homeassistant.helpers.selector import ( - NumberSelector, - NumberSelectorConfig, - TemplateSelector, + TemplateSelector ) +import logging + +LOGGER = logging.getLogger(__name__) + from .const import ( + DOMAIN, + CONF_ENGINE, CONF_API_KEY, + CONF_FOLDER_ID, CONF_CHAT_MODEL, + CONF_TEMPERATURE, + CONF_ENGINE_OPTIONS, CONF_PROMPT, + CONF_MAX_TKNS, + DEFAULT_CONF_TEMPERATURE, + DEFAULT_CONF_MAX_TKNS, DEFAULT_CHAT_MODEL, DEFAULT_PROMPT, - DOMAIN + UNIQUE_ID, ) -STEP_USER_DATA_SCHEMA = vol.Schema( +STEP_USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENGINE): selector.SelectSelector( + selector.SelectSelectorConfig(options=CONF_ENGINE_OPTIONS), + ), + } +) + +STEP_GIGACHAT_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str + } +) +STEP_YANDEXGPT_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_FOLDER_ID): str + } +) +STEP_OPENAI_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): str } ) +ENGINE_SCHEMA = { + "gigachat": STEP_GIGACHAT_SCHEMA, + "yandexgpt": STEP_YANDEXGPT_SCHEMA, + "openai": STEP_OPENAI_SCHEMA +} + DEFAULT_OPTIONS = types.MappingProxyType( { CONF_PROMPT: DEFAULT_PROMPT, @@ -45,12 +81,37 @@ async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" + if user_input is None: + return self.async_show_form(step_id="user", + data_schema=STEP_USER_SCHEMA) + + engine = user_input[CONF_ENGINE] + unique_id = UNIQUE_ID[engine] + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_show_form( + step_id=engine, data_schema=ENGINE_SCHEMA[engine] + ) + + async def async_step_gigachat( + self, user_input: dict[str, Any] | None = None) -> FlowResult: + return await self.common_model_async_step("gigachat", user_input) + + async def async_step_yandexgpt( + self, user_input: dict[str, Any] | None = None) -> FlowResult: + return await self.common_model_async_step("yandexgpt", user_input) + + async def async_step_openai(self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + return await self.common_model_async_step("openai", user_input) + + async def common_model_async_step(self, engine, user_input): if user_input is None: return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id=engine, data_schema=ENGINE_SCHEMA[engine] ) - - unique_id = "GigaChat" + user_input[CONF_ENGINE] = engine + unique_id = UNIQUE_ID[engine] await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() return self.async_create_entry(title=unique_id, data=user_input) @@ -74,14 +135,14 @@ async def async_step_init( ) -> FlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="GigaChat", data=user_input) - schema = gigachat_config_option_schema(self.config_entry.options) + return self.async_create_entry(title=self.config_entry.unique_id, data=user_input) + schema = common_config_option_schema(self.config_entry.options) return self.async_show_form( step_id="init", data_schema=vol.Schema(schema), ) -def gigachat_config_option_schema(options: MappingProxyType[str, Any]) -> dict: +def common_config_option_schema(options: MappingProxyType[str, Any]) -> dict: """Return a schema for GigaChain completion options.""" if not options: options = DEFAULT_OPTIONS @@ -95,8 +156,27 @@ def gigachat_config_option_schema(options: MappingProxyType[str, Any]) -> dict: CONF_CHAT_MODEL, description={ # New key in HA 2023.4 - "suggested_value": options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL) + "suggested_value": options.get(CONF_CHAT_MODEL, + DEFAULT_CHAT_MODEL) }, default=DEFAULT_CHAT_MODEL, ): str, + vol.Optional( + CONF_TEMPERATURE, + description={ + # New key in HA 2023.4 + "suggested_value": options.get(CONF_TEMPERATURE, + DEFAULT_CONF_TEMPERATURE) + }, + default=DEFAULT_CONF_TEMPERATURE, + ): float, + vol.Optional( + CONF_MAX_TKNS, + description={ + # New key in HA 2023.4 + "suggested_value": options.get(CONF_MAX_TKNS, + DEFAULT_CONF_MAX_TKNS) + }, + default=DEFAULT_CONF_MAX_TKNS, + ): int, } diff --git a/custom_components/gigachain/const.py b/custom_components/gigachain/const.py index ac1a5c3..489c299 100644 --- a/custom_components/gigachain/const.py +++ b/custom_components/gigachain/const.py @@ -1,7 +1,17 @@ """Constants for the GigaChain integration.""" +from homeassistant.helpers import selector DOMAIN = "gigachain" +CONF_ENGINE = "engine" +UNIQUE_ID = {"gigachat": "GigaChat", "yandexgpt": "YandexGPT", "openai": "OpenAI"} +CONF_ENGINE_OPTIONS = [ + selector.SelectOptionDict(value="gigachat", label="GigaChat"), + selector.SelectOptionDict(value="yandexgpt", label="YandexGPT"), + selector.SelectOptionDict(value="openai", label="OpenAI"), +] CONF_API_KEY = "api_key" +CONF_FOLDER_ID = "folder_id" + CONF_PROMPT = "prompt" DEFAULT_PROMPT = """Ты HAL 9000, компьютер из цикла произведений «Космическая одиссея» Артура Кларка, обладающий способностью к самообучению. Мы находимся в умном доме под управлением системы Home Assistant. @@ -25,3 +35,7 @@ CONF_CHAT_MODEL = "model" #GigaChat-Plus,GigaChat-Pro,GigaChat:latest DEFAULT_CHAT_MODEL = "GigaChat" +CONF_TEMPERATURE = "temperature" +DEFAULT_CONF_TEMPERATURE = "0.1" +CONF_MAX_TKNS = "max_tokens" +DEFAULT_CONF_MAX_TKNS = "250" diff --git a/custom_components/gigachain/manifest.json b/custom_components/gigachain/manifest.json index 026400e..801d38f 100644 --- a/custom_components/gigachain/manifest.json +++ b/custom_components/gigachain/manifest.json @@ -10,8 +10,8 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/gritaro/gigachain/issues", "requirements": [ - "gigachat", - "langchain", + "gigachat==0.1.16", + "langchain==0.1.7", "gigachain-community==0.0.16", "yandexcloud==0.259.0" ], diff --git a/custom_components/gigachain/strings.json b/custom_components/gigachain/strings.json index cd4bc14..3499fa8 100644 --- a/custom_components/gigachain/strings.json +++ b/custom_components/gigachain/strings.json @@ -2,9 +2,28 @@ "config": { "step": { "user": { - "title": "GigaChain configuration", + "title": "GigaChain configuration - select engine", "data": { - "auth_data": "Authorization data" + "engine": "LLM Engine" + } + }, + "gigachat": { + "title": "GigaChat configuration", + "data": { + "api_key": "Auth data" + } + }, + "yandexgpt": { + "title": "YandexGPT configuration", + "data": { + "api_key": "API Key", + "folder_id": "Folder ID" + } + }, + "openai": { + "title": "OpenAI configuration", + "data": { + "api_key": "API Key" } } }, @@ -15,9 +34,12 @@ "options": { "step": { "init": { + "title": "Model configuration", "data": { "prompt": "Prompt Template", - "model": "Completion Model" + "model": "Completion Model", + "temperature": "Temperature", + "max_tokens": "Max Tokens" } } } diff --git a/custom_components/gigachain/translations/en.json b/custom_components/gigachain/translations/en.json index fca91c9..3499fa8 100644 --- a/custom_components/gigachain/translations/en.json +++ b/custom_components/gigachain/translations/en.json @@ -2,9 +2,28 @@ "config": { "step": { "user": { - "title": "GigaChain configuration", + "title": "GigaChain configuration - select engine", "data": { - "api_key": "Authorization data" + "engine": "LLM Engine" + } + }, + "gigachat": { + "title": "GigaChat configuration", + "data": { + "api_key": "Auth data" + } + }, + "yandexgpt": { + "title": "YandexGPT configuration", + "data": { + "api_key": "API Key", + "folder_id": "Folder ID" + } + }, + "openai": { + "title": "OpenAI configuration", + "data": { + "api_key": "API Key" } } }, @@ -15,9 +34,12 @@ "options": { "step": { "init": { + "title": "Model configuration", "data": { "prompt": "Prompt Template", - "model": "Completion Model" + "model": "Completion Model", + "temperature": "Temperature", + "max_tokens": "Max Tokens" } } } diff --git a/custom_components/gigachain/translations/ru.json b/custom_components/gigachain/translations/ru.json index 08c70c0..c29390a 100644 --- a/custom_components/gigachain/translations/ru.json +++ b/custom_components/gigachain/translations/ru.json @@ -2,22 +2,44 @@ "config": { "step": { "user": { - "title": "GigaChain конфигурация", + "title": "GigaChain конфигурация - выбор LLM", + "data": { + "engine": "Большая языковая модель" + } + }, + "gigachat": { + "title": "Конфигурация GigaChat", "data": { "api_key": "Авторизационные данные" } + }, + "yandexgpt": { + "title": "Конфигурация YandexGPT", + "data": { + "api_key": "API ключ", + "folder_id": "Folder ID" + } + }, + "openai": { + "title": "Конфигурация OpenAI", + "data": { + "api_key": "API ключ" + } } }, "abort": { - "already_configured": "Нельзя настроить более одной интеграции" + "already_configured": "Эта модель уже настроена" } }, "options": { "step": { "init": { + "title": "Конфигурация модели", "data": { "prompt": "Промпт темплейт", - "model": "Модель" + "model": "Модель", + "temperature": "Температура", + "max_tokens": "Максимум токенов" } } }