rasa-pro 3.12.0rc2__py3-none-any.whl → 3.12.1__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 (99) hide show
  1. rasa/cli/dialogue_understanding_test.py +5 -8
  2. rasa/cli/llm_fine_tuning.py +47 -12
  3. rasa/cli/train.py +3 -0
  4. rasa/cli/utils.py +6 -0
  5. rasa/core/channels/development_inspector.py +77 -21
  6. rasa/core/channels/inspector/dist/assets/{arc-f0f8bd46.js → arc-9f1365dc.js} +1 -1
  7. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-7162c77d.js → blockDiagram-38ab4fdb-e0f81b12.js} +1 -1
  8. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-b1d0d098.js → c4Diagram-3d4e48cf-9deaee1c.js} +1 -1
  9. rasa/core/channels/inspector/dist/assets/channel-44956714.js +1 -0
  10. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-807a1b27.js → classDiagram-70f12bd4-20450a96.js} +1 -1
  11. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-5238dcdb.js → classDiagram-v2-f2320105-749d2abf.js} +1 -1
  12. rasa/core/channels/inspector/dist/assets/clone-a9475142.js +1 -0
  13. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-75dfaa67.js → createText-2e5e7dd3-bef0b38c.js} +1 -1
  14. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-df20501d.js → edges-e0da2a9e-943801a7.js} +1 -1
  15. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-13cf4797.js → erDiagram-9861fffd-d523a948.js} +1 -1
  16. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-a4991264.js → flowDb-956e92f1-54e4cf19.js} +1 -1
  17. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-ccecf773.js → flowDiagram-66a62f08-48bfbbe8.js} +1 -1
  18. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-43fa749a.js +1 -0
  19. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-b5801783.js → flowchart-elk-definition-4a651766-17c30827.js} +1 -1
  20. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-161e079a.js → ganttDiagram-c361ad54-43086f2d.js} +1 -1
  21. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-f38e86a4.js → gitGraphDiagram-72cf32ee-5c8b693e.js} +1 -1
  22. rasa/core/channels/inspector/dist/assets/{graph-be6ef5d8.js → graph-41a90d26.js} +1 -1
  23. rasa/core/channels/inspector/dist/assets/{index-3862675e-d9ce8994.js → index-3862675e-b43eeae9.js} +1 -1
  24. rasa/core/channels/inspector/dist/assets/{index-7794b245.js → index-e8affe45.js} +155 -155
  25. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-5000a3dc.js → infoDiagram-f8f76790-0b20676b.js} +1 -1
  26. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-8ef0a17a.js → journeyDiagram-49397b02-39bce7b5.js} +1 -1
  27. rasa/core/channels/inspector/dist/assets/{layout-d649bc98.js → layout-dc8eeea4.js} +1 -1
  28. rasa/core/channels/inspector/dist/assets/{line-95add810.js → line-c4d2e756.js} +1 -1
  29. rasa/core/channels/inspector/dist/assets/{linear-f6025094.js → linear-86f6f2d9.js} +1 -1
  30. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-2e8531c4.js → mindmap-definition-fc14e90a-4216f771.js} +1 -1
  31. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-918adfdb.js → pieDiagram-8a3498a8-1a0cfa96.js} +1 -1
  32. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-cbd01797.js → quadrantDiagram-120e2f19-f91e67cf.js} +1 -1
  33. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-6a8b877b.js → requirementDiagram-deff3bca-d4046bed.js} +1 -1
  34. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-c377c3fe.js → sankeyDiagram-04a897e0-2cf6d1d7.js} +1 -1
  35. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-ab9e9b7f.js → sequenceDiagram-704730f1-751ac4f5.js} +1 -1
  36. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-5e6ae67d.js → stateDiagram-587899a1-f734f4d4.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-40643476.js → stateDiagram-v2-d93cdb3a-91c65710.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-afb8d108.js → styles-6aaf32cf-e0cff7be.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-7edc9423.js → styles-9a916d00-c8029e5d.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-c1d8f7e9.js → styles-c10674c1-114f312a.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-f494b2ef.js → svgDrawCommon-08f97a94-b7b9dc00.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-11c7cdd0.js → timeline-definition-85554ec2-9536d189.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-3f191ec1.js → xychartDiagram-e933f94c-bf3b0f36.js} +1 -1
  44. rasa/core/channels/inspector/dist/index.html +1 -1
  45. rasa/core/channels/inspector/package.json +1 -0
  46. rasa/core/channels/inspector/src/App.tsx +15 -2
  47. rasa/core/channels/inspector/src/components/RasaLogo.tsx +31 -0
  48. rasa/core/channels/inspector/src/components/RecruitmentPanel.tsx +68 -0
  49. rasa/core/channels/inspector/src/components/Welcome.tsx +19 -13
  50. rasa/core/channels/inspector/yarn.lock +5 -0
  51. rasa/core/channels/voice_ready/audiocodes.py +2 -2
  52. rasa/core/channels/voice_stream/asr/asr_event.py +5 -0
  53. rasa/core/channels/voice_stream/audiocodes.py +63 -35
  54. rasa/core/channels/voice_stream/call_state.py +3 -9
  55. rasa/core/channels/voice_stream/genesys.py +40 -55
  56. rasa/core/channels/voice_stream/voice_channel.py +61 -39
  57. rasa/core/tracker_store.py +123 -34
  58. rasa/dialogue_understanding/commands/set_slot_command.py +1 -0
  59. rasa/dialogue_understanding/commands/utils.py +1 -4
  60. rasa/dialogue_understanding/generator/command_parser.py +41 -0
  61. rasa/dialogue_understanding/generator/constants.py +7 -2
  62. rasa/dialogue_understanding/generator/llm_based_command_generator.py +33 -3
  63. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +29 -48
  64. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +23 -50
  65. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +76 -24
  66. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +32 -18
  67. rasa/dialogue_understanding/processor/command_processor.py +59 -20
  68. rasa/dialogue_understanding/stack/utils.py +11 -6
  69. rasa/engine/language.py +67 -25
  70. rasa/engine/validation.py +2 -0
  71. rasa/llm_fine_tuning/conversations.py +3 -31
  72. rasa/llm_fine_tuning/llm_data_preparation_module.py +5 -3
  73. rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +18 -13
  74. rasa/llm_fine_tuning/paraphrasing_module.py +6 -2
  75. rasa/llm_fine_tuning/train_test_split_module.py +27 -27
  76. rasa/llm_fine_tuning/utils.py +7 -0
  77. rasa/model_training.py +3 -1
  78. rasa/server.py +1 -0
  79. rasa/shared/constants.py +4 -0
  80. rasa/shared/core/domain.py +6 -0
  81. rasa/shared/importers/importer.py +9 -1
  82. rasa/shared/providers/_configs/azure_entra_id_config.py +8 -8
  83. rasa/shared/providers/llm/litellm_router_llm_client.py +1 -0
  84. rasa/shared/providers/router/_base_litellm_router_client.py +38 -7
  85. rasa/shared/utils/common.py +14 -0
  86. rasa/shared/utils/llm.py +69 -13
  87. rasa/telemetry.py +13 -3
  88. rasa/tracing/instrumentation/attribute_extractors.py +2 -5
  89. rasa/validator.py +4 -4
  90. rasa/version.py +1 -1
  91. {rasa_pro-3.12.0rc2.dist-info → rasa_pro-3.12.1.dist-info}/METADATA +2 -2
  92. {rasa_pro-3.12.0rc2.dist-info → rasa_pro-3.12.1.dist-info}/RECORD +95 -94
  93. rasa/core/channels/inspector/dist/assets/channel-e265ea59.js +0 -1
  94. rasa/core/channels/inspector/dist/assets/clone-21f8a43d.js +0 -1
  95. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-5c8ce12d.js +0 -1
  96. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_default.jinja2 +0 -68
  97. {rasa_pro-3.12.0rc2.dist-info → rasa_pro-3.12.1.dist-info}/NOTICE +0 -0
  98. {rasa_pro-3.12.0rc2.dist-info → rasa_pro-3.12.1.dist-info}/WHEEL +0 -0
  99. {rasa_pro-3.12.0rc2.dist-info → rasa_pro-3.12.1.dist-info}/entry_points.txt +0 -0
@@ -13,6 +13,7 @@ import { DialogueStack } from "./components/DialogueStack";
13
13
  import { DialougeInformation } from "./components/DialogueInformation";
14
14
  import { LoadingSpinner } from "./components/LoadingSpinner";
15
15
  import { DiagramFlow } from "./components/DiagramFlow";
16
+ import { RecruitmentPanel } from "./components/RecruitmentPanel";
16
17
  import { formatSlots } from "./helpers/formatters";
17
18
  import { Slot, Stack, Event, Flow, SelectedStack, Tracker } from "./types";
18
19
  import {
@@ -35,6 +36,9 @@ export function App() {
35
36
  const [stack, setStack] = useState<Stack[]>([]);
36
37
  const [frame, setFrame] = useState<SelectedStack | undefined>(undefined);
37
38
 
39
+ // State to control the visibility of the RecruitmentPanel
40
+ const [showRecruitmentPanel, setShowRecruitmentPanel] = useState(true);
41
+
38
42
  // we only show the transcript if we are not on the socket io channel
39
43
  // on the socketio channel, we show the chat component instead
40
44
  const shouldShowTranscript = !window.location.href.includes("socketio");
@@ -165,7 +169,9 @@ export function App() {
165
169
  height: "100%",
166
170
  overflow: "hidden",
167
171
  gridTemplateColumns: "1fr",
168
- gridTemplateRows: "max-content minmax(10rem, 17.5rem) minmax(10rem, auto)",
172
+ gridTemplateRows: showRecruitmentPanel
173
+ ? "max-content max-content minmax(10rem, auto)"
174
+ : "max-content minmax(10rem, 17.5rem) minmax(10rem, auto)",
169
175
  gridRowGap: rasaSpace[1],
170
176
  };
171
177
 
@@ -177,13 +183,20 @@ export function App() {
177
183
  });
178
184
  };
179
185
 
186
+ const handleCloseRecruitmentPanel = () => {
187
+ setShowRecruitmentPanel(false);
188
+ };
189
+
180
190
  if (!rasaChatSessionId && !window.location.href.includes("socketio")) return <LoadingSpinner />;
181
191
 
182
192
  return (
183
193
  <Grid sx={gridSx}>
184
194
  <GridItem overflow="hidden">
185
195
  <Grid sx={leftColumnSx}>
186
- <Welcome sx={boxSx} />
196
+ <Welcome sx={boxSx} isRecruitmentVisible={showRecruitmentPanel} />
197
+ {showRecruitmentPanel && (
198
+ <RecruitmentPanel onClose={handleCloseRecruitmentPanel} />
199
+ )}
187
200
  <DialogueStack
188
201
  sx={boxSx}
189
202
  stack={stack}
@@ -30,3 +30,34 @@ export const RasaLogo = (props: BoxProps) => {
30
30
  </Box>
31
31
  );
32
32
  };
33
+
34
+ export const RasaLogoDark = (props: BoxProps) => {
35
+ return (
36
+ <Box
37
+ as="svg"
38
+ {...props}
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ width="41"
41
+ height="51"
42
+ fill="none"
43
+ viewBox="0 0 41 51"
44
+ >
45
+ <path
46
+ fill="#7622D2"
47
+ fillRule="evenodd"
48
+ d="M34.041 10.59V7.508H21.385v12.847h3.037v-3.867h6.582v3.854h3.037v-9.763.013zm-3.037 2.827h-6.582V10.59h6.582v2.826zM19.36 29.74V35.52H6.956v-3.083h9.366V30.64H6.956v-7.965H19.36v3.083H9.994v1.798h9.366v2.184zM34.041 25.75v-3.084H21.385v12.847h3.037v-3.867h6.582V35.5h3.037v-9.764.013zm-3.037 2.826h-6.582v-2.827h6.582v2.827z"
49
+ clipRule="evenodd"
50
+ ></path>
51
+ <path
52
+ fill="#7622D2"
53
+ d="M36.826 4.689v33.578h-5.248v5.724l-9.487-5.318-.744-.417H4.179V4.69h32.654-.007zm3.298-3.34H.881v40.258h19.618l14.368 8.054v-8.054h5.25V1.349h.007z"
54
+ ></path>
55
+ <path
56
+ fill="#7622D2"
57
+ fillRule="evenodd"
58
+ d="M15.287 15.464l3.888-1.436.185-.074V7.515H6.956v.257l-.028 12.59h3.038V17.43l2.278-.838 3.417 3.77h3.752l-4.126-4.897zM9.97 14.15v-3.55h6.351V11.8l-6.351 2.348z"
59
+ clipRule="evenodd"
60
+ ></path>
61
+ </Box>
62
+ );
63
+ };
@@ -0,0 +1,68 @@
1
+ // RecruitmentPanel.tsx
2
+ import {
3
+ Box,
4
+ Flex,
5
+ Heading,
6
+ IconButton,
7
+ Text,
8
+ Button,
9
+ } from "@chakra-ui/react";
10
+ import { CloseIcon } from "@chakra-ui/icons";
11
+ import { useOurTheme } from "../theme";
12
+
13
+ interface RecruitmentPanelProps {
14
+ onClose: () => void;
15
+ }
16
+
17
+ export const RecruitmentPanel: React.FC<RecruitmentPanelProps> = ({ onClose }) => {
18
+ const { rasaRadii } = useOurTheme();
19
+
20
+ const textColor = "white";
21
+ const iconColor = "white";
22
+
23
+ const boxSx = {
24
+ borderRadius: rasaRadii.normal,
25
+ padding: "1rem",
26
+ bgGradient: "linear(to-b, #4E61E1, #7622D2)",
27
+ color: textColor,
28
+ };
29
+
30
+ return (
31
+ <Box sx={boxSx}>
32
+ <Flex justify="space-between" align="center">
33
+ <Flex align="center">
34
+ <Heading as="h3" size="lg" fontWeight="bold" color={textColor}>
35
+ Help us Improve Rasa Pro!
36
+ </Heading>
37
+ </Flex>
38
+ <IconButton
39
+ aria-label="Close"
40
+ icon={<CloseIcon color={iconColor} />}
41
+ size="sm"
42
+ onClick={onClose}
43
+ bg="transparent"
44
+ _hover={{ bg: "rgba(255, 255, 255, 0.2)" }}
45
+ />
46
+ </Flex>
47
+ <Flex align="center" mt="0.5rem" justify="space-between">
48
+ <Text fontSize="sm" color={textColor}>
49
+ We're looking for users to share their feedback.
50
+ </Text>
51
+ <Button
52
+ as="a"
53
+ href="https://calendly.com/alvaro-rasa/rasa-pro-installation"
54
+ target="_blank"
55
+ rel="noopener noreferrer"
56
+ color="#7622D2"
57
+ bg="white"
58
+ fontWeight="bold"
59
+ ml="1rem"
60
+ size="sm"
61
+ _hover={{ bg: "whiteAlpha.800" }}
62
+ >
63
+ Sign up
64
+ </Button>
65
+ </Flex>
66
+ </Box>
67
+ );
68
+ };
@@ -5,33 +5,35 @@ import {
5
5
  Heading,
6
6
  Link,
7
7
  Text,
8
- useColorModeValue,
9
8
  } from "@chakra-ui/react";
10
- import { RasaLogo } from "./RasaLogo";
9
+ import { RasaLogo, RasaLogoDark } from "./RasaLogo";
11
10
  import { useOurTheme } from "../theme";
12
11
 
13
- export const Welcome = ({ sx, ...props }: FlexProps) => {
12
+ interface WelcomeProps extends FlexProps {
13
+ isRecruitmentVisible?: boolean;
14
+ }
15
+
16
+ export const Welcome = ({ sx, isRecruitmentVisible, ...props }: WelcomeProps) => {
14
17
  const { rasaSpace } = useOurTheme();
15
18
 
16
19
  const containerSx = {
17
20
  ...sx,
18
- color: useColorModeValue("neutral.50", "neutral.50"),
19
- bgGradient: "linear(to-b, #4E61E1, #7622D2)",
21
+ color: isRecruitmentVisible ? "black" : "neutral.50",
22
+ bg: isRecruitmentVisible ? "white" : undefined,
23
+ bgGradient: isRecruitmentVisible ? undefined : "linear(to-b, #4E61E1, #7622D2)",
20
24
  };
21
25
 
22
26
  const linkSx = {
23
27
  flexGrow: 0,
24
- color: useColorModeValue("neutral.50", "neutral.50"),
28
+ color: isRecruitmentVisible ? "#0000EE" : "neutral.50",
29
+ textDecoration: "underline",
25
30
  _hover: {
26
- color: useColorModeValue("neutral.400", "neutral.400"),
31
+ color: isRecruitmentVisible
32
+ ? "link.visited"
33
+ : "neutral.400",
27
34
  },
28
35
  };
29
36
 
30
- const logoSx = {
31
- flexShrink:0,
32
- marginLeft: "auto"
33
- };
34
-
35
37
  return (
36
38
  <Flex sx={containerSx} {...props}>
37
39
  <Box>
@@ -48,7 +50,11 @@ export const Welcome = ({ sx, ...props }: FlexProps) => {
48
50
  ml={rasaSpace[0.25]}
49
51
  >Browse the docs</Link>
50
52
  </Box>
51
- <RasaLogo sx={logoSx}/>
53
+ {isRecruitmentVisible ? (
54
+ <RasaLogoDark sx={{ flexShrink: 0, marginLeft: "auto" }}/>
55
+ ) : (
56
+ <RasaLogo sx={{ flexShrink: 0, marginLeft: "auto" }}/>
57
+ )}
52
58
  </Flex>
53
59
  );
54
60
  };
@@ -531,6 +531,11 @@
531
531
  dependencies:
532
532
  "@chakra-ui/shared-utils" "2.0.5"
533
533
 
534
+ "@chakra-ui/icons@^2.2.4":
535
+ version "2.2.4"
536
+ resolved "https://registry.yarnpkg.com/@chakra-ui/icons/-/icons-2.2.4.tgz#fc3f59a7e377d6e4efdbe8ee0a3aec7f29a4ab32"
537
+ integrity sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g==
538
+
534
539
  "@chakra-ui/image@2.1.0":
535
540
  version "2.1.0"
536
541
  resolved "https://registry.yarnpkg.com/@chakra-ui/image/-/image-2.1.0.tgz#6c205f1ca148e3bf58345b0b5d4eb3d959eb9f87"
@@ -47,8 +47,8 @@ def map_call_params(parameters: Dict[Text, Any]) -> CallParameters:
47
47
  """Map the Audiocodes parameters to the CallParameters dataclass."""
48
48
  return CallParameters(
49
49
  call_id=parameters.get("vaigConversationId"),
50
- user_phone=parameters.get("callee"),
51
- bot_phone=parameters.get("caller"),
50
+ user_phone=parameters.get("caller"),
51
+ bot_phone=parameters.get("callee"),
52
52
  user_name=parameters.get("callerDisplayName"),
53
53
  user_host=parameters.get("callerHost"),
54
54
  bot_host=parameters.get("calleeHost"),
@@ -16,3 +16,8 @@ class NewTranscript(ASREvent):
16
16
  @dataclass
17
17
  class UserIsSpeaking(ASREvent):
18
18
  pass
19
+
20
+
21
+ @dataclass
22
+ class UserSilence(ASREvent):
23
+ pass
@@ -13,6 +13,7 @@ from sanic import ( # type: ignore[attr-defined]
13
13
  )
14
14
 
15
15
  from rasa.core.channels import UserMessage
16
+ from rasa.core.channels.voice_ready.audiocodes import map_call_params
16
17
  from rasa.core.channels.voice_ready.utils import CallParameters
17
18
  from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
18
19
  from rasa.core.channels.voice_stream.call_state import (
@@ -27,24 +28,29 @@ from rasa.core.channels.voice_stream.voice_channel import (
27
28
  VoiceInputChannel,
28
29
  VoiceOutputChannel,
29
30
  )
31
+ from rasa.shared.utils.common import mark_as_beta_feature
30
32
 
31
33
  logger = structlog.get_logger(__name__)
32
-
33
-
34
- def map_call_params(data: Dict[Text, Any]) -> CallParameters:
35
- """Map the audiocodes stream parameters to the CallParameters dataclass."""
36
- return CallParameters(
37
- call_id=data["conversationId"],
38
- user_phone=data["caller"],
39
- # Bot phone is not available in the Audiocodes API
40
- direction="inbound", # AudioCodes calls are always inbound
41
- )
34
+ PREFERRED_AUDIO_FORMAT = "raw/mulaw"
42
35
 
43
36
 
44
37
  class AudiocodesVoiceOutputChannel(VoiceOutputChannel):
45
38
  @classmethod
46
39
  def name(cls) -> str:
47
- return "ac_voice"
40
+ return "audiocodes_stream"
41
+
42
+ def _ensure_stream_id(self) -> None:
43
+ """Audiocodes requires a stream ID with playStream messages."""
44
+ if "stream_id" not in call_state.channel_data:
45
+ call_state.channel_data["stream_id"] = 0
46
+
47
+ def _increment_stream_id(self) -> None:
48
+ self._ensure_stream_id()
49
+ call_state.channel_data["stream_id"] += 1
50
+
51
+ def _get_stream_id(self) -> str:
52
+ self._ensure_stream_id()
53
+ return str(call_state.channel_data["stream_id"])
48
54
 
49
55
  def rasa_audio_bytes_to_channel_bytes(
50
56
  self, rasa_audio_bytes: RasaAudioBytes
@@ -55,7 +61,7 @@ class AudiocodesVoiceOutputChannel(VoiceOutputChannel):
55
61
  media_message = json.dumps(
56
62
  {
57
63
  "type": "playStream.chunk",
58
- "streamId": str(call_state.stream_id),
64
+ "streamId": self._get_stream_id(),
59
65
  "audioChunk": channel_bytes.decode("utf-8"),
60
66
  }
61
67
  )
@@ -63,14 +69,15 @@ class AudiocodesVoiceOutputChannel(VoiceOutputChannel):
63
69
 
64
70
  async def send_start_marker(self, recipient_id: str) -> None:
65
71
  """Send playStream.start before first audio chunk."""
66
- call_state.stream_id += 1 # type: ignore[attr-defined]
72
+ self._increment_stream_id()
67
73
  media_message = json.dumps(
68
74
  {
69
75
  "type": "playStream.start",
70
- "streamId": str(call_state.stream_id),
76
+ "streamId": self._get_stream_id(),
77
+ "mediaFormat": PREFERRED_AUDIO_FORMAT,
71
78
  }
72
79
  )
73
- logger.debug("Sending start marker", stream_id=call_state.stream_id)
80
+ logger.debug("Sending start marker", stream_id=self._get_stream_id())
74
81
  await self.voice_websocket.send(media_message)
75
82
 
76
83
  async def send_intermediate_marker(self, recipient_id: str) -> None:
@@ -82,17 +89,27 @@ class AudiocodesVoiceOutputChannel(VoiceOutputChannel):
82
89
  media_message = json.dumps(
83
90
  {
84
91
  "type": "playStream.stop",
85
- "streamId": str(call_state.stream_id),
92
+ "streamId": self._get_stream_id(),
86
93
  }
87
94
  )
88
- logger.debug("Sending end marker", stream_id=call_state.stream_id)
95
+ logger.debug("Sending end marker", stream_id=self._get_stream_id())
89
96
  await self.voice_websocket.send(media_message)
90
97
 
91
98
 
92
99
  class AudiocodesVoiceInputChannel(VoiceInputChannel):
93
100
  @classmethod
94
101
  def name(cls) -> str:
95
- return "ac_voice"
102
+ return "audiocodes_stream"
103
+
104
+ def __init__(
105
+ self,
106
+ server_url: str,
107
+ asr_config: Dict,
108
+ tts_config: Dict,
109
+ monitor_silence: bool = False,
110
+ ):
111
+ mark_as_beta_feature("Audiocodes (audiocodes_stream) Channel")
112
+ super().__init__(server_url, asr_config, tts_config, monitor_silence)
96
113
 
97
114
  def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
98
115
  return RasaAudioBytes(base64.b64decode(input_bytes))
@@ -103,13 +120,23 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
103
120
  async for message in channel_websocket:
104
121
  data = json.loads(message)
105
122
  if data["type"] == "session.initiate":
106
- # retrieve parameters set in the webhook - contains info about the
107
- # caller
108
- logger.info("received initiate message", data=data)
123
+ # contains info about mediaformats
124
+ logger.info(
125
+ "audiocodes_stream.collect_call_parameters.session.initiate",
126
+ data=data,
127
+ )
109
128
  self._send_accepted(channel_websocket, data)
110
- return map_call_params(data)
129
+ elif data["type"] == "activities":
130
+ activities = data["activities"]
131
+ for activity in activities:
132
+ logger.debug(
133
+ "audiocodes_stream.collect_call_parameters.activity",
134
+ data=activity,
135
+ )
136
+ if activity["name"] == "start":
137
+ return map_call_params(activity["parameters"])
111
138
  else:
112
- logger.warning("ac_voice.unknown_message", data=data)
139
+ logger.warning("audiocodes_stream.unknown_message", data=data)
113
140
  return None
114
141
 
115
142
  def map_input_message(
@@ -121,14 +148,15 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
121
148
  if data["type"] == "activities":
122
149
  activities = data["activities"]
123
150
  for activity in activities:
124
- logger.debug("ac_voice.activity", data=activity)
151
+ logger.debug("audiocodes_stream.activity", data=activity)
125
152
  if activity["name"] == "start":
153
+ # already handled in collect_call_parameters
126
154
  pass
127
155
  elif activity["name"] == "dtmf":
128
156
  # TODO: handle DTMF input
129
157
  pass
130
158
  elif activity["name"] == "playFinished":
131
- logger.debug("ac_voice.playFinished", data=activity)
159
+ logger.debug("audiocodes_stream.playFinished", data=activity)
132
160
  if call_state.should_hangup:
133
161
  logger.info("audiocodes.hangup")
134
162
  self._send_hangup(ws, data)
@@ -136,37 +164,37 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
136
164
  # we receive a end message from audiocodes
137
165
  pass
138
166
  else:
139
- logger.warning("ac_voice.unknown_activity", data=activity)
167
+ logger.warning("audiocodes_stream.unknown_activity", data=activity)
140
168
  elif data["type"] == "userStream.start":
141
- logger.debug("ac_voice.userStream.start", data=data)
169
+ logger.debug("audiocodes_stream.userStream.start", data=data)
142
170
  self._send_recognition_started(ws, data)
143
171
  elif data["type"] == "userStream.chunk":
144
172
  audio_bytes = self.channel_bytes_to_rasa_audio_bytes(data["audioChunk"])
145
173
  return NewAudioAction(audio_bytes)
146
174
  elif data["type"] == "userStream.stop":
147
- logger.debug("ac_voice.stop_recognition", data=data)
175
+ logger.debug("audiocodes_stream.stop_recognition", data=data)
148
176
  self._send_recognition_ended(ws, data)
149
177
  elif data["type"] == "session.resume":
150
- logger.debug("ac_voice.resume", data=data)
178
+ logger.debug("audiocodes_stream.resume", data=data)
151
179
  self._send_accepted(ws, data)
152
180
  elif data["type"] == "session.end":
153
- logger.debug("ac_voice.end", data=data)
181
+ logger.debug("audiocodes_stream.end", data=data)
154
182
  return EndConversationAction()
155
183
  elif data["type"] == "connection.validate":
156
184
  # not part of call flow; only sent when integration is created
157
185
  self._send_validated(ws, data)
158
186
  else:
159
- logger.warning("ac_voice.unknown_message", data=data)
187
+ logger.warning("audiocodes_stream.unknown_message", data=data)
160
188
 
161
189
  return ContinueConversationAction()
162
190
 
163
191
  def _send_accepted(self, ws: Websocket, data: Dict[Text, Any]) -> None:
164
192
  supported_formats = data.get("supportedMediaFormats", [])
165
- preferred_format = "raw/mulaw"
193
+ preferred_format = PREFERRED_AUDIO_FORMAT
166
194
 
167
195
  if preferred_format not in supported_formats:
168
196
  logger.warning(
169
- "ac_voice.format_not_supported",
197
+ "audiocodes_stream.format_not_supported",
170
198
  supported_formats=supported_formats,
171
199
  preferred_format=preferred_format,
172
200
  )
@@ -174,7 +202,7 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
174
202
 
175
203
  payload = {
176
204
  "type": "session.accepted",
177
- "mediaFormat": "raw/mulaw",
205
+ "mediaFormat": PREFERRED_AUDIO_FORMAT,
178
206
  }
179
207
  _schedule_async_task(ws.send(json.dumps(payload)))
180
208
 
@@ -230,7 +258,7 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
230
258
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
231
259
  ) -> Blueprint:
232
260
  """Defines a Sanic bluelogger.debug."""
233
- blueprint = Blueprint("ac_voice", __name__)
261
+ blueprint = Blueprint("audiocodes_stream", __name__)
234
262
 
235
263
  @blueprint.route("/", methods=["GET"])
236
264
  async def health(_: Request) -> HTTPResponse:
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  from contextvars import ContextVar
3
3
  from dataclasses import dataclass, field
4
- from typing import Optional
4
+ from typing import Any, Dict, Optional
5
5
 
6
6
  from werkzeug.local import LocalProxy
7
7
 
@@ -19,14 +19,8 @@ class CallState:
19
19
  should_hangup: bool = False
20
20
  connection_failed: bool = False
21
21
 
22
- # Genesys requires the server and client each maintain a
23
- # monotonically increasing message sequence number.
24
- client_sequence_number: int = 0
25
- server_sequence_number: int = 0
26
- audio_buffer: bytearray = field(default_factory=bytearray)
27
-
28
- # Audiocodes requires a stream ID at start and end of stream
29
- stream_id: int = 0
22
+ # Generic field for channel-specific state data
23
+ channel_data: Dict[str, Any] = field(default_factory=dict)
30
24
 
31
25
 
32
26
  _call_state: ContextVar[CallState] = ContextVar("call_state")
@@ -27,8 +27,23 @@ from rasa.core.channels.voice_stream.voice_channel import (
27
27
  VoiceOutputChannel,
28
28
  )
29
29
 
30
- # Not mentioned in the documentation but observed in Geneys's example
31
- # https://github.com/GenesysCloudBlueprints/audioconnector-server-reference-implementation
30
+ """
31
+ Genesys throws a rate limit error with too many audio messages.
32
+ To avoid this, we buffer the audio messages and send them in chunks.
33
+
34
+ - global.inbound.binary.average.rate.per.second: 5
35
+ The allowed average rate per second of inbound binary data
36
+
37
+ - global.inbound.binary.max: 25
38
+ The maximum number of inbound binary data messages
39
+ that can be sent instantaneously
40
+
41
+ https://developer.genesys.cloud/organization/organization/limits#audiohook
42
+
43
+ The maximum binary message size is not mentioned
44
+ in the documentation but observed in their example app
45
+ https://github.com/GenesysCloudBlueprints/audioconnector-server-reference-implementation
46
+ """
32
47
  MAXIMUM_BINARY_MESSAGE_SIZE = 64000 # 64KB
33
48
  logger = structlog.get_logger(__name__)
34
49
 
@@ -56,52 +71,7 @@ class GenesysOutputChannel(VoiceOutputChannel):
56
71
  async def send_audio_bytes(
57
72
  self, recipient_id: str, audio_bytes: RasaAudioBytes
58
73
  ) -> None:
59
- """
60
- Send audio bytes to the recipient with buffering.
61
-
62
- Genesys throws a rate limit error with too many audio messages.
63
- To avoid this, we buffer the audio messages and send them in chunks.
64
-
65
- - global.inbound.binary.average.rate.per.second: 5
66
- The allowed average rate per second of inbound binary data
67
-
68
- - global.inbound.binary.max: 25
69
- The maximum number of inbound binary data messages
70
- that can be sent instantaneously
71
-
72
- https://developer.genesys.cloud/organization/organization/limits#audiohook
73
- """
74
- call_state.audio_buffer.extend(audio_bytes)
75
-
76
- # If we receive a non-standard chunk size, assume it's the end of a sequence
77
- # or buffer is more than 32KB (this is half of genesys's max audio message size)
78
- if len(audio_bytes) != 1024 or len(call_state.audio_buffer) >= (
79
- MAXIMUM_BINARY_MESSAGE_SIZE / 2
80
- ):
81
- # TODO: we should send the buffer when we receive a synthesis complete event
82
- # from TTS. This will ensure that the last audio chunk is always sent.
83
- await self._send_audio_buffer(self.voice_websocket)
84
-
85
- async def _send_audio_buffer(self, ws: Websocket) -> None:
86
- """Send the audio buffer to the recipient if it's not empty."""
87
- if call_state.audio_buffer:
88
- buffer_bytes = bytes(call_state.audio_buffer)
89
- await self._send_bytes_to_ws(ws, buffer_bytes)
90
- call_state.audio_buffer.clear()
91
-
92
- async def _send_bytes_to_ws(self, ws: Websocket, data: bytes) -> None:
93
- """Send audio bytes to the recipient as a binary websocket message."""
94
- if len(data) <= MAXIMUM_BINARY_MESSAGE_SIZE:
95
- await self.voice_websocket.send(data)
96
- else:
97
- # split the audio into chunks
98
- current_position = 0
99
- while current_position < len(data):
100
- end_position = min(
101
- current_position + MAXIMUM_BINARY_MESSAGE_SIZE, len(data)
102
- )
103
- await self.voice_websocket.send(data[current_position:end_position])
104
- current_position = end_position
74
+ await self.voice_websocket.send(audio_bytes)
105
75
 
106
76
  async def send_marker_message(self, recipient_id: str) -> None:
107
77
  """
@@ -119,6 +89,17 @@ class GenesysInputChannel(VoiceInputChannel):
119
89
  def __init__(self, *args: Any, **kwargs: Any) -> None:
120
90
  super().__init__(*args, **kwargs)
121
91
 
92
+ def _ensure_channel_data_initialized(self) -> None:
93
+ """Initialize Genesys-specific channel data if not already present.
94
+
95
+ Genesys requires the server and client each maintain a
96
+ monotonically increasing message sequence number.
97
+ """
98
+ if "server_sequence_number" not in call_state.channel_data:
99
+ call_state.channel_data["server_sequence_number"] = 0
100
+ if "client_sequence_number" not in call_state.channel_data:
101
+ call_state.channel_data["client_sequence_number"] = 0
102
+
122
103
  def _get_next_sequence(self) -> int:
123
104
  """
124
105
  Get the next message sequence number
@@ -128,23 +109,26 @@ class GenesysInputChannel(VoiceInputChannel):
128
109
  Genesys requires the server and client each maintain a
129
110
  monotonically increasing message sequence number.
130
111
  """
131
- cs = call_state
132
- cs.server_sequence_number += 1 # type: ignore[attr-defined]
133
- return cs.server_sequence_number
112
+ self._ensure_channel_data_initialized()
113
+ call_state.channel_data["server_sequence_number"] += 1
114
+ return call_state.channel_data["server_sequence_number"]
134
115
 
135
116
  def _get_last_client_sequence(self) -> int:
136
117
  """Get the last client(Genesys) sequence number."""
137
- return call_state.client_sequence_number
118
+ self._ensure_channel_data_initialized()
119
+ return call_state.channel_data["client_sequence_number"]
138
120
 
139
121
  def _update_client_sequence(self, seq: int) -> None:
140
122
  """Update the client(Genesys) sequence number."""
141
- if seq - call_state.client_sequence_number != 1:
123
+ self._ensure_channel_data_initialized()
124
+
125
+ if seq - call_state.channel_data["client_sequence_number"] != 1:
142
126
  logger.warning(
143
127
  "genesys.update_client_sequence.sequence_gap",
144
128
  received_seq=seq,
145
- last_seq=call_state.client_sequence_number,
129
+ last_seq=call_state.channel_data["client_sequence_number"],
146
130
  )
147
- call_state.client_sequence_number = seq # type: ignore[attr-defined]
131
+ call_state.channel_data["client_sequence_number"] = seq
148
132
 
149
133
  def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
150
134
  return RasaAudioBytes(input_bytes)
@@ -211,6 +195,7 @@ class GenesysInputChannel(VoiceInputChannel):
211
195
  voice_websocket,
212
196
  tts_engine,
213
197
  self.tts_cache,
198
+ min_buffer_size=MAXIMUM_BINARY_MESSAGE_SIZE // 2,
214
199
  )
215
200
 
216
201
  async def handle_open(self, ws: Websocket, message: dict) -> CallParameters: