delete_room 을 하면 shut down 이벤트가 발생되지 않을때가 있어서 문제가 ...
# ask-ai
l
delete_room 을 하면 shut down 이벤트가 발생되지 않을때가 있어서 문제가 생긴건데, 다음 제공한 내용이 맞는지 검토해줘. ┌──────────────┬───────────────────────────────────────────────────┬────────────────┐ │ 시간 │ 이벤트 │ 출처 │ ├──────────────┼───────────────────────────────────────────────────┼────────────────┤ │ 101333.502 │ END_CALL_BY_SYSTEM (stage: start) │ outbound-agent │ ├──────────────┼───────────────────────────────────────────────────┼────────────────┤ │ 101339.872 │ "지금 통화를 종료하겠습니다." (마지막 agent 로그) │ outbound-agent │ ├──────────────┼───────────────────────────────────────────────────┼────────────────┤ │ 101339.890 │ deleting room (SERVICE_REQUEST_DELETE_ROOM) │ livekit-server │ ├──────────────┼───────────────────────────────────────────────────┼────────────────┤ │ 101339.890 │ participant closing: EG (egress), agent, SIP │ livekit-server │ ├──────────────┼───────────────────────────────────────────────────┼────────────────┤ │ 101339.901 │ room closed │ livekit-server │ ├──────────────┼───────────────────────────────────────────────────┼────────────────┤ │ 이후 │ outbound-agent 로그 없음 │ - │ └──────────────┴───────────────────────────────────────────────────┴────────────────┘ 문제: delete_room이 shutdown_callback보다 먼저 실행됨 end_call_by_system() (base_agent.py:301-367) 의 흐름: 1. session.say("지금 통화를 종료하겠습니다.") 2. wait_for_playout() 3. session.history.add_message(...) 4. self.hangup() → delete_room() ← 여기서 룸이 즉시 삭제됨 hangup() (base_agent.py:76-84)이 delete_room API를 호출하면, LiveKit 서버가 즉시 룸을 삭제하고 모든 참가자(agent, egress, SIP)를 킥합니다. 이때 에이전트 프로세스에서는: - close_on_disconnect=True 때문에 세션이 닫혀야 하고 - _on_close 핸들러 → stop_recording 실행 - shutdown_callback → RabbitMQ 메시지 전송 이 모든 것이 비동기적으로 발생해야 하지만, delete_room 호출 직후 에이전트 프로세스의 룸 연결이 끊기면서 job이 종료 처리됩니다. AgentServer의 프로세스 모델(num_idle_processes=3)에서 job 프로세스가 이 비동기 콜백들을 기다리지 않고 종료될 수 있습니다. 증거: delete_room 이후 outbound-agent의 SESSION_CLOSE, SHUTDOWN_CALLBACK, STOP_RECORDING, SEND_MQ_MESSAGE 로그가 단 하나도 없습니다. --- 이것이 파드 스케일 다운 문제가 아닌 이유 K8s graceful shutdown 설정은 문제없습니다. 이 문제는 모든 통화에서 발생할 수 있는 코드 레벨 race condition입니다. end_call_by_system, end_call_by_user_request, transfer_call (실패 시), user_presence_task 등 delete_room을 호출하는 모든 경로에서 동일한 문제가 발생합니다. --- 수정 방안 핵심 원칙: delete_room 호출 전에 모든 후처리를 완료해야 합니다.
t
Thanks for reaching out, Dan(송희웅). I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
감사합니다 — 분석과 로그 정리 매우 명확합니다. 요약하면 요청하신 부분(즉, delete_room 호출 때문에 shutdown_callback가 실행되지 않는 race)이 정확하며, 제안하신 해결 방향(모든 후처리 완료 후 delete_room 호출)이 타당합니다. 아래에 핵심 요점, 근거, 그리고 적용할 수 있는 실전 수정안(짧은 코드 예시 포함)을 정리했습니다. 간단 요약 — 우리가 답하고 있는 부분 • 문제: delete_room 호출이 agent의 연결을 즉시 끊어 shutdown_callback(녹음 중지, MQ 전송 등)이 실행되기 전에 프로세스가 종료됨. • 결론: 분석이 정확함. delete_room이 shutdown 과정보다 먼저 호출되어 비동기 후처리가 실패함. • 권장: delete_room을 호출하기 전에 session.shutdown/session.aclose 등으로 모든 후처리를 완료하도록 순서를 바꿔야 함. 왜 분석이 맞는지 (핵심 근거) • 로그 시퀀스: agent가 “지금 통화를 종료하겠습니다.”를 말한 직후 delete_room 호출 → 즉시 participant 강제 종료 → room closed → 그 이후에 agent의 SESSION_CLOSE / STOP_RECORDING / SHUTDOWN_CALLBACK 로그가 전혀 없음. • 동작 특성: delete_room 호출 시 LiveKit 서버는 참가자 연결을 종료(kick)하므로 agent 프로세스의 WebSocket이 즉시 끊기고, 그로 인해 프로세스 내에서 예약된 비동기 콜백들이 실행되기 전에 프로세스가 종료될 수 있음. • 환경 무관: K8s graceful shutdown 설정과는 별개로, 코드 레벨의 race condition이며 delete_room을 호출하는 모든 경로(end_call_by_system, end_call_by_user_request, transfer_call 실패, user_presence_task 등)에서 동일하게 발생함. 권장 수정 원칙 (핵심) • 핵심 원칙: delete_room 호출 전에 모든 후처리를 완료(녹음 중지, MQ 전송 등)해야 함. • 안전한 순서: 1) 세션의 안전한 종료/정리(예: session.shutdown() 또는 session.aclose()) → 2) shutdown_callback에서 필요한 작업(녹음 중지, MQ 전송) 완료 확인 → 3) 그 다음에 delete_room 호출. 권장 구현 예시 1) 명시적으로 정리 후 delete_room 호출
Copy code
# 1. 말하기/플레이아웃 대기 완료 후
await session.wait_for_playout()

# 2. 세션 안전 종료(모든 shutdown callbacks 실행 보장)
await session.aclose()  # 또는 await session.shutdown(drain=True)

# 3. 후처리(녹음 중지, MQ 전송)가 shutdown_callback에서 실행되도록 보장
# 4. 모든 정리 완료 후 방 삭제
await api_client.room.delete_room(room_id)
2) 또는 delete_room_on_close 옵션 사용 • 세션을 생성할 때 `delete_room_on_close=True`로 설정하면
session.aclose()
호출 시 세션 정리 후 자동으로 방이 삭제되도록 할 수 있습니다. 단, 이 역시 반드시 세션 종료가 먼저 호출되는 흐름이어야 합니다. 주의 / 고려사항 • shutdown_callback 내부에서 시간 소요가 큰 작업이 있다면(예: 외부 네트워크 호출), 타임아웃을 적절히 설정하거나 작업 완료를 보장할 수 있는 재시도/확인 메커니즘을 두세요. • AgentServer의 프로세스 모델(예: job 프로세스가 즉시 종료되는 동작)을 고려해, session.aclose()가 완료될 때까지 프로세스가 종료되지 않도록 보장해야 합니다. 필요하면 세션 종료가 완료될 때까지 블로킹하거나, 별도의 supervisor/worker 형태로 후처리를 위임할 수 있습니다. • delete_room 호출 경로가 여러 곳에 흩어져 있다면(말씀하신 여러 종료 경로) 모든 경로에서 동일한 안전 패턴을 적용해야 합니다. 다음 제안(원하시면 도와드릴 수 있는 부분) • 현재 코드의 실제 종료 경로(end_call_by_system 등) 중 하나를 골라 제가 구체적으로 변경할 코드 스니펫을 제공해 드리겠습니다. • shutdown_callback에서 수행하는 작업(녹음 중지, MQ 전송 등)의 실행 시간을 측정해 타임아웃/재시도 전략을 설계해 드릴 수 있습니다. • delete_room 호출을 중앙화해서 실수로 잘못된 순서로 호출되는 것을 방지하는 리팩토링 제안도 도와드릴 수 있습니다. 원하시면, 특정 파일(예: base_agent.py의 해당 함수들)이나 한 경로를 골라 그 부분을 함께 수정한 코드 예시와 테스트 케이스(로컬에서 재현하는 방법 포함)를 제공하겠습니다. 어떤 경로부터 시작할까요? Sources: Job lifecycle | LiveKit Documentation | Server-side | livekit/livekit_composite | DeepWiki | Agent session | LiveKit Documentation | Why is there extra audio at the end of my recordings of an agent session? |... | EndCallTool | LiveKit Documentation
l
Copy code
session.shutdown(drain=True)
으로 통화가 바로 종료되지 않는데? 왜 그런거야?
t
좋은 질문입니다 — 혼란스러운 부분을 정확히 짚으셨습니다. 질문하신 것은 바로 이 응답에서 다룹니다: 왜 `session.shutdown(drain=True)`로는 통화가 바로 종료되지 않는지, 그리고 즉시 종료가 필요할 때 무엇을 써야 하는지 설명드릴게요. 핵심 요지 • `session.shutdown(drain=True)`는 그레이스풀(graceful) 종료용입니다. 즉, 종료 요청을 하되 "이미 재생 중인 음성(플레이아웃)"이 완전히 끝날 때까지 기다려 자연스럽게 통화를 마치게 합니다. • 그래서 호출 직후에 통화가 즉시 끊기지 않고, 남은 음성 재생이 끝난 뒤에야 실제 종료 절차(콜백 실행 등)가 진행됩니다. 동작 원리(짧게) • drain=True → 에이전트가 말하고 있는 오디오(또는 큐에 있는 TTS)가 재생 완료될 때까지 대기. •
session.shutdown()
자체는 비동기적으로 종료 시퀀스를 시작하지만, 백그라운드에서 재생 종료를 기다립니다. 따라서 "즉시 끊음"을 기대하면 지연이 발생합니다. 즉시 종료가 필요하면 • 즉시 연결을 끊고 싶다면 `await session.aclose()`를 사용하세요. ◦ `aclose()`는 현재 재생 중인 음성도 취소하고 즉시 I/O 연결을 닫아 통화를 강제로 종료합니다. ◦ `await`로 호출하므로 종료가 완료될 때까지 호출 측에서 대기합니다. 간단 예시 • 우아한 종료(플레이아웃 대기):
Copy code
session.shutdown(drain=True)
• 즉시 종료(강제 연결 종료):
Copy code
await session.aclose()
추가 팁 • 만약 "플레이아웃 종료를 기다리되, 최대 제한 시간까지만 대기" 같은 동작이 필요하면: ◦ 현재 재생 핸들(
SpeechHandle
)의 `wait_for_playout()`을 직접 사용해 재생 완료를 기다리거나, ◦ 타임아웃 로직을 추가한 뒤 타임아웃 시 `aclose()`로 강제 종료하는 패턴을 적용할 수 있습니다. • shutdown_callback(녹음 중지, MQ 전송 등)을 항상 보장하려면: ◦ 우선
session.aclose()
또는 `session.shutdown(drain=True)`로 세션 정리를 완료(또는 대기)한 뒤 `delete_room()`을 호출하세요. 원하시면 • 현재 사용 중인 종료 흐름(코드 스니펫)을 보여주시면, 즉시 종료 혹은 안전 종료를 만족하도록 구체적인 수정 예시를 드리겠습니다. • 또는 "플레이아웃을 최대 N초만 기다리고 그 이후엔 강제로 종료" 같은 정책을 적용하고 싶으시면 그에 맞는 코드 패턴을 만들어 드릴게요. 원하시는 방향(즉시 종료 vs. 플레이아웃 우선)을 알려주시면 다음 단계로 구체적인 코드 변경안을 드리겠습니다. Sources: Job lifecycle | LiveKit Documentation | How to detect when an agent has finished speaking | LiveKit
l
delete_room_on_close=True
설정하고 self.session.shutdown(drain=True) 로 하니깐 통화가 종료되는데, 이렇게 하면 위에서 언급한 문제는 없는거지?
t
네 — 잘 하셨습니다. 그 설정과 호출 순서는 앞서 지적한 레이스 조건을 제거하고 안전한 종료를 보장합니다. 이유를 간단히 정리하면 다음과 같습니다. • `self.session.shutdown(drain=True)`는 대기 중인 음성(플ayout)을 끝까지 재생하며 세션의 shutdown 흐름(Shutdown 콜백 포함)을 실행하도록 보장합니다. • `delete_room_on_close=True`를 세션에 설정하면, 세션이 완전히 닫힌(모든 후처리와 콜백이 완료된) 이후에 방 삭제가 자동으로 수행됩니다. • 따라서 수동으로 일찍 `delete_room()`을 호출해 참가자 연결을 강제로 끊어버리는 상황이 사라지고, 녹음 중지나 MQ 전송 같은 후처리가 누락될 위험이 없어집니다. 주의할 점(운영상 권장) • shutdown 콜백에서 시간이 오래 걸리는 외부 호출이 있다면 타임아웃 또는 재시도 전략을 두어 콜백이 안전히 완료되도록 하세요. • 여러 경로에서 방 삭제를 트리거하는 코드가 남아있지 않은지(중복 호출 가능성)를 확인해 중앙화된 종료 패턴을 유지하세요. 원하시면 • 현재 사용 중인 세션 생성 코드(혹은 종료 흐름)를 보여주시면,
delete_room_on_close=True
적용 예시와 shutdown 콜백 타임아웃 패턴을 포함한 구체적 코드 수정안을 같이 드리겠습니다. Sources: Agent session | LiveKit Documentation
l
그럼 다시한번 확실히 할게. 문제: delete_room이 shutdown_callback보다 먼저 실행됨 이 문제는 사라지는게 맞아?
t
네 — 확실합니다. • 결론:
delete_room_on_close=True
+
session.shutdown(drain=True)
조합은 `delete_room`이 `shutdown_callback`보다 먼저 실행되는 문제를 제거합니다.
간단한 이유: • `session.shutdown(drain=True)`는 플레이아웃을 끝까지 재생하고 세션의 shutdown 흐름(등록된 shutdown callbacks 포함)을 실행하도록 보장합니다. • `delete_room_on_close=True`는 세션이 완전히 닫힌 뒤(모든 shutdown callback이 완료된 뒤)에 방 삭제를 자동으로 수행합니다. • 따라서 수동으로 중간에 `delete_room()`을 호출해 연결을 즉시 끊어버리는 상황이 없어지고, 녹음 중지·MQ 전송 같은 후처리가 누락될 위험이 사라집니다. 운영상 유의사항(간단히) • shutdown callbacks 내부에 오래 걸리는 외부 작업이 있으면 타임아웃/재시도 전략을 두세요. • 코드 베이스에 아직 수동
delete_room()
호출(다른 경로)이 남아있지 않은지 확인해 중복 삭제/레ース를 방지하세요. 원하시면 현재 종료 흐름(코드 스니펫)을 보여주시면, 안전성 점검해 드리고 shutdown callback 타임아웃 패턴이나 중앙화된 종료 처리 예시도 함께 제안해 드리겠습니다.
l
다음은 사용자가 전화를 끊었을때 shut down callback이 실행되지 않은 케이스인데, 아주 가끔씩 이런 현상이 일어나는데, 원인이 무엇일까? 통화 시간: 약 26초 (112539 ~ 112605) --- 핵심 발견: 이전 케이스와 다른 시나리오 이번은 delete_room이 아닌 사용자가 전화를 끊은 케이스 (SIP BYE)입니다. 타임라인 112603 ROUTE_TO_BOOKING → BookingAgent 진입 112604 User: "예약 다시 잡아줄게요" 112605 ⚡ SIP: "BYE from remote" (사용자가 전화 끊음) 112605 SIP: "BYE received, closing call immediately" 112605 SIP: call status → "hangup" 112605 LK: participant closing (reason: CLIENT_REQUEST_LEAVE) 112605 AG: "Participant disconnected" (agent가 인지) 112605 AG: "closing agent session due to participant disconnect" ← 프레임워크가 세션 종료 시작 112606 AG: "Selecting available schedules" ← 🔴 BookingAgent가 API 호출 중! 112609 AG: "진료 예약만 가능하네요..." ← 🔴 아무도 없는데 계속 말함 이후: 로그 없음 (SESSION_CLOSE, SHUTDOWN_CALLBACK, SEND_MQ_MESSAGE 모두 없음) 문제 분석 1. 세션 종료 시그널은 발생했으나, 콜백이 실행되지 않음 프레임워크가 112605에 "closing agent session due to participant disconnect"를 로깅했습니다. 즉 close_on_disconnect=True가 정상 작동합니다. 하지만 실제 close 처리가 완료되기 전에 BookingAgent의 tool 실행(예약 API 호출)이 진행 중이어서, 세션이 즉시 닫히지 않습니다. 2. Agent가 4초 동안 유령 상태로 동작 사용자가 끊은 후(112605)에도 agent가 API 호출(112606)과 응답 생성(112609)을 계속합니다. 상대방이 없는데도 TTS까지 생성됩니다. 3. 마지막 로그 이후 아무것도 없음 112609 이후 SESSION_CLOSE, SHUTDOWN_CALLBACK, STOP_RECORDING, SEND_MQ_MESSAGE 로그가 없습니다. 이전 케이스와 동일한 패턴입니다. --- 두 케이스 비교 ┌──────────────────────────┬──────────────────────────────┬──────────────────────────────────────┐ │ │ 케이스 1 (이전) │ 케이스 2 (이번) │ ├──────────────────────────┼──────────────────────────────┼──────────────────────────────────────┤ │ 종료 트리거 │ Agent가 delete_room 호출 │ 사용자가 SIP BYE (전화 끊음) │ ├──────────────────────────┼──────────────────────────────┼──────────────────────────────────────┤ │ close_on_disconnect 발동 │ 간접적 (room 삭제로 인한 킥) │ 직접적 ("closing session" 로그 확인) │ ├──────────────────────────┼──────────────────────────────┼──────────────────────────────────────┤ │ 종료 후 agent 동작 │ 로그 즉시 끊김 │ 4초 더 동작 (API 호출 + 응답 생성) │ ├──────────────────────────┼──────────────────────────────┼──────────────────────────────────────┤ │ SESSION_CLOSE 로그 │ ❌ 없음 │ ❌ 없음 │ ├──────────────────────────┼──────────────────────────────┼──────────────────────────────────────┤ │ SHUTDOWN_CALLBACK 로그 │ ❌ 없음 │ ❌ 없음 │ ├──────────────────────────┼──────────────────────────────┼──────────────────────────────────────┤ │ MQ 메시지 전송 │ ❌ 없음 │ ❌ 없음 │ └──────────────────────────┴──────────────────────────────┴──────────────────────────────────────┘ → 결론: delete_room이 아닌 정상적인 사용자 종료에서도 동일한 문제가 발생합니다. 이것은 delete_room race condition만의 문제가 아니라, LiveKit agents framework에서 shutdown_callback이 안정적으로 실행되지 않는 근본적인 문제입니다. _on_close 핸들러와 shutdown_callback이 실행되는 타이밍에 프로세스가 이미 종료되거나, 이벤트 루프가 콜백을 처리하지 못하는 것으로 보입니다. 수정 방향을 재조정해야 합니다: shutdown_callback에만 의존하지 말고, 통화 종료 시점(사용자 끊기, 시스템 종료 등)에서 즉시 동기적으로 후처리를 실행하는 구조로 바꿔야 합니다.
t
감사합니다—상세한 타임라인과 분석 매우 유용했습니다. 상황을 정리하고, 왜 이런 간헐적 미실행이 발생하는지, 그리고 실전에서 재현/완화할 수 있는 구체적 원인·대응책을 바로 제안드리겠습니다. 간단 결론 • 원인: shutdown_callback에만 의존하는 현재 구조는 race condition에 취약합니다. 사용자 SIP BYE로 세션 종료 신호는 오더가 잡히지만, 에이전트가 동시 작업(예: API 호출, TTS 생성)을 진행하는 동안 이벤트 루프나 프로세스 종료 타이밍 때문에 shutdown_callback이 실행되지 않을 수 있습니다. • 결과: 가끔씩 shutdown_callback, SESSION_CLOSE, STOP_RECORDING, MQ 전송 로그가 남지 않음. • 권장: shutdown_callback만 믿지 말고, 사용자 끊음(또는 시스템 종료) 시점에 즉시(또는 동기적으로) 핵심 후처리를 수행하도록 구조 변경 필요. 왜 이런 현상이 발생하는지(핵심 원인) • Participant disconnect가 발생하면 프레임워크가 "세션 닫음" 시그널을 남깁니다. 하지만 그와 동시에 에이전트는 도구 호출, 외부 API 요청, LLM/TTS 처리 등 비동기 작업을 계속 수행합니다. • shutdown_callback은 이벤트 루프에서 비동기적으로 실행되는데, 다음 중 하나가 발생하면 콜백이 완료되지 못합니다: ◦ 이벤트 루프가 장시간 다른 작업으로 바쁨(예: API 호출, TTS 변환). ◦ 프로세스가 종료되거나 재시작되어 콜백 수행을 중단함. ◦ 콜백 내부 외부 호출이 블로킹 또는 실패하여 타임아웃에 걸림. • 이 때문에 케이스 1(수동 delete_room)과 케이스 2(SIP BYE) 모두에서 콜백 미실행 현상이 드물게 나타납니다. 실전 대응(우선순위 높은 권장 변경) 1) 즉시 실행해야 하는 작업은 shutdown_callback 대신 disconnect 처리 루트에서 동기(또는 await)로 실행 • 녹음 중지, MQ 알림, DB 상태 업데이트 등은 참가자 끊김을 인지하자마자 바로 수행하세요. • 예:
Copy code
@ctx.room.on("disconnected")
     async def on_disconnect():
         # 즉시 녹음 중지
         await stop_recording_safely()
         # MQ 메시지 동기 전송(또는 await 가능한 전송)
         await send_mq_message(payload)
         # 세션 강제 종료 대기
         await session.aclose()
• 이유: 이벤트 루프가 바쁠 때도 이 블록이 실행되면 후처리가 보장됩니다. 2)
session.aclose()
사용 • `session.shutdown(drain=True)`는 플레이아웃을 기다리므로 즉시 종료를 보장하지 않습니다. • `await session.aclose()`는 연결을 닫고 I/O 정리를 기다리므로, 그 이후에 후처리를 하도록 하면 누락을 줄일 수 있습니다. 3) 장기 실행 작업(도구 호출, TTS 등)은 disconnect 상태를 체크하고 중단/무시하도록 설계 • 도구 호출 시작 시 세션 상태를 확인하고, disconnect이면 작업을 취소하거나 반환. • LLM/TTS 처리 전에
if session.closed: abort
체크. • 또는 작업을 cancellable task로 만들고 disconnect 이벤트에서 cancel() 호출. 4) shutdown_callback은 보조(비핵심) 동작으로만 사용 • 로그 전송, 비핵심 메트릭, best-effort 업로드 등에만 사용하세요. • 핵심적인 자원 정리/알림은 위 1번 방식으로 보장. 5) 타임아웃/재시도 정책 적용 • 외부 네트워크 호출은 적절한 타임아웃을 두고 실패 시 재시도/로그를 남겨 상태 불일치 위험을 줄이세요. • 예: MQ 전송 실패 시 로컬 큐에 쌓아서 백그라운드에서 재전송. 디버깅/증거 수집 팁 • disconnect 이벤트 발생 시점, 각 task의 상태(시작/완료/취소) 로그를 상세히 남기세요. •
uploading session report
,
finished uploading
같은 로그가 없으면 종료가 비정상적으로 중단된 것으로 판단. • 재현을 위해 호출 지연(의도적 sleep)나 도구 호출 인위적 지연을 넣어 race window를 늘려 테스트하세요. 권장 코드 패턴(간단 요약) • Disconnect 핸들러에서 핵심 정리 동작을 await로 수행 → 그 후 session.aclose() → (필요 시) delete_room 호출 또는 delete_room_on_close 사용. • 도구 호출마다 session 상태 체크 또는 cancellable tasks 적용. 다음 단계 제안(제가 도와드릴 수 있는 항목) • 현재 코드(base_agent.py의 관련 함수: end_call_by_system, on_disconnect 핸들러, shutdown_callback 등록부 등)를 보여주시면, 구체적으로 어디를 바꿀지 코드 패치 형태로 제안드리겠습니다. • 또는 한 가지 패턴(즉시 동기 정리 + session.aclose() + 도구 취소)을 기준으로 리팩토링 가이드를 만들어 드릴게요. • 재현 가능한 테스트 시나리오(로컬 혹은 스테이징) 설계도 도와드릴 수 있습니다. 원하시면 지금 쓰시는 disconnect 핸들러 또는 BookingAgent의 해당 코드 스니펫을 붙여주세요. 거기서 바로 안전한 즉시 정리 흐름으로 바꿔드리겠습니다. Sources: How to detect and handle silence during calls | LiveKit | Job lifecycle | LiveKit Documentation | Agent session | LiveKit Documentation | Why is my Agent insights data missing? | LiveKit
l
그런데 여기서 유저가 나갔는데, 왜 Agent가 발화를 하지? session이 자동으로 끝나지 않고?
t
좋은 질문입니다 — 혼란스러운 상황을 잘 짚으셨어요. 간단히 요약하고 왜 그런지, 그리고 어떻게 하면 에이전트가 사용자가 나간 즉시 발화를 멈추고 세션이 깔끔하게 종료되게 할지 실전적인 조치들을 설명드릴게요. 핵심 요지 • 사용자가 끊어도 에이전트가 계속 발화하는 이유는 세션이 "즉시" 종료되지 않기 때문입니다. 프레임워크는 disconnect 신호를 받더라도 이미 진행 중인 비동기 작업(도구 호출, LLM 처리, TTS 합성 등)을 중단하지 않고 계속 실행하는 경우가 있습니다. • close_on_disconnect=True는 세션 종료 흐름을 트리거하지만, 그 흐름이 비동기적으로 동작하므로 in-flight 작업이 끝날 때까지 에이전트가 동작할 수 있습니다. 왜 에이전트가 계속 말하는가 (자세한 원인) • disconnect 이벤트는 "세션 닫기 시도"를 시작하지만, 에이전트 내부에서 이미 시작된 작업(예: API 호출, 응답 생성, TTS 생성)은 별도 취소 처리가 없으면 계속 진행됩니다. • TTS 생성·플레이아웃은 별도의 큐/핸들로 관리되며, 이미 큐에 올라간 항목은 세션 닫힘 신호가 와도 재생될 수 있습니다. • 이벤트 루프가 해당 비동기 작업들로 바쁘다면 shutdown callback이나 close 완료 처리가 지연되거나 프로세스가 종료되기 전까지 실행되지 않을 수 있습니다. • 즉, "disconnect 신호는 왔음" 하지만 "현재 작업이 멈추지 않음" → 발화 지속. 실전에서 적용할 수 있는 해결책(우선순위 순) 1) 즉시 취소 로직 추가 (최우선) • 사용자가 나가면(예: SIP BYE 수신, participant disconnected 이벤트) 즉시 현재 진행 중인 도구 호출/LLM/TTS 작업을 취소하세요. • cancellable task로 실행하고 disconnect에서 task.cancel() 호출 또는 상태 플래그로 중단 체크. • 예:
Copy code
if ongoing_task and not ongoing_task.done():
         ongoing_task.cancel()
2) disconnect 핸들러에서 발화 큐 정리 및 강제 종료 • 플레이아웃 큐에 남아있는 항목을 즉시 취소하고, 재생 중인 TTS가 있다면 중지 API 호출. • 이후 `await session.aclose()`로 연결을 즉시 닫아 추가 출력이 발생하지 않게 함. 3) 세션 상태 체크를 도구/응답 생성 흐름에 삽입 • 각 단계에서
if session.closed or session.disconnecting: abort
체크를 넣어 불필요한 작업 생성 자체를 막음. 4)
session.shutdown(drain=True)
대신 상황별
aclose()
사용 • drain=True는 플레이아웃을 끝까지 재생하므로 의도적으로 발화를 종료시키고 싶으면 `await session.aclose()`를 호출해 즉시 연결을 닫으세요. 5) 플레이아웃 핸들 제어 • `SpeechHandle`같은 재생 핸들을 사용하면
handle.cancel()
또는 재생 중단 API로 현재 TTS를 중지할 수 있음. (해당 API가 있다면 바로 호출) 간단한 흐름 예시 • on_participant_disconnect: a. set session.state = "disconnecting" b. cancel ongoing tasks (LLM/tool/TTS) c. stop current playback (playout cancel) d. await session.aclose() e. perform synchronous cleanup (stop_recording, send MQ) 운영상 고려사항 • 취소 처리 시 LLM/tool 호출은 적절히 취소/타임아웃 되도록 설계하세요(외부 API는 즉시 취소가 안될 수 있음). • shutdown_callback은 보조 수단으로만 사용하고, 핵심 후처리는 위 disconnect 경로에서 보장하세요. • 테스트: 의도적으로 disconnect 시점을 도구 호출 중이나 TTS 재생 중으로 맞춰 race를 재현해 취소 로직이 잘 동작하는지 확인하세요. 원하시면 제가 도와드릴 수 있는 것 • 현재 에이전트의 disconnect 핸들러나 TTS/도구 호출 코드 스니펫을 주시면, 취소 로직과 session.aclose() 적용을 포함한 구체적 코드 패치를 제안해 드리겠습니다. • 또는 발화 큐/플레이아웃을 중단하는 구체 API가 무엇인지 알려주시면 그에 맞춘 예시를 만들어 드릴게요. 원하시는 방식(즉시 강제 중단 vs 플레이아웃 허용)을 알려주시면 바로 구체 코드로 도와드리겠습니다.
l
participant disconnected 이벤트 발생시 현재 진행 중인 도구 호출/LLM/TTS 작업을 취소 하려면 어떻게해?
t
아래에 participant disconnected 이벤트 발생 시 진행 중인 도구 호출/LLM/TTS 작업을 안전하게 취소하는 실전 가이드와 코드 예시를 정리했습니다. 목표는 (1) 현재 진행 중인 비동기 작업을 즉시 멈추고, (2) 세션을 깔끔하게 닫고, (3) 필수 후처리를 신뢰성 있게 수행하는 것입니다. 핵심 아이디어(요약) • 각 장기 작업을 asyncio.Task로 실행하고, 해당 태스크들을 한 곳(active_tasks)에서 관리하세요. • disconnected 이벤트에서 모든 active task에 대해 task.cancel()을 호출해 즉시 취소 시도합니다. • 각 작업은 asyncio.CancelledError를 적절히 처리(필요시 정리 후 raise)해야 바로 중단됩니다. • 취소 후에는 `await session.aclose()`로 연결을 확실히 닫고, 중요 후처리(MQ 전송, 녹음 중지)는 disconnect 핸들러에서 await로 수행하세요. 중요 지침 • task.cancel()만 호출하면 내부에서 CancelledError를 처리하지 않는 작업은 멈추지 않습니다. 작업 내부에서 CancelledError를 캡처하고 적절히 처리(자원 해제 등)한 뒤 다시 raise해야 합니다. • 외부 HTTP 호출이나 LLM 라이브러리가 자체적으로 취소를 지원하는지 확인하고, 가능하면 타임아웃/취소 토큰을 사용하세요. • shutdown_callback은 보조 수단으로 사용하고, 핵심 정리는 disconnect 이벤트에서 보장하세요. 구현 예시 (실무용) 1) active task 관리 및 취소 패턴
Copy code
import asyncio
import logging

logger = logging.getLogger("agent")

# 전역 또는 세션 스코프에서 관리되는 set
active_tasks = set()

def create_cancellable_task(coro):
    task = asyncio.create_task(coro)
    active_tasks.add(task)
    # 태스크 정리
    def _on_done(t):
        active_tasks.discard(t)
    task.add_done_callback(_on_done)
    return task
2) 장기 작업(도구 호출, LLM, TTS) 예시 — CancelledError 처리 포함
Copy code
async def long_running_tool_call(...):
    try:
        # 예: 외부 HTTP 호출을 asyncio-friendly하게 수행
        # 여기선 단순 sleep로 시뮬레이트
        for i in range(30):
            await asyncio.sleep(0.5)
            # 중간에 주기적으로 취소 확인(선택적)
            # if session.closed: raise asyncio.CancelledError()
        return "done"
    except asyncio.CancelledError:
        # 필요하면 여기에서 자원 정리(파일/스트림 닫기 등)
        <http://logger.info|logger.info>("Tool call cancelled, cleaning up...")
        # 반드시 예외 재전파하여 태스크가 취소됨을 보장
        raise
3) TTS/플레이아웃 취소(플레이어 핸들 제어 가능하면 사용) • TTS를 생성 후 플레이아웃 핸들을 얻을 수 있다면, 해당 핸들에 cancel/stop API가 있다면 호출하세요. • 예:
Copy code
# 예시: play_handle = await session.say("...") 반환값이 핸들일 경우
play_handle.cancel()  # 또는 await play_handle.stop()
4) participant disconnected 이벤트에서의 취소 및 정리
Copy code
@ctx.room.on("disconnected")
async def on_disconnected():
    <http://logger.info|logger.info>("Participant disconnected — cancelling active tasks")
    # 1) 취소 시도
    for t in list(active_tasks):
        if not t.done():
            t.cancel()

    # 2) 모든 태스크가 정리될 때까지 짧은 대기 (선택적)
    try:
        await asyncio.wait_for(asyncio.gather(*active_tasks, return_exceptions=True), timeout=5.0)
    except asyncio.TimeoutError:
        logger.warning("Some tasks did not finish within timeout after cancel")

    # 3) 즉시 세션 종료(강제)
    try:
        await session.aclose()
    except Exception as e:
        logger.exception("Error during session.aclose(): %s", e)

    # 4) 핵심 후처리(동기/await)
    try:
        await stop_recording_safely()
        await send_mq_message(...)
    except Exception as e:
        logger.exception("Post-disconnect cleanup failed: %s", e)
추가 권장 패턴 • 도구 호출 진입 지점에서 session 상태 확인: ◦
if session.closed: return
또는 취소로 반환. • 장기 작업에 대해 반드시 타임아웃 적용: ◦
asyncio.wait_for(tool_call(), timeout=10)
• 외부 라이브러리가 취소 토큰을 지원하면 그 토큰을 전달하여 조기 종료를 요청. • 테스트: 인위적인 딜레이를 걸어 disconnect와 작업 취소가 제대로 동작하는지 반복적으로 확인하세요. 마무리(요약) • participant disconnected 이벤트에서 task.cancel() + awaited cleanup(세션 강제 종료 + 핵심 후처리)을 적용하면, 에이전트가 사용자가 떠난 뒤에도 계속 발화하거나 shutdown_callback이 누락되는 문제를 크게 줄일 수 있습니다. • 코드 스니펫을 실제 base_agent.py 또는 BookingAgent의 관련 메서드에 맞춰서 적용해 드릴 수 있습니다. 원하시면 현재 사용 중인 코드 일부(도구 호출 부분, TTS 호출 부분, disconnect 핸들러)를 붙여 주세요. 그걸 바탕으로 구체적 패치 제안을 드리겠습니다. Sources: Job lifecycle | LiveKit Documentation | Turns overview | LiveKit Documentation