This example demonstrates an automated survey calling agent that collects responses via phone calls, stores them in a CSV file, and cleans up the room after completion. The agent reads survey configuration from job metadata and uses function tools to record answers.
Prerequisites
- Add a
.envin this directory with your LiveKit credentials:LIVEKIT_URL=your_livekit_urlLIVEKIT_API_KEY=your_api_keyLIVEKIT_API_SECRET=your_api_secret - Install dependencies:pip install "livekit-agents[silero]" python-dotenv pandas
Load environment and define the AgentServer
Import the necessary modules, load environment variables, and create an AgentServer. The CSV file path is defined relative to the script location.
import loggingimport asyncioimport pandas as pdimport jsonfrom pathlib import Pathfrom dotenv import load_dotenvfrom livekit.agents import JobContext, JobProcess, AgentServer, cli, Agent, AgentSession, inference, RunContext, function_toolfrom livekit.plugins import silerofrom livekit.api import DeleteRoomRequestload_dotenv()logger = logging.getLogger("calling-agent")logger.setLevel(logging.INFO)csv_file_path = Path(__file__).parent / "survey_data.csv"server = AgentServer()
Prewarm VAD for faster connections
Preload the VAD model once per process to reduce connection latency. The VAD instance is stored in proc.userdata for reuse across sessions.
def prewarm(proc: JobProcess):proc.userdata["vad"] = silero.VAD.load()server.setup_fnc = prewarm
Define the survey agent
Create a lightweight Agent that only contains instructions and a function tool. The survey question is passed dynamically and included in the instructions. The record_survey_answer tool saves the response to CSV and deletes the room after completion.
class SurveyAgent(Agent):def __init__(self, question="Do you prefer chocolate or vanilla ice cream?", context=None, job_context=None) -> None:self.survey_question = questionself.context = context or {}self.job_context = job_contextself.survey_answer = Noneself.phone_number = self.context.get("phone_number", "unknown")self.row_index = self.context.get("row_index", 1)instructions = f"""You are conducting a brief phone survey. Your goal is to ask the following question:'{self.survey_question}'Be polite and professional. Introduce yourself as a survey caller named "Sam", ask the question,and thank them for their time. Keep the call brief and focused on getting their answer.Don't ask any follow-up questions.Note: When you have an answer to the question, use the `record_survey_answer` functionto persist what the user said."""super().__init__(instructions=instructions)@function_toolasync def record_survey_answer(self, context: RunContext, answer: str):logger.info(f"Survey answer recorded: {answer}")self.survey_answer = answerdf = pd.read_csv(csv_file_path, dtype=str)df.loc[self.row_index - 1, 'Answer'] = answerdf.loc[self.row_index - 1, 'Status'] = 'Completed'df.to_csv(csv_file_path, index=False)await asyncio.sleep(5)await self.job_context.api.room.delete_room(DeleteRoomRequest(room=self.job_context.room.name))return None, f"[Call ended]"
Create the RTC session entrypoint
Parse survey configuration from job metadata, create an AgentSession with STT/LLM/TTS/VAD, and start the session. The ctx.connect() call binds the room after session startup.
@server.rtc_session()async def entrypoint(ctx: JobContext):ctx.log_context_fields = {"room": ctx.room.name}metadata_json = ctx.job.metadatametadata = json.loads(metadata_json)phone_number = metadata.get("phone_number", "unknown")row_index = metadata.get("row_index", 1)question = metadata.get("question", "Do you prefer chocolate or vanilla ice cream?")context = {"phone_number": phone_number,"row_index": row_index}session = AgentSession(stt=inference.STT(model="deepgram/nova-3-general"),llm=inference.LLM(model="openai/gpt-4.1-mini"),tts=inference.TTS(model="cartesia/sonic-3", voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc"),vad=ctx.proc.userdata["vad"],preemptive_generation=True,)agent = SurveyAgent(question=question, context=context, job_context=ctx)await session.start(agent=agent, room=ctx.room)await ctx.connect()
Run the server
The cli.run_app() function starts the agent server and manages the worker lifecycle.
if __name__ == "__main__":cli.run_app(server)
Run it
python survey_calling_agent.py console
How it works
- Job metadata contains the survey question, phone number, and CSV row index.
- The agent introduces itself as "Sam" and asks the configured question.
- When the user responds, the agent calls
record_survey_answerto save the response. - The function tool updates the CSV file with the answer and status.
- After a brief delay, the room is automatically deleted to clean up resources.
Full example
import loggingimport asyncioimport pandas as pdimport jsonfrom pathlib import Pathfrom dotenv import load_dotenvfrom livekit.agents import JobContext, JobProcess, AgentServer, cli, Agent, AgentSession, inference, RunContext, function_toolfrom livekit.plugins import silerofrom livekit.api import DeleteRoomRequestload_dotenv()logger = logging.getLogger("calling-agent")logger.setLevel(logging.INFO)csv_file_path = Path(__file__).parent / "survey_data.csv"class SurveyAgent(Agent):def __init__(self, question="Do you prefer chocolate or vanilla ice cream?", context=None, job_context=None) -> None:self.survey_question = questionself.context = context or {}self.job_context = job_contextself.survey_answer = Noneself.phone_number = self.context.get("phone_number", "unknown")self.row_index = self.context.get("row_index", 1)instructions = f"""You are conducting a brief phone survey. Your goal is to ask the following question:'{self.survey_question}'Be polite and professional. Introduce yourself as a survey caller named "Sam", ask the question,and thank them for their time. Keep the call brief and focused on getting their answer.Don't ask any follow-up questions.Note: When you have an answer to the question, use the `record_survey_answer` functionto persist what the user said."""super().__init__(instructions=instructions)@function_toolasync def record_survey_answer(self, context: RunContext, answer: str):logger.info(f"Survey answer recorded: {answer}")logger.info(f"Row index: {self.row_index}")self.survey_answer = answerdf = pd.read_csv(csv_file_path, dtype=str)logger.info(f"CSV contents before update: {df.head()}")df.loc[self.row_index - 1, 'Answer'] = answerdf.loc[self.row_index - 1, 'Status'] = 'Completed'logger.info(f"CSV contents after update: {df.head()}")df.to_csv(csv_file_path, index=False)await asyncio.sleep(5)await self.job_context.api.room.delete_room(DeleteRoomRequest(room=self.job_context.room.name))return None, f"[Call ended]"server = AgentServer()def prewarm(proc: JobProcess):proc.userdata["vad"] = silero.VAD.load()server.setup_fnc = prewarm@server.rtc_session()async def entrypoint(ctx: JobContext):ctx.log_context_fields = {"room": ctx.room.name}metadata_json = ctx.job.metadatalogger.info(f"Received metadata: {metadata_json}")metadata = json.loads(metadata_json)phone_number = metadata.get("phone_number", "unknown")row_index = metadata.get("row_index", 1)question = metadata.get("question", "Do you prefer chocolate or vanilla ice cream?")logger.info(f"Parsed metadata - phone_number: {phone_number}, row_index: {row_index}, question: {question}")context = {"phone_number": phone_number,"row_index": row_index}session = AgentSession(stt=inference.STT(model="deepgram/nova-3-general"),llm=inference.LLM(model="openai/gpt-4.1-mini"),tts=inference.TTS(model="cartesia/sonic-3", voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc"),vad=ctx.proc.userdata["vad"],preemptive_generation=True,)agent = SurveyAgent(question=question, context=context, job_context=ctx)await session.start(agent=agent, room=ctx.room)await ctx.connect()if __name__ == "__main__":cli.run_app(server)