Compare commits

...

37 Commits

Author SHA1 Message Date
louistiti 754bd7380f build(bridge/python): 1.2.0 2023-11-14 22:38:34 +08:00
louistiti d32cda3663 build(bridge/nodejs): 1.1.0 2023-11-14 22:35:27 +08:00
louistiti c8d2e8c985 chore: bump to 1.0.0-beta.10+dev 2023-11-12 22:22:56 +08:00
louistiti 2a15434919 Merge branch 'feat-improve-ner-types' into develop 2023-11-12 18:16:37 +08:00
louistiti b2c375861b feat: identify entities from utterance samples for a better prioritization of classifications 2023-11-12 18:14:16 +08:00
Théo LUDWIG ae6fec335d fix(skill/date_time): add follow up utterance samples 2023-07-24 23:00:30 +02:00
louistiti 16302ec6e4 Merge branch 'develop' into feat-improve-ner-types 2023-07-23 11:26:38 +08:00
Louis Grenard 4d03e1bb4e docs(README): add knowledge base LLM scenario 2023-07-23 11:15:23 +08:00
Théo LUDWIG 69ce669b4c feat(tcp server): remove unnecessary data from entity + detect most populated city 2023-07-02 11:09:22 +02:00
Théo LUDWIG c512b422e7 feat(web app): add "Show more" when answer is "too big" 2023-07-01 23:33:41 +02:00
Théo LUDWIG 2ad6541c3e feat(skill/help): helps you get started with Leon 2023-07-01 23:28:55 +02:00
Théo LUDWIG 379326d7c1 feat(bridge/nodejs): simplify leon.setAnswerData 2023-07-01 23:24:29 +02:00
Théo LUDWIG 1359f5d210 feat(bridge/python): simplify leon.set_answer_data 2023-07-01 23:23:34 +02:00
Théo LUDWIG 64858ec6fe feat(tcp server): add Country information to City 2023-07-01 19:01:57 +02:00
Théo LUDWIG 166175192d feat(tcp server): better detect cities thanks to alternate names 2023-07-01 19:00:34 +02:00
Théo LUDWIG 97de1d1c3f fix(skill/date_time): correct time zone with toLocaleString method 2023-07-01 18:24:19 +02:00
louistiti 563792bd0a fix(tcp server): use root path for the time_zones.txt file 2023-06-30 22:35:58 +08:00
Théo LUDWIG 076db4c3eb fix(skill/date_time): determine correctly isDaylightSavingTime 2023-06-30 15:11:17 +02:00
Théo LUDWIG 02e1de8ffa fix: precise units used in time zone data 2023-06-29 23:14:14 +02:00
Théo LUDWIG cc01bd18b2 feat(skill/date_time): time zone 2023-06-29 23:03:03 +02:00
Théo LUDWIG 2478adbaa2 feat(server): add spacy data in entity resolution 2023-06-29 22:33:25 +02:00
Théo LUDWIG 45b177d0fb feat(tcp server): add support for time zone and more city/country data 2023-06-29 22:32:12 +02:00
Théo LUDWIG c0deafcd69 feat(server): improve NER types 2023-06-29 00:04:37 +02:00
louistiti eea6b0392d feat(skill/date_time): add new utterance samples 2023-06-29 00:17:29 +08:00
louistiti 9845612c68 Merge branch 'feat/skill-utilities-date-time' into develop 2023-06-28 23:39:25 +08:00
louistiti d46ec1d3d5 Merge branch 'feat/skill-leon-age' into develop 2023-06-28 23:39:15 +08:00
louistiti f1a65f53bc feat(skill/age): add new utterance samples 2023-06-28 23:39:07 +08:00
Théo LUDWIG 1410b72362 chore(skill/date_time): add test folder 2023-06-27 18:35:03 +02:00
Théo LUDWIG e9245b522f fix(skill/date_time): separate current date and current time 2023-06-25 22:46:10 +02:00
Théo LUDWIG 636eb0f139 feat(skill/date_time): days countdown 2023-06-25 12:27:30 +02:00
Théo LUDWIG 8645ef01fd fix: improve types for Node.js skill SDK 2023-06-25 12:08:36 +02:00
Théo LUDWIG 18867bde89 fix(server): improve json schema description for skill config 2023-06-25 12:07:41 +02:00
Théo LUDWIG d061a6c4bf feat(skill/date_time): get current week number of the year 2023-06-24 19:57:47 +02:00
Théo LUDWIG e2ec76275a feat(skill/date_time): get current date and time 2023-06-24 19:30:55 +02:00
Théo LUDWIG 767bace201 feat(skill/age): ask Leon his age 2023-06-24 13:23:19 +02:00
louistiti e70d0a0029 feat(web app): add init messages 2023-05-30 23:52:35 +08:00
louistiti 595fe78679 Merge branch 'python-sdk' into develop 2023-05-29 22:55:36 +08:00
56 changed files with 1131 additions and 648 deletions
+1 -1
View File
@@ -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...
+12
View File
@@ -287,6 +287,17 @@ small {
.bubble-container.leon {
text-align: left;
}
.show-more {
margin: 3px;
text-decoration: underline;
}
.show-more:hover {
cursor: pointer;
text-decoration: none;
}
.show-all {
max-height: 100% !important;
}
.bubble {
padding: 6px 12px;
@@ -297,6 +308,7 @@ small {
text-align: left;
opacity: 0;
animation: fadeIn 0.2s ease-in forwards;
overflow: hidden;
}
#feed .me .bubble {
background-color: #1c75db;
+19
View File
@@ -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)
}
+23
View File
@@ -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) => {
+21
View File
@@ -0,0 +1,21 @@
export const INIT_MESSAGES = [
[
`Hello there! Glad to e-meet meet you, I'm Leon, your open-source personal assistant. While I'm still learning and improving, I promise to do my best to be helpful.`
],
[
`<ul>
<li>We've got lots in the works. Check out <a href="http://roadmap.getleon.ai/" target="_blank">our roadmap</a>.</li>
<li>Stay updated on our progress by checking out our <a href="https://blog.getleon.ai/" target="_blank">blog</a>.</li>
<li>You can also get updates straight to your inbox <a href="https://newsletter.getleon.ai/subscription/form" target="_blank">here</a>.</li>
`
],
[
`Come hang out with us <a href="https://discord.gg/MNQqqKg" target="_blank">on Discord</a>! Once we release our official version, our community will be working together to build new skills for me. You won't want to miss out on the fun!`
],
[
`At the moment, I'm not using a large language model, but we're planning to incorporate some in the future to improve my abilities. You can <a href="https://github.com/leon-ai/leon#how-about-large-language-models-and-leon" target="_blank">learn more about our plans here</a>.`
],
[
`Just so you know, my creator is working tirelessly to improve my skills and features, dedicating 75% of his free time to the project on top of his full-time job. If you'd like to help speed up my development, you can sponsor his work by clicking on this link: <strong><a href='http://sponsor.getleon.ai/' target='_blank'>sponsor.getleon.ai</a></strong>. Your support would mean a lot to us. Thank you for choosing me as your assistant!`
]
]
+1
View File
@@ -2,6 +2,7 @@
"compilerOptions": {
"useDefineForClassFields": true,
"skipLibCheck": true,
"module": "ESNext",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
+66 -70
View File
@@ -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
}
}
+2
View File
@@ -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
View File
@@ -1 +1 @@
export const VERSION = '1.0.0'
export const VERSION = '1.1.0'
+33 -36
View File
@@ -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
View File
@@ -1 +1 @@
__version__ = '1.1.0'
__version__ = '1.2.0'
+51 -21
View File
@@ -68,27 +68,6 @@
"route": "/api/action/social_communication/mbti/quiz",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/have_i_been_pwned/run",
"params": []
},
{
"method": "POST",
"route": "/api/action/utilities/is_it_down/run",
"params": ["url"],
"entitiesType": "builtIn"
},
{
"method": "GET",
"route": "/api/action/utilities/speed_test/run",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/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
View File
@@ -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",
+31 -3
View File
@@ -1,10 +1,13 @@
import type { ShortLanguageCode } from '@/types'
import type {
BuiltInEntityType,
NEREntity,
NERSpacyEntity,
NLPUtterance,
NLUResult
NLUResult,
SpacyEntityType
} from '@/core/nlp/types'
import { BUILT_IN_ENTITY_TYPES, SPACY_ENTITY_TYPES } from '@/core/nlp/types'
import type {
SkillCustomEnumEntityTypeSchema,
SkillCustomRegexEntityTypeSchema,
@@ -40,6 +43,10 @@ export const MICROSOFT_BUILT_IN_ENTITIES = [
export default class NER {
private static instance: NER
public manager: NERManager
public spacyData: Map<
`${SpacyEntityType}-${string}`,
Record<string, unknown>
> = new Map()
constructor() {
if (!NER.instance) {
@@ -83,7 +90,7 @@ export default class NER {
lang
)
const { action } = classification
const promises = []
const promises: Array<Promise<void>> = []
const actionEntities = actions[action]?.entities || []
/**
@@ -121,6 +128,24 @@ export default class NER {
entity.resolution = { value: entity.sourceText }
}
if (
BUILT_IN_ENTITY_TYPES.includes(entity.entity as BuiltInEntityType)
) {
entity.type = entity.entity as BuiltInEntityType
}
if (SPACY_ENTITY_TYPES.includes(entity.entity as SpacyEntityType)) {
entity.type = entity.entity as SpacyEntityType
if (
'value' in entity.resolution &&
this.spacyData.has(`${entity.type}-${entity.resolution.value}`)
) {
entity.resolution = this.spacyData.get(
`${entity.type}-${entity.resolution.value}`
) as NERSpacyEntity['resolution']
}
}
return entity
})
@@ -139,17 +164,20 @@ export default class NER {
* Merge spaCy entities with the NER instance
*/
public async mergeSpacyEntities(utterance: NLPUtterance): Promise<void> {
this.spacyData = new Map()
const spacyEntities = await this.getSpacyEntities(utterance)
if (spacyEntities.length > 0) {
spacyEntities.forEach(({ entity, resolution }) => {
const value = StringHelper.ucFirst(resolution.value)
const spacyEntity = {
[entity]: {
options: {
[resolution.value]: [StringHelper.ucFirst(resolution.value)]
[resolution.value]: [value]
}
}
}
this.spacyData.set(`${entity}-${value}`, resolution)
MODEL_LOADER.mainNLPContainer.addEntities(spacyEntity, BRAIN.lang)
})
+214 -104
View File
@@ -29,7 +29,8 @@ export interface NLPJSProcessResult {
intent: string
score: number
}[]
intent: string // E.g. "greeting.run"
/** E.g. "greeting.run" */
intent: string
score: number
domain: NLPDomain
sourceEntities: unknown[]
@@ -40,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
+20 -12
View File
@@ -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(
+21 -9
View File
@@ -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."
]
}
}
+3 -3
View File
@@ -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"
}
}
+36 -134
View File
@@ -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
View File
@@ -1,4 +1 @@
{
"someSampleConfig": "someSampleValue",
"apiKey": "YOUR_API_KEY"
}
{}
+29
View File
@@ -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>"]
}
}
+12
View File
@@ -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"
}
}
+79
View File
@@ -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 @@
{}
View File
+32 -27
View File
@@ -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": [
{
+4 -1
View File
@@ -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"
}
}
+86
View File
@@ -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."
]
}
}
+12
View File
@@ -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"
]
}
},
+10 -10
View File
@@ -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"
}
]
}
}
-12
View File
@@ -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"
}
+2 -2
View File
@@ -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"
+3 -2
View File
@@ -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
View File
@@ -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