Add dynamic variable support to Assistant model and related components
- Introduce dynamic variable definitions in AssistantConfig and Assistant models, allowing for flexible prompt customization. - Implement validation for dynamic variable names and types in the schema. - Update backend services and routes to handle dynamic variables in assistant configurations and runtime processing. - Enhance frontend components to support dynamic variable definitions, including a new editor for managing variables. - Add tests to ensure proper functionality and validation of dynamic variables in various scenarios.
This commit is contained in:
@@ -2,8 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from urllib.parse import quote
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from models import AssistantConfig
|
||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||
from pipecat.frames.frames import OutputTransportMessageUrgentFrame, TTSSpeakFrame
|
||||
@@ -16,6 +19,11 @@ from pipecat.services.llm_service import (
|
||||
from pipecat.utils.time import time_now_iso8601
|
||||
|
||||
from services.brains.base import BaseBrain, BrainRuntime, BrainSpec
|
||||
from services.runtime_variables import (
|
||||
DynamicVariableError,
|
||||
DynamicVariableStore,
|
||||
value_at_path,
|
||||
)
|
||||
|
||||
|
||||
class PromptBrain(BaseBrain):
|
||||
@@ -25,21 +33,184 @@ class PromptBrain(BaseBrain):
|
||||
owns_context=True,
|
||||
)
|
||||
|
||||
def __init__(self, cfg: AssistantConfig):
|
||||
self._cfg = cfg
|
||||
self._dynamic_enabled = True
|
||||
self._store = DynamicVariableStore.from_config(cfg)
|
||||
self._runtime: BrainRuntime | None = None
|
||||
|
||||
async def greeting(self, cfg: AssistantConfig) -> str:
|
||||
return self._store.render(cfg.greeting) if self._dynamic_enabled else cfg.greeting
|
||||
|
||||
def system_prompt(self, cfg: AssistantConfig) -> str:
|
||||
return self._store.render(cfg.prompt) if self._dynamic_enabled else cfg.prompt
|
||||
|
||||
def build_llm(self, cfg: AssistantConfig, context: LLMContext) -> FrameProcessor:
|
||||
from services.pipecat.service_factory import create_llm
|
||||
|
||||
return create_llm(cfg)
|
||||
|
||||
async def setup(self, cfg: AssistantConfig, runtime: BrainRuntime) -> None:
|
||||
self._runtime = runtime
|
||||
schemas: list[FunctionSchema] = []
|
||||
for tool in cfg.tools:
|
||||
if tool.type != "end_call":
|
||||
if tool.type == "end_call":
|
||||
schema, handler = self._make_end_call_tool(tool, runtime)
|
||||
elif tool.type == "http":
|
||||
schema, handler = self._make_http_tool(tool, runtime)
|
||||
else:
|
||||
continue
|
||||
schema, handler = self._make_end_call_tool(tool, runtime)
|
||||
schemas.append(schema)
|
||||
runtime.llm.register_function(tool.function_name, handler)
|
||||
runtime.set_tools(schemas)
|
||||
|
||||
def record_user_message(self, content: str) -> None:
|
||||
if not self._dynamic_enabled:
|
||||
return
|
||||
self._store.record("user", content)
|
||||
self._refresh_prompt()
|
||||
|
||||
async def on_assistant_text_end(
|
||||
self,
|
||||
_turn_id: str,
|
||||
content: str,
|
||||
interrupted: bool,
|
||||
) -> None:
|
||||
if content and not interrupted:
|
||||
self._store.record("agent", content, completed_agent_turn=True)
|
||||
self._refresh_prompt()
|
||||
|
||||
def _refresh_prompt(self) -> None:
|
||||
if self._dynamic_enabled and self._runtime is not None:
|
||||
self._runtime.set_system_prompt(self._store.render(self._cfg.prompt))
|
||||
|
||||
def _make_http_tool(self, tool, runtime: BrainRuntime):
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
parameters = list(config.get("parameters") or [])
|
||||
properties = {
|
||||
str(parameter.get("name")): {
|
||||
"type": str(parameter.get("type") or "string"),
|
||||
"description": str(parameter.get("description") or ""),
|
||||
}
|
||||
for parameter in parameters
|
||||
if parameter.get("name")
|
||||
}
|
||||
required = [
|
||||
str(parameter["name"])
|
||||
for parameter in parameters
|
||||
if parameter.get("name") and parameter.get("required", True)
|
||||
]
|
||||
|
||||
tool_secrets = tool.secrets or {}
|
||||
dynamic_secrets = tool_secrets.get("dynamic_variables") or {}
|
||||
for name, value in dynamic_secrets.items():
|
||||
if not str(name).startswith("secret__"):
|
||||
raise DynamicVariableError(f"工具密钥变量必须以 secret__ 开头: {name}")
|
||||
self._store.secrets[str(name)] = str(value)
|
||||
|
||||
async def call_http(params: FunctionCallParams) -> None:
|
||||
arguments = params.arguments or {}
|
||||
url = self._store.render(str(config.get("url") or ""))
|
||||
configured_headers = self._store.render_data(
|
||||
deepcopy(config.get("headers") or {}), allow_secrets=True
|
||||
)
|
||||
secret_headers = self._store.render_data(
|
||||
deepcopy(tool_secrets.get("headers") or {}), allow_secrets=True
|
||||
)
|
||||
headers: dict[str, str] = {}
|
||||
query: dict[str, object] = {}
|
||||
body = self._store.render_data(deepcopy(config.get("body") or {}))
|
||||
|
||||
for parameter in parameters:
|
||||
name = str(parameter.get("name") or "")
|
||||
if not name or name not in arguments:
|
||||
continue
|
||||
value = arguments[name]
|
||||
location = str(parameter.get("location") or "body")
|
||||
if location == "path":
|
||||
encoded = quote(str(value), safe="")
|
||||
url = url.replace(f"{{{name}}}", encoded)
|
||||
elif location == "query":
|
||||
query[name] = value
|
||||
elif location == "header":
|
||||
headers[name] = str(value)
|
||||
else:
|
||||
body[name] = value
|
||||
|
||||
# Admin-configured headers win over model-provided arguments; secret
|
||||
# headers are applied last so an LLM can never replace credentials.
|
||||
headers.update(
|
||||
{str(key): str(value) for key, value in configured_headers.items()}
|
||||
)
|
||||
headers.update({str(key): str(value) for key, value in secret_headers.items()})
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=float(config.get("timeout_seconds") or 15),
|
||||
follow_redirects=False,
|
||||
) as client:
|
||||
response = await client.request(
|
||||
str(config.get("method") or "GET"),
|
||||
url,
|
||||
headers=headers,
|
||||
params=query,
|
||||
json=body if body else None,
|
||||
)
|
||||
response.raise_for_status()
|
||||
if len(response.content) > 1_000_000:
|
||||
raise DynamicVariableError("HTTP 工具响应超过 1 MB 限制")
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError:
|
||||
payload = {"text": response.text[:8000]}
|
||||
|
||||
updated: list[str] = []
|
||||
assignments = config.get("dynamic_variable_assignments") or {}
|
||||
for variable_name, path in assignments.items():
|
||||
try:
|
||||
value = value_at_path(payload, str(path))
|
||||
except KeyError:
|
||||
try:
|
||||
value = value_at_path({"response": payload}, str(path))
|
||||
except KeyError:
|
||||
continue
|
||||
self._store.assign(str(variable_name), value)
|
||||
updated.append(str(variable_name))
|
||||
if updated:
|
||||
self._refresh_prompt()
|
||||
await params.result_callback(
|
||||
{
|
||||
"status": "ok",
|
||||
"status_code": response.status_code,
|
||||
"data": payload,
|
||||
"updated_variables": updated,
|
||||
}
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
await params.result_callback(
|
||||
{"status": "error", "message": "HTTP 工具调用超时"}
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
await params.result_callback(
|
||||
{
|
||||
"status": "error",
|
||||
"status_code": exc.response.status_code,
|
||||
"message": "HTTP 工具返回错误状态",
|
||||
}
|
||||
)
|
||||
except (httpx.RequestError, DynamicVariableError) as exc:
|
||||
await params.result_callback(
|
||||
{"status": "error", "message": f"HTTP 工具调用失败: {exc}"}
|
||||
)
|
||||
|
||||
schema = FunctionSchema(
|
||||
name=tool.function_name,
|
||||
description=tool.description or f"调用 {tool.name}",
|
||||
properties=properties,
|
||||
required=required,
|
||||
)
|
||||
return schema, call_http
|
||||
|
||||
@staticmethod
|
||||
def _make_end_call_tool(tool, runtime: BrainRuntime):
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
|
||||
@@ -18,7 +18,7 @@ def _workflow(cfg: AssistantConfig) -> Brain:
|
||||
|
||||
|
||||
BRAIN_FACTORIES: dict[str, Callable[[AssistantConfig], Brain]] = {
|
||||
"prompt": lambda _cfg: PromptBrain(),
|
||||
"prompt": lambda cfg: PromptBrain(cfg),
|
||||
"workflow": _workflow,
|
||||
"dify": lambda _cfg: DifyBrain(),
|
||||
"fastgpt": lambda _cfg: FastGPTBrain(),
|
||||
|
||||
@@ -104,6 +104,7 @@ async def _tools_for(session: AsyncSession, assistant: Assistant) -> list[Runtim
|
||||
type=tool.type,
|
||||
description=tool.description,
|
||||
definition=tool.definition or {},
|
||||
secrets=tool.secrets or {},
|
||||
)
|
||||
for tool in tools
|
||||
]
|
||||
@@ -139,6 +140,7 @@ async def resolve_runtime_config(
|
||||
greeting=assistant.greeting,
|
||||
# prompt 现在是真列;外部类型由其平台编排,这里给个兜底
|
||||
prompt=assistant.prompt or "你是一个有帮助的助手。",
|
||||
dynamic_variable_definitions=assistant.dynamic_variable_definitions or {},
|
||||
runtimeMode=assistant.runtime_mode, # type: ignore[arg-type]
|
||||
enableInterrupt=assistant.enable_interrupt,
|
||||
turnConfig=assistant.turn_config or {},
|
||||
|
||||
@@ -40,8 +40,9 @@ class ConversationRecorder:
|
||||
assistant_name: str,
|
||||
channel: str,
|
||||
runtime_mode: str,
|
||||
session_id: str | None = None,
|
||||
) -> "ConversationRecorder | None":
|
||||
session_id = f"conv_{uuid4().hex[:20]}"
|
||||
session_id = session_id or f"conv_{uuid4().hex[:20]}"
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
db.add(
|
||||
|
||||
@@ -308,6 +308,41 @@ class VisionCaptureProcessor(FrameProcessor):
|
||||
await self.push_frame(frame, direction)
|
||||
|
||||
|
||||
class RealtimeDynamicVariableProcessor(FrameProcessor):
|
||||
"""Keep realtime system turn/history variables current between responses."""
|
||||
|
||||
def __init__(self, brain: Brain, cfg: AssistantConfig, realtime):
|
||||
super().__init__()
|
||||
self._brain = brain
|
||||
self._cfg = cfg
|
||||
self._realtime = realtime
|
||||
|
||||
async def _refresh_instructions(self) -> None:
|
||||
update = getattr(self._realtime, "update_instructions", None)
|
||||
if callable(update):
|
||||
await update(self._brain.system_prompt(self._cfg))
|
||||
|
||||
async def process_frame(self, frame, direction: FrameDirection):
|
||||
await super().process_frame(frame, direction)
|
||||
if isinstance(frame, OutputTransportMessageUrgentFrame):
|
||||
message = frame.message
|
||||
if isinstance(message, dict):
|
||||
event_type = message.get("type")
|
||||
if event_type == "transcript" and message.get("role") == "user":
|
||||
content = str(message.get("content") or "").strip()
|
||||
if content:
|
||||
self._brain.record_user_message(content)
|
||||
await self._refresh_instructions()
|
||||
elif event_type == "assistant-text-end":
|
||||
await self._brain.on_assistant_text_end(
|
||||
str(message.get("turn_id") or ""),
|
||||
str(message.get("content") or ""),
|
||||
bool(message.get("interrupted", False)),
|
||||
)
|
||||
await self._refresh_instructions()
|
||||
await self.push_frame(frame, direction)
|
||||
|
||||
|
||||
class RealtimeTextInputProcessor(FrameProcessor):
|
||||
"""Route text input directly to a realtime service without cascade semantics."""
|
||||
|
||||
@@ -709,6 +744,7 @@ async def run_pipeline(
|
||||
assistant_name=cfg.name,
|
||||
channel=channel,
|
||||
runtime_mode=cfg.runtimeMode,
|
||||
session_id=cfg.conversation_id or None,
|
||||
)
|
||||
pipeline = Pipeline(
|
||||
[
|
||||
@@ -911,6 +947,7 @@ async def run_realtime_pipeline(
|
||||
instructions=brain.system_prompt(cfg),
|
||||
)
|
||||
text_input = RealtimeTextInputProcessor()
|
||||
dynamic_variables = RealtimeDynamicVariableProcessor(brain, cfg, realtime)
|
||||
greeting = await brain.greeting(cfg)
|
||||
|
||||
recorder = await ConversationRecorder.start(
|
||||
@@ -918,12 +955,14 @@ async def run_realtime_pipeline(
|
||||
assistant_name=cfg.name,
|
||||
channel=channel,
|
||||
runtime_mode=cfg.runtimeMode,
|
||||
session_id=cfg.conversation_id or None,
|
||||
)
|
||||
pipeline = Pipeline(
|
||||
[
|
||||
transport.input(),
|
||||
text_input,
|
||||
realtime,
|
||||
dynamic_variables,
|
||||
ConversationHistoryProcessor(recorder),
|
||||
transport.output(),
|
||||
]
|
||||
|
||||
@@ -289,6 +289,12 @@ class StepFunRealtimeService(AIService):
|
||||
wait_until_ready=False,
|
||||
)
|
||||
|
||||
async def update_instructions(self, instructions: str) -> None:
|
||||
"""Refresh model instructions without rebuilding the realtime session."""
|
||||
self._instructions = instructions
|
||||
if self._session_ready.is_set():
|
||||
await self._send_session_update()
|
||||
|
||||
async def _send_event(
|
||||
self, payload: dict[str, Any], *, wait_until_ready: bool = True
|
||||
) -> None:
|
||||
|
||||
233
backend/services/runtime_variables.py
Normal file
233
backend/services/runtime_variables.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""Conversation-scoped dynamic variables for prompt pipeline assistants.
|
||||
|
||||
The renderer is deliberately small: it only understands ``{{ name }}``
|
||||
placeholders and never evaluates expressions. A value is substituted once,
|
||||
so user input cannot introduce a second template expansion.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from models import AssistantConfig
|
||||
|
||||
|
||||
Primitive = str | int | float | bool
|
||||
VARIABLE_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9_]{0,63}$")
|
||||
PLACEHOLDER = re.compile(r"{{\s*([A-Za-z][A-Za-z0-9_]*)\s*}}")
|
||||
MAX_VARIABLES = 50
|
||||
MAX_VALUE_LENGTH = 2048
|
||||
MAX_HISTORY_ENTRIES = 50
|
||||
MAX_HISTORY_JSON_LENGTH = 32_000
|
||||
|
||||
|
||||
class DynamicVariableError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _primitive(value: Any, name: str) -> Primitive:
|
||||
if not isinstance(value, (str, int, float, bool)) or value is None:
|
||||
raise DynamicVariableError(f"动态变量 {name} 仅支持字符串、数字或布尔值")
|
||||
if len(str(value)) > MAX_VALUE_LENGTH:
|
||||
raise DynamicVariableError(f"动态变量 {name} 超过 {MAX_VALUE_LENGTH} 字符")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_public(values: dict[str, Any] | None) -> dict[str, Primitive]:
|
||||
values = values or {}
|
||||
if len(values) > MAX_VARIABLES:
|
||||
raise DynamicVariableError(f"动态变量最多允许 {MAX_VARIABLES} 个")
|
||||
result: dict[str, Primitive] = {}
|
||||
for name, value in values.items():
|
||||
if not VARIABLE_NAME.fullmatch(name):
|
||||
raise DynamicVariableError(f"动态变量名称不合法: {name}")
|
||||
if name.startswith(("system__", "secret__")):
|
||||
raise DynamicVariableError(f"客户端不能设置保留变量: {name}")
|
||||
result[name] = _primitive(value, name)
|
||||
return result
|
||||
|
||||
|
||||
def _type_matches(value: Primitive, expected: str) -> bool:
|
||||
if expected == "string":
|
||||
return isinstance(value, str)
|
||||
if expected == "boolean":
|
||||
return isinstance(value, bool)
|
||||
if expected == "number":
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||
return False
|
||||
|
||||
|
||||
def _system_values(
|
||||
*, assistant_id: str | None, conversation_id: str, timezone: str
|
||||
) -> dict[str, Primitive]:
|
||||
try:
|
||||
zone = ZoneInfo(timezone)
|
||||
except ZoneInfoNotFoundError as exc:
|
||||
raise DynamicVariableError(f"无效时区: {timezone}") from exc
|
||||
now = datetime.now(zone)
|
||||
return {
|
||||
"system__agent_id": assistant_id or "",
|
||||
"system__conversation_id": conversation_id,
|
||||
"system__time": now.strftime("%A, %H:%M %d %B %Y"),
|
||||
"system__time_utc": now.astimezone(ZoneInfo("UTC")).isoformat(),
|
||||
"system__timezone": timezone,
|
||||
"system__agent_turns": 0,
|
||||
"system__conversation_history": json.dumps(
|
||||
{"entries": []}, ensure_ascii=False, separators=(",", ":")
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DynamicVariableStore:
|
||||
"""Mutable state owned by exactly one conversation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
values: dict[str, Primitive],
|
||||
secrets: dict[str, str] | None = None,
|
||||
):
|
||||
self.values = dict(values)
|
||||
self.secrets = dict(secrets or {})
|
||||
self.history: list[dict[str, str]] = []
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, cfg: AssistantConfig) -> "DynamicVariableStore":
|
||||
return cls(cfg.dynamic_variables, cfg.secret_dynamic_variables)
|
||||
|
||||
def render(self, template: str, *, allow_secrets: bool = False) -> str:
|
||||
if not template:
|
||||
return template
|
||||
timezone = str(self.values.get("system__timezone") or "Asia/Shanghai")
|
||||
try:
|
||||
now = datetime.now(ZoneInfo(timezone))
|
||||
self.values["system__time"] = now.strftime("%A, %H:%M %d %B %Y")
|
||||
self.values["system__time_utc"] = now.astimezone(ZoneInfo("UTC")).isoformat()
|
||||
except ZoneInfoNotFoundError:
|
||||
pass
|
||||
|
||||
def replace(match: re.Match[str]) -> str:
|
||||
name = match.group(1)
|
||||
if name.startswith("secret__"):
|
||||
if not allow_secrets:
|
||||
raise DynamicVariableError(f"密钥变量 {name} 只能用于 HTTP Header")
|
||||
if name not in self.secrets:
|
||||
raise DynamicVariableError(f"缺少密钥变量: {name}")
|
||||
return self.secrets[name]
|
||||
if name not in self.values:
|
||||
raise DynamicVariableError(f"缺少动态变量: {name}")
|
||||
value = self.values[name]
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
return str(value)
|
||||
|
||||
return PLACEHOLDER.sub(replace, template)
|
||||
|
||||
def render_data(self, value: Any, *, allow_secrets: bool = False) -> Any:
|
||||
if isinstance(value, str):
|
||||
return self.render(value, allow_secrets=allow_secrets)
|
||||
if isinstance(value, list):
|
||||
return [self.render_data(item, allow_secrets=allow_secrets) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: self.render_data(item, allow_secrets=allow_secrets)
|
||||
for key, item in value.items()
|
||||
}
|
||||
return deepcopy(value)
|
||||
|
||||
def record(self, role: str, content: str, *, completed_agent_turn: bool = False) -> None:
|
||||
if content:
|
||||
self.history.append({"role": role, "message": content})
|
||||
self.history = self.history[-MAX_HISTORY_ENTRIES:]
|
||||
if completed_agent_turn:
|
||||
self.values["system__agent_turns"] = int(
|
||||
self.values.get("system__agent_turns", 0)
|
||||
) + 1
|
||||
serialized = json.dumps(
|
||||
{"entries": self.history}, ensure_ascii=False, separators=(",", ":")
|
||||
)
|
||||
while len(serialized) > MAX_HISTORY_JSON_LENGTH and len(self.history) > 1:
|
||||
self.history.pop(0)
|
||||
serialized = json.dumps(
|
||||
{"entries": self.history},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
self.values["system__conversation_history"] = serialized
|
||||
|
||||
def assign(self, name: str, value: Any) -> None:
|
||||
if name.startswith(("system__", "secret__")) or not VARIABLE_NAME.fullmatch(name):
|
||||
raise DynamicVariableError(f"工具不能更新保留变量: {name}")
|
||||
self.values[name] = _primitive(value, name)
|
||||
|
||||
|
||||
def prepare_dynamic_config(
|
||||
cfg: AssistantConfig,
|
||||
client_values: dict[str, Any] | None,
|
||||
*,
|
||||
assistant_id: str | None,
|
||||
timezone: str = "Asia/Shanghai",
|
||||
trusted_values: dict[str, Any] | None = None,
|
||||
) -> AssistantConfig:
|
||||
"""Validate and merge one call's values without mutating stored config."""
|
||||
if cfg.type != "prompt":
|
||||
if client_values:
|
||||
raise DynamicVariableError("动态变量目前仅支持 prompt 助手")
|
||||
return cfg
|
||||
|
||||
supplied = _validate_public(client_values)
|
||||
# Server-side CRM/order/auth providers can pass trusted_values. They have
|
||||
# precedence over browser input but still cannot claim reserved prefixes.
|
||||
supplied.update(_validate_public(trusted_values))
|
||||
definitions = cfg.dynamic_variable_definitions or {}
|
||||
merged: dict[str, Primitive] = {}
|
||||
for name, definition in definitions.items():
|
||||
if name.startswith(("system__", "secret__")) or not VARIABLE_NAME.fullmatch(name):
|
||||
raise DynamicVariableError(f"动态变量定义名称不合法: {name}")
|
||||
if name in supplied:
|
||||
value = supplied.pop(name)
|
||||
elif "default" in definition and definition.get("default") is not None:
|
||||
value = _primitive(definition["default"], name)
|
||||
elif definition.get("required", False):
|
||||
raise DynamicVariableError(f"缺少必填动态变量: {name}")
|
||||
else:
|
||||
continue
|
||||
expected = str(definition.get("type") or "string")
|
||||
if not _type_matches(value, expected):
|
||||
raise DynamicVariableError(f"动态变量 {name} 类型应为 {expected}")
|
||||
merged[name] = value
|
||||
merged.update(supplied)
|
||||
|
||||
conversation_id = f"conv_{uuid4().hex[:20]}"
|
||||
merged.update(
|
||||
_system_values(
|
||||
assistant_id=assistant_id,
|
||||
conversation_id=conversation_id,
|
||||
timezone=timezone,
|
||||
)
|
||||
)
|
||||
prepared = cfg.model_copy(deep=True)
|
||||
prepared.dynamic_variables = merged
|
||||
prepared.conversation_id = conversation_id
|
||||
# Validate prompt and greeting before media resources are allocated.
|
||||
store = DynamicVariableStore.from_config(prepared)
|
||||
store.render(prepared.prompt)
|
||||
store.render(prepared.greeting)
|
||||
return prepared
|
||||
|
||||
|
||||
def value_at_path(payload: Any, path: str) -> Any:
|
||||
current = payload
|
||||
for part in path.split("."):
|
||||
if isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
elif isinstance(current, list) and part.isdigit() and int(part) < len(current):
|
||||
current = current[int(part)]
|
||||
else:
|
||||
raise KeyError(path)
|
||||
return current
|
||||
Reference in New Issue
Block a user