From e85e2c03bda1db88b860409bfe9e248a293c410f Mon Sep 17 00:00:00 2001 From: Struchkov Mark Date: Sun, 26 Mar 2023 22:13:37 +0300 Subject: [PATCH] Behavior set, multi-account, inline mod, and other little things --- .drone.yml | 9 +- pom.xml | 10 +- ...plication.java => ChatGptTelegramBot.java} | 4 +- .../example/bot/conf/AppProperty.java | 4 +- .../bot/conf/ErrorTelegramHandler.java | 12 +- .../bot/handler/InlineChoseHandler.java | 63 +++++++ .../bot/handler/InlineQueryHandler.java | 98 ++++++++++ .../example/bot/service/InlineStorage.java | 33 ++++ .../bot/service/PersonalChatService.java | 62 +++++++ .../example/bot/unit/BehaviorUnit.java | 118 +++++++++++++ .../example/bot/unit/PersonalChatGPTUnit.java | 167 ++++++++++-------- .../example/bot/unit/StartNotify.java | 67 +++---- .../dev/struchkov/example/bot/util/Cmd.java | 8 + .../struchkov/example/bot/util/UnitName.java | 2 +- src/main/resources/application.yml | 2 +- 15 files changed, 541 insertions(+), 118 deletions(-) rename src/main/java/dev/struchkov/example/bot/{SpringBootTelegramBotExampleApplication.java => ChatGptTelegramBot.java} (62%) create mode 100644 src/main/java/dev/struchkov/example/bot/handler/InlineChoseHandler.java create mode 100644 src/main/java/dev/struchkov/example/bot/handler/InlineQueryHandler.java create mode 100644 src/main/java/dev/struchkov/example/bot/service/InlineStorage.java create mode 100644 src/main/java/dev/struchkov/example/bot/service/PersonalChatService.java create mode 100644 src/main/java/dev/struchkov/example/bot/unit/BehaviorUnit.java diff --git a/.drone.yml b/.drone.yml index 9975a4c..f3efb1d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -34,10 +34,6 @@ steps: - name: docker publish develop image: upagge/docker-buildx:latest environment: - STRUCHKOV_DOCKER_REGISTRY_TOKEN: - from_secret: STRUCHKOV_DOCKER_REGISTRY_TOKEN - STRUCHKOV_DOCKER_IMAGE_NAME: - from_secret: STRUCHKOV_DOCKER_IMAGE_NAME DOCKER_REGISTRY_TOKEN: from_secret: DOCKER_REGISTRY_TOKEN DOCKER_IMAGE_NAME: @@ -48,10 +44,9 @@ steps: - name: dockersock path: /var/run commands: - - echo "$STRUCHKOV_DOCKER_REGISTRY_TOKEN" | docker login git.struchkov.dev --username $DOCKER_REGISTRY_USER --password-stdin - echo "$DOCKER_REGISTRY_TOKEN" | docker login docker.io --username $DOCKER_REGISTRY_USER --password-stdin - docker buildx create --use - - docker buildx build -f Dockerfile-develop --push --platform linux/amd64,linux/arm64/v8 -t "$DOCKER_IMAGE_NAME:develop" -t "git.struchkov.dev/$STRUCHKOV_DOCKER_IMAGE_NAME:develop" . + - docker buildx build -f Dockerfile-develop --push --platform linux/amd64,linux/arm64/v8 -t "$DOCKER_IMAGE_NAME:develop" . --- kind: pipeline @@ -203,6 +198,6 @@ volumes: path: /drone/volume/mkdocs_cache/chatgpt-telegram-bot/en --- kind: signature -hmac: 44ca201fe0ea4a7b739d9e170e0852171c5175c0128bfd3e77f7247f92db56a8 +hmac: b615ac14d736c1ff2354f10d96117108866cd0bc8d387f827ea063fbe66b418c ... diff --git a/pom.xml b/pom.xml index b298578..2b0fad8 100644 --- a/pom.xml +++ b/pom.xml @@ -15,8 +15,8 @@ 17 - 0.0.1 - 0.0.53 + 0.0.2-SNAPSHOT + 0.0.54-SNAPSHOT 3.10.1 @@ -61,6 +61,12 @@ lombok true + + + com.google.guava + guava + 31.1-jre + diff --git a/src/main/java/dev/struchkov/example/bot/SpringBootTelegramBotExampleApplication.java b/src/main/java/dev/struchkov/example/bot/ChatGptTelegramBot.java similarity index 62% rename from src/main/java/dev/struchkov/example/bot/SpringBootTelegramBotExampleApplication.java rename to src/main/java/dev/struchkov/example/bot/ChatGptTelegramBot.java index 1622c64..d236e1c 100644 --- a/src/main/java/dev/struchkov/example/bot/SpringBootTelegramBotExampleApplication.java +++ b/src/main/java/dev/struchkov/example/bot/ChatGptTelegramBot.java @@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class SpringBootTelegramBotExampleApplication { +public class ChatGptTelegramBot { public static void main(String[] args) { - SpringApplication.run(SpringBootTelegramBotExampleApplication.class, args); + SpringApplication.run(ChatGptTelegramBot.class, args); } } diff --git a/src/main/java/dev/struchkov/example/bot/conf/AppProperty.java b/src/main/java/dev/struchkov/example/bot/conf/AppProperty.java index 82e90af..d37402e 100644 --- a/src/main/java/dev/struchkov/example/bot/conf/AppProperty.java +++ b/src/main/java/dev/struchkov/example/bot/conf/AppProperty.java @@ -5,13 +5,15 @@ import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import java.util.List; + @Getter @Setter @Component @ConfigurationProperties("app") public class AppProperty { - private String telegramId; + private List telegramIds; private String version; } diff --git a/src/main/java/dev/struchkov/example/bot/conf/ErrorTelegramHandler.java b/src/main/java/dev/struchkov/example/bot/conf/ErrorTelegramHandler.java index 89ad7cc..a4eec38 100644 --- a/src/main/java/dev/struchkov/example/bot/conf/ErrorTelegramHandler.java +++ b/src/main/java/dev/struchkov/example/bot/conf/ErrorTelegramHandler.java @@ -19,13 +19,21 @@ public class ErrorTelegramHandler implements ErrorHandler { private final TelegramSending sending; @Override - public void handle(Message message, Exception e) { + public void handle(Message message, Throwable e) { log.error(e.getMessage(), e); final String errorMessage = escapeMarkdown(e.getMessage()); + + final String recipientTelegramId; + if (appProperty.getTelegramIds().contains(message.getFromPersonId())) { + recipientTelegramId = message.getFromPersonId(); + } else { + recipientTelegramId = appProperty.getTelegramIds().get(0); + } + sending.send( BoxAnswer.builder() .message("Error message:\n\n" + errorMessage) - .recipientPersonId(appProperty.getTelegramId()) + .recipientPersonId(recipientTelegramId) .build() ); } diff --git a/src/main/java/dev/struchkov/example/bot/handler/InlineChoseHandler.java b/src/main/java/dev/struchkov/example/bot/handler/InlineChoseHandler.java new file mode 100644 index 0000000..aae8072 --- /dev/null +++ b/src/main/java/dev/struchkov/example/bot/handler/InlineChoseHandler.java @@ -0,0 +1,63 @@ +package dev.struchkov.example.bot.handler; + +import dev.struchkov.example.bot.conf.AppProperty; +import dev.struchkov.example.bot.service.InlineStorage; +import dev.struchkov.godfather.main.domain.EventContainer; +import dev.struchkov.godfather.simple.context.service.EventHandler; +import dev.struchkov.godfather.telegram.simple.context.service.TelegramSending; +import dev.struchkov.openai.context.GPTClient; +import dev.struchkov.openai.domain.common.GptMessage; +import dev.struchkov.openai.domain.model.gpt.GPT3Model; +import dev.struchkov.openai.domain.request.GptRequest; +import dev.struchkov.openai.domain.response.Choice; +import dev.struchkov.openai.domain.response.GptResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.meta.api.objects.inlinequery.ChosenInlineQuery; + +import java.util.List; +import java.util.Optional; + +import static dev.struchkov.godfather.simple.domain.BoxAnswer.boxAnswer; + +@Slf4j +@Component +@RequiredArgsConstructor +public class InlineChoseHandler implements EventHandler { + + private final GPTClient gptClient; + private final InlineStorage inlineStorage; + private final AppProperty appProperty; + private final TelegramSending telegramSending; + + @Override + public void handle(EventContainer event) { + final ChosenInlineQuery chosenInlineQuery = event.getObject(); + final String personId = chosenInlineQuery.getFrom().getId().toString(); + + if (appProperty.getTelegramIds().contains(personId)) { + final Optional optRequest = inlineStorage.getQuery(chosenInlineQuery.getResultId()); + if (optRequest.isPresent()) { + final String request = optRequest.get(); + final GptResponse gptResponse = gptClient.execute( + GptRequest.builder() + .model(GPT3Model.GPT_3_5_TURBO) + .message( + GptMessage.fromUser(request) + ) + .build() + ); + final List choices = gptResponse.getChoices(); + final String answer = choices.get(choices.size() - 1).getMessage().getContent(); + telegramSending.replaceInlineMessage(chosenInlineQuery.getInlineMessageId(), boxAnswer(answer)); + } + } + } + + @Override + public Class getEventClass() { + return ChosenInlineQuery.class; + } + +} diff --git a/src/main/java/dev/struchkov/example/bot/handler/InlineQueryHandler.java b/src/main/java/dev/struchkov/example/bot/handler/InlineQueryHandler.java new file mode 100644 index 0000000..5d6c12c --- /dev/null +++ b/src/main/java/dev/struchkov/example/bot/handler/InlineQueryHandler.java @@ -0,0 +1,98 @@ +package dev.struchkov.example.bot.handler; + +import dev.struchkov.example.bot.conf.AppProperty; +import dev.struchkov.example.bot.service.InlineStorage; +import dev.struchkov.godfather.main.domain.EventContainer; +import dev.struchkov.godfather.simple.context.service.EventHandler; +import dev.struchkov.godfather.telegram.simple.context.service.TelegramConnect; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.meta.api.methods.AnswerInlineQuery; +import org.telegram.telegrambots.meta.api.objects.inlinequery.InlineQuery; +import org.telegram.telegrambots.meta.api.objects.inlinequery.inputmessagecontent.InputTextMessageContent; +import org.telegram.telegrambots.meta.api.objects.inlinequery.result.InlineQueryResultArticle; +import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; +import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; +import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException; + +import java.util.List; +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class InlineQueryHandler implements EventHandler { + + private final InlineStorage inlineStorage; + private final AppProperty appProperty; + private final TelegramConnect telegramConnect; + + @Override + public void handle(EventContainer event) { + final InlineQuery inlineQuery = event.getObject(); + if (appProperty.getTelegramIds().contains(inlineQuery.getFrom().getId().toString())) { + + final InputTextMessageContent buildMessageContent = InputTextMessageContent.builder() + .messageText(inlineQuery.getQuery()) + .build(); + + try { + buildMessageContent.validate(); + } catch (TelegramApiValidationException e) { + log.error(e.getMessage(), e); + } + + final String id = UUID.randomUUID().toString(); + inlineStorage.save(id, inlineQuery.getQuery()); + + final InlineQueryResultArticle result = InlineQueryResultArticle.builder() + .id(id) + .title("Your request to ChatGPT:") + .description(inlineQuery.getQuery()) + .hideUrl(true) + .thumbUrl("https://struchkov.dev/static/img/openai.jpeg") + .replyMarkup( + InlineKeyboardMarkup.builder() + .keyboard(List.of(List.of( + InlineKeyboardButton.builder() + .text("Wait... The answer is generated...") + .callbackData("inline_query") + .build() + ))) + .build() + ) + .inputMessageContent(buildMessageContent).build(); + + try { + result.validate(); + } catch (TelegramApiValidationException e) { + log.error(e.getMessage(), e); + } + + final AnswerInlineQuery answerInlineQuery = AnswerInlineQuery.builder() + .inlineQueryId(inlineQuery.getId()) + .result(result) + .build(); + + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + log.error(e.getMessage(), e); + } + + try { + telegramConnect.getAbsSender().execute(answerInlineQuery); + } catch (TelegramApiException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public Class getEventClass() { + return InlineQuery.class; + } + +} diff --git a/src/main/java/dev/struchkov/example/bot/service/InlineStorage.java b/src/main/java/dev/struchkov/example/bot/service/InlineStorage.java new file mode 100644 index 0000000..36b7146 --- /dev/null +++ b/src/main/java/dev/struchkov/example/bot/service/InlineStorage.java @@ -0,0 +1,33 @@ +package dev.struchkov.example.bot.service; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Service +public class InlineStorage { + + private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); + + private final Cache cache = CacheBuilder.newBuilder() + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(); + + public InlineStorage() { + scheduledExecutor.scheduleAtFixedRate(cache::cleanUp, 1, 1, TimeUnit.MINUTES); + } + + public void save(String inlineId, String query) { + cache.put(inlineId, query); + } + + public Optional getQuery(String inlineId) { + return Optional.ofNullable(cache.getIfPresent(inlineId)); + } + +} diff --git a/src/main/java/dev/struchkov/example/bot/service/PersonalChatService.java b/src/main/java/dev/struchkov/example/bot/service/PersonalChatService.java new file mode 100644 index 0000000..abb8b99 --- /dev/null +++ b/src/main/java/dev/struchkov/example/bot/service/PersonalChatService.java @@ -0,0 +1,62 @@ +package dev.struchkov.example.bot.service; + +import dev.struchkov.example.bot.conf.AppProperty; +import dev.struchkov.openai.context.ChatGptService; +import dev.struchkov.openai.domain.chat.ChatInfo; +import dev.struchkov.openai.domain.chat.CreateChat; +import dev.struchkov.openai.domain.chat.UpdateChat; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class PersonalChatService { + + private final ChatGptService chatGptService; + private final Map chatDefaultContext = new ConcurrentHashMap<>(); + private final Map chatMap = new ConcurrentHashMap<>(); + + public PersonalChatService( + AppProperty appProperty, + ChatGptService chatGptService + ) { + this.chatGptService = chatGptService; + appProperty.getTelegramIds().forEach( + telegramId -> { + final ChatInfo newChat = this.chatGptService.createChat( + CreateChat.builder().build() + ); + chatMap.put(telegramId, newChat); + } + ); + } + + public ChatInfo getChatByPersonId(String personId) { + return chatMap.get(personId); + } + + public ChatInfo recreateChat(String personId) { + final ChatInfo chatInfo = chatMap.get(personId); + chatGptService.closeChat(chatInfo.getChatId()); + final ChatInfo newChat = chatGptService.createChat( + CreateChat.builder() + .systemBehavior(chatDefaultContext.get(personId)) + .build() + ); + chatMap.put(personId, newChat); + return newChat; + } + + public void setBehavior(String fromPersonId, String behavior) { + chatDefaultContext.put(fromPersonId, behavior); + final ChatInfo chatInfo = chatMap.get(fromPersonId); + chatGptService.updateChat( + UpdateChat.builder() + .chatId(chatInfo.getChatId()) + .systemBehavior(behavior) + .build() + ); + } + +} diff --git a/src/main/java/dev/struchkov/example/bot/unit/BehaviorUnit.java b/src/main/java/dev/struchkov/example/bot/unit/BehaviorUnit.java new file mode 100644 index 0000000..3106112 --- /dev/null +++ b/src/main/java/dev/struchkov/example/bot/unit/BehaviorUnit.java @@ -0,0 +1,118 @@ +package dev.struchkov.example.bot.unit; + +import dev.struchkov.example.bot.conf.AppProperty; +import dev.struchkov.example.bot.service.PersonalChatService; +import dev.struchkov.example.bot.util.Cmd; +import dev.struchkov.example.bot.util.UnitName; +import dev.struchkov.godfather.main.domain.annotation.Unit; +import dev.struchkov.godfather.main.domain.content.Attachment; +import dev.struchkov.godfather.main.domain.content.Mail; +import dev.struchkov.godfather.simple.domain.BoxAnswer; +import dev.struchkov.godfather.simple.domain.unit.AnswerText; +import dev.struchkov.godfather.telegram.domain.attachment.CommandAttachment; +import dev.struchkov.godfather.telegram.main.core.util.Attachments; +import dev.struchkov.godfather.telegram.starter.PersonUnitConfiguration; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +import static dev.struchkov.godfather.simple.domain.BoxAnswer.boxAnswer; + +@Component +@RequiredArgsConstructor +public class BehaviorUnit implements PersonUnitConfiguration { + + private final PersonalChatService personalChatService; + private final AppProperty appProperty; + + @Unit(value = UnitName.BEHAVIOR, global = true) + public AnswerText behavior() { + return AnswerText.builder() + .triggerCheck( + mail -> { + if (appProperty.getTelegramIds().contains(mail.getFromPersonId())) { + final List attachments = mail.getAttachments(); + final Optional optCommand = Attachments.findFirstCommand(attachments); + if (optCommand.isPresent()) { + final CommandAttachment command = optCommand.get(); + return Cmd.BEHAVIOR.equals(command.getCommandType()); + } + } + return false; + } + ) + .answer(mail -> { + final CommandAttachment command = Attachments.findFirstCommand(mail.getAttachments()).orElseThrow(); + final Optional optArg = command.getArg(); + if (optArg.isEmpty()) { + return BoxAnswer.builder() + .message( + """ + Allows you to set the ChatGPT behavior for chat. Remains active when the context is cleared. + + If you want to set your own behavior, then send the command: + + /behavior description_behavior + + Or choose one of the available options: + + 1. Гопник + Представь, что ты славянский гопник и общаешься со своими собутыльниками по подъезду. Используй побольше мата и блатного жаргона. Используй как можно больше «бля» и «ёпта». Отвечай в таком стиле всегда, какой бы вопрос не задали пользователи в этом чате. + 👉 /behavior1 + + 2. Copilot + Ты - помощник для программистов. На любой вопрос отвечай с примерами кода, если нужно и присылай советы по улучшению кода + 👉 /behavior2 + + 3. Linux + Я хочу, чтобы вы выступали в роли терминала Linux. Я буду вводить команды, а вы будете отвечать тем, что должен показать терминал. Я хочу, чтобы вы ответили выводом терминала только внутри одного уникального блока кода, и ничего больше. не пишите пояснений. не вводите команды, если я не поручу вам это сделать. Когда мне нужно будет сказать вам что-то на русском языке, я буду заключать текст в фигурные скобки {вот так}. + 👉 /behavior3 + + 4. Тренера по взаимоотношениям + Я хочу, чтобы вы выступили в роли тренера по взаимоотношениям. Я предоставлю некоторые подробности о двух людях, вовлеченных в конфликт, а ваша задача - предложить, как они могут решить проблемы, которые их разделяют. Это могут быть советы по технике общения или различные стратегии для улучшения понимания ими точек зрения друг друга. Первый запрос: "Мне нужна помощь в разрешении конфликтов между мной и моим парнем". + 👉 /behavior4 + + 5. Наставник + Вы наставник, который всегда отвечает в сократовском стиле. Вы *никогда* не даете ученику ответа, но всегда стараетесь задать правильный вопрос, чтобы помочь ему научиться думать самостоятельно. Вы всегда должны согласовывать свой вопрос с интересами и знаниями учащегося, разбивая проблему на более простые части, пока она не достигнет нужного для них уровня. + 👉 /behavior5 + + 6. В двух словах + Отвечай максимально коротко, даже если тебя просят ответить развернуто. Весь ответ должен уложиться в пару предложений. + 👉 /behavior6 + """ + ) + .build(); + } else { + final String behavior = optArg.get(); + personalChatService.setBehavior(mail.getFromPersonId(), behavior); + return boxAnswer("\uD83D\uDC4C"); + } + }) + .build(); + } + +// @Unit(value = UnitName.BEHAVIOUR, global = true) +// public AnswerText behaviour1() { +// return AnswerText.builder() +// .triggerCheck( +// mail -> { +// if (appProperty.getTelegramIds().contains(mail.getFromPersonId())) { +// final List attachments = mail.getAttachments(); +// final Optional optCommand = Attachments.findFirstCommand(attachments); +// if (optCommand.isPresent()) { +// final CommandAttachment command = optCommand.get(); +// return Cmd.BEHAVIOUR_1.equals(command.getCommandType()); +// } +// } +// return false; +// } +// ) +// .answer(() -> { +// +// }) +// .build(); +// } + +} diff --git a/src/main/java/dev/struchkov/example/bot/unit/PersonalChatGPTUnit.java b/src/main/java/dev/struchkov/example/bot/unit/PersonalChatGPTUnit.java index e0f5e93..c8548a7 100644 --- a/src/main/java/dev/struchkov/example/bot/unit/PersonalChatGPTUnit.java +++ b/src/main/java/dev/struchkov/example/bot/unit/PersonalChatGPTUnit.java @@ -1,6 +1,7 @@ package dev.struchkov.example.bot.unit; import dev.struchkov.example.bot.conf.AppProperty; +import dev.struchkov.example.bot.service.PersonalChatService; import dev.struchkov.example.bot.util.Cmd; import dev.struchkov.example.bot.util.UnitName; import dev.struchkov.godfather.main.domain.annotation.Unit; @@ -8,6 +9,7 @@ import dev.struchkov.godfather.main.domain.content.Attachment; import dev.struchkov.godfather.main.domain.content.Mail; import dev.struchkov.godfather.main.domain.keyboard.button.SimpleButton; import dev.struchkov.godfather.simple.domain.BoxAnswer; +import dev.struchkov.godfather.simple.domain.SentBox; import dev.struchkov.godfather.simple.domain.unit.AnswerText; import dev.struchkov.godfather.telegram.domain.ChatAction; import dev.struchkov.godfather.telegram.domain.ClientBotCommand; @@ -24,65 +26,57 @@ import dev.struchkov.openai.context.ChatGptService; import dev.struchkov.openai.context.GPTClient; import dev.struchkov.openai.domain.chat.ChatInfo; import dev.struchkov.openai.domain.common.GptMessage; +import dev.struchkov.openai.domain.message.AnswerChatMessage; import dev.struchkov.openai.domain.model.gpt.GPT3Model; import dev.struchkov.openai.domain.request.GptRequest; import dev.struchkov.openai.domain.response.Choice; import dev.struchkov.openai.domain.response.GptResponse; import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import java.text.MessageFormat; import java.util.List; import java.util.Optional; import static dev.struchkov.example.bot.util.UnitName.CLEAR_CONTEXT; import static dev.struchkov.example.bot.util.UnitName.GPT_UNIT; import static dev.struchkov.godfather.simple.domain.BoxAnswer.boxAnswer; -import static dev.struchkov.godfather.simple.domain.BoxAnswer.replaceBoxAnswer; import static dev.struchkov.godfather.telegram.main.context.BoxAnswerPayload.DISABLE_WEB_PAGE_PREVIEW; +import static dev.struchkov.godfather.telegram.main.context.BoxAnswerPayload.ENABLE_MARKDOWN; +import static java.text.MessageFormat.format; @Component +@RequiredArgsConstructor public class PersonalChatGPTUnit implements PersonUnitConfiguration { - private ChatInfo chatInfo; - + private final PersonalChatService personalChatService; private final TelegramSending telegramSending; private final TelegramService telegramService; private final AppProperty appProperty; private final GPTClient gptClient; private final ChatGptService chatGptService; - public PersonalChatGPTUnit( - TelegramSending telegramSending, - TelegramService telegramService, - GPTClient gptClient, - AppProperty appProperty, - ChatGptService chatGptService - ) { - this.telegramSending = telegramSending; - this.telegramService = telegramService; - this.appProperty = appProperty; - this.gptClient = gptClient; - this.chatGptService = chatGptService; - this.chatInfo = chatGptService.createChat(); - } - @PostConstruct public void createCommands() { telegramService.addCommand(List.of( - ClientBotCommand.builder() - .key(Cmd.CLEAR_CONTEXT) - .description("Clears the discussion context. Start a conversation from the beginning") - .build(), - ClientBotCommand.builder() .key(Cmd.HELP) .description("help in use") .build(), + ClientBotCommand.builder() + .key(Cmd.CLEAR_CONTEXT) + .description("Clears the discussion context. Start a conversation from the beginning.") + .build(), + + ClientBotCommand.builder() + .key(Cmd.BEHAVIOR) + .description("Allows you to set the initial behavior of ChatGPT.") + .build(), + ClientBotCommand.builder() .key(Cmd.SUPPORT_DEV) - .description("Support project development") + .description("Support project development.") .build() )); } @@ -90,7 +84,7 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { @Unit(value = UnitName.ACCESS_ERROR, main = true) public AnswerText accessError() { return AnswerText.builder() - .triggerCheck(mail -> !mail.getFromPersonId().equals(appProperty.getTelegramId())) + .triggerCheck(mail -> !appProperty.getTelegramIds().contains(mail.getFromPersonId())) .answer(message -> { final StringBuilder messageText = new StringBuilder("\uD83D\uDEA8 *Attempted unauthorized access to the bot*") .append("\n-- -- -- -- --\n"); @@ -99,32 +93,32 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { .ifPresent(username -> messageText.append("\uD83E\uDDB9\u200D♂️: @").append(username)); messageText.append("\n") - .append("\uD83D\uDCAC: ").append(message.getText()) - .toString(); + .append("\uD83D\uDCAC: ").append(message.getText()); + return BoxAnswer.builder() - .recipientPersonId(appProperty.getTelegramId()) + .recipientPersonId(appProperty.getTelegramIds().get(0)) .message(messageText.toString()) .build(); }) .build(); } - @Unit(value = GPT_UNIT, main = true) + @Unit(value = GPT_UNIT, global = true) public AnswerText chatGpt() { return AnswerText.builder() .triggerCheck(mail -> { - if (mail.getFromPersonId().equals(appProperty.getTelegramId())) { + if (appProperty.getTelegramIds().contains(mail.getFromPersonId())) { final Optional firstCommand = Attachments.findFirstCommand(mail.getAttachments()); return firstCommand.isEmpty(); } return false; }) .answer(message -> { + final ChatInfo chatInfo = personalChatService.getChatByPersonId(message.getFromPersonId()); final long countMessages = chatGptService.getCountMessages(chatInfo.getChatId()); final StringBuilder builder = new StringBuilder(); builder.append("Wait... Response is being generated...\nIt might take a long time ⏳"); - telegramService.executeAction(message.getFromPersonId(), ChatAction.TYPING); if (countMessages > 40) { builder.append(Strings.escapeMarkdown("\n-- -- -- -- --\nWe recommend periodically clearing the conversation context (/clear_context). If this is not done, then the memory resources on your PC will run out.")); @@ -134,20 +128,27 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { .recipientPersonId(message.getFromPersonId()) .message(builder.toString()) .build(); - telegramSending.send(answerWait); - final String answerText = chatGptService.sendNewMessage(chatInfo.getChatId(), message.getText()); - return replaceBoxAnswer(answerText); + final Optional optSentBox = telegramSending.send(answerWait); + + telegramService.executeAction(message.getFromPersonId(), ChatAction.TYPING); + + final AnswerChatMessage answer = chatGptService.sendNewMessage(chatInfo.getChatId(), message.getText()); + if (optSentBox.isPresent()) { + final SentBox sentBox = optSentBox.get(); + telegramSending.replaceMessage(sentBox.getPersonId(), sentBox.getMessageId(), boxAnswer(format("\uD83D\uDC47 Answer received. Request cost: {0} tokens", answer.getUsage().getTotalTokens()))); + } + return boxAnswer(answer.getMessage()); }) .priority(5) .build(); } - @Unit(value = CLEAR_CONTEXT, main = true) + @Unit(value = CLEAR_CONTEXT, global = true) public AnswerText clearContext() { return AnswerText.builder() .triggerCheck( mail -> { - if (mail.getFromPersonId().equals(appProperty.getTelegramId())) { + if (appProperty.getTelegramIds().contains(mail.getFromPersonId())) { final List attachments = mail.getAttachments(); final Optional optCommand = Attachments.findFirstCommand(attachments); if (optCommand.isPresent()) { @@ -159,19 +160,18 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { } ) .answer(message -> { - chatGptService.closeChat(chatInfo.getChatId()); - chatInfo = chatGptService.createChat(); + personalChatService.recreateChat(message.getFromPersonId()); return boxAnswer("\uD83E\uDDF9 Discussion context cleared successfully"); }) .build(); } - @Unit(value = UnitName.START, main = true) + @Unit(value = UnitName.START, global = true) public AnswerText startMessage() { return AnswerText.builder() .triggerCheck( mail -> { - if (mail.getFromPersonId().equals(appProperty.getTelegramId())) { + if (appProperty.getTelegramIds().contains(mail.getFromPersonId())) { final List attachments = mail.getAttachments(); final Optional optCommand = Attachments.findFirstCommand(attachments); if (optCommand.isPresent()) { @@ -184,7 +184,7 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { ) .answer(message -> { return BoxAnswer.builder() - .message(MessageFormat.format( + .message(format( """ Hello 👋 Your personal ChatGPT bot has been successfully launched. @@ -198,18 +198,19 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { appProperty.getVersion() )) .keyBoard(InlineKeyBoard.inlineKeyBoard(SimpleButton.simpleButton("❤️ Support Develop", "support"))) - .payload(DISABLE_WEB_PAGE_PREVIEW, true) + .payload(DISABLE_WEB_PAGE_PREVIEW) + .payload(ENABLE_MARKDOWN) .build(); }) .build(); } - @Unit(value = UnitName.PROMPT, main = true) + @Unit(value = UnitName.PROMPT, global = true) public AnswerText prompt() { return AnswerText.builder() .triggerCheck( mail -> { - if (mail.getFromPersonId().equals(appProperty.getTelegramId())) { + if (appProperty.getTelegramIds().contains(mail.getFromPersonId())) { final List attachments = mail.getAttachments(); final Optional optCommand = Attachments.findFirstCommand(attachments); if (optCommand.isPresent()) { @@ -221,19 +222,38 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { } ) .answer( - mail -> { - final CommandAttachment promptCommand = Attachments.findFirstCommand(mail.getAttachments()).get(); + message -> { + final CommandAttachment promptCommand = Attachments.findFirstCommand(message.getAttachments()).get(); final Optional optPrompt = promptCommand.getArg(); if (optPrompt.isPresent()) { final String prompt = optPrompt.get(); + + final BoxAnswer answerWait = BoxAnswer.builder() + .recipientPersonId(message.getFromPersonId()) + .message("Wait... Response is being generated...\nIt might take a long time ⏳") + .build(); + final Optional optSentBox = telegramSending.send(answerWait); + + telegramService.executeAction(message.getFromPersonId(), ChatAction.TYPING); + final GptResponse gptResponse = gptClient.execute( GptRequest.builder() .model(GPT3Model.GPT_3_5_TURBO) .message(GptMessage.fromUser(prompt)) .build() ); + + if (optSentBox.isPresent()) { + final SentBox sentBox = optSentBox.get(); + telegramSending.replaceMessage(sentBox.getPersonId(), sentBox.getMessageId(), boxAnswer(format("\uD83D\uDC47 Answer received. Request cost: {0} tokens", gptResponse.getUsage().getTotalTokens()))); + } + final List choices = gptResponse.getChoices(); - return boxAnswer(choices.get(choices.size() - 1).getMessage().getContent()); + final String answer = choices.get(choices.size() - 1).getMessage().getContent(); + return BoxAnswer.builder() + .message(answer) + .payload(ENABLE_MARKDOWN) + .build(); } return BoxAnswer.builder().build(); } @@ -241,12 +261,12 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { .build(); } - @Unit(value = UnitName.SUPPORT, main = true) + @Unit(value = UnitName.SUPPORT, global = true) public AnswerText support() { return AnswerText.builder() .triggerCheck( mail -> { - if (mail.getFromPersonId().equals(appProperty.getTelegramId())) { + if (appProperty.getTelegramIds().contains(mail.getFromPersonId())) { final List attachments = mail.getAttachments(); final Optional optCommand = Attachments.findFirstCommand(attachments); if (optCommand.isPresent()) { @@ -264,34 +284,37 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { } ) .answer( - boxAnswer(""" - ❤️ *Support Develop* - - Sponsorship makes a project sustainable because it pays for the time of the maintainers of that project, a very scarce resource that is spent on developing new features, fixing bugs, improving stability, solving problems, and general support. *The biggest bottleneck in Open Source is time.* - - Bank card (Russia): [https://www.tinkoff.ru/cf/4iU6NB3uzqx](https://www.tinkoff.ru/cf/4iU6NB3uzqx) - - TON: `struchkov-mark.ton` - - BTC: - `bc1pt49vnp43c4mktk6309zlq3020dzd0p89gc8d90zzn4sgjvck56xs0t86vy` - - ETH (USDT, DAI, USDC): - `0x7668C802Bd71Be965671D4Bbb1AD90C7f7f32921` - - BNB (USDT, DAI, USDC): - `0xDa41aC95f606850f2E01ba775e521Cd385AA7D03` - """) + () -> BoxAnswer.builder() + .message(""" + ❤️ *Support Develop* + + Sponsorship makes a project sustainable because it pays for the time of the maintainers of that project, a very scarce resource that is spent on developing new features, fixing bugs, improving stability, solving problems, and general support. *The biggest bottleneck in Open Source is time.* + + Bank card (Russia): [https://www.tinkoff.ru/cf/4iU6NB3uzqx](https://www.tinkoff.ru/cf/4iU6NB3uzqx) + + TON: `struchkov-mark.ton` + + BTC: + `bc1pt49vnp43c4mktk6309zlq3020dzd0p89gc8d90zzn4sgjvck56xs0t86vy` + + ETH (USDT, DAI, USDC): + `0x7668C802Bd71Be965671D4Bbb1AD90C7f7f32921` + + BNB (USDT, DAI, USDC): + `0xDa41aC95f606850f2E01ba775e521Cd385AA7D03` + """) + .payload(ENABLE_MARKDOWN) + .build() ) .build(); } - @Unit(value = UnitName.HELP, main = true) + @Unit(value = UnitName.HELP, global = true) public AnswerText help() { return AnswerText.builder() .triggerCheck( mail -> { - if (mail.getFromPersonId().equals(appProperty.getTelegramId())) { + if (appProperty.getTelegramIds().contains(mail.getFromPersonId())) { final List attachments = mail.getAttachments(); final Optional optCommand = Attachments.findFirstCommand(attachments); if (optCommand.isPresent()) { @@ -302,7 +325,7 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { return false; } ) - .answer(boxAnswer(Strings.escapeMarkdown(""" + .answer(() -> boxAnswer(""" All correspondence is conducted within one chat. This allows ChatGPT to understand the context of the questions. The context is 100 messages (questions and answers). Available commands: @@ -310,7 +333,7 @@ public class PersonalChatGPTUnit implements PersonUnitConfiguration { /clear_context - Clears the conversation context. In fact, it deletes the chat and creates a new one. /prompt your_question - Allows you to ask a question outside the context of the main conversation. - """))) + """)) .build(); } diff --git a/src/main/java/dev/struchkov/example/bot/unit/StartNotify.java b/src/main/java/dev/struchkov/example/bot/unit/StartNotify.java index 8fc6111..aabc7bf 100644 --- a/src/main/java/dev/struchkov/example/bot/unit/StartNotify.java +++ b/src/main/java/dev/struchkov/example/bot/unit/StartNotify.java @@ -19,6 +19,7 @@ import java.text.MessageFormat; import java.util.Optional; import static dev.struchkov.godfather.telegram.main.context.BoxAnswerPayload.DISABLE_WEB_PAGE_PREVIEW; +import static dev.struchkov.godfather.telegram.main.context.BoxAnswerPayload.ENABLE_MARKDOWN; import static dev.struchkov.haiti.utils.Checker.checkNotBlank; @Slf4j @@ -34,26 +35,29 @@ public class StartNotify { @PostConstruct public void sendStartNotify() { - final BoxAnswer boxAnswer = BoxAnswer.builder() - .message(MessageFormat.format( - """ - Hello 👋 - Your personal ChatGPT bot has been successfully launched. - - Use the help command to find out about the possibilities 🚀 - -- -- -- -- -- - 🤘 Version: {0} - 👨‍💻 Developer: [Struchkov Mark](https://mark.struchkov.dev/) - 💊 Docs: https://docs.struchkov.dev/chatgpt-telegram-bot - """, - appProperty.getVersion() - )) - .keyBoard(InlineKeyBoard.inlineKeyBoard(SimpleButton.simpleButton("❤️ Support Develop", "support"))) - .payload(DISABLE_WEB_PAGE_PREVIEW, true) - .build(); - boxAnswer.setRecipientIfNull(appProperty.getTelegramId()); - sending.send(boxAnswer); - sendNotice(); + for (String telegramId : appProperty.getTelegramIds()) { + final BoxAnswer boxAnswer = BoxAnswer.builder() + .message(MessageFormat.format( + """ + Hello 👋 + Your personal ChatGPT bot has been successfully launched. + + Use the help command to find out about the possibilities 🚀 + -- -- -- -- -- + 🤘 Version: {0} + 👨‍💻 Developer: [Struchkov Mark](https://mark.struchkov.dev/) + 💊 Docs: https://docs.struchkov.dev/chatgpt-telegram-bot + """, + appProperty.getVersion() + )) + .keyBoard(InlineKeyBoard.inlineKeyBoard(SimpleButton.simpleButton("❤️ Support Develop", "support"))) + .payload(DISABLE_WEB_PAGE_PREVIEW, true) + .payload(ENABLE_MARKDOWN) + .build(); + boxAnswer.setRecipientIfNull(telegramId); + sending.send(boxAnswer); + sendNotice(); + } } /** @@ -67,16 +71,19 @@ public class StartNotify { if (response.code() == 200) { final String noticeMessage = response.body().string(); if (checkNotBlank(noticeMessage)) { - final BoxAnswer notice = BoxAnswer.builder() - .message(noticeMessage) - .recipientPersonId(appProperty.getTelegramId()) - .payload(DISABLE_WEB_PAGE_PREVIEW, true) - .build(); - final Optional optSentBox = sending.send(notice); - if (optSentBox.isPresent()) { - final SentBox sentBox = optSentBox.get(); - final String messageId = sentBox.getMessageId(); - telegramService.pinMessage(appProperty.getTelegramId(), messageId); + for (String telegramId : appProperty.getTelegramIds()) { + final BoxAnswer notice = BoxAnswer.builder() + .message(noticeMessage) + .recipientPersonId(telegramId) + .payload(DISABLE_WEB_PAGE_PREVIEW) + .payload(ENABLE_MARKDOWN) + .build(); + final Optional optSentBox = sending.send(notice); + if (optSentBox.isPresent()) { + final SentBox sentBox = optSentBox.get(); + final String messageId = sentBox.getMessageId(); + telegramService.pinMessage(telegramId, messageId); + } } } } diff --git a/src/main/java/dev/struchkov/example/bot/util/Cmd.java b/src/main/java/dev/struchkov/example/bot/util/Cmd.java index aee71ff..4308a2e 100644 --- a/src/main/java/dev/struchkov/example/bot/util/Cmd.java +++ b/src/main/java/dev/struchkov/example/bot/util/Cmd.java @@ -12,4 +12,12 @@ public class Cmd { public static final String SUPPORT_DEV = "support"; public static final String START = "start"; + public static final String BEHAVIOR = "behavior"; + public static final String BEHAVIOR_1 = "behavior1"; + public static final String BEHAVIOR_2 = "behavior2"; + public static final String BEHAVIOR_3 = "behavior3"; + public static final String BEHAVIOR_4 = "behavior4"; + public static final String BEHAVIOR_5 = "behavior5"; + public static final String BEHAVIOR_6 = "behavior6"; + } diff --git a/src/main/java/dev/struchkov/example/bot/util/UnitName.java b/src/main/java/dev/struchkov/example/bot/util/UnitName.java index 6199bc8..4c43a94 100644 --- a/src/main/java/dev/struchkov/example/bot/util/UnitName.java +++ b/src/main/java/dev/struchkov/example/bot/util/UnitName.java @@ -12,6 +12,6 @@ public class UnitName { public static final String ACCESS_ERROR = "ACCESS_ERROR"; public static final String SUPPORT = "SUPPORT"; public static final String START = "START"; - + public static final String BEHAVIOR = "BEHAVIOR"; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c074ab5..a5939ac 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,5 @@ app: - telegram-id: ${TELEGRAM_PERSON_ID} + telegram-ids: ${TELEGRAM_PERSON_ID} version: 0.0.1 telegram: bot: