From 044cfba9629776d53894fdf19fcb6751be17a5b2 Mon Sep 17 00:00:00 2001 From: datafloyd Date: Wed, 8 Apr 2026 15:53:04 +0400 Subject: [PATCH] Resolve conflict in cherry-pick of 46ca3c401ceee2cf8b2f695508eb4f84f8aed3b5 and change the commit message --- server/app/models/agents/workflow.rb | 6 ++ server/app/models/p2w/session.rb | 39 ++++++++ server/app/models/p2w/session_event.rb | 16 ++++ server/app/models/workspace.rb | 11 +++ ...0000_create_prompt_to_workflow_sessions.rb | 28 ++++++ ...reate_prompt_to_workflow_session_events.rb | 18 ++++ ...ent_turn_to_prompt_to_workflow_sessions.rb | 9 ++ server/db/schema.rb | 47 ++++++++++ server/spec/factories/p2w_sessions.rb | 12 +++ server/spec/models/agents/workflow_spec.rb | 5 ++ server/spec/models/p2w/session_event_spec.rb | 39 ++++++++ server/spec/models/p2w/session_spec.rb | 88 +++++++++++++++++++ 12 files changed, 318 insertions(+) create mode 100644 server/app/models/p2w/session.rb create mode 100644 server/app/models/p2w/session_event.rb create mode 100644 server/db/migrate/20260325100000_create_prompt_to_workflow_sessions.rb create mode 100644 server/db/migrate/20260325100001_create_prompt_to_workflow_session_events.rb create mode 100644 server/db/migrate/20260326100000_add_current_turn_to_prompt_to_workflow_sessions.rb create mode 100644 server/spec/factories/p2w_sessions.rb create mode 100644 server/spec/models/p2w/session_event_spec.rb create mode 100644 server/spec/models/p2w/session_spec.rb diff --git a/server/app/models/agents/workflow.rb b/server/app/models/agents/workflow.rb index dd15f4a23..40a94157e 100644 --- a/server/app/models/agents/workflow.rb +++ b/server/app/models/agents/workflow.rb @@ -10,6 +10,12 @@ class Workflow < ApplicationRecord has_many :workflow_runs, dependent: :destroy has_many :visual_components, as: :configurable, dependent: :destroy has_one :workflow_integration, dependent: :destroy +<<<<<<< HEAD +======= + has_many :workflow_sessions, dependent: :destroy + has_many :p2w_sessions, class_name: "P2w::Session", dependent: :destroy + has_many :workflow_files, class_name: "Agents::WorkflowFile", dependent: :destroy +>>>>>>> 46ca3c401 (feat(CE): prompt to workflow v2 models (#1803)) enum status: { draft: 0, published: 1 } enum trigger_type: { website_chatbot: 0, chat_assistant: 1, scheduled: 2, api_trigger: 3, slack: 4 } diff --git a/server/app/models/p2w/session.rb b/server/app/models/p2w/session.rb new file mode 100644 index 000000000..3f4324091 --- /dev/null +++ b/server/app/models/p2w/session.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module P2w + class Session < ApplicationRecord + self.table_name = "prompt_to_workflow_sessions" + + TERMINAL_STATUSES = %w[completed failed max_turns expired].freeze + + belongs_to :workflow, class_name: "Agents::Workflow" + belongs_to :workspace + has_many :events, class_name: "P2w::SessionEvent", + foreign_key: :prompt_to_workflow_session_id, + inverse_of: :session, + dependent: :destroy + + validates :session_id, presence: true, uniqueness: true + validates :workflow_id, :workspace_id, :status, :expires_at, presence: true + validates :status, inclusion: { in: %w[running clarification_pending completed failed max_turns expired] } + + scope :active, -> { where(status: %w[running clarification_pending]) } + scope :not_expired, -> { where("expires_at > ?", Time.current) } + + def terminal? + status.in?(TERMINAL_STATUSES) + end + + def replayable? + expires_at > Time.current + end + + def accepts_clarification? + status == "clarification_pending" && !expired? + end + + def expired? + expires_at <= Time.current + end + end +end diff --git a/server/app/models/p2w/session_event.rb b/server/app/models/p2w/session_event.rb new file mode 100644 index 000000000..8f40b7e88 --- /dev/null +++ b/server/app/models/p2w/session_event.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module P2w + class SessionEvent < ApplicationRecord + self.table_name = "prompt_to_workflow_session_events" + + belongs_to :session, class_name: "P2w::Session", + foreign_key: :prompt_to_workflow_session_id, + inverse_of: :events + + validates :sequence, presence: true, + uniqueness: { scope: :prompt_to_workflow_session_id }, + numericality: { greater_than_or_equal_to: 0 } + validates :event_type, presence: true + end +end diff --git a/server/app/models/workspace.rb b/server/app/models/workspace.rb index 03ebd0a00..52e30c817 100644 --- a/server/app/models/workspace.rb +++ b/server/app/models/workspace.rb @@ -31,6 +31,17 @@ class Workspace < ApplicationRecord has_many :workflow_runs, class_name: "Agents::WorkflowRun", dependent: :destroy has_many :workflow_logs, class_name: "Agents::WorkflowLog", dependent: :nullify has_many :workflow_integrations, class_name: "Agents::WorkflowIntegration", dependent: :nullify +<<<<<<< HEAD +======= + has_many :workflow_sessions, class_name: "Agents::WorkflowSession", dependent: :nullify + has_many :p2w_sessions, class_name: "P2w::Session", dependent: :destroy + has_many :hosted_data_stores, dependent: :nullify + has_many :knowledge_bases, class_name: "Agents::KnowledgeBase", dependent: :nullify + has_many :tools, class_name: "Agents::Tool", dependent: :destroy + has_many :llm_routing_logs, dependent: :destroy + has_many :llm_usage_logs, dependent: :destroy + has_many :agentic_coding_apps, class_name: "AgenticCoding::App", dependent: :destroy +>>>>>>> 46ca3c401 (feat(CE): prompt to workflow v2 models (#1803)) belongs_to :organization has_many :sso_configurations, through: :organization diff --git a/server/db/migrate/20260325100000_create_prompt_to_workflow_sessions.rb b/server/db/migrate/20260325100000_create_prompt_to_workflow_sessions.rb new file mode 100644 index 000000000..50a8d4020 --- /dev/null +++ b/server/db/migrate/20260325100000_create_prompt_to_workflow_sessions.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class CreatePromptToWorkflowSessions < ActiveRecord::Migration[7.1] + def change + create_table :prompt_to_workflow_sessions do |t| + t.uuid :session_id, null: false + t.uuid :workflow_id, null: false + t.integer :workspace_id, null: false + t.string :status, null: false, default: "running" + t.uuid :current_clarification_id + t.jsonb :state, null: false, default: {} + t.string :temporal_workflow_id + t.string :temporal_run_id + t.datetime :expires_at, null: false + t.timestamps + end + + add_index :prompt_to_workflow_sessions, :session_id, unique: true + add_index :prompt_to_workflow_sessions, :workflow_id + add_index :prompt_to_workflow_sessions, :workspace_id + add_index :prompt_to_workflow_sessions, %i[workspace_id status], name: "idx_p2w_sessions_workspace_status" + add_index :prompt_to_workflow_sessions, %i[status expires_at], name: "idx_p2w_sessions_status_expires" + add_index :prompt_to_workflow_sessions, :expires_at + + add_foreign_key :prompt_to_workflow_sessions, :workflows, column: :workflow_id, validate: false + add_foreign_key :prompt_to_workflow_sessions, :workspaces, column: :workspace_id, validate: false + end +end diff --git a/server/db/migrate/20260325100001_create_prompt_to_workflow_session_events.rb b/server/db/migrate/20260325100001_create_prompt_to_workflow_session_events.rb new file mode 100644 index 000000000..2fc7a4c1c --- /dev/null +++ b/server/db/migrate/20260325100001_create_prompt_to_workflow_session_events.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreatePromptToWorkflowSessionEvents < ActiveRecord::Migration[7.1] + def change + create_table :prompt_to_workflow_session_events do |t| + t.references :prompt_to_workflow_session, null: false, foreign_key: true + t.integer :sequence, null: false + t.string :event_type, null: false + t.jsonb :payload, null: false, default: {} + t.datetime :created_at, null: false + end + + add_index :prompt_to_workflow_session_events, + %i[prompt_to_workflow_session_id sequence], + unique: true, + name: "idx_p2w_events_session_sequence" + end +end diff --git a/server/db/migrate/20260326100000_add_current_turn_to_prompt_to_workflow_sessions.rb b/server/db/migrate/20260326100000_add_current_turn_to_prompt_to_workflow_sessions.rb new file mode 100644 index 000000000..b61e877c1 --- /dev/null +++ b/server/db/migrate/20260326100000_add_current_turn_to_prompt_to_workflow_sessions.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddCurrentTurnToPromptToWorkflowSessions < ActiveRecord::Migration[7.1] + def change + return if column_exists?(:prompt_to_workflow_sessions, :current_turn) + + add_column :prompt_to_workflow_sessions, :current_turn, :integer, null: false, default: 0 + end +end diff --git a/server/db/schema.rb b/server/db/schema.rb index ad5a40d21..02c079f36 100644 --- a/server/db/schema.rb +++ b/server/db/schema.rb @@ -277,6 +277,37 @@ t.string "organization_logo_filename" end + create_table "prompt_to_workflow_session_events", force: :cascade do |t| + t.bigint "prompt_to_workflow_session_id", null: false + t.integer "sequence", null: false + t.string "event_type", null: false + t.jsonb "payload", default: {}, null: false + t.datetime "created_at", null: false + t.index ["prompt_to_workflow_session_id", "sequence"], name: "idx_p2w_events_session_sequence", unique: true + t.index ["prompt_to_workflow_session_id"], name: "idx_on_prompt_to_workflow_session_id_4fd8c9eb0e" + end + + create_table "prompt_to_workflow_sessions", force: :cascade do |t| + t.uuid "session_id", null: false + t.uuid "workflow_id", null: false + t.integer "workspace_id", null: false + t.string "status", default: "running", null: false + t.uuid "current_clarification_id" + t.jsonb "state", default: {}, null: false + t.string "temporal_workflow_id" + t.string "temporal_run_id" + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "current_turn", default: 0, null: false + t.index ["expires_at"], name: "index_prompt_to_workflow_sessions_on_expires_at" + t.index ["session_id"], name: "index_prompt_to_workflow_sessions_on_session_id", unique: true + t.index ["status", "expires_at"], name: "idx_p2w_sessions_status_expires" + t.index ["workflow_id"], name: "index_prompt_to_workflow_sessions_on_workflow_id" + t.index ["workspace_id", "status"], name: "idx_p2w_sessions_workspace_status" + t.index ["workspace_id"], name: "index_prompt_to_workflow_sessions_on_workspace_id" + end + create_table "remote_code_executions", force: :cascade do |t| t.integer "workflow_run_id" t.integer "workspace_id", null: false @@ -709,6 +740,22 @@ add_foreign_key "edges", "components", column: "target_component_id", validate: false add_foreign_key "edges", "workflows", validate: false add_foreign_key "edges", "workspaces", validate: false +<<<<<<< HEAD +======= + add_foreign_key "hosted_data_store_tables", "hosted_data_stores", on_delete: :cascade, validate: false + add_foreign_key "hosted_data_stores", "workspaces", validate: false + add_foreign_key "knowledge_base_files", "knowledge_bases", validate: false + add_foreign_key "knowledge_bases", "workspaces", validate: false + add_foreign_key "llm_routing_logs", "components", validate: false + add_foreign_key "llm_routing_logs", "workflow_runs" + add_foreign_key "llm_routing_logs", "workspaces" + add_foreign_key "llm_usage_logs", "components", validate: false + add_foreign_key "llm_usage_logs", "workflow_runs" + add_foreign_key "llm_usage_logs", "workspaces" + add_foreign_key "prompt_to_workflow_session_events", "prompt_to_workflow_sessions" + add_foreign_key "prompt_to_workflow_sessions", "workflows", validate: false + add_foreign_key "prompt_to_workflow_sessions", "workspaces", validate: false +>>>>>>> 46ca3c401 (feat(CE): prompt to workflow v2 models (#1803)) add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade diff --git a/server/spec/factories/p2w_sessions.rb b/server/spec/factories/p2w_sessions.rb new file mode 100644 index 000000000..1ae972bf2 --- /dev/null +++ b/server/spec/factories/p2w_sessions.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :p2w_session, class: "P2w::Session" do + workspace + workflow + session_id { SecureRandom.uuid } + status { "running" } + expires_at { 30.minutes.from_now } + state { {} } + end +end diff --git a/server/spec/models/agents/workflow_spec.rb b/server/spec/models/agents/workflow_spec.rb index 26f434d45..2bd0f522b 100644 --- a/server/spec/models/agents/workflow_spec.rb +++ b/server/spec/models/agents/workflow_spec.rb @@ -9,6 +9,11 @@ it { should have_many(:edges).dependent(:destroy) } it { should have_many(:workflow_runs).dependent(:destroy) } it { should have_one(:workflow_integration).dependent(:destroy) } +<<<<<<< HEAD +======= + it { should have_many(:p2w_sessions).class_name("P2w::Session").dependent(:destroy) } + it { should have_many(:versions) } +>>>>>>> 46ca3c401 (feat(CE): prompt to workflow v2 models (#1803)) end describe "enums" do diff --git a/server/spec/models/p2w/session_event_spec.rb b/server/spec/models/p2w/session_event_spec.rb new file mode 100644 index 000000000..4a92a0dce --- /dev/null +++ b/server/spec/models/p2w/session_event_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe P2w::SessionEvent do + let(:workspace) { create(:workspace) } + let(:workflow) { create(:workflow, workspace:) } + let(:session) do + P2w::Session.create!(session_id: SecureRandom.uuid, workflow:, + workspace:, status: "running", expires_at: 30.minutes.from_now) + end + + describe "validations" do + it "requires sequence, event_type" do + event = described_class.new(prompt_to_workflow_session_id: session.id) + event.valid? + expect(event.errors[:sequence]).not_to be_empty + expect(event.errors[:event_type]).to include("can't be blank") + end + + it "enforces unique sequence per session" do + described_class.create!(prompt_to_workflow_session_id: session.id, sequence: 0, event_type: "test") + dup = described_class.new(prompt_to_workflow_session_id: session.id, sequence: 0, event_type: "test2") + expect(dup).not_to be_valid + end + + it "rejects negative sequence" do + event = described_class.new(prompt_to_workflow_session_id: session.id, sequence: -1, event_type: "test") + expect(event).not_to be_valid + end + end + + describe "associations" do + it "belongs to session" do + event = described_class.create!(prompt_to_workflow_session_id: session.id, sequence: 0, event_type: "test") + expect(event.session).to eq(session) + end + end +end diff --git a/server/spec/models/p2w/session_spec.rb b/server/spec/models/p2w/session_spec.rb new file mode 100644 index 000000000..71c0a075e --- /dev/null +++ b/server/spec/models/p2w/session_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe P2w::Session do + let(:workspace) { create(:workspace) } + let(:workflow) { create(:workflow, workspace:) } + + describe "validations" do + it "requires session_id and expires_at" do + session = described_class.new + session.valid? + expect(session.errors[:session_id]).to include("can't be blank") + expect(session.errors[:expires_at]).to include("can't be blank") + end + + it "enforces unique session_id" do + uid = SecureRandom.uuid + described_class.create!(session_id: uid, workflow:, workspace:, + expires_at: 30.minutes.from_now) + dup = described_class.new(session_id: uid, workflow:, workspace:, + expires_at: 30.minutes.from_now) + expect(dup).not_to be_valid + expect(dup.errors[:session_id]).to include("has already been taken") + end + + it "validates status inclusion" do + session = described_class.new(status: "invalid_status") + session.valid? + expect(session.errors[:status]).to include("is not included in the list") + end + end + + describe "scopes" do + let!(:running_session) do + described_class.create!(session_id: SecureRandom.uuid, workflow:, workspace:, + status: "running", expires_at: 30.minutes.from_now) + end + let!(:completed_session) do + described_class.create!(session_id: SecureRandom.uuid, workflow:, workspace:, + status: "completed", expires_at: 30.minutes.from_now) + end + let!(:expired_session) do + described_class.create!(session_id: SecureRandom.uuid, workflow:, workspace:, + status: "running", expires_at: 1.minute.ago) + end + + it ".active returns running and clarification_pending" do + expect(described_class.active).to include(running_session) + expect(described_class.active).not_to include(completed_session) + end + + it ".not_expired excludes expired sessions" do + expect(described_class.not_expired).to include(running_session) + expect(described_class.not_expired).not_to include(expired_session) + end + end + + describe "helper methods" do + let(:session) do + described_class.new(status: "running", expires_at: 30.minutes.from_now) + end + + it "#terminal? returns true for terminal statuses" do + expect(described_class.new(status: "completed")).to be_terminal + expect(described_class.new(status: "failed")).to be_terminal + expect(described_class.new(status: "running")).not_to be_terminal + end + + it "#expired? checks expires_at" do + expect(described_class.new(expires_at: 1.minute.ago)).to be_expired + expect(described_class.new(expires_at: 30.minutes.from_now)).not_to be_expired + end + + it "#replayable? returns true when not expired" do + expect(described_class.new(expires_at: 30.minutes.from_now)).to be_replayable + expect(described_class.new(expires_at: 1.minute.ago)).not_to be_replayable + end + + it "#accepts_clarification? requires clarification_pending and not expired" do + session = described_class.new(status: "clarification_pending", expires_at: 30.minutes.from_now) + expect(session.accepts_clarification?).to be true + + session.status = "running" + expect(session.accepts_clarification?).to be false + end + end +end