chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:44:08 +08:00
commit 983960e2dd
1244 changed files with 281996 additions and 0 deletions
View File
+93
View File
@@ -0,0 +1,93 @@
import sys
import json
import os
import version
DEFAULT_LEON_PROFILE = "just-me"
LEON_HOME_DIRNAME = ".leon"
argv = sys.argv[1:]
if "--runtime" in argv:
runtime_index = argv.index("--runtime")
argv = [
arg
for index, arg in enumerate(argv)
if index not in (runtime_index, runtime_index + 1)
]
INTENT_OBJ_FILE_PATH = argv[0] if argv else None
if not INTENT_OBJ_FILE_PATH:
raise Exception("Missing intent object path for skill runtime.")
with open(INTENT_OBJ_FILE_PATH, "r", encoding="utf-8") as f:
INTENT_OBJECT = json.load(f)
CODEBASE_PATH = os.path.abspath(
os.getenv("LEON_CODEBASE_PATH", "").strip() or os.getcwd()
)
LEON_HOME_PATH = os.path.abspath(
os.getenv("LEON_HOME", "").strip()
or os.path.join(os.path.expanduser("~"), LEON_HOME_DIRNAME)
)
LEON_PROFILE_NAME = os.getenv("LEON_PROFILE", "").strip() or DEFAULT_LEON_PROFILE
LEON_PROFILES_PATH = os.path.join(LEON_HOME_PATH, "profiles")
LEON_PROFILE_PATH = os.path.join(LEON_PROFILES_PATH, LEON_PROFILE_NAME)
LEON_TOOLKITS_PATH = os.path.join(LEON_HOME_PATH, "toolkits")
PROFILE_CONTEXT_PATH = os.path.join(LEON_PROFILE_PATH, "context")
PROFILE_MEMORY_PATH = os.path.join(LEON_PROFILE_PATH, "memory")
PROFILE_MEMORY_DB_PATH = os.path.join(PROFILE_MEMORY_PATH, "index.sqlite")
PROFILE_SKILLS_PATH = os.path.join(LEON_PROFILE_PATH, "skills")
PROFILE_NATIVE_SKILLS_PATH = os.path.join(PROFILE_SKILLS_PATH, "native")
PROFILE_AGENT_SKILLS_PATH = os.path.join(PROFILE_SKILLS_PATH, "agent")
PROFILE_TOOLS_PATH = os.path.join(LEON_PROFILE_PATH, "tools")
PROFILE_DISABLED_PATH = os.path.join(LEON_PROFILE_PATH, "disabled.json")
PROFILE_ALLOWED_PATH = os.path.join(LEON_PROFILE_PATH, "allowed.json")
SKILLS_ROOT_PATH = os.path.join(CODEBASE_PATH, "skills")
NATIVE_SKILLS_PATH = os.path.join(SKILLS_ROOT_PATH, "native")
AGENT_SKILLS_PATH = os.path.join(SKILLS_ROOT_PATH, "agent")
TOOLS_PATH = os.path.join(CODEBASE_PATH, "tools")
BIN_PATH = os.path.join(LEON_HOME_PATH, "bin")
BRIDGES_PATH = os.path.join(CODEBASE_PATH, "bridges")
NVIDIA_LIBS_PATH = os.path.join(BIN_PATH, "nvidia")
PYTORCH_PATH = os.path.join(BIN_PATH, "pytorch")
PYTORCH_TORCH_PATH = os.path.join(PYTORCH_PATH, "torch")
SKILL_PATH = os.path.dirname(INTENT_OBJECT["skill_config_path"])
SKILLS_PATH = SKILLS_ROOT_PATH
SKILL_LOCALE_PATH = os.path.join(
SKILL_PATH, "locales", f"{INTENT_OBJECT['extra_context']['lang']}.json"
)
if INTENT_OBJECT["skill_name"] and os.path.exists(SKILL_LOCALE_PATH):
with open(SKILL_LOCALE_PATH, "r", encoding="utf-8") as f:
SKILL_LOCALE_CONFIG_CONTENT = json.load(f)
else:
SKILL_LOCALE_CONFIG_CONTENT = {
"variables": {},
"common_answers": {},
"widget_contents": {},
"actions": {INTENT_OBJECT["action_name"]: {}},
}
SKILL_LOCALE_CONFIG = (
SKILL_LOCALE_CONFIG_CONTENT.get("actions", {})
.get(INTENT_OBJECT["action_name"], {})
.copy()
)
SKILL_LOCALE_CONFIG["variables"] = SKILL_LOCALE_CONFIG_CONTENT.get("variables", {})
SKILL_LOCALE_CONFIG["common_answers"] = SKILL_LOCALE_CONFIG_CONTENT.get(
"common_answers", {}
)
SKILL_LOCALE_CONFIG["widget_contents"] = SKILL_LOCALE_CONFIG_CONTENT.get(
"widget_contents", {}
)
LEON_VERSION = os.getenv("npm_package_version")
PYTHON_BRIDGE_VERSION = version.__version__
+124
View File
@@ -0,0 +1,124 @@
import sys
import os
import inspect
from traceback import print_exc
from importlib import util
from constants import INTENT_OBJECT, SKILL_PATH
from sdk.params_helper import ParamsHelper
# Mirror the Node bridge loader so Python actions can expose `run`,
# `default.run`, or a callable `default`.
def resolve_action_function(skill_action_module):
run_function = getattr(skill_action_module, 'run', None)
if callable(run_function):
return run_function
default_export = getattr(skill_action_module, 'default', None)
default_run_function = getattr(default_export, 'run', None)
if callable(default_run_function):
return default_run_function
if callable(default_export):
return default_export
return None
def get_skill_venv_site_packages_path():
venv_path = os.path.join(SKILL_PATH, 'src', '.venv')
candidates = [
os.path.join(
venv_path,
'Lib',
'site-packages'
),
os.path.join(
venv_path,
'lib',
f'python{sys.version_info.major}.{sys.version_info.minor}',
'site-packages'
)
]
for candidate in candidates:
if os.path.isdir(candidate):
return os.path.abspath(candidate)
return None
def main():
skill_site_packages_path = get_skill_venv_site_packages_path()
if skill_site_packages_path:
sys.path.insert(0, skill_site_packages_path)
params = {
'lang': INTENT_OBJECT['lang'],
'utterance': INTENT_OBJECT['utterance'],
'action_arguments': INTENT_OBJECT['action_arguments'],
'entities': INTENT_OBJECT['entities'],
'sentiment': INTENT_OBJECT['sentiment'],
'context_name': INTENT_OBJECT['context_name'],
'skill_name': INTENT_OBJECT['skill_name'],
'action_name': INTENT_OBJECT['action_name'],
'context': INTENT_OBJECT['context'],
'skill_config': INTENT_OBJECT['skill_config'],
'skill_config_path': INTENT_OBJECT['skill_config_path'],
'extra_context': INTENT_OBJECT['extra_context']
}
try:
sys.path.append('.')
sys.path.insert(0, os.path.dirname(SKILL_PATH))
action_path = os.path.join(
SKILL_PATH,
'src',
'actions',
INTENT_OBJECT['action_name'] + '.py'
)
spec = util.spec_from_file_location(
INTENT_OBJECT['skill_name']
+ '.src.actions.'
+ INTENT_OBJECT['action_name'],
action_path
)
if spec is None or spec.loader is None:
raise ImportError(f'Cannot load action module from "{action_path}"')
skill_action_module = util.module_from_spec(spec)
spec.loader.exec_module(skill_action_module)
run_function = resolve_action_function(skill_action_module)
if not callable(run_function):
raise TypeError(
f'Action "{INTENT_OBJECT["skill_name"]}:{INTENT_OBJECT["action_name"]}" '
'does not export a runnable action function'
)
params_helper = ParamsHelper(params)
# Inspect to decide how many args to pass
signature = inspect.signature(run_function)
param_count = len(signature.parameters)
if param_count >= 2:
run_function(params, params_helper)
elif param_count == 1:
run_function(params)
else:
run_function()
except Exception as e:
print(f"Error while running {INTENT_OBJECT['skill_name']} skill {INTENT_OBJECT['action_name']} action: {e}")
print_exc()
if __name__ == '__main__':
try:
main()
except Exception:
# Print full traceback error report if skills triggers an error from the call stack.
print_exc()
+14
View File
@@ -0,0 +1,14 @@
[project]
name = "leon-python-bridge"
# Keep this metadata version for uv project compatibility only.
# Leon runtime versioning still comes from `version.py`.
version = "1.0.0"
requires-python = "==3.11.9"
dependencies = [
"requests==2.32.3",
"beautifulsoup4==4.7.1",
"pypdl==1.5.6",
]
[tool.uv]
package = false
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Button(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Card(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Checkbox(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class CircularProgress(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Flexbox(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Form(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Icon(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class IconButton(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Image(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Input(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Link(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class List(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class ListHeader(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class ListItem(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Loader(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Progress(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Radio(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class RadioGroup(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class RangeSlider(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class ScrollContainer(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Select(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class SelectOption(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Status(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Switch(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Tab(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class TabContent(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class TabGroup(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class TabList(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
+6
View File
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class Text(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
@@ -0,0 +1,6 @@
from ..widget_component import WidgetComponent
class WidgetWrapper(WidgetComponent[dict]):
def __init__(self, props: dict):
super().__init__(props)
File diff suppressed because it is too large Load Diff
+164
View File
@@ -0,0 +1,164 @@
import random
import sys
from typing import Union, Dict, Any, Optional
from time import sleep
import json
import time
import os
from .aurora.widget_wrapper import WidgetWrapper
from .types import AnswerInput, AnswerData, AnswerConfig
from .widget_component import SUPPORTED_WIDGET_EVENTS
from ..constants import SKILL_LOCALE_CONFIG, INTENT_OBJECT
class Leon:
instance: 'Leon' = None
global_answers: Dict[str, Any] = {}
def __init__(self) -> None:
if not Leon.instance:
Leon.instance = self
self._load_global_answers()
def _load_global_answers(self) -> None:
"""Load global answers from core data directory"""
try:
lang = INTENT_OBJECT.get('lang', 'en')
answers_path = os.path.join(os.getcwd(), 'core', 'data', lang, 'answers.json')
if os.path.exists(answers_path):
with open(answers_path, 'r', encoding='utf-8') as f:
answers_data = json.load(f)
Leon.global_answers = answers_data.get('answers', {})
except Exception as e:
print(f"Warning: Could not load global answers: {e}")
Leon.global_answers = {}
@staticmethod
def _get_answer_text(answer: Union[str, AnswerConfig, None]) -> str:
"""Convert an answer config to a text-only value."""
if not answer:
return ''
if isinstance(answer, str):
return answer
return answer.get('text') or answer.get('speech') or ''
@staticmethod
def _inject_variables(answer: AnswerConfig, data_to_inject: Union[Dict[str, Any], None]) -> AnswerConfig:
"""A private helper to inject variables into an answer string or object"""
if not data_to_inject:
return answer
for key, value in data_to_inject.items():
if isinstance(answer, str):
answer = answer.replace(f"{{{{ {key} }}}}", str(value))
elif isinstance(answer, dict):
if 'text' in answer and answer['text']:
answer['text'] = answer['text'].replace(f"{{{{ {key} }}}}", str(value))
if 'speech' in answer and answer['speech']:
answer['speech'] = answer['speech'].replace(f"{{{{ {key} }}}}", str(value))
return answer
def set_answer_data(self, answer_key: str, data: Union[AnswerData, None] = None) -> Union[str, AnswerConfig]:
"""
Apply data to the answer
:param answer_key: The answer key
:param data: The data to apply
"""
try:
# Prioritize skill-specific answers, then fall back to common answers, then global answers
answers_config = (
SKILL_LOCALE_CONFIG.get('answers', {}).get(answer_key) or
SKILL_LOCALE_CONFIG.get('common_answers', {}).get(answer_key) or
Leon.global_answers.get(answer_key)
)
# In case the answer key is not found or is a raw answer
if not answers_config:
return answer_key
# Pick a random answer if it's a list
answer = random.choice(answers_config) if isinstance(answers_config, list) else answers_config
# Inject variables from the data parameter and from the global variables config
answer = self._inject_variables(answer, data)
answer = self._inject_variables(answer, SKILL_LOCALE_CONFIG.get('variables'))
return answer
except Exception as e:
print(f'Error while setting answer data. Please verify that the answer key "{answer_key}" exists in the locale configuration. Details:', e)
raise e
def answer(self, answer_input: AnswerInput) -> Optional[str]:
"""
Send an answer to the core
:param answer_input: The answer input
:return: Message ID for potential future replacement
"""
try:
key = answer_input.get('key')
resolved_answer = self.set_answer_data(key, answer_input.get('data')) if key is not None else ''
fallback_text = self._get_answer_text(resolved_answer)
if answer_input.get('widget') is not None and not fallback_text:
raise ValueError(
'Widget answers must include a text fallback via `key`.'
)
output = {
'output': {
'codes': 'widget' if answer_input.get('widget') and not answer_input.get('key') else answer_input.get('key'),
'answer': resolved_answer,
'core': answer_input.get('core'),
'replaceMessageId': answer_input.get('replaceMessageId')
}
}
widget = answer_input.get('widget')
if widget is not None:
wrapper_props = widget.wrapper_props if widget.wrapper_props else {}
output['output']['widget'] = {
'actionName': f"{INTENT_OBJECT['skill_name']}:{INTENT_OBJECT['action_name']}",
'widget': widget.widget,
'id': widget.id,
'onFetch': widget.on_fetch if hasattr(widget, 'on_fetch') else None,
'fallbackText': fallback_text,
'historyMode': answer_input.get('widgetHistoryMode') or 'persisted',
'componentTree': WidgetWrapper({
**wrapper_props,
'children': [widget.render()]
}).__dict__(),
'supportedEvents': SUPPORTED_WIDGET_EVENTS
}
answer_object = {
**INTENT_OBJECT,
**output
}
# "Temporize" for the data buffer output on the core
sleep(0.1)
# Write the answer object to stdout as a JSON string with a newline for brain chunk-by-chunk parsing
sys.stdout.write(json.dumps(answer_object) + '\n')
sys.stdout.flush()
# Return the message ID for future replacement (matches Node.js SDK)
return (
widget.id if widget else
f"msg-{int(time.time() * 1000)}-{hex(random.randint(0, 0xffffff))[2:]}"
)
except Exception as e:
print('Error while creating answer:', e)
if 'not JSON serializable' in str(e):
print("Hint: make sure that widget children components are a list. "
"E.g. { 'children': [Text({ 'children': 'Hello' })] }")
return None
leon = Leon()
+87
View File
@@ -0,0 +1,87 @@
import json
import os
from typing import TypedDict, Any
from ..constants import PROFILE_NATIVE_SKILLS_PATH, SKILL_PATH
SKILL_NAME_SUFFIX = "_skill"
def normalize_skill_name(skill_name: str) -> str:
return (
skill_name
if skill_name.endswith(SKILL_NAME_SUFFIX)
else f"{skill_name}{SKILL_NAME_SUFFIX}"
)
class MemoryOptions(TypedDict, total=False):
name: str
default_memory: Any
class Memory:
def __init__(self, options: MemoryOptions):
self.name = options['name']
self.default_memory = options['default_memory'] if 'default_memory' in options else None
self.memory_path = os.path.join(
PROFILE_NATIVE_SKILLS_PATH,
os.path.basename(SKILL_PATH),
'memory',
f'{self.name}.json'
)
self.__is_from_another_skill = False
if ':' in self.name and self.name.count(':') == 2:
self.__is_from_another_skill = True
_, skill_name, memory_name = self.name.split(':')
self.memory_path = os.path.join(
PROFILE_NATIVE_SKILLS_PATH,
normalize_skill_name(skill_name),
'memory',
memory_name + '.json'
)
def clear(self) -> None:
"""
Clear the memory and set it to the default memory value
"""
if not self.__is_from_another_skill:
self.write(self.default_memory)
else:
raise ValueError(f'You cannot clear the memory "{self.name}" as it belongs to another skill')
def read(self):
"""
Read the memory
"""
if self.__is_from_another_skill and not os.path.exists(self.memory_path):
raise ValueError(f'You cannot read the memory "{self.name}" as it belongs to another skill which hasn\'t written to this memory yet')
try:
if not os.path.exists(self.memory_path):
self.clear()
with open(self.memory_path, 'r') as f:
return json.load(f)
except Exception as e:
print(f'Error while reading memory for "{self.name}": {e}')
raise e
def write(self, memory):
"""
Write the memory
:param memory: The memory to write
"""
if not self.__is_from_another_skill:
try:
os.makedirs(os.path.dirname(self.memory_path), exist_ok=True)
with open(self.memory_path, 'w') as f:
json.dump(memory, f, indent=2)
return memory
except Exception as e:
print(f'Error while writing memory for "{self.name}": {e}')
raise e
else:
raise ValueError(f'You cannot write into the memory "{self.name}" as it belongs to another skill')
+148
View File
@@ -0,0 +1,148 @@
import json
import requests
import socket
import sys
from typing import Any, Dict, TypedDict, Union, Literal, Optional
from ..constants import LEON_VERSION, PYTHON_BRIDGE_VERSION
class NetworkOptions(TypedDict, total=False):
base_url: Optional[str]
class NetworkResponse(TypedDict):
data: Any
status_code: int
options: Dict[str, Any]
class NetworkError(Exception):
def __init__(self, response: NetworkResponse) -> None:
self.response = response
super().__init__(f"[NetworkError]: {response['status_code']}")
@staticmethod
def _format_error_data(data: Any) -> str:
if isinstance(data, str):
return data
try:
return json.dumps(data)
except Exception:
return str(data)
class NetworkRequestOptions(TypedDict, total=False):
url: str
method: Union[Literal['GET'], Literal['POST'], Literal['PUT'], Literal['PATCH'], Literal['DELETE']]
data: Dict[str, Any]
headers: Dict[str, str]
files: Dict[str, Any]
use_json: bool
response_type: Optional[Union[Literal['json'], Literal['text'], Literal['arraybuffer'], Literal['bytes']]]
class Network:
def __init__(self, options: NetworkOptions = {'base_url': None}) -> None:
self.options = options
def request(self, options: NetworkRequestOptions) -> NetworkResponse:
try:
url = options['url']
if self.options['base_url'] is not None:
url = (self.options['base_url'] or '') + url
method = options['method']
data = options.get('data', {})
headers = options.get('headers', {})
files = options.get('files')
use_json = options.get('use_json', True)
response_type = options.get('response_type', 'json')
request_kwargs: Dict[str, Any] = {
'headers': {
'User-Agent': f"Leon Personal Assistant {LEON_VERSION} - Python Bridge {PYTHON_BRIDGE_VERSION}",
**headers
}
}
# If files are provided or JSON is explicitly disabled, send form data
if files or not use_json:
request_kwargs['data'] = data
if files:
request_kwargs['files'] = files
else:
request_kwargs['json'] = data
response = requests.request(
method,
url,
**request_kwargs
)
parsed_data: Any
if response_type in ['arraybuffer', 'bytes']:
parsed_data = response.content
else:
try:
parsed_data = response.json()
except Exception:
parsed_data = response.text
network_response: NetworkResponse = {
'data': parsed_data,
'status_code': response.status_code,
'options': {**self.options, **options}
}
if response.ok:
return network_response
else:
print(
'[NetworkError]',
network_response['status_code'],
options.get('method'),
options.get('url'),
NetworkError._format_error_data(network_response['data']),
file=sys.stderr
)
raise NetworkError(network_response)
except requests.exceptions.RequestException as error:
status_code = 500
raw_data: Any = ''
if error.response is not None:
status_code = error.response.status_code
try:
raw_data = error.response.json()
except Exception:
raw_data = error.response.text
response_payload: NetworkResponse = {
'data': raw_data,
'status_code': status_code,
'options': {**self.options, **options}
}
print(
'[NetworkError]',
response_payload['status_code'],
options.get('method'),
options.get('url'),
NetworkError._format_error_data(response_payload['data']),
file=sys.stderr
)
raise NetworkError(response_payload) from error
def is_network_error(self, error: Exception) -> bool:
return isinstance(error, NetworkError)
def is_network_available(self) -> bool:
try:
socket.gethostbyname('getleon.ai')
return True
except socket.error:
return False
+131
View File
@@ -0,0 +1,131 @@
from typing import Any, Dict, List, Optional
from constants import INTENT_OBJECT
NEREntity = Dict[str, Any]
ActionParams = Dict[str, Any]
class ParamsHelper:
"""
A helper class to simplify accessing data from the action's params object
"""
def __init__(self, params: ActionParams):
self._params = params
def get_widget_id(self) -> Optional[str]:
"""
Get the widget ID if any
"""
for entity in INTENT_OBJECT['entities']:
if entity['entity'] == 'widgetid':
return entity['sourceText']
return None
def get_action_argument(self, name: str) -> Optional[Any]:
"""
Get a specific action argument from the current turn by its name
:param name: The name of the action argument to retrieve
"""
return self._params.get('action_arguments', {}).get(name)
def find_entity(self, entity_name: str) -> Optional[NEREntity]:
"""
Find the first entity in the current turn that matches the given name
:param entity_name: The name of the entity to find (e.g., 'language')
"""
entities = self._params.get('entities', [])
# A generator expression with next() is an efficient way to find the first item
return next((entity for entity in entities if entity.get('entity') == entity_name), None)
def find_last_entity(self, entity_name: str) -> Optional[NEREntity]:
"""
Find the last entity in the current turn that matches the given name.
Useful when an utterance contains duplicates
:param entity_name: The name of the entity to find (e.g., 'color')
"""
entities = self._params.get('entities', [])
# Iterate over a reversed list to find the last occurrence first
return next((entity for entity in reversed(entities) if entity.get('entity') == entity_name), None)
def find_all_entities(self, entity_name: str) -> List[NEREntity]:
"""
Find all entities in the current turn that match the given name
:param entity_name: The name of the entities to find (e.g., 'date')
"""
entities = self._params.get('entities', [])
return [entity for entity in entities if entity.get('entity') == entity_name]
def find_action_argument_from_context(self, name: str) -> Optional[Any]:
"""
Find the first action argument in the conversation context that matches the given name
:param name: The name of the action argument to find
"""
action_args_history = self._params.get('context', {}).get('action_arguments', [])
for args in action_args_history:
if args and name in args:
return args[name]
return None
def find_last_action_argument_from_context(self, name: str) -> Optional[Any]:
"""
Find the most recent value for a given action argument from the conversation context.
It searches backwards from the most recent turn
:param name: The name of the action argument to find
"""
action_args_history = self._params.get('context', {}).get('action_arguments', [])
for args in reversed(action_args_history):
if args and name in args:
return args[name]
return None
def find_last_entity_from_context(self, entity_name: str) -> Optional[NEREntity]:
"""
Find the most recently detected entity (the last one from the context) that matches the given name.
This is useful for recalling the last time a user mentioned a specific piece of information
:param entity_name: The name of the entity to find in the conversation history
"""
context_entities = self._params.get('context', {}).get('entities', [])
return next((entity for entity in reversed(context_entities) if entity.get('entity') == entity_name), None)
def find_all_entities_from_context(self, entity_name: str) -> List[NEREntity]:
"""
Find all historical entities that match the given name from the entire conversation context
:param entity_name: The name of the entities to find in the conversation history
"""
context_entities = self._params.get('context', {}).get('entities', [])
return [entity for entity in context_entities if entity.get('entity') == entity_name]
def get_context_data(self, key: str) -> Optional[Any]:
"""
Get a value stored in the generic context data store
:param key: The key to retrieve
"""
return self._params.get('context', {}).get('data', {}).get(key)
+95
View File
@@ -0,0 +1,95 @@
import json
import os
from os import path
from typing import Union, Any, overload
from ..constants import PROFILE_NATIVE_SKILLS_PATH, SKILL_PATH
class Settings:
def __init__(self):
self.settings_path = path.join(
PROFILE_NATIVE_SKILLS_PATH,
path.basename(SKILL_PATH),
'settings.json'
)
self.settings_sample_path = path.join(SKILL_PATH, 'src', 'settings.sample.json')
def is_setting_set(self, key: str) -> bool:
"""
Check if a setting is already set
:param key: The key to verify whether its value is set
"""
settings_sample = self.get_settings_sample()
settings = self.get()
return key in settings and json.dumps(settings[key]) != json.dumps(settings_sample[key])
def clear(self) -> None:
"""
Clear the settings and set it to the default settings.sample.json file
"""
settings_sample = self.get_settings_sample()
self.set(settings_sample)
def get_settings_sample(self) -> dict[str, Any]:
try:
with open(self.settings_sample_path, 'r') as file:
return json.load(file)
except Exception as e:
print(f"Error while reading settings sample at '{self.settings_sample_path}': {e}")
raise e
@overload
def get(self, key: str) -> Any: ...
@overload
def get(self, key: None = None) -> dict[str, Any]: ...
def get(self, key: Union[str, None] = None) -> Union[dict[str, Any], Any]:
"""
Get the settings
:param key: The key to get from the settings
"""
try:
if not os.path.exists(self.settings_path):
self.clear()
with open(self.settings_path, 'r') as file:
settings = json.load(file)
if key is not None:
return settings[key]
return settings
except Exception as e:
print(f"Error while reading settings at '{self.settings_path}': {e}")
raise e
@overload
def set(self, key_or_settings: dict[str, Any]) -> dict[str, Any]: ...
@overload
def set(self, key_or_settings: str, value: Any) -> dict[str, Any]: ...
def set(self, key_or_settings: Union[str, dict[str, Any]], value: Any = None) -> dict[str, Any]:
"""
Set the settings
:param key_or_settings: The key to set or the settings to set
:param value: The value to set
"""
try:
if isinstance(key_or_settings, dict):
new_settings = key_or_settings
else:
settings = self.get()
new_settings = {**settings, key_or_settings: value}
os.makedirs(path.dirname(self.settings_path), exist_ok=True)
with open(self.settings_path, 'w') as file:
json.dump(new_settings, file, indent=2)
return new_settings
except Exception as e:
print(f"Error while writing settings at '{self.settings_path}': {e}")
raise e
+46
View File
@@ -0,0 +1,46 @@
from typing import Optional, Type
from .base_tool import BaseTool
from .leon import leon
from .utils import format_file_path
class MissingToolSettingsError(Exception):
def __init__(self, missing: list[str], settings_path: str):
super().__init__(f"Missing tool settings: {', '.join(missing)}")
self.missing = missing
self.settings_path = settings_path
class ToolManager:
@staticmethod
def init_tool(tool_class: Type[BaseTool]) -> BaseTool:
tool = tool_class()
missing = tool.get_missing_settings()
if missing:
leon.answer(
{
"key": "bridges.tools.missing_settings",
"data": {
"tool_name": tool.alias_tool_name,
"missing": ", ".join(missing.get("missing", [])),
"settings_path": format_file_path(
missing.get("settings_path", "")
),
},
"core": {
"should_stop_skill": True,
},
}
)
raise MissingToolSettingsError(
missing.get("missing", []),
missing.get("settings_path", ""),
)
return tool
def is_missing_tool_settings_error(error: Exception) -> bool:
return isinstance(error, MissingToolSettingsError)
+150
View File
@@ -0,0 +1,150 @@
import json
import os
from typing import Dict, Any, Optional
from ..constants import PROFILE_TOOLS_PATH, TOOLS_PATH
from .utils import get_platform_name
def merge_missing_settings(
default_settings: Dict[str, Any], existing_settings: Dict[str, Any]
) -> Dict[str, Any]:
merged_settings = {**existing_settings}
for key, default_value in default_settings.items():
if key not in existing_settings:
merged_settings[key] = default_value
continue
existing_value = existing_settings[key]
if isinstance(default_value, dict) and isinstance(existing_value, dict):
merged_settings[key] = merge_missing_settings(
default_value, existing_value
)
return merged_settings
class ToolkitConfig:
"""Toolkit configuration loader"""
_config_cache: Dict[str, Dict[str, Any]] = {}
_settings_cache: Dict[str, Dict[str, Any]] = {}
@classmethod
def load(cls, toolkit_name: str, tool_name: str) -> Dict[str, Any]:
"""
Load tool configuration from the flat tools structure.
Args:
toolkit_name: The toolkit name (e.g., 'video_streaming')
tool_name: Name of the tool (e.g., 'ffmpeg')
"""
cache_key = toolkit_name
# Load toolkit config if not cached
if cache_key not in cls._config_cache:
config_path = os.path.join(TOOLS_PATH, toolkit_name, "toolkit.json")
try:
with open(config_path, "r", encoding="utf-8") as f:
toolkit_config = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
raise Exception(
f"Failed to load toolkit config from '{config_path}': {str(e)}"
)
cls._config_cache[cache_key] = toolkit_config
toolkit_config = cls._config_cache[cache_key]
tools_list = toolkit_config.get("tools", [])
tool_config_path = os.path.join(TOOLS_PATH, toolkit_name, tool_name, "tool.json")
if tool_name not in tools_list and not os.path.exists(tool_config_path):
toolkit_name_display = toolkit_config.get("name", "unknown")
raise Exception(
f"Tool '{tool_name}' not found in toolkit '{toolkit_name_display}'"
)
try:
with open(tool_config_path, "r", encoding="utf-8") as f:
tool_config = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
raise Exception(
f"Failed to load tool config from '{tool_config_path}': {str(e)}"
)
return tool_config
@classmethod
def load_tool_settings(
cls,
toolkit_name: str,
tool_name: str,
defaults: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Load tool-specific settings from toolkit settings file
Args:
toolkit_name: The toolkit name (e.g., 'video_streaming')
tool_name: Name of the tool (e.g., 'ffmpeg')
defaults: Default tool settings to apply when missing
"""
cache_key = f"{toolkit_name}:{tool_name}"
if cache_key in cls._settings_cache:
return cls._settings_cache[cache_key]
settings_path = os.path.join(
PROFILE_TOOLS_PATH, toolkit_name, tool_name, "settings.json"
)
settings_sample_path = os.path.join(
TOOLS_PATH, toolkit_name, tool_name, "settings.sample.json"
)
settings_dir = os.path.dirname(settings_path)
os.makedirs(settings_dir, exist_ok=True)
default_settings = defaults or {}
if os.path.exists(settings_sample_path):
try:
with open(settings_sample_path, "r", encoding="utf-8") as f:
default_settings = json.load(f)
except json.JSONDecodeError as e:
raise Exception(
f"Failed to load tool settings sample from '{settings_sample_path}': {str(e)}"
)
tool_settings: Dict[str, Any] = {}
should_write = False
if os.path.exists(settings_path):
try:
with open(settings_path, "r", encoding="utf-8") as f:
tool_settings = json.load(f)
except json.JSONDecodeError as e:
raise Exception(
f"Failed to load toolkit settings from '{settings_path}': {str(e)}"
)
else:
should_write = True
merged_settings = merge_missing_settings(default_settings, tool_settings)
if not should_write:
should_write = tool_settings != merged_settings
if should_write:
with open(settings_path, "w", encoding="utf-8") as f:
json.dump(merged_settings, f, indent=2)
cls._settings_cache[cache_key] = merged_settings
return merged_settings
@classmethod
def get_binary_url(cls, config: Dict[str, Any]) -> Optional[str]:
"""Get binary download URL for current platform with architecture granularity"""
platform_name = get_platform_name()
binaries = config.get("binaries", {})
return binaries.get(platform_name)
+82
View File
@@ -0,0 +1,82 @@
from typing import Dict, Any, Optional, Union, Literal, TypedDict
from .widget import Widget
class NLUResultSentiment(TypedDict):
vote: Optional[Union[Literal['positive'], Literal['neutral'], Literal['negative']]]
score: Optional[float]
class Context(TypedDict):
utterances: list[str]
action_arguments: list[Dict[str, Any]]
entities: list[Any]
sentiments: list[NLUResultSentiment]
data: Dict[str, Any]
class SkillConfig(TypedDict):
name: str
bridge: Union[Literal['python'], Literal['nodejs']]
version: str
workflow: list[str]
class ExtraContext(TypedDict):
lang: str
date: str
time: str
timestamp: int
date_time: str
week_day: str
class ActionParams(TypedDict):
lang: str
utterance: str
action_arguments: Dict[str, Any]
entities: list[Any]
sentiment: NLUResultSentiment
context_name: str
skill_name: str
action_name: str
context: Context
skill_config: SkillConfig
skill_config_path: str
extra_context: ExtraContext
AnswerData = Optional[Union[Dict[str, Union[str, int]], None]]
class Answer(TypedDict, total=False):
key: Optional[str]
widget: Optional[Widget]
data: Optional[AnswerData]
core: Optional[Dict[str, Any]]
replaceMessageId: Optional[str]
widgetHistoryMode: Optional[Literal['persisted', 'system_widget']]
class TextAnswer(Answer):
key: str
class WidgetAnswer(Answer):
widget: Widget
key: Optional[str]
class AnswerInput(TypedDict, total=False):
key: Optional[str]
widget: Optional[Widget]
data: Optional[AnswerData]
core: Optional[Dict[str, Any]]
replaceMessageId: Optional[str]
widgetHistoryMode: Optional[Literal['persisted', 'system_widget']]
class AnswerConfig(TypedDict, total=False):
text: Optional[str]
speech: Optional[str]
+318
View File
@@ -0,0 +1,318 @@
import platform
from typing import List, Optional
import urllib.request
import urllib.error
import math
from typing import Union
import os
import subprocess
HUGGING_FACE_URL = 'https://huggingface.co'
HUGGING_FACE_MIRROR_URL = 'https://hf-mirror.com'
ZIP_ARCHIVE_EXTENSIONS = {'.zip', '.whl'}
def can_access_hugging_face() -> bool:
"""Check if the current network can access Hugging Face
Returns:
True if Hugging Face is accessible, False otherwise
Example:
can_access_hugging_face() # returns True if accessible
"""
try:
req = urllib.request.Request(HUGGING_FACE_URL, method='HEAD')
with urllib.request.urlopen(req, timeout=5) as response:
return response.status == 200
except (urllib.error.URLError, urllib.error.HTTPError, Exception):
return False
def set_hugging_face_url(url: str) -> str:
"""Set the Hugging Face URL based on the network access
Args:
url: The URL to set
Returns:
The original URL if accessible, or the mirror URL if not accessible
Example:
set_hugging_face_url('https://huggingface.co') # returns 'https://hf-mirror.com' if not accessible
"""
if 'huggingface.co' not in url:
return url
can_access = can_access_hugging_face()
if not can_access:
return url.replace(HUGGING_FACE_URL, HUGGING_FACE_MIRROR_URL)
return url
def format_file_path(file_path: str) -> str:
"""Formats a file path as a clickable path with proper delimiters
Args:
file_path: The absolute file path to format
Returns:
A formatted string that the client can detect and make clickable
Example:
format_file_path('/Users/john/video.mp4') # returns '[FILE_PATH]/Users/john/video.mp4[/FILE_PATH]'
"""
return f"[FILE_PATH]{file_path}[/FILE_PATH]"
def format_file_paths(file_paths: List[str]) -> str:
"""Formats multiple file paths as a list of clickable paths
Args:
file_paths: List of absolute file paths
Returns:
A formatted string with multiple clickable paths
Example:
format_file_paths(['/path1', '/path2']) # returns '[FILE_PATH]/path1[/FILE_PATH], [FILE_PATH]/path2[/FILE_PATH]'
"""
return ', '.join(format_file_path(path) for path in file_paths)
def normalize_language_code(value: str) -> Optional[str]:
"""Normalize a language input to an ISO 639-1 code.
Supports direct language codes and locale tags such as ``fr-FR``.
Returns ``None`` when the input is not a supported code-like value.
"""
trimmed_value = value.strip()
if not trimmed_value:
return None
normalized_value = trimmed_value.replace('_', '-').lower()
language = normalized_value.split('-', 1)[0].strip()
if len(language) == 2 and language.isalpha():
return language
return None
def get_platform_name() -> str:
"""Get platform name with architecture granularity (matches system-helper.ts)
Returns:
Platform name string (e.g., 'linux-x86_64', 'macosx-arm64', 'win-amd64')
Example:
get_platform_name() # returns 'macosx-arm64' on Apple Silicon Mac
"""
system = platform.system().lower()
architecture = platform.machine().lower()
if system == 'linux':
if architecture in ['x86_64', 'amd64']:
return 'linux-x86_64'
elif architecture in ['aarch64', 'arm64']:
return 'linux-aarch64'
else:
# Default to x86_64 for unknown architectures on Linux
return 'linux-x86_64'
elif system == 'darwin':
if architecture in ['arm64', 'aarch64'] or 'apple' in platform.processor().lower():
return 'macosx-arm64'
else:
return 'macosx-x86_64'
elif system == 'windows':
return 'win-amd64'
else:
return 'unknown'
def is_windows() -> bool:
"""Check if current platform is Windows
Returns:
True if running on Windows, False otherwise
Example:
if is_windows(): executable_name += '.exe'
"""
return get_platform_name().startswith('win')
def is_macos() -> bool:
"""Check if current platform is macOS
Returns:
True if running on macOS, False otherwise
Example:
if is_macos(): remove_quarantine_attribute(binary_path)
"""
return get_platform_name().startswith('macosx')
def is_linux() -> bool:
"""Check if current platform is Linux
Returns:
True if running on Linux, False otherwise
Example:
if is_linux(): check_system_package('ffmpeg')
"""
return get_platform_name().startswith('linux')
def format_bytes(bytes_val: float) -> str:
"""Format bytes into human-readable units
Args:
bytes_val: The number of bytes to format
Returns:
A human-readable string representation
Example:
format_bytes(1024) # returns "1 KB"
format_bytes(1536) # returns "1.5 KB"
"""
if bytes_val == 0:
return "0 B"
k = 1024
sizes = ['B', 'KB', 'MB', 'GB', 'TB']
i = int(math.log(bytes_val) / math.log(k)) if bytes_val > 0 else 0
return f"{round(bytes_val / (k ** i), 2)} {sizes[i]}"
def format_speed(speed: Union[float, str]) -> str:
"""Format speed from MB/s to human-readable format
Args:
speed: The speed in MB/s (pypdl format) or already formatted string
Returns:
A human-readable speed string
Example:
format_speed(1.5) # returns "1.5 MB/s" (pypdl returns in MB/s)
format_speed("1.5 MB/s") # returns "1.5 MB/s" (already formatted)
"""
if isinstance(speed, str):
# If it's already formatted (e.g., "1.5 MB/s"), return as is
if '/s' in speed:
return speed
# If it's a string number, convert to float
try:
speed = float(speed)
except ValueError:
return '0 B/s'
if speed == 0:
return '0 B/s'
# pypdl returns speed in MB/s, convert to bytes/s for formatting
bytes_per_sec = speed * 1024 * 1024
return format_bytes(bytes_per_sec) + '/s'
def format_eta(eta_str: str) -> str:
"""Format ETA from HH:MM:SS to human-readable format
Args:
eta_str: The ETA in HH:MM:SS format (pypdl format)
Returns:
A human-readable ETA string
Example:
format_eta("01:02:30") # returns "1h 2m 30s"
format_eta("00:02:30") # returns "2m 30s"
format_eta("00:00:30") # returns "30s"
"""
if not eta_str or eta_str == '':
return ''
try:
# Parse HH:MM:SS format
parts = eta_str.split(':')
if len(parts) == 3:
hours = int(parts[0])
minutes = int(parts[1])
seconds = int(parts[2])
if hours > 0:
return f"{hours}h {minutes}m {seconds}s"
elif minutes > 0:
return f"{minutes}m {seconds}s"
return f"{seconds}s"
return eta_str
except (ValueError, IndexError):
return eta_str
def extract_archive(
archive_path: str,
target_path: str,
strip_components: Optional[int] = 0
) -> None:
"""Extract archive file using native system commands
Supports .zip, .tar, .tar.gz, .tar.xz, .tgz formats across all platforms
Args:
archive_path: The path to the archive file
target_path: The path to extract to
strip_components: Number of leading path components to strip (for tar archives)
Example:
extract_archive('archive.zip', 'output/dir')
extract_archive('archive.tar.xz', 'output/dir', strip_components=1)
"""
# Ensure target directory exists
os.makedirs(target_path, exist_ok=True)
ext = os.path.splitext(archive_path)[1].lower()
basename = os.path.basename(archive_path).lower()
try:
if ext in ZIP_ARCHIVE_EXTENSIONS:
if is_windows():
subprocess.run(
['tar', '-xf', archive_path, '-C', target_path],
check=True,
capture_output=True
)
else:
subprocess.run(
['unzip', '-o', '-q', archive_path, '-d', target_path],
check=True,
capture_output=True
)
elif (basename.endswith('.tar.gz') or
basename.endswith('.tar.xz') or
basename.endswith('.tgz') or
ext == '.tar'):
tar_args = ['tar', '-xf', archive_path, '-C', target_path]
if strip_components and strip_components > 0:
tar_args.append(f'--strip-components={strip_components}')
subprocess.run(tar_args, check=True, capture_output=True)
else:
raise Exception(f"Unsupported archive format: {archive_path}")
except subprocess.CalledProcessError as e:
error_output = e.stderr.decode('utf-8') if e.stderr else str(e)
raise Exception(f"Failed to extract archive '{archive_path}': {error_output}")
except Exception as e:
raise Exception(f"Failed to extract archive '{archive_path}': {str(e)}")
+119
View File
@@ -0,0 +1,119 @@
from typing import Any, Optional, Generic, TypeVar, Literal, TypedDict, Union, Dict
from dataclasses import dataclass
from abc import ABC, abstractmethod
import random
import string
from .widget_component import WidgetComponent
from ..constants import SKILL_LOCALE_CONFIG, INTENT_OBJECT
T = TypeVar('T')
UtteranceSender = Literal['leon', 'owner']
class SendUtteranceWidgetEventMethodParams(TypedDict):
from_: UtteranceSender
utterance: str
class RunSkillActionWidgetEventMethodParams(TypedDict):
action_name: str
params: Dict[str, Any]
class SendUtteranceOptions(TypedDict, total=False):
from_: Optional[UtteranceSender]
data: Optional[Dict[str, Any]]
class WidgetEventMethod(TypedDict):
methodName: Literal['send_utterance', 'run_skill_action']
methodParams: Union[
SendUtteranceWidgetEventMethodParams,
RunSkillActionWidgetEventMethodParams
]
@dataclass
class WidgetOptions(Generic[T]):
wrapper_props: dict[str, Any] = None
params: T = 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
else:
self.wrapper_props = None
self.action_name = f"{INTENT_OBJECT['skill_name']}:{INTENT_OBJECT['action_name']}"
self.params = options.params
self.widget = self.__class__.__name__
if options.on_fetch:
self.on_fetch = {
'widgetId': options.on_fetch.get('widget_id'),
'actionName': f"{INTENT_OBJECT['skill_name']}:{options.on_fetch.get('action_name')}"
}
else:
self.on_fetch = None
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:
pass
def send_utterance(self, key: str, options: Optional[Dict[str, Any]] = None) -> WidgetEventMethod:
"""
Indicate the core to send a given utterance
:param key: The key of the content
:param options: The options of the utterance
"""
utterance_content = self.content(key, options.get('data') if options else None)
from_ = options.get('from', 'owner') if options else 'owner'
return WidgetEventMethod(
methodName='send_utterance',
methodParams={
'from': from_,
'utterance': utterance_content
}
)
def run_skill_action(self, action_name: str, params: Dict[str, Any]) -> WidgetEventMethod:
"""
Indicate the core to run a given skill action
:param action_name: The name of the action
:param params: The parameters of the action
"""
return WidgetEventMethod(
methodName='run_skill_action',
methodParams={
'actionName': action_name,
'params': params
}
)
def content(self, key: str, data: Optional[Dict[str, Any]] = None) -> str:
"""
Grab and compute the target content of the widget
:param key: The key of the content
:param data: The data to apply
"""
widget_contents = SKILL_LOCALE_CONFIG.get('widget_contents', {})
if key not in widget_contents:
return 'INVALID'
content = widget_contents[key]
if isinstance(content, list):
content = random.choice(content)
if data:
for k, v in data.items():
content = content.replace(f"{{{{ {key} }}}}", str(v))
return content
@@ -0,0 +1,76 @@
from typing import TypeVar, Generic, TypedDict, List, Any
import random
import string
T = TypeVar('T')
SUPPORTED_WIDGET_EVENTS = [
'onClick',
'onSubmit',
'onChange',
'onStart',
'onEnd'
]
def generate_id() -> str:
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=5))
class WidgetEvent(TypedDict):
type: str
id: str
method: Any
class WidgetComponent(Generic[T]):
def __init__(self, props: T):
self.component = type(self).__name__
self.id = f'{self.component.lower()}-{generate_id()}'
self.props = props
self.events = self.parse_events()
def parse_events(self) -> List[WidgetEvent]:
if not self.props:
return []
event_types = [key for key in self.props if key.startswith('on') and key in SUPPORTED_WIDGET_EVENTS]
return [
WidgetEvent(
type=event_type,
id=f'{self.id}_{event_type.lower()}-{generate_id()}',
method=self.props[event_type]
)
for event_type in event_types
]
def __dict__(self):
children_value = self.props.get('children')
rest_of_values = {key: value for key, value in self.props.items() if key != 'children'
and key not in SUPPORTED_WIDGET_EVENTS}
children = None
if children_value is not None:
if isinstance(children_value, list):
children = []
for child in children_value:
if isinstance(child, WidgetComponent):
children.append(child.__dict__())
else:
children.append(child)
else:
children = children_value
result = {
'component': self.component,
'id': self.id,
'props': {
**rest_of_values,
'children': children
},
'events': [{'type': event['type'], 'id': event['id'], 'method': event['method']} for event in self.events]
}
return result
+1
View File
@@ -0,0 +1 @@
__version__ = '1.4.0'