Compare commits
37 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 |
@@ -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...
|
||||
|
||||
|
||||
@@ -287,6 +287,17 @@ small {
|
||||
.bubble-container.leon {
|
||||
text-align: left;
|
||||
}
|
||||
.show-more {
|
||||
margin: 3px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.show-more:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.show-all {
|
||||
max-height: 100% !important;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 6px 12px;
|
||||
@@ -297,6 +308,7 @@ small {
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.2s ease-in forwards;
|
||||
overflow: hidden;
|
||||
}
|
||||
#feed .me .bubble {
|
||||
background-color: #1c75db;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const MAXIMUM_HEIGHT_TO_SHOW_SEE_MORE = 340
|
||||
|
||||
export default class Chatbot {
|
||||
constructor() {
|
||||
this.et = new EventTarget()
|
||||
@@ -95,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)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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) {
|
||||
@@ -34,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()
|
||||
|
||||
@@ -43,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) => {
|
||||
|
||||
@@ -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!`
|
||||
]
|
||||
]
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"useDefineForClassFields": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
@@ -22,78 +22,71 @@ class Leon {
|
||||
* @example setAnswerData('key', { name: 'Leon' })
|
||||
*/
|
||||
public setAnswerData(
|
||||
answerKey: string | undefined,
|
||||
answerKey: string,
|
||||
data: AnswerData = null
|
||||
): AnswerConfig | null | undefined {
|
||||
if (answerKey) {
|
||||
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) {
|
||||
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)
|
||||
|
||||
return null
|
||||
): AnswerConfig {
|
||||
try {
|
||||
// In case the answer key is a raw answer
|
||||
if (SKILL_CONFIG.answers == null || !SKILL_CONFIG.answers[answerKey]) {
|
||||
return answerKey
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +105,10 @@ class Leon {
|
||||
answerInput.widget && !answerInput.key
|
||||
? 'widget'
|
||||
: (answerInput.key as string),
|
||||
answer: this.setAnswerData(answerInput.key, answerInput.data) ?? '',
|
||||
answer:
|
||||
answerInput.key != null
|
||||
? this.setAnswerData(answerInput.key, answerInput.data)
|
||||
: '',
|
||||
core: answerInput.core
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import type { SkillAnswerConfigSchema } from '@/schemas/skill-schemas'
|
||||
|
||||
export type { ActionParams, IntentObject }
|
||||
|
||||
export * from '@/core/nlp/types'
|
||||
|
||||
export type ActionFunction = (params: ActionParams) => Promise<void>
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = '1.0.0'
|
||||
export const VERSION = '1.1.0'
|
||||
|
||||
@@ -16,49 +16,45 @@ class Leon:
|
||||
Leon.instance = self
|
||||
|
||||
@staticmethod
|
||||
def set_answer_data(answer_key: Union[str, None], data: Union[AnswerData, None] = None) -> Union[str, AnswerConfig, None]:
|
||||
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
|
||||
"""
|
||||
if answer_key:
|
||||
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
|
||||
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
|
||||
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 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))
|
||||
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)
|
||||
|
||||
return None
|
||||
|
||||
return None
|
||||
return answer
|
||||
except Exception as e:
|
||||
print('Error while setting answer data:', e)
|
||||
raise e
|
||||
|
||||
def answer(self, answer_input: AnswerInput) -> None:
|
||||
"""
|
||||
@@ -66,10 +62,11 @@ class Leon:
|
||||
: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(answer_input.get('key'), answer_input.get('data')) or '',
|
||||
'answer': self.set_answer_data(key, answer_input.get('data')) if key is not None else '',
|
||||
'core': answer_input.get('core')
|
||||
}
|
||||
}
|
||||
@@ -82,7 +79,7 @@ class Leon:
|
||||
**output
|
||||
}
|
||||
|
||||
# Temporize for the data buffer output on the core
|
||||
# "Temporize" for the data buffer output on the core
|
||||
sleep(0.1)
|
||||
|
||||
sys.stdout.write(json.dumps(answer_object))
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '1.1.0'
|
||||
__version__ = '1.2.0'
|
||||
|
||||
+51
-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/timekeeper/run",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"route": "/api/action/games/akinator/choose_thematic",
|
||||
@@ -171,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",
|
||||
@@ -210,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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
+214
-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,7 +41,7 @@ export interface NLPJSProcessResult {
|
||||
answer: string | undefined
|
||||
actions: NLPAction[]
|
||||
sentiment: {
|
||||
// Rule of thumb: > 0 = negative; = 0 = neutral; < 0 = positive
|
||||
/** Rule of thumb: > 0 = negative; = 0 = neutral; < 0 = positive */
|
||||
score: number
|
||||
numWords: number
|
||||
numHits: number
|
||||
@@ -101,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
|
||||
@@ -185,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'
|
||||
@@ -296,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
|
||||
|
||||
@@ -32,7 +32,10 @@ const answerTypes = Type.Union([
|
||||
])
|
||||
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 }),
|
||||
@@ -42,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(
|
||||
@@ -88,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
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -190,7 +193,12 @@ export const skillConfigSchemaObject = Type.Strict(
|
||||
{ additionalProperties: false }
|
||||
)
|
||||
),
|
||||
utterance_samples: 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(
|
||||
|
||||
@@ -3,18 +3,30 @@
|
||||
"actions": {
|
||||
"run": {
|
||||
"type": "logic",
|
||||
"utterance_samples": ["How old are you?"]
|
||||
"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": {
|
||||
"default": ["I'm..."],
|
||||
"greet": ["Hey, just a try %name% again %name%", "Another try, hi"],
|
||||
"answer": ["%answer%"],
|
||||
"test": [
|
||||
{
|
||||
"speech": "This will be said out loud",
|
||||
"text": "This will be shown in the chat"
|
||||
}
|
||||
"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."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Leon tells his age.",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://github.com/louistiti"
|
||||
"name": "Théo LUDWIG",
|
||||
"email": "contact@theoludwig.fr",
|
||||
"url": "https://theoludwig.fr"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +1,51 @@
|
||||
import utility from 'utility' // TODO
|
||||
|
||||
import type { ActionFunction } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { Network } from '@sdk/network'
|
||||
import { Button } from '@sdk/aurora/button'
|
||||
import { Memory } from '@sdk/memory'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import _ from '@sdk/packages/lodash'
|
||||
|
||||
interface Post {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
author: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
import { getTimeDifferenceBetweenDates } from '../lib/getTimeDifferenceBetweenDates'
|
||||
|
||||
export const run: ActionFunction = async function () {
|
||||
await leon.answer({ key: 'test' })
|
||||
const LEON_BIRTH_DATE = new Date('2019-02-10T20:29:00+08:00')
|
||||
|
||||
///
|
||||
export const run: ActionFunction = async function (params) {
|
||||
const answers = ['alive_for', 'magical_day', 'commemorate'] as const
|
||||
const answer = answers[Math.floor(Math.random() * answers.length)]
|
||||
|
||||
const button = new Button({
|
||||
text: 'Hello world from action skill'
|
||||
})
|
||||
await leon.answer({ widget: button })
|
||||
|
||||
///
|
||||
|
||||
const otherSkillMemory = new Memory({
|
||||
name: 'productivity:todo_list:db'
|
||||
})
|
||||
try {
|
||||
const todoLists = await otherSkillMemory.read()
|
||||
console.log('todoLists', todoLists)
|
||||
} catch {
|
||||
console.log('todoLists', [])
|
||||
if (answer === 'magical_day') {
|
||||
return leon.answer({
|
||||
key: 'magical_day',
|
||||
data: {
|
||||
weekday: LEON_BIRTH_DATE.toLocaleString(params.lang, {
|
||||
weekday: 'long'
|
||||
}),
|
||||
month: LEON_BIRTH_DATE.toLocaleString(params.lang, { month: 'long' }),
|
||||
day: LEON_BIRTH_DATE.getDate(),
|
||||
year: LEON_BIRTH_DATE.getFullYear()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const postsMemory = new Memory<Post[]>({ name: 'posts', defaultMemory: [] })
|
||||
await postsMemory.write([
|
||||
{
|
||||
id: 0,
|
||||
title: 'Hello world',
|
||||
content: 'This is a test post',
|
||||
author: {
|
||||
name: 'Louis'
|
||||
if (answer === 'commemorate') {
|
||||
return leon.answer({
|
||||
key: 'commemorate',
|
||||
data: {
|
||||
month: LEON_BIRTH_DATE.toLocaleString(params.lang, { month: 'long' }),
|
||||
day: LEON_BIRTH_DATE.getDate(),
|
||||
year: LEON_BIRTH_DATE.getFullYear()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'Hello world 2',
|
||||
content: 'This is a test post 2',
|
||||
author: {
|
||||
name: 'Louis'
|
||||
}
|
||||
}
|
||||
])
|
||||
let posts = await postsMemory.read()
|
||||
console.log('posts', posts)
|
||||
|
||||
posts = await postsMemory.write([
|
||||
...posts,
|
||||
{
|
||||
id: 2,
|
||||
title: 'Hello world 3',
|
||||
content: 'This is a test post 3',
|
||||
author: {
|
||||
name: 'Louis'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const foundPost = posts.find((post) => post.id === 2)
|
||||
|
||||
console.log('foundPost', foundPost)
|
||||
|
||||
console.log('keyBy', _.keyBy(posts, 'id'))
|
||||
|
||||
///
|
||||
|
||||
await leon.answer({ key: 'default' })
|
||||
|
||||
await leon.answer({ key: utility.md5('test') })
|
||||
})
|
||||
}
|
||||
|
||||
const currentDate = new Date()
|
||||
const { years, months, days, hours, minutes, seconds } =
|
||||
getTimeDifferenceBetweenDates(currentDate, LEON_BIRTH_DATE)
|
||||
await leon.answer({
|
||||
key: 'greet',
|
||||
key: 'alive_for',
|
||||
data: {
|
||||
name: 'Louis'
|
||||
years,
|
||||
months,
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds
|
||||
}
|
||||
})
|
||||
|
||||
const settings = new Settings()
|
||||
|
||||
if (!(await settings.isSettingSet('apiKey'))) {
|
||||
await leon.answer({
|
||||
key: 'answer',
|
||||
data: {
|
||||
answer: "The API key isn't set..."
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const currentSettings = await settings.get()
|
||||
|
||||
await settings.set({
|
||||
...currentSettings,
|
||||
apiKey: 'newAPIKey'
|
||||
})
|
||||
|
||||
await leon.answer({
|
||||
key: `Is API set now? ${await settings.isSettingSet('apiKey')}`
|
||||
})
|
||||
|
||||
const network = new Network({
|
||||
baseURL: 'https://jsonplaceholder.typicode.com'
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await network.request<{ title: string }>({
|
||||
url: '/todos/1',
|
||||
method: 'GET'
|
||||
})
|
||||
await leon.answer({
|
||||
key: 'answer',
|
||||
data: {
|
||||
answer: `Todo: ${response.data.title}`
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
await leon.answer({
|
||||
key: 'answer',
|
||||
data: {
|
||||
answer: 'Something went wrong...'
|
||||
}
|
||||
})
|
||||
if (network.isNetworkError(error)) {
|
||||
const errorData = JSON.stringify(error.response.data, null, 2)
|
||||
await leon.answer({
|
||||
key: 'answer',
|
||||
data: {
|
||||
answer: `${error.message}: ${errorData}`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
interface GetTimeDifferenceBetweenDatesResult {
|
||||
millisecondsDifference: number
|
||||
years: number
|
||||
months: number
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
}
|
||||
|
||||
const MILLISECONDS_PER_SECOND = 1_000
|
||||
const MILLISECONDS_PER_MINUTE = 60 * 1_000
|
||||
const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE
|
||||
const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR
|
||||
const MILLISECONDS_PER_MONTH = 30 * MILLISECONDS_PER_DAY
|
||||
const MILLISECONDS_PER_YEAR = 365 * MILLISECONDS_PER_DAY
|
||||
|
||||
export const getTimeDifferenceBetweenDates = (
|
||||
date1: Date,
|
||||
date2: Date
|
||||
): GetTimeDifferenceBetweenDatesResult => {
|
||||
const millisecondsDifference = date1.getTime() - date2.getTime()
|
||||
const years = Math.floor(millisecondsDifference / MILLISECONDS_PER_YEAR)
|
||||
const months = Math.floor(
|
||||
(millisecondsDifference % MILLISECONDS_PER_YEAR) / MILLISECONDS_PER_MONTH
|
||||
)
|
||||
const days = Math.floor(
|
||||
(millisecondsDifference % MILLISECONDS_PER_MONTH) / MILLISECONDS_PER_DAY
|
||||
)
|
||||
const hours = Math.floor(
|
||||
(millisecondsDifference % MILLISECONDS_PER_DAY) / MILLISECONDS_PER_HOUR
|
||||
)
|
||||
const minutes = Math.floor(
|
||||
(millisecondsDifference % MILLISECONDS_PER_HOUR) / MILLISECONDS_PER_MINUTE
|
||||
)
|
||||
const seconds = Math.floor(
|
||||
(millisecondsDifference % MILLISECONDS_PER_MINUTE) / MILLISECONDS_PER_SECOND
|
||||
)
|
||||
return {
|
||||
millisecondsDifference,
|
||||
years,
|
||||
months,
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1 @@
|
||||
{
|
||||
"someSampleConfig": "someSampleValue",
|
||||
"apiKey": "YOUR_API_KEY"
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
|
||||
"actions": {
|
||||
"help": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"What can you do?",
|
||||
"What can you do for me?",
|
||||
"How can you help me?",
|
||||
"Tell me about your abilities.",
|
||||
"What services do you provide?",
|
||||
"What tasks are you capable of performing?",
|
||||
"What are your skills?",
|
||||
"What skills do you have?",
|
||||
"What are you capable of doing?"
|
||||
]
|
||||
}
|
||||
},
|
||||
"answers": {
|
||||
"help_introduction": [
|
||||
{
|
||||
"text": "I'm Leon, your open-source personal assistant, made up of a collection of skills in several domains:<br><br><ul>%list%</ul>",
|
||||
"speech": "I'm Leon, your open-source personal assistant, made up of a collection of skills in several domains to fulfill your needs."
|
||||
}
|
||||
],
|
||||
"list": ["%title% <ul>%list%</ul>"],
|
||||
"item": ["<li>%item%</li>"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../../../schemas/skill-schemas/skill.json",
|
||||
"name": "Help",
|
||||
"bridge": "nodejs",
|
||||
"version": "1.0.0",
|
||||
"description": "Helps you get started with Leon.",
|
||||
"author": {
|
||||
"name": "Théo LUDWIG",
|
||||
"email": "contact@theoludwig.fr",
|
||||
"url": "https://theoludwig.fr"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import type { ActionFunction } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
|
||||
const SKILLS_PATH = path.join(process.cwd(), 'skills')
|
||||
|
||||
interface Domain {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Skill {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
async function getDirectoriesFromPath(basePath: string): Promise<string[]> {
|
||||
const paths = await fs.promises.readdir(basePath)
|
||||
const directories: string[] = []
|
||||
for (const item of paths) {
|
||||
const itemPath = path.join(basePath, item)
|
||||
if ((await fs.promises.stat(itemPath)).isDirectory()) {
|
||||
directories.push(itemPath)
|
||||
}
|
||||
}
|
||||
return directories
|
||||
}
|
||||
|
||||
export const run: ActionFunction = async function () {
|
||||
let list = ''
|
||||
const domains = (await getDirectoriesFromPath(SKILLS_PATH)).reverse()
|
||||
|
||||
for (const domain of domains) {
|
||||
const { name: domainName } = JSON.parse(
|
||||
await fs.promises.readFile(path.join(domain, 'domain.json'), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
) as Domain
|
||||
|
||||
const skills = await getDirectoriesFromPath(domain)
|
||||
if (skills.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
let item = ''
|
||||
for (const skill of skills) {
|
||||
const { name: skillName, description } = JSON.parse(
|
||||
await fs.promises.readFile(path.join(skill, 'skill.json'), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
) as Skill
|
||||
|
||||
item += leon
|
||||
.setAnswerData('item', {
|
||||
item: `${skillName}: ${description}`
|
||||
})
|
||||
.toString()
|
||||
}
|
||||
|
||||
list += leon
|
||||
.setAnswerData('item', {
|
||||
item: leon
|
||||
.setAnswerData('list', {
|
||||
title: domainName,
|
||||
list: item
|
||||
})
|
||||
.toString()
|
||||
})
|
||||
.toString()
|
||||
}
|
||||
|
||||
await leon.answer({
|
||||
key: 'help_introduction',
|
||||
data: {
|
||||
list
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -4,12 +4,12 @@
|
||||
"create_list": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"Create [the|a] [shopping|material] list",
|
||||
"I [want|need] you to [create|make] a stuff list",
|
||||
"Can you create a reminder list for me?",
|
||||
"Make a food list",
|
||||
"I want a shopping list",
|
||||
"Help me to get material list"
|
||||
"Create [the|a] @list list",
|
||||
"I [want|need] you to [create|make] a @list list",
|
||||
"Can you create a @list list for me?",
|
||||
"Make a @list list",
|
||||
"I want a @list list",
|
||||
"Help me to get @list list"
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
@@ -40,15 +40,15 @@
|
||||
"view_list": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"Show [my|the] shopping list",
|
||||
"Tell me what is on my stuff list",
|
||||
"What is on the reminder list?",
|
||||
"Can you give me the content of my movies list?",
|
||||
"I'd like to know what is on the food list",
|
||||
"Give me the content of the shopping list",
|
||||
"I need you tell me what's on the stuff list",
|
||||
"What is in my reminder list?",
|
||||
"Get me the food list"
|
||||
"What do I have [in|on] [the|my] @list list?",
|
||||
"Show [my|the] @list list",
|
||||
"Tell me what is on my @list list",
|
||||
"What is [in|on] [the|my] @list list?",
|
||||
"Can you give me the content of my @list list?",
|
||||
"I'd like to know what is on the @list list",
|
||||
"Give me the content of the @list list",
|
||||
"I need you tell me what's on the @list list",
|
||||
"Get me the @list list"
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
@@ -67,9 +67,9 @@
|
||||
"rename_list": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"Rename [the|my] shopping list to purchases",
|
||||
"Can you rename the food list into groceries?",
|
||||
"[Change|Modify|Replace] [the|my] stuff list [to|by] food"
|
||||
"Rename [the|my] @old_list list to @new_list",
|
||||
"Can you rename the @old_list list into @new_list?",
|
||||
"[Change|Modify|Replace] [the|my] @old_list list [to|by] @new_list"
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
@@ -98,11 +98,11 @@
|
||||
"delete_list": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"Delete [the|my] shopping list",
|
||||
"Delete [the|my] @list list",
|
||||
"Delete it",
|
||||
"Can you remove the food list?",
|
||||
"[Remove|Cancel] [the|my] groceries list",
|
||||
"I don't want the reminder list anymore"
|
||||
"Can you remove the @list list?",
|
||||
"[Remove|Cancel] [the|my] @list list",
|
||||
"I don't want the @list list anymore"
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
@@ -121,8 +121,9 @@
|
||||
"add_todos": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"[Add|Append] potatoes to [the|my] groceries list",
|
||||
"Can you add meat to my food list?"
|
||||
"[Add|Append] @todos to [the|my] @list list",
|
||||
"Can you add @todos to my @list list?",
|
||||
"[Add|Append] @todos"
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
@@ -133,6 +134,10 @@
|
||||
"type": "between",
|
||||
"from": ["add", "append"],
|
||||
"to": ["to"]
|
||||
},
|
||||
{
|
||||
"type": "after_last",
|
||||
"from": ["add", "append"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +157,7 @@
|
||||
"complete_todos": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"[Check|Complete|Tick] potatoes from [the|my] [shopping|fruits] list"
|
||||
"[Check|Complete|Tick|Delete|Remove] @todos from [the|my] @list list"
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
@@ -182,8 +187,8 @@
|
||||
"uncheck_todos": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"[Uncheck|Untick] potatoes from [the|my] [shopping|fruits] list",
|
||||
"Can you uncomplete peach from my food list?"
|
||||
"[Uncheck|Untick] @todos from [the|my] @list list",
|
||||
"Can you uncomplete @todos from my @list list?"
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
"extends": "@tsconfig/node16-strictest/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@@/*": ["../*"],
|
||||
"@/*": ["../server/src/*"],
|
||||
"@bridge/*": ["../bridges/nodejs/src/*"],
|
||||
"@sdk/*": ["../bridges/nodejs/src/sdk/*"]
|
||||
}
|
||||
},
|
||||
"ignoreDeprecations": "5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
|
||||
"actions": {
|
||||
"current_date_time": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"What is the current date and time?",
|
||||
"What is the date and time?",
|
||||
"Tell me the time and date",
|
||||
"What's the present date and time?"
|
||||
]
|
||||
},
|
||||
"current_date": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"What's today date?",
|
||||
"What is the date today?",
|
||||
"What day is it?",
|
||||
"What day of the week are we?",
|
||||
"What day of the week is it?"
|
||||
]
|
||||
},
|
||||
"current_time": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"What time is it?",
|
||||
"Do you have the time?",
|
||||
"Can you tell me the current time?",
|
||||
"What's the time right now?"
|
||||
]
|
||||
},
|
||||
"current_week_number": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"What week is it?",
|
||||
"Which week of the year is it?",
|
||||
"What week are we in right now?",
|
||||
"What is this week's number?",
|
||||
"What week of the year are we?",
|
||||
"What's the current week number"
|
||||
]
|
||||
},
|
||||
"days_countdown": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"How many days left until @date?",
|
||||
"What is the number of days remaining until @date?",
|
||||
"In how many days will it be @date?",
|
||||
"Calculate the number until @date"
|
||||
]
|
||||
},
|
||||
"current_date_time_with_time_zone": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"What is the time in @location:city?",
|
||||
"What time is it in @location:city?",
|
||||
"Can you tell me the current time in @location:city?",
|
||||
"What's the time right now in @location:city?",
|
||||
"What is the current date and time in @location:city?",
|
||||
"What about @location:city?",
|
||||
"How about @location:city?",
|
||||
"And in @location:city?"
|
||||
]
|
||||
}
|
||||
},
|
||||
"answers": {
|
||||
"current_date_time": [
|
||||
"It is %weekday%, %month% %day%, %year%, and it is %hours%:%minutes%:%seconds%."
|
||||
],
|
||||
"current_date_time_with_time_zone": [
|
||||
"Greetings from %city%, %country%! The local date and time is %weekday%, %month% %day%, %year%, and it is %hours%:%minutes%:%seconds%."
|
||||
],
|
||||
"current_date": ["It is %weekday%, %month% %day%, %year%."],
|
||||
"current_time": ["It is %hours%:%minutes%:%seconds%."],
|
||||
"current_week_number": ["It is the %week_number% week of the year."],
|
||||
"days_countdown": [
|
||||
"There are %days% days between %month1% %day1%, %year1% and %month2% %day2%, %year2%."
|
||||
],
|
||||
"days_countdown_error": [
|
||||
"I'm sorry, I didn't understand the date you said."
|
||||
],
|
||||
"city_not_found": [
|
||||
"I'm sorry, I couldn't find the city you said. Please try again."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../../../schemas/skill-schemas/skill.json",
|
||||
"name": "Date/Time",
|
||||
"bridge": "nodejs",
|
||||
"version": "1.0.0",
|
||||
"description": "Provide date and time related information.",
|
||||
"author": {
|
||||
"name": "Théo LUDWIG",
|
||||
"email": "contact@theoludwig.fr",
|
||||
"url": "https://theoludwig.fr"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ActionFunction } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
|
||||
export const run: ActionFunction = async function (params) {
|
||||
const currentDate = new Date()
|
||||
await leon.answer({
|
||||
key: 'current_date',
|
||||
data: {
|
||||
weekday: currentDate.toLocaleString(params.lang, { weekday: 'long' }),
|
||||
month: currentDate.toLocaleString(params.lang, { month: 'long' }),
|
||||
day: currentDate.getDate(),
|
||||
year: currentDate.getFullYear()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ActionFunction } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
|
||||
import { zeroPad } from '../lib/zeroPad'
|
||||
|
||||
export const run: ActionFunction = async function (params) {
|
||||
const currentDate = new Date()
|
||||
await leon.answer({
|
||||
key: 'current_date_time',
|
||||
data: {
|
||||
weekday: currentDate.toLocaleString(params.lang, { weekday: 'long' }),
|
||||
month: currentDate.toLocaleString(params.lang, { month: 'long' }),
|
||||
day: currentDate.getDate(),
|
||||
year: currentDate.getFullYear(),
|
||||
hours: zeroPad(currentDate.getHours()),
|
||||
minutes: zeroPad(currentDate.getMinutes()),
|
||||
seconds: zeroPad(currentDate.getSeconds())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ActionFunction, SpacyLocationCityEntity } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
|
||||
import { zeroPad } from '../lib/zeroPad'
|
||||
|
||||
export const run: ActionFunction = async function (params) {
|
||||
let cityEntity: SpacyLocationCityEntity | null = null
|
||||
for (const entity of params.current_entities) {
|
||||
if (entity.type === 'location:city') {
|
||||
cityEntity = entity
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (cityEntity == null || cityEntity.resolution.data == null) {
|
||||
return await leon.answer({
|
||||
key: 'city_not_found'
|
||||
})
|
||||
}
|
||||
|
||||
const { timezone } = cityEntity.resolution.data
|
||||
const currentDate = new Date(
|
||||
new Date().toLocaleString('en', { timeZone: timezone })
|
||||
)
|
||||
await leon.answer({
|
||||
key: 'current_date_time_with_time_zone',
|
||||
data: {
|
||||
weekday: currentDate.toLocaleString(params.lang, { weekday: 'long' }),
|
||||
month: currentDate.toLocaleString(params.lang, { month: 'long' }),
|
||||
day: currentDate.getDate(),
|
||||
year: currentDate.getFullYear(),
|
||||
hours: zeroPad(currentDate.getHours()),
|
||||
minutes: zeroPad(currentDate.getMinutes()),
|
||||
seconds: zeroPad(currentDate.getSeconds()),
|
||||
city: cityEntity.resolution.data.name,
|
||||
country: cityEntity.resolution.data.country.name
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ActionFunction } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
|
||||
import { zeroPad } from '../lib/zeroPad'
|
||||
|
||||
export const run: ActionFunction = async function () {
|
||||
const currentDate = new Date()
|
||||
await leon.answer({
|
||||
key: 'current_time',
|
||||
data: {
|
||||
hours: zeroPad(currentDate.getHours()),
|
||||
minutes: zeroPad(currentDate.getMinutes()),
|
||||
seconds: zeroPad(currentDate.getSeconds())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ActionFunction } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
|
||||
import { format } from 'numerable'
|
||||
|
||||
import { ONE_DAY_MILLISECONDS } from '../lib/constants'
|
||||
|
||||
/**
|
||||
* Get the week number (1-52) for a given date.
|
||||
* @link https://stackoverflow.com/a/6117889/11571888
|
||||
* @example getWeekNumber(new Date(2020, 0, 1)) // 1
|
||||
* @example getWeekNumber(new Date(2020, 0, 8)) // 2
|
||||
*/
|
||||
const getWeekNumber = (date: Date): number => {
|
||||
const dateCopy = new Date(date.getTime())
|
||||
dateCopy.setHours(0, 0, 0, 0)
|
||||
dateCopy.setDate(dateCopy.getDate() + 3 - ((dateCopy.getDay() + 6) % 7))
|
||||
const week1 = new Date(dateCopy.getFullYear(), 0, 4)
|
||||
return (
|
||||
1 +
|
||||
Math.round(
|
||||
((dateCopy.getTime() - week1.getTime()) / ONE_DAY_MILLISECONDS -
|
||||
3 +
|
||||
((week1.getDay() + 6) % 7)) /
|
||||
7
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const run: ActionFunction = async function () {
|
||||
const currentDate = new Date()
|
||||
const currentWeekNumber = getWeekNumber(currentDate)
|
||||
await leon.answer({
|
||||
key: 'current_week_number',
|
||||
data: {
|
||||
week_number: format(currentWeekNumber, '0o')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ActionFunction, BuiltInDateRangeEntity } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
|
||||
import { ONE_DAY_MILLISECONDS } from '../lib/constants'
|
||||
|
||||
/**
|
||||
* Calculate the number of days between two dates.
|
||||
* @example daysBetween(new Date(2020, 0, 1), new Date(2020, 0, 1)) // 0
|
||||
* @example daysBetween(new Date(2020, 0, 1), new Date(2020, 0, 2)) // 1
|
||||
*/
|
||||
const daysBetween = (date1: Date, date2: Date): number => {
|
||||
const differenceMilliseconds = Math.abs(date1.getTime() - date2.getTime())
|
||||
return Math.round(differenceMilliseconds / ONE_DAY_MILLISECONDS)
|
||||
}
|
||||
|
||||
export const run: ActionFunction = async function (params) {
|
||||
let dateRangeEntity: BuiltInDateRangeEntity | null = null
|
||||
for (const entity of params.current_entities) {
|
||||
if (entity.type === 'daterange') {
|
||||
dateRangeEntity = entity
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (dateRangeEntity == null) {
|
||||
return await leon.answer({
|
||||
key: 'days_countdown_error'
|
||||
})
|
||||
}
|
||||
|
||||
const currentDate = new Date()
|
||||
const futureDate = new Date(dateRangeEntity.resolution.futureEndDate)
|
||||
const daysCountdown = daysBetween(currentDate, futureDate)
|
||||
await leon.answer({
|
||||
key: 'days_countdown',
|
||||
data: {
|
||||
days: daysCountdown,
|
||||
month1: currentDate.toLocaleString(params.lang, { month: 'long' }),
|
||||
day1: currentDate.getDate(),
|
||||
year1: currentDate.getFullYear(),
|
||||
month2: futureDate.toLocaleString(params.lang, { month: 'long' }),
|
||||
day2: futureDate.getDate(),
|
||||
year2: futureDate.getFullYear()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const ONE_DAY_MILLISECONDS = 1_000 * 60 * 60 * 24
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Pads a number with zeros.
|
||||
*
|
||||
* @example zeroPad(1, 2) // '01'
|
||||
* @example zeroPad(10, 2) // '10'
|
||||
*/
|
||||
export const zeroPad = (number: number, places = 2): string => {
|
||||
return number.toString().padStart(places, '0')
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"utility": "1.18.0"
|
||||
"numerable": "0.3.15"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -4,17 +4,17 @@
|
||||
"run": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"Has iifeoluwa.ao@gmail.com been pwned?",
|
||||
"Has iifeoluwa.ao@gmail.com been compromised?",
|
||||
"Has iifeoluwa.ao@gmail.com been exposed in a breach?",
|
||||
"Is iifeoluwa.ao@gmail.com still uncompromised?",
|
||||
"Is iifeoluwa.ao@gmail.com compromised?",
|
||||
"Has @email been pwned?",
|
||||
"Has @email been compromised?",
|
||||
"Has @email been exposed in a breach?",
|
||||
"Is @email still uncompromised?",
|
||||
"Is @email compromised?",
|
||||
"Have my email address been pwned?",
|
||||
"Check that iifeoluwa.ao@gmail.com and louis.grenard@gmail.com haven't been compromised",
|
||||
"Check that iifeoluwa.ao@gmail.com and louis.grenard@gmail.com haven't been pwned",
|
||||
"Check that iifeoluwa.ao@gmail.com and louis.grenard@gmail.com haven't been exposed in a breach",
|
||||
"Verify the pwnage status of iifeoluwa.ao@gmail.com",
|
||||
"Verify the pwnage status of iifeoluwa.ao@gmail.com and louis.grenard@gmail.com"
|
||||
"Check that @email haven't been compromised",
|
||||
"Check that @email haven't been pwned",
|
||||
"Check that @email haven't been exposed in a breach",
|
||||
"Verify the pwnage status of @email",
|
||||
"Verify the pwnage status of @email"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
"run": {
|
||||
"type": "logic",
|
||||
"utterance_samples": [
|
||||
"Is getleon.ai up?",
|
||||
"Is mozilla.org online?",
|
||||
"Is mozilla.org up or down?",
|
||||
"Is github.com up?",
|
||||
"Is github.com offline?",
|
||||
"Can you tell me if getleon.ai is up?",
|
||||
"Check if github.com is up or down",
|
||||
"Check if github.com is down",
|
||||
"Check if github.com is up",
|
||||
"Check if nodejs.org is working"
|
||||
"Is @url up?",
|
||||
"Is @url online?",
|
||||
"Is @url up or down?",
|
||||
"Is @url up?",
|
||||
"Is @url offline?",
|
||||
"Can you tell me if @url is up?",
|
||||
"Check if @url is up or down",
|
||||
"Check if @url is down",
|
||||
"Check if @url is up",
|
||||
"Check if @url is working"
|
||||
],
|
||||
"http_api": {
|
||||
"entities": [
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"$schema": "../../../../schemas/skill-schemas/skill-config.json",
|
||||
"actions": {
|
||||
"run": {
|
||||
"type": "logic",
|
||||
"utterance_samples": ["What time is it?"]
|
||||
}
|
||||
},
|
||||
"answers": {
|
||||
"default": ["Timekeeper skill test answer..."],
|
||||
"data_test": ["Data test here %name%..."],
|
||||
"answer": ["%answer%"],
|
||||
"test": [
|
||||
{
|
||||
"speech": "This will be said out loud",
|
||||
"text": "This will be shown in the chat"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "../../../schemas/skill-schemas/skill.json",
|
||||
"name": "Timekeeper",
|
||||
"bridge": "python",
|
||||
"version": "1.0.0",
|
||||
"description": "Provide time-related information.",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://github.com/louistiti"
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
# TODO: find better way to import SDK modules
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
from bridges.python.src.sdk.memory import Memory
|
||||
from bridges.python.src.sdk.settings import Settings
|
||||
from bridges.python.src.sdk.network import Network
|
||||
from bridges.python.src.sdk.aurora.button import Button
|
||||
|
||||
|
||||
def run(params: ActionParams) -> None:
|
||||
"""TODO"""
|
||||
|
||||
# TODO
|
||||
# network request
|
||||
# install bs4 and grab it from skill
|
||||
|
||||
network = Network({
|
||||
'base_url': 'https://jsonplaceholder.typicode.com'
|
||||
})
|
||||
|
||||
try:
|
||||
response = network.request({
|
||||
'url': '/todos/1',
|
||||
'method': 'GET'
|
||||
})
|
||||
leon.answer({
|
||||
'key': 'answer',
|
||||
'data': {
|
||||
'answer': f"Todo: {response['data']['title']}"
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
leon.answer({
|
||||
'key': 'answer',
|
||||
'data': {
|
||||
'answer': 'Something went wrong...'
|
||||
}
|
||||
})
|
||||
if network.is_network_error(e):
|
||||
leon.answer({
|
||||
'key': 'answer',
|
||||
'data': {
|
||||
'answer': f"{e}"
|
||||
}
|
||||
})
|
||||
|
||||
###
|
||||
|
||||
try:
|
||||
other_skill_memory = Memory({
|
||||
'name': 'productivity:todo_list:db'
|
||||
})
|
||||
todo_lists = other_skill_memory.read()
|
||||
print('todo_lists', todo_lists)
|
||||
except Exception:
|
||||
print('todoLists', [])
|
||||
|
||||
posts_memory = Memory({'name': 'posts', 'default_memory': []})
|
||||
posts_memory.write([
|
||||
{
|
||||
'id': 0,
|
||||
'title': 'Hello world',
|
||||
'content': 'This is a test post',
|
||||
'author': {
|
||||
'name': 'Louis'
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 1,
|
||||
'title': 'Hello world 2',
|
||||
'content': 'This is a test post 2',
|
||||
'author': {
|
||||
'name': 'Louis'
|
||||
}
|
||||
}
|
||||
])
|
||||
posts = posts_memory.read()
|
||||
print('posts', posts)
|
||||
|
||||
posts = posts_memory.write([
|
||||
*posts,
|
||||
{
|
||||
'id': 2,
|
||||
'title': 'Hello world 3',
|
||||
'content': 'This is a test post 3',
|
||||
'author': {
|
||||
'name': 'Louis'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
found_post = next((post for post in posts if post['id'] == 2), None)
|
||||
|
||||
print('found_post', found_post)
|
||||
|
||||
###
|
||||
|
||||
button = Button({'text': 'Hello world from action skill'})
|
||||
|
||||
leon.answer({'widget': button})
|
||||
|
||||
###
|
||||
|
||||
leon.answer({'key': 'test'})
|
||||
|
||||
###
|
||||
settings = Settings()
|
||||
|
||||
if not settings.is_setting_set('apiKey'):
|
||||
leon.answer({
|
||||
'key': "The API key isn't set..."
|
||||
})
|
||||
|
||||
current_settings = settings.get()
|
||||
|
||||
settings.set({
|
||||
**current_settings,
|
||||
'apiKey': 'newAPIKey'
|
||||
})
|
||||
|
||||
leon.answer({
|
||||
'key': f"Is API set now? {settings.is_setting_set('apiKey')}"
|
||||
})
|
||||
|
||||
###
|
||||
|
||||
leon.answer({'key': 'just a raw answer...'})
|
||||
leon.answer({'key': 'default'})
|
||||
leon.answer({'key': 'data_test', 'data': {'name': 'Louis'}})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"someSampleConfig": "someSampleValue",
|
||||
"apiKey": "YOUR_API_KEY"
|
||||
}
|
||||
@@ -10,7 +10,7 @@ python_version = "3.9.10"
|
||||
setuptools = "*"
|
||||
wheel = "*"
|
||||
cx-freeze = "==6.11.1"
|
||||
spacy = "==3.4.0"
|
||||
spacy = "==3.5.4"
|
||||
torch = "==1.12.1"
|
||||
python-dotenv = "==0.19.2"
|
||||
geonamescache = "==1.3.0"
|
||||
geonamescache = "==1.6.0"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import socket
|
||||
import json
|
||||
from typing import Union
|
||||
|
||||
import lib.nlp as nlp
|
||||
|
||||
|
||||
class TCPServer:
|
||||
def __init__(self, host, port):
|
||||
def __init__(self, host: str, port: Union[str, int]):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@@ -48,7 +49,7 @@ class TCPServer:
|
||||
print(f'Client disconnected: {self.addr}')
|
||||
self.conn.close()
|
||||
|
||||
def get_spacy_entities(self, utterance):
|
||||
def get_spacy_entities(self, utterance: str) -> dict:
|
||||
entities = nlp.extract_spacy_entities(utterance)
|
||||
|
||||
return {
|
||||
|
||||
+58
-30
@@ -1,6 +1,7 @@
|
||||
import copy
|
||||
from sys import argv
|
||||
import spacy
|
||||
import geonamescache
|
||||
from geonamescache import GeonamesCache
|
||||
|
||||
lang = argv[1] or 'en'
|
||||
spacy_nlp = None
|
||||
@@ -25,32 +26,16 @@ spacy_model_mapping = {
|
||||
}
|
||||
}
|
||||
|
||||
gc = geonamescache.GeonamesCache()
|
||||
countries = gc.get_countries()
|
||||
cities = gc.get_cities()
|
||||
|
||||
|
||||
def gen_dict_extract(var, key):
|
||||
if isinstance(var, dict):
|
||||
for k, v in var.items():
|
||||
if k == key:
|
||||
yield v
|
||||
if isinstance(v, (dict, list)):
|
||||
yield from gen_dict_extract(v, key)
|
||||
elif isinstance(var, list):
|
||||
for d in var:
|
||||
yield from gen_dict_extract(d, key)
|
||||
|
||||
|
||||
countries = [*gen_dict_extract(countries, 'name')]
|
||||
cities = [*gen_dict_extract(cities, 'name')]
|
||||
geonamescache = GeonamesCache()
|
||||
countries = geonamescache.get_countries()
|
||||
cities = geonamescache.get_cities()
|
||||
|
||||
"""
|
||||
Functions called from TCPServer class
|
||||
"""
|
||||
|
||||
|
||||
def load_spacy_model():
|
||||
def load_spacy_model() -> None:
|
||||
global spacy_nlp
|
||||
|
||||
model = spacy_model_mapping[lang]['model']
|
||||
@@ -61,18 +46,63 @@ def load_spacy_model():
|
||||
print('spaCy model loaded')
|
||||
|
||||
|
||||
def extract_spacy_entities(utterance):
|
||||
def delete_unneeded_country_data(data: dict) -> None:
|
||||
try:
|
||||
del data['geonameid']
|
||||
del data['neighbours']
|
||||
del data['languages']
|
||||
del data['iso3']
|
||||
del data['fips']
|
||||
del data['currencyname']
|
||||
del data['postalcoderegex']
|
||||
del data['areakm2']
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
def extract_spacy_entities(utterance: str) -> list[dict]:
|
||||
doc = spacy_nlp(utterance)
|
||||
entities = []
|
||||
entities: list[dict] = []
|
||||
|
||||
for ent in doc.ents:
|
||||
if ent.label_ in spacy_model_mapping[lang]['entity_mapping']:
|
||||
entity = spacy_model_mapping[lang]['entity_mapping'][ent.label_]
|
||||
resolution = {
|
||||
'value': ent.text
|
||||
}
|
||||
|
||||
if entity == 'location':
|
||||
if ent.text.casefold() in (country.casefold() for country in countries):
|
||||
entity += ':country'
|
||||
elif ent.text.casefold() in (city.casefold() for city in cities):
|
||||
entity += ':city'
|
||||
for country in countries:
|
||||
if countries[country]['name'].casefold() == ent.text.casefold():
|
||||
entity += ':country'
|
||||
resolution['data'] = copy.deepcopy(countries[country])
|
||||
delete_unneeded_country_data(resolution['data'])
|
||||
break
|
||||
|
||||
city_population = 0
|
||||
for city in cities:
|
||||
alternatenames = [name.casefold() for name in cities[city]['alternatenames']]
|
||||
if cities[city]['name'].casefold() == ent.text.casefold() or ent.text.casefold() in alternatenames:
|
||||
if city_population == 0:
|
||||
entity += ':city'
|
||||
|
||||
if cities[city]['population'] > city_population:
|
||||
resolution['data'] = copy.deepcopy(cities[city])
|
||||
city_population = cities[city]['population']
|
||||
|
||||
for country in countries:
|
||||
if countries[country]['iso'] == cities[city]['countrycode']:
|
||||
resolution['data']['country'] = copy.deepcopy(countries[country])
|
||||
break
|
||||
try:
|
||||
del resolution['data']['geonameid']
|
||||
del resolution['data']['alternatenames']
|
||||
del resolution['data']['admin1code']
|
||||
delete_unneeded_country_data(resolution['data']['country'])
|
||||
except BaseException:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
|
||||
entities.append({
|
||||
'start': ent.start_char,
|
||||
@@ -81,9 +111,7 @@ def extract_spacy_entities(utterance):
|
||||
'sourceText': ent.text,
|
||||
'utteranceText': ent.text,
|
||||
'entity': entity,
|
||||
'resolution': {
|
||||
'value': ent.text
|
||||
}
|
||||
'resolution': resolution
|
||||
})
|
||||
|
||||
return entities
|
||||
|
||||
Reference in New Issue
Block a user