├── .commitlintrc.json ├── .editorconfig ├── .env.sample ├── .eslintrc.js ├── .fake-npmrc ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .huskyrc.json ├── .lintstagedrc ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── README.ru.md ├── examples ├── audiotour │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── config.ts │ │ ├── intents.json │ │ ├── legacyAction.ts │ │ └── types.ts │ └── tsconfig.json ├── chatapp-brain │ ├── .env.sample │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── intents.json │ │ ├── middleware.ts │ │ └── system.i18n │ │ │ ├── athena.ts │ │ │ ├── index.ts │ │ │ ├── joy.ts │ │ │ └── sber.ts │ └── tsconfig.json ├── echo │ ├── .env.sample │ ├── next-env.d.ts │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── Components │ │ │ └── GlobalStyles.tsx │ │ ├── hooks │ │ │ ├── useScenario.ts │ │ │ └── useTransport.ts │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ └── index.tsx │ │ └── server.ts │ └── tsconfig.json ├── payment-chatapp │ ├── .env.sample │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── cards.ts │ │ ├── handlers.ts │ │ ├── index.ts │ │ ├── intents.ts │ │ ├── products.json │ │ └── types.ts │ └── tsconfig.json └── todo │ ├── .env.sample │ ├── README.md │ ├── fixtures │ ├── requests.json │ └── responses.json │ ├── global.d.ts │ ├── jest.config.ts │ ├── next-env.d.ts │ ├── package-lock.json │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── Components │ │ └── GlobalStyles.tsx │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── hook.ts │ │ └── index.tsx │ ├── scenario │ │ ├── handlers.ts │ │ ├── scenario.test.ts │ │ ├── scenario.ts │ │ └── types.ts │ ├── store │ │ └── index.ts │ └── types │ │ └── index.ts │ └── tsconfig.json ├── jest.config.ts ├── lerna.json ├── package-lock.json ├── package.json └── packages ├── recognizer-smartapp-brain ├── .env.sample ├── .npmrc ├── CHANGELOG.md ├── README.md ├── bin │ └── brain.js ├── package-lock.json ├── package.json ├── src │ ├── bin │ │ └── brain.ts │ ├── index.ts │ └── lib │ │ ├── index.ts │ │ ├── permittedSystemEntities.ts │ │ ├── projectData.ts │ │ └── smartAppBrainSync.ts └── tsconfig.json ├── recognizer-string-similarity ├── .npmrc ├── CHANGELOG.md ├── README.md ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.json ├── scenario ├── .npmrc ├── CHANGELOG.md ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── index.ts │ └── lib │ │ ├── createIntents.ts │ │ ├── createSaluteRequest.ts │ │ ├── createSaluteResponse.ts │ │ ├── createScenarioWalker.ts │ │ ├── createSystemScenario.ts │ │ ├── createUserScenario.ts │ │ ├── i18n.spec.ts │ │ ├── i18n.ts │ │ ├── matchers.ts │ │ ├── missingVariables.ts │ │ ├── plural │ │ └── ru.ts │ │ ├── smartpay.ts │ │ ├── smartpush.ts │ │ └── types │ │ ├── i18n.ts │ │ ├── payment.ts │ │ ├── push.ts │ │ ├── request.ts │ │ ├── response.ts │ │ ├── salute.ts │ │ ├── storage.ts │ │ └── systemMessage.ts └── tsconfig.json ├── storage-adapter-firebase ├── .npmrc ├── CHANGELOG.md ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── index.ts │ └── lib │ │ └── firebase.ts └── tsconfig.json └── storage-adapter-memory ├── .npmrc ├── CHANGELOG.md ├── README.md ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "subject-case": [0] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 120 10 | trim_trailing_whitespace = true 11 | 12 | [*.{json,md,yaml,yml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | ACCESS_TOKEN=7aadb15c-11cc-461a-8da6-94b3ae6884a2 2 | SMARTAPP_BRAIN_HOST=https://smartapp-code.sberdevices.ru 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb-base', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['import', 'prettier'], 5 | rules: { 6 | '@typescript-eslint/no-empty-function': 'off', 7 | '@typescript-eslint/no-empty-interface': 'off', 8 | 'no-restricted-syntax': 'off', 9 | 'spaced-comment': ['error', 'always', { markers: ['/'] }], /// разрешаем ts-require directive 10 | 'comma-dangle': ['error', 'always-multiline'], 11 | 'arrow-parens': ['error', 'always'], 12 | indent: 'off', 13 | 'max-len': [ 14 | 'error', 15 | 120, 16 | 2, 17 | { 18 | ignoreUrls: true, 19 | ignoreComments: false, 20 | ignoreRegExpLiterals: true, 21 | ignoreStrings: true, 22 | ignoreTemplateLiterals: true, 23 | }, 24 | ], 25 | 'implicit-arrow-linebreak': 'off', 26 | 'no-plusplus': 'off', 27 | 'max-classes-per-file': 'off', 28 | 'operator-linebreak': 'off', 29 | 'object-curly-newline': 'off', 30 | 'class-methods-use-this': 'off', 31 | 'no-confusing-arrow': 'off', 32 | 'function-paren-newline': 'off', 33 | 'no-param-reassign': 'off', 34 | 'no-shadow': 'warn', 35 | 'space-before-function-paren': 'off', 36 | 'consistent-return': 'off', 37 | 'prettier/prettier': 'error', 38 | 39 | '@typescript-eslint/explicit-function-return-type': 'off', 40 | 41 | 'import/prefer-default-export': 'off', // https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/ 42 | 'import/order': [ 43 | 'error', 44 | { 45 | groups: [['builtin', 'external'], 'internal', 'parent', 'sibling', 'index'], 46 | 'newlines-between': 'always', 47 | }, 48 | ], 49 | 'import/no-unresolved': 'off', 50 | 'import/extensions': 'off', 51 | 'import/no-extraneous-dependencies': ['off'], // можно включить тока нужно резолвы разрулить 52 | 'arrow-body-style': 'off', 53 | 'padding-line-between-statements': 'off', 54 | 'no-unused-expressions': 'off', 55 | camelcase: 'off', 56 | 'no-underscore-dangle': 'off', 57 | '@typescript-eslint/explicit-module-boundary-types': 'off', 58 | '@typescript-eslint/ban-ts-comment': 'off', 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /.fake-npmrc: -------------------------------------------------------------------------------- 1 | @salutejs:registry=https://registry.npmjs.org/ 2 | save-exact=true 3 | //registry.npmjs.org/:_authToken=${NPM_REGISTRY_TOKEN} 4 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'javascript' ] 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v2 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | - name: Cache node_modules 50 | uses: actions/cache@v1 51 | with: 52 | path: node_modules 53 | key: npm-deps-${{ hashFiles('package-lock.json') }} 54 | restore-keys: | 55 | npm-deps-${{ hashFiles('package-lock.json') }} 56 | 57 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 58 | # If this step fails, then you should remove it and run the build manually (see below) 59 | - name: Prepare 60 | run: | 61 | npm i 62 | npm run bootstrap 63 | npm run build 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | env: 13 | PR_NAME: pr-${{ github.event.number }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Use Node.js 12.x 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 12.x 21 | 22 | - name: Cache node_modules 23 | uses: actions/cache@v1 24 | with: 25 | path: node_modules 26 | key: npm-deps-${{ hashFiles('package-lock.json') }} 27 | restore-keys: | 28 | npm-deps-${{ hashFiles('package-lock.json') }} 29 | 30 | - name: Setup packages 31 | run: | 32 | npm i 33 | npm run bootstrap 34 | npm run build 35 | 36 | - name: Unit 37 | run: npm run test 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | publish: 13 | name: Publish 14 | runs-on: ubuntu-latest 15 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 18 | NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }} 19 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Prepare repository 24 | run: git fetch --unshallow --tags 25 | 26 | - name: Unset header 27 | # https://github.com/intuit/auto/issues/1030 28 | run: git config --local --unset http.https://github.com/.extraheader 29 | 30 | - name: Use Node.js 12.x 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: 12.x 34 | 35 | - name: Cache node modules 36 | uses: actions/cache@v1 37 | with: 38 | path: node_modules 39 | key: npm-deps-${{ hashFiles('package-lock.json') }} 40 | restore-keys: | 41 | npm-deps-${{ hashFiles('package-lock.json') }} 42 | 43 | - name: Setup packages 44 | run: | 45 | npm i 46 | npm run bootstrap 47 | 48 | - name: Lint 49 | run: npm run lint 50 | 51 | - name: ByPass npm auth 52 | run: cp .fake-npmrc .npmrc 53 | 54 | - name: Release Info 55 | run: npm whoami && npx auto info || echo 'auto info returned 1' 56 | 57 | - name: Create Release 58 | run: npm run release 59 | 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### to bypass npm auth 2 | .npmrc 3 | 4 | ##### OS FILES 5 | .DS_Store 6 | .history 7 | 8 | ##### Editors 9 | .*.swp 10 | .idea 11 | .vscode 12 | .vs 13 | *.iml 14 | *.sublime-project 15 | *.sublime-workspace 16 | 17 | ##### BUILD 18 | node_modules/ 19 | dist/ 20 | 21 | ##### ENV 22 | .env 23 | 24 | ##### next.js 25 | .next 26 | 27 | ##### vercel 28 | .vercel 29 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-push": "lerna run test", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx}": [ 3 | "eslint --fix", 4 | "prettier --write" 5 | ], 6 | "*.md": [ 7 | "prettier --write --parser markdown", 8 | "git add" 9 | ], 10 | "*.json": [ 11 | "prettier --write --parser json", 12 | "node", 13 | "git add" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "arrowParens": "always", 4 | "printWidth": 120, 5 | "endOfLine": "auto", 6 | "semi": true, 7 | "singleQuote": true, 8 | "tabWidth": 4, 9 | "trailingComma": "all" 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at developer@sberdevices.ru. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are open to, and grateful for, any contributions made by the community. By contributing to Redux, you agree to abide by the [code of conduct](https://github.com/sberdevices/salutejs/blob/master/CODE_OF_CONDUCT.md). 4 | 5 | ## Reporting Issues and Asking Questions 6 | 7 | Before opening an issue, please search the [issue tracker](https://github.com/sberdevices/salutejs/issues) to make sure your issue hasn't already been reported. 8 | 9 | 10 | ### Bugs and Improvements 11 | 12 | We use the issue tracker to keep track of bugs and improvements to Taskany itself, its examples, and the documentation. We encourage you to open issues to discuss improvements, architecture, theory, internal implementation, etc. If a topic has been discussed before, we will ask you to join the previous discussion. 13 | 14 | As Taskany is stable software, changes to its behavior are very carefully considered. 15 | 16 | 17 | ### Getting Help 18 | 19 | **For support or usage questions like “how do I do X with Taskany” and “my code doesn't work”, please search and ask on [Discussion](https://github.com/sberdevices/salutejs/discussions) first.** 20 | 21 | Some questions take a long time to get an answer. **If your question gets closed or you don't get a reply on Discussion for longer than a few days,** we encourage you to post an issue linking to your question. We will close your issue but this will give people watching the repo an opportunity to see your question and reply to it on Discussions if they know the answer. 22 | 23 | Please be considerate when doing this as this is not the primary purpose of the issue tracker. 24 | 25 | 26 | ### Help Us Help You 27 | 28 | On both websites, it is a good idea to structure your code and question in a way that is easy to read to entice people to answer it. For example, we encourage you to use syntax highlighting, indentation, and split text in paragraphs. 29 | 30 | Please keep in mind that people spend their free time trying to help you. You can make it easier for them if you provide versions of the relevant libraries and a runnable small project reproducing your issue. You can put your code on [JSBin](https://jsbin.com) or, for bigger projects, on GitHub. Make sure all the necessary dependencies are declared in `package.json` so anyone can run `npm install && npm start` and reproduce your issue. 31 | 32 | ## Development 33 | 34 | Visit the [issue tracker](https://github.com/sberdevices/salutejs/issues) to find a list of open issues that need attention. 35 | 36 | Fork, then clone the repo: 37 | 38 | ```sh 39 | git clone https://github.com/your-username/salutejs.git 40 | ``` 41 | 42 | ### Testing and Linting 43 | 44 | To only run linting: 45 | 46 | ```sh 47 | npm run lint 48 | ``` 49 | 50 | To only run tests: 51 | 52 | ```sh 53 | npm run test 54 | ``` 55 | 56 | ### Docs 57 | 58 | Improvements to the documentation are always welcome. You can find them in the [`docs`](/docs) path. 59 | 60 | 61 | ### Sending a Pull Request 62 | 63 | For non-trivial changes, please open an issue with a proposal for a new feature or refactoring before starting on the work. We don't want you to waste your efforts on a pull request that we won't want to accept. 64 | 65 | On the other hand, sometimes the best way to start a conversation _is_ to send a pull request. Use your best judgement! 66 | 67 | In general, the contribution workflow looks like this: 68 | 69 | - Open a new issue in the [Issue tracker](https://github.com/sberdevices/salutejs/issues). 70 | - Fork the repo. 71 | - Create a new feature branch based off the `master` branch. 72 | - Make sure all tests pass and there are no linting errors. 73 | - Submit a pull request, referencing any issues it addresses. 74 | 75 | Please try to keep your pull request focused in scope and avoid including unrelated commits. 76 | 77 | After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or improvements. 78 | 79 | Thank you for contributing! 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![salutejs](https://user-images.githubusercontent.com/982072/112627725-0606e400-8e43-11eb-86ef-a9e2fdcfc465.png) 2 | 3 | # SaluteJS 4 | 5 | __Set of minimalistic utils for [Salute Assistants](https://sber.ru/salute) scenario implementation__. 6 | 7 | - directly in code autocomplete for intents and app state; 8 | - strongly typed out of the box: whole [SmartApp API](https://developer.sberdevices.ru/docs/ru/developer_tools/amp/smartappapi_description_and_guide) types inside; 9 | - common types between scenario and [Canvas Apps](https://developer.sberdevices.ru/docs/ru/methodology/research/canvasapp); 10 | - common API with [Assistant Client](https://github.com/sberdevices/assistant-client); 11 | - runtime enitity variables and state validation; 12 | - nodejs web-frameworks integration support: [expressjs](https://github.com/expressjs), [hapi](https://github.com/hapijs/hapi), [koa](https://github.com/koajs/koa); 13 | - client frameworks integration support: [NextJS](https://github.com/vercel/next.js), [Gatsby](https://github.com/gatsbyjs); 14 | - any types of recognizers: RegExp, [String Similarity](https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient), [SmartApp Brain](https://developer.sberdevices.ru/docs/ru/developer_tools/ide/platform_ux/nlu_core_caila/nlu_core_caila); 15 | - custom recognizer API; 16 | - intents and entities sync with [SmartApp Brain](https://developer.sberdevices.ru/docs/ru/developer_tools/ide/platform_ux/nlu_core_caila/nlu_core_caila); 17 | - session persisting adapters: memory, mongodb, redis; 18 | - assistants based phrases dictionary declaration support. 19 | 20 | ## What's inside 21 | 22 | - [@salutejs/scenario](https://github.com/sberdevices/salutejs/tree/master/packages/scenario) - user scenario framework; 23 | - [@salutejs/recognizer-smartapp-brain](https://github.com/sberdevices/salutejs/tree/master/packages/recognizer-smartapp-brain) - SmartApp Brain recognizer; 24 | - [@salutejs/recognizer-string-similarity](https://github.com/sberdevices/salutejs/tree/master/packages/recognizer-string-similarity) - string similarity recognizer; 25 | - [@salutejs/storage-adapter-memory](https://github.com/sberdevices/salutejs/tree/master/packages/storage-adapter-memory) - in memory session storage adapter; 26 | 27 | ### Translations 28 | 29 | - [Русский](https://github.com/sberdevices/salutejs/blob/master/README.ru.md) 30 | 31 | #### SberDevices with :heart: 32 | -------------------------------------------------------------------------------- /README.ru.md: -------------------------------------------------------------------------------- 1 | ![salutejs](https://user-images.githubusercontent.com/982072/112627725-0606e400-8e43-11eb-86ef-a9e2fdcfc465.png) 2 | 3 | # SaluteJS 4 | 5 | __Набор утилит для описания пользовательских сценариев [семейства Виртуальных Ассистентов "Салют"](https://sber.ru/salute)__. 6 | 7 | - инструментированый код: автокомплишен по интентам и стейту приложения; 8 | - типизация из коробки: встроенные типы полностью включают в себя описание [SmartApp API](https://developer.sberdevices.ru/docs/ru/developer_tools/amp/smartappapi_description_and_guide); 9 | - единые типы команд между сценарием и [Canvas Apps](https://developer.sberdevices.ru/docs/ru/methodology/research/canvasapp); 10 | - единый формат API с [Assistant Client](https://github.com/sberdevices/assistant-client); 11 | - валидация переменных сущностей в райнтайме; 12 | - интеграция с любыми nodejs web-фреймворками: [expressjs](https://github.com/expressjs), [hapi](https://github.com/hapijs/hapi), [koa](https://github.com/koajs/koa); 13 | - интеграция с любыми клиентскими фреймворками: [NextJS](https://github.com/vercel/next.js), [Gatsby](https://github.com/gatsbyjs); 14 | - использование любых видов рекогнайзеров: RegExp, [String Similarity](https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient), [SmartApp Brain](https://developer.sberdevices.ru/docs/ru/developer_tools/ide/platform_ux/nlu_core_caila/nlu_core_caila); 15 | - API для создания своих рекогнайзеров; 16 | - синхронизация интентов и сущностей с [SmartApp Brain](https://developer.sberdevices.ru/docs/ru/developer_tools/ide/platform_ux/nlu_core_caila/nlu_core_caila); 17 | - адаптеры для работы с сессией: memory, mongodb, redis; 18 | - поддержка составления словарей реплик для всех персонажей. 19 | 20 | ## Состав пакетов 21 | 22 | - [@salutejs/scenario](https://github.com/sberdevices/salutejs/tree/master/packages/scenario) - фреймворк описания пользовательских сценариев; 23 | - [@salutejs/recognizer-smartapp-brain](https://github.com/sberdevices/salutejs/tree/master/packages/recognizer-smartapp-brain) - рекогнайзер SmartApp Brain; 24 | - [@salutejs/recognizer-string-similarity](https://github.com/sberdevices/salutejs/tree/master/packages/recognizer-string-similarity) - рекогнайзер string similarity; 25 | - [@salutejs/storage-adapter-memory](https://github.com/sberdevices/salutejs/tree/master/packages/storage-adapter-memory) - адаптер для хранения сессии в памяти процесса; 26 | 27 | #### SberDevices with :heart: 28 | -------------------------------------------------------------------------------- /examples/audiotour/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audiotour", 3 | "private": "true", 4 | "version": "1.24.0", 5 | "license": "Sber Public License at-nc-sa v.2", 6 | "author": "SberDevices Frontend Team ", 7 | "main": "index.js", 8 | "scripts": {}, 9 | "description": "", 10 | "dependencies": { 11 | "@salutejs/recognizer-smartapp-brain": "0.25.0", 12 | "@salutejs/scenario": "0.25.0", 13 | "@salutejs/storage-adapter-memory": "0.25.0", 14 | "dotenv": "8.2.0", 15 | "express": "4.17.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/audiotour/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import assert from 'assert'; 3 | import { config as dotEnv } from 'dotenv'; 4 | import { 5 | createUserScenario, 6 | createSystemScenario, 7 | createSaluteRequest, 8 | createSaluteResponse, 9 | createScenarioWalker, 10 | createMatchers, 11 | createIntents, 12 | } from '@salutejs/scenario'; 13 | import { SaluteMemoryStorage } from '@salutejs/storage-adapter-memory'; 14 | import { SmartAppBrainRecognizer } from '@salutejs/recognizer-smartapp-brain'; 15 | 16 | import model from './intents.json'; 17 | import config from './config'; 18 | import { IziRequest } from './types'; 19 | import { createLegacyAction, createLegacyGoToAction } from './legacyAction'; 20 | 21 | dotEnv(); 22 | 23 | const app = express(); 24 | app.use(express.json()); 25 | 26 | const intents = createIntents(model); 27 | 28 | const { match, intent, text, action, state, selectItem } = createMatchers(); 29 | 30 | const userScenario = createUserScenario({ 31 | ToMainPageFromMainPage: { 32 | match: match(intent('/Izi/ToMainPage'), state({ screen: 'Screen.MainPage' })), 33 | handle: ({ res }) => res.setPronounceText(config.message.TO_MAIN_PAGE.ON_MAIN_PAGE), 34 | }, 35 | ToMainPageFromTourPage: { 36 | match: match(intent('/Izi/ToMainPage'), state({ screen: 'Screen.TourPage' })), 37 | handle: ({ res }) => res.appendItem(createLegacyGoToAction('Screen.MainPage')), 38 | }, 39 | ToMainPage: { 40 | match: intent('/Izi/ToMainPage'), 41 | handle: ({ res }) => res.setPronounceText(config.message.TO_MAIN_PAGE.CONFIRMATION), 42 | children: { 43 | ToMainPageYes: { 44 | match: text('да'), 45 | handle: ({ res }) => res.appendItem(createLegacyGoToAction('Screen.MainPage')), 46 | }, 47 | ToMainPageNo: { 48 | match: text('нет'), 49 | handle: ({ res }) => res.setPronounceText('А жаль'), 50 | }, 51 | }, 52 | }, 53 | OpenItemIndex: { 54 | match: intent('/Navigation/OpenItemIndex'), 55 | handle: ({ req, res }) => { 56 | const { screen } = req.state; 57 | const number = Number(req.variables.number); 58 | 59 | if (screen === 'Screen.TourPage') { 60 | res.appendSuggestions(config.suggestions['Screen.TourStop']); 61 | } 62 | 63 | res.appendItem(createLegacyAction(selectItem({ number })(req))); 64 | }, 65 | }, 66 | RunAudioTour: { 67 | match: intent('/Izi/RunAudiotour'), 68 | handle: ({ res }) => 69 | res.appendItem( 70 | createLegacyAction({ 71 | action: { 72 | type: 'run_audiotour', 73 | }, 74 | }), 75 | ), 76 | }, 77 | Push: { 78 | match: intent('/Navigation/Push'), 79 | handle: ({ req, res }) => { 80 | const { screen } = req.state; 81 | const { UIElement, element } = req.variables; 82 | 83 | assert(typeof UIElement === 'string'); 84 | assert(typeof element === 'string'); 85 | 86 | const { id: uiElementId } = JSON.parse(UIElement); 87 | const { id: elementId } = JSON.parse(element); 88 | 89 | if (screen === 'Screen.TourStop' || (screen === 'Screen.TourPage' && elementId === 'run_audiotour')) { 90 | res.appendSuggestions(config.suggestions['Screen.TourStop']); 91 | } 92 | 93 | res.appendItem(createLegacyAction(selectItem({ id: uiElementId })(req))); 94 | }, 95 | }, 96 | ShowAllFromMainPage: { 97 | match: match(intent('/Izi/RunAudiotour'), state({ screen: 'Screen.MainPage' })), 98 | handle: ({ res }) => res.setPronounceText(config.message.PAGE_LOADED.ALL_ON_MAIN_PAGE), 99 | }, 100 | ShowAll: { 101 | match: match(intent('/Izi/ShowAll'), state({ screen: 'Screen.MainPage' })), 102 | handle: (_, dispatch) => dispatch(['ToMainPage']), 103 | }, 104 | SlotFillingIntent: { 105 | match: intent('/SlotFillingIntent'), 106 | handle: ({ res, req }) => res.setPronounceText(`Вы попросили ${req.variables.a} яблок`), 107 | children: { 108 | Hello: { 109 | match: text('привет'), 110 | handle: ({ res }) => { 111 | res.setPronounceText('привет и тебе'); 112 | }, 113 | }, 114 | }, 115 | }, 116 | EchoAction: { 117 | match: action('echo'), 118 | handle: ({ res, req }) => { 119 | const { phrase } = req.variables; 120 | assert(typeof phrase === 'string'); 121 | res.setPronounceText(phrase); 122 | }, 123 | }, 124 | }); 125 | 126 | const systemScenario = createSystemScenario({ 127 | RUN_APP: ({ res }) => { 128 | res.appendSuggestions(config.suggestions['Screen.MainPage']); 129 | res.setPronounceText(config.message.HELLO); 130 | }, 131 | NO_MATCH: ({ res }) => { 132 | res.setPronounceText('Я не понимаю'); 133 | res.appendBubble('Я не понимаю'); 134 | }, 135 | }); 136 | 137 | const scenarioWalker = createScenarioWalker({ 138 | intents, 139 | recognizer: new SmartAppBrainRecognizer(process.env.ACCESS_TOKEN, process.env.SMARTAPP_BRAIN_HOST), 140 | systemScenario, 141 | userScenario, 142 | }); 143 | 144 | const storage = new SaluteMemoryStorage(); 145 | 146 | app.post('/hook', async ({ body }, response) => { 147 | const req = createSaluteRequest(body); 148 | const res = createSaluteResponse(body); 149 | 150 | const sessionId = body.uuid.userId; 151 | const session = await storage.resolve(sessionId); 152 | 153 | await scenarioWalker({ req, res, session }); 154 | await storage.save({ id: sessionId, session }); 155 | 156 | response.status(200).json(res.message); 157 | }); 158 | 159 | app.listen(4000, () => { 160 | console.log('Listening on 4000'); 161 | }); 162 | -------------------------------------------------------------------------------- /examples/audiotour/src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | screen: { 3 | MainPage: 'Screen.MainPage', 4 | TourPage: 'Screen.TourPage', 5 | TourStop: 'Screen.TourStop', 6 | }, 7 | message: { 8 | PAGE_LOADED: { 9 | ALL_ON_MAIN_PAGE: 'Это пока все аудиогиды. Скоро появятся новые.', 10 | TourPage: 'Открываю для вас аудиогид', 11 | }, 12 | ENTITY_NOT_FOUND: 'Пока нет такого аудиогида', 13 | TO_MAIN_PAGE: { 14 | ON_MAIN_PAGE: 'Мы уже на главной', 15 | CONFIRMATION: 'Вы уверены, что хотите перейти на главную?', 16 | }, 17 | TO_EXHIBIT_PAGE: { 18 | CONFIRMATION: 'Вы хотите поставить на паузу?', 19 | }, 20 | HELLO: 'Антоша Лапушка', 21 | }, 22 | suggestions: { 23 | 'Screen.MainPage': ['Открой первую экскурсию'], 24 | 'Screen.TourPage': ['Начнем экскурсию', 'Покажи содержание', 'Все экскурсии'], 25 | 'Screen.TourStop': ['Play', 'Пауза', 'Следующая остановка', 'Выйти из прослушивания'], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /examples/audiotour/src/legacyAction.ts: -------------------------------------------------------------------------------- 1 | import { IziAppState } from './types'; 2 | 3 | /** @deprecated */ 4 | export const createLegacyAction = (item: any) => { 5 | return { 6 | command: { 7 | type: 'smart_app_data', 8 | action: { 9 | ...(item.action || {}), 10 | payload: { id: item.id, number: item.number, ...item.action?.payload }, 11 | }, 12 | }, 13 | }; 14 | }; 15 | 16 | export const createLegacyGoToAction = (screen: IziAppState['screen']) => 17 | createLegacyAction({ 18 | action: { 19 | type: 'GOTO', 20 | payload: { 21 | screen, 22 | }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /examples/audiotour/src/types.ts: -------------------------------------------------------------------------------- 1 | import { AppState, SaluteRequest, SaluteHandler, SaluteRequestVariable } from '@salutejs/scenario'; 2 | 3 | export interface IziAppState extends AppState { 4 | screen?: 'Screen.MainPage' | 'Screen.TourPage' | 'Screen.TourStop'; 5 | } 6 | 7 | export interface IziItentsVariables extends SaluteRequestVariable { 8 | UIElement?: string; 9 | element?: string; 10 | number?: string; 11 | a?: string; 12 | phrase?: string; 13 | } 14 | 15 | export type IziRequest = SaluteRequest; 16 | export type IziHandler = SaluteHandler; 17 | -------------------------------------------------------------------------------- /examples/audiotour/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "isolatedModules": true, 7 | "jsx": "preserve", 8 | "lib": [ 9 | "dom", 10 | "es2017" 11 | ], 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "noEmit": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "strict": false, 21 | "target": "es6", 22 | }, 23 | "exclude": [ 24 | "node_modules" 25 | ], 26 | "include": [ 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/chatapp-brain/.env.sample: -------------------------------------------------------------------------------- 1 | SMARTAPP_BRAIN_TOKEN= -------------------------------------------------------------------------------- /examples/chatapp-brain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatapp-brain", 3 | "private": "true", 4 | "version": "1.24.0", 5 | "license": "Sber Public License at-nc-sa v.2", 6 | "author": "SberDevices Frontend Team ", 7 | "main": "index.js", 8 | "scripts": { 9 | "start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' ./src/index.ts" 10 | }, 11 | "description": "", 12 | "dependencies": { 13 | "@salutejs/recognizer-smartapp-brain": "0.25.0", 14 | "@salutejs/scenario": "0.25.0", 15 | "@salutejs/storage-adapter-memory": "0.25.0", 16 | "dotenv": "8.2.0", 17 | "express": "4.17.1" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "4.17.11", 21 | "@types/node": "15.0.1", 22 | "nodemon": "2.0.7", 23 | "ts-node": "9.1.1", 24 | "typescript": "4.2.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/chatapp-brain/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { config as dotenv } from 'dotenv'; 3 | import { createIntents, createSystemScenario, createUserScenario, createMatchers } from '@salutejs/scenario'; 4 | import { SmartAppBrainRecognizer } from '@salutejs/recognizer-smartapp-brain'; 5 | import { SaluteMemoryStorage } from '@salutejs/storage-adapter-memory'; 6 | 7 | import { saluteExpressMiddleware } from './middleware'; 8 | import * as dictionary from './system.i18n'; 9 | import model from './intents.json'; 10 | 11 | dotenv(); 12 | 13 | const port = process.env.PORT || 3000; 14 | const app = express(); 15 | app.use(express.json()); 16 | 17 | const { intent } = createMatchers(); 18 | 19 | app.post( 20 | '/app-connector', 21 | saluteExpressMiddleware({ 22 | intents: createIntents(model.intents), 23 | recognizer: new SmartAppBrainRecognizer(), 24 | systemScenario: createSystemScenario({ 25 | RUN_APP: ({ req, res }) => { 26 | const keyset = req.i18n(dictionary); 27 | res.setPronounceText(keyset('Привет')); 28 | }, 29 | NO_MATCH: ({ req, res }) => { 30 | const keyset = req.i18n(dictionary); 31 | res.setPronounceText(keyset('404')); 32 | }, 33 | }), 34 | userScenario: createUserScenario({ 35 | calc: { 36 | match: intent('/sum'), 37 | handle: ({ req, res }) => { 38 | const keyset = req.i18n(dictionary); 39 | const { num1, num2 } = req.variables; 40 | 41 | res.setPronounceText( 42 | keyset('{result}. Это было легко!', { 43 | result: Number(num1) + Number(num2), 44 | }), 45 | ); 46 | }, 47 | }, 48 | }), 49 | storage: new SaluteMemoryStorage(), 50 | }), 51 | ); 52 | 53 | app.listen(port, () => console.log(`Salute on ${port}`)); 54 | -------------------------------------------------------------------------------- /examples/chatapp-brain/src/intents.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": { 3 | "/sum": { 4 | "matchers": [ 5 | { "type": "phrase", "rule": "Сложи @duckling.number и @duckling.number" }, 6 | { "type": "phrase", "rule": "Сколько будет @duckling.number и @duckling.number" } 7 | ], 8 | "variables": { 9 | "num1": { 10 | "required": true, 11 | "questions": [ 12 | "Что с чем?", 13 | "Мне нужно больше чисел!" 14 | ], 15 | "entity": "duckling.number" 16 | }, 17 | "num2": { 18 | "required": true, 19 | "questions": [ 20 | "А какое второе число?" 21 | ], 22 | "entity": "duckling.number" 23 | } 24 | } 25 | } 26 | }, 27 | "entities": {} 28 | } 29 | -------------------------------------------------------------------------------- /examples/chatapp-brain/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createScenarioWalker, createSaluteRequest, createSaluteResponse } from '@salutejs/scenario'; 2 | 3 | export const saluteExpressMiddleware = ({ intents, recognizer, userScenario, systemScenario, storage }) => { 4 | const scenarioWalker = createScenarioWalker({ intents, recognizer, systemScenario, userScenario }); 5 | 6 | return async ({ body }, httpRes) => { 7 | const req = createSaluteRequest(body); 8 | const res = createSaluteResponse(body); 9 | const id = body.uuid.userId; 10 | const session = await storage.resolve(id); 11 | 12 | await scenarioWalker({ req, res, session }); 13 | await storage.save({ id, session }); 14 | 15 | httpRes.json(res.message); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /examples/chatapp-brain/src/system.i18n/athena.ts: -------------------------------------------------------------------------------- 1 | export const athena = { 2 | Привет: 'Привет любимому пользователю!', 3 | 404: 'Кажется, я не расслышала', 4 | '{result}. Это было легко!': '{result}. Это было легко!', 5 | }; 6 | -------------------------------------------------------------------------------- /examples/chatapp-brain/src/system.i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sber'; 2 | export * from './joy'; 3 | export * from './athena'; 4 | -------------------------------------------------------------------------------- /examples/chatapp-brain/src/system.i18n/joy.ts: -------------------------------------------------------------------------------- 1 | export const joy = { 2 | Привет: 'Приветик, я так соскучилась!', 3 | 404: 'Не поняла', 4 | '{result}. Это было легко!': '{result}. Это было легко!', 5 | }; 6 | -------------------------------------------------------------------------------- /examples/chatapp-brain/src/system.i18n/sber.ts: -------------------------------------------------------------------------------- 1 | export const sber = { 2 | Привет: 'Привет, дорогой разработчик!', 3 | 404: 'Я не понял', 4 | '{result}. Это было легко!': '{result}. Это было легко!', 5 | }; 6 | -------------------------------------------------------------------------------- /examples/chatapp-brain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["es2017"], 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "resolveJsonModule": true, 11 | "moduleResolution": "node", 12 | "typeRoots": [ "./src/types", "./node_modules/@types" ] 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ] 17 | } -------------------------------------------------------------------------------- /examples/echo/.env.sample: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_DEV_TOKEN= 2 | NEXT_PUBLIC_DEV_PHRASE= -------------------------------------------------------------------------------- /examples/echo/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echo", 3 | "private": "true", 4 | "version": "0.22.0", 5 | "main": "index.js", 6 | "author": "SberDevices Frontend Team ", 7 | "license": "Sber Public License at-nc-sa v.2", 8 | "scripts": { 9 | "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' ./src/server.ts" 10 | }, 11 | "dependencies": { 12 | "@salutejs/scenario": "0.25.0", 13 | "@salutejs/storage-adapter-memory": "0.25.0", 14 | "@sberdevices/assistant-client": "2.15.1", 15 | "@sberdevices/plasma-icons": "1.12.0", 16 | "@sberdevices/plasma-tokens": "1.3.0", 17 | "@sberdevices/plasma-ui": "1.15.0", 18 | "express": "4.17.1", 19 | "next": "11.1.1", 20 | "react": "16.14.0", 21 | "react-dom": "16.14.0", 22 | "socket.io": "4.1.2", 23 | "socket.io-client": "4.1.2", 24 | "styled-components": "5.3.0" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "12.20.13", 28 | "@types/react": "16.14.6", 29 | "@types/react-dom": "16.9.13", 30 | "nodemon": "2.0.7", 31 | "ts-node": "9.1.1", 32 | "typescript": "4.2.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/echo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sberdevices/salutejs/33e90ce91d6bf76946d0ce8ff0d7ec07d21ea784/examples/echo/public/favicon.ico -------------------------------------------------------------------------------- /examples/echo/src/Components/GlobalStyles.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from 'react'; 2 | import { createGlobalStyle } from 'styled-components'; 3 | import { darkSber, darkEva, darkJoy } from '@sberdevices/plasma-tokens/themes'; 4 | import { 5 | text, // Цвет текста 6 | background, // Цвет подложки 7 | gradient, // Градиент 8 | } from '@sberdevices/plasma-tokens'; 9 | import { AssistantCharacterType } from '@sberdevices/assistant-client'; 10 | 11 | const themes = { 12 | sber: createGlobalStyle(darkEva), 13 | eva: createGlobalStyle(darkSber), 14 | joy: createGlobalStyle(darkJoy), 15 | }; 16 | 17 | const DocumentStyle = createGlobalStyle` 18 | html:root { 19 | min-height: 100vh; 20 | color: ${text}; 21 | background-color: ${background}; 22 | background-image: ${gradient}; 23 | } 24 | `; 25 | 26 | export const GlobalStyles: FC<{ character: AssistantCharacterType }> = ({ character }) => { 27 | const Theme = useMemo(() => themes[character], [character]); 28 | return ( 29 | <> 30 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /examples/echo/src/hooks/useScenario.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { 3 | createMatchers, 4 | createSaluteRequest, 5 | createSaluteResponse, 6 | createScenarioWalker, 7 | createSystemScenario, 8 | createUserScenario, 9 | NLPRequest, 10 | NLPResponse, 11 | SaluteHandler, 12 | SaluteRequest, 13 | } from '@salutejs/scenario'; 14 | import { SaluteMemoryStorage } from '@salutejs/storage-adapter-memory'; 15 | 16 | import { useTransport } from './useTransport'; 17 | 18 | const addition: SaluteHandler = ({ req, res }) => { 19 | const { tokenized_elements_list } = req.message; 20 | 21 | const result = tokenized_elements_list.reduce((acc, e, i) => { 22 | if (e.token_type !== 'NUM_TOKEN' || typeof e.token_value?.value !== 'number') { 23 | return acc; 24 | } 25 | 26 | return ( 27 | acc + 28 | e.token_value.value * 29 | (i > 0 && 30 | (tokenized_elements_list[i - 1].text === '-' || tokenized_elements_list[i - 1].text === 'минус') 31 | ? -1 32 | : 1) 33 | ); 34 | }, 0); 35 | 36 | res.setPronounceText(`Результат ${result}`); 37 | }; 38 | 39 | const { regexp } = createMatchers(); 40 | 41 | const userScenario = createUserScenario({ 42 | add: { 43 | match: regexp(/^сложить ((- |минус )?\d+) и ((- |минус )?\d+)$/i), 44 | handle: addition, 45 | }, 46 | }); 47 | 48 | const systemScenario = createSystemScenario({ 49 | RUN_APP: ({ res }) => { 50 | res.setPronounceText('Дай любое задание'); 51 | res.appendSuggestions(['сложи 5 и - 2']); 52 | }, 53 | }); 54 | 55 | const scenarioWalker = createScenarioWalker({ 56 | systemScenario, 57 | userScenario, 58 | }); 59 | 60 | const storage = new SaluteMemoryStorage(); 61 | 62 | export const useScenario = () => { 63 | const handleRequest = useCallback(async (request: NLPRequest): Promise => { 64 | const req = createSaluteRequest(request); 65 | const res = createSaluteResponse(request); 66 | 67 | const sessionId = request.uuid.userId; 68 | const session = await storage.resolve(sessionId); 69 | 70 | await scenarioWalker({ req, res, session }); 71 | await storage.save({ id: sessionId, session }); 72 | 73 | return res.message; 74 | }, []); 75 | 76 | useTransport(async (request, sendResponse) => { 77 | sendResponse(await handleRequest(request)); 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /examples/echo/src/hooks/useTransport.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import io from 'socket.io-client'; 3 | import { NLPRequest, NLPResponse } from '@salutejs/scenario'; 4 | 5 | const ioSocket = io(); 6 | 7 | export const useTransport = (cb: (request: NLPRequest, sendRequest: (response: NLPResponse) => void) => void) => { 8 | useEffect(() => { 9 | ioSocket.on('incoming', (request: NLPRequest) => { 10 | cb(request, (response: NLPResponse) => { 11 | ioSocket.emit('outcoming', response); 12 | }); 13 | }); 14 | 15 | return function useSocketCleanup() { 16 | ioSocket.off('incoming', cb); 17 | }; 18 | }, [cb]); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/echo/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | 3 | const CustomApp = ({ Component, pageProps }: AppProps) => { 4 | if (!process.browser) { 5 | return null; 6 | } 7 | return ; 8 | }; 9 | 10 | export default CustomApp; 11 | -------------------------------------------------------------------------------- /examples/echo/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { Container, DeviceThemeProvider } from '@sberdevices/plasma-ui'; 3 | import { 4 | AssistantCharacterType, 5 | AssistantAppState, 6 | AssistantNavigationCommand, 7 | createAssistant, 8 | createSmartappDebugger, 9 | AssistantClientCommand, 10 | } from '@sberdevices/assistant-client'; 11 | 12 | import { GlobalStyles } from '../Components/GlobalStyles'; 13 | import { useScenario } from '../hooks/useScenario'; 14 | 15 | const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; 16 | // eslint-disable-next-line prefer-destructuring 17 | const NEXT_PUBLIC_DEV_TOKEN = process.env.NEXT_PUBLIC_DEV_TOKEN; 18 | // eslint-disable-next-line prefer-destructuring 19 | const NEXT_PUBLIC_DEV_PHRASE = process.env.NEXT_PUBLIC_DEV_PHRASE; 20 | 21 | const IndexPage = () => { 22 | const [character, setCharacter] = useState('sber' as const); 23 | 24 | const assistantStateRef = useRef({}); 25 | const assistantRef = useRef>(); 26 | 27 | useScenario(); 28 | 29 | useEffect(() => { 30 | const initializeAssistant = () => { 31 | if (!IS_DEVELOPMENT) { 32 | return createAssistant({ 33 | getState: () => assistantStateRef.current, 34 | }); 35 | } 36 | 37 | if (!NEXT_PUBLIC_DEV_TOKEN || !NEXT_PUBLIC_DEV_PHRASE) { 38 | throw new Error(''); 39 | } 40 | 41 | return createSmartappDebugger({ 42 | token: NEXT_PUBLIC_DEV_TOKEN, 43 | initPhrase: NEXT_PUBLIC_DEV_PHRASE, 44 | getState: () => assistantStateRef.current, 45 | }); 46 | }; 47 | 48 | const assistant = initializeAssistant(); 49 | 50 | assistant.on('data', (command: AssistantClientCommand) => { 51 | let navigation: AssistantNavigationCommand['navigation']; 52 | switch (command.type) { 53 | case 'character': 54 | setCharacter(command.character.id); 55 | // 'sber' | 'eva' | 'joy'; 56 | break; 57 | case 'navigation': 58 | navigation = (command as AssistantNavigationCommand).navigation; 59 | break; 60 | case 'smart_app_data': 61 | // dispatch(command.smart_app_data); 62 | break; 63 | default: 64 | break; 65 | } 66 | 67 | if (navigation) { 68 | switch (navigation.command) { 69 | case 'UP': 70 | window.scrollTo(0, window.scrollY - 500); 71 | break; 72 | case 'DOWN': 73 | window.scrollTo(0, window.scrollY + 500); 74 | break; 75 | default: 76 | break; 77 | } 78 | } 79 | }); 80 | 81 | assistantRef.current = assistant; 82 | }, []); 83 | 84 | return ( 85 | 86 | 87 | 88 | 89 | ); 90 | }; 91 | 92 | export default IndexPage; 93 | -------------------------------------------------------------------------------- /examples/echo/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Server } from 'http'; 3 | import { Server as SocketServer } from 'socket.io'; 4 | import type { Socket } from 'socket.io'; 5 | import next from 'next'; 6 | import { NLPRequest, NLPResponse } from '@salutejs/scenario'; 7 | 8 | const app = express(); 9 | const server = new Server(app); 10 | const io = new SocketServer(server); 11 | let socket: Socket; 12 | 13 | app.use(express.json()); 14 | 15 | const port = parseInt(process.env.PORT, 10) || 3000; 16 | const dev = process.env.NODE_ENV !== 'production'; 17 | const nextApp = next({ dev }); 18 | const nextHandler = nextApp.getRequestHandler(); 19 | 20 | const requestMap: Record void> = {}; 21 | 22 | // socket.io server 23 | io.on('connection', (ioSocket) => { 24 | socket = ioSocket; 25 | 26 | ioSocket.on('outcoming', (data: NLPResponse) => { 27 | requestMap[data.messageId](data); 28 | delete requestMap[data.messageId]; 29 | }); 30 | }); 31 | 32 | nextApp.prepare().then(() => { 33 | app.post('/hook', async ({ body }: { body: NLPRequest }, response) => { 34 | const answer: NLPResponse = await new Promise((resolve) => { 35 | requestMap[body.messageId] = resolve; 36 | socket.emit('incoming', body); 37 | }); 38 | 39 | response.status(200).json(answer); 40 | }); 41 | 42 | app.get('*', (req, res) => { 43 | return nextHandler(req, res); 44 | }); 45 | 46 | server.listen(port); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/echo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "isolatedModules": true, 7 | "jsx": "preserve", 8 | "lib": [ 9 | "dom", 10 | "es2017" 11 | ], 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "noEmit": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "strict": false, 21 | "target": "es6", 22 | }, 23 | "exclude": [ 24 | "node_modules" 25 | ], 26 | "include": [ 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/payment-chatapp/.env.sample: -------------------------------------------------------------------------------- 1 | SMARTPAY_TOKEN= 2 | SMARTPAY_SERVICEID= -------------------------------------------------------------------------------- /examples/payment-chatapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payment-chatapp", 3 | "private": "true", 4 | "version": "0.23.0", 5 | "license": "Sber Public License at-nc-sa v.2", 6 | "author": "SberDevices Frontend Team ", 7 | "main": "src/index.js", 8 | "scripts": { 9 | "start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' ./src/index.ts" 10 | }, 11 | "dependencies": { 12 | "@salutejs/scenario": "0.25.0", 13 | "@salutejs/storage-adapter-memory": "0.25.0", 14 | "dotenv": "8.2.0", 15 | "express": "4.17.1" 16 | }, 17 | "devDependencies": { 18 | "@types/express": "4.17.11", 19 | "@types/node": "15.0.1", 20 | "nodemon": "2.0.7", 21 | "ts-node": "9.1.1", 22 | "typescript": "4.2.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/payment-chatapp/src/cards.ts: -------------------------------------------------------------------------------- 1 | import { GalleryCard, GalleryItem, LeftRightCellView, ListCard, PaymentInvoice } from '@salutejs/scenario'; 2 | import assert from 'assert'; 3 | 4 | import { Product } from './types'; 5 | 6 | export const createGalleryCard = (products: Array): GalleryCard => { 7 | assert(products.length >= 1); 8 | 9 | return { 10 | type: 'gallery_card', 11 | items: products.map( 12 | (product): GalleryItem => ({ 13 | type: 'media_gallery_item', 14 | image: { 15 | url: product.image, 16 | size: { 17 | width: 'small', 18 | aspect_ratio: 1, 19 | }, 20 | }, 21 | margins: { 22 | top: '4x', 23 | left: '6x', 24 | right: '6x', 25 | bottom: '5x', 26 | }, 27 | bottom_text: { 28 | text: `${product.price / 100} р`, 29 | typeface: 'caption', 30 | text_color: 'secondary', 31 | max_lines: 1, 32 | margins: { 33 | top: '2x', 34 | }, 35 | }, 36 | top_text: { 37 | text: product.title, 38 | typeface: 'footnote1', 39 | text_color: 'default', 40 | max_lines: 2, 41 | }, 42 | }), 43 | ) as [GalleryItem, ...GalleryItem[]], 44 | }; 45 | }; 46 | 47 | export const createOrderBundle = (bundle: PaymentInvoice['order']['order_bundle']): ListCard => { 48 | return { 49 | type: 'list_card', 50 | paddings: { 51 | top: '9x', 52 | bottom: '12x', 53 | left: '8x', 54 | right: '8x', 55 | }, 56 | cells: [ 57 | { 58 | type: 'text_cell_view', 59 | content: { 60 | text: 'Корзина', 61 | typeface: 'headline3', 62 | text_color: 'default', 63 | }, 64 | paddings: { 65 | bottom: '2x', 66 | }, 67 | }, 68 | ...bundle.map( 69 | (item): LeftRightCellView => ({ 70 | type: 'left_right_cell_view', 71 | paddings: { 72 | top: '6x', 73 | bottom: '6x', 74 | }, 75 | divider: { 76 | style: 'default', 77 | size: 'd1', 78 | }, 79 | left: { 80 | type: 'simple_left_view', 81 | texts: { 82 | title: { 83 | text: item.name, 84 | typeface: 'caption', 85 | text_color: 'default', 86 | max_lines: 0, 87 | }, 88 | subtitle: { 89 | text: `${item.quantity.value} ${item.quantity.measure}`, 90 | typeface: 'caption', 91 | text_color: 'secondary', 92 | max_lines: 0, 93 | }, 94 | }, 95 | }, 96 | right: { 97 | type: 'detail_right_view', 98 | detail: { 99 | text: `${item.item_amount / 100} р`, 100 | typeface: 'footnote2', 101 | text_color: 'default', 102 | max_lines: 0, 103 | }, 104 | }, 105 | }), 106 | ), 107 | { 108 | type: 'left_right_cell_view', 109 | paddings: { 110 | top: '10x', 111 | }, 112 | left: { 113 | type: 'simple_left_view', 114 | texts: { 115 | title: { 116 | text: 'Итого', 117 | typeface: 'footnote1', 118 | text_color: 'secondary', 119 | }, 120 | }, 121 | }, 122 | right: { 123 | type: 'detail_right_view', 124 | detail: { 125 | text: `${bundle.reduce((acc, item) => acc + item.item_amount, 0) / 100} р`, 126 | typeface: 'footnote2', 127 | text_color: 'default', 128 | max_lines: 0, 129 | }, 130 | }, 131 | }, 132 | ], 133 | }; 134 | }; 135 | -------------------------------------------------------------------------------- /examples/payment-chatapp/src/handlers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createInvoice, 3 | PayDialogFinishedServerAction, 4 | PayDialogStatuses, 5 | PaymentInvoice, 6 | SaluteHandler, 7 | SaluteRequest, 8 | TaxSystemTypes, 9 | } from '@salutejs/scenario'; 10 | 11 | import { createGalleryCard, createOrderBundle } from './cards'; 12 | import { PaymentState, Product, ProductVariable } from './types'; 13 | import products from './products.json'; 14 | 15 | const productMap: Record = products.reduce((acc, p) => { 16 | acc[p.code] = p; 17 | return acc; 18 | }, {}); 19 | 20 | export const runApp: SaluteHandler = ({ res }) => { 21 | res.appendCard(createGalleryCard(products)); 22 | res.appendSuggestions(['Хочу большую', 'Хочу маленькую']); 23 | }; 24 | 25 | export const appendHandler: SaluteHandler, PaymentState> = ({ req, res, session }) => { 26 | const { product } = req.variables; 27 | const found = products.find((p) => p.synonyms.some((s) => s.toLowerCase() === product.toLowerCase())); 28 | if (!found) { 29 | return; 30 | } 31 | 32 | if (!session.cart) { 33 | session.cart = {}; 34 | } 35 | 36 | if (!session.cart[found.code]) { 37 | session.cart[found.code] = 0; 38 | } 39 | 40 | session.cart[found.code] += 1; 41 | res.setPronounceText('Добавлено'); 42 | res.appendBubble('Добавлено'); 43 | res.appendSuggestions(['В корзину']); 44 | }; 45 | 46 | export const removeHandler: SaluteHandler, PaymentState> = ({ req, res, session }) => { 47 | const { product } = req.variables; 48 | const found = products.find((p) => p.synonyms.some((s) => s.toLowerCase() === product.toLowerCase())); 49 | if (!found) { 50 | return; 51 | } 52 | 53 | if (!session.cart) { 54 | session.cart = {}; 55 | } 56 | 57 | if (!session.cart[found.code]) { 58 | session.cart[found.code] = 0; 59 | } 60 | 61 | session.cart[found.code] = session.cart[found.code] > 0 ? session.cart[found.code] - 1 : 0; 62 | res.setPronounceText('Удалено'); 63 | res.appendBubble('Удалено'); 64 | }; 65 | 66 | export const cartHandler: SaluteHandler = ({ res, session }) => { 67 | const items: PaymentInvoice['order']['order_bundle'] = []; 68 | const cartItems = Object.keys(session.cart || {}); 69 | let index = 0; 70 | cartItems.forEach((item) => { 71 | if (session.cart[item] <= 0) { 72 | return; 73 | } 74 | 75 | items.push({ 76 | position_id: index++, 77 | name: productMap[item].title, 78 | item_code: productMap[item].code, 79 | item_price: Number(productMap[item].price), 80 | item_amount: session.cart[item] * productMap[item].price, 81 | quantity: { 82 | value: session.cart[item], 83 | measure: 'шт.', 84 | }, 85 | currency: 'RUB', 86 | tax_type: 0, 87 | tax_sum: 0, 88 | }); 89 | }); 90 | 91 | if (items.length <= 0) { 92 | res.appendBubble('Корзина пустая'); 93 | res.setPronounceText('Корзина пустая'); 94 | return; 95 | } 96 | 97 | session.bundle = items; 98 | res.appendCard(createOrderBundle(items)); 99 | res.appendSuggestions(['Оплатить', 'Продолжить']); 100 | }; 101 | 102 | export const paymentHandler: SaluteHandler = async ({ res, session }) => { 103 | const { bundle } = session; 104 | 105 | const { invoice_id, error } = await createInvoice({ 106 | invoice: { 107 | order: { 108 | order_id: new Date().getTime().toString(), 109 | order_date: `${new Date().toISOString().split('.')[0]}+03:00`, 110 | currency: 'RUB', 111 | language: 'ru-RU', 112 | service_id: process.env.SMARTPAY_SERVICEID || '', 113 | purpose: 'ПРОДАВЕЦ ВОДЫ', 114 | description: 'Заказ воды', 115 | amount: bundle.reduce((acc, item) => acc + item.item_amount, 0), 116 | tax_system: TaxSystemTypes.obshii, 117 | order_bundle: [...bundle], 118 | }, 119 | }, 120 | }); 121 | 122 | if (typeof invoice_id === 'undefined') { 123 | res.appendBubble(error.error_description); 124 | return; 125 | } 126 | 127 | res.askPayment(invoice_id); 128 | }; 129 | 130 | export const payDialogFinished: SaluteHandler = ({ req, res, session }) => { 131 | const { parameters } = (req.serverAction as unknown) as PayDialogFinishedServerAction; 132 | if (parameters.payment_response.response_code === PayDialogStatuses.success) { 133 | res.setPronounceText('Оплачено'); 134 | res.appendBubble('Оплачено'); 135 | session.cart = {}; 136 | session.bundle = []; 137 | } else { 138 | res.setPronounceText(`Ошибка ${parameters.payment_response.response_code}`); 139 | res.appendBubble(`Ошибка ${parameters.payment_response.response_code}`); 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /examples/payment-chatapp/src/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import { 3 | createIntents, 4 | createMatchers, 5 | createSaluteRequest, 6 | createSaluteResponse, 7 | createScenarioWalker, 8 | createSystemScenario, 9 | createUserScenario, 10 | SaluteRequest, 11 | } from '@salutejs/scenario'; 12 | import { SaluteMemoryStorage } from '@salutejs/storage-adapter-memory'; 13 | import { config as dotEnv } from 'dotenv'; 14 | 15 | import { appendHandler, cartHandler, payDialogFinished, paymentHandler, removeHandler, runApp } from './handlers'; 16 | import intents from './intents'; 17 | 18 | dotEnv(); 19 | 20 | const { regexp } = createMatchers(); 21 | 22 | const scenarioWalker = createScenarioWalker({ 23 | intents: createIntents(intents), 24 | systemScenario: createSystemScenario({ 25 | RUN_APP: runApp, 26 | PAY_DIALOG_FINISHED: payDialogFinished, 27 | }), 28 | userScenario: createUserScenario({ 29 | append: { 30 | match: regexp( 31 | new RegExp(`^(${intents.append.matchers.map((m) => m.rule).join('|')}) (?.+)$`, 'i'), 32 | ), 33 | handle: appendHandler, 34 | }, 35 | remove: { 36 | match: regexp( 37 | new RegExp(`^(${intents.remove.matchers.map((m) => m.rule).join('|')}) (?.+)$`, 'i'), 38 | ), 39 | handle: removeHandler, 40 | }, 41 | cart: { 42 | match: regexp(new RegExp(`^(${intents.cart.matchers.map((m) => m.rule).join('|')})$`, 'i')), 43 | handle: cartHandler, 44 | children: { 45 | payment: { 46 | match: regexp(new RegExp(`^(${intents.payment.matchers.map((m) => m.rule).join('|')})$`, 'i')), 47 | handle: paymentHandler, 48 | }, 49 | }, 50 | }, 51 | }), 52 | }); 53 | 54 | const app = express(); 55 | app.use(express.json()); 56 | 57 | const storage = new SaluteMemoryStorage(); 58 | 59 | app.post('/app-connector', async (request: Request, response: Response) => { 60 | const req = createSaluteRequest(request.body); 61 | const res = createSaluteResponse(request.body); 62 | 63 | const sessionId = request.body.uuid.userId; 64 | const session = await storage.resolve(sessionId); 65 | 66 | await scenarioWalker({ req, res, session }); 67 | await storage.save({ id: sessionId, session }); 68 | 69 | response.status(200).json(res.message); 70 | }); 71 | 72 | app.listen(3000); 73 | -------------------------------------------------------------------------------- /examples/payment-chatapp/src/intents.ts: -------------------------------------------------------------------------------- 1 | import { IntentsDict } from '@salutejs/scenario'; 2 | 3 | const convertToNewFormatMatcher = (rule: string) => { 4 | return { 5 | type: 'phrase' as const, 6 | rule, 7 | }; 8 | }; 9 | 10 | const intents: IntentsDict = { 11 | append: { 12 | matchers: ['добавить', 'хотеть'].map(convertToNewFormatMatcher), 13 | variables: { 14 | product: { 15 | required: true, 16 | }, 17 | }, 18 | }, 19 | remove: { 20 | matchers: ['удалить', 'убрать', 'не хотеть'].map(convertToNewFormatMatcher), 21 | variables: { 22 | product: { 23 | required: true, 24 | }, 25 | }, 26 | }, 27 | cart: { 28 | matchers: ['в корзина', 'показать корзина', 'перейти в корзина', 'готово'].map(convertToNewFormatMatcher), 29 | }, 30 | payment: { 31 | matchers: ['продолжить', 'оплатить'].map(convertToNewFormatMatcher), 32 | }, 33 | }; 34 | 35 | export default intents; 36 | -------------------------------------------------------------------------------- /examples/payment-chatapp/src/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "code": "1", 4 | "title": "BonAqua Вода чистая питьевая негазированная, 2 л", 5 | "image": "https://cdn1.ozone.ru/multimedia/wc1200/1019658022.jpg", 6 | "price": 8300, 7 | "synonyms": ["большая", "бонаква", "bonaqua", "вода 2 литра", "первая", "1"] 8 | }, 9 | { 10 | "code": "2", 11 | "title": "Вода питьевая Baikal 430 глубинная Байкальская негазированная, 0.85 л", 12 | "image": 13 | "https://www.waterbaikal.ru/image/cache/catalog/Bajkalskayavoda/Baikal430m/0.45/baikal_430_water-720x480.jpg", 14 | "price": 5200, 15 | "synonyms": ["маленький", "байкальская", "вода байкал", "байкал", "вторая", "2"] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /examples/payment-chatapp/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Currency, PaymentInvoice, TaxSystemTypes, SaluteRequestVariable, AppState } from '@salutejs/scenario'; 2 | 3 | export interface PayDialogInitSA { 4 | type: 'PAY_DIALOG_INIT'; 5 | payload: { 6 | items: PaymentInvoice['order']['order_bundle']; 7 | amount: number; 8 | tax_system: TaxSystemTypes; 9 | currency: Currency; 10 | }; 11 | } 12 | 13 | export interface Product { 14 | code: string; 15 | title: string; 16 | image: string; 17 | price: number; 18 | synonyms: Array; 19 | } 20 | 21 | export interface ProductVariable extends SaluteRequestVariable { 22 | product: string; 23 | } 24 | 25 | export interface PaymentState extends AppState { 26 | cart?: Record; 27 | bundle?: PaymentInvoice['order']['order_bundle']; 28 | } 29 | -------------------------------------------------------------------------------- /examples/payment-chatapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["es2017"], 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "resolveJsonModule": true, 11 | "moduleResolution": "node", 12 | "typeRoots": [ "./src/types", "./node_modules/@types" ] 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ] 17 | } -------------------------------------------------------------------------------- /examples/todo/.env.sample: -------------------------------------------------------------------------------- 1 | # Копируем токен с этой страницы https://developers.sber.ru/studio/settings/emulator 2 | NEXT_PUBLIC_DEV_TOKEN= 3 | # Запусти + Активационное имя (Запусти мой апп) 4 | NEXT_PUBLIC_DEV_PHRASE= 5 | -------------------------------------------------------------------------------- /examples/todo/README.md: -------------------------------------------------------------------------------- 1 | Пример смартаппа на next.js 2 | 3 | ## Запуск локально 4 | 5 | 1. Создать файл .env, заполнить поля в нем. 6 | 2. Запустить dev-сервер 7 | 8 | ```bash 9 | npm run dev 10 | # or 11 | yarn dev 12 | ``` 13 | 14 | 3. Сделать туннель в интернет. 15 | 16 | Например, используем `ngrok`. 17 | ```bash 18 | ngrok http 3000 19 | ``` 20 | 21 | 4. Зайти в (смартапп студию)[https://smartapp-studio.sberdevices.ru/]. 22 | 23 | Создать канвас, задать следующие параметры: 24 | - Webhook смартапа: туннель+/api/hook 25 | - Frontend Endpoint: туннель 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /examples/todo/fixtures/responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "init": {"payload":{"items":[{"bubble":{"text":"Начнем","markdown":false,"expand_policy":"auto_expand"}}],"suggestions":{"buttons":[{"title":"Запиши купить молоко","action":{"type":"text","text":"Запиши купить молоко","should_send_to_backend":true}},{"title":"Добавь запись помыть машину","action":{"type":"text","text":"Добавь запись помыть машину","should_send_to_backend":true}}]},"pronounceText":"начнем"}}, 3 | "addNote": {"payload":{"items":[{"command":{"type":"smart_app_data","smart_app_data":{"type":"add_note","payload":{"note":"Купить молоко"}}}},{"bubble":{"text":"Добавлено","markdown":false,"expand_policy":"auto_expand"}}],"suggestions":{"buttons":[{"title":"Запиши купить молоко","action":{"type":"text","text":"Запиши купить молоко","should_send_to_backend":true}},{"title":"Добавь запись помыть машину","action":{"type":"text","text":"Добавь запись помыть машину","should_send_to_backend":true}}]},"pronounceText":"Добавлено"}}, 4 | "doneNote": {"payload":{"items":[{"command":{"type":"smart_app_data","smart_app_data":{"type":"done_note","payload":{"id":"uinmh"}}}},{"bubble":{"text":"Умничка","markdown":false,"expand_policy":"auto_expand"}}],"pronounceText":"Умничка"}}, 5 | "doneNoteMissingNoteVar": {"payload":{"items":[{"command":{"type":"smart_app_error","smart_app_error":{"code":500,"description": "Missing required variable \"note\""}}}]}}, 6 | "deleteNote": {"payload":{"items":[{"bubble":{"text":"Вы уверены?","markdown":false,"expand_policy":"auto_expand"}}],"suggestions":{"buttons":[{"title":"продолжить","action":{"type":"text","text":"продолжить","should_send_to_backend":true}},{"title":"отменить","action":{"type":"text","text":"отменить","should_send_to_backend":true}}]},"pronounceText":"Вы уверены?"}}, 7 | "deleteNoteContinue": {"payload":{"items":[{"command":{"type":"smart_app_data","smart_app_data":{"type":"delete_note","payload":{"id":"uinmh"}}}},{"bubble":{"text":"Удалено","markdown":false,"expand_policy":"auto_expand"}}],"pronounceText":"Удалено"}}, 8 | "deleteNoteCancel": {"payload":{"items":[{"bubble":{"text":"Удаление отменено","markdown":false,"expand_policy":"auto_expand"}}],"pronounceText":"Удаление отменено"}}, 9 | "default": {"payload":{"items":[{"bubble":{"text":"Я не понимаю","markdown":false,"expand_policy":"auto_expand"}}],"pronounceText":"Я не понимаю"}}, 10 | "slotFillingQuestion": {"payload":{"items":[{"bubble":{"text":"Не поняла, повторите, пожалуйста","markdown":false,"expand_policy":"auto_expand"}}],"pronounceText":"Не поняла, повторите, пожалуйста"}} 11 | } -------------------------------------------------------------------------------- /examples/todo/global.d.ts: -------------------------------------------------------------------------------- 1 | export declare global { 2 | // eslint-disable-next-line @typescript-eslint/no-namespace 3 | namespace jest { 4 | interface Matchers { 5 | toBeEqualResponse(expected: NLPResponseATU); 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/todo/jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | clearMocks: true, 3 | preset: 'ts-jest', 4 | coverageDirectory: 'coverage', 5 | coverageProvider: 'v8', 6 | testEnvironment: 'node', 7 | }; 8 | -------------------------------------------------------------------------------- /examples/todo/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app", 3 | "private": "true", 4 | "version": "0.22.0", 5 | "main": "index.js", 6 | "author": "SberDevices Frontend Team ", 7 | "license": "Sber Public License at-nc-sa v.2", 8 | "scripts": { 9 | "dev": "next dev", 10 | "start": "next start", 11 | "test": "jest", 12 | "publish": "vercel --prod" 13 | }, 14 | "dependencies": { 15 | "@salutejs/scenario": "0.25.0", 16 | "@salutejs/storage-adapter-memory": "0.6.0", 17 | "@sberdevices/assistant-client": "2.14.0", 18 | "@sberdevices/plasma-icons": "1.4.0", 19 | "@sberdevices/plasma-tokens": "1.1.0", 20 | "@sberdevices/plasma-ui": "1.6.2", 21 | "@sberdevices/spatial-navigation": "1.0.2", 22 | "next": "11.1.1", 23 | "next-transpile-modules": "6.4.1", 24 | "react": "16.13.1", 25 | "react-dom": "16.13.1", 26 | "styled-components": "5.2.1" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "26.0.23", 30 | "@types/node": "12.12.30", 31 | "@types/react": "16.9.38", 32 | "@types/react-dom": "16.9.8", 33 | "@types/styled-components": "5.1.9", 34 | "jest": "26.6.3", 35 | "ts-jest": "26.5.5", 36 | "typescript": "4.2.4", 37 | "vercel": "22.0.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/todo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sberdevices/salutejs/33e90ce91d6bf76946d0ce8ff0d7ec07d21ea784/examples/todo/public/favicon.ico -------------------------------------------------------------------------------- /examples/todo/src/Components/GlobalStyles.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from 'react'; 2 | import { createGlobalStyle } from 'styled-components'; 3 | import type { AssistantCharacterType } from '@sberdevices/assistant-client'; 4 | import { darkJoy, darkEva, darkSber } from '@sberdevices/plasma-tokens/themes'; 5 | import { text, background, gradient } from '@sberdevices/plasma-tokens'; 6 | 7 | const themes = { 8 | sber: createGlobalStyle(darkEva), 9 | eva: createGlobalStyle(darkSber), 10 | joy: createGlobalStyle(darkJoy), 11 | }; 12 | 13 | const DocStyles = createGlobalStyle` 14 | * { 15 | box-sizing: border-box; 16 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 17 | -webkit-tap-highlight-color: transparent; /* i.e. Nexus5/Chrome and Kindle Fire HD 7'' */ 18 | } 19 | html { 20 | font-size: 32px; 21 | user-select: none; 22 | } 23 | body { 24 | font-family: "SB Sans Text", sans-serif; 25 | height: auto; 26 | min-height: 100%; 27 | } 28 | body:before { 29 | content: ""; 30 | position: fixed; 31 | top: 0; 32 | bottom: 0; 33 | left: 0; 34 | right: 0; 35 | color: ${text}; 36 | background: ${gradient}; 37 | background-color: ${background}; 38 | background-attachment: fixed; 39 | background-size: 100vw 100vh; 40 | z-index: -2; 41 | } 42 | `; 43 | 44 | export const GlobalStyles: FC<{ character: AssistantCharacterType }> = ({ character }) => { 45 | const Theme = useMemo(() => themes[character], [character]); 46 | 47 | return ( 48 | <> 49 | 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /examples/todo/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | 3 | const CustomApp = ({ Component, pageProps }: AppProps) => { 4 | if (!process.browser) { 5 | return null; 6 | } 7 | return ; 8 | }; 9 | 10 | export default CustomApp; 11 | -------------------------------------------------------------------------------- /examples/todo/src/pages/api/hook.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | import { handleNlpRequest } from '../../scenario/scenario'; 4 | 5 | export default async (request: NextApiRequest, response: NextApiResponse) => { 6 | response.status(200).json(await handleNlpRequest(request.body)); 7 | }; 8 | -------------------------------------------------------------------------------- /examples/todo/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useReducer, useState, useRef, useEffect, FormEvent } from 'react'; 2 | import { 3 | AssistantCharacterType, 4 | AssistantAppState, 5 | AssistantClientCustomizedCommand, 6 | AssistantNavigationCommand, 7 | AssistantSmartAppData, 8 | createAssistant, 9 | createSmartappDebugger, 10 | } from '@sberdevices/assistant-client'; 11 | import { 12 | Card, 13 | CardContent, 14 | Cell, 15 | Container, 16 | Row, 17 | Col, 18 | DeviceThemeProvider, 19 | TextBox, 20 | TextField, 21 | Checkbox, 22 | } from '@sberdevices/plasma-ui'; 23 | 24 | import { GlobalStyles } from '../Components/GlobalStyles'; 25 | import { Action, reducer } from '../store'; 26 | 27 | if (process.browser) { 28 | // @ts-ignore 29 | import('@sberdevices/spatial-navigation'); 30 | } 31 | 32 | const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; 33 | const NEXT_PUBLIC_DEV_TOKEN = process.env.NEXT_PUBLIC_DEV_TOKEN; 34 | const NEXT_PUBLIC_DEV_PHRASE = process.env.NEXT_PUBLIC_DEV_PHRASE; 35 | 36 | interface TodoCommand extends AssistantSmartAppData { 37 | smart_app_data: Action; 38 | } 39 | 40 | const IndexPage = () => { 41 | const [appState, dispatch] = useReducer(reducer, { 42 | notes: [{ id: 'uinmh', title: 'купить хлеб', completed: false }], 43 | }); 44 | 45 | const [character, setCharacter] = useState('sber' as const); 46 | const [note, setNote] = useState(''); 47 | 48 | const assistantStateRef = useRef({}); 49 | const assistantRef = useRef>(); 50 | 51 | useEffect(() => { 52 | const initializeAssistant = () => { 53 | if (!IS_DEVELOPMENT) { 54 | return createAssistant({ 55 | getState: () => assistantStateRef.current, 56 | }); 57 | } 58 | 59 | if (!NEXT_PUBLIC_DEV_TOKEN || !NEXT_PUBLIC_DEV_PHRASE) { 60 | throw new Error(''); 61 | } 62 | 63 | return createSmartappDebugger({ 64 | token: NEXT_PUBLIC_DEV_TOKEN, 65 | initPhrase: NEXT_PUBLIC_DEV_PHRASE, 66 | getState: () => assistantStateRef.current, 67 | }); 68 | }; 69 | 70 | const assistant = initializeAssistant(); 71 | 72 | assistant.on('data', (command: AssistantClientCustomizedCommand) => { 73 | let navigation: AssistantNavigationCommand['navigation']; 74 | switch (command.type) { 75 | case 'character': 76 | setCharacter(command.character.id); 77 | // 'sber' | 'eva' | 'joy'; 78 | break; 79 | case 'navigation': 80 | navigation = (command as AssistantNavigationCommand).navigation; 81 | break; 82 | case 'smart_app_data': 83 | dispatch(command.smart_app_data); 84 | break; 85 | default: 86 | break; 87 | } 88 | 89 | if (navigation) { 90 | switch (navigation.command) { 91 | case 'UP': 92 | window.scrollTo(0, window.scrollY - 500); 93 | break; 94 | case 'DOWN': 95 | window.scrollTo(0, window.scrollY + 500); 96 | break; 97 | default: 98 | break; 99 | } 100 | } 101 | }); 102 | 103 | assistantRef.current = assistant; 104 | }, []); 105 | 106 | useEffect(() => { 107 | assistantStateRef.current = { 108 | item_selector: { 109 | items: appState.notes.map(({ id, title }, index) => ({ 110 | number: index + 1, 111 | id, 112 | title, 113 | })), 114 | }, 115 | }; 116 | }, [appState]); 117 | 118 | const doneNote = (title: string) => { 119 | assistantRef.current?.sendAction({ type: 'done', payload: { note: title } }); 120 | }; 121 | 122 | return ( 123 | 124 | 125 | 126 | 127 | 128 |
) => { 130 | e.preventDefault(); 131 | dispatch({ type: 'add_note', payload: { note } }); 132 | setNote(''); 133 | }} 134 | > 135 | setNote(value)} 139 | /> 140 | 141 | 142 |
143 | 144 | {appState.notes.map((n, i) => ( 145 | 146 | 147 | 148 | } 150 | contentRight={ 151 | doneNote(n.title)} /> 152 | } 153 | /> 154 | 155 | 156 | 157 | ))} 158 | 159 |
160 |
161 | ); 162 | }; 163 | 164 | export default IndexPage; 165 | -------------------------------------------------------------------------------- /examples/todo/src/scenario/handlers.ts: -------------------------------------------------------------------------------- 1 | import { createMatchers, SaluteHandler, SaluteRequest } from '@salutejs/scenario'; 2 | 3 | import { AddNoteCommand, DeleteNoteCommand, DoneNoteCommand, NoteVariable } from './types'; 4 | 5 | const { selectItem } = createMatchers>(); 6 | 7 | const capitalize = (string: string) => string.charAt(0).toUpperCase() + string.slice(1); 8 | 9 | export const runAppHandler: SaluteHandler = ({ res }) => { 10 | res.appendSuggestions(['Запиши купить молоко', 'Добавь запись помыть машину']); 11 | res.setPronounceText('начнем'); 12 | res.appendBubble('Начнем'); 13 | }; 14 | 15 | export const noMatchHandler: SaluteHandler = ({ res }) => { 16 | res.setPronounceText('Я не понимаю'); 17 | res.appendBubble('Я не понимаю'); 18 | }; 19 | 20 | export const addNote: SaluteHandler> = ({ req, res }) => { 21 | const { note } = req.variables; 22 | res.appendCommand({ type: 'add_note', payload: { note: capitalize(note) } }); 23 | res.appendSuggestions(['Запиши купить молоко', 'Добавь запись помыть машину']); 24 | res.setPronounceText('Добавлено'); 25 | res.appendBubble('Добавлено'); 26 | res.setAutoListening(true); 27 | }; 28 | 29 | export const doneNote: SaluteHandler> = ({ req, res }) => { 30 | const { note } = req.variables; 31 | const item = selectItem({ title: note })(req); 32 | if (note && item?.id) { 33 | res.appendCommand({ 34 | type: 'done_note', 35 | payload: { id: item.id }, 36 | }); 37 | 38 | res.setPronounceText('Умничка'); 39 | res.appendBubble('Умничка'); 40 | } 41 | }; 42 | 43 | export const deleteNoteApproved: SaluteHandler, { itemId: string }> = ({ 44 | res, 45 | session, 46 | }) => { 47 | const { itemId } = session; 48 | 49 | res.appendCommand({ 50 | type: 'delete_note', 51 | payload: { id: itemId }, 52 | }); 53 | 54 | res.setPronounceText('Удалено'); 55 | res.appendBubble('Удалено'); 56 | }; 57 | 58 | export const deleteNoteCancelled: SaluteHandler = ({ res }) => { 59 | res.setPronounceText('Удаление отменено'); 60 | res.appendBubble('Удаление отменено'); 61 | }; 62 | 63 | export const deleteNote: SaluteHandler, { itemId: string }> = ({ req, res, session }) => { 64 | const { note } = req.variables; 65 | const item = selectItem({ title: note })(req); 66 | if (note && item?.id) { 67 | session.itemId = item.id; 68 | 69 | res.setPronounceText('Вы уверены?'); 70 | res.appendBubble('Вы уверены?'); 71 | res.appendSuggestions(['продолжить', 'отменить']); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /examples/todo/src/scenario/scenario.test.ts: -------------------------------------------------------------------------------- 1 | import { NLPRequestRA, NLPRequestSA, NLPResponseATU } from '@salutejs/scenario'; 2 | 3 | import Requests from '../../fixtures/requests.json'; 4 | import Responses from '../../fixtures/responses.json'; 5 | 6 | import { handleNlpRequest } from './scenario'; 7 | 8 | expect.extend({ 9 | toBeEqualResponse(received: NLPResponseATU, expected: NLPResponseATU) { 10 | expect(expected.payload.pronounceText).toEqual(received.payload.pronounceText); 11 | expect(expected.payload.items).toEqual(received.payload.items); 12 | expect(expected.payload.suggestions).toEqual(received.payload.suggestions); 13 | return { pass: true, message: () => '' }; 14 | }, 15 | }); 16 | 17 | describe('todo-scenario', () => { 18 | test('run_app', async () => { 19 | const res = await handleNlpRequest(Requests.init as NLPRequestRA); 20 | expect(Responses.init).toBeEqualResponse(res as NLPResponseATU); 21 | }); 22 | 23 | test('addNote', async () => { 24 | const res = await handleNlpRequest(Requests.addNote as NLPRequestRA); 25 | expect(Responses.addNote).toBeEqualResponse(res as NLPResponseATU); 26 | }); 27 | 28 | test('doneNote', async () => { 29 | const res = await handleNlpRequest(Requests.doneNote as NLPRequestRA); 30 | expect(Responses.doneNote).toBeEqualResponse(res as NLPResponseATU); 31 | }); 32 | 33 | test('doneNote action', async () => { 34 | const res = await handleNlpRequest(Requests.doneNoteAction as NLPRequestSA); 35 | expect(Responses.doneNote).toBeEqualResponse(res as NLPResponseATU); 36 | }); 37 | 38 | test('deleteNote continue', async () => { 39 | const res1 = await handleNlpRequest(Requests.deleteNote as NLPRequestRA); 40 | const res2 = await handleNlpRequest(Requests.deleteNoteContinue as NLPRequestRA); 41 | expect(Responses.deleteNote).toBeEqualResponse(res1 as NLPResponseATU); 42 | expect(Responses.deleteNoteContinue).toBeEqualResponse(res2 as NLPResponseATU); 43 | }); 44 | 45 | test('deleteNote cancel', async () => { 46 | const res1 = await handleNlpRequest(Requests.deleteNote as NLPRequestRA); 47 | const res2 = await handleNlpRequest(Requests.deleteNoteCancel as NLPRequestRA); 48 | expect(Responses.deleteNote).toBeEqualResponse(res1 as NLPResponseATU); 49 | expect(Responses.deleteNoteCancel).toBeEqualResponse(res2 as NLPResponseATU); 50 | }); 51 | 52 | test('deleteNote skip', async () => { 53 | const res1 = await handleNlpRequest(Requests.deleteNote as NLPRequestRA); 54 | const res2 = await handleNlpRequest(Requests.addNote as NLPRequestRA); 55 | expect(Responses.deleteNote).toBeEqualResponse(res1 as NLPResponseATU); 56 | expect(Responses.addNote).toBeEqualResponse(res2 as NLPResponseATU); 57 | }); 58 | 59 | test('default intent', async () => { 60 | const res = await handleNlpRequest(Requests.default as NLPRequestRA); 61 | expect(Responses.default).toBeEqualResponse(res as NLPResponseATU); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/todo/src/scenario/scenario.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createUserScenario, 3 | createSystemScenario, 4 | createSaluteRequest, 5 | createSaluteResponse, 6 | createScenarioWalker, 7 | createMatchers, 8 | SaluteRequest, 9 | NLPRequest, 10 | NLPResponse, 11 | } from '@salutejs/scenario'; 12 | import { SaluteMemoryStorage } from '@salutejs/storage-adapter-memory'; 13 | 14 | import { 15 | addNote, 16 | deleteNote, 17 | deleteNoteApproved, 18 | deleteNoteCancelled, 19 | doneNote, 20 | noMatchHandler, 21 | runAppHandler, 22 | } from './handlers'; 23 | 24 | const { action, regexp, text } = createMatchers(); 25 | 26 | const userScenario = createUserScenario({ 27 | Profile: { 28 | match: text('профиль'), 29 | handle: ({ res }) => { 30 | res.getProfileData(); 31 | }, 32 | children: { 33 | ProfileReceived: { 34 | match: (req) => req.request.messageName === 'TAKE_PROFILE_DATA', 35 | handle: ({ res, req }) => { 36 | const name = req.profile?.customer_name; 37 | if (name) { 38 | res.setPronounceText(`Привет, ${name}`); 39 | return; 40 | } 41 | 42 | if (req.request.messageName === 'TAKE_PROFILE_DATA') { 43 | res.setPronounceText( 44 | `Почему-то не получили ваше имя, статус ошибки ${req.request.payload.status_code.code}`, 45 | ); 46 | return; 47 | } 48 | 49 | res.setPronounceText('До свидания'); 50 | }, 51 | }, 52 | }, 53 | }, 54 | AddNote: { 55 | match: regexp(/^(записать|напомнить|добавить запись) (?.+)$/i), 56 | handle: addNote, 57 | }, 58 | DoneNote: { 59 | match: regexp(/^(выполнить?|сделать?) (?.+)$/i), 60 | handle: doneNote, 61 | }, 62 | DoneNoteAction: { 63 | match: action('done'), 64 | handle: doneNote, 65 | }, 66 | DeleteNote: { 67 | match: regexp(/^(удалить) (?.+)$/i), 68 | handle: deleteNote, 69 | children: { 70 | yes: { 71 | match: regexp(/^(да|продолжить)$/i), 72 | handle: deleteNoteApproved, 73 | }, 74 | no: { 75 | match: regexp(/^(нет|отменить)$/i), 76 | handle: deleteNoteCancelled, 77 | }, 78 | }, 79 | }, 80 | }); 81 | 82 | const scenarioWalker = createScenarioWalker({ 83 | systemScenario: createSystemScenario({ 84 | RUN_APP: runAppHandler, 85 | NO_MATCH: noMatchHandler, 86 | }), 87 | userScenario, 88 | }); 89 | 90 | const storage = new SaluteMemoryStorage(); 91 | 92 | export const handleNlpRequest = async (request: NLPRequest): Promise => { 93 | const req = createSaluteRequest(request); 94 | const res = createSaluteResponse(request); 95 | 96 | const sessionId = request.uuid.userId; 97 | const session = await storage.resolve(sessionId); 98 | 99 | await scenarioWalker({ req, res, session }); 100 | await storage.save({ id: sessionId, session }); 101 | 102 | return res.message; 103 | }; 104 | -------------------------------------------------------------------------------- /examples/todo/src/scenario/types.ts: -------------------------------------------------------------------------------- 1 | import { SaluteCommand, SaluteRequestVariable } from '@salutejs/scenario'; 2 | 3 | export interface Note { 4 | id: string; 5 | title: string; 6 | completed: boolean; 7 | } 8 | 9 | export interface InitCommand extends SaluteCommand { 10 | type: 'init'; 11 | payload: { 12 | notes: Array; 13 | }; 14 | } 15 | 16 | export interface AddNoteCommand extends SaluteCommand { 17 | type: 'add_note'; 18 | payload: { 19 | note: string; 20 | }; 21 | } 22 | 23 | export interface DoneNoteCommand extends SaluteCommand { 24 | type: 'done_note'; 25 | payload: { 26 | id: string; 27 | }; 28 | } 29 | 30 | export interface DeleteNoteCommand extends SaluteCommand { 31 | type: 'delete_note'; 32 | payload: { 33 | id: string; 34 | }; 35 | } 36 | 37 | export interface NoteVariable extends SaluteRequestVariable { 38 | note: string; 39 | } 40 | -------------------------------------------------------------------------------- /examples/todo/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { AddNoteCommand, DeleteNoteCommand, DoneNoteCommand, InitCommand, Note } from '../types'; 2 | 3 | type State = { 4 | notes: Array; 5 | }; 6 | 7 | export type Action = InitCommand | AddNoteCommand | DoneNoteCommand | DeleteNoteCommand; 8 | 9 | export const reducer = (state: State, action: Action) => { 10 | switch (action.type) { 11 | case 'init': 12 | return { 13 | ...state, 14 | notes: [...action.payload.notes], 15 | }; 16 | 17 | case 'add_note': 18 | return { 19 | ...state, 20 | notes: [ 21 | ...state.notes, 22 | { 23 | id: Math.random().toString(36).substring(7), 24 | title: action.payload.note, 25 | completed: false, 26 | }, 27 | ], 28 | }; 29 | 30 | case 'done_note': 31 | return { 32 | ...state, 33 | notes: state.notes.map((note) => (note.id === action.payload.id ? { ...note, completed: true } : note)), 34 | }; 35 | 36 | case 'delete_note': 37 | return { 38 | ...state, 39 | notes: state.notes.filter(({ id }) => id !== action.payload.id), 40 | }; 41 | 42 | default: 43 | throw new Error(); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /examples/todo/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { SaluteCommand } from '@salutejs/scenario'; 2 | 3 | export interface Note { 4 | id: string; 5 | title: string; 6 | completed: boolean; 7 | } 8 | 9 | export interface InitCommand extends SaluteCommand { 10 | type: 'init'; 11 | payload: { 12 | notes: Array; 13 | }; 14 | } 15 | 16 | export interface AddNoteCommand extends SaluteCommand { 17 | type: 'add_note'; 18 | payload: { 19 | note: string; 20 | }; 21 | } 22 | 23 | export interface DoneNoteCommand extends SaluteCommand { 24 | type: 'done_note'; 25 | payload: { 26 | id: string; 27 | }; 28 | } 29 | 30 | export interface DeleteNoteCommand extends SaluteCommand { 31 | type: 'delete_note'; 32 | payload: { 33 | id: string; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /examples/todo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "isolatedModules": true, 7 | "jsx": "preserve", 8 | "lib": [ 9 | "dom", 10 | "es2017" 11 | ], 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "noEmit": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "strict": false, 21 | "target": "es6", 22 | }, 23 | "exclude": [ 24 | "node_modules" 25 | ], 26 | "include": [ 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | import path from 'path'; 3 | import { lstatSync, readdirSync } from 'fs'; 4 | 5 | const basePath = path.resolve(__dirname, 'packages'); 6 | const packages = readdirSync(basePath).filter((name) => lstatSync(path.join(basePath, name)).isDirectory()); 7 | 8 | const config: Config.InitialOptions = { 9 | preset: 'ts-jest', 10 | coverageDirectory: 'coverage', 11 | coverageProvider: 'v8', 12 | testEnvironment: 'node', 13 | testMatch: ['/**/src/**/*.spec.{ts,js}'], 14 | moduleNameMapper: { 15 | ...packages.reduce( 16 | (acc, name) => ({ 17 | ...acc, 18 | [`@salutejs/${name}(.*)$`]: `/packages/./${name}/src/$1`, 19 | }), 20 | {}, 21 | ), 22 | }, 23 | modulePathIgnorePatterns: [...packages.reduce((acc, name) => [...acc, `/packages/${name}/dist`], [])], 24 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*", 4 | "examples/*" 5 | ], 6 | "version": "independent", 7 | "ignoreChanges": [ 8 | "*.md" 9 | ], 10 | "loglevel": "verbose", 11 | "exact": true, 12 | "command": { 13 | "bootstrap": { 14 | "npmClientArgs": [ 15 | "--no-audit", 16 | "--no-optional", 17 | "--loglevel error", 18 | "--no-progress", 19 | "--unsafe-perm", 20 | "--prefer-offline" 21 | ] 22 | }, 23 | "publish": { 24 | "verifyAccess": false, 25 | "ignoreChanges": [ 26 | "*.md", 27 | "**/test/**" 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salutejs", 3 | "private": true, 4 | "description": "Tiny helpers to make scenario for Salute Assistants family on Nodejs", 5 | "author": "SberDevices Frontend Team ", 6 | "license": "Sber Public License at-nc-sa v.2", 7 | "main": "dist/index.d.ts", 8 | "scripts": { 9 | "release": "auto shipit", 10 | "test": "lerna run test", 11 | "lint": "lerna run lint", 12 | "bootstrap": "lerna bootstrap", 13 | "build": "lerna run build", 14 | "build:w": "lerna run build:w", 15 | "ci:prepare": "npm ci && npm run bootstrap" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/sberdevices/salutejs.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/sberdevices/salutejs/issues" 23 | }, 24 | "homepage": "https://github.com/sberdevices/salutejs#readme", 25 | "devDependencies": { 26 | "@auto-it/conventional-commits": "10.29.3", 27 | "@auto-it/npm": "10.29.3", 28 | "@auto-it/slack": "10.29.3", 29 | "@commitlint/cli": "11.0.0", 30 | "@commitlint/config-conventional": "11.0.0", 31 | "@types/jest": "26.0.22", 32 | "@typescript-eslint/eslint-plugin": "4.14.2", 33 | "@typescript-eslint/parser": "4.14.2", 34 | "auto": "10.29.3", 35 | "eslint": "7.19.0", 36 | "eslint-config-airbnb-base": "14.2.1", 37 | "eslint-config-prettier": "7.2.0", 38 | "eslint-plugin-import": "2.22.1", 39 | "eslint-plugin-prettier": "3.3.1", 40 | "husky": "4.3.8", 41 | "jest": "26.6.3", 42 | "lerna": "4.0.0", 43 | "lint-staged": "10.5.4", 44 | "prettier": "2.2.1", 45 | "ts-jest": "26.5.4", 46 | "ts-node": "9.1.1", 47 | "ts-node-dev": "1.1.6", 48 | "typescript": "4.1.3" 49 | }, 50 | "auto": { 51 | "plugins": [ 52 | [ 53 | "npm", 54 | { 55 | "setRcToken": false 56 | } 57 | ], 58 | "conventional-commits", 59 | "slack" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/.env.sample: -------------------------------------------------------------------------------- 1 | SMARTAPP_BRAIN_TOKEN= 2 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/.npmrc: -------------------------------------------------------------------------------- 1 | ../../.npmrc -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.17.7 (Thu Sep 16 2021) 2 | 3 | #### 🐛 Bug Fix 4 | 5 | - fix: smartapp brain take 10 best intents [#230](https://github.com/sberdevices/salutejs/pull/230) ([@Turanchoks](https://github.com/Turanchoks)) 6 | 7 | #### Authors: 1 8 | 9 | - Pavel Remizov ([@Turanchoks](https://github.com/Turanchoks)) 10 | 11 | --- 12 | 13 | # v0.11.0 (Tue Jul 13 2021) 14 | 15 | #### 🐛 Bug Fix 16 | 17 | - docs: update recognizer-smartapp-brain readme.md [#197](https://github.com/sberdevices/salutejs/pull/197) ([@sasha-tlt](https://github.com/sasha-tlt)) 18 | 19 | #### Authors: 1 20 | 21 | - Alexander Salmin ([@sasha-tlt](https://github.com/sasha-tlt)) 22 | 23 | --- 24 | 25 | # v0.10.0 (Tue Jul 06 2021) 26 | 27 | #### 🚀 Enhancement 28 | 29 | - feat: setAutoListening/setASRHints on res [#166](https://github.com/sberdevices/salutejs/pull/166) ([@Turanchoks](https://github.com/Turanchoks)) 30 | - feat: support array WIP [#157](https://github.com/sberdevices/salutejs/pull/157) ([@Turanchoks](https://github.com/Turanchoks)) 31 | - feat: support phrase/pattern for intents in brain, #127 [#130](https://github.com/sberdevices/salutejs/pull/130) ([@Turanchoks](https://github.com/Turanchoks)) 32 | - feat: custom entities for brain [#130](https://github.com/sberdevices/salutejs/pull/130) ([@Turanchoks](https://github.com/Turanchoks)) 33 | - feat(recognizer-smartapp-brain): support dotenv in CLI [#115](https://github.com/sberdevices/salutejs/pull/115) ([@awinogradov](https://github.com/awinogradov)) 34 | - feat: add default env variables [#115](https://github.com/sberdevices/salutejs/pull/115) ([@awinogradov](https://github.com/awinogradov)) 35 | - feat(recognizer-smartapp-brain): implement CLI [#86](https://github.com/sberdevices/salutejs/pull/86) ([@awinogradov](https://github.com/awinogradov)) 36 | 37 | #### 🐛 Bug Fix 38 | 39 | - fix: Chatapp-brain example has wrong intents.json format [#194](https://github.com/sberdevices/salutejs/pull/194) ([@sasha-tlt](https://github.com/sasha-tlt)) 40 | - Bump independent versions \[skip ci\] ([@Yeti-or](https://github.com/Yeti-or)) 41 | - fix: prepare step [#193](https://github.com/sberdevices/salutejs/pull/193) ([@Yeti-or](https://github.com/Yeti-or)) 42 | - fix(recognizer-smartapp-brain): brain push custom entities check [#190](https://github.com/sberdevices/salutejs/pull/190) ([@Turanchoks](https://github.com/Turanchoks)) 43 | - chore(recognizer-smartapp-brain): add .env.sample file [#189](https://github.com/sberdevices/salutejs/pull/189) ([@awinogradov](https://github.com/awinogradov)) 44 | - docs(recognizer-smartapp-brain): add SMARTAPP_BRAIN_TOKEN [#189](https://github.com/sberdevices/salutejs/pull/189) ([@awinogradov](https://github.com/awinogradov)) 45 | - fix(recognizer-smartapp-brain): check phrases and patterns in res [#158](https://github.com/sberdevices/salutejs/pull/158) ([@awinogradov](https://github.com/awinogradov)) 46 | - fix(types): TextIntent.matchers.type is now string [#149](https://github.com/sberdevices/salutejs/pull/149) ([@Turanchoks](https://github.com/Turanchoks)) 47 | - fix(recognizer-smartapp-brain): lint errors [#145](https://github.com/sberdevices/salutejs/pull/145) ([@awinogradov](https://github.com/awinogradov)) 48 | - fix: enable strictNullChecks [#145](https://github.com/sberdevices/salutejs/pull/145) ([@awinogradov](https://github.com/awinogradov)) 49 | - docs: update env variables in README [#115](https://github.com/sberdevices/salutejs/pull/115) ([@awinogradov](https://github.com/awinogradov)) 50 | - fix(recognizer-smartapp-brain): add short bin name [#114](https://github.com/sberdevices/salutejs/pull/114) ([@awinogradov](https://github.com/awinogradov)) 51 | - chore(recognizer-smartapp-brain): remove intents normalizer [#86](https://github.com/sberdevices/salutejs/pull/86) ([@awinogradov](https://github.com/awinogradov)) 52 | - fix(recognizer-smartapp-brain): use values only, better error message [#86](https://github.com/sberdevices/salutejs/pull/86) ([@awinogradov](https://github.com/awinogradov)) 53 | - docs(recognizer-smartapp-brain): cli readme [#86](https://github.com/sberdevices/salutejs/pull/86) ([@awinogradov](https://github.com/awinogradov)) 54 | - docs(recognizer-smartapp-brain): access token readme [#86](https://github.com/sberdevices/salutejs/pull/86) ([@awinogradov](https://github.com/awinogradov)) 55 | - chore(recognizer-smartapp-brain): refactor API calls [#86](https://github.com/sberdevices/salutejs/pull/86) ([@awinogradov](https://github.com/awinogradov)) 56 | - chore: split recognizer package [#86](https://github.com/sberdevices/salutejs/pull/86) ([@awinogradov](https://github.com/awinogradov)) 57 | 58 | #### ⚠️ Pushed to `master` 59 | 60 | - chore(release): publish ([@awinogradov](https://github.com/awinogradov)) 61 | 62 | #### Authors: 4 63 | 64 | - Alexander Salmin ([@sasha-tlt](https://github.com/sasha-tlt)) 65 | - Pavel Remizov ([@Turanchoks](https://github.com/Turanchoks)) 66 | - Tony Vi ([@awinogradov](https://github.com/awinogradov)) 67 | - Vasiliy ([@Yeti-or](https://github.com/Yeti-or)) 68 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/README.md: -------------------------------------------------------------------------------- 1 | # @salutejs/recognizer-smartapp-brain 2 | 3 | SmartApp Brain — технология определения смысла фразы (намерения) пользователя. Позволяет создавать классификаторы из необработанных логов и управлять обучающими выборками. Включает готовые к использованию machine learning модели. Технология используется классификатором [SmartApp Code](https://developer.sberdevices.ru/docs/ru/developer_tools/ide/smartappcode_description_and_guide) и [SmartApp Graph](https://developer.sberdevices.ru/docs/ru/developer_tools/flow/quick_start/quick_start). Под капотом обращается к SmartApp Brain Direct API. 4 | 5 | > npm i -S @salutejs/recognizer-smartapp-brain 6 | 7 | ## Usage 8 | 9 | ### Access token 10 | 11 | 1. Создать новый смартапп типа SmartApp Code в https://developers.sber.ru/studio/workspaces/smartapps/create-app 12 | 2. Перейти в проект 13 | 3. Настройки проекта -> Классификатор -> __API-ключ Brain__ 14 | 4. Добавить токен в `.env` в корне проекта 15 | 16 | __.env__ 17 | ``` bash 18 | SMARTAPP_BRAIN_TOKEN= 19 | ``` 20 | 21 | ### Получение интентов 22 | 23 | > brain pull -t 24 | 25 | Словарь с интентами по умолчанию будет записан в `./src/intents.json`. Чтобы изменить расположение файла воспользуйтесь параметром `-p`. 26 | 27 | ### Обновление интентов 28 | 29 | Словарь интентов редактируется локально и после редактирования должен быть загружен в SmartApp Brain. 30 | 31 | > brain push -t 32 | 33 | ### Подключение интентов в коде 34 | 35 | ``` ts 36 | import { createIntents, createScenarioWalker } from '@salutejs/scenario'; 37 | import { SmartAppBrainRecognizer } from '@salutejs/recognizer-smartapp-brain'; 38 | 39 | import intentsDict from './intents.json'; 40 | 41 | const intents = createIntents(intentsDict); 42 | 43 | const scenarioWalker = createScenarioWalker({ 44 | // ... 45 | intents, 46 | recognizer: new SmartAppBrainRecognizer(process.env.SMARTAPP_BRAIN_TOKEN), 47 | // ... 48 | }); 49 | ``` 50 | 51 | 52 | #### SberDevices with :heart: 53 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/bin/brain.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/bin/brain'); 4 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/recognizer-smartapp-brain", 3 | "version": "0.25.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@salutejs/scenario": { 8 | "version": "0.6.0", 9 | "resolved": "https://registry.npmjs.org/@salutejs/scenario/-/types-0.6.0.tgz", 10 | "integrity": "sha512-FJrlzLFO7g9OeCY4JOXRTEuZ+ktVh1bJi32KbMrpBU/DNPJv60MhtdaU7FJONPOMIV44dfZVy2MzbfI3pDG63A==" 11 | }, 12 | "@types/node": { 13 | "version": "14.14.25", 14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz", 15 | "integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ==", 16 | "dev": true 17 | }, 18 | "@types/node-fetch": { 19 | "version": "2.5.8", 20 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz", 21 | "integrity": "sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==", 22 | "dev": true, 23 | "requires": { 24 | "@types/node": "*", 25 | "form-data": "^3.0.0" 26 | } 27 | }, 28 | "ansi-styles": { 29 | "version": "4.3.0", 30 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 31 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 32 | "requires": { 33 | "color-convert": "^2.0.1" 34 | } 35 | }, 36 | "asynckit": { 37 | "version": "0.4.0", 38 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 39 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 40 | "dev": true 41 | }, 42 | "chalk": { 43 | "version": "4.1.0", 44 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", 45 | "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", 46 | "requires": { 47 | "ansi-styles": "^4.1.0", 48 | "supports-color": "^7.1.0" 49 | } 50 | }, 51 | "color-convert": { 52 | "version": "2.0.1", 53 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 54 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 55 | "requires": { 56 | "color-name": "~1.1.4" 57 | } 58 | }, 59 | "color-name": { 60 | "version": "1.1.4", 61 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 62 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 63 | }, 64 | "combined-stream": { 65 | "version": "1.0.8", 66 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 67 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 68 | "dev": true, 69 | "requires": { 70 | "delayed-stream": "~1.0.0" 71 | } 72 | }, 73 | "commander": { 74 | "version": "7.2.0", 75 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 76 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" 77 | }, 78 | "delayed-stream": { 79 | "version": "1.0.0", 80 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 81 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 82 | "dev": true 83 | }, 84 | "dotenv": { 85 | "version": "8.2.0", 86 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", 87 | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" 88 | }, 89 | "form-data": { 90 | "version": "3.0.1", 91 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", 92 | "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", 93 | "dev": true, 94 | "requires": { 95 | "asynckit": "^0.4.0", 96 | "combined-stream": "^1.0.8", 97 | "mime-types": "^2.1.12" 98 | } 99 | }, 100 | "has-flag": { 101 | "version": "4.0.0", 102 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 103 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 104 | }, 105 | "is-unicode-supported": { 106 | "version": "0.1.0", 107 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 108 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" 109 | }, 110 | "log-symbols": { 111 | "version": "4.1.0", 112 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 113 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 114 | "requires": { 115 | "chalk": "^4.1.0", 116 | "is-unicode-supported": "^0.1.0" 117 | } 118 | }, 119 | "mime-db": { 120 | "version": "1.46.0", 121 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", 122 | "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", 123 | "dev": true 124 | }, 125 | "mime-types": { 126 | "version": "2.1.29", 127 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", 128 | "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", 129 | "dev": true, 130 | "requires": { 131 | "mime-db": "1.46.0" 132 | } 133 | }, 134 | "node-fetch": { 135 | "version": "2.6.1", 136 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 137 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" 138 | }, 139 | "supports-color": { 140 | "version": "7.2.0", 141 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 142 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 143 | "requires": { 144 | "has-flag": "^4.0.0" 145 | } 146 | }, 147 | "typescript": { 148 | "version": "4.1.3", 149 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", 150 | "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", 151 | "dev": true 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/recognizer-smartapp-brain", 3 | "version": "0.25.0", 4 | "description": "SmartApp Brain recognizer for SaluteJS", 5 | "author": "SberDevices Frontend Team ", 6 | "license": "Sber Public License at-nc-sa v.2", 7 | "bin": { 8 | "brain": "bin/brain.js" 9 | }, 10 | "main": "dist/index.js", 11 | "typings": "dist/index.d.ts", 12 | "homepage": "https://github.com/sberdevices/salutejs#readme", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/sberdevices/salutejs.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/sberdevices/salutejs/issues" 19 | }, 20 | "files": [ 21 | "bin", 22 | "dist", 23 | "README.md", 24 | "LICENSE.txt" 25 | ], 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "scripts": { 30 | "prepare": "npm run build", 31 | "build": "rm -rf dist && tsc", 32 | "build:w": "rm -rf dist && tsc -w", 33 | "watch": "tsc -w", 34 | "lint": "eslint --ext .js,.ts ./src" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "14.14.25", 38 | "@types/node-fetch": "2.5.8", 39 | "typescript": "4.1.3" 40 | }, 41 | "dependencies": { 42 | "@salutejs/scenario": "0.25.0", 43 | "commander": "7.2.0", 44 | "dotenv": "8.2.0", 45 | "log-symbols": "4.1.0", 46 | "node-fetch": "2.6.1" 47 | }, 48 | "gitHead": "70298e562f327c87c550171469ad7a55e0d88cf6" 49 | } 50 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/src/bin/brain.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable import/no-dynamic-require */ 3 | /* eslint-disable global-require */ 4 | /* eslint-disable @typescript-eslint/no-var-requires */ 5 | 6 | import { join } from 'path'; 7 | import { promises } from 'fs'; 8 | import { Command } from 'commander'; 9 | import logSymbols from 'log-symbols'; 10 | import { IntentsDict } from '@salutejs/scenario'; 11 | import { config as dotenv } from 'dotenv'; 12 | 13 | import { permittedSystemEntites } from '../lib/permittedSystemEntities'; 14 | import { 15 | convertIntentsForImport, 16 | getEntitiesFromResponse, 17 | getIntentsFromResponse, 18 | EntitiesDict, 19 | convertEntitiesForImport, 20 | } from '../lib/smartAppBrainSync'; 21 | import { SmartAppBrainRecognizer } from '../lib'; 22 | 23 | dotenv(); 24 | 25 | const cli = new Command(); 26 | 27 | cli.command('pull') 28 | .option('-t, --token ', 'SmartApp Brain access token', process.env.SMARTAPP_BRAIN_TOKEN) 29 | .option('-p, --path ', 'Path to intents dictionary file', 'src/intents.json') 30 | .option('-d, --debug', 'Debug', false) 31 | .action(async ({ token, path, debug }) => { 32 | const brain = new SmartAppBrainRecognizer(token); 33 | const intentsDictPath = join(process.cwd(), path); 34 | const projectData = await brain.export(); 35 | 36 | if (debug) { 37 | console.log(logSymbols.info, 'Export project data'); 38 | console.log(JSON.stringify(projectData, null, 2)); 39 | } 40 | 41 | const intentsFromResponse = getIntentsFromResponse(projectData); 42 | const entitiesFromResponse = getEntitiesFromResponse(projectData); 43 | 44 | const result = { 45 | intents: intentsFromResponse, 46 | entities: entitiesFromResponse, 47 | }; 48 | 49 | await promises.writeFile(intentsDictPath, JSON.stringify(result, null, 2)); 50 | console.log(logSymbols.success, 'Successfuly updated!'); 51 | }); 52 | 53 | cli.command('push') 54 | .option('-t, --token ', 'SmartApp Brain access token', process.env.SMARTAPP_BRAIN_TOKEN) 55 | .option('-p, --path ', 'Path to intents dictionary file', 'src/intents.json') 56 | .action(async ({ token, path }) => { 57 | const brain = new SmartAppBrainRecognizer(token); 58 | const intentsDictPath = join(process.cwd(), path); 59 | const projectData = await brain.export(); 60 | const { 61 | entities: entitiesFromFS, 62 | intents: intentsFromFS, 63 | }: { 64 | intents: IntentsDict; 65 | entities: EntitiesDict; 66 | } = require(intentsDictPath); 67 | const intentsConvertedForImport = convertIntentsForImport(intentsFromFS); 68 | const entitiesConvertedForImport = convertEntitiesForImport(entitiesFromFS); 69 | 70 | const customEntities = Object.keys(entitiesFromFS); 71 | const customEntitiesSet = new Set(customEntities); 72 | const permittedSystemEntitesSet = new Set(permittedSystemEntites); 73 | 74 | const usedSystemEntitiesSet = new Set(); 75 | 76 | for (const intent of Object.values(intentsFromFS)) { 77 | if (Array.isArray(intent.matchers)) { 78 | intent.matchers.forEach(({ rule }) => { 79 | const matched = rule.match(/@[a-zA-Z0-9._-]+/gi); 80 | if (matched) { 81 | matched.forEach((entitity) => { 82 | const normalized = entitity.replace(/^@/, ''); 83 | 84 | const isCustomEntity = customEntitiesSet.has(normalized); 85 | const isSystemEntity = permittedSystemEntitesSet.has(normalized); 86 | 87 | if (isSystemEntity) { 88 | usedSystemEntitiesSet.add(normalized); 89 | } else if (!isCustomEntity) { 90 | const allEntities = [...permittedSystemEntites, ...customEntities]; 91 | const errorMessage = [ 92 | `"${normalized}" entity not found.`, 93 | `These are allowed: ${allEntities.join(', ')}`, 94 | ]; 95 | 96 | throw new Error(errorMessage.join('\n')); 97 | } 98 | }); 99 | } 100 | }); 101 | } 102 | } 103 | 104 | projectData.intents = intentsConvertedForImport; 105 | projectData.entities = entitiesConvertedForImport; 106 | projectData.enabledSystemEntities = [...usedSystemEntitiesSet]; 107 | 108 | try { 109 | await brain.import(projectData); 110 | console.log(logSymbols.success, 'Successfuly pushed!'); 111 | } catch (error) { 112 | console.log(logSymbols.error, error.message); 113 | if (error.data) { 114 | console.log(logSymbols.error, error.data); 115 | } 116 | } 117 | }); 118 | 119 | cli.parseAsync(process.argv); 120 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { Inference, SaluteRequest, Recognizer } from '@salutejs/scenario'; 2 | import fetch, { RequestInfo, RequestInit } from 'node-fetch'; 3 | 4 | import { ProjectData } from './projectData'; 5 | 6 | // Пример использования SmartAppBrain для распознования текста 7 | // const brain = new SmartAppBrainRecognizer(process.env.ACCESS_TOKEN); 8 | 9 | // brain.inference('Забронировать столик на 2 на завтра').then((response) => { 10 | // console.log(util.inspect(response, false, 10, true)); 11 | // }); 12 | 13 | interface KnownSlot { 14 | name: string; 15 | value: string; 16 | array: boolean; 17 | } 18 | 19 | interface Entity { 20 | entity: string; 21 | slot: string; 22 | startPos: number; 23 | endPos: number; 24 | text: string; 25 | value: string; 26 | default: boolean; 27 | system: boolean; 28 | entityId: number; 29 | } 30 | 31 | interface Phrase { 32 | text: string; 33 | entities?: Entity[]; 34 | stagedPhraseIdx?: number; 35 | } 36 | 37 | interface SmartAppBrainInferenceRequest { 38 | phrase: Phrase; 39 | knownSlots?: KnownSlot[]; 40 | nBest: number; 41 | showDebugInfo?: boolean; 42 | clientId?: string; 43 | } 44 | 45 | interface SmartAppBrainInferenceResponse extends Inference { 46 | phrase: Phrase; 47 | } 48 | 49 | class FetchError extends Error { 50 | status: number; 51 | 52 | data: any; 53 | 54 | constructor(message: string, status: number, data: any) { 55 | super(message); 56 | this.status = status; 57 | this.data = data; 58 | } 59 | } 60 | 61 | export class SmartAppBrainRecognizer implements Recognizer { 62 | private static defaultInfereRequestOptions: SmartAppBrainInferenceRequest = { 63 | phrase: { 64 | text: '', 65 | }, 66 | nBest: 10, 67 | showDebugInfo: false, 68 | }; 69 | 70 | private _options: Partial = {}; 71 | 72 | protected async ask(url: RequestInfo, init: RequestInit = {}): Promise { 73 | const response = await fetch(`${this.host}/cailapub/api/caila/p/${this.accessToken}${url}`, { 74 | headers: { 75 | 'Content-Type': 'application/json', 76 | accept: 'application/json', 77 | }, 78 | method: 'POST', 79 | ...init, 80 | }); 81 | 82 | const data = await response.json(); 83 | 84 | if (response.status >= 400) { 85 | const error = new FetchError(response.statusText, response.status, data); 86 | throw error; 87 | } 88 | 89 | return data; 90 | } 91 | 92 | constructor( 93 | private accessToken = process.env.SMARTAPP_BRAIN_TOKEN, 94 | private host = 'https://smartapp-code.sberdevices.ru', 95 | ) { 96 | if (!accessToken) throw new Error('Wrong SmartApp Brain token.'); 97 | } 98 | 99 | public get options(): Partial { 100 | return this._options; 101 | } 102 | 103 | public set options(options: Partial) { 104 | this._options = options; 105 | } 106 | 107 | private buildInferenceRequest(text: string): SmartAppBrainInferenceRequest { 108 | return { 109 | ...SmartAppBrainRecognizer.defaultInfereRequestOptions, 110 | ...this.options, 111 | ...{ 112 | phrase: { 113 | ...(this.options.phrase || {}), 114 | text, 115 | }, 116 | }, 117 | }; 118 | } 119 | 120 | public inference = async ({ req }: { req: SaluteRequest }) => { 121 | const payload = this.buildInferenceRequest(req.message?.original_text); 122 | 123 | if (req.message == null) { 124 | return Promise.resolve(); 125 | } 126 | 127 | const resp: SmartAppBrainInferenceResponse = await this.ask('/nlu/inference', { 128 | body: JSON.stringify(payload), 129 | }); 130 | 131 | req.setInference(resp); 132 | }; 133 | 134 | public export = (): Promise => this.ask('/export'); 135 | 136 | public import = (projectData: ProjectData): Promise => 137 | this.ask('/import', { 138 | body: JSON.stringify(projectData), 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/src/lib/permittedSystemEntities.ts: -------------------------------------------------------------------------------- 1 | export const permittedSystemEntites = [ 2 | 'mystem.geo', 3 | 'mystem.persn', 4 | 'mystem.obsc', 5 | 'mystem.patrn', 6 | 'mystem.famn', 7 | 'pymorphy.romn', 8 | 'pymorphy.latn', 9 | 'pymorphy.numb', 10 | 'pymorphy.intg', 11 | 'pymorphy.abbr', 12 | 'pymorphy.name', 13 | 'pymorphy.surn', 14 | 'pymorphy.patr', 15 | 'pymorphy.geox', 16 | 'pymorphy.orgn', 17 | 'duckling.number', 18 | 'duckling.ordinal', 19 | 'duckling.amount-of-money', 20 | 'duckling.distance', 21 | 'duckling.time', 22 | 'duckling.date', 23 | 'duckling.time-of-day', 24 | 'duckling.duration', 25 | 'duckling.phone-number', 26 | 'duckling.email', 27 | 'duckling.url', 28 | 'mlps-obscene.obscene', 29 | 'zb.datetime', 30 | 'zb.number', 31 | ] as const; 32 | 33 | export type PermittedSystemEntitiesType = Array; 34 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/src/lib/projectData.ts: -------------------------------------------------------------------------------- 1 | interface Record { 2 | id: number; 3 | type: 'synonyms' | 'pattern'; 4 | rule: string[]; 5 | value: string; 6 | } 7 | 8 | interface EntityEntity { 9 | id: number; 10 | name: string; 11 | enabled: boolean; 12 | type: string; 13 | priority: number; 14 | noSpelling: boolean; 15 | noMorph: boolean; 16 | } 17 | 18 | interface ProjectDataEntity { 19 | entity: EntityEntity; 20 | records: Record[]; 21 | } 22 | 23 | interface PhraseEntity { 24 | entity: string; 25 | slot: string; 26 | startPos: number; 27 | endPos: number; 28 | text: string; 29 | value: string; 30 | default: boolean; 31 | system: boolean; 32 | entityId: number; 33 | } 34 | 35 | export interface Phrase { 36 | text: string; 37 | entities: PhraseEntity[] | null; 38 | stagedPhraseIdx: number | null; 39 | } 40 | 41 | interface Slot { 42 | name: string; 43 | entity?: string; 44 | required?: boolean; 45 | prompts?: string[]; 46 | array?: boolean; 47 | } 48 | 49 | export interface Intent { 50 | id: number; 51 | path: string; 52 | description: string | null; 53 | answer: string | null; 54 | customData: string | null; 55 | enabled: boolean; 56 | phrases: Phrase[]; 57 | patterns: string[]; 58 | slots: Slot[] | null; 59 | } 60 | 61 | interface Project { 62 | id: string; 63 | name: string; 64 | folder: string; 65 | } 66 | 67 | interface MlpsClassifierSettingsClass {} 68 | 69 | interface ExtendedSettings { 70 | patternsEnabled: boolean | null; 71 | tokenizerEngine: string; 72 | stsSettings?: MlpsClassifierSettingsClass; 73 | mlpsClassifierSettings?: MlpsClassifierSettingsClass; 74 | } 75 | 76 | interface Settings { 77 | language: string; 78 | spellingCorrection: boolean; 79 | classificationAlgorithm: string; 80 | timezone: string; 81 | extendedSettings: ExtendedSettings; 82 | } 83 | 84 | export interface ProjectData { 85 | project: Project; 86 | settings: Settings; 87 | intents: Intent[]; 88 | entities: ProjectDataEntity[]; 89 | enabledSystemEntities: string[]; 90 | } 91 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/src/lib/smartAppBrainSync.ts: -------------------------------------------------------------------------------- 1 | import { IntentsDict, SaluteIntent, TextIntent } from '@salutejs/scenario'; 2 | 3 | import { ProjectData, Phrase, Intent } from './projectData'; 4 | 5 | export const getIntentsFromResponse = (resp: ProjectData) => { 6 | const intents: IntentsDict = {}; 7 | 8 | for (const intent of resp.intents) { 9 | const variables: SaluteIntent['variables'] = {}; 10 | 11 | if (Array.isArray(intent.slots)) { 12 | for (const slot of intent.slots) { 13 | variables[slot.name] = { 14 | required: true, 15 | questions: slot.prompts, 16 | array: slot.array ? true : undefined, 17 | }; 18 | } 19 | } 20 | 21 | const matchers: TextIntent['matchers'] = []; 22 | 23 | if (intent.phrases) { 24 | for (const phrase of intent.phrases) { 25 | matchers.push({ 26 | type: 'phrase', 27 | rule: phrase.text, 28 | }); 29 | } 30 | } 31 | 32 | if (intent.patterns) { 33 | for (const pattern of intent.patterns) { 34 | matchers.push({ 35 | type: 'pattern', 36 | rule: pattern, 37 | }); 38 | } 39 | } 40 | 41 | intents[intent.path] = { 42 | matchers, 43 | variables, 44 | }; 45 | } 46 | 47 | return intents; 48 | }; 49 | 50 | export type EntitiesDict = Record< 51 | string, 52 | { 53 | matchers: Array<{ 54 | type: 'synonyms' | 'pattern'; 55 | rule: Array; 56 | value: string; 57 | }>; 58 | noMorph?: boolean; 59 | } 60 | >; 61 | 62 | export const getEntitiesFromResponse = (resp: ProjectData): EntitiesDict => { 63 | const entities: EntitiesDict = {}; 64 | 65 | for (const { entity, records } of resp.entities) { 66 | entities[entity.name] = { 67 | noMorph: entity.noMorph, 68 | matchers: records.map(({ type, rule, value }) => { 69 | return { 70 | type, 71 | rule, 72 | value, 73 | }; 74 | }), 75 | }; 76 | } 77 | 78 | return entities; 79 | }; 80 | 81 | export const convertEntitiesForImport = (entities: EntitiesDict) => { 82 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 83 | const projectEntities: any[] = []; 84 | for (const [key, value] of Object.entries(entities)) { 85 | const projectEntity = { 86 | entity: { 87 | name: key, 88 | // Hardcoded for now but we've seen 'dictionary' type as well 89 | type: 'annotation', 90 | noMorph: value.noMorph, 91 | }, 92 | records: value.matchers, 93 | }; 94 | 95 | projectEntities.push(projectEntity); 96 | } 97 | 98 | return projectEntities; 99 | }; 100 | 101 | export const convertIntentsForImport = (intents: IntentsDict) => { 102 | const projectIntents: Intent[] = []; 103 | 104 | for (const [key, value] of Object.entries(intents)) { 105 | const variables = value.variables || {}; 106 | const slots: NonNullable = []; 107 | 108 | // eslint-disable-next-line no-shadow 109 | for (const [key, value] of Object.entries(variables)) { 110 | slots.push({ 111 | name: key, 112 | prompts: value.questions, 113 | array: value.array, 114 | entity: value.entity, 115 | }); 116 | } 117 | 118 | const patterns: string[] = []; 119 | const phrases: Phrase[] = []; 120 | 121 | const matchers = value.matchers ?? []; 122 | 123 | for (const { type, rule } of matchers) { 124 | switch (type) { 125 | case 'phrase': 126 | phrases.push({ 127 | text: rule, 128 | entities: null, 129 | stagedPhraseIdx: null, 130 | }); 131 | break; 132 | case 'pattern': 133 | patterns.push(rule); 134 | break; 135 | default: 136 | throw new Error(`Wrong matcher type: ${type}`); 137 | } 138 | } 139 | 140 | projectIntents.push({ 141 | path: key, 142 | enabled: true, 143 | answer: null, 144 | customData: null, 145 | description: null, 146 | id: Date.now(), 147 | patterns, 148 | phrases, 149 | slots, 150 | }); 151 | } 152 | 153 | return projectIntents; 154 | }; 155 | -------------------------------------------------------------------------------- /packages/recognizer-smartapp-brain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "lib": ["es2017"], 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "resolveJsonModule": true, 10 | "moduleResolution": "node", 11 | "strictNullChecks": true, 12 | "downlevelIteration": true 13 | }, 14 | "include": ["./src"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/recognizer-string-similarity/.npmrc: -------------------------------------------------------------------------------- 1 | ../../.npmrc -------------------------------------------------------------------------------- /packages/recognizer-string-similarity/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.10.0 (Tue Jul 06 2021) 2 | 3 | #### 🚀 Enhancement 4 | 5 | - feat: setAutoListening/setASRHints on res [#166](https://github.com/sberdevices/salutejs/pull/166) ([@Turanchoks](https://github.com/Turanchoks)) 6 | 7 | #### 🐛 Bug Fix 8 | 9 | - Bump independent versions \[skip ci\] ([@Yeti-or](https://github.com/Yeti-or)) 10 | - fix: prepare step [#193](https://github.com/sberdevices/salutejs/pull/193) ([@Yeti-or](https://github.com/Yeti-or)) 11 | - fix(recognizer-string-similarity): lint errors [#145](https://github.com/sberdevices/salutejs/pull/145) ([@awinogradov](https://github.com/awinogradov)) 12 | - fix: enable strictNullChecks [#145](https://github.com/sberdevices/salutejs/pull/145) ([@awinogradov](https://github.com/awinogradov)) 13 | - chore: split recognizer package [#86](https://github.com/sberdevices/salutejs/pull/86) ([@awinogradov](https://github.com/awinogradov)) 14 | 15 | #### ⚠️ Pushed to `master` 16 | 17 | - chore(release): publish ([@awinogradov](https://github.com/awinogradov)) 18 | 19 | #### Authors: 3 20 | 21 | - Pavel Remizov ([@Turanchoks](https://github.com/Turanchoks)) 22 | - Tony Vi ([@awinogradov](https://github.com/awinogradov)) 23 | - Vasiliy ([@Yeti-or](https://github.com/Yeti-or)) 24 | -------------------------------------------------------------------------------- /packages/recognizer-string-similarity/README.md: -------------------------------------------------------------------------------- 1 | # @salutejs/recognizer-string-similarity 2 | 3 | Набор стандартных рекогнайзеров для распознования реплик пользователей. 4 | 5 | > npm i -S @salutejs/recognizer-string-similarity 6 | 7 | ## String Similarity 8 | 9 | Рекогнайзер, основанный на вычисленнии схожести реплик. Схожесть вычисляется посредством [коэффициента Сёренсена](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D1%8D%D1%84%D1%84%D0%B8%D1%86%D0%B8%D0%B5%D0%BD%D1%82_%D0%A1%D1%91%D1%80%D0%B5%D0%BD%D1%81%D0%B5%D0%BD%D0%B0). Под капотом используется пакет [string-similariy](https://github.com/aceakash/string-similarity) — реализация алгоритма на JS. 10 | 11 | ``` ts 12 | import { createScenarioWalker } from '@salutejs/scenario'; 13 | import { StringSimilarityRecognizer } from '@salutejs/recognizer'; 14 | 15 | import { intents } from './intents'; 16 | 17 | const scenarioWalker = createScenarioWalker({ 18 | // ... 19 | recognizer: new StringSimilarityRecognizer({ intents }), 20 | // ... 21 | }); 22 | ``` 23 | 24 | #### SberDevices with :heart: 25 | -------------------------------------------------------------------------------- /packages/recognizer-string-similarity/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/recognizer-string-similarity", 3 | "version": "0.25.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "14.14.25", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz", 10 | "integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ==", 11 | "dev": true 12 | }, 13 | "string-similarity": { 14 | "version": "4.0.4", 15 | "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", 16 | "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==" 17 | }, 18 | "typescript": { 19 | "version": "4.1.3", 20 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", 21 | "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", 22 | "dev": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/recognizer-string-similarity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/recognizer-string-similarity", 3 | "version": "0.25.0", 4 | "description": "String Similarity recognizer for SaluteJS", 5 | "author": "SberDevices Frontend Team ", 6 | "license": "Sber Public License at-nc-sa v.2", 7 | "main": "dist/index.js", 8 | "typings": "dist/index.d.ts", 9 | "homepage": "https://github.com/sberdevices/salutejs#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sberdevices/salutejs.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/sberdevices/salutejs/issues" 16 | }, 17 | "files": [ 18 | "dist", 19 | "README.md", 20 | "LICENSE.txt" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "prepare": "npm run build", 27 | "build": "rm -rf dist && tsc", 28 | "lint": "eslint --ext .js,.ts ./src" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "14.14.25", 32 | "typescript": "4.1.3" 33 | }, 34 | "dependencies": { 35 | "@salutejs/scenario": "0.25.0", 36 | "string-similarity": "4.0.4" 37 | }, 38 | "gitHead": "70298e562f327c87c550171469ad7a55e0d88cf6" 39 | } 40 | -------------------------------------------------------------------------------- /packages/recognizer-string-similarity/src/index.ts: -------------------------------------------------------------------------------- 1 | import ss from 'string-similarity'; 2 | import { Inference, IntentsDict, TextIntent, Recognizer, Variant } from '@salutejs/scenario'; 3 | 4 | function getRestOfMessageText(message) { 5 | const { original_text } = message; 6 | 7 | const res: string[] = []; 8 | const words = original_text.split(' '); 9 | 10 | for (let i = 1; i < words.length; i++) { 11 | const word = words[i]; 12 | 13 | res.push(word.substr(0, 1).toUpperCase() + word.substr(1)); 14 | } 15 | 16 | return res.join(' '); 17 | } 18 | 19 | const SLOT_FILLING_NOTE = 'купить хлеб'; 20 | 21 | export class StringSimilarityRecognizer implements Recognizer { 22 | private _intents: IntentsDict; 23 | 24 | constructor({ intents }: { intents: IntentsDict }) { 25 | this._intents = intents; 26 | } 27 | 28 | inference = async ({ req }) => { 29 | if (!req.message || req.server_action) { 30 | return Promise.resolve(); 31 | } 32 | 33 | const { tokenized_elements_list: tokens } = req.message; 34 | const matches = Object.keys(this._intents).reduce((arr, key) => { 35 | const intent = this._intents[key] as TextIntent; 36 | if (intent.matchers?.length) { 37 | arr.push({ key, matchers: intent.matchers, rating: 0 }); 38 | } 39 | 40 | return arr; 41 | }, [] as Array<{ key: string; matchers: TextIntent['matchers']; rating: number }>); 42 | 43 | for (let i = 0; i < tokens.length; i++) { 44 | const token = tokens[i]; 45 | 46 | if (token.token_type === 'SENTENCE_ENDPOINT_TOKEN') { 47 | break; 48 | } 49 | 50 | for (let j = 0; j < matches.length; j++) { 51 | const { matchers } = matches[j]; 52 | const { bestMatch } = ss.findBestMatch(token.lemma, matchers); 53 | 54 | if (bestMatch.rating > 0.5 && bestMatch.rating > matches[j].rating) { 55 | matches[j].rating = bestMatch.rating; 56 | } 57 | } 58 | } 59 | 60 | const result: Inference = { 61 | variants: matches 62 | .reduce((arr, match) => { 63 | if (match.rating === 0) { 64 | return arr; 65 | } 66 | 67 | const intent: Variant = { 68 | intent: { 69 | id: 0, 70 | path: match.key, 71 | slots: [], 72 | }, 73 | confidence: match.rating, 74 | slots: [], 75 | }; 76 | 77 | arr.push(intent); 78 | 79 | const vars = this._intents[match.key].variables; 80 | if (vars != null && getRestOfMessageText(req.message)) { 81 | intent.slots.push({ 82 | name: `${vars[0] || 'note'}`, 83 | value: getRestOfMessageText(req.message), 84 | array: false, 85 | }); 86 | } 87 | 88 | return arr; 89 | }, [] as Variant[]) 90 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 91 | .sort((a: any, b: any) => b - a), 92 | }; 93 | 94 | // детект ответа на дозапрос note для done_note 95 | if (!result.variants.length && req.message.original_text === SLOT_FILLING_NOTE) { 96 | result.variants.push({ 97 | confidence: 0.7, 98 | intent: { 99 | id: 0, 100 | path: 'done_note', 101 | slots: [], 102 | }, 103 | slots: [ 104 | { 105 | name: 'note', 106 | value: SLOT_FILLING_NOTE, 107 | array: false, 108 | }, 109 | ], 110 | }); 111 | } 112 | 113 | req.setInference(result); 114 | 115 | return Promise.resolve(); 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /packages/recognizer-string-similarity/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "lib": ["es2017"], 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "resolveJsonModule": true, 10 | "moduleResolution": "node", 11 | "strictNullChecks": true 12 | }, 13 | "include": ["./src"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/scenario/.npmrc: -------------------------------------------------------------------------------- 1 | ../../.npmrc -------------------------------------------------------------------------------- /packages/scenario/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/scenario", 3 | "version": "0.25.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@salutejs/types": { 8 | "version": "0.6.0", 9 | "resolved": "https://registry.npmjs.org/@salutejs/types/-/types-0.6.0.tgz", 10 | "integrity": "sha512-FJrlzLFO7g9OeCY4JOXRTEuZ+ktVh1bJi32KbMrpBU/DNPJv60MhtdaU7FJONPOMIV44dfZVy2MzbfI3pDG63A==" 11 | }, 12 | "@types/node": { 13 | "version": "14.14.41", 14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.41.tgz", 15 | "integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==", 16 | "dev": true 17 | }, 18 | "@types/node-fetch": { 19 | "version": "2.5.10", 20 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.10.tgz", 21 | "integrity": "sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ==", 22 | "dev": true, 23 | "requires": { 24 | "@types/node": "*", 25 | "form-data": "^3.0.0" 26 | } 27 | }, 28 | "asynckit": { 29 | "version": "0.4.0", 30 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 31 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 32 | "dev": true 33 | }, 34 | "combined-stream": { 35 | "version": "1.0.8", 36 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 37 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 38 | "dev": true, 39 | "requires": { 40 | "delayed-stream": "~1.0.0" 41 | } 42 | }, 43 | "delayed-stream": { 44 | "version": "1.0.0", 45 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 46 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 47 | "dev": true 48 | }, 49 | "form-data": { 50 | "version": "3.0.1", 51 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", 52 | "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", 53 | "dev": true, 54 | "requires": { 55 | "asynckit": "^0.4.0", 56 | "combined-stream": "^1.0.8", 57 | "mime-types": "^2.1.12" 58 | } 59 | }, 60 | "mime-db": { 61 | "version": "1.47.0", 62 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", 63 | "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", 64 | "dev": true 65 | }, 66 | "mime-types": { 67 | "version": "2.1.30", 68 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", 69 | "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", 70 | "dev": true, 71 | "requires": { 72 | "mime-db": "1.47.0" 73 | } 74 | }, 75 | "node-fetch": { 76 | "version": "2.6.1", 77 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 78 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" 79 | }, 80 | "typescript": { 81 | "version": "4.1.3", 82 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", 83 | "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", 84 | "dev": true 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/scenario/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/scenario", 3 | "version": "0.25.0", 4 | "description": "Tiny helpers to make scenario for Salute family", 5 | "author": "SberDevices Frontend Team ", 6 | "license": "Sber Public License at-nc-sa v.2", 7 | "main": "dist/index.js", 8 | "typings": "dist/index.d.ts", 9 | "homepage": "https://github.com/sberdevices/salutejs#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sberdevices/salutejs.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/sberdevices/salutejs/issues" 16 | }, 17 | "files": [ 18 | "dist", 19 | "README.md", 20 | "LICENSE.txt" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "prepare": "npm run build", 27 | "build": "rm -rf dist && tsc", 28 | "build:w": "rm -rf dist && tsc -w", 29 | "lint": "eslint --ext .js,.ts ./src", 30 | "test": "../../node_modules/.bin/jest --config ../../jest.config.ts" 31 | }, 32 | "devDependencies": { 33 | "@types/node-fetch": "2.5.10", 34 | "typescript": "4.1.3" 35 | }, 36 | "dependencies": { 37 | "node-fetch": "2.6.1" 38 | }, 39 | "gitHead": "70298e562f327c87c550171469ad7a55e0d88cf6" 40 | } 41 | -------------------------------------------------------------------------------- /packages/scenario/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/createSystemScenario'; 2 | export * from './lib/createUserScenario'; 3 | export * from './lib/createSaluteRequest'; 4 | export * from './lib/createSaluteResponse'; 5 | export * from './lib/missingVariables'; 6 | export * from './lib/createScenarioWalker'; 7 | export * from './lib/matchers'; 8 | export * from './lib/createIntents'; 9 | export { 10 | createInvoice, 11 | findInvoiceById, 12 | findInvoiceByServiceIdOrderId, 13 | completeInvoice, 14 | reverseInvoice, 15 | refundInvoice, 16 | } from './lib/smartpay'; 17 | export { createSmartPushSender, SendPushConfiguration } from './lib/smartpush'; 18 | export * from './lib/types/payment'; 19 | 20 | export * from './lib/types/i18n'; 21 | export * from './lib/types/payment'; 22 | export * from './lib/types/push'; 23 | export * from './lib/types/request'; 24 | export * from './lib/types/response'; 25 | export * from './lib/types/salute'; 26 | export * from './lib/types/storage'; 27 | export * from './lib/types/systemMessage'; 28 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/createIntents.ts: -------------------------------------------------------------------------------- 1 | import { IntentsDict } from './types/salute'; 2 | 3 | export const createIntents = (intents: G): G => intents; 4 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/createSaluteRequest.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from './i18n'; 2 | import { KeysetDictionary } from './types/i18n'; 3 | import { NLPRequest, NLPRequestMTS, NLPRequestSA } from './types/request'; 4 | import { Inference, SaluteRequest, SaluteRequestVariable, Variant } from './types/salute'; 5 | 6 | export const createSaluteRequest = (request: NLPRequest): SaluteRequest => { 7 | let inference: Inference; 8 | let variant: Variant; 9 | const variables: SaluteRequestVariable = {}; 10 | 11 | return { 12 | get character() { 13 | return request.payload.character.id; 14 | }, 15 | get appInfo() { 16 | return (request as NLPRequestSA).payload.app_info; 17 | }, 18 | get message() { 19 | return (request as NLPRequestMTS).payload.message; 20 | }, 21 | get systemIntent() { 22 | return (request as NLPRequestMTS).payload.intent; 23 | }, 24 | get variant() { 25 | return variant; 26 | }, 27 | get profile() { 28 | if (request.messageName === 'TAKE_PROFILE_DATA') { 29 | return request.payload.profile_data; 30 | } 31 | 32 | return undefined; 33 | }, 34 | get inference() { 35 | return inference; 36 | }, 37 | get request() { 38 | return request; 39 | }, 40 | get state() { 41 | return (request as NLPRequestMTS).payload.meta?.current_app?.state; 42 | }, 43 | get serverAction() { 44 | return (request as NLPRequestSA).payload.server_action; 45 | }, 46 | get voiceAction() { 47 | return ( 48 | !(request as NLPRequestSA).payload.server_action && 49 | (request as NLPRequestMTS).payload.intent !== 'close_app' && 50 | (request as NLPRequestMTS).payload.intent !== 'run_app' 51 | ); 52 | }, 53 | get variables() { 54 | return variables; 55 | }, 56 | setInference: (value: Inference) => { 57 | inference = value; 58 | }, 59 | setVariable: (name: string, value: string) => { 60 | variables[name] = value; 61 | }, 62 | i18n: (keyset: KeysetDictionary) => { 63 | return i18n(request.payload.character.id)(keyset); 64 | }, 65 | setVariant(v) { 66 | variant = v; 67 | }, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/createScenarioWalker.ts: -------------------------------------------------------------------------------- 1 | import { createUserScenario } from './createUserScenario'; 2 | import { SystemScenario } from './createSystemScenario'; 3 | import { lookupMissingVariables } from './missingVariables'; 4 | import { 5 | IntentsDict, 6 | Recognizer, 7 | SaluteRequest, 8 | SaluteRequestVariable, 9 | SaluteResponse, 10 | SaluteSession, 11 | } from './types/salute'; 12 | import { AppState } from './types/systemMessage'; 13 | 14 | interface ScenarioWalkerOptions { 15 | intents?: IntentsDict; 16 | recognizer?: Recognizer; 17 | systemScenario: SystemScenario; 18 | userScenario?: ReturnType; 19 | slotFillingConfidence?: number; 20 | } 21 | 22 | export const createScenarioWalker = ({ 23 | intents, 24 | recognizer, 25 | systemScenario, 26 | userScenario, 27 | slotFillingConfidence = 0, 28 | }: ScenarioWalkerOptions) => async ({ 29 | req, 30 | res, 31 | session, 32 | }: { 33 | req: SaluteRequest; 34 | res: SaluteResponse; 35 | session: SaluteSession; 36 | }) => { 37 | const dispatch = async (path: string[]) => { 38 | if (!userScenario) return; 39 | 40 | const state = userScenario.getByPath(path); 41 | 42 | if (state) { 43 | session.path = path; 44 | req.currentState = { 45 | path: session.path, 46 | state, 47 | }; 48 | 49 | if (req.variant && intents) { 50 | // SLOTFILING LOGIC START 51 | let currentIntent = req.variant; 52 | 53 | if (session.path.length > 0 && session.slotFilling) { 54 | // ищем связь с текущим интентом в сессии и результатах распознавания 55 | const connected = (req.inference?.variants || []).find( 56 | (v) => v.confidence >= slotFillingConfidence && v.intent.path === session.currentIntent, 57 | ); 58 | currentIntent = connected || req.variant; 59 | } 60 | 61 | const currentIntentPath = currentIntent.intent.path; 62 | session.currentIntent = currentIntentPath; 63 | 64 | // Here we substitue some variables even if their name is different 65 | // it is important for slot filling since smart app brain can't tell 66 | // a variable name if there are multiple slots with the same entity type 67 | // in a single intent. 68 | currentIntent.slots.forEach((slot) => { 69 | if (slot.array) { 70 | if (typeof req.variables[slot.name] === 'undefined') { 71 | req.setVariable(slot.name, []); 72 | } 73 | 74 | ((req.variables[slot.name] as unknown) as Array).push(slot.value); 75 | return; 76 | } 77 | 78 | if (slot.name in req.variables && session.missingVariableName) { 79 | const variableName = slot.name; 80 | const areSlotTypesEqual = 81 | intents[currentIntentPath]?.variables?.[session.missingVariableName].entity === 82 | intents[currentIntentPath]?.variables?.[variableName].entity; 83 | 84 | if (areSlotTypesEqual) { 85 | req.setVariable(session.missingVariableName, slot.value); 86 | delete session.missingVariableName; 87 | } 88 | } else { 89 | req.setVariable(slot.name, slot.value); 90 | } 91 | }); 92 | 93 | // ищем незаполненные переменные, задаем вопрос пользователю 94 | const missingVars = lookupMissingVariables(currentIntentPath, intents, req.variables); 95 | if (missingVars.length > 0) { 96 | // сохраняем состояние в сессии 97 | Object.keys(req.variables).forEach((name) => { 98 | session.variables[name] = req.variables[name]; 99 | }); 100 | 101 | // задаем вопрос 102 | const { question, name } = missingVars[0]; 103 | 104 | session.missingVariableName = name; 105 | 106 | res.appendBubble(question); 107 | res.setPronounceText(question); 108 | 109 | // устанавливаем флаг слотфиллинга, на него будем смотреть при следующем запросе пользователя 110 | session.slotFilling = true; 111 | 112 | return; 113 | } 114 | // SLOTFILING LOGIC END 115 | } 116 | 117 | await state.handle({ req, res, session: session.state, history: {} }, dispatch); 118 | } 119 | }; 120 | 121 | const saluteHandlerOpts = { req, res, session: session.state, history: {} }; 122 | 123 | if (req.systemIntent === 'run_app') { 124 | if (req.serverAction?.action_id === 'PAY_DIALOG_FINISHED') { 125 | if (typeof systemScenario.PAY_DIALOG_FINISHED === 'undefined') { 126 | res.appendError({ 127 | code: 404, 128 | description: 'Missing handler for action: "PAY_DIALOG_FINISHED"', 129 | }); 130 | return; 131 | } 132 | 133 | systemScenario.PAY_DIALOG_FINISHED(saluteHandlerOpts, dispatch); 134 | return; 135 | } 136 | 137 | await systemScenario.RUN_APP(saluteHandlerOpts, dispatch); 138 | return; 139 | } 140 | 141 | if (req.systemIntent === 'close_app') { 142 | systemScenario.CLOSE_APP(saluteHandlerOpts, dispatch); 143 | return; 144 | } 145 | 146 | // restore request from session 147 | Object.keys(session.variables).forEach((name) => { 148 | req.setVariable(name, session.variables[name]); 149 | }); 150 | 151 | if (typeof intents !== undefined && userScenario) { 152 | // restore request from server_action payload 153 | if (req.serverAction) { 154 | Object.keys((req.serverAction.payload || {}) as Record).forEach((key) => { 155 | req.setVariable(key, (req.serverAction?.payload as Record)[key]); 156 | }); 157 | } 158 | 159 | if (req.voiceAction && typeof recognizer !== 'undefined') { 160 | await recognizer.inference({ req, res, session }); 161 | } 162 | 163 | const scenarioState = userScenario.resolve(session.path, req); 164 | 165 | if (req.serverAction && typeof intents !== 'undefined') { 166 | if (!scenarioState) { 167 | res.appendError({ 168 | code: 404, 169 | description: `Missing handler for action: "${req.serverAction.type}"`, 170 | }); 171 | 172 | return; 173 | } 174 | 175 | const missingVars = lookupMissingVariables(req.serverAction.type, intents, req.variables); 176 | if (missingVars.length) { 177 | res.appendError({ 178 | code: 500, 179 | description: `Missing required variables: ${missingVars.map(({ name }) => `"${name}"`).join(', ')}`, 180 | }); 181 | 182 | return; 183 | } 184 | } 185 | 186 | if (scenarioState) { 187 | req.currentState = scenarioState; 188 | await dispatch(scenarioState.path); 189 | 190 | if (!req.currentState.state.children && !session.slotFilling) { 191 | session.path = []; 192 | session.variables = {}; 193 | session.currentIntent = undefined; 194 | } 195 | 196 | return; 197 | } 198 | } 199 | 200 | systemScenario.NO_MATCH(saluteHandlerOpts, dispatch); 201 | }; 202 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/createSystemScenario.ts: -------------------------------------------------------------------------------- 1 | import { SaluteHandler } from './types/salute'; 2 | 3 | export type SystemScenario = { 4 | RUN_APP: SaluteHandler; 5 | CLOSE_APP: SaluteHandler; 6 | NO_MATCH: SaluteHandler; 7 | PAY_DIALOG_FINISHED?: SaluteHandler; 8 | }; 9 | 10 | export const createSystemScenario = (systemScenarioSchema?: Partial): SystemScenario => { 11 | return { 12 | RUN_APP: ({ res }) => { 13 | res.setPronounceText('Добро пожаловать!'); 14 | }, 15 | CLOSE_APP: () => {}, 16 | NO_MATCH: ({ res }) => { 17 | res.setPronounceText('Не понимаю'); 18 | }, 19 | ...systemScenarioSchema, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/createUserScenario.ts: -------------------------------------------------------------------------------- 1 | import { SaluteRequest, ScenarioSchema } from './types/salute'; 2 | 3 | export function createUserScenario(scenarioSchema: ScenarioSchema) { 4 | /** 5 | * Возвращает вложенные обработчики для указанного пути в дереве диалогов 6 | * @param path путь в дереве диалога 7 | * @returns undefined или потомки 8 | */ 9 | const getByPath = (path: string[]) => { 10 | let obj = scenarioSchema[path[0]]; 11 | for (const p of path.slice(1)) { 12 | if (obj.children) { 13 | obj = obj.children[p]; 14 | } else { 15 | return undefined; 16 | } 17 | } 18 | 19 | return obj; 20 | }; 21 | 22 | /** 23 | * Возвращает обработчик запроса для указанного пути в дереве диалогов 24 | * @param path путь в дереве диалога, поиск будет выполнен среди вложенных обработчиков 25 | * @param req объект запроса 26 | * @returns Возвращает объект вида { path, state }, где state - обработчик, path - путь из дерева диалогов 27 | */ 28 | const resolve = (path: string[], req: R) => { 29 | let matchedState: { 30 | path: string[]; 31 | state: ScenarioSchema['string']; 32 | } | null = null; 33 | 34 | if (path.length > 0) { 35 | const state = getByPath(path); 36 | 37 | if (state?.children) { 38 | for (const el of Object.keys(state?.children)) { 39 | const nextState = state?.children[el]; 40 | if (nextState.match(req)) { 41 | matchedState = { state: nextState, path: [...path, el] }; 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | 48 | if (!matchedState) { 49 | for (const el of Object.keys(scenarioSchema)) { 50 | const nextState = scenarioSchema[el]; 51 | if (nextState.match(req)) { 52 | matchedState = { state: nextState, path: [el] }; 53 | break; 54 | } 55 | } 56 | } 57 | 58 | return matchedState; 59 | }; 60 | 61 | return { 62 | getByPath, 63 | resolve, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/i18n.spec.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from './i18n'; 2 | 3 | describe('i18n', () => { 4 | const keysetDict = { 5 | joy: { 6 | '{count} яблок': { 7 | many: '{count} apples', 8 | none: 'no apples', 9 | one: '{count} apple', 10 | some: '{count} apples', 11 | }, 12 | Пример: 'Example', 13 | 'ул. {street} неподалеку': '{street} st. nearby', 14 | }, 15 | sber: { 16 | '{count} яблок у {number} студентов': { 17 | many: '{count} яблок у {number} студентов', 18 | none: 'нет яблок', 19 | one: '{count} яблоко у {number} студентов', 20 | some: '{count} яблока у {number} студентов', 21 | }, 22 | Пример: 'Пример', 23 | 'ул. {street} неподалеку': 'ул. {street} совсем рядом', 24 | 'Только СБЕР': 'Только СБЕР', 25 | Привет: ['Дорый день!', 'Как долго вас не было!'], 26 | }, 27 | }; 28 | 29 | describe('sber (default)', () => { 30 | it('should handle pluralization', () => { 31 | const adapterI18n = i18n()(keysetDict); 32 | 33 | expect(adapterI18n('{count} яблок у {number} студентов', { count: 0, number: 42 })).toEqual('нет яблок'); 34 | expect(adapterI18n('{count} яблок у {number} студентов', { count: 1, number: 42 })).toEqual( 35 | '1 яблоко у 42 студентов', 36 | ); 37 | expect(adapterI18n('{count} яблок у {number} студентов', { count: 3, number: 42 })).toEqual( 38 | '3 яблока у 42 студентов', 39 | ); 40 | expect(adapterI18n('{count} яблок у {number} студентов', { count: 5, number: 42 })).toEqual( 41 | '5 яблок у 42 студентов', 42 | ); 43 | expect(adapterI18n('{count} яблок у {number} студентов', { count: 22, number: 42 })).toEqual( 44 | '22 яблока у 42 студентов', 45 | ); 46 | expect(adapterI18n('{count} яблок у {number} студентов', { count: 25, number: 42 })).toEqual( 47 | '25 яблок у 42 студентов', 48 | ); 49 | expect(adapterI18n('{count} яблок у {number} студентов', { count: 1001, number: 42 })).toEqual( 50 | '1001 яблоко у 42 студентов', 51 | ); 52 | }); 53 | 54 | it('should get random phrase', () => { 55 | const adapterI18n = i18n()(keysetDict); 56 | const phrase = adapterI18n('Привет'); 57 | 58 | expect(['Дорый день!', 'Как долго вас не было!'].includes(phrase)).toBeTruthy(); 59 | }); 60 | }); 61 | 62 | describe('joy', () => { 63 | it('should get simple text', () => { 64 | expect(i18n('joy')(keysetDict)('Пример')).toEqual('Example'); 65 | }); 66 | 67 | it('should substitute params', () => { 68 | expect(i18n('joy')(keysetDict)('ул. {street} неподалеку', { street: 'Тверская' })).toEqual( 69 | 'Тверская st. nearby', 70 | ); 71 | }); 72 | 73 | it('should handle pluralization', () => { 74 | const adapterI18n = i18n('joy')(keysetDict); 75 | 76 | expect(adapterI18n('{count} яблок', { count: 0 })).toEqual('no apples'); 77 | expect(adapterI18n('{count} яблок', { count: 1 })).toEqual('1 apple'); 78 | expect(adapterI18n('{count} яблок', { count: 2 })).toEqual('2 apples'); 79 | expect(adapterI18n('{count} яблок', { count: 1001 })).toEqual('1001 apple'); 80 | }); 81 | }); 82 | 83 | describe('missing translate', () => { 84 | it('missing in eva', () => { 85 | expect(i18n('eva')(keysetDict)('Только СБЕР')).toEqual('Только СБЕР'); 86 | }); 87 | 88 | it('missing in everywhere', () => { 89 | expect(i18n('eva')(keysetDict)('Нигде нет перевода')).toEqual('Нигде нет перевода'); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/i18n.ts: -------------------------------------------------------------------------------- 1 | import ruPlural from './plural/ru'; 2 | import { I18nBaseOptions, I18nOptions, I18nPluralOptions, IPluralForms, KeysetDictionary } from './types/i18n'; 3 | import { CharacterId } from './types/systemMessage'; 4 | 5 | export type PluralFunction = (count: number, params: IPluralForms) => string; 6 | 7 | const pluralMap = { 8 | ru: ruPlural, 9 | }; 10 | 11 | export type I18nRaw = Array | number | undefined>; 12 | /** 13 | * Поддерживаемые персонажи. 14 | */ 15 | export type Character = CharacterId; 16 | /** 17 | * Поддерживаемые языки. 18 | */ 19 | export type Language = keyof typeof pluralMap; 20 | /** 21 | * Текущий язык 22 | */ 23 | const _lang: Language = 'ru'; 24 | /** 25 | * Язык для замены, если нет перевода для текущего языка 26 | */ 27 | const _defLang: Language = 'ru'; 28 | /** 29 | * Подставляет параметры в шаблон локализационного ключа. 30 | * Разбирает ключ в массив: 'foo {bar} zoo {too}' => ['foo ', bar, ' zoo ', too] 31 | * 32 | * @param template шаблон ключа 33 | * @param options параметры для подстановки в шаблон 34 | */ 35 | function generateText(template: string, options: I18nBaseOptions): I18nRaw { 36 | const res: Array> = []; 37 | const len = template.length; 38 | let pos = 0; 39 | 40 | while (pos < len) { 41 | const p1 = template.indexOf('{', pos); 42 | if (p1 === -1) { 43 | // нет открывающих фигурных скобок - копируем весь остаток строки 44 | res.push(template.substring(pos)); 45 | return res; 46 | } 47 | 48 | const p2 = template.indexOf('}', p1); 49 | if (p2 === -1) { 50 | res.push(template.substring(pos)); 51 | // edge case: не хватает закрывающей фигурной скобки - копируем весь остаток строки 52 | // чтобы быть полностью совместимым с оригинальной реализацией, надо сделать 53 | // res.push( 54 | // template.substring(pos, p1), 55 | // template.substring(p1 + 1) 56 | // ); 57 | return res; 58 | } 59 | 60 | res.push(template.substring(pos, p1)); 61 | 62 | const opts = options[template.substring(p1 + 1, p2)]; 63 | if (opts) res.push(opts); 64 | 65 | pos = p2 + 1; 66 | } 67 | 68 | return res; 69 | } 70 | /** 71 | * Плюрализует локализационный ключ 72 | * 73 | * @param plural формы плюрализации 74 | * @param options динамические параметры ключа 75 | */ 76 | function generateTextWithPlural(plural: IPluralForms, options: I18nPluralOptions): I18nRaw { 77 | const pluralizer = pluralMap[_lang] || pluralMap[_defLang]; 78 | const template: string = pluralizer(options.count, plural); 79 | 80 | return generateText(template, options); 81 | } 82 | /** 83 | * Разбора ключа. 84 | * 85 | * @param character текущий персонаж 86 | * @param keyset словарь с переводами 87 | * @param key ключ для кейсета 88 | * @param options динамические параметры ключа 89 | */ 90 | function _i18n(character: Character, keyset: KeysetDictionary, key: string, options: I18nOptions = {}): I18nRaw { 91 | const keysetKey = (keyset[character] && keyset[character][key]) || keyset.sber[key]; 92 | 93 | if (Array.isArray(keysetKey)) { 94 | return generateText(keysetKey[Math.floor(Math.random() * keysetKey.length)], options); 95 | } 96 | 97 | if (typeof keysetKey === 'string') { 98 | return generateText(keysetKey, options); 99 | } 100 | 101 | if (keysetKey) { 102 | return generateTextWithPlural(keysetKey, options as I18nPluralOptions); 103 | } 104 | 105 | return [key]; 106 | } 107 | /** 108 | * Локализация ключей по словарю. 109 | * 110 | * @param character текущий персонаж 111 | * @param keyset словарь с переводами 112 | * @param key ключ для кейсета 113 | * @param options динамические параметры ключа 114 | */ 115 | export const i18n = (character: Character = 'sber') => (keyset: KeysetDictionary) => ( 116 | key: string, 117 | options: I18nOptions = {}, 118 | ) => _i18n(character, keyset, key, options).join(''); 119 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/matchers.ts: -------------------------------------------------------------------------------- 1 | import { IntentsDict, SaluteRequest } from './types/salute'; 2 | import { AppState } from './types/systemMessage'; 3 | 4 | export const compare = (expected, actual) => { 5 | if (typeof expected !== typeof actual) return false; 6 | if (typeof expected !== 'object' || expected === null) { 7 | return expected === actual; 8 | } 9 | 10 | if (Array.isArray(expected)) { 11 | return expected.every((exp) => [].some.call(actual, (act) => compare(exp, act))); 12 | } 13 | 14 | return Object.keys(expected).every((key) => { 15 | const ao = actual[key]; 16 | const eo = expected[key]; 17 | 18 | if (typeof eo === 'object' && eo !== null && ao !== null) { 19 | return compare(eo, ao); 20 | } 21 | if (typeof eo === 'boolean') { 22 | return eo !== (ao == null); 23 | } 24 | 25 | return ao === eo; 26 | }); 27 | }; 28 | 29 | export function createMatchers() { 30 | const intent = (expected: keyof I, { confidence }: { confidence: number } = { confidence: 0.7 }) => (req: R) => { 31 | if (!req.inference?.variants.length) return false; 32 | 33 | const variant = req.inference?.variants.find((v) => v.intent.path === expected && v.confidence >= confidence); 34 | 35 | if (variant) { 36 | req.setVariant(variant); 37 | return true; 38 | } 39 | 40 | return false; 41 | }; 42 | 43 | const text = (expected: string, { normalized = false }: { normalized: boolean } = { normalized: false }) => ( 44 | req: R, 45 | ) => { 46 | const testText = normalized ? req.message?.human_normalized_text : req.message?.original_text; 47 | 48 | return expected === testText; 49 | }; 50 | 51 | const state = (expected: Partial) => (req: R) => compare(expected, req.state); 52 | 53 | const action = (expected: string) => (req: R) => req.serverAction?.type === expected; 54 | 55 | const selectItems = (expected: AppState) => (req: R) => 56 | req.state?.item_selector?.items?.filter((i) => compare(expected, i)); 57 | 58 | const selectItem = (expected: AppState) => (req: R) => { 59 | const items = selectItems(expected)(req); 60 | 61 | if (items) return items[0]; 62 | }; 63 | 64 | const regexp = (re: RegExp, { normalized = true }: { normalized: boolean } = { normalized: true }) => (req: R) => { 65 | const testText = normalized ? req.message?.human_normalized_text : req.message?.original_text; 66 | 67 | const result = re.exec(testText); 68 | 69 | if (result === null) { 70 | return false; 71 | } 72 | 73 | if (result.groups) { 74 | Object.assign(req.variables, result.groups); 75 | } 76 | 77 | return true; 78 | }; 79 | 80 | const match = (...matchers: Array<(req: R) => boolean>) => (req: R): boolean => { 81 | for (let i = 0; i < matchers.length; i++) { 82 | if (!matchers[i](req)) return false; 83 | } 84 | 85 | return true; 86 | }; 87 | 88 | return { match, intent, text, state, selectItem, selectItems, action, regexp }; 89 | } 90 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/missingVariables.ts: -------------------------------------------------------------------------------- 1 | import { IntentsDict } from './types/salute'; 2 | 3 | type MissingVariablesType = { name: string; question: string; entity?: string }[]; 4 | 5 | export const lookupMissingVariables = ( 6 | intent: string, 7 | intents: IntentsDict, 8 | variables: Record, 9 | ): MissingVariablesType => { 10 | const missing: MissingVariablesType = []; 11 | const vars = intents[intent]?.variables || {}; 12 | 13 | Object.keys(vars).forEach((v) => { 14 | const { questions, required, entity } = vars[v]; 15 | if (questions && questions?.length && required && variables[v] === undefined) { 16 | const questionNo = Math.floor(Math.random() * questions.length); 17 | missing.push({ name: v, question: questions[questionNo], entity }); 18 | } 19 | }); 20 | 21 | return missing; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/plural/ru.ts: -------------------------------------------------------------------------------- 1 | import { IPluralForms } from '../types/i18n'; 2 | 3 | export default (count: number, params: IPluralForms): string => { 4 | const lastNumber = count % 10; 5 | const lastNumbers = count % 100; 6 | 7 | if (!count) { 8 | return params.none || ''; 9 | } 10 | if (lastNumber === 1 && lastNumbers !== 11) { 11 | return params.one; 12 | } 13 | if (lastNumber > 1 && lastNumber < 5 && (lastNumbers < 10 || lastNumbers > 20)) { 14 | return params.some; 15 | } 16 | 17 | return params.many || ''; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/smartpay.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | import { PaymentInvoiceQuery, PaymentInvoiceAnswer, PaymentResponse, PaymentStatus } from './types/payment'; 4 | 5 | const API_URL = 'https://smartmarket.online.sberbank.ru/smartpay/v1'; 6 | 7 | const callApi = ( 8 | url: string, 9 | { method, body }: { method?: 'patch' | 'post' | 'put' | 'delete'; body?: unknown } = {}, 10 | ): Promise => 11 | fetch(url, { 12 | headers: { 13 | accept: 'application/json', 14 | 'Content-Type': 'application/json', 15 | Authorization: `Bearer ${process.env.SMARTPAY_TOKEN}`, 16 | }, 17 | method: method || 'get', 18 | body: body ? JSON.stringify(body) : undefined, 19 | }).then((response) => response.json()); 20 | 21 | /** 22 | * Регистрирует платеж 23 | * @param invoice объект счета 24 | * @returns объект-статус регистрации платежа 25 | */ 26 | export const createInvoice = (invoice: PaymentInvoiceQuery): Promise => 27 | callApi(`${API_URL}/invoices`, { 28 | method: 'post', 29 | body: invoice, 30 | }); 31 | 32 | /** 33 | * Возвращает статус платежа по идентификатору счета 34 | * @param invoiceId идентификатор счета 35 | * @param params объект с параметрами запроса - статусом счета и временем ожидания результата 36 | * @returns объект-статус платежа 37 | */ 38 | export const findInvoiceById = ( 39 | invoiceId: string, 40 | params: { invStatus?: string; wait: number }, 41 | ): Promise => 42 | callApi( 43 | `${API_URL}/invoices/${encodeURIComponent(invoiceId)}${ 44 | params && (params.invStatus || typeof params.wait !== 'undefined') 45 | ? `?${params.invStatus ?? `inv_status=${encodeURIComponent(!params.invStatus)}`}${ 46 | params.wait ?? `${params.invStatus ?? '&'}wait=${encodeURIComponent(params.wait)}` 47 | }` 48 | : '' 49 | }`, 50 | ); 51 | 52 | /** 53 | * Возвращает статус платежа по идентификатору сервиса и заказа 54 | * @param serviceId Идентификатор сервиса, полученный при выдаче токена для авторизации запроса 55 | * @param orderId Идентификатор заказа для сервиса платежей 56 | * @param params объект с параметрами запроса - статусом счета и временем ожидания результата 57 | * @returns объект-статус платежа 58 | */ 59 | export const findInvoiceByServiceIdOrderId = ( 60 | serviceId: string, 61 | orderId: string, 62 | params: { invStatus?: string; wait?: number } = {}, 63 | ): Promise => 64 | callApi( 65 | `${API_URL}/invoices/0?service_id=${encodeURIComponent(serviceId)}&order_id=${encodeURIComponent(orderId)}${ 66 | params.invStatus ?? 67 | `&inv_status=${encodeURIComponent(!params.invStatus)}${ 68 | params.wait ?? `&wait=${encodeURIComponent(!params.wait)}` 69 | }` 70 | }`, 71 | ); 72 | 73 | /** 74 | * Завершает платеж 75 | * @param invoiceId идентификатор счета 76 | * @returns объект-результат запроса 77 | */ 78 | export const completeInvoice = (invoiceId: string): Promise => 79 | callApi(`${API_URL}/invoice/${encodeURIComponent(invoiceId)}`, { 80 | method: 'put', 81 | }); 82 | 83 | /** 84 | * Отменяет платеж 85 | * @param invoiceId идентификатор счета 86 | * @returns объект-результат запроса 87 | */ 88 | export const reverseInvoice = (invoiceId: string): Promise => 89 | callApi(`${API_URL}/invoice/${encodeURIComponent(invoiceId)}`, { 90 | method: 'delete', 91 | }); 92 | 93 | /** 94 | * Возвращает платеж 95 | * @param invoiceId идентификатор счета 96 | * @returns объект-результат запроса 97 | */ 98 | export const refundInvoice = (invoiceId: string): Promise => 99 | callApi(`${API_URL}/invoice/${encodeURIComponent(invoiceId)}`, { 100 | method: 'patch', 101 | }); 102 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/smartpush.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | import { DeliveryConfig, Destination, SmartPushRequest, SmartPushResponse } from './types/push'; 4 | import { DefaultChannels } from './types/systemMessage'; 5 | 6 | const URL = 'https://salute.online.sberbank.ru:9443/api/v2/smartpush/apprequest'; 7 | const TOKEN_URL = 'https://salute.online.sberbank.ru:9443/api/v2/oauth'; 8 | 9 | const uuidv4 = () => 10 | 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 11 | // eslint-disable-next-line no-bitwise 12 | const r = (Math.random() * 16) | 0; 13 | // eslint-disable-next-line no-bitwise 14 | const v = c === 'x' ? r : (r & 0x3) | 0x8; 15 | return v.toString(16); 16 | }); 17 | 18 | const requestAccesToken = async ({ 19 | clientId, 20 | secret, 21 | scope, 22 | requestId, 23 | }: { 24 | clientId: string; 25 | secret: string; 26 | scope: string[]; 27 | requestId: string; 28 | }): Promise<{ access_token?: string; expires_at?: number; code?: number; message?: string }> => { 29 | return fetch(TOKEN_URL, { 30 | headers: { 31 | Authorization: `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`, 32 | 'Content-Type': 'application/x-www-form-urlencoded', 33 | Rquid: requestId, 34 | }, 35 | method: 'POST', 36 | body: `scope=${scope}`, 37 | }).then((res) => res.json()); 38 | }; 39 | 40 | export interface SendPushConfiguration { 41 | projectId: string; 42 | clientIdSub: string; 43 | deliveryConfig: { 44 | destinations: Omit[]; 45 | } & Omit; 46 | } 47 | 48 | const sendPush = async ( 49 | accessToken: string, 50 | requestId: string, 51 | messageId: number, 52 | { projectId, clientIdSub, deliveryConfig }: SendPushConfiguration, 53 | ): Promise => { 54 | const { destinations, ...delivery } = deliveryConfig; 55 | const body: SmartPushRequest = { 56 | protocolVersion: 'V1', 57 | messageName: 'SEND_PUSH', 58 | messageId, 59 | payload: { 60 | sender: { 61 | projectId, 62 | }, 63 | recipient: { 64 | clientId: { 65 | idType: 'SUB', 66 | id: clientIdSub, 67 | }, 68 | }, 69 | deliveryConfig: { 70 | ...delivery, 71 | destinations: destinations.map(({ surface, ...destination }) => ({ 72 | ...destination, 73 | surface, 74 | channel: DefaultChannels[surface], 75 | })), 76 | }, 77 | }, 78 | }; 79 | 80 | const answer = await fetch(URL, { 81 | headers: { 82 | Authorization: `Bearer ${accessToken}`, 83 | 'Content-Type': 'application/json', 84 | Rquid: requestId, 85 | }, 86 | method: 'post', 87 | body: JSON.stringify({ requestPayload: body }), 88 | }); 89 | 90 | return answer.json(); 91 | }; 92 | 93 | export const createSmartPushSender = async ( 94 | { clientId, secret }: { clientId: string; secret: string } = { 95 | clientId: process.env.SMARTPUSH_CLIENTID || '', 96 | secret: process.env.SMARTPUSH_SECRET || '', 97 | }, 98 | ) => { 99 | if (!clientId || !secret) { 100 | throw new Error('clientId and secret must be defined'); 101 | } 102 | 103 | const accessTokenReqId = uuidv4(); 104 | const { access_token, message } = await requestAccesToken({ 105 | clientId, 106 | secret, 107 | scope: ['SMART_PUSH'], 108 | requestId: accessTokenReqId, 109 | }); 110 | 111 | if (typeof access_token === 'undefined') { 112 | throw new Error( 113 | `Authorization failed. Please, check clientId and secret values. RequestId: ${accessTokenReqId}. ${message}`, 114 | ); 115 | } 116 | 117 | let messageId = 0; 118 | 119 | return (push: SendPushConfiguration) => sendPush(access_token, uuidv4(), ++messageId, push); 120 | }; 121 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/types/i18n.ts: -------------------------------------------------------------------------------- 1 | export interface IPluralForms { 2 | one: string; 3 | some: string; 4 | many?: string; 5 | none?: string; 6 | } 7 | export type I18nBaseOptions = Record | number | undefined>; 8 | export type I18nPluralOptions = I18nBaseOptions & { 9 | count: number; 10 | }; 11 | export type I18nOptions = I18nBaseOptions | I18nPluralOptions; 12 | export type KeysetKey = string | string[] | IPluralForms; 13 | export type Keyset = Record; 14 | export type KeysetDictionary = Record; 15 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/types/request.ts: -------------------------------------------------------------------------------- 1 | import { SystemMessage, SystemMessageName, SystemMessagePayload } from './systemMessage'; 2 | 3 | export type NLPRequestType = 4 | | Extract 5 | | 'TAKE_PROFILE_DATA'; 6 | 7 | export interface NLPRequestBody extends Pick { 8 | /** Тип запроса */ 9 | messageName: T; 10 | /** 11 | * Коллекция, в которой в зависимости от потребителя 12 | * и messageName передается дополнительная информация. 13 | */ 14 | payload: P; 15 | } 16 | 17 | export type SharedRequestPayload = Pick< 18 | SystemMessagePayload, 19 | 'device' | 'app_info' | 'projectName' | 'strategies' | 'character' 20 | >; 21 | 22 | export type MTSPayload = SharedRequestPayload & 23 | Pick< 24 | SystemMessagePayload, 25 | | 'intent' 26 | | 'original_intent' 27 | | 'intent_meta' 28 | | 'meta' 29 | | 'selected_item' 30 | | 'new_session' 31 | | 'annotations' 32 | | 'message' 33 | >; 34 | 35 | /** MESSAGE_TO_SKILL */ 36 | export type NLPRequestMTS = NLPRequestBody, MTSPayload>; 37 | 38 | export interface SAPayload extends SharedRequestPayload, Pick { 39 | server_action?: { 40 | payload: unknown; 41 | type: string; 42 | }; 43 | } 44 | 45 | /** 46 | * SERVER_ACTION 47 | * Вы можете получать информацию о действиях пользователя в приложении, например, нажатии кнопок. 48 | * Вы также можете отслеживать фоновые действия полноэкранных приложений. 49 | */ 50 | export type NLPRequestSA = NLPRequestBody, SAPayload>; 51 | 52 | export interface RAPayload extends SharedRequestPayload { 53 | /** Интент, который приходит при запуске смартапа */ 54 | intent: 'run_app'; 55 | server_action?: { 56 | payload: unknown; 57 | type: string; 58 | }; 59 | } 60 | 61 | /** RUN_APP */ 62 | export type NLPRequestRA = NLPRequestBody, RAPayload>; 63 | 64 | /** 65 | * CLOSE_APP 66 | * Когда пользователь произносит команду для остановки приложения, 67 | * Ассистент передаёт сообщение CLOSE_APP в текущий открытый смартап и закрывает его. 68 | * Ассистент не ждёт ответа от смартапа. Содержимое сообщения совпадает с содержимым payload сообщения MESSAGE_TO_SKILL. 69 | */ 70 | export type NLPRequestСA = NLPRequestBody, MTSPayload>; 71 | 72 | type TakeProfileDataStatuses = 73 | // SUCCESS 74 | // Данные существуют и получено клиентское согласие 75 | | { 76 | code: 1; 77 | description: string; 78 | } 79 | // EMPTY DATA 80 | // Данные отсутствуют в профиле 81 | | { 82 | code: 100; 83 | description: string; 84 | } 85 | // CLIENT DENIED 86 | // Клиент отклонил автозаполнение 87 | | { 88 | code: 101; 89 | description: string; 90 | } 91 | // FORBIDDEN 92 | // Запрещенный вызов от смартапа для GET_PROFILE_DATA 93 | | { 94 | code: 102; 95 | description: string; 96 | } 97 | // FORBIDDEN REQUEST 98 | // Запрещенный вызов от смартапа для CHOOSE_PROFILE_DATA и DETAILED_PROFILE_DATA 99 | // в случае отсутствия клиентского согласия 100 | | { 101 | code: 103; 102 | description: string; 103 | } 104 | // Access Denied 105 | // Запрещенный вызов от смартапа для CHOOSE_PROFILE_DATA и DETAILED_PROFILE_DATA 106 | // в случае отсутствия прав на изменение или уточнение данных 107 | | { 108 | code: 104; 109 | description: string; 110 | }; 111 | 112 | export type NLPRequestTPD = NLPRequestBody< 113 | Extract, 114 | SharedRequestPayload & { 115 | profile_data: { 116 | customer_name?: string; 117 | surname?: string; 118 | patronymic?: string; 119 | address?: { 120 | address_string: string; 121 | address_type: string; 122 | alias: string; 123 | comment: string; 124 | confirmed: boolean; 125 | country: string; 126 | city: string; 127 | district: string; 128 | location: { 129 | latitude: number; 130 | longitude: number; 131 | }; 132 | last_used: boolean; 133 | apartment: string; 134 | entrance: string; 135 | floor: string; 136 | region: string; 137 | street: string; 138 | house: string; 139 | settlement?: string; 140 | }; 141 | phone_number?: string; 142 | }; 143 | status_code: TakeProfileDataStatuses; 144 | } 145 | >; 146 | 147 | export type NLPRequest = NLPRequestRA | NLPRequestСA | NLPRequestMTS | NLPRequestSA | NLPRequestTPD; 148 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/types/response.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SystemMessage, 3 | SystemMessageName, 4 | SystemMessagePayload, 5 | AssistantCommand, 6 | BubbleCommand, 7 | CardCommand, 8 | PolicyRunAppComand, 9 | AppInfo, 10 | } from './systemMessage'; 11 | 12 | export type NLPResponseType = 13 | | Extract 14 | | 'GET_PROFILE_DATA' 15 | | 'CHOOSE_PROFILE_DATA' 16 | | 'DETAILED_PROFILE_DATA'; 17 | 18 | export interface NLPResponseBody extends Pick { 19 | /** Тип ответа. Определяет логику обработки. */ 20 | messageName: T; 21 | /** Объект с данными, которые зависят от типа сообщения */ 22 | payload: P; 23 | } 24 | 25 | export type SharedResponsePayload = Pick; 26 | 27 | export type ATUItemsType = AssistantCommand | BubbleCommand | CardCommand | PolicyRunAppComand; 28 | 29 | export interface ATUPayload 30 | extends SharedResponsePayload, 31 | Pick< 32 | SystemMessagePayload, 33 | | 'auto_listening' 34 | | 'pronounceText' 35 | | 'pronounceTextType' 36 | | 'emotion' 37 | | 'suggestions' 38 | | 'intent' 39 | | 'asr_hints' 40 | > { 41 | /** Список команд и элементов интерфейса смартапа */ 42 | items: ATUItemsType[]; 43 | /** 44 | * Сообщает ассистенту о завершении работы смартапа. 45 | * В приложениях типа Canvas App необходимо самостоятельно закрывать окно приложения 46 | * после завершения работы смартапа. Для этого требуется передать ассистенту команду close_app 47 | * с помощью метода assistant.close() или window.AssistantHost.close(), 48 | * если вы не используете Assistant Client. 49 | */ 50 | finished: boolean; 51 | app_info?: AppInfo; 52 | } 53 | 54 | /** ANSWER_TO_USER Response */ 55 | export type NLPResponseATU = NLPResponseBody, ATUPayload>; 56 | 57 | export interface PRAPayload extends SharedResponsePayload { 58 | server_action: { 59 | app_info: 60 | | { 61 | systemName: string; 62 | } 63 | | { 64 | projectId: string; 65 | }; 66 | parameters?: Record; 67 | }; 68 | } 69 | 70 | /** POLICY_RUN_APP Response */ 71 | export type NLPResponsePRA = NLPResponseBody, PRAPayload>; 72 | 73 | export interface NFPayload extends SharedResponsePayload { 74 | /** Интент, который смартап получит в следующем ответе ассистента */ 75 | intent?: string; 76 | } 77 | 78 | /** NOTHING_FOUND Response */ 79 | export type NLPResponseNF = NLPResponseBody, NFPayload>; 80 | 81 | export interface EPayload extends SharedResponsePayload, Pick { 82 | /** Код ошибки */ 83 | code: number; 84 | /** Интент, который смартап получит в следующем ответе ассистента */ 85 | intent?: string; 86 | } 87 | 88 | /** ERROR Response */ 89 | export type NLPResponseE = NLPResponseBody, EPayload>; 90 | 91 | type ProfileDataFieldsType = 'address' | 'phone_number'; 92 | type ProfileDataPayloadFieldsType = { 93 | fields: Array; 94 | }; 95 | 96 | export type NLPResponseGPD = NLPResponseBody, SharedResponsePayload>; 97 | export type NLPResponseCPD = NLPResponseBody< 98 | Extract, 99 | SharedResponsePayload & ProfileDataPayloadFieldsType 100 | >; 101 | export type NLPResponseGDPD = NLPResponseBody< 102 | Extract, 103 | SharedResponsePayload & ProfileDataPayloadFieldsType 104 | >; 105 | 106 | export type NLPResponse = 107 | | NLPResponseATU 108 | | NLPResponseE 109 | | NLPResponseNF 110 | | NLPResponsePRA 111 | | NLPResponseGPD 112 | | NLPResponseCPD 113 | | NLPResponseGDPD; 114 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/types/salute.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable no-use-before-define */ 3 | import { 4 | AppInfo, 5 | CharacterId, 6 | AppState, 7 | Message, 8 | SmartAppErrorCommand, 9 | EmotionId, 10 | Card, 11 | Bubble, 12 | Button, 13 | ASRHints, 14 | PolicyRunAppComand, 15 | } from './systemMessage'; 16 | import { NLPRequest, NLPRequestTPD } from './request'; 17 | import { NLPResponse, NLPResponseCPD } from './response'; 18 | import { KeysetDictionary, I18nOptions } from './i18n'; 19 | 20 | interface IntentSlot { 21 | name: string; // имя сущности 22 | entity: string; // тип сущности 23 | required: boolean; // наличие сущности обязательно 24 | prompts: string[]; // ??? 25 | array: boolean; 26 | } 27 | 28 | interface FoundSlot { 29 | name: string; 30 | value: string; 31 | array: boolean; 32 | } 33 | 34 | export interface Intent { 35 | id: number; 36 | path: string; 37 | answer?: string; 38 | customData?: string; 39 | slots: IntentSlot[]; // сущности в фразе 40 | } 41 | 42 | export interface Variant { 43 | intent: Intent; 44 | confidence: number; // вероятностная оценка соответствия интента 45 | slots: FoundSlot[]; // распознанные сущности 46 | } 47 | 48 | export interface Inference { 49 | variants: Variant[]; 50 | } 51 | 52 | export interface SaluteCommand { 53 | type: string; 54 | payload?: { [key: string]: unknown }; 55 | } 56 | 57 | export type SaluteRequestVariable = Record; 58 | 59 | export interface SaluteRequest { 60 | readonly character: CharacterId; 61 | readonly appInfo: AppInfo; 62 | readonly message: Message; 63 | readonly serverAction?: A; 64 | readonly voiceAction: boolean; 65 | readonly systemIntent: string; 66 | readonly variant: Variant; 67 | readonly inference?: Inference; 68 | readonly request: NLPRequest; 69 | readonly state?: S; 70 | readonly variables: V; 71 | readonly profile?: NLPRequestTPD['payload']['profile_data']; 72 | setInference: (value: Inference) => void; 73 | setVariable: (name: string, value: unknown) => void; 74 | currentState?: { 75 | path: string[]; 76 | state: ScenarioSchema['string']; 77 | }; 78 | i18n: (keyset: KeysetDictionary) => (key: string, options?: I18nOptions) => string; 79 | setVariant: (intent: Variant) => void; 80 | } 81 | 82 | export interface SaluteResponse { 83 | appendBubble: (bubble: string, options?: { expand_policy?: Bubble['expand_policy']; markdown?: boolean }) => void; 84 | appendCard: (card: Card) => void; 85 | appendCommand: (command: T) => void; 86 | /** @deprecated */ 87 | appendItem: (command: any) => void; 88 | appendError: (error: SmartAppErrorCommand['smart_app_error']) => void; 89 | appendSuggestions: (suggestions: Array) => void; 90 | askPayment: (invoiceId: number) => void; 91 | finish: () => void; 92 | runApp: (server_action: PolicyRunAppComand['nodes']['server_action']) => void; 93 | setIntent: (text: string) => void; 94 | setPronounceText: (text: string, options?: { ssml?: boolean }) => void; 95 | setAutoListening: (value: boolean) => void; 96 | setASRHints: (hints: ASRHints) => void; 97 | setEmotion: (emotion: EmotionId) => void; 98 | getProfileData: () => void; 99 | chooseProfileData: (fields: NLPResponseCPD['payload']['fields']) => void; 100 | getDetailedProfileData: () => void; 101 | openDeepLink: (deepLink: string) => void; 102 | overrideFrontendEndpoint: (frontendEndpoint: string) => void; 103 | readonly message: NLPResponse; 104 | } 105 | 106 | export type SaluteHandler< 107 | Rq extends SaluteRequest = SaluteRequest, 108 | S extends Record = Record, 109 | Rs extends SaluteResponse = SaluteResponse, 110 | H extends Record = Record 111 | > = (options: { req: Rq; res: Rs; session: S; history: H }, dispatch?: (path: string[]) => void) => void; 112 | 113 | export interface SaluteIntentVariable { 114 | required?: boolean; 115 | questions?: string[]; 116 | array?: boolean; 117 | entity?: string; 118 | } 119 | 120 | export interface TextIntent { 121 | matchers: Array<{ 122 | type: string; 123 | rule: string; 124 | }>; 125 | } 126 | 127 | export interface ServerActionIntent { 128 | action: string; 129 | } 130 | 131 | export type SaluteIntent = ( 132 | | (Required & Partial) 133 | | (Required & Partial) 134 | ) & { 135 | variables?: Record; 136 | }; 137 | 138 | export type IntentsDict = Record; 139 | 140 | export interface SaluteSession { 141 | path: string[]; 142 | slotFilling: boolean; 143 | variables: { 144 | [key: string]: unknown; 145 | }; 146 | currentIntent?: string; 147 | state: Record; 148 | missingVariableName?: string; 149 | } 150 | 151 | export interface Recognizer { 152 | inference: (options: { req: SaluteRequest; res: SaluteResponse; session: SaluteSession }) => void; 153 | } 154 | 155 | export type ScenarioSchema = Record< 156 | string, 157 | { 158 | match: (req: Rq) => boolean; 159 | schema?: string; 160 | handle: Sh; 161 | children?: ScenarioSchema; 162 | } 163 | >; 164 | -------------------------------------------------------------------------------- /packages/scenario/src/lib/types/storage.ts: -------------------------------------------------------------------------------- 1 | import { SaluteSession } from './salute'; 2 | 3 | export interface SaluteSessionStorage { 4 | resolve: (id: string) => Promise; 5 | save: ({ id, session }: { id: string; session: SaluteSession }) => Promise; 6 | reset: (id: string) => Promise; 7 | } 8 | -------------------------------------------------------------------------------- /packages/scenario/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "lib": ["es2017"], 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "resolveJsonModule": true, 10 | "moduleResolution": "node", 11 | "strictNullChecks": true 12 | }, 13 | "include": ["./src"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/storage-adapter-firebase/.npmrc: -------------------------------------------------------------------------------- 1 | ../../.npmrc -------------------------------------------------------------------------------- /packages/storage-adapter-firebase/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.10.1 (Tue Jul 06 2021) 2 | 3 | #### 🐛 Bug Fix 4 | 5 | - fix: eslint errors [#196](https://github.com/sberdevices/salutejs/pull/196) ([@Yeti-or](https://github.com/Yeti-or)) 6 | 7 | #### Authors: 1 8 | 9 | - Vasiliy ([@Yeti-or](https://github.com/Yeti-or)) 10 | 11 | --- 12 | 13 | # v0.10.0 (Tue Jul 06 2021) 14 | 15 | #### 🚀 Enhancement 16 | 17 | - feat: setAutoListening/setASRHints on res [#166](https://github.com/sberdevices/salutejs/pull/166) ([@Turanchoks](https://github.com/Turanchoks)) 18 | - feat: package for storing session in firebase [#82](https://github.com/sberdevices/salutejs/pull/82) ([@snyuryev](https://github.com/snyuryev)) 19 | 20 | #### 🐛 Bug Fix 21 | 22 | - Bump independent versions \[skip ci\] ([@Yeti-or](https://github.com/Yeti-or)) 23 | - fix: prepare step [#193](https://github.com/sberdevices/salutejs/pull/193) ([@Yeti-or](https://github.com/Yeti-or)) 24 | 25 | #### ⚠️ Pushed to `master` 26 | 27 | - chore(release): publish ([@awinogradov](https://github.com/awinogradov)) 28 | 29 | #### Authors: 4 30 | 31 | - Pavel Remizov ([@Turanchoks](https://github.com/Turanchoks)) 32 | - Sergey Yuryev ([@snyuryev](https://github.com/snyuryev)) 33 | - Tony Vi ([@awinogradov](https://github.com/awinogradov)) 34 | - Vasiliy ([@Yeti-or](https://github.com/Yeti-or)) 35 | -------------------------------------------------------------------------------- /packages/storage-adapter-firebase/README.md: -------------------------------------------------------------------------------- 1 | # @salutejs/storage-adapter-firebase 2 | 3 | Адаптеры для работы с сессией пользователя на уровне сценария. 4 | 5 | > npm i -S @salutejs/storage-adapter-firebase 6 | 7 | ## SaluteMemoryStorage 8 | 9 | Адаптер для хранения сессии в Firebase Realtime database 10 | 11 | #### SberDevices with :heart: 12 | -------------------------------------------------------------------------------- /packages/storage-adapter-firebase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/storage-adapter-firebase", 3 | "version": "0.25.0", 4 | "description": "Firebase database adapter for Salute family", 5 | "author": "SberDevices Frontend Team ", 6 | "license": "Sber Public License at-nc-sa v.2", 7 | "main": "dist/index.js", 8 | "typings": "dist/index.d.ts", 9 | "homepage": "https://github.com/sberdevices/salutejs#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sberdevices/salutejs.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/sberdevices/salutejs/issues" 16 | }, 17 | "files": [ 18 | "dist", 19 | "README.md", 20 | "LICENSE.txt" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "prepare": "npm run build", 27 | "build": "rm -rf dist && tsc", 28 | "lint": "eslint --ext .js,.ts ./src" 29 | }, 30 | "devDependencies": { 31 | "firebase-admin": "9.2.0", 32 | "typescript": "4.1.3" 33 | }, 34 | "dependencies": { 35 | "@salutejs/scenario": "0.25.0" 36 | }, 37 | "gitHead": "70298e562f327c87c550171469ad7a55e0d88cf6" 38 | } 39 | -------------------------------------------------------------------------------- /packages/storage-adapter-firebase/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/firebase'; 2 | -------------------------------------------------------------------------------- /packages/storage-adapter-firebase/src/lib/firebase.ts: -------------------------------------------------------------------------------- 1 | import { SaluteSession, SaluteSessionStorage } from '@salutejs/scenario'; 2 | import * as admin from 'firebase-admin'; 3 | 4 | /** 5 | * Session storage adapter for Firebase Database 6 | */ 7 | export class SaluteFirebaseSessionStorage implements SaluteSessionStorage { 8 | /** Database in your Firebase. */ 9 | private db: admin.database.Database; 10 | 11 | /** Specific location in your Firebase Database for session data. */ 12 | private path: string; 13 | 14 | /** 15 | * @param {admin.database.Database} db Database in your Firebase 16 | * for session data. 17 | * @param {string} path Specific location in your Firebase Database 18 | * for session data. 19 | * @example 20 | * admin.initializeApp(functions.config().firebase); 21 | * const storage = new SaluteFirebaseSessionStorage(admin.database()); 22 | * @example 23 | * admin.initializeApp(functions.config().firebase); 24 | * const storage = 25 | * new SaluteFirebaseSessionStorage(admin.database(), 'sessions'); 26 | * @example 27 | * admin.initializeApp(functions.config().firebase); 28 | * const storage = 29 | * new SaluteFirebaseSessionStorage(admin.database(), 'sber/sessions'); 30 | */ 31 | constructor(db: admin.database.Database, path = 'sessions') { 32 | this.db = db; 33 | this.path = path.replace(/\/$/, ''); 34 | } 35 | 36 | /** 37 | * @param {string} id Session id 38 | * @returns {admin.database.Reference} Firebase DB reference 39 | */ 40 | getRef(id: string): admin.database.Reference { 41 | return this.db.ref(`${this.path}/${id}`); 42 | } 43 | 44 | /** 45 | * Resolve session data with session id 46 | * @param {string} id Salute session id. 47 | * @returns {Promise} A promise to resolve session data. 48 | */ 49 | async resolve(id: string): Promise { 50 | const { val: data } = await this.getRef(id).once('value'); 51 | return { 52 | path: [], 53 | variables: {}, 54 | slotFilling: false, 55 | state: {}, 56 | ...(data || {}), 57 | }; 58 | } 59 | 60 | /** 61 | * Reset session data with session id 62 | * @param {string} id Salute session id. 63 | * @returns {Promise} A promise to reset session data. 64 | */ 65 | reset(id: string): Promise { 66 | return this.getRef(id).remove(); 67 | } 68 | 69 | /** 70 | * Save session data with session id 71 | * @param {string} id Salute session id. 72 | * @param {SaluteSession} session Salute session data. 73 | * @returns {Promise} A promise to save session data. 74 | */ 75 | save({ id, session }: { id: string; session: SaluteSession }): Promise { 76 | const value = { ...session }; 77 | // remove undefined fields, because firebase crashes with undefined-values 78 | Object.keys(value).forEach((key) => typeof value[key] === 'undefined' && delete value[key]); 79 | 80 | return this.getRef(id).set(value); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/storage-adapter-firebase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "lib": ["es2017"], 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "resolveJsonModule": true, 10 | "moduleResolution": "node" 11 | }, 12 | "include": ["./src"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/storage-adapter-memory/.npmrc: -------------------------------------------------------------------------------- 1 | ../../.npmrc -------------------------------------------------------------------------------- /packages/storage-adapter-memory/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.10.0 (Tue Jul 06 2021) 2 | 3 | #### 🚀 Enhancement 4 | 5 | - feat: setAutoListening/setASRHints on res [#166](https://github.com/sberdevices/salutejs/pull/166) ([@Turanchoks](https://github.com/Turanchoks)) 6 | 7 | #### 🐛 Bug Fix 8 | 9 | - Bump independent versions \[skip ci\] ([@Yeti-or](https://github.com/Yeti-or)) 10 | - fix: prepare step [#193](https://github.com/sberdevices/salutejs/pull/193) ([@Yeti-or](https://github.com/Yeti-or)) 11 | - fix: enable strictNullChecks [#145](https://github.com/sberdevices/salutejs/pull/145) ([@awinogradov](https://github.com/awinogradov)) 12 | - chore: package-lock up to date [#106](https://github.com/sberdevices/salutejs/pull/106) ([@awinogradov](https://github.com/awinogradov)) 13 | - chore: split memory package [#105](https://github.com/sberdevices/salutejs/pull/105) ([@awinogradov](https://github.com/awinogradov)) 14 | 15 | #### ⚠️ Pushed to `master` 16 | 17 | - chore(release): publish ([@awinogradov](https://github.com/awinogradov)) 18 | 19 | #### Authors: 3 20 | 21 | - Pavel Remizov ([@Turanchoks](https://github.com/Turanchoks)) 22 | - Tony Vi ([@awinogradov](https://github.com/awinogradov)) 23 | - Vasiliy ([@Yeti-or](https://github.com/Yeti-or)) 24 | -------------------------------------------------------------------------------- /packages/storage-adapter-memory/README.md: -------------------------------------------------------------------------------- 1 | # @salutejs/storage-adapter-memory 2 | 3 | Адаптер для хранения сессии в памяти процесса. 4 | 5 | > npm i -S @salutejs/storage-adapter-memory 6 | 7 | ## Использование 8 | 9 | ``` ts 10 | import { createSaluteRequest, createSaluteResponse, createScenarioWalker } from '@salutejs/scenario'; 11 | import { SaluteMemoryStorage } from '@salutejs/memory'; 12 | import express from 'express'; 13 | 14 | //... 15 | 16 | const app = express(); 17 | app.use(express.json()); 18 | 19 | const storage = new SaluteMemoryStorage(); 20 | const scenarioWalker = createScenarioWalker({ 21 | intents, 22 | recognizer, 23 | systemScenario, 24 | userScenario, 25 | }); 26 | 27 | app.post('/', async ({ body }, response) => { 28 | const req = createSaluteRequest(body); 29 | const res = createSaluteResponse(body); 30 | const session = await storage.resolve(body.uuid.sessionId); 31 | 32 | await scenarioWalker({ req, res, session }); 33 | 34 | await storage.save({ id: body.uuid.sessionId, session }); 35 | 36 | response.status(200).json(res.message); 37 | }); 38 | 39 | ``` 40 | 41 | #### SberDevices with :heart: 42 | -------------------------------------------------------------------------------- /packages/storage-adapter-memory/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/storage-adapter-memory", 3 | "version": "0.25.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "typescript": { 8 | "version": "4.1.3", 9 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", 10 | "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/storage-adapter-memory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salutejs/storage-adapter-memory", 3 | "version": "0.25.0", 4 | "description": "In memory storage adapter for SaluteJS", 5 | "author": "SberDevices Frontend Team ", 6 | "license": "Sber Public License at-nc-sa v.2", 7 | "main": "dist/index.js", 8 | "typings": "dist/index.d.ts", 9 | "homepage": "https://github.com/sberdevices/salutejs#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sberdevices/salutejs.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/sberdevices/salutejs/issues" 16 | }, 17 | "files": [ 18 | "dist", 19 | "README.md", 20 | "LICENSE.txt" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "prepare": "npm run build", 27 | "build": "rm -rf dist && tsc", 28 | "lint": "eslint --ext .js,.ts ./src" 29 | }, 30 | "devDependencies": { 31 | "typescript": "4.1.3" 32 | }, 33 | "dependencies": { 34 | "@salutejs/scenario": "0.25.0" 35 | }, 36 | "gitHead": "70298e562f327c87c550171469ad7a55e0d88cf6" 37 | } 38 | -------------------------------------------------------------------------------- /packages/storage-adapter-memory/src/index.ts: -------------------------------------------------------------------------------- 1 | import { SaluteSession, SaluteSessionStorage } from '@salutejs/scenario'; 2 | 3 | export class SaluteMemoryStorage implements SaluteSessionStorage { 4 | private sessions: Record = {}; 5 | 6 | async resolve(id: string) { 7 | return Promise.resolve( 8 | this.sessions[id] || { 9 | path: [], 10 | variables: {}, 11 | slotFilling: false, 12 | state: {}, 13 | }, 14 | ); 15 | } 16 | 17 | async save({ id, session }: { id: string; session: SaluteSession }) { 18 | this.sessions[id] = session; 19 | 20 | return Promise.resolve(); 21 | } 22 | 23 | async reset(id: string) { 24 | this.sessions[id] = this.sessions[id] || { 25 | path: [], 26 | variables: {}, 27 | slotFilling: false, 28 | state: {}, 29 | }; 30 | 31 | return Promise.resolve(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/storage-adapter-memory/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "lib": ["es2017"], 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "resolveJsonModule": true, 10 | "moduleResolution": "node", 11 | "strictNullChecks": true 12 | }, 13 | "include": ["./src"] 14 | } 15 | --------------------------------------------------------------------------------