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.
- checksums.yaml +4 -4
- data/README.md +10 -4
- data/lib/active_agent/base.rb +3 -2
- data/lib/active_agent/concerns/provider.rb +6 -2
- data/lib/active_agent/concerns/rescue.rb +39 -0
- data/lib/active_agent/concerns/streaming.rb +2 -1
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
- data/lib/active_agent/dashboard/config/routes.rb +78 -0
- data/lib/active_agent/dashboard/engine.rb +39 -0
- data/lib/active_agent/dashboard.rb +151 -0
- data/lib/active_agent/providers/_base_provider.rb +2 -1
- data/lib/active_agent/providers/anthropic_provider.rb +14 -4
- data/lib/active_agent/providers/azure/_types.rb +5 -0
- data/lib/active_agent/providers/azure/options.rb +111 -0
- data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_provider.rb +133 -0
- data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
- data/lib/active_agent/providers/bedrock/_types.rb +8 -0
- data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
- data/lib/active_agent/providers/bedrock/options.rb +77 -0
- data/lib/active_agent/providers/bedrock_provider.rb +84 -0
- data/lib/active_agent/providers/common/messages/_types.rb +6 -2
- data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
- data/lib/active_agent/providers/gemini/_types.rb +19 -0
- data/lib/active_agent/providers/gemini/options.rb +41 -0
- data/lib/active_agent/providers/gemini_provider.rb +94 -0
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
- data/lib/active_agent/providers/open_ai/chat_provider.rb +2 -0
- data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
- data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
- data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
- data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
- data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
- data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
- data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
- data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
- data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
- data/lib/active_agent/railtie.rb +32 -1
- data/lib/active_agent/telemetry/configuration.rb +213 -0
- data/lib/active_agent/telemetry/instrumentation.rb +155 -0
- data/lib/active_agent/telemetry/reporter.rb +176 -0
- data/lib/active_agent/telemetry/span.rb +267 -0
- data/lib/active_agent/telemetry/tracer.rb +184 -0
- data/lib/active_agent/telemetry.rb +162 -0
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +2 -0
- data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
- data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
- data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
- metadata +99 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a1deba19c874b6abb36731bcee93bddf79bc193a3443a234cc811d9a4cc8383
|
|
4
|
+
data.tar.gz: 6f8847ab0d5eebf0ce26a8425f7630c884f0b9af71384c67325e3c06a8b0a8ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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="
|
|
3
|
-
<img alt="
|
|
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/
|
|
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
|
|
data/lib/active_agent/base.rb
CHANGED
|
@@ -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
|
|
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"
|
|
13
|
-
"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
|
-
|
|
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)
|
data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb
ADDED
|
@@ -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
|
data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb
ADDED
|
@@ -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
|
data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb
ADDED
|
@@ -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
|