ruby_llm-agents 0.5.0 → 1.0.0.beta.1
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 +189 -31
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +1 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +172 -34
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -366
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -210
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
- data/lib/ruby_llm/agents/configuration.rb +0 -394
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Controller for viewing workflow details and per-workflow analytics
|
|
6
|
+
#
|
|
7
|
+
# Provides detailed views for individual workflows including structure
|
|
8
|
+
# visualization, per-step/branch performance, route distribution,
|
|
9
|
+
# and execution history.
|
|
10
|
+
#
|
|
11
|
+
# @see AgentRegistry For workflow discovery
|
|
12
|
+
# @see Paginatable For pagination implementation
|
|
13
|
+
# @see Filterable For filter parsing and validation
|
|
14
|
+
# @api private
|
|
15
|
+
class WorkflowsController < ApplicationController
|
|
16
|
+
include Paginatable
|
|
17
|
+
include Filterable
|
|
18
|
+
|
|
19
|
+
# Shows detailed view for a specific workflow
|
|
20
|
+
#
|
|
21
|
+
# Loads workflow configuration (if class exists), statistics,
|
|
22
|
+
# filtered executions, chart data, and step-level analytics.
|
|
23
|
+
# Works for both active workflows and deleted workflows with history.
|
|
24
|
+
#
|
|
25
|
+
# @return [void]
|
|
26
|
+
def show
|
|
27
|
+
@workflow_type = CGI.unescape(params[:id])
|
|
28
|
+
@workflow_class = AgentRegistry.find(@workflow_type)
|
|
29
|
+
@workflow_active = @workflow_class.present?
|
|
30
|
+
|
|
31
|
+
# Determine workflow type from class or execution history
|
|
32
|
+
@workflow_type_kind = detect_workflow_type_kind
|
|
33
|
+
|
|
34
|
+
load_workflow_stats
|
|
35
|
+
load_filter_options
|
|
36
|
+
load_filtered_executions
|
|
37
|
+
load_chart_data
|
|
38
|
+
load_step_stats
|
|
39
|
+
|
|
40
|
+
if @workflow_class
|
|
41
|
+
load_workflow_config
|
|
42
|
+
end
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
Rails.logger.error("[RubyLLM::Agents] Error loading workflow #{@workflow_type}: #{e.message}")
|
|
45
|
+
redirect_to ruby_llm_agents.agents_path, alert: "Error loading workflow details"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Detects the workflow type kind (pipeline, parallel, router)
|
|
51
|
+
#
|
|
52
|
+
# @return [String, nil] The workflow type kind
|
|
53
|
+
def detect_workflow_type_kind
|
|
54
|
+
if @workflow_class
|
|
55
|
+
ancestors = @workflow_class.ancestors.map { |a| a.name.to_s }
|
|
56
|
+
if ancestors.include?("RubyLLM::Agents::Workflow::Pipeline")
|
|
57
|
+
"pipeline"
|
|
58
|
+
elsif ancestors.include?("RubyLLM::Agents::Workflow::Parallel")
|
|
59
|
+
"parallel"
|
|
60
|
+
elsif ancestors.include?("RubyLLM::Agents::Workflow::Router")
|
|
61
|
+
"router"
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
# Fallback to execution history
|
|
65
|
+
Execution.by_agent(@workflow_type)
|
|
66
|
+
.where.not(workflow_type: nil)
|
|
67
|
+
.pluck(:workflow_type)
|
|
68
|
+
.first
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Loads all-time and today's statistics for the workflow
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
def load_workflow_stats
|
|
76
|
+
@stats = Execution.stats_for(@workflow_type, period: :all_time)
|
|
77
|
+
@stats_today = Execution.stats_for(@workflow_type, period: :today)
|
|
78
|
+
|
|
79
|
+
# Additional stats for new schema fields
|
|
80
|
+
workflow_scope = Execution.by_agent(@workflow_type)
|
|
81
|
+
@cache_hit_rate = workflow_scope.cache_hit_rate
|
|
82
|
+
@streaming_rate = workflow_scope.streaming_rate
|
|
83
|
+
@avg_ttft = workflow_scope.avg_time_to_first_token
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Loads available filter options from execution history
|
|
87
|
+
#
|
|
88
|
+
# @return [void]
|
|
89
|
+
def load_filter_options
|
|
90
|
+
filter_data = Execution.by_agent(@workflow_type)
|
|
91
|
+
.where.not(agent_version: nil)
|
|
92
|
+
.or(Execution.by_agent(@workflow_type).where.not(model_id: nil))
|
|
93
|
+
.or(Execution.by_agent(@workflow_type).where.not(temperature: nil))
|
|
94
|
+
.pluck(:agent_version, :model_id, :temperature)
|
|
95
|
+
|
|
96
|
+
@versions = filter_data.map(&:first).compact.uniq.sort.reverse
|
|
97
|
+
@models = filter_data.map { |d| d[1] }.compact.uniq.sort
|
|
98
|
+
@temperatures = filter_data.map(&:last).compact.uniq.sort
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Loads paginated and filtered executions with statistics
|
|
102
|
+
#
|
|
103
|
+
# @return [void]
|
|
104
|
+
def load_filtered_executions
|
|
105
|
+
base_scope = build_filtered_scope
|
|
106
|
+
result = paginate(base_scope)
|
|
107
|
+
@executions = result[:records]
|
|
108
|
+
@pagination = result[:pagination]
|
|
109
|
+
|
|
110
|
+
@filter_stats = {
|
|
111
|
+
total_count: result[:pagination][:total_count],
|
|
112
|
+
total_cost: base_scope.sum(:total_cost),
|
|
113
|
+
total_tokens: base_scope.sum(:total_tokens)
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Builds a filtered scope for the current workflow's executions
|
|
118
|
+
#
|
|
119
|
+
# @return [ActiveRecord::Relation] Filtered execution scope
|
|
120
|
+
def build_filtered_scope
|
|
121
|
+
scope = Execution.by_agent(@workflow_type)
|
|
122
|
+
|
|
123
|
+
# Apply status filter with validation
|
|
124
|
+
statuses = parse_array_param(:statuses)
|
|
125
|
+
scope = apply_status_filter(scope, statuses) if statuses.any?
|
|
126
|
+
|
|
127
|
+
# Apply version filter
|
|
128
|
+
versions = parse_array_param(:versions)
|
|
129
|
+
scope = scope.where(agent_version: versions) if versions.any?
|
|
130
|
+
|
|
131
|
+
# Apply model filter
|
|
132
|
+
models = parse_array_param(:models)
|
|
133
|
+
scope = scope.where(model_id: models) if models.any?
|
|
134
|
+
|
|
135
|
+
# Apply temperature filter
|
|
136
|
+
temperatures = parse_array_param(:temperatures)
|
|
137
|
+
scope = scope.where(temperature: temperatures) if temperatures.any?
|
|
138
|
+
|
|
139
|
+
# Apply time range filter with validation
|
|
140
|
+
days = parse_days_param
|
|
141
|
+
scope = apply_time_filter(scope, days)
|
|
142
|
+
|
|
143
|
+
scope
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Loads chart data for workflow performance visualization
|
|
147
|
+
#
|
|
148
|
+
# @return [void]
|
|
149
|
+
def load_chart_data
|
|
150
|
+
@trend_data = Execution.trend_analysis(agent_type: @workflow_type, days: 30)
|
|
151
|
+
@status_distribution = Execution.by_agent(@workflow_type).group(:status).count
|
|
152
|
+
@finish_reason_distribution = Execution.by_agent(@workflow_type).finish_reason_distribution
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Loads per-step/branch statistics for workflow analytics
|
|
156
|
+
#
|
|
157
|
+
# @return [void]
|
|
158
|
+
def load_step_stats
|
|
159
|
+
@step_stats = calculate_step_stats
|
|
160
|
+
@route_distribution = calculate_route_distribution if @workflow_type_kind == "router"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Calculates per-step/branch performance statistics
|
|
164
|
+
#
|
|
165
|
+
# @return [Array<Hash>] Array of step stats hashes
|
|
166
|
+
def calculate_step_stats
|
|
167
|
+
# Get root workflow executions
|
|
168
|
+
root_executions = Execution.by_agent(@workflow_type)
|
|
169
|
+
.root_executions
|
|
170
|
+
.where("created_at > ?", 30.days.ago)
|
|
171
|
+
.pluck(:id)
|
|
172
|
+
|
|
173
|
+
return [] if root_executions.empty?
|
|
174
|
+
|
|
175
|
+
# Aggregate child execution stats by workflow_step
|
|
176
|
+
child_stats = Execution.where(parent_execution_id: root_executions)
|
|
177
|
+
.group(:workflow_step)
|
|
178
|
+
.select(
|
|
179
|
+
"workflow_step",
|
|
180
|
+
"COUNT(*) as execution_count",
|
|
181
|
+
"AVG(duration_ms) as avg_duration_ms",
|
|
182
|
+
"SUM(total_cost) as total_cost",
|
|
183
|
+
"AVG(total_cost) as avg_cost",
|
|
184
|
+
"SUM(total_tokens) as total_tokens",
|
|
185
|
+
"AVG(total_tokens) as avg_tokens",
|
|
186
|
+
"SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count",
|
|
187
|
+
"SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Get agent type mappings for each step
|
|
191
|
+
step_agent_map = Execution.where(parent_execution_id: root_executions)
|
|
192
|
+
.where.not(workflow_step: nil)
|
|
193
|
+
.group(:workflow_step)
|
|
194
|
+
.pluck(:workflow_step, Arel.sql("MAX(agent_type)"))
|
|
195
|
+
.to_h
|
|
196
|
+
|
|
197
|
+
child_stats.map do |row|
|
|
198
|
+
next if row.workflow_step.blank?
|
|
199
|
+
|
|
200
|
+
execution_count = row.execution_count.to_i
|
|
201
|
+
success_count = row.success_count.to_i
|
|
202
|
+
success_rate = execution_count > 0 ? (success_count.to_f / execution_count * 100).round(1) : 0
|
|
203
|
+
|
|
204
|
+
{
|
|
205
|
+
name: row.workflow_step,
|
|
206
|
+
agent_type: step_agent_map[row.workflow_step],
|
|
207
|
+
execution_count: execution_count,
|
|
208
|
+
success_rate: success_rate,
|
|
209
|
+
avg_duration_ms: row.avg_duration_ms.to_f.round(0),
|
|
210
|
+
total_cost: row.total_cost.to_f.round(4),
|
|
211
|
+
avg_cost: row.avg_cost.to_f.round(6),
|
|
212
|
+
total_tokens: row.total_tokens.to_i,
|
|
213
|
+
avg_tokens: row.avg_tokens.to_f.round(0)
|
|
214
|
+
}
|
|
215
|
+
end.compact
|
|
216
|
+
end
|
|
217
|
+
|
|
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
|
+
# Loads the current workflow class configuration
|
|
242
|
+
#
|
|
243
|
+
# @return [void]
|
|
244
|
+
def load_workflow_config
|
|
245
|
+
@config = {
|
|
246
|
+
# Basic configuration
|
|
247
|
+
version: safe_call(@workflow_class, :version),
|
|
248
|
+
description: safe_call(@workflow_class, :description),
|
|
249
|
+
timeout: safe_call(@workflow_class, :timeout),
|
|
250
|
+
max_cost: safe_call(@workflow_class, :max_cost)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Workflow-specific configuration
|
|
254
|
+
case @workflow_type_kind
|
|
255
|
+
when "pipeline"
|
|
256
|
+
load_pipeline_config
|
|
257
|
+
when "parallel"
|
|
258
|
+
load_parallel_config
|
|
259
|
+
when "router"
|
|
260
|
+
load_router_config
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Loads pipeline-specific configuration
|
|
265
|
+
#
|
|
266
|
+
# @return [void]
|
|
267
|
+
def load_pipeline_config
|
|
268
|
+
@steps = extract_steps(@workflow_class)
|
|
269
|
+
@config[:steps_count] = @steps.size
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Loads parallel-specific configuration
|
|
273
|
+
#
|
|
274
|
+
# @return [void]
|
|
275
|
+
def load_parallel_config
|
|
276
|
+
@branches = extract_branches(@workflow_class)
|
|
277
|
+
@config[:branches_count] = @branches.size
|
|
278
|
+
@config[:fail_fast] = safe_call(@workflow_class, :fail_fast?)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Loads router-specific configuration
|
|
282
|
+
#
|
|
283
|
+
# @return [void]
|
|
284
|
+
def load_router_config
|
|
285
|
+
@routes = extract_routes(@workflow_class)
|
|
286
|
+
@config[:routes_count] = @routes.size
|
|
287
|
+
@config[:classifier_model] = safe_call(@workflow_class, :classifier_model)
|
|
288
|
+
@config[:classifier_temperature] = safe_call(@workflow_class, :classifier_temperature)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Extracts steps from a pipeline workflow class
|
|
292
|
+
#
|
|
293
|
+
# @param klass [Class] The workflow class
|
|
294
|
+
# @return [Array<Hash>] Array of step hashes
|
|
295
|
+
def extract_steps(klass)
|
|
296
|
+
return [] unless klass.respond_to?(:steps)
|
|
297
|
+
|
|
298
|
+
klass.steps.map do |name, config|
|
|
299
|
+
{
|
|
300
|
+
name: name,
|
|
301
|
+
agent: config[:agent]&.name,
|
|
302
|
+
optional: config[:continue_on_error] || false
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Extracts branches from a parallel workflow class
|
|
308
|
+
#
|
|
309
|
+
# @param klass [Class] The workflow class
|
|
310
|
+
# @return [Array<Hash>] Array of branch hashes
|
|
311
|
+
def extract_branches(klass)
|
|
312
|
+
return [] unless klass.respond_to?(:branches)
|
|
313
|
+
|
|
314
|
+
klass.branches.map do |name, config|
|
|
315
|
+
{
|
|
316
|
+
name: name,
|
|
317
|
+
agent: config[:agent]&.name,
|
|
318
|
+
optional: config[:optional] || false
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Extracts routes from a router workflow class
|
|
324
|
+
#
|
|
325
|
+
# @param klass [Class] The workflow class
|
|
326
|
+
# @return [Array<Hash>] Array of route hashes
|
|
327
|
+
def extract_routes(klass)
|
|
328
|
+
return [] unless klass.respond_to?(:routes)
|
|
329
|
+
|
|
330
|
+
klass.routes.map do |name, config|
|
|
331
|
+
{
|
|
332
|
+
name: name,
|
|
333
|
+
agent: config[:agent]&.name,
|
|
334
|
+
description: config[:description],
|
|
335
|
+
default: config[:default] || false
|
|
336
|
+
}
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Safely calls a method on a class, returning nil if method doesn't exist
|
|
341
|
+
#
|
|
342
|
+
# @param klass [Class, nil] The class to call the method on
|
|
343
|
+
# @param method_name [Symbol] The method to call
|
|
344
|
+
# @return [Object, nil] The result or nil
|
|
345
|
+
def safe_call(klass, method_name)
|
|
346
|
+
return nil unless klass
|
|
347
|
+
return nil unless klass.respond_to?(method_name)
|
|
348
|
+
|
|
349
|
+
klass.public_send(method_name)
|
|
350
|
+
rescue StandardError
|
|
351
|
+
nil
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
@@ -24,6 +24,31 @@ module RubyLLM
|
|
|
24
24
|
RubyLLM::Agents::Engine.routes.url_helpers
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Returns the URL for "All Tenants" (clears tenant filter)
|
|
28
|
+
#
|
|
29
|
+
# Handles two scenarios:
|
|
30
|
+
# 1. Query param routes - removes tenant_id from query params
|
|
31
|
+
# 2. Path-based tenant routes - navigates to equivalent global route
|
|
32
|
+
#
|
|
33
|
+
# @return [String] URL without tenant filtering
|
|
34
|
+
def all_tenants_url
|
|
35
|
+
# Map tenant-specific path routes to their global equivalents
|
|
36
|
+
tenant_route_mappings = {
|
|
37
|
+
"tenant" => ruby_llm_agents.api_configuration_path,
|
|
38
|
+
"edit_tenant" => ruby_llm_agents.edit_api_configuration_path
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Check if current action has a global equivalent
|
|
42
|
+
if tenant_route_mappings.key?(action_name)
|
|
43
|
+
base_path = tenant_route_mappings[action_name]
|
|
44
|
+
query = request.query_parameters.except("tenant_id")
|
|
45
|
+
query.any? ? "#{base_path}?#{query.to_query}" : base_path
|
|
46
|
+
else
|
|
47
|
+
# For query param routes, just remove tenant_id
|
|
48
|
+
url_for(request.query_parameters.except("tenant_id"))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
27
52
|
# Formats large numbers with human-readable suffixes (K, M, B)
|
|
28
53
|
#
|
|
29
54
|
# @param number [Numeric, nil] The number to format
|
|
@@ -72,6 +72,9 @@ module RubyLLM
|
|
|
72
72
|
has_many :child_executions, class_name: "RubyLLM::Agents::Execution",
|
|
73
73
|
foreign_key: :parent_execution_id, dependent: :nullify, inverse_of: :parent_execution
|
|
74
74
|
|
|
75
|
+
# Polymorphic association to tenant model (for llm_tenant DSL)
|
|
76
|
+
belongs_to :tenant_record, polymorphic: true, optional: true
|
|
77
|
+
|
|
75
78
|
# Validations
|
|
76
79
|
validates :agent_type, :model_id, :started_at, presence: true
|
|
77
80
|
validates :status, inclusion: { in: statuses.keys }
|
|
@@ -6,7 +6,7 @@ module RubyLLM
|
|
|
6
6
|
#
|
|
7
7
|
# Stores per-tenant budget limits that override the global configuration.
|
|
8
8
|
# Supports runtime updates without application restarts.
|
|
9
|
-
# Supports
|
|
9
|
+
# Supports cost-based (USD), token-based, and execution-based limits.
|
|
10
10
|
#
|
|
11
11
|
# @!attribute [rw] tenant_id
|
|
12
12
|
# @return [String] Unique identifier for the tenant
|
|
@@ -20,6 +20,10 @@ module RubyLLM
|
|
|
20
20
|
# @return [Integer, nil] Daily token limit (across all models)
|
|
21
21
|
# @!attribute [rw] monthly_token_limit
|
|
22
22
|
# @return [Integer, nil] Monthly token limit (across all models)
|
|
23
|
+
# @!attribute [rw] daily_execution_limit
|
|
24
|
+
# @return [Integer, nil] Daily execution/call limit
|
|
25
|
+
# @!attribute [rw] monthly_execution_limit
|
|
26
|
+
# @return [Integer, nil] Monthly execution/call limit
|
|
23
27
|
# @!attribute [rw] per_agent_daily
|
|
24
28
|
# @return [Hash] Per-agent daily cost limits: { "AgentName" => limit }
|
|
25
29
|
# @!attribute [rw] per_agent_monthly
|
|
@@ -28,25 +32,30 @@ module RubyLLM
|
|
|
28
32
|
# @return [String] Enforcement mode: "none", "soft", or "hard"
|
|
29
33
|
# @!attribute [rw] inherit_global_defaults
|
|
30
34
|
# @return [Boolean] Whether to fall back to global config for unset limits
|
|
35
|
+
# @!attribute [rw] tenant_record
|
|
36
|
+
# @return [ActiveRecord::Base, nil] Polymorphic association to tenant model
|
|
31
37
|
#
|
|
32
|
-
# @example Creating a tenant budget with cost and
|
|
38
|
+
# @example Creating a tenant budget with cost, token, and execution limits
|
|
33
39
|
# TenantBudget.create!(
|
|
34
40
|
# tenant_id: "acme_corp",
|
|
35
41
|
# name: "Acme Corporation",
|
|
36
|
-
# daily_limit: 50.0,
|
|
37
|
-
# monthly_limit: 500.0,
|
|
42
|
+
# daily_limit: 50.0, # USD
|
|
43
|
+
# monthly_limit: 500.0, # USD
|
|
38
44
|
# daily_token_limit: 1_000_000,
|
|
39
45
|
# monthly_token_limit: 10_000_000,
|
|
40
|
-
#
|
|
46
|
+
# daily_execution_limit: 500,
|
|
47
|
+
# monthly_execution_limit: 10_000,
|
|
41
48
|
# enforcement: "hard"
|
|
42
49
|
# )
|
|
43
50
|
#
|
|
44
|
-
# @example Fetching budget for a tenant
|
|
45
|
-
# budget = TenantBudget.for_tenant(
|
|
46
|
-
# budget.effective_daily_limit
|
|
47
|
-
# budget.effective_daily_token_limit
|
|
51
|
+
# @example Fetching budget for a tenant object
|
|
52
|
+
# budget = TenantBudget.for_tenant(organization)
|
|
53
|
+
# budget.effective_daily_limit # => 50.0 (cost)
|
|
54
|
+
# budget.effective_daily_token_limit # => 1_000_000 (tokens)
|
|
55
|
+
# budget.effective_daily_execution_limit # => 500 (executions)
|
|
48
56
|
#
|
|
49
57
|
# @see RubyLLM::Agents::BudgetTracker
|
|
58
|
+
# @see RubyLLM::Agents::LLMTenant
|
|
50
59
|
# @api public
|
|
51
60
|
class TenantBudget < ::ActiveRecord::Base
|
|
52
61
|
self.table_name = "ruby_llm_agents_tenant_budgets"
|
|
@@ -54,6 +63,9 @@ module RubyLLM
|
|
|
54
63
|
# Valid enforcement modes
|
|
55
64
|
ENFORCEMENT_MODES = %w[none soft hard].freeze
|
|
56
65
|
|
|
66
|
+
# Polymorphic association to the tenant model (e.g., Organization, Account)
|
|
67
|
+
belongs_to :tenant_record, polymorphic: true, optional: true
|
|
68
|
+
|
|
57
69
|
# Validations
|
|
58
70
|
validates :tenant_id, presence: true, uniqueness: true
|
|
59
71
|
validates :enforcement, inclusion: { in: ENFORCEMENT_MODES }, allow_nil: true
|
|
@@ -61,15 +73,23 @@ module RubyLLM
|
|
|
61
73
|
numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
62
74
|
validates :daily_token_limit, :monthly_token_limit,
|
|
63
75
|
numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
|
|
76
|
+
validates :daily_execution_limit, :monthly_execution_limit,
|
|
77
|
+
numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
|
|
64
78
|
|
|
65
79
|
# Finds a budget for the given tenant
|
|
66
80
|
#
|
|
67
|
-
# @param
|
|
81
|
+
# @param tenant [String, Object] The tenant identifier string or object with llm_tenant_id
|
|
68
82
|
# @return [TenantBudget, nil] The budget record or nil if not found
|
|
69
|
-
def self.for_tenant(
|
|
70
|
-
return nil if
|
|
83
|
+
def self.for_tenant(tenant)
|
|
84
|
+
return nil if tenant.blank?
|
|
71
85
|
|
|
72
|
-
|
|
86
|
+
if tenant.respond_to?(:llm_tenant_id)
|
|
87
|
+
# Object with llm_tenant DSL - try polymorphic first, then tenant_id
|
|
88
|
+
find_by(tenant_record: tenant) || find_by(tenant_id: tenant.llm_tenant_id)
|
|
89
|
+
else
|
|
90
|
+
# String tenant_id
|
|
91
|
+
find_by(tenant_id: tenant.to_s)
|
|
92
|
+
end
|
|
73
93
|
end
|
|
74
94
|
|
|
75
95
|
# Finds or creates a budget for the given tenant
|
|
@@ -154,6 +174,26 @@ module RubyLLM
|
|
|
154
174
|
global_config&.dig(:global_monthly_tokens)
|
|
155
175
|
end
|
|
156
176
|
|
|
177
|
+
# Returns the effective daily execution limit, considering inheritance
|
|
178
|
+
#
|
|
179
|
+
# @return [Integer, nil] The daily execution limit or nil if not set
|
|
180
|
+
def effective_daily_execution_limit
|
|
181
|
+
return daily_execution_limit if daily_execution_limit.present?
|
|
182
|
+
return nil unless inherit_global_defaults
|
|
183
|
+
|
|
184
|
+
global_config&.dig(:global_daily_executions)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Returns the effective monthly execution limit, considering inheritance
|
|
188
|
+
#
|
|
189
|
+
# @return [Integer, nil] The monthly execution limit or nil if not set
|
|
190
|
+
def effective_monthly_execution_limit
|
|
191
|
+
return monthly_execution_limit if monthly_execution_limit.present?
|
|
192
|
+
return nil unless inherit_global_defaults
|
|
193
|
+
|
|
194
|
+
global_config&.dig(:global_monthly_executions)
|
|
195
|
+
end
|
|
196
|
+
|
|
157
197
|
# Returns the effective enforcement mode
|
|
158
198
|
#
|
|
159
199
|
# @return [Symbol] :none, :soft, or :hard
|
|
@@ -183,9 +223,12 @@ module RubyLLM
|
|
|
183
223
|
global_monthly: effective_monthly_limit,
|
|
184
224
|
per_agent_daily: merged_per_agent_daily,
|
|
185
225
|
per_agent_monthly: merged_per_agent_monthly,
|
|
186
|
-
# Token limits
|
|
226
|
+
# Token limits
|
|
187
227
|
global_daily_tokens: effective_daily_token_limit,
|
|
188
|
-
global_monthly_tokens: effective_monthly_token_limit
|
|
228
|
+
global_monthly_tokens: effective_monthly_token_limit,
|
|
229
|
+
# Execution limits
|
|
230
|
+
global_daily_executions: effective_daily_execution_limit,
|
|
231
|
+
global_monthly_executions: effective_monthly_execution_limit
|
|
189
232
|
}
|
|
190
233
|
end
|
|
191
234
|
|
|
@@ -60,12 +60,19 @@ module RubyLLM
|
|
|
60
60
|
#
|
|
61
61
|
# @return [Array<String>] Agent class names
|
|
62
62
|
def file_system_agents
|
|
63
|
-
# Ensure all agent classes are loaded
|
|
63
|
+
# Ensure all agent and workflow classes are loaded
|
|
64
64
|
eager_load_agents!
|
|
65
65
|
|
|
66
|
-
# Find all descendants of
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
# Find all descendants of all base classes
|
|
67
|
+
agents = RubyLLM::Agents::Base.descendants.map(&:name).compact
|
|
68
|
+
workflows = RubyLLM::Agents::Workflow.descendants.map(&:name).compact
|
|
69
|
+
embedders = RubyLLM::Agents::Embedder.descendants.map(&:name).compact
|
|
70
|
+
moderators = RubyLLM::Agents::Moderator.descendants.map(&:name).compact
|
|
71
|
+
speakers = RubyLLM::Agents::Speaker.descendants.map(&:name).compact
|
|
72
|
+
transcribers = RubyLLM::Agents::Transcriber.descendants.map(&:name).compact
|
|
73
|
+
image_generators = RubyLLM::Agents::ImageGenerator.descendants.map(&:name).compact
|
|
74
|
+
|
|
75
|
+
(agents + workflows + embedders + moderators + speakers + transcribers + image_generators).uniq
|
|
69
76
|
rescue StandardError => e
|
|
70
77
|
Rails.logger.error("[RubyLLM::Agents] Error loading agents from file system: #{e.message}")
|
|
71
78
|
[]
|
|
@@ -81,17 +88,19 @@ module RubyLLM
|
|
|
81
88
|
[]
|
|
82
89
|
end
|
|
83
90
|
|
|
84
|
-
# Eager loads all agent files to register descendants
|
|
91
|
+
# Eager loads all agent and workflow files to register descendants
|
|
85
92
|
#
|
|
86
93
|
# @return [void]
|
|
87
94
|
def eager_load_agents!
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
%w[agents workflows embedders moderators speakers transcribers image_generators].each do |dir|
|
|
96
|
+
path = Rails.root.join("app", dir)
|
|
97
|
+
next unless path.exist?
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
Dir.glob(path.join("**", "*.rb")).each do |file|
|
|
100
|
+
require_dependency file
|
|
101
|
+
rescue LoadError, StandardError => e
|
|
102
|
+
Rails.logger.error("[RubyLLM::Agents] Failed to load file #{file}: #{e.message}")
|
|
103
|
+
end
|
|
95
104
|
end
|
|
96
105
|
end
|
|
97
106
|
|
|
@@ -125,8 +134,11 @@ module RubyLLM
|
|
|
125
134
|
agent_class = find(agent_type)
|
|
126
135
|
stats = fetch_stats(agent_type)
|
|
127
136
|
|
|
137
|
+
# Detect the agent type (agent, workflow, embedder, moderator, speaker, transcriber)
|
|
138
|
+
detected_type = detect_agent_type(agent_class)
|
|
139
|
+
|
|
128
140
|
# Check if this is a workflow class vs a regular agent
|
|
129
|
-
is_workflow =
|
|
141
|
+
is_workflow = detected_type == "workflow"
|
|
130
142
|
|
|
131
143
|
# Determine specific workflow type and children
|
|
132
144
|
workflow_type = is_workflow ? detect_workflow_type(agent_class) : nil
|
|
@@ -136,6 +148,7 @@ module RubyLLM
|
|
|
136
148
|
name: agent_type,
|
|
137
149
|
class: agent_class,
|
|
138
150
|
active: agent_class.present?,
|
|
151
|
+
agent_type: detected_type,
|
|
139
152
|
is_workflow: is_workflow,
|
|
140
153
|
workflow_type: workflow_type,
|
|
141
154
|
workflow_children: workflow_children,
|
|
@@ -209,6 +222,32 @@ module RubyLLM
|
|
|
209
222
|
end
|
|
210
223
|
end
|
|
211
224
|
|
|
225
|
+
# Detects the agent type from class hierarchy
|
|
226
|
+
#
|
|
227
|
+
# @param agent_class [Class, nil] The agent class
|
|
228
|
+
# @return [String] "agent", "workflow", "embedder", "moderator", "speaker", "transcriber", or "image_generator"
|
|
229
|
+
def detect_agent_type(agent_class)
|
|
230
|
+
return "agent" unless agent_class
|
|
231
|
+
|
|
232
|
+
ancestors = agent_class.ancestors.map { |a| a.name.to_s }
|
|
233
|
+
|
|
234
|
+
if ancestors.include?("RubyLLM::Agents::Embedder")
|
|
235
|
+
"embedder"
|
|
236
|
+
elsif ancestors.include?("RubyLLM::Agents::Moderator")
|
|
237
|
+
"moderator"
|
|
238
|
+
elsif ancestors.include?("RubyLLM::Agents::Speaker")
|
|
239
|
+
"speaker"
|
|
240
|
+
elsif ancestors.include?("RubyLLM::Agents::Transcriber")
|
|
241
|
+
"transcriber"
|
|
242
|
+
elsif ancestors.include?("RubyLLM::Agents::ImageGenerator")
|
|
243
|
+
"image_generator"
|
|
244
|
+
elsif ancestors.include?("RubyLLM::Agents::Workflow")
|
|
245
|
+
"workflow"
|
|
246
|
+
else
|
|
247
|
+
"agent"
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
212
251
|
# Extracts child agents from workflow DSL configuration
|
|
213
252
|
#
|
|
214
253
|
# @param agent_class [Class, nil] The workflow class
|