Enhance conversation history and runtime variable management
- Update ConversationRecorder to include source and nodeId metadata in transcripts for better context tracking. - Introduce optional variable handling in DynamicVariableStore, allowing for unset variables to be rendered as empty without raising errors. - Refactor WorkflowBrain to apply turn configurations and manage interaction policies dynamically, improving agent responsiveness. - Implement tests to ensure proper handling of updated session variables and workflow metadata in various scenarios.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Conversation-scoped dynamic variables for prompt pipeline assistants.
|
||||
"""Conversation-scoped dynamic variables shared by Prompt and Workflow.
|
||||
|
||||
The renderer is deliberately small: it only understands ``{{ name }}``
|
||||
placeholders and never evaluates expressions. A value is substituted once,
|
||||
@@ -21,6 +21,7 @@ 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*}}")
|
||||
FULL_PLACEHOLDER = re.compile(r"^{{\s*([A-Za-z][A-Za-z0-9_]*)\s*}}$")
|
||||
MAX_VARIABLES = 50
|
||||
MAX_VALUE_LENGTH = 2048
|
||||
MAX_HISTORY_ENTRIES = 50
|
||||
@@ -91,37 +92,71 @@ class DynamicVariableStore:
|
||||
self,
|
||||
values: dict[str, Primitive],
|
||||
secrets: dict[str, str] | None = None,
|
||||
*,
|
||||
optional_names: set[str] | None = None,
|
||||
variable_types: dict[str, str] | None = None,
|
||||
):
|
||||
self.values = dict(values)
|
||||
self.secrets = dict(secrets or {})
|
||||
self.optional_names = set(optional_names or set())
|
||||
self.variable_types = dict(variable_types or {})
|
||||
self.history: list[dict[str, str]] = []
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, cfg: AssistantConfig) -> "DynamicVariableStore":
|
||||
return cls(cfg.dynamic_variables, cfg.secret_dynamic_variables)
|
||||
definitions = cfg.dynamic_variable_definitions or {}
|
||||
optional_names = {
|
||||
name
|
||||
for name, definition in definitions.items()
|
||||
if not definition.get("required", False)
|
||||
and definition.get("default") is None
|
||||
}
|
||||
variable_types = {
|
||||
name: str(definition.get("type") or "string")
|
||||
for name, definition in definitions.items()
|
||||
}
|
||||
return cls(
|
||||
cfg.dynamic_variables,
|
||||
cfg.secret_dynamic_variables,
|
||||
optional_names=optional_names,
|
||||
variable_types=variable_types,
|
||||
)
|
||||
|
||||
def render(self, template: str, *, allow_secrets: bool = False) -> str:
|
||||
if not template:
|
||||
return template
|
||||
def _refresh_time(self) -> None:
|
||||
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()
|
||||
self.values["system__time_utc"] = now.astimezone(
|
||||
ZoneInfo("UTC")
|
||||
).isoformat()
|
||||
except ZoneInfoNotFoundError:
|
||||
pass
|
||||
|
||||
def _resolve(self, name: str, *, allow_secrets: bool) -> Primitive:
|
||||
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 in self.values:
|
||||
return self.values[name]
|
||||
# Optional variables intentionally remain absent from ``values`` so an
|
||||
# ``exists`` expression can distinguish unset from an explicit value.
|
||||
# Text templates still render predictably instead of failing the call.
|
||||
if name in self.optional_names:
|
||||
return ""
|
||||
raise DynamicVariableError(f"缺少动态变量: {name}")
|
||||
|
||||
def render(self, template: str, *, allow_secrets: bool = False) -> str:
|
||||
if not template:
|
||||
return template
|
||||
self._refresh_time()
|
||||
|
||||
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]
|
||||
value = self._resolve(name, allow_secrets=allow_secrets)
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
return str(value)
|
||||
@@ -130,6 +165,12 @@ class DynamicVariableStore:
|
||||
|
||||
def render_data(self, value: Any, *, allow_secrets: bool = False) -> Any:
|
||||
if isinstance(value, str):
|
||||
exact = FULL_PLACEHOLDER.fullmatch(value)
|
||||
if exact:
|
||||
self._refresh_time()
|
||||
return deepcopy(
|
||||
self._resolve(exact.group(1), allow_secrets=allow_secrets)
|
||||
)
|
||||
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]
|
||||
@@ -163,7 +204,11 @@ class DynamicVariableStore:
|
||||
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)
|
||||
primitive = _primitive(value, name)
|
||||
expected = self.variable_types.get(name)
|
||||
if expected and not _type_matches(primitive, expected):
|
||||
raise DynamicVariableError(f"动态变量 {name} 类型应为 {expected}")
|
||||
self.values[name] = primitive
|
||||
|
||||
|
||||
def prepare_dynamic_config(
|
||||
|
||||
Reference in New Issue
Block a user