Merge pull request #970 from pipecat-ai/mb/user-controlled-run-llm
Add an override_run_llm option to optionally defer function call completion
This commit is contained in:
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added `FunctionCallResultProperties` dataclass to provide a structured way to
|
||||||
|
control function call behavior, including:
|
||||||
|
|
||||||
|
- `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`.
|
||||||
|
|
||||||
@@ -30,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Modified `OpenAIAssistantContextAggregator` to support controlled completions
|
||||||
|
and to emit context update callbacks via `FunctionCallResultProperties`.
|
||||||
|
|
||||||
- Added `aws_session_token` to the `PollyTTSService`.
|
- Added `aws_session_token` to the `PollyTTSService`.
|
||||||
|
|
||||||
- Changed the default model for `PlayHTHttpTTSService` to `Play3.0-mini-http`.
|
- Changed the default model for `PlayHTHttpTTSService` to `Play3.0-mini-http`.
|
||||||
|
|||||||
@@ -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,7 +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
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import copy
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Awaitable, Callable, List
|
from typing import Any, Awaitable, Callable, List, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -218,23 +218,22 @@ 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):
|
||||||
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=run_llm,
|
properties=properties,
|
||||||
)
|
)
|
||||||
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=run_llm,
|
properties=properties,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Push frame both downstream and upstream
|
|
||||||
await llm.push_frame(result_frame_downstream, FrameDirection.DOWNSTREAM)
|
await llm.push_frame(result_frame_downstream, FrameDirection.DOWNSTREAM)
|
||||||
await llm.push_frame(result_frame_upstream, FrameDirection.UPSTREAM)
|
await llm.push_frame(result_frame_upstream, FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +778,12 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
run_llm = True
|
if properties and properties.run_llm is not None:
|
||||||
|
# If the tool call result has a run_llm property, use it
|
||||||
|
run_llm = properties.run_llm
|
||||||
|
else:
|
||||||
|
# Default behavior
|
||||||
|
run_llm = True
|
||||||
elif aggregation:
|
elif aggregation:
|
||||||
self._context.add_message({"role": "assistant", "content": aggregation})
|
self._context.add_message({"role": "assistant", "content": aggregation})
|
||||||
|
|
||||||
@@ -793,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)
|
||||||
|
|||||||
@@ -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,7 +285,12 @@ class GoogleAssistantContextAggregator(OpenAIAssistantContextAggregator):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
run_llm = not bool(self._function_calls_in_progress)
|
if properties and properties.run_llm is not None:
|
||||||
|
# If the tool call result has a run_llm property, use it
|
||||||
|
run_llm = properties.run_llm
|
||||||
|
else:
|
||||||
|
# Default behavior is to run the LLM if there are no function calls in progress
|
||||||
|
run_llm = not bool(self._function_calls_in_progress)
|
||||||
else:
|
else:
|
||||||
if aggregation.strip():
|
if aggregation.strip():
|
||||||
self._context.add_message(
|
self._context.add_message(
|
||||||
@@ -303,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)
|
||||||
|
|||||||
@@ -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,8 +69,13 @@ class GrokAssistantContextAggregator(OpenAIAssistantContextAggregator):
|
|||||||
"tool_call_id": frame.tool_call_id,
|
"tool_call_id": frame.tool_call_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# Only run the LLM if there are no more function calls in progress.
|
if properties and properties.run_llm is not None:
|
||||||
run_llm = not bool(self._function_calls_in_progress)
|
# If the tool call result has a run_llm property, use it
|
||||||
|
run_llm = properties.run_llm
|
||||||
|
else:
|
||||||
|
# Default behavior is to run the LLM if there are no 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})
|
||||||
|
|
||||||
@@ -84,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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +583,13 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator):
|
|||||||
"tool_call_id": frame.tool_call_id,
|
"tool_call_id": frame.tool_call_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# Only run the LLM if there are no more function calls in progress.
|
if properties and properties.run_llm is not None:
|
||||||
run_llm = not bool(self._function_calls_in_progress)
|
# If the tool call result has a run_llm property, use it
|
||||||
|
run_llm = properties.run_llm
|
||||||
|
else:
|
||||||
|
# Default behavior is to run the LLM if there are no 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})
|
||||||
|
|
||||||
@@ -599,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)
|
||||||
|
|||||||
Reference in New Issue
Block a user