Compare commits

...

55 Commits

Author SHA1 Message Date
louistiti 9c5d2ea0c8 Merge branch 'develop'
Build / build (push) Waiting to run
Lint / lint (push) Waiting to run
Tests / tests (push) Waiting to run
2023-05-01 10:15:43 +08:00
louistiti b7a4f69565 build: release 1.0.0-beta.8 2023-05-01 10:09:29 +08:00
louistiti 7b8a324658 fix(server): catch error on start for telemetry client 2023-05-01 01:54:19 +08:00
louistiti 59cfded2bb feat(server): send Node.js bridge version to telemetry 2023-05-01 01:26:28 +08:00
louistiti 8dc8c05d7d refactor(scripts): add bin level for Node.js bridge 2023-05-01 01:13:19 +08:00
louistiti 0e4ec4fb97 refactor(scripts): exclude current Node.js bridge archive from new archive 2023-05-01 00:49:35 +08:00
louistiti bb00677568 feat(scripts): add build for Node.js bridge 2023-05-01 00:36:03 +08:00
louistiti 308b63f576 fix(scripts): tmp setup Node.js bridge removal 2023-04-30 23:49:21 +08:00
louistiti 40819e8e17 feat(bridge/nodejs): release process 2023-04-30 23:39:39 +08:00
louistiti 0fba50905f feat(bridge/nodejs): entry point build 2023-04-30 21:36:07 +08:00
louistiti 419bfe3d78 refactor(scripts): commit-msg scopes 2023-04-30 20:35:56 +08:00
louistiti 1847248f12 feat: add instanceID on check script 2023-04-30 10:45:51 +08:00
louistiti eed4a9597c refactor(server): get Node.js version from process.versions.node 2023-04-30 10:32:01 +08:00
louistiti c3452d2170 feat(server): add minimum RAM requirement on pre-check 2023-04-30 10:31:18 +08:00
louistiti 0a14567776 refactor: round up total RAM on check script 2023-04-30 10:13:41 +08:00
louistiti 71573b78d1 refactor: instance ID log on postinstall 2023-04-29 21:04:29 +08:00
louistiti 722dba875b feat: do not download binaries if the latest version is already installed 2023-04-29 20:58:10 +08:00
louistiti 593419639e refactor: naming 2023-04-27 20:24:16 +08:00
louistiti 8b2f06e305 feat(server): bootstrap Node.js bridge entry point 2023-04-27 20:16:08 +08:00
louistiti 12e4b7bccf ci: use ubuntu-20.04 for the draft-release job 2023-04-26 23:00:01 +08:00
louistiti 63a6c2d161 ci: force ubuntu-20.04 and npm install as there is no package-lock.json anymore 2023-04-26 22:49:54 +08:00
louistiti f0e898e3c0 ci: remove npm caching 2023-04-26 22:45:34 +08:00
louistiti 10bb4af4dc build: bump Python bridge to 1.0.1 2023-04-26 22:35:48 +08:00
louistiti 1af3fc2011 style: better info message on prompt false for pre release binaries 2023-04-26 22:34:47 +08:00
louistiti 781cb072a3 fix: use version.py to prompt version bump confirmation on pre-release binaries 2023-04-26 22:33:10 +08:00
louistiti 73fe21f02e Merge branch 'fix-python-bridge-requests-certs' into develop 2023-04-26 22:24:44 +08:00
louistiti 0947bd7877 fix(server): close error log file handler 2023-04-26 21:31:12 +08:00
Divlo 07d2a41ecb fix(python bridge): include requests.certs file 2023-04-23 22:32:43 +02:00
louistiti 9a66b677e0 feat(server): sanitize current OS profile session name on telemetry error 2023-04-23 22:14:06 +08:00
louistiti fe580cfb92 feat: sanitize current OS profile session name for better privacy 2023-04-23 22:12:19 +08:00
louistiti 075f4458a0 feat(server): support isGitpod telemetry 2023-04-22 23:51:08 +08:00
louistiti 094b1218e6 refactor(server): replace HAS_LOGGER by IS_TELEMETRY_ENABLED for the Telemetry service 2023-04-18 20:09:42 +08:00
louistiti b16475766a fix(server): create errors.log if it does not exist before parsing logs 2023-04-18 09:21:05 +08:00
louistiti e5fdeba50b fix(server): ensure the errors.log file exist before reporting errors 2023-04-18 09:05:35 +08:00
louistiti 12b0fe6525 Merge branch 'telemetry' into develop 2023-04-16 16:46:58 +08:00
louistiti 302e1e11e0 feat(server): switch telemetry to production mode 2023-04-16 16:46:51 +08:00
louistiti 8d5bca0d2b refactor(server): handle telemetry on NLU edge cases 2023-04-16 15:36:59 +08:00
louistiti 994cca7bf4 refactor: do not call any core singletons during setup 2023-04-15 00:32:28 +08:00
louistiti a03afa78be refactor(server): telemetry utterance less parameters 2023-04-13 22:44:28 +08:00
louistiti a1f93a6922 fix(server): await for logical skill execution 2023-04-13 22:20:06 +08:00
louistiti 2678f6f04a feat(server): add isProduction data to telemetry 2023-04-13 00:41:02 +08:00
louistiti 03acb35e84 feat(server): complete telemetry client 2023-04-13 00:11:23 +08:00
louistiti 337854ab02 feat: kick off telemetry client 2023-04-12 00:25:40 +08:00
louistiti 19c569b76a Merge branch 'develop' into telemetry 2023-04-11 22:39:50 +08:00
louistiti 5b4cd9da81 Merge branch 'feat/better-json-schemas-developer-experience' into develop 2023-04-11 20:20:07 +08:00
louistiti 40069ab5f0 feat(server): kick off telemetry 2023-04-10 22:35:37 +08:00
Divlo 8c89cbf43c Merge branch 'develop' into feat/better-json-schemas-developer-experience 2023-04-05 00:13:34 +02:00
Divlo 31e44cde37 fix(server): improve schemas by adding descriptions 2023-01-06 22:20:55 +01:00
Divlo 248dbb84f0 chore: add $schema property to all JSON files 2023-01-06 21:48:35 +01:00
Divlo 609fb676e4 feat(server): pre-check validate $schema property 2023-01-06 21:48:14 +01:00
Divlo 5d866664b2 fix(server): remove additionalProperties in schemas to allow $schema property 2023-01-06 21:18:27 +01:00
Divlo ba09ef10b1 feat: add generate:json-schemas script 2023-01-06 21:18:19 +01:00
louistiti cb2db411ce Merge branch 'develop' 2022-08-24 21:20:41 +08:00
louistiti f179e1070c Merge branch 'develop' 2022-02-07 17:06:08 +08:00
louistiti fdeff12762 Merge branch 'develop' into origin/master 2021-12-28 21:27:12 +08:00
132 changed files with 1459 additions and 456 deletions
+2 -2
View File
@@ -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
+6 -3
View File
@@ -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
@@ -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
+3 -4
View File
@@ -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
+3
View File
@@ -21,10 +21,12 @@ package-lock.json
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
@@ -33,3 +35,4 @@ skills/**/memory/*.json
core/data/models/*.nlp
package.json.backup
.python-version
schemas/**/*.json
+4
View File
@@ -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._
+9 -8
View File
@@ -56,18 +56,19 @@ Here is how LLMs may help Leon in the future:
### 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.
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.
<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>
---
## 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>
---
+2
View File
@@ -0,0 +1,2 @@
package-lock=false
save-exact=true
View File
+15
View File
@@ -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"
}
}
+5
View File
@@ -0,0 +1,5 @@
import { VERSION } from './version'
console.log('[WIP] Node.js bridge', VERSION)
// TODO
+1
View File
@@ -0,0 +1 @@
export const VERSION = '0.0.0'
+23
View File
@@ -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"]
}
+3 -1
View File
@@ -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')]
}
}
+1 -1
View File
@@ -1 +1 @@
__version__ = '1.0.0'
__version__ = '1.0.1'
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/voice-config-schemas/amazon.json",
"credentials": {
"accessKeyId": "",
"secretAccessKey": ""
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/voice-config-schemas/google-cloud.json",
"type": "service_account",
"project_id": "",
"private_key_id": "",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/voice-config-schemas/watson-stt.json",
"apikey": "",
"url": "https://stream.watsonplatform.net/speech-to-text/api"
}
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/voice-config-schemas/watson-tts.json",
"apikey": "",
"url": "https://stream.watsonplatform.net/text-to-speech/api"
}
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/global-data/global-answers.json",
"answers": {
"success": {},
"errors": {
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/global-data/global-entity.json",
"options": {
"red": {
"synonyms": ["red"],
+1
View File
@@ -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": {
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/global-data/global-answers.json",
"answers": {
"success": {},
"errors": {
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/global-data/global-entity.json",
"options": {
"rouge": {
"synonyms": ["rouge"],
+1
View File
@@ -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": {
+6 -2
View File
@@ -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",
@@ -47,9 +47,11 @@
"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",
@@ -57,6 +59,7 @@
"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",
@@ -109,6 +112,7 @@
"@types/archiver": "5.3.2",
"@types/cli-spinner": "0.2.1",
"@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",
+69 -23
View File
@@ -9,10 +9,13 @@ 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'
@@ -29,8 +32,15 @@ import { SystemHelper } from '@/helpers/system-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,
@@ -39,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,
@@ -62,13 +73,16 @@ 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 } = SystemHelper.getInformation()
@@ -76,7 +90,7 @@ BUILD_TARGETS.set('tcp-server', {
* 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 })
@@ -92,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)
}
}
/**
@@ -143,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()
})()
+67 -61
View File
@@ -11,14 +11,16 @@ 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()
@@ -31,7 +33,6 @@ dotenv.config()
try {
const nodeMinRequiredVersion = '16'
const npmMinRequiredVersion = '8'
const minimumRequiredRAM = 4
const flitePath = 'bin/flite/flite'
const coquiLanguageModelPath = 'bin/coqui/huge-vocabulary.scorer'
const amazonPath = 'core/config/voice/amazon.json'
@@ -43,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 },
@@ -116,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')
@@ -124,8 +126,8 @@ dotenv.config()
*/
LogHelper.info('Leon version')
LogHelper.success(`${version}\n`)
pastebinData.leonVersion = version
LogHelper.success(`${LEON_VERSION}\n`)
reportDataInput.leonVersion = LEON_VERSION
/**
* Environment checking
@@ -144,10 +146,10 @@ dotenv.config()
}
const totalRAMInGB = SystemHelper.getTotalRAM()
if (totalRAMInGB < minimumRequiredRAM) {
if (Math.round(totalRAMInGB) < MINIMUM_REQUIRED_RAM) {
report.can_run.v = false
LogHelper.error(
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${minimumRequiredRAM} GB of RAM`
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM`
)
} else {
LogHelper.success(`Total RAM: ${totalRAMInGB} GB`)
@@ -162,8 +164,8 @@ dotenv.config()
LogHelper.success(`${JSON.stringify(osInfo)}\n`)
}
pastebinData.environment.osDetails = osInfo
pastebinData.environment.totalRAMInGB = totalRAMInGB
reportDataInput.environment.osDetails = osInfo
reportDataInput.environment.totalRAMInGB = totalRAMInGB
;(
await Promise.all([
command('node --version', { shell: true }),
@@ -195,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
}
}
})
@@ -207,7 +209,7 @@ 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 {
@@ -224,16 +226,16 @@ dotenv.config()
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)
}
/**
@@ -241,7 +243,7 @@ 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...')
@@ -253,7 +255,7 @@ dotenv.config()
]
LogHelper.info(tcpServerCommand)
pastebinData.tcpServer.command = tcpServerCommand
reportDataInput.tcpServer.command = tcpServerCommand
if (osInfo.platform === 'darwin') {
LogHelper.info(
@@ -280,7 +282,7 @@ dotenv.config()
if (!ignoredWarnings.some((w) => newData.includes(w))) {
tcpServerOutput += newData
report.can_start_tcp_server.v = false
pastebinData.tcpServer.error = newData
reportDataInput.tcpServer.error = newData
LogHelper.error(`Cannot start the TCP server: ${newData}`)
}
})
@@ -292,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}\n`
`TCP server startup time: ${reportDataInput.tcpServer.startTime}\n`
)
/**
@@ -325,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
}
/**
@@ -354,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
}
/**
@@ -382,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
}
/**
@@ -525,7 +527,11 @@ 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')
@@ -533,11 +539,11 @@ dotenv.config()
try {
const { data } = await axios.post('https://getleon.ai/api/report', {
report: pastebinData
report: reportDataInput
})
const { data: reportData } = data
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}`)
}
+1 -1
View File
@@ -18,7 +18,7 @@ import { LogHelper } from '@/helpers/log-helper'
'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}'
'(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')
+5 -5
View File
@@ -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...')
@@ -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()
}
}
+80
View File
@@ -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')
}
@@ -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}`)
}
})()
+14 -6
View File
@@ -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)
}
+2 -1
View File
@@ -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(
+31
View File
@@ -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}`)
}
}
+159
View File
@@ -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')
}
-105
View File
@@ -1,105 +0,0 @@
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,
PYTHON_BRIDGE_DIST_PATH,
TCP_SERVER_DIST_PATH,
PYTHON_BRIDGE_BIN_NAME,
TCP_SERVER_BIN_NAME,
PYTHON_BRIDGE_VERSION,
TCP_SERVER_VERSION
} from '@/constants'
import { LogHelper } from '@/helpers/log-helper'
/**
* Set up Python binaries according to the given setup target
* 1. Delete the existing dist binaries if already exist
* 2. Download the latest Python binaries from GitHub releases
* 3. Extract the downloaded ZIP file to the dist folder
*/
const PYTHON_TARGETS = new Map()
PYTHON_TARGETS.set('python-bridge', {
name: 'Python bridge',
distPath: PYTHON_BRIDGE_DIST_PATH,
archiveName: `${PYTHON_BRIDGE_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
version: PYTHON_BRIDGE_VERSION
})
PYTHON_TARGETS.set('tcp-server', {
name: 'TCP server',
distPath: TCP_SERVER_DIST_PATH,
archiveName: `${TCP_SERVER_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
version: TCP_SERVER_VERSION
})
const setupPythonBinaries = async (key) => {
const { name, distPath, archiveName, version } = PYTHON_TARGETS.get(key)
const buildPath = path.join(distPath, BINARIES_FOLDER_NAME)
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 = path.resolve(distPath)
await extractZip(archivePath, { dir: absoluteDistPath })
LogHelper.success(`${name} extracted`)
await fs.promises.rm(archivePath, { recursive: true, force: true })
LogHelper.success(`${name} ready`)
} catch (error) {
throw new Error(`Failed to set up ${name}: ${error}`)
}
}
export default async () => {
await setupPythonBinaries('python-bridge')
await setupPythonBinaries('tcp-server')
}
+8 -4
View File
@@ -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 setupPythonBinaries from './setup-python-binaries'
import setupBinaries from './setup-binaries'
import createInstanceID from './create-instance-id'
// Do not load ".env" file because it is not created yet
@@ -20,10 +22,12 @@ import setupPythonBinaries from './setup-python-binaries'
LoaderHelper.start()
await Promise.all([setupCore(), setupSkillsConfig()])
LoaderHelper.stop()
await setupPythonBinaries()
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!')
+43 -6
View File
@@ -18,12 +18,28 @@ export const GITHUB_URL = 'https://github.com/leon-ai/leon'
* Binaries / distribution
*/
export const BINARIES_FOLDER_NAME = SystemHelper.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 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'
@@ -32,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("'")
@@ -39,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'
@@ -52,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
@@ -91,11 +118,11 @@ 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 = Number(process.env['LEON_PY_TCP_SERVER_PORT'])
export const IS_TELEMETRY_ENABLED = process.env['LEON_TELEMETRY'] === 'true'
/**
* Paths
*/
@@ -108,3 +135,13 @@ export const SERVER_PATH = path.join(
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
+46 -21
View File
@@ -9,15 +9,24 @@ import type {
NERCustomEntity,
NLUResult
} from '@/core/nlp/types'
import type { SkillConfigSchema } from '@/schemas/skill-schemas'
import type { SkillConfigSchema, SkillSchema } from '@/schemas/skill-schemas'
import type {
BrainProcessResult,
IntentObject,
SkillResult
} from '@/core/brain/types'
import { SkillActionType, SkillOutputType } 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, TMP_PATH } from '@/constants'
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'
@@ -162,11 +171,11 @@ export default class Brain {
*/
private createIntentObject(
nluResult: NLUResult,
utteranceId: string,
utteranceID: string,
slots: IntentObject['slots']
): IntentObject {
return {
id: utteranceId,
id: utteranceID,
lang: this._lang,
domain: nluResult.classification.domain,
skill: nluResult.classification.skill,
@@ -190,7 +199,7 @@ export default class Brain {
const obj = JSON.parse(data.toString())
if (typeof obj === 'object') {
if (obj.output.type === SkillOutputType.Intermediate) {
if (obj.output.type === SkillOutputTypes.Intermediate) {
LogHelper.title(`${this.skillFriendlyName} skill`)
LogHelper.info(data.toString())
@@ -258,7 +267,8 @@ export default class Brain {
*/
private async executeLogicActionSkill(
nluResult: NLUResult,
utteranceId: string,
skillBridge: SkillSchema['bridge'],
utteranceID: string,
intentObjectPath: string
): Promise<void> {
// Ensure the process is empty (to be able to execute other processes outside of Brain)
@@ -273,7 +283,7 @@ export default class Brain {
const intentObject = this.createIntentObject(
nluResult,
utteranceId,
utteranceID,
slots
)
@@ -282,10 +292,20 @@ export default class Brain {
intentObjectPath,
JSON.stringify(intentObject)
)
this.skillProcess = spawn(
`${PYTHON_BRIDGE_BIN_PATH} "${intentObjectPath}"`,
{ shell: true }
)
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}`)
}
@@ -299,8 +319,8 @@ export default class Brain {
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 utteranceID = `${Date.now()}-${StringHelper.random(4)}`
const intentObjectPath = path.join(TMP_PATH, `${utteranceID}.json`)
const speeches: string[] = []
// Reset skill output
@@ -334,20 +354,25 @@ export default class Brain {
? actions[action.next_action]
: null
if (actionType === SkillActionType.Logic) {
if (actionType === SkillActionTypes.Logic) {
/**
* "Logic" action skill execution
*/
this.executeLogicActionSkill(nluResult, utteranceId, intentObjectPath)
const domainName = nluResult.classification.domain
const skillName = nluResult.classification.skill
const { name: domainFriendlyName } =
await SkillDomainHelper.getSkillDomainInfo(domainName)
const { name: skillFriendlyName } =
const { name: skillFriendlyName, bridge: skillBridge } =
await SkillDomainHelper.getSkillInfo(domainName, skillName)
await this.executeLogicActionSkill(
nluResult,
skillBridge,
utteranceID,
intentObjectPath
)
this.domainFriendlyName = domainFriendlyName
this.skillFriendlyName = skillFriendlyName
@@ -383,7 +408,7 @@ export default class Brain {
// Synchronize the downloaded content if enabled
if (
skillResult.output.type === SkillOutputType.End &&
skillResult.output.type === SkillOutputTypes.End &&
skillResult.output.options['synchronization'] &&
skillResult.output.options['synchronization'].enabled &&
skillResult.output.options['synchronization'].enabled ===
@@ -438,7 +463,7 @@ export default class Brain {
}
resolve({
utteranceId,
utteranceID,
lang: this._lang,
...nluResult,
speeches,
@@ -567,7 +592,7 @@ export default class Brain {
}
resolve({
utteranceId,
utteranceID,
lang: this._lang,
...nluResult,
speeches: [answer as string],
+8 -4
View File
@@ -28,7 +28,7 @@ export interface SkillResult {
entities: NEREntity[]
slots: NLUSlots
output: {
type: SkillOutputType
type: SkillOutputTypes
codes: string[]
speech: string
core: SkillCoreData | undefined
@@ -37,11 +37,15 @@ export interface SkillResult {
}
}
export enum SkillOutputType {
export enum SkillBridges {
Python = 'python',
NodeJS = 'nodejs'
}
export enum SkillOutputTypes {
Intermediate = 'inter',
End = 'end'
}
export enum SkillActionType {
export enum SkillActionTypes {
Logic = 'logic',
Dialog = 'dialog'
}
@@ -63,7 +67,7 @@ export interface IntentObject {
export interface BrainProcessResult extends NLUResult {
speeches: string[]
executionTime: number
utteranceId?: string
utteranceID?: string
lang?: ShortLanguageCode
core?: SkillCoreData | undefined
action?: SkillConfigSchema['actions'][string]
+5 -5
View File
@@ -1,13 +1,13 @@
import type { FastifyPluginAsync } from 'fastify'
import { version } from '@@/package.json'
import {
LEON_VERSION,
HAS_AFTER_SPEECH,
HAS_LOGGER,
HAS_STT,
HAS_TTS,
STT_PROVIDER,
TTS_PROVIDER
TTS_PROVIDER,
IS_TELEMETRY_ENABLED
} from '@/constants'
import { LogHelper } from '@/helpers/log-helper'
import type { APIOptions } from '@/core/http-server/http-server'
@@ -30,7 +30,7 @@ export const getInfo: FastifyPluginAsync<APIOptions> = async (
code: 'info_pulled',
message,
after_speech: HAS_AFTER_SPEECH,
logger: HAS_LOGGER,
telemetry: IS_TELEMETRY_ENABLED,
stt: {
enabled: HAS_STT,
provider: STT_PROVIDER
@@ -39,7 +39,7 @@ export const getInfo: FastifyPluginAsync<APIOptions> = async (
enabled: HAS_TTS,
provider: TTS_PROVIDER
},
version
version: LEON_VERSION
})
}
})
+9 -5
View File
@@ -5,8 +5,12 @@ import fastifyStatic from '@fastify/static'
import { Type } from '@sinclair/typebox'
import type { Static } from '@sinclair/typebox'
import { version } from '@@/package.json'
import { LEON_NODE_ENV, HAS_LOGGER, HAS_OVER_HTTP } from '@/constants'
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'
@@ -60,12 +64,12 @@ export default class HTTPServer {
LogHelper.title('Initialization')
LogHelper.info(`The current env is ${LEON_NODE_ENV}`)
LogHelper.info(`The current version is ${version}`)
LogHelper.info(`The current version is ${LEON_VERSION}`)
LogHelper.info(`The current time zone is ${DateHelper.getTimeZone()}`)
const sLogger = !HAS_LOGGER ? 'disabled' : 'enabled'
LogHelper.info(`Collaborative logger ${sLogger}`)
const isTelemetryEnabled = IS_TELEMETRY_ENABLED ? 'enabled' : 'disabled'
LogHelper.info(`Telemetry ${isTelemetryEnabled}`)
await this.bootstrap()
}
+5 -11
View File
@@ -5,6 +5,7 @@ import kill from 'tree-kill'
import type { Language, ShortLanguageCode } from '@/types'
import type {
NLUProcessResult,
NLPAction,
NLPDomain,
NLPJSProcessResult,
@@ -12,7 +13,6 @@ import type {
NLPUtterance,
NLUResult
} from '@/core/nlp/types'
import type { BrainProcessResult } from '@/core/brain/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'
@@ -21,13 +21,7 @@ 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'
type NLUProcessResult = Promise<Partial<
BrainProcessResult & {
processingTime: number
nluProcessingTime: number
}
> | null>
import { Telemetry } from '@/telemetry'
export const DEFAULT_NLU_RESULT = {
utterance: '',
@@ -91,7 +85,7 @@ export default class NLU {
* pick-up the right classification
* and extract entities
*/
public process(utterance: NLPUtterance): NLUProcessResult {
public process(utterance: NLPUtterance): Promise<NLUProcessResult | null> {
const processingTimeStart = Date.now()
return new Promise(async (resolve, reject) => {
@@ -185,8 +179,6 @@ export default class NLU {
return resolve(null)
}
// this.sendLog()
if (intent === 'None') {
const fallback = this.fallback(
langs[LangHelper.getLongCode(locale)].fallbacks
@@ -202,6 +194,8 @@ export default class NLU {
const msg = 'Intent not found'
LogHelper.warning(msg)
Telemetry.utterance({ utterance, lang: BRAIN.lang })
return resolve(null)
}
+8
View File
@@ -1,4 +1,5 @@
import type { ShortLanguageCode } from '@/types'
import type { BrainProcessResult } from '@/core/brain/types'
/**
* NLP types
@@ -9,6 +10,13 @@ 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
+6 -1
View File
@@ -14,6 +14,7 @@ import {
} from '@/core'
import { LogHelper } from '@/helpers/log-helper'
import { LangHelper } from '@/helpers/lang-helper'
import { Telemetry } from '@/telemetry'
interface HotwordDataEvent {
hotword: string
@@ -115,7 +116,11 @@ export default class SocketServer {
LogHelper.time('Utterance processed in')
BRAIN.isMuted = false
await NLU.process(utterance)
const processedData = await NLU.process(utterance)
if (processedData) {
Telemetry.utterance(processedData)
}
LogHelper.title('Execution Time')
LogHelper.timeEnd('Utterance processed in')
+31 -4
View File
@@ -4,7 +4,7 @@ import path from 'node:path'
import { DateHelper } from '@/helpers/date-helper'
export class LogHelper {
static readonly ERRORS_PATH = path.join(
static readonly ERRORS_FILE_PATH = path.join(
__dirname,
'..',
'..',
@@ -47,10 +47,10 @@ export class LogHelper {
public static error(value: string): void {
const data = `${DateHelper.getDateTime()} - ${value}`
if (fs.existsSync(LogHelper.ERRORS_PATH)) {
fs.appendFileSync(LogHelper.ERRORS_PATH, `\n${data}`)
if (fs.existsSync(this.ERRORS_FILE_PATH)) {
fs.appendFileSync(this.ERRORS_FILE_PATH, `\n${data}`)
} else {
fs.writeFileSync(LogHelper.ERRORS_PATH, data, { flag: 'wx' })
fs.writeFileSync(this.ERRORS_FILE_PATH, data, { flag: 'wx' })
}
console.error('\x1b[31m🚨 %s\x1b[0m', value)
@@ -83,4 +83,31 @@ export class LogHelper {
public static timeEnd(value: string): void {
console.timeEnd(`🕑 \x1b[36m${value}\x1b[0m`)
}
/**
* Parse error logs and return an array of log errors
* @example parseErrorLogs() // 'Failed to connect to the TCP server: Error: read ECONNRESET'
*/
public static async parseErrorLogs(): Promise<string[]> {
if (!fs.existsSync(LogHelper.ERRORS_FILE_PATH)) {
const fileHandle = await fs.promises.open(LogHelper.ERRORS_FILE_PATH, 'w')
await fileHandle.close()
}
const errorFileContent = await fs.promises.readFile(
LogHelper.ERRORS_FILE_PATH,
'utf8'
)
const errorLogs = errorFileContent
.trim()
.split(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2} - /)
// Remove the first empty string if there's one
if (errorLogs[0] === '') {
errorLogs.shift()
}
return errorLogs
}
}
+29
View File
@@ -118,4 +118,33 @@ export class SystemHelper {
public static getTotalRAM(): number {
return Number((os.totalmem() / (1_024 * 1_024 * 1_024)).toFixed(2))
}
/**
* Get the Node.js version of the current process
* @example getNodeJSVersion() // '18.15.0'
*/
public static getNodeJSVersion(): string {
return process.versions.node || '0.0.0'
}
/**
* Get the npm version used to run the current process
* @example getNPMVersion() // '9.5.0'
*/
public static getNPMVersion(): string {
return (
process.env['npm_config_user_agent']?.split('/')[1]?.split(' ')[0] ||
'0.0.0'
)
}
/**
* Replace all current session profile name occurrences with {username} placeholder
* @example sanitizeUsername('/home/louis') // '/home/{username}'
*/
public static sanitizeUsername(str: string): string {
const { username } = os.userInfo()
return str.replace(new RegExp(username, 'g'), '{username}')
}
}
+39
View File
@@ -1,12 +1,16 @@
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import {
IS_DEVELOPMENT_ENV,
IS_TELEMETRY_ENABLED,
LANG as LEON_LANG,
TCP_SERVER_BIN_PATH
} from '@/constants'
import { TCP_CLIENT, HTTP_SERVER, SOCKET_SERVER } from '@/core'
import { Telemetry } from '@/telemetry'
import { LangHelper } from '@/helpers/lang-helper'
import { LogHelper } from '@/helpers/log-helper'
;(async (): Promise<void> => {
process.title = 'leon'
@@ -31,4 +35,39 @@ import { LangHelper } from '@/helpers/lang-helper'
// Start the socket server
SOCKET_SERVER.init()
// Telemetry events
if (IS_TELEMETRY_ENABLED) {
Telemetry.start()
// Watch for errors in the error log file and report them to the telemetry service
fs.watchFile(LogHelper.ERRORS_FILE_PATH, async () => {
const logErrors = await LogHelper.parseErrorLogs()
const lastError = logErrors[logErrors.length - 1] || ''
Telemetry.error(lastError)
})
setInterval(() => {
Telemetry.heartbeat()
}, 1_000 * 3_600)
;[
'exit',
'SIGINT',
'SIGUSR1',
'SIGUSR2',
'uncaughtException',
'SIGTERM'
].forEach((eventType) => {
process.on(eventType, () => {
Telemetry.stop()
global.tcpServerProcess.kill()
setTimeout(() => {
process.exit(0)
}, 1_000)
})
})
}
})()
+46 -3
View File
@@ -29,21 +29,37 @@ import {
import { LogHelper } from '@/helpers/log-helper'
import { LangHelper } from '@/helpers/lang-helper'
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
import { VOICE_CONFIG_PATH, GLOBAL_DATA_PATH } from '@/constants'
import {
MINIMUM_REQUIRED_RAM,
VOICE_CONFIG_PATH,
GLOBAL_DATA_PATH
} from '@/constants'
import { getGlobalEntitiesPath, getGlobalResolversPath } from '@/utilities'
import { SystemHelper } from '@/helpers/system-helper'
interface ObjectUnknown {
[key: string]: unknown
}
const validateSchema = (
schemaName: string,
schema: ObjectUnknown,
contentToValidate: ObjectUnknown,
customErrorMessage: string
): void => {
const schemaFile = `${schemaName}.json`
const validate = ajv.compile(schema)
const isValid = validate(contentToValidate)
const isValidSchemaKey =
typeof contentToValidate['$schema'] === 'string' &&
contentToValidate['$schema'].endsWith(schemaFile)
const isValid = validate(contentToValidate) && isValidSchemaKey
if (!isValid) {
LogHelper.error(customErrorMessage)
if (!isValidSchemaKey) {
LogHelper.error(
`The schema key "$schema" is not valid. Expected "${schemaName}", but got "${contentToValidate['$schema']}".`
)
}
LogHelper.error(customErrorMessage)
const errors = new AggregateAjvError(validate.errors ?? [])
for (const error of errors) {
@@ -55,8 +71,11 @@ const validateSchema = (
/**
* Pre-checking
* Ensure JSON files are correctly formatted
*
* - Ensure the system requirements are met
* - Ensure JSON files are correctly formatted
*/
const VOICE_CONFIG_SCHEMAS = {
amazon: amazonVoiceConfiguration,
'google-cloud': googleCloudVoiceConfiguration,
@@ -72,6 +91,23 @@ const GLOBAL_DATA_SCHEMAS = {
;(async (): Promise<void> => {
LogHelper.title('Pre-checking')
/**
* System requirements checking
*/
LogHelper.info('Checking system requirements...')
const totalRAMInGB = Math.round(SystemHelper.getTotalRAM())
if (totalRAMInGB < MINIMUM_REQUIRED_RAM) {
LogHelper.warning(
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM. It may not work as expected.`
)
} else {
LogHelper.success(
`Minimum required RAM: ${MINIMUM_REQUIRED_RAM} GB | Total RAM: ${totalRAMInGB} GB`
)
}
/**
* Voice configuration checking
*/
@@ -88,6 +124,7 @@ const GLOBAL_DATA_SCHEMAS = {
)
const [configName] = file.split('.') as [keyof typeof VOICE_CONFIG_SCHEMAS]
validateSchema(
`voice-config-schemas/${configName}`,
VOICE_CONFIG_SCHEMAS[configName],
config,
`The voice configuration schema "${voiceConfigPath}" is not valid:`
@@ -116,6 +153,7 @@ const GLOBAL_DATA_SCHEMAS = {
await fs.promises.readFile(globalEntityPath, 'utf8')
)
validateSchema(
'global-data/global-entity',
globalEntitySchemaObject,
globalEntity,
`The global entity schema "${globalEntityPath}" is not valid:`
@@ -136,6 +174,7 @@ const GLOBAL_DATA_SCHEMAS = {
await fs.promises.readFile(globalResolverPath, 'utf8')
)
validateSchema(
'global-data/global-resolver',
globalResolverSchemaObject,
globalResolver,
`The global resolver schema "${globalResolverPath}" is not valid:`
@@ -150,6 +189,7 @@ const GLOBAL_DATA_SCHEMAS = {
await fs.promises.readFile(globalAnswersPath, 'utf8')
)
validateSchema(
'global-data/global-answers',
GLOBAL_DATA_SCHEMAS.answers,
answers,
`The global answers schema "${globalAnswersPath}" is not valid:`
@@ -173,6 +213,7 @@ const GLOBAL_DATA_SCHEMAS = {
await fs.promises.readFile(pathToDomain, 'utf8')
)
validateSchema(
'skill-schemas/domain',
domainSchemaObject,
domainObject,
`The domain schema "${pathToDomain}" is not valid:`
@@ -192,6 +233,7 @@ const GLOBAL_DATA_SCHEMAS = {
await fs.promises.readFile(pathToSkill, 'utf8')
)
validateSchema(
'skill-schemas/skill',
skillSchemaObject,
skillObject,
`The skill schema "${pathToSkill}" is not valid:`
@@ -211,6 +253,7 @@ const GLOBAL_DATA_SCHEMAS = {
await fs.promises.readFile(skillConfigPath, 'utf8')
)
validateSchema(
'skill-schemas/skill-config',
skillConfigSchemaObject,
skillConfig,
`The skill config schema "${skillConfigPath}" is not valid:`
+25 -28
View File
@@ -17,40 +17,37 @@ export const globalEntitySchemaObject = Type.Strict(
)
)
},
{ additionalProperties: false }
{
description:
'Global entities can hold data that can directly be reused in skills.'
}
)
)
export const globalResolverSchemaObject = Type.Strict(
Type.Object(
{
name: Type.String(),
intents: Type.Record(
Type.String(),
Type.Object(
{
utterance_samples: Type.Array(Type.String()),
value: Type.Unknown()
},
{ additionalProperties: false }
)
Type.Object({
name: Type.String(),
intents: Type.Record(
Type.String(),
Type.Object(
{
utterance_samples: Type.Array(Type.String()),
value: Type.Unknown()
},
{ additionalProperties: false }
)
},
{ additionalProperties: false }
)
)
})
)
export const globalAnswersSchemaObject = Type.Strict(
Type.Object(
{
answers: Type.Record(
Type.String(),
Type.Union([
Type.Record(Type.String(), Type.String()),
Type.Array(Type.String())
])
)
},
{ additionalProperties: false }
)
Type.Object({
answers: Type.Record(
Type.String(),
Type.Union([
Type.Record(Type.String(), Type.String()),
Type.Array(Type.String())
])
)
})
)
export type GlobalEntitySchema = Static<typeof globalEntitySchemaObject>
+173 -104
View File
@@ -2,9 +2,22 @@ import type { Static } from '@sinclair/typebox'
import { Type } from '@sinclair/typebox'
import { globalResolverSchemaObject } from '@/schemas/global-data-schemas'
import { SkillBridges } from '@/core/brain/types'
const skillBridges = [Type.Literal('python'), Type.Null()]
const skillActionTypes = [Type.Literal('logic'), Type.Literal('dialog')]
const skillBridges = [
Type.Literal(SkillBridges.Python),
Type.Literal(SkillBridges.NodeJS),
Type.Null()
]
const skillActionTypes = [
Type.Literal('logic', {
description: 'It runs the business logic implemented in actions via code.'
}),
Type.Literal('dialog', {
description:
"Action that don't need code to run. Leon actually just answers without any business logic."
})
]
const skillDataTypes = [
Type.Literal('skill_resolver'),
Type.Literal('global_resolver'),
@@ -21,7 +34,11 @@ const skillCustomEnumEntityType = Type.Object(
})
)
},
{ additionalProperties: false }
{
additionalProperties: false,
description:
'Enum: define a bag of words and synonyms that should match your new entity.'
}
)
const skillCustomRegexEntityType = Type.Object(
{
@@ -29,7 +46,10 @@ const skillCustomRegexEntityType = Type.Object(
name: Type.String({ minLength: 1 }),
regex: Type.String({ minLength: 1 })
},
{ additionalProperties: false }
{
additionalProperties: false,
description: 'Regex: you can create an entity based on a regex.'
}
)
const skillCustomTrimEntityType = Type.Object(
{
@@ -60,7 +80,11 @@ const skillCustomTrimEntityType = Type.Object(
])
)
},
{ additionalProperties: false }
{
additionalProperties: false,
description:
'Trim: you can pick up a data from an utterance by clearly defining conditions (e.g: pick up what is after the last "with" word of the utterance).'
}
)
)
},
@@ -73,119 +97,164 @@ const skillCustomEntityTypes = [
]
export const domainSchemaObject = Type.Strict(
Type.Object(
{
name: Type.String({ minLength: 1 })
},
{ additionalProperties: false }
)
Type.Object({
name: Type.String({ minLength: 1, description: 'The name of the domain.' })
})
)
export const skillSchemaObject = Type.Strict(
Type.Object(
{
name: Type.String({ minLength: 1 }),
bridge: Type.Union(skillBridges),
version: Type.String({ minLength: 1 }),
description: Type.String({ minLength: 1 }),
author: Type.Object(
Type.Object({
name: Type.String({ minLength: 1, description: 'The name of the skill.' }),
bridge: Type.Union(skillBridges, { description: 'Bridge SDK.' }),
version: Type.String({
minLength: 1,
description: 'Version following semver.'
}),
description: Type.String({
minLength: 1,
description: 'This helps people understand what your skill does.'
}),
author: Type.Object(
{
name: Type.String({ minLength: 1, description: 'Name of the author.' }),
email: Type.Optional(
Type.String({
minLength: 1,
maxLength: 254,
format: 'email',
description: 'Email address of the author.'
})
),
url: Type.Optional(
Type.String({
minLength: 1,
maxLength: 255,
format: 'uri',
description: 'Website of the author.'
})
)
},
{
additionalProperties: false,
description:
'A person who has been involved in creating or maintaining this skill.'
}
)
})
)
export const skillConfigSchemaObject = Type.Strict(
Type.Object({
variables: Type.Optional(Type.Record(Type.String(), Type.String())),
actions: Type.Record(
Type.String(),
Type.Object(
{
name: Type.String({ minLength: 1 }),
email: Type.Optional(
Type.String({ minLength: 1, maxLength: 254, format: 'email' })
type: Type.Union(skillActionTypes),
loop: Type.Optional(
Type.Object(
{
expected_item: Type.Object(
{
type: Type.Union(skillDataTypes),
name: Type.String()
},
{ description: 'An item can be a entity or a resolver.' }
)
},
{
additionalProperties: false,
description:
'The action loop is a concept to keep Leon triggering the same skill action until the logic of the skill breaks the loop according to new utterances content.'
}
)
),
url: Type.Optional(
Type.String({ minLength: 1, maxLength: 255, format: 'uri' })
http_api: Type.Optional(
Type.Object(
{
entities: Type.Array(
Type.Object(
{
entity: Type.String(),
resolution: Type.Array(Type.String())
},
{ additionalProperties: false }
)
)
},
{ additionalProperties: false }
)
),
utterance_samples: Type.Optional(Type.Array(Type.String())),
answers: Type.Optional(Type.Array(Type.String())),
unknown_answers: Type.Optional(Type.Array(Type.String())),
suggestions: Type.Optional(
Type.Array(Type.String(), {
description:
'Suggestions are a simple way to suggest Leon owners what can be answered next.'
})
),
slots: Type.Optional(
Type.Array(
Type.Object(
{
name: Type.String(),
item: Type.Object(
{
type: Type.Union(skillDataTypes),
name: Type.String()
},
{ additionalProperties: false }
),
questions: Type.Array(Type.String()),
suggestions: Type.Optional(
Type.Array(Type.String(), {
description:
'Suggestions are a simple way to suggest Leon owners what can be answered next.'
})
)
},
{
additionalProperties: false,
description:
'A slot expects a type of data called "item", and makes use of questions to let Leon owners knows what data they need to provide.'
}
),
{
description:
'Depending on how skill developers wants to design their skill, they have the possibility to ask for more information before to get to the meat of the skill. In this way, Leon can gather these information to operate the skill in a complete manner. These information are called "slots".'
}
)
),
entities: Type.Optional(Type.Union(skillCustomEntityTypes)),
next_action: Type.Optional(
Type.String({
description:
'The next action property is useful when a skill needs to follow a specific order of actions, it helps to connect actions in a specific order to feed the context with data.'
})
)
},
{ additionalProperties: false }
)
},
{ additionalProperties: false }
)
)
export const skillConfigSchemaObject = Type.Strict(
Type.Object(
{
variables: Type.Optional(Type.Record(Type.String(), Type.String())),
actions: Type.Record(
),
answers: Type.Optional(
Type.Record(Type.String(), Type.Array(Type.String()))
),
entities: Type.Optional(Type.Record(Type.String(), Type.String())),
resolvers: Type.Optional(
Type.Record(
Type.String(),
Type.Object(
{
type: Type.Union(skillActionTypes),
loop: Type.Optional(
Type.Object(
{
expected_item: Type.Object({
type: Type.Union(skillDataTypes),
name: Type.String()
})
},
{ additionalProperties: false }
)
),
http_api: Type.Optional(
Type.Object(
{
entities: Type.Array(
Type.Object(
{
entity: Type.String(),
resolution: Type.Array(Type.String())
},
{ additionalProperties: false }
)
)
},
{ additionalProperties: false }
)
),
utterance_samples: Type.Optional(Type.Array(Type.String())),
answers: Type.Optional(Type.Array(Type.String())),
unknown_answers: Type.Optional(Type.Array(Type.String())),
suggestions: Type.Optional(Type.Array(Type.String())),
slots: Type.Optional(
Type.Array(
Type.Object(
{
name: Type.String(),
item: Type.Object(
{
type: Type.Union(skillDataTypes),
name: Type.String()
},
{ additionalProperties: false }
),
questions: Type.Array(Type.String()),
suggestions: Type.Optional(Type.Array(Type.String()))
},
{ additionalProperties: false }
)
)
),
entities: Type.Optional(Type.Union(skillCustomEntityTypes)),
next_action: Type.Optional(Type.String())
intents: globalResolverSchemaObject.properties.intents
},
{ additionalProperties: false }
)
),
answers: Type.Optional(
Type.Record(Type.String(), Type.Array(Type.String()))
),
entities: Type.Optional(Type.Record(Type.String(), Type.String())),
resolvers: Type.Optional(
Type.Record(
Type.String(),
Type.Object(
{
intents: globalResolverSchemaObject.properties.intents
},
{ additionalProperties: false }
)
)
),
{
description:
'You can see resolvers as utterance samples that are converted (resolved) to a value of your choice. They are very handy when skills expect specific utterances and then according to these utterances attribute a value that can be handled by the skill. If a skill action expects to receive a resolver, then Leon will convert the value for you and this value will be usable from the skill action code. Any value can be passed to resolvers which allow a large possibilities of usages.'
}
)
},
{ additionalProperties: false }
)
)
})
)
export type DomainSchema = Static<typeof domainSchemaObject>
+23 -32
View File
@@ -2,42 +2,33 @@ import type { Static } from '@sinclair/typebox'
import { Type } from '@sinclair/typebox'
export const amazonVoiceConfiguration = Type.Strict(
Type.Object(
{
credentials: Type.Object({
accessKeyId: Type.String(),
secretAccessKey: Type.String()
}),
region: Type.String()
},
{ additionalProperties: false }
)
Type.Object({
credentials: Type.Object({
accessKeyId: Type.String(),
secretAccessKey: Type.String()
}),
region: Type.String()
})
)
export const googleCloudVoiceConfiguration = Type.Strict(
Type.Object(
{
type: Type.Literal('service_account'),
project_id: Type.String(),
private_key_id: Type.String(),
private_key: Type.String(),
client_email: Type.String({ format: 'email' }),
client_id: Type.String(),
auth_uri: Type.String({ format: 'uri' }),
token_uri: Type.String({ format: 'uri' }),
auth_provider_x509_cert_url: Type.String({ format: 'uri' }),
client_x509_cert_url: Type.String({ format: 'uri' })
},
{ additionalProperties: false }
)
Type.Object({
type: Type.Literal('service_account'),
project_id: Type.String(),
private_key_id: Type.String(),
private_key: Type.String(),
client_email: Type.String({ format: 'email' }),
client_id: Type.String(),
auth_uri: Type.String({ format: 'uri' }),
token_uri: Type.String({ format: 'uri' }),
auth_provider_x509_cert_url: Type.String({ format: 'uri' }),
client_x509_cert_url: Type.String({ format: 'uri' })
})
)
export const watsonVoiceConfiguration = Type.Strict(
Type.Object(
{
apikey: Type.String(),
url: Type.String({ format: 'uri' })
},
{ additionalProperties: false }
)
Type.Object({
apikey: Type.String(),
url: Type.String({ format: 'uri' })
})
)
export type AmazonVoiceConfigurationSchema = Static<
+263
View File
@@ -0,0 +1,263 @@
import os from 'node:os'
import type { Os } from 'getos'
import axios from 'axios'
import osName from 'os-name'
import getos from 'getos'
import type {
NEREntity,
NLPUtterance,
NLUProcessResult,
NLUResult
} from '@/core/nlp/types'
import {
IS_TELEMETRY_ENABLED,
INSTANCE_ID,
IS_GITPOD,
IS_DEVELOPMENT_ENV,
IS_PRODUCTION_ENV,
LANG,
LEON_VERSION,
NODEJS_BRIDGE_VERSION,
PYTHON_BRIDGE_VERSION,
STT_PROVIDER,
TCP_SERVER_VERSION,
TTS_PROVIDER
} from '@/constants'
import { SystemHelper } from '@/helpers/system-helper'
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
import { LogHelper } from '@/helpers/log-helper'
interface PostIntallResponse {
instanceID: string
birthDate: number
}
enum EventNames {
Heartbeat = 'HEARTBEAT',
Stopped = 'STOPPED'
}
export class Telemetry {
private static readonly serviceURL = 'https://telemetry.getleon.ai'
// private static readonly serviceURL = 'http://localhost:3000'
private static readonly instanceID = INSTANCE_ID
private static readonly axios = axios.create({
baseURL: this.serviceURL,
timeout: 7_000
})
public static async postInstall(): Promise<PostIntallResponse | unknown> {
try {
const { data } = await this.axios.post('/on-post-install', {
instanceID: this.instanceID,
isGitpod: IS_GITPOD
})
return data
} catch (e) {
return {}
}
}
public static async start(): Promise<void> {
if (IS_TELEMETRY_ENABLED) {
try {
const platform = os.platform()
const data = {
isProduction: IS_PRODUCTION_ENV,
isGitpod: IS_GITPOD,
isOnline: true,
language: LANG,
sttProvider: STT_PROVIDER,
ttsProvider: TTS_PROVIDER,
coreVersion: LEON_VERSION,
nodeJSBridgeVersion: NODEJS_BRIDGE_VERSION,
pythonBridgeVersion: PYTHON_BRIDGE_VERSION,
tcpServerVersion: TCP_SERVER_VERSION,
environment: {
osDetails: {
type: os.type(),
platform,
arch: os.arch(),
cpus: os.cpus().length,
release: os.release(),
osName: osName(),
distro: null as Os | null
},
totalRAMInGB: SystemHelper.getTotalRAM(),
nodeVersion: SystemHelper.getNodeJSVersion(),
npmVersion: SystemHelper.getNPMVersion()
}
}
if (platform === 'linux') {
getos(async (e, os) => {
if (e) {
/* */
}
data.environment.osDetails.distro = os
try {
await this.axios.post('/on-start', {
instanceID: this.instanceID,
data
})
} catch (e) {
if (IS_DEVELOPMENT_ENV) {
LogHelper.title('Telemetry')
LogHelper.warning(
`Failed to send start data to telemetry service: ${e}`
)
}
}
})
} else {
await this.axios.post('/on-start', {
instanceID: this.instanceID,
data
})
}
} catch (e) {
if (IS_DEVELOPMENT_ENV) {
LogHelper.title('Telemetry')
LogHelper.warning(
`Failed to send start data to telemetry service: ${e}`
)
}
}
}
}
public static async utterance(
processedData: NLUProcessResult | null
): Promise<void> {
if (IS_TELEMETRY_ENABLED) {
try {
if (processedData?.classification) {
const {
classification: {
domain: triggeredDomain,
skill: triggeredSkill,
action: triggeredAction,
confidence: probability
},
utterance,
entities
} = processedData as NLUResult
const skill = await SkillDomainHelper.getSkillInfo(
triggeredDomain,
triggeredSkill
)
await this.axios.post('/on-utterance', {
instanceID: this.instanceID,
data: {
triggeredDomain: triggeredDomain || null,
triggeredSkill: triggeredSkill || null,
triggeredAction: triggeredAction || null,
probability,
language: processedData?.lang || null,
processingTime: processedData?.processingTime || 0,
executionTime: processedData?.executionTime || 0,
nluProcessingTime: processedData?.nluProcessingTime || 0,
value: this.anonymizeEntities(utterance, entities) || utterance,
triggeredSkillVersion: skill.version || null,
triggeredSkillBridge: skill.bridge || null
}
})
} else if (JSON.stringify(processedData) !== JSON.stringify({})) {
// Not in a skill loop
await this.axios.post('/on-utterance', {
instanceID: this.instanceID,
data: {
language: processedData?.lang || null,
value: processedData?.utterance
}
})
}
} catch (e) {
if (IS_DEVELOPMENT_ENV) {
LogHelper.title('Telemetry')
LogHelper.warning(
`Failed to send utterance data to telemetry service: ${e}`
)
}
}
}
}
public static async error(error: string): Promise<void> {
if (IS_TELEMETRY_ENABLED) {
try {
await this.axios.post('/on-error', {
instanceID: this.instanceID,
error: SystemHelper.sanitizeUsername(error)
})
} catch (e) {
if (IS_DEVELOPMENT_ENV) {
LogHelper.title('Telemetry')
LogHelper.warning(`Failed to send error to telemetry service: ${e}`)
}
}
}
}
public static async stop(): Promise<void> {
if (IS_TELEMETRY_ENABLED) {
try {
await this.sendEvent(EventNames.Stopped)
} catch (e) {
if (IS_DEVELOPMENT_ENV) {
LogHelper.title('Telemetry')
LogHelper.warning(
`Failed to send stop event to telemetry service: ${e}`
)
}
}
}
}
public static async heartbeat(): Promise<void> {
if (IS_TELEMETRY_ENABLED) {
try {
await this.sendEvent(EventNames.Heartbeat)
} catch (e) {
if (IS_DEVELOPMENT_ENV) {
LogHelper.title('Telemetry')
LogHelper.warning(
`Failed to send heartbeat event to telemetry service: ${e}`
)
}
}
}
}
private static async sendEvent(eventName: EventNames): Promise<void> {
if (IS_TELEMETRY_ENABLED) {
try {
await this.axios.post('/on-event', {
instanceID: this.instanceID,
eventName
})
} catch (e) {
if (IS_DEVELOPMENT_ENV) {
LogHelper.title('Telemetry')
LogHelper.warning(`Failed to send event to telemetry service: ${e}`)
}
}
}
}
private static anonymizeEntities(
utterance: NLPUtterance,
entities: NEREntity[]
): NLPUtterance {
entities.forEach((entity) => {
utterance = utterance.replace(entity.sourceText, `{${entity.entity}}`)
})
return utterance
}
}
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "Business & Finance"
}
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "Food & Drink"
}
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"choose_thematic": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Akinator",
"bridge": "python",
"version": "1.0.0",
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "Games"
}
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"setup": {
"type": "logic",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Guess the Number",
"bridge": "python",
"version": "1.0.0",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"start": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Rochambeau",
"bridge": "python",
"version": "1.0.0",
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "Health & Fitness"
}
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "Knowledge & Education"
}
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"variables": {
"blue_leon": "#1C75DB",
"pink_leon": "#ED297A"
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Color",
"bridge": null,
"version": "1.0.0",
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "Leon"
}
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Good Bye",
"bridge": null,
"version": "1.0.0",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "logic",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "logic",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Greeting",
"bridge": "python",
"version": "1.0.0",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"variables": {
"leon_introduction_1": "I'm your daily personal assistant. I have been created by Louis. I'm very happy to serve you everyday.",
"leon_introduction_2": "The question is, who are you? I'm kidding! I'm your daily personal assistant. Louis created me to make your life easier.",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "logic",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Introduction",
"bridge": "python",
"version": "1.0.0",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Joke",
"bridge": null,
"version": "1.0.0",
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Meaning of Life",
"bridge": null,
"version": "1.0.0",
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Partner Assistant",
"bridge": null,
"version": "1.0.0",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "logic",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "logic",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Random Number",
"bridge": "python",
"version": "1.0.0",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
"actions": {
"run": {
"type": "dialog",
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/skill-schemas/skill.json",
"name": "Welcome",
"bridge": null,
"version": "1.0.0",
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "Movies & TV"
}
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "Music & Audio"
}
+1
View File
@@ -1,3 +1,4 @@
{
"$schema": "../../schemas/skill-schemas/domain.json",
"name": "News"
}

Some files were not shown because too many files have changed in this diff Show More