Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions server/app/models/agents/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ class Component < ApplicationRecord
inverse_of: :target_component

enum component_type: { chat_input: 0, chat_output: 1, data_storage: 2, llm_model: 3, prompt_template: 4,
<<<<<<< HEAD
vector_store: 5, python_custom: 6 }
=======
vector_store: 5, python_custom: 6, conditional: 7, guardrails: 8, tool: 9, agent: 10,
knowledge_base: 11, llm_router: 12, human_in_loop: 13 }
>>>>>>> d6dadb6dd (feat(CE): add workflow approval model (#1708))

validates :name, presence: true
validates :component_type, presence: true
Expand Down
16 changes: 16 additions & 0 deletions server/app/models/agents/workflow_approval.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Agents
class WorkflowApproval < ApplicationRecord
belongs_to :workflow_run, class_name: "Agents::WorkflowRun"
belongs_to :workspace
belongs_to :resolved_by, class_name: "User", optional: true

validates :workflow_run_id, :workspace_id, :status, :message,
:temporal_workflow_id, :temporal_run_id, presence: true

enum :status, { pending: 0, approved: 1, rejected: 2, timed_out: 3 }

scope :active, -> { where(status: :pending) }
end
end
21 changes: 18 additions & 3 deletions server/app/models/agents/workflow_run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ class WorkflowRun < ApplicationRecord
belongs_to :workspace

has_one :workflow_log, class_name: "Agents::WorkflowLog", dependent: :destroy
<<<<<<< HEAD
=======
has_many :workflow_approvals, class_name: "Agents::WorkflowApproval", dependent: :destroy
has_many :llm_routing_logs, dependent: :destroy
has_many :llm_usage_logs, dependent: :destroy
>>>>>>> d6dadb6dd (feat(CE): add workflow approval model (#1708))

after_initialize :set_defaults, if: :new_record?

scope :active, -> { where(status: %i[pending in_progress]) }
scope :active, -> { where(status: %i[pending in_progress action_required]) }

aasm column: :status, whiny_transitions: true do
state :pending, initial: true
state :in_progress
state :action_required
state :completed
state :failed
state :cancelled
Expand All @@ -28,16 +35,24 @@ class WorkflowRun < ApplicationRecord
transitions from: %i[pending in_progress], to: :in_progress
end

event :pause_for_approval do
transitions from: :in_progress, to: :action_required
end

event :resume do
transitions from: :action_required, to: :in_progress
end

event :complete do
transitions from: :in_progress, to: :completed
end

event :fail do
transitions from: %i[pending in_progress], to: :failed
transitions from: %i[pending in_progress action_required], to: :failed
end

event :cancel do
transitions from: %i[pending in_progress], to: :cancelled
transitions from: %i[pending in_progress action_required], to: :cancelled
end
end

Expand Down
1 change: 1 addition & 0 deletions server/app/models/workspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Workspace < ApplicationRecord
has_many :chat_messages, dependent: :nullify
has_many :workflows, class_name: "Agents::Workflow", dependent: :destroy
has_many :workflow_runs, class_name: "Agents::WorkflowRun", dependent: :destroy
has_many :workflow_approvals, class_name: "Agents::WorkflowApproval", dependent: :destroy
has_many :workflow_logs, class_name: "Agents::WorkflowLog", dependent: :nullify
has_many :workflow_integrations, class_name: "Agents::WorkflowIntegration", dependent: :nullify

Expand Down
29 changes: 29 additions & 0 deletions server/db/migrate/20260305100000_create_workflow_approvals.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

class CreateWorkflowApprovals < ActiveRecord::Migration[7.1]
def change
create_table :workflow_approvals do |t|
t.references :workflow_run, null: false, foreign_key: { to_table: :workflow_runs }
t.references :workspace, null: false, foreign_key: true
t.string :component_id, null: false
t.integer :status, null: false, default: 0
t.text :message, null: false
t.jsonb :input_data
t.string :temporal_workflow_id, null: false
t.string :temporal_run_id, null: false
t.references :resolved_by, foreign_key: { to_table: :users }, null: true
t.text :resolution_note
t.datetime :timeout_at
t.string :timeout_action, default: "reject"
t.datetime :resolved_at

t.timestamps
end

add_index :workflow_approvals, :status
add_index :workflow_approvals, %i[workflow_run_id component_id],
unique: true,
where: "status = 0",
name: "idx_workflow_approvals_unique_pending"
end
end
29 changes: 29 additions & 0 deletions server/db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions server/spec/factories/agents/workflow_approvals.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

FactoryBot.define do
factory :workflow_approval, class: "Agents::WorkflowApproval" do
association :workflow_run, factory: :workflow_run
association :workspace
component_id { "hitl-#{SecureRandom.hex(4)}" }
status { :pending }
message { "Please review and approve this workflow step." }
input_data { { "input_text" => "Sample input data" } }
temporal_workflow_id { "workflow-#{SecureRandom.hex(8)}-dag-#{SecureRandom.hex(4)}" }
temporal_run_id { SecureRandom.uuid }
timeout_action { "reject" }

trait :pending do
status { :pending }
end

trait :approved do
status { :approved }
resolved_at { Time.current }
resolution_note { "Approved" }
end

trait :rejected do
status { :rejected }
resolved_at { Time.current }
resolution_note { "Rejected" }
end

trait :timed_out do
status { :timed_out }
resolved_at { Time.current }
end

trait :with_timeout do
timeout_at { 24.hours.from_now }
timeout_action { "reject" }
end

trait :with_resolved_by do
association :resolved_by, factory: :user
resolved_at { Time.current }
end
end
end
4 changes: 4 additions & 0 deletions server/spec/factories/agents/workflow_runs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
status { "cancelled" }
end

trait :action_required do
status { "action_required" }
end

trait :with_inputs do
inputs { { "key1" => "value1", "key2" => { "nested" => "value" } } }
end
Expand Down
11 changes: 11 additions & 0 deletions server/spec/models/agents/component_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@
llm_model: 3,
prompt_template: 4,
vector_store: 5,
<<<<<<< HEAD
python_custom: 6
=======
python_custom: 6,
conditional: 7,
guardrails: 8,
tool: 9,
agent: 10,
knowledge_base: 11,
llm_router: 12,
human_in_loop: 13
>>>>>>> d6dadb6dd (feat(CE): add workflow approval model (#1708))
)
}
end
Expand Down
81 changes: 81 additions & 0 deletions server/spec/models/agents/workflow_approval_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Agents::WorkflowApproval, type: :model do
describe "associations" do
it { should belong_to(:workflow_run).class_name("Agents::WorkflowRun") }
it { should belong_to(:workspace) }
it { should belong_to(:resolved_by).class_name("User").optional }
end

describe "validations" do
it { should validate_presence_of(:workflow_run_id) }
it { should validate_presence_of(:workspace_id) }
it { should validate_presence_of(:status) }
it { should validate_presence_of(:message) }
it { should validate_presence_of(:temporal_workflow_id) }
it { should validate_presence_of(:temporal_run_id) }
end

describe "enum" do
it { should define_enum_for(:status).with_values(pending: 0, approved: 1, rejected: 2, timed_out: 3) }
end

describe "scopes" do
let!(:pending_approval) { create(:workflow_approval, :pending) }
let!(:approved_approval) { create(:workflow_approval, :approved) }
let!(:rejected_approval) { create(:workflow_approval, :rejected) }
let!(:timed_out_approval) { create(:workflow_approval, :timed_out) }

describe ".active" do
it "returns only pending approvals" do
expect(described_class.active).to include(pending_approval)
expect(described_class.active).not_to include(approved_approval, rejected_approval, timed_out_approval)
end
end
end

describe "factory" do
it "creates a valid workflow_approval" do
approval = create(:workflow_approval)
expect(approval).to be_valid
expect(approval).to be_pending
end

it "creates approved approval" do
approval = create(:workflow_approval, :approved)
expect(approval).to be_approved
expect(approval.resolved_at).to be_present
end

it "creates rejected approval" do
approval = create(:workflow_approval, :rejected)
expect(approval).to be_rejected
end

it "creates timed_out approval" do
approval = create(:workflow_approval, :timed_out)
expect(approval).to be_timed_out
end
end

describe "attributes" do
let(:approval) { create(:workflow_approval) }

it "stores input_data as jsonb" do
approval.update!(input_data: { "key" => "value", "nested" => { "a" => 1 } })
expect(approval.reload.input_data).to eq({ "key" => "value", "nested" => { "a" => 1 } })
end

it "stores temporal workflow and run IDs" do
expect(approval.temporal_workflow_id).to be_present
expect(approval.temporal_run_id).to be_present
end

it "allows optional fields to be nil" do
approval = create(:workflow_approval, resolved_by: nil, resolution_note: nil, timeout_at: nil)
expect(approval).to be_valid
end
end
end
Loading
Loading