Get rid of ThoughtTranscriptProcessor, moving its logic into AssistantTranscriptProcessor instead
This commit is contained in:
@@ -144,10 +144,9 @@ async def run_bot(
|
|||||||
transcript.user(), # User transcripts
|
transcript.user(), # User transcripts
|
||||||
context_aggregator.user(), # User responses
|
context_aggregator.user(), # User responses
|
||||||
llm, # LLM
|
llm, # LLM
|
||||||
transcript.thought(), # Thought transcripts
|
|
||||||
tts, # TTS
|
tts, # TTS
|
||||||
transport.output(), # Transport bot output
|
transport.output(), # Transport bot output
|
||||||
transcript.assistant(), # Assistant transcripts
|
transcript.assistant(), # Assistant transcripts (including thoughts)
|
||||||
context_aggregator.assistant(), # Assistant spoken responses
|
context_aggregator.assistant(), # Assistant spoken responses
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -118,10 +118,9 @@ async def run_bot(
|
|||||||
transcript.user(), # User transcripts
|
transcript.user(), # User transcripts
|
||||||
context_aggregator.user(), # User responses
|
context_aggregator.user(), # User responses
|
||||||
llm, # LLM
|
llm, # LLM
|
||||||
transcript.thought(), # Thought transcripts
|
|
||||||
tts, # TTS
|
tts, # TTS
|
||||||
transport.output(), # Transport bot output
|
transport.output(), # Transport bot output
|
||||||
transcript.assistant(), # Assistant transcripts
|
transcript.assistant(), # Assistant transcripts (including thoughts)
|
||||||
context_aggregator.assistant(), # Assistant spoken responses
|
context_aggregator.assistant(), # Assistant spoken responses
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -85,14 +85,20 @@ class UserTranscriptProcessor(BaseTranscriptProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class AssistantTranscriptProcessor(BaseTranscriptProcessor):
|
class AssistantTranscriptProcessor(BaseTranscriptProcessor):
|
||||||
"""Processes assistant TTS text frames into timestamped conversation messages.
|
"""Processes assistant TTS text frames and LLM thought frames into timestamped messages.
|
||||||
|
|
||||||
This processor aggregates TTS text frames into complete utterances and emits them as
|
This processor aggregates both TTS text frames and LLM thought frames into
|
||||||
transcript messages. Utterances are completed when:
|
complete utterances and thoughts, emitting them as transcript messages.
|
||||||
|
|
||||||
|
An assistant utterance is completed when:
|
||||||
- The bot stops speaking (BotStoppedSpeakingFrame)
|
- The bot stops speaking (BotStoppedSpeakingFrame)
|
||||||
- The bot is interrupted (InterruptionFrame)
|
- The bot is interrupted (InterruptionFrame)
|
||||||
- The pipeline ends (EndFrame)
|
- The pipeline ends (EndFrame, CancelFrame)
|
||||||
|
|
||||||
|
A thought is completed when:
|
||||||
|
- The thought ends (LLMThoughtEndFrame)
|
||||||
|
- The bot is interrupted (InterruptionFrame)
|
||||||
|
- The pipeline ends (EndFrame, CancelFrame)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -102,131 +108,36 @@ class AssistantTranscriptProcessor(BaseTranscriptProcessor):
|
|||||||
**kwargs: Additional arguments passed to parent class.
|
**kwargs: Additional arguments passed to parent class.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._current_text_parts: List[TextPartForConcatenation] = []
|
|
||||||
self._aggregation_start_time: Optional[str] = None
|
|
||||||
|
|
||||||
async def _emit_aggregated_text(self):
|
self._current_assistant_text_parts: List[TextPartForConcatenation] = []
|
||||||
|
self._assistant_text_start_time: Optional[str] = None
|
||||||
|
|
||||||
|
self._current_thought_parts: List[TextPartForConcatenation] = []
|
||||||
|
self._thought_start_time: Optional[str] = None
|
||||||
|
self._thought_active = False
|
||||||
|
|
||||||
|
async def _emit_aggregated_assistant_text(self):
|
||||||
"""Aggregates and emits text fragments as a transcript message.
|
"""Aggregates and emits text fragments as a transcript message.
|
||||||
|
|
||||||
This method uses a heuristic to automatically detect whether text fragments
|
This method aggregates text fragments that may arrive in multiple
|
||||||
contain embedded spacing (spaces at the beginning or end of fragments) or not,
|
TTSTextFrame instances and emits them as a single TranscriptionMessage.
|
||||||
and applies the appropriate joining strategy. It handles fragments from different
|
|
||||||
TTS services with different formatting patterns.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
Fragments with embedded spacing (concatenated)::
|
|
||||||
|
|
||||||
TTSTextFrame: ["Hello"]
|
|
||||||
TTSTextFrame: [" there"] # Leading space
|
|
||||||
TTSTextFrame: ["!"]
|
|
||||||
TTSTextFrame: [" How"] # Leading space
|
|
||||||
TTSTextFrame: ["'s"]
|
|
||||||
TTSTextFrame: [" it"] # Leading space
|
|
||||||
|
|
||||||
Result: "Hello there! How's it"
|
|
||||||
|
|
||||||
Fragments with trailing spaces (concatenated)::
|
|
||||||
|
|
||||||
TTSTextFrame: ["Hel"]
|
|
||||||
TTSTextFrame: ["lo "] # Trailing space
|
|
||||||
TTSTextFrame: ["to "] # Trailing space
|
|
||||||
TTSTextFrame: ["you"]
|
|
||||||
|
|
||||||
Result: "Hello to you"
|
|
||||||
|
|
||||||
Word-by-word fragments without spacing (joined with spaces)::
|
|
||||||
|
|
||||||
TTSTextFrame: ["Hello"]
|
|
||||||
TTSTextFrame: ["there"]
|
|
||||||
TTSTextFrame: ["how"]
|
|
||||||
TTSTextFrame: ["are"]
|
|
||||||
TTSTextFrame: ["you"]
|
|
||||||
|
|
||||||
Result: "Hello there how are you"
|
|
||||||
"""
|
"""
|
||||||
if self._current_text_parts and self._aggregation_start_time:
|
if self._current_assistant_text_parts and self._assistant_text_start_time:
|
||||||
content = concatenate_aggregated_text(self._current_text_parts)
|
content = concatenate_aggregated_text(self._current_assistant_text_parts)
|
||||||
if content:
|
if content:
|
||||||
logger.trace(f"Emitting aggregated assistant message: {content}")
|
logger.trace(f"Emitting aggregated assistant message: {content}")
|
||||||
message = TranscriptionMessage(
|
message = TranscriptionMessage(
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content=content,
|
content=content,
|
||||||
timestamp=self._aggregation_start_time,
|
timestamp=self._assistant_text_start_time,
|
||||||
)
|
)
|
||||||
await self._emit_update([message])
|
await self._emit_update([message])
|
||||||
else:
|
else:
|
||||||
logger.trace("No content to emit after stripping whitespace")
|
logger.trace("No content to emit after stripping whitespace")
|
||||||
|
|
||||||
# Reset aggregation state
|
# Reset aggregation state
|
||||||
self._current_text_parts = []
|
self._current_assistant_text_parts = []
|
||||||
self._aggregation_start_time = None
|
self._assistant_text_start_time = None
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
||||||
"""Process frames into assistant conversation messages.
|
|
||||||
|
|
||||||
Handles different frame types:
|
|
||||||
|
|
||||||
- TTSTextFrame: Aggregates text for current utterance
|
|
||||||
- BotStoppedSpeakingFrame: Completes current utterance
|
|
||||||
- InterruptionFrame: Completes current utterance due to interruption
|
|
||||||
- EndFrame: Completes current utterance at pipeline end
|
|
||||||
- CancelFrame: Completes current utterance due to cancellation
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: Input frame to process.
|
|
||||||
direction: Frame processing direction.
|
|
||||||
"""
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
|
|
||||||
if isinstance(frame, (InterruptionFrame, CancelFrame)):
|
|
||||||
# Push frame first otherwise our emitted transcription update frame
|
|
||||||
# might get cleaned up.
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
# Emit accumulated text with interruptions
|
|
||||||
await self._emit_aggregated_text()
|
|
||||||
elif isinstance(frame, TTSTextFrame):
|
|
||||||
# Start timestamp on first text part
|
|
||||||
if not self._aggregation_start_time:
|
|
||||||
self._aggregation_start_time = time_now_iso8601()
|
|
||||||
|
|
||||||
self._current_text_parts.append(
|
|
||||||
TextPartForConcatenation(
|
|
||||||
frame.text, includes_inter_part_spaces=frame.includes_inter_frame_spaces
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Push frame.
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
elif isinstance(frame, (BotStoppedSpeakingFrame, EndFrame)):
|
|
||||||
# Emit accumulated text when bot finishes speaking or pipeline ends.
|
|
||||||
await self._emit_aggregated_text()
|
|
||||||
# Push frame.
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
else:
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
|
|
||||||
|
|
||||||
class ThoughtTranscriptProcessor(BaseTranscriptProcessor):
|
|
||||||
"""Processes LLM thought frames into timestamped thought messages.
|
|
||||||
|
|
||||||
This processor aggregates LLM thought text frames into complete thoughts
|
|
||||||
and emits them as thought transcript messages. Thoughts are completed when:
|
|
||||||
|
|
||||||
- A thought ends (LLMThoughtEndFrame)
|
|
||||||
- The bot is interrupted (InterruptionFrame)
|
|
||||||
- The pipeline ends (EndFrame)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
"""Initialize processor with thought aggregation state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
**kwargs: Additional arguments passed to parent class.
|
|
||||||
"""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._current_thought_parts: List[TextPartForConcatenation] = []
|
|
||||||
self._thought_start_time: Optional[str] = None
|
|
||||||
self._thought_active = False
|
|
||||||
|
|
||||||
async def _emit_aggregated_thought(self):
|
async def _emit_aggregated_thought(self):
|
||||||
"""Aggregates and emits thought text fragments as a thought transcript message.
|
"""Aggregates and emits thought text fragments as a thought transcript message.
|
||||||
@@ -252,16 +163,18 @@ class ThoughtTranscriptProcessor(BaseTranscriptProcessor):
|
|||||||
self._thought_active = False
|
self._thought_active = False
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
"""Process frames into thought transcript messages.
|
"""Process frames into assistant conversation messages and thought messages.
|
||||||
|
|
||||||
Handles different frame types:
|
Handles different frame types:
|
||||||
|
|
||||||
|
- TTSTextFrame: Aggregates text for current utterance
|
||||||
- LLMThoughtStartFrame: Begins aggregating a new thought
|
- LLMThoughtStartFrame: Begins aggregating a new thought
|
||||||
- LLMThoughtTextFrame: Aggregates text for current thought
|
- LLMThoughtTextFrame: Aggregates text for current thought
|
||||||
- LLMThoughtEndFrame: Completes current thought
|
- LLMThoughtEndFrame: Completes current thought
|
||||||
- InterruptionFrame: Completes current thought due to interruption
|
- BotStoppedSpeakingFrame: Completes current utterance
|
||||||
- EndFrame: Completes current thought at pipeline end
|
- InterruptionFrame: Completes current utterance and thought due to interruption
|
||||||
- CancelFrame: Completes current thought due to cancellation
|
- EndFrame: Completes current utterance and thought at pipeline end
|
||||||
|
- CancelFrame: Completes current utterance and thought due to cancellation
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
frame: Input frame to process.
|
frame: Input frame to process.
|
||||||
@@ -273,7 +186,8 @@ class ThoughtTranscriptProcessor(BaseTranscriptProcessor):
|
|||||||
# Push frame first otherwise our emitted transcription update frame
|
# Push frame first otherwise our emitted transcription update frame
|
||||||
# might get cleaned up.
|
# might get cleaned up.
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
# Emit accumulated thought with interruptions
|
# Emit accumulated text and thought with interruptions
|
||||||
|
await self._emit_aggregated_assistant_text()
|
||||||
if self._thought_active:
|
if self._thought_active:
|
||||||
await self._emit_aggregated_thought()
|
await self._emit_aggregated_thought()
|
||||||
elif isinstance(frame, LLMThoughtStartFrame):
|
elif isinstance(frame, LLMThoughtStartFrame):
|
||||||
@@ -299,9 +213,24 @@ class ThoughtTranscriptProcessor(BaseTranscriptProcessor):
|
|||||||
await self._emit_aggregated_thought()
|
await self._emit_aggregated_thought()
|
||||||
# Push frame.
|
# Push frame.
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
elif isinstance(frame, EndFrame):
|
elif isinstance(frame, TTSTextFrame):
|
||||||
|
# Start timestamp on first text part
|
||||||
|
if not self._assistant_text_start_time:
|
||||||
|
self._assistant_text_start_time = time_now_iso8601()
|
||||||
|
|
||||||
|
self._current_assistant_text_parts.append(
|
||||||
|
TextPartForConcatenation(
|
||||||
|
frame.text, includes_inter_part_spaces=frame.includes_inter_frame_spaces
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Push frame.
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
elif isinstance(frame, (BotStoppedSpeakingFrame, EndFrame)):
|
||||||
|
# Emit accumulated text when bot finishes speaking or pipeline ends.
|
||||||
|
await self._emit_aggregated_assistant_text()
|
||||||
# Emit accumulated thought at pipeline end if still active
|
# Emit accumulated thought at pipeline end if still active
|
||||||
if self._thought_active:
|
if isinstance(frame, EndFrame) and self._thought_active:
|
||||||
await self._emit_aggregated_thought()
|
await self._emit_aggregated_thought()
|
||||||
# Push frame.
|
# Push frame.
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
@@ -312,8 +241,9 @@ class ThoughtTranscriptProcessor(BaseTranscriptProcessor):
|
|||||||
class TranscriptProcessor:
|
class TranscriptProcessor:
|
||||||
"""Factory for creating and managing transcript processors.
|
"""Factory for creating and managing transcript processors.
|
||||||
|
|
||||||
Provides unified access to user, assistant, and thought transcript processors
|
Provides unified access to user and assistant transcript processors
|
||||||
with shared event handling.
|
with shared event handling. The assistant processor handles both TTS text
|
||||||
|
and LLM thought frames.
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
@@ -326,10 +256,9 @@ class TranscriptProcessor:
|
|||||||
transcript.user(), # User transcripts
|
transcript.user(), # User transcripts
|
||||||
context_aggregator.user(),
|
context_aggregator.user(),
|
||||||
llm,
|
llm,
|
||||||
transcript.thought(), # Thought transcripts
|
|
||||||
tts,
|
tts,
|
||||||
transport.output(),
|
transport.output(),
|
||||||
transcript.assistant(), # Assistant transcripts
|
transcript.assistant(), # Assistant transcripts (including thoughts)
|
||||||
context_aggregator.assistant(),
|
context_aggregator.assistant(),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -343,7 +272,6 @@ class TranscriptProcessor:
|
|||||||
"""Initialize factory."""
|
"""Initialize factory."""
|
||||||
self._user_processor = None
|
self._user_processor = None
|
||||||
self._assistant_processor = None
|
self._assistant_processor = None
|
||||||
self._thought_processor = None
|
|
||||||
self._event_handlers = {}
|
self._event_handlers = {}
|
||||||
|
|
||||||
def user(self, **kwargs) -> UserTranscriptProcessor:
|
def user(self, **kwargs) -> UserTranscriptProcessor:
|
||||||
@@ -386,26 +314,6 @@ class TranscriptProcessor:
|
|||||||
|
|
||||||
return self._assistant_processor
|
return self._assistant_processor
|
||||||
|
|
||||||
def thought(self, **kwargs) -> ThoughtTranscriptProcessor:
|
|
||||||
"""Get the thought transcript processor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
**kwargs: Arguments specific to ThoughtTranscriptProcessor.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The thought transcript processor instance.
|
|
||||||
"""
|
|
||||||
if self._thought_processor is None:
|
|
||||||
self._thought_processor = ThoughtTranscriptProcessor(**kwargs)
|
|
||||||
# Apply any registered event handlers
|
|
||||||
for event_name, handler in self._event_handlers.items():
|
|
||||||
|
|
||||||
@self._thought_processor.event_handler(event_name)
|
|
||||||
async def thought_handler(processor, frame):
|
|
||||||
return await handler(processor, frame)
|
|
||||||
|
|
||||||
return self._thought_processor
|
|
||||||
|
|
||||||
def event_handler(self, event_name: str):
|
def event_handler(self, event_name: str):
|
||||||
"""Register event handler for both processors.
|
"""Register event handler for both processors.
|
||||||
|
|
||||||
@@ -432,12 +340,6 @@ class TranscriptProcessor:
|
|||||||
async def assistant_handler(processor, frame):
|
async def assistant_handler(processor, frame):
|
||||||
return await handler(processor, frame)
|
return await handler(processor, frame)
|
||||||
|
|
||||||
if self._thought_processor:
|
|
||||||
|
|
||||||
@self._thought_processor.event_handler(event_name)
|
|
||||||
async def thought_handler(processor, frame):
|
|
||||||
return await handler(processor, frame)
|
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|||||||
Reference in New Issue
Block a user