meta_workflows 0.9.18 → 0.9.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/meta_workflows/controllers/structured_form_submit_controller.js +18 -0
  3. data/app/assets/stylesheets/meta_workflows/application.css +221 -0
  4. data/app/controllers/meta_workflows/humans_controller.rb +3 -2
  5. data/app/controllers/meta_workflows/structured_humans_controller.rb +74 -0
  6. data/app/helpers/meta_workflows/meta_workflows_helper.rb +6 -1
  7. data/app/jobs/meta_workflows/concerns/error_handling.rb +31 -0
  8. data/app/jobs/meta_workflows/human_input_job.rb +25 -2
  9. data/app/jobs/meta_workflows/meta_job.rb +17 -21
  10. data/app/models/meta_workflows/execution_chat_history.rb +23 -0
  11. data/app/models/meta_workflows/workflow_execution.rb +1 -0
  12. data/app/services/meta_workflows/message_history_service.rb +42 -0
  13. data/app/views/meta_workflows/_assistant_message_bubble.html.erb +9 -0
  14. data/app/views/meta_workflows/_checkbox_input.html.erb +24 -0
  15. data/app/views/meta_workflows/_lexi_chat_alpha_tray.html.erb +7 -1
  16. data/app/views/meta_workflows/_radio_input.html.erb +26 -0
  17. data/app/views/meta_workflows/_response_form_lexi.html.erb +34 -31
  18. data/app/views/meta_workflows/_response_lexi.html.erb +27 -36
  19. data/app/views/meta_workflows/_slider_input.html.erb +60 -0
  20. data/app/views/meta_workflows/_structured_input.html.erb +28 -0
  21. data/app/views/meta_workflows/_user_message_bubble.html.erb +8 -0
  22. data/config/routes.rb +1 -0
  23. data/db/migrate/20250714192853_create_meta_workflows_execution_chat_histories.rb +12 -0
  24. data/lib/meta_workflows/version.rb +1 -1
  25. data/lib/services/meta_workflows/meta_workflow_service.rb +14 -0
  26. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f70452cae76da68d70cc4e7b0f5c311ca8d287aac2526983fcee42e5cdb6555a
4
- data.tar.gz: 47d7637b8ec224c160b62ac55f363b07515d1e6d6a43350dc4127afb0839f32e
3
+ metadata.gz: 3b91afe608b3c5260eebee5a4cc1211621c85171cf2ddc3740f5c695bc6df73f
4
+ data.tar.gz: cbafaa30a15e992936bdbe66ec444fb15740bbac999c2137e11f67d26870eea0
5
5
  SHA512:
6
- metadata.gz: 3b4bce3cecda4fa622ecc808fd5293f47e321c29bcd6ba1afb4b06f0aad1766307459275bb5bfaa89a03f0fd47f0dcc15cd55acbef3f33cfc8d42c7aa0690694
7
- data.tar.gz: a1881bb94b04d89dba925e60d1915a16db2b32f0fd28a60bb1fa1517743e52be625bbbb7a32e69c7c264e95235b3b4848cf151dccf27aa7c0a8ac5e1f3574f76
6
+ metadata.gz: '0886296d806086437b57857c1e509ea76a6db6c784df7bee23ff54428220d819e94265083165a0e33a17715f9ab00a9f0efd0a553d365c21b5b68a93b7c56c4e'
7
+ data.tar.gz: 76b74c3f7508be1ee96547583adaf43a13f41c8af6ba74023fdcd3cafd0cd59c34ff535eeb2490ee4ba0c03c23c75bfb6ed248630d179938e9dedd724fd50add
@@ -0,0 +1,18 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ['form', 'radioInput', 'checkboxInput', 'sliderInput'];
5
+
6
+ connect() {
7
+ this.submitted = false;
8
+ }
9
+
10
+ handleSubmit() {
11
+ if (this.submitted) {
12
+ return;
13
+ }
14
+
15
+ this.submitted = true;
16
+ this.formTarget.requestSubmit();
17
+ }
18
+ }
@@ -824,3 +824,224 @@
824
824
  .lexi-error-content .lexi-loader-ellipses {
825
825
  margin-left: 0.375rem;
826
826
  }
827
+
828
+ /* Structured Input Styles */
829
+ .structured-input-container {
830
+ padding: 1rem;
831
+ background-color: rgba(255, 255, 255, 0.9);
832
+ border-radius: 0.75rem;
833
+ border: 1px solid var(--gray-300);
834
+ margin-bottom: 1rem;
835
+ }
836
+
837
+ .structured-input-options {
838
+ display: flex;
839
+ flex-direction: column;
840
+ gap: 0.75rem;
841
+ }
842
+
843
+ /* Radio Button Styles */
844
+ .structured-radio-option {
845
+ display: flex;
846
+ align-items: center;
847
+ gap: 0.75rem;
848
+ padding: 0.75rem 1rem;
849
+ border: 1px solid var(--gray-300);
850
+ border-radius: 0.5rem;
851
+ background-color: white;
852
+ cursor: pointer;
853
+ transition: all 0.2s ease;
854
+ }
855
+
856
+ .structured-radio-option:hover {
857
+ background-color: var(--gray-50);
858
+ border-color: var(--purple-300);
859
+ }
860
+
861
+ .structured-radio-option:has(.structured-radio-input:checked) {
862
+ background-color: var(--purple-50);
863
+ border-color: var(--purple-500);
864
+ }
865
+
866
+ .structured-radio-input {
867
+ width: 1.25rem;
868
+ height: 1.25rem;
869
+ accent-color: var(--purple-600);
870
+ cursor: pointer;
871
+ }
872
+
873
+ .structured-radio-label {
874
+ font-size: 1rem;
875
+ font-weight: 500;
876
+ color: var(--gray-800);
877
+ cursor: pointer;
878
+ flex: 1;
879
+ }
880
+
881
+ /* Checkbox Styles */
882
+ .structured-checkbox-option {
883
+ display: flex;
884
+ align-items: center;
885
+ gap: 0.75rem;
886
+ padding: 0.75rem 1rem;
887
+ border: 1px solid var(--gray-300);
888
+ border-radius: 0.5rem;
889
+ background-color: white;
890
+ cursor: pointer;
891
+ transition: all 0.2s ease;
892
+ }
893
+
894
+ .structured-checkbox-option:hover {
895
+ background-color: var(--gray-50);
896
+ border-color: var(--purple-300);
897
+ }
898
+
899
+ .structured-checkbox-option:has(.structured-checkbox-input:checked) {
900
+ background-color: var(--purple-50);
901
+ border-color: var(--purple-500);
902
+ }
903
+
904
+ .structured-checkbox-input {
905
+ width: 1.25rem;
906
+ height: 1.25rem;
907
+ accent-color: var(--purple-600);
908
+ cursor: pointer;
909
+ }
910
+
911
+ .structured-checkbox-label {
912
+ font-size: 1rem;
913
+ font-weight: 500;
914
+ color: var(--gray-800);
915
+ cursor: pointer;
916
+ flex: 1;
917
+ }
918
+
919
+ /* Slider Styles */
920
+ .structured-slider-container {
921
+ padding: 1rem;
922
+ }
923
+
924
+ .structured-slider-labels {
925
+ display: flex;
926
+ justify-content: space-between;
927
+ margin-bottom: 1rem;
928
+ font-size: 0.875rem;
929
+ color: var(--gray-600);
930
+ font-weight: 500;
931
+ }
932
+
933
+ .structured-slider-wrapper {
934
+ display: flex;
935
+ flex-direction: column;
936
+ gap: 0.5rem;
937
+ }
938
+
939
+ .structured-slider-input {
940
+ width: 100%;
941
+ height: 0.5rem;
942
+ border-radius: 0.25rem;
943
+ background: var(--gray-200);
944
+ outline: none;
945
+ -webkit-appearance: none;
946
+ appearance: none;
947
+ cursor: pointer;
948
+ }
949
+
950
+ .structured-slider-input::-webkit-slider-thumb {
951
+ -webkit-appearance: none;
952
+ appearance: none;
953
+ width: 1.5rem;
954
+ height: 1.5rem;
955
+ border-radius: 50%;
956
+ background: var(--purple-600);
957
+ cursor: pointer;
958
+ border: 2px solid white;
959
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
960
+ transition: all 0.2s ease;
961
+ }
962
+
963
+ .structured-slider-input::-webkit-slider-thumb:hover {
964
+ background: var(--purple-700);
965
+ transform: scale(1.1);
966
+ }
967
+
968
+ .structured-slider-input::-moz-range-thumb {
969
+ width: 1.5rem;
970
+ height: 1.5rem;
971
+ border-radius: 50%;
972
+ background: var(--purple-600);
973
+ cursor: pointer;
974
+ border: 2px solid white;
975
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
976
+ transition: all 0.2s ease;
977
+ }
978
+
979
+ .structured-slider-input::-moz-range-thumb:hover {
980
+ background: var(--purple-700);
981
+ transform: scale(1.1);
982
+ }
983
+
984
+ .structured-slider-value {
985
+ text-align: center;
986
+ font-size: 1.125rem;
987
+ font-weight: 600;
988
+ color: var(--purple-700);
989
+ padding: 0.5rem;
990
+ background-color: var(--purple-50);
991
+ border-radius: 0.375rem;
992
+ border: 1px solid var(--purple-200);
993
+ }
994
+
995
+ /* Structured Input Submit Button */
996
+ .structured-submit-button {
997
+ width: 3rem;
998
+ height: 3rem;
999
+ border-radius: 50%;
1000
+ display: flex;
1001
+ align-items: center;
1002
+ justify-content: center;
1003
+ background-color: var(--purple-600);
1004
+ color: white;
1005
+ transition: all 0.2s;
1006
+ border: none;
1007
+ cursor: pointer;
1008
+ margin-top: 1rem;
1009
+ align-self: flex-end;
1010
+ }
1011
+
1012
+ .structured-submit-button:hover {
1013
+ background-color: var(--purple-700);
1014
+ transform: scale(1.05);
1015
+ }
1016
+
1017
+ .structured-submit-button:disabled {
1018
+ background-color: var(--gray-400);
1019
+ cursor: not-allowed;
1020
+ transform: none;
1021
+ }
1022
+
1023
+ /* Structured Input Validation Styles */
1024
+ .structured-input-container[data-invalid="true"] {
1025
+ border-color: var(--red-500);
1026
+ background-color: var(--red-50);
1027
+ }
1028
+
1029
+ .structured-input-error {
1030
+ color: var(--red-600);
1031
+ font-size: 0.875rem;
1032
+ margin-top: 0.5rem;
1033
+ font-weight: 500;
1034
+ }
1035
+
1036
+ /* Screen reader only class for accessibility */
1037
+ .sr-only {
1038
+ position: absolute;
1039
+ width: 1px;
1040
+ height: 1px;
1041
+ padding: 0;
1042
+ margin: -1px;
1043
+ overflow: hidden;
1044
+ clip: rect(0, 0, 0, 0);
1045
+ white-space: nowrap;
1046
+ border: 0;
1047
+ }
@@ -9,10 +9,10 @@ module MetaWorkflows
9
9
  def update
10
10
  auto_advancing = should_auto_advance?
11
11
 
12
- if params[:advance].present? || auto_advancing
12
+ if params[:manual_advance].present? || auto_advancing
13
13
  @workflow_execution.increment_step
14
14
  render_loader_stream
15
- process_human_input(auto_advancing:, manual_advancing: params[:advance].present?)
15
+ process_human_input(auto_advancing:, manual_advancing: params[:manual_advance].present?)
16
16
  else
17
17
  render_response_form_stream
18
18
  process_human_input
@@ -23,6 +23,7 @@ module MetaWorkflows
23
23
 
24
24
  def should_auto_advance?
25
25
  step_repetitions = current_step_repetitions
26
+ return true if params[:auto_advance].present?
26
27
  return false unless step_repetitions
27
28
 
28
29
  new_repetition = @workflow_step.increment_repetition!
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ class StructuredHumansController < MetaController
5
+ include MetaWorkflows::MetaWorkflowsHelper
6
+ include MetaWorkflows::Streamable
7
+ before_action :set_workflow_data, only: [:update]
8
+
9
+ def update
10
+ render_loader_stream
11
+ process_structured_human_input
12
+ @workflow_execution.increment_step
13
+ end
14
+
15
+ private
16
+
17
+ def process_structured_human_input
18
+ MetaWorkflows::HumanInputJob.perform_later(
19
+ user_id: current_user.id,
20
+ record: @record,
21
+ auto_advancing: true,
22
+ manual_advancing: false,
23
+ params: {
24
+ inputs: extract_structured_inputs,
25
+ chat_id: params[:chat_id],
26
+ prompt_id: @prompt_id
27
+ }
28
+ )
29
+ end
30
+
31
+ def extract_structured_inputs
32
+ step_data = @workflow_execution.step_data(@workflow_execution.current_step)
33
+ structured_input_config = step_data&.dig('structured_input')
34
+ return params[:message] unless structured_input_config
35
+
36
+ case structured_input_config['type']
37
+ when 'single_choice'
38
+ params[:single_choice_selection]
39
+ when 'multiple_choice'
40
+ params[:multiple_choice_selections]
41
+ when 'range'
42
+ params[:range_value]
43
+ else
44
+ params[:message]
45
+ end
46
+ end
47
+
48
+ def render_loader_stream
49
+ respond_to do |format|
50
+ format.turbo_stream do
51
+ render turbo_stream: [
52
+ build_loader_stream(record: @record, workflow_execution: @workflow_execution),
53
+ build_response_form_stream(record: @record, workflow_execution: @workflow_execution,
54
+ response_enabled: false, responding: false, chat_id: params[:chat_id])
55
+ ]
56
+ end
57
+ end
58
+ end
59
+
60
+ def render_response_form_stream
61
+ respond_to do |format|
62
+ format.turbo_stream do
63
+ render turbo_stream: build_response_form_stream(
64
+ record: @record,
65
+ workflow_execution: @workflow_execution,
66
+ response_enabled: true,
67
+ responding: true,
68
+ chat_id: params[:chat_id]
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module MetaWorkflows
4
4
  module MetaWorkflowsHelper
5
- def target_frame_id(record, form: nil, loader: nil)
5
+ def target_frame_id(record, form: nil, loader: nil, structured_input: nil)
6
6
  base_id = "#{record.class.name.downcase}_#{record.id}"
7
7
 
8
8
  return "#{base_id}_loader" if loader
9
9
  return "#{base_id}_form" if form
10
+ return "#{base_id}_structured_input" if structured_input
10
11
 
11
12
  base_id
12
13
  end
@@ -27,6 +28,10 @@ module MetaWorkflows
27
28
  'meta_workflows/response_form_lexi'
28
29
  end
29
30
 
31
+ def meta_structured_input
32
+ 'meta_workflows/structured_input'
33
+ end
34
+
30
35
  def meta_response
31
36
  'meta_workflows/response_lexi'
32
37
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ module Concerns
5
+ module ErrorHandling
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def handle_llm_error(error, workflow_step, workflow_execution, conversation)
11
+ log_error(error)
12
+ store_error_in_workflow_step(error, workflow_step, conversation)
13
+ broadcast_error_response(workflow_execution)
14
+ end
15
+
16
+ def log_error(error)
17
+ Rails.logger.error("LLM Error in MetaJob: #{error.message}")
18
+ Rails.logger.error(error.backtrace.join("\n"))
19
+ end
20
+
21
+ def store_error_in_workflow_step(error, workflow_step, conversation)
22
+ workflow_step&.record_error(error, additional_details: {
23
+ job_class: self.class.name,
24
+ chat_id: chat&.id,
25
+ conversation_id: conversation&.id,
26
+ full_response_length: full_response&.length || 0
27
+ })
28
+ end
29
+ end
30
+ end
31
+ end
@@ -5,8 +5,11 @@ module MetaWorkflows
5
5
  include MetaWorkflows::MetaWorkflowsHelper
6
6
  queue_as :default
7
7
 
8
+ attr_accessor :current_step_data, :structured_input_config, :is_structured_input
9
+
8
10
  def perform(user_id:, record:, params:, auto_advancing: false, manual_advancing: false)
9
11
  setup(user_id, record, auto_advancing, manual_advancing)
12
+ set_workflow_step_variables
10
13
  conversation = initialize_conversation(params)
11
14
  process_and_broadcast(conversation) unless manual_advancing
12
15
  advance_workflow(params) if advancing?
@@ -14,6 +17,12 @@ module MetaWorkflows
14
17
 
15
18
  private
16
19
 
20
+ def set_workflow_step_variables
21
+ @current_step_data = workflow_execution&.step_data(workflow_execution.current_step)
22
+ @structured_input_config = current_step_data&.dig('structured_input')
23
+ @is_structured_input = structured_input_config.present? && current_step_data&.dig('action') == 'structured_human'
24
+ end
25
+
17
26
  def broadcast_response(chat, full_response)
18
27
  user_messages = chat.messages.where(role: 'user').order(:created_at)
19
28
  messages = chat.messages.order(:created_at)
@@ -42,7 +51,6 @@ module MetaWorkflows
42
51
  end
43
52
 
44
53
  def broadcast_form(chat)
45
- workflow_execution = active_workflow_execution
46
54
  Turbo::StreamsChannel.broadcast_replace_to(
47
55
  turbo_stream_name(record),
48
56
  target: target_frame_id(record, form: true),
@@ -51,7 +59,22 @@ module MetaWorkflows
51
59
  workflow_execution_id: workflow_execution&.id,
52
60
  response_enabled: true,
53
61
  chat_id: chat.id,
54
- step_has_repetitions: current_step_has_repetitions?(workflow_execution) }
62
+ step_has_repetitions: current_step_has_repetitions?(workflow_execution),
63
+ is_structured_input: is_structured_input }
64
+ )
65
+ end
66
+
67
+ def broadcast_structured_input(chat)
68
+ Turbo::StreamsChannel.broadcast_replace_to(
69
+ turbo_stream_name(record),
70
+ target: target_frame_id(record, structured_input: true),
71
+ partial: meta_structured_input,
72
+ locals: {
73
+ record: record,
74
+ chat_id: chat.id,
75
+ structured_input_config: structured_input_config,
76
+ is_structured_input: is_structured_input
77
+ }
55
78
  )
56
79
  end
57
80
 
@@ -2,9 +2,11 @@
2
2
 
3
3
  module MetaWorkflows
4
4
  class MetaJob < MetaWorkflows::ApplicationJob
5
+ include MetaWorkflows::Concerns::ErrorHandling
5
6
  include MetaWorkflows::Streamable
6
7
 
7
- attr_accessor :chat, :inputs, :full_response, :user_id, :record, :auto_advancing, :manual_advancing
8
+ attr_accessor :chat, :inputs, :full_response, :user_id, :record, :auto_advancing, :manual_advancing,
9
+ :workflow_execution
8
10
 
9
11
  private
10
12
 
@@ -14,6 +16,7 @@ module MetaWorkflows
14
16
  @full_response = +''
15
17
  @auto_advancing = auto_advancing
16
18
  @manual_advancing = manual_advancing
19
+ @workflow_execution = active_workflow_execution
17
20
  end
18
21
 
19
22
  def initialize_conversation(params)
@@ -27,7 +30,6 @@ module MetaWorkflows
27
30
  def initialize_new_conversation(params)
28
31
  inputs = params.symbolize_keys[:inputs]
29
32
 
30
- workflow_execution = active_workflow_execution
31
33
  current_step = workflow_execution.workflow_steps&.find_by(step: workflow_execution.current_step)
32
34
  @chat = current_step&.chat
33
35
 
@@ -44,7 +46,6 @@ module MetaWorkflows
44
46
  end
45
47
 
46
48
  def process_and_broadcast(conversation)
47
- workflow_execution = active_workflow_execution
48
49
  workflow_step = current_workflow_step(workflow_execution)
49
50
 
50
51
  begin
@@ -70,27 +71,18 @@ module MetaWorkflows
70
71
 
71
72
  def finalize_conversation(conversation)
72
73
  chat.update!(conversation_id: conversation.id) if chat.conversation_id.blank?
73
- broadcast_form(chat) unless auto_advancing
74
- end
75
-
76
- def handle_llm_error(error, workflow_step, workflow_execution, conversation)
77
- log_error(error)
78
- store_error_in_workflow_step(error, workflow_step, conversation)
79
- broadcast_error_response(workflow_execution)
80
- end
74
+ persist_messages_to_history
75
+ return if auto_advancing
81
76
 
82
- def log_error(error)
83
- Rails.logger.error("LLM Error in MetaJob: #{error.message}")
84
- Rails.logger.error(error.backtrace.join("\n"))
77
+ broadcast_form(chat)
78
+ broadcast_structured_input(chat)
85
79
  end
86
80
 
87
- def store_error_in_workflow_step(error, workflow_step, conversation)
88
- workflow_step&.record_error(error, additional_details: {
89
- job_class: self.class.name,
90
- chat_id: chat&.id,
91
- conversation_id: conversation&.id,
92
- full_response_length: full_response&.length || 0
93
- })
81
+ def persist_messages_to_history
82
+ MetaWorkflows::MessageHistoryService.new(
83
+ chat: chat,
84
+ workflow_execution: workflow_execution
85
+ ).persist_messages_to_history
94
86
  end
95
87
 
96
88
  def current_workflow_step(workflow_execution)
@@ -103,6 +95,10 @@ module MetaWorkflows
103
95
  raise NotImplementedError
104
96
  end
105
97
 
98
+ def broadcast_structured_input(chat)
99
+ raise NotImplementedError
100
+ end
101
+
106
102
  def broadcast_response(chat, full_response)
107
103
  raise NotImplementedError
108
104
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ class ExecutionChatHistory < ApplicationRecord
5
+ belongs_to :workflow_execution, class_name: 'MetaWorkflows::WorkflowExecution'
6
+
7
+ def append_message(message_data)
8
+ update!(history: history + [message_data])
9
+ end
10
+
11
+ def clear_history
12
+ update!(history: [])
13
+ end
14
+
15
+ def message_count
16
+ history.length
17
+ end
18
+
19
+ def last_message
20
+ history.last
21
+ end
22
+ end
23
+ end
@@ -7,6 +7,7 @@ module MetaWorkflows
7
7
  belongs_to :workflow, class_name: 'MetaWorkflows::Workflow'
8
8
  belongs_to :record, polymorphic: true
9
9
  has_many :workflow_steps, dependent: :destroy, class_name: 'MetaWorkflows::WorkflowStep'
10
+ has_one :execution_chat_history, dependent: :destroy, class_name: 'MetaWorkflows::ExecutionChatHistory'
10
11
 
11
12
  # Default values for fields
12
13
  attribute :current_step, :integer, default: 0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ class MessageHistoryService < MetaWorkflows::ApplicationService
5
+ def initialize(chat:, workflow_execution:)
6
+ super()
7
+ @chat = chat
8
+ @workflow_execution = workflow_execution
9
+ end
10
+
11
+ def persist_messages_to_history
12
+ # Ensure ExecutionChatHistory exists
13
+ chat_history = workflow_execution.execution_chat_history ||
14
+ workflow_execution.create_execution_chat_history!
15
+
16
+ # Get all messages from the current chat (minus the first message since that is the system message)
17
+ messages = chat.messages.order(:created_at)[1..]
18
+
19
+ # Convert messages to a format suitable for persistence
20
+ messages.each do |message|
21
+ message_data = {
22
+ role: message.role,
23
+ content: message.content,
24
+ model_id: message.model_id,
25
+ input_tokens: message.input_tokens,
26
+ output_tokens: message.output_tokens,
27
+ created_at: message.created_at.iso8601,
28
+ step: workflow_execution.current_step
29
+ }
30
+
31
+ # Only append if not already in history (to avoid duplicates)
32
+ unless chat_history.history.any? { |h| h['content'] == message.content && h['role'] == message.role }
33
+ chat_history.append_message(message_data)
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :chat, :workflow_execution
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ <!-- Lexi message bubble (left-aligned, white background) -->
2
+ <div class="lexi-message-assistant">
3
+ <!-- Message Bubble -->
4
+ <div class="lexi-message-bubble-assistant">
5
+ <div class="lexi-message-content-assistant">
6
+ <%= markdown(content) %>
7
+ </div>
8
+ </div>
9
+ </div>
@@ -0,0 +1,24 @@
1
+ <div class="structured-input-container"
2
+ role="group"
3
+ aria-labelledby="structured-checkbox-legend">
4
+ <legend id="structured-checkbox-legend" class="sr-only">Select one or more options</legend>
5
+ <div class="structured-input-options">
6
+ <% options.each_with_index do |option, index| %>
7
+ <div class="structured-checkbox-option">
8
+ <input
9
+ type="checkbox"
10
+ id="multiple_choice_<%= index %>"
11
+ name="multiple_choice_selections[]"
12
+ value="<%= option['value'] %>"
13
+ class="structured-checkbox-input"
14
+ data-action="change->meta-workflows--structured-form-submit#handleSubmit"
15
+ aria-describedby="structured-checkbox-help"
16
+ >
17
+ <label for="multiple_choice_<%= index %>" class="structured-checkbox-label">
18
+ <%= option['label'] %>
19
+ </label>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+ <div id="structured-checkbox-help" class="sr-only">Use space to select/deselect options</div>
24
+ </div>
@@ -1,6 +1,7 @@
1
1
  <%# Lexi Chat Alpha Tray (Center Display) %>
2
2
  <% active_execution = local_assigns[:record]&.workflow_executions&.order(created_at: :desc)&.first %>
3
3
  <% active_chat = active_execution&.workflow_steps&.last&.chat %>
4
+ <% chat_history = active_execution&.execution_chat_history %>
4
5
 
5
6
  <div class="lexi-chat-alpha-tray">
6
7
  <%# Header with Lexi logo left and close button right %>
@@ -34,13 +35,18 @@
34
35
  user_messages: active_chat&.messages&.where(role: 'user')&.order(:created_at) || [],
35
36
  last_message: active_chat&.messages&.last,
36
37
  full_response: nil,
37
- is_streaming: false
38
+ is_streaming: false,
39
+ chat_history: chat_history
38
40
  } %>
39
41
  </div>
40
42
 
41
43
  <%# Input area (fixed at bottom) %>
42
44
  <div class="lexi-chat-alpha-input-area">
43
45
  <div class="lexi-chat-alpha-input-container">
46
+ <!-- Structured Input (always rendered, conditionally displays content) -->
47
+ <%= render meta_structured_input, record: local_assigns[:record], structured_input_config: false, is_structured_input: false %>
48
+
49
+ <!-- Regular Form Input (always present) -->
44
50
  <%= render partial: meta_response_form, locals: {record: local_assigns[:record], response_enabled: true, workflow_execution_id: active_execution&.id, chat_id: active_execution&.workflow_steps&.last&.chat&.id, step_has_repetitions: true } %>
45
51
  </div>
46
52
  </div>
@@ -0,0 +1,26 @@
1
+ <div class="structured-input-container"
2
+ role="radiogroup"
3
+ aria-labelledby="structured-radio-legend"
4
+ aria-required="true">
5
+ <legend id="structured-radio-legend" class="sr-only">Select one option</legend>
6
+ <div class="structured-input-options">
7
+ <% options.each_with_index do |option, index| %>
8
+ <div class="structured-radio-option">
9
+ <input
10
+ type="radio"
11
+ id="single_choice_<%= index %>"
12
+ name="single_choice_selection"
13
+ value="<%= option['value'] %>"
14
+ class="structured-radio-input"
15
+ data-action="change->meta-workflows--structured-form-submit#handleSubmit"
16
+ aria-describedby="structured-radio-help"
17
+ required
18
+ >
19
+ <label for="single_choice_<%= index %>" class="structured-radio-label">
20
+ <%= option['label'] %>
21
+ </label>
22
+ </div>
23
+ <% end %>
24
+ </div>
25
+ <div id="structured-radio-help" class="sr-only">Use arrow keys to navigate between options</div>
26
+ </div>
@@ -1,44 +1,47 @@
1
1
  <%= turbo_frame_tag target_frame_id(record, form: true) do %>
2
2
  <div>
3
- <%= form_with url: (workflow_execution_id.present? ? meta_workflows.human_path(workflow_execution_id) : "#"),
3
+ <%= form_with url: workflow_execution_id.present? ? meta_workflows.human_path(workflow_execution_id) : "#",
4
4
  method: :patch,
5
5
  id: "#{target_frame_id(record, form: true)}_lexi",
6
6
  data: { controller: "meta-workflows--lexi-form-submit", "meta-workflows--lexi-form-submit-target": "form" } do |form| %>
7
7
  <%= form.hidden_field :chat_id, value: chat_id %>
8
+ <% if local_assigns[:is_structured_input] %>
9
+ <%= form.hidden_field :auto_advance, value: true %>
10
+ <% end %>
8
11
  <fieldset>
9
12
  <div class="lexi-form-container">
10
13
  <!-- Input Container -->
11
14
  <div class="lexi-input-container lexi-input-max-height">
12
- <!-- Input area with icons -->
13
- <div class="lexi-textarea-wrapper">
14
- <%= form.text_area :message, rows: 1, placeholder: random_chat_placeholder, disabled: local_assigns[:responding] || !local_assigns[:response_enabled], class: "lexi-textarea lexi-textarea-min-height", data: { action: "keypress.enter->meta-workflows--lexi-form-submit#handleKeyDown submit->meta-workflows--lexi-form-submit#handleSubmit", "meta-workflows--lexi-form-submit-target": "textarea" } %>
15
- </div>
15
+ <!-- Text Input -->
16
+ <div class="lexi-textarea-wrapper">
17
+ <%= form.text_area :message, rows: 1, placeholder: random_chat_placeholder, disabled: local_assigns[:responding] || !local_assigns[:response_enabled], class: "lexi-textarea lexi-textarea-min-height", data: { action: "keypress.enter->meta-workflows--lexi-form-submit#handleKeyDown submit->meta-workflows--lexi-form-submit#handleSubmit", "meta-workflows--lexi-form-submit-target": "textarea" } %>
18
+ </div>
16
19
 
17
- <div class="lexi-input-controls">
18
- <div class="lexi-input-icons">
19
- <button type="button" class="lexi-icon-button" disabled>
20
- <i class="fa-solid fa-microphone text-lg"></i>
21
- </button>
22
- <button type="button" class="lexi-icon-button" disabled>
23
- <i class="fa-solid fa-headphones text-lg"></i>
24
- </button>
25
- <button type="button" class="lexi-icon-button" disabled>
26
- <i class="fa-solid fa-waveform-lines text-lg"></i>
27
- </button>
28
- </div>
29
- <div>
30
- <!-- Send button -->
31
- <% if local_assigns[:responding] %>
32
- <button type="button" class="lexi-send-button responding" disabled>
33
- <i class="fa-solid fa-arrow-up text-lg"></i>
34
- </button>
35
- <% else %>
36
- <%= form.button type: "submit", class: "lexi-send-button sm-btn sm-btn-primary" do %>
37
- <i class="fa-solid fa-arrow-up text-lg"></i>
38
- <% end %>
39
- <% end %>
40
- </div>
41
- </div>
20
+ <div class="lexi-input-controls">
21
+ <div class="lexi-input-icons">
22
+ <button type="button" class="lexi-icon-button" disabled>
23
+ <i class="fa-solid fa-microphone text-lg"></i>
24
+ </button>
25
+ <button type="button" class="lexi-icon-button" disabled>
26
+ <i class="fa-solid fa-headphones text-lg"></i>
27
+ </button>
28
+ <button type="button" class="lexi-icon-button" disabled>
29
+ <i class="fa-solid fa-waveform-lines text-lg"></i>
30
+ </button>
31
+ </div>
32
+ <div>
33
+ <!-- Send button -->
34
+ <% if local_assigns[:responding] %>
35
+ <button type="button" class="lexi-send-button responding" disabled>
36
+ <i class="fa-solid fa-arrow-up text-lg"></i>
37
+ </button>
38
+ <% else %>
39
+ <%= form.button type: "submit", class: "lexi-send-button sm-btn sm-btn-primary" do %>
40
+ <i class="fa-solid fa-arrow-up text-lg"></i>
41
+ <% end %>
42
+ <% end %>
43
+ </div>
44
+ </div>
42
45
  </div>
43
46
 
44
47
  <p class="lexi-recording-notice">This chat is being recorded.</p>
@@ -48,7 +51,7 @@
48
51
  <!-- Empty placeholder to prevent layout jank -->
49
52
  <div aria-hidden="true"></div>
50
53
  <% else %>
51
- <%= form.button type: "submit", name: "advance", value: "true", class: "lexi-advance-button #{'opacity-50 cursor-not-allowed' if local_assigns[:responding]}", disabled: local_assigns[:responding] do %>
54
+ <%= form.button type: "submit", name: "manual_advance", value: "true", class: "lexi-advance-button #{'opacity-50 cursor-not-allowed' if local_assigns[:responding]}", disabled: local_assigns[:responding] do %>
52
55
  <i class="fa-light fa-arrow-right"></i> Next
53
56
  <% end %>
54
57
  <% end %>
@@ -1,46 +1,37 @@
1
1
  <%= turbo_frame_tag target_frame_id(record) do %>
2
2
  <div id="response-content-container" class="lexi-response-container" data-controller="response-scroll">
3
- <% if chat.present? && chat.messages.any? %>
4
-
5
- <!-- Show all saved messages except the first user message -->
6
- <% messages.each_with_index do |message, index| %>
7
- <% next if message.role == 'system' %>
8
- <% next if message.role == 'user' && message == user_messages.first %>
9
- <% next if is_streaming && message.role == 'assistant' && message == last_message %>
3
+
4
+ <!-- Show historical messages from ExecutionChatHistory -->
5
+ <% if local_assigns[:chat_history]&.history&.any? %>
6
+ <% local_assigns[:chat_history].history.each do |historical_message| %>
7
+ <% next if historical_message['role'] == 'system' %>
10
8
 
11
- <% if message.role == 'user' %>
12
- <!-- User message bubble (right-aligned, purple background) -->
13
- <div class="lexi-message-user">
14
- <div class="lexi-message-bubble-user">
15
- <div class="lexi-message-content-user">
16
- <%= simple_format(message.content) %>
17
- </div>
18
- </div>
19
- </div>
20
-
21
- <% elsif message.role == 'assistant' %>
22
- <!-- Lexi message bubble (left-aligned, white background) -->
23
- <div class="lexi-message-assistant">
24
- <!-- Message Bubble -->
25
- <div class="lexi-message-bubble-assistant">
26
- <div class="lexi-message-content-assistant">
27
- <%= markdown(message.content) %>
28
- </div>
29
- </div>
30
- </div>
9
+ <% if historical_message['role'] == 'user' %>
10
+ <%= render 'meta_workflows/user_message_bubble', content: historical_message['content'] %>
11
+ <% elsif historical_message['role'] == 'assistant' %>
12
+ <%= render 'meta_workflows/assistant_message_bubble', content: historical_message['content'] %>
31
13
  <% end %>
32
14
  <% end %>
33
-
15
+ <% end %>
16
+
17
+ <% if chat.present? && chat.messages.any? %>
34
18
  <!-- Show streaming response -->
35
19
  <% if is_streaming %>
36
- <div class="lexi-message-assistant">
37
- <!-- Streaming Message Bubble -->
38
- <div class="lexi-message-bubble-assistant">
39
- <div class="lexi-message-content-assistant">
40
- <%= markdown(full_response) %>
41
- </div>
42
- </div>
43
- </div>
20
+
21
+ <!-- Show all saved messages except the first user message -->
22
+ <% messages.each_with_index do |message, index| %>
23
+ <% next if message.role == 'system' %>
24
+ <% next if message.role == 'user' && message == user_messages.first %>
25
+ <% next if is_streaming && message.role == 'assistant' && message == last_message %>
26
+
27
+ <% if message.role == 'user' %>
28
+ <%= render 'meta_workflows/user_message_bubble', content: message.content %>
29
+ <% elsif message.role == 'assistant' %>
30
+ <%= render 'meta_workflows/assistant_message_bubble', content: message.content %>
31
+ <% end %>
32
+ <% end %>
33
+
34
+ <%= render 'meta_workflows/assistant_message_bubble', content: full_response %>
44
35
  <% end %>
45
36
  <% end %>
46
37
 
@@ -0,0 +1,60 @@
1
+ <div class="structured-input-container"
2
+ role="group"
3
+ aria-labelledby="structured-slider-legend">
4
+ <legend id="structured-slider-legend" class="sr-only">Select a value using the slider</legend>
5
+ <div class="structured-slider-container">
6
+ <div class="structured-slider-labels">
7
+ <span class="structured-slider-label-min"><%= options['min']['label'] %></span>
8
+ <span class="structured-slider-label-max"><%= options['max']['label'] %></span>
9
+ </div>
10
+
11
+ <div class="structured-slider-wrapper">
12
+ <input
13
+ type="range"
14
+ id="range_value"
15
+ name="range_value"
16
+ min="<%= options['min']['value'] %>"
17
+ max="<%= options['max']['value'] %>"
18
+ step="<%= options['increment'] %>"
19
+ value="<%= options['min']['value'] %>"
20
+ class="structured-slider-input"
21
+ data-action="change->meta-workflows--structured-form-submit#handleSubmit"
22
+ aria-describedby="structured-slider-help structured-slider-value-display"
23
+ aria-valuemin="<%= options['min']['value'] %>"
24
+ aria-valuemax="<%= options['max']['value'] %>"
25
+ aria-valuenow="<%= options['min']['value'] %>"
26
+ aria-valuetext="<%= options['min']['value'] %>"
27
+ required
28
+ >
29
+ </div>
30
+
31
+ <div class="structured-slider-value">
32
+ <span class="structured-slider-current-value"
33
+ id="structured-slider-value-display"
34
+ data-range-display="<%= options['min']['value'] %>"
35
+ aria-live="polite">
36
+ <%= options['min']['value'] %>
37
+ </span>
38
+ </div>
39
+ <div id="structured-slider-help" class="sr-only">Use arrow keys to adjust the value</div>
40
+ </div>
41
+ </div>
42
+
43
+ <script>
44
+ // Update the displayed value when slider changes
45
+ document.addEventListener('DOMContentLoaded', function() {
46
+ const slider = document.getElementById('range_value');
47
+ const valueDisplay = document.querySelector('.structured-slider-current-value');
48
+
49
+ if (slider && valueDisplay) {
50
+ slider.addEventListener('input', function() {
51
+ valueDisplay.textContent = this.value;
52
+ valueDisplay.setAttribute('data-range-display', this.value);
53
+
54
+ // Update ARIA attributes for accessibility
55
+ slider.setAttribute('aria-valuenow', this.value);
56
+ slider.setAttribute('aria-valuetext', this.value);
57
+ });
58
+ }
59
+ });
60
+ </script>
@@ -0,0 +1,28 @@
1
+ <%= turbo_frame_tag target_frame_id(record, structured_input: true) do %>
2
+ <% if local_assigns[:is_structured_input] && structured_input_config.present? %>
3
+ <%
4
+ # Get workflow execution for form submission
5
+ workflow_execution = record.workflow_executions.order(created_at: :desc).first
6
+ form_url = workflow_execution ? meta_workflows.structured_human_path(workflow_execution.id) : "#"
7
+ chat_id = local_assigns[:chat_id] || workflow_execution&.workflow_steps&.last&.chat&.id
8
+ %>
9
+
10
+ <%= form_with url: form_url,
11
+ method: :patch,
12
+ id: "#{target_frame_id(record, structured_input: true)}_form",
13
+ data: { controller: "meta-workflows--structured-form-submit", "meta-workflows--structured-form-submit-target": "form" } do |form| %>
14
+ <%= form.hidden_field :chat_id, value: chat_id %>
15
+
16
+ <div class="structured-input-wrapper">
17
+ <% case structured_input_config['type'] %>
18
+ <% when 'single_choice' %>
19
+ <%= render 'meta_workflows/radio_input', options: structured_input_config['options'] %>
20
+ <% when 'multiple_choice' %>
21
+ <%= render 'meta_workflows/checkbox_input', options: structured_input_config['options'] %>
22
+ <% when 'range' %>
23
+ <%= render 'meta_workflows/slider_input', options: structured_input_config['options'] %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
27
+ <% end %>
28
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <!-- User message bubble (right-aligned, purple background) -->
2
+ <div class="lexi-message-user">
3
+ <div class="lexi-message-bubble-user">
4
+ <div class="lexi-message-content-user">
5
+ <%= simple_format(content) %>
6
+ </div>
7
+ </div>
8
+ </div>
data/config/routes.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  MetaWorkflows::Engine.routes.draw do
4
4
  resources :humans, only: [:update]
5
+ resources :structured_humans, only: [:update]
5
6
 
6
7
  # Workflow imports - RESTful routes
7
8
  resources :workflow_imports, only: %i[new create], path: 'workflow_imports' do
@@ -0,0 +1,12 @@
1
+ class CreateMetaWorkflowsExecutionChatHistories < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :meta_workflows_execution_chat_histories do |t|
4
+ t.references :workflow_execution, null: false, foreign_key: { to_table: :meta_workflows_workflow_executions }
5
+ t.jsonb :history, null: false, default: []
6
+
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :meta_workflows_execution_chat_histories, :workflow_execution_id, unique: true, name: 'index_execution_chat_histories_on_workflow_execution_id'
11
+ end
12
+ end
@@ -3,7 +3,7 @@
3
3
  module MetaWorkflows
4
4
  MAJOR = 0
5
5
  MINOR = 9
6
- PATCH = 18 # this is automatically incremented by the build process
6
+ PATCH = 20 # this is automatically incremented by the build process
7
7
 
8
8
  VERSION = "#{MetaWorkflows::MAJOR}.#{MetaWorkflows::MINOR}.#{MetaWorkflows::PATCH}".freeze
9
9
  end
@@ -45,6 +45,8 @@ module Services
45
45
  process_agent_action(conversation, workflow_execution, workflow)
46
46
  when 'human'
47
47
  process_human_action(execution_step, workflow_execution)
48
+ when 'structured_human'
49
+ process_structured_human_action(execution_step, workflow_execution)
48
50
  when 'record_redirect'
49
51
  process_record_redirect_action(workflow_execution)
50
52
  when 'collection_create'
@@ -133,6 +135,18 @@ module Services
133
135
  # the step advancement is handled in the human input job
134
136
  end
135
137
 
138
+ def process_structured_human_action(execution_step, workflow_execution)
139
+ ::MetaWorkflows::HumanInputJob.perform_later(
140
+ user_id: user&.id,
141
+ record: workflow_execution.record,
142
+ params: {
143
+ inputs: inputs,
144
+ prompt_id: execution_step['prompt_id']
145
+ }
146
+ )
147
+ # the step advancement is handled in the human input job
148
+ end
149
+
136
150
  def process_record_redirect_action(workflow_execution)
137
151
  ::MetaWorkflows::RecordRedirectJob.perform_later(workflow_execution: workflow_execution)
138
152
  workflow_execution.increment_step
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: meta_workflows
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.18
4
+ version: 0.9.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Medovyy
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-07-14 00:00:00.000000000 Z
12
+ date: 2025-07-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -125,6 +125,7 @@ files:
125
125
  - app/assets/javascripts/meta_workflows/controllers/onboarding_controller.js
126
126
  - app/assets/javascripts/meta_workflows/controllers/redirect_controller.js
127
127
  - app/assets/javascripts/meta_workflows/controllers/response_scroll_controller.js
128
+ - app/assets/javascripts/meta_workflows/controllers/structured_form_submit_controller.js
128
129
  - app/assets/javascripts/meta_workflows/controllers/tray_controller.js
129
130
  - app/assets/javascripts/meta_workflows_manifest.js
130
131
  - app/assets/stylesheets/meta_workflows/application.css
@@ -135,6 +136,7 @@ files:
135
136
  - app/controllers/meta_workflows/debug_controller.rb
136
137
  - app/controllers/meta_workflows/humans_controller.rb
137
138
  - app/controllers/meta_workflows/meta_controller.rb
139
+ - app/controllers/meta_workflows/structured_humans_controller.rb
138
140
  - app/controllers/meta_workflows/workflow_imports_controller.rb
139
141
  - app/helpers/meta_workflows/application_helper.rb
140
142
  - app/helpers/meta_workflows/debug_helper.rb
@@ -143,6 +145,7 @@ files:
143
145
  - app/helpers/meta_workflows/meta_workflows_helper.rb
144
146
  - app/helpers/meta_workflows/status_badge_helper.rb
145
147
  - app/jobs/meta_workflows/application_job.rb
148
+ - app/jobs/meta_workflows/concerns/error_handling.rb
146
149
  - app/jobs/meta_workflows/human_input_job.rb
147
150
  - app/jobs/meta_workflows/meta_job.rb
148
151
  - app/jobs/meta_workflows/meta_workflow_job.rb
@@ -152,6 +155,7 @@ files:
152
155
  - app/models/meta_workflows.rb
153
156
  - app/models/meta_workflows/application_record.rb
154
157
  - app/models/meta_workflows/chat.rb
158
+ - app/models/meta_workflows/execution_chat_history.rb
155
159
  - app/models/meta_workflows/message.rb
156
160
  - app/models/meta_workflows/tool_call.rb
157
161
  - app/models/meta_workflows/workflow.rb
@@ -159,16 +163,23 @@ files:
159
163
  - app/models/meta_workflows/workflow_step.rb
160
164
  - app/services/meta_workflows/application_service.rb
161
165
  - app/services/meta_workflows/execution_filter_service.rb
166
+ - app/services/meta_workflows/message_history_service.rb
162
167
  - app/services/meta_workflows/workflow_import_service.rb
163
168
  - app/sidekiq/meta_workflows/tools/meta_workflow_tool.rb
164
169
  - app/views/layouts/meta_workflows/application.html.erb
170
+ - app/views/meta_workflows/_assistant_message_bubble.html.erb
171
+ - app/views/meta_workflows/_checkbox_input.html.erb
165
172
  - app/views/meta_workflows/_error_message.html.erb
166
173
  - app/views/meta_workflows/_lexi_chat_alpha_tray.html.erb
167
174
  - app/views/meta_workflows/_lexi_chat_right_tray.html.erb
168
175
  - app/views/meta_workflows/_loader_message.html.erb
176
+ - app/views/meta_workflows/_radio_input.html.erb
169
177
  - app/views/meta_workflows/_redirect.html.erb
170
178
  - app/views/meta_workflows/_response_form_lexi.html.erb
171
179
  - app/views/meta_workflows/_response_lexi.html.erb
180
+ - app/views/meta_workflows/_slider_input.html.erb
181
+ - app/views/meta_workflows/_structured_input.html.erb
182
+ - app/views/meta_workflows/_user_message_bubble.html.erb
172
183
  - app/views/meta_workflows/debug/executions.html.erb
173
184
  - app/views/meta_workflows/debug/show_execution.html.erb
174
185
  - app/views/meta_workflows/debug/show_workflow.html.erb
@@ -186,6 +197,7 @@ files:
186
197
  - db/migrate/20250618175439_add_call_id_and_status_to_meta_workflows_tool_calls.rb
187
198
  - db/migrate/20250626211926_add_repetition_to_meta_workflows_workflow_steps.rb
188
199
  - db/migrate/20250709153017_add_recipe_to_meta_workflows_workflow_executions.rb
200
+ - db/migrate/20250714192853_create_meta_workflows_execution_chat_histories.rb
189
201
  - lib/meta_workflows.rb
190
202
  - lib/meta_workflows/asset_installer.rb
191
203
  - lib/meta_workflows/configuration.rb