steep-mechanic-38687
12/22/2025, 3:24 PM@ctx.room.on("participant_attributes_changed")
def _on_participant_attributes_changed(
changed_attributes: dict[str, str], participant: rtc.Participant
):
if (
participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP
and "sip.callStatus" in changed_attributes
):
call_status = changed_attributes["sip.callStatus"]
if call_status == "active":
<http://logger.info|logger.info>(f"Callee answered the call (room: {ctx.room.name})")
nonlocal call_was_answered
call_was_answered = True
nonlocal call_answered_at
call_answered_at = datetime.now()
async def say_first_sentence():
<http://logger.info|logger.info>(f"Generating first sentence: {first_sentence}")
await session.say(first_sentence)
<http://logger.info|logger.info>("First sentence delivered")
task = asyncio.create_task(say_first_sentence())
task.add_done_callback(
lambda t: <http://logger.info|logger.info>(f"First sentence task finished: {t.result()}")
)tall-airline-16756
12/22/2025, 3:24 PMtall-airline-16756
12/22/2025, 3:24 PMparticipant_attributes_changed event and checking participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP targets SIP participants.
• Checking "sip.callStatus" in changed_attributes and testing for "active" correctly detects when the callee answered.
• Creating an async task for session.say(...) avoids blocking the event handler; the add_done_callback provides completion logging.
• Using nonlocal for call_was_answered and call_answered_at is valid if those variables are defined in the enclosing scope.
Optional recommendations (not required to function)
• Add a guard to avoid handling duplicate events:
◦ e.g., if call_was_answered: return before you set it.
• Add try/except around session.say(...) inside say_first_sentence() to catch TTS/network errors and log them.
• Keep a reference to tasks (e.g., append to a list) until they finish to avoid accidental GC; your add_done_callback helps but storing tasks is more explicit.
• Include identifying info in logs (like participant.identity or ctx.room.name) for easier debugging.
• If timing matters, consider recording call_answered_at before creating the task.
If you’d like, I can:
• Show a short revised snippet with the try/except and idempotency guard.
• Help add task-tracking boilerplate. Which would you prefer? Sources: Participant attributes and metadata | LiveKit docs | Handling events | LiveKit docs | SIP Lifecycle Management Agent | LiveKit docs | Job lifecycle | LiveKit docs | Agents telephony integration | LiveKit docs