diff --git a/examples/ros-service-call-adapter/adapter/config.py b/examples/ros-service-call-adapter/adapter/config.py index 8a8f1f4fd..77cfccd97 100644 --- a/examples/ros-service-call-adapter/adapter/config.py +++ b/examples/ros-service-call-adapter/adapter/config.py @@ -1,13 +1,52 @@ from json import load as json_load - - +import json +from json_schema_validator import JsonSchemaValidator +from formant.sdk.agent.v1.client import Client as AgentClient class Config: """The config class loads the config.json from memory.""" + def callabck(self, config): + self.config_raw = config + def __init__(self): - with open("config.json", "r") as f: - self.config_json = json_load(f) + + agentClient = AgentClient() + + JsonSchemaValidator( + agentClient, + "Ros Service Call Adapter", + self.callback, + True, + False, + ) + + self._config = {} + self._config["service-command"] = ["rosservice"] + self._config["api-button-mapping"] = {} + self._config["ros-button-mapping"] = {} + + for ros_button_config in self.config_raw["ros-button-mapping"]: + topic = ros_button_config["topic"] + button_config_raw = ros_button_config["button-config"] + + try: + button_config_json = json.loads(button_config_raw) + except json.decoder.JSONDecodeError: + continue + + self._config["ros-button-mapping"][topic] = button_config_json + + for api_button_config in self.config_raw["api-button-mapping"]: + button_name = api_button_config["api_button_name"] + button_config_raw = api_button_config["button-config"] + + try: + button_config_json = json.loads(button_config_raw) + except json.decoder.JSONDecodeError: + continue + + self._config["api-button-mapping"][button_name] = button_config_json def get_config(self): - return self.config_json + return self._config diff --git a/examples/ros-service-call-adapter/adapter/config_schema.json b/examples/ros-service-call-adapter/adapter/config_schema.json new file mode 100644 index 000000000..9fa1d2b08 --- /dev/null +++ b/examples/ros-service-call-adapter/adapter/config_schema.json @@ -0,0 +1,51 @@ +{ + "title": "Ros Service Call Adapter Configuration", + "description": "Configuration for button toggle adapter.", + "type": "object", + "properties": { + "ros-button-mapping": { + "type": "array", + "title": "ROS Buttons", + "description": "Buttons that are configured to call a ros service", + "items": { + "title": "ROS Button", + "description": "ROS Button that is configured to call a ros service", + "type": "object", + "properties": { + "topic": { + "type": "string", + "title": "Button Topic", + "description": "Topic to listening for button presses on" + }, + "button-config": { + "type": "string", + "title": "ROS Service Configuration", + "description": "Configuration for the ROS service to call" + } + } + } + }, + "api-button-mapping": { + "type": "array", + "title": "API Buttons", + "description": "API Buttons configured to call a ros service", + "items": { + "title": "API Button", + "description": "API Buttons configured to call a ros service", + "type": "object", + "properties": { + "api_button_name": { + "type": "string", + "title": "API Button Name", + "description": "Name of button" + }, + "button-config": { + "type": "string", + "title": "ROS Service Configuration", + "description": "Configuration for the ROS service to call" + } + } + } + } + } +} \ No newline at end of file diff --git a/examples/ros-service-call-adapter/adapter/json_schema_validator.py b/examples/ros-service-call-adapter/adapter/json_schema_validator.py new file mode 100644 index 000000000..29ef6f4f4 --- /dev/null +++ b/examples/ros-service-call-adapter/adapter/json_schema_validator.py @@ -0,0 +1,142 @@ +from typing import Callable, Dict, Optional, Type +import json +import jsonschema +import logging +from threading import Lock +from functools import reduce +import re + +from formant.sdk.agent.v1.client import Client as AgentClient + +logging.basicConfig() + +JSONDecodeError = Exception # type: Type[Exception] +try: + JSONDecodeError = json.decoder.JSONDecodeError +except AttributeError: + # python2 doesn't have JSON Decode Error + pass + + +class JsonSchemaValidator: + def __init__( + self, + client, # type: AgentClient + adapter_name, # type: str + update_adapter_config_callback, # type: Callable[[Dict], None] + validate=True, # type: bool + use_app_config=True, # type: bool + logger=None, # type: Optional[logging.Logger] + logger_level=logging.INFO, # type: int + ): + self._lock = Lock() + self._client = client + self._adapter_name = adapter_name + self._update_adapter_config_callback = update_adapter_config_callback + self._use_app_config = use_app_config + self._validate = validate + if logger is None: + logger = logging.getLogger(adapter_name) + logger.setLevel(logger_level) + self.logger = logger + self._client.register_config_update_callback(self._update_adapter_config) + if self._client.ignore_unavailable: + self._update_adapter_config() + + def _update_adapter_config(self): + # Consumer might not be threadsafe + with self._lock: + try: + configs = [ + self._get_config_from_adapter_configuration(), + self._get_config_from_json(), + ] + config = reduce(lambda s1, s2: s1 or s2, configs) + if config is None: + raise Exception( + "Could not get configuration for '%s'" % self._adapter_name + ) + if self._use_app_config: + config = self._inject_app_config(config) + if self._validate: + self._validate_schema(config) + try: + self._update_adapter_config_callback(config) + except Exception as e: + self.logger.error( + "Error calling update config callback %s" % str(e) + ) + except Exception as e: + self.logger.warn("Failed to load config: %s" % str(e)) + self._client.create_event( + "%s configuration loading failed: %s." + % (self._adapter_name, str(e)), + notify=False, + severity="warning", + ) + + def _get_config_from_adapter_configuration(self): + self.logger.info("Trying to get config from adapter config") + try: + adapters = self._client.get_agent_configuration().document.adapters + for adapter in adapters: + try: + config = json.loads(adapter.configuration) + except: + continue + if self._adapter_name in adapter.name: + self.logger.info("Got config from adapter config") + return config + except Exception as e: + self.logger.warn("Error getting config from adapter config: %s" % str(e)) + return None + + def _inject_app_config(self, config: Dict): + """ + This function replaces all instances of + {{key}} with `key` from app_config + """ + config_string = json.dumps(config) + pattern = r"\{\{(.+?)\}\}" + matches = re.findall(pattern, config_string) + for match in matches: + val = self._client.get_app_config(match) + config_string = config_string.replace("{{%s}}" % match, val) + return json.loads(config_string) + + def _get_config_from_json(self): + # Try to get config from config.json + self.logger.info("Trying to get config from config.json file") + try: + with open("config.json") as f: + config = json.loads(f.read()) + return config + except Exception as e: + self.logger.info("Error getting config from config.json: %s" % str(e)) + return None + + def _validate_schema(self, config_blob): + # Validate configuration based on schema + with open("config_schema.json") as f: + try: + self.config_schema = json.load(f) + self.logger.info("Loaded config schema from config_schema.json file") + except JSONDecodeError as e: + self.logger.warn( + "Could not load config schema. Is the file valid json?" + ) + raise Exception("config schema error: %s" % str(e)) + self.logger.info("Validating config...") + try: + jsonschema.validate(config_blob, self.config_schema) + self.logger.info("Validation succeeded") + except ( + jsonschema.ValidationError, + jsonschema.SchemaError, + jsonschema.RefResolutionError, + ) as e: + self.logger.warn("Validation failed %s: %s", type(e).__name__, str(e)) + except Exception as e: + self.logger.warn( + "Validation failed with unkown error %s: %s", type(e).__name__, str(e) + ) \ No newline at end of file diff --git a/examples/ros-service-call-adapter/config_schema.json b/examples/ros-service-call-adapter/config_schema.json new file mode 100644 index 000000000..9fa1d2b08 --- /dev/null +++ b/examples/ros-service-call-adapter/config_schema.json @@ -0,0 +1,51 @@ +{ + "title": "Ros Service Call Adapter Configuration", + "description": "Configuration for button toggle adapter.", + "type": "object", + "properties": { + "ros-button-mapping": { + "type": "array", + "title": "ROS Buttons", + "description": "Buttons that are configured to call a ros service", + "items": { + "title": "ROS Button", + "description": "ROS Button that is configured to call a ros service", + "type": "object", + "properties": { + "topic": { + "type": "string", + "title": "Button Topic", + "description": "Topic to listening for button presses on" + }, + "button-config": { + "type": "string", + "title": "ROS Service Configuration", + "description": "Configuration for the ROS service to call" + } + } + } + }, + "api-button-mapping": { + "type": "array", + "title": "API Buttons", + "description": "API Buttons configured to call a ros service", + "items": { + "title": "API Button", + "description": "API Buttons configured to call a ros service", + "type": "object", + "properties": { + "api_button_name": { + "type": "string", + "title": "API Button Name", + "description": "Name of button" + }, + "button-config": { + "type": "string", + "title": "ROS Service Configuration", + "description": "Configuration for the ROS service to call" + } + } + } + } + } +} \ No newline at end of file diff --git a/examples/ros-service-call-adapter/json_schema_validator.py b/examples/ros-service-call-adapter/json_schema_validator.py new file mode 100644 index 000000000..29ef6f4f4 --- /dev/null +++ b/examples/ros-service-call-adapter/json_schema_validator.py @@ -0,0 +1,142 @@ +from typing import Callable, Dict, Optional, Type +import json +import jsonschema +import logging +from threading import Lock +from functools import reduce +import re + +from formant.sdk.agent.v1.client import Client as AgentClient + +logging.basicConfig() + +JSONDecodeError = Exception # type: Type[Exception] +try: + JSONDecodeError = json.decoder.JSONDecodeError +except AttributeError: + # python2 doesn't have JSON Decode Error + pass + + +class JsonSchemaValidator: + def __init__( + self, + client, # type: AgentClient + adapter_name, # type: str + update_adapter_config_callback, # type: Callable[[Dict], None] + validate=True, # type: bool + use_app_config=True, # type: bool + logger=None, # type: Optional[logging.Logger] + logger_level=logging.INFO, # type: int + ): + self._lock = Lock() + self._client = client + self._adapter_name = adapter_name + self._update_adapter_config_callback = update_adapter_config_callback + self._use_app_config = use_app_config + self._validate = validate + if logger is None: + logger = logging.getLogger(adapter_name) + logger.setLevel(logger_level) + self.logger = logger + self._client.register_config_update_callback(self._update_adapter_config) + if self._client.ignore_unavailable: + self._update_adapter_config() + + def _update_adapter_config(self): + # Consumer might not be threadsafe + with self._lock: + try: + configs = [ + self._get_config_from_adapter_configuration(), + self._get_config_from_json(), + ] + config = reduce(lambda s1, s2: s1 or s2, configs) + if config is None: + raise Exception( + "Could not get configuration for '%s'" % self._adapter_name + ) + if self._use_app_config: + config = self._inject_app_config(config) + if self._validate: + self._validate_schema(config) + try: + self._update_adapter_config_callback(config) + except Exception as e: + self.logger.error( + "Error calling update config callback %s" % str(e) + ) + except Exception as e: + self.logger.warn("Failed to load config: %s" % str(e)) + self._client.create_event( + "%s configuration loading failed: %s." + % (self._adapter_name, str(e)), + notify=False, + severity="warning", + ) + + def _get_config_from_adapter_configuration(self): + self.logger.info("Trying to get config from adapter config") + try: + adapters = self._client.get_agent_configuration().document.adapters + for adapter in adapters: + try: + config = json.loads(adapter.configuration) + except: + continue + if self._adapter_name in adapter.name: + self.logger.info("Got config from adapter config") + return config + except Exception as e: + self.logger.warn("Error getting config from adapter config: %s" % str(e)) + return None + + def _inject_app_config(self, config: Dict): + """ + This function replaces all instances of + {{key}} with `key` from app_config + """ + config_string = json.dumps(config) + pattern = r"\{\{(.+?)\}\}" + matches = re.findall(pattern, config_string) + for match in matches: + val = self._client.get_app_config(match) + config_string = config_string.replace("{{%s}}" % match, val) + return json.loads(config_string) + + def _get_config_from_json(self): + # Try to get config from config.json + self.logger.info("Trying to get config from config.json file") + try: + with open("config.json") as f: + config = json.loads(f.read()) + return config + except Exception as e: + self.logger.info("Error getting config from config.json: %s" % str(e)) + return None + + def _validate_schema(self, config_blob): + # Validate configuration based on schema + with open("config_schema.json") as f: + try: + self.config_schema = json.load(f) + self.logger.info("Loaded config schema from config_schema.json file") + except JSONDecodeError as e: + self.logger.warn( + "Could not load config schema. Is the file valid json?" + ) + raise Exception("config schema error: %s" % str(e)) + self.logger.info("Validating config...") + try: + jsonschema.validate(config_blob, self.config_schema) + self.logger.info("Validation succeeded") + except ( + jsonschema.ValidationError, + jsonschema.SchemaError, + jsonschema.RefResolutionError, + ) as e: + self.logger.warn("Validation failed %s: %s", type(e).__name__, str(e)) + except Exception as e: + self.logger.warn( + "Validation failed with unkown error %s: %s", type(e).__name__, str(e) + ) \ No newline at end of file