feat: Persist Variables for Enhanced Debugging Workflow (#20699)
This pull request introduces a feature aimed at improving the debugging experience during workflow editing. With the addition of variable persistence, the system will automatically retain the output variables from previously executed nodes. These persisted variables can then be reused when debugging subsequent nodes, eliminating the need for repetitive manual input. By streamlining this aspect of the workflow, the feature minimizes user errors and significantly reduces debugging effort, offering a smoother and more efficient experience. Key highlights of this change: - Automatic persistence of output variables for executed nodes. - Reuse of persisted variables to simplify input steps for nodes requiring them (e.g., `code`, `template`, `variable_assigner`). - Enhanced debugging experience with reduced friction. Closes #19735.
This commit is contained in:
@@ -1,165 +0,0 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from core.variables import (
|
||||
ArrayNumberVariable,
|
||||
ArrayObjectVariable,
|
||||
ArrayStringVariable,
|
||||
FloatVariable,
|
||||
IntegerVariable,
|
||||
ObjectSegment,
|
||||
SecretVariable,
|
||||
StringVariable,
|
||||
)
|
||||
from core.variables.exc import VariableError
|
||||
from core.variables.segments import ArrayAnySegment
|
||||
from factories import variable_factory
|
||||
|
||||
|
||||
def test_string_variable():
|
||||
test_data = {"value_type": "string", "name": "test_text", "value": "Hello, World!"}
|
||||
result = variable_factory.build_conversation_variable_from_mapping(test_data)
|
||||
assert isinstance(result, StringVariable)
|
||||
|
||||
|
||||
def test_integer_variable():
|
||||
test_data = {"value_type": "number", "name": "test_int", "value": 42}
|
||||
result = variable_factory.build_conversation_variable_from_mapping(test_data)
|
||||
assert isinstance(result, IntegerVariable)
|
||||
|
||||
|
||||
def test_float_variable():
|
||||
test_data = {"value_type": "number", "name": "test_float", "value": 3.14}
|
||||
result = variable_factory.build_conversation_variable_from_mapping(test_data)
|
||||
assert isinstance(result, FloatVariable)
|
||||
|
||||
|
||||
def test_secret_variable():
|
||||
test_data = {"value_type": "secret", "name": "test_secret", "value": "secret_value"}
|
||||
result = variable_factory.build_conversation_variable_from_mapping(test_data)
|
||||
assert isinstance(result, SecretVariable)
|
||||
|
||||
|
||||
def test_invalid_value_type():
|
||||
test_data = {"value_type": "unknown", "name": "test_invalid", "value": "value"}
|
||||
with pytest.raises(VariableError):
|
||||
variable_factory.build_conversation_variable_from_mapping(test_data)
|
||||
|
||||
|
||||
def test_build_a_blank_string():
|
||||
result = variable_factory.build_conversation_variable_from_mapping(
|
||||
{
|
||||
"value_type": "string",
|
||||
"name": "blank",
|
||||
"value": "",
|
||||
}
|
||||
)
|
||||
assert isinstance(result, StringVariable)
|
||||
assert result.value == ""
|
||||
|
||||
|
||||
def test_build_a_object_variable_with_none_value():
|
||||
var = variable_factory.build_segment(
|
||||
{
|
||||
"key1": None,
|
||||
}
|
||||
)
|
||||
assert isinstance(var, ObjectSegment)
|
||||
assert var.value["key1"] is None
|
||||
|
||||
|
||||
def test_object_variable():
|
||||
mapping = {
|
||||
"id": str(uuid4()),
|
||||
"value_type": "object",
|
||||
"name": "test_object",
|
||||
"description": "Description of the variable.",
|
||||
"value": {
|
||||
"key1": "text",
|
||||
"key2": 2,
|
||||
},
|
||||
}
|
||||
variable = variable_factory.build_conversation_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ObjectSegment)
|
||||
assert isinstance(variable.value["key1"], str)
|
||||
assert isinstance(variable.value["key2"], int)
|
||||
|
||||
|
||||
def test_array_string_variable():
|
||||
mapping = {
|
||||
"id": str(uuid4()),
|
||||
"value_type": "array[string]",
|
||||
"name": "test_array",
|
||||
"description": "Description of the variable.",
|
||||
"value": [
|
||||
"text",
|
||||
"text",
|
||||
],
|
||||
}
|
||||
variable = variable_factory.build_conversation_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ArrayStringVariable)
|
||||
assert isinstance(variable.value[0], str)
|
||||
assert isinstance(variable.value[1], str)
|
||||
|
||||
|
||||
def test_array_number_variable():
|
||||
mapping = {
|
||||
"id": str(uuid4()),
|
||||
"value_type": "array[number]",
|
||||
"name": "test_array",
|
||||
"description": "Description of the variable.",
|
||||
"value": [
|
||||
1,
|
||||
2.0,
|
||||
],
|
||||
}
|
||||
variable = variable_factory.build_conversation_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ArrayNumberVariable)
|
||||
assert isinstance(variable.value[0], int)
|
||||
assert isinstance(variable.value[1], float)
|
||||
|
||||
|
||||
def test_array_object_variable():
|
||||
mapping = {
|
||||
"id": str(uuid4()),
|
||||
"value_type": "array[object]",
|
||||
"name": "test_array",
|
||||
"description": "Description of the variable.",
|
||||
"value": [
|
||||
{
|
||||
"key1": "text",
|
||||
"key2": 1,
|
||||
},
|
||||
{
|
||||
"key1": "text",
|
||||
"key2": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
variable = variable_factory.build_conversation_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ArrayObjectVariable)
|
||||
assert isinstance(variable.value[0], dict)
|
||||
assert isinstance(variable.value[1], dict)
|
||||
assert isinstance(variable.value[0]["key1"], str)
|
||||
assert isinstance(variable.value[0]["key2"], int)
|
||||
assert isinstance(variable.value[1]["key1"], str)
|
||||
assert isinstance(variable.value[1]["key2"], int)
|
||||
|
||||
|
||||
def test_variable_cannot_large_than_200_kb():
|
||||
with pytest.raises(VariableError):
|
||||
variable_factory.build_conversation_variable_from_mapping(
|
||||
{
|
||||
"id": str(uuid4()),
|
||||
"value_type": "string",
|
||||
"name": "test_text",
|
||||
"value": "a" * 1024 * 201,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_array_none_variable():
|
||||
var = variable_factory.build_segment([None, None, None, None])
|
||||
assert isinstance(var, ArrayAnySegment)
|
||||
assert var.value == [None, None, None, None]
|
||||
@@ -1,58 +0,0 @@
|
||||
from core.helper import encrypter
|
||||
from core.variables import SecretVariable, StringVariable
|
||||
from core.workflow.entities.variable_pool import VariablePool
|
||||
from core.workflow.enums import SystemVariableKey
|
||||
|
||||
|
||||
def test_segment_group_to_text():
|
||||
variable_pool = VariablePool(
|
||||
system_variables={
|
||||
SystemVariableKey("user_id"): "fake-user-id",
|
||||
},
|
||||
user_inputs={},
|
||||
environment_variables=[
|
||||
SecretVariable(name="secret_key", value="fake-secret-key"),
|
||||
],
|
||||
conversation_variables=[],
|
||||
)
|
||||
variable_pool.add(("node_id", "custom_query"), "fake-user-query")
|
||||
template = (
|
||||
"Hello, {{#sys.user_id#}}! Your query is {{#node_id.custom_query#}}. And your key is {{#env.secret_key#}}."
|
||||
)
|
||||
segments_group = variable_pool.convert_template(template)
|
||||
|
||||
assert segments_group.text == "Hello, fake-user-id! Your query is fake-user-query. And your key is fake-secret-key."
|
||||
assert segments_group.log == (
|
||||
f"Hello, fake-user-id! Your query is fake-user-query."
|
||||
f" And your key is {encrypter.obfuscated_token('fake-secret-key')}."
|
||||
)
|
||||
|
||||
|
||||
def test_convert_constant_to_segment_group():
|
||||
variable_pool = VariablePool(
|
||||
system_variables={},
|
||||
user_inputs={},
|
||||
environment_variables=[],
|
||||
conversation_variables=[],
|
||||
)
|
||||
template = "Hello, world!"
|
||||
segments_group = variable_pool.convert_template(template)
|
||||
assert segments_group.text == "Hello, world!"
|
||||
assert segments_group.log == "Hello, world!"
|
||||
|
||||
|
||||
def test_convert_variable_to_segment_group():
|
||||
variable_pool = VariablePool(
|
||||
system_variables={
|
||||
SystemVariableKey("user_id"): "fake-user-id",
|
||||
},
|
||||
user_inputs={},
|
||||
environment_variables=[],
|
||||
conversation_variables=[],
|
||||
)
|
||||
template = "{{#sys.user_id#}}"
|
||||
segments_group = variable_pool.convert_template(template)
|
||||
assert segments_group.text == "fake-user-id"
|
||||
assert segments_group.log == "fake-user-id"
|
||||
assert isinstance(segments_group.value[0], StringVariable)
|
||||
assert segments_group.value[0].value == "fake-user-id"
|
||||
@@ -1,90 +0,0 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from core.variables import (
|
||||
ArrayFileVariable,
|
||||
ArrayVariable,
|
||||
FloatVariable,
|
||||
IntegerVariable,
|
||||
ObjectVariable,
|
||||
SecretVariable,
|
||||
SegmentType,
|
||||
StringVariable,
|
||||
)
|
||||
|
||||
|
||||
def test_frozen_variables():
|
||||
var = StringVariable(name="text", value="text")
|
||||
with pytest.raises(ValidationError):
|
||||
var.value = "new value"
|
||||
|
||||
int_var = IntegerVariable(name="integer", value=42)
|
||||
with pytest.raises(ValidationError):
|
||||
int_var.value = 100
|
||||
|
||||
float_var = FloatVariable(name="float", value=3.14)
|
||||
with pytest.raises(ValidationError):
|
||||
float_var.value = 2.718
|
||||
|
||||
secret_var = SecretVariable(name="secret", value="secret_value")
|
||||
with pytest.raises(ValidationError):
|
||||
secret_var.value = "new_secret_value"
|
||||
|
||||
|
||||
def test_variable_value_type_immutable():
|
||||
with pytest.raises(ValidationError):
|
||||
StringVariable(value_type=SegmentType.ARRAY_ANY, name="text", value="text")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
StringVariable.model_validate({"value_type": "not text", "name": "text", "value": "text"})
|
||||
|
||||
var = IntegerVariable(name="integer", value=42)
|
||||
with pytest.raises(ValidationError):
|
||||
IntegerVariable(value_type=SegmentType.ARRAY_ANY, name=var.name, value=var.value)
|
||||
|
||||
var = FloatVariable(name="float", value=3.14)
|
||||
with pytest.raises(ValidationError):
|
||||
FloatVariable(value_type=SegmentType.ARRAY_ANY, name=var.name, value=var.value)
|
||||
|
||||
var = SecretVariable(name="secret", value="secret_value")
|
||||
with pytest.raises(ValidationError):
|
||||
SecretVariable(value_type=SegmentType.ARRAY_ANY, name=var.name, value=var.value)
|
||||
|
||||
|
||||
def test_object_variable_to_object():
|
||||
var = ObjectVariable(
|
||||
name="object",
|
||||
value={
|
||||
"key1": {
|
||||
"key2": "value2",
|
||||
},
|
||||
"key2": ["value5_1", 42, {}],
|
||||
},
|
||||
)
|
||||
|
||||
assert var.to_object() == {
|
||||
"key1": {
|
||||
"key2": "value2",
|
||||
},
|
||||
"key2": [
|
||||
"value5_1",
|
||||
42,
|
||||
{},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_variable_to_object():
|
||||
var = StringVariable(name="text", value="text")
|
||||
assert var.to_object() == "text"
|
||||
var = IntegerVariable(name="integer", value=42)
|
||||
assert var.to_object() == 42
|
||||
var = FloatVariable(name="float", value=3.14)
|
||||
assert var.to_object() == 3.14
|
||||
var = SecretVariable(name="secret", value="secret_value")
|
||||
assert var.to_object() == "secret_value"
|
||||
|
||||
|
||||
def test_array_file_variable_is_array_variable():
|
||||
var = ArrayFileVariable(name="files", value=[])
|
||||
assert isinstance(var, ArrayVariable)
|
||||
Reference in New Issue
Block a user