diff --git a/index.js b/index.js index acb13a6..a87aff4 100644 --- a/index.js +++ b/index.js @@ -3,8 +3,6 @@ */ 'use strict'; -/** @typedef {import('./src/Processor').ProcessorOptions} ProcessorOptions */ - const Processor = require('./src/Processor'); const Router = require('./src/Router'); const Request = require('./src/Request'); diff --git a/jsconfig.json b/jsconfig.json index f276b4f..2bd29c8 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"], + "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string", "dom"], "target": "ESNext", "allowSyntheticDefaultImports": true, "checkJs": true, @@ -14,4 +14,4 @@ "doc", "docs" ] -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index fc044ec..531f042 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "webalize": "^0.1.0" }, "devDependencies": { + "@types/mocha": "^10.0.10", "cpy-cli": "^5.0.0", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", @@ -1027,6 +1028,13 @@ "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", "dev": true }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -8076,6 +8084,12 @@ "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", "dev": true }, + "@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, "@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", diff --git a/package.json b/package.json index b852170..c10f143 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wingbot", - "version": "3.69.8", + "version": "3.70.0-alpha.2", "description": "Enterprise Messaging Bot Conversation Engine", "main": "index.js", "type": "commonjs", @@ -37,6 +37,7 @@ }, "homepage": "https://github.com/wingbot.ai/wingbot#readme", "devDependencies": { + "@types/mocha": "^10.0.10", "cpy-cli": "^5.0.0", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", @@ -69,4 +70,4 @@ "axios": "^1.6.4", "handlebars": "^4.0.0" } -} \ No newline at end of file +} diff --git a/src/LLM.js b/src/LLM.js index 16dee77..0cfa2a6 100644 --- a/src/LLM.js +++ b/src/LLM.js @@ -8,7 +8,7 @@ const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps'); /** @typedef {import('./Responder')} Responder */ /** @typedef {import('./Responder').Persona} Persona */ /** @typedef {import('./Router').BaseConfiguration} BaseConfiguration */ -/** @typedef {import('./LLMSession').LLMMessage} LLMMessage */ +/** @typedef {import('./LLMSession').LLMMessage} LLMMessage */ /** @typedef {import('./LLMSession').LLMRole} LLMRole */ /** @typedef {import('./LLMSession')} LLMSession */ /** @typedef {import('./transcript/transcriptFromHistory').Transcript} Transcript */ diff --git a/src/LLMMockProvider.js b/src/LLMMockProvider.js index 1ef008a..acf906c 100644 --- a/src/LLMMockProvider.js +++ b/src/LLMMockProvider.js @@ -38,20 +38,27 @@ class LLMMockProvider { if (prompt.length === 0) { throw new Error('Empty prompt'); } - const stats = prompt.reduce((o, m) => Object.assign(o, { - [m.role]: (o[m.role] || 0) + 1 - }), { system: 0, assistant: 0, user: 0 }); - - const statsText = JSON.stringify(stats) - .replace(/"/g, ''); - - const message = this._sequence[this._index]; - this._index = (this._index + 1) % this._sequence.length; + // const stats = prompt.reduce((o, m) => Object.assign(o, { + // [m.role]: (o[m.role] || 0) + 1 + // }), { system: 0, assistant: 0, user: 0 }); + // + // const statsText = JSON.stringify(stats) + // .replace(/"/g, ''); + // + /// / const message = this._sequence[this._index]; + /// / this._index = (this._index + 1) % this._sequence.length; + // + // return { + // role: LLM.ROLE_ASSISTANT, + // finishReason: 'length', + // eslint-disable-next-line max-len + // content: `${statsText} > ${LLMMockProvider.DEFAULT_MODEL}: ${prompt.map((m) => m.content).join(' ')}` + // }; return { role: LLM.ROLE_ASSISTANT, finishReason: 'length', - content: `${statsText} > ${LLMMockProvider.DEFAULT_MODEL}: ${message}` + content: `${options.model || LLMMockProvider.DEFAULT_MODEL}:${prompt.map((m) => m.content).join(' ')}` }; } diff --git a/src/LLMSession.js b/src/LLMSession.js index 5508734..4706257 100644 --- a/src/LLMSession.js +++ b/src/LLMSession.js @@ -35,7 +35,7 @@ class LLMSession { /** * * @param {LLM} llm - * @param {LLMMessage[]} [chat] + * @param {LLMMessage[]} [chat] * @param {SendCallback} [onSend] */ constructor (llm, chat = [], onSend = () => {}) { @@ -43,7 +43,7 @@ class LLMSession { this._onSend = onSend; - /** @type {LLMMessage[]} */ + /** @type {LLMMessage[]} */ this._chat = chat; this._generatedIndex = null; @@ -63,6 +63,7 @@ class LLMSession { } _mergeSystem () { + /** @type {LLMMessage[]} */ const sysMessages = []; const otherMessages = this._chat.filter((message) => { @@ -81,7 +82,7 @@ class LLMSession { const content = sysMessages.reduce((reduced, current, i) => { if (i === 0) { - return current.content; + return current.content || ''; } if (!reduced.match(promptRegex)) { return `${reduced}\n\n${current.content}`; @@ -189,7 +190,7 @@ class LLMSession { /** * * @param {LLMProviderOptions} [options={}] - * @returns {Promise} + * @returns {Promise>} */ async generate (options = {}) { const result = await this._llm.generate(this, options); diff --git a/src/Responder.js b/src/Responder.js index 118a529..df10707 100644 --- a/src/Responder.js +++ b/src/Responder.js @@ -248,6 +248,7 @@ class Responder { this._llmContext.set(contextType, []); } this._llmContext.get(contextType).push(systemPrompt.trim()); + return this; } diff --git a/src/resolvers/contextMessage.js b/src/resolvers/contextMessage.js new file mode 100644 index 0000000..1ede5fa --- /dev/null +++ b/src/resolvers/contextMessage.js @@ -0,0 +1,38 @@ +/* + * @author Vojtech Jedlicka + */ +'use strict'; + +const Router = require('../Router'); +// eslint-disable-next-line no-unused-vars +const Responder = require('../Responder'); +const { getLanguageText } = require('./utils'); +const compileWithState = require('../utils/compileWithState'); + +/** @typedef {import('../BuildRouter').BotContext} BotContext */ +/** @typedef {import('../Router').Resolver} Resolver */ +/** @typedef {import('./utils').Translations} Translations */ + +/** + * + * @param {object} params + * @param {Translations} params.context + * @param {string} [params.type] + * @param {BotContext} context + * @returns {Resolver} + */ +// eslint-disable-next-line no-unused-vars +function contextMessage (params, context) { + + return async (req, res) => { + const translated = getLanguageText(params.context, req.state.lang); + const translatedText = Array.isArray(translated) ? translated[0] : translated; + const statefulPrompt = compileWithState(req, res, translatedText); + + res.llmAddSystemPrompt(statefulPrompt, params.type); + + return Router.CONTINUE; + }; +} + +module.exports = contextMessage; diff --git a/src/resolvers/index.js b/src/resolvers/index.js index e0c289c..ea688ca 100644 --- a/src/resolvers/index.js +++ b/src/resolvers/index.js @@ -17,10 +17,12 @@ const subscribtions = require('./subscribtions'); const setState = require('./setState'); const expectedInput = require('./expectedInput'); const skip = require('./skip'); +const contextMessage = require('./contextMessage'); module.exports = { path, message, + contextMessage, include, postback, expected, diff --git a/src/resolvers/message.js b/src/resolvers/message.js index 1f795d8..7fbbe7e 100644 --- a/src/resolvers/message.js +++ b/src/resolvers/message.js @@ -270,12 +270,23 @@ function selectTranslation (resolverId, params, texts, data) { ]; } -/** @typedef {import('../BuildRouter').BotContext} BotContext */ -/** @typedef {import('../Router').Resolver} Resolver */ +/** @typedef {import('../BuildRouter').BotContext} BotContext */ +/** @typedef {import('../Router').Resolver} Resolver */ /** * * @param {object} params + * @param {any} params.text + * @param {boolean} [params.hasCondition] + * @param {string} [params.conditionFn] + * @param {string} [params.conditionDesc] + * @param {boolean} [params.hasEditableCondition] + * @param {any} [params.editableCondition] + * @param {string} [params.mode] + * @param {string} [params.persist] + * @param {any[]} [params.replies] + * @param {"message" | "prompt"} [params.type] + * @param {string} [params.llmContextType] * @param {BotContext} context * @returns {Resolver} */ @@ -284,10 +295,40 @@ function message (params, context = {}) { // @ts-ignore isLastIndex, isLastMessage, linksMap, configuration, resolverId } = context; + if (typeof params.text !== 'string' && !Array.isArray(params.text)) { throw new Error('Message should be a text!'); } + if (params.type === 'prompt') { + return async (req, res) => { + const session = await res.llmSessionWithHistory(params.llmContextType); + + const translatedObject = getLanguageText(params.text, req.lang); + const translatedText = Array.isArray(translatedObject) + ? translatedObject[0] + : translatedObject; + + const response = await session.systemPrompt(translatedText) + .debug() + .generate(); + + // if (response.finishReason && response.finishReason !== 'length') { + // // error maybe? + // return Router.END; + // } + + if (!response.content) { + // no response? + return Router.CONTINUE; + } + + res.text(response.content); + + return Router.CONTINUE; + }; + } + // parse quick replies let quickReplies; if (params.replies && !Array.isArray(params.replies)) { diff --git a/src/resolvers/utils.js b/src/resolvers/utils.js index 13f2dfc..5e514a6 100644 --- a/src/resolvers/utils.js +++ b/src/resolvers/utils.js @@ -62,7 +62,7 @@ function isTextObjectEmpty (text) { * @param {Translations} translations * @param {string} [lang] * @param {boolean} [disableDefaulting] - it will try to find translation for other language - * @returns {null|string} + * @returns {null|string|string[]} */ function getLanguageText (translations, lang = null, disableDefaulting = false) { let foundText; diff --git a/test/llm.test.js b/test/llm.test.js index 6d99402..c742692 100644 --- a/test/llm.test.js +++ b/test/llm.test.js @@ -7,6 +7,8 @@ const { strict: assert } = require('assert'); const Router = require('../src/Router'); const Tester = require('../src/Tester'); +const message = require('../src/resolvers/message'); +const contextMessage = require('../src/resolvers/contextMessage'); describe('', () => { @@ -57,4 +59,46 @@ describe('', () => { t.debug(); }); + it('should work with resolvers', async () => { + const bot = new Router(); + + bot.use(contextMessage({ + context: 'The user is 5 years old.' + }, {})); + + bot.use(message({ + text: 'Based on the users age explain what nuclear fusion is in 3 sentences.', + type: 'prompt' + }, {})); + + const t = new Tester(bot); + + await t.text('hello this is user'); + + t.any() + .contains('The user is 5 years old.') + .contains('Based on the users age explain what nuclear fusion is in 3 sentences.'); + }); + + it('should work with resolvers', async () => { + const bot = new Router(); + + bot.use(contextMessage({ + context: 'The user is 5 years old.' + }, {})); + + bot.use(message({ + text: 'Based on the users age explain what nuclear fusion is in 3 sentences.', + type: 'prompt' + }, {})); + + const t = new Tester(bot); + + await t.text('hello this is user'); + + t.any() + .contains('The user is 5 years old.') + .contains('Based on the users age explain what nuclear fusion is in 3 sentences.'); + }); + }); diff --git a/test/resolvers/message.js b/test/resolvers/message.js index 36c3b05..0290f87 100644 --- a/test/resolvers/message.js +++ b/test/resolvers/message.js @@ -54,6 +54,7 @@ describe('Message voice control', () => { it('shows translated quick replies', async () => { const bot = new Router(); + // @ts-ignore bot.use('go', message({ text: [ { l: 'cs', t: ['Czech text'] }, diff --git a/test/transcript/transcriptFromHistory.test.js b/test/transcript/transcriptFromHistory.test.js index a2e5e2a..d07fcd4 100755 --- a/test/transcript/transcriptFromHistory.test.js +++ b/test/transcript/transcriptFromHistory.test.js @@ -52,8 +52,6 @@ describe('transcriptFromHistory', () => { const transcript = await transcriptFromHistory(t.senderLogger, t.senderId, t.pageId); - // console.log(transcript); - assert.deepStrictEqual(transcript.map((tr) => ({ ...tr, timestamp: null })), [ { fromBot: false,