├── python ├── msa │ ├── __init__.py │ ├── cli │ │ └── __init__.py │ ├── tools │ │ ├── __init__.py │ │ └── msa_rest_client_loader.py │ ├── utils │ │ ├── __init__.py │ │ └── asyncio_utils.py │ ├── builtins │ │ ├── __init__.py │ │ ├── intents │ │ │ ├── __init__.py │ │ │ ├── events.py │ │ │ └── handlers.py │ │ ├── signals │ │ │ ├── __init__.py │ │ │ ├── events.py │ │ │ ├── server_api.py │ │ │ ├── client_api.py │ │ │ └── handlers.py │ │ ├── conversation │ │ │ ├── __init__.py │ │ │ ├── events.py │ │ │ ├── client_api.py │ │ │ ├── server_api.py │ │ │ └── handlers.py │ │ └── scripting │ │ │ ├── __init__.py │ │ │ ├── entities.py │ │ │ └── server_api.py │ ├── plugins │ │ ├── __init__.py │ │ ├── notifications │ │ │ ├── notification_providers.py │ │ │ ├── __init__.py │ │ │ ├── events.py │ │ │ └── handlers.py │ │ └── rss_feed │ │ │ ├── __init__.py │ │ │ ├── events.py │ │ │ └── handlers.py │ ├── version.py │ ├── server │ │ ├── server_request.py │ │ ├── default_routes.py │ │ ├── server_response.py │ │ ├── event_propagate.py │ │ ├── url_param_parser.py │ │ └── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── loader.py │ │ ├── event_handler.py │ │ └── config_manager.py │ ├── api │ │ ├── context.py │ │ ├── patchable_api.py │ │ ├── __init__.py │ │ ├── base_methods.py │ │ └── patcher.py │ ├── data │ │ └── __init__.py │ └── __main__.py ├── tests │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── patchable_api_test.py │ │ └── patcher_test.py │ ├── builtins │ │ ├── __init__.py │ │ ├── intents │ │ │ ├── __init__.py │ │ │ └── handlers_test.py │ │ ├── scripting │ │ │ └── __init__.py │ │ ├── signals │ │ │ └── __init__.py │ │ └── conversation │ │ │ ├── __init__.py │ │ │ └── client_api_test.py │ ├── core │ │ ├── __init__.py │ │ ├── loader_test.py │ │ └── event_handler_test.py │ ├── plugins │ │ ├── __init__.py │ │ └── notifications │ │ │ ├── __init__.py │ │ │ └── handlers_test.py │ ├── server │ │ └── __init__.py │ └── async_test_util.py ├── sphinx │ ├── readme.md │ ├── plugins.md │ ├── update_api_dir.sh │ ├── changelog.md │ ├── api │ │ ├── modules.rst │ │ ├── msa.data.rst │ │ ├── msa.plugins.rst │ │ ├── msa.builtins.rst │ │ ├── msa.cli.rst │ │ ├── msa.utils.rst │ │ ├── msa.tools.rst │ │ ├── msa.rst │ │ ├── msa.builtins.intents.rst │ │ ├── msa.plugins.rss_feed.rst │ │ ├── msa.plugins.notifications.rst │ │ ├── msa.api.rst │ │ ├── msa.builtins.signals.rst │ │ ├── msa.builtins.conversation.rst │ │ ├── msa.core.rst │ │ ├── msa.server.rst │ │ └── msa.builtins.scripting.rst │ ├── modules.rst │ ├── architecture.md │ ├── installation.md │ ├── Makefile │ ├── index.rst │ ├── make.bat │ ├── contributor_guide.md │ ├── getting_started.md │ ├── configuration.md │ └── using_the_cli.md ├── terminal_client │ ├── __init__.py │ └── __main__.py ├── monitor.sh ├── rtd-requirements.txt ├── test_and_coverage.sh ├── msa_config.json ├── pyproject.toml └── things-to-do.txt ├── .valgrindrc ├── obj ├── agent │ └── .scm-include ├── cfg │ └── .scm-include ├── cmd │ └── .scm-include ├── event │ └── .scm-include ├── input │ └── .scm-include ├── log │ └── .scm-include ├── util │ └── .scm-include ├── output │ └── .scm-include ├── platform │ └── .scm-include └── plugin │ └── .scm-include ├── plugins ├── autoload │ └── .scm-include └── example │ ├── Makefile │ └── example.cpp ├── docs ├── architecture │ ├── dia │ │ ├── edt.dia │ │ ├── 03-system-overview.dia │ │ ├── 02-device-layout-single.dia │ │ └── 01-device-layout-multiple.dia │ ├── png │ │ ├── edt.png │ │ ├── 03-system-overview.png │ │ ├── 02-device-layout-single.png │ │ └── 01-device-layout-multiple.png │ ├── rework │ │ ├── user_script_submission.png │ │ ├── system_topology_diagram.png │ │ ├── user_hook_script_submission.png │ │ ├── publisher_subscriber_pattern.png │ │ ├── startup_and_echo_sequence_diagram.png │ │ ├── user_script_submission_interactive.png │ │ ├── system_topology_diagram.mermaid │ │ ├── user_script_submission.mermaid │ │ ├── user_hook_script_submission.mermaid │ │ ├── build_all.sh │ │ ├── user_script_submission_interactive.mermaid │ │ ├── publisher_subscriber_pattern.mermaid │ │ └── startup_and_echo_sequence_diagram.mermaid │ └── states.md └── INSTALL.md ├── compat ├── platform │ ├── lib │ │ ├── unix.hpp │ │ ├── android.hpp │ │ ├── win32.hpp │ │ ├── lib.cpp │ │ ├── win32.cpp │ │ ├── android.cpp │ │ ├── unix.cpp │ │ └── lib.hpp │ ├── thread │ │ ├── thread.cpp │ │ ├── unix.hpp │ │ ├── android.hpp │ │ ├── win32.hpp │ │ └── thread.hpp │ ├── file │ │ ├── file.hpp │ │ ├── unix.cpp │ │ ├── android.cpp │ │ ├── win32.cpp │ │ └── file.cpp │ ├── win32.hpp │ └── unix.hpp ├── compiler │ ├── msvc.hpp │ ├── gcc.hpp │ └── unknown.hpp └── compat.hpp ├── src ├── util │ ├── util.cpp │ ├── util.hpp │ ├── string.hpp │ ├── var.hpp │ └── string.cpp ├── event │ ├── topics.hpp │ ├── hooks.hpp │ ├── dispatch.hpp │ ├── timer_hooks.hpp │ ├── timer.hpp │ ├── handler.hpp │ ├── event.cpp │ ├── event.hpp │ └── handler.cpp ├── cmd │ ├── hooks.hpp │ └── cmd.hpp ├── input │ ├── hooks.hpp │ └── input.hpp ├── debug_macros.hpp ├── plugin │ └── hooks.hpp ├── log │ ├── hooks.hpp │ └── log.hpp ├── agent │ ├── hooks.hpp │ ├── agent.hpp │ └── agent.cpp ├── output │ ├── hooks.hpp │ └── output.hpp ├── main.cpp └── msa.hpp ├── .travis.yml ├── test └── libdl_close_leak.supp ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── scripts └── os_modules.mk ├── msa.cfg ├── .drone.yml ├── .gitignore └── Makefile /python/msa/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/msa/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/msa/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/msa/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/msa/builtins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/msa/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/builtins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/msa/version.py: -------------------------------------------------------------------------------- 1 | v = "0.1.0" 2 | -------------------------------------------------------------------------------- /python/sphinx/readme.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /python/terminal_client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/builtins/intents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/builtins/scripting/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/builtins/signals/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/monitor.sh: -------------------------------------------------------------------------------- 1 | rlwrap nc localhost 50101 2 | -------------------------------------------------------------------------------- /python/tests/builtins/conversation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/plugins/notifications/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.valgrindrc: -------------------------------------------------------------------------------- 1 | --suppressions=test/libdl_close_leak.supp 2 | 3 | -------------------------------------------------------------------------------- /python/rtd-requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | schema 3 | termcolor 4 | colorama 5 | -------------------------------------------------------------------------------- /python/sphinx/plugins.md: -------------------------------------------------------------------------------- 1 | # Extending MSA with Plugins 2 | 3 | **[[STUB]]** -------------------------------------------------------------------------------- /python/sphinx/update_api_dir.sh: -------------------------------------------------------------------------------- 1 | rm -rf api 2 | sphinx-apidoc -o api ../msa 3 | -------------------------------------------------------------------------------- /obj/agent/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/cfg/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/cmd/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/event/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/input/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/log/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/util/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/output/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/platform/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /obj/plugin/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /plugins/autoload/.scm-include: -------------------------------------------------------------------------------- 1 | Keep this file so source control includes this directory 2 | -------------------------------------------------------------------------------- /python/sphinx/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | #### Version 1.0 4 | Initial release. 5 | -------------------------------------------------------------------------------- /python/sphinx/api/modules.rst: -------------------------------------------------------------------------------- 1 | msa 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | msa 8 | -------------------------------------------------------------------------------- /python/sphinx/modules.rst: -------------------------------------------------------------------------------- 1 | msa 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | msa 8 | -------------------------------------------------------------------------------- /docs/architecture/dia/edt.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/dia/edt.dia -------------------------------------------------------------------------------- /docs/architecture/png/edt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/png/edt.png -------------------------------------------------------------------------------- /docs/architecture/dia/03-system-overview.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/dia/03-system-overview.dia -------------------------------------------------------------------------------- /docs/architecture/png/03-system-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/png/03-system-overview.png -------------------------------------------------------------------------------- /docs/architecture/dia/02-device-layout-single.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/dia/02-device-layout-single.dia -------------------------------------------------------------------------------- /docs/architecture/png/02-device-layout-single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/png/02-device-layout-single.png -------------------------------------------------------------------------------- /docs/architecture/dia/01-device-layout-multiple.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/dia/01-device-layout-multiple.dia -------------------------------------------------------------------------------- /docs/architecture/png/01-device-layout-multiple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/png/01-device-layout-multiple.png -------------------------------------------------------------------------------- /docs/architecture/rework/user_script_submission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/rework/user_script_submission.png -------------------------------------------------------------------------------- /docs/architecture/rework/system_topology_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/rework/system_topology_diagram.png -------------------------------------------------------------------------------- /docs/architecture/rework/user_hook_script_submission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/rework/user_hook_script_submission.png -------------------------------------------------------------------------------- /docs/architecture/rework/publisher_subscriber_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/rework/publisher_subscriber_pattern.png -------------------------------------------------------------------------------- /docs/architecture/rework/startup_and_echo_sequence_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/rework/startup_and_echo_sequence_diagram.png -------------------------------------------------------------------------------- /docs/architecture/rework/user_script_submission_interactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moe-serifu-circle/moe-serifu-agent/HEAD/docs/architecture/rework/user_script_submission_interactive.png -------------------------------------------------------------------------------- /python/msa/plugins/notifications/notification_providers.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class NotificationProvider(Enum): 5 | pushbullet = "pushbullet" 6 | email = "email" 7 | slack = "slack" 8 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.data.rst: -------------------------------------------------------------------------------- 1 | msa.data package 2 | ================ 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: msa.data 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /compat/platform/lib/unix.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_LIB_LIB_HPP 2 | #error "Do not include compat libs directly" 3 | #endif 4 | 5 | namespace msa { namespace lib { 6 | 7 | typedef void * Handle; 8 | 9 | } } 10 | 11 | -------------------------------------------------------------------------------- /compat/platform/thread/thread.cpp: -------------------------------------------------------------------------------- 1 | #include "thread.hpp" 2 | 3 | #if defined(__WIN32) 4 | #include "win32.cpp" 5 | #elif defined(__ANDROID__) 6 | #include "android.cpp" 7 | #else 8 | #include "unix.cpp" 9 | #endif 10 | -------------------------------------------------------------------------------- /compat/platform/lib/android.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_LIB_LIB_HPP 2 | #error "Do not include compat libs directly" 3 | #endif 4 | 5 | namespace msa { namespace lib { 6 | 7 | typedef void * Handle; 8 | 9 | } } 10 | 11 | -------------------------------------------------------------------------------- /compat/platform/lib/win32.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_LIB_LIB_HPP 2 | #error "Do not include compat libs directly" 3 | #endif 4 | 5 | #include 6 | 7 | namespace msa { namespace lib { 8 | 9 | typedef HMODULE Handle; 10 | 11 | } } 12 | 13 | -------------------------------------------------------------------------------- /python/sphinx/architecture.md: -------------------------------------------------------------------------------- 1 | # Architectural Overview 2 | 3 | > Note: This section is very techy. If you are not interested or knowledgeable in programming or how the internals of 4 | > the MSA work, this section is likely not for you. 5 | 6 | 7 | **[[STUB]]** 8 | 9 | -------------------------------------------------------------------------------- /python/test_and_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run coverage and unittests 4 | coverage run --omit **/virtualenvs/**,**/tests/** -m unittest discover -p "*_test.py" 5 | 6 | exit_code=$? 7 | 8 | # build html report 9 | coverage html 10 | 11 | exit $exit_code 12 | -------------------------------------------------------------------------------- /python/msa/server/server_request.py: -------------------------------------------------------------------------------- 1 | class SeverRequest: 2 | def __init__(self, source, verb, route, data, url_variables): 3 | self.source = source 4 | self.data = data 5 | self.verb = verb 6 | self.route = route 7 | self.url_variables = url_variables 8 | -------------------------------------------------------------------------------- /python/msa/core/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | supervisor_instance = None 4 | 5 | 6 | def get_supervisor() -> "Supervisor": 7 | if not supervisor_instance: 8 | raise Exception("Supervisor has not yet been set up!") 9 | return supervisor_instance 10 | -------------------------------------------------------------------------------- /python/sphinx/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | > **Note:** Currently the only way to run MSA is from source see: [Running From Source](contributor_guide.html#running-from-source) 4 | ## Windows Installation 5 | 6 | **[STUB]** 7 | 8 | ## Linux/MacOS Installation 9 | 10 | **[STUB]** 11 | -------------------------------------------------------------------------------- /python/msa/plugins/rss_feed/__init__.py: -------------------------------------------------------------------------------- 1 | from schema import Schema, And 2 | from msa.plugins.rss_feed import handlers 3 | 4 | handler_factories = [handlers.RssPollingHandler] 5 | 6 | entities_list = [] 7 | 8 | 9 | config_schema = Schema({"feed_url": And(str, len), "poll_crontab": "* * * * *"}) 10 | -------------------------------------------------------------------------------- /src/util/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util/util.hpp" 2 | 3 | namespace msa { namespace util { 4 | 5 | extern void sleep_milli(int millisec) { 6 | msa::platform::sleep(millisec); 7 | } 8 | 9 | extern bool check_stdin_ready() 10 | { 11 | return msa::platform::select_stdin(); 12 | } 13 | 14 | } } 15 | -------------------------------------------------------------------------------- /python/msa/builtins/intents/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from schema import Schema 5 | from msa.builtins.intents import handlers 6 | 7 | 8 | handler_factories = [handlers.IntentToEventHandler] 9 | 10 | entities_list = [] 11 | 12 | 13 | config_schema = Schema(None) 14 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.plugins.rst: -------------------------------------------------------------------------------- 1 | msa.plugins package 2 | =================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | msa.plugins.notifications 10 | msa.plugins.rss_feed 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: msa.plugins 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /python/msa/api/context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from enum import Enum 4 | 5 | 6 | class ApiContext(Enum): 7 | # use if instead of using a network transport we should call the api implementations 8 | # directly 9 | local = 0 10 | 11 | # use for rest API 12 | rest = 1 13 | 14 | # use for websocket API 15 | websocket = 2 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | 3 | branches: 4 | only: 5 | - master 6 | - dev 7 | 8 | before_install: 9 | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 10 | - sudo apt-get update -qq 11 | 12 | install: 13 | - sudo apt-get install -qq g++-4.8 14 | - export CXX="g++-4.8" 15 | 16 | # try to build the normal bin and the testing bin 17 | script: make && make clean && make debug -------------------------------------------------------------------------------- /python/msa/plugins/rss_feed/events.py: -------------------------------------------------------------------------------- 1 | from msa.core.event import Event 2 | from schema import Schema 3 | 4 | 5 | class RssFeedRequestEvent(Event): 6 | """ 7 | RssFeedRequestEvent schema: 8 | - timestamp: datetime in yyyy-mm-dd hh:mm:ss:xx format of starup event 9 | """ 10 | 11 | def __init__(self): 12 | super().__init__(priority=40, schema=Schema(None)) 13 | -------------------------------------------------------------------------------- /src/event/topics.hpp: -------------------------------------------------------------------------------- 1 | // List for using with X-Macro style of maintaining event topics 2 | 3 | // Syntax is MSA_EVENT_TOPIC(name, priority) 4 | // Greater priority topics interrupt event handling of lower-numbered 5 | // priority topics. 6 | 7 | MSA_EVENT_TOPIC(EVENT_STACK_CLEARED, 0) 8 | MSA_EVENT_TOPIC(EVENT_HANDLED, 0) 9 | MSA_EVENT_TOPIC(EVENT_INTERRUPTED, 0) 10 | MSA_EVENT_TOPIC(TEXT_INPUT, 10) 11 | -------------------------------------------------------------------------------- /python/tests/async_test_util.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest import mock 3 | 4 | 5 | def async_run(loop, coro): 6 | return loop.run_until_complete(coro) 7 | 8 | 9 | def AsyncMock(*args, **kwargs): 10 | m = mock.MagicMock(*args, **kwargs) 11 | 12 | async def mock_coro(*args, **kwargs): 13 | return m(*args, **kwargs) 14 | 15 | mock_coro.mock = m 16 | return mock_coro 17 | -------------------------------------------------------------------------------- /docs/architecture/rework/system_topology_diagram.mermaid: -------------------------------------------------------------------------------- 1 | graph TD; 2 | subgraph Computer #1 3 | u[User] --> cli[CLI Tool] 4 | end 5 | subgraph Computer #2 6 | u2[Other user] --> c[Front End Client] 7 | end 8 | 9 | subgraph network interaction 10 | c[Front End Client] --> api[MSA API] 11 | cli --> api 12 | end 13 | 14 | subgraph Computer #3 15 | api --> s[MSA Daemon] 16 | end 17 | 18 | 19 | -------------------------------------------------------------------------------- /python/msa/api/patchable_api.py: -------------------------------------------------------------------------------- 1 | # _!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class MsaApi(dict): 6 | def __init__(self, *args, **kwargs): 7 | super(MsaApi, self).__init__(*args, **kwargs) 8 | self.__dict__ = self 9 | 10 | def __str__(self): 11 | return ( 12 | f"<{self.__class__.__module__}.{self.__class__.__name__} at {hex(id(self))}" 13 | ) 14 | -------------------------------------------------------------------------------- /compat/compiler/msvc.hpp: -------------------------------------------------------------------------------- 1 | // DO NOT INCLUDE THIS FILE DIRECTLY 2 | #ifndef COMPAT_COMPAT_HPP 3 | #error "do not include compatibility files directly; include compat/compat.hpp instead" 4 | #endif 5 | 6 | // UNUSED(x) surround a parameter with this to mark it as not used 7 | #ifdef UNUSED 8 | #error "UNUSED is already def'd, need a new name here" 9 | #else 10 | #define UNUSED(x) __pragma(warning(suppress:4100)) x 11 | #endif -------------------------------------------------------------------------------- /python/sphinx/api/msa.builtins.rst: -------------------------------------------------------------------------------- 1 | msa.builtins package 2 | ==================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | msa.builtins.conversation 10 | msa.builtins.intents 11 | msa.builtins.scripting 12 | msa.builtins.signals 13 | 14 | Module contents 15 | --------------- 16 | 17 | .. automodule:: msa.builtins 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | -------------------------------------------------------------------------------- /compat/compiler/gcc.hpp: -------------------------------------------------------------------------------- 1 | // DO NOT INCLUDE THIS FILE DIRECTLY 2 | #ifndef COMPAT_COMPAT_HPP 3 | #error "do not include compatibility files directly; include compat/compat.hpp instead" 4 | #endif 5 | 6 | // UNUSED(x) surround a parameter with this to mark it as not used 7 | #ifdef UNUSED 8 | #error "UNUSED is already def'd, need a new name here" 9 | #else 10 | #define UNUSED(x) UNUSED_ ## x __attribute__((unused)) 11 | #endif 12 | -------------------------------------------------------------------------------- /python/msa/data/__init__.py: -------------------------------------------------------------------------------- 1 | from tortoise import Tortoise 2 | 3 | from tortoise.backends.base.config_generator import generate_config 4 | 5 | __models__ = [] 6 | 7 | 8 | async def start_db_engine(): 9 | await Tortoise.init(db_url="sqlite://./msa.db", modules={"models": ["msa.data"]}) 10 | await Tortoise.generate_schemas(safe=True) 11 | 12 | 13 | async def stop_db_engine(_): 14 | await Tortoise.close_connections() 15 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.cli.rst: -------------------------------------------------------------------------------- 1 | msa.cli package 2 | =============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.cli.interpreter module 8 | -------------------------- 9 | 10 | .. automodule:: msa.cli.interpreter 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: msa.cli 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.utils.rst: -------------------------------------------------------------------------------- 1 | msa.utils package 2 | ================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.utils.asyncio\_utils module 8 | ------------------------------- 9 | 10 | .. automodule:: msa.utils.asyncio_utils 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: msa.utils 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /compat/platform/thread/unix.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_THREAD_THREAD_HPP 2 | #error "Do not include compat libs directly" 3 | #endif 4 | 5 | #include 6 | 7 | namespace msa { namespace thread { 8 | 9 | typedef pthread_t Thread; 10 | typedef pthread_attr_t Attributes; 11 | typedef pthread_mutex_t Mutex; 12 | typedef pthread_mutexattr_t MutexAttributes; 13 | typedef pthread_cond_t Cond; 14 | typedef pthread_condattr_t CondAttributes; 15 | 16 | } } 17 | -------------------------------------------------------------------------------- /compat/platform/thread/android.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_THREAD_THREAD_HPP 2 | #error "Do not include compat libs directly" 3 | #endif 4 | 5 | #include 6 | 7 | namespace msa { namespace thread { 8 | 9 | typedef pthread_t Thread; 10 | typedef pthread_attr_t Attributes; 11 | typedef pthread_mutex_t Mutex; 12 | typedef pthread_mutexattr_t MutexAttributes; 13 | typedef pthread_cond_t Cond; 14 | typedef pthread_condattr_t CondAttributes; 15 | 16 | } } 17 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.tools.rst: -------------------------------------------------------------------------------- 1 | msa.tools package 2 | ================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.tools.msa\_rest\_client\_loader module 8 | ------------------------------------------ 9 | 10 | .. automodule:: msa.tools.msa_rest_client_loader 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: msa.tools 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/architecture/rework/user_script_submission.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant User 3 | participant cli as MSA CLI Tool 4 | participant daemon as MSA Daemon 5 | 6 | 7 | User->>User: User Writes script 8 | User->>daemon: Starts daemon 9 | User->>cli: Submits script to CLI 10 | cli->>daemon: Connect 11 | cli->>daemon: Upload Script and schedule 12 | loop Scheduling Loop 13 | daemon->>daemon: Time passes 14 | daemon->>daemon: MSA Daemon runs submitted script at scheduled time 15 | end 16 | 17 | -------------------------------------------------------------------------------- /docs/architecture/rework/user_hook_script_submission.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant user as User 3 | participant cli as MSA CLI Tool REPL 4 | participant daemon as MSA Daemon 5 | 6 | user->>user:Write hook script 7 | user->>daemon: Starts daemon 8 | user->>cli: Upload hook via cli 9 | cli->>daemon: Connect 10 | 11 | cli->>daemon: Upload hook script 12 | loop Scheduling Loop 13 | daemon->>daemon: Time passes 14 | daemon->>daemon: run script when event of type (according to configured hook) fires 15 | end 16 | 17 | 18 | -------------------------------------------------------------------------------- /python/msa/builtins/signals/__init__.py: -------------------------------------------------------------------------------- 1 | from schema import Schema 2 | from msa.builtins.signals import handlers 3 | from msa.builtins.signals import server_api 4 | from msa.builtins.signals import client_api 5 | 6 | handler_factories = [ 7 | handlers.StartupEventTrigger, 8 | handlers.NetworkPropagateEventHandler, 9 | ] 10 | 11 | 12 | # entity_setup = None 13 | 14 | entities_list = [] 15 | 16 | 17 | register_client_api = client_api.register_endpoints 18 | register_server_api = server_api.register_routes 19 | 20 | config_schema = Schema(None) 21 | -------------------------------------------------------------------------------- /compat/platform/thread/win32.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_THREAD_THREAD_HPP 2 | #error "Do not include compat libs directly" 3 | #endif 4 | 5 | // this file is for win32 specific thread definitions 6 | 7 | extern "C" { 8 | #include 9 | } 10 | 11 | namespace msa { namespace thread { 12 | 13 | typedef DWORD Thread; 14 | typedef struct attr_type Attributes; 15 | typedef struct mutex_type Mutex; 16 | typedef struct mutex_attr_type MutexAttributes; 17 | typedef struct cond_type Cond; 18 | typedef struct cond_attr_type CondAttributes; 19 | 20 | } } 21 | -------------------------------------------------------------------------------- /python/msa_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "agent": { 3 | "name": "Masa-chan", 4 | "user_title": "Onee-chan" 5 | }, 6 | "plugin_modules": [ 7 | "rss_feed" 8 | ], 9 | 10 | "module_config": { 11 | "rss_feed": { 12 | "feed_url": "http://feeds.feedburner.com/crunchyroll/animenews", 13 | "poll_crontab": "* * * * *" 14 | } 15 | }, 16 | 17 | "logging": { 18 | "global_log_level": "debug", 19 | "log_file_location": "msa.log", 20 | "truncate_log_file": false, 21 | "granular_log_levels": [ 22 | 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/libdl_close_leak.supp: -------------------------------------------------------------------------------- 1 | # The implementation of libdl used by Valgrind introduces a memory leak in 2 | # programs that are compiled with -lpthread when dlclose() is called from a 3 | # different thread than the one that called dlopen(). 4 | # 5 | # This has been observed in v3.10 of Valgrind on Debian and v3.11 of Valgrind 6 | # on Ubuntu. 7 | # 8 | # This suppression file keeps that leak from the output of our test program. 9 | 10 | { 11 | Suppress Valgrind's multithreaded leak in dlopen/dlclose 12 | Memcheck:Leak 13 | fun:calloc 14 | fun:_dlerror_run 15 | ... 16 | } 17 | -------------------------------------------------------------------------------- /compat/compiler/unknown.hpp: -------------------------------------------------------------------------------- 1 | // DO NOT INCLUDE THIS FILE DIRECTLY 2 | #ifndef COMPAT_COMPAT_HPP 3 | #error "do not include compatibility files directly; include compat/compat.hpp instead" 4 | #endif 5 | 6 | // This file is included when the current compiler is not one of the detectable types. 7 | // It provides defaults for the compiler-specific macros declared in the other files. 8 | 9 | // UNUSED(x) surround a parameter with this to mark it as not used 10 | #ifdef UNUSED 11 | #error "UNUSED is already def'd, need a new name here" 12 | #else 13 | #define UNUSED(x) x 14 | #endif 15 | -------------------------------------------------------------------------------- /python/msa/server/default_routes.py: -------------------------------------------------------------------------------- 1 | from msa.version import v as msa_version 2 | 3 | from msa.server.server_response import ServerResponseText, ServerResponseType 4 | 5 | 6 | def register_default_routes(route_adapter): 7 | @route_adapter.get("/ping") 8 | async def ping_handler(request=None, raw_data=None): 9 | return ServerResponseText(ServerResponseType.success, text="pong") 10 | 11 | @route_adapter.get("/version") 12 | async def version_handler(request=None, raw_data=None): 13 | return ServerResponseText(ServerResponseType.success, text=msa_version) 14 | -------------------------------------------------------------------------------- /compat/platform/file/file.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_FILE_FILE_HPP 2 | #define COMPAT_PLATFORM_FILE_FILE_HPP 3 | 4 | #include 5 | #include 6 | 7 | // functions for manipulating the filesystem in a cross-platform way 8 | 9 | namespace msa { namespace file { 10 | 11 | extern void list(const std::string &dir_path, std::vector &files); 12 | extern const std::string &dir_separator(); 13 | extern void join(std::string &base, const std::string &next); 14 | extern void basename(std::string &path, const std::string &suffix = ""); 15 | 16 | } } 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /compat/platform/win32.hpp: -------------------------------------------------------------------------------- 1 | // DO NOT INCLUDE THIS FILE DIRECTLY 2 | #ifndef COMPAT_COMPAT_HPP 3 | #error "do not include compatibility files directly; include compat/compat.hpp instead" 4 | #endif 5 | 6 | extern "C" { 7 | #include 8 | } 9 | 10 | namespace msa { namespace platform { 11 | 12 | static inline void sleep(int millisec) 13 | { 14 | Sleep(millisec); 15 | } 16 | 17 | static inline bool select_stdin() 18 | { 19 | HANDLE stdin = GetStandardHandle(STD_INPUT_HANDLE); 20 | return (WaitForSingleObject(stdin, 0) == WAIT_OBJECT_0); 21 | } 22 | 23 | } } 24 | 25 | -------------------------------------------------------------------------------- /python/msa/builtins/conversation/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from schema import Schema 5 | from msa.builtins.conversation import handlers 6 | 7 | # from msa.builtins.conversation import entities 8 | from msa.builtins.conversation import server_api 9 | from msa.builtins.conversation import client_api 10 | 11 | handler_factories = [handlers.ConversationInputEventHandler] 12 | 13 | entities_list = [] 14 | 15 | register_client_api = client_api.register_endpoints 16 | register_server_api = server_api.register_routes 17 | 18 | 19 | config_schema = Schema(None) 20 | -------------------------------------------------------------------------------- /docs/architecture/rework/build_all.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | rm *.png 4 | 5 | 6 | function build_diagram(){ 7 | echo "Building $1.png" 8 | 9 | if [[ $2 != "" && $3 != "" ]]; then 10 | mmdc -i $1.mermaid -o $1.png -w $2 -H $3 11 | else 12 | mmdc -i $1.mermaid -o $1.png 13 | fi 14 | } 15 | 16 | build_diagram startup_and_echo_sequence_diagram 1568 3184 17 | build_diagram user_script_submission 18 | build_diagram user_script_submission_interactive 19 | build_diagram user_hook_script_submission 20 | build_diagram system_topology_diagram 21 | 22 | 23 | build_diagram publisher_subscriber_pattern 24 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.rst: -------------------------------------------------------------------------------- 1 | msa package 2 | =========== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | msa.api 10 | msa.builtins 11 | msa.cli 12 | msa.core 13 | msa.data 14 | msa.plugins 15 | msa.server 16 | msa.tools 17 | msa.utils 18 | 19 | Submodules 20 | ---------- 21 | 22 | msa.version module 23 | ------------------ 24 | 25 | .. automodule:: msa.version 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | 31 | Module contents 32 | --------------- 33 | 34 | .. automodule:: msa 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | -------------------------------------------------------------------------------- /compat/platform/lib/lib.cpp: -------------------------------------------------------------------------------- 1 | #include "lib.hpp" 2 | 3 | namespace msa { namespace lib { 4 | 5 | struct library_type 6 | { 7 | Handle handle; 8 | std::string name; 9 | std::string path; 10 | }; 11 | 12 | library_error::library_error(const std::string &lib_name, const std::string &what) : 13 | std::runtime_error(what), 14 | lib_name(lib_name) 15 | {} 16 | 17 | const std::string &library_error::name() const 18 | { 19 | return lib_name; 20 | } 21 | 22 | } } 23 | 24 | #if defined(__WIN32) 25 | #include "win32.cpp" 26 | #elif defined(__ANDROID__) 27 | #include "android.cpp" 28 | #else 29 | #include "unix.cpp" 30 | #endif 31 | -------------------------------------------------------------------------------- /python/msa/builtins/intents/events.py: -------------------------------------------------------------------------------- 1 | from schema import Schema, And, Or, Optional 2 | from msa.core.event import Event 3 | 4 | 5 | class IntentEvent(Event): 6 | """ 7 | IntentEvent schema: 8 | - type: The type of the intent. This should be the full namespace path of the event that should be created if the 9 | appropriate plugin is loaded. 10 | - context: The data that the new event should be initialized with. 11 | """ 12 | 13 | def __init__(self): 14 | super().__init__( 15 | priority=50, 16 | schema=Schema({"type": And(str, len), Optional("context"): Or(dict, None)}), 17 | ) 18 | -------------------------------------------------------------------------------- /src/cmd/hooks.hpp: -------------------------------------------------------------------------------- 1 | // Functions in the cmd module that dynamically-loaded modules (plugins) are allowed to call. 2 | 3 | // Define the MSA_MODULE_HOOK macro to arrange the hooks as needed, and then include this file. 4 | // MSA_MODULE_HOOK will declare hooks with the following arguments: 5 | // MSA_MODULE_HOOK(return-spec, func-name, ...) where ... is the arguments of the hook 6 | // This file should only be included from cmd module code. 7 | 8 | #ifndef MSA_MODULE_HOOK 9 | #error "cannot include cmd hooks before MSA_MODULE_HOOK macro is defined" 10 | #endif 11 | 12 | MSA_MODULE_HOOK(void, get_commands, msa::Handle hdl, std::vector &list) 13 | 14 | -------------------------------------------------------------------------------- /src/util/util.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_UTIL_UTIL_HPP 2 | #define MSA_UTIL_UTIL_HPP 3 | 4 | /** 5 | * util.hpp 6 | * 7 | * Contains platform-independent utility functions that can see cross-module 8 | * usage but don't have enough coupled functionality to warrant their own module. 9 | * 10 | * This file (and it's .cpp file) shall not include any other MSA module; if there 11 | * is a temptation to add such a dependency, it may be better to move the function 12 | * that is causing the dependency into the module that it depends on. 13 | */ 14 | 15 | namespace msa { namespace util { 16 | 17 | extern void sleep_milli(int millisec); 18 | extern bool check_stdin_ready(); 19 | 20 | } } 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /compat/compat.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_COMPAT_HPP 2 | #define COMPAT_COMPAT_HPP 3 | 4 | // Provides a unified interface for platform and compiler specific-functions. 5 | 6 | 7 | // COMPILER 8 | 9 | #if defined(_MSC_VER) 10 | #include "compiler/msvc.hpp" 11 | #elif defined(__GNUC__) 12 | #include "compiler/gcc.hpp" 13 | #else 14 | #include "compiler/unknown.hpp" 15 | #endif 16 | 17 | 18 | // PLATFORM 19 | 20 | #if defined(_WIN32) || defined(_WIN32_) || defined(WIN32) 21 | #include "platform/win32.hpp" 22 | #elif defined(__ANDROID__) 23 | #include "platform/android.hpp" 24 | #else 25 | // assume it's unix, or at least something posix-ish 26 | #include "platform/unix.hpp" 27 | #endif 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.builtins.intents.rst: -------------------------------------------------------------------------------- 1 | msa.builtins.intents package 2 | ============================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.builtins.intents.events module 8 | ---------------------------------- 9 | 10 | .. automodule:: msa.builtins.intents.events 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.builtins.intents.handlers module 16 | ------------------------------------ 17 | 18 | .. automodule:: msa.builtins.intents.handlers 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: msa.builtins.intents 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.plugins.rss_feed.rst: -------------------------------------------------------------------------------- 1 | msa.plugins.rss\_feed package 2 | ============================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.plugins.rss\_feed.events module 8 | ----------------------------------- 9 | 10 | .. automodule:: msa.plugins.rss_feed.events 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.plugins.rss\_feed.handlers module 16 | ------------------------------------- 17 | 18 | .. automodule:: msa.plugins.rss_feed.handlers 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: msa.plugins.rss_feed 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /scripts/os_modules.mk: -------------------------------------------------------------------------------- 1 | ################################################### 2 | # Recipes auto-generated by gendeps.py script. # 3 | # These recipes should not be edited by hand; any # 4 | # changes will be overwritten. # 5 | ################################################### 6 | 7 | $(ODIR)/platform/thread.o: $(OS_SDIR)/platform/thread/thread.cpp 8 | $(CXX) -c -o $@ $(OS_SDIR)/platform/thread/thread.cpp $(CXXFLAGS) 9 | 10 | $(ODIR)/platform/file.o: $(OS_SDIR)/platform/file/file.cpp 11 | $(CXX) -c -o $@ $(OS_SDIR)/platform/file/file.cpp $(CXXFLAGS) 12 | 13 | $(ODIR)/platform/lib.o: $(OS_SDIR)/platform/lib/lib.cpp $(OS_SDIR)/platform/file/file.hpp 14 | $(CXX) -c -o $@ $(OS_SDIR)/platform/lib/lib.cpp $(CXXFLAGS) 15 | 16 | -------------------------------------------------------------------------------- /src/input/hooks.hpp: -------------------------------------------------------------------------------- 1 | // Functions in the input module that dynamically-loaded modules (plugins) are allowed to call. 2 | 3 | // Define the MSA_MODULE_HOOK macro to arrange the hooks as needed, and then include this file. 4 | // MSA_MODULE_HOOK will declare hooks with the following arguments: 5 | // MSA_MODULE_HOOK(return-spec, func-name, ...) where ... is the arguments of the hook 6 | // This file should only be included from input module code. 7 | 8 | #ifndef MSA_MODULE_HOOK 9 | #error "cannot include input hooks before MSA_MODULE_HOOK macro is defined" 10 | #endif 11 | 12 | MSA_MODULE_HOOK(void, enable_device, msa::Handle hdl, const std::string &id) 13 | MSA_MODULE_HOOK(void, disable_device, msa::Handle hdl, const std::string &id) 14 | 15 | -------------------------------------------------------------------------------- /python/sphinx/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | api_dir: 15 | rm -rf api 16 | sphinx-apidoc -o api ../msa 17 | 18 | .PHONY: help Makefile 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /msa.cfg: -------------------------------------------------------------------------------- 1 | # The main config file for the Moe Serifu Agent project 2 | 3 | [log] 4 | global_level = trace 5 | 6 | level = trace 7 | type = file 8 | format = text 9 | output = "[%1$-25s] %2$-15s: %3$-5s %4$s" 10 | location = msa.log 11 | open_mode = overwrite 12 | 13 | [event] 14 | idle_sleep_time = 1 15 | 16 | # tick resolution is in milliseconds, and is not guaranteed 17 | tick_resolution = 10 18 | 19 | [agent] 20 | name = Masa-chan 21 | user_title = Onee-chan 22 | 23 | [input] 24 | type = TTY 25 | id = stdin 26 | handler = get_tty_input 27 | 28 | [output] 29 | type = TTY 30 | id = STDOUT 31 | handler = print_to_stdout 32 | 33 | [command] 34 | startup = "echo Hi there, $USER_TITLE! I am at your command." 35 | 36 | [plugin] 37 | dir = plugins/autoload 38 | 39 | -------------------------------------------------------------------------------- /python/tests/api/patchable_api_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import unittest 6 | 7 | from msa.api.patchable_api import MsaApi 8 | 9 | 10 | class PatchableApiTest(unittest.TestCase): 11 | def test_str(self): 12 | api = MsaApi() 13 | api_str = str(api) 14 | 15 | assert MsaApi.__name__ in api_str 16 | 17 | def test_patching_methods_onto_api(self): 18 | def new_method(): 19 | return "patched_method_result" 20 | 21 | api = MsaApi() 22 | 23 | api.new_method = new_method 24 | 25 | assert getattr(api, "new_method", None) is not None 26 | assert callable(api.new_method) 27 | 28 | assert api.new_method() == "patched_method_result" 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report bugs and other broken behavior 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment:** 27 | - OS: [e.g. iOS] 28 | - Version: [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /compat/platform/file/unix.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace msa { namespace file { 8 | 9 | static const std::string DIR_SEPARATOR = "/"; 10 | 11 | extern void list(const std::string &dir_path, std::vector &files) 12 | { 13 | DIR *d = opendir(dir_path.c_str()); 14 | if (d == NULL) 15 | { 16 | throw std::logic_error("could not open dir (" + std::to_string(errno) + "): " + dir_path); 17 | } 18 | struct dirent *entry; 19 | while ((entry = readdir(d)) != NULL) 20 | { 21 | files.push_back(std::string(entry->d_name)); 22 | } 23 | closedir(d); 24 | } 25 | 26 | extern const std::string &dir_separator() 27 | { 28 | return DIR_SEPARATOR; 29 | } 30 | 31 | } } 32 | -------------------------------------------------------------------------------- /compat/platform/file/android.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace msa { namespace file { 8 | 9 | static const std::string DIR_SEPARATOR = "/"; 10 | 11 | extern void list(const std::string &dir_path, std::vector &files) 12 | { 13 | DIR *d = opendir(dir_path.c_str()); 14 | if (d == NULL) 15 | { 16 | throw std::logic_error("could not open dir (" + std::to_string(errno) + "): " + dir_path); 17 | } 18 | struct dirent *entry; 19 | while ((entry = readdir(d)) != NULL) 20 | { 21 | files.push_back(std::string(entry->d_name)); 22 | } 23 | closedir(d); 24 | } 25 | 26 | extern const std::string &dir_separator() 27 | { 28 | return DIR_SEPARATOR; 29 | } 30 | 31 | } } 32 | 33 | -------------------------------------------------------------------------------- /python/msa/builtins/scripting/__init__.py: -------------------------------------------------------------------------------- 1 | from schema import Schema 2 | from msa.builtins.scripting import handlers 3 | from msa.builtins.scripting import entities 4 | from msa.builtins.scripting import server_api 5 | from msa.builtins.scripting import client_api 6 | 7 | handler_factories = [ 8 | handlers.AddScriptHandler, 9 | handlers.StartupEventHandler, 10 | handlers.TriggerScriptRunHandler, 11 | handlers.TriggerScriptListHandler, 12 | handlers.TriggerGetScriptHandler, 13 | handlers.TriggerDeleteScriptHandler, 14 | ] 15 | 16 | 17 | entities_list = [entities.ScriptEntity, entities.ScriptRunResultEntity] 18 | 19 | register_client_api = client_api.register_endpoints 20 | register_server_api = server_api.register_routes 21 | 22 | config_schema = Schema(None) 23 | -------------------------------------------------------------------------------- /python/msa/builtins/conversation/events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from schema import Schema, And 4 | from msa.core.event import Event 5 | 6 | 7 | class ConversationInputEvent(Event): 8 | """ 9 | ConversationInputEvent schema: 10 | - input: A conversational statement or question to the agent. 11 | """ 12 | 13 | def __init__(self): 14 | super().__init__(priority=50, schema=Schema({"input": And(str, len)})) 15 | 16 | 17 | class ConversationOutputEvent(Event): 18 | """ 19 | ConversationOutputEvent schema: 20 | - output: A conversational response to a statement or question given to the agent. 21 | """ 22 | 23 | def __init__(self): 24 | super().__init__(priority=50, schema=Schema({"output": And(str, len)})) 25 | -------------------------------------------------------------------------------- /python/sphinx/index.rst: -------------------------------------------------------------------------------- 1 | .. Moe Serifu Agent documentation master file, created by 2 | sphinx-quickstart on Tue Dec 4 10:15:37 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | 8 | Welcome to Moe Serifu Agent's documentation! 9 | ============================================ 10 | 11 | 12 | Contents 13 | -------- 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | readme 18 | installation 19 | getting_started 20 | configuration 21 | using_the_cli 22 | plugins 23 | architecture 24 | contributor_guide 25 | changelog 26 | api/msa 27 | 28 | 29 | 30 | 31 | 32 | Indices and tables 33 | ================== 34 | 35 | * :ref:`genindex` 36 | * :ref:`modindex` 37 | * :ref:`search` 38 | -------------------------------------------------------------------------------- /python/msa/utils/asyncio_utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | def sync_to_async(func): 5 | async def wrap_async(*args, **kwargs): 6 | loop = asyncio.get_running_loop() 7 | 8 | def func_with_args(): 9 | func(*args, **kwargs) 10 | 11 | return await loop.run_in_executor(None, func_with_args) 12 | 13 | return wrap_async 14 | 15 | 16 | @sync_to_async 17 | def async_read(file_name, mode): 18 | with open(file_name, mode) as f: 19 | return f.read() 20 | 21 | 22 | def run_async(coroutine): 23 | loop = asyncio.get_event_loop() 24 | if loop.is_running(): 25 | raise Exception( 26 | "Asyncio event loop cannot be running in order to use run_async helper function." 27 | ) 28 | return loop.run_until_complete(coroutine) 29 | -------------------------------------------------------------------------------- /python/msa/server/server_response.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ServerResponseType(Enum): 5 | success = "success" 6 | failure = "failure" 7 | 8 | 9 | class ServerResponse: 10 | def __init__(self, response_status: ServerResponseType): 11 | self.data = {"status": response_status.value} 12 | 13 | def get_data(self): 14 | return self.data 15 | 16 | 17 | class ServerResponseText(ServerResponse): 18 | def __init__(self, response_status: ServerResponseType, text): 19 | super().__init__(response_status) 20 | self.data["text"] = text 21 | 22 | 23 | class ServerResponseJson(ServerResponse): 24 | def __init__(self, response_status: ServerResponseType, payload): 25 | super().__init__(response_status) 26 | self.data["payload"] = payload 27 | -------------------------------------------------------------------------------- /src/event/hooks.hpp: -------------------------------------------------------------------------------- 1 | // Functions in the event module that dynamically-loaded modules (plugins) are allowed to call. 2 | 3 | // Define the MSA_MODULE_HOOK macro to arrange the hooks as needed, and then include this file. 4 | // MSA_MODULE_HOOK will declare hooks with the following arguments: 5 | // MSA_MODULE_HOOK(return-spec, func-name, ...) where ... is the arguments of the hook 6 | // This file should only be included from event module code. 7 | 8 | #ifndef MSA_MODULE_HOOK 9 | #error "cannot include event hooks before MSA_MODULE_HOOK macro is defined" 10 | #endif 11 | 12 | MSA_MODULE_HOOK(void, subscribe, msa::Handle msa, Topic topic, EventHandler handler) 13 | MSA_MODULE_HOOK(void, unsubscribe, msa::Handle msa, Topic topic, EventHandler handler) 14 | MSA_MODULE_HOOK(void, generate, msa::Handle msa, const Topic topic, const IArgs &args) 15 | -------------------------------------------------------------------------------- /python/msa/builtins/conversation/client_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | async def talk(self, input): 6 | """ 7 | Interact with the conversation module builtin to MSA. Expects a natural language message, MSA should respond in kind. 8 | 9 | :async: 10 | :param input: A natural language message to send to MSA. 11 | :return: `None` 12 | """ 13 | response = await self.client.post("/conversation/talk", payload={"input": input}) 14 | 15 | if response.status == "failed": 16 | raise Exception("Server Error: \n" + response.text) 17 | 18 | response = response.text 19 | 20 | if response is None: 21 | print("It seems there was an issue.") 22 | else: 23 | print(response) 24 | 25 | 26 | def register_endpoints(api_binder): 27 | api_binder.register_method()(talk) 28 | -------------------------------------------------------------------------------- /compat/platform/file/win32.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | namespace msa { namespace file { 7 | 8 | static const std::string DIR_SEPARATOR = "\\"; 9 | 10 | extern void list(const std::string &dir_path, std::vector &files) 11 | { 12 | std::string scan_criteria = dir_path; 13 | join(scan_criteria, "*.*"); 14 | WIN32_FIND_DATA find_data; 15 | HANDLE find_handle = FindFirstFile(scan_criteria.c_str(), &find_data); 16 | if (find_handle == INVALID_HANDLE_VALUE) 17 | { 18 | throw std::logic_error("could not open dir: " + dir_path); 19 | } 20 | do 21 | { 22 | files.push_back(find_data.cFileName); 23 | } while(FindNextFile(find_handle, &find_data)); 24 | FindClose(find_handle); 25 | } 26 | 27 | extern const std::string &dir_separator() 28 | { 29 | return DIR_SEPARATOR; 30 | } 31 | 32 | } } 33 | -------------------------------------------------------------------------------- /python/msa/builtins/signals/events.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from schema import Schema, And, Or 3 | 4 | from msa.core.event import Event 5 | 6 | 7 | class StartupEvent(Event): 8 | """ 9 | StartEvent schema: 10 | - timestamp: datetime in yyyy-mm-dd hh:mm:ss:xx format of starup event 11 | """ 12 | 13 | def __init__(self): 14 | super().__init__(priority=0, schema=Schema({"timestamp": And(str, len)})) 15 | 16 | 17 | class RequestDisburseEventsToNetworkEvent(Event): 18 | """ 19 | RequestDisburseEventsToClientEvent schema: 20 | """ 21 | 22 | def __init__(self): 23 | super().__init__(priority=0, schema=Schema({})) 24 | 25 | 26 | class DisburseEventsToNetworkEvent(Event): 27 | """ 28 | DisburseEventsToNetworkEvent schema: 29 | """ 30 | 31 | def __init__(self): 32 | super().__init__(priority=0, schema=Schema({"events": [dict]})) 33 | -------------------------------------------------------------------------------- /src/event/dispatch.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_EVENT_DISPATCH_HPP 2 | #define MSA_EVENT_DISPATCH_HPP 3 | 4 | #include "msa.hpp" 5 | #include "event/handler.hpp" 6 | #include "event/timer.hpp" 7 | #include "cfg/cfg.hpp" 8 | 9 | namespace msa { namespace event { 10 | 11 | extern int init(msa::Handle msa, const msa::cfg::Section &config); 12 | extern int quit(msa::Handle msa); 13 | extern int setup(msa::Handle hdl); 14 | extern int teardown(msa::Handle hdl); 15 | extern const PluginHooks *get_plugin_hooks(); 16 | 17 | #define MSA_MODULE_HOOK(retspec, name, ...) extern retspec name(__VA_ARGS__); 18 | #include "event/hooks.hpp" 19 | #undef MSA_MODULE_HOOK 20 | 21 | struct plugin_hooks_type 22 | { 23 | #define MSA_MODULE_HOOK(retspec, name, ...) retspec (*name)(__VA_ARGS__); 24 | #include "event/hooks.hpp" 25 | #undef MSA_MODULE_HOOK 26 | TimerHooks timer; 27 | }; 28 | } } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /src/debug_macros.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_DEBUG_MACROS_HPP 2 | #define MSA_DEBUG_MACROS_HPP 3 | 4 | #include 5 | 6 | // Contains application-scope preprocessor macros for debugging. 7 | 8 | // Before adding anything here, consider carefully whether it would make a better candidate for a 9 | // msa::util function. It should only be defined as a macro here if it would involve code erasure 10 | // in some conditions, and those conditions are not compiler-specific and are not platform-specific. 11 | 12 | #ifdef DEBUG 13 | #define DEBUG_TEST_VAR 1 14 | #else 15 | #define DEBUG_TEST_VAR 0 16 | #endif 17 | 18 | // MACROS 19 | 20 | // Use this function like printf; it will print the msg 21 | // to stderr 22 | #define DEBUG_PRINTF(...) \ 23 | do { \ 24 | if (DEBUG_TEST_VAR) \ 25 | { \ 26 | fprintf(stderr, __VA_ARGS__); \ 27 | } \ 28 | } while (0) 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /src/plugin/hooks.hpp: -------------------------------------------------------------------------------- 1 | // Functions in the plugin module that dynamically-loaded modules (plugins) are allowed to call. 2 | 3 | // Define the MSA_MODULE_HOOK macro to arrange the hooks as needed, and then include this file. 4 | // MSA_MODULE_HOOK will declare hooks with the following arguments: 5 | // MSA_MODULE_HOOK(return-spec, func-name, ...) where ... is the arguments of the hook 6 | // This file should only be included from plugin module code. 7 | 8 | #ifndef MSA_MODULE_HOOK 9 | #error "cannot include plugin hooks before MSA_MODULE_HOOK macro is defined" 10 | #endif 11 | 12 | MSA_MODULE_HOOK(void, get_loaded, msa::Handle hdl, std::vector &ids) 13 | MSA_MODULE_HOOK(bool, is_enabled, msa::Handle hdl, const std::string &id) 14 | MSA_MODULE_HOOK(bool, is_loaded, msa::Handle hdl, const std::string &id) 15 | MSA_MODULE_HOOK(const Info*, get_info, msa::Handle hdl, const std::string &id) 16 | 17 | -------------------------------------------------------------------------------- /python/msa/builtins/scripting/entities.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from tortoise.models import Model 4 | from tortoise import fields 5 | 6 | 7 | class ScriptEntity(Model): 8 | id = fields.IntField(pk=True) 9 | name = fields.TextField() 10 | crontab = fields.TextField(null=True) 11 | created = fields.DatetimeField(auto_now_add=True) 12 | last_edited = fields.DatetimeField(auto_now=True) 13 | last_run = fields.DatetimeField(null=True) 14 | script_contents = fields.TextField() 15 | 16 | def __str__(self): 17 | return f"ScriptEntity<{self.id}:{self.name}>" 18 | 19 | 20 | class ScriptRunResultEntity(Model): 21 | id = fields.IntField(pk=True) 22 | script = fields.ForeignKeyField( 23 | "models.ScriptEntity", related_name="script_run_results" 24 | ) 25 | run_log = fields.TextField() 26 | run_result = fields.TextField() 27 | created = fields.DatetimeField(auto_now_add=True) 28 | -------------------------------------------------------------------------------- /python/msa/tools/msa_rest_client_loader.py: -------------------------------------------------------------------------------- 1 | from msa.api import get_api 2 | from msa.api.context import ApiContext 3 | from msa.core.config_manager import ConfigManager 4 | from msa.utils.asyncio_utils import run_async 5 | 6 | 7 | class MsaRestClientLoader: 8 | def __init__(self, host="localhost", port=8080, config_overrides={}): 9 | self.host = host 10 | self.port = port 11 | self.config_manager = ConfigManager(config_overrides) 12 | self.config = self.config_manager.get_config() 13 | 14 | self.api = None 15 | 16 | def load(self): 17 | if self.api is not None: 18 | return self.api 19 | 20 | self.api = get_api( 21 | ApiContext.rest, 22 | self.config["plugin_modules"], 23 | host=self.host, 24 | port=self.port, 25 | ) 26 | 27 | run_async(self.api.client.connect()) 28 | 29 | return self.api 30 | -------------------------------------------------------------------------------- /plugins/example/Makefile: -------------------------------------------------------------------------------- 1 | # Example makefile 2 | # 3 | # This serves as an example for compiling C++ plugins for Moe Serifu. 4 | # This Makefile is not called from any recipe in the main project 5 | # Makefile, but its instructions may be replicated 6 | 7 | 8 | # Set up flags 9 | # 10 | # The vital flags in CXXFLAGS are -fPIC (or not if you don't mind not being 11 | # supported by older architectures) and -I../../src. The include should be 12 | # replaced with the location of the MSA headers on your system. 13 | # 14 | # LDFLAGS must also contain -shared, otherwise the .so won't get linked as 15 | # shared objects. 16 | CXX ?= g++ 17 | CXXFLAGS ?= -I../../src -fPIC -std=c++11 -Wall -Wextra -Wpedantic 18 | LDFLAGS ?= -shared 19 | 20 | .PHONY: clean 21 | 22 | example.so: example.o 23 | $(CXX) -o $@ example.o $(LDFLAGS) 24 | 25 | example.o: example.cpp 26 | $(CXX) -c -o $@ example.cpp $(CXXFLAGS) 27 | 28 | clean: 29 | rm -f *.o 30 | rm -f *.so 31 | 32 | -------------------------------------------------------------------------------- /python/sphinx/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /python/msa/builtins/conversation/server_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from msa.core import get_supervisor 4 | from msa.api import ApiContext 5 | from msa.server.server_response import ServerResponseText, ServerResponseType 6 | 7 | 8 | async def talk(request): 9 | """ 10 | 11 | :param request: 12 | :return: 13 | """ 14 | from msa.builtins.conversation.events import ( 15 | ConversationInputEvent, 16 | ConversationOutputEvent, 17 | ) 18 | 19 | new_event = ConversationInputEvent().init(request.data).source(request.source) 20 | 21 | supervisor = get_supervisor() 22 | supervisor.fire_event(new_event) 23 | 24 | response_event = await supervisor.listen_for_result(ConversationOutputEvent) 25 | return ServerResponseText(ServerResponseType.success, response_event.data["output"]) 26 | 27 | 28 | def register_routes(route_binder): 29 | 30 | route_binder.post("/conversation/talk")(talk) 31 | -------------------------------------------------------------------------------- /compat/platform/unix.hpp: -------------------------------------------------------------------------------- 1 | // DO NOT INCLUDE THIS FILE DIRECTLY 2 | #ifndef COMPAT_COMPAT_HPP 3 | #error "do not include compatibility files directly; include compat/compat.hpp instead" 4 | #endif 5 | 6 | // This file is compatible with unix and linux systems; really anything that tries to 7 | // to follow the posix standard 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | namespace msa { namespace platform { 15 | 16 | static inline void sleep(int millisec) 17 | { 18 | struct timespec t; 19 | t.tv_sec = millisec / 1000; 20 | t.tv_nsec = (uint64_t) (millisec % 1000) * UINT64_C(1000000); 21 | nanosleep(&t, NULL); 22 | } 23 | 24 | static inline bool select_stdin() 25 | { 26 | static fd_set fds; 27 | static struct timeval tv; 28 | tv.tv_sec = 0; 29 | tv.tv_usec = 100; 30 | FD_ZERO(&fds); 31 | FD_SET(0, &fds); 32 | select(1, &fds, NULL, NULL, &tv); 33 | return FD_ISSET(0, &fds); 34 | } 35 | 36 | } } 37 | 38 | -------------------------------------------------------------------------------- /src/log/hooks.hpp: -------------------------------------------------------------------------------- 1 | // Functions in the log module that dynamically-loaded modules (plugins) are allowed to call. 2 | 3 | // Define the MSA_MODULE_HOOK macro to arrange the hooks as needed, and then include this file. 4 | // MSA_MODULE_HOOK will declare hooks with the following arguments: 5 | // MSA_MODULE_HOOK(return-spec, func-name, ...) where ... is the arguments of the hook 6 | // This file should only be included from log module code. 7 | 8 | #ifndef MSA_MODULE_HOOK 9 | #error "cannot include log hooks before MSA_MODULE_HOOK macro is defined" 10 | #endif 11 | 12 | 13 | MSA_MODULE_HOOK(void, trace, msa::Handle hdl, const std::string &msg) 14 | MSA_MODULE_HOOK(void, debug, msa::Handle hdl, const std::string &msg) 15 | MSA_MODULE_HOOK(void, info, msa::Handle hdl, const std::string &msg) 16 | MSA_MODULE_HOOK(void, warn, msa::Handle hdl, const std::string &msg) 17 | MSA_MODULE_HOOK(void, error, msa::Handle hdl, const std::string &msg) 18 | MSA_MODULE_HOOK(Level, get_level, msa::Handle hdl) 19 | 20 | -------------------------------------------------------------------------------- /src/util/string.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_UTIL_STRING_HPP 2 | #define MSA_UTIL_STRING_HPP 3 | 4 | #include 5 | #include 6 | 7 | namespace msa { namespace string { 8 | 9 | typedef std::string String; 10 | 11 | extern const String default_ws; 12 | 13 | extern String &left_trim(String &str, const String &search = default_ws); 14 | extern String &right_trim(String &str, const String &search = default_ws); 15 | extern String &trim(String &str, const String &search = default_ws); 16 | extern String &to_upper(String &str); 17 | extern String &to_lower(String &str); 18 | extern void tokenize(const String &str, char separator, std::vector &output); 19 | extern bool ends_with(const String &str, const String &suffix); 20 | extern bool starts_with(const String &str, const String &prefix); 21 | 22 | #ifdef DEBUG 23 | // only included for use with gdb in an environment where it doesn't have std::string's 24 | // complete type 25 | extern const char *dump(const String &str); 26 | #endif 27 | 28 | } } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.plugins.notifications.rst: -------------------------------------------------------------------------------- 1 | msa.plugins.notifications package 2 | ================================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.plugins.notifications.events module 8 | --------------------------------------- 9 | 10 | .. automodule:: msa.plugins.notifications.events 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.plugins.notifications.handlers module 16 | ----------------------------------------- 17 | 18 | .. automodule:: msa.plugins.notifications.handlers 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | msa.plugins.notifications.notification\_providers module 24 | -------------------------------------------------------- 25 | 26 | .. automodule:: msa.plugins.notifications.notification_providers 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: msa.plugins.notifications 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /compat/platform/file/file.cpp: -------------------------------------------------------------------------------- 1 | #include "file.hpp" 2 | 3 | #if defined(__WIN32) 4 | #include "win32.cpp" 5 | #elif defined(__ANDROID__) 6 | #include "android.cpp" 7 | #else 8 | #include "unix.cpp" 9 | #endif 10 | 11 | namespace msa { namespace file { 12 | 13 | extern void join(std::string &base, const std::string &next) 14 | { 15 | base += dir_separator() + next; 16 | } 17 | 18 | extern void basename(std::string &path, const std::string &suffix) 19 | { 20 | const std::string sep = dir_separator(); 21 | while (path.substr(path.size() - sep.size()) == sep) 22 | { 23 | for (size_t i = 0; i < sep.size(); i++) 24 | { 25 | path.erase(path.size() - 1); 26 | } 27 | } 28 | size_t sep_pos = path.rfind(sep); 29 | if (sep_pos != std::string::npos) 30 | { 31 | path = path.substr(sep_pos + sep.size()); 32 | } 33 | if (suffix != "" && path.size() > suffix.size() && path.substr(path.size() - suffix.size()) == suffix) 34 | { 35 | path = path.substr(0, path.size() - suffix.size()); 36 | } 37 | } 38 | 39 | } } 40 | 41 | -------------------------------------------------------------------------------- /compat/platform/lib/win32.cpp: -------------------------------------------------------------------------------- 1 | #include "platform/file/file.hpp" 2 | 3 | namespace msa { namespace lib { 4 | 5 | extern Library *open(const std::string &path) 6 | { 7 | std::string name = path; 8 | msa::file::basename(name, ".so"); 9 | HMODULE handle_ptr = LoadLibrary(path); 10 | if (handle_ptr == NULL) 11 | { 12 | throw library_error(name, "could not load library"); 13 | } 14 | Library *lib = new Library; 15 | lib->handle = handle_ptr; 16 | lib->name = name; 17 | lib->path = path; 18 | return lib; 19 | } 20 | 21 | extern void *get_untyped_symbol(Library *lib, const std::string &symbol) 22 | { 23 | FARPROC sym_addr = GetProcAddress(lib->handle, symbol.c_str()); 24 | if (sym_addr != NULL) 25 | { 26 | throw library_error(lib->name, "could not find symbol: " + symbol); 27 | } 28 | return (void *)sym_addr; 29 | } 30 | 31 | extern void close(Library *lib) 32 | { 33 | if (!FreeLibrary(lib->handle)) 34 | { 35 | throw library_error(lib->name, "could not close library"); 36 | } 37 | delete lib; 38 | } 39 | 40 | } } 41 | -------------------------------------------------------------------------------- /python/msa/server/event_propagate.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class EventPropagationRouter: 5 | def __init__(self): 6 | self.app = None 7 | self.event_bus = None 8 | 9 | def register_propagate_subscription(self, event_bus): 10 | self.event_bus = event_bus 11 | event_bus.subscribe(".*", self.handle_event_propagate) 12 | 13 | async def app_start(self, app): 14 | self.app = app 15 | 16 | async def app_stop(self, app): 17 | pass 18 | # self.event_bus.unsubscri 19 | 20 | async def handle_event_propagate(self, event): 21 | if not event._network_propagate: 22 | return 23 | 24 | if "websockets" not in self.app: 25 | return 26 | 27 | for ws in self.app["websockets"]: 28 | try: 29 | await ws.send_str( 30 | json.dumps( 31 | {"type": "event_propagate", "payload": event.get_metadata()} 32 | ) 33 | ) 34 | except: 35 | pass 36 | -------------------------------------------------------------------------------- /python/msa/server/url_param_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | url_param_re = re.compile("{([0-9a-zA-Z_]+)}") 4 | 5 | 6 | class UrlParamParser: 7 | def __init__(self, route): 8 | self.route = route 9 | self.fields = [] 10 | 11 | self._build() 12 | 13 | def _build(self): 14 | url_matcher = self.route 15 | 16 | matches = url_param_re.findall(self.route) 17 | for grp in matches: 18 | self.fields.append(grp) 19 | 20 | url_matcher = url_matcher.replace("{" + grp + "}", "([a-zA-Z0-9-_.]+)") 21 | 22 | self.matcher = re.compile("^" + url_matcher + "/?$") 23 | 24 | def match(self, route): 25 | return self.matcher.match(route) is not None 26 | 27 | def resolve_params(self, route): 28 | params = {} 29 | 30 | match = self.matcher.search(route) 31 | if not match: 32 | return {} 33 | 34 | groups = list(match.groups()) 35 | 36 | for grp, field in zip(groups, self.fields): 37 | params[field] = grp 38 | 39 | return params 40 | -------------------------------------------------------------------------------- /src/agent/hooks.hpp: -------------------------------------------------------------------------------- 1 | // Functions in the agent module that dynamically-loaded modules (plugins) are allowed to call. 2 | 3 | // Define the MSA_MODULE_HOOK macro to arrange the hooks as needed, and then include this file. 4 | // MSA_MODULE_HOOK will declare hooks with the following arguments: 5 | // MSA_MODULE_HOOK(return-spec, func-name, ...) where ... is the arguments of the hook 6 | // This file should only be included from agent module code. 7 | 8 | #ifndef MSA_MODULE_HOOK 9 | #error "cannot include agent hooks before MSA_MODULE_HOOK macro is defined" 10 | #endif 11 | 12 | MSA_MODULE_HOOK(void, say, msa::Handle hdl, const std::string &text) 13 | MSA_MODULE_HOOK(void, print_prompt_char, msa::Handle hdl) 14 | MSA_MODULE_HOOK(void, register_substitution, msa::Handle hdl, const std::string &name) 15 | MSA_MODULE_HOOK(void, set_substitution, msa::Handle hdl, const std::string &name, const std::string &value) 16 | MSA_MODULE_HOOK(void, unregister_substitution, msa::Handle hdl, const std::string &name) 17 | MSA_MODULE_HOOK(void, get_substitutions, msa::Handle hdl, std::vector &subs) 18 | 19 | -------------------------------------------------------------------------------- /src/event/timer_hooks.hpp: -------------------------------------------------------------------------------- 1 | // Functions in the event/timer sub-module that dynamically-loaded modules (plugins) are allowed to call. 2 | 3 | // Define the MSA_MODULE_HOOK macro to arrange the hooks as needed, and then include this file. 4 | // MSA_MODULE_HOOK will declare hooks with the following arguments: 5 | // MSA_MODULE_HOOK(return-spec, func-name, ...) where ... is the arguments of the hook 6 | // This file should only be included from event module code. 7 | 8 | #ifndef MSA_MODULE_HOOK 9 | #error "cannot include timer event hooks before MSA_MODULE_HOOK macro is defined" 10 | #endif 11 | 12 | MSA_MODULE_HOOK(int16_t, schedule, msa::Handle msa, time_t timestamp, const Topic topic, const IArgs &args) 13 | MSA_MODULE_HOOK(int16_t, delay, msa::Handle msa, std::chrono::milliseconds delay, const Topic topic, const IArgs &args) 14 | MSA_MODULE_HOOK(int16_t, add_timer, msa::Handle msa, std::chrono::milliseconds period, const Topic topic, const IArgs &args) 15 | MSA_MODULE_HOOK(void, remove_timer, msa::Handle msa, int16_t id) 16 | MSA_MODULE_HOOK(void, get_timers, msa::Handle msa, std::vector &list) 17 | -------------------------------------------------------------------------------- /src/util/var.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_UTIL_VAR_HPP 2 | #define MSA_UTIL_VAR_HPP 3 | 4 | #include 5 | #include 6 | 7 | namespace msa { namespace var { 8 | 9 | typedef struct expander_type Expander; 10 | 11 | extern void create_expander(Expander **ex); 12 | extern void dispose_expander(Expander *ex); 13 | extern void register_internal(Expander *ex, const std::string &var); 14 | extern void unregister_internal(Expander *ex, const std::string &var); 15 | extern bool is_registered(const Expander *ex, const std::string &var); 16 | extern void set_value(Expander *ex, const std::string &var, const std::string &value); 17 | extern const std::string &get_value(const Expander *ex, const std::string &var); 18 | extern void register_external(Expander *ex, const std::string &var, std::string *value_ptr); 19 | extern void unregister_external(Expander *ex, const std::string &var); 20 | extern void expand(Expander *ex, std::string &text); 21 | extern bool is_valid_identifier(const std::string &str); 22 | extern void get_defined(const Expander *ex, std::vector &vars); 23 | 24 | } } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /docs/architecture/rework/user_script_submission_interactive.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant user as User 3 | participant cli as MSA CLI Tool REPL 4 | participant daemon as MSA Daemon 5 | 6 | 7 | user->>daemon: Starts daemon 8 | user->>cli: Starts CLI in REPL mode 9 | cli->>daemon: Connect 10 | cli->>user: Display interactive prompt 11 | user->>cli: Inputs command to begin recording session 12 | loop User writes script 13 | user->>cli: Sends commands to REPL 14 | cli->>cli:Record command from user to pending script 15 | cli->>daemon: invokes MSA API as needed 16 | daemon->>cli: respond to invoked command 17 | cli->>user:display result of command eval 18 | end 19 | user->>cli:inputs command to stop recording 20 | cli->>cli: cli saves pending script 21 | cli->>user:cli displays recorded script (via editor?) 22 | user->>user:edits script [optionally] 23 | user->>cli:submit script to server for scheduling 24 | cli->>daemon: Upload Script and schedule 25 | loop Scheduling Loop 26 | daemon->>daemon: Time passes 27 | daemon->>daemon: MSA Daemon runs submitted script at scheduled time 28 | end 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Please reference an issue, or fill out the below information 2 | 3 | - Issue: 4 | 5 | --- 6 | 7 | * **Please check if the PR fulfills these requirements** 8 | - [ ] The commit message follows our guidelines 9 | - [ ] Tests for the changes have been added (for bug fixes / features) 10 | - [ ] Docs have been added / updated (for bug fixes / features) 11 | 12 | 13 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 14 | 15 | 16 | 17 | * **What is the current behavior?** (You can also link to an open issue here) 18 | 19 | 20 | 21 | * **What is the new behavior (if this is a feature change)?** 22 | 23 | 24 | 25 | * **Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?) 26 | 27 | 28 | 29 | * **Other information**: 30 | 31 | --- 32 | 33 | ❗️🖊 If you have not yet, please sign our [Contributor License Agreement](https://docs.google.com/forms/d/e/1FAIpQLSfJPpwIv8MJkzxj41zBhnBDkrziQr9Tn_Z8hl5fRyY0xZnDuQ/viewform). 34 | Your PR will not be accepted until we have confirmed that you have signed. 35 | 36 | -------------------------------------------------------------------------------- /python/msa/plugins/notifications/__init__.py: -------------------------------------------------------------------------------- 1 | from schema import Schema, Optional, And, Or 2 | 3 | from msa.plugins.notifications import handlers 4 | 5 | handler_factories = [handlers.SendNotificationEventHandler] 6 | 7 | entities_list = [] 8 | 9 | config_schema = Schema( 10 | { 11 | Optional("preferred_provider"): Or("pushbullet", "email", "slack"), 12 | "providers": { 13 | Optional("pushbullet"): {"token": And(str, len)}, 14 | Optional("email"): { 15 | "host": And(str, len), 16 | "port": int, 17 | "from": And(str, len), 18 | Optional("password"): And(str, len), 19 | Optional("username"): And(str, len), 20 | Optional("ssl"): bool, 21 | Optional("tls"): bool, 22 | }, 23 | Optional("slack"): { 24 | "webhook_url": And(str, len), 25 | Optional("username"): And(str, len), 26 | Optional("icon_url"): And(str, len), 27 | Optional("icon_emoji"): And(str, len), 28 | }, 29 | }, 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.api.rst: -------------------------------------------------------------------------------- 1 | msa.api package 2 | =============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.api.api\_clients module 8 | --------------------------- 9 | 10 | .. automodule:: msa.api.api_clients 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.api.base\_methods module 16 | ---------------------------- 17 | 18 | .. automodule:: msa.api.base_methods 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | msa.api.context module 24 | ---------------------- 25 | 26 | .. automodule:: msa.api.context 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | msa.api.patchable\_api module 32 | ----------------------------- 33 | 34 | .. automodule:: msa.api.patchable_api 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | msa.api.patcher module 40 | ---------------------- 41 | 42 | .. automodule:: msa.api.patcher 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | 48 | Module contents 49 | --------------- 50 | 51 | .. automodule:: msa.api 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.builtins.signals.rst: -------------------------------------------------------------------------------- 1 | msa.builtins.signals package 2 | ============================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.builtins.signals.client\_api module 8 | --------------------------------------- 9 | 10 | .. automodule:: msa.builtins.signals.client_api 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.builtins.signals.events module 16 | ---------------------------------- 17 | 18 | .. automodule:: msa.builtins.signals.events 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | msa.builtins.signals.handlers module 24 | ------------------------------------ 25 | 26 | .. automodule:: msa.builtins.signals.handlers 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | msa.builtins.signals.server\_api module 32 | --------------------------------------- 33 | 34 | .. automodule:: msa.builtins.signals.server_api 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: msa.builtins.signals 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /src/event/timer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_EVENT_TIMER_HPP 2 | #define MSA_EVENT_TIMER_HPP 3 | 4 | #include "msa.hpp" 5 | #include "cmd/cmd.hpp" 6 | #include "event/event.hpp" 7 | 8 | #include 9 | #include 10 | 11 | namespace msa { namespace event { 12 | 13 | extern std::vector get_timer_commands(); 14 | extern void check_timers(msa::Handle hdl); 15 | extern int create_timer_context(TimerContext **ctx); 16 | extern void dispose_timer_context(TimerContext *ctx); 17 | extern void set_tick_resolution(TimerContext *ctx, int res); 18 | extern void clear_timers(TimerContext *ctx); 19 | extern void sys_remove_timer(msa::Handle msa, int16_t id); 20 | extern int16_t sys_add_timer(msa::Handle msa, std::chrono::milliseconds period, const Topic topic, const IArgs &args); 21 | 22 | #define MSA_MODULE_HOOK(retspec, name, ...) extern retspec name(__VA_ARGS__); 23 | #include "event/timer_hooks.hpp" 24 | #undef MSA_MODULE_HOOK 25 | 26 | struct TimerHooks 27 | { 28 | #define MSA_MODULE_HOOK(retspec, name, ...) retspec (*name)(__VA_ARGS__); 29 | #include "event/timer_hooks.hpp" 30 | #undef MSA_MODULE_HOOK 31 | }; 32 | } } 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /python/msa/core/loader.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | 4 | # builtin modules to load 5 | builtin_module_names = ["signals", "scripting", "conversation", "intents"] 6 | 7 | 8 | def load_builtin_modules(): 9 | """Loads builtin modules.""" 10 | plugin_modules = [] 11 | 12 | for module_name in builtin_module_names: 13 | module = importlib.import_module("msa.builtins.{}".format(module_name)) 14 | module.module_name = module_name 15 | plugin_modules.append(module) 16 | 17 | return plugin_modules 18 | 19 | 20 | def load_plugin_modules(plugin_module_names): 21 | """Loads plugin modules as specified in the configuration file. 22 | 23 | Parameters 24 | ---------- 25 | plugin_module_names : List[str] 26 | Plugin module names to load. Module names should be fully qualified modules existing in `msa.plugins`. 27 | 28 | Returns 29 | ------- 30 | 31 | """ 32 | 33 | plugin_modules = [] 34 | 35 | for module_name in plugin_module_names: 36 | module = importlib.import_module("msa.plugins.{}".format(module_name)) 37 | module.module_name = module_name 38 | plugin_modules.append(module) 39 | 40 | return plugin_modules 41 | -------------------------------------------------------------------------------- /src/input/input.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_INPUT_INPUT_HPP 2 | #define MSA_INPUT_INPUT_HPP 3 | 4 | #include "msa.hpp" 5 | #include "cfg/cfg.hpp" 6 | 7 | #include 8 | #include 9 | 10 | namespace msa { namespace input { 11 | 12 | enum class InputType 13 | { 14 | TTY, 15 | TCP, 16 | UDP 17 | }; 18 | 19 | typedef struct chunk_type 20 | { 21 | std::string text; 22 | } Chunk; 23 | 24 | typedef struct device_type Device; 25 | 26 | extern int init(msa::Handle hdl, const msa::cfg::Section &config); 27 | extern int quit(msa::Handle hdl); 28 | extern void add_device(msa::Handle hdl, InputType type, void *device_id); 29 | extern void get_devices(msa::Handle hdl, std::vector *list); 30 | extern void remove_device(msa::Handle hdl, const std::string &id); 31 | extern const PluginHooks *get_plugin_hooks(); 32 | 33 | #define MSA_MODULE_HOOK(retspec, name, ...) extern retspec name(__VA_ARGS__); 34 | #include "input/hooks.hpp" 35 | #undef MSA_MODULE_HOOK 36 | 37 | struct plugin_hooks_type 38 | { 39 | #define MSA_MODULE_HOOK(retspec, name, ...) retspec (*name)(__VA_ARGS__); 40 | #include "input/hooks.hpp" 41 | #undef MSA_MODULE_HOOK 42 | }; 43 | } } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /compat/platform/lib/android.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "platform/file/file.hpp" 4 | 5 | namespace msa { namespace lib { 6 | 7 | extern Library *open(const std::string &path) 8 | { 9 | std::string name = path; 10 | msa::file::basename(name, ".so"); 11 | void *handle_ptr = dlopen(path.c_str(), RTLD_NOW); 12 | if (handle_ptr == NULL) 13 | { 14 | throw library_error(name, "could not load library"); 15 | } 16 | Library *lib = new Library; 17 | lib->handle = handle_ptr; 18 | lib->name = name; 19 | lib->path = path; 20 | return lib; 21 | } 22 | 23 | extern void *get_untyped_symbol(Library *lib, const std::string &symbol) 24 | { 25 | dlerror(); 26 | void *sym_addr = dlsym(lib->handle, symbol.c_str()); 27 | const char *err_msg = dlerror(); 28 | if (err_msg != NULL) 29 | { 30 | throw library_error(lib->name, "could not find symbol: " + symbol); 31 | } 32 | return sym_addr; 33 | } 34 | 35 | extern void close(Library *lib) 36 | { 37 | int status = dlclose(lib->handle); 38 | if (status != 0) 39 | { 40 | throw library_error(lib->name, "could not close library (dlclose() returned " + std::to_string(status) + ")"); 41 | } 42 | delete lib; 43 | } 44 | 45 | } } 46 | -------------------------------------------------------------------------------- /docs/architecture/rework/publisher_subscriber_pattern.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant u as User 3 | participant a as Asyncio 4 | participant s as Supervisor 5 | participant eb as EventBus 6 | participant hdlr1 as Event Handler 1 7 | participant hdlr2 as Event Handler 2 8 | 9 | u->>a:Start 10 | u->>s:Start 11 | s ->> eb: create 12 | s->>hdlr1: Load 13 | s->>hdlr2: Load 14 | s->>hdlr1:Tell handler to register event types 15 | hdlr1->>eb:Register event type 16 | s->>hdlr2:Tell handler to register event types 17 | hdlr2->>eb:Register event type 18 | s->>hdlr1: Tell handler to register event type subscriptions 19 | hdlr1->>eb: Register subscriptions 20 | s->>hdlr2:Tell handler to register event type subscriptions 21 | hdlr2->>eb: Register subscriptions 22 | s->>hdlr1:Signal start corioutine 23 | s->>hdlr2:Signal start corioutine 24 | hdlr1->eb:await event 25 | hdlr1->>a:yield 26 | hdlr2->eb:await event 27 | hdlr2->>a:yield 28 | loop supervisor wait 29 | s->>s:Wait for shutdown 30 | end 31 | u->>a:Input 32 | a->>hdlr1:Pass text input 33 | hdlr1->>eb:Create event and propagate 34 | eb->eb:Search for handlers subscribed to event type 35 | eb->>hdlr2:Propogate event to handler subscribed to event type 36 | hdlr2->hdlr2:Handle event 37 | 38 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.builtins.conversation.rst: -------------------------------------------------------------------------------- 1 | msa.builtins.conversation package 2 | ================================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.builtins.conversation.client\_api module 8 | -------------------------------------------- 9 | 10 | .. automodule:: msa.builtins.conversation.client_api 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.builtins.conversation.events module 16 | --------------------------------------- 17 | 18 | .. automodule:: msa.builtins.conversation.events 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | msa.builtins.conversation.handlers module 24 | ----------------------------------------- 25 | 26 | .. automodule:: msa.builtins.conversation.handlers 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | msa.builtins.conversation.server\_api module 32 | -------------------------------------------- 33 | 34 | .. automodule:: msa.builtins.conversation.server_api 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: msa.builtins.conversation 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /compat/platform/lib/unix.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "platform/file/file.hpp" 4 | 5 | namespace msa { namespace lib { 6 | 7 | extern Library *open(const std::string &path) 8 | { 9 | std::string name = path; 10 | msa::file::basename(name, ".so"); 11 | void *handle_ptr = dlopen(path.c_str(), RTLD_NOW); 12 | if (handle_ptr == NULL) 13 | { 14 | #ifdef DEBUG 15 | printf("%s\n", dlerror()); 16 | #endif 17 | throw library_error(name, "could not load library"); 18 | } 19 | Library *lib = new Library; 20 | lib->handle = handle_ptr; 21 | lib->name = name; 22 | lib->path = path; 23 | return lib; 24 | } 25 | 26 | extern void *get_untyped_symbol(Library *lib, const std::string &symbol) 27 | { 28 | dlerror(); 29 | void *sym_addr = dlsym(lib->handle, symbol.c_str()); 30 | const char *err_msg = dlerror(); 31 | if (err_msg != NULL) 32 | { 33 | throw library_error(lib->name, "could not find symbol: " + symbol); 34 | } 35 | return sym_addr; 36 | } 37 | 38 | extern void close(Library *lib) 39 | { 40 | int status = dlclose(lib->handle); 41 | if (status != 0) 42 | { 43 | throw library_error(lib->name, "could not close library (dlclose() returned " + std::to_string(status) + ")"); 44 | } 45 | delete lib; 46 | } 47 | 48 | } } 49 | -------------------------------------------------------------------------------- /python/sphinx/contributor_guide.md: -------------------------------------------------------------------------------- 1 | # Contributor Guide 2 | 3 | ## Running from source 4 | 5 | ### Installation 6 | 1. If you do not have it already, please install the python package manager `poetry`: See [poetry installation instructions](https://python-poetry.org/docs/#installation). 7 | 2. Clone the repository `git clone https://github.com/moe-serifu-circle/moe-serifu-agent.git` 8 | 3. Open a terminal and navigate to the location you cloned the repository to. 9 | 4. Run `poetry install` and `poetry shell` to install the python requirements and enter a virtual environment. 10 | 11 | ### Starting the daemon 12 | 1. Open a terminal and navigate to the location you cloned the repository to. 13 | 2. Run `python -m msa daemon` to start the daemon. The daemon should now be hosted on [http://localhost:8080](http://localhost:8080). 14 | 15 | ### Starting the Developer CLI 16 | 17 | Note: In order to use the CLI you must start the daemon first in another terminal. 18 | 19 | 1. Open a terminal and navigate to the location you cloned the repository to. 20 | 2. Run `python -m msa cli` to start the cli which will connect to the daemon. You should be greeted with a python interpreter looking interface, this is the developer cli. 21 | 22 | 23 | ## Plugin Development 24 | 25 | **[STUB]** 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | 2 | pipeline: 3 | test-moeserifu-release: 4 | image: gcc:5 5 | commands: 6 | - make 7 | - make clean 8 | when: 9 | event: [pull_request, push] 10 | branch: master 11 | 12 | test-moeserifu-debug: 13 | image: gcc:5 14 | commands: 15 | - make debug 16 | - make clean 17 | when: 18 | event: [pull_request, push] 19 | branch: master 20 | 21 | test-plugins-release: 22 | image: gcc:5 23 | commands: 24 | - make plugins 25 | - make clean-plugins 26 | when: 27 | event: [pull_request, push] 28 | branch: master 29 | 30 | test-plugins-debug: 31 | image: gcc:5 32 | commands: 33 | - make debug plugins 34 | - make clean 35 | when: 36 | event: [pull_request, push] 37 | branch: master 38 | 39 | run-python-tests: 40 | image: python:3.8 41 | environment: 42 | TEST: "1" 43 | commands: 44 | - cd python 45 | - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - 46 | - $HOME/.poetry/bin/poetry install -E feed_plugin -E notifications_plugin 47 | - $HOME/.poetry/bin/poetry run ./test_and_coverage.sh 48 | when: 49 | event: [pull_request, push] 50 | branch: master 51 | 52 | -------------------------------------------------------------------------------- /python/tests/core/loader_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | 4 | 5 | from msa.core.loader import ( 6 | load_builtin_modules, 7 | load_plugin_modules, 8 | builtin_module_names, 9 | ) 10 | 11 | 12 | class LoaderTest(unittest.TestCase): 13 | @patch("importlib.import_module") 14 | def test_loading_builtin_modules(self, import_module_mock): 15 | module_source = { 16 | f"msa.builtins.{name}": FakeModule() for name in builtin_module_names 17 | } 18 | 19 | import_module_mock.side_effect = lambda e: module_source[e] 20 | 21 | modules = load_builtin_modules() 22 | 23 | self.assertEqual(builtin_module_names, [m.module_name for m in modules]) 24 | 25 | @patch("importlib.import_module") 26 | def test_loading_plugin_modules(self, import_module_mock): 27 | 28 | module_names = ["test_module_1", "test_module_2", "test_module_3"] 29 | module_source = {f"msa.plugins.{name}": FakeModule() for name in module_names} 30 | 31 | import_module_mock.side_effect = lambda e: module_source[e] 32 | 33 | modules = load_plugin_modules(module_names) 34 | 35 | self.assertEqual(module_names, [m.module_name for m in modules]) 36 | 37 | 38 | class FakeModule: 39 | pass 40 | -------------------------------------------------------------------------------- /docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | MSA is executed from the command line, and can be executed in a variety of environments. 5 | 6 | Dependencies 7 | ------------ 8 | 9 | You will need the following dependencies: 10 | 11 | * make OR some other compatible Makefile reader 12 | * g++ OR clang OR msvc OR other compatible C++ compiler 13 | 14 | Usually, these can be installed on your system through its package manager. 15 | 16 | In Debian-based systems: 17 | 18 | ``` 19 | $ sudo apt-get install make g++ 20 | ``` 21 | 22 | MSA can also be built on Android devices. [Termux](https://play.google.com/store/apps/details?id=com.termux) 23 | is a free terminal that is fully-featured even on non-rooted devices and even provides a package management 24 | system and repository. To install the dependencies, do: 25 | 26 | ``` 27 | $ apt install clang make 28 | ``` 29 | 30 | Building & Running 31 | ------------------ 32 | Once the dependencies are installed, download the source code and navigate to the project folder and execute 33 | the make script: 34 | 35 | ``` 36 | $ make 37 | ``` 38 | 39 | To build with debug symbols, build the debug target: 40 | 41 | ``` 42 | $ make debug 43 | ``` 44 | 45 | Finally, run the project by executing the `moe-serifu` executable: 46 | 47 | ``` 48 | $ ./moe-serifu 49 | ``` 50 | -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "msa" 3 | version = "1.2.0" 4 | description = "Moe Serifu Agent (MSA) is an event-driven personal assistant system that presents itself as existing in a particular location (like a house or a smartphone) and performs various tasks as directed by the user." 5 | authors = ["moe-serifu-circle"] 6 | license = "LGPL-3.0+" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8" 10 | click = "^7.0" 11 | schema = "^0.7.0" 12 | termcolor = "^1.1" 13 | colorama = "^0.4.1" 14 | prompt-toolkit = "^2.0" 15 | pygments = "^2.3" 16 | aiohttp = "^3.5" 17 | sphinx-rtd-theme = "^0.4.3" 18 | aiocron = "^1.3" 19 | croniter = "^0.3.30" 20 | aiosqlite = "^0.10.0" 21 | tortoise-orm = "^0.13.7" 22 | tabulate = "^0.8.7" 23 | 24 | 25 | # below `extras`. They can be opted into by apps. 26 | feedparser = { version = "^6.0.1", optional=true } 27 | notifiers = { version = "^1.2.1", optional=true } 28 | docutils = "0.16" 29 | 30 | 31 | [tool.poetry.dev-dependencies] 32 | sphinx = "^2.0" 33 | sphinx-rtd-theme = "^0.4.3" 34 | recommonmark = "^0.5.0" 35 | coverage = "^5.0.4" 36 | aiomonitor = "^0.4.5" 37 | asynctest = "^0.13.0" 38 | 39 | [tool.poetry.extras] 40 | feed_plugin = ["feedparser"] 41 | notifications_plugin = ["notifiers"] 42 | 43 | [build-system] 44 | requires = ["poetry>=0.12"] 45 | build-backend = "poetry.masonry.api" 46 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.core.rst: -------------------------------------------------------------------------------- 1 | msa.core package 2 | ================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.core.config\_manager module 8 | ------------------------------- 9 | 10 | .. automodule:: msa.core.config_manager 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.core.event module 16 | --------------------- 17 | 18 | .. automodule:: msa.core.event 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | msa.core.event\_bus module 24 | -------------------------- 25 | 26 | .. automodule:: msa.core.event_bus 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | msa.core.event\_handler module 32 | ------------------------------ 33 | 34 | .. automodule:: msa.core.event_handler 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | msa.core.loader module 40 | ---------------------- 41 | 42 | .. automodule:: msa.core.loader 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | msa.core.supervisor module 48 | -------------------------- 49 | 50 | .. automodule:: msa.core.supervisor 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: msa.core 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /compat/platform/lib/lib.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_LIB_LIB_HPP 2 | #define COMPAT_PLATFORM_LIB_LIB_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // functions for loading libraries 9 | 10 | namespace msa { namespace lib { 11 | 12 | typedef struct library_type Library; 13 | 14 | class library_error : public std::runtime_error 15 | { 16 | public: 17 | library_error(const std::string &lib_name, const std::string &what); 18 | const std::string &name() const; 19 | private: 20 | const std::string lib_name; 21 | }; 22 | 23 | extern Library *open(const std::string &path); 24 | extern void *get_untyped_symbol(Library *lib, const std::string &symbol); 25 | extern void close(Library *lib); 26 | 27 | template 28 | T get_symbol(Library *lib, const std::string &symbol) 29 | { 30 | static_assert(std::is_pointer::value, "get_symbol can only be called on pointer type"); 31 | void *untyped = get_untyped_symbol(lib, symbol); 32 | union 33 | { 34 | T typed_ptr; 35 | void *untyped_ptr; 36 | } alias; 37 | alias.untyped_ptr = untyped; 38 | T sym = alias.typed_ptr; 39 | return sym; 40 | } 41 | 42 | } } 43 | 44 | #if defined(__WIN32) 45 | #include "win32.hpp" 46 | #elif defined(__ANDROID__) 47 | #include "android.hpp" 48 | #else 49 | #include "unix.hpp" 50 | #endif 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /src/output/hooks.hpp: -------------------------------------------------------------------------------- 1 | // Functions in the output module that dynamically-loaded modules (plugins) are allowed to call. 2 | 3 | // Define the MSA_MODULE_HOOK macro to arrange the hooks as needed, and then include this file. 4 | // MSA_MODULE_HOOK will declare hooks with the following arguments: 5 | // MSA_MODULE_HOOK(return-spec, func-name, ...) where ... is the arguments of the hook 6 | // This file should only be included from output module code. 7 | 8 | #ifndef MSA_MODULE_HOOK 9 | #error "cannot include output hooks before MSA_MODULE_HOOK macro is defined" 10 | #endif 11 | 12 | MSA_MODULE_HOOK(void, write, msa::Handle hdl, const Chunk *chunk) 13 | MSA_MODULE_HOOK(void, write_text, msa::Handle hdl, const std::string &text) 14 | MSA_MODULE_HOOK(void, switch_device, msa::Handle hdl, const std::string &id) 15 | MSA_MODULE_HOOK(void, get_devices, msa::Handle hdl, std::vector *list) 16 | MSA_MODULE_HOOK(void, get_active_device, msa::Handle hdl, std::string &id) 17 | MSA_MODULE_HOOK(void, create_chunk, Chunk **chunk, const std::string &text) 18 | MSA_MODULE_HOOK(void, dispose_chunk, Chunk *chunk) 19 | MSA_MODULE_HOOK(void, create_handler, OutputHandler **handler, const std::string &name, OutputHandlerFunc func) 20 | MSA_MODULE_HOOK(void, dispose_handler, OutputHandler *handler) 21 | MSA_MODULE_HOOK(const std::string&, get_handler_name, const OutputHandler *handler) 22 | 23 | -------------------------------------------------------------------------------- /python/msa/api/__init__.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import asyncio 3 | 4 | from msa.api.patcher import ApiPatcher 5 | from msa.api.context import ApiContext 6 | from msa.api import api_clients 7 | 8 | 9 | def get_api(context, plugin_whitelist=None, **kwargs): 10 | """ 11 | kwargs should only be provided if the context you are attempting to retrieve has not been loaded/patched yet. 12 | """ 13 | if context is None: 14 | raise Exception("get_api: context cannot be None.") 15 | 16 | if not isinstance(context, ApiContext): 17 | raise Exception(f"Invalid api context provided: '{context}'.") 18 | 19 | if len(kwargs.keys()) == 0: 20 | api_instance = ApiPatcher.load(context) 21 | else: 22 | if context == ApiContext.local: 23 | api_client = api_clients.ApiLocalClient(**kwargs) 24 | 25 | elif context == ApiContext.rest: 26 | api_client = api_clients.ApiRestClient(**kwargs) 27 | 28 | elif context == ApiContext.websocket: 29 | api_client = api_clients.ApiWebsocketClient(**kwargs) 30 | 31 | if plugin_whitelist is None: 32 | raise Exception( 33 | "get_api: plugin_whitelist cannot be none when **kwargs are provided to get_api." 34 | ) 35 | 36 | api_instance = ApiPatcher.load(context, api_client, plugin_whitelist) 37 | 38 | return api_instance 39 | -------------------------------------------------------------------------------- /compat/platform/thread/thread.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_PLATFORM_THREAD_THREAD_HPP 2 | #define COMPAT_PLATFORM_THREAD_THREAD_HPP 3 | 4 | #if defined(__WIN32) 5 | #include "win32.hpp" 6 | #elif defined(__ANDROID__) 7 | #include "android.hpp" 8 | #else 9 | #include "unix.hpp" 10 | #endif 11 | 12 | namespace msa { namespace thread { 13 | 14 | extern int init(); 15 | extern int quit(); 16 | extern int create(Thread *thread, const Attributes *attr, void *(*start_routine)(void *), void *arg, const char *name); 17 | extern int join(Thread thread, void **value_ptr); 18 | extern int set_name(Thread thread, const char *name); 19 | extern int get_name(Thread thread, char *name, size_t len); 20 | extern Thread self(); 21 | 22 | extern int attr_init(Attributes *attr); 23 | extern int attr_set_detach(Attributes *attr, bool detach); 24 | extern int attr_get_detach(const Attributes *attr, bool *detach); 25 | extern int attr_destroy(Attributes *attr); 26 | 27 | extern int mutex_init(Mutex *mutex, const MutexAttributes *attr); 28 | extern int mutex_destroy(Mutex *mutex); 29 | extern int mutex_lock(Mutex *mutex); 30 | extern int mutex_unlock(Mutex *mutex); 31 | 32 | extern int cond_init(Cond *cond, const CondAttributes *attr); 33 | extern int cond_destroy(Cond *cond); 34 | extern int cond_wait(Cond *cond, Mutex *mutex); 35 | extern int cond_broadcast(Cond *cond); 36 | extern int cond_signal(Cond *cond); 37 | 38 | } } 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /python/msa/builtins/signals/server_api.py: -------------------------------------------------------------------------------- 1 | from msa.core import get_supervisor 2 | import json 3 | from msa.server.server_response import ( 4 | ServerResponseText, 5 | ServerResponseType, 6 | ServerResponseJson, 7 | ) 8 | 9 | 10 | async def trigger_event(request): 11 | """ 12 | 13 | :param request: 14 | :return: 15 | """ 16 | from msa.core.event import Event 17 | 18 | new_event = Event.deserialize(request.data) 19 | get_supervisor().fire_event(new_event) 20 | 21 | return ServerResponseText( 22 | ServerResponseType.success, text=f"Event {new_event} sucessfully triggered." 23 | ) 24 | 25 | 26 | async def get_events(request): 27 | """ 28 | 29 | :param request: 30 | :return: 31 | """ 32 | 33 | from msa.builtins.signals.events import ( 34 | RequestDisburseEventsToNetworkEvent, 35 | DisburseEventsToNetworkEvent, 36 | ) 37 | 38 | new_event = RequestDisburseEventsToNetworkEvent().init({}) 39 | get_supervisor().fire_event(new_event) 40 | 41 | response_event = await get_supervisor().listen_for_result( 42 | DisburseEventsToNetworkEvent 43 | ) 44 | 45 | return ServerResponseJson( 46 | ServerResponseType.success, payload=response_event.get_metadata() 47 | ) 48 | 49 | 50 | def register_routes(route_binder): 51 | 52 | route_binder.post("/signals/trigger_event")(trigger_event) 53 | route_binder.get("/signals/events")(get_events) 54 | -------------------------------------------------------------------------------- /src/output/output.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_OUTPUT_OUTPUT_HPP 2 | #define MSA_OUTPUT_OUTPUT_HPP 3 | 4 | #include "msa.hpp" 5 | #include "cfg/cfg.hpp" 6 | 7 | #include 8 | #include 9 | 10 | namespace msa { namespace output { 11 | 12 | enum class OutputType 13 | { 14 | TTY, 15 | TCP, 16 | UDP 17 | }; 18 | 19 | typedef struct chunk_type Chunk; 20 | 21 | typedef struct device_type Device; 22 | 23 | typedef void (*OutputHandlerFunc)(msa::Handle hdl, const Chunk *ch, Device *dev); 24 | 25 | typedef struct output_handler_type OutputHandler; 26 | 27 | extern int init(msa::Handle hdl, const msa::cfg::Section &config); 28 | extern int quit(msa::Handle hdl); 29 | 30 | 31 | extern void add_device(msa::Handle hdl, OutputType type, void *device_id); 32 | extern void remove_device(msa::Handle hdl, const std::string &id); 33 | 34 | extern void register_handler(msa::Handle hdl, OutputType type, const OutputHandler *handler); 35 | extern void unregister_handler(msa::Handle hdl, OutputType type, const OutputHandler *handler); 36 | extern const PluginHooks *get_plugin_hooks(); 37 | 38 | #define MSA_MODULE_HOOK(retspec, name, ...) extern retspec name(__VA_ARGS__); 39 | #include "output/hooks.hpp" 40 | #undef MSA_MODULE_HOOK 41 | 42 | struct plugin_hooks_type 43 | { 44 | #define MSA_MODULE_HOOK(retspec, name, ...) retspec (*name)(__VA_ARGS__); 45 | #include "output/hooks.hpp" 46 | #undef MSA_MODULE_HOOK 47 | }; 48 | 49 | } } 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.server.rst: -------------------------------------------------------------------------------- 1 | msa.server package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.server.default\_routes module 8 | --------------------------------- 9 | 10 | .. automodule:: msa.server.default_routes 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.server.event\_propagate module 16 | ---------------------------------- 17 | 18 | .. automodule:: msa.server.event_propagate 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | msa.server.route\_adapter module 24 | -------------------------------- 25 | 26 | .. automodule:: msa.server.route_adapter 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | msa.server.server\_request module 32 | --------------------------------- 33 | 34 | .. automodule:: msa.server.server_request 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | msa.server.server\_response module 40 | ---------------------------------- 41 | 42 | .. automodule:: msa.server.server_response 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | msa.server.url\_param\_parser module 48 | ------------------------------------ 49 | 50 | .. automodule:: msa.server.url_param_parser 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: msa.server 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /python/things-to-do.txt: -------------------------------------------------------------------------------- 1 | - [X] enable submitting scripts to server and running on a schedule 2 | - [X] enable creating custom db tables for plugins 3 | 4 | - [ ] Schema versioning and migrations for entities 5 | - consider switching to sql alchemy 6 | - homeassistant reference: https://github.com/home-assistant/core/blob/7dd42bc32d70f5ef2bb7a5910226a6672523d3eb/homeassistant/components/recorder/migration.py 7 | 8 | - [x] implement listing uploaded scripts 9 | - if there is a crontab, include the time of next execution 10 | - [x] implement deleting uploaded scripts 11 | - [ ] implement logging script results 12 | - should this include stdout + stderr outputs? 13 | - how should this be cleaned up? 14 | - [ ] implement pullinhg script run logs 15 | - [x] implement downloading uploaded scripts 16 | - [ ] impoement adhoc running an uploaded script. 17 | 18 | - [ ] implement basic counter service 19 | - [ ] implement basic reminder/timer service 20 | - [X] allow plugins to send events to client 21 | - [X] allow clients to send events to server 22 | - [ ] create basic javascript client 23 | - [ ] plan out stt & tts 24 | - [ ] plan out NLP 25 | - [ ] create basic characture mesh 26 | - [ ] create basic webGL client 27 | - [ ] research useful things the bot could do 28 | - [ ] checkout mycroft 29 | - [ ] checkout home assistant 30 | - [ ] checkout jasper voice assistant 31 | - [ ] consider various means of providing sensors/inputs to app 32 | - [ ] look into building/generating rasberry pi images 33 | 34 | -------------------------------------------------------------------------------- /python/tests/core/event_handler_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import unittest 4 | from unittest import mock 5 | 6 | from tests.async_test_util import async_run 7 | from msa.core.event_handler import EventHandler 8 | 9 | 10 | class EventHandlerTest(unittest.TestCase): 11 | def setUp(self): 12 | self.loop = asyncio.new_event_loop() 13 | self.dummy_logger = logging.getLogger("foo") 14 | self.dummy_logger.addHandler(logging.NullHandler()) 15 | self.event_bus = FakeEventBus() 16 | 17 | def test_constructor(self): 18 | 19 | self.event_handler = DummyEventHandler( 20 | self.loop, self.event_bus, self.dummy_logger 21 | ) 22 | 23 | assert self.event_handler.loop == self.loop 24 | assert self.event_handler.event_bus == self.event_bus 25 | assert self.event_handler.logger == self.dummy_logger 26 | 27 | def test_init_hook(self): 28 | self.event_handler = DummyEventHandler( 29 | self.loop, self.event_bus, self.dummy_logger 30 | ) 31 | async_run(self.loop, self.event_handler.init()) 32 | 33 | 34 | class DummyEventHandler(EventHandler): 35 | def __init__(self, loop, queue, logger): 36 | super().__init__(loop, queue, logger) 37 | 38 | self.read_from_queue = [] 39 | 40 | async def handle(self): 41 | data = await self.event_queue.get() 42 | self.read_from_queue.append(data) 43 | 44 | 45 | class FakeEventBus: 46 | pass 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /python/msa/plugins/notifications/events.py: -------------------------------------------------------------------------------- 1 | from schema import Schema, And, Or, Optional, Use 2 | from msa.core.event import Event 3 | from msa.plugins.notifications.notification_providers import NotificationProvider 4 | 5 | 6 | class SendNotificationEvent(Event): 7 | """ 8 | SendNotificationEvent schema: 9 | - timestamp: datetime in yyyy-mm-dd hh:mm:ss:xx format of starup event 10 | """ 11 | 12 | def __init__(self): 13 | super().__init__( 14 | priority=40, 15 | schema=Schema( 16 | { 17 | "provider": Or( 18 | NotificationProvider.slack.value, 19 | NotificationProvider.email.value, 20 | NotificationProvider.pushbullet.value, 21 | ), 22 | Optional("target"): And(str, len), 23 | "title": And(str, len), 24 | "message": And(str, len), 25 | } 26 | ), 27 | ) 28 | 29 | 30 | class SendPreferredNotificationEvent(Event): 31 | """ 32 | SendNotificationEvent schema: 33 | - timestamp: datetime in yyyy-mm-dd hh:mm:ss:xx format of starup event 34 | """ 35 | 36 | def __init__(self): 37 | super().__init__( 38 | priority=40, 39 | schema=Schema( 40 | { 41 | Optional("target"): And(str, len), 42 | "title": And(str, len), 43 | "message": And(str, len), 44 | } 45 | ), 46 | ) 47 | -------------------------------------------------------------------------------- /python/msa/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import click 4 | 5 | 6 | @click.group(invoke_without_command=True) 7 | @click.option("--config-file", default="msa_config.json", help="The config file to use") 8 | @click.option( 9 | "--log-level", 10 | default=None, 11 | help="Override the log level defined in the config file. Can be either: 'debug', 'info' 'warn', or 'error'. Defaults to None.", 12 | ) 13 | @click.pass_context 14 | def main(ctx, config_file, log_level): 15 | 16 | ctx.obj["cli_overrides"] = {"log_level": log_level, "config_file": config_file} 17 | 18 | if ctx.invoked_subcommand is None: 19 | ctx.invoke(daemon) 20 | 21 | 22 | @main.command() 23 | @click.pass_context 24 | def daemon(ctx): 25 | 26 | from msa.server import start_server 27 | 28 | start_server(ctx.obj) 29 | 30 | 31 | @main.command() 32 | @click.option( 33 | "-s", "--script", help="If provided will run the specified script and then exit." 34 | ) 35 | @click.pass_context 36 | def cli(ctx, script): 37 | from msa.cli.interpreter import Interpreter 38 | 39 | interpreter = Interpreter(ctx.obj) 40 | 41 | if script is not None: 42 | if os.path.exists(script) and os.path.isfile(script): 43 | interpreter.execute_script(script) 44 | else: 45 | print( 46 | "-s flag provided. Unable to load script {}. Please verify that the file exists and the provided path is correct.".format( 47 | script 48 | ) 49 | ) 50 | else: 51 | interpreter.start() 52 | 53 | 54 | if __name__ == "__main__": 55 | main(obj={}) 56 | -------------------------------------------------------------------------------- /src/event/handler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_EVENT_HANDLER_HPP 2 | #define MSA_EVENT_HANDLER_HPP 3 | 4 | #include "msa.hpp" 5 | #include "event/event.hpp" 6 | 7 | namespace msa { namespace event { 8 | 9 | typedef struct handler_synchronization_type HandlerSync; 10 | 11 | typedef void (*EventHandler)(msa::Handle hdl, const Event *const e, HandlerSync *const sync); 12 | 13 | // creates a handler sync and initializes the variables in it 14 | extern void create_handler_sync(HandlerSync **sync); 15 | 16 | // destroys handler sync and frees resources associated with it 17 | extern void dispose_handler_sync(HandlerSync *sync); 18 | 19 | // called on a handler to tell it to suspend at next safe point 20 | extern void suspend_handler(HandlerSync *sync); 21 | 22 | // called on a handler to tell it to resume execution 23 | extern void resume_handler(HandlerSync *sync); 24 | 25 | // check if a handler has entered an interrupt point and is waiting 26 | // to be resumed. 27 | extern bool handler_suspended(HandlerSync *sync); 28 | 29 | // called on a handler to mark it as the origin of a system call 30 | extern void set_handler_syscall_origin(HandlerSync *sync); 31 | 32 | // called on a handler to clear its status as the origin of a system call 33 | extern void clear_handler_syscall_origin(HandlerSync *sync); 34 | 35 | // check if a handler is the origin of a system call 36 | extern bool handler_syscall_origin(HandlerSync *sync); 37 | 38 | // Defines a safe point inside of an event handler. Use these 39 | // to mark where a handler can safely be suspended/interrupted 40 | extern void HANDLER_INTERRUPT_POINT(HandlerSync *sync); 41 | 42 | } } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /python/sphinx/api/msa.builtins.scripting.rst: -------------------------------------------------------------------------------- 1 | msa.builtins.scripting package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | msa.builtins.scripting.client\_api module 8 | ----------------------------------------- 9 | 10 | .. automodule:: msa.builtins.scripting.client_api 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | msa.builtins.scripting.entities module 16 | -------------------------------------- 17 | 18 | .. automodule:: msa.builtins.scripting.entities 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | msa.builtins.scripting.events module 24 | ------------------------------------ 25 | 26 | .. automodule:: msa.builtins.scripting.events 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | msa.builtins.scripting.handlers module 32 | -------------------------------------- 33 | 34 | .. automodule:: msa.builtins.scripting.handlers 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | msa.builtins.scripting.script\_execution\_manager module 40 | -------------------------------------------------------- 41 | 42 | .. automodule:: msa.builtins.scripting.script_execution_manager 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | msa.builtins.scripting.server\_api module 48 | ----------------------------------------- 49 | 50 | .. automodule:: msa.builtins.scripting.server_api 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: msa.builtins.scripting 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /python/tests/api/patcher_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | import asyncio 5 | 6 | from msa.api import ApiContext 7 | from msa.api.patcher import ApiPatcher 8 | from msa.api import api_clients 9 | 10 | 11 | class PatcherTest(unittest.TestCase): 12 | def setUp(self): 13 | # clear cache 14 | ApiPatcher.cache = {} 15 | 16 | def test_init_and_load_with_no_api_context(self): 17 | 18 | with self.assertRaises(Exception) as cm: 19 | ApiPatcher.load(None) 20 | 21 | self.assertEqual( 22 | str(cm.exception), "ApiPatcher: api_context cannot be None." 23 | ) 24 | 25 | def test_init_and_load_first_time_with_no_api_client(self): 26 | 27 | api_context = ApiContext.local 28 | 29 | with self.assertRaises(Exception) as cm: 30 | ApiPatcher.load(api_context) 31 | 32 | self.assertEqual( 33 | str(cm.exception), 34 | f"ApiPatcher: api_client cannot be None when context '{api_context}' has never been patched and loaded.", 35 | ) 36 | 37 | def test_init_and_load_first_time_with_api_client(self): 38 | 39 | loop = asyncio.new_event_loop() 40 | api_context = ApiContext.local 41 | api_client = api_clients.ApiLocalClient(loop) 42 | print(api_client) 43 | 44 | with self.assertRaises(Exception) as cm: 45 | ApiPatcher.load(api_context, api_client) 46 | 47 | self.assertEqual( 48 | str(cm.exception), 49 | f"ApiPatcher: plugin_whitelist cannot be None when context '{api_context}' has never been patched and loaded.", 50 | ) 51 | -------------------------------------------------------------------------------- /python/msa/builtins/signals/client_api.py: -------------------------------------------------------------------------------- 1 | from msa.core.event import Event 2 | 3 | 4 | async def trigger_event(self, event): 5 | """ 6 | Takes an `msa.core.event.Event` subclass to propagate to the daemon. 7 | 8 | :async: 9 | :param msa.core.event.Event event: The event to propagate to the daemon. `event._network_propagate` must be set to 10 | `True`, otherwise the event will not be propagated. 11 | :return: `None` 12 | """ 13 | if not event._network_propagate: 14 | print( 15 | "WARNING: event._network_propagate is not True, cancelling network propagation." 16 | ) 17 | 18 | response = await self.client.post( 19 | "/signals/trigger_event", payload=event.get_metadata() 20 | ) 21 | 22 | if not response: 23 | return 24 | if response.status != "success": 25 | raise Exception(response.json["message"]) 26 | 27 | 28 | async def get_events(self): 29 | """ 30 | Fetches any events that have not been dispersed to clients. 31 | 32 | :async: 33 | :return: List[msa.core.event.Event] 34 | """ 35 | response = await self.client.get("/signals/events") 36 | 37 | if not response: 38 | return 39 | if response.status != "success": 40 | raise Exception(response.raw) 41 | 42 | # load disburse event 43 | disburse_event = Event.deserialize(response.json) 44 | 45 | # load sent events 46 | deserialized_events = [] 47 | for raw_event in disburse_event.data["events"]: 48 | new_event = Event.deserialize(raw_event) 49 | deserialized_events.append(new_event) 50 | 51 | return deserialized_events 52 | 53 | 54 | def register_endpoints(api_binder): 55 | 56 | api_binder.register_method()(trigger_event) 57 | api_binder.register_method()(get_events) 58 | -------------------------------------------------------------------------------- /src/agent/agent.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_AGENT_AGENT_HPP 2 | #define MSA_AGENT_AGENT_HPP 3 | 4 | #include "msa.hpp" 5 | #include "cfg/cfg.hpp" 6 | 7 | // Start of hooks' includes 8 | #include 9 | #include 10 | // End of hooks' includes 11 | 12 | // Moe Serifu Agent state and manipulation 13 | 14 | namespace msa { namespace agent { 15 | 16 | // macro would mask enum constant 17 | #ifdef DEBUG 18 | #define DEBUG_TEMP_OFF 19 | #undef DEBUG 20 | #endif 21 | enum class State 22 | { 23 | IDLE, 24 | ALERT, 25 | LISTEN, 26 | ERO, 27 | CONVERSE, 28 | DEBUG 29 | }; 30 | #ifdef DEBUG_TEMP_OFF 31 | #undef DEBUG_TEMP_OFF 32 | #define DEBUG 33 | #endif 34 | 35 | enum class Mood 36 | { 37 | NORMAL 38 | }; 39 | 40 | typedef struct agent_type 41 | { 42 | // name of the agent 43 | std::string name; 44 | 45 | // current activity 46 | State state; 47 | 48 | // positive attitude to the master user, 49 | // TODO: make this into an ID->attitude table 50 | uint32_t attitude; 51 | 52 | // current emotional state, affected by context and responses 53 | Mood mood; 54 | 55 | // creates a new agent, n is the name of the agent 56 | agent_type(const std::string &n); 57 | } Agent; 58 | 59 | extern int init(msa::Handle hdl, const msa::cfg::Section &config); 60 | extern int quit(msa::Handle hdl); 61 | extern const Agent *get_agent(msa::Handle hdl); 62 | extern const PluginHooks *get_plugin_hooks(); 63 | 64 | #define MSA_MODULE_HOOK(retspec, name, ...) extern retspec name(__VA_ARGS__); 65 | #include "agent/hooks.hpp" 66 | #undef MSA_MODULE_HOOK 67 | 68 | struct plugin_hooks_type 69 | { 70 | #define MSA_MODULE_HOOK(retspec, name, ...) retspec (*name)(__VA_ARGS__); 71 | #include "agent/hooks.hpp" 72 | #undef MSA_MODULE_HOOK 73 | }; 74 | 75 | } } 76 | #endif 77 | -------------------------------------------------------------------------------- /python/msa/builtins/signals/handlers.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | import datetime 3 | from msa.core.event_handler import EventHandler 4 | from msa.core import get_supervisor 5 | from msa.builtins.signals import events 6 | 7 | 8 | class StartupEventTrigger(EventHandler): 9 | """ 10 | Fires off a startup event and then exits 11 | """ 12 | 13 | def __init__(self, loop, event_bus, logger, config=None): 14 | super().__init__(loop, event_bus, logger, config) 15 | 16 | async def init(self): 17 | # trigger startup hook later 18 | self.loop.call_later(1, self.trigger_event) 19 | 20 | def trigger_event(self): 21 | new_event = events.StartupEvent() 22 | new_event.init( 23 | {"timestamp": datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S:%f")} 24 | ) 25 | get_supervisor().fire_event(new_event) 26 | 27 | 28 | class NetworkPropagateEventHandler(EventHandler): 29 | def __init__(self, loop, event_bus, logger, config=None): 30 | super().__init__(loop, event_bus, logger, config) 31 | 32 | event_bus.subscribe(".*", self.handle) 33 | 34 | self.buffered_events = deque(maxlen=10) 35 | 36 | async def handle(self, event): 37 | 38 | if isinstance(event, events.RequestDisburseEventsToNetworkEvent): 39 | self.handle_disburse_request() 40 | return 41 | 42 | if not event._network_propagate: 43 | return 44 | 45 | self.buffered_events.append(event) 46 | 47 | def handle_disburse_request(self): 48 | new_event = events.DisburseEventsToNetworkEvent().init( 49 | {"events": [event.get_metadata() for event in self.buffered_events]} 50 | ) 51 | self.buffered_events.clear() 52 | get_supervisor().fire_event(new_event) 53 | -------------------------------------------------------------------------------- /python/tests/builtins/conversation/client_api_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import MagicMock, AsyncMock, patch 3 | 4 | from msa.builtins.conversation.client_api import talk 5 | from msa.api.api_clients import ApiResponse 6 | 7 | from msa.utils.asyncio_utils import run_async 8 | 9 | 10 | class ClientApiTest(unittest.TestCase): 11 | @patch("builtins.print") 12 | def test_talk_success(self, printMock): 13 | 14 | response = "my response!" 15 | slf = MagicMock() 16 | slf.client.post = AsyncMock( 17 | return_value=ApiResponse({"status": "success", "text": response}) 18 | ) 19 | input_message = "hello, how are you?" 20 | 21 | run_async(talk(slf, input_message)) 22 | 23 | printMock.assert_called_once_with(response) 24 | 25 | @patch("builtins.print") 26 | def test_talk_failed_bad_response(self, printMock): 27 | 28 | response = {"msg": "my response!"} 29 | slf = MagicMock() 30 | slf.client.post = AsyncMock( 31 | return_value=ApiResponse({"status": "success", "json": response}) 32 | ) 33 | input_message = "hello, how are you?" 34 | 35 | run_async(talk(slf, input_message)) 36 | printMock.assert_called_once_with("It seems there was an issue.") 37 | 38 | def test_talk_failed_good_response(self): 39 | 40 | response = "fake error message" 41 | slf = MagicMock() 42 | slf.client.post = AsyncMock( 43 | return_value=ApiResponse({"status": "failed", "text": response}) 44 | ) 45 | input_message = "hello, how are you?" 46 | 47 | with self.assertRaises(Exception) as cm: 48 | run_async(talk(slf, input_message)) 49 | 50 | the_exception = cm.exception 51 | self.assertEqual(str(the_exception), f"Server Error: \n{response}") 52 | -------------------------------------------------------------------------------- /python/sphinx/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | !!! Until our first official release, the only supported way of installing MSA is from source. Please see [Running From Source](contributor_guide.html#running-from-source).:w 4 | 5 | The goal of this getting started guide is to walk you through the basic usage of MSA. 6 | 7 | **Note:** This guide assumes that you have already installed the `moe-serifu-agent` package, if not, please see the 8 | [Installation](installation.html) page. 9 | 10 | **Note**: This guide will explain how to run MSA from the `moe-serifu-agent` package. To run from source see 11 | [Running From Source](contributor_guide.html#running-from-source) 12 | 13 | ## Up and running 14 | 15 | ### Starting the daemon 16 | 17 | Before we can start interacting with MSA we need to start it. To start MSA, run `moe-serifu-agent daemon` in a terminal. By default the daemon will run on port `8080`. 18 | 19 | ### Connecting to the daemon with a client 20 | 21 | #### The development Command Line Interface (also called the cli) 22 | 23 | To start the cli, open a new terminal on the same computer running the daemon. Then run `moe-serifu-agent cli`. 24 | This should start the interactive cli. This tool is an interactive python interpreter for writing and uploading scripts. 25 | It is the lowest level way of interacting with MSA. 26 | 27 | To exit at any time, press `Ctrl+c` or type `quit()`. 28 | 29 | 30 | ## Next Steps 31 | 32 | Now that we have covered how to get up and running with MSA, here are a few more topics worth reading: 33 | - [Customizing your MSA](configuration): Covers setting up a configuration file that you can use to customize the 34 | behavior of MSA. 35 | - [Using the CLI](using_the_cli): A tutorial on using the cli to write scripts for MSA to run, and to build custom automations. 36 | - [Extending MSA with plugins](plugins): Adding additional functionality through MSA plugins. 37 | 38 | -------------------------------------------------------------------------------- /python/msa/builtins/conversation/handlers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from msa.core.event_handler import EventHandler 4 | from msa.core import get_supervisor 5 | from msa.builtins.conversation import events 6 | from msa.builtins.intents.events import IntentEvent 7 | 8 | 9 | class ConversationInputEventHandler(EventHandler): 10 | """ 11 | Handles ConversationInputEvents 12 | """ 13 | 14 | def __init__(self, loop, event_bus, logger, config=None): 15 | super().__init__(loop, event_bus, logger, config) 16 | self.event_bus.subscribe(events.ConversationInputEvent, self.handle) 17 | 18 | async def handle(self, event): 19 | # consider 20 | # - https://github.com/gunthercox/ChatterBot 21 | # - https://github.com/huggingface/transformers 22 | 23 | input = self.normalize(event.data["input"]).lower() 24 | 25 | # TODO use an intents file and nlp model 26 | # See for a decent tutorial: https://data-flair.training/blogs/python-chatbot-project/ 27 | # in the future intent types will be derived from the intents file. 28 | 29 | if input == "hello" or input == "hi": 30 | output = "Hello, how are you?" 31 | new_event = events.ConversationOutputEvent().init({"output": output}) 32 | 33 | elif "how are you" in input: 34 | output = "I am well thank you. What can I do for you?" 35 | new_event = events.ConversationOutputEvent().init({"output": output}) 36 | 37 | elif "fetch feeds" in input: 38 | new_event = IntentEvent().init( 39 | {"type": "msa.plugins.rss_feed.events.RssFeedRequestEvent"} 40 | ) 41 | 42 | else: 43 | output = "I am afraid I don't know what to say." 44 | new_event = events.ConversationOutputEvent().init({"output": output}) 45 | 46 | get_supervisor().fire_event(new_event) 47 | 48 | def normalize(self, string): 49 | return string.strip().lower().replace("\n", "") 50 | -------------------------------------------------------------------------------- /python/msa/builtins/intents/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from msa.core.event_handler import EventHandler 3 | from msa.core import get_supervisor 4 | from msa.builtins.intents.events import IntentEvent 5 | from msa.core.event import Event 6 | 7 | 8 | class IntentToEventHandler(EventHandler): 9 | """ 10 | Handles IntentEvents and converts them to specific event types. 11 | 12 | This allows specific event types to be generated without placing a Type-dependency on a specific plugin or handler. 13 | When an Intent is received, if the plugin providing required event type is not loaded, no typed event will be 14 | propagated. 15 | """ 16 | 17 | def __init__(self, loop, event_bus, logger, config=None): 18 | super().__init__(loop, event_bus, logger, config) 19 | self.event_bus.subscribe(IntentEvent, self.handle) 20 | 21 | async def handle(self, event): 22 | 23 | event_type = event.data["type"] 24 | event_data = event.data.get("context", None) 25 | subclasses = Event.__subclasses__() 26 | 27 | found_cls = False 28 | self.logger.debug( 29 | f"Attempting to translate Intent to event of type: {event_type}" 30 | ) 31 | for cls in subclasses: 32 | mod = f"{cls.__module__}.{cls.__name__}" 33 | self.logger.debug(f"Checking: {mod}") 34 | if event_type == mod: 35 | found_cls = True 36 | break 37 | 38 | if not found_cls: 39 | self.logger.warning( 40 | f"Failed to translate Intent to Event. Could not find event: {event_type}" 41 | ) 42 | return 43 | 44 | try: 45 | new_event = cls().init(event_data) 46 | except: 47 | self.logger.exception( 48 | "While attempting to convert an intent to an event, the event initialization failed " 49 | "due to a schema validation error" 50 | ) 51 | return 52 | 53 | get_supervisor().fire_event(new_event) 54 | -------------------------------------------------------------------------------- /src/log/log.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_LOG_LOG_HPP 2 | #define MSA_LOG_LOG_HPP 3 | 4 | #include "msa.hpp" 5 | #include "cfg/cfg.hpp" 6 | 7 | #include 8 | #include 9 | 10 | namespace msa { namespace log { 11 | 12 | enum class StreamType 13 | { 14 | FILE 15 | }; 16 | 17 | // we have a constant named "DEBUG"; turn off that macro if it's on 18 | #ifdef DEBUG 19 | #define DEBUG_TEMP_OFF 20 | #undef DEBUG 21 | #endif 22 | enum class Level 23 | { 24 | TRACE, 25 | DEBUG, 26 | INFO, 27 | WARN, 28 | ERROR 29 | }; 30 | #ifdef DEBUG_TEMP_OFF 31 | #undef DEBUG_TEMP_OFF 32 | #define DEBUG 33 | #endif 34 | 35 | enum class Format 36 | { 37 | TEXT, 38 | XML 39 | }; 40 | 41 | enum class OpenMode 42 | { 43 | OVERWRITE, 44 | APPEND 45 | }; 46 | 47 | typedef size_t stream_id; 48 | 49 | extern int init(msa::Handle hdl, const msa::cfg::Section &config); 50 | extern int quit(msa::Handle hdl); 51 | 52 | // creates a new log stream and returns the ID of the stream 53 | extern stream_id create_stream(msa::Handle hdl, StreamType type, const std::string &location, Format fmt, const std::string &output_format_string, OpenMode open_mode); 54 | extern void set_level(msa::Handle hdl, Level level); 55 | extern void set_stream_level(msa::Handle hdl, stream_id id, Level level); 56 | extern Level get_stream_level(msa::Handle hdl, stream_id id); 57 | extern void trace(msa::Handle hdl, const char *msg); 58 | extern void debug(msa::Handle hdl, const char *msg); 59 | extern void info(msa::Handle hdl, const char *msg); 60 | extern void warn(msa::Handle hdl, const char *msg); 61 | extern void error(msa::Handle hdl, const char *msg); 62 | extern const PluginHooks *get_plugin_hooks(); 63 | 64 | #define MSA_MODULE_HOOK(retspec, name, ...) extern retspec name(__VA_ARGS__); 65 | #include "log/hooks.hpp" 66 | #undef MSA_MODULE_HOOK 67 | 68 | struct plugin_hooks_type 69 | { 70 | #define MSA_MODULE_HOOK(retspec, name, ...) retspec (*name)(__VA_ARGS__); 71 | #include "log/hooks.hpp" 72 | #undef MSA_MODULE_HOOK 73 | }; 74 | } } 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /python/msa/plugins/rss_feed/handlers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from msa.builtins.intents.events import IntentEvent 4 | from msa.core.event_handler import EventHandler 5 | from msa.core import get_supervisor 6 | from msa.builtins.conversation.events import ConversationOutputEvent 7 | from msa.plugins.rss_feed.events import RssFeedRequestEvent 8 | 9 | try: 10 | import feedparser 11 | except: 12 | print("rss_feed plugin, requires the feed_plugin extra to be installed.") 13 | exit() 14 | 15 | 16 | class RssPollingHandler(EventHandler): 17 | """ 18 | Handles ConversationInputEvents 19 | """ 20 | 21 | def __init__(self, loop, event_bus, logger, config=None): 22 | super().__init__(loop, event_bus, logger, config) 23 | 24 | self.event_bus.subscribe(RssFeedRequestEvent, self.handle_request_feed) 25 | 26 | self.feed = None 27 | 28 | def schedule(self): 29 | return [(self.config.get("poll_crontab", "0 * * * *"), self.check_feed)] 30 | 31 | async def check_feed(self, time): 32 | self.feed = await self.loop.run_in_executor(None, self.fetch_feeds) 33 | 34 | def fetch_feeds(self): 35 | return feedparser.parse(self.config["feed_url"]) 36 | 37 | async def handle_request_feed(self, event): 38 | 39 | if self.feed is None: 40 | await self.check_feed(None) 41 | 42 | title = self.feed["feed"]["title"] 43 | 44 | output = f"{title}:\n" 45 | for entry in self.feed["entries"][:10]: 46 | title = entry["title"] 47 | link = entry["link"] 48 | 49 | output += f"• {title}: {link}\n" 50 | 51 | output_event = ( 52 | ConversationOutputEvent().init({"output": output}).network_propagate() 53 | ) 54 | 55 | notify_intent = IntentEvent().init( 56 | { 57 | "type": "msa.plugins.notifications.events.SendPreferredNotificationEvent", 58 | "context": { 59 | "title": "RSS Feeds Ready", 60 | "message": "I finished loading your RSS feeds!", 61 | }, 62 | } 63 | ) 64 | 65 | get_supervisor().fire_event(output_event) 66 | get_supervisor().fire_event(notify_intent) 67 | -------------------------------------------------------------------------------- /python/terminal_client/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | import aiohttp 6 | from prompt_toolkit.shortcuts.prompt import PromptSession 7 | from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop 8 | 9 | import msa 10 | from msa.api import get_api, run_async 11 | from msa.api.context import ApiContext 12 | 13 | use_asyncio_event_loop() 14 | 15 | loop = asyncio.get_event_loop() 16 | host = "localhost" 17 | port = 8080 18 | plugins = [] 19 | 20 | # a very simple conversational client 21 | api = get_api(ApiContext.rest, plugins, host=host, port=port) 22 | 23 | 24 | async def startup_check(): 25 | 26 | await api.client.connect() 27 | 28 | try: 29 | await api.check_connection() 30 | 31 | expected_version = "0.1.0" 32 | server_version = await api.get_version() 33 | if expected_version != server_version: 34 | print("Server version does not match required version!") 35 | print("Expected version:", expected_version) 36 | print("Server version: ", server_version) 37 | exit(1) 38 | finally: 39 | await api.client.disconnect() 40 | 41 | 42 | run_async(startup_check()) 43 | 44 | # start websocket connection 45 | 46 | 47 | async def interact(api, state): 48 | try: 49 | while True: 50 | if not "prompt_session" in state: 51 | prompt_session = PromptSession() 52 | state["prompt_session"] = prompt_session 53 | else: 54 | prompt_session = state["prompt_session"] 55 | 56 | try: 57 | text = await prompt_session.prompt(">>> ", async_=True) 58 | text = text.strip().replace("\n", "") 59 | 60 | if text == "quit" or text == "exit": 61 | break 62 | 63 | await api.talk(text) 64 | 65 | except (KeyboardInterrupt, EOFError): 66 | print("Shutting down...") 67 | finally: 68 | await api.client.stop() 69 | return 70 | finally: 71 | await ws_client.client.stop() 72 | 73 | 74 | ws_client = get_api( 75 | ApiContext.websocket, plugins, interact=interact, loop=loop, host=host, port=port 76 | ) 77 | ws_client.client.start() 78 | -------------------------------------------------------------------------------- /plugins/example/example.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin/plugin.hpp" 2 | #include "agent/agent.hpp" 3 | #include "cmd/cmd.hpp" 4 | 5 | extern "C" const msa::plugin::Info *msa_plugin_register(const msa::PluginHooks *hooks); 6 | 7 | namespace dekarrin { 8 | 9 | typedef struct env 10 | { 11 | std::vector *commands; 12 | } Env; 13 | 14 | static int init(msa::Handle hdl, void **env); 15 | static int quit(msa::Handle hdl, void *env); 16 | static int add_commands(msa::Handle hdl, void *plugin_env, std::vector &new_commands); 17 | static msa::cmd::Result love_func(msa::Handle hdl, const msa::cmd::ParamList ¶ms, msa::event::HandlerSync *const sync); 18 | 19 | static const msa::plugin::FunctionTable function_table = {init, quit, NULL, NULL, NULL, add_commands}; 20 | static const msa::plugin::Info plugin_info = {"example", "Example Plugin", {"dekarrin"}, msa::plugin::Version(1, 0, 0, 0), &function_table}; 21 | 22 | static const msa::PluginHooks *msa_sys; 23 | 24 | static int init(msa::Handle hdl __attribute__((unused)), void **env) 25 | { 26 | Env *my_env = new Env; 27 | my_env->commands = new std::vector; 28 | my_env->commands->push_back(msa::cmd::Command("LOVE", "execute a test function", "", love_func)); 29 | *env = my_env; 30 | return 0; 31 | } 32 | 33 | static int quit(msa::Handle hdl __attribute__((unused)), void *env) 34 | { 35 | Env *my_env = (Env *) env; 36 | delete my_env->commands; 37 | delete my_env; 38 | return 0; 39 | } 40 | 41 | static int add_commands(msa::Handle hdl __attribute__((unused)), void *env, std::vector &new_commands) 42 | { 43 | Env *my_env = (Env *) env; 44 | for (size_t i = 0; i < my_env->commands->size(); i++) 45 | { 46 | new_commands.push_back(&my_env->commands->at(i)); 47 | } 48 | return 0; 49 | } 50 | 51 | static msa::cmd::Result love_func(msa::Handle hdl, const msa::cmd::ParamList &args __attribute__((unused)), msa::event::HandlerSync *const sync __attribute__((unused))) 52 | { 53 | msa_sys->agent->say(hdl, "$USER_TITLE, the new command works!"); 54 | return msa::cmd::Result(0); 55 | } 56 | 57 | } 58 | 59 | extern "C" const msa::plugin::Info *msa_plugin_register(const msa::PluginHooks *hooks) 60 | { 61 | dekarrin::msa_sys = hooks; 62 | return &dekarrin::plugin_info; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/event/event.cpp: -------------------------------------------------------------------------------- 1 | #include "event/event.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace msa { namespace event { 7 | 8 | struct topic_attr { 9 | uint8_t priority; 10 | }; 11 | 12 | static const struct topic_attr topic_attr_table[] = { 13 | #define MSA_EVENT_TOPIC(enum_name, priority) {priority}, 14 | #include "event/topics.hpp" 15 | #undef MSA_EVENT_TOPIC 16 | }; 17 | 18 | extern bool operator<(const Event &e1, const Event &e2) 19 | { 20 | return e1.attributes->priority < e2.attributes->priority; 21 | } 22 | 23 | extern bool operator>(const Event &e1, const Event &e2) 24 | { 25 | return (e2 < e1); 26 | } 27 | 28 | extern bool operator<=(const Event &e1, const Event &e2) 29 | { 30 | return !(e1 > e2); 31 | } 32 | 33 | extern bool operator>=(const Event &e1, const Event &e2) 34 | { 35 | return !(e1 < e2); 36 | } 37 | 38 | extern bool operator==(const Event &e1, const Event &e2) 39 | { 40 | return e1.attributes->priority == e2.attributes->priority; 41 | } 42 | 43 | extern bool operator!=(const Event &e1, const Event &e2) 44 | { 45 | return !(e1 == e2); 46 | } 47 | 48 | extern int max_topic_index() 49 | { 50 | return sizeof(topic_attr_table) / sizeof(struct topic_attr) - 1; 51 | } 52 | 53 | static const struct topic_attr *get_topic_attr(Topic t) 54 | { 55 | int t_val = static_cast(t); 56 | if (t_val > max_topic_index() || t_val < 0) 57 | { 58 | throw std::invalid_argument("unknown topic; did you add the topic to the attr table?"); 59 | } 60 | size_t idx = static_cast(t); 61 | return (topic_attr_table + idx); 62 | } 63 | 64 | extern const Event *create(Topic topic, const IArgs &args) 65 | { 66 | Event *e = new Event; 67 | e->generation_time = time(NULL); 68 | e->attributes = get_topic_attr(topic); 69 | e->topic = topic; 70 | e->args = args.copy(); 71 | return e; 72 | } 73 | 74 | extern void dispose(const Event *e) 75 | { 76 | delete e; 77 | } 78 | 79 | extern uint8_t get_priority(const Event *e) 80 | { 81 | return e->attributes->priority; 82 | } 83 | 84 | extern std::string topic_str(Topic t) 85 | { 86 | #define MSA_EVENT_TOPIC(enum_name, priority) if (t == Topic::enum_name) return #enum_name; 87 | #include "event/topics.hpp" 88 | #undef MSA_EVENT_TOPIC 89 | return std::to_string(static_cast(t)); 90 | } 91 | } } 92 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* Main source code file. */ 2 | 3 | #include "msa.hpp" 4 | #include "util/util.hpp" 5 | 6 | #include 7 | #include 8 | 9 | int main(int argc, char *argv[]) { 10 | const char *cfg_path; 11 | if (argc < 2) 12 | { 13 | fprintf(stderr, "no config file given, defaulting to 'msa.cfg'\n"); 14 | cfg_path = "msa.cfg"; 15 | } 16 | else 17 | { 18 | cfg_path = argv[1]; 19 | } 20 | 21 | msa::init(); 22 | 23 | msa::Handle hdl; 24 | int succ = msa::start(&hdl, cfg_path); 25 | if (succ != MSA_SUCCESS) 26 | { 27 | const char *err_msg; 28 | switch (succ) 29 | { 30 | case MSA_ERR_CONFIG: 31 | err_msg = "could not load config file"; 32 | break; 33 | case MSA_ERR_LOG: 34 | err_msg = "could not init logging system"; 35 | break; 36 | case MSA_ERR_INPUT: 37 | err_msg = "could not start input system"; 38 | break; 39 | case MSA_ERR_OUTPUT: 40 | err_msg = "could not start output system"; 41 | break; 42 | case MSA_ERR_EVENT: 43 | err_msg = "could not start event dispatcher"; 44 | break; 45 | case MSA_ERR_AGENT: 46 | err_msg = "could not init agent state machine"; 47 | break; 48 | case MSA_ERR_CMD: 49 | err_msg = "could not start command hooks"; 50 | break; 51 | default: 52 | err_msg = "unknown problem"; 53 | break; 54 | } 55 | fprintf(stderr, "\nMSA init failed: error %d - %s\n", succ, err_msg); 56 | // if we got a config error or a log error, the log has not yet been started 57 | if (succ != MSA_ERR_CONFIG && succ != MSA_ERR_LOG) 58 | { 59 | fprintf(stderr, "See log for more details\n"); 60 | } 61 | return EXIT_FAILURE; 62 | } 63 | DEBUG_PRINTF("(Waiting for EDT to start)\n"); 64 | while (hdl->status == msa::Status::CREATED) 65 | { 66 | msa::util::sleep_milli(50); 67 | } 68 | DEBUG_PRINTF("(System is ready)\n"); 69 | while (hdl->status == msa::Status::RUNNING) 70 | { 71 | msa::util::sleep_milli(50); 72 | } 73 | DEBUG_PRINTF("(Waiting for MSA to exit)\n"); 74 | while (hdl->status != msa::Status::STOPPED) 75 | { 76 | msa::util::sleep_milli(50); 77 | } 78 | if (hdl->status == msa::Status::STOPPED) 79 | { 80 | DEBUG_PRINTF("(MSA system has exited)\n"); 81 | msa::dispose(hdl); 82 | } 83 | else 84 | { 85 | DEBUG_PRINTF("(MSA state is not shutdown, but still terminating)\n"); 86 | } 87 | 88 | msa::quit(); 89 | return EXIT_SUCCESS; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /docs/architecture/rework/startup_and_echo_sequence_diagram.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant a as Asyncio 3 | participant s as Supervisor 4 | participant eb as Event Bus 5 | participant cr as Command Reg. Hdlr. 6 | participant tty as TTY Hdlr. 7 | participant e as Echo Hdlr. 8 | 9 | note left of a: Bootstrap 10 | a->>+s: Corioutine Start 11 | s->>+eb:Instanciate 12 | s->>+cr:Load Module 13 | cr->>-a:Yield 14 | s->>+tty:Load Module 15 | tty->>-a:Yield 16 | s->>+e:Load Module 17 | e->>-a:Yield 18 | s->>+cr:Initialize 19 | cr->>-s:return 20 | s->>+tty:Initialize 21 | tty->>-s:return 22 | s->>+e:Initialize 23 | e->>-s:return 24 | e->>eb:Register Command Event 25 | eb->>a:Deposit Event In Event Queue 26 | a--x+cr:Give event from event queue 27 | cr->>cr:Handle Event from event queue 28 | cr->>-a:Yield 29 | a--x+tty:Give event from event queue 30 | tty->>tty:Reject event (not of tty handler types) 31 | tty->>-a:Yield 32 | a--x+e:Give event from Event Queue 33 | e->>e:Reject event (not of echo handler types) 34 | e->>-a:Yield 35 | s->>s:Start main event loop 36 | s->>-a: Yield 37 | note left of a: Echo evt demo 38 | loop Main Loop 39 | a->>a: wait for interaction 40 | a--x+tty: Input on stdin pass to tty handler prompt 41 | tty->>eb: Propagate TextInputEvent 42 | eb->>a:Insert TextInputEvent into evt. queue 43 | tty->>-a:Yield 44 | a--xcr: Give TxtInEvt. 45 | cr->>cr:Parse text as command 46 | cr->>eb: Propagate new command type event (CTE) echo 47 | eb->>a:Insert CTE into evt. queue 48 | cr->>a: Yield 49 | a--x+tty:Give TxtInEvt. 50 | tty->>tty:Reject event (not of tty handler types) 51 | tty->-a:Yield 52 | a--x+e:Give TxtInEvt. 53 | e->>e:Reject event (not of echo handler types) 54 | e->>-a:Yield 55 | a--x+cr: Give CTE echo 56 | cr->>cr:Reject event (not of command registry types) 57 | cr->>-a:Yield 58 | a--x+tty:Give CTE echo 59 | tty-->tty:Reject event (not of tty handler types) 60 | tty-->-a:Yield 61 | a--x+e:Give CTE echo 62 | e->>e: Process event 63 | e->>eb: Propagate TextOutputEvent (TOE) 64 | eb->>a: Insert TOE intro event queue 65 | e->>-a:Yield 66 | a--x+cr: Give TOE 67 | cr->>cr:Reject event (not of command registry types) 68 | cr->>-a:Yield 69 | a--x+tty: Give TOE 70 | tty->>tty: Process event, print text to tty 71 | tty->>-a:Yield 72 | a--x+e: Give TOE 73 | e->>e:Reject event (not of command registry types) 74 | e->>-a:Yield 75 | end 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/event/event.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_EVENT_EVENT_HPP 2 | #define MSA_EVENT_EVENT_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace msa { namespace event { 9 | 10 | enum class Topic 11 | { 12 | #define MSA_EVENT_TOPIC(enum_name, priority) enum_name, 13 | #include "event/topics.hpp" 14 | #undef MSA_EVENT_TOPIC 15 | }; 16 | 17 | // wraps the actual event args so that users of the event API do not have to manually provide 18 | // move, copy, and delete operators for every actual type of event args unless they wish to 19 | // implement IArgs 20 | class IArgs { 21 | public: 22 | virtual ~IArgs() {} 23 | virtual IArgs *copy() const = 0; 24 | }; 25 | 26 | // concrete implementation of IArgs that just defers copy operation to resident object 27 | template 28 | class Args : public IArgs 29 | { 30 | public: 31 | Args(const T &wrapped) : args(new T(wrapped)) 32 | {} 33 | 34 | virtual ~Args() 35 | { 36 | delete args; 37 | } 38 | 39 | Args(const Args &other) : args(new T(*other.args)) 40 | {} 41 | 42 | Args &operator=(const Args &other) 43 | { 44 | T *temp_args = new T(*other.args); 45 | delete args; 46 | args = temp_args; 47 | return *this; 48 | } 49 | 50 | virtual IArgs *copy() const 51 | { 52 | return new Args(*this); 53 | } 54 | 55 | const T &get_args() const 56 | { 57 | return *args; 58 | } 59 | 60 | private: 61 | T *args; 62 | }; 63 | 64 | template 65 | Args wrap(const T &arg) 66 | { 67 | return Args(arg); 68 | } 69 | 70 | struct topic_attr; 71 | 72 | typedef struct event_type 73 | { 74 | Topic topic; 75 | const topic_attr *attributes; 76 | time_t generation_time; 77 | IArgs *args; 78 | } Event; 79 | 80 | extern bool operator<(const Event &e1, const Event &e2); 81 | extern bool operator>(const Event &e1, const Event &e2); 82 | extern bool operator<=(const Event &e1, const Event &e2); 83 | extern bool operator>=(const Event &e1, const Event &e2); 84 | extern bool operator==(const Event &e1, const Event &e2); 85 | extern bool operator!=(const Event &e1, const Event &e2); 86 | 87 | extern const Event *create(Topic topic, const IArgs &args); 88 | extern void dispose(const Event *e); 89 | extern uint8_t get_priority(const Event *e); 90 | extern int max_topic_index(); 91 | extern std::string topic_str(Topic t); 92 | 93 | } } 94 | 95 | #endif 96 | -------------------------------------------------------------------------------- /python/msa/core/event_handler.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Dict, Optional, List, Tuple, Callable, Coroutine 3 | import logging 4 | from datetime import datetime 5 | 6 | 7 | import msa 8 | from msa.core.event_bus import EventBus 9 | 10 | 11 | class EventHandler: 12 | """The base event handler class, all other event handlers should be a subclass of this type. 13 | 14 | Attributes 15 | ---------- 16 | loop : asyncio.AbstractEventLoop 17 | the main event loop. 18 | event_bus : msa.core.event_bus.EventBus 19 | an event loop that this handler may attempt to read events out of by awaiting on 20 | it.""" 21 | 22 | def __init__( 23 | self, 24 | loop: asyncio.AbstractEventLoop, 25 | event_bus: EventBus, 26 | logger: logging.Logger, 27 | config: Optional[Dict] = None, 28 | ): 29 | """Creates a new event handler. Subclasses should call the base constructor before setting up their own internal 30 | state. 31 | 32 | Parameters 33 | ---------- 34 | loop : asyncio.AbstractEventLoop 35 | an asyncio event loop. 36 | event_bus : msa.core.event_bus.EventBus 37 | An instance of the event bus that the handler can use to subscribe to events. 38 | logger : logging.Logger 39 | A logger instance specific to this event handler.""" 40 | self.loop = loop 41 | self.event_bus = event_bus 42 | self.logger = logger 43 | self.config = config 44 | 45 | async def init(self): 46 | """An optional initialization hook, may be used for executing setup code before all handlers have benn fully 47 | started.""" 48 | pass 49 | 50 | def schedule( 51 | self, 52 | ) -> List[Tuple[str, Callable[[], Coroutine[datetime, None, None]]]]: 53 | """An optional hook, may be used for scheduling one or more methods/functions to be periodically called.. 54 | 55 | The expected return value is a list of tuples. Each tuple should be a crontab string, followed by a coroutine 56 | that should be executed periodically based on the given crontab. 57 | 58 | Invalid crontabs will result in a warning in the log, and the coroutine will not be scheduled. 59 | 60 | Returns 61 | ------ 62 | List[Tuple[str, Callable[[], Coroutine[None]]]] 63 | """ 64 | return [] 65 | 66 | schedule.base_class = True 67 | -------------------------------------------------------------------------------- /src/event/handler.cpp: -------------------------------------------------------------------------------- 1 | #include "event/handler.hpp" 2 | 3 | #include "platform/thread/thread.hpp" 4 | 5 | namespace msa { namespace event { 6 | 7 | struct handler_synchronization_type { 8 | msa::thread::Cond resume_cond; 9 | msa::thread::Mutex suspend_mutex; 10 | bool suspend_flag; 11 | bool in_wait_loop; 12 | bool syscall_origin; 13 | }; 14 | 15 | extern void create_handler_sync(HandlerSync **sync) 16 | { 17 | handler_synchronization_type *handler_sync = new handler_synchronization_type; 18 | msa::thread::cond_init(&handler_sync->resume_cond, NULL); 19 | msa::thread::mutex_init(&handler_sync->suspend_mutex, NULL); 20 | handler_sync->suspend_flag = false; 21 | handler_sync->in_wait_loop = false; 22 | handler_sync->syscall_origin = false; 23 | *sync = handler_sync; 24 | } 25 | 26 | extern void dispose_handler_sync(HandlerSync *sync) 27 | { 28 | msa::thread::cond_destroy(&sync->resume_cond); 29 | msa::thread::mutex_destroy(&sync->suspend_mutex); 30 | delete sync; 31 | } 32 | 33 | extern void suspend_handler(HandlerSync *sync) 34 | { 35 | msa::thread::mutex_lock(&sync->suspend_mutex); 36 | sync->suspend_flag = true; 37 | msa::thread::mutex_unlock(&sync->suspend_mutex); 38 | } 39 | 40 | extern void resume_handler(HandlerSync *sync) 41 | { 42 | msa::thread::mutex_lock(&sync->suspend_mutex); 43 | sync->suspend_flag = false; 44 | msa::thread::cond_broadcast(&sync->resume_cond); 45 | msa::thread::mutex_unlock(&sync->suspend_mutex); 46 | } 47 | 48 | extern bool handler_suspended(HandlerSync *sync) 49 | { 50 | msa::thread::mutex_lock(&sync->suspend_mutex); 51 | bool suspended = sync->in_wait_loop; 52 | msa::thread::mutex_unlock(&sync->suspend_mutex); 53 | return suspended; 54 | } 55 | 56 | extern void set_handler_syscall_origin(HandlerSync *sync) 57 | { 58 | sync->syscall_origin = true; 59 | } 60 | 61 | extern void clear_handler_syscall_origin(HandlerSync *sync) 62 | { 63 | sync->syscall_origin = false; 64 | } 65 | 66 | extern bool handler_syscall_origin(HandlerSync *sync) 67 | { 68 | return sync->syscall_origin; 69 | } 70 | 71 | extern void HANDLER_INTERRUPT_POINT(HandlerSync *sync) 72 | { 73 | msa::thread::mutex_lock(&sync->suspend_mutex); 74 | while (sync->suspend_flag) 75 | { 76 | sync->in_wait_loop = true; 77 | msa::thread::cond_wait(&sync->resume_cond, &sync->suspend_mutex); 78 | } 79 | sync->in_wait_loop = false; 80 | msa::thread::mutex_unlock(&sync->suspend_mutex); 81 | } 82 | 83 | } } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Libraries 12 | *.lib 13 | *.a 14 | *.la 15 | *.lo 16 | 17 | # Shared objects (inc. Windows DLLs) 18 | *.dll 19 | *.so 20 | *.so.* 21 | *.dylib 22 | 23 | # Executables 24 | *.exe 25 | *.app 26 | *.i*86 27 | *.x86_64 28 | *.hex 29 | *.out 30 | 31 | # Debug files 32 | *.dSYM/ 33 | *.su 34 | 35 | # main bin files 36 | moe-serifu 37 | *.log 38 | 39 | ## Python Gitignore 40 | # Byte-compiled / optimized / DLL files 41 | __pycache__/ 42 | *.py[cod] 43 | *$py.class 44 | 45 | # C extensions 46 | *.so 47 | 48 | # Distribution / packaging 49 | .Python 50 | build/ 51 | develop-eggs/ 52 | dist/ 53 | downloads/ 54 | eggs/ 55 | .eggs/ 56 | lib/ 57 | lib64/ 58 | parts/ 59 | sdist/ 60 | var/ 61 | wheels/ 62 | *.egg-info/ 63 | .installed.cfg 64 | *.egg 65 | MANIFEST 66 | 67 | # PyInstaller 68 | # Usually these files are written by a python script from a template 69 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 70 | *.manifest 71 | *.spec 72 | 73 | # Installer logs 74 | pip-log.txt 75 | pip-delete-this-directory.txt 76 | 77 | # Unit test / coverage reports 78 | htmlcov/ 79 | .tox/ 80 | .nox/ 81 | .coverage 82 | .coverage.* 83 | .cache 84 | nosetests.xml 85 | coverage.xml 86 | *.cover 87 | .hypothesis/ 88 | .pytest_cache/ 89 | 90 | # Translations 91 | *.mo 92 | *.pot 93 | 94 | # Django stuff: 95 | *.log 96 | local_settings.py 97 | db.sqlite3 98 | 99 | # Flask stuff: 100 | instance/ 101 | .webassets-cache 102 | 103 | # Scrapy stuff: 104 | .scrapy 105 | 106 | # Sphinx documentation 107 | docs/_build/ 108 | 109 | # PyBuilder 110 | target/ 111 | 112 | # Jupyter Notebook 113 | .ipynb_checkpoints 114 | 115 | # IPython 116 | profile_default/ 117 | ipython_config.py 118 | 119 | # pyenv 120 | .python-version 121 | 122 | # celery beat schedule file 123 | celerybeat-schedule 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | .idea/ 153 | 154 | msa.log 155 | 156 | _build/ 157 | msa.db* 158 | .msa_cli_history 159 | 160 | -------------------------------------------------------------------------------- /src/util/string.cpp: -------------------------------------------------------------------------------- 1 | #include "util/string.hpp" 2 | #include 3 | 4 | namespace msa { namespace string { 5 | 6 | const String default_ws = " \t\r\n"; 7 | 8 | extern String &left_trim(String &str, const String &search) 9 | { 10 | if (str == "" || search == "") 11 | { 12 | return str; 13 | } 14 | size_t p = str.find_first_not_of(search); 15 | if (p == String::npos) 16 | { 17 | // it is entirely the string, return a blank one 18 | str = ""; 19 | return str; 20 | } 21 | str = str.substr(p); 22 | return str; 23 | } 24 | 25 | extern String &right_trim(String &str, const String &search) 26 | { 27 | if (str == "" || search == "") 28 | { 29 | return str; 30 | } 31 | size_t p = str.find_last_not_of(search); 32 | if (p == String::npos) 33 | { 34 | str = ""; 35 | return str; 36 | } 37 | str = str.substr(0, p + 1); 38 | return str; 39 | } 40 | 41 | extern String &trim(String &str, const String &search) 42 | { 43 | if (str == "" || search == "") 44 | { 45 | return str; 46 | } 47 | left_trim(str, search); 48 | right_trim(str, search); 49 | return str; 50 | } 51 | 52 | extern String &to_upper(String &str) 53 | { 54 | for (auto &c : str) 55 | { 56 | c = (char) std::toupper(c); 57 | } 58 | return str; 59 | } 60 | 61 | extern String &to_lower(String &str) 62 | { 63 | for (auto &c : str) 64 | { 65 | c = (char) std::tolower(c); 66 | } 67 | return str; 68 | } 69 | 70 | extern void tokenize(const String &str, char separator, std::vector &output) 71 | { 72 | String cur_token; 73 | for (size_t i = 0; i < str.size(); i++) 74 | { 75 | if (str[i] != separator) 76 | { 77 | cur_token.push_back(str[i]); 78 | } 79 | else if (cur_token.size() > 0) 80 | { 81 | output.push_back(cur_token); 82 | cur_token = ""; 83 | } 84 | } 85 | if (cur_token.size() > 0) 86 | { 87 | output.push_back(cur_token); 88 | } 89 | } 90 | 91 | extern bool ends_with(const String &str, const String &suffix) 92 | { 93 | if (str.size() < suffix.size()) 94 | { 95 | return false; 96 | } 97 | size_t start_pos = str.size() - suffix.size(); 98 | return (str.compare(start_pos, String::npos, suffix) == 0); 99 | } 100 | 101 | extern bool starts_with(const String &str, const String &prefix) 102 | { 103 | if (str.size() < prefix.size()) 104 | { 105 | return false; 106 | } 107 | return (str.compare(0, prefix.size(), prefix) == 0); 108 | } 109 | 110 | #ifdef DEBUG 111 | extern const char *dump(const String &str) 112 | { 113 | return str.c_str(); 114 | } 115 | #endif 116 | 117 | } } 118 | 119 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ODIR ?= obj 2 | SDIR ?= src 3 | OS_SDIR ?= compat 4 | PLDIR ?= plugins 5 | 6 | INCLUDE_DIRS = -I$(SDIR) -I$(OS_SDIR) 7 | 8 | # python interpreter is normally set by the shebang of the scripts, but some environments don't support standard 9 | # shebangs, and should export this variable before running scripts: 10 | PYTHON ?= 11 | 12 | CXX ?= g++ 13 | CXXFLAGS ?= -std=c++11 -Wall -Wextra -Wpedantic -pthread $(INCLUDE_DIRS) -include compat/compat.hpp 14 | LDFLAGS ?= -ldl -lpthread 15 | 16 | DEP_TARGETS ?= agent/agent.o util/util.o msa.o event/event.o event/handler.o event/dispatch.o event/timer.o input/input.o util/string.o cfg/cfg.o cmd/cmd.o log/log.o output/output.o util/var.o plugin/plugin.o 17 | DEP_INCS = $(patsubst %.o,$(SDIR)/%.hpp,$(DEP_TARGETS)) 18 | DEP_OBJS = $(patsubst %,$(ODIR)/%,$(DEP_TARGETS)) 19 | DEP_SOURCES = $(patsubst %.o,%.cpp,$(DEP_TARGETS)) 20 | DEP_EXS = $(SDIR)/debug_macros.hpp 21 | 22 | OS_DEP_TARGETS = thread/thread.o file/file.o lib/lib.o 23 | OS_DEP_OBJS = $(patsubst %,$(ODIR)/platform/%,$(notdir $(OS_DEP_TARGETS))) 24 | OS_DEP_SOURCES = $(patsubst %.o,platform/%.cpp,$(OS_DEP_TARGETS)) 25 | 26 | .PHONY: clean test all debug plugins clean-plugins gen-deps 27 | 28 | all: moe-serifu plugins 29 | 30 | debug: CXXFLAGS += -g -O0 -Werror -DDEBUG 31 | debug: moe-serifu 32 | 33 | test: debug 34 | valgrind --leak-check=yes --track-origins=yes moe-serifu 35 | 36 | clean: clean-plugins 37 | rm -f $(ODIR)/*.o 38 | rm -f $(patsubst %,$(ODIR)/%*.o,$(sort $(subst ./,,$(dir $(DEP_TARGETS))))) 39 | rm -f $(ODIR)/platform/*.o 40 | rm -f moe-serifu 41 | 42 | gen-deps: 43 | $(PYTHON) scripts/gendeps.py SDIR $(SDIR) $(INCLUDE_DIRS) $(patsubst %,-E%,$(DEP_EXS)) $(DEP_SOURCES) > scripts/modules.mk 44 | $(PYTHON) scripts/gendeps.py OS_SDIR $(OS_SDIR) $(INCLUDE_DIRS) $(OS_DEP_SOURCES) -c1 > scripts/os_modules.mk 45 | 46 | include scripts/*.mk 47 | 48 | 49 | # ----------------- # 50 | # Binary Recipies # 51 | # ----------------- # 52 | 53 | moe-serifu: $(ODIR)/main.o $(DEP_OBJS) $(OS_DEP_OBJS) 54 | $(CXX) -o $@ $^ $(CXXFLAGS) $(LDFLAGS) 55 | 56 | $(ODIR)/main.o: $(SDIR)/main.cpp $(DEP_INCS) 57 | $(CXX) -c -o $@ $(SDIR)/main.cpp $(CXXFLAGS) 58 | 59 | 60 | # --------- # 61 | # Plugins # 62 | # --------- # 63 | 64 | clean-plugins: 65 | rm -f $(PLDIR)/autoload/*.so 66 | rm -f $(PLDIR)/example/*.so 67 | rm -f $(PLDIR)/example/*.o 68 | 69 | plugins: $(PLDIR)/autoload/example.so 70 | 71 | $(PLDIR)/autoload/example.so: $(PLDIR)/example/example.so 72 | cp $(PLDIR)/example/example.so $(PLDIR)/autoload/example.so 73 | 74 | $(PLDIR)/example/example.so: $(PLDIR)/example/example.o 75 | $(CXX) -o $@ $(PLDIR)/example/example.o -shared 76 | 77 | $(PLDIR)/example/example.o: $(PLDIR)/example/example.cpp $(SDIR)/plugin/plugin.hpp 78 | $(CXX) -c -o $@ $(PLDIR)/example/example.cpp -I$(SDIR) -include compat/compat.hpp -fPIC -std=c++11 -Wall -Wextra -Wpedantic 79 | 80 | -------------------------------------------------------------------------------- /docs/architecture/states.md: -------------------------------------------------------------------------------- 1 | # Agent state initial design 2 | 3 | ******* 4 | 5 | ## Agent State 6 | 7 | The state of the MSA can be one of several options, which determine how it acts. 8 | 9 | ### Idle State 10 | 11 | This is the default state of the MSA and is entered into when it does not have 12 | any active goals to pursue. It will spend some time waiting, but will then 13 | choose one of some random frivolous activity to do. The MSA will move about the 14 | active I/O device network as specified by the activity. 15 | 16 | ### Alerting State 17 | 18 | The MSA will enter this state when it has discovered information that must be 19 | shared with the user. The MSA will drop everything of a lower priority and rush 20 | to alert the user. If a phone is present and is on the user's person, the MSA 21 | will move to the phone to directly deliver the message. 22 | 23 | If multiple alerts are to be delivered, they will form a priority queue which 24 | will all be delivered at once. 25 | 26 | The MSA will only exit the alert state once it has successfully delivered the 27 | message and had it acknowledged by the user. 28 | 29 | ### Attentive State 30 | 31 | The MSA will enter this state when it receives a summons from the user. It will 32 | travel to the output device nearest the user and wait expectantly for a command 33 | delivered to it via the I/O network. 34 | 35 | ### Debug State 36 | 37 | In this state, the MSA provides a direct command and query interface via 38 | command-line interface. A request to transition into this state overrides all 39 | other commands and immediately presents the Command Line Interface. Prior to 40 | entering debug state, the MSA will save its current state and resume it once 41 | the debug state is exited. 42 | 43 | ### Directed-Activity State 44 | 45 | This state is activity mode for entertainment. The MSA will undergo a variety of 46 | interactions and self-directed actions designed to appeal to the user without 47 | growing too repetitive. 48 | 49 | Verification can be enabled such that this state cannot be transitioned into 50 | without prior authorization. 51 | 52 | This state must be enabled manually while the MSA is in attentive state; the 53 | only way a transition to Directed-Activity state can be initiated is by explicit 54 | command from the user or explicit scheduling by the user. 55 | 56 | ### Conversing State 57 | 58 | In this state, the MSA will engage in conversation with the user. Each 59 | conversation will have a goal, such as entertainment, cheering up the user, etc. 60 | During the conversation, the sentiment of the user's responses will be analyzed 61 | to determine whether the previous MSA output was good or bad, and common 62 | patterns are stored for future use. 63 | 64 | During this state, the MSA can attempt to learn facts about the user, such as 65 | their hobbies, family, studies, work, daily life, etc. This information can then 66 | be used during future conversations. 67 | -------------------------------------------------------------------------------- /python/msa/server/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import WSCloseCode 3 | 4 | from msa.data import start_db_engine, stop_db_engine 5 | from msa.server.route_adapter import RouteAdapter 6 | from msa.server.default_routes import register_default_routes 7 | from msa.server.event_propagate import EventPropagationRouter 8 | 9 | from aiohttp import web 10 | 11 | # create global route adapter 12 | route_adapter_instance = RouteAdapter() 13 | register_default_routes(route_adapter_instance) 14 | 15 | 16 | async def start_supervisor(app): # pragma: no coverage 17 | app["supervisor"].start() 18 | 19 | 20 | async def stop_supervisor(app): # pragma: no coverage 21 | app["supervisor"].logger.info("*** trigger shutdown") 22 | await app["supervisor"].exit() 23 | 24 | 25 | async def on_shutdown(app): # pragma: no coverage 26 | for ws in set(app["websockets"]): 27 | await ws.close(code=WSCloseCode.GOING_AWAY, message="Server shutdown") 28 | 29 | 30 | def start_server(config_context): # pragma: no coverage 31 | app = web.Application() 32 | 33 | event_propagation_router = EventPropagationRouter() 34 | 35 | app["config_context"] = config_context 36 | 37 | # get loop and db 38 | loop = asyncio.get_event_loop() 39 | 40 | # init supervisor 41 | from msa import core as msa_core 42 | from msa.core.supervisor import Supervisor 43 | 44 | supervisor = Supervisor() 45 | msa_core.supervisor_instance = supervisor 46 | app["supervisor"] = supervisor 47 | supervisor.init(loop, app["config_context"], route_adapter_instance) 48 | loop.run_until_complete(start_db_engine()) 49 | 50 | event_propagation_router.register_propagate_subscription(supervisor.event_bus) 51 | 52 | # register routes 53 | route_adapter_instance.register_app(app) 54 | app.add_routes(route_adapter_instance.get_route_table()) 55 | 56 | # startup hooks 57 | app.on_startup.append(event_propagation_router.app_start) 58 | app.on_startup.append(start_supervisor) 59 | 60 | # onshutdown 61 | app.on_shutdown.append(on_shutdown) 62 | 63 | # cleanup routes 64 | app.on_cleanup.append(stop_supervisor) 65 | app.on_cleanup.append(event_propagation_router.app_stop) 66 | app.on_cleanup.append(stop_db_engine) 67 | 68 | try: 69 | import aiomonitor 70 | 71 | host, port = "127.0.0.1", 8080 72 | locals_ = {"port": port, "host": host} 73 | with aiomonitor.start_monitor(loop=loop, locals=locals_): 74 | # run application with built in aiohttp run_app function 75 | 76 | try: 77 | web.run_app(app, port=port, host=host) 78 | except Exception as e: 79 | print(e) 80 | finally: 81 | loop.close() 82 | 83 | except: 84 | try: 85 | web.run_app(app) 86 | except Exception as e: 87 | print(e) 88 | finally: 89 | loop.close() 90 | -------------------------------------------------------------------------------- /src/cmd/cmd.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_CMD_CMD_HPP 2 | #define MSA_CMD_CMD_HPP 3 | 4 | // Handles definitions and specifications of commands 5 | 6 | #include "msa.hpp" 7 | #include "cfg/cfg.hpp" 8 | #include "event/handler.hpp" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace msa { namespace cmd { 15 | 16 | class ParamList 17 | { 18 | public: 19 | ParamList(); 20 | ParamList(const std::vector &tokens, const std::string &opts); 21 | const std::string &command() const; 22 | const std::string &operator[](size_t index) const; 23 | const std::string &get_arg(size_t index) const; 24 | size_t arg_count() const; 25 | bool has_option(char opt) const; 26 | const std::string &get_option(char opt) const; 27 | size_t option_count(char opt) const; 28 | const std::vector &all_option_args(char opt) const; 29 | std::string str() const; 30 | 31 | private: 32 | std::string _command; 33 | std::vector _args; 34 | std::map> _options; 35 | }; 36 | 37 | class Result 38 | { 39 | public: 40 | Result(int status) : 41 | _status(status), 42 | _value() 43 | {} 44 | 45 | Result(const std::string &retval) : 46 | _status(0), 47 | _value(retval) 48 | {} 49 | 50 | Result(int status, const std::string &retval) : 51 | _status(status), 52 | _value(retval) 53 | {} 54 | 55 | const std::string &value() const 56 | { 57 | return _value; 58 | } 59 | 60 | int status() const 61 | { 62 | return _status; 63 | } 64 | 65 | private: 66 | int _status; 67 | std::string _value; 68 | 69 | }; 70 | 71 | typedef Result (*CommandHandler)(msa::Handle hdl, const ParamList &args, msa::event::HandlerSync *const sync); 72 | 73 | class Command 74 | { 75 | public: 76 | Command(const std::string &invoke, const std::string &desc, const std::string &usage, CommandHandler handler) : 77 | invoke(invoke), 78 | desc(desc), 79 | usage(usage), 80 | options(""), 81 | handler(handler) 82 | {} 83 | 84 | Command(const std::string &invoke, const std::string &desc, const std::string &usage, const std::string &options, CommandHandler handler) : 85 | invoke(invoke), 86 | desc(desc), 87 | usage(usage), 88 | options(options), 89 | handler(handler) 90 | {} 91 | 92 | std::string invoke; 93 | std::string desc; 94 | std::string usage; 95 | std::string options; 96 | CommandHandler handler; 97 | }; 98 | 99 | extern int init(msa::Handle hdl, const msa::cfg::Section &config); 100 | extern int quit(msa::Handle hdl); 101 | extern void register_command(msa::Handle hdl, const Command *cmd); 102 | extern void unregister_command(msa::Handle hdl, const Command *cmd); 103 | extern const PluginHooks *get_plugin_hooks(); 104 | 105 | #define MSA_MODULE_HOOK(retspec, name, ...) extern retspec name(__VA_ARGS__); 106 | #include "cmd/hooks.hpp" 107 | #undef MSA_MODULE_HOOK 108 | 109 | struct plugin_hooks_type 110 | { 111 | #define MSA_MODULE_HOOK(retspec, name, ...) retspec (*name)(__VA_ARGS__); 112 | #include "cmd/hooks.hpp" 113 | #undef MSA_MODULE_HOOK 114 | }; 115 | } } 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /python/msa/builtins/scripting/server_api.py: -------------------------------------------------------------------------------- 1 | from msa.core import get_supervisor 2 | from msa.server.server_response import ( 3 | ServerResponseText, 4 | ServerResponseType, 5 | ServerResponseJson, 6 | ) 7 | 8 | 9 | async def add_script(request): 10 | """ 11 | 12 | :param request: 13 | :return: 14 | """ 15 | from msa.builtins.scripting.events import AddScriptEvent 16 | 17 | new_event = AddScriptEvent().init(request.data) 18 | get_supervisor().fire_event(new_event) 19 | 20 | return ServerResponseText( 21 | ServerResponseType.success, f"{request.data['name']} was successfully uploaded" 22 | ) 23 | 24 | 25 | async def list_scripts(request): 26 | """ 27 | Lists scripts that are available to run on the daemon. 28 | 29 | :param request: The details of the incoming request. 30 | :type request: :class:`msa.server.route_adapter.ServerRequest` 31 | :return: A response to send to the client 32 | :rtype: :class:`Dict` or :class:`NoneType` 33 | """ 34 | 35 | from msa.builtins.scripting.events import TriggerListScriptsEvent, ListScriptsEvent 36 | 37 | new_event = TriggerListScriptsEvent().init(None) 38 | get_supervisor().fire_event(new_event) 39 | 40 | response_event = await get_supervisor().listen_for_result(ListScriptsEvent) 41 | 42 | return ServerResponseJson( 43 | ServerResponseType.success, payload={"scripts": response_event.data["scripts"]} 44 | ) 45 | 46 | 47 | async def get_script(request): 48 | """ 49 | Fetch a script that has been uploaded to the daemon and return it as a string 50 | 51 | :param request: The details of the incoming request. 52 | :type request: :class:`msa.server.route_adapter.ServerRequest` 53 | :return: A response to send to the client 54 | :rtype: :class:`Dict` or :class:`NoneType` 55 | """ 56 | 57 | from msa.builtins.scripting.events import TriggerGetScriptEvent, GetScriptEvent 58 | 59 | new_event = TriggerGetScriptEvent().init({"name": request.url_variables["name"]}) 60 | get_supervisor().fire_event(new_event) 61 | 62 | response_event = await get_supervisor().listen_for_result(GetScriptEvent) 63 | 64 | return ServerResponseJson( 65 | ServerResponseType.success, payload={"script": response_event.data} 66 | ) 67 | 68 | 69 | async def delete_script(request): 70 | """ 71 | Delete a script that has been uploaded to the daemon, cancelling it if it is running or scheduled to run. 72 | 73 | :param request: The details of the incoming request. 74 | :type request: :class:`msa.server.route_adapter.ServerRequest` 75 | :return: A response to send to the client 76 | :rtype: :class:`Dict` or :class:`NoneType` 77 | """ 78 | 79 | from msa.builtins.scripting.events import ( 80 | TriggerDeleteScriptEvent, 81 | ScriptDeletedEvent, 82 | ) 83 | 84 | new_event = TriggerDeleteScriptEvent().init({"name": request.url_variables["name"]}) 85 | get_supervisor().fire_event(new_event) 86 | 87 | response_event = await get_supervisor().listen_for_result(ScriptDeletedEvent) 88 | 89 | return ServerResponseJson(ServerResponseType.success, payload=response_event.data) 90 | 91 | 92 | def register_routes(route_binder): 93 | 94 | route_binder.post("/scripting/script")(add_script) 95 | route_binder.get("/scripting/script")(list_scripts) 96 | route_binder.get("/scripting/script/{name}")(get_script) 97 | route_binder.delete("/scripting/script/{name}")(delete_script) 98 | -------------------------------------------------------------------------------- /python/msa/api/base_methods.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | 4 | from msa.version import v as msa_version 5 | 6 | 7 | async def ping(self, quiet=False): 8 | """ 9 | Send's a ping to the daemon. If the ping succeeds, you should see a pong response. 10 | 11 | :async: 12 | :param bool quiet: Print's the response of `True`. 13 | :return: `None` 14 | """ 15 | response = await self.client.get("/ping") 16 | 17 | if not response: 18 | return 19 | 20 | if response.status != "success": 21 | raise Exception(response.raw) 22 | 23 | if not quiet: 24 | print(response.text) 25 | 26 | 27 | async def check_version(self, quiet=False): 28 | """ 29 | Called automatically at startup, ensures that the cli version and the daemon versions match. 30 | 31 | :async: 32 | :param bool quiet: Print's the response of `True`. 33 | :return: `None` 34 | """ 35 | response = await self.client.get("/version") 36 | 37 | if not response: 38 | print("Unable to verify server version. Exiting.") 39 | # we have been unable to connect to the server and should exit in this case 40 | exit(1) 41 | 42 | if response.status != "success": 43 | raise Exception(response.raw) 44 | 45 | if response.text != msa_version or not quiet: 46 | server_version = response.json["text"] 47 | print("Server Version:", server_version) 48 | print("Client Version:", msa_version) 49 | if server_version != msa_version: 50 | print("Warning: Client and server versions mismatch.\nExiting.", flush=True) 51 | exit(1) 52 | 53 | 54 | async def get_version(self): 55 | """ 56 | Fetches the daemon's version. 57 | 58 | :async: 59 | :return: `None` 60 | """ 61 | response = await self.client.get("/version") 62 | 63 | if not response: 64 | print("Unable to verify server version. Exiting.") 65 | # we have been unable to connect to the server and should exit in this case 66 | exit(1) 67 | 68 | if response.status != "success": 69 | raise Exception(response.raw) 70 | 71 | return response.text 72 | 73 | 74 | async def check_connection(self): 75 | """ 76 | Raises an exception if the cli cannot contact the daemon. 77 | 78 | :async: 79 | :return: `None` 80 | """ 81 | n = 0 82 | fail = 3 83 | while n < fail: 84 | try: 85 | await self.ping(quiet=True) 86 | if n > 0: 87 | print("There we go! Connection succeeded!") 88 | return 89 | except requests.exceptions.ConnectionError: 90 | n += 1 91 | 92 | if n == 1: 93 | print("Having trouble connecting... Trying again.") 94 | elif n == 2: 95 | print("Hmm, something must be up.") 96 | time.sleep(2) 97 | 98 | print( 99 | "Unfortunately I was unable to reach the msa daemon instance at {}".format( 100 | self.base_url 101 | ), 102 | flush=True, 103 | ) 104 | print("Exiting.", flush=True) 105 | exit(1) 106 | 107 | 108 | def register_base_methods(api_wrapper): 109 | 110 | api_wrapper.register_method()(ping) 111 | api_wrapper.register_method()(check_version) 112 | api_wrapper.register_method()(get_version) 113 | api_wrapper.register_method()(check_connection) 114 | -------------------------------------------------------------------------------- /python/tests/builtins/intents/handlers_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | from unittest.mock import patch, MagicMock 4 | 5 | from msa.builtins.intents.handlers import IntentToEventHandler 6 | from msa.builtins.intents.events import IntentEvent 7 | from msa.core.event import Event 8 | from schema import Schema 9 | 10 | 11 | class MyEvent(Event): 12 | def __init__(self): 13 | super().__init__(priority=50, schema=Schema({"prop_1": 1, "prop_2": "2"})) 14 | 15 | 16 | class IntentToEventHandlerTest(unittest.TestCase): 17 | @patch("msa.core.supervisor_instance") 18 | @patch("msa.builtins.intents.handlers.Event.__subclasses__") 19 | def test_converting_intent_to_event(self, Event_subclass_mock, SupervisorMock): 20 | 21 | loop = asyncio.get_event_loop() 22 | event_bus_mock = MagicMock() 23 | logger_mock = MagicMock() 24 | handler = IntentToEventHandler(loop, event_bus_mock, logger_mock) 25 | 26 | MyEvent.__module__ = "my_module" 27 | 28 | Event_subclass_mock.return_value = [MyEvent] 29 | 30 | incoming_event = IntentEvent().init( 31 | {"type": "my_module.MyEvent", "context": {"prop_1": 1, "prop_2": "2"}} 32 | ) 33 | 34 | loop.run_until_complete(handler.handle(incoming_event)) 35 | 36 | fired_event = SupervisorMock.fire_event.call_args_list[0][0][0] 37 | 38 | self.assertIsInstance(fired_event, MyEvent) 39 | self.assertIn("prop_1", fired_event.data) 40 | self.assertEqual(1, fired_event.data["prop_1"]) 41 | self.assertIn("prop_2", fired_event.data) 42 | self.assertEqual("2", fired_event.data["prop_2"]) 43 | 44 | @patch("msa.core.supervisor_instance") 45 | @patch("msa.builtins.intents.handlers.Event.__subclasses__") 46 | def test_class_not_found(self, Event_subclass_mock, SupervisorMock): 47 | 48 | loop = asyncio.get_event_loop() 49 | event_bus_mock = MagicMock() 50 | logger_mock = MagicMock() 51 | handler = IntentToEventHandler(loop, event_bus_mock, logger_mock) 52 | 53 | MyEvent.__module__ = "my_module" 54 | 55 | Event_subclass_mock.return_value = [MyEvent] 56 | 57 | incoming_event = IntentEvent().init( 58 | {"type": "my_module.MyOther", "context": {"prop_1": 1, "prop_2": "2"}} 59 | ) 60 | 61 | loop.run_until_complete(handler.handle(incoming_event)) 62 | 63 | SupervisorMock.fire_event.assert_not_called() 64 | 65 | logger_mock.warning.assert_called_once_with( 66 | f"Failed to translate Intent to Event. Could not find event: my_module.MyOther" 67 | ) 68 | 69 | @patch("msa.core.supervisor_instance") 70 | @patch("msa.builtins.intents.handlers.Event.__subclasses__") 71 | def test_converted_event_fails_to_validate( 72 | self, Event_subclass_mock, SupervisorMock 73 | ): 74 | 75 | loop = asyncio.get_event_loop() 76 | event_bus_mock = MagicMock() 77 | logger_mock = MagicMock() 78 | handler = IntentToEventHandler(loop, event_bus_mock, logger_mock) 79 | 80 | MyEvent.__module__ = "my_module" 81 | 82 | Event_subclass_mock.return_value = [MyEvent] 83 | 84 | incoming_event = IntentEvent().init( 85 | {"type": "my_module.MyEvent", "context": {"prop_1": 1, "prop_2": 2}} 86 | ) 87 | 88 | loop.run_until_complete(handler.handle(incoming_event)) 89 | 90 | SupervisorMock.fire_event.assert_not_called() 91 | 92 | logger_mock.exception.assert_called_once_with( 93 | "While attempting to convert an intent to an event, the event initialization failed " 94 | "due to a schema validation error" 95 | ) 96 | -------------------------------------------------------------------------------- /src/msa.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MSA_MSA_HPP 2 | #define MSA_MSA_HPP 3 | 4 | #include "debug_macros.hpp" 5 | 6 | // DO NOT INCLUDE ANY MSA HEADER FILES HERE! 7 | // If you find you have to, re-think your design. 8 | // It will break here. 9 | 10 | #define MSA_SUCCESS 0 11 | #define MSA_ERR_EVENT 1 12 | #define MSA_ERR_INPUT 2 13 | #define MSA_ERR_AGENT 3 14 | #define MSA_ERR_CONFIG 4 15 | #define MSA_ERR_CMD 5 16 | #define MSA_ERR_LOG 6 17 | #define MSA_ERR_OUTPUT 7 18 | #define MSA_ERR_PLUGIN 8 19 | 20 | namespace msa { 21 | 22 | namespace input { 23 | 24 | typedef struct input_context_type InputContext; 25 | typedef struct plugin_hooks_type PluginHooks; 26 | 27 | } 28 | 29 | namespace output { 30 | 31 | typedef struct output_context_type OutputContext; 32 | typedef struct plugin_hooks_type PluginHooks; 33 | 34 | } 35 | 36 | namespace event { 37 | 38 | typedef struct event_dispatch_context_type EventDispatchContext; 39 | struct TimerContext; 40 | typedef struct plugin_hooks_type PluginHooks; 41 | 42 | } 43 | 44 | namespace agent { 45 | 46 | typedef struct agent_context_type AgentContext; 47 | typedef struct plugin_hooks_type PluginHooks; 48 | 49 | } 50 | 51 | namespace cmd { 52 | 53 | typedef struct command_context_type CommandContext; 54 | typedef struct plugin_hooks_type PluginHooks; 55 | 56 | } 57 | 58 | namespace log { 59 | 60 | typedef struct log_context_type LogContext; 61 | typedef struct plugin_hooks_type PluginHooks; 62 | 63 | } 64 | 65 | namespace plugin { 66 | 67 | typedef struct plugin_context_type PluginContext; 68 | typedef struct plugin_hooks_type PluginHooks; 69 | 70 | } 71 | 72 | enum class Status 73 | { 74 | CREATED, 75 | RUNNING, 76 | STOP_REQUESTED, 77 | STOPPED 78 | }; 79 | 80 | struct environment_type 81 | { 82 | Status status; 83 | msa::event::EventDispatchContext *event; 84 | // timer is not actually a separate module from event, but we give it special status 85 | // because it must be able to get its context without relying on the event dispatch 86 | // module. 87 | msa::event::TimerContext *timer; 88 | msa::input::InputContext *input; 89 | msa::output::OutputContext *output; 90 | msa::agent::AgentContext *agent; 91 | msa::cmd::CommandContext *cmd; 92 | msa::log::LogContext *log; 93 | msa::plugin::PluginContext *plugin; 94 | }; 95 | 96 | typedef struct environment_type* Handle; 97 | 98 | typedef struct plugin_hooks_type 99 | { 100 | const msa::event::PluginHooks *event; 101 | const msa::input::PluginHooks *input; 102 | const msa::output::PluginHooks *output; 103 | const msa::agent::PluginHooks *agent; 104 | const msa::cmd::PluginHooks *cmd; 105 | const msa::log::PluginHooks *log; 106 | const msa::plugin::PluginHooks *plugin; 107 | } PluginHooks; 108 | 109 | // global library initializer. Must call before creating handles with start() 110 | extern void init(); 111 | 112 | // starts an MSA instance. 113 | extern int start(Handle *hdl, const char *config_path); 114 | 115 | // global library destructor. Must call in order to free global resources. Do 116 | // not call until all handles have been properly released with dispose() 117 | extern void quit(); 118 | 119 | // stops an MSA instance. This will invalidate most of the environment but the 120 | // status will still be accessible. Use dispose() to completely free the 121 | // handle. 122 | extern int stop(Handle hdl); 123 | 124 | // disposes a handle. Do not call on an active MSA handle; call stop() on 125 | // that handle first. 126 | extern int dispose(Handle hdl); 127 | 128 | // gets the global plugin hooks table 129 | extern const PluginHooks *get_plugin_hooks(); 130 | 131 | } 132 | 133 | #endif 134 | -------------------------------------------------------------------------------- /python/msa/api/patcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import asyncio 4 | from functools import partial 5 | 6 | from msa.api.context import ApiContext 7 | from msa.utils.asyncio_utils import run_async 8 | from msa.api.patchable_api import MsaApi 9 | 10 | from msa.api.base_methods import register_base_methods 11 | from msa.core.loader import load_builtin_modules, load_plugin_modules 12 | 13 | 14 | class ApiPatcher: 15 | cache = {} 16 | 17 | def __init__(self, api_context, api_client, plugin_whitelist): 18 | self.api = MsaApi() 19 | self.api.context = api_context 20 | self.api.client = api_client 21 | self.api.client.api = self.api 22 | self.plugin_whitelist = plugin_whitelist 23 | self.endpoints = [] 24 | 25 | self._registration_frozen = False 26 | self._process_registrations() 27 | self._registration_frozen = True 28 | 29 | @staticmethod 30 | def load(api_context, api_client=None, plugin_whitelist=None): 31 | if api_context is None: 32 | raise Exception(f"{ApiPatcher.__name__}: api_context cannot be None.") 33 | 34 | if api_context in ApiPatcher.cache: 35 | if api_client is not None: 36 | raise Exception( 37 | f"{ApiPatcher.__name__}: api_client cannot be non-None when context '{api_context}' already been patched and loaded." 38 | ) 39 | 40 | return ApiPatcher.cache[api_context].api 41 | 42 | else: 43 | if api_client is None: 44 | raise Exception( 45 | f"{ApiPatcher.__name__}: api_client cannot be None when context '{api_context}' has never been patched and loaded." 46 | ) 47 | if plugin_whitelist is None: 48 | raise Exception( 49 | f"{ApiPatcher.__name__}: plugin_whitelist cannot be None when context '{api_context}' has never been patched and loaded." 50 | ) 51 | 52 | ApiPatcher.cache[api_context] = ApiPatcher( 53 | api_context, api_client, plugin_whitelist 54 | ) 55 | return ApiPatcher.cache[api_context].api 56 | 57 | def _process_registrations(self): 58 | register_base_methods(self) 59 | 60 | for module in load_builtin_modules(): 61 | if hasattr(module, "register_client_api") and callable( 62 | module.register_client_api 63 | ): 64 | module.register_client_api(self) 65 | 66 | for module in load_plugin_modules(self.plugin_whitelist): 67 | if hasattr(module, "register_client_api") and callable( 68 | module.register_client_api 69 | ): 70 | module.register_client_api(self) 71 | 72 | def help_func(): 73 | # TODO enhance this a lot 74 | print("List of active functions:\n", "\n- ".join(self.endpoints)) 75 | 76 | self.api["help"] = help_func 77 | 78 | def register_method(self): 79 | if not self._registration_frozen: 80 | 81 | def decorator(func): 82 | async_name = func.__name__ 83 | async_func = partial(func, self.api) 84 | self.api[async_name] = async_func 85 | self.endpoints.append(async_name) 86 | 87 | if self.api.context is ApiContext.rest: 88 | sync_name = "sync_" + func.__name__ 89 | self.api[sync_name] = lambda *args, **kwargs: run_async( 90 | async_func(*args, **kwargs) 91 | ) 92 | self.endpoints.append(sync_name) 93 | 94 | return decorator 95 | else: 96 | raise Exception( 97 | f"MsaAPI Method Registration is frozen, failed to register method." 98 | ) 99 | -------------------------------------------------------------------------------- /python/msa/plugins/notifications/handlers.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from msa.core.event_handler import EventHandler 4 | from msa.plugins.notifications.events import ( 5 | SendNotificationEvent, 6 | SendPreferredNotificationEvent, 7 | ) 8 | from msa.plugins.notifications.notification_providers import NotificationProvider 9 | from msa.utils.asyncio_utils import sync_to_async 10 | 11 | try: 12 | from notifiers import get_notifier 13 | except: 14 | print( 15 | "notifications plugin, requires the notifications_plugin extra to be installed." 16 | ) 17 | exit() 18 | 19 | 20 | class SendNotificationEventHandler(EventHandler): 21 | """ 22 | Handles ConversationInputEvents 23 | """ 24 | 25 | def __init__(self, loop, event_bus, logger, config=None): 26 | super().__init__(loop, event_bus, logger, config) 27 | 28 | self.providers = {} 29 | 30 | for provider_type, provider_config in config["providers"].items(): 31 | provider_type_enum = NotificationProvider[provider_type] 32 | if provider_type_enum == NotificationProvider.pushbullet: 33 | pushbullet = get_notifier("pushbullet") 34 | 35 | part = partial(pushbullet.notify, token=provider_config["token"]) 36 | 37 | self.providers[NotificationProvider.pushbullet] = sync_to_async( 38 | lambda title, message, target: part(title=title, message=message) 39 | ) 40 | 41 | elif provider_type_enum == NotificationProvider.email: 42 | email = get_notifier("email") 43 | 44 | part = partial( 45 | email.notify, 46 | from_=provider_config["from"], 47 | username=provider_config.get("username"), 48 | password=provider_config.get("password"), 49 | ssl=provider_config.get("ssl", False), 50 | tls=provider_config.get("tls", False), 51 | ) 52 | 53 | self.providers[NotificationProvider.email] = sync_to_async( 54 | lambda title, message, target: part( 55 | subject=title, message=message, to=target 56 | ) 57 | ) 58 | 59 | elif provider_type_enum == NotificationProvider.slack: 60 | slack = get_notifier("slack") 61 | 62 | self.providers[NotificationProvider.slack] = sync_to_async( 63 | lambda title, message, target: slack.notify( 64 | message=f"{title}:\n{message}", channel=target 65 | ) 66 | ) 67 | 68 | preferred_provider = config.get("preferred_provider", None) 69 | if preferred_provider: 70 | preferred_provider_type = NotificationProvider[preferred_provider] 71 | self.preferred_provider = self.providers[preferred_provider_type] 72 | self.event_bus.subscribe( 73 | SendPreferredNotificationEvent, self.handle_preferred 74 | ) 75 | 76 | self.event_bus.subscribe(SendNotificationEvent, self.handle_notify) 77 | 78 | async def handle_notify(self, event): 79 | 80 | provider = event.data["provider"] 81 | provider_enum = NotificationProvider(provider) 82 | title = event.data["title"] 83 | message = event.data["message"] 84 | target = event.data.get("target", None) 85 | 86 | if provider_enum not in self.providers: 87 | self.logger.error( 88 | f"Incoming event refers to an unknown or a not configured provider: {provider}" 89 | ) 90 | 91 | await self.providers[provider_enum](title, message, target) 92 | 93 | async def handle_preferred(self, event): 94 | title = event.data["title"] 95 | message = event.data["message"] 96 | target = event.data.get("target", None) 97 | 98 | await self.preferred_provider(title, message, target) 99 | -------------------------------------------------------------------------------- /python/sphinx/configuration.md: -------------------------------------------------------------------------------- 1 | 2 | # Configuration File 3 | 4 | The configuration file is the easiest way to begin configuring MSA to your liking. The configuration file os a JSON 5 | file. JSON stands for JavaScript Object Notation, and is a common way of storing structured data. As tutorials on 6 | how to write JSON are easily found, we will avoid going into specifics with how json works here. The most you need to 7 | know is that JSON is a series of key -> value associations. 8 | 9 | This guide will refer to various nested configuration values in the config file, in order to easily reference a given 10 | JSON value we will use the following naming scheme: `agent.name` to refer to the `"Masa-chan"` value of 11 | `{"agent": { "name": "Masa-Chan" }}` easily. 12 | 13 | ## Configuration Values 14 | 15 | ### Agent 16 | The agent section configures the behavior and appearance of the agent. 17 | 18 | #### agent.name 19 | The name MSA will refer to itself as. 20 | 21 | Example: 22 | ```json 23 | { 24 | "agent": { 25 | "name": "Your humble servant" 26 | } 27 | } 28 | ``` 29 | 30 | #### agent.user_title 31 | The name MSA will refer to the user as. 32 | 33 | Example: 34 | ```json 35 | { 36 | "agent": { 37 | "user_title": "Supreme Leader" 38 | } 39 | } 40 | ``` 41 | 42 | ### Plugin Modules 43 | The plugin modules section, allows a user to configure which third-party plugins to load when MSA starts. It should 44 | be a list of plugin modules to load at startup. 45 | 46 | Example: 47 | ```json 48 | { 49 | "plugin_modules": [ 50 | "my_demo_plugin" 51 | ] 52 | } 53 | ``` 54 | 55 | 56 | ### Module Config 57 | The module config section is a mapping of module name to JSON object. The JSON object is configuration 58 | values that will be passed to the module to modify its behavior. 59 | 60 | Example: 61 | ```json 62 | { 63 | "module_config": { 64 | "my_demo_plugin": { 65 | "my_demo_message": "hello world" 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | ### Logging 72 | The logging section, allows you to configure how MSA will record information about how well it is running, 73 | It will also record any errors that are encountered. 74 | 75 | #### logging.global_log_level 76 | Sets the global log level. Must be one of "error", "warn", "info", or "debug". The global 77 | log level defines how verbose all modules will be with their logging. 78 | 79 | Example: 80 | ```json 81 | { 82 | "logging": { 83 | "global_log_level": "info" 84 | } 85 | } 86 | ``` 87 | 88 | #### logging.log_file_location 89 | The file that the logging output is written to. 90 | Example: 91 | ```json 92 | { 93 | "logging": { 94 | "log_file_location": "my_custom_file.log" 95 | } 96 | } 97 | ``` 98 | 99 | 100 | #### logging.truncate_log_file 101 | Toggles overwriting or truncating the log file when MSA starts up. If `false` log files will be preserved between runs. 102 | Example: 103 | ```json 104 | { 105 | "logging": { 106 | "truncate_log_file": false 107 | } 108 | } 109 | ``` 110 | 111 | 112 | #### logging.granular_log_levels 113 | A module to log level mapping that overrides the `logging.global_log_level` setting for that module. This can be used to 114 | increase logging or suppress a module that is logging too much unneeded information. Log level values must be one of 115 | "error", "warn", "info", or "debug". 116 | 117 | Example: 118 | ```json 119 | { 120 | "logging": { 121 | "granular_log_levels": [ 122 | { "namespace": "echo", "log_level": "debug"}, 123 | { "namespace": "command_registry", "log_level": "error"} 124 | ] 125 | } 126 | } 127 | ``` 128 | 129 | ## Example configuration 130 | ```json 131 | { 132 | "agent": { 133 | "name": "Masa-chan", 134 | "user_title": "Onee-chan" 135 | }, 136 | "plugin_modules": [ 137 | 138 | ], 139 | 140 | "module_config": { 141 | 142 | }, 143 | 144 | "logging": { 145 | "global_log_level": "info", 146 | "log_file_location": "msa.log", 147 | "truncate_log_file": false, 148 | "granular_log_levels": [ 149 | { "namespace": "echo", "log_level": "debug"}, 150 | { "namespace": "command_registry", "log_level": "error"} 151 | ] 152 | } 153 | } 154 | ``` 155 | 156 | 157 | -------------------------------------------------------------------------------- /python/msa/core/config_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from types import MappingProxyType 4 | from schema import Schema, And, Or, Use 5 | from pathlib import Path 6 | 7 | CONFIG_SCHEMA = Schema( 8 | { 9 | "agent": {"name": And(str, len), "user_title": And(str, len)}, 10 | "plugin_modules": [And(str, len)], 11 | "module_config": And(dict), 12 | # namespace of module -> dict of config values 13 | "logging": { 14 | "global_log_level": Or( 15 | None, 16 | And( 17 | str, 18 | Use(str.upper), 19 | lambda s: s in ("DEBUG", "INFO", "ERROR", "WARN"), 20 | ), 21 | ), 22 | "log_file_location": And(str, len), 23 | "granular_log_levels": [ 24 | { 25 | "namespace": And(str, len), 26 | "level": And( 27 | str, 28 | Use(str.upper), 29 | lambda s: s in ("DEBUG", "INFO", "ERROR", "WARN"), 30 | Use(lambda e: getattr(logging, e)), 31 | ), 32 | } 33 | ], 34 | "truncate_log_file": bool, 35 | }, 36 | } 37 | ) 38 | 39 | 40 | class ConfigManager: 41 | """A class that reads, validates, and exposes the application configuration. 42 | 43 | 44 | :param cli_overrides A dictionary of overrides for various config values. 45 | Supported config overrides: 46 | - *config_file*: The path to the msa config file. If not provided or the provided value is not a valid path to 47 | a file, ConfigManager will search $HOME/.msa_config.json and ./msa_config.json in that order. 48 | - *log_level* Override the root log level of MSA. Valid values are "debug", "info", "warn", "error" 49 | """ 50 | 51 | def __init__(self, cli_overrides): 52 | 53 | self.config_file = cli_overrides.get("config_file", None) 54 | self.cli_overrides = cli_overrides 55 | self.config = None 56 | 57 | self.logger = logging.getLogger("msa.core.config_manager.ConfigManager") 58 | 59 | self.load() 60 | self.apply_cli_overrides() 61 | self.validate() 62 | 63 | def load(self): 64 | """Loads the configuration value into memory. The file location is derived from the command line interface 65 | options.""" 66 | 67 | paths_to_check = [Path("./msa_config.json"), Path.home() / ".msa_config.json"] 68 | if self.config_file is not None: 69 | paths_to_check.insert(0, Path(self.config_file)) 70 | 71 | success = False 72 | for path in paths_to_check: 73 | self.logger.info(f"Searching path for msa config file: '{path}'") 74 | if not path.exists(): 75 | self.logger.warning( 76 | f"Provided config file path: {path} does not exist." 77 | ) 78 | continue 79 | 80 | if not path.is_file(): 81 | self.logger.warning(f"Provided config file path: {path} is not a file.") 82 | continue 83 | 84 | with path.open("r") as f: 85 | self.logger.info(f"Successfully loaded config file: {path}") 86 | self.config = json.load(f) 87 | success = True 88 | break 89 | 90 | if not success: 91 | msg = "Failed for find a valid msa config file." 92 | self.logger.error(msg) 93 | raise MissingConfigFileException(msg) 94 | 95 | def apply_cli_overrides(self): 96 | """Applies any command line interface overrides of configuration file values.""" 97 | for key, value in self.cli_overrides.items(): 98 | if key == "log_level" and value is not None: 99 | self.config["logging"]["global_log_level"] = value 100 | 101 | def validate(self): 102 | """Validates the configfile against the config schema.""" 103 | self.config = CONFIG_SCHEMA.validate(self.config) 104 | 105 | def get_config(self): 106 | """Returns the validated application configuration.""" 107 | return self.config 108 | 109 | 110 | class MissingConfigFileException(Exception): 111 | pass 112 | -------------------------------------------------------------------------------- /python/sphinx/using_the_cli.md: -------------------------------------------------------------------------------- 1 | # Using the CLI 2 | 3 | ## `cli` basics 4 | When you start the cli, you will be greated with something like the following: 5 | ```bash 6 | >>> 7 | ``` 8 | 9 | At it's core the cli is stripped down python interactive interpreter. Similar to if you just ran `python` on your terminal. 10 | There are some notable differences: 11 | - The presence of a `msa_api` object which is used for interacting with the daemon. 12 | - Some "meta-commands" for the cli that allow special functionality within the cli. This includes recording scripts and saving them. 13 | 14 | ## Intro to meta-commands 15 | 16 | Meta-commands are executed within the cli and do not directly touch the daemon. 17 | 18 | Available meta commands can be listed by typing `# help` in the cli e.g.: 19 | ```bash 20 | >>> # help 21 | MSA Interpreter Help: 22 | Availiable Commands: 23 | # help: Show this help text 24 | # record : Begin recording commands to a script. 25 | # record stop: stop recording commands, save the script, and open to review. 26 | ``` 27 | 28 | **Note:** the result of this `help` meta-command is subject to change as meta-commands are added or removed. 29 | 30 | See below for an example using meta-commands to record a script interactively and sending it to the daemon to run periodically. 31 | 32 | ## Intro to the `msa_api` object 33 | When you start the cli a `msa_api` object is inserted into scope to facilitate interaction with the daemon process. 34 | The `msa_api` object can be used to interface with the daemon process. 35 | 36 | ### Basic usage 37 | Below you can find a list of methods available on `msa_api`. 38 | 39 | The methods marked `async` below need to be prefixed with `await`. For example: 40 | ```python 41 | >>> await msa_api.talk("hello") 42 | ``` 43 | 44 | ### Recording and uploading scripts 45 | 46 | One of the meta-commands available in the cli is the `# record` command. 47 | 48 | As in the following example, it is possible to record an commands and save them as a script which can then be uploaded 49 | to the daemon. 50 | 51 | ```python 52 | $ moe-serifu-agent 53 | >>> # record test.py 54 | >>> await msa_api.talk("Hello, how are you?") 55 | I am well thank you. What can I do for you? 56 | >>> await msa_api.talk("Please turn on the livingroom light") 57 | I am afraid I don't know what to say. 58 | >>> # record stop 59 | Opening test.py 60 | ``` 61 | 62 | Now if we `ls` the current directory, we should see a new file `test.py` has been created, containing the following: 63 | ```python 64 | await msa_api.talk("Hello, how are you?") 65 | await msa_api.talk("Please turn on the livingroom light.") 66 | ``` 67 | 68 | We can also upload the script to the daemon and schedule it to run every so often. 69 | ```python 70 | >>> await msa_api.upload_script("demo_script", crontab="* * * * *", file_name="test.py") 71 | demo_script was sucessfully uploaded 72 | ``` 73 | 74 | The test script we uploaded should run once every minute. Unfortunately this is not very useful as we cannot see the 75 | conversation response from MSA because the daemon doesn't know where to send the response. Future tutorials will walk 76 | through some more interesting uses for uploaded scripts such as triggering events. 77 | 78 | One final note on uploading scripts is that they are saved to MSA's database and are reloaded at startup. 79 | 80 | 81 | ## `msa_api` Reference 82 | 83 | ```eval_rst 84 | .. py:class:: msa_api 85 | 86 | .. automodule:: msa.api.base_methods 87 | :members: 88 | :show-inheritance: 89 | 90 | .. automodule:: msa.builtins.conversation.client_api 91 | :members: 92 | :show-inheritance: 93 | 94 | .. automodule:: msa.builtins.scripting.client_api 95 | :members: 96 | :show-inheritance: 97 | 98 | .. automodule:: msa.builtins.signals.client_api 99 | :members: 100 | ``` 101 | 102 | -------------------------------------------------------------------------------- /python/tests/plugins/notifications/handlers_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | from unittest.mock import patch, MagicMock 4 | 5 | from msa.plugins.notifications.handlers import ( 6 | SendNotificationEventHandler, 7 | SendPreferredNotificationEvent, 8 | ) 9 | from msa.plugins.notifications.events import SendNotificationEvent 10 | from msa.core.event import Event 11 | from tests.async_test_util import AsyncMock 12 | from schema import Schema 13 | 14 | 15 | class IntentToEventHandlerTest(unittest.TestCase): 16 | @patch("msa.core.supervisor_instance") 17 | @patch("msa.plugins.notifications.handlers.get_notifier") 18 | def test_converting_intent_to_event(self, get_notifier_mock, SupervisorMock): 19 | 20 | loop = asyncio.get_event_loop() 21 | event_bus_mock = MagicMock() 22 | logger_mock = MagicMock() 23 | config = { 24 | "preferred_provider": "pushbullet", 25 | "providers": { 26 | "pushbullet": {"token": "abcd1234"}, 27 | "email": { 28 | "host": "http://test.com", 29 | "port": 0000, 30 | "from": "test@test.com", 31 | }, 32 | "slack": {"webhook_url": "http://slack.com/api/asdfasdfadf-fake"}, 33 | }, 34 | } 35 | 36 | # set up notify mocks 37 | 38 | notifiers = { 39 | "pushbullet": MagicMock(), 40 | "email": MagicMock(), 41 | "slack": MagicMock(), 42 | } 43 | 44 | call_args = { 45 | "pushbullet": [ 46 | "test", 47 | "test message", 48 | config["providers"]["pushbullet"]["token"], 49 | ], 50 | "email": [ 51 | "test", 52 | "test message", 53 | "test_target", 54 | config["providers"]["email"]["from"], 55 | None, 56 | None, 57 | False, 58 | False, 59 | ], 60 | "slack": ["test:\ntest_message" "test_target"], 61 | } 62 | 63 | get_notifier_mock.side_effect = lambda k: notifiers[k] 64 | 65 | handler = SendNotificationEventHandler( 66 | loop, event_bus_mock, logger_mock, config 67 | ) 68 | 69 | events_data = [ 70 | { 71 | "provider": prov, 72 | "title": "test", 73 | "message": "test message", 74 | "target": "test_target", 75 | } 76 | for prov in ["pushbullet", "email", "slack"] 77 | ] 78 | 79 | events = [SendNotificationEvent().init(data) for data in events_data] 80 | 81 | for event in events: 82 | loop.run_until_complete(handler.handle_notify(event)) 83 | 84 | for name, notifier in notifiers.items(): 85 | notifier.notify.called_once_with(*call_args[name]) 86 | 87 | @patch("msa.core.supervisor_instance") 88 | @patch("msa.plugins.notifications.handlers.get_notifier") 89 | def test_preferrred_provider(self, get_notifier_mock, SupervisorMock): 90 | 91 | loop = asyncio.get_event_loop() 92 | event_bus_mock = MagicMock() 93 | logger_mock = MagicMock() 94 | config = { 95 | "preferred_provider": "pushbullet", 96 | "providers": { 97 | "pushbullet": {"token": "abcd1234"}, 98 | "email": { 99 | "host": "http://test.com", 100 | "port": 0000, 101 | "from": "test@test.com", 102 | }, 103 | "slack": {"webhook_url": "http://slack.com/api/asdfasdfadf-fake"}, 104 | }, 105 | } 106 | 107 | # set up notify mocks 108 | 109 | notifiers = {"pushbullet": MagicMock()} 110 | 111 | call_args = { 112 | "pushbullet": [ 113 | "test", 114 | "test message", 115 | config["providers"]["pushbullet"]["token"], 116 | ] 117 | } 118 | 119 | get_notifier_mock.side_effect = lambda k: notifiers.get(k, MagicMock()) 120 | 121 | handler = SendNotificationEventHandler( 122 | loop, event_bus_mock, logger_mock, config 123 | ) 124 | 125 | event_data = { 126 | "title": "test", 127 | "message": "test message", 128 | "target": "test_target", 129 | } 130 | 131 | event = SendPreferredNotificationEvent().init(event_data) 132 | 133 | loop.run_until_complete(handler.handle_preferred(event)) 134 | 135 | for name, notifier in notifiers.items(): 136 | notifier.notify.called_once_with(*call_args[name]) 137 | -------------------------------------------------------------------------------- /src/agent/agent.cpp: -------------------------------------------------------------------------------- 1 | #include "agent/agent.hpp" 2 | #include "log/log.hpp" 3 | #include "output/output.hpp" 4 | #include "util/var.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace msa { namespace agent { 11 | 12 | static const PluginHooks HOOKS = { 13 | #define MSA_MODULE_HOOK(retspec, name, ...) name, 14 | #include "agent/hooks.hpp" 15 | #undef MSA_MODULE_HOOK 16 | }; 17 | 18 | struct agent_context_type 19 | { 20 | Agent *agent; 21 | std::string user_title; 22 | msa::var::Expander *expander; 23 | }; 24 | 25 | Agent::agent_type(const std::string &n) : name(n), state(State::IDLE), attitude(0), mood(Mood::NORMAL) 26 | {} 27 | 28 | static int create_agent_context(AgentContext **ctx); 29 | static int dispose_agent_context(AgentContext *ctx); 30 | static void read_config(msa::Handle hdl, const msa::cfg::Section &config); 31 | static void add_default_substitutions(msa::Handle hdl); 32 | static void remove_default_substitutions(msa::Handle hdl); 33 | 34 | extern int init(msa::Handle hdl, const msa::cfg::Section &config) 35 | { 36 | int status = create_agent_context(&hdl->agent); 37 | if (status != 0) 38 | { 39 | msa::log::error(hdl, "Could not create agent context"); 40 | return status; 41 | } 42 | try 43 | { 44 | read_config(hdl, config); 45 | } 46 | catch (const msa::cfg::config_error &e) 47 | { 48 | msa::log::error(hdl, "Could not read agent config: " + std::string(e.what())); 49 | return -1; 50 | } 51 | add_default_substitutions(hdl); 52 | return 0; 53 | } 54 | 55 | extern int quit(msa::Handle hdl) 56 | { 57 | remove_default_substitutions(hdl); 58 | int status = dispose_agent_context(hdl->agent); 59 | if (status != 0) 60 | { 61 | msa::log::error(hdl, "Could not dispose agent context"); 62 | return status; 63 | } 64 | return 0; 65 | } 66 | 67 | extern const PluginHooks *get_plugin_hooks() 68 | { 69 | return &HOOKS; 70 | } 71 | 72 | extern const Agent *get_agent(msa::Handle hdl) 73 | { 74 | return hdl->agent->agent; 75 | } 76 | 77 | extern void print_prompt_char(msa::Handle hdl) 78 | { 79 | std::string output_text = "> "; 80 | msa::var::expand(hdl->agent->expander, output_text); 81 | msa::output::write_text(hdl, output_text); 82 | fflush(stdout); 83 | } 84 | 85 | extern void say(msa::Handle hdl, const std::string &text) 86 | { 87 | std::string output_text = "$AGENT_NAME: \"" + text + "\"\n"; 88 | msa::var::expand(hdl->agent->expander, output_text); 89 | msa::output::write_text(hdl, output_text); 90 | } 91 | 92 | extern void register_substitution(msa::Handle hdl, const std::string &name) 93 | { 94 | AgentContext *ctx = hdl->agent; 95 | msa::var::register_internal(ctx->expander, name); 96 | } 97 | 98 | extern void set_substitution(msa::Handle hdl, const std::string &name, const std::string &value) 99 | { 100 | AgentContext *ctx = hdl->agent; 101 | msa::var::set_value(ctx->expander, name, value); 102 | } 103 | 104 | extern void unregister_substitution(msa::Handle hdl, const std::string &name) 105 | { 106 | AgentContext *ctx = hdl->agent; 107 | msa::var::unregister_internal(ctx->expander, name); 108 | } 109 | 110 | extern void get_substitutions(msa::Handle hdl, std::vector &subs) 111 | { 112 | AgentContext *ctx = hdl->agent; 113 | msa::var::get_defined(ctx->expander, subs); 114 | } 115 | 116 | static int create_agent_context(AgentContext **ctx_ptr) 117 | { 118 | AgentContext *ctx = new AgentContext; 119 | ctx->agent = NULL; 120 | msa::var::create_expander(&ctx->expander); 121 | *ctx_ptr = ctx; 122 | return 0; 123 | } 124 | 125 | static int dispose_agent_context(AgentContext *ctx) 126 | { 127 | if (ctx->agent != NULL) 128 | { 129 | delete ctx->agent; 130 | } 131 | msa::var::dispose_expander(ctx->expander); 132 | delete ctx; 133 | return 0; 134 | } 135 | 136 | static void read_config(msa::Handle hdl, const msa::cfg::Section &config) 137 | { 138 | std::string name = config.get_or("NAME", "DEFAULT_NAME"); 139 | std::string user_title = config.get_or("USER_TITLE", "Master"); 140 | hdl->agent->agent = new Agent(name); 141 | hdl->agent->user_title = user_title; 142 | } 143 | 144 | static void add_default_substitutions(msa::Handle hdl) 145 | { 146 | AgentContext *ctx = hdl->agent; 147 | msa::var::register_external(ctx->expander, "USER_TITLE", &ctx->user_title); 148 | msa::var::register_external(ctx->expander, "AGENT_NAME", &ctx->agent->name); 149 | } 150 | 151 | static void remove_default_substitutions(msa::Handle hdl) 152 | { 153 | AgentContext *ctx = hdl->agent; 154 | msa::var::unregister_external(ctx->expander, "USER_TITLE"); 155 | msa::var::unregister_external(ctx->expander, "AGENT_NAME"); 156 | } 157 | 158 | } } 159 | --------------------------------------------------------------------------------