Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):

llm = AnthropicLLMService(
api_key=os.getenv("ANTHROPIC_API_KEY"),
enable_async_tool_cancellation=True,
settings=AnthropicLLMService.Settings(
system_instruction=(
"You are a helpful assistant in a voice conversation. "
Expand All @@ -139,9 +140,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
timeout_secs=30,
)

@llm.event_handler("on_function_calls_started")
Comment thread
filipi87 marked this conversation as resolved.
async def on_function_calls_started(service, function_calls):
await tts.queue_frame(TTSSpeakFrame("Sure, tracking your location now."))
@llm.event_handler("on_function_calls_cancelled")
async def on_function_calls_cancelled(service, function_calls):
for item in function_calls:
logger.info(f"Function call cancelled: {item.function_name} [{item.tool_call_id}]")

location_function = FunctionSchema(
name="track_current_location",
Expand Down
6 changes: 6 additions & 0 deletions examples/function-calling/function-calling-anthropic-async.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):

llm = AnthropicLLMService(
api_key=os.getenv("ANTHROPIC_API_KEY"),
enable_async_tool_cancellation=True,
settings=AnthropicLLMService.Settings(
system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.",
),
Expand All @@ -92,6 +93,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
)
llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation)

@llm.event_handler("on_function_calls_cancelled")
async def on_function_calls_cancelled(service, function_calls):
for item in function_calls:
logger.info(f"Function call cancelled: {item.function_name} [{item.tool_call_id}]")

weather_function = FunctionSchema(
name="get_current_weather",
description="Get the current weather",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):

llm = GoogleLLMService(
api_key=os.getenv("GOOGLE_API_KEY"),
enable_async_tool_cancellation=True,
settings=GoogleLLMService.Settings(
system_instruction=(
"You are a helpful assistant in a voice conversation. "
Expand All @@ -143,6 +144,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_function_calls_started(service, function_calls):
await tts.queue_frame(TTSSpeakFrame("Sure, tracking your location now."))

@llm.event_handler("on_function_calls_cancelled")
async def on_function_calls_cancelled(service, function_calls):
for item in function_calls:
logger.info(f"Function call cancelled: {item.function_name} [{item.tool_call_id}]")

location_function = FunctionSchema(
name="track_current_location",
description="Start tracking the user's current GPS location, reporting position updates until the user reaches their destination.",
Expand Down
6 changes: 6 additions & 0 deletions examples/function-calling/function-calling-google-async.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):

llm = GoogleLLMService(
api_key=os.getenv("GOOGLE_API_KEY"),
enable_async_tool_cancellation=True,
settings=GoogleLLMService.Settings(
system_instruction=system_prompt,
),
Expand All @@ -140,6 +141,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_function_calls_started(service, function_calls):
await tts.queue_frame(TTSSpeakFrame("Let me check on that."))

@llm.event_handler("on_function_calls_cancelled")
async def on_function_calls_cancelled(service, function_calls):
for item in function_calls:
logger.info(f"Function call cancelled: {item.function_name} [{item.tool_call_id}]")

weather_function = FunctionSchema(
name="get_weather",
description="Get the current weather",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):

llm = OpenAILLMService(
api_key=os.getenv("OPENAI_API_KEY"),
enable_async_tool_cancellation=True,
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a helpful assistant in a voice conversation. "
Expand All @@ -143,6 +144,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_function_calls_started(service, function_calls):
await tts.queue_frame(TTSSpeakFrame("Sure, tracking your location now."))

@llm.event_handler("on_function_calls_cancelled")
async def on_function_calls_cancelled(service, function_calls):
for item in function_calls:
logger.info(f"Function call cancelled: {item.function_name} [{item.tool_call_id}]")

location_function = FunctionSchema(
name="track_current_location",
description="Start tracking the user's current GPS location, reporting position updates until the user reaches their destination.",
Expand Down Expand Up @@ -181,6 +187,9 @@ async def on_function_calls_started(service, function_calls):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])

@transport.event_handler("on_client_disconnected")
Expand Down
9 changes: 9 additions & 0 deletions examples/function-calling/function-calling-openai-async.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):

llm = OpenAILLMService(
api_key=os.getenv("OPENAI_API_KEY"),
enable_async_tool_cancellation=True,
settings=OpenAILLMService.Settings(
system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.",
),
Expand All @@ -106,6 +107,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_function_calls_started(service, function_calls):
await tts.queue_frame(TTSSpeakFrame("Let me check on that."))

@llm.event_handler("on_function_calls_cancelled")
async def on_function_calls_cancelled(service, function_calls):
for item in function_calls:
logger.info(f"Function call cancelled: {item.function_name} [{item.tool_call_id}]")

weather_function = FunctionSchema(
name="get_current_weather",
description="Get the current weather",
Expand Down Expand Up @@ -165,6 +171,9 @@ async def on_function_calls_started(service, function_calls):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])

@transport.event_handler("on_client_disconnected")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):

llm = OpenAIResponsesLLMService(
api_key=os.getenv("OPENAI_API_KEY"),
enable_async_tool_cancellation=True,
settings=OpenAIResponsesLLMService.Settings(
system_instruction=(
"You are a helpful assistant in a voice conversation. "
Expand All @@ -143,6 +144,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_function_calls_started(service, function_calls):
await tts.queue_frame(TTSSpeakFrame("Sure, tracking your location now."))

@llm.event_handler("on_function_calls_cancelled")
async def on_function_calls_cancelled(service, function_calls):
for item in function_calls:
logger.info(f"Function call cancelled: {item.function_name} [{item.tool_call_id}]")

location_function = FunctionSchema(
name="track_current_location",
description="Track the device's current GPS location during a road trip, reporting position updates as the vehicle moves through cities until it reaches the final destination.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):

llm = OpenAIResponsesLLMService(
api_key=os.getenv("OPENAI_API_KEY"),
enable_async_tool_cancellation=True,
settings=OpenAIResponsesLLMService.Settings(
system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.",
),
Expand Down Expand Up @@ -104,6 +105,11 @@ async def on_function_calls_started(service, function_calls):
# matching, forcing a full context resend.
await tts.queue_frame(TTSSpeakFrame("Let me check on that.", append_to_context=False))

@llm.event_handler("on_function_calls_cancelled")
async def on_function_calls_cancelled(service, function_calls):
for item in function_calls:
logger.info(f"Function call cancelled: {item.function_name} [{item.tool_call_id}]")

weather_function = FunctionSchema(
name="get_current_weather",
description="Get the current weather",
Expand Down
45 changes: 44 additions & 1 deletion src/pipecat/adapters/base_llm_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
adapters that handle tool format conversion and standardization.
"""

import warnings
from abc import ABC, abstractmethod
from typing import Any, Dict, Generic, List, Optional, TypeVar

from loguru import logger

from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.processors.aggregators.llm_context import (
LLMContext,
Expand Down Expand Up @@ -48,6 +50,21 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]):
def __init__(self):
"""Initialize the adapter."""
self._warned_system_instruction = False
self._builtin_tools: Dict[str, FunctionSchema] = {}

@property
def builtin_tools(self) -> Dict[str, FunctionSchema]:
"""Built-in tools automatically merged into every inference request.

Keyed by tool name for O(1) lookup, insertion, and removal. The
service injects tools here so they are sent transparently on every
inference request without the user having to add them to their
``ToolsSchema``.

Returns:
Mutable dict mapping tool name to ``FunctionSchema``.
"""
return self._builtin_tools

@property
@abstractmethod
Expand Down Expand Up @@ -122,15 +139,41 @@ def get_messages(self, context: LLMContext) -> List[LLMContextMessage]:
def from_standard_tools(self, tools: Any) -> List[Any] | NotGiven:
"""Convert tools from standard format to provider format.

Built-in tools are automatically merged into the schema before conversion so that every
inference request receives them without the user having to declare them explicitly.

Args:
tools: Tools in standard format or provider-specific format.

Returns:
List of tools converted to provider format, or original tools
if not in standard format.
"""
if self._builtin_tools:
if isinstance(tools, ToolsSchema):
tools = ToolsSchema(
standard_tools=tools.standard_tools + list(self._builtin_tools.values()),
custom_tools=tools.custom_tools,
)
else:
# User supplied tools in a legacy/provider-specific format.
# Built-in tools cannot be safely merged, so they will not be injected.
# Migrate to ToolsSchema to enable built-in tool support; use custom_tools
# as an escape hatch for any provider-specific tools that don't fit the
# standard schema.
if tools is not None:
warnings.warn(
"Built-in tools (e.g. async tool cancellation) could not be injected "
"because the supplied tools are not a ToolsSchema instance. "
"Migrate to ToolsSchema to enable built-in tool support. "
"Use ToolsSchema(custom_tools=...) as an escape hatch for any "
"provider-specific tools that don't fit the standard schema.",
DeprecationWarning,
stacklevel=2,
)
# Fall through and return the original tools unchanged.

if isinstance(tools, ToolsSchema):
logger.debug(f"Retrieving the tools using the adapter: {type(self)}")
return self.to_provider_tools_format(tools)
# Fallback to return the same tools in case they are not in a standard format
return tools
Expand Down
Loading
Loading