Merge pull request #4385 from pipecat-ai/mb/runner-session-id

feat(runner): add session_id to RunnerArguments
This commit is contained in:
Mark Backman
2026-04-29 13:17:47 -04:00
committed by GitHub
3 changed files with 47 additions and 14 deletions

1
changelog/4385.added.md Normal file
View File

@@ -0,0 +1 @@
- Added a `session_id` field to `RunnerArguments` so bots can log or trace a per-session identifier in local development the same way they can in Pipecat Cloud. The development runner now mints a UUID at every construction site, and paths that already returned a `sessionId` to the caller (Daily `/start`, dial-in webhook) share that same UUID with the runner args instead of generating two. The SmallWebRTC `/api/offer` endpoint also accepts an optional `session_id` query parameter so the `/sessions/{session_id}/...` proxy can thread it through.

View File

@@ -180,7 +180,7 @@ async def _run_telephony_bot(websocket: WebSocket, args: argparse.Namespace):
bot_module = _get_bot_module() bot_module = _get_bot_module()
# Just pass the WebSocket - let the bot handle parsing # Just pass the WebSocket - let the bot handle parsing
runner_args = WebSocketRunnerArguments(websocket=websocket) runner_args = WebSocketRunnerArguments(websocket=websocket, session_id=str(uuid.uuid4()))
runner_args.cli_args = args runner_args.cli_args = args
await bot_module.bot(runner_args) await bot_module.bot(runner_args)
@@ -267,15 +267,25 @@ def _setup_webrtc_routes(app: FastAPI, args: argparse.Namespace):
) )
@app.post("/api/offer") @app.post("/api/offer")
async def offer(request: SmallWebRTCRequest, background_tasks: BackgroundTasks): async def offer(
request: SmallWebRTCRequest,
background_tasks: BackgroundTasks,
session_id: str | None = None,
):
"""Handle WebRTC offer requests via SmallWebRTCRequestHandler.""" """Handle WebRTC offer requests via SmallWebRTCRequestHandler."""
# When called via the /sessions/{session_id}/api/offer proxy the
# session_id is threaded through; for direct /api/offer calls we mint
# one so bots see a stable identifier in either path.
resolved_session_id = session_id or str(uuid.uuid4())
# Prepare runner arguments with the callback to run your bot # Prepare runner arguments with the callback to run your bot
async def webrtc_connection_callback(connection: SmallWebRTCConnection): async def webrtc_connection_callback(connection: SmallWebRTCConnection):
bot_module = _get_bot_module() bot_module = _get_bot_module()
runner_args = SmallWebRTCRunnerArguments( runner_args = SmallWebRTCRunnerArguments(
webrtc_connection=connection, body=request.request_data webrtc_connection=connection,
body=request.request_data,
session_id=resolved_session_id,
) )
runner_args.cli_args = args runner_args.cli_args = args
background_tasks.add_task(bot_module.bot, runner_args) background_tasks.add_task(bot_module.bot, runner_args)
@@ -343,7 +353,7 @@ def _setup_webrtc_routes(app: FastAPI, args: argparse.Namespace):
or request_data.get("requestData") or request_data.get("requestData")
or active_session, or active_session,
) )
return await offer(webrtc_request, background_tasks) return await offer(webrtc_request, background_tasks, session_id=session_id)
elif request.method == HTTPMethod.PATCH.value: elif request.method == HTTPMethod.PATCH.value:
patch_request = SmallWebRTCPatchRequest( patch_request = SmallWebRTCPatchRequest(
pc_id=request_data["pc_id"], pc_id=request_data["pc_id"],
@@ -490,7 +500,9 @@ def _setup_whatsapp_routes(app: FastAPI, args: argparse.Namespace):
connection: The established WebRTC connection connection: The established WebRTC connection
""" """
bot_module = _get_bot_module() bot_module = _get_bot_module()
runner_args = SmallWebRTCRunnerArguments(webrtc_connection=connection) runner_args = SmallWebRTCRunnerArguments(
webrtc_connection=connection, session_id=str(uuid.uuid4())
)
runner_args.cli_args = args runner_args.cli_args = args
background_tasks.add_task(bot_module.bot, runner_args) background_tasks.add_task(bot_module.bot, runner_args)
@@ -554,7 +566,9 @@ def _setup_daily_routes(app: FastAPI, args: argparse.Namespace):
# Start the bot in the background with empty body for GET requests # Start the bot in the background with empty body for GET requests
bot_module = _get_bot_module() bot_module = _get_bot_module()
runner_args = DailyRunnerArguments(room_url=room_url, token=token) runner_args = DailyRunnerArguments(
room_url=room_url, token=token, session_id=str(uuid.uuid4())
)
runner_args.cli_args = args runner_args.cli_args = args
asyncio.create_task(bot_module.bot(runner_args)) asyncio.create_task(bot_module.bot(runner_args))
return RedirectResponse(room_url) return RedirectResponse(room_url)
@@ -590,6 +604,7 @@ def _setup_daily_routes(app: FastAPI, args: argparse.Namespace):
existing_room_url = os.getenv("DAILY_ROOM_URL") existing_room_url = os.getenv("DAILY_ROOM_URL")
session_id = str(uuid.uuid4())
result = None result = None
# Configure room if: # Configure room if:
@@ -638,14 +653,16 @@ def _setup_daily_routes(app: FastAPI, args: argparse.Namespace):
room_properties=room_properties, room_properties=room_properties,
token_properties=token_properties, token_properties=token_properties,
) )
runner_args = DailyRunnerArguments(room_url=room_url, token=token, body=body) runner_args = DailyRunnerArguments(
room_url=room_url, token=token, body=body, session_id=session_id
)
result = { result = {
"dailyRoom": room_url, "dailyRoom": room_url,
"dailyToken": token, "dailyToken": token,
"sessionId": str(uuid.uuid4()), "sessionId": session_id,
} }
else: else:
runner_args = RunnerArguments(body=body) runner_args = RunnerArguments(body=body, session_id=session_id)
# Update CLI args. # Update CLI args.
runner_args.cli_args = args runner_args.cli_args = args
@@ -749,20 +766,21 @@ def _setup_daily_routes(app: FastAPI, args: argparse.Namespace):
daily_api_url=daily_api_url, daily_api_url=daily_api_url,
) )
# Generate session ID for both the runner args and the response
session_id = str(uuid.uuid4())
# Start bot with dial-in context # Start bot with dial-in context
bot_module = _get_bot_module() bot_module = _get_bot_module()
runner_args = DailyRunnerArguments( runner_args = DailyRunnerArguments(
room_url=room_config.room_url, room_url=room_config.room_url,
token=room_config.token, token=room_config.token,
body=request_body.model_dump(), body=request_body.model_dump(),
session_id=session_id,
) )
runner_args.cli_args = args runner_args.cli_args = args
asyncio.create_task(bot_module.bot(runner_args)) asyncio.create_task(bot_module.bot(runner_args))
# Generate session ID
session_id = str(uuid.uuid4())
# Return response matching Pipecat Cloud format # Return response matching Pipecat Cloud format
return { return {
"dailyRoom": room_config.room_url, "dailyRoom": room_config.room_url,
@@ -838,7 +856,9 @@ async def _run_daily_direct(args: argparse.Namespace):
room_url, token = await configure(session, room_exp_duration=PIPECAT_ROOM_EXP_HOURS) room_url, token = await configure(session, room_exp_duration=PIPECAT_ROOM_EXP_HOURS)
# Direct connections have no request body, so use empty dict # Direct connections have no request body, so use empty dict
runner_args = DailyRunnerArguments(room_url=room_url, token=token) runner_args = DailyRunnerArguments(
room_url=room_url, token=token, session_id=str(uuid.uuid4())
)
runner_args.handle_sigint = True runner_args.handle_sigint = True
runner_args.cli_args = args runner_args.cli_args = args

View File

@@ -58,13 +58,25 @@ class DailyDialinRequest(BaseModel):
@dataclass @dataclass
class RunnerArguments: class RunnerArguments:
"""Base class for runner session arguments.""" """Base class for runner session arguments.
Parameters:
handle_sigint: Whether the bot should install a SIGINT handler.
handle_sigterm: Whether the bot should install a SIGTERM handler.
pipeline_idle_timeout_secs: Seconds the pipeline may stay idle before
shutting down.
body: Optional request body data passed from the runner entry point.
session_id: Identifier for this bot session.
cli_args: Parsed CLI arguments from the runner, when launched via the
development runner.
"""
# Use kw_only so subclasses don't need to worry about ordering. # Use kw_only so subclasses don't need to worry about ordering.
handle_sigint: bool = field(init=False, kw_only=True) handle_sigint: bool = field(init=False, kw_only=True)
handle_sigterm: bool = field(init=False, kw_only=True) handle_sigterm: bool = field(init=False, kw_only=True)
pipeline_idle_timeout_secs: int = field(init=False, kw_only=True) pipeline_idle_timeout_secs: int = field(init=False, kw_only=True)
body: Any | None = field(default_factory=dict, kw_only=True) body: Any | None = field(default_factory=dict, kw_only=True)
session_id: str | None = field(default=None, kw_only=True)
cli_args: argparse.Namespace | None = field(default=None, init=False, kw_only=True) cli_args: argparse.Namespace | None = field(default=None, init=False, kw_only=True)
def __post_init__(self): def __post_init__(self):