From 06573358ea83f8e6f290c389b2023fb4ca6ebb89 Mon Sep 17 00:00:00 2001 From: PiotrWodecki Date: Tue, 2 Jun 2026 11:51:19 +0200 Subject: [PATCH 1/4] Fix type errors in Python docs snippets Fixes surfaced by the new typecheck:python pipeline: - create_room calls now pass RoomOptions instead of raw kwargs (livestreams, whip-whep, livestreaming). - FishjamNotifier uses fishjam_id / management_token kwargs instead of server_address / server_api_token. - AgentSession.add_track replaces non-existent create_track; matching send_chunk / interrupt calls are awaited. - Import fixes: IncomingTrackData (was AgentResponseTrackData), TrackEncoding from fishjam.events (was fishjam.agent), TRACK_ENCODING_PCM16 enum value (was PCM16), AgentOptions, ServerMessagePeerAdded, ServerMessageRoomCreated, pydantic.BaseModel, google.genai client wiring. - vapi_server_sdk replaces non-existent 'vapi' package. - Top-level async with / await wrapped in async def run() + asyncio.run(run()) so snippets are runnable as written. --- docs/explanation/livestreams.mdx | 10 +-- docs/how-to/backend/fastapi-example.mdx | 3 +- docs/how-to/backend/server-setup.mdx | 1 + docs/how-to/backend/whip-whep.mdx | 8 +- docs/integrations/gemini-live-integration.mdx | 87 ++++++++++--------- docs/integrations/vapi-integration.mdx | 2 +- docs/tutorials/agents.mdx | 85 ++++++++++-------- docs/tutorials/backend-quick-start.mdx | 1 + docs/tutorials/livestreaming.mdx | 4 +- 9 files changed, 112 insertions(+), 89 deletions(-) diff --git a/docs/explanation/livestreams.mdx b/docs/explanation/livestreams.mdx index d0f7c5d9..c60e8e6d 100644 --- a/docs/explanation/livestreams.mdx +++ b/docs/explanation/livestreams.mdx @@ -41,15 +41,15 @@ By default, created livestreams are `private`, to prevent possible bugs. ```python - from fishjam import FishjamClient + from fishjam import FishjamClient, RoomOptions fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, ); - private_room = fishjam_client.create_room(room_type="livestream") # [!code highlight] - public_room = fishjam_client.create_room(room_type="livestream", public=True) # [!code highlight] + private_room = fishjam_client.create_room(RoomOptions(room_type="livestream")) # [!code highlight] + public_room = fishjam_client.create_room(RoomOptions(room_type="livestream", public=True)) # [!code highlight] ``` @@ -93,14 +93,14 @@ Note that for development purposes, you can [use the Sandbox API to generate a v ```python - from fishjam import FishjamClient + from fishjam import FishjamClient, RoomOptions fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, ); - private_room = fishjam_client.create_room(room_type="livestream") + private_room = fishjam_client.create_room(RoomOptions(room_type="livestream")) viewer_token = fishjam_client.create_livestream_viewer_token(private_room.id) # [!code highlight] ``` diff --git a/docs/how-to/backend/fastapi-example.mdx b/docs/how-to/backend/fastapi-example.mdx index dd886255..51869ac9 100644 --- a/docs/how-to/backend/fastapi-example.mdx +++ b/docs/how-to/backend/fastapi-example.mdx @@ -77,8 +77,9 @@ It doesn't interact with the FastAPI library per se, just start the event loop a ```python import asyncio from fishjam import FishjamNotifier +from fishjam.events import ServerMessagePeerAdded -notifier = FishjamNotifier(server_address=fishjam_id, server_api_token=management_token) +notifier = FishjamNotifier(fishjam_id=fishjam_id, management_token=management_token) @notifier.on_server_notification def handle_notification(notification): diff --git a/docs/how-to/backend/server-setup.mdx b/docs/how-to/backend/server-setup.mdx index 8d303cba..356c9d72 100644 --- a/docs/how-to/backend/server-setup.mdx +++ b/docs/how-to/backend/server-setup.mdx @@ -276,6 +276,7 @@ It sets up a websocket connection with a Fishjam instance and provides a simple ```python import asyncio from fishjam import FishjamNotifier + from fishjam.events import ServerMessageRoomCreated notifier = FishjamNotifier(fishjam_id, management_token) diff --git a/docs/how-to/backend/whip-whep.mdx b/docs/how-to/backend/whip-whep.mdx index 8c3ef4e4..9b8c8bf3 100644 --- a/docs/how-to/backend/whip-whep.mdx +++ b/docs/how-to/backend/whip-whep.mdx @@ -71,14 +71,14 @@ You can read about this in detail in [Production Livestreaming with Server SDKs] ```python - from fishjam import FishjamClient + from fishjam import FishjamClient, RoomOptions fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, ); - room = fishjam_client.create_room(room_type="livestream") + room = fishjam_client.create_room(RoomOptions(room_type="livestream")) streamer_token = fishjam_client.create_livestream_streamer_token(room.id) # [!code highlight] ``` @@ -150,14 +150,14 @@ You can read about this in detail in [Production Livestreaming with Server SDKs] ```python - from fishjam import FishjamClient + from fishjam import FishjamClient, RoomOptions fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, ); - room = fishjam_client.create_room(room_type="livestream", public=False) + room = fishjam_client.create_room(RoomOptions(room_type="livestream", public=False)) # ... diff --git a/docs/integrations/gemini-live-integration.mdx b/docs/integrations/gemini-live-integration.mdx index 33518a1e..de5ee95d 100644 --- a/docs/integrations/gemini-live-integration.mdx +++ b/docs/integrations/gemini-live-integration.mdx @@ -227,50 +227,57 @@ Fishjam handles raw bytes, while Google GenAI SDKs often expect Base64 strings. Now we connect the websocket loops. We need to forward incoming Fishjam audio to Google, and forward incoming Google audio to Fishjam. ```python import asyncio - from fishjam.integrations.gemini import GeminiIntegration + import os + from google import genai from google.genai.types import Blob, Modality + from fishjam.integrations.gemini import GeminiIntegration GEMINI_MODEL = "gemini-2.5-flash-native-audio-preview-12-2025" - async with agent.connect() as fishjam_session: - - # Use our preset to match the required audio format (24kHz) - outgoing_track = await fishjam_session.add_track(GeminiIntegration.GEMINI_OUTPUT_AUDIO_SETTINGS) - - async with gen_ai.aio.live.connect( - model=GEMINI_MODEL, - config={"response_modalities": [Modality.AUDIO]} - ) as gemini_session: - - # Fishjam -> Google - async def forward_audio_to_gemini(): - async for track_data in fishjam_session.receive(): - await gemini_session.send_realtime_input(audio=Blob( - mime_type=GeminiIntegration.GEMINI_AUDIO_MIME_TYPE, - data=track_data.data - )) - - # Google -> Fishjam - async def forward_audio_to_fishjam(): - async for msg in gemini_session.receive(): - server_content = msg.server_content - - if server_content is None: - continue - - if server_content.interrupted: - await outgoing_track.interrupt() - - if server_content.model_turn and server_content.model_turn.parts: - for part in server_content.model_turn.parts: - if part.inline_data and part.inline_data.data: - await outgoing_track.send_chunk(part.inline_data.data) - - # Run both loops concurrently - await asyncio.gather( - forward_audio_to_gemini(), - forward_audio_to_fishjam() - ) + genai_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) + + async def run(): + async with agent.connect() as fishjam_session: + + # Use our preset to match the required audio format (24kHz) + outgoing_track = await fishjam_session.add_track(GeminiIntegration.GEMINI_OUTPUT_AUDIO_SETTINGS) + + async with genai_client.aio.live.connect( + model=GEMINI_MODEL, + config={"response_modalities": [Modality.AUDIO]} + ) as gemini_session: + + # Fishjam -> Google + async def forward_audio_to_gemini(): + async for track_data in fishjam_session.receive(): + await gemini_session.send_realtime_input(audio=Blob( + mime_type=GeminiIntegration.GEMINI_AUDIO_MIME_TYPE, + data=track_data.data + )) + + # Google -> Fishjam + async def forward_audio_to_fishjam(): + async for msg in gemini_session.receive(): + server_content = msg.server_content + + if server_content is None: + continue + + if server_content.interrupted: + await outgoing_track.interrupt() + + if server_content.model_turn and server_content.model_turn.parts: + for part in server_content.model_turn.parts: + if part.inline_data and part.inline_data.data: + await outgoing_track.send_chunk(part.inline_data.data) + + # Run both loops concurrently + await asyncio.gather( + forward_audio_to_gemini(), + forward_audio_to_fishjam() + ) + + asyncio.run(run()) ``` diff --git a/docs/integrations/vapi-integration.mdx b/docs/integrations/vapi-integration.mdx index 397bed25..630fbd9b 100644 --- a/docs/integrations/vapi-integration.mdx +++ b/docs/integrations/vapi-integration.mdx @@ -93,7 +93,7 @@ You can reference an existing assistant by its ID, or create a transient assista ```python import os - from vapi import Vapi + from vapi_server_sdk import Vapi vapi_client = Vapi(token=os.environ["VAPI_API_KEY"]) diff --git a/docs/tutorials/agents.mdx b/docs/tutorials/agents.mdx index aa751d6f..b745f9c4 100644 --- a/docs/tutorials/agents.mdx +++ b/docs/tutorials/agents.mdx @@ -108,19 +108,23 @@ However, it's likely that in your scenario you'll want to use the [Selective Sub ```python - from fishjam import FishjamClient - from fishjam.agent import AgentResponseTrackData + import asyncio + from fishjam import FishjamClient, AgentOptions + from fishjam.agent import IncomingTrackData fishjam_client = FishjamClient(fishjam_id, management_token) agent_options = AgentOptions(subscribe_mode="auto") - agent = fishjam_client.create_agent(room_id) + agent = fishjam_client.create_agent(room_id, agent_options) + + async def run(): + # the agent will disconnect once you exit the context + async with agent.connect() as session: + async for track_data in session.receive(): + # process the incoming data + pass - # the agent will disconnect once you exit the context - async with agent.connect() as session: - async for track_data in session.receive(): - # process the incoming data - pass + asyncio.run(run()) ``` @@ -239,28 +243,33 @@ You can interrupt the currently played audio chunk. See the example below. ```python - from fishjam import FishjamClient - from fishjam.agent import AgentResponseTrackData, OutgoingAudioTrackOptions, TrackEncoding + import asyncio + from fishjam import FishjamClient, AgentOptions + from fishjam.agent import OutgoingAudioTrackOptions + from fishjam.events import TrackEncoding fishjam_client = FishjamClient(fishjam_id, management_token) agent_options = AgentOptions(subscribe_mode="auto") - agent = fishjam_client.create_agent(room_id) + agent = fishjam_client.create_agent(room_id, agent_options) + + track_options = OutgoingAudioTrackOptions(encoding=TrackEncoding.TRACK_ENCODING_PCM16) - track_options = OutgoingAudioTrackOptions(encoding=TrackEncoding.PCM16) + async def run(): + # the agent will disconnect once you exit the context + async with agent.connect() as session: + outgoing_track = await session.add_track(track_options) - # the agent will disconnect once you exit the context - async with agent.connect() as session: - outgoing_track = session.create_track(track_options) + # that's a dummy chatbot + async for chatbot_data in chatbot.receive(): + await outgoing_track.send_chunk(chatbot_data) - # that's a dummy chatbot - async for chatbot_data in chatbot.receive(): - outgoing_track.send_chunk(chatbot_data) + # you're able to interrupt the currently played audio chunk + # [!code highlight:1] + await outgoing_track.interrupt() - # you're able to interrupt the currently played audio chunk - # [!code highlight:1] - ongoing_track.interrupt() + asyncio.run(run()) ``` @@ -321,18 +330,21 @@ Video frame capture is rate-limited to one frame per second per track. agent = fishjam_client.create_agent(room_id) - async with agent.connect() as session: - # Request a frame - # [!code highlight:1] - await session.capture_image(track_id) - - # Captured frames arrive as IncomingTrackImage messages - async for message in session.receive(): - match message: - case IncomingTrackImage() as msg if msg.track_id == track_id: - data = msg.data - # process the image data - pass + async def run(): + async with agent.connect() as session: + # Request a frame + # [!code highlight:1] + await session.capture_image(track_id) + + # Captured frames arrive as IncomingTrackImage messages + async for message in session.receive(): + match message: + case IncomingTrackImage() as msg if msg.track_id == track_id: + data = msg.data + # process the image data + pass + + asyncio.run(run()) ``` @@ -365,9 +377,10 @@ After you're done using an agent, you can disconnect it from the room. ```python - # the agent will disconnect once you exit the context - async with agent.connect() as session: - pass + async def run(): + # the agent will disconnect once you exit the context + async with agent.connect() as session: + pass ``` diff --git a/docs/tutorials/backend-quick-start.mdx b/docs/tutorials/backend-quick-start.mdx index 0c423d2b..a711f38b 100644 --- a/docs/tutorials/backend-quick-start.mdx +++ b/docs/tutorials/backend-quick-start.mdx @@ -354,6 +354,7 @@ Build a simple HTTP endpoint that your client apps can call: ```python import os from fastapi import FastAPI, HTTPException + from pydantic import BaseModel from fishjam import FishjamClient, PeerOptions app = FastAPI() diff --git a/docs/tutorials/livestreaming.mdx b/docs/tutorials/livestreaming.mdx index a1cca7af..57e5814b 100644 --- a/docs/tutorials/livestreaming.mdx +++ b/docs/tutorials/livestreaming.mdx @@ -429,7 +429,7 @@ Our backend needs to do three things to be ready for streaming: ```python - from fishjam import FishjamClient + from fishjam import FishjamClient, RoomOptions fishjam_client = FishjamClient( fishjam_id=fishjam_id, @@ -437,7 +437,7 @@ Our backend needs to do three things to be ready for streaming: ); # 1. - room = fishjam_client.create_room(room_type="livestream") + room = fishjam_client.create_room(RoomOptions(room_type="livestream")) # 2. streamer_token = fishjam_client.create_livestream_streamer_token(room.id) # 3. From 2d3fc7a215a80c16e5671276d664f21380e79669 Mon Sep 17 00:00:00 2001 From: PiotrWodecki Date: Tue, 2 Jun 2026 12:16:30 +0200 Subject: [PATCH 2/4] Address Copilot review feedback - Drop stray ';' from FishjamClient constructor calls in livestreams, whip-whep, and livestreaming snippets so they're valid Python. - gemini-live integration: reuse the gen_ai client created in Step 1 instead of constructing a new genai.Client with a different env var. - vapi-integration install snippet: 'vapi' -> 'vapi_server_sdk' to match the actual PyPI package the import now uses. - agents.mdx disconnect snippet: add 'import asyncio' and 'asyncio.run(run())' so the example actually runs. --- docs/explanation/livestreams.mdx | 4 ++-- docs/how-to/backend/whip-whep.mdx | 4 ++-- docs/integrations/gemini-live-integration.mdx | 6 +----- docs/integrations/vapi-integration.mdx | 2 +- docs/tutorials/agents.mdx | 4 ++++ docs/tutorials/livestreaming.mdx | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/explanation/livestreams.mdx b/docs/explanation/livestreams.mdx index c60e8e6d..3fd869bb 100644 --- a/docs/explanation/livestreams.mdx +++ b/docs/explanation/livestreams.mdx @@ -46,7 +46,7 @@ By default, created livestreams are `private`, to prevent possible bugs. fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, - ); + ) private_room = fishjam_client.create_room(RoomOptions(room_type="livestream")) # [!code highlight] public_room = fishjam_client.create_room(RoomOptions(room_type="livestream", public=True)) # [!code highlight] @@ -98,7 +98,7 @@ Note that for development purposes, you can [use the Sandbox API to generate a v fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, - ); + ) private_room = fishjam_client.create_room(RoomOptions(room_type="livestream")) viewer_token = fishjam_client.create_livestream_viewer_token(private_room.id) # [!code highlight] diff --git a/docs/how-to/backend/whip-whep.mdx b/docs/how-to/backend/whip-whep.mdx index 9b8c8bf3..a5b61c6c 100644 --- a/docs/how-to/backend/whip-whep.mdx +++ b/docs/how-to/backend/whip-whep.mdx @@ -76,7 +76,7 @@ You can read about this in detail in [Production Livestreaming with Server SDKs] fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, - ); + ) room = fishjam_client.create_room(RoomOptions(room_type="livestream")) streamer_token = fishjam_client.create_livestream_streamer_token(room.id) # [!code highlight] @@ -155,7 +155,7 @@ You can read about this in detail in [Production Livestreaming with Server SDKs] fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, - ); + ) room = fishjam_client.create_room(RoomOptions(room_type="livestream", public=False)) diff --git a/docs/integrations/gemini-live-integration.mdx b/docs/integrations/gemini-live-integration.mdx index de5ee95d..bc0f138f 100644 --- a/docs/integrations/gemini-live-integration.mdx +++ b/docs/integrations/gemini-live-integration.mdx @@ -227,22 +227,18 @@ Fishjam handles raw bytes, while Google GenAI SDKs often expect Base64 strings. Now we connect the websocket loops. We need to forward incoming Fishjam audio to Google, and forward incoming Google audio to Fishjam. ```python import asyncio - import os - from google import genai from google.genai.types import Blob, Modality from fishjam.integrations.gemini import GeminiIntegration GEMINI_MODEL = "gemini-2.5-flash-native-audio-preview-12-2025" - genai_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) - async def run(): async with agent.connect() as fishjam_session: # Use our preset to match the required audio format (24kHz) outgoing_track = await fishjam_session.add_track(GeminiIntegration.GEMINI_OUTPUT_AUDIO_SETTINGS) - async with genai_client.aio.live.connect( + async with gen_ai.aio.live.connect( model=GEMINI_MODEL, config={"response_modalities": [Modality.AUDIO]} ) as gemini_session: diff --git a/docs/integrations/vapi-integration.mdx b/docs/integrations/vapi-integration.mdx index 630fbd9b..80eb7db5 100644 --- a/docs/integrations/vapi-integration.mdx +++ b/docs/integrations/vapi-integration.mdx @@ -50,7 +50,7 @@ You will need: ```bash - pip install fishjam-server-sdk vapi + pip install fishjam-server-sdk vapi_server_sdk ``` diff --git a/docs/tutorials/agents.mdx b/docs/tutorials/agents.mdx index b745f9c4..9087dc0a 100644 --- a/docs/tutorials/agents.mdx +++ b/docs/tutorials/agents.mdx @@ -377,10 +377,14 @@ After you're done using an agent, you can disconnect it from the room. ```python + import asyncio + async def run(): # the agent will disconnect once you exit the context async with agent.connect() as session: pass + + asyncio.run(run()) ``` diff --git a/docs/tutorials/livestreaming.mdx b/docs/tutorials/livestreaming.mdx index 57e5814b..eb80db0d 100644 --- a/docs/tutorials/livestreaming.mdx +++ b/docs/tutorials/livestreaming.mdx @@ -434,7 +434,7 @@ Our backend needs to do three things to be ready for streaming: fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, - ); + ) # 1. room = fishjam_client.create_room(RoomOptions(room_type="livestream")) From 0e22732a4352217a16976dcbf8a865599333517e Mon Sep 17 00:00:00 2001 From: PiotrWodecki Date: Tue, 2 Jun 2026 12:23:15 +0200 Subject: [PATCH 3/4] Recursively init submodules in prepare.sh scripts/prepare.sh used 'git submodule update --init --remote' (non-recursive) to advance submodules to their tracked-branch tip. Upstream web-client-sdk recently converted react-native-webrtc from an npm dep into a nested submodule referenced as 'workspace:*'. Without --recursive, that nested submodule directory stays empty after the remote bump, and 'yarn install' inside mobile-client fails with 'Workspace not found'. Adding --recursive (and matching --recursive on 'git submodule sync') so nested submodules are initialized too. --- scripts/prepare.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/prepare.sh b/scripts/prepare.sh index 12a8fb52..c472a91e 100755 --- a/scripts/prepare.sh +++ b/scripts/prepare.sh @@ -7,8 +7,8 @@ echo $ROOTDIR cd $ROOTDIR printf "Synchronising submodules... " -git submodule sync >>/dev/null -git submodule update --init --remote >>/dev/null +git submodule sync --recursive >>/dev/null +git submodule update --init --recursive --remote >>/dev/null # cd packages/web-client-sdk/ # yarn && yarn build From c686292aadc12f59c3bc58ddbffabc4726496439 Mon Sep 17 00:00:00 2001 From: PiotrWodecki Date: Tue, 2 Jun 2026 12:29:06 +0200 Subject: [PATCH 4/4] Stop pulling submodule tips on every prepare run scripts/prepare.sh was running 'git submodule update --init --remote', advancing every submodule to the tip of its tracked branch on every install. That made docs CI non-deterministic and surfaced upstream breakage in unrelated PRs: web-client-sdk main moved past v0.27.0 to a state where react-native-webrtc became a nested workspace submodule whose build chain (react-native-builder-bob 0.18 + recent babel) fails with "'production' is not a valid target". prepare.sh now just initializes submodules at the commits pinned in this repo. The deliberate 'pull latest SDK' workflow already lives in scripts/update.sh (yarn update-modules). --- docs/integrations/vapi-integration.mdx | 2 +- scripts/prepare.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/vapi-integration.mdx b/docs/integrations/vapi-integration.mdx index 80eb7db5..c8684887 100644 --- a/docs/integrations/vapi-integration.mdx +++ b/docs/integrations/vapi-integration.mdx @@ -93,7 +93,7 @@ You can reference an existing assistant by its ID, or create a transient assista ```python import os - from vapi_server_sdk import Vapi + from vapi import Vapi vapi_client = Vapi(token=os.environ["VAPI_API_KEY"]) diff --git a/scripts/prepare.sh b/scripts/prepare.sh index c472a91e..bee5f0ac 100755 --- a/scripts/prepare.sh +++ b/scripts/prepare.sh @@ -8,7 +8,7 @@ cd $ROOTDIR printf "Synchronising submodules... " git submodule sync --recursive >>/dev/null -git submodule update --init --recursive --remote >>/dev/null +git submodule update --init --recursive >>/dev/null # cd packages/web-client-sdk/ # yarn && yarn build