ruby_llm-agents 0.3.4 → 0.3.5
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 +132 -1263
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +80 -16
- data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
- data/app/models/ruby_llm/agents/execution/analytics.rb +17 -27
- data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
- data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
- data/app/models/ruby_llm/agents/execution.rb +9 -1
- data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
- data/app/views/layouts/{rubyllm → ruby_llm}/agents/application.html.erb +91 -29
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
- data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
- data/app/views/{rubyllm → ruby_llm}/agents/agents/show.html.erb +77 -20
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/index.html.erb +9 -6
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
- data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
- data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +137 -126
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_breadcrumbs.html.erb +2 -2
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
- data/lib/ruby_llm/agents/alert_manager.rb +20 -16
- data/lib/ruby_llm/agents/base/caching.rb +4 -7
- data/lib/ruby_llm/agents/base/cost_calculation.rb +5 -3
- data/lib/ruby_llm/agents/base/execution.rb +61 -9
- data/lib/ruby_llm/agents/base/reliability_execution.rb +14 -9
- data/lib/ruby_llm/agents/base.rb +26 -0
- data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
- data/lib/ruby_llm/agents/cache_helper.rb +98 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
- data/lib/ruby_llm/agents/configuration.rb +40 -1
- data/lib/ruby_llm/agents/engine.rb +65 -1
- data/lib/ruby_llm/agents/inflections.rb +14 -0
- data/lib/ruby_llm/agents/instrumentation.rb +66 -0
- data/lib/ruby_llm/agents/reliability.rb +8 -2
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
- data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
- data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
- data/lib/ruby_llm/agents/workflow/result.rb +390 -0
- data/lib/ruby_llm/agents/workflow/router.rb +429 -0
- data/lib/ruby_llm/agents/workflow.rb +232 -0
- data/lib/ruby_llm/agents.rb +1 -0
- metadata +50 -60
- data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
- data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +0 -46
- data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_budgets_bar.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_now_strip.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_top_errors.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_nav_link.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
# Sequential workflow execution pattern
|
|
7
|
+
#
|
|
8
|
+
# Executes agents in order, passing each step's output as input to the next.
|
|
9
|
+
# Supports conditional step skipping, error handling, and input transformation.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic pipeline
|
|
12
|
+
# class ContentPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
13
|
+
# version "1.0"
|
|
14
|
+
#
|
|
15
|
+
# step :extract, agent: ExtractorAgent
|
|
16
|
+
# step :validate, agent: ValidatorAgent
|
|
17
|
+
# step :format, agent: FormatterAgent
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# result = ContentPipeline.call(text: "raw input")
|
|
21
|
+
# result.steps[:extract].content # First step output
|
|
22
|
+
# result.content # Final output
|
|
23
|
+
#
|
|
24
|
+
# @example With conditional skipping
|
|
25
|
+
# class ConditionalPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
26
|
+
# step :check, agent: CheckerAgent
|
|
27
|
+
# step :process, agent: ProcessorAgent, skip_on: ->(ctx) { ctx[:check].content[:skip] }
|
|
28
|
+
# step :finalize, agent: FinalizerAgent
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# @example With input transformation
|
|
32
|
+
# class TransformPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
33
|
+
# step :analyze, agent: AnalyzerAgent
|
|
34
|
+
# step :enrich, agent: EnricherAgent
|
|
35
|
+
#
|
|
36
|
+
# def before_enrich(context)
|
|
37
|
+
# { data: context[:analyze].content, extra_field: "value" }
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# @api public
|
|
42
|
+
class Pipeline < Workflow
|
|
43
|
+
# Simple error result for failed steps
|
|
44
|
+
# @api private
|
|
45
|
+
class ErrorResult
|
|
46
|
+
attr_reader :error_class, :error_message, :step_name
|
|
47
|
+
|
|
48
|
+
def initialize(step_name:, error_class:, error_message:)
|
|
49
|
+
@step_name = step_name
|
|
50
|
+
@error_class = error_class
|
|
51
|
+
@error_message = error_message
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def content = nil
|
|
55
|
+
def success? = false
|
|
56
|
+
def error? = true
|
|
57
|
+
def input_tokens = 0
|
|
58
|
+
def output_tokens = 0
|
|
59
|
+
def total_tokens = 0
|
|
60
|
+
def cached_tokens = 0
|
|
61
|
+
def input_cost = 0.0
|
|
62
|
+
def output_cost = 0.0
|
|
63
|
+
def total_cost = 0.0
|
|
64
|
+
|
|
65
|
+
def to_h
|
|
66
|
+
{
|
|
67
|
+
error: true,
|
|
68
|
+
step_name: step_name,
|
|
69
|
+
error_class: error_class,
|
|
70
|
+
error_message: error_message
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
# Returns the defined steps
|
|
77
|
+
#
|
|
78
|
+
# @return [Hash<Symbol, Hash>] Step configurations
|
|
79
|
+
def steps
|
|
80
|
+
@steps ||= {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Inherits steps from parent class
|
|
84
|
+
def inherited(subclass)
|
|
85
|
+
super
|
|
86
|
+
subclass.instance_variable_set(:@steps, steps.dup)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Defines a pipeline step
|
|
90
|
+
#
|
|
91
|
+
# @param name [Symbol] Step identifier
|
|
92
|
+
# @param agent [Class] The agent class to execute
|
|
93
|
+
# @param skip_on [Proc, nil] Lambda to determine if step should be skipped
|
|
94
|
+
# @param continue_on_error [Boolean] Whether to continue if step fails
|
|
95
|
+
# @param optional [Boolean] Mark step as optional (alias for continue_on_error)
|
|
96
|
+
# @return [void]
|
|
97
|
+
#
|
|
98
|
+
# @example Basic step
|
|
99
|
+
# step :process, agent: ProcessorAgent
|
|
100
|
+
#
|
|
101
|
+
# @example With skip condition
|
|
102
|
+
# step :validate, agent: ValidatorAgent, skip_on: ->(ctx) { ctx[:input][:skip_validation] }
|
|
103
|
+
#
|
|
104
|
+
# @example Optional step (won't fail pipeline)
|
|
105
|
+
# step :enrich, agent: EnricherAgent, optional: true
|
|
106
|
+
def step(name, agent:, skip_on: nil, continue_on_error: false, optional: false)
|
|
107
|
+
steps[name] = {
|
|
108
|
+
agent: agent,
|
|
109
|
+
skip_on: skip_on,
|
|
110
|
+
continue_on_error: continue_on_error || optional,
|
|
111
|
+
index: steps.size
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Executes the pipeline
|
|
117
|
+
#
|
|
118
|
+
# Runs each step sequentially, passing output to the next step.
|
|
119
|
+
# Tracks all step results and builds aggregate metrics.
|
|
120
|
+
#
|
|
121
|
+
# @yield [chunk] Yields chunks when streaming (passed to individual agents)
|
|
122
|
+
# @return [WorkflowResult] The pipeline result
|
|
123
|
+
def call(&block)
|
|
124
|
+
instrument_workflow do
|
|
125
|
+
execute_pipeline(&block)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Executes the pipeline steps
|
|
132
|
+
#
|
|
133
|
+
# @return [WorkflowResult] The pipeline result
|
|
134
|
+
def execute_pipeline(&block)
|
|
135
|
+
context = { input: options }
|
|
136
|
+
step_results = {}
|
|
137
|
+
errors = {}
|
|
138
|
+
last_successful_result = nil
|
|
139
|
+
status = "success"
|
|
140
|
+
|
|
141
|
+
self.class.steps.each do |name, config|
|
|
142
|
+
# Check skip condition
|
|
143
|
+
if should_skip_step?(config, context)
|
|
144
|
+
step_results[name] = SkippedResult.new(name, reason: "skip_on condition met")
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
# Build input for this step
|
|
150
|
+
step_input = before_step(name, context)
|
|
151
|
+
|
|
152
|
+
# Execute the step
|
|
153
|
+
result = execute_agent(
|
|
154
|
+
config[:agent],
|
|
155
|
+
step_input,
|
|
156
|
+
step_name: name,
|
|
157
|
+
&block
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
step_results[name] = result
|
|
161
|
+
context[name] = result
|
|
162
|
+
last_successful_result = result
|
|
163
|
+
|
|
164
|
+
# Check if step failed
|
|
165
|
+
if result.respond_to?(:error?) && result.error?
|
|
166
|
+
errors[name] = StandardError.new(result.error_message || "Step failed")
|
|
167
|
+
|
|
168
|
+
unless config[:continue_on_error]
|
|
169
|
+
status = "error"
|
|
170
|
+
break
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
status = "partial" if status == "success"
|
|
174
|
+
end
|
|
175
|
+
rescue StandardError => e
|
|
176
|
+
# Handle step execution errors
|
|
177
|
+
errors[name] = e
|
|
178
|
+
step_results[name] = build_error_result(name, e)
|
|
179
|
+
context[name] = step_results[name]
|
|
180
|
+
|
|
181
|
+
# Check continue_on_error first - if true, continue without calling handler
|
|
182
|
+
if config[:continue_on_error]
|
|
183
|
+
status = "partial" if status == "success"
|
|
184
|
+
next
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Call error handler hook for non-optional steps
|
|
188
|
+
action = on_step_failure(name, e, context)
|
|
189
|
+
|
|
190
|
+
case action
|
|
191
|
+
when :skip
|
|
192
|
+
status = "partial" if status == "success"
|
|
193
|
+
next
|
|
194
|
+
when :abort
|
|
195
|
+
status = "error"
|
|
196
|
+
break
|
|
197
|
+
when Result, Workflow::Result
|
|
198
|
+
# Use the returned result as the step result
|
|
199
|
+
step_results[name] = action
|
|
200
|
+
context[name] = action
|
|
201
|
+
last_successful_result = action
|
|
202
|
+
status = "partial" if status == "success"
|
|
203
|
+
else
|
|
204
|
+
status = "error"
|
|
205
|
+
break
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Build final content from last successful step
|
|
211
|
+
final_content = extract_final_content(last_successful_result, context)
|
|
212
|
+
|
|
213
|
+
build_pipeline_result(
|
|
214
|
+
content: final_content,
|
|
215
|
+
steps: step_results,
|
|
216
|
+
errors: errors,
|
|
217
|
+
status: status
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Checks if a step should be skipped
|
|
222
|
+
#
|
|
223
|
+
# @param config [Hash] Step configuration
|
|
224
|
+
# @param context [Hash] Current workflow context
|
|
225
|
+
# @return [Boolean] true if step should be skipped
|
|
226
|
+
def should_skip_step?(config, context)
|
|
227
|
+
return false unless config[:skip_on]
|
|
228
|
+
|
|
229
|
+
config[:skip_on].call(context)
|
|
230
|
+
rescue StandardError => e
|
|
231
|
+
Rails.logger.warn("[RubyLLM::Agents::Pipeline] skip_on evaluation failed: #{e.message}")
|
|
232
|
+
false
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Hook for handling step failures
|
|
236
|
+
#
|
|
237
|
+
# Override in subclass to customize error handling.
|
|
238
|
+
#
|
|
239
|
+
# @param step_name [Symbol] The failed step
|
|
240
|
+
# @param error [Exception] The error
|
|
241
|
+
# @param context [Hash] Current workflow context
|
|
242
|
+
# @return [Symbol, Result] :skip to continue, :abort to stop, or a fallback Result
|
|
243
|
+
def on_step_failure(step_name, error, context)
|
|
244
|
+
# Default: check if method exists for specific step
|
|
245
|
+
method_name = :"on_#{step_name}_failure"
|
|
246
|
+
if respond_to?(method_name, true)
|
|
247
|
+
send(method_name, error, context)
|
|
248
|
+
else
|
|
249
|
+
:abort
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Extracts the final content from the pipeline
|
|
254
|
+
#
|
|
255
|
+
# @param last_result [Result, nil] The last successful result
|
|
256
|
+
# @param context [Hash] The workflow context
|
|
257
|
+
# @return [Object] The final content
|
|
258
|
+
def extract_final_content(last_result, context)
|
|
259
|
+
if last_result.respond_to?(:content)
|
|
260
|
+
last_result.content
|
|
261
|
+
elsif context.keys.size > 1
|
|
262
|
+
# Return the last non-input context entry
|
|
263
|
+
last_key = context.keys.reject { |k| k == :input }.last
|
|
264
|
+
context[last_key]&.content
|
|
265
|
+
else
|
|
266
|
+
nil
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Builds an error result for a failed step
|
|
271
|
+
#
|
|
272
|
+
# @param step_name [Symbol] The step name
|
|
273
|
+
# @param error [Exception] The error
|
|
274
|
+
# @return [ErrorResult] Error result object
|
|
275
|
+
def build_error_result(step_name, error)
|
|
276
|
+
ErrorResult.new(
|
|
277
|
+
step_name: step_name,
|
|
278
|
+
error_class: error.class.name,
|
|
279
|
+
error_message: error.message
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Builds the final pipeline result
|
|
284
|
+
#
|
|
285
|
+
# @param content [Object] Final content
|
|
286
|
+
# @param steps [Hash] Step results
|
|
287
|
+
# @param errors [Hash] Step errors
|
|
288
|
+
# @param status [String] Final status
|
|
289
|
+
# @return [WorkflowResult] The pipeline result
|
|
290
|
+
def build_pipeline_result(content:, steps:, errors:, status:)
|
|
291
|
+
Workflow::Result.new(
|
|
292
|
+
content: content,
|
|
293
|
+
workflow_type: self.class.name,
|
|
294
|
+
workflow_id: workflow_id,
|
|
295
|
+
steps: steps,
|
|
296
|
+
errors: errors,
|
|
297
|
+
status: status,
|
|
298
|
+
started_at: @workflow_started_at,
|
|
299
|
+
completed_at: Time.current,
|
|
300
|
+
duration_ms: (((Time.current - @workflow_started_at) * 1000).round if @workflow_started_at)
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
# Result wrapper for workflow executions with aggregate metrics
|
|
7
|
+
#
|
|
8
|
+
# Extends the base Result class with workflow-specific data including
|
|
9
|
+
# step results, branch results, routing information, and aggregated
|
|
10
|
+
# token/cost metrics across all child executions.
|
|
11
|
+
#
|
|
12
|
+
# @example Pipeline result
|
|
13
|
+
# result = ContentPipeline.call(text: "input")
|
|
14
|
+
# result.content # Final output
|
|
15
|
+
# result.steps[:extract] # Individual step result
|
|
16
|
+
# result.total_cost # Sum of all steps
|
|
17
|
+
#
|
|
18
|
+
# @example Parallel result
|
|
19
|
+
# result = ReviewAnalyzer.call(text: "review")
|
|
20
|
+
# result.branches[:sentiment] # Branch result
|
|
21
|
+
# result.failed_branches # [:toxicity] if it failed
|
|
22
|
+
#
|
|
23
|
+
# @example Router result
|
|
24
|
+
# result = SupportRouter.call(message: "billing issue")
|
|
25
|
+
# result.routed_to # :billing
|
|
26
|
+
# result.classification # Classification details
|
|
27
|
+
#
|
|
28
|
+
# @api public
|
|
29
|
+
class Result
|
|
30
|
+
extend ActiveSupport::Delegation
|
|
31
|
+
|
|
32
|
+
# @!attribute [r] content
|
|
33
|
+
# @return [Object] The final processed content
|
|
34
|
+
attr_reader :content
|
|
35
|
+
|
|
36
|
+
# @!attribute [r] workflow_type
|
|
37
|
+
# @return [String] The workflow class name
|
|
38
|
+
attr_reader :workflow_type
|
|
39
|
+
|
|
40
|
+
# @!attribute [r] workflow_id
|
|
41
|
+
# @return [String] Unique identifier for this workflow execution
|
|
42
|
+
attr_reader :workflow_id
|
|
43
|
+
|
|
44
|
+
# @!group Step/Branch Results
|
|
45
|
+
|
|
46
|
+
# @!attribute [r] steps
|
|
47
|
+
# @return [Hash<Symbol, Result>] Results from pipeline steps
|
|
48
|
+
attr_reader :steps
|
|
49
|
+
|
|
50
|
+
# @!attribute [r] branches
|
|
51
|
+
# @return [Hash<Symbol, Result>] Results from parallel branches
|
|
52
|
+
attr_reader :branches
|
|
53
|
+
|
|
54
|
+
# @!endgroup
|
|
55
|
+
|
|
56
|
+
# @!group Router Results
|
|
57
|
+
|
|
58
|
+
# @!attribute [r] routed_to
|
|
59
|
+
# @return [Symbol, nil] The route that was selected
|
|
60
|
+
attr_reader :routed_to
|
|
61
|
+
|
|
62
|
+
# @!attribute [r] classification
|
|
63
|
+
# @return [Hash, nil] Classification details from router
|
|
64
|
+
attr_reader :classification
|
|
65
|
+
|
|
66
|
+
# @!attribute [r] classifier_result
|
|
67
|
+
# @return [Result, nil] The classifier agent's result
|
|
68
|
+
attr_reader :classifier_result
|
|
69
|
+
|
|
70
|
+
# @!endgroup
|
|
71
|
+
|
|
72
|
+
# @!group Timing
|
|
73
|
+
|
|
74
|
+
# @!attribute [r] started_at
|
|
75
|
+
# @return [Time] When the workflow started
|
|
76
|
+
attr_reader :started_at
|
|
77
|
+
|
|
78
|
+
# @!attribute [r] completed_at
|
|
79
|
+
# @return [Time] When the workflow completed
|
|
80
|
+
attr_reader :completed_at
|
|
81
|
+
|
|
82
|
+
# @!attribute [r] duration_ms
|
|
83
|
+
# @return [Integer] Total workflow duration in milliseconds
|
|
84
|
+
attr_reader :duration_ms
|
|
85
|
+
|
|
86
|
+
# @!endgroup
|
|
87
|
+
|
|
88
|
+
# @!group Status
|
|
89
|
+
|
|
90
|
+
# @!attribute [r] status
|
|
91
|
+
# @return [String] Workflow status: "success", "error", "partial"
|
|
92
|
+
attr_reader :status
|
|
93
|
+
|
|
94
|
+
# @!attribute [r] error_class
|
|
95
|
+
# @return [String, nil] Error class if failed
|
|
96
|
+
attr_reader :error_class
|
|
97
|
+
|
|
98
|
+
# @!attribute [r] error_message
|
|
99
|
+
# @return [String, nil] Error message if failed
|
|
100
|
+
attr_reader :error_message
|
|
101
|
+
|
|
102
|
+
# @!attribute [r] errors
|
|
103
|
+
# @return [Hash<Symbol, Exception>] Errors by step/branch name
|
|
104
|
+
attr_reader :errors
|
|
105
|
+
|
|
106
|
+
# @!endgroup
|
|
107
|
+
|
|
108
|
+
# Creates a new WorkflowResult
|
|
109
|
+
#
|
|
110
|
+
# @param content [Object] The final processed content
|
|
111
|
+
# @param options [Hash] Additional result metadata
|
|
112
|
+
def initialize(content:, **options)
|
|
113
|
+
@content = content
|
|
114
|
+
@workflow_type = options[:workflow_type]
|
|
115
|
+
@workflow_id = options[:workflow_id]
|
|
116
|
+
|
|
117
|
+
# Step/branch results
|
|
118
|
+
@steps = options[:steps] || {}
|
|
119
|
+
@branches = options[:branches] || {}
|
|
120
|
+
|
|
121
|
+
# Router results
|
|
122
|
+
@routed_to = options[:routed_to]
|
|
123
|
+
@classification = options[:classification]
|
|
124
|
+
@classifier_result = options[:classifier_result]
|
|
125
|
+
|
|
126
|
+
# Timing
|
|
127
|
+
@started_at = options[:started_at]
|
|
128
|
+
@completed_at = options[:completed_at]
|
|
129
|
+
@duration_ms = options[:duration_ms]
|
|
130
|
+
|
|
131
|
+
# Status
|
|
132
|
+
@status = options[:status] || "success"
|
|
133
|
+
@error_class = options[:error_class]
|
|
134
|
+
@error_message = options[:error_message]
|
|
135
|
+
@errors = options[:errors] || {}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns all child results (steps + branches + classifier)
|
|
139
|
+
#
|
|
140
|
+
# @return [Array<Result>] All child results
|
|
141
|
+
def child_results
|
|
142
|
+
results = []
|
|
143
|
+
results.concat(steps.values) if steps.any?
|
|
144
|
+
results.concat(branches.values) if branches.any?
|
|
145
|
+
results << classifier_result if classifier_result
|
|
146
|
+
results.compact
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @!group Aggregate Metrics
|
|
150
|
+
|
|
151
|
+
# Returns total input tokens across all child executions
|
|
152
|
+
#
|
|
153
|
+
# @return [Integer] Total input tokens
|
|
154
|
+
def input_tokens
|
|
155
|
+
child_results.sum { |r| r.input_tokens || 0 }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Returns total output tokens across all child executions
|
|
159
|
+
#
|
|
160
|
+
# @return [Integer] Total output tokens
|
|
161
|
+
def output_tokens
|
|
162
|
+
child_results.sum { |r| r.output_tokens || 0 }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Returns total tokens across all child executions
|
|
166
|
+
#
|
|
167
|
+
# @return [Integer] Total tokens
|
|
168
|
+
def total_tokens
|
|
169
|
+
input_tokens + output_tokens
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Returns total cached tokens across all child executions
|
|
173
|
+
#
|
|
174
|
+
# @return [Integer] Total cached tokens
|
|
175
|
+
def cached_tokens
|
|
176
|
+
child_results.sum { |r| r.cached_tokens || 0 }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Returns total input cost across all child executions
|
|
180
|
+
#
|
|
181
|
+
# @return [Float] Total input cost in USD
|
|
182
|
+
def input_cost
|
|
183
|
+
child_results.sum { |r| r.input_cost || 0.0 }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Returns total output cost across all child executions
|
|
187
|
+
#
|
|
188
|
+
# @return [Float] Total output cost in USD
|
|
189
|
+
def output_cost
|
|
190
|
+
child_results.sum { |r| r.output_cost || 0.0 }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Returns total cost across all child executions
|
|
194
|
+
#
|
|
195
|
+
# @return [Float] Total cost in USD
|
|
196
|
+
def total_cost
|
|
197
|
+
child_results.sum { |r| r.total_cost || 0.0 }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Returns classification cost (router workflows only)
|
|
201
|
+
#
|
|
202
|
+
# @return [Float] Classification cost in USD
|
|
203
|
+
def classification_cost
|
|
204
|
+
classifier_result&.total_cost || 0.0
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @!endgroup
|
|
208
|
+
|
|
209
|
+
# @!group Status Helpers
|
|
210
|
+
|
|
211
|
+
# Returns whether the workflow succeeded
|
|
212
|
+
#
|
|
213
|
+
# @return [Boolean] true if status is "success"
|
|
214
|
+
def success?
|
|
215
|
+
status == "success"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Returns whether the workflow failed
|
|
219
|
+
#
|
|
220
|
+
# @return [Boolean] true if status is "error"
|
|
221
|
+
def error?
|
|
222
|
+
status == "error"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Returns whether the workflow partially succeeded
|
|
226
|
+
#
|
|
227
|
+
# @return [Boolean] true if status is "partial"
|
|
228
|
+
def partial?
|
|
229
|
+
status == "partial"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# @!endgroup
|
|
233
|
+
|
|
234
|
+
# @!group Pipeline Helpers
|
|
235
|
+
|
|
236
|
+
# Returns whether all pipeline steps succeeded
|
|
237
|
+
#
|
|
238
|
+
# @return [Boolean] true if all steps successful
|
|
239
|
+
def all_steps_successful?
|
|
240
|
+
return true if steps.empty?
|
|
241
|
+
|
|
242
|
+
steps.values.all? { |r| r.respond_to?(:success?) ? r.success? : true }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Returns the names of failed steps
|
|
246
|
+
#
|
|
247
|
+
# @return [Array<Symbol>] Failed step names
|
|
248
|
+
def failed_steps
|
|
249
|
+
steps.select { |_, r| r.respond_to?(:error?) && r.error? }.keys
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Returns the names of skipped steps
|
|
253
|
+
#
|
|
254
|
+
# @return [Array<Symbol>] Skipped step names
|
|
255
|
+
def skipped_steps
|
|
256
|
+
steps.select { |_, r| r.respond_to?(:skipped?) && r.skipped? }.keys
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# @!endgroup
|
|
260
|
+
|
|
261
|
+
# @!group Parallel Helpers
|
|
262
|
+
|
|
263
|
+
# Returns whether all parallel branches succeeded
|
|
264
|
+
#
|
|
265
|
+
# @return [Boolean] true if all branches successful
|
|
266
|
+
def all_branches_successful?
|
|
267
|
+
return true if branches.empty?
|
|
268
|
+
|
|
269
|
+
branches.values.all? { |r| r.nil? || (r.respond_to?(:success?) ? r.success? : true) }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Returns the names of failed branches
|
|
273
|
+
#
|
|
274
|
+
# @return [Array<Symbol>] Failed branch names
|
|
275
|
+
def failed_branches
|
|
276
|
+
failed = branches.select { |_, r| r.respond_to?(:error?) && r.error? }.keys
|
|
277
|
+
failed += errors.keys
|
|
278
|
+
failed.uniq
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Returns the names of successful branches
|
|
282
|
+
#
|
|
283
|
+
# @return [Array<Symbol>] Successful branch names
|
|
284
|
+
def successful_branches
|
|
285
|
+
branches.select { |_, r| r.respond_to?(:success?) && r.success? }.keys
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# @!endgroup
|
|
289
|
+
|
|
290
|
+
# Converts the result to a hash
|
|
291
|
+
#
|
|
292
|
+
# @return [Hash] All result data
|
|
293
|
+
def to_h
|
|
294
|
+
{
|
|
295
|
+
content: content,
|
|
296
|
+
workflow_type: workflow_type,
|
|
297
|
+
workflow_id: workflow_id,
|
|
298
|
+
status: status,
|
|
299
|
+
steps: steps.transform_values { |r| r.respond_to?(:to_h) ? r.to_h : r },
|
|
300
|
+
branches: branches.transform_values { |r| r.respond_to?(:to_h) ? r.to_h : r },
|
|
301
|
+
routed_to: routed_to,
|
|
302
|
+
classification: classification,
|
|
303
|
+
input_tokens: input_tokens,
|
|
304
|
+
output_tokens: output_tokens,
|
|
305
|
+
total_tokens: total_tokens,
|
|
306
|
+
cached_tokens: cached_tokens,
|
|
307
|
+
input_cost: input_cost,
|
|
308
|
+
output_cost: output_cost,
|
|
309
|
+
total_cost: total_cost,
|
|
310
|
+
started_at: started_at,
|
|
311
|
+
completed_at: completed_at,
|
|
312
|
+
duration_ms: duration_ms,
|
|
313
|
+
error_class: error_class,
|
|
314
|
+
error_message: error_message,
|
|
315
|
+
errors: errors.transform_values { |e| { class: e.class.name, message: e.message } }
|
|
316
|
+
}
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Delegate hash methods to content for convenience
|
|
320
|
+
delegate :[], :dig, :keys, :values, :each, :map, to: :content, allow_nil: true
|
|
321
|
+
|
|
322
|
+
# Custom to_json that includes workflow metadata
|
|
323
|
+
#
|
|
324
|
+
# @param args [Array] Arguments passed to to_json
|
|
325
|
+
# @return [String] JSON representation
|
|
326
|
+
def to_json(*args)
|
|
327
|
+
to_h.to_json(*args)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Represents a skipped step result
|
|
332
|
+
class SkippedResult
|
|
333
|
+
attr_reader :step_name, :reason
|
|
334
|
+
|
|
335
|
+
def initialize(step_name, reason: nil)
|
|
336
|
+
@step_name = step_name
|
|
337
|
+
@reason = reason
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def content
|
|
341
|
+
nil
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def success?
|
|
345
|
+
true
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def error?
|
|
349
|
+
false
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def skipped?
|
|
353
|
+
true
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def input_tokens
|
|
357
|
+
0
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def output_tokens
|
|
361
|
+
0
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def total_tokens
|
|
365
|
+
0
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def cached_tokens
|
|
369
|
+
0
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def input_cost
|
|
373
|
+
0.0
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def output_cost
|
|
377
|
+
0.0
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def total_cost
|
|
381
|
+
0.0
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def to_h
|
|
385
|
+
{ skipped: true, step_name: step_name, reason: reason }
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|