diff --git a/src/botservice.js b/src/botservice.js new file mode 100644 index 0000000..1672ea2 --- /dev/null +++ b/src/botservice.js @@ -0,0 +1,83 @@ +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.` + }, + ] + + let appendDiagramInstructions = false + + 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) + log.trace({answer}) + wsClient.userTyping(post.channel_id, post.id ?? "") + 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..98b4b30 --- /dev/null +++ b/src/openai-thread-completion.js @@ -0,0 +1,21 @@ +const { Configuration, OpenAIApi } = require("openai"); +const configuration = new Configuration({ + apiKey: process.env["OPENAI_API_KEY"] +}); +const openai = new OpenAIApi(configuration); + +const model = process.env["OPENAI_MODEL_NAME"] ?? 'image-alpha-001' +const max_tokens = Number(process.env["OPENAI_MAX_TOKENS"] ?? 2000) + +async function continueThread(messages) { + const response = await openai.generateImage({ + prompt: messages, + model, + n: 1, + size: "512x512", + response_format: "b64_json", + }); + return response.json?.data?.choices?.[0]?.message?.content +} + +module.exports = { continueThread }