LLMService: add new FunctionCallsStartedFrame

This commit is contained in:
Aleix Conchillo Flaqué
2025-05-30 09:55:40 -07:00
parent f0cbdc4e68
commit 297afdd126
4 changed files with 53 additions and 32 deletions

View File

@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added a new frame `FunctionCallsStartedFrame`. This frame is pushed both
upstream and downstream from the LLM service to indicate that one or more
function calls are going to be executed.
- Added LLM services `on_function_calls_started` event. This event will be - Added LLM services `on_function_calls_started` event. This event will be
triggered when the LLM service receives function calls from the model and is triggered when the LLM service receives function calls from the model and is
going to start executing them. going to start executing them.

View File

@@ -667,6 +667,32 @@ class MetricsFrame(SystemFrame):
data: List[MetricsData] data: List[MetricsData]
@dataclass
class FunctionCallFromLLM:
"""Represents a function call returned by the LLM to be registered for execution.
Attributes:
function_name (str): The name of the function.
tool_call_id (str): A unique identifier for the function call.
arguments (Mapping[str, Any]): The arguments for the function.
context (OpenAILLMContext): The LLM context.
"""
function_name: str
tool_call_id: str
arguments: Mapping[str, Any]
context: Any
@dataclass
class FunctionCallsStartedFrame(SystemFrame):
"""A frame signaling that one or more function call execution is going to
start."""
function_calls: Sequence[FunctionCallFromLLM]
@dataclass @dataclass
class FunctionCallInProgressFrame(SystemFrame): class FunctionCallInProgressFrame(SystemFrame):
"""A frame signaling that a function call is in progress.""" """A frame signaling that a function call is in progress."""

View File

@@ -23,6 +23,7 @@ from pipecat.frames.frames import (
FunctionCallCancelFrame, FunctionCallCancelFrame,
FunctionCallInProgressFrame, FunctionCallInProgressFrame,
FunctionCallResultFrame, FunctionCallResultFrame,
FunctionCallsStartedFrame,
InterimTranscriptionFrame, InterimTranscriptionFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
@@ -500,7 +501,7 @@ class LLMAssistantContextAggregator(LLMContextResponseAggregator):
self._params.expect_stripped_words = kwargs["expect_stripped_words"] self._params.expect_stripped_words = kwargs["expect_stripped_words"]
self._started = 0 self._started = 0
self._function_calls_in_progress: Dict[str, FunctionCallInProgressFrame] = {} self._function_calls_in_progress: Dict[str, Optional[FunctionCallInProgressFrame]] = {}
self._context_updated_tasks: Set[asyncio.Task] = set() self._context_updated_tasks: Set[asyncio.Task] = set()
async def handle_aggregation(self, aggregation: str): async def handle_aggregation(self, aggregation: str):
@@ -538,6 +539,8 @@ class LLMAssistantContextAggregator(LLMContextResponseAggregator):
self.set_tools(frame.tools) self.set_tools(frame.tools)
elif isinstance(frame, LLMSetToolChoiceFrame): elif isinstance(frame, LLMSetToolChoiceFrame):
self.set_tool_choice(frame.tool_choice) self.set_tool_choice(frame.tool_choice)
elif isinstance(frame, FunctionCallsStartedFrame):
await self._handle_function_calls_started(frame)
elif isinstance(frame, FunctionCallInProgressFrame): elif isinstance(frame, FunctionCallInProgressFrame):
await self._handle_function_call_in_progress(frame) await self._handle_function_call_in_progress(frame)
elif isinstance(frame, FunctionCallResultFrame): elif isinstance(frame, FunctionCallResultFrame):
@@ -574,6 +577,12 @@ class LLMAssistantContextAggregator(LLMContextResponseAggregator):
self._started = 0 self._started = 0
self.reset() self.reset()
async def _handle_function_calls_started(self, frame: FunctionCallsStartedFrame):
function_names = [f"{f.function_name}:{f.tool_call_id}" for f in frame.function_calls]
logger.debug(f"{self} FunctionCallsStartedFrame: {function_names}")
for function_call in frame.function_calls:
self._function_calls_in_progress[function_call.tool_call_id] = None
async def _handle_function_call_in_progress(self, frame: FunctionCallInProgressFrame): async def _handle_function_call_in_progress(self, frame: FunctionCallInProgressFrame):
logger.debug( logger.debug(
f"{self} FunctionCallInProgressFrame: [{frame.function_name}:{frame.tool_call_id}]" f"{self} FunctionCallInProgressFrame: [{frame.function_name}:{frame.tool_call_id}]"
@@ -597,9 +606,10 @@ class LLMAssistantContextAggregator(LLMContextResponseAggregator):
await self.handle_function_call_result(frame) await self.handle_function_call_result(frame)
run_llm = False
# Run inference if the function call result requires it. # Run inference if the function call result requires it.
if frame.result: if frame.result:
run_llm = False
if properties and properties.run_llm is not None: if properties and properties.run_llm is not None:
# If the tool call result has a run_llm property, use it. # If the tool call result has a run_llm property, use it.
run_llm = properties.run_llm run_llm = properties.run_llm
@@ -610,8 +620,8 @@ class LLMAssistantContextAggregator(LLMContextResponseAggregator):
# If this is the last function call in progress, run the LLM. # If this is the last function call in progress, run the LLM.
run_llm = not bool(self._function_calls_in_progress) run_llm = not bool(self._function_calls_in_progress)
if run_llm: if run_llm:
await self.push_context_frame(FrameDirection.UPSTREAM) await self.push_context_frame(FrameDirection.UPSTREAM)
# Call the `on_context_updated` callback once the function call result # Call the `on_context_updated` callback once the function call result
# is added to the context. Also, run this in a separate task to make # is added to the context. Also, run this in a separate task to make

View File

@@ -18,9 +18,11 @@ from pipecat.frames.frames import (
EndFrame, EndFrame,
Frame, Frame,
FunctionCallCancelFrame, FunctionCallCancelFrame,
FunctionCallFromLLM,
FunctionCallInProgressFrame, FunctionCallInProgressFrame,
FunctionCallResultFrame, FunctionCallResultFrame,
FunctionCallResultProperties, FunctionCallResultProperties,
FunctionCallsStartedFrame,
StartFrame, StartFrame,
StartInterruptionFrame, StartInterruptionFrame,
UserImageRequestFrame, UserImageRequestFrame,
@@ -66,24 +68,6 @@ class FunctionCallParams:
result_callback: FunctionCallResultCallback result_callback: FunctionCallResultCallback
@dataclass
class FunctionCallFromLLM:
"""Represents a function call returned by the LLM to be registered for execution.
Attributes:
function_name (str): The name of the function.
tool_call_id (str): A unique identifier for the function call.
arguments (Mapping[str, Any]): The arguments for the function.
context (OpenAILLMContext): The LLM context.
"""
function_name: str
tool_call_id: str
arguments: Mapping[str, Any]
context: OpenAILLMContext
@dataclass @dataclass
class FunctionCallRegistryItem: class FunctionCallRegistryItem:
"""Represents an entry in our function call registry. This is what the user """Represents an entry in our function call registry. This is what the user
@@ -238,8 +222,13 @@ class LLMService(AIService):
async def run_function_calls(self, function_calls: Sequence[FunctionCallFromLLM]): async def run_function_calls(self, function_calls: Sequence[FunctionCallFromLLM]):
await self._call_event_handler("on_function_calls_started", function_calls) await self._call_event_handler("on_function_calls_started", function_calls)
total_function_calls = len(function_calls) # Push frame both downstream and upstream
for index, function_call in enumerate(function_calls): started_frame_downstream = FunctionCallsStartedFrame(function_calls=function_calls)
started_frame_upstream = FunctionCallsStartedFrame(function_calls=function_calls)
await self.push_frame(started_frame_downstream, FrameDirection.DOWNSTREAM)
await self.push_frame(started_frame_upstream, FrameDirection.UPSTREAM)
for function_call in function_calls:
if function_call.function_name in self._functions.keys(): if function_call.function_name in self._functions.keys():
item = self._functions[function_call.function_name] item = self._functions[function_call.function_name]
elif None in self._functions.keys(): elif None in self._functions.keys():
@@ -250,20 +239,12 @@ class LLMService(AIService):
) )
continue continue
# If we are not running in parallel, run inference on the last
# function call. Otherwise, the last function call to finish is the
# one that will run the inference.
run_llm = None
if not self._run_in_parallel:
run_llm = index == total_function_calls - 1
runner_item = FunctionCallRunnerItem( runner_item = FunctionCallRunnerItem(
registry_item=item, registry_item=item,
function_name=function_call.function_name, function_name=function_call.function_name,
tool_call_id=function_call.tool_call_id, tool_call_id=function_call.tool_call_id,
arguments=function_call.arguments, arguments=function_call.arguments,
context=function_call.context, context=function_call.context,
run_llm=run_llm,
) )
if self._run_in_parallel: if self._run_in_parallel: