meta_workflows 0.8.23 → 0.9.0
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.
- checksums.yaml +4 -4
- data/app/controllers/meta_workflows/base_debug_controller.rb +46 -0
- data/app/controllers/meta_workflows/debug_controller.rb +1 -42
- data/app/controllers/meta_workflows/workflow_imports_controller.rb +85 -0
- data/app/services/meta_workflows/application_service.rb +38 -0
- data/app/services/meta_workflows/workflow_import_service.rb +116 -0
- data/app/views/meta_workflows/_lexi_chat_alpha_tray.html.erb +1 -1
- data/app/views/meta_workflows/_lexi_chat_right_tray.html.erb +1 -1
- data/app/views/meta_workflows/debug/workflows.html.erb +23 -10
- data/app/views/meta_workflows/workflow_imports/new.html.erb +171 -0
- data/config/routes.rb +7 -0
- data/lib/meta_workflows/version.rb +2 -2
- data/lib/services/meta_workflows/asset_installer_service.rb +95 -0
- data/lib/tasks/asset_installer_tasks.rake +12 -0
- data/lib/tasks/meta_workflows_tasks.rake +23 -136
- metadata +8 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3d50e1943ccd15113eba9afb7c8cbdc01d438c6d3d49a88ceeff0c99585f6a35
|
4
|
+
data.tar.gz: 903ed9aabed86d16a846caf25a96497ebd31ea3c6f1872a4c1ce4a55dbdfaeb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5641f236a0e35b67e90a4a557015d6135f2c03c8061ea5bfd2c4adc5c5ce20b9e90e3a357e5764479f71f3db3aaf81acba5093c6aae4102a7d49881584b0bce7
|
7
|
+
data.tar.gz: f591fffcd47a4f0ae49746a3d0aaa2d4c495eb39c6a171bedc049906617deba5493bf2e984ce1b1b4c50c6ad73d4811a774eb51a8b1894295e5a0c6278271ff7
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MetaWorkflows
|
4
|
+
class BaseDebugController < MetaWorkflows::ApplicationController
|
5
|
+
include MetaWorkflows::DebugHelper
|
6
|
+
|
7
|
+
layout 'meta_workflows/debug'
|
8
|
+
|
9
|
+
# Apply authentication only in production environment
|
10
|
+
before_action :authenticate_with_basic_auth, if: :production_environment?
|
11
|
+
|
12
|
+
# Skip CSRF protection for this debug interface
|
13
|
+
skip_before_action :verify_authenticity_token
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def authenticate_with_basic_auth
|
18
|
+
return unless production_environment?
|
19
|
+
return unless authentication_required?
|
20
|
+
|
21
|
+
authenticate_or_request_with_http_basic do |username, password|
|
22
|
+
username == ENV.fetch('SIDEKIQ_USER', nil) &&
|
23
|
+
password == ENV.fetch('SIDEKIQ_PASSWORD', nil)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def production_environment?
|
30
|
+
Rails.env.production?
|
31
|
+
end
|
32
|
+
|
33
|
+
def authentication_required?
|
34
|
+
ENV['SIDEKIQ_USER'].present? && ENV['SIDEKIQ_PASSWORD'].present?
|
35
|
+
end
|
36
|
+
|
37
|
+
def paginate_collection(collection)
|
38
|
+
# Use Kaminari if available, otherwise limit to 25 records
|
39
|
+
if defined?(Kaminari) && collection.respond_to?(:page)
|
40
|
+
collection.page(params[:page]).per(25)
|
41
|
+
else
|
42
|
+
collection.limit(25).offset(((params[:page]&.to_i || 1) - 1) * 25)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,17 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module MetaWorkflows
|
4
|
-
class DebugController < MetaWorkflows::
|
5
|
-
include MetaWorkflows::DebugHelper
|
6
|
-
|
7
|
-
layout 'meta_workflows/debug'
|
8
|
-
|
9
|
-
# Apply authentication only in production environment
|
10
|
-
before_action :authenticate_with_basic_auth, if: :production_environment?
|
11
|
-
|
12
|
-
# Skip CSRF protection for this debug interface
|
13
|
-
skip_before_action :verify_authenticity_token
|
14
|
-
|
4
|
+
class DebugController < MetaWorkflows::BaseDebugController
|
15
5
|
def index
|
16
6
|
redirect_to workflows_path
|
17
7
|
end
|
@@ -66,36 +56,5 @@ module MetaWorkflows
|
|
66
56
|
format.html { redirect_to execution_path(@execution), alert: t('meta_workflows.export_only_json') }
|
67
57
|
end
|
68
58
|
end
|
69
|
-
|
70
|
-
protected
|
71
|
-
|
72
|
-
def authenticate_with_basic_auth
|
73
|
-
return unless production_environment?
|
74
|
-
return unless authentication_required?
|
75
|
-
|
76
|
-
authenticate_or_request_with_http_basic do |username, password|
|
77
|
-
username == ENV.fetch('SIDEKIQ_USER', nil) &&
|
78
|
-
password == ENV.fetch('SIDEKIQ_PASSWORD', nil)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
private
|
83
|
-
|
84
|
-
def production_environment?
|
85
|
-
Rails.env.production?
|
86
|
-
end
|
87
|
-
|
88
|
-
def authentication_required?
|
89
|
-
ENV['SIDEKIQ_USER'].present? && ENV['SIDEKIQ_PASSWORD'].present?
|
90
|
-
end
|
91
|
-
|
92
|
-
def paginate_collection(collection)
|
93
|
-
# Use Kaminari if available, otherwise limit to 25 records
|
94
|
-
if defined?(Kaminari) && collection.respond_to?(:page)
|
95
|
-
collection.page(params[:page]).per(25)
|
96
|
-
else
|
97
|
-
collection.limit(25).offset(((params[:page]&.to_i || 1) - 1) * 25)
|
98
|
-
end
|
99
|
-
end
|
100
59
|
end
|
101
60
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MetaWorkflows
|
4
|
+
class WorkflowImportsController < MetaWorkflows::BaseDebugController
|
5
|
+
def new
|
6
|
+
# Show the import form
|
7
|
+
end
|
8
|
+
|
9
|
+
def create
|
10
|
+
result = perform_import
|
11
|
+
return unless result
|
12
|
+
|
13
|
+
handle_import_result(result)
|
14
|
+
end
|
15
|
+
|
16
|
+
def bulk
|
17
|
+
import_service = MetaWorkflows::WorkflowImportService.new
|
18
|
+
result = import_service.import_from_directory
|
19
|
+
|
20
|
+
if result.success?
|
21
|
+
data = result.data
|
22
|
+
flash[:notice] = data[:message]
|
23
|
+
else
|
24
|
+
flash[:alert] = result.error
|
25
|
+
end
|
26
|
+
|
27
|
+
redirect_to workflows_path
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def perform_import
|
33
|
+
import_service = MetaWorkflows::WorkflowImportService.new
|
34
|
+
|
35
|
+
case params[:import_type]
|
36
|
+
when 'file'
|
37
|
+
handle_file_import(import_service)
|
38
|
+
when 'text'
|
39
|
+
handle_text_import(import_service)
|
40
|
+
when 'directory'
|
41
|
+
import_service.import_from_directory
|
42
|
+
else
|
43
|
+
flash[:alert] = 'Please provide either a file, YAML text, or select directory import'
|
44
|
+
render :new
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_file_import(import_service)
|
50
|
+
if params[:workflow_file].present?
|
51
|
+
import_service.import_from_file(params[:workflow_file])
|
52
|
+
else
|
53
|
+
flash[:alert] = 'Please provide either a file, YAML text, or select directory import'
|
54
|
+
render :new
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_text_import(import_service)
|
60
|
+
if params[:workflow_name].present? && params[:yaml_content].present?
|
61
|
+
import_service.import_from_yaml_text(params[:workflow_name], params[:yaml_content])
|
62
|
+
else
|
63
|
+
flash[:alert] = 'Please provide either a file, YAML text, or select directory import'
|
64
|
+
render :new
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def handle_import_result(result)
|
70
|
+
if result.success?
|
71
|
+
import_result = result.data
|
72
|
+
flash[:notice] = success_message(import_result)
|
73
|
+
redirect_to workflows_path
|
74
|
+
else
|
75
|
+
flash[:alert] = result.error
|
76
|
+
render :new
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def success_message(import_result)
|
81
|
+
action = import_result[:action] == 'created' ? 'imported' : 'updated'
|
82
|
+
"Workflow '#{import_result[:workflow_name]}' #{action} successfully!"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MetaWorkflows
|
4
|
+
class ApplicationService
|
5
|
+
def self.call(...)
|
6
|
+
new(...).call
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def success(data = nil)
|
12
|
+
ServiceResult.new(success: true, data: data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def failure(error = nil)
|
16
|
+
ServiceResult.new(success: false, error: error)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Simple result object for service responses
|
21
|
+
class ServiceResult
|
22
|
+
attr_reader :data, :error
|
23
|
+
|
24
|
+
def initialize(success:, data: nil, error: nil)
|
25
|
+
@success = success
|
26
|
+
@data = data
|
27
|
+
@error = error
|
28
|
+
end
|
29
|
+
|
30
|
+
def success?
|
31
|
+
@success
|
32
|
+
end
|
33
|
+
|
34
|
+
def failure?
|
35
|
+
!@success
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MetaWorkflows
|
4
|
+
class WorkflowImportService < ApplicationService
|
5
|
+
attr_reader :import_results
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
@import_results = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def import_from_file(file)
|
13
|
+
return failure('No file provided') if file.blank?
|
14
|
+
|
15
|
+
begin
|
16
|
+
yaml_content = YAML.safe_load(file.read)
|
17
|
+
workflow_name = extract_workflow_name(file.original_filename)
|
18
|
+
|
19
|
+
result = save_workflow(workflow_name, yaml_content)
|
20
|
+
@import_results << result
|
21
|
+
|
22
|
+
success(result)
|
23
|
+
rescue Psych::SyntaxError => e
|
24
|
+
failure("Invalid YAML format: #{e.message}")
|
25
|
+
rescue StandardError => e
|
26
|
+
failure("Error processing file: #{e.message}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def import_from_yaml_text(workflow_name, yaml_text)
|
31
|
+
return failure('Workflow name cannot be blank') if workflow_name.blank?
|
32
|
+
return failure('YAML content cannot be blank') if yaml_text.blank?
|
33
|
+
|
34
|
+
begin
|
35
|
+
yaml_content = YAML.safe_load(yaml_text)
|
36
|
+
result = save_workflow(workflow_name.underscore, yaml_content)
|
37
|
+
@import_results << result
|
38
|
+
|
39
|
+
success(result)
|
40
|
+
rescue Psych::SyntaxError => e
|
41
|
+
failure("Invalid YAML format: #{e.message}")
|
42
|
+
rescue StandardError => e
|
43
|
+
failure("Error processing YAML: #{e.message}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def import_from_directory(directory_path = nil)
|
48
|
+
directory_path ||= Rails.root.join('workflows')
|
49
|
+
|
50
|
+
return failure("Directory not found: #{directory_path}") unless Dir.exist?(directory_path)
|
51
|
+
|
52
|
+
yaml_files = Dir.glob(File.join(directory_path, '*.yml'))
|
53
|
+
return failure('No YAML files found in directory') if yaml_files.empty?
|
54
|
+
|
55
|
+
successful_imports = 0
|
56
|
+
failed_imports = 0
|
57
|
+
|
58
|
+
yaml_files.each do |file_path|
|
59
|
+
result = import_single_file(file_path)
|
60
|
+
@import_results << result
|
61
|
+
|
62
|
+
if result[:success]
|
63
|
+
successful_imports += 1
|
64
|
+
else
|
65
|
+
failed_imports += 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
message = "Import completed: #{successful_imports} successful, #{failed_imports} failed"
|
70
|
+
success({ message: message, successful: successful_imports, failed: failed_imports })
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def extract_workflow_name(filename)
|
76
|
+
File.basename(filename, '.yml').underscore
|
77
|
+
end
|
78
|
+
|
79
|
+
def import_single_file(file_path)
|
80
|
+
file_name = File.basename(file_path, '.yml')
|
81
|
+
workflow_name = file_name.underscore
|
82
|
+
|
83
|
+
begin
|
84
|
+
yaml_content = YAML.load_file(file_path)
|
85
|
+
save_workflow(workflow_name, yaml_content)
|
86
|
+
rescue StandardError => e
|
87
|
+
{
|
88
|
+
success: false,
|
89
|
+
workflow_name: workflow_name,
|
90
|
+
error: e.message,
|
91
|
+
file_path: file_path
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def save_workflow(workflow_name, yaml_content)
|
97
|
+
workflow = MetaWorkflows::Workflow.find_or_initialize_by(name: workflow_name)
|
98
|
+
workflow.recipe = yaml_content
|
99
|
+
|
100
|
+
if workflow.save
|
101
|
+
{
|
102
|
+
success: true,
|
103
|
+
workflow_name: workflow.name,
|
104
|
+
action: workflow.previously_new_record? ? 'created' : 'updated',
|
105
|
+
workflow: workflow
|
106
|
+
}
|
107
|
+
else
|
108
|
+
{
|
109
|
+
success: false,
|
110
|
+
workflow_name: workflow_name,
|
111
|
+
error: workflow.errors.full_messages.join(', ')
|
112
|
+
}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -25,7 +25,7 @@
|
|
25
25
|
<!-- Main content area (chat + input) -->
|
26
26
|
<div class="flex-1 flex flex-col min-h-0">
|
27
27
|
<%# Chat messages area %>
|
28
|
-
<div class="flex-1 overflow-y-auto py-4 space-y-4 min-h-0">
|
28
|
+
<div id="main-scroll-container" class="flex-1 overflow-y-auto py-4 space-y-4 min-h-0">
|
29
29
|
<%= render partial: meta_loader, locals: {record: local_assigns[:record], step_progress: ["Generating draft course objectives...", "Talking to the LLM..."] } %>
|
30
30
|
</div>
|
31
31
|
|
@@ -16,7 +16,7 @@
|
|
16
16
|
</div>
|
17
17
|
|
18
18
|
<%# Chat messages area %>
|
19
|
-
<div class="flex-1 overflow-y-auto py-4 space-y-4">
|
19
|
+
<div id="main-scroll-container" class="flex-1 overflow-y-auto py-4 space-y-4">
|
20
20
|
<%= render partial: meta_loader, locals: {record: local_assigns[:record], step_progress: ["Generating draft course objectives...", "Talking to the LLM..."] } %>
|
21
21
|
</div>
|
22
22
|
|
@@ -9,16 +9,29 @@
|
|
9
9
|
</p>
|
10
10
|
</div>
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
<%=
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
class: "
|
21
|
-
|
12
|
+
<div class="flex items-center space-x-3">
|
13
|
+
<!-- Import Actions -->
|
14
|
+
<div class="flex items-center space-x-2">
|
15
|
+
<%= link_to "Import Workflow", new_workflow_import_path,
|
16
|
+
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %>
|
17
|
+
|
18
|
+
<%= button_to "Reimport All", bulk_import_workflow_imports_path,
|
19
|
+
method: :post,
|
20
|
+
class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
|
21
|
+
data: { confirm: "This will reimport all workflows from the workflows directory. Continue?" } %>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<!-- Search -->
|
25
|
+
<div class="w-64">
|
26
|
+
<%= form_with url: workflows_path, method: :get, local: true, class: "flex" do |form| %>
|
27
|
+
<%= form.text_field :search,
|
28
|
+
placeholder: "Search workflows...",
|
29
|
+
value: params[:search],
|
30
|
+
class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
31
|
+
<%= form.submit "Search",
|
32
|
+
class: "ml-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
33
|
+
<% end %>
|
34
|
+
</div>
|
22
35
|
</div>
|
23
36
|
</div>
|
24
37
|
</div>
|
@@ -0,0 +1,171 @@
|
|
1
|
+
<div class="bg-white shadow rounded-lg">
|
2
|
+
<!-- Header -->
|
3
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
4
|
+
<div class="flex items-center justify-between">
|
5
|
+
<div>
|
6
|
+
<h2 class="text-lg font-medium text-gray-900">Import Workflow</h2>
|
7
|
+
<p class="mt-1 text-sm text-gray-500">
|
8
|
+
Import a new workflow or update an existing one
|
9
|
+
</p>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div>
|
13
|
+
<%= link_to "Back to Workflows", workflows_path,
|
14
|
+
class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<!-- Import Form -->
|
20
|
+
<div class="px-6 py-6">
|
21
|
+
<%= form_with url: workflow_imports_path, method: :post, local: true, multipart: true, class: "space-y-6" do |form| %>
|
22
|
+
|
23
|
+
<!-- Import Type Selection -->
|
24
|
+
<div>
|
25
|
+
<label class="text-base font-medium text-gray-900">Import Method</label>
|
26
|
+
<p class="text-sm leading-5 text-gray-500">Choose how you want to import the workflow</p>
|
27
|
+
<fieldset class="mt-4">
|
28
|
+
<legend class="sr-only">Import method</legend>
|
29
|
+
<div class="space-y-4">
|
30
|
+
<div class="flex items-center">
|
31
|
+
<%= form.radio_button :import_type, "file", id: "import_type_file",
|
32
|
+
class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300",
|
33
|
+
checked: true %>
|
34
|
+
<label for="import_type_file" class="ml-3 block text-sm font-medium text-gray-700">
|
35
|
+
Upload YAML File
|
36
|
+
</label>
|
37
|
+
</div>
|
38
|
+
<div class="flex items-center">
|
39
|
+
<%= form.radio_button :import_type, "text", id: "import_type_text",
|
40
|
+
class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" %>
|
41
|
+
<label for="import_type_text" class="ml-3 block text-sm font-medium text-gray-700">
|
42
|
+
Paste YAML Content
|
43
|
+
</label>
|
44
|
+
</div>
|
45
|
+
<div class="flex items-center">
|
46
|
+
<%= form.radio_button :import_type, "directory", id: "import_type_directory",
|
47
|
+
class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" %>
|
48
|
+
<label for="import_type_directory" class="ml-3 block text-sm font-medium text-gray-700">
|
49
|
+
Import from workflows directory
|
50
|
+
</label>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
</fieldset>
|
54
|
+
</div>
|
55
|
+
|
56
|
+
<!-- File Upload Section -->
|
57
|
+
<div id="file_upload_section" class="space-y-4">
|
58
|
+
<div>
|
59
|
+
<label for="workflow_file" class="block text-sm font-medium text-gray-700">
|
60
|
+
Upload Workflow File
|
61
|
+
</label>
|
62
|
+
<div class="mt-1">
|
63
|
+
<%= form.file_field :workflow_file,
|
64
|
+
accept: ".yml,.yaml",
|
65
|
+
class: "block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" %>
|
66
|
+
</div>
|
67
|
+
<p class="mt-2 text-sm text-gray-500">
|
68
|
+
Upload a YAML file containing the workflow definition
|
69
|
+
</p>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
|
73
|
+
<!-- Text Input Section -->
|
74
|
+
<div id="text_input_section" class="space-y-4" style="display: none;">
|
75
|
+
<div>
|
76
|
+
<label for="workflow_name" class="block text-sm font-medium text-gray-700">
|
77
|
+
Workflow Name
|
78
|
+
</label>
|
79
|
+
<div class="mt-1">
|
80
|
+
<%= form.text_field :workflow_name,
|
81
|
+
placeholder: "e.g., post_creation",
|
82
|
+
class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
|
83
|
+
</div>
|
84
|
+
<p class="mt-2 text-sm text-gray-500">
|
85
|
+
Enter a unique name for this workflow (will be converted to snake_case)
|
86
|
+
</p>
|
87
|
+
</div>
|
88
|
+
|
89
|
+
<div>
|
90
|
+
<label for="yaml_content" class="block text-sm font-medium text-gray-700">
|
91
|
+
YAML Content
|
92
|
+
</label>
|
93
|
+
<div class="mt-1">
|
94
|
+
<%= form.text_area :yaml_content,
|
95
|
+
rows: 20,
|
96
|
+
placeholder: "steps:\n - name: \"example_step\"\n action: \"human\"\n prompt_id: 'example_prompt'\n ...",
|
97
|
+
class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono" %>
|
98
|
+
</div>
|
99
|
+
<p class="mt-2 text-sm text-gray-500">
|
100
|
+
Paste the YAML content for the workflow
|
101
|
+
</p>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<!-- Directory Import Section -->
|
106
|
+
<div id="directory_import_section" class="space-y-4" style="display: none;">
|
107
|
+
<div class="rounded-md bg-blue-50 p-4">
|
108
|
+
<div class="flex">
|
109
|
+
<div class="flex-shrink-0">
|
110
|
+
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
111
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
112
|
+
</svg>
|
113
|
+
</div>
|
114
|
+
<div class="ml-3 flex-1 md:flex md:justify-between">
|
115
|
+
<p class="text-sm text-blue-700">
|
116
|
+
This will import all YAML files from the <code>workflows</code> directory.
|
117
|
+
Existing workflows will be updated with new content.
|
118
|
+
</p>
|
119
|
+
</div>
|
120
|
+
</div>
|
121
|
+
</div>
|
122
|
+
</div>
|
123
|
+
|
124
|
+
<!-- Submit Button -->
|
125
|
+
<div class="flex justify-end">
|
126
|
+
<%= form.submit "Import Workflow",
|
127
|
+
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
128
|
+
</div>
|
129
|
+
<% end %>
|
130
|
+
</div>
|
131
|
+
</div>
|
132
|
+
|
133
|
+
<script>
|
134
|
+
// JavaScript to toggle form sections based on selected import type
|
135
|
+
document.addEventListener('DOMContentLoaded', function() {
|
136
|
+
const importTypeRadios = document.querySelectorAll('input[name="import_type"]');
|
137
|
+
const fileSection = document.getElementById('file_upload_section');
|
138
|
+
const textSection = document.getElementById('text_input_section');
|
139
|
+
const directorySection = document.getElementById('directory_import_section');
|
140
|
+
|
141
|
+
function toggleSections() {
|
142
|
+
const selectedType = document.querySelector('input[name="import_type"]:checked').value;
|
143
|
+
|
144
|
+
// Hide all sections
|
145
|
+
fileSection.style.display = 'none';
|
146
|
+
textSection.style.display = 'none';
|
147
|
+
directorySection.style.display = 'none';
|
148
|
+
|
149
|
+
// Show relevant section
|
150
|
+
switch (selectedType) {
|
151
|
+
case 'file':
|
152
|
+
fileSection.style.display = 'block';
|
153
|
+
break;
|
154
|
+
case 'text':
|
155
|
+
textSection.style.display = 'block';
|
156
|
+
break;
|
157
|
+
case 'directory':
|
158
|
+
directorySection.style.display = 'block';
|
159
|
+
break;
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
// Add event listeners
|
164
|
+
importTypeRadios.forEach(radio => {
|
165
|
+
radio.addEventListener('change', toggleSections);
|
166
|
+
});
|
167
|
+
|
168
|
+
// Initialize on page load
|
169
|
+
toggleSections();
|
170
|
+
});
|
171
|
+
</script>
|
data/config/routes.rb
CHANGED
@@ -3,6 +3,13 @@
|
|
3
3
|
MetaWorkflows::Engine.routes.draw do
|
4
4
|
resources :humans, only: [:update]
|
5
5
|
|
6
|
+
# Workflow imports - RESTful routes
|
7
|
+
resources :workflow_imports, only: %i[new create], path: 'workflow_imports' do
|
8
|
+
collection do
|
9
|
+
post :bulk, as: :bulk_import
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
6
13
|
# Debug interface routes
|
7
14
|
root 'debug#index'
|
8
15
|
get 'workflows', to: 'debug#workflows'
|
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
module MetaWorkflows
|
4
4
|
MAJOR = 0
|
5
|
-
MINOR =
|
6
|
-
PATCH =
|
5
|
+
MINOR = 9
|
6
|
+
PATCH = 0 # this is automatically incremented by the build process
|
7
7
|
|
8
8
|
VERSION = "#{MetaWorkflows::MAJOR}.#{MetaWorkflows::MINOR}.#{MetaWorkflows::PATCH}".freeze
|
9
9
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../meta_workflows/asset_installer'
|
4
|
+
|
5
|
+
module MetaWorkflows
|
6
|
+
# Service class to handle asset installation
|
7
|
+
class AssetInstallerService
|
8
|
+
def install
|
9
|
+
puts 'Installing meta_workflows assets...'
|
10
|
+
|
11
|
+
installer = MetaWorkflows::AssetInstaller.new
|
12
|
+
|
13
|
+
# Pre-installation validation
|
14
|
+
unless installer.valid_environment?
|
15
|
+
puts 'Installation aborted due to validation errors.'
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
process_asset_installation(installer)
|
21
|
+
rescue MetaWorkflows::AssetInstaller::InstallationError => e
|
22
|
+
handle_installation_error(e)
|
23
|
+
rescue StandardError => e
|
24
|
+
handle_unexpected_error(e)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def process_asset_installation(installer)
|
31
|
+
assets = discover_and_validate_assets(installer)
|
32
|
+
return if assets.empty?
|
33
|
+
|
34
|
+
result = installer.install_assets(assets)
|
35
|
+
display_installation_results(installer, assets, result)
|
36
|
+
end
|
37
|
+
|
38
|
+
def discover_and_validate_assets(installer)
|
39
|
+
assets = installer.discover_assets
|
40
|
+
|
41
|
+
if assets.empty?
|
42
|
+
puts 'No assets found to install.'
|
43
|
+
return []
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "Found #{assets.count} asset(s) to install:"
|
47
|
+
assets.each { |asset| puts " - #{asset[:relative_path]}" }
|
48
|
+
puts
|
49
|
+
|
50
|
+
assets
|
51
|
+
end
|
52
|
+
|
53
|
+
def display_installation_results(installer, assets, result)
|
54
|
+
puts
|
55
|
+
if result[:success]
|
56
|
+
display_successful_installation(installer, assets, result)
|
57
|
+
else
|
58
|
+
display_partial_installation(installer, assets, result)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def display_successful_installation(installer, assets, result)
|
63
|
+
puts 'Installation completed successfully!'
|
64
|
+
puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped."
|
65
|
+
|
66
|
+
return unless result[:copied].positive?
|
67
|
+
|
68
|
+
puts
|
69
|
+
installer.display_integration_instructions(assets, result)
|
70
|
+
end
|
71
|
+
|
72
|
+
def display_partial_installation(installer, assets, result)
|
73
|
+
puts 'Installation completed with errors.'
|
74
|
+
puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped, " \
|
75
|
+
"#{result[:failed]} file(s) failed."
|
76
|
+
puts 'Some files may have been partially installed. Please review the output above.'
|
77
|
+
|
78
|
+
return unless result[:copied].positive?
|
79
|
+
|
80
|
+
puts
|
81
|
+
puts 'For successfully copied files, see integration instructions below:'
|
82
|
+
installer.display_integration_instructions(assets, result)
|
83
|
+
end
|
84
|
+
|
85
|
+
def handle_installation_error(error)
|
86
|
+
puts "Installation failed: #{error.message}"
|
87
|
+
puts 'Installation aborted.'
|
88
|
+
end
|
89
|
+
|
90
|
+
def handle_unexpected_error(error)
|
91
|
+
puts "Unexpected error during asset installation: #{error.message}"
|
92
|
+
puts 'Installation aborted.'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../services/meta_workflows/asset_installer_service'
|
4
|
+
|
5
|
+
namespace :meta_workflows do
|
6
|
+
namespace :install do
|
7
|
+
desc 'Install meta_workflows assets (JavaScript controllers and stylesheets) into host application'
|
8
|
+
task assets: :environment do
|
9
|
+
MetaWorkflows::AssetInstallerService.new.install
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -2,152 +2,39 @@
|
|
2
2
|
|
3
3
|
require 'yaml'
|
4
4
|
require 'fileutils'
|
5
|
-
require_relative '../meta_workflows/asset_installer'
|
6
5
|
|
7
6
|
namespace :meta_workflows do
|
8
7
|
desc 'Import workflows from YAML files in the workflows directory'
|
9
8
|
task import: :environment do
|
10
|
-
MetaWorkflows::
|
11
|
-
|
9
|
+
import_service = MetaWorkflows::WorkflowImportService.new
|
10
|
+
result = import_service.import_from_directory
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
if result.success?
|
13
|
+
puts result.data[:message]
|
14
|
+
display_import_results(import_service) if import_service.import_results.any?
|
15
|
+
else
|
16
|
+
puts "Import failed: #{result.error}"
|
17
|
+
exit 1
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@workflows_path = Rails.root.join('workflows')
|
26
|
-
end
|
27
|
-
|
28
|
-
def import_workflows
|
29
|
-
puts "Importing workflows from #{@workflows_path}..."
|
30
|
-
|
31
|
-
Dir.glob(File.join(@workflows_path, '*.yml')).each do |file_path|
|
32
|
-
import_single_workflow(file_path)
|
33
|
-
end
|
34
|
-
|
35
|
-
puts 'Workflow import completed!'
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def import_single_workflow(file_path)
|
41
|
-
file_name = File.basename(file_path, '.yml')
|
42
|
-
workflow_name = file_name.underscore
|
43
|
-
|
44
|
-
begin
|
45
|
-
yaml_content = YAML.load_file(file_path)
|
46
|
-
save_workflow(workflow_name, yaml_content)
|
47
|
-
rescue StandardError => e
|
48
|
-
puts "Error processing #{file_path}: #{e.message}"
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def save_workflow(workflow_name, yaml_content)
|
53
|
-
workflow = MetaWorkflows::Workflow.find_or_initialize_by(name: workflow_name)
|
54
|
-
workflow.recipe = yaml_content
|
55
|
-
|
56
|
-
if workflow.save
|
57
|
-
puts "Successfully imported workflow: #{workflow.name}"
|
58
|
-
else
|
59
|
-
puts "Failed to import workflow #{workflow_name}: #{workflow.errors.full_messages.join(', ')}"
|
60
|
-
end
|
61
|
-
end
|
22
|
+
def display_import_results(import_service)
|
23
|
+
puts "\nDetailed results:"
|
24
|
+
import_service.import_results.each do |import_result|
|
25
|
+
display_single_import_result(import_result)
|
62
26
|
end
|
27
|
+
end
|
63
28
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
# Pre-installation validation
|
72
|
-
unless installer.valid_environment?
|
73
|
-
puts 'Installation aborted due to validation errors.'
|
74
|
-
return
|
75
|
-
end
|
76
|
-
|
77
|
-
begin
|
78
|
-
process_asset_installation(installer)
|
79
|
-
rescue MetaWorkflows::AssetInstaller::InstallationError => e
|
80
|
-
handle_installation_error(e)
|
81
|
-
rescue StandardError => e
|
82
|
-
handle_unexpected_error(e)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
private
|
87
|
-
|
88
|
-
def process_asset_installation(installer)
|
89
|
-
assets = discover_and_validate_assets(installer)
|
90
|
-
return if assets.empty?
|
91
|
-
|
92
|
-
result = installer.install_assets(assets)
|
93
|
-
display_installation_results(installer, assets, result)
|
94
|
-
end
|
95
|
-
|
96
|
-
def discover_and_validate_assets(installer)
|
97
|
-
assets = installer.discover_assets
|
98
|
-
|
99
|
-
if assets.empty?
|
100
|
-
puts 'No assets found to install.'
|
101
|
-
return []
|
102
|
-
end
|
103
|
-
|
104
|
-
puts "Found #{assets.count} asset(s) to install:"
|
105
|
-
assets.each { |asset| puts " - #{asset[:relative_path]}" }
|
106
|
-
puts
|
107
|
-
|
108
|
-
assets
|
109
|
-
end
|
110
|
-
|
111
|
-
def display_installation_results(installer, assets, result)
|
112
|
-
puts
|
113
|
-
if result[:success]
|
114
|
-
display_successful_installation(installer, assets, result)
|
115
|
-
else
|
116
|
-
display_partial_installation(installer, assets, result)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def display_successful_installation(installer, assets, result)
|
121
|
-
puts 'Installation completed successfully!'
|
122
|
-
puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped."
|
123
|
-
|
124
|
-
return unless result[:copied].positive?
|
125
|
-
|
126
|
-
puts
|
127
|
-
installer.display_integration_instructions(assets, result)
|
128
|
-
end
|
129
|
-
|
130
|
-
def display_partial_installation(installer, assets, result)
|
131
|
-
puts 'Installation completed with errors.'
|
132
|
-
puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped, " \
|
133
|
-
"#{result[:failed]} file(s) failed."
|
134
|
-
puts 'Some files may have been partially installed. Please review the output above.'
|
135
|
-
|
136
|
-
return unless result[:copied].positive?
|
137
|
-
|
138
|
-
puts
|
139
|
-
puts 'For successfully copied files, see integration instructions below:'
|
140
|
-
installer.display_integration_instructions(assets, result)
|
141
|
-
end
|
142
|
-
|
143
|
-
def handle_installation_error(error)
|
144
|
-
puts "Installation failed: #{error.message}"
|
145
|
-
puts 'Installation aborted.'
|
146
|
-
end
|
147
|
-
|
148
|
-
def handle_unexpected_error(error)
|
149
|
-
puts "Unexpected error during asset installation: #{error.message}"
|
150
|
-
puts 'Installation aborted.'
|
151
|
-
end
|
29
|
+
def display_single_import_result(import_result)
|
30
|
+
if import_result[:success]
|
31
|
+
action = determine_import_action(import_result[:action])
|
32
|
+
puts "✓ Successfully #{action} workflow: #{import_result[:workflow_name]}"
|
33
|
+
else
|
34
|
+
puts "✗ Failed to import workflow #{import_result[:workflow_name]}: #{import_result[:error]}"
|
152
35
|
end
|
153
36
|
end
|
37
|
+
|
38
|
+
def determine_import_action(action)
|
39
|
+
action == 'created' ? 'imported' : 'updated'
|
40
|
+
end
|
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.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leonid Medovyy
|
@@ -127,9 +127,11 @@ files:
|
|
127
127
|
- app/assets/stylesheets/meta_workflows/application.css
|
128
128
|
- app/controllers/concerns/tray_configurable.rb
|
129
129
|
- app/controllers/meta_workflows/application_controller.rb
|
130
|
+
- app/controllers/meta_workflows/base_debug_controller.rb
|
130
131
|
- app/controllers/meta_workflows/debug_controller.rb
|
131
132
|
- app/controllers/meta_workflows/humans_controller.rb
|
132
133
|
- app/controllers/meta_workflows/meta_controller.rb
|
134
|
+
- app/controllers/meta_workflows/workflow_imports_controller.rb
|
133
135
|
- app/helpers/meta_workflows/application_helper.rb
|
134
136
|
- app/helpers/meta_workflows/debug_helper.rb
|
135
137
|
- app/helpers/meta_workflows/execution_helper.rb
|
@@ -150,7 +152,9 @@ files:
|
|
150
152
|
- app/models/meta_workflows/workflow.rb
|
151
153
|
- app/models/meta_workflows/workflow_execution.rb
|
152
154
|
- app/models/meta_workflows/workflow_step.rb
|
155
|
+
- app/services/meta_workflows/application_service.rb
|
153
156
|
- app/services/meta_workflows/execution_filter_service.rb
|
157
|
+
- app/services/meta_workflows/workflow_import_service.rb
|
154
158
|
- app/sidekiq/meta_workflows/tools/meta_workflow_tool.rb
|
155
159
|
- app/views/layouts/meta_workflows/application.html.erb
|
156
160
|
- app/views/layouts/meta_workflows/debug.html.erb
|
@@ -164,6 +168,7 @@ files:
|
|
164
168
|
- app/views/meta_workflows/debug/show_execution.html.erb
|
165
169
|
- app/views/meta_workflows/debug/show_workflow.html.erb
|
166
170
|
- app/views/meta_workflows/debug/workflows.html.erb
|
171
|
+
- app/views/meta_workflows/workflow_imports/new.html.erb
|
167
172
|
- config/routes.rb
|
168
173
|
- db/migrate/20250530220618_create_meta_workflows_workflows.rb
|
169
174
|
- db/migrate/20250530220634_create_meta_workflows_workflow_executions.rb
|
@@ -182,8 +187,10 @@ files:
|
|
182
187
|
- lib/meta_workflows/export_execution_service.rb
|
183
188
|
- lib/meta_workflows/version.rb
|
184
189
|
- lib/services/meta_workflows/application_service.rb
|
190
|
+
- lib/services/meta_workflows/asset_installer_service.rb
|
185
191
|
- lib/services/meta_workflows/meta_workflow_service.rb
|
186
192
|
- lib/services/meta_workflows/updaters/meta_service.rb
|
193
|
+
- lib/tasks/asset_installer_tasks.rake
|
187
194
|
- lib/tasks/git_hooks.rake
|
188
195
|
- lib/tasks/meta_workflows_tasks.rake
|
189
196
|
homepage: https://github.com/strongmind/meta-workflows
|