Refactor for new on_context_updated callback and new frame properties

This commit is contained in:
Mark Backman
2025-01-13 17:20:41 -05:00
parent 1ca6ecc46e
commit 8c0ecb89de
7 changed files with 67 additions and 43 deletions

View File

@@ -9,10 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support in `FunctionCallResultFrame` for controlling LLM completions - Added `FunctionCallResultProperties` dataclass to provide a structured way to
via `override_run_llm` flag. When set to `True`, the `run_llm` parameter control function call behavior, including:
determines whether a completion is triggered, allowing finer control over LLM
behavior in function calls. - `run_llm`: Controls whether to trigger LLM completion
- `on_context_updated`: Optional callback triggered after context update
- Added a new foundational example `07e-interruptible-playht-http.py` for easy - Added a new foundational example `07e-interruptible-playht-http.py` for easy
testing of `PlayHTHttpTTSService`. testing of `PlayHTHttpTTSService`.
@@ -35,12 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Added `aws_session_token` to the `PollyTTSService`. - Modified `OpenAIAssistantContextAggregator` to support controlled completions
and to emit context update callbacks via `FunctionCallResultProperties`.
- Modified `OpenAIAssistantContextAggregator` to respect `override_run_llm` - Added `aws_session_token` to the `PollyTTSService`.
flag when processing function call results. This allows external control over
whether function calls trigger LLM completions while maintaining backward
compatibility with existing code.
- Changed the default model for `PlayHTHttpTTSService` to `Play3.0-mini-http`. - Changed the default model for `PlayHTHttpTTSService` to `Play3.0-mini-http`.

View File

@@ -5,7 +5,7 @@
# #
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, List, Literal, Mapping, Optional, Tuple from typing import Any, Awaitable, Callable, List, Literal, Mapping, Optional, Tuple
from pipecat.audio.vad.vad_analyzer import VADParams from pipecat.audio.vad.vad_analyzer import VADParams
from pipecat.clocks.base_clock import BaseClock from pipecat.clocks.base_clock import BaseClock
@@ -321,6 +321,14 @@ class LLMEnablePromptCachingFrame(DataFrame):
enable: bool enable: bool
@dataclass
class FunctionCallResultProperties:
"""Properties for a function call result frame."""
run_llm: Optional[bool] = None
on_context_updated: Optional[Callable[[], Awaitable[None]]] = None
@dataclass @dataclass
class FunctionCallResultFrame(DataFrame): class FunctionCallResultFrame(DataFrame):
"""A frame containing the result of an LLM function (tool) call.""" """A frame containing the result of an LLM function (tool) call."""
@@ -329,8 +337,7 @@ class FunctionCallResultFrame(DataFrame):
tool_call_id: str tool_call_id: str
arguments: str arguments: str
result: Any result: Any
run_llm: bool = True properties: Optional[FunctionCallResultProperties] = None
override_run_llm: bool = False
@dataclass @dataclass

View File

@@ -218,30 +218,20 @@ class OpenAILLMContext:
await llm.push_frame(progress_frame_upstream, FrameDirection.UPSTREAM) await llm.push_frame(progress_frame_upstream, FrameDirection.UPSTREAM)
# Define a callback function that pushes a FunctionCallResultFrame upstream & downstream. # Define a callback function that pushes a FunctionCallResultFrame upstream & downstream.
async def function_call_result_callback(result): async def function_call_result_callback(result, *, properties=None):
# Extract result and frame parameters if provided
if isinstance(result, dict) and "result" in result:
frame_run_llm = result.get("run_llm", run_llm)
frame_override = result.get("override_run_llm", False)
else:
frame_run_llm = run_llm
frame_override = False
result_frame_downstream = FunctionCallResultFrame( result_frame_downstream = FunctionCallResultFrame(
function_name=function_name, function_name=function_name,
tool_call_id=tool_call_id, tool_call_id=tool_call_id,
arguments=arguments, arguments=arguments,
result=result, result=result,
run_llm=frame_run_llm, properties=properties,
override_run_llm=frame_override,
) )
result_frame_upstream = FunctionCallResultFrame( result_frame_upstream = FunctionCallResultFrame(
function_name=function_name, function_name=function_name,
tool_call_id=tool_call_id, tool_call_id=tool_call_id,
arguments=arguments, arguments=arguments,
result=result, result=result,
run_llm=frame_run_llm, properties=properties,
override_run_llm=frame_override,
) )
await llm.push_frame(result_frame_downstream, FrameDirection.DOWNSTREAM) await llm.push_frame(result_frame_downstream, FrameDirection.DOWNSTREAM)

View File

@@ -21,6 +21,7 @@ from pipecat.frames.frames import (
Frame, Frame,
FunctionCallInProgressFrame, FunctionCallInProgressFrame,
FunctionCallResultFrame, FunctionCallResultFrame,
FunctionCallResultProperties,
LLMEnablePromptCachingFrame, LLMEnablePromptCachingFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
@@ -742,6 +743,7 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
return return
run_llm = False run_llm = False
properties: Optional[FunctionCallResultProperties] = None
aggregation = self._aggregation aggregation = self._aggregation
self._reset() self._reset()
@@ -749,6 +751,7 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
try: try:
if self._function_call_result: if self._function_call_result:
frame = self._function_call_result frame = self._function_call_result
properties = frame.properties
self._function_call_result = None self._function_call_result = None
if frame.result: if frame.result:
assistant_message = {"role": "assistant", "content": []} assistant_message = {"role": "assistant", "content": []}
@@ -775,13 +778,11 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
], ],
} }
) )
if frame.override_run_llm: if properties and properties.run_llm is not None:
# Explicit override # If the tool call result has a run_llm property, use it
print("Explicit override") run_llm = properties.run_llm
run_llm = frame.run_llm
else: else:
# Default behavior # Default behavior
print("Default behavior")
run_llm = True run_llm = True
elif aggregation: elif aggregation:
self._context.add_message({"role": "assistant", "content": aggregation}) self._context.add_message({"role": "assistant", "content": aggregation})
@@ -800,6 +801,10 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
if run_llm: if run_llm:
await self._user_context_aggregator.push_context_frame() await self._user_context_aggregator.push_context_frame()
# Emit the on_context_updated callback once the function call result is added to the context
if properties and properties.on_context_updated is not None:
await properties.on_context_updated()
# Push context frame # Push context frame
frame = OpenAILLMContextFrame(self._context) frame = OpenAILLMContextFrame(self._context)
await self.push_frame(frame) await self.push_frame(frame)

View File

@@ -19,6 +19,7 @@ from pipecat.frames.frames import (
AudioRawFrame, AudioRawFrame,
ErrorFrame, ErrorFrame,
Frame, Frame,
FunctionCallResultProperties,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
@@ -245,6 +246,7 @@ class GoogleAssistantContextAggregator(OpenAIAssistantContextAggregator):
return return
run_llm = False run_llm = False
properties: Optional[FunctionCallResultProperties] = None
aggregation = self._aggregation aggregation = self._aggregation
self._reset() self._reset()
@@ -252,6 +254,7 @@ class GoogleAssistantContextAggregator(OpenAIAssistantContextAggregator):
try: try:
if self._function_call_result: if self._function_call_result:
frame = self._function_call_result frame = self._function_call_result
properties = frame.properties
self._function_call_result = None self._function_call_result = None
if frame.result: if frame.result:
logger.debug(f"FunctionCallResultFrame result: {frame.arguments}") logger.debug(f"FunctionCallResultFrame result: {frame.arguments}")
@@ -282,11 +285,11 @@ class GoogleAssistantContextAggregator(OpenAIAssistantContextAggregator):
], ],
) )
) )
if frame.override_run_llm: if properties and properties.run_llm is not None:
# Explicit override # If the tool call result has a run_llm property, use it
run_llm = frame.run_llm run_llm = properties.run_llm
else: else:
# Default behavior # Default behavior is to run the LLM if there are no function calls in progress
run_llm = not bool(self._function_calls_in_progress) run_llm = not bool(self._function_calls_in_progress)
else: else:
if aggregation.strip(): if aggregation.strip():
@@ -308,6 +311,10 @@ class GoogleAssistantContextAggregator(OpenAIAssistantContextAggregator):
if run_llm: if run_llm:
await self._user_context_aggregator.push_context_frame() await self._user_context_aggregator.push_context_frame()
# Emit the on_context_updated callback once the function call result is added to the context
if properties and properties.on_context_updated is not None:
await properties.on_context_updated()
# Push context frame # Push context frame
frame = OpenAILLMContextFrame(self._context) frame = OpenAILLMContextFrame(self._context)
await self.push_frame(frame) await self.push_frame(frame)

View File

@@ -7,9 +7,11 @@
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
from loguru import logger from loguru import logger
from pipecat.frames.frames import FunctionCallResultProperties
from pipecat.metrics.metrics import LLMTokenUsage from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.openai_llm_context import ( from pipecat.processors.aggregators.openai_llm_context import (
OpenAILLMContext, OpenAILLMContext,
@@ -32,6 +34,7 @@ class GrokAssistantContextAggregator(OpenAIAssistantContextAggregator):
return return
run_llm = False run_llm = False
properties: Optional[FunctionCallResultProperties] = None
aggregation = self._aggregation aggregation = self._aggregation
self._reset() self._reset()
@@ -39,6 +42,7 @@ class GrokAssistantContextAggregator(OpenAIAssistantContextAggregator):
try: try:
if self._function_call_result: if self._function_call_result:
frame = self._function_call_result frame = self._function_call_result
properties = frame.properties
self._function_call_result = None self._function_call_result = None
if frame.result: if frame.result:
# Grok requires an empty content field for function calls # Grok requires an empty content field for function calls
@@ -65,12 +69,13 @@ class GrokAssistantContextAggregator(OpenAIAssistantContextAggregator):
"tool_call_id": frame.tool_call_id, "tool_call_id": frame.tool_call_id,
} }
) )
if frame.override_run_llm: if properties and properties.run_llm is not None:
# Explicit override # If the tool call result has a run_llm property, use it
run_llm = frame.run_llm run_llm = properties.run_llm
else: else:
# Default behavior # Default behavior is to run the LLM if there are no function calls in progress
run_llm = not bool(self._function_calls_in_progress) run_llm = not bool(self._function_calls_in_progress)
else: else:
self._context.add_message({"role": "assistant", "content": aggregation}) self._context.add_message({"role": "assistant", "content": aggregation})
@@ -88,6 +93,10 @@ class GrokAssistantContextAggregator(OpenAIAssistantContextAggregator):
if run_llm: if run_llm:
await self._user_context_aggregator.push_context_frame() await self._user_context_aggregator.push_context_frame()
# Emit the on_context_updated callback once the function call result is added to the context
if properties and properties.on_context_updated is not None:
await properties.on_context_updated()
frame = OpenAILLMContextFrame(self._context) frame = OpenAILLMContextFrame(self._context)
await self.push_frame(frame) await self.push_frame(frame)

View File

@@ -21,6 +21,7 @@ from pipecat.frames.frames import (
Frame, Frame,
FunctionCallInProgressFrame, FunctionCallInProgressFrame,
FunctionCallResultFrame, FunctionCallResultFrame,
FunctionCallResultProperties,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
@@ -549,6 +550,7 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator):
return return
run_llm = False run_llm = False
properties: Optional[FunctionCallResultProperties] = None
aggregation = self._aggregation aggregation = self._aggregation
self._reset() self._reset()
@@ -556,6 +558,7 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator):
try: try:
if self._function_call_result: if self._function_call_result:
frame = self._function_call_result frame = self._function_call_result
properties = frame.properties
self._function_call_result = None self._function_call_result = None
if frame.result: if frame.result:
self._context.add_message( self._context.add_message(
@@ -580,13 +583,13 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator):
"tool_call_id": frame.tool_call_id, "tool_call_id": frame.tool_call_id,
} }
) )
if properties and properties.run_llm is not None:
if frame.override_run_llm: # If the tool call result has a run_llm property, use it
# Explicit override run_llm = properties.run_llm
run_llm = frame.run_llm
else: else:
# Default behavior # Default behavior is to run the LLM if there are no function calls in progress
run_llm = not bool(self._function_calls_in_progress) run_llm = not bool(self._function_calls_in_progress)
else: else:
self._context.add_message({"role": "assistant", "content": aggregation}) self._context.add_message({"role": "assistant", "content": aggregation})
@@ -604,6 +607,10 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator):
if run_llm: if run_llm:
await self._user_context_aggregator.push_context_frame() await self._user_context_aggregator.push_context_frame()
# Emit the on_context_updated callback once the function call result is added to the context
if properties and properties.on_context_updated is not None:
await properties.on_context_updated()
# Push context frame # Push context frame
frame = OpenAILLMContextFrame(self._context) frame = OpenAILLMContextFrame(self._context)
await self.push_frame(frame) await self.push_frame(frame)