Add dynamic variables support in session management and UI components. Implement validation rules for dynamic variables in metadata, including key format and value constraints. Enhance session start handling to manage dynamic variable errors. Update documentation and tests to reflect new functionality.
This commit is contained in:
@@ -28,6 +28,11 @@ from models.ws_v1 import (
|
||||
ToolCallResultsMessage,
|
||||
)
|
||||
|
||||
_DYNAMIC_VARIABLE_KEY_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,63}$")
|
||||
_DYNAMIC_VARIABLE_PLACEHOLDER_RE = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}")
|
||||
_DYNAMIC_VARIABLE_MAX_ITEMS = 30
|
||||
_DYNAMIC_VARIABLE_VALUE_MAX_CHARS = 1000
|
||||
|
||||
|
||||
class WsSessionState(str, Enum):
|
||||
"""Protocol state machine for WS sessions."""
|
||||
@@ -60,6 +65,7 @@ class Session:
|
||||
"knowledge",
|
||||
"knowledgeBaseId",
|
||||
"openerAudio",
|
||||
"dynamicVariables",
|
||||
"history",
|
||||
"userId",
|
||||
"assistantId",
|
||||
@@ -332,6 +338,16 @@ class Session:
|
||||
)
|
||||
metadata = self._merge_runtime_metadata(server_runtime, self._sanitize_untrusted_runtime_metadata(workflow_runtime))
|
||||
metadata = self._merge_runtime_metadata(metadata, client_runtime)
|
||||
metadata, dynamic_var_error = self._apply_dynamic_variables(metadata, raw_metadata)
|
||||
if dynamic_var_error:
|
||||
await self._send_error(
|
||||
"client",
|
||||
dynamic_var_error["message"],
|
||||
dynamic_var_error["code"],
|
||||
stage="protocol",
|
||||
retryable=False,
|
||||
)
|
||||
return
|
||||
|
||||
# Create history call record early so later turn callbacks can append transcripts.
|
||||
await self._start_history_bridge(metadata)
|
||||
@@ -798,6 +814,132 @@ class Session:
|
||||
merged[key] = value
|
||||
return merged
|
||||
|
||||
def _extract_dynamic_template_keys(self, text: Any) -> List[str]:
|
||||
"""Collect placeholder keys from a template string."""
|
||||
if text is None:
|
||||
return []
|
||||
keys: List[str] = []
|
||||
seen = set()
|
||||
for match in _DYNAMIC_VARIABLE_PLACEHOLDER_RE.finditer(str(text)):
|
||||
key = str(match.group(1) or "")
|
||||
if key and key not in seen:
|
||||
seen.add(key)
|
||||
keys.append(key)
|
||||
return keys
|
||||
|
||||
def _render_dynamic_template(
|
||||
self,
|
||||
template: str,
|
||||
dynamic_vars: Dict[str, str],
|
||||
) -> tuple[str, List[str]]:
|
||||
"""Render one template text and return missing keys if any."""
|
||||
missing = set()
|
||||
|
||||
def _replace(match: re.Match) -> str:
|
||||
key = str(match.group(1) or "")
|
||||
if key not in dynamic_vars:
|
||||
missing.add(key)
|
||||
return match.group(0)
|
||||
return dynamic_vars[key]
|
||||
|
||||
rendered = _DYNAMIC_VARIABLE_PLACEHOLDER_RE.sub(_replace, str(template or ""))
|
||||
return rendered, sorted(missing)
|
||||
|
||||
def _apply_dynamic_variables(
|
||||
self,
|
||||
metadata: Dict[str, Any],
|
||||
client_metadata: Dict[str, Any],
|
||||
) -> tuple[Dict[str, Any], Optional[Dict[str, str]]]:
|
||||
"""
|
||||
Apply session.start metadata.dynamicVariables to prompt/greeting templates.
|
||||
|
||||
Returns:
|
||||
tuple(merged_metadata, error_payload)
|
||||
error_payload shape: {"code": "...", "message": "..."}
|
||||
"""
|
||||
merged = dict(metadata or {})
|
||||
raw_dynamic_vars = client_metadata.get("dynamicVariables")
|
||||
dynamic_vars: Dict[str, str] = {}
|
||||
|
||||
if raw_dynamic_vars is not None:
|
||||
if not isinstance(raw_dynamic_vars, dict):
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_invalid",
|
||||
"message": "metadata.dynamicVariables must be an object with string key/value pairs",
|
||||
}
|
||||
if len(raw_dynamic_vars) > _DYNAMIC_VARIABLE_MAX_ITEMS:
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_invalid",
|
||||
"message": f"metadata.dynamicVariables cannot exceed {_DYNAMIC_VARIABLE_MAX_ITEMS} entries",
|
||||
}
|
||||
|
||||
for raw_key, raw_value in raw_dynamic_vars.items():
|
||||
if not isinstance(raw_key, str):
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_invalid",
|
||||
"message": "metadata.dynamicVariables keys must be strings",
|
||||
}
|
||||
key = raw_key.strip()
|
||||
if not _DYNAMIC_VARIABLE_KEY_RE.match(key):
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_invalid",
|
||||
"message": (
|
||||
"Invalid dynamic variable key "
|
||||
f"'{raw_key}'. Expected ^[a-zA-Z_][a-zA-Z0-9_]{{0,63}}$"
|
||||
),
|
||||
}
|
||||
if key in dynamic_vars:
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_invalid",
|
||||
"message": f"Duplicate dynamic variable key '{key}'",
|
||||
}
|
||||
if not isinstance(raw_value, str):
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_invalid",
|
||||
"message": f"Dynamic variable '{key}' value must be a string",
|
||||
}
|
||||
if len(raw_value) > _DYNAMIC_VARIABLE_VALUE_MAX_CHARS:
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_invalid",
|
||||
"message": (
|
||||
f"Dynamic variable '{key}' exceeds "
|
||||
f"{_DYNAMIC_VARIABLE_VALUE_MAX_CHARS} characters"
|
||||
),
|
||||
}
|
||||
dynamic_vars[key] = raw_value
|
||||
|
||||
template_keys = set(self._extract_dynamic_template_keys(merged.get("systemPrompt")))
|
||||
template_keys.update(self._extract_dynamic_template_keys(merged.get("greeting")))
|
||||
if not template_keys:
|
||||
return merged, None
|
||||
if raw_dynamic_vars is None:
|
||||
missing = ", ".join(sorted(template_keys))
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_missing",
|
||||
"message": f"Missing dynamic variables for placeholders: {missing}",
|
||||
}
|
||||
|
||||
missing_keys = sorted([key for key in template_keys if key not in dynamic_vars])
|
||||
if missing_keys:
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_missing",
|
||||
"message": f"Missing dynamic variables for placeholders: {', '.join(missing_keys)}",
|
||||
}
|
||||
|
||||
for field in ("systemPrompt", "greeting"):
|
||||
value = merged.get(field)
|
||||
if value is None:
|
||||
continue
|
||||
rendered, unresolved = self._render_dynamic_template(str(value), dynamic_vars)
|
||||
if unresolved:
|
||||
return merged, {
|
||||
"code": "protocol.dynamic_variables_missing",
|
||||
"message": f"Missing dynamic variables for placeholders: {', '.join(unresolved)}",
|
||||
}
|
||||
merged[field] = rendered
|
||||
|
||||
return merged, None
|
||||
|
||||
async def _load_server_runtime_metadata(
|
||||
self,
|
||||
client_metadata: Dict[str, Any],
|
||||
|
||||
Reference in New Issue
Block a user