From 3d1660deaba69dd617e8d0e866cd78ed314ae660 Mon Sep 17 00:00:00 2001 From: tessmania90 Date: Thu, 6 Apr 2023 06:11:27 +0200 Subject: [PATCH] Changes --- Dockerfile | 1 - src/botservice.js | 96 +++++++++++++++++++++++++++++++++ src/mm-client.js | 31 +++++++++++ src/openai-thread-completion.js | 24 +++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/botservice.js create mode 100644 src/mm-client.js create mode 100644 src/openai-thread-completion.js diff --git a/Dockerfile b/Dockerfile index c0c1e24..dad3402 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,5 @@ USER node COPY --from=npm_builder [ "/app/node_modules/", "./node_modules/" ] COPY --from=npm_builder [ "/app/src/", "./src/" ] -COPY [ "./license.md", "./" ] ENTRYPOINT [ "node", "src/botservice.js" ] \ No newline at end of file diff --git a/src/botservice.js b/src/botservice.js new file mode 100644 index 0000000..44b0da9 --- /dev/null +++ b/src/botservice.js @@ -0,0 +1,96 @@ +const continueThread = require('./openai-thread-completion').continueThread +const { Log } = require('debug-level') + +require('babel-polyfill'); +require('isomorphic-fetch'); +const { mmClient, wsClient } = require('./mm-client') + +// the mattermost library uses FormData - so here is a polyfill +if (!global.FormData) { + global.FormData = require('form-data'); +} + +Log.options({ json: true, colors: true }) +Log.wrapConsole('bot-ws', { level4log: 'INFO' }) +const log = new Log('bot') + +let meId = null; +mmClient.getMe().then(me => meId = me.id) + +const name = process.env['MATTERMOST_BOTNAME'] || '@dall-e' + +wsClient.addMessageListener(async function (event) { + if (['posted'].includes(event.event) && meId) { + const post = JSON.parse(event.data.post); + if (post.root_id === "" && (!event.data.mentions || (!JSON.parse(event.data.mentions).includes(meId)))) { + // we're not in a thread and we are not mentioned - ignore the message + } else { + if (post.user_id !== meId) { + const chatmessages = [ + { + "role": "system", + "content": `You are a helpful assistant named ${name} who provides succinct answers in Markdown format.` + }, + ] + + const thread = await mmClient.getPostThread(post.id, true, false, true) + + const posts = [...new Set(thread.order)].map(id => thread.posts[id]) + .filter(a => a.create_at > Date.now() - 1000 * 60 * 60 * 24 * 1) + .sort((a, b) => a.create_at - b.create_at) + + let assistantCount = 0; + posts.forEach(threadPost => { + log.trace({msg: threadPost}) + if (threadPost.user_id === meId) { + chatmessages.push({role: "assistant", content: threadPost.props.originalMessage ?? threadPost.message}) + assistantCount++ + } else { + if (threadPost.message.includes(name)){ + assistantCount++; + } + chatmessages.push({role: "user", content: threadPost.message}) + } + }) + + // see if we are actually part of the conversation - + // ignore conversations where we were never mentioned or participated. + if (assistantCount > 0){ + wsClient.userTyping(post.channel_id, post.id ?? "") + log.trace({chatmessages}) + const answer = await continueThread(chatmessages) + const fileData = new FormData(); + fileData.append('files', answer, 'dall-e-image.png'); + const uploadResponse = await mmClient.uploadFile(fileData, post.channel_id); + const fileId = JSON.parse(uploadResponse).file_infos[0].id; + log.trace({answer}) + wsClient.userTyping(post.channel_id, post.id ?? "") + const message = `![alt text](${mmClient.getFileUrl(fileId)})`; + const props = { + attachments: [ + { + text: '', + title: '', + image_url: mmClient.getFileUrl(fileId), + fallback: message + } + ] + }; +const newPost = await mmClient.createPost({ + message: message, + channel_id: post.channel_id, + props, + root_id: post.root_id || post.id, + file_ids: fileId ? [fileId] : undefined +}); + log.trace({msg: newPost}) + } + } + } + } else { + log.debug({msg: event}) + } +}); + + + diff --git a/src/mm-client.js b/src/mm-client.js new file mode 100644 index 0000000..abb3820 --- /dev/null +++ b/src/mm-client.js @@ -0,0 +1,31 @@ +const Client4 = require('@mattermost/client').Client4 +const WebSocketClient = require('@mattermost/client').WebSocketClient +const { Log } = require('debug-level') +const log = new Log('bot') + +if (!global.WebSocket) { + global.WebSocket = require('ws'); +} + +const mattermostToken = process.env['MATTERMOST_TOKEN'] +const matterMostURLString = process.env['MATTERMOST_URL'] + +const client = new Client4() +client.setUrl(matterMostURLString) +client.setToken(mattermostToken) + +const wsClient = new WebSocketClient(); +let matterMostURL = new URL(matterMostURLString); +const wsUrl = `${matterMostURL.protocol === 'https:' ? 'wss' : 'ws'}://${matterMostURL.host}/api/v4/websocket` + +new Promise((resolve, reject) => { + wsClient.addCloseListener(connectFailCount => reject()) + wsClient.addErrorListener(event => { reject(event) }) +}).then(() => process.exit(0)).catch(reason => { log.error(reason); process.exit(-1)}) + +wsClient.initialize(wsUrl, mattermostToken) + +module.exports = { + mmClient: client, + wsClient +} diff --git a/src/openai-thread-completion.js b/src/openai-thread-completion.js new file mode 100644 index 0000000..d6e8140 --- /dev/null +++ b/src/openai-thread-completion.js @@ -0,0 +1,24 @@ +const { Configuration, OpenAIApi } = require("openai"); +const configuration = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}); +const openai = new OpenAIApi(configuration); + +// Function to generate an image using Dall-E from OpenAI + +async function continueThread(messages) { +const promptText = messages.map(msg => msg.content).join(' '); // assume that the content key exist in the messages object +const response = await openai.createImage({ + prompt: promptText, + n: 1, + size: "512x512", + response_format: "url" + }); + + const imageUrl = await response.data.url;// oder response.content.url() + const imageResponse = await fetch(imageUrl); + const imageBuffer = await imageResponse.buffer(); + + return imageBuffer; +} +module.exports = { continueThread }