diff options
-rw-r--r-- | README.md | 16 | ||||
-rw-r--r-- | agl_service_voiceagent/client.py | 93 | ||||
-rw-r--r-- | agl_service_voiceagent/config.ini | 7 | ||||
-rw-r--r-- | agl_service_voiceagent/protos/voice_agent.proto | 30 | ||||
-rw-r--r-- | agl_service_voiceagent/service.py | 26 | ||||
-rw-r--r-- | agl_service_voiceagent/servicers/voice_agent_servicer.py | 136 | ||||
-rw-r--r-- | agl_service_voiceagent/utils/config.py | 7 | ||||
-rw-r--r-- | agl_service_voiceagent/utils/kuksa_interface.py | 66 | ||||
-rw-r--r-- | agl_service_voiceagent/utils/mapper.py | 22 | ||||
-rw-r--r-- | mappings/vss_signals_spec.json | 24 |
10 files changed, 342 insertions, 85 deletions
@@ -48,19 +48,21 @@ voiceagent-service run-server --config CONFIG_FILE_PATH ``` #### Running the Client -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 interact with the gRPC server, you can run the client by specifying one of the following actions: +- GetStatus: Get the current status of the Voice Agent service. +- DetectWakeWord: Detect wake-word from the user's voice. +- ExecuteVoiceCommand: Execute a voice command from the user. +- ExecuteTextCommand: Execute a text command from the user. -To run the client in a Wake-word mode, use the following command: +To test out the WakeWord functionality, use the following command: ```bash -voiceagent-service run-client --server_address SERVER_IP --server_port SERVER_PORT --mode wake-word +voiceagent-service run-client --server_address SERVER_IP --server_port SERVER_PORT --action DetectWakeWord ``` 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: +To issue a voice command, use the following command: ```bash -voiceagent-service run-client --server_address SERVER_IP --server_port SERVER_PORT --mode manual --nlu NLU_ENGINE +voiceagent-service run-client --server_address SERVER_IP --server_port SERVER_PORT --action ExecuteVoiceCommand --mode manual --nlu NLU_ENGINE ``` 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. diff --git a/agl_service_voiceagent/client.py b/agl_service_voiceagent/client.py index 12804e1..88ef785 100644 --- a/agl_service_voiceagent/client.py +++ b/agl_service_voiceagent/client.py @@ -19,7 +19,7 @@ import grpc from agl_service_voiceagent.generated import voice_agent_pb2 from agl_service_voiceagent.generated import voice_agent_pb2_grpc -def run_client(server_address, server_port, mode, nlu_engine, recording_time): +def run_client(server_address, server_port, action, 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...") @@ -27,7 +27,16 @@ def run_client(server_address, server_port, mode, nlu_engine, recording_time): with grpc.insecure_channel(SERVER_URL) as channel: print("Press Ctrl+C to stop the client.") print("Voice Agent Client started!") - if mode == 'wake-word': + if action == 'GetStatus': + stub = voice_agent_pb2_grpc.VoiceAgentServiceStub(channel) + print("[+] Checking status...") + status_request = voice_agent_pb2.Empty() + status_result = stub.CheckServiceStatus(status_request) + print("Version:", status_result.version) + print("Status:", status_result.status) + print("Wake Word:", status_result.wake_word) + + elif action == 'DetectWakeWord': stub = voice_agent_pb2_grpc.VoiceAgentServiceStub(channel) print("[+] Listening for wake word...") wake_request = voice_agent_pb2.Empty() @@ -39,43 +48,75 @@ def run_client(server_address, server_port, mode, nlu_engine, recording_time): print("Wake word status: ", wake_result.status) wake_word_detected = True break + + elif action == 'ExecuteVoiceCommand': + if mode == 'auto': + raise ValueError("[-] Auto mode is not implemented yet.") - elif mode == 'auto': - raise ValueError("[-] Auto mode is not implemented yet.") + elif mode == 'manual': + stub = voice_agent_pb2_grpc.VoiceAgentServiceStub(channel) + print("[+] Recording voice command in manual mode...") + record_start_request = voice_agent_pb2.RecognizeVoiceControl(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 - elif mode == 'manual': - stub = voice_agent_pb2_grpc.VoiceAgentServiceStub(channel) - 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(recording_time) # pause here for the number of seconds passed by user or default 5 seconds + + record_stop_request = voice_agent_pb2.RecognizeVoiceControl(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 recording ended!") + + status = "Uh oh! Status is unknown." + if record_result.status == voice_agent_pb2.REC_SUCCESS: + status = "Yay! Status is success." + elif record_result.status == voice_agent_pb2.VOICE_NOT_RECOGNIZED: + status = "Voice not recognized." + elif record_result.status == voice_agent_pb2.INTENT_NOT_RECOGNIZED: + status = "Intent not recognized." - time.sleep(recording_time) # pause here for the number of seconds passed by user or default 5 seconds + # Process the response + print("Status:", status) + print("Command:", record_result.command) + print("Intent:", record_result.intent) + intent_slots = [] + for slot in record_result.intent_slots: + print("Slot Name:", slot.name) + print("Slot Value:", slot.value) + i_slot = voice_agent_pb2.IntentSlot(name=slot.name, value=slot.value) + intent_slots.append(i_slot) + + 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.ExecuteCommand(exec_voice_command_request) - 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 recording ended!") + elif action == 'ExecuteTextCommand': + text_input = input("[+] Enter text command: ") + stub = voice_agent_pb2_grpc.VoiceAgentServiceStub(channel) + recognize_text_request = voice_agent_pb2.RecognizeTextControl(text_command=text_input, nlu_model=nlu_engine) + response = stub.RecognizeTextCommand(recognize_text_request) + status = "Uh oh! Status is unknown." - if record_result.status == voice_agent_pb2.REC_SUCCESS: + if response.status == voice_agent_pb2.REC_SUCCESS: status = "Yay! Status is success." - elif record_result.status == voice_agent_pb2.VOICE_NOT_RECOGNIZED: - status = "Voice not recognized." - elif record_result.status == voice_agent_pb2.INTENT_NOT_RECOGNIZED: + elif response.status == voice_agent_pb2.NLU_MODEL_NOT_SUPPORTED: + status = "NLU model not supported." + elif response.status == voice_agent_pb2.INTENT_NOT_RECOGNIZED: status = "Intent not recognized." # Process the response print("Status:", status) - print("Command:", record_result.command) - print("Intent:", record_result.intent) + print("Command:", response.command) + print("Intent:", response.intent) intent_slots = [] - for slot in record_result.intent_slots: + for slot in response.intent_slots: print("Slot Name:", slot.name) print("Slot Value:", slot.value) i_slot = voice_agent_pb2.IntentSlot(name=slot.name, value=slot.value) intent_slots.append(i_slot) - - 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 + + if response.status == voice_agent_pb2.REC_SUCCESS: + print("[+] Executing voice command...") + exec_text_command_request = voice_agent_pb2.ExecuteInput(intent=response.intent, intent_slots=intent_slots) + response = stub.ExecuteCommand(exec_text_command_request)
\ No newline at end of file diff --git a/agl_service_voiceagent/config.ini b/agl_service_voiceagent/config.ini index 81d4e69..1651da5 100644 --- a/agl_service_voiceagent/config.ini +++ b/agl_service_voiceagent/config.ini @@ -17,10 +17,11 @@ store_voice_commands = 0 [Kuksa] ip = 127.0.0.1 -port = 8090 -protocol = ws -insecure = True +port = 55555 +protocol = grpc +insecure = 0 token = PYTHON_DIR/kuksa_certificates/jwt/super-admin.json.token +tls_server_name = Server [Mapper] intents_vss_map = /usr/share/nlu/mappings/intents_vss_map.json diff --git a/agl_service_voiceagent/protos/voice_agent.proto b/agl_service_voiceagent/protos/voice_agent.proto index 8c3ab65..40dfe6a 100644 --- a/agl_service_voiceagent/protos/voice_agent.proto +++ b/agl_service_voiceagent/protos/voice_agent.proto @@ -3,9 +3,12 @@ syntax = "proto3"; service VoiceAgentService { rpc CheckServiceStatus(Empty) returns (ServiceStatus); + rpc S_DetectWakeWord(stream VoiceAudio) returns (stream WakeWordStatus); // Stream version of DetectWakeWord, assumes audio is coming from client rpc DetectWakeWord(Empty) returns (stream WakeWordStatus); - rpc RecognizeVoiceCommand(stream RecognizeControl) returns (RecognizeResult); - rpc ExecuteVoiceCommand(ExecuteInput) returns (ExecuteResult); + rpc S_RecognizeVoiceCommand(stream S_RecognizeVoiceControl) returns (RecognizeResult); // Stream version of RecognizeVoiceCommand, assumes audio is coming from client + rpc RecognizeVoiceCommand(stream RecognizeVoiceControl) returns (RecognizeResult); + rpc RecognizeTextCommand(RecognizeTextControl) returns (RecognizeResult); + rpc ExecuteCommand(ExecuteInput) returns (ExecuteResult); } @@ -30,6 +33,8 @@ enum RecognizeStatusType { REC_PROCESSING = 2; VOICE_NOT_RECOGNIZED = 3; INTENT_NOT_RECOGNIZED = 4; + TEXT_NOT_RECOGNIZED = 5; + NLU_MODEL_NOT_SUPPORTED = 6; } enum ExecuteStatusType { @@ -46,19 +51,38 @@ message Empty {} message ServiceStatus { string version = 1; bool status = 2; + string wake_word = 3; +} + +message VoiceAudio { + bytes audio_chunk = 1; + string audio_format = 2; + int32 sample_rate = 3; + string language = 4; } message WakeWordStatus { bool status = 1; } -message RecognizeControl { +message S_RecognizeVoiceControl { + VoiceAudio audio_stream = 1; + NLUModel nlu_model = 2; + string stream_id = 3; +} + +message RecognizeVoiceControl { RecordAction action = 1; NLUModel nlu_model = 2; RecordMode record_mode = 3; string stream_id = 4; } +message RecognizeTextControl { + string text_command = 1; + NLUModel nlu_model = 2; +} + message IntentSlot { string name = 1; string value = 2; diff --git a/agl_service_voiceagent/service.py b/agl_service_voiceagent/service.py index 9682b56..baf7b02 100644 --- a/agl_service_voiceagent/service.py +++ b/agl_service_voiceagent/service.py @@ -46,6 +46,7 @@ def main(): server_parser = subparsers.add_parser('run-server', help='Run the Voice Agent gRPC Server') client_parser = subparsers.add_parser('run-client', help='Run the Voice Agent gRPC Client') + # Add the arguments for the server server_parser.add_argument('--default', action='store_true', help='Starts the server based on default config file.') server_parser.add_argument('--config', required=False, help='Path to a config file. Server is started based on this config file.') server_parser.add_argument('--stt-model-path', required=False, help='Path to the Speech To Text model for Voice Commad detection. Currently only supports VOSK Kaldi.') @@ -58,10 +59,12 @@ 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.') + # Add the arguments for the client 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', help='NLU engine to use. Supported NLU egnines: "snips" and "rasa".') + client_parser.add_argument('--action', required=True, help='Action to perform. Supported actions: "GetStatus", "DetectWakeWord", "ExecuteVoiceCommand" and "ExecuteTextCommand".') + client_parser.add_argument('--mode', help='Mode to run the client in. Supported modes: "auto" and "manual".') + client_parser.add_argument('--nlu', help='NLU engine/model to use. Supported NLU engines: "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() @@ -170,14 +173,15 @@ def main(): server_address = args.server_address server_port = args.server_port nlu_engine = "" - mode = args.mode - recording_time = 5 + mode = "" + action = args.action + recording_time = 5 # seconds - if mode not in ['wake-word', 'auto', 'manual']: - print("Error: Invalid value for --mode. Supported modes: 'wake-word', 'auto' and 'manual'. Use --help to see available options.") + if action not in ["GetStatus", "DetectWakeWord", "ExecuteVoiceCommand", "ExecuteTextCommand"]: + print("Error: Invalid value for --action. Supported actions: 'GetStatus', 'DetectWakeWord', 'ExecuteVoiceCommand' and 'ExecuteTextCommand'. Use --help to see available options.") exit(1) - if mode in ["auto", "manual"]: + if action in ["ExecuteVoiceCommand", "ExecuteTextCommand"]: 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) @@ -186,11 +190,17 @@ def main(): 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 action in ["ExecuteVoiceCommand"]: + if not args.mode: + print("Error: The --mode flag is missing. Please provide a value for mode. Supported modes: 'auto' and 'manual'. Use --help to see available options.") + exit(1) + mode = args.mode if mode == "manual" and args.recording_time: recording_time = int(args.recording_time) - run_client(server_address, server_port, mode, nlu_engine, recording_time) + run_client(server_address, server_port, action, 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 c9b671d..0027c96 100644 --- a/agl_service_voiceagent/servicers/voice_agent_servicer.py +++ b/agl_service_voiceagent/servicers/voice_agent_servicer.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import grpc +import json import time import threading from agl_service_voiceagent.generated import voice_agent_pb2 @@ -98,14 +98,26 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): response = voice_agent_pb2.ServiceStatus( version=self.service_version, - status=True + status=True, + wake_word=self.wake_word, ) + + # Convert the response object to a JSON string and log it + response_data = { + "version": self.service_version, + "status": True, + "wake_word": self.wake_word, + } + response_json = json.dumps(response_data) + self.logger.info(f"[ReqID#{request_id}] Returning response to client {client_ip} from CheckServiceStatus end-point. Response: {response_json}") + return response def DetectWakeWord(self, request, context): """ - Detect the wake word using the wake word detection model. + Detect the wake word using the wake word detection model. This method records voice on server side. If your client + and server are not on the same machine, then you should use the `S_DetectWakeWord` method instead. """ # Log the unique request ID, client's IP address, and the endpoint request_id = generate_unique_uuid(8) @@ -131,11 +143,14 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): def RecognizeVoiceCommand(self, requests, context): """ - Recognize the voice command using the STT model and extract the intent using the NLU model. + Recognize the voice command using the STT model and extract the intent using the NLU model. This method records voice + on server side, meaning the client only sends a START and STOP request to the server. If your client and server are + not on the same machine, then you should use the `S_RecognizeVoiceCommand` method instead. """ stt = "" intent = "" intent_slots = [] + log_intent_slots = [] for request in requests: if request.record_mode == voice_agent_pb2.MANUAL: @@ -182,9 +197,11 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): if not intent or intent == "": status = voice_agent_pb2.INTENT_NOT_RECOGNIZED - - for action, value in intent_actions.items(): - intent_slots.append(voice_agent_pb2.IntentSlot(name=action, value=value)) + + else: + for action, value in intent_actions.items(): + intent_slots.append(voice_agent_pb2.IntentSlot(name=action, value=value)) + log_intent_slots.append({"name": action, "value": value}) elif request.nlu_model == voice_agent_pb2.RASA: extracted_intent = self.rasa_interface.extract_intent(stt) @@ -193,8 +210,13 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): if not intent or intent == "": status = voice_agent_pb2.INTENT_NOT_RECOGNIZED - for action, value in intent_actions.items(): - intent_slots.append(voice_agent_pb2.IntentSlot(name=action, value=value)) + else: + for action, value in intent_actions.items(): + intent_slots.append(voice_agent_pb2.IntentSlot(name=action, value=value)) + log_intent_slots.append({"name": action, "value": value}) + + else: + status = voice_agent_pb2.NLU_MODEL_NOT_SUPPORTED else: stt = "" @@ -216,17 +238,95 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): stream_id=stream_uuid, status=status ) + + # Convert the response object to a JSON string and log it + response_data = { + "command": stt, + "intent": intent, + "intent_slots": log_intent_slots, + "stream_id": stream_uuid, + "status": status + } + response_json = json.dumps(response_data) + self.logger.info(f"[ReqID#{stream_uuid}] Returning {request.action} request response to client {client_ip} from RecognizeVoiceCommand end-point. Response: {response_json}") + + return response + + + def RecognizeTextCommand(self, request, context): + """ + Recognize the text command using the STT model and extract the intent using the NLU model. + """ + intent = "" + intent_slots = [] + log_intent_slots = [] + + stream_uuid = generate_unique_uuid(8) + text_command = request.text_command + 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 request to RecognizeTextCommand end-point.") + + if request.nlu_model == voice_agent_pb2.SNIPS: + extracted_intent = self.snips_interface.extract_intent(text_command) + intent, intent_actions = self.snips_interface.process_intent(extracted_intent) + + if not intent or intent == "": + status = voice_agent_pb2.INTENT_NOT_RECOGNIZED + + else: + for action, value in intent_actions.items(): + intent_slots.append(voice_agent_pb2.IntentSlot(name=action, value=value)) + log_intent_slots.append({"name": action, "value": value}) + + elif request.nlu_model == voice_agent_pb2.RASA: + extracted_intent = self.rasa_interface.extract_intent(text_command) + intent, intent_actions = self.rasa_interface.process_intent(extracted_intent) + + if not intent or intent == "": + status = voice_agent_pb2.INTENT_NOT_RECOGNIZED + + else: + for action, value in intent_actions.items(): + intent_slots.append(voice_agent_pb2.IntentSlot(name=action, value=value)) + log_intent_slots.append({"name": action, "value": value}) + + else: + status = voice_agent_pb2.NLU_MODEL_NOT_SUPPORTED + + # Process the request and generate a RecognizeResult + response = voice_agent_pb2.RecognizeResult( + command=text_command, + intent=intent, + intent_slots=intent_slots, + stream_id=stream_uuid, + status=status + ) + + # Convert the response object to a JSON string and log it + response_data = { + "command": text_command, + "intent": intent, + "intent_slots": log_intent_slots, + "stream_id": stream_uuid, + "status": status + } + response_json = json.dumps(response_data) + self.logger.info(f"[ReqID#{stream_uuid}] Returning response to client {client_ip} from RecognizeTextCommand end-point. Response: {response_json}") + return response - def ExecuteVoiceCommand(self, request, context): + def ExecuteCommand(self, request, context): """ 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.") + self.logger.info(f"[ReqID#{request_id}] Client {client_ip} made a request to ExecuteCommand end-point.") intent = request.intent intent_slots = request.intent_slots @@ -238,7 +338,7 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): print(intent) print(processed_slots) - execution_list = self.mapper.parse_intent(intent, processed_slots) + execution_list = self.mapper.parse_intent(intent, processed_slots, req_id=request_id) 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 @@ -285,15 +385,23 @@ class VoiceAgentServicer(voice_agent_pb2_grpc.VoiceAgentServiceServicer): else: exec_response = f"Uh oh, there is no value set for intent '{intent}'. Why not try setting a value first?" - exec_status = voice_agent_pb2.EXEC_KUKSA_CONN_ERROR + exec_status = voice_agent_pb2.KUKSA_CONN_ERROR else: exec_response = "Uh oh, I failed to connect to Kuksa." - exec_status = voice_agent_pb2.EXEC_KUKSA_CONN_ERROR + exec_status = voice_agent_pb2.KUKSA_CONN_ERROR response = voice_agent_pb2.ExecuteResult( response=exec_response, status=exec_status ) + # Convert the response object to a JSON string and log it + response_data = { + "response": exec_response, + "status": exec_status + } + response_json = json.dumps(response_data) + self.logger.info(f"[ReqID#{request_id}] Returning response to client {client_ip} from ExecuteCommand end-point. Response: {response_json}") + return response diff --git a/agl_service_voiceagent/utils/config.py b/agl_service_voiceagent/utils/config.py index e0b053e..7a5c28a 100644 --- a/agl_service_voiceagent/utils/config.py +++ b/agl_service_voiceagent/utils/config.py @@ -44,9 +44,14 @@ def load_config(): 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 = logging.getLogger("agl_service_voiceagent") 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-") + # remove unwanted third-party loggers + logging.getLogger("snips_inference_agl").setLevel(logging.WARNING) + logging.getLogger("asyncio").setLevel(logging.WARNING) + logging.getLogger("grpc").setLevel(logging.WARNING) + else: raise Exception("Config file path not provided.") diff --git a/agl_service_voiceagent/utils/kuksa_interface.py b/agl_service_voiceagent/utils/kuksa_interface.py index 0881660..ca1090b 100644 --- a/agl_service_voiceagent/utils/kuksa_interface.py +++ b/agl_service_voiceagent/utils/kuksa_interface.py @@ -52,16 +52,49 @@ class KuksaInterface: # get config values self.ip = str(get_config_value("ip", "Kuksa")) self.port = str(get_config_value("port", "Kuksa")) - self.insecure = get_config_value("insecure", "Kuksa") + self.insecure = bool(int(get_config_value("insecure", "Kuksa"))) self.protocol = get_config_value("protocol", "Kuksa") self.token = get_config_value("token", "Kuksa") + self.tls_server_name = get_config_value("tls_server_name", "Kuksa") self.logger = get_logger() + # validate config + if not self.validate_config(): + exit(1) + print(self.ip, self.port, self.insecure, self.protocol, self.token) # define class methods self.kuksa_client = None + def validate_config(self): + """ + Validate the Kuksa client configuration. + + Returns: + bool: True if the configuration is valid, False otherwise. + """ + if self.ip is None: + print("[-] Error: Kuksa IP address is not set.") + self.logger.error("Kuksa IP address is not set.") + return False + + if self.port is None: + print("[-] Error: Kuksa port is not set.") + self.logger.error("Kuksa port is not set.") + return False + + if self.token is None: + print("[-] Warning: Kuksa auth token is not set.") + self.logger.warning("Kuksa auth token is not set.") + + if self.protocol != "ws" and self.protocol != "grpc": + print("[-] Error: Invalid Kuksa protocol. Only 'ws' and 'grpc' are supported.") + self.logger.error("Invalid Kuksa protocol. Only 'ws' and 'grpc' are supported.") + return False + + return True + def get_kuksa_client(self): """ @@ -97,6 +130,7 @@ class KuksaInterface: "port": self.port, "insecure": self.insecure, "protocol": self.protocol, + "tls_server_name": self.tls_server_name }) self.kuksa_client.start() time.sleep(2) # Give the thread time to start @@ -119,11 +153,17 @@ class KuksaInterface: """ if self.kuksa_client: response = self.kuksa_client.authorize(self.token) - response = json.loads(response) - if "error" in response: + + if self.protocol == "ws" and "error" in json.loads(response): + response = json.loads(response) error_message = response.get("error", "Unknown error") print(f"[-] Error: Authorization failed. {error_message}") self.logger.error(f"Authorization failed. {error_message}") + + elif self.protocol == "grpc" and "error" in response: + print("[-] Error: Authorization failed.") + self.logger.error("Authorization failed.") + else: print("[+] Kuksa client authorized successfully.") self.logger.info("Kuksa client authorized successfully.") @@ -146,17 +186,19 @@ class KuksaInterface: if self.kuksa_client is None: 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 + return result if self.get_kuksa_status(): try: response = self.kuksa_client.setValue(path, value) - response = json.loads(response) + if not "error" in response: print(f"[+] Value '{value}' sent to Kuksa successfully.") result = True + else: - error_message = response.get("error", "Unknown error") + response = json.loads(response) + error_message = response.get("error", "{\"message\": \"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}") @@ -184,19 +226,23 @@ class KuksaInterface: if self.kuksa_client is None: 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 + return result if self.get_kuksa_status(): try: response = self.kuksa_client.getValue(path) response = json.loads(response) - if not "error" in response: + if self.protocol == "ws" and not "error" in response: result = response.get("data", None) result = result.get("dp", None) result = result.get("value", None) - + + elif self.protocol == "grpc" and not "error" in response: + result = response.get("value", None) + result = result.get("value", None) + else: - error_message = response.get("error", "Unknown error") + error_message = response.get("error", "{\"message\": \"Unknown error.\"}") 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}") diff --git a/agl_service_voiceagent/utils/mapper.py b/agl_service_voiceagent/utils/mapper.py index f24f44f..e42921a 100644 --- a/agl_service_voiceagent/utils/mapper.py +++ b/agl_service_voiceagent/utils/mapper.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json 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 @@ -126,7 +127,7 @@ class Intent2VSSMapper: return result - def parse_intent(self, intent_name, intent_slots = []): + def parse_intent(self, intent_name, intent_slots = [], req_id = ""): """ Parses an intent, extracting relevant VSS signals, actions, modifiers, and values based on the intent and its associated slots. @@ -148,11 +149,13 @@ class Intent2VSSMapper: action = self.determine_action(signal_data, intent_slots) modifier = self.determine_modifier(signal_data, intent_slots) value = self.determine_value(signal_data, intent_slots) + original_value = value if value != None and not self.verify_value(signal_data, value): value = None change_factor = signal_data["default_change_factor"] + log_change_factor = change_factor if action in ["increase", "decrease"]: if value and modifier == "to": @@ -160,6 +163,7 @@ class Intent2VSSMapper: elif value and modifier == "by": execution_list.append({"action": action, "signal": signal_name, "factor": str(value)}) + log_change_factor = value elif value: execution_list.append({"action": action, "signal": signal_name, "value": str(value)}) @@ -173,6 +177,20 @@ class Intent2VSSMapper: if action == "set" and value != None: execution_list.append({"action": action, "signal": signal_name, "value": str(value)}) + + + # log the mapping data + mapping_log_data = { + "Signal": signal_name, + "Action": action, + "Modifier": modifier, + "OriginalValue": original_value, + "ProcessedValue": value, + "ChangeFactor": log_change_factor + } + mapping_log_data = json.dumps(mapping_log_data) + print(f"[+] Mapper Log: {mapping_log_data}") + self.logger.info(f"[ReqID#{req_id}] Mapper Log: {mapping_log_data}") return execution_list @@ -259,6 +277,8 @@ class Intent2VSSMapper: Returns: bool: True if the value is valid, False otherwise. """ + value = int(value) if value.isnumeric() else float(value) if value.replace('.', '', 1).isnumeric() else value + if value in signal_data["values"]["ignore"]: return False diff --git a/mappings/vss_signals_spec.json b/mappings/vss_signals_spec.json index f589297..996e1c7 100644 --- a/mappings/vss_signals_spec.json +++ b/mappings/vss_signals_spec.json @@ -175,16 +175,16 @@ "default_change_factor": 2, "actions": { "set": { - "intents": ["hvac_fan_speed_action"], + "intents": ["hvac_temperature_action"], "synonyms": ["set", "change", "adjust"] }, "increase": { - "intents":["hvac_fan_speed_action"], + "intents":["hvac_temperature_action"], "synonyms": ["increase", "up", "boost", "boosting", "raise", "heat", "warm", "warmer"], "modifier_intents": ["to_or_by"] }, "decrease": { - "intents": ["hvac_fan_speed_action"], + "intents": ["hvac_temperature_action"], "synonyms": ["decrease", "lower", "down", "cool", "colder", "reduce", "back"], "modifier_intents": ["to_or_by"] } @@ -209,16 +209,16 @@ "default_change_factor": 2, "actions": { "set": { - "intents": ["hvac_fan_speed_action"], + "intents": ["hvac_temperature_action"], "synonyms": ["set", "change", "adjust"] }, "increase": { - "intents":["hvac_fan_speed_action"], + "intents":["hvac_temperature_action"], "synonyms": ["increase", "up", "boost", "boosting", "raise", "heat", "warm", "warmer"], "modifier_intents": ["to_or_by"] }, "decrease": { - "intents": ["hvac_fan_speed_action"], + "intents": ["hvac_temperature_action"], "synonyms": ["decrease", "lower", "down", "cool", "colder", "reduce", "back"], "modifier_intents": ["to_or_by"] } @@ -243,16 +243,16 @@ "default_change_factor": 2, "actions": { "set": { - "intents": ["hvac_fan_speed_action"], + "intents": ["hvac_temperature_action"], "synonyms": ["set", "change", "adjust"] }, "increase": { - "intents":["hvac_fan_speed_action"], + "intents":["hvac_temperature_action"], "synonyms": ["increase", "up", "boost", "boosting", "raise", "heat", "warm", "warmer"], "modifier_intents": ["to_or_by"] }, "decrease": { - "intents": ["hvac_fan_speed_action"], + "intents": ["hvac_temperature_action"], "synonyms": ["decrease", "lower", "down", "cool", "colder", "reduce", "back"], "modifier_intents": ["to_or_by"] } @@ -277,16 +277,16 @@ "default_change_factor": 2, "actions": { "set": { - "intents": ["hvac_fan_speed_action"], + "intents": ["hvac_temperature_action"], "synonyms": ["set", "change", "adjust"] }, "increase": { - "intents":["hvac_fan_speed_action"], + "intents":["hvac_temperature_action"], "synonyms": ["increase", "up", "boost", "boosting", "raise", "heat", "warm", "warmer"], "modifier_intents": ["to_or_by"] }, "decrease": { - "intents": ["hvac_fan_speed_action"], + "intents": ["hvac_temperature_action"], "synonyms": ["decrease", "lower", "down", "cool", "colder", "reduce", "back"], "modifier_intents": ["to_or_by"] } |