AutoLove: апдейты девушке с YandexGPT

МЕНЮ


Главная страница
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Салют! Меня зовут Григорий, я главный по спецпроектам в AllSee. Как и у многих из нас, у меня есть вторая половинка, и ей свойственно требовать внимания. Сам по себе я человек занятой и мне бывает трудно отвлечься от дел и написать апдейт девушке, из-за чего приходиться терпеть капризы по причине «недостатка внимания». В статье я рассказываю, как YandexGPT и Python-Telegram «уделяют внимание» моей девушке.

Какие вводные?

Есть чат в Telegram, куда нужно писать апдейты: пожелать доброго утра, написать вечером, как я скучаю, спросить, как дела и так далее. Я хочу автоматизировать отправку сообщений в определённые временные слоты. Тексты апдейтов могут быть вариативными и содержать опциональные вопросы. Для генерации апдейтов я буду использовать YandexGPT API, а для взаимодействия с Telegram — python-telegram.

Routines (служебные функции)

Hidden text

Чтение YAML и JSON

def read_yaml(path: str) -> dict:     with open(path, 'r') as stream:         return yaml.safe_load(stream)   def read_json(path: str) -> dict:     with open(path, 'r') as stream:         return json.load(stream)

Преобразование списков строк формата «00:00» в списки datetime.time

def yaml_to_datetime_time(times_str: list) -> list:     return [         datetime.time(             *map(                 int,                 time_str.split(':')             )         )         for time_str         in times_str     ]

YandexGPT API

Перейдём сразу к коду для отправки запросов:

 def send_completion_request(           self,           messages: List[dict],           temperature: float = 0.6,           max_tokens: int = 1000,           stream: bool = False,           completion_url: str = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"   ) -> dict:       # checking if IAM token and catalog id is set       if not self._iam_token or not self._catalog_id:           raise Exception("IAM token and catalog id must be set to send completion requests")       # sending request       headers = {           "Content-Type": "application/json",           "Authorization": f"Bearer {self._iam_token}",           "x-folder-id": self._catalog_id       }       data = {           "modelUri": f"gpt://{self._catalog_id}/{self.model_type}/latest",           "completionOptions": {               "stream": stream,               "temperature": temperature,               "maxTokens": str(max_tokens)           },           "messages": messages       }       response = requests.post(completion_url, headers=headers, json=data)       # checking response       if response.status_code == 200:           return response.json()       else:           raise Exception(f"Failed to send completion request. Status code: {response.status_code} {response.text}") 

Помимо опциональных параметров temperature, max_tokens, stream, completion_url требуется задать IAM?токен и ID каталога Yandex Cloud.

Я автоматизировал генерацию IAM?токена, поэтому для работы будет достаточно передать YAML?конфиг с указанием следующих полей (как создать ключ авторизации и где взять ID сервисного аккаунта):

ServiceAccountID: 1111111111111 ServiceAccountKeyID: 11111111111 CatalogID: 11111111

Сгенерированный в формате JSON ключ также нужно будет передавать объекту класса ChatGPT.

Вот как выглядит полная реализация класса:

Hidden text
class YandexGPT:     available_models = [         'yandexgpt',         'yandexgpt-lite',         'summarization'     ]      def __init__(             self,             model_type: str = 'yandexgpt',             iam_token: str = None,             catalog_id: str = None,             yandex_cloud_config_file_path: str = None,             yandex_gpt_key_file_path: str = None,             iam_url: str = "https://iam.api.cloud.yandex.net/iam/v1/tokens"     ) -> None:         # setting config files         self._yandex_cloud_config_file_path = yandex_cloud_config_file_path         self._yandex_gpt_key_file_path = yandex_gpt_key_file_path         self._iam_url = iam_url         # setting model type         if model_type not in self.available_models:             raise ValueError(f"Model type must be one of {self.available_models}")         else:             self.model_type = model_type         # setting IAM token         if not iam_token:             self._set_iam_token()         else:             self._iam_token = iam_token         # setting catalog id         if not catalog_id:             self._set_catalog_id()         else:             self._catalog_id = catalog_id      def _set_iam_token(self) -> None:         # reading yaml config         config = read_yaml(self._yandex_cloud_config_file_path)         # reading json key         key = read_json(self._yandex_gpt_key_file_path)         # generating jwt token         jwt_token = self._generate_jwt_token(             service_account_id=config['ServiceAccountID'],             private_key=key['private_key'],             key_id=config['ServiceAccountKeyID'],             url=self._iam_url         )         # sending request to get IAM token and setting IAM token         self._iam_token = self._swap_jwt_to_iam(             jwt_token=jwt_token,             url=self._iam_url         )      @staticmethod     def _swap_jwt_to_iam(             jwt_token: str,             url: str = "https://iam.api.cloud.yandex.net/iam/v1/tokens"     ) -> str:         # sending request to get IAM token         headers = {             "Content-Type": "application/json"         }         data = {             "jwt": jwt_token         }         response = requests.post(url, headers=headers, json=data)         # checking response         if response.status_code == 200:             return response.json()['iamToken']         else:             raise Exception(                 f"Failed to get IAM token. "                 f"Status code: {response.status_code}"                 f" "                 f"{response.text}"             )      @staticmethod     def _generate_jwt_token(             service_account_id: str,             private_key: str,             key_id: str,             url: str = 'https://iam.api.cloud.yandex.net/iam/v1/tokens'     ) -> str:         # generating jwt token         now = int(time.time())         payload = {             'aud': url,             'iss': service_account_id,             'iat': now,             'exp': now + 360         }         encoded_token = jwt.encode(             payload,             private_key,             algorithm='PS256',             headers={'kid': key_id}         )         return encoded_token      def _set_catalog_id(self) -> None:         # reading yaml config file         config = read_yaml(self._yandex_cloud_config_file_path)         # setting catalog id from config file         self._catalog_id = config['CatalogID']      def send_completion_request(             self,             messages: List[dict],             temperature: float = 0.6,             max_tokens: int = 1000,             stream: bool = False,             completion_url: str = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"     ) -> dict:         # checking if IAM token and catalog id is set         if not self._iam_token or not self._catalog_id:             raise Exception("IAM token and catalog id must be set to send completion requests")         # sending request         headers = {             "Content-Type": "application/json",             "Authorization": f"Bearer {self._iam_token}",             "x-folder-id": self._catalog_id         }         data = {             "modelUri": f"gpt://{self._catalog_id}/{self.model_type}/latest",             "completionOptions": {                 "stream": stream,                 "temperature": temperature,                 "maxTokens": str(max_tokens)             },             "messages": messages         }         response = requests.post(completion_url, headers=headers, json=data)         # checking response         if response.status_code == 200:             return response.json()         else:             raise Exception(f"Failed to send completion request. Status code: {response.status_code} {response.text}")

Я предусмотрел возможность передачи параметров класса напрямую, а также возможность изменить логику инициализации из конфиг файлов  для того чтобы каждый желающий мог адаптировать данный функционал под конкретные сценарии использования.

Python-Telegram

Создадим дочерний от telegram.client.Telegram и YangexGPT класс. Он должен уметь определять временной слот и отправлять сгенерированный текст целевому контакту.

Hidden text
class AutoChat(Telegram, YandexGPT):     def __init__(             self,             target_username: str = None,             sending_time: List[datetime.time] = None,             chat_system_prompt: List[str] = None,             chat_mode: bool = None,             timezone: pytz.timezone = None,             telegram_config_file_path: str = None,             auto_chat_config_file_path: str = None,             *args,             **kwargs,     ) -> None:         # initializing telegram         # checking if api_id, api_hash, phone, database_encryption_key, files_directory and login in **kwargs         if all(                 key                 in kwargs                 for key                 in [                     'api_id',                     'api_hash',                     'phone',                     'database_encryption_key',                     'files_directory',                     'login'                 ]         ):             Telegram.__init__(                 *args,                 **kwargs,             )         # if there is no then calling custom telegram init method         else:             self._init_telegram(telegram_config_file_path)         # initializing yandex gpt         # checking if iam_token and catalog_id in **kwargs         if all(                 key                 in kwargs                 for key                 in [                     'iam_token',                     'catalog_id'                 ]         ):             YandexGPT.__init__(                 *args,                 **kwargs             )         # if there is no then calling custom yandex gpt init method         else:             self._init_yandex_gpt(                 *args,                 **kwargs             )         # initializing auto chat         # checking if target_username, sending_time, chat_system_prompt, chat_mode, timezone is set         if all([             target_username,             sending_time,             chat_system_prompt,             chat_mode,             timezone         ]):             self._target_user_id = self._get_target_user_id(target_username)             self._sending_time = yaml_to_datetime_time(sending_time)             self._chat_system_prompt = chat_system_prompt             self._chat_mode = chat_mode             self._timezone = timezone         # if there is no then calling custom auto chat init method         else:             self._init_autochat(auto_chat_config_file_path)      def _init_yandex_gpt(             self,             *args,             **kwargs     ) -> None:         # checking if yandex_cloud_config_file_path and yandex_gpt_key_file_path in **kwargs         if not all(                 key                 in kwargs                 for key                 in [                     'yandex_cloud_config_file_path',                     'yandex_gpt_key_file_path'                 ]         ):             raise ValueError(                 "AutoChat args must contain either"                 " 'yandex_cloud_config_file_path' and "                 "'yandex_gpt_key_file_path' or "                 "'iam_token' and 'catalog_id'"             )         # if they are than calling custom yandex gpt init method         else:             YandexGPT.__init__(                 self,                 *args,                 **kwargs             )      def _init_telegram(             self,             telegram_config_file_path: str = None,     ) -> None:         # checking if telegram_config_file_path is set         if not telegram_config_file_path:             raise ValueError(                 "AutoChat args must contain either"                 " 'telegram_config_file_path' or "                 "'api_id', 'api_hash', 'phone', "                 "'database_encryption_key', "                 "'files_directory' and 'login'"             )         # if it is than calling custom telegram init method         else:             # reading config file             telegram_config = read_yaml(telegram_config_file_path)             # initializing Telegram class             Telegram.__init__(                 self,                 api_id=int(telegram_config['ApiId']),                 api_hash=telegram_config['ApiHash'],                 phone=telegram_config['PhoneNumber'],                 database_encryption_key=telegram_config['DatabaseEncryptionKey'],                 files_directory=os.path.join(                     os.getcwd(),                     telegram_config['FilesDirectory']                 ),                 login=telegram_config['Login'],             )      def _init_autochat(             self,             auto_chat_config_file_path: str = None     ) -> None:         # checking if auto_chat_config_file_path is set         if not auto_chat_config_file_path:             raise ValueError(                 "AutoChat args must contain ether 'auto_chat_config_file_path'"                 " or 'target_username', 'sending_time', 'chat_system_prompt',"                 " 'chat_mode' and 'timezone'"             )         # if it is than calling custom auto chat init method         # reading config file         auto_chat_config = read_yaml(auto_chat_config_file_path)         # setting autochat         self._target_user_id = self._get_target_user_id(auto_chat_config['TargetUsername'])         self._sending_time = yaml_to_datetime_time(auto_chat_config['SendingTime'])         self._chat_system_prompt = auto_chat_config['ChatSystemPrompt']         self._chat_mode = auto_chat_config['ChatMode']         self._timezone = pytz.timezone(auto_chat_config['TimeZone'])      def _get_target_user_id(             self,             target_username: str     ) -> int:         # getting target user id         return int(self._get_contact_by_username(target_username)['id'])      def _get_client_contacts(self) -> List[dict]:         # getting client chats         chats = self.get_chats()         chats.wait()         # getting contacts from private chats         contacts = []         for chat_id in chats.update['chat_ids']:             chat = self.get_chat(chat_id)             chat.wait()             if chat.update['type']['@type'] == 'chatTypePrivate':                 contact = self.get_user(chat_id)                 contact.wait()                 contacts.append(contact.update)         # returning contacts         return contacts      def _get_contact_by_username(             self,             target_username: str     ) -> dict:         # getting list of all contacts         contacts = self._get_client_contacts()         # finding target contact by username         for contact in contacts:             if contact['username'] == target_username:                 return contact         # raise ValueError if there is no such contact         raise ValueError(f"Contact with username {target_username} not found")      def send_message_to_target_username(             self,             text: Union[str, Element],             entities: Union[List[dict], None] = None,     ) -> Union[AsyncResult, None]:         # sending message to target contact         return self.send_message(             chat_id=self._target_user_id,             text=text,             entities=entities         )      def which_sending_time(self) -> int:         # getting current time without seconds and microseconds         current_time = (             datetime.datetime             .now(self._timezone)             .replace(second=0, microsecond=0)             .time()         )         # checking if current time is in sending time list and returning its index or -1 if it is not         for index, sending_time_unit in enumerate(self._sending_time):             if current_time == sending_time_unit:                 return index         return -1      def check_time_and_send(self):         # checking if current time is in sending time list         which_sending_time = self.which_sending_time()         # if it is than sending message         if which_sending_time > -1:             prompt = self._chat_system_prompt[which_sending_time]             text = self.send_completion_request(                 messages=[                     {                         "role": "system",                         "text": prompt                     }                 ]             )['result']['alternatives'][0]['message']['text']             self.send_message_to_target_username(text=text)

Как видно, я также вынес весь код связанный с инициализацией целевых параметров в отдельные методы и оставил возможность загрузить готовые параметры вместо конфигов при их наличии.

Для инициализации родительского класса Telegram будем передавать в AutoChat следующие параметры (как их получить и для чего они нужны смотрите тут и тут):

ApiId: 1111111 ApiHash: 1111111111 DatabaseEncryptionKey: 11111111 FilesDirectory: data PhoneNumber: '+11111111' Login: true

А вот настройки связанные с логикой отправки сообщений:

TargetUsername: aaaaaa SendingTime: [   '6:15',   '18:20' ] ChatSystemPrompt: [   'Ты - парень программист, у тебя есть девушка.    Пожелай твоей девушке доброго утра.   Можешь спросить опциональные релевантные вопросы у девушки.   Используй немного предложений: от 1 до 3.   Можешь использовать эмодзи.   В качестве ответа дай ТОЛЬКО текст пожелания.   Если ты дашь дополнительные комментарии к тексту - ты заплатишь штраф 999999 рублей.',   'Ты - парень программист, у тебя есть девушка.    Пожелай твоей девушке хорошего вечера, скажи, что скучаешь.   Можешь спросить опциональные релевантные вопросы у девушки.   Используй немного предложений: от 1 до 3.   Можешь использовать эмодзи.   В качестве ответа дай ТОЛЬКО текст пожелания.   Если ты дашь дополнительные комментарии к тексту - ты заплатишь штраф 999999 рублей.' ] ChatMode: false TimeZone: Europe/Moscow

Запуск автоапдейтов

Мы вышли на финишную прямую, далее только запуск нашего решения и проверка результатов.

  • Python?скрипт для запуска и остановки циклов проверки и отправки апдейтов

import threading import argparse import signal import time  from auto_chat.auto_chat import AutoChat   def auto_chat_check_loop(autochat_instance: AutoChat, exit_flag_instance: threading.Event) -> None:     # infinite loop with check_time_and_send every 60 seconds     while not exit_flag_instance.is_set():         try:             autochat_instance.check_time_and_send()         except Exception as e:             print(f"Error while executing check_time_and_send: {e}")         time.sleep(60)   def stop_program(         exit_flag_instance: threading.Event,         autochat_instance: AutoChat,         autochat_thread_instance: threading.Thread ) -> None:     print(' ' + 'Stopping the program...')     exit_flag_instance.set()     autochat_instance.stop()     autochat_thread_instance.join()     print('Program stopped.')   if __name__ == "__main__":     print('Starting the program...')     # parsing arguments     parser = argparse.ArgumentParser(description='AutoChat Configuration')     parser.add_argument('--telegram-config', type=str, help='Path to Telegram config file')     parser.add_argument('--auto-chat-config', type=str, help='Path to AutoChat config file')     parser.add_argument('--yandex-cloud-config', type=str, help='Path to Yandex Cloud config file')     parser.add_argument('--yandex-gpt-key', type=str, help='Path to Yandex GPT key file')     args = parser.parse_args()     # initializing auto chat     autochat = AutoChat(         telegram_config_file_path=args.telegram_config,         auto_chat_config_file_path=args.auto_chat_config,         yandex_cloud_config_file_path=args.yandex_cloud_config,         yandex_gpt_key_file_path=args.yandex_gpt_key     )     # initializing exit flag     exit_flag = threading.Event()     # starting auto chat loop     autochat_thread = threading.Thread(         target=auto_chat_check_loop,         args=(autochat, exit_flag)     )     autochat_thread.start()     print('Program is running...' + ' ' + 'Use Ctrl+C or just exit to stop the program')     # setting stop signals     signal.signal(signal.SIGINT, lambda sig, frame: stop_program(exit_flag, autochat, autochat_thread))     signal.signal(signal.SIGTERM, lambda sig, frame: stop_program(exit_flag, autochat, autochat_thread))
  • Dockefile (укажите актуальные для вас пути к конфигам)

# using python parent image FROM python:3-slim  # defining workdir WORKDIR /auto_chat  # copying the current directory contents into the container COPY ./ /auto_chat  # installing any needed dependencies specified in requirements.txt RUN pip install --no-cache-dir -r requirements.txt  # installing openssl 1.1 using the package manager RUN apt-get update RUN apt-get install -y wget RUN wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb  # running start script (ENTRYPOINT instead of CMD to be sure that our run.py would catch stop signal) ENTRYPOINT [      "python",      "run.py",      "--telegram-config",      "config/telegram.yaml",      "--auto-chat-config",      "config/autolove.yaml",      "--yandex-cloud-config",      "config/yandex_cloud.yaml",      "--yandex-gpt-key",      "keys/yandex_authorization_key.json"  ]
  • Сборка и запуск контейнера (при первом запуске нужно будет ввести код телеграмм, далее можно использовать detach-режим)

docker build -t auto_chat . docker run -i --name auto_chat-container auto_chat

Результат

Результат меня радует, поставленную задачу программа решает, однако нужно будет добавить в промпт требование не навязывать помощь?

Заключение

Я рассказал вам, как решал задачу автоматической отправки апдейтов девушке.

Были ли трудности, оставшиеся за кадром? Абсолютно! Отдельно отмечу танцы с IAM?токенами. Но, как говорится, всё, что нас не убивает, делает нас сильнее.

Всю кодовую базу вы можете найти в репозитории проекта. Если кто?то вдохновиться проектом и решит самостоятельно модифицировать мои наработки — буду рад принять ваш pull request.

Что же дальше? Чат в реальном времени, интеграция базы знаний? Обязательно, если это будет вам интересно. Пишите, что думаете в комментариях или мне в telegram. Будем на связи??


Источник: habr.com

Комментарии: