Enhance WebSocket session configuration by introducing an optional config.resolved event, which provides a public snapshot of the session's configuration. Update the API reference documentation to clarify the conditions under which this event is emitted and the details it includes. Modify session management to respect the new setting for emitting configuration details, ensuring sensitive information remains secure. Update tests to validate the new behavior and ensure compliance with the updated configuration schema.

This commit is contained in:
Xin Wang
2026-03-01 23:08:44 +08:00
parent 2418df80e5
commit 3643431565
6 changed files with 165 additions and 58 deletions

View File

@@ -1,7 +1,6 @@
"""Session management for active calls."""
import asyncio
import hashlib
import json
import re
import time
@@ -383,15 +382,18 @@ class Session:
audio=message.audio.model_dump() if message.audio else {},
)
)
await self._send_event(
ev(
"config.resolved",
trackId=self.TRACK_CONTROL,
config=self._build_config_resolved(metadata),
if settings.ws_emit_config_resolved:
await self._send_event(
ev(
"config.resolved",
trackId=self.TRACK_CONTROL,
config=self._build_config_resolved(metadata),
)
)
)
else:
logger.debug("Session {} skipped config.resolved (ws_emit_config_resolved=false)", self.id)
# Emit opener only after frontend has received session.started/config events.
# Emit opener only after frontend has received session.started (and optional config event).
await self.pipeline.emit_initial_greeting()
async def _handle_session_stop(self, reason: Optional[str]) -> None:
@@ -1118,25 +1120,36 @@ class Session:
return sanitized, None
def _build_config_resolved(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
"""Build public resolved config payload (secrets removed)."""
system_prompt = str(metadata.get("systemPrompt") or self.pipeline.conversation.system_prompt or "")
prompt_hash = hashlib.sha256(system_prompt.encode("utf-8")).hexdigest() if system_prompt else None
"""Build public resolved config payload (SaaS-safe, no internal runtime details)."""
runtime = self.pipeline.resolved_runtime_config()
runtime_output = runtime.get("output", {}) if isinstance(runtime, dict) else {}
output_mode = str(runtime_output.get("mode") or "").strip().lower() if isinstance(runtime_output, dict) else ""
if output_mode not in {"audio", "text"}:
output_mode = "audio"
return {
"appId": metadata.get("assistantId"),
"channel": metadata.get("channel"),
"configVersionId": metadata.get("configVersionId") or metadata.get("config_version_id"),
"prompt": {"sha256": prompt_hash},
"output": runtime.get("output", {}),
"services": runtime.get("services", {}),
"tools": runtime.get("tools", {}),
tools_allowlist: List[str] = []
runtime_tools = runtime.get("tools", {}) if isinstance(runtime, dict) else {}
if isinstance(runtime_tools, dict):
allowlist = runtime_tools.get("allowlist", [])
if isinstance(allowlist, list):
tools_allowlist = [str(item) for item in allowlist if item is not None and str(item).strip()]
resolved: Dict[str, Any] = {
"output": {"mode": output_mode},
"tools": {
"enabled": bool(tools_allowlist),
"count": len(tools_allowlist),
},
"tracks": {
"audio_in": self.TRACK_AUDIO_IN,
"audio_out": self.TRACK_AUDIO_OUT,
"control": self.TRACK_CONTROL,
},
}
if metadata.get("channel") is not None:
resolved["channel"] = metadata.get("channel")
return resolved
def _extract_json_obj(self, text: str) -> Optional[Dict[str, Any]]:
"""Best-effort extraction of a JSON object from freeform text."""