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,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
# Model catalogs for the agent builder/editor dropdowns.
|
|
6
|
+
#
|
|
7
|
+
# Hosted providers get a curated list of current models (kept here, server
|
|
8
|
+
# side, so the UI can't drift stale). Ollama is queried live from the
|
|
9
|
+
# account's configured host (Settings -> Provider API Keys, falling back to
|
|
10
|
+
# the platform config) so locally pulled models appear; OpenRouter is
|
|
11
|
+
# queried from its public catalog. Live lookups fall back to the curated
|
|
12
|
+
# list on any failure.
|
|
13
|
+
class ProviderModelsController < BaseController
|
|
14
|
+
before_action :require_owner!
|
|
15
|
+
|
|
16
|
+
FETCH_TIMEOUT_SECONDS = 4
|
|
17
|
+
|
|
18
|
+
# Fallbacks when a live lookup isn't possible (reviewed 2026-07). First
|
|
19
|
+
# entry is the default the builder preselects.
|
|
20
|
+
CURATED = {
|
|
21
|
+
"openai" => %w[gpt-5.1 gpt-5 gpt-5-mini gpt-5-nano],
|
|
22
|
+
"anthropic" => %w[claude-opus-5 claude-fable-5 claude-sonnet-5 claude-opus-4-8 claude-haiku-4-5],
|
|
23
|
+
"openrouter" => %w[anthropic/claude-sonnet-4.5 openai/gpt-5.1 meta-llama/llama-3.3-70b-instruct qwen/qwen3-32b],
|
|
24
|
+
"ollama" => %w[qwen3:8b llama3.2 mistral gemma3]
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# GET /api/provider_models?provider=ollama
|
|
28
|
+
def index
|
|
29
|
+
provider = params[:provider].to_s
|
|
30
|
+
unless Agent::PROVIDERS.include?(provider)
|
|
31
|
+
return render json: { error: "Unknown provider: #{provider}" }, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
models, source = case provider
|
|
35
|
+
when "ollama" then live_ollama_models
|
|
36
|
+
when "openrouter" then live_openrouter_models
|
|
37
|
+
when "anthropic" then live_anthropic_models
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if models.blank?
|
|
41
|
+
models = CURATED.fetch(provider)
|
|
42
|
+
source = "curated"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
render json: { provider: provider, models: models, source: source }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# The owner's configured Ollama endpoint, else the host app's default
|
|
51
|
+
# from config/active_agent.yml.
|
|
52
|
+
def ollama_host
|
|
53
|
+
key = owner_provider_key("ollama")
|
|
54
|
+
host = key&.credential.presence || ActiveAgent.configuration[:ollama]&.dig(:host)
|
|
55
|
+
host.presence
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def owner_provider_key(provider)
|
|
59
|
+
owned(ProviderKey).find_by(provider: provider)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def live_ollama_models
|
|
63
|
+
host = ollama_host
|
|
64
|
+
return nil unless host
|
|
65
|
+
|
|
66
|
+
data = fetch_json(URI.join("#{host.chomp('/')}/", "models"))
|
|
67
|
+
ids = Array(data&.dig("data")).filter_map { |model| model["id"] }
|
|
68
|
+
[ ids.sort, "live" ] if ids.any?
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
Rails.logger.warn("[ProviderModels] ollama lookup failed: #{e.message}")
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Queries the Anthropic Models API with the account's key (newest first,
|
|
75
|
+
# as returned by the API) so new model releases appear without a deploy.
|
|
76
|
+
def live_anthropic_models
|
|
77
|
+
key = current_user_provider_key("anthropic")&.credential
|
|
78
|
+
return nil if key.blank?
|
|
79
|
+
|
|
80
|
+
data = Rails.cache.fetch("provider_models:anthropic:#{Digest::SHA256.hexdigest(key)}", expires_in: 1.hour) do
|
|
81
|
+
uri = URI.parse("https://api.anthropic.com/v1/models?limit=50")
|
|
82
|
+
response = Net::HTTP.start(
|
|
83
|
+
uri.host, uri.port,
|
|
84
|
+
use_ssl: true, open_timeout: FETCH_TIMEOUT_SECONDS, read_timeout: FETCH_TIMEOUT_SECONDS
|
|
85
|
+
) { |http| http.get(uri.request_uri, { "x-api-key" => key, "anthropic-version" => "2023-06-01" }) }
|
|
86
|
+
response.code.to_i == 200 ? JSON.parse(response.body) : nil
|
|
87
|
+
end
|
|
88
|
+
ids = Array(data&.dig("data")).filter_map { |model| model["id"] }
|
|
89
|
+
[ ids, "live" ] if ids.any?
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
Rails.logger.warn("[ProviderModels] anthropic lookup failed: #{e.message}")
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def live_openrouter_models
|
|
96
|
+
data = Rails.cache.fetch("provider_models:openrouter", expires_in: 1.hour) do
|
|
97
|
+
fetch_json(URI.parse("https://openrouter.ai/api/v1/models"))
|
|
98
|
+
end
|
|
99
|
+
ids = Array(data&.dig("data")).filter_map { |model| model["id"] }
|
|
100
|
+
[ ids.sort.first(100), "live" ] if ids.any?
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
Rails.logger.warn("[ProviderModels] openrouter lookup failed: #{e.message}")
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def fetch_json(uri)
|
|
107
|
+
response = Net::HTTP.start(
|
|
108
|
+
uri.host, uri.port,
|
|
109
|
+
use_ssl: uri.scheme == "https",
|
|
110
|
+
open_timeout: FETCH_TIMEOUT_SECONDS,
|
|
111
|
+
read_timeout: FETCH_TIMEOUT_SECONDS
|
|
112
|
+
) { |http| http.get(uri.request_uri) }
|
|
113
|
+
return nil unless response.code.to_i == 200
|
|
114
|
+
|
|
115
|
+
JSON.parse(response.body)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
class SandboxesController < BaseController
|
|
6
|
+
# Authenticated like the rest of the dashboard. This controller used to
|
|
7
|
+
# opt out entirely for an anonymous free tier, which made #run and
|
|
8
|
+
# #compare an open proxy: any unauthenticated caller could execute
|
|
9
|
+
# arbitrary prompts against the host app's provider credentials, and
|
|
10
|
+
# #show would read back any session by id. Opting out also skipped the
|
|
11
|
+
# callback that refuses to serve an unauthenticated dashboard outside
|
|
12
|
+
# development, so it bypassed that safeguard too.
|
|
13
|
+
|
|
14
|
+
before_action :require_execution_enabled!, only: [ :run, :compare ]
|
|
15
|
+
before_action :enforce_execution_quota!, only: [ :compare ]
|
|
16
|
+
before_action :set_sandbox, only: [ :show, :run, :destroy ]
|
|
17
|
+
|
|
18
|
+
# POST /api/sandboxes/compare
|
|
19
|
+
# Run multiple providers in a single sandbox using parallel generation jobs
|
|
20
|
+
def compare
|
|
21
|
+
providers = params[:providers] || %w[anthropic openai ollama]
|
|
22
|
+
task = params[:task]
|
|
23
|
+
sandbox_id = params[:sandbox_id]
|
|
24
|
+
|
|
25
|
+
return render json: { error: "Task required" }, status: :bad_request unless task.present?
|
|
26
|
+
return render json: { error: "At least 2 providers required" }, status: :bad_request if providers.size < 2
|
|
27
|
+
|
|
28
|
+
# Validate providers
|
|
29
|
+
invalid = providers - %w[anthropic openai ollama]
|
|
30
|
+
return render json: { error: "Invalid providers: #{invalid.join(', ')}" }, status: :bad_request if invalid.any?
|
|
31
|
+
|
|
32
|
+
# Use existing sandbox or create a new one (single container per user)
|
|
33
|
+
sandbox = if sandbox_id.present?
|
|
34
|
+
owned(SandboxSession).find_by!(session_id: sandbox_id)
|
|
35
|
+
else
|
|
36
|
+
s = SandboxSession.new(sandbox_type: params[:sandbox_type] || "playwright_mcp")
|
|
37
|
+
s.user = current_user if s.respond_to?(:user=)
|
|
38
|
+
s.save!
|
|
39
|
+
s.provision!
|
|
40
|
+
s.reload
|
|
41
|
+
s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless sandbox.can_run?
|
|
45
|
+
return render json: {
|
|
46
|
+
error: sandbox.expired? ? "Session expired" : "Maximum runs exceeded",
|
|
47
|
+
sandbox: sandbox.summary
|
|
48
|
+
}, status: :unprocessable_entity
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sandbox.update!(status: :running)
|
|
52
|
+
comparison_id = SecureRandom.uuid
|
|
53
|
+
|
|
54
|
+
# Spawn a separate generation job for each provider (all in same sandbox)
|
|
55
|
+
runs = providers.map do |provider|
|
|
56
|
+
run_id = SecureRandom.uuid
|
|
57
|
+
SandboxRunJob.perform_later(sandbox.id, run_id, task, provider)
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
provider: provider,
|
|
61
|
+
run_id: run_id,
|
|
62
|
+
status: "running"
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
render json: {
|
|
67
|
+
comparison_id: comparison_id,
|
|
68
|
+
task: task,
|
|
69
|
+
sandbox: sandbox.summary,
|
|
70
|
+
runs: runs
|
|
71
|
+
}, status: :accepted
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# GET /api/sandboxes
|
|
75
|
+
# List available sandbox types and sample tasks
|
|
76
|
+
def index
|
|
77
|
+
render json: {
|
|
78
|
+
sandbox_types: SandboxSession::SANDBOX_TYPES,
|
|
79
|
+
free_tier_limits: SandboxSession::FREE_TIER_LIMITS,
|
|
80
|
+
templates: free_tier_templates,
|
|
81
|
+
sample_tasks: sample_tasks
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# POST /api/sandboxes
|
|
86
|
+
# Create a new sandbox session, owned by whoever opened it.
|
|
87
|
+
def create
|
|
88
|
+
@sandbox = SandboxSession.new(sandbox_params)
|
|
89
|
+
# Guarded: the association only exists when the host app configured a
|
|
90
|
+
# user model, and a single-user install configures none.
|
|
91
|
+
@sandbox.user = current_user if @sandbox.respond_to?(:user=)
|
|
92
|
+
@sandbox.agent_template = AgentTemplate.find_by(slug: params[:template_slug]) if params[:template_slug]
|
|
93
|
+
|
|
94
|
+
if @sandbox.save
|
|
95
|
+
@sandbox.provision!
|
|
96
|
+
@sandbox.reload # Reload to get updated status after provisioning
|
|
97
|
+
render json: { sandbox: @sandbox.summary }, status: :created
|
|
98
|
+
else
|
|
99
|
+
render json: { errors: @sandbox.errors.full_messages }, status: :unprocessable_entity
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# GET /api/sandboxes/:session_id
|
|
104
|
+
# Get sandbox status and run history
|
|
105
|
+
def show
|
|
106
|
+
render json: { sandbox: @sandbox.details }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# POST /api/sandboxes/:session_id/run
|
|
110
|
+
# Execute a task in the sandbox
|
|
111
|
+
def run
|
|
112
|
+
unless @sandbox.can_run?
|
|
113
|
+
return render json: {
|
|
114
|
+
error: @sandbox.expired? ? "Session expired" : "Maximum runs exceeded",
|
|
115
|
+
sandbox: @sandbox.summary
|
|
116
|
+
}, status: :unprocessable_entity
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Whatever limits the host app imposes on running an agent.
|
|
120
|
+
if (denial = ActionAgent.quota_denial(current_owner, :execution))
|
|
121
|
+
return render json: {
|
|
122
|
+
error: "Plan limit reached",
|
|
123
|
+
upgrade_required: true,
|
|
124
|
+
message: denial
|
|
125
|
+
}, status: :payment_required
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
task = params[:task]
|
|
129
|
+
return render json: { error: "Task required" }, status: :bad_request unless task.present?
|
|
130
|
+
|
|
131
|
+
# Provider selection (default to anthropic)
|
|
132
|
+
provider = params[:provider] || "anthropic"
|
|
133
|
+
unless %w[anthropic openai ollama].include?(provider)
|
|
134
|
+
return render json: { error: "Invalid provider" }, status: :bad_request
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
record_execution_usage
|
|
138
|
+
|
|
139
|
+
@sandbox.update!(status: :running)
|
|
140
|
+
|
|
141
|
+
# Execute via job for async processing
|
|
142
|
+
run_id = SecureRandom.uuid
|
|
143
|
+
SandboxRunJob.perform_later(@sandbox.id, run_id, task, provider)
|
|
144
|
+
|
|
145
|
+
render json: {
|
|
146
|
+
run_id: run_id,
|
|
147
|
+
status: "running",
|
|
148
|
+
provider: provider,
|
|
149
|
+
sandbox: @sandbox.summary
|
|
150
|
+
}, status: :accepted
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# DELETE /api/sandboxes/:session_id
|
|
154
|
+
# End sandbox session
|
|
155
|
+
def destroy
|
|
156
|
+
@sandbox.expire!
|
|
157
|
+
render json: { deleted: true }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def set_sandbox
|
|
163
|
+
@sandbox = owned(SandboxSession).find_by!(session_id: params[:id])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def sandbox_params
|
|
167
|
+
params.permit(:sandbox_type)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def free_tier_templates
|
|
171
|
+
AgentTemplate.free_tier.featured.map do |template|
|
|
172
|
+
{
|
|
173
|
+
slug: template.slug,
|
|
174
|
+
name: template.name,
|
|
175
|
+
description: template.description,
|
|
176
|
+
icon: template.icon,
|
|
177
|
+
sandbox_type: template.preset_type == "playwright" ? "playwright_mcp" : template.preset_type
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def sample_tasks
|
|
183
|
+
{
|
|
184
|
+
playwright_mcp: [
|
|
185
|
+
{
|
|
186
|
+
name: "Screenshot Example.com",
|
|
187
|
+
task: "Take a screenshot of https://example.com",
|
|
188
|
+
description: "Navigate to example.com and capture a screenshot"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "Extract Hacker News Headlines",
|
|
192
|
+
task: "Go to https://news.ycombinator.com and list the top 5 story titles with their scores",
|
|
193
|
+
description: "Scrape the front page of Hacker News"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "Check Wikipedia",
|
|
197
|
+
task: "Navigate to https://en.wikipedia.org/wiki/Artificial_intelligence and extract the first paragraph",
|
|
198
|
+
description: "Extract content from Wikipedia"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "GitHub Trending",
|
|
202
|
+
task: "Visit https://github.com/trending and list the top 3 trending repositories",
|
|
203
|
+
description: "Check GitHub's trending repositories"
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
terminal: [
|
|
207
|
+
{
|
|
208
|
+
name: "System Info",
|
|
209
|
+
task: "Show the current system information",
|
|
210
|
+
description: "Display OS, memory, and CPU details"
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
research: [
|
|
214
|
+
{
|
|
215
|
+
name: "Topic Summary",
|
|
216
|
+
task: "Research and summarize the latest developments in AI",
|
|
217
|
+
description: "Search and compile information on a topic"
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|