AWSBedrockLLMService: fix function calling

This commit is contained in:
Aleix Conchillo Flaqué
2025-05-06 21:07:09 -07:00
parent a8405649d0
commit 458549f7df
6 changed files with 178 additions and 11 deletions

View File

@@ -17,7 +17,6 @@ from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.aws.llm import AWSBedrockLLMService from pipecat.services.aws.llm import AWSBedrockLLMService
from pipecat.services.aws.stt import AWSTranscribeSTTService from pipecat.services.aws.stt import AWSTranscribeSTTService
from pipecat.services.aws.tts import AWSPollyTTSService from pipecat.services.aws.tts import AWSPollyTTSService
from pipecat.transcriptions.language import Language
from pipecat.transports.base_transport import TransportParams from pipecat.transports.base_transport import TransportParams
from pipecat.transports.network.small_webrtc import SmallWebRTCTransport from pipecat.transports.network.small_webrtc import SmallWebRTCTransport
from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
@@ -42,9 +41,7 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac
tts = AWSPollyTTSService( tts = AWSPollyTTSService(
region="us-west-2", # only specific regions support generative TTS region="us-west-2", # only specific regions support generative TTS
voice_id="Joanna", voice_id="Joanna",
params=AWSPollyTTSService.InputParams( params=AWSPollyTTSService.InputParams(engine="generative", rate="1.1"),
engine="generative", language=Language.EN_US, rate="1.1"
),
) )
llm = AWSBedrockLLMService( llm = AWSBedrockLLMService(

View File

@@ -0,0 +1,139 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import argparse
import os
from dotenv import load_dotenv
from loguru import logger
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.aws.llm import AWSBedrockLLMService
from pipecat.services.aws.stt import AWSTranscribeSTTService
from pipecat.services.aws.tts import AWSPollyTTSService
from pipecat.services.llm_service import FunctionCallParams
from pipecat.transports.base_transport import TransportParams
from pipecat.transports.network.small_webrtc import SmallWebRTCTransport
from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
load_dotenv(override=True)
async def fetch_weather_from_api(params: FunctionCallParams):
await params.result_callback({"conditions": "nice", "temperature": "75"})
async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespace):
logger.info(f"Starting bot")
transport = SmallWebRTCTransport(
webrtc_connection=webrtc_connection,
params=TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(),
),
)
stt = AWSTranscribeSTTService()
tts = AWSPollyTTSService(
region="us-west-2", # only specific regions support generative TTS
voice_id="Joanna",
params=AWSPollyTTSService.InputParams(engine="generative", rate="1.1"),
)
llm = AWSBedrockLLMService(
aws_region="us-west-2",
model="us.anthropic.claude-3-5-haiku-20241022-v1:0",
params=AWSBedrockLLMService.InputParams(temperature=0.8, latency="optimized"),
)
# You can also register a function_name of None to get all functions
# sent to the same callback with an additional function_name parameter.
llm.register_function("get_current_weather", fetch_weather_from_api)
weather_function = FunctionSchema(
name="get_current_weather",
description="Get the current weather",
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 user's location.",
},
},
required=["location", "format"],
)
tools = ToolsSchema(standard_tools=[weather_function])
messages = [
{
"role": "system",
"content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.",
},
]
context = OpenAILLMContext(messages, tools)
context_aggregator = llm.create_context_aggregator(context)
pipeline = Pipeline(
[
transport.input(),
stt,
context_aggregator.user(),
llm,
tts,
transport.output(),
context_aggregator.assistant(),
]
)
task = PipelineTask(
pipeline,
params=PipelineParams(
allow_interruptions=True,
enable_metrics=True,
enable_usage_metrics=True,
report_only_initial_ttfb=True,
),
)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
messages.append({"role": "user", "content": "Please introduce yourself to the user."})
await task.queue_frames([context_aggregator.user().get_context_frame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
@transport.event_handler("on_client_closed")
async def on_client_closed(transport, client):
logger.info(f"Client closed connection")
await task.cancel()
runner = PipelineRunner(handle_sigint=False)
await runner.run(task)
if __name__ == "__main__":
from run import main
main()

View File

@@ -4,7 +4,7 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
from typing import Any, Dict, List, Union from typing import Any, Dict, List
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
from pipecat.adapters.schemas.function_schema import FunctionSchema from pipecat.adapters.schemas.function_schema import FunctionSchema

View File

@@ -11,7 +11,7 @@ from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.adapters.schemas.tools_schema import ToolsSchema
class BedrockLLMAdapter(BaseLLMAdapter): class AWSBedrockLLMAdapter(BaseLLMAdapter):
@staticmethod @staticmethod
def _to_bedrock_function_format(function: FunctionSchema) -> Dict[str, Any]: def _to_bedrock_function_format(function: FunctionSchema) -> Dict[str, Any]:
return { return {

View File

@@ -17,6 +17,7 @@ from loguru import logger
from PIL import Image from PIL import Image
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pipecat.adapters.services.bedrock_adapter import AWSBedrockLLMAdapter
from pipecat.frames.frames import ( from pipecat.frames.frames import (
Frame, Frame,
FunctionCallCancelFrame, FunctionCallCancelFrame,
@@ -92,7 +93,6 @@ class AWSBedrockLLMContext(OpenAILLMContext):
@classmethod @classmethod
def from_openai_context(cls, openai_context: OpenAILLMContext): def from_openai_context(cls, openai_context: OpenAILLMContext):
logger.debug("from_openai_context called")
self = cls( self = cls(
messages=openai_context.messages, messages=openai_context.messages,
tools=openai_context.tools, tools=openai_context.tools,
@@ -105,7 +105,7 @@ class AWSBedrockLLMContext(OpenAILLMContext):
@classmethod @classmethod
def from_messages(cls, messages: List[dict]) -> "AWSBedrockLLMContext": def from_messages(cls, messages: List[dict]) -> "AWSBedrockLLMContext":
self = cls(messages=messages) self = cls(messages=messages)
# self._restructure_from_openai_messages() self._restructure_from_openai_messages()
return self return self
@classmethod @classmethod
@@ -118,7 +118,7 @@ class AWSBedrockLLMContext(OpenAILLMContext):
def set_messages(self, messages: List): def set_messages(self, messages: List):
self._messages[:] = messages self._messages[:] = messages
# self._restructure_from_openai_messages() self._restructure_from_openai_messages()
# convert a message in AWS Bedrock format into one or more messages in OpenAI format # convert a message in AWS Bedrock format into one or more messages in OpenAI format
def to_standard_messages(self, obj): def to_standard_messages(self, obj):
@@ -334,7 +334,6 @@ class AWSBedrockLLMContext(OpenAILLMContext):
""" """
# Handle system message if present at the beginning # Handle system message if present at the beginning
logger.debug(f"_restructure_from_bedrock_messages: {self.messages}")
if self.messages and self.messages[0]["role"] == "system": if self.messages and self.messages[0]["role"] == "system":
if len(self.messages) == 1: if len(self.messages) == 1:
self.messages[0]["role"] = "user" self.messages[0]["role"] = "user"
@@ -375,7 +374,6 @@ class AWSBedrockLLMContext(OpenAILLMContext):
self.messages.extend(merged_messages) self.messages.extend(merged_messages)
def _restructure_from_openai_messages(self): def _restructure_from_openai_messages(self):
logger.debug(f"_restructure_from_openai_messages: {self.messages}")
# first, map across self._messages calling self.from_standard_message(m) to modify messages in place # first, map across self._messages calling self.from_standard_message(m) to modify messages in place
try: try:
self._messages[:] = [self.from_standard_message(m) for m in self._messages] self._messages[:] = [self.from_standard_message(m) for m in self._messages]
@@ -517,6 +515,9 @@ class AWSBedrockLLMService(LLMService):
""" """
# Overriding the default adapter to use the Anthropic one.
adapter_class = AWSBedrockLLMAdapter
class InputParams(BaseModel): class InputParams(BaseModel):
max_tokens: Optional[int] = Field(default_factory=lambda: 4096, ge=1) max_tokens: Optional[int] = Field(default_factory=lambda: 4096, ge=1)
temperature: Optional[float] = Field(default_factory=lambda: 0.7, ge=0.0, le=1.0) temperature: Optional[float] = Field(default_factory=lambda: 0.7, ge=0.0, le=1.0)

View File

@@ -11,6 +11,7 @@ from openai.types.chat import ChatCompletionToolParam
from pipecat.adapters.schemas.function_schema import FunctionSchema from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema
from pipecat.adapters.services.anthropic_adapter import AnthropicLLMAdapter from pipecat.adapters.services.anthropic_adapter import AnthropicLLMAdapter
from pipecat.adapters.services.bedrock_adapter import AWSBedrockLLMAdapter
from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter
from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter
from pipecat.adapters.services.open_ai_realtime_adapter import OpenAIRealtimeLLMAdapter from pipecat.adapters.services.open_ai_realtime_adapter import OpenAIRealtimeLLMAdapter
@@ -174,3 +175,32 @@ class TestFunctionAdapters(unittest.TestCase):
tools_def = self.tools_def tools_def = self.tools_def
tools_def.custom_tools = {AdapterType.GEMINI: [search_tool]} tools_def.custom_tools = {AdapterType.GEMINI: [search_tool]}
assert GeminiLLMAdapter().to_provider_tools_format(tools_def) == expected assert GeminiLLMAdapter().to_provider_tools_format(tools_def) == expected
def test_bedrock_adapter(self):
"""Test AWS Bedrock adapter format transformation."""
expected = [
{
"toolSpec": {
"name": "get_weather",
"description": "Get the weather in a given location",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use.",
},
"location": {
"type": "string",
"description": "The city, e.g. San Francisco",
},
},
"required": ["location", "format"],
}
},
}
}
]
assert AWSBedrockLLMAdapter().to_provider_tools_format(self.tools_def) == expected