Merge pull request #306 from pipecat-ai/aleix/remove-llm-response-start-end-frame

remove LLMResponseStartFrame and LLMResponseEndFrame
This commit is contained in:
Aleix Conchillo Flaqué
2024-07-17 21:51:02 -07:00
committed by GitHub
8 changed files with 41 additions and 47 deletions

View File

@@ -5,6 +5,28 @@ All notable changes to **pipecat** will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Removed
- We remove the `LLMResponseStartFrame` and `LLMResponseEndFrame` frames. These
were added in the past to properly handle interruptions for the
`LLMAssistantContextAggregator`. But the `LLMContextAggregator` is now based
on `LLMResponseAggregator` which handles interruptions properly by just
processing the `StartInterruptionFrame`, so there's no need for these extra
frames any more.
### Fixed
- `TTSService` end of sentence detection has been improved. It now works with
acronyms, numbers, hours and others.
### Performance
- `CartesiaTTSService` now uses websockets which improves speed. It also
leverages the new Cartesia contexts which maintains generated audio prosody
when multiple inputs are sent, therefore improving audio quality a lot.
## [0.0.36] - 2024-07-02 ## [0.0.36] - 2024-07-02
### Added ### Added

View File

@@ -282,27 +282,13 @@ class EndFrame(ControlFrame):
@dataclass @dataclass
class LLMFullResponseStartFrame(ControlFrame): class LLMFullResponseStartFrame(ControlFrame):
"""Used to indicate the beginning of a full LLM response. Following """Used to indicate the beginning of an LLM response. Following by one or
LLMResponseStartFrame, TextFrame and LLMResponseEndFrame for each sentence more TextFrame and a final LLMFullResponseEndFrame."""
until a LLMFullResponseEndFrame."""
pass pass
@dataclass @dataclass
class LLMFullResponseEndFrame(ControlFrame): class LLMFullResponseEndFrame(ControlFrame):
"""Indicates the end of a full LLM response."""
pass
@dataclass
class LLMResponseStartFrame(ControlFrame):
"""Used to indicate the beginning of an LLM response. Following TextFrames
are part of the LLM response until an LLMResponseEndFrame"""
pass
@dataclass
class LLMResponseEndFrame(ControlFrame):
"""Indicates the end of an LLM response.""" """Indicates the end of an LLM response."""
pass pass

View File

@@ -14,8 +14,6 @@ from pipecat.frames.frames import (
InterimTranscriptionFrame, InterimTranscriptionFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMResponseEndFrame,
LLMResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
StartInterruptionFrame, StartInterruptionFrame,
TranscriptionFrame, TranscriptionFrame,
@@ -173,7 +171,7 @@ class LLMUserResponseAggregator(LLMResponseAggregator):
class LLMFullResponseAggregator(FrameProcessor): class LLMFullResponseAggregator(FrameProcessor):
"""This class aggregates Text frames until it receives a """This class aggregates Text frames until it receives a
LLMResponseEndFrame, then emits the concatenated text as LLMFullResponseEndFrame, then emits the concatenated text as
a single text frame. a single text frame.
given the following frames: given the following frames:
@@ -182,12 +180,12 @@ class LLMFullResponseAggregator(FrameProcessor):
TextFrame(" world.") TextFrame(" world.")
TextFrame(" I am") TextFrame(" I am")
TextFrame(" an LLM.") TextFrame(" an LLM.")
LLMResponseEndFrame()] LLMFullResponseEndFrame()]
this processor will yield nothing for the first 4 frames, then this processor will yield nothing for the first 4 frames, then
TextFrame("Hello, world. I am an LLM.") TextFrame("Hello, world. I am an LLM.")
LLMResponseEndFrame() LLMFullResponseEndFrame()
when passed the last frame. when passed the last frame.
@@ -203,9 +201,9 @@ class LLMFullResponseAggregator(FrameProcessor):
>>> asyncio.run(print_frames(aggregator, TextFrame(" world."))) >>> asyncio.run(print_frames(aggregator, TextFrame(" world.")))
>>> asyncio.run(print_frames(aggregator, TextFrame(" I am"))) >>> asyncio.run(print_frames(aggregator, TextFrame(" I am")))
>>> asyncio.run(print_frames(aggregator, TextFrame(" an LLM."))) >>> asyncio.run(print_frames(aggregator, TextFrame(" an LLM.")))
>>> asyncio.run(print_frames(aggregator, LLMResponseEndFrame())) >>> asyncio.run(print_frames(aggregator, LLMFullResponseEndFrame()))
Hello, world. I am an LLM. Hello, world. I am an LLM.
LLMResponseEndFrame LLMFullResponseEndFrame
""" """
def __init__(self): def __init__(self):
@@ -234,6 +232,11 @@ class LLMContextAggregator(LLMResponseAggregator):
async def _push_aggregation(self): async def _push_aggregation(self):
if len(self._aggregation) > 0: if len(self._aggregation) > 0:
self._context.add_message({"role": self._role, "content": self._aggregation}) self._context.add_message({"role": self._role, "content": self._aggregation})
# Reset the aggregation. Reset it before pushing it down, otherwise
# if the tasks gets cancelled we won't be able to clear things up.
self._aggregation = ""
frame = OpenAILLMContextFrame(self._context) frame = OpenAILLMContextFrame(self._context)
await self.push_frame(frame) await self.push_frame(frame)
@@ -247,9 +250,10 @@ class LLMAssistantContextAggregator(LLMContextAggregator):
messages=[], messages=[],
context=context, context=context,
role="assistant", role="assistant",
start_frame=LLMResponseStartFrame, start_frame=LLMFullResponseStartFrame,
end_frame=LLMResponseEndFrame, end_frame=LLMFullResponseEndFrame,
accumulator_frame=TextFrame accumulator_frame=TextFrame,
handle_interruptions=True
) )

View File

@@ -11,8 +11,6 @@ from pipecat.frames.frames import (
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMResponseEndFrame,
LLMResponseStartFrame,
TextFrame) TextFrame)
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
@@ -69,9 +67,7 @@ class LangchainProcessor(FrameProcessor):
{self._transcript_key: text}, {self._transcript_key: text},
config={"configurable": {"session_id": self._participant_id}}, config={"configurable": {"session_id": self._participant_id}},
): ):
await self.push_frame(LLMResponseStartFrame())
await self.push_frame(TextFrame(self.__get_token_value(token))) await self.push_frame(TextFrame(self.__get_token_value(token)))
await self.push_frame(LLMResponseEndFrame())
except GeneratorExit: except GeneratorExit:
logger.warning(f"{self} generator was closed prematurely") logger.warning(f"{self} generator was closed prematurely")
except Exception as e: except Exception as e:

View File

@@ -12,8 +12,6 @@ from pipecat.frames.frames import (
VisionImageRawFrame, VisionImageRawFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMResponseStartFrame,
LLMResponseEndFrame,
LLMFullResponseEndFrame LLMFullResponseEndFrame
) )
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
@@ -118,9 +116,7 @@ class AnthropicLLMService(LLMService):
async for event in response: async for event in response:
# logger.debug(f"Anthropic LLM event: {event}") # logger.debug(f"Anthropic LLM event: {event}")
if (event.type == "content_block_delta"): if (event.type == "content_block_delta"):
await self.push_frame(LLMResponseStartFrame())
await self.push_frame(TextFrame(event.delta.text)) await self.push_frame(TextFrame(event.delta.text))
await self.push_frame(LLMResponseEndFrame())
except Exception as e: except Exception as e:
logger.exception(f"{self} exception: {e}") logger.exception(f"{self} exception: {e}")

View File

@@ -14,8 +14,6 @@ from pipecat.frames.frames import (
VisionImageRawFrame, VisionImageRawFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMResponseStartFrame,
LLMResponseEndFrame,
LLMFullResponseEndFrame LLMFullResponseEndFrame
) )
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
@@ -95,9 +93,7 @@ class GoogleLLMService(LLMService):
async for chunk in self._async_generator_wrapper(response): async for chunk in self._async_generator_wrapper(response):
try: try:
text = chunk.text text = chunk.text
await self.push_frame(LLMResponseStartFrame())
await self.push_frame(TextFrame(text)) await self.push_frame(TextFrame(text))
await self.push_frame(LLMResponseEndFrame())
except Exception as e: except Exception as e:
# Google LLMs seem to flag safety issues a lot! # Google LLMs seem to flag safety issues a lot!
if chunk.candidates[0].finish_reason == 3: if chunk.candidates[0].finish_reason == 3:

View File

@@ -21,8 +21,6 @@ from pipecat.frames.frames import (
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMResponseEndFrame,
LLMResponseStartFrame,
TextFrame, TextFrame,
URLImageRawFrame, URLImageRawFrame,
VisionImageRawFrame VisionImageRawFrame
@@ -151,9 +149,7 @@ class BaseOpenAILLMService(LLMService):
# Keep iterating through the response to collect all the argument fragments # Keep iterating through the response to collect all the argument fragments
arguments += tool_call.function.arguments arguments += tool_call.function.arguments
elif chunk.choices[0].delta.content: elif chunk.choices[0].delta.content:
await self.push_frame(LLMResponseStartFrame())
await self.push_frame(TextFrame(chunk.choices[0].delta.content)) await self.push_frame(TextFrame(chunk.choices[0].delta.content))
await self.push_frame(LLMResponseEndFrame())
# if we got a function name and arguments, check to see if it's a function with # if we got a function name and arguments, check to see if it's a function with
# a registered handler. If so, run the registered callback, save the result to # a registered handler. If so, run the registered callback, save the result to

View File

@@ -8,8 +8,6 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.frames.frames import ( from pipecat.frames.frames import (
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMResponseEndFrame,
LLMResponseStartFrame,
TextFrame TextFrame
) )
from pipecat.utils.test_frame_processor import TestFrameProcessor from pipecat.utils.test_frame_processor import TestFrameProcessor
@@ -64,7 +62,7 @@ if __name__ == "__main__":
llm.register_function("get_current_weather", get_weather_from_api) llm.register_function("get_current_weather", get_weather_from_api)
t = TestFrameProcessor([ t = TestFrameProcessor([
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
[LLMResponseStartFrame, TextFrame, LLMResponseEndFrame], TextFrame,
LLMFullResponseEndFrame LLMFullResponseEndFrame
]) ])
llm.link(t) llm.link(t)
@@ -98,7 +96,7 @@ if __name__ == "__main__":
llm.register_function("get_current_weather", get_weather_from_api) llm.register_function("get_current_weather", get_weather_from_api)
t = TestFrameProcessor([ t = TestFrameProcessor([
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
[LLMResponseStartFrame, TextFrame, LLMResponseEndFrame], TextFrame,
LLMFullResponseEndFrame LLMFullResponseEndFrame
]) ])
llm.link(t) llm.link(t)
@@ -121,7 +119,7 @@ if __name__ == "__main__":
api_key = os.getenv("OPENAI_API_KEY") api_key = os.getenv("OPENAI_API_KEY")
t = TestFrameProcessor([ t = TestFrameProcessor([
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
[LLMResponseStartFrame, TextFrame, LLMResponseEndFrame], TextFrame,
LLMFullResponseEndFrame LLMFullResponseEndFrame
]) ])
llm = OpenAILLMService( llm = OpenAILLMService(