You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2191 lines
60 KiB
2191 lines
60 KiB
/*
|
|
* 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"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/spf13/cast"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/sync/errgroup"
|
|
"gorm.io/gorm"
|
|
|
|
"strconv"
|
|
|
|
einoCompose "github.com/cloudwego/eino/compose"
|
|
|
|
"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
|
|
workflowModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/workflow"
|
|
cloudworkflow "github.com/coze-dev/coze-studio/backend/api/model/workflow"
|
|
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
|
|
"github.com/coze-dev/coze-studio/backend/domain/workflow"
|
|
"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/convert"
|
|
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/repo"
|
|
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
|
|
"github.com/coze-dev/coze-studio/backend/infra/contract/cache"
|
|
"github.com/coze-dev/coze-studio/backend/infra/contract/chatmodel"
|
|
"github.com/coze-dev/coze-studio/backend/infra/contract/idgen"
|
|
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/logs"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
|
|
"github.com/coze-dev/coze-studio/backend/types/errno"
|
|
)
|
|
|
|
type impl struct {
|
|
repo workflow.Repository
|
|
*asToolImpl
|
|
*executableImpl
|
|
*conversationImpl
|
|
}
|
|
|
|
func NewWorkflowService(repo workflow.Repository) workflow.Service {
|
|
return &impl{
|
|
repo: repo,
|
|
asToolImpl: &asToolImpl{
|
|
repo: repo,
|
|
},
|
|
executableImpl: &executableImpl{
|
|
repo: repo,
|
|
},
|
|
conversationImpl: &conversationImpl{repo: repo},
|
|
}
|
|
}
|
|
|
|
func NewWorkflowRepository(idgen idgen.IDGenerator, db *gorm.DB, redis cache.Cmdable, tos storage.Storage,
|
|
cpStore einoCompose.CheckPointStore, chatModel chatmodel.BaseChatModel, cfg workflow.WorkflowConfig) (workflow.Repository, error) {
|
|
return repo.NewRepository(idgen, db, redis, tos, cpStore, chatModel, cfg)
|
|
|
|
}
|
|
|
|
func (i *impl) ListNodeMeta(_ context.Context, nodeTypes map[entity.NodeType]bool) (map[string][]*entity.NodeTypeMeta, []entity.Category, error) {
|
|
// Initialize result maps
|
|
nodeMetaMap := make(map[string][]*entity.NodeTypeMeta)
|
|
|
|
// Helper function to check if a type should be included based on the filter
|
|
shouldInclude := func(meta *entity.NodeTypeMeta) bool {
|
|
if meta.Disabled {
|
|
return false
|
|
}
|
|
nodeType := meta.Key
|
|
if nodeTypes == nil || len(nodeTypes) == 0 {
|
|
return true // No filter, include all
|
|
}
|
|
_, ok := nodeTypes[nodeType]
|
|
return ok
|
|
}
|
|
|
|
// Process standard node types
|
|
for _, meta := range entity.NodeTypeMetas {
|
|
if shouldInclude(meta) {
|
|
nodeMetaMap[meta.Category] = append(nodeMetaMap[meta.Category], meta)
|
|
}
|
|
}
|
|
|
|
return nodeMetaMap, entity.Categories, nil
|
|
}
|
|
|
|
func (i *impl) Create(ctx context.Context, meta *vo.MetaCreate) (int64, error) {
|
|
id, err := i.repo.CreateMeta(ctx, &vo.Meta{
|
|
CreatorID: meta.CreatorID,
|
|
SpaceID: meta.SpaceID,
|
|
ContentType: meta.ContentType,
|
|
Name: meta.Name,
|
|
Desc: meta.Desc,
|
|
IconURI: meta.IconURI,
|
|
AppID: meta.AppID,
|
|
Mode: meta.Mode,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// save the initialized canvas information to the draft
|
|
if err = i.Save(ctx, id, meta.InitCanvasSchema); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (i *impl) Save(ctx context.Context, id int64, schema string) (err error) {
|
|
var draft vo.Canvas
|
|
if err = sonic.UnmarshalString(schema, &draft); err != nil {
|
|
return vo.WrapError(errno.ErrSerializationDeserializationFail, err)
|
|
}
|
|
|
|
var inputParams, outputParams string
|
|
inputs, outputs := extractInputsAndOutputsNamedInfoList(&draft)
|
|
if inputParams, err = sonic.MarshalString(inputs); err != nil {
|
|
return vo.WrapError(errno.ErrSerializationDeserializationFail, err)
|
|
}
|
|
|
|
if outputParams, err = sonic.MarshalString(outputs); err != nil {
|
|
return vo.WrapError(errno.ErrSerializationDeserializationFail, err)
|
|
}
|
|
|
|
testRunSuccess, err := i.calculateTestRunSuccess(ctx, &draft, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
commitID, err := i.repo.GenID(ctx) // generate a new commit ID for this draft version
|
|
if err != nil {
|
|
return vo.WrapError(errno.ErrIDGenError, err)
|
|
}
|
|
|
|
return i.repo.CreateOrUpdateDraft(ctx, id, &vo.DraftInfo{
|
|
Canvas: schema,
|
|
DraftMeta: &vo.DraftMeta{
|
|
TestRunSuccess: testRunSuccess,
|
|
Modified: true,
|
|
},
|
|
InputParamsStr: inputParams,
|
|
OutputParamsStr: outputParams,
|
|
CommitID: strconv.FormatInt(commitID, 10),
|
|
})
|
|
}
|
|
|
|
func extractInputsAndOutputsNamedInfoList(c *vo.Canvas) (inputs []*vo.NamedTypeInfo, outputs []*vo.NamedTypeInfo) {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
logs.Warnf("failed to extract inputs and outputs: %v", err)
|
|
}
|
|
}()
|
|
var (
|
|
startNode *vo.Node
|
|
endNode *vo.Node
|
|
)
|
|
inputs = make([]*vo.NamedTypeInfo, 0)
|
|
outputs = make([]*vo.NamedTypeInfo, 0)
|
|
for _, node := range c.Nodes {
|
|
if startNode != nil && endNode != nil {
|
|
break
|
|
}
|
|
if node.Type == entity.NodeTypeEntry.IDStr() {
|
|
startNode = node
|
|
}
|
|
if node.Type == entity.NodeTypeExit.IDStr() {
|
|
endNode = node
|
|
}
|
|
}
|
|
|
|
var err error
|
|
if startNode != nil {
|
|
inputs, err = slices.TransformWithErrorCheck(startNode.Data.Outputs, func(o any) (*vo.NamedTypeInfo, error) {
|
|
v, err := vo.ParseVariable(o)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nInfo, err := convert.VariableToNamedTypeInfo(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nInfo, nil
|
|
})
|
|
if err != nil {
|
|
logs.Warn(fmt.Sprintf("transform start node outputs to named info failed, err=%v", err))
|
|
}
|
|
}
|
|
|
|
if endNode != nil {
|
|
outputs, err = slices.TransformWithErrorCheck(endNode.Data.Inputs.InputParameters, func(a *vo.Param) (*vo.NamedTypeInfo, error) {
|
|
return convert.BlockInputToNamedTypeInfo(a.Name, a.Input)
|
|
})
|
|
if err != nil {
|
|
logs.Warn(fmt.Sprintf("transform end node inputs to named info failed, err=%v", err))
|
|
}
|
|
}
|
|
|
|
return inputs, outputs
|
|
}
|
|
|
|
func (i *impl) Delete(ctx context.Context, policy *vo.DeletePolicy) (ids []int64, err error) {
|
|
if policy.ID != nil || len(policy.IDs) == 1 {
|
|
var id int64
|
|
if policy.ID != nil {
|
|
id = *policy.ID
|
|
} else {
|
|
id = policy.IDs[0]
|
|
}
|
|
|
|
if err = i.repo.Delete(ctx, id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return []int64{id}, nil
|
|
}
|
|
|
|
ids = policy.IDs
|
|
if policy.AppID != nil {
|
|
metas, _, err := i.repo.MGetMetas(ctx, &vo.MetaQuery{
|
|
AppID: policy.AppID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ids = maps.Keys(metas)
|
|
}
|
|
|
|
if err = i.repo.MDelete(ctx, ids); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ids, nil
|
|
}
|
|
|
|
func (i *impl) Get(ctx context.Context, policy *vo.GetPolicy) (*entity.Workflow, error) {
|
|
return i.repo.GetEntity(ctx, policy)
|
|
}
|
|
|
|
func (i *impl) GetWorkflowReference(ctx context.Context, id int64) (map[int64]*vo.Meta, error) {
|
|
parent, err := i.repo.MGetReferences(ctx, &vo.MGetReferencePolicy{
|
|
ReferredIDs: []int64{id},
|
|
ReferringBizType: []vo.ReferringBizType{vo.ReferringBizTypeWorkflow},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(parent) == 0 {
|
|
// if not parent, it means that it is not cited, so it is returned empty
|
|
return map[int64]*vo.Meta{}, nil
|
|
}
|
|
|
|
wfIDs := make(map[int64]struct{}, len(parent))
|
|
for _, ref := range parent {
|
|
wfIDs[ref.ReferringID] = struct{}{}
|
|
}
|
|
ret, _, err := i.repo.MGetMetas(ctx, &vo.MetaQuery{
|
|
IDs: maps.Keys(wfIDs),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
type workflowIdentity struct {
|
|
ID string `json:"id"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
func getAllSubWorkflowIdentities(c *vo.Canvas) []*workflowIdentity {
|
|
workflowEntities := make([]*workflowIdentity, 0)
|
|
|
|
var collectSubWorkFlowEntities func(nodes []*vo.Node)
|
|
collectSubWorkFlowEntities = func(nodes []*vo.Node) {
|
|
for _, n := range nodes {
|
|
if n.Type == entity.NodeTypeSubWorkflow.IDStr() {
|
|
workflowEntities = append(workflowEntities, &workflowIdentity{
|
|
ID: n.Data.Inputs.WorkflowID,
|
|
Version: n.Data.Inputs.WorkflowVersion,
|
|
})
|
|
}
|
|
if len(n.Blocks) > 0 {
|
|
collectSubWorkFlowEntities(n.Blocks)
|
|
}
|
|
}
|
|
}
|
|
|
|
collectSubWorkFlowEntities(c.Nodes)
|
|
|
|
return workflowEntities
|
|
}
|
|
|
|
func (i *impl) ValidateTree(ctx context.Context, id int64, validateConfig vo.ValidateTreeConfig) ([]*cloudworkflow.ValidateTreeInfo, error) {
|
|
wfValidateInfos := make([]*cloudworkflow.ValidateTreeInfo, 0)
|
|
issues, err := validateWorkflowTree(ctx, validateConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to validate work flow: %w", err)
|
|
}
|
|
|
|
if len(issues) > 0 {
|
|
wfValidateInfos = append(wfValidateInfos, &cloudworkflow.ValidateTreeInfo{
|
|
WorkflowID: strconv.FormatInt(id, 10),
|
|
Errors: toValidateErrorData(issues),
|
|
})
|
|
}
|
|
|
|
c := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(validateConfig.CanvasSchema, &c)
|
|
if err != nil {
|
|
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail,
|
|
fmt.Errorf("failed to unmarshal canvas schema: %w", err))
|
|
}
|
|
|
|
subWorkflowIdentities := getAllSubWorkflowIdentities(c)
|
|
|
|
if len(subWorkflowIdentities) > 0 {
|
|
var ids []int64
|
|
for _, e := range subWorkflowIdentities {
|
|
if e.Version != "" {
|
|
continue
|
|
}
|
|
// only project-level workflows need to validate sub-workflows
|
|
ids = append(ids, cast.ToInt64(e.ID)) // TODO: this should be int64 from the start
|
|
}
|
|
if len(ids) == 0 {
|
|
return wfValidateInfos, nil
|
|
}
|
|
workflows, _, err := i.MGet(ctx, &vo.MGetPolicy{
|
|
MetaQuery: vo.MetaQuery{
|
|
IDs: ids,
|
|
},
|
|
QType: workflowModel.FromDraft,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, wf := range workflows {
|
|
issues, err = validateWorkflowTree(ctx, vo.ValidateTreeConfig{
|
|
CanvasSchema: wf.Canvas,
|
|
AppID: wf.AppID, // application workflow use same app id
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(issues) > 0 {
|
|
wfValidateInfos = append(wfValidateInfos, &cloudworkflow.ValidateTreeInfo{
|
|
WorkflowID: strconv.FormatInt(wf.ID, 10),
|
|
Name: wf.Name,
|
|
Errors: toValidateErrorData(issues),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return wfValidateInfos, err
|
|
}
|
|
|
|
func (i *impl) QueryNodeProperties(ctx context.Context, wfID int64) (map[string]*vo.NodeProperty, error) {
|
|
draftInfo, err := i.repo.DraftV2(ctx, wfID, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
canvasSchema := draftInfo.Canvas
|
|
if len(canvasSchema) == 0 {
|
|
return nil, fmt.Errorf("no canvas schema")
|
|
}
|
|
|
|
mainCanvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(canvasSchema, mainCanvas)
|
|
if err != nil {
|
|
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
|
|
}
|
|
|
|
mainCanvas.Nodes, mainCanvas.Edges = adaptor.PruneIsolatedNodes(mainCanvas.Nodes, mainCanvas.Edges, nil)
|
|
nodePropertyMap, err := i.collectNodePropertyMap(ctx, mainCanvas)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nodePropertyMap, nil
|
|
}
|
|
|
|
func (i *impl) collectNodePropertyMap(ctx context.Context, canvas *vo.Canvas) (map[string]*vo.NodeProperty, error) {
|
|
nodePropertyMap := make(map[string]*vo.NodeProperty)
|
|
|
|
// If it is a nested type, you need to set its parent node
|
|
for _, n := range canvas.Nodes {
|
|
if len(n.Blocks) > 0 {
|
|
for _, nb := range n.Blocks {
|
|
nb.SetParent(n)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, n := range canvas.Nodes {
|
|
if n.Type == entity.NodeTypeSubWorkflow.IDStr() {
|
|
nodeSchema := &schema.NodeSchema{
|
|
Key: vo.NodeKey(n.ID),
|
|
Type: entity.NodeTypeSubWorkflow,
|
|
Name: n.Data.Meta.Title,
|
|
}
|
|
err := convert.SetInputsForNodeSchema(n, nodeSchema)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prop := &vo.NodeProperty{
|
|
Type: nodeSchema.Type.IDStr(),
|
|
IsEnableUserQuery: isEnableUserQuery(nodeSchema),
|
|
IsEnableChatHistory: isEnableChatHistory(nodeSchema),
|
|
IsRefGlobalVariable: isRefGlobalVariable(nodeSchema),
|
|
}
|
|
nodePropertyMap[string(nodeSchema.Key)] = prop
|
|
wid, err := strconv.ParseInt(n.Data.Inputs.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return nil, vo.WrapError(errno.ErrSchemaConversionFail, err)
|
|
}
|
|
|
|
var canvasSchema string
|
|
if n.Data.Inputs.WorkflowVersion != "" {
|
|
versionInfo, existed, err := i.repo.GetVersion(ctx, wid, n.Data.Inputs.WorkflowVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !existed {
|
|
return nil, vo.WrapError(errno.ErrWorkflowNotFound, fmt.Errorf("workflow version %s not found for ID %d: %w", n.Data.Inputs.WorkflowVersion, wid, err), errorx.KV("id", strconv.FormatInt(wid, 10)))
|
|
}
|
|
canvasSchema = versionInfo.Canvas
|
|
} else {
|
|
draftInfo, err := i.repo.DraftV2(ctx, wid, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
canvasSchema = draftInfo.Canvas
|
|
}
|
|
|
|
if len(canvasSchema) == 0 {
|
|
return nil, fmt.Errorf("workflow id %v ,not get canvas schema, version %v", wid, n.Data.Inputs.WorkflowVersion)
|
|
}
|
|
|
|
c := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(canvasSchema, c)
|
|
if err != nil {
|
|
return nil, vo.WrapError(errno.ErrSchemaConversionFail, err)
|
|
}
|
|
ret, err := i.collectNodePropertyMap(ctx, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prop.SubWorkflow = ret
|
|
|
|
} else {
|
|
nodeSchemas, _, err := adaptor.NodeToNodeSchema(ctx, n, canvas)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, nodeSchema := range nodeSchemas {
|
|
nodePropertyMap[string(nodeSchema.Key)] = &vo.NodeProperty{
|
|
Type: nodeSchema.Type.IDStr(),
|
|
IsEnableUserQuery: isEnableUserQuery(nodeSchema),
|
|
IsEnableChatHistory: isEnableChatHistory(nodeSchema),
|
|
IsRefGlobalVariable: isRefGlobalVariable(nodeSchema),
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
return nodePropertyMap, nil
|
|
}
|
|
|
|
func isEnableUserQuery(s *schema.NodeSchema) bool {
|
|
if s == nil {
|
|
return false
|
|
}
|
|
if s.Type != entity.NodeTypeEntry {
|
|
return false
|
|
}
|
|
|
|
if len(s.OutputSources) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, source := range s.OutputSources {
|
|
fieldPath := source.Path
|
|
if len(fieldPath) == 1 && (fieldPath[0] == "BOT_USER_INPUT" || fieldPath[0] == "USER_INPUT") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isEnableChatHistory(s *schema.NodeSchema) bool {
|
|
if s == nil {
|
|
return false
|
|
}
|
|
|
|
chatHistoryAware, ok := s.Configs.(schema.ChatHistoryAware)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return chatHistoryAware.ChatHistoryEnabled()
|
|
}
|
|
|
|
func isRefGlobalVariable(s *schema.NodeSchema) bool {
|
|
for _, source := range s.InputSources {
|
|
if source.IsRefGlobalVariable() {
|
|
return true
|
|
}
|
|
}
|
|
for _, source := range s.OutputSources {
|
|
if source.IsRefGlobalVariable() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (i *impl) CreateChatFlowRole(ctx context.Context, role *vo.ChatFlowRoleCreate) (int64, error) {
|
|
id, err := i.repo.CreateChatFlowRoleConfig(ctx, &entity.ChatFlowRole{
|
|
Name: role.Name,
|
|
Description: role.Description,
|
|
WorkflowID: role.WorkflowID,
|
|
CreatorID: role.CreatorID,
|
|
AudioConfig: role.AudioConfig,
|
|
UserInputConfig: role.UserInputConfig,
|
|
AvatarUri: role.AvatarUri,
|
|
BackgroundImageInfo: role.BackgroundImageInfo,
|
|
OnboardingInfo: role.OnboardingInfo,
|
|
SuggestReplyInfo: role.SuggestReplyInfo,
|
|
})
|
|
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (i *impl) UpdateChatFlowRole(ctx context.Context, workflowID int64, role *vo.ChatFlowRoleUpdate) error {
|
|
err := i.repo.UpdateChatFlowRoleConfig(ctx, workflowID, role)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *impl) GetChatFlowRole(ctx context.Context, workflowID int64, version string) (*entity.ChatFlowRole, error) {
|
|
role, err, isExist := i.repo.GetChatFlowRoleConfig(ctx, workflowID, version)
|
|
if !isExist {
|
|
logs.CtxWarnf(ctx, "chat flow role not exist, workflow id %v, version %v", workflowID, version)
|
|
// Return (nil, nil) on 'NotExist' to align with the production behavior,
|
|
// where the GET API may be called before the CREATE API during chatflow creation.
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return role, nil
|
|
}
|
|
|
|
func (i *impl) GetWorkflowVersionsByConnector(ctx context.Context, connectorID, workflowID int64, limit int) ([]string, error) {
|
|
return i.repo.GetVersionListByConnectorAndWorkflowID(ctx, connectorID, workflowID, limit)
|
|
}
|
|
|
|
func (i *impl) DeleteChatFlowRole(ctx context.Context, id int64, workflowID int64) error {
|
|
return i.repo.DeleteChatFlowRoleConfig(ctx, id, workflowID)
|
|
}
|
|
|
|
func (i *impl) PublishChatFlowRole(ctx context.Context, policy *vo.PublishRolePolicy) error {
|
|
if policy.WorkflowID == 0 || policy.CreatorID == 0 || policy.Version == "" {
|
|
logs.CtxErrorf(ctx, "invalid publish role policy, workflow id %v, creator id %v should not be zero, version %v should not be empty", policy.WorkflowID, policy.CreatorID, policy.Version)
|
|
return vo.WrapError(errno.ErrInvalidParameter, fmt.Errorf("invalid publish role policy, workflow id %v, creator id %v should not be zero, version %v should not be empty", policy.WorkflowID, policy.CreatorID, policy.Version))
|
|
}
|
|
wf, err := i.repo.GetEntity(ctx, &vo.GetPolicy{
|
|
ID: policy.WorkflowID,
|
|
MetaOnly: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if wf.Mode != cloudworkflow.WorkflowMode_ChatFlow {
|
|
return vo.WrapError(errno.ErrChatFlowRoleOperationFail, fmt.Errorf("workflow id %v, mode %v is not a chatflow", policy.WorkflowID, wf.Mode))
|
|
}
|
|
role, err, isExist := i.repo.GetChatFlowRoleConfig(ctx, policy.WorkflowID, "")
|
|
if !isExist {
|
|
logs.CtxErrorf(ctx, "get draft chat flow role nil, workflow id %v", policy.WorkflowID)
|
|
return vo.WrapError(errno.ErrChatFlowRoleOperationFail, fmt.Errorf("get draft chat flow role nil, workflow id %v", policy.WorkflowID))
|
|
}
|
|
if err != nil {
|
|
return vo.WrapIfNeeded(errno.ErrChatFlowRoleOperationFail, err)
|
|
}
|
|
|
|
_, err = i.repo.CreateChatFlowRoleConfig(ctx, &entity.ChatFlowRole{
|
|
Name: role.Name,
|
|
Description: role.Description,
|
|
WorkflowID: policy.WorkflowID,
|
|
CreatorID: policy.CreatorID,
|
|
AudioConfig: role.AudioConfig,
|
|
UserInputConfig: role.UserInputConfig,
|
|
AvatarUri: role.AvatarUri,
|
|
BackgroundImageInfo: role.BackgroundImageInfo,
|
|
OnboardingInfo: role.OnboardingInfo,
|
|
SuggestReplyInfo: role.SuggestReplyInfo,
|
|
Version: policy.Version,
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func canvasToRefs(referringID int64, canvasStr string) (map[entity.WorkflowReferenceKey]struct{}, error) {
|
|
var canvas vo.Canvas
|
|
if err := sonic.UnmarshalString(canvasStr, &canvas); err != nil {
|
|
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
|
|
}
|
|
|
|
wfRefs := map[entity.WorkflowReferenceKey]struct{}{}
|
|
var getRefFn func([]*vo.Node) error
|
|
getRefFn = func(nodes []*vo.Node) error {
|
|
for _, node := range nodes {
|
|
if node.Type == entity.NodeTypeSubWorkflow.IDStr() {
|
|
referredID, err := strconv.ParseInt(node.Data.Inputs.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return vo.WrapError(errno.ErrSchemaConversionFail, err)
|
|
}
|
|
wfRefs[entity.WorkflowReferenceKey{
|
|
ReferredID: referredID,
|
|
ReferringID: referringID,
|
|
ReferType: vo.ReferTypeSubWorkflow,
|
|
ReferringBizType: vo.ReferringBizTypeWorkflow,
|
|
}] = struct{}{}
|
|
} else if node.Type == entity.NodeTypeLLM.IDStr() {
|
|
if node.Data.Inputs.LLM != nil {
|
|
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
|
|
for _, w := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
|
|
referredID, err := strconv.ParseInt(w.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return vo.WrapError(errno.ErrSchemaConversionFail, err)
|
|
}
|
|
wfRefs[entity.WorkflowReferenceKey{
|
|
ReferredID: referredID,
|
|
ReferringID: referringID,
|
|
ReferType: vo.ReferTypeTool,
|
|
ReferringBizType: vo.ReferringBizTypeWorkflow,
|
|
}] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
} else if len(node.Blocks) > 0 {
|
|
for _, subNode := range node.Blocks {
|
|
if err := getRefFn([]*vo.Node{subNode}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := getRefFn(canvas.Nodes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return wfRefs, nil
|
|
}
|
|
|
|
func (i *impl) Publish(ctx context.Context, policy *vo.PublishPolicy) (err error) {
|
|
meta, err := i.repo.GetMeta(ctx, policy.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if meta.LatestPublishedVersion != nil {
|
|
latestVersion, err := parseVersion(*meta.LatestPublishedVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currentVersion, err := parseVersion(policy.Version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !isIncremental(latestVersion, currentVersion) {
|
|
return fmt.Errorf("the version number is not self-incrementing, old version %v, current version is %v", *meta.LatestPublishedVersion, policy.Version)
|
|
}
|
|
}
|
|
|
|
draft, err := i.repo.DraftV2(ctx, policy.ID, policy.CommitID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !policy.Force && !draft.TestRunSuccess {
|
|
return fmt.Errorf("workflow %d's current draft needs to pass the test run before publishing", policy.ID)
|
|
}
|
|
|
|
wfRefs, err := canvasToRefs(policy.ID, draft.Canvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
versionInfo := &vo.VersionInfo{
|
|
VersionMeta: &vo.VersionMeta{
|
|
Version: policy.Version,
|
|
VersionDescription: policy.VersionDescription,
|
|
VersionCreatorID: policy.CreatorID,
|
|
},
|
|
CanvasInfo: vo.CanvasInfo{
|
|
Canvas: draft.Canvas,
|
|
InputParamsStr: draft.InputParamsStr,
|
|
OutputParamsStr: draft.OutputParamsStr,
|
|
},
|
|
CommitID: draft.CommitID,
|
|
}
|
|
|
|
if err = i.repo.CreateVersion(ctx, policy.ID, versionInfo, wfRefs); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *impl) UpdateMeta(ctx context.Context, id int64, metaUpdate *vo.MetaUpdate) (err error) {
|
|
err = i.repo.UpdateMeta(ctx, id, metaUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if metaUpdate.WorkflowMode != nil && *metaUpdate.WorkflowMode == cloudworkflow.WorkflowMode_ChatFlow {
|
|
err = i.adaptToChatFlow(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *impl) CopyWorkflow(ctx context.Context, workflowID int64, policy vo.CopyWorkflowPolicy) (*entity.Workflow, error) {
|
|
wf, err := i.repo.CopyWorkflow(ctx, workflowID, policy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// chat flow should copy role config
|
|
if wf.Mode == cloudworkflow.WorkflowMode_ChatFlow {
|
|
role, err, isExist := i.repo.GetChatFlowRoleConfig(ctx, workflowID, "")
|
|
if !isExist {
|
|
logs.CtxErrorf(ctx, "get draft chat flow role nil, workflow id %v", workflowID)
|
|
return nil, vo.WrapError(errno.ErrChatFlowRoleOperationFail, fmt.Errorf("get draft chat flow role nil, workflow id %v", workflowID))
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, vo.WrapIfNeeded(errno.ErrChatFlowRoleOperationFail, err)
|
|
}
|
|
_, err = i.repo.CreateChatFlowRoleConfig(ctx, &entity.ChatFlowRole{
|
|
Name: role.Name,
|
|
Description: role.Description,
|
|
WorkflowID: wf.ID,
|
|
CreatorID: wf.CreatorID,
|
|
AudioConfig: role.AudioConfig,
|
|
UserInputConfig: role.UserInputConfig,
|
|
AvatarUri: role.AvatarUri,
|
|
BackgroundImageInfo: role.BackgroundImageInfo,
|
|
OnboardingInfo: role.OnboardingInfo,
|
|
SuggestReplyInfo: role.SuggestReplyInfo,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
}
|
|
|
|
return wf, nil
|
|
|
|
}
|
|
|
|
func (i *impl) ReleaseApplicationWorkflows(ctx context.Context, appID int64, config *vo.ReleaseWorkflowConfig) ([]*vo.ValidateIssue, error) {
|
|
if len(config.ConnectorIDs) == 0 {
|
|
return nil, fmt.Errorf("connector ids is required")
|
|
}
|
|
|
|
allWorkflowsInApp, _, err := i.MGet(ctx, &vo.MGetPolicy{
|
|
MetaQuery: vo.MetaQuery{
|
|
AppID: &appID,
|
|
},
|
|
QType: workflowModel.FromDraft,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
relatedPlugins := make(map[int64]*plugin.PluginEntity, len(config.PluginIDs))
|
|
relatedWorkflow := make(map[int64]entity.IDVersionPair, len(allWorkflowsInApp))
|
|
|
|
for _, wf := range allWorkflowsInApp {
|
|
relatedWorkflow[wf.ID] = entity.IDVersionPair{
|
|
ID: wf.ID,
|
|
Version: config.Version,
|
|
}
|
|
}
|
|
|
|
for _, id := range config.PluginIDs {
|
|
relatedPlugins[id] = &plugin.PluginEntity{
|
|
PluginID: id,
|
|
PluginVersion: &config.Version,
|
|
}
|
|
}
|
|
|
|
vIssues := make([]*vo.ValidateIssue, 0)
|
|
|
|
willPublishWorkflows := make([]*entity.Workflow, 0)
|
|
|
|
if len(config.WorkflowIDs) == 0 {
|
|
willPublishWorkflows = allWorkflowsInApp
|
|
} else {
|
|
willPublishWorkflows, _, err = i.MGet(ctx, &vo.MGetPolicy{
|
|
MetaQuery: vo.MetaQuery{
|
|
AppID: &appID,
|
|
IDs: config.WorkflowIDs,
|
|
},
|
|
QType: workflowModel.FromDraft,
|
|
})
|
|
}
|
|
|
|
for _, wf := range willPublishWorkflows {
|
|
issues, err := validateWorkflowTree(ctx, vo.ValidateTreeConfig{
|
|
CanvasSchema: wf.Canvas,
|
|
AppID: ptr.Of(appID),
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(issues) > 0 {
|
|
vIssues = append(vIssues, toValidateIssue(wf.ID, wf.Name, issues))
|
|
}
|
|
|
|
}
|
|
if len(vIssues) > 0 {
|
|
return vIssues, nil
|
|
}
|
|
|
|
for _, wf := range willPublishWorkflows {
|
|
c := &vo.Canvas{}
|
|
err := sonic.UnmarshalString(wf.Canvas, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = replaceRelatedWorkflowOrExternalResourceInWorkflowNodes(c.Nodes, relatedWorkflow, vo.ExternalResourceRelated{
|
|
PluginMap: relatedPlugins,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
canvasSchema, err := sonic.MarshalString(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wf.Canvas = canvasSchema
|
|
|
|
}
|
|
|
|
userID := ctxutil.MustGetUIDFromCtx(ctx)
|
|
workflowsToPublish := make(map[int64]*vo.VersionInfo)
|
|
for _, wf := range willPublishWorkflows {
|
|
inputStr, err := sonic.MarshalString(wf.InputParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outputStr, err := sonic.MarshalString(wf.OutputParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
workflowsToPublish[wf.ID] = &vo.VersionInfo{
|
|
VersionMeta: &vo.VersionMeta{
|
|
Version: config.Version,
|
|
VersionCreatorID: userID,
|
|
},
|
|
CanvasInfo: vo.CanvasInfo{
|
|
Canvas: wf.Canvas,
|
|
InputParamsStr: inputStr,
|
|
OutputParamsStr: outputStr,
|
|
},
|
|
CommitID: wf.CommitID,
|
|
}
|
|
}
|
|
|
|
workflowIDs := make([]int64, 0, len(willPublishWorkflows))
|
|
for id, vInfo := range workflowsToPublish {
|
|
// if version existed skip
|
|
_, existed, err := i.repo.GetVersion(ctx, id, config.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existed {
|
|
continue
|
|
}
|
|
wfRefs, err := canvasToRefs(id, vInfo.Canvas)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
workflowIDs = append(workflowIDs, id)
|
|
if err = i.repo.CreateVersion(ctx, id, vInfo, wfRefs); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = i.ReleaseConversationTemplate(ctx, appID, config.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, wf := range willPublishWorkflows {
|
|
if wf.Mode == cloudworkflow.WorkflowMode_ChatFlow {
|
|
err = i.PublishChatFlowRole(ctx, &vo.PublishRolePolicy{
|
|
WorkflowID: wf.ID,
|
|
CreatorID: wf.CreatorID,
|
|
Version: config.Version,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, connectorID := range config.ConnectorIDs {
|
|
err = i.repo.BatchCreateConnectorWorkflowVersion(ctx, appID, connectorID, workflowIDs, config.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (i *impl) CopyWorkflowFromAppToLibrary(ctx context.Context, workflowID int64, appID int64, related vo.ExternalResourceRelated) (*entity.CopyWorkflowFromAppToLibraryResult, error) {
|
|
type copiedWorkflow struct {
|
|
id int64
|
|
draftInfo *vo.DraftInfo
|
|
refWfs map[int64]*copiedWorkflow
|
|
}
|
|
var (
|
|
err error
|
|
vIssues = make([]*vo.ValidateIssue, 0)
|
|
draftVersion *vo.DraftInfo
|
|
workflowPublishVersion = "v0.0.1"
|
|
)
|
|
|
|
draftVersion, err = i.repo.DraftV2(ctx, workflowID, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
issues, err := validateWorkflowTree(ctx, vo.ValidateTreeConfig{
|
|
CanvasSchema: draftVersion.Canvas,
|
|
AppID: ptr.Of(appID),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
draftWorkflows, wid2Named, err := i.repo.GetDraftWorkflowsByAppID(ctx, appID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(issues) > 0 {
|
|
vIssues = append(vIssues, toValidateIssue(workflowID, wid2Named[workflowID], issues))
|
|
}
|
|
|
|
var validateAndBuildWorkflowReference func(nodes []*vo.Node, wf *copiedWorkflow) error
|
|
hasVerifiedWorkflowIDMap := make(map[int64]bool)
|
|
|
|
validateAndBuildWorkflowReference = func(nodes []*vo.Node, wf *copiedWorkflow) error {
|
|
for _, node := range nodes {
|
|
if node.Type == entity.NodeTypeSubWorkflow.IDStr() {
|
|
var (
|
|
v *vo.DraftInfo
|
|
wfID int64
|
|
ok bool
|
|
)
|
|
wfID, err = strconv.ParseInt(node.Data.Inputs.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v, ok = draftWorkflows[wfID]; !ok {
|
|
continue
|
|
}
|
|
if _, ok = wf.refWfs[wfID]; ok {
|
|
continue
|
|
}
|
|
|
|
if !hasVerifiedWorkflowIDMap[wfID] {
|
|
issues, err = validateWorkflowTree(ctx, vo.ValidateTreeConfig{
|
|
CanvasSchema: v.Canvas,
|
|
AppID: ptr.Of(appID),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(issues) > 0 {
|
|
vIssues = append(vIssues, toValidateIssue(wfID, wid2Named[wfID], issues))
|
|
}
|
|
hasVerifiedWorkflowIDMap[wfID] = true
|
|
}
|
|
|
|
swf := &copiedWorkflow{
|
|
id: wfID,
|
|
draftInfo: v,
|
|
refWfs: make(map[int64]*copiedWorkflow),
|
|
}
|
|
wf.refWfs[wfID] = swf
|
|
var subCanvas *vo.Canvas
|
|
err = sonic.UnmarshalString(v.Canvas, &subCanvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = validateAndBuildWorkflowReference(subCanvas.Nodes, swf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
if node.Type == entity.NodeTypeLLM.IDStr() {
|
|
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
|
|
for _, w := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
|
|
var (
|
|
v *vo.DraftInfo
|
|
wfID int64
|
|
ok bool
|
|
)
|
|
wfID, err = strconv.ParseInt(w.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v, ok = draftWorkflows[wfID]; !ok {
|
|
continue
|
|
}
|
|
|
|
if _, ok = wf.refWfs[wfID]; ok {
|
|
continue
|
|
}
|
|
|
|
if !hasVerifiedWorkflowIDMap[wfID] {
|
|
issues, err = validateWorkflowTree(ctx, vo.ValidateTreeConfig{
|
|
CanvasSchema: v.Canvas,
|
|
AppID: ptr.Of(appID),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(issues) > 0 {
|
|
vIssues = append(vIssues, toValidateIssue(wfID, wid2Named[wfID], issues))
|
|
}
|
|
hasVerifiedWorkflowIDMap[wfID] = true
|
|
}
|
|
|
|
swf := &copiedWorkflow{
|
|
id: wfID,
|
|
draftInfo: v,
|
|
refWfs: make(map[int64]*copiedWorkflow),
|
|
}
|
|
wf.refWfs[wfID] = swf
|
|
var subCanvas *vo.Canvas
|
|
err = sonic.UnmarshalString(v.Canvas, &subCanvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = validateAndBuildWorkflowReference(subCanvas.Nodes, swf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(node.Blocks) > 0 {
|
|
err := validateAndBuildWorkflowReference(node.Blocks, wf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
copiedWf := &copiedWorkflow{
|
|
id: workflowID,
|
|
draftInfo: draftVersion,
|
|
refWfs: make(map[int64]*copiedWorkflow),
|
|
}
|
|
draftCanvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(draftVersion.Canvas, &draftCanvas)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = validateAndBuildWorkflowReference(draftCanvas.Nodes, copiedWf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(vIssues) > 0 {
|
|
return &entity.CopyWorkflowFromAppToLibraryResult{
|
|
ValidateIssues: vIssues,
|
|
}, nil
|
|
}
|
|
|
|
var copyAndPublishWorkflowProcess func(wf *copiedWorkflow) error
|
|
|
|
hasPublishedWorkflows := make(map[int64]entity.IDVersionPair)
|
|
copiedWorkflowArray := make([]*entity.Workflow, 0)
|
|
copyAndPublishWorkflowProcess = func(wf *copiedWorkflow) error {
|
|
for _, refWorkflow := range wf.refWfs {
|
|
err := copyAndPublishWorkflowProcess(refWorkflow)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, ok := hasPublishedWorkflows[wf.id]; !ok {
|
|
|
|
var (
|
|
draftCanvasString = wf.draftInfo.Canvas
|
|
inputParams = wf.draftInfo.InputParamsStr
|
|
outputParams = wf.draftInfo.OutputParamsStr
|
|
)
|
|
|
|
canvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(draftCanvasString, &canvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = replaceRelatedWorkflowOrExternalResourceInWorkflowNodes(canvas.Nodes, hasPublishedWorkflows, related)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
modifiedCanvasString, err := sonic.MarshalString(canvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cwf, err := i.CopyWorkflow(ctx, wf.id, vo.CopyWorkflowPolicy{
|
|
TargetAppID: ptr.Of(int64(0)),
|
|
ModifiedCanvasSchema: ptr.Of(modifiedCanvasString),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wfRefs, err := canvasToRefs(cwf.ID, modifiedCanvasString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = i.repo.CreateVersion(ctx, cwf.ID, &vo.VersionInfo{
|
|
CommitID: cwf.CommitID,
|
|
VersionMeta: &vo.VersionMeta{
|
|
Version: workflowPublishVersion,
|
|
VersionCreatorID: ctxutil.MustGetUIDFromCtx(ctx),
|
|
},
|
|
CanvasInfo: vo.CanvasInfo{
|
|
Canvas: modifiedCanvasString,
|
|
InputParamsStr: inputParams,
|
|
OutputParamsStr: outputParams,
|
|
},
|
|
}, wfRefs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
copiedWorkflowArray = append(copiedWorkflowArray, cwf)
|
|
|
|
hasPublishedWorkflows[wf.id] = entity.IDVersionPair{
|
|
ID: cwf.ID,
|
|
Version: workflowPublishVersion,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err = copyAndPublishWorkflowProcess(copiedWf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &entity.CopyWorkflowFromAppToLibraryResult{
|
|
WorkflowIDVersionMap: hasPublishedWorkflows,
|
|
CopiedWorkflows: copiedWorkflowArray,
|
|
}, nil
|
|
|
|
}
|
|
|
|
func (i *impl) DuplicateWorkflowsByAppID(ctx context.Context, sourceAppID, targetAppID int64, related vo.ExternalResourceRelated) ([]*entity.Workflow, error) {
|
|
|
|
type copiedWorkflow struct {
|
|
id int64
|
|
draftInfo *vo.DraftInfo
|
|
refWfs map[int64]*copiedWorkflow
|
|
err error
|
|
draftVersion *vo.DraftInfo
|
|
}
|
|
|
|
draftWorkflows, _, err := i.repo.GetDraftWorkflowsByAppID(ctx, sourceAppID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var duplicateWorkflowProcess func(workflowID int64, info *vo.DraftInfo) error
|
|
|
|
hasCopiedWorkflows := make(map[int64]entity.IDVersionPair)
|
|
var buildWorkflowReference func(nodes []*vo.Node, wf *copiedWorkflow) error
|
|
buildWorkflowReference = func(nodes []*vo.Node, wf *copiedWorkflow) error {
|
|
for _, node := range nodes {
|
|
if node.Type == entity.NodeTypeSubWorkflow.IDStr() {
|
|
var (
|
|
v *vo.DraftInfo
|
|
wfID int64
|
|
ok bool
|
|
)
|
|
wfID, err = strconv.ParseInt(node.Data.Inputs.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v, ok = draftWorkflows[wfID]; !ok {
|
|
continue
|
|
}
|
|
if _, ok = wf.refWfs[wfID]; ok {
|
|
continue
|
|
}
|
|
|
|
swf := &copiedWorkflow{
|
|
id: wfID,
|
|
draftInfo: v,
|
|
refWfs: make(map[int64]*copiedWorkflow),
|
|
}
|
|
wf.refWfs[wfID] = swf
|
|
var subCanvas *vo.Canvas
|
|
err = sonic.UnmarshalString(v.Canvas, &subCanvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = buildWorkflowReference(subCanvas.Nodes, swf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
if node.Type == entity.NodeTypeLLM.IDStr() {
|
|
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
|
|
for _, w := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
|
|
var (
|
|
v *vo.DraftInfo
|
|
wfID int64
|
|
ok bool
|
|
)
|
|
wfID, err = strconv.ParseInt(w.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v, ok = draftWorkflows[wfID]; !ok {
|
|
continue
|
|
}
|
|
|
|
if _, ok = wf.refWfs[wfID]; ok {
|
|
continue
|
|
}
|
|
|
|
swf := &copiedWorkflow{
|
|
id: wfID,
|
|
draftInfo: v,
|
|
refWfs: make(map[int64]*copiedWorkflow),
|
|
}
|
|
wf.refWfs[wfID] = swf
|
|
var subCanvas *vo.Canvas
|
|
err = sonic.UnmarshalString(v.Canvas, &subCanvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = buildWorkflowReference(subCanvas.Nodes, swf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
if len(node.Blocks) > 0 {
|
|
err := buildWorkflowReference(node.Blocks, wf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
copiedWorkflowArray := make([]*entity.Workflow, 0)
|
|
var duplicateWorkflow func(wf *copiedWorkflow) error
|
|
duplicateWorkflow = func(wf *copiedWorkflow) error {
|
|
for _, refWorkflow := range wf.refWfs {
|
|
err := duplicateWorkflow(refWorkflow)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, ok := hasCopiedWorkflows[wf.id]; !ok {
|
|
draftCanvasString := wf.draftInfo.Canvas
|
|
canvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(draftCanvasString, &canvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = replaceRelatedWorkflowOrExternalResourceInWorkflowNodes(canvas.Nodes, hasCopiedWorkflows, related)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
modifiedCanvasString, err := sonic.MarshalString(canvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cwf, err := i.CopyWorkflow(ctx, wf.id, vo.CopyWorkflowPolicy{
|
|
TargetAppID: ptr.Of(targetAppID),
|
|
ModifiedCanvasSchema: ptr.Of(modifiedCanvasString),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
copiedWorkflowArray = append(copiedWorkflowArray, cwf)
|
|
|
|
hasCopiedWorkflows[wf.id] = entity.IDVersionPair{
|
|
ID: cwf.ID,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
duplicateWorkflowProcess = func(workflowID int64, draftVersion *vo.DraftInfo) error {
|
|
copiedWf := &copiedWorkflow{
|
|
id: workflowID,
|
|
draftInfo: draftVersion,
|
|
refWfs: make(map[int64]*copiedWorkflow),
|
|
}
|
|
draftCanvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(draftVersion.Canvas, &draftCanvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = buildWorkflowReference(draftCanvas.Nodes, copiedWf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = duplicateWorkflow(copiedWf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for workflowID, draftVersion := range draftWorkflows {
|
|
if _, ok := hasCopiedWorkflows[workflowID]; ok {
|
|
continue
|
|
}
|
|
err = duplicateWorkflowProcess(workflowID, draftVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = i.repo.CopyTemplateConversationByAppID(ctx, sourceAppID, targetAppID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return copiedWorkflowArray, nil
|
|
|
|
}
|
|
|
|
func (i *impl) SyncRelatedWorkflowResources(ctx context.Context, appID int64, relatedWorkflows map[int64]entity.IDVersionPair, related vo.ExternalResourceRelated) error {
|
|
draftVersions, _, err := i.repo.GetDraftWorkflowsByAppID(ctx, appID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
commitIDs, err := i.repo.GenMultiIDs(ctx, len(draftVersions)-len(relatedWorkflows))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g := &errgroup.Group{}
|
|
idx := 0
|
|
for id, vInfo := range draftVersions {
|
|
if _, ok := relatedWorkflows[id]; ok {
|
|
continue
|
|
}
|
|
commitID := commitIDs[idx]
|
|
idx++
|
|
verInfo := vInfo
|
|
wid := id
|
|
g.Go(func() error {
|
|
canvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(verInfo.Canvas, &canvas)
|
|
err = replaceRelatedWorkflowOrExternalResourceInWorkflowNodes(canvas.Nodes, relatedWorkflows, related)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
modifiedCanvasString, err := sonic.MarshalString(canvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return i.repo.CreateOrUpdateDraft(ctx, wid, &vo.DraftInfo{
|
|
DraftMeta: &vo.DraftMeta{
|
|
TestRunSuccess: false,
|
|
Modified: true,
|
|
},
|
|
Canvas: modifiedCanvasString,
|
|
InputParamsStr: verInfo.InputParamsStr,
|
|
OutputParamsStr: verInfo.OutputParamsStr,
|
|
CommitID: strconv.FormatInt(commitID, 10),
|
|
})
|
|
|
|
})
|
|
}
|
|
return g.Wait()
|
|
|
|
}
|
|
|
|
func (i *impl) GetWorkflowDependenceResource(ctx context.Context, workflowID int64) (*vo.DependenceResource, error) {
|
|
wf, err := i.Get(ctx, &vo.GetPolicy{
|
|
ID: workflowID,
|
|
QType: workflowModel.FromDraft,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
canvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(wf.Canvas, canvas)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ds := &vo.DependenceResource{
|
|
PluginIDs: make([]int64, 0),
|
|
KnowledgeIDs: make([]int64, 0),
|
|
DatabaseIDs: make([]int64, 0),
|
|
}
|
|
var collectDependence func(nodes []*vo.Node) error
|
|
collectDependence = func(nodes []*vo.Node) error {
|
|
for _, node := range nodes {
|
|
nType := entity.IDStrToNodeType(node.Type)
|
|
meta := entity.NodeMetaByNodeType(nType)
|
|
if meta.UseDatabase {
|
|
dsList := node.Data.Inputs.DatabaseInfoList
|
|
if len(dsList) == 0 {
|
|
return fmt.Errorf("database info is requird")
|
|
}
|
|
for _, d := range dsList {
|
|
dsID, err := strconv.ParseInt(d.DatabaseInfoID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ds.DatabaseIDs = append(ds.DatabaseIDs, dsID)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if meta.UseKnowledge {
|
|
datasetListInfoParam := node.Data.Inputs.DatasetParam[0]
|
|
datasetIDs := datasetListInfoParam.Input.Value.Content.([]any)
|
|
for _, id := range datasetIDs {
|
|
k, err := strconv.ParseInt(id.(string), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ds.KnowledgeIDs = append(ds.KnowledgeIDs, k)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if meta.UsePlugin {
|
|
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")
|
|
}
|
|
|
|
pVersion := pluginVersionParam.Input.Value.Content.(string)
|
|
if pVersion == "0" { // version = 0 to represent the plug-in in the app
|
|
ds.PluginIDs = append(ds.PluginIDs, pID)
|
|
}
|
|
}
|
|
|
|
switch nType {
|
|
case entity.NodeTypeLLM:
|
|
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
|
|
for idx := range node.Data.Inputs.FCParam.PluginFCParam.PluginList {
|
|
if node.Data.Inputs.FCParam.PluginFCParam.PluginList[idx].IsDraft {
|
|
pl := node.Data.Inputs.FCParam.PluginFCParam.PluginList[idx]
|
|
pluginID, err := strconv.ParseInt(pl.PluginID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ds.PluginIDs = append(ds.PluginIDs, pluginID)
|
|
|
|
}
|
|
}
|
|
}
|
|
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.KnowledgeFCParam != nil {
|
|
for idx := range node.Data.Inputs.FCParam.KnowledgeFCParam.KnowledgeList {
|
|
kn := node.Data.Inputs.FCParam.KnowledgeFCParam.KnowledgeList[idx]
|
|
kid, err := strconv.ParseInt(kn.ID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ds.KnowledgeIDs = append(ds.KnowledgeIDs, kid)
|
|
|
|
}
|
|
}
|
|
|
|
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
|
|
for idx := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
|
|
if node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList[idx].IsDraft {
|
|
wID, err := strconv.ParseInt(node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList[idx].WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wfe, err := i.repo.GetEntity(ctx, &vo.GetPolicy{
|
|
ID: wID,
|
|
QType: workflowModel.FromDraft,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
workflowToolCanvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(wfe.Canvas, workflowToolCanvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = collectDependence(workflowToolCanvas.Nodes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case entity.NodeTypeSubWorkflow:
|
|
if node.Data.Inputs.WorkflowVersion == "" {
|
|
wfID, err := strconv.ParseInt(node.Data.Inputs.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
subWorkflow, err := i.repo.GetEntity(ctx, &vo.GetPolicy{
|
|
ID: wfID,
|
|
QType: workflowModel.FromDraft,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
subCanvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(subWorkflow.Canvas, subCanvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = collectDependence(subCanvas.Nodes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err = collectDependence(canvas.Nodes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ds, nil
|
|
|
|
}
|
|
|
|
func (i *impl) checkBotAgentNode(node *vo.Node) error {
|
|
if node.Type == entity.NodeTypeCreateConversation.IDStr() || node.Type == entity.NodeTypeConversationDelete.IDStr() || node.Type == entity.NodeTypeConversationUpdate.IDStr() || node.Type == entity.NodeTypeConversationList.IDStr() {
|
|
return errors.New("conversation-related nodes are not supported in chatflow")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *impl) validateNodesRecursively(ctx context.Context, nodes []*vo.Node, checkType cloudworkflow.CheckType, visited map[string]struct{}, repo workflow.Repository) error {
|
|
queue := make([]*vo.Node, 0, len(nodes))
|
|
queue = append(queue, nodes...)
|
|
|
|
for len(queue) > 0 {
|
|
node := queue[0]
|
|
queue = queue[1:]
|
|
|
|
if node == nil {
|
|
continue
|
|
}
|
|
|
|
var checkErr error
|
|
switch checkType {
|
|
case cloudworkflow.CheckType_BotAgent:
|
|
checkErr = i.checkBotAgentNode(node)
|
|
default:
|
|
// For now, we only handle BotAgent check, so we can do nothing here.
|
|
// In the future, if there are other check types that need to be validated on every node, this logic will need to be adjusted.
|
|
}
|
|
if checkErr != nil {
|
|
return checkErr
|
|
}
|
|
|
|
// Enqueue nested nodes for BFS traversal. This handles Loop, Batch, and other nodes with nested blocks.
|
|
if len(node.Blocks) > 0 {
|
|
queue = append(queue, node.Blocks...)
|
|
}
|
|
|
|
if node.Type == entity.NodeTypeSubWorkflow.IDStr() && node.Data != nil && node.Data.Inputs != nil {
|
|
workflowIDStr := node.Data.Inputs.WorkflowID
|
|
if workflowIDStr == "" {
|
|
continue
|
|
}
|
|
|
|
workflowID, err := strconv.ParseInt(workflowIDStr, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid workflow ID in sub-workflow node %s: %w", node.ID, err)
|
|
}
|
|
|
|
version := node.Data.Inputs.WorkflowVersion
|
|
qType := workflowModel.FromDraft
|
|
if version != "" {
|
|
qType = workflowModel.FromSpecificVersion
|
|
}
|
|
|
|
visitedKey := fmt.Sprintf("%d:%s", workflowID, version)
|
|
if _, ok := visited[visitedKey]; ok {
|
|
continue
|
|
}
|
|
visited[visitedKey] = struct{}{}
|
|
|
|
subWfEntity, err := repo.GetEntity(ctx, &vo.GetPolicy{
|
|
ID: workflowID,
|
|
QType: qType,
|
|
Version: version,
|
|
})
|
|
if err != nil {
|
|
delete(visited, visitedKey)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
continue
|
|
}
|
|
return fmt.Errorf("failed to get sub-workflow entity %d: %w", workflowID, err)
|
|
}
|
|
|
|
var canvas vo.Canvas
|
|
if err := sonic.UnmarshalString(subWfEntity.Canvas, &canvas); err != nil {
|
|
return fmt.Errorf("failed to unmarshal canvas for workflow %d: %w", subWfEntity.ID, err)
|
|
}
|
|
|
|
queue = append(queue, canvas.Nodes...)
|
|
}
|
|
|
|
if node.Type == entity.NodeTypeLLM.IDStr() && node.Data != nil && node.Data.Inputs != nil && node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
|
|
for _, subWfInfo := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
|
|
if subWfInfo.WorkflowID == "" {
|
|
continue
|
|
}
|
|
workflowID, err := strconv.ParseInt(subWfInfo.WorkflowID, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid workflow ID in large model node %s: %w", node.ID, err)
|
|
}
|
|
|
|
version := subWfInfo.WorkflowVersion
|
|
qType := workflowModel.FromDraft
|
|
if version != "" {
|
|
qType = workflowModel.FromSpecificVersion
|
|
}
|
|
|
|
visitedKey := fmt.Sprintf("%d:%s", workflowID, version)
|
|
if _, ok := visited[visitedKey]; ok {
|
|
continue
|
|
}
|
|
visited[visitedKey] = struct{}{}
|
|
|
|
subWfEntity, err := repo.GetEntity(ctx, &vo.GetPolicy{
|
|
ID: workflowID,
|
|
QType: qType,
|
|
Version: version,
|
|
})
|
|
if err != nil {
|
|
delete(visited, visitedKey)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
continue
|
|
}
|
|
return fmt.Errorf("failed to get sub-workflow entity %d from large model node: %w", workflowID, err)
|
|
}
|
|
|
|
var canvas vo.Canvas
|
|
if err := sonic.UnmarshalString(subWfEntity.Canvas, &canvas); err != nil {
|
|
return fmt.Errorf("failed to unmarshal canvas for workflow %d from large model node: %w", subWfEntity.ID, err)
|
|
}
|
|
|
|
queue = append(queue, canvas.Nodes...)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *impl) WorkflowSchemaCheck(ctx context.Context, wf *entity.Workflow, checks []cloudworkflow.CheckType) ([]*cloudworkflow.CheckResult, error) {
|
|
checkResults := make([]*cloudworkflow.CheckResult, 0, len(checks))
|
|
|
|
var canvas vo.Canvas
|
|
if err := sonic.UnmarshalString(wf.Canvas, &canvas); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal canvas for workflow %d: %w", wf.ID, err)
|
|
}
|
|
|
|
for _, checkType := range checks {
|
|
visited := make(map[string]struct{})
|
|
visitedKey := fmt.Sprintf("%d:%s", wf.ID, wf.GetVersion())
|
|
visited[visitedKey] = struct{}{}
|
|
|
|
err := i.validateNodesRecursively(ctx, canvas.Nodes, checkType, visited, i.repo)
|
|
|
|
if err != nil {
|
|
checkResults = append(checkResults, &cloudworkflow.CheckResult{
|
|
IsPass: false,
|
|
Reason: err.Error(),
|
|
Type: checkType,
|
|
})
|
|
} else {
|
|
checkResults = append(checkResults, &cloudworkflow.CheckResult{
|
|
IsPass: true,
|
|
Type: checkType,
|
|
Reason: "",
|
|
})
|
|
}
|
|
}
|
|
return checkResults, nil
|
|
}
|
|
|
|
func (i *impl) MGet(ctx context.Context, policy *vo.MGetPolicy) ([]*entity.Workflow, int64, error) {
|
|
if policy.MetaOnly {
|
|
metas, total, err := i.repo.MGetMetas(ctx, &policy.MetaQuery)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
result := make([]*entity.Workflow, len(metas))
|
|
var index int
|
|
|
|
if len(metas) == 0 {
|
|
return result, 0, nil
|
|
}
|
|
|
|
for id := range metas {
|
|
wf := &entity.Workflow{
|
|
ID: id,
|
|
Meta: metas[id],
|
|
}
|
|
result[index] = wf
|
|
index++
|
|
}
|
|
return result, total, nil
|
|
}
|
|
|
|
ioF := func(inputParam, outputParam string) (input []*vo.NamedTypeInfo, output []*vo.NamedTypeInfo, err error) {
|
|
if inputParam != "" {
|
|
err := sonic.UnmarshalString(inputParam, &input)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
if outputParam != "" {
|
|
err := sonic.UnmarshalString(outputParam, &output)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
return input, output, err
|
|
}
|
|
|
|
switch policy.QType {
|
|
case workflowModel.FromDraft:
|
|
return i.repo.MGetDrafts(ctx, policy)
|
|
case workflowModel.FromSpecificVersion:
|
|
if len(policy.IDs) == 0 || len(policy.Versions) != len(policy.IDs) {
|
|
return nil, 0, fmt.Errorf("ids and versions are required when MGet from specific versions")
|
|
}
|
|
|
|
metas, total, err := i.repo.MGetMetas(ctx, &policy.MetaQuery)
|
|
if err != nil {
|
|
return nil, total, err
|
|
}
|
|
|
|
result := make([]*entity.Workflow, len(metas))
|
|
index := 0
|
|
|
|
for id, version := range policy.Versions {
|
|
v, existed, err := i.repo.GetVersion(ctx, id, version)
|
|
if err != nil {
|
|
return nil, total, err
|
|
}
|
|
if !existed {
|
|
return nil, total, vo.WrapError(errno.ErrWorkflowNotFound, fmt.Errorf("workflow version %s not found for ID %d: %w", version, id, err), errorx.KV("id", strconv.FormatInt(id, 10)))
|
|
}
|
|
inputs, outputs, err := ioF(v.InputParamsStr, v.OutputParamsStr)
|
|
if err != nil {
|
|
return nil, total, err
|
|
}
|
|
|
|
wf := &entity.Workflow{
|
|
ID: id,
|
|
Meta: metas[id],
|
|
CommitID: v.CommitID,
|
|
CanvasInfo: &vo.CanvasInfo{
|
|
Canvas: v.Canvas,
|
|
InputParams: inputs,
|
|
OutputParams: outputs,
|
|
InputParamsStr: v.InputParamsStr,
|
|
OutputParamsStr: v.OutputParamsStr,
|
|
},
|
|
VersionMeta: v.VersionMeta,
|
|
}
|
|
result[index] = wf
|
|
index++
|
|
}
|
|
|
|
return result, total, nil
|
|
case workflowModel.FromLatestVersion:
|
|
return i.repo.MGetLatestVersion(ctx, policy)
|
|
default:
|
|
panic("not implemented")
|
|
}
|
|
}
|
|
|
|
func (i *impl) BindConvRelatedInfo(ctx context.Context, convID int64, info entity.ConvRelatedInfo) error {
|
|
return i.repo.BindConvRelatedInfo(ctx, convID, info)
|
|
}
|
|
|
|
func (i *impl) GetConvRelatedInfo(ctx context.Context, convID int64) (*entity.ConvRelatedInfo, bool, func() error, error) {
|
|
return i.repo.GetConvRelatedInfo(ctx, convID)
|
|
}
|
|
|
|
func (i *impl) calculateTestRunSuccess(ctx context.Context, c *vo.Canvas, wid int64) (bool, error) {
|
|
sc, err := adaptor.CanvasToWorkflowSchema(ctx, c)
|
|
if err != nil { // not even legal, test run can't possibly be successful
|
|
return false, nil
|
|
}
|
|
|
|
existedDraft, err := i.repo.DraftV2(ctx, wid, "")
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return false, nil // previous draft version not exists, does not have any test run
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
var existedDraftCanvas vo.Canvas
|
|
err = sonic.UnmarshalString(existedDraft.Canvas, &existedDraftCanvas)
|
|
existedSc, err := adaptor.CanvasToWorkflowSchema(ctx, &existedDraftCanvas)
|
|
if err == nil { // the old existing draft is legal, check if it's equal to the new draft in terms of execution
|
|
if !existedSc.IsEqual(sc) { // there is modification to the execution logic, needs new test run
|
|
return false, nil
|
|
}
|
|
} else { // the old existing draft is not legal, of course haven't any successful test run
|
|
return false, nil
|
|
}
|
|
|
|
return existedDraft.TestRunSuccess, nil // inherit previous draft snapshot's test run success flag
|
|
}
|
|
|
|
func replaceRelatedWorkflowOrExternalResourceInWorkflowNodes(nodes []*vo.Node, relatedWorkflows map[int64]entity.IDVersionPair, related vo.ExternalResourceRelated) error {
|
|
var (
|
|
hasWorkflowRelated = len(relatedWorkflows) > 0
|
|
hasPluginRelated = len(related.PluginMap) > 0
|
|
hasKnowledgeRelated = len(related.KnowledgeMap) > 0
|
|
hasDatabaseRelated = len(related.DatabaseMap) > 0
|
|
)
|
|
|
|
for _, node := range nodes {
|
|
nType := entity.IDStrToNodeType(node.Type)
|
|
meta := entity.NodeMetaByNodeType(nType)
|
|
if meta.UseDatabase {
|
|
if !hasDatabaseRelated || node.Data.Inputs.DatabaseNode == nil {
|
|
continue
|
|
}
|
|
dsList := node.Data.Inputs.DatabaseInfoList
|
|
for idx := range dsList {
|
|
databaseInfo := dsList[idx]
|
|
did, err := strconv.ParseInt(databaseInfo.DatabaseInfoID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if refDatabaseID, ok := related.DatabaseMap[did]; ok {
|
|
databaseInfo.DatabaseInfoID = strconv.FormatInt(refDatabaseID, 10)
|
|
}
|
|
|
|
}
|
|
continue
|
|
}
|
|
|
|
if meta.UseKnowledge {
|
|
if !hasKnowledgeRelated || node.Data.Inputs.Knowledge == nil {
|
|
continue
|
|
}
|
|
datasetListInfoParam := node.Data.Inputs.DatasetParam[0]
|
|
knowledgeIDs := datasetListInfoParam.Input.Value.Content.([]any)
|
|
for idx := range knowledgeIDs {
|
|
kid, err := strconv.ParseInt(knowledgeIDs[idx].(string), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if refKnowledgeID, ok := related.KnowledgeMap[kid]; ok {
|
|
knowledgeIDs[idx] = strconv.FormatInt(refKnowledgeID, 10)
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if meta.UsePlugin {
|
|
if !hasPluginRelated || node.Data.Inputs.PluginAPIParam == nil {
|
|
continue
|
|
}
|
|
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 := related.PluginMap[pID]; ok {
|
|
pluginIDParam.Input.Value.Content = strconv.FormatInt(refPlugin.PluginID, 10)
|
|
if refPlugin.PluginVersion != nil {
|
|
pluginVersionParam.Input.Value.Content = *refPlugin.PluginVersion
|
|
}
|
|
}
|
|
|
|
apiIDParam, ok := apiParams["apiID"]
|
|
if !ok {
|
|
return fmt.Errorf("apiID param is not found")
|
|
}
|
|
|
|
apiID, err := strconv.ParseInt(apiIDParam.Input.Value.Content.(string), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if refApiID, ok := related.PluginToolMap[apiID]; ok {
|
|
apiIDParam.Input.Value.Content = strconv.FormatInt(refApiID, 10)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
switch nType {
|
|
case entity.NodeTypeSubWorkflow:
|
|
if !hasWorkflowRelated || node.Data.Inputs.SubWorkflow == nil {
|
|
continue
|
|
}
|
|
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
|
|
}
|
|
case entity.NodeTypeLLM:
|
|
if node.Data.Inputs.LLM == nil {
|
|
continue
|
|
}
|
|
if hasWorkflowRelated && 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
|
|
}
|
|
node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList[idx] = wf
|
|
}
|
|
|
|
}
|
|
if hasPluginRelated && 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
|
|
}
|
|
|
|
toolID, err := strconv.ParseInt(pl.ApiId, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if refPlugin, ok := related.PluginMap[pluginID]; ok {
|
|
tID, ok := related.PluginToolMap[toolID]
|
|
if ok {
|
|
pl.ApiId = strconv.FormatInt(tID, 10)
|
|
}
|
|
pl.PluginID = strconv.FormatInt(refPlugin.PluginID, 10)
|
|
if refPlugin.PluginVersion != nil {
|
|
pl.PluginVersion = *refPlugin.PluginVersion
|
|
|
|
pl.IsDraft = false
|
|
}
|
|
|
|
}
|
|
node.Data.Inputs.FCParam.PluginFCParam.PluginList[idx] = pl
|
|
|
|
}
|
|
}
|
|
if hasKnowledgeRelated && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.KnowledgeFCParam != nil {
|
|
for idx := range node.Data.Inputs.FCParam.KnowledgeFCParam.KnowledgeList {
|
|
kn := node.Data.Inputs.FCParam.KnowledgeFCParam.KnowledgeList[idx]
|
|
kid, err := strconv.ParseInt(kn.ID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if refKnowledgeID, ok := related.KnowledgeMap[kid]; ok {
|
|
kn.ID = strconv.FormatInt(refKnowledgeID, 10)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
if len(node.Blocks) > 0 {
|
|
err := replaceRelatedWorkflowOrExternalResourceInWorkflowNodes(node.Blocks, relatedWorkflows, related)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func RegisterAllNodeAdaptors() {
|
|
adaptor.RegisterAllNodeAdaptors()
|
|
}
|
|
func (i *impl) adaptToChatFlow(ctx context.Context, wID int64) error {
|
|
wfEntity, err := i.repo.GetEntity(ctx, &vo.GetPolicy{
|
|
ID: wID,
|
|
QType: workflowModel.FromDraft,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
canvas := &vo.Canvas{}
|
|
err = sonic.UnmarshalString(wfEntity.Canvas, canvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var startNode *vo.Node
|
|
for _, node := range canvas.Nodes {
|
|
if node.Type == entity.NodeTypeEntry.IDStr() {
|
|
startNode = node
|
|
break
|
|
}
|
|
}
|
|
|
|
if startNode == nil {
|
|
return fmt.Errorf("can not find start node")
|
|
}
|
|
|
|
vMap := make(map[string]bool)
|
|
for _, o := range startNode.Data.Outputs {
|
|
v, err := vo.ParseVariable(o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
vMap[v.Name] = true
|
|
}
|
|
|
|
if _, ok := vMap[vo.UserInputKey]; !ok {
|
|
startNode.Data.Outputs = append(startNode.Data.Outputs, &vo.Variable{
|
|
Name: vo.UserInputKey,
|
|
Type: vo.VariableTypeString,
|
|
})
|
|
}
|
|
if _, ok := vMap[vo.ConversationNameKey]; !ok {
|
|
startNode.Data.Outputs = append(startNode.Data.Outputs, &vo.Variable{
|
|
Name: vo.ConversationNameKey,
|
|
Type: vo.VariableTypeString,
|
|
DefaultValue: "Default",
|
|
})
|
|
}
|
|
canvasStr, err := sonic.MarshalString(canvas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return i.Save(ctx, wID, canvasStr)
|
|
}
|
|
|