Is it a bad idea to call send/send_nowait ourselve...
# ask-ai
t
Is it a bad idea to call send/send_nowait ourselves when implementing our custom llm adapter? when would you use send vs. send_nowait? from future import annotations import asyncio import contextlib from collections import deque from collections.abc import AsyncIterator from typing import Generic, Protocol, TypeVar T = TypeVar("T") T_co = TypeVar("T_co", covariant=True) T_contra = TypeVar("T_contra", contravariant=True) # Based on asyncio.Queue, see https://github.com/python/cpython/blob/main/Lib/asyncio/queues.py class ChanClosed(Exception): pass class ChanFull(Exception): pass class ChanEmpty(Exception): pass class ChanSender(Protocol[T_contra]): async def send(self, value: T_contra) -> None: ... def send_nowait(self, value: T_contra) -> None: ... def close(self) -> None: ... class ChanReceiver(Protocol[T_co]): async def recv(self) -> T_co: ... def recv_nowait(self) -> T_co: ... def close(self) -> None: ... def __aiter__(self) -> AsyncIterator[T_co]: ... async def __anext__(self) -> T_co: ... class Chan(Generic[T]): def __init__( self, maxsize: int = 0, loop: asyncio.AbstractEventLoop | None = None, ) -> None: self._loop = loop or asyncio.get_event_loop() self._maxsize = max(maxsize, 0) # self._finished_ev = asyncio.Event() self._close_ev = asyncio.Event() self._closed = False self._gets: deque[asyncio.Future[T | None]] = deque() self._puts: deque[asyncio.Future[T | None]] = deque() self._queue: deque[T] = deque() def _wakeup_next(self, waiters: deque[asyncio.Future[T | None]]) -> None: while waiters: waiter = waiters.popleft() if not waiter.done(): waiter.set_result(None) break async def send(self, value: T) -> None: while self.full() and not self._close_ev.is_set(): p = self._loop.create_future() self._puts.append(p) try: await p except ChanClosed: raise except: p.cancel() with contextlib.suppress(ValueError): self._puts.remove(p) if not self.full() and not p.cancelled(): self._wakeup_next(self._puts) raise self.send_nowait(value) def send_nowait(self, value: T) -> None: if self.full(): raise ChanFull if self._close_ev.is_set(): raise ChanClosed self._queue.append(value) self._wakeup_next(self._gets) async def recv(self) -> T: while self.empty() and not self._close_ev.is_set(): g = self._loop.create_future() self._gets.append(g) try: await g except ChanClosed: raise except Exception: g.cancel() with contextlib.suppress(ValueError): self._gets.remove(g) if not self.empty() and not g.cancelled(): self._wakeup_next(self._gets) raise return self.recv_nowait() def recv_nowait(self) -> T: if self.empty(): if self._close_ev.is_set(): raise ChanClosed else: raise ChanEmpty item = self._queue.popleft() # if self.empty() and self._close_ev.is_set(): # self._finished_ev.set() self._wakeup_next(self._puts) return item def close(self) -> None: self._closed = True self._close_ev.set() for putter in self._puts: if not putter.cancelled(): putter.set_exception(ChanClosed()) while len(self._gets) > self.qsize(): getter = self._gets.pop() if not getter.cancelled(): getter.set_exception(ChanClosed()) while self._gets: self._wakeup_next(self._gets) # if self.empty(): # self._finished_ev.set() @property def closed(self) -> bool: return self._closed # async def join(self) -> None: # await self._finished_ev.wait() def qsize(self) -> int: """the number of elements queued (unread) in the channel buffer""" return len(self._queue) def full(self) -> bool: if self._maxsize <= 0: return False else: return self.qsize() >= self._maxsize def empty(self) -> bool: return not self._queue def __aiter__(self) -> AsyncIterator[T]: return self async def __anext__(self) -> T: try: return await self.recv() except ChanClosed: raise StopAsyncIteration from None
t
Thanks for reaching out, Tim. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
When you're working on implementing a custom LLM adapter, deciding between
send
and
send_nowait
really comes down to what fits your needs best: • Consider
send_nowait
if you want non-blocking, immediate sending. This works wonderfully for streaming partial results (like chat chunks) or when you're in thread-safe situations where you can’t wait. Just a heads-up, it will raise
ChanFull
if the buffer is full or
ChanClosed
if the channel is closed. • Go with
send
if you prefer to wait for buffer space to make sure your message gets through. It blocks until space opens up, which is great for scenarios where you want to control the flow and backpressure is okay. For most real-time uses, especially streaming events in LLM adapters,
send_nowait
tends to be the best choice to keep things quick and responsive. Just remember to wrap it in exception handling so you can smoothly manage
ChanClosed
situations. Sources: livekit-plugins/livekit-plugins-azure/livekit/plugins/azure/stt.py
t
In practice, for a voice use case, what would be the UX like using send and potential advantages/disadvantages,
t
I'd love to better understand the specific voice use case you're thinking about. For instance, are you looking at real-time transcription, voice commands, or perhaps something else? Knowing this will really help us explore the UX implications of using
send
versus
send_nowait
together. I'm here to help you through this!
t
voice chat
t
When it comes to voice chat, choosing between
send
and
send_nowait
really shapes the user experience, so let’s walk through it together: • Using
send_nowait
: This approach is fantastic for keeping things quick and natural. It sends data right away—like audio frames or text chunks—without waiting, which helps conversations flow smoothly and feel real-time. That said, it can sometimes lead to message loss if there’s congestion, which might cause a few phrases to be missed or responses to feel incomplete. • Using
send
: This one waits until there’s room to send the message, so it’s reliable in making sure nothing gets lost. But, it can slow things down, making the chat feel a bit less responsive and natural. For voice chat, most folks lean towards
send_nowait
because keeping the conversation lively is key. To keep things running smoothly, you can add some smart safeguards like retrying sends, monitoring sessions, and having fallback plans—these help handle any hiccups gracefully and keep your users happy. Sources: examples/other/translation/multi-user-translator.py | An open source stack for real-time multimodal AI
t
OK but what are meaningufl implications of send_nowait? Does it mean some parts of the streamed chat chunks may not be sent in order?
t
I'm glad you asked about
send_nowait
! Rest assured, using it won't cause streamed chat chunks to be sent out of order. The system is designed to keep everything in the right sequence, so all chunks are processed exactly in the order they're sent. This means that even partial content, like text deltas or tool calls, flows smoothly and coherently, making sure your experience stays seamless. Sources: livekit-plugins/livekit-plugins-aws/livekit/plugins/aws/experimental/realti... | Sending files &amp; bytes | LiveKit Docs