rasa-pro 3.14.0.dev20250731__py3-none-any.whl → 3.14.0.dev20250818__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rasa-pro might be problematic. Click here for more details.

Files changed (70) hide show
  1. rasa/core/channels/development_inspector.py +47 -14
  2. rasa/core/channels/inspector/dist/assets/{arc-0b11fe30.js → arc-1ddec37b.js} +1 -1
  3. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-9eef30a7.js → blockDiagram-38ab4fdb-18af387c.js} +1 -1
  4. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-03e94f28.js → c4Diagram-3d4e48cf-250127a3.js} +1 -1
  5. rasa/core/channels/inspector/dist/assets/channel-59f6d54b.js +1 -0
  6. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-95c09eba.js → classDiagram-70f12bd4-c3388b34.js} +1 -1
  7. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-38e8446c.js → classDiagram-v2-f2320105-9c893a82.js} +1 -1
  8. rasa/core/channels/inspector/dist/assets/clone-26177ddb.js +1 -0
  9. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-57dc3038.js → createText-2e5e7dd3-c111213b.js} +1 -1
  10. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-4bac0545.js → edges-e0da2a9e-812a729d.js} +1 -1
  11. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-81795c90.js → erDiagram-9861fffd-fd5051bc.js} +1 -1
  12. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-89489ae6.js → flowDb-956e92f1-3287ac02.js} +1 -1
  13. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-cd152627.js → flowDiagram-66a62f08-692fb0b2.js} +1 -1
  14. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-29c03f5a.js +1 -0
  15. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-3da369bc.js → flowchart-elk-definition-4a651766-008376f1.js} +1 -1
  16. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-85ec16f8.js → ganttDiagram-c361ad54-df330a69.js} +1 -1
  17. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-495bc140.js → gitGraphDiagram-72cf32ee-e03676fb.js} +1 -1
  18. rasa/core/channels/inspector/dist/assets/{graph-1ec4d266.js → graph-46fad2ba.js} +1 -1
  19. rasa/core/channels/inspector/dist/assets/{index-3862675e-0a0e97c9.js → index-3862675e-a484ac55.js} +1 -1
  20. rasa/core/channels/inspector/dist/assets/{index-c804b295.js → index-a003633f.js} +164 -164
  21. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-4d54bcde.js → infoDiagram-f8f76790-3f9e6ec2.js} +1 -1
  22. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-dc097114.js → journeyDiagram-49397b02-79f72383.js} +1 -1
  23. rasa/core/channels/inspector/dist/assets/{layout-1a08981e.js → layout-aad098e5.js} +1 -1
  24. rasa/core/channels/inspector/dist/assets/{line-95f7f1d3.js → line-219ab7ae.js} +1 -1
  25. rasa/core/channels/inspector/dist/assets/{linear-97e69543.js → linear-2cddbe62.js} +1 -1
  26. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-8c71ff03.js → mindmap-definition-fc14e90a-1d41ed99.js} +1 -1
  27. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-f14c71c7.js → pieDiagram-8a3498a8-cc496ee8.js} +1 -1
  28. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-f1d3c9ff.js → quadrantDiagram-120e2f19-84d32884.js} +1 -1
  29. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-bfa2412f.js → requirementDiagram-deff3bca-c0deb984.js} +1 -1
  30. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-53f2c97b.js → sankeyDiagram-04a897e0-b9d7fd62.js} +1 -1
  31. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-319d7c0e.js → sequenceDiagram-704730f1-7d517565.js} +1 -1
  32. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-76a09418.js → stateDiagram-587899a1-98ef9b27.js} +1 -1
  33. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-a67f15d4.js → stateDiagram-v2-d93cdb3a-cee70748.js} +1 -1
  34. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-0654e7c3.js → styles-6aaf32cf-3f9d1c96.js} +1 -1
  35. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-1394bb9d.js → styles-9a916d00-67471923.js} +1 -1
  36. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-e4c5bdae.js → styles-c10674c1-bd093fb7.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-50957104.js → svgDrawCommon-08f97a94-675794e8.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-b0885a6a.js → timeline-definition-85554ec2-0ac67617.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-79e6541a.js → xychartDiagram-e933f94c-c018dc37.js} +1 -1
  40. rasa/core/channels/inspector/dist/index.html +2 -2
  41. rasa/core/channels/inspector/index.html +1 -1
  42. rasa/core/channels/inspector/src/App.tsx +53 -7
  43. rasa/core/channels/inspector/src/components/Chat.tsx +3 -2
  44. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +1 -1
  45. rasa/core/channels/inspector/src/components/LatencyDisplay.tsx +268 -0
  46. rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -2
  47. rasa/core/channels/inspector/src/helpers/audio/audiostream.ts +8 -3
  48. rasa/core/channels/inspector/src/types.ts +8 -0
  49. rasa/core/channels/studio_chat.py +59 -15
  50. rasa/core/channels/voice_stream/audiocodes.py +2 -2
  51. rasa/core/channels/voice_stream/browser_audio.py +20 -3
  52. rasa/core/channels/voice_stream/call_state.py +13 -2
  53. rasa/core/channels/voice_stream/genesys.py +2 -2
  54. rasa/core/channels/voice_stream/jambonz.py +2 -2
  55. rasa/core/channels/voice_stream/twilio_media_streams.py +2 -2
  56. rasa/core/channels/voice_stream/voice_channel.py +83 -13
  57. rasa/core/nlg/contextual_response_rephraser.py +13 -2
  58. rasa/dialogue_understanding/processor/command_processor.py +27 -11
  59. rasa/model_manager/socket_bridge.py +1 -2
  60. rasa/studio/upload.py +7 -4
  61. rasa/studio/utils.py +33 -22
  62. rasa/version.py +1 -1
  63. {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/METADATA +6 -6
  64. {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/RECORD +67 -66
  65. rasa/core/channels/inspector/dist/assets/channel-51d02e9e.js +0 -1
  66. rasa/core/channels/inspector/dist/assets/clone-cc738fa6.js +0 -1
  67. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-0c716443.js +0 -1
  68. {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/NOTICE +0 -0
  69. {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/WHEEL +0 -0
  70. {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,7 @@ import {
5
5
  useColorModeValue,
6
6
  useToast,
7
7
  } from '@chakra-ui/react'
8
- import { useEffect, useState } from 'react'
8
+ import { useEffect, useState, useCallback } from 'react'
9
9
  import axios from 'axios'
10
10
  import { useOurTheme } from './theme'
11
11
  import { Welcome } from './components/Welcome'
@@ -23,6 +23,7 @@ import {
23
23
  } from './helpers/utils'
24
24
  import queryString from 'query-string'
25
25
  import { Chat } from './components/Chat'
26
+ import { LatencyDisplay } from './components/LatencyDisplay'
26
27
  import useWebSocket, { ReadyState } from 'react-use-websocket'
27
28
 
28
29
  export function App() {
@@ -35,6 +36,7 @@ export function App() {
35
36
  const [story, setStory] = useState<string>('')
36
37
  const [stack, setStack] = useState<Stack[]>([])
37
38
  const [frame, setFrame] = useState<SelectedStack | undefined>(undefined)
39
+ const [latency, setLatency] = useState<any>(null)
38
40
 
39
41
  // State to control the visibility of the RecruitmentPanel
40
42
  const [showRecruitmentPanel, setShowRecruitmentPanel] = useState(true)
@@ -127,6 +129,13 @@ export function App() {
127
129
  !rasaChatSessionId ||
128
130
  lastJsonMessage?.sender_id === rasaChatSessionId
129
131
  ) {
132
+ if (lastJsonMessage.latency) {
133
+ console.log('Latency update:', lastJsonMessage.latency)
134
+ setLatency((prevLatency: any) => ({
135
+ ...prevLatency,
136
+ ...lastJsonMessage.latency,
137
+ }))
138
+ }
130
139
  setSlots(formatSlots(lastJsonMessage.slots))
131
140
  setEvents(lastJsonMessage.events)
132
141
  const updatedStack = createHistoricalStack(
@@ -175,6 +184,21 @@ export function App() {
175
184
  : 'max-content minmax(10rem, 17.5rem) minmax(10rem, auto)',
176
185
  gridRowGap: rasaSpace[1],
177
186
  }
187
+ const rightColumnSx = {
188
+ height: '100%',
189
+ overflow: 'hidden',
190
+ gridTemplateColumns: '1fr',
191
+ gridTemplateRows: 'max-content 1fr',
192
+ gridRowGap: rasaSpace[1],
193
+ }
194
+
195
+ const chatContainerSx = {
196
+ ...borderRadiusSx,
197
+ padding: rasaSpace[1],
198
+ bg: useColorModeValue('neutral.50', 'neutral.50'),
199
+ overflow: 'auto', // Allow scrolling for chat
200
+ height: '100%',
201
+ }
178
202
 
179
203
  const onFrameSelected = (stack: Stack) => {
180
204
  setFrame({
@@ -188,8 +212,25 @@ export function App() {
188
212
  setShowRecruitmentPanel(false)
189
213
  }
190
214
 
215
+ const onLatencyUpdate = useCallback((newLatency: any) => {
216
+ setLatency((prevLatency: any) => ({
217
+ ...prevLatency,
218
+ ...newLatency,
219
+ }))
220
+ }, [])
221
+
222
+ // Make latency update function available globally for audio stream
223
+ useEffect(() => {
224
+ if (window.location.href.includes('browser_audio')) {
225
+ ;(window as any).updateLatency = onLatencyUpdate
226
+ }
227
+ return () => {
228
+ delete (window as any).updateLatency
229
+ }
230
+ }, [onLatencyUpdate])
231
+
191
232
  if (!rasaChatSessionId && !window.location.href.includes('socketio'))
192
- return <LoadingSpinner />
233
+ return <LoadingSpinner onLatencyUpdate={onLatencyUpdate} />
193
234
 
194
235
  return (
195
236
  <Grid sx={gridSx}>
@@ -222,11 +263,16 @@ export function App() {
222
263
  slots={slots}
223
264
  />
224
265
  </GridItem>
225
- {shouldShowTranscript && (
226
- <GridItem>
227
- <Chat events={events || []} />
228
- </GridItem>
229
- )}
266
+ <GridItem overflow="hidden">
267
+ <Grid sx={rightColumnSx}>
268
+ <LatencyDisplay latency={latency} sx={boxSx} />
269
+ {shouldShowTranscript && (
270
+ <GridItem sx={chatContainerSx}>
271
+ <Chat events={events || []} />
272
+ </GridItem>
273
+ )}
274
+ </Grid>
275
+ </GridItem>
230
276
  </Grid>
231
277
  )
232
278
  }
@@ -5,6 +5,7 @@ import { Command, Event } from '../types'
5
5
 
6
6
  interface Props extends FlexProps {
7
7
  events: Event[]
8
+ hasLatencyDisplay?: boolean
8
9
  }
9
10
 
10
11
  export const Chat = ({ sx, events, ...props }: Props) => {
@@ -12,9 +13,9 @@ export const Chat = ({ sx, events, ...props }: Props) => {
12
13
  ...sx,
13
14
  p: 0,
14
15
  flexDirection: 'column',
16
+ height: '100%',
15
17
  }
16
18
 
17
- const maxHeight = document.documentElement.scrollHeight - 64
18
19
  // 21 and 25 are the rem number we're using for the columns. We add 0.75rem for the padding
19
20
  // A potential improvement would be to add a onresize event for both width and height
20
21
  let remReference = 21.75
@@ -110,7 +111,7 @@ export const Chat = ({ sx, events, ...props }: Props) => {
110
111
  borderRadius: '10px',
111
112
  border: 'none',
112
113
  width: columnWidth,
113
- height: maxHeight,
114
+ height: '100%',
114
115
  }}
115
116
  history={messages}
116
117
  demo={true}
@@ -22,7 +22,7 @@ export const DiagramFlow = ({ stackFrame, stepTrail, flows, slots }: Props) => {
22
22
 
23
23
  const config = {
24
24
  startOnLoad: true,
25
- logLevel: 'info',
25
+ logLevel: 'warning',
26
26
  flowchart: {
27
27
  useMaxWidth: false,
28
28
  },
@@ -0,0 +1,268 @@
1
+ import {
2
+ Box,
3
+ Flex,
4
+ FlexProps,
5
+ Text,
6
+ useColorModeValue,
7
+ Tooltip,
8
+ Table,
9
+ Tbody,
10
+ Tr,
11
+ Td,
12
+ } from '@chakra-ui/react'
13
+ import { useOurTheme } from '../theme'
14
+ import { LatencyData } from '../types'
15
+
16
+ interface Props extends FlexProps {
17
+ latency: LatencyData
18
+ }
19
+
20
+ /**
21
+ * Simple latency display for text-only conversations.
22
+ * Shows a single response time value.
23
+ */
24
+ const MinimalDisplay = ({ latency, sx, ...props }: Props) => {
25
+ const containerSx = {
26
+ ...sx,
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ }
30
+
31
+ const getLatencyColor = (latency: number) => {
32
+ if (latency < 1500) return 'green.500'
33
+ if (latency < 2500) return 'orange.500'
34
+ return 'red.500'
35
+ }
36
+
37
+ const value = Math.round(latency.rasa_processing_latency_ms || 0);
38
+ const color = getLatencyColor(value);
39
+
40
+ return (
41
+ <Flex sx={containerSx} {...props}>
42
+ <Text fontSize="md" color={useColorModeValue('gray.700', 'gray.300')}>
43
+ Response latency:
44
+ <Text as="span" ml={2} fontWeight="bold" color={color}>
45
+ {value}ms
46
+ </Text>
47
+ </Text>
48
+ </Flex>
49
+ )
50
+ }
51
+
52
+ /**
53
+ * Detailed latency waterfall chart for voice conversations.
54
+ * Displays processing times for ASR, Rasa, and TTS components.
55
+ */
56
+ const WaterfallDisplay = ({ latency, sx, ...props }: Props) => {
57
+ const { rasaSpace } = useOurTheme()
58
+
59
+ const containerSx = {
60
+ ...sx,
61
+ flexDirection: 'column',
62
+ gap: rasaSpace[1],
63
+ }
64
+
65
+ const headerSx = {
66
+ fontSize: 'sm',
67
+ fontWeight: 'bold',
68
+ color: useColorModeValue('gray.700', 'gray.300'),
69
+ }
70
+
71
+ const waterfallBarSx = {
72
+ height: '24px',
73
+ borderRadius: '4px',
74
+ overflow: 'hidden',
75
+ border: '1px solid',
76
+ borderColor: useColorModeValue('gray.200', 'gray.600'),
77
+ }
78
+
79
+ const legendTableSx = {
80
+ size: 'sm',
81
+ mt: rasaSpace[0.5]
82
+ }
83
+
84
+ const getLatencyColor = (type: string) => {
85
+ const colors: { [key: string]: string } = {
86
+ asr: 'blue.500',
87
+ rasa: 'purple.500',
88
+ tts_first: 'orange.500',
89
+ tts_complete: 'green.500',
90
+ }
91
+ return colors[type] || 'gray.500'
92
+ }
93
+
94
+ const getLatencyDescription = (type: string) => {
95
+ const descriptions: { [key: string]: string } = {
96
+ asr: 'Time from the first Partial Transcript event to the Final Transcript event from Speech Recognition. It also includes the time taken by the user to speak.',
97
+ rasa: 'Time taken by Rasa to process the text from the Final Transcript event from Speech Recognition until a text response is generated.',
98
+ tts_first: 'Time between the request sent to Text-to-Speech processing and the first byte of audio received by Rasa.',
99
+ tts_complete: 'Time taken by Text-to-Speech to complete audio generation. It depends on the length of the text and could overlap with the Bot speaking time.'
100
+ }
101
+ return descriptions[type] || ''
102
+ }
103
+
104
+ // Metrics for proportional display (only actual processing latencies)
105
+ const chartMetrics = [
106
+ latency.asr_latency_ms && {
107
+ type: 'asr',
108
+ label: 'ASR',
109
+ value: latency.asr_latency_ms,
110
+ },
111
+ latency.rasa_processing_latency_ms && {
112
+ type: 'rasa',
113
+ label: 'Rasa',
114
+ value: latency.rasa_processing_latency_ms,
115
+ },
116
+ latency.tts_complete_latency_ms && {
117
+ type: 'tts_complete',
118
+ label: 'TTS',
119
+ value: latency.tts_complete_latency_ms,
120
+ },
121
+ ].filter(Boolean)
122
+
123
+ // All metrics for legend display
124
+ const allMetrics = [
125
+ latency.asr_latency_ms && {
126
+ type: 'asr',
127
+ label: 'ASR',
128
+ value: latency.asr_latency_ms,
129
+ },
130
+ latency.rasa_processing_latency_ms && {
131
+ type: 'rasa',
132
+ label: 'Rasa',
133
+ value: latency.rasa_processing_latency_ms,
134
+ },
135
+ latency.tts_first_byte_latency_ms && {
136
+ type: 'tts_first',
137
+ label: 'TTS First Byte',
138
+ value: latency.tts_first_byte_latency_ms,
139
+ },
140
+ latency.tts_complete_latency_ms && {
141
+ type: 'tts_complete',
142
+ label: 'TTS Complete',
143
+ value: latency.tts_complete_latency_ms,
144
+ },
145
+ ].filter(Boolean)
146
+
147
+ // Calculate total for proportional sizing (only processing latencies)
148
+ const totalLatency = chartMetrics.reduce(
149
+ (sum: number, metric: any) => sum + metric.value,
150
+ 0,
151
+ )
152
+
153
+ // Calculate total latency for title (Rasa + TTS First Byte)
154
+ const totalDisplayLatency =
155
+ (latency.rasa_processing_latency_ms || 0) +
156
+ (latency.tts_first_byte_latency_ms || 0);
157
+
158
+ return (
159
+ <Flex sx={containerSx} {...props}>
160
+ <Text sx={headerSx}>
161
+ Response latency: ~{Math.round(totalDisplayLatency)}ms
162
+ </Text>
163
+
164
+ {/* Waterfall Bar */}
165
+ <Box>
166
+ <Flex sx={waterfallBarSx}>
167
+ {chartMetrics.map((metric: any) => {
168
+ const widthPercentage =
169
+ totalLatency > 0 ? (metric.value / totalLatency) * 100 : 0
170
+ return (
171
+ <Tooltip
172
+ key={metric.type}
173
+ label={
174
+ <Box>
175
+ <Text fontWeight="bold">
176
+ {metric.label}: {Math.round(metric.value)}ms
177
+ </Text>
178
+ <Text fontSize="xs" mt={1}>
179
+ {getLatencyDescription(metric.type)}
180
+ </Text>
181
+ </Box>
182
+ }
183
+ hasArrow
184
+ placement="top"
185
+ >
186
+ <Box
187
+ bg={getLatencyColor(metric.type)}
188
+ width={`${widthPercentage}%`}
189
+ height="100%"
190
+ minWidth="40px" // Increased minimum width for better visibility
191
+ cursor="pointer"
192
+ _hover={{ opacity: 0.8 }}
193
+ />
194
+ </Tooltip>
195
+ )
196
+ })}
197
+ </Flex>
198
+
199
+ {/* Legend */}
200
+ <Table sx={legendTableSx}>
201
+ <Tbody>
202
+ {/* Split metrics into pairs for 2x2 table */}
203
+ {Array.from(
204
+ { length: Math.ceil(allMetrics.length / 2) },
205
+ (_, rowIndex) => (
206
+ <Tr key={rowIndex}>
207
+ {allMetrics
208
+ .slice(rowIndex * 2, rowIndex * 2 + 2)
209
+ .map((metric: any) => (
210
+ <Td key={metric.type} p={rasaSpace[0.25]} border="none">
211
+ <Flex align="center" gap={rasaSpace[0.25]}>
212
+ <Box
213
+ width="8px"
214
+ height="8px"
215
+ bg={getLatencyColor(metric.type)}
216
+ borderRadius="2px"
217
+ flexShrink={0}
218
+ />
219
+ <Text
220
+ fontSize="xs"
221
+ color={useColorModeValue('gray.600', 'gray.400')}
222
+ noOfLines={1}
223
+ >
224
+ {metric.label}: {Math.round(metric.value)}ms
225
+ </Text>
226
+ </Flex>
227
+ </Td>
228
+ ))}
229
+ {/* Fill empty cell if odd number of metrics */}
230
+ {allMetrics.length % 2 !== 0 &&
231
+ rowIndex === Math.ceil(allMetrics.length / 2) - 1 && (
232
+ <Td p={rasaSpace[0.25]} border="none" />
233
+ )}
234
+ </Tr>
235
+ ),
236
+ )}
237
+ </Tbody>
238
+ </Table>
239
+ </Box>
240
+ </Flex>
241
+ )
242
+ }
243
+
244
+ /**
245
+ * Displays processing latency information for the conversation.
246
+ * Shows either a detailed waterfall chart for voice conversations or
247
+ * a simpler display for text-only conversations.
248
+ */
249
+ export const LatencyDisplay = ({
250
+ sx,
251
+ latency,
252
+ ...props
253
+ }: Props) => {
254
+ if (!latency) {
255
+ console.warn('Latency data is not available')
256
+ return null
257
+ }
258
+
259
+ // Show waterfall if voice metrics are available, otherwise show minimal display
260
+ const isVoiceMetricsAvailable =
261
+ latency.asr_latency_ms && latency.tts_complete_latency_ms
262
+
263
+ if (isVoiceMetricsAvailable) {
264
+ return <WaterfallDisplay latency={latency} sx={sx} {...props} />
265
+ }
266
+
267
+ return <MinimalDisplay latency={latency} sx={sx} {...props} />
268
+ }
@@ -8,7 +8,11 @@ import {
8
8
  import { useOurTheme } from '../theme'
9
9
  import { createAudioConnection } from '../helpers/audio/audiostream.ts'
10
10
 
11
- export const LoadingSpinner = () => {
11
+ interface LoadingSpinnerProps {
12
+ onLatencyUpdate?: (latency: any) => void
13
+ }
14
+
15
+ export const LoadingSpinner = ({ onLatencyUpdate }: LoadingSpinnerProps) => {
12
16
  const { rasaSpace } = useOurTheme()
13
17
  const isVoice = window.location.href.includes('browser_audio')
14
18
  const text = isVoice
@@ -27,7 +31,7 @@ export const LoadingSpinner = () => {
27
31
  {isVoice ? (
28
32
  <Button
29
33
  onClick={async () =>
30
- await createAudioConnection(window.location.href)
34
+ await createAudioConnection(window.location.href, onLatencyUpdate)
31
35
  }
32
36
  >
33
37
  Go
@@ -187,7 +187,7 @@ const setupAudioPlayback = async (socket: WebSocket): Promise<AudioQueue> => {
187
187
  }
188
188
 
189
189
  const addDataToAudioQueue =
190
- (audioQueue: AudioQueue) => (message: MessageEvent<any>) => {
190
+ (audioQueue: AudioQueue, onLatencyUpdate?: (latency: any) => void) => (message: MessageEvent<any>) => {
191
191
  try {
192
192
  const data = JSON.parse(message.data.toString())
193
193
  if (data['error']) {
@@ -199,6 +199,10 @@ const addDataToAudioQueue =
199
199
  const audioData = intToFloatArray(int32Data)
200
200
  audioQueue.write(audioData)
201
201
  } else if (data['marker']) {
202
+ if (data['latency'] && onLatencyUpdate) {
203
+ onLatencyUpdate(data['latency'])
204
+ }
205
+ console.log('Voice Latency Metrics:', data['latency'])
202
206
  audioQueue.addMarker(data['marker'])
203
207
  }
204
208
  } catch (error) {
@@ -231,8 +235,9 @@ function getWebSocketUrl(baseUrl: string) {
231
235
  * Creates a WebSocket connection for browser audio and streams microphone input to the server
232
236
  *
233
237
  * @param baseUrl - The base URL (e.g., "https://example.com" or "http://localhost:5005")
238
+ * @param onLatencyUpdate - Optional callback function to receive latency updates
234
239
  */
235
- export async function createAudioConnection(baseUrl: string) {
240
+ export async function createAudioConnection(baseUrl: string, onLatencyUpdate?: (latency: any) => void) {
236
241
  const websocketURL = getWebSocketUrl(baseUrl)
237
242
  const socket = new WebSocket(websocketURL)
238
243
 
@@ -241,5 +246,5 @@ export async function createAudioConnection(baseUrl: string) {
241
246
  }
242
247
 
243
248
  const audioQueue = await setupAudioPlayback(socket)
244
- socket.onmessage = addDataToAudioQueue(audioQueue)
249
+ socket.onmessage = addDataToAudioQueue(audioQueue, onLatencyUpdate)
245
250
  }
@@ -42,11 +42,19 @@ export interface Stack {
42
42
  ended: boolean
43
43
  }
44
44
 
45
+ export interface LatencyData {
46
+ rasa_processing_latency_ms?: number
47
+ asr_latency_ms?: number
48
+ tts_first_byte_latency_ms?: number
49
+ tts_complete_latency_ms?: number
50
+ }
51
+
45
52
  export interface Tracker {
46
53
  sender_id: string
47
54
  slots: { [key: string]: unknown }
48
55
  events: Event[]
49
56
  stack: Stack[]
57
+ latency?: LatencyData
50
58
  }
51
59
 
52
60
  export interface Flow {
@@ -4,6 +4,7 @@ import asyncio
4
4
  import audioop
5
5
  import base64
6
6
  import json
7
+ import time
7
8
  import uuid
8
9
  from functools import partial
9
10
  from typing import (
@@ -18,6 +19,7 @@ from typing import (
18
19
  Tuple,
19
20
  )
20
21
 
22
+ import orjson
21
23
  import structlog
22
24
 
23
25
  from rasa.core.channels import UserMessage
@@ -52,7 +54,9 @@ if TYPE_CHECKING:
52
54
  structlogger = structlog.get_logger()
53
55
 
54
56
 
55
- def tracker_as_dump(tracker: "DialogueStateTracker") -> str:
57
+ def tracker_as_dump(
58
+ tracker: "DialogueStateTracker", latency: Optional[float] = None
59
+ ) -> str:
56
60
  """Create a dump of the tracker state."""
57
61
  from rasa.shared.core.trackers import get_trackers_for_conversation_sessions
58
62
 
@@ -64,7 +68,10 @@ def tracker_as_dump(tracker: "DialogueStateTracker") -> str:
64
68
  last_tracker = multiple_tracker_sessions[-1]
65
69
 
66
70
  state = last_tracker.current_state(EventVerbosity.AFTER_RESTART)
67
- return json.dumps(state)
71
+
72
+ if latency is not None:
73
+ state["latency"] = {"rasa_processing_latency_ms": latency}
74
+ return orjson.dumps(state, option=orjson.OPT_SERIALIZE_NUMPY).decode("utf-8")
68
75
 
69
76
 
70
77
  def does_need_action_prediction(tracker: "DialogueStateTracker") -> bool:
@@ -178,6 +185,7 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
178
185
  # `background_tasks` holds the asyncio tasks for voice streaming
179
186
  self.active_connections: Dict[str, SocketIOVoiceWebsocketAdapter] = {}
180
187
  self.background_tasks: Dict[str, asyncio.Task] = {}
188
+ self._turn_start_times: Dict[Text, float] = {}
181
189
 
182
190
  self._register_tracker_update_hook()
183
191
 
@@ -204,7 +212,7 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
204
212
  metadata_key=credentials.get("metadata_key", "metadata"),
205
213
  )
206
214
 
207
- async def emit(self, event: str, data: Dict, room: str) -> None:
215
+ async def emit(self, event: str, data: str, room: str) -> None:
208
216
  """Emits an event to the websocket."""
209
217
  if not self.sio:
210
218
  structlogger.error("studio_chat.emit.sio_not_initialized")
@@ -214,14 +222,32 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
214
222
  def _register_tracker_update_hook(self) -> None:
215
223
  plugin_manager().register(StudioTrackerUpdatePlugin(self))
216
224
 
217
- async def on_tracker_updated(self, tracker: "DialogueStateTracker") -> None:
225
+ async def on_tracker_updated(
226
+ self, tracker: "DialogueStateTracker", latency: Optional[float] = None
227
+ ) -> None:
218
228
  """Triggers a tracker update notification after a change to the tracker."""
219
- await self.publish_tracker_update(tracker.sender_id, tracker_as_dump(tracker))
229
+ await self.publish_tracker_update(
230
+ tracker.sender_id, tracker_as_dump(tracker, latency)
231
+ )
220
232
 
221
- async def publish_tracker_update(self, sender_id: str, tracker_dump: Dict) -> None:
233
+ async def publish_tracker_update(self, sender_id: str, tracker_dump: str) -> None:
222
234
  """Publishes a tracker update notification to the websocket."""
223
235
  await self.emit("tracker", tracker_dump, room=sender_id)
224
236
 
237
+ def _record_turn_start_time(self, sender_id: Text) -> None:
238
+ """Records the start time of a new turn."""
239
+ self._turn_start_times[sender_id] = time.time()
240
+
241
+ def _get_latency(self, sender_id: Text) -> Optional[float]:
242
+ """Returns the latency of the current turn in milliseconds."""
243
+ if sender_id not in self._turn_start_times:
244
+ return None
245
+
246
+ latency = (time.time() - self._turn_start_times[sender_id]) * 1000
247
+ # The turn is over, so we can remove the start time
248
+ del self._turn_start_times[sender_id]
249
+ return latency
250
+
225
251
  async def on_message_proxy(
226
252
  self,
227
253
  on_new_message: Callable[["UserMessage"], Awaitable[Any]],
@@ -231,6 +257,7 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
231
257
 
232
258
  Triggers a tracker update notification after processing the message.
233
259
  """
260
+ self._record_turn_start_time(message.sender_id)
234
261
  await on_new_message(message)
235
262
 
236
263
  if not self.agent or not self.agent.is_ready():
@@ -249,7 +276,8 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
249
276
  structlogger.error("studio_chat.on_message_proxy.tracker_not_found")
250
277
  return
251
278
 
252
- await self.on_tracker_updated(tracker)
279
+ latency = self._get_latency(message.sender_id)
280
+ await self.on_tracker_updated(tracker, latency)
253
281
 
254
282
  async def emit_error(self, message: str, room: str, e: Exception) -> None:
255
283
  await self.emit(
@@ -339,14 +367,14 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
339
367
  elif "marker" in message:
340
368
  if message["marker"] == call_state.latest_bot_audio_id:
341
369
  # Just finished streaming last audio bytes
342
- call_state.is_bot_speaking = False # type: ignore[attr-defined]
370
+ call_state.is_bot_speaking = False
343
371
  if call_state.should_hangup:
344
372
  structlogger.debug(
345
373
  "studio_chat.hangup", marker=call_state.latest_bot_audio_id
346
374
  )
347
375
  return EndConversationAction()
348
376
  else:
349
- call_state.is_bot_speaking = True # type: ignore[attr-defined]
377
+ call_state.is_bot_speaking = True
350
378
  return ContinueConversationAction()
351
379
 
352
380
  def create_output_channel(
@@ -429,9 +457,8 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
429
457
  def blueprint(
430
458
  self, on_new_message: Callable[["UserMessage"], Awaitable[Any]]
431
459
  ) -> SocketBlueprint:
432
- socket_blueprint = super().blueprint(
433
- partial(self.on_message_proxy, on_new_message)
434
- )
460
+ proxied_on_message = partial(self.on_message_proxy, on_new_message)
461
+ socket_blueprint = super().blueprint(proxied_on_message)
435
462
 
436
463
  if not self.sio:
437
464
  structlogger.error("studio_chat.blueprint.sio_not_initialized")
@@ -466,7 +493,7 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
466
493
 
467
494
  # start a voice session if requested
468
495
  if data and data.get("is_voice", False):
469
- self._start_voice_session(data["session_id"], sid, on_new_message)
496
+ self._start_voice_session(data["session_id"], sid, proxied_on_message)
470
497
 
471
498
  @self.sio.on(self.user_message_evt, namespace=self.namespace)
472
499
  async def handle_message(sid: Text, data: Dict) -> None:
@@ -480,7 +507,7 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
480
507
  return
481
508
 
482
509
  # Handle text messages
483
- await self.handle_user_message(sid, data, on_new_message)
510
+ await self.handle_user_message(sid, data, proxied_on_message)
484
511
 
485
512
  @self.sio.on("update_tracker", namespace=self.namespace)
486
513
  async def on_update_tracker(sid: Text, data: Dict) -> None:
@@ -504,7 +531,24 @@ class StudioVoiceOutputChannel(VoiceOutputChannel):
504
531
 
505
532
  def create_marker_message(self, recipient_id: str) -> Tuple[str, str]:
506
533
  message_id = uuid.uuid4().hex
507
- return json.dumps({"marker": message_id}), message_id
534
+ marker_data = {"marker": message_id}
535
+
536
+ # Include comprehensive latency information if available
537
+ latency_data = {
538
+ "asr_latency_ms": call_state.asr_latency_ms,
539
+ "rasa_processing_latency_ms": call_state.rasa_processing_latency_ms,
540
+ "tts_first_byte_latency_ms": call_state.tts_first_byte_latency_ms,
541
+ "tts_complete_latency_ms": call_state.tts_complete_latency_ms,
542
+ }
543
+
544
+ # Filter out None values from latency data
545
+ latency_data = {k: v for k, v in latency_data.items() if v is not None}
546
+
547
+ # Add latency data to marker if any metrics are available
548
+ if latency_data:
549
+ marker_data["latency"] = latency_data # type: ignore[assignment]
550
+
551
+ return json.dumps(marker_data), message_id
508
552
 
509
553
 
510
554
  class SocketIOVoiceWebsocketAdapter:
@@ -88,7 +88,7 @@ class AudiocodesVoiceOutputChannel(VoiceOutputChannel):
88
88
  # however, Audiocodes does not have an event to indicate that.
89
89
  # This is an approximation, as the bot will be sent the audio chunks next
90
90
  # which are played to the user immediately.
91
- call_state.is_bot_speaking = True # type: ignore[attr-defined]
91
+ call_state.is_bot_speaking = True
92
92
 
93
93
  async def send_intermediate_marker(self, recipient_id: str) -> None:
94
94
  """Audiocodes doesn't need intermediate markers, so do nothing."""
@@ -187,7 +187,7 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
187
187
  pass
188
188
  elif activity["name"] == "playFinished":
189
189
  logger.debug("audiocodes_stream.playFinished", data=activity)
190
- call_state.is_bot_speaking = False # type: ignore[attr-defined]
190
+ call_state.is_bot_speaking = False
191
191
  if call_state.should_hangup:
192
192
  logger.info("audiocodes_stream.hangup")
193
193
  self._send_hangup(ws, data)