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.
- rasa/core/channels/development_inspector.py +47 -14
- rasa/core/channels/inspector/dist/assets/{arc-0b11fe30.js → arc-1ddec37b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-9eef30a7.js → blockDiagram-38ab4fdb-18af387c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-03e94f28.js → c4Diagram-3d4e48cf-250127a3.js} +1 -1
- rasa/core/channels/inspector/dist/assets/channel-59f6d54b.js +1 -0
- rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-95c09eba.js → classDiagram-70f12bd4-c3388b34.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-38e8446c.js → classDiagram-v2-f2320105-9c893a82.js} +1 -1
- rasa/core/channels/inspector/dist/assets/clone-26177ddb.js +1 -0
- rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-57dc3038.js → createText-2e5e7dd3-c111213b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-4bac0545.js → edges-e0da2a9e-812a729d.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-81795c90.js → erDiagram-9861fffd-fd5051bc.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-89489ae6.js → flowDb-956e92f1-3287ac02.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-cd152627.js → flowDiagram-66a62f08-692fb0b2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-29c03f5a.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-3da369bc.js → flowchart-elk-definition-4a651766-008376f1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-85ec16f8.js → ganttDiagram-c361ad54-df330a69.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-495bc140.js → gitGraphDiagram-72cf32ee-e03676fb.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{graph-1ec4d266.js → graph-46fad2ba.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-3862675e-0a0e97c9.js → index-3862675e-a484ac55.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-c804b295.js → index-a003633f.js} +164 -164
- rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-4d54bcde.js → infoDiagram-f8f76790-3f9e6ec2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-dc097114.js → journeyDiagram-49397b02-79f72383.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-1a08981e.js → layout-aad098e5.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-95f7f1d3.js → line-219ab7ae.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-97e69543.js → linear-2cddbe62.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-8c71ff03.js → mindmap-definition-fc14e90a-1d41ed99.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-f14c71c7.js → pieDiagram-8a3498a8-cc496ee8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-f1d3c9ff.js → quadrantDiagram-120e2f19-84d32884.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-bfa2412f.js → requirementDiagram-deff3bca-c0deb984.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-53f2c97b.js → sankeyDiagram-04a897e0-b9d7fd62.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-319d7c0e.js → sequenceDiagram-704730f1-7d517565.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-76a09418.js → stateDiagram-587899a1-98ef9b27.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-a67f15d4.js → stateDiagram-v2-d93cdb3a-cee70748.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-0654e7c3.js → styles-6aaf32cf-3f9d1c96.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9a916d00-1394bb9d.js → styles-9a916d00-67471923.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-c10674c1-e4c5bdae.js → styles-c10674c1-bd093fb7.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-50957104.js → svgDrawCommon-08f97a94-675794e8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-b0885a6a.js → timeline-definition-85554ec2-0ac67617.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-79e6541a.js → xychartDiagram-e933f94c-c018dc37.js} +1 -1
- rasa/core/channels/inspector/dist/index.html +2 -2
- rasa/core/channels/inspector/index.html +1 -1
- rasa/core/channels/inspector/src/App.tsx +53 -7
- rasa/core/channels/inspector/src/components/Chat.tsx +3 -2
- rasa/core/channels/inspector/src/components/DiagramFlow.tsx +1 -1
- rasa/core/channels/inspector/src/components/LatencyDisplay.tsx +268 -0
- rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -2
- rasa/core/channels/inspector/src/helpers/audio/audiostream.ts +8 -3
- rasa/core/channels/inspector/src/types.ts +8 -0
- rasa/core/channels/studio_chat.py +59 -15
- rasa/core/channels/voice_stream/audiocodes.py +2 -2
- rasa/core/channels/voice_stream/browser_audio.py +20 -3
- rasa/core/channels/voice_stream/call_state.py +13 -2
- rasa/core/channels/voice_stream/genesys.py +2 -2
- rasa/core/channels/voice_stream/jambonz.py +2 -2
- rasa/core/channels/voice_stream/twilio_media_streams.py +2 -2
- rasa/core/channels/voice_stream/voice_channel.py +83 -13
- rasa/core/nlg/contextual_response_rephraser.py +13 -2
- rasa/dialogue_understanding/processor/command_processor.py +27 -11
- rasa/model_manager/socket_bridge.py +1 -2
- rasa/studio/upload.py +7 -4
- rasa/studio/utils.py +33 -22
- rasa/version.py +1 -1
- {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/METADATA +6 -6
- {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/RECORD +67 -66
- rasa/core/channels/inspector/dist/assets/channel-51d02e9e.js +0 -1
- rasa/core/channels/inspector/dist/assets/clone-cc738fa6.js +0 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-0c716443.js +0 -1
- {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/NOTICE +0 -0
- {rasa_pro-3.14.0.dev20250731.dist-info → rasa_pro-3.14.0.dev20250818.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
226
|
-
<
|
|
227
|
-
<
|
|
228
|
-
|
|
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:
|
|
114
|
+
height: '100%',
|
|
114
115
|
}}
|
|
115
116
|
history={messages}
|
|
116
117
|
demo={true}
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
433
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|