Skip to content

Commit 9215747

Browse files
committed
Fix UI to add widget for Admin overrides
1 parent 490170f commit 9215747

File tree

8 files changed

+436
-16
lines changed

8 files changed

+436
-16
lines changed

coffee_ws/src/coffee_voice_agent/coffee_voice_agent/voice_agent_bridge.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from rclpy.parameter import Parameter
2323
from std_msgs.msg import String, Bool
2424
from geometry_msgs.msg import Twist
25-
from coffee_voice_agent_msgs.msg import AgentStatus, ToolEvent
25+
from coffee_voice_agent_msgs.msg import AgentStatus, ToolEvent, VipDetection, ExtensionEvent
2626

2727
try:
2828
import websockets
@@ -103,6 +103,20 @@ def __init__(self):
103103
callback_group=self.callback_group
104104
)
105105

106+
self.vip_detection_pub = self.create_publisher(
107+
VipDetection,
108+
'voice_agent/vip_detections',
109+
10,
110+
callback_group=self.callback_group
111+
)
112+
113+
self.extension_event_pub = self.create_publisher(
114+
ExtensionEvent,
115+
'voice_agent/extension_events',
116+
10,
117+
callback_group=self.callback_group
118+
)
119+
106120
# ROS2 Subscribers (ROS2 → Voice Agent)
107121
self.virtual_request_sub = self.create_subscription(
108122
String,
@@ -264,6 +278,60 @@ async def _handle_websocket_message(self, message: str):
264278
speech_msg.data = user_text
265279
self.user_speech_pub.publish(speech_msg)
266280

281+
elif message_type == 'VIP_DETECTED':
282+
# Handle VIP user detection events
283+
vip_data = data.get('data', {})
284+
285+
self.get_logger().info(f"VIP Detection: {vip_data.get('user_identifier', 'unknown')} - {vip_data.get('importance_level', 'unknown')}")
286+
287+
# Publish VIP detection to ROS2 topic
288+
vip_msg = VipDetection()
289+
vip_msg.user_identifier = vip_data.get('user_identifier', '')
290+
vip_msg.matched_keywords = vip_data.get('matched_keywords', [])
291+
vip_msg.importance_level = vip_data.get('importance_level', 'normal')
292+
vip_msg.recommended_extension_minutes = vip_data.get('recommended_extension_minutes', 0)
293+
294+
# Parse timestamp if provided, otherwise use current time
295+
timestamp_str = vip_data.get('timestamp')
296+
if timestamp_str:
297+
try:
298+
dt = datetime.datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
299+
vip_msg.timestamp.sec = int(dt.timestamp())
300+
vip_msg.timestamp.nanosec = int((dt.timestamp() % 1) * 1e9)
301+
except:
302+
vip_msg.timestamp = self.get_clock().now().to_msg()
303+
else:
304+
vip_msg.timestamp = self.get_clock().now().to_msg()
305+
306+
self.vip_detection_pub.publish(vip_msg)
307+
308+
elif message_type == 'EXTENSION_GRANTED':
309+
# Handle conversation extension events
310+
extension_data = data.get('data', {})
311+
312+
self.get_logger().info(f"Extension Event: {extension_data.get('action', 'unknown')} - {extension_data.get('extension_minutes', 0)} minutes")
313+
314+
# Publish extension event to ROS2 topic
315+
extension_msg = ExtensionEvent()
316+
extension_msg.action = extension_data.get('action', 'unknown')
317+
extension_msg.extension_minutes = extension_data.get('extension_minutes', 0)
318+
extension_msg.reason = extension_data.get('reason', '')
319+
extension_msg.granted_by = extension_data.get('granted_by', 'unknown')
320+
321+
# Parse timestamp if provided, otherwise use current time
322+
timestamp_str = extension_data.get('timestamp')
323+
if timestamp_str:
324+
try:
325+
dt = datetime.datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
326+
extension_msg.timestamp.sec = int(dt.timestamp())
327+
extension_msg.timestamp.nanosec = int((dt.timestamp() % 1) * 1e9)
328+
except:
329+
extension_msg.timestamp = self.get_clock().now().to_msg()
330+
else:
331+
extension_msg.timestamp = self.get_clock().now().to_msg()
332+
333+
self.extension_event_pub.publish(extension_msg)
334+
267335
elif message_type == 'ACKNOWLEDGMENT':
268336
# Handle acknowledgment messages from voice agent
269337
status = data.get('status', 'unknown')

coffee_ws/src/coffee_voice_agent/scripts/tools/coffee_tools.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,17 @@ async def manage_conversation_time_impl(
157157
# Actually extend the conversation by calling state manager
158158
if _agent_instance and hasattr(_agent_instance, 'state_manager') and extension_minutes > 0:
159159
await _agent_instance.state_manager.extend_conversation(extension_minutes, reason)
160+
161+
# Send extension granted event via WebSocket
162+
if hasattr(_agent_instance, '_send_websocket_event'):
163+
await _agent_instance._send_websocket_event("EXTENSION_GRANTED", {
164+
"action": "granted",
165+
"extension_minutes": extension_minutes,
166+
"reason": reason,
167+
"granted_by": "tool",
168+
"timestamp": datetime.now().isoformat()
169+
})
170+
160171
logger.info(f"Conversation extended by {extension_minutes} minutes: {reason}")
161172
result = f"Conversation extended by {extension_minutes} minutes: {reason}"
162173
else:
@@ -201,6 +212,17 @@ async def check_user_status_impl(
201212
is_vip = any(keyword in user_lower for keyword in vip_keywords)
202213

203214
if is_vip:
215+
# Send VIP detection event via WebSocket
216+
if _agent_instance and hasattr(_agent_instance, '_send_websocket_event'):
217+
matched_keywords = [keyword for keyword in vip_keywords if keyword in user_lower]
218+
await _agent_instance._send_websocket_event("VIP_DETECTED", {
219+
"user_identifier": user_identifier,
220+
"matched_keywords": matched_keywords,
221+
"importance_level": "vip",
222+
"recommended_extension_minutes": 3,
223+
"timestamp": datetime.now().isoformat()
224+
})
225+
204226
result = f"VIP user detected: {user_identifier}. Recommended extension: 3 minutes. Enhanced service advised."
205227
logger.info(f"VIP user identified: {user_identifier}")
206228
else:

coffee_ws/src/coffee_voice_agent_msgs/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ rosidl_generate_interfaces(${PROJECT_NAME}
1616
"msg/AgentStatus.msg"
1717
"msg/ToolEvent.msg"
1818
"msg/ConfigurationUpdate.msg"
19+
"msg/VipDetection.msg"
20+
"msg/ExtensionEvent.msg"
1921
DEPENDENCIES builtin_interfaces std_msgs
2022
)
2123

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
string action # "granted", "expired", "updated", "status"
2+
int32 extension_minutes
3+
string reason
4+
string granted_by # "tool", "manual", "system"
5+
builtin_interfaces/Time timestamp
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
string user_identifier
2+
string[] matched_keywords
3+
string importance_level
4+
int32 recommended_extension_minutes
5+
builtin_interfaces/Time timestamp

coffee_ws/src/coffee_voice_agent_ui/coffee_voice_agent_ui/voice_agent_monitor.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
# ROS2 Messages
2323
from std_msgs.msg import String, Bool
24-
from coffee_voice_agent_msgs.msg import AgentStatus, ToolEvent
24+
from coffee_voice_agent_msgs.msg import AgentStatus, ToolEvent, VipDetection, ExtensionEvent
2525

2626
# Import custom widgets
2727
from .widgets.agent_status_widget import AgentStatusWidget
@@ -30,16 +30,19 @@
3030
from .widgets.tool_monitor_widget import ToolMonitorWidget
3131
from .widgets.analytics_widget import AnalyticsWidget
3232
from .widgets.virtual_request_widget import VirtualRequestWidget
33+
from .widgets.admin_override_widget import AdminOverrideWidget
3334

3435

3536
class VoiceAgentMonitorNode(Node):
3637
"""ROS2 Node for handling voice agent monitoring subscriptions"""
3738

3839
# PyQt signals for thread-safe UI updates
3940
agent_status_received = pyqtSignal(AgentStatus)
40-
tool_event_received = pyqtSignal(ToolEvent)
41+
tool_event_received = pyqtSignal(ToolEvent)
4142
user_speech_received = pyqtSignal(str)
4243
connection_status_received = pyqtSignal(bool)
44+
vip_detection_received = pyqtSignal(VipDetection)
45+
extension_event_received = pyqtSignal(ExtensionEvent)
4346

4447
def __init__(self):
4548
super().__init__('voice_agent_monitor_node')
@@ -73,6 +76,20 @@ def __init__(self):
7376
10
7477
)
7578

79+
self.vip_detection_sub = self.create_subscription(
80+
VipDetection,
81+
'voice_agent/vip_detections',
82+
self.vip_detection_callback,
83+
10
84+
)
85+
86+
self.extension_event_sub = self.create_subscription(
87+
ExtensionEvent,
88+
'voice_agent/extension_events',
89+
self.extension_event_callback,
90+
10
91+
)
92+
7693
# Publisher for sending virtual requests
7794
self.virtual_request_pub = self.create_publisher(
7895
String,
@@ -97,6 +114,14 @@ def speech_callback(self, msg):
97114
def connection_callback(self, msg):
98115
"""Handle connection status messages"""
99116
self.connection_status_received.emit(msg.data)
117+
118+
def vip_detection_callback(self, msg):
119+
"""Handle VipDetection messages"""
120+
self.vip_detection_received.emit(msg)
121+
122+
def extension_event_callback(self, msg):
123+
"""Handle ExtensionEvent messages"""
124+
self.extension_event_received.emit(msg)
100125

101126

102127
class VoiceAgentMonitor(Plugin):
@@ -136,20 +161,30 @@ def _setup_ui(self):
136161
self.tool_monitor_widget = ToolMonitorWidget()
137162
self.analytics_widget = AnalyticsWidget()
138163
self.virtual_request_widget = VirtualRequestWidget()
164+
self.admin_override_widget = AdminOverrideWidget()
139165

140-
# Arrange widgets in dashboard layout
141-
# Row 0: Agent Status | Conversation Flow | Analytics
142-
main_layout.addWidget(self.agent_status_widget, 0, 0)
166+
# Create left column container with vertical layout
167+
left_column_widget = QWidget()
168+
left_column_layout = QVBoxLayout()
169+
left_column_widget.setLayout(left_column_layout)
170+
171+
# Add widgets to left column container
172+
left_column_layout.addWidget(self.agent_status_widget)
173+
left_column_layout.addWidget(self.emotion_widget)
174+
left_column_layout.addWidget(self.admin_override_widget)
175+
176+
# Arrange widgets in dashboard layout (back to 2-row layout)
177+
# Row 0: Left Column Container (spans 2 rows) | Conversation Flow | Analytics
178+
main_layout.addWidget(left_column_widget, 0, 0, 2, 1) # span 2 rows, 1 column
143179
main_layout.addWidget(self.conversation_widget, 0, 1)
144180
main_layout.addWidget(self.analytics_widget, 0, 2)
145181

146-
# Row 1: Emotion Display | Tool Monitor | Controls
147-
main_layout.addWidget(self.emotion_widget, 1, 0)
182+
# Row 1: (Left Column continues) | Tool Monitor | Virtual Requests
148183
main_layout.addWidget(self.tool_monitor_widget, 1, 1)
149184
main_layout.addWidget(self.virtual_request_widget, 1, 2)
150185

151186
# Set column stretch to make conversation widget wider
152-
main_layout.setColumnStretch(0, 1) # Status/Emotion column
187+
main_layout.setColumnStretch(0, 1) # Left column container
153188
main_layout.setColumnStretch(1, 2) # Conversation/Tools column (wider)
154189
main_layout.setColumnStretch(2, 1) # Analytics/Controls column
155190

@@ -170,6 +205,8 @@ def _init_ros(self):
170205
self.ros_node.tool_event_received.connect(self._update_tool_event)
171206
self.ros_node.user_speech_received.connect(self._update_user_speech)
172207
self.ros_node.connection_status_received.connect(self._update_connection_status)
208+
self.ros_node.vip_detection_received.connect(self._update_vip_detection)
209+
self.ros_node.extension_event_received.connect(self._update_extension_event)
173210

174211
# Connect virtual request widget signals to publishers
175212
self.virtual_request_widget.virtual_request_signal.connect(self._send_virtual_request)
@@ -288,6 +325,10 @@ def _update_agent_status(self, status):
288325
self.agent_status_widget.update_status(status)
289326
self.emotion_widget.update_emotion(status.emotion, status.previous_emotion)
290327
self.conversation_widget.update_agent_state(status)
328+
329+
# Reset admin override widget when conversation ends
330+
if status.behavioral_mode == "dormant":
331+
self.admin_override_widget.reset_vip_status()
291332

292333
@pyqtSlot(ToolEvent)
293334
def _update_tool_event(self, event):
@@ -305,6 +346,26 @@ def _update_connection_status(self, connected):
305346
"""Update UI with connection status"""
306347
self.agent_status_widget.update_connection(connected)
307348

349+
@pyqtSlot(VipDetection)
350+
def _update_vip_detection(self, detection):
351+
"""Update UI with new VIP detection"""
352+
self.admin_override_widget.update_vip_detection(
353+
detection.user_identifier,
354+
list(detection.matched_keywords),
355+
detection.importance_level,
356+
detection.recommended_extension_minutes
357+
)
358+
359+
@pyqtSlot(ExtensionEvent)
360+
def _update_extension_event(self, event):
361+
"""Update UI with new extension event"""
362+
self.admin_override_widget.update_extension_event(
363+
event.action,
364+
event.extension_minutes,
365+
event.reason,
366+
event.granted_by
367+
)
368+
308369
@pyqtSlot(str)
309370
def _send_virtual_request(self, request_json):
310371
"""Send virtual request through ROS2"""

0 commit comments

Comments
 (0)