refactor: how to add a node type in workflow (#558)

This commit is contained in:
shentongmartin
2025-08-05 14:02:33 +08:00
committed by GitHub
parent 5dafd81a3f
commit bb6ff0026b
96 changed files with 8305 additions and 8717 deletions

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package loop
package _break
import (
"context"
@@ -22,21 +22,36 @@ import (
"github.com/cloudwego/eino/compose"
"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/nodes"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
)
type Break struct {
parentIntermediateStore variable.Store
}
func NewBreak(_ context.Context, store variable.Store) (*Break, error) {
type Config struct{}
func (c *Config) Adapt(_ context.Context, n *vo.Node, _ ...nodes.AdaptOption) (*schema.NodeSchema, error) {
return &schema.NodeSchema{
Key: vo.NodeKey(n.ID),
Type: entity.NodeTypeBreak,
Name: n.Data.Meta.Title,
Configs: c,
}, nil
}
func (c *Config) Build(_ context.Context, _ *schema.NodeSchema, _ ...schema.BuildOption) (any, error) {
return &Break{
parentIntermediateStore: store,
parentIntermediateStore: &nodes.ParentIntermediateStore{},
}, nil
}
const BreakKey = "$break"
func (b *Break) DoBreak(ctx context.Context, _ map[string]any) (map[string]any, error) {
func (b *Break) Invoke(ctx context.Context, _ map[string]any) (map[string]any, error) {
err := b.parentIntermediateStore.Set(ctx, compose.FieldPath{BreakKey}, true)
if err != nil {
return nil, err

View File

@@ -0,0 +1,47 @@
/*
* 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 _continue
import (
"context"
"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/nodes"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
)
type Continue struct{}
type Config struct{}
func (c *Config) Adapt(_ context.Context, n *vo.Node, _ ...nodes.AdaptOption) (*schema.NodeSchema, error) {
return &schema.NodeSchema{
Key: vo.NodeKey(n.ID),
Type: entity.NodeTypeContinue,
Name: n.Data.Meta.Title,
Configs: c,
}, nil
}
func (c *Config) Build(_ context.Context, _ *schema.NodeSchema, _ ...schema.BuildOption) (any, error) {
return &Continue{}, nil
}
func (co *Continue) Invoke(_ context.Context, in map[string]any) (map[string]any, error) {
return in, nil
}

View File

@@ -27,53 +27,150 @@ import (
"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/convert"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/execute"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes"
_break "github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes/loop/break"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
)
type Loop struct {
config *Config
outputs map[string]*vo.FieldSource
outputVars map[string]string
inner compose.Runnable[map[string]any, map[string]any]
nodeKey vo.NodeKey
loopType Type
inputArrays []string
intermediateVars map[string]*vo.TypeInfo
}
type Config struct {
LoopNodeKey vo.NodeKey
LoopType Type
InputArrays []string
IntermediateVars map[string]*vo.TypeInfo
Outputs []*vo.FieldInfo
Inner compose.Runnable[map[string]any, map[string]any]
}
type Type string
const (
ByArray Type = "by_array"
ByIteration Type = "by_iteration"
Infinite Type = "infinite"
)
func NewLoop(_ context.Context, conf *Config) (*Loop, error) {
if conf == nil {
return nil, errors.New("config is nil")
func (c *Config) Adapt(_ context.Context, n *vo.Node, _ ...nodes.AdaptOption) (*schema.NodeSchema, error) {
if n.Parent() != nil {
return nil, fmt.Errorf("loop node cannot have parent: %s", n.Parent().ID)
}
if conf.LoopType == ByArray {
if len(conf.InputArrays) == 0 {
ns := &schema.NodeSchema{
Key: vo.NodeKey(n.ID),
Type: entity.NodeTypeLoop,
Name: n.Data.Meta.Title,
Configs: c,
}
loopType, err := toLoopType(n.Data.Inputs.LoopType)
if err != nil {
return nil, err
}
c.LoopType = loopType
intermediateVars := make(map[string]*vo.TypeInfo)
for _, param := range n.Data.Inputs.VariableParameters {
tInfo, err := convert.CanvasBlockInputToTypeInfo(param.Input)
if err != nil {
return nil, err
}
intermediateVars[param.Name] = tInfo
ns.SetInputType(param.Name, tInfo)
sources, err := convert.CanvasBlockInputToFieldInfo(param.Input, compose.FieldPath{param.Name}, nil)
if err != nil {
return nil, err
}
ns.AddInputSource(sources...)
}
c.IntermediateVars = intermediateVars
if err := convert.SetInputsForNodeSchema(n, ns); err != nil {
return nil, err
}
if err := convert.SetOutputsForNodeSchema(n, ns); err != nil {
return nil, err
}
for _, fieldInfo := range ns.OutputSources {
if fieldInfo.Source.Ref != nil {
if len(fieldInfo.Source.Ref.FromPath) == 1 {
if _, ok := intermediateVars[fieldInfo.Source.Ref.FromPath[0]]; ok {
fieldInfo.Source.Ref.VariableType = ptr.Of(vo.ParentIntermediate)
}
}
}
}
loopCount := n.Data.Inputs.LoopCount
if loopCount != nil {
typeInfo, err := convert.CanvasBlockInputToTypeInfo(loopCount)
if err != nil {
return nil, err
}
ns.SetInputType(Count, typeInfo)
sources, err := convert.CanvasBlockInputToFieldInfo(loopCount, compose.FieldPath{Count}, nil)
if err != nil {
return nil, err
}
ns.AddInputSource(sources...)
}
for key, tInfo := range ns.InputTypes {
if tInfo.Type != vo.DataTypeArray {
continue
}
if _, ok := intermediateVars[key]; ok { // exclude arrays in intermediate vars
continue
}
c.InputArrays = append(c.InputArrays, key)
}
return ns, nil
}
func toLoopType(l vo.LoopType) (Type, error) {
switch l {
case vo.LoopTypeArray:
return ByArray, nil
case vo.LoopTypeCount:
return ByIteration, nil
case vo.LoopTypeInfinite:
return Infinite, nil
default:
return "", fmt.Errorf("unsupported loop type: %s", l)
}
}
func (c *Config) Build(_ context.Context, ns *schema.NodeSchema, opts ...schema.BuildOption) (any, error) {
if c.LoopType == ByArray {
if len(c.InputArrays) == 0 {
return nil, errors.New("input arrays is empty when loop type is ByArray")
}
}
loop := &Loop{
config: conf,
outputs: make(map[string]*vo.FieldSource),
outputVars: make(map[string]string),
options := schema.GetBuildOptions(opts...)
if options.Inner == nil {
return nil, errors.New("inner workflow is required for Loop Node")
}
for _, info := range conf.Outputs {
loop := &Loop{
outputs: make(map[string]*vo.FieldSource),
outputVars: make(map[string]string),
inputArrays: c.InputArrays,
nodeKey: ns.Key,
intermediateVars: c.IntermediateVars,
inner: options.Inner,
loopType: c.LoopType,
}
for _, info := range ns.OutputSources {
if len(info.Path) != 1 {
return nil, fmt.Errorf("invalid output path: %s", info.Path)
}
@@ -87,7 +184,7 @@ func NewLoop(_ context.Context, conf *Config) (*Loop, error) {
return nil, fmt.Errorf("loop output refers to intermediate variable, but path length > 1: %v", fromPath)
}
if _, ok := conf.IntermediateVars[fromPath[0]]; !ok {
if _, ok := c.IntermediateVars[fromPath[0]]; !ok {
return nil, fmt.Errorf("loop output refers to intermediate variable, but not found in intermediate vars: %v", fromPath)
}
@@ -102,18 +199,27 @@ func NewLoop(_ context.Context, conf *Config) (*Loop, error) {
return loop, nil
}
type Type string
const (
ByArray Type = "by_array"
ByIteration Type = "by_iteration"
Infinite Type = "infinite"
)
const (
Count = "loopCount"
)
func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.NestedWorkflowOption) (out map[string]any, err error) {
func (l *Loop) Invoke(ctx context.Context, in map[string]any, opts ...nodes.NodeOption) (
out map[string]any, err error) {
maxIter, err := l.getMaxIter(in)
if err != nil {
return nil, err
}
arrays := make(map[string][]any, len(l.config.InputArrays))
for _, arrayKey := range l.config.InputArrays {
arrays := make(map[string][]any, len(l.inputArrays))
for _, arrayKey := range l.inputArrays {
a, ok := nodes.TakeMapValue(in, compose.FieldPath{arrayKey})
if !ok {
return nil, fmt.Errorf("incoming array not present in input: %s", arrayKey)
@@ -121,10 +227,7 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
arrays[arrayKey] = a.([]any)
}
options := &nodes.NestedWorkflowOptions{}
for _, opt := range opts {
opt(options)
}
options := nodes.GetCommonOptions(&nodes.NodeOptions{}, opts...)
var (
existingCState *nodes.NestedWorkflowState
@@ -134,7 +237,7 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
)
err = compose.ProcessState(ctx, func(ctx context.Context, getter nodes.NestedWorkflowAware) error {
var e error
existingCState, _, e = getter.GetNestedWorkflowState(l.config.LoopNodeKey)
existingCState, _, e = getter.GetNestedWorkflowState(l.nodeKey)
if e != nil {
return e
}
@@ -150,15 +253,15 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
for k := range existingCState.IntermediateVars {
intermediateVars[k] = ptr.Of(existingCState.IntermediateVars[k])
}
intermediateVars[BreakKey] = &hasBreak
intermediateVars[_break.BreakKey] = &hasBreak
} else {
output = make(map[string]any, len(l.outputs))
for k := range l.outputs {
output[k] = make([]any, 0)
}
intermediateVars = make(map[string]*any, len(l.config.IntermediateVars))
for varKey := range l.config.IntermediateVars {
intermediateVars = make(map[string]*any, len(l.intermediateVars))
for varKey := range l.intermediateVars {
v, ok := nodes.TakeMapValue(in, compose.FieldPath{varKey})
if !ok {
return nil, fmt.Errorf("incoming intermediate variable not present in input: %s", varKey)
@@ -166,10 +269,10 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
intermediateVars[varKey] = &v
}
intermediateVars[BreakKey] = &hasBreak
intermediateVars[_break.BreakKey] = &hasBreak
}
ctx = nodes.InitIntermediateVars(ctx, intermediateVars, l.config.IntermediateVars)
ctx = nodes.InitIntermediateVars(ctx, intermediateVars, l.intermediateVars)
getIthInput := func(i int) (map[string]any, map[string]any, error) {
input := make(map[string]any)
@@ -190,13 +293,13 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
input[k] = v
}
input[string(l.config.LoopNodeKey)+"#index"] = int64(i)
input[string(l.nodeKey)+"#index"] = int64(i)
items := make(map[string]any)
for arrayKey := range arrays {
ele := arrays[arrayKey][i]
items[arrayKey] = ele
currentKey := string(l.config.LoopNodeKey) + "#" + arrayKey
currentKey := string(l.nodeKey) + "#" + arrayKey
// Recursively expand map[string]any elements
var expand func(prefix string, val interface{})
@@ -276,7 +379,7 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
}
}
taskOutput, err := l.config.Inner.Invoke(subCtx, input, ithOpts...)
taskOutput, err := l.inner.Invoke(subCtx, input, ithOpts...)
if err != nil {
info, ok := compose.ExtractInterruptInfo(err)
if !ok {
@@ -322,29 +425,26 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
if len(index2InterruptInfo) > 0 { // this invocation of batch.Execute has new interruptions
iEvent := &entity.InterruptEvent{
NodeKey: l.config.LoopNodeKey,
NodeKey: l.nodeKey,
NodeType: entity.NodeTypeLoop,
NestedInterruptInfo: index2InterruptInfo, // only emit the newly generated interruptInfo
}
err := compose.ProcessState(ctx, func(ctx context.Context, setter nodes.NestedWorkflowAware) error {
if e := setter.SaveNestedWorkflowState(l.config.LoopNodeKey, compState); e != nil {
if e := setter.SaveNestedWorkflowState(l.nodeKey, compState); e != nil {
return e
}
return setter.SetInterruptEvent(l.config.LoopNodeKey, iEvent)
return setter.SetInterruptEvent(l.nodeKey, iEvent)
})
if err != nil {
return nil, err
}
fmt.Println("save interruptEvent in state within loop: ", iEvent)
fmt.Println("save composite info in state within loop: ", compState)
return nil, compose.InterruptAndRerun
} else {
err := compose.ProcessState(ctx, func(ctx context.Context, setter nodes.NestedWorkflowAware) error {
return setter.SaveNestedWorkflowState(l.config.LoopNodeKey, compState)
return setter.SaveNestedWorkflowState(l.nodeKey, compState)
})
if err != nil {
return nil, err
@@ -354,8 +454,7 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
}
if existingCState != nil && len(existingCState.Index2InterruptInfo) > 0 {
fmt.Println("no interrupt thrown this round, but has historical interrupt events: ", existingCState.Index2InterruptInfo)
panic("impossible")
panic(fmt.Sprintf("no interrupt thrown this round, but has historical interrupt events: %v", existingCState.Index2InterruptInfo))
}
for outputVarKey, intermediateVarKey := range l.outputVars {
@@ -368,9 +467,9 @@ func (l *Loop) Execute(ctx context.Context, in map[string]any, opts ...nodes.Nes
func (l *Loop) getMaxIter(in map[string]any) (int, error) {
maxIter := math.MaxInt
switch l.config.LoopType {
switch l.loopType {
case ByArray:
for _, arrayKey := range l.config.InputArrays {
for _, arrayKey := range l.inputArrays {
a, ok := nodes.TakeMapValue(in, compose.FieldPath{arrayKey})
if !ok {
return 0, fmt.Errorf("incoming array not present in input: %s", arrayKey)
@@ -394,7 +493,7 @@ func (l *Loop) getMaxIter(in map[string]any) (int, error) {
maxIter = int(iter.(int64))
case Infinite:
default:
return 0, fmt.Errorf("loop type not supported: %v", l.config.LoopType)
return 0, fmt.Errorf("loop type not supported: %v", l.loopType)
}
return maxIter, nil
@@ -409,8 +508,8 @@ func convertIntermediateVars(vars map[string]*any) map[string]any {
}
func (l *Loop) ToCallbackInput(_ context.Context, in map[string]any) (map[string]any, error) {
trimmed := make(map[string]any, len(l.config.InputArrays))
for _, arrayKey := range l.config.InputArrays {
trimmed := make(map[string]any, len(l.inputArrays))
for _, arrayKey := range l.inputArrays {
if v, ok := in[arrayKey]; ok {
trimmed[arrayKey] = v
}