Skip to content

Commit e886fdd

Browse files
committed
Fix monitoring for agent-bridge configuration changes
1 parent 2343e2b commit e886fdd

File tree

4 files changed

+369
-10
lines changed

4 files changed

+369
-10
lines changed

coffee_ws/src/coffee_voice_agent_ui/coffee_voice_agent_ui/voice_agent_monitor.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import rclpy
1111
from rclpy.node import Node
1212
from rclpy.executors import MultiThreadedExecutor
13+
from rclpy.parameter import Parameter
1314
import threading
1415
from datetime import datetime
1516

@@ -185,6 +186,96 @@ def _init_ros(self):
185186
self.ros_executor.add_node(self.ros_node)
186187
self.ros_thread = threading.Thread(target=self.ros_executor.spin, daemon=True)
187188
self.ros_thread.start()
189+
190+
# Query and distribute initial configuration (with delay for bridge startup)
191+
QTimer.singleShot(2000, self._query_and_distribute_config) # Wait 2 seconds for bridge to initialize
192+
193+
# Set up periodic config refresh in case bridge connects later
194+
self.config_refresh_timer = QTimer()
195+
self.config_refresh_timer.timeout.connect(self._retry_config_if_fallback)
196+
self.config_refresh_timer.start(10000) # Check every 10 seconds
197+
198+
def _query_and_distribute_config(self):
199+
"""Query configuration parameters from bridge and distribute to widgets"""
200+
try:
201+
# Query parameters from the voice agent bridge
202+
config_data = {}
203+
config_source = "fallback"
204+
205+
# Try to get parameters from bridge using parameter client
206+
try:
207+
from rclpy.parameter_client import AsyncParameterClient
208+
209+
# Create parameter client to query bridge node
210+
param_client = AsyncParameterClient(self.ros_node, 'voice_agent_bridge')
211+
212+
# Wait for bridge node to be available (timeout after 5 seconds)
213+
if param_client.wait_for_services(timeout_sec=5.0):
214+
# Query timeout parameters from bridge
215+
parameter_names = ['user_response_timeout', 'max_conversation_time', 'config_received']
216+
217+
# Use async parameter client - get future and wait for result
218+
future = param_client.get_parameters(parameter_names)
219+
220+
# Spin until the future is complete (with timeout)
221+
import time
222+
start_time = time.time()
223+
timeout = 5.0
224+
225+
while not future.done() and (time.time() - start_time) < timeout:
226+
rclpy.spin_once(self.ros_node, timeout_sec=0.1)
227+
228+
if future.done():
229+
parameters = future.result().values
230+
231+
config_data = {
232+
'user_response_timeout': parameters[0].double_value if parameters[0].type == 3 else 15.0, # PARAMETER_DOUBLE = 3
233+
'max_conversation_time': parameters[1].double_value if parameters[1].type == 3 else 180.0
234+
}
235+
236+
# Check if we got real configuration from agent using config_received flag
237+
config_received = parameters[2].bool_value if parameters[2].type == 1 else False # PARAMETER_BOOL = 1
238+
239+
if config_received:
240+
config_source = "agent" # Bridge has received agent configuration
241+
else:
242+
config_source = "fallback" # Bridge still using defaults
243+
244+
self.ros_node.get_logger().info(f"Retrieved configuration from bridge: {config_data} (source: {config_source})")
245+
else:
246+
raise Exception("Parameter query timed out")
247+
else:
248+
raise Exception("Bridge node not available")
249+
250+
except Exception as e:
251+
# Parameters not available, use fallback values
252+
self.ros_node.get_logger().warn(f"Could not query bridge parameters: {e}, using fallback configuration")
253+
config_data = {
254+
'user_response_timeout': 15.0,
255+
'max_conversation_time': 180.0
256+
}
257+
config_source = "fallback"
258+
259+
# Distribute configuration to widgets
260+
if hasattr(self, 'conversation_widget'):
261+
self.conversation_widget.update_configuration(config_data, config_source)
262+
self.ros_node.get_logger().info("Updated conversation widget configuration")
263+
264+
# Add configuration info to analytics data
265+
self.last_config_update = {
266+
'config_data': config_data,
267+
'config_source': config_source,
268+
'timestamp': datetime.now()
269+
}
270+
271+
except Exception as e:
272+
self.ros_node.get_logger().error(f"Error in configuration query and distribution: {e}")
273+
274+
def _retry_config_if_fallback(self):
275+
"""Retry configuration query if still using fallback values"""
276+
if (hasattr(self, 'last_config_update') and
277+
self.last_config_update.get('config_source') == 'fallback'):
278+
self._query_and_distribute_config()
188279

189280
def _setup_timers(self):
190281
"""Set up update timers for the UI"""

coffee_ws/src/coffee_voice_agent_ui/coffee_voice_agent_ui/voice_agent_monitor_app.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import rclpy
1919
from rclpy.node import Node
2020
from rclpy.executors import MultiThreadedExecutor
21+
from rclpy.parameter import Parameter
2122
import threading
2223
from datetime import datetime
2324

@@ -231,6 +232,14 @@ def _init_ros(self):
231232
self.ros_executor.add_node(self.ros_node)
232233
self.ros_thread = threading.Thread(target=self.ros_executor.spin, daemon=True)
233234
self.ros_thread.start()
235+
236+
# Query and distribute initial configuration (with delay for bridge startup)
237+
QTimer.singleShot(2000, self._query_and_distribute_config) # Wait 2 seconds for bridge to initialize
238+
239+
# Set up periodic config refresh in case bridge connects later
240+
self.config_refresh_timer = QTimer()
241+
self.config_refresh_timer.timeout.connect(self._retry_config_if_fallback)
242+
self.config_refresh_timer.start(10000) # Check every 10 seconds
234243

235244
def _setup_timers(self):
236245
"""Set up update timers for the UI"""
@@ -244,6 +253,87 @@ def _setup_timers(self):
244253
self.analytics_timer.timeout.connect(self._update_analytics)
245254
self.analytics_timer.start(1000) # 1 FPS for analytics
246255

256+
def _query_and_distribute_config(self):
257+
"""Query configuration parameters from bridge and distribute to widgets"""
258+
try:
259+
# Query parameters from the voice agent bridge
260+
config_data = {}
261+
config_source = "fallback"
262+
263+
# Try to get parameters from bridge using parameter client
264+
try:
265+
from rclpy.parameter_client import AsyncParameterClient
266+
267+
# Create parameter client to query bridge node
268+
param_client = AsyncParameterClient(self.ros_node, 'voice_agent_bridge')
269+
270+
# Wait for bridge node to be available (timeout after 5 seconds)
271+
if param_client.wait_for_services(timeout_sec=5.0):
272+
# Query timeout parameters from bridge
273+
parameter_names = ['user_response_timeout', 'max_conversation_time', 'config_received']
274+
275+
# Use async parameter client - get future and wait for result
276+
future = param_client.get_parameters(parameter_names)
277+
278+
# Spin until the future is complete (with timeout)
279+
import time
280+
start_time = time.time()
281+
timeout = 5.0
282+
283+
while not future.done() and (time.time() - start_time) < timeout:
284+
rclpy.spin_once(self.ros_node, timeout_sec=0.1)
285+
286+
if future.done():
287+
parameters = future.result().values
288+
289+
config_data = {
290+
'user_response_timeout': parameters[0].double_value if parameters[0].type == 3 else 15.0, # PARAMETER_DOUBLE = 3
291+
'max_conversation_time': parameters[1].double_value if parameters[1].type == 3 else 180.0
292+
}
293+
294+
# Check if we got real configuration from agent using config_received flag
295+
config_received = parameters[2].bool_value if parameters[2].type == 1 else False # PARAMETER_BOOL = 1
296+
if config_received:
297+
config_source = "agent" # Bridge has received agent configuration
298+
else:
299+
config_source = "fallback" # Bridge still using defaults
300+
301+
self.ros_node.get_logger().info(f"Retrieved configuration from bridge: {config_data} (source: {config_source})")
302+
else:
303+
raise Exception("Parameter query timed out")
304+
else:
305+
raise Exception("Bridge node not available")
306+
307+
except Exception as e:
308+
# Parameters not available, use fallback values
309+
self.ros_node.get_logger().warn(f"Could not query bridge parameters: {e}, using fallback configuration")
310+
config_data = {
311+
'user_response_timeout': 15.0,
312+
'max_conversation_time': 180.0
313+
}
314+
config_source = "fallback"
315+
316+
# Distribute configuration to widgets
317+
if hasattr(self, 'conversation_widget'):
318+
self.conversation_widget.update_configuration(config_data, config_source)
319+
self.ros_node.get_logger().info("Updated conversation widget configuration")
320+
321+
# Add configuration info to analytics data
322+
self.last_config_update = {
323+
'config_data': config_data,
324+
'config_source': config_source,
325+
'timestamp': datetime.now()
326+
}
327+
328+
except Exception as e:
329+
self.ros_node.get_logger().error(f"Error in configuration query and distribution: {e}")
330+
331+
def _retry_config_if_fallback(self):
332+
"""Retry configuration query if still using fallback values"""
333+
if (hasattr(self, 'last_config_update') and
334+
self.last_config_update.get('config_source') == 'fallback'):
335+
self._query_and_distribute_config()
336+
247337
@pyqtSlot(AgentStatus)
248338
def _update_agent_status(self, status):
249339
"""Update UI with new agent status"""
@@ -312,6 +402,8 @@ def closeEvent(self, event):
312402
self.update_timer.stop()
313403
if hasattr(self, 'analytics_timer'):
314404
self.analytics_timer.stop()
405+
if hasattr(self, 'config_refresh_timer'):
406+
self.config_refresh_timer.stop()
315407

316408
# Clean up ROS
317409
if hasattr(self, 'ros_executor'):

coffee_ws/src/coffee_voice_agent_ui/coffee_voice_agent_ui/widgets/analytics_widget.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,34 +414,50 @@ def _process_conversation_data(self, conversation_data):
414414
# Session ended - determine outcome type
415415
duration = datetime.now() - self.current_session_start
416416

417-
# Analyze conversation outcome (this could be enhanced with more data from conversation_data)
418-
# For now, we use heuristics based on duration and available data
417+
# Analyze conversation outcome using enhanced timeout information
419418
outcome_type = 'completed' # Default assumption
419+
timeout_category = 'none' # Track which timeout caused ending
420420

421-
# Check for timeout indicators
422-
if conversation_data.get('timeout_reached', False):
423-
outcome_type = 'timeout'
421+
# Check for timeout indicators from conversation widget
422+
if conversation_data.get('max_timeout_reached', False):
423+
outcome_type = 'max_timeout'
424+
timeout_category = 'max_conversation'
425+
elif conversation_data.get('timeout_reached', False):
426+
outcome_type = 'user_timeout'
427+
timeout_category = 'user_response'
424428
elif conversation_data.get('user_disconnected', False):
425429
outcome_type = 'user_disconnect'
426430
elif conversation_data.get('natural_ending', False):
427431
outcome_type = 'natural'
428432
elif duration.total_seconds() < 30: # Very short conversations might be failures
429433
outcome_type = 'failed'
430434

435+
# Check warning states for analytics
436+
approaching_max_timeout = conversation_data.get('approaching_max_timeout', False)
437+
approaching_user_timeout = conversation_data.get('approaching_user_timeout', False)
438+
431439
session_record = {
432440
'start': self.current_session_start,
433441
'duration': duration,
434442
'outcome': outcome_type,
443+
'timeout_category': timeout_category,
435444
'turn_count': conversation_data.get('turn_count', 0),
436-
'successful': outcome_type in ['completed', 'natural']
445+
'successful': outcome_type in ['completed', 'natural'],
446+
'user_response_timeout': conversation_data.get('user_response_timeout', 15.0),
447+
'max_conversation_time': conversation_data.get('max_conversation_time', 180.0),
448+
'config_source': conversation_data.get('config_source', 'unknown'),
449+
'approaching_max_timeout': approaching_max_timeout,
450+
'approaching_user_timeout': approaching_user_timeout
437451
}
438452

439453
self.conversation_sessions.append(session_record)
440454
self.conversation_outcomes.append({
441455
'timestamp': datetime.now(),
442456
'outcome': outcome_type,
457+
'timeout_category': timeout_category,
443458
'duration': duration,
444-
'successful': outcome_type in ['completed', 'natural']
459+
'successful': outcome_type in ['completed', 'natural'],
460+
'config_source': conversation_data.get('config_source', 'unknown')
445461
})
446462

447463
self.current_session_start = None
@@ -671,6 +687,38 @@ def _calculate_emotion_trends(self):
671687
else:
672688
self.peak_happiness_label.setText("Peak happiness: --")
673689
self.most_curious_label.setText("Most curious: --")
690+
691+
# Add timeout analytics to emotion trends section
692+
self._update_timeout_analytics()
693+
694+
def _update_timeout_analytics(self):
695+
"""Update timeout analytics in the existing emotion trend labels"""
696+
# Calculate timeout statistics from recent outcomes
697+
recent_outcomes = [outcome for outcome in self.conversation_outcomes
698+
if (datetime.now() - outcome['timestamp']).total_seconds() < 3600]
699+
700+
if recent_outcomes:
701+
# Count different timeout types
702+
max_timeout_count = sum(1 for o in recent_outcomes if o.get('timeout_category') == 'max_conversation')
703+
user_timeout_count = sum(1 for o in recent_outcomes if o.get('timeout_category') == 'user_response')
704+
natural_count = sum(1 for o in recent_outcomes if o.get('outcome') == 'natural')
705+
total_conversations = len(recent_outcomes)
706+
707+
# Update peak happiness label with timeout completion info
708+
if total_conversations > 0:
709+
natural_percentage = (natural_count / total_conversations) * 100
710+
self.peak_happiness_label.setText(f"Natural endings: {natural_count}/{total_conversations} ({natural_percentage:.0f}%)")
711+
else:
712+
self.peak_happiness_label.setText("Natural endings: --")
713+
714+
# Update curious label with timeout breakdown
715+
if max_timeout_count > 0 or user_timeout_count > 0:
716+
self.most_curious_label.setText(f"Timeouts: {max_timeout_count} max, {user_timeout_count} user")
717+
else:
718+
self.most_curious_label.setText("Timeouts: None recently")
719+
else:
720+
self.peak_happiness_label.setText("Natural endings: --")
721+
self.most_curious_label.setText("Timeouts: --")
674722

675723
def _calculate_system_metrics(self):
676724
"""Calculate and update system metrics from real data"""

0 commit comments

Comments
 (0)