chore(api/core): apply ruff reformatting (#7624)

This commit is contained in:
Bowen Liang 2024-09-10 17:00:20 +08:00 committed by GitHub
commit 2cf1187b32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
724 changed files with 21180 additions and 21123 deletions

View file

@ -5,10 +5,7 @@ from core.workflow.graph_engine.entities.event import GraphEngineEvent
class WorkflowCallback(ABC):
@abstractmethod
def on_event(
self,
event: GraphEngineEvent
) -> None:
def on_event(self, event: GraphEngineEvent) -> None:
"""
Published event
"""

View file

@ -8,9 +8,11 @@ class BaseNodeData(ABC, BaseModel):
title: str
desc: Optional[str] = None
class BaseIterationNodeData(BaseNodeData):
start_node_id: Optional[str] = None
class BaseIterationState(BaseModel):
iteration_node_id: str
index: int
@ -19,4 +21,4 @@ class BaseIterationState(BaseModel):
class MetaData(BaseModel):
pass
metadata: MetaData
metadata: MetaData

View file

@ -12,28 +12,28 @@ class NodeType(Enum):
Node Types.
"""
START = 'start'
END = 'end'
ANSWER = 'answer'
LLM = 'llm'
KNOWLEDGE_RETRIEVAL = 'knowledge-retrieval'
IF_ELSE = 'if-else'
CODE = 'code'
TEMPLATE_TRANSFORM = 'template-transform'
QUESTION_CLASSIFIER = 'question-classifier'
HTTP_REQUEST = 'http-request'
TOOL = 'tool'
VARIABLE_AGGREGATOR = 'variable-aggregator'
START = "start"
END = "end"
ANSWER = "answer"
LLM = "llm"
KNOWLEDGE_RETRIEVAL = "knowledge-retrieval"
IF_ELSE = "if-else"
CODE = "code"
TEMPLATE_TRANSFORM = "template-transform"
QUESTION_CLASSIFIER = "question-classifier"
HTTP_REQUEST = "http-request"
TOOL = "tool"
VARIABLE_AGGREGATOR = "variable-aggregator"
# TODO: merge this into VARIABLE_AGGREGATOR
VARIABLE_ASSIGNER = 'variable-assigner'
LOOP = 'loop'
ITERATION = 'iteration'
ITERATION_START = 'iteration-start' # fake start node for iteration
PARAMETER_EXTRACTOR = 'parameter-extractor'
CONVERSATION_VARIABLE_ASSIGNER = 'assigner'
VARIABLE_ASSIGNER = "variable-assigner"
LOOP = "loop"
ITERATION = "iteration"
ITERATION_START = "iteration-start" # fake start node for iteration
PARAMETER_EXTRACTOR = "parameter-extractor"
CONVERSATION_VARIABLE_ASSIGNER = "assigner"
@classmethod
def value_of(cls, value: str) -> 'NodeType':
def value_of(cls, value: str) -> "NodeType":
"""
Get value of given node type.
@ -43,7 +43,7 @@ class NodeType(Enum):
for node_type in cls:
if node_type.value == value:
return node_type
raise ValueError(f'invalid node type value {value}')
raise ValueError(f"invalid node type value {value}")
class NodeRunMetadataKey(Enum):
@ -51,16 +51,16 @@ class NodeRunMetadataKey(Enum):
Node Run Metadata Key.
"""
TOTAL_TOKENS = 'total_tokens'
TOTAL_PRICE = 'total_price'
CURRENCY = 'currency'
TOOL_INFO = 'tool_info'
ITERATION_ID = 'iteration_id'
ITERATION_INDEX = 'iteration_index'
PARALLEL_ID = 'parallel_id'
PARALLEL_START_NODE_ID = 'parallel_start_node_id'
PARENT_PARALLEL_ID = 'parent_parallel_id'
PARENT_PARALLEL_START_NODE_ID = 'parent_parallel_start_node_id'
TOTAL_TOKENS = "total_tokens"
TOTAL_PRICE = "total_price"
CURRENCY = "currency"
TOOL_INFO = "tool_info"
ITERATION_ID = "iteration_id"
ITERATION_INDEX = "iteration_index"
PARALLEL_ID = "parallel_id"
PARALLEL_START_NODE_ID = "parallel_start_node_id"
PARENT_PARALLEL_ID = "parent_parallel_id"
PARENT_PARALLEL_START_NODE_ID = "parent_parallel_start_node_id"
class NodeRunResult(BaseModel):
@ -85,6 +85,7 @@ class UserFrom(Enum):
"""
User from
"""
ACCOUNT = "account"
END_USER = "end-user"

View file

@ -5,5 +5,6 @@ class VariableSelector(BaseModel):
"""
Variable Selector.
"""
variable: str
value_selector: list[str]

View file

@ -23,23 +23,19 @@ class VariablePool(BaseModel):
# Other elements of the selector are the keys in the second-level dictionary. To get the key, we hash the
# elements of the selector except the first one.
variable_dictionary: dict[str, dict[int, Segment]] = Field(
description='Variables mapping',
default=defaultdict(dict)
description="Variables mapping", default=defaultdict(dict)
)
# TODO: This user inputs is not used for pool.
user_inputs: Mapping[str, Any] = Field(
description='User inputs',
description="User inputs",
)
system_variables: Mapping[SystemVariableKey, Any] = Field(
description='System variables',
description="System variables",
)
environment_variables: Sequence[Variable] = Field(
description="Environment variables.",
default_factory=list
)
environment_variables: Sequence[Variable] = Field(description="Environment variables.", default_factory=list)
conversation_variables: Sequence[Variable] | None = None

View file

@ -46,13 +46,16 @@ class WorkflowRunState:
current_iteration_state: Optional[BaseIterationState]
def __init__(self, workflow: Workflow,
start_at: float,
variable_pool: VariablePool,
user_id: str,
user_from: UserFrom,
invoke_from: InvokeFrom,
workflow_call_depth: int):
def __init__(
self,
workflow: Workflow,
start_at: float,
variable_pool: VariablePool,
user_id: str,
user_from: UserFrom,
invoke_from: InvokeFrom,
workflow_call_depth: int,
):
self.workflow_id = workflow.id
self.tenant_id = workflow.tenant_id
self.app_id = workflow.app_id

View file

@ -8,19 +8,13 @@ from core.workflow.graph_engine.entities.runtime_route_state import RouteNodeSta
class RunConditionHandler(ABC):
def __init__(self,
init_params: GraphInitParams,
graph: Graph,
condition: RunCondition):
def __init__(self, init_params: GraphInitParams, graph: Graph, condition: RunCondition):
self.init_params = init_params
self.graph = graph
self.condition = condition
@abstractmethod
def check(self,
graph_runtime_state: GraphRuntimeState,
previous_route_node_state: RouteNodeState
) -> bool:
def check(self, graph_runtime_state: GraphRuntimeState, previous_route_node_state: RouteNodeState) -> bool:
"""
Check if the condition can be executed

View file

@ -4,10 +4,7 @@ from core.workflow.graph_engine.entities.runtime_route_state import RouteNodeSta
class BranchIdentifyRunConditionHandler(RunConditionHandler):
def check(self,
graph_runtime_state: GraphRuntimeState,
previous_route_node_state: RouteNodeState) -> bool:
def check(self, graph_runtime_state: GraphRuntimeState, previous_route_node_state: RouteNodeState) -> bool:
"""
Check if the condition can be executed

View file

@ -5,10 +5,7 @@ from core.workflow.utils.condition.processor import ConditionProcessor
class ConditionRunConditionHandlerHandler(RunConditionHandler):
def check(self,
graph_runtime_state: GraphRuntimeState,
previous_route_node_state: RouteNodeState
) -> bool:
def check(self, graph_runtime_state: GraphRuntimeState, previous_route_node_state: RouteNodeState) -> bool:
"""
Check if the condition can be executed
@ -22,8 +19,7 @@ class ConditionRunConditionHandlerHandler(RunConditionHandler):
# process condition
condition_processor = ConditionProcessor()
input_conditions, group_result = condition_processor.process_conditions(
variable_pool=graph_runtime_state.variable_pool,
conditions=self.condition.conditions
variable_pool=graph_runtime_state.variable_pool, conditions=self.condition.conditions
)
# Apply the logical operator for the current case

View file

@ -9,9 +9,7 @@ from core.workflow.graph_engine.entities.run_condition import RunCondition
class ConditionManager:
@staticmethod
def get_condition_handler(
init_params: GraphInitParams,
graph: Graph,
run_condition: RunCondition
init_params: GraphInitParams, graph: Graph, run_condition: RunCondition
) -> RunConditionHandler:
"""
Get condition handler
@ -22,14 +20,6 @@ class ConditionManager:
:return: condition handler
"""
if run_condition.type == "branch_identify":
return BranchIdentifyRunConditionHandler(
init_params=init_params,
graph=graph,
condition=run_condition
)
return BranchIdentifyRunConditionHandler(init_params=init_params, graph=graph, condition=run_condition)
else:
return ConditionRunConditionHandlerHandler(
init_params=init_params,
graph=graph,
condition=run_condition
)
return ConditionRunConditionHandlerHandler(init_params=init_params, graph=graph, condition=run_condition)

View file

@ -34,38 +34,25 @@ class Graph(BaseModel):
root_node_id: str = Field(..., description="root node id of the graph")
node_ids: list[str] = Field(default_factory=list, description="graph node ids")
node_id_config_mapping: dict[str, dict] = Field(
default_factory=list,
description="node configs mapping (node id: node config)"
default_factory=list, description="node configs mapping (node id: node config)"
)
edge_mapping: dict[str, list[GraphEdge]] = Field(
default_factory=dict,
description="graph edge mapping (source node id: edges)"
default_factory=dict, description="graph edge mapping (source node id: edges)"
)
reverse_edge_mapping: dict[str, list[GraphEdge]] = Field(
default_factory=dict,
description="reverse graph edge mapping (target node id: edges)"
default_factory=dict, description="reverse graph edge mapping (target node id: edges)"
)
parallel_mapping: dict[str, GraphParallel] = Field(
default_factory=dict,
description="graph parallel mapping (parallel id: parallel)"
default_factory=dict, description="graph parallel mapping (parallel id: parallel)"
)
node_parallel_mapping: dict[str, str] = Field(
default_factory=dict,
description="graph node parallel mapping (node id: parallel id)"
)
answer_stream_generate_routes: AnswerStreamGenerateRoute = Field(
...,
description="answer stream generate routes"
)
end_stream_param: EndStreamParam = Field(
...,
description="end stream param"
default_factory=dict, description="graph node parallel mapping (node id: parallel id)"
)
answer_stream_generate_routes: AnswerStreamGenerateRoute = Field(..., description="answer stream generate routes")
end_stream_param: EndStreamParam = Field(..., description="end stream param")
@classmethod
def init(cls,
graph_config: Mapping[str, Any],
root_node_id: Optional[str] = None) -> "Graph":
def init(cls, graph_config: Mapping[str, Any], root_node_id: Optional[str] = None) -> "Graph":
"""
Init graph
@ -74,7 +61,7 @@ class Graph(BaseModel):
:return: graph
"""
# edge configs
edge_configs = graph_config.get('edges')
edge_configs = graph_config.get("edges")
if edge_configs is None:
edge_configs = []
@ -85,14 +72,14 @@ class Graph(BaseModel):
reverse_edge_mapping: dict[str, list[GraphEdge]] = {}
target_edge_ids = set()
for edge_config in edge_configs:
source_node_id = edge_config.get('source')
source_node_id = edge_config.get("source")
if not source_node_id:
continue
if source_node_id not in edge_mapping:
edge_mapping[source_node_id] = []
target_node_id = edge_config.get('target')
target_node_id = edge_config.get("target")
if not target_node_id:
continue
@ -107,23 +94,18 @@ class Graph(BaseModel):
# parse run condition
run_condition = None
if edge_config.get('sourceHandle') and edge_config.get('sourceHandle') != 'source':
run_condition = RunCondition(
type='branch_identify',
branch_identify=edge_config.get('sourceHandle')
)
if edge_config.get("sourceHandle") and edge_config.get("sourceHandle") != "source":
run_condition = RunCondition(type="branch_identify", branch_identify=edge_config.get("sourceHandle"))
graph_edge = GraphEdge(
source_node_id=source_node_id,
target_node_id=target_node_id,
run_condition=run_condition
source_node_id=source_node_id, target_node_id=target_node_id, run_condition=run_condition
)
edge_mapping[source_node_id].append(graph_edge)
reverse_edge_mapping[target_node_id].append(graph_edge)
# node configs
node_configs = graph_config.get('nodes')
node_configs = graph_config.get("nodes")
if not node_configs:
raise ValueError("Graph must have at least one node")
@ -133,7 +115,7 @@ class Graph(BaseModel):
root_node_configs = []
all_node_id_config_mapping: dict[str, dict] = {}
for node_config in node_configs:
node_id = node_config.get('id')
node_id = node_config.get("id")
if not node_id:
continue
@ -142,30 +124,29 @@ class Graph(BaseModel):
all_node_id_config_mapping[node_id] = node_config
root_node_ids = [node_config.get('id') for node_config in root_node_configs]
root_node_ids = [node_config.get("id") for node_config in root_node_configs]
# fetch root node
if not root_node_id:
# if no root node id, use the START type node as root node
root_node_id = next((node_config.get("id") for node_config in root_node_configs
if node_config.get('data', {}).get('type', '') == NodeType.START.value), None)
root_node_id = next(
(
node_config.get("id")
for node_config in root_node_configs
if node_config.get("data", {}).get("type", "") == NodeType.START.value
),
None,
)
if not root_node_id or root_node_id not in root_node_ids:
raise ValueError(f"Root node id {root_node_id} not found in the graph")
# Check whether it is connected to the previous node
cls._check_connected_to_previous_node(
route=[root_node_id],
edge_mapping=edge_mapping
)
cls._check_connected_to_previous_node(route=[root_node_id], edge_mapping=edge_mapping)
# fetch all node ids from root node
node_ids = [root_node_id]
cls._recursively_add_node_ids(
node_ids=node_ids,
edge_mapping=edge_mapping,
node_id=root_node_id
)
cls._recursively_add_node_ids(node_ids=node_ids, edge_mapping=edge_mapping, node_id=root_node_id)
node_id_config_mapping = {node_id: all_node_id_config_mapping[node_id] for node_id in node_ids}
@ -177,29 +158,26 @@ class Graph(BaseModel):
reverse_edge_mapping=reverse_edge_mapping,
start_node_id=root_node_id,
parallel_mapping=parallel_mapping,
node_parallel_mapping=node_parallel_mapping
node_parallel_mapping=node_parallel_mapping,
)
# Check if it exceeds N layers of parallel
for parallel in parallel_mapping.values():
if parallel.parent_parallel_id:
cls._check_exceed_parallel_limit(
parallel_mapping=parallel_mapping,
level_limit=3,
parent_parallel_id=parallel.parent_parallel_id
parallel_mapping=parallel_mapping, level_limit=3, parent_parallel_id=parallel.parent_parallel_id
)
# init answer stream generate routes
answer_stream_generate_routes = AnswerStreamGeneratorRouter.init(
node_id_config_mapping=node_id_config_mapping,
reverse_edge_mapping=reverse_edge_mapping
node_id_config_mapping=node_id_config_mapping, reverse_edge_mapping=reverse_edge_mapping
)
# init end stream param
end_stream_param = EndStreamGeneratorRouter.init(
node_id_config_mapping=node_id_config_mapping,
reverse_edge_mapping=reverse_edge_mapping,
node_parallel_mapping=node_parallel_mapping
node_parallel_mapping=node_parallel_mapping,
)
# init graph
@ -212,14 +190,14 @@ class Graph(BaseModel):
parallel_mapping=parallel_mapping,
node_parallel_mapping=node_parallel_mapping,
answer_stream_generate_routes=answer_stream_generate_routes,
end_stream_param=end_stream_param
end_stream_param=end_stream_param,
)
return graph
def add_extra_edge(self, source_node_id: str,
target_node_id: str,
run_condition: Optional[RunCondition] = None) -> None:
def add_extra_edge(
self, source_node_id: str, target_node_id: str, run_condition: Optional[RunCondition] = None
) -> None:
"""
Add extra edge to the graph
@ -237,9 +215,7 @@ class Graph(BaseModel):
return
graph_edge = GraphEdge(
source_node_id=source_node_id,
target_node_id=target_node_id,
run_condition=run_condition
source_node_id=source_node_id, target_node_id=target_node_id, run_condition=run_condition
)
self.edge_mapping[source_node_id].append(graph_edge)
@ -254,17 +230,18 @@ class Graph(BaseModel):
for node_id in self.node_ids:
if node_id not in self.edge_mapping:
leaf_node_ids.append(node_id)
elif (len(self.edge_mapping[node_id]) == 1
and self.edge_mapping[node_id][0].target_node_id == self.root_node_id):
elif (
len(self.edge_mapping[node_id]) == 1
and self.edge_mapping[node_id][0].target_node_id == self.root_node_id
):
leaf_node_ids.append(node_id)
return leaf_node_ids
@classmethod
def _recursively_add_node_ids(cls,
node_ids: list[str],
edge_mapping: dict[str, list[GraphEdge]],
node_id: str) -> None:
def _recursively_add_node_ids(
cls, node_ids: list[str], edge_mapping: dict[str, list[GraphEdge]], node_id: str
) -> None:
"""
Recursively add node ids
@ -278,17 +255,11 @@ class Graph(BaseModel):
node_ids.append(graph_edge.target_node_id)
cls._recursively_add_node_ids(
node_ids=node_ids,
edge_mapping=edge_mapping,
node_id=graph_edge.target_node_id
node_ids=node_ids, edge_mapping=edge_mapping, node_id=graph_edge.target_node_id
)
@classmethod
def _check_connected_to_previous_node(
cls,
route: list[str],
edge_mapping: dict[str, list[GraphEdge]]
) -> None:
def _check_connected_to_previous_node(cls, route: list[str], edge_mapping: dict[str, list[GraphEdge]]) -> None:
"""
Check whether it is connected to the previous node
"""
@ -299,7 +270,9 @@ class Graph(BaseModel):
continue
if graph_edge.target_node_id in route:
raise ValueError(f"Node {graph_edge.source_node_id} is connected to the previous node, please check the graph.")
raise ValueError(
f"Node {graph_edge.source_node_id} is connected to the previous node, please check the graph."
)
new_route = route[:]
new_route.append(graph_edge.target_node_id)
@ -316,7 +289,7 @@ class Graph(BaseModel):
start_node_id: str,
parallel_mapping: dict[str, GraphParallel],
node_parallel_mapping: dict[str, str],
parent_parallel: Optional[GraphParallel] = None
parent_parallel: Optional[GraphParallel] = None,
) -> None:
"""
Recursively add parallel ids
@ -355,14 +328,14 @@ class Graph(BaseModel):
parallel = GraphParallel(
start_from_node_id=start_node_id,
parent_parallel_id=parent_parallel.id if parent_parallel else None,
parent_parallel_start_node_id=parent_parallel.start_from_node_id if parent_parallel else None
parent_parallel_start_node_id=parent_parallel.start_from_node_id if parent_parallel else None,
)
parallel_mapping[parallel.id] = parallel
in_branch_node_ids = cls._fetch_all_node_ids_in_parallels(
edge_mapping=edge_mapping,
reverse_edge_mapping=reverse_edge_mapping,
parallel_branch_node_ids=parallel_branch_node_ids
parallel_branch_node_ids=parallel_branch_node_ids,
)
# collect all branches node ids
@ -403,14 +376,25 @@ class Graph(BaseModel):
continue
if (
(node_parallel_mapping.get(target_node_id) and node_parallel_mapping.get(target_node_id) == parent_parallel_id)
or (parent_parallel and parent_parallel.end_to_node_id and target_node_id == parent_parallel.end_to_node_id)
(
node_parallel_mapping.get(target_node_id)
and node_parallel_mapping.get(target_node_id) == parent_parallel_id
)
or (
parent_parallel
and parent_parallel.end_to_node_id
and target_node_id == parent_parallel.end_to_node_id
)
or (not node_parallel_mapping.get(target_node_id) and not parent_parallel)
):
outside_parallel_target_node_ids.add(target_node_id)
if len(outside_parallel_target_node_ids) == 1:
if parent_parallel and parent_parallel.end_to_node_id and parallel.end_to_node_id == parent_parallel.end_to_node_id:
if (
parent_parallel
and parent_parallel.end_to_node_id
and parallel.end_to_node_id == parent_parallel.end_to_node_id
):
parallel.end_to_node_id = None
else:
parallel.end_to_node_id = outside_parallel_target_node_ids.pop()
@ -420,18 +404,20 @@ class Graph(BaseModel):
if parallel:
current_parallel = parallel
elif parent_parallel:
if not parent_parallel.end_to_node_id or (parent_parallel.end_to_node_id and graph_edge.target_node_id != parent_parallel.end_to_node_id):
if not parent_parallel.end_to_node_id or (
parent_parallel.end_to_node_id and graph_edge.target_node_id != parent_parallel.end_to_node_id
):
current_parallel = parent_parallel
else:
# fetch parent parallel's parent parallel
parent_parallel_parent_parallel_id = parent_parallel.parent_parallel_id
if parent_parallel_parent_parallel_id:
parent_parallel_parent_parallel = parallel_mapping.get(parent_parallel_parent_parallel_id)
if (
parent_parallel_parent_parallel
and (
not parent_parallel_parent_parallel.end_to_node_id
or (parent_parallel_parent_parallel.end_to_node_id and graph_edge.target_node_id != parent_parallel_parent_parallel.end_to_node_id)
if parent_parallel_parent_parallel and (
not parent_parallel_parent_parallel.end_to_node_id
or (
parent_parallel_parent_parallel.end_to_node_id
and graph_edge.target_node_id != parent_parallel_parent_parallel.end_to_node_id
)
):
current_parallel = parent_parallel_parent_parallel
@ -442,7 +428,7 @@ class Graph(BaseModel):
start_node_id=graph_edge.target_node_id,
parallel_mapping=parallel_mapping,
node_parallel_mapping=node_parallel_mapping,
parent_parallel=current_parallel
parent_parallel=current_parallel,
)
@classmethod
@ -451,7 +437,7 @@ class Graph(BaseModel):
parallel_mapping: dict[str, GraphParallel],
level_limit: int,
parent_parallel_id: str,
current_level: int = 1
current_level: int = 1,
) -> None:
"""
Check if it exceeds N layers of parallel
@ -459,25 +445,27 @@ class Graph(BaseModel):
parent_parallel = parallel_mapping.get(parent_parallel_id)
if not parent_parallel:
return
current_level += 1
if current_level > level_limit:
raise ValueError(f"Exceeds {level_limit} layers of parallel")
if parent_parallel.parent_parallel_id:
cls._check_exceed_parallel_limit(
parallel_mapping=parallel_mapping,
level_limit=level_limit,
parent_parallel_id=parent_parallel.parent_parallel_id,
current_level=current_level
current_level=current_level,
)
@classmethod
def _recursively_add_parallel_node_ids(cls,
branch_node_ids: list[str],
edge_mapping: dict[str, list[GraphEdge]],
merge_node_id: str,
start_node_id: str) -> None:
def _recursively_add_parallel_node_ids(
cls,
branch_node_ids: list[str],
edge_mapping: dict[str, list[GraphEdge]],
merge_node_id: str,
start_node_id: str,
) -> None:
"""
Recursively add node ids
@ -487,21 +475,22 @@ class Graph(BaseModel):
:param start_node_id: start node id
"""
for graph_edge in edge_mapping.get(start_node_id, []):
if (graph_edge.target_node_id != merge_node_id
and graph_edge.target_node_id not in branch_node_ids):
if graph_edge.target_node_id != merge_node_id and graph_edge.target_node_id not in branch_node_ids:
branch_node_ids.append(graph_edge.target_node_id)
cls._recursively_add_parallel_node_ids(
branch_node_ids=branch_node_ids,
edge_mapping=edge_mapping,
merge_node_id=merge_node_id,
start_node_id=graph_edge.target_node_id
start_node_id=graph_edge.target_node_id,
)
@classmethod
def _fetch_all_node_ids_in_parallels(cls,
edge_mapping: dict[str, list[GraphEdge]],
reverse_edge_mapping: dict[str, list[GraphEdge]],
parallel_branch_node_ids: list[str]) -> dict[str, list[str]]:
def _fetch_all_node_ids_in_parallels(
cls,
edge_mapping: dict[str, list[GraphEdge]],
reverse_edge_mapping: dict[str, list[GraphEdge]],
parallel_branch_node_ids: list[str],
) -> dict[str, list[str]]:
"""
Fetch all node ids in parallels
"""
@ -513,7 +502,7 @@ class Graph(BaseModel):
cls._recursively_fetch_routes(
edge_mapping=edge_mapping,
start_node_id=parallel_branch_node_id,
routes_node_ids=routes_node_ids[parallel_branch_node_id]
routes_node_ids=routes_node_ids[parallel_branch_node_id],
)
# fetch leaf node ids from routes node ids
@ -529,13 +518,13 @@ class Graph(BaseModel):
for branch_node_id2, inner_route2 in routes_node_ids.items():
if (
branch_node_id != branch_node_id2
branch_node_id != branch_node_id2
and node_id in inner_route2
and len(reverse_edge_mapping.get(node_id, [])) > 1
and cls._is_node_in_routes(
reverse_edge_mapping=reverse_edge_mapping,
start_node_id=node_id,
routes_node_ids=routes_node_ids
routes_node_ids=routes_node_ids,
)
):
if node_id not in merge_branch_node_ids:
@ -551,23 +540,18 @@ class Graph(BaseModel):
for node_id, branch_node_ids in merge_branch_node_ids.items():
for node_id2, branch_node_ids2 in merge_branch_node_ids.items():
if node_id != node_id2 and set(branch_node_ids) == set(branch_node_ids2):
if (node_id, node_id2) not in duplicate_end_node_ids and (node_id2, node_id) not in duplicate_end_node_ids:
if (node_id, node_id2) not in duplicate_end_node_ids and (
node_id2,
node_id,
) not in duplicate_end_node_ids:
duplicate_end_node_ids[(node_id, node_id2)] = branch_node_ids
for (node_id, node_id2), branch_node_ids in duplicate_end_node_ids.items():
# check which node is after
if cls._is_node2_after_node1(
node1_id=node_id,
node2_id=node_id2,
edge_mapping=edge_mapping
):
if cls._is_node2_after_node1(node1_id=node_id, node2_id=node_id2, edge_mapping=edge_mapping):
if node_id in merge_branch_node_ids:
del merge_branch_node_ids[node_id2]
elif cls._is_node2_after_node1(
node1_id=node_id2,
node2_id=node_id,
edge_mapping=edge_mapping
):
elif cls._is_node2_after_node1(node1_id=node_id2, node2_id=node_id, edge_mapping=edge_mapping):
if node_id2 in merge_branch_node_ids:
del merge_branch_node_ids[node_id]
@ -599,16 +583,15 @@ class Graph(BaseModel):
branch_node_ids=in_branch_node_ids[branch_node_id],
edge_mapping=edge_mapping,
merge_node_id=merge_node_id,
start_node_id=branch_node_id
start_node_id=branch_node_id,
)
return in_branch_node_ids
@classmethod
def _recursively_fetch_routes(cls,
edge_mapping: dict[str, list[GraphEdge]],
start_node_id: str,
routes_node_ids: list[str]) -> None:
def _recursively_fetch_routes(
cls, edge_mapping: dict[str, list[GraphEdge]], start_node_id: str, routes_node_ids: list[str]
) -> None:
"""
Recursively fetch route
"""
@ -621,28 +604,25 @@ class Graph(BaseModel):
routes_node_ids.append(graph_edge.target_node_id)
cls._recursively_fetch_routes(
edge_mapping=edge_mapping,
start_node_id=graph_edge.target_node_id,
routes_node_ids=routes_node_ids
edge_mapping=edge_mapping, start_node_id=graph_edge.target_node_id, routes_node_ids=routes_node_ids
)
@classmethod
def _is_node_in_routes(cls,
reverse_edge_mapping: dict[str, list[GraphEdge]],
start_node_id: str,
routes_node_ids: dict[str, list[str]]) -> bool:
def _is_node_in_routes(
cls, reverse_edge_mapping: dict[str, list[GraphEdge]], start_node_id: str, routes_node_ids: dict[str, list[str]]
) -> bool:
"""
Recursively check if the node is in the routes
"""
if start_node_id not in reverse_edge_mapping:
return False
all_routes_node_ids = set()
parallel_start_node_ids: dict[str, list[str]] = {}
for branch_node_id, node_ids in routes_node_ids.items():
for node_id in node_ids:
all_routes_node_ids.add(node_id)
if branch_node_id in reverse_edge_mapping:
for graph_edge in reverse_edge_mapping[branch_node_id]:
if graph_edge.source_node_id not in parallel_start_node_ids:
@ -655,38 +635,34 @@ class Graph(BaseModel):
if set(branch_node_ids) == set(routes_node_ids.keys()):
parallel_start_node_id = p_start_node_id
return True
if not parallel_start_node_id:
raise Exception("Parallel start node id not found")
for graph_edge in reverse_edge_mapping[start_node_id]:
if graph_edge.source_node_id not in all_routes_node_ids or graph_edge.source_node_id != parallel_start_node_id:
if (
graph_edge.source_node_id not in all_routes_node_ids
or graph_edge.source_node_id != parallel_start_node_id
):
return False
return True
@classmethod
def _is_node2_after_node1(
cls,
node1_id: str,
node2_id: str,
edge_mapping: dict[str, list[GraphEdge]]
) -> bool:
def _is_node2_after_node1(cls, node1_id: str, node2_id: str, edge_mapping: dict[str, list[GraphEdge]]) -> bool:
"""
is node2 after node1
"""
if node1_id not in edge_mapping:
return False
for graph_edge in edge_mapping[node1_id]:
if graph_edge.target_node_id == node2_id:
return True
if cls._is_node2_after_node1(
node1_id=graph_edge.target_node_id,
node2_id=node2_id,
edge_mapping=edge_mapping
node1_id=graph_edge.target_node_id, node2_id=node2_id, edge_mapping=edge_mapping
):
return True
return False
return False

View file

@ -10,7 +10,7 @@ from core.workflow.graph_engine.entities.runtime_route_state import RuntimeRoute
class GraphRuntimeState(BaseModel):
variable_pool: VariablePool = Field(..., description="variable pool")
"""variable pool"""
start_at: float = Field(..., description="start time")
"""start time"""
total_tokens: int = 0

View file

@ -18,4 +18,4 @@ class RunCondition(BaseModel):
@property
def hash(self) -> str:
return hashlib.sha256(self.model_dump_json().encode()).hexdigest()
return hashlib.sha256(self.model_dump_json().encode()).hexdigest()

View file

@ -68,13 +68,11 @@ class RouteNodeState(BaseModel):
class RuntimeRouteState(BaseModel):
routes: dict[str, list[str]] = Field(
default_factory=dict,
description="graph state routes (source_node_state_id: target_node_state_id)"
default_factory=dict, description="graph state routes (source_node_state_id: target_node_state_id)"
)
node_state_mapping: dict[str, RouteNodeState] = Field(
default_factory=dict,
description="node state mapping (route_node_state_id: route_node_state)"
default_factory=dict, description="node state mapping (route_node_state_id: route_node_state)"
)
def create_node_state(self, node_id: str) -> RouteNodeState:
@ -99,13 +97,13 @@ class RuntimeRouteState(BaseModel):
self.routes[source_node_state_id].append(target_node_state_id)
def get_routes_with_node_state_by_source_node_state_id(self, source_node_state_id: str) \
-> list[RouteNodeState]:
def get_routes_with_node_state_by_source_node_state_id(self, source_node_state_id: str) -> list[RouteNodeState]:
"""
Get routes with node state by source node id
:param source_node_state_id: source node state id
:return: routes with node state
"""
return [self.node_state_mapping[target_state_id]
for target_state_id in self.routes.get(source_node_state_id, [])]
return [
self.node_state_mapping[target_state_id] for target_state_id in self.routes.get(source_node_state_id, [])
]

View file

@ -48,8 +48,9 @@ logger = logging.getLogger(__name__)
class GraphEngineThreadPool(ThreadPoolExecutor):
def __init__(self, max_workers=None, thread_name_prefix='',
initializer=None, initargs=(), max_submit_count=100) -> None:
def __init__(
self, max_workers=None, thread_name_prefix="", initializer=None, initargs=(), max_submit_count=100
) -> None:
super().__init__(max_workers, thread_name_prefix, initializer, initargs)
self.max_submit_count = max_submit_count
self.submit_count = 0
@ -57,9 +58,9 @@ class GraphEngineThreadPool(ThreadPoolExecutor):
def submit(self, fn, *args, **kwargs):
self.submit_count += 1
self.check_is_full()
return super().submit(fn, *args, **kwargs)
def check_is_full(self) -> None:
print(f"submit_count: {self.submit_count}, max_submit_count: {self.max_submit_count}")
if self.submit_count > self.max_submit_count:
@ -70,21 +71,21 @@ class GraphEngine:
workflow_thread_pool_mapping: dict[str, GraphEngineThreadPool] = {}
def __init__(
self,
tenant_id: str,
app_id: str,
workflow_type: WorkflowType,
workflow_id: str,
user_id: str,
user_from: UserFrom,
invoke_from: InvokeFrom,
call_depth: int,
graph: Graph,
graph_config: Mapping[str, Any],
variable_pool: VariablePool,
max_execution_steps: int,
max_execution_time: int,
thread_pool_id: Optional[str] = None
self,
tenant_id: str,
app_id: str,
workflow_type: WorkflowType,
workflow_id: str,
user_id: str,
user_from: UserFrom,
invoke_from: InvokeFrom,
call_depth: int,
graph: Graph,
graph_config: Mapping[str, Any],
variable_pool: VariablePool,
max_execution_steps: int,
max_execution_time: int,
thread_pool_id: Optional[str] = None,
) -> None:
thread_pool_max_submit_count = 100
thread_pool_max_workers = 10
@ -93,12 +94,14 @@ class GraphEngine:
if thread_pool_id:
if not thread_pool_id in GraphEngine.workflow_thread_pool_mapping:
raise ValueError(f"Max submit count {thread_pool_max_submit_count} of workflow thread pool reached.")
self.thread_pool_id = thread_pool_id
self.thread_pool = GraphEngine.workflow_thread_pool_mapping[thread_pool_id]
self.is_main_thread_pool = False
else:
self.thread_pool = GraphEngineThreadPool(max_workers=thread_pool_max_workers, max_submit_count=thread_pool_max_submit_count)
self.thread_pool = GraphEngineThreadPool(
max_workers=thread_pool_max_workers, max_submit_count=thread_pool_max_submit_count
)
self.thread_pool_id = str(uuid.uuid4())
self.is_main_thread_pool = True
GraphEngine.workflow_thread_pool_mapping[self.thread_pool_id] = self.thread_pool
@ -113,13 +116,10 @@ class GraphEngine:
user_id=user_id,
user_from=user_from,
invoke_from=invoke_from,
call_depth=call_depth
call_depth=call_depth,
)
self.graph_runtime_state = GraphRuntimeState(
variable_pool=variable_pool,
start_at=time.perf_counter()
)
self.graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
self.max_execution_steps = max_execution_steps
self.max_execution_time = max_execution_time
@ -136,37 +136,40 @@ class GraphEngine:
stream_processor_cls = EndStreamProcessor
stream_processor = stream_processor_cls(
graph=self.graph,
variable_pool=self.graph_runtime_state.variable_pool
graph=self.graph, variable_pool=self.graph_runtime_state.variable_pool
)
# run graph
generator = stream_processor.process(
self._run(start_node_id=self.graph.root_node_id)
)
generator = stream_processor.process(self._run(start_node_id=self.graph.root_node_id))
for item in generator:
try:
yield item
if isinstance(item, NodeRunFailedEvent):
yield GraphRunFailedEvent(error=item.route_node_state.failed_reason or 'Unknown error.')
yield GraphRunFailedEvent(error=item.route_node_state.failed_reason or "Unknown error.")
return
elif isinstance(item, NodeRunSucceededEvent):
if item.node_type == NodeType.END:
self.graph_runtime_state.outputs = (item.route_node_state.node_run_result.outputs
if item.route_node_state.node_run_result
and item.route_node_state.node_run_result.outputs
else {})
self.graph_runtime_state.outputs = (
item.route_node_state.node_run_result.outputs
if item.route_node_state.node_run_result
and item.route_node_state.node_run_result.outputs
else {}
)
elif item.node_type == NodeType.ANSWER:
if "answer" not in self.graph_runtime_state.outputs:
self.graph_runtime_state.outputs["answer"] = ""
self.graph_runtime_state.outputs["answer"] += "\n" + (item.route_node_state.node_run_result.outputs.get("answer", "")
if item.route_node_state.node_run_result
and item.route_node_state.node_run_result.outputs
else "")
self.graph_runtime_state.outputs["answer"] = self.graph_runtime_state.outputs["answer"].strip()
self.graph_runtime_state.outputs["answer"] += "\n" + (
item.route_node_state.node_run_result.outputs.get("answer", "")
if item.route_node_state.node_run_result
and item.route_node_state.node_run_result.outputs
else ""
)
self.graph_runtime_state.outputs["answer"] = self.graph_runtime_state.outputs[
"answer"
].strip()
except Exception as e:
logger.exception(f"Graph run failed: {str(e)}")
yield GraphRunFailedEvent(error=str(e))
@ -186,12 +189,12 @@ class GraphEngine:
del GraphEngine.workflow_thread_pool_mapping[self.thread_pool_id]
def _run(
self,
start_node_id: str,
in_parallel_id: Optional[str] = None,
parent_parallel_id: Optional[str] = None,
parent_parallel_start_node_id: Optional[str] = None
) -> Generator[GraphEngineEvent, None, None]:
self,
start_node_id: str,
in_parallel_id: Optional[str] = None,
parent_parallel_id: Optional[str] = None,
parent_parallel_start_node_id: Optional[str] = None,
) -> Generator[GraphEngineEvent, None, None]:
parallel_start_node_id = None
if in_parallel_id:
parallel_start_node_id = start_node_id
@ -201,31 +204,28 @@ class GraphEngine:
while True:
# max steps reached
if self.graph_runtime_state.node_run_steps > self.max_execution_steps:
raise GraphRunFailedError('Max steps {} reached.'.format(self.max_execution_steps))
raise GraphRunFailedError("Max steps {} reached.".format(self.max_execution_steps))
# or max execution time reached
if self._is_timed_out(
start_at=self.graph_runtime_state.start_at,
max_execution_time=self.max_execution_time
start_at=self.graph_runtime_state.start_at, max_execution_time=self.max_execution_time
):
raise GraphRunFailedError('Max execution time {}s reached.'.format(self.max_execution_time))
raise GraphRunFailedError("Max execution time {}s reached.".format(self.max_execution_time))
# init route node state
route_node_state = self.graph_runtime_state.node_run_state.create_node_state(
node_id=next_node_id
)
route_node_state = self.graph_runtime_state.node_run_state.create_node_state(node_id=next_node_id)
# get node config
node_id = route_node_state.node_id
node_config = self.graph.node_id_config_mapping.get(node_id)
if not node_config:
raise GraphRunFailedError(f'Node {node_id} config not found.')
raise GraphRunFailedError(f"Node {node_id} config not found.")
# convert to specific node
node_type = NodeType.value_of(node_config.get('data', {}).get('type'))
node_type = NodeType.value_of(node_config.get("data", {}).get("type"))
node_cls = node_classes.get(node_type)
if not node_cls:
raise GraphRunFailedError(f'Node {node_id} type {node_type} not found.')
raise GraphRunFailedError(f"Node {node_id} type {node_type} not found.")
previous_node_id = previous_route_node_state.node_id if previous_route_node_state else None
@ -237,7 +237,7 @@ class GraphEngine:
graph=self.graph,
graph_runtime_state=self.graph_runtime_state,
previous_node_id=previous_node_id,
thread_pool_id=self.thread_pool_id
thread_pool_id=self.thread_pool_id,
)
try:
@ -248,7 +248,7 @@ class GraphEngine:
parallel_id=in_parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
for item in generator:
@ -263,8 +263,7 @@ class GraphEngine:
# append route
if previous_route_node_state:
self.graph_runtime_state.node_run_state.add_route(
source_node_state_id=previous_route_node_state.id,
target_node_state_id=route_node_state.id
source_node_state_id=previous_route_node_state.id, target_node_state_id=route_node_state.id
)
except Exception as e:
route_node_state.status = RouteNodeState.Status.FAILED
@ -279,13 +278,15 @@ class GraphEngine:
parallel_id=in_parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
raise e
# It may not be necessary, but it is necessary. :)
if (self.graph.node_id_config_mapping[next_node_id]
.get("data", {}).get("type", "").lower() == NodeType.END.value):
if (
self.graph.node_id_config_mapping[next_node_id].get("data", {}).get("type", "").lower()
== NodeType.END.value
):
break
previous_route_node_state = route_node_state
@ -305,7 +306,7 @@ class GraphEngine:
run_condition=edge.run_condition,
).check(
graph_runtime_state=self.graph_runtime_state,
previous_route_node_state=previous_route_node_state
previous_route_node_state=previous_route_node_state,
)
if not result:
@ -343,14 +344,14 @@ class GraphEngine:
if not result:
continue
if len(sub_edge_mappings) == 1:
final_node_id = edge.target_node_id
else:
parallel_generator = self._run_parallel_branches(
edge_mappings=sub_edge_mappings,
in_parallel_id=in_parallel_id,
parallel_start_node_id=parallel_start_node_id
parallel_start_node_id=parallel_start_node_id,
)
for item in parallel_generator:
@ -369,7 +370,7 @@ class GraphEngine:
parallel_generator = self._run_parallel_branches(
edge_mappings=edge_mappings,
in_parallel_id=in_parallel_id,
parallel_start_node_id=parallel_start_node_id
parallel_start_node_id=parallel_start_node_id,
)
for item in parallel_generator:
@ -383,14 +384,14 @@ class GraphEngine:
next_node_id = final_node_id
if in_parallel_id and self.graph.node_parallel_mapping.get(next_node_id, '') != in_parallel_id:
if in_parallel_id and self.graph.node_parallel_mapping.get(next_node_id, "") != in_parallel_id:
break
def _run_parallel_branches(
self,
edge_mappings: list[GraphEdge],
in_parallel_id: Optional[str] = None,
parallel_start_node_id: Optional[str] = None,
self,
edge_mappings: list[GraphEdge],
in_parallel_id: Optional[str] = None,
parallel_start_node_id: Optional[str] = None,
) -> Generator[GraphEngineEvent | str, None, None]:
# if nodes has no run conditions, parallel run all nodes
parallel_id = self.graph.node_parallel_mapping.get(edge_mappings[0].target_node_id)
@ -398,14 +399,18 @@ class GraphEngine:
node_id = edge_mappings[0].target_node_id
node_config = self.graph.node_id_config_mapping.get(node_id)
if not node_config:
raise GraphRunFailedError(f'Node {node_id} related parallel not found or incorrectly connected to multiple parallel branches.')
raise GraphRunFailedError(
f"Node {node_id} related parallel not found or incorrectly connected to multiple parallel branches."
)
node_title = node_config.get('data', {}).get('title')
raise GraphRunFailedError(f'Node {node_title} related parallel not found or incorrectly connected to multiple parallel branches.')
node_title = node_config.get("data", {}).get("title")
raise GraphRunFailedError(
f"Node {node_title} related parallel not found or incorrectly connected to multiple parallel branches."
)
parallel = self.graph.parallel_mapping.get(parallel_id)
if not parallel:
raise GraphRunFailedError(f'Parallel {parallel_id} not found.')
raise GraphRunFailedError(f"Parallel {parallel_id} not found.")
# run parallel nodes, run in new thread and use queue to get results
q: queue.Queue = queue.Queue()
@ -417,19 +422,22 @@ class GraphEngine:
for edge in edge_mappings:
if (
edge.target_node_id not in self.graph.node_parallel_mapping
or self.graph.node_parallel_mapping.get(edge.target_node_id, '') != parallel_id
or self.graph.node_parallel_mapping.get(edge.target_node_id, "") != parallel_id
):
continue
futures.append(
self.thread_pool.submit(self._run_parallel_node, **{
'flask_app': current_app._get_current_object(), # type: ignore[attr-defined]
'q': q,
'parallel_id': parallel_id,
'parallel_start_node_id': edge.target_node_id,
'parent_parallel_id': in_parallel_id,
'parent_parallel_start_node_id': parallel_start_node_id,
})
self.thread_pool.submit(
self._run_parallel_node,
**{
"flask_app": current_app._get_current_object(), # type: ignore[attr-defined]
"q": q,
"parallel_id": parallel_id,
"parallel_start_node_id": edge.target_node_id,
"parent_parallel_id": in_parallel_id,
"parent_parallel_start_node_id": parallel_start_node_id,
},
)
)
succeeded_count = 0
@ -451,7 +459,7 @@ class GraphEngine:
raise GraphRunFailedError(event.error)
except queue.Empty:
continue
# wait all threads
wait(futures)
@ -461,72 +469,80 @@ class GraphEngine:
yield final_node_id
def _run_parallel_node(
self,
flask_app: Flask,
q: queue.Queue,
parallel_id: str,
parallel_start_node_id: str,
parent_parallel_id: Optional[str] = None,
parent_parallel_start_node_id: Optional[str] = None,
self,
flask_app: Flask,
q: queue.Queue,
parallel_id: str,
parallel_start_node_id: str,
parent_parallel_id: Optional[str] = None,
parent_parallel_start_node_id: Optional[str] = None,
) -> None:
"""
Run parallel nodes
"""
with flask_app.app_context():
try:
q.put(ParallelBranchRunStartedEvent(
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
))
q.put(
ParallelBranchRunStartedEvent(
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
)
# run node
generator = self._run(
start_node_id=parallel_start_node_id,
in_parallel_id=parallel_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
for item in generator:
q.put(item)
# trigger graph run success event
q.put(ParallelBranchRunSucceededEvent(
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
))
q.put(
ParallelBranchRunSucceededEvent(
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
)
except GraphRunFailedError as e:
q.put(ParallelBranchRunFailedEvent(
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id,
error=e.error
))
q.put(
ParallelBranchRunFailedEvent(
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id,
error=e.error,
)
)
except Exception as e:
logger.exception("Unknown Error when generating in parallel")
q.put(ParallelBranchRunFailedEvent(
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id,
error=str(e)
))
q.put(
ParallelBranchRunFailedEvent(
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id,
error=str(e),
)
)
finally:
db.session.remove()
def _run_node(
self,
node_instance: BaseNode,
route_node_state: RouteNodeState,
parallel_id: Optional[str] = None,
parallel_start_node_id: Optional[str] = None,
parent_parallel_id: Optional[str] = None,
parent_parallel_start_node_id: Optional[str] = None,
self,
node_instance: BaseNode,
route_node_state: RouteNodeState,
parallel_id: Optional[str] = None,
parallel_start_node_id: Optional[str] = None,
parent_parallel_id: Optional[str] = None,
parent_parallel_start_node_id: Optional[str] = None,
) -> Generator[GraphEngineEvent, None, None]:
"""
Run node
@ -542,7 +558,7 @@ class GraphEngine:
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
db.session.close()
@ -567,7 +583,7 @@ class GraphEngine:
if run_result.status == WorkflowNodeExecutionStatus.FAILED:
yield NodeRunFailedEvent(
error=route_node_state.failed_reason or 'Unknown error.',
error=route_node_state.failed_reason or "Unknown error.",
id=node_instance.id,
node_id=node_instance.node_id,
node_type=node_instance.node_type,
@ -576,7 +592,7 @@ class GraphEngine:
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
elif run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED:
if run_result.metadata and run_result.metadata.get(NodeRunMetadataKey.TOTAL_TOKENS):
@ -596,7 +612,7 @@ class GraphEngine:
self._append_variables_recursively(
node_id=node_instance.node_id,
variable_key_list=[variable_key],
variable_value=variable_value
variable_value=variable_value,
)
# add parallel info to run result metadata
@ -608,7 +624,9 @@ class GraphEngine:
run_result.metadata[NodeRunMetadataKey.PARALLEL_START_NODE_ID] = parallel_start_node_id
if parent_parallel_id and parent_parallel_start_node_id:
run_result.metadata[NodeRunMetadataKey.PARENT_PARALLEL_ID] = parent_parallel_id
run_result.metadata[NodeRunMetadataKey.PARENT_PARALLEL_START_NODE_ID] = parent_parallel_start_node_id
run_result.metadata[NodeRunMetadataKey.PARENT_PARALLEL_START_NODE_ID] = (
parent_parallel_start_node_id
)
yield NodeRunSucceededEvent(
id=node_instance.id,
@ -619,7 +637,7 @@ class GraphEngine:
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
break
@ -635,7 +653,7 @@ class GraphEngine:
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
elif isinstance(item, RunRetrieverResourceEvent):
yield NodeRunRetrieverResourceEvent(
@ -649,7 +667,7 @@ class GraphEngine:
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
except GenerateTaskStoppedException:
# trigger node run failed event
@ -665,7 +683,7 @@ class GraphEngine:
parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id
parent_parallel_start_node_id=parent_parallel_start_node_id,
)
return
except Exception as e:
@ -674,10 +692,7 @@ class GraphEngine:
finally:
db.session.close()
def _append_variables_recursively(self,
node_id: str,
variable_key_list: list[str],
variable_value: VariableValue):
def _append_variables_recursively(self, node_id: str, variable_key_list: list[str], variable_value: VariableValue):
"""
Append variables recursively
:param node_id: node id
@ -685,10 +700,7 @@ class GraphEngine:
:param variable_value: variable value
:return:
"""
self.graph_runtime_state.variable_pool.add(
[node_id] + variable_key_list,
variable_value
)
self.graph_runtime_state.variable_pool.add([node_id] + variable_key_list, variable_value)
# if variable_value is a dict, then recursively append variables
if isinstance(variable_value, dict):
@ -696,9 +708,7 @@ class GraphEngine:
# construct new key list
new_key_list = variable_key_list + [key]
self._append_variables_recursively(
node_id=node_id,
variable_key_list=new_key_list,
variable_value=value
node_id=node_id, variable_key_list=new_key_list, variable_value=value
)
def _is_timed_out(self, start_at: float, max_execution_time: int) -> bool:

View file

@ -29,14 +29,12 @@ class AnswerNode(BaseNode):
# generate routes
generate_routes = AnswerStreamGeneratorRouter.extract_generate_route_from_node_data(node_data)
answer = ''
answer = ""
for part in generate_routes:
if part.type == GenerateRouteChunk.ChunkType.VAR:
part = cast(VarGenerateRouteChunk, part)
value_selector = part.value_selector
value = self.graph_runtime_state.variable_pool.get(
value_selector
)
value = self.graph_runtime_state.variable_pool.get(value_selector)
if value:
answer += value.markdown
@ -44,19 +42,11 @@ class AnswerNode(BaseNode):
part = cast(TextGenerateRouteChunk, part)
answer += part.text
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs={
"answer": answer
}
)
return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs={"answer": answer})
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: AnswerNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: AnswerNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -73,6 +63,6 @@ class AnswerNode(BaseNode):
variable_mapping = {}
for variable_selector in variable_selectors:
variable_mapping[node_id + '.' + variable_selector.variable] = variable_selector.value_selector
variable_mapping[node_id + "." + variable_selector.variable] = variable_selector.value_selector
return variable_mapping

View file

@ -1,4 +1,3 @@
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
from core.workflow.entities.node_entities import NodeType
from core.workflow.nodes.answer.entities import (
@ -12,12 +11,12 @@ from core.workflow.utils.variable_template_parser import VariableTemplateParser
class AnswerStreamGeneratorRouter:
@classmethod
def init(cls,
node_id_config_mapping: dict[str, dict],
reverse_edge_mapping: dict[str, list["GraphEdge"]] # type: ignore[name-defined]
) -> AnswerStreamGenerateRoute:
def init(
cls,
node_id_config_mapping: dict[str, dict],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
) -> AnswerStreamGenerateRoute:
"""
Get stream generate routes.
:return:
@ -25,7 +24,7 @@ class AnswerStreamGeneratorRouter:
# parse stream output node value selectors of answer nodes
answer_generate_route: dict[str, list[GenerateRouteChunk]] = {}
for answer_node_id, node_config in node_id_config_mapping.items():
if not node_config.get('data', {}).get('type') == NodeType.ANSWER.value:
if not node_config.get("data", {}).get("type") == NodeType.ANSWER.value:
continue
# get generate route for stream output
@ -37,12 +36,11 @@ class AnswerStreamGeneratorRouter:
answer_dependencies = cls._fetch_answers_dependencies(
answer_node_ids=answer_node_ids,
reverse_edge_mapping=reverse_edge_mapping,
node_id_config_mapping=node_id_config_mapping
node_id_config_mapping=node_id_config_mapping,
)
return AnswerStreamGenerateRoute(
answer_generate_route=answer_generate_route,
answer_dependencies=answer_dependencies
answer_generate_route=answer_generate_route, answer_dependencies=answer_dependencies
)
@classmethod
@ -56,8 +54,7 @@ class AnswerStreamGeneratorRouter:
variable_selectors = variable_template_parser.extract_variable_selectors()
value_selector_mapping = {
variable_selector.variable: variable_selector.value_selector
for variable_selector in variable_selectors
variable_selector.variable: variable_selector.value_selector for variable_selector in variable_selectors
}
variable_keys = list(value_selector_mapping.keys())
@ -71,21 +68,17 @@ class AnswerStreamGeneratorRouter:
template = node_data.answer
for var in variable_keys:
template = template.replace(f'{{{{{var}}}}}', f'Ω{{{{{var}}}}}Ω')
template = template.replace(f"{{{{{var}}}}}", f"Ω{{{{{var}}}}}Ω")
generate_routes: list[GenerateRouteChunk] = []
for part in template.split('Ω'):
for part in template.split("Ω"):
if part:
if cls._is_variable(part, variable_keys):
var_key = part.replace('Ω', '').replace('{{', '').replace('}}', '')
var_key = part.replace("Ω", "").replace("{{", "").replace("}}", "")
value_selector = value_selector_mapping[var_key]
generate_routes.append(VarGenerateRouteChunk(
value_selector=value_selector
))
generate_routes.append(VarGenerateRouteChunk(value_selector=value_selector))
else:
generate_routes.append(TextGenerateRouteChunk(
text=part
))
generate_routes.append(TextGenerateRouteChunk(text=part))
return generate_routes
@ -101,15 +94,16 @@ class AnswerStreamGeneratorRouter:
@classmethod
def _is_variable(cls, part, variable_keys):
cleaned_part = part.replace('{{', '').replace('}}', '')
return part.startswith('{{') and cleaned_part in variable_keys
cleaned_part = part.replace("{{", "").replace("}}", "")
return part.startswith("{{") and cleaned_part in variable_keys
@classmethod
def _fetch_answers_dependencies(cls,
answer_node_ids: list[str],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
node_id_config_mapping: dict[str, dict]
) -> dict[str, list[str]]:
def _fetch_answers_dependencies(
cls,
answer_node_ids: list[str],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
node_id_config_mapping: dict[str, dict],
) -> dict[str, list[str]]:
"""
Fetch answer dependencies
:param answer_node_ids: answer node ids
@ -127,19 +121,20 @@ class AnswerStreamGeneratorRouter:
answer_node_id=answer_node_id,
node_id_config_mapping=node_id_config_mapping,
reverse_edge_mapping=reverse_edge_mapping,
answer_dependencies=answer_dependencies
answer_dependencies=answer_dependencies,
)
return answer_dependencies
@classmethod
def _recursive_fetch_answer_dependencies(cls,
current_node_id: str,
answer_node_id: str,
node_id_config_mapping: dict[str, dict],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
answer_dependencies: dict[str, list[str]]
) -> None:
def _recursive_fetch_answer_dependencies(
cls,
current_node_id: str,
answer_node_id: str,
node_id_config_mapping: dict[str, dict],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
answer_dependencies: dict[str, list[str]],
) -> None:
"""
Recursive fetch answer dependencies
:param current_node_id: current node id
@ -152,11 +147,11 @@ class AnswerStreamGeneratorRouter:
reverse_edges = reverse_edge_mapping.get(current_node_id, [])
for edge in reverse_edges:
source_node_id = edge.source_node_id
source_node_type = node_id_config_mapping[source_node_id].get('data', {}).get('type')
source_node_type = node_id_config_mapping[source_node_id].get("data", {}).get("type")
if source_node_type in (
NodeType.ANSWER.value,
NodeType.IF_ELSE.value,
NodeType.QUESTION_CLASSIFIER,
NodeType.ANSWER.value,
NodeType.IF_ELSE.value,
NodeType.QUESTION_CLASSIFIER,
):
answer_dependencies[answer_node_id].append(source_node_id)
else:
@ -165,5 +160,5 @@ class AnswerStreamGeneratorRouter:
answer_node_id=answer_node_id,
node_id_config_mapping=node_id_config_mapping,
reverse_edge_mapping=reverse_edge_mapping,
answer_dependencies=answer_dependencies
answer_dependencies=answer_dependencies,
)

View file

@ -18,7 +18,6 @@ logger = logging.getLogger(__name__)
class AnswerStreamProcessor(StreamProcessor):
def __init__(self, graph: Graph, variable_pool: VariablePool) -> None:
super().__init__(graph, variable_pool)
self.generate_routes = graph.answer_stream_generate_routes
@ -27,9 +26,7 @@ class AnswerStreamProcessor(StreamProcessor):
self.route_position[answer_node_id] = 0
self.current_stream_chunk_generating_node_ids: dict[str, list[str]] = {}
def process(self,
generator: Generator[GraphEngineEvent, None, None]
) -> Generator[GraphEngineEvent, None, None]:
def process(self, generator: Generator[GraphEngineEvent, None, None]) -> Generator[GraphEngineEvent, None, None]:
for event in generator:
if isinstance(event, NodeRunStartedEvent):
if event.route_node_state.node_id == self.graph.root_node_id and not self.rest_node_ids:
@ -47,9 +44,9 @@ class AnswerStreamProcessor(StreamProcessor):
]
else:
stream_out_answer_node_ids = self._get_stream_out_answer_node_ids(event)
self.current_stream_chunk_generating_node_ids[
event.route_node_state.node_id
] = stream_out_answer_node_ids
self.current_stream_chunk_generating_node_ids[event.route_node_state.node_id] = (
stream_out_answer_node_ids
)
for _ in stream_out_answer_node_ids:
yield event
@ -77,9 +74,9 @@ class AnswerStreamProcessor(StreamProcessor):
self.rest_node_ids = self.graph.node_ids.copy()
self.current_stream_chunk_generating_node_ids = {}
def _generate_stream_outputs_when_node_finished(self,
event: NodeRunSucceededEvent
) -> Generator[GraphEngineEvent, None, None]:
def _generate_stream_outputs_when_node_finished(
self, event: NodeRunSucceededEvent
) -> Generator[GraphEngineEvent, None, None]:
"""
Generate stream outputs.
:param event: node run succeeded event
@ -87,10 +84,13 @@ class AnswerStreamProcessor(StreamProcessor):
"""
for answer_node_id, position in self.route_position.items():
# all depends on answer node id not in rest node ids
if (event.route_node_state.node_id != answer_node_id
and (answer_node_id not in self.rest_node_ids
or not all(dep_id not in self.rest_node_ids
for dep_id in self.generate_routes.answer_dependencies[answer_node_id]))):
if event.route_node_state.node_id != answer_node_id and (
answer_node_id not in self.rest_node_ids
or not all(
dep_id not in self.rest_node_ids
for dep_id in self.generate_routes.answer_dependencies[answer_node_id]
)
):
continue
route_position = self.route_position[answer_node_id]
@ -115,9 +115,7 @@ class AnswerStreamProcessor(StreamProcessor):
if not value_selector:
break
value = self.variable_pool.get(
value_selector
)
value = self.variable_pool.get(value_selector)
if value is None:
break
@ -158,8 +156,9 @@ class AnswerStreamProcessor(StreamProcessor):
continue
# all depends on answer node id not in rest node ids
if all(dep_id not in self.rest_node_ids
for dep_id in self.generate_routes.answer_dependencies[answer_node_id]):
if all(
dep_id not in self.rest_node_ids for dep_id in self.generate_routes.answer_dependencies[answer_node_id]
):
if route_position >= len(self.generate_routes.answer_generate_route[answer_node_id]):
continue
@ -213,7 +212,7 @@ class AnswerStreamProcessor(StreamProcessor):
return None
if isinstance(value, dict):
if '__variant' in value and value['__variant'] == FileVar.__name__:
if "__variant" in value and value["__variant"] == FileVar.__name__:
return value
elif isinstance(value, FileVar):
return value.to_dict()

View file

@ -7,16 +7,13 @@ from core.workflow.graph_engine.entities.graph import Graph
class StreamProcessor(ABC):
def __init__(self, graph: Graph, variable_pool: VariablePool) -> None:
self.graph = graph
self.variable_pool = variable_pool
self.rest_node_ids = graph.node_ids.copy()
@abstractmethod
def process(self,
generator: Generator[GraphEngineEvent, None, None]
) -> Generator[GraphEngineEvent, None, None]:
def process(self, generator: Generator[GraphEngineEvent, None, None]) -> Generator[GraphEngineEvent, None, None]:
raise NotImplementedError
def _remove_unreachable_nodes(self, event: NodeRunSucceededEvent) -> None:
@ -35,9 +32,11 @@ class StreamProcessor(ABC):
reachable_node_ids = []
unreachable_first_node_ids = []
for edge in self.graph.edge_mapping[finished_node_id]:
if (edge.run_condition
and edge.run_condition.branch_identify
and run_result.edge_source_handle == edge.run_condition.branch_identify):
if (
edge.run_condition
and edge.run_condition.branch_identify
and run_result.edge_source_handle == edge.run_condition.branch_identify
):
reachable_node_ids.extend(self._fetch_node_ids_in_reachable_branch(edge.target_node_id))
continue
else:

View file

@ -9,6 +9,7 @@ class AnswerNodeData(BaseNodeData):
"""
Answer Node Data.
"""
answer: str = Field(..., description="answer template string")
@ -28,6 +29,7 @@ class VarGenerateRouteChunk(GenerateRouteChunk):
"""
Var Generate Route Chunk.
"""
type: GenerateRouteChunk.ChunkType = GenerateRouteChunk.ChunkType.VAR
"""generate route chunk type"""
value_selector: list[str] = Field(..., description="value selector")
@ -37,6 +39,7 @@ class TextGenerateRouteChunk(GenerateRouteChunk):
"""
Text Generate Route Chunk.
"""
type: GenerateRouteChunk.ChunkType = GenerateRouteChunk.ChunkType.TEXT
"""generate route chunk type"""
text: str = Field(..., description="text")
@ -52,11 +55,10 @@ class AnswerStreamGenerateRoute(BaseModel):
"""
AnswerStreamGenerateRoute entity
"""
answer_dependencies: dict[str, list[str]] = Field(
...,
description="answer dependencies (answer node id -> dependent answer node ids)"
..., description="answer dependencies (answer node id -> dependent answer node ids)"
)
answer_generate_route: dict[str, list[GenerateRouteChunk]] = Field(
...,
description="answer generate route (answer node id -> generate route chunks)"
..., description="answer generate route (answer node id -> generate route chunks)"
)

View file

@ -15,14 +15,16 @@ class BaseNode(ABC):
_node_data_cls: type[BaseNodeData]
_node_type: NodeType
def __init__(self,
id: str,
config: Mapping[str, Any],
graph_init_params: GraphInitParams,
graph: Graph,
graph_runtime_state: GraphRuntimeState,
previous_node_id: Optional[str] = None,
thread_pool_id: Optional[str] = None) -> None:
def __init__(
self,
id: str,
config: Mapping[str, Any],
graph_init_params: GraphInitParams,
graph: Graph,
graph_runtime_state: GraphRuntimeState,
previous_node_id: Optional[str] = None,
thread_pool_id: Optional[str] = None,
) -> None:
self.id = id
self.tenant_id = graph_init_params.tenant_id
self.app_id = graph_init_params.app_id
@ -46,8 +48,7 @@ class BaseNode(ABC):
self.node_data = self._node_data_cls(**config.get("data", {}))
@abstractmethod
def _run(self) \
-> NodeRunResult | Generator[RunEvent | InNodeEvent, None, None]:
def _run(self) -> NodeRunResult | Generator[RunEvent | InNodeEvent, None, None]:
"""
Run node
:return:
@ -62,14 +63,14 @@ class BaseNode(ABC):
result = self._run()
if isinstance(result, NodeRunResult):
yield RunCompletedEvent(
run_result=result
)
yield RunCompletedEvent(run_result=result)
else:
yield from result
@classmethod
def extract_variable_selector_to_variable_mapping(cls, graph_config: Mapping[str, Any], config: dict) -> Mapping[str, Sequence[str]]:
def extract_variable_selector_to_variable_mapping(
cls, graph_config: Mapping[str, Any], config: dict
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
:param graph_config: graph config
@ -82,17 +83,12 @@ class BaseNode(ABC):
node_data = cls._node_data_cls(**config.get("data", {}))
return cls._extract_variable_selector_to_variable_mapping(
graph_config=graph_config,
node_id=node_id,
node_data=node_data
graph_config=graph_config, node_id=node_id, node_data=node_data
)
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: BaseNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: BaseNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping

View file

@ -25,11 +25,10 @@ class CodeNode(BaseNode):
"""
code_language = CodeLanguage.PYTHON3
if filters:
code_language = (filters.get("code_language", CodeLanguage.PYTHON3))
code_language = filters.get("code_language", CodeLanguage.PYTHON3)
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
code_provider: type[CodeNodeProvider] = next(p for p in providers
if p.is_accept_language(code_language))
code_provider: type[CodeNodeProvider] = next(p for p in providers if p.is_accept_language(code_language))
return code_provider.get_default_config()
@ -63,17 +62,9 @@ class CodeNode(BaseNode):
# Transform result
result = self._transform_result(result, node_data.outputs)
except (CodeExecutionException, ValueError) as e:
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs=variables,
error=str(e)
)
return NodeRunResult(status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, error=str(e))
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs=variables,
outputs=result
)
return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=variables, outputs=result)
def _check_string(self, value: str, variable: str) -> str:
"""
@ -87,12 +78,14 @@ class CodeNode(BaseNode):
return None
else:
raise ValueError(f"Output variable `{variable}` must be a string")
if len(value) > dify_config.CODE_MAX_STRING_LENGTH:
raise ValueError(f'The length of output variable `{variable}` must be'
f' less than {dify_config.CODE_MAX_STRING_LENGTH} characters')
return value.replace('\x00', '')
if len(value) > dify_config.CODE_MAX_STRING_LENGTH:
raise ValueError(
f"The length of output variable `{variable}` must be"
f" less than {dify_config.CODE_MAX_STRING_LENGTH} characters"
)
return value.replace("\x00", "")
def _check_number(self, value: Union[int, float], variable: str) -> Union[int, float]:
"""
@ -108,20 +101,24 @@ class CodeNode(BaseNode):
raise ValueError(f"Output variable `{variable}` must be a number")
if value > dify_config.CODE_MAX_NUMBER or value < dify_config.CODE_MIN_NUMBER:
raise ValueError(f'Output variable `{variable}` is out of range,'
f' it must be between {dify_config.CODE_MIN_NUMBER} and {dify_config.CODE_MAX_NUMBER}.')
raise ValueError(
f"Output variable `{variable}` is out of range,"
f" it must be between {dify_config.CODE_MIN_NUMBER} and {dify_config.CODE_MAX_NUMBER}."
)
if isinstance(value, float):
# raise error if precision is too high
if len(str(value).split('.')[1]) > dify_config.CODE_MAX_PRECISION:
raise ValueError(f'Output variable `{variable}` has too high precision,'
f' it must be less than {dify_config.CODE_MAX_PRECISION} digits.')
if len(str(value).split(".")[1]) > dify_config.CODE_MAX_PRECISION:
raise ValueError(
f"Output variable `{variable}` has too high precision,"
f" it must be less than {dify_config.CODE_MAX_PRECISION} digits."
)
return value
def _transform_result(self, result: dict, output_schema: Optional[dict[str, CodeNodeData.Output]],
prefix: str = '',
depth: int = 1) -> dict:
def _transform_result(
self, result: dict, output_schema: Optional[dict[str, CodeNodeData.Output]], prefix: str = "", depth: int = 1
) -> dict:
"""
Transform result
:param result: result
@ -139,183 +136,181 @@ class CodeNode(BaseNode):
self._transform_result(
result=output_value,
output_schema=None,
prefix=f'{prefix}.{output_name}' if prefix else output_name,
depth=depth + 1
prefix=f"{prefix}.{output_name}" if prefix else output_name,
depth=depth + 1,
)
elif isinstance(output_value, int | float):
self._check_number(
value=output_value,
variable=f'{prefix}.{output_name}' if prefix else output_name
value=output_value, variable=f"{prefix}.{output_name}" if prefix else output_name
)
elif isinstance(output_value, str):
self._check_string(
value=output_value,
variable=f'{prefix}.{output_name}' if prefix else output_name
value=output_value, variable=f"{prefix}.{output_name}" if prefix else output_name
)
elif isinstance(output_value, list):
first_element = output_value[0] if len(output_value) > 0 else None
if first_element is not None:
if isinstance(first_element, int | float) and all(value is None or isinstance(value, int | float) for value in output_value):
if isinstance(first_element, int | float) and all(
value is None or isinstance(value, int | float) for value in output_value
):
for i, value in enumerate(output_value):
self._check_number(
value=value,
variable=f'{prefix}.{output_name}[{i}]' if prefix else f'{output_name}[{i}]'
variable=f"{prefix}.{output_name}[{i}]" if prefix else f"{output_name}[{i}]",
)
elif isinstance(first_element, str) and all(value is None or isinstance(value, str) for value in output_value):
elif isinstance(first_element, str) and all(
value is None or isinstance(value, str) for value in output_value
):
for i, value in enumerate(output_value):
self._check_string(
value=value,
variable=f'{prefix}.{output_name}[{i}]' if prefix else f'{output_name}[{i}]'
variable=f"{prefix}.{output_name}[{i}]" if prefix else f"{output_name}[{i}]",
)
elif isinstance(first_element, dict) and all(value is None or isinstance(value, dict) for value in output_value):
elif isinstance(first_element, dict) and all(
value is None or isinstance(value, dict) for value in output_value
):
for i, value in enumerate(output_value):
if value is not None:
self._transform_result(
result=value,
output_schema=None,
prefix=f'{prefix}.{output_name}[{i}]' if prefix else f'{output_name}[{i}]',
depth=depth + 1
prefix=f"{prefix}.{output_name}[{i}]" if prefix else f"{output_name}[{i}]",
depth=depth + 1,
)
else:
raise ValueError(f'Output {prefix}.{output_name} is not a valid array. make sure all elements are of the same type.')
raise ValueError(
f"Output {prefix}.{output_name} is not a valid array. make sure all elements are of the same type."
)
elif isinstance(output_value, type(None)):
pass
else:
raise ValueError(f'Output {prefix}.{output_name} is not a valid type.')
raise ValueError(f"Output {prefix}.{output_name} is not a valid type.")
return result
parameters_validated = {}
for output_name, output_config in output_schema.items():
dot = '.' if prefix else ''
dot = "." if prefix else ""
if output_name not in result:
raise ValueError(f'Output {prefix}{dot}{output_name} is missing.')
if output_config.type == 'object':
raise ValueError(f"Output {prefix}{dot}{output_name} is missing.")
if output_config.type == "object":
# check if output is object
if not isinstance(result.get(output_name), dict):
if isinstance(result.get(output_name), type(None)):
transformed_result[output_name] = None
else:
raise ValueError(
f'Output {prefix}{dot}{output_name} is not an object, got {type(result.get(output_name))} instead.'
f"Output {prefix}{dot}{output_name} is not an object, got {type(result.get(output_name))} instead."
)
else:
transformed_result[output_name] = self._transform_result(
result=result[output_name],
output_schema=output_config.children,
prefix=f'{prefix}.{output_name}',
depth=depth + 1
prefix=f"{prefix}.{output_name}",
depth=depth + 1,
)
elif output_config.type == 'number':
elif output_config.type == "number":
# check if number available
transformed_result[output_name] = self._check_number(
value=result[output_name],
variable=f'{prefix}{dot}{output_name}'
value=result[output_name], variable=f"{prefix}{dot}{output_name}"
)
elif output_config.type == 'string':
elif output_config.type == "string":
# check if string available
transformed_result[output_name] = self._check_string(
value=result[output_name],
variable=f'{prefix}{dot}{output_name}',
variable=f"{prefix}{dot}{output_name}",
)
elif output_config.type == 'array[number]':
elif output_config.type == "array[number]":
# check if array of number available
if not isinstance(result[output_name], list):
if isinstance(result[output_name], type(None)):
transformed_result[output_name] = None
else:
raise ValueError(
f'Output {prefix}{dot}{output_name} is not an array, got {type(result.get(output_name))} instead.'
f"Output {prefix}{dot}{output_name} is not an array, got {type(result.get(output_name))} instead."
)
else:
if len(result[output_name]) > dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH:
raise ValueError(
f'The length of output variable `{prefix}{dot}{output_name}` must be'
f' less than {dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH} elements.'
f"The length of output variable `{prefix}{dot}{output_name}` must be"
f" less than {dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH} elements."
)
transformed_result[output_name] = [
self._check_number(
value=value,
variable=f'{prefix}{dot}{output_name}[{i}]'
)
self._check_number(value=value, variable=f"{prefix}{dot}{output_name}[{i}]")
for i, value in enumerate(result[output_name])
]
elif output_config.type == 'array[string]':
elif output_config.type == "array[string]":
# check if array of string available
if not isinstance(result[output_name], list):
if isinstance(result[output_name], type(None)):
transformed_result[output_name] = None
else:
raise ValueError(
f'Output {prefix}{dot}{output_name} is not an array, got {type(result.get(output_name))} instead.'
f"Output {prefix}{dot}{output_name} is not an array, got {type(result.get(output_name))} instead."
)
else:
if len(result[output_name]) > dify_config.CODE_MAX_STRING_ARRAY_LENGTH:
raise ValueError(
f'The length of output variable `{prefix}{dot}{output_name}` must be'
f' less than {dify_config.CODE_MAX_STRING_ARRAY_LENGTH} elements.'
f"The length of output variable `{prefix}{dot}{output_name}` must be"
f" less than {dify_config.CODE_MAX_STRING_ARRAY_LENGTH} elements."
)
transformed_result[output_name] = [
self._check_string(
value=value,
variable=f'{prefix}{dot}{output_name}[{i}]'
)
self._check_string(value=value, variable=f"{prefix}{dot}{output_name}[{i}]")
for i, value in enumerate(result[output_name])
]
elif output_config.type == 'array[object]':
elif output_config.type == "array[object]":
# check if array of object available
if not isinstance(result[output_name], list):
if isinstance(result[output_name], type(None)):
transformed_result[output_name] = None
else:
raise ValueError(
f'Output {prefix}{dot}{output_name} is not an array, got {type(result.get(output_name))} instead.'
f"Output {prefix}{dot}{output_name} is not an array, got {type(result.get(output_name))} instead."
)
else:
if len(result[output_name]) > dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH:
raise ValueError(
f'The length of output variable `{prefix}{dot}{output_name}` must be'
f' less than {dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH} elements.'
f"The length of output variable `{prefix}{dot}{output_name}` must be"
f" less than {dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH} elements."
)
for i, value in enumerate(result[output_name]):
if not isinstance(value, dict):
if isinstance(value, type(None)):
pass
else:
raise ValueError(
f'Output {prefix}{dot}{output_name}[{i}] is not an object, got {type(value)} instead at index {i}.'
f"Output {prefix}{dot}{output_name}[{i}] is not an object, got {type(value)} instead at index {i}."
)
transformed_result[output_name] = [
None if value is None else self._transform_result(
None
if value is None
else self._transform_result(
result=value,
output_schema=output_config.children,
prefix=f'{prefix}{dot}{output_name}[{i}]',
depth=depth + 1
prefix=f"{prefix}{dot}{output_name}[{i}]",
depth=depth + 1,
)
for i, value in enumerate(result[output_name])
]
else:
raise ValueError(f'Output type {output_config.type} is not supported.')
raise ValueError(f"Output type {output_config.type} is not supported.")
parameters_validated[output_name] = True
# check if all output parameters are validated
if len(parameters_validated) != len(result):
raise ValueError('Not all output parameters are validated.')
raise ValueError("Not all output parameters are validated.")
return transformed_result
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: CodeNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: CodeNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -325,5 +320,6 @@ class CodeNode(BaseNode):
:return:
"""
return {
node_id + '.' + variable_selector.variable: variable_selector.value_selector for variable_selector in node_data.variables
node_id + "." + variable_selector.variable: variable_selector.value_selector
for variable_selector in node_data.variables
}

View file

@ -11,9 +11,10 @@ class CodeNodeData(BaseNodeData):
"""
Code Node Data.
"""
class Output(BaseModel):
type: Literal['string', 'number', 'object', 'array[string]', 'array[number]', 'array[object]']
children: Optional[dict[str, 'Output']] = None
type: Literal["string", "number", "object", "array[string]", "array[number]", "array[object]"]
children: Optional[dict[str, "Output"]] = None
class Dependency(BaseModel):
name: str
@ -23,4 +24,4 @@ class CodeNodeData(BaseNodeData):
code_language: Literal[CodeLanguage.PYTHON3, CodeLanguage.JAVASCRIPT]
code: str
outputs: dict[str, Output]
dependencies: Optional[list[Dependency]] = None
dependencies: Optional[list[Dependency]] = None

View file

@ -25,18 +25,11 @@ class EndNode(BaseNode):
value = self.graph_runtime_state.variable_pool.get_any(variable_selector.value_selector)
outputs[variable_selector.variable] = value
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs=outputs,
outputs=outputs
)
return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=outputs, outputs=outputs)
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: EndNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: EndNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping

View file

@ -3,13 +3,13 @@ from core.workflow.nodes.end.entities import EndNodeData, EndStreamParam
class EndStreamGeneratorRouter:
@classmethod
def init(cls,
node_id_config_mapping: dict[str, dict],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
node_parallel_mapping: dict[str, str]
) -> EndStreamParam:
def init(
cls,
node_id_config_mapping: dict[str, dict],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
node_parallel_mapping: dict[str, str],
) -> EndStreamParam:
"""
Get stream generate routes.
:return:
@ -17,7 +17,7 @@ class EndStreamGeneratorRouter:
# parse stream output node value selector of end nodes
end_stream_variable_selectors_mapping: dict[str, list[list[str]]] = {}
for end_node_id, node_config in node_id_config_mapping.items():
if not node_config.get('data', {}).get('type') == NodeType.END.value:
if not node_config.get("data", {}).get("type") == NodeType.END.value:
continue
# skip end node in parallel
@ -33,18 +33,18 @@ class EndStreamGeneratorRouter:
end_dependencies = cls._fetch_ends_dependencies(
end_node_ids=end_node_ids,
reverse_edge_mapping=reverse_edge_mapping,
node_id_config_mapping=node_id_config_mapping
node_id_config_mapping=node_id_config_mapping,
)
return EndStreamParam(
end_stream_variable_selector_mapping=end_stream_variable_selectors_mapping,
end_dependencies=end_dependencies
end_dependencies=end_dependencies,
)
@classmethod
def extract_stream_variable_selector_from_node_data(cls,
node_id_config_mapping: dict[str, dict],
node_data: EndNodeData) -> list[list[str]]:
def extract_stream_variable_selector_from_node_data(
cls, node_id_config_mapping: dict[str, dict], node_data: EndNodeData
) -> list[list[str]]:
"""
Extract stream variable selector from node data
:param node_id_config_mapping: node id config mapping
@ -59,21 +59,22 @@ class EndStreamGeneratorRouter:
continue
node_id = variable_selector.value_selector[0]
if node_id != 'sys' and node_id in node_id_config_mapping:
if node_id != "sys" and node_id in node_id_config_mapping:
node = node_id_config_mapping[node_id]
node_type = node.get('data', {}).get('type')
node_type = node.get("data", {}).get("type")
if (
variable_selector.value_selector not in value_selectors
and node_type == NodeType.LLM.value
and variable_selector.value_selector[1] == 'text'
and node_type == NodeType.LLM.value
and variable_selector.value_selector[1] == "text"
):
value_selectors.append(variable_selector.value_selector)
return value_selectors
@classmethod
def _extract_stream_variable_selector(cls, node_id_config_mapping: dict[str, dict], config: dict) \
-> list[list[str]]:
def _extract_stream_variable_selector(
cls, node_id_config_mapping: dict[str, dict], config: dict
) -> list[list[str]]:
"""
Extract stream variable selector from node config
:param node_id_config_mapping: node id config mapping
@ -84,11 +85,12 @@ class EndStreamGeneratorRouter:
return cls.extract_stream_variable_selector_from_node_data(node_id_config_mapping, node_data)
@classmethod
def _fetch_ends_dependencies(cls,
end_node_ids: list[str],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
node_id_config_mapping: dict[str, dict]
) -> dict[str, list[str]]:
def _fetch_ends_dependencies(
cls,
end_node_ids: list[str],
reverse_edge_mapping: dict[str, list["GraphEdge"]], # type: ignore[name-defined]
node_id_config_mapping: dict[str, dict],
) -> dict[str, list[str]]:
"""
Fetch end dependencies
:param end_node_ids: end node ids
@ -106,20 +108,21 @@ class EndStreamGeneratorRouter:
end_node_id=end_node_id,
node_id_config_mapping=node_id_config_mapping,
reverse_edge_mapping=reverse_edge_mapping,
end_dependencies=end_dependencies
end_dependencies=end_dependencies,
)
return end_dependencies
@classmethod
def _recursive_fetch_end_dependencies(cls,
current_node_id: str,
end_node_id: str,
node_id_config_mapping: dict[str, dict],
reverse_edge_mapping: dict[str, list["GraphEdge"]],
# type: ignore[name-defined]
end_dependencies: dict[str, list[str]]
) -> None:
def _recursive_fetch_end_dependencies(
cls,
current_node_id: str,
end_node_id: str,
node_id_config_mapping: dict[str, dict],
reverse_edge_mapping: dict[str, list["GraphEdge"]],
# type: ignore[name-defined]
end_dependencies: dict[str, list[str]],
) -> None:
"""
Recursive fetch end dependencies
:param current_node_id: current node id
@ -132,10 +135,10 @@ class EndStreamGeneratorRouter:
reverse_edges = reverse_edge_mapping.get(current_node_id, [])
for edge in reverse_edges:
source_node_id = edge.source_node_id
source_node_type = node_id_config_mapping[source_node_id].get('data', {}).get('type')
source_node_type = node_id_config_mapping[source_node_id].get("data", {}).get("type")
if source_node_type in (
NodeType.IF_ELSE.value,
NodeType.QUESTION_CLASSIFIER,
NodeType.IF_ELSE.value,
NodeType.QUESTION_CLASSIFIER,
):
end_dependencies[end_node_id].append(source_node_id)
else:
@ -144,5 +147,5 @@ class EndStreamGeneratorRouter:
end_node_id=end_node_id,
node_id_config_mapping=node_id_config_mapping,
reverse_edge_mapping=reverse_edge_mapping,
end_dependencies=end_dependencies
end_dependencies=end_dependencies,
)

View file

@ -15,7 +15,6 @@ logger = logging.getLogger(__name__)
class EndStreamProcessor(StreamProcessor):
def __init__(self, graph: Graph, variable_pool: VariablePool) -> None:
super().__init__(graph, variable_pool)
self.end_stream_param = graph.end_stream_param
@ -26,9 +25,7 @@ class EndStreamProcessor(StreamProcessor):
self.has_outputed = False
self.outputed_node_ids = set()
def process(self,
generator: Generator[GraphEngineEvent, None, None]
) -> Generator[GraphEngineEvent, None, None]:
def process(self, generator: Generator[GraphEngineEvent, None, None]) -> Generator[GraphEngineEvent, None, None]:
for event in generator:
if isinstance(event, NodeRunStartedEvent):
if event.route_node_state.node_id == self.graph.root_node_id and not self.rest_node_ids:
@ -38,7 +35,7 @@ class EndStreamProcessor(StreamProcessor):
elif isinstance(event, NodeRunStreamChunkEvent):
if event.in_iteration_id:
if self.has_outputed and event.node_id not in self.outputed_node_ids:
event.chunk_content = '\n' + event.chunk_content
event.chunk_content = "\n" + event.chunk_content
self.outputed_node_ids.add(event.node_id)
self.has_outputed = True
@ -51,13 +48,13 @@ class EndStreamProcessor(StreamProcessor):
]
else:
stream_out_end_node_ids = self._get_stream_out_end_node_ids(event)
self.current_stream_chunk_generating_node_ids[
event.route_node_state.node_id
] = stream_out_end_node_ids
self.current_stream_chunk_generating_node_ids[event.route_node_state.node_id] = (
stream_out_end_node_ids
)
if stream_out_end_node_ids:
if self.has_outputed and event.node_id not in self.outputed_node_ids:
event.chunk_content = '\n' + event.chunk_content
event.chunk_content = "\n" + event.chunk_content
self.outputed_node_ids.add(event.node_id)
self.has_outputed = True
@ -86,9 +83,9 @@ class EndStreamProcessor(StreamProcessor):
self.rest_node_ids = self.graph.node_ids.copy()
self.current_stream_chunk_generating_node_ids = {}
def _generate_stream_outputs_when_node_finished(self,
event: NodeRunSucceededEvent
) -> Generator[GraphEngineEvent, None, None]:
def _generate_stream_outputs_when_node_finished(
self, event: NodeRunSucceededEvent
) -> Generator[GraphEngineEvent, None, None]:
"""
Generate stream outputs.
:param event: node run succeeded event
@ -96,10 +93,12 @@ class EndStreamProcessor(StreamProcessor):
"""
for end_node_id, position in self.route_position.items():
# all depends on end node id not in rest node ids
if (event.route_node_state.node_id != end_node_id
and (end_node_id not in self.rest_node_ids
or not all(dep_id not in self.rest_node_ids
for dep_id in self.end_stream_param.end_dependencies[end_node_id]))):
if event.route_node_state.node_id != end_node_id and (
end_node_id not in self.rest_node_ids
or not all(
dep_id not in self.rest_node_ids for dep_id in self.end_stream_param.end_dependencies[end_node_id]
)
):
continue
route_position = self.route_position[end_node_id]
@ -116,9 +115,7 @@ class EndStreamProcessor(StreamProcessor):
if not value_selector:
continue
value = self.variable_pool.get(
value_selector
)
value = self.variable_pool.get(value_selector)
if value is None:
break
@ -128,7 +125,7 @@ class EndStreamProcessor(StreamProcessor):
if text:
current_node_id = value_selector[0]
if self.has_outputed and current_node_id not in self.outputed_node_ids:
text = '\n' + text
text = "\n" + text
self.outputed_node_ids.add(current_node_id)
self.has_outputed = True
@ -165,8 +162,7 @@ class EndStreamProcessor(StreamProcessor):
continue
# all depends on end node id not in rest node ids
if all(dep_id not in self.rest_node_ids
for dep_id in self.end_stream_param.end_dependencies[end_node_id]):
if all(dep_id not in self.rest_node_ids for dep_id in self.end_stream_param.end_dependencies[end_node_id]):
if route_position >= len(self.end_stream_param.end_stream_variable_selector_mapping[end_node_id]):
continue
@ -178,7 +174,7 @@ class EndStreamProcessor(StreamProcessor):
break
position += 1
if not value_selector:
continue

View file

@ -8,6 +8,7 @@ class EndNodeData(BaseNodeData):
"""
END Node Data.
"""
outputs: list[VariableSelector]
@ -15,11 +16,10 @@ class EndStreamParam(BaseModel):
"""
EndStreamParam entity
"""
end_dependencies: dict[str, list[str]] = Field(
...,
description="end dependencies (end node id -> dependent node ids)"
..., description="end dependencies (end node id -> dependent node ids)"
)
end_stream_variable_selector_mapping: dict[str, list[list[str]]] = Field(
...,
description="end stream variable selector mapping (end node id -> stream variable selectors)"
..., description="end stream variable selector mapping (end node id -> stream variable selectors)"
)

View file

@ -7,32 +7,32 @@ from core.workflow.entities.base_node_data_entities import BaseNodeData
class HttpRequestNodeAuthorizationConfig(BaseModel):
type: Literal[None, 'basic', 'bearer', 'custom']
type: Literal[None, "basic", "bearer", "custom"]
api_key: Union[None, str] = None
header: Union[None, str] = None
class HttpRequestNodeAuthorization(BaseModel):
type: Literal['no-auth', 'api-key']
type: Literal["no-auth", "api-key"]
config: Optional[HttpRequestNodeAuthorizationConfig] = None
@field_validator('config', mode='before')
@field_validator("config", mode="before")
@classmethod
def check_config(cls, v: HttpRequestNodeAuthorizationConfig, values: ValidationInfo):
"""
Check config, if type is no-auth, config should be None, otherwise it should be a dict.
"""
if values.data['type'] == 'no-auth':
if values.data["type"] == "no-auth":
return None
else:
if not v or not isinstance(v, dict):
raise ValueError('config should be a dict')
raise ValueError("config should be a dict")
return v
class HttpRequestNodeBody(BaseModel):
type: Literal['none', 'form-data', 'x-www-form-urlencoded', 'raw-text', 'json']
type: Literal["none", "form-data", "x-www-form-urlencoded", "raw-text", "json"]
data: Union[None, str] = None
@ -47,7 +47,7 @@ class HttpRequestNodeData(BaseNodeData):
Code Node Data.
"""
method: Literal['get', 'post', 'put', 'patch', 'delete', 'head']
method: Literal["get", "post", "put", "patch", "delete", "head"]
url: str
authorization: HttpRequestNodeAuthorization
headers: str

View file

@ -33,12 +33,12 @@ class HttpExecutorResponse:
check if response is file
"""
content_type = self.get_content_type()
file_content_types = ['image', 'audio', 'video']
file_content_types = ["image", "audio", "video"]
return any(v in content_type for v in file_content_types)
def get_content_type(self) -> str:
return self.headers.get('content-type', '')
return self.headers.get("content-type", "")
def extract_file(self) -> tuple[str, bytes]:
"""
@ -47,28 +47,28 @@ class HttpExecutorResponse:
if self.is_file:
return self.get_content_type(), self.body
return '', b''
return "", b""
@property
def content(self) -> str:
if isinstance(self.response, httpx.Response):
return self.response.text
else:
raise ValueError(f'Invalid response type {type(self.response)}')
raise ValueError(f"Invalid response type {type(self.response)}")
@property
def body(self) -> bytes:
if isinstance(self.response, httpx.Response):
return self.response.content
else:
raise ValueError(f'Invalid response type {type(self.response)}')
raise ValueError(f"Invalid response type {type(self.response)}")
@property
def status_code(self) -> int:
if isinstance(self.response, httpx.Response):
return self.response.status_code
else:
raise ValueError(f'Invalid response type {type(self.response)}')
raise ValueError(f"Invalid response type {type(self.response)}")
@property
def size(self) -> int:
@ -77,11 +77,11 @@ class HttpExecutorResponse:
@property
def readable_size(self) -> str:
if self.size < 1024:
return f'{self.size} bytes'
return f"{self.size} bytes"
elif self.size < 1024 * 1024:
return f'{(self.size / 1024):.2f} KB'
return f"{(self.size / 1024):.2f} KB"
else:
return f'{(self.size / 1024 / 1024):.2f} MB'
return f"{(self.size / 1024 / 1024):.2f} MB"
class HttpExecutor:
@ -120,7 +120,7 @@ class HttpExecutor:
"""
check if body is json
"""
if body and body.type == 'json' and body.data:
if body and body.type == "json" and body.data:
try:
json.loads(body.data)
return True
@ -134,15 +134,15 @@ class HttpExecutor:
"""
Convert the string like `aa:bb\n cc:dd` to dict `{aa:bb, cc:dd}`
"""
kv_paris = convert_text.split('\n')
kv_paris = convert_text.split("\n")
result = {}
for kv in kv_paris:
if not kv.strip():
continue
kv = kv.split(':', maxsplit=1)
kv = kv.split(":", maxsplit=1)
if len(kv) == 1:
k, v = kv[0], ''
k, v = kv[0], ""
else:
k, v = kv
result[k.strip()] = v
@ -166,31 +166,31 @@ class HttpExecutor:
# check if it's a valid JSON
is_valid_json = self._is_json_body(node_data.body)
body_data = node_data.body.data or ''
body_data = node_data.body.data or ""
if body_data:
body_data, body_data_variable_selectors = self._format_template(body_data, variable_pool, is_valid_json)
content_type_is_set = any(key.lower() == 'content-type' for key in self.headers)
if node_data.body.type == 'json' and not content_type_is_set:
self.headers['Content-Type'] = 'application/json'
elif node_data.body.type == 'x-www-form-urlencoded' and not content_type_is_set:
self.headers['Content-Type'] = 'application/x-www-form-urlencoded'
content_type_is_set = any(key.lower() == "content-type" for key in self.headers)
if node_data.body.type == "json" and not content_type_is_set:
self.headers["Content-Type"] = "application/json"
elif node_data.body.type == "x-www-form-urlencoded" and not content_type_is_set:
self.headers["Content-Type"] = "application/x-www-form-urlencoded"
if node_data.body.type in ['form-data', 'x-www-form-urlencoded']:
if node_data.body.type in ["form-data", "x-www-form-urlencoded"]:
body = self._to_dict(body_data)
if node_data.body.type == 'form-data':
self.files = {k: ('', v) for k, v in body.items()}
random_str = lambda n: ''.join([chr(randint(97, 122)) for _ in range(n)])
self.boundary = f'----WebKitFormBoundary{random_str(16)}'
if node_data.body.type == "form-data":
self.files = {k: ("", v) for k, v in body.items()}
random_str = lambda n: "".join([chr(randint(97, 122)) for _ in range(n)])
self.boundary = f"----WebKitFormBoundary{random_str(16)}"
self.headers['Content-Type'] = f'multipart/form-data; boundary={self.boundary}'
self.headers["Content-Type"] = f"multipart/form-data; boundary={self.boundary}"
else:
self.body = urlencode(body)
elif node_data.body.type in ['json', 'raw-text']:
elif node_data.body.type in ["json", "raw-text"]:
self.body = body_data
elif node_data.body.type == 'none':
self.body = ''
elif node_data.body.type == "none":
self.body = ""
self.variable_selectors = (
server_url_variable_selectors
@ -202,23 +202,23 @@ class HttpExecutor:
def _assembling_headers(self) -> dict[str, Any]:
authorization = deepcopy(self.authorization)
headers = deepcopy(self.headers) or {}
if self.authorization.type == 'api-key':
if self.authorization.type == "api-key":
if self.authorization.config is None:
raise ValueError('self.authorization config is required')
raise ValueError("self.authorization config is required")
if authorization.config is None:
raise ValueError('authorization config is required')
raise ValueError("authorization config is required")
if self.authorization.config.api_key is None:
raise ValueError('api_key is required')
raise ValueError("api_key is required")
if not authorization.config.header:
authorization.config.header = 'Authorization'
authorization.config.header = "Authorization"
if self.authorization.config.type == 'bearer':
headers[authorization.config.header] = f'Bearer {authorization.config.api_key}'
elif self.authorization.config.type == 'basic':
headers[authorization.config.header] = f'Basic {authorization.config.api_key}'
elif self.authorization.config.type == 'custom':
if self.authorization.config.type == "bearer":
headers[authorization.config.header] = f"Bearer {authorization.config.api_key}"
elif self.authorization.config.type == "basic":
headers[authorization.config.header] = f"Basic {authorization.config.api_key}"
elif self.authorization.config.type == "custom":
headers[authorization.config.header] = authorization.config.api_key
return headers
@ -230,10 +230,13 @@ class HttpExecutor:
if isinstance(response, httpx.Response):
executor_response = HttpExecutorResponse(response)
else:
raise ValueError(f'Invalid response type {type(response)}')
raise ValueError(f"Invalid response type {type(response)}")
threshold_size = dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE if executor_response.is_file \
threshold_size = (
dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE
if executor_response.is_file
else dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE
)
if executor_response.size > threshold_size:
raise ValueError(
f'{"File" if executor_response.is_file else "Text"} size is too large,'
@ -248,17 +251,17 @@ class HttpExecutor:
do http request depending on api bundle
"""
kwargs = {
'url': self.server_url,
'headers': headers,
'params': self.params,
'timeout': (self.timeout.connect, self.timeout.read, self.timeout.write),
'follow_redirects': True,
"url": self.server_url,
"headers": headers,
"params": self.params,
"timeout": (self.timeout.connect, self.timeout.read, self.timeout.write),
"follow_redirects": True,
}
if self.method in ('get', 'head', 'post', 'put', 'delete', 'patch'):
if self.method in ("get", "head", "post", "put", "delete", "patch"):
response = getattr(ssrf_proxy, self.method)(data=self.body, files=self.files, **kwargs)
else:
raise ValueError(f'Invalid http method {self.method}')
raise ValueError(f"Invalid http method {self.method}")
return response
def invoke(self) -> HttpExecutorResponse:
@ -280,15 +283,15 @@ class HttpExecutor:
"""
server_url = self.server_url
if self.params:
server_url += f'?{urlencode(self.params)}'
server_url += f"?{urlencode(self.params)}"
raw_request = f'{self.method.upper()} {server_url} HTTP/1.1\n'
raw_request = f"{self.method.upper()} {server_url} HTTP/1.1\n"
headers = self._assembling_headers()
for k, v in headers.items():
# get authorization header
if self.authorization.type == 'api-key':
authorization_header = 'Authorization'
if self.authorization.type == "api-key":
authorization_header = "Authorization"
if self.authorization.config and self.authorization.config.header:
authorization_header = self.authorization.config.header
@ -296,21 +299,21 @@ class HttpExecutor:
raw_request += f'{k}: {"*" * len(v)}\n'
continue
raw_request += f'{k}: {v}\n'
raw_request += f"{k}: {v}\n"
raw_request += '\n'
raw_request += "\n"
# if files, use multipart/form-data with boundary
if self.files:
boundary = self.boundary
raw_request += f'--{boundary}'
raw_request += f"--{boundary}"
for k, v in self.files.items():
raw_request += f'\nContent-Disposition: form-data; name="{k}"\n\n'
raw_request += f'{v[1]}\n'
raw_request += f'--{boundary}'
raw_request += '--'
raw_request += f"{v[1]}\n"
raw_request += f"--{boundary}"
raw_request += "--"
else:
raw_request += self.body or ''
raw_request += self.body or ""
return raw_request
@ -328,9 +331,9 @@ class HttpExecutor:
for variable_selector in variable_selectors:
variable = variable_pool.get_any(variable_selector.value_selector)
if variable is None:
raise ValueError(f'Variable {variable_selector.variable} not found')
raise ValueError(f"Variable {variable_selector.variable} not found")
if escape_quotes and isinstance(variable, str):
value = variable.replace('"', '\\"').replace('\n', '\\n')
value = variable.replace('"', '\\"').replace("\n", "\\n")
else:
value = variable
variable_value_mapping[variable_selector.variable] = value

View file

@ -31,18 +31,18 @@ class HttpRequestNode(BaseNode):
@classmethod
def get_default_config(cls, filters: dict | None = None) -> dict:
return {
'type': 'http-request',
'config': {
'method': 'get',
'authorization': {
'type': 'no-auth',
"type": "http-request",
"config": {
"method": "get",
"authorization": {
"type": "no-auth",
},
'body': {'type': 'none'},
'timeout': {
"body": {"type": "none"},
"timeout": {
**HTTP_REQUEST_DEFAULT_TIMEOUT.model_dump(),
'max_connect_timeout': dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
'max_read_timeout': dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
'max_write_timeout': dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
"max_connect_timeout": dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
"max_read_timeout": dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
"max_write_timeout": dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
},
},
}
@ -52,9 +52,8 @@ class HttpRequestNode(BaseNode):
# TODO: Switch to use segment directly
if node_data.authorization.config and node_data.authorization.config.api_key:
node_data.authorization.config.api_key = parser.convert_template(
template=node_data.authorization.config.api_key,
variable_pool=self.graph_runtime_state.variable_pool
).text
template=node_data.authorization.config.api_key, variable_pool=self.graph_runtime_state.variable_pool
).text
# init http executor
http_executor = None
@ -62,7 +61,7 @@ class HttpRequestNode(BaseNode):
http_executor = HttpExecutor(
node_data=node_data,
timeout=self._get_request_timeout(node_data),
variable_pool=self.graph_runtime_state.variable_pool
variable_pool=self.graph_runtime_state.variable_pool,
)
# invoke http executor
@ -71,7 +70,7 @@ class HttpRequestNode(BaseNode):
process_data = {}
if http_executor:
process_data = {
'request': http_executor.to_raw_request(),
"request": http_executor.to_raw_request(),
}
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
@ -84,13 +83,13 @@ class HttpRequestNode(BaseNode):
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs={
'status_code': response.status_code,
'body': response.content if not files else '',
'headers': response.headers,
'files': files,
"status_code": response.status_code,
"body": response.content if not files else "",
"headers": response.headers,
"files": files,
},
process_data={
'request': http_executor.to_raw_request(),
"request": http_executor.to_raw_request(),
},
)
@ -107,10 +106,7 @@ class HttpRequestNode(BaseNode):
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: HttpRequestNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: HttpRequestNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -126,11 +122,11 @@ class HttpRequestNode(BaseNode):
variable_mapping = {}
for variable_selector in variable_selectors:
variable_mapping[node_id + '.' + variable_selector.variable] = variable_selector.value_selector
variable_mapping[node_id + "." + variable_selector.variable] = variable_selector.value_selector
return variable_mapping
except Exception as e:
logging.exception(f'Failed to extract variable selector to variable mapping: {e}')
logging.exception(f"Failed to extract variable selector to variable mapping: {e}")
return {}
def extract_files(self, url: str, response: HttpExecutorResponse) -> list[FileVar]:
@ -144,7 +140,7 @@ class HttpRequestNode(BaseNode):
# extract filename from url
filename = path.basename(url)
# extract extension if possible
extension = guess_extension(mimetype) or '.bin'
extension = guess_extension(mimetype) or ".bin"
tool_file = ToolFileManager.create_file_by_raw(
user_id=self.user_id,

View file

@ -15,6 +15,7 @@ class IfElseNodeData(BaseNodeData):
"""
Case entity representing a single logical condition group
"""
case_id: str
logical_operator: Literal["and", "or"]
conditions: list[Condition]

View file

@ -20,13 +20,9 @@ class IfElseNode(BaseNode):
node_data = self.node_data
node_data = cast(IfElseNodeData, node_data)
node_inputs: dict[str, list] = {
"conditions": []
}
node_inputs: dict[str, list] = {"conditions": []}
process_datas: dict[str, list] = {
"condition_results": []
}
process_datas: dict[str, list] = {"condition_results": []}
input_conditions = []
final_result = False
@ -37,8 +33,7 @@ class IfElseNode(BaseNode):
if node_data.cases:
for case in node_data.cases:
input_conditions, group_result = condition_processor.process_conditions(
variable_pool=self.graph_runtime_state.variable_pool,
conditions=case.conditions
variable_pool=self.graph_runtime_state.variable_pool, conditions=case.conditions
)
# Apply the logical operator for the current case
@ -60,8 +55,7 @@ class IfElseNode(BaseNode):
else:
# Fallback to old structure if cases are not defined
input_conditions, group_result = condition_processor.process_conditions(
variable_pool=self.graph_runtime_state.variable_pool,
conditions=node_data.conditions
variable_pool=self.graph_runtime_state.variable_pool, conditions=node_data.conditions
)
final_result = all(group_result) if node_data.logical_operator == "and" else any(group_result)
@ -69,21 +63,14 @@ class IfElseNode(BaseNode):
selected_case_id = "true" if final_result else "false"
process_datas["condition_results"].append(
{
"group": "default",
"results": group_result,
"final_result": final_result
}
{"group": "default", "results": group_result, "final_result": final_result}
)
node_inputs["conditions"] = input_conditions
except Exception as e:
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs=node_inputs,
process_data=process_datas,
error=str(e)
status=WorkflowNodeExecutionStatus.FAILED, inputs=node_inputs, process_data=process_datas, error=str(e)
)
outputs = {"result": final_result, "selected_case_id": selected_case_id}
@ -93,17 +80,14 @@ class IfElseNode(BaseNode):
inputs=node_inputs,
process_data=process_datas,
edge_source_handle=selected_case_id if selected_case_id else "false", # Use case ID or 'default'
outputs=outputs
outputs=outputs,
)
return data
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: IfElseNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: IfElseNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping

View file

@ -7,21 +7,25 @@ class IterationNodeData(BaseIterationNodeData):
"""
Iteration Node Data.
"""
parent_loop_id: Optional[str] = None # redundant field, not used currently
iterator_selector: list[str] # variable selector
output_selector: list[str] # output selector
parent_loop_id: Optional[str] = None # redundant field, not used currently
iterator_selector: list[str] # variable selector
output_selector: list[str] # output selector
class IterationStartNodeData(BaseNodeData):
"""
Iteration Start Node Data.
"""
pass
class IterationState(BaseIterationState):
"""
Iteration State.
"""
outputs: list[Any] = None
current_output: Optional[Any] = None
@ -29,6 +33,7 @@ class IterationState(BaseIterationState):
"""
Data.
"""
iterator_length: int
def get_last_output(self) -> Optional[Any]:
@ -38,9 +43,9 @@ class IterationState(BaseIterationState):
if self.outputs:
return self.outputs[-1]
return None
def get_current_output(self) -> Optional[Any]:
"""
Get current output.
"""
return self.current_output
return self.current_output

View file

@ -33,6 +33,7 @@ class IterationNode(BaseNode):
"""
Iteration Node.
"""
_node_data_cls = IterationNodeData
_node_type = NodeType.ITERATION
@ -45,31 +46,26 @@ class IterationNode(BaseNode):
if not iterator_list_segment:
raise ValueError(f"Iterator variable {self.node_data.iterator_selector} not found")
iterator_list_value = iterator_list_segment.to_object()
if not isinstance(iterator_list_value, list):
raise ValueError(f"Invalid iterator value: {iterator_list_value}, please provide a list.")
inputs = {
"iterator_selector": iterator_list_value
}
inputs = {"iterator_selector": iterator_list_value}
graph_config = self.graph_config
if not self.node_data.start_node_id:
raise ValueError(f'field start_node_id in iteration {self.node_id} not found')
raise ValueError(f"field start_node_id in iteration {self.node_id} not found")
root_node_id = self.node_data.start_node_id
# init graph
iteration_graph = Graph.init(
graph_config=graph_config,
root_node_id=root_node_id
)
iteration_graph = Graph.init(graph_config=graph_config, root_node_id=root_node_id)
if not iteration_graph:
raise ValueError('iteration graph not found')
raise ValueError("iteration graph not found")
leaf_node_ids = iteration_graph.get_leaf_node_ids()
iteration_leaf_node_ids = []
@ -97,26 +93,21 @@ class IterationNode(BaseNode):
Condition(
variable_selector=[self.node_id, "index"],
comparison_operator="<",
value=str(len(iterator_list_value))
value=str(len(iterator_list_value)),
)
]
)
],
),
)
variable_pool = self.graph_runtime_state.variable_pool
# append iteration variable (item, index) to variable pool
variable_pool.add(
[self.node_id, 'index'],
0
)
variable_pool.add(
[self.node_id, 'item'],
iterator_list_value[0]
)
variable_pool.add([self.node_id, "index"], 0)
variable_pool.add([self.node_id, "item"], iterator_list_value[0])
# init graph engine
from core.workflow.graph_engine.graph_engine import GraphEngine
graph_engine = GraphEngine(
tenant_id=self.tenant_id,
app_id=self.app_id,
@ -130,7 +121,7 @@ class IterationNode(BaseNode):
graph_config=graph_config,
variable_pool=variable_pool,
max_execution_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS,
max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME
max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME,
)
start_at = datetime.now(timezone.utc).replace(tzinfo=None)
@ -142,10 +133,8 @@ class IterationNode(BaseNode):
iteration_node_data=self.node_data,
start_at=start_at,
inputs=inputs,
metadata={
"iterator_length": len(iterator_list_value)
},
predecessor_node_id=self.previous_node_id
metadata={"iterator_length": len(iterator_list_value)},
predecessor_node_id=self.previous_node_id,
)
yield IterationRunNextEvent(
@ -154,7 +143,7 @@ class IterationNode(BaseNode):
iteration_node_type=self.node_type,
iteration_node_data=self.node_data,
index=0,
pre_iteration_output=None
pre_iteration_output=None,
)
outputs: list[Any] = []
@ -176,7 +165,9 @@ class IterationNode(BaseNode):
if NodeRunMetadataKey.ITERATION_ID not in metadata:
metadata[NodeRunMetadataKey.ITERATION_ID] = self.node_id
metadata[NodeRunMetadataKey.ITERATION_INDEX] = variable_pool.get_any([self.node_id, 'index'])
metadata[NodeRunMetadataKey.ITERATION_INDEX] = variable_pool.get_any(
[self.node_id, "index"]
)
event.route_node_state.node_run_result.metadata = metadata
yield event
@ -192,21 +183,15 @@ class IterationNode(BaseNode):
variable_pool.remove_node(node_id)
# move to next iteration
current_index = variable_pool.get([self.node_id, 'index'])
current_index = variable_pool.get([self.node_id, "index"])
if current_index is None:
raise ValueError(f'iteration {self.node_id} current index not found')
raise ValueError(f"iteration {self.node_id} current index not found")
next_index = int(current_index.to_object()) + 1
variable_pool.add(
[self.node_id, 'index'],
next_index
)
variable_pool.add([self.node_id, "index"], next_index)
if next_index < len(iterator_list_value):
variable_pool.add(
[self.node_id, 'item'],
iterator_list_value[next_index]
)
variable_pool.add([self.node_id, "item"], iterator_list_value[next_index])
yield IterationRunNextEvent(
iteration_id=self.id,
@ -214,8 +199,9 @@ class IterationNode(BaseNode):
iteration_node_type=self.node_type,
iteration_node_data=self.node_data,
index=next_index,
pre_iteration_output=jsonable_encoder(
current_iteration_output) if current_iteration_output else None
pre_iteration_output=jsonable_encoder(current_iteration_output)
if current_iteration_output
else None,
)
elif isinstance(event, BaseGraphEvent):
if isinstance(event, GraphRunFailedEvent):
@ -227,13 +213,9 @@ class IterationNode(BaseNode):
iteration_node_data=self.node_data,
start_at=start_at,
inputs=inputs,
outputs={
"output": jsonable_encoder(outputs)
},
outputs={"output": jsonable_encoder(outputs)},
steps=len(iterator_list_value),
metadata={
"total_tokens": graph_engine.graph_runtime_state.total_tokens
},
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
error=event.error,
)
@ -255,21 +237,14 @@ class IterationNode(BaseNode):
iteration_node_data=self.node_data,
start_at=start_at,
inputs=inputs,
outputs={
"output": jsonable_encoder(outputs)
},
outputs={"output": jsonable_encoder(outputs)},
steps=len(iterator_list_value),
metadata={
"total_tokens": graph_engine.graph_runtime_state.total_tokens
}
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
)
yield RunCompletedEvent(
run_result=NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs={
'output': jsonable_encoder(outputs)
}
status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs={"output": jsonable_encoder(outputs)}
)
)
except Exception as e:
@ -282,16 +257,11 @@ class IterationNode(BaseNode):
iteration_node_data=self.node_data,
start_at=start_at,
inputs=inputs,
outputs={
"output": jsonable_encoder(outputs)
},
outputs={"output": jsonable_encoder(outputs)},
steps=len(iterator_list_value),
metadata={
"total_tokens": graph_engine.graph_runtime_state.total_tokens
},
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
error=str(e),
)
yield RunCompletedEvent(
run_result=NodeRunResult(
@ -301,15 +271,12 @@ class IterationNode(BaseNode):
)
finally:
# remove iteration variable (item, index) from variable pool after iteration run completed
variable_pool.remove([self.node_id, 'index'])
variable_pool.remove([self.node_id, 'item'])
variable_pool.remove([self.node_id, "index"])
variable_pool.remove([self.node_id, "item"])
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: IterationNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: IterationNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -319,36 +286,33 @@ class IterationNode(BaseNode):
:return:
"""
variable_mapping = {
f'{node_id}.input_selector': node_data.iterator_selector,
f"{node_id}.input_selector": node_data.iterator_selector,
}
# init graph
iteration_graph = Graph.init(
graph_config=graph_config,
root_node_id=node_data.start_node_id
)
iteration_graph = Graph.init(graph_config=graph_config, root_node_id=node_data.start_node_id)
if not iteration_graph:
raise ValueError('iteration graph not found')
raise ValueError("iteration graph not found")
for sub_node_id, sub_node_config in iteration_graph.node_id_config_mapping.items():
if sub_node_config.get('data', {}).get('iteration_id') != node_id:
if sub_node_config.get("data", {}).get("iteration_id") != node_id:
continue
# variable selector to variable mapping
try:
# Get node class
from core.workflow.nodes.node_mapping import node_classes
node_type = NodeType.value_of(sub_node_config.get('data', {}).get('type'))
node_type = NodeType.value_of(sub_node_config.get("data", {}).get("type"))
node_cls = node_classes.get(node_type)
if not node_cls:
continue
node_cls = cast(BaseNode, node_cls)
sub_node_variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
graph_config=graph_config,
config=sub_node_config
graph_config=graph_config, config=sub_node_config
)
sub_node_variable_mapping = cast(dict[str, list[str]], sub_node_variable_mapping)
except NotImplementedError:
@ -356,7 +320,8 @@ class IterationNode(BaseNode):
# remove iteration variables
sub_node_variable_mapping = {
sub_node_id + '.' + key: value for key, value in sub_node_variable_mapping.items()
sub_node_id + "." + key: value
for key, value in sub_node_variable_mapping.items()
if value[0] != node_id
}
@ -364,8 +329,7 @@ class IterationNode(BaseNode):
# remove variable out from iteration
variable_mapping = {
key: value for key, value in variable_mapping.items()
if value[0] not in iteration_graph.node_ids
key: value for key, value in variable_mapping.items() if value[0] not in iteration_graph.node_ids
}
return variable_mapping

View file

@ -11,6 +11,7 @@ class IterationStartNode(BaseNode):
"""
Iteration Start Node.
"""
_node_data_cls = IterationStartNodeData
_node_type = NodeType.ITERATION_START
@ -18,16 +19,11 @@ class IterationStartNode(BaseNode):
"""
Run the node.
"""
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED
)
return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED)
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: IterationNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: IterationNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping

View file

@ -9,6 +9,7 @@ class RerankingModelConfig(BaseModel):
"""
Reranking Model Config.
"""
provider: str
model: str
@ -17,6 +18,7 @@ class VectorSetting(BaseModel):
"""
Vector Setting.
"""
vector_weight: float
embedding_provider_name: str
embedding_model_name: str
@ -26,6 +28,7 @@ class KeywordSetting(BaseModel):
"""
Keyword Setting.
"""
keyword_weight: float
@ -33,6 +36,7 @@ class WeightedScoreConfig(BaseModel):
"""
Weighted score Config.
"""
vector_setting: VectorSetting
keyword_setting: KeywordSetting
@ -41,17 +45,20 @@ class MultipleRetrievalConfig(BaseModel):
"""
Multiple Retrieval Config.
"""
top_k: int
score_threshold: Optional[float] = None
reranking_mode: str = 'reranking_model'
reranking_mode: str = "reranking_model"
reranking_enable: bool = True
reranking_model: Optional[RerankingModelConfig] = None
weights: Optional[WeightedScoreConfig] = None
class ModelConfig(BaseModel):
"""
Model Config.
Model Config.
"""
provider: str
name: str
mode: str
@ -62,6 +69,7 @@ class SingleRetrievalConfig(BaseModel):
"""
Single Retrieval Config.
"""
model: ModelConfig
@ -69,9 +77,10 @@ class KnowledgeRetrievalNodeData(BaseNodeData):
"""
Knowledge retrieval Node Data.
"""
type: str = 'knowledge-retrieval'
type: str = "knowledge-retrieval"
query_variable_selector: list[str]
dataset_ids: list[str]
retrieval_mode: Literal['single', 'multiple']
retrieval_mode: Literal["single", "multiple"]
multiple_retrieval_config: Optional[MultipleRetrievalConfig] = None
single_retrieval_config: Optional[SingleRetrievalConfig] = None

View file

@ -24,14 +24,11 @@ from models.workflow import WorkflowNodeExecutionStatus
logger = logging.getLogger(__name__)
default_retrieval_model = {
'search_method': RetrievalMethod.SEMANTIC_SEARCH.value,
'reranking_enable': False,
'reranking_model': {
'reranking_provider_name': '',
'reranking_model_name': ''
},
'top_k': 2,
'score_threshold_enabled': False
"search_method": RetrievalMethod.SEMANTIC_SEARCH.value,
"reranking_enable": False,
"reranking_model": {"reranking_provider_name": "", "reranking_model_name": ""},
"top_k": 2,
"score_threshold_enabled": False,
}
@ -45,62 +42,47 @@ class KnowledgeRetrievalNode(BaseNode):
# extract variables
variable = self.graph_runtime_state.variable_pool.get_any(node_data.query_variable_selector)
query = variable
variables = {
'query': query
}
variables = {"query": query}
if not query:
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs=variables,
error="Query is required."
status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, error="Query is required."
)
# retrieve knowledge
try:
results = self._fetch_dataset_retriever(
node_data=node_data, query=query
)
outputs = {
'result': results
}
results = self._fetch_dataset_retriever(node_data=node_data, query=query)
outputs = {"result": results}
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs=variables,
process_data=None,
outputs=outputs
status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=variables, process_data=None, outputs=outputs
)
except Exception as e:
logger.exception("Error when running knowledge retrieval node")
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs=variables,
error=str(e)
)
return NodeRunResult(status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, error=str(e))
def _fetch_dataset_retriever(self, node_data: KnowledgeRetrievalNodeData, query: str) -> list[
dict[str, Any]]:
def _fetch_dataset_retriever(self, node_data: KnowledgeRetrievalNodeData, query: str) -> list[dict[str, Any]]:
available_datasets = []
dataset_ids = node_data.dataset_ids
# Subquery: Count the number of available documents for each dataset
subquery = db.session.query(
Document.dataset_id,
func.count(Document.id).label('available_document_count')
).filter(
Document.indexing_status == 'completed',
Document.enabled == True,
Document.archived == False,
Document.dataset_id.in_(dataset_ids)
).group_by(Document.dataset_id).having(
func.count(Document.id) > 0
).subquery()
subquery = (
db.session.query(Document.dataset_id, func.count(Document.id).label("available_document_count"))
.filter(
Document.indexing_status == "completed",
Document.enabled == True,
Document.archived == False,
Document.dataset_id.in_(dataset_ids),
)
.group_by(Document.dataset_id)
.having(func.count(Document.id) > 0)
.subquery()
)
results = db.session.query(Dataset).join(
subquery, Dataset.id == subquery.c.dataset_id
).filter(
Dataset.tenant_id == self.tenant_id,
Dataset.id.in_(dataset_ids)
).all()
results = (
db.session.query(Dataset)
.join(subquery, Dataset.id == subquery.c.dataset_id)
.filter(Dataset.tenant_id == self.tenant_id, Dataset.id.in_(dataset_ids))
.all()
)
for dataset in results:
# pass if dataset is not available
@ -117,16 +99,14 @@ class KnowledgeRetrievalNode(BaseNode):
model_type_instance = cast(LargeLanguageModel, model_type_instance)
# get model schema
model_schema = model_type_instance.get_model_schema(
model=model_config.model,
credentials=model_config.credentials
model=model_config.model, credentials=model_config.credentials
)
if model_schema:
planning_strategy = PlanningStrategy.REACT_ROUTER
features = model_schema.features
if features:
if ModelFeature.TOOL_CALL in features \
or ModelFeature.MULTI_TOOL_CALL in features:
if ModelFeature.TOOL_CALL in features or ModelFeature.MULTI_TOOL_CALL in features:
planning_strategy = PlanningStrategy.ROUTER
all_documents = dataset_retrieval.single_retrieve(
available_datasets=available_datasets,
@ -137,111 +117,111 @@ class KnowledgeRetrievalNode(BaseNode):
query=query,
model_config=model_config,
model_instance=model_instance,
planning_strategy=planning_strategy
planning_strategy=planning_strategy,
)
elif node_data.retrieval_mode == DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE.value:
if node_data.multiple_retrieval_config.reranking_mode == 'reranking_model':
if node_data.multiple_retrieval_config.reranking_mode == "reranking_model":
reranking_model = {
'reranking_provider_name': node_data.multiple_retrieval_config.reranking_model.provider,
'reranking_model_name': node_data.multiple_retrieval_config.reranking_model.model
"reranking_provider_name": node_data.multiple_retrieval_config.reranking_model.provider,
"reranking_model_name": node_data.multiple_retrieval_config.reranking_model.model,
}
weights = None
elif node_data.multiple_retrieval_config.reranking_mode == 'weighted_score':
elif node_data.multiple_retrieval_config.reranking_mode == "weighted_score":
reranking_model = None
weights = {
'vector_setting': {
"vector_setting": {
"vector_weight": node_data.multiple_retrieval_config.weights.vector_setting.vector_weight,
"embedding_provider_name": node_data.multiple_retrieval_config.weights.vector_setting.embedding_provider_name,
"embedding_model_name": node_data.multiple_retrieval_config.weights.vector_setting.embedding_model_name,
},
'keyword_setting': {
"keyword_setting": {
"keyword_weight": node_data.multiple_retrieval_config.weights.keyword_setting.keyword_weight
}
},
}
else:
reranking_model = None
weights = None
all_documents = dataset_retrieval.multiple_retrieve(self.app_id, self.tenant_id, self.user_id,
self.user_from.value,
available_datasets, query,
node_data.multiple_retrieval_config.top_k,
node_data.multiple_retrieval_config.score_threshold,
node_data.multiple_retrieval_config.reranking_mode,
reranking_model,
weights,
node_data.multiple_retrieval_config.reranking_enable,
)
all_documents = dataset_retrieval.multiple_retrieve(
self.app_id,
self.tenant_id,
self.user_id,
self.user_from.value,
available_datasets,
query,
node_data.multiple_retrieval_config.top_k,
node_data.multiple_retrieval_config.score_threshold,
node_data.multiple_retrieval_config.reranking_mode,
reranking_model,
weights,
node_data.multiple_retrieval_config.reranking_enable,
)
context_list = []
if all_documents:
document_score_list = {}
page_number_list = {}
for item in all_documents:
if item.metadata.get('score'):
document_score_list[item.metadata['doc_id']] = item.metadata['score']
if item.metadata.get("score"):
document_score_list[item.metadata["doc_id"]] = item.metadata["score"]
# both 'page' and 'score' are metadata fields
if item.metadata.get('page'):
page_number_list[item.metadata['doc_id']] = item.metadata['page']
if item.metadata.get("page"):
page_number_list[item.metadata["doc_id"]] = item.metadata["page"]
index_node_ids = [document.metadata['doc_id'] for document in all_documents]
index_node_ids = [document.metadata["doc_id"] for document in all_documents]
segments = DocumentSegment.query.filter(
DocumentSegment.dataset_id.in_(dataset_ids),
DocumentSegment.completed_at.isnot(None),
DocumentSegment.status == 'completed',
DocumentSegment.status == "completed",
DocumentSegment.enabled == True,
DocumentSegment.index_node_id.in_(index_node_ids)
DocumentSegment.index_node_id.in_(index_node_ids),
).all()
if segments:
index_node_id_to_position = {id: position for position, id in enumerate(index_node_ids)}
sorted_segments = sorted(segments,
key=lambda segment: index_node_id_to_position.get(segment.index_node_id,
float('inf')))
sorted_segments = sorted(
segments, key=lambda segment: index_node_id_to_position.get(segment.index_node_id, float("inf"))
)
for segment in sorted_segments:
dataset = Dataset.query.filter_by(
id=segment.dataset_id
dataset = Dataset.query.filter_by(id=segment.dataset_id).first()
document = Document.query.filter(
Document.id == segment.document_id,
Document.enabled == True,
Document.archived == False,
).first()
document = Document.query.filter(Document.id == segment.document_id,
Document.enabled == True,
Document.archived == False,
).first()
resource_number = 1
if dataset and document:
source = {
'metadata': {
'_source': 'knowledge',
'position': resource_number,
'dataset_id': dataset.id,
'dataset_name': dataset.name,
'document_id': document.id,
'document_name': document.name,
'document_data_source_type': document.data_source_type,
'page': page_number_list.get(segment.index_node_id, None),
'segment_id': segment.id,
'retriever_from': 'workflow',
'score': document_score_list.get(segment.index_node_id, None),
'segment_hit_count': segment.hit_count,
'segment_word_count': segment.word_count,
'segment_position': segment.position,
'segment_index_node_hash': segment.index_node_hash,
"metadata": {
"_source": "knowledge",
"position": resource_number,
"dataset_id": dataset.id,
"dataset_name": dataset.name,
"document_id": document.id,
"document_name": document.name,
"document_data_source_type": document.data_source_type,
"page": page_number_list.get(segment.index_node_id, None),
"segment_id": segment.id,
"retriever_from": "workflow",
"score": document_score_list.get(segment.index_node_id, None),
"segment_hit_count": segment.hit_count,
"segment_word_count": segment.word_count,
"segment_position": segment.position,
"segment_index_node_hash": segment.index_node_hash,
},
'title': document.name
"title": document.name,
}
if segment.answer:
source['content'] = f'question:{segment.get_sign_content()} \nanswer:{segment.answer}'
source["content"] = f"question:{segment.get_sign_content()} \nanswer:{segment.answer}"
else:
source['content'] = segment.get_sign_content()
source["content"] = segment.get_sign_content()
context_list.append(source)
resource_number += 1
return context_list
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: KnowledgeRetrievalNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: KnowledgeRetrievalNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -251,11 +231,12 @@ class KnowledgeRetrievalNode(BaseNode):
:return:
"""
variable_mapping = {}
variable_mapping[node_id + '.query'] = node_data.query_variable_selector
variable_mapping[node_id + ".query"] = node_data.query_variable_selector
return variable_mapping
def _fetch_model_config(self, node_data: KnowledgeRetrievalNodeData) -> tuple[
ModelInstance, ModelConfigWithCredentialsEntity]:
def _fetch_model_config(
self, node_data: KnowledgeRetrievalNodeData
) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]:
"""
Fetch model config
:param node_data: node data
@ -266,10 +247,7 @@ class KnowledgeRetrievalNode(BaseNode):
model_manager = ModelManager()
model_instance = model_manager.get_model_instance(
tenant_id=self.tenant_id,
model_type=ModelType.LLM,
provider=provider_name,
model=model_name
tenant_id=self.tenant_id, model_type=ModelType.LLM, provider=provider_name, model=model_name
)
provider_model_bundle = model_instance.provider_model_bundle
@ -280,8 +258,7 @@ class KnowledgeRetrievalNode(BaseNode):
# check model
provider_model = provider_model_bundle.configuration.get_provider_model(
model=model_name,
model_type=ModelType.LLM
model=model_name, model_type=ModelType.LLM
)
if provider_model is None:
@ -297,19 +274,16 @@ class KnowledgeRetrievalNode(BaseNode):
# model config
completion_params = node_data.single_retrieval_config.model.completion_params
stop = []
if 'stop' in completion_params:
stop = completion_params['stop']
del completion_params['stop']
if "stop" in completion_params:
stop = completion_params["stop"]
del completion_params["stop"]
# get model mode
model_mode = node_data.single_retrieval_config.model.mode
if not model_mode:
raise ValueError("LLM mode is required.")
model_schema = model_type_instance.get_model_schema(
model_name,
model_credentials
)
model_schema = model_type_instance.get_model_schema(model_name, model_credentials)
if not model_schema:
raise ValueError(f"Model {model_name} not exist.")

View file

@ -11,6 +11,7 @@ class ModelConfig(BaseModel):
"""
Model Config.
"""
provider: str
name: str
mode: str
@ -21,6 +22,7 @@ class ContextConfig(BaseModel):
"""
Context Config.
"""
enabled: bool
variable_selector: Optional[list[str]] = None
@ -29,37 +31,47 @@ class VisionConfig(BaseModel):
"""
Vision Config.
"""
class Configs(BaseModel):
"""
Configs.
"""
detail: Literal['low', 'high']
detail: Literal["low", "high"]
enabled: bool
configs: Optional[Configs] = None
class PromptConfig(BaseModel):
"""
Prompt Config.
"""
jinja2_variables: Optional[list[VariableSelector]] = None
class LLMNodeChatModelMessage(ChatModelMessage):
"""
LLM Node Chat Model Message.
"""
jinja2_text: Optional[str] = None
class LLMNodeCompletionModelPromptTemplate(CompletionModelPromptTemplate):
"""
LLM Node Chat Model Prompt Template.
"""
jinja2_text: Optional[str] = None
class LLMNodeData(BaseNodeData):
"""
LLM Node Data.
"""
model: ModelConfig
prompt_template: Union[list[LLMNodeChatModelMessage], LLMNodeCompletionModelPromptTemplate]
prompt_config: Optional[PromptConfig] = None

View file

@ -45,11 +45,11 @@ if TYPE_CHECKING:
from core.file.file_obj import FileVar
class ModelInvokeCompleted(BaseModel):
"""
Model invoke completed
"""
text: str
usage: LLMUsage
finish_reason: Optional[str] = None
@ -89,7 +89,7 @@ class LLMNode(BaseNode):
files = self._fetch_files(node_data, variable_pool)
if files:
node_inputs['#files#'] = [file.to_dict() for file in files]
node_inputs["#files#"] = [file.to_dict() for file in files]
# fetch context value
generator = self._fetch_context(node_data, variable_pool)
@ -100,7 +100,7 @@ class LLMNode(BaseNode):
yield event
if context:
node_inputs['#context#'] = context # type: ignore
node_inputs["#context#"] = context # type: ignore
# fetch model config
model_instance, model_config = self._fetch_model_config(node_data.model)
@ -111,24 +111,22 @@ class LLMNode(BaseNode):
# fetch prompt messages
prompt_messages, stop = self._fetch_prompt_messages(
node_data=node_data,
query=variable_pool.get_any(['sys', SystemVariableKey.QUERY.value])
if node_data.memory else None,
query=variable_pool.get_any(["sys", SystemVariableKey.QUERY.value]) if node_data.memory else None,
query_prompt_template=node_data.memory.query_prompt_template if node_data.memory else None,
inputs=inputs,
files=files,
context=context,
memory=memory,
model_config=model_config
model_config=model_config,
)
process_data = {
'model_mode': model_config.mode,
'prompts': PromptMessageUtil.prompt_messages_to_prompt_for_saving(
model_mode=model_config.mode,
prompt_messages=prompt_messages
"model_mode": model_config.mode,
"prompts": PromptMessageUtil.prompt_messages_to_prompt_for_saving(
model_mode=model_config.mode, prompt_messages=prompt_messages
),
'model_provider': model_config.provider,
'model_name': model_config.model,
"model_provider": model_config.provider,
"model_name": model_config.model,
}
# handle invoke result
@ -136,10 +134,10 @@ class LLMNode(BaseNode):
node_data_model=node_data.model,
model_instance=model_instance,
prompt_messages=prompt_messages,
stop=stop
stop=stop,
)
result_text = ''
result_text = ""
usage = LLMUsage.empty_usage()
finish_reason = None
for event in generator:
@ -156,16 +154,12 @@ class LLMNode(BaseNode):
status=WorkflowNodeExecutionStatus.FAILED,
error=str(e),
inputs=node_inputs,
process_data=process_data
process_data=process_data,
)
)
return
outputs = {
'text': result_text,
'usage': jsonable_encoder(usage),
'finish_reason': finish_reason
}
outputs = {"text": result_text, "usage": jsonable_encoder(usage), "finish_reason": finish_reason}
yield RunCompletedEvent(
run_result=NodeRunResult(
@ -176,17 +170,19 @@ class LLMNode(BaseNode):
metadata={
NodeRunMetadataKey.TOTAL_TOKENS: usage.total_tokens,
NodeRunMetadataKey.TOTAL_PRICE: usage.total_price,
NodeRunMetadataKey.CURRENCY: usage.currency
NodeRunMetadataKey.CURRENCY: usage.currency,
},
llm_usage=usage
llm_usage=usage,
)
)
def _invoke_llm(self, node_data_model: ModelConfig,
model_instance: ModelInstance,
prompt_messages: list[PromptMessage],
stop: Optional[list[str]] = None) \
-> Generator[RunEvent | ModelInvokeCompleted, None, None]:
def _invoke_llm(
self,
node_data_model: ModelConfig,
model_instance: ModelInstance,
prompt_messages: list[PromptMessage],
stop: Optional[list[str]] = None,
) -> Generator[RunEvent | ModelInvokeCompleted, None, None]:
"""
Invoke large language model
:param node_data_model: node data model
@ -206,9 +202,7 @@ class LLMNode(BaseNode):
)
# handle invoke result
generator = self._handle_invoke_result(
invoke_result=invoke_result
)
generator = self._handle_invoke_result(invoke_result=invoke_result)
usage = LLMUsage.empty_usage()
for event in generator:
@ -219,8 +213,9 @@ class LLMNode(BaseNode):
# deduct quota
self.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
def _handle_invoke_result(self, invoke_result: LLMResult | Generator) \
-> Generator[RunEvent | ModelInvokeCompleted, None, None]:
def _handle_invoke_result(
self, invoke_result: LLMResult | Generator
) -> Generator[RunEvent | ModelInvokeCompleted, None, None]:
"""
Handle invoke result
:param invoke_result: invoke result
@ -231,17 +226,14 @@ class LLMNode(BaseNode):
model = None
prompt_messages: list[PromptMessage] = []
full_text = ''
full_text = ""
usage = None
finish_reason = None
for result in invoke_result:
text = result.delta.message.content
full_text += text
yield RunStreamChunkEvent(
chunk_content=text,
from_variable_selector=[self.node_id, 'text']
)
yield RunStreamChunkEvent(chunk_content=text, from_variable_selector=[self.node_id, "text"])
if not model:
model = result.model
@ -258,15 +250,11 @@ class LLMNode(BaseNode):
if not usage:
usage = LLMUsage.empty_usage()
yield ModelInvokeCompleted(
text=full_text,
usage=usage,
finish_reason=finish_reason
)
yield ModelInvokeCompleted(text=full_text, usage=usage, finish_reason=finish_reason)
def _transform_chat_messages(self,
messages: list[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate
) -> list[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate:
def _transform_chat_messages(
self, messages: list[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate
) -> list[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate:
"""
Transform chat messages
@ -275,13 +263,13 @@ class LLMNode(BaseNode):
"""
if isinstance(messages, LLMNodeCompletionModelPromptTemplate):
if messages.edition_type == 'jinja2' and messages.jinja2_text:
if messages.edition_type == "jinja2" and messages.jinja2_text:
messages.text = messages.jinja2_text
return messages
for message in messages:
if message.edition_type == 'jinja2' and message.jinja2_text:
if message.edition_type == "jinja2" and message.jinja2_text:
message.text = message.jinja2_text
return messages
@ -300,17 +288,15 @@ class LLMNode(BaseNode):
for variable_selector in node_data.prompt_config.jinja2_variables or []:
variable = variable_selector.variable
value = variable_pool.get_any(
variable_selector.value_selector
)
value = variable_pool.get_any(variable_selector.value_selector)
def parse_dict(d: dict) -> str:
"""
Parse dict into string
"""
# check if it's a context structure
if 'metadata' in d and '_source' in d['metadata'] and 'content' in d:
return d['content']
if "metadata" in d and "_source" in d["metadata"] and "content" in d:
return d["content"]
# else, parse the dict
try:
@ -321,7 +307,7 @@ class LLMNode(BaseNode):
if isinstance(value, str):
value = value
elif isinstance(value, list):
result = ''
result = ""
for item in value:
if isinstance(item, dict):
result += parse_dict(item)
@ -331,7 +317,7 @@ class LLMNode(BaseNode):
result += str(item)
else:
result += str(item)
result += '\n'
result += "\n"
value = result.strip()
elif isinstance(value, dict):
value = parse_dict(value)
@ -366,18 +352,19 @@ class LLMNode(BaseNode):
for variable_selector in variable_selectors:
variable_value = variable_pool.get_any(variable_selector.value_selector)
if variable_value is None:
raise ValueError(f'Variable {variable_selector.variable} not found')
raise ValueError(f"Variable {variable_selector.variable} not found")
inputs[variable_selector.variable] = variable_value
memory = node_data.memory
if memory and memory.query_prompt_template:
query_variable_selectors = (VariableTemplateParser(template=memory.query_prompt_template)
.extract_variable_selectors())
query_variable_selectors = VariableTemplateParser(
template=memory.query_prompt_template
).extract_variable_selectors()
for variable_selector in query_variable_selectors:
variable_value = variable_pool.get_any(variable_selector.value_selector)
if variable_value is None:
raise ValueError(f'Variable {variable_selector.variable} not found')
raise ValueError(f"Variable {variable_selector.variable} not found")
inputs[variable_selector.variable] = variable_value
@ -393,7 +380,7 @@ class LLMNode(BaseNode):
if not node_data.vision.enabled:
return []
files = variable_pool.get_any(['sys', SystemVariableKey.FILES.value])
files = variable_pool.get_any(["sys", SystemVariableKey.FILES.value])
if not files:
return []
@ -415,29 +402,25 @@ class LLMNode(BaseNode):
context_value = variable_pool.get_any(node_data.context.variable_selector)
if context_value:
if isinstance(context_value, str):
yield RunRetrieverResourceEvent(
retriever_resources=[],
context=context_value
)
yield RunRetrieverResourceEvent(retriever_resources=[], context=context_value)
elif isinstance(context_value, list):
context_str = ''
context_str = ""
original_retriever_resource = []
for item in context_value:
if isinstance(item, str):
context_str += item + '\n'
context_str += item + "\n"
else:
if 'content' not in item:
raise ValueError(f'Invalid context structure: {item}')
if "content" not in item:
raise ValueError(f"Invalid context structure: {item}")
context_str += item['content'] + '\n'
context_str += item["content"] + "\n"
retriever_resource = self._convert_to_original_retriever_resource(item)
if retriever_resource:
original_retriever_resource.append(retriever_resource)
yield RunRetrieverResourceEvent(
retriever_resources=original_retriever_resource,
context=context_str.strip()
retriever_resources=original_retriever_resource, context=context_str.strip()
)
def _convert_to_original_retriever_resource(self, context_dict: dict) -> Optional[dict]:
@ -446,34 +429,38 @@ class LLMNode(BaseNode):
:param context_dict: context dict
:return:
"""
if ('metadata' in context_dict and '_source' in context_dict['metadata']
and context_dict['metadata']['_source'] == 'knowledge'):
metadata = context_dict.get('metadata', {})
if (
"metadata" in context_dict
and "_source" in context_dict["metadata"]
and context_dict["metadata"]["_source"] == "knowledge"
):
metadata = context_dict.get("metadata", {})
source = {
'position': metadata.get('position'),
'dataset_id': metadata.get('dataset_id'),
'dataset_name': metadata.get('dataset_name'),
'document_id': metadata.get('document_id'),
'document_name': metadata.get('document_name'),
'data_source_type': metadata.get('document_data_source_type'),
'segment_id': metadata.get('segment_id'),
'retriever_from': metadata.get('retriever_from'),
'score': metadata.get('score'),
'hit_count': metadata.get('segment_hit_count'),
'word_count': metadata.get('segment_word_count'),
'segment_position': metadata.get('segment_position'),
'index_node_hash': metadata.get('segment_index_node_hash'),
'content': context_dict.get('content'),
'page': metadata.get('page'),
"position": metadata.get("position"),
"dataset_id": metadata.get("dataset_id"),
"dataset_name": metadata.get("dataset_name"),
"document_id": metadata.get("document_id"),
"document_name": metadata.get("document_name"),
"data_source_type": metadata.get("document_data_source_type"),
"segment_id": metadata.get("segment_id"),
"retriever_from": metadata.get("retriever_from"),
"score": metadata.get("score"),
"hit_count": metadata.get("segment_hit_count"),
"word_count": metadata.get("segment_word_count"),
"segment_position": metadata.get("segment_position"),
"index_node_hash": metadata.get("segment_index_node_hash"),
"content": context_dict.get("content"),
"page": metadata.get("page"),
}
return source
return None
def _fetch_model_config(self, node_data_model: ModelConfig) -> tuple[
ModelInstance, ModelConfigWithCredentialsEntity]:
def _fetch_model_config(
self, node_data_model: ModelConfig
) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]:
"""
Fetch model config
:param node_data_model: node data model
@ -484,10 +471,7 @@ class LLMNode(BaseNode):
model_manager = ModelManager()
model_instance = model_manager.get_model_instance(
tenant_id=self.tenant_id,
model_type=ModelType.LLM,
provider=provider_name,
model=model_name
tenant_id=self.tenant_id, model_type=ModelType.LLM, provider=provider_name, model=model_name
)
provider_model_bundle = model_instance.provider_model_bundle
@ -498,8 +482,7 @@ class LLMNode(BaseNode):
# check model
provider_model = provider_model_bundle.configuration.get_provider_model(
model=model_name,
model_type=ModelType.LLM
model=model_name, model_type=ModelType.LLM
)
if provider_model is None:
@ -515,19 +498,16 @@ class LLMNode(BaseNode):
# model config
completion_params = node_data_model.completion_params
stop = []
if 'stop' in completion_params:
stop = completion_params['stop']
del completion_params['stop']
if "stop" in completion_params:
stop = completion_params["stop"]
del completion_params["stop"]
# get model mode
model_mode = node_data_model.mode
if not model_mode:
raise ValueError("LLM mode is required.")
model_schema = model_type_instance.get_model_schema(
model_name,
model_credentials
)
model_schema = model_type_instance.get_model_schema(model_name, model_credentials)
if not model_schema:
raise ValueError(f"Model {model_name} not exist.")
@ -543,9 +523,9 @@ class LLMNode(BaseNode):
stop=stop,
)
def _fetch_memory(self, node_data_memory: Optional[MemoryConfig],
variable_pool: VariablePool,
model_instance: ModelInstance) -> Optional[TokenBufferMemory]:
def _fetch_memory(
self, node_data_memory: Optional[MemoryConfig], variable_pool: VariablePool, model_instance: ModelInstance
) -> Optional[TokenBufferMemory]:
"""
Fetch memory
:param node_data_memory: node data memory
@ -556,35 +536,35 @@ class LLMNode(BaseNode):
return None
# get conversation id
conversation_id = variable_pool.get_any(['sys', SystemVariableKey.CONVERSATION_ID.value])
conversation_id = variable_pool.get_any(["sys", SystemVariableKey.CONVERSATION_ID.value])
if conversation_id is None:
return None
# get conversation
conversation = db.session.query(Conversation).filter(
Conversation.app_id == self.app_id,
Conversation.id == conversation_id
).first()
conversation = (
db.session.query(Conversation)
.filter(Conversation.app_id == self.app_id, Conversation.id == conversation_id)
.first()
)
if not conversation:
return None
memory = TokenBufferMemory(
conversation=conversation,
model_instance=model_instance
)
memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance)
return memory
def _fetch_prompt_messages(self, node_data: LLMNodeData,
query: Optional[str],
query_prompt_template: Optional[str],
inputs: dict[str, str],
files: list["FileVar"],
context: Optional[str],
memory: Optional[TokenBufferMemory],
model_config: ModelConfigWithCredentialsEntity) \
-> tuple[list[PromptMessage], Optional[list[str]]]:
def _fetch_prompt_messages(
self,
node_data: LLMNodeData,
query: Optional[str],
query_prompt_template: Optional[str],
inputs: dict[str, str],
files: list["FileVar"],
context: Optional[str],
memory: Optional[TokenBufferMemory],
model_config: ModelConfigWithCredentialsEntity,
) -> tuple[list[PromptMessage], Optional[list[str]]]:
"""
Fetch prompt messages
:param node_data: node data
@ -601,7 +581,7 @@ class LLMNode(BaseNode):
prompt_messages = prompt_transform.get_prompt(
prompt_template=node_data.prompt_template,
inputs=inputs,
query=query if query else '',
query=query if query else "",
files=files,
context=context,
memory_config=node_data.memory,
@ -621,8 +601,11 @@ class LLMNode(BaseNode):
if not isinstance(prompt_message.content, str):
prompt_message_content = []
for content_item in prompt_message.content:
if vision_enabled and content_item.type == PromptMessageContentType.IMAGE and isinstance(
content_item, ImagePromptMessageContent):
if (
vision_enabled
and content_item.type == PromptMessageContentType.IMAGE
and isinstance(content_item, ImagePromptMessageContent)
):
# Override vision config if LLM node has vision config
if vision_detail:
content_item.detail = ImagePromptMessageContent.DETAIL(vision_detail)
@ -632,15 +615,18 @@ class LLMNode(BaseNode):
if len(prompt_message_content) > 1:
prompt_message.content = prompt_message_content
elif (len(prompt_message_content) == 1
and prompt_message_content[0].type == PromptMessageContentType.TEXT):
elif (
len(prompt_message_content) == 1 and prompt_message_content[0].type == PromptMessageContentType.TEXT
):
prompt_message.content = prompt_message_content[0].data
filtered_prompt_messages.append(prompt_message)
if not filtered_prompt_messages:
raise ValueError("No prompt found in the LLM configuration. "
"Please ensure a prompt is properly configured before proceeding.")
raise ValueError(
"No prompt found in the LLM configuration. "
"Please ensure a prompt is properly configured before proceeding."
)
return filtered_prompt_messages, stop
@ -678,7 +664,7 @@ class LLMNode(BaseNode):
elif quota_unit == QuotaUnit.CREDITS:
used_quota = 1
if 'gpt-4' in model_instance.model:
if "gpt-4" in model_instance.model:
used_quota = 20
else:
used_quota = 1
@ -689,16 +675,13 @@ class LLMNode(BaseNode):
Provider.provider_name == model_instance.provider,
Provider.provider_type == ProviderType.SYSTEM.value,
Provider.quota_type == system_configuration.current_quota_type.value,
Provider.quota_limit > Provider.quota_used
).update({'quota_used': Provider.quota_used + used_quota})
Provider.quota_limit > Provider.quota_used,
).update({"quota_used": Provider.quota_used + used_quota})
db.session.commit()
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: LLMNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: LLMNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -712,11 +695,11 @@ class LLMNode(BaseNode):
variable_selectors = []
if isinstance(prompt_template, list):
for prompt in prompt_template:
if prompt.edition_type != 'jinja2':
if prompt.edition_type != "jinja2":
variable_template_parser = VariableTemplateParser(template=prompt.text)
variable_selectors.extend(variable_template_parser.extract_variable_selectors())
else:
if prompt_template.edition_type != 'jinja2':
if prompt_template.edition_type != "jinja2":
variable_template_parser = VariableTemplateParser(template=prompt_template.text)
variable_selectors = variable_template_parser.extract_variable_selectors()
@ -726,39 +709,38 @@ class LLMNode(BaseNode):
memory = node_data.memory
if memory and memory.query_prompt_template:
query_variable_selectors = (VariableTemplateParser(template=memory.query_prompt_template)
.extract_variable_selectors())
query_variable_selectors = VariableTemplateParser(
template=memory.query_prompt_template
).extract_variable_selectors()
for variable_selector in query_variable_selectors:
variable_mapping[variable_selector.variable] = variable_selector.value_selector
if node_data.context.enabled:
variable_mapping['#context#'] = node_data.context.variable_selector
variable_mapping["#context#"] = node_data.context.variable_selector
if node_data.vision.enabled:
variable_mapping['#files#'] = ['sys', SystemVariableKey.FILES.value]
variable_mapping["#files#"] = ["sys", SystemVariableKey.FILES.value]
if node_data.memory:
variable_mapping['#sys.query#'] = ['sys', SystemVariableKey.QUERY.value]
variable_mapping["#sys.query#"] = ["sys", SystemVariableKey.QUERY.value]
if node_data.prompt_config:
enable_jinja = False
if isinstance(prompt_template, list):
for prompt in prompt_template:
if prompt.edition_type == 'jinja2':
if prompt.edition_type == "jinja2":
enable_jinja = True
break
else:
if prompt_template.edition_type == 'jinja2':
if prompt_template.edition_type == "jinja2":
enable_jinja = True
if enable_jinja:
for variable_selector in node_data.prompt_config.jinja2_variables or []:
variable_mapping[variable_selector.variable] = variable_selector.value_selector
variable_mapping = {
node_id + '.' + key: value for key, value in variable_mapping.items()
}
variable_mapping = {node_id + "." + key: value for key, value in variable_mapping.items()}
return variable_mapping
@ -775,26 +757,19 @@ class LLMNode(BaseNode):
"prompt_templates": {
"chat_model": {
"prompts": [
{
"role": "system",
"text": "You are a helpful AI assistant.",
"edition_type": "basic"
}
{"role": "system", "text": "You are a helpful AI assistant.", "edition_type": "basic"}
]
},
"completion_model": {
"conversation_histories_role": {
"user_prefix": "Human",
"assistant_prefix": "Assistant"
},
"conversation_histories_role": {"user_prefix": "Human", "assistant_prefix": "Assistant"},
"prompt": {
"text": "Here is the chat histories between human and assistant, inside "
"<histories></histories> XML tags.\n\n<histories>\n{{"
"#histories#}}\n</histories>\n\n\nHuman: {{#sys.query#}}\n\nAssistant:",
"edition_type": "basic"
"<histories></histories> XML tags.\n\n<histories>\n{{"
"#histories#}}\n</histories>\n\n\nHuman: {{#sys.query#}}\n\nAssistant:",
"edition_type": "basic",
},
"stop": ["Human:"]
}
"stop": ["Human:"],
},
}
}
},
}

View file

@ -1,4 +1,3 @@
from core.workflow.entities.base_node_data_entities import BaseIterationNodeData, BaseIterationState
@ -7,7 +6,8 @@ class LoopNodeData(BaseIterationNodeData):
Loop Node Data.
"""
class LoopState(BaseIterationState):
"""
Loop State.
"""
"""

View file

@ -10,6 +10,7 @@ class LoopNode(BaseNode):
"""
Loop Node.
"""
_node_data_cls = LoopNodeData
_node_type = NodeType.LOOP
@ -21,14 +22,16 @@ class LoopNode(BaseNode):
"""
Get conditions.
"""
node_id = node_config.get('id')
node_id = node_config.get("id")
if not node_id:
return []
# TODO waiting for implementation
return [Condition(
variable_selector=[node_id, 'index'],
comparison_operator="",
value_type="value_selector",
value_selector=[]
)]
return [
Condition(
variable_selector=[node_id, "index"],
comparison_operator="",
value_type="value_selector",
value_selector=[],
)
]

View file

@ -8,47 +8,52 @@ from core.workflow.entities.base_node_data_entities import BaseNodeData
class ModelConfig(BaseModel):
"""
Model Config.
Model Config.
"""
provider: str
name: str
mode: str
completion_params: dict[str, Any] = {}
class ParameterConfig(BaseModel):
"""
Parameter Config.
"""
name: str
type: Literal['string', 'number', 'bool', 'select', 'array[string]', 'array[number]', 'array[object]']
type: Literal["string", "number", "bool", "select", "array[string]", "array[number]", "array[object]"]
options: Optional[list[str]] = None
description: str
required: bool
@field_validator('name', mode='before')
@field_validator("name", mode="before")
@classmethod
def validate_name(cls, value) -> str:
if not value:
raise ValueError('Parameter name is required')
if value in ['__reason', '__is_success']:
raise ValueError('Invalid parameter name, __reason and __is_success are reserved')
raise ValueError("Parameter name is required")
if value in ["__reason", "__is_success"]:
raise ValueError("Invalid parameter name, __reason and __is_success are reserved")
return value
class ParameterExtractorNodeData(BaseNodeData):
"""
Parameter Extractor Node Data.
"""
model: ModelConfig
query: list[str]
parameters: list[ParameterConfig]
instruction: Optional[str] = None
memory: Optional[MemoryConfig] = None
reasoning_mode: Literal['function_call', 'prompt']
reasoning_mode: Literal["function_call", "prompt"]
@field_validator('reasoning_mode', mode='before')
@field_validator("reasoning_mode", mode="before")
@classmethod
def set_reasoning_mode(cls, v) -> str:
return v or 'function_call'
return v or "function_call"
def get_parameter_json_schema(self) -> dict:
"""
@ -56,32 +61,26 @@ class ParameterExtractorNodeData(BaseNodeData):
:return: parameter json schema
"""
parameters = {
'type': 'object',
'properties': {},
'required': []
}
parameters = {"type": "object", "properties": {}, "required": []}
for parameter in self.parameters:
parameter_schema = {
'description': parameter.description
}
parameter_schema = {"description": parameter.description}
if parameter.type in ['string', 'select']:
parameter_schema['type'] = 'string'
elif parameter.type.startswith('array'):
parameter_schema['type'] = 'array'
if parameter.type in ["string", "select"]:
parameter_schema["type"] = "string"
elif parameter.type.startswith("array"):
parameter_schema["type"] = "array"
nested_type = parameter.type[6:-1]
parameter_schema['items'] = {'type': nested_type}
parameter_schema["items"] = {"type": nested_type}
else:
parameter_schema['type'] = parameter.type
parameter_schema["type"] = parameter.type
if parameter.type == 'select':
parameter_schema['enum'] = parameter.options
if parameter.type == "select":
parameter_schema["enum"] = parameter.options
parameters["properties"][parameter.name] = parameter_schema
parameters['properties'][parameter.name] = parameter_schema
if parameter.required:
parameters['required'].append(parameter.name)
parameters["required"].append(parameter.name)
return parameters
return parameters

View file

@ -45,6 +45,7 @@ class ParameterExtractorNode(LLMNode):
"""
Parameter Extractor Node.
"""
_node_data_cls = ParameterExtractorNodeData
_node_type = NodeType.PARAMETER_EXTRACTOR
@ -57,11 +58,8 @@ class ParameterExtractorNode(LLMNode):
"model": {
"prompt_templates": {
"completion_model": {
"conversation_histories_role": {
"user_prefix": "Human",
"assistant_prefix": "Assistant"
},
"stop": ["Human:"]
"conversation_histories_role": {"user_prefix": "Human", "assistant_prefix": "Assistant"},
"stop": ["Human:"],
}
}
}
@ -78,9 +76,9 @@ class ParameterExtractorNode(LLMNode):
query = variable
inputs = {
'query': query,
'parameters': jsonable_encoder(node_data.parameters),
'instruction': jsonable_encoder(node_data.instruction),
"query": query,
"parameters": jsonable_encoder(node_data.parameters),
"instruction": jsonable_encoder(node_data.instruction),
}
model_instance, model_config = self._fetch_model_config(node_data.model)
@ -95,30 +93,29 @@ class ParameterExtractorNode(LLMNode):
# fetch memory
memory = self._fetch_memory(node_data.memory, self.graph_runtime_state.variable_pool, model_instance)
if set(model_schema.features or []) & {ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL} \
and node_data.reasoning_mode == 'function_call':
# use function call
if (
set(model_schema.features or []) & {ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL}
and node_data.reasoning_mode == "function_call"
):
# use function call
prompt_messages, prompt_message_tools = self._generate_function_call_prompt(
node_data, query, self.graph_runtime_state.variable_pool, model_config, memory
)
else:
# use prompt engineering
prompt_messages = self._generate_prompt_engineering_prompt(node_data,
query,
self.graph_runtime_state.variable_pool,
model_config,
memory)
prompt_messages = self._generate_prompt_engineering_prompt(
node_data, query, self.graph_runtime_state.variable_pool, model_config, memory
)
prompt_message_tools = []
process_data = {
'model_mode': model_config.mode,
'prompts': PromptMessageUtil.prompt_messages_to_prompt_for_saving(
model_mode=model_config.mode,
prompt_messages=prompt_messages
"model_mode": model_config.mode,
"prompts": PromptMessageUtil.prompt_messages_to_prompt_for_saving(
model_mode=model_config.mode, prompt_messages=prompt_messages
),
'usage': None,
'function': {} if not prompt_message_tools else jsonable_encoder(prompt_message_tools[0]),
'tool_call': None,
"usage": None,
"function": {} if not prompt_message_tools else jsonable_encoder(prompt_message_tools[0]),
"tool_call": None,
}
try:
@ -129,20 +126,17 @@ class ParameterExtractorNode(LLMNode):
tools=prompt_message_tools,
stop=model_config.stop,
)
process_data['usage'] = jsonable_encoder(usage)
process_data['tool_call'] = jsonable_encoder(tool_call)
process_data['llm_text'] = text
process_data["usage"] = jsonable_encoder(usage)
process_data["tool_call"] = jsonable_encoder(tool_call)
process_data["llm_text"] = text
except Exception as e:
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs=inputs,
process_data=process_data,
outputs={
'__is_success': 0,
'__reason': str(e)
},
outputs={"__is_success": 0, "__reason": str(e)},
error=str(e),
metadata={}
metadata={},
)
error = None
@ -167,24 +161,23 @@ class ParameterExtractorNode(LLMNode):
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs=inputs,
process_data=process_data,
outputs={
'__is_success': 1 if not error else 0,
'__reason': error,
**result
},
outputs={"__is_success": 1 if not error else 0, "__reason": error, **result},
metadata={
NodeRunMetadataKey.TOTAL_TOKENS: usage.total_tokens,
NodeRunMetadataKey.TOTAL_PRICE: usage.total_price,
NodeRunMetadataKey.CURRENCY: usage.currency
NodeRunMetadataKey.CURRENCY: usage.currency,
},
llm_usage=usage
llm_usage=usage,
)
def _invoke_llm(self, node_data_model: ModelConfig,
model_instance: ModelInstance,
prompt_messages: list[PromptMessage],
tools: list[PromptMessageTool],
stop: list[str]) -> tuple[str, LLMUsage, Optional[AssistantPromptMessage.ToolCall]]:
def _invoke_llm(
self,
node_data_model: ModelConfig,
model_instance: ModelInstance,
prompt_messages: list[PromptMessage],
tools: list[PromptMessageTool],
stop: list[str],
) -> tuple[str, LLMUsage, Optional[AssistantPromptMessage.ToolCall]]:
"""
Invoke large language model
:param node_data_model: node data model
@ -217,32 +210,35 @@ class ParameterExtractorNode(LLMNode):
return text, usage, tool_call
def _generate_function_call_prompt(self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
memory: Optional[TokenBufferMemory],
) -> tuple[list[PromptMessage], list[PromptMessageTool]]:
def _generate_function_call_prompt(
self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
memory: Optional[TokenBufferMemory],
) -> tuple[list[PromptMessage], list[PromptMessageTool]]:
"""
Generate function call prompt.
"""
query = FUNCTION_CALLING_EXTRACTOR_USER_TEMPLATE.format(content=query, structure=json.dumps(
node_data.get_parameter_json_schema()))
query = FUNCTION_CALLING_EXTRACTOR_USER_TEMPLATE.format(
content=query, structure=json.dumps(node_data.get_parameter_json_schema())
)
prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True)
rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, '')
prompt_template = self._get_function_calling_prompt_template(node_data, query, variable_pool, memory,
rest_token)
rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, "")
prompt_template = self._get_function_calling_prompt_template(
node_data, query, variable_pool, memory, rest_token
)
prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template,
inputs={},
query='',
query="",
files=[],
context='',
context="",
memory_config=node_data.memory,
memory=None,
model_config=model_config
model_config=model_config,
)
# find last user message
@ -255,124 +251,125 @@ class ParameterExtractorNode(LLMNode):
example_messages = []
for example in FUNCTION_CALLING_EXTRACTOR_EXAMPLE:
id = uuid.uuid4().hex
example_messages.extend([
UserPromptMessage(content=example['user']['query']),
AssistantPromptMessage(
content=example['assistant']['text'],
tool_calls=[
AssistantPromptMessage.ToolCall(
id=id,
type='function',
function=AssistantPromptMessage.ToolCall.ToolCallFunction(
name=example['assistant']['function_call']['name'],
arguments=json.dumps(example['assistant']['function_call']['parameters']
)
))
]
),
ToolPromptMessage(
content='Great! You have called the function with the correct parameters.',
tool_call_id=id
),
AssistantPromptMessage(
content='I have extracted the parameters, let\'s move on.',
)
])
example_messages.extend(
[
UserPromptMessage(content=example["user"]["query"]),
AssistantPromptMessage(
content=example["assistant"]["text"],
tool_calls=[
AssistantPromptMessage.ToolCall(
id=id,
type="function",
function=AssistantPromptMessage.ToolCall.ToolCallFunction(
name=example["assistant"]["function_call"]["name"],
arguments=json.dumps(example["assistant"]["function_call"]["parameters"]),
),
)
],
),
ToolPromptMessage(
content="Great! You have called the function with the correct parameters.", tool_call_id=id
),
AssistantPromptMessage(
content="I have extracted the parameters, let's move on.",
),
]
)
prompt_messages = prompt_messages[:last_user_message_idx] + \
example_messages + prompt_messages[last_user_message_idx:]
prompt_messages = (
prompt_messages[:last_user_message_idx] + example_messages + prompt_messages[last_user_message_idx:]
)
# generate tool
tool = PromptMessageTool(
name=FUNCTION_CALLING_EXTRACTOR_NAME,
description='Extract parameters from the natural language text',
description="Extract parameters from the natural language text",
parameters=node_data.get_parameter_json_schema(),
)
return prompt_messages, [tool]
def _generate_prompt_engineering_prompt(self,
data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
memory: Optional[TokenBufferMemory],
) -> list[PromptMessage]:
def _generate_prompt_engineering_prompt(
self,
data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
memory: Optional[TokenBufferMemory],
) -> list[PromptMessage]:
"""
Generate prompt engineering prompt.
"""
model_mode = ModelMode.value_of(data.model.mode)
if model_mode == ModelMode.COMPLETION:
return self._generate_prompt_engineering_completion_prompt(
data, query, variable_pool, model_config, memory
)
return self._generate_prompt_engineering_completion_prompt(data, query, variable_pool, model_config, memory)
elif model_mode == ModelMode.CHAT:
return self._generate_prompt_engineering_chat_prompt(
data, query, variable_pool, model_config, memory
)
return self._generate_prompt_engineering_chat_prompt(data, query, variable_pool, model_config, memory)
else:
raise ValueError(f"Invalid model mode: {model_mode}")
def _generate_prompt_engineering_completion_prompt(self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
memory: Optional[TokenBufferMemory],
) -> list[PromptMessage]:
def _generate_prompt_engineering_completion_prompt(
self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
memory: Optional[TokenBufferMemory],
) -> list[PromptMessage]:
"""
Generate completion prompt.
"""
prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True)
rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, '')
prompt_template = self._get_prompt_engineering_prompt_template(node_data, query, variable_pool, memory,
rest_token)
rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, "")
prompt_template = self._get_prompt_engineering_prompt_template(
node_data, query, variable_pool, memory, rest_token
)
prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template,
inputs={
'structure': json.dumps(node_data.get_parameter_json_schema())
},
query='',
inputs={"structure": json.dumps(node_data.get_parameter_json_schema())},
query="",
files=[],
context='',
context="",
memory_config=node_data.memory,
memory=memory,
model_config=model_config
model_config=model_config,
)
return prompt_messages
def _generate_prompt_engineering_chat_prompt(self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
memory: Optional[TokenBufferMemory],
) -> list[PromptMessage]:
def _generate_prompt_engineering_chat_prompt(
self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
memory: Optional[TokenBufferMemory],
) -> list[PromptMessage]:
"""
Generate chat prompt.
"""
prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True)
rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, '')
rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, "")
prompt_template = self._get_prompt_engineering_prompt_template(
node_data,
CHAT_GENERATE_JSON_USER_MESSAGE_TEMPLATE.format(
structure=json.dumps(node_data.get_parameter_json_schema()),
text=query
structure=json.dumps(node_data.get_parameter_json_schema()), text=query
),
variable_pool, memory, rest_token
variable_pool,
memory,
rest_token,
)
prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template,
inputs={},
query='',
query="",
files=[],
context='',
context="",
memory_config=node_data.memory,
memory=None,
model_config=model_config
model_config=model_config,
)
# find last user message
@ -384,18 +381,23 @@ class ParameterExtractorNode(LLMNode):
# add example messages before last user message
example_messages = []
for example in CHAT_EXAMPLE:
example_messages.extend([
UserPromptMessage(content=CHAT_GENERATE_JSON_USER_MESSAGE_TEMPLATE.format(
structure=json.dumps(example['user']['json']),
text=example['user']['query'],
)),
AssistantPromptMessage(
content=json.dumps(example['assistant']['json']),
)
])
example_messages.extend(
[
UserPromptMessage(
content=CHAT_GENERATE_JSON_USER_MESSAGE_TEMPLATE.format(
structure=json.dumps(example["user"]["json"]),
text=example["user"]["query"],
)
),
AssistantPromptMessage(
content=json.dumps(example["assistant"]["json"]),
),
]
)
prompt_messages = prompt_messages[:last_user_message_idx] + \
example_messages + prompt_messages[last_user_message_idx:]
prompt_messages = (
prompt_messages[:last_user_message_idx] + example_messages + prompt_messages[last_user_message_idx:]
)
return prompt_messages
@ -410,28 +412,28 @@ class ParameterExtractorNode(LLMNode):
if parameter.required and parameter.name not in result:
raise ValueError(f"Parameter {parameter.name} is required")
if parameter.type == 'select' and parameter.options and result.get(parameter.name) not in parameter.options:
if parameter.type == "select" and parameter.options and result.get(parameter.name) not in parameter.options:
raise ValueError(f"Invalid `select` value for parameter {parameter.name}")
if parameter.type == 'number' and not isinstance(result.get(parameter.name), int | float):
if parameter.type == "number" and not isinstance(result.get(parameter.name), int | float):
raise ValueError(f"Invalid `number` value for parameter {parameter.name}")
if parameter.type == 'bool' and not isinstance(result.get(parameter.name), bool):
if parameter.type == "bool" and not isinstance(result.get(parameter.name), bool):
raise ValueError(f"Invalid `bool` value for parameter {parameter.name}")
if parameter.type == 'string' and not isinstance(result.get(parameter.name), str):
if parameter.type == "string" and not isinstance(result.get(parameter.name), str):
raise ValueError(f"Invalid `string` value for parameter {parameter.name}")
if parameter.type.startswith('array'):
if parameter.type.startswith("array"):
if not isinstance(result.get(parameter.name), list):
raise ValueError(f"Invalid `array` value for parameter {parameter.name}")
nested_type = parameter.type[6:-1]
for item in result.get(parameter.name):
if nested_type == 'number' and not isinstance(item, int | float):
if nested_type == "number" and not isinstance(item, int | float):
raise ValueError(f"Invalid `array[number]` value for parameter {parameter.name}")
if nested_type == 'string' and not isinstance(item, str):
if nested_type == "string" and not isinstance(item, str):
raise ValueError(f"Invalid `array[string]` value for parameter {parameter.name}")
if nested_type == 'object' and not isinstance(item, dict):
if nested_type == "object" and not isinstance(item, dict):
raise ValueError(f"Invalid `array[object]` value for parameter {parameter.name}")
return result
@ -443,12 +445,12 @@ class ParameterExtractorNode(LLMNode):
for parameter in data.parameters:
if parameter.name in result:
# transform value
if parameter.type == 'number':
if parameter.type == "number":
if isinstance(result[parameter.name], int | float):
transformed_result[parameter.name] = result[parameter.name]
elif isinstance(result[parameter.name], str):
try:
if '.' in result[parameter.name]:
if "." in result[parameter.name]:
result[parameter.name] = float(result[parameter.name])
else:
result[parameter.name] = int(result[parameter.name])
@ -465,40 +467,40 @@ class ParameterExtractorNode(LLMNode):
# transformed_result[parameter.name] = bool(result[parameter.name].lower() == 'true')
# elif isinstance(result[parameter.name], int):
# transformed_result[parameter.name] = bool(result[parameter.name])
elif parameter.type in ['string', 'select']:
elif parameter.type in ["string", "select"]:
if isinstance(result[parameter.name], str):
transformed_result[parameter.name] = result[parameter.name]
elif parameter.type.startswith('array'):
elif parameter.type.startswith("array"):
if isinstance(result[parameter.name], list):
nested_type = parameter.type[6:-1]
transformed_result[parameter.name] = []
for item in result[parameter.name]:
if nested_type == 'number':
if nested_type == "number":
if isinstance(item, int | float):
transformed_result[parameter.name].append(item)
elif isinstance(item, str):
try:
if '.' in item:
if "." in item:
transformed_result[parameter.name].append(float(item))
else:
transformed_result[parameter.name].append(int(item))
except ValueError:
pass
elif nested_type == 'string':
elif nested_type == "string":
if isinstance(item, str):
transformed_result[parameter.name].append(item)
elif nested_type == 'object':
elif nested_type == "object":
if isinstance(item, dict):
transformed_result[parameter.name].append(item)
if parameter.name not in transformed_result:
if parameter.type == 'number':
if parameter.type == "number":
transformed_result[parameter.name] = 0
elif parameter.type == 'bool':
elif parameter.type == "bool":
transformed_result[parameter.name] = False
elif parameter.type in ['string', 'select']:
transformed_result[parameter.name] = ''
elif parameter.type.startswith('array'):
elif parameter.type in ["string", "select"]:
transformed_result[parameter.name] = ""
elif parameter.type.startswith("array"):
transformed_result[parameter.name] = []
return transformed_result
@ -514,24 +516,24 @@ class ParameterExtractorNode(LLMNode):
"""
stack = []
for i, c in enumerate(text):
if c == '{' or c == '[':
if c == "{" or c == "[":
stack.append(c)
elif c == '}' or c == ']':
elif c == "}" or c == "]":
# check if stack is empty
if not stack:
return text[:i]
# check if the last element in stack is matching
if (c == '}' and stack[-1] == '{') or (c == ']' and stack[-1] == '['):
if (c == "}" and stack[-1] == "{") or (c == "]" and stack[-1] == "["):
stack.pop()
if not stack:
return text[:i + 1]
return text[: i + 1]
else:
return text[:i]
return None
# extract json from the text
for idx in range(len(result)):
if result[idx] == '{' or result[idx] == '[':
if result[idx] == "{" or result[idx] == "[":
json_str = extract_json(result[idx:])
if json_str:
try:
@ -554,12 +556,12 @@ class ParameterExtractorNode(LLMNode):
"""
result = {}
for parameter in data.parameters:
if parameter.type == 'number':
if parameter.type == "number":
result[parameter.name] = 0
elif parameter.type == 'bool':
elif parameter.type == "bool":
result[parameter.name] = False
elif parameter.type in ['string', 'select']:
result[parameter.name] = ''
elif parameter.type in ["string", "select"]:
result[parameter.name] = ""
return result
@ -575,71 +577,76 @@ class ParameterExtractorNode(LLMNode):
return variable_template_parser.format(inputs)
def _get_function_calling_prompt_template(self, node_data: ParameterExtractorNodeData, query: str,
variable_pool: VariablePool,
memory: Optional[TokenBufferMemory],
max_token_limit: int = 2000) \
-> list[ChatModelMessage]:
def _get_function_calling_prompt_template(
self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
memory: Optional[TokenBufferMemory],
max_token_limit: int = 2000,
) -> list[ChatModelMessage]:
model_mode = ModelMode.value_of(node_data.model.mode)
input_text = query
memory_str = ''
instruction = self._render_instruction(node_data.instruction or '', variable_pool)
memory_str = ""
instruction = self._render_instruction(node_data.instruction or "", variable_pool)
if memory:
memory_str = memory.get_history_prompt_text(max_token_limit=max_token_limit,
message_limit=node_data.memory.window.size)
memory_str = memory.get_history_prompt_text(
max_token_limit=max_token_limit, message_limit=node_data.memory.window.size
)
if model_mode == ModelMode.CHAT:
system_prompt_messages = ChatModelMessage(
role=PromptMessageRole.SYSTEM,
text=FUNCTION_CALLING_EXTRACTOR_SYSTEM_PROMPT.format(histories=memory_str, instruction=instruction)
)
user_prompt_message = ChatModelMessage(
role=PromptMessageRole.USER,
text=input_text
text=FUNCTION_CALLING_EXTRACTOR_SYSTEM_PROMPT.format(histories=memory_str, instruction=instruction),
)
user_prompt_message = ChatModelMessage(role=PromptMessageRole.USER, text=input_text)
return [system_prompt_messages, user_prompt_message]
else:
raise ValueError(f"Model mode {model_mode} not support.")
def _get_prompt_engineering_prompt_template(self, node_data: ParameterExtractorNodeData, query: str,
variable_pool: VariablePool,
memory: Optional[TokenBufferMemory],
max_token_limit: int = 2000) \
-> list[ChatModelMessage]:
def _get_prompt_engineering_prompt_template(
self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
memory: Optional[TokenBufferMemory],
max_token_limit: int = 2000,
) -> list[ChatModelMessage]:
model_mode = ModelMode.value_of(node_data.model.mode)
input_text = query
memory_str = ''
instruction = self._render_instruction(node_data.instruction or '', variable_pool)
memory_str = ""
instruction = self._render_instruction(node_data.instruction or "", variable_pool)
if memory:
memory_str = memory.get_history_prompt_text(max_token_limit=max_token_limit,
message_limit=node_data.memory.window.size)
memory_str = memory.get_history_prompt_text(
max_token_limit=max_token_limit, message_limit=node_data.memory.window.size
)
if model_mode == ModelMode.CHAT:
system_prompt_messages = ChatModelMessage(
role=PromptMessageRole.SYSTEM,
text=FUNCTION_CALLING_EXTRACTOR_SYSTEM_PROMPT.format(histories=memory_str, instruction=instruction)
)
user_prompt_message = ChatModelMessage(
role=PromptMessageRole.USER,
text=input_text
text=FUNCTION_CALLING_EXTRACTOR_SYSTEM_PROMPT.format(histories=memory_str, instruction=instruction),
)
user_prompt_message = ChatModelMessage(role=PromptMessageRole.USER, text=input_text)
return [system_prompt_messages, user_prompt_message]
elif model_mode == ModelMode.COMPLETION:
return CompletionModelPromptTemplate(
text=COMPLETION_GENERATE_JSON_PROMPT.format(histories=memory_str,
text=input_text,
instruction=instruction)
.replace('{γγγ', '')
.replace('}γγγ', '')
text=COMPLETION_GENERATE_JSON_PROMPT.format(
histories=memory_str, text=input_text, instruction=instruction
)
.replace("{γγγ", "")
.replace("}γγγ", "")
)
else:
raise ValueError(f"Model mode {model_mode} not support.")
def _calculate_rest_token(self, node_data: ParameterExtractorNodeData, query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
context: Optional[str]) -> int:
def _calculate_rest_token(
self,
node_data: ParameterExtractorNodeData,
query: str,
variable_pool: VariablePool,
model_config: ModelConfigWithCredentialsEntity,
context: Optional[str],
) -> int:
prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True)
model_instance, model_config = self._fetch_model_config(node_data.model)
@ -659,12 +666,12 @@ class ParameterExtractorNode(LLMNode):
prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template,
inputs={},
query='',
query="",
files=[],
context=context,
memory_config=node_data.memory,
memory=None,
model_config=model_config
model_config=model_config,
)
rest_tokens = 2000
@ -673,26 +680,28 @@ class ParameterExtractorNode(LLMNode):
model_type_instance = model_config.provider_model_bundle.model_type_instance
model_type_instance = cast(LargeLanguageModel, model_type_instance)
curr_message_tokens = model_type_instance.get_num_tokens(
model_config.model,
model_config.credentials,
prompt_messages
) + 1000 # add 1000 to ensure tool call messages
curr_message_tokens = (
model_type_instance.get_num_tokens(model_config.model, model_config.credentials, prompt_messages) + 1000
) # add 1000 to ensure tool call messages
max_tokens = 0
for parameter_rule in model_config.model_schema.parameter_rules:
if (parameter_rule.name == 'max_tokens'
or (parameter_rule.use_template and parameter_rule.use_template == 'max_tokens')):
max_tokens = (model_config.parameters.get(parameter_rule.name)
or model_config.parameters.get(parameter_rule.use_template)) or 0
if parameter_rule.name == "max_tokens" or (
parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
):
max_tokens = (
model_config.parameters.get(parameter_rule.name)
or model_config.parameters.get(parameter_rule.use_template)
) or 0
rest_tokens = model_context_tokens - max_tokens - curr_message_tokens
rest_tokens = max(rest_tokens, 0)
return rest_tokens
def _fetch_model_config(self, node_data_model: ModelConfig) -> tuple[
ModelInstance, ModelConfigWithCredentialsEntity]:
def _fetch_model_config(
self, node_data_model: ModelConfig
) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]:
"""
Fetch model config.
"""
@ -703,10 +712,7 @@ class ParameterExtractorNode(LLMNode):
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: ParameterExtractorNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: ParameterExtractorNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -715,17 +721,13 @@ class ParameterExtractorNode(LLMNode):
:param node_data: node data
:return:
"""
variable_mapping = {
'query': node_data.query
}
variable_mapping = {"query": node_data.query}
if node_data.instruction:
variable_template_parser = VariableTemplateParser(template=node_data.instruction)
for selector in variable_template_parser.extract_variable_selectors():
variable_mapping[selector.variable] = selector.value_selector
variable_mapping = {
node_id + '.' + key: value for key, value in variable_mapping.items()
}
variable_mapping = {node_id + "." + key: value for key, value in variable_mapping.items()}
return variable_mapping

View file

@ -1,4 +1,4 @@
FUNCTION_CALLING_EXTRACTOR_NAME = 'extract_parameters'
FUNCTION_CALLING_EXTRACTOR_NAME = "extract_parameters"
FUNCTION_CALLING_EXTRACTOR_SYSTEM_PROMPT = f"""You are a helpful assistant tasked with extracting structured information based on specific criteria provided. Follow the guidelines below to ensure consistency and accuracy.
### Task
@ -35,61 +35,48 @@ FUNCTION_CALLING_EXTRACTOR_USER_TEMPLATE = f"""extract structured information fr
</structure>
"""
FUNCTION_CALLING_EXTRACTOR_EXAMPLE = [{
'user': {
'query': 'What is the weather today in SF?',
'function': {
'name': FUNCTION_CALLING_EXTRACTOR_NAME,
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': 'The location to get the weather information',
'required': True
FUNCTION_CALLING_EXTRACTOR_EXAMPLE = [
{
"user": {
"query": "What is the weather today in SF?",
"function": {
"name": FUNCTION_CALLING_EXTRACTOR_NAME,
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The location to get the weather information",
"required": True,
},
},
"required": ["location"],
},
'required': ['location']
}
}
},
},
"assistant": {
"text": "I need always call the function with the correct parameters. in this case, I need to call the function with the location parameter.",
"function_call": {"name": FUNCTION_CALLING_EXTRACTOR_NAME, "parameters": {"location": "San Francisco"}},
},
},
'assistant': {
'text': 'I need always call the function with the correct parameters. in this case, I need to call the function with the location parameter.',
'function_call' : {
'name': FUNCTION_CALLING_EXTRACTOR_NAME,
'parameters': {
'location': 'San Francisco'
}
}
}
}, {
'user': {
'query': 'I want to eat some apple pie.',
'function': {
'name': FUNCTION_CALLING_EXTRACTOR_NAME,
'parameters': {
'type': 'object',
'properties': {
'food': {
'type': 'string',
'description': 'The food to eat',
'required': True
}
{
"user": {
"query": "I want to eat some apple pie.",
"function": {
"name": FUNCTION_CALLING_EXTRACTOR_NAME,
"parameters": {
"type": "object",
"properties": {"food": {"type": "string", "description": "The food to eat", "required": True}},
"required": ["food"],
},
'required': ['food']
}
}
},
},
"assistant": {
"text": "I need always call the function with the correct parameters. in this case, I need to call the function with the food parameter.",
"function_call": {"name": FUNCTION_CALLING_EXTRACTOR_NAME, "parameters": {"food": "apple pie"}},
},
},
'assistant': {
'text': 'I need always call the function with the correct parameters. in this case, I need to call the function with the food parameter.',
'function_call' : {
'name': FUNCTION_CALLING_EXTRACTOR_NAME,
'parameters': {
'food': 'apple pie'
}
}
}
}]
]
COMPLETION_GENERATE_JSON_PROMPT = """### Instructions:
Some extra information are provided below, I should always follow the instructions as possible as I can.
@ -161,46 +148,33 @@ Inside <text></text> XML tags, there is a text that you should convert to a JSON
</text>
"""
CHAT_EXAMPLE = [{
'user': {
'query': 'What is the weather today in SF?',
'json': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': 'The location to get the weather information',
'required': True
}
CHAT_EXAMPLE = [
{
"user": {
"query": "What is the weather today in SF?",
"json": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The location to get the weather information",
"required": True,
}
},
"required": ["location"],
},
'required': ['location']
}
},
"assistant": {"text": "I need to output a valid JSON object.", "json": {"location": "San Francisco"}},
},
'assistant': {
'text': 'I need to output a valid JSON object.',
'json': {
'location': 'San Francisco'
}
}
}, {
'user': {
'query': 'I want to eat some apple pie.',
'json': {
'type': 'object',
'properties': {
'food': {
'type': 'string',
'description': 'The food to eat',
'required': True
}
{
"user": {
"query": "I want to eat some apple pie.",
"json": {
"type": "object",
"properties": {"food": {"type": "string", "description": "The food to eat", "required": True}},
"required": ["food"],
},
'required': ['food']
}
},
"assistant": {"text": "I need to output a valid JSON object.", "json": {"result": "apple pie"}},
},
'assistant': {
'text': 'I need to output a valid JSON object.',
'json': {
'result': 'apple pie'
}
}
}]
]

View file

@ -8,8 +8,9 @@ from core.workflow.entities.base_node_data_entities import BaseNodeData
class ModelConfig(BaseModel):
"""
Model Config.
Model Config.
"""
provider: str
name: str
mode: str
@ -20,6 +21,7 @@ class ClassConfig(BaseModel):
"""
Class Config.
"""
id: str
name: str
@ -28,8 +30,9 @@ class QuestionClassifierNodeData(BaseNodeData):
"""
Knowledge retrieval Node Data.
"""
query_variable_selector: list[str]
type: str = 'question-classifier'
type: str = "question-classifier"
model: ModelConfig
classes: list[ClassConfig]
instruction: Optional[str] = None

View file

@ -45,34 +45,25 @@ class QuestionClassifierNode(LLMNode):
# extract variables
variable = variable_pool.get(node_data.query_variable_selector)
query = variable.value if variable else None
variables = {
'query': query
}
variables = {"query": query}
# fetch model config
model_instance, model_config = self._fetch_model_config(node_data.model)
# fetch memory
memory = self._fetch_memory(node_data.memory, variable_pool, model_instance)
# fetch instruction
instruction = self._format_instruction(node_data.instruction, variable_pool) if node_data.instruction else ''
instruction = self._format_instruction(node_data.instruction, variable_pool) if node_data.instruction else ""
node_data.instruction = instruction
# fetch prompt messages
prompt_messages, stop = self._fetch_prompt(
node_data=node_data,
context='',
query=query,
memory=memory,
model_config=model_config
node_data=node_data, context="", query=query, memory=memory, model_config=model_config
)
# handle invoke result
generator = self._invoke_llm(
node_data_model=node_data.model,
model_instance=model_instance,
prompt_messages=prompt_messages,
stop=stop
node_data_model=node_data.model, model_instance=model_instance, prompt_messages=prompt_messages, stop=stop
)
result_text = ''
result_text = ""
usage = LLMUsage.empty_usage()
finish_reason = None
for event in generator:
@ -87,8 +78,8 @@ class QuestionClassifierNode(LLMNode):
try:
result_text_json = parse_and_check_json_markdown(result_text, [])
# result_text_json = json.loads(result_text.strip('```JSON\n'))
if 'category_name' in result_text_json and 'category_id' in result_text_json:
category_id_result = result_text_json['category_id']
if "category_name" in result_text_json and "category_id" in result_text_json:
category_id_result = result_text_json["category_id"]
classes = node_data.classes
classes_map = {class_.id: class_.name for class_ in classes}
category_ids = [_class.id for _class in classes]
@ -100,17 +91,14 @@ class QuestionClassifierNode(LLMNode):
logging.error(f"Failed to parse result text: {result_text}")
try:
process_data = {
'model_mode': model_config.mode,
'prompts': PromptMessageUtil.prompt_messages_to_prompt_for_saving(
model_mode=model_config.mode,
prompt_messages=prompt_messages
"model_mode": model_config.mode,
"prompts": PromptMessageUtil.prompt_messages_to_prompt_for_saving(
model_mode=model_config.mode, prompt_messages=prompt_messages
),
'usage': jsonable_encoder(usage),
'finish_reason': finish_reason
}
outputs = {
'class_name': category_name
"usage": jsonable_encoder(usage),
"finish_reason": finish_reason,
}
outputs = {"class_name": category_name}
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
@ -121,9 +109,9 @@ class QuestionClassifierNode(LLMNode):
metadata={
NodeRunMetadataKey.TOTAL_TOKENS: usage.total_tokens,
NodeRunMetadataKey.TOTAL_PRICE: usage.total_price,
NodeRunMetadataKey.CURRENCY: usage.currency
NodeRunMetadataKey.CURRENCY: usage.currency,
},
llm_usage=usage
llm_usage=usage,
)
except ValueError as e:
@ -134,17 +122,14 @@ class QuestionClassifierNode(LLMNode):
metadata={
NodeRunMetadataKey.TOTAL_TOKENS: usage.total_tokens,
NodeRunMetadataKey.TOTAL_PRICE: usage.total_price,
NodeRunMetadataKey.CURRENCY: usage.currency
NodeRunMetadataKey.CURRENCY: usage.currency,
},
llm_usage=usage
llm_usage=usage,
)
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: QuestionClassifierNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: QuestionClassifierNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -153,7 +138,7 @@ class QuestionClassifierNode(LLMNode):
:param node_data: node data
:return:
"""
variable_mapping = {'query': node_data.query_variable_selector}
variable_mapping = {"query": node_data.query_variable_selector}
variable_selectors = []
if node_data.instruction:
variable_template_parser = VariableTemplateParser(template=node_data.instruction)
@ -161,10 +146,8 @@ class QuestionClassifierNode(LLMNode):
for variable_selector in variable_selectors:
variable_mapping[variable_selector.variable] = variable_selector.value_selector
variable_mapping = {
node_id + '.' + key: value for key, value in variable_mapping.items()
}
variable_mapping = {node_id + "." + key: value for key, value in variable_mapping.items()}
return variable_mapping
@classmethod
@ -174,19 +157,16 @@ class QuestionClassifierNode(LLMNode):
:param filters: filter by node config parameters.
:return:
"""
return {
"type": "question-classifier",
"config": {
"instructions": ""
}
}
return {"type": "question-classifier", "config": {"instructions": ""}}
def _fetch_prompt(self, node_data: QuestionClassifierNodeData,
query: str,
context: Optional[str],
memory: Optional[TokenBufferMemory],
model_config: ModelConfigWithCredentialsEntity) \
-> tuple[list[PromptMessage], Optional[list[str]]]:
def _fetch_prompt(
self,
node_data: QuestionClassifierNodeData,
query: str,
context: Optional[str],
memory: Optional[TokenBufferMemory],
model_config: ModelConfigWithCredentialsEntity,
) -> tuple[list[PromptMessage], Optional[list[str]]]:
"""
Fetch prompt
:param node_data: node data
@ -202,118 +182,122 @@ class QuestionClassifierNode(LLMNode):
prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template,
inputs={},
query='',
query="",
files=[],
context=context,
memory_config=node_data.memory,
memory=None,
model_config=model_config
model_config=model_config,
)
stop = model_config.stop
return prompt_messages, stop
def _calculate_rest_token(self, node_data: QuestionClassifierNodeData, query: str,
model_config: ModelConfigWithCredentialsEntity,
context: Optional[str]) -> int:
def _calculate_rest_token(
self,
node_data: QuestionClassifierNodeData,
query: str,
model_config: ModelConfigWithCredentialsEntity,
context: Optional[str],
) -> int:
prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True)
prompt_template = self._get_prompt_template(node_data, query, None, 2000)
prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template,
inputs={},
query='',
query="",
files=[],
context=context,
memory_config=node_data.memory,
memory=None,
model_config=model_config
model_config=model_config,
)
rest_tokens = 2000
model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE)
if model_context_tokens:
model_instance = ModelInstance(
provider_model_bundle=model_config.provider_model_bundle,
model=model_config.model
provider_model_bundle=model_config.provider_model_bundle, model=model_config.model
)
curr_message_tokens = model_instance.get_llm_num_tokens(
prompt_messages
)
curr_message_tokens = model_instance.get_llm_num_tokens(prompt_messages)
max_tokens = 0
for parameter_rule in model_config.model_schema.parameter_rules:
if (parameter_rule.name == 'max_tokens'
or (parameter_rule.use_template and parameter_rule.use_template == 'max_tokens')):
max_tokens = (model_config.parameters.get(parameter_rule.name)
or model_config.parameters.get(parameter_rule.use_template)) or 0
if parameter_rule.name == "max_tokens" or (
parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
):
max_tokens = (
model_config.parameters.get(parameter_rule.name)
or model_config.parameters.get(parameter_rule.use_template)
) or 0
rest_tokens = model_context_tokens - max_tokens - curr_message_tokens
rest_tokens = max(rest_tokens, 0)
return rest_tokens
def _get_prompt_template(self, node_data: QuestionClassifierNodeData, query: str,
memory: Optional[TokenBufferMemory],
max_token_limit: int = 2000) \
-> Union[list[ChatModelMessage], CompletionModelPromptTemplate]:
def _get_prompt_template(
self,
node_data: QuestionClassifierNodeData,
query: str,
memory: Optional[TokenBufferMemory],
max_token_limit: int = 2000,
) -> Union[list[ChatModelMessage], CompletionModelPromptTemplate]:
model_mode = ModelMode.value_of(node_data.model.mode)
classes = node_data.classes
categories = []
for class_ in classes:
category = {
'category_id': class_.id,
'category_name': class_.name
}
category = {"category_id": class_.id, "category_name": class_.name}
categories.append(category)
instruction = node_data.instruction if node_data.instruction else ''
instruction = node_data.instruction if node_data.instruction else ""
input_text = query
memory_str = ''
memory_str = ""
if memory:
memory_str = memory.get_history_prompt_text(max_token_limit=max_token_limit,
message_limit=node_data.memory.window.size)
memory_str = memory.get_history_prompt_text(
max_token_limit=max_token_limit, message_limit=node_data.memory.window.size
)
prompt_messages = []
if model_mode == ModelMode.CHAT:
system_prompt_messages = ChatModelMessage(
role=PromptMessageRole.SYSTEM,
text=QUESTION_CLASSIFIER_SYSTEM_PROMPT.format(histories=memory_str)
role=PromptMessageRole.SYSTEM, text=QUESTION_CLASSIFIER_SYSTEM_PROMPT.format(histories=memory_str)
)
prompt_messages.append(system_prompt_messages)
user_prompt_message_1 = ChatModelMessage(
role=PromptMessageRole.USER,
text=QUESTION_CLASSIFIER_USER_PROMPT_1
role=PromptMessageRole.USER, text=QUESTION_CLASSIFIER_USER_PROMPT_1
)
prompt_messages.append(user_prompt_message_1)
assistant_prompt_message_1 = ChatModelMessage(
role=PromptMessageRole.ASSISTANT,
text=QUESTION_CLASSIFIER_ASSISTANT_PROMPT_1
role=PromptMessageRole.ASSISTANT, text=QUESTION_CLASSIFIER_ASSISTANT_PROMPT_1
)
prompt_messages.append(assistant_prompt_message_1)
user_prompt_message_2 = ChatModelMessage(
role=PromptMessageRole.USER,
text=QUESTION_CLASSIFIER_USER_PROMPT_2
role=PromptMessageRole.USER, text=QUESTION_CLASSIFIER_USER_PROMPT_2
)
prompt_messages.append(user_prompt_message_2)
assistant_prompt_message_2 = ChatModelMessage(
role=PromptMessageRole.ASSISTANT,
text=QUESTION_CLASSIFIER_ASSISTANT_PROMPT_2
role=PromptMessageRole.ASSISTANT, text=QUESTION_CLASSIFIER_ASSISTANT_PROMPT_2
)
prompt_messages.append(assistant_prompt_message_2)
user_prompt_message_3 = ChatModelMessage(
role=PromptMessageRole.USER,
text=QUESTION_CLASSIFIER_USER_PROMPT_3.format(input_text=input_text,
categories=json.dumps(categories, ensure_ascii=False),
classification_instructions=instruction)
text=QUESTION_CLASSIFIER_USER_PROMPT_3.format(
input_text=input_text,
categories=json.dumps(categories, ensure_ascii=False),
classification_instructions=instruction,
),
)
prompt_messages.append(user_prompt_message_3)
return prompt_messages
elif model_mode == ModelMode.COMPLETION:
return CompletionModelPromptTemplate(
text=QUESTION_CLASSIFIER_COMPLETION_PROMPT.format(histories=memory_str,
input_text=input_text,
categories=json.dumps(categories),
classification_instructions=instruction,
ensure_ascii=False)
text=QUESTION_CLASSIFIER_COMPLETION_PROMPT.format(
histories=memory_str,
input_text=input_text,
categories=json.dumps(categories),
classification_instructions=instruction,
ensure_ascii=False,
)
)
else:
@ -329,14 +313,12 @@ class QuestionClassifierNode(LLMNode):
variable = variable_pool.get(variable_selector.value_selector)
variable_value = variable.value if variable else None
if variable_value is None:
raise ValueError(f'Variable {variable_selector.variable} not found')
raise ValueError(f"Variable {variable_selector.variable} not found")
inputs[variable_selector.variable] = variable_value
prompt_template = PromptTemplateParser(template=instruction, with_variable_tmpl=True)
prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs}
instruction = prompt_template.format(
prompt_inputs
)
instruction = prompt_template.format(prompt_inputs)
return instruction

View file

@ -1,5 +1,3 @@
QUESTION_CLASSIFIER_SYSTEM_PROMPT = """
### Job Description',
You are a text classification engine that analyzes text data and assigns categories based on user input or automatically determined categories.

View file

@ -10,4 +10,5 @@ class StartNodeData(BaseNodeData):
"""
Start Node Data
"""
variables: Sequence[VariableEntity] = Field(default_factory=list)

View file

@ -1,4 +1,3 @@
from collections.abc import Mapping, Sequence
from typing import Any
@ -22,20 +21,13 @@ class StartNode(BaseNode):
system_inputs = self.graph_runtime_state.variable_pool.system_variables
for var in system_inputs:
node_inputs[SYSTEM_VARIABLE_NODE_ID + '.' + var] = system_inputs[var]
node_inputs[SYSTEM_VARIABLE_NODE_ID + "." + var] = system_inputs[var]
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs=node_inputs,
outputs=node_inputs
)
return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=node_inputs, outputs=node_inputs)
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: StartNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: StartNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping

View file

@ -1,5 +1,3 @@
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.variable_entities import VariableSelector
@ -8,5 +6,6 @@ class TemplateTransformNodeData(BaseNodeData):
"""
Code Node Data.
"""
variables: list[VariableSelector]
template: str
template: str

View file

@ -8,7 +8,7 @@ from core.workflow.nodes.base_node import BaseNode
from core.workflow.nodes.template_transform.entities import TemplateTransformNodeData
from models.workflow import WorkflowNodeExecutionStatus
MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH = int(os.environ.get('TEMPLATE_TRANSFORM_MAX_LENGTH', '80000'))
MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH = int(os.environ.get("TEMPLATE_TRANSFORM_MAX_LENGTH", "80000"))
class TemplateTransformNode(BaseNode):
@ -24,15 +24,7 @@ class TemplateTransformNode(BaseNode):
"""
return {
"type": "template-transform",
"config": {
"variables": [
{
"variable": "arg1",
"value_selector": []
}
],
"template": "{{ arg1 }}"
}
"config": {"variables": [{"variable": "arg1", "value_selector": []}], "template": "{{ arg1 }}"},
}
def _run(self) -> NodeRunResult:
@ -51,38 +43,25 @@ class TemplateTransformNode(BaseNode):
# Run code
try:
result = CodeExecutor.execute_workflow_code_template(
language=CodeLanguage.JINJA2,
code=node_data.template,
inputs=variables
language=CodeLanguage.JINJA2, code=node_data.template, inputs=variables
)
except CodeExecutionException as e:
return NodeRunResult(
inputs=variables,
status=WorkflowNodeExecutionStatus.FAILED,
error=str(e)
)
return NodeRunResult(inputs=variables, status=WorkflowNodeExecutionStatus.FAILED, error=str(e))
if len(result['result']) > MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH:
if len(result["result"]) > MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH:
return NodeRunResult(
inputs=variables,
status=WorkflowNodeExecutionStatus.FAILED,
error=f"Output length exceeds {MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH} characters"
error=f"Output length exceeds {MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH} characters",
)
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs=variables,
outputs={
'output': result['result']
}
status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=variables, outputs={"output": result["result"]}
)
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: TemplateTransformNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: TemplateTransformNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -92,5 +71,6 @@ class TemplateTransformNode(BaseNode):
:return:
"""
return {
node_id + '.' + variable_selector.variable: variable_selector.value_selector for variable_selector in node_data.variables
node_id + "." + variable_selector.variable: variable_selector.value_selector
for variable_selector in node_data.variables
}

View file

@ -8,46 +8,47 @@ from core.workflow.entities.base_node_data_entities import BaseNodeData
class ToolEntity(BaseModel):
provider_id: str
provider_type: Literal['builtin', 'api', 'workflow']
provider_name: str # redundancy
provider_type: Literal["builtin", "api", "workflow"]
provider_name: str # redundancy
tool_name: str
tool_label: str # redundancy
tool_label: str # redundancy
tool_configurations: dict[str, Any]
@field_validator('tool_configurations', mode='before')
@field_validator("tool_configurations", mode="before")
@classmethod
def validate_tool_configurations(cls, value, values: ValidationInfo):
if not isinstance(value, dict):
raise ValueError('tool_configurations must be a dictionary')
for key in values.data.get('tool_configurations', {}).keys():
value = values.data.get('tool_configurations', {}).get(key)
raise ValueError("tool_configurations must be a dictionary")
for key in values.data.get("tool_configurations", {}).keys():
value = values.data.get("tool_configurations", {}).get(key)
if not isinstance(value, str | int | float | bool):
raise ValueError(f'{key} must be a string')
raise ValueError(f"{key} must be a string")
return value
class ToolNodeData(BaseNodeData, ToolEntity):
class ToolInput(BaseModel):
# TODO: check this type
value: Union[Any, list[str]]
type: Literal['mixed', 'variable', 'constant']
type: Literal["mixed", "variable", "constant"]
@field_validator('type', mode='before')
@field_validator("type", mode="before")
@classmethod
def check_type(cls, value, validation_info: ValidationInfo):
typ = value
value = validation_info.data.get('value')
if typ == 'mixed' and not isinstance(value, str):
raise ValueError('value must be a string')
elif typ == 'variable':
value = validation_info.data.get("value")
if typ == "mixed" and not isinstance(value, str):
raise ValueError("value must be a string")
elif typ == "variable":
if not isinstance(value, list):
raise ValueError('value must be a list')
raise ValueError("value must be a list")
for val in value:
if not isinstance(val, str):
raise ValueError('value must be a list of strings')
elif typ == 'constant' and not isinstance(value, str | int | float | bool):
raise ValueError('value must be a string, int, float, or bool')
raise ValueError("value must be a list of strings")
elif typ == "constant" and not isinstance(value, str | int | float | bool):
raise ValueError("value must be a string, int, float, or bool")
return typ
"""

View file

@ -34,10 +34,7 @@ class ToolNode(BaseNode):
node_data = cast(ToolNodeData, self.node_data)
# fetch tool icon
tool_info = {
'provider_type': node_data.provider_type,
'provider_id': node_data.provider_id
}
tool_info = {"provider_type": node_data.provider_type, "provider_id": node_data.provider_id}
# get tool runtime
try:
@ -48,16 +45,21 @@ class ToolNode(BaseNode):
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs={},
metadata={
NodeRunMetadataKey.TOOL_INFO: tool_info
},
error=f'Failed to get tool runtime: {str(e)}'
metadata={NodeRunMetadataKey.TOOL_INFO: tool_info},
error=f"Failed to get tool runtime: {str(e)}",
)
# get parameters
tool_parameters = tool_runtime.get_runtime_parameters() or []
parameters = self._generate_parameters(tool_parameters=tool_parameters, variable_pool=self.graph_runtime_state.variable_pool, node_data=node_data)
parameters_for_log = self._generate_parameters(tool_parameters=tool_parameters, variable_pool=self.graph_runtime_state.variable_pool, node_data=node_data, for_log=True)
parameters = self._generate_parameters(
tool_parameters=tool_parameters, variable_pool=self.graph_runtime_state.variable_pool, node_data=node_data
)
parameters_for_log = self._generate_parameters(
tool_parameters=tool_parameters,
variable_pool=self.graph_runtime_state.variable_pool,
node_data=node_data,
for_log=True,
)
try:
messages = ToolEngine.workflow_invoke(
@ -72,10 +74,8 @@ class ToolNode(BaseNode):
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs=parameters_for_log,
metadata={
NodeRunMetadataKey.TOOL_INFO: tool_info
},
error=f'Failed to invoke tool: {str(e)}',
metadata={NodeRunMetadataKey.TOOL_INFO: tool_info},
error=f"Failed to invoke tool: {str(e)}",
)
# convert tool messages
@ -83,15 +83,9 @@ class ToolNode(BaseNode):
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs={
'text': plain_text,
'files': files,
'json': json
},
metadata={
NodeRunMetadataKey.TOOL_INFO: tool_info
},
inputs=parameters_for_log
outputs={"text": plain_text, "files": files, "json": json},
metadata={NodeRunMetadataKey.TOOL_INFO: tool_info},
inputs=parameters_for_log,
)
def _generate_parameters(
@ -123,12 +117,10 @@ class ToolNode(BaseNode):
result[parameter_name] = None
continue
if parameter.type == ToolParameter.ToolParameterType.FILE:
result[parameter_name] = [
v.to_dict() for v in self._fetch_files(variable_pool)
]
result[parameter_name] = [v.to_dict() for v in self._fetch_files(variable_pool)]
else:
tool_input = node_data.tool_parameters[parameter_name]
if tool_input.type == 'variable':
if tool_input.type == "variable":
# TODO: check if the variable exists in the variable pool
parameter_value = variable_pool.get(tool_input.value).value
else:
@ -142,12 +134,11 @@ class ToolNode(BaseNode):
return result
def _fetch_files(self, variable_pool: VariablePool) -> list[FileVar]:
variable = variable_pool.get(['sys', SystemVariableKey.FILES.value])
variable = variable_pool.get(["sys", SystemVariableKey.FILES.value])
assert isinstance(variable, ArrayAnyVariable | ArrayAnySegment)
return list(variable.value) if variable else []
def _convert_tool_messages(self, messages: list[ToolInvokeMessage])\
-> tuple[str, list[FileVar], list[dict]]:
def _convert_tool_messages(self, messages: list[ToolInvokeMessage]) -> tuple[str, list[FileVar], list[dict]]:
"""
Convert ToolInvokeMessages into tuple[plain_text, files]
"""
@ -172,38 +163,44 @@ class ToolNode(BaseNode):
result = []
for response in tool_response:
if response.type == ToolInvokeMessage.MessageType.IMAGE_LINK or \
response.type == ToolInvokeMessage.MessageType.IMAGE:
if (
response.type == ToolInvokeMessage.MessageType.IMAGE_LINK
or response.type == ToolInvokeMessage.MessageType.IMAGE
):
url = response.message
ext = path.splitext(url)[1]
mimetype = response.meta.get('mime_type', 'image/jpeg')
filename = response.save_as or url.split('/')[-1]
transfer_method = response.meta.get('transfer_method', FileTransferMethod.TOOL_FILE)
mimetype = response.meta.get("mime_type", "image/jpeg")
filename = response.save_as or url.split("/")[-1]
transfer_method = response.meta.get("transfer_method", FileTransferMethod.TOOL_FILE)
# get tool file id
tool_file_id = url.split('/')[-1].split('.')[0]
result.append(FileVar(
tenant_id=self.tenant_id,
type=FileType.IMAGE,
transfer_method=transfer_method,
url=url,
related_id=tool_file_id,
filename=filename,
extension=ext,
mime_type=mimetype,
))
tool_file_id = url.split("/")[-1].split(".")[0]
result.append(
FileVar(
tenant_id=self.tenant_id,
type=FileType.IMAGE,
transfer_method=transfer_method,
url=url,
related_id=tool_file_id,
filename=filename,
extension=ext,
mime_type=mimetype,
)
)
elif response.type == ToolInvokeMessage.MessageType.BLOB:
# get tool file id
tool_file_id = response.message.split('/')[-1].split('.')[0]
result.append(FileVar(
tenant_id=self.tenant_id,
type=FileType.IMAGE,
transfer_method=FileTransferMethod.TOOL_FILE,
related_id=tool_file_id,
filename=response.save_as,
extension=path.splitext(response.save_as)[1],
mime_type=response.meta.get('mime_type', 'application/octet-stream'),
))
tool_file_id = response.message.split("/")[-1].split(".")[0]
result.append(
FileVar(
tenant_id=self.tenant_id,
type=FileType.IMAGE,
transfer_method=FileTransferMethod.TOOL_FILE,
related_id=tool_file_id,
filename=response.save_as,
extension=path.splitext(response.save_as)[1],
mime_type=response.meta.get("mime_type", "application/octet-stream"),
)
)
elif response.type == ToolInvokeMessage.MessageType.LINK:
pass # TODO:
@ -213,21 +210,23 @@ class ToolNode(BaseNode):
"""
Extract tool response text
"""
return '\n'.join([
f'{message.message}' if message.type == ToolInvokeMessage.MessageType.TEXT else
f'Link: {message.message}' if message.type == ToolInvokeMessage.MessageType.LINK else ''
for message in tool_response
])
return "\n".join(
[
f"{message.message}"
if message.type == ToolInvokeMessage.MessageType.TEXT
else f"Link: {message.message}"
if message.type == ToolInvokeMessage.MessageType.LINK
else ""
for message in tool_response
]
)
def _extract_tool_response_json(self, tool_response: list[ToolInvokeMessage]) -> list[dict]:
return [message.message for message in tool_response if message.type == ToolInvokeMessage.MessageType.JSON]
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: ToolNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: ToolNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
@ -239,17 +238,15 @@ class ToolNode(BaseNode):
result = {}
for parameter_name in node_data.tool_parameters:
input = node_data.tool_parameters[parameter_name]
if input.type == 'mixed':
if input.type == "mixed":
selectors = VariableTemplateParser(input.value).extract_variable_selectors()
for selector in selectors:
result[selector.variable] = selector.value_selector
elif input.type == 'variable':
elif input.type == "variable":
result[parameter_name] = input.value
elif input.type == 'constant':
elif input.type == "constant":
pass
result = {
node_id + '.' + key: value for key, value in result.items()
}
result = {node_id + "." + key: value for key, value in result.items()}
return result

View file

@ -1,5 +1,3 @@
from typing import Literal, Optional
from pydantic import BaseModel
@ -11,23 +9,27 @@ class AdvancedSettings(BaseModel):
"""
Advanced setting.
"""
group_enabled: bool
class Group(BaseModel):
"""
Group.
"""
output_type: Literal['string', 'number', 'object', 'array[string]', 'array[number]', 'array[object]']
output_type: Literal["string", "number", "object", "array[string]", "array[number]", "array[object]"]
variables: list[list[str]]
group_name: str
groups: list[Group]
class VariableAssignerNodeData(BaseNodeData):
"""
Knowledge retrieval Node Data.
"""
type: str = 'variable-assigner'
type: str = "variable-assigner"
output_type: str
variables: list[list[str]]
advanced_settings: Optional[AdvancedSettings] = None

View file

@ -21,13 +21,9 @@ class VariableAggregatorNode(BaseNode):
for selector in node_data.variables:
variable = self.graph_runtime_state.variable_pool.get_any(selector)
if variable is not None:
outputs = {
"output": variable
}
outputs = {"output": variable}
inputs = {
'.'.join(selector[1:]): variable
}
inputs = {".".join(selector[1:]): variable}
break
else:
for group in node_data.advanced_settings.groups:
@ -35,24 +31,15 @@ class VariableAggregatorNode(BaseNode):
variable = self.graph_runtime_state.variable_pool.get_any(selector)
if variable is not None:
outputs[group.group_name] = {
'output': variable
}
inputs['.'.join(selector[1:])] = variable
outputs[group.group_name] = {"output": variable}
inputs[".".join(selector[1:])] = variable
break
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs=outputs,
inputs=inputs
)
return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs=outputs, inputs=inputs)
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls,
graph_config: Mapping[str, Any],
node_id: str,
node_data: VariableAssignerNodeData
cls, graph_config: Mapping[str, Any], node_id: str, node_data: VariableAssignerNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping

View file

@ -2,7 +2,7 @@ from .node import VariableAssignerNode
from .node_data import VariableAssignerData, WriteMode
__all__ = [
'VariableAssignerNode',
'VariableAssignerData',
'WriteMode',
"VariableAssignerNode",
"VariableAssignerData",
"WriteMode",
]

View file

@ -24,43 +24,43 @@ class VariableAssignerNode(BaseNode):
# Should be String, Number, Object, ArrayString, ArrayNumber, ArrayObject
original_variable = self.graph_runtime_state.variable_pool.get(data.assigned_variable_selector)
if not isinstance(original_variable, Variable):
raise VariableAssignerNodeError('assigned variable not found')
raise VariableAssignerNodeError("assigned variable not found")
match data.write_mode:
case WriteMode.OVER_WRITE:
income_value = self.graph_runtime_state.variable_pool.get(data.input_variable_selector)
if not income_value:
raise VariableAssignerNodeError('input value not found')
updated_variable = original_variable.model_copy(update={'value': income_value.value})
raise VariableAssignerNodeError("input value not found")
updated_variable = original_variable.model_copy(update={"value": income_value.value})
case WriteMode.APPEND:
income_value = self.graph_runtime_state.variable_pool.get(data.input_variable_selector)
if not income_value:
raise VariableAssignerNodeError('input value not found')
raise VariableAssignerNodeError("input value not found")
updated_value = original_variable.value + [income_value.value]
updated_variable = original_variable.model_copy(update={'value': updated_value})
updated_variable = original_variable.model_copy(update={"value": updated_value})
case WriteMode.CLEAR:
income_value = get_zero_value(original_variable.value_type)
updated_variable = original_variable.model_copy(update={'value': income_value.to_object()})
updated_variable = original_variable.model_copy(update={"value": income_value.to_object()})
case _:
raise VariableAssignerNodeError(f'unsupported write mode: {data.write_mode}')
raise VariableAssignerNodeError(f"unsupported write mode: {data.write_mode}")
# Over write the variable.
self.graph_runtime_state.variable_pool.add(data.assigned_variable_selector, updated_variable)
# TODO: Move database operation to the pipeline.
# Update conversation variable.
conversation_id = self.graph_runtime_state.variable_pool.get(['sys', 'conversation_id'])
conversation_id = self.graph_runtime_state.variable_pool.get(["sys", "conversation_id"])
if not conversation_id:
raise VariableAssignerNodeError('conversation_id not found')
raise VariableAssignerNodeError("conversation_id not found")
update_conversation_variable(conversation_id=conversation_id.text, variable=updated_variable)
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs={
'value': income_value.to_object(),
"value": income_value.to_object(),
},
)
@ -72,7 +72,7 @@ def update_conversation_variable(conversation_id: str, variable: Variable):
with Session(db.engine) as session:
row = session.scalar(stmt)
if not row:
raise VariableAssignerNodeError('conversation variable not found in the database')
raise VariableAssignerNodeError("conversation variable not found in the database")
row.data = variable.model_dump_json()
session.commit()
@ -84,8 +84,8 @@ def get_zero_value(t: SegmentType):
case SegmentType.OBJECT:
return factory.build_segment({})
case SegmentType.STRING:
return factory.build_segment('')
return factory.build_segment("")
case SegmentType.NUMBER:
return factory.build_segment(0)
case _:
raise VariableAssignerNodeError(f'unsupported variable type: {t}')
raise VariableAssignerNodeError(f"unsupported variable type: {t}")

View file

@ -6,14 +6,14 @@ from core.workflow.entities.base_node_data_entities import BaseNodeData
class WriteMode(str, Enum):
OVER_WRITE = 'over-write'
APPEND = 'append'
CLEAR = 'clear'
OVER_WRITE = "over-write"
APPEND = "append"
CLEAR = "clear"
class VariableAssignerData(BaseNodeData):
title: str = 'Variable Assigner'
desc: Optional[str] = 'Assign a value to a variable'
title: str = "Variable Assigner"
desc: Optional[str] = "Assign a value to a variable"
assigned_variable_selector: Sequence[str]
write_mode: WriteMode
input_variable_selector: Sequence[str]

View file

@ -7,11 +7,26 @@ class Condition(BaseModel):
"""
Condition entity
"""
variable_selector: list[str]
comparison_operator: Literal[
# for string or array
"contains", "not contains", "start with", "end with", "is", "is not", "empty", "not empty",
"contains",
"not contains",
"start with",
"end with",
"is",
"is not",
"empty",
"not empty",
# for number
"=", "", ">", "<", "", "", "null", "not null"
"=",
"",
">",
"<",
"",
"",
"null",
"not null",
]
value: Optional[str] = None

View file

@ -15,9 +15,7 @@ class ConditionProcessor:
index = 0
for condition in conditions:
index += 1
actual_value = variable_pool.get_any(
condition.variable_selector
)
actual_value = variable_pool.get_any(condition.variable_selector)
expected_value = None
if condition.value is not None:
@ -25,9 +23,7 @@ class ConditionProcessor:
variable_selectors = variable_template_parser.extract_variable_selectors()
if variable_selectors:
for variable_selector in variable_selectors:
value = variable_pool.get_any(
variable_selector.value_selector
)
value = variable_pool.get_any(variable_selector.value_selector)
expected_value = variable_template_parser.format({variable_selector.variable: value})
if expected_value is None:
@ -40,7 +36,7 @@ class ConditionProcessor:
{
"actual_value": actual_value,
"expected_value": expected_value,
"comparison_operator": comparison_operator
"comparison_operator": comparison_operator,
}
)
@ -50,10 +46,10 @@ class ConditionProcessor:
return input_conditions, group_result
def evaluate_condition(
self,
actual_value: Optional[str | int | float | dict[Any, Any] | list[Any] | FileVar | None],
comparison_operator: str,
expected_value: Optional[str] = None
self,
actual_value: Optional[str | int | float | dict[Any, Any] | list[Any] | FileVar | None],
comparison_operator: str,
expected_value: Optional[str] = None,
) -> bool:
"""
Evaluate condition
@ -109,7 +105,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, str | list):
raise ValueError('Invalid actual value type: string or array')
raise ValueError("Invalid actual value type: string or array")
if expected_value not in actual_value:
return False
@ -126,7 +122,7 @@ class ConditionProcessor:
return True
if not isinstance(actual_value, str | list):
raise ValueError('Invalid actual value type: string or array')
raise ValueError("Invalid actual value type: string or array")
if expected_value in actual_value:
return False
@ -143,7 +139,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, str):
raise ValueError('Invalid actual value type: string')
raise ValueError("Invalid actual value type: string")
if not actual_value.startswith(expected_value):
return False
@ -160,7 +156,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, str):
raise ValueError('Invalid actual value type: string')
raise ValueError("Invalid actual value type: string")
if not actual_value.endswith(expected_value):
return False
@ -177,7 +173,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, str):
raise ValueError('Invalid actual value type: string')
raise ValueError("Invalid actual value type: string")
if actual_value != expected_value:
return False
@ -194,7 +190,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, str):
raise ValueError('Invalid actual value type: string')
raise ValueError("Invalid actual value type: string")
if actual_value == expected_value:
return False
@ -231,7 +227,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, int | float):
raise ValueError('Invalid actual value type: number')
raise ValueError("Invalid actual value type: number")
if isinstance(actual_value, int):
expected_value = int(expected_value)
@ -253,7 +249,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, int | float):
raise ValueError('Invalid actual value type: number')
raise ValueError("Invalid actual value type: number")
if isinstance(actual_value, int):
expected_value = int(expected_value)
@ -275,7 +271,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, int | float):
raise ValueError('Invalid actual value type: number')
raise ValueError("Invalid actual value type: number")
if isinstance(actual_value, int):
expected_value = int(expected_value)
@ -297,7 +293,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, int | float):
raise ValueError('Invalid actual value type: number')
raise ValueError("Invalid actual value type: number")
if isinstance(actual_value, int):
expected_value = int(expected_value)
@ -308,8 +304,9 @@ class ConditionProcessor:
return False
return True
def _assert_greater_than_or_equal(self, actual_value: Optional[int | float],
expected_value: str | int | float) -> bool:
def _assert_greater_than_or_equal(
self, actual_value: Optional[int | float], expected_value: str | int | float
) -> bool:
"""
Assert greater than or equal
:param actual_value: actual value
@ -320,7 +317,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, int | float):
raise ValueError('Invalid actual value type: number')
raise ValueError("Invalid actual value type: number")
if isinstance(actual_value, int):
expected_value = int(expected_value)
@ -331,8 +328,9 @@ class ConditionProcessor:
return False
return True
def _assert_less_than_or_equal(self, actual_value: Optional[int | float],
expected_value: str | int | float) -> bool:
def _assert_less_than_or_equal(
self, actual_value: Optional[int | float], expected_value: str | int | float
) -> bool:
"""
Assert less than or equal
:param actual_value: actual value
@ -343,7 +341,7 @@ class ConditionProcessor:
return False
if not isinstance(actual_value, int | float):
raise ValueError('Invalid actual value type: number')
raise ValueError("Invalid actual value type: number")
if isinstance(actual_value, int):
expected_value = int(expected_value)

View file

@ -5,7 +5,7 @@ from typing import Any
from core.workflow.entities.variable_entities import VariableSelector
from core.workflow.entities.variable_pool import VariablePool
REGEX = re.compile(r'\{\{(#[a-zA-Z0-9_]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}')
REGEX = re.compile(r"\{\{(#[a-zA-Z0-9_]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}")
def parse_mixed_template(*, template: str, variable_pool: VariablePool) -> str:
@ -20,7 +20,7 @@ def parse_mixed_template(*, template: str, variable_pool: VariablePool) -> str:
# e.g. ('#node_id.query.name#', ['node_id', 'query', 'name'])
key_selectors = filter(
lambda t: len(t[1]) >= 2,
((key, selector.replace('#', '').split('.')) for key, selector in zip(variable_keys, variable_keys)),
((key, selector.replace("#", "").split(".")) for key, selector in zip(variable_keys, variable_keys)),
)
inputs = {key: variable_pool.get_any(selector) for key, selector in key_selectors}
@ -29,13 +29,13 @@ def parse_mixed_template(*, template: str, variable_pool: VariablePool) -> str:
# return original matched string if key not found
value = inputs.get(key, match.group(0))
if value is None:
value = ''
value = ""
value = str(value)
# remove template variables if required
return re.sub(REGEX, r'{\1}', value)
return re.sub(REGEX, r"{\1}", value)
result = re.sub(REGEX, replacer, template)
result = re.sub(r'<\|.*?\|>', '', result)
result = re.sub(r"<\|.*?\|>", "", result)
return result
@ -101,8 +101,8 @@ class VariableTemplateParser:
"""
variable_selectors = []
for variable_key in self.variable_keys:
remove_hash = variable_key.replace('#', '')
split_result = remove_hash.split('.')
remove_hash = variable_key.replace("#", "")
split_result = remove_hash.split(".")
if len(split_result) < 2:
continue
@ -127,7 +127,7 @@ class VariableTemplateParser:
value = inputs.get(key, match.group(0)) # return original matched string if key not found
if value is None:
value = ''
value = ""
# convert the value to string
if isinstance(value, list | dict | bool | int | float):
value = str(value)
@ -136,7 +136,7 @@ class VariableTemplateParser:
return VariableTemplateParser.remove_template_variables(value)
prompt = re.sub(REGEX, replacer, self.template)
return re.sub(r'<\|.*?\|>', '', prompt)
return re.sub(r"<\|.*?\|>", "", prompt)
@classmethod
def remove_template_variables(cls, text: str):
@ -149,4 +149,4 @@ class VariableTemplateParser:
Returns:
The text with template variables removed.
"""
return re.sub(REGEX, r'{\1}', text)
return re.sub(REGEX, r"{\1}", text)

View file

@ -33,19 +33,19 @@ logger = logging.getLogger(__name__)
class WorkflowEntry:
def __init__(
self,
tenant_id: str,
app_id: str,
workflow_id: str,
workflow_type: WorkflowType,
graph_config: Mapping[str, Any],
graph: Graph,
user_id: str,
user_from: UserFrom,
invoke_from: InvokeFrom,
call_depth: int,
variable_pool: VariablePool,
thread_pool_id: Optional[str] = None
self,
tenant_id: str,
app_id: str,
workflow_id: str,
workflow_type: WorkflowType,
graph_config: Mapping[str, Any],
graph: Graph,
user_id: str,
user_from: UserFrom,
invoke_from: InvokeFrom,
call_depth: int,
variable_pool: VariablePool,
thread_pool_id: Optional[str] = None,
) -> None:
"""
Init workflow entry
@ -65,7 +65,7 @@ class WorkflowEntry:
# check call depth
workflow_call_max_depth = dify_config.WORKFLOW_CALL_MAX_DEPTH
if call_depth > workflow_call_max_depth:
raise ValueError('Max workflow call depth {} reached.'.format(workflow_call_max_depth))
raise ValueError("Max workflow call depth {} reached.".format(workflow_call_max_depth))
# init workflow run state
self.graph_engine = GraphEngine(
@ -82,13 +82,13 @@ class WorkflowEntry:
variable_pool=variable_pool,
max_execution_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS,
max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME,
thread_pool_id=thread_pool_id
thread_pool_id=thread_pool_id,
)
def run(
self,
*,
callbacks: Sequence[WorkflowCallback],
self,
*,
callbacks: Sequence[WorkflowCallback],
) -> Generator[GraphEngineEvent, None, None]:
"""
:param callbacks: workflow callbacks
@ -101,9 +101,7 @@ class WorkflowEntry:
for event in generator:
if callbacks:
for callback in callbacks:
callback.on_event(
event=event
)
callback.on_event(event=event)
yield event
except GenerateTaskStoppedException:
pass
@ -111,20 +109,12 @@ class WorkflowEntry:
logger.exception("Unknown Error when workflow entry running")
if callbacks:
for callback in callbacks:
callback.on_event(
event=GraphRunFailedEvent(
error=str(e)
)
)
callback.on_event(event=GraphRunFailedEvent(error=str(e)))
return
@classmethod
def single_step_run(
cls,
workflow: Workflow,
node_id: str,
user_id: str,
user_inputs: dict
cls, workflow: Workflow, node_id: str, user_id: str, user_inputs: dict
) -> tuple[BaseNode, Generator[RunEvent | InNodeEvent, None, None]]:
"""
Single step run workflow node
@ -137,30 +127,30 @@ class WorkflowEntry:
# fetch node info from workflow graph
graph = workflow.graph_dict
if not graph:
raise ValueError('workflow graph not found')
raise ValueError("workflow graph not found")
nodes = graph.get('nodes')
nodes = graph.get("nodes")
if not nodes:
raise ValueError('nodes not found in workflow graph')
raise ValueError("nodes not found in workflow graph")
# fetch node config from node id
node_config = None
for node in nodes:
if node.get('id') == node_id:
if node.get("id") == node_id:
node_config = node
break
if not node_config:
raise ValueError('node id not found in workflow graph')
raise ValueError("node id not found in workflow graph")
# Get node class
node_type = NodeType.value_of(node_config.get('data', {}).get('type'))
node_type = NodeType.value_of(node_config.get("data", {}).get("type"))
node_cls = node_classes.get(node_type)
node_cls = cast(type[BaseNode], node_cls)
if not node_cls:
raise ValueError(f'Node class not found for node type {node_type}')
raise ValueError(f"Node class not found for node type {node_type}")
# init variable pool
variable_pool = VariablePool(
system_variables={},
@ -169,9 +159,7 @@ class WorkflowEntry:
)
# init graph
graph = Graph.init(
graph_config=workflow.graph_dict
)
graph = Graph.init(graph_config=workflow.graph_dict)
# init workflow run state
node_instance: BaseNode = node_cls(
@ -186,21 +174,17 @@ class WorkflowEntry:
user_id=user_id,
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0
call_depth=0,
),
graph=graph,
graph_runtime_state=GraphRuntimeState(
variable_pool=variable_pool,
start_at=time.perf_counter()
)
graph_runtime_state=GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()),
)
try:
# variable selector to variable mapping
try:
variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
graph_config=workflow.graph_dict,
config=node_config
graph_config=workflow.graph_dict, config=node_config
)
except NotImplementedError:
variable_mapping = {}
@ -211,7 +195,7 @@ class WorkflowEntry:
variable_pool=variable_pool,
tenant_id=workflow.tenant_id,
node_type=node_type,
node_data=node_instance.node_data
node_data=node_instance.node_data,
)
# run node
@ -219,10 +203,7 @@ class WorkflowEntry:
return node_instance, generator
except Exception as e:
raise WorkflowNodeRunFailedError(
node_instance=node_instance,
error=str(e)
)
raise WorkflowNodeRunFailedError(node_instance=node_instance, error=str(e))
@classmethod
def handle_special_values(cls, value: Optional[Mapping[str, Any]]) -> Optional[dict]:
@ -259,21 +240,20 @@ class WorkflowEntry:
variable_pool: VariablePool,
tenant_id: str,
node_type: NodeType,
node_data: BaseNodeData
node_data: BaseNodeData,
) -> None:
for node_variable, variable_selector in variable_mapping.items():
# fetch node id and variable key from node_variable
node_variable_list = node_variable.split('.')
node_variable_list = node_variable.split(".")
if len(node_variable_list) < 1:
raise ValueError(f'Invalid node variable {node_variable}')
node_variable_key = '.'.join(node_variable_list[1:])
raise ValueError(f"Invalid node variable {node_variable}")
if (
node_variable_key not in user_inputs
and node_variable not in user_inputs
) and not variable_pool.get(variable_selector):
raise ValueError(f'Variable key {node_variable} not found in user inputs.')
node_variable_key = ".".join(node_variable_list[1:])
if (node_variable_key not in user_inputs and node_variable not in user_inputs) and not variable_pool.get(
variable_selector
):
raise ValueError(f"Variable key {node_variable} not found in user inputs.")
# fetch variable node id from variable selector
variable_node_id = variable_selector[0]
@ -294,16 +274,17 @@ class WorkflowEntry:
detail = node_data.vision.configs.detail if node_data.vision.configs else None
for item in input_value:
if isinstance(item, dict) and 'type' in item and item['type'] == 'image':
transfer_method = FileTransferMethod.value_of(item.get('transfer_method'))
if isinstance(item, dict) and "type" in item and item["type"] == "image":
transfer_method = FileTransferMethod.value_of(item.get("transfer_method"))
file = FileVar(
tenant_id=tenant_id,
type=FileType.IMAGE,
transfer_method=transfer_method,
url=item.get('url') if transfer_method == FileTransferMethod.REMOTE_URL else None,
related_id=item.get(
'upload_file_id') if transfer_method == FileTransferMethod.LOCAL_FILE else None,
extra_config=FileExtraConfig(image_config={'detail': detail} if detail else None),
url=item.get("url") if transfer_method == FileTransferMethod.REMOTE_URL else None,
related_id=item.get("upload_file_id")
if transfer_method == FileTransferMethod.LOCAL_FILE
else None,
extra_config=FileExtraConfig(image_config={"detail": detail} if detail else None),
)
new_value.append(file)