AWSBedrockLLMService: fix function calling
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
139
examples/foundational/14r-function-calling-aws.py
Normal file
139
examples/foundational/14r-function-calling-aws.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, 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()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user