These messages are developer instructions to the assistant (e.g. "Please introduce yourself to the user"), not simulated user input. The "developer" role is semantically correct for this purpose.
204 lines
6.8 KiB
Python
204 lines
6.8 KiB
Python
#
|
|
# Copyright (c) 2024-2026, Daily
|
|
#
|
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
#
|
|
|
|
import os
|
|
from datetime import datetime
|
|
|
|
from dotenv import load_dotenv
|
|
from loguru import logger
|
|
|
|
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
|
from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema
|
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
|
from pipecat.audio.vad.vad_analyzer import VADParams
|
|
from pipecat.frames.frames import EndTaskFrame, LLMRunFrame
|
|
from pipecat.pipeline.pipeline import Pipeline
|
|
from pipecat.pipeline.runner import PipelineRunner
|
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
from pipecat.processors.aggregators.llm_response_universal import (
|
|
LLMContextAggregatorPair,
|
|
LLMUserAggregatorParams,
|
|
)
|
|
from pipecat.processors.frame_processor import FrameDirection
|
|
from pipecat.runner.types import RunnerArguments
|
|
from pipecat.runner.utils import create_transport
|
|
from pipecat.services.google.gemini_live.llm import GeminiLiveLLMService
|
|
from pipecat.services.llm_service import FunctionCallParams
|
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
|
from pipecat.transports.daily.transport import DailyParams
|
|
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
|
|
|
|
load_dotenv(override=True)
|
|
|
|
|
|
async def fetch_weather_from_api(params: FunctionCallParams):
|
|
temperature = 75 if params.arguments["format"] == "fahrenheit" else 24
|
|
await params.result_callback(
|
|
{
|
|
"conditions": "nice",
|
|
"temperature": temperature,
|
|
"format": params.arguments["format"],
|
|
"timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
|
|
}
|
|
)
|
|
|
|
|
|
async def fetch_restaurant_recommendation(params: FunctionCallParams):
|
|
await params.result_callback({"name": "The Golden Dragon"})
|
|
|
|
|
|
async def end_conversation(params: FunctionCallParams):
|
|
await params.result_callback({"success": True})
|
|
await params.llm.push_frame(EndTaskFrame(), FrameDirection.UPSTREAM)
|
|
|
|
|
|
system_instruction = """
|
|
You are a helpful assistant who can answer questions and use tools.
|
|
|
|
You have three tools available to you:
|
|
1. get_current_weather: Use this tool to get the current weather in a specific location.
|
|
2. get_restaurant_recommendation: Use this tool to get a restaurant recommendation in a specific location.
|
|
3. end_conversation: Use this tool to gracefully end the conversation.
|
|
|
|
After you've responded to the user three times, do two things, in order:
|
|
1. Politely let them know that that's all the time you have today and say goodbye.
|
|
2. *WITHOUT WAITING FOR THE USER TO RESPOND*, call the end_conversation tool to gracefully end the conversation.
|
|
"""
|
|
|
|
|
|
# We use lambdas to defer transport parameter creation until the transport
|
|
# type is selected at runtime.
|
|
transport_params = {
|
|
"daily": lambda: DailyParams(
|
|
audio_in_enabled=True,
|
|
audio_out_enabled=True,
|
|
),
|
|
"twilio": lambda: FastAPIWebsocketParams(
|
|
audio_in_enabled=True,
|
|
audio_out_enabled=True,
|
|
),
|
|
"webrtc": lambda: TransportParams(
|
|
audio_in_enabled=True,
|
|
audio_out_enabled=True,
|
|
),
|
|
}
|
|
|
|
|
|
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|
logger.info(f"Starting bot")
|
|
|
|
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"],
|
|
)
|
|
restaurant_function = FunctionSchema(
|
|
name="get_restaurant_recommendation",
|
|
description="Get a restaurant recommendation",
|
|
properties={
|
|
"location": {
|
|
"type": "string",
|
|
"description": "The city and state, e.g. San Francisco, CA",
|
|
},
|
|
},
|
|
required=["location"],
|
|
)
|
|
end_conversation_function = FunctionSchema(
|
|
name="end_conversation",
|
|
description="Gracefully end the conversation",
|
|
properties={},
|
|
required=[],
|
|
)
|
|
search_tool = {"google_search": {}}
|
|
tools = ToolsSchema(
|
|
standard_tools=[weather_function, restaurant_function, end_conversation_function],
|
|
custom_tools={AdapterType.GEMINI: [search_tool]},
|
|
)
|
|
|
|
llm = GeminiLiveLLMService(
|
|
api_key=os.getenv("GOOGLE_API_KEY"),
|
|
settings=GeminiLiveLLMService.Settings(
|
|
system_instruction=system_instruction,
|
|
),
|
|
tools=tools,
|
|
)
|
|
|
|
llm.register_function("get_current_weather", fetch_weather_from_api)
|
|
llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation)
|
|
llm.register_function("end_conversation", end_conversation)
|
|
|
|
context = LLMContext(
|
|
[{"role": "developer", "content": "Say hello."}],
|
|
)
|
|
user_aggregator, assistant_aggregator = LLMContextAggregatorPair(
|
|
context,
|
|
user_params=LLMUserAggregatorParams(
|
|
# Set stop_secs to something roughly similar to the internal setting
|
|
# of the Multimodal Live api, just to align events. This doesn't
|
|
# really matter because we can only use the Multimodal Live API's
|
|
# phrase endpointing, for now.
|
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5))
|
|
),
|
|
)
|
|
|
|
pipeline = Pipeline(
|
|
[
|
|
transport.input(),
|
|
user_aggregator,
|
|
llm,
|
|
transport.output(),
|
|
assistant_aggregator,
|
|
]
|
|
)
|
|
|
|
task = PipelineTask(
|
|
pipeline,
|
|
params=PipelineParams(
|
|
enable_metrics=True,
|
|
enable_usage_metrics=True,
|
|
),
|
|
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
|
|
)
|
|
|
|
@transport.event_handler("on_client_connected")
|
|
async def on_client_connected(transport, client):
|
|
logger.info(f"Client connected")
|
|
# Kick off the conversation.
|
|
await task.queue_frames([LLMRunFrame()])
|
|
|
|
@transport.event_handler("on_client_disconnected")
|
|
async def on_client_disconnected(transport, client):
|
|
logger.info(f"Client disconnected")
|
|
await task.cancel()
|
|
|
|
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
|
|
|
|
await runner.run(task)
|
|
|
|
|
|
async def bot(runner_args: RunnerArguments):
|
|
"""Main bot entry point compatible with Pipecat Cloud."""
|
|
transport = await create_transport(runner_args, transport_params)
|
|
await run_bot(transport, runner_args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
from pipecat.runner.run import main
|
|
|
|
main()
|