summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMalik Talha <talhamalik727x@gmail.com>2023-11-12 03:50:42 +0500
committerMalik Talha <talhamalik727x@gmail.com>2023-11-12 03:50:42 +0500
commit9629533242da050b0d30173d6becea20191c0b2b (patch)
tree137db0e12094773f364aa3a5ca8ba0810cd7e1aa
parenta644c90d7409f48c49fbd5ddca7ecfe35de15953 (diff)
Add grpc support and improvements to voice agent
Add grpc support, enhance logging functionality, re-write the client side code, add new rpc end point to handle text-based commands. and minor bug fixes. Bug-AGL: SPEC-4906 Signed-off-by: Malik Talha <talhamalik727x@gmail.com> Change-Id: If65c5940a78c5496110ff596fa744e3c93c78033
-rw-r--r--README.md16
-rw-r--r--agl_service_voiceagent/client.py93
-rw-r--r--agl_service_voiceagent/config.ini7
-rw-r--r--agl_service_voiceagent/protos/voice_agent.proto30
-rw-r--r--agl_service_voiceagent/service.py26
-rw-r--r--agl_service_voiceagent/servicers/voice_agent_servicer.py136
-rw-r--r--agl_service_voiceagent/utils/config.py7
-rw-r--r--agl_service_voiceagent/utils/kuksa_interface.py66
-rw-r--r--agl_service_voiceagent/utils/mapper.py22
-rw-r--r--mappings/vss_signals_spec.json24
10 files changed, 342 insertions, 85 deletions
diff --git a/README.md b/README.md
index 2bd6082..d418994 100644
--- a/README.md
+++ b/README.md
@@ -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"]
}