ruby_llm-agents 1.3.3 → 2.0.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  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 +46 -10
  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 +87 -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 +528 -989
  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 +9 -14
  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 +9 -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 +58 -262
  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 +52 -6
  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 +58 -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/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. 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
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
50
33
  return
51
34
  end
52
35
 
53
36
  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
141
- return
142
- end
143
-
144
- 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,13 @@ 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
175
-
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
181
-
182
63
  def show_post_upgrade_message
183
64
  say ""
184
65
  say "RubyLLM::Agents upgrade complete!", :green
185
66
  say ""
186
67
  say "Next steps:"
187
68
  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"
190
- 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
-
199
- say ""
200
- say "=" * 60
201
- say " File Migration Summary", :green
202
- say "=" * 60
203
- say ""
204
- say "Your agents and tools have been migrated to the new structure:"
205
- say ""
206
- say " app/agents/ → app/#{root_dir}/agents/" if @agents_migrated
207
- say " app/tools/ → app/#{root_dir}/tools/" if @tools_migrated
208
- say ""
209
- say "Classes are now namespaced under #{namespace}::"
210
- say ""
211
- say " Before: GeneralAgent.call(...)"
212
- say " After: #{namespace}::GeneralAgent.call(...)"
69
+ say " 2. Run your test suite to verify everything works"
213
70
  say ""
214
71
  end
215
72
 
@@ -223,118 +80,57 @@ module RubyLlmAgents
223
80
  "db/migrate"
224
81
  end
225
82
 
226
- def column_exists?(table, column)
227
- return false unless ActiveRecord::Base.connection.table_exists?(table)
83
+ # Check if the split has already been completed:
84
+ # - execution_details table exists
85
+ # - No detail columns remain on executions
86
+ # - No deprecated columns remain on executions
87
+ def already_split?
88
+ return false unless table_exists?(:ruby_llm_agents_execution_details)
89
+ return false if has_detail_columns_on_executions?
90
+ return false if has_deprecated_columns_on_executions?
228
91
 
229
- ActiveRecord::Base.connection.column_exists?(table, column)
230
- rescue StandardError
231
- false
92
+ true
232
93
  end
233
94
 
234
- def table_exists?(table)
235
- ActiveRecord::Base.connection.table_exists?(table)
236
- rescue StandardError
237
- false
238
- end
239
-
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
95
+ # Detail columns that should only exist on execution_details, not executions
96
+ DETAIL_COLUMNS = %i[
97
+ error_message system_prompt user_prompt response messages_summary
98
+ tool_calls attempts fallback_chain parameters routed_to
99
+ classification_result cached_at cache_creation_tokens
100
+ ].freeze
270
101
 
271
- # Create destination directory
272
- FileUtils.mkdir_p(destination)
102
+ # Niche columns that should be in metadata JSON, not separate columns
103
+ NICHE_COLUMNS = %i[
104
+ span_id response_cache_key time_to_first_token_ms
105
+ retryable rate_limited fallback_reason
106
+ ].freeze
273
107
 
274
- # Track conflicts and migrated files
275
- conflicts = []
276
- migrated = []
108
+ # Deprecated columns
109
+ DEPRECATED_COLUMNS = %i[
110
+ agent_version workflow_id workflow_type workflow_step
111
+ tenant_record_type tenant_record_id
112
+ ].freeze
277
113
 
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
296
-
297
- # Cleanup empty source directory
298
- cleanup_empty_directory(source, old_dir)
299
-
300
- # Track that migration happened for summary
301
- instance_variable_set("@#{old_dir}_migrated", migrated.any?)
302
-
303
- { migrated: migrated, conflicts: conflicts }
114
+ def has_detail_columns_on_executions?
115
+ DETAIL_COLUMNS.any? { |col| column_exists?(:ruby_llm_agents_executions, col) }
304
116
  end
305
117
 
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)
118
+ def has_deprecated_columns_on_executions?
119
+ (NICHE_COLUMNS + DEPRECATED_COLUMNS).any? { |col| column_exists?(:ruby_llm_agents_executions, col) }
317
120
  end
318
121
 
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?(".", "..")
122
+ def column_exists?(table, column)
123
+ return false unless ActiveRecord::Base.connection.table_exists?(table)
325
124
 
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
125
+ ActiveRecord::Base.connection.column_exists?(table, column)
126
+ rescue StandardError
127
+ false
128
+ end
330
129
 
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
130
+ def table_exists?(table)
131
+ ActiveRecord::Base.connection.table_exists?(table)
132
+ rescue StandardError
133
+ false
338
134
  end
339
135
  end
340
136
  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,