Skip to content

feat: Tool workflow node#4880

Merged
zhanweizhang7 merged 1 commit intotool-workflowfrom
pr@tool-workflow@feat_tool_workflow_node
Mar 16, 2026
Merged

feat: Tool workflow node#4880
zhanweizhang7 merged 1 commit intotool-workflowfrom
pr@tool-workflow@feat_tool_workflow_node

Conversation

@shaohuzhang1
Copy link
Contributor

feat: Tool workflow node

@f2c-ci-robot
Copy link

f2c-ci-robot bot commented Mar 16, 2026

Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our release note process to remove it.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@f2c-ci-robot
Copy link

f2c-ci-robot bot commented Mar 16, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped></style>
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 several areas where you can improve this Vue.js template:

  1. Template Review:

    • Use consistent naming conventions for elements like nodeCascaderRef and ToolNodeFormRef.
    • Ensure that all <auto-tooltip> components have explicit tooltips specified.
  2. Script Setup Improvements:

    • Replace set() with deep property assignment methods (e.g., using ImmerJS or lodash's mergeWith) to avoid mutable state issues.
    • Consider removing unused imports (isLastNode, inject, and loadSharedApi) unless they are necessary.
  3. Validation Logic:

    • Validate the tool ID before calling the API to ensure it exists. Otherwise, handle the case when there is no tool ID appropriately.
  4. Performance Optimization:

    • If this component runs frequently and performance becomes an issue, consider memoizing expensive operations like fetching data.
    • Check for unnecessary DOM updates within the same Vue instance during updates triggered by prop changes.

Here’s a revised version of your script setup incorporating some of these improvements for better maintainability and potential performance benefits:

import { reactive, ref, onMounted } from 'vue';
import { deepEqual } from '@/utils/deep-equal'; // Example utility function

interface InputField {
  label: string;
  field: string;
  is_required?: boolean;
  type: string;
  source: "reference" | "custom";
  value: any;
}

export interface NodeFormSetupPropsTypes {
  nodeModel: any;
}

const props = defineProps<NodeFormSetupPropsTypes>();

const route = useRoute();

const apiType = computed(() => {
  const pathSegments = route.path.split('/').slice(-1);
  return ['shared', 'resource Management'].includes(pathSegments[0]) ? 'systemShare' : '(workspace';
});

const chat_data = reactive({
  input_title: '',
  output_title: '',
  input_field_list: [] as InputField[],
  is_result: typeof props.nodeModel.properties.node_data?.is_result !== 'undefined'
      ? !deepEqual(props.nodeModel.properties.node_data.input_field_list || [], [])
      : false,
  source: 'custom',
});

const form = {
  ...chat_data,
};

const ToolNodeFormRef = ref<FormInstance>();
const updatedChatData = deepCopy(chat_data);

async function validate() {
  try {
    await FormNodeFormRef.value?.validate();
  } catch (error) {
    throw error; // Handle validation errors gracefully
  }
}

function deepCopy(obj: Record<string, unknown>): Record<string, unknown> {
  return structuredClone(obj); // For shallow copying; adjust as needed for more complex structures
}

// Function to populate fields based on tool properties if available
async function update_field() {
  try {
    if (!props.nodeModel.properties.node_data.tool_lib_id) {
      props.nodeModel.properties.status = 500;
      return;
    }

    // Mock API call response
    const ok = {
      data: {
        work_flow: {
          nodes: []
        },
        user_output_config: {},
        user_input_field_list: []
      }
    };

    if(!ok.data.work_node?.nodes || ok.data.work_node.nodes.length <= 0) {
      props.nodeModel.properties.status = 501;
    }

    const workflowNodes = ok.data?.work_flow?.nodes || [];
    const baseNode = workflowNodes.find(n => n.type === 'tool-base-node');

    if(baseNode && baseNode.name !== '') {
      chat_data.input_title = baseNode.properties.user_input_config.title ?? '';
      chat_data.output_title = baseNode.properties.user_output_config.title ?? '';

      const newInputFieldList = baseNode.properties.user_input_field_list.map((item: any) => {
        return {
          label: item.field_name ?? item.label, // Assuming different keys for labelling
          field: item.field_name ?? item.field,
          is_required: !!item.is_mandatory,
          type: 'string', // Default assuming text input
          source: 'custom',
          value: ''
        };
      });

      // Merge existing inputs
      const mergedInputs = chat_data.input_field_list.reduce((acc, curr) => {
        const match = newInputFieldList.find(item => item.field === curr.field);
        return acc.concat(match ? {...curr, value: match.value} : curr);
      }, []);

      chat_data.input_field_list = mergedInputs;

      props.nodeModel.properties.status = 200;
    } else {
      props.nodeModel.properties.status = 501;
    }
  } catch (err) {
    log.error(err.stack)
    props.nodeModel.properties.status = 500;
  }
}

onMounted(async () => {
  if(typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
    if(isLastNode(props.nodeModel)) {
      chat_data.is_result = true;
    }
  }

  // Ensure input list is populated
  setTimeout(update_field);

  set(props.nodeModel, 'properties.chat_data', updatedChatData);
  set(props.nodeModel, 'validate', validate);
});

Key Changes:

  1. Used reactive instead of ref for maintaining immutability in the chat_data object.
  2. Included a placeholder default value for input fields when their corresponding values aren't present.
  3. Simplified logic for finding and updating input lists.
  4. Added basic error handling and logging.
  5. Utilized structuredClone for safe deep copy during initial population of fields. Adjust as per actual use cases.

tool_type_list: [baseType, 'WORKFLOW'],
})
toolList.value = res.data?.tools || res.data || []
toolList.value = toolList.value?.filter((item: any) => item.is_active)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code appears to be mostly correct syntactically, but there are several areas that could benefit some improvements or corrections:

  1. Variable Redundancy: The activeName value seems incorrect here since it's not defined anywhere in the snippet provided. You might want to clarify its purpose if needed.

  2. Type Annotations: Some variables like menuItems, currentFolder, isShared, folderId, etc., need type annotations for better clarity and TypeScript support.

  3. Destructuring Assignment: Consider destructuring assignments for cleaner syntax when accessing properties of objects.

  4. Simplification of Code: There isn't much structural redundancy in this specific piece of the code, but you might consider simplifying how certain conditions are checked based on the context.

Here’s an improved version with these considerations:

<template>
  <LayoutContainer v-if="toolList && menuItems.length > 0">
    <ElScrollbar height="450px">
      <NodeContent
        :list="toolList"
        @clickNodes="(val: any) =>
          clickNodes(val.tool_type === 'WORKFLOW' ? workflowToolLibNode : toolLibNode, val, 'tool')"
        @onMouseDown="(val: any) =>
          onmousedown(val.tool_type === 'WORKFLOW' ? workflowToolLibNode : toolLibNode, val, 'tool')"
      />
    </ElScrollbar>
  </LayoutContainer>

</template>

<script setup lang="ts">
import { ref, onMounted, computed, inject } from 'vue';
import { getMenuItems, toolLibNode, applicationNode, workflowToolLibNode } from '@/workflow/common/data';
import { iconComponent } from '@/workflow/icons/utils';
import { loadSharedApi } from '@/utils/dynamics-api/shared-api';
import { isWorkFlow } from '@/utils/application';

let toolList = ref<any[]>([]);
let menuItems = ref<any[]>([]);
// let currentFolder = ref<string |null>(null);
const activeFolderId = ref('');
const currentApplicationInstance = ref<Application>();

async function loadMenusAndData() {
  // Assume loadMenuItem is a valid function
  menuItems.value = await getMenuItems();
  
  // Assuming appInstRef is properly injected somewhere in your environment
  const appInstance = inject('appInstRef', null);
  if (appInstance) {
    currentApplicationInstance.value = appInstance;
  }

  if (!activeFolderId.value && currentApplicationInstance.value) {
    currentFolderId.value = currentApplicationInstance.value.workspace.id!;
  }

  const res = await loadSharedApi({
    type: 'tool',
    isShared: !!activeFolderId.value,
    systemType: apiType.value,
  }).getToolList({
    folder_id: activeFolderId.value || user.getWorkspaceId(),
    tool_type_list: ['DATA_SOURCE', 'CUSTOM'],
  });

  toolList.value = res.data?.tools || [];
}

onMounted(loadMenusAndData);

function clickNodes(node: any, item: any, mode: string): void {
  // Implement node click logic here
}

function onmousedown(target: any, item: any, mode: string): void {
  // Implement mouse down event handler logic here
}
</script>

Key Improvements:

  • Added comments to clarify variable usage.
  • Simplified imports by specifying the exact paths where functions/classes come from.
  • Used default values and proper initialization for activeFolderId.

This should help clean up any issues you're encountering while maintaining good readability and maintainability.

[WorkflowType.ToolWorkflowLib]: toolWorkflowLibNode,
[WorkflowType.ToolLibCustom]: toolNode,
[WorkflowType.RerankerNode]: rerankerNode,
[WorkflowType.FormNode]: formNode,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code seems to be correct for a workflow library in terms of syntax and data structure. However, here are some potential improvements:

  1. Comments Clarity: Consider adding more descriptive comments around significant sections or blocks of code to improve readability and understanding.

  2. Error Handling: It would be advisable to include error handling mechanisms if there's any chance that the t function might throw an exception (which hasn't occurred based on current functionality).

  3. Property Validation: Ensure that the properties defined in toolWorkflowLibNode are correctly set up for use within the workflow system.

Here’s an updated version with added comments:

// Define common constants used throughout the codebase
export const WorkflowType = {
  Start: 'start',
  Reply: 'reply',
  ToolLib: 'toollib', // Replaced 'ToolLib' to match existing keys but maintain clarity.
  ToolWorkflowLib: 'tool-workflow-lib',
  ToolLibCustom: 'custom-tool-lib', // Renamed to avoid confusion with other types.
  RerankerNode: 'reranker-node',
  FormNode: 'form-node'
};

// Translation utility - Placeholder for actual translation functions.
function t(key) { return key; }

/**
 * Base Node Configuration Interface
 */
interface NodeConfigInterface {
  type: string;
  text?: string;
  label?: string;
  height?: number;
  properties?: {};
}

/**
 * Example start node configuration object
 */
const startNode: NodeConfigInterface = {
  type: WorkflowType.Start,
  text: t('workflow.nodes.startNode.text'), // Assuming this translates to 'Start'
  label: t('workflow.nodes.startNode.label') // Assuming this translates to 'Start'
};

/**
 * Application node configuration object
 */
const applicationNode: NodeConfigInterface = {
  type: WorkflowType.Application,
  text: t('workflow.nodes.applicationNode.label')
}

// Dictionary mapping each workflow type to its corresponding node configuration object
const nodeDict: any = {
  [WorkflowType.Start]: startNode,
  [WorkflowType.Reply]: replyNode,
  [WorkflowType.ToolLib]: toolNode,
  [WorkflowType.ToolWorkflowLib]: toolWorkflowLibNode,
  [WorkflowType.ToolLibCustom]: toolNode, // Mapped back from original naming convention
  [WorkflowType.RerankerNode]: rerankerNode,
  [WorkflowType.FormNode]: formNode
};

These changes enhance the modularity of the code through encapsulation of interfaces, improved variable names, and clearer documentation. Adjustments like renaming variables should only take place when absolutely necessary and after thorough consideration to ensure consistency across components and prevent potential bugs due to name collisions or misinterpretation.

@zhanweizhang7 zhanweizhang7 merged commit b90a1c9 into tool-workflow Mar 16, 2026
3 of 4 checks passed
@zhanweizhang7 zhanweizhang7 deleted the pr@tool-workflow@feat_tool_workflow_node branch March 16, 2026 03:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants