Update PatternPairAggregator patterns to replace pattern_id with type to simplify the API

This commit is contained in:
mattie ruth backman
2025-11-04 16:22:34 -05:00
parent 82b9c4f0b6
commit e9de9daf8c
4 changed files with 46 additions and 59 deletions

View File

@@ -111,10 +111,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Add pattern for voice switching # Add pattern for voice switching
pattern_aggregator.add_pattern_pair( pattern_aggregator.add_pattern_pair(
pattern_id="voice_tag", type="voice",
start_pattern="<voice>", start_pattern="<voice>",
end_pattern="</voice>", end_pattern="</voice>",
type="voice",
action=MatchAction.REMOVE, # Remove tags from final text action=MatchAction.REMOVE, # Remove tags from final text
) )
@@ -130,7 +129,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
else: else:
logger.warning(f"Unknown voice: {voice_name}") logger.warning(f"Unknown voice: {voice_name}")
pattern_aggregator.on_pattern_match("voice_tag", on_voice_tag) pattern_aggregator.on_pattern_match("voice", on_voice_tag)
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))

View File

@@ -118,21 +118,15 @@ class IVRProcessor(FrameProcessor):
def _setup_xml_patterns(self): def _setup_xml_patterns(self):
"""Set up XML pattern detection and handlers.""" """Set up XML pattern detection and handlers."""
# Register DTMF pattern # Register DTMF pattern
self._aggregator.add_pattern_pair( self._aggregator.add_pattern_pair("dtmf", "<dtmf>", "</dtmf>", action=MatchAction.REMOVE)
"dtmf", "<dtmf>", "</dtmf>", type="dtmf", action=MatchAction.REMOVE
)
self._aggregator.on_pattern_match("dtmf", self._handle_dtmf_action) self._aggregator.on_pattern_match("dtmf", self._handle_dtmf_action)
# Register mode pattern # Register mode pattern
self._aggregator.add_pattern_pair( self._aggregator.add_pattern_pair("mode", "<mode>", "</mode>", action=MatchAction.REMOVE)
"mode", "<mode>", "</mode>", type="mode", action=MatchAction.REMOVE
)
self._aggregator.on_pattern_match("mode", self._handle_mode_action) self._aggregator.on_pattern_match("mode", self._handle_mode_action)
# Register IVR pattern # Register IVR pattern
self._aggregator.add_pattern_pair( self._aggregator.add_pattern_pair("ivr", "<ivr>", "</ivr>", action=MatchAction.REMOVE)
"ivr", "<ivr>", "</ivr>", type="ivr", action=MatchAction.REMOVE
)
self._aggregator.on_pattern_match("ivr", self._handle_ivr_action) self._aggregator.on_pattern_match("ivr", self._handle_ivr_action)
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):

View File

@@ -44,27 +44,25 @@ class PatternMatch(Aggregation):
content between the patterns. content between the patterns.
""" """
def __init__(self, pattern_id: str, full_match: str, content: str, type: str): def __init__(self, content: str, type: str, full_match: str):
"""Initialize a pattern match. """Initialize a pattern match.
Args: Args:
pattern_id: The identifier of the matched pattern pair. type: The type of the matched pattern pair. It should be representative
of the content type (e.g., 'sentence', 'code', 'speaker', 'custom').
full_match: The complete text including start and end patterns. full_match: The complete text including start and end patterns.
content: The text content between the start and end patterns. content: The text content between the start and end patterns.
type: The type of aggregation the matched content represents
(e.g., 'code', 'speaker', 'custom').
""" """
super().__init__(text=content, type=type) super().__init__(text=content, type=type)
self.pattern_id = pattern_id
self.full_match = full_match self.full_match = full_match
def __str__(self) -> str: def __str__(self) -> str:
"""Return a string representation of the pattern match. """Return a string representation of the pattern match.
Returns: Returns:
A descriptive string showing the pattern ID and content. A descriptive string showing the pattern type and content.
""" """
return f"PatternMatch(id={self.pattern_id}, content={self.text}, full_match={self.full_match}, type={self.type})" return f"PatternMatch(type={self.type}, content={self.text}, full_match={self.full_match})"
class PatternPairAggregator(BaseTextAggregator): class PatternPairAggregator(BaseTextAggregator):
@@ -110,10 +108,9 @@ class PatternPairAggregator(BaseTextAggregator):
def add_pattern_pair( def add_pattern_pair(
self, self,
pattern_id: str, type: str,
start_pattern: str, start_pattern: str,
end_pattern: str, end_pattern: str,
type: str,
action: MatchAction = MatchAction.REMOVE, action: MatchAction = MatchAction.REMOVE,
) -> "PatternPairAggregator": ) -> "PatternPairAggregator":
"""Add a pattern pair to detect in the text. """Add a pattern pair to detect in the text.
@@ -123,11 +120,11 @@ class PatternPairAggregator(BaseTextAggregator):
the end pattern, and treat the content between them as a match. the end pattern, and treat the content between them as a match.
Args: Args:
pattern_id: Unique identifier for this pattern pair. type: Identifier for this pattern pair. Should be unique and ideally descriptive.
(e.g., 'code', 'speaker', 'custom'). type can not be 'sentence' as that is
reserved for the default behavior.
start_pattern: Pattern that marks the beginning of content. start_pattern: Pattern that marks the beginning of content.
end_pattern: Pattern that marks the end of content. end_pattern: Pattern that marks the end of content.
type: The type of aggregation the matched content represents
(e.g., 'code', 'speaker', 'custom').
action: What to do when a complete pattern is matched: action: What to do when a complete pattern is matched:
- MatchAction.REMOVE: Remove the matched pattern from the text. - MatchAction.REMOVE: Remove the matched pattern from the text.
- MatchAction.KEEP: Keep the matched pattern in the text and treat it as - MatchAction.KEEP: Keep the matched pattern in the text and treat it as
@@ -139,7 +136,11 @@ class PatternPairAggregator(BaseTextAggregator):
Returns: Returns:
Self for method chaining. Self for method chaining.
""" """
self._patterns[pattern_id] = { if type == "sentence":
raise ValueError(
"The aggregation type 'sentence' is reserved for default behavior and can not be used for custom patterns."
)
self._patterns[type] = {
"start": start_pattern, "start": start_pattern,
"end": end_pattern, "end": end_pattern,
"type": type, "type": type,
@@ -148,22 +149,22 @@ class PatternPairAggregator(BaseTextAggregator):
return self return self
def on_pattern_match( def on_pattern_match(
self, pattern_id: str, handler: Callable[[PatternMatch], Awaitable[None]] self, type: str, handler: Callable[[PatternMatch], Awaitable[None]]
) -> "PatternPairAggregator": ) -> "PatternPairAggregator":
"""Register a handler for when a pattern pair is matched. """Register a handler for when a pattern pair is matched.
The handler will be called whenever a complete match for the The handler will be called whenever a complete match for the
specified pattern ID is found in the text. specified type is found in the text.
Args: Args:
pattern_id: ID of the pattern pair to match. type: The type of the pattern pair to trigger the handler.
handler: Async function to call when pattern is matched. handler: Async function to call when pattern is matched.
The function should accept a PatternMatch object. The function should accept a PatternMatch object.
Returns: Returns:
Self for method chaining. Self for method chaining.
""" """
self._handlers[pattern_id] = handler self._handlers[type] = handler
return self return self
async def _process_complete_patterns(self, text: str) -> Tuple[List[PatternMatch], str]: async def _process_complete_patterns(self, text: str) -> Tuple[List[PatternMatch], str]:
@@ -184,12 +185,11 @@ class PatternPairAggregator(BaseTextAggregator):
all_matches = [] all_matches = []
processed_text = text processed_text = text
for pattern_id, pattern_info in self._patterns.items(): for type, pattern_info in self._patterns.items():
# Escape special regex characters in the patterns # Escape special regex characters in the patterns
start = re.escape(pattern_info["start"]) start = re.escape(pattern_info["start"])
end = re.escape(pattern_info["end"]) end = re.escape(pattern_info["end"])
action = pattern_info["action"] action = pattern_info["action"]
match_type = pattern_info["type"]
# Create regex to match from start pattern to end pattern # Create regex to match from start pattern to end pattern
# The .*? is non-greedy to handle nested patterns # The .*? is non-greedy to handle nested patterns
@@ -204,16 +204,14 @@ class PatternPairAggregator(BaseTextAggregator):
full_match = match.group(0) # Full match including patterns full_match = match.group(0) # Full match including patterns
# Create pattern match object # Create pattern match object
pattern_match = PatternMatch( pattern_match = PatternMatch(content=content, type=type, full_match=full_match)
pattern_id=pattern_id, full_match=full_match, content=content, type=match_type
)
# Call the appropriate handler if registered # Call the appropriate handler if registered
if pattern_id in self._handlers: if type in self._handlers:
try: try:
await self._handlers[pattern_id](pattern_match) await self._handlers[type](pattern_match)
except Exception as e: except Exception as e:
logger.error(f"Error in pattern handler for {pattern_id}: {e}") logger.error(f"Error in pattern handler for {type}: {e}")
# Remove the pattern from the text if configured # Remove the pattern from the text if configured
if action == MatchAction.REMOVE: if action == MatchAction.REMOVE:
@@ -233,10 +231,10 @@ class PatternPairAggregator(BaseTextAggregator):
text: The text to check for incomplete patterns. text: The text to check for incomplete patterns.
Returns: Returns:
A tuple of (start_index, type) if an incomplete pattern is found, A tuple of (start_index, pattern_info) if an incomplete pattern is found,
or None if no patterns are found or all patterns are complete. or None if no patterns are found or all patterns are complete.
""" """
for pattern_id, pattern_info in self._patterns.items(): for type, pattern_info in self._patterns.items():
start = pattern_info["start"] start = pattern_info["start"]
end = pattern_info["end"] end = pattern_info["end"]
@@ -280,10 +278,10 @@ class PatternPairAggregator(BaseTextAggregator):
if len(patterns) > 0: if len(patterns) > 0:
if len(patterns) > 1: if len(patterns) > 1:
logger.warning( logger.warning(
f"Multiple patterns matched: {[p.pattern_id for p in patterns]}. Only the first pattern will be returned." f"Multiple patterns matched: {[p.type for p in patterns]}. Only the first pattern will be returned."
) )
# If the pattern found is set to be aggregated, return it # If the pattern found is set to be aggregated, return it
action = self._patterns[patterns[0].pattern_id].get("action", MatchAction.REMOVE) action = self._patterns[patterns[0].type].get("action", MatchAction.REMOVE)
if action == MatchAction.AGGREGATE: if action == MatchAction.AGGREGATE:
self._text = "" self._text = ""
return patterns[0] return patterns[0]
@@ -300,7 +298,7 @@ class PatternPairAggregator(BaseTextAggregator):
# Otherwise, strip the text up to the start pattern and return it # Otherwise, strip the text up to the start pattern and return it
result = self._text[: pattern_start[0]] result = self._text[: pattern_start[0]]
self._text = self._text[pattern_start[0] :] self._text = self._text[pattern_start[0] :]
return PatternMatch(f"_sentence", result, result, "sentence") return PatternMatch(content=result, type="sentence", full_match=result)
# Find sentence boundary if no incomplete patterns # Find sentence boundary if no incomplete patterns
eos_marker = match_endofsentence(self._text) eos_marker = match_endofsentence(self._text)
@@ -308,7 +306,7 @@ class PatternPairAggregator(BaseTextAggregator):
# Extract text up to the sentence boundary # Extract text up to the sentence boundary
result = self._text[:eos_marker] result = self._text[:eos_marker]
self._text = self._text[eos_marker:] self._text = self._text[eos_marker:]
return PatternMatch(f"_sentence", result, result, "sentence") return PatternMatch(content=result, type="sentence", full_match=result)
# No complete sentence found yet # No complete sentence found yet
return None return None

View File

@@ -22,17 +22,15 @@ class TestPatternPairAggregator(unittest.IsolatedAsyncioTestCase):
# Add a test pattern # Add a test pattern
self.aggregator.add_pattern_pair( self.aggregator.add_pattern_pair(
pattern_id="test_pattern", type="test_pattern",
start_pattern="<test>", start_pattern="<test>",
end_pattern="</test>", end_pattern="</test>",
type="test",
action=MatchAction.REMOVE, action=MatchAction.REMOVE,
) )
self.aggregator.add_pattern_pair( self.aggregator.add_pattern_pair(
pattern_id="code_pattern", type="code_pattern",
start_pattern="<code>", start_pattern="<code>",
end_pattern="</code>", end_pattern="</code>",
type="code",
action=MatchAction.AGGREGATE, action=MatchAction.AGGREGATE,
) )
@@ -45,7 +43,7 @@ class TestPatternPairAggregator(unittest.IsolatedAsyncioTestCase):
result = await self.aggregator.aggregate("Hello <test>pattern") result = await self.aggregator.aggregate("Hello <test>pattern")
self.assertIsNone(result) self.assertIsNone(result)
self.assertEqual(self.aggregator.text.text, "Hello <test>pattern") self.assertEqual(self.aggregator.text.text, "Hello <test>pattern")
self.assertEqual(self.aggregator.text.type, "test") self.assertEqual(self.aggregator.text.type, "test_pattern")
# Second part completes the pattern and includes an exclamation point # Second part completes the pattern and includes an exclamation point
result = await self.aggregator.aggregate(" content</test>!") result = await self.aggregator.aggregate(" content</test>!")
@@ -54,7 +52,7 @@ class TestPatternPairAggregator(unittest.IsolatedAsyncioTestCase):
self.test_handler.assert_called_once() self.test_handler.assert_called_once()
call_args = self.test_handler.call_args[0][0] call_args = self.test_handler.call_args[0][0]
self.assertIsInstance(call_args, PatternMatch) self.assertIsInstance(call_args, PatternMatch)
self.assertEqual(call_args.pattern_id, "test_pattern") self.assertEqual(call_args.type, "test_pattern")
self.assertEqual(call_args.full_match, "<test>pattern content</test>") self.assertEqual(call_args.full_match, "<test>pattern content</test>")
self.assertEqual(call_args.text, "pattern content") self.assertEqual(call_args.text, "pattern content")
@@ -75,7 +73,7 @@ class TestPatternPairAggregator(unittest.IsolatedAsyncioTestCase):
result = await self.aggregator.aggregate("Here is code <code>pattern") result = await self.aggregator.aggregate("Here is code <code>pattern")
self.assertEqual(result.text, "Here is code ") self.assertEqual(result.text, "Here is code ")
self.assertEqual(self.aggregator.text.text, "<code>pattern") self.assertEqual(self.aggregator.text.text, "<code>pattern")
self.assertEqual(self.aggregator.text.type, "code") self.assertEqual(self.aggregator.text.type, "code_pattern")
# Second part completes the pattern and includes an exclamation point # Second part completes the pattern and includes an exclamation point
result = await self.aggregator.aggregate(" content</code>") result = await self.aggregator.aggregate(" content</code>")
@@ -84,11 +82,11 @@ class TestPatternPairAggregator(unittest.IsolatedAsyncioTestCase):
self.code_handler.assert_called_once() self.code_handler.assert_called_once()
call_args = self.code_handler.call_args[0][0] call_args = self.code_handler.call_args[0][0]
self.assertIsInstance(call_args, PatternMatch) self.assertIsInstance(call_args, PatternMatch)
self.assertEqual(call_args.pattern_id, "code_pattern") self.assertEqual(call_args.type, "code_pattern")
self.assertEqual(call_args.full_match, "<code>pattern content</code>") self.assertEqual(call_args.full_match, "<code>pattern content</code>")
self.assertEqual(call_args.text, "pattern content") self.assertEqual(call_args.text, "pattern content")
self.assertEqual(result.text, "pattern content") self.assertEqual(result.text, "pattern content")
self.assertEqual(result.type, "code") self.assertEqual(result.type, "code_pattern")
# Next sentence should be processed separately # Next sentence should be processed separately
result = await self.aggregator.aggregate(" This is another sentence.") result = await self.aggregator.aggregate(" This is another sentence.")
@@ -110,7 +108,7 @@ class TestPatternPairAggregator(unittest.IsolatedAsyncioTestCase):
# Buffer should contain the incomplete text # Buffer should contain the incomplete text
self.assertEqual(self.aggregator.text.text, "Hello <test>pattern content") self.assertEqual(self.aggregator.text.text, "Hello <test>pattern content")
self.assertEqual(self.aggregator.text.type, "test") self.assertEqual(self.aggregator.text.type, "test_pattern")
# Reset and confirm buffer is cleared # Reset and confirm buffer is cleared
await self.aggregator.reset() await self.aggregator.reset()
@@ -122,18 +120,16 @@ class TestPatternPairAggregator(unittest.IsolatedAsyncioTestCase):
emphasis_handler = AsyncMock() emphasis_handler = AsyncMock()
self.aggregator.add_pattern_pair( self.aggregator.add_pattern_pair(
pattern_id="voice", type="voice",
start_pattern="<voice>", start_pattern="<voice>",
end_pattern="</voice>", end_pattern="</voice>",
type="voice",
action=MatchAction.REMOVE, action=MatchAction.REMOVE,
) )
self.aggregator.add_pattern_pair( self.aggregator.add_pattern_pair(
pattern_id="emphasis", type="emphasis",
start_pattern="<em>", start_pattern="<em>",
end_pattern="</em>", end_pattern="</em>",
type="emphasis",
action=MatchAction.KEEP, # Keep emphasis tags action=MatchAction.KEEP, # Keep emphasis tags
) )
@@ -147,12 +143,12 @@ class TestPatternPairAggregator(unittest.IsolatedAsyncioTestCase):
# Both handlers should be called with correct data # Both handlers should be called with correct data
voice_handler.assert_called_once() voice_handler.assert_called_once()
voice_match = voice_handler.call_args[0][0] voice_match = voice_handler.call_args[0][0]
self.assertEqual(voice_match.pattern_id, "voice") self.assertEqual(voice_match.type, "voice")
self.assertEqual(voice_match.text, "female") self.assertEqual(voice_match.text, "female")
emphasis_handler.assert_called_once() emphasis_handler.assert_called_once()
emphasis_match = emphasis_handler.call_args[0][0] emphasis_match = emphasis_handler.call_args[0][0]
self.assertEqual(emphasis_match.pattern_id, "emphasis") self.assertEqual(emphasis_match.type, "emphasis")
self.assertEqual(emphasis_match.text, "very") self.assertEqual(emphasis_match.text, "very")
# Voice pattern should be removed, emphasis pattern should remain # Voice pattern should be removed, emphasis pattern should remain