ruby_llm-agents 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
- data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
- data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
- data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
- data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
- data/app/models/ruby_llm/agents/execution.rb +50 -14
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
- data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
- data/app/models/ruby_llm/agents/tenant.rb +146 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
- data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
- data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
- data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
- data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
- data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
- data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
- data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
- data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
- data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
- data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
- data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
- data/config/routes.rb +1 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
- data/lib/ruby_llm/agents/core/configuration.rb +55 -43
- data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
- data/lib/ruby_llm/agents/pipeline.rb +69 -0
- data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
- data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
- data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
- data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
- data/lib/ruby_llm/agents/workflow/result.rb +202 -0
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
- data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
- metadata +43 -6
- data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
- data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
- data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
- data/lib/ruby_llm/agents/workflow/router.rb +0 -429
|
@@ -16,6 +16,24 @@ module RubyLLM
|
|
|
16
16
|
include Paginatable
|
|
17
17
|
include Filterable
|
|
18
18
|
|
|
19
|
+
# Lists all registered workflows with their details
|
|
20
|
+
#
|
|
21
|
+
# Uses AgentRegistry to discover workflows from both file system
|
|
22
|
+
# and execution history. Separates workflows by type for sub-tabs.
|
|
23
|
+
# Supports sorting by various columns.
|
|
24
|
+
#
|
|
25
|
+
# @return [void]
|
|
26
|
+
def index
|
|
27
|
+
all_items = AgentRegistry.all_with_details
|
|
28
|
+
@workflows = all_items.select { |a| a[:is_workflow] }
|
|
29
|
+
@sort_params = parse_sort_params
|
|
30
|
+
@workflows = sort_workflows(@workflows)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
Rails.logger.error("[RubyLLM::Agents] Error loading workflows: #{e.message}")
|
|
33
|
+
@workflows = []
|
|
34
|
+
flash.now[:alert] = "Error loading workflows list"
|
|
35
|
+
end
|
|
36
|
+
|
|
19
37
|
# Shows detailed view for a specific workflow
|
|
20
38
|
#
|
|
21
39
|
# Loads workflow configuration (if class exists), statistics,
|
|
@@ -42,23 +60,20 @@ module RubyLLM
|
|
|
42
60
|
end
|
|
43
61
|
rescue StandardError => e
|
|
44
62
|
Rails.logger.error("[RubyLLM::Agents] Error loading workflow #{@workflow_type}: #{e.message}")
|
|
45
|
-
redirect_to ruby_llm_agents.
|
|
63
|
+
redirect_to ruby_llm_agents.workflows_path, alert: "Error loading workflow details"
|
|
46
64
|
end
|
|
47
65
|
|
|
48
66
|
private
|
|
49
67
|
|
|
50
|
-
# Detects the workflow type kind
|
|
68
|
+
# Detects the workflow type kind
|
|
69
|
+
#
|
|
70
|
+
# All workflows now use the DSL and return "workflow" type.
|
|
51
71
|
#
|
|
52
72
|
# @return [String, nil] The workflow type kind
|
|
53
73
|
def detect_workflow_type_kind
|
|
54
74
|
if @workflow_class
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"pipeline"
|
|
58
|
-
elsif ancestors.include?("RubyLLM::Agents::Workflow::Parallel")
|
|
59
|
-
"parallel"
|
|
60
|
-
elsif ancestors.include?("RubyLLM::Agents::Workflow::Router")
|
|
61
|
-
"router"
|
|
75
|
+
if @workflow_class.respond_to?(:step_configs) && @workflow_class.step_configs.any?
|
|
76
|
+
"workflow"
|
|
62
77
|
end
|
|
63
78
|
else
|
|
64
79
|
# Fallback to execution history
|
|
@@ -157,7 +172,6 @@ module RubyLLM
|
|
|
157
172
|
# @return [void]
|
|
158
173
|
def load_step_stats
|
|
159
174
|
@step_stats = calculate_step_stats
|
|
160
|
-
@route_distribution = calculate_route_distribution if @workflow_type_kind == "router"
|
|
161
175
|
end
|
|
162
176
|
|
|
163
177
|
# Calculates per-step/branch performance statistics
|
|
@@ -215,29 +229,6 @@ module RubyLLM
|
|
|
215
229
|
end.compact
|
|
216
230
|
end
|
|
217
231
|
|
|
218
|
-
# Calculates route distribution for router workflows
|
|
219
|
-
#
|
|
220
|
-
# @return [Hash] Route distribution data
|
|
221
|
-
def calculate_route_distribution
|
|
222
|
-
# Get route distribution from routed_to field
|
|
223
|
-
distribution = Execution.by_agent(@workflow_type)
|
|
224
|
-
.where("created_at > ?", 30.days.ago)
|
|
225
|
-
.where.not(routed_to: nil)
|
|
226
|
-
.group(:routed_to)
|
|
227
|
-
.count
|
|
228
|
-
|
|
229
|
-
total = distribution.values.sum
|
|
230
|
-
return {} if total.zero?
|
|
231
|
-
|
|
232
|
-
# Add percentage and sorting
|
|
233
|
-
distribution.transform_values do |count|
|
|
234
|
-
{
|
|
235
|
-
count: count,
|
|
236
|
-
percentage: (count.to_f / total * 100).round(1)
|
|
237
|
-
}
|
|
238
|
-
end.sort_by { |_k, v| -v[:count] }.to_h
|
|
239
|
-
end
|
|
240
|
-
|
|
241
232
|
# Loads the current workflow class configuration
|
|
242
233
|
#
|
|
243
234
|
# @return [void]
|
|
@@ -250,91 +241,252 @@ module RubyLLM
|
|
|
250
241
|
max_cost: safe_call(@workflow_class, :max_cost)
|
|
251
242
|
}
|
|
252
243
|
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
when "pipeline"
|
|
256
|
-
load_pipeline_config
|
|
257
|
-
when "parallel"
|
|
258
|
-
load_parallel_config
|
|
259
|
-
when "router"
|
|
260
|
-
load_router_config
|
|
261
|
-
end
|
|
244
|
+
# Load unified workflow structure for all types
|
|
245
|
+
load_unified_workflow_config
|
|
262
246
|
end
|
|
263
247
|
|
|
264
|
-
# Loads
|
|
248
|
+
# Loads unified workflow configuration for all workflow types
|
|
265
249
|
#
|
|
266
250
|
# @return [void]
|
|
267
|
-
def
|
|
268
|
-
@
|
|
251
|
+
def load_unified_workflow_config
|
|
252
|
+
@parallel_groups = []
|
|
253
|
+
@input_schema_fields = {}
|
|
254
|
+
@lifecycle_hooks = {}
|
|
255
|
+
|
|
256
|
+
# All workflows use DSL
|
|
257
|
+
@steps = extract_dsl_steps(@workflow_class)
|
|
258
|
+
@parallel_groups = extract_parallel_groups(@workflow_class)
|
|
259
|
+
@lifecycle_hooks = extract_lifecycle_hooks(@workflow_class)
|
|
260
|
+
|
|
269
261
|
@config[:steps_count] = @steps.size
|
|
262
|
+
@config[:parallel_groups_count] = @parallel_groups.size
|
|
263
|
+
@config[:has_routing] = @steps.any? { |s| s[:routing] }
|
|
264
|
+
@config[:has_conditions] = @steps.any? { |s| s[:if_condition] || s[:unless_condition] }
|
|
265
|
+
@config[:has_retries] = @steps.any? { |s| s[:retry_config] }
|
|
266
|
+
@config[:has_fallbacks] = @steps.any? { |s| s[:fallbacks]&.any? }
|
|
267
|
+
@config[:has_lifecycle_hooks] = @lifecycle_hooks.values.any? { |v| v.to_i > 0 }
|
|
268
|
+
@config[:has_input_schema] = @workflow_class.respond_to?(:input_schema) && @workflow_class.input_schema.present?
|
|
269
|
+
|
|
270
|
+
if @config[:has_input_schema]
|
|
271
|
+
@input_schema_fields = @workflow_class.input_schema.fields.transform_values(&:to_h)
|
|
272
|
+
end
|
|
270
273
|
end
|
|
271
274
|
|
|
272
|
-
#
|
|
275
|
+
# Extracts steps from a DSL-based workflow class with full configuration
|
|
273
276
|
#
|
|
274
|
-
# @
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
# @param klass [Class] The workflow class
|
|
278
|
+
# @return [Array<Hash>] Array of step hashes with full DSL metadata
|
|
279
|
+
def extract_dsl_steps(klass)
|
|
280
|
+
return [] unless klass.respond_to?(:step_metadata) && klass.respond_to?(:step_configs)
|
|
281
|
+
|
|
282
|
+
step_configs = klass.step_configs
|
|
283
|
+
|
|
284
|
+
klass.step_metadata.map do |meta|
|
|
285
|
+
# Handle wait steps - they have type: :wait and aren't in step_configs
|
|
286
|
+
if meta[:type] == :wait
|
|
287
|
+
{
|
|
288
|
+
name: meta[:name],
|
|
289
|
+
type: :wait,
|
|
290
|
+
wait_type: meta[:wait_type],
|
|
291
|
+
ui_label: meta[:ui_label],
|
|
292
|
+
timeout: meta[:timeout],
|
|
293
|
+
duration: meta[:duration],
|
|
294
|
+
poll_interval: meta[:poll_interval],
|
|
295
|
+
on_timeout: meta[:on_timeout],
|
|
296
|
+
notify: meta[:notify],
|
|
297
|
+
approvers: meta[:approvers],
|
|
298
|
+
parallel: false
|
|
299
|
+
}.compact
|
|
300
|
+
else
|
|
301
|
+
config = step_configs[meta[:name]]
|
|
302
|
+
step_hash = {
|
|
303
|
+
name: meta[:name],
|
|
304
|
+
agent: meta[:agent],
|
|
305
|
+
description: meta[:description],
|
|
306
|
+
ui_label: meta[:ui_label],
|
|
307
|
+
optional: meta[:optional],
|
|
308
|
+
timeout: meta[:timeout],
|
|
309
|
+
routing: meta[:routing],
|
|
310
|
+
parallel: meta[:parallel],
|
|
311
|
+
parallel_group: meta[:parallel_group],
|
|
312
|
+
custom_block: config&.custom_block?,
|
|
313
|
+
# New composition features
|
|
314
|
+
workflow: meta[:workflow],
|
|
315
|
+
iteration: meta[:iteration],
|
|
316
|
+
iteration_concurrency: meta[:iteration_concurrency]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Add extended configuration from StepConfig
|
|
320
|
+
if config
|
|
321
|
+
step_hash.merge!(
|
|
322
|
+
retry_config: extract_retry_config(config),
|
|
323
|
+
fallbacks: config.fallbacks.map(&:name),
|
|
324
|
+
if_condition: describe_condition(config.if_condition),
|
|
325
|
+
unless_condition: describe_condition(config.unless_condition),
|
|
326
|
+
has_input_mapper: config.input_mapper.present?,
|
|
327
|
+
pick_fields: config.pick_fields,
|
|
328
|
+
pick_from: config.pick_from,
|
|
329
|
+
default_value: config.default_value,
|
|
330
|
+
routes: extract_routes(config),
|
|
331
|
+
# Iteration error handling
|
|
332
|
+
iteration_fail_fast: config.iteration_fail_fast?,
|
|
333
|
+
continue_on_error: config.continue_on_error?
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Add sub-workflow metadata for nested workflow steps
|
|
337
|
+
if config.workflow? && config.agent
|
|
338
|
+
step_hash[:sub_workflow] = extract_sub_workflow_metadata(config.agent)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
step_hash.compact
|
|
343
|
+
end
|
|
344
|
+
end
|
|
279
345
|
end
|
|
280
346
|
|
|
281
|
-
#
|
|
347
|
+
# Extracts retry configuration in a display-friendly format
|
|
282
348
|
#
|
|
283
|
-
# @
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
349
|
+
# @param config [StepConfig] The step configuration
|
|
350
|
+
# @return [Hash, nil] Retry config hash or nil
|
|
351
|
+
def extract_retry_config(config)
|
|
352
|
+
retry_cfg = config.retry_config
|
|
353
|
+
return nil unless retry_cfg && retry_cfg[:max].to_i > 0
|
|
354
|
+
|
|
355
|
+
{
|
|
356
|
+
max: retry_cfg[:max],
|
|
357
|
+
backoff: retry_cfg[:backoff],
|
|
358
|
+
delay: retry_cfg[:delay]
|
|
359
|
+
}
|
|
289
360
|
end
|
|
290
361
|
|
|
291
|
-
#
|
|
362
|
+
# Describes a condition for display
|
|
292
363
|
#
|
|
293
|
-
# @param
|
|
294
|
-
# @return [
|
|
295
|
-
def
|
|
296
|
-
return
|
|
364
|
+
# @param condition [Symbol, Proc, nil] The condition
|
|
365
|
+
# @return [String, nil] Human-readable description
|
|
366
|
+
def describe_condition(condition)
|
|
367
|
+
return nil if condition.nil?
|
|
368
|
+
|
|
369
|
+
case condition
|
|
370
|
+
when Symbol then condition.to_s
|
|
371
|
+
when Proc then "lambda"
|
|
372
|
+
else condition.to_s
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Extracts routes from a routing step
|
|
377
|
+
#
|
|
378
|
+
# @param config [StepConfig] The step configuration
|
|
379
|
+
# @return [Array<Hash>, nil] Array of route hashes or nil
|
|
380
|
+
def extract_routes(config)
|
|
381
|
+
return nil unless config.routing? && config.block
|
|
382
|
+
|
|
383
|
+
builder = RubyLLM::Agents::Workflow::DSL::RouteBuilder.new
|
|
384
|
+
config.block.call(builder)
|
|
297
385
|
|
|
298
|
-
|
|
386
|
+
routes = builder.routes.map do |name, route_config|
|
|
299
387
|
{
|
|
300
|
-
name: name,
|
|
301
|
-
agent:
|
|
302
|
-
|
|
303
|
-
|
|
388
|
+
name: name.to_s,
|
|
389
|
+
agent: route_config[:agent]&.name,
|
|
390
|
+
timeout: extract_timeout_value(route_config[:options][:timeout]),
|
|
391
|
+
fallback: Array(route_config[:options][:fallback]).first&.then { |f| f.respond_to?(:name) ? f.name : f.to_s },
|
|
392
|
+
has_input_mapper: route_config[:options][:input].present?,
|
|
393
|
+
if_condition: describe_condition(route_config[:options][:if]),
|
|
394
|
+
default: false
|
|
395
|
+
}.compact
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Add default route
|
|
399
|
+
if builder.default
|
|
400
|
+
routes << {
|
|
401
|
+
name: "default",
|
|
402
|
+
agent: builder.default[:agent]&.name,
|
|
403
|
+
timeout: extract_timeout_value(builder.default[:options][:timeout]),
|
|
404
|
+
has_input_mapper: builder.default[:options][:input].present?,
|
|
405
|
+
default: true
|
|
406
|
+
}.compact
|
|
304
407
|
end
|
|
408
|
+
|
|
409
|
+
routes
|
|
410
|
+
rescue StandardError => e
|
|
411
|
+
Rails.logger.debug "[RubyLLM::Agents] Could not extract routes: #{e.message}"
|
|
412
|
+
nil
|
|
305
413
|
end
|
|
306
414
|
|
|
307
|
-
# Extracts
|
|
415
|
+
# Extracts timeout value handling ActiveSupport::Duration
|
|
308
416
|
#
|
|
309
|
-
# @param
|
|
310
|
-
# @return [
|
|
311
|
-
def
|
|
312
|
-
return
|
|
417
|
+
# @param timeout [Integer, ActiveSupport::Duration, nil] The timeout value
|
|
418
|
+
# @return [Integer, nil] Timeout in seconds or nil
|
|
419
|
+
def extract_timeout_value(timeout)
|
|
420
|
+
return nil if timeout.nil?
|
|
421
|
+
|
|
422
|
+
timeout.respond_to?(:to_i) ? timeout.to_i : timeout
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Extracts metadata for a nested sub-workflow
|
|
426
|
+
#
|
|
427
|
+
# @param workflow_class [Class] The sub-workflow class
|
|
428
|
+
# @return [Hash] Sub-workflow metadata including steps preview and budget info
|
|
429
|
+
def extract_sub_workflow_metadata(workflow_class)
|
|
430
|
+
return nil unless workflow_class.respond_to?(:step_metadata)
|
|
431
|
+
|
|
432
|
+
{
|
|
433
|
+
name: workflow_class.name,
|
|
434
|
+
description: safe_call(workflow_class, :description),
|
|
435
|
+
timeout: safe_call(workflow_class, :timeout),
|
|
436
|
+
max_cost: safe_call(workflow_class, :max_cost),
|
|
437
|
+
max_recursion_depth: safe_call(workflow_class, :max_recursion_depth),
|
|
438
|
+
steps_count: workflow_class.step_configs.size,
|
|
439
|
+
steps_preview: extract_sub_workflow_steps_preview(workflow_class)
|
|
440
|
+
}.compact
|
|
441
|
+
rescue StandardError => e
|
|
442
|
+
Rails.logger.debug "[RubyLLM::Agents] Could not extract sub-workflow metadata: #{e.message}"
|
|
443
|
+
nil
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Extracts a simplified steps preview for sub-workflow display
|
|
447
|
+
#
|
|
448
|
+
# @param workflow_class [Class] The sub-workflow class
|
|
449
|
+
# @return [Array<Hash>] Simplified step hashes for preview
|
|
450
|
+
def extract_sub_workflow_steps_preview(workflow_class)
|
|
451
|
+
return [] unless workflow_class.respond_to?(:step_metadata)
|
|
313
452
|
|
|
314
|
-
|
|
453
|
+
workflow_class.step_metadata.map do |meta|
|
|
315
454
|
{
|
|
316
|
-
name: name,
|
|
317
|
-
agent:
|
|
318
|
-
|
|
319
|
-
|
|
455
|
+
name: meta[:name],
|
|
456
|
+
agent: meta[:agent]&.gsub(/Agent$/, "")&.gsub(/Workflow$/, ""),
|
|
457
|
+
routing: meta[:routing],
|
|
458
|
+
iteration: meta[:iteration],
|
|
459
|
+
workflow: meta[:workflow],
|
|
460
|
+
parallel: meta[:parallel]
|
|
461
|
+
}.compact
|
|
320
462
|
end
|
|
463
|
+
rescue StandardError
|
|
464
|
+
[]
|
|
321
465
|
end
|
|
322
466
|
|
|
323
|
-
# Extracts
|
|
467
|
+
# Extracts parallel groups from a DSL-based workflow class
|
|
324
468
|
#
|
|
325
469
|
# @param klass [Class] The workflow class
|
|
326
|
-
# @return [Array<Hash>] Array of
|
|
327
|
-
def
|
|
328
|
-
return [] unless klass.respond_to?(:
|
|
470
|
+
# @return [Array<Hash>] Array of parallel group hashes
|
|
471
|
+
def extract_parallel_groups(klass)
|
|
472
|
+
return [] unless klass.respond_to?(:parallel_groups)
|
|
329
473
|
|
|
330
|
-
klass.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
474
|
+
klass.parallel_groups.map(&:to_h)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Extracts lifecycle hooks from a workflow class
|
|
478
|
+
#
|
|
479
|
+
# @param klass [Class] The workflow class
|
|
480
|
+
# @return [Hash] Hash of hook types to counts
|
|
481
|
+
def extract_lifecycle_hooks(klass)
|
|
482
|
+
return {} unless klass.respond_to?(:lifecycle_hooks)
|
|
483
|
+
|
|
484
|
+
hooks = klass.lifecycle_hooks
|
|
485
|
+
{
|
|
486
|
+
before_workflow: hooks[:before_workflow]&.size || 0,
|
|
487
|
+
after_workflow: hooks[:after_workflow]&.size || 0,
|
|
488
|
+
on_step_error: hooks[:on_step_error]&.size || 0
|
|
489
|
+
}
|
|
338
490
|
end
|
|
339
491
|
|
|
340
492
|
# Safely calls a method on a class, returning nil if method doesn't exist
|
|
@@ -350,6 +502,43 @@ module RubyLLM
|
|
|
350
502
|
rescue StandardError
|
|
351
503
|
nil
|
|
352
504
|
end
|
|
505
|
+
|
|
506
|
+
# Parses and validates sort parameters from request
|
|
507
|
+
#
|
|
508
|
+
# @return [Hash] Hash with :column and :direction keys
|
|
509
|
+
def parse_sort_params
|
|
510
|
+
allowed_columns = %w[name workflow_type execution_count total_cost success_rate last_executed]
|
|
511
|
+
column = params[:sort]
|
|
512
|
+
direction = params[:direction]
|
|
513
|
+
|
|
514
|
+
{
|
|
515
|
+
column: allowed_columns.include?(column) ? column : "name",
|
|
516
|
+
direction: %w[asc desc].include?(direction) ? direction : "asc"
|
|
517
|
+
}
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Sorts workflows array by the specified column and direction
|
|
521
|
+
#
|
|
522
|
+
# @param workflows [Array<Hash>] Array of workflow hashes
|
|
523
|
+
# @return [Array<Hash>] Sorted array
|
|
524
|
+
def sort_workflows(workflows)
|
|
525
|
+
column = @sort_params[:column].to_sym
|
|
526
|
+
direction = @sort_params[:direction]
|
|
527
|
+
|
|
528
|
+
sorted = workflows.sort_by do |w|
|
|
529
|
+
value = w[column]
|
|
530
|
+
case column
|
|
531
|
+
when :name
|
|
532
|
+
value.to_s.downcase
|
|
533
|
+
when :last_executed
|
|
534
|
+
value || Time.at(0)
|
|
535
|
+
else
|
|
536
|
+
value || 0
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
direction == "desc" ? sorted.reverse : sorted
|
|
541
|
+
end
|
|
353
542
|
end
|
|
354
543
|
end
|
|
355
544
|
end
|
|
@@ -11,6 +11,39 @@ module RubyLLM
|
|
|
11
11
|
module ApplicationHelper
|
|
12
12
|
include Chartkick::Helper if defined?(Chartkick)
|
|
13
13
|
|
|
14
|
+
# Wiki base URL for documentation links
|
|
15
|
+
WIKI_BASE_URL = "https://github.com/adham90/ruby_llm-agents/wiki/".freeze
|
|
16
|
+
|
|
17
|
+
# Page to documentation mapping
|
|
18
|
+
DOC_PAGES = {
|
|
19
|
+
"dashboard/index" => "Dashboard",
|
|
20
|
+
"agents/index" => "Agent-DSL",
|
|
21
|
+
"agents/show" => "Agent-DSL",
|
|
22
|
+
"workflows/index" => "Workflows",
|
|
23
|
+
"workflows/show" => "Workflows",
|
|
24
|
+
"executions/index" => "Execution-Tracking",
|
|
25
|
+
"executions/show" => "Execution-Tracking",
|
|
26
|
+
"tenants/index" => "Multi-Tenancy",
|
|
27
|
+
"system_config/show" => "Configuration",
|
|
28
|
+
"api_configurations/show" => "Configuration"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# Returns the documentation URL for the current page or a specific page key
|
|
32
|
+
#
|
|
33
|
+
# @param page_key [String, nil] Optional page key (e.g., "agents/index")
|
|
34
|
+
# @return [String, nil] The documentation URL or nil if no mapping exists
|
|
35
|
+
# @example Get documentation URL for current page
|
|
36
|
+
# documentation_url #=> "https://github.com/adham90/ruby_llm-agents/wiki/Agent-DSL"
|
|
37
|
+
# @example Get documentation URL for specific page
|
|
38
|
+
# documentation_url("dashboard/index") #=> "https://github.com/adham90/ruby_llm-agents/wiki/Dashboard"
|
|
39
|
+
def documentation_url(page_key = nil)
|
|
40
|
+
key = page_key || "#{controller_name}/#{action_name}"
|
|
41
|
+
doc_page = DOC_PAGES[key]
|
|
42
|
+
return nil unless doc_page
|
|
43
|
+
|
|
44
|
+
"#{WIKI_BASE_URL}#{doc_page}"
|
|
45
|
+
end
|
|
46
|
+
|
|
14
47
|
# Returns the URL helpers for the engine's routes
|
|
15
48
|
#
|
|
16
49
|
# Use this to generate paths and URLs within the dashboard views.
|
|
@@ -250,6 +283,73 @@ module RubyLLM
|
|
|
250
283
|
end
|
|
251
284
|
end
|
|
252
285
|
|
|
286
|
+
# Compact comparison indicator with arrow for now strip metrics
|
|
287
|
+
#
|
|
288
|
+
# Shows a colored arrow indicator showing percentage change vs previous period.
|
|
289
|
+
# For errors/cost/duration: decrease is good (green). For success/tokens: increase is good.
|
|
290
|
+
#
|
|
291
|
+
# @param change_pct [Float, nil] Percentage change from previous period
|
|
292
|
+
# @param metric_type [Symbol] Type of metric (:success, :errors, :cost, :duration, :tokens)
|
|
293
|
+
# @return [ActiveSupport::SafeBuffer, String] HTML span with indicator or empty string
|
|
294
|
+
def comparison_indicator(change_pct, metric_type: :count)
|
|
295
|
+
return "".html_safe if change_pct.nil?
|
|
296
|
+
|
|
297
|
+
# For errors/cost/duration, decrease is good. For success/tokens, increase is good.
|
|
298
|
+
positive_is_good = metric_type.in?(%i[success tokens count])
|
|
299
|
+
is_improvement = positive_is_good ? change_pct > 0 : change_pct < 0
|
|
300
|
+
|
|
301
|
+
arrow = change_pct > 0 ? "\u2191" : "\u2193"
|
|
302
|
+
color = is_improvement ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"
|
|
303
|
+
|
|
304
|
+
content_tag(:span, "#{arrow}#{change_pct.abs}%", class: "text-xs font-medium #{color} ml-1")
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Returns human-readable display name for time range
|
|
308
|
+
#
|
|
309
|
+
# @param range [String] Range parameter (today, 7d, 30d, 60d, 90d, or custom YYYY-MM-DD_YYYY-MM-DD)
|
|
310
|
+
# @return [String] Human-readable range name
|
|
311
|
+
# @example
|
|
312
|
+
# range_display_name("7d") #=> "Last 7 Days"
|
|
313
|
+
# range_display_name("2024-01-01_2024-01-15") #=> "Jan 1 - Jan 15"
|
|
314
|
+
def range_display_name(range)
|
|
315
|
+
case range
|
|
316
|
+
when "today" then "Today"
|
|
317
|
+
when "7d" then "Last 7 Days"
|
|
318
|
+
when "30d" then "Last 30 Days"
|
|
319
|
+
when "60d" then "Last 60 Days"
|
|
320
|
+
when "90d" then "Last 90 Days"
|
|
321
|
+
else
|
|
322
|
+
if range&.include?("_")
|
|
323
|
+
from_str, to_str = range.split("_")
|
|
324
|
+
from_date = Date.parse(from_str) rescue nil
|
|
325
|
+
to_date = Date.parse(to_str) rescue nil
|
|
326
|
+
if from_date && to_date
|
|
327
|
+
"#{from_date.strftime('%b %-d')} - #{to_date.strftime('%b %-d')}"
|
|
328
|
+
else
|
|
329
|
+
"Custom Range"
|
|
330
|
+
end
|
|
331
|
+
else
|
|
332
|
+
"Today"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Formats milliseconds to human-readable duration
|
|
338
|
+
#
|
|
339
|
+
# @param ms [Numeric, nil] Duration in milliseconds
|
|
340
|
+
# @return [String] Human-readable duration (e.g., "150ms", "2.5s", "1.2m")
|
|
341
|
+
def format_duration_ms(ms)
|
|
342
|
+
return "0ms" if ms.nil? || ms.zero?
|
|
343
|
+
|
|
344
|
+
if ms < 1000
|
|
345
|
+
"#{ms.round}ms"
|
|
346
|
+
elsif ms < 60_000
|
|
347
|
+
"#{(ms / 1000.0).round(1)}s"
|
|
348
|
+
else
|
|
349
|
+
"#{(ms / 60_000.0).round(1)}m"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
253
353
|
# Returns the appropriate row background class based on change significance
|
|
254
354
|
#
|
|
255
355
|
# @param change_pct [Float] Percentage change
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Mailer for sending alert notifications via email
|
|
6
|
+
#
|
|
7
|
+
# Delivers alert notifications when important events occur like
|
|
8
|
+
# budget exceedance or circuit breaker activation.
|
|
9
|
+
#
|
|
10
|
+
# @example Sending an alert email
|
|
11
|
+
# AlertMailer.alert_notification(
|
|
12
|
+
# event: :budget_hard_cap,
|
|
13
|
+
# payload: { limit: 100.0, total: 105.0 },
|
|
14
|
+
# recipient: "admin@example.com"
|
|
15
|
+
# ).deliver_later
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
class AlertMailer < ApplicationMailer
|
|
19
|
+
# Sends an alert notification email
|
|
20
|
+
#
|
|
21
|
+
# @param event [Symbol] The event type (e.g., :budget_soft_cap, :breaker_open)
|
|
22
|
+
# @param payload [Hash] Event-specific data
|
|
23
|
+
# @param recipient [String] Email address of the recipient
|
|
24
|
+
# @return [Mail::Message]
|
|
25
|
+
def alert_notification(event:, payload:, recipient:)
|
|
26
|
+
@event = event
|
|
27
|
+
@payload = payload
|
|
28
|
+
@title = event_title(event)
|
|
29
|
+
@severity = event_severity(event)
|
|
30
|
+
@color = event_color(event)
|
|
31
|
+
@timestamp = Time.current
|
|
32
|
+
|
|
33
|
+
mail(
|
|
34
|
+
to: recipient,
|
|
35
|
+
subject: "[RubyLLM::Agents Alert] #{@title}"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Returns human-readable title for event type
|
|
42
|
+
#
|
|
43
|
+
# @param event [Symbol] The event type
|
|
44
|
+
# @return [String] Human-readable title
|
|
45
|
+
def event_title(event)
|
|
46
|
+
case event
|
|
47
|
+
when :budget_soft_cap then "Budget Soft Cap Reached"
|
|
48
|
+
when :budget_hard_cap then "Budget Hard Cap Exceeded"
|
|
49
|
+
when :breaker_open then "Circuit Breaker Opened"
|
|
50
|
+
when :agent_anomaly then "Agent Anomaly Detected"
|
|
51
|
+
else event.to_s.titleize
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns severity level for event type
|
|
56
|
+
#
|
|
57
|
+
# @param event [Symbol] The event type
|
|
58
|
+
# @return [String] Severity level
|
|
59
|
+
def event_severity(event)
|
|
60
|
+
case event
|
|
61
|
+
when :budget_soft_cap then "Warning"
|
|
62
|
+
when :budget_hard_cap then "Critical"
|
|
63
|
+
when :breaker_open then "Critical"
|
|
64
|
+
when :agent_anomaly then "Warning"
|
|
65
|
+
else "Info"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns color for event type
|
|
70
|
+
#
|
|
71
|
+
# @param event [Symbol] The event type
|
|
72
|
+
# @return [String] Hex color code
|
|
73
|
+
def event_color(event)
|
|
74
|
+
case event
|
|
75
|
+
when :budget_soft_cap then "#FFA500" # Orange
|
|
76
|
+
when :budget_hard_cap then "#FF0000" # Red
|
|
77
|
+
when :breaker_open then "#FF0000" # Red
|
|
78
|
+
when :agent_anomaly then "#FFA500" # Orange
|
|
79
|
+
else "#0000FF" # Blue
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Base mailer class for RubyLLM::Agents
|
|
6
|
+
#
|
|
7
|
+
# Host application must configure ActionMailer with SMTP settings
|
|
8
|
+
# for email delivery to work.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
class ApplicationMailer < ::ActionMailer::Base
|
|
12
|
+
default from: -> { default_from_address }
|
|
13
|
+
|
|
14
|
+
layout false # Templates are self-contained
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def default_from_address
|
|
19
|
+
RubyLLM::Agents.configuration.alerts&.dig(:email_from) ||
|
|
20
|
+
"noreply@#{default_host}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def default_host
|
|
24
|
+
::ActionMailer::Base.default_url_options[:host] || "example.com"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|