Add Inworld Realtime Service (#4140)

* Add Inworld Realtime LLM service

Adds a WebSocket-based realtime service for Inworld's cascade
STT/LLM/TTS API with semantic VAD, function calling, and streaming
transcription support.

New files:
- src/pipecat/services/inworld/realtime/ (service, events)
- src/pipecat/adapters/services/inworld_realtime_adapter.py
- examples/foundational/19zb-inworld-realtime.py

Also includes:
- websockets dependency for inworld extra in pyproject.toml
- Adapter and settings tests matching OpenAI/Grok realtime patterns
- Fix for double-response when server-side VAD is enabled

* Prefer init-provided system instruction in Inworld Realtime

Adopt _resolve_system_instruction() from BaseLLMAdapter, matching the
pattern applied to OpenAI Realtime, Grok Realtime, Gemini Live, and
Nova Sonic in the pk/realtime-services-init-v-context-system-instructions-cleanup
branch.

* Update changelog entry with PR number

* Fix changelog format to use bullet point

* Polish PR: default model, example cleanup, changelog update

- Change default model from gpt-4.1-nano to gpt-4.1-mini
- Add function calling demo to example
- Remove demo-testing artifact from system instruction
- Mention Router support in changelog

* Address PR review feedback for Inworld Realtime

- Move example to examples/realtime/realtime-inworld.py
- Change initial context role from "user" to "developer"
- Remove explicit sample rates from example; sync them in
  _ensure_audio_config so Inworld gets the transport's actual rates
- Add audio race condition guard in _handle_evt_audio_delta (matches
  OpenAI realtime pattern)
- Convert remaining "system"/"developer" messages to "user" in adapter
- Add clarifying comment for local-VAD vs server-VAD metrics paths

* Simplify example, add provider tracking, remove local VAD path

- Remove function calling from example, switch model to xai/grok-4-1-fast-non-reasoning
- Add pipecat-realtime session key prefix and provider_data metadata
  for Inworld traffic attribution
- Remove local VAD code path (Inworld only supports server-side VAD)
- Use typed InputAudioBufferAppendEvent for audio sends

* Default TTS model to inworld-tts-1.5-max

* Remove dead shimmed tools code, set STT/VAD defaults

- Remove non-functional AdapterType.SHIM custom tools code from adapter
- Default STT model to assemblyai/u3-rt-pro
- Default VAD eagerness to low
This commit is contained in:
Cale Shapera
2026-04-09 10:04:17 -07:00
committed by GitHub
parent 76601944c6
commit ec574edd53
10 changed files with 2561 additions and 1 deletions

View File

@@ -10,6 +10,8 @@ from unittest.mock import patch
from pipecat.services.deepgram.sagemaker.stt import DeepgramSageMakerSTTSettings
from pipecat.services.deepgram.stt import DeepgramSTTService, DeepgramSTTSettings
from pipecat.services.inworld.realtime import events as inworld_events
from pipecat.services.inworld.realtime.llm import InworldRealtimeLLMSettings
from pipecat.services.openai.realtime import events
from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMSettings
from pipecat.services.settings import (
@@ -979,3 +981,200 @@ class TestGrokRealtimeSettingsFromMapping:
assert store.session_properties.instructions == "Be concise."
assert store.session_properties.voice == "Eve"
assert store.system_instruction == "Be concise."
# ---------------------------------------------------------------------------
# InworldRealtimeLLMSettings: apply_update with bidirectional sync
# ---------------------------------------------------------------------------
class TestInworldRealtimeSettingsApplyUpdate:
def _make_store(self, **kwargs) -> InworldRealtimeLLMSettings:
"""Helper to build a store-mode InworldRealtimeLLMSettings."""
defaults = dict(
model="openai/gpt-4.1-nano",
system_instruction=None,
temperature=None,
max_tokens=None,
top_p=None,
top_k=None,
frequency_penalty=None,
presence_penalty=None,
seed=None,
filter_incomplete_user_turns=False,
user_turn_completion_config=None,
session_properties=inworld_events.SessionProperties(),
)
defaults.update(kwargs)
return InworldRealtimeLLMSettings(**defaults)
def test_top_level_model_syncs_to_sp(self):
"""Updating top-level model should propagate to session_properties.model."""
store = self._make_store()
delta = InworldRealtimeLLMSettings(model="openai/gpt-4.1")
changed = store.apply_update(delta)
assert "model" in changed
assert store.model == "openai/gpt-4.1"
assert store.session_properties.model == "openai/gpt-4.1"
def test_top_level_system_instruction_syncs_to_sp(self):
"""Updating top-level system_instruction should propagate to session_properties.instructions."""
store = self._make_store()
delta = InworldRealtimeLLMSettings(system_instruction="Be helpful.")
changed = store.apply_update(delta)
assert "system_instruction" in changed
assert store.system_instruction == "Be helpful."
assert store.session_properties.instructions == "Be helpful."
def test_sp_replaces_wholesale(self):
"""session_properties in delta replaces the entire stored SP."""
store = self._make_store(
session_properties=inworld_events.SessionProperties(
output_modalities=["audio", "text"],
instructions="Old instructions.",
),
system_instruction="Old instructions.",
)
new_sp = inworld_events.SessionProperties(output_modalities=["text"])
delta = InworldRealtimeLLMSettings(session_properties=new_sp)
changed = store.apply_update(delta)
assert "session_properties" in changed
assert store.session_properties.output_modalities == ["text"]
# model is synced from top-level
assert store.session_properties.model == "openai/gpt-4.1-nano"
def test_sp_model_syncs_to_top_level(self):
"""session_properties.model should sync to top-level model."""
store = self._make_store()
new_sp = inworld_events.SessionProperties(model="openai/gpt-4.1")
delta = InworldRealtimeLLMSettings(session_properties=new_sp)
changed = store.apply_update(delta)
assert "model" in changed
assert store.model == "openai/gpt-4.1"
assert store.session_properties.model == "openai/gpt-4.1"
def test_sp_instructions_syncs_to_top_level(self):
"""session_properties.instructions should sync to top-level system_instruction."""
store = self._make_store()
new_sp = inworld_events.SessionProperties(instructions="New instructions.")
delta = InworldRealtimeLLMSettings(session_properties=new_sp)
changed = store.apply_update(delta)
assert "system_instruction" in changed
assert store.system_instruction == "New instructions."
assert store.session_properties.instructions == "New instructions."
def test_top_level_model_takes_precedence_over_sp_model(self):
"""When both model and session_properties.model are in the delta, top-level wins."""
store = self._make_store()
new_sp = inworld_events.SessionProperties(model="sp-model")
delta = InworldRealtimeLLMSettings(model="top-model", session_properties=new_sp)
store.apply_update(delta)
assert store.model == "top-model"
assert store.session_properties.model == "top-model"
def test_top_level_si_takes_precedence_over_sp_instructions(self):
"""When both system_instruction and SP.instructions are in delta, top-level wins."""
store = self._make_store()
new_sp = inworld_events.SessionProperties(instructions="sp instructions")
delta = InworldRealtimeLLMSettings(
system_instruction="top instructions",
session_properties=new_sp,
)
store.apply_update(delta)
assert store.system_instruction == "top instructions"
assert store.session_properties.instructions == "top instructions"
def test_non_synced_field_update_does_not_affect_sp(self):
"""Updating a non-synced field like temperature shouldn't touch session_properties."""
store = self._make_store(
session_properties=inworld_events.SessionProperties(instructions="Keep me."),
system_instruction="Keep me.",
)
original_sp = store.session_properties
delta = InworldRealtimeLLMSettings(temperature=0.5)
changed = store.apply_update(delta)
assert "temperature" in changed
assert store.temperature == 0.5
# SP should be untouched (same object)
assert store.session_properties is original_sp
assert store.session_properties.instructions == "Keep me."
# ---------------------------------------------------------------------------
# InworldRealtimeLLMSettings: from_mapping
# ---------------------------------------------------------------------------
class TestInworldRealtimeSettingsFromMapping:
def test_sp_keys_route_to_session_properties(self):
"""SessionProperties fields (instructions, output_modalities) route into nested SP."""
delta = InworldRealtimeLLMSettings.from_mapping(
{"instructions": "Be concise.", "output_modalities": ["text"]}
)
assert is_given(delta.session_properties)
assert delta.session_properties.instructions == "Be concise."
assert delta.session_properties.output_modalities == ["text"]
def test_model_routes_to_top_level(self):
"""model should go to the top-level field, not session_properties."""
delta = InworldRealtimeLLMSettings.from_mapping({"model": "openai/gpt-4.1"})
assert delta.model == "openai/gpt-4.1"
# No session_properties should be created since no SP keys were present
assert not is_given(delta.session_properties)
def test_unknown_keys_go_to_extra(self):
"""Unrecognized keys should land in extra."""
delta = InworldRealtimeLLMSettings.from_mapping({"unknown_param": 42})
assert not is_given(delta.model)
assert not is_given(delta.session_properties)
assert delta.extra == {"unknown_param": 42}
def test_mixed_keys(self):
"""model + SP keys + unknown keys are routed correctly."""
delta = InworldRealtimeLLMSettings.from_mapping(
{
"model": "openai/gpt-4.1",
"instructions": "Be helpful.",
"unknown": "val",
}
)
assert delta.model == "openai/gpt-4.1"
assert is_given(delta.session_properties)
assert delta.session_properties.instructions == "Be helpful."
assert delta.extra == {"unknown": "val"}
def test_roundtrip_from_mapping_apply_update(self):
"""Simulate dict-style update: from_mapping -> apply_update."""
store = InworldRealtimeLLMSettings(
model="openai/gpt-4.1-nano",
system_instruction=None,
temperature=None,
max_tokens=None,
top_p=None,
top_k=None,
frequency_penalty=None,
presence_penalty=None,
seed=None,
filter_incomplete_user_turns=False,
user_turn_completion_config=None,
session_properties=inworld_events.SessionProperties(),
)
raw = {"instructions": "Be concise.", "output_modalities": ["text"]}
delta = InworldRealtimeLLMSettings.from_mapping(raw)
changed = store.apply_update(delta)
assert "session_properties" in changed
assert store.session_properties.instructions == "Be concise."
assert store.session_properties.output_modalities == ["text"]
assert store.system_instruction == "Be concise."