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.
- checksums.yaml +4 -4
- data/README.md +101 -334
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
- data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
- data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
- data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
- data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
- data/app/models/ruby_llm/agents/execution.rb +46 -10
- data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
- data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
- data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
- data/app/models/ruby_llm/agents/tenant.rb +2 -3
- data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
- data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
- data/app/views/layouts/ruby_llm/agents/application.html.erb +87 -252
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
- data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
- data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
- data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
- data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
- data/app/views/ruby_llm/agents/executions/show.html.erb +528 -989
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
- data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
- data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
- data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
- data/config/routes.rb +0 -13
- data/lib/generators/ruby_llm_agents/install_generator.rb +9 -14
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
- data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +9 -12
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
- data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
- data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +58 -262
- data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
- data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
- data/lib/ruby_llm/agents/base_agent.rb +52 -6
- data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
- data/lib/ruby_llm/agents/core/base.rb +23 -55
- data/lib/ruby_llm/agents/core/configuration.rb +58 -117
- data/lib/ruby_llm/agents/core/errors.rb +0 -58
- data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
- data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +157 -17
- data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
- data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -2
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
- data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
- data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/editor.rb +0 -1
- data/lib/ruby_llm/agents/image/generator.rb +0 -21
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/transformer.rb +0 -1
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
- data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +1 -0
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
- data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
- data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
- data/lib/ruby_llm/agents/rails/engine.rb +6 -6
- data/lib/ruby_llm/agents/results/base.rb +1 -49
- data/lib/ruby_llm/agents/text/embedder.rb +0 -1
- data/lib/ruby_llm/agents.rb +1 -9
- data/lib/tasks/ruby_llm_agents.rake +34 -0
- metadata +12 -81
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
- data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
- data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
- data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
- data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
- data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
- data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
- data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
- data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
- data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
- data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
- data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
- data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
- data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
- data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
- data/lib/ruby_llm/agents/text/moderator.rb +0 -237
- data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
- data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
- data/lib/ruby_llm/agents/workflow/async.rb +0 -220
- data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
- data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
- data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
- data/lib/ruby_llm/agents/workflow/result.rb +0 -592
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
- data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Workflow
|
|
6
|
-
# Instrumentation concern for workflow execution tracking
|
|
7
|
-
#
|
|
8
|
-
# Provides comprehensive workflow tracking including:
|
|
9
|
-
# - Root execution record creation for the workflow
|
|
10
|
-
# - Timing metrics (started_at, completed_at, duration_ms)
|
|
11
|
-
# - Aggregate token usage and cost across all steps/branches
|
|
12
|
-
# - Workflow-specific metadata (workflow_id, workflow_type)
|
|
13
|
-
# - Error handling with proper status updates
|
|
14
|
-
#
|
|
15
|
-
# @api private
|
|
16
|
-
module Instrumentation
|
|
17
|
-
extend ActiveSupport::Concern
|
|
18
|
-
|
|
19
|
-
included do
|
|
20
|
-
# @!attribute [rw] execution_id
|
|
21
|
-
# The ID of the workflow's root execution record
|
|
22
|
-
# @return [Integer, nil]
|
|
23
|
-
attr_accessor :execution_id
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Wraps workflow execution with comprehensive metrics tracking
|
|
27
|
-
#
|
|
28
|
-
# Creates a root execution record for the workflow and tracks
|
|
29
|
-
# aggregate metrics from all child executions.
|
|
30
|
-
#
|
|
31
|
-
# @yield The block containing the workflow execution
|
|
32
|
-
# @return [WorkflowResult] The workflow result
|
|
33
|
-
def instrument_workflow(&block)
|
|
34
|
-
started_at = Time.current
|
|
35
|
-
@workflow_started_at = started_at
|
|
36
|
-
|
|
37
|
-
# Create workflow execution record
|
|
38
|
-
execution = create_workflow_execution(started_at)
|
|
39
|
-
@execution_id = execution&.id
|
|
40
|
-
@root_execution_id = execution&.id
|
|
41
|
-
|
|
42
|
-
begin
|
|
43
|
-
result = if self.class.timeout
|
|
44
|
-
Timeout.timeout(self.class.timeout) { yield }
|
|
45
|
-
else
|
|
46
|
-
yield
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
complete_workflow_execution(
|
|
50
|
-
execution,
|
|
51
|
-
completed_at: Time.current,
|
|
52
|
-
status: result.status,
|
|
53
|
-
result: result
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
result
|
|
57
|
-
rescue Timeout::Error => e
|
|
58
|
-
complete_workflow_execution(
|
|
59
|
-
execution,
|
|
60
|
-
completed_at: Time.current,
|
|
61
|
-
status: "timeout",
|
|
62
|
-
error: e
|
|
63
|
-
)
|
|
64
|
-
raise
|
|
65
|
-
rescue WorkflowCostExceededError => e
|
|
66
|
-
complete_workflow_execution(
|
|
67
|
-
execution,
|
|
68
|
-
completed_at: Time.current,
|
|
69
|
-
status: "error",
|
|
70
|
-
error: e
|
|
71
|
-
)
|
|
72
|
-
raise
|
|
73
|
-
rescue StandardError => e
|
|
74
|
-
complete_workflow_execution(
|
|
75
|
-
execution,
|
|
76
|
-
completed_at: Time.current,
|
|
77
|
-
status: "error",
|
|
78
|
-
error: e
|
|
79
|
-
)
|
|
80
|
-
raise
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
# Creates the initial workflow execution record
|
|
87
|
-
#
|
|
88
|
-
# @param started_at [Time] When the workflow started
|
|
89
|
-
# @return [RubyLLM::Agents::Execution, nil] The created record
|
|
90
|
-
def create_workflow_execution(started_at)
|
|
91
|
-
RubyLLM::Agents::Execution.create!(
|
|
92
|
-
agent_type: self.class.name,
|
|
93
|
-
agent_version: self.class.version,
|
|
94
|
-
model_id: "workflow",
|
|
95
|
-
temperature: nil,
|
|
96
|
-
started_at: started_at,
|
|
97
|
-
status: "running",
|
|
98
|
-
parameters: Redactor.redact(options),
|
|
99
|
-
metadata: workflow_metadata,
|
|
100
|
-
workflow_id: workflow_id,
|
|
101
|
-
workflow_type: workflow_type_name
|
|
102
|
-
)
|
|
103
|
-
rescue StandardError => e
|
|
104
|
-
Rails.logger.error("[RubyLLM::Agents::Workflow] Failed to create workflow execution: #{e.message}")
|
|
105
|
-
nil
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Updates the workflow execution record with completion data
|
|
109
|
-
#
|
|
110
|
-
# @param execution [Execution, nil] The execution record
|
|
111
|
-
# @param completed_at [Time] When the workflow completed
|
|
112
|
-
# @param status [String] Final status
|
|
113
|
-
# @param result [WorkflowResult, nil] The workflow result
|
|
114
|
-
# @param error [Exception, nil] The error if failed
|
|
115
|
-
def complete_workflow_execution(execution, completed_at:, status:, result: nil, error: nil)
|
|
116
|
-
return unless execution
|
|
117
|
-
|
|
118
|
-
started_at = execution.started_at
|
|
119
|
-
duration_ms = ((completed_at - started_at) * 1000).round
|
|
120
|
-
|
|
121
|
-
update_data = {
|
|
122
|
-
completed_at: completed_at,
|
|
123
|
-
duration_ms: duration_ms,
|
|
124
|
-
status: status
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
# Add aggregate metrics from result
|
|
128
|
-
if result
|
|
129
|
-
update_data.merge!(
|
|
130
|
-
input_tokens: result.input_tokens,
|
|
131
|
-
output_tokens: result.output_tokens,
|
|
132
|
-
total_tokens: result.total_tokens,
|
|
133
|
-
cached_tokens: result.cached_tokens,
|
|
134
|
-
input_cost: result.input_cost,
|
|
135
|
-
output_cost: result.output_cost,
|
|
136
|
-
total_cost: result.total_cost
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
# Store step/branch results summary
|
|
140
|
-
update_data[:response] = build_response_summary(result)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Add error data if failed
|
|
144
|
-
if error
|
|
145
|
-
update_data.merge!(
|
|
146
|
-
error_class: error.class.name,
|
|
147
|
-
error_message: error.message.to_s.truncate(65535)
|
|
148
|
-
)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
execution.update!(update_data)
|
|
152
|
-
rescue StandardError => e
|
|
153
|
-
Rails.logger.error("[RubyLLM::Agents::Workflow] Failed to update workflow execution #{execution&.id}: #{e.message}")
|
|
154
|
-
mark_workflow_failed!(execution, error: error || e)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Emergency fallback to mark workflow as failed
|
|
158
|
-
#
|
|
159
|
-
# @param execution [Execution, nil] The execution record
|
|
160
|
-
# @param error [Exception, nil] The error
|
|
161
|
-
def mark_workflow_failed!(execution, error: nil)
|
|
162
|
-
return unless execution&.id
|
|
163
|
-
|
|
164
|
-
update_data = {
|
|
165
|
-
status: "error",
|
|
166
|
-
completed_at: Time.current,
|
|
167
|
-
error_class: error&.class&.name || "UnknownError",
|
|
168
|
-
error_message: error&.message&.to_s&.truncate(65535) || "Unknown error"
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
execution.class.where(id: execution.id, status: "running").update_all(update_data)
|
|
172
|
-
rescue StandardError => e
|
|
173
|
-
Rails.logger.error("[RubyLLM::Agents::Workflow] CRITICAL: Failed to mark workflow #{execution&.id} as failed: #{e.message}")
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Builds a summary of step/branch results for storage
|
|
177
|
-
#
|
|
178
|
-
# @param result [WorkflowResult] The workflow result
|
|
179
|
-
# @return [Hash] Summary data
|
|
180
|
-
def build_response_summary(result)
|
|
181
|
-
summary = {
|
|
182
|
-
workflow_type: result.workflow_type,
|
|
183
|
-
status: result.status
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if result.steps.any?
|
|
187
|
-
summary[:steps] = result.steps.transform_values do |r|
|
|
188
|
-
{
|
|
189
|
-
status: r.respond_to?(:success?) ? (r.success? ? "success" : "error") : "unknown",
|
|
190
|
-
total_cost: r.respond_to?(:total_cost) ? r.total_cost : 0,
|
|
191
|
-
duration_ms: r.respond_to?(:duration_ms) ? r.duration_ms : nil
|
|
192
|
-
}
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
if result.branches.any?
|
|
197
|
-
summary[:branches] = result.branches.transform_values do |r|
|
|
198
|
-
next { status: "error" } if r.nil?
|
|
199
|
-
|
|
200
|
-
{
|
|
201
|
-
status: r.respond_to?(:success?) ? (r.success? ? "success" : "error") : "unknown",
|
|
202
|
-
total_cost: r.respond_to?(:total_cost) ? r.total_cost : 0,
|
|
203
|
-
duration_ms: r.respond_to?(:duration_ms) ? r.duration_ms : nil
|
|
204
|
-
}
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
if result.routed_to
|
|
209
|
-
summary[:routed_to] = result.routed_to
|
|
210
|
-
summary[:classification_cost] = result.classification_cost
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
summary
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Returns workflow-specific metadata
|
|
217
|
-
#
|
|
218
|
-
# @return [Hash] Workflow metadata
|
|
219
|
-
def workflow_metadata
|
|
220
|
-
base_metadata = {
|
|
221
|
-
workflow_id: workflow_id,
|
|
222
|
-
workflow_type: workflow_type_name
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
# Allow subclasses to add custom metadata
|
|
226
|
-
if respond_to?(:execution_metadata, true)
|
|
227
|
-
base_metadata.merge(execution_metadata)
|
|
228
|
-
else
|
|
229
|
-
base_metadata
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Returns the workflow type name for storage
|
|
234
|
-
#
|
|
235
|
-
# @return [String] The workflow type
|
|
236
|
-
def workflow_type_name
|
|
237
|
-
"workflow"
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Hook for subclasses to add custom metadata
|
|
241
|
-
#
|
|
242
|
-
# @return [Hash] Custom metadata
|
|
243
|
-
def execution_metadata
|
|
244
|
-
{}
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
end
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Workflow
|
|
6
|
-
module Notifiers
|
|
7
|
-
# Base class for approval notification adapters
|
|
8
|
-
#
|
|
9
|
-
# Subclasses should implement the #notify and optionally #remind methods
|
|
10
|
-
# to send notifications through their respective channels.
|
|
11
|
-
#
|
|
12
|
-
# @example Creating a custom notifier
|
|
13
|
-
# class SmsNotifier < Base
|
|
14
|
-
# def notify(approval, message)
|
|
15
|
-
# # Send SMS via Twilio
|
|
16
|
-
# end
|
|
17
|
-
# end
|
|
18
|
-
#
|
|
19
|
-
# @api public
|
|
20
|
-
class Base
|
|
21
|
-
# Send a notification for an approval request
|
|
22
|
-
#
|
|
23
|
-
# @param approval [Approval] The approval request
|
|
24
|
-
# @param message [String] The notification message
|
|
25
|
-
# @return [Boolean] true if notification was sent successfully
|
|
26
|
-
def notify(approval, message)
|
|
27
|
-
raise NotImplementedError, "#{self.class}#notify must be implemented"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Send a reminder for a pending approval
|
|
31
|
-
#
|
|
32
|
-
# @param approval [Approval] The approval request
|
|
33
|
-
# @param message [String] The reminder message
|
|
34
|
-
# @return [Boolean] true if reminder was sent successfully
|
|
35
|
-
def remind(approval, message)
|
|
36
|
-
notify(approval, "[Reminder] #{message}")
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Send an escalation notice
|
|
40
|
-
#
|
|
41
|
-
# @param approval [Approval] The approval request
|
|
42
|
-
# @param message [String] The escalation message
|
|
43
|
-
# @param escalate_to [String] The escalation target
|
|
44
|
-
# @return [Boolean] true if escalation was sent successfully
|
|
45
|
-
def escalate(approval, message, escalate_to:)
|
|
46
|
-
notify(approval, "[Escalation to #{escalate_to}] #{message}")
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Registry for notifier instances
|
|
51
|
-
#
|
|
52
|
-
# @api private
|
|
53
|
-
class Registry
|
|
54
|
-
class << self
|
|
55
|
-
# Returns the registered notifiers
|
|
56
|
-
#
|
|
57
|
-
# @return [Hash<Symbol, Base>]
|
|
58
|
-
def notifiers
|
|
59
|
-
@notifiers ||= {}
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Register a notifier
|
|
63
|
-
#
|
|
64
|
-
# @param name [Symbol] The notifier name
|
|
65
|
-
# @param notifier [Base] The notifier instance
|
|
66
|
-
# @return [void]
|
|
67
|
-
def register(name, notifier)
|
|
68
|
-
notifiers[name.to_sym] = notifier
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Get a registered notifier
|
|
72
|
-
#
|
|
73
|
-
# @param name [Symbol] The notifier name
|
|
74
|
-
# @return [Base, nil]
|
|
75
|
-
def get(name)
|
|
76
|
-
notifiers[name.to_sym]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Check if a notifier is registered
|
|
80
|
-
#
|
|
81
|
-
# @param name [Symbol] The notifier name
|
|
82
|
-
# @return [Boolean]
|
|
83
|
-
def registered?(name)
|
|
84
|
-
notifiers.key?(name.to_sym)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Send notification through specified channels
|
|
88
|
-
#
|
|
89
|
-
# @param approval [Approval] The approval request
|
|
90
|
-
# @param message [String] The notification message
|
|
91
|
-
# @param channels [Array<Symbol>] The notification channels
|
|
92
|
-
# @return [Hash<Symbol, Boolean>] Results per channel
|
|
93
|
-
def notify_all(approval, message, channels:)
|
|
94
|
-
results = {}
|
|
95
|
-
channels.each do |channel|
|
|
96
|
-
notifier = get(channel)
|
|
97
|
-
if notifier
|
|
98
|
-
results[channel] = notifier.notify(approval, message)
|
|
99
|
-
else
|
|
100
|
-
results[channel] = false
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
results
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Reset the registry (useful for testing)
|
|
107
|
-
#
|
|
108
|
-
# @return [void]
|
|
109
|
-
def reset!
|
|
110
|
-
@notifiers = {}
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Workflow
|
|
6
|
-
module Notifiers
|
|
7
|
-
# Email notification adapter for approval requests
|
|
8
|
-
#
|
|
9
|
-
# Uses ActionMailer if available, or a configured mailer class.
|
|
10
|
-
# Can be configured with custom templates and delivery options.
|
|
11
|
-
#
|
|
12
|
-
# @example Configuration
|
|
13
|
-
# RubyLLM::Agents::Workflow::Notifiers::Email.configure do |config|
|
|
14
|
-
# config.mailer_class = ApprovalMailer
|
|
15
|
-
# config.from = "approvals@example.com"
|
|
16
|
-
# end
|
|
17
|
-
#
|
|
18
|
-
# @api public
|
|
19
|
-
class Email < Base
|
|
20
|
-
class << self
|
|
21
|
-
attr_accessor :mailer_class, :from_address, :subject_prefix
|
|
22
|
-
|
|
23
|
-
# Configure the email notifier
|
|
24
|
-
#
|
|
25
|
-
# @yield [self] The email notifier class
|
|
26
|
-
# @return [void]
|
|
27
|
-
def configure
|
|
28
|
-
yield self
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Reset configuration to defaults
|
|
32
|
-
#
|
|
33
|
-
# @return [void]
|
|
34
|
-
def reset!
|
|
35
|
-
@mailer_class = nil
|
|
36
|
-
@from_address = nil
|
|
37
|
-
@subject_prefix = nil
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# @param mailer_class [Class, nil] Custom mailer class
|
|
42
|
-
# @param from [String, nil] From address
|
|
43
|
-
# @param subject_prefix [String, nil] Subject line prefix
|
|
44
|
-
def initialize(mailer_class: nil, from: nil, subject_prefix: nil)
|
|
45
|
-
@mailer_class = mailer_class || self.class.mailer_class
|
|
46
|
-
@from_address = from || self.class.from_address || "noreply@example.com"
|
|
47
|
-
@subject_prefix = subject_prefix || self.class.subject_prefix || "[Approval Required]"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Send an email notification
|
|
51
|
-
#
|
|
52
|
-
# @param approval [Approval] The approval request
|
|
53
|
-
# @param message [String] The notification message
|
|
54
|
-
# @return [Boolean] true if email was queued
|
|
55
|
-
def notify(approval, message)
|
|
56
|
-
if @mailer_class
|
|
57
|
-
send_via_mailer(approval, message)
|
|
58
|
-
elsif defined?(ActionMailer)
|
|
59
|
-
send_via_action_mailer(approval, message)
|
|
60
|
-
else
|
|
61
|
-
log_notification(approval, message)
|
|
62
|
-
false
|
|
63
|
-
end
|
|
64
|
-
rescue StandardError => e
|
|
65
|
-
handle_error(e, approval)
|
|
66
|
-
false
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
def send_via_mailer(approval, message)
|
|
72
|
-
if @mailer_class.respond_to?(:approval_request)
|
|
73
|
-
mail = @mailer_class.approval_request(approval, message)
|
|
74
|
-
deliver_mail(mail)
|
|
75
|
-
true
|
|
76
|
-
else
|
|
77
|
-
false
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def send_via_action_mailer(approval, message)
|
|
82
|
-
# Generic ActionMailer support if no custom mailer is configured
|
|
83
|
-
# Applications should configure a mailer_class for production use
|
|
84
|
-
log_notification(approval, message)
|
|
85
|
-
false
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def deliver_mail(mail)
|
|
89
|
-
if mail.respond_to?(:deliver_later)
|
|
90
|
-
mail.deliver_later
|
|
91
|
-
elsif mail.respond_to?(:deliver_now)
|
|
92
|
-
mail.deliver_now
|
|
93
|
-
elsif mail.respond_to?(:deliver)
|
|
94
|
-
mail.deliver
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def log_notification(approval, message)
|
|
99
|
-
if defined?(Rails) && Rails.logger
|
|
100
|
-
Rails.logger.info(
|
|
101
|
-
"[RubyLLM::Agents] Email notification for approval #{approval.id}: #{message}"
|
|
102
|
-
)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def handle_error(error, approval)
|
|
107
|
-
if defined?(Rails) && Rails.logger
|
|
108
|
-
Rails.logger.error(
|
|
109
|
-
"[RubyLLM::Agents] Failed to send email for approval #{approval.id}: #{error.message}"
|
|
110
|
-
)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "net/http"
|
|
4
|
-
require "uri"
|
|
5
|
-
require "json"
|
|
6
|
-
|
|
7
|
-
module RubyLLM
|
|
8
|
-
module Agents
|
|
9
|
-
class Workflow
|
|
10
|
-
module Notifiers
|
|
11
|
-
# Slack notification adapter for approval requests
|
|
12
|
-
#
|
|
13
|
-
# Sends notifications via Slack webhooks or the Slack API.
|
|
14
|
-
# Supports rich message formatting with blocks.
|
|
15
|
-
#
|
|
16
|
-
# @example Using a webhook
|
|
17
|
-
# notifier = Slack.new(webhook_url: "https://hooks.slack.com/...")
|
|
18
|
-
# notifier.notify(approval, "Please review this request")
|
|
19
|
-
#
|
|
20
|
-
# @example Using the API
|
|
21
|
-
# notifier = Slack.new(api_token: "xoxb-...", channel: "#approvals")
|
|
22
|
-
#
|
|
23
|
-
# @api public
|
|
24
|
-
class Slack < Base
|
|
25
|
-
class << self
|
|
26
|
-
attr_accessor :webhook_url, :api_token, :default_channel
|
|
27
|
-
|
|
28
|
-
# Configure the Slack notifier
|
|
29
|
-
#
|
|
30
|
-
# @yield [self] The Slack notifier class
|
|
31
|
-
# @return [void]
|
|
32
|
-
def configure
|
|
33
|
-
yield self
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Reset configuration to defaults
|
|
37
|
-
#
|
|
38
|
-
# @return [void]
|
|
39
|
-
def reset!
|
|
40
|
-
@webhook_url = nil
|
|
41
|
-
@api_token = nil
|
|
42
|
-
@default_channel = nil
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# @param webhook_url [String, nil] Slack webhook URL
|
|
47
|
-
# @param api_token [String, nil] Slack API token (for posting via API)
|
|
48
|
-
# @param channel [String, nil] Default channel for messages
|
|
49
|
-
def initialize(webhook_url: nil, api_token: nil, channel: nil)
|
|
50
|
-
@webhook_url = webhook_url || self.class.webhook_url
|
|
51
|
-
@api_token = api_token || self.class.api_token
|
|
52
|
-
@channel = channel || self.class.default_channel
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Send a Slack notification
|
|
56
|
-
#
|
|
57
|
-
# @param approval [Approval] The approval request
|
|
58
|
-
# @param message [String] The notification message
|
|
59
|
-
# @return [Boolean] true if notification was sent
|
|
60
|
-
def notify(approval, message)
|
|
61
|
-
payload = build_payload(approval, message)
|
|
62
|
-
|
|
63
|
-
if @webhook_url
|
|
64
|
-
send_webhook(payload)
|
|
65
|
-
elsif @api_token
|
|
66
|
-
send_api(payload)
|
|
67
|
-
else
|
|
68
|
-
log_notification(approval, message)
|
|
69
|
-
false
|
|
70
|
-
end
|
|
71
|
-
rescue StandardError => e
|
|
72
|
-
handle_error(e, approval)
|
|
73
|
-
false
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
def build_payload(approval, message)
|
|
79
|
-
{
|
|
80
|
-
text: message,
|
|
81
|
-
blocks: build_blocks(approval, message)
|
|
82
|
-
}.tap do |payload|
|
|
83
|
-
payload[:channel] = @channel if @channel && @api_token
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def build_blocks(approval, message)
|
|
88
|
-
[
|
|
89
|
-
{
|
|
90
|
-
type: "header",
|
|
91
|
-
text: {
|
|
92
|
-
type: "plain_text",
|
|
93
|
-
text: "Approval Required: #{approval.name}",
|
|
94
|
-
emoji: true
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
type: "section",
|
|
99
|
-
text: {
|
|
100
|
-
type: "mrkdwn",
|
|
101
|
-
text: message
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
type: "section",
|
|
106
|
-
fields: [
|
|
107
|
-
{
|
|
108
|
-
type: "mrkdwn",
|
|
109
|
-
text: "*Workflow:*\n#{approval.workflow_type}"
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
type: "mrkdwn",
|
|
113
|
-
text: "*Workflow ID:*\n#{approval.workflow_id}"
|
|
114
|
-
}
|
|
115
|
-
]
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
type: "context",
|
|
119
|
-
elements: [
|
|
120
|
-
{
|
|
121
|
-
type: "mrkdwn",
|
|
122
|
-
text: "Approval ID: `#{approval.id}`"
|
|
123
|
-
}
|
|
124
|
-
]
|
|
125
|
-
}
|
|
126
|
-
]
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def send_webhook(payload)
|
|
130
|
-
uri = URI.parse(@webhook_url)
|
|
131
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
132
|
-
http.use_ssl = uri.scheme == "https"
|
|
133
|
-
http.open_timeout = 5
|
|
134
|
-
http.read_timeout = 10
|
|
135
|
-
|
|
136
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
137
|
-
request["Content-Type"] = "application/json"
|
|
138
|
-
request.body = payload.to_json
|
|
139
|
-
|
|
140
|
-
response = http.request(request)
|
|
141
|
-
response.code.to_i == 200
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def send_api(payload)
|
|
145
|
-
uri = URI.parse("https://slack.com/api/chat.postMessage")
|
|
146
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
147
|
-
http.use_ssl = true
|
|
148
|
-
http.open_timeout = 5
|
|
149
|
-
http.read_timeout = 10
|
|
150
|
-
|
|
151
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
152
|
-
request["Content-Type"] = "application/json"
|
|
153
|
-
request["Authorization"] = "Bearer #{@api_token}"
|
|
154
|
-
request.body = payload.to_json
|
|
155
|
-
|
|
156
|
-
response = http.request(request)
|
|
157
|
-
result = JSON.parse(response.body)
|
|
158
|
-
result["ok"] == true
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def log_notification(approval, message)
|
|
162
|
-
if defined?(Rails) && Rails.logger
|
|
163
|
-
Rails.logger.info(
|
|
164
|
-
"[RubyLLM::Agents] Slack notification for approval #{approval.id}: #{message}"
|
|
165
|
-
)
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def handle_error(error, approval)
|
|
170
|
-
if defined?(Rails) && Rails.logger
|
|
171
|
-
Rails.logger.error(
|
|
172
|
-
"[RubyLLM::Agents] Failed to send Slack message for approval #{approval.id}: #{error.message}"
|
|
173
|
-
)
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|