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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
  7. data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
  9. data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
  10. data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
  12. data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
  13. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
  14. data/app/models/ruby_llm/agents/execution.rb +50 -14
  15. data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
  16. data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
  17. data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
  18. data/app/models/ruby_llm/agents/tenant.rb +146 -0
  19. data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
  20. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  21. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  22. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  23. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  24. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  25. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  26. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  27. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  28. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  30. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  31. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  32. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  33. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  34. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  35. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  36. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  37. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  38. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  39. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  40. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  41. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  42. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  43. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  44. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  45. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  46. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  47. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  48. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  49. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  50. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  51. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  52. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  53. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  54. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  55. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  56. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  57. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  58. data/config/routes.rb +1 -1
  59. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  60. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  61. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  62. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  65. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  66. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  67. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  68. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  69. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  70. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  71. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
  72. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  73. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  74. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
  75. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  76. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  77. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  78. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  79. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  80. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  81. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  82. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  83. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  84. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  85. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  86. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  87. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  88. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  89. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  90. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
  91. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
  92. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  93. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  94. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  95. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  96. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  97. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  98. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  99. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  100. data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
  101. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  102. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  103. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  104. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  105. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  106. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  107. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  108. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  109. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  110. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  111. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  112. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  113. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  114. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  115. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  116. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  117. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  118. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
  119. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  120. data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
  121. data/lib/ruby_llm/agents/core/version.rb +1 -1
  122. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  123. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
  124. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  125. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  126. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  127. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  128. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  129. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  130. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  131. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  132. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  133. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  134. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  135. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  136. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  137. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  138. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  139. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  140. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  141. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  142. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  143. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  144. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  145. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  146. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  147. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  148. metadata +43 -6
  149. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  150. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  151. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  152. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -1,429 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- # Conditional routing workflow pattern
7
- #
8
- # Classifies input and routes to the appropriate specialized agent.
9
- # Uses a fast, cheap model for classification by default, then
10
- # delegates to the selected route's agent.
11
- #
12
- # @example Basic router
13
- # class SupportRouter < RubyLLM::Agents::Workflow::Router
14
- # version "1.0"
15
- #
16
- # classifier_model "gpt-4o-mini"
17
- #
18
- # route :billing, to: BillingAgent, description: "Billing, charges, refunds"
19
- # route :technical, to: TechSupportAgent, description: "Bugs, errors, crashes"
20
- # route :sales, to: SalesAgent, description: "Pricing, plans, upgrades"
21
- # route :default, to: GeneralAgent # Fallback
22
- # end
23
- #
24
- # result = SupportRouter.call(message: "I was charged twice")
25
- # result.routed_to # :billing
26
- # result.classification_cost # Cost of classifier
27
- # result.content # BillingAgent's response
28
- #
29
- # @example With custom classification
30
- # class CustomRouter < RubyLLM::Agents::Workflow::Router
31
- # route :fast, to: FastAgent, description: "Quick tasks"
32
- # route :slow, to: SlowAgent, description: "Complex tasks"
33
- #
34
- # def classify(input)
35
- # # Custom classification logic
36
- # input[:message].length > 100 ? :slow : :fast
37
- # end
38
- # end
39
- #
40
- # @example With input transformation
41
- # class TransformRouter < RubyLLM::Agents::Workflow::Router
42
- # route :analyze, to: AnalyzerAgent, description: "Analysis requests"
43
- #
44
- # def before_route(input, chosen_route)
45
- # input.merge(route_context: chosen_route, priority: "high")
46
- # end
47
- # end
48
- #
49
- # @api public
50
- class Router < Workflow
51
- class << self
52
- # Returns the defined routes
53
- #
54
- # @return [Hash<Symbol, Hash>] Route configurations
55
- def routes
56
- @routes ||= {}
57
- end
58
-
59
- # Inherits routes from parent class
60
- def inherited(subclass)
61
- super
62
- subclass.instance_variable_set(:@routes, routes.dup)
63
- subclass.instance_variable_set(:@classifier_model, @classifier_model)
64
- subclass.instance_variable_set(:@classifier_temperature, @classifier_temperature)
65
- end
66
-
67
- # Defines a route
68
- #
69
- # @param name [Symbol] Route identifier
70
- # @param to [Class] The agent class to route to
71
- # @param description [String, nil] Description for LLM classification
72
- # @param match [Proc, nil] Lambda for rule-based matching (bypasses LLM)
73
- # @return [void]
74
- #
75
- # @example Basic route
76
- # route :billing, to: BillingAgent, description: "Billing and payment issues"
77
- #
78
- # @example Default/fallback route
79
- # route :default, to: GeneralAgent
80
- #
81
- # @example With rule-based matching
82
- # route :urgent, to: UrgentAgent, match: ->(input) { input[:priority] == "urgent" }
83
- def route(name, to:, description: nil, match: nil)
84
- routes[name] = {
85
- agent: to,
86
- description: description,
87
- match: match
88
- }
89
- end
90
-
91
- # Sets or returns the classifier model
92
- #
93
- # @param value [String, nil] Model ID
94
- # @return [String] The classifier model
95
- def classifier_model(value = nil)
96
- if value
97
- @classifier_model = value
98
- else
99
- @classifier_model || RubyLLM::Agents.configuration.default_model || "gpt-4o-mini"
100
- end
101
- end
102
-
103
- # Sets or returns the classifier temperature
104
- #
105
- # @param value [Float, nil] Temperature (0.0-2.0)
106
- # @return [Float] The classifier temperature
107
- def classifier_temperature(value = nil)
108
- if value
109
- @classifier_temperature = value
110
- else
111
- @classifier_temperature || 0.0
112
- end
113
- end
114
- end
115
-
116
- # Executes the router workflow
117
- #
118
- # Classifies input and routes to the appropriate agent.
119
- #
120
- # @yield [chunk] Yields chunks when streaming (passed to routed agent)
121
- # @return [WorkflowResult] The router result
122
- def call(&block)
123
- instrument_workflow do
124
- execute_router(&block)
125
- end
126
- end
127
-
128
- # Override to provide custom classification logic
129
- #
130
- # @param input [Hash] The input to classify
131
- # @return [Symbol] The route name to use
132
- def classify(input)
133
- # First, try rule-based matching
134
- rule_match = try_rule_matching(input)
135
- return rule_match if rule_match
136
-
137
- # Fall back to LLM classification
138
- llm_classify(input)
139
- end
140
-
141
- protected
142
-
143
- # Hook to transform input before routing
144
- #
145
- # @param input [Hash] Original input
146
- # @param chosen_route [Symbol] The selected route
147
- # @return [Hash] Transformed input for the routed agent
148
- def before_route(input, chosen_route)
149
- input
150
- end
151
-
152
- private
153
-
154
- # Executes the router logic
155
- #
156
- # @return [WorkflowResult] The router result
157
- def execute_router(&block)
158
- # Classify the input
159
- classification_start = Time.current
160
- chosen_route, classifier_result = perform_classification(options)
161
- classification_time = ((Time.current - classification_start) * 1000).round
162
-
163
- # Validate route exists
164
- route_config = self.class.routes[chosen_route]
165
- unless route_config
166
- # Fall back to default route
167
- chosen_route = :default
168
- route_config = self.class.routes[:default]
169
-
170
- unless route_config
171
- raise RouterError, "Route '#{chosen_route}' not found and no default route defined"
172
- end
173
- end
174
-
175
- # Transform input for the routed agent
176
- routed_input = before_route(options, chosen_route)
177
-
178
- # Execute the routed agent
179
- agent_result = execute_agent(
180
- route_config[:agent],
181
- routed_input,
182
- step_name: chosen_route,
183
- &block
184
- )
185
-
186
- build_router_result(
187
- content: agent_result.content,
188
- routed_to: chosen_route,
189
- routed_result: agent_result,
190
- classifier_result: classifier_result,
191
- classification_time_ms: classification_time
192
- )
193
- rescue RouterError
194
- # Re-raise configuration errors
195
- raise
196
- rescue StandardError => e
197
- build_router_result(
198
- content: nil,
199
- routed_to: nil,
200
- classifier_result: classifier_result,
201
- error: e,
202
- status: "error"
203
- )
204
- end
205
-
206
- # Performs classification and returns route + classifier result
207
- #
208
- # @param input [Hash] Input to classify
209
- # @return [Array<Symbol, Result>] Route name and classifier result
210
- def perform_classification(input)
211
- # Check if subclass overrides classify completely
212
- if self.class.instance_method(:classify).owner != Router
213
- # Custom classify method - no classifier_result
214
- route = classify(input)
215
- [route, nil]
216
- else
217
- classify_with_result(input)
218
- end
219
- end
220
-
221
- # Classifies input and returns both route and result
222
- #
223
- # @param input [Hash] Input to classify
224
- # @return [Array<Symbol, Result>] Route name and classifier result
225
- def classify_with_result(input)
226
- # First, try rule-based matching
227
- rule_match = try_rule_matching(input)
228
- return [rule_match, nil] if rule_match
229
-
230
- # Fall back to LLM classification
231
- result = llm_classify_with_result(input)
232
- route = parse_classification(result.content)
233
- [route, result]
234
- end
235
-
236
- # Tries rule-based matching before LLM classification
237
- #
238
- # @param input [Hash] Input to match
239
- # @return [Symbol, nil] Matched route or nil
240
- def try_rule_matching(input)
241
- self.class.routes.each do |name, config|
242
- next unless config[:match]
243
- next if name == :default
244
-
245
- begin
246
- return name if config[:match].call(input)
247
- rescue StandardError => e
248
- Rails.logger.warn("[RubyLLM::Agents::Router] Match rule for #{name} failed: #{e.message}")
249
- end
250
- end
251
- nil
252
- end
253
-
254
- # Classifies input using LLM (returns only route)
255
- #
256
- # @param input [Hash] Input to classify
257
- # @return [Symbol] The classified route
258
- def llm_classify(input)
259
- result = llm_classify_with_result(input)
260
- parse_classification(result.content)
261
- end
262
-
263
- # Classifies input using LLM and returns full result
264
- #
265
- # @param input [Hash] Input to classify
266
- # @return [Result] The classifier result
267
- def llm_classify_with_result(input)
268
- prompt = build_classifier_prompt(input)
269
-
270
- # Use RubyLLM directly for classification
271
- client = RubyLLM.chat
272
- .with_model(self.class.classifier_model)
273
- .with_temperature(self.class.classifier_temperature)
274
-
275
- response = client.ask(prompt)
276
-
277
- # Build a simple result for tracking
278
- build_classifier_result(response)
279
- end
280
-
281
- # Builds the classification prompt
282
- #
283
- # @param input [Hash] Input to classify
284
- # @return [String] The classification prompt
285
- def build_classifier_prompt(input)
286
- routes_with_descriptions = self.class.routes
287
- .reject { |name, _| name == :default }
288
- .select { |_, config| config[:description] }
289
-
290
- if routes_with_descriptions.empty?
291
- raise RouterError, "No routes with descriptions defined. Add descriptions or override #classify"
292
- end
293
-
294
- routes_desc = routes_with_descriptions.map do |name, config|
295
- "- #{name}: #{config[:description]}"
296
- end.join("\n")
297
-
298
- # Extract the main content to classify
299
- content = extract_classifiable_content(input)
300
-
301
- <<~PROMPT
302
- Classify the following input into exactly one category.
303
-
304
- Categories:
305
- #{routes_desc}
306
-
307
- Input: #{content}
308
-
309
- Respond with ONLY the category name, nothing else. The response must be exactly one of: #{routes_with_descriptions.keys.join(', ')}
310
- PROMPT
311
- end
312
-
313
- # Extracts the main content from input for classification
314
- #
315
- # @param input [Hash] The input
316
- # @return [String] Content to classify
317
- def extract_classifiable_content(input)
318
- # Try common keys
319
- %i[message text content query input prompt].each do |key|
320
- return input[key].to_s if input[key].present?
321
- end
322
-
323
- # Fall back to first string value or serialized input
324
- input.values.find { |v| v.is_a?(String) && v.present? } || input.to_json
325
- end
326
-
327
- # Parses the LLM classification response
328
- #
329
- # @param content [String] The LLM response
330
- # @return [Symbol] The parsed route name
331
- def parse_classification(content)
332
- return :default unless content
333
-
334
- # Clean up response
335
- cleaned = content.to_s.strip.downcase.gsub(/[^a-z0-9_]/, "")
336
-
337
- # Find matching route
338
- self.class.routes.keys.find { |name| name.to_s == cleaned } || :default
339
- end
340
-
341
- # Builds a result object for the classifier
342
- #
343
- # @param response [RubyLLM::Message] The LLM response
344
- # @return [Result] Simple result for tracking
345
- def build_classifier_result(response)
346
- RubyLLM::Agents::Result.new(
347
- content: response.content,
348
- input_tokens: response.input_tokens,
349
- output_tokens: response.output_tokens,
350
- total_cost: calculate_classifier_cost(response),
351
- model_id: self.class.classifier_model,
352
- temperature: self.class.classifier_temperature
353
- )
354
- end
355
-
356
- # Calculates classifier cost
357
- #
358
- # @param response [RubyLLM::Message] The LLM response
359
- # @return [Float] Cost in USD
360
- def calculate_classifier_cost(response)
361
- model_info, _provider = RubyLLM::Models.resolve(self.class.classifier_model)
362
- return 0.0 unless model_info&.pricing
363
-
364
- input_price = model_info.pricing.text_tokens&.input || 0
365
- output_price = model_info.pricing.text_tokens&.output || 0
366
-
367
- input_cost = ((response.input_tokens || 0) / 1_000_000.0) * input_price
368
- output_cost = ((response.output_tokens || 0) / 1_000_000.0) * output_price
369
-
370
- input_cost + output_cost
371
- rescue StandardError
372
- 0.0
373
- end
374
-
375
- # Builds the final router result
376
- #
377
- # @param content [Object] Final content
378
- # @param routed_to [Symbol] The selected route
379
- # @param routed_result [Result, nil] The routed agent's result
380
- # @param classifier_result [Result, nil] The classifier result
381
- # @param classification_time_ms [Integer, nil] Classification time
382
- # @param error [Exception, nil] Error if failed
383
- # @param status [String] Final status
384
- # @return [WorkflowResult] The router result
385
- def build_router_result(content:, routed_to:, routed_result: nil, classifier_result: nil,
386
- classification_time_ms: nil, error: nil, status: nil)
387
- # Build branches hash with routed result
388
- branches = {}
389
- branches[routed_to] = routed_result if routed_to && routed_result
390
-
391
- # Build classification info
392
- classification = {
393
- route: routed_to,
394
- classifier_model: self.class.classifier_model,
395
- classification_time_ms: classification_time_ms,
396
- method: classifier_result ? "llm" : "rule"
397
- }
398
-
399
- final_status = status || (error ? "error" : "success")
400
-
401
- result = Workflow::Result.new(
402
- content: content,
403
- workflow_type: self.class.name,
404
- workflow_id: workflow_id,
405
- routed_to: routed_to,
406
- classification: classification,
407
- classifier_result: classifier_result,
408
- branches: branches,
409
- status: final_status,
410
- started_at: @workflow_started_at,
411
- completed_at: Time.current,
412
- duration_ms: (((Time.current - @workflow_started_at) * 1000).round if @workflow_started_at)
413
- )
414
-
415
- if error
416
- result.instance_variable_set(:@error_class, error.class.name)
417
- result.instance_variable_set(:@error_message, error.message)
418
- result.instance_variable_set(:@errors, { routing: error })
419
- end
420
-
421
- result
422
- end
423
- end
424
-
425
- # Error raised for router-specific issues
426
- class RouterError < StandardError; end
427
- end
428
- end
429
- end