rails_ai_admin 0.1.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 +7 -0
- data/CHANGELOG.md +458 -0
- data/LICENSE +21 -0
- data/README.md +292 -0
- data/SECURITY.md +14 -0
- data/THIRD_PARTY_NOTICES.md +19 -0
- data/app/assets/builds/rails_ai_admin.LICENSE.txt +55 -0
- data/app/assets/builds/rails_ai_admin.css +345 -0
- data/app/assets/builds/rails_ai_admin.js +33 -0
- data/app/controllers/rails_ai_admin/application_controller.rb +255 -0
- data/app/controllers/rails_ai_admin/assistant_controller.rb +164 -0
- data/app/controllers/rails_ai_admin/audit_controller.rb +16 -0
- data/app/controllers/rails_ai_admin/collection_jobs_controller.rb +33 -0
- data/app/controllers/rails_ai_admin/collection_operations_controller.rb +79 -0
- data/app/controllers/rails_ai_admin/custom_controller.rb +26 -0
- data/app/controllers/rails_ai_admin/dashboard_controller.rb +17 -0
- data/app/controllers/rails_ai_admin/exports_controller.rb +31 -0
- data/app/controllers/rails_ai_admin/files_controller.rb +37 -0
- data/app/controllers/rails_ai_admin/imports_controller.rb +41 -0
- data/app/controllers/rails_ai_admin/mcp_controller.rb +81 -0
- data/app/controllers/rails_ai_admin/mcp_tokens_controller.rb +45 -0
- data/app/controllers/rails_ai_admin/operations_controller.rb +105 -0
- data/app/controllers/rails_ai_admin/references_controller.rb +16 -0
- data/app/controllers/rails_ai_admin/resources_controller.rb +290 -0
- data/app/controllers/rails_ai_admin/search_controller.rb +39 -0
- data/app/controllers/rails_ai_admin/selections_controller.rb +95 -0
- data/app/controllers/rails_ai_admin/surfaces_controller.rb +122 -0
- data/app/helpers/rails_ai_admin/dashboard_helper.rb +28 -0
- data/app/helpers/rails_ai_admin/fields_helper.rb +242 -0
- data/app/helpers/rails_ai_admin/filters_helper.rb +40 -0
- data/app/helpers/rails_ai_admin/history_helper.rb +36 -0
- data/app/helpers/rails_ai_admin/navigation_helper.rb +54 -0
- data/app/helpers/rails_ai_admin/tool_views_helper.rb +48 -0
- data/app/jobs/rails_ai_admin/assistant_turn_job.rb +15 -0
- data/app/jobs/rails_ai_admin/collections/execution_job.rb +12 -0
- data/app/jobs/rails_ai_admin/outbox/delivery_job.rb +13 -0
- data/app/models/rails_ai_admin/application_record.rb +8 -0
- data/app/models/rails_ai_admin/audit_event.rb +12 -0
- data/app/models/rails_ai_admin/batch_item.rb +8 -0
- data/app/models/rails_ai_admin/operation_run.rb +20 -0
- data/app/models/rails_ai_admin/selection.rb +9 -0
- data/app/models/rails_ai_admin/selection_item.rb +9 -0
- data/app/models/rails_ai_admin/work_item.rb +18 -0
- data/app/views/layouts/rails_ai_admin/application.html.erb +49 -0
- data/app/views/layouts/rails_ai_admin/frame.html.erb +5 -0
- data/app/views/layouts/rails_ai_admin/status.html.erb +13 -0
- data/app/views/rails_ai_admin/assistant/_ask.html.erb +16 -0
- data/app/views/rails_ai_admin/assistant/_transcript.html.erb +45 -0
- data/app/views/rails_ai_admin/assistant/frame.html.erb +1 -0
- data/app/views/rails_ai_admin/assistant/index.html.erb +14 -0
- data/app/views/rails_ai_admin/assistant/show.html.erb +44 -0
- data/app/views/rails_ai_admin/audit/index.html.erb +40 -0
- data/app/views/rails_ai_admin/collection_jobs/index.html.erb +5 -0
- data/app/views/rails_ai_admin/collection_jobs/show.html.erb +24 -0
- data/app/views/rails_ai_admin/collection_operations/confirmation.html.erb +25 -0
- data/app/views/rails_ai_admin/collection_operations/failure.html.erb +10 -0
- data/app/views/rails_ai_admin/collection_operations/new.html.erb +11 -0
- data/app/views/rails_ai_admin/dashboard/index.html.erb +33 -0
- data/app/views/rails_ai_admin/fields/_index.html.erb +1 -0
- data/app/views/rails_ai_admin/fields/_input.html.erb +21 -0
- data/app/views/rails_ai_admin/fields/_reference.html.erb +1 -0
- data/app/views/rails_ai_admin/fields/_reference_picker.html.erb +14 -0
- data/app/views/rails_ai_admin/fields/_row.html.erb +27 -0
- data/app/views/rails_ai_admin/fields/_show.html.erb +1 -0
- data/app/views/rails_ai_admin/imports/new.html.erb +34 -0
- data/app/views/rails_ai_admin/mcp_tokens/index.html.erb +33 -0
- data/app/views/rails_ai_admin/operations/confirmation.html.erb +32 -0
- data/app/views/rails_ai_admin/operations/failure.html.erb +5 -0
- data/app/views/rails_ai_admin/operations/form.html.erb +13 -0
- data/app/views/rails_ai_admin/resources/_associations.html.erb +12 -0
- data/app/views/rails_ai_admin/resources/_errors.html.erb +14 -0
- data/app/views/rails_ai_admin/resources/_recovery.html.erb +5 -0
- data/app/views/rails_ai_admin/resources/confirm_destroy.html.erb +11 -0
- data/app/views/rails_ai_admin/resources/edit.html.erb +1 -0
- data/app/views/rails_ai_admin/resources/form.html.erb +16 -0
- data/app/views/rails_ai_admin/resources/history.html.erb +27 -0
- data/app/views/rails_ai_admin/resources/index.html.erb +88 -0
- data/app/views/rails_ai_admin/resources/new.html.erb +1 -0
- data/app/views/rails_ai_admin/resources/show.html.erb +12 -0
- data/app/views/rails_ai_admin/search/index.html.erb +20 -0
- data/app/views/rails_ai_admin/selections/confirm_destroy.html.erb +13 -0
- data/app/views/rails_ai_admin/selections/destroyed.html.erb +15 -0
- data/app/views/rails_ai_admin/selections/show.html.erb +19 -0
- data/app/views/rails_ai_admin/shared/_input_fields.html.erb +23 -0
- data/app/views/rails_ai_admin/shared/_navigation.html.erb +8 -0
- data/app/views/rails_ai_admin/shared/_session.html.erb +5 -0
- data/app/views/rails_ai_admin/statuses/show.html.erb +4 -0
- data/app/views/rails_ai_admin/surfaces/_operation_review.html.erb +43 -0
- data/app/views/rails_ai_admin/surfaces/show.html.erb +4 -0
- data/config/locales/en.yml +549 -0
- data/config/routes.rb +64 -0
- data/db/assistant_migrate/20261103000001_create_rails_ai_admin_assistant.rb +138 -0
- data/db/assistant_migrate/20261112000001_upgrade_rails_ai_admin_assistant_to_ruby_llm_2_2.rb +102 -0
- data/db/assistant_migrate/20261114000001_add_streaming_and_access_tokens_to_rails_ai_admin_assistant.rb +29 -0
- data/db/migrate/20261029000001_create_rails_ai_admin_execution_tables.rb +35 -0
- data/db/migrate/20261029000002_add_callback_warning_to_rails_ai_admin_runs.rb +7 -0
- data/db/migrate/20261030000003_create_rails_ai_admin_work_items.rb +26 -0
- data/db/migrate/20261030000004_create_rails_ai_admin_selections.rb +28 -0
- data/db/migrate/20261030000005_create_rails_ai_admin_collection_runs.rb +41 -0
- data/db/migrate/20261113000001_add_record_index_to_rails_ai_admin_audit_events.rb +8 -0
- data/lib/generators/rails_ai_admin/assistant/assistant_generator.rb +25 -0
- data/lib/generators/rails_ai_admin/assistant/templates/admin_assistant.rb +18 -0
- data/lib/generators/rails_ai_admin/engine_migrations.rb +42 -0
- data/lib/generators/rails_ai_admin/install/engine_mounts.rb +19 -0
- data/lib/generators/rails_ai_admin/install/install_generator.rb +53 -0
- data/lib/generators/rails_ai_admin/install/templates/access_test.rb +17 -0
- data/lib/generators/rails_ai_admin/install/templates/admin_context.rb +25 -0
- data/lib/generators/rails_ai_admin/install/templates/admin_policy_adapter.rb +28 -0
- data/lib/generators/rails_ai_admin/install/templates/example_admin.rb +10 -0
- data/lib/generators/rails_ai_admin/install/templates/initializer.rb +7 -0
- data/lib/generators/rails_ai_admin/override/override_generator.rb +46 -0
- data/lib/generators/rails_ai_admin/resource/resource_generator.rb +88 -0
- data/lib/rails_ai_admin/assistant/continuation.rb +33 -0
- data/lib/rails_ai_admin/assistant/coordinator.rb +306 -0
- data/lib/rails_ai_admin/assistant/records.rb +71 -0
- data/lib/rails_ai_admin/assistant/runtime.rb +49 -0
- data/lib/rails_ai_admin/assistant/tools.rb +477 -0
- data/lib/rails_ai_admin/assistant/transcript.rb +140 -0
- data/lib/rails_ai_admin/assistant.rb +50 -0
- data/lib/rails_ai_admin/authorization/access.rb +296 -0
- data/lib/rails_ai_admin/authorization/adapter.rb +92 -0
- data/lib/rails_ai_admin/authorization/pundit_adapter.rb +195 -0
- data/lib/rails_ai_admin/collections/claim.rb +91 -0
- data/lib/rails_ai_admin/collections/contract.rb +113 -0
- data/lib/rails_ai_admin/collections/dispatcher.rb +126 -0
- data/lib/rails_ai_admin/collections/queries.rb +79 -0
- data/lib/rails_ai_admin/collections/result.rb +15 -0
- data/lib/rails_ai_admin/collections/runner.rb +208 -0
- data/lib/rails_ai_admin/configuration.rb +178 -0
- data/lib/rails_ai_admin/context.rb +29 -0
- data/lib/rails_ai_admin/doctor.rb +301 -0
- data/lib/rails_ai_admin/engine.rb +24 -0
- data/lib/rails_ai_admin/error.rb +16 -0
- data/lib/rails_ai_admin/field.rb +625 -0
- data/lib/rails_ai_admin/fields/association.rb +188 -0
- data/lib/rails_ai_admin/fields/computation.rb +43 -0
- data/lib/rails_ai_admin/fields/dependency_graph.rb +45 -0
- data/lib/rails_ai_admin/fields/file_value.rb +23 -0
- data/lib/rails_ai_admin/fields/hook_signatures.rb +18 -0
- data/lib/rails_ai_admin/fields/projection.rb +78 -0
- data/lib/rails_ai_admin/fields/registry.rb +48 -0
- data/lib/rails_ai_admin/fields/scalar_schema.rb +49 -0
- data/lib/rails_ai_admin/fields/type.rb +50 -0
- data/lib/rails_ai_admin/forms/plan.rb +90 -0
- data/lib/rails_ai_admin/input_schema.rb +76 -0
- data/lib/rails_ai_admin/integrations/propose_operation.rb +150 -0
- data/lib/rails_ai_admin/integrations/tool_views.rb +367 -0
- data/lib/rails_ai_admin/mcp.rb +130 -0
- data/lib/rails_ai_admin/navigation/list_state.rb +194 -0
- data/lib/rails_ai_admin/navigation/menu.rb +31 -0
- data/lib/rails_ai_admin/navigation/settings.rb +28 -0
- data/lib/rails_ai_admin/operation.rb +120 -0
- data/lib/rails_ai_admin/operations/default_handlers.rb +17 -0
- data/lib/rails_ai_admin/operations/execution_context.rb +38 -0
- data/lib/rails_ai_admin/operations/form_intent.rb +69 -0
- data/lib/rails_ai_admin/operations/registry.rb +80 -0
- data/lib/rails_ai_admin/operations/result.rb +21 -0
- data/lib/rails_ai_admin/operations/runner.rb +633 -0
- data/lib/rails_ai_admin/operations/runner_fingerprint.rb +12 -0
- data/lib/rails_ai_admin/outbox/delivery_context.rb +51 -0
- data/lib/rails_ai_admin/outbox/dispatcher.rb +189 -0
- data/lib/rails_ai_admin/outbox/registry.rb +36 -0
- data/lib/rails_ai_admin/outbox/request_digest.rb +42 -0
- data/lib/rails_ai_admin/outbox/writer.rb +43 -0
- data/lib/rails_ai_admin/presentation/association_panel.rb +90 -0
- data/lib/rails_ai_admin/presentation/audit_log.rb +63 -0
- data/lib/rails_ai_admin/presentation/csv_export.rb +85 -0
- data/lib/rails_ai_admin/presentation/csv_import.rb +142 -0
- data/lib/rails_ai_admin/presentation/dashboard.rb +122 -0
- data/lib/rails_ai_admin/presentation/history.rb +46 -0
- data/lib/rails_ai_admin/presentation/index_settings.rb +77 -0
- data/lib/rails_ai_admin/presentation/outcomes.rb +44 -0
- data/lib/rails_ai_admin/presentation/status.rb +40 -0
- data/lib/rails_ai_admin/prune.rb +61 -0
- data/lib/rails_ai_admin/queries/list.rb +162 -0
- data/lib/rails_ai_admin/queries/named_scope.rb +65 -0
- data/lib/rails_ai_admin/record_version.rb +40 -0
- data/lib/rails_ai_admin/registry.rb +45 -0
- data/lib/rails_ai_admin/request_log_filter.rb +24 -0
- data/lib/rails_ai_admin/resource.rb +249 -0
- data/lib/rails_ai_admin/resource_identity.rb +15 -0
- data/lib/rails_ai_admin/schema_suggestions.rb +105 -0
- data/lib/rails_ai_admin/selections/builder.rb +162 -0
- data/lib/rails_ai_admin/templates/catalog.rb +84 -0
- data/lib/rails_ai_admin/templates/inspector.rb +94 -0
- data/lib/rails_ai_admin/templates/metadata.rb +48 -0
- data/lib/rails_ai_admin/templates/safe_paths.rb +30 -0
- data/lib/rails_ai_admin/test_kit.rb +103 -0
- data/lib/rails_ai_admin/version.rb +5 -0
- data/lib/rails_ai_admin/work.rb +48 -0
- data/lib/rails_ai_admin.rb +112 -0
- data/lib/tasks/rails_ai_admin.rake +58 -0
- metadata +328 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Opt-in admin Chat storage, installed only by `rails_ai_admin:assistant`.
|
|
4
|
+
# Chats, messages and turns are Engine-owned. Tool calls and usages live in
|
|
5
|
+
# RubyLLM's own shared tables, isolated per chat by their polymorphic message
|
|
6
|
+
# and chat columns; they are created here only when the host has none yet.
|
|
7
|
+
class CreateRailsAiAdminAssistant < ActiveRecord::Migration[8.0]
|
|
8
|
+
def up
|
|
9
|
+
key = reference_type
|
|
10
|
+
create_ruby_llm_tables(key)
|
|
11
|
+
|
|
12
|
+
create_table :rails_ai_admin_assistant_chats, id: key do |t|
|
|
13
|
+
t.string :ref, null: false, limit: 36
|
|
14
|
+
t.string :actor_ref, :tenant_ref, :security_version, null: false, limit: 150
|
|
15
|
+
t.string :title, limit: 120
|
|
16
|
+
t.string :status, null: false, default: "active"
|
|
17
|
+
t.references :ruby_llm_model, type: key, foreign_key: { to_table: :ruby_llm_models }, index: { name: "raa_assistant_chats_model" }
|
|
18
|
+
t.boolean :cancelled, null: false, default: false
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
add_index :rails_ai_admin_assistant_chats, :ref, unique: true, name: "raa_assistant_chats_ref"
|
|
22
|
+
add_index :rails_ai_admin_assistant_chats, [ :tenant_ref, :actor_ref, :updated_at ], name: "raa_assistant_chats_owner"
|
|
23
|
+
add_check_constraint :rails_ai_admin_assistant_chats, "status IN ('active', 'archived')", name: "raa_assistant_chats_status"
|
|
24
|
+
|
|
25
|
+
# Columns follow RubyLLM 2.1.1's message template; the 2.2 upgrade migration adds the rest.
|
|
26
|
+
create_table :rails_ai_admin_assistant_messages, id: key do |t|
|
|
27
|
+
t.references :chat, null: false, type: key, foreign_key: { to_table: :rails_ai_admin_assistant_chats }, index: { name: "raa_assistant_messages_chat" }
|
|
28
|
+
t.string :role, null: false
|
|
29
|
+
t.text :content
|
|
30
|
+
t.boolean :cache_until_here, null: false, default: false
|
|
31
|
+
t.string :cache_ttl
|
|
32
|
+
t.text :thinking_text, :thinking_signature
|
|
33
|
+
t.json :citations, :server_tool_calls, :raw_content, :raw_reasoning
|
|
34
|
+
t.string :finish_reason
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# One persistent inbox per chat: human messages and Surface continuations
|
|
39
|
+
# are admitted once by key and run strictly in sequence under a lease.
|
|
40
|
+
create_table :rails_ai_admin_assistant_turns, id: :string, limit: 36 do |t|
|
|
41
|
+
t.references :chat, null: false, type: key, foreign_key: { to_table: :rails_ai_admin_assistant_chats }, index: false
|
|
42
|
+
t.integer :sequence, null: false
|
|
43
|
+
t.string :kind, null: false
|
|
44
|
+
t.string :admission_key, null: false, limit: 100
|
|
45
|
+
t.text :content, null: false
|
|
46
|
+
t.string :status, null: false, default: "queued"
|
|
47
|
+
t.references :user_message, type: key, foreign_key: { to_table: :rails_ai_admin_assistant_messages }, index: { name: "raa_assistant_turns_message" }
|
|
48
|
+
t.string :lease_owner, limit: 36
|
|
49
|
+
t.datetime :lease_expires_at, :next_attempt_at, :completed_at
|
|
50
|
+
t.integer :generation, :attempts, null: false, default: 0
|
|
51
|
+
t.string :error_code, limit: 64
|
|
52
|
+
t.timestamps
|
|
53
|
+
end
|
|
54
|
+
add_index :rails_ai_admin_assistant_turns, [ :chat_id, :sequence ], unique: true, name: "raa_assistant_turns_sequence"
|
|
55
|
+
add_index :rails_ai_admin_assistant_turns, [ :chat_id, :admission_key ], unique: true, name: "raa_assistant_turns_admission"
|
|
56
|
+
add_index :rails_ai_admin_assistant_turns, [ :status, :lease_expires_at ], name: "raa_assistant_turns_due"
|
|
57
|
+
add_check_constraint :rails_ai_admin_assistant_turns, "kind IN ('message', 'continuation')", name: "raa_assistant_turns_kind"
|
|
58
|
+
add_check_constraint :rails_ai_admin_assistant_turns, "status IN ('queued', 'running', 'completed', 'failed', 'cancelled')", name: "raa_assistant_turns_status"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# RubyLLM's shared tables may belong to the host product; they are kept.
|
|
62
|
+
def down
|
|
63
|
+
drop_table :rails_ai_admin_assistant_turns
|
|
64
|
+
drop_table :rails_ai_admin_assistant_messages
|
|
65
|
+
drop_table :rails_ai_admin_assistant_chats
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Polymorphic tool-call/usage columns must hold admin message and chat keys,
|
|
71
|
+
# so admin keys follow an existing RubyLLM installation.
|
|
72
|
+
def reference_type
|
|
73
|
+
return :bigint unless table_exists?(:ruby_llm_tool_calls)
|
|
74
|
+
column = connection.columns(:ruby_llm_tool_calls).find { |candidate| candidate.name == "message_id" }
|
|
75
|
+
column&.type == :uuid ? :uuid : :bigint
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Mirrors the RubyLLM 2.1.1 install template for the tables admin Chat uses;
|
|
79
|
+
# UpgradeRailsAiAdminAssistantToRubyLlm22 brings them to 2.2.
|
|
80
|
+
def create_ruby_llm_tables(key)
|
|
81
|
+
json = connection.adapter_name.match?(/postg/i) ? :jsonb : :json
|
|
82
|
+
unless table_exists?(:ruby_llm_models)
|
|
83
|
+
create_table :ruby_llm_models, id: key do |t|
|
|
84
|
+
t.string :model_id, :name, :provider, null: false
|
|
85
|
+
t.string :family
|
|
86
|
+
t.datetime :model_created_at
|
|
87
|
+
t.integer :context_window, :max_output_tokens
|
|
88
|
+
t.date :knowledge_cutoff
|
|
89
|
+
t.datetime :unlisted_at
|
|
90
|
+
t.column :modalities, json, default: {}
|
|
91
|
+
t.column :capabilities, json, default: []
|
|
92
|
+
t.column :pricing, json, default: {}
|
|
93
|
+
t.column :metadata, json, default: {}
|
|
94
|
+
t.timestamps
|
|
95
|
+
t.index [ :provider, :model_id ], unique: true
|
|
96
|
+
t.index :provider
|
|
97
|
+
t.index :family
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
unless table_exists?(:ruby_llm_tool_calls)
|
|
101
|
+
create_table :ruby_llm_tool_calls, id: key do |t|
|
|
102
|
+
t.references :message, polymorphic: true, null: false, type: key, index: false
|
|
103
|
+
t.references :result, polymorphic: true, type: key, index: false
|
|
104
|
+
t.string :tool_call_id, :name, null: false
|
|
105
|
+
t.text :thought_signature
|
|
106
|
+
t.string :approval
|
|
107
|
+
t.boolean :remote, default: false, null: false
|
|
108
|
+
t.column :arguments, json, default: {}
|
|
109
|
+
t.column :mcp_state, json
|
|
110
|
+
t.column :mcp_result, json
|
|
111
|
+
t.timestamps
|
|
112
|
+
t.index [ :message_type, :message_id ]
|
|
113
|
+
t.index [ :result_type, :result_id ]
|
|
114
|
+
t.index :tool_call_id, unique: true
|
|
115
|
+
t.index :name
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
return if table_exists?(:ruby_llm_usages)
|
|
119
|
+
create_table :ruby_llm_usages, id: key do |t|
|
|
120
|
+
t.references :chat, polymorphic: true, type: key, index: false
|
|
121
|
+
t.references :message, polymorphic: true, type: key, index: false
|
|
122
|
+
t.references :owner, polymorphic: true, type: key, index: false
|
|
123
|
+
t.string :operation, :provider, :model, :status, null: false
|
|
124
|
+
t.integer :input_tokens, :output_tokens, :cache_read_tokens, :cache_write_tokens, :thinking_tokens
|
|
125
|
+
t.column :server_tool_use, json
|
|
126
|
+
t.decimal :input_cost, :output_cost, :cache_read_cost, :cache_write_cost, :thinking_cost, :total_cost, precision: 16, scale: 10
|
|
127
|
+
t.timestamps
|
|
128
|
+
t.index [ :chat_type, :chat_id ]
|
|
129
|
+
t.index [ :message_type, :message_id ]
|
|
130
|
+
t.index [ :owner_type, :owner_id ]
|
|
131
|
+
t.index :status
|
|
132
|
+
end
|
|
133
|
+
entry = ::RubyLLM::Accounting::Usage::Entry
|
|
134
|
+
quote = ->(values) { values.map { |value| connection.quote(value.to_s) }.join(", ") }
|
|
135
|
+
add_check_constraint :ruby_llm_usages, "operation IN (#{quote.call(entry::OPERATIONS)})"
|
|
136
|
+
add_check_constraint :ruby_llm_usages, "status IN (#{quote.call(entry::STATUSES)})"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adds the storage RubyLLM 2.2 reads and writes, following its
|
|
4
|
+
# upgrade_ruby_llm_to_2_2 template. Every step is skipped when already present,
|
|
5
|
+
# so it is safe beside a host that ran RubyLLM's own install or upgrade for the
|
|
6
|
+
# shared tables (tool calls, usages, queued messages, chat files).
|
|
7
|
+
class UpgradeRailsAiAdminAssistantToRubyLlm22 < ActiveRecord::Migration[8.0]
|
|
8
|
+
CHATS = :rails_ai_admin_assistant_chats
|
|
9
|
+
MESSAGES = :rails_ai_admin_assistant_messages
|
|
10
|
+
|
|
11
|
+
def up
|
|
12
|
+
add_column CHATS, :claim_token, :string unless column_exists?(CHATS, :claim_token)
|
|
13
|
+
add_column CHATS, :claimed_until, :datetime unless column_exists?(CHATS, :claimed_until)
|
|
14
|
+
unless index_exists?(CHATS, :claimed_until, name: "raa_assistant_chats_claim")
|
|
15
|
+
add_index CHATS, :claimed_until, where: "claim_token IS NOT NULL", name: "raa_assistant_chats_claim"
|
|
16
|
+
end
|
|
17
|
+
add_column MESSAGES, :phase, :string unless column_exists?(MESSAGES, :phase)
|
|
18
|
+
add_column MESSAGES, :continuation, :text unless column_exists?(MESSAGES, :continuation)
|
|
19
|
+
|
|
20
|
+
upgrade_tool_calls
|
|
21
|
+
upgrade_usages
|
|
22
|
+
key = reference_type
|
|
23
|
+
create_queued_messages(key)
|
|
24
|
+
create_chat_files(key)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# RubyLLM's shared tables may belong to the host product; they are kept.
|
|
28
|
+
def down
|
|
29
|
+
remove_index CHATS, name: "raa_assistant_chats_claim", if_exists: true
|
|
30
|
+
remove_column CHATS, :claimed_until, if_exists: true
|
|
31
|
+
remove_column CHATS, :claim_token, if_exists: true
|
|
32
|
+
remove_column MESSAGES, :continuation, if_exists: true
|
|
33
|
+
remove_column MESSAGES, :phase, if_exists: true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def upgrade_tool_calls
|
|
39
|
+
unless column_exists?(:ruby_llm_tool_calls, :provider_call_id)
|
|
40
|
+
if column_exists?(:ruby_llm_tool_calls, :provider_id)
|
|
41
|
+
rename_column :ruby_llm_tool_calls, :provider_id, :provider_call_id
|
|
42
|
+
else
|
|
43
|
+
add_column :ruby_llm_tool_calls, :provider_call_id, :string
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
add_column :ruby_llm_tool_calls, :namespace, :string unless column_exists?(:ruby_llm_tool_calls, :namespace)
|
|
47
|
+
add_column :ruby_llm_tool_calls, :claimed_until, :datetime unless column_exists?(:ruby_llm_tool_calls, :claimed_until)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# An operation check written by 2.1 lacks code_execution. Admin Chat never
|
|
51
|
+
# runs code, so it is left alone; a host that does runs RubyLLM's upgrade.
|
|
52
|
+
def upgrade_usages
|
|
53
|
+
%i[workflow_id workflow_name workflow_step_id workflow_step_name].each do |column|
|
|
54
|
+
add_column :ruby_llm_usages, column, :string unless column_exists?(:ruby_llm_usages, column)
|
|
55
|
+
end
|
|
56
|
+
add_index :ruby_llm_usages, :workflow_id unless index_exists?(:ruby_llm_usages, :workflow_id)
|
|
57
|
+
add_column :ruby_llm_usages, :characters, :integer unless column_exists?(:ruby_llm_usages, :characters)
|
|
58
|
+
add_column :ruby_llm_usages, :audio_seconds, :decimal, precision: 16, scale: 6 unless column_exists?(:ruby_llm_usages, :audio_seconds)
|
|
59
|
+
add_column :ruby_llm_usages, :search_units, :integer unless column_exists?(:ruby_llm_usages, :search_units)
|
|
60
|
+
%i[characters_cost audio_seconds_cost search_units_cost].each do |column|
|
|
61
|
+
add_column :ruby_llm_usages, column, :decimal, precision: 16, scale: 10 unless column_exists?(:ruby_llm_usages, column)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def create_queued_messages(key)
|
|
66
|
+
return if table_exists?(:ruby_llm_queued_messages)
|
|
67
|
+
create_table :ruby_llm_queued_messages, id: key do |t|
|
|
68
|
+
t.references :chat, polymorphic: true, null: false, type: key, index: false
|
|
69
|
+
t.references :sender, polymorphic: true, type: key, index: false
|
|
70
|
+
t.text :content
|
|
71
|
+
t.boolean :steer, default: true, null: false
|
|
72
|
+
t.timestamps
|
|
73
|
+
t.index [ :chat_type, :chat_id ]
|
|
74
|
+
t.index [ :sender_type, :sender_id ]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def create_chat_files(key)
|
|
79
|
+
unless table_exists?(:ruby_llm_chat_files)
|
|
80
|
+
create_table :ruby_llm_chat_files, id: key do |t|
|
|
81
|
+
t.references :chat, polymorphic: true, null: false, type: key, index: false
|
|
82
|
+
t.references :owner, polymorphic: true, type: key, index: false
|
|
83
|
+
t.string :path, null: false
|
|
84
|
+
t.string :content_type
|
|
85
|
+
t.bigint :byte_size
|
|
86
|
+
t.string :digest
|
|
87
|
+
t.text :text
|
|
88
|
+
t.string :read_digest
|
|
89
|
+
t.timestamps
|
|
90
|
+
t.index [ :chat_type, :chat_id, :path ]
|
|
91
|
+
t.index [ :owner_type, :owner_id ]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
add_column :ruby_llm_chat_files, :read_digest, :string unless column_exists?(:ruby_llm_chat_files, :read_digest)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# New polymorphic columns hold the same keys as the existing tool calls.
|
|
98
|
+
def reference_type
|
|
99
|
+
column = connection.columns(:ruby_llm_tool_calls).find { |candidate| candidate.name == "message_id" }
|
|
100
|
+
column&.type == :uuid ? :uuid : :bigint
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Streaming replies keep the text of the request in progress on its turn, and
|
|
4
|
+
# MCP clients authenticate with personal access tokens. Steps already present
|
|
5
|
+
# are skipped.
|
|
6
|
+
class AddStreamingAndAccessTokensToRailsAiAdminAssistant < ActiveRecord::Migration[8.0]
|
|
7
|
+
def up
|
|
8
|
+
add_column :rails_ai_admin_assistant_turns, :draft, :text unless column_exists?(:rails_ai_admin_assistant_turns, :draft)
|
|
9
|
+
return if table_exists?(:rails_ai_admin_access_tokens)
|
|
10
|
+
create_table :rails_ai_admin_access_tokens, id: :string, limit: 36 do |t|
|
|
11
|
+
t.string :actor_ref, :tenant_ref, null: false, limit: 150
|
|
12
|
+
t.string :security_version, null: false, limit: 150
|
|
13
|
+
t.string :name, null: false, limit: 80
|
|
14
|
+
# SHA-256 of the token; the token itself is shown once and never stored.
|
|
15
|
+
t.string :token_digest, null: false, limit: 64
|
|
16
|
+
t.datetime :expires_at, null: false
|
|
17
|
+
t.datetime :last_used_at
|
|
18
|
+
t.datetime :revoked_at
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
add_index :rails_ai_admin_access_tokens, :token_digest, unique: true, name: "raa_access_tokens_digest"
|
|
22
|
+
add_index :rails_ai_admin_access_tokens, [ :actor_ref, :tenant_ref ], name: "raa_access_tokens_owner"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def down
|
|
26
|
+
drop_table :rails_ai_admin_access_tokens, if_exists: true
|
|
27
|
+
remove_column :rails_ai_admin_assistant_turns, :draft, if_exists: true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsAiAdminExecutionTables < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rails_ai_admin_operation_runs, id: :string, limit: 36 do |t|
|
|
6
|
+
t.string :actor_ref, :tenant_ref, :security_version, null: false, limit: 150
|
|
7
|
+
t.string :resource_key, :operation, :operation_version, null: false
|
|
8
|
+
t.string :target_id, :record_id, limit: 150
|
|
9
|
+
t.bigint :expected_version
|
|
10
|
+
t.string :input_digest, :approved_digest, limit: 64
|
|
11
|
+
t.string :status, null: false, default: "proposed"
|
|
12
|
+
t.string :origin, null: false, default: "ui"
|
|
13
|
+
t.string :approved_by_ref, limit: 150
|
|
14
|
+
t.datetime :confirmed_at, :expires_at, :completed_at
|
|
15
|
+
t.bigint :audit_event_id
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
add_index :rails_ai_admin_operation_runs, [ :tenant_ref, :actor_ref, :created_at ], name: "raa_runs_owner"
|
|
19
|
+
add_index :rails_ai_admin_operation_runs, [ :status, :expires_at ], name: "raa_runs_expiry"
|
|
20
|
+
add_check_constraint :rails_ai_admin_operation_runs, "status IN ('proposed', 'running', 'succeeded')", name: "raa_runs_status"
|
|
21
|
+
|
|
22
|
+
create_table :rails_ai_admin_audit_events do |t|
|
|
23
|
+
t.string :actor_ref, :tenant_ref, null: false, limit: 150
|
|
24
|
+
t.string :resource_key, :operation, :status, :code, null: false
|
|
25
|
+
t.string :record_id, limit: 150
|
|
26
|
+
t.string :correlation_ref, null: false, limit: 36
|
|
27
|
+
t.string :origin, null: false
|
|
28
|
+
t.json :changed_fields, null: false, default: []
|
|
29
|
+
t.datetime :created_at, null: false
|
|
30
|
+
end
|
|
31
|
+
add_index :rails_ai_admin_audit_events, [ :tenant_ref, :created_at ], name: "raa_audit_tenant"
|
|
32
|
+
add_index :rails_ai_admin_audit_events, :correlation_ref, name: "raa_audit_correlation"
|
|
33
|
+
add_foreign_key :rails_ai_admin_operation_runs, :rails_ai_admin_audit_events, column: :audit_event_id
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsAiAdminWorkItems < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rails_ai_admin_work_items, id: :string, limit: 36 do |t|
|
|
6
|
+
t.string :operation_run_id, null: false, limit: 36
|
|
7
|
+
t.string :work_key, :kind, :handler_version, :payload_digest, null: false
|
|
8
|
+
t.json :payload, null: false, default: {}
|
|
9
|
+
t.string :status, null: false, default: "pending"
|
|
10
|
+
t.bigint :expected_version
|
|
11
|
+
t.integer :generation, :attempts, null: false, default: 0
|
|
12
|
+
t.integer :effect_generation
|
|
13
|
+
t.string :effect_digest, limit: 64
|
|
14
|
+
t.string :lease_owner, limit: 36
|
|
15
|
+
t.datetime :lease_expires_at, :next_attempt_at, :effect_started_at, :completed_at
|
|
16
|
+
t.datetime :deadline_at, null: false
|
|
17
|
+
t.string :last_code
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
add_index :rails_ai_admin_work_items, [ :operation_run_id, :work_key ], unique: true, name: "raa_work_intent_key"
|
|
21
|
+
add_index :rails_ai_admin_work_items, [ :status, :next_attempt_at ], name: "raa_work_pending"
|
|
22
|
+
add_index :rails_ai_admin_work_items, [ :status, :lease_expires_at ], name: "raa_work_lease"
|
|
23
|
+
add_foreign_key :rails_ai_admin_work_items, :rails_ai_admin_operation_runs, column: :operation_run_id
|
|
24
|
+
add_check_constraint :rails_ai_admin_work_items, "status IN ('pending', 'running', 'succeeded', 'failed', 'cancelled', 'reconciliation_required')", name: "raa_work_status"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsAiAdminSelections < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rails_ai_admin_selections, id: :string, limit: 36 do |t|
|
|
6
|
+
t.string :actor_ref, :tenant_ref, :security_version, null: false, limit: 150
|
|
7
|
+
t.string :resource_key, null: false
|
|
8
|
+
t.string :resource_version, :manifest_digest, null: false, limit: 64
|
|
9
|
+
t.integer :item_count, null: false
|
|
10
|
+
t.datetime :expires_at, null: false
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :rails_ai_admin_selections, [ :tenant_ref, :actor_ref, :created_at ], name: "raa_selections_owner"
|
|
14
|
+
add_index :rails_ai_admin_selections, :expires_at, name: "raa_selections_expiry"
|
|
15
|
+
add_check_constraint :rails_ai_admin_selections, "item_count BETWEEN 1 AND 100", name: "raa_selections_count"
|
|
16
|
+
|
|
17
|
+
create_table :rails_ai_admin_selection_items do |t|
|
|
18
|
+
t.string :selection_id, null: false, limit: 36
|
|
19
|
+
t.string :target_id, null: false, limit: 150
|
|
20
|
+
t.bigint :expected_version, null: false
|
|
21
|
+
t.integer :position, null: false
|
|
22
|
+
end
|
|
23
|
+
add_foreign_key :rails_ai_admin_selection_items, :rails_ai_admin_selections, column: :selection_id
|
|
24
|
+
add_index :rails_ai_admin_selection_items, [ :selection_id, :target_id ], unique: true, name: "raa_selection_targets"
|
|
25
|
+
add_index :rails_ai_admin_selection_items, [ :selection_id, :position ], unique: true, name: "raa_selection_order"
|
|
26
|
+
add_check_constraint :rails_ai_admin_selection_items, "expected_version >= 0 AND position BETWEEN 0 AND 99", name: "raa_selection_item_bounds"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsAiAdminCollectionRuns < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
remove_check_constraint :rails_ai_admin_operation_runs, "status IN ('proposed', 'running', 'succeeded')", name: "raa_runs_status"
|
|
6
|
+
change_table :rails_ai_admin_operation_runs do |t|
|
|
7
|
+
t.string :selection_id, :parent_run_id, limit: 36
|
|
8
|
+
t.string :selection_digest, :manifest_resource_version, limit: 64
|
|
9
|
+
t.json :input_payload, :manifest_payload
|
|
10
|
+
t.datetime :execution_deadline, :cancellation_requested_at
|
|
11
|
+
end
|
|
12
|
+
add_foreign_key :rails_ai_admin_operation_runs, :rails_ai_admin_selections, column: :selection_id
|
|
13
|
+
add_foreign_key :rails_ai_admin_operation_runs, :rails_ai_admin_operation_runs, column: :parent_run_id
|
|
14
|
+
add_index :rails_ai_admin_operation_runs, [ :parent_run_id, :target_id ], unique: true, name: "raa_child_target"
|
|
15
|
+
add_check_constraint :rails_ai_admin_operation_runs,
|
|
16
|
+
"status IN ('proposed', 'queued', 'running', 'succeeded', 'failed', 'partially_succeeded', 'cancelled', 'reconciliation_required')", name: "raa_runs_status"
|
|
17
|
+
|
|
18
|
+
create_table :rails_ai_admin_batch_items, id: :string, limit: 36 do |t|
|
|
19
|
+
t.string :parent_run_id, :operation_run_id, null: false, limit: 36
|
|
20
|
+
t.bigint :selection_item_id, null: false
|
|
21
|
+
t.string :target_id, null: false, limit: 150
|
|
22
|
+
t.bigint :expected_version, null: false
|
|
23
|
+
t.integer :position, null: false
|
|
24
|
+
t.string :status, null: false, default: "queued"
|
|
25
|
+
t.string :lease_owner, limit: 36
|
|
26
|
+
t.integer :generation, :attempts, null: false, default: 0
|
|
27
|
+
t.string :last_code, limit: 64
|
|
28
|
+
t.datetime :lease_expires_at, :next_attempt_at, :completed_at
|
|
29
|
+
t.timestamps
|
|
30
|
+
end
|
|
31
|
+
add_foreign_key :rails_ai_admin_batch_items, :rails_ai_admin_operation_runs, column: :parent_run_id
|
|
32
|
+
add_foreign_key :rails_ai_admin_batch_items, :rails_ai_admin_operation_runs, column: :operation_run_id
|
|
33
|
+
add_foreign_key :rails_ai_admin_batch_items, :rails_ai_admin_selection_items, column: :selection_item_id
|
|
34
|
+
add_index :rails_ai_admin_batch_items, :operation_run_id, unique: true, name: "raa_batch_child"
|
|
35
|
+
add_index :rails_ai_admin_batch_items, [ :parent_run_id, :target_id ], unique: true, name: "raa_batch_target"
|
|
36
|
+
add_index :rails_ai_admin_batch_items, [ :parent_run_id, :position ], unique: true, name: "raa_batch_order"
|
|
37
|
+
add_index :rails_ai_admin_batch_items, [ :status, :lease_expires_at, :next_attempt_at ], name: "raa_batch_due"
|
|
38
|
+
add_check_constraint :rails_ai_admin_batch_items, "status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')", name: "raa_batch_status"
|
|
39
|
+
add_check_constraint :rails_ai_admin_batch_items, "position BETWEEN 0 AND 99 AND expected_version >= 0 AND generation >= 0 AND attempts >= 0", name: "raa_batch_bounds"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Serves a record's history page: one tenant, resource and record, newest first.
|
|
4
|
+
class AddRecordIndexToRailsAiAdminAuditEvents < ActiveRecord::Migration[8.0]
|
|
5
|
+
def change
|
|
6
|
+
add_index :rails_ai_admin_audit_events, [ :tenant_ref, :resource_key, :record_id, :id ], name: "raa_audit_record"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require_relative "../engine_migrations"
|
|
5
|
+
|
|
6
|
+
module RailsAiAdmin
|
|
7
|
+
module Generators
|
|
8
|
+
# Adds opt-in admin Chat storage and a closed provider adapter. Nothing is
|
|
9
|
+
# enabled until the host sets `config.assistant = true` and returns a
|
|
10
|
+
# RubyLLM::Context from the adapter.
|
|
11
|
+
class AssistantGenerator < Rails::Generators::Base
|
|
12
|
+
include EngineMigrations
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
def install
|
|
16
|
+
copy_engine_migrations("db/assistant_migrate")
|
|
17
|
+
target = "app/rails_ai_admin/admin_assistant.rb"
|
|
18
|
+
template "admin_assistant.rb", target unless File.exist?(File.join(destination_root, target))
|
|
19
|
+
say "The assistant stays unavailable: AdminAssistant#llm_context returns nil, and no resource grants assistant?."
|
|
20
|
+
say "Add ruby_llm (#{Configuration::OPTIONAL_GEMS.fetch('ruby_llm').fetch(:requirement).join(', ')}) to the Gemfile, run db:migrate, then set config.assistant = true, config.assistant_adapter = \"AdminAssistant\" and config.assistant_model."
|
|
21
|
+
say "Schedule rails_ai_admin:assistant:scan to recover lost jobs. Guide: #{GUIDES}/_advanced/admin-assistant.md"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Builds the admin assistant's provider configuration for one admin Context.
|
|
2
|
+
# Returning nil keeps the assistant unavailable for that actor; this is the
|
|
3
|
+
# default. Read credentials from your secret store and never change global
|
|
4
|
+
# RubyLLM settings here: the admin's provider settings stay separate from your
|
|
5
|
+
# product's.
|
|
6
|
+
class AdminAssistant
|
|
7
|
+
def llm_context(context:)
|
|
8
|
+
nil
|
|
9
|
+
# key = Rails.application.credentials.dig(:admin_assistant, :openai_api_key)
|
|
10
|
+
# return unless key
|
|
11
|
+
#
|
|
12
|
+
# RubyLLM.context do |config|
|
|
13
|
+
# config.openai_api_key = key
|
|
14
|
+
# config.request_timeout = 60
|
|
15
|
+
# config.max_retries = 1
|
|
16
|
+
# end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/migration"
|
|
4
|
+
|
|
5
|
+
module RailsAiAdmin
|
|
6
|
+
module Generators
|
|
7
|
+
# Copies each engine migration the host does not have yet. A migration the
|
|
8
|
+
# host already has is kept even with --force: it may have run, and
|
|
9
|
+
# replacing it under a new timestamp would run it twice. Schema changes
|
|
10
|
+
# ship as new migrations.
|
|
11
|
+
# The gem ships no docs/ directory, so generator output links the guides
|
|
12
|
+
# in the source repository (RAA-026).
|
|
13
|
+
GUIDES = "https://github.com/yorzi/rails_ai_admin/blob/main/docs/site"
|
|
14
|
+
|
|
15
|
+
module EngineMigrations # :nodoc:
|
|
16
|
+
def self.included(base)
|
|
17
|
+
base.include Rails::Generators::Migration
|
|
18
|
+
base.extend ClassMethods
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module ClassMethods # :nodoc:
|
|
22
|
+
def next_migration_number(dirname)
|
|
23
|
+
[ Time.now.utc.strftime("%Y%m%d%H%M%S"), format("%014d", current_migration_number(dirname) + 1) ].max
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def copy_engine_migrations(directory)
|
|
30
|
+
Engine.root.join(directory).glob("*.rb").sort.each do |source|
|
|
31
|
+
stem = source.basename(".rb").to_s.sub(/\A\d+_/, "")
|
|
32
|
+
existing = self.class.migration_exists?(File.join(destination_root, "db/migrate"), stem)
|
|
33
|
+
if existing
|
|
34
|
+
say_status :exist, Pathname.new(existing).relative_path_from(Pathname.new(destination_root)).to_s, :blue
|
|
35
|
+
else
|
|
36
|
+
migration_template source.to_s, "db/migrate/#{stem}.rb"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiAdmin
|
|
4
|
+
module Generators
|
|
5
|
+
# Uses Rails' loaded routes, including drawn files and namespace prefixes.
|
|
6
|
+
module EngineMounts # :nodoc:
|
|
7
|
+
def self.names(routes)
|
|
8
|
+
routes.routes.filter_map do |route|
|
|
9
|
+
endpoint = route.app
|
|
10
|
+
while endpoint.is_a?(ActionDispatch::Routing::Mapper::Constraints)
|
|
11
|
+
endpoint = endpoint.app
|
|
12
|
+
end
|
|
13
|
+
next unless endpoint.equal?(RailsAiAdmin::Engine) || endpoint.is_a?(RailsAiAdmin::Engine)
|
|
14
|
+
route.parts.empty? && route.verb.empty? ? route.name.to_s : :unsupported
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require_relative "engine_mounts"
|
|
5
|
+
require_relative "../engine_migrations"
|
|
6
|
+
|
|
7
|
+
module RailsAiAdmin
|
|
8
|
+
module Generators
|
|
9
|
+
# Installs conventional tables and a deliberately closed host context.
|
|
10
|
+
class InstallGenerator < Rails::Generators::Base
|
|
11
|
+
include EngineMigrations
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
def initialize(args = [], options = {}, config = {})
|
|
15
|
+
@host_application = config.fetch(:application) { Rails.application }
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def install
|
|
20
|
+
unless @host_application && @host_application.root.realpath == Pathname.new(destination_root).realpath
|
|
21
|
+
raise Rails::Generators::Error, "Run install inside the target Rails application so its actual routes can be checked. No files were changed."
|
|
22
|
+
end
|
|
23
|
+
mounts = EngineMounts.names(@host_application.routes)
|
|
24
|
+
if mounts.empty? && @host_application.routes.named_routes.names.map(&:to_s).include?("rails_ai_admin")
|
|
25
|
+
raise Rails::Generators::Error, "The rails_ai_admin route name belongs to another endpoint. Resolve the conflict before installation. No files were changed."
|
|
26
|
+
end
|
|
27
|
+
if mounts.any? && mounts != [ "rails_ai_admin" ]
|
|
28
|
+
raise Rails::Generators::Error, "Use one RailsAiAdmin::Engine mount whose final helper is rails_ai_admin. Custom paths are supported; other aliases, name prefixes, dynamic parameters and multiple mounts require review. No files were changed."
|
|
29
|
+
end
|
|
30
|
+
%w[AdminContext AdminPolicyAdapter ExampleAdmin].each do |constant|
|
|
31
|
+
target = File.join(destination_root, "app/rails_ai_admin/#{constant.underscore}.rb")
|
|
32
|
+
if Object.const_defined?(constant) && !File.exist?(target)
|
|
33
|
+
raise Rails::Generators::Error, "Constant #{constant} already exists outside the generated path"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
copy_engine_migrations("db/migrate")
|
|
37
|
+
template "initializer.rb", "config/initializers/rails_ai_admin.rb"
|
|
38
|
+
template "admin_context.rb", "app/rails_ai_admin/admin_context.rb"
|
|
39
|
+
{
|
|
40
|
+
"admin_policy_adapter.rb" => "app/rails_ai_admin/admin_policy_adapter.rb",
|
|
41
|
+
"example_admin.rb" => "app/rails_ai_admin/example_admin.rb",
|
|
42
|
+
"access_test.rb" => "test/rails_ai_admin/access_test.rb"
|
|
43
|
+
}.each do |source, target|
|
|
44
|
+
template source, target unless File.exist?(File.join(destination_root, target))
|
|
45
|
+
end
|
|
46
|
+
route 'mount RailsAiAdmin::Engine => "/admin", as: :rails_ai_admin' if mounts.empty?
|
|
47
|
+
say "Access remains closed. Implement AdminContext and an explicit policy adapter; then run db:migrate and rails_ai_admin:doctor."
|
|
48
|
+
say "ExampleAdmin is unregistered. Review it or generate your own resource. Run bin/rails test test/rails_ai_admin/access_test.rb."
|
|
49
|
+
say "Guide: #{GUIDES}/_getting_started/getting-started.md"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class RailsAiAdminAccessTest < ActionDispatch::IntegrationTest
|
|
4
|
+
# Closed means no admin page renders: a 401, or with config.authentication_path
|
|
5
|
+
# a redirect to sign-in.
|
|
6
|
+
test "anonymous access stays closed" do
|
|
7
|
+
path = Rails.application.routes.url_helpers.rails_ai_admin_path
|
|
8
|
+
get path
|
|
9
|
+
sign_in = RailsAiAdmin.configuration.authentication_path
|
|
10
|
+
if sign_in
|
|
11
|
+
assert_response :see_other
|
|
12
|
+
assert_equal sign_in, URI(response.location).path
|
|
13
|
+
else
|
|
14
|
+
assert_response :unauthorized
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class AdminContext
|
|
2
|
+
def resolve(controller)
|
|
3
|
+
# TODO: authenticate through your host's public session/authentication API.
|
|
4
|
+
# Return RailsAiAdmin::Context.new(actor:, tenant:, security_version:).
|
|
5
|
+
# Increment security_version when session, role or tenant authority changes.
|
|
6
|
+
nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Working example for an app made with `bin/rails generate authentication`
|
|
10
|
+
# and a boolean `admin` column on users. Replace the method above with it
|
|
11
|
+
# once you have reviewed it. Devise apps read controller.warden.user(:user).
|
|
12
|
+
#
|
|
13
|
+
# # One fixed tenant for an app without accounts. With an Account model,
|
|
14
|
+
# # pass the user's account instead and scope the policy by it.
|
|
15
|
+
# SingleTenant = Data.define(:id)
|
|
16
|
+
# TENANT = SingleTenant.new(id: "app")
|
|
17
|
+
#
|
|
18
|
+
# def resolve(controller)
|
|
19
|
+
# session = Session.find_by(id: controller.request.cookie_jar.signed[:session_id])
|
|
20
|
+
# user = session&.user
|
|
21
|
+
# return unless user&.admin?
|
|
22
|
+
# # Any change to the user, including the admin flag, invalidates open forms.
|
|
23
|
+
# RailsAiAdmin::Context.new(actor: user, tenant: TENANT, security_version: user.updated_at.to_i.to_s)
|
|
24
|
+
# end
|
|
25
|
+
end
|