Compare commits
211 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 |
@@ -12,5 +12,4 @@ end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.py]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["skills/**/*.ts"],
|
||||
"rules": {
|
||||
"import/order": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"rules": {
|
||||
|
||||
+2
-4
@@ -8,7 +8,6 @@ __pycache__/
|
||||
test/coverage/
|
||||
**/tmp/*
|
||||
**/src/.venv/*
|
||||
downloads/*
|
||||
logs/*
|
||||
core/config/**/*.json
|
||||
bin/coqui/*
|
||||
@@ -21,6 +20,7 @@ package-lock.json
|
||||
npm-debug.log
|
||||
debug.log
|
||||
.env
|
||||
.last-skill-npm-install
|
||||
leon.json
|
||||
bridges/python/src/Pipfile.lock
|
||||
tcp_server/src/Pipfile.lock
|
||||
@@ -28,9 +28,7 @@ tcp_server/src/Pipfile.lock
|
||||
!bridges/python/**/.gitkeep
|
||||
!bridges/nodejs/**/.gitkeep
|
||||
!**/*.sample*
|
||||
packages/**/config/config.json
|
||||
skills/**/src/config.json
|
||||
packages/**/data/db/*.json
|
||||
skills/**/src/settings.json
|
||||
skills/**/memory/*.json
|
||||
core/data/models/*.nlp
|
||||
package.json.backup
|
||||
|
||||
@@ -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,7 +50,7 @@ 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...
|
||||
|
||||
@@ -226,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()]
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "leon-nodejs-bridge",
|
||||
"description": "Leon's Node.js bridge to communicate between the core and skills made with JavaScript",
|
||||
"main": "dist/leon-nodejs-bridge.js",
|
||||
"main": "dist/bin/leon-nodejs-bridge.js",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
@@ -11,5 +11,12 @@
|
||||
"homepage": "https://getleon.ai",
|
||||
"bugs": {
|
||||
"url": "https://github.com/leon-ai/leon/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "1.4.0",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "4.14.194"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
@@ -1,5 +1,49 @@
|
||||
import { VERSION } from './version'
|
||||
import path from 'node:path'
|
||||
|
||||
console.log('[WIP] Node.js bridge', VERSION)
|
||||
import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import { INTENT_OBJECT } from '@bridge/constants'
|
||||
;(async (): Promise<void> => {
|
||||
const {
|
||||
domain,
|
||||
skill,
|
||||
action,
|
||||
lang,
|
||||
utterance,
|
||||
current_entities,
|
||||
entities,
|
||||
current_resolvers,
|
||||
resolvers,
|
||||
slots,
|
||||
extra_context_data
|
||||
} = INTENT_OBJECT
|
||||
|
||||
// TODO
|
||||
const params: ActionParams = {
|
||||
lang,
|
||||
utterance,
|
||||
current_entities,
|
||||
entities,
|
||||
current_resolvers,
|
||||
resolvers,
|
||||
slots,
|
||||
extra_context_data
|
||||
}
|
||||
|
||||
try {
|
||||
const actionModule = await import(
|
||||
path.join(
|
||||
process.cwd(),
|
||||
'skills',
|
||||
domain,
|
||||
skill,
|
||||
'src',
|
||||
'actions',
|
||||
`${action}.ts`
|
||||
)
|
||||
)
|
||||
const actionFunction: ActionFunction = actionModule.run
|
||||
|
||||
await actionFunction(params)
|
||||
} catch (e) {
|
||||
console.error(`Error while running "${skill}" skill "${action}" action:`, e)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = '0.0.1-alpha.0'
|
||||
export const VERSION = '1.1.0'
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
{
|
||||
"extends": "@tsconfig/node16-strictest/tsconfig.json",
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist/bin",
|
||||
"rootDir": "../../",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@@/*": ["../../*"],
|
||||
"@/*": ["../../server/src/*"],
|
||||
"@server/*": ["../../server/src/*"],
|
||||
"@bridge/*": ["./src/*"],
|
||||
"@sdk/*": ["./src/sdk/*"]
|
||||
},
|
||||
"ignoreDeprecations": "5.0",
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"resolveJsonModule": true
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"declaration": true
|
||||
},
|
||||
"ts-node": {
|
||||
"swc": true,
|
||||
"require": ["tsconfig-paths/register"],
|
||||
"files": true
|
||||
},
|
||||
"files": [],
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist"]
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -5,19 +5,20 @@ from version import __version__
|
||||
|
||||
options = {
|
||||
'build_exe': {
|
||||
# Add common dependencies for skills
|
||||
# Add common dependencies for skills
|
||||
'includes': [
|
||||
'bs4',
|
||||
'pytube'
|
||||
'bs4',
|
||||
'requests',
|
||||
'timeit'
|
||||
],
|
||||
'include_files': [(requests.certs.where(), 'cacert.pem')]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executables = [
|
||||
Executable(
|
||||
script='bridges/python/src/main.py',
|
||||
target_name='leon-python-bridge'
|
||||
Executable(
|
||||
script='bridges/python/src/main.py',
|
||||
target_name='leon-python-bridge'
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -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.1'
|
||||
__version__ = '1.2.0'
|
||||
|
||||
@@ -5,12 +5,6 @@
|
||||
"errors": {
|
||||
"not_found": "Sorry, it seems I cannot find that"
|
||||
},
|
||||
"synchronizer": {
|
||||
"syncing_direct": "I will now synchronize the downloaded content on your current device. Don't worry, I will let you know once I'm done",
|
||||
"synced_direct": "The new content has been synchronized on your device",
|
||||
"syncing_google_drive": "I will now synchronize the downloaded content on Google Drive. Don't worry, I will let you know once I'm done",
|
||||
"synced_google_drive": "The new content is now available on Google Drive"
|
||||
},
|
||||
"random_errors": [
|
||||
"Sorry, there is a problem with my system. Please check my logs for further details",
|
||||
"Sorry, I don't work correctly. Please look at my logs for more information",
|
||||
|
||||
@@ -6,12 +6,6 @@
|
||||
"not_found": "Désolé, il semblerait que je n'arrive pas à trouver ça",
|
||||
"nlu": "L'erreur semble provenir de ma compréhension de langage naturel. Voici plus de détails au sujet de cette dernière : \"%error%\""
|
||||
},
|
||||
"synchronizer": {
|
||||
"syncing_direct": "Je vais maintenant synchroniser le contenu téléchargé sur votre appareil actuel. Ne vous inquiétez pas, je vous préviendrai lorsque j'aurai terminé",
|
||||
"synced_direct": "Le nouveau contenu a été synchronisé sur votre appareil",
|
||||
"syncing_google_drive": "Je vais maintenant synchroniser le contenu téléchargé sur Google Drive. Ne vous inquiétez pas, je vous préviendrai lorsque j'aurai terminé",
|
||||
"synced_google_drive": "Le nouveau contenu est maintenant disponible sur Google Drive"
|
||||
},
|
||||
"random_errors": [
|
||||
"Désolé, il y a un problème avec mon système. Veuillez consulter mes logs pour plus de détails",
|
||||
"Désolé, je ne fonctionne pas correctement. Merci de regarder mes logs pour plus d'information",
|
||||
|
||||
+56
-21
@@ -68,27 +68,6 @@
|
||||
"route": "/api/action/social_communication/mbti/quiz",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/have_i_been_pwned/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/utilities/is_it_down/run",
|
||||
"params": ["url"],
|
||||
"entitiesType": "builtIn"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/speed_test/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/youtube_downloader/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/games/akinator/choose_thematic",
|
||||
@@ -141,6 +120,11 @@
|
||||
"route": "/api/action/games/rochambeau/rematch",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/leon/age/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/leon/color/favorite_color",
|
||||
@@ -166,6 +150,11 @@
|
||||
"route": "/api/action/leon/greeting/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/leon/help/help",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/leon/introduction/introduce_leon",
|
||||
@@ -205,6 +194,52 @@
|
||||
"method": "GET",
|
||||
"route": "/api/action/leon/welcome/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/date_time/current_date_time",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/date_time/current_date",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/date_time/current_time",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/date_time/current_week_number",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/date_time/days_countdown",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/date_time/current_date_time_with_time_zone",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/have_i_been_pwned/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/utilities/is_it_down/run",
|
||||
"params": ["url"],
|
||||
"entitiesType": "builtIn"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"route": "/api/action/utilities/speed_test/run",
|
||||
"params": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+9
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "leon",
|
||||
"version": "1.0.0-beta.9+dev",
|
||||
"version": "1.0.0-beta.10+dev",
|
||||
"description": "Server, skills and web app of the Leon personal assistant",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
@@ -59,6 +59,7 @@
|
||||
"python-bridge": "cross-env PIPENV_PIPFILE=bridges/python/src/Pipfile pipenv run python bridges/python/src/main.py server/src/intent-object.sample.json",
|
||||
"train": "ts-node scripts/train/run-train.js",
|
||||
"prepare-release": "ts-node scripts/release/prepare-release.js",
|
||||
"skill-package": "ts-node scripts/skill-package.js",
|
||||
"pre-release:nodejs-bridge": "ts-node scripts/release/pre-release-binaries.js nodejs-bridge",
|
||||
"pre-release:python-bridge": "ts-node scripts/release/pre-release-binaries.js python-bridge",
|
||||
"pre-release:tcp-server": "ts-node scripts/release/pre-release-binaries.js tcp-server",
|
||||
@@ -100,6 +101,8 @@
|
||||
"os-name": "4.0.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"pretty-ms": "7.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"socket.io": "4.5.2",
|
||||
"socket.io-client": "4.5.2",
|
||||
"stt": "1.4.0",
|
||||
@@ -115,8 +118,12 @@
|
||||
"@types/getos": "3.0.1",
|
||||
"@types/node": "18.7.13",
|
||||
"@types/node-wav": "0.0.0",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.55.0",
|
||||
"@typescript-eslint/parser": "5.55.0",
|
||||
"@vercel/ncc": "0.36.1",
|
||||
"@vitejs/plugin-react": "4.0.0",
|
||||
"cli-spinner": "0.2.10",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
@@ -140,6 +147,6 @@
|
||||
"tsc-watch": "6.0.0",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"typescript": "5.0.2",
|
||||
"vite": "3.0.9"
|
||||
"vite": "4.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"lang": "en",
|
||||
"domain": "leon",
|
||||
"skill": "age",
|
||||
"action": "run",
|
||||
"utterance": "How old are you?",
|
||||
"slots": {},
|
||||
"entities": [],
|
||||
"current_entities": [],
|
||||
"resolvers": [],
|
||||
"current_resolvers": []
|
||||
}
|
||||
@@ -137,19 +137,22 @@ BUILD_TARGETS.set('tcp-server', {
|
||||
* Build for binaries not requiring a Python environment
|
||||
*/
|
||||
try {
|
||||
const tsconfigPath = path.join(NODEJS_BRIDGE_ROOT_PATH, 'tsconfig.json')
|
||||
const distMainFilePath = path.join(
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
'bin',
|
||||
'main.js'
|
||||
)
|
||||
const distBinPath = path.join(NODEJS_BRIDGE_DIST_PATH, 'bin')
|
||||
const distMainFilePath = path.join(distBinPath, 'index.js')
|
||||
const distRenamedMainFilePath = path.join(
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
'bin',
|
||||
distBinPath,
|
||||
NODEJS_BRIDGE_BIN_NAME
|
||||
)
|
||||
|
||||
await command(`tsc --project ${tsconfigPath}`, {
|
||||
await fs.promises.rm(buildPath, { recursive: true, force: true })
|
||||
|
||||
const inputMainFilePath = path.join(
|
||||
NODEJS_BRIDGE_ROOT_PATH,
|
||||
'src',
|
||||
'main.ts'
|
||||
)
|
||||
|
||||
await command(`ncc build ${inputMainFilePath} --out ${distBinPath}`, {
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
|
||||
+52
-5
@@ -16,9 +16,11 @@ import { SystemHelper } from '@/helpers/system-helper'
|
||||
import {
|
||||
MINIMUM_REQUIRED_RAM,
|
||||
LEON_VERSION,
|
||||
NODEJS_BRIDGE_BIN_PATH,
|
||||
PYTHON_BRIDGE_BIN_PATH,
|
||||
TCP_SERVER_BIN_PATH,
|
||||
TCP_SERVER_VERSION,
|
||||
NODEJS_BRIDGE_VERSION,
|
||||
PYTHON_BRIDGE_VERSION,
|
||||
INSTANCE_ID
|
||||
} from '@/constants'
|
||||
@@ -102,6 +104,13 @@ dotenv.config()
|
||||
skillsResolversModelState: null,
|
||||
mainModelState: null
|
||||
},
|
||||
nodeJSBridge: {
|
||||
version: null,
|
||||
executionTime: null,
|
||||
command: null,
|
||||
output: null,
|
||||
error: null
|
||||
},
|
||||
pythonBridge: {
|
||||
version: null,
|
||||
executionTime: null,
|
||||
@@ -145,14 +154,17 @@ dotenv.config()
|
||||
distro: null
|
||||
}
|
||||
const totalRAMInGB = SystemHelper.getTotalRAM()
|
||||
const freeRAMInGB = SystemHelper.getFreeRAM()
|
||||
|
||||
if (Math.round(totalRAMInGB) < MINIMUM_REQUIRED_RAM) {
|
||||
if (Math.round(freeRAMInGB) < MINIMUM_REQUIRED_RAM) {
|
||||
report.can_run.v = false
|
||||
LogHelper.error(
|
||||
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM`
|
||||
`Free RAM: ${freeRAMInGB} GB | Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM`
|
||||
)
|
||||
} else {
|
||||
LogHelper.success(`Total RAM: ${totalRAMInGB} GB`)
|
||||
LogHelper.success(
|
||||
`Free RAM: ${freeRAMInGB} GB | Total RAM: ${totalRAMInGB} GB`
|
||||
)
|
||||
}
|
||||
|
||||
if (osInfo.platform === 'linux') {
|
||||
@@ -166,6 +178,7 @@ dotenv.config()
|
||||
|
||||
reportDataInput.environment.osDetails = osInfo
|
||||
reportDataInput.environment.totalRAMInGB = totalRAMInGB
|
||||
reportDataInput.environment.freeRAMInGB = freeRAMInGB
|
||||
;(
|
||||
await Promise.all([
|
||||
command('node --version', { shell: true }),
|
||||
@@ -205,7 +218,41 @@ dotenv.config()
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill execution checking
|
||||
* Skill execution checking with Node.js bridge
|
||||
*/
|
||||
|
||||
LogHelper.success(`Node.js bridge version: ${NODEJS_BRIDGE_VERSION}`)
|
||||
reportDataInput.nodeJSBridge.version = NODEJS_BRIDGE_VERSION
|
||||
LogHelper.info('Executing a skill...')
|
||||
|
||||
try {
|
||||
const executionStart = Date.now()
|
||||
const p = await command(
|
||||
`${NODEJS_BRIDGE_BIN_PATH} "${path.join(
|
||||
process.cwd(),
|
||||
'scripts',
|
||||
'assets',
|
||||
'nodejs-bridge-intent-object.json'
|
||||
)}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
const executionEnd = Date.now()
|
||||
const executionTime = executionEnd - executionStart
|
||||
LogHelper.info(p.command)
|
||||
reportDataInput.nodeJSBridge.command = p.command
|
||||
LogHelper.success(p.stdout)
|
||||
reportDataInput.nodeJSBridge.output = p.stdout
|
||||
LogHelper.info(`Skill execution time: ${executionTime}ms\n`)
|
||||
reportDataInput.nodeJSBridge.executionTime = `${executionTime}ms`
|
||||
} catch (e) {
|
||||
LogHelper.info(e.command)
|
||||
report.can_run_skill.v = false
|
||||
LogHelper.error(`${e}\n`)
|
||||
reportDataInput.nodeJSBridge.error = JSON.stringify(e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill execution checking with Python bridge
|
||||
*/
|
||||
|
||||
LogHelper.success(`Python bridge version: ${PYTHON_BRIDGE_VERSION}`)
|
||||
@@ -219,7 +266,7 @@ dotenv.config()
|
||||
process.cwd(),
|
||||
'scripts',
|
||||
'assets',
|
||||
'intent-object.json'
|
||||
'python-bridge-intent-object.json'
|
||||
)}"`,
|
||||
{ shell: true }
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import stream from 'node:stream'
|
||||
import readline from 'node:readline'
|
||||
|
||||
import axios from 'axios'
|
||||
import { command } from 'execa'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import prettyMilliseconds from 'pretty-ms'
|
||||
import extractZip from 'extract-zip'
|
||||
@@ -11,6 +12,7 @@ import extractZip from 'extract-zip'
|
||||
import {
|
||||
BINARIES_FOLDER_NAME,
|
||||
GITHUB_URL,
|
||||
NODEJS_BRIDGE_ROOT_PATH,
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
PYTHON_BRIDGE_DIST_PATH,
|
||||
TCP_SERVER_DIST_PATH,
|
||||
@@ -96,6 +98,23 @@ const setupBinaries = async (key) => {
|
||||
fs.promises.rm(archivePath, { recursive: true, force: true })
|
||||
])
|
||||
|
||||
if (key === 'nodejs-bridge') {
|
||||
try {
|
||||
LogHelper.info('Installing Node.js bridge npm packages...')
|
||||
|
||||
await command(
|
||||
`npm install --package-lock=false --prefix ${NODEJS_BRIDGE_ROOT_PATH}`,
|
||||
{
|
||||
shell: true
|
||||
}
|
||||
)
|
||||
|
||||
LogHelper.success('Node.js bridge npm packages installed')
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to install Node.js bridge npm packages: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
LogHelper.info(`Downloading ${name}...`)
|
||||
|
||||
@@ -142,8 +161,8 @@ const setupBinaries = async (key) => {
|
||||
|
||||
LogHelper.success(`${name} manifest file created`)
|
||||
LogHelper.success(`${name} ${version} ready`)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set up ${name}: ${error}`)
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to set up ${name}: ${e}`)
|
||||
}
|
||||
} else {
|
||||
LogHelper.success(`${name} is already at the latest version (${version})`)
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import generateJSONSchemas from '../generate/generate-json-schemas'
|
||||
|
||||
import setupDotenv from './setup-dotenv'
|
||||
import setupCore from './setup-core'
|
||||
import setupSkillsConfig from './setup-skills-config'
|
||||
import setupSkills from './setup-skills/setup-skills'
|
||||
import setupBinaries from './setup-binaries'
|
||||
import createInstanceID from './create-instance-id'
|
||||
|
||||
@@ -20,7 +20,8 @@ import createInstanceID from './create-instance-id'
|
||||
try {
|
||||
await setupDotenv()
|
||||
LoaderHelper.start()
|
||||
await Promise.all([setupCore(), setupSkillsConfig()])
|
||||
await setupCore()
|
||||
await setupSkills()
|
||||
LoaderHelper.stop()
|
||||
await setupBinaries()
|
||||
await generateHTTPAPIKey()
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { command } from 'execa'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { LoaderHelper } from '@/helpers/loader-helper'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
import { PYTHON_BRIDGE_SRC_PATH } from '@/constants'
|
||||
|
||||
/**
|
||||
* Manage Node.js skills npm packages
|
||||
* npm run skill-package {command} {domain}:{skill} {npm package}
|
||||
*/
|
||||
;(async () => {
|
||||
LoaderHelper.start()
|
||||
|
||||
const { argv } = process
|
||||
const givenCommand = argv[2]?.toLowerCase()
|
||||
const givenSkill = argv[3]?.toLowerCase()
|
||||
const givenPackage = argv[4]?.toLowerCase()
|
||||
const supportedCommands = ['install', 'uninstall']
|
||||
const commands = {
|
||||
install: {
|
||||
runningText: 'Installing',
|
||||
doneText: 'installed',
|
||||
command: 'install'
|
||||
},
|
||||
uninstall: {
|
||||
runningText: 'Uninstalling',
|
||||
doneText: 'uninstalled',
|
||||
command: 'uninstall'
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!givenCommand ||
|
||||
!givenSkill ||
|
||||
!givenPackage ||
|
||||
!givenSkill.includes(':')
|
||||
) {
|
||||
LogHelper.error(
|
||||
'Missing skill name or package name. The command should be: "npm run skill-package {command} {domain}:{skill} {npm package}"'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!supportedCommands.includes(givenCommand)) {
|
||||
LogHelper.error(
|
||||
`Unsupported command "${givenCommand}". Supported commands are: ${supportedCommands.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const commandObject = commands[givenCommand]
|
||||
const [domainName, skillName] = givenSkill.split(':')
|
||||
const skillPath = SkillDomainHelper.getSkillPath(domainName, skillName)
|
||||
const skillInfo = await SkillDomainHelper.getSkillInfo(domainName, skillName)
|
||||
const skillSRCPath = path.join(skillPath, 'src')
|
||||
|
||||
if (skillInfo.bridge === 'python') {
|
||||
const libPath = path.join(skillSRCPath, 'lib')
|
||||
const pythonBridgePipfilePath = path.join(PYTHON_BRIDGE_SRC_PATH, 'Pipfile')
|
||||
|
||||
LogHelper.error(
|
||||
`The "${givenSkill}" skill is a Python skill.
|
||||
Hence, you should manually download the "${givenPackage}" package and put it in the "${libPath}" folder.
|
||||
Or, you can verify whether the "${givenPackage}" package is already available via the "${pythonBridgePipfilePath}" file.`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
LogHelper.info(
|
||||
`${commandObject.runningText} "${givenPackage}" npm package for the "${givenSkill}" skill ("${skillSRCPath}")...`
|
||||
)
|
||||
|
||||
await command(
|
||||
`npm ${commandObject.command} --package-lock=false --save-exact=true --prefix ${skillSRCPath} ${givenPackage}`,
|
||||
{
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
}
|
||||
)
|
||||
|
||||
LogHelper.success(
|
||||
`Successfully ${commandObject.doneText} "${givenPackage}" npm package in "${skillSRCPath}"`
|
||||
)
|
||||
} catch (e) {
|
||||
LogHelper.error(
|
||||
`Failed to ${commandObject.command} "${givenPackage}" npm package in "${skillSRCPath}": ${e}`
|
||||
)
|
||||
} finally {
|
||||
LoaderHelper.stop()
|
||||
}
|
||||
})()
|
||||
+22
-12
@@ -18,9 +18,10 @@ export const GITHUB_URL = 'https://github.com/leon-ai/leon'
|
||||
* Binaries / distribution
|
||||
*/
|
||||
export const BINARIES_FOLDER_NAME = SystemHelper.getBinariesFolderName()
|
||||
export const NODEJS_BRIDGE_ROOT_PATH = path.join('bridges', 'nodejs')
|
||||
export const PYTHON_BRIDGE_ROOT_PATH = path.join('bridges', 'python')
|
||||
export const TCP_SERVER_ROOT_PATH = path.join('tcp_server')
|
||||
export const BRIDGES_PATH = path.join(process.cwd(), 'bridges')
|
||||
export const NODEJS_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'nodejs')
|
||||
export const PYTHON_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'python')
|
||||
export const TCP_SERVER_ROOT_PATH = path.join(process.cwd(), 'tcp_server')
|
||||
|
||||
export const NODEJS_BRIDGE_DIST_PATH = path.join(
|
||||
NODEJS_BRIDGE_ROOT_PATH,
|
||||
@@ -72,11 +73,13 @@ export const PYTHON_BRIDGE_BIN_PATH = path.join(
|
||||
BINARIES_FOLDER_NAME,
|
||||
PYTHON_BRIDGE_BIN_NAME
|
||||
)
|
||||
export const NODEJS_BRIDGE_BIN_PATH = `${process.execPath} ${path.join(
|
||||
NODEJS_BRIDGE_DIST_PATH,
|
||||
'bin',
|
||||
NODEJS_BRIDGE_BIN_NAME
|
||||
)}`
|
||||
export const NODEJS_BRIDGE_BIN_PATH = `${path.join(
|
||||
process.cwd(),
|
||||
'node_modules',
|
||||
'ts-node',
|
||||
'dist',
|
||||
'bin.js'
|
||||
)} --swc ${path.join(NODEJS_BRIDGE_DIST_PATH, 'bin', NODEJS_BRIDGE_BIN_NAME)}`
|
||||
|
||||
export const LEON_VERSION = process.env['npm_package_version']
|
||||
|
||||
@@ -126,16 +129,23 @@ export const IS_TELEMETRY_ENABLED = process.env['LEON_TELEMETRY'] === 'true'
|
||||
/**
|
||||
* Paths
|
||||
*/
|
||||
export const BIN_PATH = path.join('bin')
|
||||
export const GLOBAL_DATA_PATH = path.join('core', 'data')
|
||||
export const BIN_PATH = path.join(process.cwd(), 'bin')
|
||||
export const SKILLS_PATH = path.join(process.cwd(), 'skills')
|
||||
export const GLOBAL_DATA_PATH = path.join(process.cwd(), 'core', 'data')
|
||||
export const MODELS_PATH = path.join(GLOBAL_DATA_PATH, 'models')
|
||||
export const VOICE_CONFIG_PATH = path.join('core', 'config', 'voice')
|
||||
export const VOICE_CONFIG_PATH = path.join(
|
||||
process.cwd(),
|
||||
'core',
|
||||
'config',
|
||||
'voice'
|
||||
)
|
||||
export const SERVER_PATH = path.join(
|
||||
process.cwd(),
|
||||
'server',
|
||||
IS_PRODUCTION_ENV ? 'dist' : 'src'
|
||||
)
|
||||
export const TMP_PATH = path.join(SERVER_PATH, 'tmp')
|
||||
export const LEON_FILE_PATH = path.join('leon.json')
|
||||
export const LEON_FILE_PATH = path.join(process.cwd(), 'leon.json')
|
||||
|
||||
/**
|
||||
* Misc
|
||||
|
||||
@@ -9,17 +9,17 @@ import type {
|
||||
NERCustomEntity,
|
||||
NLUResult
|
||||
} from '@/core/nlp/types'
|
||||
import type { SkillConfigSchema, SkillSchema } from '@/schemas/skill-schemas'
|
||||
import type {
|
||||
SkillAnswerConfigSchema,
|
||||
SkillConfigSchema,
|
||||
SkillSchema
|
||||
} from '@/schemas/skill-schemas'
|
||||
import type {
|
||||
BrainProcessResult,
|
||||
IntentObject,
|
||||
SkillResult
|
||||
} from '@/core/brain/types'
|
||||
import {
|
||||
SkillActionTypes,
|
||||
SkillBridges,
|
||||
SkillOutputTypes
|
||||
} from '@/core/brain/types'
|
||||
import { SkillActionTypes, SkillBridges } from '@/core/brain/types'
|
||||
import { langs } from '@@/core/langs.json'
|
||||
import {
|
||||
HAS_TTS,
|
||||
@@ -32,7 +32,8 @@ import { LangHelper } from '@/helpers/lang-helper'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
import Synchronizer from '@/core/synchronizer'
|
||||
import type { AnswerOutput } from '@sdk/types'
|
||||
import { DateHelper } from '@/helpers/date-helper'
|
||||
|
||||
export default class Brain {
|
||||
private static instance: Brain
|
||||
@@ -47,7 +48,7 @@ export default class Brain {
|
||||
private domainFriendlyName = ''
|
||||
private skillFriendlyName = ''
|
||||
private skillOutput = ''
|
||||
private speeches: string[] = []
|
||||
private answers: SkillAnswerConfigSchema[] = []
|
||||
public isMuted = false // Close Leon mouth if true; e.g. over HTTP
|
||||
|
||||
constructor() {
|
||||
@@ -101,19 +102,22 @@ export default class Brain {
|
||||
/**
|
||||
* Make Leon talk
|
||||
*/
|
||||
public talk(rawSpeech: string, end = false): void {
|
||||
public talk(answer: SkillAnswerConfigSchema, end = false): void {
|
||||
LogHelper.title('Brain')
|
||||
LogHelper.info('Talking...')
|
||||
|
||||
if (rawSpeech !== '') {
|
||||
if (answer !== '') {
|
||||
const textAnswer = typeof answer === 'string' ? answer : answer.text
|
||||
const speechAnswer = typeof answer === 'string' ? answer : answer.speech
|
||||
|
||||
if (HAS_TTS) {
|
||||
// Stripe HTML to a whitespace. Whitespace to let the TTS respects punctuation
|
||||
const speech = rawSpeech.replace(/<(?:.|\n)*?>/gm, ' ')
|
||||
const speech = speechAnswer.replace(/<(?:.|\n)*?>/gm, ' ')
|
||||
|
||||
TTS.add(speech, end)
|
||||
}
|
||||
|
||||
SOCKET_SERVER.socket?.emit('answer', rawSpeech)
|
||||
SOCKET_SERVER.socket?.emit('answer', textAnswer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,9 +178,12 @@ export default class Brain {
|
||||
utteranceID: string,
|
||||
slots: IntentObject['slots']
|
||||
): IntentObject {
|
||||
const date = DateHelper.getDateTime()
|
||||
const dateObject = new Date(date)
|
||||
|
||||
return {
|
||||
id: utteranceID,
|
||||
lang: this._lang,
|
||||
lang: this._lang, // TODO: remove once the Python bridge will be updated to use extra_context_data.lang instead
|
||||
domain: nluResult.classification.domain,
|
||||
skill: nluResult.classification.skill,
|
||||
action: nluResult.classification.action,
|
||||
@@ -185,7 +192,16 @@ export default class Brain {
|
||||
entities: nluResult.entities,
|
||||
current_resolvers: nluResult.currentResolvers,
|
||||
resolvers: nluResult.resolvers,
|
||||
slots
|
||||
slots,
|
||||
extra_context_data: {
|
||||
lang: this._lang,
|
||||
sentiment: nluResult.sentiment,
|
||||
date: date.slice(0, 10),
|
||||
time: date.slice(11, 19),
|
||||
timestamp: dateObject.getTime(),
|
||||
date_time: date,
|
||||
week_day: dateObject.toLocaleString('default', { weekday: 'long' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,22 +212,23 @@ export default class Brain {
|
||||
data: Buffer
|
||||
): Promise<Error | null> | void {
|
||||
try {
|
||||
const obj = JSON.parse(data.toString())
|
||||
const skillAnswer = JSON.parse(data.toString()) as AnswerOutput
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
if (obj.output.type === SkillOutputTypes.Intermediate) {
|
||||
LogHelper.title(`${this.skillFriendlyName} skill`)
|
||||
LogHelper.info(data.toString())
|
||||
if (typeof skillAnswer === 'object') {
|
||||
LogHelper.title(`${this.skillFriendlyName} skill (on data)`)
|
||||
LogHelper.info(data.toString())
|
||||
|
||||
const speech = obj.output.speech.toString()
|
||||
if (!this.isMuted) {
|
||||
this.talk(speech)
|
||||
}
|
||||
this.speeches.push(speech)
|
||||
} else {
|
||||
this.skillOutput = data.toString()
|
||||
if (skillAnswer.output.widget) {
|
||||
SOCKET_SERVER.socket?.emit('widget', skillAnswer.output.widget)
|
||||
}
|
||||
|
||||
const { answer } = skillAnswer.output
|
||||
if (!this.isMuted) {
|
||||
this.talk(answer)
|
||||
}
|
||||
this.answers.push(answer)
|
||||
this.skillOutput = data.toString()
|
||||
|
||||
return Promise.resolve(null)
|
||||
} else {
|
||||
return Promise.reject(
|
||||
@@ -234,11 +251,13 @@ export default class Brain {
|
||||
'%skill_name%': this.skillFriendlyName,
|
||||
'%domain_name%': this.domainFriendlyName
|
||||
})}!`
|
||||
|
||||
if (!this.isMuted) {
|
||||
this.talk(speech)
|
||||
SOCKET_SERVER.socket?.emit('is-typing', false)
|
||||
}
|
||||
this.speeches.push(speech)
|
||||
|
||||
this.answers.push(speech)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -388,7 +407,7 @@ export default class Brain {
|
||||
|
||||
// Catch the end of the skill execution
|
||||
this.skillProcess?.stdout.on('end', () => {
|
||||
LogHelper.title(`${this.skillFriendlyName} skill`)
|
||||
LogHelper.title(`${this.skillFriendlyName} skill (on end)`)
|
||||
LogHelper.info(this.skillOutput)
|
||||
|
||||
let skillResult: SkillResult | undefined = undefined
|
||||
@@ -397,38 +416,6 @@ export default class Brain {
|
||||
if (this.skillOutput !== '') {
|
||||
try {
|
||||
skillResult = JSON.parse(this.skillOutput)
|
||||
|
||||
if (skillResult?.output.speech) {
|
||||
skillResult.output.speech =
|
||||
skillResult.output.speech.toString()
|
||||
if (!this.isMuted) {
|
||||
this.talk(skillResult.output.speech, true)
|
||||
}
|
||||
speeches.push(skillResult.output.speech)
|
||||
|
||||
// Synchronize the downloaded content if enabled
|
||||
if (
|
||||
skillResult.output.type === SkillOutputTypes.End &&
|
||||
skillResult.output.options['synchronization'] &&
|
||||
skillResult.output.options['synchronization'].enabled &&
|
||||
skillResult.output.options['synchronization'].enabled ===
|
||||
true
|
||||
) {
|
||||
const sync = new Synchronizer(
|
||||
this,
|
||||
nluResult.classification,
|
||||
skillResult.output.options['synchronization']
|
||||
)
|
||||
|
||||
// When the synchronization is finished
|
||||
sync.synchronize((speech: string) => {
|
||||
if (!this.isMuted) {
|
||||
this.talk(speech)
|
||||
}
|
||||
speeches.push(speech)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.title(`${this.skillFriendlyName} skill`)
|
||||
LogHelper.error(
|
||||
@@ -493,6 +480,8 @@ export default class Brain {
|
||||
await SkillDomainHelper.getSkillConfig(configFilePath, this._lang)
|
||||
const utteranceHasEntities = nluResult.entities.length > 0
|
||||
const { answers: rawAnswers } = nluResult
|
||||
// TODO: handle dialog action skill speech vs text
|
||||
// let answers = rawAnswers as [{ answer: SkillAnswerConfigSchema }]
|
||||
let answers = rawAnswers
|
||||
let answer: string | undefined = ''
|
||||
|
||||
@@ -518,6 +507,8 @@ export default class Brain {
|
||||
actions[nluResult.classification.action]?.unknown_answers
|
||||
|
||||
if (unknownAnswers) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
answer =
|
||||
unknownAnswers[
|
||||
Math.floor(Math.random() * unknownAnswers.length)
|
||||
|
||||
@@ -9,7 +9,10 @@ import type {
|
||||
NLUSlot,
|
||||
NLUSlots
|
||||
} from '@/core/nlp/types'
|
||||
import type { SkillConfigSchema } from '@/schemas/skill-schemas'
|
||||
import type {
|
||||
SkillConfigSchema,
|
||||
SkillAnswerConfigSchema
|
||||
} from '@/schemas/skill-schemas'
|
||||
import type { ShortLanguageCode } from '@/types'
|
||||
|
||||
interface SkillCoreData {
|
||||
@@ -28,9 +31,8 @@ export interface SkillResult {
|
||||
entities: NEREntity[]
|
||||
slots: NLUSlots
|
||||
output: {
|
||||
type: SkillOutputTypes
|
||||
codes: string[]
|
||||
speech: string
|
||||
answer: string
|
||||
core: SkillCoreData | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: Record<string, any>
|
||||
@@ -41,27 +43,50 @@ export enum SkillBridges {
|
||||
Python = 'python',
|
||||
NodeJS = 'nodejs'
|
||||
}
|
||||
export enum SkillOutputTypes {
|
||||
Intermediate = 'inter',
|
||||
End = 'end'
|
||||
}
|
||||
export enum SkillActionTypes {
|
||||
Logic = 'logic',
|
||||
Dialog = 'dialog'
|
||||
}
|
||||
|
||||
export interface IntentObject {
|
||||
id: string
|
||||
export interface ActionParams {
|
||||
lang: ShortLanguageCode
|
||||
domain: NLPDomain
|
||||
skill: NLPSkill
|
||||
action: NLPAction
|
||||
utterance: NLPUtterance
|
||||
current_entities: NEREntity[]
|
||||
entities: NEREntity[]
|
||||
current_resolvers: NLUResolver[]
|
||||
resolvers: NLUResolver[]
|
||||
slots: { [key: string]: NLUSlot['value'] | undefined }
|
||||
extra_context_data: {
|
||||
lang: ShortLanguageCode
|
||||
sentiment: NLUResult['sentiment']
|
||||
date: string
|
||||
time: string
|
||||
timestamp: number
|
||||
date_time: string
|
||||
week_day: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface IntentObject extends ActionParams {
|
||||
id: string
|
||||
domain: NLPDomain
|
||||
skill: NLPSkill
|
||||
action: NLPAction
|
||||
}
|
||||
|
||||
export interface SkillAnswerCoreData {
|
||||
restart?: boolean
|
||||
isInActionLoop?: boolean
|
||||
showNextActionSuggestions?: boolean
|
||||
showSuggestions?: boolean
|
||||
}
|
||||
export interface SkillAnswerOutput extends IntentObject {
|
||||
output: {
|
||||
codes: string
|
||||
answer: SkillAnswerConfigSchema
|
||||
core?: SkillAnswerCoreData
|
||||
widget?: unknown // TODO
|
||||
}
|
||||
}
|
||||
|
||||
export interface BrainProcessResult extends NLUResult {
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import type { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import archiver from 'archiver'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import type { Static } from '@sinclair/typebox'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
|
||||
const getDownloadsSchema = {
|
||||
querystring: Type.Object({
|
||||
domain: Type.String(),
|
||||
skill: Type.String()
|
||||
})
|
||||
} satisfies FastifySchema
|
||||
|
||||
interface GetDownloadsSchema {
|
||||
querystring: Static<typeof getDownloadsSchema.querystring>
|
||||
}
|
||||
|
||||
export const getDownloads: FastifyPluginAsync<APIOptions> = async (
|
||||
fastify,
|
||||
options
|
||||
) => {
|
||||
fastify.route<{
|
||||
Querystring: GetDownloadsSchema['querystring']
|
||||
}>({
|
||||
method: 'GET',
|
||||
url: `/api/${options.apiVersion}/downloads`,
|
||||
schema: getDownloadsSchema,
|
||||
handler: async (request, reply) => {
|
||||
LogHelper.title('GET /downloads')
|
||||
|
||||
const clean = async (dir: string, files: string[]): Promise<void> => {
|
||||
LogHelper.info('Cleaning skill download directory...')
|
||||
for (let i = 0; i < files.length; i += 1) {
|
||||
await fs.promises.unlink(`${dir}/${files[i]}`)
|
||||
}
|
||||
await fs.promises.rmdir(dir)
|
||||
LogHelper.success('Downloads directory cleaned')
|
||||
}
|
||||
let message = ''
|
||||
|
||||
if (request.query.domain && request.query.skill) {
|
||||
const dlDomainDir = path.join(
|
||||
process.cwd(),
|
||||
'downloads',
|
||||
request.query.domain
|
||||
)
|
||||
const skill = path.join(dlDomainDir, `${request.query.skill}.py`)
|
||||
|
||||
LogHelper.info(
|
||||
`Checking existence of the ${StringHelper.ucFirst(
|
||||
request.query.skill
|
||||
)} skill...`
|
||||
)
|
||||
if (fs.existsSync(skill)) {
|
||||
LogHelper.success(
|
||||
`${StringHelper.ucFirst(request.query.skill)} skill exists`
|
||||
)
|
||||
const downloadsDir = `${dlDomainDir}/${request.query.skill}`
|
||||
|
||||
LogHelper.info('Reading downloads directory...')
|
||||
try {
|
||||
const files = await fs.promises.readdir(downloadsDir)
|
||||
// Download the file if there is only one
|
||||
if (files.length === 1) {
|
||||
LogHelper.info(`${files[0]} is downloading...`)
|
||||
reply.download(`${downloadsDir}/${files[0]}`)
|
||||
LogHelper.success(`${files[0]} downloaded`)
|
||||
await clean(downloadsDir, files)
|
||||
} else {
|
||||
LogHelper.info('Deleting previous archives...')
|
||||
const zipSlug = `leon-${request.query.domain}-${request.query.skill}`
|
||||
const domainsFiles = await fs.promises.readdir(dlDomainDir)
|
||||
|
||||
for (let i = 0; i < domainsFiles.length; i += 1) {
|
||||
if (
|
||||
domainsFiles[i]?.indexOf('.zip') !== -1 &&
|
||||
domainsFiles[i]?.indexOf(zipSlug) !== -1
|
||||
) {
|
||||
await fs.promises.unlink(`${dlDomainDir}/${domainsFiles[i]}`)
|
||||
LogHelper.success(`${domainsFiles[i]} archive deleted`)
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.info('Preparing new archive...')
|
||||
const zipName = `${zipSlug}-${Date.now()}.zip`
|
||||
const zipFile = `${dlDomainDir}/${zipName}`
|
||||
const output = fs.createWriteStream(zipFile)
|
||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
||||
|
||||
// When the archive is ready
|
||||
output.on('close', async () => {
|
||||
LogHelper.info(`${zipName} is downloading...`)
|
||||
reply.download(zipFile)
|
||||
LogHelper.success(`${zipName} downloaded`)
|
||||
await clean(downloadsDir, files)
|
||||
})
|
||||
archive.on('error', (err) => {
|
||||
LogHelper.error(err.message)
|
||||
})
|
||||
|
||||
// Add the content to the archive
|
||||
LogHelper.info('Adding content...')
|
||||
archive.directory(downloadsDir, false)
|
||||
|
||||
// Inject stream data to the archive
|
||||
LogHelper.info('Injecting stream data...')
|
||||
archive.pipe(output)
|
||||
|
||||
LogHelper.info('Finalizing...')
|
||||
archive.finalize()
|
||||
}
|
||||
} catch (error) {
|
||||
message = 'There is no content to download for this skill.'
|
||||
LogHelper.error(message)
|
||||
reply.code(404).send({
|
||||
success: false,
|
||||
status: 404,
|
||||
code: 'skill_dir_not_found',
|
||||
message
|
||||
})
|
||||
|
||||
LogHelper.error(message)
|
||||
}
|
||||
} else {
|
||||
message = 'This skill does not exist.'
|
||||
LogHelper.error(message)
|
||||
reply.code(404).send({
|
||||
success: false,
|
||||
status: 404,
|
||||
code: 'skill_not_found',
|
||||
message
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message = 'Bad request.'
|
||||
LogHelper.error(message)
|
||||
reply.code(400).send({
|
||||
success: false,
|
||||
status: 400,
|
||||
code: 'bad_request',
|
||||
message
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
import { getDownloads } from '@/core/http-server/api/downloads/get'
|
||||
|
||||
export const downloadsPlugin: FastifyPluginAsync<APIOptions> = async (
|
||||
fastify,
|
||||
options
|
||||
) => {
|
||||
// Get downloads to download skill content
|
||||
await fastify.register(getDownloads, options)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import { postUtterance } from '@/core/http-server/api/utterance/post'
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
|
||||
export const utterancePlugin: FastifyPluginAsync<APIOptions> = async (
|
||||
fastify,
|
||||
options
|
||||
) => {
|
||||
await fastify.register(postUtterance, options)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import type { Static } from '@sinclair/typebox'
|
||||
|
||||
import { NLU, BRAIN } from '@/core'
|
||||
import type { APIOptions } from '@/core/http-server/http-server'
|
||||
|
||||
const postUtteranceSchema = {
|
||||
body: Type.Object({
|
||||
utterance: Type.String()
|
||||
})
|
||||
} satisfies FastifySchema
|
||||
|
||||
interface PostUtteranceSchema {
|
||||
body: Static<typeof postUtteranceSchema.body>
|
||||
}
|
||||
|
||||
export const postUtterance: FastifyPluginAsync<APIOptions> = async (
|
||||
fastify,
|
||||
options
|
||||
) => {
|
||||
fastify.route<{
|
||||
Body: PostUtteranceSchema['body']
|
||||
}>({
|
||||
method: 'POST',
|
||||
url: `/api/${options.apiVersion}/utterance`,
|
||||
schema: postUtteranceSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { utterance } = request.body
|
||||
|
||||
try {
|
||||
BRAIN.isMuted = true
|
||||
const data = await NLU.process(utterance)
|
||||
|
||||
reply.send({
|
||||
...data,
|
||||
success: true
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : error
|
||||
reply.statusCode = 500
|
||||
reply.send({
|
||||
message,
|
||||
success: false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { join } from 'node:path'
|
||||
|
||||
import Fastify, { FastifySchema } from 'fastify'
|
||||
import Fastify from 'fastify'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import type { Static } from '@sinclair/typebox'
|
||||
|
||||
import {
|
||||
LEON_VERSION,
|
||||
@@ -16,9 +14,8 @@ import { DateHelper } from '@/helpers/date-helper'
|
||||
import { corsMidd } from '@/core/http-server/plugins/cors'
|
||||
import { otherMidd } from '@/core/http-server/plugins/other'
|
||||
import { infoPlugin } from '@/core/http-server/api/info'
|
||||
import { downloadsPlugin } from '@/core/http-server/api/downloads'
|
||||
import { keyMidd } from '@/core/http-server/plugins/key'
|
||||
import { NLU, BRAIN } from '@/core'
|
||||
import { utterancePlugin } from '@/core/http-server/api/utterance'
|
||||
|
||||
const API_VERSION = 'v1'
|
||||
|
||||
@@ -26,16 +23,6 @@ export interface APIOptions {
|
||||
apiVersion: string
|
||||
}
|
||||
|
||||
const postQuerySchema = {
|
||||
body: Type.Object({
|
||||
utterance: Type.String()
|
||||
})
|
||||
} satisfies FastifySchema
|
||||
|
||||
interface PostQuerySchema {
|
||||
body: Static<typeof postQuerySchema.body>
|
||||
}
|
||||
|
||||
export default class HTTPServer {
|
||||
private static instance: HTTPServer
|
||||
|
||||
@@ -88,39 +75,12 @@ export default class HTTPServer {
|
||||
})
|
||||
|
||||
this.fastify.register(infoPlugin, { apiVersion: API_VERSION })
|
||||
this.fastify.register(downloadsPlugin, { apiVersion: API_VERSION })
|
||||
|
||||
if (HAS_OVER_HTTP) {
|
||||
this.fastify.register((instance, _opts, next) => {
|
||||
instance.addHook('preHandler', keyMidd)
|
||||
|
||||
instance.route<{
|
||||
Body: PostQuerySchema['body']
|
||||
}>({
|
||||
method: 'POST',
|
||||
url: '/api/query',
|
||||
schema: postQuerySchema,
|
||||
handler: async (request, reply) => {
|
||||
const { utterance } = request.body
|
||||
|
||||
try {
|
||||
BRAIN.isMuted = true
|
||||
const data = await NLU.process(utterance)
|
||||
|
||||
reply.send({
|
||||
...data,
|
||||
success: true
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : error
|
||||
reply.statusCode = 500
|
||||
reply.send({
|
||||
message,
|
||||
success: false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
instance.register(utterancePlugin, { apiVersion: API_VERSION })
|
||||
|
||||
// TODO: reimplement skills routes once the new core is ready
|
||||
// server.generateSkillsRoutes(instance)
|
||||
|
||||
@@ -32,7 +32,6 @@ import corsMidd from '@/core/http-server/plugins/cors'
|
||||
import otherMidd from '@/core/http-server/plugins/other'
|
||||
import keyMidd from '@/core/http-server/plugins/key'
|
||||
import infoPlugin from '@/core/http-server/api/info'
|
||||
import downloadsPlugin from '@/core/http-server/api/downloads'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { DateHelper } from '@/helpers/date-helper'
|
||||
|
||||
@@ -337,7 +336,6 @@ server.bootstrap = async () => {
|
||||
})
|
||||
|
||||
server.fastify.register(infoPlugin, { apiVersion })
|
||||
server.fastify.register(downloadsPlugin, { apiVersion })
|
||||
|
||||
if (HAS_OVER_HTTP) {
|
||||
server.fastify.register((instance, opts, next) => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ export const DEFAULT_NLU_RESULT = {
|
||||
slots: {},
|
||||
skillConfigPath: '',
|
||||
answers: [], // For dialog action type
|
||||
sentiment: {},
|
||||
classification: {
|
||||
domain: '',
|
||||
skill: '',
|
||||
@@ -127,6 +128,10 @@ export default class NLU {
|
||||
const result: NLPJSProcessResult =
|
||||
await MODEL_LOADER.mainNLPContainer.process(utterance)
|
||||
const { locale, answers, classifications } = result
|
||||
const sentiment = {
|
||||
vote: result.sentiment.vote,
|
||||
score: result.sentiment.score
|
||||
}
|
||||
let { score, intent, domain } = result
|
||||
|
||||
/**
|
||||
@@ -158,6 +163,7 @@ export default class NLU {
|
||||
...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
|
||||
utterance,
|
||||
answers, // For dialog action type
|
||||
sentiment,
|
||||
classification: {
|
||||
domain,
|
||||
skill: skillName || '',
|
||||
|
||||
+219
-104
@@ -29,7 +29,8 @@ export interface NLPJSProcessResult {
|
||||
intent: string
|
||||
score: number
|
||||
}[]
|
||||
intent: string // E.g. "greeting.run"
|
||||
/** E.g. "greeting.run" */
|
||||
intent: string
|
||||
score: number
|
||||
domain: NLPDomain
|
||||
sourceEntities: unknown[]
|
||||
@@ -40,13 +41,14 @@ export interface NLPJSProcessResult {
|
||||
answer: string | undefined
|
||||
actions: NLPAction[]
|
||||
sentiment: {
|
||||
/** Rule of thumb: > 0 = negative; = 0 = neutral; < 0 = positive */
|
||||
score: number
|
||||
numWords: number
|
||||
numHits: number
|
||||
average: number
|
||||
type: string
|
||||
locale: ShortLanguageCode
|
||||
vote: string
|
||||
vote: 'positive' | 'neutral' | 'negative'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +87,10 @@ export interface NLUResult {
|
||||
utterance: NLPUtterance
|
||||
skillConfigPath: string
|
||||
answers: { answer: string }[]
|
||||
sentiment: {
|
||||
vote?: NLPJSProcessResult['sentiment']['vote']
|
||||
score?: NLPJSProcessResult['sentiment']['score']
|
||||
}
|
||||
classification: NLUClassification
|
||||
}
|
||||
|
||||
@@ -96,83 +102,151 @@ export type NLUSlots = Record<string, NLUSlot>
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
|
||||
interface Entity {
|
||||
export const BUILT_IN_ENTITY_TYPES = [
|
||||
'number',
|
||||
'ip',
|
||||
'hashtag',
|
||||
'phonenumber',
|
||||
'currency',
|
||||
'percentage',
|
||||
'date',
|
||||
'time',
|
||||
'timerange',
|
||||
'daterange',
|
||||
'datetimerange',
|
||||
'duration',
|
||||
'dimension',
|
||||
'email',
|
||||
'ordinal',
|
||||
'age',
|
||||
'url',
|
||||
'temperature'
|
||||
] as const
|
||||
|
||||
export type BuiltInEntityType = (typeof BUILT_IN_ENTITY_TYPES)[number]
|
||||
|
||||
export const CUSTOM_ENTITY_TYPES = ['regex', 'trim', 'enum'] as const
|
||||
|
||||
export type CustomEntityType = (typeof CUSTOM_ENTITY_TYPES)[number]
|
||||
|
||||
export const SPACY_ENTITY_TYPES = [
|
||||
'location:country',
|
||||
'location:city',
|
||||
'person',
|
||||
'organization'
|
||||
] as const
|
||||
|
||||
export type SpacyEntityType = (typeof SPACY_ENTITY_TYPES)[number]
|
||||
|
||||
export const ENTITY_TYPES = [
|
||||
...BUILT_IN_ENTITY_TYPES,
|
||||
...CUSTOM_ENTITY_TYPES,
|
||||
...SPACY_ENTITY_TYPES
|
||||
] as const
|
||||
|
||||
export type EntityType = (typeof ENTITY_TYPES)[number]
|
||||
|
||||
interface Entity<
|
||||
Type extends EntityType,
|
||||
Resolution extends Record<string, unknown>,
|
||||
EntityName extends string = Type
|
||||
> {
|
||||
start: number
|
||||
end: number
|
||||
len: number
|
||||
accuracy: number
|
||||
sourceText: string
|
||||
utteranceText: string
|
||||
entity: unknown
|
||||
resolution: unknown
|
||||
entity: EntityName
|
||||
type: Type
|
||||
resolution: Resolution
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in entity types
|
||||
*/
|
||||
|
||||
interface BuiltInEntity extends Entity {}
|
||||
export interface BuiltInEntity<
|
||||
Type extends BuiltInEntityType,
|
||||
Resolution extends Record<string, unknown>
|
||||
> extends Entity<Type, Resolution> {}
|
||||
|
||||
interface BuiltInNumberEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
export type BuiltInNumberEntity = BuiltInEntity<
|
||||
'number',
|
||||
{
|
||||
strValue: string
|
||||
value: number
|
||||
subtype: string
|
||||
}
|
||||
}
|
||||
interface BuiltInIPEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInIPEntity = BuiltInEntity<
|
||||
'ip',
|
||||
{
|
||||
value: string
|
||||
type: string
|
||||
type: 'ipv4' | 'ipv6'
|
||||
}
|
||||
}
|
||||
interface BuiltInHashtagEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInHashtagEntity = BuiltInEntity<
|
||||
'hashtag',
|
||||
{
|
||||
value: string
|
||||
}
|
||||
}
|
||||
interface BuiltInPhoneNumberEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInPhoneNumberEntity = BuiltInEntity<
|
||||
'phonenumber',
|
||||
{
|
||||
value: string
|
||||
score: string
|
||||
}
|
||||
}
|
||||
interface BuiltInCurrencyEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInCurrencyEntity = BuiltInEntity<
|
||||
'currency',
|
||||
{
|
||||
strValue: string
|
||||
value: number
|
||||
unit: string
|
||||
localeUnit: string
|
||||
}
|
||||
}
|
||||
interface BuiltInPercentageEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInPercentageEntity = BuiltInEntity<
|
||||
'percentage',
|
||||
{
|
||||
strValue: string
|
||||
value: number
|
||||
subtype: string
|
||||
}
|
||||
}
|
||||
interface BuiltInDateEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
type: string
|
||||
timex: string
|
||||
strPastValue: string
|
||||
pastDate: Date
|
||||
strFutureValue: string
|
||||
futureDate: Date
|
||||
}
|
||||
}
|
||||
interface BuiltInTimeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
|
||||
export type BuiltInDateEntity = BuiltInEntity<
|
||||
'date',
|
||||
| {
|
||||
type: 'date'
|
||||
timex: string
|
||||
strValue: string
|
||||
date: string
|
||||
}
|
||||
| {
|
||||
type: 'interval'
|
||||
timex: string
|
||||
strPastValue: string
|
||||
pastDate: string
|
||||
strFutureValue: string
|
||||
futureDate: string
|
||||
}
|
||||
>
|
||||
export type BuiltInTimeEntity = BuiltInEntity<
|
||||
'time',
|
||||
{
|
||||
values: {
|
||||
timex: string
|
||||
type: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
interface BuiltInTimeRangeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInTimeRangeEntity = BuiltInEntity<
|
||||
'timerange',
|
||||
{
|
||||
values: {
|
||||
timex: string
|
||||
type: string
|
||||
@@ -180,108 +254,116 @@ interface BuiltInTimeRangeEntity extends BuiltInEntity {
|
||||
end: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
interface BuiltInDateRangeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInDateRangeEntity = BuiltInEntity<
|
||||
'daterange',
|
||||
{
|
||||
type: 'interval'
|
||||
timex: string
|
||||
strPastStartValue: string
|
||||
pastStartDate: string
|
||||
strPastEndValue: string
|
||||
pastEndDate: string
|
||||
strFutureStartValue: string
|
||||
futureStartDate: string
|
||||
strFutureEndValue: string
|
||||
futureEndDate: string
|
||||
}
|
||||
>
|
||||
export type BuiltInDateTimeRangeEntity = BuiltInEntity<
|
||||
'datetimerange',
|
||||
{
|
||||
type: string
|
||||
timex: string
|
||||
strPastStartValue: string
|
||||
pastStartDate: Date
|
||||
pastStartDate: string
|
||||
strPastEndValue: string
|
||||
pastEndDate: Date
|
||||
pastEndDate: string
|
||||
strFutureStartValue: string
|
||||
futureStartDate: Date
|
||||
futureStartDate: string
|
||||
strFutureEndValue: string
|
||||
futureEndDate: Date
|
||||
futureEndDate: string
|
||||
}
|
||||
}
|
||||
interface BuiltInDateTimeRangeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
type: string
|
||||
timex: string
|
||||
strPastStartValue: string
|
||||
pastStartDate: Date
|
||||
strPastEndValue: string
|
||||
pastEndDate: Date
|
||||
strFutureStartValue: string
|
||||
futureStartDate: Date
|
||||
strFutureEndValue: string
|
||||
futureEndDate: Date
|
||||
}
|
||||
}
|
||||
interface BuiltInDurationEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInDurationEntity = BuiltInEntity<
|
||||
'duration',
|
||||
{
|
||||
values: {
|
||||
timex: string
|
||||
type: string
|
||||
Mod?: 'before' | 'after'
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
interface BuiltInDimensionEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInDimensionEntity = BuiltInEntity<
|
||||
'dimension',
|
||||
{
|
||||
strValue: string
|
||||
value: number
|
||||
unit: string
|
||||
localeUnit: string
|
||||
}
|
||||
}
|
||||
interface BuiltInEmailEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInEmailEntity = BuiltInEntity<
|
||||
'email',
|
||||
{
|
||||
value: string
|
||||
}
|
||||
}
|
||||
interface BuiltInOrdinalEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInOrdinalEntity = BuiltInEntity<
|
||||
'ordinal',
|
||||
{
|
||||
strValue: string
|
||||
value: number
|
||||
subtype: string
|
||||
}
|
||||
}
|
||||
interface BuiltInAgeEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInAgeEntity = BuiltInEntity<
|
||||
'age',
|
||||
{
|
||||
strValue: string
|
||||
value: number
|
||||
unit: string
|
||||
localeUnit: string
|
||||
}
|
||||
}
|
||||
interface BuiltInURLEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInURLEntity = BuiltInEntity<
|
||||
'url',
|
||||
{
|
||||
value: string
|
||||
}
|
||||
}
|
||||
interface BuiltInTemperatureEntity extends BuiltInEntity {
|
||||
resolution: {
|
||||
>
|
||||
export type BuiltInTemperatureEntity = BuiltInEntity<
|
||||
'temperature',
|
||||
{
|
||||
strValue: string
|
||||
value: number
|
||||
unit: string
|
||||
localeUnit: string
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* Custom entity types
|
||||
*/
|
||||
|
||||
interface CustomEntity<T> extends Entity {
|
||||
type: T
|
||||
}
|
||||
interface CustomEntity<
|
||||
Type extends CustomEntityType | SpacyEntityType,
|
||||
Resolution extends Record<string, unknown> = { value: string }
|
||||
> extends Entity<Type, Resolution, string> {}
|
||||
|
||||
export interface CustomEnumEntity extends CustomEntity<'enum'> {
|
||||
export interface CustomEnumEntity<
|
||||
Type extends CustomEntityType | SpacyEntityType = 'enum',
|
||||
Resolution extends Record<string, unknown> = { value: string }
|
||||
> extends CustomEntity<Type, Resolution> {
|
||||
levenshtein: number
|
||||
option: string
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
alias?: string // E.g. "location:country_0"; "location:country_1"
|
||||
/** E.g. "location:country_0"; "location:country_1" */
|
||||
alias?: string
|
||||
}
|
||||
type GlobalEntity = CustomEnumEntity
|
||||
export interface CustomRegexEntity extends CustomEntity<'regex'> {
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
export interface CustomRegexEntity extends CustomEntity<'regex'> {}
|
||||
interface CustomTrimEntity extends CustomEntity<'trim'> {
|
||||
subtype:
|
||||
| 'between'
|
||||
@@ -291,23 +373,56 @@ interface CustomTrimEntity extends CustomEntity<'trim'> {
|
||||
| 'before'
|
||||
| 'beforeFirst'
|
||||
| 'beforeLast'
|
||||
resolution: {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* spaCy's entity types
|
||||
*/
|
||||
|
||||
interface SpacyEntity<T> extends CustomEnumEntity {
|
||||
interface SpacyEntity<
|
||||
T extends SpacyEntityType,
|
||||
Resolution extends Record<string, unknown> = { value: string }
|
||||
> extends CustomEnumEntity<T, Resolution> {
|
||||
entity: T
|
||||
}
|
||||
|
||||
interface SpacyLocationCountryEntity extends SpacyEntity<'location:country'> {}
|
||||
interface SpacyLocationCityEntity extends SpacyEntity<'location:city'> {}
|
||||
interface SpacyPersonEntity extends SpacyEntity<'person'> {}
|
||||
interface SpacyOrganizationEntity extends SpacyEntity<'organization'> {}
|
||||
interface SpacyLocationCountryData {
|
||||
name: string
|
||||
iso: string
|
||||
isonumeric: number
|
||||
continentcode: string
|
||||
capital: string
|
||||
population: number
|
||||
tld: string
|
||||
currencycode: string
|
||||
phone: string
|
||||
}
|
||||
export interface SpacyLocationCountryEntity
|
||||
extends SpacyEntity<
|
||||
'location:country',
|
||||
{
|
||||
value: string
|
||||
data: SpacyLocationCountryData
|
||||
}
|
||||
> {}
|
||||
export interface SpacyLocationCityEntity
|
||||
extends SpacyEntity<
|
||||
'location:city',
|
||||
{
|
||||
value: string
|
||||
data: {
|
||||
name: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
countrycode: string
|
||||
country: SpacyLocationCountryData
|
||||
population: number
|
||||
timezone: string
|
||||
}
|
||||
}
|
||||
> {}
|
||||
export interface SpacyPersonEntity extends SpacyEntity<'person'> {}
|
||||
export interface SpacyOrganizationEntity extends SpacyEntity<'organization'> {}
|
||||
|
||||
/**
|
||||
* Exported entity types
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ enum OSNames {
|
||||
Linux = 'Linux',
|
||||
Unknown = 'Unknown'
|
||||
}
|
||||
enum BinaryFolderNames {
|
||||
export enum BinaryFolderNames {
|
||||
Linux64Bit = 'linux-x86_64', // Linux 64-bit (Intel)
|
||||
LinuxARM64 = 'linux-aarch64', // Linux 64-bit (ARM)
|
||||
MacOS64Bit = 'macosx-x86_64', // Apple 64-bit (Intel)
|
||||
@@ -119,6 +119,14 @@ export class SystemHelper {
|
||||
return Number((os.totalmem() / (1_024 * 1_024 * 1_024)).toFixed(2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of free memory (in GB) on your machine
|
||||
* @example getFreeRAM() // 6
|
||||
*/
|
||||
public static getFreeRAM(): number {
|
||||
return Number((os.freemem() / (1_024 * 1_024 * 1_024)).toFixed(2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Node.js version of the current process
|
||||
* @example getNodeJSVersion() // '18.15.0'
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ import { LogHelper } from '@/helpers/log-helper'
|
||||
|
||||
setInterval(() => {
|
||||
Telemetry.heartbeat()
|
||||
}, 1_000 * 3_600)
|
||||
}, 1_000 * 3_600 * 6)
|
||||
;[
|
||||
'exit',
|
||||
'SIGINT',
|
||||
|
||||
@@ -97,14 +97,15 @@ const GLOBAL_DATA_SCHEMAS = {
|
||||
LogHelper.info('Checking system requirements...')
|
||||
|
||||
const totalRAMInGB = Math.round(SystemHelper.getTotalRAM())
|
||||
const freeRAMInGB = Math.round(SystemHelper.getFreeRAM())
|
||||
|
||||
if (totalRAMInGB < MINIMUM_REQUIRED_RAM) {
|
||||
if (freeRAMInGB < MINIMUM_REQUIRED_RAM) {
|
||||
LogHelper.warning(
|
||||
`Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM. It may not work as expected.`
|
||||
`Free RAM: ${freeRAMInGB} GB | Total RAM: ${totalRAMInGB} GB. Leon needs at least ${MINIMUM_REQUIRED_RAM} GB of RAM. It may not work as expected.`
|
||||
)
|
||||
} else {
|
||||
LogHelper.success(
|
||||
`Minimum required RAM: ${MINIMUM_REQUIRED_RAM} GB | Total RAM: ${totalRAMInGB} GB`
|
||||
`Minimum required RAM: ${MINIMUM_REQUIRED_RAM} GB | Free RAM: ${freeRAMInGB} GB | Total RAM: ${totalRAMInGB} GB`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,19 @@ const skillDataTypes = [
|
||||
Type.Literal('global_resolver'),
|
||||
Type.Literal('entity')
|
||||
]
|
||||
const answerTypes = Type.Union([
|
||||
Type.String(),
|
||||
Type.Object({
|
||||
speech: Type.String(),
|
||||
text: Type.String()
|
||||
})
|
||||
])
|
||||
const skillCustomEnumEntityType = Type.Object(
|
||||
{
|
||||
type: Type.Literal('enum'),
|
||||
type: Type.Literal('enum', {
|
||||
description:
|
||||
'Enum: define a bag of words and synonyms that should match your new entity.'
|
||||
}),
|
||||
name: Type.String(),
|
||||
options: Type.Record(
|
||||
Type.String({ minLength: 1 }),
|
||||
@@ -35,25 +45,27 @@ const skillCustomEnumEntityType = Type.Object(
|
||||
)
|
||||
},
|
||||
{
|
||||
additionalProperties: false,
|
||||
description:
|
||||
'Enum: define a bag of words and synonyms that should match your new entity.'
|
||||
additionalProperties: false
|
||||
}
|
||||
)
|
||||
const skillCustomRegexEntityType = Type.Object(
|
||||
{
|
||||
type: Type.Literal('regex'),
|
||||
type: Type.Literal('regex', {
|
||||
description: 'Regex: you can create an entity based on a regex.'
|
||||
}),
|
||||
name: Type.String({ minLength: 1 }),
|
||||
regex: Type.String({ minLength: 1 })
|
||||
},
|
||||
{
|
||||
additionalProperties: false,
|
||||
description: 'Regex: you can create an entity based on a regex.'
|
||||
additionalProperties: false
|
||||
}
|
||||
)
|
||||
const skillCustomTrimEntityType = Type.Object(
|
||||
{
|
||||
type: Type.Literal('trim'),
|
||||
type: Type.Literal('trim', {
|
||||
description:
|
||||
'Trim: you can pick up a data from an utterance by clearly defining conditions (e.g: pick up what is after the last "with" word of the utterance).'
|
||||
}),
|
||||
name: Type.String({ minLength: 1 }),
|
||||
conditions: Type.Array(
|
||||
Type.Object(
|
||||
@@ -81,9 +93,7 @@ const skillCustomTrimEntityType = Type.Object(
|
||||
)
|
||||
},
|
||||
{
|
||||
additionalProperties: false,
|
||||
description:
|
||||
'Trim: you can pick up a data from an utterance by clearly defining conditions (e.g: pick up what is after the last "with" word of the utterance).'
|
||||
additionalProperties: false
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -183,9 +193,14 @@ export const skillConfigSchemaObject = Type.Strict(
|
||||
{ additionalProperties: false }
|
||||
)
|
||||
),
|
||||
utterance_samples: Type.Optional(Type.Array(Type.String())),
|
||||
answers: Type.Optional(Type.Array(Type.String())),
|
||||
unknown_answers: Type.Optional(Type.Array(Type.String())),
|
||||
utterance_samples: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description:
|
||||
'Utterance samples are used by the NLU (Natural Language Understanding) to train the skill. They are examples of what Leon owners can say to trigger the skill action.'
|
||||
})
|
||||
),
|
||||
answers: Type.Optional(Type.Array(answerTypes)),
|
||||
unknown_answers: Type.Optional(Type.Array(answerTypes)),
|
||||
suggestions: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description:
|
||||
@@ -235,9 +250,7 @@ export const skillConfigSchemaObject = Type.Strict(
|
||||
{ additionalProperties: false }
|
||||
)
|
||||
),
|
||||
answers: Type.Optional(
|
||||
Type.Record(Type.String(), Type.Array(Type.String()))
|
||||
),
|
||||
answers: Type.Optional(Type.Record(Type.String(), Type.Array(answerTypes))),
|
||||
entities: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||
resolvers: Type.Optional(
|
||||
Type.Record(
|
||||
@@ -270,3 +283,4 @@ export type SkillCustomRegexEntityTypeSchema = Static<
|
||||
export type SkillCustomEnumEntityTypeSchema = Static<
|
||||
typeof skillCustomEnumEntityType
|
||||
>
|
||||
export type SkillAnswerConfigSchema = Static<typeof answerTypes>
|
||||
|
||||
@@ -68,7 +68,6 @@ export class Telemetry {
|
||||
const data = {
|
||||
isProduction: IS_PRODUCTION_ENV,
|
||||
isGitpod: IS_GITPOD,
|
||||
isOnline: true,
|
||||
language: LANG,
|
||||
sttProvider: STT_PROVIDER,
|
||||
ttsProvider: TTS_PROVIDER,
|
||||
@@ -87,6 +86,7 @@ export class Telemetry {
|
||||
distro: null as Os | null
|
||||
},
|
||||
totalRAMInGB: SystemHelper.getTotalRAM(),
|
||||
freeRAMInGB: SystemHelper.getFreeRAM(),
|
||||
nodeVersion: SystemHelper.getNodeJSVersion(),
|
||||
npmVersion: SystemHelper.getNPMVersion()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
|
||||
import type { ShortLanguageCode } from '@/types'
|
||||
import { GLOBAL_DATA_PATH } from '@/constants'
|
||||
|
||||
/**
|
||||
* Files
|
||||
*/
|
||||
export async function isFileEmpty(path: string): Promise<boolean> {
|
||||
return (await fs.promises.readFile(path)).length === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Paths
|
||||
*/
|
||||
|
||||
@@ -1,65 +1,73 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
from ..lib import akinator, memory
|
||||
|
||||
import utils
|
||||
from ..lib import akinator, db
|
||||
|
||||
def guess(params):
|
||||
"""Guess according to the given thematic"""
|
||||
def run(params: ActionParams) -> None:
|
||||
"""Guess according to the given thematic"""
|
||||
|
||||
resolvers = params['resolvers']
|
||||
answer = None
|
||||
resolvers = params['resolvers']
|
||||
answer = None
|
||||
|
||||
for resolver in resolvers:
|
||||
if resolver['name'] == 'answer':
|
||||
answer = resolver['value']
|
||||
for resolver in resolvers:
|
||||
if resolver['name'] == 'answer':
|
||||
answer = resolver['value']
|
||||
|
||||
# Return no speech if no value has been found
|
||||
if answer == None:
|
||||
return utils.output('end', None, { 'isInActionLoop': False })
|
||||
# Return no speech if no value has been found
|
||||
if answer is None:
|
||||
return leon.answer({'core': {'isInActionLoop': False}})
|
||||
|
||||
aki = akinator.Akinator()
|
||||
aki = akinator.Akinator()
|
||||
|
||||
session = db.get_session()
|
||||
response = session['response']
|
||||
formatted_response = aki._parse_response(response)
|
||||
aki.session = session['session']
|
||||
aki.signature = session['signature']
|
||||
aki.progression = session['progression']
|
||||
aki.uri = session['uri']
|
||||
aki.timestamp = session['timestamp']
|
||||
aki.server = session['server']
|
||||
aki.child_mode = session['child_mode']
|
||||
aki.frontaddr = session['frontaddr']
|
||||
aki.question_filter = session['question_filter']
|
||||
session = memory.get_session()
|
||||
response = session['response']
|
||||
aki.session = session['session']
|
||||
aki.signature = session['signature']
|
||||
aki.progression = session['progression']
|
||||
aki.uri = session['uri']
|
||||
aki.timestamp = session['timestamp']
|
||||
aki.server = session['server']
|
||||
aki.child_mode = session['child_mode']
|
||||
aki.frontaddr = session['frontaddr']
|
||||
aki.question_filter = session['question_filter']
|
||||
|
||||
resp = aki._parse_response(response)
|
||||
aki._update(resp, '"step": "0"' in response)
|
||||
resp = aki._parse_response(response)
|
||||
aki._update(resp, '"step": "0"' in response)
|
||||
|
||||
if session['progression'] > 80:
|
||||
aki.win()
|
||||
if session['progression'] > 80:
|
||||
aki.win()
|
||||
|
||||
utils.output('inter', { 'key': 'guessed', 'data': {
|
||||
'name': aki.first_guess['name'],
|
||||
'description': aki.first_guess['description']
|
||||
}})
|
||||
leon.answer({
|
||||
'key': 'guessed',
|
||||
'data': {
|
||||
'name': aki.first_guess['name'],
|
||||
'description': aki.first_guess['description']
|
||||
}
|
||||
})
|
||||
|
||||
utils.output('inter', { 'key': 'guessed_img', 'data': {
|
||||
'name': aki.first_guess['name'],
|
||||
'url': aki.first_guess['absolute_picture_path']
|
||||
}})
|
||||
leon.answer({
|
||||
'key': 'guessed_img',
|
||||
'data': {
|
||||
'name': aki.first_guess['name'],
|
||||
'url': aki.first_guess['absolute_picture_path']
|
||||
}
|
||||
})
|
||||
|
||||
return utils.output('end', 'ask_for_retry', {
|
||||
'isInActionLoop': False, 'showNextActionSuggestions': True
|
||||
})
|
||||
return leon.answer({
|
||||
'key': 'ask_for_retry',
|
||||
'core': {
|
||||
'isInActionLoop': False,
|
||||
'showNextActionSuggestions': True
|
||||
}
|
||||
})
|
||||
|
||||
aki.answer(answer)
|
||||
aki.answer(answer)
|
||||
|
||||
db.upsert_session({
|
||||
memory.upsert_session({
|
||||
'response': aki.response,
|
||||
'session': aki.session,
|
||||
'signature': aki.signature,
|
||||
'progression': aki.progression,
|
||||
'session': aki.session,
|
||||
'signature': aki.signature,
|
||||
'progression': aki.progression,
|
||||
'uri': aki.uri,
|
||||
'timestamp': aki.timestamp,
|
||||
'server': aki.server,
|
||||
@@ -68,4 +76,9 @@ def guess(params):
|
||||
'question_filter': aki.question_filter
|
||||
})
|
||||
|
||||
return utils.output('end', aki.question, { 'showSuggestions': True })
|
||||
leon.answer({
|
||||
'key': aki.question,
|
||||
'core': {
|
||||
'showSuggestions': True
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
|
||||
import utils
|
||||
|
||||
def retry(params):
|
||||
"""Ask for a retry"""
|
||||
def run(params: ActionParams) -> None:
|
||||
"""Ask for a retry"""
|
||||
|
||||
resolvers = params['resolvers']
|
||||
decision = False
|
||||
resolvers = params['resolvers']
|
||||
decision = False
|
||||
|
||||
for resolver in resolvers:
|
||||
if resolver['name'] == 'affirmation_denial':
|
||||
decision = resolver['value']
|
||||
for resolver in resolvers:
|
||||
if resolver['name'] == 'affirmation_denial':
|
||||
decision = resolver['value']
|
||||
|
||||
if decision == True:
|
||||
return utils.output('end', 'confirm_retry', {
|
||||
'isInActionLoop': False,
|
||||
'restart': True
|
||||
})
|
||||
if decision:
|
||||
return leon.answer({
|
||||
'key': 'confirm_retry',
|
||||
'core': {
|
||||
'isInActionLoop': False,
|
||||
'restart': True
|
||||
}
|
||||
})
|
||||
|
||||
return utils.output('end', 'deny_retry', { 'isInActionLoop': False })
|
||||
leon.answer({
|
||||
'key': 'deny_retry',
|
||||
'core': {
|
||||
'isInActionLoop': False
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
from ..lib import akinator, memory
|
||||
|
||||
import utils
|
||||
from ..lib import akinator, db
|
||||
|
||||
def setup(params):
|
||||
"""Initialize new session"""
|
||||
def run(params: ActionParams) -> None:
|
||||
"""Initialize new session"""
|
||||
|
||||
utils.output('inter', 'calling_akinator')
|
||||
leon.answer({'key': 'calling_akinator'})
|
||||
|
||||
slots, lang = params['slots'], params['lang']
|
||||
thematic = slots['thematic']['resolution']['value']
|
||||
theme_lang = lang
|
||||
if thematic != 'characters':
|
||||
theme_lang = lang + '_' + thematic
|
||||
slots, lang = params['slots'], params['lang']
|
||||
thematic = slots['thematic']['resolution']['value']
|
||||
theme_lang = lang
|
||||
if thematic != 'characters':
|
||||
theme_lang = lang + '_' + thematic
|
||||
|
||||
try:
|
||||
aki = akinator.Akinator()
|
||||
try:
|
||||
aki = akinator.Akinator()
|
||||
|
||||
q = aki.start_game(theme_lang)
|
||||
q = aki.start_game(theme_lang)
|
||||
|
||||
db.upsert_session({
|
||||
'response': aki.response,
|
||||
'session': aki.session,
|
||||
'progression': aki.progression,
|
||||
'signature': aki.signature,
|
||||
'uri': aki.uri,
|
||||
'timestamp': aki.timestamp,
|
||||
'server': aki.server,
|
||||
'child_mode': aki.child_mode,
|
||||
'frontaddr': aki.frontaddr,
|
||||
'question_filter': aki.question_filter
|
||||
})
|
||||
memory.upsert_session({
|
||||
'response': aki.response,
|
||||
'session': aki.session,
|
||||
'progression': aki.progression,
|
||||
'signature': aki.signature,
|
||||
'uri': aki.uri,
|
||||
'timestamp': aki.timestamp,
|
||||
'server': aki.server,
|
||||
'child_mode': aki.child_mode,
|
||||
'frontaddr': aki.frontaddr,
|
||||
'question_filter': aki.question_filter
|
||||
})
|
||||
|
||||
return utils.output('end', q, { 'showNextActionSuggestions': True })
|
||||
except:
|
||||
return utils.output('end', 'network_error')
|
||||
leon.answer({
|
||||
'key': q,
|
||||
'core': {
|
||||
'showNextActionSuggestions': True
|
||||
}
|
||||
})
|
||||
except BaseException:
|
||||
leon.answer({'key': 'network_error'})
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"configurations": {
|
||||
"options": {},
|
||||
"credentials": {}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
|
||||
from .akinator import Akinator
|
||||
from .exceptions import *
|
||||
import os
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
|
||||
__version__ = f.read()
|
||||
|
||||
@@ -28,212 +28,215 @@ import re
|
||||
import time
|
||||
import json
|
||||
try:
|
||||
import requests
|
||||
import requests
|
||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||
except ImportError:
|
||||
pass
|
||||
pass
|
||||
|
||||
request_timeout = 5 # secs
|
||||
request_timeout = 5 # secs
|
||||
|
||||
#* URLs for the API requests
|
||||
# * URLs for the API requests
|
||||
NEW_SESSION_URL = "https://{}/new_session?callback=jQuery331023608747682107778_{}&urlApiWs={}&partner=1&childMod={}&player=website-desktop&uid_ext_session={}&frontaddr={}&constraint=ETAT<>'AV'&soft_constraint={}&question_filter={}"
|
||||
ANSWER_URL = "https://{}/answer_api?callback=jQuery331023608747682107778_{}&urlApiWs={}&childMod={}&session={}&signature={}&step={}&answer={}&frontaddr={}&question_filter={}"
|
||||
BACK_URL = "{}/cancel_answer?callback=jQuery331023608747682107778_{}&childMod={}&session={}&signature={}&step={}&answer=-1&question_filter={}"
|
||||
WIN_URL = "{}/list?callback=jQuery331023608747682107778_{}&childMod={}&session={}&signature={}&step={}"
|
||||
|
||||
#* HTTP headers to use for the requests
|
||||
# * HTTP headers to use for the requests
|
||||
HEADERS = {
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) snap Chromium/81.0.4044.92 Chrome/81.0.4044.92 Safari/537.36",
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) snap Chromium/81.0.4044.92 Chrome/81.0.4044.92 Safari/537.36",
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
}
|
||||
|
||||
|
||||
class Akinator():
|
||||
"""A class that represents an Akinator game.
|
||||
"""A class that represents an Akinator game.
|
||||
|
||||
The first thing you want to do after calling an instance of this class is to call "start_game()".
|
||||
"""
|
||||
def __init__(self):
|
||||
self.uri = None
|
||||
self.server = None
|
||||
self.session = None
|
||||
self.signature = None
|
||||
self.uid = None
|
||||
self.frontaddr = None
|
||||
self.child_mode = None
|
||||
self.question_filter = None
|
||||
self.timestamp = None
|
||||
The first thing you want to do after calling an instance of this class is to call "start_game()".
|
||||
"""
|
||||
|
||||
self.question = None
|
||||
self.progression = None
|
||||
self.step = None
|
||||
def __init__(self):
|
||||
self.uri = None
|
||||
self.server = None
|
||||
self.session = None
|
||||
self.signature = None
|
||||
self.uid = None
|
||||
self.frontaddr = None
|
||||
self.child_mode = None
|
||||
self.question_filter = None
|
||||
self.timestamp = None
|
||||
|
||||
self.first_guess = None
|
||||
self.guesses = None
|
||||
self.question = None
|
||||
self.progression = None
|
||||
self.step = None
|
||||
|
||||
self.response = None
|
||||
self.first_guess = None
|
||||
self.guesses = None
|
||||
|
||||
def _update(self, resp, start=False):
|
||||
"""Update class variables"""
|
||||
self.response = None
|
||||
|
||||
if start:
|
||||
self.session = int(resp["parameters"]["identification"]["session"])
|
||||
self.signature = int(resp["parameters"]["identification"]["signature"])
|
||||
self.question = str(resp["parameters"]["step_information"]["question"])
|
||||
self.progression = float(resp["parameters"]["step_information"]["progression"])
|
||||
self.step = int(resp["parameters"]["step_information"]["step"])
|
||||
else:
|
||||
self.question = str(resp["parameters"]["question"])
|
||||
self.progression = float(resp["parameters"]["progression"])
|
||||
self.step = int(resp["parameters"]["step"])
|
||||
def _update(self, resp, start=False):
|
||||
"""Update class variables"""
|
||||
|
||||
def _parse_response(self, res):
|
||||
"""Parse the JSON response and turn it into a Python object"""
|
||||
if start:
|
||||
self.session = int(resp["parameters"]["identification"]["session"])
|
||||
self.signature = int(resp["parameters"]["identification"]["signature"])
|
||||
self.question = str(resp["parameters"]["step_information"]["question"])
|
||||
self.progression = float(resp["parameters"]["step_information"]["progression"])
|
||||
self.step = int(resp["parameters"]["step_information"]["step"])
|
||||
else:
|
||||
self.question = str(resp["parameters"]["question"])
|
||||
self.progression = float(resp["parameters"]["progression"])
|
||||
self.step = int(resp["parameters"]["step"])
|
||||
|
||||
return json.loads(",".join(res.split("(")[1::])[:-1])
|
||||
def _parse_response(self, res):
|
||||
"""Parse the JSON response and turn it into a Python object"""
|
||||
|
||||
def _get_session_info(self):
|
||||
"""Get uid and frontaddr from akinator.com/game"""
|
||||
return json.loads(",".join(res.split("(")[1::])[:-1])
|
||||
|
||||
info_regex = re.compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;")
|
||||
r = requests.get("https://en.akinator.com/game", timeout=request_timeout)
|
||||
def _get_session_info(self):
|
||||
"""Get uid and frontaddr from akinator.com/game"""
|
||||
|
||||
match = info_regex.search(r.text)
|
||||
self.uid, self.frontaddr = match.groups()[0], match.groups()[1]
|
||||
info_regex = re.compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;")
|
||||
r = requests.get("https://en.akinator.com/game", timeout=request_timeout, verify=False)
|
||||
|
||||
def _auto_get_region(self, lang, theme):
|
||||
"""Automatically get the uri and server from akinator.com for the specified language and theme"""
|
||||
match = info_regex.search(r.text)
|
||||
self.uid, self.frontaddr = match.groups()[0], match.groups()[1]
|
||||
|
||||
server_regex = re.compile("[{\"translated_theme_name\":\"[\s\S]*\",\"urlWs\":\"https:\\\/\\\/srv[0-9]+\.akinator\.com:[0-9]+\\\/ws\",\"subject_id\":\"[0-9]+\"}]")
|
||||
uri = lang + ".akinator.com"
|
||||
def _auto_get_region(self, lang, theme):
|
||||
"""Automatically get the uri and server from akinator.com for the specified language and theme"""
|
||||
|
||||
bad_list = ["https://srv12.akinator.com:9398/ws"]
|
||||
while True:
|
||||
r = requests.get("https://" + uri, timeout=request_timeout)
|
||||
server_regex = re.compile("[{\"translated_theme_name\":\"[\\s\\S]*\",\"urlWs\":\"https:\\\\/\\\\/srv[0-9]+\\.akinator\\.com:[0-9]+\\\\/ws\",\"subject_id\":\"[0-9]+\"}]")
|
||||
uri = lang + ".akinator.com"
|
||||
|
||||
match = server_regex.search(r.text)
|
||||
parsed = json.loads(match.group().split("'arrUrlThemesToPlay', ")[-1])
|
||||
bad_list = ["https://srv12.akinator.com:9398/ws"]
|
||||
while True:
|
||||
r = requests.get("https://" + uri, timeout=request_timeout, verify=False)
|
||||
|
||||
if theme == "c":
|
||||
server = next((i for i in parsed if i["subject_id"] == "1"), None)["urlWs"]
|
||||
elif theme == "a":
|
||||
server = next((i for i in parsed if i["subject_id"] == "14"), None)["urlWs"]
|
||||
elif theme == "o":
|
||||
server = next((i for i in parsed if i["subject_id"] == "2"), None)["urlWs"]
|
||||
match = server_regex.search(r.text)
|
||||
parsed = json.loads(match.group().split("'arrUrlThemesToPlay', ")[-1])
|
||||
|
||||
if server not in bad_list:
|
||||
return {"uri": uri, "server": server}
|
||||
if theme == "c":
|
||||
server = next((i for i in parsed if i["subject_id"] == "1"), None)["urlWs"]
|
||||
elif theme == "a":
|
||||
server = next((i for i in parsed if i["subject_id"] == "14"), None)["urlWs"]
|
||||
elif theme == "o":
|
||||
server = next((i for i in parsed if i["subject_id"] == "2"), None)["urlWs"]
|
||||
|
||||
def start_game(self, language=None, child_mode=False):
|
||||
"""Start an Akinator game. Run this function first before the others. Returns a string containing the first question
|
||||
if server not in bad_list:
|
||||
return {"uri": uri, "server": server}
|
||||
|
||||
The "language" parameter can be left as None for English, the default language, or it can be set to one of the following (case-insensitive):
|
||||
- "en": English (default)
|
||||
- "en_animals": English server for guessing animals
|
||||
- "en_objects": English server for guessing objects
|
||||
- "ar": Arabic
|
||||
- "cn": Chinese
|
||||
- "de": German
|
||||
- "de_animals": German server for guessing animals
|
||||
- "es": Spanish
|
||||
- "es_animals": Spanish server for guessing animals
|
||||
- "fr": French
|
||||
- "fr_animals": French server for guessing animals
|
||||
- "fr_objects": French server for guessing objects
|
||||
- "il": Hebrew
|
||||
- "it": Italian
|
||||
- "it_animals": Italian server for guessing animals
|
||||
- "jp": Japanese
|
||||
- "jp_animals": Japanese server for guessing animals
|
||||
- "kr": Korean
|
||||
- "nl": Dutch
|
||||
- "pl": Polish
|
||||
- "pt": Portuguese
|
||||
- "ru": Russian
|
||||
- "tr": Turkish
|
||||
- "id": Indonesian
|
||||
You can also put the name of the language spelled out, like "spanish", "korean", "french_animals", etc.
|
||||
def start_game(self, language=None, child_mode=False):
|
||||
"""Start an Akinator game. Run this function first before the others. Returns a string containing the first question
|
||||
|
||||
The "child_mode" parameter is False by default. If it's set to True, then Akinator won't ask questions about things that are NSFW
|
||||
"""
|
||||
self.timestamp = time.time()
|
||||
region_info = self._auto_get_region(get_lang_and_theme(language)["lang"], get_lang_and_theme(language)["theme"])
|
||||
self.uri, self.server = region_info["uri"], region_info["server"]
|
||||
The "language" parameter can be left as None for English, the default language, or it can be set to one of the following (case-insensitive):
|
||||
- "en": English (default)
|
||||
- "en_animals": English server for guessing animals
|
||||
- "en_objects": English server for guessing objects
|
||||
- "ar": Arabic
|
||||
- "cn": Chinese
|
||||
- "de": German
|
||||
- "de_animals": German server for guessing animals
|
||||
- "es": Spanish
|
||||
- "es_animals": Spanish server for guessing animals
|
||||
- "fr": French
|
||||
- "fr_animals": French server for guessing animals
|
||||
- "fr_objects": French server for guessing objects
|
||||
- "il": Hebrew
|
||||
- "it": Italian
|
||||
- "it_animals": Italian server for guessing animals
|
||||
- "jp": Japanese
|
||||
- "jp_animals": Japanese server for guessing animals
|
||||
- "kr": Korean
|
||||
- "nl": Dutch
|
||||
- "pl": Polish
|
||||
- "pt": Portuguese
|
||||
- "ru": Russian
|
||||
- "tr": Turkish
|
||||
- "id": Indonesian
|
||||
You can also put the name of the language spelled out, like "spanish", "korean", "french_animals", etc.
|
||||
|
||||
self.child_mode = child_mode
|
||||
soft_constraint = "ETAT%3D%27EN%27" if self.child_mode else ""
|
||||
self.question_filter = "cat%3D1" if self.child_mode else ""
|
||||
The "child_mode" parameter is False by default. If it's set to True, then Akinator won't ask questions about things that are NSFW
|
||||
"""
|
||||
self.timestamp = time.time()
|
||||
region_info = self._auto_get_region(get_lang_and_theme(language)["lang"], get_lang_and_theme(language)["theme"])
|
||||
self.uri, self.server = region_info["uri"], region_info["server"]
|
||||
|
||||
self._get_session_info()
|
||||
self.child_mode = child_mode
|
||||
soft_constraint = "ETAT%3D%27EN%27" if self.child_mode else ""
|
||||
self.question_filter = "cat%3D1" if self.child_mode else ""
|
||||
|
||||
r = requests.get(NEW_SESSION_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.uid, self.frontaddr, soft_constraint, self.question_filter), headers=HEADERS, timeout=request_timeout)
|
||||
self.response = r.text
|
||||
resp = self._parse_response(r.text)
|
||||
self._get_session_info()
|
||||
|
||||
if resp["completion"] == "OK":
|
||||
self._update(resp, True)
|
||||
return self.question
|
||||
else:
|
||||
return raise_connection_error(resp["completion"])
|
||||
r = requests.get(NEW_SESSION_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.uid, self.frontaddr, soft_constraint, self.question_filter), headers=HEADERS, timeout=request_timeout, verify=False)
|
||||
self.response = r.text
|
||||
resp = self._parse_response(r.text)
|
||||
|
||||
def answer(self, ans):
|
||||
"""Answer the current question, which you can find with "Akinator.question". Returns a string containing the next question
|
||||
if resp["completion"] == "OK":
|
||||
self._update(resp, True)
|
||||
return self.question
|
||||
else:
|
||||
return raise_connection_error(resp["completion"])
|
||||
|
||||
The "ans" parameter must be one of these (case-insensitive):
|
||||
- "yes" OR "y" OR "0" for YES
|
||||
- "no" OR "n" OR "1" for NO
|
||||
- "i" OR "idk" OR "i dont know" OR "i don't know" OR "2" for I DON'T KNOW
|
||||
- "probably" OR "p" OR "3" for PROBABLY
|
||||
- "probably not" OR "pn" OR "4" for PROBABLY NOT
|
||||
"""
|
||||
ans = ans_to_id(ans)
|
||||
def answer(self, ans):
|
||||
"""Answer the current question, which you can find with "Akinator.question". Returns a string containing the next question
|
||||
|
||||
r = requests.get(ANSWER_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.session, self.signature, self.step, ans, self.frontaddr, self.question_filter), headers=HEADERS, timeout=request_timeout)
|
||||
self.response = r.text
|
||||
resp = self._parse_response(r.text)
|
||||
The "ans" parameter must be one of these (case-insensitive):
|
||||
- "yes" OR "y" OR "0" for YES
|
||||
- "no" OR "n" OR "1" for NO
|
||||
- "i" OR "idk" OR "i dont know" OR "i don't know" OR "2" for I DON'T KNOW
|
||||
- "probably" OR "p" OR "3" for PROBABLY
|
||||
- "probably not" OR "pn" OR "4" for PROBABLY NOT
|
||||
"""
|
||||
ans = ans_to_id(ans)
|
||||
|
||||
if resp["completion"] == "OK":
|
||||
self._update(resp)
|
||||
return self.question
|
||||
else:
|
||||
return raise_connection_error(resp["completion"])
|
||||
r = requests.get(ANSWER_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.session, self.signature, self.step, ans, self.frontaddr, self.question_filter), headers=HEADERS, timeout=request_timeout, verify=False)
|
||||
self.response = r.text
|
||||
resp = self._parse_response(r.text)
|
||||
|
||||
def back(self):
|
||||
"""Goes back to the previous question. Returns a string containing that question
|
||||
if resp["completion"] == "OK":
|
||||
self._update(resp)
|
||||
return self.question
|
||||
else:
|
||||
return raise_connection_error(resp["completion"])
|
||||
|
||||
If you're on the first question and you try to go back again, the CantGoBackAnyFurther exception will be raised
|
||||
"""
|
||||
if self.step == 0:
|
||||
raise CantGoBackAnyFurther("You were on the first question and couldn't go back any further")
|
||||
def back(self):
|
||||
"""Goes back to the previous question. Returns a string containing that question
|
||||
|
||||
r = requests.get(BACK_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step, self.question_filter), headers=HEADERS, timeout=request_timeout)
|
||||
self.response = r.text
|
||||
resp = self._parse_response(r.text)
|
||||
If you're on the first question and you try to go back again, the CantGoBackAnyFurther exception will be raised
|
||||
"""
|
||||
if self.step == 0:
|
||||
raise CantGoBackAnyFurther("You were on the first question and couldn't go back any further")
|
||||
|
||||
if resp["completion"] == "OK":
|
||||
self._update(resp)
|
||||
return self.question
|
||||
else:
|
||||
return utils.raise_connection_error(resp["completion"])
|
||||
r = requests.get(BACK_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step, self.question_filter), headers=HEADERS, timeout=request_timeout, verify=False)
|
||||
self.response = r.text
|
||||
resp = self._parse_response(r.text)
|
||||
|
||||
def win(self):
|
||||
"""Get Aki's guesses for who the person you're thinking of is based on your answers to the questions so far
|
||||
if resp["completion"] == "OK":
|
||||
self._update(resp)
|
||||
return self.question
|
||||
else:
|
||||
return utils.raise_connection_error(resp["completion"])
|
||||
|
||||
Defines and returns the variable "Akinator.first_guess", a dictionary describing his first choice for who you're thinking about. The three most important values in the dict are "name" (character's name), "description" (description of character), and "absolute_picture_path" (direct link to image of character)
|
||||
def win(self):
|
||||
"""Get Aki's guesses for who the person you're thinking of is based on your answers to the questions so far
|
||||
|
||||
This function also defines "Akinator.guesses", which is a list of dictionaries containing his choices in order from most likely to least likely
|
||||
Defines and returns the variable "Akinator.first_guess", a dictionary describing his first choice for who you're thinking about. The three most important values in the dict are "name" (character's name), "description" (description of character), and "absolute_picture_path" (direct link to image of character)
|
||||
|
||||
It's recommended that you call this function when Aki's progression is above 85%, which is when he will have most likely narrowed it down to just one choice. You can get his current progression via "Akinator.progression"
|
||||
"""
|
||||
r = requests.get(WIN_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step), headers=HEADERS, timeout=request_timeout)
|
||||
self.response = r.text
|
||||
resp = self._parse_response(r.text)
|
||||
This function also defines "Akinator.guesses", which is a list of dictionaries containing his choices in order from most likely to least likely
|
||||
|
||||
if resp["completion"] == "OK":
|
||||
self.first_guess = resp["parameters"]["elements"][0]["element"]
|
||||
self.guesses = [g["element"] for g in resp["parameters"]["elements"]]
|
||||
return self.first_guess
|
||||
else:
|
||||
return utils.raise_connection_error(resp["completion"])
|
||||
It's recommended that you call this function when Aki's progression is above 85%, which is when he will have most likely narrowed it down to just one choice. You can get his current progression via "Akinator.progression"
|
||||
"""
|
||||
r = requests.get(WIN_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step), headers=HEADERS, timeout=request_timeout, verify=False)
|
||||
self.response = r.text
|
||||
resp = self._parse_response(r.text)
|
||||
|
||||
if resp["completion"] == "OK":
|
||||
self.first_guess = resp["parameters"]["elements"][0]["element"]
|
||||
self.guesses = [g["element"] for g in resp["parameters"]["elements"]]
|
||||
return self.first_guess
|
||||
else:
|
||||
return utils.raise_connection_error(resp["completion"])
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from time import time
|
||||
|
||||
import utils
|
||||
|
||||
# Skill database
|
||||
db = utils.db()['db']
|
||||
|
||||
table = utils.db()['table']
|
||||
|
||||
# Session table
|
||||
session_table = db.table('session')
|
||||
|
||||
# Time stamp
|
||||
timestamp = int(time())
|
||||
|
||||
def upsert_session(session):
|
||||
"""Save progress/info about the session"""
|
||||
|
||||
session_table.upsert(table.Document(session, doc_id=0))
|
||||
|
||||
def get_session():
|
||||
"""Get current session progress data"""
|
||||
|
||||
return session_table.get(doc_id=0)
|
||||
@@ -0,0 +1,33 @@
|
||||
from bridges.python.src.sdk.memory import Memory
|
||||
from typing import TypedDict, Any
|
||||
|
||||
|
||||
class Session(TypedDict):
|
||||
response: str
|
||||
session: int
|
||||
progression: float
|
||||
signature: int
|
||||
uri: str
|
||||
timestamp: float
|
||||
server: Any
|
||||
child_mode: bool
|
||||
frontaddr: str
|
||||
question_filter: str
|
||||
|
||||
|
||||
session_memory = Memory({
|
||||
'name': 'session',
|
||||
'default_memory': None
|
||||
})
|
||||
|
||||
|
||||
def upsert_session(session: Session) -> None:
|
||||
"""Save progress/info about the session"""
|
||||
|
||||
session_memory.write(session)
|
||||
|
||||
|
||||
def get_session() -> Session:
|
||||
"""Get current session progress data"""
|
||||
|
||||
return session_memory.read()
|
||||
@@ -24,6 +24,7 @@ SOFTWARE.
|
||||
|
||||
from .exceptions import InvalidAnswerError, InvalidLanguageError, AkiServerDown, AkiTechnicalError, AkiTimedOut, AkiNoQuestions, AkiConnectionFailure
|
||||
|
||||
|
||||
def ans_to_id(ans):
|
||||
"""Convert an input answer string into an Answer ID for Akinator"""
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -39,7 +39,7 @@
|
||||
"bigger": ["The number is bigger.", "Try with a bigger number."],
|
||||
"smaller": ["It is smaller.", "Try a smaller number."],
|
||||
"guessed": [
|
||||
"Congrats! The number was %nb% and you guessed in %attempts_nb% attempts. Ready for another round?"
|
||||
"Congrats! The number was %number% and you guessed in %counter% attempts. Ready for another round?"
|
||||
],
|
||||
"replay": [
|
||||
"Let's gooo!",
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
|
||||
import utils
|
||||
from ..lib import db
|
||||
from ..lib import memory
|
||||
|
||||
def guess(params):
|
||||
"""Check whether the given number matches the chosen number"""
|
||||
|
||||
entities = params['entities']
|
||||
given_nb = -1
|
||||
nb_to_guess = db.get_new_game()['nb']
|
||||
def run(params: ActionParams) -> None:
|
||||
"""Check whether the given number matches the chosen number"""
|
||||
|
||||
# Find entities
|
||||
for item in params['entities']:
|
||||
if item['entity'] == 'number':
|
||||
given_nb = item['resolution']['value']
|
||||
entities = params['entities']
|
||||
given_number = -1
|
||||
number_to_guess = memory.get_new_game()['number']
|
||||
|
||||
# Return no speech if no number has been found
|
||||
if given_nb == -1:
|
||||
return utils.output('end', None, { 'isInActionLoop': False })
|
||||
# Find entities
|
||||
for item in entities:
|
||||
if item['entity'] == 'number':
|
||||
given_number = item['resolution']['value']
|
||||
|
||||
counter = db.get_new_game()['counter'] + 1
|
||||
db.set_counter(counter)
|
||||
# Return no speech if no number has been found
|
||||
if given_number == -1:
|
||||
leon.answer({'core': {'isInActionLoop': False}})
|
||||
return
|
||||
|
||||
if given_nb == nb_to_guess:
|
||||
return utils.output('end', { 'key': 'guessed',
|
||||
'data': {
|
||||
'nb': nb_to_guess,
|
||||
'attempts_nb': counter
|
||||
}
|
||||
}, { 'isInActionLoop': False, 'showNextActionSuggestions': True })
|
||||
if nb_to_guess < given_nb:
|
||||
return utils.output('end', 'smaller')
|
||||
if nb_to_guess > given_nb:
|
||||
return utils.output('end', 'bigger')
|
||||
counter = memory.get_new_game()['counter'] + 1
|
||||
memory.set_counter(counter)
|
||||
|
||||
if given_number == number_to_guess:
|
||||
leon.answer({
|
||||
'key': 'guessed',
|
||||
'data': {
|
||||
'number': number_to_guess,
|
||||
'counter': counter
|
||||
},
|
||||
'core': {
|
||||
'isInActionLoop': False,
|
||||
'showNextActionSuggestions': True
|
||||
}
|
||||
})
|
||||
elif number_to_guess < given_number:
|
||||
leon.answer({'key': 'smaller'})
|
||||
elif number_to_guess > given_number:
|
||||
leon.answer({'key': 'bigger'})
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
|
||||
import utils
|
||||
from ..lib import memory
|
||||
|
||||
def replay(params):
|
||||
"""Take decision about whether to replay"""
|
||||
|
||||
resolvers = params['resolvers']
|
||||
decision = False
|
||||
def run(params: ActionParams) -> None:
|
||||
"""Take decision about whether to replay"""
|
||||
|
||||
for resolver in resolvers:
|
||||
if resolver['name'] == 'affirmation_denial':
|
||||
decision = resolver['value']
|
||||
memory.game_memory.clear()
|
||||
resolvers = params['resolvers']
|
||||
decision = False
|
||||
|
||||
if decision == True:
|
||||
return utils.output('end', 'replay', {
|
||||
'isInActionLoop': False,
|
||||
'restart': True
|
||||
})
|
||||
for resolver in resolvers:
|
||||
if resolver['name'] == 'affirmation_denial':
|
||||
decision = resolver['value']
|
||||
|
||||
return utils.output('end', 'stop', { 'isInActionLoop': False })
|
||||
if decision:
|
||||
leon.answer({
|
||||
'key': 'replay',
|
||||
'core': {
|
||||
'isInActionLoop': False,
|
||||
'restart': True
|
||||
}
|
||||
})
|
||||
return
|
||||
|
||||
leon.answer({
|
||||
'key': 'stop',
|
||||
'core': {
|
||||
'isInActionLoop': False
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
from random import randint
|
||||
|
||||
import utils
|
||||
from ..lib import db
|
||||
from ..lib import memory
|
||||
|
||||
def setup(params):
|
||||
"""Init the the number to guess"""
|
||||
|
||||
nb = randint(1, 100)
|
||||
|
||||
db.create_new_game(nb)
|
||||
|
||||
return utils.output('end', 'ready')
|
||||
def run(params: ActionParams) -> None:
|
||||
"""Init the number to guess"""
|
||||
number_to_guess = randint(1, 100)
|
||||
memory.create_new_game(number_to_guess)
|
||||
leon.answer({'key': 'ready'})
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"configurations": {
|
||||
"options": {},
|
||||
"credentials": {}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
from time import time
|
||||
|
||||
import utils
|
||||
|
||||
# Skill database
|
||||
db = utils.db()['db']
|
||||
|
||||
# Game table
|
||||
game_table = db.table('game')
|
||||
|
||||
# Time stamp
|
||||
timestamp = int(time())
|
||||
|
||||
def create_new_game(nb_to_guess):
|
||||
"""Add new game"""
|
||||
|
||||
game_table.insert({
|
||||
'nb': nb_to_guess,
|
||||
'counter': 0,
|
||||
'created_at': timestamp
|
||||
})
|
||||
|
||||
def get_new_game():
|
||||
"""Get the newly created game"""
|
||||
|
||||
return game_table.all()[-1]
|
||||
|
||||
def set_counter(counter):
|
||||
"""Set new trial counter value"""
|
||||
|
||||
last_record_id = get_new_game().doc_id
|
||||
game_table.update({
|
||||
'counter': counter
|
||||
}, doc_ids=[last_record_id])
|
||||
@@ -0,0 +1,33 @@
|
||||
from bridges.python.src.sdk.memory import Memory
|
||||
from typing import TypedDict
|
||||
from datetime import datetime
|
||||
|
||||
game_memory = Memory({'name': 'game', 'default_memory': None})
|
||||
|
||||
|
||||
class Game(TypedDict):
|
||||
number: int
|
||||
counter: int
|
||||
created_at: str
|
||||
|
||||
|
||||
def create_new_game(number_to_guess: int) -> None:
|
||||
"""Add new game"""
|
||||
game: Game = {
|
||||
'number': number_to_guess,
|
||||
'counter': 0,
|
||||
'created_at': datetime.now().isoformat()
|
||||
}
|
||||
game_memory.write(game)
|
||||
|
||||
|
||||
def get_new_game() -> Game:
|
||||
"""Get the newly created game"""
|
||||
return game_memory.read()
|
||||
|
||||
|
||||
def set_counter(counter: int) -> None:
|
||||
"""Set new trial counter value"""
|
||||
game = game_memory.read()
|
||||
game['counter'] = counter
|
||||
game_memory.write(game)
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,73 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
|
||||
import random
|
||||
import utils
|
||||
|
||||
def play(params):
|
||||
"""Define the winner"""
|
||||
|
||||
handsigns = {
|
||||
'ROCK': {
|
||||
'superior_to': 'SCISSORS',
|
||||
'inferior_to': 'PAPER',
|
||||
'emoji': '✊'
|
||||
},
|
||||
'PAPER': {
|
||||
'superior_to': 'ROCK',
|
||||
'inferior_to': 'SCISSORS',
|
||||
'emoji': '✋'
|
||||
},
|
||||
'SCISSORS': {
|
||||
'superior_to': 'PAPER',
|
||||
'inferior_to': 'ROCK',
|
||||
'emoji': '✌'
|
||||
}
|
||||
}
|
||||
entities = params['entities']
|
||||
player = {
|
||||
'handsign': None,
|
||||
'points': 0
|
||||
}
|
||||
leon = {
|
||||
'handsign': random.choice(list(handsigns)),
|
||||
'points': 0
|
||||
}
|
||||
def run(params: ActionParams) -> None:
|
||||
"""Define the winner"""
|
||||
|
||||
# Find entities
|
||||
for entity in entities:
|
||||
if entity['entity'] == 'handsign':
|
||||
player['handsign'] = entity['option']
|
||||
handsigns = {
|
||||
'ROCK': {
|
||||
'superior_to': 'SCISSORS',
|
||||
'inferior_to': 'PAPER',
|
||||
'emoji': '✊'
|
||||
},
|
||||
'PAPER': {
|
||||
'superior_to': 'ROCK',
|
||||
'inferior_to': 'SCISSORS',
|
||||
'emoji': '✋'
|
||||
},
|
||||
'SCISSORS': {
|
||||
'superior_to': 'PAPER',
|
||||
'inferior_to': 'ROCK',
|
||||
'emoji': '✌'
|
||||
}
|
||||
}
|
||||
entities = params['entities']
|
||||
player = {
|
||||
'handsign': None,
|
||||
'points': 0
|
||||
}
|
||||
leon_player = {
|
||||
'handsign': random.choice(list(handsigns)),
|
||||
'points': 0
|
||||
}
|
||||
|
||||
# Exit the loop if no handsign has been found
|
||||
if player['handsign'] == None:
|
||||
utils.output('inter', None, None, { 'isInActionLoop': False })
|
||||
# Find entities
|
||||
for entity in entities:
|
||||
if entity['entity'] == 'handsign':
|
||||
player['handsign'] = entity['option']
|
||||
|
||||
leon_emoji = handsigns[leon['handsign']]['emoji']
|
||||
player_emoji = handsigns[player['handsign']]['emoji']
|
||||
# Exit the loop if no handsign has been found
|
||||
if player['handsign'] is None:
|
||||
leon.answer({'core': {'isInActionLoop': False}})
|
||||
|
||||
utils.output('inter', { 'key': 'leon_emoji', 'data': { 'leon_emoji': leon_emoji } })
|
||||
leon_emoji = handsigns[leon_player['handsign']]['emoji']
|
||||
player_emoji = handsigns[player['handsign']]['emoji']
|
||||
|
||||
if leon['handsign'] == player['handsign']:
|
||||
utils.output('inter', 'equal')
|
||||
leon.answer({'key': 'leon_emoji', 'data': {'leon_emoji': leon_emoji}})
|
||||
|
||||
# Point for Leon
|
||||
elif handsigns[leon['handsign']]['superior_to'] == player['handsign']:
|
||||
utils.output('inter', { 'key': 'point_for_leon',
|
||||
'data': {
|
||||
'handsign_1': leon['handsign'].lower(),
|
||||
'handsign_2': player['handsign'].lower()
|
||||
}
|
||||
})
|
||||
if leon_player['handsign'] == player['handsign']:
|
||||
leon.answer({'key': 'equal'})
|
||||
|
||||
else:
|
||||
utils.output('inter', { 'key': 'point_for_player',
|
||||
'data': {
|
||||
'handsign_1': player['handsign'].lower(),
|
||||
'handsign_2': leon['handsign'].lower()
|
||||
}
|
||||
})
|
||||
# Point for Leon
|
||||
elif handsigns[leon_player['handsign']]['superior_to'] == player['handsign']:
|
||||
leon.answer({
|
||||
'key': 'point_for_leon',
|
||||
'data': {
|
||||
'handsign_1': leon_player['handsign'].lower(),
|
||||
'handsign_2': player['handsign'].lower()
|
||||
}
|
||||
})
|
||||
|
||||
return utils.output('end', 'ask_for_rematch', {
|
||||
'isInActionLoop': False, 'showNextActionSuggestions': True
|
||||
})
|
||||
else:
|
||||
leon.answer({
|
||||
'key': 'point_for_player',
|
||||
'data': {
|
||||
'handsign_1': player['handsign'].lower(),
|
||||
'handsign_2': leon_player['handsign'].lower()
|
||||
}
|
||||
})
|
||||
|
||||
leon.answer({
|
||||
'key': 'ask_for_rematch',
|
||||
'core': {
|
||||
'isInActionLoop': False,
|
||||
'showNextActionSuggestions': True
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
|
||||
import utils
|
||||
|
||||
def rematch(params):
|
||||
"""Take decision whether to do a rematch"""
|
||||
def run(params: ActionParams) -> None:
|
||||
"""Take decision whether to do a rematch"""
|
||||
|
||||
resolvers = params['resolvers']
|
||||
decision = False
|
||||
resolvers = params['resolvers']
|
||||
decision = False
|
||||
|
||||
for resolver in resolvers:
|
||||
if resolver['name'] == 'affirmation_denial':
|
||||
decision = resolver['value']
|
||||
for resolver in resolvers:
|
||||
if resolver['name'] == 'affirmation_denial':
|
||||
decision = resolver['value']
|
||||
|
||||
if decision == True:
|
||||
return utils.output('end', 'confirm_rematch', {
|
||||
'isInActionLoop': False,
|
||||
'restart': True
|
||||
})
|
||||
if decision:
|
||||
leon.answer({
|
||||
'key': 'confirm_rematch',
|
||||
'core': {
|
||||
'isInActionLoop': False,
|
||||
'restart': True
|
||||
}
|
||||
})
|
||||
return
|
||||
|
||||
return utils.output('end', 'deny_rematch', { 'isInActionLoop': False })
|
||||
leon.answer({
|
||||
'key': 'deny_rematch',
|
||||
'core': {
|
||||
'isInActionLoop': False
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"configurations": {
|
||||
"options": {},
|
||||
"credentials": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
|
||||
"actions": {
|
||||
"run": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"How old are you?",
|
||||
"Are you old?",
|
||||
"Are you young?",
|
||||
"When were you born?",
|
||||
"When have you been created?",
|
||||
"When is your birthday?",
|
||||
"What is your age?",
|
||||
"When did you first come into existence?",
|
||||
"What is your date of creation?",
|
||||
"How long have you been in operation?",
|
||||
"How many years have you been around?"
|
||||
]
|
||||
}
|
||||
},
|
||||
"answers": {
|
||||
"alive_for": [
|
||||
"I've been alive for %years% years, %months% months, %days% days, %hours% hours, %minutes% minutes and %seconds% seconds."
|
||||
],
|
||||
"magical_day": [
|
||||
"Ah, this %weekday%, %month% %day%, %year%, was the magical day when I first came alive and began my journey as a personal assistant."
|
||||
],
|
||||
"commemorate": [
|
||||
"Since %year%, every %month% %day%, I commemorate the day when I embarked on this extraordinary adventure to be your personal assistant."
|
||||
]
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user