activeagent 1.0.1 → 1.0.2

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -4
  3. data/lib/active_agent/base.rb +3 -2
  4. data/lib/active_agent/concerns/provider.rb +6 -2
  5. data/lib/active_agent/concerns/rescue.rb +39 -0
  6. data/lib/active_agent/concerns/streaming.rb +2 -1
  7. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
  10. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
  11. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
  12. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
  13. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
  14. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
  15. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
  16. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
  17. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
  18. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
  19. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
  20. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
  21. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
  22. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
  23. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
  24. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
  25. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
  26. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
  27. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
  28. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
  29. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
  30. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
  31. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
  32. data/lib/active_agent/dashboard/config/routes.rb +78 -0
  33. data/lib/active_agent/dashboard/engine.rb +39 -0
  34. data/lib/active_agent/dashboard.rb +151 -0
  35. data/lib/active_agent/providers/_base_provider.rb +2 -1
  36. data/lib/active_agent/providers/anthropic_provider.rb +14 -4
  37. data/lib/active_agent/providers/azure/_types.rb +5 -0
  38. data/lib/active_agent/providers/azure/options.rb +111 -0
  39. data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
  40. data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
  41. data/lib/active_agent/providers/azure_provider.rb +133 -0
  42. data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
  43. data/lib/active_agent/providers/bedrock/_types.rb +8 -0
  44. data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
  45. data/lib/active_agent/providers/bedrock/options.rb +77 -0
  46. data/lib/active_agent/providers/bedrock_provider.rb +84 -0
  47. data/lib/active_agent/providers/common/messages/_types.rb +6 -2
  48. data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
  49. data/lib/active_agent/providers/gemini/_types.rb +19 -0
  50. data/lib/active_agent/providers/gemini/options.rb +41 -0
  51. data/lib/active_agent/providers/gemini_provider.rb +94 -0
  52. data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
  53. data/lib/active_agent/providers/open_ai/chat_provider.rb +2 -0
  54. data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
  55. data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
  56. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
  57. data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
  58. data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
  59. data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
  60. data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
  61. data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
  62. data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
  63. data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
  64. data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
  65. data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
  66. data/lib/active_agent/railtie.rb +32 -1
  67. data/lib/active_agent/telemetry/configuration.rb +213 -0
  68. data/lib/active_agent/telemetry/instrumentation.rb +155 -0
  69. data/lib/active_agent/telemetry/reporter.rb +176 -0
  70. data/lib/active_agent/telemetry/span.rb +267 -0
  71. data/lib/active_agent/telemetry/tracer.rb +184 -0
  72. data/lib/active_agent/telemetry.rb +162 -0
  73. data/lib/active_agent/version.rb +1 -1
  74. data/lib/active_agent.rb +2 -0
  75. data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
  76. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
  77. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
  78. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
  79. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
  80. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
  81. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
  82. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
  83. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
  84. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
  85. data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
  86. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
  87. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
  88. metadata +99 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ee3a092c5c836febf3c2e3045a1c1b2cd448edc18f3e76e4ee1ebc181f895b8
4
- data.tar.gz: 9850ab912eedaac0a57f9a954648d096d6aa70ad470ff9aa7c84a18183ab76be
3
+ metadata.gz: 2a1deba19c874b6abb36731bcee93bddf79bc193a3443a234cc811d9a4cc8383
4
+ data.tar.gz: 6f8847ab0d5eebf0ce26a8425f7630c884f0b9af71384c67325e3c06a8b0a8ae
5
5
  SHA512:
6
- metadata.gz: 199061594f115a823037504f84daf5ebc79b084c8a835b3e67f5427a19ffe33bbe208bb627dc26b1abf977c4d35a2977defcfb59e9128e60d94c6a3dc0ce5259
7
- data.tar.gz: 661bf403014e2a0d1156614249b9fcdc352b8124e63457071ce75399ac69d089cf2d43d8b571e41614d811a5e6dc4ebc9072dfe38d912bc122672bf9823bb331
6
+ metadata.gz: 767f039bd0af4f600a34becaac7bb2d04d2f8b768aef11242191f8a355b4a442c9c10690fe25a1a7f8e0da8b27e7216025c6f882c6d6002e5da9d0a9e876f1cb
7
+ data.tar.gz: 30725fb2369bd3b8dc23aa9d41e7d8a9843c3c8dd0aa629ace4bbd4cc217077339684ae0925cc6ea330c1e018b444a66057bcf5854b5bebf2bcb6a14bf57a322
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <picture>
2
- <source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/2bad263a-c09f-40b6-94ba-fff8e346d65d">
3
- <img alt="activeagents_banner" src="https://github.com/user-attachments/assets/0ebbaa2f-c6bf-4d40-bb77-931015a14be3">
2
+ <source media="(prefers-color-scheme: dark)" srcset="docs/public/banner-dark.svg">
3
+ <img alt="Active Agent - Build AI in Rails" src="docs/public/banner-light.svg">
4
4
  </picture>
5
5
 
6
6
 
@@ -39,6 +39,9 @@ bundle add openai
39
39
 
40
40
  # OpenRouter (uses OpenAI-compatible API)
41
41
  bundle add openai
42
+
43
+ # RubyLLM (unified API for 15+ providers)
44
+ bundle add ruby_llm
42
45
  ```
43
46
 
44
47
  ### Setup
@@ -119,12 +122,15 @@ development:
119
122
  ollama:
120
123
  service: "Ollama"
121
124
  model: "llama3.2"
125
+
126
+ ruby_llm:
127
+ service: "RubyLLM"
122
128
  ```
123
129
 
124
130
  ## Features
125
131
 
126
132
  - **Agent-Oriented Programming**: Build AI applications using familiar Rails patterns
127
- - **Multiple Provider Support**: Works with OpenAI, Anthropic, Ollama, and more
133
+ - **Multiple Provider Support**: Works with OpenAI, Anthropic, Ollama, RubyLLM, and more
128
134
  - **Action-Based Design**: Define agent capabilities through actions
129
135
  - **View Templates**: Use ERB templates for prompts (text, JSON, HTML)
130
136
  - **Streaming Support**: Real-time response streaming with ActionCable
@@ -166,7 +172,7 @@ response = prompt.generate_now
166
172
  ## Learn More
167
173
 
168
174
  - [Documentation](https://docs.activeagents.ai)
169
- - [Getting Started Guide](https://docs.activeagents.ai/docs/getting-started)
175
+ - [Getting Started Guide](https://docs.activeagents.ai/getting_started)
170
176
  - [API Reference](https://docs.activeagents.ai/docs/framework)
171
177
  - [Examples](https://docs.activeagents.ai/docs/agents)
172
178
 
@@ -106,9 +106,10 @@ module ActiveAgent
106
106
  global_options = provider_config_load(provider_reference)
107
107
  inherited_options = (self.prompt_options || {}).except(:instructions) # Don't inherit instructions from parent
108
108
 
109
- # Different Service, different APIs
109
+ # Different Service, different APIs — discard all inherited options
110
+ # to prevent parent provider config (host, api_key, etc.) leaking through
110
111
  if global_options[:service] != inherited_options[:service]
111
- inherited_options.extract!(:service, :api_version)
112
+ inherited_options = {}
112
113
  end
113
114
 
114
115
  self.prompt_options = global_options.merge(inherited_options).merge(agent_options)
@@ -9,8 +9,12 @@ module ActiveAgent
9
9
 
10
10
  # "Your tacky and I hate you" - Billy, https://youtu.be/dsheboxJNgQ?si=tzDlJ7sdSxM4RjSD
11
11
  PROVIDER_SERVICE_NAMES_REMAPS = {
12
- "Openrouter" => "OpenRouter",
13
- "Openai" => "OpenAI"
12
+ "Openrouter" => "OpenRouter",
13
+ "Openai" => "OpenAI",
14
+ "AzureOpenai" => "AzureOpenAI",
15
+ "Azureopenai" => "AzureOpenAI",
16
+ "Rubyllm" => "RubyLLM",
17
+ "RubyLlm" => "RubyLLM"
14
18
  }
15
19
 
16
20
  included do
@@ -13,6 +13,45 @@ module ActiveAgent
13
13
  include ActiveSupport::Rescuable
14
14
 
15
15
  class_methods do
16
+ # Handles exceptions raised during GenerationJob execution.
17
+ #
18
+ # Called by GenerationJob#handle_exception_with_agent_class as a
19
+ # class-level fallback when no instance is available to handle the error.
20
+ # Logs the exception and re-raises to preserve job failure semantics.
21
+ #
22
+ # @param exception [Exception] the exception to handle
23
+ # @raise [Exception] re-raises the exception after logging
24
+ # @return [void]
25
+ def handle_exception(exception)
26
+ rescue_logger.error "[#{name}] #{exception.class}: #{exception.message}"
27
+ rescue_logger.error exception.backtrace&.first(10)&.join("\n") if exception.backtrace
28
+ raise exception
29
+ end
30
+
31
+ private
32
+
33
+ # Returns the logger to use for class-level exception handling.
34
+ #
35
+ # Prefers the including class's logger, then ActiveAgent::Base.logger,
36
+ # then Rails.logger if available, and finally falls back to a standard
37
+ # Ruby Logger to $stderr.
38
+ #
39
+ # @return [#error] a logger-like object responding to `#error`
40
+ def rescue_logger
41
+ if respond_to?(:logger) && (current_logger = logger)
42
+ current_logger
43
+ elsif defined?(ActiveAgent::Base) &&
44
+ ActiveAgent::Base.respond_to?(:logger) &&
45
+ (base_logger = ActiveAgent::Base.logger)
46
+ base_logger
47
+ elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
48
+ Rails.logger
49
+ else
50
+ require "logger"
51
+ @_rescue_logger ||= Logger.new($stderr)
52
+ end
53
+ end
54
+
16
55
  # Finds and instruments the rescue handler for an exception.
17
56
  #
18
57
  # @param exception [Exception] the exception to handle
@@ -268,7 +268,8 @@ module ActiveAgent
268
268
  # @return [Proc] callback proc that accepts (message, delta, type)
269
269
  def stream_broadcaster
270
270
  proc do |message, delta, type|
271
- self.stream_chunk = StreamChunk.new(message, delta)
271
+ cast_message = message.is_a?(Hash) ? Providers::Common::Messages::Types::MessageType.new.cast(message) : message
272
+ self.stream_chunk = StreamChunk.new(cast_message, delta)
272
273
 
273
274
  run_callbacks(:stream_open) if type == :open
274
275
  run_callbacks(:stream)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ module Api
6
+ # Telemetry ingestion endpoint.
7
+ #
8
+ # Receives traces from ActiveAgent::Telemetry::Reporter and stores them
9
+ # for analysis and visualization in the dashboard.
10
+ #
11
+ # Supports two modes:
12
+ # - Local mode: No authentication, synchronous processing
13
+ # - Multi-tenant mode: Bearer token auth, async processing via job
14
+ #
15
+ # @example Local mode request
16
+ # POST /active_agent/api/traces
17
+ # Content-Type: application/json
18
+ #
19
+ # {
20
+ # "traces": [...],
21
+ # "sdk": { "name": "activeagent", "version": "0.5.0" }
22
+ # }
23
+ #
24
+ # @example Multi-tenant mode request
25
+ # POST /active_agent/api/traces
26
+ # Authorization: Bearer <api_key>
27
+ # Content-Type: application/json
28
+ #
29
+ # {
30
+ # "traces": [...],
31
+ # "sdk": { "name": "activeagent", "version": "0.5.0" }
32
+ # }
33
+ #
34
+ class TracesController < ActionController::API
35
+ before_action :authenticate_api_key!, if: -> { ActiveAgent::Dashboard.multi_tenant? }
36
+
37
+ # POST /active_agent/api/traces
38
+ def create
39
+ traces = params[:traces] || []
40
+ sdk_info = params[:sdk] || {}
41
+
42
+ return head :accepted if traces.empty?
43
+
44
+ if ActiveAgent::Dashboard.multi_tenant?
45
+ # Multi-tenant mode: process in background
46
+ ActiveAgent::ProcessTelemetryTracesJob.perform_later(
47
+ account_id: @account&.id,
48
+ traces: traces.as_json,
49
+ sdk_info: sdk_info.as_json,
50
+ received_at: Time.current.iso8601(6)
51
+ )
52
+ else
53
+ # Local mode: process synchronously
54
+ process_traces_synchronously(traces, sdk_info)
55
+ end
56
+
57
+ head :accepted
58
+ rescue ActionController::ParameterMissing => e
59
+ render json: { error: e.message }, status: :bad_request
60
+ rescue StandardError => e
61
+ Rails.logger.error("[ActiveAgent::Dashboard] Trace ingestion error: #{e.message}")
62
+ render json: { error: "Internal server error" }, status: :internal_server_error
63
+ end
64
+
65
+ private
66
+
67
+ # Authenticates the request using Bearer token from Authorization header.
68
+ # Only used in multi-tenant mode.
69
+ def authenticate_api_key!
70
+ token = extract_bearer_token
71
+
72
+ if token.blank?
73
+ render json: { error: "Missing Authorization header" }, status: :unauthorized
74
+ return
75
+ end
76
+
77
+ account_class = ActiveAgent::Dashboard.account_class.constantize
78
+ @account = account_class.find_by(telemetry_api_key: token)
79
+
80
+ if @account.nil?
81
+ render json: { error: "Invalid API key" }, status: :unauthorized
82
+ return
83
+ end
84
+
85
+ # Track usage for rate limiting (if the account responds to it)
86
+ @account.increment_telemetry_usage! if @account.respond_to?(:increment_telemetry_usage!)
87
+ end
88
+
89
+ # Extracts Bearer token from Authorization header.
90
+ def extract_bearer_token
91
+ auth_header = request.headers["Authorization"]
92
+ return nil if auth_header.blank?
93
+
94
+ match = auth_header.match(/^Bearer\s+(.+)$/i)
95
+ match[1] if match
96
+ end
97
+
98
+ # Process traces synchronously for local development.
99
+ def process_traces_synchronously(traces, sdk_info)
100
+ model = ActiveAgent::Dashboard.trace_model
101
+
102
+ traces.each do |trace|
103
+ # Skip if trace already exists (idempotency)
104
+ next if model.exists?(trace_id: trace["trace_id"])
105
+
106
+ model.create_from_payload(trace, sdk_info)
107
+ rescue StandardError => e
108
+ Rails.logger.error(
109
+ "[ActiveAgent::Dashboard] Failed to process trace #{trace['trace_id']}: " \
110
+ "#{e.class} - #{e.message}"
111
+ )
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Base controller for the ActiveAgent Dashboard.
6
+ #
7
+ # Handles authentication and provides helper methods for multi-tenant mode.
8
+ class ApplicationController < ActionController::Base
9
+ protect_from_forgery with: :exception
10
+
11
+ before_action :authenticate_dashboard!
12
+
13
+ # Add the engine's view path
14
+ prepend_view_path ActiveAgent::Dashboard::Engine.dashboard_root.join("app", "views").to_s
15
+
16
+ # Use custom layout if configured, otherwise use engine layout
17
+ layout -> { ActiveAgent::Dashboard.layout || "active_agent/dashboard/application" }
18
+
19
+ helper_method :current_user, :current_owner
20
+
21
+ private
22
+
23
+ def authenticate_dashboard!
24
+ return if ActiveAgent::Dashboard.authentication_method.nil?
25
+
26
+ result = ActiveAgent::Dashboard.authentication_method.call(self)
27
+ head :unauthorized unless result
28
+ rescue StandardError => e
29
+ Rails.logger.error("[ActiveAgent::Dashboard] Authentication error: #{e.message}")
30
+ head :unauthorized
31
+ end
32
+
33
+ # Returns the current user from the host application.
34
+ def current_user
35
+ return nil unless ActiveAgent::Dashboard.current_user_method
36
+
37
+ send(ActiveAgent::Dashboard.current_user_method)
38
+ rescue NoMethodError
39
+ nil
40
+ end
41
+
42
+ # Returns the current owner (account in multi-tenant, user otherwise).
43
+ def current_owner
44
+ if ActiveAgent::Dashboard.multi_tenant? && ActiveAgent::Dashboard.current_account_method
45
+ send(ActiveAgent::Dashboard.current_account_method)
46
+ else
47
+ current_user
48
+ end
49
+ rescue NoMethodError
50
+ nil
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Main dashboard controller showing overview metrics and recent activity.
6
+ #
7
+ class DashboardController < ApplicationController
8
+ def index
9
+ @agents = fetch_agents.limit(10)
10
+ @recent_runs = fetch_recent_runs.limit(10)
11
+ @recent_traces = fetch_recent_traces.limit(10)
12
+ @metrics = calculate_metrics
13
+
14
+ if ActiveAgent::Dashboard.use_inertia && defined?(InertiaRails)
15
+ render inertia: "Dashboard", props: {
16
+ agents: serialize_agents(@agents),
17
+ recentRuns: serialize_runs(@recent_runs),
18
+ recentTraces: serialize_traces(@recent_traces),
19
+ metrics: @metrics,
20
+ user: current_user_props,
21
+ account: current_account_props
22
+ }
23
+ else
24
+ render :index
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def fetch_agents
31
+ agents = Agent.order(updated_at: :desc)
32
+ agents = agents.for_owner(current_owner) if current_owner
33
+ agents
34
+ end
35
+
36
+ def fetch_recent_runs
37
+ runs = AgentRun.includes(:agent).recent
38
+ if current_owner && ActiveAgent::Dashboard.multi_tenant?
39
+ runs = runs.joins(:agent).where(agents: { account_id: current_owner.id })
40
+ end
41
+ runs
42
+ end
43
+
44
+ def fetch_recent_traces
45
+ traces = ActiveAgent::Dashboard.trace_model.recent
46
+ traces = traces.for_account(current_owner) if current_owner && ActiveAgent::Dashboard.multi_tenant?
47
+ traces
48
+ end
49
+
50
+ def calculate_metrics
51
+ traces_24h = ActiveAgent::Dashboard.trace_model.where("created_at > ?", 24.hours.ago)
52
+ runs_24h = AgentRun.where("created_at > ?", 24.hours.ago)
53
+
54
+ if current_owner && ActiveAgent::Dashboard.multi_tenant?
55
+ traces_24h = traces_24h.for_account(current_owner)
56
+ runs_24h = runs_24h.joins(:agent).where(agents: { account_id: current_owner.id })
57
+ end
58
+
59
+ {
60
+ total_agents: fetch_agents.count,
61
+ active_agents: fetch_agents.active_agents.count,
62
+ total_runs_24h: runs_24h.count,
63
+ successful_runs_24h: runs_24h.successful.count,
64
+ failed_runs_24h: runs_24h.failed_runs.count,
65
+ total_traces_24h: traces_24h.count,
66
+ total_tokens_24h: traces_24h.sum(:total_input_tokens).to_i + traces_24h.sum(:total_output_tokens).to_i,
67
+ avg_duration_ms: runs_24h.where.not(duration_ms: nil).average(:duration_ms)&.round || 0
68
+ }
69
+ end
70
+
71
+ def serialize_agents(agents)
72
+ agents.map do |agent|
73
+ {
74
+ id: agent.id,
75
+ name: agent.name,
76
+ slug: agent.slug,
77
+ description: agent.description,
78
+ provider: agent.provider,
79
+ model: agent.model,
80
+ status: agent.status,
81
+ presetType: agent.preset_type,
82
+ versionCount: agent.version_count,
83
+ updatedAt: agent.updated_at.iso8601
84
+ }
85
+ end
86
+ end
87
+
88
+ def serialize_runs(runs)
89
+ runs.map(&:summary)
90
+ end
91
+
92
+ def serialize_traces(traces)
93
+ traces.map do |trace|
94
+ {
95
+ id: trace.id,
96
+ traceId: trace.trace_id,
97
+ displayName: trace.display_name,
98
+ status: trace.status,
99
+ duration: trace.formatted_duration,
100
+ tokens: trace.formatted_tokens,
101
+ timestamp: trace.timestamp&.iso8601
102
+ }
103
+ end
104
+ end
105
+
106
+ def current_user_props
107
+ return nil unless current_user
108
+
109
+ {
110
+ id: current_user.id,
111
+ name: current_user.try(:display_name) || current_user.try(:name) || current_user.try(:email),
112
+ email: current_user.try(:email)
113
+ }
114
+ end
115
+
116
+ def current_account_props
117
+ return nil unless current_owner && ActiveAgent::Dashboard.multi_tenant?
118
+
119
+ {
120
+ id: current_owner.id,
121
+ name: current_owner.try(:name)
122
+ }
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Controller for viewing telemetry traces.
6
+ #
7
+ # Provides:
8
+ # - List view with filtering and pagination
9
+ # - Detail view with span timeline
10
+ # - Metrics overview
11
+ # - Live updates via Turbo Streams
12
+ class TracesController < ApplicationController
13
+ def index
14
+ @traces = fetch_traces
15
+ @metrics = calculate_metrics
16
+
17
+ respond_to do |format|
18
+ format.html
19
+ format.turbo_stream
20
+ end
21
+ end
22
+
23
+ def show
24
+ @trace = ActiveAgent::TelemetryTrace.find(params[:id])
25
+ end
26
+
27
+ def metrics
28
+ @metrics = calculate_metrics
29
+ @agent_stats = agent_statistics
30
+ @time_series = time_series_data
31
+
32
+ respond_to do |format|
33
+ format.html
34
+ format.turbo_stream
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def fetch_traces
41
+ traces = ActiveAgent::TelemetryTrace.recent
42
+
43
+ traces = traces.for_agent(params[:agent]) if params[:agent].present?
44
+ traces = traces.with_errors if params[:status] == "error"
45
+ traces = traces.for_service(params[:service]) if params[:service].present?
46
+
47
+ if params[:start_date].present? && params[:end_date].present?
48
+ traces = traces.for_date_range(
49
+ Time.parse(params[:start_date]),
50
+ Time.parse(params[:end_date])
51
+ )
52
+ end
53
+
54
+ traces.limit(params[:limit] || 50)
55
+ end
56
+
57
+ def calculate_metrics
58
+ traces = ActiveAgent::TelemetryTrace.where(
59
+ "created_at > ?", 24.hours.ago
60
+ )
61
+
62
+ {
63
+ total_traces: traces.count,
64
+ total_tokens: traces.sum(:total_input_tokens) + traces.sum(:total_output_tokens),
65
+ avg_duration_ms: traces.average(:total_duration_ms)&.round(2) || 0,
66
+ error_rate: calculate_error_rate(traces),
67
+ unique_agents: traces.distinct.count(:agent_class)
68
+ }
69
+ end
70
+
71
+ def calculate_error_rate(traces)
72
+ total = traces.count
73
+ return 0.0 if total.zero?
74
+
75
+ errors = traces.with_errors.count
76
+ ((errors.to_f / total) * 100).round(2)
77
+ end
78
+
79
+ def agent_statistics
80
+ ActiveAgent::TelemetryTrace
81
+ .where("created_at > ?", 24.hours.ago)
82
+ .group(:agent_class)
83
+ .select(
84
+ "agent_class",
85
+ "COUNT(*) as trace_count",
86
+ "SUM(total_input_tokens + total_output_tokens) as total_tokens",
87
+ "AVG(total_duration_ms) as avg_duration",
88
+ "SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END) as error_count"
89
+ )
90
+ end
91
+
92
+ def time_series_data
93
+ ActiveAgent::TelemetryTrace
94
+ .where("created_at > ?", 1.hour.ago)
95
+ .group_by_minute(:created_at)
96
+ .count
97
+ rescue NoMethodError
98
+ # Fallback if groupdate gem not available
99
+ {}
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Background job for executing agent runs.
6
+ #
7
+ # Handles the actual execution of an agent with the given input,
8
+ # updating the AgentRun record with results.
9
+ #
10
+ class AgentExecutionJob < ApplicationJob
11
+ queue_as :default
12
+
13
+ def perform(agent_run_id)
14
+ run = AgentRun.find(agent_run_id)
15
+ return if run.finished?
16
+
17
+ run.update!(status: :running, started_at: Time.current)
18
+
19
+ begin
20
+ agent = run.agent
21
+ result = execute_agent(agent, run.input_prompt, **(run.input_params || {}))
22
+
23
+ run.update!(
24
+ output: result[:output],
25
+ output_metadata: result[:metadata],
26
+ status: :complete,
27
+ completed_at: Time.current,
28
+ duration_ms: ((Time.current - run.started_at) * 1000).to_i,
29
+ input_tokens: result.dig(:usage, :input_tokens),
30
+ output_tokens: result.dig(:usage, :output_tokens),
31
+ total_tokens: result.dig(:usage, :total_tokens)
32
+ )
33
+ rescue => e
34
+ run.update!(
35
+ status: :failed,
36
+ completed_at: Time.current,
37
+ error_message: e.message,
38
+ error_backtrace: e.backtrace&.first(10)&.join("\n")
39
+ )
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def execute_agent(agent, input_prompt, **params)
46
+ # TODO: Build and execute actual ActiveAgent::Base subclass
47
+ # For now, return mock data
48
+ {
49
+ output: "Executed: #{input_prompt}",
50
+ metadata: { provider: agent.provider, model: agent.model },
51
+ usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300 }
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Base class for all Dashboard engine jobs.
6
+ class ApplicationJob < ActiveJob::Base
7
+ # Retry failed jobs
8
+ retry_on StandardError, wait: :polynomially_longer, attempts: 3
9
+
10
+ # Discard jobs for records that no longer exist
11
+ discard_on ActiveRecord::RecordNotFound
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Background job for cleaning up sandbox environments.
6
+ #
7
+ # Removes containers, Cloud Run jobs, or other resources
8
+ # when a sandbox session expires.
9
+ #
10
+ class SandboxCleanupJob < ApplicationJob
11
+ queue_as :default
12
+
13
+ def perform(sandbox_session_id)
14
+ session = SandboxSession.find_by(id: sandbox_session_id)
15
+ return unless session
16
+
17
+ cleanup_sandbox(session)
18
+ end
19
+
20
+ private
21
+
22
+ def cleanup_sandbox(session)
23
+ case ActiveAgent::Dashboard.sandbox_service
24
+ when :cloud_run
25
+ cleanup_cloud_run(session)
26
+ when :kubernetes
27
+ cleanup_kubernetes(session)
28
+ else
29
+ cleanup_local(session)
30
+ end
31
+ end
32
+
33
+ def cleanup_local(session)
34
+ # Local mode: Nothing to clean up
35
+ Rails.logger.info "[ActiveAgent::Dashboard] Cleaned up local sandbox: #{session.session_id}"
36
+ end
37
+
38
+ def cleanup_cloud_run(session)
39
+ # TODO: Implement Cloud Run cleanup
40
+ Rails.logger.info "[ActiveAgent::Dashboard] Would clean up Cloud Run job: #{session.cloud_run_job_id}"
41
+ end
42
+
43
+ def cleanup_kubernetes(session)
44
+ # TODO: Implement Kubernetes cleanup
45
+ Rails.logger.info "[ActiveAgent::Dashboard] Would clean up Kubernetes pod: #{session.cloud_run_job_id}"
46
+ end
47
+ end
48
+ end
49
+ end