cartesia-line 0.1.2__tar.gz → 0.1.4__tar.gz

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 cartesia-line might be problematic. Click here for more details.

Files changed (44) hide show
  1. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/PKG-INFO +1 -1
  2. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/cartesia_line.egg-info/PKG-INFO +1 -1
  3. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/cartesia_line.egg-info/SOURCES.txt +3 -0
  4. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/__init__.py +0 -6
  5. cartesia_line-0.1.4/line/utils/dtmf_lookahead_buffer.py +128 -0
  6. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/utils/gemini_utils.py +6 -0
  7. cartesia_line-0.1.4/line/utils/log_aiter.py +123 -0
  8. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/pyproject.toml +1 -1
  9. cartesia_line-0.1.4/tests/test_dtmf_lookahead_buffer.py +173 -0
  10. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/LICENSE +0 -0
  11. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/README.md +0 -0
  12. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/cartesia_line.egg-info/dependency_links.txt +0 -0
  13. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/cartesia_line.egg-info/requires.txt +0 -0
  14. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/cartesia_line.egg-info/top_level.txt +0 -0
  15. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/bridge.py +0 -0
  16. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/bus.py +0 -0
  17. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/call_request.py +0 -0
  18. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/evals/__init__.py +0 -0
  19. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/evals/conversation_runner.py +0 -0
  20. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/evals/similarity_utils.py +0 -0
  21. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/evals/turn.py +0 -0
  22. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/events.py +0 -0
  23. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/harness.py +0 -0
  24. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/harness_types.py +0 -0
  25. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/nodes/__init__.py +0 -0
  26. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/nodes/base.py +0 -0
  27. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/nodes/conversation_context.py +0 -0
  28. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/nodes/reasoning.py +0 -0
  29. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/routes.py +0 -0
  30. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/tools/__init__.py +0 -0
  31. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/tools/system_tools.py +0 -0
  32. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/tools/tool_types.py +0 -0
  33. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/user_bridge.py +0 -0
  34. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/utils/__init__.py +0 -0
  35. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/utils/aio.py +0 -0
  36. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/utils/openai_utils.py +0 -0
  37. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/utils/str.py +0 -0
  38. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/voice_agent_app.py +0 -0
  39. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/line/voice_agent_system.py +0 -0
  40. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/setup.cfg +0 -0
  41. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/tests/test_bridge.py +0 -0
  42. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/tests/test_bus.py +0 -0
  43. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/tests/test_routes.py +0 -0
  44. {cartesia_line-0.1.2 → cartesia_line-0.1.4}/tests/test_similarity_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cartesia-line
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Cartesia Voice Agents SDK
5
5
  Author-email: "Cartesia AI, Inc." <support@cartesia.ai>
6
6
  License: Apache 2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cartesia-line
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Cartesia Voice Agents SDK
5
5
  Author-email: "Cartesia AI, Inc." <support@cartesia.ai>
6
6
  License: Apache 2.0
@@ -30,10 +30,13 @@ line/tools/system_tools.py
30
30
  line/tools/tool_types.py
31
31
  line/utils/__init__.py
32
32
  line/utils/aio.py
33
+ line/utils/dtmf_lookahead_buffer.py
33
34
  line/utils/gemini_utils.py
35
+ line/utils/log_aiter.py
34
36
  line/utils/openai_utils.py
35
37
  line/utils/str.py
36
38
  tests/test_bridge.py
37
39
  tests/test_bus.py
40
+ tests/test_dtmf_lookahead_buffer.py
38
41
  tests/test_routes.py
39
42
  tests/test_similarity_utils.py
@@ -3,7 +3,6 @@
3
3
  from line.bridge import Bridge
4
4
  from line.bus import Bus, Message
5
5
  from line.call_request import CallRequest, PreCallResult
6
- from line.evals import AgentTurn, ConversationRunner, Turn, UserTurn
7
6
  from line.nodes.conversation_context import ConversationContext
8
7
 
9
8
  # Reasoning components
@@ -27,9 +26,4 @@ __all__ = [
27
26
  "VoiceAgentApp",
28
27
  "VoiceAgentSystem",
29
28
  "register_observability_event",
30
- "AgentTurn",
31
- "ConversationRunner",
32
- "Turn",
33
- "UserTurn",
34
- "SimilarityUtils",
35
29
  ]
@@ -0,0 +1,128 @@
1
+ from collections.abc import Generator
2
+ import re
3
+ from typing import Union
4
+
5
+ from line.events import AgentResponse, DTMFOutputEvent
6
+
7
+ DTMF_EXPRESSION = "dtmf="
8
+
9
+
10
+ class DTMFLookAheadStringBuffer:
11
+ """
12
+ Wrapper ontop of DTMFLookAheadCharacterBuffer, but will yield strings instead of characters
13
+ """
14
+
15
+ def __init__(self):
16
+ self.buffer = DTMFLookAheadCharacterBuffer()
17
+
18
+ def feed(self, string: str) -> Generator[Union[AgentResponse, DTMFOutputEvent], None, None]:
19
+ for char in string:
20
+ for item in self.buffer.feed(char):
21
+ if isinstance(item, DTMFOutputEvent):
22
+ for digit in item.button:
23
+ yield DTMFOutputEvent(button=digit)
24
+ else:
25
+ yield item
26
+
27
+ def flush(self) -> Generator[Union[AgentResponse, DTMFOutputEvent], None, None]:
28
+ for item in self.buffer.flush():
29
+ if isinstance(item, DTMFOutputEvent):
30
+ for digit in split_dtmf_output(item):
31
+ yield digit
32
+ else:
33
+ yield item
34
+
35
+
36
+ def split_dtmf_output(item: DTMFOutputEvent) -> Generator[DTMFOutputEvent, None, None]:
37
+ """
38
+ DTMFOutputEvent(button="12") -> [DTMFOutputEvent(button="1"), DTMFOutputEvent(button="2")]
39
+ """
40
+ for digit in item.button:
41
+ yield DTMFOutputEvent(button=digit)
42
+
43
+
44
+ class DTMFLookAheadCharacterBuffer:
45
+ """
46
+ A look ahead buffer that will replace DTMF expressions with DTMF output events
47
+
48
+ Why do we have this:
49
+ - Sometimes, gemini will yield ["hello dtmf", "=123 world"]
50
+ - We want to yield [AgentResponse("hello"), DTMFOutputEvent("123"), AgentResponse("world")]
51
+ - This is a look ahead buffer, so we need to keep track of the buffer and the chunks
52
+ """
53
+
54
+ def __init__(self):
55
+ self.non_dtmf_buffer = ""
56
+ self.dtmf_buffer = ""
57
+
58
+ self.chunks = []
59
+
60
+ self.full_expression = re.compile(r"dtmf=(\d+)")
61
+ self.dtmf_preamble = "dtmf="
62
+
63
+ def feed(self, char: str) -> Generator[Union[AgentResponse, DTMFOutputEvent], None, None]:
64
+ """
65
+ Feed a character into the buffer and see if we yield anything
66
+ """
67
+ # new character is in a word boundary: flush
68
+ is_word_char = re.match(r"[\w0-9=]", char)
69
+ # No match and will not be a match: buffer next char onto non-dtmf buffer
70
+ if not self.dtmf_buffer and not self.dtmf_preamble.startswith(char):
71
+ self.non_dtmf_buffer += char
72
+ return
73
+
74
+ # No match and might be a match: buffer next char to dtmf buffer
75
+ if not self.dtmf_buffer and self.dtmf_preamble.startswith(char):
76
+ self.dtmf_buffer += char
77
+ return
78
+
79
+ if self.dtmf_buffer and not is_word_char:
80
+ # We are in a match, flush both
81
+ if self.dtmf_buffer.startswith(self.dtmf_preamble):
82
+ if self.non_dtmf_buffer:
83
+ yield AgentResponse(content=self.non_dtmf_buffer)
84
+
85
+ captured = self.dtmf_buffer.replace(self.dtmf_preamble, "", 1)
86
+ yield DTMFOutputEvent(
87
+ button=captured,
88
+ )
89
+
90
+ else:
91
+ # Otherwise, we didn't build enough in the dtmf buffer so move the buffer and then flush
92
+ to_flush = self.non_dtmf_buffer + self.dtmf_buffer
93
+ if to_flush:
94
+ yield AgentResponse(content=to_flush)
95
+
96
+ # Next reset
97
+ self.non_dtmf_buffer = ""
98
+ self.dtmf_buffer = ""
99
+
100
+ # Finally, recursively feed the character back in
101
+ for item in self.feed(char):
102
+ yield item
103
+ return
104
+
105
+ # New character is not in a word boundary, keep accumulating and checking
106
+ if self.dtmf_buffer and is_word_char:
107
+ self.dtmf_buffer += char
108
+
109
+ return
110
+
111
+ raise RuntimeError(f"Invalid state: {char=} {self.dtmf_buffer=} {self.non_dtmf_buffer=}")
112
+
113
+ def flush(self) -> Generator[Union[AgentResponse, DTMFOutputEvent], None, None]:
114
+ if self.dtmf_buffer.startswith(self.dtmf_preamble):
115
+ captured = self.dtmf_buffer.replace(self.dtmf_preamble, "", 1)
116
+ if self.non_dtmf_buffer:
117
+ yield AgentResponse(content=self.non_dtmf_buffer)
118
+
119
+ yield DTMFOutputEvent(button=captured)
120
+ else:
121
+ content = self.non_dtmf_buffer + self.dtmf_buffer
122
+ if content:
123
+ yield AgentResponse(content=content)
124
+
125
+ # Cleanup
126
+ self.non_dtmf_buffer = ""
127
+ self.dtmf_buffer = ""
128
+ return
@@ -11,6 +11,8 @@ from loguru import logger
11
11
 
12
12
  from line.events import (
13
13
  AgentResponse,
14
+ DTMFInputEvent,
15
+ DTMFOutputEvent,
14
16
  EventInstance,
15
17
  EventType,
16
18
  ToolResult,
@@ -65,6 +67,10 @@ def convert_messages_to_gemini(
65
67
  gemini_messages.append(types.ModelContent(parts=[types.Part.from_text(text=event.content)]))
66
68
  elif isinstance(event, UserTranscriptionReceived):
67
69
  gemini_messages.append(types.UserContent(parts=[types.Part.from_text(text=event.content)]))
70
+ elif isinstance(event, DTMFInputEvent):
71
+ gemini_messages.append(types.UserContent(parts=[types.Part.from_text(text=event.button)]))
72
+ elif isinstance(event, DTMFOutputEvent):
73
+ gemini_messages.append(types.ModelContent(parts=[types.Part.from_text(text=event.button)]))
68
74
  elif isinstance(event, ToolResult):
69
75
  # Gemini 400s if a ToolResult is the first message in the context, so don't add it:
70
76
  if not gemini_messages:
@@ -0,0 +1,123 @@
1
+ from contextlib import contextmanager
2
+ import datetime
3
+
4
+ from loguru import logger
5
+
6
+
7
+ def log_afunc(log_result=False, message=None):
8
+ """Decorator to log execution time of async functions
9
+
10
+ Args:
11
+ log_result: If True, also log the function result
12
+ message: Optional message to include in log statements for additional context
13
+ """
14
+
15
+ def decorator(func):
16
+ async def wrapper(*args, **kwargs):
17
+ # Use custom message if provided, otherwise use function name
18
+ log_message = message or func.__name__
19
+
20
+ logger.info(f"[START FUNC] {log_message}")
21
+ start_time = datetime.datetime.now()
22
+ try:
23
+ result = await func(*args, **kwargs)
24
+ end_time = datetime.datetime.now()
25
+ duration = (end_time - start_time).total_seconds()
26
+ logger.info(f"[END FUNC] {log_message}. elapsed_time={duration:.2f}s")
27
+ if log_result:
28
+ logger.info(f"[RESULT FUNC] {log_message}. {result=}")
29
+ return result
30
+ except Exception as e:
31
+ raise e
32
+
33
+ return wrapper
34
+
35
+ return decorator
36
+
37
+
38
+ def log_aiter_func(message=None, show_each=False):
39
+ """Decorator to log timing information for async generator functions
40
+
41
+ Args:
42
+ message: Optional message to include in log statements for additional context
43
+ measure_each: If True, log time between each iteration
44
+
45
+ Usage:
46
+ @time_aiter_func(message="Processing data", measure_each=True)
47
+ async def my_generator():
48
+ yield item
49
+ """
50
+
51
+ def decorator(func):
52
+ async def wrapper(*args, **kwargs):
53
+ # Use custom message if provided, otherwise use function name
54
+ log_message = message or func.__name__
55
+
56
+ logger.info(f"[START ITER] {log_message}")
57
+ start_time = datetime.datetime.now()
58
+ item_count = 0
59
+ last_time = start_time
60
+ iter_timings = []
61
+
62
+ try:
63
+ async for item in func(*args, **kwargs):
64
+ current_time = datetime.datetime.now()
65
+ time_since_last = (current_time - last_time).total_seconds()
66
+ iter_timings.append(round(time_since_last, 2))
67
+
68
+ if item_count == 0:
69
+ logger.info(
70
+ f"[FIRST_ITEM ITER] {log_message}. time_to_first_item={time_since_last:.2f}s"
71
+ )
72
+
73
+ item_count += 1
74
+ last_time = current_time
75
+ yield item
76
+ except Exception as e:
77
+ logger.exception(f"[ERROR] {log_message}. {e}")
78
+ raise e
79
+
80
+ finally:
81
+ end_time = datetime.datetime.now()
82
+ total_duration = (end_time - start_time).total_seconds()
83
+
84
+ # Use iter_timings for all timing information
85
+ time_to_first_item = f"{iter_timings[0]:.2f}s" if iter_timings else "0s"
86
+
87
+ displayed_timings = iter_timings if show_each else []
88
+
89
+ logger.info(
90
+ f"[END ITER] {log_message}. items_yielded_count={item_count},"
91
+ + f" total_time={total_duration:.2f}s, time_to_first_item={time_to_first_item},"
92
+ + f" iter_timings={displayed_timings}"
93
+ )
94
+
95
+ return wrapper
96
+
97
+ return decorator
98
+
99
+
100
+ @contextmanager
101
+ def context_log(message: str):
102
+ """
103
+ Context manager that logs entry and exit messages.
104
+
105
+ Args:
106
+ message: The message to log (will be prefixed with [ENTER] and [EXIT])
107
+
108
+ Example:
109
+ with context_logger("Starting service"):
110
+ # do something
111
+ """
112
+ logger.info(f"[START] {message}")
113
+ start_time = datetime.datetime.now()
114
+
115
+ try:
116
+ yield
117
+ except Exception as e:
118
+ duration = (datetime.datetime.now() - start_time).total_seconds()
119
+ logger.exception(f"[ERROR] {message}. duration={duration:.2f}s. {e}")
120
+ raise
121
+ finally:
122
+ duration = (datetime.datetime.now() - start_time).total_seconds()
123
+ logger.info(f"[EXIT] {message}. duration={duration:.2f}s")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cartesia-line"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Cartesia Voice Agents SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,173 @@
1
+ from line.events import AgentResponse, DTMFOutputEvent
2
+ from line.utils.dtmf_lookahead_buffer import (
3
+ DTMFLookAheadCharacterBuffer,
4
+ DTMFLookAheadStringBuffer,
5
+ )
6
+
7
+
8
+ def test_dtmf_char_buffer_base_case():
9
+ actual = []
10
+ buffer = DTMFLookAheadCharacterBuffer()
11
+
12
+ actual.extend(list(buffer.feed("h")))
13
+ actual.extend(list(buffer.feed("e")))
14
+ actual.extend(list(buffer.feed("l")))
15
+ actual.extend(list(buffer.feed("l")))
16
+ actual.extend(list(buffer.feed("o")))
17
+
18
+ assert actual == []
19
+
20
+ actual = list(buffer.flush())
21
+ assert actual == [AgentResponse(content="hello")]
22
+
23
+
24
+ def test_dtmf_char_buffer_dtmf_case():
25
+ actual = []
26
+ buffer = DTMFLookAheadCharacterBuffer()
27
+
28
+ actual.extend(list(buffer.feed("d")))
29
+ assert actual == []
30
+ actual.extend(list(buffer.feed("t")))
31
+ assert actual == []
32
+ actual.extend(list(buffer.feed("m")))
33
+ assert actual == []
34
+ actual.extend(list(buffer.feed("f")))
35
+ assert actual == []
36
+ actual.extend(list(buffer.feed("=")))
37
+
38
+ assert actual == []
39
+
40
+ actual.extend(list(buffer.feed("1")))
41
+ assert actual == []
42
+
43
+ actual.extend(list(buffer.feed("2")))
44
+ assert actual == []
45
+
46
+ actual.extend(list(buffer.feed(" ")))
47
+ assert actual == [DTMFOutputEvent(button="12")]
48
+
49
+ actual = list(buffer.flush())
50
+ assert actual == [AgentResponse(content=" ")]
51
+
52
+
53
+ def test_dtmf_char_buffer_mixed_case():
54
+ """
55
+ 'hello dtmf=123 dtmf=3world'
56
+ """
57
+ actual = []
58
+ buffer = DTMFLookAheadCharacterBuffer()
59
+
60
+ actual.extend(list(buffer.feed("h")))
61
+ actual.extend(list(buffer.feed("e")))
62
+ actual.extend(list(buffer.feed("l")))
63
+ actual.extend(list(buffer.feed("l")))
64
+ actual.extend(list(buffer.feed("o")))
65
+ actual.extend(list(buffer.feed(" ")))
66
+ actual.extend(list(buffer.feed("d")))
67
+ actual.extend(list(buffer.feed("t")))
68
+ actual.extend(list(buffer.feed("m")))
69
+ actual.extend(list(buffer.feed("f")))
70
+ actual.extend(list(buffer.feed("=")))
71
+ actual.extend(list(buffer.feed("1")))
72
+ actual.extend(list(buffer.feed("2")))
73
+ actual.extend(list(buffer.feed("3")))
74
+
75
+ assert actual == []
76
+
77
+ actual.extend(list(buffer.feed(" ")))
78
+ assert actual == [AgentResponse(content="hello "), DTMFOutputEvent(button="123")]
79
+
80
+ actual.extend(list(buffer.feed("d")))
81
+ actual.extend(list(buffer.feed("t")))
82
+ actual.extend(list(buffer.feed("m")))
83
+ actual.extend(list(buffer.feed("f")))
84
+ actual.extend(list(buffer.feed("=")))
85
+ actual.extend(list(buffer.feed("3")))
86
+ assert actual == [AgentResponse(content="hello "), DTMFOutputEvent(button="123")]
87
+
88
+ actual.extend(list(buffer.feed(" ")))
89
+ assert actual == [
90
+ AgentResponse(content="hello "),
91
+ DTMFOutputEvent(button="123"),
92
+ AgentResponse(content=" "),
93
+ DTMFOutputEvent(button="3"),
94
+ ]
95
+
96
+ actual.extend(list(buffer.feed("w")))
97
+ actual.extend(list(buffer.feed("o")))
98
+ actual.extend(list(buffer.feed("r")))
99
+ actual.extend(list(buffer.feed("l")))
100
+ actual.extend(list(buffer.feed("d")))
101
+ actual.extend(list(buffer.flush()))
102
+
103
+ assert actual == [
104
+ AgentResponse(content="hello "),
105
+ DTMFOutputEvent(button="123"),
106
+ AgentResponse(content=" "),
107
+ DTMFOutputEvent(button="3"),
108
+ AgentResponse(content=" world"),
109
+ ]
110
+
111
+
112
+ def test_dtmf_string_buffer_base_case():
113
+ actual = []
114
+ buffer = DTMFLookAheadStringBuffer()
115
+
116
+ actual.extend(list(buffer.feed("hello")))
117
+ assert actual == []
118
+
119
+ actual.extend(list(buffer.flush()))
120
+ assert actual == [AgentResponse(content="hello")]
121
+
122
+
123
+ def test_dtmf_string_buffer_dtmf_case():
124
+ actual = []
125
+ buffer = DTMFLookAheadStringBuffer()
126
+
127
+ actual.extend(list(buffer.feed("dtmf=3")))
128
+ assert actual == []
129
+
130
+ actual.extend(list(buffer.flush()))
131
+ assert actual == [DTMFOutputEvent(button="3")]
132
+
133
+
134
+ def test_dtmf_string_buffer_mixed_case():
135
+ actual = []
136
+ buffer = DTMFLookAheadStringBuffer()
137
+
138
+ actual.extend(list(buffer.feed("hello ")))
139
+ actual.extend(list(buffer.feed("dtm")))
140
+ actual.extend(list(buffer.feed("f=123 dtmf=3 w")))
141
+
142
+ assert actual == [
143
+ AgentResponse(content="hello "),
144
+ DTMFOutputEvent(button="1"),
145
+ DTMFOutputEvent(button="2"),
146
+ DTMFOutputEvent(button="3"),
147
+ AgentResponse(content=" "),
148
+ DTMFOutputEvent(button="3"),
149
+ ]
150
+ actual.extend(list(buffer.feed("orld")))
151
+ actual.extend(list(buffer.flush()))
152
+ assert actual == [
153
+ AgentResponse(content="hello "),
154
+ DTMFOutputEvent(button="1"),
155
+ DTMFOutputEvent(button="2"),
156
+ DTMFOutputEvent(button="3"),
157
+ AgentResponse(content=" "),
158
+ DTMFOutputEvent(button="3"),
159
+ AgentResponse(content=" world"),
160
+ ]
161
+
162
+
163
+ def test_dtmf_string_buffer_mixed_case_2():
164
+ actual = []
165
+ buffer = DTMFLookAheadStringBuffer()
166
+
167
+ actual.extend(list(buffer.feed("Hi")))
168
+ actual.extend(list(buffer.feed(", this is Angela calling Andrew Clement?")))
169
+ actual.extend(list(buffer.flush()))
170
+ assert [
171
+ AgentResponse(content="Hi, this is Angela calling Andrew"),
172
+ AgentResponse(content=" Clement?"),
173
+ ] == actual
File without changes
File without changes
File without changes
File without changes