├── .env.example ├── .gitignore ├── README.md ├── requirements.txt └── tavus_agent.py /.env.example: -------------------------------------------------------------------------------- 1 | CARTESIA_API_KEY= 2 | OPENAI_API_KEY= 3 | DEEPGRAM_API_KEY= 4 | 5 | TAVUS_API_KEY= 6 | TAVUS_REPLICA_ID= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | venv/ 3 | env/ 4 | .DS_Store 5 | 6 | .env.local 7 | .env 8 | .env.production 9 | 10 | __pycache__/ 11 | *.pyc 12 | *.pyo 13 | *.pyd 14 | *.pyw -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pipecat Tavus AI Avatar Agent 2 | 3 | This is an example of a Tavus avatar that uses the Pipecat SDK to create an AI agent that can be used in a Daily room. 4 | 5 | ## Installation 6 | 7 | ```console 8 | python -m venv venv 9 | source venv/bin/activate 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | ## Setup 14 | 15 | In this project's directory, run the following command to copy the `.env.example` file to `.env`: 16 | 17 | ```console 18 | cp .env.example .env 19 | ``` 20 | 21 | Edit the `.env` file with your own values. 22 | 23 | ### Tavus 24 | 25 | Visit https://tavus.io to get your `TAVUS_API_KEY` and `TAVUS_REPLICA_ID`. 26 | 27 | ### OpenAI 28 | 29 | Visit https://platform.openai.com to get your `OPENAI_API_KEY`. 30 | 31 | ### Cartesia 32 | 33 | Visit https://cartesia.ai to get your `CARTESIA_API_KEY`. 34 | 35 | ### Deepgram 36 | 37 | Visit https://deepgram.com to get your `DEEPGRAM_API_KEY`. 38 | 39 | ## Usage 40 | 41 | Run the following command to start the agent: 42 | ```console 43 | python tavus_agent.py 44 | ``` 45 | 46 | Then, from the logs in above, find and copy the `URL` of the room and paste it into your browser. It should look something like: `Joining https://tavus.daily.co/`. Follow that link in the browser to join the room and begin speaking with your avatar. 47 | 48 | 49 | ## References 50 | 51 | - [Pipecat AI Documentation](https://docs.pipecat.ai/server/services/video/tavus) 52 | - [Pipecat AI GitHub Example](https://github.com/pipecat-ai/pipecat/blob/main/examples/foundational/21-tavus-layer.py) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pipecat-ai[daily,deepgram,cartesia,openai,silero,tavus] 2 | python-dotenv~=1.0 -------------------------------------------------------------------------------- /tavus_agent.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024–2025, Daily 3 | # 4 | # SPDX-License-Identifier: BSD 2-Clause License 5 | # 6 | 7 | import asyncio 8 | import os 9 | import sys 10 | from typing import Any, Mapping 11 | 12 | import aiohttp 13 | from dotenv import load_dotenv 14 | from loguru import logger 15 | 16 | from pipecat.audio.vad.silero import SileroVADAnalyzer 17 | from pipecat.pipeline.pipeline import Pipeline 18 | from pipecat.pipeline.runner import PipelineRunner 19 | from pipecat.pipeline.task import PipelineParams, PipelineTask 20 | from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext 21 | from pipecat.services.cartesia.tts import CartesiaTTSService 22 | from pipecat.services.deepgram.stt import DeepgramSTTService 23 | from pipecat.services.openai.llm import OpenAILLMService 24 | from pipecat.services.tavus.video import TavusVideoService 25 | from pipecat.transports.services.daily import DailyParams, DailyTransport 26 | 27 | load_dotenv(override=True) 28 | 29 | logger.remove(0) 30 | logger.add(sys.stderr, level="DEBUG") 31 | 32 | 33 | async def main(): 34 | async with aiohttp.ClientSession() as session: 35 | tavus = TavusVideoService( 36 | api_key=os.getenv("TAVUS_API_KEY"), 37 | replica_id=os.getenv("TAVUS_REPLICA_ID"), 38 | session=session, 39 | ) 40 | 41 | # get persona, look up persona_name, set this as the bot name to ignore 42 | persona_name = await tavus.get_persona_name() 43 | room_url = await tavus.initialize() 44 | 45 | transport = DailyTransport( 46 | room_url=room_url, 47 | token=None, 48 | bot_name="Pipecat bot", 49 | params=DailyParams( 50 | vad_enabled=True, 51 | vad_analyzer=SileroVADAnalyzer(), 52 | vad_audio_passthrough=True, 53 | ), 54 | ) 55 | 56 | stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) 57 | 58 | tts = CartesiaTTSService( 59 | api_key=os.getenv("CARTESIA_API_KEY"), 60 | voice_id="156fb8d2-335b-4950-9cb3-a2d33befec77", 61 | ) 62 | 63 | llm = OpenAILLMService(model="gpt-4o-mini") 64 | 65 | messages = [ 66 | { 67 | "role": "system", 68 | "content": "You are a helpful assistant named Luna. 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.", 69 | }, 70 | ] 71 | 72 | context = OpenAILLMContext(messages) 73 | context_aggregator = llm.create_context_aggregator(context) 74 | 75 | pipeline = Pipeline( 76 | [ 77 | transport.input(), # Transport user input 78 | stt, # STT 79 | context_aggregator.user(), # User responses 80 | llm, # LLM 81 | tts, # TTS 82 | tavus, # Tavus output layer 83 | transport.output(), # Transport bot output 84 | context_aggregator.assistant(), # Assistant spoken responses 85 | ] 86 | ) 87 | 88 | task = PipelineTask( 89 | pipeline, 90 | params=PipelineParams( 91 | # We just use 16000 because that's what Tavus is expecting and 92 | # we avoid resampling. 93 | audio_in_sample_rate=16000, 94 | audio_out_sample_rate=16000, 95 | allow_interruptions=True, 96 | enable_metrics=True, 97 | enable_usage_metrics=True, 98 | report_only_initial_ttfb=True, 99 | ), 100 | ) 101 | 102 | @transport.event_handler("on_participant_joined") 103 | async def on_participant_joined( 104 | transport: DailyTransport, participant: Mapping[str, Any] 105 | ) -> None: 106 | # Ignore the Tavus replica's microphone 107 | if participant.get("info", {}).get("userName", "") == persona_name: 108 | logger.debug(f"Ignoring {participant['id']}'s microphone") 109 | await transport.update_subscriptions( 110 | participant_settings={ 111 | participant["id"]: { 112 | "media": {"microphone": "unsubscribed"}, 113 | } 114 | } 115 | ) 116 | 117 | if participant.get("info", {}).get("userName", "") != persona_name: 118 | # Kick off the conversation. 119 | messages.append( 120 | {"role": "system", "content": "Please introduce yourself to the user."} 121 | ) 122 | await task.queue_frames([context_aggregator.user().get_context_frame()]) 123 | 124 | @transport.event_handler("on_participant_left") 125 | async def on_participant_left(transport, participant, reason): 126 | await task.cancel() 127 | 128 | runner = PipelineRunner() 129 | 130 | await runner.run(task) 131 | 132 | 133 | if __name__ == "__main__": 134 | asyncio.run(main()) --------------------------------------------------------------------------------