├── .github ├── FUNDING.yml ├── lock.yml ├── stale.yml └── workflows │ ├── release.yml │ ├── lint-fixer.yml │ └── ci.yml ├── JuicyChatBot.png ├── images └── git-stats.png ├── .gitignore ├── types.d.ts ├── tsconfig.json ├── .eslintrc.cjs ├── .mailmap ├── LICENSE ├── factory.ts ├── README.md ├── package.json ├── nlp ├── sentiment-analyzer.ts ├── sentiment.ts └── index.ts ├── index.ts └── test └── initialize.spec.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://sponsor.owasp-juice.shop 2 | -------------------------------------------------------------------------------- /JuicyChatBot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juice-shop/juicy-chat-bot/HEAD/JuicyChatBot.png -------------------------------------------------------------------------------- /images/git-stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juice-shop/juicy-chat-bot/HEAD/images/git-stats.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | test.js 4 | .idea/ 5 | .nyc_output/ 6 | build/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@nlpjs/sentiment'; 2 | declare module '@nlpjs/core'; 3 | declare module '@nlpjs/core-loader'; 4 | declare module '@nlpjs/lang-en'; 5 | declare module '@nlpjs/language'; 6 | declare module '@nlpjs/nlp'; 7 | declare module '@nlpjs/nlu'; 8 | declare module '@nlpjs/evaluator'; 9 | declare module '@nlpjs/request'; 10 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | --- 2 | daysUntilLock: 365 3 | skipCreatedBefore: false 4 | exemptLabels: [] 5 | lockLabel: false 6 | lockComment: > 7 | This thread has been automatically locked because it has not had 8 | recent activity after it was closed. :lock: Please open a new issue 9 | for regressions or related bugs. 10 | setLockReason: false 11 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | daysUntilStale: 28 3 | daysUntilClose: 14 4 | exemptLabels: 5 | - bounty 6 | - critical 7 | - feature 8 | - technical debt 9 | staleLabel: stale 10 | markComment: > 11 | This issue has been automatically marked as `stale` because it has not had 12 | recent activity. :calendar: It will be _closed automatically_ in one week if no further activity occurs. 13 | closeComment: false 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "declaration": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "skipLibCheck": true, 12 | "sourceMap": true 13 | }, 14 | "include": [ 15 | "./**/*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "standard-with-typescript", 4 | ], 5 | plugins: ["@typescript-eslint"], 6 | env: { 7 | node: true, 8 | }, 9 | globals: { 10 | Atomics: "readonly", 11 | SharedArrayBuffer: "readonly", 12 | }, 13 | parser: "@typescript-eslint/parser", 14 | parserOptions: { 15 | ecmaVersion: 2020, 16 | project: "./tsconfig.json", 17 | }, 18 | ignorePatterns: [ 19 | ".eslintrc.js", 20 | "dist/**", 21 | "build/**", 22 | "node_modules/**", 23 | ], 24 | }; -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Björn Kimminich Bjoern Kimminich 2 | Björn Kimminich Bjoern Kimminich 3 | Björn Kimminich Björn Kimminich 4 | Björn Kimminich Björn Kimminich 5 | Björn Kimminich bjoern.kimminich 6 | Björn Kimminich Björn Kimminich 7 | Scar26 Mohit Sharma <41830515+Scar26@users.noreply.github.com> -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release Pipeline" 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v5 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | registry-url: 'https://registry.npmjs.org' 18 | - run: npm install 19 | - run: npm run build 20 | - name: Publish package on NPM 📦 21 | run: npm publish --provenance --access public 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | notify-slack: 25 | if: always() 26 | needs: 27 | - publish 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: "Slack workflow notification" 31 | uses: Gamesight/slack-workflow-status@master 32 | with: 33 | repo_token: ${{ secrets.GITHUB_TOKEN }} 34 | slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} 35 | -------------------------------------------------------------------------------- /.github/workflows/lint-fixer.yml: -------------------------------------------------------------------------------- 1 | name: "Let me lint:fix that for you" 2 | 3 | on: [push] 4 | 5 | jobs: 6 | LMLFTFY: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out Git repository 10 | uses: actions/checkout@v5 11 | - name: Install Linter 12 | run: | 13 | npm install --ignore-scripts 14 | - name: Fix everything which can be fixed 15 | run: 'npm run lint:fix' 16 | - uses: stefanzweifel/git-auto-commit-action@v4.0.0 17 | with: 18 | commit_message: "Auto-fix linting issues" 19 | 20 | # Optional name of the branch the commit should be pushed to 21 | # Required if Action is used in Workflow listening to the `pull_request` event 22 | branch: ${{ github.head_ref }} 23 | 24 | # Optional git params 25 | commit_options: '--signoff' 26 | 27 | # Optional commit user and author settings 28 | commit_user_name: JuiceShopBot 29 | commit_user_email: 61591748+JuiceShopBot@users.noreply.github.com 30 | commit_author: JuiceShopBot <61591748+JuiceShopBot@users.noreply.github.com> 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Björn Kimminich & the OWASP Juice Shop contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /factory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023 Bjoern Kimminich & the OWASP Juice Shop contributors. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /* eslint-disable no-unused-vars */ 7 | declare const Nlp: new (settings?: any) => any; 8 | declare let training: { state: boolean, data: any }; 9 | 10 | var trainingSet = training.data; 11 | const model = new Nlp({ languages: ['en'], nlu: { log: false }, autoSave: false, autoLoad: false, modelFileName: '' }); 12 | 13 | var users = { 14 | idmap: {} as Record, 15 | 16 | addUser: function (token: string, name: string): void { 17 | this.idmap[token] = name; 18 | }, 19 | 20 | get: function (token: string): string | undefined { 21 | return this.idmap[token]; 22 | } 23 | }; 24 | 25 | function train(): Promise { 26 | trainingSet.data.forEach((query: any) => { 27 | query.utterances.forEach((utterance: string) => { 28 | model.addDocument(trainingSet.lang, utterance, query.intent); 29 | }); 30 | query.answers.forEach((answer: any) => { 31 | model.addAnswer(trainingSet.lang, query.intent, answer); 32 | }); 33 | }); 34 | return model.train().then(() => { training.state = true; }); 35 | } 36 | 37 | function processQuery(query: string, token: string): Promise | { action: string, body: string } { if (users.get(token)) { 38 | return model.process(trainingSet.lang, query); 39 | } else { 40 | return { action: 'unrecognized', body: 'user does not exist' }; 41 | } 42 | } 43 | 44 | function currentUser(token: string): string | undefined { 45 | return users.get(token); 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Juice Shop CTF Logo](https://github.com/juice-shop/juicy-chat-bot/raw/master/JuicyChatBot.png) Juicy Chat Bot 2 | 3 | [![npm Downloads](https://img.shields.io/npm/dm/juicy-chat-bot.svg)](https://www.npmjs.com/package/juicy-chat-bot) 4 | [![CI Pipeline](https://github.com/juice-shop/juicy-chat-bot/actions/workflows/ci.yml/badge.svg)](https://github.com/juice-shop/juicy-chat-bot/actions/workflows/ci.yml) 5 | [![Coverage Status](https://coveralls.io/repos/github/juice-shop/juicy-chat-bot/badge.svg?branch=master)](https://coveralls.io/github/juice-shop/juicy-chat-bot?branch=master) 6 | [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 7 | 8 | Smart, friendly and helpful chat bot for OWASP Juice Shop. 9 | 10 | ## GitHub Contributors 11 | 12 | As reported by [`git-stats -a -s '2020'`](https://www.npmjs.com/package/git-stats) analysis 13 | of `master` as of Wed, 12 Oct 2022 after deduplication with `.mailmap`. 14 | 15 | ![Top git contributors](images/git-stats.png) 16 | 17 | ## Stargazers (over time) 18 | 19 | [![Stargazers over time](https://starchart.cc/juice-shop/juicy-chat-bot.svg)](https://starchart.cc/juice-shop/juice-shop-ctf) 20 | 21 | ## Licensing [![license](https://img.shields.io/github/license/juice-shop/juicy-chat-bot.svg)](LICENSE) 22 | 23 | This program is free software: you can redistribute it and/or modify it 24 | under the terms of the [MIT license](LICENSE). Juicy Chat Bot and any 25 | contributions are Copyright © by Bjoern Kimminich & the OWASP Juice Shop 26 | contributors 2020-2023. 27 | 28 | --- 29 | 30 | ⚠️ _This software contains **intentional security flaws** for 31 | educational purposes! Depending on this module might put your own system 32 | at risk! **Do not use** this as an actual foundation for 33 | production-grade chat bot solutions!_ 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juicy-chat-bot", 3 | "version": "0.9.0", 4 | "types": "dist/index.d.ts", 5 | "files": [ 6 | "dist/**/*.js", 7 | "dist/**/*.d.ts", 8 | "!dist/test/*", 9 | "types.d.ts" 10 | ], 11 | "description": "A light-weight and totally \"secure\" library to easily deploy simple chat bots", 12 | "keywords": [ 13 | "npm", 14 | "chatbot", 15 | "juice-shop", 16 | "OWASP" 17 | ], 18 | "homepage": "https://owasp-juice.shop", 19 | "bugs": { 20 | "url": "https://github.com/juice-shop/juicy-chat-bot/issues" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/juice-shop/juicy-chat-bot.git" 25 | }, 26 | "license": "MIT", 27 | "author": "Björn Kimminich (https://kimminich.de)", 28 | "contributors": [ 29 | "Scar26", 30 | "Björn Kimminich" 31 | ], 32 | "main": "dist/index.js", 33 | "scripts": { 34 | "build": "tsc", 35 | "lint": "eslint index.ts nlp --ext .ts", 36 | "lint:fix": "eslint index.ts nlp --ext .ts --fix", 37 | "test": "c8 node dist/test/initialize.spec.js" 38 | }, 39 | "c8": { 40 | "all": true, 41 | "report-dir": "./build/reports/coverage", 42 | "exclude-after-remap": true, 43 | "exclude": [ 44 | "build/**", 45 | "dist/**", 46 | "node_modules/**", 47 | ".eslintrc.cjs", 48 | "**.d.ts" 49 | ], 50 | "reporter": [ 51 | "lcov", 52 | "text-summary" 53 | ] 54 | }, 55 | "dependencies": { 56 | "@nlpjs/core-loader": "^4.4.0", 57 | "@nlpjs/evaluator": "^4.4.0", 58 | "@nlpjs/lang-en": "^4.4.0", 59 | "@nlpjs/language": "^4.3.0", 60 | "@nlpjs/nlp": "^4.4.0", 61 | "@nlpjs/nlu": "^4.4.0", 62 | "@nlpjs/request": "^4.4.0", 63 | "@nlpjs/sentiment": "^4.4.0", 64 | "vm2": "3.9.17" 65 | }, 66 | "devDependencies": { 67 | "@types/node": "^24.3.0", 68 | "c8": "^10.1.3", 69 | "eslint-config-standard-with-typescript": "^43.0.1", 70 | "standard": "^14.3.1", 71 | "typescript": "^5.9.2" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /nlp/sentiment-analyzer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) AXA Group Operations Spain S.A. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | import { LangEn } from '@nlpjs/lang-en' 25 | import { Nlu } from '@nlpjs/nlu' 26 | import { SentimentAnalyzer as SentimentAnalyzerBase } from '@nlpjs/sentiment' 27 | import type { SentimentResult } from './sentiment' 28 | 29 | type SentimentAnalyzerSettings = Record 30 | 31 | interface Container { 32 | use: (module: any) => void 33 | [key: string]: any 34 | } 35 | 36 | type GetSentimentSettings = Record 37 | 38 | interface GetSentimentInput { 39 | utterance: string 40 | locale: string 41 | [key: string]: any 42 | } 43 | 44 | class SentimentAnalyzer extends SentimentAnalyzerBase { 45 | constructor ( 46 | settings: SentimentAnalyzerSettings = {}, 47 | container?: Container 48 | ) { 49 | super(settings, container) 50 | this.container.use(LangEn) 51 | this.container.use(Nlu) 52 | } 53 | 54 | async getSentiment ( 55 | utterance: string, 56 | locale: string = 'en', 57 | settings: GetSentimentSettings = {} 58 | ): Promise { 59 | const input: GetSentimentInput = { 60 | utterance, 61 | locale, 62 | ...settings 63 | } 64 | const result: SentimentResult = await this.process(input) 65 | return result 66 | } 67 | } 68 | 69 | export default SentimentAnalyzer 70 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023 Bjoern Kimminich & the OWASP Juice Shop contributors. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { VM } from 'vm2' 7 | import fs from 'fs' 8 | import path from 'path' 9 | import NlpManager from './nlp' 10 | 11 | const ctx = fs.readFileSync(path.join(__dirname, 'factory.js')).toString() 12 | 13 | interface Training { 14 | state: boolean 15 | data: string 16 | } 17 | 18 | interface BotResponse { 19 | action: string 20 | body: string 21 | } 22 | 23 | interface BotQueryResponse { 24 | action: string 25 | body?: string 26 | [key: string]: any 27 | } 28 | 29 | class Bot { 30 | name: string 31 | greeting: string 32 | defaultResponse!: BotResponse 33 | training!: Training 34 | factory: VM 35 | 36 | constructor ( 37 | name: string, 38 | greeting: string, 39 | trainingSet: string, 40 | defaultResponse: string 41 | ) { 42 | this.name = name 43 | this.greeting = greeting 44 | this.defaultResponse = { action: 'response', body: defaultResponse } 45 | this.training = { 46 | state: false, 47 | data: trainingSet 48 | } 49 | this.factory = new VM({ 50 | sandbox: { 51 | Nlp: NlpManager, 52 | training: this.training 53 | } 54 | }) 55 | this.factory.run(ctx) 56 | this.factory.run(`trainingSet=${trainingSet}`) 57 | } 58 | 59 | greet (token: string): string { 60 | return this.render(this.greeting, token) 61 | } 62 | 63 | render (statement: string, token: string): string { 64 | const currentUser = String(this.factory.run(`currentUser("${token}")`)) 65 | return statement.replace(//g, this.name).replace(//g, currentUser) 66 | } 67 | 68 | addUser (token: string, name: string): void { 69 | this.factory.run(`users.addUser("${token}", "${name}")`) 70 | } 71 | 72 | getUser (token: string): string { 73 | return this.factory.run(`users.get("${token}")`) 74 | } 75 | 76 | async respond (query: string, token: string): Promise { 77 | const response: BotQueryResponse = (await this.factory.run(`processQuery("${query}", "${token}")`)).answer 78 | if (response == null) { 79 | return this.defaultResponse 80 | } else { 81 | if (response.body != null) { 82 | response.body = this.render(response.body, token) 83 | } 84 | return response as BotResponse 85 | } 86 | } 87 | 88 | train (): any { 89 | return this.factory.run('train()') 90 | } 91 | } 92 | 93 | export default Bot 94 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI Pipeline" 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | paths-ignore: 7 | - '*.md' 8 | - 'LICENSE' 9 | tags-ignore: 10 | - '*' 11 | pull_request: 12 | branches: 13 | - develop 14 | paths-ignore: 15 | - '*.md' 16 | - 'LICENSE' 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: "Check out Git repository" 22 | uses: actions/checkout@v5 23 | - name: "Use Node.js" 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 22 27 | - name: "Install application" 28 | run: npm install --ignore-scripts 29 | - name: "Lint code" 30 | run: npm run lint 31 | test: 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | # Only one version should upload coverage to avoid duplicate reports 36 | include: 37 | - node-version: 20 38 | upload_coverage: false 39 | - node-version: 22 40 | upload_coverage: true 41 | - node-version: 24 42 | upload_coverage: false 43 | steps: 44 | - name: "Check out Git repository" 45 | uses: actions/checkout@v5 46 | - name: "Use Node.js ${{ matrix.node-version }}" 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{ matrix.node-version }} 50 | - name: "Cache Node.js modules" 51 | uses: actions/cache@v4 52 | with: 53 | path: ~/.npm 54 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package.json') }} 55 | restore-keys: | 56 | ${{ runner.OS }}-node- 57 | ${{ runner.OS }}- 58 | - name: "Install application" 59 | run: npm install 60 | - name: "Install application" 61 | run: npm run build 62 | - name: "Execute unit tests" 63 | run: npm test 64 | - name: "Publish coverage to Coveralls" 65 | if: github.event_name == 'push' && matrix.upload_coverage 66 | uses: coverallsapp/github-action@master 67 | with: 68 | github-token: ${{ secrets.GITHUB_TOKEN }} 69 | path-to-lcov: ./build/reports/coverage/lcov.info 70 | notify-slack: 71 | if: github.event_name == 'push' && (success() || failure()) 72 | needs: 73 | - lint 74 | - test 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: "Slack workflow notification" 78 | uses: Gamesight/slack-workflow-status@master 79 | with: 80 | repo_token: ${{ secrets.GITHUB_TOKEN }} 81 | slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} 82 | -------------------------------------------------------------------------------- /nlp/sentiment.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) AXA Group Operations Spain S.A. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | import SentimentAnalyzer from './sentiment-analyzer' 25 | 26 | type SentimentManagerSettings = Record 27 | 28 | /** 29 | * Class for the sentiment anlysis manager, able to manage 30 | * several different languages at the same time. 31 | */ 32 | export interface SentimentResult { 33 | score: number 34 | average: number 35 | numWords: number 36 | numHits: number 37 | type: string 38 | locale: string 39 | } 40 | 41 | interface TranslatedSentiment { 42 | score: number 43 | comparative: number 44 | vote: 'positive' | 'negative' | 'neutral' 45 | numWords: number 46 | numHits: number 47 | type: string 48 | language: string 49 | } 50 | 51 | /** 52 | * Class for the sentiment anlysis manager, able to manage 53 | * several different languages at the same time. 54 | */ 55 | class SentimentManager { 56 | settings: SentimentManagerSettings 57 | languages: Record 58 | analyzer: SentimentAnalyzer 59 | 60 | /** 61 | * Constructor of the class. 62 | */ 63 | constructor (settings?: SentimentManagerSettings) { 64 | this.settings = settings ?? {} 65 | this.languages = {} 66 | this.analyzer = new SentimentAnalyzer() 67 | } 68 | 69 | addLanguage (): void { 70 | } 71 | 72 | translate (sentiment: SentimentResult): TranslatedSentiment { 73 | let vote: 'positive' | 'negative' | 'neutral' 74 | if (sentiment.score > 0) { 75 | vote = 'positive' 76 | } else if (sentiment.score < 0) { 77 | vote = 'negative' 78 | } else { 79 | vote = 'neutral' 80 | } 81 | return { 82 | score: sentiment.score, 83 | comparative: sentiment.average, 84 | vote, 85 | numWords: sentiment.numWords, 86 | numHits: sentiment.numHits, 87 | type: sentiment.type, 88 | language: sentiment.locale 89 | } 90 | } 91 | 92 | async process ( 93 | locale: string, 94 | phrase: string 95 | ): Promise { 96 | const sentiment = await this.analyzer.getSentiment( 97 | phrase, 98 | locale, 99 | this.settings 100 | ) 101 | return this.translate(sentiment as SentimentResult) 102 | } 103 | } 104 | 105 | export default SentimentManager 106 | -------------------------------------------------------------------------------- /test/initialize.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023 Bjoern Kimminich & the OWASP Juice Shop contributors. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { describe, it, beforeEach } from 'node:test'; 7 | import assert from 'node:assert'; 8 | import Bot from '../index'; 9 | 10 | interface BotResponse { 11 | action: string; 12 | body: string; 13 | } 14 | 15 | interface Answer { 16 | action: string; 17 | body: string; 18 | } 19 | 20 | interface TrainingData { 21 | intent: string; 22 | utterances: string[]; 23 | answers: Answer[]; 24 | } 25 | 26 | interface TrainingSet { 27 | lang: string; 28 | data: TrainingData[]; 29 | } 30 | 31 | const trainingSet: TrainingSet = { 32 | lang: 'en', 33 | data: [ 34 | { 35 | intent: 'greetings.bye', 36 | utterances: ['goodbye for now', 'bye bye take care'], 37 | answers: [{ action: 'response', body: 'Ok Cya' }] 38 | }, 39 | { 40 | intent: 'greetings.hello', 41 | utterances: ['hello', 'hi', 'howdy'], 42 | answers: [{ action: 'response', body: 'Hello ' }] 43 | }, 44 | { 45 | intent: 'jokes.chucknorris', 46 | utterances: ['tell me a chuck norris joke'], 47 | answers: [ 48 | { action: 'response', body: 'Chuck Norris has two speeds: Walk and Kill.' }, 49 | { action: 'response', body: 'Time waits for no man. Unless that man is Chuck Norris.' } 50 | ] 51 | } 52 | ] 53 | }; 54 | 55 | describe('Initialize', () => { 56 | let bot: Bot; 57 | 58 | beforeEach(() => { 59 | bot = new Bot('Jeff', 'Ma Nemma ', JSON.stringify(trainingSet), 'lalala'); 60 | assert.doesNotThrow(() => bot.addUser('123', 'test-user')); 61 | }); 62 | 63 | it('should set up greeting for bot', () => { 64 | assert.strictEqual(bot.greet('123'), 'Ma Nemma Jeff'); 65 | }); 66 | 67 | it('should register training set in bot', () => { 68 | assert.deepStrictEqual(bot.factory.run('trainingSet'), trainingSet); 69 | }); 70 | 71 | it('should recognize registered user from token', async () => { 72 | await bot.train(); 73 | assert.strictEqual(bot.training.state, true); 74 | assert.deepStrictEqual(await bot.respond('hi bot', '123'), { 75 | action: 'response', 76 | body: 'Hello test-user' 77 | }); 78 | }); 79 | 80 | it('should register new user with corresponding token', async () => { 81 | await bot.train(); 82 | assert.doesNotThrow(() => bot.addUser('1234', 'user2')); 83 | assert.strictEqual(await bot.getUser('1234'), 'user2'); 84 | assert.deepStrictEqual(await bot.respond('hi bot', '1234'), { 85 | action: 'response', 86 | body: 'Hello user2' 87 | }); 88 | }); 89 | 90 | it('should respond to queries after training', async () => { 91 | await bot.train(); 92 | assert.deepStrictEqual(await bot.respond('bye', '123'), { 93 | action: 'response', 94 | body: 'Ok Cya' 95 | }); 96 | }); 97 | 98 | it('should respond randomly from multiple available answers', async () => { 99 | const responseCounter: Record = { 100 | 'Chuck Norris has two speeds: Walk and Kill.': 0, 101 | 'Time waits for no man. Unless that man is Chuck Norris.': 0 102 | }; 103 | const possibleAnswers: string[] = Object.keys(responseCounter); 104 | 105 | await bot.train(); 106 | for (let i = 0; i < 100; i++) { 107 | const response: BotResponse = await bot.respond('tell me a chuck norris joke', '123'); 108 | assert.strictEqual(response.action, 'response'); 109 | assert.ok(possibleAnswers.includes(response.body)); 110 | responseCounter[response.body]++; 111 | } 112 | assert.ok(responseCounter['Chuck Norris has two speeds: Walk and Kill.'] > 20); 113 | assert.ok(responseCounter['Time waits for no man. Unless that man is Chuck Norris.'] > 20); 114 | }); 115 | 116 | it('should respond with default response to unrecognized query', async () => { 117 | await bot.train(); 118 | assert.deepStrictEqual(await bot.respond('blabla blubb blubb', '123'), { 119 | action: 'response', 120 | body: 'lalala' 121 | }); 122 | }); 123 | }); -------------------------------------------------------------------------------- /nlp/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) AXA Group Operations Spain S.A. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | import fs from 'fs' 25 | import { containerBootstrap } from '@nlpjs/core-loader' 26 | import { Language } from '@nlpjs/language' 27 | import { LangEn } from '@nlpjs/lang-en' 28 | import { Nlp } from '@nlpjs/nlp' 29 | import { Evaluator, Template } from '@nlpjs/evaluator' 30 | import { fs as requestfs } from '@nlpjs/request' 31 | import SentimentManager from './sentiment' 32 | import type { SentimentResult } from './sentiment' 33 | 34 | type ActionParameters = Record 35 | 36 | type ActionFunction = (...args: any[]) => any 37 | 38 | class NlpManager { 39 | settings: any 40 | container: any 41 | nlp: any 42 | sentimentManager: SentimentManager 43 | constructor (settings = {}) { 44 | this.settings = settings 45 | if (this.settings.container == null) { 46 | this.settings.container = containerBootstrap() 47 | } 48 | this.container = this.settings.container 49 | this.container.registerConfiguration('ner', { 50 | entityPreffix: '%', 51 | entitySuffix: '%' 52 | }) 53 | this.container.register('fs', requestfs) 54 | this.container.register('Language', Language, false) 55 | this.container.use(LangEn) 56 | this.container.use(Evaluator) 57 | this.container.use(Template) 58 | this.nlp = new Nlp(this.settings) 59 | this.sentimentManager = new SentimentManager() 60 | } 61 | 62 | addDocument (locale: string, utterance: string, intent: string): void { 63 | return this.nlp.addDocument(locale, utterance, intent) 64 | } 65 | 66 | removeDocument (locale: string, utterance: string, intent: string): void { 67 | return this.nlp.removeDocument(locale, utterance, intent) 68 | } 69 | 70 | addLanguage (locale: string): void { 71 | return this.nlp.addLanguage(locale) 72 | } 73 | 74 | assignDomain (locale: string, intent: string, domain: string): void { 75 | return this.nlp.assignDomain(locale, intent, domain) 76 | } 77 | 78 | getIntentDomain (locale: string, intent: string): string | undefined { 79 | return this.nlp.getIntentDomain(locale, intent) 80 | } 81 | 82 | getDomains (): string[] { 83 | return this.nlp.getDomains() 84 | } 85 | 86 | guessLanguage (text: string): string { 87 | return this.nlp.guessLanguage(text) 88 | } 89 | 90 | addAction ( 91 | intent: string, 92 | action: string, 93 | parameters: ActionParameters, 94 | fn?: ActionFunction 95 | ): void { 96 | let finalFn = fn 97 | if (finalFn == null) { 98 | finalFn = this.settings.action != null ? (this.settings.action)[action] : undefined 99 | } 100 | return this.nlp.addAction(intent, action, parameters, finalFn) 101 | } 102 | 103 | getActions (intent: string): Array<{ action: string, parameters: ActionParameters, fn?: ActionFunction }> { 104 | return this.nlp.getActions(intent) 105 | } 106 | 107 | removeAction ( 108 | intent: string, 109 | action: string, 110 | parameters: ActionParameters 111 | ): any { 112 | return this.nlp.removeAction(intent, action, parameters) 113 | } 114 | 115 | removeActions (intent: string): Array<{ action: string, parameters: ActionParameters, fn?: ActionFunction }> { 116 | return this.nlp.removeActions(intent) 117 | } 118 | 119 | addAnswer ( 120 | locale: string, 121 | intent: string, 122 | answer: string, 123 | opts?: Record 124 | ): any { 125 | return this.nlp.addAnswer(locale, intent, answer, opts) 126 | } 127 | 128 | removeAnswer ( 129 | locale: string, 130 | intent: string, 131 | answer: string, 132 | opts?: Record 133 | ): any { 134 | return this.nlp.removeAnswer(locale, intent, answer, opts) 135 | } 136 | 137 | findAllAnswers (locale: string, intent: string): string[] { 138 | return this.nlp.findAllAnswers(locale, intent) 139 | } 140 | 141 | async getSentiment (locale: string, utterance: string): Promise { 142 | const sentiment: { sentiment: any } = await this.nlp.getSentiment( 143 | locale, 144 | utterance 145 | ) 146 | return this.sentimentManager.translate(sentiment.sentiment as SentimentResult) 147 | } 148 | 149 | addNamedEntityText ( 150 | entityName: string, 151 | optionName: string, 152 | languages: string[] | string, 153 | texts: string[] | string 154 | ): any { 155 | return this.nlp.addNerRuleOptionTexts( 156 | languages, 157 | entityName, 158 | optionName, 159 | texts 160 | ) 161 | } 162 | 163 | removeNamedEntityText ( 164 | entityName: string, 165 | optionName: string, 166 | languages: string[] | string, 167 | texts: string[] | string 168 | ): any { 169 | return this.nlp.removeNerRuleOptionTexts( 170 | languages, 171 | entityName, 172 | optionName, 173 | texts 174 | ) 175 | } 176 | 177 | addRegexEntity ( 178 | entityName: string, 179 | languages: string[] | string, 180 | regex: RegExp | string 181 | ): any { 182 | return this.nlp.addNerRegexRule(languages, entityName, regex) 183 | } 184 | 185 | addBetweenCondition ( 186 | locale: string, 187 | name: string, 188 | left: string[] | string, 189 | right: string[] | string, 190 | opts?: Record 191 | ): any { 192 | return this.nlp.addNerBetweenCondition(locale, name, left, right, opts) 193 | } 194 | 195 | addPositionCondition ( 196 | locale: string, 197 | name: string, 198 | position: number, 199 | words: string[] | string, 200 | opts?: Record 201 | ): any { 202 | return this.nlp.addNerPositionCondition( 203 | locale, 204 | name, 205 | position, 206 | words, 207 | opts 208 | ) 209 | } 210 | 211 | addAfterCondition ( 212 | locale: string, 213 | name: string, 214 | words: string[] | string, 215 | opts?: Record 216 | ): any { 217 | return this.nlp.addNerAfterCondition(locale, name, words, opts) 218 | } 219 | 220 | addAfterFirstCondition ( 221 | locale: string, 222 | name: string, 223 | words: string[] | string, 224 | opts?: Record 225 | ): any { 226 | return this.nlp.addNerAfterFirstCondition(locale, name, words, opts) 227 | } 228 | 229 | addAfterLastCondition ( 230 | locale: string, 231 | name: string, 232 | words: string[] | string, 233 | opts?: Record 234 | ): any { 235 | return this.nlp.addNerAfterLastCondition(locale, name, words, opts) 236 | } 237 | 238 | addBeforeCondition ( 239 | locale: string, 240 | name: string, 241 | words: string[] | string, 242 | opts?: Record 243 | ): any { 244 | return this.nlp.addNerBeforeCondition(locale, name, words, opts) 245 | } 246 | 247 | addBeforeFirstCondition ( 248 | locale: string, 249 | name: string, 250 | words: string[] | string, 251 | opts?: Record 252 | ): any { 253 | return this.nlp.addNerBeforeFirstCondition(locale, name, words, opts) 254 | } 255 | 256 | addBeforeLastCondition ( 257 | locale: string, 258 | name: string, 259 | words: string[] | string, 260 | opts?: Record 261 | ): any { 262 | return this.nlp.addNerBeforeLastCondition(locale, name, words, opts) 263 | } 264 | 265 | describeLanguage (locale: string, name: string): any { 266 | return this.nlp.describeLanguage(locale, name) 267 | } 268 | 269 | beginEdit (): void {} 270 | 271 | train (): any { 272 | return this.nlp.train() 273 | } 274 | 275 | classify ( 276 | locale: string, 277 | utterance: string, 278 | settings?: Record 279 | ): any { 280 | return this.nlp.classify(locale, utterance, settings) 281 | } 282 | 283 | async process ( 284 | locale: string, 285 | utterance: string, 286 | context?: any, 287 | settings?: any 288 | ): Promise { 289 | const result = await this.nlp.process(locale, utterance, context, settings) 290 | if (this.settings.processTransformer != null) { 291 | return this.settings.processTransformer(result) 292 | } 293 | return result 294 | } 295 | 296 | extractEntities ( 297 | locale: string, 298 | utterance: string, 299 | context?: any, 300 | settings?: any 301 | ): any { 302 | return this.nlp.extractEntities(locale, utterance, context, settings) 303 | } 304 | 305 | toObj (): any { 306 | return this.nlp.toJSON() 307 | } 308 | 309 | fromObj (obj: any): any { 310 | return this.nlp.fromJSON(obj) 311 | } 312 | 313 | /** 314 | * Export NLP manager information as a string. 315 | * @param {Boolean} minified If true, the returned JSON will have no spacing or indentation. 316 | * @returns {String} NLP manager information as a JSON string. 317 | */ 318 | export (minified = false): string { 319 | const clone = this.toObj() 320 | return minified ? JSON.stringify(clone) : JSON.stringify(clone, null, 2) 321 | } 322 | 323 | /** 324 | * Load NLP manager information from a string. 325 | * @param {String|Object} data JSON string or object to load NLP manager information from. 326 | */ 327 | import (data: string | object): void { 328 | const clone = typeof data === 'string' ? JSON.parse(data) : data 329 | this.fromObj(clone) 330 | } 331 | 332 | /** 333 | * Save the NLP manager information into a file. 334 | * @param {String} srcFileName Filename for saving the NLP manager. 335 | */ 336 | save (srcFileName?: string, minified = false): void { 337 | const fileName = srcFileName ?? 'model.nlp' 338 | fs.writeFileSync(fileName, this.export(minified), 'utf8') 339 | } 340 | 341 | /** 342 | * Load the NLP manager information from a file. 343 | * @param {String} srcFilename Filename for loading the NLP manager. 344 | */ 345 | load (srcFileName?: string): void { 346 | const fileName = srcFileName ?? 'model.nlp' 347 | const data = fs.readFileSync(fileName, 'utf8') 348 | this.import(data) 349 | } 350 | } 351 | 352 | export default NlpManager 353 | --------------------------------------------------------------------------------