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