Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions ui/src/workflow/common/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const end_nodes_dict = {
[WorkflowMode.Application]: end_nodes,
[WorkflowMode.Knowledge]: [WorkflowType.KnowledgeWriteNode],
[WorkflowMode.ApplicationLoop]: loop_end_nodes,
[WorkflowMode.KnowledgeLoop]: loop_end_nodes,
[WorkflowMode.KnowledgeLoop]: [...loop_end_nodes, WorkflowType.KnowledgeWriteNode],
}

export class WorkFlowInstance {
Expand Down Expand Up @@ -218,9 +218,11 @@ export class WorkFlowInstance {

export class KnowledgeWorkFlowInstance extends WorkFlowInstance {
is_valid_start_node() {
const start_node_list = this.nodes.filter(
(item) => item.properties.kind === WorkflowKind.DataSource,
)
const start_node_list =
this.workflowModel == WorkflowMode.Knowledge
? this.nodes.filter((item) => item.properties.kind === WorkflowKind.DataSource)
: this.nodes.filter((item) => item.type === WorkflowType.LoopStartNode)

if (start_node_list.length == 0) {
throw t('workflow.validate.startNodeRequired')
}
Expand All @@ -239,9 +241,7 @@ export class KnowledgeWorkFlowInstance extends WorkFlowInstance {

is_valid_work_flow() {
this.workFlowNodes = []
const start_node_list = this.nodes.filter(
(item) => item.properties.kind === WorkflowKind.DataSource,
)
const start_node_list = this.get_start_nodes()
start_node_list.forEach((n) => {
this._is_valid_work_flow(n)
})
Expand All @@ -250,10 +250,12 @@ export class KnowledgeWorkFlowInstance extends WorkFlowInstance {
.filter(
(node: any) =>
node.id !== WorkflowType.KnowledgeBase &&
node.type !== WorkflowType.LoopStartNode &&
node.properties.kind !== WorkflowKind.DataSource,
)
.filter((node) => !this.workFlowNodes.includes(node))
if (notInWorkFlowNodes.length > 0) {
console.log('ss')
throw `${t('workflow.validate.notInWorkFlowNode')}:${notInWorkFlowNodes.map((node) => node.properties.stepName).join(',')}`
}
this.workFlowNodes = []
Expand All @@ -263,6 +265,7 @@ export class KnowledgeWorkFlowInstance extends WorkFlowInstance {
for (const node of this.nodes) {
if (
node.type !== WorkflowType.KnowledgeBase &&
node.type !== WorkflowType.LoopStartNode &&
node.properties.kind !== WorkflowKind.DataSource
) {
if (!this.edges.some((edge) => edge.targetNodeId === node.id)) {
Expand All @@ -271,4 +274,89 @@ export class KnowledgeWorkFlowInstance extends WorkFlowInstance {
}
}
}
get_start_nodes() {
if (this.workflowModel == WorkflowMode.Knowledge) {
return this.nodes.filter((item) => item.properties.kind === WorkflowKind.DataSource)
} else {
return this.nodes.filter((item) => item.type === WorkflowType.LoopStartNode)
}
}
get_end_nodes() {
const start_node_list = this.get_start_nodes()
return start_node_list.flatMap((n) => {
return this._get_end_nodes(n, [])
})
}
_get_end_nodes(startNode: any, value: Array<any>) {
const next = this.get_next_nodes(startNode)
if (next.length == 0) {
value.push(startNode)
} else {
next.forEach((n) => {
this._get_end_nodes(n, value)
})
}
return value
}

/**
* 获取流程下一个节点列表
* @param node 节点
* @returns 节点列表
*/
get_next_nodes(node: any) {
const edge_list = this.edges.filter((edge) => edge.sourceNodeId == node.id)
const node_list = edge_list
.map((edge) => this.nodes.filter((node) => node.id == edge.targetNodeId))
.reduce((x, y) => [...x, ...y], [])

return node_list
}

/**
* 校验节点
* @param node 节点
*/
is_valid_node(node: any) {
if (node.properties.status && node.properties.status === 500) {
throw `${node.properties.stepName} ${t('workflow.validate.nodeUnavailable')}`
}
if (node.type === WorkflowType.Condition) {
const branch_list = node.properties.node_data.branch
for (const branch of branch_list) {
const source_anchor_id = `${node.id}_${branch.id}_right`
const edge_list = this.edges.filter((edge) => edge.sourceAnchorId == source_anchor_id)
if (edge_list.length == 0) {
throw `${node.properties.stepName} ${t('workflow.validate.needConnect1')}${branch.type}${t('workflow.validate.needConnect2')}`
}
}
} else {
const edge_list = this.edges.filter((edge) => edge.sourceNodeId == node.id)
const end = end_nodes_dict[this.workflowModel]
if (this.workflowModel == WorkflowMode.KnowledgeLoop) {
if (edge_list.length == 0 && !end.includes(node.type)) {
throw `${node.properties.stepName} ${t('workflow.validate.cannotEndNode')}`
}
return
}
if (edge_list.length == 0 && !end.includes(node.type)) {
if (node.type == WorkflowType.LoopNode) {
if (node.properties.node_data.loop_body) {
const end_nodes = new KnowledgeWorkFlowInstance(
node.properties.node_data.loop_body,
WorkflowMode.KnowledgeLoop,
).get_end_nodes()
if (!end_nodes.every((n) => end.includes(n.type))) {
throw `${node.properties.stepName} ${t('workflow.validate.cannotEndNode')}`
}
}
} else {
throw `${node.properties.stepName} ${t('workflow.validate.cannotEndNode')}`
}
}
}
if (node.properties.status && node.properties.status !== 200) {
throw `${node.properties.stepName} ${t('workflow.validate.nodeUnavailable')}`
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reviewing the provided code, here are some observations and suggested improvements:

  1. Duplicated Logic: The is_valid_work_flow method checks both data source nodes and loop start nodes depending on whether the workflow mode is knowledge. This logic can be refactored into a single function to improve maintainability.

  2. Code Duplication: There are multiple occurrences of similar logic to filter nodes. Refactoring these blocks can reduce redundancy.

  3. Edge Cases Missing:

    • Ensure that all types of nodes (DataSource, LoopStartNode) are correctly handled in validation methods.
    • Handle the case where a node has no edges after validation.
  4. Logging and Error Handling: Consider adding more comprehensive logging and better error messages to help with debugging and user experience.

  5. Performance Optimization: If there's a lot of processing happening during certain functions, consider optimizing loops or using data structures that allow faster lookups.

Here's an optimized version of the relevant parts:

class WorkFlowInstance {
  constructor(nodes, edge) {}

  validate(workflowModel) {
    // Common setup for validating workflows
    this.workflowModel = workflowModel;
    this.nodes = nodes;
    this.edges = edge;

    try {
      this.is_valid_start_node();
      this.ensureOnlyDataSourcesExist(this.workflowModel);
      this.is_valid_nodes(this.workflowModel);
      this.is_valid_edges(workflowModel);

      // Add any additional validations specific to the workflow model
    } catch (error) {
      console.error(error.message);
    }
  }

  ensureOnlyDataSourcesExist(workflowModel) {
    if (workflowModel !== WorkflowMode.Knowledge || workflowModel !== WorkflowMode.Customize) {
      return true; // No need to verify data sources for other modes
    }

    const nonDataSourceNodes = this.nonDataSourceNodes.filter((node) => !end_nodes_dict[workflowModel].includes(node.properties.kind));
    if (nonDataSourceNodes.length > 0) {
      throw new Error(`${t('workflow.validate.missingDataSource')}`);
    }

    return true;
  }

  isValidNode(properties, type) {
    // Check node properties based on its type
    // You might want to use a switch statement instead
    if (properties.status === 500) {
      throw new Error(`The node "${properties.stepName}" is unavailable.`);
    }
    if ([WorkflowType.Condition].includes(type)) {
      // Handle conditions separately
      const branches = properties.node_data.branch;
      branches.forEach((branchType) => {
        if (!this.edges.some(edge => edge.sourceAnchorId.endsWith(branchType + '_right'))) {
          throw new Error(`Need to connect "${properties.stepName} (${type}) with: ${branchType}`);
        }
      });
    } else if (![...end_nodes_dict[this.workflowModel]].includes(type)) {
      if (type === WorkflowType.LoopNode && properties.node_data.loop_body) {
        const bodyEdges = new KnowledgeWorkFlowInstance(properties.node_data.loop_body, WorkflowMode.KnowledgeLoop).edges;
        if (!bodyEdges.every(edge => edge.targetNodeProperties.kind !== WorkflowKind.LoopingEnd)) {
          throw new Error(`Cannot finish Loop Node "${properties.stepName}".`);
        }
      } else {
        throw new Error(`Cannot end Workflow at node "${properties.stepName}" (${type}). End nodes should be: ${Object.keys(end_nodes_dict[this.workflowModel]).join(', ')}`);
      }
    }
    if (properties.status !== 200) {
      throw new Error(`The node "${properties.stepName}" is not available.`);
    }
  }

  private nonDataSourceNodes(): { id: string, properties }: any[] {
    return this.nodes.filter(node => ![WorkflowType.DataSource, WorkflowType.LoopStartNode].includes(node.type));
  }

  private getNodesByType(nodes, type): { id: string, properties }: any[] {
    return nodes.filter(node => node.type === type);
  }
}

This version consolidates and simplifies the logic while maintaining readability and robustness. Make sure to adjust the error messages and exception handling according to your application's needs.

7 changes: 5 additions & 2 deletions ui/src/workflow/nodes/loop-body-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Dagre from '@/workflow/plugins/dagre'
import { initDefaultShortcut } from '@/workflow/common/shortcut'
import LoopBodyContainer from '@/workflow/nodes/loop-body-node/LoopBodyContainer.vue'
import { WorkflowMode } from '@/enums/application'
import { WorkFlowInstance } from '@/workflow/common/validate'
import { WorkFlowInstance, KnowledgeWorkFlowInstance } from '@/workflow/common/validate'
import { t } from '@/locales'
import { disconnectByFlow } from '@/workflow/common/teleport'
const loop_workflow_mode = inject('loopWorkflowMode') || WorkflowMode.ApplicationLoop
Expand All @@ -21,7 +21,10 @@ const props = defineProps<{ nodeModel: any }>()
const containerRef = ref()
const LoopBodyContainerRef = ref<InstanceType<typeof LoopBodyContainer>>()
const validate = () => {
const workflow = new WorkFlowInstance(lf.value.getGraphData(), WorkflowMode.ApplicationLoop)
const workflow =
loop_workflow_mode == WorkflowMode.ApplicationLoop
? new WorkFlowInstance(lf.value.getGraphData(), WorkflowMode.ApplicationLoop)
: new KnowledgeWorkFlowInstance(lf.value.getGraphData(), WorkflowMode.KnowledgeLoop)
return Promise.all(lf.value.graphModel.nodes.map((element: any) => element?.validate?.()))
.then(() => {
const loop_node_id = props.nodeModel.properties.loop_node_id
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no major structural issues or obvious bugs in your provided code snippet. However, here are a few areas where improvements can be made for clarity and maintainability:

  1. Variable Naming: The variable knowledge_work_flow_instance could be more descriptive, maybe something like instanceForKnowledgeLoop. This will make it easier to understand its purpose.

  2. Conditional Logic Formatting: Consider using template literals for constructing the message in the console log at the end of the validate function (line 44). It would look cleaner and more readable.

Here's the updated code snippet with these suggestions:

import Dagre from '@/workflow/plugins/dagre';
import { initDefaultShortcut } from '@/workflow/common/shortcut';
import LoopBodyContainer from '@/workflow/nodes/loop-body-node/LoopBodyContainer.vue';
import { WorkflowMode } from '@/enums/application';
import { WorkFlowInstance, KnowledgeWorkFlowInstance } from '@/workflow/common/validate';
import { t } from '@/locales';
import { disconnectByFlow } from '@/workflow/common/teleport';
const loop_workflow_mode = inject('loopWorkflowMode') || WorkflowMode.ApplicationLoop;
const props = defineProps<{ nodeModel: any }>();
const containerRef = ref();
const LoopBodyContainerRef = ref<InstanceType<typeof LoopBodyContainer>>();
const validate = () => {
  const workflow =
    loop_workflow_mode === WorkflowMode.ApplicationLoop
      ? new WorkFlowInstance(lf.value.getGraphData(), WorkflowMode.ApplicationLoop)
      : new KnowledgeWorkFlowInstance(lf.value.getGraphData(), WorkflowMode.KnowledgeLoop);
  return Promise.all(lf.value.graphModel.nodes.map((element: any) => element?.validate?.()))
    .then(() => {
      const loop_node_id = props.nodeModel.properties.loop_node_id;
      
      // Use template literal for more organized string
      console.log(t("Validate completed successfully.", { node_id: loop_node_id }));
      
      // Rest of the validation logic may follow...
    })
    .catch((error) => {
      console.error(t("Validation failed:", { error_message: error.message }));
    })
    .finally(() => {});
};

These changes improve readability and maintainability within your codebase.

Expand Down
Loading