Update foundation examples 22b, 22c, and 22d to be ready for function calling
This commit is contained in:
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved foundational examples 22b, 22c, and 22d to support function calling.
|
||||||
|
With these base examples, `FunctionCallInProgressFrame` and
|
||||||
|
`FunctionCallResultFrame` will no longer be blocked by the gates.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed a `SentryMetrics` issue that was preventing any metrics to be sent to
|
- Fixed a `SentryMetrics` issue that was preventing any metrics to be sent to
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import time
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from openai.types.chat import ChatCompletionToolParam
|
||||||
from runner import configure
|
from runner import configure
|
||||||
|
|
||||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
@@ -19,6 +20,8 @@ from pipecat.frames.frames import (
|
|||||||
CancelFrame,
|
CancelFrame,
|
||||||
EndFrame,
|
EndFrame,
|
||||||
Frame,
|
Frame,
|
||||||
|
FunctionCallInProgressFrame,
|
||||||
|
FunctionCallResultFrame,
|
||||||
LLMMessagesFrame,
|
LLMMessagesFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
@@ -26,6 +29,7 @@ from pipecat.frames.frames import (
|
|||||||
SystemFrame,
|
SystemFrame,
|
||||||
TextFrame,
|
TextFrame,
|
||||||
TranscriptionFrame,
|
TranscriptionFrame,
|
||||||
|
TTSSpeakFrame,
|
||||||
UserStartedSpeakingFrame,
|
UserStartedSpeakingFrame,
|
||||||
UserStoppedSpeakingFrame,
|
UserStoppedSpeakingFrame,
|
||||||
)
|
)
|
||||||
@@ -156,6 +160,11 @@ class OutputGate(FrameProcessor):
|
|||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Don't block function call frames
|
||||||
|
if isinstance(frame, (FunctionCallInProgressFrame, FunctionCallResultFrame)):
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
return
|
||||||
|
|
||||||
# Ignore frames that are not following the direction of this gate.
|
# Ignore frames that are not following the direction of this gate.
|
||||||
if direction != FrameDirection.DOWNSTREAM:
|
if direction != FrameDirection.DOWNSTREAM:
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
@@ -186,6 +195,16 @@ class OutputGate(FrameProcessor):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
async def start_fetch_weather(function_name, llm, context):
|
||||||
|
"""Push a frame to the LLM; this is handy when the LLM response might take a while."""
|
||||||
|
await llm.push_frame(TTSSpeakFrame("Let me check on that."))
|
||||||
|
logger.debug(f"Starting fetch_weather_from_api with function_name: {function_name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_weather_from_api(function_name, tool_call_id, args, llm, context, result_callback):
|
||||||
|
await result_callback({"conditions": "nice", "temperature": "75"})
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
(room_url, _) = await configure(session)
|
(room_url, _) = await configure(session)
|
||||||
@@ -216,6 +235,34 @@ async def main():
|
|||||||
|
|
||||||
# This is the regular LLM.
|
# This is the regular LLM.
|
||||||
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o")
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o")
|
||||||
|
# Register a function_name of None to get all functions
|
||||||
|
# sent to the same callback with an additional function_name parameter.
|
||||||
|
llm.register_function(None, fetch_weather_from_api, start_callback=start_fetch_weather)
|
||||||
|
|
||||||
|
tools = [
|
||||||
|
ChatCompletionToolParam(
|
||||||
|
type="function",
|
||||||
|
function={
|
||||||
|
"name": "get_current_weather",
|
||||||
|
"description": "Get the current weather",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The city and state, e.g. San Francisco, CA",
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["celsius", "fahrenheit"],
|
||||||
|
"description": "The temperature unit to use. Infer this from the users location.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["location", "format"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
{
|
{
|
||||||
@@ -224,7 +271,7 @@ async def main():
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
context = OpenAILLMContext(messages)
|
context = OpenAILLMContext(messages, tools)
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
# We have instructed the LLM to return 'YES' if it thinks the user
|
# We have instructed the LLM to return 'YES' if it thinks the user
|
||||||
@@ -265,6 +312,8 @@ async def main():
|
|||||||
or isinstance(frame, LLMMessagesFrame)
|
or isinstance(frame, LLMMessagesFrame)
|
||||||
or isinstance(frame, StartInterruptionFrame)
|
or isinstance(frame, StartInterruptionFrame)
|
||||||
or isinstance(frame, StopInterruptionFrame)
|
or isinstance(frame, StopInterruptionFrame)
|
||||||
|
or isinstance(frame, FunctionCallInProgressFrame)
|
||||||
|
or isinstance(frame, FunctionCallResultFrame)
|
||||||
)
|
)
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import time
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from openai.types.chat import ChatCompletionToolParam
|
||||||
from runner import configure
|
from runner import configure
|
||||||
|
|
||||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
@@ -19,6 +20,8 @@ from pipecat.frames.frames import (
|
|||||||
CancelFrame,
|
CancelFrame,
|
||||||
EndFrame,
|
EndFrame,
|
||||||
Frame,
|
Frame,
|
||||||
|
FunctionCallInProgressFrame,
|
||||||
|
FunctionCallResultFrame,
|
||||||
LLMMessagesFrame,
|
LLMMessagesFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
@@ -26,6 +29,7 @@ from pipecat.frames.frames import (
|
|||||||
SystemFrame,
|
SystemFrame,
|
||||||
TextFrame,
|
TextFrame,
|
||||||
TranscriptionFrame,
|
TranscriptionFrame,
|
||||||
|
TTSSpeakFrame,
|
||||||
UserStartedSpeakingFrame,
|
UserStartedSpeakingFrame,
|
||||||
UserStoppedSpeakingFrame,
|
UserStoppedSpeakingFrame,
|
||||||
)
|
)
|
||||||
@@ -360,6 +364,11 @@ class OutputGate(FrameProcessor):
|
|||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Don't block function call frames
|
||||||
|
if isinstance(frame, (FunctionCallInProgressFrame, FunctionCallResultFrame)):
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
return
|
||||||
|
|
||||||
# Ignore frames that are not following the direction of this gate.
|
# Ignore frames that are not following the direction of this gate.
|
||||||
if direction != FrameDirection.DOWNSTREAM:
|
if direction != FrameDirection.DOWNSTREAM:
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
@@ -390,6 +399,16 @@ class OutputGate(FrameProcessor):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
async def start_fetch_weather(function_name, llm, context):
|
||||||
|
"""Push a frame to the LLM; this is handy when the LLM response might take a while."""
|
||||||
|
await llm.push_frame(TTSSpeakFrame("Let me check on that."))
|
||||||
|
logger.debug(f"Starting fetch_weather_from_api with function_name: {function_name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_weather_from_api(function_name, tool_call_id, args, llm, context, result_callback):
|
||||||
|
await result_callback({"conditions": "nice", "temperature": "75"})
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
(room_url, _) = await configure(session)
|
(room_url, _) = await configure(session)
|
||||||
@@ -426,6 +445,34 @@ async def main():
|
|||||||
api_key=os.getenv("OPENAI_API_KEY"),
|
api_key=os.getenv("OPENAI_API_KEY"),
|
||||||
model="gpt-4o",
|
model="gpt-4o",
|
||||||
)
|
)
|
||||||
|
# Register a function_name of None to get all functions
|
||||||
|
# sent to the same callback with an additional function_name parameter.
|
||||||
|
llm.register_function(None, fetch_weather_from_api, start_callback=start_fetch_weather)
|
||||||
|
|
||||||
|
tools = [
|
||||||
|
ChatCompletionToolParam(
|
||||||
|
type="function",
|
||||||
|
function={
|
||||||
|
"name": "get_current_weather",
|
||||||
|
"description": "Get the current weather",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The city and state, e.g. San Francisco, CA",
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["celsius", "fahrenheit"],
|
||||||
|
"description": "The temperature unit to use. Infer this from the users location.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["location", "format"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
{
|
{
|
||||||
@@ -434,7 +481,7 @@ async def main():
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
context = OpenAILLMContext(messages)
|
context = OpenAILLMContext(messages, tools)
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
# We have instructed the LLM to return 'YES' if it thinks the user
|
# We have instructed the LLM to return 'YES' if it thinks the user
|
||||||
@@ -474,6 +521,8 @@ async def main():
|
|||||||
or isinstance(frame, LLMMessagesFrame)
|
or isinstance(frame, LLMMessagesFrame)
|
||||||
or isinstance(frame, StartInterruptionFrame)
|
or isinstance(frame, StartInterruptionFrame)
|
||||||
or isinstance(frame, StopInterruptionFrame)
|
or isinstance(frame, StopInterruptionFrame)
|
||||||
|
or isinstance(frame, FunctionCallInProgressFrame)
|
||||||
|
or isinstance(frame, FunctionCallResultFrame)
|
||||||
)
|
)
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from pipecat.frames.frames import (
|
|||||||
CancelFrame,
|
CancelFrame,
|
||||||
EndFrame,
|
EndFrame,
|
||||||
Frame,
|
Frame,
|
||||||
|
FunctionCallInProgressFrame,
|
||||||
|
FunctionCallResultFrame,
|
||||||
InputAudioRawFrame,
|
InputAudioRawFrame,
|
||||||
LLMFullResponseEndFrame,
|
LLMFullResponseEndFrame,
|
||||||
LLMFullResponseStartFrame,
|
LLMFullResponseStartFrame,
|
||||||
@@ -575,6 +577,11 @@ class OutputGate(FrameProcessor):
|
|||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Don't block function call frames
|
||||||
|
if isinstance(frame, (FunctionCallInProgressFrame, FunctionCallResultFrame)):
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
return
|
||||||
|
|
||||||
# Ignore frames that are not following the direction of this gate.
|
# Ignore frames that are not following the direction of this gate.
|
||||||
if direction != FrameDirection.DOWNSTREAM:
|
if direction != FrameDirection.DOWNSTREAM:
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
@@ -672,12 +679,6 @@ async def main():
|
|||||||
context = OpenAILLMContext()
|
context = OpenAILLMContext()
|
||||||
context_aggregator = conversation_llm.create_context_aggregator(context)
|
context_aggregator = conversation_llm.create_context_aggregator(context)
|
||||||
|
|
||||||
# We have instructed the LLM to return 'True' if it thinks the user
|
|
||||||
# completed a sentence. So, if it's 'True' we will return true in this
|
|
||||||
# predicate which will wake up the notifier.
|
|
||||||
async def wake_check_filter(frame):
|
|
||||||
return frame.text == "True"
|
|
||||||
|
|
||||||
# This is a notifier that we use to synchronize the two LLMs.
|
# This is a notifier that we use to synchronize the two LLMs.
|
||||||
notifier = EventNotifier()
|
notifier = EventNotifier()
|
||||||
|
|
||||||
@@ -694,14 +695,6 @@ async def main():
|
|||||||
async def block_user_stopped_speaking(frame):
|
async def block_user_stopped_speaking(frame):
|
||||||
return not isinstance(frame, UserStoppedSpeakingFrame)
|
return not isinstance(frame, UserStoppedSpeakingFrame)
|
||||||
|
|
||||||
async def pass_only_llm_trigger_frames(frame):
|
|
||||||
return (
|
|
||||||
isinstance(frame, OpenAILLMContextFrame)
|
|
||||||
or isinstance(frame, LLMMessagesFrame)
|
|
||||||
or isinstance(frame, StartInterruptionFrame)
|
|
||||||
or isinstance(frame, StopInterruptionFrame)
|
|
||||||
)
|
|
||||||
|
|
||||||
conversation_audio_context_assembler = ConversationAudioContextAssembler(context=context)
|
conversation_audio_context_assembler = ConversationAudioContextAssembler(context=context)
|
||||||
|
|
||||||
user_aggregator_buffer = UserAggregatorBuffer()
|
user_aggregator_buffer = UserAggregatorBuffer()
|
||||||
|
|||||||
Reference in New Issue
Block a user