actionagent 0.0.0 → 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/LICENSE +21 -0
- data/README.md +103 -0
- data/app/assets/builds/action_agent.css +2 -0
- data/app/assets/builds/action_agent.js +163 -0
- data/app/controllers/action_agent/api/agent_runs_controller.rb +128 -0
- data/app/controllers/action_agent/api/agents_controller.rb +411 -0
- data/app/controllers/action_agent/api/analytics_controller.rb +94 -0
- data/app/controllers/action_agent/api/api_keys_controller.rb +45 -0
- data/app/controllers/action_agent/api/base_controller.rb +112 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +148 -0
- data/app/controllers/action_agent/api/instance_tiers_controller.rb +106 -0
- data/app/controllers/action_agent/api/interactions_controller.rb +197 -0
- data/app/controllers/action_agent/api/mcp_controller.rb +218 -0
- data/app/controllers/action_agent/api/mcp_servers_controller.rb +156 -0
- data/app/controllers/action_agent/api/metrics_controller.rb +178 -0
- data/app/controllers/action_agent/api/provider_keys_controller.rb +52 -0
- data/app/controllers/action_agent/api/provider_models_controller.rb +119 -0
- data/app/controllers/action_agent/api/sandboxes_controller.rb +224 -0
- data/app/controllers/action_agent/api/session_recordings_controller.rb +372 -0
- data/app/controllers/action_agent/api/templates_controller.rb +94 -0
- data/app/controllers/action_agent/api/tools_controller.rb +58 -0
- data/app/controllers/action_agent/api/trace_reports_controller.rb +68 -0
- data/app/controllers/action_agent/api/traces_controller.rb +136 -0
- data/app/controllers/action_agent/application_controller.rb +105 -0
- data/app/controllers/action_agent/dashboard_controller.rb +104 -0
- data/app/controllers/action_agent/traces_controller.rb +121 -0
- data/app/jobs/action_agent/agent_execution_job.rb +52 -0
- data/app/jobs/action_agent/application_job.rb +12 -0
- data/app/jobs/action_agent/process_telemetry_traces_job.rb +86 -0
- data/app/jobs/action_agent/sandbox_cleanup_job.rb +42 -0
- data/app/jobs/action_agent/sandbox_provision_job.rb +56 -0
- data/app/jobs/action_agent/sandbox_run_job.rb +285 -0
- data/app/jobs/action_agent/trace_retention_job.rb +57 -0
- data/app/models/action_agent/agent.rb +343 -0
- data/app/models/action_agent/agent_context.rb +129 -0
- data/app/models/action_agent/agent_generation.rb +48 -0
- data/app/models/action_agent/agent_memory.rb +50 -0
- data/app/models/action_agent/agent_memory_entry.rb +14 -0
- data/app/models/action_agent/agent_message.rb +43 -0
- data/app/models/action_agent/agent_run.rb +151 -0
- data/app/models/action_agent/agent_template.rb +182 -0
- data/app/models/action_agent/agent_version.rb +48 -0
- data/app/models/action_agent/api_key.rb +53 -0
- data/app/models/action_agent/application_record.rb +27 -0
- data/app/models/action_agent/evaluation.rb +80 -0
- data/app/models/action_agent/evaluation_run.rb +20 -0
- data/app/models/action_agent/model_pricing.rb +80 -0
- data/app/models/action_agent/provider_key.rb +60 -0
- data/app/models/action_agent/recording_action.rb +119 -0
- data/app/models/action_agent/recording_snapshot.rb +88 -0
- data/app/models/action_agent/sandbox_instance_tier.rb +368 -0
- data/app/models/action_agent/sandbox_run.rb +45 -0
- data/app/models/action_agent/sandbox_session.rb +160 -0
- data/app/models/action_agent/session_recording.rb +178 -0
- data/app/models/action_agent/telemetry_trace.rb +357 -0
- data/app/models/concerns/action_agent/adapter_aware.rb +50 -0
- data/app/models/concerns/action_agent/ownable.rb +86 -0
- data/app/models/concerns/action_agent/session_recordable.rb +91 -0
- data/app/queries/action_agent/agent_executions.rb +201 -0
- data/app/serializers/action_agent/agent_message_serializer.rb +23 -0
- data/app/serializers/action_agent/interaction_preview.rb +22 -0
- data/app/serializers/action_agent/telemetry_trace_serializer.rb +122 -0
- data/app/serializers/action_agent/trace_interaction_serializer.rb +246 -0
- data/app/services/action_agent/agent_execution_service.rb +572 -0
- data/app/services/action_agent/agent_registrar.rb +197 -0
- data/app/services/action_agent/agent_scorecard.rb +191 -0
- data/app/services/action_agent/agent_toolbox.rb +504 -0
- data/app/services/action_agent/evaluation_runner_service.rb +481 -0
- data/app/services/action_agent/mcp_catalog.rb +247 -0
- data/app/services/action_agent/mcp_recording_middleware.rb +241 -0
- data/app/services/action_agent/mock_sandbox_backend.rb +52 -0
- data/app/services/action_agent/playwright_mcp_client.rb +148 -0
- data/app/services/action_agent/sandbox_orchestrator.rb +242 -0
- data/app/services/action_agent/session_recording_service.rb +228 -0
- data/app/services/action_agent/tool_discovery.rb +617 -0
- data/app/views/action_agent/dashboard/index.html.erb +5 -0
- data/app/views/action_agent/traces/_trace_detail.html.erb +117 -0
- data/app/views/action_agent/traces/index.html.erb +135 -0
- data/app/views/action_agent/traces/metrics.html.erb +145 -0
- data/app/views/action_agent/traces/show.html.erb +36 -0
- data/app/views/layouts/action_agent/application.html.erb +94 -0
- data/app/views/layouts/action_agent/react.html.erb +19 -0
- data/config/routes.rb +144 -0
- data/lib/action_agent/compatibility.rb +49 -0
- data/lib/action_agent/engine.rb +51 -0
- data/lib/action_agent/version.rb +5 -0
- data/lib/action_agent.rb +388 -0
- data/lib/actionagent.rb +6 -0
- data/lib/generators/action_agent/install_generator.rb +137 -0
- data/lib/generators/action_agent/templates/action_agent.rb.erb +82 -0
- data/lib/generators/action_agent/templates/add_agent_id_to_active_agent_telemetry_traces.rb.erb +24 -0
- data/lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb +319 -0
- data/lib/generators/action_agent/templates/create_active_agent_telemetry_traces.rb.erb +58 -0
- metadata +209 -12
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
class AgentRun < ApplicationRecord
|
|
5
|
+
belongs_to :agent
|
|
6
|
+
|
|
7
|
+
# Status enum
|
|
8
|
+
enum :status, { pending: 0, running: 1, complete: 2, failed: 3, cancelled: 4 }
|
|
9
|
+
|
|
10
|
+
# Validations
|
|
11
|
+
validates :trace_id, presence: true
|
|
12
|
+
|
|
13
|
+
# Scopes
|
|
14
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
15
|
+
scope :successful, -> { where(status: :complete) }
|
|
16
|
+
scope :failed_runs, -> { where(status: :failed) }
|
|
17
|
+
scope :today, -> { where("created_at >= ?", Time.current.beginning_of_day) }
|
|
18
|
+
|
|
19
|
+
# Callbacks
|
|
20
|
+
before_validation :set_trace_id, on: :create
|
|
21
|
+
after_update_commit :broadcast_update, if: :saved_change_to_status?
|
|
22
|
+
|
|
23
|
+
# Add a log entry
|
|
24
|
+
def add_log(message, level: :info)
|
|
25
|
+
new_logs = logs || []
|
|
26
|
+
new_logs << {
|
|
27
|
+
timestamp: Time.current.iso8601,
|
|
28
|
+
level: level.to_s,
|
|
29
|
+
message: message
|
|
30
|
+
}
|
|
31
|
+
update!(logs: new_logs)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Appends a progress event to logs mid-run so pollers can stream what the
|
|
35
|
+
# agent is doing (pending llm/tool/agent calls). Events pair up by eid:
|
|
36
|
+
# a "started" event is pending until a "done"/"error" with the same eid
|
|
37
|
+
# lands. update_column: no validations/callbacks, safe from the run's own
|
|
38
|
+
# execution thread; reads current DB state so add_log interleaves safely.
|
|
39
|
+
def append_event(eid:, kind:, label:, status: "done", detail: nil, duration_ms: nil)
|
|
40
|
+
event = {
|
|
41
|
+
"at" => Time.current.iso8601(3),
|
|
42
|
+
"eid" => eid,
|
|
43
|
+
"kind" => kind.to_s,
|
|
44
|
+
"label" => label.to_s,
|
|
45
|
+
"status" => status.to_s
|
|
46
|
+
}
|
|
47
|
+
event["detail"] = detail.to_s.byteslice(0, 1200).to_s.scrub if detail
|
|
48
|
+
event["duration_ms"] = duration_ms if duration_ms
|
|
49
|
+
current = self.class.where(id: id).pick(:logs) || []
|
|
50
|
+
update_column(:logs, current + [ event ])
|
|
51
|
+
event
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Stable short fingerprint of the instructions this run executed under —
|
|
55
|
+
# the grouping key (with model) for configuration cohorts when comparing
|
|
56
|
+
# instruction/model changes.
|
|
57
|
+
def instructions_digest
|
|
58
|
+
instructions = output_metadata&.dig("instructions")
|
|
59
|
+
return nil if instructions.blank?
|
|
60
|
+
|
|
61
|
+
Digest::SHA256.hexdigest(instructions).first(8)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Deterministic memorable name for the digest ("calm-heron") — reads far
|
|
65
|
+
# better than hex when comparing cohorts, and is stable across runs and
|
|
66
|
+
# deployments because it's derived from the digest alone.
|
|
67
|
+
CODENAME_ADJECTIVES = %w[
|
|
68
|
+
calm brisk quiet bold amber coral dusky fresh golden keen
|
|
69
|
+
lively mellow nimble pale rustic silver tidal vivid wry zesty
|
|
70
|
+
arid breezy crisp dapper eager foggy hazy icy jolly lunar
|
|
71
|
+
misty polar
|
|
72
|
+
].freeze
|
|
73
|
+
CODENAME_NOUNS = %w[
|
|
74
|
+
heron otter falcon cedar willow harbor mesa ridge grove delta
|
|
75
|
+
prairie summit canyon reef atoll fjord tundra oasis lagoon dune
|
|
76
|
+
glacier meadow bluff cove marsh basin knoll strait quarry vale
|
|
77
|
+
hollow crag
|
|
78
|
+
].freeze
|
|
79
|
+
|
|
80
|
+
def instructions_codename
|
|
81
|
+
digest = instructions_digest
|
|
82
|
+
return nil unless digest
|
|
83
|
+
|
|
84
|
+
value = digest.to_i(16)
|
|
85
|
+
"#{CODENAME_ADJECTIVES[value % 32]}-#{CODENAME_NOUNS[(value / 32) % 32]}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Calculate duration if not set
|
|
89
|
+
def calculated_duration_ms
|
|
90
|
+
return duration_ms if duration_ms.present?
|
|
91
|
+
return nil unless started_at && completed_at
|
|
92
|
+
|
|
93
|
+
((completed_at - started_at) * 1000).to_i
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if run is still in progress
|
|
97
|
+
def in_progress?
|
|
98
|
+
pending? || running?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check if run is finished
|
|
102
|
+
def finished?
|
|
103
|
+
complete? || failed? || cancelled?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get a summary for display
|
|
107
|
+
def summary
|
|
108
|
+
{
|
|
109
|
+
id: id,
|
|
110
|
+
status: status,
|
|
111
|
+
input_preview: input_prompt&.truncate(100),
|
|
112
|
+
output_preview: output&.truncate(200),
|
|
113
|
+
duration_ms: calculated_duration_ms,
|
|
114
|
+
tokens: total_tokens,
|
|
115
|
+
provider: output_metadata&.dig("provider"),
|
|
116
|
+
model: output_metadata&.dig("model"),
|
|
117
|
+
action_name: action_name || output_metadata&.dig("action") || "ask",
|
|
118
|
+
instructions_digest: instructions_digest,
|
|
119
|
+
instructions_codename: instructions_codename,
|
|
120
|
+
instructions_preview: output_metadata&.dig("instructions")&.truncate(120),
|
|
121
|
+
created_at: created_at,
|
|
122
|
+
error: error_message
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Stream output updates via ActionCable
|
|
127
|
+
def broadcast_update
|
|
128
|
+
payload = { type: "update", run: summary }
|
|
129
|
+
ActionCable.server.broadcast("agent_run_#{id}", payload)
|
|
130
|
+
ActionCable.server.broadcast("agent_runs_#{agent_id}", payload)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Cancel a running execution
|
|
134
|
+
def cancel!
|
|
135
|
+
return unless in_progress?
|
|
136
|
+
|
|
137
|
+
update!(
|
|
138
|
+
status: :cancelled,
|
|
139
|
+
completed_at: Time.current,
|
|
140
|
+
error_message: "Cancelled by user"
|
|
141
|
+
)
|
|
142
|
+
broadcast_update
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def set_trace_id
|
|
148
|
+
self.trace_id ||= SecureRandom.uuid
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
class AgentTemplate < ApplicationRecord
|
|
5
|
+
# Validations
|
|
6
|
+
validates :name, presence: true
|
|
7
|
+
validates :slug, presence: true, uniqueness: true
|
|
8
|
+
validates :category, presence: true
|
|
9
|
+
|
|
10
|
+
# Scopes
|
|
11
|
+
scope :featured, -> { where(featured: true) }
|
|
12
|
+
scope :by_category, ->(cat) { where(category: cat) }
|
|
13
|
+
scope :popular, -> { order(usage_count: :desc) }
|
|
14
|
+
scope :public_templates, -> { where(public: true) }
|
|
15
|
+
scope :free_tier, -> { where(free_tier: true) }
|
|
16
|
+
|
|
17
|
+
# Categories
|
|
18
|
+
CATEGORIES = %w[
|
|
19
|
+
productivity
|
|
20
|
+
development
|
|
21
|
+
research
|
|
22
|
+
creative
|
|
23
|
+
data
|
|
24
|
+
automation
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
# Create an agent from this template for a user
|
|
28
|
+
def create_agent_for(user, name: nil)
|
|
29
|
+
agent = user.agents.build(
|
|
30
|
+
name: name || self.name,
|
|
31
|
+
description: description,
|
|
32
|
+
provider: provider,
|
|
33
|
+
model: model,
|
|
34
|
+
instructions: instructions,
|
|
35
|
+
preset_type: preset_type,
|
|
36
|
+
appearance: appearance,
|
|
37
|
+
instruction_sets: instruction_sets,
|
|
38
|
+
tools: tools,
|
|
39
|
+
mcp_servers: mcp_servers,
|
|
40
|
+
model_config: model_config,
|
|
41
|
+
status: :draft
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if agent.save
|
|
45
|
+
increment!(:usage_count)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
agent
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Seed default templates
|
|
52
|
+
def self.seed_defaults!
|
|
53
|
+
templates = [
|
|
54
|
+
{
|
|
55
|
+
name: "Code Assistant",
|
|
56
|
+
slug: "code-assistant",
|
|
57
|
+
description: "A helpful coding assistant that can explain code, suggest improvements, and help debug issues.",
|
|
58
|
+
category: "development",
|
|
59
|
+
provider: "openai",
|
|
60
|
+
model: "gpt-4o",
|
|
61
|
+
preset_type: "terminal",
|
|
62
|
+
appearance: { hat: "fedora", heldItem: "terminal" },
|
|
63
|
+
instruction_sets: %w[github ruby rails typescript],
|
|
64
|
+
tools: %w[terminal code filesystem],
|
|
65
|
+
model_config: { temperature: 0.3 },
|
|
66
|
+
instructions: "You are a senior software engineer with expertise in multiple programming languages. Help users with:\n- Code explanations and reviews\n- Debugging issues\n- Suggesting best practices\n- Writing tests\n\nAlways explain your reasoning and provide examples when helpful.",
|
|
67
|
+
icon: "💻",
|
|
68
|
+
featured: true
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "Research Assistant",
|
|
72
|
+
slug: "research-assistant",
|
|
73
|
+
description: "Helps research topics, summarize information, and organize findings.",
|
|
74
|
+
category: "research",
|
|
75
|
+
provider: "anthropic",
|
|
76
|
+
model: "claude-sonnet-5",
|
|
77
|
+
preset_type: "research",
|
|
78
|
+
appearance: { hat: "safari", heldItem: "magnifyingGlass" },
|
|
79
|
+
instruction_sets: %w[github python],
|
|
80
|
+
tools: %w[fetch search memory],
|
|
81
|
+
model_config: { temperature: 0.5 },
|
|
82
|
+
instructions: "You are a thorough research assistant. Help users by:\n- Searching for relevant information\n- Summarizing complex topics\n- Organizing findings into clear reports\n- Identifying key insights and patterns\n\nAlways cite sources when available and distinguish between facts and opinions.",
|
|
83
|
+
icon: "🔍",
|
|
84
|
+
featured: true
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "Writing Assistant",
|
|
88
|
+
slug: "writing-assistant",
|
|
89
|
+
description: "Helps with writing, editing, and improving text content.",
|
|
90
|
+
category: "creative",
|
|
91
|
+
provider: "openai",
|
|
92
|
+
model: "gpt-4o",
|
|
93
|
+
preset_type: "writing",
|
|
94
|
+
appearance: { hat: "fedora", hatAccessory: "feather", heldItem: "scroll" },
|
|
95
|
+
instruction_sets: [],
|
|
96
|
+
tools: %w[edit translate],
|
|
97
|
+
model_config: { temperature: 0.7 },
|
|
98
|
+
instructions: "You are a skilled writer and editor. Help users with:\n- Writing and editing content\n- Improving clarity and flow\n- Adjusting tone for different audiences\n- Grammar and style corrections\n\nMaintain the author's voice while suggesting improvements.",
|
|
99
|
+
icon: "✍️",
|
|
100
|
+
featured: true
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "Browser Automation",
|
|
104
|
+
slug: "browser-automation",
|
|
105
|
+
description: "Automates web browsing tasks like form filling, data extraction, and testing.",
|
|
106
|
+
category: "automation",
|
|
107
|
+
provider: "anthropic",
|
|
108
|
+
model: "claude-sonnet-5",
|
|
109
|
+
preset_type: "playwright",
|
|
110
|
+
appearance: { hat: "fedora", hatAccessory: "theaterMasks", heldItem: "browser" },
|
|
111
|
+
instruction_sets: %w[typescript],
|
|
112
|
+
tools: %w[playwright filesystem],
|
|
113
|
+
model_config: { temperature: 0.2 },
|
|
114
|
+
instructions: "You are a browser automation specialist. Help users by:\n- Navigating web pages\n- Filling out forms\n- Extracting data from websites\n- Testing web applications\n\nAlways wait for page loads and handle errors gracefully.",
|
|
115
|
+
icon: "🎭",
|
|
116
|
+
featured: false
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "Data Analyst",
|
|
120
|
+
slug: "data-analyst",
|
|
121
|
+
description: "Analyzes data, creates visualizations, and provides insights.",
|
|
122
|
+
category: "data",
|
|
123
|
+
provider: "openai",
|
|
124
|
+
model: "gpt-4o",
|
|
125
|
+
preset_type: "documentAnalysis",
|
|
126
|
+
appearance: { hat: "fedora", heldItem: "document" },
|
|
127
|
+
instruction_sets: %w[python],
|
|
128
|
+
tools: %w[code database filesystem],
|
|
129
|
+
model_config: { temperature: 0.3 },
|
|
130
|
+
instructions: "You are a data analyst. Help users by:\n- Analyzing datasets\n- Creating visualizations\n- Finding patterns and insights\n- Generating reports\n\nExplain your methodology and provide clear interpretations of results.",
|
|
131
|
+
icon: "📊",
|
|
132
|
+
featured: true
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "DevOps Assistant",
|
|
136
|
+
slug: "devops-assistant",
|
|
137
|
+
description: "Helps with infrastructure, deployments, and system administration.",
|
|
138
|
+
category: "development",
|
|
139
|
+
provider: "openai",
|
|
140
|
+
model: "gpt-4o",
|
|
141
|
+
preset_type: "terminal",
|
|
142
|
+
appearance: { hat: "fedora", heldItem: "terminal" },
|
|
143
|
+
instruction_sets: %w[docker kubernetes aws gcp],
|
|
144
|
+
tools: %w[terminal filesystem code],
|
|
145
|
+
model_config: { temperature: 0.2 },
|
|
146
|
+
instructions: "You are a DevOps engineer. Help users with:\n- Infrastructure setup and management\n- CI/CD pipeline configuration\n- Container orchestration\n- Cloud resource management\n\nAlways prioritize security and follow best practices.",
|
|
147
|
+
icon: "🚀",
|
|
148
|
+
featured: false
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "PlaywrightMCP Demo",
|
|
152
|
+
slug: "playwright-mcp-demo",
|
|
153
|
+
description: "Free browser automation demo using Playwright MCP. Navigate sites, take screenshots, and extract content.",
|
|
154
|
+
category: "automation",
|
|
155
|
+
provider: "anthropic",
|
|
156
|
+
model: "claude-sonnet-5",
|
|
157
|
+
preset_type: "playwright",
|
|
158
|
+
appearance: { hat: "fedora", hatAccessory: "theaterMasks", heldItem: "browser" },
|
|
159
|
+
instruction_sets: [],
|
|
160
|
+
tools: %w[playwright],
|
|
161
|
+
mcp_servers: {
|
|
162
|
+
playwright: {
|
|
163
|
+
command: "npx",
|
|
164
|
+
args: [ "-y", "@anthropic/mcp-server-playwright" ]
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
model_config: { temperature: 0.2, max_tokens: 4096 },
|
|
168
|
+
instructions: "You are a browser automation assistant using Playwright MCP.\n\nAvailable actions:\n- browser_navigate: Go to a URL\n- browser_snapshot: Get the accessibility tree\n- browser_click: Click on an element\n- browser_type: Type text into an input\n- browser_take_screenshot: Capture the page\n- browser_wait_for: Wait for text or element\n\nGuidelines:\n1. Always take a snapshot first to understand the page\n2. Use element refs from snapshots for interactions\n3. Wait for page loads before taking actions\n4. Handle errors gracefully\n5. Limit yourself to 10 steps maximum\n\nAlways describe what you see and what actions you're taking.",
|
|
169
|
+
icon: "🎭",
|
|
170
|
+
featured: true,
|
|
171
|
+
free_tier: true
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
templates.each do |template_attrs|
|
|
176
|
+
AgentTemplate.find_or_create_by!(slug: template_attrs[:slug]) do |t|
|
|
177
|
+
t.assign_attributes(template_attrs)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
class AgentVersion < ApplicationRecord
|
|
5
|
+
belongs_to :agent
|
|
6
|
+
|
|
7
|
+
validates :version_number, presence: true, uniqueness: { scope: :agent_id }
|
|
8
|
+
validates :configuration_snapshot, presence: true
|
|
9
|
+
|
|
10
|
+
# Scopes
|
|
11
|
+
scope :recent, -> { order(version_number: :desc) }
|
|
12
|
+
scope :by_version, ->(num) { where(version_number: num) }
|
|
13
|
+
|
|
14
|
+
# Compare two versions
|
|
15
|
+
def diff(other_version)
|
|
16
|
+
return {} unless other_version
|
|
17
|
+
|
|
18
|
+
changes = {}
|
|
19
|
+
configuration_snapshot.each do |key, value|
|
|
20
|
+
other_value = other_version.configuration_snapshot[key]
|
|
21
|
+
if value != other_value
|
|
22
|
+
changes[key] = { from: other_value, to: value }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
changes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get previous version
|
|
29
|
+
def previous
|
|
30
|
+
agent.agent_versions.where("version_number < ?", version_number).order(version_number: :desc).first
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get next version
|
|
34
|
+
def next_version
|
|
35
|
+
agent.agent_versions.where("version_number > ?", version_number).order(version_number: :asc).first
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if this is the latest version
|
|
39
|
+
def latest?
|
|
40
|
+
agent.latest_version&.id == id
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if this is the initial version
|
|
44
|
+
def initial?
|
|
45
|
+
version_number == 1
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# API key generated from Settings -> API Keys. Authenticates requests to
|
|
5
|
+
# the telemetry ingest endpoint as a Bearer token.
|
|
6
|
+
#
|
|
7
|
+
# The token is generated server-side (never accepted from user input) and
|
|
8
|
+
# encrypted at rest with Active Record Encryption, so the host app needs
|
|
9
|
+
# `rails db:encryption:init` before creating keys. Deterministic
|
|
10
|
+
# encryption keeps find_by(token:) lookups working against the ciphertext.
|
|
11
|
+
# Set ActionAgent.encrypt_credentials = false to store tokens
|
|
12
|
+
# in plain text instead; that is a downgrade, not a default.
|
|
13
|
+
class ApiKey < ApplicationRecord
|
|
14
|
+
TOKEN_PREFIX = "aa_"
|
|
15
|
+
|
|
16
|
+
include Ownable
|
|
17
|
+
owned_by :account, :user
|
|
18
|
+
|
|
19
|
+
encrypts :token, deterministic: true if ActionAgent.encrypt_credentials
|
|
20
|
+
|
|
21
|
+
validates :name, presence: true, length: { maximum: 100 }
|
|
22
|
+
validates :token, presence: true, uniqueness: true
|
|
23
|
+
|
|
24
|
+
before_validation :generate_token, on: :create
|
|
25
|
+
|
|
26
|
+
# Finds the key for a presented bearer token. Returns nil for blank or
|
|
27
|
+
# unknown tokens.
|
|
28
|
+
def self.authenticate(token)
|
|
29
|
+
return nil if token.blank?
|
|
30
|
+
|
|
31
|
+
find_by(token: token)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def touch_last_used!
|
|
35
|
+
# Throttled to avoid a write per ingest request.
|
|
36
|
+
update_column(:last_used_at, Time.current) if last_used_at.nil? || last_used_at < 1.minute.ago
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Safe to display in the dashboard key list: "aa_3xam…k9Q2"
|
|
40
|
+
def masked_token
|
|
41
|
+
"#{token_prefix}…#{token.last(4)}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def generate_token
|
|
47
|
+
return if token.present?
|
|
48
|
+
|
|
49
|
+
self.token = "#{TOKEN_PREFIX}#{SecureRandom.base58(32)}"
|
|
50
|
+
self.token_prefix = token.first(TOKEN_PREFIX.length + 4)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# Base class for all Dashboard engine models.
|
|
5
|
+
#
|
|
6
|
+
# Table names come from ActionAgent.table_name_prefix through
|
|
7
|
+
# Rails' standard namespaced-model resolution, so the engine's own
|
|
8
|
+
# migrations (active_agent_agents, active_agent_agent_runs, ...) and a
|
|
9
|
+
# host app that already owns the tables unprefixed are both supported
|
|
10
|
+
# without touching the models.
|
|
11
|
+
#
|
|
12
|
+
# Ownership is configurable: multi-tenant installs scope records to an
|
|
13
|
+
# Account, single-tenant installs to a User, and a single-user install
|
|
14
|
+
# scopes to nothing at all.
|
|
15
|
+
class ApplicationRecord < ::ActiveRecord::Base
|
|
16
|
+
include AdapterAware
|
|
17
|
+
|
|
18
|
+
self.abstract_class = true
|
|
19
|
+
|
|
20
|
+
# Models that are not themselves owned still answer the ownership
|
|
21
|
+
# questions, so callers can scope any dashboard relation uniformly.
|
|
22
|
+
class << self
|
|
23
|
+
def owner_association = nil
|
|
24
|
+
def for_owner(_owner) = all
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# An evaluation definition for an agent: a named set of criteria scored
|
|
5
|
+
# against the agent's recorded behavior — its recent generations
|
|
6
|
+
# (solid_agent's agent_generations dataset) and its telemetry traces.
|
|
7
|
+
#
|
|
8
|
+
# Criteria are stored as an array of { "key", "type", "config" } hashes.
|
|
9
|
+
# Rule-based criterion types score each sampled generation
|
|
10
|
+
# deterministically; telemetry criterion types score aggregates over the
|
|
11
|
+
# agent's traces (error rate, latency); the llm_judge type asks a judge
|
|
12
|
+
# model to score each sample and requires a configured provider.
|
|
13
|
+
class Evaluation < ApplicationRecord
|
|
14
|
+
belongs_to :agent
|
|
15
|
+
has_many :evaluation_runs, dependent: :destroy
|
|
16
|
+
|
|
17
|
+
# judge_defined: the judge model authors the KPI criteria itself from the
|
|
18
|
+
# agent's instructions + sample interactions on the first run, then scores
|
|
19
|
+
# against them (criteria stay persisted/editable so scores are comparable
|
|
20
|
+
# across runs and models).
|
|
21
|
+
JUDGE_KINDS = %w[rules llm judge_defined].freeze
|
|
22
|
+
|
|
23
|
+
RULE_CRITERION_TYPES = %w[
|
|
24
|
+
response_present min_length max_latency_ms token_budget contains not_contains
|
|
25
|
+
].freeze
|
|
26
|
+
# Scored from the agent's telemetry traces (aggregate, not per-sample).
|
|
27
|
+
TELEMETRY_CRITERION_TYPES = %w[trace_error_rate trace_latency].freeze
|
|
28
|
+
CRITERION_TYPES = (RULE_CRITERION_TYPES + TELEMETRY_CRITERION_TYPES + %w[llm_judge]).freeze
|
|
29
|
+
|
|
30
|
+
validates :name, presence: true, uniqueness: { scope: :agent_id }
|
|
31
|
+
validates :judge_kind, inclusion: { in: JUDGE_KINDS }
|
|
32
|
+
validates :sample_size, numericality: { greater_than: 0, less_than_or_equal_to: 100 }
|
|
33
|
+
validate :validate_criteria
|
|
34
|
+
|
|
35
|
+
scope :recent, -> { order(updated_at: :desc) }
|
|
36
|
+
|
|
37
|
+
def latest_run
|
|
38
|
+
evaluation_runs.order(created_at: :desc).first
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def judge_defined?
|
|
42
|
+
judge_kind == "judge_defined"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Candidate models for per-cohort comparison scoring (config, optional).
|
|
46
|
+
def compare_models
|
|
47
|
+
Array(config["compare_models"]).map(&:to_s).reject(&:blank?)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def run!
|
|
51
|
+
EvaluationRunnerService.call(self)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def llm_criteria
|
|
55
|
+
criteria.select { |c| c["type"] == "llm_judge" }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def validate_criteria
|
|
61
|
+
if criteria.blank?
|
|
62
|
+
# judge_defined evaluations start empty — the judge authors the KPIs
|
|
63
|
+
# on the first run.
|
|
64
|
+
errors.add(:criteria, "must include at least one criterion") unless judge_defined?
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
criteria.each do |criterion|
|
|
69
|
+
unless criterion.is_a?(Hash) && criterion["key"].present?
|
|
70
|
+
errors.add(:criteria, "entries must have a key")
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
unless CRITERION_TYPES.include?(criterion["type"])
|
|
75
|
+
errors.add(:criteria, "unknown criterion type #{criterion['type']}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# One execution of an Evaluation over a sample of the agent's generations.
|
|
5
|
+
# scores: { criterion_key => { "score", "min", "max", "passed", "total" } }
|
|
6
|
+
class EvaluationRun < ApplicationRecord
|
|
7
|
+
belongs_to :evaluation
|
|
8
|
+
|
|
9
|
+
enum :status, { pending: 0, running: 1, complete: 2, failed: 3 }
|
|
10
|
+
|
|
11
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
12
|
+
|
|
13
|
+
def average_score
|
|
14
|
+
values = scores.values.map { |s| s["score"] }.compact
|
|
15
|
+
return nil if values.empty?
|
|
16
|
+
|
|
17
|
+
(values.sum.to_f / values.size).round(3)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# Estimates LLM spend from token counts. The activeagent gem's telemetry
|
|
5
|
+
# records tokens only; the platform layers pricing on top for the cost
|
|
6
|
+
# figures shown in Traces and Metrics.
|
|
7
|
+
#
|
|
8
|
+
# Rates come from RubyLLM's model registry (USD per million tokens,
|
|
9
|
+
# maintained upstream per model) when the model is known there; the static
|
|
10
|
+
# pattern table below is the fallback for aliases/self-hosted models, and
|
|
11
|
+
# a conservative blended rate covers everything else so totals stay
|
|
12
|
+
# meaningful. Costs are always presented as estimates.
|
|
13
|
+
class ModelPricing
|
|
14
|
+
PRICES = [
|
|
15
|
+
# [pattern, input $/1M, output $/1M]
|
|
16
|
+
[ /gpt-4o-mini/i, 0.15, 0.60 ],
|
|
17
|
+
[ /gpt-4o/i, 2.50, 10.00 ],
|
|
18
|
+
[ /gpt-4\.1-nano/i, 0.10, 0.40 ],
|
|
19
|
+
[ /gpt-4\.1-mini/i, 0.40, 1.60 ],
|
|
20
|
+
[ /gpt-4\.1/i, 2.00, 8.00 ],
|
|
21
|
+
[ /o3-mini|o4-mini/i, 1.10, 4.40 ],
|
|
22
|
+
[ /claude.*(fable|mythos)/i, 10.00, 50.00 ],
|
|
23
|
+
[ /claude.*haiku-?4/i, 1.00, 5.00 ],
|
|
24
|
+
[ /claude.*(haiku)/i, 0.80, 4.00 ],
|
|
25
|
+
[ /claude.*(sonnet)/i, 3.00, 15.00 ],
|
|
26
|
+
[ /claude.*opus-(5|4-[5-9])/i, 5.00, 25.00 ],
|
|
27
|
+
[ /claude.*(opus)/i, 15.00, 75.00 ],
|
|
28
|
+
[ /gemini.*flash/i, 0.10, 0.40 ],
|
|
29
|
+
[ /gemini.*pro/i, 1.25, 10.00 ],
|
|
30
|
+
[ /llama|mistral|mixtral|qwen|deepseek/i, 0.20, 0.60 ],
|
|
31
|
+
# Zero-prices "mock-*" traces recorded before the mock fallback was
|
|
32
|
+
# removed, so legacy rows never register as real spend.
|
|
33
|
+
[ /mock/i, 0.0, 0.0 ]
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
# Fallback blended rate for unknown models ($/1M input, $/1M output)
|
|
37
|
+
DEFAULT_RATE = [ 1.00, 4.00 ].freeze
|
|
38
|
+
|
|
39
|
+
# @return [Float, nil] estimated USD cost, nil when there is nothing to price
|
|
40
|
+
def self.estimate(model:, input_tokens:, output_tokens:)
|
|
41
|
+
input = input_tokens.to_i
|
|
42
|
+
output = output_tokens.to_i
|
|
43
|
+
return nil if input.zero? && output.zero?
|
|
44
|
+
|
|
45
|
+
input_rate, output_rate = rate_for(model)
|
|
46
|
+
((input * input_rate) + (output * output_rate)) / 1_000_000.0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.rate_for(model)
|
|
50
|
+
return DEFAULT_RATE if model.blank?
|
|
51
|
+
|
|
52
|
+
registry_rate(model) || static_rate(model)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Exact per-model rates from RubyLLM's registry. Lookups are memoized —
|
|
56
|
+
# the registry scan is not free and trace serialization calls this per
|
|
57
|
+
# row.
|
|
58
|
+
def self.registry_rate(model)
|
|
59
|
+
@registry_rates ||= {}
|
|
60
|
+
return @registry_rates[model] if @registry_rates.key?(model)
|
|
61
|
+
|
|
62
|
+
@registry_rates[model] = begin
|
|
63
|
+
info = RubyLLM.models.find(model.to_s)
|
|
64
|
+
tokens = info&.pricing&.text_tokens
|
|
65
|
+
if tokens&.input && tokens&.output
|
|
66
|
+
[ tokens.input, tokens.output ]
|
|
67
|
+
end
|
|
68
|
+
rescue StandardError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.static_rate(model)
|
|
74
|
+
PRICES.each do |pattern, input_rate, output_rate|
|
|
75
|
+
return [ input_rate, output_rate ] if model.to_s.match?(pattern)
|
|
76
|
+
end
|
|
77
|
+
DEFAULT_RATE
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|