remove LLMResponseStartFrame and LLMResponseEndFrame

This was added in the past to properly handle interruptions for the
LLMAssistantContextAggregator. But this is not necessary anymore since we can
handle interruptions by just processing the StartInterruptionFrame, so there's
no need for these extra frames.
This commit is contained in:
Aleix Conchillo Flaqué
2024-07-17 20:47:15 -07:00
parent d1b62c5495
commit 37027f68cb
8 changed files with 30 additions and 47 deletions

View File

@@ -5,6 +5,17 @@ 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.
## [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(