Blaze Livekit plugin integration - #5050
Conversation
4653f88 to
77090e4
Compare
|
Hi @tinalenguyen, This PR adds the Blaze provider integration for LiveKit Agents and keeps the implementation scoped under It follows the existing STT/TTS/LLM plugin structure used by other provider integrations. Iβve also addressed the outstanding review comments on the PR. Would appreciate a review when you have time. Thanks! |
|
hi @HoangPN711! thank you for the contribution, could you bump the version and add the plugin to this pyproject file |
|
Hi @tinalenguyen , thanks for reviewing this PR! Updated as requested: Bumped livekit-plugins-blaze to 1.5.9 (aligned with the current livekit-agents release) Please let me know if any further changes are needed. |
2b94037 to
e9052a6
Compare
1cea05c to
2c82c75
Compare
CI ruff format --check failed after the streaming STT changes.
ChunkedStream early-return on blank text left the emitter unstarted, so base end_input() raised RuntimeError. Always initialize (and flush) so empty synthesis completes cleanly.
The transcribe endpoint only accepts ModelVersionEnum v1.0|v2.0. stt-async-1.5 belongs to /v1/stt/execute (async jobs). Keep stt-stream-1.5 as the realtime WebSocket default.
AgentSession stt turn-detection opens the user turn on START_OF_SPEECH. Emit it on the first non-empty partial/final and re-arm after END_OF_SPEECH.
WebSocket audio chunks are not sample-aligned. Carry trailing odd
bytes and only fade/push even-length PCM so cast('h') never raises.
result.get("confidence", 1.0) still yields None when the key is present
with a null value, which crashes %.3f logging. Match the streaming path.
POST /v1/tts/realtime returns 404 on the public gateway β realtime TTS is WebSocket-only. ChunkedStream now follows the same handshake as streaming TTS (auth β speech-start β query β speech-end) so synthesize() works for the plugin tester and any non-streaming callers.
- Point plugin docs at https://blaze.vn - Set authors to LiveKit <hello@livekit.io> - Empty py.typed PEP 561 marker to match sibling plugins - Convert PCMβWAV via rtc.AudioFrame.to_wav_bytes() - Drop custom TTS sentence batching; use framework SentenceTokenizer - Merge all blaze unit tests into tests/test_plugin_blaze.py
BlazeConfig accepts api_token, not auth_token (which is the per-plugin constructor param). Align the package docstring example.
Match llm.py: POST /v1/voicebot-call/{bot_id}/chat-conversion-stream.
Do not early-return when chat context only has system/developer messages. Blaze loads the voicebot prompt server-side; match agents-js by still POSTing messages=[] so generate_reply can speak first.
e042630 to
65a0085
Compare
|
Rebased onto latest Conflict resolutions:
Pushed to |
|
Friendly reminder: this Blaze LiveKit plugin PR is ready for re-review when you have a moment, @tinalenguyen.
Happy to address any further feedback. Thanks! |
|
Hi @HoangPN711, when trying the LLM of the plugin, I kept hitting this error: livekit.agents._exceptions.APIStatusError: message='Chatbot service error 422: {"detail":[{"type":"list_type","loc":["body"],"msg":"Input should be a valid list","input":{"messages":[]}}]}', status_code=422, retryable=False, request_id=267a6876-363d-4f36-b687-e69d2ce11714, body={"detail":[{"type":"list_type","loc":["body"],"msg":"Input should be a valid list","input":{"messages":[]}}]}are you able to reproduce this? |
Blaze /chat-conversion-stream expects a list body, matching agents-js.
Sending {"messages": [...]} caused 422 list_type (Tina's repro with []).
|
Hi @tinalenguyen β yes, that 422 is reproducible. Thanks for catching it. Root cause: Fix in
Please try the LLM again on the latest commit. Happy to dig further if anything still fails. |
- STT: raise APIConnectionError on unexpected peer close so the framework pump reconnects (normal close ended async-for silently) - TTS: set input_done only after successful input drain so reconnect does not drop remaining tokenizer/input text - Upgrade remote http:// API bases to wss:// so auth tokens are not sent on cleartext WebSockets (localhost keeps ws://)
|
Addressed the latest Devin review findings in
All 56 blaze unit tests pass. Thanks! |
Keep blaze plugin alongside new bland optional dep; bump blaze to 1.6.8 and regenerate uv.lock. Optional-deps pins follow main (>=1.6.8).
|
Resolved merge conflicts with latest
Head includes prior LLM body + STT/TTS WS fixes. Happy to address any further feedback. |
| if not text.strip(): | ||
| # Empty result β decide whether to buffer or discard | ||
| self._pending_empty_count += 1 | ||
| total_pending_duration = pending_duration + segment_duration | ||
|
|
||
| if ( | ||
| self._pending_empty_count <= self._max_pending_segments | ||
| and total_pending_duration <= self._max_pending_duration | ||
| ): | ||
| # Buffer this segment's PCM for the next call | ||
| self._pending_pcm = pcm_data # includes already-prepended pending | ||
| self._pending_sample_rate = sample_rate | ||
| self._pending_num_channels = num_channels | ||
| logger.info( | ||
| "[%s] STT empty β buffered (count=%d, duration=%.1fs, latency=%.3fs)", | ||
| request_id, | ||
| self._pending_empty_count, | ||
| total_pending_duration, | ||
| latency, | ||
| ) | ||
| else: | ||
| # Safety limit reached β discard buffer | ||
| logger.info( | ||
| "[%s] STT empty β discarded pending buffer " | ||
| "(count=%d, duration=%.1fs, latency=%.3fs)", | ||
| request_id, | ||
| self._pending_empty_count, | ||
| total_pending_duration, | ||
| latency, | ||
| ) | ||
| self._pending_pcm = b"" | ||
| self._pending_empty_count = 0 | ||
|
|
There was a problem hiding this comment.
π‘ Leftover speech audio can be mixed into a different conversation's transcription
Unrecognized audio is stashed on the shared speech-to-text object (self._pending_pcm at livekit-plugins/livekit-plugins-blaze/livekit/plugins/blaze/stt.py:366) instead of per-conversation state, so audio left over from one caller can be glued onto the front of another caller's next utterance.
Impact: When one speech-to-text instance serves more than one conversation at a time, a caller can get a transcript containing someone else's speech, or their own words duplicated.
Instance-level pending PCM buffer is mutated by every concurrent recognize call
STT._recognize_impl keeps _pending_pcm, _pending_empty_count, _pending_sample_rate, and _last_recognize_time on the STT object itself (livekit-plugins/livekit-plugins-blaze/livekit/plugins/blaze/stt.py:126-135). A single STT instance is commonly reused across sessions/streams (e.g. via StreamAdapter, FallbackAdapter, or a plugin created once and passed to multiple AgentSessions). Each _recognize_impl call reads self._pending_pcm at line 264-271 and writes it at line 366, with no per-stream scoping and no locking, so two interleaved segments from different streams will prepend each other's PCM. The idle-timeout auto-clear at lines 251-261 also uses a single shared _last_recognize_time, so activity in one stream keeps another stream's stale buffer alive.
A fix would be to move the accumulation state into a per-stream object (or key it by the calling stream) rather than storing it on the shared STT instance.
Was this helpful? React with π or π to provide feedback.
There was a problem hiding this comment.
Addressed in b344111: empty-segment PCM accumulation now uses a task-local ContextVar (_RecognizePending) instead of fields on the shared STT instance. Concurrent StreamAdapter/AgentSession tasks each get their own buffer so leftover audio cannot be prepended across conversations. Unit test test_stt_pending_pcm_is_task_local covers isolation.
- Keep empty-segment PCM accumulation in a task-local ContextVar so concurrent streams/sessions cannot mix leftover audio - After TTS starts draining input, do not private-reconnect on WS drop; raise APIConnectionError so framework can replay buffered text - Wrap lines over 100 chars (ruff / project style)
|
Addressed the latest Devin review findings in
57 blaze unit tests pass. |
| except httpx.TimeoutException as e: | ||
| raise APITimeoutError(f"LLM request timed out: {e}") from e | ||
| except httpx.NetworkError as e: | ||
| raise APIConnectionError(f"LLM network error: {e}") from e | ||
| except APIStatusError: | ||
| raise | ||
| except Exception as e: | ||
| raise APIConnectionError(f"LLM connection error: {e}") from e |
There was a problem hiding this comment.
π΄ Chatbot answers can be spoken twice when the connection drops mid-answer
A failure that happens after part of the answer was already delivered is reported as retryable (raise APIConnectionError(...) at livekit-plugins/livekit-plugins-blaze/livekit/plugins/blaze/llm.py:412-419) instead of final, so the whole answer is requested again and the already-delivered part is repeated.
Impact: The user can hear the beginning of the assistant's reply repeated (or a duplicated/garbled reply) whenever the chat connection hiccups mid-response.
Mechanism: framework retry loop replays `_run()` after chunks were already pushed
LLMStream._main_task (livekit-agents/livekit/agents/llm/llm.py:254-300) retries _run() for any APIError whose retryable flag is True. APIError/APIConnectionError/APITimeoutError default to retryable=True (livekit-agents/livekit/agents/_exceptions.py:40,112,119). Chunks already sent via self._event_ch.send_nowait(chunk) (llm.py:396, llm.py:410) are not retracted, so the retry appends a second copy of the response to the same stream.
Other providers guard against this by flipping a retryable flag to False as soon as the first chunk is emitted β see livekit-agents/livekit/agents/inference/llm.py:463-498 and livekit-plugins/livekit-plugins-anthropic/livekit/plugins/anthropic/llm.py:340-349.
Fix sketch: track a local retryable = True that is set to False right after the first send_nowait, and pass retryable=retryable to the raised APITimeoutError/APIConnectionError/APIStatusError.
Prompt for agents
In livekit-plugins/livekit-plugins-blaze/livekit/plugins/blaze/llm.py, LLMStream._run() pushes ChatChunks into self._event_ch as SSE data arrives, but every error it raises (APITimeoutError, APIConnectionError, APIStatusError) uses the default retryable=True. The base LLMStream._main_task in livekit-agents/livekit/agents/llm/llm.py re-invokes _run() for retryable APIErrors, which replays the request and re-emits content the consumer already received, producing duplicated assistant text/speech. Follow the convention used by livekit-agents/livekit/agents/inference/llm.py and the anthropic plugin: keep a local `retryable` flag initialized to True, set it to False the moment the first chunk is sent to self._event_ch (both the tool-call chunk and the content chunk paths), and pass retryable=retryable when constructing the raised errors.
Was this helpful? React with π or π to provide feedback.
Summary
Add Blaze plugin support for LiveKit Agents.
Changes
Motivation
Enable Blaze voice AI services to be used through the existing LiveKit Agents plugin architecture.
Notes
The implementation is isolated under
livekit-plugins/livekit-plugins-blazeand follows the existing provider plugin pattern.