xyflow change to flowgraam
This commit is contained in:
@@ -2,20 +2,34 @@ package dsl
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const SchemaVersion = 2
|
||||
|
||||
type Definition struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
EntryNodeID string `json:"entryNodeId"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
Edges []Edge `json:"edges"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Position Position `json:"position"`
|
||||
Config json.RawMessage `json:"config"`
|
||||
Inputs map[string]VariableSelector `json:"inputs,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Meta NodeMeta `json:"meta"`
|
||||
Data NodeData `json:"data"`
|
||||
Blocks []Node `json:"blocks,omitempty"`
|
||||
Edges []Edge `json:"edges,omitempty"`
|
||||
}
|
||||
|
||||
type NodeMeta struct {
|
||||
Position Position `json:"position"`
|
||||
}
|
||||
|
||||
type NodeData struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Config json.RawMessage `json:"config,omitempty"`
|
||||
Inputs json.RawMessage `json:"inputs,omitempty"`
|
||||
Outputs json.RawMessage `json:"outputs,omitempty"`
|
||||
InputsValues map[string]Value `json:"inputsValues,omitempty"`
|
||||
Extra map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
@@ -24,9 +38,25 @@ type Position struct {
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
SourceNodeID string `json:"sourceNodeID"`
|
||||
TargetNodeID string `json:"targetNodeID"`
|
||||
SourcePortID string `json:"sourcePortID,omitempty"`
|
||||
TargetPortID string `json:"targetPortID,omitempty"`
|
||||
}
|
||||
|
||||
type ValueType string
|
||||
|
||||
const (
|
||||
ValueTypeConstant ValueType = "constant"
|
||||
ValueTypeRef ValueType = "ref"
|
||||
ValueTypeTemplate ValueType = "template"
|
||||
)
|
||||
|
||||
type Value struct {
|
||||
Type ValueType `json:"type"`
|
||||
Content []string `json:"content,omitempty"`
|
||||
ConstantContent any `json:"-"`
|
||||
RawContent json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
type ConditionConfig struct {
|
||||
@@ -42,13 +72,118 @@ type ConditionBranch struct {
|
||||
}
|
||||
|
||||
type Condition struct {
|
||||
Expression string `json:"expression,omitempty"`
|
||||
Left *VariableSelector `json:"left,omitempty"`
|
||||
Operator string `json:"operator,omitempty"`
|
||||
Right any `json:"right,omitempty"`
|
||||
Expression string `json:"expression,omitempty"`
|
||||
Left *Value `json:"left,omitempty"`
|
||||
Operator string `json:"operator,omitempty"`
|
||||
Right any `json:"right,omitempty"`
|
||||
}
|
||||
|
||||
type VariableSelector struct {
|
||||
NodeID string `json:"nodeId"`
|
||||
Field string `json:"field"`
|
||||
func RefValue(nodeID string, field string) Value {
|
||||
return Value{Type: ValueTypeRef, Content: []string{nodeID, field}}
|
||||
}
|
||||
|
||||
func ConstantValue(value any) Value {
|
||||
raw, _ := json.Marshal(value)
|
||||
return Value{Type: ValueTypeConstant, ConstantContent: value, RawContent: raw}
|
||||
}
|
||||
|
||||
func TemplateValue(value string) Value {
|
||||
return Value{Type: ValueTypeTemplate, Content: []string{value}}
|
||||
}
|
||||
|
||||
func (v Value) Ref() (nodeID string, field string, ok bool) {
|
||||
if v.Type != ValueTypeRef || len(v.Content) < 2 {
|
||||
return "", "", false
|
||||
}
|
||||
return v.Content[0], v.Content[1], true
|
||||
}
|
||||
|
||||
func (v *Value) UnmarshalJSON(data []byte) error {
|
||||
type alias struct {
|
||||
Type ValueType `json:"type"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
}
|
||||
var parsed alias
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Type = parsed.Type
|
||||
v.RawContent = append(v.RawContent[:0], parsed.Content...)
|
||||
switch parsed.Type {
|
||||
case ValueTypeRef:
|
||||
var content []string
|
||||
if len(parsed.Content) > 0 {
|
||||
if err := json.Unmarshal(parsed.Content, &content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
v.Content = content
|
||||
case ValueTypeTemplate:
|
||||
var content string
|
||||
if len(parsed.Content) > 0 {
|
||||
if err := json.Unmarshal(parsed.Content, &content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
v.Content = []string{content}
|
||||
case ValueTypeConstant:
|
||||
if len(parsed.Content) > 0 {
|
||||
if err := json.Unmarshal(parsed.Content, &v.ConstantContent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
if len(parsed.Content) > 0 {
|
||||
var content []string
|
||||
if err := json.Unmarshal(parsed.Content, &content); err == nil {
|
||||
v.Content = content
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v Value) MarshalJSON() ([]byte, error) {
|
||||
type alias struct {
|
||||
Type ValueType `json:"type"`
|
||||
Content any `json:"content,omitempty"`
|
||||
}
|
||||
var content any
|
||||
switch v.Type {
|
||||
case ValueTypeRef:
|
||||
content = v.Content
|
||||
case ValueTypeTemplate:
|
||||
if len(v.Content) > 0 {
|
||||
content = v.Content[0]
|
||||
}
|
||||
case ValueTypeConstant:
|
||||
content = v.ConstantContent
|
||||
default:
|
||||
if len(v.Content) > 0 {
|
||||
content = v.Content
|
||||
}
|
||||
}
|
||||
return json.Marshal(alias{Type: v.Type, Content: content})
|
||||
}
|
||||
|
||||
func (d *NodeData) UnmarshalJSON(data []byte) error {
|
||||
type alias NodeData
|
||||
var parsed alias
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
return err
|
||||
}
|
||||
extra := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal(data, &extra); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(extra, "title")
|
||||
delete(extra, "config")
|
||||
delete(extra, "inputs")
|
||||
delete(extra, "outputs")
|
||||
delete(extra, "inputsValues")
|
||||
*d = NodeData(parsed)
|
||||
if len(extra) > 0 {
|
||||
d.Extra = extra
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package dsl_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"agent-desk/internal/ai/workflow/dsl"
|
||||
)
|
||||
|
||||
func TestDefinitionUnmarshalsFlowGramStyleSchema(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"schemaVersion": 2,
|
||||
"nodes": [{
|
||||
"id": "send_1",
|
||||
"type": "send_reply",
|
||||
"meta": {
|
||||
"position": { "x": 360, "y": 120 }
|
||||
},
|
||||
"data": {
|
||||
"title": "发送回复",
|
||||
"config": { "text": "hello" },
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"replyText": { "type": "string" }
|
||||
},
|
||||
"required": ["replyText"]
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sent": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"inputsValues": {
|
||||
"replyText": {
|
||||
"type": "ref",
|
||||
"content": ["start_1", "userMessage"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}],
|
||||
"edges": [{
|
||||
"sourceNodeID": "start_1",
|
||||
"targetNodeID": "send_1",
|
||||
"sourcePortID": "default"
|
||||
}]
|
||||
}`)
|
||||
|
||||
var def dsl.Definition
|
||||
if err := json.Unmarshal(raw, &def); err != nil {
|
||||
t.Fatalf("unmarshal definition: %v", err)
|
||||
}
|
||||
|
||||
if def.SchemaVersion != 2 {
|
||||
t.Fatalf("unexpected schema version: %d", def.SchemaVersion)
|
||||
}
|
||||
node := def.Nodes[0]
|
||||
if node.ID != "send_1" || node.Type != "send_reply" {
|
||||
t.Fatalf("unexpected node identity: %#v", node)
|
||||
}
|
||||
if node.Meta.Position.X != 360 || node.Meta.Position.Y != 120 {
|
||||
t.Fatalf("unexpected node position: %#v", node.Meta.Position)
|
||||
}
|
||||
if node.Data.Title != "发送回复" {
|
||||
t.Fatalf("unexpected node title: %q", node.Data.Title)
|
||||
}
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal(node.Data.Config, &config); err != nil {
|
||||
t.Fatalf("unmarshal config: %v", err)
|
||||
}
|
||||
if config["text"] != "hello" {
|
||||
t.Fatalf("unexpected config: %s", node.Data.Config)
|
||||
}
|
||||
replyText := node.Data.InputsValues["replyText"]
|
||||
if replyText.Type != dsl.ValueTypeRef || len(replyText.Content) != 2 || replyText.Content[0] != "start_1" || replyText.Content[1] != "userMessage" {
|
||||
t.Fatalf("unexpected replyText value: %#v", replyText)
|
||||
}
|
||||
edge := def.Edges[0]
|
||||
if edge.SourceNodeID != "start_1" || edge.TargetNodeID != "send_1" || edge.SourcePortID != "default" {
|
||||
t.Fatalf("unexpected edge: %#v", edge)
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,8 @@ func DefaultRegistry() *Registry {
|
||||
output("riskSignals", VariableTypeStringArray, "Detected risk signals."),
|
||||
output("reason", VariableTypeString, "Decision reason."),
|
||||
},
|
||||
DefaultInputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
DefaultInputs: map[string]dsl.Value{
|
||||
"userMessage": dsl.RefValue("start_1", "userMessage"),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
@@ -123,8 +123,8 @@ func DefaultRegistry() *Registry {
|
||||
output("items", VariableTypeObjectArray, "Retrieved knowledge items."),
|
||||
output("summary", VariableTypeString, "Short retrieval summary."),
|
||||
},
|
||||
DefaultInputs: map[string]dsl.VariableSelector{
|
||||
"query": {NodeID: "start_1", Field: "userMessage"},
|
||||
DefaultInputs: map[string]dsl.Value{
|
||||
"query": dsl.RefValue("start_1", "userMessage"),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
|
||||
@@ -41,16 +41,16 @@ type VariableValueOption struct {
|
||||
}
|
||||
|
||||
type NodeSpec struct {
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
RiskLevel NodeRiskLevel `json:"riskLevel"`
|
||||
Interruptible bool `json:"interruptible"`
|
||||
RequiresConfirmationPredecessor bool `json:"requiresConfirmationPredecessor"`
|
||||
ConfigSchema any `json:"configSchema,omitempty"`
|
||||
InputSchema []VariableSpec `json:"inputSchema,omitempty"`
|
||||
OutputSchema []VariableSpec `json:"outputSchema,omitempty"`
|
||||
DefaultInputs map[string]dsl.VariableSelector `json:"defaultInputs,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
RiskLevel NodeRiskLevel `json:"riskLevel"`
|
||||
Interruptible bool `json:"interruptible"`
|
||||
RequiresConfirmationPredecessor bool `json:"requiresConfirmationPredecessor"`
|
||||
ConfigSchema any `json:"configSchema,omitempty"`
|
||||
InputSchema []VariableSpec `json:"inputSchema,omitempty"`
|
||||
OutputSchema []VariableSpec `json:"outputSchema,omitempty"`
|
||||
DefaultInputs map[string]dsl.Value `json:"defaultInputs,omitempty"`
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
|
||||
@@ -53,7 +53,6 @@ type definitionValidator struct {
|
||||
func (v *definitionValidator) validate() {
|
||||
v.validateNodes()
|
||||
v.validateEdges()
|
||||
v.validateEntry()
|
||||
v.validateReachability()
|
||||
v.validateConfirmationGuards()
|
||||
v.validateVariableMappings()
|
||||
@@ -98,53 +97,29 @@ func (v *definitionValidator) validateNodes() {
|
||||
}
|
||||
|
||||
func (v *definitionValidator) validateEdges() {
|
||||
seen := make(map[string]struct{}, len(v.def.Edges))
|
||||
for index, edge := range v.def.Edges {
|
||||
edge.ID = strings.TrimSpace(edge.ID)
|
||||
edge.Source = strings.TrimSpace(edge.Source)
|
||||
edge.Target = strings.TrimSpace(edge.Target)
|
||||
source := strings.TrimSpace(edge.SourceNodeID)
|
||||
target := strings.TrimSpace(edge.TargetNodeID)
|
||||
field := fmt.Sprintf("edges[%d]", index)
|
||||
if edge.ID == "" {
|
||||
v.addError(field+".id", "edge id is required")
|
||||
} else if _, exists := seen[edge.ID]; exists {
|
||||
v.addError(field+".id", "duplicate edge id: "+edge.ID)
|
||||
if source == "" {
|
||||
v.addError(field+".sourceNodeID", "edge source node is required")
|
||||
} else if _, ok := v.nodesByID[source]; !ok {
|
||||
v.addError(field+".sourceNodeID", "edge source node does not exist: "+source)
|
||||
}
|
||||
seen[edge.ID] = struct{}{}
|
||||
if edge.Source == "" {
|
||||
v.addError(field+".source", "edge source is required")
|
||||
} else if _, ok := v.nodesByID[edge.Source]; !ok {
|
||||
v.addError(field+".source", "edge source node does not exist: "+edge.Source)
|
||||
if target == "" {
|
||||
v.addError(field+".targetNodeID", "edge target node is required")
|
||||
} else if _, ok := v.nodesByID[target]; !ok {
|
||||
v.addError(field+".targetNodeID", "edge target node does not exist: "+target)
|
||||
}
|
||||
if edge.Target == "" {
|
||||
v.addError(field+".target", "edge target is required")
|
||||
} else if _, ok := v.nodesByID[edge.Target]; !ok {
|
||||
v.addError(field+".target", "edge target node does not exist: "+edge.Target)
|
||||
if source != "" && target != "" {
|
||||
v.outgoing[source] = append(v.outgoing[source], target)
|
||||
v.incoming[target] = append(v.incoming[target], source)
|
||||
}
|
||||
if edge.Source != "" && edge.Target != "" {
|
||||
v.outgoing[edge.Source] = append(v.outgoing[edge.Source], edge.Target)
|
||||
v.incoming[edge.Target] = append(v.incoming[edge.Target], edge.Source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *definitionValidator) validateEntry() {
|
||||
entryNodeID := strings.TrimSpace(v.def.EntryNodeID)
|
||||
if entryNodeID == "" {
|
||||
v.addError("entryNodeId", "entry node id is required")
|
||||
return
|
||||
}
|
||||
entry, ok := v.nodesByID[entryNodeID]
|
||||
if !ok {
|
||||
v.addError("entryNodeId", "entry node does not exist: "+entryNodeID)
|
||||
return
|
||||
}
|
||||
if entry.Type != registry.NodeTypeStart {
|
||||
v.addError("entryNodeId", "entry node must be the start node")
|
||||
}
|
||||
}
|
||||
|
||||
func (v *definitionValidator) validateReachability() {
|
||||
entryNodeID := strings.TrimSpace(v.def.EntryNodeID)
|
||||
entryNodeID := v.entryNodeID()
|
||||
if entryNodeID == "" {
|
||||
return
|
||||
}
|
||||
@@ -187,17 +162,17 @@ func (v *definitionValidator) validateConfirmationGuards() {
|
||||
}
|
||||
|
||||
func (v *definitionValidator) validateConfirmedInput(nodeID string, node dsl.Node) {
|
||||
selector, ok := node.Inputs["confirmed"]
|
||||
if !ok || strings.TrimSpace(selector.NodeID) == "" || strings.TrimSpace(selector.Field) == "" {
|
||||
value, ok := node.Data.InputsValues["confirmed"]
|
||||
sourceNodeID, sourceField, refOK := value.Ref()
|
||||
if !ok || !refOK || strings.TrimSpace(sourceNodeID) == "" || strings.TrimSpace(sourceField) == "" {
|
||||
return
|
||||
}
|
||||
sourceNodeID := strings.TrimSpace(selector.NodeID)
|
||||
sourceNode, ok := v.nodesByID[sourceNodeID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if sourceNode.Type != registry.NodeTypeHumanConfirm || strings.TrimSpace(selector.Field) != "confirmed" {
|
||||
v.addError("nodes."+nodeID+".inputs.confirmed", "confirmed input must come from human_confirm.confirmed")
|
||||
if sourceNode.Type != registry.NodeTypeHumanConfirm || strings.TrimSpace(sourceField) != "confirmed" {
|
||||
v.addError("nodes."+nodeID+".data.inputsValues.confirmed", "confirmed input must come from human_confirm.confirmed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,47 +186,55 @@ func (v *definitionValidator) validateVariableMappings() {
|
||||
if !input.Required {
|
||||
continue
|
||||
}
|
||||
selector, ok := node.Inputs[input.Name]
|
||||
if !ok || strings.TrimSpace(selector.NodeID) == "" || strings.TrimSpace(selector.Field) == "" {
|
||||
v.addError("nodes."+id+".inputs."+input.Name, "required input mapping is missing: "+input.Name)
|
||||
value, ok := node.Data.InputsValues[input.Name]
|
||||
if !ok {
|
||||
v.addError("nodes."+id+".data.inputsValues."+input.Name, "required input mapping is missing: "+input.Name)
|
||||
continue
|
||||
}
|
||||
v.validateInputSelector(id, input, selector)
|
||||
v.validateInputValue(id, input, value)
|
||||
}
|
||||
for inputName, selector := range node.Inputs {
|
||||
if strings.TrimSpace(selector.NodeID) == "" || strings.TrimSpace(selector.Field) == "" {
|
||||
v.addError("nodes."+id+".inputs."+inputName, "input mapping source is required")
|
||||
continue
|
||||
}
|
||||
for inputName, value := range node.Data.InputsValues {
|
||||
if _, ok := findInputSpec(spec.InputSchema, inputName); ok {
|
||||
continue
|
||||
}
|
||||
sourceNode, sourceOK := v.nodesByID[strings.TrimSpace(selector.NodeID)]
|
||||
sourceNodeID, sourceField, refOK := value.Ref()
|
||||
if !refOK {
|
||||
continue
|
||||
}
|
||||
sourceNode, sourceOK := v.nodesByID[strings.TrimSpace(sourceNodeID)]
|
||||
if !sourceOK {
|
||||
v.addError("nodes."+id+".inputs."+inputName, "input source node does not exist: "+selector.NodeID)
|
||||
v.addError("nodes."+id+".data.inputsValues."+inputName, "input source node does not exist: "+sourceNodeID)
|
||||
continue
|
||||
}
|
||||
sourceSpec, sourceSpecOK := v.registry.Get(sourceNode.Type)
|
||||
if !sourceSpecOK {
|
||||
continue
|
||||
}
|
||||
if _, ok := findOutputSpec(sourceSpec.OutputSchema, selector.Field); !ok {
|
||||
v.addError("nodes."+id+".inputs."+inputName, "input source field does not exist: "+selector.NodeID+"."+selector.Field)
|
||||
if _, ok := findOutputSpec(sourceSpec.OutputSchema, sourceField); !ok {
|
||||
v.addError("nodes."+id+".data.inputsValues."+inputName, "input source field does not exist: "+sourceNodeID+"."+sourceField)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *definitionValidator) validateInputSelector(nodeID string, input registry.VariableSpec, selector dsl.VariableSelector) {
|
||||
sourceNodeID := strings.TrimSpace(selector.NodeID)
|
||||
sourceField := strings.TrimSpace(selector.Field)
|
||||
func (v *definitionValidator) validateInputValue(nodeID string, input registry.VariableSpec, value dsl.Value) {
|
||||
sourceNodeID, sourceField, ok := value.Ref()
|
||||
if !ok {
|
||||
if value.Type == dsl.ValueTypeConstant || value.Type == dsl.ValueTypeTemplate {
|
||||
return
|
||||
}
|
||||
v.addError("nodes."+nodeID+".data.inputsValues."+input.Name, "input mapping source is required")
|
||||
return
|
||||
}
|
||||
sourceNodeID = strings.TrimSpace(sourceNodeID)
|
||||
sourceField = strings.TrimSpace(sourceField)
|
||||
sourceNode, ok := v.nodesByID[sourceNodeID]
|
||||
if !ok {
|
||||
v.addError("nodes."+nodeID+".inputs."+input.Name, "input source node does not exist: "+sourceNodeID)
|
||||
v.addError("nodes."+nodeID+".data.inputsValues."+input.Name, "input source node does not exist: "+sourceNodeID)
|
||||
return
|
||||
}
|
||||
if !v.hasPath(sourceNodeID, nodeID, make(map[string]struct{})) {
|
||||
v.addError("nodes."+nodeID+".inputs."+input.Name, "input source node is not available before current node: "+sourceNodeID)
|
||||
v.addError("nodes."+nodeID+".data.inputsValues."+input.Name, "input source node is not available before current node: "+sourceNodeID)
|
||||
return
|
||||
}
|
||||
sourceSpec, ok := v.registry.Get(sourceNode.Type)
|
||||
@@ -260,11 +243,11 @@ func (v *definitionValidator) validateInputSelector(nodeID string, input registr
|
||||
}
|
||||
output, ok := findOutputSpec(sourceSpec.OutputSchema, sourceField)
|
||||
if !ok {
|
||||
v.addError("nodes."+nodeID+".inputs."+input.Name, "input source field does not exist: "+sourceNodeID+"."+sourceField)
|
||||
v.addError("nodes."+nodeID+".data.inputsValues."+input.Name, "input source field does not exist: "+sourceNodeID+"."+sourceField)
|
||||
return
|
||||
}
|
||||
if !variableTypesCompatible(input.Type, output.Type) {
|
||||
v.addError("nodes."+nodeID+".inputs."+input.Name, fmt.Sprintf("input type mismatch: %s expects %s but %s.%s is %s", input.Name, input.Type, sourceNodeID, sourceField, output.Type))
|
||||
v.addError("nodes."+nodeID+".data.inputsValues."+input.Name, fmt.Sprintf("input type mismatch: %s expects %s but %s.%s is %s", input.Name, input.Type, sourceNodeID, sourceField, output.Type))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,8 +258,8 @@ func (v *definitionValidator) validateConditions() {
|
||||
}
|
||||
field := fmt.Sprintf("nodes[%d].config.branches", index)
|
||||
config := dsl.ConditionConfig{}
|
||||
if len(node.Config) > 0 {
|
||||
if err := json.Unmarshal(node.Config, &config); err != nil {
|
||||
if len(node.Data.Config) > 0 {
|
||||
if err := json.Unmarshal(node.Data.Config, &config); err != nil {
|
||||
v.addError(field, "condition branches config must be valid JSON")
|
||||
continue
|
||||
}
|
||||
@@ -341,9 +324,10 @@ func (v *definitionValidator) validateCondition(field string, sourceNodeID strin
|
||||
v.addError(field+".left", "condition left variable is required")
|
||||
return
|
||||
}
|
||||
sourceSelectorNodeID := strings.TrimSpace(condition.Left.NodeID)
|
||||
sourceField := strings.TrimSpace(condition.Left.Field)
|
||||
if sourceSelectorNodeID == "" || sourceField == "" {
|
||||
sourceSelectorNodeID, sourceField, leftOK := condition.Left.Ref()
|
||||
sourceSelectorNodeID = strings.TrimSpace(sourceSelectorNodeID)
|
||||
sourceField = strings.TrimSpace(sourceField)
|
||||
if !leftOK || sourceSelectorNodeID == "" || sourceField == "" {
|
||||
v.addError(field+".left", "condition left variable is required")
|
||||
return
|
||||
}
|
||||
@@ -462,13 +446,20 @@ func (v *definitionValidator) hasEdgeTo(sourceID string, targetID string) bool {
|
||||
return true
|
||||
}
|
||||
for _, edge := range v.def.Edges {
|
||||
if strings.TrimSpace(edge.Source) == sourceID && strings.TrimSpace(edge.Target) == targetID {
|
||||
if strings.TrimSpace(edge.SourceNodeID) == sourceID && strings.TrimSpace(edge.TargetNodeID) == targetID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *definitionValidator) entryNodeID() string {
|
||||
if len(v.startNodeIDs) != 1 {
|
||||
return ""
|
||||
}
|
||||
return v.startNodeIDs[0]
|
||||
}
|
||||
|
||||
func findInputSpec(items []registry.VariableSpec, name string) (registry.VariableSpec, bool) {
|
||||
name = strings.TrimSpace(name)
|
||||
for _, item := range items {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"agent-desk/internal/ai/workflow/validator"
|
||||
)
|
||||
|
||||
func TestValidateDefinitionAcceptsMinimalConversationFlow(t *testing.T) {
|
||||
func TestValidateDefinitionAcceptsMinimalFlowGramStyleFlow(t *testing.T) {
|
||||
result := validator.ValidateDefinition(minimalDefinition(), registry.DefaultRegistry())
|
||||
|
||||
if !result.Valid {
|
||||
@@ -21,8 +21,8 @@ func TestValidateDefinitionAcceptsMinimalConversationFlow(t *testing.T) {
|
||||
func TestValidateDefinitionRejectsMissingStart(t *testing.T) {
|
||||
def := minimalDefinition()
|
||||
def.Nodes = []dsl.Node{
|
||||
{ID: "reply_1", Type: "send_reply", Config: json.RawMessage(`{"text":"hello"}`)},
|
||||
{ID: "end_1", Type: "end"},
|
||||
node("reply_1", "send_reply", inputs("replyText", dsl.RefValue("start_1", "userMessage")), nil),
|
||||
node("end_1", "end", nil, nil),
|
||||
}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
@@ -35,148 +35,9 @@ func TestValidateDefinitionRejectsMissingStart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsUnknownNodeType(t *testing.T) {
|
||||
func TestValidateDefinitionRejectsMissingRequiredInputValue(t *testing.T) {
|
||||
def := minimalDefinition()
|
||||
def.Nodes = append(def.Nodes, dsl.Node{ID: "unknown_1", Type: "unknown_node"})
|
||||
def.Edges = append(def.Edges, dsl.Edge{ID: "e3", Source: "reply_1", Target: "unknown_1"})
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid {
|
||||
t.Fatalf("expected unknown node type to be invalid")
|
||||
}
|
||||
if !hasValidationMessage(result, "unknown node type") {
|
||||
t.Fatalf("expected unknown-node error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsUnguardedCreateTicket(t *testing.T) {
|
||||
def := dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: "start"},
|
||||
{ID: "draft_1", Type: "prepare_ticket_draft"},
|
||||
{ID: "create_1", Type: "create_ticket"},
|
||||
{ID: "end_1", Type: "end"},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "e1", Source: "start_1", Target: "draft_1"},
|
||||
{ID: "e2", Source: "draft_1", Target: "create_1"},
|
||||
{ID: "e3", Source: "create_1", Target: "end_1"},
|
||||
},
|
||||
}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid {
|
||||
t.Fatalf("expected unguarded create_ticket to be invalid")
|
||||
}
|
||||
if !hasValidationMessage(result, "requires human_confirm") {
|
||||
t.Fatalf("expected confirmation guard error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionAcceptsConfirmedCreateTicket(t *testing.T) {
|
||||
def := dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: "start"},
|
||||
{ID: "draft_1", Type: "prepare_ticket_draft", Inputs: map[string]dsl.VariableSelector{
|
||||
"issue": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "confirm_1", Type: "human_confirm", Inputs: map[string]dsl.VariableSelector{
|
||||
"prompt": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "create_1", Type: "create_ticket", Inputs: map[string]dsl.VariableSelector{
|
||||
"ticketDraft": {NodeID: "draft_1", Field: "ticketDraft"},
|
||||
"confirmed": {NodeID: "confirm_1", Field: "confirmed"},
|
||||
}},
|
||||
{ID: "end_1", Type: "end"},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "e1", Source: "start_1", Target: "draft_1"},
|
||||
{ID: "e2", Source: "draft_1", Target: "confirm_1"},
|
||||
{ID: "e3", Source: "confirm_1", Target: "create_1"},
|
||||
{ID: "e4", Source: "create_1", Target: "end_1"},
|
||||
},
|
||||
}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if !result.Valid {
|
||||
t.Fatalf("expected confirmed create_ticket to be valid, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionAcceptsDirectHandoffToHuman(t *testing.T) {
|
||||
def := dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: "start"},
|
||||
{ID: "handoff_1", Type: "handoff_to_human", Inputs: map[string]dsl.VariableSelector{
|
||||
"reason": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "end_1", Type: "end"},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "e1", Source: "start_1", Target: "handoff_1"},
|
||||
{ID: "e2", Source: "handoff_1", Target: "end_1"},
|
||||
},
|
||||
}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if !result.Valid {
|
||||
t.Fatalf("expected direct handoff_to_human to be valid, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsConfirmedInputFromNonConfirmNode(t *testing.T) {
|
||||
def := dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: "start"},
|
||||
{ID: "analysis_1", Type: "analyze_conversation", Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "draft_1", Type: "prepare_ticket_draft", Inputs: map[string]dsl.VariableSelector{
|
||||
"issue": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "confirm_1", Type: "human_confirm", Inputs: map[string]dsl.VariableSelector{
|
||||
"prompt": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "create_1", Type: "create_ticket", Inputs: map[string]dsl.VariableSelector{
|
||||
"ticketDraft": {NodeID: "draft_1", Field: "ticketDraft"},
|
||||
"confirmed": {NodeID: "analysis_1", Field: "needTicket"},
|
||||
}},
|
||||
{ID: "end_1", Type: "end"},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "e1", Source: "start_1", Target: "analysis_1"},
|
||||
{ID: "e2", Source: "analysis_1", Target: "draft_1"},
|
||||
{ID: "e3", Source: "draft_1", Target: "confirm_1"},
|
||||
{ID: "e4", Source: "confirm_1", Target: "create_1"},
|
||||
{ID: "e5", Source: "create_1", Target: "end_1"},
|
||||
},
|
||||
}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid {
|
||||
t.Fatalf("expected confirmed input from non-confirm node to be invalid")
|
||||
}
|
||||
if !hasValidationMessage(result, "confirmed input must come from human_confirm.confirmed") {
|
||||
t.Fatalf("expected confirmed-source error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsMissingRequiredInputMapping(t *testing.T) {
|
||||
def := minimalDefinition()
|
||||
def.Nodes[1].Inputs = nil
|
||||
def.Nodes[1].Data.InputsValues = nil
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
@@ -189,8 +50,8 @@ func TestValidateDefinitionRejectsMissingRequiredInputMapping(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsUnknownInputSourceNode(t *testing.T) {
|
||||
def := mappedReplyDefinition()
|
||||
def.Nodes[1].Inputs["replyText"] = dsl.VariableSelector{NodeID: "missing_1", Field: "replyText"}
|
||||
def := minimalDefinition()
|
||||
def.Nodes[1].Data.InputsValues["replyText"] = dsl.RefValue("missing_1", "replyText")
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
@@ -202,9 +63,25 @@ func TestValidateDefinitionRejectsUnknownInputSourceNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsUnavailableInputSourceNode(t *testing.T) {
|
||||
def := minimalDefinition()
|
||||
def.Nodes = append(def.Nodes, node("late_1", "llm_reply", inputs("userMessage", dsl.RefValue("reply_1", "sent")), nil))
|
||||
def.Edges = append(def.Edges, edge("reply_1", "late_1"))
|
||||
def.Nodes[1].Data.InputsValues["replyText"] = dsl.RefValue("late_1", "replyText")
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid {
|
||||
t.Fatalf("expected downstream input source to be invalid")
|
||||
}
|
||||
if !hasValidationMessage(result, "input source node is not available before current node") {
|
||||
t.Fatalf("expected source availability error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsUnknownInputSourceField(t *testing.T) {
|
||||
def := mappedReplyDefinition()
|
||||
def.Nodes[1].Inputs["replyText"] = dsl.VariableSelector{NodeID: "start_1", Field: "missing"}
|
||||
def := minimalDefinition()
|
||||
def.Nodes[1].Data.InputsValues["replyText"] = dsl.RefValue("start_1", "missing")
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
@@ -217,8 +94,8 @@ func TestValidateDefinitionRejectsUnknownInputSourceField(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsIncompatibleInputType(t *testing.T) {
|
||||
def := mappedReplyDefinition()
|
||||
def.Nodes[1].Inputs["replyText"] = dsl.VariableSelector{NodeID: "start_1", Field: "conversationId"}
|
||||
def := minimalDefinition()
|
||||
def.Nodes[1].Data.InputsValues["replyText"] = dsl.RefValue("start_1", "conversationId")
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
@@ -230,69 +107,83 @@ func TestValidateDefinitionRejectsIncompatibleInputType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionAcceptsMappedKnowledgeFlow(t *testing.T) {
|
||||
func TestValidateDefinitionAcceptsConfirmedCreateTicket(t *testing.T) {
|
||||
def := dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
SchemaVersion: dsl.SchemaVersion,
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: "start"},
|
||||
{ID: "retrieve_1", Type: "knowledge_retrieve", Inputs: map[string]dsl.VariableSelector{
|
||||
"query": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "reply_1", Type: "send_reply", Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "end_1", Type: "end"},
|
||||
node("start_1", "start", nil, nil),
|
||||
node("draft_1", "prepare_ticket_draft", inputs("issue", dsl.RefValue("start_1", "userMessage")), nil),
|
||||
node("confirm_1", "human_confirm", inputs("prompt", dsl.RefValue("start_1", "userMessage")), nil),
|
||||
node("create_1", "create_ticket", map[string]dsl.Value{
|
||||
"ticketDraft": dsl.RefValue("draft_1", "ticketDraft"),
|
||||
"confirmed": dsl.RefValue("confirm_1", "confirmed"),
|
||||
}, nil),
|
||||
node("end_1", "end", nil, nil),
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "e1", Source: "start_1", Target: "retrieve_1"},
|
||||
{ID: "e2", Source: "retrieve_1", Target: "reply_1"},
|
||||
{ID: "e3", Source: "reply_1", Target: "end_1"},
|
||||
edge("start_1", "draft_1"),
|
||||
edge("draft_1", "confirm_1"),
|
||||
edge("confirm_1", "create_1"),
|
||||
edge("create_1", "end_1"),
|
||||
},
|
||||
}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if !result.Valid {
|
||||
t.Fatalf("expected mapped knowledge flow to be valid, got %#v", result.Errors)
|
||||
t.Fatalf("expected confirmed create_ticket to be valid, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsUnknownConditionOperator(t *testing.T) {
|
||||
def := conditionDefinition()
|
||||
var config dsl.ConditionConfig
|
||||
if err := json.Unmarshal(def.Nodes[1].Config, &config); err != nil {
|
||||
t.Fatalf("unmarshal condition config: %v", err)
|
||||
func TestValidateDefinitionRejectsConfirmedInputFromNonConfirmNode(t *testing.T) {
|
||||
def := minimalDefinition()
|
||||
def.Nodes = []dsl.Node{
|
||||
node("start_1", "start", nil, nil),
|
||||
node("draft_1", "prepare_ticket_draft", inputs("issue", dsl.RefValue("start_1", "userMessage")), nil),
|
||||
node("create_1", "create_ticket", map[string]dsl.Value{
|
||||
"ticketDraft": dsl.RefValue("draft_1", "ticketDraft"),
|
||||
"confirmed": dsl.RefValue("start_1", "userMessage"),
|
||||
}, nil),
|
||||
node("end_1", "end", nil, nil),
|
||||
}
|
||||
config.Branches[0].Condition.Operator = "regex"
|
||||
raw, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal condition config: %v", err)
|
||||
def.Edges = []dsl.Edge{
|
||||
edge("start_1", "draft_1"),
|
||||
edge("draft_1", "create_1"),
|
||||
edge("create_1", "end_1"),
|
||||
}
|
||||
def.Nodes[1].Config = raw
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid {
|
||||
t.Fatalf("expected unknown condition operator to be invalid")
|
||||
t.Fatalf("expected confirmed input from non-confirm node to be invalid")
|
||||
}
|
||||
if !hasValidationMessage(result, "unsupported condition operator") {
|
||||
t.Fatalf("expected condition operator error, got %#v", result.Errors)
|
||||
if !hasValidationMessage(result, "confirmed input must come from human_confirm.confirmed") {
|
||||
t.Fatalf("expected confirmed-source error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsConditionBranchTargetWithoutEdge(t *testing.T) {
|
||||
def := conditionDefinition()
|
||||
def.Edges = []dsl.Edge{edge("start_1", "condition_1")}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid {
|
||||
t.Fatalf("expected condition branch target without edge to be invalid")
|
||||
}
|
||||
if !hasValidationMessage(result, "condition branch target must have an outgoing edge") {
|
||||
t.Fatalf("expected branch edge error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsUnknownConditionVariable(t *testing.T) {
|
||||
def := conditionDefinition()
|
||||
var config dsl.ConditionConfig
|
||||
if err := json.Unmarshal(def.Nodes[1].Config, &config); err != nil {
|
||||
if err := json.Unmarshal(def.Nodes[1].Data.Config, &config); err != nil {
|
||||
t.Fatalf("unmarshal condition config: %v", err)
|
||||
}
|
||||
config.Branches[0].Condition.Left.Field = "missing"
|
||||
raw, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal condition config: %v", err)
|
||||
}
|
||||
def.Nodes[1].Config = raw
|
||||
config.Branches[0].Condition.Left = &dsl.Value{Type: dsl.ValueTypeRef, Content: []string{"start_1", "missing"}}
|
||||
def.Nodes[1].Data.Config = mustJSON(config)
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
@@ -304,123 +195,30 @@ func TestValidateDefinitionRejectsUnknownConditionVariable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsInvalidConditionEnumValue(t *testing.T) {
|
||||
def := policyConditionDefinition("unknown_action")
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid {
|
||||
t.Fatalf("expected invalid enum condition value to be rejected")
|
||||
}
|
||||
if !hasValidationMessage(result, "condition comparison value is not allowed") {
|
||||
t.Fatalf("expected condition enum value error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsConditionDefaultBranchBeforeLast(t *testing.T) {
|
||||
def := conditionDefinition()
|
||||
var config dsl.ConditionConfig
|
||||
if err := json.Unmarshal(def.Nodes[1].Config, &config); err != nil {
|
||||
t.Fatalf("unmarshal condition config: %v", err)
|
||||
}
|
||||
config.Branches[0], config.Branches[1] = config.Branches[1], config.Branches[0]
|
||||
raw, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal condition config: %v", err)
|
||||
}
|
||||
def.Nodes[1].Config = raw
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid {
|
||||
t.Fatalf("expected default branch before last to be invalid")
|
||||
}
|
||||
if !hasValidationMessage(result, "default condition branch must be last") {
|
||||
t.Fatalf("expected default branch order error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func minimalDefinition() dsl.Definition {
|
||||
return dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
SchemaVersion: dsl.SchemaVersion,
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: "start"},
|
||||
{ID: "reply_1", Type: "send_reply", Config: json.RawMessage(`{"text":"hello"}`), Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "end_1", Type: "end"},
|
||||
node("start_1", "start", nil, nil),
|
||||
node("reply_1", "send_reply", inputs("replyText", dsl.RefValue("start_1", "userMessage")), map[string]any{"text": "hello"}),
|
||||
node("end_1", "end", nil, nil),
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "e1", Source: "start_1", Target: "reply_1"},
|
||||
{ID: "e2", Source: "reply_1", Target: "end_1"},
|
||||
edge("start_1", "reply_1"),
|
||||
edge("reply_1", "end_1"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func policyConditionDefinition(action any) dsl.Definition {
|
||||
conditionConfig, _ := json.Marshal(dsl.ConditionConfig{
|
||||
Branches: []dsl.ConditionBranch{
|
||||
{
|
||||
ID: "direct",
|
||||
Name: "Direct",
|
||||
TargetNodeID: "end_1",
|
||||
Condition: &dsl.Condition{
|
||||
Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"},
|
||||
Operator: "eq",
|
||||
Right: action,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "default",
|
||||
Name: "Default",
|
||||
TargetNodeID: "end_1",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
return dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: "start"},
|
||||
{ID: "understanding_1", Type: "conversation_understanding", Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "policy_1", Type: "reply_policy", Inputs: map[string]dsl.VariableSelector{
|
||||
"messageIntent": {NodeID: "understanding_1", Field: "messageIntent"},
|
||||
"answerScope": {NodeID: "understanding_1", Field: "answerScope"},
|
||||
}},
|
||||
{ID: "condition_1", Type: "condition", Config: conditionConfig},
|
||||
{ID: "end_1", Type: "end"},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "e1", Source: "start_1", Target: "understanding_1"},
|
||||
{ID: "e2", Source: "understanding_1", Target: "policy_1"},
|
||||
{ID: "e3", Source: "policy_1", Target: "condition_1"},
|
||||
{ID: "e4", Source: "condition_1", Target: "end_1"},
|
||||
{ID: "e5", Source: "condition_1", Target: "end_1"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func mappedReplyDefinition() dsl.Definition {
|
||||
def := minimalDefinition()
|
||||
def.Nodes[1].Inputs = map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "start_1", Field: "userMessage"},
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func conditionDefinition() dsl.Definition {
|
||||
conditionConfig, _ := json.Marshal(dsl.ConditionConfig{
|
||||
conditionConfig := dsl.ConditionConfig{
|
||||
Branches: []dsl.ConditionBranch{
|
||||
{
|
||||
ID: "hello",
|
||||
Name: "Hello",
|
||||
TargetNodeID: "end_1",
|
||||
Condition: &dsl.Condition{
|
||||
Left: &dsl.VariableSelector{NodeID: "start_1", Field: "userMessage"},
|
||||
Left: &dsl.Value{Type: dsl.ValueTypeRef, Content: []string{"start_1", "userMessage"}},
|
||||
Operator: "eq",
|
||||
Right: "hello",
|
||||
},
|
||||
@@ -432,23 +230,54 @@ func conditionDefinition() dsl.Definition {
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
SchemaVersion: dsl.SchemaVersion,
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: "start"},
|
||||
{ID: "condition_1", Type: "condition", Config: conditionConfig},
|
||||
{ID: "end_1", Type: "end"},
|
||||
node("start_1", "start", nil, nil),
|
||||
node("condition_1", "condition", nil, conditionConfig),
|
||||
node("end_1", "end", nil, nil),
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "e1", Source: "start_1", Target: "condition_1"},
|
||||
{ID: "e2", Source: "condition_1", Target: "end_1"},
|
||||
{ID: "e3", Source: "condition_1", Target: "end_1"},
|
||||
edge("start_1", "condition_1"),
|
||||
edge("condition_1", "end_1"),
|
||||
edge("condition_1", "end_1"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func node(id string, nodeType string, inputValues map[string]dsl.Value, config any) dsl.Node {
|
||||
return dsl.Node{
|
||||
ID: id,
|
||||
Type: nodeType,
|
||||
Meta: dsl.NodeMeta{Position: dsl.Position{X: 0, Y: 0}},
|
||||
Data: dsl.NodeData{
|
||||
Title: nodeType,
|
||||
Config: mustJSON(config),
|
||||
InputsValues: inputValues,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func edge(source string, target string) dsl.Edge {
|
||||
return dsl.Edge{SourceNodeID: source, TargetNodeID: target}
|
||||
}
|
||||
|
||||
func inputs(name string, value dsl.Value) map[string]dsl.Value {
|
||||
return map[string]dsl.Value{name: value}
|
||||
}
|
||||
|
||||
func mustJSON(value any) json.RawMessage {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func hasValidationMessage(result validator.Result, want string) bool {
|
||||
for _, item := range result.Errors {
|
||||
if strings.Contains(item.Message, want) {
|
||||
|
||||
Reference in New Issue
Block a user