新版本流程编辑器
This commit is contained in:
@@ -265,11 +265,13 @@ func (s *runState) startNodeID() string {
|
||||
func (e *Executor) executeNode(ctx context.Context, state *runState, node dsl.Node) error {
|
||||
switch node.Type {
|
||||
case workflowregistry.NodeTypeStart:
|
||||
userMessage := strings.TrimSpace(state.input.UserMessage.Content)
|
||||
state.setNodeVars(node.ID, map[string]any{
|
||||
"conversationId": state.input.Conversation.ID,
|
||||
"messageId": state.input.UserMessage.ID,
|
||||
"aiAgentId": state.input.AIAgent.ID,
|
||||
"userMessage": strings.TrimSpace(state.input.UserMessage.Content),
|
||||
"userMessage": userMessage,
|
||||
"query": userMessage,
|
||||
"conversationState": state.input.Conversation.Status,
|
||||
})
|
||||
case workflowregistry.NodeTypeConversationUnderstanding:
|
||||
@@ -292,6 +294,8 @@ func (e *Executor) executeNode(ctx context.Context, state *runState, node dsl.No
|
||||
return e.executeCreateTicket(state, node)
|
||||
case workflowregistry.NodeTypeLLMReply:
|
||||
return e.executeLLMReply(ctx, state, node)
|
||||
case workflowregistry.NodeTypeLLM:
|
||||
return e.executeOfficialLLM(ctx, state, node)
|
||||
case workflowregistry.NodeTypeSendReply:
|
||||
replyText := strings.TrimSpace(toString(state.resolveInput(node, "replyText")))
|
||||
state.result.ReplyText = replyText
|
||||
@@ -302,13 +306,38 @@ func (e *Executor) executeNode(ctx context.Context, state *runState, node dsl.No
|
||||
case workflowregistry.NodeTypeHandoffToHuman:
|
||||
return e.executeHandoffToHuman(state, node)
|
||||
case workflowregistry.NodeTypeEnd:
|
||||
state.setNodeVars(node.ID, map[string]any{"status": "completed"})
|
||||
outputs := state.resolvedInputs(node)
|
||||
outputs["status"] = "completed"
|
||||
state.setNodeVars(node.ID, outputs)
|
||||
if state.result.ReplyText == "" {
|
||||
state.result.ReplyText = strings.TrimSpace(toString(outputs["result"]))
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported workflow node type: %s", node.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) executeOfficialLLM(ctx context.Context, state *runState, node dsl.Node) error {
|
||||
systemPrompt := strings.TrimSpace(toString(state.resolveInput(node, "systemPrompt")))
|
||||
if systemPrompt == "" {
|
||||
systemPrompt = strings.TrimSpace(state.input.AIAgent.SystemPrompt)
|
||||
}
|
||||
userPrompt := strings.TrimSpace(toString(state.resolveInput(node, "prompt")))
|
||||
if userPrompt == "" {
|
||||
userPrompt = strings.TrimSpace(state.input.UserMessage.Content)
|
||||
}
|
||||
result, err := ai.LLM.ChatWithConfig(ctx, state.input.AIConfig, systemPrompt, userPrompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.result.PromptTokens += result.PromptTokens
|
||||
state.result.CompletionTokens += result.CompletionTokens
|
||||
state.result.ReplyText = strings.TrimSpace(result.Content)
|
||||
state.setNodeVars(node.ID, map[string]any{"result": result.Content})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) executeConversationUnderstanding(state *runState, node dsl.Node) error {
|
||||
rawMessage := strings.TrimSpace(toString(state.resolveInput(node, "userMessage")))
|
||||
if rawMessage == "" {
|
||||
@@ -862,6 +891,9 @@ func (s *runState) nextNodeID(sourceNodeID string) (string, bool, error) {
|
||||
if strings.TrimSpace(node.Type) != workflowregistry.NodeTypeCondition {
|
||||
return strings.TrimSpace(edges[0].TargetNodeID), true, nil
|
||||
}
|
||||
if rawConditions, ok := node.Data.Extra["conditions"]; ok {
|
||||
return s.nextFlowGramConditionNodeID(sourceNodeID, rawConditions)
|
||||
}
|
||||
config := dsl.ConditionConfig{}
|
||||
if len(node.Data.Config) > 0 {
|
||||
if err := json.Unmarshal(node.Data.Config, &config); err != nil {
|
||||
@@ -913,6 +945,100 @@ func (s *runState) nextNodeID(sourceNodeID string) (string, bool, error) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (s *runState) nextFlowGramConditionNodeID(sourceNodeID string, raw json.RawMessage) (string, bool, error) {
|
||||
var conditions []dsl.FlowGramConditionItem
|
||||
if err := json.Unmarshal(raw, &conditions); err != nil {
|
||||
return "", false, fmt.Errorf("invalid FlowGram condition data: %w", err)
|
||||
}
|
||||
evaluations := make([]conditionEvaluation, 0, len(conditions))
|
||||
for _, item := range conditions {
|
||||
matched, evaluation, err := s.evaluateFlowGramCondition(sourceNodeID, item)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
evaluations = append(evaluations, evaluation)
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
if edge, ok := s.edgeForSourcePort(sourceNodeID, item.Key); ok {
|
||||
targetNodeID := strings.TrimSpace(edge.TargetNodeID)
|
||||
s.branchDecisions[sourceNodeID] = branchDecision{
|
||||
SelectedEdgeID: strings.TrimSpace(item.Key),
|
||||
SelectedBranchID: strings.TrimSpace(item.Key),
|
||||
SelectedTargetNodeID: targetNodeID,
|
||||
Reason: "condition branch matched",
|
||||
Evaluations: evaluations,
|
||||
}
|
||||
return targetNodeID, true, nil
|
||||
}
|
||||
}
|
||||
if edge, ok := s.edgeForSourcePort(sourceNodeID, "else"); ok {
|
||||
targetNodeID := strings.TrimSpace(edge.TargetNodeID)
|
||||
s.branchDecisions[sourceNodeID] = branchDecision{
|
||||
SelectedEdgeID: "else",
|
||||
SelectedBranchID: "else",
|
||||
SelectedTargetNodeID: targetNodeID,
|
||||
Reason: "no condition branch matched; selected else branch",
|
||||
Evaluations: evaluations,
|
||||
}
|
||||
return targetNodeID, true, nil
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (s *runState) evaluateFlowGramCondition(sourceNodeID string, item dsl.FlowGramConditionItem) (bool, conditionEvaluation, error) {
|
||||
left := s.resolveValue(item.Value.Left)
|
||||
right := s.resolveValue(item.Value.Right)
|
||||
operator := strings.TrimSpace(item.Value.Operator)
|
||||
evaluation := conditionEvaluation{
|
||||
EdgeID: strings.TrimSpace(item.Key),
|
||||
BranchID: strings.TrimSpace(item.Key),
|
||||
SourceNodeID: sourceNodeID,
|
||||
Operator: operator,
|
||||
LeftValue: left,
|
||||
RightValue: right,
|
||||
}
|
||||
evaluation.SourceNodeID, evaluation.SourceField, _ = item.Value.Left.Ref()
|
||||
var matched bool
|
||||
switch operator {
|
||||
case "eq", "equals":
|
||||
matched = compareString(left, right) == 0
|
||||
case "neq", "not_equals":
|
||||
matched = compareString(left, right) != 0
|
||||
case "contains":
|
||||
matched = strings.Contains(toString(left), toString(right))
|
||||
case "exists":
|
||||
matched = exists(left)
|
||||
case "not_exists":
|
||||
matched = !exists(left)
|
||||
case "truthy", "is_true":
|
||||
matched = truthy(left)
|
||||
case "falsy", "is_false":
|
||||
matched = !truthy(left)
|
||||
case "gt":
|
||||
matched = compareNumber(left, right) > 0
|
||||
case "gte":
|
||||
matched = compareNumber(left, right) >= 0
|
||||
case "lt":
|
||||
matched = compareNumber(left, right) < 0
|
||||
case "lte":
|
||||
matched = compareNumber(left, right) <= 0
|
||||
default:
|
||||
return false, evaluation, fmt.Errorf("unsupported workflow condition operator: %s", operator)
|
||||
}
|
||||
evaluation.Matched = matched
|
||||
return matched, evaluation, nil
|
||||
}
|
||||
|
||||
func (s *runState) edgeForSourcePort(sourceNodeID string, sourcePortID string) (dsl.Edge, bool) {
|
||||
for _, edge := range s.outgoing[sourceNodeID] {
|
||||
if strings.TrimSpace(edge.SourcePortID) == strings.TrimSpace(sourcePortID) {
|
||||
return edge, true
|
||||
}
|
||||
}
|
||||
return dsl.Edge{}, false
|
||||
}
|
||||
|
||||
func (s *runState) evaluateConditionBranch(sourceNodeID string, branch dsl.ConditionBranch) (bool, conditionEvaluation, error) {
|
||||
condition := branch.Condition
|
||||
targetNodeID := strings.TrimSpace(branch.TargetNodeID)
|
||||
|
||||
@@ -44,6 +44,61 @@ func TestExecutorRoutesByConditionNodeBranch(t *testing.T) {
|
||||
assertPath(t, result.NodePath, []string{"start_1", "condition_1", "vip_reply", "send_vip", "end_1"})
|
||||
}
|
||||
|
||||
func TestExecutorRoutesOfficialFlowGramConditionPorts(t *testing.T) {
|
||||
definition := officialFlowGramConditionDefinition()
|
||||
result, err := NewExecutor().Execute(context.Background(), Input{
|
||||
Definition: definition,
|
||||
UserMessage: models.Message{Content: "hello FlowGram"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute workflow: %v", err)
|
||||
}
|
||||
assertPath(t, result.NodePath, []string{"start_0", "condition_0", "matched_end"})
|
||||
|
||||
result, err = NewExecutor().Execute(context.Background(), Input{
|
||||
Definition: definition,
|
||||
UserMessage: models.Message{Content: "goodbye"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute workflow else branch: %v", err)
|
||||
}
|
||||
assertPath(t, result.NodePath, []string{"start_0", "condition_0", "else_end"})
|
||||
}
|
||||
|
||||
func officialFlowGramConditionDefinition() dsl.Definition {
|
||||
return dsl.Definition{
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_0", Type: workflowregistry.NodeTypeStart, Data: dsl.NodeData{Title: "Start"}},
|
||||
{
|
||||
ID: "condition_0",
|
||||
Type: workflowregistry.NodeTypeCondition,
|
||||
Data: dsl.NodeData{
|
||||
Title: "Condition",
|
||||
Extra: map[string]json.RawMessage{
|
||||
"conditions": mustMarshalWorkflowTestConfig([]dsl.FlowGramConditionItem{
|
||||
{
|
||||
Key: "if_0",
|
||||
Value: dsl.FlowGramCondition{
|
||||
Left: dsl.RefValue("start_0", "query"),
|
||||
Operator: "contains",
|
||||
Right: dsl.ConstantValue("hello"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{ID: "matched_end", Type: workflowregistry.NodeTypeEnd, Data: dsl.NodeData{Title: "End"}},
|
||||
{ID: "else_end", Type: workflowregistry.NodeTypeEnd, Data: dsl.NodeData{Title: "End"}},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{SourceNodeID: "start_0", TargetNodeID: "condition_0"},
|
||||
{SourceNodeID: "condition_0", TargetNodeID: "matched_end", SourcePortID: "if_0"},
|
||||
{SourceNodeID: "condition_0", TargetNodeID: "else_end", SourcePortID: "else"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutorConditionNodeTraceExplainsMatchedEdge(t *testing.T) {
|
||||
result, err := NewExecutor().Execute(context.Background(), Input{
|
||||
Definition: conditionalReplyDefinition(),
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -339,6 +339,63 @@ func (s *aiWorkflowService) PublishWorkflow(req request.PublishAIWorkflowRequest
|
||||
}
|
||||
|
||||
func defaultAgentWorkflowDefinition() dsl.Definition {
|
||||
return officialDefaultAgentWorkflowDefinition()
|
||||
}
|
||||
|
||||
func officialDefaultAgentWorkflowDefinition() dsl.Definition {
|
||||
return dsl.Definition{
|
||||
Nodes: []dsl.Node{
|
||||
{
|
||||
ID: "start_0",
|
||||
Type: workflowregistry.NodeTypeStart,
|
||||
Meta: dsl.NodeMeta{Position: dsl.Position{X: 180, Y: 300}},
|
||||
Data: dsl.NodeData{
|
||||
Title: "Start",
|
||||
Outputs: json.RawMessage(`{"type":"object","properties":{"query":{"type":"string","default":"Hello Flow."}}}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "llm_0",
|
||||
Type: workflowregistry.NodeTypeLLM,
|
||||
Meta: dsl.NodeMeta{Position: dsl.Position{X: 640, Y: 220}},
|
||||
Data: dsl.NodeData{
|
||||
Title: "LLM",
|
||||
InputsValues: map[string]dsl.Value{
|
||||
"modelName": dsl.ConstantValue("gpt-3.5-turbo"),
|
||||
"apiKey": dsl.ConstantValue("sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"),
|
||||
"apiHost": dsl.ConstantValue("https://mock-ai-url/api/v3"),
|
||||
"temperature": dsl.ConstantValue(0.5),
|
||||
"systemPrompt": dsl.TemplateValue(
|
||||
"# Role\nYou are an AI assistant.\n",
|
||||
),
|
||||
"prompt": dsl.TemplateValue(""),
|
||||
},
|
||||
Inputs: json.RawMessage(`{"type":"object","required":["modelName","apiKey","apiHost","temperature","prompt"],"properties":{"modelName":{"type":"string"},"apiKey":{"type":"string"},"apiHost":{"type":"string"},"temperature":{"type":"number"},"systemPrompt":{"type":"string","extra":{"formComponent":"prompt-editor"}},"prompt":{"type":"string","extra":{"formComponent":"prompt-editor"}}}}`),
|
||||
Outputs: json.RawMessage(`{"type":"object","properties":{"result":{"type":"string"}}}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "end_0",
|
||||
Type: workflowregistry.NodeTypeEnd,
|
||||
Meta: dsl.NodeMeta{Position: dsl.Position{X: 1100, Y: 300}},
|
||||
Data: dsl.NodeData{
|
||||
Title: "End",
|
||||
InputsValues: map[string]dsl.Value{
|
||||
"result": dsl.RefValue("llm_0", "result"),
|
||||
},
|
||||
Inputs: json.RawMessage(`{"type":"object","properties":{"result":{"type":"string"}}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{SourceNodeID: "start_0", TargetNodeID: "llm_0"},
|
||||
{SourceNodeID: "llm_0", TargetNodeID: "end_0"},
|
||||
},
|
||||
GlobalVariable: json.RawMessage(`{"type":"object","properties":{}}`),
|
||||
}
|
||||
}
|
||||
|
||||
func legacyDefaultAgentWorkflowDefinition() dsl.Definition {
|
||||
return dsl.Definition{
|
||||
SchemaVersion: dsl.SchemaVersion,
|
||||
Nodes: []dsl.Node{
|
||||
|
||||
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -40,6 +41,26 @@ func TestAIWorkflowServiceValidateDefinitionReportsErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIWorkflowServiceDefaultDefinitionUsesOfficialFlowGramModel(t *testing.T) {
|
||||
definition := defaultAgentWorkflowDefinition()
|
||||
if definition.SchemaVersion != 0 {
|
||||
t.Fatalf("official FlowGram definition must not contain the legacy schemaVersion, got %d", definition.SchemaVersion)
|
||||
}
|
||||
if len(definition.Nodes) != 3 {
|
||||
t.Fatalf("default node count = %d, want 3", len(definition.Nodes))
|
||||
}
|
||||
nodeTypes := []string{definition.Nodes[0].Type, definition.Nodes[1].Type, definition.Nodes[2].Type}
|
||||
if strings.Join(nodeTypes, ",") != "start,llm,end" {
|
||||
t.Fatalf("default node types = %v, want [start llm end]", nodeTypes)
|
||||
}
|
||||
if len(definition.GlobalVariable) == 0 {
|
||||
t.Fatalf("official FlowGram globalVariable is required")
|
||||
}
|
||||
if result := AIWorkflowService.ValidateDefinition(definition); !result.Valid {
|
||||
t.Fatalf("default official FlowGram definition is invalid: %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIWorkflowServicePublishCreatesImmutableVersion(t *testing.T) {
|
||||
setupAIWorkflowTestDB(t)
|
||||
operator := aiWorkflowTestOperator()
|
||||
|
||||
Reference in New Issue
Block a user