Selam, """End call function tool.""" from liveki...
# ask-ai
a
Selam, """End call function tool.""" from livekit.agents import RunContext, get_job_context from livekit.agents.llm import function_tool from ..utils.logger import get_logger from ..utils.sip import hangup from ..utils.speech_output import speak_message logger = get_logger("end_call") @function_tool( name="end_call", description=( "Telefon görüşmesini kapatır. " "Yalnızca kullanıcı açıkça kapatmak istediğinde çağır. " "Bu aracı çağırmadan önce veda cümlesi kurma. " "Söylenecek tek cümleyi sadece 'closing_message' parametresine yaz. " "Araç, aktif konuşmayı keser ve bu cümleyi sesli oynatır; " "kullanıcı bu sırada araya girerse kapatmayı iptal eder." ), ) async def end_call( context: RunContext, closing_message: str = "Görüşme için teşekkür ederim, iyi günler.", ) -> str | None: """Telefon görüşmesini kapat. Akış: 1) Mevcut agent konuşması varsa keser. 2)
closing_message
cümlesini sesli oynatır. 3) Kullanıcı gerçekten araya girerse kapatmayı iptal eder. 4) Aksi halde çağrıyı sonlandırır. """ ctx = get_job_context() session = context.session logger.info(f"Ending call started - room: {ctx.room.name}") # Tool'a girmeden önce aktif agent konuşmasını kes ve playback state'ini stabilize et. try: session.interrupt() logger.info(f"Pre-tool speech interrupted - room: {ctx.room.name}") except Exception as e: logger.warning(f"Pre-tool speech interrupt failed: {e}") try: await context.wait_for_playout() logger.info( f"Pre-tool wait_for_playout completed - room: {ctx.room.name}" ) except Exception as wait_err: logger.warning( f"Pre-tool wait_for_playout failed: {wait_err}" ) interrupted = await speak_message( session, closing_message.strip() or "Görüşme için teşekkür ederim, iyi günler.", wait_for_playout=True, allow_interruptions=True, ) if interrupted: logger.info( f"Kullanıcı araya girdi, kapatmaktan vazgeçiliyor - room: {ctx.room.name}" ) return None logger.info(f"Closing message played, ending call now - room: {ctx.room.name}") # Mark call as agent-ended for reporting. current_agent = getattr(session, "current_agent", None) if current_agent: session_report = getattr(current_agent, "_session_report_service", None) if session_report: session_report.set_ended_by("agent") await hangup(ctx, reason="agent_ended") {"@timestamp": "2026-03-05T145329.454Z", "log.level": "INFO", "message": "Ending call started - room: 69a998c3a3d90d6fa858474f", "service.name": "voice-kit-agent", "event.dataset": "end_call", "logger.name": "end_call", "call_id": "69a998c3a3d90d6fa858474f", "room_name": "69a998c3a3d90d6fa858474f", "job_id": "AJ_Aw66XHu76W4E"} {"@timestamp": "2026-03-05T145329.455Z", "log.level": "INFO", "message": "Pre-tool speech interrupted - room: 69a998c3a3d90d6fa858474f", "service.name": "voice-kit-agent", "event.dataset": "end_call", "logger.name": "end_call", "call_id": "69a998c3a3d90d6fa858474f", "room_name": "69a998c3a3d90d6fa858474f", "job_id": "AJ_Aw66XHu76W4E"} {"@timestamp": "2026-03-05T145329.490Z", "log.level": "INFO", "message": "Pre-tool wait_for_playout completed - room: 69a998c3a3d90d6fa858474f", "service.name": "voice-kit-agent", "event.dataset": "end_call", "logger.name": "end_call", "call_id": "69a998c3a3d90d6fa858474f", "room_name": "69a998c3a3d90d6fa858474f", "job_id": "AJ_Aw66XHu76W4E"} {"message": "tool_choice is not supported by the Google Realtime API.", "level": "WARNING", "name": "livekit.plugins.google", "pid": 118, "job_id": "AJ_Aw66XHu76W4E", "room_id": "RM_nEnAotDyx6k4", "timestamp": "2026-03-05T145329.500204+00:00"} {"message": "speech not done in time after interruption, cancelling the speech arbitrarily.", "level": "ERROR", "name": "livekit.agents", "speech_id": "speech_094f921d8821", "timeout": 5.0, "pid": 118, "job_id": "AJ_Aw66XHu76W4E", "room_id": "RM_nEnAotDyx6k4", "timestamp": "2026-03-05T145334.455719+00:00"} {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gemini-2.5-flash-native-audio-preview-12-2025", "model_provider": "Gemini", "ttft": 0.3, "input_tokens": 7434, "cached_input_tokens": 0, "input_text_tokens": 7406, "input_cached_text_tokens": 0, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 28, "input_cached_audio_tokens": 0, "output_tokens": 234, "output_text_tokens": 0, "output_audio_tokens": 234, "output_image_tokens": 0, "total_tokens": 7668, "tokens_per_second": 40.16, "pid": 118, "job_id": "AJ_Aw66XHu76W4E", "room_id": "RM_nEnAotDyx6k4", "timestamp": "2026-03-05T145335.929654+00:00"} {"message": "tool_choice is not supported by the Google Realtime API.", "level": "WARNING", "name": "livekit.plugins.google", "pid": 118, "job_id": "AJ_Aw66XHu76W4E", "room_id": "RM_nEnAotDyx6k4", "timestamp": "2026-03-05T145335.940921+00:00"} {"@timestamp": "2026-03-05T145335.952Z", "log.level": "INFO", "message": "Closing message played, ending call now - room: 69a998c3a3d90d6fa858474f", "service.name": "voice-kit-agent", "event.dataset": "end_call", "logger.name": "end_call", "call_id": "69a998c3a3d90d6fa858474f", "room_name": "69a998c3a3d90d6fa858474f", "job_id": "AJ_Aw66XHu76W4E"} {"@timestamp": "2026-03-05T145335.990Z", "log.level": "DEBUG", "message": "Bağlantı kesildi: 4 (PARTICIPANT_REMOVED)", "service.name": "voice-kit-agent", "event.dataset": "agent-factory", "logger.name": "agent-factory", "room_name": "69a998c3a3d90d6fa858474f", "job_id": "AJ_Aw66XHu76W4E"} {"@timestamp": "2026-03-05T145335.990Z", "log.level": "DEBUG", "message": " Çağrı sonlandı: agent (sebep: PARTICIPANT_REMOVED, sonuç: completed)", "service.name": "voice-kit-agent", "event.dataset": "agent-factory", "logger.name": "agent-factory", "room_name": "69a998c3a3d90d6fa858474f", "job_id": "AJ_Aw66XHu76W4E"} {"message": "speech not done in time after interruption, cancelling the speech arbitrarily.", "level": "ERROR", "name": "livekit.agents", "speech_id": "speech_094f921d8821", "timeout": 5.0, "pid": 118, "job_id": "AJ_Aw66XHu76W4E", "room_id": "RM_nEnAotDyx6k4", "timestamp": "2026-03-05T145334.455719+00:00"} bu hatayı alıyorum. Bunu nasıl çözebilirim?
t
Thanks for reaching out, İbrahim. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Selam — teşekkürler, paylaştığın kod ve loglar çok yardımcı oldu. Sorunun ne olduğuna ve nasıl düzeltebileceğine dair kısa ve uygulanabilir bir yanıt hazırladım. Özet olarak ne oluyor • Hata "speech not done in time after interruption, cancelling the speech arbitrarily." — bu, konuşma (TTS/playout) bir kesintiden sonra 5 saniye içinde tamamlanmadığı için framework tarafından iptal edildiğini gösteriyor. • Loglarda ayrıca Google Realtime model kullanıldığı görünüyor (gemini-2.5-flash-native-audio-preview…). Realtime modellerde playout ve kesinti işleyişi sunucu tarafı davranışları nedeniyle beklenmedik gecikmeler veya eksik tamamlanma sinyalleri verebilir. • Mevcut kodunda
speak_message(..., wait_for_playout=True, allow_interruptions=True)
çağrısı sonrası bekleme yapılıyor; ancak kesinti durumunda bu bekleme takılabiliyor ve 5s zaman aşımı tetikleniyor. Hızlı çözüm önerileri (en etkili olanlar önde) 1) wait_for_playout True kullanmayı bırak (engelleyici beklemeyi kaldır) • Konuşmayı başlat ama bloklama yapma; sonrasında kısa bir süre sonra
handle.interrupted
kontrol et. • Örnek akış: ◦ Başlat: handle = speak_message(..., wait_for_playout=False, allow_interruptions=True) ◦ Kısa bekleme: await asyncio.sleep(0.5) ◦ Eğer handle.interrupted ise iptal et; değilse hangup işlemini sürdür. • Neden: Realtime modellerde güvenilir playout bitiş sinyali alınamayabilir; bloklamayı kaldırmak zaman aşımı hatasını önler. 2) Eğer mutlaka beklemen gerekiyorsa: kendi timeout’ını arttır ve asyncio.wait_for ile yakala • Örnek: a. handle = speak_message(..., wait_for_playout=False, allow_interruptions=True) b. try: await asyncio.wait_for(handle, timeout=10.0) except asyncio.TimeoutError: logger.warning("Playback timed out"); assume interrupted or proceed safely • Neden: Framework’in 5s varsayılanı yetersiz kalıyorsa, kontrollü ve logged bir şekilde daha uzun bekleyebilirsin. 3) Kritik kapanış konuşmasında kesintileri geçici olarak devre dışı bırak • Eğer kapanış cümlesinin tamamen okunması çok önemliyse, kısa süreliğine kesintileri kapat: ◦ context.disallow_interruptions() / context.allow_interruptions() (kodunda böyle bir API varsa) ◦ Ancak: Kullanıcının gerçekten aramayı bitirmesini isteyeceksen UX açısından dikkatli ol — zorlayıcı olabilir. 4) speak_message’ın döndürdüğü handle’ın `interrupted`/durum alanını kullan • speak_message’ın bir SpeechHandle döndürdüğünden emin ol; sonra onun
.interrupted
veya
.done
benzeri alanından gerçek kullanıcı kesmesi mi yoksa framework timeout’u mu olduğunu ayırt et. Kod örneği (özet, uyarlayabilirsin):
Copy code
handle = speak_message(
    session,
    closing_message.strip() or "Görüşme için teşekkür ederim, iyi günler.",
    wait_for_playout=False,
    allow_interruptions=True,
)

# kısa bir grace period
await asyncio.sleep(0.5)
if getattr(handle, "interrupted", False):
    <http://logger.info|logger.info>("Kullanıcı araya girdi, kapatma iptal ediliyor")
    return None

# devam edip kapat
await hangup(ctx, reason="agent_ended")
Ek tavsiyeler • speak_message içindeki implementasyonun gerçekten bir handle döndürdüğünden emin ol; eğer speak_message zaten await ediliyorsa
wait_for_playout=False
ile non-blocking versiyonunu kullan. • Loglarda
speech_id
ile birlikte çıktıları izle; böylece gerçek kullanıcı müdahalesi ile framework timeout’unu ayırt edebilirsin. • Realtime model kullanıyorsan, mümkünse kritik kapanışlarda non-realtime (buffered) TTS’ye geçiş veya daha kısa/tek cümleli farewell kullanmak stabiliteyi artırır. Özet • Hata, speak_message sonrası playout tamamlanmadığı için 5s timeout yüzünden çıkıyor. • En iyi pratik:
wait_for_playout=True
yerine non-blocking başlat + kısa bekleme + handle.interrupted kontrolü veya kontrollü, daha uzun timeout kullanmak. • Kapanışı kesin yapmadan önce kullanıcı kesintisini handle üzerinden kontrol etmen yeterli olacaktır. İstersen kodunun ilgili kısmını ben senin için doğrudan düzeltebilirim (tam
speak_message
imzasını ve
SpeechHandle
yapısını paylaşırsan). Hangi yaklaşımı tercih edersin: hemen non-blocking ile devam etmek mi, yoksa timeout’ı yükseltip daha kontrollü beklemek mi? Sources: Realtime models overview | LiveKit Documentation | How to detect when an agent has finished speaking | LiveKit
a
def _is_realtime_session(session: AgentSession) -> bool: """Return True when session runs in realtime pipeline mode.""" agent = getattr(session, "_agent", None) return bool(agent and getattr(agent, "_pipeline_mode", "standard") == "realtime") async def speak_message( session: AgentSession, message: str, *, allow_interruptions: bool = True, add_to_chat_ctx: bool = False, wait_for_playout: bool = False, ) -> bool: """ Speak
message
through the active pipeline. - Standard pipeline: session.say(message, ...) - Realtime pipeline: session.generate_reply(instructions=...) Args: session: AgentSession instance. message: Text to speak. allow_interruptions: Users can interrupt. (Standard pipeline only) add_to_chat_ctx: Add to chat context. (Standard pipeline only) wait_for_playout: Wait for audio to finish playing before returning. Returns: True if the user interrupted, False otherwise. If wait_for_playout=False, False means interruption status is not yet known. """ if _is_realtime_session(session): safe_announce_instructions = ( "Kullanıcıya yalnızca şu cümleyi aynen söyle. " "Ek cümle kurma, açıklama yapma:\n" f"{message}" ) # generate_reply is sync and returns SpeechHandle. handle = session.generate_reply( instructions=safe_announce_instructions, allow_interruptions=allow_interruptions, input_modality="audio", ) else: handle = session.say( message, allow_interruptions=allow_interruptions, add_to_chat_ctx=add_to_chat_ctx, ) if wait_for_playout: await handle.wait_for_playout() return handle.interrupted return False Bunu da gönderiyorum sana.
t
Teşekkürler — ek kodla beraber durumu net gördüm. Paylaştığın
speak_message
ve
_is_realtime_session
fonksiyonları, loglardaki hatanın kaynağını doğruluyor: realtime pipeline (generate_reply) kullanıldığında `wait_for_playout=True
ile
handle.wait_for_playout()` bekleniyor ve kesinti sonrası framework 5s içinde bitiş sinyali alamayınca "speech not done in time..." hatasını atıyor. Ne yapmalısın (özet ve adımlar) • Realtime oturumlarda
wait_for_playout=True
kullanmaktan kaçın. Realtime modelleri playout bitiş sinyallerini güvenilir şekilde sağlamıyor; bu yüzden bloklayıcı bekleme zaman aşımına uğruyor. • Bunun yerine non-blocking başlat + kısa grace period ile
handle.interrupted
kontrol et veya kendi kontrollü timeout mekanizmanı kullan. Önerilen kod değişikliği (uyarlaması kolay, küçük) 1) speak_message çağrısını non-blocking yap: •
wait_for_playout=False
ile çağır. • Döndürülen
handle
üzerinden kısa bir süre sonra
interrupted
kontrol et. Örnek (end_call içindeki ilgili kısma uygula): 1. speak_message çağır:
Copy code
handle = speak_message(
    session,
    closing_message.strip() or "Görüşme için teşekkür ederim, iyi günler.",
    wait_for_playout=False,
    allow_interruptions=True,
)
1. Kısa grace period ve kontrol:
Copy code
await asyncio.sleep(0.5)  # 0.5s gibi kısa bir bekleme
if getattr(handle, "interrupted", False):
    <http://logger.info|logger.info>("Kullanıcı araya girdi, kapatma iptal ediliyor - room: {ctx.room.name}")
    return None
1. Kapatmayı sürdür:
Copy code
# closing message çalınıyor; kullanıcı kesmediyse kapat
await hangup(ctx, reason="agent_ended")
Alternatif: Kontrollü bekleme + genişletilmiş timeout • Eğer tamamen beklemek istiyorsan,
handle.wait_for_playout()
kullanma yerine
asyncio.wait_for(handle, timeout=...)
veya
await asyncio.wait_for(handle.wait_for_playout(), timeout=10.0)
gibi daha uzun, yakalanabilir bir timeout kullan. • Timeout yakalandığında bunu logla ve kapatmaya karar ver ya da iptal et. Böylece framework'ün 5s kısıtına takılmaktan kurtulursun. Neden bu güvenli/iyi: • Realtime pipeline (generate_reply) playout bitişini garantilemediği için bloklayıcı bekleme hata üretir. • Non-blocking yaklaşım hem UX açısından daha kararlı (kapanış mesajı başlar ve kullanıcı müdahalesi varsa yakalanır) hem de framework zaman aşımı loglarını engeller. • Kısa grace period ile gerçek kullanıcı müdahalesini yakalayabilir, spam/ses gecikmesi gibi framework gecikmelerini hataya dönüştürmezsin. Ek doğrulamalar / dikkat etmen gerekenler •
speak_message
çağrısının gerçekten bir
handle
döndürdüğünden emin ol (senin kodda generate_reply/say SpeechHandle dönüyor gibi görünüyor; handle üzerinde
interrupted
alanı mevcut mu kontrol et). • Eğer handle üzerinde
.interrupted
yoksa, speak_message içinden
handle
yerine uygun durum bilgisini sağlayan bir wrapper döndürmeyi düşün. • Kapatma sırasında kritik bir kullanım varsa (mesajın kesin okunması gerektiği durum), kısa süreliğine kesintileri devre dışı bırakma seçeneğini düşün; fakat kullanıcı deneyimini bozabileceğini unutma. İstersen ben şu adımlarda yardımcı olabilirim: • Senin için end_call fonksiyonunu paylaştığın speak_message ile uyumlu olacak şekilde direkt düzeltilmiş hâlini yazayım. • Ya da hangi timeout/await stratejisini tercih edeceğini söyle, ona göre örnek kod gönderiyim. Hangi yaklaşımı tercih edersin: "non-blocking + kısa kontrol" mı yoksa "uzatılmış kontrollü timeout" mu?