Add workflow runtime execution and condition validation
This commit is contained in:
@@ -2,9 +2,16 @@ package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/ai/runtime/executor"
|
||||
workflowexecutor "agent-desk/internal/ai/runtime/workflow"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/utils"
|
||||
"agent-desk/internal/repositories"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -24,32 +31,29 @@ func NewService() *Service {
|
||||
|
||||
func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) {
|
||||
req.UserMessage.Content = utils.BuildRuntimeMessageText(req.UserMessage.MessageType, req.UserMessage.Content)
|
||||
aiAgent, err := prepareWorkflowAgent(req.AIAgent)
|
||||
aiAgent, workflow, err := prepareWorkflowAgent(req.AIAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AIAgent = aiAgent
|
||||
toolSet, err := s.prepare.prepareToolsForRun(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.ToolSet = toolSet
|
||||
summary, err := s.runtime.ExecuteRun(ctx, executor.RunInput{
|
||||
workflowResult, err := workflowexecutor.NewExecutor().Execute(ctx, workflowexecutor.Input{
|
||||
Definition: workflow.Definition,
|
||||
Conversation: req.Conversation,
|
||||
UserMessage: req.UserMessage,
|
||||
AIAgent: req.AIAgent,
|
||||
AIConfig: req.AIConfig,
|
||||
CheckPointID: req.CheckPointID,
|
||||
ToolSet: req.ToolSet,
|
||||
})
|
||||
if err != nil {
|
||||
return toSummary(summary), err
|
||||
return nil, err
|
||||
}
|
||||
return toSummary(summary), nil
|
||||
if err := writeWorkflowRun(req, workflow, workflowResult, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toWorkflowSummary(workflowResult, req.AIConfig.ModelName), nil
|
||||
}
|
||||
|
||||
func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, error) {
|
||||
aiAgent, err := prepareWorkflowAgent(req.AIAgent)
|
||||
aiAgent, _, err := prepareWorkflowAgent(req.AIAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,3 +76,65 @@ func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, erro
|
||||
}
|
||||
return toSummary(summary), nil
|
||||
}
|
||||
|
||||
func toWorkflowSummary(result *workflowexecutor.Result, modelName string) *Summary {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
trace := map[string]any{
|
||||
"status": result.Status,
|
||||
"nodePath": result.NodePath,
|
||||
}
|
||||
traceData, _ := json.Marshal(trace)
|
||||
return &Summary{
|
||||
Status: result.Status,
|
||||
ReplyText: result.ReplyText,
|
||||
ModelName: modelName,
|
||||
PromptTokens: result.PromptTokens,
|
||||
CompletionTokens: result.CompletionTokens,
|
||||
RetrieverCount: result.RetrieverCount,
|
||||
TraceData: string(traceData),
|
||||
}
|
||||
}
|
||||
|
||||
func writeWorkflowRun(req Request, workflow resolvedWorkflow, result *workflowexecutor.Result, errorMessage string) error {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
endedAt := now
|
||||
nodeTypes := make(map[string]string, len(workflow.Definition.Nodes))
|
||||
for _, node := range workflow.Definition.Nodes {
|
||||
nodeTypes[node.ID] = node.Type
|
||||
}
|
||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
run := &models.AIWorkflowRun{
|
||||
WorkflowID: workflow.WorkflowID,
|
||||
WorkflowVersionID: workflow.VersionID,
|
||||
ConversationID: req.Conversation.ID,
|
||||
AIAgentID: req.AIAgent.ID,
|
||||
MessageID: req.UserMessage.ID,
|
||||
Status: 1,
|
||||
StartedAt: now,
|
||||
EndedAt: &endedAt,
|
||||
ErrorMessage: errorMessage,
|
||||
}
|
||||
if err := repositories.AIWorkflowRunRepository.Create(ctx.Tx, run); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, nodeID := range result.NodePath {
|
||||
nodeRun := &models.AIWorkflowNodeRun{
|
||||
WorkflowRunID: run.ID,
|
||||
NodeID: nodeID,
|
||||
NodeType: nodeTypes[nodeID],
|
||||
Status: 1,
|
||||
StartedAt: now,
|
||||
EndedAt: &endedAt,
|
||||
}
|
||||
if err := repositories.AIWorkflowNodeRunRepository.Create(ctx.Tx, nodeRun); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent models.AIAgent) []strin
|
||||
ret = append(ret, graphTools...)
|
||||
}
|
||||
}
|
||||
if result, err := resolveAgentWorkflow(aiAgent); err == nil {
|
||||
ret = append(ret, result.ToolCodes...)
|
||||
if workflow, err := resolveAgentWorkflow(aiAgent); err == nil {
|
||||
ret = append(ret, workflow.Compiled.ToolCodes...)
|
||||
}
|
||||
return toolx.NormalizeToolCodes(ret)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -104,7 +105,7 @@ func TestPrepareWorkflowAgentAppendsPublishedWorkflow(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
agent, err := prepareWorkflowAgent(models.AIAgent{
|
||||
agent, _, err := prepareWorkflowAgent(models.AIAgent{
|
||||
SystemPrompt: "Base prompt.",
|
||||
WorkflowVersionID: version.ID,
|
||||
})
|
||||
@@ -120,7 +121,7 @@ func TestPrepareWorkflowAgentAppendsPublishedWorkflow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareWorkflowAgentRejectsMissingPublishedWorkflow(t *testing.T) {
|
||||
_, err := prepareWorkflowAgent(models.AIAgent{})
|
||||
_, _, err := prepareWorkflowAgent(models.AIAgent{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected missing workflow version error")
|
||||
}
|
||||
@@ -143,7 +144,7 @@ func TestPrepareWorkflowAgentRejectsDeletedPublishedWorkflow(t *testing.T) {
|
||||
t.Fatalf("delete workflow version: %v", err)
|
||||
}
|
||||
|
||||
_, err := prepareWorkflowAgent(models.AIAgent{WorkflowVersionID: version.ID})
|
||||
_, _, err := prepareWorkflowAgent(models.AIAgent{WorkflowVersionID: version.ID})
|
||||
if err == nil {
|
||||
t.Fatalf("expected invalid workflow version error")
|
||||
}
|
||||
@@ -152,13 +153,61 @@ func TestPrepareWorkflowAgentRejectsDeletedPublishedWorkflow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceRunExecutesPublishedWorkflow(t *testing.T) {
|
||||
setupWorkflowRuntimeTestDB(t)
|
||||
version := createWorkflowRuntimeTestVersion(t, dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: workflowregistry.NodeTypeStart, Name: "Start"},
|
||||
{ID: "reply_1", Type: workflowregistry.NodeTypeLLMReply, Name: "Reply", Config: []byte(`{"staticReply":"workflow reply"}`)},
|
||||
{ID: "send_1", Type: workflowregistry.NodeTypeSendReply, Name: "Send", Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "reply_1", Field: "replyText"},
|
||||
}},
|
||||
{ID: "end_1", Type: workflowregistry.NodeTypeEnd, Name: "End"},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "edge_start_reply", Source: "start_1", Target: "reply_1"},
|
||||
{ID: "edge_reply_send", Source: "reply_1", Target: "send_1"},
|
||||
{ID: "edge_send_end", Source: "send_1", Target: "end_1"},
|
||||
},
|
||||
})
|
||||
|
||||
summary, err := NewService().Run(context.Background(), Request{
|
||||
UserMessage: models.Message{Content: "hello"},
|
||||
AIAgent: models.AIAgent{
|
||||
WorkflowVersionID: version.ID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("run workflow: %v", err)
|
||||
}
|
||||
if summary.ReplyText != "workflow reply" {
|
||||
t.Fatalf("unexpected workflow reply: %q", summary.ReplyText)
|
||||
}
|
||||
var runCount int64
|
||||
if err := sqls.DB().Model(&models.AIWorkflowRun{}).Count(&runCount).Error; err != nil {
|
||||
t.Fatalf("count workflow runs: %v", err)
|
||||
}
|
||||
if runCount != 1 {
|
||||
t.Fatalf("expected one workflow run, got %d", runCount)
|
||||
}
|
||||
var nodeRunCount int64
|
||||
if err := sqls.DB().Model(&models.AIWorkflowNodeRun{}).Count(&nodeRunCount).Error; err != nil {
|
||||
t.Fatalf("count workflow node runs: %v", err)
|
||||
}
|
||||
if nodeRunCount != 4 {
|
||||
t.Fatalf("expected four workflow node runs, got %d", nodeRunCount)
|
||||
}
|
||||
}
|
||||
|
||||
func setupWorkflowRuntimeTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&models.AIWorkflowVersion{}); err != nil {
|
||||
if err := db.AutoMigrate(&models.AIWorkflowVersion{}, &models.AIWorkflowRun{}, &models.AIWorkflowNodeRun{}); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
sqls.SetDB(db)
|
||||
|
||||
@@ -14,35 +14,47 @@ import (
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
func resolveAgentWorkflow(aiAgent models.AIAgent) (compiler.Result, error) {
|
||||
type resolvedWorkflow struct {
|
||||
Definition dsl.Definition
|
||||
Compiled compiler.Result
|
||||
WorkflowID int64
|
||||
VersionID int64
|
||||
}
|
||||
|
||||
func resolveAgentWorkflow(aiAgent models.AIAgent) (resolvedWorkflow, error) {
|
||||
if aiAgent.WorkflowVersionID <= 0 {
|
||||
return compiler.Result{}, errorsx.InvalidParam("workflow version is required")
|
||||
return resolvedWorkflow{}, errorsx.InvalidParam("workflow version is required")
|
||||
}
|
||||
version := repositories.AIWorkflowVersionRepository.Get(sqls.DB(), aiAgent.WorkflowVersionID)
|
||||
if version == nil || version.Status != enums.StatusOk {
|
||||
return compiler.Result{}, errorsx.InvalidParam("workflow version does not exist")
|
||||
return resolvedWorkflow{}, errorsx.InvalidParam("workflow version does not exist")
|
||||
}
|
||||
var def dsl.Definition
|
||||
if err := json.Unmarshal([]byte(version.Definition), &def); err != nil {
|
||||
return compiler.Result{}, errorsx.InvalidParam("workflow definition is invalid")
|
||||
return resolvedWorkflow{}, errorsx.InvalidParam("workflow definition is invalid")
|
||||
}
|
||||
return compiler.Compile(def), nil
|
||||
return resolvedWorkflow{
|
||||
Definition: def,
|
||||
Compiled: compiler.Compile(def),
|
||||
WorkflowID: version.WorkflowID,
|
||||
VersionID: version.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func prepareWorkflowAgent(aiAgent models.AIAgent) (models.AIAgent, error) {
|
||||
result, err := resolveAgentWorkflow(aiAgent)
|
||||
func prepareWorkflowAgent(aiAgent models.AIAgent) (models.AIAgent, resolvedWorkflow, error) {
|
||||
workflow, err := resolveAgentWorkflow(aiAgent)
|
||||
if err != nil {
|
||||
return aiAgent, err
|
||||
return aiAgent, resolvedWorkflow{}, err
|
||||
}
|
||||
if strings.TrimSpace(result.Appendix) == "" {
|
||||
return aiAgent, nil
|
||||
if strings.TrimSpace(workflow.Compiled.Appendix) == "" {
|
||||
return aiAgent, workflow, nil
|
||||
}
|
||||
prompt := strings.TrimSpace(aiAgent.SystemPrompt)
|
||||
appendix := strings.TrimSpace(result.Appendix)
|
||||
appendix := strings.TrimSpace(workflow.Compiled.Appendix)
|
||||
if prompt == "" {
|
||||
aiAgent.SystemPrompt = appendix
|
||||
return aiAgent, nil
|
||||
return aiAgent, workflow, nil
|
||||
}
|
||||
aiAgent.SystemPrompt = prompt + "\n\n" + appendix
|
||||
return aiAgent, nil
|
||||
return aiAgent, workflow, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user