activeagent 1.0.0 → 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/CHANGELOG.md +71 -0
- 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 +4 -1
- data/lib/active_agent/providers/anthropic/options.rb +4 -6
- data/lib/active_agent/providers/anthropic/request.rb +28 -3
- data/lib/active_agent/providers/anthropic/transforms.rb +131 -2
- data/lib/active_agent/providers/anthropic_provider.rb +97 -30
- 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 +42 -31
- data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
- data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
- data/lib/active_agent/providers/concerns/previewable.rb +39 -5
- data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -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 +120 -4
- data/lib/active_agent/providers/open_ai/chat_provider.rb +40 -0
- data/lib/active_agent/providers/open_ai/responses/request.rb +17 -2
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +135 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +38 -0
- data/lib/active_agent/providers/open_router/request.rb +20 -0
- data/lib/active_agent/providers/open_router/transforms.rb +30 -0
- data/lib/active_agent/providers/open_router_provider.rb +14 -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 +101 -14
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/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,77 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
Major refactor with breaking changes. Complete provider rewrite. New modular architecture.
|
|
11
11
|
|
|
12
12
|
**Requirements:** Ruby 3.1+, Rails 7.0+/8.0+/8.1+
|
|
13
|
+
## What's Changed
|
|
14
|
+
* Major Framework Refactor: ActiveAgent v1.0.0 by @sirwolfgang in https://github.com/activeagents/activeagent/pull/259
|
|
15
|
+
* Add API gem version testing and fix Anthropic 1.14.0 compatibility by @sirwolfgang in https://github.com/activeagents/activeagent/pull/265
|
|
16
|
+
* Fix version compatiblity issue for vitepress by @sirwolfgang in https://github.com/activeagents/activeagent/pull/266
|
|
17
|
+
* Add missing API Keys by @sirwolfgang in https://github.com/activeagents/activeagent/pull/267
|
|
18
|
+
* Fix website links by @sirwolfgang in https://github.com/activeagents/activeagent/pull/268
|
|
19
|
+
* chore: remove `standard` from dev dependencies by @okuramasafumi in https://github.com/activeagents/activeagent/pull/272
|
|
20
|
+
* Add thread safety tests by @sirwolfgang in https://github.com/activeagents/activeagent/pull/275
|
|
21
|
+
* Refactor: Leverage Native Gem Types Across All Providers by @sirwolfgang in https://github.com/activeagents/activeagent/pull/271
|
|
22
|
+
* Improved Usage Tracking by @sirwolfgang in https://github.com/activeagents/activeagent/pull/274
|
|
23
|
+
|
|
24
|
+
## New Contributors
|
|
25
|
+
* @okuramasafumi made their first contribution in https://github.com/activeagents/activeagent/pull/272
|
|
26
|
+
|
|
27
|
+
**Full Changelog**: https://github.com/activeagents/activeagent/compare/v0.6.3...v1.0.0
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
**Universal Tools Format**
|
|
32
|
+
```ruby
|
|
33
|
+
# Single format works across all providers (Anthropic, OpenAI, OpenRouter, Ollama, Mock)
|
|
34
|
+
tools: [{
|
|
35
|
+
name: "get_weather",
|
|
36
|
+
description: "Get current weather",
|
|
37
|
+
parameters: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
location: { type: "string", description: "City and state" }
|
|
41
|
+
},
|
|
42
|
+
required: ["location"]
|
|
43
|
+
}
|
|
44
|
+
}]
|
|
45
|
+
|
|
46
|
+
# Tool choice normalization
|
|
47
|
+
tool_choice: "auto" # Let model decide
|
|
48
|
+
tool_choice: "required" # Force tool use
|
|
49
|
+
tool_choice: { name: "get_weather" } # Force specific tool
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Automatic conversion to provider-specific formats. Old formats still work (backward compatible).
|
|
53
|
+
|
|
54
|
+
**Model Context Protocol (MCP) Support**
|
|
55
|
+
```ruby
|
|
56
|
+
# Universal MCP format works across providers (Anthropic, OpenAI)
|
|
57
|
+
class MyAgent < ActiveAgent::Base
|
|
58
|
+
generate_with :anthropic, model: "claude-haiku-4-5"
|
|
59
|
+
|
|
60
|
+
def research
|
|
61
|
+
prompt(
|
|
62
|
+
message: "Research AI developments",
|
|
63
|
+
mcps: [{
|
|
64
|
+
name: "github",
|
|
65
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
66
|
+
authorization: ENV["GITHUB_MCP_TOKEN"]
|
|
67
|
+
}]
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Common format: `{name: "server", url: "https://...", authorization: "token"}`
|
|
74
|
+
- Auto-converts to provider native formats
|
|
75
|
+
- Anthropic: Beta API support, up to 20 servers per request
|
|
76
|
+
- OpenAI: Responses API with pre-built connectors (Dropbox, Google Drive, etc.)
|
|
77
|
+
- Backwards compatible: accepts both `mcps` and `mcp_servers` parameters
|
|
78
|
+
- Comprehensive documentation with tested examples
|
|
79
|
+
- Full VCR test coverage with real MCP endpoints
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- Shared `ToolChoiceClearing` concern eliminates duplication across providers
|
|
13
84
|
|
|
14
85
|
### Breaking Changes
|
|
15
86
|
|
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
|