新版本流程编辑器
This commit is contained in:
@@ -5,10 +5,11 @@ import "encoding/json"
|
||||
const SchemaVersion = 2
|
||||
|
||||
type Definition struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
Annotations []Node `json:"annotations,omitempty"`
|
||||
Edges []Edge `json:"edges"`
|
||||
SchemaVersion int `json:"schemaVersion,omitempty"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
Annotations []Node `json:"annotations,omitempty"`
|
||||
Edges []Edge `json:"edges"`
|
||||
GlobalVariable json.RawMessage `json:"globalVariable,omitempty"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
@@ -79,6 +80,17 @@ type Condition struct {
|
||||
Right any `json:"right,omitempty"`
|
||||
}
|
||||
|
||||
type FlowGramConditionItem struct {
|
||||
Key string `json:"key"`
|
||||
Value FlowGramCondition `json:"value"`
|
||||
}
|
||||
|
||||
type FlowGramCondition struct {
|
||||
Left Value `json:"left"`
|
||||
Operator string `json:"operator"`
|
||||
Right Value `json:"right"`
|
||||
}
|
||||
|
||||
func RefValue(nodeID string, field string) Value {
|
||||
return Value{Type: ValueTypeRef, Content: []string{nodeID, field}}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,18 @@ const (
|
||||
NodeTypeKnowledgeRetrieve = "knowledge_retrieve"
|
||||
NodeTypeAnswerabilityGate = "answerability_gate"
|
||||
NodeTypeLLMReply = "llm_reply"
|
||||
NodeTypeLLM = "llm"
|
||||
NodeTypeHTTP = "http"
|
||||
NodeTypeCode = "code"
|
||||
NodeTypeVariable = "variable"
|
||||
NodeTypeMultiCondition = "multi-condition"
|
||||
NodeTypeLoop = "loop"
|
||||
NodeTypeBlockStart = "block-start"
|
||||
NodeTypeBlockEnd = "block-end"
|
||||
NodeTypeComment = "comment"
|
||||
NodeTypeContinue = "continue"
|
||||
NodeTypeBreak = "break"
|
||||
NodeTypeGroup = "group"
|
||||
NodeTypeCondition = "condition"
|
||||
NodeTypeAnalyzeConversation = "analyze_conversation"
|
||||
NodeTypePrepareTicketDraft = "prepare_ticket_draft"
|
||||
@@ -297,9 +309,39 @@ func DefaultRegistry() *Registry {
|
||||
output("status", "结束状态", VariableTypeString, "工作流执行结束时的状态。"),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeLLM,
|
||||
Title: "LLM",
|
||||
Description: "Call the large language model and generate responses.",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
OutputSchema: []VariableSpec{
|
||||
output("result", "Result", VariableTypeString, "The generated model response."),
|
||||
},
|
||||
},
|
||||
officialNodeSpec(NodeTypeHTTP, "HTTP", "Send an HTTP request."),
|
||||
officialNodeSpec(NodeTypeCode, "Code", "Run JavaScript code."),
|
||||
officialNodeSpec(NodeTypeVariable, "Variable", "Assign workflow variables."),
|
||||
officialNodeSpec(NodeTypeMultiCondition, "Multi Condition", "Route through multiple condition branches."),
|
||||
officialNodeSpec(NodeTypeLoop, "Loop", "Iterate over an array in a sub-canvas."),
|
||||
officialNodeSpec(NodeTypeBlockStart, "Block Start", "Start a container block."),
|
||||
officialNodeSpec(NodeTypeBlockEnd, "Block End", "End a container block."),
|
||||
officialNodeSpec(NodeTypeComment, "Comment", "Add a canvas annotation."),
|
||||
officialNodeSpec(NodeTypeContinue, "Continue", "Continue the current loop."),
|
||||
officialNodeSpec(NodeTypeBreak, "Break", "Break the current loop."),
|
||||
officialNodeSpec(NodeTypeGroup, "Group", "Group related workflow nodes."),
|
||||
)
|
||||
}
|
||||
|
||||
func officialNodeSpec(nodeType string, title string, description string) NodeSpec {
|
||||
return NodeSpec{
|
||||
Type: nodeType,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Icon: "",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
}
|
||||
}
|
||||
|
||||
func requiredInput(name string, label string, variableType VariableType, description string) VariableSpec {
|
||||
return VariableSpec{Name: name, Label: label, Type: variableType, Required: true, Description: description}
|
||||
}
|
||||
|
||||
@@ -257,6 +257,10 @@ func (v *definitionValidator) validateConditions() {
|
||||
if strings.TrimSpace(node.Type) != registry.NodeTypeCondition {
|
||||
continue
|
||||
}
|
||||
if rawConditions, ok := node.Data.Extra["conditions"]; ok {
|
||||
v.validateFlowGramConditions(index, node, rawConditions)
|
||||
continue
|
||||
}
|
||||
field := fmt.Sprintf("nodes[%d].config.branches", index)
|
||||
config := dsl.ConditionConfig{}
|
||||
if len(node.Data.Config) > 0 {
|
||||
@@ -307,6 +311,72 @@ func (v *definitionValidator) validateConditions() {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *definitionValidator) validateFlowGramConditions(index int, node dsl.Node, raw json.RawMessage) {
|
||||
field := fmt.Sprintf("nodes[%d].data.conditions", index)
|
||||
var conditions []dsl.FlowGramConditionItem
|
||||
if err := json.Unmarshal(raw, &conditions); err != nil {
|
||||
v.addError(field, "condition data must be valid JSON")
|
||||
return
|
||||
}
|
||||
if len(conditions) == 0 {
|
||||
v.addError(field, "condition node must include at least one condition")
|
||||
return
|
||||
}
|
||||
seenKeys := make(map[string]struct{}, len(conditions))
|
||||
for conditionIndex, item := range conditions {
|
||||
itemField := fmt.Sprintf("%s[%d]", field, conditionIndex)
|
||||
key := strings.TrimSpace(item.Key)
|
||||
if key == "" {
|
||||
v.addError(itemField+".key", "condition key is required")
|
||||
} else if _, exists := seenKeys[key]; exists {
|
||||
v.addError(itemField+".key", "duplicate condition key: "+key)
|
||||
}
|
||||
seenKeys[key] = struct{}{}
|
||||
if !v.hasConditionPortEdge(strings.TrimSpace(node.ID), key) {
|
||||
v.addError(itemField+".key", "condition output port must have an outgoing edge: "+key)
|
||||
}
|
||||
v.validateFlowGramCondition(itemField+".value", strings.TrimSpace(node.ID), item.Value)
|
||||
}
|
||||
if !v.hasConditionPortEdge(strings.TrimSpace(node.ID), "else") {
|
||||
v.addError(field, "condition else port must have an outgoing edge")
|
||||
}
|
||||
}
|
||||
|
||||
func (v *definitionValidator) validateFlowGramCondition(field string, sourceNodeID string, condition dsl.FlowGramCondition) {
|
||||
operator := strings.TrimSpace(condition.Operator)
|
||||
if !isSupportedConditionOperator(operator) {
|
||||
v.addError(field+".operator", "unsupported condition operator: "+operator)
|
||||
return
|
||||
}
|
||||
sourceSelectorNodeID, sourceField, ok := condition.Left.Ref()
|
||||
sourceSelectorNodeID = strings.TrimSpace(sourceSelectorNodeID)
|
||||
sourceField = strings.TrimSpace(sourceField)
|
||||
if !ok || sourceSelectorNodeID == "" || sourceField == "" {
|
||||
v.addError(field+".left", "condition left variable is required")
|
||||
return
|
||||
}
|
||||
if _, exists := v.nodesByID[sourceSelectorNodeID]; !exists {
|
||||
v.addError(field+".left", "condition source node does not exist: "+sourceSelectorNodeID)
|
||||
return
|
||||
}
|
||||
if sourceNodeID != "" && !v.hasPath(sourceSelectorNodeID, sourceNodeID, make(map[string]struct{})) && sourceSelectorNodeID != sourceNodeID {
|
||||
v.addError(field+".left", "condition source node is not available before branch: "+sourceSelectorNodeID)
|
||||
}
|
||||
if !conditionOperatorWithoutRight(operator) && condition.Right.Type == "" {
|
||||
v.addError(field+".right", "condition comparison value is required")
|
||||
}
|
||||
}
|
||||
|
||||
func (v *definitionValidator) hasConditionPortEdge(sourceID string, sourcePortID string) bool {
|
||||
for _, edge := range v.def.Edges {
|
||||
if strings.TrimSpace(edge.SourceNodeID) == sourceID &&
|
||||
strings.TrimSpace(edge.SourcePortID) == sourcePortID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *definitionValidator) validateKnowledgeRetrieveConfigs() {
|
||||
for index, node := range v.def.Nodes {
|
||||
if strings.TrimSpace(node.Type) != registry.NodeTypeKnowledgeRetrieve {
|
||||
|
||||
@@ -18,6 +18,46 @@ func TestValidateDefinitionAcceptsMinimalFlowGramStyleFlow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionAcceptsOfficialFlowGramCondition(t *testing.T) {
|
||||
def := dsl.Definition{
|
||||
Nodes: []dsl.Node{
|
||||
node("start_0", "start", nil, nil),
|
||||
{
|
||||
ID: "condition_0",
|
||||
Type: "condition",
|
||||
Data: dsl.NodeData{
|
||||
Title: "Condition",
|
||||
Extra: map[string]json.RawMessage{
|
||||
"conditions": mustJSON([]dsl.FlowGramConditionItem{
|
||||
{
|
||||
Key: "if_0",
|
||||
Value: dsl.FlowGramCondition{
|
||||
Left: dsl.RefValue("start_0", "query"),
|
||||
Operator: "contains",
|
||||
Right: dsl.ConstantValue("hello"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
node("matched_end", "end", nil, nil),
|
||||
node("else_end", "end", nil, nil),
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
edge("start_0", "condition_0"),
|
||||
portEdge("condition_0", "matched_end", "if_0"),
|
||||
portEdge("condition_0", "else_end", "else"),
|
||||
},
|
||||
}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if !result.Valid {
|
||||
t.Fatalf("expected official FlowGram condition to be valid, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsMissingStart(t *testing.T) {
|
||||
def := minimalDefinition()
|
||||
def.Nodes = []dsl.Node{
|
||||
|
||||
Reference in New Issue
Block a user