1
1
mirror of https://github.com/leon-ai/leon.git synced 2024-12-24 17:23:23 +03:00

feat: new onFetch skill API and more

This commit is contained in:
louistiti 2024-09-01 23:16:14 +08:00
parent 9aa9e0ba49
commit 8bca0595b4
No known key found for this signature in database
GPG Key ID: 92CD6A2E497E1669
16 changed files with 160 additions and 144 deletions

View File

@ -128,7 +128,7 @@ export default class Chatbot {
}
const data = await axios.get(
`${this.serverURL}/api/v1/fetch-widget?skill_action=${widgetContainer.onFetchAction}&widget_id=${widgetContainer.widgetId}`
`${this.serverURL}/api/v1/fetch-widget?skill_action=${widgetContainer.onFetch.actionName}&widget_id=${widgetContainer.widgetId}`
)
const fetchedWidget = data.data.widget
const reactNode = fetchedWidget
@ -201,7 +201,7 @@ export default class Chatbot {
/**
* On widget fetching, render the loader
*/
if (isCreatingFromLoadingFeed && parsedWidget.onFetchAction) {
if (isCreatingFromLoadingFeed && parsedWidget.onFetch) {
const root = createRoot(container)
root.render(
@ -217,7 +217,7 @@ export default class Chatbot {
WIDGETS_TO_FETCH.push({
reactRootNode: root,
widgetId: parsedWidget.id,
onFetchAction: parsedWidget.onFetchAction
onFetch: parsedWidget.onFetch
})
return

View File

@ -133,7 +133,7 @@ class Leon {
actionName: `${INTENT_OBJECT.domain}:${INTENT_OBJECT.skill}:${INTENT_OBJECT.action}`,
widget: answerInput.widget.widget,
id: answerInput.widget.id,
onFetchAction: answerInput.widget.onFetchAction,
onFetch: answerInput.widget.onFetch ?? null,
componentTree: new WidgetWrapper({
...answerInput.widget.wrapperProps,
children: [answerInput.widget.render()]

View File

@ -1,7 +1,7 @@
import { INTENT_OBJECT } from '@bridge/constants'
/**
* Get the widget ID if any
* Get the widget id if any
* @example getWidgetId() // 'timerwidget-5q1xlzeh
*/
export function getWidgetId(): string | null {

View File

@ -5,11 +5,6 @@ import { WidgetComponent } from '@sdk/widget-component'
type UtteranceSender = 'leon' | 'owner'
interface FetchWidgetDataWidgetEventMethodParams {
actionName: string
widgetId: string
dataToSet: string[]
}
interface SendUtteranceWidgetEventMethodParams {
from: UtteranceSender
utterance: string
@ -28,11 +23,13 @@ export interface WidgetEventMethod {
methodParams:
| SendUtteranceWidgetEventMethodParams
| RunSkillActionWidgetEventMethodParams
| FetchWidgetDataWidgetEventMethodParams
}
export interface WidgetOptions<T = unknown> {
wrapperProps?: Omit<WidgetWrapperProps, 'children'>
onFetchAction?: string
onFetch?: {
widgetId?: string | undefined
actionName: string
}
params: T
}
@ -40,7 +37,7 @@ export abstract class Widget<T = unknown> {
public actionName: string
public id: string
public widget: string
public onFetchAction: string | null = null
public onFetch: WidgetOptions<T>['onFetch'] | null = null
public wrapperProps: WidgetOptions<T>['wrapperProps']
public params: WidgetOptions<T>['params']
@ -48,15 +45,20 @@ export abstract class Widget<T = unknown> {
if (options?.wrapperProps) {
this.wrapperProps = options.wrapperProps
}
if (options?.onFetchAction) {
this.onFetchAction = `${INTENT_OBJECT.domain}:${INTENT_OBJECT.skill}:${options.onFetchAction}`
}
this.actionName = `${INTENT_OBJECT.domain}:${INTENT_OBJECT.skill}:${INTENT_OBJECT.action}`
this.widget = this.constructor.name
this.id = `${this.widget.toLowerCase()}-${Math.random()
.toString(36)
.substring(2, 10)}`
this.params = options.params
this.widget = this.constructor.name
if (options?.onFetch) {
this.onFetch = {
widgetId: options.onFetch.widgetId,
actionName: `${INTENT_OBJECT.domain}:${INTENT_OBJECT.skill}:${options.onFetch.actionName}`
}
}
this.id =
options.onFetch?.widgetId ||
`${this.widget.toLowerCase()}-${Math.random()
.toString(36)
.substring(2, 10)}`
}
/**

View File

@ -84,12 +84,12 @@ class Leon:
'actionName': f"{INTENT_OBJECT['domain']}:{INTENT_OBJECT['skill']}:{INTENT_OBJECT['action']}",
'widget': widget.widget,
'id': widget.id,
'onFetchAction': widget.on_fetch_action,
'onFetch': widget.on_fetch if hasattr(widget, 'on_fetch') else None,
'componentTree': WidgetWrapper({
**wrapper_props,
'children': [widget.render()]
}).__dict__(),
'supportedEvents': SUPPORTED_WIDGET_EVENTS # You might need to define this constant
'supportedEvents': SUPPORTED_WIDGET_EVENTS
}
answer_object = {

View File

@ -2,6 +2,7 @@ from typing import Any, Optional, Generic, TypeVar, Literal, TypedDict, Union, D
from dataclasses import dataclass
from abc import ABC, abstractmethod
import random
import string
from .widget_component import WidgetComponent
from ..constants import SKILL_CONFIG, INTENT_OBJECT
@ -11,12 +12,6 @@ T = TypeVar('T')
UtteranceSender = Literal['leon', 'owner']
class FetchWidgetDataWidgetEventMethodParams(TypedDict):
action_name: str
widget_id: str
data_to_set: list[str]
class SendUtteranceWidgetEventMethodParams(TypedDict):
from_: UtteranceSender
utterance: str
@ -36,8 +31,7 @@ class WidgetEventMethod(TypedDict):
methodName: Literal['send_utterance', 'run_skill_action']
methodParams: Union[
SendUtteranceWidgetEventMethodParams,
RunSkillActionWidgetEventMethodParams,
FetchWidgetDataWidgetEventMethodParams
RunSkillActionWidgetEventMethodParams
]
@ -45,18 +39,23 @@ class WidgetEventMethod(TypedDict):
class WidgetOptions(Generic[T]):
wrapper_props: dict[str, Any] = None
params: T = None
on_fetch_action: Optional[str] = None
on_fetch: Optional[dict[str, Any]] = None
class Widget(ABC, Generic[T]):
def __init__(self, options: WidgetOptions[T]):
if options.wrapper_props:
self.wrapper_props = options.wrapper_props
self.action_name = f"{INTENT_OBJECT['domain']}:{INTENT_OBJECT['skill']}:{INTENT_OBJECT['action']}"
self.widget = self.__class__.__name__
self.id = f"{self.widget.lower()}-{random.randint(100000, 999999)}"
self.wrapper_props = options.wrapper_props
self.params = options.params
self.on_fetch_action = f"{INTENT_OBJECT['domain']}:{INTENT_OBJECT['skill']}:{options.on_fetch_action}" \
if options.on_fetch_action else None
self.widget = self.__class__.__name__
if options.on_fetch:
self.on_fetch = {
'widgetId': options.on_fetch.get('widget_id'),
'actionName': f"{INTENT_OBJECT['domain']}:{INTENT_OBJECT['skill']}:{options.on_fetch.get('action_name')}"
}
self.id = options.on_fetch.get('widget_id') if options.on_fetch \
else f"{self.widget.lower()}-{''.join(random.choices(string.ascii_lowercase + string.digits, k=8))}"
@abstractmethod
def render(self) -> WidgetComponent:

View File

@ -1,4 +1,6 @@
from typing import TypeVar, Generic, TypedDict, List, Any
import random
import string
T = TypeVar('T')
@ -12,8 +14,6 @@ SUPPORTED_WIDGET_EVENTS = [
def generate_id() -> str:
import random
import string
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=5))

View File

@ -52,64 +52,6 @@
"route": "/api/action/games/rochambeau/rematch",
"params": []
},
{
"method": "POST",
"route": "/api/action/news/github_trends/run",
"params": ["number", "daterange"],
"entitiesType": "builtIn"
},
{
"method": "GET",
"route": "/api/action/news/product_hunt_trends/run",
"params": []
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/create_list",
"params": ["list"],
"entitiesType": "trim"
},
{
"method": "GET",
"route": "/api/action/productivity/todo_list/view_lists",
"params": []
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/view_list",
"params": ["list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/rename_list",
"params": ["old_list", "new_list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/delete_list",
"params": ["list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/add_todos",
"params": ["todos", "list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/complete_todos",
"params": ["todos", "list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/uncheck_todos",
"params": ["todos", "list"],
"entitiesType": "trim"
},
{
"method": "GET",
"route": "/api/action/leon/age/run",
@ -185,11 +127,64 @@
"route": "/api/action/leon/thanks/run",
"params": []
},
{
"method": "POST",
"route": "/api/action/news/github_trends/run",
"params": ["number", "daterange"],
"entitiesType": "builtIn"
},
{
"method": "GET",
"route": "/api/action/unknown/widget-playground/run",
"route": "/api/action/news/product_hunt_trends/run",
"params": []
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/create_list",
"params": ["list"],
"entitiesType": "trim"
},
{
"method": "GET",
"route": "/api/action/productivity/todo_list/view_lists",
"params": []
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/view_list",
"params": ["list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/rename_list",
"params": ["old_list", "new_list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/delete_list",
"params": ["list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/add_todos",
"params": ["todos", "list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/complete_todos",
"params": ["todos", "list"],
"entitiesType": "trim"
},
{
"method": "POST",
"route": "/api/action/productivity/todo_list/uncheck_todos",
"params": ["todos", "list"],
"entitiesType": "trim"
},
{
"method": "GET",
"route": "/api/action/social_communication/conversation/setup",
@ -215,6 +210,11 @@
"route": "/api/action/social_communication/mbti/quiz",
"params": []
},
{
"method": "GET",
"route": "/api/action/unknown/widget-playground/run",
"params": []
},
{
"method": "GET",
"route": "/api/action/utilities/date_time/current_date_time",

View File

@ -98,7 +98,10 @@ export interface SkillAnswerOutput extends IntentObject {
id: string
componentTree: WidgetWrapper
supportedEvents: typeof SUPPORTED_WIDGET_EVENTS
onFetchAction: string | null
onFetch: {
widgetId?: string
actionName: string
} | null
}
}
}

View File

@ -1,5 +1,7 @@
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.types import ActionParams
from bridges.python.src.sdk.widget import WidgetOptions
from ..widgets.todos_list_widget import TodosListWidget
from ..lib import memory
from typing import Union
@ -25,7 +27,11 @@ def run(params: ActionParams) -> None:
}
})
memory.create_todo_list(list_name)
todos_list_widget = TodosListWidget(WidgetOptions())
memory.create_todo_list(
todos_list_widget.id,
list_name
)
leon.answer({
'key': 'list_created',

View File

@ -1,7 +1,8 @@
from bridges.python.src.sdk.leon import leon
from bridges.python.src.sdk.toolbox import get_widget_id
from bridges.python.src.sdk.types import ActionParams
from bridges.python.src.sdk.widget import WidgetOptions
from ..widgets.todos_list_widget import TodosListWidget, TodosListWidgetParams
from ..widgets.todos_list_widget import TodosListWidget
from ..lib import memory
from typing import Union
@ -10,6 +11,7 @@ from typing import Union
def run(params: ActionParams) -> None:
"""View a to-do list"""
widget_id = get_widget_id()
list_name: Union[str, None] = None
for item in params['entities']:
@ -27,6 +29,7 @@ def run(params: ActionParams) -> None:
}
})
# TODO: if widget_id, get list by widget_id, otherwise get by list_name
todos = memory.get_todo_items(list_name)
if len(todos) == 0:
@ -37,10 +40,15 @@ def run(params: ActionParams) -> None:
}
})
todos_list_options: WidgetOptions[TodosListWidgetParams] = WidgetOptions(
wrapper_props={'noPadding': True},
params={'list_name': list_name, 'todos': todos}
todos_list_widget = TodosListWidget(
WidgetOptions(
wrapper_props={'noPadding': True},
params={'list_name': list_name, 'todos': todos},
on_fetch={
'widget_id': widget_id,
'action_name': 'view_list'
}
)
)
todos_list_widget = TodosListWidget(todos_list_options)
leon.answer({'widget': todos_list_widget})

View File

@ -15,13 +15,14 @@ todo_items_memory = Memory({
})
class TodoList(TypedDict):
class TodoListMemory(TypedDict):
widget_id: str
name: str
created_at: str
updated_at: str
class TodoItem(TypedDict):
class TodoItemMemory(TypedDict):
todo_list_name: str
name: str
is_completed: bool
@ -29,21 +30,22 @@ class TodoItem(TypedDict):
updated_at: str
def create_todo_list(name: str) -> None:
def create_todo_list(widget_id: str, name: str) -> None:
"""Create a new todo list"""
datetime_now = datetime.now().isoformat()
todo_list = TodoList(
todo_list = TodoListMemory(
widget_id=widget_id,
name=name,
created_at=datetime_now,
updated_at=datetime_now
)
todo_lists: list[TodoList] = todo_lists_memory.read()
todo_lists: list[TodoListMemory] = todo_lists_memory.read()
todo_lists.append(todo_list)
todo_lists_memory.write(todo_lists)
def get_todo_lists() -> list[TodoList]:
def get_todo_lists() -> list[TodoListMemory]:
"""Get all todo lists"""
return todo_lists_memory.read()
@ -52,7 +54,7 @@ def get_todo_lists() -> list[TodoList]:
def update_todo_list(old_name: str, new_name: str) -> None:
"""Update a todo list name"""
todo_lists: list[TodoList] = todo_lists_memory.read()
todo_lists: list[TodoListMemory] = todo_lists_memory.read()
for todo_list in todo_lists:
if todo_list['name'] == old_name:
todo_list['name'] = new_name
@ -60,7 +62,7 @@ def update_todo_list(old_name: str, new_name: str) -> None:
break
todo_lists_memory.write(todo_lists)
todo_items: list[TodoItem] = todo_items_memory.read()
todo_items: list[TodoItemMemory] = todo_items_memory.read()
for todo_item in todo_items:
if todo_item['todo_list_name'] == old_name:
todo_item['todo_list_name'] = new_name
@ -71,14 +73,14 @@ def update_todo_list(old_name: str, new_name: str) -> None:
def delete_todo_list(name: str) -> None:
"""Delete a todo list and its todos"""
todo_lists: list[TodoList] = todo_lists_memory.read()
todo_lists: list[TodoListMemory] = todo_lists_memory.read()
for todo_list in todo_lists:
if todo_list['name'] == name:
todo_lists.remove(todo_list)
break
todo_lists_memory.write(todo_lists)
todo_items: list[TodoItem] = todo_items_memory.read()
todo_items: list[TodoItemMemory] = todo_items_memory.read()
for todo_item in todo_items:
if todo_item['todo_list_name'] == name:
todo_items.remove(todo_item)
@ -94,7 +96,7 @@ def count_todo_lists() -> int:
def has_todo_list(name: str) -> bool:
"""Check if a todo list already exist"""
todo_lists: list[TodoList] = todo_lists_memory.read()
todo_lists: list[TodoListMemory] = todo_lists_memory.read()
for todo_list in todo_lists:
if todo_list['name'] == name:
return True
@ -107,22 +109,22 @@ def create_todo_item(todo_list_name: str, name: str) -> None:
if not has_todo_list(todo_list_name):
create_todo_list(todo_list_name)
datetime_now = datetime.now().isoformat()
todo_item = TodoItem(
todo_item = TodoItemMemory(
todo_list_name=todo_list_name,
name=name,
is_completed=False,
created_at=datetime_now,
updated_at=datetime_now
)
todo_items: list[TodoItem] = todo_items_memory.read()
todo_items: list[TodoItemMemory] = todo_items_memory.read()
todo_items.append(todo_item)
todo_items_memory.write(todo_items)
def get_todo_items(todo_list_name: str) -> list[TodoItem]:
def get_todo_items(todo_list_name: str) -> list[TodoItemMemory]:
"""Get all todo items of a todo list"""
todo_items: list[TodoItem] = todo_items_memory.read()
todo_items: list[TodoItemMemory] = todo_items_memory.read()
return [todo_item for todo_item in todo_items if todo_item['todo_list_name'] == todo_list_name]
@ -132,24 +134,24 @@ def count_todo_items(todo_list_name: str) -> int:
return len(get_todo_items(todo_list_name))
def get_completed_todo_items(todo_list_name: str) -> list[TodoItem]:
def get_completed_todo_items(todo_list_name: str) -> list[TodoItemMemory]:
"""Get all completed todo items of a todo list"""
todo_items: list[TodoItem] = todo_items_memory.read()
todo_items: list[TodoItemMemory] = todo_items_memory.read()
return [todo_item for todo_item in todo_items if todo_item['todo_list_name'] == todo_list_name and todo_item['is_completed']]
def get_uncompleted_todo_items(todo_list_name: str) -> list[TodoItem]:
def get_uncompleted_todo_items(todo_list_name: str) -> list[TodoItemMemory]:
"""Get all uncompleted todo items of a todo list"""
todo_items: list[TodoItem] = todo_items_memory.read()
todo_items: list[TodoItemMemory] = todo_items_memory.read()
return [todo_item for todo_item in todo_items if todo_item['todo_list_name'] == todo_list_name and not todo_item['is_completed']]
def complete_todo_item(todo_list_name: str, name: str) -> None:
"""Complete a todo item"""
todo_items: list[TodoItem] = todo_items_memory.read()
todo_items: list[TodoItemMemory] = todo_items_memory.read()
for todo_item in todo_items:
if todo_item['todo_list_name'] == todo_list_name and todo_item['name'] == name:
todo_item['is_completed'] = True
@ -161,7 +163,7 @@ def complete_todo_item(todo_list_name: str, name: str) -> None:
def uncomplete_todo_item(todo_list_name: str, name: str) -> None:
"""Uncomplete a todo item"""
todo_items: list[TodoItem] = todo_items_memory.read()
todo_items: list[TodoItemMemory] = todo_items_memory.read()
for todo_item in todo_items:
if todo_item['todo_list_name'] == todo_list_name and todo_item['name'] == name:
todo_item['is_completed'] = False

View File

@ -14,6 +14,7 @@ class TodoType(TypedDict):
class TodosListWidgetParams(TypedDict):
id: str
list_name: str
todos: list[TodoType]

View File

@ -24,13 +24,15 @@ export const run: ActionFunction = async function () {
const timerWidget = new TimerWidget({
params: {
id: widgetId ?? timerMemory.widgetId,
seconds: remainingTime,
initialProgress,
initialDuration: duration,
interval
},
onFetchAction: 'check_timer'
onFetch: {
widgetId: widgetId ?? timerMemory.widgetId,
actionName: 'check_timer'
}
})
await leon.answer({ widget: timerWidget })

View File

@ -29,18 +29,16 @@ export const run: ActionFunction = async function (params) {
initialProgress: 0,
interval
},
onFetchAction: 'check_timer'
onFetch: {
actionName: 'check_timer'
}
})
await createTimerMemory(timerWidget.id, seconds, interval)
// TODO: return a speech without new utterance
/*await leon.answer({
widget: timerWidget,
speech: 'I set a timer for ... ...'
})*/
await leon.answer({
widget: timerWidget,
key: 'timer_set'
})
await Promise.all([
createTimerMemory(timerWidget.id, seconds, interval),
leon.answer({
widget: timerWidget,
key: 'timer_set'
})
])
}

View File

@ -8,16 +8,11 @@ interface Params {
interval: number
initialProgress: number
initialDuration?: number
id?: string
}
export class TimerWidget extends Widget<Params> {
constructor(options: WidgetOptions<Params>) {
super(options)
if (options.params.id) {
this.id = options.params.id
}
}
public render(): WidgetComponent {