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],
|
||||
|
||||
@@ -65,7 +65,11 @@ Rules:
|
||||
"mode": "audio"
|
||||
},
|
||||
"systemPrompt": "You are concise.",
|
||||
"greeting": "Hi, how can I help?"
|
||||
"greeting": "Hi, how can I help?",
|
||||
"dynamicVariables": {
|
||||
"customer_name": "Alice",
|
||||
"plan_tier": "Pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -74,6 +78,13 @@ Rules:
|
||||
- Client-side `metadata.services` is ignored.
|
||||
- Service config (including secrets) is resolved server-side (env/backend).
|
||||
- Client should pass stable IDs (`appId`, `channel`, `configVersionId`) plus small runtime overrides (e.g. `output`, `bargeIn`, greeting/prompt style hints).
|
||||
- `metadata.dynamicVariables` is optional and must be an object of string key/value pairs.
|
||||
- Key pattern: `^[a-zA-Z_][a-zA-Z0-9_]{0,63}$`
|
||||
- Max entries: 30
|
||||
- Max value length: 1000 chars
|
||||
- Placeholder format in `systemPrompt` and `greeting`: `{{variable_name}}`.
|
||||
- Missing referenced placeholders reject `session.start` with `protocol.dynamic_variables_missing`.
|
||||
- Invalid `dynamicVariables` payload rejects `session.start` with `protocol.dynamic_variables_invalid`.
|
||||
|
||||
Text-only mode:
|
||||
- Set `metadata.output.mode = "text"`.
|
||||
|
||||
@@ -105,7 +105,11 @@
|
||||
"mode": "audio"
|
||||
},
|
||||
"systemPrompt": "你是简洁助手",
|
||||
"greeting": "你好,我能帮你什么?"
|
||||
"greeting": "你好,我能帮你什么?",
|
||||
"dynamicVariables": {
|
||||
"customer_name": "Alice",
|
||||
"plan_tier": "Pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -135,12 +139,22 @@
|
||||
- `bargeIn`
|
||||
- `knowledge`
|
||||
- `knowledgeBaseId`
|
||||
- `openerAudio`
|
||||
- `history`
|
||||
- `userId`
|
||||
- `assistantId`
|
||||
- `source`
|
||||
- `dynamicVariables`
|
||||
- 客户端传入 `metadata.services` 会被忽略(服务端会记录 warning),服务配置由后端/环境变量决定。
|
||||
|
||||
`metadata.dynamicVariables` 规则:
|
||||
- 可选;必须是 `object<string,string>`。
|
||||
- key 正则:`^[a-zA-Z_][a-zA-Z0-9_]{0,63}$`
|
||||
- 最多 30 个变量,单个 value 最长 1000 字符。
|
||||
- `systemPrompt` / `greeting` 中支持占位符语法:`{{variable_name}}`。
|
||||
- 若模板引用了缺失变量,`session.start` 会被拒绝,错误码 `protocol.dynamic_variables_missing`。
|
||||
- 若 `dynamicVariables` 结构/内容非法,`session.start` 会被拒绝,错误码 `protocol.dynamic_variables_invalid`。
|
||||
|
||||
`output.mode` 用法:
|
||||
- `"audio"`(默认语音输出)
|
||||
- `"text"`(纯文本输出)
|
||||
@@ -517,4 +531,3 @@ Server <- output.audio.end
|
||||
Client -> session.stop
|
||||
Server <- session.stopped
|
||||
```
|
||||
|
||||
|
||||
96
engine/tests/test_dynamic_variables.py
Normal file
96
engine/tests/test_dynamic_variables.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from core.session import Session
|
||||
|
||||
|
||||
def _session() -> Session:
|
||||
# Skip heavyweight runtime initialization (VAD model, services, transport).
|
||||
return Session.__new__(Session)
|
||||
|
||||
|
||||
def test_apply_dynamic_variables_substitutes_prompt_and_greeting():
|
||||
session = _session()
|
||||
metadata = {
|
||||
"systemPrompt": "Hello {{customer_name}}, plan={{plan_tier}}.",
|
||||
"greeting": "Hi {{customer_name}}",
|
||||
}
|
||||
client_metadata = {
|
||||
"dynamicVariables": {
|
||||
"customer_name": "Alice",
|
||||
"plan_tier": "Pro",
|
||||
}
|
||||
}
|
||||
|
||||
resolved, error = session._apply_dynamic_variables(metadata, client_metadata)
|
||||
|
||||
assert error is None
|
||||
assert resolved["systemPrompt"] == "Hello Alice, plan=Pro."
|
||||
assert resolved["greeting"] == "Hi Alice"
|
||||
|
||||
|
||||
def test_apply_dynamic_variables_missing_value_rejects_session_start():
|
||||
session = _session()
|
||||
metadata = {
|
||||
"systemPrompt": "Hello {{customer_name}}",
|
||||
"greeting": "Plan {{plan_tier}}",
|
||||
}
|
||||
client_metadata = {
|
||||
"dynamicVariables": {
|
||||
"customer_name": "Alice",
|
||||
}
|
||||
}
|
||||
|
||||
_, error = session._apply_dynamic_variables(metadata, client_metadata)
|
||||
|
||||
assert error is not None
|
||||
assert error["code"] == "protocol.dynamic_variables_missing"
|
||||
assert "plan_tier" in error["message"]
|
||||
|
||||
|
||||
def test_apply_dynamic_variables_requires_string_values():
|
||||
session = _session()
|
||||
metadata = {
|
||||
"systemPrompt": "Hello {{customer_name}}",
|
||||
"greeting": "Hi",
|
||||
}
|
||||
client_metadata = {
|
||||
"dynamicVariables": {
|
||||
"customer_name": 123,
|
||||
}
|
||||
}
|
||||
|
||||
_, error = session._apply_dynamic_variables(metadata, client_metadata)
|
||||
|
||||
assert error is not None
|
||||
assert error["code"] == "protocol.dynamic_variables_invalid"
|
||||
assert "must be a string" in error["message"]
|
||||
|
||||
|
||||
def test_apply_dynamic_variables_missing_object_rejects_when_template_uses_placeholders():
|
||||
session = _session()
|
||||
metadata = {
|
||||
"systemPrompt": "Hello {{customer_name}}",
|
||||
"greeting": "Hi",
|
||||
}
|
||||
|
||||
_, error = session._apply_dynamic_variables(metadata, {})
|
||||
|
||||
assert error is not None
|
||||
assert error["code"] == "protocol.dynamic_variables_missing"
|
||||
assert "customer_name" in error["message"]
|
||||
|
||||
|
||||
def test_apply_dynamic_variables_no_placeholder_keeps_metadata():
|
||||
session = _session()
|
||||
metadata = {
|
||||
"systemPrompt": "Hello there",
|
||||
"greeting": "Hi",
|
||||
}
|
||||
client_metadata = {
|
||||
"dynamicVariables": {
|
||||
"customer_name": "Alice",
|
||||
}
|
||||
}
|
||||
|
||||
resolved, error = session._apply_dynamic_variables(metadata, client_metadata)
|
||||
|
||||
assert error is None
|
||||
assert resolved == metadata
|
||||
@@ -1528,6 +1528,29 @@ const TOOL_PARAMETER_HINTS: Record<string, any> = {
|
||||
const getDefaultToolParameters = (toolId: string) =>
|
||||
TOOL_PARAMETER_HINTS[toolId] || { type: 'object', properties: {} };
|
||||
|
||||
type DynamicVariableEntry = {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const DYNAMIC_VARIABLE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/;
|
||||
const DYNAMIC_VARIABLE_PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
||||
const DYNAMIC_VARIABLE_MAX_ITEMS = 30;
|
||||
const DYNAMIC_VARIABLE_MAX_VALUE_LENGTH = 1000;
|
||||
|
||||
const extractDynamicTemplateKeys = (text: string): string[] => {
|
||||
if (!text) return [];
|
||||
const keys = new Set<string>();
|
||||
const pattern = new RegExp(DYNAMIC_VARIABLE_PLACEHOLDER_RE);
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
if (match[1]) {
|
||||
keys.add(match[1]);
|
||||
}
|
||||
}
|
||||
return Array.from(keys);
|
||||
};
|
||||
|
||||
// Stable transcription log so the scroll container is not recreated on every render (avoids scroll jumping)
|
||||
const TranscriptionLog: React.FC<{
|
||||
scrollRef: React.RefObject<HTMLDivElement | null>;
|
||||
@@ -1619,12 +1642,30 @@ export const DebugDrawer: React.FC<{
|
||||
const [captureConfigOpen, setCaptureConfigOpen] = useState(false);
|
||||
const [captureConfigView, setCaptureConfigView] = useState<string>('');
|
||||
const [settingsDrawerOpen, setSettingsDrawerOpen] = useState(false);
|
||||
const [dynamicVariables, setDynamicVariables] = useState<DynamicVariableEntry[]>([]);
|
||||
const dynamicVariableSeqRef = useRef(0);
|
||||
const [wsUrl, setWsUrl] = useState<string>(() => {
|
||||
const fromStorage = localStorage.getItem('debug_ws_url');
|
||||
if (fromStorage) return fromStorage;
|
||||
const defaultHost = window.location.hostname || 'localhost';
|
||||
return `ws://${defaultHost}:8000/ws`;
|
||||
});
|
||||
const nextDynamicVariableId = () => {
|
||||
dynamicVariableSeqRef.current += 1;
|
||||
return `var_${dynamicVariableSeqRef.current}`;
|
||||
};
|
||||
const isDynamicVariablesLocked =
|
||||
wsStatus === 'connecting'
|
||||
|| wsStatus === 'ready'
|
||||
|| callStatus === 'calling'
|
||||
|| callStatus === 'active'
|
||||
|| textSessionStarted;
|
||||
const requiredTemplateVariableKeys = useMemo(() => {
|
||||
const keys = new Set<string>();
|
||||
extractDynamicTemplateKeys(String(assistant.prompt || '')).forEach((key) => keys.add(key));
|
||||
extractDynamicTemplateKeys(String(assistant.opener || '')).forEach((key) => keys.add(key));
|
||||
return Array.from(keys).sort();
|
||||
}, [assistant.opener, assistant.prompt]);
|
||||
|
||||
// Media State
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
@@ -1714,6 +1755,11 @@ export const DebugDrawer: React.FC<{
|
||||
wsStatusRef.current = wsStatus;
|
||||
}, [wsStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
dynamicVariableSeqRef.current = 0;
|
||||
setDynamicVariables([]);
|
||||
}, [assistant.id, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('debug_audio_aec', aecEnabled ? '1' : '0');
|
||||
}, [aecEnabled]);
|
||||
@@ -2068,7 +2114,9 @@ export const DebugDrawer: React.FC<{
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setMessages(prev => [...prev, { role: 'model', text: "Error: Failed to connect to AI service." }]);
|
||||
const errMessage = (e as Error)?.message || 'Failed to connect to AI service.';
|
||||
setMessages(prev => [...prev, { role: 'model', text: `Error: ${errMessage}` }]);
|
||||
setWsError(errMessage);
|
||||
setIsLoading(false);
|
||||
} finally {
|
||||
if (mode !== 'text') setIsLoading(false);
|
||||
@@ -2096,6 +2144,82 @@ export const DebugDrawer: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const addDynamicVariableRow = () => {
|
||||
if (isDynamicVariablesLocked) return;
|
||||
setDynamicVariables((prev) => {
|
||||
if (prev.length >= DYNAMIC_VARIABLE_MAX_ITEMS) return prev;
|
||||
return [...prev, { id: nextDynamicVariableId(), key: '', value: '' }];
|
||||
});
|
||||
};
|
||||
|
||||
const updateDynamicVariableRow = (rowId: string, field: 'key' | 'value', value: string) => {
|
||||
if (isDynamicVariablesLocked) return;
|
||||
setDynamicVariables((prev) => prev.map((item) => (item.id === rowId ? { ...item, [field]: value } : item)));
|
||||
};
|
||||
|
||||
const removeDynamicVariableRow = (rowId: string) => {
|
||||
if (isDynamicVariablesLocked) return;
|
||||
setDynamicVariables((prev) => prev.filter((item) => item.id !== rowId));
|
||||
};
|
||||
|
||||
const buildDynamicVariablesPayload = (): { variables: Record<string, string>; error?: string } => {
|
||||
const variables: Record<string, string> = {};
|
||||
const nonEmptyRows = dynamicVariables
|
||||
.map((row, index) => ({ ...row, index, key: row.key.trim() }))
|
||||
.filter((row) => row.key !== '' || row.value !== '');
|
||||
|
||||
if (nonEmptyRows.length > DYNAMIC_VARIABLE_MAX_ITEMS) {
|
||||
return {
|
||||
variables,
|
||||
error: `Dynamic variable count cannot exceed ${DYNAMIC_VARIABLE_MAX_ITEMS}.`,
|
||||
};
|
||||
}
|
||||
|
||||
for (const row of nonEmptyRows) {
|
||||
if (!row.key) {
|
||||
return {
|
||||
variables,
|
||||
error: `Dynamic variable row ${row.index + 1} is missing key.`,
|
||||
};
|
||||
}
|
||||
if (!DYNAMIC_VARIABLE_KEY_RE.test(row.key)) {
|
||||
return {
|
||||
variables,
|
||||
error: `Invalid dynamic variable key "${row.key}". Use letters, digits, underscore and start with a letter/underscore.`,
|
||||
};
|
||||
}
|
||||
if (row.value === '') {
|
||||
return {
|
||||
variables,
|
||||
error: `Dynamic variable "${row.key}" is missing value.`,
|
||||
};
|
||||
}
|
||||
if (row.value.length > DYNAMIC_VARIABLE_MAX_VALUE_LENGTH) {
|
||||
return {
|
||||
variables,
|
||||
error: `Dynamic variable "${row.key}" exceeds ${DYNAMIC_VARIABLE_MAX_VALUE_LENGTH} characters.`,
|
||||
};
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(variables, row.key)) {
|
||||
return {
|
||||
variables,
|
||||
error: `Duplicate dynamic variable key "${row.key}".`,
|
||||
};
|
||||
}
|
||||
variables[row.key] = row.value;
|
||||
}
|
||||
|
||||
const missingTemplateKeys = requiredTemplateVariableKeys.filter((key) => !Object.prototype.hasOwnProperty.call(variables, key));
|
||||
if (missingTemplateKeys.length > 0) {
|
||||
return {
|
||||
variables,
|
||||
error: `Missing required dynamic variables: ${missingTemplateKeys.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { variables };
|
||||
};
|
||||
|
||||
const buildLocalResolvedRuntime = () => {
|
||||
const warnings: string[] = [];
|
||||
const services: Record<string, any> = {};
|
||||
@@ -2203,11 +2327,18 @@ export const DebugDrawer: React.FC<{
|
||||
};
|
||||
|
||||
const fetchRuntimeMetadata = async (): Promise<Record<string, any>> => {
|
||||
const dynamicVariablesResult = buildDynamicVariablesPayload();
|
||||
if (dynamicVariablesResult.error) {
|
||||
throw new Error(dynamicVariablesResult.error);
|
||||
}
|
||||
const localResolved = buildLocalResolvedRuntime();
|
||||
const mergedMetadata: Record<string, any> = {
|
||||
...localResolved.sessionStartMetadata,
|
||||
...(sessionMetadataExtras || {}),
|
||||
};
|
||||
if (Object.keys(dynamicVariablesResult.variables).length > 0) {
|
||||
mergedMetadata.dynamicVariables = dynamicVariablesResult.variables;
|
||||
}
|
||||
// Engine resolves trusted runtime config by top-level assistant/app ID.
|
||||
// Keep these IDs at metadata root so backend /assistants/{id}/config is reachable.
|
||||
if (!mergedMetadata.assistantId && assistant.id) {
|
||||
@@ -2654,6 +2785,70 @@ export const DebugDrawer: React.FC<{
|
||||
Auto Gain Control (AGC)
|
||||
</label>
|
||||
</div>
|
||||
<div className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground">Dynamic Variables</p>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6"
|
||||
onClick={addDynamicVariableRow}
|
||||
disabled={isDynamicVariablesLocked || dynamicVariables.length >= DYNAMIC_VARIABLE_MAX_ITEMS}
|
||||
title={isDynamicVariablesLocked ? 'Disable editing while session is active' : 'Add variable'}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Use placeholders like {'{{customer_name}}'} in prompt/opener.
|
||||
</p>
|
||||
{requiredTemplateVariableKeys.length > 0 && (
|
||||
<p className="text-[11px] text-amber-300/90">
|
||||
Required: {requiredTemplateVariableKeys.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
{dynamicVariables.length === 0 ? (
|
||||
<div className="text-[11px] text-muted-foreground/80 border border-dashed border-white/10 rounded-md px-2 py-2">
|
||||
No variables added.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-52 overflow-auto pr-1">
|
||||
{dynamicVariables.map((row, index) => (
|
||||
<div key={row.id} className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
|
||||
<Input
|
||||
value={row.key}
|
||||
onChange={(e) => updateDynamicVariableRow(row.id, 'key', e.target.value)}
|
||||
placeholder={`key_${index + 1}`}
|
||||
disabled={isDynamicVariablesLocked}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={row.value}
|
||||
onChange={(e) => updateDynamicVariableRow(row.id, 'value', e.target.value)}
|
||||
placeholder="value"
|
||||
disabled={isDynamicVariablesLocked}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 text-muted-foreground hover:text-red-300"
|
||||
onClick={() => removeDynamicVariableRow(row.id)}
|
||||
disabled={isDynamicVariablesLocked}
|
||||
title="Remove variable"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{isDynamicVariablesLocked && (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Editing is locked while conversation is starting/active.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-md border border-white/10 bg-black/30">
|
||||
<button
|
||||
className="w-full px-3 py-2 text-left text-xs text-muted-foreground hover:text-foreground flex items-center justify-between"
|
||||
|
||||
Reference in New Issue
Block a user