From a644c90d7409f48c49fbd5ddca7ecfe35de15953 Mon Sep 17 00:00:00 2001 From: Malik Talha Date: Thu, 2 Nov 2023 03:11:43 +0500 Subject: Update voice agent service Modify default configuration, add detailed file based logging functionality, and use placeholders instead of fixed values in default config. Bug-AGL: SPEC-4906 Signed-off-by: Malik Talha Change-Id: Ib75af153555e7cdde38c67414df8326799e22c8d --- .gitignore | 6 +- README.md | 11 +++- agl_service_voiceagent/client.py | 31 +++++----- agl_service_voiceagent/config.ini | 12 ++-- agl_service_voiceagent/nlu/rasa_interface.py | 2 +- agl_service_voiceagent/server.py | 6 +- agl_service_voiceagent/service.py | 70 ++++++++++++++-------- .../servicers/voice_agent_servicer.py | 50 +++++++++++++++- agl_service_voiceagent/utils/config.py | 30 +++++++++- agl_service_voiceagent/utils/kuksa_interface.py | 35 ++++++++--- agl_service_voiceagent/utils/mapper.py | 10 +++- setup.py | 2 +- 12 files changed, 195 insertions(+), 70 deletions(-) diff --git a/.gitignore b/.gitignore index de438c0..2035a32 100644 --- a/.gitignore +++ b/.gitignore @@ -168,9 +168,7 @@ cython_debug/ # model dirs *-model/ -# grpc files - -generated/ - # logs dir logs/ + +generated/ diff --git a/README.md b/README.md index adc48ee..2bd6082 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,17 @@ To interact with the gRPC server, you can run the client in different modes: - Wake-word Mode: Detects wake words and triggers voice commands. - Manual Mode: Manually control voice command recognition. -To run the client in a specific mode, use the following command: +To run the client in a Wake-word mode, use the following command: +```bash +voiceagent-service run-client --server_address SERVER_IP --server_port SERVER_PORT --mode wake-word +``` +Replace `SERVER_IP` with IP address of the running Voice Agent server, and `SERVER_PORT` with the port of the running Voice Agent server. +To run the client in Manual mode, use the following command: ```bash -voiceagent-service run-client --mode MODE --nlu NLU_ENGINE +voiceagent-service run-client --server_address SERVER_IP --server_port SERVER_PORT --mode manual --nlu NLU_ENGINE ``` -Replace MODE with the desired mode (e.g., "wake-word") and NLU_ENGINE with the preferred NLU engine (e.g., "snips"). +Replace `NLU_ENGINE` with the preferred NLU engine ("snips" or "rasa"), `SERVER_IP` with IP address of the running Voice Agent server, and `SERVER_PORT` with the port of the running Voice Agent server. You can also pass a custom value to flag `--recording-time` if you want to change the default recording time from 5 seconds to any other value. ## Configuration Configuration options for the AGL Voice Agent Service can be found in the default `config.ini` file. You can customize various settings, including the AI models, audio directories, and Kuksa integration. **Important:** while manually making changes to the config file make sure you add trailing slash to all the directory paths, ie. the paths to directories should always end with a `/`. diff --git a/agl_service_voiceagent/client.py b/agl_service_voiceagent/client.py index 922e08c..12804e1 100644 --- a/agl_service_voiceagent/client.py +++ b/agl_service_voiceagent/client.py @@ -18,11 +18,10 @@ import time import grpc from agl_service_voiceagent.generated import voice_agent_pb2 from agl_service_voiceagent.generated import voice_agent_pb2_grpc -from agl_service_voiceagent.utils.config import get_config_value -def run_client(mode, nlu_model): - SERVER_URL = get_config_value('SERVER_ADDRESS') + ":" + str(get_config_value('SERVER_PORT')) - nlu_model = voice_agent_pb2.SNIPS if nlu_model == "snips" else voice_agent_pb2.RASA +def run_client(server_address, server_port, mode, nlu_engine, recording_time): + SERVER_URL = server_address + ":" + server_port + nlu_engine = voice_agent_pb2.RASA if nlu_engine == "rasa" else voice_agent_pb2.SNIPS print("Starting Voice Agent Client...") print(f"Client connecting to URL: {SERVER_URL}") with grpc.insecure_channel(SERVER_URL) as channel: @@ -30,7 +29,7 @@ def run_client(mode, nlu_model): print("Voice Agent Client started!") if mode == 'wake-word': stub = voice_agent_pb2_grpc.VoiceAgentServiceStub(channel) - print("Listening for wake word...") + print("[+] Listening for wake word...") wake_request = voice_agent_pb2.Empty() wake_results = stub.DetectWakeWord(wake_request) wake_word_detected = False @@ -42,18 +41,20 @@ def run_client(mode, nlu_model): break elif mode == 'auto': - raise ValueError("Auto mode is not implemented yet.") + raise ValueError("[-] Auto mode is not implemented yet.") elif mode == 'manual': stub = voice_agent_pb2_grpc.VoiceAgentServiceStub(channel) - print("Recording voice command...") - record_start_request = voice_agent_pb2.RecognizeControl(action=voice_agent_pb2.START, nlu_model=nlu_model, record_mode=voice_agent_pb2.MANUAL) + print("[+] Recording voice command in manual mode...") + record_start_request = voice_agent_pb2.RecognizeControl(action=voice_agent_pb2.START, nlu_model=nlu_engine, record_mode=voice_agent_pb2.MANUAL) response = stub.RecognizeVoiceCommand(iter([record_start_request])) stream_id = response.stream_id - time.sleep(5) # any arbitrary pause here - record_stop_request = voice_agent_pb2.RecognizeControl(action=voice_agent_pb2.STOP, nlu_model=nlu_model, record_mode=voice_agent_pb2.MANUAL, stream_id=stream_id) + + time.sleep(recording_time) # pause here for the number of seconds passed by user or default 5 seconds + + record_stop_request = voice_agent_pb2.RecognizeControl(action=voice_agent_pb2.STOP, nlu_model=nlu_engine, record_mode=voice_agent_pb2.MANUAL, stream_id=stream_id) record_result = stub.RecognizeVoiceCommand(iter([record_stop_request])) - print("Voice command recorded!") + print("[+] Voice command recording ended!") status = "Uh oh! Status is unknown." if record_result.status == voice_agent_pb2.REC_SUCCESS: @@ -64,8 +65,8 @@ def run_client(mode, nlu_model): status = "Intent not recognized." # Process the response - print("Command:", record_result.command) print("Status:", status) + print("Command:", record_result.command) print("Intent:", record_result.intent) intent_slots = [] for slot in record_result.intent_slots: @@ -74,5 +75,7 @@ def run_client(mode, nlu_model): i_slot = voice_agent_pb2.IntentSlot(name=slot.name, value=slot.value) intent_slots.append(i_slot) - exec_voice_command_request = voice_agent_pb2.ExecuteInput(intent=record_result.intent, intent_slots=intent_slots) - response = stub.ExecuteVoiceCommand(exec_voice_command_request) \ No newline at end of file + if record_result.status == voice_agent_pb2.REC_SUCCESS: + print("[+] Executing voice command...") + exec_voice_command_request = voice_agent_pb2.ExecuteInput(intent=record_result.intent, intent_slots=intent_slots) + response = stub.ExecuteVoiceCommand(exec_voice_command_request) \ No newline at end of file diff --git a/agl_service_voiceagent/config.ini b/agl_service_voiceagent/config.ini index 074f6a8..81d4e69 100644 --- a/agl_service_voiceagent/config.ini +++ b/agl_service_voiceagent/config.ini @@ -1,17 +1,17 @@ [General] base_audio_dir = /usr/share/nlu/commands/ -stt_model_path = /usr/share/vosk/vosk-model-small-en-us-0.15/ -wake_word_model_path = /usr/share/vosk/vosk-model-small-en-us-0.15/ +stt_model_path = /usr/share/vosk/VOSK_STT_MODEL_NAME/ +wake_word_model_path = /usr/share/vosk/VOSK_WWD_MODEL_NAME/ snips_model_path = /usr/share/nlu/snips/model/ channels = 1 sample_rate = 16000 bits_per_sample = 16 -wake_word = hello auto +wake_word = WAKE_WORD_VALUE server_port = 51053 server_address = 127.0.0.1 rasa_model_path = /usr/share/nlu/rasa/models/ rasa_server_port = 51054 -rasa_detached_mode = 0 +rasa_detached_mode = 1 base_log_dir = /usr/share/nlu/logs/ store_voice_commands = 0 @@ -20,8 +20,8 @@ ip = 127.0.0.1 port = 8090 protocol = ws insecure = True -token = /usr/lib/python3.10/site-packages/kuksa_certificates/jwt/super-admin.json.token +token = PYTHON_DIR/kuksa_certificates/jwt/super-admin.json.token [Mapper] intents_vss_map = /usr/share/nlu/mappings/intents_vss_map.json -vss_signals_spec = /usr/share/nlu/mappings/vss_signals_spec.json \ No newline at end of file +vss_signals_spec = /usr/share/nlu/mappings/vss_signals_spec.json diff --git a/agl_service_voiceagent/nlu/rasa_interface.py b/agl_service_voiceagent/nlu/rasa_interface.py index 537a318..350bddf 100644 --- a/agl_service_voiceagent/nlu/rasa_interface.py +++ b/agl_service_voiceagent/nlu/rasa_interface.py @@ -40,7 +40,7 @@ class RASAInterface: self.max_threads = max_threads self.server_process = None self.thread_pool = ThreadPoolExecutor(max_workers=max_threads) - self.log_file = log_dir+"rasa_server_logs.txt" + self.log_file = log_dir+"rasa_server.log" def _start_server(self): diff --git a/agl_service_voiceagent/server.py b/agl_service_voiceagent/server.py index d8ce785..aa107dc 100644 --- a/agl_service_voiceagent/server.py +++ b/agl_service_voiceagent/server.py @@ -18,12 +18,12 @@ import grpc from concurrent import futures from agl_service_voiceagent.generated import voice_agent_pb2_grpc from agl_service_voiceagent.servicers.voice_agent_servicer import VoiceAgentServicer -from agl_service_voiceagent.utils.config import get_config_value +from agl_service_voiceagent.utils.config import get_config_value, get_logger def run_server(): + logger = get_logger() SERVER_URL = get_config_value('SERVER_ADDRESS') + ":" + str(get_config_value('SERVER_PORT')) print("Starting Voice Agent Service...") - print(f"Server running at URL: {SERVER_URL}") print(f"STT Model Path: {get_config_value('STT_MODEL_PATH')}") print(f"Audio Store Directory: {get_config_value('BASE_AUDIO_DIR')}") server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) @@ -31,5 +31,7 @@ def run_server(): server.add_insecure_port(SERVER_URL) print("Press Ctrl+C to stop the server.") print("Voice Agent Server started!") + print(f"Server running at URL: {SERVER_URL}") + logger.info(f"Voice Agent Service started in server mode! Server running at URL: {SERVER_URL}") server.start() server.wait_for_termination() \ No newline at end of file diff --git a/agl_service_voiceagent/service.py b/agl_service_voiceagent/service.py index 784d8d9..9682b56 100644 --- a/agl_service_voiceagent/service.py +++ b/agl_service_voiceagent/service.py @@ -25,15 +25,14 @@ generated_dir = os.path.join(current_dir, "generated") sys.path.append(generated_dir) import argparse -from agl_service_voiceagent.utils.config import set_config_path, load_config, update_config_value, get_config_value +from agl_service_voiceagent.utils.config import set_config_path, load_config, update_config_value, get_config_value, get_logger from agl_service_voiceagent.utils.common import add_trailing_slash from agl_service_voiceagent.server import run_server from agl_service_voiceagent.client import run_client - def print_version(): print("Automotive Grade Linux (AGL)") - print(f"Voice Agent Service v0.3.0") + print(f"Voice Agent Service v0.4.0") def main(): @@ -59,8 +58,11 @@ def main(): server_parser.add_argument('--audio-store-dir', required=False, help='Directory to store the generated audio files.') server_parser.add_argument('--log-store-dir', required=False, help='Directory to store the generated log files.') + client_parser.add_argument('--server-address', required=True, help='Address of the gRPC server running the Voice Agent Service.') + client_parser.add_argument('--server-port', required=True, help='Port of the gRPC server running the Voice Agent Service.') client_parser.add_argument('--mode', required=True, help='Mode to run the client in. Supported modes: "wake-word", "auto" and "manual".') - client_parser.add_argument('--nlu', required=True, help='NLU engine to use. Supported NLU egnines: "snips" and "rasa".') + client_parser.add_argument('--nlu', help='NLU engine to use. Supported NLU egnines: "snips" and "rasa".') + client_parser.add_argument('--recording-time', help='Number of seconds to continue recording the voice command. Required by the \'manual\' mode. Defaults to 10 seconds.') args = parser.parse_args() @@ -70,19 +72,24 @@ def main(): elif args.subcommand == 'run-server': if not args.default and not args.config: if not args.stt_model_path: - raise ValueError("The --stt-model-path is missing. Please provide a value. Use --help to see available options.") + print("Error: The --stt-model-path is missing. Please provide a value. Use --help to see available options.") + exit(1) if not args.snips_model_path: - raise ValueError("The --snips-model-path is missing. Please provide a value. Use --help to see available options.") + print("Error: The --snips-model-path is missing. Please provide a value. Use --help to see available options.") + exit(1) if not args.rasa_model_path: - raise ValueError("The --rasa-model-path is missing. Please provide a value. Use --help to see available options.") + print("Error: The --rasa-model-path is missing. Please provide a value. Use --help to see available options.") + exit(1) if not args.intents_vss_map_path: - raise ValueError("The --intents-vss-map-path is missing. Please provide a value. Use --help to see available options.") + print("Error: The --intents-vss-map-path is missing. Please provide a value. Use --help to see available options.") + exit(1) if not args.vss_signals_spec_path: - raise ValueError("The --vss-signals-spec is missing. Please provide a value. Use --help to see available options.") + print("Error: The --vss-signals-spec-path is missing. Please provide a value. Use --help to see available options.") + exit(1) # Contruct the default config file path config_path = os.path.join(current_dir, "config.ini") @@ -90,6 +97,9 @@ def main(): # Load the config values from the config file set_config_path(config_path) load_config() + + logger = get_logger() + logger.info("Starting Voice Agent Service in server mode using CLI provided params...") # Get the values provided by the user stt_path = args.stt_model_path @@ -135,6 +145,9 @@ def main(): print(f"New config file path provided: {cli_config_path}. Overriding the default config file path.") set_config_path(cli_config_path) load_config() + + logger = get_logger() + logger.info(f"Starting Voice Agent Service in server mode using provided config file at path '{cli_config_path}' ...") elif args.default: # Contruct the default config file path @@ -144,33 +157,40 @@ def main(): set_config_path(config_path) load_config() + logger = get_logger() + logger.info(f"Starting Voice Agent Service in server mode using the default config file...") + # create the base audio dir if not exists if not os.path.exists(get_config_value('BASE_AUDIO_DIR')): os.makedirs(get_config_value('BASE_AUDIO_DIR')) - - # create the base log dir if not exists - if not os.path.exists(get_config_value('BASE_LOG_DIR')): - os.makedirs(get_config_value('BASE_LOG_DIR')) run_server() elif args.subcommand == 'run-client': - # Contruct the default config file path - config_path = os.path.join(current_dir, "config.ini") - - # Load the config values from the config file - set_config_path(config_path) - load_config() - + server_address = args.server_address + server_port = args.server_port + nlu_engine = "" mode = args.mode + recording_time = 5 + if mode not in ['wake-word', 'auto', 'manual']: - raise ValueError("Invalid mode. Supported modes: 'wake-word', 'auto' and 'manual'. Use --help to see available options.") + print("Error: Invalid value for --mode. Supported modes: 'wake-word', 'auto' and 'manual'. Use --help to see available options.") + exit(1) - model = args.nlu - if model not in ['snips', 'rasa']: - raise ValueError("Invalid NLU engine. Supported NLU engines: 'snips' and 'rasa'. Use --help to see available options.") + if mode in ["auto", "manual"]: + if not args.nlu: + print("Error: The --nlu flag is missing. Please provide a value for intent engine. Supported NLU engines: 'snips' and 'rasa'. Use --help to see available options.") + exit(1) + + nlu_engine = args.nlu + if nlu_engine not in ['snips', 'rasa']: + print("Error: Invalid value for --nlu. Supported NLU engines: 'snips' and 'rasa'. Use --help to see available options.") + exit(1) + + if mode == "manual" and args.recording_time: + recording_time = int(args.recording_time) - run_client(mode, model) + run_client(server_address, server_port, mode, nlu_engine, recording_time) else: print_version() diff --git a/agl_service_voiceagent/servicers/voice_agent_servicer.py b/agl_service_voiceagent/servicers/voice_agent_servicer.py index 69af10b..c9b671d 100644 --- a/agl_service_voiceagent/servicers/voice_agent_servicer.py +++ b/agl_service_voiceagent/servicers/voice_agent_servicer.py @@ -24,7 +24,7 @@ from agl_service_voiceagent.utils.wake_word import WakeWordDetector from agl_service_voiceagent.utils.stt_model import STTModel from agl_service_voiceagent.utils.kuksa_interface import KuksaInterface from agl_service_voiceagent.utils.mapper import Intent2VSSMapper -from agl_service_voiceagent.utils.config import get_config_value +from agl_service_voiceagent.utils.config import get_config_value, get_logger from agl_service_voiceagent.utils.common import generate_unique_uuid, delete_file from agl_service_voiceagent.nlu.snips_interface import SnipsInterface from agl_service_voiceagent.nlu.rasa_interface import RASAInterface @@ -40,7 +40,7 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): Constructor for VoiceAgentServicer class. """ # Get the config values - self.service_version = "v0.3.0" + self.service_version = "v0.4.0" self.wake_word = get_config_value('WAKE_WORD') self.base_audio_dir = get_config_value('BASE_AUDIO_DIR') self.channels = int(get_config_value('CHANNELS')) @@ -54,28 +54,48 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): self.rasa_detached_mode = bool(int(get_config_value('RASA_DETACHED_MODE'))) self.base_log_dir = get_config_value('BASE_LOG_DIR') self.store_voice_command = bool(int(get_config_value('STORE_VOICE_COMMANDS'))) + self.logger = get_logger() # Initialize class methods + self.logger.info("Loading Speech to Text and Wake Word Model...") self.stt_model = STTModel(self.stt_model_path, self.sample_rate) self.stt_wake_word_model = STTModel(self.wake_word_model_path, self.sample_rate) + self.logger.info("Speech to Text and Wake Word Model loaded successfully.") + + self.logger.info("Starting SNIPS intent engine...") self.snips_interface = SnipsInterface(self.snips_model_path) + self.logger.info("SNIPS intent engine started successfully!") + self.rasa_interface = RASAInterface(self.rasa_server_port, self.rasa_model_path, self.base_log_dir) # Only start RASA server if its not in detached mode, else we assume server is already running if not self.rasa_detached_mode: + self.logger.info(f"Starting RASA intent engine server as a subprocess...") self.rasa_interface.start_server() + self.logger.info(f"RASA intent engine server started successfully! RASA server running at URL: 127.0.0.1:{self.rasa_server_port}") + + else: + self.logger.info(f"RASA intent engine detached mode detected! Assuming RASA server is running at URL: 127.0.0.1:{self.rasa_server_port}") self.rvc_stream_uuids = {} self.kuksa_client = KuksaInterface() self.kuksa_client.connect_kuksa_client() self.kuksa_client.authorize_kuksa_client() + + self.logger.info(f"Loading and parsing mapping files...") self.mapper = Intent2VSSMapper() + self.logger.info(f"Successfully loaded and parsed mapping files.") def CheckServiceStatus(self, request, context): """ Check the status of the Voice Agent service including the version. """ + # Log the unique request ID, client's IP address, and the endpoint + request_id = generate_unique_uuid(8) + client_ip = context.peer() + self.logger.info(f"[ReqID#{request_id}] Client {client_ip} made a request to CheckServiceStatus end-point.") + response = voice_agent_pb2.ServiceStatus( version=self.service_version, status=True @@ -87,6 +107,11 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): """ Detect the wake word using the wake word detection model. """ + # Log the unique request ID, client's IP address, and the endpoint + request_id = generate_unique_uuid(8) + client_ip = context.peer() + self.logger.info(f"[ReqID#{request_id}] Client {client_ip} made a request to DetectWakeWord end-point.") + wake_word_detector = WakeWordDetector(self.wake_word, self.stt_model, self.channels, self.sample_rate, self.bits_per_sample) wake_word_detector.create_pipeline() detection_thread = threading.Thread(target=wake_word_detector.start_listening) @@ -118,6 +143,11 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): if request.action == voice_agent_pb2.START: status = voice_agent_pb2.REC_PROCESSING stream_uuid = generate_unique_uuid(8) + + # Log the unique request ID, client's IP address, and the endpoint + client_ip = context.peer() + self.logger.info(f"[ReqID#{stream_uuid}] Client {client_ip} made a manual START request to RecognizeVoiceCommand end-point.") + recorder = AudioRecorder(self.stt_model, self.base_audio_dir, self.channels, self.sample_rate, self.bits_per_sample) recorder.set_pipeline_mode("manual") audio_file = recorder.create_pipeline() @@ -133,6 +163,10 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): stream_uuid = request.stream_id status = voice_agent_pb2.REC_SUCCESS + # Log the unique request ID, client's IP address, and the endpoint + client_ip = context.peer() + self.logger.info(f"[ReqID#{stream_uuid}] Client {client_ip} made a manual STOP request to RecognizeVoiceCommand end-point.") + recorder = self.rvc_stream_uuids[stream_uuid]["recorder"] audio_file = self.rvc_stream_uuids[stream_uuid]["audio_file"] del self.rvc_stream_uuids[stream_uuid] @@ -189,6 +223,11 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): """ Execute the voice command by sending the intent to Kuksa. """ + # Log the unique request ID, client's IP address, and the endpoint + request_id = generate_unique_uuid(8) + client_ip = context.peer() + self.logger.info(f"[ReqID#{request_id}] Client {client_ip} made a request to ExecuteVoiceCommand end-point.") + intent = request.intent intent_slots = request.intent_slots processed_slots = [] @@ -203,6 +242,13 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): exec_response = f"Sorry, I failed to execute command against intent '{intent}'. Maybe try again with more specific instructions." exec_status = voice_agent_pb2.EXEC_ERROR + # Check for kuksa status, and try re-connecting again if status is False + if not self.kuksa_client.get_kuksa_status(): + self.logger.error(f"[ReqID#{request_id}] Kuksa client found disconnected. Trying to close old instance and re-connecting...") + self.kuksa_client.close_kuksa_client() + self.kuksa_client.connect_kuksa_client() + self.kuksa_client.authorize_kuksa_client() + for execution_item in execution_list: print(execution_item) action = execution_item["action"] diff --git a/agl_service_voiceagent/utils/config.py b/agl_service_voiceagent/utils/config.py index 7295c7f..e0b053e 100644 --- a/agl_service_voiceagent/utils/config.py +++ b/agl_service_voiceagent/utils/config.py @@ -14,10 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import logging import configparser config = configparser.ConfigParser() config_path = None +logger = None def set_config_path(path): """ @@ -25,14 +28,25 @@ def set_config_path(path): """ global config_path config_path = path - config.read(config_path) def load_config(): """ - Loads the config file. + Loads the config file and initializes the logger. + + Also creates logging directory if it doesn't already exist. """ if config_path is not None: + global logger config.read(config_path) + + # create the base log dir if not exists + if not os.path.exists(get_config_value('BASE_LOG_DIR')): + os.makedirs(get_config_value('BASE_LOG_DIR')) + + logging.basicConfig(filename=get_config_value('BASE_LOG_DIR')+'voiceagent_server.log', level=logging.DEBUG, format='[%(asctime)s] [%(name)s] [%(levelname)s]: (%(filename)s:%(funcName)s) %(message)s', filemode='a') + logger = logging.getLogger() + logger.info("-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-") + else: raise Exception("Config file path not provided.") @@ -52,3 +66,15 @@ def get_config_value(key, group="General"): Gets a value from the config file. """ return config.get(group, key) + +def get_logger(): + """ + Gets the initialized logger. + """ + if logger is not None: + return logger + + else: + logging.basicConfig(level=logging.DEBUG) + print("[-] Error: Failed to get logger. Logger is not initialized!") + logging.error("Failed to get logger. Logger is not initialized!") diff --git a/agl_service_voiceagent/utils/kuksa_interface.py b/agl_service_voiceagent/utils/kuksa_interface.py index 9270379..0881660 100644 --- a/agl_service_voiceagent/utils/kuksa_interface.py +++ b/agl_service_voiceagent/utils/kuksa_interface.py @@ -18,7 +18,7 @@ import time import json import threading from kuksa_client import KuksaClientThread -from agl_service_voiceagent.utils.config import get_config_value +from agl_service_voiceagent.utils.config import get_config_value, get_logger class KuksaInterface: """ @@ -55,6 +55,7 @@ class KuksaInterface: self.insecure = get_config_value("insecure", "Kuksa") self.protocol = get_config_value("protocol", "Kuksa") self.token = get_config_value("token", "Kuksa") + self.logger = get_logger() print(self.ip, self.port, self.insecure, self.protocol, self.token) @@ -102,11 +103,14 @@ class KuksaInterface: if not self.get_kuksa_status(): print("[-] Error: Connection to Kuksa server failed.") + self.logger.error("Connection to Kuksa server failed.") else: print("[+] Connection to Kuksa established.") + self.logger.info("Connection to Kuksa established.") except Exception as e: print("[-] Error: Connection to Kuksa server failed. ", str(e)) + self.logger.error(f"Connection to Kuksa server failed. {str(e)}") def authorize_kuksa_client(self): @@ -119,10 +123,13 @@ class KuksaInterface: if "error" in response: error_message = response.get("error", "Unknown error") print(f"[-] Error: Authorization failed. {error_message}") + self.logger.error(f"Authorization failed. {error_message}") else: print("[+] Kuksa client authorized successfully.") + self.logger.info("Kuksa client authorized successfully.") else: print("[-] Error: Kuksa client is not initialized. Call `connect_kuksa_client` first.") + self.logger.error("Kuksa client is not initialized. Call `connect_kuksa_client` first.") def send_values(self, path=None, value=None): @@ -137,7 +144,8 @@ class KuksaInterface: """ result = False if self.kuksa_client is None: - print("[-] Error: Kuksa client is not initialized.") + print(f"[-] Error: Failed to send value '{value}' to Kuksa. Kuksa client is not initialized.") + self.logger.error(f"Failed to send value '{value}' to Kuksa. Kuksa client is not initialized.") return if self.get_kuksa_status(): @@ -150,12 +158,15 @@ class KuksaInterface: else: error_message = response.get("error", "Unknown error") print(f"[-] Error: Failed to send value '{value}' to Kuksa. {error_message}") + self.logger.error(f"Failed to send value '{value}' to Kuksa. {error_message}") except Exception as e: - print("[-] Error: Failed to send values to Kuksa. ", str(e)) + print(f"[-] Error: Failed to send value '{value}' to Kuksa. ", str(e)) + self.logger.error(f"Failed to send value '{value}' to Kuksa. {str(e)}") else: - print("[-] Error: Connection to Kuksa failed.") + print(f"[-] Error: Failed to send value '{value}' to Kuksa. Connection to Kuksa failed.") + self.logger.error(f"Failed to send value '{value}' to Kuksa. Connection to Kuksa failed.") return result @@ -171,7 +182,8 @@ class KuksaInterface: """ result = None if self.kuksa_client is None: - print("[-] Error: Kuksa client is not initialized.") + print(f"[-] Error: Failed to get value at path '{path}' from Kuksa. Kuksa client is not initialized.") + self.logger.error(f"Failed to get value at path '{path}' from Kuksa. Kuksa client is not initialized.") return if self.get_kuksa_status(): @@ -185,13 +197,16 @@ class KuksaInterface: else: error_message = response.get("error", "Unknown error") - print(f"[-] Error: Failed to get value from Kuksa. {error_message}") + print(f"[-] Error: Failed to get value at path '{path}' from Kuksa. {error_message}") + self.logger.error(f"Failed to get value at path '{path}' from Kuksa. {error_message}") except Exception as e: - print("[-] Error: Failed to get values from Kuksa. ", str(e)) + print(f"[-] Error: Failed to get value at path '{path}' from Kuksa. ", str(e)) + self.logger.error(f"Failed to get value at path '{path}' from Kuksa. {str(e)}") else: - print("[-] Error: Connection to Kuksa failed.") + print(f"[-] Error: Failed to get value at path '{path}' from Kuksa. Connection to Kuksa failed.") + self.logger.error(f"Failed to get value at path '{path}' from Kuksa. Connection to Kuksa failed.") return result @@ -206,5 +221,7 @@ class KuksaInterface: self.kuksa_client.stop() self.kuksa_client = None print("[+] Kuksa client stopped.") + self.logger.info("Kuksa client stopped.") except Exception as e: - print("[-] Error: Failed to close Kuksa client. ", str(e)) \ No newline at end of file + print("[-] Error: Failed to close Kuksa client. ", str(e)) + self.logger.error(f"Failed to close Kuksa client. {str(e)}") \ No newline at end of file diff --git a/agl_service_voiceagent/utils/mapper.py b/agl_service_voiceagent/utils/mapper.py index 7529645..f24f44f 100644 --- a/agl_service_voiceagent/utils/mapper.py +++ b/agl_service_voiceagent/utils/mapper.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from agl_service_voiceagent.utils.config import get_config_value +from agl_service_voiceagent.utils.config import get_config_value, get_logger from agl_service_voiceagent.utils.common import load_json_file, words_to_number @@ -33,6 +33,7 @@ class Intent2VSSMapper: vss_signals_spec_file = get_config_value("vss_signals_spec", "Mapper") self.intents_vss_map = load_json_file(intents_vss_map_file).get("intents", {}) self.vss_signals_spec = load_json_file(vss_signals_spec_file).get("signals", {}) + self.logger = get_logger() if not self.validate_signal_spec_structure(): raise ValueError("[-] Invalid VSS signal specification structure.") @@ -49,6 +50,7 @@ class Intent2VSSMapper: # Check if the required keys are present in the signal data if not all(key in signal_data for key in ['default_value', 'default_change_factor', 'actions', 'values', 'default_fallback', 'value_set_intents']): print(f"[-] {signal_name}: Missing required keys in signal data.") + self.logger.error(f"{signal_name}: Missing required keys in signal data.") return False actions = signal_data['actions'] @@ -56,12 +58,14 @@ class Intent2VSSMapper: # Check if 'actions' is a dictionary with at least one action if not isinstance(actions, dict) or not actions: print(f"[-] {signal_name}: Invalid 'actions' key in signal data. Must be an object with at least one action.") + self.logger.error(f"{signal_name}: Invalid 'actions' key in signal data. Must be an object with at least one action.") return False # Check if the actions match the allowed actions ["set", "increase", "decrease"] for action in actions.keys(): if action not in ["set", "increase", "decrease"]: print(f"[-] {signal_name}: Invalid action in signal data. Allowed actions: ['set', 'increase', 'decrease']") + self.logger.error(f"{signal_name}: Invalid action in signal data. Allowed actions: ['set', 'increase', 'decrease']") return False # Check if the 'synonyms' list is present for each action and is either a list or None @@ -69,6 +73,7 @@ class Intent2VSSMapper: synonyms = action_data.get('synonyms') if synonyms is not None and (not isinstance(synonyms, list) or not all(isinstance(synonym, str) for synonym in synonyms)): print(f"[-] {signal_name}: Invalid 'synonyms' value in signal data. Must be a list of strings.") + self.logger.error(f"{signal_name}: Invalid 'synonyms' value in signal data. Must be a list of strings.") return False values = signal_data['values'] @@ -76,11 +81,13 @@ class Intent2VSSMapper: # Check if 'values' is a dictionary with the required keys if not isinstance(values, dict) or not all(key in values for key in ['ranged', 'start', 'end', 'ignore', 'additional']): print(f"[-] {signal_name}: Invalid 'values' key in signal data. Required keys: ['ranged', 'start', 'end', 'ignore', 'additional']") + self.logger.error(f"{signal_name}: Invalid 'values' key in signal data. Required keys: ['ranged', 'start', 'end', 'ignore', 'additional']") return False # Check if 'ranged' is a boolean if not isinstance(values['ranged'], bool): print(f"[-] {signal_name}: Invalid 'ranged' value in signal data. Allowed values: [true, false]") + self.logger.error(f"{signal_name}: Invalid 'ranged' value in signal data. Allowed values: [true, false]") return False default_fallback = signal_data['default_fallback'] @@ -88,6 +95,7 @@ class Intent2VSSMapper: # Check if 'default_fallback' is a boolean if not isinstance(default_fallback, bool): print(f"[-] {signal_name}: Invalid 'default_fallback' value in signal data. Allowed values: [true, false]") + self.logger.error(f"{signal_name}: Invalid 'default_fallback' value in signal data. Allowed values: [true, false]") return False # If all checks pass, the self.vss_signals_spec structure is valid diff --git a/setup.py b/setup.py index 2c8bf18..99f6ace 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ packages = [p for p in find_packages() setup( name="agl_service_voiceagent", - version="0.3.0", + version="0.4.0", description="A gRPC-based voice agent service designed for Automotive Grade Linux (AGL). This service leverages GStreamer, Vosk, Snips, and RASA to seamlessly process user voice commands. It converts spoken words into text, extracts intents from these commands, and performs actions through the Kuksa interface.", url="https://github.com/malik727/agl-service-voiceagent", author="Malik Talha", -- cgit 1.2.3-korg