Compare commits
265 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 754bd7380f | |||
| d32cda3663 | |||
| c8d2e8c985 | |||
| 2a15434919 | |||
| b2c375861b | |||
| ae6fec335d | |||
| 16302ec6e4 | |||
| 4d03e1bb4e | |||
| 69ce669b4c | |||
| c512b422e7 | |||
| 2ad6541c3e | |||
| 379326d7c1 | |||
| 1359f5d210 | |||
| 64858ec6fe | |||
| 166175192d | |||
| 97de1d1c3f | |||
| 563792bd0a | |||
| 076db4c3eb | |||
| 02e1de8ffa | |||
| cc01bd18b2 | |||
| 2478adbaa2 | |||
| 45b177d0fb | |||
| c0deafcd69 | |||
| eea6b0392d | |||
| 9845612c68 | |||
| d46ec1d3d5 | |||
| f1a65f53bc | |||
| 1410b72362 | |||
| e9245b522f | |||
| 636eb0f139 | |||
| 8645ef01fd | |||
| 18867bde89 | |||
| d061a6c4bf | |||
| e2ec76275a | |||
| 767bace201 | |||
| e70d0a0029 | |||
| 595fe78679 | |||
| 69cb6a610f | |||
| 10a03d90e3 | |||
| 251950384e | |||
| 8c91d245e8 | |||
| df343ce94f | |||
| 7c73f435af | |||
| b5ef7242af | |||
| 16e1f7a34a | |||
| e42a54d593 | |||
| cd3bcd5d6e | |||
| 2ea6237b1f | |||
| 9782526c42 | |||
| 3bf7150387 | |||
| c00c85fd4f | |||
| a76d05f68a | |||
| f17a4d3a2a | |||
| 850ea46ec6 | |||
| a540307241 | |||
| e440e4f715 | |||
| d1fe81ddc6 | |||
| 3648fef5fb | |||
| 1f9137bb20 | |||
| 2e6abef507 | |||
| a55fbcd532 | |||
| 924317c614 | |||
| 0c44916aa2 | |||
| 2a0cc0644e | |||
| 4f5b385fd7 | |||
| 30e405bbe3 | |||
| 8c105a043d | |||
| 99b5e7ab6d | |||
| b9dbc5012c | |||
| 0b16eebfbd | |||
| c078505846 | |||
| 421232a6dc | |||
| c62368ddad | |||
| f956e1abc6 | |||
| dc88ac244d | |||
| 4cd01a4baf | |||
| 06cda4a1f0 | |||
| dc7615723d | |||
| 14275be5f7 | |||
| 158df2f0f1 | |||
| 6bed516f89 | |||
| 7e72928ad6 | |||
| 4a7e77141e | |||
| 25ab60e5a2 | |||
| f8e6012cbe | |||
| 1c5d2874b0 | |||
| 3449c14d47 | |||
| 51f57e3255 | |||
| 7fd4be1248 | |||
| 3b77d64d4f | |||
| ab7e2a9b10 | |||
| b2e4ebf1be | |||
| 3c9a210081 | |||
| 7a9068283b | |||
| 66dc0aab0f | |||
| 91e48af263 | |||
| 3a230633d8 | |||
| 6c3a03d16a | |||
| 8a60b3d0d3 | |||
| cb3d3bf5a8 | |||
| f3257ad7b0 | |||
| cb29848f45 | |||
| ebaa3c4f9b | |||
| de17719537 | |||
| 47d5a48ac9 | |||
| e8ceed62eb | |||
| bbc1f8a79c | |||
| 289efb5610 | |||
| 05a4b2c773 | |||
| a32ad86342 | |||
| a2eacc3930 | |||
| a8c079fd5c | |||
| b389bdd8c6 | |||
| 750d1de8b4 | |||
| d295105fa6 | |||
| e2899c8d9a | |||
| 7f9b60a6cd | |||
| 9e77953976 | |||
| cb6462b749 | |||
| 07a3b1701d | |||
| f6300a3fb8 | |||
| 46299ef4eb | |||
| 259dc53213 | |||
| 51152a0fe3 | |||
| 01305af669 | |||
| 5539130dd9 | |||
| efd1061f30 | |||
| f6a501d4e1 | |||
| 839c7bfc3c | |||
| d3ba85cb36 | |||
| 623039415a | |||
| 7a75c985e0 | |||
| 0f2a579a66 | |||
| 63573083fa | |||
| 34aa188643 | |||
| 76dfe72f5f | |||
| 7a31158497 | |||
| 96145bb2f7 | |||
| 17cb808aab | |||
| b757d2d4ae | |||
| ce001c2e65 | |||
| a3b8c40714 | |||
| 0283878a90 | |||
| 3ed19bd6d0 | |||
| c163f5971c | |||
| 6a01e1e743 | |||
| b06eab9107 | |||
| 12ce47b937 | |||
| c8fc0068d1 | |||
| e32bf370db | |||
| 9ef4f234b8 | |||
| 6d2292f508 | |||
| b897ae160f | |||
| a51d92bf08 | |||
| 6d495cf43c | |||
| d6933a9e35 | |||
| 127a10f8ed | |||
| ef223a2901 | |||
| 9a06c64a5d | |||
| ad5edb5ad5 | |||
| ee0b080522 | |||
| be04b10645 | |||
| ab62a65e57 | |||
| 99ac5f18c8 | |||
| 87a7077854 | |||
| af2a124165 | |||
| 8e55c481d4 | |||
| d7b814761f | |||
| 4dde49e545 | |||
| 34b29d0e61 | |||
| 954126315f | |||
| e240a2db3c | |||
| 4abb68c1b3 | |||
| 3ea1a3666b | |||
| d3e0810298 | |||
| ef6d46cf8e | |||
| e65ecd5b67 | |||
| 5946a00427 | |||
| 6b239ae38b | |||
| 224158812f | |||
| ee9794cd9f | |||
| ad497d36c3 | |||
| 5eb62ddcf5 | |||
| ec819e1765 | |||
| 2c2f49290a | |||
| f936652007 | |||
| 950985ca86 | |||
| 8147d8423b | |||
| 3f556494c2 | |||
| 426c816ca4 | |||
| dd0eab51b6 | |||
| 07e253f663 | |||
| de08807f1a | |||
| 2cb030f47b | |||
| 673cf0426f | |||
| e89b5b0ea3 | |||
| 6b76e6a102 | |||
| 1625aaf3cd | |||
| b65cfb70d6 | |||
| 89a6c04a1e | |||
| e42655a5ff | |||
| c9678fe34f | |||
| 8349e41205 | |||
| 759e67ac40 | |||
| fc1fa56eab | |||
| 8f5bd3a9a5 | |||
| a836a7cc8c | |||
| 2df22770e0 | |||
| 9b839d3e66 | |||
| 2e3796642d | |||
| b027905ed0 | |||
| 4c9dfe667b | |||
| ed215feb81 | |||
| cd82a3a11b | |||
| b7a4f69565 | |||
| 7b8a324658 | |||
| 59cfded2bb | |||
| 8dc8c05d7d | |||
| 0e4ec4fb97 | |||
| bb00677568 | |||
| 308b63f576 | |||
| 40819e8e17 | |||
| 0fba50905f | |||
| 419bfe3d78 | |||
| 1847248f12 | |||
| eed4a9597c | |||
| c3452d2170 | |||
| 0a14567776 | |||
| 71573b78d1 | |||
| 722dba875b | |||
| 593419639e | |||
| 8b2f06e305 | |||
| 12e4b7bccf | |||
| 63a6c2d161 | |||
| f0e898e3c0 | |||
| 10bb4af4dc | |||
| 1af3fc2011 | |||
| 781cb072a3 | |||
| 73fe21f02e | |||
| 0947bd7877 | |||
| 07d2a41ecb | |||
| 9a66b677e0 | |||
| fe580cfb92 | |||
| 075f4458a0 | |||
| 094b1218e6 | |||
| b16475766a | |||
| e5fdeba50b | |||
| 12b0fe6525 | |||
| 302e1e11e0 | |||
| 8d5bca0d2b | |||
| 994cca7bf4 | |||
| a03afa78be | |||
| a1f93a6922 | |||
| 2678f6f04a | |||
| 03acb35e84 | |||
| 337854ab02 | |||
| 19c569b76a | |||
| 5b4cd9da81 | |||
| 40069ab5f0 | |||
| 8c89cbf43c | |||
| 31e44cde37 | |||
| 248dbb84f0 | |||
| 609fb676e4 | |||
| 5d866664b2 | |||
| ba09ef10b1 |
@@ -12,5 +12,4 @@ end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.py]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ LEON_HTTP_API_KEY=
|
||||
# Language used for the HTTP API
|
||||
LEON_HTTP_API_LANG=en-US
|
||||
|
||||
# Enable/disable collaborative logger
|
||||
LEON_LOGGER=true
|
||||
# Enable/disable telemetry
|
||||
LEON_TELEMETRY=true
|
||||
|
||||
# Python TCP server
|
||||
LEON_PY_TCP_SERVER_HOST=0.0.0.0
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["skills/**/*.ts"],
|
||||
"rules": {
|
||||
"import/order": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"rules": {
|
||||
|
||||
@@ -123,11 +123,14 @@ Types define which kind of changes you made to the project.
|
||||
|
||||
Scopes define high-level nodes of Leon.
|
||||
|
||||
- web app
|
||||
- bridge/python
|
||||
- bridge/nodejs
|
||||
- docker
|
||||
- hotword
|
||||
- scripts
|
||||
- server
|
||||
- tcp server
|
||||
- python bridge
|
||||
- hotword
|
||||
- web app
|
||||
- skill/skill_name
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Pre-release Node.js bridge
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Set Node.js bridge version
|
||||
working-directory: bridges/nodejs/src
|
||||
run: |
|
||||
echo "NODEJS_BRIDGE_VERSION=$(node --require fs --eval "const fs = require('node:fs'); const [, VERSION] = fs.readFileSync('version.ts', 'utf8').split(\"'\"); console.log(VERSION)")" >> $GITHUB_ENV
|
||||
|
||||
- name: Display Node.js bridge version
|
||||
run: |
|
||||
echo "Node.js bridge version: ${{ env.NODEJS_BRIDGE_VERSION }}"
|
||||
|
||||
- name: Install core
|
||||
run: npm install
|
||||
|
||||
- name: Build Node.js bridge
|
||||
run: npm run build:nodejs-bridge
|
||||
|
||||
- name: Upload Node.js bridge
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: bridges/nodejs/dist/*.zip
|
||||
|
||||
draft-release:
|
||||
name: Draft-release
|
||||
needs: [build]
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Set Node.js bridge version
|
||||
working-directory: bridges/nodejs/src
|
||||
run: |
|
||||
echo "NODEJS_BRIDGE_VERSION=$(node --require fs --eval "const fs = require('node:fs'); const [, VERSION] = fs.readFileSync('version.ts', 'utf8').split(\"'\"); console.log(VERSION)")" >> $GITHUB_ENV
|
||||
|
||||
- name: Download Node.js bridge
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: bridges/nodejs/dist
|
||||
|
||||
- uses: marvinpinto/action-automatic-releases@latest
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
automatic_release_tag: nodejs-bridge_v${{ env.NODEJS_BRIDGE_VERSION }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
title: Node.js Bridge ${{ env.NODEJS_BRIDGE_VERSION }}
|
||||
files: bridges/nodejs/dist/artifact/*.zip
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-20.04, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -33,7 +33,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
|
||||
- name: Set Python bridge version
|
||||
working-directory: bridges/python/src
|
||||
@@ -45,7 +44,7 @@ jobs:
|
||||
echo "Python bridge version: ${{ env.PYTHON_BRIDGE_VERSION }}"
|
||||
|
||||
- name: Install core
|
||||
run: npm ci
|
||||
run: npm install
|
||||
|
||||
- name: Set up Python bridge
|
||||
run: npm run setup:python-bridge
|
||||
@@ -61,7 +60,7 @@ jobs:
|
||||
draft-release:
|
||||
name: Draft-release
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-20.04, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -33,7 +33,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
|
||||
- name: Set TCP server version
|
||||
working-directory: tcp_server/src
|
||||
@@ -45,7 +44,7 @@ jobs:
|
||||
echo "TCP server version: ${{ env.TCP_SERVER_VERSION }}"
|
||||
|
||||
- name: Install core
|
||||
run: npm ci
|
||||
run: npm install
|
||||
|
||||
- name: Set up TCP server
|
||||
run: npm run setup:tcp-server
|
||||
@@ -61,7 +60,7 @@ jobs:
|
||||
draft-release:
|
||||
name: Draft-release
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
|
||||
+5
-4
@@ -8,7 +8,6 @@ __pycache__/
|
||||
test/coverage/
|
||||
**/tmp/*
|
||||
**/src/.venv/*
|
||||
downloads/*
|
||||
logs/*
|
||||
core/config/**/*.json
|
||||
bin/coqui/*
|
||||
@@ -21,15 +20,17 @@ 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
|
||||
!tcp_server/**/.gitkeep
|
||||
!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
|
||||
.python-version
|
||||
schemas/**/*.json
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# [1.0.0-beta.8](https://github.com/leon-ai/leon/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2023-05-01) / Binaries and TypeScript Rewrite
|
||||
|
||||
_Please refer to [our latest blog post](https://blog.getleon.ai/binaries-and-typescript-rewrite-1-0-0-beta-8/) for more information on the new release of our dear Leon._
|
||||
|
||||
# [1.0.0-beta.7](https://github.com/leon-ai/leon/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-08-24) / A Much Better NLP
|
||||
|
||||
_Please [read this blog post](https://blog.getleon.ai/a-much-better-nlp-and-future-1-0-0-beta-7/) to know more about all the new features and the exciting future of Leon._
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<p>Hello from React</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,9 @@
|
||||
import type React from 'react'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Button({ children }: Props) {
|
||||
return <button>{children}</button>
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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!`
|
||||
]
|
||||
]
|
||||
@@ -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
@@ -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()]
|
||||
})
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
package-lock=false
|
||||
save-exact=true
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "leon-nodejs-bridge",
|
||||
"description": "Leon's Node.js bridge to communicate between the core and skills made with JavaScript",
|
||||
"main": "dist/bin/leon-nodejs-bridge.js",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://getleon.ai",
|
||||
"bugs": {
|
||||
"url": "https://github.com/leon-ai/leon/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "1.4.0",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "4.14.194"
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -0,0 +1,49 @@
|
||||
import path from 'node:path'
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
})()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const VERSION = '1.1.0'
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/bin",
|
||||
"rootDir": "../../",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@@/*": ["../../*"],
|
||||
"@/*": ["../../server/src/*"],
|
||||
"@server/*": ["../../server/src/*"],
|
||||
"@bridge/*": ["./src/*"],
|
||||
"@sdk/*": ["./src/sdk/*"]
|
||||
},
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"declaration": true
|
||||
},
|
||||
"ts-node": {
|
||||
"swc": true,
|
||||
"require": ["tsconfig-paths/register"],
|
||||
"files": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
class Widget:
|
||||
def __init__(self, options):
|
||||
self.type = type(self).__name__
|
||||
self.options = options
|
||||
@@ -1,21 +1,24 @@
|
||||
from cx_Freeze import setup, Executable
|
||||
import requests.certs
|
||||
|
||||
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'
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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 @@
|
||||
__version__ = '1.0.0'
|
||||
__version__ = '1.2.0'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../schemas/voice-config-schemas/amazon.json",
|
||||
"credentials": {
|
||||
"accessKeyId": "",
|
||||
"secretAccessKey": ""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../schemas/voice-config-schemas/google-cloud.json",
|
||||
"type": "service_account",
|
||||
"project_id": "",
|
||||
"private_key_id": "",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../schemas/voice-config-schemas/watson-stt.json",
|
||||
"apikey": "",
|
||||
"url": "https://stream.watsonplatform.net/speech-to-text/api"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../schemas/voice-config-schemas/watson-tts.json",
|
||||
"apikey": "",
|
||||
"url": "https://stream.watsonplatform.net/text-to-speech/api"
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
{
|
||||
"$schema": "../../../schemas/global-data/global-answers.json",
|
||||
"answers": {
|
||||
"success": {},
|
||||
"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",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"red": {
|
||||
"synonyms": ["red"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"LOW": {
|
||||
"synonyms": ["low"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"Alexa": {
|
||||
"synonyms": ["Alexa"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-resolver.json",
|
||||
"name": "affirmation_denial",
|
||||
"intents": {
|
||||
"affirmation": {
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
{
|
||||
"$schema": "../../../schemas/global-data/global-answers.json",
|
||||
"answers": {
|
||||
"success": {},
|
||||
"errors": {
|
||||
"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",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"rouge": {
|
||||
"synonyms": ["rouge"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"bas": {
|
||||
"synonyms": ["bas", "basse"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-entity.json",
|
||||
"options": {
|
||||
"Alexa": {
|
||||
"synonyms": ["Alexa"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/global-data/global-resolver.json",
|
||||
"name": "affirmation_denial",
|
||||
"intents": {
|
||||
"affirmation": {
|
||||
|
||||
+56
-21
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+14
-3
@@ -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",
|
||||
@@ -47,9 +47,11 @@
|
||||
"prepare": "husky install",
|
||||
"generate:skills-endpoints": "ts-node scripts/generate/run-generate-skills-endpoints.js",
|
||||
"generate:http-api-key": "ts-node scripts/generate/run-generate-http-api-key.js",
|
||||
"generate:json-schemas": "ts-node scripts/generate/run-generate-json-schemas.js",
|
||||
"build": "npm run build:app && npm run build:server",
|
||||
"build:app": "cross-env LEON_NODE_ENV=production ts-node scripts/app/run-build-app.js",
|
||||
"build:server": "npm run delete-dist:server && npm run train && npm run generate:skills-endpoints && tsc && resolve-tspaths && shx rm -rf server/dist/core server/dist/package.json && shx mv -f server/dist/server/src/* server/dist && shx rm -rf server/dist/server && shx mkdir -p server/dist/tmp",
|
||||
"build:server": "npm run delete-dist:server && npm run train && npm run generate:skills-endpoints && tsc --project tsconfig.json && resolve-tspaths && shx rm -rf server/dist/core server/dist/package.json && shx mv -f server/dist/server/src/* server/dist && shx rm -rf server/dist/server && shx mkdir -p server/dist/tmp",
|
||||
"build:nodejs-bridge": "ts-node scripts/build-binaries.js nodejs-bridge",
|
||||
"build:python-bridge": "ts-node scripts/build-binaries.js python-bridge",
|
||||
"build:tcp-server": "ts-node scripts/build-binaries.js tcp-server",
|
||||
"start:tcp-server": "cross-env PIPENV_PIPFILE=tcp_server/src/Pipfile pipenv run python tcp_server/src/main.py",
|
||||
@@ -57,6 +59,8 @@
|
||||
"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",
|
||||
"check": "ts-node scripts/check.js",
|
||||
@@ -97,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",
|
||||
@@ -109,10 +115,15 @@
|
||||
"@types/archiver": "5.3.2",
|
||||
"@types/cli-spinner": "0.2.1",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@types/getos": "3.0.1",
|
||||
"@types/node": "18.7.13",
|
||||
"@types/node-wav": "0.0.0",
|
||||
"@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",
|
||||
@@ -136,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": []
|
||||
}
|
||||
+77
-23
@@ -9,10 +9,13 @@ import {
|
||||
PYTHON_BRIDGE_SRC_PATH,
|
||||
TCP_SERVER_SRC_PATH,
|
||||
BINARIES_FOLDER_NAME,
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
PYTHON_BRIDGE_DIST_PATH,
|
||||
TCP_SERVER_DIST_PATH,
|
||||
NODEJS_BRIDGE_BIN_NAME,
|
||||
PYTHON_BRIDGE_BIN_NAME,
|
||||
TCP_SERVER_BIN_NAME
|
||||
TCP_SERVER_BIN_NAME,
|
||||
NODEJS_BRIDGE_ROOT_PATH
|
||||
} from '@/constants'
|
||||
import { OSTypes } from '@/types'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
@@ -29,8 +32,15 @@ import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
const BUILD_TARGETS = new Map()
|
||||
|
||||
BUILD_TARGETS.set('nodejs-bridge', {
|
||||
name: 'Node.js bridge',
|
||||
needsPythonEnv: false,
|
||||
distPath: NODEJS_BRIDGE_DIST_PATH,
|
||||
archiveName: `${NODEJS_BRIDGE_BIN_NAME.split('.')[0]}.zip`
|
||||
})
|
||||
BUILD_TARGETS.set('python-bridge', {
|
||||
name: 'Python bridge',
|
||||
needsPythonEnv: true,
|
||||
pipfilePath: path.join(PYTHON_BRIDGE_SRC_PATH, 'Pipfile'),
|
||||
setupFilePath: path.join(PYTHON_BRIDGE_SRC_PATH, 'setup.py'),
|
||||
distPath: PYTHON_BRIDGE_DIST_PATH,
|
||||
@@ -39,6 +49,7 @@ BUILD_TARGETS.set('python-bridge', {
|
||||
})
|
||||
BUILD_TARGETS.set('tcp-server', {
|
||||
name: 'TCP server',
|
||||
needsPythonEnv: true,
|
||||
pipfilePath: path.join(TCP_SERVER_SRC_PATH, 'Pipfile'),
|
||||
setupFilePath: path.join(TCP_SERVER_SRC_PATH, 'setup.py'),
|
||||
distPath: TCP_SERVER_DIST_PATH,
|
||||
@@ -62,13 +73,16 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
|
||||
const {
|
||||
name: buildTarget,
|
||||
needsPythonEnv,
|
||||
pipfilePath,
|
||||
setupFilePath,
|
||||
distPath,
|
||||
archiveName,
|
||||
dotVenvPath
|
||||
} = BUILD_TARGETS.get(givenBuildTarget)
|
||||
const buildPath = path.join(distPath, BINARIES_FOLDER_NAME)
|
||||
const buildPath = needsPythonEnv
|
||||
? path.join(distPath, BINARIES_FOLDER_NAME)
|
||||
: path.join(distPath, 'bin')
|
||||
|
||||
const { type: osType } = SystemHelper.getInformation()
|
||||
|
||||
@@ -76,7 +90,7 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
* Install requirements
|
||||
*/
|
||||
try {
|
||||
if (osType === OSTypes.Linux) {
|
||||
if (needsPythonEnv && osType === OSTypes.Linux) {
|
||||
LogHelper.info('Checking whether the "patchelf" utility can be found...')
|
||||
|
||||
await command('patchelf --version', { shell: true })
|
||||
@@ -92,30 +106,66 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build
|
||||
*/
|
||||
try {
|
||||
LogHelper.info(`Building the ${buildTarget}...`)
|
||||
LogHelper.info(`Building the ${buildTarget}...`)
|
||||
|
||||
// Required environment variables to set up
|
||||
process.env.PIPENV_PIPFILE = pipfilePath
|
||||
process.env.PIPENV_VENV_IN_PROJECT = true
|
||||
if (needsPythonEnv) {
|
||||
/**
|
||||
* Build for binaries requiring a Python environment
|
||||
*/
|
||||
try {
|
||||
// Required environment variables to set up
|
||||
process.env.PIPENV_PIPFILE = pipfilePath
|
||||
process.env.PIPENV_VENV_IN_PROJECT = true
|
||||
|
||||
await command(
|
||||
`pipenv run python ${setupFilePath} build --build-exe ${buildPath}`,
|
||||
{
|
||||
await command(
|
||||
`pipenv run python ${setupFilePath} build --build-exe ${buildPath}`,
|
||||
{
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
}
|
||||
)
|
||||
|
||||
LogHelper.success(`The ${buildTarget} has been built`)
|
||||
} catch (e) {
|
||||
LogHelper.error(
|
||||
`An error occurred while building the ${buildTarget}. Try to delete the ${dotVenvPath} folder, run the setup command then build again: ${e}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Build for binaries not requiring a Python environment
|
||||
*/
|
||||
try {
|
||||
const distBinPath = path.join(NODEJS_BRIDGE_DIST_PATH, 'bin')
|
||||
const distMainFilePath = path.join(distBinPath, 'index.js')
|
||||
const distRenamedMainFilePath = path.join(
|
||||
distBinPath,
|
||||
NODEJS_BRIDGE_BIN_NAME
|
||||
)
|
||||
|
||||
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'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
LogHelper.success(`The ${buildTarget} has been built`)
|
||||
} catch (e) {
|
||||
LogHelper.error(
|
||||
`An error occurred while building the ${buildTarget}. Try to delete the ${dotVenvPath} folder, run the setup command then build again: ${e}`
|
||||
)
|
||||
process.exit(1)
|
||||
await fs.promises.rename(distMainFilePath, distRenamedMainFilePath)
|
||||
|
||||
LogHelper.success(`The ${buildTarget} has been built`)
|
||||
} catch (e) {
|
||||
LogHelper.error(
|
||||
`An error occurred while building the ${buildTarget}: ${e}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +193,11 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
|
||||
archive.pipe(output)
|
||||
|
||||
archive.directory(buildPath, BINARIES_FOLDER_NAME)
|
||||
if (needsPythonEnv) {
|
||||
archive.directory(buildPath, BINARIES_FOLDER_NAME)
|
||||
} else {
|
||||
archive.directory(buildPath, 'bin')
|
||||
}
|
||||
|
||||
await archive.finalize()
|
||||
})()
|
||||
|
||||
+117
-64
@@ -11,14 +11,18 @@ import axios from 'axios'
|
||||
import osName from 'os-name'
|
||||
import getos from 'getos'
|
||||
|
||||
import { version } from '@@/package.json'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
import {
|
||||
MINIMUM_REQUIRED_RAM,
|
||||
LEON_VERSION,
|
||||
NODEJS_BRIDGE_BIN_PATH,
|
||||
PYTHON_BRIDGE_BIN_PATH,
|
||||
TCP_SERVER_BIN_PATH,
|
||||
TCP_SERVER_VERSION,
|
||||
PYTHON_BRIDGE_VERSION
|
||||
NODEJS_BRIDGE_VERSION,
|
||||
PYTHON_BRIDGE_VERSION,
|
||||
INSTANCE_ID
|
||||
} from '@/constants'
|
||||
|
||||
dotenv.config()
|
||||
@@ -31,7 +35,6 @@ dotenv.config()
|
||||
try {
|
||||
const nodeMinRequiredVersion = '16'
|
||||
const npmMinRequiredVersion = '8'
|
||||
const minimumRequiredRAM = 4
|
||||
const flitePath = 'bin/flite/flite'
|
||||
const coquiLanguageModelPath = 'bin/coqui/huge-vocabulary.scorer'
|
||||
const amazonPath = 'core/config/voice/amazon.json'
|
||||
@@ -43,34 +46,6 @@ dotenv.config()
|
||||
const skillsResolversNlpModelPath =
|
||||
'core/data/models/leon-skills-resolvers-model.nlp'
|
||||
const mainNlpModelPath = 'core/data/models/leon-main-model.nlp'
|
||||
const pastebinData = {
|
||||
leonVersion: null,
|
||||
environment: {
|
||||
osDetails: null,
|
||||
nodeVersion: null,
|
||||
npmVersion: null
|
||||
},
|
||||
nlpModels: {
|
||||
globalResolversModelState: null,
|
||||
skillsResolversModelState: null,
|
||||
mainModelState: null
|
||||
},
|
||||
pythonBridge: {
|
||||
version: null,
|
||||
executionTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
tcpServer: {
|
||||
version: null,
|
||||
startTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
report: null
|
||||
}
|
||||
const report = {
|
||||
can_run: { title: 'Run', type: 'error', v: true },
|
||||
can_run_skill: { title: 'Run skills', type: 'error', v: true },
|
||||
@@ -116,6 +91,42 @@ dotenv.config()
|
||||
v: true
|
||||
}
|
||||
}
|
||||
let reportDataInput = {
|
||||
leonVersion: null,
|
||||
instanceID: INSTANCE_ID || null,
|
||||
environment: {
|
||||
osDetails: null,
|
||||
nodeVersion: null,
|
||||
npmVersion: null
|
||||
},
|
||||
nlpModels: {
|
||||
globalResolversModelState: null,
|
||||
skillsResolversModelState: null,
|
||||
mainModelState: null
|
||||
},
|
||||
nodeJSBridge: {
|
||||
version: null,
|
||||
executionTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
pythonBridge: {
|
||||
version: null,
|
||||
executionTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
tcpServer: {
|
||||
version: null,
|
||||
startTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
report: null
|
||||
}
|
||||
|
||||
LogHelper.title('Checking')
|
||||
|
||||
@@ -124,8 +135,8 @@ dotenv.config()
|
||||
*/
|
||||
|
||||
LogHelper.info('Leon version')
|
||||
LogHelper.success(`${version}\n`)
|
||||
pastebinData.leonVersion = version
|
||||
LogHelper.success(`${LEON_VERSION}\n`)
|
||||
reportDataInput.leonVersion = LEON_VERSION
|
||||
|
||||
/**
|
||||
* Environment checking
|
||||
@@ -143,14 +154,17 @@ dotenv.config()
|
||||
distro: null
|
||||
}
|
||||
const totalRAMInGB = SystemHelper.getTotalRAM()
|
||||
const freeRAMInGB = SystemHelper.getFreeRAM()
|
||||
|
||||
if (totalRAMInGB < minimumRequiredRAM) {
|
||||
if (Math.round(freeRAMInGB) < MINIMUM_REQUIRED_RAM) {
|
||||
report.can_run.v = false
|
||||
LogHelper.error(
|
||||
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${minimumRequiredRAM} 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') {
|
||||
@@ -162,8 +176,9 @@ dotenv.config()
|
||||
LogHelper.success(`${JSON.stringify(osInfo)}\n`)
|
||||
}
|
||||
|
||||
pastebinData.environment.osDetails = osInfo
|
||||
pastebinData.environment.totalRAMInGB = totalRAMInGB
|
||||
reportDataInput.environment.osDetails = osInfo
|
||||
reportDataInput.environment.totalRAMInGB = totalRAMInGB
|
||||
reportDataInput.environment.freeRAMInGB = freeRAMInGB
|
||||
;(
|
||||
await Promise.all([
|
||||
command('node --version', { shell: true }),
|
||||
@@ -195,19 +210,53 @@ dotenv.config()
|
||||
} else {
|
||||
LogHelper.success(`${p.stdout}\n`)
|
||||
if (p.command.includes('node --version')) {
|
||||
pastebinData.environment.nodeVersion = p.stdout
|
||||
reportDataInput.environment.nodeVersion = p.stdout
|
||||
} else if (p.command.includes('npm --version')) {
|
||||
pastebinData.environment.npmVersion = p.stdout
|
||||
reportDataInput.environment.npmVersion = p.stdout
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 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}`)
|
||||
pastebinData.pythonBridge.version = PYTHON_BRIDGE_VERSION
|
||||
reportDataInput.pythonBridge.version = PYTHON_BRIDGE_VERSION
|
||||
LogHelper.info('Executing a skill...')
|
||||
|
||||
try {
|
||||
@@ -217,23 +266,23 @@ dotenv.config()
|
||||
process.cwd(),
|
||||
'scripts',
|
||||
'assets',
|
||||
'intent-object.json'
|
||||
'python-bridge-intent-object.json'
|
||||
)}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
const executionEnd = Date.now()
|
||||
const executionTime = executionEnd - executionStart
|
||||
LogHelper.info(p.command)
|
||||
pastebinData.pythonBridge.command = p.command
|
||||
reportDataInput.pythonBridge.command = p.command
|
||||
LogHelper.success(p.stdout)
|
||||
pastebinData.pythonBridge.output = p.stdout
|
||||
reportDataInput.pythonBridge.output = p.stdout
|
||||
LogHelper.info(`Skill execution time: ${executionTime}ms\n`)
|
||||
pastebinData.pythonBridge.executionTime = `${executionTime}ms`
|
||||
reportDataInput.pythonBridge.executionTime = `${executionTime}ms`
|
||||
} catch (e) {
|
||||
LogHelper.info(e.command)
|
||||
report.can_run_skill.v = false
|
||||
LogHelper.error(`${e}\n`)
|
||||
pastebinData.pythonBridge.error = JSON.stringify(e)
|
||||
reportDataInput.pythonBridge.error = JSON.stringify(e)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,7 +290,7 @@ dotenv.config()
|
||||
*/
|
||||
|
||||
LogHelper.success(`TCP server version: ${TCP_SERVER_VERSION}`)
|
||||
pastebinData.tcpServer.version = TCP_SERVER_VERSION
|
||||
reportDataInput.tcpServer.version = TCP_SERVER_VERSION
|
||||
|
||||
LogHelper.info('Starting the TCP server...')
|
||||
|
||||
@@ -253,7 +302,7 @@ dotenv.config()
|
||||
]
|
||||
|
||||
LogHelper.info(tcpServerCommand)
|
||||
pastebinData.tcpServer.command = tcpServerCommand
|
||||
reportDataInput.tcpServer.command = tcpServerCommand
|
||||
|
||||
if (osInfo.platform === 'darwin') {
|
||||
LogHelper.info(
|
||||
@@ -280,7 +329,7 @@ dotenv.config()
|
||||
if (!ignoredWarnings.some((w) => newData.includes(w))) {
|
||||
tcpServerOutput += newData
|
||||
report.can_start_tcp_server.v = false
|
||||
pastebinData.tcpServer.error = newData
|
||||
reportDataInput.tcpServer.error = newData
|
||||
LogHelper.error(`Cannot start the TCP server: ${newData}`)
|
||||
}
|
||||
})
|
||||
@@ -292,16 +341,16 @@ dotenv.config()
|
||||
|
||||
const error = `The TCP server timed out after ${timeout}ms`
|
||||
LogHelper.error(error)
|
||||
pastebinData.tcpServer.error = error
|
||||
reportDataInput.tcpServer.error = error
|
||||
report.can_start_tcp_server.v = false
|
||||
}, timeout)
|
||||
|
||||
p.stdout.on('end', async () => {
|
||||
const tcpServerEnd = Date.now()
|
||||
pastebinData.tcpServer.output = tcpServerOutput
|
||||
pastebinData.tcpServer.startTime = `${tcpServerEnd - tcpServerStart}ms`
|
||||
reportDataInput.tcpServer.output = tcpServerOutput
|
||||
reportDataInput.tcpServer.startTime = `${tcpServerEnd - tcpServerStart}ms`
|
||||
LogHelper.info(
|
||||
`TCP server startup time: ${pastebinData.tcpServer.startTime}\n`
|
||||
`TCP server startup time: ${reportDataInput.tcpServer.startTime}\n`
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -325,12 +374,12 @@ dotenv.config()
|
||||
LogHelper.error(
|
||||
`${state}. Try to generate a new one: "npm run train"\n`
|
||||
)
|
||||
pastebinData.nlpModels.globalResolversModelState = state
|
||||
reportDataInput.nlpModels.globalResolversModelState = state
|
||||
} else {
|
||||
const state = 'Found and valid'
|
||||
|
||||
LogHelper.success(`${state}\n`)
|
||||
pastebinData.nlpModels.globalResolversModelState = state
|
||||
reportDataInput.nlpModels.globalResolversModelState = state
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,12 +403,12 @@ dotenv.config()
|
||||
LogHelper.error(
|
||||
`${state}. Try to generate a new one: "npm run train"\n`
|
||||
)
|
||||
pastebinData.nlpModels.skillsResolversModelState = state
|
||||
reportDataInput.nlpModels.skillsResolversModelState = state
|
||||
} else {
|
||||
const state = 'Found and valid'
|
||||
|
||||
LogHelper.success(`${state}\n`)
|
||||
pastebinData.nlpModels.skillsResolversModelState = state
|
||||
reportDataInput.nlpModels.skillsResolversModelState = state
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,12 +431,12 @@ dotenv.config()
|
||||
LogHelper.error(
|
||||
`${state}. Try to generate a new one: "npm run train"\n`
|
||||
)
|
||||
pastebinData.nlpModels.mainModelState = state
|
||||
reportDataInput.nlpModels.mainModelState = state
|
||||
} else {
|
||||
const state = 'Found and valid'
|
||||
|
||||
LogHelper.success(`${state}\n`)
|
||||
pastebinData.nlpModels.mainModelState = state
|
||||
reportDataInput.nlpModels.mainModelState = state
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,7 +574,11 @@ dotenv.config()
|
||||
LogHelper.error('Please fix the errors above')
|
||||
}
|
||||
|
||||
pastebinData.report = report
|
||||
reportDataInput.report = report
|
||||
|
||||
reportDataInput = JSON.parse(
|
||||
SystemHelper.sanitizeUsername(JSON.stringify(reportDataInput))
|
||||
)
|
||||
|
||||
LogHelper.title('REPORT URL')
|
||||
|
||||
@@ -533,11 +586,11 @@ dotenv.config()
|
||||
|
||||
try {
|
||||
const { data } = await axios.post('https://getleon.ai/api/report', {
|
||||
report: pastebinData
|
||||
report: reportDataInput
|
||||
})
|
||||
const { data: reportData } = data
|
||||
const { data: responseReportData } = data
|
||||
|
||||
LogHelper.success(`Report URL: ${reportData.reportUrl}`)
|
||||
LogHelper.success(`Report URL: ${responseReportData.reportUrl}`)
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to send report: ${e}`)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { LogHelper } from '@/helpers/log-helper'
|
||||
'utf8'
|
||||
)
|
||||
const regex =
|
||||
'(build|BREAKING|chore|ci|docs|feat|fix|perf|refactor|style|test)(\\((web app|docker|server|hotword|tcp server|python bridge|skill\\/([\\w-]+)))?\\)?: .{1,50}'
|
||||
'(build|BREAKING|chore|ci|docs|feat|fix|perf|refactor|style|test)(\\((web app|scripts|docker|server|hotword|tcp server|bridge\\/(python|nodejs)|skill\\/([\\w-]+)))?\\)?: .{1,50}'
|
||||
|
||||
if (commitMessage.match(regex) !== null) {
|
||||
LogHelper.success('Commit message validated')
|
||||
|
||||
@@ -14,7 +14,7 @@ dotenv.config()
|
||||
* Generate HTTP API key script
|
||||
* save it in the .env file
|
||||
*/
|
||||
const generateHttpApiKey = () =>
|
||||
const generateHTTPAPIKey = () =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
LogHelper.info('Generating the HTTP API key...')
|
||||
|
||||
@@ -56,17 +56,17 @@ export default () =>
|
||||
!process.env.LEON_HTTP_API_KEY ||
|
||||
process.env.LEON_HTTP_API_KEY === ''
|
||||
) {
|
||||
await generateHttpApiKey()
|
||||
await generateHTTPAPIKey()
|
||||
} else if (!process.env.IS_DOCKER) {
|
||||
const answer = await prompt({
|
||||
type: 'confirm',
|
||||
name: 'generate.httpApiKey',
|
||||
name: 'generate.httpAPIKey',
|
||||
message: 'Do you want to regenerate the HTTP API key?',
|
||||
default: false
|
||||
})
|
||||
|
||||
if (answer.generate.httpApiKey === true) {
|
||||
await generateHttpApiKey()
|
||||
if (answer.generate.httpAPIKey === true) {
|
||||
await generateHTTPAPIKey()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import {
|
||||
domainSchemaObject,
|
||||
skillSchemaObject,
|
||||
skillConfigSchemaObject
|
||||
} from '@/schemas/skill-schemas'
|
||||
import {
|
||||
globalEntitySchemaObject,
|
||||
globalResolverSchemaObject,
|
||||
globalAnswersSchemaObject
|
||||
} from '@/schemas/global-data-schemas'
|
||||
import {
|
||||
amazonVoiceConfiguration,
|
||||
googleCloudVoiceConfiguration,
|
||||
watsonVoiceConfiguration
|
||||
} from '@/schemas/voice-config-schemas'
|
||||
|
||||
/**
|
||||
* Generate JSON schemas
|
||||
* @param {string} categoryName
|
||||
* @param {Map<string, Object>} schemas
|
||||
*/
|
||||
export const generateSchemas = async (categoryName, schemas) => {
|
||||
const categorySchemasPath = path.join(process.cwd(), 'schemas', categoryName)
|
||||
|
||||
await fs.promises.mkdir(categorySchemasPath, { recursive: true })
|
||||
|
||||
for (const [schemaName, schemaObject] of schemas.entries()) {
|
||||
const schemaPath = path.join(categorySchemasPath, `${schemaName}.json`)
|
||||
|
||||
await fs.promises.writeFile(
|
||||
schemaPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
$schema: 'https://json-schema.org/draft-07/schema',
|
||||
...schemaObject
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
LogHelper.info('Generating the JSON schemas...')
|
||||
|
||||
await Promise.all([
|
||||
generateSchemas(
|
||||
'global-data',
|
||||
new Map([
|
||||
['global-entity', globalEntitySchemaObject],
|
||||
['global-resolver', globalResolverSchemaObject],
|
||||
['global-answers', globalAnswersSchemaObject]
|
||||
])
|
||||
),
|
||||
generateSchemas(
|
||||
'skill-schemas',
|
||||
new Map([
|
||||
['domain', domainSchemaObject],
|
||||
['skill', skillSchemaObject],
|
||||
['skill-config', skillConfigSchemaObject]
|
||||
])
|
||||
),
|
||||
generateSchemas(
|
||||
'voice-config-schemas',
|
||||
new Map([
|
||||
['amazon', amazonVoiceConfiguration],
|
||||
['google-cloud', googleCloudVoiceConfiguration],
|
||||
['watson-stt', watsonVoiceConfiguration],
|
||||
['watson-tts', watsonVoiceConfiguration]
|
||||
])
|
||||
)
|
||||
])
|
||||
|
||||
LogHelper.success('JSON schemas generated')
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
import generateJsonSchemas from './generate-json-schemas'
|
||||
|
||||
/**
|
||||
* Execute the generating JSON schemas script
|
||||
*/
|
||||
;(async () => {
|
||||
try {
|
||||
await generateJsonSchemas()
|
||||
} catch (error) {
|
||||
LogHelper.error(`Failed to generate the json schemas: ${error}`)
|
||||
}
|
||||
})()
|
||||
@@ -3,7 +3,11 @@ import path from 'node:path'
|
||||
import { prompt } from 'inquirer'
|
||||
import { command } from 'execa'
|
||||
|
||||
import { PYTHON_BRIDGE_SRC_PATH, TCP_SERVER_SRC_PATH } from '@/constants'
|
||||
import {
|
||||
NODEJS_BRIDGE_SRC_PATH,
|
||||
PYTHON_BRIDGE_SRC_PATH,
|
||||
TCP_SERVER_SRC_PATH
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
|
||||
@@ -15,13 +19,17 @@ import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
|
||||
const BUILD_TARGETS = new Map()
|
||||
|
||||
BUILD_TARGETS.set('nodejs-bridge', {
|
||||
workflowFileName: 'pre-release-nodejs-bridge.yml',
|
||||
versionFilePath: path.join(NODEJS_BRIDGE_SRC_PATH, 'version.ts')
|
||||
})
|
||||
BUILD_TARGETS.set('python-bridge', {
|
||||
workflowFileName: 'pre-release-python-bridge.yml',
|
||||
setupFilePath: path.join(PYTHON_BRIDGE_SRC_PATH, 'setup.py')
|
||||
versionFilePath: path.join(PYTHON_BRIDGE_SRC_PATH, 'version.py')
|
||||
})
|
||||
BUILD_TARGETS.set('tcp-server', {
|
||||
workflowFileName: 'pre-release-tcp-server.yml',
|
||||
setupFilePath: path.join(TCP_SERVER_SRC_PATH, 'setup.py')
|
||||
versionFilePath: path.join(TCP_SERVER_SRC_PATH, 'version.py')
|
||||
})
|
||||
;(async () => {
|
||||
LoaderHelper.start()
|
||||
@@ -29,21 +37,21 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
const { argv } = process
|
||||
const givenReleaseTarget = argv[2].toLowerCase()
|
||||
const givenBranch = argv[3]?.toLowerCase()
|
||||
const { workflowFileName, setupFilePath } =
|
||||
const { workflowFileName, versionFilePath } =
|
||||
BUILD_TARGETS.get(givenReleaseTarget)
|
||||
|
||||
LoaderHelper.stop()
|
||||
const answer = await prompt({
|
||||
type: 'confirm',
|
||||
name: 'binary.bumped',
|
||||
message: `Have you bumped the version number of the binary from the "${setupFilePath}" file?`,
|
||||
message: `Have you bumped the version number of the binary from the "${versionFilePath}" file?`,
|
||||
default: false
|
||||
})
|
||||
LoaderHelper.start()
|
||||
|
||||
if (!answer.binary.bumped) {
|
||||
LogHelper.info(
|
||||
'Please bump the version number of the binary from the setup file before continuing'
|
||||
'Please bump the version number of the binary from the version file before continuing'
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ export default (version) =>
|
||||
LogHelper.info('Updating version...')
|
||||
|
||||
const promises = []
|
||||
const files = ['package.json', 'package-lock.json']
|
||||
// const files = ['package.json', 'package-lock.json']
|
||||
const files = ['package.json']
|
||||
|
||||
for (let i = 0; i < files.length; i += 1) {
|
||||
promises.push(
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { LEON_FILE_PATH } from '@/constants'
|
||||
import { Telemetry } from '@/telemetry'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
const { instanceID, birthDate } = await Telemetry.postInstall()
|
||||
|
||||
if (!fs.existsSync(LEON_FILE_PATH)) {
|
||||
await fs.promises.writeFile(
|
||||
LEON_FILE_PATH,
|
||||
JSON.stringify(
|
||||
{
|
||||
instanceID,
|
||||
birthDate
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
LogHelper.success(`Instance ID created: ${instanceID}`)
|
||||
} else {
|
||||
LogHelper.success(`Instance ID already exists: ${instanceID}`)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.warning(`Failed to create the instance ID: ${e}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import stream from 'node:stream'
|
||||
import readline from 'node:readline'
|
||||
|
||||
import axios from 'axios'
|
||||
import { command } from 'execa'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import prettyMilliseconds from 'pretty-ms'
|
||||
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,
|
||||
NODEJS_BRIDGE_BIN_NAME,
|
||||
PYTHON_BRIDGE_BIN_NAME,
|
||||
TCP_SERVER_BIN_NAME,
|
||||
NODEJS_BRIDGE_VERSION,
|
||||
PYTHON_BRIDGE_VERSION,
|
||||
TCP_SERVER_VERSION
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
/**
|
||||
* Set up binaries according to the given setup target
|
||||
* 1. Delete the existing dist binaries if already exist
|
||||
* 2. Download the latest binaries from GitHub releases
|
||||
* 3. Extract the downloaded ZIP file to the dist folder
|
||||
*/
|
||||
|
||||
const TARGETS = new Map()
|
||||
|
||||
TARGETS.set('nodejs-bridge', {
|
||||
name: 'Node.js bridge',
|
||||
distPath: NODEJS_BRIDGE_DIST_PATH,
|
||||
manifestPath: path.join(NODEJS_BRIDGE_DIST_PATH, 'manifest.json'),
|
||||
archiveName: `${NODEJS_BRIDGE_BIN_NAME.split('.')[0]}.zip`,
|
||||
version: NODEJS_BRIDGE_VERSION,
|
||||
isPlatformDependent: false // Need to be built for the target platform or not
|
||||
})
|
||||
TARGETS.set('python-bridge', {
|
||||
name: 'Python bridge',
|
||||
distPath: PYTHON_BRIDGE_DIST_PATH,
|
||||
manifestPath: path.join(PYTHON_BRIDGE_DIST_PATH, 'manifest.json'),
|
||||
archiveName: `${PYTHON_BRIDGE_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
|
||||
version: PYTHON_BRIDGE_VERSION,
|
||||
isPlatformDependent: true
|
||||
})
|
||||
TARGETS.set('tcp-server', {
|
||||
name: 'TCP server',
|
||||
distPath: TCP_SERVER_DIST_PATH,
|
||||
manifestPath: path.join(TCP_SERVER_DIST_PATH, 'manifest.json'),
|
||||
archiveName: `${TCP_SERVER_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
|
||||
version: TCP_SERVER_VERSION,
|
||||
isPlatformDependent: true
|
||||
})
|
||||
|
||||
async function createManifestFile(manifestPath, name, version) {
|
||||
const manifest = {
|
||||
name,
|
||||
version,
|
||||
setupDate: Date.now()
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2))
|
||||
}
|
||||
|
||||
const setupBinaries = async (key) => {
|
||||
const {
|
||||
name,
|
||||
distPath,
|
||||
archiveName,
|
||||
version,
|
||||
manifestPath,
|
||||
isPlatformDependent
|
||||
} = TARGETS.get(key)
|
||||
let manifest = null
|
||||
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'))
|
||||
|
||||
LogHelper.info(`Found ${name} ${manifest.version}`)
|
||||
LogHelper.info(`Latest version is ${version}`)
|
||||
}
|
||||
|
||||
if (!manifest || manifest.version !== version) {
|
||||
const buildPath = isPlatformDependent
|
||||
? path.join(distPath, BINARIES_FOLDER_NAME)
|
||||
: path.join(distPath, 'bin')
|
||||
const archivePath = path.join(distPath, archiveName)
|
||||
|
||||
await Promise.all([
|
||||
fs.promises.rm(buildPath, { recursive: true, force: true }),
|
||||
fs.promises.rm(archivePath, { recursive: true, force: true })
|
||||
])
|
||||
|
||||
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}...`)
|
||||
|
||||
const archiveWriter = fs.createWriteStream(archivePath)
|
||||
const latestReleaseAssetURL = `${GITHUB_URL}/releases/download/${key}_v${version}/${archiveName}`
|
||||
const { data } = await axios.get(latestReleaseAssetURL, {
|
||||
responseType: 'stream',
|
||||
onDownloadProgress: ({ loaded, total, progress, estimated, rate }) => {
|
||||
const percentage = Math.floor(progress * 100)
|
||||
const downloadedSize = prettyBytes(loaded)
|
||||
const totalSize = prettyBytes(total)
|
||||
const estimatedTime = !estimated
|
||||
? 0
|
||||
: prettyMilliseconds(estimated * 1_000, { secondsDecimalDigits: 0 })
|
||||
const downloadRate = !rate ? 0 : prettyBytes(rate)
|
||||
|
||||
readline.clearLine(process.stdout, 0)
|
||||
readline.cursorTo(process.stdout, 0, null)
|
||||
process.stdout.write(
|
||||
`Download progress: ${percentage}% (${downloadedSize}/${totalSize} | ${downloadRate}/s | ${estimatedTime} ETA)`
|
||||
)
|
||||
|
||||
if (percentage === 100) {
|
||||
process.stdout.write('\n')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
data.pipe(archiveWriter)
|
||||
await stream.promises.finished(archiveWriter)
|
||||
|
||||
LogHelper.success(`${name} downloaded`)
|
||||
LogHelper.info(`Extracting ${name}...`)
|
||||
|
||||
const absoluteDistPath = path.resolve(distPath)
|
||||
await extractZip(archivePath, { dir: absoluteDistPath })
|
||||
|
||||
LogHelper.success(`${name} extracted`)
|
||||
|
||||
await Promise.all([
|
||||
fs.promises.rm(archivePath, { recursive: true, force: true }),
|
||||
createManifestFile(manifestPath, name, version)
|
||||
])
|
||||
|
||||
LogHelper.success(`${name} manifest file created`)
|
||||
LogHelper.success(`${name} ${version} ready`)
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to set up ${name}: ${e}`)
|
||||
}
|
||||
} else {
|
||||
LogHelper.success(`${name} is already at the latest version (${version})`)
|
||||
}
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
await setupBinaries('nodejs-bridge')
|
||||
await setupBinaries('python-bridge')
|
||||
await setupBinaries('tcp-server')
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import stream from 'node:stream'
|
||||
import readline from 'node:readline'
|
||||
|
||||
import axios from 'axios'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import prettyMilliseconds from 'pretty-ms'
|
||||
import extractZip from 'extract-zip'
|
||||
|
||||
import {
|
||||
BINARIES_FOLDER_NAME,
|
||||
GITHUB_URL,
|
||||
PYTHON_BRIDGE_DIST_PATH,
|
||||
TCP_SERVER_DIST_PATH,
|
||||
PYTHON_BRIDGE_BIN_NAME,
|
||||
TCP_SERVER_BIN_NAME,
|
||||
PYTHON_BRIDGE_VERSION,
|
||||
TCP_SERVER_VERSION
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
/**
|
||||
* Set up Python binaries according to the given setup target
|
||||
* 1. Delete the existing dist binaries if already exist
|
||||
* 2. Download the latest Python binaries from GitHub releases
|
||||
* 3. Extract the downloaded ZIP file to the dist folder
|
||||
*/
|
||||
|
||||
const PYTHON_TARGETS = new Map()
|
||||
|
||||
PYTHON_TARGETS.set('python-bridge', {
|
||||
name: 'Python bridge',
|
||||
distPath: PYTHON_BRIDGE_DIST_PATH,
|
||||
archiveName: `${PYTHON_BRIDGE_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
|
||||
version: PYTHON_BRIDGE_VERSION
|
||||
})
|
||||
PYTHON_TARGETS.set('tcp-server', {
|
||||
name: 'TCP server',
|
||||
distPath: TCP_SERVER_DIST_PATH,
|
||||
archiveName: `${TCP_SERVER_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
|
||||
version: TCP_SERVER_VERSION
|
||||
})
|
||||
|
||||
const setupPythonBinaries = async (key) => {
|
||||
const { name, distPath, archiveName, version } = PYTHON_TARGETS.get(key)
|
||||
const buildPath = path.join(distPath, BINARIES_FOLDER_NAME)
|
||||
const archivePath = path.join(distPath, archiveName)
|
||||
|
||||
await Promise.all([
|
||||
fs.promises.rm(buildPath, { recursive: true, force: true }),
|
||||
fs.promises.rm(archivePath, { recursive: true, force: true })
|
||||
])
|
||||
|
||||
try {
|
||||
LogHelper.info(`Downloading ${name}...`)
|
||||
|
||||
const archiveWriter = fs.createWriteStream(archivePath)
|
||||
const latestReleaseAssetURL = `${GITHUB_URL}/releases/download/${key}_v${version}/${archiveName}`
|
||||
const { data } = await axios.get(latestReleaseAssetURL, {
|
||||
responseType: 'stream',
|
||||
onDownloadProgress: ({ loaded, total, progress, estimated, rate }) => {
|
||||
const percentage = Math.floor(progress * 100)
|
||||
const downloadedSize = prettyBytes(loaded)
|
||||
const totalSize = prettyBytes(total)
|
||||
const estimatedTime = !estimated
|
||||
? 0
|
||||
: prettyMilliseconds(estimated * 1_000, { secondsDecimalDigits: 0 })
|
||||
const downloadRate = !rate ? 0 : prettyBytes(rate)
|
||||
|
||||
readline.clearLine(process.stdout, 0)
|
||||
readline.cursorTo(process.stdout, 0, null)
|
||||
process.stdout.write(
|
||||
`Download progress: ${percentage}% (${downloadedSize}/${totalSize} | ${downloadRate}/s | ${estimatedTime} ETA)`
|
||||
)
|
||||
|
||||
if (percentage === 100) {
|
||||
process.stdout.write('\n')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
data.pipe(archiveWriter)
|
||||
await stream.promises.finished(archiveWriter)
|
||||
|
||||
LogHelper.success(`${name} downloaded`)
|
||||
LogHelper.info(`Extracting ${name}...`)
|
||||
|
||||
const absoluteDistPath = path.resolve(distPath)
|
||||
await extractZip(archivePath, { dir: absoluteDistPath })
|
||||
|
||||
LogHelper.success(`${name} extracted`)
|
||||
|
||||
await fs.promises.rm(archivePath, { recursive: true, force: true })
|
||||
|
||||
LogHelper.success(`${name} ready`)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set up ${name}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
await setupPythonBinaries('python-bridge')
|
||||
await setupPythonBinaries('tcp-server')
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
+11
-6
@@ -2,12 +2,14 @@ import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
import train from '../train/train'
|
||||
import generateHttpApiKey from '../generate/generate-http-api-key'
|
||||
import generateHTTPAPIKey from '../generate/generate-http-api-key'
|
||||
import generateJSONSchemas from '../generate/generate-json-schemas'
|
||||
|
||||
import setupDotenv from './setup-dotenv'
|
||||
import setupCore from './setup-core'
|
||||
import setupSkillsConfig from './setup-skills-config'
|
||||
import setupPythonBinaries from './setup-python-binaries'
|
||||
import setupSkills from './setup-skills/setup-skills'
|
||||
import setupBinaries from './setup-binaries'
|
||||
import createInstanceID from './create-instance-id'
|
||||
|
||||
// Do not load ".env" file because it is not created yet
|
||||
|
||||
@@ -18,12 +20,15 @@ import setupPythonBinaries from './setup-python-binaries'
|
||||
try {
|
||||
await setupDotenv()
|
||||
LoaderHelper.start()
|
||||
await Promise.all([setupCore(), setupSkillsConfig()])
|
||||
await setupCore()
|
||||
await setupSkills()
|
||||
LoaderHelper.stop()
|
||||
await setupPythonBinaries()
|
||||
await generateHttpApiKey()
|
||||
await setupBinaries()
|
||||
await generateHTTPAPIKey()
|
||||
await generateJSONSchemas()
|
||||
LoaderHelper.start()
|
||||
await train()
|
||||
await createInstanceID()
|
||||
|
||||
LogHelper.default('')
|
||||
LogHelper.success('Hooray! Leon is installed and ready to go!')
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})()
|
||||
+56
-9
@@ -18,12 +18,29 @@ export const GITHUB_URL = 'https://github.com/leon-ai/leon'
|
||||
* Binaries / distribution
|
||||
*/
|
||||
export const BINARIES_FOLDER_NAME = SystemHelper.getBinariesFolderName()
|
||||
export const PYTHON_BRIDGE_DIST_PATH = path.join('bridges', 'python', 'dist')
|
||||
export const TCP_SERVER_DIST_PATH = path.join('tcp_server', 'dist')
|
||||
export const 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 PYTHON_BRIDGE_SRC_PATH = path.join('bridges', 'python', 'src')
|
||||
export const TCP_SERVER_SRC_PATH = path.join('tcp_server', 'src')
|
||||
export const NODEJS_BRIDGE_DIST_PATH = path.join(
|
||||
NODEJS_BRIDGE_ROOT_PATH,
|
||||
'dist'
|
||||
)
|
||||
export const PYTHON_BRIDGE_DIST_PATH = path.join(
|
||||
PYTHON_BRIDGE_ROOT_PATH,
|
||||
'dist'
|
||||
)
|
||||
export const TCP_SERVER_DIST_PATH = path.join(TCP_SERVER_ROOT_PATH, 'dist')
|
||||
|
||||
export const NODEJS_BRIDGE_SRC_PATH = path.join(NODEJS_BRIDGE_ROOT_PATH, 'src')
|
||||
export const PYTHON_BRIDGE_SRC_PATH = path.join(PYTHON_BRIDGE_ROOT_PATH, 'src')
|
||||
export const TCP_SERVER_SRC_PATH = path.join(TCP_SERVER_ROOT_PATH, 'src')
|
||||
|
||||
const NODEJS_BRIDGE_VERSION_FILE_PATH = path.join(
|
||||
NODEJS_BRIDGE_SRC_PATH,
|
||||
'version.ts'
|
||||
)
|
||||
const PYTHON_BRIDGE_VERSION_FILE_PATH = path.join(
|
||||
PYTHON_BRIDGE_SRC_PATH,
|
||||
'version.py'
|
||||
@@ -32,6 +49,9 @@ const TCP_SERVER_VERSION_FILE_PATH = path.join(
|
||||
TCP_SERVER_SRC_PATH,
|
||||
'version.py'
|
||||
)
|
||||
export const [, NODEJS_BRIDGE_VERSION] = fs
|
||||
.readFileSync(NODEJS_BRIDGE_VERSION_FILE_PATH, 'utf8')
|
||||
.split("'")
|
||||
export const [, PYTHON_BRIDGE_VERSION] = fs
|
||||
.readFileSync(PYTHON_BRIDGE_VERSION_FILE_PATH, 'utf8')
|
||||
.split("'")
|
||||
@@ -39,6 +59,7 @@ export const [, TCP_SERVER_VERSION] = fs
|
||||
.readFileSync(TCP_SERVER_VERSION_FILE_PATH, 'utf8')
|
||||
.split("'")
|
||||
|
||||
export const NODEJS_BRIDGE_BIN_NAME = 'leon-nodejs-bridge.js'
|
||||
export const PYTHON_BRIDGE_BIN_NAME = 'leon-python-bridge'
|
||||
export const TCP_SERVER_BIN_NAME = 'leon-tcp-server'
|
||||
|
||||
@@ -52,6 +73,15 @@ export const PYTHON_BRIDGE_BIN_PATH = path.join(
|
||||
BINARIES_FOLDER_NAME,
|
||||
PYTHON_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']
|
||||
|
||||
/**
|
||||
* spaCy models
|
||||
@@ -91,20 +121,37 @@ export const HAS_OVER_HTTP = process.env['LEON_OVER_HTTP'] === 'true'
|
||||
export const HTTP_API_KEY = process.env['LEON_HTTP_API_KEY']
|
||||
export const HTTP_API_LANG = process.env['LEON_HTTP_API_LANG']
|
||||
|
||||
export const HAS_LOGGER = process.env['LEON_LOGGER'] === 'true'
|
||||
|
||||
export const TCP_SERVER_HOST = process.env['LEON_PY_TCP_SERVER_HOST']
|
||||
export const TCP_SERVER_PORT = Number(process.env['LEON_PY_TCP_SERVER_PORT'])
|
||||
|
||||
export const IS_TELEMETRY_ENABLED = process.env['LEON_TELEMETRY'] === 'true'
|
||||
|
||||
/**
|
||||
* Paths
|
||||
*/
|
||||
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(process.cwd(), 'leon.json')
|
||||
|
||||
/**
|
||||
* Misc
|
||||
*/
|
||||
export const MINIMUM_REQUIRED_RAM = 4
|
||||
export const INSTANCE_ID = fs.existsSync(LEON_FILE_PATH)
|
||||
? JSON.parse(fs.readFileSync(LEON_FILE_PATH, 'utf8')).instanceID
|
||||
: null
|
||||
export const IS_GITPOD = process.env['GITPOD_WORKSPACE_URL'] !== undefined
|
||||
|
||||
@@ -9,21 +9,31 @@ import type {
|
||||
NERCustomEntity,
|
||||
NLUResult
|
||||
} from '@/core/nlp/types'
|
||||
import type { SkillConfigSchema } from '@/schemas/skill-schemas'
|
||||
import type {
|
||||
SkillAnswerConfigSchema,
|
||||
SkillConfigSchema,
|
||||
SkillSchema
|
||||
} from '@/schemas/skill-schemas'
|
||||
import type {
|
||||
BrainProcessResult,
|
||||
IntentObject,
|
||||
SkillResult
|
||||
} from '@/core/brain/types'
|
||||
import { SkillActionType, SkillOutputType } from '@/core/brain/types'
|
||||
import { SkillActionTypes, SkillBridges } from '@/core/brain/types'
|
||||
import { langs } from '@@/core/langs.json'
|
||||
import { HAS_TTS, PYTHON_BRIDGE_BIN_PATH, TMP_PATH } from '@/constants'
|
||||
import {
|
||||
HAS_TTS,
|
||||
PYTHON_BRIDGE_BIN_PATH,
|
||||
NODEJS_BRIDGE_BIN_PATH,
|
||||
TMP_PATH
|
||||
} from '@/constants'
|
||||
import { SOCKET_SERVER, TTS } from '@/core'
|
||||
import { LangHelper } from '@/helpers/lang-helper'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
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
|
||||
@@ -38,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() {
|
||||
@@ -92,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,12 +175,15 @@ export default class Brain {
|
||||
*/
|
||||
private createIntentObject(
|
||||
nluResult: NLUResult,
|
||||
utteranceId: string,
|
||||
utteranceID: string,
|
||||
slots: IntentObject['slots']
|
||||
): IntentObject {
|
||||
const date = DateHelper.getDateTime()
|
||||
const dateObject = new Date(date)
|
||||
|
||||
return {
|
||||
id: utteranceId,
|
||||
lang: this._lang,
|
||||
id: utteranceID,
|
||||
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,
|
||||
@@ -176,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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,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 === SkillOutputType.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(
|
||||
@@ -225,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +286,8 @@ export default class Brain {
|
||||
*/
|
||||
private async executeLogicActionSkill(
|
||||
nluResult: NLUResult,
|
||||
utteranceId: string,
|
||||
skillBridge: SkillSchema['bridge'],
|
||||
utteranceID: string,
|
||||
intentObjectPath: string
|
||||
): Promise<void> {
|
||||
// Ensure the process is empty (to be able to execute other processes outside of Brain)
|
||||
@@ -273,7 +302,7 @@ export default class Brain {
|
||||
|
||||
const intentObject = this.createIntentObject(
|
||||
nluResult,
|
||||
utteranceId,
|
||||
utteranceID,
|
||||
slots
|
||||
)
|
||||
|
||||
@@ -282,10 +311,20 @@ export default class Brain {
|
||||
intentObjectPath,
|
||||
JSON.stringify(intentObject)
|
||||
)
|
||||
this.skillProcess = spawn(
|
||||
`${PYTHON_BRIDGE_BIN_PATH} "${intentObjectPath}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
|
||||
if (skillBridge === SkillBridges.Python) {
|
||||
this.skillProcess = spawn(
|
||||
`${PYTHON_BRIDGE_BIN_PATH} "${intentObjectPath}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
} else if (skillBridge === SkillBridges.NodeJS) {
|
||||
this.skillProcess = spawn(
|
||||
`${NODEJS_BRIDGE_BIN_PATH} "${intentObjectPath}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
} else {
|
||||
LogHelper.error(`The skill bridge is not supported: ${skillBridge}`)
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.error(`Failed to save intent object: ${e}`)
|
||||
}
|
||||
@@ -299,8 +338,8 @@ export default class Brain {
|
||||
const executionTimeStart = Date.now()
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
const utteranceId = `${Date.now()}-${StringHelper.random(4)}`
|
||||
const intentObjectPath = path.join(TMP_PATH, `${utteranceId}.json`)
|
||||
const utteranceID = `${Date.now()}-${StringHelper.random(4)}`
|
||||
const intentObjectPath = path.join(TMP_PATH, `${utteranceID}.json`)
|
||||
const speeches: string[] = []
|
||||
|
||||
// Reset skill output
|
||||
@@ -334,20 +373,25 @@ export default class Brain {
|
||||
? actions[action.next_action]
|
||||
: null
|
||||
|
||||
if (actionType === SkillActionType.Logic) {
|
||||
if (actionType === SkillActionTypes.Logic) {
|
||||
/**
|
||||
* "Logic" action skill execution
|
||||
*/
|
||||
|
||||
this.executeLogicActionSkill(nluResult, utteranceId, intentObjectPath)
|
||||
|
||||
const domainName = nluResult.classification.domain
|
||||
const skillName = nluResult.classification.skill
|
||||
const { name: domainFriendlyName } =
|
||||
await SkillDomainHelper.getSkillDomainInfo(domainName)
|
||||
const { name: skillFriendlyName } =
|
||||
const { name: skillFriendlyName, bridge: skillBridge } =
|
||||
await SkillDomainHelper.getSkillInfo(domainName, skillName)
|
||||
|
||||
await this.executeLogicActionSkill(
|
||||
nluResult,
|
||||
skillBridge,
|
||||
utteranceID,
|
||||
intentObjectPath
|
||||
)
|
||||
|
||||
this.domainFriendlyName = domainFriendlyName
|
||||
this.skillFriendlyName = skillFriendlyName
|
||||
|
||||
@@ -363,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
|
||||
@@ -372,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 === SkillOutputType.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(
|
||||
@@ -438,7 +450,7 @@ export default class Brain {
|
||||
}
|
||||
|
||||
resolve({
|
||||
utteranceId,
|
||||
utteranceID,
|
||||
lang: this._lang,
|
||||
...nluResult,
|
||||
speeches,
|
||||
@@ -468,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 = ''
|
||||
|
||||
@@ -493,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)
|
||||
@@ -567,7 +583,7 @@ export default class Brain {
|
||||
}
|
||||
|
||||
resolve({
|
||||
utteranceId,
|
||||
utteranceID,
|
||||
lang: this._lang,
|
||||
...nluResult,
|
||||
speeches: [answer as string],
|
||||
|
||||
@@ -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,42 +31,68 @@ export interface SkillResult {
|
||||
entities: NEREntity[]
|
||||
slots: NLUSlots
|
||||
output: {
|
||||
type: SkillOutputType
|
||||
codes: string[]
|
||||
speech: string
|
||||
answer: string
|
||||
core: SkillCoreData | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export enum SkillOutputType {
|
||||
Intermediate = 'inter',
|
||||
End = 'end'
|
||||
export enum SkillBridges {
|
||||
Python = 'python',
|
||||
NodeJS = 'nodejs'
|
||||
}
|
||||
export enum SkillActionType {
|
||||
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 {
|
||||
speeches: string[]
|
||||
executionTime: number
|
||||
utteranceId?: string
|
||||
utteranceID?: string
|
||||
lang?: ShortLanguageCode
|
||||
core?: SkillCoreData | undefined
|
||||
action?: SkillConfigSchema['actions'][string]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import { version } from '@@/package.json'
|
||||
import {
|
||||
LEON_VERSION,
|
||||
HAS_AFTER_SPEECH,
|
||||
HAS_LOGGER,
|
||||
HAS_STT,
|
||||
HAS_TTS,
|
||||
STT_PROVIDER,
|
||||
TTS_PROVIDER
|
||||
TTS_PROVIDER,
|
||||
IS_TELEMETRY_ENABLED
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
@@ -30,7 +30,7 @@ export const getInfo: FastifyPluginAsync<APIOptions> = async (
|
||||
code: 'info_pulled',
|
||||
message,
|
||||
after_speech: HAS_AFTER_SPEECH,
|
||||
logger: HAS_LOGGER,
|
||||
telemetry: IS_TELEMETRY_ENABLED,
|
||||
stt: {
|
||||
enabled: HAS_STT,
|
||||
provider: STT_PROVIDER
|
||||
@@ -39,7 +39,7 @@ export const getInfo: FastifyPluginAsync<APIOptions> = async (
|
||||
enabled: HAS_TTS,
|
||||
provider: TTS_PROVIDER
|
||||
},
|
||||
version
|
||||
version: LEON_VERSION
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
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 { version } from '@@/package.json'
|
||||
import { LEON_NODE_ENV, HAS_LOGGER, HAS_OVER_HTTP } from '@/constants'
|
||||
import {
|
||||
LEON_VERSION,
|
||||
LEON_NODE_ENV,
|
||||
HAS_OVER_HTTP,
|
||||
IS_TELEMETRY_ENABLED
|
||||
} from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { DateHelper } from '@/helpers/date-helper'
|
||||
import { corsMidd } from '@/core/http-server/plugins/cors'
|
||||
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'
|
||||
|
||||
@@ -22,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
|
||||
|
||||
@@ -60,12 +51,12 @@ export default class HTTPServer {
|
||||
|
||||
LogHelper.title('Initialization')
|
||||
LogHelper.info(`The current env is ${LEON_NODE_ENV}`)
|
||||
LogHelper.info(`The current version is ${version}`)
|
||||
LogHelper.info(`The current version is ${LEON_VERSION}`)
|
||||
|
||||
LogHelper.info(`The current time zone is ${DateHelper.getTimeZone()}`)
|
||||
|
||||
const sLogger = !HAS_LOGGER ? 'disabled' : 'enabled'
|
||||
LogHelper.info(`Collaborative logger ${sLogger}`)
|
||||
const isTelemetryEnabled = IS_TELEMETRY_ENABLED ? 'enabled' : 'disabled'
|
||||
LogHelper.info(`Telemetry ${isTelemetryEnabled}`)
|
||||
|
||||
await this.bootstrap()
|
||||
}
|
||||
@@ -84,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) => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import kill from 'tree-kill'
|
||||
|
||||
import type { Language, ShortLanguageCode } from '@/types'
|
||||
import type {
|
||||
NLUProcessResult,
|
||||
NLPAction,
|
||||
NLPDomain,
|
||||
NLPJSProcessResult,
|
||||
@@ -12,7 +13,6 @@ import type {
|
||||
NLPUtterance,
|
||||
NLUResult
|
||||
} from '@/core/nlp/types'
|
||||
import type { BrainProcessResult } from '@/core/brain/types'
|
||||
import { langs } from '@@/core/langs.json'
|
||||
import { TCP_SERVER_BIN_PATH } from '@/constants'
|
||||
import { TCP_CLIENT, BRAIN, SOCKET_SERVER, MODEL_LOADER, NER } from '@/core'
|
||||
@@ -21,13 +21,7 @@ import { LangHelper } from '@/helpers/lang-helper'
|
||||
import { ActionLoop } from '@/core/nlp/nlu/action-loop'
|
||||
import { SlotFilling } from '@/core/nlp/nlu/slot-filling'
|
||||
import Conversation, { DEFAULT_ACTIVE_CONTEXT } from '@/core/nlp/conversation'
|
||||
|
||||
type NLUProcessResult = Promise<Partial<
|
||||
BrainProcessResult & {
|
||||
processingTime: number
|
||||
nluProcessingTime: number
|
||||
}
|
||||
> | null>
|
||||
import { Telemetry } from '@/telemetry'
|
||||
|
||||
export const DEFAULT_NLU_RESULT = {
|
||||
utterance: '',
|
||||
@@ -38,6 +32,7 @@ export const DEFAULT_NLU_RESULT = {
|
||||
slots: {},
|
||||
skillConfigPath: '',
|
||||
answers: [], // For dialog action type
|
||||
sentiment: {},
|
||||
classification: {
|
||||
domain: '',
|
||||
skill: '',
|
||||
@@ -91,7 +86,7 @@ export default class NLU {
|
||||
* pick-up the right classification
|
||||
* and extract entities
|
||||
*/
|
||||
public process(utterance: NLPUtterance): NLUProcessResult {
|
||||
public process(utterance: NLPUtterance): Promise<NLUProcessResult | null> {
|
||||
const processingTimeStart = Date.now()
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
@@ -133,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
|
||||
|
||||
/**
|
||||
@@ -164,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 || '',
|
||||
@@ -185,8 +185,6 @@ export default class NLU {
|
||||
return resolve(null)
|
||||
}
|
||||
|
||||
// this.sendLog()
|
||||
|
||||
if (intent === 'None') {
|
||||
const fallback = this.fallback(
|
||||
langs[LangHelper.getLongCode(locale)].fallbacks
|
||||
@@ -202,6 +200,8 @@ export default class NLU {
|
||||
const msg = 'Intent not found'
|
||||
LogHelper.warning(msg)
|
||||
|
||||
Telemetry.utterance({ utterance, lang: BRAIN.lang })
|
||||
|
||||
return resolve(null)
|
||||
}
|
||||
|
||||
|
||||
+227
-104
@@ -1,4 +1,5 @@
|
||||
import type { ShortLanguageCode } from '@/types'
|
||||
import type { BrainProcessResult } from '@/core/brain/types'
|
||||
|
||||
/**
|
||||
* NLP types
|
||||
@@ -9,6 +10,13 @@ export type NLPSkill = string
|
||||
export type NLPAction = string
|
||||
export type NLPUtterance = string
|
||||
|
||||
export type NLUProcessResult = Partial<
|
||||
BrainProcessResult & {
|
||||
processingTime: number
|
||||
nluProcessingTime: number
|
||||
}
|
||||
>
|
||||
|
||||
export interface NLPJSProcessResult {
|
||||
locale: ShortLanguageCode
|
||||
utterance: NLPUtterance
|
||||
@@ -21,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[]
|
||||
@@ -32,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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +87,10 @@ export interface NLUResult {
|
||||
utterance: NLPUtterance
|
||||
skillConfigPath: string
|
||||
answers: { answer: string }[]
|
||||
sentiment: {
|
||||
vote?: NLPJSProcessResult['sentiment']['vote']
|
||||
score?: NLPJSProcessResult['sentiment']['score']
|
||||
}
|
||||
classification: NLUClassification
|
||||
}
|
||||
|
||||
@@ -88,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
|
||||
@@ -172,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'
|
||||
@@ -283,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
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/core'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { LangHelper } from '@/helpers/lang-helper'
|
||||
import { Telemetry } from '@/telemetry'
|
||||
|
||||
interface HotwordDataEvent {
|
||||
hotword: string
|
||||
@@ -115,7 +116,11 @@ export default class SocketServer {
|
||||
LogHelper.time('Utterance processed in')
|
||||
|
||||
BRAIN.isMuted = false
|
||||
await NLU.process(utterance)
|
||||
const processedData = await NLU.process(utterance)
|
||||
|
||||
if (processedData) {
|
||||
Telemetry.utterance(processedData)
|
||||
}
|
||||
|
||||
LogHelper.title('Execution Time')
|
||||
LogHelper.timeEnd('Utterance processed in')
|
||||
|
||||
@@ -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
|
||||
@@ -4,7 +4,7 @@ import path from 'node:path'
|
||||
import { DateHelper } from '@/helpers/date-helper'
|
||||
|
||||
export class LogHelper {
|
||||
static readonly ERRORS_PATH = path.join(
|
||||
static readonly ERRORS_FILE_PATH = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
@@ -47,10 +47,10 @@ export class LogHelper {
|
||||
public static error(value: string): void {
|
||||
const data = `${DateHelper.getDateTime()} - ${value}`
|
||||
|
||||
if (fs.existsSync(LogHelper.ERRORS_PATH)) {
|
||||
fs.appendFileSync(LogHelper.ERRORS_PATH, `\n${data}`)
|
||||
if (fs.existsSync(this.ERRORS_FILE_PATH)) {
|
||||
fs.appendFileSync(this.ERRORS_FILE_PATH, `\n${data}`)
|
||||
} else {
|
||||
fs.writeFileSync(LogHelper.ERRORS_PATH, data, { flag: 'wx' })
|
||||
fs.writeFileSync(this.ERRORS_FILE_PATH, data, { flag: 'wx' })
|
||||
}
|
||||
|
||||
console.error('\x1b[31m🚨 %s\x1b[0m', value)
|
||||
@@ -83,4 +83,31 @@ export class LogHelper {
|
||||
public static timeEnd(value: string): void {
|
||||
console.timeEnd(`🕑 \x1b[36m${value}\x1b[0m`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse error logs and return an array of log errors
|
||||
* @example parseErrorLogs() // 'Failed to connect to the TCP server: Error: read ECONNRESET'
|
||||
*/
|
||||
public static async parseErrorLogs(): Promise<string[]> {
|
||||
if (!fs.existsSync(LogHelper.ERRORS_FILE_PATH)) {
|
||||
const fileHandle = await fs.promises.open(LogHelper.ERRORS_FILE_PATH, 'w')
|
||||
|
||||
await fileHandle.close()
|
||||
}
|
||||
|
||||
const errorFileContent = await fs.promises.readFile(
|
||||
LogHelper.ERRORS_FILE_PATH,
|
||||
'utf8'
|
||||
)
|
||||
const errorLogs = errorFileContent
|
||||
.trim()
|
||||
.split(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2} - /)
|
||||
|
||||
// Remove the first empty string if there's one
|
||||
if (errorLogs[0] === '') {
|
||||
errorLogs.shift()
|
||||
}
|
||||
|
||||
return errorLogs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user