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:
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user