SkipTagsAggregator and PatternPairAggregator now subclass SimpleTextAggregator

This commit is contained in:
Mark Backman
2025-12-02 18:24:35 -05:00
parent ffbb6e5937
commit 7e9d67002e
5 changed files with 171 additions and 119 deletions

View File

@@ -17,8 +17,8 @@ from typing import Awaitable, Callable, List, Optional, Tuple
from loguru import logger
from pipecat.utils.string import match_endofsentence
from pipecat.utils.text.base_text_aggregator import Aggregation, AggregationType, BaseTextAggregator
from pipecat.utils.text.base_text_aggregator import Aggregation, AggregationType
from pipecat.utils.text.simple_text_aggregator import SimpleTextAggregator
class MatchAction(Enum):
@@ -72,7 +72,7 @@ class PatternMatch(Aggregation):
return f"PatternMatch(type={self.type}, text={self.text}, full_match={self.full_match})"
class PatternPairAggregator(BaseTextAggregator):
class PatternPairAggregator(SimpleTextAggregator):
"""Aggregator that identifies and processes content between pattern pairs.
This aggregator buffers text until it can identify complete pattern pairs
@@ -97,7 +97,7 @@ class PatternPairAggregator(BaseTextAggregator):
Creates an empty aggregator with no patterns or handlers registered.
Text buffering and pattern detection will begin when text is aggregated.
"""
self._text = ""
super().__init__()
self._patterns = {}
self._handlers = {}
@@ -309,9 +309,8 @@ class PatternPairAggregator(BaseTextAggregator):
"""Aggregate text and process pattern pairs.
This method adds the new text to the buffer, processes any complete pattern
pairs, and returns processed text up to sentence boundaries if possible.
If there are incomplete patterns (start without matching end), it will
continue buffering text.
pairs, and uses the parent's lookahead logic for sentence detection when
no patterns are active.
Args:
text: New text to add to the buffer.
@@ -355,38 +354,25 @@ class PatternPairAggregator(BaseTextAggregator):
content=result.strip(), type=AggregationType.SENTENCE, full_match=result
)
# Find sentence boundary if no incomplete patterns
eos_marker = match_endofsentence(self._text)
if eos_marker:
# Extract text up to the sentence boundary
result = self._text[:eos_marker]
self._text = self._text[eos_marker:]
# Use parent's lookahead logic for sentence detection
aggregation = await super()._check_sentence_with_lookahead(text)
if aggregation:
# Convert to PatternMatch for consistency with return type
return PatternMatch(
content=result.strip(), type=AggregationType.SENTENCE, full_match=result
content=aggregation.text, type=aggregation.type, full_match=aggregation.text
)
# No complete sentence found yet
return None
async def flush(self) -> Optional[Aggregation]:
"""Flush any pending aggregation.
For PatternPairAggregator, there is no lookahead buffering, so this
simply returns None. Any remaining text can be retrieved via the
.text property.
Returns:
None, as this aggregator does not buffer pending sentences.
"""
return None
async def handle_interruption(self):
"""Handle interruptions by clearing the buffer.
"""Handle interruptions by clearing the buffer and pattern state.
Called when an interruption occurs in the processing pipeline,
to reset the state and discard any partially aggregated text.
"""
self._text = ""
await super().handle_interruption()
# Pattern and handler state persists across interruptions
async def reset(self):
"""Clear the internally aggregated text.
@@ -394,4 +380,5 @@ class PatternPairAggregator(BaseTextAggregator):
Resets the aggregator to its initial state, discarding any
buffered text and clearing pattern tracking state.
"""
self._text = ""
await super().reset()
# Pattern and handler state persists across resets

View File

@@ -60,6 +60,22 @@ class SimpleTextAggregator(BaseTextAggregator):
# Add new text to buffer
self._text += text
# Delegate to sentence detection logic
return await self._check_sentence_with_lookahead(text)
async def _check_sentence_with_lookahead(self, text: str) -> Optional[Aggregation]:
"""Check for sentence boundaries using lookahead logic.
This method implements the core sentence detection logic with lookahead.
Subclasses can call this via super() to reuse the lookahead behavior
while adding their own logic (e.g., tag handling, pattern matching).
Args:
text: The most recently added text (used for lookahead check).
Returns:
Aggregation if sentence found, None otherwise.
"""
# If we need lookahead, check if we now have non-whitespace
if self._needs_lookahead:
# Check if the new character is non-whitespace

View File

@@ -13,11 +13,12 @@ as a unit regardless of internal punctuation.
from typing import Optional, Sequence
from pipecat.utils.string import StartEndTags, match_endofsentence, parse_start_end_tags
from pipecat.utils.text.base_text_aggregator import Aggregation, AggregationType, BaseTextAggregator
from pipecat.utils.string import StartEndTags, parse_start_end_tags
from pipecat.utils.text.base_text_aggregator import Aggregation, AggregationType
from pipecat.utils.text.simple_text_aggregator import SimpleTextAggregator
class SkipTagsAggregator(BaseTextAggregator):
class SkipTagsAggregator(SimpleTextAggregator):
"""Aggregator that prevents end of sentence matching between start/end tags.
This aggregator buffers text until it finds an end of sentence or a start
@@ -37,27 +38,17 @@ class SkipTagsAggregator(BaseTextAggregator):
tags: Sequence of StartEndTags objects defining the tag pairs
that should prevent sentence boundary detection.
"""
self._text = ""
super().__init__()
self._tags = tags
self._current_tag: Optional[StartEndTags] = None
self._current_tag_index: int = 0
@property
def text(self) -> Aggregation:
"""Get the currently buffered text.
Returns:
The current text buffer content that hasn't been processed yet.
"""
return Aggregation(text=self._text.strip(), type=AggregationType.SENTENCE)
async def aggregate(self, text: str) -> Optional[Aggregation]:
"""Aggregate text while respecting tag boundaries.
This method adds the new text to the buffer, processes any complete
pattern pairs, and returns processed text up to sentence boundaries if
possible. If there are incomplete patterns (start without matching
end), it will continue buffering text.
This method adds the new text to the buffer, updates tag state,
and uses the parent's lookahead logic for sentence detection when
not inside tags.
Args:
text: New text to add to the buffer.
@@ -70,46 +61,34 @@ class SkipTagsAggregator(BaseTextAggregator):
# Add new text to buffer
self._text += text
# Update tag state
(self._current_tag, self._current_tag_index) = parse_start_end_tags(
self._text, self._tags, self._current_tag, self._current_tag_index
)
# Find sentence boundary if no incomplete patterns
if not self._current_tag:
eos_marker = match_endofsentence(self._text)
if eos_marker:
# Extract text up to the sentence boundary
result = self._text[:eos_marker]
self._text = self._text[eos_marker:]
return Aggregation(text=result.strip(), type=AggregationType.SENTENCE)
# If inside tags, don't check for sentences
if self._current_tag:
return None
# No complete sentence found yet
return None
async def flush(self) -> Optional[Aggregation]:
"""Flush any pending aggregation.
For SkipTagsAggregator, there is no lookahead buffering, so this
simply returns None. Any remaining text can be retrieved via the
.text property.
Returns:
None, as this aggregator does not buffer pending sentences.
"""
return None
# Otherwise, use parent's lookahead logic for sentence detection
return await super()._check_sentence_with_lookahead(text)
async def handle_interruption(self):
"""Handle interruptions by clearing the buffer.
"""Handle interruptions by clearing the buffer and tag state.
Called when an interruption occurs in the processing pipeline,
to reset the state and discard any partially aggregated text.
"""
self._text = ""
await super().handle_interruption()
self._current_tag = None
self._current_tag_index = 0
async def reset(self):
"""Clear the internally aggregated text.
"""Clear the internally aggregated text and tag state.
Resets the aggregator to its initial state, discarding any
buffered text.
"""
self._text = ""
await super().reset()
self._current_tag = None
self._current_tag_index = 0