ruby_llm-agents 1.3.4 → 2.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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Idempotent migration to split large payload columns from executions to execution_details.
4
+ #
5
+ # Handles all upgrade scenarios:
6
+ # - execution_details table doesn't exist: creates it and migrates data
7
+ # - execution_details table exists but old columns remain on executions: migrates data and removes columns
8
+ # - already clean: no-ops safely
9
+ class SplitExecutionDetailsFromExecutions < ActiveRecord::Migration<%= migration_version %>
10
+ # Columns that belong on execution_details, not executions
11
+ DETAIL_COLUMNS = %i[
12
+ error_message system_prompt user_prompt response messages_summary
13
+ tool_calls attempts fallback_chain parameters routed_to
14
+ classification_result cached_at cache_creation_tokens
15
+ ].freeze
16
+
17
+ # Niche columns moved to metadata JSON
18
+ NICHE_COLUMNS = %i[
19
+ span_id response_cache_key time_to_first_token_ms
20
+ retryable rate_limited fallback_reason
21
+ ].freeze
22
+
23
+ # Polymorphic tenant columns removed from executions (access via Tenant model)
24
+ TENANT_RECORD_COLUMNS = %i[tenant_record_type tenant_record_id].freeze
25
+
26
+ def up
27
+ create_details_table_if_needed
28
+ backfill_and_remove_old_columns
29
+ remove_niche_columns
30
+ remove_tenant_record_columns
31
+ ensure_required_columns
32
+ cleanup_indexes
33
+ end
34
+
35
+ def down
36
+ raise ActiveRecord::IrreversibleMigration,
37
+ "This migration cannot be reversed. Use rails db:schema:load to restore."
38
+ end
39
+
40
+ private
41
+
42
+ def create_details_table_if_needed
43
+ return if table_exists?(:ruby_llm_agents_execution_details)
44
+
45
+ create_table :ruby_llm_agents_execution_details do |t|
46
+ t.references :execution, null: false,
47
+ foreign_key: { to_table: :ruby_llm_agents_executions, on_delete: :cascade },
48
+ index: { unique: true }
49
+
50
+ t.text :error_message
51
+ t.text :system_prompt
52
+ t.text :user_prompt
53
+ t.json :response, default: {}
54
+ t.json :messages_summary, default: {}, null: false
55
+ t.json :tool_calls, default: [], null: false
56
+ t.json :attempts, default: [], null: false
57
+ t.json :fallback_chain
58
+ t.json :parameters, default: {}, null: false
59
+ t.string :routed_to
60
+ t.json :classification_result
61
+ t.datetime :cached_at
62
+ t.integer :cache_creation_tokens, default: 0
63
+
64
+ t.timestamps
65
+ end
66
+ end
67
+
68
+ def backfill_and_remove_old_columns
69
+ # Only proceed if detail columns still exist on executions
70
+ columns_present = DETAIL_COLUMNS.select { |col| column_exists?(:ruby_llm_agents_executions, col) }
71
+ return if columns_present.empty?
72
+
73
+ # Backfill data from executions to execution_details
74
+ say_with_time "Backfilling execution_details from executions" do
75
+ backfill_execution_details(columns_present)
76
+ end
77
+
78
+ # Remove old columns from executions
79
+ columns_present.each do |col|
80
+ remove_column :ruby_llm_agents_executions, col
81
+ end
82
+ end
83
+
84
+ def backfill_execution_details(columns_present)
85
+ batch_size = 1000
86
+ count = 0
87
+
88
+ # Build WHERE clause to only copy rows that have data
89
+ has_data_conditions = columns_present.map { |col| "e.#{col} IS NOT NULL" }.join(" OR ")
90
+
91
+ loop do
92
+ ids = exec_query(<<~SQL).rows.flatten
93
+ SELECT e.id FROM ruby_llm_agents_executions e
94
+ LEFT JOIN ruby_llm_agents_execution_details d ON d.execution_id = e.id
95
+ WHERE d.id IS NULL AND (#{has_data_conditions})
96
+ ORDER BY e.id
97
+ LIMIT #{batch_size}
98
+ SQL
99
+
100
+ break if ids.empty?
101
+
102
+ # Build dynamic column lists based on what actually exists
103
+ detail_cols = %w[execution_id created_at updated_at] + columns_present.map(&:to_s)
104
+ select_exprs = %w[id created_at updated_at] + columns_present.map { |col|
105
+ case col
106
+ when :messages_summary then "COALESCE(messages_summary, '{}')"
107
+ when :tool_calls then "COALESCE(tool_calls, '[]')"
108
+ when :attempts then "COALESCE(attempts, '[]')"
109
+ when :parameters then "COALESCE(parameters, '{}')"
110
+ else col.to_s
111
+ end
112
+ }
113
+
114
+ execute <<~SQL
115
+ INSERT INTO ruby_llm_agents_execution_details (#{detail_cols.join(', ')})
116
+ SELECT #{select_exprs.join(', ')}
117
+ FROM ruby_llm_agents_executions
118
+ WHERE id IN (#{ids.join(',')})
119
+ SQL
120
+
121
+ count += ids.size
122
+ end
123
+
124
+ count
125
+ end
126
+
127
+ def remove_niche_columns
128
+ NICHE_COLUMNS.each do |col|
129
+ if column_exists?(:ruby_llm_agents_executions, col)
130
+ remove_column :ruby_llm_agents_executions, col
131
+ end
132
+ end
133
+ end
134
+
135
+ def remove_tenant_record_columns
136
+ remove_index :ruby_llm_agents_executions, column: TENANT_RECORD_COLUMNS,
137
+ name: "index_executions_on_tenant_record", if_exists: true
138
+
139
+ TENANT_RECORD_COLUMNS.each do |col|
140
+ if column_exists?(:ruby_llm_agents_executions, col)
141
+ remove_column :ruby_llm_agents_executions, col
142
+ end
143
+ end
144
+ end
145
+
146
+ def ensure_required_columns
147
+ unless column_exists?(:ruby_llm_agents_executions, :execution_type)
148
+ add_column :ruby_llm_agents_executions, :execution_type, :string, null: false, default: "chat"
149
+ end
150
+ unless column_exists?(:ruby_llm_agents_executions, :chosen_model_id)
151
+ add_column :ruby_llm_agents_executions, :chosen_model_id, :string
152
+ end
153
+ unless column_exists?(:ruby_llm_agents_executions, :messages_count)
154
+ add_column :ruby_llm_agents_executions, :messages_count, :integer, default: 0, null: false
155
+ end
156
+ unless column_exists?(:ruby_llm_agents_executions, :attempts_count)
157
+ add_column :ruby_llm_agents_executions, :attempts_count, :integer, default: 1, null: false
158
+ end
159
+ unless column_exists?(:ruby_llm_agents_executions, :tool_calls_count)
160
+ add_column :ruby_llm_agents_executions, :tool_calls_count, :integer, default: 0, null: false
161
+ end
162
+ unless column_exists?(:ruby_llm_agents_executions, :streaming)
163
+ add_column :ruby_llm_agents_executions, :streaming, :boolean, default: false
164
+ end
165
+ unless column_exists?(:ruby_llm_agents_executions, :finish_reason)
166
+ add_column :ruby_llm_agents_executions, :finish_reason, :string
167
+ end
168
+ unless column_exists?(:ruby_llm_agents_executions, :cache_hit)
169
+ add_column :ruby_llm_agents_executions, :cache_hit, :boolean, default: false
170
+ end
171
+ unless column_exists?(:ruby_llm_agents_executions, :trace_id)
172
+ add_column :ruby_llm_agents_executions, :trace_id, :string
173
+ add_index :ruby_llm_agents_executions, :trace_id
174
+ end
175
+ unless column_exists?(:ruby_llm_agents_executions, :request_id)
176
+ add_column :ruby_llm_agents_executions, :request_id, :string
177
+ add_index :ruby_llm_agents_executions, :request_id
178
+ end
179
+ unless column_exists?(:ruby_llm_agents_executions, :parent_execution_id)
180
+ add_column :ruby_llm_agents_executions, :parent_execution_id, :bigint
181
+ add_index :ruby_llm_agents_executions, :parent_execution_id
182
+ add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
183
+ column: :parent_execution_id, on_delete: :nullify
184
+ end
185
+ unless column_exists?(:ruby_llm_agents_executions, :root_execution_id)
186
+ add_column :ruby_llm_agents_executions, :root_execution_id, :bigint
187
+ add_index :ruby_llm_agents_executions, :root_execution_id
188
+ add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
189
+ column: :root_execution_id, on_delete: :nullify
190
+ end
191
+ unless column_exists?(:ruby_llm_agents_executions, :tenant_id)
192
+ add_column :ruby_llm_agents_executions, :tenant_id, :string
193
+ end
194
+ unless column_exists?(:ruby_llm_agents_executions, :cached_tokens)
195
+ add_column :ruby_llm_agents_executions, :cached_tokens, :integer, default: 0
196
+ end
197
+ end
198
+
199
+ def cleanup_indexes
200
+ # Remove single-column indexes that are redundant with composite ones
201
+ %i[duration_ms total_cost messages_count attempts_count tool_calls_count
202
+ chosen_model_id execution_type response_cache_key agent_type tenant_id].each do |col|
203
+ remove_index :ruby_llm_agents_executions, col, if_exists: true
204
+ end
205
+
206
+ # Ensure composite tenant indexes exist
207
+ unless index_exists?(:ruby_llm_agents_executions, [:tenant_id, :created_at])
208
+ add_index :ruby_llm_agents_executions, [:tenant_id, :created_at]
209
+ end
210
+ unless index_exists?(:ruby_llm_agents_executions, [:tenant_id, :status])
211
+ add_index :ruby_llm_agents_executions, [:tenant_id, :status]
212
+ end
213
+
214
+ # Remove workflow indexes (feature removed)
215
+ remove_index :ruby_llm_agents_executions, [:workflow_id, :workflow_step], if_exists: true
216
+ remove_index :ruby_llm_agents_executions, :workflow_id, if_exists: true
217
+ remove_index :ruby_llm_agents_executions, :workflow_type, if_exists: true
218
+
219
+ # Remove workflow columns if present
220
+ %i[workflow_id workflow_type workflow_step].each do |col|
221
+ if column_exists?(:ruby_llm_agents_executions, col)
222
+ remove_column :ruby_llm_agents_executions, col
223
+ end
224
+ end
225
+
226
+ # Remove agent_version if present
227
+ if column_exists?(:ruby_llm_agents_executions, :agent_version)
228
+ remove_index :ruby_llm_agents_executions, [:agent_type, :agent_version], if_exists: true
229
+ remove_column :ruby_llm_agents_executions, :agent_version
230
+ end
231
+ end
232
+ end
@@ -2,8 +2,6 @@
2
2
 
3
3
  require "rails/generators"
4
4
  require "rails/generators/active_record"
5
- require "fileutils"
6
-
7
5
  module RubyLlmAgents
8
6
  # Upgrade generator for ruby_llm-agents
9
7
  #
@@ -11,142 +9,37 @@ module RubyLlmAgents
11
9
  # rails generate ruby_llm_agents:upgrade
12
10
  #
13
11
  # This will create any missing migrations for upgrading from older versions.
12
+ # It handles all upgrade scenarios:
13
+ #
14
+ # - v0.x/v1.x -> v2.0: Splits detail columns from executions to execution_details,
15
+ # removes deprecated columns, renames tenant_budgets to tenants
16
+ # - v2.0 -> latest: No-ops safely if already up to date
14
17
  #
15
18
  class UpgradeGenerator < ::Rails::Generators::Base
16
19
  include ::ActiveRecord::Generators::Migration
17
20
 
18
21
  source_root File.expand_path("templates", __dir__)
19
22
 
20
- def create_add_prompts_migration
21
- # Check if columns already exist
22
- if column_exists?(:ruby_llm_agents_executions, :system_prompt)
23
- say_status :skip, "system_prompt column already exists", :yellow
24
- return
25
- end
26
-
27
- migration_template(
28
- "add_prompts_migration.rb.tt",
29
- File.join(db_migrate_path, "add_prompts_to_ruby_llm_agents_executions.rb")
30
- )
31
- end
32
-
33
- def create_add_attempts_migration
34
- # Check if columns already exist
35
- if column_exists?(:ruby_llm_agents_executions, :attempts)
36
- say_status :skip, "attempts column already exists", :yellow
37
- return
38
- end
39
-
40
- migration_template(
41
- "add_attempts_migration.rb.tt",
42
- File.join(db_migrate_path, "add_attempts_to_ruby_llm_agents_executions.rb")
43
- )
44
- end
45
-
46
- def create_add_streaming_migration
47
- # Check if columns already exist
48
- if column_exists?(:ruby_llm_agents_executions, :streaming)
49
- say_status :skip, "streaming column already exists", :yellow
50
- return
51
- end
52
-
53
- migration_template(
54
- "add_streaming_migration.rb.tt",
55
- File.join(db_migrate_path, "add_streaming_to_ruby_llm_agents_executions.rb")
56
- )
57
- end
58
-
59
- def create_add_tracing_migration
60
- # Check if columns already exist
61
- if column_exists?(:ruby_llm_agents_executions, :trace_id)
62
- say_status :skip, "trace_id column already exists", :yellow
63
- return
64
- end
65
-
66
- migration_template(
67
- "add_tracing_migration.rb.tt",
68
- File.join(db_migrate_path, "add_tracing_to_ruby_llm_agents_executions.rb")
69
- )
70
- end
71
-
72
- def create_add_routing_migration
73
- # Check if columns already exist
74
- if column_exists?(:ruby_llm_agents_executions, :fallback_reason)
75
- say_status :skip, "fallback_reason column already exists", :yellow
76
- return
77
- end
78
-
79
- migration_template(
80
- "add_routing_migration.rb.tt",
81
- File.join(db_migrate_path, "add_routing_to_ruby_llm_agents_executions.rb")
82
- )
83
- end
84
-
85
- def create_add_finish_reason_migration
86
- # Check if columns already exist
87
- if column_exists?(:ruby_llm_agents_executions, :finish_reason)
88
- say_status :skip, "finish_reason column already exists", :yellow
89
- return
90
- end
91
-
92
- migration_template(
93
- "add_finish_reason_migration.rb.tt",
94
- File.join(db_migrate_path, "add_finish_reason_to_ruby_llm_agents_executions.rb")
95
- )
96
- end
97
-
98
- def create_add_caching_migration
99
- # Check if columns already exist
100
- if column_exists?(:ruby_llm_agents_executions, :cache_hit)
101
- say_status :skip, "cache_hit column already exists", :yellow
102
- return
103
- end
104
-
105
- migration_template(
106
- "add_caching_migration.rb.tt",
107
- File.join(db_migrate_path, "add_caching_to_ruby_llm_agents_executions.rb")
108
- )
109
- end
110
-
111
- def create_add_tool_calls_migration
112
- # Check if columns already exist
113
- if column_exists?(:ruby_llm_agents_executions, :tool_calls)
114
- say_status :skip, "tool_calls column already exists", :yellow
115
- return
116
- end
117
-
118
- migration_template(
119
- "add_tool_calls_migration.rb.tt",
120
- File.join(db_migrate_path, "add_tool_calls_to_ruby_llm_agents_executions.rb")
121
- )
122
- end
123
-
124
- def create_add_workflow_migration
125
- # Check if columns already exist
126
- if column_exists?(:ruby_llm_agents_executions, :workflow_id)
127
- say_status :skip, "workflow_id column already exists", :yellow
128
- return
129
- end
130
-
131
- migration_template(
132
- "add_workflow_migration.rb.tt",
133
- File.join(db_migrate_path, "add_workflow_to_ruby_llm_agents_executions.rb")
134
- )
135
- end
136
-
137
- def create_add_execution_type_migration
138
- # Check if columns already exist
139
- if column_exists?(:ruby_llm_agents_executions, :execution_type)
140
- say_status :skip, "execution_type column already exists", :yellow
23
+ # Main upgrade: split execution_details from executions table
24
+ #
25
+ # This single migration handles ALL schema transitions:
26
+ # - Creates execution_details table if missing
27
+ # - Migrates data from old columns on executions to execution_details
28
+ # - Removes deprecated columns (detail, niche, workflow, agent_version)
29
+ # - Adds any missing columns that should stay on executions
30
+ def create_split_execution_details_migration
31
+ if already_split?
32
+ say_status :skip, "execution_details already split from executions", :yellow
141
33
  return
142
34
  end
143
35
 
144
36
  migration_template(
145
- "add_execution_type_migration.rb.tt",
146
- File.join(db_migrate_path, "add_execution_type_to_ruby_llm_agents_executions.rb")
37
+ "split_execution_details_migration.rb.tt",
38
+ File.join(db_migrate_path, "split_execution_details_from_executions.rb")
147
39
  )
148
40
  end
149
41
 
42
+ # Rename tenant_budgets to tenants (v1.x -> v2.0 upgrade)
150
43
  def create_rename_tenant_budgets_migration
151
44
  # Skip if already using new table name
152
45
  if table_exists?(:ruby_llm_agents_tenants)
@@ -167,49 +60,35 @@ module RubyLlmAgents
167
60
  )
168
61
  end
169
62
 
170
- def migrate_agents_directory
171
- root_dir = RubyLLM::Agents.configuration.root_directory
172
- namespace = RubyLLM::Agents.configuration.root_namespace
173
- migrate_directory("agents", "#{root_dir}/agents", namespace)
174
- end
63
+ def suggest_config_consolidation
64
+ ruby_llm_initializer = File.join(destination_root, "config/initializers/ruby_llm.rb")
65
+ agents_initializer = File.join(destination_root, "config/initializers/ruby_llm_agents.rb")
175
66
 
176
- def migrate_tools_directory
177
- root_dir = RubyLLM::Agents.configuration.root_directory
178
- namespace = RubyLLM::Agents.configuration.root_namespace
179
- migrate_directory("tools", "#{root_dir}/tools", namespace)
180
- end
67
+ return unless File.exist?(ruby_llm_initializer) && File.exist?(agents_initializer)
181
68
 
182
- def show_post_upgrade_message
183
69
  say ""
184
- say "RubyLLM::Agents upgrade complete!", :green
185
- say ""
186
- say "Next steps:"
187
- say " 1. Run migrations: rails db:migrate"
188
- say " 2. Update class references in your controllers, views, and tests"
189
- say " 3. Run your test suite to find any broken references"
70
+ say "Optional: You can now consolidate your API key configuration.", :yellow
190
71
  say ""
191
- end
192
-
193
- def show_migration_summary
194
- namespace = RubyLLM::Agents.configuration.root_namespace
195
- root_dir = RubyLLM::Agents.configuration.root_directory
196
-
197
- return unless @agents_migrated || @tools_migrated
198
-
72
+ say "Move your API keys from config/initializers/ruby_llm.rb"
73
+ say "into config/initializers/ruby_llm_agents.rb:"
199
74
  say ""
200
- say "=" * 60
201
- say " File Migration Summary", :green
202
- say "=" * 60
75
+ say " RubyLLM::Agents.configure do |config|"
76
+ say " config.openai_api_key = ENV['OPENAI_API_KEY']"
77
+ say " config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']"
78
+ say " # ... rest of your agent config"
79
+ say " end"
203
80
  say ""
204
- say "Your agents and tools have been migrated to the new structure:"
81
+ say "Then delete config/initializers/ruby_llm.rb if it only contained API keys."
205
82
  say ""
206
- say " app/agents/ → app/#{root_dir}/agents/" if @agents_migrated
207
- say " app/tools/ → app/#{root_dir}/tools/" if @tools_migrated
83
+ end
84
+
85
+ def show_post_upgrade_message
208
86
  say ""
209
- say "Classes are now namespaced under #{namespace}::"
87
+ say "RubyLLM::Agents upgrade complete!", :green
210
88
  say ""
211
- say " Before: GeneralAgent.call(...)"
212
- say " After: #{namespace}::GeneralAgent.call(...)"
89
+ say "Next steps:"
90
+ say " 1. Run migrations: rails db:migrate"
91
+ say " 2. Run your test suite to verify everything works"
213
92
  say ""
214
93
  end
215
94
 
@@ -223,118 +102,57 @@ module RubyLlmAgents
223
102
  "db/migrate"
224
103
  end
225
104
 
226
- def column_exists?(table, column)
227
- return false unless ActiveRecord::Base.connection.table_exists?(table)
228
-
229
- ActiveRecord::Base.connection.column_exists?(table, column)
230
- rescue StandardError
231
- false
232
- end
105
+ # Check if the split has already been completed:
106
+ # - execution_details table exists
107
+ # - No detail columns remain on executions
108
+ # - No deprecated columns remain on executions
109
+ def already_split?
110
+ return false unless table_exists?(:ruby_llm_agents_execution_details)
111
+ return false if has_detail_columns_on_executions?
112
+ return false if has_deprecated_columns_on_executions?
233
113
 
234
- def table_exists?(table)
235
- ActiveRecord::Base.connection.table_exists?(table)
236
- rescue StandardError
237
- false
114
+ true
238
115
  end
239
116
 
240
- def migrate_directory(old_dir, new_dir, namespace)
241
- source = Rails.root.join("app", old_dir)
242
- destination = Rails.root.join("app", new_dir)
243
-
244
- # Skip if source doesn't exist
245
- unless File.directory?(source)
246
- say_status :skip, "app/#{old_dir}/ does not exist", :yellow
247
- return
248
- end
249
-
250
- # Skip if source and destination are the same
251
- if source.to_s == destination.to_s
252
- say_status :skip, "app/#{old_dir}/ is already at destination", :yellow
253
- return
254
- end
255
-
256
- files_to_migrate = Dir.glob("#{source}/**/*.rb")
257
- if files_to_migrate.empty?
258
- say_status :skip, "app/#{old_dir}/ has no Ruby files to migrate", :yellow
259
- return
260
- end
261
-
262
- if options[:pretend]
263
- say_status :preview, "Would move #{files_to_migrate.size} files from app/#{old_dir}/ → app/#{new_dir}/", :yellow
264
- files_to_migrate.each do |file|
265
- relative = file.sub("#{source}/", "")
266
- say_status :would_move, relative, :cyan
267
- end
268
- return
269
- end
270
-
271
- # Create destination directory
272
- FileUtils.mkdir_p(destination)
273
-
274
- # Track conflicts and migrated files
275
- conflicts = []
276
- migrated = []
277
-
278
- files_to_migrate.each do |file|
279
- relative_path = file.sub("#{source}/", "")
280
- dest_file = File.join(destination, relative_path)
281
-
282
- # Skip if destination file already exists (conflict)
283
- if File.exist?(dest_file)
284
- conflicts << relative_path
285
- say_status :conflict, "app/#{new_dir}/#{relative_path} already exists, skipping", :red
286
- next
287
- end
288
-
289
- FileUtils.mkdir_p(File.dirname(dest_file))
290
- FileUtils.mv(file, dest_file)
291
-
292
- wrap_in_namespace(dest_file, namespace)
293
- say_status :migrated, "app/#{new_dir}/#{relative_path}", :green
294
- migrated << relative_path
295
- end
117
+ # Detail columns that should only exist on execution_details, not executions
118
+ DETAIL_COLUMNS = %i[
119
+ error_message system_prompt user_prompt response messages_summary
120
+ tool_calls attempts fallback_chain parameters routed_to
121
+ classification_result cached_at cache_creation_tokens
122
+ ].freeze
296
123
 
297
- # Cleanup empty source directory
298
- cleanup_empty_directory(source, old_dir)
124
+ # Niche columns that should be in metadata JSON, not separate columns
125
+ NICHE_COLUMNS = %i[
126
+ span_id response_cache_key time_to_first_token_ms
127
+ retryable rate_limited fallback_reason
128
+ ].freeze
299
129
 
300
- # Track that migration happened for summary
301
- instance_variable_set("@#{old_dir}_migrated", migrated.any?)
130
+ # Deprecated columns
131
+ DEPRECATED_COLUMNS = %i[
132
+ agent_version workflow_id workflow_type workflow_step
133
+ tenant_record_type tenant_record_id
134
+ ].freeze
302
135
 
303
- { migrated: migrated, conflicts: conflicts }
136
+ def has_detail_columns_on_executions?
137
+ DETAIL_COLUMNS.any? { |col| column_exists?(:ruby_llm_agents_executions, col) }
304
138
  end
305
139
 
306
- def wrap_in_namespace(file, namespace)
307
- content = File.read(file)
308
-
309
- # Skip if already namespaced
310
- return if content.include?("module #{namespace}")
311
-
312
- # Wrap content in namespace with proper indentation
313
- indented = content.lines.map { |line| line.empty? || line.strip.empty? ? line : " #{line}" }.join
314
- wrapped = "module #{namespace}\n#{indented}end\n"
315
-
316
- File.write(file, wrapped)
140
+ def has_deprecated_columns_on_executions?
141
+ (NICHE_COLUMNS + DEPRECATED_COLUMNS).any? { |col| column_exists?(:ruby_llm_agents_executions, col) }
317
142
  end
318
143
 
319
- def cleanup_empty_directory(dir, dir_name)
320
- return unless File.directory?(dir)
321
-
322
- # Remove empty subdirectories first (deepest first)
323
- Dir.glob("#{dir}/**/", File::FNM_DOTMATCH).sort_by(&:length).reverse.each do |subdir|
324
- next if subdir.end_with?(".", "..")
144
+ def column_exists?(table, column)
145
+ return false unless ActiveRecord::Base.connection.table_exists?(table)
325
146
 
326
- FileUtils.rmdir(subdir) if File.directory?(subdir) && Dir.empty?(subdir)
327
- rescue Errno::ENOTEMPTY, Errno::ENOENT
328
- # Directory not empty or already removed
329
- end
147
+ ActiveRecord::Base.connection.column_exists?(table, column)
148
+ rescue StandardError
149
+ false
150
+ end
330
151
 
331
- # Remove main directory if empty
332
- if File.directory?(dir) && Dir.empty?(dir)
333
- FileUtils.rmdir(dir)
334
- say_status :removed, "app/#{dir_name}/ (empty)", :yellow
335
- end
336
- rescue Errno::ENOTEMPTY
337
- say_status :kept, "app/#{dir_name}/ (not empty)", :yellow
152
+ def table_exists?(table)
153
+ ActiveRecord::Base.connection.table_exists?(table)
154
+ rescue StandardError
155
+ false
338
156
  end
339
157
  end
340
158
  end
@@ -353,7 +353,6 @@ module RubyLLM
353
353
  "ruby_llm_agents",
354
354
  "speech",
355
355
  self.class.name,
356
- self.class.version,
357
356
  resolved_provider,
358
357
  resolved_model,
359
358
  resolved_voice,
@@ -355,7 +355,6 @@ module RubyLLM
355
355
  "ruby_llm_agents",
356
356
  "transcription",
357
357
  self.class.name,
358
- self.class.version,
359
358
  resolved_model,
360
359
  resolved_language,
361
360
  self.class.output_format,