activeagent 1.0.3 → 1.2.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 +132 -0
- data/README.md +39 -3
- data/lib/active_agent/base.rb +4 -0
- data/lib/active_agent/model_capabilities.rb +89 -0
- data/lib/active_agent/providers/_base_provider.rb +23 -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/ollama/chat/transforms.rb +9 -3
- data/lib/active_agent/railtie.rb +5 -0
- data/lib/active_agent/telemetry/configuration.rb +115 -171
- data/lib/active_agent/telemetry/instrumentation.rb +141 -14
- data/lib/active_agent/telemetry/reporter.rb +12 -165
- data/lib/active_agent/telemetry/span.rb +18 -245
- data/lib/active_agent/telemetry/tool_origin.rb +90 -0
- data/lib/active_agent/telemetry/tracer.rb +67 -70
- data/lib/active_agent/telemetry.rb +2 -2
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +1 -1
- metadata +38 -42
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +0 -117
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +0 -54
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +0 -126
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +0 -103
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +0 -56
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +0 -14
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +0 -49
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +0 -65
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +0 -77
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +0 -256
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +0 -113
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +0 -208
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +0 -60
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +0 -46
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +0 -125
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +0 -83
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +0 -52
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +0 -169
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +0 -193
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +0 -198
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +0 -105
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +0 -135
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +0 -143
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +0 -36
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +0 -94
- data/lib/active_agent/dashboard/config/routes.rb +0 -78
- data/lib/active_agent/dashboard/engine.rb +0 -39
- data/lib/active_agent/dashboard.rb +0 -151
- 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
- data/lib/generators/active_agent/dashboard/install_generator.rb +0 -64
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +0 -30
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +0 -30
|
@@ -1,103 +0,0 @@
|
|
|
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
|
|
@@ -1,56 +0,0 @@
|
|
|
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
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
|
@@ -1,49 +0,0 @@
|
|
|
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
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveAgent
|
|
4
|
-
module Dashboard
|
|
5
|
-
# Background job for provisioning sandbox environments.
|
|
6
|
-
#
|
|
7
|
-
# Creates isolated execution environments (Docker, Cloud Run, etc.)
|
|
8
|
-
# for running agents with tools.
|
|
9
|
-
#
|
|
10
|
-
class SandboxProvisionJob < ApplicationJob
|
|
11
|
-
queue_as :default
|
|
12
|
-
|
|
13
|
-
def perform(sandbox_session_id)
|
|
14
|
-
session = SandboxSession.find(sandbox_session_id)
|
|
15
|
-
return unless session.provisioning?
|
|
16
|
-
|
|
17
|
-
begin
|
|
18
|
-
result = provision_sandbox(session)
|
|
19
|
-
|
|
20
|
-
session.mark_ready!(
|
|
21
|
-
sandbox_url: result[:url],
|
|
22
|
-
sandbox_job_id: result[:job_id]
|
|
23
|
-
)
|
|
24
|
-
rescue => e
|
|
25
|
-
session.update!(
|
|
26
|
-
status: :failed,
|
|
27
|
-
error_message: e.message
|
|
28
|
-
)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def provision_sandbox(session)
|
|
35
|
-
case ActiveAgent::Dashboard.sandbox_service
|
|
36
|
-
when :cloud_run
|
|
37
|
-
provision_cloud_run(session)
|
|
38
|
-
when :kubernetes
|
|
39
|
-
provision_kubernetes(session)
|
|
40
|
-
else
|
|
41
|
-
provision_local(session)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def provision_local(session)
|
|
46
|
-
# Local mode: No actual provisioning needed
|
|
47
|
-
# The sandbox runs in the same process or via Docker
|
|
48
|
-
{
|
|
49
|
-
url: "http://localhost:#{3000 + session.id}",
|
|
50
|
-
job_id: "local-#{session.session_id}"
|
|
51
|
-
}
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def provision_cloud_run(session)
|
|
55
|
-
# TODO: Implement Cloud Run provisioning
|
|
56
|
-
raise NotImplementedError, "Cloud Run provisioning not yet implemented in engine"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def provision_kubernetes(session)
|
|
60
|
-
# TODO: Implement Kubernetes provisioning
|
|
61
|
-
raise NotImplementedError, "Kubernetes provisioning not yet implemented in engine"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveAgent
|
|
4
|
-
# Processes telemetry traces received from ActiveAgent clients.
|
|
5
|
-
#
|
|
6
|
-
# This job handles the asynchronous processing of trace data to avoid
|
|
7
|
-
# blocking the ingestion endpoint. It:
|
|
8
|
-
# - Creates TelemetryTrace records for each trace
|
|
9
|
-
# - Updates aggregate statistics
|
|
10
|
-
# - Handles any errors gracefully
|
|
11
|
-
#
|
|
12
|
-
# @example Local mode
|
|
13
|
-
# ActiveAgent::ProcessTelemetryTracesJob.perform_later(
|
|
14
|
-
# traces: [...],
|
|
15
|
-
# sdk_info: { name: "activeagent", version: "0.5.0" },
|
|
16
|
-
# received_at: "2024-01-15T10:30:00Z"
|
|
17
|
-
# )
|
|
18
|
-
#
|
|
19
|
-
# @example Multi-tenant mode
|
|
20
|
-
# ActiveAgent::ProcessTelemetryTracesJob.perform_later(
|
|
21
|
-
# account_id: 1,
|
|
22
|
-
# traces: [...],
|
|
23
|
-
# sdk_info: { name: "activeagent", version: "0.5.0" },
|
|
24
|
-
# received_at: "2024-01-15T10:30:00Z"
|
|
25
|
-
# )
|
|
26
|
-
#
|
|
27
|
-
class ProcessTelemetryTracesJob < ::ActiveJob::Base
|
|
28
|
-
queue_as :default
|
|
29
|
-
|
|
30
|
-
# Maximum traces to process in a single job to avoid memory issues
|
|
31
|
-
MAX_TRACES_PER_JOB = 100
|
|
32
|
-
|
|
33
|
-
def perform(account_id: nil, traces:, sdk_info:, received_at:)
|
|
34
|
-
account = resolve_account(account_id)
|
|
35
|
-
|
|
36
|
-
# In multi-tenant mode, require an account
|
|
37
|
-
if ActiveAgent::Dashboard.multi_tenant? && account.nil?
|
|
38
|
-
Rails.logger.warn("[ProcessTelemetryTracesJob] Skipping traces - no valid account")
|
|
39
|
-
return
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
traces = traces.take(MAX_TRACES_PER_JOB)
|
|
43
|
-
|
|
44
|
-
traces.each do |trace|
|
|
45
|
-
process_trace(trace, sdk_info, account)
|
|
46
|
-
rescue StandardError => e
|
|
47
|
-
Rails.logger.error(
|
|
48
|
-
"[ProcessTelemetryTracesJob] Failed to process trace #{trace['trace_id']}: " \
|
|
49
|
-
"#{e.class} - #{e.message}"
|
|
50
|
-
)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def resolve_account(account_id)
|
|
57
|
-
return nil unless ActiveAgent::Dashboard.multi_tenant?
|
|
58
|
-
return nil unless account_id
|
|
59
|
-
|
|
60
|
-
account_class = ActiveAgent::Dashboard.account_class.constantize
|
|
61
|
-
account_class.find_by(id: account_id)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def process_trace(trace, sdk_info, account)
|
|
65
|
-
model = ActiveAgent::Dashboard.trace_model
|
|
66
|
-
|
|
67
|
-
# Build uniqueness scope
|
|
68
|
-
scope = model.where(trace_id: trace["trace_id"])
|
|
69
|
-
scope = scope.where(account: account) if ActiveAgent::Dashboard.multi_tenant? && account
|
|
70
|
-
|
|
71
|
-
# Skip if trace already exists (idempotency)
|
|
72
|
-
return if scope.exists?
|
|
73
|
-
|
|
74
|
-
model.create_from_payload(trace, sdk_info, account: account)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveAgent
|
|
4
|
-
module Dashboard
|
|
5
|
-
# Represents an AI agent configuration.
|
|
6
|
-
#
|
|
7
|
-
# Agents are the core entity in the dashboard, storing all configuration
|
|
8
|
-
# needed to execute AI interactions including provider settings, instructions,
|
|
9
|
-
# tools, and appearance.
|
|
10
|
-
#
|
|
11
|
-
# Supports both local (single-user) and multi-tenant (account-scoped) modes.
|
|
12
|
-
#
|
|
13
|
-
# @example Creating an agent
|
|
14
|
-
# agent = ActiveAgent::Dashboard::Agent.create!(
|
|
15
|
-
# name: "Code Assistant",
|
|
16
|
-
# provider: "openai",
|
|
17
|
-
# model: "gpt-4o"
|
|
18
|
-
# )
|
|
19
|
-
#
|
|
20
|
-
# @example Executing an agent
|
|
21
|
-
# run = agent.execute("Explain this code", code: "def foo; end")
|
|
22
|
-
#
|
|
23
|
-
class Agent < ApplicationRecord
|
|
24
|
-
# Associations - owner is optional to support both modes
|
|
25
|
-
belongs_to :user, class_name: ActiveAgent::Dashboard.user_class, optional: true if ActiveAgent::Dashboard.user_class
|
|
26
|
-
belongs_to :account, class_name: ActiveAgent::Dashboard.account_class, optional: true if ActiveAgent::Dashboard.multi_tenant?
|
|
27
|
-
|
|
28
|
-
has_many :agent_versions, class_name: "ActiveAgent::Dashboard::AgentVersion", dependent: :destroy
|
|
29
|
-
has_many :agent_runs, class_name: "ActiveAgent::Dashboard::AgentRun", dependent: :destroy
|
|
30
|
-
|
|
31
|
-
# Validations
|
|
32
|
-
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
33
|
-
validates :slug, presence: true, format: { with: /\A[a-z0-9\-_]+\z/ }
|
|
34
|
-
validates :provider, presence: true
|
|
35
|
-
validates :model, presence: true
|
|
36
|
-
|
|
37
|
-
# Ensure slug uniqueness within scope
|
|
38
|
-
if ActiveAgent::Dashboard.multi_tenant?
|
|
39
|
-
validates :slug, uniqueness: { scope: :account_id }
|
|
40
|
-
else
|
|
41
|
-
validates :slug, uniqueness: { scope: :user_id }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Status enum
|
|
45
|
-
enum :status, { draft: 0, active: 1, archived: 2 }
|
|
46
|
-
|
|
47
|
-
# Callbacks
|
|
48
|
-
before_validation :generate_slug, on: :create
|
|
49
|
-
after_create :create_initial_version
|
|
50
|
-
after_update :create_version_on_config_change, if: :configuration_changed?
|
|
51
|
-
|
|
52
|
-
# Scopes
|
|
53
|
-
scope :active_agents, -> { where(status: :active) }
|
|
54
|
-
scope :by_provider, ->(provider) { where(provider: provider) }
|
|
55
|
-
scope :with_tool, ->(tool) { where("tools @> ?", [ tool ].to_json) }
|
|
56
|
-
|
|
57
|
-
# Available presets matching AgentAvatar component
|
|
58
|
-
PRESET_TYPES = %w[
|
|
59
|
-
terminal webDeveloper documentAnalysis writing translation
|
|
60
|
-
playwright research imageAnalysis computerUse productDesign
|
|
61
|
-
].freeze
|
|
62
|
-
|
|
63
|
-
# Available instruction sets
|
|
64
|
-
INSTRUCTION_SETS = %w[
|
|
65
|
-
github ruby rails aws gcp python typescript docker kubernetes
|
|
66
|
-
].freeze
|
|
67
|
-
|
|
68
|
-
# Available tools/MCPs
|
|
69
|
-
AVAILABLE_TOOLS = %w[
|
|
70
|
-
terminal playwright filesystem code database slack fetch search edit translate memory
|
|
71
|
-
].freeze
|
|
72
|
-
|
|
73
|
-
# Available providers
|
|
74
|
-
PROVIDERS = %w[openai anthropic ollama openrouter requesty].freeze
|
|
75
|
-
|
|
76
|
-
# Returns the configuration as a hash for versioning
|
|
77
|
-
def configuration_snapshot
|
|
78
|
-
{
|
|
79
|
-
name: name,
|
|
80
|
-
description: description,
|
|
81
|
-
provider: provider,
|
|
82
|
-
model: model,
|
|
83
|
-
instructions: instructions,
|
|
84
|
-
preset_type: preset_type,
|
|
85
|
-
appearance: appearance,
|
|
86
|
-
instruction_sets: instruction_sets,
|
|
87
|
-
tools: tools,
|
|
88
|
-
mcp_servers: mcp_servers,
|
|
89
|
-
model_config: model_config,
|
|
90
|
-
response_format: response_format
|
|
91
|
-
}
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Restore from a version
|
|
95
|
-
def restore_from_version!(version)
|
|
96
|
-
config = version.configuration_snapshot
|
|
97
|
-
update!(
|
|
98
|
-
instructions: config["instructions"],
|
|
99
|
-
preset_type: config["preset_type"],
|
|
100
|
-
appearance: config["appearance"],
|
|
101
|
-
instruction_sets: config["instruction_sets"],
|
|
102
|
-
tools: config["tools"],
|
|
103
|
-
mcp_servers: config["mcp_servers"],
|
|
104
|
-
model_config: config["model_config"],
|
|
105
|
-
response_format: config["response_format"]
|
|
106
|
-
)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Get the latest version
|
|
110
|
-
def latest_version
|
|
111
|
-
agent_versions.order(version_number: :desc).first
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Get version count
|
|
115
|
-
def version_count
|
|
116
|
-
agent_versions.count
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Generate Ruby agent class code
|
|
120
|
-
def to_agent_class_code
|
|
121
|
-
<<~RUBY
|
|
122
|
-
class #{agent_class_name || name.camelize}Agent < ApplicationAgent
|
|
123
|
-
generate_with :#{provider}, model: "#{model}"#{model_config_code}
|
|
124
|
-
|
|
125
|
-
def perform
|
|
126
|
-
prompt#{instructions_code}
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
RUBY
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Execute a run with this agent
|
|
133
|
-
def execute(input_prompt, **params)
|
|
134
|
-
run = agent_runs.create!(
|
|
135
|
-
input_prompt: input_prompt,
|
|
136
|
-
input_params: params,
|
|
137
|
-
status: :pending,
|
|
138
|
-
trace_id: SecureRandom.uuid
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
# Queue the execution job
|
|
142
|
-
ActiveAgent::Dashboard::AgentExecutionJob.perform_later(run.id)
|
|
143
|
-
|
|
144
|
-
run
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Quick test execution (synchronous)
|
|
148
|
-
def test_execute(input_prompt, **params)
|
|
149
|
-
run = agent_runs.create!(
|
|
150
|
-
input_prompt: input_prompt,
|
|
151
|
-
input_params: params,
|
|
152
|
-
status: :running,
|
|
153
|
-
trace_id: SecureRandom.uuid,
|
|
154
|
-
started_at: Time.current
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
begin
|
|
158
|
-
result = build_and_execute_agent(input_prompt, **params)
|
|
159
|
-
|
|
160
|
-
run.update!(
|
|
161
|
-
output: result[:output],
|
|
162
|
-
output_metadata: result[:metadata],
|
|
163
|
-
status: :complete,
|
|
164
|
-
completed_at: Time.current,
|
|
165
|
-
duration_ms: ((Time.current - run.started_at) * 1000).to_i,
|
|
166
|
-
input_tokens: result.dig(:usage, :input_tokens),
|
|
167
|
-
output_tokens: result.dig(:usage, :output_tokens),
|
|
168
|
-
total_tokens: result.dig(:usage, :total_tokens)
|
|
169
|
-
)
|
|
170
|
-
rescue => e
|
|
171
|
-
run.update!(
|
|
172
|
-
status: :failed,
|
|
173
|
-
completed_at: Time.current,
|
|
174
|
-
error_message: e.message,
|
|
175
|
-
error_backtrace: e.backtrace&.first(10)&.join("\n")
|
|
176
|
-
)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
run
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
private
|
|
183
|
-
|
|
184
|
-
def generate_slug
|
|
185
|
-
return if slug.present?
|
|
186
|
-
|
|
187
|
-
base_slug = name.to_s.parameterize
|
|
188
|
-
self.slug = base_slug
|
|
189
|
-
|
|
190
|
-
# Ensure uniqueness within scope
|
|
191
|
-
counter = 1
|
|
192
|
-
scope = self.class.where(slug: slug)
|
|
193
|
-
scope = scope.where(account_id: account_id) if respond_to?(:account_id) && account_id
|
|
194
|
-
scope = scope.where(user_id: user_id) if respond_to?(:user_id) && user_id
|
|
195
|
-
|
|
196
|
-
while scope.exists?
|
|
197
|
-
self.slug = "#{base_slug}-#{counter}"
|
|
198
|
-
scope = self.class.where(slug: slug)
|
|
199
|
-
scope = scope.where(account_id: account_id) if respond_to?(:account_id) && account_id
|
|
200
|
-
scope = scope.where(user_id: user_id) if respond_to?(:user_id) && user_id
|
|
201
|
-
counter += 1
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def create_initial_version
|
|
206
|
-
agent_versions.create!(
|
|
207
|
-
version_number: 1,
|
|
208
|
-
change_summary: "Initial creation",
|
|
209
|
-
configuration_snapshot: configuration_snapshot
|
|
210
|
-
)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def configuration_changed?
|
|
214
|
-
saved_changes.keys.any? do |key|
|
|
215
|
-
%w[instructions preset_type appearance instruction_sets tools mcp_servers model_config response_format].include?(key)
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def create_version_on_config_change
|
|
220
|
-
next_version = (latest_version&.version_number || 0) + 1
|
|
221
|
-
changed_fields = saved_changes.keys.select do |key|
|
|
222
|
-
%w[instructions preset_type appearance instruction_sets tools mcp_servers model_config response_format].include?(key)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
agent_versions.create!(
|
|
226
|
-
version_number: next_version,
|
|
227
|
-
change_summary: "Updated: #{changed_fields.join(', ')}",
|
|
228
|
-
configuration_snapshot: configuration_snapshot
|
|
229
|
-
)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def model_config_code
|
|
233
|
-
return "" if model_config.blank?
|
|
234
|
-
|
|
235
|
-
configs = model_config.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
|
236
|
-
", #{configs}"
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def instructions_code
|
|
240
|
-
return "" if instructions.blank?
|
|
241
|
-
|
|
242
|
-
"\n prompt instructions: <<~INSTRUCTIONS\n #{instructions.gsub("\n", "\n ")}\n INSTRUCTIONS"
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def build_and_execute_agent(input_prompt, **params)
|
|
246
|
-
# TODO: Implement actual ActiveAgent execution
|
|
247
|
-
# This will create a dynamic agent class and execute it
|
|
248
|
-
{
|
|
249
|
-
output: "Mock response for: #{input_prompt}",
|
|
250
|
-
metadata: { provider: provider, model: model },
|
|
251
|
-
usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 }
|
|
252
|
-
}
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
end
|