Compare commits

...

333 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
louistiti 5061cac591 Merge branch 'http-server-refactoring' into develop 2023-04-04 23:09:49 +08:00
louistiti f660506d98 chore(server): add TODO for HTTP API skills routes 2023-04-04 23:08:54 +08:00
louistiti 25f81d3c3c Merge branch 'refactor/fastify-api' into http-server-refactoring 2023-04-04 23:03:03 +08:00
Divlo d261967920 refactor(server): http-server with over http enabled 2023-04-03 20:58:02 +02:00
Divlo 907fb1a129 Merge branch 'http-server-refactoring' into refactor/fastify-api 2023-04-03 19:19:21 +02:00
Divlo 7d63108e8d fix(server): async hooks http-server 2023-04-03 19:12:54 +02:00
louistiti 274d7d52fd refactor(server): remove legacy server/src/tts/tts.js 2023-04-03 23:26:52 +08:00
louistiti d815ab8bff refactor(server): socket server interfaces 2023-04-03 23:24:54 +08:00
louistiti 35da3a388a refactor(server): socket server typing 2023-04-03 23:22:07 +08:00
Divlo a29296c612 refactor(server): api and plugins for http-server 2023-04-03 17:20:00 +02:00
louistiti b93180a822 fix(server): ASR not getting ready 2023-04-03 23:04:18 +08:00
louistiti 9fa0c2e1ba chore: re-enable linter on commit 2023-04-03 22:29:21 +08:00
louistiti 2115054de6 Merge branch 'refactor/fs-promises' into http-server-refactoring 2023-04-03 21:59:20 +08:00
louistiti f167f64cfb fix: simultaneous skills execution; Akinator new step 0 2023-04-03 21:55:36 +08:00
Divlo abc6b21df2 fix: make cleanActiveContext synchronous 2023-04-03 08:33:45 +02:00
Divlo bd66bb5cb7 refactor: usage of fs.promises 2023-04-02 20:34:50 +02:00
Divlo c49f931da4 style: fix linting 2023-04-02 19:18:15 +02:00
louistiti 070f0e1469 refactor(server): change HTTP server props accessiblity 2023-04-02 19:11:37 +08:00
louistiti bf4806c987 refactor(server): finish NLU typing 2023-04-02 18:42:00 +08:00
louistiti 37e5ca0e87 refactor(server): simpler error handler from the NLU action execution 2023-04-02 18:04:25 +08:00
louistiti a6011e3cc6 refactor(server): NLP.js process raw type 2023-04-02 18:00:10 +08:00
louistiti 44882129a7 refactor(server): NLU default values 2023-04-02 16:56:53 +08:00
louistiti 8282c37a5a refactor(server): slot filling 2023-03-27 22:23:10 +08:00
louistiti 26ff4520c8 refactor(server): action loop 2023-03-27 22:09:32 +08:00
louistiti e2f0df2cb6 refactor(server): unify join path 2023-03-27 21:51:40 +08:00
louistiti 7259ceda59 refactor(server): unify skill config getter 2023-03-27 21:42:33 +08:00
louistiti 7f99372130 Merge branch 'develop' into http-server-refactoring 2023-03-26 18:52:19 +08:00
Louis Grenard 0cbd438a34 docs: question LLMs to large language models 2023-03-26 10:23:11 +08:00
Louis Grenard 71b30a7f97 docs: add current state to the README 2023-03-26 10:21:22 +08:00
louistiti bfdc60fa65 refactor(server): slot filling 2023-03-22 23:54:58 +08:00
louistiti 99af936803 refactor(server): action loop 2023-03-22 23:34:39 +08:00
louistiti d41e960373 refactor(server): brain types 2023-03-22 22:31:42 +08:00
louistiti ae04ba369c refactor(server): NLP conversation 2023-03-22 22:19:29 +08:00
louistiti afff309d43 refactor(server): skillConfigPath naming unification 2023-03-22 22:08:35 +08:00
louistiti 16c25c9036 chore: upgrade async to latest 2023-03-20 23:46:27 +08:00
louistiti cd551197ea chore: upgrade Fastify to latest 2023-03-20 23:37:55 +08:00
louistiti f6077d58f1 chore: upgrade to TypeScript 5+ 2023-03-20 23:35:07 +08:00
louistiti eb026cb74a refactor(server): move some NER methods from NLU to NER 2023-03-20 23:15:26 +08:00
louistiti 7d6698df17 refactor(server): NLU method accessibility 2023-03-20 22:45:11 +08:00
louistiti a215fc8770 refactor(server): default NLU result 2023-03-20 22:35:44 +08:00
louistiti a8e3fb8b87 refactor(server): NER 2023-03-20 22:04:04 +08:00
louistiti 6820501da6 refactor(server): remove JavaScript NER and NLU 2023-03-20 21:22:33 +08:00
louistiti b51bf4b1c2 refactor(server): model loader error handling 2023-03-20 20:52:42 +08:00
louistiti 5369da1734 refactor(server): models loader 2023-03-18 10:47:44 +08:00
louistiti d5d8d56124 refactor(server): brain done 2023-03-15 22:30:22 +08:00
louistiti fbcc089e2b refactor(server): brain options 2023-03-07 23:22:35 +08:00
louistiti 76dacb4f5e refactor(server): brain logic action skill execution 2023-03-07 22:20:40 +08:00
louistiti 24ec0f0927 refactor(server): start brain refactoring 2023-03-05 23:07:34 +08:00
louistiti b8aac89c4d refactor(server): brain NLU result renaming 2023-03-04 06:48:04 +08:00
louistiti 72a4f787a3 refactor(server): brain typing done 2023-02-25 09:32:11 +08:00
louistiti 137032b1c0 refactor(server): fix brain answers types 2023-02-25 08:46:23 +08:00
louistiti 5011d906e7 chore: more comment and remove socket server legacy comments 2023-02-21 00:15:42 +08:00
louistiti 35d171e421 chore: comment for what to do next 2023-02-21 00:11:32 +08:00
louistiti 20e7a98241 refactor(server): continue brain typing 2023-02-21 00:00:14 +08:00
louistiti abf811f2e4 refactor(server): handle skill result in brain 2023-02-20 22:58:15 +08:00
louistiti 4d7c49e1ee refactor(server): load models and init core instances only once 2023-02-20 21:56:22 +08:00
louistiti e182da0048 refactor(server): remove unecessary socket connections 2023-02-20 20:26:38 +08:00
louistiti f81255aa20 refactor(server): better STT/TTS logs 2023-02-20 20:31:42 +08:00
louistiti 7aaca4dfc8 refactor(server): only keep types file at the first-node level 2023-02-20 00:30:17 +08:00
louistiti 4a74ee030a refactor(server): BrainProcessResult action; nextAction type to skill config schema action [string] 2023-02-20 00:09:38 +08:00
louistiti 7c76e538ab refactor(server): define BrainProcessResult and refactor schema types naming 2023-02-20 00:07:42 +08:00
louistiti 87ed70d313 refactor(server): define NLUSlot 2023-02-19 23:15:34 +08:00
louistiti 98bb70bee6 refactor(server): define SkillOutputType 2023-02-19 20:56:35 +08:00
louistiti a77f0e2473 refactor(server): implement all NER entity types 2023-02-19 20:48:48 +08:00
louistiti 03b128a544 chore: new ESLint rules for TypeScript 2023-02-19 00:02:15 +08:00
louistiti 9c6a60f92c refactor(server): set SkillConfig actions type in brain 2023-02-18 23:49:10 +08:00
louistiti 311923255e fix(server): fail when first execute a dialog action 2023-02-18 19:51:56 +08:00
louistiti cb77b4fc95 refactor(server): get correct audio duration from TTS 2023-02-18 19:16:26 +08:00
louistiti 4883d7723b refactor(server): synthesizers and parsers debug (tmp) 2023-02-13 23:57:29 +08:00
louistiti eb72a61bad refactor(server): brain tmp 2023-02-12 23:21:19 +08:00
louistiti 6dfeb6480f chore: add save-exact npm option to install exact packages version 2023-02-11 19:35:15 +08:00
louistiti 51f9a29679 chore: lock all deps 2023-02-11 11:36:08 +08:00
louistiti d51bf36566 refactor(server): NLU and brain kick off (tmp) 2023-02-10 22:18:56 +08:00
louistiti 9c9f944350 refactor(server): switch from brain instance within NLU to singleton brain 2023-02-09 21:15:49 +08:00
louistiti 7b557da549 refactor(server): NLP.js TS declarations + NLU kick off 2023-02-09 21:01:33 +08:00
louistiti 849749b900 refactor(server): add lang to TTS init 2023-02-07 22:41:02 +08:00
louistiti 528da32d03 refactor(server): STT parser abstract class 2023-02-07 22:36:59 +08:00
louistiti 9ef7476267 refactor(server): Watson synthesizer + synthesizer abstract class 2023-02-07 22:21:42 +08:00
louistiti 0f897942fa refactor(server): remove already-refactored synthesizers 2023-02-07 21:55:14 +08:00
louistiti 885f3738fc refactor(server): TTS Google Cloud synthesizer 2023-02-07 21:35:27 +08:00
louistiti ab0d88accf refactor(server): add STT parser parent class 2023-02-07 21:30:47 +08:00
louistiti 1146322196 refactor(server): TTS Flite synthesizer + synthesizer parent class 2023-02-07 21:26:14 +08:00
louistiti 157e28a063 Merge remote-tracking branch 'origin/develop' into http-server-refactoring 2023-02-03 00:18:56 +08:00
louistiti c8bc9ecbb0 refactor(server): HTTP server; brain entries; STT parsers; TTS synthesizers (WIP) 2023-02-03 00:14:23 +08:00
Louis Grenard 18ba094647 docs: add sponsor to README.md 2023-01-31 23:22:31 +08: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 19ce6eb9c2 refactor(server): socket server and brain (WIP) 2022-11-20 23:21:06 +08:00
louistiti 934689ecf7 chore: merge branch 'develop' into http-server-refactoring 2022-11-19 21:15:35 +08:00
louistiti 2f56a14d68 chore: delete package-lock.json file due to @ffmpeg-installer/ffmpeg optionalDependencies binaries 2022-11-19 21:14:36 +08:00
louistiti cc05ce3d76 chore: format pre-check 2022-11-19 21:13:00 +08:00
louistiti 5959b43e0a chore: upgrade TypeScript to latest 2022-11-19 21:12:43 +08:00
louistiti b9a6ee08fe refactor(server): split socket server (WIP) 2022-11-19 20:57:45 +08:00
louistiti b1739bbe6b Merge branch 'develop' into http-server-refactoring 2022-11-14 23:57:41 +08:00
louistiti cad6d7f725 Merge branch 'feat/add-typebox-ajv' into develop 2022-11-14 23:56:08 +08:00
louistiti a08ddb2e26 Merge branch 'develop' into feat/add-typebox-ajv 2022-11-14 23:56:03 +08:00
louistiti c7aa84f9c4 docs: remove sponsor 2022-11-14 23:50:58 +08:00
louistiti ec10030951 refactor: modify voice config samples default values 2022-11-14 23:49:44 +08:00
louistiti b298667e3d Merge branch 'docs/fix-contributing-python-environment' into develop 2022-11-14 23:41:13 +08:00
louistiti 69e90c3b5f refactor(server): HTTP server and structure (wip) 2022-11-14 00:49:24 +08:00
Divlo 2ada7f2efa docs: make clear that commands should be run on Ubuntu in CONTRIBUTING to set up the Python Environment 2022-11-13 13:41:51 +01:00
Divlo 28c15885fd fix: improve error display in pre-check 2022-11-13 13:36:54 +01:00
Divlo 194a9b1bbb fix: add http_api property to skill config schema
http_api is used by `skills/news/github_trends/config/en.json`.
2022-11-13 13:08:32 +01:00
Divlo 4ce9676319 fix: include voice configuration sample schemas 2022-11-13 13:02:51 +01:00
Divlo fb75b40fb6 feat: validate JSON Schemas with AJV 2022-11-13 13:00:15 +01:00
Divlo d973ab4318 chore: delete schemas.ts files outside server 2022-11-13 12:48:43 +01:00
Divlo f8b08f38b1 fix: stricter JSON Schemas
- Type.Strict()
- Type.Object({ additionalProperties: false })
- Type.String({ format: 'email' | 'uri' })
2022-11-13 12:46:31 +01:00
louistiti b3b10e326f refactor(server): prepare upcoming core singletons registration 2022-11-13 01:12:20 +08:00
louistiti efe865a4c5 refactor(server): reorganize TCP client accessors 2022-11-13 00:40:14 +08:00
louistiti a37ac96411 refactor(server): protect TCP client from mutability 2022-11-13 00:16:53 +08:00
louistiti 8a16a3d97e refactor(server): LEON_NODE_ENV constant 2022-11-12 23:20:48 +08:00
louistiti 3d9d9496b7 fix(server): reset connection state and connection counter 2022-11-12 23:09:22 +08:00
louistiti 862112ed5f refactor(server): TCP client connect method 2022-11-12 22:51:47 +08:00
louistiti ea76739d88 refactor(server): TCP client to singleton 2022-11-12 22:31:49 +08:00
louistiti c53feba8f5 chore: latest package-lock.json 2022-11-12 10:00:22 +08:00
louistiti 62d511f967 chore: merge 2022-11-12 09:48:14 +08:00
louistiti 664359158e chore: merge 2022-11-12 09:46:50 +08:00
louistiti 8ecde83b89 refactor(server): TCP client to TypeScript 2022-11-11 23:32:33 +08:00
louistiti 420f87daca docs: add contributing guide for the Python bridge and TCP server 2022-11-11 22:54:28 +08:00
louistiti c722050aec refactor: add types to schemas object on pre-checking 2022-11-11 22:27:47 +08:00
louistiti c434b550a5 Merge branch 'feat/add-typebox-ajv' into develop 2022-11-11 21:50:55 +08:00
louistiti 06a2adf62b feat: add pre-checking for all JSON configs 2022-11-11 21:43:24 +08:00
louistiti bb0cf69659 feat: add pre-checking for all JSON configs 2022-11-11 11:43:24 +08:00
louistiti e50fadd142 Merge branch 'develop' into feat/add-typebox-ajv 2022-11-08 20:59:25 +08:00
louistiti 327cbff184 Merge branch 'develop' into feat/add-typebox-ajv 2022-11-08 20:59:25 +08:00
louistiti 62853a340f feat: add total RAM data to the checking script 2022-11-08 20:10:46 +08:00
louistiti 86852a8cac feat: add total RAM data to the checking script 2022-11-08 20:10:46 +08:00
louistiti efc94a2ac3 Merge branch 'develop' into feat/add-typebox-ajv 2022-11-06 23:13:49 +08:00
louistiti fd4f429b86 Merge branch 'develop' into feat/add-typebox-ajv 2022-11-06 23:13:49 +08:00
louistiti f5404920fa Merge branch 'develop' into feat/add-typebox-ajv 2022-11-06 23:13:49 +08:00
louistiti a0d8c7251c refactor: add full intent object path on skill execution during checking 2022-11-06 22:25:39 +08:00
louistiti 303ebea3f3 refactor: add full intent object path on skill execution during checking 2022-11-06 22:25:39 +08:00
louistiti c38012afcf refactor: add full intent object path on skill execution during checking 2022-11-06 22:25:39 +08:00
louistiti becac3b0f6 fix(server): Python bridge fails to execute on Windows intent object absolute path because of whitespace 2022-11-06 22:18:09 +08:00
louistiti 0245712e24 fix(server): Python bridge fails to execute on Windows intent object absolute path because of whitespace 2022-11-06 22:18:09 +08:00
louistiti 4b003a852e fix(server): Python bridge fails to execute on Windows intent object absolute path because of whitespace 2022-11-06 22:18:09 +08:00
louistiti ddf389ffd1 chore: add Python bridge npm script 2022-11-06 21:57:04 +08:00
louistiti 1d59b0a774 chore: add Python bridge npm script 2022-11-06 21:57:04 +08:00
louistiti 74582899a2 chore: add Python bridge npm script 2022-11-06 21:57:04 +08:00
louistiti 8a91dcb85d refactor(web app): minor style improvements 2022-11-05 23:33:15 +08:00
louistiti 8fbc4bb23f refactor(web app): minor style improvements 2022-11-05 23:33:15 +08:00
louistiti 61d31d9b52 refactor(web app): minor style improvements 2022-11-05 23:33:15 +08:00
louistiti a81e536fd5 refactor(web app): adapt new Vite config 2022-11-05 23:17:18 +08:00
louistiti 25fdaeeab2 refactor(web app): adapt new Vite config 2022-11-05 23:17:18 +08:00
louistiti c4885cb324 refactor(web app): adapt new Vite config 2022-11-05 23:17:18 +08:00
Divlo 71e0f1933c refactor: domain, skill-config and skill in skills/schemas.ts 2022-11-01 21:51:19 +01:00
Divlo 65246dc7cd refactor: domain, skill-config and skill in skills/schemas.ts 2022-11-01 21:51:19 +01:00
Divlo 32fd041495 refactor: domain, skill-config and skill in skills/schemas.ts 2022-11-01 21:51:19 +01:00
louistiti 6c292994fd feat: add data schemas 2022-11-02 00:21:14 +08:00
louistiti 6e8c326a9c feat: add data schemas 2022-11-02 00:21:14 +08:00
louistiti a4e9b1e1cc feat: add data schemas 2022-11-02 00:21:14 +08:00
louistiti 29d7f16b2c feat: add voice configuration schemas 2022-11-01 22:09:52 +08:00
louistiti e3c0a180af feat: add voice configuration schemas 2022-11-01 22:09:52 +08:00
louistiti 360c4d4f6f feat: add voice configuration schemas 2022-11-01 22:09:52 +08:00
louistiti 1a580d1051 Merge branch 'develop' into feat/add-typebox-ajv 2022-11-01 21:45:50 +08:00
louistiti f13df6ab76 Merge branch 'develop' into feat/add-typebox-ajv 2022-11-01 21:45:50 +08:00
louistiti 239472b61e Merge branch 'develop' into feat/add-typebox-ajv 2022-11-01 21:45:50 +08:00
Louis Grenard 83c501dc8d docs: add new sponsor 2022-11-01 21:44:44 +08:00
Louis Grenard 67d07ed7e6 docs: add new sponsor 2022-11-01 21:44:44 +08:00
Louis Grenard 18a728ff4b docs: add new sponsor 2022-10-30 22:23:40 +08:00
Louis Grenard b77e3a99e9 docs: add new sponsor 2022-10-30 22:23:40 +08:00
louistiti daba510755 feat: add distro level info in check report 2022-10-29 11:48:16 +08:00
louistiti dcc04de6bc feat: add distro level info in check report 2022-10-29 11:48:16 +08:00
louistiti 38693e42ef Merge branch 'fix/docker-issues-with-ssl' into develop 2022-10-23 22:55:04 +08:00
louistiti ceaff26d2d Merge branch 'fix/docker-issues-with-ssl' into develop 2022-10-23 22:55:04 +08:00
Divlo db373b66a6 fix(docker): issues with SSL 2022-10-23 12:37:36 +02:00
Divlo fb03359400 fix(docker): issues with SSL 2022-10-23 12:37:36 +02:00
Divlo d8e40c9cce refactor(server): replace unknown return type with TypeBox schema types 2022-10-23 11:09:07 +02:00
Divlo ecca030ab9 refactor(server): replace unknown return type with TypeBox schema types 2022-10-23 11:09:07 +02:00
Divlo 35322b39e3 refactor(server): replace unknown return type with TypeBox schema types 2022-10-23 11:09:07 +02:00
Divlo be079b87c8 feat(server): add JSON models with TypeBox
Later, we will be able to validate JSON files thanks to AJV.
2022-10-23 11:05:55 +02:00
Divlo 2a7b2f45d6 feat(server): add JSON models with TypeBox
Later, we will be able to validate JSON files thanks to AJV.
2022-10-23 11:05:55 +02:00
Divlo 2a48bc6343 feat(server): add JSON models with TypeBox
Later, we will be able to validate JSON files thanks to AJV.
2022-10-23 11:05:55 +02:00
louistiti 5acd22a885 Merge branch 'python-bridge-tcp-server-binaries' into develop 2022-10-23 12:01:21 +08:00
louistiti 632a048c6b Merge branch 'python-bridge-tcp-server-binaries' into develop 2022-10-23 12:01:21 +08:00
louistiti e1e4910399 fix(docker): based on Ubuntu image 2022-10-23 11:46:15 +08:00
louistiti 5bd67be2e5 fix(docker): based on Ubuntu image 2022-10-23 11:46:15 +08:00
louistiti 482f7bfbb8 chore(docker): .dockerignore path 2022-10-23 10:58:25 +08:00
louistiti 2d6f16a311 chore(docker): .dockerignore path 2022-10-23 10:58:25 +08:00
louistiti 3f8027af2a refactor: from superagent to axios 2022-10-23 10:55:03 +08:00
louistiti da063b73bc refactor: from superagent to axios 2022-10-23 10:55:03 +08:00
louistiti 3f97a12153 fix(python bridge): remove unecessary sqlite3 import 2022-10-23 10:07:14 +08:00
louistiti 6bee56580b fix(python bridge): remove unecessary sqlite3 import 2022-10-23 10:07:14 +08:00
louistiti ad1708abbb feat: ignore given warnings for TCP server binary on check script 2022-10-23 01:58:13 +08:00
louistiti bd1671c440 feat: ignore given warnings for TCP server binary on check script 2022-10-23 01:58:13 +08:00
louistiti e527110b9c feat: add binaries download progress 2022-10-23 01:00:55 +08:00
louistiti 0cf39c181f feat: add binaries download progress 2022-10-23 01:00:55 +08:00
louistiti c0886c086f Merge branch 'feat/download-unzip-python-bridge-tcp-server-binaries' into python-bridge-tcp-server-binaries 2022-10-22 23:23:08 +08:00
louistiti e459c1011c Merge branch 'feat/download-unzip-python-bridge-tcp-server-binaries' into python-bridge-tcp-server-binaries 2022-10-22 23:23:08 +08:00
louistiti 9928509171 refactor: minor setup-python-binaries renaming 2022-10-22 23:22:50 +08:00
louistiti 16444ae79f refactor: minor setup-python-binaries renaming 2022-10-22 23:22:50 +08:00
louistiti 75a351e77d chore: fix typo in report logs 2022-10-22 17:50:35 +08:00
louistiti 5c02e906fa chore: fix typo in report logs 2022-10-22 17:50:35 +08:00
louistiti b74e7beb29 ci: pre-release TCP server workflow 2022-10-22 15:14:17 +08:00
louistiti 03b0d08ae0 ci: pre-release TCP server workflow 2022-10-22 15:14:17 +08:00
louistiti 2183ffa4f0 refactor: send check report to the production URL 2022-10-19 23:28:33 +08:00
louistiti 6dbdfe7ba5 refactor: send check report to the production URL 2022-10-19 23:28:33 +08:00
Divlo 8ab9c2e4df feat: download and unzip TCP Server + Python Bridge on postinstall 2022-10-19 13:23:28 +02:00
Divlo c912b637ee feat: download and unzip TCP Server + Python Bridge on postinstall 2022-10-19 13:23:28 +02:00
louistiti fcce24d68c feat: send report to Pastebin and generate URL 2022-10-19 01:17:57 +08:00
louistiti 4df23cfe45 feat: send report to Pastebin and generate URL 2022-10-19 01:17:57 +08:00
louistiti 76e700e9ce feat: provide Python bridge and TCP server version in report 2022-10-19 00:40:34 +08:00
louistiti 65592b9af5 feat: provide Python bridge and TCP server version in report 2022-10-19 00:40:34 +08:00
louistiti 2463759717 refactor: check script according to binaries changes 2022-10-19 00:22:37 +08:00
louistiti 8f9937ddee refactor: check script according to binaries changes 2022-10-19 00:22:37 +08:00
louistiti 02c28b85f6 refactor: remove unnecessary checks 2022-10-16 23:25:58 +08:00
louistiti 5042bcc95d refactor: remove unnecessary checks 2022-10-16 23:25:58 +08:00
louistiti cc6503e6f7 ci: remove tests for Ubuntu AArch64 2022-10-16 01:14:45 +08:00
louistiti 8de9d84f1f ci: test Ubuntu AArch64 2022-10-15 23:36:34 +08:00
louistiti 29c582c89e ci: build on Ubuntu AArch64 2022-10-15 23:24:45 +08:00
louistiti bc53a36cd8 ci: split macOS ARM64 build into isolated job 2022-10-15 22:25:05 +08:00
louistiti 84cdc7fa60 ci: add macOS ARM64 self-hosted runner 2022-10-15 22:09:12 +08:00
louistiti 1dc0451f0f refactor: remove setuptools and wheel force install for macOS ARM64 arch 2022-10-15 21:38:22 +08:00
louistiti 1beebcccd2 refactor: remove Rust compiler version number from TCP server setup 2022-10-15 21:11:29 +08:00
louistiti b6b42c422c feat: install additional resources for macOS ARM64 arch 2022-10-15 21:03:44 +08:00
louistiti bda75915b2 chore: ignore Fleet folder 2022-10-15 20:46:57 +08:00
Divlo b099d53871 fix(docker): install Python binaries from postinstall script 2022-10-13 22:59:39 +02:00
Divlo c6ad1968e7 fix(docker): install Python binaries from postinstall script 2022-10-13 22:59:39 +02:00
louistiti bc37bd1687 ci: try pre-release with all OSes 2022-10-13 01:42:36 +08:00
louistiti 7161cd5996 ci: remove the lock file for Windows 2022-10-13 01:29:37 +08:00
louistiti 23b7d5b49e ci: remove skip lock for Windows 2022-10-13 01:22:40 +08:00
louistiti 599f9561e4 ci: add PIP_NO_BINARY 2022-10-13 01:11:12 +08:00
louistiti bd45220c66 ci: set up MSBuild if Windows 2022-10-13 01:03:14 +08:00
louistiti 53757651c8 ci: make use of Pipenv 2022.7.24 and skip lock on Windows 2022-10-13 00:53:58 +08:00
louistiti bd1bde0814 ci: try with --deploy flag to install Python deps 2022-10-13 00:10:45 +08:00
louistiti ad044814a5 Merge branch 'develop' into python-bridge-tcp-server-binaries 2022-10-12 23:53:16 +08:00
louistiti aede3cbf52 ci: try Windows Pipenv deps install 2022-10-12 23:50:46 +08:00
louistiti 79115d1b25 ci: try to build on Windows without catching exceptions 2022-10-12 23:37:23 +08:00
louistiti 404a1d5b3f Merge branch 'develop' into python-bridge-tcp-server-binaries 2022-10-12 23:11:07 +08:00
louistiti 8a191b9c91 ci: tru the Windows runner without the rm command 2022-10-11 00:47:45 +08:00
louistiti 56a9a796bd ci: remove Pipfile.lock on Windows 2022-10-11 00:43:31 +08:00
louistiti 5bc0a7f97b ci: upgrade Pip to latest and try Windows runner 2022-10-11 00:36:27 +08:00
louistiti 7fe7b2feaa ci: test to upload artifact with default name 2022-10-11 00:03:36 +08:00
louistiti ee0134598c ci: upload artifacts then download and pre-release 2022-10-10 09:11:56 +08:00
louistiti b19e5ad0cb ci: remove Windows 2022-10-10 09:02:03 +08:00
louistiti de442741e5 ci: remove Pipenv cache from pre-release binaries workflows 2022-10-10 00:14:37 +08:00
louistiti f43e17aad4 ci: install Pipenv on pre-release binaries workflows 2022-10-10 00:04:28 +08:00
louistiti ddbe4f0fd1 ci: checkout repo first on pre-release binaries workflows 2022-10-10 00:01:24 +08:00
louistiti f17a601d8e refactor: exit pre-release binaries script 2022-10-09 23:58:59 +08:00
louistiti 5bace9545a ci: add environment variables to locate virtual env 2022-10-09 23:58:05 +08:00
louistiti f170a1dd8e Merge remote-tracking branch 'origin/develop' into python-bridge-tcp-server-binaries 2022-10-09 23:53:49 +08:00
louistiti 91eac5c875 refactor: pre-release binaries script give only workflow name 2022-10-09 23:53:40 +08:00
louistiti 518889b459 fix: rename GitHub workflows from the pre-release-binaries script 2022-10-09 23:39:22 +08:00
louistiti cc8a32d17d ci: pre-release TCP server 2022-10-09 22:03:26 +08:00
louistiti a00b01c041 ci: pre-release Python bridge 2022-10-09 22:00:56 +08:00
louistiti 7db5c064d9 refactor: rename GitHub workflows release script as pre-release 2022-10-09 21:27:12 +08:00
louistiti ed8390a36c feat: trigger GitHub workflows script done 2022-10-09 21:16:42 +08:00
louistiti 1d45c2e715 feat(tcp server): versioning 2022-10-09 00:31:52 +08:00
louistiti 784f86f6f3 feat(python bridge): versioning 2022-10-09 00:29:30 +08:00
louistiti 9d865291f0 feat(server): add warning hint on macOS TCP server cold start 2022-10-07 19:18:36 +08:00
louistiti bc7dd0f836 fix: comply macOS x86_64 and macOS arm64 to Python sysconfig.get_platform() 2022-10-07 18:25:51 +08:00
louistiti 8d64ad9650 refactor: remove LC_ALL and LANG env vars 2022-10-07 18:23:10 +08:00
louistiti b3fdde9d83 chore: explicit exclusion of TCP server and Python bridge on TypeScript watcher 2022-10-07 15:47:55 +08:00
louistiti ea6fc4108a perf(tcp server): option to download spaCy models through mirror in China 2022-10-07 15:31:37 +08:00
louistiti 40958f2fb4 fix(tcp server): handle Windows child process does not throw exception 2022-10-07 14:10:32 +08:00
louistiti 8e0d18a596 perf(tcp server): do not download Python deps but spaCy models when necessary 2022-10-07 13:44:48 +08:00
louistiti 861d773cdd feat: on Python dev env setup, provide Windows hint related to Microsoft C++ Build Tools 2022-10-07 13:24:58 +08:00
louistiti 6bd9f0ef9d fix: always make use of latest Python build deps 2022-10-07 13:16:35 +08:00
louistiti d69157439c feat: add hint on failed build 2022-10-07 12:07:48 +08:00
louistiti 1a3d4f2204 feat: map Python bridge and TCP server binaries to runtime code 2022-10-07 11:53:43 +08:00
louistiti b59f6a7264 perf(tcp server): optimize spaCy models checking 2022-10-07 11:26:39 +08:00
louistiti bf902fd2d9 feat: universal build binaries script 2022-10-07 10:41:04 +08:00
louistiti a8272e1bf9 feat: pack TCP server and optimize setup scripts 2022-10-07 02:37:43 +08:00
louistiti b96070085b feat(tcp server): set up script 2022-10-06 23:20:30 +08:00
louistiti 48688dc6e6 chore: upgrade archiver to latest 2022-10-06 19:48:17 +08:00
louistiti 9dd9b010ed feat(python bridge): pack build 2022-10-06 19:45:35 +08:00
louistiti 0888ae5033 feat(python bridge): build script 2022-10-06 19:01:50 +08:00
louistiti 2d7be3348a feat(python bridge): set up script 2022-10-06 16:27:20 +08:00
louistiti bee3fbb9e2 refactor(tcp server): move tokenizers private libraries to lib location 2022-10-06 08:34:30 +08:00
louistiti 55db68400d fix(tcp server): include private libraries of the tokenizers package 2022-10-06 01:25:26 +08:00
louistiti 7d2432c56c feat(tcp server): include tokenizers files on build 2022-10-06 01:11:52 +08:00
louistiti 48731e8ddf chore(tcp server): upgrade to latest setuptools version 2022-10-06 00:59:20 +08:00
louistiti 4d0a97f9c2 refactor(tcp server): remove cx_Freeze from Pipfile to install it individually during the dev phase 2022-10-06 00:13:54 +08:00
louistiti 133310bd7d fix: remove requests.certs import from the TCP server and Python bridge setups 2022-10-05 01:31:46 +08:00
louistiti 69bf9cebb3 fix: upgrade cx_Freeze to 6.11.1 with --no-binary to avoid OpenSSL conflict with urllib3 2022-10-04 23:57:18 +08:00
louistiti 91e39a0a37 chore: add TCP server and Python bridge as high-level nodes 2022-10-04 17:16:41 +08:00
louistiti 586ca11106 feat: isolate TCP server 2022-10-04 17:10:14 +08:00
louistiti 20b967b4ea chore: transparent distribution structure 2022-10-04 14:30:10 +08:00
louistiti dd9d024975 feat: Python bridge binary build with cx_Freeze 2022-10-04 14:19:56 +08:00
louistiti 8d1ab6973a feat: Python bridge build on macOS x86_64 arch 2022-10-02 15:43:48 +08: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
225 changed files with 6941 additions and 30850 deletions
+9 -2
View File
@@ -1,2 +1,9 @@
node_modules/
bridges/python/src/.venv/*
__pycache__/
**/dist/*
**/build/
**/node_modules/
**/tmp/*
**/src/.venv/*
logs/*
*.pyc
.DS_Store
+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
+16 -1
View File
@@ -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
View File
@@ -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.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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
+41 -11
View File
@@ -7,12 +7,13 @@ env:
PIPENV_VENV_IN_PROJECT: true
jobs:
pre-release:
name: Pre-release
build:
name: Build
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-20.04, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
@@ -24,32 +25,62 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: 3.9.10
# cache: pipenv
- name: Install Pipenv
run: pip install --upgrade pip && pip install pipenv
run: pip install --upgrade pip && pip install pipenv==2022.7.24
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: lts/*
cache: npm
- name: Set TCP server version
working-directory: ./tcp_server/src
working-directory: tcp_server/src
run: |
echo "TCP_SERVER_VERSION=$(python -c "from version import __version__; print(__version__)")" >> $GITHUB_ENV
- name: Display TCP server version
run: |
echo "TCP server version: ${{ env.TCP_SERVER_VERSION }}"
- name: Install core
run: npm ci
run: npm install
- name: Setup TCP server
- name: Set up TCP server
run: npm run setup:tcp-server
- name: Build TCP server
run: npm run build:tcp-server
- name: Upload TCP server
uses: actions/upload-artifact@v3
with:
path: tcp_server/dist/*.zip
draft-release:
name: Draft-release
needs: [build]
runs-on: ubuntu-20.04
steps:
- name: Clone repository
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: 3.9.10
- name: Set TCP server version
working-directory: tcp_server/src
run: |
echo "TCP_SERVER_VERSION=$(python -c "from version import __version__; print(__version__)")" >> $GITHUB_ENV
- name: Download TCP server
uses: actions/download-artifact@v3
with:
path: tcp_server/dist
- uses: marvinpinto/action-automatic-releases@latest
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -57,5 +88,4 @@ jobs:
draft: true
prerelease: false
title: TCP Server ${{ env.TCP_SERVER_VERSION }}
files: |
tcp_server/dist/*.zip
files: tcp_server/dist/artifact/*.zip
+4
View File
@@ -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
+2
View File
@@ -1 +1,3 @@
engine-strict=true
package-lock=false
save-exact=true
+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._
+22 -31
View File
@@ -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
+55 -22
View File
@@ -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.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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>
&nbsp; &nbsp; &nbsp;
<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
+7 -5
View File
@@ -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% {
+1 -1
View File
@@ -100,7 +100,7 @@ export default class Client {
}
}
}
}, 1000)
}, 1_000)
}, data.duration + 500)
}
})
+96 -96
View File
@@ -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
View File
@@ -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
}
}
})
+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
View File
@@ -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
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": ""
+7 -6
View File
@@ -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/"
}
+2 -1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/voice-config-schemas/watson-stt.json",
"apikey": "",
"url": ""
"url": "https://stream.watsonplatform.net/speech-to-text/api"
}
+2 -1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../../../schemas/voice-config-schemas/watson-tts.json",
"apikey": "",
"url": ""
"url": "https://stream.watsonplatform.net/text-to-speech/api"
}
+6 -6
View File
@@ -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
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": {
@@ -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
}
+5 -4
View File
@@ -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
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": {
+89 -89
View File
@@ -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": []
}
]
}
+2 -2
View File
@@ -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
View File
@@ -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)
}
}
})
})()
-27191
View File
File diff suppressed because it is too large Load Diff
+75 -58
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",
@@ -40,89 +40,106 @@
"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/run-check.js",
"check": "ts-node scripts/check.js",
"docker:build": "docker build -t leon-ai/leon .",
"docker:run": "docker compose up",
"docker:dev": "docker compose --file=docker-compose.dev.yml up",
"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
View File
@@ -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
View File
@@ -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')
+343 -199
View File
@@ -1,207 +1,310 @@
import fs from 'node:fs'
import path from 'node:path'
import os from 'node:os'
import { spawn } from 'node:child_process'
import dotenv from 'dotenv'
import { command } from 'execa'
import semver from 'semver'
import kill from 'tree-kill'
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 { PYTHON_BRIDGE_BIN_PATH, EN_SPACY_MODEL_NAME } from '@/constants'
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,
INSTANCE_ID
} from '@/constants'
dotenv.config()
/**
* Checking script
* Help to figure out what is installed or not
* Help to figure out the setup state
*/
export default () =>
new Promise(async (resolve, reject) => {
try {
const nodeMinRequiredVersion = '10'
const npmMinRequiredVersion = '5'
const pythonMinRequiredVersion = '3'
const flitePath = 'bin/flite/flite'
const coquiLanguageModelPath = 'bin/coqui/huge-vocabulary.scorer'
const amazonPath = 'core/config/voice/amazon.json'
const googleCloudPath = 'core/config/voice/google-cloud.json'
const watsonSttPath = 'core/config/voice/watson-stt.json'
const watsonTtsPath = 'core/config/voice/watson-tts.json'
const globalResolversNlpModelPath =
'core/data/models/leon-global-resolvers-model.nlp'
const skillsResolversNlpModelPath =
'core/data/models/leon-skills-resolvers-model.nlp'
const mainNlpModelPath = 'core/data/models/leon-main-model.nlp'
const report = {
can_run: { title: 'Run', type: 'error', v: true },
can_run_skill: { title: 'Run skills', type: 'error', v: true },
can_text: { title: 'Reply you by texting', type: 'error', v: true },
has_spacy_model: {
title: 'spaCy model is installed',
type: 'error',
v: true
},
can_amazon_polly_tts: {
title: 'Amazon Polly text-to-speech',
type: 'warning',
v: true
},
can_google_cloud_tts: {
title: 'Google Cloud text-to-speech',
type: 'warning',
v: true
},
can_watson_tts: {
title: 'Watson text-to-speech',
type: 'warning',
v: true
},
can_offline_tts: {
title: 'Offline text-to-speech',
type: 'warning',
v: true
},
can_google_cloud_stt: {
title: 'Google Cloud speech-to-text',
type: 'warning',
v: true
},
can_watson_stt: {
title: 'Watson speech-to-text',
type: 'warning',
v: true
},
can_offline_stt: {
title: 'Offline speech-to-text',
type: 'warning',
v: true
}
;(async () => {
try {
const nodeMinRequiredVersion = '16'
const npmMinRequiredVersion = '8'
const flitePath = 'bin/flite/flite'
const coquiLanguageModelPath = 'bin/coqui/huge-vocabulary.scorer'
const amazonPath = 'core/config/voice/amazon.json'
const googleCloudPath = 'core/config/voice/google-cloud.json'
const watsonSttPath = 'core/config/voice/watson-stt.json'
const watsonTtsPath = 'core/config/voice/watson-tts.json'
const globalResolversNlpModelPath =
'core/data/models/leon-global-resolvers-model.nlp'
const skillsResolversNlpModelPath =
'core/data/models/leon-skills-resolvers-model.nlp'
const mainNlpModelPath = 'core/data/models/leon-main-model.nlp'
const report = {
can_run: { title: 'Run', type: 'error', v: true },
can_run_skill: { title: 'Run skills', type: 'error', v: true },
can_text: { title: 'Reply you by texting', type: 'error', v: true },
can_start_tcp_server: {
title: 'Start the TCP server',
type: 'error',
v: true
},
can_amazon_polly_tts: {
title: 'Amazon Polly text-to-speech',
type: 'warning',
v: true
},
can_google_cloud_tts: {
title: 'Google Cloud text-to-speech',
type: 'warning',
v: true
},
can_watson_tts: {
title: 'Watson text-to-speech',
type: 'warning',
v: true
},
can_offline_tts: {
title: 'Offline text-to-speech',
type: 'warning',
v: true
},
can_google_cloud_stt: {
title: 'Google Cloud speech-to-text',
type: 'warning',
v: true
},
can_watson_stt: {
title: 'Watson speech-to-text',
type: 'warning',
v: true
},
can_offline_stt: {
title: 'Offline speech-to-text',
type: 'warning',
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')
LogHelper.title('Checking')
/**
* Leon version checking
*/
/**
* Leon version checking
*/
LogHelper.info('Leon version')
LogHelper.success(`${version}\n`)
LogHelper.info('Leon version')
LogHelper.success(`${LEON_VERSION}\n`)
reportDataInput.leonVersion = LEON_VERSION
/**
* Environment checking
*/
LogHelper.info('OS')
/**
* Environment checking
*/
const osInfo = {
type: os.type(),
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
release: os.release()
}
LogHelper.info('Environment')
const osInfo = {
type: os.type(),
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
release: os.release(),
osName: osName(),
distro: null
}
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`)
;(
await Promise.all([
command('node --version', { shell: true }),
command('npm --version', { shell: true }),
command('pipenv --version', { shell: true })
])
).forEach((p) => {
LogHelper.info(p.command)
}
if (
p.command.indexOf('node --version') !== -1 &&
!semver.satisfies(
semver.clean(p.stdout),
`>=${nodeMinRequiredVersion}`
)
) {
Object.keys(report).forEach((item) => {
if (report[item].type === 'error') report[item].v = false
})
LogHelper.error(
`${p.stdout}\nThe Node.js version must be >=${nodeMinRequiredVersion}. Please install it: https://nodejs.org (or use nvm)\n`
)
} else if (
p.command.indexOf('npm --version') !== -1 &&
!semver.satisfies(
semver.clean(p.stdout),
`>=${npmMinRequiredVersion}`
)
) {
Object.keys(report).forEach((item) => {
if (report[item].type === 'error') report[item].v = false
})
LogHelper.error(
`${p.stdout}\nThe npm version must be >=${npmMinRequiredVersion}. Please install it: https://www.npmjs.com/get-npm (or use nvm)\n`
)
} else {
LogHelper.success(`${p.stdout}\n`)
}
})
;(
await Promise.all([
command('pipenv --where', { shell: true }),
command('pipenv run python --version', { shell: true })
])
).forEach((p) => {
LogHelper.info(p.command)
reportDataInput.environment.osDetails = osInfo
reportDataInput.environment.totalRAMInGB = totalRAMInGB
;(
await Promise.all([
command('node --version', { shell: true }),
command('npm --version', { shell: true })
])
).forEach((p) => {
LogHelper.info(p.command)
if (
p.command.indexOf('pipenv run python --version') !== -1 &&
!semver.satisfies(
p.stdout.split(' ')[1],
`>=${pythonMinRequiredVersion}`
)
) {
Object.keys(report).forEach((item) => {
if (report[item].type === 'error') report[item].v = false
})
LogHelper.error(
`${p.stdout}\nThe Python version must be >=${pythonMinRequiredVersion}. Please install it: https://www.python.org/downloads\n`
)
} else {
LogHelper.success(`${p.stdout}\n`)
}
})
/**
* Skill execution checking
*/
try {
LogHelper.time('Skill execution time')
const p = await command(
`${PYTHON_BRIDGE_BIN_PATH} scripts/assets/intent-object.json`,
{ shell: true }
)
LogHelper.timeEnd('Skill execution time')
LogHelper.info(p.command)
LogHelper.success(`${p.stdout}\n`)
} catch (e) {
LogHelper.info(e.command)
report.can_run_skill.v = false
LogHelper.error(`${e}\n`)
}
/**
* spaCy model checking
*/
try {
const p = await command(
`pipenv run python -c "import ${EN_SPACY_MODEL_NAME}"`,
{ shell: true }
)
LogHelper.info(p.command)
LogHelper.success(`spaCy model installed\n`)
} catch (e) {
LogHelper.info(e.command)
report.has_spacy_model.v = false
if (
p.command.indexOf('node --version') !== -1 &&
!semver.satisfies(semver.clean(p.stdout), `>=${nodeMinRequiredVersion}`)
) {
Object.keys(report).forEach((item) => {
if (report[item].type === 'error') report[item].v = false
})
LogHelper.error(
'No spaCy model is installed. It is recommended to run the following command: "npm run clean:python-deps"\n'
`${p.stdout}\nThe Node.js version must be >=${nodeMinRequiredVersion}. Please install it: https://nodejs.org (or use nvm)\n`
)
} else if (
p.command.indexOf('npm --version') !== -1 &&
!semver.satisfies(semver.clean(p.stdout), `>=${npmMinRequiredVersion}`)
) {
Object.keys(report).forEach((item) => {
if (report[item].type === 'error') report[item].v = false
})
LogHelper.error(
`${p.stdout}\nThe npm version must be >=${npmMinRequiredVersion}. Please install it: https://www.npmjs.com/get-npm (or use nvm)\n`
)
} else {
LogHelper.success(`${p.stdout}\n`)
if (p.command.includes('node --version')) {
reportDataInput.environment.nodeVersion = p.stdout
} else if (p.command.includes('npm --version')) {
reportDataInput.environment.npmVersion = p.stdout
}
}
})
/**
* Skill execution checking
*/
LogHelper.success(`Python bridge 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} "${path.join(
process.cwd(),
'scripts',
'assets',
'intent-object.json'
)}"`,
{ shell: true }
)
const executionEnd = Date.now()
const executionTime = executionEnd - executionStart
LogHelper.info(p.command)
reportDataInput.pythonBridge.command = p.command
LogHelper.success(p.stdout)
reportDataInput.pythonBridge.output = p.stdout
LogHelper.info(`Skill execution time: ${executionTime}ms\n`)
reportDataInput.pythonBridge.executionTime = `${executionTime}ms`
} catch (e) {
LogHelper.info(e.command)
report.can_run_skill.v = false
LogHelper.error(`${e}\n`)
reportDataInput.pythonBridge.error = JSON.stringify(e)
}
/**
* TCP server startup checking
*/
LogHelper.success(`TCP server 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)
reportDataInput.tcpServer.command = tcpServerCommand
if (osInfo.platform === 'darwin') {
LogHelper.info(
'For the first start, it may take a few minutes to cold start the TCP server on macOS. No worries it is a one-time thing'
)
}
let tcpServerOutput = ''
p.stdout.on('data', (data) => {
const newData = data.toString()
tcpServerOutput += newData
if (newData?.toLowerCase().includes('waiting for')) {
kill(p.pid)
LogHelper.success('The TCP server can successfully start')
}
})
p.stderr.on('data', (data) => {
const newData = data.toString()
// 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
// In case it takes too long, force kill
setTimeout(() => {
kill(p.pid)
const error = `The TCP server timed out after ${timeout}ms`
LogHelper.error(error)
reportDataInput.tcpServer.error = error
report.can_start_tcp_server.v = false
}, timeout)
p.stdout.on('end', async () => {
const tcpServerEnd = Date.now()
reportDataInput.tcpServer.output = tcpServerOutput
reportDataInput.tcpServer.startTime = `${tcpServerEnd - tcpServerStart}ms`
LogHelper.info(
`TCP server startup time: ${reportDataInput.tcpServer.startTime}\n`
)
/**
* Global resolvers NLP model checking
@@ -211,18 +314,25 @@ export default () =>
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'
report.can_text.v = false
Object.keys(report).forEach((item) => {
if (item.indexOf('stt') !== -1 || item.indexOf('tts') !== -1)
report[item].v = false
})
LogHelper.error(
'Global resolvers NLP model not found or broken. Try to generate a new one: "npm run train"\n'
`${state}. Try to generate a new one: "npm run train"\n`
)
reportDataInput.nlpModels.globalResolversModelState = state
} else {
LogHelper.success('Found and valid\n')
const state = 'Found and valid'
LogHelper.success(`${state}\n`)
reportDataInput.nlpModels.globalResolversModelState = state
}
/**
@@ -233,18 +343,25 @@ export default () =>
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'
report.can_text.v = false
Object.keys(report).forEach((item) => {
if (item.indexOf('stt') !== -1 || item.indexOf('tts') !== -1)
report[item].v = false
})
LogHelper.error(
'Skills resolvers NLP model not found or broken. Try to generate a new one: "npm run train"\n'
`${state}. Try to generate a new one: "npm run train"\n`
)
reportDataInput.nlpModels.skillsResolversModelState = state
} else {
LogHelper.success('Found and valid\n')
const state = 'Found and valid'
LogHelper.success(`${state}\n`)
reportDataInput.nlpModels.skillsResolversModelState = state
}
/**
@@ -255,18 +372,24 @@ export default () =>
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'
report.can_text.v = false
Object.keys(report).forEach((item) => {
if (item.indexOf('stt') !== -1 || item.indexOf('tts') !== -1)
report[item].v = false
})
LogHelper.error(
'Main NLP model not found or broken. Try to generate a new one: "npm run train"\n'
`${state}. Try to generate a new one: "npm run train"\n`
)
reportDataInput.nlpModels.mainModelState = state
} else {
LogHelper.success('Found and valid\n')
const state = 'Found and valid'
LogHelper.success(`${state}\n`)
reportDataInput.nlpModels.mainModelState = state
}
/**
@@ -276,7 +399,7 @@ export default () =>
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 === ''
@@ -294,7 +417,7 @@ export default () =>
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)
@@ -315,7 +438,7 @@ export default () =>
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)
@@ -336,7 +459,7 @@ export default () =>
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`)
@@ -345,7 +468,7 @@ export default () =>
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)
@@ -394,7 +517,7 @@ export default () =>
report.can_run.v &&
report.can_run_skill.v &&
report.can_text.v &&
report.has_spacy_model.v
report.can_start_tcp_server.v
) {
LogHelper.success('Hooray! Leon can run correctly')
LogHelper.info(
@@ -404,9 +527,30 @@ export default () =>
LogHelper.error('Please fix the errors above')
}
resolve()
} catch (e) {
LogHelper.error(e)
reject()
}
})
reportDataInput.report = report
reportDataInput = JSON.parse(
SystemHelper.sanitizeUsername(JSON.stringify(reportDataInput))
)
LogHelper.title('REPORT URL')
LogHelper.info('Sending report...')
try {
const { data } = await axios.post('https://getleon.ai/api/report', {
report: reportDataInput
})
const { data: responseReportData } = data
LogHelper.success(`Report URL: ${responseReportData.reportUrl}`)
} catch (e) {
LogHelper.error(`Failed to send report: ${e}`)
}
process.exit(0)
})
} catch (e) {
LogHelper.error(e)
}
})()
+4 -4
View File
@@ -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
View File
@@ -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)
}
}
})()
+7 -7
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...')
@@ -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()
}
}
+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')
}
@@ -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}`)
}
})()
+13 -10
View File
@@ -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)
+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(
-16
View File
@@ -1,16 +0,0 @@
import { LoaderHelper } from '@/helpers/loader-helper'
import check from './check'
/**
* Execute the checking script
*/
;(async () => {
try {
LoaderHelper.start()
await check()
LoaderHelper.stop()
} catch (e) {
LoaderHelper.stop()
}
})()
+1 -1
View File
@@ -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}`)
}
})()
+1 -1
View File
@@ -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}`)
}
})()
+1 -1
View File
@@ -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}`)
}
})()
+2 -2
View File
@@ -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 -3
View File
@@ -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'
}
+5 -5
View File
@@ -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'
}
+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')
}
+7 -7
View File
@@ -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()
})
+9 -8
View File
@@ -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) {
+3 -3
View File
@@ -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)
+8 -6
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 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)
+25
View File
@@ -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'
]
)
+83 -15
View File
@@ -1,9 +1,10 @@
import path from 'node:path'
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()
@@ -11,16 +12,53 @@ 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'
)
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("'")
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'
@@ -34,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
@@ -47,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
@@ -58,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']
@@ -73,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
-77
View File
@@ -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
+77
View File
@@ -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()
}
)
})
}
}
+1
View File
@@ -0,0 +1 @@
export type ASRAudioFormat = 'wav' | 'webm'
-511
View File
@@ -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
+608
View File
@@ -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
})
}
}
})
}
}
+75
View File
@@ -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)
}
+154
View File
@@ -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()}`)
@@ -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
@@ -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
@@ -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
+38
View File
@@ -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()
-205
View File
@@ -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
+178
View File
@@ -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
}
}
+185
View File
@@ -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')
)
}
}
})
}
}
+263
View File
@@ -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()
})
}
}
+347
View File
@@ -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
}
}
+183
View File
@@ -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
}
}

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