Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,40 @@ def network

if topic.present?
workflow_state = DiscourseWorkflow::WorkflowState.find_by(topic_id: topic.id)
workflow = workflow_state.workflow
workflow = workflow_state&.workflow

lanes = workflow.workflow_step.order(:position).map do |step|
raise Discourse::NotFound unless workflow

# Preload steps + options (and their workflow_option) to avoid extra queries when iterating
steps = workflow.workflow_steps.order(:position).includes(workflow_step_options: :workflow_option)

# Lanes: unique categories in order of step position
lanes = steps.map do |step|
category = Category.find(step.category_id)
{
name: Category.find(step.category_id).name,
name: category.name,
link: "/c/#{step.category_id}"
}
}
end.uniq { |lane| lane[:name] }

nodes = workflow.workflow_step.order(:position).map do |step|
# Nodes: one per step
nodes = steps.map do |step|
category_name = Category.find(step.category_id).name

{
id: step.name,
lane: lanes.find_index { |lane| lane[:name] == Category.find(step.category_id).name },
active: step.id == workflow_state.workflow_step_id
id: step.name,
lane: lanes.find_index { |lane| lane[:name] == category_name },
active: step.id == workflow_state.workflow_step_id
}
end

# Links: from each step via its options
links = []

workflow.workflow_step.order(:position).each do |step|
step.workflow_step_option.each do |option|
target_step = WorkflowStep.find(option.target_step_id)
steps.each do |step|
step.workflow_step_options.each do |option|
target_step = DiscourseWorkflow::WorkflowStep.find(option.target_step_id)

links << {
source: step.name,
target: target_step.name,
Expand All @@ -44,34 +56,8 @@ def network
nodes: nodes,
links: links
}

# const workflowData = {
# lanes: [
# { name: "Preparers", link: "https://example.com/preparers" },
# { name: "Reviewers", link: "https://example.com/reviewers" },
# { name: "Finalisers", link: "https://example.com/finalisers" },
# { name: "Approvers", link: "https://example.com/approvers" },
# { name: "Completed", link: "https://example.com/completed" }
# ],
# nodes: [
# { id: 'Step A', lane: 0, active: false },
# { id: 'Step B', lane: 1, active: false },
# { id: 'Step C', lane: 0, active: true },
# { id: 'Step D', lane: 2, active: false },
# { id: 'Step E', lane: 3, active: false },
# { id: 'Step F', lane: 4, active: false },

# ],
# links: [
# { source: 'Step A', target: 'Step B', action: 'start' },
# { source: 'Step B', target: 'Step A', action: 'reject' },
# { source: 'Step B', target: 'Step C', action: 'accept' },
# { source: 'Step C', target: 'Step D', action: 'process' },
# { source: 'Step D', target: 'Step E', action: 'finalize' },
# { source: 'Step E', target: 'Step F', action: 'confirmed' },
# { source: 'Step E', target: 'Step C', action: 'reopended' }
# ]

else
raise Discourse::NotFound
end
Comment on lines +59 to 61
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The else branch on line 59-61 is unreachable. Topic.find on line 8 will raise ActiveRecord::RecordNotFound if the topic doesn't exist, so topic.present? will always be true at line 10. Consider removing this unreachable else block.

Suggested change
else
raise Discourse::NotFound
end

Copilot uses AI. Check for mistakes.
end
end
Expand Down
4 changes: 2 additions & 2 deletions app/models/discourse_workflow/workflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class Workflow < ActiveRecord::Base
validate :ensure_name_ascii
validates :slug, presence: true, uniqueness: true

has_many :workflow_step, dependent: :destroy
has_many :workflow_state
has_many :workflow_steps, dependent: :destroy
has_many :workflow_states

scope :ordered, -> { order("lower(name) ASC") }

Expand Down
2 changes: 2 additions & 0 deletions app/models/discourse_workflow/workflow_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module ::DiscourseWorkflow
class WorkflowState < ActiveRecord::Base
self.table_name = 'workflow_states'

belongs_to :topic
belongs_to :workflow
belongs_to :workflow_step
end
Expand Down
4 changes: 2 additions & 2 deletions app/models/discourse_workflow/workflow_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ module ::DiscourseWorkflow
class WorkflowStep < ActiveRecord::Base
self.table_name = 'workflow_steps'
belongs_to :workflow
has_many :workflow_step_option
has_many :workflow_state
has_many :workflow_step_options
has_many :workflow_states

validates :category_id, presence: true
validates :name, presence: true
Expand Down
65 changes: 44 additions & 21 deletions lib/discourse_workflow/ai_actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,74 @@

module DiscourseWorkflow
class AiActions

def transition_all
WorkflowState.all.each do |workflow_state|
if workflow_state.workflow_step.ai_enabled && workflow_state.workflow_step.workflow_step_option.count > 0
DiscourseWorkflow::WorkflowState
.includes(
:topic,
workflow_step: {
workflow_step_options: :workflow_option
}
)
.find_each do |workflow_state|
step = workflow_state.workflow_step
next unless step

# skip if AI not enabled or no options
next unless step.ai_enabled
next if step.workflow_step_options.empty?

ai_transition(workflow_state)
end
end
end

def ai_transition(workflow_state)
client = OpenAI::Client.new(access_token: SiteSetting.workflow_openai_api_key)
step = workflow_state.workflow_step
topic = workflow_state.topic
return unless step && topic

client =
OpenAI::Client.new(access_token: SiteSetting.workflow_openai_api_key)
model_name = SiteSetting.workflow_ai_model
system_prompt = SiteSetting.workflow_ai_prompt_system
base_user_prompt = workflow_state.workflow_step.ai_prompt
system_prompt = SiteSetting.workflow_ai_prompt_system
base_user_prompt = step.ai_prompt

return if base_user_prompt.blank?

# get option slugs for this step
options =
step.workflow_step_options.map { |o| o.workflow_option&.slug }.compact

return if !base_user_prompt.present?
return if options.empty?

options = workflow_state.workflow_step.workflow_step_option.map(&:workflow_option)&.pluck(:slug)
user_prompt = base_user_prompt.gsub(/{{options}}/, options.join(', '))
topic = Topic.find(workflow_state.topic_id)
user_prompt = base_user_prompt.gsub(/{{options}}/, options.join(", "))
user_prompt = user_prompt.gsub(/{{topic}}/, topic.first_post.raw)

messages = [{ "role": "system", "content": system_prompt }]
messages << { "role": "user", "content": user_prompt }
messages = [
{ role: "system", content: system_prompt },
{ role: "user", content: user_prompt }
]

response = client.chat(
parameters: {
response =
client.chat(
parameters: {
model: model_name,
messages: messages,
max_tokens: 8,
temperature: 0.1,
})
temperature: 0.1
}
)

if response["error"]
begin
raise StandardError, response["error"]["message"]
rescue => e
Rails.logger.error("Workflow: There was a problem: #{e}")
# I18n.t('ai_topic_summary.errors.general')
end
else
result = response.dig("choices", 0, "message", "content")
return
end

result = result.strip.chomp('.').downcase if result.present?
result = response.dig("choices", 0, "message", "content")
result = result.strip.chomp(".").downcase if result.present?

if result.present? && options.include?(result)
Transition.new.transition(nil, topic, result)
Expand Down
10 changes: 8 additions & 2 deletions lib/discourse_workflow/topic_extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ module DiscourseWorkflow
module TopicExtension
extend ActiveSupport::Concern

prepended { validates_with NotMidwayValidator, on: :create }
prepended do
has_one :workflow_state,
class_name: "DiscourseWorkflow::WorkflowState",
foreign_key: :topic_id

validates_with NotMidwayValidator, on: :create
end

def is_workflow_topic?
WorkflowState.exists?(topic_id: self.id)
workflow_state.present?
end
end
end
Loading