feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
356
backend/domain/workflow/service/utils.go
Normal file
356
backend/domain/workflow/service/utils.go
Normal file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cloudworkflow "github.com/coze-dev/coze-studio/backend/api/model/ocean/cloud/workflow"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/crossdomain/variable"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/adaptor"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/validate"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
|
||||
"github.com/coze-dev/coze-studio/backend/types/errno"
|
||||
)
|
||||
|
||||
func validateWorkflowTree(ctx context.Context, config vo.ValidateTreeConfig) ([]*validate.Issue, error) {
|
||||
c := &vo.Canvas{}
|
||||
err := sonic.UnmarshalString(config.CanvasSchema, &c)
|
||||
if err != nil {
|
||||
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail,
|
||||
fmt.Errorf("failed to unmarshal canvas schema: %w", err))
|
||||
}
|
||||
|
||||
c.Nodes, c.Edges = adaptor.PruneIsolatedNodes(c.Nodes, c.Edges, nil)
|
||||
validator, err := validate.NewCanvasValidator(ctx, &validate.Config{
|
||||
Canvas: c,
|
||||
AppID: config.AppID,
|
||||
AgentID: config.AgentID,
|
||||
VariablesMetaGetter: variable.GetVariablesMetaGetter(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to new canvas validate : %w", err)
|
||||
}
|
||||
|
||||
var issues []*validate.Issue
|
||||
issues, err = validator.ValidateConnections(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check connectivity : %w", err)
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
issues, err = validator.DetectCycles(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check loops: %w", err)
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
issues, err = validator.ValidateNestedFlows(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check nested batch or recurse: %w", err)
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
issues, err = validator.CheckRefVariable(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check ref variable: %w", err)
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
issues, err = validator.CheckGlobalVariables(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check global variables: %w", err)
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
issues, err = validator.CheckSubWorkFlowTerminatePlanType(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check sub workflow terminate plan type: %w", err)
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func convertToValidationError(issue *validate.Issue) *cloudworkflow.ValidateErrorData {
|
||||
e := &cloudworkflow.ValidateErrorData{}
|
||||
e.Message = issue.Message
|
||||
if issue.NodeErr != nil {
|
||||
e.Type = cloudworkflow.ValidateErrorType_BotValidateNodeErr
|
||||
e.NodeError = &cloudworkflow.NodeError{
|
||||
NodeID: issue.NodeErr.NodeID,
|
||||
}
|
||||
} else if issue.PathErr != nil {
|
||||
e.Type = cloudworkflow.ValidateErrorType_BotValidatePathErr
|
||||
e.PathError = &cloudworkflow.PathError{
|
||||
Start: issue.PathErr.StartNode,
|
||||
End: issue.PathErr.EndNode,
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func toValidateErrorData(issues []*validate.Issue) []*cloudworkflow.ValidateErrorData {
|
||||
validateErrors := make([]*cloudworkflow.ValidateErrorData, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
validateErrors = append(validateErrors, convertToValidationError(issue))
|
||||
}
|
||||
return validateErrors
|
||||
}
|
||||
|
||||
func toValidateIssue(id int64, name string, issues []*validate.Issue) *vo.ValidateIssue {
|
||||
vIssue := &vo.ValidateIssue{
|
||||
WorkflowID: id,
|
||||
WorkflowName: name,
|
||||
}
|
||||
for _, issue := range issues {
|
||||
vIssue.IssueMessages = append(vIssue.IssueMessages, issue.Message)
|
||||
}
|
||||
return vIssue
|
||||
}
|
||||
|
||||
type version struct {
|
||||
Prefix string
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
}
|
||||
|
||||
func parseVersion(versionString string) (_ version, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = vo.WrapError(errno.ErrInvalidVersionName, err)
|
||||
}
|
||||
}()
|
||||
if !strings.HasPrefix(versionString, "v") {
|
||||
return version{}, fmt.Errorf("invalid prefix format: %s", versionString)
|
||||
}
|
||||
versionString = strings.TrimPrefix(versionString, "v")
|
||||
parts := strings.Split(versionString, ".")
|
||||
if len(parts) != 3 {
|
||||
return version{}, fmt.Errorf("invalid version format: %s", versionString)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return version{}, fmt.Errorf("invalid major version: %s", parts[0])
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return version{}, fmt.Errorf("invalid minor version: %s", parts[1])
|
||||
}
|
||||
|
||||
patch, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return version{}, fmt.Errorf("invalid patch version: %s", parts[2])
|
||||
}
|
||||
|
||||
return version{Major: major, Minor: minor, Patch: patch}, nil
|
||||
}
|
||||
|
||||
func isIncremental(prev version, next version) bool {
|
||||
if next.Major < prev.Major {
|
||||
return false
|
||||
}
|
||||
if next.Major > prev.Major {
|
||||
return true
|
||||
}
|
||||
|
||||
if next.Minor < prev.Minor {
|
||||
return false
|
||||
}
|
||||
if next.Minor > prev.Minor {
|
||||
return true
|
||||
}
|
||||
|
||||
return next.Patch > prev.Patch
|
||||
}
|
||||
|
||||
func replaceRelatedWorkflowOrPluginInWorkflowNodes(nodes []*vo.Node, relatedWorkflows map[int64]entity.IDVersionPair, relatedPlugins map[int64]vo.PluginEntity) error {
|
||||
for _, node := range nodes {
|
||||
if node.Type == vo.BlockTypeBotSubWorkflow {
|
||||
workflowID, err := strconv.ParseInt(node.Data.Inputs.WorkflowID, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if wf, ok := relatedWorkflows[workflowID]; ok {
|
||||
node.Data.Inputs.WorkflowID = strconv.FormatInt(wf.ID, 10)
|
||||
node.Data.Inputs.WorkflowVersion = wf.Version
|
||||
}
|
||||
}
|
||||
if node.Type == vo.BlockTypeBotAPI {
|
||||
apiParams := slices.ToMap(node.Data.Inputs.APIParams, func(e *vo.Param) (string, *vo.Param) {
|
||||
return e.Name, e
|
||||
})
|
||||
pluginIDParam, ok := apiParams["pluginID"]
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin id param is not found")
|
||||
}
|
||||
|
||||
pID, err := strconv.ParseInt(pluginIDParam.Input.Value.Content.(string), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pluginVersionParam, ok := apiParams["pluginVersion"]
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin version param is not found")
|
||||
}
|
||||
|
||||
if refPlugin, ok := relatedPlugins[pID]; ok {
|
||||
pluginIDParam.Input.Value.Content = refPlugin.PluginID
|
||||
if refPlugin.PluginVersion != nil {
|
||||
pluginVersionParam.Input.Value.Content = *refPlugin.PluginVersion
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if node.Type == vo.BlockTypeBotLLM {
|
||||
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
|
||||
for idx := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
|
||||
wf := node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList[idx]
|
||||
workflowID, err := strconv.ParseInt(wf.WorkflowID, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if refWf, ok := relatedWorkflows[workflowID]; ok {
|
||||
wf.WorkflowID = strconv.FormatInt(refWf.ID, 10)
|
||||
wf.WorkflowVersion = refWf.Version
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
|
||||
for idx := range node.Data.Inputs.FCParam.PluginFCParam.PluginList {
|
||||
pl := node.Data.Inputs.FCParam.PluginFCParam.PluginList[idx]
|
||||
pluginID, err := strconv.ParseInt(pl.PluginID, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if refPlugin, ok := relatedPlugins[pluginID]; ok {
|
||||
pl.PluginID = strconv.FormatInt(refPlugin.PluginID, 10)
|
||||
if refPlugin.PluginVersion != nil {
|
||||
pl.PluginVersion = *refPlugin.PluginVersion
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if len(node.Blocks) > 0 {
|
||||
err := replaceRelatedWorkflowOrPluginInWorkflowNodes(node.Blocks, relatedWorkflows, relatedPlugins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// entityNodeTypeToBlockType converts an entity.NodeType to the corresponding vo.BlockType.
|
||||
func entityNodeTypeToBlockType(nodeType entity.NodeType) (vo.BlockType, error) {
|
||||
switch nodeType {
|
||||
case entity.NodeTypeEntry:
|
||||
return vo.BlockTypeBotStart, nil
|
||||
case entity.NodeTypeExit:
|
||||
return vo.BlockTypeBotEnd, nil
|
||||
case entity.NodeTypeLLM:
|
||||
return vo.BlockTypeBotLLM, nil
|
||||
case entity.NodeTypePlugin:
|
||||
return vo.BlockTypeBotAPI, nil
|
||||
case entity.NodeTypeCodeRunner:
|
||||
return vo.BlockTypeBotCode, nil
|
||||
case entity.NodeTypeKnowledgeRetriever:
|
||||
return vo.BlockTypeBotDataset, nil
|
||||
case entity.NodeTypeSelector:
|
||||
return vo.BlockTypeCondition, nil
|
||||
case entity.NodeTypeSubWorkflow:
|
||||
return vo.BlockTypeBotSubWorkflow, nil
|
||||
case entity.NodeTypeDatabaseCustomSQL:
|
||||
return vo.BlockTypeDatabase, nil
|
||||
case entity.NodeTypeOutputEmitter:
|
||||
return vo.BlockTypeBotMessage, nil
|
||||
case entity.NodeTypeTextProcessor:
|
||||
return vo.BlockTypeBotText, nil
|
||||
case entity.NodeTypeQuestionAnswer:
|
||||
return vo.BlockTypeQuestion, nil
|
||||
case entity.NodeTypeBreak:
|
||||
return vo.BlockTypeBotBreak, nil
|
||||
case entity.NodeTypeVariableAssigner:
|
||||
return vo.BlockTypeBotAssignVariable, nil
|
||||
case entity.NodeTypeVariableAssignerWithinLoop:
|
||||
return vo.BlockTypeBotLoopSetVariable, nil
|
||||
case entity.NodeTypeLoop:
|
||||
return vo.BlockTypeBotLoop, nil
|
||||
case entity.NodeTypeIntentDetector:
|
||||
return vo.BlockTypeBotIntent, nil
|
||||
case entity.NodeTypeKnowledgeIndexer:
|
||||
return vo.BlockTypeBotDatasetWrite, nil
|
||||
case entity.NodeTypeBatch:
|
||||
return vo.BlockTypeBotBatch, nil
|
||||
case entity.NodeTypeContinue:
|
||||
return vo.BlockTypeBotContinue, nil
|
||||
case entity.NodeTypeInputReceiver:
|
||||
return vo.BlockTypeBotInput, nil
|
||||
case entity.NodeTypeDatabaseUpdate:
|
||||
return vo.BlockTypeDatabaseUpdate, nil
|
||||
case entity.NodeTypeDatabaseQuery:
|
||||
return vo.BlockTypeDatabaseSelect, nil
|
||||
case entity.NodeTypeDatabaseDelete:
|
||||
return vo.BlockTypeDatabaseDelete, nil
|
||||
case entity.NodeTypeHTTPRequester:
|
||||
return vo.BlockTypeBotHttp, nil
|
||||
case entity.NodeTypeDatabaseInsert:
|
||||
return vo.BlockTypeDatabaseInsert, nil
|
||||
case entity.NodeTypeVariableAggregator:
|
||||
return vo.BlockTypeBotVariableMerge, nil
|
||||
case entity.NodeTypeJsonSerialization:
|
||||
return vo.BlockTypeJsonSerialization, nil
|
||||
case entity.NodeTypeJsonDeserialization:
|
||||
return vo.BlockTypeJsonDeserialization, nil
|
||||
case entity.NodeTypeKnowledgeDeleter:
|
||||
return vo.BlockTypeBotDatasetDelete, nil
|
||||
|
||||
default:
|
||||
return "", vo.WrapError(errno.ErrSchemaConversionFail,
|
||||
fmt.Errorf("cannot map entity node type '%s' to a workflow.NodeTemplateType", nodeType))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user