Update OpenAIRealtimeLLMService to work with LLMContext and LLMContextAggregatorPair (cont'd).

Make `LLMUserAggregator` push the `LLMSetToolsFrame`s, in case a speech-to-speech service that needs to handle the frame itself—like `OpenAIRealtimeLLMService`—is downstream. As far as I can tell, pushing `LLMSetToolsFrame` should otherwise have no unwanted side effects.
This commit is contained in:
Paul Kompfner
2025-10-23 16:05:41 -04:00
parent 0282033208
commit 1f96cdf970
2 changed files with 38 additions and 5 deletions

View File

@@ -5,6 +5,7 @@
# #
import asyncio
import os import os
from datetime import datetime from datetime import datetime
@@ -14,7 +15,7 @@ from loguru import logger
from pipecat.adapters.schemas.function_schema import FunctionSchema from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.audio.vad.silero import SileroVADAnalyzer from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TranscriptionMessage from pipecat.frames.frames import LLMRunFrame, LLMSetToolsFrame, TranscriptionMessage
from pipecat.observers.loggers.transcription_log_observer import TranscriptionLogObserver from pipecat.observers.loggers.transcription_log_observer import TranscriptionLogObserver
from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.runner import PipelineRunner
@@ -53,6 +54,18 @@ async def fetch_weather_from_api(params: FunctionCallParams):
) )
async def get_news(params: FunctionCallParams):
await params.result_callback(
{
"news": [
"Massive UFO currently hovering above New York City",
"Stock markets reach all-time highs",
"Living dinosaur species discovered in the Amazon rainforest",
],
}
)
async def fetch_restaurant_recommendation(params: FunctionCallParams): async def fetch_restaurant_recommendation(params: FunctionCallParams):
await params.result_callback({"name": "The Golden Dragon"}) await params.result_callback({"name": "The Golden Dragon"})
@@ -74,6 +87,13 @@ weather_function = FunctionSchema(
required=["location", "format"], required=["location", "format"],
) )
get_news_function = FunctionSchema(
name="get_news",
description="Get the current news.",
properties={},
required=[],
)
restaurant_function = FunctionSchema( restaurant_function = FunctionSchema(
name="get_restaurant_recommendation", name="get_restaurant_recommendation",
description="Get a restaurant recommendation", description="Get a restaurant recommendation",
@@ -141,10 +161,6 @@ even if you're asked about them.
You are participating in a voice conversation. Keep your responses concise, short, and to the point You are participating in a voice conversation. Keep your responses concise, short, and to the point
unless specifically asked to elaborate on a topic. unless specifically asked to elaborate on a topic.
You have access to the following tools:
- get_current_weather: Get the current weather for a given location.
- get_restaurant_recommendation: Get a restaurant recommendation for a given location.
Remember, your responses should be short. Just one or two sentences, usually. Respond in English.""", Remember, your responses should be short. Just one or two sentences, usually. Respond in English.""",
) )
@@ -158,6 +174,7 @@ Remember, your responses should be short. Just one or two sentences, usually. Re
# llm.register_function(None, fetch_weather_from_api) # llm.register_function(None, fetch_weather_from_api)
llm.register_function("get_current_weather", fetch_weather_from_api) llm.register_function("get_current_weather", fetch_weather_from_api)
llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation) llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation)
llm.register_function("get_news", get_news)
transcript = TranscriptProcessor() transcript = TranscriptProcessor()
@@ -199,6 +216,16 @@ Remember, your responses should be short. Just one or two sentences, usually. Re
# Kick off the conversation. # Kick off the conversation.
await task.queue_frames([LLMRunFrame()]) await task.queue_frames([LLMRunFrame()])
async def set_tools_after_delay():
await asyncio.sleep(15)
new_tools = ToolsSchema(
standard_tools=[weather_function, restaurant_function, get_news_function]
)
logger.info("Registering new tool with LLMSetToolsFrame")
await task.queue_frames([LLMSetToolsFrame(tools=new_tools)])
asyncio.create_task(set_tools_after_delay())
@transport.event_handler("on_client_disconnected") @transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client): async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected") logger.info(f"Client disconnected")

View File

@@ -290,6 +290,12 @@ class LLMUserAggregator(LLMContextAggregator):
await self._handle_llm_messages_update(frame) await self._handle_llm_messages_update(frame)
elif isinstance(frame, LLMSetToolsFrame): elif isinstance(frame, LLMSetToolsFrame):
self.set_tools(frame.tools) self.set_tools(frame.tools)
# Push the LLMSetToolsFrame as well, since speech-to-speech LLM
# services (like OpenAI Realtime) may need to know about tool
# changes; unlike text-based LLM services they won't just "pick up
# the change" on the next LLM run, as the LLM is continuously
# running.
await self.push_frame(frame, direction)
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, SpeechControlParamsFrame): elif isinstance(frame, SpeechControlParamsFrame):