Compare commits

...

218 Commits

Author SHA1 Message Date
louistiti 754bd7380f build(bridge/python): 1.2.0 2023-11-14 22:38:34 +08:00
louistiti d32cda3663 build(bridge/nodejs): 1.1.0 2023-11-14 22:35:27 +08:00
louistiti c8d2e8c985 chore: bump to 1.0.0-beta.10+dev 2023-11-12 22:22:56 +08:00
louistiti 2a15434919 Merge branch 'feat-improve-ner-types' into develop 2023-11-12 18:16:37 +08:00
louistiti b2c375861b feat: identify entities from utterance samples for a better prioritization of classifications 2023-11-12 18:14:16 +08:00
Théo LUDWIG ae6fec335d fix(skill/date_time): add follow up utterance samples 2023-07-24 23:00:30 +02:00
louistiti 16302ec6e4 Merge branch 'develop' into feat-improve-ner-types 2023-07-23 11:26:38 +08:00
Louis Grenard 4d03e1bb4e docs(README): add knowledge base LLM scenario 2023-07-23 11:15:23 +08:00
Théo LUDWIG 69ce669b4c feat(tcp server): remove unnecessary data from entity + detect most populated city 2023-07-02 11:09:22 +02:00
Théo LUDWIG c512b422e7 feat(web app): add "Show more" when answer is "too big" 2023-07-01 23:33:41 +02:00
Théo LUDWIG 2ad6541c3e feat(skill/help): helps you get started with Leon 2023-07-01 23:28:55 +02:00
Théo LUDWIG 379326d7c1 feat(bridge/nodejs): simplify leon.setAnswerData 2023-07-01 23:24:29 +02:00
Théo LUDWIG 1359f5d210 feat(bridge/python): simplify leon.set_answer_data 2023-07-01 23:23:34 +02:00
Théo LUDWIG 64858ec6fe feat(tcp server): add Country information to City 2023-07-01 19:01:57 +02:00
Théo LUDWIG 166175192d feat(tcp server): better detect cities thanks to alternate names 2023-07-01 19:00:34 +02:00
Théo LUDWIG 97de1d1c3f fix(skill/date_time): correct time zone with toLocaleString method 2023-07-01 18:24:19 +02:00
louistiti 563792bd0a fix(tcp server): use root path for the time_zones.txt file 2023-06-30 22:35:58 +08:00
Théo LUDWIG 076db4c3eb fix(skill/date_time): determine correctly isDaylightSavingTime 2023-06-30 15:11:17 +02:00
Théo LUDWIG 02e1de8ffa fix: precise units used in time zone data 2023-06-29 23:14:14 +02:00
Théo LUDWIG cc01bd18b2 feat(skill/date_time): time zone 2023-06-29 23:03:03 +02:00
Théo LUDWIG 2478adbaa2 feat(server): add spacy data in entity resolution 2023-06-29 22:33:25 +02:00
Théo LUDWIG 45b177d0fb feat(tcp server): add support for time zone and more city/country data 2023-06-29 22:32:12 +02:00
Théo LUDWIG c0deafcd69 feat(server): improve NER types 2023-06-29 00:04:37 +02:00
louistiti eea6b0392d feat(skill/date_time): add new utterance samples 2023-06-29 00:17:29 +08:00
louistiti 9845612c68 Merge branch 'feat/skill-utilities-date-time' into develop 2023-06-28 23:39:25 +08:00
louistiti d46ec1d3d5 Merge branch 'feat/skill-leon-age' into develop 2023-06-28 23:39:15 +08:00
louistiti f1a65f53bc feat(skill/age): add new utterance samples 2023-06-28 23:39:07 +08:00
Théo LUDWIG 1410b72362 chore(skill/date_time): add test folder 2023-06-27 18:35:03 +02:00
Théo LUDWIG e9245b522f fix(skill/date_time): separate current date and current time 2023-06-25 22:46:10 +02:00
Théo LUDWIG 636eb0f139 feat(skill/date_time): days countdown 2023-06-25 12:27:30 +02:00
Théo LUDWIG 8645ef01fd fix: improve types for Node.js skill SDK 2023-06-25 12:08:36 +02:00
Théo LUDWIG 18867bde89 fix(server): improve json schema description for skill config 2023-06-25 12:07:41 +02:00
Théo LUDWIG d061a6c4bf feat(skill/date_time): get current week number of the year 2023-06-24 19:57:47 +02:00
Théo LUDWIG e2ec76275a feat(skill/date_time): get current date and time 2023-06-24 19:30:55 +02:00
Théo LUDWIG 767bace201 feat(skill/age): ask Leon his age 2023-06-24 13:23:19 +02:00
louistiti e70d0a0029 feat(web app): add init messages 2023-05-30 23:52:35 +08:00
louistiti 595fe78679 Merge branch 'python-sdk' into develop 2023-05-29 22:55:36 +08:00
louistiti 69cb6a610f build(bridge/nodejs): bump to 1.0.0 2023-05-29 22:01:32 +08:00
louistiti 10a03d90e3 build(bridge/python): bump to 1.1.0 2023-05-29 22:01:06 +08:00
louistiti 251950384e refactor(server): rename obj to skillAnswer 2023-05-28 12:53:44 +08:00
louistiti 8c91d245e8 refactor(server): remove speech prop from skill output backward compatibility 2023-05-28 12:49:56 +08:00
louistiti df343ce94f fix(skill/akinator): typo ShowNextActionSuggestions to showNextActionSuggestions 2023-05-28 10:12:40 +08:00
louistiti 7c73f435af fix(skill/akinator): disable urllib3 cert warning 2023-05-28 10:07:23 +08:00
louistiti b5ef7242af fix(skill/have_i_been_pwned): use current_entities instead of entities 2023-05-28 09:23:50 +08:00
Divlo 16e1f7a34a chore: remove dead code 2023-05-28 01:32:57 +02:00
Divlo e42a54d593 refactor(skill/akinator): usage of new SDK 2023-05-28 01:10:03 +02:00
Divlo cd3bcd5d6e refactor(server): leon over http with /api/utterance 2023-05-28 00:31:40 +02:00
Divlo 2ea6237b1f chore: remove /api/downloads 2023-05-27 23:53:20 +02:00
Divlo 9782526c42 chore: remove legacy Python bridge/utils 2023-05-27 23:48:39 +02:00
Divlo 3bf7150387 refactor(skill/have_i_been_pwned): usage of new SDK 2023-05-27 23:34:15 +02:00
Divlo c00c85fd4f fix(bridge): better types and network error 2023-05-27 20:29:01 +02:00
Divlo a76d05f68a refactor(skill/product_hunt_trends): usage of new SDK 2023-05-27 20:21:03 +02:00
Divlo f17a4d3a2a refactor(skill/github_trends): usage of new SDK 2023-05-27 18:06:31 +02:00
Divlo 850ea46ec6 style(skill/mbti): add missing space for consistency (oops) 2023-05-27 12:54:16 +02:00
Divlo a540307241 refactor(skill/mbti): usage of new SDK 2023-05-27 12:43:19 +02:00
Divlo e440e4f715 refactor(skill/speed_test): usage of new SDK 2023-05-27 12:00:13 +02:00
louistiti d1fe81ddc6 refactor: modify settings method from isAlreadySet to isSettingSet 2023-05-27 09:47:09 +08:00
louistiti 3648fef5fb refactor(bridge/python): add key to settings.isAlreadySet 2023-05-27 09:00:00 +08:00
louistiti 1f9137bb20 refactor(bridge/nodejs): add key to setting.isAlreadySet 2023-05-27 08:47:24 +08:00
louistiti 2e6abef507 chore: lint 2023-05-27 08:06:11 +08:00
louistiti a55fbcd532 fix: ignore error if skill.json isn't found when getting skill info 2023-05-27 08:05:22 +08:00
Divlo 924317c614 refactor(skill/is_it_down): usage of new SDK 2023-05-27 00:14:51 +02:00
Divlo 0c44916aa2 chore(bridge): add comments (jsdoc/docstring) for settings 2023-05-26 23:50:36 +02:00
Divlo 2a0cc0644e chore: remove youtube_downloader skill 2023-05-26 23:34:37 +02:00
Divlo 4f5b385fd7 chore: rename config.json to settings.json 2023-05-26 23:31:25 +02:00
Divlo 30e405bbe3 feat(bridge/python): add Settings module 2023-05-26 23:03:02 +02:00
Divlo 8c105a043d feat(bridge/nodejs): add Settings module 2023-05-26 23:01:54 +02:00
Divlo 99b5e7ab6d chore: remove synchronization capability 2023-05-26 22:58:26 +02:00
Divlo b9dbc5012c Revert "chore: add __init__.py files to avoid mypy linting error"
This reverts commit 0b16eebfbd.
2023-05-26 10:12:43 +02:00
Divlo 0b16eebfbd chore: add __init__.py files to avoid mypy linting error
Ref: https://github.com/python/mypy/issues/8686#issuecomment-1178977365
2023-05-25 22:56:38 +02:00
Divlo c078505846 refactor(skill/todo_list): usage of new SDK 2023-05-25 22:47:58 +02:00
Divlo 421232a6dc refactor(skills): usage of datetime ISO 8601 string format instead of timestamp 2023-05-25 19:05:51 +02:00
louistiti c62368ddad chore: comments and French punctuation 2023-05-25 08:34:20 +08:00
Divlo f956e1abc6 fix(bridge): memory path always defined 2023-05-25 01:04:28 +02:00
Divlo dc88ac244d perf(skill/guess_the_number): only store one game at a time to avoid increasing the size of memory unnecessarily 2023-05-25 00:38:34 +02:00
Divlo 4cd01a4baf refactor(skill/random_number): usage of new SDK 2023-05-25 00:12:59 +02:00
Divlo 06cda4a1f0 fix(bridge/nodejs): allow memory that stores null value 2023-05-25 00:04:25 +02:00
Divlo dc7615723d fix(bridge/python): allow memory that stores null value 2023-05-24 23:57:44 +02:00
Divlo 14275be5f7 refactor(skill/introduction): usage of new SDK 2023-05-24 23:57:18 +02:00
Divlo 158df2f0f1 refactor(skill/greeting): usage of new SDK 2023-05-24 23:57:05 +02:00
Divlo 6bed516f89 refactor(skill/rochambeau): usage of new SDK 2023-05-24 23:22:43 +02:00
Divlo 7e72928ad6 refactor(skill/guess_the_number): usage of new SDK 2023-05-24 23:10:14 +02:00
louistiti 4a7e77141e refactor: remove Python shebangs 2023-05-24 20:32:43 +08:00
louistiti 25ab60e5a2 feat: add Leon user agent from bridges 2023-05-24 19:53:34 +08:00
Divlo f8e6012cbe fix(bridge/python): add types 2023-05-23 19:50:19 +02:00
louistiti 1c5d2874b0 refactor(bridge/python): add memory comments 2023-05-23 21:37:12 +08:00
louistiti 3449c14d47 feat(bridge/python): functional memory 2023-05-23 21:27:35 +08:00
louistiti 51f57e3255 feat(bridge/python): memory write method 2023-05-23 20:10:48 +08:00
louistiti 7fd4be1248 feat(bridge/python): memory read method 2023-05-23 20:09:09 +08:00
louistiti 3b77d64d4f feat(bridge/python): memory clear method 2023-05-23 20:06:35 +08:00
louistiti ab7e2a9b10 refactor(bridge/nodejs): remove Aurora index file 2023-05-22 22:54:00 +08:00
louistiti b2e4ebf1be feat(bridge/python): support widget answer 2023-05-22 22:17:53 +08:00
louistiti 3c9a210081 feat(bridge/python): leon.answer usage 2023-05-22 21:14:52 +08:00
louistiti 7a9068283b refactor(bridge/python): new bridge main file 2023-05-22 20:32:36 +08:00
louistiti 66dc0aab0f feat(skill/timekeeper): boilerplate 2023-05-22 20:18:13 +08:00
Divlo 91e48af263 feat(bridge/python): add Network module (WIP) 2023-05-21 23:20:58 +02:00
louistiti 3a230633d8 feat(bridge/python): memory kick off 2023-05-21 19:12:30 +08:00
louistiti 6c3a03d16a feat(bridge/python): add widget class 2023-05-21 19:03:54 +08:00
louistiti 8a60b3d0d3 Merge branch 'python-sdk-fixes' into python-sdk 2023-05-21 18:51:42 +08:00
Divlo cb3d3bf5a8 fix(bridge/nodejs): add lang to action params 2023-05-20 20:34:12 +02:00
Divlo f3257ad7b0 style(bridge/python): format with autopep8 2023-05-20 20:33:37 +02:00
Divlo cb29848f45 fix(bridge/python): better typings 2023-05-20 19:49:56 +02:00
Divlo ebaa3c4f9b fix(bridge/python): inconsistent indentation TabError 2023-05-20 19:49:07 +02:00
louistiti de17719537 feat(bridge/python): typing (WIP) 2023-05-20 23:28:44 +08:00
louistiti 47d5a48ac9 fix(bridge/python): import INTENT_OBJECT to leon 2023-05-20 23:17:42 +08:00
louistiti e8ceed62eb feat(bridge/python): leon.answer 2023-05-20 23:16:50 +08:00
louistiti bbc1f8a79c feat(bridge/python): leon.set_answer_data 2023-05-20 23:09:45 +08:00
louistiti 289efb5610 feat(bridge/python): leon.get_src_config 2023-05-20 22:49:17 +08:00
louistiti 05a4b2c773 feat(bridge/python): Leon singleton 2023-05-20 22:42:30 +08:00
louistiti a32ad86342 feat(bridge/python): add constants 2023-05-20 22:40:38 +08:00
louistiti a2eacc3930 fix(server): add lang back to the intent object till Python bridge is updated 2023-05-19 22:09:01 +08:00
louistiti a8c079fd5c feat: provide built-in context data to skills 2023-05-19 21:47:23 +08:00
louistiti b389bdd8c6 feat(server): provide sentiment analysis to NLU result 2023-05-19 21:40:24 +08:00
louistiti 750d1de8b4 chore: merge branch 'skill-text-n-speech-difference' into develop 2023-05-18 21:14:09 +08:00
louistiti d295105fa6 chore: disable import/order ESLint rule for skills 2023-05-18 21:06:04 +08:00
louistiti e2899c8d9a refactor(skill/good_bye): rollback as speech/text answer is only supported in logic skills 2023-05-18 20:38:26 +08:00
louistiti 7f9b60a6cd feat(server): differentiate skill text answer from speech answer (WIP) 2023-05-18 00:00:40 +08:00
louistiti 9e77953976 refactor(bridge/nodejs): use AnswerConfig type from core schema 2023-05-17 22:21:11 +08:00
louistiti cb6462b749 feat: define new skill answer format 2023-05-17 18:31:39 +08:00
Louis Grenard 07a3b1701d docs: update README.md 2023-05-16 23:14:02 +08:00
Louis Grenard f6300a3fb8 docs: update README.md 2023-05-16 23:14:02 +08:00
louistiti 46299ef4eb Merge branch 'skill-widget-ideas' into develop 2023-05-16 23:08:00 +08:00
louistiti 259dc53213 Merge branch 'skill-widget-ideas' into develop 2023-05-16 23:08:00 +08:00
louistiti 51152a0fe3 refactor(bridge/nodejs): protected memory constructor 2023-05-16 23:07:12 +08:00
louistiti 01305af669 refactor(bridge/nodejs): protected memory constructor 2023-05-16 23:07:12 +08:00
Divlo 5539130dd9 fix: typo missing "GB" for Free RAM 2023-05-15 23:42:27 +02:00
Divlo efd1061f30 fix: typo missing "GB" for Free RAM 2023-05-15 23:42:27 +02:00
Divlo f6a501d4e1 fix(bridge/nodejs): improve Memory types with external skill memory 2023-05-15 22:58:40 +02:00
Divlo 839c7bfc3c fix(bridge/nodejs): improve Memory types with external skill memory 2023-05-15 22:58:40 +02:00
Divlo d3ba85cb36 feat(bridge/nodejs): add Widget abstract class 2023-05-15 21:18:54 +02:00
Divlo 623039415a feat(bridge/nodejs): add Widget abstract class 2023-05-15 21:18:54 +02:00
louistiti 7a75c985e0 Merge branch 'skill-widget-poc' into develop 2023-05-15 21:31:02 +08:00
louistiti 0f2a579a66 refactor(web app): remove original app rendering 2023-05-15 21:30:42 +08:00
louistiti 63573083fa feat(web app): render widget 2023-05-15 21:25:20 +08:00
louistiti 34aa188643 feat: skill -> core -> client widget support 2023-05-15 01:13:18 +08:00
louistiti 76dfe72f5f feat(web app): kick off Aurora structure 2023-05-15 00:00:21 +08:00
louistiti 7a31158497 feat(web app): set up React 2023-05-14 23:43:42 +08:00
louistiti 96145bb2f7 feat(web app): add vite React plugin 2023-05-14 23:23:17 +08:00
louistiti 17cb808aab chore: install required npm deps 2023-05-14 23:22:01 +08:00
louistiti b757d2d4ae chore: upgrade vite to latest 2023-05-14 23:20:41 +08:00
louistiti ce001c2e65 feat(bridge/nodejs): support read memory from other skills 2023-05-14 21:00:52 +08:00
louistiti a3b8c40714 refactor(scripts): unify skills setup over one iteration only 2023-05-14 12:10:55 +08:00
louistiti 0283878a90 refactor(bridge/nodejs): force run to be the skill action entry point 2023-05-14 09:15:05 +08:00
louistiti 3ed19bd6d0 Merge branch 'nodejs-bridge-memory' into develop 2023-05-14 08:58:28 +08:00
louistiti c163f5971c feat(bridge/nodejs): sync return new memory value on write 2023-05-14 08:58:19 +08:00
louistiti 6a01e1e743 Merge branch 'nodejs-bridge-improvements' into nodejs-bridge-memory 2023-05-14 08:52:03 +08:00
louistiti b06eab9107 style(bridge/nodejs): memory exception/error renaming 2023-05-14 08:51:52 +08:00
Divlo 12ce47b937 fix(bridge/nodejs): usage of getleon.ai to try network available 2023-05-14 00:39:56 +02:00
Divlo c8fc0068d1 feat(bridge/nodejs): improvements to memory 2023-05-14 00:39:30 +02:00
louistiti e32bf370db Merge branch 'nodejs-bridge-memory' into develop 2023-05-14 00:11:10 +08:00
louistiti 9ef4f234b8 Merge branch 'develop' into nodejs-bridge-memory 2023-05-14 00:08:18 +08:00
louistiti 6d2292f508 fix(server): pre-check add process.cwd() to all constant paths 2023-05-14 00:06:43 +08:00
louistiti b897ae160f feat(bridge/nodejs): simple get / set memory 2023-05-13 23:52:54 +08:00
louistiti a51d92bf08 refactor(bridge/nodejs): remove Prisma 2023-05-13 19:04:54 +08:00
louistiti 6d495cf43c refactor(scripts): explicit Node.js bridge npm packages install 2023-05-13 00:30:15 +08:00
louistiti d6933a9e35 refactor(bridge/nodejs): remove lowdb and tiny custom ORM in favor of Prisma 2023-05-13 00:26:51 +08:00
louistiti 127a10f8ed feat(scripts): install Node.js bridge npm packages on setup 2023-05-12 23:54:57 +08:00
louistiti ef223a2901 feat(bridge/nodejs): make use of Prisma from skill action (PoC) 2023-05-11 23:39:17 +08:00
louistiti 9a06c64a5d feat(bridge/nodejs): Prisma SQLite setup 2023-05-11 23:26:42 +08:00
louistiti ad5edb5ad5 feat(bridge/nodejs): support nested props find in memory 2023-05-11 22:30:22 +08:00
louistiti ee0b080522 feat(bridge/nodejs): support updateOne memory method 2023-05-11 21:50:29 +08:00
louistiti be04b10645 feat(bridge/nodejs): support findMany memory method 2023-05-11 21:41:51 +08:00
louistiti ab62a65e57 feat(bridge/nodejs): support findOne memory method 2023-05-11 21:40:22 +08:00
louistiti 99ac5f18c8 feat(bridge/nodejs): give public access to Low instance 2023-05-11 21:21:26 +08:00
louistiti 87a7077854 refactor(bridge/nodejs): rename create with createOne 2023-05-11 21:04:18 +08:00
louistiti af2a124165 refactor(bridge/nodejs): rename insert to create 2023-05-11 20:59:26 +08:00
louistiti 8e55c481d4 feat(bridge/nodejs): support insertMany memory method 2023-05-11 20:51:19 +08:00
louistiti d7b814761f feat(bridge/nodejs): tiny ORM kick off 2023-05-11 20:49:40 +08:00
louistiti 4dde49e545 feat(bridge/nodejs): memory database pattern (WIP) 2023-05-10 23:29:41 +08:00
Divlo 34b29d0e61 fix(bridge/nodejs): memory ESM dynamic imports 2023-05-10 00:45:21 +02:00
louistiti 954126315f refactor(bridge/nodejs): TSConfig ESM NCC 2023-05-09 21:48:07 +08:00
louistiti e240a2db3c feat(bridge/nodejs): memory ESM debugging (WIP) 2023-05-09 21:39:18 +08:00
louistiti 4abb68c1b3 Merge branch 'nodejs-bridge' into develop 2023-05-08 22:05:42 +08:00
louistiti 3ea1a3666b feat(bridge/nodejs): add isNetworkAvailable network method 2023-05-08 22:03:30 +08:00
louistiti d3e0810298 fix(bridge/nodejs): temporize data buffer on output 2023-05-08 22:01:46 +08:00
louistiti ef6d46cf8e refactor(scripts): remove cmd output on install Node.js skills packages 2023-05-08 21:57:41 +08:00
louistiti e65ecd5b67 fix(scripts): install Node.js skills packages timestamp comparison 2023-05-08 21:45:06 +08:00
louistiti 5946a00427 feat(scripts): skill package install/uninstall package script 2023-05-08 21:35:14 +08:00
louistiti 6b239ae38b style(scripts): fix linting 2023-05-08 21:24:26 +08:00
louistiti 224158812f feat(scripts): install Node.js skills packages 2023-05-08 21:23:33 +08:00
Divlo ee9794cd9f Revert "feat(bridge/nodejs): add leon.getVersions"
This reverts commit ec819e1765.
2023-05-08 10:58:31 +02:00
Divlo ad497d36c3 style: fix linting 2023-05-07 21:44:55 +02:00
Divlo 5eb62ddcf5 chore: add leon-nodejs-bridge as local dependency 2023-05-07 21:43:03 +02:00
Divlo ec819e1765 feat(bridge/nodejs): add leon.getVersions 2023-05-07 21:38:09 +02:00
louistiti 2c2f49290a Merge branch 'nodejs-bridge' into develop 2023-05-07 21:28:28 +08:00
louistiti f936652007 Merge branch 'nodejs-bridge-improvements' into nodejs-bridge 2023-05-07 21:26:12 +08:00
Divlo 950985ca86 feat(bridge/nodejs): finish network implementation 2023-05-07 15:07:20 +02:00
louistiti 8147d8423b feat(scripts): add Node.js bridge on check script 2023-05-07 20:09:37 +08:00
louistiti 3f556494c2 Merge branch 'nodejs-bridge-improvements' into nodejs-bridge 2023-05-07 19:07:05 +08:00
Divlo 426c816ca4 build(bridge/nodejs): avoid installing dependencies each time we build 2023-05-07 01:14:44 +02:00
louistiti dd0eab51b6 Merge branch 'HEAD' into nodejs-bridge 2023-05-06 22:47:36 +08:00
Divlo 07e253f663 build(bridge/nodejs): usage of ncc 2023-05-06 15:38:49 +02:00
Divlo de08807f1a feat(bridge/nodejs): add Network module (WIP) 2023-05-06 14:21:24 +02:00
Divlo 2cb030f47b fix(bridge/nodejs): usage of types from server/core/brain/types 2023-05-06 14:03:56 +02:00
Divlo 673cf0426f fix(bridge/nodejs): build error 2023-05-06 13:55:43 +02:00
louistiti e89b5b0ea3 refactor(bridge/nodejs): isolate types from bridge 2023-05-06 18:26:22 +08:00
Divlo 6b76e6a102 fix(bridge/nodejs): stricter leon.getSRCConfig types 2023-05-05 20:24:01 +02:00
Divlo 1625aaf3cd fix(bridge/nodejs): typings improvements 2023-05-05 20:00:51 +02:00
louistiti b65cfb70d6 feat(bridge/nodejs): final answer bridge 2023-05-05 21:21:27 +08:00
louistiti 89a6c04a1e refactor(bridge/nodejs): new answer types (WIP) 2023-05-05 00:34:16 +08:00
louistiti e42655a5ff refactor(server): remove skill answer type complexity 2023-05-04 22:34:29 +08:00
louistiti c9678fe34f refactor(bridge/python): flush standard output every time 2023-05-04 21:40:11 +08:00
louistiti 8349e41205 feat(bridge/nodejs): answers 2023-05-03 23:28:12 +08:00
louistiti 759e67ac40 fix(bridge/nodejs): SDK mapping 2023-05-03 16:02:57 +08:00
louistiti fc1fa56eab Merge branch 'develop' into nodejs-bridge 2023-05-03 14:54:51 +08:00
louistiti 8f5bd3a9a5 feat: add free RAM check 2023-05-03 14:54:23 +08:00
louistiti a836a7cc8c feat(bridge/nodejs): resolve SDK mapping 2023-05-03 09:27:23 +08:00
louistiti 2df22770e0 refactor(bridge/nodejs): SDK mapping 2023-05-03 08:39:06 +08:00
louistiti 9b839d3e66 feat(bridge/nodejs): first draft 2023-05-02 23:53:41 +08:00
louistiti 2e3796642d refactor(server): send heartbeat every 6 hours instead 2023-05-02 16:43:04 +08:00
louistiti b027905ed0 refactor(bridge/nodejs): package.json new dist entry point path 2023-05-01 23:01:46 +08:00
louistiti 4c9dfe667b build(bridge/nodejs): bump 0.0.1-alpha.0 2023-05-01 22:49:32 +08:00
louistiti ed215feb81 refactor(scripts): Node.js bridge bin dist 2023-05-01 22:48:31 +08:00
louistiti cd82a3a11b chore: bump to 1.0.0-beta.9+dev 2023-05-01 10:23:19 +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
201 changed files with 5780 additions and 4037 deletions
-1
View File
@@ -12,5 +12,4 @@ end_of_line = lf
insert_final_newline = true
[*.py]
indent_style = tab
indent_size = 4
+6
View File
@@ -67,6 +67,12 @@
]
},
"overrides": [
{
"files": ["skills/**/*.ts"],
"rules": {
"import/order": "off"
}
},
{
"files": ["*.ts"],
"rules": {
+2 -4
View File
@@ -8,7 +8,6 @@ __pycache__/
test/coverage/
**/tmp/*
**/src/.venv/*
downloads/*
logs/*
core/config/**/*.json
bin/coqui/*
@@ -21,6 +20,7 @@ package-lock.json
npm-debug.log
debug.log
.env
.last-skill-npm-install
leon.json
bridges/python/src/Pipfile.lock
tcp_server/src/Pipfile.lock
@@ -28,9 +28,7 @@ tcp_server/src/Pipfile.lock
!bridges/python/**/.gitkeep
!bridges/nodejs/**/.gitkeep
!**/*.sample*
packages/**/config/config.json
skills/**/src/config.json
packages/**/data/db/*.json
skills/**/src/settings.json
skills/**/memory/*.json
core/data/models/*.nlp
package.json.backup
+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._
+12 -11
View File
@@ -36,7 +36,7 @@ _<p align="center">Your open-source personal assistant.</p>_
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.
While I would love to devote more time to Leon, I'm currently unable to do so because I have bills to pay. 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 🙂
@@ -50,24 +50,25 @@ 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 features: let skills leverage LLMs to provide out-of-the-box NLP features such as summarization, knowledge base, 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.
Once the new core released, we'll work on the community aspect of Leon. For example, better organize [our Discord](https://discord.gg/MNQqqKg), planify regular calls, work on skills together, etc. It is very important for Leon to have a real community. At that moment, the skills platform will already be online, so it'll be easier to sync our progress and publish new skills.
- Feel free to check out the Git development branches and our [next major milestones](https://blog.getleon.ai/a-much-better-nlp-and-future-1-0-0-beta-7/#whats-next).
- And the [detailed roadmap](http://roadmap.getleon.ai).
- Many exciting things are coming up, hence no new documentation and test are going to be written until the official release of Leon.
<h2 align="center">📢 Notice 📢</h2>
<p align="center">
<a href="https://blog.getleon.ai/a-much-better-nlp-and-future-1-0-0-beta-7/"><img width="400" src="https://blog.getleon.ai/static/a62ac28a01cb6898e299dced40875a68/c1b63/beta-7.png" /></a>
<br>
Many exciting things are coming up, hence no new documentation and test are going to be written until the official release of Leon. Feel free to <a href="https://discord.gg/MNQqqKg"><b>join us on Discord</b></a> to know more and to read the <a href="https://blog.getleon.ai/a-much-better-nlp-and-future-1-0-0-beta-7/"><b>"A Much Better NLP and Future" blog post</b></a>.
</p>
<br><br>
---
## Latest Release
Check out the [latest release blog post](https://blog.getleon.ai/binaries-and-typescript-rewrite-1-0-0-beta-8/).
<a href="https://blog.getleon.ai/binaries-and-typescript-rewrite-1-0-0-beta-8/"><img width="400" src="https://blog.getleon.ai/static/a0d1cbafd1968e7531dc17e229f8cc61/aa440/beta-8.png" /></a>
---
@@ -225,7 +226,7 @@ You'll find a write-up on this [blog post](https://blog.getleon.ai/the-story-beh
<sub><sup>10 USD / month</sup></sub>
</td>
<td align="center" valign="middle" width="128">
<a href="http://sponsor.getleon.ai/">
<a href="http://sponsor.getleon.ai/">
You?
</a>
</td>
+12
View File
@@ -287,6 +287,17 @@ small {
.bubble-container.leon {
text-align: left;
}
.show-more {
margin: 3px;
text-decoration: underline;
}
.show-more:hover {
cursor: pointer;
text-decoration: none;
}
.show-all {
max-height: 100% !important;
}
.bubble {
padding: 6px 12px;
@@ -297,6 +308,7 @@ small {
text-align: left;
opacity: 0;
animation: fadeIn 0.2s ease-in forwards;
overflow: hidden;
}
#feed .me .bubble {
background-color: #1c75db;
+1
View File
@@ -9,6 +9,7 @@
</head>
<body class="settingup">
<main>
<div id="root"></div>
<div id="feed">
<p id="no-bubble" class="hide">
You can start to interact with Leon, don't be shy.
+9
View File
@@ -0,0 +1,9 @@
function App() {
return (
<>
<p>Hello from React</p>
</>
)
}
export default App
+9
View File
@@ -0,0 +1,9 @@
import type React from 'react'
interface Props {
children: React.ReactNode
}
export function Button({ children }: Props) {
return <button>{children}</button>
}
+22
View File
@@ -1,3 +1,5 @@
const MAXIMUM_HEIGHT_TO_SHOW_SEE_MORE = 340
export default class Chatbot {
constructor() {
this.et = new EventTarget()
@@ -60,6 +62,9 @@ export default class Chatbot {
}
loadFeed() {
/**
* TODO: widget: load widget from local storage
*/
return new Promise((resolve) => {
if (this.parsedBubbles === null || this.parsedBubbles.length === 0) {
this.noBubbleMessage.classList.remove('hide')
@@ -92,6 +97,23 @@ export default class Chatbot {
this.feed.appendChild(container).appendChild(bubble)
if (container.clientHeight > MAXIMUM_HEIGHT_TO_SHOW_SEE_MORE) {
bubble.style.maxHeight = `${MAXIMUM_HEIGHT_TO_SHOW_SEE_MORE}px`
const showMore = document.createElement('p')
const showMoreText = 'Show more'
showMore.className = 'show-more'
showMore.innerHTML = showMoreText
container.appendChild(showMore)
showMore.addEventListener('click', () => {
bubble.classList.toggle('show-all')
showMore.innerHTML =
showMore.innerHTML === showMoreText ? 'Show less' : showMoreText
})
}
if (save) {
this.saveBubble(who, string)
}
+50 -4
View File
@@ -1,6 +1,10 @@
import { io } from 'socket.io-client'
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Button } from './aurora/button'
import Chatbot from './chatbot'
import { INIT_MESSAGES } from './constants'
export default class Client {
constructor(client, serverUrl, input, res) {
@@ -31,6 +35,24 @@ export default class Client {
return this._recorder
}
async sendInitMessages() {
for (let i = 0; i < INIT_MESSAGES.length; i++) {
const messages = INIT_MESSAGES[i]
const message = messages[Math.floor(Math.random() * messages.length)]
const sendingDelay = Math.floor(Math.random() * 2000) + 1000
const typingFactorDelay = Math.floor(Math.random() * 4) + 2
setTimeout(() => {
this.chatbot.isTyping('leon', true)
}, sendingDelay / typingFactorDelay)
await new Promise((resolve) => setTimeout(resolve, sendingDelay))
this.chatbot.receivedFrom('leon', message)
this.chatbot.isTyping('leon', false)
}
}
init(loader) {
this.chatbot.init()
@@ -40,6 +62,10 @@ export default class Client {
this.socket.on('ready', () => {
loader.stop()
if (this.chatbot.parsedBubbles?.length === 0) {
this.sendInitMessages()
}
})
this.socket.on('answer', (data) => {
@@ -63,6 +89,30 @@ export default class Client {
cb('string-received')
})
this.socket.on('widget', (data) => {
/**
* TODO: widget: widget handler to core/skill; dynamic component rendering
*/
console.log('data', data)
const container = document.createElement('div')
container.className = 'widget'
this.chatbot.feed.appendChild(container)
const root = createRoot(container)
// TODO: widget: pass props and dynamic component loading according to type
const widgets = {
Button: (options) => {
return Button({
children: options.text
})
}
}
root.render(widgets[data.type](data.options))
})
this.socket.on('audio-forwarded', (data, cb) => {
const ctx = new AudioContext()
const source = ctx.createBufferSource()
@@ -108,10 +158,6 @@ export default class Client {
cb('audio-received')
})
this.socket.on('download', (data) => {
window.location = `${this.serverUrl}/api/v1/downloads?domain=${data.domain}&skill=${data.skill}`
})
if (this.history !== null) {
this.parsedHistory = JSON.parse(this.history)
}
+21
View File
@@ -0,0 +1,21 @@
export const INIT_MESSAGES = [
[
`Hello there! Glad to e-meet meet you, I'm Leon, your open-source personal assistant. While I'm still learning and improving, I promise to do my best to be helpful.`
],
[
`<ul>
<li>We've got lots in the works. Check out <a href="http://roadmap.getleon.ai/" target="_blank">our roadmap</a>.</li>
<li>Stay updated on our progress by checking out our <a href="https://blog.getleon.ai/" target="_blank">blog</a>.</li>
<li>You can also get updates straight to your inbox <a href="https://newsletter.getleon.ai/subscription/form" target="_blank">here</a>.</li>
`
],
[
`Come hang out with us <a href="https://discord.gg/MNQqqKg" target="_blank">on Discord</a>! Once we release our official version, our community will be working together to build new skills for me. You won't want to miss out on the fun!`
],
[
`At the moment, I'm not using a large language model, but we're planning to incorporate some in the future to improve my abilities. You can <a href="https://github.com/leon-ai/leon#how-about-large-language-models-and-leon" target="_blank">learn more about our plans here</a>.`
],
[
`Just so you know, my creator is working tirelessly to improve my skills and features, dedicating 75% of his free time to the project on top of his full-time job. If you'd like to help speed up my development, you can sponsor his work by clicking on this link: <strong><a href='http://sponsor.getleon.ai/' target='_blank'>sponsor.getleon.ai</a></strong>. Your support would mean a lot to us. Thank you for choosing me as your assistant!`
]
]
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"useDefineForClassFields": true,
"skipLibCheck": true,
"module": "ESNext",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
+3 -1
View File
@@ -2,6 +2,7 @@ import dns from 'node:dns'
import dotenv from 'dotenv'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
dotenv.config()
@@ -20,5 +21,6 @@ export default defineConfig({
},
server: {
port: 3000
}
},
plugins: [react()]
})
+8 -1
View File
@@ -1,7 +1,7 @@
{
"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",
"main": "dist/bin/leon-nodejs-bridge.js",
"author": {
"name": "Louis Grenard",
"email": "louis@getleon.ai",
@@ -11,5 +11,12 @@
"homepage": "https://getleon.ai",
"bugs": {
"url": "https://github.com/leon-ai/leon/issues"
},
"dependencies": {
"axios": "1.4.0",
"lodash": "4.17.21"
},
"devDependencies": {
"@types/lodash": "4.14.194"
}
}
+32
View File
@@ -0,0 +1,32 @@
import fs from 'node:fs'
import path from 'node:path'
import { SKILLS_PATH as SKILLS_ROOT_PATH } from '@/constants'
import type { SkillConfigSchema } from '@server/schemas/skill-schemas'
import type { IntentObject } from '@sdk/types'
const {
argv: [, , INTENT_OBJ_FILE_PATH]
} = process
export const INTENT_OBJECT: IntentObject = JSON.parse(
fs.readFileSync(INTENT_OBJ_FILE_PATH as string, 'utf8')
)
export const SKILL_PATH = path.join(
SKILLS_ROOT_PATH,
INTENT_OBJECT.domain,
INTENT_OBJECT.skill
)
export const SKILLS_PATH = SKILLS_ROOT_PATH
export const SKILL_CONFIG: SkillConfigSchema = JSON.parse(
fs.readFileSync(
path.join(
SKILL_PATH,
'config',
INTENT_OBJECT.extra_context_data.lang + '.json'
),
'utf8'
)
)
export { LEON_VERSION, NODEJS_BRIDGE_VERSION } from '@/constants'
+47 -3
View File
@@ -1,5 +1,49 @@
import { VERSION } from './version'
import path from 'node:path'
console.log('[WIP] Node.js bridge', VERSION)
import type { ActionFunction, ActionParams } from '@sdk/types'
import { INTENT_OBJECT } from '@bridge/constants'
;(async (): Promise<void> => {
const {
domain,
skill,
action,
lang,
utterance,
current_entities,
entities,
current_resolvers,
resolvers,
slots,
extra_context_data
} = INTENT_OBJECT
// TODO
const params: ActionParams = {
lang,
utterance,
current_entities,
entities,
current_resolvers,
resolvers,
slots,
extra_context_data
}
try {
const actionModule = await import(
path.join(
process.cwd(),
'skills',
domain,
skill,
'src',
'actions',
`${action}.ts`
)
)
const actionFunction: ActionFunction = actionModule.run
await actionFunction(params)
} catch (e) {
console.error(`Error while running "${skill}" skill "${action}" action:`, e)
}
})()
+12
View File
@@ -0,0 +1,12 @@
import { Widget } from '../widget'
// TODO: contains the button API. rendering engine <-> SDK
interface ButtonOptions {
text: string
}
export class Button extends Widget<ButtonOptions> {
public constructor(options: ButtonOptions) {
super(options)
}
}
+130
View File
@@ -0,0 +1,130 @@
import type {
AnswerData,
AnswerInput,
AnswerOutput,
AnswerConfig
} from '@sdk/types'
import { INTENT_OBJECT, SKILL_CONFIG } from '@bridge/constants'
class Leon {
private static instance: Leon
constructor() {
if (!Leon.instance) {
Leon.instance = this
}
}
/**
* Apply data to the answer
* @param answerKey The answer key
* @param data The data to apply
* @example setAnswerData('key', { name: 'Leon' })
*/
public setAnswerData(
answerKey: string,
data: AnswerData = null
): AnswerConfig {
try {
// In case the answer key is a raw answer
if (SKILL_CONFIG.answers == null || !SKILL_CONFIG.answers[answerKey]) {
return answerKey
}
const answers = SKILL_CONFIG.answers[answerKey] ?? ''
let answer: AnswerConfig
if (Array.isArray(answers)) {
answer = answers[Math.floor(Math.random() * answers.length)] ?? ''
} else {
answer = answers
}
if (data != null) {
for (const key in data) {
// In case the answer needs speech and text differentiation
if (typeof answer !== 'string' && answer.text) {
answer.text = answer.text.replaceAll(`%${key}%`, String(data[key]))
answer.speech = answer.speech.replaceAll(
`%${key}%`,
String(data[key])
)
} else {
answer = (answer as string).replaceAll(
`%${key}%`,
String(data[key])
)
}
}
}
if (SKILL_CONFIG.variables) {
const { variables } = SKILL_CONFIG
for (const key in variables) {
// In case the answer needs speech and text differentiation
if (typeof answer !== 'string' && answer.text) {
answer.text = answer.text.replaceAll(
`%${key}%`,
String(variables[key])
)
answer.speech = answer.speech.replaceAll(
`%${key}%`,
String(variables[key])
)
} else {
answer = (answer as string).replaceAll(
`%${key}%`,
String(variables[key])
)
}
}
}
return answer
} catch (e) {
console.error('Error while setting answer data:', e)
throw e
}
}
/**
* Send an answer to the core
* @param answerInput The answer input
* @example answer({ key: 'greet' }) // 'Hello world'
* @example answer({ key: 'welcome', data: { name: 'Louis' } }) // 'Welcome Louis'
* @example answer({ key: 'confirm', core: { restart: true } }) // 'Would you like to retry?'
*/
public async answer(answerInput: AnswerInput): Promise<void> {
try {
const answerObject: AnswerOutput = {
...INTENT_OBJECT,
output: {
codes:
answerInput.widget && !answerInput.key
? 'widget'
: (answerInput.key as string),
answer:
answerInput.key != null
? this.setAnswerData(answerInput.key, answerInput.data)
: '',
core: answerInput.core
}
}
if (answerInput.widget) {
answerObject.output.widget = answerInput.widget
}
// "Temporize" for the data buffer output on the core
await new Promise((r) => setTimeout(r, 100))
process.stdout.write(JSON.stringify(answerObject))
} catch (e) {
console.error('Error while creating answer:', e)
}
}
}
export const leon = new Leon()
+99
View File
@@ -0,0 +1,99 @@
import path from 'node:path'
import fs from 'node:fs'
import { SKILL_PATH, SKILLS_PATH } from '@bridge/constants'
interface MemoryOptions<T> {
name: string
defaultMemory?: T
}
export class Memory<T = unknown> {
private readonly memoryPath: string
private readonly name: string
private readonly defaultMemory: T | undefined
private isFromAnotherSkill: boolean
constructor(options: MemoryOptions<T>) {
const { name, defaultMemory } = options
this.name = name
this.defaultMemory = defaultMemory
this.memoryPath = path.join(SKILL_PATH, 'memory', `${this.name}.json`)
this.isFromAnotherSkill = false
if (this.name.includes(':') && this.name.split(':').length === 3) {
this.isFromAnotherSkill = true
const [domainName, skillName, memoryName] = this.name.split(':')
this.memoryPath = path.join(
SKILLS_PATH,
domainName as string,
skillName as string,
'memory',
`${memoryName}.json`
)
}
}
/**
* Clear the memory and set it to the default memory value
* @example clear()
*/
public async clear(): Promise<void> {
if (!this.isFromAnotherSkill) {
await this.write(this.defaultMemory as T)
} else {
throw new Error(
`You cannot clear the memory "${this.name}" as it belongs to another skill`
)
}
}
/**
* Read the memory
* @example read()
*/
public async read(): Promise<T> {
if (this.isFromAnotherSkill && !fs.existsSync(this.memoryPath)) {
throw new Error(
`You cannot read the memory "${this.name}" as it belongs to another skill which haven't written to this memory yet`
)
}
try {
if (!fs.existsSync(this.memoryPath)) {
await this.clear()
}
return JSON.parse(await fs.promises.readFile(this.memoryPath, 'utf-8'))
} catch (e) {
console.error(`Error while reading memory for "${this.name}":`, e)
throw e
}
}
/**
* Write the memory
* @param memory The memory to write
* @example write({ foo: 'bar' }) // { foo: 'bar' }
*/
public async write(memory: T): Promise<T> {
if (!this.isFromAnotherSkill) {
try {
await fs.promises.writeFile(
this.memoryPath,
JSON.stringify(memory, null, 2)
)
return memory
} catch (e) {
console.error(`Error while writing memory for "${this.name}":`, e)
throw e
}
} else {
throw new Error(
`You cannot write into the memory "${this.name}" as it belongs to another skill`
)
}
}
}
+147
View File
@@ -0,0 +1,147 @@
import dns from 'node:dns'
import type { AxiosInstance } from 'axios'
import axios from 'axios'
import { LEON_VERSION, NODEJS_BRIDGE_VERSION } from '@bridge/constants'
interface NetworkOptions {
/** `baseURL` will be prepended to `url`. It can be convenient to set `baseURL` for an instance of `Network` to pass relative URLs. */
baseURL?: string
}
interface NetworkRequestOptions {
/** Server URL that will be used for the request. */
url: string
/** Request method to be used when making the request. */
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
/** Data to be sent as the request body. */
data?: Record<string, unknown>
/** Custom headers to be sent. */
headers?: Record<string, string>
}
interface NetworkResponse<ResponseData> {
/** Data provided by the server. */
data: ResponseData
/** HTTP status code from the server response. */
statusCode: number
/** Options that was provided for the request. */
options: NetworkRequestOptions & NetworkOptions
}
export class NetworkError<ResponseErrorData = unknown> extends Error {
public readonly response: NetworkResponse<ResponseErrorData>
public constructor(response: NetworkResponse<ResponseErrorData>) {
super(`[NetworkError]: ${response.statusCode} ${response.data}`)
this.response = response
Object.setPrototypeOf(this, NetworkError.prototype)
}
}
export class Network {
private readonly options: NetworkOptions
private axios: AxiosInstance
public constructor(options: NetworkOptions = {}) {
this.options = options
this.axios = axios.create({
baseURL: this.options.baseURL
})
}
/**
* Send HTTP request
* @param options Request options
* @example request({ url: '/send', method: 'POST', data: { message: 'Hi' } })
*/
public async request<ResponseData = unknown, ResponseErrorData = unknown>(
options: NetworkRequestOptions
): Promise<NetworkResponse<ResponseData>> {
try {
const response = await this.axios.request<string>({
url: options.url,
method: options.method.toLowerCase(),
data: options.data,
transformResponse: (data) => {
return data
},
headers: {
'User-Agent': `Leon Personal Assistant ${LEON_VERSION} - Node.js Bridge ${NODEJS_BRIDGE_VERSION}`,
...options.headers
}
})
let data = {} as ResponseData
try {
data = JSON.parse(response.data)
} catch {
data = response.data as ResponseData
}
return {
data,
statusCode: response.status,
options: {
...this.options,
...options
}
}
} catch (error) {
let statusCode = 500
let dataRawText = ''
if (axios.isAxiosError(error)) {
dataRawText = error?.response?.data ?? ''
statusCode = error?.response?.status ?? 500
}
let data: ResponseErrorData
try {
data = JSON.parse(dataRawText)
} catch {
data = dataRawText as ResponseErrorData
}
throw new NetworkError<ResponseErrorData>({
data,
statusCode,
options: {
...this.options,
...options
}
})
}
}
/**
* Check if error is a network error
* @param error Error to check
* @example isNetworkError(error) // false
*/
public isNetworkError<ResponseErrorData = unknown>(
error: unknown
): error is NetworkError<ResponseErrorData> {
return error instanceof NetworkError
}
/**
* Verify whether there is an Internet connectivity
* @example isNetworkAvailable() // true
*/
public async isNetworkAvailable(): Promise<boolean> {
try {
await dns.promises.resolve('getleon.ai')
return true
} catch {
return false
}
}
}
@@ -0,0 +1 @@
export { default } from 'lodash'
+125
View File
@@ -0,0 +1,125 @@
import path from 'node:path'
import fs from 'node:fs'
import { SKILL_PATH } from '@bridge/constants'
export class Settings<T extends Record<string, unknown>> {
private readonly settingsPath: string
private readonly settingsSamplePath: string
public constructor() {
this.settingsPath = path.join(SKILL_PATH, 'src', 'settings.json')
this.settingsSamplePath = path.join(
SKILL_PATH,
'src',
'settings.sample.json'
)
}
/**
* Check if a setting is already set
* @param key The key to verify whether its value is set
* @returns isSettingSet('apiKey') // true
*/
public async isSettingSet(key: string): Promise<boolean> {
const settingsSample = await this.getSettingsSample()
const settings = await this.get()
return (
!!settings[key] &&
JSON.stringify(settings[key]) !== JSON.stringify(settingsSample[key])
)
}
/**
* Clear the settings and set it to the default settings.sample.json file
* @example clear()
*/
public async clear(): Promise<void> {
const settingsSample = await this.getSettingsSample()
await this.set(settingsSample)
}
private async getSettingsSample(): Promise<T> {
try {
return JSON.parse(
await fs.promises.readFile(this.settingsSamplePath, 'utf8')
)
} catch (e) {
console.error(
`Error while reading settings sample at "${this.settingsSamplePath}":`,
e
)
throw e
}
}
/**
* Get the settings
* @param key The key of the setting to get
* @example get('API_KEY') // 'value'
* @example get() // { API_KEY: 'value' }
*/
public async get<Key extends keyof T>(key: Key): Promise<T[Key]>
public async get(): Promise<T>
public async get<Key extends keyof T>(key?: Key): Promise<T | T[Key]> {
try {
if (!fs.existsSync(this.settingsPath)) {
await this.clear()
}
const settings = JSON.parse(
await fs.promises.readFile(this.settingsPath, 'utf8')
)
if (key != null) {
return settings[key]
}
return settings
} catch (e) {
console.error(
`Error while reading settings at "${this.settingsPath}":`,
e
)
throw e
}
}
/**
* Set the settings
* @param key The key of the setting to set
* @param value The value of the setting to set
* @example set({ API_KEY: 'value' }) // { API_KEY: 'value' }
*/
public async set<Key extends keyof T>(key: Key, value: T[Key]): Promise<T>
public async set(settings: T): Promise<T>
public async set<Key extends keyof T>(
keyOrSettings: Key | T,
value?: T[Key]
): Promise<T> {
try {
const settings = await this.get()
const newSettings =
typeof keyOrSettings === 'object'
? keyOrSettings
: { ...settings, [keyOrSettings]: value }
await fs.promises.writeFile(
this.settingsPath,
JSON.stringify(newSettings, null, 2)
)
return newSettings
} catch (e) {
console.error(
`Error while writing settings at "${this.settingsPath}":`,
e
)
throw e
}
}
}
+37
View File
@@ -0,0 +1,37 @@
/**
* Action types
*/
import type {
ActionParams,
IntentObject,
SkillAnswerCoreData,
SkillAnswerOutput
} from '@/core/brain/types'
import type { SkillAnswerConfigSchema } from '@/schemas/skill-schemas'
export type { ActionParams, IntentObject }
export * from '@/core/nlp/types'
export type ActionFunction = (params: ActionParams) => Promise<void>
/**
* Answer types
*/
export interface Answer {
key?: string
widget?: unknown // TODO
data?: AnswerData
core?: SkillAnswerCoreData
}
export interface TextAnswer extends Answer {
key: string
}
export interface WidgetAnswer extends Answer {
widget: unknown
key?: string
}
export type AnswerData = Record<string, string | number> | null
export type AnswerInput = TextAnswer | WidgetAnswer
export type AnswerOutput = SkillAnswerOutput
export type AnswerConfig = SkillAnswerConfigSchema
+9
View File
@@ -0,0 +1,9 @@
export abstract class Widget<T> {
public readonly type: string
public readonly options: T
protected constructor(options: T) {
this.type = this.constructor.name
this.options = options
}
}
+1 -1
View File
@@ -1 +1 @@
export const VERSION = '0.0.0'
export const VERSION = '1.1.0'
+11 -10
View File
@@ -1,23 +1,24 @@
{
"extends": "@tsconfig/node16-strictest/tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"outDir": "./dist/bin",
"rootDir": "../../",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@@/*": ["../../*"],
"@/*": ["../../server/src/*"],
"@server/*": ["../../server/src/*"],
"@bridge/*": ["./src/*"],
"@sdk/*": ["./src/sdk/*"]
},
"ignoreDeprecations": "5.0",
"allowJs": true,
"checkJs": false,
"resolveJsonModule": true
"exactOptionalPropertyTypes": false,
"declaration": true
},
"ts-node": {
"swc": true,
"require": ["tsconfig-paths/register"],
"files": true
},
"files": [],
"include": ["src/**/*"],
"exclude": ["dist"]
"exclude": ["node_modules", "dist"]
}
-2
View File
@@ -11,6 +11,4 @@ setuptools = "*"
wheel = "*"
cx-freeze = "==6.11.1"
requests = "==2.28.1"
pytube = "==9.5.0"
tinydb = "==4.7.0"
beautifulsoup4 = "==4.7.1"
+30
View File
@@ -0,0 +1,30 @@
import sys
import json
import os
import version
INTENT_OBJ_FILE_PATH = sys.argv[1]
with open(INTENT_OBJ_FILE_PATH, 'r') as f:
INTENT_OBJECT = json.load(f)
SKILLS_ROOT_PATH = os.path.join(
os.getcwd(),
'skills'
)
SKILL_PATH = os.path.join(
SKILLS_ROOT_PATH,
INTENT_OBJECT['domain'],
INTENT_OBJECT['skill']
)
SKILLS_PATH = SKILLS_ROOT_PATH
with open(os.path.join(SKILL_PATH, 'config', INTENT_OBJECT['extra_context_data']['lang'] + '.json'), 'r') as f:
SKILL_CONFIG = json.load(f)
LEON_VERSION = os.getenv('npm_package_version')
PYTHON_BRIDGE_VERSION = version.__version__
+34 -27
View File
@@ -1,37 +1,44 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import utils
import sys
from traceback import print_exc
from sys import path
from json import dumps, loads
from importlib import import_module
from constants import INTENT_OBJECT
def main():
"""Dynamically import skills related to the args and print the output"""
params = {
'lang': INTENT_OBJECT['lang'],
'utterance': INTENT_OBJECT['utterance'],
'current_entities': INTENT_OBJECT['current_entities'],
'entities': INTENT_OBJECT['entities'],
'current_resolvers': INTENT_OBJECT['current_resolvers'],
'resolvers': INTENT_OBJECT['resolvers'],
'slots': INTENT_OBJECT['slots'],
'extra_context_data': INTENT_OBJECT['extra_context_data']
}
path.append('.')
try:
sys.path.append('.')
intent_obj = utils.get_intent_obj()
skill_action_module = import_module(
'skills.'
+ INTENT_OBJECT['domain']
+ '.'
+ INTENT_OBJECT['skill']
+ '.src.actions.'
+ INTENT_OBJECT['action']
)
skill = import_module('skills.' + intent_obj['domain'] + '.' + intent_obj['skill'] + '.src.actions.' + intent_obj['action'])
getattr(skill_action_module, 'run')(params)
except Exception as e:
print(f"Error while running {INTENT_OBJECT['skill']} skill {INTENT_OBJECT['action']} action: {e}")
print_exc()
params = {
'lang': intent_obj['lang'],
'utterance': intent_obj['utterance'],
'current_entities': intent_obj['current_entities'],
'entities': intent_obj['entities'],
'current_resolvers': intent_obj['current_resolvers'],
'resolvers': intent_obj['resolvers'],
'slots': intent_obj['slots']
}
return getattr(skill, intent_obj['action'])(params)
if __name__ == '__main__':
try:
raise main()
except Exception as e:
# Print full traceback error report if skills triggers an error from the call stack
if 'exceptions must derive from BaseException' not in str(e):
print_exc()
try:
raise main()
except Exception as e:
# Print full traceback error report if skills triggers an error from the call stack
if 'exceptions must derive from BaseException' not in str(e):
print_exc()
+9
View File
@@ -0,0 +1,9 @@
from ..widget import Widget
# TODO: contains the button API. rendering engine <-> SDK
class Button(Widget):
def __init__(self, options):
super().__init__(options)
+92
View File
@@ -0,0 +1,92 @@
import random
import sys
from typing import Union
from time import sleep
import json
from .types import AnswerInput, AnswerData, AnswerConfig
from ..constants import SKILL_CONFIG, INTENT_OBJECT
class Leon:
instance: 'Leon' = None
def __init__(self) -> None:
if not Leon.instance:
Leon.instance = self
@staticmethod
def set_answer_data(answer_key: str, data: Union[AnswerData, None] = None) -> Union[str, AnswerConfig]:
"""
Apply data to the answer
:param answer_key: The answer key
:param data: The data to apply
"""
try:
# In case the answer key is a raw answer
if SKILL_CONFIG.get('answers') is None or SKILL_CONFIG['answers'].get(answer_key) is None:
return answer_key
answers = SKILL_CONFIG['answers'].get(answer_key, '')
if isinstance(answers, list):
answer = answers[random.randrange(len(answers))]
else:
answer = answers
if data:
for key, value in data.items():
# In case the answer needs speech and text differentiation
if not isinstance(answer, str) and answer.get('text'):
answer['text'] = answer['text'].replace('%{}%'.format(key), str(value))
answer['speech'] = answer['speech'].replace('%{}%'.format(key), str(value))
else:
answer = answer.replace('%{}%'.format(key), str(value))
if SKILL_CONFIG.get('variables'):
for key, value in SKILL_CONFIG['variables'].items():
# In case the answer needs speech and text differentiation
if not isinstance(answer, str) and answer.get('text'):
answer['text'] = answer['text'].replace('%{}%'.format(key), str(value))
answer['speech'] = answer['speech'].replace('%{}%'.format(key), str(value))
else:
answer = answer.replace('%{}%'.format(key), str(value))
return answer
except Exception as e:
print('Error while setting answer data:', e)
raise e
def answer(self, answer_input: AnswerInput) -> None:
"""
Send an answer to the core
:param answer_input: The answer input
"""
try:
key = answer_input.get('key')
output = {
'output': {
'codes': 'widget' if answer_input.get('widget') and not answer_input.get('key') else answer_input.get('key'),
'answer': self.set_answer_data(key, answer_input.get('data')) if key is not None else '',
'core': answer_input.get('core')
}
}
if answer_input.get('widget'):
output['output']['widget'] = answer_input['widget'].__dict__
answer_object = {
**INTENT_OBJECT,
**output
}
# "Temporize" for the data buffer output on the core
sleep(0.1)
sys.stdout.write(json.dumps(answer_object))
sys.stdout.flush()
except Exception as e:
print('Error while creating answer:', e)
leon = Leon()
+76
View File
@@ -0,0 +1,76 @@
import json
import os
from typing import TypedDict, Any
from ..constants import SKILL_PATH, SKILLS_PATH
class MemoryOptions(TypedDict, total=False):
name: str
default_memory: Any
class Memory:
def __init__(self, options: MemoryOptions):
self.name = options['name']
self.default_memory = options['default_memory'] if 'default_memory' in options else None
self.memory_path = self.memory_path = os.path.join(
SKILL_PATH,
'memory',
f'{self.name}.json'
)
self.__is_from_another_skill = False
if ':' in self.name and self.name.count(':') == 2:
self.__is_from_another_skill = True
domain_name, skill_name, memory_name = self.name.split(':')
self.memory_path = os.path.join(
SKILLS_PATH,
domain_name,
skill_name,
'memory',
memory_name + '.json'
)
def clear(self) -> None:
"""
Clear the memory and set it to the default memory value
"""
if not self.__is_from_another_skill:
self.write(self.default_memory)
else:
raise ValueError(f'You cannot clear the memory "{self.name}" as it belongs to another skill')
def read(self):
"""
Read the memory
"""
if self.__is_from_another_skill and not os.path.exists(self.memory_path):
raise ValueError(f'You cannot read the memory "{self.name}" as it belongs to another skill which hasn\'t written to this memory yet')
try:
if not os.path.exists(self.memory_path):
self.clear()
with open(self.memory_path, 'r') as f:
return json.load(f)
except Exception as e:
print(f'Error while reading memory for "{self.name}": {e}')
raise e
def write(self, memory):
"""
Write the memory
:param memory: The memory to write
"""
if not self.__is_from_another_skill:
try:
with open(self.memory_path, 'w') as f:
json.dump(memory, f, indent=2)
return memory
except Exception as e:
print(f'Error while writing memory for "{self.name}": {e}')
raise e
else:
raise ValueError(f'You cannot write into the memory "{self.name}" as it belongs to another skill')
+92
View File
@@ -0,0 +1,92 @@
import requests
import socket
from typing import Any, Dict, TypedDict, Union, Literal, Optional
from ..constants import LEON_VERSION, PYTHON_BRIDGE_VERSION
class NetworkOptions(TypedDict, total=False):
base_url: Optional[str]
class NetworkResponse(TypedDict):
data: Any
status_code: int
options: Dict[str, Any]
class NetworkError(Exception):
def __init__(self, response: NetworkResponse) -> None:
self.response = response
super().__init__(f"[NetworkError]: {response['status_code']} {response['data']}")
class NetworkRequestOptions(TypedDict, total=False):
url: str
method: Union[Literal['GET'], Literal['POST'], Literal['PUT'], Literal['PATCH'], Literal['DELETE']]
data: Dict[str, Any]
headers: Dict[str, str]
class Network:
def __init__(self, options: NetworkOptions = {'base_url': None}) -> None:
self.options = options
def request(self, options: NetworkRequestOptions) -> NetworkResponse:
try:
url = options['url']
if self.options['base_url'] is not None:
url = self.options['base_url'] + url
method = options['method']
data = options.get('data', {})
headers = options.get('headers', {})
response = requests.request(
method,
url,
json=data,
headers={
'User-Agent': f"Leon Personal Assistant {LEON_VERSION} - Python Bridge {PYTHON_BRIDGE_VERSION}",
**headers
}
)
data = {}
try:
data = response.json()
except Exception:
data = response.text
network_response: NetworkResponse = {
'data': data,
'status_code': response.status_code,
'options': {**self.options, **options}
}
if response.ok:
return network_response
else:
raise NetworkError(network_response)
except requests.exceptions.RequestException as error:
data = {}
try:
data = error.response.json()
except Exception:
data = error.response.text
raise NetworkError({
'data': data,
'status_code': error.response.status_code,
'options': {**self.options, **options}
}) from error
def is_network_error(self, error: Exception) -> bool:
return isinstance(error, NetworkError)
def is_network_available(self) -> bool:
try:
socket.gethostbyname('getleon.ai')
return True
except socket.error:
return False
+91
View File
@@ -0,0 +1,91 @@
import json
import os
from os import path
from typing import Union, Any, overload
from ..constants import SKILL_PATH
class Settings:
def __init__(self):
self.settings_path = path.join(SKILL_PATH, 'src', 'settings.json')
self.settings_sample_path = path.join(SKILL_PATH, 'src', 'settings.sample.json')
def is_setting_set(self, key: str) -> bool:
"""
Check if a setting is already set
:param key: The key to verify whether its value is set
"""
settings_sample = self.get_settings_sample()
settings = self.get()
return key in settings and json.dumps(settings[key]) != json.dumps(settings_sample[key])
def clear(self) -> None:
"""
Clear the settings and set it to the default settings.sample.json file
"""
settings_sample = self.get_settings_sample()
self.set(settings_sample)
def get_settings_sample(self) -> dict[str, Any]:
try:
with open(self.settings_sample_path, 'r') as file:
return json.load(file)
except Exception as e:
print(f"Error while reading settings sample at '{self.settings_sample_path}': {e}")
raise e
@overload
def get(self, key: str) -> Any: ...
@overload
def get(self, key: None = None) -> dict[str, Any]: ...
def get(self, key: Union[str, None] = None) -> Union[dict[str, Any], Any]:
"""
Get the settings
:param key: The key to get from the settings
"""
try:
if not os.path.exists(self.settings_path):
self.clear()
with open(self.settings_path, 'r') as file:
settings = json.load(file)
if key is not None:
return settings[key]
return settings
except Exception as e:
print(f"Error while reading settings at '{self.settings_path}': {e}")
raise e
@overload
def set(self, key_or_settings: dict[str, Any]) -> dict[str, Any]: ...
@overload
def set(self, key_or_settings: str, value: Any) -> dict[str, Any]: ...
def set(self, key_or_settings: Union[str, dict[str, Any]], value: Any = None) -> dict[str, Any]:
"""
Set the settings
:param key_or_settings: The key to set or the settings to set
:param value: The value to set
"""
try:
settings = self.get()
if isinstance(key_or_settings, dict):
new_settings = key_or_settings
else:
new_settings = {**settings, key_or_settings: value}
with open(self.settings_path, 'w') as file:
json.dump(new_settings, file, indent=2)
return new_settings
except Exception as e:
print(f"Error while writing settings at '{self.settings_path}': {e}")
raise e
+49
View File
@@ -0,0 +1,49 @@
from typing import Dict, Any, Optional, Union, Literal, TypedDict
class NLUResultSentiment(TypedDict):
vote: Optional[Union[Literal['positive'], Literal['neutral'], Literal['negative']]]
score: Optional[float]
class ExtraContextData(TypedDict):
lang: str
sentiment: str
date: str
time: str
timestamp: int
date_time: str
week_day: str
class ActionParams(TypedDict):
lang: str
utterance: str
current_entities: list[Any]
entities: list[Any]
current_resolvers: list[Any]
resolvers: list[Any]
slots: Dict[str, Any]
extra_context_data: ExtraContextData
AnswerData = Optional[Union[Dict[str, Union[str, int]], None]]
class Answer(TypedDict):
key: Optional[str]
widget: Optional[Any]
data: Optional[AnswerData]
core: Optional[Dict[str, Any]]
class AnswerInput(TypedDict, total=False):
key: Optional[str]
widget: Any
data: Optional[AnswerData]
core: Dict[str, Any]
class AnswerConfig(TypedDict):
text: str
speech: str
+4
View File
@@ -0,0 +1,4 @@
class Widget:
def __init__(self, options):
self.type = type(self).__name__
self.options = options
+8 -7
View File
@@ -5,19 +5,20 @@ from version import __version__
options = {
'build_exe': {
# Add common dependencies for skills
# Add common dependencies for skills
'includes': [
'bs4',
'pytube'
'bs4',
'requests',
'timeit'
],
'include_files': [(requests.certs.where(), 'cacert.pem')]
}
}
}
executables = [
Executable(
script='bridges/python/src/main.py',
target_name='leon-python-bridge'
Executable(
script='bridges/python/src/main.py',
target_name='leon-python-bridge'
)
]
-147
View File
@@ -1,147 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from json import loads, dumps
from pathlib import Path
from random import choice
from vars import useragent
from tinydb import TinyDB, Query, table, operations
from time import sleep
import sys
import os
import requests
import re
dirname = os.path.dirname(os.path.realpath(__file__))
intent_object_path = sys.argv[1]
codes = []
intent_obj_file = open(intent_object_path, 'r', encoding = 'utf8')
intent_obj = loads(intent_obj_file.read())
intent_obj_file.close()
def get_intent_obj():
"""Return intent object"""
return intent_obj
def translate(key, dict = { }):
"""Pickup the language file according to the cmd arg
and return the value according to the params"""
# "Temporize" for the data buffer ouput on the core
sleep(0.1)
output = ''
variables = { }
file = open(os.path.join(os.getcwd(), 'skills', intent_obj['domain'], intent_obj['skill'], 'config', intent_obj['lang'] + '.json'), 'r', encoding = 'utf8')
obj = loads(file.read())
file.close()
# In case the key is a raw answer
if key not in obj['answers']:
return key
prop = obj['answers'][key]
if 'variables' in obj:
variables = obj['variables']
if isinstance(prop, list):
output = choice(prop)
else:
output = prop
if dict:
for key in dict:
output = output.replace('%' + key + '%', str(dict[key]))
if variables:
for key in variables:
output = output.replace('%' + key + '%', str(variables[key]))
return output
def output(type, content = '', core = { }):
"""Communicate with the core"""
if isinstance(content, dict):
speech = translate(content['key'], content['data'])
codes.append(content['key'])
else:
content = str(content)
speech = translate(content)
codes.append(content)
print(dumps({
'domain': intent_obj['domain'],
'skill': intent_obj['skill'],
'action': intent_obj['action'],
'lang': intent_obj['lang'],
'utterance': intent_obj['utterance'],
'entities': intent_obj['entities'],
'slots': intent_obj['slots'],
'output': {
'type': type,
'codes': codes,
'speech': speech,
'core': core,
'options': config('options')
}
}))
if (type == 'inter'):
sys.stdout.flush()
def http(method, url, headers = None):
"""Send HTTP request with the Leon user agent"""
session = requests.Session()
session.headers.update({ 'User-Agent': useragent, 'Cache-Control': 'no-cache' })
if headers != None:
session.headers.update(headers)
return session.request(method, url)
def config(key):
"""Get a skill configuration value"""
file = open(os.path.join(os.getcwd(), 'skills', intent_obj['domain'], intent_obj['skill'], 'src/config.json'), 'r', encoding = 'utf8')
obj = loads(file.read())
file.close()
return obj['configurations'][key]
def create_dl_dir():
"""Create the downloads folder of a current skill"""
dl_dir = os.path.join(os.getcwd(), 'downloads')
# dl_dir = os.path.dirname(os.path.realpath(__file__)) + '/../../../../downloads/'
skill_dl_dir = os.path.join(dl_dir, intent_obj['domain'], intent_obj['skill'])
Path(skill_dl_dir).mkdir(parents = True, exist_ok = True)
return skill_dl_dir
def db(db_type = 'tinydb'):
"""Create a new dedicated database
for a specific skill"""
if db_type == 'tinydb':
ext = '.json' if os.environ.get('LEON_NODE_ENV') != 'testing' else '.spec.json'
db = TinyDB(os.path.join(os.getcwd(), 'skills', intent_obj['domain'], intent_obj['skill'], 'memory/db' + ext))
return {
'db': db,
'query': Query,
'table': table,
'operations': operations
}
def get_table(slug):
"""Get a table from a specific skill"""
domain, skill, table = slug.split('.')
ext = '.json' if os.environ.get('LEON_NODE_ENV') != 'testing' else '.spec.json'
db = TinyDB(os.path.join(os.getcwd(), 'skills', domain, skill, 'memory/db' + ext))
return db.table(table)
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import sys
import os
from json import loads
packagejsonfile = open(os.path.join(os.getcwd(), 'package.json'), 'r', encoding = 'utf8')
packagejson = loads(packagejsonfile.read())
packagejsonfile.close()
useragent = 'Leon-Personal-Assistant/' + packagejson['version']
+1 -1
View File
@@ -1 +1 @@
__version__ = '1.0.1'
__version__ = '1.2.0'
-6
View File
@@ -5,12 +5,6 @@
"errors": {
"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",
"synced_direct": "The new content has been synchronized on your device",
"syncing_google_drive": "I will now synchronize the downloaded content on Google Drive. Don't worry, I will let you know once I'm done",
"synced_google_drive": "The new content is now available on Google Drive"
},
"random_errors": [
"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",
-6
View File
@@ -6,12 +6,6 @@
"not_found": "Désolé, il semblerait que je n'arrive pas à trouver ça",
"nlu": "L'erreur semble provenir de ma compréhension de langage naturel. Voici plus de détails au sujet de cette dernière : \"%error%\""
},
"synchronizer": {
"syncing_direct": "Je vais maintenant synchroniser le contenu téléchargé sur votre appareil actuel. Ne vous inquiétez pas, je vous préviendrai lorsque j'aurai terminé",
"synced_direct": "Le nouveau contenu a été synchronisé sur votre appareil",
"syncing_google_drive": "Je vais maintenant synchroniser le contenu téléchargé sur Google Drive. Ne vous inquiétez pas, je vous préviendrai lorsque j'aurai terminé",
"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. 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",
+56 -21
View File
@@ -68,27 +68,6 @@
"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",
@@ -141,6 +120,11 @@
"route": "/api/action/games/rochambeau/rematch",
"params": []
},
{
"method": "GET",
"route": "/api/action/leon/age/run",
"params": []
},
{
"method": "GET",
"route": "/api/action/leon/color/favorite_color",
@@ -166,6 +150,11 @@
"route": "/api/action/leon/greeting/run",
"params": []
},
{
"method": "GET",
"route": "/api/action/leon/help/help",
"params": []
},
{
"method": "GET",
"route": "/api/action/leon/introduction/introduce_leon",
@@ -205,6 +194,52 @@
"method": "GET",
"route": "/api/action/leon/welcome/run",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/date_time/current_date_time",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/date_time/current_date",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/date_time/current_time",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/date_time/current_week_number",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/date_time/days_countdown",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/date_time/current_date_time_with_time_zone",
"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": []
}
]
}
+9 -2
View File
@@ -1,6 +1,6 @@
{
"name": "leon",
"version": "1.0.0-beta.8+dev",
"version": "1.0.0-beta.10+dev",
"description": "Server, skills and web app of the Leon personal assistant",
"author": {
"name": "Louis Grenard",
@@ -59,6 +59,7 @@
"python-bridge": "cross-env PIPENV_PIPFILE=bridges/python/src/Pipfile pipenv run python bridges/python/src/main.py server/src/intent-object.sample.json",
"train": "ts-node scripts/train/run-train.js",
"prepare-release": "ts-node scripts/release/prepare-release.js",
"skill-package": "ts-node scripts/skill-package.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",
@@ -100,6 +101,8 @@
"os-name": "4.0.1",
"pretty-bytes": "5.6.0",
"pretty-ms": "7.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"socket.io": "4.5.2",
"socket.io-client": "4.5.2",
"stt": "1.4.0",
@@ -115,8 +118,12 @@
"@types/getos": "3.0.1",
"@types/node": "18.7.13",
"@types/node-wav": "0.0.0",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"@typescript-eslint/eslint-plugin": "5.55.0",
"@typescript-eslint/parser": "5.55.0",
"@vercel/ncc": "0.36.1",
"@vitejs/plugin-react": "4.0.0",
"cli-spinner": "0.2.10",
"eslint": "8.22.0",
"eslint-config-prettier": "8.5.0",
@@ -140,6 +147,6 @@
"tsc-watch": "6.0.0",
"tsconfig-paths": "4.1.2",
"typescript": "5.0.2",
"vite": "3.0.9"
"vite": "4.3.5"
}
}
@@ -0,0 +1,12 @@
{
"lang": "en",
"domain": "leon",
"skill": "age",
"action": "run",
"utterance": "How old are you?",
"slots": {},
"entities": [],
"current_entities": [],
"resolvers": [],
"current_resolvers": []
}
+14 -6
View File
@@ -82,7 +82,7 @@ BUILD_TARGETS.set('tcp-server', {
} = BUILD_TARGETS.get(givenBuildTarget)
const buildPath = needsPythonEnv
? path.join(distPath, BINARIES_FOLDER_NAME)
: distPath
: path.join(distPath, 'bin')
const { type: osType } = SystemHelper.getInformation()
@@ -137,14 +137,22 @@ BUILD_TARGETS.set('tcp-server', {
* 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 distBinPath = path.join(NODEJS_BRIDGE_DIST_PATH, 'bin')
const distMainFilePath = path.join(distBinPath, 'index.js')
const distRenamedMainFilePath = path.join(
NODEJS_BRIDGE_DIST_PATH,
distBinPath,
NODEJS_BRIDGE_BIN_NAME
)
await command(`tsc --project ${tsconfigPath}`, {
await fs.promises.rm(buildPath, { recursive: true, force: true })
const inputMainFilePath = path.join(
NODEJS_BRIDGE_ROOT_PATH,
'src',
'main.ts'
)
await command(`ncc build ${inputMainFilePath} --out ${distBinPath}`, {
shell: true,
stdio: 'inherit'
})
@@ -188,7 +196,7 @@ BUILD_TARGETS.set('tcp-server', {
if (needsPythonEnv) {
archive.directory(buildPath, BINARIES_FOLDER_NAME)
} else {
archive.glob(`**/!(${archiveName})`, { cwd: distPath })
archive.directory(buildPath, 'bin')
}
await archive.finalize()
+52 -5
View File
@@ -16,9 +16,11 @@ import { SystemHelper } from '@/helpers/system-helper'
import {
MINIMUM_REQUIRED_RAM,
LEON_VERSION,
NODEJS_BRIDGE_BIN_PATH,
PYTHON_BRIDGE_BIN_PATH,
TCP_SERVER_BIN_PATH,
TCP_SERVER_VERSION,
NODEJS_BRIDGE_VERSION,
PYTHON_BRIDGE_VERSION,
INSTANCE_ID
} from '@/constants'
@@ -102,6 +104,13 @@ dotenv.config()
skillsResolversModelState: null,
mainModelState: null
},
nodeJSBridge: {
version: null,
executionTime: null,
command: null,
output: null,
error: null
},
pythonBridge: {
version: null,
executionTime: null,
@@ -145,14 +154,17 @@ dotenv.config()
distro: null
}
const totalRAMInGB = SystemHelper.getTotalRAM()
const freeRAMInGB = SystemHelper.getFreeRAM()
if (Math.round(totalRAMInGB) < MINIMUM_REQUIRED_RAM) {
if (Math.round(freeRAMInGB) < MINIMUM_REQUIRED_RAM) {
report.can_run.v = false
LogHelper.error(
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM`
`Free RAM: ${freeRAMInGB} GB | Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM`
)
} else {
LogHelper.success(`Total RAM: ${totalRAMInGB} GB`)
LogHelper.success(
`Free RAM: ${freeRAMInGB} GB | Total RAM: ${totalRAMInGB} GB`
)
}
if (osInfo.platform === 'linux') {
@@ -166,6 +178,7 @@ dotenv.config()
reportDataInput.environment.osDetails = osInfo
reportDataInput.environment.totalRAMInGB = totalRAMInGB
reportDataInput.environment.freeRAMInGB = freeRAMInGB
;(
await Promise.all([
command('node --version', { shell: true }),
@@ -205,7 +218,41 @@ dotenv.config()
})
/**
* Skill execution checking
* Skill execution checking with Node.js bridge
*/
LogHelper.success(`Node.js bridge version: ${NODEJS_BRIDGE_VERSION}`)
reportDataInput.nodeJSBridge.version = NODEJS_BRIDGE_VERSION
LogHelper.info('Executing a skill...')
try {
const executionStart = Date.now()
const p = await command(
`${NODEJS_BRIDGE_BIN_PATH} "${path.join(
process.cwd(),
'scripts',
'assets',
'nodejs-bridge-intent-object.json'
)}"`,
{ shell: true }
)
const executionEnd = Date.now()
const executionTime = executionEnd - executionStart
LogHelper.info(p.command)
reportDataInput.nodeJSBridge.command = p.command
LogHelper.success(p.stdout)
reportDataInput.nodeJSBridge.output = p.stdout
LogHelper.info(`Skill execution time: ${executionTime}ms\n`)
reportDataInput.nodeJSBridge.executionTime = `${executionTime}ms`
} catch (e) {
LogHelper.info(e.command)
report.can_run_skill.v = false
LogHelper.error(`${e}\n`)
reportDataInput.nodeJSBridge.error = JSON.stringify(e)
}
/**
* Skill execution checking with Python bridge
*/
LogHelper.success(`Python bridge version: ${PYTHON_BRIDGE_VERSION}`)
@@ -219,7 +266,7 @@ dotenv.config()
process.cwd(),
'scripts',
'assets',
'intent-object.json'
'python-bridge-intent-object.json'
)}"`,
{ shell: true }
)
+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(
+23 -4
View File
@@ -4,6 +4,7 @@ import stream from 'node:stream'
import readline from 'node:readline'
import axios from 'axios'
import { command } from 'execa'
import prettyBytes from 'pretty-bytes'
import prettyMilliseconds from 'pretty-ms'
import extractZip from 'extract-zip'
@@ -11,6 +12,7 @@ import extractZip from 'extract-zip'
import {
BINARIES_FOLDER_NAME,
GITHUB_URL,
NODEJS_BRIDGE_ROOT_PATH,
NODEJS_BRIDGE_DIST_PATH,
PYTHON_BRIDGE_DIST_PATH,
TCP_SERVER_DIST_PATH,
@@ -88,7 +90,7 @@ const setupBinaries = async (key) => {
if (!manifest || manifest.version !== version) {
const buildPath = isPlatformDependent
? path.join(distPath, BINARIES_FOLDER_NAME)
: distPath
: path.join(distPath, 'bin')
const archivePath = path.join(distPath, archiveName)
await Promise.all([
@@ -96,6 +98,23 @@ const setupBinaries = async (key) => {
fs.promises.rm(archivePath, { recursive: true, force: true })
])
if (key === 'nodejs-bridge') {
try {
LogHelper.info('Installing Node.js bridge npm packages...')
await command(
`npm install --package-lock=false --prefix ${NODEJS_BRIDGE_ROOT_PATH}`,
{
shell: true
}
)
LogHelper.success('Node.js bridge npm packages installed')
} catch (e) {
throw new Error(`Failed to install Node.js bridge npm packages: ${e}`)
}
}
try {
LogHelper.info(`Downloading ${name}...`)
@@ -142,8 +161,8 @@ const setupBinaries = async (key) => {
LogHelper.success(`${name} manifest file created`)
LogHelper.success(`${name} ${version} ready`)
} catch (error) {
throw new Error(`Failed to set up ${name}: ${error}`)
} catch (e) {
throw new Error(`Failed to set up ${name}: ${e}`)
}
} else {
LogHelper.success(`${name} is already at the latest version (${version})`)
@@ -151,7 +170,7 @@ const setupBinaries = async (key) => {
}
export default async () => {
// await setupBinaries('nodejs-bridge')
await setupBinaries('nodejs-bridge')
await setupBinaries('python-bridge')
await setupBinaries('tcp-server')
}
-102
View File
@@ -1,102 +0,0 @@
import fs from 'node:fs'
import path from 'node:path'
import { commandSync } from 'execa'
import { LogHelper } from '@/helpers/log-helper'
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
/**
* Set up skills configuration
*/
export default () =>
new Promise(async (resolve, reject) => {
LogHelper.info('Setting up skills configuration...')
const skillDomains = await SkillDomainHelper.getSkillDomains()
for (const currentDomain of skillDomains.values()) {
const skillKeys = Object.keys(currentDomain.skills)
// Browse skills
for (let j = 0; j < skillKeys.length; j += 1) {
const skillFriendlyName = skillKeys[j]
const currentSkill = currentDomain.skills[skillFriendlyName]
const configDir = path.join(currentSkill.path, 'src')
const configFile = path.join(configDir, 'config.json')
const configSampleFile = path.join(configDir, 'config.sample.json')
// If there is a bridge set from the skill config
if (currentSkill.bridge) {
// Check if the config and config.sample file exist
if (fs.existsSync(configFile) && fs.existsSync(configSampleFile)) {
const config = JSON.parse(
await fs.promises.readFile(configFile, 'utf8')
)?.configurations
const configSample = JSON.parse(
await fs.promises.readFile(configSampleFile, 'utf8')
)?.configurations
const configKeys = Object.keys(config)
const configSampleKeys = Object.keys(configSample)
// Check if there is a new config key in the config sample compared to the config.json
if (
JSON.stringify(configKeys) !== JSON.stringify(configSampleKeys)
) {
// Browse config keys of the new skill config
for (let j = 0; j < configSampleKeys.length; j += 1) {
// Check if the current config key does not exist
if (configKeys.includes(configSampleKeys[j]) === false) {
LogHelper.info(
`Adding new configuration key "${configSampleKeys[j]}" for the ${skillFriendlyName} skill...`
)
// Prepare to inject the new config key object
const configKey = {
[configSampleKeys[j]]: configSample[configSampleKeys[j]]
}
try {
// Add new skill configuration in the config.json file
commandSync(
`json -I -f ${configFile} -e 'this.configurations.${
configSampleKeys[j]
}=${JSON.stringify(configKey[configSampleKeys[j]])}'`,
{ shell: true }
)
LogHelper.success(
`"${configSampleKeys[j]}" configuration key added to ${configFile}`
)
} catch (e) {
LogHelper.error(
`Error while adding "${configSampleKeys[j]}" configuration key to ${configFile}: ${e}`
)
reject()
}
}
}
}
} else if (!fs.existsSync(configSampleFile)) {
// Stop the setup if the config.sample.json of the current skill does not exist
LogHelper.error(
`The "${skillFriendlyName}" skill configuration file does not exist. Try to pull the project (git pull)`
)
reject()
} else {
// Duplicate config.sample.json of the current skill to config.json
fs.createReadStream(configSampleFile).pipe(
fs.createWriteStream(`${configDir}/config.json`)
)
LogHelper.success(
`"${skillFriendlyName}" skill configuration file created`
)
resolve()
}
}
}
}
LogHelper.success('Skills configured')
resolve()
})
@@ -0,0 +1,65 @@
import path from 'node:path'
import fs from 'node:fs'
import { command } from 'execa'
import { isFileEmpty } from '@/utilities'
import { LogHelper } from '@/helpers/log-helper'
/**
* Install Node.js skills packages on setup
* 1. If skill is Node.js, then verify if an installation is needed
* 2. If install is needed, then install npm packages
*/
export default async function (skillFriendlyName, currentSkill) {
if (currentSkill.bridge === 'nodejs') {
const skillSRCPath = path.join(currentSkill.path, 'src')
const skillPackageJSONPath = path.join(skillSRCPath, 'package.json')
if (fs.existsSync(skillPackageJSONPath)) {
const isPackageJSONEmpty = await isFileEmpty(skillPackageJSONPath)
if (!isPackageJSONEmpty) {
const packageJSONMtime = (await fs.promises.stat(skillPackageJSONPath))
.mtime
const lastSkillNPMInstallFilePath = path.join(
skillSRCPath,
'.last-skill-npm-install'
)
if (fs.existsSync(lastSkillNPMInstallFilePath)) {
const lastSkillNPMInstallTime = new Date(
Number(
await fs.promises.readFile(lastSkillNPMInstallFilePath, 'utf8')
)
)
if (packageJSONMtime <= lastSkillNPMInstallTime) {
LogHelper.success(
`"${skillFriendlyName}" skill npm packages are up-to-date`
)
return
}
}
LogHelper.info(
`Installing npm packages for the "${skillFriendlyName}" skill...`
)
await command(
`npm install --package-lock=false --prefix ${skillSRCPath}`,
{
shell: true
}
)
await fs.promises.writeFile(
lastSkillNPMInstallFilePath,
`${Date.now()}`
)
LogHelper.success(`"${skillFriendlyName}" skill npm packages installed`)
}
}
}
}
@@ -0,0 +1,77 @@
import fs from 'node:fs'
import path from 'node:path'
import { commandSync } from 'execa'
import { LogHelper } from '@/helpers/log-helper'
/**
* Set up skills settings
*/
export default async function (skillFriendlyName, currentSkill) {
const skillSrcPath = path.join(currentSkill.path, 'src')
const settingsPath = path.join(skillSrcPath, 'settings.json')
const settingsSamplePath = path.join(skillSrcPath, 'settings.sample.json')
// If there is a bridge set from the skill settings
if (currentSkill.bridge) {
// Check if the settings and settings.sample file exist
if (fs.existsSync(settingsPath) && fs.existsSync(settingsSamplePath)) {
const settings = JSON.parse(
await fs.promises.readFile(settingsPath, 'utf8')
)
const settingsSample = JSON.parse(
await fs.promises.readFile(settingsSamplePath, 'utf8')
)
const settingsKeys = Object.keys(settings)
const settingsSampleKeys = Object.keys(settingsSample)
// Check if there is a new settings key in the settings sample compared to the settings.json
if (JSON.stringify(settingsKeys) !== JSON.stringify(settingsSampleKeys)) {
// Browse settings keys of the new settings config
for (let j = 0; j < settingsSampleKeys.length; j += 1) {
// Check if the current settings key does not exist
if (!settingsKeys.includes(settingsSampleKeys[j])) {
LogHelper.info(
`Adding new settings key "${settingsSampleKeys[j]}" for the ${skillFriendlyName} skill...`
)
// Prepare to inject the new settings key object
const configKey = {
[settingsSampleKeys[j]]: settingsSample[settingsSampleKeys[j]]
}
try {
// Add new skill settings in the settings.json file
commandSync(
`json -I -f ${settingsPath} -e 'this.${
settingsSampleKeys[j]
}=${JSON.stringify(configKey[settingsSampleKeys[j]])}'`,
{ shell: true }
)
LogHelper.success(
`"${settingsSampleKeys[j]}" settings key added to ${settingsPath}`
)
} catch (e) {
LogHelper.error(
`Error while adding "${settingsSampleKeys[j]}" settings key to ${settingsPath}: ${e}`
)
}
}
}
}
} else if (!fs.existsSync(settingsSamplePath)) {
// Stop the setup if the settings.sample.json of the current skill does not exist
LogHelper.error(
`The "${skillFriendlyName}" skill settings file does not exist. Try to pull the project (git pull)`
)
} else {
// Duplicate settings.sample.json of the current skill to settings.json
fs.createReadStream(settingsSamplePath).pipe(
fs.createWriteStream(`${skillSrcPath}/settings.json`)
)
LogHelper.success(`"${skillFriendlyName}" skill settings file created`)
}
}
}
@@ -0,0 +1,37 @@
import { LogHelper } from '@/helpers/log-helper'
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
import setupSkillsSettings from './setup-skills-settings'
import installNodejsSkillsPackages from './install-nodejs-skills-packages'
/**
* Browse skills and set them up
*/
export default async function () {
LogHelper.info('Setting up skills...')
try {
const skillDomains = await SkillDomainHelper.getSkillDomains()
for (const currentDomain of skillDomains.values()) {
const skillKeys = Object.keys(currentDomain.skills)
// Browse skills
for (let i = 0; i < skillKeys.length; i += 1) {
const skillFriendlyName = skillKeys[i]
const currentSkill = currentDomain.skills[skillFriendlyName]
LogHelper.info(`Setting up "${skillFriendlyName}" skill...`)
await setupSkillsSettings(skillFriendlyName, currentSkill)
await installNodejsSkillsPackages(skillFriendlyName, currentSkill)
LogHelper.success(`"${skillFriendlyName}" skill set up`)
}
}
LogHelper.success('Skills are set up')
} catch (e) {
LogHelper.error(`Failed to set up skills: ${e}`)
}
}
+3 -2
View File
@@ -7,7 +7,7 @@ import generateJSONSchemas from '../generate/generate-json-schemas'
import setupDotenv from './setup-dotenv'
import setupCore from './setup-core'
import setupSkillsConfig from './setup-skills-config'
import setupSkills from './setup-skills/setup-skills'
import setupBinaries from './setup-binaries'
import createInstanceID from './create-instance-id'
@@ -20,7 +20,8 @@ import createInstanceID from './create-instance-id'
try {
await setupDotenv()
LoaderHelper.start()
await Promise.all([setupCore(), setupSkillsConfig()])
await setupCore()
await setupSkills()
LoaderHelper.stop()
await setupBinaries()
await generateHTTPAPIKey()
+96
View File
@@ -0,0 +1,96 @@
import path from 'node:path'
import { command } from 'execa'
import { LogHelper } from '@/helpers/log-helper'
import { LoaderHelper } from '@/helpers/loader-helper'
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
import { PYTHON_BRIDGE_SRC_PATH } from '@/constants'
/**
* Manage Node.js skills npm packages
* npm run skill-package {command} {domain}:{skill} {npm package}
*/
;(async () => {
LoaderHelper.start()
const { argv } = process
const givenCommand = argv[2]?.toLowerCase()
const givenSkill = argv[3]?.toLowerCase()
const givenPackage = argv[4]?.toLowerCase()
const supportedCommands = ['install', 'uninstall']
const commands = {
install: {
runningText: 'Installing',
doneText: 'installed',
command: 'install'
},
uninstall: {
runningText: 'Uninstalling',
doneText: 'uninstalled',
command: 'uninstall'
}
}
if (
!givenCommand ||
!givenSkill ||
!givenPackage ||
!givenSkill.includes(':')
) {
LogHelper.error(
'Missing skill name or package name. The command should be: "npm run skill-package {command} {domain}:{skill} {npm package}"'
)
process.exit(1)
}
if (!supportedCommands.includes(givenCommand)) {
LogHelper.error(
`Unsupported command "${givenCommand}". Supported commands are: ${supportedCommands.join(
', '
)}`
)
process.exit(1)
}
const commandObject = commands[givenCommand]
const [domainName, skillName] = givenSkill.split(':')
const skillPath = SkillDomainHelper.getSkillPath(domainName, skillName)
const skillInfo = await SkillDomainHelper.getSkillInfo(domainName, skillName)
const skillSRCPath = path.join(skillPath, 'src')
if (skillInfo.bridge === 'python') {
const libPath = path.join(skillSRCPath, 'lib')
const pythonBridgePipfilePath = path.join(PYTHON_BRIDGE_SRC_PATH, 'Pipfile')
LogHelper.error(
`The "${givenSkill}" skill is a Python skill.
Hence, you should manually download the "${givenPackage}" package and put it in the "${libPath}" folder.
Or, you can verify whether the "${givenPackage}" package is already available via the "${pythonBridgePipfilePath}" file.`
)
process.exit(1)
}
try {
LogHelper.info(
`${commandObject.runningText} "${givenPackage}" npm package for the "${givenSkill}" skill ("${skillSRCPath}")...`
)
await command(
`npm ${commandObject.command} --package-lock=false --save-exact=true --prefix ${skillSRCPath} ${givenPackage}`,
{
shell: true,
stdio: 'inherit'
}
)
LogHelper.success(
`Successfully ${commandObject.doneText} "${givenPackage}" npm package in "${skillSRCPath}"`
)
} catch (e) {
LogHelper.error(
`Failed to ${commandObject.command} "${givenPackage}" npm package in "${skillSRCPath}": ${e}`
)
} finally {
LoaderHelper.stop()
}
})()
+22 -11
View File
@@ -18,9 +18,10 @@ export const GITHUB_URL = 'https://github.com/leon-ai/leon'
* Binaries / distribution
*/
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 BRIDGES_PATH = path.join(process.cwd(), 'bridges')
export const NODEJS_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'nodejs')
export const PYTHON_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'python')
export const TCP_SERVER_ROOT_PATH = path.join(process.cwd(), 'tcp_server')
export const NODEJS_BRIDGE_DIST_PATH = path.join(
NODEJS_BRIDGE_ROOT_PATH,
@@ -72,10 +73,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,
NODEJS_BRIDGE_BIN_NAME
)}`
export const NODEJS_BRIDGE_BIN_PATH = `${path.join(
process.cwd(),
'node_modules',
'ts-node',
'dist',
'bin.js'
)} --swc ${path.join(NODEJS_BRIDGE_DIST_PATH, 'bin', NODEJS_BRIDGE_BIN_NAME)}`
export const LEON_VERSION = process.env['npm_package_version']
@@ -125,16 +129,23 @@ 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 BIN_PATH = path.join(process.cwd(), 'bin')
export const SKILLS_PATH = path.join(process.cwd(), 'skills')
export const GLOBAL_DATA_PATH = path.join(process.cwd(), 'core', 'data')
export const MODELS_PATH = path.join(GLOBAL_DATA_PATH, 'models')
export const VOICE_CONFIG_PATH = path.join('core', 'config', 'voice')
export const VOICE_CONFIG_PATH = path.join(
process.cwd(),
'core',
'config',
'voice'
)
export const SERVER_PATH = path.join(
process.cwd(),
'server',
IS_PRODUCTION_ENV ? 'dist' : 'src'
)
export const TMP_PATH = path.join(SERVER_PATH, 'tmp')
export const LEON_FILE_PATH = path.join('leon.json')
export const LEON_FILE_PATH = path.join(process.cwd(), 'leon.json')
/**
* Misc
+51 -60
View File
@@ -9,17 +9,17 @@ import type {
NERCustomEntity,
NLUResult
} from '@/core/nlp/types'
import type { SkillConfigSchema, SkillSchema } from '@/schemas/skill-schemas'
import type {
SkillAnswerConfigSchema,
SkillConfigSchema,
SkillSchema
} from '@/schemas/skill-schemas'
import type {
BrainProcessResult,
IntentObject,
SkillResult
} from '@/core/brain/types'
import {
SkillActionTypes,
SkillBridges,
SkillOutputTypes
} from '@/core/brain/types'
import { SkillActionTypes, SkillBridges } from '@/core/brain/types'
import { langs } from '@@/core/langs.json'
import {
HAS_TTS,
@@ -32,7 +32,8 @@ 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'
import type { AnswerOutput } from '@sdk/types'
import { DateHelper } from '@/helpers/date-helper'
export default class Brain {
private static instance: Brain
@@ -47,7 +48,7 @@ export default class Brain {
private domainFriendlyName = ''
private skillFriendlyName = ''
private skillOutput = ''
private speeches: string[] = []
private answers: SkillAnswerConfigSchema[] = []
public isMuted = false // Close Leon mouth if true; e.g. over HTTP
constructor() {
@@ -101,19 +102,22 @@ export default class Brain {
/**
* Make Leon talk
*/
public talk(rawSpeech: string, end = false): void {
public talk(answer: SkillAnswerConfigSchema, end = false): void {
LogHelper.title('Brain')
LogHelper.info('Talking...')
if (rawSpeech !== '') {
if (answer !== '') {
const textAnswer = typeof answer === 'string' ? answer : answer.text
const speechAnswer = typeof answer === 'string' ? answer : answer.speech
if (HAS_TTS) {
// Stripe HTML to a whitespace. Whitespace to let the TTS respects punctuation
const speech = rawSpeech.replace(/<(?:.|\n)*?>/gm, ' ')
const speech = speechAnswer.replace(/<(?:.|\n)*?>/gm, ' ')
TTS.add(speech, end)
}
SOCKET_SERVER.socket?.emit('answer', rawSpeech)
SOCKET_SERVER.socket?.emit('answer', textAnswer)
}
}
@@ -174,9 +178,12 @@ export default class Brain {
utteranceID: string,
slots: IntentObject['slots']
): IntentObject {
const date = DateHelper.getDateTime()
const dateObject = new Date(date)
return {
id: utteranceID,
lang: this._lang,
lang: this._lang, // TODO: remove once the Python bridge will be updated to use extra_context_data.lang instead
domain: nluResult.classification.domain,
skill: nluResult.classification.skill,
action: nluResult.classification.action,
@@ -185,7 +192,16 @@ export default class Brain {
entities: nluResult.entities,
current_resolvers: nluResult.currentResolvers,
resolvers: nluResult.resolvers,
slots
slots,
extra_context_data: {
lang: this._lang,
sentiment: nluResult.sentiment,
date: date.slice(0, 10),
time: date.slice(11, 19),
timestamp: dateObject.getTime(),
date_time: date,
week_day: dateObject.toLocaleString('default', { weekday: 'long' })
}
}
}
@@ -196,22 +212,23 @@ export default class Brain {
data: Buffer
): Promise<Error | null> | void {
try {
const obj = JSON.parse(data.toString())
const skillAnswer = JSON.parse(data.toString()) as AnswerOutput
if (typeof obj === 'object') {
if (obj.output.type === SkillOutputTypes.Intermediate) {
LogHelper.title(`${this.skillFriendlyName} skill`)
LogHelper.info(data.toString())
if (typeof skillAnswer === 'object') {
LogHelper.title(`${this.skillFriendlyName} skill (on data)`)
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()
if (skillAnswer.output.widget) {
SOCKET_SERVER.socket?.emit('widget', skillAnswer.output.widget)
}
const { answer } = skillAnswer.output
if (!this.isMuted) {
this.talk(answer)
}
this.answers.push(answer)
this.skillOutput = data.toString()
return Promise.resolve(null)
} else {
return Promise.reject(
@@ -234,11 +251,13 @@ export default class Brain {
'%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)
this.answers.push(speech)
}
/**
@@ -388,7 +407,7 @@ export default class Brain {
// Catch the end of the skill execution
this.skillProcess?.stdout.on('end', () => {
LogHelper.title(`${this.skillFriendlyName} skill`)
LogHelper.title(`${this.skillFriendlyName} skill (on end)`)
LogHelper.info(this.skillOutput)
let skillResult: SkillResult | undefined = undefined
@@ -397,38 +416,6 @@ export default class Brain {
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(
@@ -493,6 +480,8 @@ export default class Brain {
await SkillDomainHelper.getSkillConfig(configFilePath, this._lang)
const utteranceHasEntities = nluResult.entities.length > 0
const { answers: rawAnswers } = nluResult
// TODO: handle dialog action skill speech vs text
// let answers = rawAnswers as [{ answer: SkillAnswerConfigSchema }]
let answers = rawAnswers
let answer: string | undefined = ''
@@ -518,6 +507,8 @@ export default class Brain {
actions[nluResult.classification.action]?.unknown_answers
if (unknownAnswers) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
answer =
unknownAnswers[
Math.floor(Math.random() * unknownAnswers.length)
+37 -12
View File
@@ -9,7 +9,10 @@ import type {
NLUSlot,
NLUSlots
} from '@/core/nlp/types'
import type { SkillConfigSchema } from '@/schemas/skill-schemas'
import type {
SkillConfigSchema,
SkillAnswerConfigSchema
} from '@/schemas/skill-schemas'
import type { ShortLanguageCode } from '@/types'
interface SkillCoreData {
@@ -28,9 +31,8 @@ export interface SkillResult {
entities: NEREntity[]
slots: NLUSlots
output: {
type: SkillOutputTypes
codes: string[]
speech: string
answer: string
core: SkillCoreData | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: Record<string, any>
@@ -41,27 +43,50 @@ 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
export interface ActionParams {
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 }
extra_context_data: {
lang: ShortLanguageCode
sentiment: NLUResult['sentiment']
date: string
time: string
timestamp: number
date_time: string
week_day: string
}
}
export interface IntentObject extends ActionParams {
id: string
domain: NLPDomain
skill: NLPSkill
action: NLPAction
}
export interface SkillAnswerCoreData {
restart?: boolean
isInActionLoop?: boolean
showNextActionSuggestions?: boolean
showSuggestions?: boolean
}
export interface SkillAnswerOutput extends IntentObject {
output: {
codes: string
answer: SkillAnswerConfigSchema
core?: SkillAnswerCoreData
widget?: unknown // TODO
}
}
export interface BrainProcessResult extends NLUResult {
@@ -1,152 +0,0 @@
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,12 +0,0 @@
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)
}
@@ -0,0 +1,11 @@
import type { FastifyPluginAsync } from 'fastify'
import { postUtterance } from '@/core/http-server/api/utterance/post'
import type { APIOptions } from '@/core/http-server/http-server'
export const utterancePlugin: FastifyPluginAsync<APIOptions> = async (
fastify,
options
) => {
await fastify.register(postUtterance, options)
}
@@ -0,0 +1,49 @@
import type { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Type } from '@sinclair/typebox'
import type { Static } from '@sinclair/typebox'
import { NLU, BRAIN } from '@/core'
import type { APIOptions } from '@/core/http-server/http-server'
const postUtteranceSchema = {
body: Type.Object({
utterance: Type.String()
})
} satisfies FastifySchema
interface PostUtteranceSchema {
body: Static<typeof postUtteranceSchema.body>
}
export const postUtterance: FastifyPluginAsync<APIOptions> = async (
fastify,
options
) => {
fastify.route<{
Body: PostUtteranceSchema['body']
}>({
method: 'POST',
url: `/api/${options.apiVersion}/utterance`,
schema: postUtteranceSchema,
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
})
}
}
})
}
+3 -43
View File
@@ -1,9 +1,7 @@
import { join } from 'node:path'
import Fastify, { FastifySchema } from 'fastify'
import Fastify from 'fastify'
import fastifyStatic from '@fastify/static'
import { Type } from '@sinclair/typebox'
import type { Static } from '@sinclair/typebox'
import {
LEON_VERSION,
@@ -16,9 +14,8 @@ 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'
import { utterancePlugin } from '@/core/http-server/api/utterance'
const API_VERSION = 'v1'
@@ -26,16 +23,6 @@ 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
@@ -88,39 +75,12 @@ export default class HTTPServer {
})
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
})
}
}
})
instance.register(utterancePlugin, { apiVersion: API_VERSION })
// TODO: reimplement skills routes once the new core is ready
// server.generateSkillsRoutes(instance)
@@ -32,7 +32,6 @@ import corsMidd from '@/core/http-server/plugins/cors'
import otherMidd from '@/core/http-server/plugins/other'
import keyMidd from '@/core/http-server/plugins/key'
import infoPlugin from '@/core/http-server/api/info'
import downloadsPlugin from '@/core/http-server/api/downloads'
import { LogHelper } from '@/helpers/log-helper'
import { DateHelper } from '@/helpers/date-helper'
@@ -337,7 +336,6 @@ server.bootstrap = async () => {
})
server.fastify.register(infoPlugin, { apiVersion })
server.fastify.register(downloadsPlugin, { apiVersion })
if (HAS_OVER_HTTP) {
server.fastify.register((instance, opts, next) => {
+31 -3
View File
@@ -1,10 +1,13 @@
import type { ShortLanguageCode } from '@/types'
import type {
BuiltInEntityType,
NEREntity,
NERSpacyEntity,
NLPUtterance,
NLUResult
NLUResult,
SpacyEntityType
} from '@/core/nlp/types'
import { BUILT_IN_ENTITY_TYPES, SPACY_ENTITY_TYPES } from '@/core/nlp/types'
import type {
SkillCustomEnumEntityTypeSchema,
SkillCustomRegexEntityTypeSchema,
@@ -40,6 +43,10 @@ export const MICROSOFT_BUILT_IN_ENTITIES = [
export default class NER {
private static instance: NER
public manager: NERManager
public spacyData: Map<
`${SpacyEntityType}-${string}`,
Record<string, unknown>
> = new Map()
constructor() {
if (!NER.instance) {
@@ -83,7 +90,7 @@ export default class NER {
lang
)
const { action } = classification
const promises = []
const promises: Array<Promise<void>> = []
const actionEntities = actions[action]?.entities || []
/**
@@ -121,6 +128,24 @@ export default class NER {
entity.resolution = { value: entity.sourceText }
}
if (
BUILT_IN_ENTITY_TYPES.includes(entity.entity as BuiltInEntityType)
) {
entity.type = entity.entity as BuiltInEntityType
}
if (SPACY_ENTITY_TYPES.includes(entity.entity as SpacyEntityType)) {
entity.type = entity.entity as SpacyEntityType
if (
'value' in entity.resolution &&
this.spacyData.has(`${entity.type}-${entity.resolution.value}`)
) {
entity.resolution = this.spacyData.get(
`${entity.type}-${entity.resolution.value}`
) as NERSpacyEntity['resolution']
}
}
return entity
})
@@ -139,17 +164,20 @@ export default class NER {
* Merge spaCy entities with the NER instance
*/
public async mergeSpacyEntities(utterance: NLPUtterance): Promise<void> {
this.spacyData = new Map()
const spacyEntities = await this.getSpacyEntities(utterance)
if (spacyEntities.length > 0) {
spacyEntities.forEach(({ entity, resolution }) => {
const value = StringHelper.ucFirst(resolution.value)
const spacyEntity = {
[entity]: {
options: {
[resolution.value]: [StringHelper.ucFirst(resolution.value)]
[resolution.value]: [value]
}
}
}
this.spacyData.set(`${entity}-${value}`, resolution)
MODEL_LOADER.mainNLPContainer.addEntities(spacyEntity, BRAIN.lang)
})
+6
View File
@@ -32,6 +32,7 @@ export const DEFAULT_NLU_RESULT = {
slots: {},
skillConfigPath: '',
answers: [], // For dialog action type
sentiment: {},
classification: {
domain: '',
skill: '',
@@ -127,6 +128,10 @@ export default class NLU {
const result: NLPJSProcessResult =
await MODEL_LOADER.mainNLPContainer.process(utterance)
const { locale, answers, classifications } = result
const sentiment = {
vote: result.sentiment.vote,
score: result.sentiment.score
}
let { score, intent, domain } = result
/**
@@ -158,6 +163,7 @@ export default class NLU {
...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
utterance,
answers, // For dialog action type
sentiment,
classification: {
domain,
skill: skillName || '',
+219 -104
View File
@@ -29,7 +29,8 @@ export interface NLPJSProcessResult {
intent: string
score: number
}[]
intent: string // E.g. "greeting.run"
/** E.g. "greeting.run" */
intent: string
score: number
domain: NLPDomain
sourceEntities: unknown[]
@@ -40,13 +41,14 @@ export interface NLPJSProcessResult {
answer: string | undefined
actions: NLPAction[]
sentiment: {
/** Rule of thumb: > 0 = negative; = 0 = neutral; < 0 = positive */
score: number
numWords: number
numHits: number
average: number
type: string
locale: ShortLanguageCode
vote: string
vote: 'positive' | 'neutral' | 'negative'
}
}
@@ -85,6 +87,10 @@ export interface NLUResult {
utterance: NLPUtterance
skillConfigPath: string
answers: { answer: string }[]
sentiment: {
vote?: NLPJSProcessResult['sentiment']['vote']
score?: NLPJSProcessResult['sentiment']['score']
}
classification: NLUClassification
}
@@ -96,83 +102,151 @@ export type NLUSlots = Record<string, NLUSlot>
/* eslint-disable @typescript-eslint/no-empty-interface */
interface Entity {
export const BUILT_IN_ENTITY_TYPES = [
'number',
'ip',
'hashtag',
'phonenumber',
'currency',
'percentage',
'date',
'time',
'timerange',
'daterange',
'datetimerange',
'duration',
'dimension',
'email',
'ordinal',
'age',
'url',
'temperature'
] as const
export type BuiltInEntityType = (typeof BUILT_IN_ENTITY_TYPES)[number]
export const CUSTOM_ENTITY_TYPES = ['regex', 'trim', 'enum'] as const
export type CustomEntityType = (typeof CUSTOM_ENTITY_TYPES)[number]
export const SPACY_ENTITY_TYPES = [
'location:country',
'location:city',
'person',
'organization'
] as const
export type SpacyEntityType = (typeof SPACY_ENTITY_TYPES)[number]
export const ENTITY_TYPES = [
...BUILT_IN_ENTITY_TYPES,
...CUSTOM_ENTITY_TYPES,
...SPACY_ENTITY_TYPES
] as const
export type EntityType = (typeof ENTITY_TYPES)[number]
interface Entity<
Type extends EntityType,
Resolution extends Record<string, unknown>,
EntityName extends string = Type
> {
start: number
end: number
len: number
accuracy: number
sourceText: string
utteranceText: string
entity: unknown
resolution: unknown
entity: EntityName
type: Type
resolution: Resolution
}
/**
* Built-in entity types
*/
interface BuiltInEntity extends Entity {}
export interface BuiltInEntity<
Type extends BuiltInEntityType,
Resolution extends Record<string, unknown>
> extends Entity<Type, Resolution> {}
interface BuiltInNumberEntity extends BuiltInEntity {
resolution: {
export type BuiltInNumberEntity = BuiltInEntity<
'number',
{
strValue: string
value: number
subtype: string
}
}
interface BuiltInIPEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInIPEntity = BuiltInEntity<
'ip',
{
value: string
type: string
type: 'ipv4' | 'ipv6'
}
}
interface BuiltInHashtagEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInHashtagEntity = BuiltInEntity<
'hashtag',
{
value: string
}
}
interface BuiltInPhoneNumberEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInPhoneNumberEntity = BuiltInEntity<
'phonenumber',
{
value: string
score: string
}
}
interface BuiltInCurrencyEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInCurrencyEntity = BuiltInEntity<
'currency',
{
strValue: string
value: number
unit: string
localeUnit: string
}
}
interface BuiltInPercentageEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInPercentageEntity = BuiltInEntity<
'percentage',
{
strValue: string
value: number
subtype: string
}
}
interface BuiltInDateEntity extends BuiltInEntity {
resolution: {
type: string
timex: string
strPastValue: string
pastDate: Date
strFutureValue: string
futureDate: Date
}
}
interface BuiltInTimeEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInDateEntity = BuiltInEntity<
'date',
| {
type: 'date'
timex: string
strValue: string
date: string
}
| {
type: 'interval'
timex: string
strPastValue: string
pastDate: string
strFutureValue: string
futureDate: string
}
>
export type BuiltInTimeEntity = BuiltInEntity<
'time',
{
values: {
timex: string
type: string
value: string
}[]
}
}
interface BuiltInTimeRangeEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInTimeRangeEntity = BuiltInEntity<
'timerange',
{
values: {
timex: string
type: string
@@ -180,108 +254,116 @@ interface BuiltInTimeRangeEntity extends BuiltInEntity {
end: string
}[]
}
}
interface BuiltInDateRangeEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInDateRangeEntity = BuiltInEntity<
'daterange',
{
type: 'interval'
timex: string
strPastStartValue: string
pastStartDate: string
strPastEndValue: string
pastEndDate: string
strFutureStartValue: string
futureStartDate: string
strFutureEndValue: string
futureEndDate: string
}
>
export type BuiltInDateTimeRangeEntity = BuiltInEntity<
'datetimerange',
{
type: string
timex: string
strPastStartValue: string
pastStartDate: Date
pastStartDate: string
strPastEndValue: string
pastEndDate: Date
pastEndDate: string
strFutureStartValue: string
futureStartDate: Date
futureStartDate: string
strFutureEndValue: string
futureEndDate: Date
futureEndDate: string
}
}
interface BuiltInDateTimeRangeEntity extends BuiltInEntity {
resolution: {
type: string
timex: string
strPastStartValue: string
pastStartDate: Date
strPastEndValue: string
pastEndDate: Date
strFutureStartValue: string
futureStartDate: Date
strFutureEndValue: string
futureEndDate: Date
}
}
interface BuiltInDurationEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInDurationEntity = BuiltInEntity<
'duration',
{
values: {
timex: string
type: string
Mod?: 'before' | 'after'
value: string
}[]
}
}
interface BuiltInDimensionEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInDimensionEntity = BuiltInEntity<
'dimension',
{
strValue: string
value: number
unit: string
localeUnit: string
}
}
interface BuiltInEmailEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInEmailEntity = BuiltInEntity<
'email',
{
value: string
}
}
interface BuiltInOrdinalEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInOrdinalEntity = BuiltInEntity<
'ordinal',
{
strValue: string
value: number
subtype: string
}
}
interface BuiltInAgeEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInAgeEntity = BuiltInEntity<
'age',
{
strValue: string
value: number
unit: string
localeUnit: string
}
}
interface BuiltInURLEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInURLEntity = BuiltInEntity<
'url',
{
value: string
}
}
interface BuiltInTemperatureEntity extends BuiltInEntity {
resolution: {
>
export type BuiltInTemperatureEntity = BuiltInEntity<
'temperature',
{
strValue: string
value: number
unit: string
localeUnit: string
}
}
>
/**
* Custom entity types
*/
interface CustomEntity<T> extends Entity {
type: T
}
interface CustomEntity<
Type extends CustomEntityType | SpacyEntityType,
Resolution extends Record<string, unknown> = { value: string }
> extends Entity<Type, Resolution, string> {}
export interface CustomEnumEntity extends CustomEntity<'enum'> {
export interface CustomEnumEntity<
Type extends CustomEntityType | SpacyEntityType = 'enum',
Resolution extends Record<string, unknown> = { value: string }
> extends CustomEntity<Type, Resolution> {
levenshtein: number
option: string
resolution: {
value: string
}
alias?: string // E.g. "location:country_0"; "location:country_1"
/** E.g. "location:country_0"; "location:country_1" */
alias?: string
}
type GlobalEntity = CustomEnumEntity
export interface CustomRegexEntity extends CustomEntity<'regex'> {
resolution: {
value: string
}
}
export interface CustomRegexEntity extends CustomEntity<'regex'> {}
interface CustomTrimEntity extends CustomEntity<'trim'> {
subtype:
| 'between'
@@ -291,23 +373,56 @@ interface CustomTrimEntity extends CustomEntity<'trim'> {
| 'before'
| 'beforeFirst'
| 'beforeLast'
resolution: {
value: string
}
}
/**
* spaCy's entity types
*/
interface SpacyEntity<T> extends CustomEnumEntity {
interface SpacyEntity<
T extends SpacyEntityType,
Resolution extends Record<string, unknown> = { value: string }
> extends CustomEnumEntity<T, Resolution> {
entity: T
}
interface SpacyLocationCountryEntity extends SpacyEntity<'location:country'> {}
interface SpacyLocationCityEntity extends SpacyEntity<'location:city'> {}
interface SpacyPersonEntity extends SpacyEntity<'person'> {}
interface SpacyOrganizationEntity extends SpacyEntity<'organization'> {}
interface SpacyLocationCountryData {
name: string
iso: string
isonumeric: number
continentcode: string
capital: string
population: number
tld: string
currencycode: string
phone: string
}
export interface SpacyLocationCountryEntity
extends SpacyEntity<
'location:country',
{
value: string
data: SpacyLocationCountryData
}
> {}
export interface SpacyLocationCityEntity
extends SpacyEntity<
'location:city',
{
value: string
data: {
name: string
latitude: number
longitude: number
countrycode: string
country: SpacyLocationCountryData
population: number
timezone: string
}
}
> {}
export interface SpacyPersonEntity extends SpacyEntity<'person'> {}
export interface SpacyOrganizationEntity extends SpacyEntity<'organization'> {}
/**
* Exported entity types
-246
View File
@@ -1,246 +0,0 @@
/* eslint-disable */
// TODO: remove the synchronization capability
import fs from 'node:fs'
import path from 'node:path'
import { google } from 'googleapis'
import { waterfall } from 'async'
import { LogHelper } from '@/helpers/log-helper'
class Synchronizer {
constructor(brain, classification, sync) {
this.brain = brain
this.classification = classification
this.sync = sync
this.downloadDir = `${__dirname}/../../../downloads/${this.classification.domain}/${this.classification.skill}`
LogHelper.title('Synchronizer')
LogHelper.success('New instance')
}
/**
* Choose the right method to synchronize
*/
async synchronize(cb) {
let code = 'synced_direct'
this.brain.talk(
`${this.brain.wernicke(
'synchronizer',
`syncing_${this.sync.method.toLowerCase().replace('-', '_')}`
)}.`
)
this.brain.socket.emit('is-typing', false)
if (this.sync.method === 'google-drive') {
code = 'synced_google_drive'
await this.googleDrive()
} else {
await this.direct()
}
return cb(`${this.brain.wernicke('synchronizer', code)}.`)
}
/**
* Direct synchronization method
*/
direct() {
return new Promise((resolve) => {
this.brain.socket.emit('download', {
domain: this.classification.domain,
skill: this.classification.skill,
action: this.classification.action
})
resolve()
})
}
/**
* Google Drive synchronization method
*/
googleDrive() {
return new Promise(async (resolve, reject) => {
const driveFolderName = `leon-${this.classification.domain}-${this.classification.skill}`
const folderMimeType = 'application/vnd.google-apps.folder'
const entities = await fs.promises.readdir(this.downloadDir)
const key = JSON.parse(
await fs.promises.readFile(
path.join(
process.cwd(),
'core/config/synchronizer/google-drive.json'
),
'utf8'
)
)
const authClient = new google.auth.JWT(
key.client_email,
key,
key.private_key,
// Available scopes: https://developers.google.com/identity/protocols/googlescopes
['https://www.googleapis.com/auth/drive'],
null
)
const drive = google.drive({
version: 'v3',
auth: authClient
})
let folderId = ''
waterfall(
[
(cb) => {
drive.files.list({}, (err, list) => {
if (err) {
LogHelper.error(`Error during listing: ${err}`)
return reject(err)
}
cb(null, list)
return true
})
},
(list, cb) => {
if (list.data.files.length === 0) {
return cb(null, false, folderId)
}
// Browse entities
for (let i = 0; i < list.data.files.length; i += 1) {
// In case the skill folder exists
if (
list.data.files[i].mimeType === folderMimeType &&
list.data.files[i].name === driveFolderName
) {
folderId = list.data.files[i].id
return cb(null, true, folderId)
} else if (i + 1 === list.data.files.length) {
return cb(null, false, folderId)
}
// TODO: UI toolbox to reach this scope
// Delete Drive files
/* setTimeout(() => {
drive.files.delete({ fileId: list.data.files[i].id })
LOG.title('Synchronizer'); LOG.success(`"${list.data.files[i].id}" deleted`)
}, 200 * i) */
}
return false
},
(folderExists, folderId, cb) => {
if (folderExists === false) {
// Create the skill folder if it does not exist
drive.files.create(
{
resource: {
name: driveFolderName,
mimeType: folderMimeType
},
fields: 'id'
},
(err, folder) => {
if (err) {
LogHelper.error(`Error during the folder creation: ${err}`)
return reject(err)
}
folderId = folder.data.id
LogHelper.title('Synchronizer')
LogHelper.success(
`"${driveFolderName}" folder created on Google Drive`
)
// Give ownership
return drive.permissions.create(
{
resource: {
type: 'user',
role: 'owner',
emailAddress: this.sync.email
},
emailMessage:
'Hey, I created a new folder to wrap your new content, cheers. Leon.',
transferOwnership: true,
fileId: folderId
},
(err) => {
if (err) {
LogHelper.error(
`Error during the folder permission creation: ${err}`
)
return reject(err)
}
LogHelper.success(
`"${driveFolderName}" ownership transferred`
)
cb(null, folderId)
return true
}
)
}
)
} else {
return cb(null, folderId)
}
return false
},
(folderId, cb) => {
let iEntities = 0
const upload = (i) => {
drive.files.create(
{
resource: {
name: entities[i],
parents: [folderId]
},
media: {
body: fs.createReadStream(
`${this.downloadDir}/${entities[i]}`
)
},
fields: 'id'
},
(err) => {
if (err) {
LogHelper.error(
`Error during the "${entities[i]}" file creation: ${err}`
)
return reject(err)
}
iEntities += 1
LogHelper.title('Synchronizer')
LogHelper.success(
`"${entities[i]}" file added to Google Drive`
)
if (iEntities === entities.length) {
cb(null)
}
return true
}
)
}
// Browse entities in Leon's memory
for (let i = 0; i < entities.length; i += 1) {
// Upload file to Drive
upload(i)
}
}
],
(err) => {
if (err) {
LogHelper.error(err)
return reject(err)
}
// Content available on Google Drive
resolve()
return true
}
)
})
}
}
export default Synchronizer
+24 -10
View File
@@ -9,6 +9,7 @@ import type {
SkillConfigSchema,
SkillBridgeSchema
} from '@/schemas/skill-schemas'
import { SKILLS_PATH } from '@/constants'
interface SkillDomain {
name: string
@@ -27,8 +28,6 @@ interface SkillConfigWithGlobalEntities
entities: Record<string, GlobalEntitySchema>
}
const DOMAINS_DIR = path.join(process.cwd(), 'skills')
export class SkillDomainHelper {
/**
* List all skills domains with skills data inside
@@ -38,9 +37,9 @@ export class SkillDomainHelper {
await Promise.all(
(
await fs.promises.readdir(DOMAINS_DIR)
await fs.promises.readdir(SKILLS_PATH)
).map(async (entity) => {
const domainPath = path.join(DOMAINS_DIR, entity)
const domainPath = path.join(SKILLS_PATH, entity)
if ((await fs.promises.stat(domainPath)).isDirectory()) {
const skills: SkillDomain['skills'] = {}
@@ -54,11 +53,14 @@ export class SkillDomainHelper {
const skillPath = path.join(domainPath, skillAliasName)
if ((await fs.promises.stat(skillPath)).isDirectory()) {
const skillJSONPath = path.join(skillPath, 'skill.json')
if (!fs.existsSync(skillJSONPath)) {
continue
}
const { name: skillName, bridge: skillBridge } = JSON.parse(
await fs.promises.readFile(
path.join(skillPath, 'skill.json'),
'utf8'
)
await fs.promises.readFile(skillJSONPath, 'utf8')
) as SkillSchema
skills[skillName] = {
@@ -93,7 +95,7 @@ export class SkillDomainHelper {
): Promise<DomainSchema> {
return JSON.parse(
await fs.promises.readFile(
path.join(DOMAINS_DIR, domain, 'domain.json'),
path.join(SKILLS_PATH, domain, 'domain.json'),
'utf8'
)
)
@@ -110,12 +112,24 @@ export class SkillDomainHelper {
): Promise<SkillSchema> {
return JSON.parse(
await fs.promises.readFile(
path.join(DOMAINS_DIR, domain, skill, 'skill.json'),
path.join(SKILLS_PATH, domain, skill, 'skill.json'),
'utf8'
)
)
}
/**
* Get skill path
* @param domain Domain where the skill belongs
* @param skill Skill to get path from
*/
public static getSkillPath(
domain: SkillDomain['name'],
skill: SkillSchema['name']
): string {
return path.join(SKILLS_PATH, domain, skill)
}
/**
* Get skill config
* @param configFilePath Path of the skill config file
+9 -1
View File
@@ -8,7 +8,7 @@ enum OSNames {
Linux = 'Linux',
Unknown = 'Unknown'
}
enum BinaryFolderNames {
export enum BinaryFolderNames {
Linux64Bit = 'linux-x86_64', // Linux 64-bit (Intel)
LinuxARM64 = 'linux-aarch64', // Linux 64-bit (ARM)
MacOS64Bit = 'macosx-x86_64', // Apple 64-bit (Intel)
@@ -119,6 +119,14 @@ export class SystemHelper {
return Number((os.totalmem() / (1_024 * 1_024 * 1_024)).toFixed(2))
}
/**
* Get the amount of free memory (in GB) on your machine
* @example getFreeRAM() // 6
*/
public static getFreeRAM(): number {
return Number((os.freemem() / (1_024 * 1_024 * 1_024)).toFixed(2))
}
/**
* Get the Node.js version of the current process
* @example getNodeJSVersion() // '18.15.0'
+1 -1
View File
@@ -50,7 +50,7 @@ import { LogHelper } from '@/helpers/log-helper'
setInterval(() => {
Telemetry.heartbeat()
}, 1_000 * 3_600)
}, 1_000 * 3_600 * 6)
;[
'exit',
'SIGINT',
+4 -3
View File
@@ -97,14 +97,15 @@ const GLOBAL_DATA_SCHEMAS = {
LogHelper.info('Checking system requirements...')
const totalRAMInGB = Math.round(SystemHelper.getTotalRAM())
const freeRAMInGB = Math.round(SystemHelper.getFreeRAM())
if (totalRAMInGB < MINIMUM_REQUIRED_RAM) {
if (freeRAMInGB < MINIMUM_REQUIRED_RAM) {
LogHelper.warning(
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM. It may not work as expected.`
`Free RAM: ${freeRAMInGB} GB | Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM. It may not work as expected.`
)
} else {
LogHelper.success(
`Minimum required RAM: ${MINIMUM_REQUIRED_RAM} GB | Total RAM: ${totalRAMInGB} GB`
`Minimum required RAM: ${MINIMUM_REQUIRED_RAM} GB | Free RAM: ${freeRAMInGB} GB | Total RAM: ${totalRAMInGB} GB`
)
}
+31 -17
View File
@@ -23,9 +23,19 @@ const skillDataTypes = [
Type.Literal('global_resolver'),
Type.Literal('entity')
]
const answerTypes = Type.Union([
Type.String(),
Type.Object({
speech: Type.String(),
text: Type.String()
})
])
const skillCustomEnumEntityType = Type.Object(
{
type: Type.Literal('enum'),
type: Type.Literal('enum', {
description:
'Enum: define a bag of words and synonyms that should match your new entity.'
}),
name: Type.String(),
options: Type.Record(
Type.String({ minLength: 1 }),
@@ -35,25 +45,27 @@ const skillCustomEnumEntityType = Type.Object(
)
},
{
additionalProperties: false,
description:
'Enum: define a bag of words and synonyms that should match your new entity.'
additionalProperties: false
}
)
const skillCustomRegexEntityType = Type.Object(
{
type: Type.Literal('regex'),
type: Type.Literal('regex', {
description: 'Regex: you can create an entity based on a regex.'
}),
name: Type.String({ minLength: 1 }),
regex: Type.String({ minLength: 1 })
},
{
additionalProperties: false,
description: 'Regex: you can create an entity based on a regex.'
additionalProperties: false
}
)
const skillCustomTrimEntityType = Type.Object(
{
type: Type.Literal('trim'),
type: Type.Literal('trim', {
description:
'Trim: you can pick up a data from an utterance by clearly defining conditions (e.g: pick up what is after the last "with" word of the utterance).'
}),
name: Type.String({ minLength: 1 }),
conditions: Type.Array(
Type.Object(
@@ -81,9 +93,7 @@ const skillCustomTrimEntityType = Type.Object(
)
},
{
additionalProperties: false,
description:
'Trim: you can pick up a data from an utterance by clearly defining conditions (e.g: pick up what is after the last "with" word of the utterance).'
additionalProperties: false
}
)
)
@@ -183,9 +193,14 @@ export const skillConfigSchemaObject = Type.Strict(
{ additionalProperties: false }
)
),
utterance_samples: Type.Optional(Type.Array(Type.String())),
answers: Type.Optional(Type.Array(Type.String())),
unknown_answers: Type.Optional(Type.Array(Type.String())),
utterance_samples: Type.Optional(
Type.Array(Type.String(), {
description:
'Utterance samples are used by the NLU (Natural Language Understanding) to train the skill. They are examples of what Leon owners can say to trigger the skill action.'
})
),
answers: Type.Optional(Type.Array(answerTypes)),
unknown_answers: Type.Optional(Type.Array(answerTypes)),
suggestions: Type.Optional(
Type.Array(Type.String(), {
description:
@@ -235,9 +250,7 @@ export const skillConfigSchemaObject = Type.Strict(
{ additionalProperties: false }
)
),
answers: Type.Optional(
Type.Record(Type.String(), Type.Array(Type.String()))
),
answers: Type.Optional(Type.Record(Type.String(), Type.Array(answerTypes))),
entities: Type.Optional(Type.Record(Type.String(), Type.String())),
resolvers: Type.Optional(
Type.Record(
@@ -270,3 +283,4 @@ export type SkillCustomRegexEntityTypeSchema = Static<
export type SkillCustomEnumEntityTypeSchema = Static<
typeof skillCustomEnumEntityType
>
export type SkillAnswerConfigSchema = Static<typeof answerTypes>
+26 -11
View File
@@ -19,6 +19,7 @@ import {
IS_PRODUCTION_ENV,
LANG,
LEON_VERSION,
NODEJS_BRIDGE_VERSION,
PYTHON_BRIDGE_VERSION,
STT_PROVIDER,
TCP_SERVER_VERSION,
@@ -47,13 +48,17 @@ export class Telemetry {
timeout: 7_000
})
public static async postInstall(): Promise<PostIntallResponse> {
const { data } = await this.axios.post('/on-post-install', {
instanceID: this.instanceID,
isGitpod: IS_GITPOD
})
public static async postInstall(): Promise<PostIntallResponse | unknown> {
try {
const { data } = await this.axios.post('/on-post-install', {
instanceID: this.instanceID,
isGitpod: IS_GITPOD
})
return data
return data
} catch (e) {
return {}
}
}
public static async start(): Promise<void> {
@@ -63,11 +68,11 @@ export class Telemetry {
const data = {
isProduction: IS_PRODUCTION_ENV,
isGitpod: IS_GITPOD,
isOnline: true,
language: LANG,
sttProvider: STT_PROVIDER,
ttsProvider: TTS_PROVIDER,
coreVersion: LEON_VERSION,
nodeJSBridgeVersion: NODEJS_BRIDGE_VERSION,
pythonBridgeVersion: PYTHON_BRIDGE_VERSION,
tcpServerVersion: TCP_SERVER_VERSION,
environment: {
@@ -81,6 +86,7 @@ export class Telemetry {
distro: null as Os | null
},
totalRAMInGB: SystemHelper.getTotalRAM(),
freeRAMInGB: SystemHelper.getFreeRAM(),
nodeVersion: SystemHelper.getNodeJSVersion(),
npmVersion: SystemHelper.getNPMVersion()
}
@@ -93,10 +99,19 @@ export class Telemetry {
}
data.environment.osDetails.distro = os
await this.axios.post('/on-start', {
instanceID: this.instanceID,
data
})
try {
await this.axios.post('/on-start', {
instanceID: this.instanceID,
data
})
} catch (e) {
if (IS_DEVELOPMENT_ENV) {
LogHelper.title('Telemetry')
LogHelper.warning(
`Failed to send start data to telemetry service: ${e}`
)
}
}
})
} else {
await this.axios.post('/on-start', {
+8
View File
@@ -1,8 +1,16 @@
import path from 'node:path'
import fs from 'node:fs'
import type { ShortLanguageCode } from '@/types'
import { GLOBAL_DATA_PATH } from '@/constants'
/**
* Files
*/
export async function isFileEmpty(path: string): Promise<boolean> {
return (await fs.promises.readFile(path)).length === 0
}
/**
* Paths
*/
+61 -48
View File
@@ -1,65 +1,73 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
from ..lib import akinator, memory
import utils
from ..lib import akinator, db
def guess(params):
"""Guess according to the given thematic"""
def run(params: ActionParams) -> None:
"""Guess according to the given thematic"""
resolvers = params['resolvers']
answer = None
resolvers = params['resolvers']
answer = None
for resolver in resolvers:
if resolver['name'] == 'answer':
answer = resolver['value']
for resolver in resolvers:
if resolver['name'] == 'answer':
answer = resolver['value']
# Return no speech if no value has been found
if answer == None:
return utils.output('end', None, { 'isInActionLoop': False })
# Return no speech if no value has been found
if answer is None:
return leon.answer({'core': {'isInActionLoop': False}})
aki = akinator.Akinator()
aki = akinator.Akinator()
session = db.get_session()
response = session['response']
formatted_response = aki._parse_response(response)
aki.session = session['session']
aki.signature = session['signature']
aki.progression = session['progression']
aki.uri = session['uri']
aki.timestamp = session['timestamp']
aki.server = session['server']
aki.child_mode = session['child_mode']
aki.frontaddr = session['frontaddr']
aki.question_filter = session['question_filter']
session = memory.get_session()
response = session['response']
aki.session = session['session']
aki.signature = session['signature']
aki.progression = session['progression']
aki.uri = session['uri']
aki.timestamp = session['timestamp']
aki.server = session['server']
aki.child_mode = session['child_mode']
aki.frontaddr = session['frontaddr']
aki.question_filter = session['question_filter']
resp = aki._parse_response(response)
aki._update(resp, '"step": "0"' in response)
resp = aki._parse_response(response)
aki._update(resp, '"step": "0"' in response)
if session['progression'] > 80:
aki.win()
if session['progression'] > 80:
aki.win()
utils.output('inter', { 'key': 'guessed', 'data': {
'name': aki.first_guess['name'],
'description': aki.first_guess['description']
}})
leon.answer({
'key': 'guessed',
'data': {
'name': aki.first_guess['name'],
'description': aki.first_guess['description']
}
})
utils.output('inter', { 'key': 'guessed_img', 'data': {
'name': aki.first_guess['name'],
'url': aki.first_guess['absolute_picture_path']
}})
leon.answer({
'key': 'guessed_img',
'data': {
'name': aki.first_guess['name'],
'url': aki.first_guess['absolute_picture_path']
}
})
return utils.output('end', 'ask_for_retry', {
'isInActionLoop': False, 'showNextActionSuggestions': True
})
return leon.answer({
'key': 'ask_for_retry',
'core': {
'isInActionLoop': False,
'showNextActionSuggestions': True
}
})
aki.answer(answer)
aki.answer(answer)
db.upsert_session({
memory.upsert_session({
'response': aki.response,
'session': aki.session,
'signature': aki.signature,
'progression': aki.progression,
'session': aki.session,
'signature': aki.signature,
'progression': aki.progression,
'uri': aki.uri,
'timestamp': aki.timestamp,
'server': aki.server,
@@ -68,4 +76,9 @@ def guess(params):
'question_filter': aki.question_filter
})
return utils.output('end', aki.question, { 'showSuggestions': True })
leon.answer({
'key': aki.question,
'core': {
'showSuggestions': True
}
})
+23 -16
View File
@@ -1,22 +1,29 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
import utils
def retry(params):
"""Ask for a retry"""
def run(params: ActionParams) -> None:
"""Ask for a retry"""
resolvers = params['resolvers']
decision = False
resolvers = params['resolvers']
decision = False
for resolver in resolvers:
if resolver['name'] == 'affirmation_denial':
decision = resolver['value']
for resolver in resolvers:
if resolver['name'] == 'affirmation_denial':
decision = resolver['value']
if decision == True:
return utils.output('end', 'confirm_retry', {
'isInActionLoop': False,
'restart': True
})
if decision:
return leon.answer({
'key': 'confirm_retry',
'core': {
'isInActionLoop': False,
'restart': True
}
})
return utils.output('end', 'deny_retry', { 'isInActionLoop': False })
leon.answer({
'key': 'deny_retry',
'core': {
'isInActionLoop': False
}
})
+34 -30
View File
@@ -1,38 +1,42 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
from ..lib import akinator, memory
import utils
from ..lib import akinator, db
def setup(params):
"""Initialize new session"""
def run(params: ActionParams) -> None:
"""Initialize new session"""
utils.output('inter', 'calling_akinator')
leon.answer({'key': 'calling_akinator'})
slots, lang = params['slots'], params['lang']
thematic = slots['thematic']['resolution']['value']
theme_lang = lang
if thematic != 'characters':
theme_lang = lang + '_' + thematic
slots, lang = params['slots'], params['lang']
thematic = slots['thematic']['resolution']['value']
theme_lang = lang
if thematic != 'characters':
theme_lang = lang + '_' + thematic
try:
aki = akinator.Akinator()
try:
aki = akinator.Akinator()
q = aki.start_game(theme_lang)
q = aki.start_game(theme_lang)
db.upsert_session({
'response': aki.response,
'session': aki.session,
'progression': aki.progression,
'signature': aki.signature,
'uri': aki.uri,
'timestamp': aki.timestamp,
'server': aki.server,
'child_mode': aki.child_mode,
'frontaddr': aki.frontaddr,
'question_filter': aki.question_filter
})
memory.upsert_session({
'response': aki.response,
'session': aki.session,
'progression': aki.progression,
'signature': aki.signature,
'uri': aki.uri,
'timestamp': aki.timestamp,
'server': aki.server,
'child_mode': aki.child_mode,
'frontaddr': aki.frontaddr,
'question_filter': aki.question_filter
})
return utils.output('end', q, { 'showNextActionSuggestions': True })
except:
return utils.output('end', 'network_error')
leon.answer({
'key': q,
'core': {
'showNextActionSuggestions': True
}
})
except BaseException:
leon.answer({'key': 'network_error'})
@@ -1,6 +0,0 @@
{
"configurations": {
"options": {},
"credentials": {}
}
}
+1 -1
View File
@@ -25,9 +25,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from .akinator import Akinator
from .exceptions import *
import os
with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
__version__ = f.read()
+162 -159
View File
@@ -28,212 +28,215 @@ import re
import time
import json
try:
import requests
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
except ImportError:
pass
pass
request_timeout = 5 # secs
request_timeout = 5 # secs
#* URLs for the API requests
# * URLs for the API requests
NEW_SESSION_URL = "https://{}/new_session?callback=jQuery331023608747682107778_{}&urlApiWs={}&partner=1&childMod={}&player=website-desktop&uid_ext_session={}&frontaddr={}&constraint=ETAT<>'AV'&soft_constraint={}&question_filter={}"
ANSWER_URL = "https://{}/answer_api?callback=jQuery331023608747682107778_{}&urlApiWs={}&childMod={}&session={}&signature={}&step={}&answer={}&frontaddr={}&question_filter={}"
BACK_URL = "{}/cancel_answer?callback=jQuery331023608747682107778_{}&childMod={}&session={}&signature={}&step={}&answer=-1&question_filter={}"
WIN_URL = "{}/list?callback=jQuery331023608747682107778_{}&childMod={}&session={}&signature={}&step={}"
#* HTTP headers to use for the requests
# * HTTP headers to use for the requests
HEADERS = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.9",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) snap Chromium/81.0.4044.92 Chrome/81.0.4044.92 Safari/537.36",
"x-requested-with": "XMLHttpRequest",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.9",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) snap Chromium/81.0.4044.92 Chrome/81.0.4044.92 Safari/537.36",
"x-requested-with": "XMLHttpRequest",
}
class Akinator():
"""A class that represents an Akinator game.
"""A class that represents an Akinator game.
The first thing you want to do after calling an instance of this class is to call "start_game()".
"""
def __init__(self):
self.uri = None
self.server = None
self.session = None
self.signature = None
self.uid = None
self.frontaddr = None
self.child_mode = None
self.question_filter = None
self.timestamp = None
The first thing you want to do after calling an instance of this class is to call "start_game()".
"""
self.question = None
self.progression = None
self.step = None
def __init__(self):
self.uri = None
self.server = None
self.session = None
self.signature = None
self.uid = None
self.frontaddr = None
self.child_mode = None
self.question_filter = None
self.timestamp = None
self.first_guess = None
self.guesses = None
self.question = None
self.progression = None
self.step = None
self.response = None
self.first_guess = None
self.guesses = None
def _update(self, resp, start=False):
"""Update class variables"""
self.response = None
if start:
self.session = int(resp["parameters"]["identification"]["session"])
self.signature = int(resp["parameters"]["identification"]["signature"])
self.question = str(resp["parameters"]["step_information"]["question"])
self.progression = float(resp["parameters"]["step_information"]["progression"])
self.step = int(resp["parameters"]["step_information"]["step"])
else:
self.question = str(resp["parameters"]["question"])
self.progression = float(resp["parameters"]["progression"])
self.step = int(resp["parameters"]["step"])
def _update(self, resp, start=False):
"""Update class variables"""
def _parse_response(self, res):
"""Parse the JSON response and turn it into a Python object"""
if start:
self.session = int(resp["parameters"]["identification"]["session"])
self.signature = int(resp["parameters"]["identification"]["signature"])
self.question = str(resp["parameters"]["step_information"]["question"])
self.progression = float(resp["parameters"]["step_information"]["progression"])
self.step = int(resp["parameters"]["step_information"]["step"])
else:
self.question = str(resp["parameters"]["question"])
self.progression = float(resp["parameters"]["progression"])
self.step = int(resp["parameters"]["step"])
return json.loads(",".join(res.split("(")[1::])[:-1])
def _parse_response(self, res):
"""Parse the JSON response and turn it into a Python object"""
def _get_session_info(self):
"""Get uid and frontaddr from akinator.com/game"""
return json.loads(",".join(res.split("(")[1::])[:-1])
info_regex = re.compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;")
r = requests.get("https://en.akinator.com/game", timeout=request_timeout)
def _get_session_info(self):
"""Get uid and frontaddr from akinator.com/game"""
match = info_regex.search(r.text)
self.uid, self.frontaddr = match.groups()[0], match.groups()[1]
info_regex = re.compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;")
r = requests.get("https://en.akinator.com/game", timeout=request_timeout, verify=False)
def _auto_get_region(self, lang, theme):
"""Automatically get the uri and server from akinator.com for the specified language and theme"""
match = info_regex.search(r.text)
self.uid, self.frontaddr = match.groups()[0], match.groups()[1]
server_regex = re.compile("[{\"translated_theme_name\":\"[\s\S]*\",\"urlWs\":\"https:\\\/\\\/srv[0-9]+\.akinator\.com:[0-9]+\\\/ws\",\"subject_id\":\"[0-9]+\"}]")
uri = lang + ".akinator.com"
def _auto_get_region(self, lang, theme):
"""Automatically get the uri and server from akinator.com for the specified language and theme"""
bad_list = ["https://srv12.akinator.com:9398/ws"]
while True:
r = requests.get("https://" + uri, timeout=request_timeout)
server_regex = re.compile("[{\"translated_theme_name\":\"[\\s\\S]*\",\"urlWs\":\"https:\\\\/\\\\/srv[0-9]+\\.akinator\\.com:[0-9]+\\\\/ws\",\"subject_id\":\"[0-9]+\"}]")
uri = lang + ".akinator.com"
match = server_regex.search(r.text)
parsed = json.loads(match.group().split("'arrUrlThemesToPlay', ")[-1])
bad_list = ["https://srv12.akinator.com:9398/ws"]
while True:
r = requests.get("https://" + uri, timeout=request_timeout, verify=False)
if theme == "c":
server = next((i for i in parsed if i["subject_id"] == "1"), None)["urlWs"]
elif theme == "a":
server = next((i for i in parsed if i["subject_id"] == "14"), None)["urlWs"]
elif theme == "o":
server = next((i for i in parsed if i["subject_id"] == "2"), None)["urlWs"]
match = server_regex.search(r.text)
parsed = json.loads(match.group().split("'arrUrlThemesToPlay', ")[-1])
if server not in bad_list:
return {"uri": uri, "server": server}
if theme == "c":
server = next((i for i in parsed if i["subject_id"] == "1"), None)["urlWs"]
elif theme == "a":
server = next((i for i in parsed if i["subject_id"] == "14"), None)["urlWs"]
elif theme == "o":
server = next((i for i in parsed if i["subject_id"] == "2"), None)["urlWs"]
def start_game(self, language=None, child_mode=False):
"""Start an Akinator game. Run this function first before the others. Returns a string containing the first question
if server not in bad_list:
return {"uri": uri, "server": server}
The "language" parameter can be left as None for English, the default language, or it can be set to one of the following (case-insensitive):
- "en": English (default)
- "en_animals": English server for guessing animals
- "en_objects": English server for guessing objects
- "ar": Arabic
- "cn": Chinese
- "de": German
- "de_animals": German server for guessing animals
- "es": Spanish
- "es_animals": Spanish server for guessing animals
- "fr": French
- "fr_animals": French server for guessing animals
- "fr_objects": French server for guessing objects
- "il": Hebrew
- "it": Italian
- "it_animals": Italian server for guessing animals
- "jp": Japanese
- "jp_animals": Japanese server for guessing animals
- "kr": Korean
- "nl": Dutch
- "pl": Polish
- "pt": Portuguese
- "ru": Russian
- "tr": Turkish
- "id": Indonesian
You can also put the name of the language spelled out, like "spanish", "korean", "french_animals", etc.
def start_game(self, language=None, child_mode=False):
"""Start an Akinator game. Run this function first before the others. Returns a string containing the first question
The "child_mode" parameter is False by default. If it's set to True, then Akinator won't ask questions about things that are NSFW
"""
self.timestamp = time.time()
region_info = self._auto_get_region(get_lang_and_theme(language)["lang"], get_lang_and_theme(language)["theme"])
self.uri, self.server = region_info["uri"], region_info["server"]
The "language" parameter can be left as None for English, the default language, or it can be set to one of the following (case-insensitive):
- "en": English (default)
- "en_animals": English server for guessing animals
- "en_objects": English server for guessing objects
- "ar": Arabic
- "cn": Chinese
- "de": German
- "de_animals": German server for guessing animals
- "es": Spanish
- "es_animals": Spanish server for guessing animals
- "fr": French
- "fr_animals": French server for guessing animals
- "fr_objects": French server for guessing objects
- "il": Hebrew
- "it": Italian
- "it_animals": Italian server for guessing animals
- "jp": Japanese
- "jp_animals": Japanese server for guessing animals
- "kr": Korean
- "nl": Dutch
- "pl": Polish
- "pt": Portuguese
- "ru": Russian
- "tr": Turkish
- "id": Indonesian
You can also put the name of the language spelled out, like "spanish", "korean", "french_animals", etc.
self.child_mode = child_mode
soft_constraint = "ETAT%3D%27EN%27" if self.child_mode else ""
self.question_filter = "cat%3D1" if self.child_mode else ""
The "child_mode" parameter is False by default. If it's set to True, then Akinator won't ask questions about things that are NSFW
"""
self.timestamp = time.time()
region_info = self._auto_get_region(get_lang_and_theme(language)["lang"], get_lang_and_theme(language)["theme"])
self.uri, self.server = region_info["uri"], region_info["server"]
self._get_session_info()
self.child_mode = child_mode
soft_constraint = "ETAT%3D%27EN%27" if self.child_mode else ""
self.question_filter = "cat%3D1" if self.child_mode else ""
r = requests.get(NEW_SESSION_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.uid, self.frontaddr, soft_constraint, self.question_filter), headers=HEADERS, timeout=request_timeout)
self.response = r.text
resp = self._parse_response(r.text)
self._get_session_info()
if resp["completion"] == "OK":
self._update(resp, True)
return self.question
else:
return raise_connection_error(resp["completion"])
r = requests.get(NEW_SESSION_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.uid, self.frontaddr, soft_constraint, self.question_filter), headers=HEADERS, timeout=request_timeout, verify=False)
self.response = r.text
resp = self._parse_response(r.text)
def answer(self, ans):
"""Answer the current question, which you can find with "Akinator.question". Returns a string containing the next question
if resp["completion"] == "OK":
self._update(resp, True)
return self.question
else:
return raise_connection_error(resp["completion"])
The "ans" parameter must be one of these (case-insensitive):
- "yes" OR "y" OR "0" for YES
- "no" OR "n" OR "1" for NO
- "i" OR "idk" OR "i dont know" OR "i don't know" OR "2" for I DON'T KNOW
- "probably" OR "p" OR "3" for PROBABLY
- "probably not" OR "pn" OR "4" for PROBABLY NOT
"""
ans = ans_to_id(ans)
def answer(self, ans):
"""Answer the current question, which you can find with "Akinator.question". Returns a string containing the next question
r = requests.get(ANSWER_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.session, self.signature, self.step, ans, self.frontaddr, self.question_filter), headers=HEADERS, timeout=request_timeout)
self.response = r.text
resp = self._parse_response(r.text)
The "ans" parameter must be one of these (case-insensitive):
- "yes" OR "y" OR "0" for YES
- "no" OR "n" OR "1" for NO
- "i" OR "idk" OR "i dont know" OR "i don't know" OR "2" for I DON'T KNOW
- "probably" OR "p" OR "3" for PROBABLY
- "probably not" OR "pn" OR "4" for PROBABLY NOT
"""
ans = ans_to_id(ans)
if resp["completion"] == "OK":
self._update(resp)
return self.question
else:
return raise_connection_error(resp["completion"])
r = requests.get(ANSWER_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.session, self.signature, self.step, ans, self.frontaddr, self.question_filter), headers=HEADERS, timeout=request_timeout, verify=False)
self.response = r.text
resp = self._parse_response(r.text)
def back(self):
"""Goes back to the previous question. Returns a string containing that question
if resp["completion"] == "OK":
self._update(resp)
return self.question
else:
return raise_connection_error(resp["completion"])
If you're on the first question and you try to go back again, the CantGoBackAnyFurther exception will be raised
"""
if self.step == 0:
raise CantGoBackAnyFurther("You were on the first question and couldn't go back any further")
def back(self):
"""Goes back to the previous question. Returns a string containing that question
r = requests.get(BACK_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step, self.question_filter), headers=HEADERS, timeout=request_timeout)
self.response = r.text
resp = self._parse_response(r.text)
If you're on the first question and you try to go back again, the CantGoBackAnyFurther exception will be raised
"""
if self.step == 0:
raise CantGoBackAnyFurther("You were on the first question and couldn't go back any further")
if resp["completion"] == "OK":
self._update(resp)
return self.question
else:
return utils.raise_connection_error(resp["completion"])
r = requests.get(BACK_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step, self.question_filter), headers=HEADERS, timeout=request_timeout, verify=False)
self.response = r.text
resp = self._parse_response(r.text)
def win(self):
"""Get Aki's guesses for who the person you're thinking of is based on your answers to the questions so far
if resp["completion"] == "OK":
self._update(resp)
return self.question
else:
return utils.raise_connection_error(resp["completion"])
Defines and returns the variable "Akinator.first_guess", a dictionary describing his first choice for who you're thinking about. The three most important values in the dict are "name" (character's name), "description" (description of character), and "absolute_picture_path" (direct link to image of character)
def win(self):
"""Get Aki's guesses for who the person you're thinking of is based on your answers to the questions so far
This function also defines "Akinator.guesses", which is a list of dictionaries containing his choices in order from most likely to least likely
Defines and returns the variable "Akinator.first_guess", a dictionary describing his first choice for who you're thinking about. The three most important values in the dict are "name" (character's name), "description" (description of character), and "absolute_picture_path" (direct link to image of character)
It's recommended that you call this function when Aki's progression is above 85%, which is when he will have most likely narrowed it down to just one choice. You can get his current progression via "Akinator.progression"
"""
r = requests.get(WIN_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step), headers=HEADERS, timeout=request_timeout)
self.response = r.text
resp = self._parse_response(r.text)
This function also defines "Akinator.guesses", which is a list of dictionaries containing his choices in order from most likely to least likely
if resp["completion"] == "OK":
self.first_guess = resp["parameters"]["elements"][0]["element"]
self.guesses = [g["element"] for g in resp["parameters"]["elements"]]
return self.first_guess
else:
return utils.raise_connection_error(resp["completion"])
It's recommended that you call this function when Aki's progression is above 85%, which is when he will have most likely narrowed it down to just one choice. You can get his current progression via "Akinator.progression"
"""
r = requests.get(WIN_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step), headers=HEADERS, timeout=request_timeout, verify=False)
self.response = r.text
resp = self._parse_response(r.text)
if resp["completion"] == "OK":
self.first_guess = resp["parameters"]["elements"][0]["element"]
self.guesses = [g["element"] for g in resp["parameters"]["elements"]]
return self.first_guess
else:
return utils.raise_connection_error(resp["completion"])
-24
View File
@@ -1,24 +0,0 @@
from time import time
import utils
# Skill database
db = utils.db()['db']
table = utils.db()['table']
# Session table
session_table = db.table('session')
# Time stamp
timestamp = int(time())
def upsert_session(session):
"""Save progress/info about the session"""
session_table.upsert(table.Document(session, doc_id=0))
def get_session():
"""Get current session progress data"""
return session_table.get(doc_id=0)
+33
View File
@@ -0,0 +1,33 @@
from bridges.python.src.sdk.memory import Memory
from typing import TypedDict, Any
class Session(TypedDict):
response: str
session: int
progression: float
signature: int
uri: str
timestamp: float
server: Any
child_mode: bool
frontaddr: str
question_filter: str
session_memory = Memory({
'name': 'session',
'default_memory': None
})
def upsert_session(session: Session) -> None:
"""Save progress/info about the session"""
session_memory.write(session)
def get_session() -> Session:
"""Get current session progress data"""
return session_memory.read()
+1
View File
@@ -24,6 +24,7 @@ SOFTWARE.
from .exceptions import InvalidAnswerError, InvalidLanguageError, AkiServerDown, AkiTechnicalError, AkiTimedOut, AkiNoQuestions, AkiConnectionFailure
def ans_to_id(ans):
"""Convert an input answer string into an Answer ID for Akinator"""
@@ -0,0 +1 @@
{}
+1 -1
View File
@@ -39,7 +39,7 @@
"bigger": ["The number is bigger.", "Try with a bigger number."],
"smaller": ["It is smaller.", "Try a smaller number."],
"guessed": [
"Congrats! The number was %nb% and you guessed in %attempts_nb% attempts. Ready for another round?"
"Congrats! The number was %number% and you guessed in %counter% attempts. Ready for another round?"
],
"replay": [
"Let's gooo!",
@@ -1,36 +1,42 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
import utils
from ..lib import db
from ..lib import memory
def guess(params):
"""Check whether the given number matches the chosen number"""
entities = params['entities']
given_nb = -1
nb_to_guess = db.get_new_game()['nb']
def run(params: ActionParams) -> None:
"""Check whether the given number matches the chosen number"""
# Find entities
for item in params['entities']:
if item['entity'] == 'number':
given_nb = item['resolution']['value']
entities = params['entities']
given_number = -1
number_to_guess = memory.get_new_game()['number']
# Return no speech if no number has been found
if given_nb == -1:
return utils.output('end', None, { 'isInActionLoop': False })
# Find entities
for item in entities:
if item['entity'] == 'number':
given_number = item['resolution']['value']
counter = db.get_new_game()['counter'] + 1
db.set_counter(counter)
# Return no speech if no number has been found
if given_number == -1:
leon.answer({'core': {'isInActionLoop': False}})
return
if given_nb == nb_to_guess:
return utils.output('end', { 'key': 'guessed',
'data': {
'nb': nb_to_guess,
'attempts_nb': counter
}
}, { 'isInActionLoop': False, 'showNextActionSuggestions': True })
if nb_to_guess < given_nb:
return utils.output('end', 'smaller')
if nb_to_guess > given_nb:
return utils.output('end', 'bigger')
counter = memory.get_new_game()['counter'] + 1
memory.set_counter(counter)
if given_number == number_to_guess:
leon.answer({
'key': 'guessed',
'data': {
'number': number_to_guess,
'counter': counter
},
'core': {
'isInActionLoop': False,
'showNextActionSuggestions': True
}
})
elif number_to_guess < given_number:
leon.answer({'key': 'smaller'})
elif number_to_guess > given_number:
leon.answer({'key': 'bigger'})
@@ -1,22 +1,33 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
import utils
from ..lib import memory
def replay(params):
"""Take decision about whether to replay"""
resolvers = params['resolvers']
decision = False
def run(params: ActionParams) -> None:
"""Take decision about whether to replay"""
for resolver in resolvers:
if resolver['name'] == 'affirmation_denial':
decision = resolver['value']
memory.game_memory.clear()
resolvers = params['resolvers']
decision = False
if decision == True:
return utils.output('end', 'replay', {
'isInActionLoop': False,
'restart': True
})
for resolver in resolvers:
if resolver['name'] == 'affirmation_denial':
decision = resolver['value']
return utils.output('end', 'stop', { 'isInActionLoop': False })
if decision:
leon.answer({
'key': 'replay',
'core': {
'isInActionLoop': False,
'restart': True
}
})
return
leon.answer({
'key': 'stop',
'core': {
'isInActionLoop': False
}
})
@@ -1,16 +1,12 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
from random import randint
import utils
from ..lib import db
from ..lib import memory
def setup(params):
"""Init the the number to guess"""
nb = randint(1, 100)
db.create_new_game(nb)
return utils.output('end', 'ready')
def run(params: ActionParams) -> None:
"""Init the number to guess"""
number_to_guess = randint(1, 100)
memory.create_new_game(number_to_guess)
leon.answer({'key': 'ready'})
@@ -1,6 +0,0 @@
{
"configurations": {
"options": {},
"credentials": {}
}
}
@@ -1,34 +0,0 @@
from time import time
import utils
# Skill database
db = utils.db()['db']
# Game table
game_table = db.table('game')
# Time stamp
timestamp = int(time())
def create_new_game(nb_to_guess):
"""Add new game"""
game_table.insert({
'nb': nb_to_guess,
'counter': 0,
'created_at': timestamp
})
def get_new_game():
"""Get the newly created game"""
return game_table.all()[-1]
def set_counter(counter):
"""Set new trial counter value"""
last_record_id = get_new_game().doc_id
game_table.update({
'counter': counter
}, doc_ids=[last_record_id])
@@ -0,0 +1,33 @@
from bridges.python.src.sdk.memory import Memory
from typing import TypedDict
from datetime import datetime
game_memory = Memory({'name': 'game', 'default_memory': None})
class Game(TypedDict):
number: int
counter: int
created_at: str
def create_new_game(number_to_guess: int) -> None:
"""Add new game"""
game: Game = {
'number': number_to_guess,
'counter': 0,
'created_at': datetime.now().isoformat()
}
game_memory.write(game)
def get_new_game() -> Game:
"""Get the newly created game"""
return game_memory.read()
def set_counter(counter: int) -> None:
"""Set new trial counter value"""
game = game_memory.read()
game['counter'] = counter
game_memory.write(game)
@@ -0,0 +1 @@
{}
+67 -61
View File
@@ -1,73 +1,79 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
import random
import utils
def play(params):
"""Define the winner"""
handsigns = {
'ROCK': {
'superior_to': 'SCISSORS',
'inferior_to': 'PAPER',
'emoji': ''
},
'PAPER': {
'superior_to': 'ROCK',
'inferior_to': 'SCISSORS',
'emoji': ''
},
'SCISSORS': {
'superior_to': 'PAPER',
'inferior_to': 'ROCK',
'emoji': ''
}
}
entities = params['entities']
player = {
'handsign': None,
'points': 0
}
leon = {
'handsign': random.choice(list(handsigns)),
'points': 0
}
def run(params: ActionParams) -> None:
"""Define the winner"""
# Find entities
for entity in entities:
if entity['entity'] == 'handsign':
player['handsign'] = entity['option']
handsigns = {
'ROCK': {
'superior_to': 'SCISSORS',
'inferior_to': 'PAPER',
'emoji': ''
},
'PAPER': {
'superior_to': 'ROCK',
'inferior_to': 'SCISSORS',
'emoji': ''
},
'SCISSORS': {
'superior_to': 'PAPER',
'inferior_to': 'ROCK',
'emoji': ''
}
}
entities = params['entities']
player = {
'handsign': None,
'points': 0
}
leon_player = {
'handsign': random.choice(list(handsigns)),
'points': 0
}
# Exit the loop if no handsign has been found
if player['handsign'] == None:
utils.output('inter', None, None, { 'isInActionLoop': False })
# Find entities
for entity in entities:
if entity['entity'] == 'handsign':
player['handsign'] = entity['option']
leon_emoji = handsigns[leon['handsign']]['emoji']
player_emoji = handsigns[player['handsign']]['emoji']
# Exit the loop if no handsign has been found
if player['handsign'] is None:
leon.answer({'core': {'isInActionLoop': False}})
utils.output('inter', { 'key': 'leon_emoji', 'data': { 'leon_emoji': leon_emoji } })
leon_emoji = handsigns[leon_player['handsign']]['emoji']
player_emoji = handsigns[player['handsign']]['emoji']
if leon['handsign'] == player['handsign']:
utils.output('inter', 'equal')
leon.answer({'key': 'leon_emoji', 'data': {'leon_emoji': leon_emoji}})
# Point for Leon
elif handsigns[leon['handsign']]['superior_to'] == player['handsign']:
utils.output('inter', { 'key': 'point_for_leon',
'data': {
'handsign_1': leon['handsign'].lower(),
'handsign_2': player['handsign'].lower()
}
})
if leon_player['handsign'] == player['handsign']:
leon.answer({'key': 'equal'})
else:
utils.output('inter', { 'key': 'point_for_player',
'data': {
'handsign_1': player['handsign'].lower(),
'handsign_2': leon['handsign'].lower()
}
})
# Point for Leon
elif handsigns[leon_player['handsign']]['superior_to'] == player['handsign']:
leon.answer({
'key': 'point_for_leon',
'data': {
'handsign_1': leon_player['handsign'].lower(),
'handsign_2': player['handsign'].lower()
}
})
return utils.output('end', 'ask_for_rematch', {
'isInActionLoop': False, 'showNextActionSuggestions': True
})
else:
leon.answer({
'key': 'point_for_player',
'data': {
'handsign_1': player['handsign'].lower(),
'handsign_2': leon_player['handsign'].lower()
}
})
leon.answer({
'key': 'ask_for_rematch',
'core': {
'isInActionLoop': False,
'showNextActionSuggestions': True
}
})
+24 -16
View File
@@ -1,22 +1,30 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
import utils
def rematch(params):
"""Take decision whether to do a rematch"""
def run(params: ActionParams) -> None:
"""Take decision whether to do a rematch"""
resolvers = params['resolvers']
decision = False
resolvers = params['resolvers']
decision = False
for resolver in resolvers:
if resolver['name'] == 'affirmation_denial':
decision = resolver['value']
for resolver in resolvers:
if resolver['name'] == 'affirmation_denial':
decision = resolver['value']
if decision == True:
return utils.output('end', 'confirm_rematch', {
'isInActionLoop': False,
'restart': True
})
if decision:
leon.answer({
'key': 'confirm_rematch',
'core': {
'isInActionLoop': False,
'restart': True
}
})
return
return utils.output('end', 'deny_rematch', { 'isInActionLoop': False })
leon.answer({
'key': 'deny_rematch',
'core': {
'isInActionLoop': False
}
})
@@ -1,6 +0,0 @@
{
"configurations": {
"options": {},
"credentials": {}
}
}
@@ -0,0 +1 @@
{}

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