Compare commits
327 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c5d2ea0c8 | |||
| b7a4f69565 | |||
| 7b8a324658 | |||
| 59cfded2bb | |||
| 8dc8c05d7d | |||
| 0e4ec4fb97 | |||
| bb00677568 | |||
| 308b63f576 | |||
| 40819e8e17 | |||
| 0fba50905f | |||
| 419bfe3d78 | |||
| 1847248f12 | |||
| eed4a9597c | |||
| c3452d2170 | |||
| 0a14567776 | |||
| 71573b78d1 | |||
| 722dba875b | |||
| 593419639e | |||
| 8b2f06e305 | |||
| 12e4b7bccf | |||
| 63a6c2d161 | |||
| f0e898e3c0 | |||
| 10bb4af4dc | |||
| 1af3fc2011 | |||
| 781cb072a3 | |||
| 73fe21f02e | |||
| 0947bd7877 | |||
| 07d2a41ecb | |||
| 9a66b677e0 | |||
| fe580cfb92 | |||
| 075f4458a0 | |||
| 094b1218e6 | |||
| b16475766a | |||
| e5fdeba50b | |||
| 12b0fe6525 | |||
| 302e1e11e0 | |||
| 8d5bca0d2b | |||
| 994cca7bf4 | |||
| a03afa78be | |||
| a1f93a6922 | |||
| 2678f6f04a | |||
| 03acb35e84 | |||
| 337854ab02 | |||
| 19c569b76a | |||
| 5b4cd9da81 | |||
| 40069ab5f0 | |||
| 8c89cbf43c | |||
| 5061cac591 | |||
| f660506d98 | |||
| 25f81d3c3c | |||
| d261967920 | |||
| 907fb1a129 | |||
| 7d63108e8d | |||
| 274d7d52fd | |||
| d815ab8bff | |||
| 35da3a388a | |||
| a29296c612 | |||
| b93180a822 | |||
| 9fa0c2e1ba | |||
| 2115054de6 | |||
| f167f64cfb | |||
| abc6b21df2 | |||
| bd66bb5cb7 | |||
| c49f931da4 | |||
| 070f0e1469 | |||
| bf4806c987 | |||
| 37e5ca0e87 | |||
| a6011e3cc6 | |||
| 44882129a7 | |||
| 8282c37a5a | |||
| 26ff4520c8 | |||
| e2f0df2cb6 | |||
| 7259ceda59 | |||
| 7f99372130 | |||
| 0cbd438a34 | |||
| 71b30a7f97 | |||
| bfdc60fa65 | |||
| 99af936803 | |||
| d41e960373 | |||
| ae04ba369c | |||
| afff309d43 | |||
| 16c25c9036 | |||
| cd551197ea | |||
| f6077d58f1 | |||
| eb026cb74a | |||
| 7d6698df17 | |||
| a215fc8770 | |||
| a8e3fb8b87 | |||
| 6820501da6 | |||
| b51bf4b1c2 | |||
| 5369da1734 | |||
| d5d8d56124 | |||
| fbcc089e2b | |||
| 76dacb4f5e | |||
| 24ec0f0927 | |||
| b8aac89c4d | |||
| 72a4f787a3 | |||
| 137032b1c0 | |||
| 5011d906e7 | |||
| 35d171e421 | |||
| 20e7a98241 | |||
| abf811f2e4 | |||
| 4d7c49e1ee | |||
| e182da0048 | |||
| f81255aa20 | |||
| 7aaca4dfc8 | |||
| 4a74ee030a | |||
| 7c76e538ab | |||
| 87ed70d313 | |||
| 98bb70bee6 | |||
| a77f0e2473 | |||
| 03b128a544 | |||
| 9c6a60f92c | |||
| 311923255e | |||
| cb77b4fc95 | |||
| 4883d7723b | |||
| eb72a61bad | |||
| 6dfeb6480f | |||
| 51f9a29679 | |||
| d51bf36566 | |||
| 9c9f944350 | |||
| 7b557da549 | |||
| 849749b900 | |||
| 528da32d03 | |||
| 9ef7476267 | |||
| 0f897942fa | |||
| 885f3738fc | |||
| ab0d88accf | |||
| 1146322196 | |||
| 157e28a063 | |||
| c8bc9ecbb0 | |||
| 18ba094647 | |||
| 31e44cde37 | |||
| 248dbb84f0 | |||
| 609fb676e4 | |||
| 5d866664b2 | |||
| ba09ef10b1 | |||
| 19ce6eb9c2 | |||
| 934689ecf7 | |||
| 2f56a14d68 | |||
| cc05ce3d76 | |||
| 5959b43e0a | |||
| b9a6ee08fe | |||
| b1739bbe6b | |||
| cad6d7f725 | |||
| a08ddb2e26 | |||
| c7aa84f9c4 | |||
| ec10030951 | |||
| b298667e3d | |||
| 69e90c3b5f | |||
| 2ada7f2efa | |||
| 28c15885fd | |||
| 194a9b1bbb | |||
| 4ce9676319 | |||
| fb75b40fb6 | |||
| d973ab4318 | |||
| f8b08f38b1 | |||
| b3b10e326f | |||
| efe865a4c5 | |||
| a37ac96411 | |||
| 8a16a3d97e | |||
| 3d9d9496b7 | |||
| 862112ed5f | |||
| ea76739d88 | |||
| c53feba8f5 | |||
| 62d511f967 | |||
| 664359158e | |||
| 8ecde83b89 | |||
| 420f87daca | |||
| c722050aec | |||
| c434b550a5 | |||
| 06a2adf62b | |||
| bb0cf69659 | |||
| e50fadd142 | |||
| 327cbff184 | |||
| 62853a340f | |||
| 86852a8cac | |||
| efc94a2ac3 | |||
| fd4f429b86 | |||
| f5404920fa | |||
| a0d8c7251c | |||
| 303ebea3f3 | |||
| c38012afcf | |||
| becac3b0f6 | |||
| 0245712e24 | |||
| 4b003a852e | |||
| ddf389ffd1 | |||
| 1d59b0a774 | |||
| 74582899a2 | |||
| 8a91dcb85d | |||
| 8fbc4bb23f | |||
| 61d31d9b52 | |||
| a81e536fd5 | |||
| 25fdaeeab2 | |||
| c4885cb324 | |||
| 71e0f1933c | |||
| 65246dc7cd | |||
| 32fd041495 | |||
| 6c292994fd | |||
| 6e8c326a9c | |||
| a4e9b1e1cc | |||
| 29d7f16b2c | |||
| e3c0a180af | |||
| 360c4d4f6f | |||
| 1a580d1051 | |||
| f13df6ab76 | |||
| 239472b61e | |||
| 83c501dc8d | |||
| 67d07ed7e6 | |||
| 18a728ff4b | |||
| b77e3a99e9 | |||
| daba510755 | |||
| dcc04de6bc | |||
| 38693e42ef | |||
| ceaff26d2d | |||
| db373b66a6 | |||
| fb03359400 | |||
| d8e40c9cce | |||
| ecca030ab9 | |||
| 35322b39e3 | |||
| be079b87c8 | |||
| 2a7b2f45d6 | |||
| 2a48bc6343 | |||
| 5acd22a885 | |||
| 632a048c6b | |||
| e1e4910399 | |||
| 5bd67be2e5 | |||
| 482f7bfbb8 | |||
| 2d6f16a311 | |||
| 3f8027af2a | |||
| da063b73bc | |||
| 3f97a12153 | |||
| 6bee56580b | |||
| ad1708abbb | |||
| bd1671c440 | |||
| e527110b9c | |||
| 0cf39c181f | |||
| c0886c086f | |||
| e459c1011c | |||
| 9928509171 | |||
| 16444ae79f | |||
| 75a351e77d | |||
| 5c02e906fa | |||
| 03b0d08ae0 | |||
| 6dbdfe7ba5 | |||
| 8ab9c2e4df | |||
| c912b637ee | |||
| 4df23cfe45 | |||
| 65592b9af5 | |||
| 8f9937ddee | |||
| 5042bcc95d | |||
| cc6503e6f7 | |||
| 8de9d84f1f | |||
| 29c582c89e | |||
| bc53a36cd8 | |||
| 84cdc7fa60 | |||
| 1dc0451f0f | |||
| 1beebcccd2 | |||
| b6b42c422c | |||
| bda75915b2 | |||
| b099d53871 | |||
| c6ad1968e7 | |||
| bc37bd1687 | |||
| 7161cd5996 | |||
| 23b7d5b49e | |||
| 599f9561e4 | |||
| bd45220c66 | |||
| 53757651c8 | |||
| bd1bde0814 | |||
| ad044814a5 | |||
| aede3cbf52 | |||
| 79115d1b25 | |||
| 404a1d5b3f | |||
| 8a191b9c91 | |||
| 56a9a796bd | |||
| 5bc0a7f97b | |||
| 7fe7b2feaa | |||
| ee0134598c | |||
| b19e5ad0cb | |||
| de442741e5 | |||
| f43e17aad4 | |||
| ddbe4f0fd1 | |||
| f17a601d8e | |||
| 5bace9545a | |||
| f170a1dd8e | |||
| 91eac5c875 | |||
| 518889b459 | |||
| cc8a32d17d | |||
| a00b01c041 | |||
| 7db5c064d9 | |||
| ed8390a36c | |||
| 1d45c2e715 | |||
| 784f86f6f3 | |||
| 9d865291f0 | |||
| bc7dd0f836 | |||
| 8d64ad9650 | |||
| b3fdde9d83 | |||
| ea6fc4108a | |||
| 40958f2fb4 | |||
| 8e0d18a596 | |||
| 861d773cdd | |||
| 6bd9f0ef9d | |||
| d69157439c | |||
| 1a3d4f2204 | |||
| b59f6a7264 | |||
| bf902fd2d9 | |||
| a8272e1bf9 | |||
| b96070085b | |||
| 48688dc6e6 | |||
| 9dd9b010ed | |||
| 0888ae5033 | |||
| 2d7be3348a | |||
| bee3fbb9e2 | |||
| 55db68400d | |||
| 7d2432c56c | |||
| 48731e8ddf | |||
| 4d0a97f9c2 | |||
| 133310bd7d | |||
| 69bf9cebb3 | |||
| 91e39a0a37 | |||
| 586ca11106 | |||
| 20b967b4ea | |||
| dd9d024975 | |||
| 8d1ab6973a | |||
| cb2db411ce | |||
| f179e1070c | |||
| fdeff12762 |
+9
-2
@@ -1,2 +1,9 @@
|
||||
node_modules/
|
||||
bridges/python/src/.venv/*
|
||||
__pycache__/
|
||||
**/dist/*
|
||||
**/build/
|
||||
**/node_modules/
|
||||
**/tmp/*
|
||||
**/src/.venv/*
|
||||
logs/*
|
||||
*.pyc
|
||||
.DS_Store
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ LEON_HTTP_API_KEY=
|
||||
# Language used for the HTTP API
|
||||
LEON_HTTP_API_LANG=en-US
|
||||
|
||||
# Enable/disable collaborative logger
|
||||
LEON_LOGGER=true
|
||||
# Enable/disable telemetry
|
||||
LEON_TELEMETRY=true
|
||||
|
||||
# Python TCP server
|
||||
LEON_PY_TCP_SERVER_HOST=0.0.0.0
|
||||
|
||||
+16
-1
@@ -26,14 +26,29 @@
|
||||
"plugins": ["@typescript-eslint", "unicorn", "import"],
|
||||
"ignorePatterns": "*.spec.js",
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": ["off"],
|
||||
"no-async-promise-executor": ["off"],
|
||||
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
|
||||
"prefer-destructuring": ["error"],
|
||||
"comma-dangle": ["error", "never"],
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"unicorn/prefer-node-protocol": "error",
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "comma",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"import/no-named-as-default": "off",
|
||||
"import/no-named-as-default-member": "off",
|
||||
"import/order": [
|
||||
|
||||
+78
-4
@@ -51,7 +51,7 @@ Choose the setup method you want to go for.
|
||||
|
||||
### Single-Click
|
||||
|
||||
Gitpod will automatically setup an environment and run an instance for you.
|
||||
Gitpod will automatically set up an environment and run an instance for you.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/leon-ai/leon)
|
||||
|
||||
@@ -123,11 +123,14 @@ Types define which kind of changes you made to the project.
|
||||
|
||||
Scopes define high-level nodes of Leon.
|
||||
|
||||
- web app
|
||||
- bridge/python
|
||||
- bridge/nodejs
|
||||
- docker
|
||||
- hotword
|
||||
- scripts
|
||||
- server
|
||||
- tcp server
|
||||
- python bridge
|
||||
- hotword
|
||||
- web app
|
||||
- skill/skill_name
|
||||
|
||||
### Examples
|
||||
@@ -149,6 +152,77 @@ By sponsoring the project you make the project sustainable and faster to develop
|
||||
|
||||
The focus is not only limited to the activity you see on GitHub but also a lot of thinking about the direction of the project. Which is naturally related to the overall design, architecture, vision, learning process and so on...
|
||||
|
||||
## Contributing to the Python Bridge or TCP HttpServer
|
||||
|
||||
Leon makes use of two binaries, the Python bridge and the TCP server. These binaries are compiled from Python sources.
|
||||
|
||||
The Python bridge is used to communicate between the core and skills made with Python.
|
||||
|
||||
The TCP server is used to communicate between the core and third-party nodes, such as spaCy.
|
||||
|
||||
### Set Up the Python Environment
|
||||
|
||||
To contribute to these parts, you need to set up a Python environment running with a specific Python version and a specific Pipenv version.
|
||||
|
||||
It is recommended to use Pyenv to manage your Python versions.
|
||||
|
||||
If you are on GNU/Linux Ubuntu, you can run the following to install Pyenv, otherwise, please refer to the [Pyenv documentation to install it](https://github.com/pyenv/pyenv#installation):
|
||||
|
||||
```bash
|
||||
# Update registry
|
||||
sudo apt-get update
|
||||
|
||||
# Install Pyenv deps
|
||||
sudo apt-get install make build-essential libssl-dev zlib1g-dev \
|
||||
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
|
||||
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
|
||||
|
||||
# Install Pyenv
|
||||
curl https://pyenv.run | bash
|
||||
|
||||
# Add output lines to .bashrc
|
||||
|
||||
# Restart shell
|
||||
exec "$SHELL"
|
||||
```
|
||||
|
||||
Once Pyenv installed, run:
|
||||
|
||||
```bash
|
||||
# Install Python
|
||||
pyenv install 3.9.10 --force
|
||||
pyenv global 3.9.10
|
||||
|
||||
# Install Pipenv
|
||||
pip install pipenv==2022.7.24
|
||||
```
|
||||
|
||||
Your Python environment should be ready now. So now you can set up the respective environments according to what you are going to contribute to and build them:
|
||||
|
||||
```bash
|
||||
# Set up the Python bridge environment
|
||||
npm run setup:python-bridge
|
||||
|
||||
# Set up the TCP server environment
|
||||
npm run setup:tcp-server
|
||||
# If you are in China, you can run this to download models faster:
|
||||
npm run setup:tcp-server cn
|
||||
|
||||
# Once your code changes are done, you can build via:
|
||||
|
||||
# Build the Python bridge
|
||||
npm run build:python-bridge
|
||||
|
||||
# Build the TCP server
|
||||
npm run build:tcp-server
|
||||
|
||||
# Run the Python bridge
|
||||
./bridges/python/dist/{OS-CPU_ARCH}/leon-python-bridge server/src/intent-object.sample.json
|
||||
|
||||
# Run the TCP server
|
||||
./tcp_server/dist/{OS-CPU_ARCH}/leon-tcp-server en
|
||||
```
|
||||
|
||||
## Spread the Word
|
||||
|
||||
Use [#LeonAI](<https://twitter.com/search?f=live&q=%23LeonAI%20(from%3Agrenlouis%20OR%20from%3Alouistiti_fr)&src=typed_query>) if you tweet about Leon and/or mention [@grenlouis](https://twitter.com/grenlouis).
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Pre-release Node.js bridge
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Set Node.js bridge version
|
||||
working-directory: bridges/nodejs/src
|
||||
run: |
|
||||
echo "NODEJS_BRIDGE_VERSION=$(node --require fs --eval "const fs = require('node:fs'); const [, VERSION] = fs.readFileSync('version.ts', 'utf8').split(\"'\"); console.log(VERSION)")" >> $GITHUB_ENV
|
||||
|
||||
- name: Display Node.js bridge version
|
||||
run: |
|
||||
echo "Node.js bridge version: ${{ env.NODEJS_BRIDGE_VERSION }}"
|
||||
|
||||
- name: Install core
|
||||
run: npm install
|
||||
|
||||
- name: Build Node.js bridge
|
||||
run: npm run build:nodejs-bridge
|
||||
|
||||
- name: Upload Node.js bridge
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: bridges/nodejs/dist/*.zip
|
||||
|
||||
draft-release:
|
||||
name: Draft-release
|
||||
needs: [build]
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Set Node.js bridge version
|
||||
working-directory: bridges/nodejs/src
|
||||
run: |
|
||||
echo "NODEJS_BRIDGE_VERSION=$(node --require fs --eval "const fs = require('node:fs'); const [, VERSION] = fs.readFileSync('version.ts', 'utf8').split(\"'\"); console.log(VERSION)")" >> $GITHUB_ENV
|
||||
|
||||
- name: Download Node.js bridge
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: bridges/nodejs/dist
|
||||
|
||||
- uses: marvinpinto/action-automatic-releases@latest
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
automatic_release_tag: nodejs-bridge_v${{ env.NODEJS_BRIDGE_VERSION }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
title: Node.js Bridge ${{ env.NODEJS_BRIDGE_VERSION }}
|
||||
files: bridges/nodejs/dist/artifact/*.zip
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-20.04, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -33,7 +33,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
|
||||
- name: Set Python bridge version
|
||||
working-directory: bridges/python/src
|
||||
@@ -45,7 +44,7 @@ jobs:
|
||||
echo "Python bridge version: ${{ env.PYTHON_BRIDGE_VERSION }}"
|
||||
|
||||
- name: Install core
|
||||
run: npm ci
|
||||
run: npm install
|
||||
|
||||
- name: Set up Python bridge
|
||||
run: npm run setup:python-bridge
|
||||
@@ -61,7 +60,7 @@ jobs:
|
||||
draft-release:
|
||||
name: Draft-release
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-20.04, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -33,7 +33,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
|
||||
- name: Set TCP server version
|
||||
working-directory: tcp_server/src
|
||||
@@ -45,7 +44,7 @@ jobs:
|
||||
echo "TCP server version: ${{ env.TCP_SERVER_VERSION }}"
|
||||
|
||||
- name: Install core
|
||||
run: npm ci
|
||||
run: npm install
|
||||
|
||||
- name: Set up TCP server
|
||||
run: npm run setup:tcp-server
|
||||
@@ -61,7 +60,7 @@ jobs:
|
||||
draft-release:
|
||||
name: Draft-release
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
|
||||
@@ -14,16 +14,19 @@ core/config/**/*.json
|
||||
bin/coqui/*
|
||||
bin/flite/*
|
||||
|
||||
package-lock.json
|
||||
*.pyc
|
||||
.DS_Store
|
||||
*.sublime-workspace
|
||||
npm-debug.log
|
||||
debug.log
|
||||
.env
|
||||
leon.json
|
||||
bridges/python/src/Pipfile.lock
|
||||
tcp_server/src/Pipfile.lock
|
||||
!tcp_server/**/.gitkeep
|
||||
!bridges/python/**/.gitkeep
|
||||
!bridges/nodejs/**/.gitkeep
|
||||
!**/*.sample*
|
||||
packages/**/config/config.json
|
||||
skills/**/src/config.json
|
||||
@@ -32,3 +35,4 @@ skills/**/memory/*.json
|
||||
core/data/models/*.nlp
|
||||
package.json.backup
|
||||
.python-version
|
||||
schemas/**/*.json
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# [1.0.0-beta.8](https://github.com/leon-ai/leon/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2023-05-01) / Binaries and TypeScript Rewrite
|
||||
|
||||
_Please refer to [our latest blog post](https://blog.getleon.ai/binaries-and-typescript-rewrite-1-0-0-beta-8/) for more information on the new release of our dear Leon._
|
||||
|
||||
# [1.0.0-beta.7](https://github.com/leon-ai/leon/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-08-24) / A Much Better NLP
|
||||
|
||||
_Please [read this blog post](https://blog.getleon.ai/a-much-better-nlp-and-future-1-0-0-beta-7/) to know more about all the new features and the exciting future of Leon._
|
||||
|
||||
+22
-31
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:latest
|
||||
FROM ubuntu:20.04
|
||||
ENV IS_DOCKER true
|
||||
|
||||
# Replace shell with bash so we can source files
|
||||
@@ -15,18 +15,23 @@ RUN apt-get update && apt-get install --yes -q --no-install-recommends \
|
||||
curl \
|
||||
git \
|
||||
wget \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libbz2-dev \
|
||||
libreadline-dev \
|
||||
libsqlite3-dev \
|
||||
llvm \
|
||||
libncurses5-dev \
|
||||
xz-utils \
|
||||
tk-dev libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libffi-dev \
|
||||
liblzma-dev
|
||||
libssl-dev \
|
||||
openssl \
|
||||
libz-dev \
|
||||
zlib1g-dev \
|
||||
libbz2-dev \
|
||||
libreadline-dev \
|
||||
libsqlite3-dev \
|
||||
llvm \
|
||||
libncurses5-dev \
|
||||
xz-utils \
|
||||
tk-dev libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libffi-dev \
|
||||
liblzma-dev \
|
||||
libgdbm-dev \
|
||||
libnss3-dev \
|
||||
libc6-dev
|
||||
|
||||
# Run the container as an unprivileged user
|
||||
RUN groupadd docker && useradd -g docker -s /bin/bash -m docker
|
||||
@@ -34,28 +39,14 @@ USER docker
|
||||
WORKDIR /home/docker
|
||||
|
||||
# Install Node.js with nvm
|
||||
ENV NVM_DIR /home/docker/nvm
|
||||
ENV NODE_VERSION v16.17.0
|
||||
ENV NVM_DIR /home/docker/.nvm
|
||||
ENV NODE_VERSION v16.18.0
|
||||
|
||||
RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash
|
||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
|
||||
RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION"
|
||||
|
||||
ENV NODE_PATH $NVM_DIR/versions/node/$NODE_VERSION/lib/node_modules
|
||||
ENV PATH $NVM_DIR/versions/node/$NODE_VERSION/bin:$PATH
|
||||
|
||||
# Install Python with pyenv
|
||||
RUN git clone --depth=1 https://github.com/pyenv/pyenv.git .pyenv
|
||||
ENV PYENV_ROOT="/home/docker/.pyenv"
|
||||
ENV PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${PATH}"
|
||||
|
||||
ENV PYTHON_VERSION=3.9.10
|
||||
RUN pyenv install ${PYTHON_VERSION}
|
||||
RUN pyenv global ${PYTHON_VERSION}
|
||||
|
||||
# Install Pipenv
|
||||
ENV PYTHON_BIN_PATH /home/docker/.local/bin
|
||||
ENV PATH="${PYTHON_BIN_PATH}:${PATH}"
|
||||
RUN python -m pip install --user --force-reinstall pipenv virtualenv
|
||||
ENV PATH $NVM_DIR/versions/node/$NODE_VERSION/bin:$PATH
|
||||
|
||||
# Install Leon
|
||||
WORKDIR /home/docker/leon
|
||||
|
||||
@@ -28,14 +28,47 @@ _<p align="center">Your open-source personal assistant.</p>_
|
||||
<a href="https://blog.getleon.ai/the-story-behind-leon/">Story</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<h2 align="center">📢 Notice 📢</h2>
|
||||
<p align="center">
|
||||
<a href="https://blog.getleon.ai/a-much-better-nlp-and-future-1-0-0-beta-7/"><img width="400" src="https://blog.getleon.ai/static/a62ac28a01cb6898e299dced40875a68/c1b63/beta-7.png" /></a>
|
||||
<br>
|
||||
Many exciting things are coming up, hence no new documentation and test are going to be written until the official release of Leon. Feel free to <a href="https://discord.gg/MNQqqKg"><b>join us on Discord</b></a> to know more and to read the <a href="https://blog.getleon.ai/a-much-better-nlp-and-future-1-0-0-beta-7/"><b>"A Much Better NLP and Future" blog post</b></a>.
|
||||
</p>
|
||||
<br><br>
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
### Why is there a small amount of contributors?
|
||||
|
||||
I'm taking a lot of time to work on the new core of Leon due to personal reasons. I can only work on it after work and on weekends. Hence, **I'm blocking any potential contribution as the whole core of Leon is coming with many breaking changes**. Many of you are willing to contribute in Leon (create new skills, help to improve the core, translations and so on...), a big thanks to every one of you!
|
||||
|
||||
I wish one day I could work on Leon full time, but it's not possible at the moment as I need to pay my bills. I have some ideas about how to monetize Leon in the future (Leon's core will always remain open source), but before to get there there is still a long way to go.
|
||||
|
||||
Until then, any financial support by [sponsoring Leon](http://sponsor.getleon.ai) is much appreciated 🙂
|
||||
|
||||
### How about large language models and Leon?
|
||||
|
||||
Since AI gained in popularity and large language models are getting more and more traction, many of you joined our community. A huge welcome to all of you! 🤗
|
||||
|
||||
At the moment, Leon's NLU will remain intents first with his own model without relying on an LLM. It is important that Leon can run 100% offline and I'm confident that with the downsizing techniques such as quantization Leon will sooner or later work with LLMs at his core and still be able to run on edge.
|
||||
|
||||
Here is how LLMs may help Leon in the future:
|
||||
|
||||
- Intent fallback: when an utterance cannot match an intent, then rely on an LLM to provide results.
|
||||
- New named entity recognition engine: provide a better solution to extract entities from utterances such as fruits, numbers, cities, durations, persons, etc.
|
||||
- Skill features: let skills leverage LLMs to provide out-of-the-box NLP features such as summarization, translation, sentiment analysis and so on...
|
||||
- Skill building: LLMs can help to develop skills such as paraphrasing utterance samples, translate answers, convert code from our Python bridge to the upcoming JavaScript bridge and vice versa, etc.
|
||||
- More...
|
||||
|
||||
### What's Next?
|
||||
|
||||
Once the new core released, we'll work on the community aspect of Leon. For example, better organize [our Discord](https://discord.gg/MNQqqKg), planify regular calls, work on skills together, etc. It is very important for Leon to have a real community. At that moment, the skills platform will already be online, so it'll be easier to sync our progress and publish new skills.
|
||||
|
||||
- Feel free to check out the Git development branches and our [next major milestones](https://blog.getleon.ai/a-much-better-nlp-and-future-1-0-0-beta-7/#whats-next).
|
||||
- And the [detailed roadmap](http://roadmap.getleon.ai).
|
||||
- Many exciting things are coming up, hence no new documentation and test are going to be written until the official release of Leon.
|
||||
|
||||
---
|
||||
|
||||
## Latest Release
|
||||
|
||||
Check out the [latest release blog post](https://blog.getleon.ai/binaries-and-typescript-rewrite-1-0-0-beta-8/).
|
||||
|
||||
<a href="https://blog.getleon.ai/binaries-and-typescript-rewrite-1-0-0-beta-8/"><img width="400" src="https://blog.getleon.ai/static/a0d1cbafd1968e7531dc17e229f8cc61/aa440/beta-8.png" /></a>
|
||||
|
||||
---
|
||||
|
||||
@@ -68,8 +101,8 @@ If you want to, Leon can communicate with you by being **offline to protect your
|
||||
> - Skills
|
||||
> - The web app
|
||||
> - The hotword node
|
||||
> - The TCP server (for inter-process communication between Leon and third-party processes such as spaCy)
|
||||
> - The Python bridge (the connector between Python core and skills)
|
||||
> - The TCP server (for inter-process communication between Leon and third-party nodes such as spaCy)
|
||||
> - The Python bridge (the connector between the core and skills made with Python)
|
||||
|
||||
### What is Leon able to do?
|
||||
|
||||
@@ -81,7 +114,7 @@ Sounds good to you? Then let's get started!
|
||||
|
||||
## ☁️ Try with a Single-Click
|
||||
|
||||
Gitpod will automatically setup an environment and run an instance for you.
|
||||
Gitpod will automatically set up an environment and run an instance for you.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/leon-ai/leon)
|
||||
|
||||
@@ -172,6 +205,13 @@ You'll find a write-up on this [blog post](https://blog.getleon.ai/the-story-beh
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle" width="128">
|
||||
<a href="https://github.com/Appwrite">
|
||||
<img src="https://github.com/Appwrite.png?size=128" />
|
||||
Appwrite
|
||||
</a><br>
|
||||
<sub><sup>250 USD / month</sup></sub>
|
||||
</td>
|
||||
<td align="center" valign="middle" width="128">
|
||||
<img src="https://getleon.ai/img/anonymous.svg" width="128" />
|
||||
Anonymous
|
||||
@@ -179,16 +219,9 @@ You'll find a write-up on this [blog post](https://blog.getleon.ai/the-story-beh
|
||||
<sub><sup>100 USD / month</sup></sub>
|
||||
</td>
|
||||
<td align="center" valign="middle" width="128">
|
||||
<a href="https://github.com/phareal">
|
||||
<img src="https://github.com/phareal.png?size=128" />
|
||||
phareal
|
||||
</a><br>
|
||||
<sub><sup>30 USD / month</sup></sub>
|
||||
</td>
|
||||
<td align="center" valign="middle" width="128">
|
||||
<a href="https://github.com/KeithIMyers">
|
||||
<img src="https://github.com/KeithIMyers.png?size=128" />
|
||||
Keith Myers
|
||||
<a href="https://github.com/herbundkraut">
|
||||
<img src="https://github.com/herbundkraut.png?size=128" />
|
||||
herbundkraut
|
||||
</a><br>
|
||||
<sub><sup>10 USD / month</sup></sub>
|
||||
</td>
|
||||
@@ -220,7 +253,7 @@ The focus is not only limited to the activity you see on GitHub but also a lot o
|
||||
</a>
|
||||
|
||||
<a href="https://www.aoz.studio">
|
||||
<img src="https://user-images.githubusercontent.com/1731544/153794939-c42f1b10-a15d-4e82-b448-dc95bfe85b1c.png" alt="AOZ Studio" width="64" />
|
||||
<img src="https://getleon.ai/_next/image?url=%2Fimg%2Fthanks%2Faoz-studio.png&w=384&q=75" alt="AOZ Studio" width="128" />
|
||||
</a>
|
||||
|
||||
## 📝 License
|
||||
|
||||
@@ -125,8 +125,10 @@ table {
|
||||
}
|
||||
|
||||
:root {
|
||||
--black-color: #151718;
|
||||
--black-color: #000;
|
||||
--light-black-color: #222426;
|
||||
--white-color: #fff;
|
||||
--grey-color: #323739;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -193,7 +195,7 @@ input {
|
||||
color: var(--white-color);
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-bottom: 2px solid var(--white-color);
|
||||
border-bottom: 2px solid var(--grey-color);
|
||||
background: none;
|
||||
font-weight: 400;
|
||||
font-size: 4em;
|
||||
@@ -222,7 +224,7 @@ small {
|
||||
top: 10%;
|
||||
height: 50%;
|
||||
overflow-y: auto;
|
||||
border: 2px solid var(--white-color);
|
||||
border: 2px solid var(--grey-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
#feed::-webkit-scrollbar {
|
||||
@@ -302,8 +304,8 @@ small {
|
||||
right: 0;
|
||||
}
|
||||
#feed .leon .bubble {
|
||||
background-color: #eee;
|
||||
color: var(--black-color);
|
||||
background-color: var(--light-black-color);
|
||||
color: var(--white-color);
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
100% {
|
||||
|
||||
@@ -100,7 +100,7 @@ export default class Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
}, 1_000)
|
||||
}, data.duration + 500)
|
||||
}
|
||||
})
|
||||
|
||||
+96
-96
@@ -1,4 +1,4 @@
|
||||
import request from 'superagent'
|
||||
import axios from 'axios'
|
||||
|
||||
import Loader from './loader'
|
||||
import Client from './client'
|
||||
@@ -11,126 +11,107 @@ const config = {
|
||||
server_host: import.meta.env.VITE_LEON_HOST,
|
||||
server_port: import.meta.env.VITE_LEON_PORT,
|
||||
min_decibels: -40, // Noise detection sensitivity
|
||||
max_blank_time: 1000 // Maximum time to consider a blank (ms)
|
||||
max_blank_time: 1_000 // Maximum time to consider a blank (ms)
|
||||
}
|
||||
const serverUrl =
|
||||
import.meta.env.VITE_LEON_NODE_ENV === 'production'
|
||||
? ''
|
||||
: `${config.server_host}:${config.server_port}`
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const loader = new Loader()
|
||||
|
||||
loader.start()
|
||||
|
||||
request.get(`${serverUrl}/api/v1/info`).end((err, res) => {
|
||||
if (err || !res.ok) {
|
||||
console.error(err.response.error.message)
|
||||
} else {
|
||||
const input = document.querySelector('#utterance')
|
||||
const mic = document.querySelector('#mic-button')
|
||||
const v = document.querySelector('#version small')
|
||||
const client = new Client(config.app, serverUrl, input, res.body)
|
||||
let rec = {}
|
||||
let chunks = []
|
||||
try {
|
||||
const response = await axios.get(`${serverUrl}/api/v1/info`)
|
||||
const input = document.querySelector('#utterance')
|
||||
const mic = document.querySelector('#mic-button')
|
||||
const v = document.querySelector('#version small')
|
||||
const client = new Client(config.app, serverUrl, input, response.data)
|
||||
let rec = {}
|
||||
let chunks = []
|
||||
|
||||
v.innerHTML += client.info.version
|
||||
v.innerHTML += client.info.version
|
||||
|
||||
client.init(loader)
|
||||
client.init(loader)
|
||||
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
.then((stream) => {
|
||||
if (MediaRecorder) {
|
||||
rec = new Recorder(stream, mic, client.info)
|
||||
client.recorder = rec
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
.then((stream) => {
|
||||
if (MediaRecorder) {
|
||||
rec = new Recorder(stream, mic, client.info)
|
||||
client.recorder = rec
|
||||
|
||||
rec.ondataavailable((e) => {
|
||||
chunks.push(e.data)
|
||||
})
|
||||
rec.ondataavailable((e) => {
|
||||
chunks.push(e.data)
|
||||
})
|
||||
|
||||
rec.onstart(() => {
|
||||
/* */
|
||||
})
|
||||
rec.onstart(() => {
|
||||
/* */
|
||||
})
|
||||
|
||||
rec.onstop(() => {
|
||||
const blob = new Blob(chunks)
|
||||
chunks = []
|
||||
rec.enabled = false
|
||||
rec.onstop(() => {
|
||||
const blob = new Blob(chunks)
|
||||
chunks = []
|
||||
rec.enabled = false
|
||||
|
||||
// Ensure there are some data
|
||||
if (blob.size >= 1000) {
|
||||
client.socket.emit('recognize', blob)
|
||||
}
|
||||
})
|
||||
// Ensure there are some data
|
||||
if (blob.size >= 1_000) {
|
||||
client.socket.emit('recognize', blob)
|
||||
}
|
||||
})
|
||||
|
||||
listener.listening(
|
||||
stream,
|
||||
config.min_decibels,
|
||||
config.max_blank_time,
|
||||
() => {
|
||||
// Noise detected
|
||||
rec.noiseDetected = true
|
||||
},
|
||||
() => {
|
||||
// Noise ended
|
||||
listener.listening(
|
||||
stream,
|
||||
config.min_decibels,
|
||||
config.max_blank_time,
|
||||
() => {
|
||||
// Noise detected
|
||||
rec.noiseDetected = true
|
||||
},
|
||||
() => {
|
||||
// Noise ended
|
||||
|
||||
rec.noiseDetected = false
|
||||
if (rec.enabled && !rec.hotwordTriggered) {
|
||||
rec.stop()
|
||||
rec.enabled = false
|
||||
rec.hotwordTriggered = false
|
||||
rec.countSilenceAfterTalk = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
client.socket.on('enable-record', () => {
|
||||
rec.hotwordTriggered = true
|
||||
rec.start()
|
||||
setTimeout(() => {
|
||||
rec.noiseDetected = false
|
||||
if (rec.enabled && !rec.hotwordTriggered) {
|
||||
rec.stop()
|
||||
rec.enabled = false
|
||||
rec.hotwordTriggered = false
|
||||
}, config.max_blank_time)
|
||||
rec.enabled = true
|
||||
})
|
||||
} else {
|
||||
console.error('MediaRecorder is not supported on your browser.')
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
'MediaDevices.getUserMedia() threw the following error:',
|
||||
err
|
||||
rec.countSilenceAfterTalk = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
console.error(
|
||||
'MediaDevices.getUserMedia() is not supported on your browser.'
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
onkeydowndocument(e, () => {
|
||||
if (rec.enabled === false) {
|
||||
input.value = ''
|
||||
rec.start()
|
||||
rec.enabled = true
|
||||
client.socket.on('enable-record', () => {
|
||||
rec.hotwordTriggered = true
|
||||
rec.start()
|
||||
setTimeout(() => {
|
||||
rec.hotwordTriggered = false
|
||||
}, config.max_blank_time)
|
||||
rec.enabled = true
|
||||
})
|
||||
} else {
|
||||
rec.stop()
|
||||
rec.enabled = false
|
||||
console.error('MediaRecorder is not supported on your browser.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
onkeydowninput(e, client)
|
||||
})
|
||||
|
||||
mic.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
'MediaDevices.getUserMedia() threw the following error:',
|
||||
err
|
||||
)
|
||||
})
|
||||
} else {
|
||||
console.error(
|
||||
'MediaDevices.getUserMedia() is not supported on your browser.'
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
onkeydowndocument(e, () => {
|
||||
if (rec.enabled === false) {
|
||||
input.value = ''
|
||||
rec.start()
|
||||
rec.enabled = true
|
||||
} else {
|
||||
@@ -138,6 +119,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
rec.enabled = false
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
onkeydowninput(e, client)
|
||||
})
|
||||
|
||||
mic.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (rec.enabled === false) {
|
||||
rec.start()
|
||||
rec.enabled = true
|
||||
} else {
|
||||
rec.stop()
|
||||
rec.enabled = false
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
alert(`Error: ${e.message}; ${JSON.stringify(e.response?.data)}`)
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
+10
-2
@@ -1,16 +1,24 @@
|
||||
import dns from 'node:dns'
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
dns.setDefaultResultOrder('verbatim')
|
||||
|
||||
// Map necessary Leon's env vars as Vite only expose VITE_*
|
||||
process.env.VITE_LEON_NODE_ENV = process.env.LEON_NODE_ENV
|
||||
process.env.VITE_LEON_HOST = process.env.LEON_HOST
|
||||
process.env.VITE_LEON_PORT = process.env.LEON_PORT
|
||||
|
||||
export default {
|
||||
export default defineConfig({
|
||||
root: 'app/src',
|
||||
build: {
|
||||
outDir: '../dist',
|
||||
emptyOutDir: true
|
||||
},
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
package-lock=false
|
||||
save-exact=true
|
||||
Vendored
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "leon-nodejs-bridge",
|
||||
"description": "Leon's Node.js bridge to communicate between the core and skills made with JavaScript",
|
||||
"main": "dist/leon-nodejs-bridge.js",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://getleon.ai",
|
||||
"bugs": {
|
||||
"url": "https://github.com/leon-ai/leon/issues"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { VERSION } from './version'
|
||||
|
||||
console.log('[WIP] Node.js bridge', VERSION)
|
||||
|
||||
// TODO
|
||||
@@ -0,0 +1 @@
|
||||
export const VERSION = '0.0.0'
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "@tsconfig/node16-strictest/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"ignoreDeprecations": "5.0",
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"ts-node": {
|
||||
"swc": true,
|
||||
"require": ["tsconfig-paths/register"],
|
||||
"files": true
|
||||
},
|
||||
"files": [],
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
from cx_Freeze import setup, Executable
|
||||
import requests.certs
|
||||
|
||||
from version import __version__
|
||||
|
||||
@@ -8,7 +9,8 @@ options = {
|
||||
'includes': [
|
||||
'bs4',
|
||||
'pytube'
|
||||
]
|
||||
],
|
||||
'include_files': [(requests.certs.where(), 'cacert.pem')]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from tinydb import TinyDB, Query, table, operations
|
||||
from time import sleep
|
||||
import sys
|
||||
import os
|
||||
import sqlite3
|
||||
import requests
|
||||
import re
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '1.0.0'
|
||||
__version__ = '1.0.1'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../schemas/voice-config-schemas/amazon.json",
|
||||
"credentials": {
|
||||
"accessKeyId": "",
|
||||
"secretAccessKey": ""
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"type": "",
|
||||
"$schema": "../../../schemas/voice-config-schemas/google-cloud.json",
|
||||
"type": "service_account",
|
||||
"project_id": "",
|
||||
"private_key_id": "",
|
||||
"private_key": "",
|
||||
"client_email": "",
|
||||
"client_email": "example@iam.gserviceaccount.com",
|
||||
"client_id": "",
|
||||
"auth_uri": "",
|
||||
"token_uri": "",
|
||||
"auth_provider_x509_cert_url": "",
|
||||
"client_x509_cert_url": ""
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://accounts.google.com/o/oauth2/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../schemas/voice-config-schemas/watson-stt.json",
|
||||
"apikey": "",
|
||||
"url": ""
|
||||
"url": "https://stream.watsonplatform.net/speech-to-text/api"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../schemas/voice-config-schemas/watson-tts.json",
|
||||
"apikey": "",
|
||||
"url": ""
|
||||
"url": "https://stream.watsonplatform.net/text-to-speech/api"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"$schema": "../../../schemas/global-data/global-answers.json",
|
||||
"answers": {
|
||||
"success": {},
|
||||
"errors": {
|
||||
"not_found": "Sorry, it seems I cannot find that",
|
||||
"nlu": "It might come from my natural language understanding, the error returned is: \"%error%\""
|
||||
"not_found": "Sorry, it seems I cannot find that"
|
||||
},
|
||||
"synchronizer": {
|
||||
"syncing_direct": "I will now synchronize the downloaded content on your current device. Don't worry, I will let you know once I'm done",
|
||||
@@ -12,10 +12,10 @@
|
||||
"synced_google_drive": "The new content is now available on Google Drive"
|
||||
},
|
||||
"random_errors": [
|
||||
"Sorry, there is a problem with my system",
|
||||
"Sorry, I don't work correctly",
|
||||
"Sorry, you need to fix me",
|
||||
"Sorry, I cannot do that because I'm broken"
|
||||
"Sorry, there is a problem with my system. Please check my logs for further details",
|
||||
"Sorry, I don't work correctly. Please look at my logs for more information",
|
||||
"Sorry, you need to fix me. Please take a look at my logs for further information",
|
||||
"Sorry, I cannot do that because I'm broken. Please check my logs for further details"
|
||||
],
|
||||
"random_skill_errors": [
|
||||
"Sorry, it seems I have a problem with the \"%skill_name%\" skill from the \"%domain_name%\" domain",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"red": {
|
||||
"synonyms": ["red"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"LOW": {
|
||||
"synonyms": ["low"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"Alexa": {
|
||||
"synonyms": ["Alexa"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-resolver.json",
|
||||
"name": "affirmation_denial",
|
||||
"intents": {
|
||||
"affirmation": {
|
||||
@@ -22,7 +23,14 @@
|
||||
"That works",
|
||||
"Go ahead",
|
||||
"Why not",
|
||||
"Please"
|
||||
"Please",
|
||||
"Absolutely",
|
||||
"Precisely",
|
||||
"Spot on",
|
||||
"Undoubtedly",
|
||||
"Certainly",
|
||||
"Without a doubt",
|
||||
"Definitely"
|
||||
],
|
||||
"value": true
|
||||
},
|
||||
@@ -36,7 +44,10 @@
|
||||
"No thanks",
|
||||
"No I'm fine",
|
||||
"Hell no",
|
||||
"Please do not"
|
||||
"Please do not",
|
||||
"I disagree",
|
||||
"Negative",
|
||||
"Not at all"
|
||||
],
|
||||
"value": false
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../schemas/global-data/global-answers.json",
|
||||
"answers": {
|
||||
"success": {},
|
||||
"errors": {
|
||||
@@ -12,10 +13,10 @@
|
||||
"synced_google_drive": "Le nouveau contenu est maintenant disponible sur Google Drive"
|
||||
},
|
||||
"random_errors": [
|
||||
"Désolé, il y a un problème avec mon système",
|
||||
"Désolé, je ne fonctionne pas correctement",
|
||||
"Désolé, vous devez me réparer",
|
||||
"Désolé, je ne peux aboutir à votre demande parce que je suis cassé"
|
||||
"Désolé, il y a un problème avec mon système. Veuillez consulter mes logs pour plus de détails",
|
||||
"Désolé, je ne fonctionne pas correctement. Merci de regarder mes logs pour plus d'information",
|
||||
"Désolé, vous devez me réparer. Veuillez vérifier mes logs pour en savoir plus",
|
||||
"Désolé, je ne peux aboutir à votre demande parce que je suis cassé. Regardez mes logs pour plus de détails"
|
||||
],
|
||||
"random_skill_errors": [
|
||||
"Désolé, il semblerait y avoir un problème avec le skill \"%skill_name%\" du domaine \"%domain_name%\"",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"rouge": {
|
||||
"synonyms": ["rouge"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"bas": {
|
||||
"synonyms": ["bas", "basse"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"Alexa": {
|
||||
"synonyms": ["Alexa"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-resolver.json",
|
||||
"name": "affirmation_denial",
|
||||
"intents": {
|
||||
"affirmation": {
|
||||
|
||||
+89
-89
@@ -1,5 +1,94 @@
|
||||
{
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/news/github_trends/run",
|
||||
"params": ["number", "daterange"],
|
||||
"entitiesType": "builtIn"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/news/product_hunt_trends/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/create_list",
|
||||
"params": ["list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/productivity/todo_list/view_lists",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/view_list",
|
||||
"params": ["list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/rename_list",
|
||||
"params": ["old_list", "new_list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/delete_list",
|
||||
"params": ["list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/add_todos",
|
||||
"params": ["todos", "list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/complete_todos",
|
||||
"params": ["todos", "list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/uncheck_todos",
|
||||
"params": ["todos", "list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/social_communication/mbti/setup",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/social_communication/mbti/quiz",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/have_i_been_pwned/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/utilities/is_it_down/run",
|
||||
"params": ["url"],
|
||||
"entitiesType": "builtIn"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/speed_test/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/youtube_downloader/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/games/akinator/choose_thematic",
|
||||
@@ -116,95 +205,6 @@
|
||||
"method": "GET",
|
||||
"route": "/api/action/leon/welcome/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/news/github_trends/run",
|
||||
"params": ["number", "daterange"],
|
||||
"entitiesType": "builtIn"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/news/product_hunt_trends/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/create_list",
|
||||
"params": ["list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/productivity/todo_list/view_lists",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/view_list",
|
||||
"params": ["list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/rename_list",
|
||||
"params": ["old_list", "new_list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/delete_list",
|
||||
"params": ["list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/add_todos",
|
||||
"params": ["todos", "list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/complete_todos",
|
||||
"params": ["todos", "list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/productivity/todo_list/uncheck_todos",
|
||||
"params": ["todos", "list"],
|
||||
"entitiesType": "trim"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/social_communication/mbti/setup",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/social_communication/mbti/quiz",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/have_i_been_pwned/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/utilities/is_it_down/run",
|
||||
"params": ["url"],
|
||||
"entitiesType": "builtIn"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/speed_test/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/youtube_downloader/run",
|
||||
"params": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ services:
|
||||
tty: true
|
||||
command: 'npm run dev:server && npm run dev:app'
|
||||
volumes:
|
||||
- './:/app'
|
||||
- '/app/node_modules'
|
||||
- './:/home/docker/leon'
|
||||
- '/home/docker/leon/node_modules'
|
||||
network_mode: 'host'
|
||||
|
||||
+11
-10
@@ -3,7 +3,7 @@
|
||||
* You can consider to run this file on a different hardware
|
||||
*/
|
||||
|
||||
const request = require('superagent')
|
||||
const axios = require('axios')
|
||||
const record = require('node-record-lpcm16')
|
||||
const { Detector, Models } = require('@bugsounet/snowboy')
|
||||
const { io } = require('socket.io-client')
|
||||
@@ -21,15 +21,10 @@ socket.on('connect', () => {
|
||||
console.log('Connected to the server')
|
||||
console.log('Waiting for hotword...')
|
||||
})
|
||||
;(async () => {
|
||||
try {
|
||||
await axios.get(`${url}/api/v1/info`)
|
||||
|
||||
request.get(`${url}/api/v1/info`).end((err, res) => {
|
||||
if (err || !res.ok) {
|
||||
if (!err.response) {
|
||||
console.error(`Failed to reach the server: ${err}`)
|
||||
} else {
|
||||
console.error(err.response.error.message)
|
||||
}
|
||||
} else {
|
||||
const models = new Models()
|
||||
|
||||
models.add({
|
||||
@@ -78,5 +73,11 @@ request.get(`${url}/api/v1/info`).end((err, res) => {
|
||||
})
|
||||
|
||||
mic.pipe(detector)
|
||||
} catch (e) {
|
||||
if (!e.response) {
|
||||
console.error(`Failed to reach the server: ${e}`)
|
||||
} else {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
Generated
-27191
File diff suppressed because it is too large
Load Diff
+74
-57
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "leon",
|
||||
"version": "1.0.0-beta.8+dev",
|
||||
"version": "1.0.0-beta.8",
|
||||
"description": "Server, skills and web app of the Leon personal assistant",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
@@ -40,21 +40,26 @@
|
||||
"postinstall": "ts-node scripts/setup/setup.js",
|
||||
"dev:app": "vite --config app/vite.config.js",
|
||||
"dev:server": "npm run train && npm run generate:skills-endpoints && cross-env LEON_NODE_ENV=development tsc-watch --noClear --onSuccess \"nodemon\"",
|
||||
"dev:server:no-lint": "npm run train && npm run generate:skills-endpoints && cross-env LEON_NODE_ENV=development \"nodemon\"",
|
||||
"wake": "cross-env LEON_HOST=http://localhost LEON_PORT=1337 node hotword/index.js",
|
||||
"delete-dist:server": "shx rm -rf ./server/dist",
|
||||
"clean:python-deps": "shx rm -rf ./bridges/python/src/.venv && npm run postinstall",
|
||||
"prepare": "husky install",
|
||||
"generate:skills-endpoints": "ts-node scripts/generate/run-generate-skills-endpoints.js",
|
||||
"generate:http-api-key": "ts-node scripts/generate/run-generate-http-api-key.js",
|
||||
"generate:json-schemas": "ts-node scripts/generate/run-generate-json-schemas.js",
|
||||
"build": "npm run build:app && npm run build:server",
|
||||
"build:app": "cross-env LEON_NODE_ENV=production ts-node scripts/app/run-build-app.js",
|
||||
"build:server": "npm run delete-dist:server && npm run train && npm run generate:skills-endpoints && tsc && resolve-tspaths && shx rm -rf server/dist/core server/dist/package.json && shx mv -f server/dist/server/src/* server/dist && shx rm -rf server/dist/server && shx mkdir -p server/dist/tmp",
|
||||
"build:server": "npm run delete-dist:server && npm run train && npm run generate:skills-endpoints && tsc --project tsconfig.json && resolve-tspaths && shx rm -rf server/dist/core server/dist/package.json && shx mv -f server/dist/server/src/* server/dist && shx rm -rf server/dist/server && shx mkdir -p server/dist/tmp",
|
||||
"build:nodejs-bridge": "ts-node scripts/build-binaries.js nodejs-bridge",
|
||||
"build:python-bridge": "ts-node scripts/build-binaries.js python-bridge",
|
||||
"build:tcp-server": "ts-node scripts/build-binaries.js tcp-server",
|
||||
"start:tcp-server": "cross-env PIPENV_PIPFILE=tcp_server/src/Pipfile pipenv run python tcp_server/src/main.py",
|
||||
"start": "cross-env LEON_NODE_ENV=production node ./server/dist/index.js",
|
||||
"start": "cross-env LEON_NODE_ENV=production node server/dist/pre-check.js && node server/dist/index.js",
|
||||
"python-bridge": "cross-env PIPENV_PIPFILE=bridges/python/src/Pipfile pipenv run python bridges/python/src/main.py server/src/intent-object.sample.json",
|
||||
"train": "ts-node scripts/train/run-train.js",
|
||||
"prepare-release": "ts-node scripts/release/prepare-release.js",
|
||||
"pre-release:nodejs-bridge": "ts-node scripts/release/pre-release-binaries.js nodejs-bridge",
|
||||
"pre-release:python-bridge": "ts-node scripts/release/pre-release-binaries.js python-bridge",
|
||||
"pre-release:tcp-server": "ts-node scripts/release/pre-release-binaries.js tcp-server",
|
||||
"check": "ts-node scripts/check.js",
|
||||
@@ -64,65 +69,77 @@
|
||||
"docker:check": "docker run --rm --interactive leon-ai/leon npm run check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-polly": "^3.18.0",
|
||||
"@fastify/static": "^6.5.0",
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
"@ffprobe-installer/ffprobe": "^1.4.1",
|
||||
"@google-cloud/speech": "^4.2.0",
|
||||
"@google-cloud/text-to-speech": "^3.2.1",
|
||||
"@nlpjs/builtin-microsoft": "^4.22.7",
|
||||
"@nlpjs/core-loader": "^4.22.7",
|
||||
"@nlpjs/lang-all": "^4.22.12",
|
||||
"@nlpjs/nlp": "^4.22.17",
|
||||
"archiver": "^5.3.1",
|
||||
"async": "^3.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"execa": "^5.0.0",
|
||||
"fastify": "^4.5.3",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"googleapis": "^67.1.1",
|
||||
"ibm-watson": "^6.1.1",
|
||||
"@aws-sdk/client-polly": "3.18.0",
|
||||
"@fastify/static": "6.9.0",
|
||||
"@ffmpeg-installer/ffmpeg": "1.1.0",
|
||||
"@ffprobe-installer/ffprobe": "1.4.1",
|
||||
"@google-cloud/speech": "4.2.0",
|
||||
"@google-cloud/text-to-speech": "3.2.1",
|
||||
"@nlpjs/builtin-microsoft": "4.22.7",
|
||||
"@nlpjs/core-loader": "4.22.7",
|
||||
"@nlpjs/lang-all": "4.22.12",
|
||||
"@nlpjs/nlp": "4.22.17",
|
||||
"@segment/ajv-human-errors": "2.1.2",
|
||||
"@sinclair/typebox": "0.25.8",
|
||||
"ajv": "8.11.0",
|
||||
"ajv-formats": "2.1.1",
|
||||
"archiver": "5.3.1",
|
||||
"async": "3.2.4",
|
||||
"axios": "1.1.2",
|
||||
"cross-env": "7.0.3",
|
||||
"dayjs": "1.11.5",
|
||||
"dotenv": "10.0.0",
|
||||
"execa": "5.0.0",
|
||||
"extract-zip": "2.0.1",
|
||||
"fastify": "4.15.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"getos": "3.2.1",
|
||||
"googleapis": "67.1.1",
|
||||
"ibm-watson": "6.1.1",
|
||||
"node-wav": "0.0.2",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"socket.io": "^4.5.2",
|
||||
"socket.io-client": "^4.5.2",
|
||||
"stt": "^1.4.0",
|
||||
"superagent": "^8.0.0",
|
||||
"tree-kill": "^1.2.2"
|
||||
"os-name": "4.0.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"pretty-ms": "7.0.1",
|
||||
"socket.io": "4.5.2",
|
||||
"socket.io-client": "4.5.2",
|
||||
"stt": "1.4.0",
|
||||
"tree-kill": "1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nlpjs/utils": "^4.24.1",
|
||||
"@swc/core": "^1.2.245",
|
||||
"@tsconfig/node16-strictest": "^1.0.3",
|
||||
"@nlpjs/utils": "4.24.1",
|
||||
"@swc/core": "1.3.14",
|
||||
"@tsconfig/node16-strictest": "1.0.4",
|
||||
"@types/archiver": "5.3.2",
|
||||
"@types/cli-spinner": "0.2.1",
|
||||
"@types/node": "^18.7.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
||||
"@typescript-eslint/parser": "^5.36.1",
|
||||
"cli-spinner": "^0.2.10",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@types/getos": "3.0.1",
|
||||
"@types/node": "18.7.13",
|
||||
"@types/node-wav": "0.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.55.0",
|
||||
"@typescript-eslint/parser": "5.55.0",
|
||||
"cli-spinner": "0.2.10",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-import-resolver-typescript": "3.5.1",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-unicorn": "43.0.2",
|
||||
"git-changelog": "^2.0.0",
|
||||
"husky": "^7.0.0",
|
||||
"inquirer": "^8.1.0",
|
||||
"jest": "^27.4.7",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-extended": "^2.0.0",
|
||||
"json": "^10.0.0",
|
||||
"lint-staged": "^13.0.3",
|
||||
"nodemon": "^2.0.19",
|
||||
"prettier": "2.7.1",
|
||||
"resolve-tspaths": "^0.7.4",
|
||||
"semver": "^7.3.5",
|
||||
"shx": "^0.3.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-watch": "^5.0.3",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.8.2",
|
||||
"vite": "^3.0.9"
|
||||
"git-changelog": "2.0.0",
|
||||
"husky": "7.0.0",
|
||||
"inquirer": "8.1.0",
|
||||
"jest": "27.4.7",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"jest-extended": "2.0.0",
|
||||
"json": "10.0.0",
|
||||
"lint-staged": "13.0.3",
|
||||
"nodemon": "2.0.19",
|
||||
"prettier": "2.8.7",
|
||||
"resolve-tspaths": "0.8.8",
|
||||
"semver": "7.3.5",
|
||||
"shx": "0.3.3",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-watch": "6.0.0",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"typescript": "5.0.2",
|
||||
"vite": "3.0.9"
|
||||
}
|
||||
}
|
||||
|
||||
+72
-25
@@ -9,14 +9,18 @@ import {
|
||||
PYTHON_BRIDGE_SRC_PATH,
|
||||
TCP_SERVER_SRC_PATH,
|
||||
BINARIES_FOLDER_NAME,
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
PYTHON_BRIDGE_DIST_PATH,
|
||||
TCP_SERVER_DIST_PATH,
|
||||
NODEJS_BRIDGE_BIN_NAME,
|
||||
PYTHON_BRIDGE_BIN_NAME,
|
||||
TCP_SERVER_BIN_NAME
|
||||
TCP_SERVER_BIN_NAME,
|
||||
NODEJS_BRIDGE_ROOT_PATH
|
||||
} from '@/constants'
|
||||
import { OSTypes } from '@/types'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
import { OSHelper, OSTypes } from '@/helpers/os-helper'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
/**
|
||||
* Build binaries for the given OS according to the given build target
|
||||
@@ -28,8 +32,15 @@ import { OSHelper, OSTypes } from '@/helpers/os-helper'
|
||||
|
||||
const BUILD_TARGETS = new Map()
|
||||
|
||||
BUILD_TARGETS.set('nodejs-bridge', {
|
||||
name: 'Node.js bridge',
|
||||
needsPythonEnv: false,
|
||||
distPath: NODEJS_BRIDGE_DIST_PATH,
|
||||
archiveName: `${NODEJS_BRIDGE_BIN_NAME.split('.')[0]}.zip`
|
||||
})
|
||||
BUILD_TARGETS.set('python-bridge', {
|
||||
name: 'Python bridge',
|
||||
needsPythonEnv: true,
|
||||
pipfilePath: path.join(PYTHON_BRIDGE_SRC_PATH, 'Pipfile'),
|
||||
setupFilePath: path.join(PYTHON_BRIDGE_SRC_PATH, 'setup.py'),
|
||||
distPath: PYTHON_BRIDGE_DIST_PATH,
|
||||
@@ -38,6 +49,7 @@ BUILD_TARGETS.set('python-bridge', {
|
||||
})
|
||||
BUILD_TARGETS.set('tcp-server', {
|
||||
name: 'TCP server',
|
||||
needsPythonEnv: true,
|
||||
pipfilePath: path.join(TCP_SERVER_SRC_PATH, 'Pipfile'),
|
||||
setupFilePath: path.join(TCP_SERVER_SRC_PATH, 'setup.py'),
|
||||
distPath: TCP_SERVER_DIST_PATH,
|
||||
@@ -61,21 +73,24 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
|
||||
const {
|
||||
name: buildTarget,
|
||||
needsPythonEnv,
|
||||
pipfilePath,
|
||||
setupFilePath,
|
||||
distPath,
|
||||
archiveName,
|
||||
dotVenvPath
|
||||
} = BUILD_TARGETS.get(givenBuildTarget)
|
||||
const buildPath = path.join(distPath, BINARIES_FOLDER_NAME)
|
||||
const buildPath = needsPythonEnv
|
||||
? path.join(distPath, BINARIES_FOLDER_NAME)
|
||||
: distPath
|
||||
|
||||
const { type: osType } = OSHelper.getInformation()
|
||||
const { type: osType } = SystemHelper.getInformation()
|
||||
|
||||
/**
|
||||
* Install requirements
|
||||
*/
|
||||
try {
|
||||
if (osType === OSTypes.Linux) {
|
||||
if (needsPythonEnv && osType === OSTypes.Linux) {
|
||||
LogHelper.info('Checking whether the "patchelf" utility can be found...')
|
||||
|
||||
await command('patchelf --version', { shell: true })
|
||||
@@ -91,30 +106,58 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build
|
||||
*/
|
||||
try {
|
||||
LogHelper.info(`Building the ${buildTarget}...`)
|
||||
LogHelper.info(`Building the ${buildTarget}...`)
|
||||
|
||||
// Required environment variables to set up
|
||||
process.env.PIPENV_PIPFILE = pipfilePath
|
||||
process.env.PIPENV_VENV_IN_PROJECT = true
|
||||
if (needsPythonEnv) {
|
||||
/**
|
||||
* Build for binaries requiring a Python environment
|
||||
*/
|
||||
try {
|
||||
// Required environment variables to set up
|
||||
process.env.PIPENV_PIPFILE = pipfilePath
|
||||
process.env.PIPENV_VENV_IN_PROJECT = true
|
||||
|
||||
await command(
|
||||
`pipenv run python ${setupFilePath} build --build-exe ${buildPath}`,
|
||||
{
|
||||
await command(
|
||||
`pipenv run python ${setupFilePath} build --build-exe ${buildPath}`,
|
||||
{
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
}
|
||||
)
|
||||
|
||||
LogHelper.success(`The ${buildTarget} has been built`)
|
||||
} catch (e) {
|
||||
LogHelper.error(
|
||||
`An error occurred while building the ${buildTarget}. Try to delete the ${dotVenvPath} folder, run the setup command then build again: ${e}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Build for binaries not requiring a Python environment
|
||||
*/
|
||||
try {
|
||||
const tsconfigPath = path.join(NODEJS_BRIDGE_ROOT_PATH, 'tsconfig.json')
|
||||
const distMainFilePath = path.join(NODEJS_BRIDGE_DIST_PATH, 'main.js')
|
||||
const distRenamedMainFilePath = path.join(
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
NODEJS_BRIDGE_BIN_NAME
|
||||
)
|
||||
|
||||
await command(`tsc --project ${tsconfigPath}`, {
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
LogHelper.success(`The ${buildTarget} has been built`)
|
||||
} catch (e) {
|
||||
LogHelper.error(
|
||||
`An error occurred while building the ${buildTarget}. Try to delete the ${dotVenvPath} folder, run the setup command then build again: ${e}`
|
||||
)
|
||||
process.exit(1)
|
||||
await fs.promises.rename(distMainFilePath, distRenamedMainFilePath)
|
||||
|
||||
LogHelper.success(`The ${buildTarget} has been built`)
|
||||
} catch (e) {
|
||||
LogHelper.error(
|
||||
`An error occurred while building the ${buildTarget}: ${e}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,7 +185,11 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
|
||||
archive.pipe(output)
|
||||
|
||||
archive.directory(buildPath, BINARIES_FOLDER_NAME)
|
||||
if (needsPythonEnv) {
|
||||
archive.directory(buildPath, BINARIES_FOLDER_NAME)
|
||||
} else {
|
||||
archive.glob(`**/!(${archiveName})`, { cwd: distPath })
|
||||
}
|
||||
|
||||
await archive.finalize()
|
||||
})()
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import execa from 'execa'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { OSHelper } from '@/helpers/os-helper'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
/**
|
||||
* Check OS environment
|
||||
@@ -10,7 +10,7 @@ export default () =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
LogHelper.info('Checking OS environment...')
|
||||
|
||||
const info = OSHelper.getInformation()
|
||||
const info = SystemHelper.getInformation()
|
||||
|
||||
if (info.type === 'windows') {
|
||||
LogHelper.error('Voice offline mode is not available on Windows')
|
||||
|
||||
+123
-78
@@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
@@ -6,15 +7,20 @@ import dotenv from 'dotenv'
|
||||
import { command } from 'execa'
|
||||
import semver from 'semver'
|
||||
import kill from 'tree-kill'
|
||||
import request from 'superagent'
|
||||
import axios from 'axios'
|
||||
import osName from 'os-name'
|
||||
import getos from 'getos'
|
||||
|
||||
import { version } from '@@/package.json'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
import {
|
||||
MINIMUM_REQUIRED_RAM,
|
||||
LEON_VERSION,
|
||||
PYTHON_BRIDGE_BIN_PATH,
|
||||
TCP_SERVER_BIN_PATH,
|
||||
TCP_SERVER_VERSION,
|
||||
PYTHON_BRIDGE_VERSION
|
||||
PYTHON_BRIDGE_VERSION,
|
||||
INSTANCE_ID
|
||||
} from '@/constants'
|
||||
|
||||
dotenv.config()
|
||||
@@ -26,7 +32,7 @@ dotenv.config()
|
||||
;(async () => {
|
||||
try {
|
||||
const nodeMinRequiredVersion = '16'
|
||||
const npmMinRequiredVersion = '5'
|
||||
const npmMinRequiredVersion = '8'
|
||||
const flitePath = 'bin/flite/flite'
|
||||
const coquiLanguageModelPath = 'bin/coqui/huge-vocabulary.scorer'
|
||||
const amazonPath = 'core/config/voice/amazon.json'
|
||||
@@ -38,34 +44,6 @@ dotenv.config()
|
||||
const skillsResolversNlpModelPath =
|
||||
'core/data/models/leon-skills-resolvers-model.nlp'
|
||||
const mainNlpModelPath = 'core/data/models/leon-main-model.nlp'
|
||||
const pastebinData = {
|
||||
leonVersion: null,
|
||||
environment: {
|
||||
osDetails: null,
|
||||
nodeVersion: null,
|
||||
npmVersion: null
|
||||
},
|
||||
nlpModels: {
|
||||
globalResolversModelState: null,
|
||||
skillsResolversModelState: null,
|
||||
mainModelState: null
|
||||
},
|
||||
pythonBridge: {
|
||||
version: null,
|
||||
executionTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
tcpServer: {
|
||||
version: null,
|
||||
startTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
report: null
|
||||
}
|
||||
const report = {
|
||||
can_run: { title: 'Run', type: 'error', v: true },
|
||||
can_run_skill: { title: 'Run skills', type: 'error', v: true },
|
||||
@@ -111,6 +89,35 @@ dotenv.config()
|
||||
v: true
|
||||
}
|
||||
}
|
||||
let reportDataInput = {
|
||||
leonVersion: null,
|
||||
instanceID: INSTANCE_ID || null,
|
||||
environment: {
|
||||
osDetails: null,
|
||||
nodeVersion: null,
|
||||
npmVersion: null
|
||||
},
|
||||
nlpModels: {
|
||||
globalResolversModelState: null,
|
||||
skillsResolversModelState: null,
|
||||
mainModelState: null
|
||||
},
|
||||
pythonBridge: {
|
||||
version: null,
|
||||
executionTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
tcpServer: {
|
||||
version: null,
|
||||
startTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
report: null
|
||||
}
|
||||
|
||||
LogHelper.title('Checking')
|
||||
|
||||
@@ -119,24 +126,46 @@ dotenv.config()
|
||||
*/
|
||||
|
||||
LogHelper.info('Leon version')
|
||||
LogHelper.success(`${version}\n`)
|
||||
pastebinData.leonVersion = version
|
||||
LogHelper.success(`${LEON_VERSION}\n`)
|
||||
reportDataInput.leonVersion = LEON_VERSION
|
||||
|
||||
/**
|
||||
* Environment checking
|
||||
*/
|
||||
|
||||
LogHelper.info('OS')
|
||||
LogHelper.info('Environment')
|
||||
|
||||
const osInfo = {
|
||||
type: os.type(),
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
cpus: os.cpus().length,
|
||||
release: os.release()
|
||||
release: os.release(),
|
||||
osName: osName(),
|
||||
distro: null
|
||||
}
|
||||
LogHelper.success(`${JSON.stringify(osInfo)}\n`)
|
||||
pastebinData.environment.osDetails = osInfo
|
||||
const totalRAMInGB = SystemHelper.getTotalRAM()
|
||||
|
||||
if (Math.round(totalRAMInGB) < MINIMUM_REQUIRED_RAM) {
|
||||
report.can_run.v = false
|
||||
LogHelper.error(
|
||||
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM`
|
||||
)
|
||||
} else {
|
||||
LogHelper.success(`Total RAM: ${totalRAMInGB} GB`)
|
||||
}
|
||||
|
||||
if (osInfo.platform === 'linux') {
|
||||
getos((e, os) => {
|
||||
osInfo.distro = os
|
||||
LogHelper.success(`${JSON.stringify(osInfo)}\n`)
|
||||
})
|
||||
} else {
|
||||
LogHelper.success(`${JSON.stringify(osInfo)}\n`)
|
||||
}
|
||||
|
||||
reportDataInput.environment.osDetails = osInfo
|
||||
reportDataInput.environment.totalRAMInGB = totalRAMInGB
|
||||
;(
|
||||
await Promise.all([
|
||||
command('node --version', { shell: true }),
|
||||
@@ -168,9 +197,9 @@ dotenv.config()
|
||||
} else {
|
||||
LogHelper.success(`${p.stdout}\n`)
|
||||
if (p.command.includes('node --version')) {
|
||||
pastebinData.environment.nodeVersion = p.stdout
|
||||
reportDataInput.environment.nodeVersion = p.stdout
|
||||
} else if (p.command.includes('npm --version')) {
|
||||
pastebinData.environment.npmVersion = p.stdout
|
||||
reportDataInput.environment.npmVersion = p.stdout
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -180,28 +209,33 @@ dotenv.config()
|
||||
*/
|
||||
|
||||
LogHelper.success(`Python bridge version: ${PYTHON_BRIDGE_VERSION}`)
|
||||
pastebinData.pythonBridge.version = PYTHON_BRIDGE_VERSION
|
||||
reportDataInput.pythonBridge.version = PYTHON_BRIDGE_VERSION
|
||||
LogHelper.info('Executing a skill...')
|
||||
|
||||
try {
|
||||
const executionStart = Date.now()
|
||||
const p = await command(
|
||||
`${PYTHON_BRIDGE_BIN_PATH} scripts/assets/intent-object.json`,
|
||||
`${PYTHON_BRIDGE_BIN_PATH} "${path.join(
|
||||
process.cwd(),
|
||||
'scripts',
|
||||
'assets',
|
||||
'intent-object.json'
|
||||
)}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
const executionEnd = Date.now()
|
||||
const executionTime = executionEnd - executionStart
|
||||
LogHelper.info(p.command)
|
||||
pastebinData.pythonBridge.command = p.command
|
||||
reportDataInput.pythonBridge.command = p.command
|
||||
LogHelper.success(p.stdout)
|
||||
pastebinData.pythonBridge.output = p.stdout
|
||||
reportDataInput.pythonBridge.output = p.stdout
|
||||
LogHelper.info(`Skill execution time: ${executionTime}ms\n`)
|
||||
pastebinData.pythonBridge.executionTime = `${executionTime}ms`
|
||||
reportDataInput.pythonBridge.executionTime = `${executionTime}ms`
|
||||
} catch (e) {
|
||||
LogHelper.info(e.command)
|
||||
report.can_run_skill.v = false
|
||||
LogHelper.error(`${e}\n`)
|
||||
pastebinData.pythonBridge.error = JSON.stringify(e)
|
||||
reportDataInput.pythonBridge.error = JSON.stringify(e)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,16 +243,19 @@ dotenv.config()
|
||||
*/
|
||||
|
||||
LogHelper.success(`TCP server version: ${TCP_SERVER_VERSION}`)
|
||||
pastebinData.tcpServer.version = TCP_SERVER_VERSION
|
||||
reportDataInput.tcpServer.version = TCP_SERVER_VERSION
|
||||
|
||||
LogHelper.info('Starting the TCP server...')
|
||||
|
||||
const tcpServerCommand = `${TCP_SERVER_BIN_PATH} en`
|
||||
const tcpServerStart = Date.now()
|
||||
const p = spawn(tcpServerCommand, { shell: true })
|
||||
const ignoredWarnings = [
|
||||
'UserWarning: Unable to retrieve source for @torch.jit._overload function'
|
||||
]
|
||||
|
||||
LogHelper.info(tcpServerCommand)
|
||||
pastebinData.tcpServer.command = tcpServerCommand
|
||||
reportDataInput.tcpServer.command = tcpServerCommand
|
||||
|
||||
if (osInfo.platform === 'darwin') {
|
||||
LogHelper.info(
|
||||
@@ -240,10 +277,14 @@ dotenv.config()
|
||||
|
||||
p.stderr.on('data', (data) => {
|
||||
const newData = data.toString()
|
||||
tcpServerOutput += newData
|
||||
report.can_start_tcp_server.v = false
|
||||
pastebinData.tcpServer.error = newData
|
||||
LogHelper.error(`Cannot start the TCP server: ${newData}`)
|
||||
|
||||
// Ignore given warnings on stderr output
|
||||
if (!ignoredWarnings.some((w) => newData.includes(w))) {
|
||||
tcpServerOutput += newData
|
||||
report.can_start_tcp_server.v = false
|
||||
reportDataInput.tcpServer.error = newData
|
||||
LogHelper.error(`Cannot start the TCP server: ${newData}`)
|
||||
}
|
||||
})
|
||||
|
||||
const timeout = 3 * 60_000
|
||||
@@ -253,16 +294,16 @@ dotenv.config()
|
||||
|
||||
const error = `The TCP server timed out after ${timeout}ms`
|
||||
LogHelper.error(error)
|
||||
pastebinData.tcpServer.error = error
|
||||
reportDataInput.tcpServer.error = error
|
||||
report.can_start_tcp_server.v = false
|
||||
}, timeout)
|
||||
|
||||
p.stdout.on('end', async () => {
|
||||
const tcpServerEnd = Date.now()
|
||||
pastebinData.tcpServer.output = tcpServerOutput
|
||||
pastebinData.tcpServer.startTime = `${tcpServerEnd - tcpServerStart}ms`
|
||||
reportDataInput.tcpServer.output = tcpServerOutput
|
||||
reportDataInput.tcpServer.startTime = `${tcpServerEnd - tcpServerStart}ms`
|
||||
LogHelper.info(
|
||||
`TCP server startup time: ${pastebinData.tcpServer.startTime}ms\n`
|
||||
`TCP server startup time: ${reportDataInput.tcpServer.startTime}\n`
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -273,7 +314,8 @@ dotenv.config()
|
||||
|
||||
if (
|
||||
!fs.existsSync(globalResolversNlpModelPath) ||
|
||||
!Object.keys(fs.readFileSync(globalResolversNlpModelPath)).length
|
||||
!Object.keys(await fs.promises.readFile(globalResolversNlpModelPath))
|
||||
.length
|
||||
) {
|
||||
const state = 'Global resolvers NLP model not found or broken'
|
||||
|
||||
@@ -285,12 +327,12 @@ dotenv.config()
|
||||
LogHelper.error(
|
||||
`${state}. Try to generate a new one: "npm run train"\n`
|
||||
)
|
||||
pastebinData.nlpModels.globalResolversModelState = state
|
||||
reportDataInput.nlpModels.globalResolversModelState = state
|
||||
} else {
|
||||
const state = 'Found and valid'
|
||||
|
||||
LogHelper.success(`${state}\n`)
|
||||
pastebinData.nlpModels.globalResolversModelState = state
|
||||
reportDataInput.nlpModels.globalResolversModelState = state
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,7 +343,8 @@ dotenv.config()
|
||||
|
||||
if (
|
||||
!fs.existsSync(skillsResolversNlpModelPath) ||
|
||||
!Object.keys(fs.readFileSync(skillsResolversNlpModelPath)).length
|
||||
!Object.keys(await fs.promises.readFile(skillsResolversNlpModelPath))
|
||||
.length
|
||||
) {
|
||||
const state = 'Skills resolvers NLP model not found or broken'
|
||||
|
||||
@@ -313,12 +356,12 @@ dotenv.config()
|
||||
LogHelper.error(
|
||||
`${state}. Try to generate a new one: "npm run train"\n`
|
||||
)
|
||||
pastebinData.nlpModels.skillsResolversModelState = state
|
||||
reportDataInput.nlpModels.skillsResolversModelState = state
|
||||
} else {
|
||||
const state = 'Found and valid'
|
||||
|
||||
LogHelper.success(`${state}\n`)
|
||||
pastebinData.nlpModels.skillsResolversModelState = state
|
||||
reportDataInput.nlpModels.skillsResolversModelState = state
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,7 +372,7 @@ dotenv.config()
|
||||
|
||||
if (
|
||||
!fs.existsSync(mainNlpModelPath) ||
|
||||
!Object.keys(fs.readFileSync(mainNlpModelPath)).length
|
||||
!Object.keys(await fs.promises.readFile(mainNlpModelPath)).length
|
||||
) {
|
||||
const state = 'Main NLP model not found or broken'
|
||||
|
||||
@@ -341,12 +384,12 @@ dotenv.config()
|
||||
LogHelper.error(
|
||||
`${state}. Try to generate a new one: "npm run train"\n`
|
||||
)
|
||||
pastebinData.nlpModels.mainModelState = state
|
||||
reportDataInput.nlpModels.mainModelState = state
|
||||
} else {
|
||||
const state = 'Found and valid'
|
||||
|
||||
LogHelper.success(`${state}\n`)
|
||||
pastebinData.nlpModels.mainModelState = state
|
||||
reportDataInput.nlpModels.mainModelState = state
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,7 +399,7 @@ dotenv.config()
|
||||
LogHelper.info('Amazon Polly TTS')
|
||||
|
||||
try {
|
||||
const json = JSON.parse(fs.readFileSync(amazonPath))
|
||||
const json = JSON.parse(await fs.promises.readFile(amazonPath))
|
||||
if (
|
||||
json.credentials.accessKeyId === '' ||
|
||||
json.credentials.secretAccessKey === ''
|
||||
@@ -374,7 +417,7 @@ dotenv.config()
|
||||
LogHelper.info('Google Cloud TTS/STT')
|
||||
|
||||
try {
|
||||
const json = JSON.parse(fs.readFileSync(googleCloudPath))
|
||||
const json = JSON.parse(await fs.promises.readFile(googleCloudPath))
|
||||
const results = []
|
||||
Object.keys(json).forEach((item) => {
|
||||
if (json[item] === '') results.push(false)
|
||||
@@ -395,7 +438,7 @@ dotenv.config()
|
||||
LogHelper.info('Watson TTS')
|
||||
|
||||
try {
|
||||
const json = JSON.parse(fs.readFileSync(watsonTtsPath))
|
||||
const json = JSON.parse(await fs.promises.readFile(watsonTtsPath))
|
||||
const results = []
|
||||
Object.keys(json).forEach((item) => {
|
||||
if (json[item] === '') results.push(false)
|
||||
@@ -416,7 +459,7 @@ dotenv.config()
|
||||
if (!fs.existsSync(flitePath)) {
|
||||
report.can_offline_tts.v = false
|
||||
LogHelper.warning(
|
||||
`Cannot find ${flitePath}. You can setup the offline TTS by running: "npm run setup:offline-tts"\n`
|
||||
`Cannot find ${flitePath}. You can set up the offline TTS by running: "npm run setup:offline-tts"\n`
|
||||
)
|
||||
} else {
|
||||
LogHelper.success(`Found Flite at ${flitePath}\n`)
|
||||
@@ -425,7 +468,7 @@ dotenv.config()
|
||||
LogHelper.info('Watson STT')
|
||||
|
||||
try {
|
||||
const json = JSON.parse(fs.readFileSync(watsonSttPath))
|
||||
const json = JSON.parse(await fs.promises.readFile(watsonSttPath))
|
||||
const results = []
|
||||
Object.keys(json).forEach((item) => {
|
||||
if (json[item] === '') results.push(false)
|
||||
@@ -484,21 +527,23 @@ dotenv.config()
|
||||
LogHelper.error('Please fix the errors above')
|
||||
}
|
||||
|
||||
pastebinData.report = report
|
||||
reportDataInput.report = report
|
||||
|
||||
reportDataInput = JSON.parse(
|
||||
SystemHelper.sanitizeUsername(JSON.stringify(reportDataInput))
|
||||
)
|
||||
|
||||
LogHelper.title('REPORT URL')
|
||||
|
||||
LogHelper.info('Sending report...')
|
||||
|
||||
try {
|
||||
const { body } = await request
|
||||
.post('https://getleon.ai/api/report')
|
||||
.send({
|
||||
report: pastebinData
|
||||
})
|
||||
const { data: reportData } = body
|
||||
const { data } = await axios.post('https://getleon.ai/api/report', {
|
||||
report: reportDataInput
|
||||
})
|
||||
const { data: responseReportData } = data
|
||||
|
||||
LogHelper.success(`Report URL: ${reportData.reportUrl}`)
|
||||
LogHelper.success(`Report URL: ${responseReportData.reportUrl}`)
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to send report: ${e}`)
|
||||
}
|
||||
|
||||
@@ -22,13 +22,13 @@ export default () =>
|
||||
try {
|
||||
// TODO: handle case where the memory folder contain multiple DB nodes
|
||||
const dbFolder = join(currentSkill.path, 'memory')
|
||||
const dbTestFiles = fs
|
||||
.readdirSync(dbFolder)
|
||||
.filter((entity) => entity.indexOf('.spec.json') !== -1)
|
||||
const dbTestFiles = (await fs.promises.readdir(dbFolder)).filter(
|
||||
(entity) => entity.indexOf('.spec.json') !== -1
|
||||
)
|
||||
|
||||
if (dbTestFiles.length > 0) {
|
||||
LogHelper.info(`Deleting ${dbTestFiles[0]}...`)
|
||||
fs.unlinkSync(join(dbFolder, dbTestFiles[0]))
|
||||
await fs.promises.unlink(join(dbFolder, dbTestFiles[0]))
|
||||
LogHelper.success(`${dbTestFiles[0]} deleted`)
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
+20
-15
@@ -6,24 +6,29 @@ import { LogHelper } from '@/helpers/log-helper'
|
||||
* This script is executed after "git commit" or "git merge" (Git hook https://git-scm.com/docs/githooks#_commit_msg)
|
||||
* it ensures the authenticity of commit messages
|
||||
*/
|
||||
LogHelper.info('Checking commit message...')
|
||||
;(async () => {
|
||||
LogHelper.info('Checking commit message...')
|
||||
|
||||
const commitEditMsgFile = '.git/COMMIT_EDITMSG'
|
||||
const commitEditMsgFile = '.git/COMMIT_EDITMSG'
|
||||
|
||||
if (fs.existsSync(commitEditMsgFile)) {
|
||||
try {
|
||||
const commitMessage = fs.readFileSync(commitEditMsgFile, 'utf8')
|
||||
const regex =
|
||||
'(build|BREAKING|chore|ci|docs|feat|fix|perf|refactor|style|test)(\\((web app|docker|server|hotword|tcp server|python bridge|skill\\/([\\w-]+)))?\\)?: .{1,50}'
|
||||
if (fs.existsSync(commitEditMsgFile)) {
|
||||
try {
|
||||
const commitMessage = await fs.promises.readFile(
|
||||
commitEditMsgFile,
|
||||
'utf8'
|
||||
)
|
||||
const regex =
|
||||
'(build|BREAKING|chore|ci|docs|feat|fix|perf|refactor|style|test)(\\((web app|scripts|docker|server|hotword|tcp server|bridge\\/(python|nodejs)|skill\\/([\\w-]+)))?\\)?: .{1,50}'
|
||||
|
||||
if (commitMessage.match(regex) !== null) {
|
||||
LogHelper.success('Commit message validated')
|
||||
} else {
|
||||
LogHelper.error(`Commit message does not match the format: ${regex}`)
|
||||
if (commitMessage.match(regex) !== null) {
|
||||
LogHelper.success('Commit message validated')
|
||||
} else {
|
||||
LogHelper.error(`Commit message does not match the format: ${regex}`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.error(e.message)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.error(e.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -14,7 +14,7 @@ dotenv.config()
|
||||
* Generate HTTP API key script
|
||||
* save it in the .env file
|
||||
*/
|
||||
const generateHttpApiKey = () =>
|
||||
const generateHTTPAPIKey = () =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
LogHelper.info('Generating the HTTP API key...')
|
||||
|
||||
@@ -23,7 +23,7 @@ const generateHttpApiKey = () =>
|
||||
const str = StringHelper.random(11)
|
||||
const dotEnvPath = path.join(process.cwd(), '.env')
|
||||
const envVarKey = 'LEON_HTTP_API_KEY'
|
||||
let content = fs.readFileSync(dotEnvPath, 'utf8')
|
||||
let content = await fs.promises.readFile(dotEnvPath, 'utf8')
|
||||
|
||||
shasum.update(str)
|
||||
const sha1 = shasum.digest('hex')
|
||||
@@ -39,7 +39,7 @@ const generateHttpApiKey = () =>
|
||||
|
||||
content = lines.join('\n')
|
||||
|
||||
fs.writeFileSync(dotEnvPath, content)
|
||||
await fs.promises.writeFile(dotEnvPath, content)
|
||||
LogHelper.success('HTTP API key generated')
|
||||
|
||||
resolve()
|
||||
@@ -56,17 +56,17 @@ export default () =>
|
||||
!process.env.LEON_HTTP_API_KEY ||
|
||||
process.env.LEON_HTTP_API_KEY === ''
|
||||
) {
|
||||
await generateHttpApiKey()
|
||||
await generateHTTPAPIKey()
|
||||
} else if (!process.env.IS_DOCKER) {
|
||||
const answer = await prompt({
|
||||
type: 'confirm',
|
||||
name: 'generate.httpApiKey',
|
||||
name: 'generate.httpAPIKey',
|
||||
message: 'Do you want to regenerate the HTTP API key?',
|
||||
default: false
|
||||
})
|
||||
|
||||
if (answer.generate.httpApiKey === true) {
|
||||
await generateHttpApiKey()
|
||||
if (answer.generate.httpAPIKey === true) {
|
||||
await generateHTTPAPIKey()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import {
|
||||
domainSchemaObject,
|
||||
skillSchemaObject,
|
||||
skillConfigSchemaObject
|
||||
} from '@/schemas/skill-schemas'
|
||||
import {
|
||||
globalEntitySchemaObject,
|
||||
globalResolverSchemaObject,
|
||||
globalAnswersSchemaObject
|
||||
} from '@/schemas/global-data-schemas'
|
||||
import {
|
||||
amazonVoiceConfiguration,
|
||||
googleCloudVoiceConfiguration,
|
||||
watsonVoiceConfiguration
|
||||
} from '@/schemas/voice-config-schemas'
|
||||
|
||||
/**
|
||||
* Generate JSON schemas
|
||||
* @param {string} categoryName
|
||||
* @param {Map<string, Object>} schemas
|
||||
*/
|
||||
export const generateSchemas = async (categoryName, schemas) => {
|
||||
const categorySchemasPath = path.join(process.cwd(), 'schemas', categoryName)
|
||||
|
||||
await fs.promises.mkdir(categorySchemasPath, { recursive: true })
|
||||
|
||||
for (const [schemaName, schemaObject] of schemas.entries()) {
|
||||
const schemaPath = path.join(categorySchemasPath, `${schemaName}.json`)
|
||||
|
||||
await fs.promises.writeFile(
|
||||
schemaPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
$schema: 'https://json-schema.org/draft-07/schema',
|
||||
...schemaObject
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
LogHelper.info('Generating the JSON schemas...')
|
||||
|
||||
await Promise.all([
|
||||
generateSchemas(
|
||||
'global-data',
|
||||
new Map([
|
||||
['global-entity', globalEntitySchemaObject],
|
||||
['global-resolver', globalResolverSchemaObject],
|
||||
['global-answers', globalAnswersSchemaObject]
|
||||
])
|
||||
),
|
||||
generateSchemas(
|
||||
'skill-schemas',
|
||||
new Map([
|
||||
['domain', domainSchemaObject],
|
||||
['skill', skillSchemaObject],
|
||||
['skill-config', skillConfigSchemaObject]
|
||||
])
|
||||
),
|
||||
generateSchemas(
|
||||
'voice-config-schemas',
|
||||
new Map([
|
||||
['amazon', amazonVoiceConfiguration],
|
||||
['google-cloud', googleCloudVoiceConfiguration],
|
||||
['watson-stt', watsonVoiceConfiguration],
|
||||
['watson-tts', watsonVoiceConfiguration]
|
||||
])
|
||||
)
|
||||
])
|
||||
|
||||
LogHelper.success('JSON schemas generated')
|
||||
}
|
||||
@@ -39,7 +39,9 @@ export default () =>
|
||||
|
||||
// Check if a new routing generation is necessary
|
||||
if (fs.existsSync(outputFilePath)) {
|
||||
const mtimeEndpoints = fs.statSync(outputFilePath).mtime.getTime()
|
||||
const mtimeEndpoints = (
|
||||
await fs.promises.stat(outputFilePath)
|
||||
).mtime.getTime()
|
||||
|
||||
let i = 0
|
||||
for (const currentDomain of skillDomains.values()) {
|
||||
@@ -49,7 +51,7 @@ export default () =>
|
||||
for (let j = 0; j < skillKeys.length; j += 1) {
|
||||
const skillFriendlyName = skillKeys[j]
|
||||
const currentSkill = currentDomain.skills[skillFriendlyName]
|
||||
const fileInfo = fs.statSync(
|
||||
const fileInfo = await fs.promises.stat(
|
||||
path.join(currentSkill.path, 'config', `${lang}.json`)
|
||||
)
|
||||
const mtime = fileInfo.mtime.getTime()
|
||||
@@ -91,7 +93,7 @@ export default () =>
|
||||
`${lang}.json`
|
||||
)
|
||||
const { actions } = JSON.parse(
|
||||
fs.readFileSync(configFilePath, 'utf8')
|
||||
await fs.promises.readFile(configFilePath, 'utf8')
|
||||
)
|
||||
const actionsKeys = Object.keys(actions)
|
||||
|
||||
@@ -145,7 +147,10 @@ export default () =>
|
||||
|
||||
LogHelper.info(`Writing ${outputFile} file...`)
|
||||
try {
|
||||
fs.writeFileSync(outputFilePath, JSON.stringify(finalObj, null, 2))
|
||||
await fs.promises.writeFile(
|
||||
outputFilePath,
|
||||
JSON.stringify(finalObj, null, 2)
|
||||
)
|
||||
LogHelper.success(`${outputFile} file generated`)
|
||||
resolve()
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
import generateJsonSchemas from './generate-json-schemas'
|
||||
|
||||
/**
|
||||
* Execute the generating JSON schemas script
|
||||
*/
|
||||
;(async () => {
|
||||
try {
|
||||
await generateJsonSchemas()
|
||||
} catch (error) {
|
||||
LogHelper.error(`Failed to generate the json schemas: ${error}`)
|
||||
}
|
||||
})()
|
||||
@@ -35,9 +35,12 @@ export default (version) =>
|
||||
|
||||
const repoUrl = sh.stdout.substr(0, sh.stdout.lastIndexOf('.git'))
|
||||
const previousTag = sh.stdout.substr(sh.stdout.indexOf('\n') + 1).trim()
|
||||
const changelogData = fs.readFileSync(changelog, 'utf8')
|
||||
const changelogData = await fs.promises.readFile(changelog, 'utf8')
|
||||
const compareUrl = `${repoUrl}/compare/${previousTag}...v${version}`
|
||||
let tmpData = fs.readFileSync(`scripts/tmp/${tmpChangelog}`, 'utf8')
|
||||
let tmpData = await fs.promises.readFile(
|
||||
`scripts/tmp/${tmpChangelog}`,
|
||||
'utf8'
|
||||
)
|
||||
|
||||
LogHelper.success(`Remote origin URL gotten: ${repoUrl}.git`)
|
||||
LogHelper.success(`Previous tag gotten: ${previousTag}`)
|
||||
@@ -46,14 +49,14 @@ export default (version) =>
|
||||
tmpData = tmpData.replace(version, `[${version}](${compareUrl})`)
|
||||
}
|
||||
|
||||
fs.writeFile(changelog, `${tmpData}${changelogData}`, (err) => {
|
||||
if (err) LogHelper.error(`Failed to write into file: ${err}`)
|
||||
else {
|
||||
fs.unlinkSync(`scripts/tmp/${tmpChangelog}`)
|
||||
LogHelper.success(`${changelog} generated`)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
try {
|
||||
await fs.promises.writeFile(changelog, `${tmpData}${changelogData}`)
|
||||
await fs.promises.unlink(`scripts/tmp/${tmpChangelog}`)
|
||||
LogHelper.success(`${changelog} generated`)
|
||||
resolve()
|
||||
} catch (error) {
|
||||
LogHelper.error(`Failed to write into file: ${error}`)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.error(`Error during git commands: ${e}`)
|
||||
reject(e)
|
||||
|
||||
@@ -3,7 +3,11 @@ import path from 'node:path'
|
||||
import { prompt } from 'inquirer'
|
||||
import { command } from 'execa'
|
||||
|
||||
import { PYTHON_BRIDGE_SRC_PATH, TCP_SERVER_SRC_PATH } from '@/constants'
|
||||
import {
|
||||
NODEJS_BRIDGE_SRC_PATH,
|
||||
PYTHON_BRIDGE_SRC_PATH,
|
||||
TCP_SERVER_SRC_PATH
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
|
||||
@@ -15,13 +19,17 @@ import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
|
||||
const BUILD_TARGETS = new Map()
|
||||
|
||||
BUILD_TARGETS.set('nodejs-bridge', {
|
||||
workflowFileName: 'pre-release-nodejs-bridge.yml',
|
||||
versionFilePath: path.join(NODEJS_BRIDGE_SRC_PATH, 'version.ts')
|
||||
})
|
||||
BUILD_TARGETS.set('python-bridge', {
|
||||
workflowFileName: 'pre-release-python-bridge.yml',
|
||||
setupFilePath: path.join(PYTHON_BRIDGE_SRC_PATH, 'setup.py')
|
||||
versionFilePath: path.join(PYTHON_BRIDGE_SRC_PATH, 'version.py')
|
||||
})
|
||||
BUILD_TARGETS.set('tcp-server', {
|
||||
workflowFileName: 'pre-release-tcp-server.yml',
|
||||
setupFilePath: path.join(TCP_SERVER_SRC_PATH, 'setup.py')
|
||||
versionFilePath: path.join(TCP_SERVER_SRC_PATH, 'version.py')
|
||||
})
|
||||
;(async () => {
|
||||
LoaderHelper.start()
|
||||
@@ -29,21 +37,21 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
const { argv } = process
|
||||
const givenReleaseTarget = argv[2].toLowerCase()
|
||||
const givenBranch = argv[3]?.toLowerCase()
|
||||
const { workflowFileName, setupFilePath } =
|
||||
const { workflowFileName, versionFilePath } =
|
||||
BUILD_TARGETS.get(givenReleaseTarget)
|
||||
|
||||
LoaderHelper.stop()
|
||||
const answer = await prompt({
|
||||
type: 'confirm',
|
||||
name: 'binary.bumped',
|
||||
message: `Have you bumped the version number of the binary from the "${setupFilePath}" file?`,
|
||||
message: `Have you bumped the version number of the binary from the "${versionFilePath}" file?`,
|
||||
default: false
|
||||
})
|
||||
LoaderHelper.start()
|
||||
|
||||
if (!answer.binary.bumped) {
|
||||
LogHelper.info(
|
||||
'Please bump the version number of the binary from the setup file before continuing'
|
||||
'Please bump the version number of the binary from the version file before continuing'
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ export default (version) =>
|
||||
LogHelper.info('Updating version...')
|
||||
|
||||
const promises = []
|
||||
const files = ['package.json', 'package-lock.json']
|
||||
// const files = ['package.json', 'package-lock.json']
|
||||
const files = ['package.json']
|
||||
|
||||
for (let i = 0; i < files.length; i += 1) {
|
||||
promises.push(
|
||||
|
||||
@@ -9,6 +9,6 @@ import setupHotword from './setup-hotword'
|
||||
try {
|
||||
await setupHotword()
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to setup offline hotword: ${e}`)
|
||||
LogHelper.error(`Failed to set up offline hotword: ${e}`)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -9,6 +9,6 @@ import setupStt from './setup-stt'
|
||||
try {
|
||||
await setupStt()
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to setup offline STT: ${e}`)
|
||||
LogHelper.error(`Failed to set up offline STT: ${e}`)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -9,6 +9,6 @@ import setupTts from './setup-tts'
|
||||
try {
|
||||
await setupTts()
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to setup offline TTS: ${e}`)
|
||||
LogHelper.error(`Failed to set up offline TTS: ${e}`)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { command } from 'execa'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { OSHelper } from '@/helpers/os-helper'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
/**
|
||||
* Setup offline hotword detection
|
||||
@@ -10,7 +10,7 @@ export default () =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
LogHelper.info('Setting up offline hotword detection...')
|
||||
|
||||
const info = OSHelper.getInformation()
|
||||
const info = SystemHelper.getInformation()
|
||||
let pkgm = 'apt-get install'
|
||||
if (info.type === 'macos') {
|
||||
pkgm = 'brew'
|
||||
|
||||
@@ -3,10 +3,10 @@ import fs from 'node:fs'
|
||||
import { command } from 'execa'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { OSHelper } from '@/helpers/os-helper'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
/**
|
||||
* Setup offline speech-to-text
|
||||
* Set up offline speech-to-text
|
||||
*/
|
||||
export default () =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
@@ -17,7 +17,7 @@ export default () =>
|
||||
// check this repo for updates: https://github.com/coqui-ai/STT-models/tree/main/english/coqui
|
||||
const coquiModelVersion = '1.0.0'
|
||||
let downloader = 'wget'
|
||||
if (OSHelper.getInformation().type === 'macos') {
|
||||
if (SystemHelper.getInformation().type === 'macos') {
|
||||
downloader = 'curl -L -O'
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import fs from 'node:fs'
|
||||
import { command } from 'execa'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { OSHelper } from '@/helpers/os-helper'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
/**
|
||||
* Setup offline text-to-speech
|
||||
* Set up offline text-to-speech
|
||||
*/
|
||||
export default () =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
@@ -15,11 +15,11 @@ export default () =>
|
||||
const destFliteFolder = 'bin/flite'
|
||||
const tmpDir = 'scripts/tmp'
|
||||
let makeCores = ''
|
||||
if (OSHelper.getNumberOfCPUCores() > 2) {
|
||||
makeCores = `-j ${OSHelper.getNumberOfCPUCores() - 2}`
|
||||
if (SystemHelper.getNumberOfCPUCores() > 2) {
|
||||
makeCores = `-j ${SystemHelper.getNumberOfCPUCores() - 2}`
|
||||
}
|
||||
let downloader = 'wget'
|
||||
if (OSHelper.getInformation().type === 'macos') {
|
||||
if (SystemHelper.getInformation().type === 'macos') {
|
||||
downloader = 'curl -L -O'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { LEON_FILE_PATH } from '@/constants'
|
||||
import { Telemetry } from '@/telemetry'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
const { instanceID, birthDate } = await Telemetry.postInstall()
|
||||
|
||||
if (!fs.existsSync(LEON_FILE_PATH)) {
|
||||
await fs.promises.writeFile(
|
||||
LEON_FILE_PATH,
|
||||
JSON.stringify(
|
||||
{
|
||||
instanceID,
|
||||
birthDate
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
LogHelper.success(`Instance ID created: ${instanceID}`)
|
||||
} else {
|
||||
LogHelper.success(`Instance ID already exists: ${instanceID}`)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.warning(`Failed to create the instance ID: ${e}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import stream from 'node:stream'
|
||||
import readline from 'node:readline'
|
||||
|
||||
import axios from 'axios'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import prettyMilliseconds from 'pretty-ms'
|
||||
import extractZip from 'extract-zip'
|
||||
|
||||
import {
|
||||
BINARIES_FOLDER_NAME,
|
||||
GITHUB_URL,
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
PYTHON_BRIDGE_DIST_PATH,
|
||||
TCP_SERVER_DIST_PATH,
|
||||
NODEJS_BRIDGE_BIN_NAME,
|
||||
PYTHON_BRIDGE_BIN_NAME,
|
||||
TCP_SERVER_BIN_NAME,
|
||||
NODEJS_BRIDGE_VERSION,
|
||||
PYTHON_BRIDGE_VERSION,
|
||||
TCP_SERVER_VERSION
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
/**
|
||||
* Set up binaries according to the given setup target
|
||||
* 1. Delete the existing dist binaries if already exist
|
||||
* 2. Download the latest binaries from GitHub releases
|
||||
* 3. Extract the downloaded ZIP file to the dist folder
|
||||
*/
|
||||
|
||||
const TARGETS = new Map()
|
||||
|
||||
TARGETS.set('nodejs-bridge', {
|
||||
name: 'Node.js bridge',
|
||||
distPath: NODEJS_BRIDGE_DIST_PATH,
|
||||
manifestPath: path.join(NODEJS_BRIDGE_DIST_PATH, 'manifest.json'),
|
||||
archiveName: `${NODEJS_BRIDGE_BIN_NAME.split('.')[0]}.zip`,
|
||||
version: NODEJS_BRIDGE_VERSION,
|
||||
isPlatformDependent: false // Need to be built for the target platform or not
|
||||
})
|
||||
TARGETS.set('python-bridge', {
|
||||
name: 'Python bridge',
|
||||
distPath: PYTHON_BRIDGE_DIST_PATH,
|
||||
manifestPath: path.join(PYTHON_BRIDGE_DIST_PATH, 'manifest.json'),
|
||||
archiveName: `${PYTHON_BRIDGE_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
|
||||
version: PYTHON_BRIDGE_VERSION,
|
||||
isPlatformDependent: true
|
||||
})
|
||||
TARGETS.set('tcp-server', {
|
||||
name: 'TCP server',
|
||||
distPath: TCP_SERVER_DIST_PATH,
|
||||
manifestPath: path.join(TCP_SERVER_DIST_PATH, 'manifest.json'),
|
||||
archiveName: `${TCP_SERVER_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
|
||||
version: TCP_SERVER_VERSION,
|
||||
isPlatformDependent: true
|
||||
})
|
||||
|
||||
async function createManifestFile(manifestPath, name, version) {
|
||||
const manifest = {
|
||||
name,
|
||||
version,
|
||||
setupDate: Date.now()
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2))
|
||||
}
|
||||
|
||||
const setupBinaries = async (key) => {
|
||||
const {
|
||||
name,
|
||||
distPath,
|
||||
archiveName,
|
||||
version,
|
||||
manifestPath,
|
||||
isPlatformDependent
|
||||
} = TARGETS.get(key)
|
||||
let manifest = null
|
||||
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'))
|
||||
|
||||
LogHelper.info(`Found ${name} ${manifest.version}`)
|
||||
LogHelper.info(`Latest version is ${version}`)
|
||||
}
|
||||
|
||||
if (!manifest || manifest.version !== version) {
|
||||
const buildPath = isPlatformDependent
|
||||
? path.join(distPath, BINARIES_FOLDER_NAME)
|
||||
: path.join(distPath, 'bin')
|
||||
const archivePath = path.join(distPath, archiveName)
|
||||
|
||||
await Promise.all([
|
||||
fs.promises.rm(buildPath, { recursive: true, force: true }),
|
||||
fs.promises.rm(archivePath, { recursive: true, force: true })
|
||||
])
|
||||
|
||||
try {
|
||||
LogHelper.info(`Downloading ${name}...`)
|
||||
|
||||
const archiveWriter = fs.createWriteStream(archivePath)
|
||||
const latestReleaseAssetURL = `${GITHUB_URL}/releases/download/${key}_v${version}/${archiveName}`
|
||||
const { data } = await axios.get(latestReleaseAssetURL, {
|
||||
responseType: 'stream',
|
||||
onDownloadProgress: ({ loaded, total, progress, estimated, rate }) => {
|
||||
const percentage = Math.floor(progress * 100)
|
||||
const downloadedSize = prettyBytes(loaded)
|
||||
const totalSize = prettyBytes(total)
|
||||
const estimatedTime = !estimated
|
||||
? 0
|
||||
: prettyMilliseconds(estimated * 1_000, { secondsDecimalDigits: 0 })
|
||||
const downloadRate = !rate ? 0 : prettyBytes(rate)
|
||||
|
||||
readline.clearLine(process.stdout, 0)
|
||||
readline.cursorTo(process.stdout, 0, null)
|
||||
process.stdout.write(
|
||||
`Download progress: ${percentage}% (${downloadedSize}/${totalSize} | ${downloadRate}/s | ${estimatedTime} ETA)`
|
||||
)
|
||||
|
||||
if (percentage === 100) {
|
||||
process.stdout.write('\n')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
data.pipe(archiveWriter)
|
||||
await stream.promises.finished(archiveWriter)
|
||||
|
||||
LogHelper.success(`${name} downloaded`)
|
||||
LogHelper.info(`Extracting ${name}...`)
|
||||
|
||||
const absoluteDistPath = isPlatformDependent
|
||||
? path.resolve(distPath)
|
||||
: path.resolve(distPath, 'bin')
|
||||
await extractZip(archivePath, { dir: absoluteDistPath })
|
||||
|
||||
LogHelper.success(`${name} extracted`)
|
||||
|
||||
await Promise.all([
|
||||
fs.promises.rm(archivePath, { recursive: true, force: true }),
|
||||
createManifestFile(manifestPath, name, version)
|
||||
])
|
||||
|
||||
LogHelper.success(`${name} manifest file created`)
|
||||
LogHelper.success(`${name} ${version} ready`)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set up ${name}: ${error}`)
|
||||
}
|
||||
} else {
|
||||
LogHelper.success(`${name} is already at the latest version (${version})`)
|
||||
}
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
await setupBinaries('nodejs-bridge')
|
||||
await setupBinaries('python-bridge')
|
||||
await setupBinaries('tcp-server')
|
||||
}
|
||||
@@ -4,23 +4,23 @@ import path from 'node:path'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
/**
|
||||
* Setup Leon's core configuration
|
||||
* Set up Leon's core configuration
|
||||
*/
|
||||
export default () =>
|
||||
new Promise((resolve) => {
|
||||
new Promise(async (resolve) => {
|
||||
LogHelper.info('Configuring core...')
|
||||
|
||||
const dir = 'core/config'
|
||||
const list = (dir) => {
|
||||
const entities = fs.readdirSync(dir)
|
||||
const list = async (dir) => {
|
||||
const entities = await fs.promises.readdir(dir)
|
||||
|
||||
// Browse core config entities
|
||||
for (let i = 0; i < entities.length; i += 1) {
|
||||
const file = `${entities[i].replace('.sample.json', '.json')}`
|
||||
// Recursive if the entity is a directory
|
||||
const way = path.join(dir, entities[i])
|
||||
if (fs.statSync(way).isDirectory()) {
|
||||
list(way)
|
||||
if ((await fs.promises.stat(way)).isDirectory()) {
|
||||
await list(way)
|
||||
} else if (
|
||||
entities[i].indexOf('.sample.json') !== -1 &&
|
||||
!fs.existsSync(`${dir}/${file}`)
|
||||
@@ -40,6 +40,6 @@ export default () =>
|
||||
}
|
||||
}
|
||||
|
||||
list(dir)
|
||||
await list(dir)
|
||||
resolve()
|
||||
})
|
||||
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
PYTHON_BRIDGE_SRC_PATH,
|
||||
TCP_SERVER_SRC_PATH
|
||||
} from '@/constants'
|
||||
import { CPUArchitectures, OSTypes } from '@/types'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
import { CPUArchitectures, OSHelper, OSTypes } from '@/helpers/os-helper'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
/**
|
||||
* Set up development environment according to the given setup target
|
||||
@@ -128,19 +129,19 @@ SPACY_MODELS.set('fr', {
|
||||
|
||||
const pipfileMtime = fs.statSync(pipfilePath).mtime
|
||||
const hasDotVenv = fs.existsSync(dotVenvPath)
|
||||
const { type: osType, cpuArchitecture } = OSHelper.getInformation()
|
||||
const { type: osType, cpuArchitecture } = SystemHelper.getInformation()
|
||||
const installPythonPackages = async () => {
|
||||
LogHelper.info(`Installing Python packages from ${pipfilePath}.lock...`)
|
||||
|
||||
// Delete .venv directory to reset the development environment
|
||||
if (hasDotVenv) {
|
||||
LogHelper.info(`Deleting ${dotVenvPath}...`)
|
||||
fs.rmSync(dotVenvPath, { recursive: true, force: true })
|
||||
await fs.promises.rm(dotVenvPath, { recursive: true, force: true })
|
||||
LogHelper.success(`${dotVenvPath} deleted`)
|
||||
}
|
||||
|
||||
try {
|
||||
await command(`pipenv install --verbose --site-packages`, {
|
||||
await command('pipenv install --verbose --site-packages', {
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
@@ -154,21 +155,21 @@ SPACY_MODELS.set('fr', {
|
||||
LogHelper.info(
|
||||
'Installing Rust installer as it is needed for the "tokenizers" package for macOS ARM64 architecture...'
|
||||
)
|
||||
await command(`curl https://sh.rustup.rs -sSf | sh -s -- -y`, {
|
||||
await command('curl https://sh.rustup.rs -sSf | sh -s -- -y', {
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
LogHelper.success('Rust installer installed')
|
||||
|
||||
LogHelper.info('Reloading configuration from "$HOME/.cargo/env"...')
|
||||
await command(`source "$HOME/.cargo/env"`, {
|
||||
await command('source "$HOME/.cargo/env"', {
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
LogHelper.success('Configuration reloaded')
|
||||
|
||||
LogHelper.info('Checking Rust compiler version...')
|
||||
await command(`rustc --version`, {
|
||||
await command('rustc --version', {
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
@@ -210,7 +211,7 @@ SPACY_MODELS.set('fr', {
|
||||
await installPythonPackages()
|
||||
} else {
|
||||
if (fs.existsSync(dotProjectPath)) {
|
||||
const dotProjectMtime = fs.statSync(dotProjectPath).mtime
|
||||
const dotProjectMtime = (await fs.promises.stat(dotProjectPath)).mtime
|
||||
|
||||
// Check if Python deps tree has been modified since the initial setup
|
||||
if (pipfileMtime > dotProjectMtime) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { LogHelper } from '@/helpers/log-helper'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
|
||||
/**
|
||||
* Setup skills configuration
|
||||
* Set up skills configuration
|
||||
*/
|
||||
export default () =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
@@ -31,10 +31,10 @@ export default () =>
|
||||
// Check if the config and config.sample file exist
|
||||
if (fs.existsSync(configFile) && fs.existsSync(configSampleFile)) {
|
||||
const config = JSON.parse(
|
||||
fs.readFileSync(configFile, 'utf8')
|
||||
await fs.promises.readFile(configFile, 'utf8')
|
||||
)?.configurations
|
||||
const configSample = JSON.parse(
|
||||
fs.readFileSync(configSampleFile, 'utf8')
|
||||
await fs.promises.readFile(configSampleFile, 'utf8')
|
||||
)?.configurations
|
||||
const configKeys = Object.keys(config)
|
||||
const configSampleKeys = Object.keys(configSample)
|
||||
|
||||
@@ -2,12 +2,14 @@ import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
import train from '../train/train'
|
||||
import generateHttpApiKey from '../generate/generate-http-api-key'
|
||||
import generateHTTPAPIKey from '../generate/generate-http-api-key'
|
||||
import generateJSONSchemas from '../generate/generate-json-schemas'
|
||||
|
||||
import setupDotenv from './setup-dotenv'
|
||||
import setupCore from './setup-core'
|
||||
import setupSkillsConfig from './setup-skills-config'
|
||||
// import setupPythonPackages from './setup-python-packages'
|
||||
import setupBinaries from './setup-binaries'
|
||||
import createInstanceID from './create-instance-id'
|
||||
|
||||
// Do not load ".env" file because it is not created yet
|
||||
|
||||
@@ -19,13 +21,13 @@ import setupSkillsConfig from './setup-skills-config'
|
||||
await setupDotenv()
|
||||
LoaderHelper.start()
|
||||
await Promise.all([setupCore(), setupSkillsConfig()])
|
||||
// TODO: download Python bridge archive; unpack it; remove archive
|
||||
// TODO: download TCP server archive; unpack it; remove archive
|
||||
// await setupPythonPackages()
|
||||
LoaderHelper.stop()
|
||||
await generateHttpApiKey()
|
||||
await setupBinaries()
|
||||
await generateHTTPAPIKey()
|
||||
await generateJSONSchemas()
|
||||
LoaderHelper.start()
|
||||
await train()
|
||||
await createInstanceID()
|
||||
|
||||
LogHelper.default('')
|
||||
LogHelper.success('Hooray! Leon is installed and ready to go!')
|
||||
|
||||
@@ -8,16 +8,17 @@ import { LogHelper } from '@/helpers/log-helper'
|
||||
* Add global entities annotations (@...)
|
||||
*/
|
||||
export default (lang, nlp) =>
|
||||
new Promise((resolve) => {
|
||||
new Promise(async (resolve) => {
|
||||
LogHelper.title('Global entities training')
|
||||
|
||||
const globalEntitiesPath = path.join(
|
||||
process.cwd(),
|
||||
'core/data',
|
||||
'core',
|
||||
'data',
|
||||
lang,
|
||||
'global-entities'
|
||||
)
|
||||
const globalEntityFiles = fs.readdirSync(globalEntitiesPath)
|
||||
const globalEntityFiles = await fs.promises.readdir(globalEntitiesPath)
|
||||
const newEntitiesObj = {}
|
||||
|
||||
for (let i = 0; i < globalEntityFiles.length; i += 1) {
|
||||
@@ -27,7 +28,9 @@ export default (lang, nlp) =>
|
||||
globalEntitiesPath,
|
||||
globalEntityFileName
|
||||
)
|
||||
const { options } = JSON.parse(fs.readFileSync(globalEntityPath, 'utf8'))
|
||||
const { options } = JSON.parse(
|
||||
await fs.promises.readFile(globalEntityPath, 'utf8')
|
||||
)
|
||||
const optionKeys = Object.keys(options)
|
||||
const optionsObj = {}
|
||||
|
||||
|
||||
@@ -9,22 +9,23 @@ import { LogHelper } from '@/helpers/log-helper'
|
||||
* Train global resolvers
|
||||
*/
|
||||
export default (lang, nlp) =>
|
||||
new Promise((resolve) => {
|
||||
new Promise(async (resolve) => {
|
||||
LogHelper.title('Global resolvers training')
|
||||
|
||||
const resolversPath = path.join(
|
||||
process.cwd(),
|
||||
'core/data',
|
||||
'core',
|
||||
'data',
|
||||
lang,
|
||||
'global-resolvers'
|
||||
)
|
||||
const resolverFiles = fs.readdirSync(resolversPath)
|
||||
const resolverFiles = await fs.promises.readdir(resolversPath)
|
||||
|
||||
for (let i = 0; i < resolverFiles.length; i += 1) {
|
||||
const resolverFileName = resolverFiles[i]
|
||||
const resolverPath = path.join(resolversPath, resolverFileName)
|
||||
const { name: resolverName, intents: resolverIntents } = JSON.parse(
|
||||
fs.readFileSync(resolverPath, 'utf8')
|
||||
await fs.promises.readFile(resolverPath, 'utf8')
|
||||
)
|
||||
const intentKeys = Object.keys(resolverIntents)
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import addFormats from 'ajv-formats'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
export const ajv = addFormats(
|
||||
new Ajv({
|
||||
allErrors: true,
|
||||
verbose: true
|
||||
}),
|
||||
[
|
||||
'date-time',
|
||||
'time',
|
||||
'date',
|
||||
'email',
|
||||
'hostname',
|
||||
'ipv4',
|
||||
'ipv6',
|
||||
'uri',
|
||||
'uri-reference',
|
||||
'uuid',
|
||||
'uri-template',
|
||||
'json-pointer',
|
||||
'relative-json-pointer',
|
||||
'regex'
|
||||
]
|
||||
)
|
||||
+67
-15
@@ -3,8 +3,8 @@ import fs from 'node:fs'
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import type { LongLanguageCode } from '@/helpers/lang-helper'
|
||||
import { OSHelper } from '@/helpers/os-helper'
|
||||
import type { LongLanguageCode } from '@/types'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
@@ -12,16 +12,34 @@ const PRODUCTION_ENV = 'production'
|
||||
const DEVELOPMENT_ENV = 'development'
|
||||
const TESTING_ENV = 'testing'
|
||||
|
||||
export const GITHUB_URL = 'https://github.com/leon-ai/leon'
|
||||
|
||||
/**
|
||||
* Binaries / distribution
|
||||
*/
|
||||
export const BINARIES_FOLDER_NAME = OSHelper.getBinariesFolderName()
|
||||
export const PYTHON_BRIDGE_DIST_PATH = path.join('bridges', 'python', 'dist')
|
||||
export const TCP_SERVER_DIST_PATH = path.join('tcp_server', 'dist')
|
||||
export const BINARIES_FOLDER_NAME = SystemHelper.getBinariesFolderName()
|
||||
export const NODEJS_BRIDGE_ROOT_PATH = path.join('bridges', 'nodejs')
|
||||
export const PYTHON_BRIDGE_ROOT_PATH = path.join('bridges', 'python')
|
||||
export const TCP_SERVER_ROOT_PATH = path.join('tcp_server')
|
||||
|
||||
export const PYTHON_BRIDGE_SRC_PATH = path.join('bridges', 'python', 'src')
|
||||
export const TCP_SERVER_SRC_PATH = path.join('tcp_server', 'src')
|
||||
export const NODEJS_BRIDGE_DIST_PATH = path.join(
|
||||
NODEJS_BRIDGE_ROOT_PATH,
|
||||
'dist'
|
||||
)
|
||||
export const PYTHON_BRIDGE_DIST_PATH = path.join(
|
||||
PYTHON_BRIDGE_ROOT_PATH,
|
||||
'dist'
|
||||
)
|
||||
export const TCP_SERVER_DIST_PATH = path.join(TCP_SERVER_ROOT_PATH, 'dist')
|
||||
|
||||
export const NODEJS_BRIDGE_SRC_PATH = path.join(NODEJS_BRIDGE_ROOT_PATH, 'src')
|
||||
export const PYTHON_BRIDGE_SRC_PATH = path.join(PYTHON_BRIDGE_ROOT_PATH, 'src')
|
||||
export const TCP_SERVER_SRC_PATH = path.join(TCP_SERVER_ROOT_PATH, 'src')
|
||||
|
||||
const NODEJS_BRIDGE_VERSION_FILE_PATH = path.join(
|
||||
NODEJS_BRIDGE_SRC_PATH,
|
||||
'version.ts'
|
||||
)
|
||||
const PYTHON_BRIDGE_VERSION_FILE_PATH = path.join(
|
||||
PYTHON_BRIDGE_SRC_PATH,
|
||||
'version.py'
|
||||
@@ -30,6 +48,9 @@ const TCP_SERVER_VERSION_FILE_PATH = path.join(
|
||||
TCP_SERVER_SRC_PATH,
|
||||
'version.py'
|
||||
)
|
||||
export const [, NODEJS_BRIDGE_VERSION] = fs
|
||||
.readFileSync(NODEJS_BRIDGE_VERSION_FILE_PATH, 'utf8')
|
||||
.split("'")
|
||||
export const [, PYTHON_BRIDGE_VERSION] = fs
|
||||
.readFileSync(PYTHON_BRIDGE_VERSION_FILE_PATH, 'utf8')
|
||||
.split("'")
|
||||
@@ -37,6 +58,7 @@ export const [, TCP_SERVER_VERSION] = fs
|
||||
.readFileSync(TCP_SERVER_VERSION_FILE_PATH, 'utf8')
|
||||
.split("'")
|
||||
|
||||
export const NODEJS_BRIDGE_BIN_NAME = 'leon-nodejs-bridge.js'
|
||||
export const PYTHON_BRIDGE_BIN_NAME = 'leon-python-bridge'
|
||||
export const TCP_SERVER_BIN_NAME = 'leon-tcp-server'
|
||||
|
||||
@@ -50,6 +72,13 @@ export const PYTHON_BRIDGE_BIN_PATH = path.join(
|
||||
BINARIES_FOLDER_NAME,
|
||||
PYTHON_BRIDGE_BIN_NAME
|
||||
)
|
||||
export const NODEJS_BRIDGE_BIN_PATH = `${process.execPath} ${path.join(
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
'bin',
|
||||
NODEJS_BRIDGE_BIN_NAME
|
||||
)}`
|
||||
|
||||
export const LEON_VERSION = process.env['npm_package_version']
|
||||
|
||||
/**
|
||||
* spaCy models
|
||||
@@ -63,10 +92,10 @@ export const FR_SPACY_MODEL_VERSION = '3.4.0'
|
||||
/**
|
||||
* Environments
|
||||
*/
|
||||
export const IS_PRODUCTION_ENV = process.env['LEON_NODE_ENV'] === PRODUCTION_ENV
|
||||
export const IS_DEVELOPMENT_ENV =
|
||||
process.env['LEON_NODE_ENV'] === DEVELOPMENT_ENV
|
||||
export const IS_TESTING_ENV = process.env['LEON_NODE_ENV'] === TESTING_ENV
|
||||
export const LEON_NODE_ENV = process.env['LEON_NODE_ENV'] || PRODUCTION_ENV
|
||||
export const IS_PRODUCTION_ENV = LEON_NODE_ENV === PRODUCTION_ENV
|
||||
export const IS_DEVELOPMENT_ENV = LEON_NODE_ENV === DEVELOPMENT_ENV
|
||||
export const IS_TESTING_ENV = LEON_NODE_ENV === TESTING_ENV
|
||||
|
||||
/**
|
||||
* Leon environment preferences
|
||||
@@ -74,7 +103,7 @@ export const IS_TESTING_ENV = process.env['LEON_NODE_ENV'] === TESTING_ENV
|
||||
export const LANG = process.env['LEON_LANG'] as LongLanguageCode
|
||||
|
||||
export const HOST = process.env['LEON_HOST']
|
||||
export const PORT = process.env['LEON_PORT']
|
||||
export const PORT = Number(process.env['LEON_PORT'])
|
||||
|
||||
export const TIME_ZONE = process.env['LEON_TIME_ZONE']
|
||||
|
||||
@@ -89,7 +118,30 @@ export const HAS_OVER_HTTP = process.env['LEON_OVER_HTTP'] === 'true'
|
||||
export const HTTP_API_KEY = process.env['LEON_HTTP_API_KEY']
|
||||
export const HTTP_API_LANG = process.env['LEON_HTTP_API_LANG']
|
||||
|
||||
export const HAS_LOGGER = process.env['LEON_LOGGER'] === 'true'
|
||||
|
||||
export const TCP_SERVER_HOST = process.env['LEON_PY_TCP_SERVER_HOST']
|
||||
export const TCP_SERVER_PORT = process.env['LEON_PY_TCP_SERVER_PORT']
|
||||
export const TCP_SERVER_PORT = Number(process.env['LEON_PY_TCP_SERVER_PORT'])
|
||||
|
||||
export const IS_TELEMETRY_ENABLED = process.env['LEON_TELEMETRY'] === 'true'
|
||||
|
||||
/**
|
||||
* Paths
|
||||
*/
|
||||
export const BIN_PATH = path.join('bin')
|
||||
export const GLOBAL_DATA_PATH = path.join('core', 'data')
|
||||
export const MODELS_PATH = path.join(GLOBAL_DATA_PATH, 'models')
|
||||
export const VOICE_CONFIG_PATH = path.join('core', 'config', 'voice')
|
||||
export const SERVER_PATH = path.join(
|
||||
'server',
|
||||
IS_PRODUCTION_ENV ? 'dist' : 'src'
|
||||
)
|
||||
export const TMP_PATH = path.join(SERVER_PATH, 'tmp')
|
||||
export const LEON_FILE_PATH = path.join('leon.json')
|
||||
|
||||
/**
|
||||
* Misc
|
||||
*/
|
||||
export const MINIMUM_REQUIRED_RAM = 4
|
||||
export const INSTANCE_ID = fs.existsSync(LEON_FILE_PATH)
|
||||
? JSON.parse(fs.readFileSync(LEON_FILE_PATH, 'utf8')).instanceID
|
||||
: null
|
||||
export const IS_GITPOD = process.env['GITPOD_WORKSPACE_URL'] !== undefined
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { path as ffmpegPath } from '@ffmpeg-installer/ffmpeg'
|
||||
import Ffmpeg from 'fluent-ffmpeg'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
const audios = {
|
||||
webm: `${__dirname}/../tmp/speech.webm`,
|
||||
wav: `${__dirname}/../tmp/speech.wav`
|
||||
}
|
||||
|
||||
class Asr {
|
||||
constructor() {
|
||||
this.blob = {}
|
||||
|
||||
LogHelper.title('ASR')
|
||||
LogHelper.success('New instance')
|
||||
}
|
||||
|
||||
static get audios() {
|
||||
return audios
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode audio blob to WAVE file
|
||||
* and forward the WAVE file to the STT parser
|
||||
*/
|
||||
run(blob, stt) {
|
||||
return new Promise((resolve, reject) => {
|
||||
LogHelper.title('ASR')
|
||||
|
||||
this.blob = blob
|
||||
|
||||
fs.writeFile(audios.webm, Buffer.from(this.blob), 'binary', (err) => {
|
||||
if (err) {
|
||||
reject({ type: 'error', obj: err })
|
||||
return
|
||||
}
|
||||
|
||||
const ffmpeg = new Ffmpeg()
|
||||
ffmpeg.setFfmpegPath(ffmpegPath)
|
||||
|
||||
/**
|
||||
* Encode WebM file to WAVE file
|
||||
* ffmpeg -i speech.webm -acodec pcm_s16le -ar 16000 -ac 1 speech.wav
|
||||
*/
|
||||
ffmpeg
|
||||
.addInput(audios.webm)
|
||||
.on('start', () => {
|
||||
LogHelper.info('Encoding WebM file to WAVE file...')
|
||||
})
|
||||
.on('end', () => {
|
||||
LogHelper.success('Encoding done')
|
||||
|
||||
if (Object.keys(stt).length === 0) {
|
||||
reject({
|
||||
type: 'warning',
|
||||
obj: new Error('The speech recognition is not ready yet')
|
||||
})
|
||||
} else {
|
||||
stt.parse(audios.wav)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
.on('error', (err) => {
|
||||
reject({ type: 'error', obj: new Error(`Encoding error ${err}`) })
|
||||
})
|
||||
.outputOptions(['-acodec pcm_s16le', '-ar 16000', '-ac 1'])
|
||||
.output(audios.wav)
|
||||
.run()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Asr
|
||||
@@ -0,0 +1,77 @@
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { path as ffmpegPath } from '@ffmpeg-installer/ffmpeg'
|
||||
import ffmpeg from 'fluent-ffmpeg'
|
||||
|
||||
import { TMP_PATH } from '@/constants'
|
||||
import { STT } from '@/core'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
export default class ASR {
|
||||
private static instance: ASR
|
||||
|
||||
public audioPaths = {
|
||||
webm: path.join(TMP_PATH, 'speech.webm'),
|
||||
wav: path.join(TMP_PATH, 'speech.wav')
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (!ASR.instance) {
|
||||
LogHelper.title('ASR')
|
||||
LogHelper.success('New instance')
|
||||
|
||||
ASR.instance = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode audio blob to WAVE file
|
||||
* and forward the WAVE file to the STT parser
|
||||
*/
|
||||
public encode(blob: Buffer): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
LogHelper.title('ASR')
|
||||
|
||||
fs.writeFile(
|
||||
this.audioPaths.webm,
|
||||
Buffer.from(blob),
|
||||
'binary',
|
||||
async (err) => {
|
||||
if (err) {
|
||||
reject(new Error(`${err}`))
|
||||
return
|
||||
}
|
||||
|
||||
ffmpeg.setFfmpegPath(ffmpegPath)
|
||||
|
||||
/**
|
||||
* Encode WebM file to WAVE file
|
||||
* ffmpeg -i speech.webm -acodec pcm_s16le -ar 16000 -ac 1 speech.wav
|
||||
*/
|
||||
ffmpeg()
|
||||
.addInput(this.audioPaths.webm)
|
||||
.on('start', () => {
|
||||
LogHelper.info('Encoding WebM file to WAVE file...')
|
||||
})
|
||||
.on('end', () => {
|
||||
LogHelper.success('Encoding done')
|
||||
|
||||
if (!STT.isParserReady) {
|
||||
reject(new Error('The speech recognition is not ready yet'))
|
||||
} else {
|
||||
STT.transcribe(this.audioPaths.wav)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
.on('error', (err) => {
|
||||
reject(new Error(`Encoding error ${err}`))
|
||||
})
|
||||
.outputOptions(['-acodec pcm_s16le', '-ar 16000', '-ac 1'])
|
||||
.output(this.audioPaths.wav)
|
||||
.run()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type ASRAudioFormat = 'wav' | 'webm'
|
||||
@@ -1,511 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
import { langs } from '@@/core/langs.json'
|
||||
import { HAS_TTS, PYTHON_BRIDGE_BIN_PATH } from '@/constants'
|
||||
import { LangHelper } from '@/helpers/lang-helper'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
import Synchronizer from '@/core/synchronizer'
|
||||
|
||||
class Brain {
|
||||
constructor() {
|
||||
this._lang = 'en'
|
||||
this.broca = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(process.cwd(), 'core/data', this._lang, 'answers.json'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
this.process = {}
|
||||
this.interOutput = {}
|
||||
this.finalOutput = {}
|
||||
this._socket = {}
|
||||
this._stt = {}
|
||||
this._tts = {}
|
||||
|
||||
LogHelper.title('Brain')
|
||||
LogHelper.success('New instance')
|
||||
}
|
||||
|
||||
get socket() {
|
||||
return this._socket
|
||||
}
|
||||
|
||||
set socket(newSocket) {
|
||||
this._socket = newSocket
|
||||
}
|
||||
|
||||
get stt() {
|
||||
return this._stt
|
||||
}
|
||||
|
||||
set stt(newStt) {
|
||||
this._stt = newStt
|
||||
}
|
||||
|
||||
get tts() {
|
||||
return this._tts
|
||||
}
|
||||
|
||||
set tts(newTts) {
|
||||
this._tts = newTts
|
||||
}
|
||||
|
||||
get lang() {
|
||||
return this._lang
|
||||
}
|
||||
|
||||
set lang(newLang) {
|
||||
this._lang = newLang
|
||||
// Update broca
|
||||
this.broca = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(process.cwd(), 'core/data', this._lang, 'answers.json'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
if (HAS_TTS) {
|
||||
this._tts.init(this._lang, () => {
|
||||
LogHelper.title('Brain')
|
||||
LogHelper.info('Language has changed')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete intent object file
|
||||
*/
|
||||
static deleteIntentObjFile(intentObjectPath) {
|
||||
try {
|
||||
if (fs.existsSync(intentObjectPath)) {
|
||||
fs.unlinkSync(intentObjectPath)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to delete intent object file: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make Leon talk
|
||||
*/
|
||||
talk(rawSpeech, end = false) {
|
||||
LogHelper.title('Leon')
|
||||
LogHelper.info('Talking...')
|
||||
|
||||
if (rawSpeech !== '') {
|
||||
if (HAS_TTS) {
|
||||
// Stripe HTML to a whitespace. Whitespace to let the TTS respects punctuation
|
||||
const speech = rawSpeech.replace(/<(?:.|\n)*?>/gm, ' ')
|
||||
|
||||
this._tts.add(speech, end)
|
||||
}
|
||||
|
||||
this._socket.emit('answer', rawSpeech)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pickup speech info we need to return
|
||||
*/
|
||||
wernicke(type, key, obj) {
|
||||
let answer = ''
|
||||
|
||||
// Choose a random answer or a specific one
|
||||
const property = this.broca.answers[type]
|
||||
if (property.constructor === [].constructor) {
|
||||
answer = property[Math.floor(Math.random() * property.length)]
|
||||
} else {
|
||||
answer = property
|
||||
}
|
||||
|
||||
// Select a specific key
|
||||
if (key !== '' && typeof key !== 'undefined') {
|
||||
answer = answer[key]
|
||||
}
|
||||
|
||||
// Parse sentence's value(s) and replace with the given object
|
||||
if (typeof obj !== 'undefined' && Object.keys(obj).length > 0) {
|
||||
answer = StringHelper.findAndMap(answer, obj)
|
||||
}
|
||||
|
||||
return answer
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Python skills
|
||||
* TODO: split into several methods
|
||||
*/
|
||||
execute(obj, opts) {
|
||||
const executionTimeStart = Date.now()
|
||||
opts = opts || {
|
||||
mute: false // Close Leon mouth e.g. over HTTP
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const utteranceId = `${Date.now()}-${StringHelper.random(4)}`
|
||||
const intentObjectPath = path.join(
|
||||
__dirname,
|
||||
`../tmp/${utteranceId}.json`
|
||||
)
|
||||
const speeches = []
|
||||
|
||||
// Ask to repeat if Leon is not sure about the request
|
||||
if (
|
||||
obj.classification.confidence <
|
||||
langs[LangHelper.getLongCode(this._lang)].min_confidence
|
||||
) {
|
||||
if (!opts.mute) {
|
||||
const speech = `${this.wernicke('random_not_sure')}.`
|
||||
|
||||
speeches.push(speech)
|
||||
this.talk(speech, true)
|
||||
this._socket.emit('is-typing', false)
|
||||
}
|
||||
|
||||
const executionTimeEnd = Date.now()
|
||||
const executionTime = executionTimeEnd - executionTimeStart
|
||||
|
||||
resolve({
|
||||
speeches,
|
||||
executionTime
|
||||
})
|
||||
} else {
|
||||
const {
|
||||
configDataFilePath,
|
||||
classification: { action: actionName }
|
||||
} = obj
|
||||
const { actions } = JSON.parse(
|
||||
fs.readFileSync(configDataFilePath, 'utf8')
|
||||
)
|
||||
const action = actions[actionName]
|
||||
const { type: actionType } = action
|
||||
const nextAction = action.next_action
|
||||
? actions[action.next_action]
|
||||
: null
|
||||
|
||||
if (actionType === 'logic') {
|
||||
/**
|
||||
* "Logic" action skill execution
|
||||
*/
|
||||
|
||||
// Ensure the process is empty (to be able to execute other processes outside of Brain)
|
||||
if (Object.keys(this.process).length === 0) {
|
||||
/**
|
||||
* Execute a skill in a standalone way (CLI):
|
||||
*
|
||||
* 1. Need to be at the root of the project
|
||||
* 2. Edit: server/src/intent-object.sample.json
|
||||
* 3. Run: PIPENV_PIPFILE=bridges/python/src/Pipfile pipenv run
|
||||
* python bridges/python/src/main.py server/src/intent-object.sample.json
|
||||
*/
|
||||
const slots = {}
|
||||
if (obj.slots) {
|
||||
Object.keys(obj.slots)?.forEach((slotName) => {
|
||||
slots[slotName] = obj.slots[slotName].value
|
||||
})
|
||||
}
|
||||
const intentObj = {
|
||||
id: utteranceId,
|
||||
lang: this._lang,
|
||||
domain: obj.classification.domain,
|
||||
skill: obj.classification.skill,
|
||||
action: obj.classification.action,
|
||||
utterance: obj.utterance,
|
||||
current_entities: obj.currentEntities,
|
||||
entities: obj.entities,
|
||||
current_resolvers: obj.currentResolvers,
|
||||
resolvers: obj.resolvers,
|
||||
slots
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(intentObjectPath, JSON.stringify(intentObj))
|
||||
this.process = spawn(
|
||||
`${PYTHON_BRIDGE_BIN_PATH} ${intentObjectPath}`,
|
||||
{ shell: true }
|
||||
)
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to save intent object: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
const domainName = obj.classification.domain
|
||||
const skillName = obj.classification.skill
|
||||
const { name: domainFriendlyName } =
|
||||
SkillDomainHelper.getSkillDomainInfo(domainName)
|
||||
const { name: skillFriendlyName } = SkillDomainHelper.getSkillInfo(
|
||||
domainName,
|
||||
skillName
|
||||
)
|
||||
let output = ''
|
||||
|
||||
// Read output
|
||||
this.process.stdout.on('data', (data) => {
|
||||
const executionTimeEnd = Date.now()
|
||||
const executionTime = executionTimeEnd - executionTimeStart
|
||||
|
||||
try {
|
||||
const obj = JSON.parse(data.toString())
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
if (obj.output.type === 'inter') {
|
||||
LogHelper.title(`${skillFriendlyName} skill`)
|
||||
LogHelper.info(data.toString())
|
||||
|
||||
this.interOutput = obj.output
|
||||
|
||||
const speech = obj.output.speech.toString()
|
||||
if (!opts.mute) {
|
||||
this.talk(speech)
|
||||
}
|
||||
speeches.push(speech)
|
||||
} else {
|
||||
output += data
|
||||
}
|
||||
} else {
|
||||
/* istanbul ignore next */
|
||||
reject({
|
||||
type: 'warning',
|
||||
obj: new Error(
|
||||
`The "${skillFriendlyName}" skill from the "${domainFriendlyName}" domain is not well configured. Check the configuration file.`
|
||||
),
|
||||
speeches,
|
||||
executionTime
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.title('Brain')
|
||||
LogHelper.debug(`process.stdout: ${String(data)}`)
|
||||
|
||||
/* istanbul ignore next */
|
||||
reject({
|
||||
type: 'error',
|
||||
obj: new Error(
|
||||
`The "${skillFriendlyName}" skill from the "${domainFriendlyName}" domain isn't returning JSON format.`
|
||||
),
|
||||
speeches,
|
||||
executionTime
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Handle error
|
||||
this.process.stderr.on('data', (data) => {
|
||||
const speech = `${this.wernicke('random_skill_errors', '', {
|
||||
'%skill_name%': skillFriendlyName,
|
||||
'%domain_name%': domainFriendlyName
|
||||
})}!`
|
||||
if (!opts.mute) {
|
||||
this.talk(speech)
|
||||
this._socket.emit('is-typing', false)
|
||||
}
|
||||
speeches.push(speech)
|
||||
|
||||
Brain.deleteIntentObjFile(intentObjectPath)
|
||||
|
||||
LogHelper.title(`${skillFriendlyName} skill`)
|
||||
LogHelper.error(data.toString())
|
||||
|
||||
const executionTimeEnd = Date.now()
|
||||
const executionTime = executionTimeEnd - executionTimeStart
|
||||
reject({
|
||||
type: 'error',
|
||||
obj: new Error(data),
|
||||
speeches,
|
||||
executionTime
|
||||
})
|
||||
})
|
||||
|
||||
// Catch the end of the skill execution
|
||||
this.process.stdout.on('end', () => {
|
||||
LogHelper.title(`${skillFriendlyName} skill`)
|
||||
LogHelper.info(output)
|
||||
|
||||
this.finalOutput = output
|
||||
|
||||
// Check if there is an output (no skill error)
|
||||
if (this.finalOutput !== '') {
|
||||
this.finalOutput = JSON.parse(this.finalOutput).output
|
||||
|
||||
let { speech } = this.finalOutput
|
||||
if (speech) {
|
||||
speech = speech.toString()
|
||||
if (!opts.mute) {
|
||||
this.talk(speech, true)
|
||||
}
|
||||
speeches.push(speech)
|
||||
|
||||
/* istanbul ignore next */
|
||||
// Synchronize the downloaded content if enabled
|
||||
if (
|
||||
this.finalOutput.type === 'end' &&
|
||||
this.finalOutput.options.synchronization &&
|
||||
this.finalOutput.options.synchronization.enabled &&
|
||||
this.finalOutput.options.synchronization.enabled === true
|
||||
) {
|
||||
const sync = new Synchronizer(
|
||||
this,
|
||||
obj.classification,
|
||||
this.finalOutput.options.synchronization
|
||||
)
|
||||
|
||||
// When the synchronization is finished
|
||||
sync.synchronize((speech) => {
|
||||
if (!opts.mute) {
|
||||
this.talk(speech)
|
||||
}
|
||||
speeches.push(speech)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Brain.deleteIntentObjFile(intentObjectPath)
|
||||
|
||||
if (!opts.mute) {
|
||||
this._socket.emit('is-typing', false)
|
||||
}
|
||||
|
||||
const executionTimeEnd = Date.now()
|
||||
const executionTime = executionTimeEnd - executionTimeStart
|
||||
|
||||
// Send suggestions to the client
|
||||
if (
|
||||
nextAction?.suggestions &&
|
||||
this.finalOutput.core?.showNextActionSuggestions
|
||||
) {
|
||||
this._socket.emit('suggest', nextAction.suggestions)
|
||||
}
|
||||
if (action?.suggestions && this.finalOutput.core?.showSuggestions) {
|
||||
this._socket.emit('suggest', action.suggestions)
|
||||
}
|
||||
|
||||
resolve({
|
||||
utteranceId,
|
||||
lang: this._lang,
|
||||
...obj,
|
||||
speeches,
|
||||
core: this.finalOutput.core,
|
||||
action,
|
||||
nextAction,
|
||||
executionTime // In ms, skill execution time only
|
||||
})
|
||||
})
|
||||
|
||||
// Reset the child process
|
||||
this.process = {}
|
||||
} else {
|
||||
/**
|
||||
* "Dialog" action skill execution
|
||||
*/
|
||||
|
||||
const configFilePath = path.join(
|
||||
process.cwd(),
|
||||
'skills',
|
||||
obj.classification.domain,
|
||||
obj.classification.skill,
|
||||
'config',
|
||||
`${this._lang}.json`
|
||||
)
|
||||
const { actions, entities } = await SkillDomainHelper.getSkillConfig(
|
||||
configFilePath,
|
||||
this._lang
|
||||
)
|
||||
const utteranceHasEntities = obj.entities.length > 0
|
||||
const { answers: rawAnswers } = obj
|
||||
let answers = rawAnswers
|
||||
let answer = ''
|
||||
|
||||
if (!utteranceHasEntities) {
|
||||
answers = answers.filter(
|
||||
({ answer }) => answer.indexOf('{{') === -1
|
||||
)
|
||||
} else {
|
||||
answers = answers.filter(
|
||||
({ answer }) => answer.indexOf('{{') !== -1
|
||||
)
|
||||
}
|
||||
|
||||
// When answers are simple without required entity
|
||||
if (answers.length === 0) {
|
||||
answer =
|
||||
rawAnswers[Math.floor(Math.random() * rawAnswers.length)]?.answer
|
||||
|
||||
// In case the expected answer requires a known entity
|
||||
if (answer.indexOf('{{') !== -1) {
|
||||
// TODO
|
||||
answers = actions[obj.classification.action]?.unknown_answers
|
||||
answer = answers[Math.floor(Math.random() * answers.length)]
|
||||
}
|
||||
} else {
|
||||
answer = answers[Math.floor(Math.random() * answers.length)]?.answer
|
||||
|
||||
/**
|
||||
* In case the utterance contains entities, and the picked up answer too,
|
||||
* then map them (utterance <-> answer)
|
||||
*/
|
||||
if (utteranceHasEntities && answer.indexOf('{{') !== -1) {
|
||||
obj.currentEntities.forEach((entityObj) => {
|
||||
answer = StringHelper.findAndMap(answer, {
|
||||
[`{{ ${entityObj.entity} }}`]: entityObj.resolution.value
|
||||
})
|
||||
|
||||
// Find matches and map deeper data from the NLU file (global entities)
|
||||
const matches = answer.match(/{{.+?}}/g)
|
||||
|
||||
matches?.forEach((match) => {
|
||||
let newStr = match.substring(3)
|
||||
|
||||
newStr = newStr.substring(0, newStr.indexOf('}}') - 1)
|
||||
|
||||
const [entity, dataKey] = newStr.split('.')
|
||||
|
||||
if (entity === entityObj.entity) {
|
||||
// e.g. entities.color.options.red.data.usage
|
||||
const valuesArr =
|
||||
entities[entity].options[entityObj.option].data[dataKey]
|
||||
|
||||
answer = StringHelper.findAndMap(answer, {
|
||||
[match]:
|
||||
valuesArr[Math.floor(Math.random() * valuesArr.length)]
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const executionTimeEnd = Date.now()
|
||||
const executionTime = executionTimeEnd - executionTimeStart
|
||||
|
||||
if (!opts.mute) {
|
||||
this.talk(answer, true)
|
||||
this._socket.emit('is-typing', false)
|
||||
}
|
||||
|
||||
// Send suggestions to the client
|
||||
if (nextAction?.suggestions) {
|
||||
this._socket.emit('suggest', nextAction.suggestions)
|
||||
}
|
||||
|
||||
resolve({
|
||||
utteranceId,
|
||||
lang: this._lang,
|
||||
...obj,
|
||||
speeches: [answer],
|
||||
core: this.finalOutput.core,
|
||||
action,
|
||||
nextAction,
|
||||
executionTime // In ms, skill execution time only
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Brain
|
||||
@@ -0,0 +1,608 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'
|
||||
|
||||
import type { ShortLanguageCode } from '@/types'
|
||||
import type { GlobalAnswersSchema } from '@/schemas/global-data-schemas'
|
||||
import type {
|
||||
CustomEnumEntity,
|
||||
NERCustomEntity,
|
||||
NLUResult
|
||||
} from '@/core/nlp/types'
|
||||
import type { SkillConfigSchema, SkillSchema } from '@/schemas/skill-schemas'
|
||||
import type {
|
||||
BrainProcessResult,
|
||||
IntentObject,
|
||||
SkillResult
|
||||
} from '@/core/brain/types'
|
||||
import {
|
||||
SkillActionTypes,
|
||||
SkillBridges,
|
||||
SkillOutputTypes
|
||||
} from '@/core/brain/types'
|
||||
import { langs } from '@@/core/langs.json'
|
||||
import {
|
||||
HAS_TTS,
|
||||
PYTHON_BRIDGE_BIN_PATH,
|
||||
NODEJS_BRIDGE_BIN_PATH,
|
||||
TMP_PATH
|
||||
} from '@/constants'
|
||||
import { SOCKET_SERVER, TTS } from '@/core'
|
||||
import { LangHelper } from '@/helpers/lang-helper'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
import Synchronizer from '@/core/synchronizer'
|
||||
|
||||
export default class Brain {
|
||||
private static instance: Brain
|
||||
private _lang: ShortLanguageCode = 'en'
|
||||
private broca: GlobalAnswersSchema = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(process.cwd(), 'core', 'data', this._lang, 'answers.json'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
private skillProcess: ChildProcessWithoutNullStreams | undefined = undefined
|
||||
private domainFriendlyName = ''
|
||||
private skillFriendlyName = ''
|
||||
private skillOutput = ''
|
||||
private speeches: string[] = []
|
||||
public isMuted = false // Close Leon mouth if true; e.g. over HTTP
|
||||
|
||||
constructor() {
|
||||
if (!Brain.instance) {
|
||||
LogHelper.title('Brain')
|
||||
LogHelper.success('New instance')
|
||||
|
||||
Brain.instance = this
|
||||
}
|
||||
}
|
||||
|
||||
public get lang(): ShortLanguageCode {
|
||||
return this._lang
|
||||
}
|
||||
|
||||
public set lang(newLang: ShortLanguageCode) {
|
||||
this._lang = newLang
|
||||
// Update broca
|
||||
this.broca = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(process.cwd(), 'core', 'data', this._lang, 'answers.json'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
if (HAS_TTS) {
|
||||
this.updateTTSLang(this._lang)
|
||||
}
|
||||
}
|
||||
|
||||
private async updateTTSLang(newLang: ShortLanguageCode): Promise<void> {
|
||||
await TTS.init(newLang)
|
||||
|
||||
LogHelper.title('Brain')
|
||||
LogHelper.info('Language has changed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete intent object file
|
||||
*/
|
||||
private static deleteIntentObjFile(intentObjectPath: string): void {
|
||||
try {
|
||||
if (fs.existsSync(intentObjectPath)) {
|
||||
fs.unlinkSync(intentObjectPath)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to delete intent object file: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make Leon talk
|
||||
*/
|
||||
public talk(rawSpeech: string, end = false): void {
|
||||
LogHelper.title('Brain')
|
||||
LogHelper.info('Talking...')
|
||||
|
||||
if (rawSpeech !== '') {
|
||||
if (HAS_TTS) {
|
||||
// Stripe HTML to a whitespace. Whitespace to let the TTS respects punctuation
|
||||
const speech = rawSpeech.replace(/<(?:.|\n)*?>/gm, ' ')
|
||||
|
||||
TTS.add(speech, end)
|
||||
}
|
||||
|
||||
SOCKET_SERVER.socket?.emit('answer', rawSpeech)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pickup speech info we need to return
|
||||
*/
|
||||
public wernicke(
|
||||
type: string,
|
||||
key?: string,
|
||||
obj?: Record<string, unknown>
|
||||
): string {
|
||||
let answerObject: Record<string, string> = {}
|
||||
let answer = ''
|
||||
|
||||
// Choose a random answer or a specific one
|
||||
let property = this.broca.answers[type]
|
||||
if (property?.constructor === [].constructor) {
|
||||
property = property as string[]
|
||||
answer = property[Math.floor(Math.random() * property.length)] as string
|
||||
} else {
|
||||
answerObject = property as Record<string, string>
|
||||
}
|
||||
|
||||
// Select a specific key
|
||||
if (key !== '' && typeof key !== 'undefined') {
|
||||
answer = answerObject[key] as string
|
||||
}
|
||||
|
||||
// Parse sentence's value(s) and replace with the given object
|
||||
if (typeof obj !== 'undefined' && Object.keys(obj).length > 0) {
|
||||
answer = StringHelper.findAndMap(answer, obj)
|
||||
}
|
||||
|
||||
return answer
|
||||
}
|
||||
|
||||
private shouldAskToRepeat(nluResult: NLUResult): boolean {
|
||||
return (
|
||||
nluResult.classification.confidence <
|
||||
langs[LangHelper.getLongCode(this._lang)].min_confidence
|
||||
)
|
||||
}
|
||||
|
||||
private handleAskToRepeat(nluResult: NLUResult): void {
|
||||
if (!this.isMuted) {
|
||||
const speech = `${this.wernicke('random_not_sure')}.`
|
||||
|
||||
this.talk(speech, true)
|
||||
SOCKET_SERVER.socket?.emit('ask-to-repeat', nluResult)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the intent object that will be passed to the skill
|
||||
*/
|
||||
private createIntentObject(
|
||||
nluResult: NLUResult,
|
||||
utteranceID: string,
|
||||
slots: IntentObject['slots']
|
||||
): IntentObject {
|
||||
return {
|
||||
id: utteranceID,
|
||||
lang: this._lang,
|
||||
domain: nluResult.classification.domain,
|
||||
skill: nluResult.classification.skill,
|
||||
action: nluResult.classification.action,
|
||||
utterance: nluResult.utterance,
|
||||
current_entities: nluResult.currentEntities,
|
||||
entities: nluResult.entities,
|
||||
current_resolvers: nluResult.currentResolvers,
|
||||
resolvers: nluResult.resolvers,
|
||||
slots
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the skill process output
|
||||
*/
|
||||
private handleLogicActionSkillProcessOutput(
|
||||
data: Buffer
|
||||
): Promise<Error | null> | void {
|
||||
try {
|
||||
const obj = JSON.parse(data.toString())
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
if (obj.output.type === SkillOutputTypes.Intermediate) {
|
||||
LogHelper.title(`${this.skillFriendlyName} skill`)
|
||||
LogHelper.info(data.toString())
|
||||
|
||||
const speech = obj.output.speech.toString()
|
||||
if (!this.isMuted) {
|
||||
this.talk(speech)
|
||||
}
|
||||
this.speeches.push(speech)
|
||||
} else {
|
||||
this.skillOutput = data.toString()
|
||||
}
|
||||
|
||||
return Promise.resolve(null)
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`The "${this.skillFriendlyName}" skill from the "${this.domainFriendlyName}" domain is not well configured. Check the configuration file.`
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.title('Brain')
|
||||
LogHelper.debug(`process.stdout: ${String(data)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speak about an error happened regarding a specific skill
|
||||
*/
|
||||
private speakSkillError(): void {
|
||||
const speech = `${this.wernicke('random_skill_errors', '', {
|
||||
'%skill_name%': this.skillFriendlyName,
|
||||
'%domain_name%': this.domainFriendlyName
|
||||
})}!`
|
||||
if (!this.isMuted) {
|
||||
this.talk(speech)
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
}
|
||||
this.speeches.push(speech)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the skill process error
|
||||
*/
|
||||
private handleLogicActionSkillProcessError(
|
||||
data: Buffer,
|
||||
intentObjectPath: string
|
||||
): Error {
|
||||
this.speakSkillError()
|
||||
|
||||
Brain.deleteIntentObjFile(intentObjectPath)
|
||||
|
||||
LogHelper.title(`${this.skillFriendlyName} skill`)
|
||||
LogHelper.error(data.toString())
|
||||
|
||||
return new Error(data.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an action logic skill in a standalone way (CLI):
|
||||
*
|
||||
* 1. Need to be at the root of the project
|
||||
* 2. Edit: server/src/intent-object.sample.json
|
||||
* 3. Run: npm run python-bridge
|
||||
*/
|
||||
private async executeLogicActionSkill(
|
||||
nluResult: NLUResult,
|
||||
skillBridge: SkillSchema['bridge'],
|
||||
utteranceID: string,
|
||||
intentObjectPath: string
|
||||
): Promise<void> {
|
||||
// Ensure the process is empty (to be able to execute other processes outside of Brain)
|
||||
if (!this.skillProcess) {
|
||||
const slots: IntentObject['slots'] = {}
|
||||
|
||||
if (nluResult.slots) {
|
||||
Object.keys(nluResult.slots)?.forEach((slotName) => {
|
||||
slots[slotName] = nluResult.slots[slotName]?.value
|
||||
})
|
||||
}
|
||||
|
||||
const intentObject = this.createIntentObject(
|
||||
nluResult,
|
||||
utteranceID,
|
||||
slots
|
||||
)
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile(
|
||||
intentObjectPath,
|
||||
JSON.stringify(intentObject)
|
||||
)
|
||||
|
||||
if (skillBridge === SkillBridges.Python) {
|
||||
this.skillProcess = spawn(
|
||||
`${PYTHON_BRIDGE_BIN_PATH} "${intentObjectPath}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
} else if (skillBridge === SkillBridges.NodeJS) {
|
||||
this.skillProcess = spawn(
|
||||
`${NODEJS_BRIDGE_BIN_PATH} "${intentObjectPath}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
} else {
|
||||
LogHelper.error(`The skill bridge is not supported: ${skillBridge}`)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to save intent object: ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Python skills
|
||||
*/
|
||||
public execute(nluResult: NLUResult): Promise<Partial<BrainProcessResult>> {
|
||||
const executionTimeStart = Date.now()
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
const utteranceID = `${Date.now()}-${StringHelper.random(4)}`
|
||||
const intentObjectPath = path.join(TMP_PATH, `${utteranceID}.json`)
|
||||
const speeches: string[] = []
|
||||
|
||||
// Reset skill output
|
||||
this.skillOutput = ''
|
||||
|
||||
// Ask to repeat if Leon is not sure about the request
|
||||
if (this.shouldAskToRepeat(nluResult)) {
|
||||
this.handleAskToRepeat(nluResult)
|
||||
|
||||
const executionTimeEnd = Date.now()
|
||||
const executionTime = executionTimeEnd - executionTimeStart
|
||||
|
||||
resolve({
|
||||
speeches,
|
||||
executionTime
|
||||
})
|
||||
} else {
|
||||
const {
|
||||
skillConfigPath,
|
||||
classification: { action: actionName }
|
||||
} = nluResult
|
||||
const { actions } = await SkillDomainHelper.getSkillConfig(
|
||||
skillConfigPath,
|
||||
this._lang
|
||||
)
|
||||
const action = actions[
|
||||
actionName
|
||||
] as SkillConfigSchema['actions'][string]
|
||||
const { type: actionType } = action
|
||||
const nextAction = action.next_action
|
||||
? actions[action.next_action]
|
||||
: null
|
||||
|
||||
if (actionType === SkillActionTypes.Logic) {
|
||||
/**
|
||||
* "Logic" action skill execution
|
||||
*/
|
||||
|
||||
const domainName = nluResult.classification.domain
|
||||
const skillName = nluResult.classification.skill
|
||||
const { name: domainFriendlyName } =
|
||||
await SkillDomainHelper.getSkillDomainInfo(domainName)
|
||||
const { name: skillFriendlyName, bridge: skillBridge } =
|
||||
await SkillDomainHelper.getSkillInfo(domainName, skillName)
|
||||
|
||||
await this.executeLogicActionSkill(
|
||||
nluResult,
|
||||
skillBridge,
|
||||
utteranceID,
|
||||
intentObjectPath
|
||||
)
|
||||
|
||||
this.domainFriendlyName = domainFriendlyName
|
||||
this.skillFriendlyName = skillFriendlyName
|
||||
|
||||
// Read skill output
|
||||
this.skillProcess?.stdout.on('data', (data: Buffer) => {
|
||||
this.handleLogicActionSkillProcessOutput(data)
|
||||
})
|
||||
|
||||
// Handle error
|
||||
this.skillProcess?.stderr.on('data', (data: Buffer) => {
|
||||
this.handleLogicActionSkillProcessError(data, intentObjectPath)
|
||||
})
|
||||
|
||||
// Catch the end of the skill execution
|
||||
this.skillProcess?.stdout.on('end', () => {
|
||||
LogHelper.title(`${this.skillFriendlyName} skill`)
|
||||
LogHelper.info(this.skillOutput)
|
||||
|
||||
let skillResult: SkillResult | undefined = undefined
|
||||
|
||||
// Check if there is an output (no skill error)
|
||||
if (this.skillOutput !== '') {
|
||||
try {
|
||||
skillResult = JSON.parse(this.skillOutput)
|
||||
|
||||
if (skillResult?.output.speech) {
|
||||
skillResult.output.speech =
|
||||
skillResult.output.speech.toString()
|
||||
if (!this.isMuted) {
|
||||
this.talk(skillResult.output.speech, true)
|
||||
}
|
||||
speeches.push(skillResult.output.speech)
|
||||
|
||||
// Synchronize the downloaded content if enabled
|
||||
if (
|
||||
skillResult.output.type === SkillOutputTypes.End &&
|
||||
skillResult.output.options['synchronization'] &&
|
||||
skillResult.output.options['synchronization'].enabled &&
|
||||
skillResult.output.options['synchronization'].enabled ===
|
||||
true
|
||||
) {
|
||||
const sync = new Synchronizer(
|
||||
this,
|
||||
nluResult.classification,
|
||||
skillResult.output.options['synchronization']
|
||||
)
|
||||
|
||||
// When the synchronization is finished
|
||||
sync.synchronize((speech: string) => {
|
||||
if (!this.isMuted) {
|
||||
this.talk(speech)
|
||||
}
|
||||
speeches.push(speech)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.title(`${this.skillFriendlyName} skill`)
|
||||
LogHelper.error(
|
||||
`There is an error on the final output: ${String(e)}`
|
||||
)
|
||||
|
||||
this.speakSkillError()
|
||||
}
|
||||
}
|
||||
|
||||
Brain.deleteIntentObjFile(intentObjectPath)
|
||||
|
||||
if (!this.isMuted) {
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
}
|
||||
|
||||
const executionTimeEnd = Date.now()
|
||||
const executionTime = executionTimeEnd - executionTimeStart
|
||||
|
||||
// Send suggestions to the client
|
||||
if (
|
||||
nextAction?.suggestions &&
|
||||
skillResult?.output.core?.showNextActionSuggestions
|
||||
) {
|
||||
SOCKET_SERVER.socket?.emit('suggest', nextAction.suggestions)
|
||||
}
|
||||
if (
|
||||
action?.suggestions &&
|
||||
skillResult?.output.core?.showSuggestions
|
||||
) {
|
||||
SOCKET_SERVER.socket?.emit('suggest', action.suggestions)
|
||||
}
|
||||
|
||||
resolve({
|
||||
utteranceID,
|
||||
lang: this._lang,
|
||||
...nluResult,
|
||||
speeches,
|
||||
core: skillResult?.output.core,
|
||||
action,
|
||||
nextAction,
|
||||
executionTime // In ms, skill execution time only
|
||||
})
|
||||
})
|
||||
|
||||
// Reset the child process
|
||||
this.skillProcess = undefined
|
||||
} else {
|
||||
/**
|
||||
* "Dialog" action skill execution
|
||||
*/
|
||||
|
||||
const configFilePath = path.join(
|
||||
process.cwd(),
|
||||
'skills',
|
||||
nluResult.classification.domain,
|
||||
nluResult.classification.skill,
|
||||
'config',
|
||||
this._lang + '.json'
|
||||
)
|
||||
const { actions, entities: skillConfigEntities } =
|
||||
await SkillDomainHelper.getSkillConfig(configFilePath, this._lang)
|
||||
const utteranceHasEntities = nluResult.entities.length > 0
|
||||
const { answers: rawAnswers } = nluResult
|
||||
let answers = rawAnswers
|
||||
let answer: string | undefined = ''
|
||||
|
||||
if (!utteranceHasEntities) {
|
||||
answers = answers.filter(
|
||||
({ answer }) => answer.indexOf('{{') === -1
|
||||
)
|
||||
} else {
|
||||
answers = answers.filter(
|
||||
({ answer }) => answer.indexOf('{{') !== -1
|
||||
)
|
||||
}
|
||||
|
||||
// When answers are simple without required entity
|
||||
if (answers.length === 0) {
|
||||
answer =
|
||||
rawAnswers[Math.floor(Math.random() * rawAnswers.length)]?.answer
|
||||
|
||||
// In case the expected answer requires a known entity
|
||||
if (answer?.indexOf('{{') !== -1) {
|
||||
// TODO
|
||||
const unknownAnswers =
|
||||
actions[nluResult.classification.action]?.unknown_answers
|
||||
|
||||
if (unknownAnswers) {
|
||||
answer =
|
||||
unknownAnswers[
|
||||
Math.floor(Math.random() * unknownAnswers.length)
|
||||
]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
answer = answers[Math.floor(Math.random() * answers.length)]?.answer
|
||||
|
||||
/**
|
||||
* In case the utterance contains entities, and the picked up answer too,
|
||||
* then map them (utterance <-> answer)
|
||||
*/
|
||||
if (utteranceHasEntities && answer?.indexOf('{{') !== -1) {
|
||||
nluResult.currentEntities.forEach((entityObj) => {
|
||||
answer = StringHelper.findAndMap(answer as string, {
|
||||
[`{{ ${entityObj.entity} }}`]: (entityObj as NERCustomEntity)
|
||||
.resolution.value
|
||||
})
|
||||
|
||||
/**
|
||||
* Find matches and map deeper data from the NLU file (global entities)
|
||||
* TODO: handle more entity types, not only enums for global entities?
|
||||
*/
|
||||
const matches = answer.match(/{{.+?}}/g)
|
||||
|
||||
matches?.forEach((match) => {
|
||||
let newStr = match.substring(3)
|
||||
|
||||
newStr = newStr.substring(0, newStr.indexOf('}}') - 1)
|
||||
|
||||
const [entity, dataKey] = newStr.split('.')
|
||||
|
||||
if (entity && dataKey && entity === entityObj.entity) {
|
||||
const { option } = entityObj as CustomEnumEntity
|
||||
|
||||
const entityOption =
|
||||
skillConfigEntities[entity]?.options[option]
|
||||
const entityOptionData = entityOption?.data
|
||||
let valuesArr: string[] = []
|
||||
|
||||
if (entityOptionData) {
|
||||
// e.g. entities.color.options.red.data.hexa[]
|
||||
valuesArr = entityOptionData[dataKey] as string[]
|
||||
}
|
||||
|
||||
if (valuesArr.length > 0) {
|
||||
answer = StringHelper.findAndMap(answer as string, {
|
||||
[match]:
|
||||
valuesArr[
|
||||
Math.floor(Math.random() * valuesArr.length)
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const executionTimeEnd = Date.now()
|
||||
const executionTime = executionTimeEnd - executionTimeStart
|
||||
|
||||
if (!this.isMuted) {
|
||||
this.talk(answer as string, true)
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
}
|
||||
|
||||
// Send suggestions to the client
|
||||
if (nextAction?.suggestions) {
|
||||
SOCKET_SERVER.socket?.emit('suggest', nextAction.suggestions)
|
||||
}
|
||||
|
||||
resolve({
|
||||
utteranceID,
|
||||
lang: this._lang,
|
||||
...nluResult,
|
||||
speeches: [answer as string],
|
||||
core: {},
|
||||
action,
|
||||
nextAction,
|
||||
executionTime // In ms, skill execution time only
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type {
|
||||
NEREntity,
|
||||
NLPAction,
|
||||
NLPDomain,
|
||||
NLPSkill,
|
||||
NLPUtterance,
|
||||
NLUResolver,
|
||||
NLUResult,
|
||||
NLUSlot,
|
||||
NLUSlots
|
||||
} from '@/core/nlp/types'
|
||||
import type { SkillConfigSchema } from '@/schemas/skill-schemas'
|
||||
import type { ShortLanguageCode } from '@/types'
|
||||
|
||||
interface SkillCoreData {
|
||||
restart?: boolean
|
||||
isInActionLoop?: boolean
|
||||
showNextActionSuggestions?: boolean
|
||||
showSuggestions?: boolean
|
||||
}
|
||||
|
||||
export interface SkillResult {
|
||||
domain: NLPDomain
|
||||
skill: NLPSkill
|
||||
action: NLPAction
|
||||
lang: ShortLanguageCode
|
||||
utterance: NLPUtterance
|
||||
entities: NEREntity[]
|
||||
slots: NLUSlots
|
||||
output: {
|
||||
type: SkillOutputTypes
|
||||
codes: string[]
|
||||
speech: string
|
||||
core: SkillCoreData | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export enum SkillBridges {
|
||||
Python = 'python',
|
||||
NodeJS = 'nodejs'
|
||||
}
|
||||
export enum SkillOutputTypes {
|
||||
Intermediate = 'inter',
|
||||
End = 'end'
|
||||
}
|
||||
export enum SkillActionTypes {
|
||||
Logic = 'logic',
|
||||
Dialog = 'dialog'
|
||||
}
|
||||
|
||||
export interface IntentObject {
|
||||
id: string
|
||||
lang: ShortLanguageCode
|
||||
domain: NLPDomain
|
||||
skill: NLPSkill
|
||||
action: NLPAction
|
||||
utterance: NLPUtterance
|
||||
current_entities: NEREntity[]
|
||||
entities: NEREntity[]
|
||||
current_resolvers: NLUResolver[]
|
||||
resolvers: NLUResolver[]
|
||||
slots: { [key: string]: NLUSlot['value'] | undefined }
|
||||
}
|
||||
|
||||
export interface BrainProcessResult extends NLUResult {
|
||||
speeches: string[]
|
||||
executionTime: number
|
||||
utteranceID?: string
|
||||
lang?: ShortLanguageCode
|
||||
core?: SkillCoreData | undefined
|
||||
action?: SkillConfigSchema['actions'][string]
|
||||
nextAction?: SkillConfigSchema['actions'][string] | null | undefined
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import archiver from 'archiver'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
|
||||
const getDownloads = async (fastify, options) => {
|
||||
fastify.get(`/api/${options.apiVersion}/downloads`, (request, reply) => {
|
||||
LogHelper.title('GET /downloads')
|
||||
|
||||
const clean = (dir, files) => {
|
||||
LogHelper.info('Cleaning skill download directory...')
|
||||
for (let i = 0; i < files.length; i += 1) {
|
||||
fs.unlinkSync(`${dir}/${files[i]}`)
|
||||
}
|
||||
fs.rmdirSync(dir)
|
||||
LogHelper.success('Downloads directory cleaned')
|
||||
}
|
||||
let message = ''
|
||||
|
||||
if (request.query.domain && request.query.skill) {
|
||||
const dlDomainDir = path.join(
|
||||
process.cwd(),
|
||||
'downloads',
|
||||
request.query.domain
|
||||
)
|
||||
const skill = path.join(dlDomainDir, `${request.query.skill}.py`)
|
||||
|
||||
LogHelper.info(
|
||||
`Checking existence of the ${StringHelper.ucFirst(
|
||||
request.query.skill
|
||||
)} skill...`
|
||||
)
|
||||
if (fs.existsSync(skill)) {
|
||||
LogHelper.success(
|
||||
`${StringHelper.ucFirst(request.query.skill)} skill exists`
|
||||
)
|
||||
const downloadsDir = `${dlDomainDir}/${request.query.skill}`
|
||||
|
||||
LogHelper.info('Reading downloads directory...')
|
||||
fs.readdir(downloadsDir, (err, files) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
message = 'There is no content to download for this skill.'
|
||||
LogHelper.error(message)
|
||||
reply.code(404).send({
|
||||
success: false,
|
||||
status: 404,
|
||||
code: 'skill_dir_not_found',
|
||||
message
|
||||
})
|
||||
} else {
|
||||
if (err) LogHelper.error(err)
|
||||
|
||||
// Download the file if there is only one
|
||||
if (files.length === 1) {
|
||||
LogHelper.info(`${files[0]} is downloading...`)
|
||||
reply.download(`${downloadsDir}/${files[0]}`)
|
||||
LogHelper.success(`${files[0]} downloaded`)
|
||||
clean(downloadsDir, files)
|
||||
} else {
|
||||
LogHelper.info('Deleting previous archives...')
|
||||
const zipSlug = `leon-${request.query.domain}-${request.query.skill}`
|
||||
const domainsFiles = fs.readdirSync(dlDomainDir)
|
||||
|
||||
for (let i = 0; i < domainsFiles.length; i += 1) {
|
||||
if (
|
||||
domainsFiles[i].indexOf('.zip') !== -1 &&
|
||||
domainsFiles[i].indexOf(zipSlug) !== -1
|
||||
) {
|
||||
fs.unlinkSync(`${dlDomainDir}/${domainsFiles[i]}`)
|
||||
LogHelper.success(`${domainsFiles[i]} archive deleted`)
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.info('Preparing new archive...')
|
||||
const zipName = `${zipSlug}-${Date.now()}.zip`
|
||||
const zipFile = `${dlDomainDir}/${zipName}`
|
||||
const output = fs.createWriteStream(zipFile)
|
||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
||||
|
||||
// When the archive is ready
|
||||
output.on('close', () => {
|
||||
LogHelper.info(`${zipName} is downloading...`)
|
||||
reply.download(zipFile, (err) => {
|
||||
if (err) LogHelper.error(err)
|
||||
|
||||
LogHelper.success(`${zipName} downloaded`)
|
||||
|
||||
clean(downloadsDir, files)
|
||||
})
|
||||
})
|
||||
archive.on('error', (err) => {
|
||||
LogHelper.error(err)
|
||||
})
|
||||
|
||||
// Add the content to the archive
|
||||
LogHelper.info('Adding content...')
|
||||
archive.directory(downloadsDir, false)
|
||||
|
||||
// Inject stream data to the archive
|
||||
LogHelper.info('Injecting stream data...')
|
||||
archive.pipe(output)
|
||||
|
||||
LogHelper.info('Finalizing...')
|
||||
archive.finalize()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
message = 'This skill does not exist.'
|
||||
LogHelper.error(message)
|
||||
reply.code(404).send({
|
||||
success: false,
|
||||
status: 404,
|
||||
code: 'skill_not_found',
|
||||
message
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message = 'Bad request.'
|
||||
LogHelper.error(message)
|
||||
reply.code(400).send({
|
||||
success: false,
|
||||
status: 400,
|
||||
code: 'bad_request',
|
||||
message
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default getDownloads
|
||||
@@ -0,0 +1,152 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import type { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import archiver from 'archiver'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import type { Static } from '@sinclair/typebox'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
|
||||
const getDownloadsSchema = {
|
||||
querystring: Type.Object({
|
||||
domain: Type.String(),
|
||||
skill: Type.String()
|
||||
})
|
||||
} satisfies FastifySchema
|
||||
|
||||
interface GetDownloadsSchema {
|
||||
querystring: Static<typeof getDownloadsSchema.querystring>
|
||||
}
|
||||
|
||||
export const getDownloads: FastifyPluginAsync<APIOptions> = async (
|
||||
fastify,
|
||||
options
|
||||
) => {
|
||||
fastify.route<{
|
||||
Querystring: GetDownloadsSchema['querystring']
|
||||
}>({
|
||||
method: 'GET',
|
||||
url: `/api/${options.apiVersion}/downloads`,
|
||||
schema: getDownloadsSchema,
|
||||
handler: async (request, reply) => {
|
||||
LogHelper.title('GET /downloads')
|
||||
|
||||
const clean = async (dir: string, files: string[]): Promise<void> => {
|
||||
LogHelper.info('Cleaning skill download directory...')
|
||||
for (let i = 0; i < files.length; i += 1) {
|
||||
await fs.promises.unlink(`${dir}/${files[i]}`)
|
||||
}
|
||||
await fs.promises.rmdir(dir)
|
||||
LogHelper.success('Downloads directory cleaned')
|
||||
}
|
||||
let message = ''
|
||||
|
||||
if (request.query.domain && request.query.skill) {
|
||||
const dlDomainDir = path.join(
|
||||
process.cwd(),
|
||||
'downloads',
|
||||
request.query.domain
|
||||
)
|
||||
const skill = path.join(dlDomainDir, `${request.query.skill}.py`)
|
||||
|
||||
LogHelper.info(
|
||||
`Checking existence of the ${StringHelper.ucFirst(
|
||||
request.query.skill
|
||||
)} skill...`
|
||||
)
|
||||
if (fs.existsSync(skill)) {
|
||||
LogHelper.success(
|
||||
`${StringHelper.ucFirst(request.query.skill)} skill exists`
|
||||
)
|
||||
const downloadsDir = `${dlDomainDir}/${request.query.skill}`
|
||||
|
||||
LogHelper.info('Reading downloads directory...')
|
||||
try {
|
||||
const files = await fs.promises.readdir(downloadsDir)
|
||||
// Download the file if there is only one
|
||||
if (files.length === 1) {
|
||||
LogHelper.info(`${files[0]} is downloading...`)
|
||||
reply.download(`${downloadsDir}/${files[0]}`)
|
||||
LogHelper.success(`${files[0]} downloaded`)
|
||||
await clean(downloadsDir, files)
|
||||
} else {
|
||||
LogHelper.info('Deleting previous archives...')
|
||||
const zipSlug = `leon-${request.query.domain}-${request.query.skill}`
|
||||
const domainsFiles = await fs.promises.readdir(dlDomainDir)
|
||||
|
||||
for (let i = 0; i < domainsFiles.length; i += 1) {
|
||||
if (
|
||||
domainsFiles[i]?.indexOf('.zip') !== -1 &&
|
||||
domainsFiles[i]?.indexOf(zipSlug) !== -1
|
||||
) {
|
||||
await fs.promises.unlink(`${dlDomainDir}/${domainsFiles[i]}`)
|
||||
LogHelper.success(`${domainsFiles[i]} archive deleted`)
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.info('Preparing new archive...')
|
||||
const zipName = `${zipSlug}-${Date.now()}.zip`
|
||||
const zipFile = `${dlDomainDir}/${zipName}`
|
||||
const output = fs.createWriteStream(zipFile)
|
||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
||||
|
||||
// When the archive is ready
|
||||
output.on('close', async () => {
|
||||
LogHelper.info(`${zipName} is downloading...`)
|
||||
reply.download(zipFile)
|
||||
LogHelper.success(`${zipName} downloaded`)
|
||||
await clean(downloadsDir, files)
|
||||
})
|
||||
archive.on('error', (err) => {
|
||||
LogHelper.error(err.message)
|
||||
})
|
||||
|
||||
// Add the content to the archive
|
||||
LogHelper.info('Adding content...')
|
||||
archive.directory(downloadsDir, false)
|
||||
|
||||
// Inject stream data to the archive
|
||||
LogHelper.info('Injecting stream data...')
|
||||
archive.pipe(output)
|
||||
|
||||
LogHelper.info('Finalizing...')
|
||||
archive.finalize()
|
||||
}
|
||||
} catch (error) {
|
||||
message = 'There is no content to download for this skill.'
|
||||
LogHelper.error(message)
|
||||
reply.code(404).send({
|
||||
success: false,
|
||||
status: 404,
|
||||
code: 'skill_dir_not_found',
|
||||
message
|
||||
})
|
||||
|
||||
LogHelper.error(message)
|
||||
}
|
||||
} else {
|
||||
message = 'This skill does not exist.'
|
||||
LogHelper.error(message)
|
||||
reply.code(404).send({
|
||||
success: false,
|
||||
status: 404,
|
||||
code: 'skill_not_found',
|
||||
message
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message = 'Bad request.'
|
||||
LogHelper.error(message)
|
||||
reply.code(400).send({
|
||||
success: false,
|
||||
status: 400,
|
||||
code: 'bad_request',
|
||||
message
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import getDownloads from '@/core/http-server/api/downloads/get'
|
||||
|
||||
const downloadsPlugin = async (fastify, options) => {
|
||||
// Get downloads to download skill content
|
||||
fastify.register(getDownloads, options)
|
||||
}
|
||||
|
||||
export default downloadsPlugin
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
import { getDownloads } from '@/core/http-server/api/downloads/get'
|
||||
|
||||
export const downloadsPlugin: FastifyPluginAsync<APIOptions> = async (
|
||||
fastify,
|
||||
options
|
||||
) => {
|
||||
// Get downloads to download skill content
|
||||
await fastify.register(getDownloads, options)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { version } from '@@/package.json'
|
||||
import {
|
||||
HAS_AFTER_SPEECH,
|
||||
HAS_LOGGER,
|
||||
HAS_STT,
|
||||
HAS_TTS,
|
||||
STT_PROVIDER,
|
||||
TTS_PROVIDER
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
const getInfo = async (fastify, options) => {
|
||||
fastify.get(`/api/${options.apiVersion}/info`, (request, reply) => {
|
||||
LogHelper.title('GET /info')
|
||||
|
||||
const message = 'Information pulled.'
|
||||
|
||||
LogHelper.success(message)
|
||||
|
||||
reply.send({
|
||||
success: true,
|
||||
status: 200,
|
||||
code: 'info_pulled',
|
||||
message,
|
||||
after_speech: HAS_AFTER_SPEECH,
|
||||
logger: HAS_LOGGER,
|
||||
stt: {
|
||||
enabled: HAS_STT,
|
||||
provider: STT_PROVIDER
|
||||
},
|
||||
tts: {
|
||||
enabled: HAS_TTS,
|
||||
provider: TTS_PROVIDER
|
||||
},
|
||||
version
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default getInfo
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import {
|
||||
LEON_VERSION,
|
||||
HAS_AFTER_SPEECH,
|
||||
HAS_STT,
|
||||
HAS_TTS,
|
||||
STT_PROVIDER,
|
||||
TTS_PROVIDER,
|
||||
IS_TELEMETRY_ENABLED
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
|
||||
export const getInfo: FastifyPluginAsync<APIOptions> = async (
|
||||
fastify,
|
||||
options
|
||||
) => {
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: `/api/${options.apiVersion}/info`,
|
||||
handler: async (_request, reply) => {
|
||||
LogHelper.title('GET /info')
|
||||
const message = 'Information pulled.'
|
||||
LogHelper.success(message)
|
||||
|
||||
reply.send({
|
||||
success: true,
|
||||
status: 200,
|
||||
code: 'info_pulled',
|
||||
message,
|
||||
after_speech: HAS_AFTER_SPEECH,
|
||||
telemetry: IS_TELEMETRY_ENABLED,
|
||||
stt: {
|
||||
enabled: HAS_STT,
|
||||
provider: STT_PROVIDER
|
||||
},
|
||||
tts: {
|
||||
enabled: HAS_TTS,
|
||||
provider: TTS_PROVIDER
|
||||
},
|
||||
version: LEON_VERSION
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import getInfo from '@/core/http-server/api/info/get'
|
||||
|
||||
const infoPlugin = async (fastify, options) => {
|
||||
// Get information to init client
|
||||
fastify.register(getInfo, options)
|
||||
}
|
||||
|
||||
export default infoPlugin
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import { getInfo } from '@/core/http-server/api/info/get'
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
|
||||
export const infoPlugin: FastifyPluginAsync<APIOptions> = async (
|
||||
fastify,
|
||||
options
|
||||
) => {
|
||||
// Get information to init client
|
||||
await fastify.register(getInfo, options)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { join } from 'node:path'
|
||||
|
||||
import Fastify, { FastifySchema } from 'fastify'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import type { Static } from '@sinclair/typebox'
|
||||
|
||||
import {
|
||||
LEON_VERSION,
|
||||
LEON_NODE_ENV,
|
||||
HAS_OVER_HTTP,
|
||||
IS_TELEMETRY_ENABLED
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { DateHelper } from '@/helpers/date-helper'
|
||||
import { corsMidd } from '@/core/http-server/plugins/cors'
|
||||
import { otherMidd } from '@/core/http-server/plugins/other'
|
||||
import { infoPlugin } from '@/core/http-server/api/info'
|
||||
import { downloadsPlugin } from '@/core/http-server/api/downloads'
|
||||
import { keyMidd } from '@/core/http-server/plugins/key'
|
||||
import { NLU, BRAIN } from '@/core'
|
||||
|
||||
const API_VERSION = 'v1'
|
||||
|
||||
export interface APIOptions {
|
||||
apiVersion: string
|
||||
}
|
||||
|
||||
const postQuerySchema = {
|
||||
body: Type.Object({
|
||||
utterance: Type.String()
|
||||
})
|
||||
} satisfies FastifySchema
|
||||
|
||||
interface PostQuerySchema {
|
||||
body: Static<typeof postQuerySchema.body>
|
||||
}
|
||||
|
||||
export default class HTTPServer {
|
||||
private static instance: HTTPServer
|
||||
|
||||
private fastify = Fastify()
|
||||
|
||||
public httpServer = this.fastify.server
|
||||
|
||||
constructor(public readonly host: string, public readonly port: number) {
|
||||
if (!HTTPServer.instance) {
|
||||
LogHelper.title('HTTP Server')
|
||||
LogHelper.success('New instance')
|
||||
|
||||
HTTPServer.instance = this
|
||||
}
|
||||
|
||||
this.host = host
|
||||
this.port = port
|
||||
}
|
||||
|
||||
/**
|
||||
* Server entry point
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
this.fastify.addHook('onRequest', corsMidd)
|
||||
this.fastify.addHook('preValidation', otherMidd)
|
||||
|
||||
LogHelper.title('Initialization')
|
||||
LogHelper.info(`The current env is ${LEON_NODE_ENV}`)
|
||||
LogHelper.info(`The current version is ${LEON_VERSION}`)
|
||||
|
||||
LogHelper.info(`The current time zone is ${DateHelper.getTimeZone()}`)
|
||||
|
||||
const isTelemetryEnabled = IS_TELEMETRY_ENABLED ? 'enabled' : 'disabled'
|
||||
LogHelper.info(`Telemetry ${isTelemetryEnabled}`)
|
||||
|
||||
await this.bootstrap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap API
|
||||
*/
|
||||
private async bootstrap(): Promise<void> {
|
||||
// Render the web app
|
||||
this.fastify.register(fastifyStatic, {
|
||||
root: join(process.cwd(), 'app', 'dist'),
|
||||
prefix: '/'
|
||||
})
|
||||
this.fastify.get('/', (_request, reply) => {
|
||||
reply.sendFile('index.html')
|
||||
})
|
||||
|
||||
this.fastify.register(infoPlugin, { apiVersion: API_VERSION })
|
||||
this.fastify.register(downloadsPlugin, { apiVersion: API_VERSION })
|
||||
|
||||
if (HAS_OVER_HTTP) {
|
||||
this.fastify.register((instance, _opts, next) => {
|
||||
instance.addHook('preHandler', keyMidd)
|
||||
|
||||
instance.route<{
|
||||
Body: PostQuerySchema['body']
|
||||
}>({
|
||||
method: 'POST',
|
||||
url: '/api/query',
|
||||
schema: postQuerySchema,
|
||||
handler: async (request, reply) => {
|
||||
const { utterance } = request.body
|
||||
|
||||
try {
|
||||
BRAIN.isMuted = true
|
||||
const data = await NLU.process(utterance)
|
||||
|
||||
reply.send({
|
||||
...data,
|
||||
success: true
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : error
|
||||
reply.statusCode = 500
|
||||
reply.send({
|
||||
message,
|
||||
success: false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: reimplement skills routes once the new core is ready
|
||||
// server.generateSkillsRoutes(instance)
|
||||
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await this.listen()
|
||||
} catch (e) {
|
||||
LogHelper.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch server
|
||||
*/
|
||||
private async listen(): Promise<void> {
|
||||
this.fastify.listen(
|
||||
{
|
||||
port: this.port,
|
||||
host: '0.0.0.0'
|
||||
},
|
||||
() => {
|
||||
LogHelper.title('Initialization')
|
||||
LogHelper.success(`Server is available at ${this.host}:${this.port}`)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// TODO: delete this file once multi clients are reimplemented
|
||||
|
||||
import { join } from 'node:path'
|
||||
|
||||
import Fastify from 'fastify'
|
||||
@@ -13,13 +17,15 @@ import {
|
||||
HAS_TTS,
|
||||
HOST,
|
||||
IS_DEVELOPMENT_ENV,
|
||||
LEON_NODE_ENV,
|
||||
PORT,
|
||||
STT_PROVIDER,
|
||||
TTS_PROVIDER
|
||||
} from '@/constants'
|
||||
import { TCP_CLIENT } from '@/core'
|
||||
import Nlu from '@/core/nlu'
|
||||
import Brain from '@/core/brain'
|
||||
import Asr from '@/core/asr'
|
||||
import Asr from '@/core/asr/asr'
|
||||
import Stt from '@/stt/stt'
|
||||
import Tts from '@/tts/tts'
|
||||
import corsMidd from '@/core/http-server/plugins/cors'
|
||||
@@ -216,10 +222,10 @@ server.handleOnConnection = (socket) => {
|
||||
const provider = await addProvider(socket.id)
|
||||
|
||||
// Check whether the TCP client is connected to the TCP server
|
||||
if (global.tcpClient.isConnected) {
|
||||
if (TCP_CLIENT.isConnected) {
|
||||
socket.emit('ready')
|
||||
} else {
|
||||
global.tcpClient.ee.on('connected', () => {
|
||||
TCP_CLIENT.ee.on('connected', () => {
|
||||
socket.emit('ready')
|
||||
})
|
||||
}
|
||||
@@ -379,7 +385,7 @@ server.init = async () => {
|
||||
server.fastify.addHook('preValidation', otherMidd)
|
||||
|
||||
LogHelper.title('Initialization')
|
||||
LogHelper.success(`The current env is ${process.env.LEON_NODE_ENV}`)
|
||||
LogHelper.success(`The current env is ${LEON_NODE_ENV}`)
|
||||
LogHelper.success(`The current version is ${version}`)
|
||||
|
||||
LogHelper.success(`The current time zone is ${DateHelper.getTimeZone()}`)
|
||||
+3
-3
@@ -1,6 +1,8 @@
|
||||
import type { onRequestHookHandler } from 'fastify'
|
||||
|
||||
import { HOST, IS_PRODUCTION_ENV } from '@/constants'
|
||||
|
||||
const corsMidd = async (request, reply) => {
|
||||
export const corsMidd: onRequestHookHandler = async (_request, reply) => {
|
||||
// Allow only a specific client to request to the API (depending on the env)
|
||||
if (!IS_PRODUCTION_ENV) {
|
||||
reply.header('Access-Control-Allow-Origin', `${HOST}:3000`)
|
||||
@@ -14,5 +16,3 @@ const corsMidd = async (request, reply) => {
|
||||
|
||||
reply.header('Access-Control-Allow-Credentials', true)
|
||||
}
|
||||
|
||||
export default corsMidd
|
||||
+3
-3
@@ -1,6 +1,8 @@
|
||||
import type { preHandlerHookHandler } from 'fastify'
|
||||
|
||||
import { HTTP_API_KEY } from '@/constants'
|
||||
|
||||
const keyMidd = async (request, reply) => {
|
||||
export const keyMidd: preHandlerHookHandler = async (request, reply) => {
|
||||
const apiKey = request.headers['x-api-key']
|
||||
if (!apiKey || apiKey !== HTTP_API_KEY) {
|
||||
reply.statusCode = 401
|
||||
@@ -10,5 +12,3 @@ const keyMidd = async (request, reply) => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default keyMidd
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import type { preValidationHookHandler } from 'fastify'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
const otherMidd = async (request, reply) => {
|
||||
export const otherMidd: preValidationHookHandler = async (request, reply) => {
|
||||
// Disable from the header, else it makes hacker's life easier to know more about our system
|
||||
reply.removeHeader('X-Powered-By')
|
||||
LogHelper.title('Requesting')
|
||||
LogHelper.info(`${request.method} ${request.url}`)
|
||||
}
|
||||
|
||||
export default otherMidd
|
||||
@@ -0,0 +1,38 @@
|
||||
import { HOST, PORT, TCP_SERVER_HOST, TCP_SERVER_PORT } from '@/constants'
|
||||
import TCPClient from '@/core/tcp-client'
|
||||
import HTTPServer from '@/core/http-server/http-server'
|
||||
import SocketServer from '@/core/socket-server'
|
||||
import SpeechToText from '@/core/stt/stt'
|
||||
import TextToSpeech from '@/core/tts/tts'
|
||||
import AutomaticSpeechRecognition from '@/core/asr/asr'
|
||||
import NamedEntityRecognition from '@/core/nlp/nlu/ner'
|
||||
import ModelLoader from '@/core/nlp/nlu/model-loader'
|
||||
import NaturalLanguageUnderstanding from '@/core/nlp/nlu/nlu'
|
||||
import Brain from '@/core/brain/brain'
|
||||
|
||||
/**
|
||||
* Register core singletons
|
||||
*/
|
||||
|
||||
export const TCP_CLIENT = new TCPClient(
|
||||
String(TCP_SERVER_HOST),
|
||||
TCP_SERVER_PORT
|
||||
)
|
||||
|
||||
export const HTTP_SERVER = new HTTPServer(String(HOST), PORT)
|
||||
|
||||
export const SOCKET_SERVER = new SocketServer()
|
||||
|
||||
export const STT = new SpeechToText()
|
||||
|
||||
export const TTS = new TextToSpeech()
|
||||
|
||||
export const ASR = new AutomaticSpeechRecognition()
|
||||
|
||||
export const NER = new NamedEntityRecognition()
|
||||
|
||||
export const MODEL_LOADER = new ModelLoader()
|
||||
|
||||
export const NLU = new NaturalLanguageUnderstanding()
|
||||
|
||||
export const BRAIN = new Brain()
|
||||
@@ -1,205 +0,0 @@
|
||||
/**
|
||||
* @nlpjs/core is dedicated to web (browsers)
|
||||
* @nlpjs/core-loader can make use of file system
|
||||
* https://github.com/axa-group/nlp.js/issues/766#issuecomment-750315909
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
|
||||
class Ner {
|
||||
constructor(ner) {
|
||||
this.ner = ner
|
||||
|
||||
LogHelper.title('NER')
|
||||
LogHelper.success('New instance')
|
||||
}
|
||||
|
||||
static logExtraction(entities) {
|
||||
LogHelper.title('NER')
|
||||
LogHelper.success('Entities found:')
|
||||
entities.forEach((ent) =>
|
||||
LogHelper.success(`{ value: ${ent.sourceText}, entity: ${ent.entity} }`)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab entities and match them with the utterance
|
||||
*/
|
||||
extractEntities(lang, utteranceSamplesFilePath, obj) {
|
||||
return new Promise(async (resolve) => {
|
||||
LogHelper.title('NER')
|
||||
LogHelper.info('Searching for entities...')
|
||||
|
||||
const { classification } = obj
|
||||
// Remove end-punctuation and add an end-whitespace
|
||||
const utterance = `${StringHelper.removeEndPunctuation(obj.utterance)} `
|
||||
const { actions } = JSON.parse(
|
||||
fs.readFileSync(utteranceSamplesFilePath, 'utf8')
|
||||
)
|
||||
const { action } = classification
|
||||
const promises = []
|
||||
const actionEntities = actions[action].entities || []
|
||||
|
||||
/**
|
||||
* Browse action entities
|
||||
* Dynamic injection of the action entities depending of the entity type
|
||||
*/
|
||||
for (let i = 0; i < actionEntities.length; i += 1) {
|
||||
const entity = actionEntities[i]
|
||||
|
||||
if (entity.type === 'regex') {
|
||||
promises.push(this.injectRegexEntity(lang, entity))
|
||||
} else if (entity.type === 'trim') {
|
||||
promises.push(this.injectTrimEntity(lang, entity))
|
||||
} else if (entity.type === 'enum') {
|
||||
promises.push(this.injectEnumEntity(lang, entity))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
const { entities } = await this.ner.process({
|
||||
locale: lang,
|
||||
text: utterance
|
||||
})
|
||||
|
||||
// Normalize entities
|
||||
entities.map((entity) => {
|
||||
// Trim whitespace at the beginning and the end of the entity value
|
||||
entity.sourceText = entity.sourceText.trim()
|
||||
entity.utteranceText = entity.utteranceText.trim()
|
||||
|
||||
// Add resolution property to stay consistent with all entities
|
||||
if (!entity.resolution) {
|
||||
entity.resolution = { value: entity.sourceText }
|
||||
}
|
||||
|
||||
return entity
|
||||
})
|
||||
|
||||
if (entities.length > 0) {
|
||||
Ner.logExtraction(entities)
|
||||
return resolve(entities)
|
||||
}
|
||||
|
||||
LogHelper.title('NER')
|
||||
LogHelper.info('No entity found')
|
||||
return resolve([])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get spaCy entities from the TCP server
|
||||
*/
|
||||
static getSpacyEntities(utterance) {
|
||||
return new Promise((resolve) => {
|
||||
const spacyEntitiesReceivedHandler = async ({ spacyEntities }) => {
|
||||
resolve(spacyEntities)
|
||||
}
|
||||
|
||||
global.tcpClient.ee.removeAllListeners()
|
||||
global.tcpClient.ee.on(
|
||||
'spacy-entities-received',
|
||||
spacyEntitiesReceivedHandler
|
||||
)
|
||||
|
||||
global.tcpClient.emit('get-spacy-entities', utterance)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject trim type entities
|
||||
*/
|
||||
injectTrimEntity(lang, entity) {
|
||||
return new Promise((resolve) => {
|
||||
for (let j = 0; j < entity.conditions.length; j += 1) {
|
||||
const condition = entity.conditions[j]
|
||||
const conditionMethod = `add${StringHelper.snakeToPascalCase(
|
||||
condition.type
|
||||
)}Condition`
|
||||
|
||||
if (condition.type === 'between') {
|
||||
/**
|
||||
* Conditions: https://github.com/axa-group/nlp.js/blob/master/docs/v3/ner-manager.md#trim-named-entities
|
||||
* e.g. list.addBetweenCondition('en', 'list', 'create a', 'list')
|
||||
*/
|
||||
this.ner[conditionMethod](
|
||||
lang,
|
||||
entity.name,
|
||||
condition.from,
|
||||
condition.to
|
||||
)
|
||||
} else if (condition.type.indexOf('after') !== -1) {
|
||||
const rule = {
|
||||
type: 'afterLast',
|
||||
words: condition.from,
|
||||
options: {}
|
||||
}
|
||||
this.ner.addRule(lang, entity.name, 'trim', rule)
|
||||
this.ner[conditionMethod](lang, entity.name, condition.from)
|
||||
} else if (condition.type.indexOf('before') !== -1) {
|
||||
this.ner[conditionMethod](lang, entity.name, condition.to)
|
||||
}
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject regex type entities
|
||||
*/
|
||||
injectRegexEntity(lang, entity) {
|
||||
return new Promise((resolve) => {
|
||||
this.ner.addRegexRule(lang, entity.name, new RegExp(entity.regex, 'g'))
|
||||
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject enum type entities
|
||||
*/
|
||||
injectEnumEntity(lang, entity) {
|
||||
return new Promise((resolve) => {
|
||||
const { name: entityName, options } = entity
|
||||
const optionKeys = Object.keys(options)
|
||||
|
||||
optionKeys.forEach((optionName) => {
|
||||
const { synonyms } = options[optionName]
|
||||
|
||||
this.ner.addRuleOptionTexts(lang, entityName, optionName, synonyms)
|
||||
})
|
||||
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Microsoft builtin entities
|
||||
* https://github.com/axa-group/nlp.js/blob/master/packages/builtin-microsoft/src/builtin-microsoft.js
|
||||
*/
|
||||
static getMicrosoftBuiltinEntities() {
|
||||
return [
|
||||
'Number',
|
||||
'Ordinal',
|
||||
'Percentage',
|
||||
'Age',
|
||||
'Currency',
|
||||
'Dimension',
|
||||
'Temperature',
|
||||
'DateTime',
|
||||
'PhoneNumber',
|
||||
'IpAddress',
|
||||
// Disable booleans to handle it ourselves
|
||||
// 'Boolean',
|
||||
'Email',
|
||||
'Hashtag',
|
||||
'URL'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default Ner
|
||||
@@ -1,60 +1,94 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import type { ShortLanguageCode } from '@/types'
|
||||
import type {
|
||||
NEREntity,
|
||||
NLPAction,
|
||||
NLPDomain,
|
||||
NLPUtterance,
|
||||
NLUSlot,
|
||||
NLUSlots
|
||||
} from '@/core/nlp/types'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
|
||||
const maxContextHistory = 5
|
||||
const defaultActiveContext = {
|
||||
interface ConversationContext {
|
||||
name: string | null
|
||||
domain: NLPDomain
|
||||
intent: string
|
||||
currentEntities: NEREntity[]
|
||||
entities: NEREntity[]
|
||||
slots: NLUSlots
|
||||
isInActionLoop: boolean
|
||||
nextAction: NLPAction | null
|
||||
originalUtterance: NLPUtterance | null
|
||||
activatedAt: number
|
||||
skillConfigPath: string
|
||||
actionName: NLPAction
|
||||
lang: ShortLanguageCode
|
||||
}
|
||||
|
||||
type ConversationPreviousContext = Record<string, ConversationContext> | null
|
||||
|
||||
const MAX_CONTEXT_HISTORY = 5
|
||||
export const DEFAULT_ACTIVE_CONTEXT = {
|
||||
name: null,
|
||||
domain: null,
|
||||
intent: null,
|
||||
domain: '',
|
||||
intent: '',
|
||||
currentEntities: [],
|
||||
entities: [],
|
||||
slots: {},
|
||||
isInActionLoop: false,
|
||||
nextAction: null,
|
||||
originalUtterance: null,
|
||||
activatedAt: 0
|
||||
activatedAt: 0,
|
||||
skillConfigPath: '',
|
||||
actionName: '',
|
||||
lang: 'en'
|
||||
}
|
||||
|
||||
class Conversation {
|
||||
export default class Conversation {
|
||||
// Identify conversations to allow more features in the future (multiple speakers, etc.)
|
||||
public id: string
|
||||
private _previousContexts: ConversationPreviousContext = {}
|
||||
private _activeContext: ConversationContext = DEFAULT_ACTIVE_CONTEXT
|
||||
|
||||
constructor(id = 'conv0') {
|
||||
// Identify conversations to allow more features in the future (multiple speakers, etc.)
|
||||
this._id = id
|
||||
this._activeContext = defaultActiveContext
|
||||
this._previousContexts = {}
|
||||
this.id = id
|
||||
|
||||
LogHelper.title('Conversation')
|
||||
LogHelper.success('New instance')
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._id
|
||||
}
|
||||
|
||||
get activeContext() {
|
||||
public get activeContext(): ConversationContext {
|
||||
return this._activeContext
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate context according to the triggered action
|
||||
*/
|
||||
set activeContext(contextObj) {
|
||||
public async setActiveContext(
|
||||
nluContext: ConversationContext
|
||||
): Promise<void> {
|
||||
const {
|
||||
slots,
|
||||
isInActionLoop,
|
||||
configDataFilePath,
|
||||
skillConfigPath,
|
||||
actionName,
|
||||
lang,
|
||||
domain,
|
||||
intent,
|
||||
entities
|
||||
} = contextObj
|
||||
} = nluContext
|
||||
const slotKeys = Object.keys(slots)
|
||||
const [skillName] = intent.split('.')
|
||||
const newContextName = `${domain}.${skillName}`
|
||||
const { actions } = JSON.parse(fs.readFileSync(configDataFilePath, 'utf8'))
|
||||
const { actions } = await SkillDomainHelper.getSkillConfig(
|
||||
skillConfigPath,
|
||||
lang
|
||||
)
|
||||
// Grab next action from the NLU data file
|
||||
const { next_action: nextAction } = actions[actionName]
|
||||
const { next_action: nextAction } = actions[actionName] as {
|
||||
next_action: string
|
||||
}
|
||||
|
||||
// If slots are required to trigger next actions, then go through the context activation
|
||||
if (slotKeys.length > 0) {
|
||||
@@ -66,6 +100,7 @@ class Conversation {
|
||||
this.pushToPreviousContextsStack()
|
||||
// Activate new context
|
||||
this._activeContext = {
|
||||
...DEFAULT_ACTIVE_CONTEXT,
|
||||
name: newContextName,
|
||||
domain,
|
||||
intent,
|
||||
@@ -74,7 +109,7 @@ class Conversation {
|
||||
slots: {},
|
||||
isInActionLoop,
|
||||
nextAction,
|
||||
originalUtterance: contextObj.originalUtterance,
|
||||
originalUtterance: nluContext.originalUtterance,
|
||||
activatedAt: Date.now()
|
||||
}
|
||||
|
||||
@@ -101,6 +136,7 @@ class Conversation {
|
||||
if (this._activeContext.name !== newContextName) {
|
||||
// Activate new context
|
||||
this._activeContext = {
|
||||
...DEFAULT_ACTIVE_CONTEXT,
|
||||
name: newContextName,
|
||||
domain,
|
||||
intent,
|
||||
@@ -109,7 +145,7 @@ class Conversation {
|
||||
slots: {},
|
||||
isInActionLoop,
|
||||
nextAction,
|
||||
originalUtterance: contextObj.originalUtterance,
|
||||
originalUtterance: nluContext.originalUtterance,
|
||||
activatedAt: Date.now()
|
||||
}
|
||||
|
||||
@@ -123,26 +159,30 @@ class Conversation {
|
||||
}
|
||||
}
|
||||
|
||||
get previousContexts() {
|
||||
public get previousContexts(): ConversationPreviousContext {
|
||||
return this._previousContexts
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there is an active context
|
||||
*/
|
||||
hasActiveContext() {
|
||||
public hasActiveContext(): boolean {
|
||||
return !!this._activeContext.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Set slots in active context
|
||||
*/
|
||||
setSlots(lang, entities, slots = this._activeContext.slots) {
|
||||
public setSlots(
|
||||
lang: ShortLanguageCode,
|
||||
entities: NEREntity[],
|
||||
slots = this._activeContext.slots
|
||||
): void {
|
||||
const slotKeys = Object.keys(slots)
|
||||
|
||||
for (let i = 0; i < slotKeys.length; i += 1) {
|
||||
const key = slotKeys[i]
|
||||
const slotObj = slots[key]
|
||||
const key = slotKeys[i] as string
|
||||
const slotObj = slots[key] as NLUSlot
|
||||
const isFirstSet = key.includes('#')
|
||||
let slotName = slotObj.name
|
||||
let slotEntity = slotObj.expectedEntity
|
||||
@@ -150,16 +190,17 @@ class Conversation {
|
||||
|
||||
// If it's the first slot setting grabbed from the model or not
|
||||
if (isFirstSet) {
|
||||
;[slotName, slotEntity] = key.split('#')
|
||||
questions = slotObj.locales[lang]
|
||||
;[slotName, slotEntity] = key.split('#') as [string, string]
|
||||
questions = slotObj.locales?.[lang] as string[]
|
||||
}
|
||||
|
||||
// Match the slot with the submitted entity and ensure the slot hasn't been filled yet
|
||||
const [foundEntity] = entities.filter(
|
||||
({ entity }) => entity === slotEntity && !slotObj.isFilled
|
||||
)
|
||||
const pickedQuestion =
|
||||
questions[Math.floor(Math.random() * questions.length)]
|
||||
const pickedQuestion = questions[
|
||||
Math.floor(Math.random() * questions.length)
|
||||
] as string
|
||||
const slot = this._activeContext.slots[slotName]
|
||||
const newSlot = {
|
||||
name: slotName,
|
||||
@@ -181,6 +222,8 @@ class Conversation {
|
||||
!slot.isFilled ||
|
||||
(slot.isFilled &&
|
||||
newSlot.isFilled &&
|
||||
'value' in slot.value.resolution &&
|
||||
'value' in newSlot.value.resolution &&
|
||||
slot.value.resolution.value !== newSlot.value.resolution.value)
|
||||
) {
|
||||
if (newSlot?.isFilled) {
|
||||
@@ -200,48 +243,54 @@ class Conversation {
|
||||
/**
|
||||
* Get the not yet filled slot if there is any
|
||||
*/
|
||||
getNotFilledSlot() {
|
||||
public getNotFilledSlot(): NLUSlot | null {
|
||||
const slotsKeys = Object.keys(this._activeContext.slots)
|
||||
const [notFilledSlotKey] = slotsKeys.filter(
|
||||
(slotKey) => !this._activeContext.slots[slotKey].isFilled
|
||||
(slotKey) => !this._activeContext.slots[slotKey]?.isFilled
|
||||
)
|
||||
|
||||
return this._activeContext.slots[notFilledSlotKey]
|
||||
if (notFilledSlotKey !== undefined) {
|
||||
return this._activeContext.slots[notFilledSlotKey] as NLUSlot
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether slots are all filled
|
||||
*/
|
||||
areSlotsAllFilled() {
|
||||
public areSlotsAllFilled(): boolean {
|
||||
return !this.getNotFilledSlot()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up active context
|
||||
*/
|
||||
cleanActiveContext() {
|
||||
public cleanActiveContext(): void {
|
||||
LogHelper.title('Conversation')
|
||||
LogHelper.info('Clean active context')
|
||||
|
||||
this.pushToPreviousContextsStack()
|
||||
this._activeContext = defaultActiveContext
|
||||
this._activeContext = DEFAULT_ACTIVE_CONTEXT
|
||||
}
|
||||
|
||||
/**
|
||||
* Push active context to the previous contexts stack
|
||||
*/
|
||||
pushToPreviousContextsStack() {
|
||||
const previousContextsKeys = Object.keys(this._previousContexts)
|
||||
private pushToPreviousContextsStack(): void {
|
||||
if (this._previousContexts) {
|
||||
const previousContextsKeys = Object.keys(this._previousContexts)
|
||||
|
||||
// Remove the oldest context from the history stack if it reaches the maximum limit
|
||||
if (previousContextsKeys.length >= maxContextHistory) {
|
||||
delete this._previousContexts[previousContextsKeys[0]]
|
||||
}
|
||||
// Remove the oldest context from the history stack if it reaches the maximum limit
|
||||
if (previousContextsKeys.length >= MAX_CONTEXT_HISTORY) {
|
||||
delete this._previousContexts[previousContextsKeys[0] as string]
|
||||
}
|
||||
|
||||
if (this._activeContext.name) {
|
||||
this._previousContexts[this._activeContext.name] = this._activeContext
|
||||
if (this._activeContext.name) {
|
||||
this._previousContexts[this._activeContext.name] = this._activeContext
|
||||
}
|
||||
} else {
|
||||
LogHelper.warning('No previous context found')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Conversation
|
||||
@@ -0,0 +1,178 @@
|
||||
import fs from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import type { NLPUtterance } from '@/core/nlp/types'
|
||||
import type { BrainProcessResult } from '@/core/brain/types'
|
||||
import { BRAIN, MODEL_LOADER, NER, NLU } from '@/core'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
import { DEFAULT_NLU_RESULT } from '@/core/nlp/nlu/nlu'
|
||||
|
||||
interface ResolveResolversResult {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export class ActionLoop {
|
||||
/**
|
||||
* Handle action loop logic before NLU processing
|
||||
*/
|
||||
public static async handle(
|
||||
utterance: NLPUtterance
|
||||
): Promise<Partial<BrainProcessResult> | null> {
|
||||
const { domain, intent } = NLU.conversation.activeContext
|
||||
const [skillName, actionName] = intent.split('.') as [string, string]
|
||||
const skillConfigPath = join(
|
||||
process.cwd(),
|
||||
'skills',
|
||||
domain,
|
||||
skillName,
|
||||
'config',
|
||||
BRAIN.lang + '.json'
|
||||
)
|
||||
NLU.nluResult = {
|
||||
...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
|
||||
slots: NLU.conversation.activeContext.slots,
|
||||
utterance,
|
||||
skillConfigPath,
|
||||
classification: {
|
||||
domain,
|
||||
skill: skillName,
|
||||
action: actionName,
|
||||
confidence: 1
|
||||
}
|
||||
}
|
||||
NLU.nluResult.entities = await NER.extractEntities(
|
||||
BRAIN.lang,
|
||||
skillConfigPath,
|
||||
NLU.nluResult
|
||||
)
|
||||
|
||||
const { actions, resolvers } = await SkillDomainHelper.getSkillConfig(
|
||||
skillConfigPath,
|
||||
BRAIN.lang
|
||||
)
|
||||
const action = actions[NLU.nluResult.classification.action]
|
||||
if (action?.loop) {
|
||||
const { name: expectedItemName, type: expectedItemType } =
|
||||
action.loop.expected_item
|
||||
let hasMatchingEntity = false
|
||||
let hasMatchingResolver = false
|
||||
|
||||
if (expectedItemType === 'entity') {
|
||||
hasMatchingEntity =
|
||||
NLU.nluResult.entities.filter(
|
||||
({ entity }) => expectedItemName === entity
|
||||
).length > 0
|
||||
} else if (expectedItemType.indexOf('resolver') !== -1) {
|
||||
const nlpObjs = {
|
||||
global_resolver: MODEL_LOADER.globalResolversNLPContainer,
|
||||
skill_resolver: MODEL_LOADER.skillsResolversNLPContainer
|
||||
}
|
||||
const result = await nlpObjs[expectedItemType].process(utterance)
|
||||
const { intent } = result
|
||||
|
||||
const resolveResolvers = async (
|
||||
resolver: string,
|
||||
intent: string
|
||||
): Promise<[ResolveResolversResult]> => {
|
||||
const resolversPath = join(
|
||||
process.cwd(),
|
||||
'core',
|
||||
'data',
|
||||
BRAIN.lang,
|
||||
'global-resolvers'
|
||||
)
|
||||
// Load the skill resolver or the global resolver
|
||||
const resolvedIntents = !intent.includes('resolver.global')
|
||||
? resolvers && resolvers[resolver]
|
||||
: JSON.parse(
|
||||
await fs.promises.readFile(
|
||||
join(resolversPath, `${resolver}.json`),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
// E.g. resolver.global.denial -> denial
|
||||
intent = intent.substring(intent.lastIndexOf('.') + 1)
|
||||
|
||||
return [
|
||||
{
|
||||
name: expectedItemName,
|
||||
value: resolvedIntents.intents[intent].value
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Resolve resolver if global resolver or skill resolver has been found
|
||||
if (
|
||||
intent &&
|
||||
(intent.includes('resolver.global') ||
|
||||
intent.includes(`resolver.${skillName}`))
|
||||
) {
|
||||
LogHelper.title('NLU')
|
||||
LogHelper.success('Resolvers resolved:')
|
||||
NLU.nluResult.resolvers = await resolveResolvers(
|
||||
expectedItemName,
|
||||
intent
|
||||
)
|
||||
NLU.nluResult.resolvers.forEach((resolver) =>
|
||||
LogHelper.success(`${intent}: ${JSON.stringify(resolver)}`)
|
||||
)
|
||||
hasMatchingResolver = NLU.nluResult.resolvers.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure expected items are in the utterance, otherwise clean context and reprocess
|
||||
if (!hasMatchingEntity && !hasMatchingResolver) {
|
||||
BRAIN.talk(`${BRAIN.wernicke('random_context_out_of_topic')}.`)
|
||||
NLU.conversation.cleanActiveContext()
|
||||
await NLU.process(utterance)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const processedData = await BRAIN.execute(NLU.nluResult)
|
||||
// Reprocess with the original utterance that triggered the context at first
|
||||
if (processedData.core?.restart === true) {
|
||||
const { originalUtterance } = NLU.conversation.activeContext
|
||||
|
||||
NLU.conversation.cleanActiveContext()
|
||||
|
||||
if (originalUtterance !== null) {
|
||||
await NLU.process(originalUtterance)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* In case there is no next action to prepare anymore
|
||||
* and there is an explicit stop of the loop from the skill
|
||||
*/
|
||||
if (
|
||||
!processedData.action?.next_action &&
|
||||
processedData.core?.isInActionLoop === false
|
||||
) {
|
||||
NLU.conversation.cleanActiveContext()
|
||||
return null
|
||||
}
|
||||
|
||||
// Break the action loop and prepare for the next action if necessary
|
||||
if (processedData.core?.isInActionLoop === false) {
|
||||
NLU.conversation.activeContext.isInActionLoop =
|
||||
!!processedData.action?.loop
|
||||
NLU.conversation.activeContext.actionName = processedData.action
|
||||
?.next_action as string
|
||||
NLU.conversation.activeContext.intent = `${processedData.classification?.skill}.${processedData.action?.next_action}`
|
||||
}
|
||||
|
||||
return processedData
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { containerBootstrap } from '@nlpjs/core-loader'
|
||||
import { Nlp } from '@nlpjs/nlp'
|
||||
import { BuiltinMicrosoft } from '@nlpjs/builtin-microsoft'
|
||||
import { LangAll } from '@nlpjs/lang-all'
|
||||
|
||||
import { MODELS_PATH } from '@/constants'
|
||||
import { NER } from '@/core'
|
||||
import { MICROSOFT_BUILT_IN_ENTITIES } from '@/core/nlp/nlu/ner'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type NLPContainer = undefined | any
|
||||
|
||||
export default class ModelLoader {
|
||||
private static instance: ModelLoader
|
||||
public mainNLPContainer: NLPContainer
|
||||
public globalResolversNLPContainer: NLPContainer
|
||||
public skillsResolversNLPContainer: NLPContainer
|
||||
|
||||
constructor() {
|
||||
if (!ModelLoader.instance) {
|
||||
LogHelper.title('Model Loader')
|
||||
LogHelper.success('New instance')
|
||||
|
||||
ModelLoader.instance = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NLP models exists
|
||||
*/
|
||||
public hasNlpModels(): boolean {
|
||||
return (
|
||||
!!this.globalResolversNLPContainer &&
|
||||
!!this.skillsResolversNLPContainer &&
|
||||
!!this.mainNLPContainer
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all NLP models at once
|
||||
*/
|
||||
public loadNLPModels(): Promise<[void, void, void]> {
|
||||
return Promise.all([
|
||||
this.loadGlobalResolversModel(
|
||||
path.join(MODELS_PATH, 'leon-global-resolvers-model.nlp')
|
||||
),
|
||||
this.loadSkillsResolversModel(
|
||||
path.join(MODELS_PATH, 'leon-skills-resolvers-model.nlp')
|
||||
),
|
||||
this.loadMainModel(path.join(MODELS_PATH, 'leon-main-model.nlp'))
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the global resolvers NLP model from the latest training
|
||||
*/
|
||||
private loadGlobalResolversModel(modelPath: string): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!fs.existsSync(modelPath)) {
|
||||
LogHelper.title('Model Loader')
|
||||
|
||||
reject(
|
||||
new Error(
|
||||
'The global resolvers NLP model does not exist, please run: npm run train'
|
||||
)
|
||||
)
|
||||
} else {
|
||||
LogHelper.title('Model Loader')
|
||||
|
||||
try {
|
||||
const container = await containerBootstrap()
|
||||
|
||||
container.use(Nlp)
|
||||
container.use(LangAll)
|
||||
|
||||
this.globalResolversNLPContainer = container.get('nlp')
|
||||
const nluManager = container.get('nlu-manager')
|
||||
nluManager.settings.spellCheck = true
|
||||
|
||||
await this.globalResolversNLPContainer.load(modelPath)
|
||||
LogHelper.success('Global resolvers NLP model loaded')
|
||||
|
||||
resolve()
|
||||
} catch (e) {
|
||||
reject(
|
||||
new Error(
|
||||
'An error occurred while loading the global resolvers NLP model'
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the skills resolvers NLP model from the latest training
|
||||
*/
|
||||
private loadSkillsResolversModel(modelPath: string): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!fs.existsSync(modelPath)) {
|
||||
LogHelper.title('Model Loader')
|
||||
|
||||
reject({
|
||||
type: 'warning',
|
||||
obj: new Error(
|
||||
'The skills resolvers NLP model does not exist, please run: npm run train'
|
||||
)
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
const container = await containerBootstrap()
|
||||
|
||||
container.use(Nlp)
|
||||
container.use(LangAll)
|
||||
|
||||
this.skillsResolversNLPContainer = container.get('nlp')
|
||||
const nluManager = container.get('nlu-manager')
|
||||
nluManager.settings.spellCheck = true
|
||||
|
||||
await this.skillsResolversNLPContainer.load(modelPath)
|
||||
LogHelper.success('Skills resolvers NLP model loaded')
|
||||
|
||||
resolve()
|
||||
} catch (e) {
|
||||
reject(
|
||||
new Error(
|
||||
'An error occurred while loading the skills resolvers NLP model'
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the main NLP model from the latest training
|
||||
*/
|
||||
private loadMainModel(modelPath: string): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!fs.existsSync(modelPath)) {
|
||||
LogHelper.title('Model Loader')
|
||||
|
||||
reject({
|
||||
type: 'warning',
|
||||
obj: new Error(
|
||||
'The main NLP model does not exist, please run: npm run train'
|
||||
)
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
const container = await containerBootstrap()
|
||||
|
||||
container.register(
|
||||
'extract-builtin-??',
|
||||
new BuiltinMicrosoft({
|
||||
builtins: MICROSOFT_BUILT_IN_ENTITIES
|
||||
}),
|
||||
true
|
||||
)
|
||||
container.use(Nlp)
|
||||
container.use(LangAll)
|
||||
|
||||
this.mainNLPContainer = container.get('nlp')
|
||||
const nluManager = container.get('nlu-manager')
|
||||
nluManager.settings.spellCheck = true
|
||||
|
||||
await this.mainNLPContainer.load(modelPath)
|
||||
LogHelper.success('Main NLP model loaded')
|
||||
|
||||
NER.manager = this.mainNLPContainer.ner
|
||||
|
||||
resolve()
|
||||
} catch (e) {
|
||||
reject(
|
||||
new Error('An error occurred while loading the main NLP model')
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import type { ShortLanguageCode } from '@/types'
|
||||
import type {
|
||||
NEREntity,
|
||||
NERSpacyEntity,
|
||||
NLPUtterance,
|
||||
NLUResult
|
||||
} from '@/core/nlp/types'
|
||||
import type {
|
||||
SkillCustomEnumEntityTypeSchema,
|
||||
SkillCustomRegexEntityTypeSchema,
|
||||
SkillCustomTrimEntityTypeSchema
|
||||
} from '@/schemas/skill-schemas'
|
||||
import { BRAIN, MODEL_LOADER, TCP_CLIENT } from '@/core'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type NERManager = undefined | any
|
||||
|
||||
// https://github.com/axa-group/nlp.js/blob/master/packages/builtin-microsoft/src/builtin-microsoft.js
|
||||
export const MICROSOFT_BUILT_IN_ENTITIES = [
|
||||
'Number',
|
||||
'Ordinal',
|
||||
'Percentage',
|
||||
'Age',
|
||||
'Currency',
|
||||
'Dimension',
|
||||
'Temperature',
|
||||
'DateTime',
|
||||
'PhoneNumber',
|
||||
'IpAddress',
|
||||
// Disable booleans to handle it ourselves
|
||||
// 'Boolean',
|
||||
'Email',
|
||||
'Hashtag',
|
||||
'URL'
|
||||
]
|
||||
|
||||
export default class NER {
|
||||
private static instance: NER
|
||||
public manager: NERManager
|
||||
|
||||
constructor() {
|
||||
if (!NER.instance) {
|
||||
LogHelper.title('NER')
|
||||
LogHelper.success('New instance')
|
||||
|
||||
NER.instance = this
|
||||
}
|
||||
}
|
||||
|
||||
private static logExtraction(entities: NEREntity[]): void {
|
||||
LogHelper.title('NER')
|
||||
LogHelper.success('Entities found:')
|
||||
|
||||
entities.forEach((entity) =>
|
||||
LogHelper.success(
|
||||
`{ value: ${entity.sourceText}, entity: ${entity.entity} }`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab entities and match them with the utterance
|
||||
*/
|
||||
public extractEntities(
|
||||
lang: ShortLanguageCode,
|
||||
skillConfigPath: string,
|
||||
nluResult: NLUResult
|
||||
): Promise<NEREntity[]> {
|
||||
return new Promise(async (resolve) => {
|
||||
LogHelper.title('NER')
|
||||
LogHelper.info('Looking for entities...')
|
||||
|
||||
const { classification } = nluResult
|
||||
// Remove end-punctuation and add an end-whitespace
|
||||
const utterance = `${StringHelper.removeEndPunctuation(
|
||||
nluResult.utterance
|
||||
)} `
|
||||
const { actions } = await SkillDomainHelper.getSkillConfig(
|
||||
skillConfigPath,
|
||||
lang
|
||||
)
|
||||
const { action } = classification
|
||||
const promises = []
|
||||
const actionEntities = actions[action]?.entities || []
|
||||
|
||||
/**
|
||||
* Browse action entities
|
||||
* Dynamic injection of the action entities depending on the entity type
|
||||
*/
|
||||
for (let i = 0; i < actionEntities.length; i += 1) {
|
||||
const entity = actionEntities[i]
|
||||
|
||||
if (entity?.type === 'regex') {
|
||||
promises.push(this.injectRegexEntity(lang, entity))
|
||||
} else if (entity?.type === 'trim') {
|
||||
promises.push(this.injectTrimEntity(lang, entity))
|
||||
} else if (entity?.type === 'enum') {
|
||||
promises.push(this.injectEnumEntity(lang, entity))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
const { entities }: { entities: NEREntity[] } =
|
||||
await this.manager.process({
|
||||
locale: lang,
|
||||
text: utterance
|
||||
})
|
||||
|
||||
// Normalize entities
|
||||
entities.map((entity) => {
|
||||
// Trim whitespace at the beginning and the end of the entity value
|
||||
entity.sourceText = entity.sourceText.trim()
|
||||
entity.utteranceText = entity.utteranceText.trim()
|
||||
|
||||
// Add resolution property to stay consistent with all entities
|
||||
if (!entity.resolution) {
|
||||
entity.resolution = { value: entity.sourceText }
|
||||
}
|
||||
|
||||
return entity
|
||||
})
|
||||
|
||||
if (entities.length > 0) {
|
||||
NER.logExtraction(entities)
|
||||
return resolve(entities)
|
||||
}
|
||||
|
||||
LogHelper.title('NER')
|
||||
LogHelper.info('No entity found')
|
||||
return resolve([])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge spaCy entities with the NER instance
|
||||
*/
|
||||
public async mergeSpacyEntities(utterance: NLPUtterance): Promise<void> {
|
||||
const spacyEntities = await this.getSpacyEntities(utterance)
|
||||
|
||||
if (spacyEntities.length > 0) {
|
||||
spacyEntities.forEach(({ entity, resolution }) => {
|
||||
const spacyEntity = {
|
||||
[entity]: {
|
||||
options: {
|
||||
[resolution.value]: [StringHelper.ucFirst(resolution.value)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MODEL_LOADER.mainNLPContainer.addEntities(spacyEntity, BRAIN.lang)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get spaCy entities from the TCP server
|
||||
*/
|
||||
private getSpacyEntities(utterance: NLPUtterance): Promise<NERSpacyEntity[]> {
|
||||
return new Promise((resolve) => {
|
||||
const spacyEntitiesReceivedHandler = async ({
|
||||
spacyEntities
|
||||
}: {
|
||||
spacyEntities: NERSpacyEntity[]
|
||||
}): Promise<void> => {
|
||||
resolve(spacyEntities)
|
||||
}
|
||||
|
||||
TCP_CLIENT.ee.removeAllListeners()
|
||||
TCP_CLIENT.ee.on('spacy-entities-received', spacyEntitiesReceivedHandler)
|
||||
|
||||
TCP_CLIENT.emit('get-spacy-entities', utterance)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject trim type entities
|
||||
*/
|
||||
private injectTrimEntity(
|
||||
lang: ShortLanguageCode,
|
||||
entityConfig: SkillCustomTrimEntityTypeSchema
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
for (let j = 0; j < entityConfig.conditions.length; j += 1) {
|
||||
const condition = entityConfig.conditions[j]
|
||||
const conditionMethod = `add${StringHelper.snakeToPascalCase(
|
||||
condition?.type || ''
|
||||
)}Condition`
|
||||
|
||||
if (condition?.type === 'between') {
|
||||
/**
|
||||
* Conditions: https://github.com/axa-group/nlp.js/blob/master/docs/v3/ner-manager.md#trim-named-entities
|
||||
* e.g. list.addBetweenCondition('en', 'list', 'create a', 'list')
|
||||
*/
|
||||
this.manager[conditionMethod](
|
||||
lang,
|
||||
entityConfig.name,
|
||||
condition?.from,
|
||||
condition?.to
|
||||
)
|
||||
} else if (condition?.type.indexOf('after') !== -1) {
|
||||
const rule = {
|
||||
type: 'afterLast',
|
||||
words: condition?.from,
|
||||
options: {}
|
||||
}
|
||||
this.manager.addRule(lang, entityConfig.name, 'trim', rule)
|
||||
this.manager[conditionMethod](
|
||||
lang,
|
||||
entityConfig.name,
|
||||
condition?.from
|
||||
)
|
||||
} else if (condition.type.indexOf('before') !== -1) {
|
||||
this.manager[conditionMethod](lang, entityConfig.name, condition.to)
|
||||
}
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject regex type entities
|
||||
*/
|
||||
private injectRegexEntity(
|
||||
lang: ShortLanguageCode,
|
||||
entityConfig: SkillCustomRegexEntityTypeSchema
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.manager.addRegexRule(
|
||||
lang,
|
||||
entityConfig.name,
|
||||
new RegExp(entityConfig.regex, 'g')
|
||||
)
|
||||
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject enum type entities
|
||||
*/
|
||||
private injectEnumEntity(
|
||||
lang: ShortLanguageCode,
|
||||
entityConfig: SkillCustomEnumEntityTypeSchema
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const { name: entityName, options } = entityConfig
|
||||
const optionKeys = Object.keys(options)
|
||||
|
||||
optionKeys.forEach((optionName) => {
|
||||
const { synonyms } = options[optionName] as { synonyms: string[] }
|
||||
|
||||
this.manager.addRuleOptionTexts(lang, entityName, optionName, synonyms)
|
||||
})
|
||||
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
import { join } from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
import kill from 'tree-kill'
|
||||
|
||||
import type { Language, ShortLanguageCode } from '@/types'
|
||||
import type {
|
||||
NLUProcessResult,
|
||||
NLPAction,
|
||||
NLPDomain,
|
||||
NLPJSProcessResult,
|
||||
NLPSkill,
|
||||
NLPUtterance,
|
||||
NLUResult
|
||||
} from '@/core/nlp/types'
|
||||
import { langs } from '@@/core/langs.json'
|
||||
import { TCP_SERVER_BIN_PATH } from '@/constants'
|
||||
import { TCP_CLIENT, BRAIN, SOCKET_SERVER, MODEL_LOADER, NER } from '@/core'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { LangHelper } from '@/helpers/lang-helper'
|
||||
import { ActionLoop } from '@/core/nlp/nlu/action-loop'
|
||||
import { SlotFilling } from '@/core/nlp/nlu/slot-filling'
|
||||
import Conversation, { DEFAULT_ACTIVE_CONTEXT } from '@/core/nlp/conversation'
|
||||
import { Telemetry } from '@/telemetry'
|
||||
|
||||
export const DEFAULT_NLU_RESULT = {
|
||||
utterance: '',
|
||||
currentEntities: [],
|
||||
entities: [],
|
||||
currentResolvers: [],
|
||||
resolvers: [],
|
||||
slots: {},
|
||||
skillConfigPath: '',
|
||||
answers: [], // For dialog action type
|
||||
classification: {
|
||||
domain: '',
|
||||
skill: '',
|
||||
action: '',
|
||||
confidence: 0
|
||||
}
|
||||
}
|
||||
|
||||
export default class NLU {
|
||||
private static instance: NLU
|
||||
public nluResult: NLUResult = DEFAULT_NLU_RESULT
|
||||
public conversation = new Conversation('conv0')
|
||||
|
||||
constructor() {
|
||||
if (!NLU.instance) {
|
||||
LogHelper.title('NLU')
|
||||
LogHelper.success('New instance')
|
||||
|
||||
NLU.instance = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new language; recreate a new TCP server with new language; and reprocess understanding
|
||||
*/
|
||||
private switchLanguage(
|
||||
utterance: NLPUtterance,
|
||||
locale: ShortLanguageCode
|
||||
): void {
|
||||
const connectedHandler = async (): Promise<void> => {
|
||||
await this.process(utterance)
|
||||
}
|
||||
|
||||
BRAIN.lang = locale
|
||||
BRAIN.talk(`${BRAIN.wernicke('random_language_switch')}.`, true)
|
||||
|
||||
// Recreate a new TCP server process and reconnect the TCP client
|
||||
kill(global.tcpServerProcess.pid as number, () => {
|
||||
global.tcpServerProcess = spawn(`${TCP_SERVER_BIN_PATH} ${locale}`, {
|
||||
shell: true
|
||||
})
|
||||
|
||||
TCP_CLIENT.connect()
|
||||
TCP_CLIENT.ee.removeListener('connected', connectedHandler)
|
||||
TCP_CLIENT.ee.on('connected', connectedHandler)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify the utterance,
|
||||
* pick-up the right classification
|
||||
* and extract entities
|
||||
*/
|
||||
public process(utterance: NLPUtterance): Promise<NLUProcessResult | null> {
|
||||
const processingTimeStart = Date.now()
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
LogHelper.title('NLU')
|
||||
LogHelper.info('Processing...')
|
||||
|
||||
if (!MODEL_LOADER.hasNlpModels()) {
|
||||
if (!BRAIN.isMuted) {
|
||||
BRAIN.talk(`${BRAIN.wernicke('random_errors')}!`)
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
}
|
||||
|
||||
const msg =
|
||||
'An NLP model is missing, please rebuild the project or if you are in dev run: npm run train'
|
||||
LogHelper.error(msg)
|
||||
return reject(msg)
|
||||
}
|
||||
|
||||
// Add spaCy entities
|
||||
await NER.mergeSpacyEntities(utterance)
|
||||
|
||||
// Pre NLU processing according to the active context if there is one
|
||||
if (this.conversation.hasActiveContext()) {
|
||||
// When the active context is in an action loop, then directly trigger the action
|
||||
if (this.conversation.activeContext.isInActionLoop) {
|
||||
return resolve(await ActionLoop.handle(utterance))
|
||||
}
|
||||
|
||||
// When the active context has slots filled
|
||||
if (Object.keys(this.conversation.activeContext.slots).length > 0) {
|
||||
try {
|
||||
return resolve(await SlotFilling.handle(utterance))
|
||||
} catch (e) {
|
||||
return reject({})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: NLPJSProcessResult =
|
||||
await MODEL_LOADER.mainNLPContainer.process(utterance)
|
||||
const { locale, answers, classifications } = result
|
||||
let { score, intent, domain } = result
|
||||
|
||||
/**
|
||||
* If a context is active, then use the appropriate classification based on score probability.
|
||||
* E.g. 1. Create my shopping list; 2. Actually delete it.
|
||||
* If there are several "delete it" across skills, Leon needs to make use of
|
||||
* the current context ({domain}.{skill}) to define the most accurate classification
|
||||
*/
|
||||
if (this.conversation.hasActiveContext()) {
|
||||
classifications.forEach(({ intent: newIntent, score: newScore }) => {
|
||||
if (newScore > 0.6) {
|
||||
const [skillName] = newIntent.split('.')
|
||||
const newDomain = MODEL_LOADER.mainNLPContainer.getIntentDomain(
|
||||
locale,
|
||||
newIntent
|
||||
)
|
||||
const contextName = `${newDomain}.${skillName}`
|
||||
if (this.conversation.activeContext.name === contextName) {
|
||||
score = newScore
|
||||
intent = newIntent
|
||||
domain = newDomain
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const [skillName, actionName] = intent.split('.')
|
||||
this.nluResult = {
|
||||
...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
|
||||
utterance,
|
||||
answers, // For dialog action type
|
||||
classification: {
|
||||
domain,
|
||||
skill: skillName || '',
|
||||
action: actionName || '',
|
||||
confidence: score
|
||||
}
|
||||
}
|
||||
|
||||
const isSupportedLanguage = LangHelper.getShortCodes().includes(locale)
|
||||
if (!isSupportedLanguage) {
|
||||
BRAIN.talk(`${BRAIN.wernicke('random_language_not_supported')}.`, true)
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
return resolve({})
|
||||
}
|
||||
|
||||
// Trigger language switching
|
||||
if (BRAIN.lang !== locale) {
|
||||
this.switchLanguage(utterance, locale)
|
||||
return resolve(null)
|
||||
}
|
||||
|
||||
if (intent === 'None') {
|
||||
const fallback = this.fallback(
|
||||
langs[LangHelper.getLongCode(locale)].fallbacks
|
||||
)
|
||||
|
||||
if (!fallback) {
|
||||
if (!BRAIN.isMuted) {
|
||||
BRAIN.talk(`${BRAIN.wernicke('random_unknown_intents')}.`, true)
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
}
|
||||
|
||||
LogHelper.title('NLU')
|
||||
const msg = 'Intent not found'
|
||||
LogHelper.warning(msg)
|
||||
|
||||
Telemetry.utterance({ utterance, lang: BRAIN.lang })
|
||||
|
||||
return resolve(null)
|
||||
}
|
||||
|
||||
this.nluResult = fallback
|
||||
}
|
||||
|
||||
LogHelper.title('NLU')
|
||||
LogHelper.success(
|
||||
`Intent found: ${this.nluResult.classification.skill}.${this.nluResult.classification.action} (domain: ${this.nluResult.classification.domain})`
|
||||
)
|
||||
|
||||
const skillConfigPath = join(
|
||||
process.cwd(),
|
||||
'skills',
|
||||
this.nluResult.classification.domain,
|
||||
this.nluResult.classification.skill,
|
||||
'config',
|
||||
BRAIN.lang + '.json'
|
||||
)
|
||||
this.nluResult.skillConfigPath = skillConfigPath
|
||||
|
||||
try {
|
||||
this.nluResult.entities = await NER.extractEntities(
|
||||
BRAIN.lang,
|
||||
skillConfigPath,
|
||||
this.nluResult
|
||||
)
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to extract entities: ${e}`)
|
||||
}
|
||||
|
||||
const shouldSlotLoop = await SlotFilling.route(intent)
|
||||
if (shouldSlotLoop) {
|
||||
return resolve({})
|
||||
}
|
||||
|
||||
// In case all slots have been filled in the first utterance
|
||||
if (
|
||||
this.conversation.hasActiveContext() &&
|
||||
Object.keys(this.conversation.activeContext.slots).length > 0
|
||||
) {
|
||||
try {
|
||||
return resolve(await SlotFilling.handle(utterance))
|
||||
} catch (e) {
|
||||
return reject({})
|
||||
}
|
||||
}
|
||||
|
||||
const newContextName = `${this.nluResult.classification.domain}.${skillName}`
|
||||
if (this.conversation.activeContext.name !== newContextName) {
|
||||
this.conversation.cleanActiveContext()
|
||||
}
|
||||
await this.conversation.setActiveContext({
|
||||
...DEFAULT_ACTIVE_CONTEXT,
|
||||
lang: BRAIN.lang,
|
||||
slots: {},
|
||||
isInActionLoop: false,
|
||||
originalUtterance: this.nluResult.utterance,
|
||||
skillConfigPath: this.nluResult.skillConfigPath,
|
||||
actionName: this.nluResult.classification.action,
|
||||
domain: this.nluResult.classification.domain,
|
||||
intent,
|
||||
entities: this.nluResult.entities
|
||||
})
|
||||
// Pass current utterance entities to the NLU result object
|
||||
this.nluResult.currentEntities =
|
||||
this.conversation.activeContext.currentEntities
|
||||
// Pass context entities to the NLU result object
|
||||
this.nluResult.entities = this.conversation.activeContext.entities
|
||||
|
||||
try {
|
||||
const processedData = await BRAIN.execute(this.nluResult)
|
||||
|
||||
// Prepare next action if there is one queuing
|
||||
if (processedData.nextAction) {
|
||||
this.conversation.cleanActiveContext()
|
||||
await this.conversation.setActiveContext({
|
||||
...DEFAULT_ACTIVE_CONTEXT,
|
||||
lang: BRAIN.lang,
|
||||
slots: {},
|
||||
isInActionLoop: !!processedData.nextAction.loop,
|
||||
originalUtterance: processedData.utterance ?? '',
|
||||
skillConfigPath: processedData.skillConfigPath || '',
|
||||
actionName: processedData.action?.next_action || '',
|
||||
domain: processedData.classification?.domain || '',
|
||||
intent: `${processedData.classification?.skill}.${processedData.action?.next_action}`,
|
||||
entities: []
|
||||
})
|
||||
}
|
||||
|
||||
const processingTimeEnd = Date.now()
|
||||
const processingTime = processingTimeEnd - processingTimeStart
|
||||
|
||||
return resolve({
|
||||
processingTime, // In ms, total time
|
||||
...processedData,
|
||||
nluProcessingTime:
|
||||
processingTime - (processedData?.executionTime || 0) // In ms, NLU processing time only
|
||||
})
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to execute action: ${e}`
|
||||
|
||||
LogHelper.error(errorMessage)
|
||||
|
||||
if (!BRAIN.isMuted) {
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
}
|
||||
|
||||
return reject(new Error(errorMessage))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pickup and compare the right fallback
|
||||
* according to the wished skill action
|
||||
*/
|
||||
private fallback(fallbacks: Language['fallbacks']): NLUResult | null {
|
||||
const words = this.nluResult.utterance.toLowerCase().split(' ')
|
||||
|
||||
if (fallbacks.length > 0) {
|
||||
LogHelper.info('Looking for fallbacks...')
|
||||
const tmpWords = []
|
||||
|
||||
for (let i = 0; i < fallbacks.length; i += 1) {
|
||||
for (let j = 0; j < fallbacks[i]!.words.length; j += 1) {
|
||||
if (words.includes(fallbacks[i]!.words[j] as string)) {
|
||||
tmpWords.push(fallbacks[i]?.words[j])
|
||||
}
|
||||
}
|
||||
|
||||
if (JSON.stringify(tmpWords) === JSON.stringify(fallbacks[i]?.words)) {
|
||||
this.nluResult.entities = []
|
||||
this.nluResult.classification.domain = fallbacks[i]
|
||||
?.domain as NLPDomain
|
||||
this.nluResult.classification.skill = fallbacks[i]?.skill as NLPSkill
|
||||
this.nluResult.classification.action = fallbacks[i]
|
||||
?.action as NLPAction
|
||||
this.nluResult.classification.confidence = 1
|
||||
|
||||
LogHelper.success('Fallback found')
|
||||
return this.nluResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { join } from 'node:path'
|
||||
|
||||
import type { NLPUtterance } from '@/core/nlp/types'
|
||||
import type { BrainProcessResult } from '@/core/brain/types'
|
||||
import { BRAIN, MODEL_LOADER, NER, NLU, SOCKET_SERVER } from '@/core'
|
||||
import { DEFAULT_NLU_RESULT } from '@/core/nlp/nlu/nlu'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
import { DEFAULT_ACTIVE_CONTEXT } from '@/core/nlp/conversation'
|
||||
|
||||
export class SlotFilling {
|
||||
/**
|
||||
* Handle slot filling
|
||||
*/
|
||||
public static async handle(
|
||||
utterance: NLPUtterance
|
||||
): Promise<Partial<BrainProcessResult> | null> {
|
||||
const processedData = await this.fillSlot(utterance)
|
||||
|
||||
/**
|
||||
* In case the slot filling has been interrupted. e.g. context change, etc.
|
||||
* Then reprocess with the new utterance
|
||||
*/
|
||||
if (!processedData) {
|
||||
await NLU.process(utterance)
|
||||
return null
|
||||
}
|
||||
|
||||
if (processedData && Object.keys(processedData).length > 0) {
|
||||
// Set new context with the next action if there is one
|
||||
if (processedData.action?.next_action) {
|
||||
await NLU.conversation.setActiveContext({
|
||||
...DEFAULT_ACTIVE_CONTEXT,
|
||||
lang: BRAIN.lang,
|
||||
slots: processedData.slots || {},
|
||||
isInActionLoop: !!processedData.nextAction?.loop,
|
||||
originalUtterance: processedData.utterance ?? null,
|
||||
skillConfigPath: processedData.skillConfigPath || '',
|
||||
actionName: processedData.action.next_action,
|
||||
domain: processedData.classification?.domain || '',
|
||||
intent: `${processedData.classification?.skill}.${processedData.action.next_action}`,
|
||||
entities: []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return processedData
|
||||
}
|
||||
|
||||
/**
|
||||
* Build NLU data result object based on slots
|
||||
* and ask for more entities if necessary
|
||||
*/
|
||||
public static async fillSlot(
|
||||
utterance: NLPUtterance
|
||||
): Promise<Partial<BrainProcessResult> | null> {
|
||||
if (!NLU.conversation.activeContext.nextAction) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { domain, intent } = NLU.conversation.activeContext
|
||||
const [skillName, actionName] = intent.split('.') as [string, string]
|
||||
const skillConfigPath = join(
|
||||
process.cwd(),
|
||||
'skills',
|
||||
domain,
|
||||
skillName,
|
||||
'config',
|
||||
BRAIN.lang + '.json'
|
||||
)
|
||||
|
||||
NLU.nluResult = {
|
||||
...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
|
||||
utterance,
|
||||
classification: {
|
||||
domain,
|
||||
skill: skillName,
|
||||
action: actionName,
|
||||
confidence: 1
|
||||
}
|
||||
}
|
||||
|
||||
const entities = await NER.extractEntities(
|
||||
BRAIN.lang,
|
||||
skillConfigPath,
|
||||
NLU.nluResult
|
||||
)
|
||||
|
||||
// Continue to loop for questions if a slot has been filled correctly
|
||||
let notFilledSlot = NLU.conversation.getNotFilledSlot()
|
||||
if (notFilledSlot && entities.length > 0) {
|
||||
const hasMatch = entities.some(
|
||||
({ entity }) => entity === notFilledSlot?.expectedEntity
|
||||
)
|
||||
|
||||
if (hasMatch) {
|
||||
NLU.conversation.setSlots(BRAIN.lang, entities)
|
||||
|
||||
notFilledSlot = NLU.conversation.getNotFilledSlot()
|
||||
if (notFilledSlot) {
|
||||
BRAIN.talk(notFilledSlot.pickedQuestion)
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!NLU.conversation.areSlotsAllFilled()) {
|
||||
BRAIN.talk(`${BRAIN.wernicke('random_context_out_of_topic')}.`)
|
||||
} else {
|
||||
NLU.nluResult = {
|
||||
...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
|
||||
// Assign slots only if there is a next action
|
||||
slots: NLU.conversation.activeContext.nextAction
|
||||
? NLU.conversation.activeContext.slots
|
||||
: {},
|
||||
utterance: NLU.conversation.activeContext.originalUtterance ?? '',
|
||||
skillConfigPath,
|
||||
classification: {
|
||||
domain,
|
||||
skill: skillName,
|
||||
action: NLU.conversation.activeContext.nextAction,
|
||||
confidence: 1
|
||||
}
|
||||
}
|
||||
|
||||
NLU.conversation.cleanActiveContext()
|
||||
|
||||
return BRAIN.execute(NLU.nluResult)
|
||||
}
|
||||
|
||||
NLU.conversation.cleanActiveContext()
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide what to do with slot filling.
|
||||
* 1. Activate context
|
||||
* 2. If the context is expecting slots, then loop over questions to slot fill
|
||||
* 3. Or go to the brain executor if all slots have been filled in one shot
|
||||
*/
|
||||
public static async route(intent: string): Promise<boolean> {
|
||||
const slots =
|
||||
await MODEL_LOADER.mainNLPContainer.slotManager.getMandatorySlots(intent)
|
||||
const hasMandatorySlots = Object.keys(slots)?.length > 0
|
||||
|
||||
if (hasMandatorySlots) {
|
||||
await NLU.conversation.setActiveContext({
|
||||
...DEFAULT_ACTIVE_CONTEXT,
|
||||
lang: BRAIN.lang,
|
||||
slots,
|
||||
isInActionLoop: false,
|
||||
originalUtterance: NLU.nluResult.utterance,
|
||||
skillConfigPath: NLU.nluResult.skillConfigPath,
|
||||
actionName: NLU.nluResult.classification.action,
|
||||
domain: NLU.nluResult.classification.domain,
|
||||
intent,
|
||||
entities: NLU.nluResult.entities
|
||||
})
|
||||
|
||||
const notFilledSlot = NLU.conversation.getNotFilledSlot()
|
||||
// Loop for questions if a slot hasn't been filled
|
||||
if (notFilledSlot) {
|
||||
const { actions } = await SkillDomainHelper.getSkillConfig(
|
||||
NLU.nluResult.skillConfigPath,
|
||||
BRAIN.lang
|
||||
)
|
||||
const [currentSlot] =
|
||||
actions[NLU.nluResult.classification.action]?.slots?.filter(
|
||||
({ name }) => name === notFilledSlot.name
|
||||
) ?? []
|
||||
|
||||
SOCKET_SERVER.socket?.emit('suggest', currentSlot?.suggestions)
|
||||
BRAIN.talk(notFilledSlot.pickedQuestion)
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import type { ShortLanguageCode } from '@/types'
|
||||
import type { BrainProcessResult } from '@/core/brain/types'
|
||||
|
||||
/**
|
||||
* NLP types
|
||||
*/
|
||||
|
||||
export type NLPDomain = string
|
||||
export type NLPSkill = string
|
||||
export type NLPAction = string
|
||||
export type NLPUtterance = string
|
||||
|
||||
export type NLUProcessResult = Partial<
|
||||
BrainProcessResult & {
|
||||
processingTime: number
|
||||
nluProcessingTime: number
|
||||
}
|
||||
>
|
||||
|
||||
export interface NLPJSProcessResult {
|
||||
locale: ShortLanguageCode
|
||||
utterance: NLPUtterance
|
||||
settings: unknown
|
||||
languageGuessed: boolean
|
||||
localeIso2: ShortLanguageCode
|
||||
language: string
|
||||
explanation: []
|
||||
classifications: {
|
||||
intent: string
|
||||
score: number
|
||||
}[]
|
||||
intent: string // E.g. "greeting.run"
|
||||
score: number
|
||||
domain: NLPDomain
|
||||
sourceEntities: unknown[]
|
||||
entities: NEREntity[]
|
||||
answers: {
|
||||
answer: string
|
||||
}[]
|
||||
answer: string | undefined
|
||||
actions: NLPAction[]
|
||||
sentiment: {
|
||||
score: number
|
||||
numWords: number
|
||||
numHits: number
|
||||
average: number
|
||||
type: string
|
||||
locale: ShortLanguageCode
|
||||
vote: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NLU types
|
||||
*/
|
||||
|
||||
export interface NLUSlot {
|
||||
name: string
|
||||
expectedEntity: string
|
||||
value: NEREntity
|
||||
isFilled: boolean
|
||||
questions: string[]
|
||||
pickedQuestion: string
|
||||
locales?: Record<string, string[]> // From NLP.js
|
||||
}
|
||||
|
||||
export interface NLUClassification {
|
||||
domain: NLPDomain
|
||||
skill: NLPSkill
|
||||
action: NLPAction
|
||||
confidence: number
|
||||
}
|
||||
|
||||
export interface NLUResolver {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface NLUResult {
|
||||
currentEntities: NEREntity[]
|
||||
entities: NEREntity[]
|
||||
currentResolvers: NLUResolver[]
|
||||
resolvers: NLUResolver[]
|
||||
slots: NLUSlots
|
||||
utterance: NLPUtterance
|
||||
skillConfigPath: string
|
||||
answers: { answer: string }[]
|
||||
classification: NLUClassification
|
||||
}
|
||||
|
||||
export type NLUSlots = Record<string, NLUSlot>
|
||||
|
||||
/**
|
||||
* NER types
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
|
||||
interface Entity {
|
||||
start: number
|
||||
end: number
|
||||
len: number
|
||||
accuracy: number
|
||||
sourceText: string
|
||||
utteranceText: string
|
||||
entity: unknown
|
||||
resolution: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in entity types
|
||||
*/
|
||||
|
||||
interface BuiltInEntity extends Entity {}
|
||||
|
||||
interface BuiltInNumberEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
strValue: string
|
||||
value: number
|
||||
subtype: string
|
||||
}
|
||||
}
|
||||
interface BuiltInIPEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
value: string
|
||||
type: string
|
||||
}
|
||||
}
|
||||
interface BuiltInHashtagEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
interface BuiltInPhoneNumberEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
value: string
|
||||
score: string
|
||||
}
|
||||
}
|
||||
interface BuiltInCurrencyEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
strValue: string
|
||||
value: number
|
||||
unit: string
|
||||
localeUnit: string
|
||||
}
|
||||
}
|
||||
interface BuiltInPercentageEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
strValue: string
|
||||
value: number
|
||||
subtype: string
|
||||
}
|
||||
}
|
||||
interface BuiltInDateEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
type: string
|
||||
timex: string
|
||||
strPastValue: string
|
||||
pastDate: Date
|
||||
strFutureValue: string
|
||||
futureDate: Date
|
||||
}
|
||||
}
|
||||
interface BuiltInTimeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
values: {
|
||||
timex: string
|
||||
type: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
interface BuiltInTimeRangeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
values: {
|
||||
timex: string
|
||||
type: string
|
||||
start: string
|
||||
end: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
interface BuiltInDateRangeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
type: string
|
||||
timex: string
|
||||
strPastStartValue: string
|
||||
pastStartDate: Date
|
||||
strPastEndValue: string
|
||||
pastEndDate: Date
|
||||
strFutureStartValue: string
|
||||
futureStartDate: Date
|
||||
strFutureEndValue: string
|
||||
futureEndDate: Date
|
||||
}
|
||||
}
|
||||
interface BuiltInDateTimeRangeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
type: string
|
||||
timex: string
|
||||
strPastStartValue: string
|
||||
pastStartDate: Date
|
||||
strPastEndValue: string
|
||||
pastEndDate: Date
|
||||
strFutureStartValue: string
|
||||
futureStartDate: Date
|
||||
strFutureEndValue: string
|
||||
futureEndDate: Date
|
||||
}
|
||||
}
|
||||
interface BuiltInDurationEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
values: {
|
||||
timex: string
|
||||
type: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
interface BuiltInDimensionEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
strValue: string
|
||||
value: number
|
||||
unit: string
|
||||
localeUnit: string
|
||||
}
|
||||
}
|
||||
interface BuiltInEmailEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
interface BuiltInOrdinalEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
strValue: string
|
||||
value: number
|
||||
subtype: string
|
||||
}
|
||||
}
|
||||
interface BuiltInAgeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
strValue: string
|
||||
value: number
|
||||
unit: string
|
||||
localeUnit: string
|
||||
}
|
||||
}
|
||||
interface BuiltInURLEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
interface BuiltInTemperatureEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
strValue: string
|
||||
value: number
|
||||
unit: string
|
||||
localeUnit: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom entity types
|
||||
*/
|
||||
|
||||
interface CustomEntity<T> extends Entity {
|
||||
type: T
|
||||
}
|
||||
|
||||
export interface CustomEnumEntity extends CustomEntity<'enum'> {
|
||||
levenshtein: number
|
||||
option: string
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
alias?: string // E.g. "location:country_0"; "location:country_1"
|
||||
}
|
||||
type GlobalEntity = CustomEnumEntity
|
||||
export interface CustomRegexEntity extends CustomEntity<'regex'> {
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
interface CustomTrimEntity extends CustomEntity<'trim'> {
|
||||
subtype:
|
||||
| 'between'
|
||||
| 'after'
|
||||
| 'afterFirst'
|
||||
| 'afterLast'
|
||||
| 'before'
|
||||
| 'beforeFirst'
|
||||
| 'beforeLast'
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* spaCy's entity types
|
||||
*/
|
||||
|
||||
interface SpacyEntity<T> extends CustomEnumEntity {
|
||||
entity: T
|
||||
}
|
||||
|
||||
interface SpacyLocationCountryEntity extends SpacyEntity<'location:country'> {}
|
||||
interface SpacyLocationCityEntity extends SpacyEntity<'location:city'> {}
|
||||
interface SpacyPersonEntity extends SpacyEntity<'person'> {}
|
||||
interface SpacyOrganizationEntity extends SpacyEntity<'organization'> {}
|
||||
|
||||
/**
|
||||
* Exported entity types
|
||||
*/
|
||||
|
||||
export type NERBuiltInEntity =
|
||||
| BuiltInNumberEntity
|
||||
| BuiltInIPEntity
|
||||
| BuiltInHashtagEntity
|
||||
| BuiltInPhoneNumberEntity
|
||||
| BuiltInCurrencyEntity
|
||||
| BuiltInPercentageEntity
|
||||
| BuiltInDateEntity
|
||||
| BuiltInTimeEntity
|
||||
| BuiltInTimeRangeEntity
|
||||
| BuiltInDateRangeEntity
|
||||
| BuiltInDateTimeRangeEntity
|
||||
| BuiltInDurationEntity
|
||||
| BuiltInDimensionEntity
|
||||
| BuiltInEmailEntity
|
||||
| BuiltInOrdinalEntity
|
||||
| BuiltInAgeEntity
|
||||
| BuiltInURLEntity
|
||||
| BuiltInTemperatureEntity
|
||||
|
||||
export type NERCustomEntity =
|
||||
| CustomEnumEntity
|
||||
| CustomRegexEntity
|
||||
| CustomTrimEntity
|
||||
|
||||
export type NERGlobalEntity = GlobalEntity
|
||||
|
||||
export type NERSpacyEntity =
|
||||
| SpacyLocationCountryEntity
|
||||
| SpacyLocationCityEntity
|
||||
| SpacyPersonEntity
|
||||
| SpacyOrganizationEntity
|
||||
|
||||
export type NEREntity =
|
||||
| NERBuiltInEntity
|
||||
| NERCustomEntity
|
||||
| NERGlobalEntity
|
||||
| NERSpacyEntity
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user