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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +103 -0
  4. data/app/assets/builds/action_agent.css +2 -0
  5. data/app/assets/builds/action_agent.js +163 -0
  6. data/app/controllers/action_agent/api/agent_runs_controller.rb +128 -0
  7. data/app/controllers/action_agent/api/agents_controller.rb +411 -0
  8. data/app/controllers/action_agent/api/analytics_controller.rb +94 -0
  9. data/app/controllers/action_agent/api/api_keys_controller.rb +45 -0
  10. data/app/controllers/action_agent/api/base_controller.rb +112 -0
  11. data/app/controllers/action_agent/api/evaluations_controller.rb +148 -0
  12. data/app/controllers/action_agent/api/instance_tiers_controller.rb +106 -0
  13. data/app/controllers/action_agent/api/interactions_controller.rb +197 -0
  14. data/app/controllers/action_agent/api/mcp_controller.rb +218 -0
  15. data/app/controllers/action_agent/api/mcp_servers_controller.rb +156 -0
  16. data/app/controllers/action_agent/api/metrics_controller.rb +178 -0
  17. data/app/controllers/action_agent/api/provider_keys_controller.rb +52 -0
  18. data/app/controllers/action_agent/api/provider_models_controller.rb +119 -0
  19. data/app/controllers/action_agent/api/sandboxes_controller.rb +224 -0
  20. data/app/controllers/action_agent/api/session_recordings_controller.rb +372 -0
  21. data/app/controllers/action_agent/api/templates_controller.rb +94 -0
  22. data/app/controllers/action_agent/api/tools_controller.rb +58 -0
  23. data/app/controllers/action_agent/api/trace_reports_controller.rb +68 -0
  24. data/app/controllers/action_agent/api/traces_controller.rb +136 -0
  25. data/app/controllers/action_agent/application_controller.rb +105 -0
  26. data/app/controllers/action_agent/dashboard_controller.rb +104 -0
  27. data/app/controllers/action_agent/traces_controller.rb +121 -0
  28. data/app/jobs/action_agent/agent_execution_job.rb +52 -0
  29. data/app/jobs/action_agent/application_job.rb +12 -0
  30. data/app/jobs/action_agent/process_telemetry_traces_job.rb +86 -0
  31. data/app/jobs/action_agent/sandbox_cleanup_job.rb +42 -0
  32. data/app/jobs/action_agent/sandbox_provision_job.rb +56 -0
  33. data/app/jobs/action_agent/sandbox_run_job.rb +285 -0
  34. data/app/jobs/action_agent/trace_retention_job.rb +57 -0
  35. data/app/models/action_agent/agent.rb +343 -0
  36. data/app/models/action_agent/agent_context.rb +129 -0
  37. data/app/models/action_agent/agent_generation.rb +48 -0
  38. data/app/models/action_agent/agent_memory.rb +50 -0
  39. data/app/models/action_agent/agent_memory_entry.rb +14 -0
  40. data/app/models/action_agent/agent_message.rb +43 -0
  41. data/app/models/action_agent/agent_run.rb +151 -0
  42. data/app/models/action_agent/agent_template.rb +182 -0
  43. data/app/models/action_agent/agent_version.rb +48 -0
  44. data/app/models/action_agent/api_key.rb +53 -0
  45. data/app/models/action_agent/application_record.rb +27 -0
  46. data/app/models/action_agent/evaluation.rb +80 -0
  47. data/app/models/action_agent/evaluation_run.rb +20 -0
  48. data/app/models/action_agent/model_pricing.rb +80 -0
  49. data/app/models/action_agent/provider_key.rb +60 -0
  50. data/app/models/action_agent/recording_action.rb +119 -0
  51. data/app/models/action_agent/recording_snapshot.rb +88 -0
  52. data/app/models/action_agent/sandbox_instance_tier.rb +368 -0
  53. data/app/models/action_agent/sandbox_run.rb +45 -0
  54. data/app/models/action_agent/sandbox_session.rb +160 -0
  55. data/app/models/action_agent/session_recording.rb +178 -0
  56. data/app/models/action_agent/telemetry_trace.rb +357 -0
  57. data/app/models/concerns/action_agent/adapter_aware.rb +50 -0
  58. data/app/models/concerns/action_agent/ownable.rb +86 -0
  59. data/app/models/concerns/action_agent/session_recordable.rb +91 -0
  60. data/app/queries/action_agent/agent_executions.rb +201 -0
  61. data/app/serializers/action_agent/agent_message_serializer.rb +23 -0
  62. data/app/serializers/action_agent/interaction_preview.rb +22 -0
  63. data/app/serializers/action_agent/telemetry_trace_serializer.rb +122 -0
  64. data/app/serializers/action_agent/trace_interaction_serializer.rb +246 -0
  65. data/app/services/action_agent/agent_execution_service.rb +572 -0
  66. data/app/services/action_agent/agent_registrar.rb +197 -0
  67. data/app/services/action_agent/agent_scorecard.rb +191 -0
  68. data/app/services/action_agent/agent_toolbox.rb +504 -0
  69. data/app/services/action_agent/evaluation_runner_service.rb +481 -0
  70. data/app/services/action_agent/mcp_catalog.rb +247 -0
  71. data/app/services/action_agent/mcp_recording_middleware.rb +241 -0
  72. data/app/services/action_agent/mock_sandbox_backend.rb +52 -0
  73. data/app/services/action_agent/playwright_mcp_client.rb +148 -0
  74. data/app/services/action_agent/sandbox_orchestrator.rb +242 -0
  75. data/app/services/action_agent/session_recording_service.rb +228 -0
  76. data/app/services/action_agent/tool_discovery.rb +617 -0
  77. data/app/views/action_agent/dashboard/index.html.erb +5 -0
  78. data/app/views/action_agent/traces/_trace_detail.html.erb +117 -0
  79. data/app/views/action_agent/traces/index.html.erb +135 -0
  80. data/app/views/action_agent/traces/metrics.html.erb +145 -0
  81. data/app/views/action_agent/traces/show.html.erb +36 -0
  82. data/app/views/layouts/action_agent/application.html.erb +94 -0
  83. data/app/views/layouts/action_agent/react.html.erb +19 -0
  84. data/config/routes.rb +144 -0
  85. data/lib/action_agent/compatibility.rb +49 -0
  86. data/lib/action_agent/engine.rb +51 -0
  87. data/lib/action_agent/version.rb +5 -0
  88. data/lib/action_agent.rb +388 -0
  89. data/lib/actionagent.rb +6 -0
  90. data/lib/generators/action_agent/install_generator.rb +137 -0
  91. data/lib/generators/action_agent/templates/action_agent.rb.erb +82 -0
  92. data/lib/generators/action_agent/templates/add_agent_id_to_active_agent_telemetry_traces.rb.erb +24 -0
  93. data/lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb +319 -0
  94. data/lib/generators/action_agent/templates/create_active_agent_telemetry_traces.rb.erb +58 -0
  95. metadata +209 -12
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ class SandboxProvisionJob < ApplicationJob
5
+ queue_as :sandboxes
6
+
7
+ # Provision a Cloud Run sandbox for the session
8
+ # Each sandbox is an instance of the ActiveAgents application running in sandbox mode
9
+ def perform(sandbox_session_id)
10
+ sandbox = SandboxSession.find(sandbox_session_id)
11
+ return if sandbox.ready? || sandbox.expired?
12
+
13
+ # In development/test, simulate provisioning
14
+ if Rails.env.development? || Rails.env.test?
15
+ simulate_provisioning(sandbox)
16
+ return
17
+ end
18
+
19
+ # Hand off to whichever backend this install registered — the engine
20
+ # ships only the in-memory one, so a real container/job comes from the
21
+ # host app's backend (see ActionAgent.sandbox_backends).
22
+ result = SandboxOrchestrator.new.create_sandbox(sandbox)
23
+
24
+ sandbox.mark_ready!(
25
+ cloud_run_url: result[:url],
26
+ cloud_run_job_id: result[:sandbox_id]
27
+ )
28
+
29
+ # Broadcast status update
30
+ broadcast_sandbox_update(sandbox)
31
+ rescue StandardError => e
32
+ Rails.logger.error("Sandbox provision failed: #{e.message}")
33
+ sandbox.update!(status: :failed)
34
+ broadcast_sandbox_update(sandbox)
35
+ end
36
+
37
+ private
38
+
39
+ def simulate_provisioning(sandbox)
40
+ # Simulate a small delay for provisioning
41
+ sleep(0.5)
42
+
43
+ sandbox.mark_ready!(
44
+ cloud_run_url: "http://localhost:3000/api/sandbox",
45
+ cloud_run_job_id: "local-#{sandbox.session_id[0..7]}"
46
+ )
47
+ end
48
+
49
+ def broadcast_sandbox_update(sandbox)
50
+ ActionCable.server.broadcast(
51
+ "sandbox_#{sandbox.session_id}",
52
+ { type: "status_update", sandbox: sandbox.summary }
53
+ )
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ class SandboxRunJob < ApplicationJob
5
+ queue_as :sandboxes
6
+
7
+ PROVIDER_MODELS = {
8
+ "anthropic" => "claude-haiku-4-5",
9
+ "openai" => "gpt-4o",
10
+ "ollama" => "llama3.1:8b"
11
+ }.freeze
12
+
13
+ # Execute a task in the sandbox using ActiveAgent's generate_later
14
+ def perform(sandbox_session_id, run_id, task, provider = "anthropic")
15
+ sandbox = SandboxSession.find(sandbox_session_id)
16
+ return unless sandbox.can_run?
17
+
18
+ started_at = Time.current
19
+
20
+ # Broadcast that run has started
21
+ broadcast_run_started(sandbox, run_id, task, provider)
22
+
23
+ begin
24
+ # Use ActiveAgent with generate_later for async processing
25
+ # The agent's after_generation callback will handle recording results
26
+ if defined?(ActiveAgent::Base)
27
+ execute_with_active_agent(sandbox, run_id, task, provider, started_at)
28
+ else
29
+ # Fallback to direct API calls if ActiveAgent not available
30
+ execute_with_fallback(sandbox, run_id, task, provider, started_at)
31
+ end
32
+ rescue => e
33
+ Rails.logger.error("Sandbox run failed: #{e.message}")
34
+ sandbox.update!(status: :ready, error_message: e.message)
35
+ broadcast_run_error(sandbox, run_id, provider, e.message)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def execute_with_active_agent(sandbox, run_id, task, provider, started_at)
42
+ # Use generate_now here since we're already in a background job
43
+ # The ActiveAgent will handle the API call and callbacks
44
+ response = SandboxDemoAgent.with(
45
+ task: task,
46
+ provider: provider,
47
+ sandbox_session_id: sandbox.id,
48
+ run_id: run_id,
49
+ started_at: started_at
50
+ ).ask.generate_now
51
+
52
+ duration_ms = ((Time.current - started_at) * 1000).to_i
53
+
54
+ # Extract result from response
55
+ content = response&.message&.content || "No response generated"
56
+ input_tokens = response&.usage&.[](:input_tokens) || 0
57
+ output_tokens = response&.usage&.[](:output_tokens) || 0
58
+ tokens = input_tokens + output_tokens
59
+
60
+ # Record the run
61
+ run = sandbox.record_run!(
62
+ task: task,
63
+ result: content,
64
+ duration_ms: duration_ms,
65
+ tokens: tokens,
66
+ screenshots: [],
67
+ provider: provider
68
+ )
69
+
70
+ sandbox.update!(status: :ready)
71
+ broadcast_run_complete(sandbox, run_id, run)
72
+
73
+ rescue => e
74
+ Rails.logger.error("ActiveAgent execution failed: #{e.message}")
75
+ # Fallback to direct API
76
+ execute_with_fallback(sandbox, run_id, task, provider, started_at)
77
+ end
78
+
79
+ def execute_with_fallback(sandbox, run_id, task, provider, started_at)
80
+ result = case provider
81
+ when "anthropic"
82
+ execute_with_anthropic(task)
83
+ when "openai"
84
+ execute_with_openai(task)
85
+ when "ollama"
86
+ execute_with_ollama(task)
87
+ else
88
+ { content: "Error: Unknown provider '#{provider}'", tokens: 0, screenshots: [] }
89
+ end
90
+
91
+ duration_ms = ((Time.current - started_at) * 1000).to_i
92
+
93
+ # Record the run
94
+ run = sandbox.record_run!(
95
+ task: task,
96
+ result: result[:content],
97
+ duration_ms: duration_ms,
98
+ tokens: result[:tokens] || 0,
99
+ screenshots: result[:screenshots] || [],
100
+ provider: provider
101
+ )
102
+
103
+ sandbox.update!(status: :ready)
104
+ broadcast_run_complete(sandbox, run_id, run)
105
+ end
106
+
107
+ def execute_with_anthropic(task)
108
+ api_key = Rails.application.credentials.dig(:anthropic, :api_key) || ENV["ANTHROPIC_API_KEY"]
109
+
110
+ unless api_key.present?
111
+ Rails.logger.warn("Anthropic API key not configured")
112
+ return { content: "Error: Anthropic API key is not configured — set ANTHROPIC_API_KEY or add it to credentials", tokens: 0, screenshots: [] }
113
+ end
114
+
115
+ require "net/http"
116
+ require "json"
117
+
118
+ uri = URI("https://api.anthropic.com/v1/messages")
119
+ http = Net::HTTP.new(uri.host, uri.port)
120
+ http.use_ssl = true
121
+ http.read_timeout = 60
122
+
123
+ request = Net::HTTP::Post.new(uri)
124
+ request["Content-Type"] = "application/json"
125
+ request["x-api-key"] = api_key
126
+ request["anthropic-version"] = "2023-06-01"
127
+
128
+ request.body = {
129
+ model: PROVIDER_MODELS["anthropic"],
130
+ max_tokens: 1024,
131
+ system: default_system_prompt,
132
+ messages: [ { role: "user", content: task } ]
133
+ }.to_json
134
+
135
+ response = http.request(request)
136
+ result = JSON.parse(response.body)
137
+
138
+ if result["error"]
139
+ raise "Anthropic API error: #{result['error']['message']}"
140
+ end
141
+
142
+ content = result.dig("content", 0, "text") || "No response generated"
143
+ input_tokens = result.dig("usage", "input_tokens") || 0
144
+ output_tokens = result.dig("usage", "output_tokens") || 0
145
+
146
+ { content: content, tokens: input_tokens + output_tokens, screenshots: [] }
147
+ rescue => e
148
+ Rails.logger.error("Anthropic API call failed: #{e.message}")
149
+ { content: "Error: #{e.message}", tokens: 0, screenshots: [] }
150
+ end
151
+
152
+ def execute_with_openai(task)
153
+ api_key = Rails.application.credentials.dig(:openai, :api_key) || ENV["OPENAI_API_KEY"]
154
+
155
+ unless api_key.present?
156
+ Rails.logger.warn("OpenAI API key not configured")
157
+ return { content: "Error: OpenAI API key is not configured — set OPENAI_API_KEY or add it to credentials", tokens: 0, screenshots: [] }
158
+ end
159
+
160
+ require "net/http"
161
+ require "json"
162
+
163
+ uri = URI("https://api.openai.com/v1/chat/completions")
164
+ http = Net::HTTP.new(uri.host, uri.port)
165
+ http.use_ssl = true
166
+ http.read_timeout = 60
167
+
168
+ request = Net::HTTP::Post.new(uri)
169
+ request["Content-Type"] = "application/json"
170
+ request["Authorization"] = "Bearer #{api_key}"
171
+
172
+ request.body = {
173
+ model: PROVIDER_MODELS["openai"],
174
+ max_tokens: 1024,
175
+ messages: [
176
+ { role: "system", content: default_system_prompt },
177
+ { role: "user", content: task }
178
+ ]
179
+ }.to_json
180
+
181
+ response = http.request(request)
182
+ result = JSON.parse(response.body)
183
+
184
+ if result["error"]
185
+ raise "OpenAI API error: #{result['error']['message']}"
186
+ end
187
+
188
+ content = result.dig("choices", 0, "message", "content") || "No response generated"
189
+ prompt_tokens = result.dig("usage", "prompt_tokens") || 0
190
+ completion_tokens = result.dig("usage", "completion_tokens") || 0
191
+
192
+ { content: content, tokens: prompt_tokens + completion_tokens, screenshots: [] }
193
+ rescue => e
194
+ Rails.logger.error("OpenAI API call failed: #{e.message}")
195
+ { content: "Error: #{e.message}", tokens: 0, screenshots: [] }
196
+ end
197
+
198
+ def execute_with_ollama(task)
199
+ ollama_url = Rails.application.credentials.dig(:ollama, :url) || ENV["OLLAMA_URL"] || "http://localhost:11434"
200
+
201
+ require "net/http"
202
+ require "json"
203
+
204
+ uri = URI("#{ollama_url}/api/generate")
205
+ http = Net::HTTP.new(uri.host, uri.port)
206
+ http.use_ssl = uri.scheme == "https"
207
+ http.read_timeout = 120 # Ollama can be slower
208
+
209
+ request = Net::HTTP::Post.new(uri)
210
+ request["Content-Type"] = "application/json"
211
+
212
+ request.body = {
213
+ model: PROVIDER_MODELS["ollama"],
214
+ prompt: "#{default_system_prompt}\n\nUser: #{task}\n\nAssistant:",
215
+ stream: false
216
+ }.to_json
217
+
218
+ response = http.request(request)
219
+ result = JSON.parse(response.body)
220
+
221
+ if result["error"]
222
+ raise "Ollama API error: #{result['error']}"
223
+ end
224
+
225
+ content = result["response"] || "No response generated"
226
+ # Ollama doesn't provide token counts in the same way
227
+ tokens = (result["prompt_eval_count"] || 0) + (result["eval_count"] || 0)
228
+
229
+ { content: content, tokens: tokens, screenshots: [] }
230
+ rescue => e
231
+ Rails.logger.error("Ollama API call failed: #{e.message}")
232
+ { content: "Error: #{e.message}\n\nMake sure Ollama is running locally with: ollama serve", tokens: 0, screenshots: [] }
233
+ end
234
+
235
+ def default_system_prompt
236
+ <<~PROMPT
237
+ You are a helpful AI assistant. Answer the user's question or complete their task concisely and accurately.
238
+
239
+ If the task involves browser automation, describe what actions you would take step by step.
240
+ PROMPT
241
+ end
242
+
243
+ def broadcast_run_started(sandbox, run_id, task, provider)
244
+ ActionCable.server.broadcast(
245
+ "sandbox_#{sandbox.session_id}",
246
+ {
247
+ type: "run_started",
248
+ run_id: run_id,
249
+ provider: provider,
250
+ task: task,
251
+ started_at: Time.current.iso8601
252
+ }
253
+ )
254
+ Rails.logger.info "[SandboxRunJob] Broadcast run_started for #{provider} (#{run_id})"
255
+ end
256
+
257
+ def broadcast_run_complete(sandbox, run_id, run)
258
+ ActionCable.server.broadcast(
259
+ "sandbox_#{sandbox.session_id}",
260
+ {
261
+ type: "run_complete",
262
+ run_id: run_id,
263
+ provider: run[:provider],
264
+ run: run,
265
+ sandbox: sandbox.summary
266
+ }
267
+ )
268
+ Rails.logger.info "[SandboxRunJob] Broadcast run_complete for #{run[:provider]} (#{run_id})"
269
+ end
270
+
271
+ def broadcast_run_error(sandbox, run_id, provider, error)
272
+ ActionCable.server.broadcast(
273
+ "sandbox_#{sandbox.session_id}",
274
+ {
275
+ type: "run_error",
276
+ run_id: run_id,
277
+ provider: provider,
278
+ error: error,
279
+ sandbox: sandbox.summary
280
+ }
281
+ )
282
+ Rails.logger.info "[SandboxRunJob] Broadcast run_error for #{provider} (#{run_id})"
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # Prunes telemetry traces past the configured retention window.
5
+ #
6
+ # NOT SCHEDULED BY DEFAULT: deleting trace history is a decision for the
7
+ # app that owns the data. To enable it, set a window and schedule the job:
8
+ #
9
+ # ActionAgent.trace_retention = 30.days
10
+ #
11
+ # # config/recurring.yml
12
+ # trace_retention:
13
+ # class: ActionAgent::TraceRetentionJob
14
+ # schedule: every day at 4am
15
+ #
16
+ # A host app with per-tenant retention rules (the activeagents.ai platform
17
+ # prunes per plan) passes a callable instead, receiving each owner and
18
+ # returning that owner's window.
19
+ class TraceRetentionJob < ApplicationJob
20
+ queue_as :default
21
+
22
+ BATCH_SIZE = 5_000
23
+
24
+ def perform
25
+ window = ActionAgent.trace_retention
26
+ return if window.nil?
27
+
28
+ if ActionAgent.multi_tenant? && (owners = ActionAgent.owner_class)
29
+ owners.find_each { |owner| prune(traces.for_account(owner), window_for(window, owner)) }
30
+ else
31
+ prune(traces.all, window_for(window, nil))
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def traces
38
+ ActionAgent.trace_model
39
+ end
40
+
41
+ def window_for(window, owner)
42
+ window.respond_to?(:call) ? window.call(owner) : window
43
+ end
44
+
45
+ # Deletes in batches so a long-neglected install doesn't build one
46
+ # enormous statement.
47
+ def prune(scope, window)
48
+ return if window.nil?
49
+
50
+ cutoff = window.ago
51
+ loop do
52
+ deleted = scope.where(timestamp: ...cutoff).limit(BATCH_SIZE).delete_all
53
+ break if deleted < BATCH_SIZE
54
+ end
55
+ end
56
+ end
57
+ end