activeagent 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/README.md +26 -0
- data/lib/active_agent/base.rb +4 -0
- data/lib/active_agent/concerns/view.rb +1 -1
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +27 -6
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +11 -1
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +15 -12
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +27 -7
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +9 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +1 -1
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +18 -2
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +15 -3
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +3 -1
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +4 -4
- data/lib/active_agent/dashboard/config/routes.rb +5 -64
- data/lib/active_agent/dashboard/engine.rb +19 -15
- data/lib/active_agent/dashboard.rb +13 -3
- data/lib/active_agent/model_capabilities.rb +89 -0
- data/lib/active_agent/providers/_base_provider.rb +23 -2
- data/lib/active_agent/providers/anthropic/request.rb +3 -1
- data/lib/active_agent/providers/anthropic/transforms.rb +88 -0
- data/lib/active_agent/providers/bedrock_provider.rb +2 -2
- data/lib/active_agent/providers/concerns/exception_handler.rb +12 -1
- data/lib/active_agent/providers/errors.rb +140 -0
- data/lib/active_agent/providers/mock/request.rb +1 -4
- data/lib/active_agent/providers/ollama/chat/transforms.rb +9 -3
- data/lib/active_agent/providers/open_ai/chat_provider.rb +3 -3
- data/lib/active_agent/providers/requesty/_types.rb +16 -0
- data/lib/active_agent/providers/requesty/options.rb +39 -0
- data/lib/active_agent/providers/requesty_provider.rb +63 -0
- data/lib/active_agent/railtie.rb +5 -0
- data/lib/active_agent/telemetry/configuration.rb +112 -172
- data/lib/active_agent/telemetry/instrumentation.rb +137 -14
- data/lib/active_agent/telemetry/reporter.rb +12 -165
- data/lib/active_agent/telemetry/span.rb +18 -245
- data/lib/active_agent/telemetry/tracer.rb +67 -70
- data/lib/active_agent/telemetry.rb +1 -2
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +5 -0
- data/lib/generators/active_agent/dashboard/install_generator.rb +30 -2
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +41 -4
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +20 -4
- metadata +39 -16
- data/lib/generators/active_agent/dashboard/install/install_generator.rb +0 -96
- data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +0 -89
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +0 -42
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +0 -38
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +0 -22
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +0 -53
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +0 -28
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +0 -43
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +0 -44
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +0 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f352ce28ead5e975e8d64c76c4a263c1ac7cac3b202ea6fef1a137f899e3aea0
|
|
4
|
+
data.tar.gz: d163e4360d38fdf440465ec0caf76dc9c196cab342de0de4cf67de020b31d0e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8aed9bb0e1d7bec5b5e178940e4e43c8a117c82d36db703d34cbef485b0492d29d628350bb956cf709ee504a81327e152d5510d3c1b81cf43d5ecc70f33ffae8
|
|
7
|
+
data.tar.gz: 6d8b3c9b6d6f7286985206a42db8949067dbb46bc39db67e4eb507ccc1a9dd405ed46720c06878c4d5ea748f8da184072ce4e9bf9951361d4fe5172ae65770d8
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,77 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-08-12
|
|
9
|
+
|
|
10
|
+
### Dashboard — self-hosted (enterprise) mount readiness
|
|
11
|
+
|
|
12
|
+
The engine can now be mounted in any Rails app as the self-hosted
|
|
13
|
+
observability surface (see `docs/framework/self-hosted-observability.md`):
|
|
14
|
+
|
|
15
|
+
- **One install generator**: the duplicate `active_agent:dashboard:install`
|
|
16
|
+
variant that copied eight migrations (agents, sandboxes, recordings —
|
|
17
|
+
tables for models with no shipped controllers or routes) is removed.
|
|
18
|
+
The surviving generator installs the telemetry traces table only and
|
|
19
|
+
gains `--skip_migrations` / `--skip_routes`; its initializer template now
|
|
20
|
+
covers authentication, `ingest_api_key`, and multi-tenant options.
|
|
21
|
+
- **Canonical mount path is `/activeagents`** (generator, dummy app and
|
|
22
|
+
docs updated). `Telemetry::Configuration#resolved_endpoint` now reports
|
|
23
|
+
the ingest path for wherever the engine is actually mounted — any mount
|
|
24
|
+
path, including `/` on a dedicated subdomain — instead of a hardcoded
|
|
25
|
+
constant, falling back to `LOCAL_ENDPOINT_PATH` when it isn't mounted.
|
|
26
|
+
Note this is informational: `local_storage` capture writes through the
|
|
27
|
+
trace model without HTTP, and remote apps set `endpoint:` explicitly.
|
|
28
|
+
- **`TracesController` honors configuration**: index/metrics/time-series
|
|
29
|
+
queries now go through `ActiveAgent::Dashboard.trace_model` (previously
|
|
30
|
+
only `show` did) and are scoped with `for_account(current_owner)`, so a
|
|
31
|
+
`trace_model_class` override and multi-tenant scoping apply everywhere.
|
|
32
|
+
- **Single-tenant ingest auth**: new `config.ingest_api_key` requires a
|
|
33
|
+
matching Bearer token on `POST <mount>/api/traces` when set. The
|
|
34
|
+
telemetry reporter and ruby_llm_telemetry already send their `api_key`
|
|
35
|
+
as a Bearer header, so remote apps need no changes.
|
|
36
|
+
- **Metrics page no longer 500s with data**: the per-agent stats table
|
|
37
|
+
read a grouped SQL alias through a model method that expected per-trace
|
|
38
|
+
token columns.
|
|
39
|
+
- **Mount detection is route-set based**: the ingest path is resolved by
|
|
40
|
+
locating the mounted engine in the host's routes rather than assuming
|
|
41
|
+
the default `active_agent_path` helper, so `mount ... => "/", as:
|
|
42
|
+
:something_else` and constraint-wrapped (subdomain) mounts resolve
|
|
43
|
+
correctly instead of silently falling back.
|
|
44
|
+
- Deprecated the never-consumed `base_controller_class` config attribute:
|
|
45
|
+
it remains a no-op accessor with its historical default so existing
|
|
46
|
+
initializers keep booting, and will be removed in the next major.
|
|
47
|
+
|
|
48
|
+
### Dashboard & Telemetry — dev console readiness
|
|
49
|
+
|
|
50
|
+
The dashboard engine — Active Agent's local dev console — now works out of
|
|
51
|
+
the box (production observability is the hosted platform product):
|
|
52
|
+
|
|
53
|
+
- **Engine load paths fixed**: `Engine.find_root` now points at the
|
|
54
|
+
dashboard directory, so `ActiveAgent::TelemetryTrace`,
|
|
55
|
+
`ProcessTelemetryTracesJob`, the API controller, views and engine routes
|
|
56
|
+
are auto-discovered in host apps (previously they required manual
|
|
57
|
+
`require`s). The engine is also required eagerly with Rails, since
|
|
58
|
+
engines defined lazily miss initializer collection.
|
|
59
|
+
- **Routes now match shipped controllers**: the engine exposes traces,
|
|
60
|
+
metrics and the ingest API (`<mount>/api/traces`); routes to
|
|
61
|
+
never-shipped controllers (agents, sandboxes, templates, recordings,
|
|
62
|
+
api/v1) were removed. Engine root renders the traces index.
|
|
63
|
+
- **`local_storage` telemetry mode fixed**: tracer payloads are
|
|
64
|
+
symbol-keyed and were silently dropped by the string-keyed ingestion
|
|
65
|
+
normalizer; the reporter now stringifies and honors
|
|
66
|
+
`ActiveAgent::Dashboard.trace_model` overrides.
|
|
67
|
+
- **Token totals no longer double-count**: instrumentation mirrors LLM
|
|
68
|
+
token usage onto the root span; `TelemetryTrace.create_from_payload`
|
|
69
|
+
now counts child spans as the source of truth.
|
|
70
|
+
- **Span waterfall renders real offsets** (was pinned to 0ms), turbo-rails
|
|
71
|
+
is now optional (previously 500s without it), layout route helpers fixed,
|
|
72
|
+
`Agent.for_owner` scope added, synchronous ingest capped at 100
|
|
73
|
+
traces/request.
|
|
74
|
+
- **New docs** (`docs/framework/dashboard.md`, README section) covering
|
|
75
|
+
install, authentication (none by default — see docs), remote ingestion
|
|
76
|
+
and multi-tenant mode; dashboard engine test suite added
|
|
77
|
+
(`test/dashboard/`).
|
|
78
|
+
|
|
8
79
|
## [1.0.0] - 2025-11-21
|
|
9
80
|
|
|
10
81
|
Major refactor with breaking changes. Complete provider rewrite. New modular architecture.
|
data/README.md
CHANGED
|
@@ -127,6 +127,32 @@ development:
|
|
|
127
127
|
service: "RubyLLM"
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
## Dev Console & Observability
|
|
131
|
+
|
|
132
|
+
Active Agent includes a local dev console — traces with span waterfalls,
|
|
133
|
+
token usage, and per-agent metrics — as a mountable Rails engine, so you
|
|
134
|
+
can watch your agents while you build:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
rails generate active_agent:dashboard:install
|
|
138
|
+
rails db:migrate
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# config/active_agent.yml
|
|
143
|
+
telemetry:
|
|
144
|
+
enabled: true
|
|
145
|
+
local_storage: true
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Open `/activeagents` and every generation appears as a trace. See
|
|
149
|
+
[docs/framework/dashboard.md](docs/framework/dashboard.md) for
|
|
150
|
+
authentication, remote ingestion, and multi-tenant mode. For production
|
|
151
|
+
monitoring — evaluations, cost estimates, retention, team workspaces —
|
|
152
|
+
point telemetry at the hosted platform at
|
|
153
|
+
[activeagents.ai](https://activeagents.ai); every workspace starts with a
|
|
154
|
+
free low-volume trial.
|
|
155
|
+
|
|
130
156
|
## Features
|
|
131
157
|
|
|
132
158
|
- **Agent-Oriented Programming**: Build AI applications using familiar Rails patterns
|
data/lib/active_agent/base.rb
CHANGED
|
@@ -304,6 +304,10 @@ module ActiveAgent
|
|
|
304
304
|
# Render out proc/lamda attributes before rendering templates
|
|
305
305
|
parameters.deep_transform_values! { _1.respond_to?(:call) ? _1.call : _1 }
|
|
306
306
|
|
|
307
|
+
# Strip parameters the target model rejects (e.g. temperature/top_p
|
|
308
|
+
# on thinking-first models) before they reach the provider.
|
|
309
|
+
ModelCapabilities.sanitize!(parameters)
|
|
310
|
+
|
|
307
311
|
# Apply Callbacks
|
|
308
312
|
parameters.merge!(
|
|
309
313
|
trace_id: prompt_options[:trace_id] || SecureRandom.uuid,
|
|
@@ -142,7 +142,7 @@ module ActiveAgent
|
|
|
142
142
|
|
|
143
143
|
return if !strict && !lookup_context.exists?(template_name, _prefixes, false, [], **options)
|
|
144
144
|
|
|
145
|
-
template = lookup_context.
|
|
145
|
+
template = lookup_context.find(template_name, _prefixes, false, [], **options)
|
|
146
146
|
|
|
147
147
|
render_to_string(template: template.virtual_path, locals:, layout: false).chomp.presence
|
|
148
148
|
end
|
data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb
CHANGED
|
@@ -9,11 +9,14 @@ module ActiveAgent
|
|
|
9
9
|
# for analysis and visualization in the dashboard.
|
|
10
10
|
#
|
|
11
11
|
# Supports two modes:
|
|
12
|
-
# - Local mode:
|
|
13
|
-
#
|
|
12
|
+
# - Local mode: synchronous processing; unauthenticated unless
|
|
13
|
+
# ActiveAgent::Dashboard.ingest_api_key is set (set it whenever the
|
|
14
|
+
# mount is reachable beyond your own machine)
|
|
15
|
+
# - Multi-tenant mode: per-account Bearer token auth, async processing
|
|
16
|
+
# via job
|
|
14
17
|
#
|
|
15
18
|
# @example Local mode request
|
|
16
|
-
# POST /
|
|
19
|
+
# POST <mount>/api/traces (e.g. /activeagents/api/traces)
|
|
17
20
|
# Content-Type: application/json
|
|
18
21
|
#
|
|
19
22
|
# {
|
|
@@ -22,7 +25,7 @@ module ActiveAgent
|
|
|
22
25
|
# }
|
|
23
26
|
#
|
|
24
27
|
# @example Multi-tenant mode request
|
|
25
|
-
# POST /
|
|
28
|
+
# POST <mount>/api/traces (e.g. /activeagents/api/traces)
|
|
26
29
|
# Authorization: Bearer <api_key>
|
|
27
30
|
# Content-Type: application/json
|
|
28
31
|
#
|
|
@@ -33,10 +36,15 @@ module ActiveAgent
|
|
|
33
36
|
#
|
|
34
37
|
class TracesController < ActionController::API
|
|
35
38
|
before_action :authenticate_api_key!, if: -> { ActiveAgent::Dashboard.multi_tenant? }
|
|
39
|
+
before_action :authenticate_ingest_key!, unless: -> { ActiveAgent::Dashboard.multi_tenant? }
|
|
36
40
|
|
|
37
|
-
#
|
|
41
|
+
# Maximum traces accepted per request (mirrors
|
|
42
|
+
# ProcessTelemetryTracesJob::MAX_TRACES_PER_JOB).
|
|
43
|
+
MAX_TRACES_PER_REQUEST = 100
|
|
44
|
+
|
|
45
|
+
# POST <mount>/api/traces (e.g. /activeagents/api/traces)
|
|
38
46
|
def create
|
|
39
|
-
traces = params[:traces]
|
|
47
|
+
traces = Array(params[:traces]).take(MAX_TRACES_PER_REQUEST)
|
|
40
48
|
sdk_info = params[:sdk] || {}
|
|
41
49
|
|
|
42
50
|
return head :accepted if traces.empty?
|
|
@@ -86,6 +94,19 @@ module ActiveAgent
|
|
|
86
94
|
@account.increment_telemetry_usage! if @account.respond_to?(:increment_telemetry_usage!)
|
|
87
95
|
end
|
|
88
96
|
|
|
97
|
+
# Requires the configured single-tenant ingest key when one is set.
|
|
98
|
+
# The telemetry reporter and ruby_llm_telemetry both send their
|
|
99
|
+
# api_key as a Bearer header, so remote apps work unchanged.
|
|
100
|
+
def authenticate_ingest_key!
|
|
101
|
+
expected = ActiveAgent::Dashboard.ingest_api_key
|
|
102
|
+
return if expected.blank?
|
|
103
|
+
|
|
104
|
+
token = extract_bearer_token
|
|
105
|
+
return if token.present? && ActiveSupport::SecurityUtils.secure_compare(token, expected)
|
|
106
|
+
|
|
107
|
+
render json: { error: "Invalid API key" }, status: :unauthorized
|
|
108
|
+
end
|
|
109
|
+
|
|
89
110
|
# Extracts Bearer token from Authorization header.
|
|
90
111
|
def extract_bearer_token
|
|
91
112
|
auth_header = request.headers["Authorization"]
|
data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb
CHANGED
|
@@ -21,7 +21,17 @@ module ActiveAgent
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
23
|
def authenticate_dashboard!
|
|
24
|
-
|
|
24
|
+
if ActiveAgent::Dashboard.authentication_method.nil?
|
|
25
|
+
# Traces contain prompts, outputs, and error backtraces. Refuse to
|
|
26
|
+
# serve them unauthenticated in production rather than exposing
|
|
27
|
+
# them to the internet by default.
|
|
28
|
+
if Rails.env.production?
|
|
29
|
+
render plain: "ActiveAgent Dashboard: set ActiveAgent::Dashboard.authentication_method " \
|
|
30
|
+
"(see docs/framework/dashboard.md) to enable access in production.",
|
|
31
|
+
status: :forbidden
|
|
32
|
+
end
|
|
33
|
+
return
|
|
34
|
+
end
|
|
25
35
|
|
|
26
36
|
result = ActiveAgent::Dashboard.authentication_method.call(self)
|
|
27
37
|
head :unauthorized unless result
|
data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb
CHANGED
|
@@ -6,23 +6,26 @@ module ActiveAgent
|
|
|
6
6
|
#
|
|
7
7
|
class DashboardController < ApplicationController
|
|
8
8
|
def index
|
|
9
|
+
unless ActiveAgent::Dashboard.use_inertia && defined?(InertiaRails)
|
|
10
|
+
# No ERB overview ships yet — the traces list is the dashboard.
|
|
11
|
+
# Loading agent/run data here would also require the full
|
|
12
|
+
# dashboard tables, which telemetry-only installs don't have.
|
|
13
|
+
return redirect_to(traces_path)
|
|
14
|
+
end
|
|
15
|
+
|
|
9
16
|
@agents = fetch_agents.limit(10)
|
|
10
17
|
@recent_runs = fetch_recent_runs.limit(10)
|
|
11
18
|
@recent_traces = fetch_recent_traces.limit(10)
|
|
12
19
|
@metrics = calculate_metrics
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
else
|
|
24
|
-
render :index
|
|
25
|
-
end
|
|
21
|
+
render inertia: "Dashboard", props: {
|
|
22
|
+
agents: serialize_agents(@agents),
|
|
23
|
+
recentRuns: serialize_runs(@recent_runs),
|
|
24
|
+
recentTraces: serialize_traces(@recent_traces),
|
|
25
|
+
metrics: @metrics,
|
|
26
|
+
user: current_user_props,
|
|
27
|
+
account: current_account_props
|
|
28
|
+
}
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
private
|
|
@@ -16,12 +16,14 @@ module ActiveAgent
|
|
|
16
16
|
|
|
17
17
|
respond_to do |format|
|
|
18
18
|
format.html
|
|
19
|
-
|
|
19
|
+
# turbo-rails is optional; responding to an unregistered MIME
|
|
20
|
+
# type raises in host apps without it.
|
|
21
|
+
format.turbo_stream if turbo_stream_available?
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def show
|
|
24
|
-
@trace =
|
|
26
|
+
@trace = scoped_traces.find(params[:id])
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def metrics
|
|
@@ -31,14 +33,32 @@ module ActiveAgent
|
|
|
31
33
|
|
|
32
34
|
respond_to do |format|
|
|
33
35
|
format.html
|
|
34
|
-
format.turbo_stream
|
|
36
|
+
format.turbo_stream if turbo_stream_available?
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
private
|
|
39
41
|
|
|
42
|
+
def turbo_stream_available?
|
|
43
|
+
Mime::Type.lookup_by_extension(:turbo_stream).present?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# All queries honor the configured trace model and, in multi-tenant
|
|
47
|
+
# mode, the current owner's account (for_account no-ops otherwise).
|
|
48
|
+
#
|
|
49
|
+
# In multi-tenant mode an unresolvable owner returns nothing rather
|
|
50
|
+
# than falling through: current_owner degrades to current_user when
|
|
51
|
+
# current_account_method is unset, which would scope by a user id
|
|
52
|
+
# against account_id — another tenant's traces.
|
|
53
|
+
def scoped_traces
|
|
54
|
+
model = ActiveAgent::Dashboard.trace_model
|
|
55
|
+
return model.none if ActiveAgent::Dashboard.multi_tenant? && current_owner.nil?
|
|
56
|
+
|
|
57
|
+
model.for_account(current_owner)
|
|
58
|
+
end
|
|
59
|
+
|
|
40
60
|
def fetch_traces
|
|
41
|
-
traces =
|
|
61
|
+
traces = scoped_traces.recent
|
|
42
62
|
|
|
43
63
|
traces = traces.for_agent(params[:agent]) if params[:agent].present?
|
|
44
64
|
traces = traces.with_errors if params[:status] == "error"
|
|
@@ -55,7 +75,7 @@ module ActiveAgent
|
|
|
55
75
|
end
|
|
56
76
|
|
|
57
77
|
def calculate_metrics
|
|
58
|
-
traces =
|
|
78
|
+
traces = scoped_traces.where(
|
|
59
79
|
"created_at > ?", 24.hours.ago
|
|
60
80
|
)
|
|
61
81
|
|
|
@@ -77,7 +97,7 @@ module ActiveAgent
|
|
|
77
97
|
end
|
|
78
98
|
|
|
79
99
|
def agent_statistics
|
|
80
|
-
|
|
100
|
+
scoped_traces
|
|
81
101
|
.where("created_at > ?", 24.hours.ago)
|
|
82
102
|
.group(:agent_class)
|
|
83
103
|
.select(
|
|
@@ -90,7 +110,7 @@ module ActiveAgent
|
|
|
90
110
|
end
|
|
91
111
|
|
|
92
112
|
def time_series_data
|
|
93
|
-
|
|
113
|
+
scoped_traces
|
|
94
114
|
.where("created_at > ?", 1.hour.ago)
|
|
95
115
|
.group_by_minute(:created_at)
|
|
96
116
|
.count
|
|
@@ -39,7 +39,16 @@ module ActiveAgent
|
|
|
39
39
|
return
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Process in bounded slices; anything beyond the cap is re-enqueued
|
|
43
|
+
# rather than silently dropped (the client already received 202).
|
|
44
|
+
remainder = traces.drop(MAX_TRACES_PER_JOB)
|
|
42
45
|
traces = traces.take(MAX_TRACES_PER_JOB)
|
|
46
|
+
if remainder.any?
|
|
47
|
+
self.class.perform_later(
|
|
48
|
+
account_id: account_id, traces: remainder,
|
|
49
|
+
sdk_info: sdk_info, received_at: received_at
|
|
50
|
+
)
|
|
51
|
+
end
|
|
43
52
|
|
|
44
53
|
traces.each do |trace|
|
|
45
54
|
process_trace(trace, sdk_info, account)
|
|
@@ -71,7 +71,7 @@ module ActiveAgent
|
|
|
71
71
|
].freeze
|
|
72
72
|
|
|
73
73
|
# Available providers
|
|
74
|
-
PROVIDERS = %w[openai anthropic ollama openrouter].freeze
|
|
74
|
+
PROVIDERS = %w[openai anthropic ollama openrouter requesty].freeze
|
|
75
75
|
|
|
76
76
|
# Returns the configuration as a hash for versioning
|
|
77
77
|
def configuration_snapshot
|
|
@@ -54,13 +54,20 @@ module ActiveAgent
|
|
|
54
54
|
spans = trace["spans"] || []
|
|
55
55
|
root_span = spans.find { |s| s["parent_span_id"].nil? } || spans.first || {}
|
|
56
56
|
|
|
57
|
-
# Calculate totals from all spans
|
|
58
57
|
total_duration = root_span["duration_ms"]
|
|
58
|
+
|
|
59
|
+
# Instrumentation mirrors LLM token usage onto the root span for
|
|
60
|
+
# display, so summing every span double-counts. When child spans
|
|
61
|
+
# carry token data, they are the source of truth; the root span only
|
|
62
|
+
# counts for single-span traces.
|
|
63
|
+
counted_spans = spans.reject { |s| s["parent_span_id"].nil? }
|
|
64
|
+
counted_spans = spans if counted_spans.none? { |s| span_token_sum(s).positive? }
|
|
65
|
+
|
|
59
66
|
total_input = 0
|
|
60
67
|
total_output = 0
|
|
61
68
|
total_thinking = 0
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
counted_spans.each do |span|
|
|
64
71
|
tokens = span["tokens"] || {}
|
|
65
72
|
total_input += (tokens["input"] || 0)
|
|
66
73
|
total_output += (tokens["output"] || 0)
|
|
@@ -100,6 +107,15 @@ module ActiveAgent
|
|
|
100
107
|
create!(attrs)
|
|
101
108
|
end
|
|
102
109
|
|
|
110
|
+
# Sums a span's token counts (used to decide which spans carry the
|
|
111
|
+
# authoritative token data during ingestion).
|
|
112
|
+
#
|
|
113
|
+
# @api private
|
|
114
|
+
def self.span_token_sum(span)
|
|
115
|
+
tokens = span["tokens"] || {}
|
|
116
|
+
tokens.fetch("input", 0).to_i + tokens.fetch("output", 0).to_i + tokens.fetch("thinking", 0).to_i
|
|
117
|
+
end
|
|
118
|
+
|
|
103
119
|
# Returns the root span of this trace.
|
|
104
120
|
#
|
|
105
121
|
# @return [Hash, nil] The root span or nil
|
data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb
CHANGED
|
@@ -59,11 +59,23 @@
|
|
|
59
59
|
<div class="mt-4">
|
|
60
60
|
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Span Timeline</h4>
|
|
61
61
|
<div class="relative bg-gray-100 dark:bg-gray-800 rounded-lg p-4">
|
|
62
|
-
<% total_duration = trace.total_duration_ms || 1 %>
|
|
62
|
+
<% total_duration = (trace.total_duration_ms || 1).to_f %>
|
|
63
|
+
<% total_duration = 1.0 if total_duration.zero? %>
|
|
64
|
+
<%
|
|
65
|
+
# Offsets relative to the root span's start. Span payloads carry
|
|
66
|
+
# absolute ISO8601 start_time values, not relative offsets.
|
|
67
|
+
root = trace.root_span || trace.spans.first || {}
|
|
68
|
+
root_start = Time.parse(root["start_time"]) rescue nil
|
|
69
|
+
%>
|
|
63
70
|
<% trace.spans.each_with_index do |span, index| %>
|
|
64
71
|
<%
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
relative_ms = span["start_time_relative_ms"]
|
|
73
|
+
if relative_ms.nil? && root_start && span["start_time"].present?
|
|
74
|
+
span_start = Time.parse(span["start_time"]) rescue nil
|
|
75
|
+
relative_ms = ((span_start - root_start) * 1000).round(2) if span_start
|
|
76
|
+
end
|
|
77
|
+
start_pct = ((relative_ms || 0).to_f / total_duration * 100).clamp(0, 100)
|
|
78
|
+
width_pct = ((span["duration_ms"] || 0).to_f / total_duration * 100).clamp(0.5, [ 100 - start_pct, 0.5 ].max)
|
|
67
79
|
span_type = span["type"] || "unknown"
|
|
68
80
|
colors = {
|
|
69
81
|
"root" => "bg-gray-400",
|
|
@@ -114,7 +114,9 @@
|
|
|
114
114
|
<%= number_with_delimiter(stat.trace_count) %>
|
|
115
115
|
</td>
|
|
116
116
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
117
|
-
|
|
117
|
+
<%# stat is a grouped row: read the SQL alias, not the model
|
|
118
|
+
method (which sums per-trace token columns absent here) %>
|
|
119
|
+
<%= number_to_human(stat[:total_tokens] || 0, precision: 1) %>
|
|
118
120
|
</td>
|
|
119
121
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
120
122
|
<% avg = stat.avg_duration.to_f %>
|
data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb
CHANGED
|
@@ -69,11 +69,11 @@
|
|
|
69
69
|
</div>
|
|
70
70
|
<div class="ml-10 flex items-baseline space-x-4">
|
|
71
71
|
<%= link_to "Traces",
|
|
72
|
-
|
|
73
|
-
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path ==
|
|
72
|
+
traces_path,
|
|
73
|
+
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path == traces_path ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'}" %>
|
|
74
74
|
<%= link_to "Metrics",
|
|
75
|
-
|
|
76
|
-
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path ==
|
|
75
|
+
metrics_traces_path,
|
|
76
|
+
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path == metrics_traces_path ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'}" %>
|
|
77
77
|
</div>
|
|
78
78
|
</div>
|
|
79
79
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
@@ -1,78 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
ActiveAgent::Dashboard::Engine.routes.draw do
|
|
4
|
-
#
|
|
5
|
-
root to: "
|
|
6
|
-
|
|
7
|
-
# Dashboard
|
|
4
|
+
# The functional dashboard surface: traces + metrics.
|
|
5
|
+
root to: "traces#index"
|
|
8
6
|
get "dashboard", to: "dashboard#index"
|
|
9
7
|
|
|
10
|
-
# Traces
|
|
11
8
|
resources :traces, only: [ :index, :show ] do
|
|
12
9
|
collection do
|
|
13
10
|
get :metrics
|
|
14
11
|
end
|
|
15
12
|
end
|
|
16
13
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
member do
|
|
20
|
-
post :execute
|
|
21
|
-
post :test
|
|
22
|
-
get :versions
|
|
23
|
-
post :restore_version
|
|
24
|
-
end
|
|
25
|
-
resources :runs, controller: "agent_runs", only: [ :index, :show ] do
|
|
26
|
-
member do
|
|
27
|
-
post :cancel
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Agent Templates
|
|
33
|
-
resources :templates, only: [ :index, :show ] do
|
|
34
|
-
member do
|
|
35
|
-
post :create_agent
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Sandbox Sessions
|
|
40
|
-
resources :sandboxes, controller: "sandbox_sessions" do
|
|
41
|
-
member do
|
|
42
|
-
post :provision
|
|
43
|
-
post :execute
|
|
44
|
-
post :expire
|
|
45
|
-
end
|
|
46
|
-
resources :runs, controller: "sandbox_runs", only: [ :index, :show ]
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Session Recordings
|
|
50
|
-
resources :recordings, controller: "session_recordings", only: [ :index, :show ] do
|
|
51
|
-
member do
|
|
52
|
-
get :timeline
|
|
53
|
-
get :playback
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# API namespace
|
|
14
|
+
# Telemetry ingestion, relative to wherever the engine is mounted:
|
|
15
|
+
# <mount>/api/traces (e.g. /activeagents/api/traces at the default mount).
|
|
58
16
|
namespace :api do
|
|
59
|
-
|
|
60
|
-
# Telemetry ingestion
|
|
61
|
-
resources :traces, only: [ :create ]
|
|
62
|
-
|
|
63
|
-
# Agent execution API
|
|
64
|
-
resources :agents, only: [ :index, :show ] do
|
|
65
|
-
member do
|
|
66
|
-
post :execute
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Sandbox API
|
|
71
|
-
resources :sandboxes, only: [ :create, :show ] do
|
|
72
|
-
member do
|
|
73
|
-
post :execute
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
17
|
+
resources :traces, only: [ :create ]
|
|
77
18
|
end
|
|
78
19
|
end
|
|
@@ -4,36 +4,40 @@ module ActiveAgent
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
# Rails Engine for the ActiveAgent Dashboard.
|
|
6
6
|
#
|
|
7
|
-
# Provides
|
|
8
|
-
#
|
|
7
|
+
# Provides the free self-hosted dashboard: telemetry traces, metrics,
|
|
8
|
+
# and the local trace ingestion API.
|
|
9
9
|
#
|
|
10
10
|
# Mount in your routes:
|
|
11
|
-
# mount ActiveAgent::Dashboard::Engine => "/
|
|
11
|
+
# mount ActiveAgent::Dashboard::Engine => "/activeagents"
|
|
12
12
|
#
|
|
13
13
|
class Engine < ::Rails::Engine
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
|
|
14
|
+
# The engine lives at lib/active_agent/dashboard rather than the gem
|
|
15
|
+
# root. Rails derives an engine's load paths (app/models,
|
|
16
|
+
# app/controllers, app/jobs, app/views, config/routes.rb) from
|
|
17
|
+
# find_root when +config+ is first materialized — which
|
|
18
|
+
# isolate_namespace below triggers via +routes+ — so this override
|
|
19
|
+
# must be defined before anything touches config. Overriding only
|
|
20
|
+
# +root+ leaves the engine's classes off host applications' autoload
|
|
21
|
+
# paths entirely.
|
|
22
|
+
def self.find_root(_from)
|
|
23
|
+
root
|
|
24
|
+
end
|
|
18
25
|
|
|
19
|
-
# Override engine root to point to the dashboard directory
|
|
20
|
-
# This must be set before paths are computed
|
|
21
26
|
def self.root
|
|
22
27
|
@root ||= Pathname.new(File.expand_path("..", __FILE__))
|
|
23
28
|
end
|
|
24
29
|
|
|
30
|
+
isolate_namespace ActiveAgent::Dashboard
|
|
31
|
+
|
|
32
|
+
# Use a unique engine name to avoid conflicts
|
|
33
|
+
engine_name "active_agent"
|
|
34
|
+
|
|
25
35
|
# Alias for consistency with existing code
|
|
26
36
|
def self.dashboard_root
|
|
27
37
|
root
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
config.active_agent_dashboard = ActiveSupport::OrderedOptions.new
|
|
31
|
-
|
|
32
|
-
initializer "active_agent.dashboard.append_view_paths" do
|
|
33
|
-
ActiveSupport.on_load(:action_controller) do
|
|
34
|
-
prepend_view_path ActiveAgent::Dashboard::Engine.root.join("app", "views").to_s
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
end
|
|
@@ -8,7 +8,7 @@ module ActiveAgent
|
|
|
8
8
|
# Mount the engine in your routes to access the full dashboard:
|
|
9
9
|
#
|
|
10
10
|
# # config/routes.rb
|
|
11
|
-
# mount ActiveAgent::Dashboard::Engine => "/
|
|
11
|
+
# mount ActiveAgent::Dashboard::Engine => "/activeagents"
|
|
12
12
|
#
|
|
13
13
|
# The dashboard provides:
|
|
14
14
|
# - Agent management: Create, edit, version, and execute agents
|
|
@@ -91,7 +91,16 @@ module ActiveAgent
|
|
|
91
91
|
# @return [Object, nil] Object responding to #signed_url_for and #fetch_snapshot
|
|
92
92
|
attr_accessor :storage_service
|
|
93
93
|
|
|
94
|
-
#
|
|
94
|
+
# Bearer token required by the ingest API in single-tenant mode. When
|
|
95
|
+
# unset the local ingest endpoint accepts unauthenticated posts, so set
|
|
96
|
+
# it whenever the mount is reachable beyond your own machine.
|
|
97
|
+
# (Multi-tenant mode authenticates per-account keys instead.)
|
|
98
|
+
# @return [String, nil]
|
|
99
|
+
attr_accessor :ingest_api_key
|
|
100
|
+
|
|
101
|
+
# @deprecated Never consumed — dashboard controllers inherit
|
|
102
|
+
# ActionController::Base. Retained as a no-op so existing
|
|
103
|
+
# initializers that set it keep booting; remove in the next major.
|
|
95
104
|
# @return [String]
|
|
96
105
|
attr_accessor :base_controller_class
|
|
97
106
|
|
|
@@ -141,7 +150,8 @@ module ActiveAgent
|
|
|
141
150
|
@sandbox_service = :local
|
|
142
151
|
@sandbox_limits = nil
|
|
143
152
|
@storage_service = nil
|
|
144
|
-
@
|
|
153
|
+
@ingest_api_key = nil
|
|
154
|
+
@base_controller_class = "ActionController::Base" # deprecated no-op
|
|
145
155
|
end
|
|
146
156
|
end
|
|
147
157
|
|