From dbbcc0c2865293629ddd2e47c0fbb3ad871d72b6 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Tue, 27 Aug 2024 22:00:31 +0530 Subject: [PATCH] feat: qr command --- QuickRepliesApp.ts | 4 + app.json | 104 ++++++++++---------- i18n/en.json | 3 + src/commands/QrCommand.ts | 108 +++++++++++++++++++++ src/handlers/AIHandler.ts | 112 ++++++++++++++-------- src/handlers/ExecuteBlockActionHandler.ts | 40 ++++---- src/helper/message.ts | 2 +- src/storage/userPreferenceStorage.ts | 2 +- 8 files changed, 263 insertions(+), 112 deletions(-) create mode 100644 src/commands/QrCommand.ts diff --git a/QuickRepliesApp.ts b/QuickRepliesApp.ts index 48a0d53..3ae7b77 100644 --- a/QuickRepliesApp.ts +++ b/QuickRepliesApp.ts @@ -32,6 +32,7 @@ import { import { ActionButton } from './src/enum/modals/common/ActionButtons'; import { ExecuteActionButtonHandler } from './src/handlers/ExecuteActionButtonHandler'; import { settings } from './src/config/settings'; +import { QrCommand } from './src/commands/QrCommand'; export class QuickRepliesApp extends App { private elementBuilder: ElementBuilder; @@ -49,6 +50,9 @@ export class QuickRepliesApp extends App { await configuration.slashCommands.provideSlashCommand( new QsCommand(this), ); + await configuration.slashCommands.provideSlashCommand( + new QrCommand(this), + ); this.elementBuilder = new ElementBuilder(this.getID()); this.blockBuilder = new BlockBuilder(this.getID()); diff --git a/app.json b/app.json index ac25ae3..5e1fa09 100644 --- a/app.json +++ b/app.json @@ -1,54 +1,54 @@ { - "id": "e664d2cb-7beb-413a-837a-80fd840c387b", - "version": "0.0.1", - "requiredApiVersion": "^1.44.0", - "iconFile": "icon.png", - "author": { - "name": "Vipin Chaudhary", - "homepage": "https://github.com/RocketChat/Apps.QuickReplies", - "support": "https://github.com/RocketChat/Apps.QuickReplies/issues" - }, - "name": "QuickReplies", - "nameSlug": "quickreplies", - "classFile": "QuickRepliesApp.ts", - "description": "Instantly craft and send customizable responses within Rocket.Chat.", - "implements": [], - "permissions": [ - { - "name": "ui.registerButtons" - }, - { - "name": "api" - }, - { - "name": "slashcommand" - }, - { - "name": "server-setting.read" - }, - { - "name": "room.read" - }, - { - "name": "persistence" - }, - { - "name": "ui.interact" - }, - { - "name": "networking" - }, - { - "name": "message.write" - }, - { - "name": "user.read" - }, - { - "name": "room.write" - }, - { - "name": "message.read" - } - ] + "id": "e664d2cb-7beb-413a-837a-80fd840c387b", + "version": "0.0.1", + "requiredApiVersion": "^1.44.0", + "iconFile": "icon.png", + "author": { + "name": "Vipin Chaudhary", + "homepage": "https://github.com/RocketChat/Apps.QuickReplies", + "support": "https://github.com/RocketChat/Apps.QuickReplies/issues" + }, + "name": "QuickReplies", + "nameSlug": "quickreplies", + "classFile": "QuickRepliesApp.ts", + "description": "Instantly craft and send customizable responses within Rocket.Chat.", + "implements": [], + "permissions": [ + { + "name": "ui.registerButtons" + }, + { + "name": "api" + }, + { + "name": "slashcommand" + }, + { + "name": "server-setting.read" + }, + { + "name": "room.read" + }, + { + "name": "persistence" + }, + { + "name": "ui.interact" + }, + { + "name": "networking" + }, + { + "name": "message.write" + }, + { + "name": "user.read" + }, + { + "name": "room.write" + }, + { + "name": "message.read" + } + ] } \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index 1028bce..17f5b61 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -17,6 +17,9 @@ "Quick_Search_Command_Params": "", "Quick_Search_Command_Description": "Search Quick Replies to send. Type /qs .", "Quick_Search_Command_Preview_Title": "Quick Search Command Preview", + "Quick_Response_Command_Parameters": "qr params", + "Quick_Response_Command_Description": "Generate a response for last message using AI.", + "Quick_Response_Command_Preview_Title": "Quick Response Command Preview", "Create_Quick_Reply_Action_Button_Label": "Create a new Quick Reply", "List_Quick_Reply_Action_Button_Label": "List all Quick Reply", "Reply_Using_AI_Label": "🤖 Reply using AI", diff --git a/src/commands/QrCommand.ts b/src/commands/QrCommand.ts new file mode 100644 index 0000000..81cfb1b --- /dev/null +++ b/src/commands/QrCommand.ts @@ -0,0 +1,108 @@ +import { + ISlashCommand, + ISlashCommandPreview, + ISlashCommandPreviewItem, + SlashCommandContext, + SlashCommandPreviewItemType, +} from '@rocket.chat/apps-engine/definition/slashcommands'; +import { QuickRepliesApp } from '../../QuickRepliesApp'; +import { + IRead, + IModify, + IHttp, + IPersistence, +} from '@rocket.chat/apps-engine/definition/accessors'; +import { getSortedMessages, sendMessage } from '../helper/message'; +import AIHandler from '../handlers/AIHandler'; +import { UserPreferenceStorage } from '../storage/userPreferenceStorage'; +import { sendNotification } from '../helper/notification'; + +export class QrCommand implements ISlashCommand { + constructor(private readonly app: QuickRepliesApp) {} + + public command = 'qr'; + public i18nDescription = 'Quick_Response_Command_Description'; + public providesPreview = true; + public i18nParamsExample = 'Quick_Response_Command_Params'; + + public async executor( + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence, + ): Promise { + // Placeholder for command execution logic + } + + public async previewer( + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence, + ): Promise { + const sender = context.getSender(); + const room = context.getRoom(); + const prevMessages = await getSortedMessages(room.id, read); + const lastMessage = prevMessages[0]; + + const userPreference = new UserPreferenceStorage( + persis, + read.getPersistenceReader(), + sender.id, + ); + + const Preference = await userPreference.getUserPreference(); + const AiHandler = new AIHandler(this.app, http, Preference); + + let items = [] as ISlashCommandPreviewItem[]; + if (lastMessage.text && lastMessage.sender._id != sender.id) { + const data = await AiHandler.handleResponse( + lastMessage?.text, + '', + true, + ); + + if (data.success) { + const str = data.response; + const arr = JSON.parse(str); + items = arr.map((message, index) => ({ + id: (index + 1).toString(), + type: SlashCommandPreviewItemType.TEXT, + value: message, + })) as ISlashCommandPreviewItem[]; + + return { + i18nTitle: 'Quick_Response_Command_Preview_Title', + items, + }; + } else { + await sendNotification(read, modify, sender, room, { + message: data.response, + }); + items = []; + return { + i18nTitle: 'Quick_Response_Command_Preview_Title', + items, + }; + } + } else { + items = []; + return { i18nTitle: 'Quick_Response_Command_Preview_Title', items }; + } + } + + public async executePreviewItem( + item: ISlashCommandPreviewItem, + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence, + ): Promise { + const room = context.getRoom(); + const message = item.value; + await sendMessage(modify, context.getSender(), room, message); + } +} diff --git a/src/handlers/AIHandler.ts b/src/handlers/AIHandler.ts index e4ddf52..7de9c60 100644 --- a/src/handlers/AIHandler.ts +++ b/src/handlers/AIHandler.ts @@ -2,7 +2,6 @@ import { IHttp, IHttpResponse, } from '@rocket.chat/apps-engine/definition/accessors'; -import { IUser } from '@rocket.chat/apps-engine/definition/users'; import { QuickRepliesApp } from '../../QuickRepliesApp'; import { SettingEnum } from '../config/settings'; import { @@ -21,11 +20,12 @@ class AIHandler { private language = this.userPreference.language; public async handleResponse( - user: IUser, message: string, prompt: string, - ): Promise { + Multiple?: boolean, + ): Promise<{ response: string; success: boolean }> { let aiProvider: string; + if ( this.userPreference.AIusagePreference === AIusagePreferenceEnum.Personal @@ -38,18 +38,21 @@ class AIHandler { .getValueById(SettingEnum.AI_PROVIDER_OPTOIN_ID); } + const content = Multiple + ? this.getMultiReponsePrompt(message) + : this.getSingleResponsePrompt(message, prompt); switch (aiProvider) { case AIProviderEnum.SelfHosted: case SettingEnum.SELF_HOSTED_MODEL: - return this.handleSelfHostedModel(user, message, prompt); + return this.handleSelfHostedModel(content); case AIProviderEnum.OpenAI: case SettingEnum.OPEN_AI: - return this.handleOpenAI(user, message, prompt); + return this.handleOpenAI(content); case AIProviderEnum.Gemini: case SettingEnum.GEMINI: - return this.handleGemini(user, message, prompt); + return this.handleGemini(content); default: const errorMsg = @@ -59,19 +62,20 @@ class AIHandler { : t('AI_Not_Configured_Admin', this.language); this.app.getLogger().log(errorMsg); - return errorMsg; + return { response: errorMsg, success: false }; } } - private getPrompt(message: string, prompt: string): string { - return `Write a reply to this message: "${message}". ${this.userPreference.AIconfiguration.AIPrompt} and Use the following as a prompt or response reply: "${prompt}" and make sure you respond with just the reply without quotes.`; + private getSingleResponsePrompt(message: string, prompt: string): string { + return `You are a function that generates responses from a message and prompt message is : "${message}". ${this.userPreference.AIconfiguration.AIPrompt} and Use the following as a prompt : "${prompt}" and make sure you respond with just the reply without quotes.`; + } + private getMultiReponsePrompt(message: string): string { + return `You are a function that generates responses from a message. Give me an array of strings as responses to this message: ${message}. Example: ["hey", "how are you doing", "can I help you"]. Respond exactly like this; don't do anything else. No variable naming or code quotes—just an array of strings for the responses.`; } private async handleSelfHostedModel( - user: IUser, - message: string, prompt: string, - ): Promise { + ): Promise<{ response: string; success: boolean }> { try { const url = await this.getSelfHostedModelUrl(); @@ -81,15 +85,21 @@ class AIHandler { this.userPreference.AIusagePreference === AIusagePreferenceEnum.Personal ) { - return t( - 'AI_Self_Hosted_Model_Not_Configured', - this.language, - ); + return { + response: t( + 'AI_Self_Hosted_Model_Not_Configured', + this.language, + ), + success: false, + }; } else { - return t( - 'AI_Workspace_Model_Not_Configured', - this.language, - ); + return { + response: t( + 'AI_Workspace_Model_Not_Configured', + this.language, + ), + success: false, + }; } } @@ -97,7 +107,7 @@ class AIHandler { messages: [ { role: 'system', - content: this.getPrompt(message, prompt), + content: prompt, }, ], temperature: 0, @@ -115,15 +125,24 @@ class AIHandler { if (!response || !response.data) { this.app.getLogger().log('No response data received from AI.'); - return t('AI_Something_Went_Wrong', this.language); + return { + response: t('AI_Something_Went_Wrong', this.language), + success: false, + }; } - - return response.data.choices[0].message.content; + this.app.getLogger().log(response.data.choices[0].message.content); + return { + response: response.data.choices[0].message.content, + success: true, + }; } catch (error) { this.app .getLogger() .log(`Error in handleSelfHostedModel: ${error.message}`); - return t('AI_Something_Went_Wrong', this.language); + return { + response: t('AI_Something_Went_Wrong', this.language), + success: false, + }; } } @@ -142,10 +161,8 @@ class AIHandler { } private async handleOpenAI( - user: IUser, - message: string, prompt: string, - ): Promise { + ): Promise<{ response: string; success: boolean }> { try { const { openaikey, openaimodel } = await this.getOpenAIConfig(); @@ -157,7 +174,7 @@ class AIHandler { ? t('AI_OpenAI_Model_Not_Configured', this.language) : t('AI_Not_Configured_Admin', this.language); - return errorMsg; + return { response: errorMsg, success: false }; } const response: IHttpResponse = await this.http.post( @@ -172,7 +189,7 @@ class AIHandler { messages: [ { role: 'system', - content: this.getPrompt(message, prompt), + content: prompt, }, ], }), @@ -181,14 +198,20 @@ class AIHandler { if (!response || !response.data) { this.app.getLogger().log('No response data received from AI.'); - return t('AI_Something_Went_Wrong', this.language); + return { + response: t('AI_Something_Went_Wrong', this.language), + success: false, + }; } const { choices } = response.data; - return choices[0].message.content; + return { response: choices[0].message.content, success: true }; } catch (error) { this.app.getLogger().log(`Error in handleOpenAI: ${error.message}`); - return t('AI_Something_Went_Wrong', this.language); + return { + response: t('AI_Something_Went_Wrong', this.language), + success: false, + }; } } @@ -219,10 +242,8 @@ class AIHandler { } } private async handleGemini( - user: IUser, - message: string, prompt: string, - ): Promise { + ): Promise<{ response: string; success: boolean }> { try { const geminiAPIkey = await this.getGeminiAPIKey(); @@ -235,7 +256,7 @@ class AIHandler { ? t('AI_Gemini_Model_Not_Configured', this.language) : t('AI_Not_Configured_Admin', this.language); - return errorMsg; + return { response: errorMsg, success: false }; } const response: IHttpResponse = await this.http.post( @@ -248,7 +269,7 @@ class AIHandler { contents: [ { parts: { - text: this.getPrompt(message, prompt), + text: prompt, }, }, ], @@ -260,14 +281,23 @@ class AIHandler { this.app .getLogger() .log('No response content received from AI.'); - return t('AI_Something_Went_Wrong', this.language); + return { + response: t('AI_Something_Went_Wrong', this.language), + success: false, + }; } const data = response.data; - return data.candidates[0].content.parts[0].text; + return { + response: data.candidates[0].content.parts[0].text, + success: true, + }; } catch (error) { this.app.getLogger().log(`Error in handleGemini: ${error.message}`); - return t('AI_Something_Went_Wrong', this.language); + return { + response: t('AI_Something_Went_Wrong', this.language), + success: false, + }; } } diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index 5912997..5d51fc6 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -253,29 +253,35 @@ export class ExecuteBlockActionHandler { const Preference = await userPreference.getUserPreference(); - const response = await new AIHandler( + const data = await new AIHandler( this.app, this.http, Preference, - ).handleResponse(user, message, prompt); + ).handleResponse(message, prompt); + if (data.success) { + await aiStorage.updateResponse(data.response); - await aiStorage.updateResponse(response); + const updatedModal = await ReplyAIModal( + this.app, + user, + this.read, + this.persistence, + this.modify, + room, + language, + message, + data.response, + ); - const updatedModal = await ReplyAIModal( - this.app, - user, - this.read, - this.persistence, - this.modify, - room, - language, - message, - response, - ); + return this.context + .getInteractionResponder() + .updateModalViewResponse(updatedModal); + } else { + return this.context + .getInteractionResponder() + .errorResponse(); + } - return this.context - .getInteractionResponder() - .updateModalViewResponse(updatedModal); case UserPreferenceModalEnum.AI_PREFERENCE_DROPDOWN_ACTION_ID: if (value === AIusagePreferenceEnum.Personal) { existingPreference.AIusagePreference = diff --git a/src/helper/message.ts b/src/helper/message.ts index ad5096a..d5cf78e 100644 --- a/src/helper/message.ts +++ b/src/helper/message.ts @@ -147,7 +147,7 @@ async function buildPrivateMessageReplacements( }; } -async function getSortedMessages(roomId: string, read: IRead) { +export async function getSortedMessages(roomId: string, read: IRead) { const prevMessages: IMessageRaw[] = await read .getRoomReader() .getMessages(roomId, {}); diff --git a/src/storage/userPreferenceStorage.ts b/src/storage/userPreferenceStorage.ts index 2491cc4..f38d2be 100644 --- a/src/storage/userPreferenceStorage.ts +++ b/src/storage/userPreferenceStorage.ts @@ -89,7 +89,7 @@ export class UserPreferenceStorage implements IuserPreferenceStorage { language: Language.en, AIusagePreference: AIusagePreferenceEnum.Workspace, AIconfiguration: { - AIPrompt: `Keep the comprehensive clear and concise reply, and ensure it's well-articulated and helpfull`, + AIPrompt: `Keep it clear and concise`, AIProvider: AIProviderEnum.SelfHosted, gemini: { apiKey: '',