activeagent 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/README.md +10 -4
- data/lib/active_agent/base.rb +3 -2
- data/lib/active_agent/concerns/provider.rb +6 -2
- data/lib/active_agent/concerns/rescue.rb +39 -0
- data/lib/active_agent/concerns/streaming.rb +2 -1
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
- data/lib/active_agent/dashboard/config/routes.rb +78 -0
- data/lib/active_agent/dashboard/engine.rb +39 -0
- data/lib/active_agent/dashboard.rb +151 -0
- data/lib/active_agent/providers/_base_provider.rb +4 -1
- data/lib/active_agent/providers/anthropic/options.rb +4 -6
- data/lib/active_agent/providers/anthropic/request.rb +28 -3
- data/lib/active_agent/providers/anthropic/transforms.rb +131 -2
- data/lib/active_agent/providers/anthropic_provider.rb +97 -30
- data/lib/active_agent/providers/azure/_types.rb +5 -0
- data/lib/active_agent/providers/azure/options.rb +111 -0
- data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_provider.rb +133 -0
- data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
- data/lib/active_agent/providers/bedrock/_types.rb +8 -0
- data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
- data/lib/active_agent/providers/bedrock/options.rb +77 -0
- data/lib/active_agent/providers/bedrock_provider.rb +84 -0
- data/lib/active_agent/providers/common/messages/_types.rb +42 -31
- data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
- data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
- data/lib/active_agent/providers/concerns/previewable.rb +39 -5
- data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
- data/lib/active_agent/providers/gemini/_types.rb +19 -0
- data/lib/active_agent/providers/gemini/options.rb +41 -0
- data/lib/active_agent/providers/gemini_provider.rb +94 -0
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +120 -4
- data/lib/active_agent/providers/open_ai/chat_provider.rb +40 -0
- data/lib/active_agent/providers/open_ai/responses/request.rb +17 -2
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +135 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +38 -0
- data/lib/active_agent/providers/open_router/request.rb +20 -0
- data/lib/active_agent/providers/open_router/transforms.rb +30 -0
- data/lib/active_agent/providers/open_router_provider.rb +14 -0
- data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
- data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
- data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
- data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
- data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
- data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
- data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
- data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
- data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
- data/lib/active_agent/railtie.rb +32 -1
- data/lib/active_agent/telemetry/configuration.rb +213 -0
- data/lib/active_agent/telemetry/instrumentation.rb +155 -0
- data/lib/active_agent/telemetry/reporter.rb +176 -0
- data/lib/active_agent/telemetry/span.rb +267 -0
- data/lib/active_agent/telemetry/tracer.rb +184 -0
- data/lib/active_agent/telemetry.rb +162 -0
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +2 -0
- data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
- data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
- data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
- metadata +101 -14
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require_relative "_base_provider"
|
|
2
|
+
|
|
3
|
+
require_gem!(:openai, __FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "open_ai_provider"
|
|
6
|
+
require_relative "azure/_types"
|
|
7
|
+
|
|
8
|
+
module ActiveAgent
|
|
9
|
+
module Providers
|
|
10
|
+
# Provider for Azure OpenAI Service via OpenAI-compatible API.
|
|
11
|
+
#
|
|
12
|
+
# Azure OpenAI uses the same API structure as OpenAI but with different
|
|
13
|
+
# authentication (api-key header) and endpoint configuration (resource + deployment).
|
|
14
|
+
#
|
|
15
|
+
# @example Configuration in active_agent.yml
|
|
16
|
+
# azure_openai:
|
|
17
|
+
# service: "AzureOpenAI"
|
|
18
|
+
# api_key: <%= ENV["AZURE_OPENAI_API_KEY"] %>
|
|
19
|
+
# azure_resource: "mycompany"
|
|
20
|
+
# deployment_id: "gpt-4-deployment"
|
|
21
|
+
# api_version: "2024-10-21"
|
|
22
|
+
#
|
|
23
|
+
# @see OpenAI::ChatProvider
|
|
24
|
+
class AzureProvider < OpenAI::ChatProvider
|
|
25
|
+
# @return [String]
|
|
26
|
+
def self.service_name
|
|
27
|
+
"AzureOpenAI"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Class]
|
|
31
|
+
def self.options_klass
|
|
32
|
+
Azure::Options
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [ActiveModel::Type::Value]
|
|
36
|
+
def self.prompt_request_type
|
|
37
|
+
OpenAI::Chat::RequestType.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [ActiveModel::Type::Value]
|
|
41
|
+
def self.embed_request_type
|
|
42
|
+
OpenAI::Embedding::RequestType.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns a configured Azure OpenAI client.
|
|
46
|
+
#
|
|
47
|
+
# Uses a custom client subclass that handles Azure-specific authentication
|
|
48
|
+
# (api-key header instead of Authorization: Bearer).
|
|
49
|
+
#
|
|
50
|
+
# @return [AzureClient] the configured Azure client
|
|
51
|
+
def client
|
|
52
|
+
@client ||= AzureClient.new(
|
|
53
|
+
api_key: options.api_key,
|
|
54
|
+
base_url: options.base_url,
|
|
55
|
+
api_version: options.api_version,
|
|
56
|
+
max_retries: options.max_retries,
|
|
57
|
+
timeout: options.timeout,
|
|
58
|
+
initial_retry_delay: options.initial_retry_delay,
|
|
59
|
+
max_retry_delay: options.max_retry_delay
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Custom OpenAI client for Azure OpenAI Service.
|
|
64
|
+
#
|
|
65
|
+
# Azure uses different authentication headers (api-key instead of Authorization: Bearer)
|
|
66
|
+
# and requires api-version as a query parameter on all requests.
|
|
67
|
+
class AzureClient < ::OpenAI::Client
|
|
68
|
+
# @return [String]
|
|
69
|
+
attr_reader :api_version
|
|
70
|
+
|
|
71
|
+
# Creates a new Azure OpenAI client.
|
|
72
|
+
#
|
|
73
|
+
# @param api_key [String] Azure OpenAI API key
|
|
74
|
+
# @param base_url [String] Azure endpoint URL
|
|
75
|
+
# @param api_version [String] API version (e.g., "2024-10-21")
|
|
76
|
+
# @param max_retries [Integer] Maximum retry attempts
|
|
77
|
+
# @param timeout [Float] Request timeout in seconds
|
|
78
|
+
# @param initial_retry_delay [Float] Initial delay between retries
|
|
79
|
+
# @param max_retry_delay [Float] Maximum delay between retries
|
|
80
|
+
def initialize(
|
|
81
|
+
api_key:,
|
|
82
|
+
base_url:,
|
|
83
|
+
api_version:,
|
|
84
|
+
max_retries: self.class::DEFAULT_MAX_RETRIES,
|
|
85
|
+
timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
|
|
86
|
+
initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
|
|
87
|
+
max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
|
|
88
|
+
)
|
|
89
|
+
@api_version = api_version
|
|
90
|
+
|
|
91
|
+
super(
|
|
92
|
+
api_key: api_key,
|
|
93
|
+
base_url: base_url,
|
|
94
|
+
max_retries: max_retries,
|
|
95
|
+
timeout: timeout,
|
|
96
|
+
initial_retry_delay: initial_retry_delay,
|
|
97
|
+
max_retry_delay: max_retry_delay
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Azure uses api-key header instead of Authorization: Bearer.
|
|
104
|
+
#
|
|
105
|
+
# @return [Hash{String=>String}]
|
|
106
|
+
def auth_headers
|
|
107
|
+
return {} if @api_key.nil?
|
|
108
|
+
|
|
109
|
+
{ "api-key" => @api_key }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Builds request with Azure-specific query parameters.
|
|
113
|
+
#
|
|
114
|
+
# Injects api-version into extra_query for all requests.
|
|
115
|
+
#
|
|
116
|
+
# @param req [Hash] Request parameters
|
|
117
|
+
# @param opts [Hash] Request options
|
|
118
|
+
# @return [Hash] Built request
|
|
119
|
+
def build_request(req, opts)
|
|
120
|
+
# Inject api-version into extra_query
|
|
121
|
+
opts = opts.dup
|
|
122
|
+
opts[:extra_query] = (opts[:extra_query] || {}).merge("api-version" => @api_version)
|
|
123
|
+
|
|
124
|
+
super(req, opts)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Aliases for provider loading with different service name variations
|
|
130
|
+
AzureOpenAIProvider = AzureProvider
|
|
131
|
+
AzureOpenaiProvider = AzureProvider
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "options"
|
|
4
|
+
require_relative "bearer_client"
|
|
5
|
+
require_relative "../anthropic/_types"
|
|
6
|
+
|
|
7
|
+
# Bedrock uses the same request/response types as Anthropic.
|
|
8
|
+
# The BedrockClient handles all protocol translation internally.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
module Bedrock
|
|
6
|
+
# Client for AWS Bedrock using bearer token (API key) authentication.
|
|
7
|
+
#
|
|
8
|
+
# Subclasses Anthropic::Client directly to reuse its built-in bearer
|
|
9
|
+
# token support via the +auth_token+ parameter, while adding Bedrock-
|
|
10
|
+
# specific request transformations (URL path rewriting, anthropic_version
|
|
11
|
+
# injection) copied from Anthropic::BedrockClient.
|
|
12
|
+
#
|
|
13
|
+
# This avoids Anthropic::BedrockClient which requires SigV4 credentials
|
|
14
|
+
# and would fail when only a bearer token is available.
|
|
15
|
+
#
|
|
16
|
+
# @see https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-use.html
|
|
17
|
+
class BearerClient < ::Anthropic::Client
|
|
18
|
+
BEDROCK_VERSION = "bedrock-2023-05-31"
|
|
19
|
+
|
|
20
|
+
# @return [String]
|
|
21
|
+
attr_reader :aws_region
|
|
22
|
+
|
|
23
|
+
# @param aws_region [String] AWS region for the Bedrock endpoint
|
|
24
|
+
# @param bearer_token [String] AWS Bedrock API key (bearer token)
|
|
25
|
+
# @param base_url [String, nil] Override the default Bedrock endpoint
|
|
26
|
+
# @param max_retries [Integer]
|
|
27
|
+
# @param timeout [Float]
|
|
28
|
+
# @param initial_retry_delay [Float]
|
|
29
|
+
# @param max_retry_delay [Float]
|
|
30
|
+
def initialize(
|
|
31
|
+
aws_region:,
|
|
32
|
+
bearer_token:,
|
|
33
|
+
base_url: nil,
|
|
34
|
+
max_retries: self.class::DEFAULT_MAX_RETRIES,
|
|
35
|
+
timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
|
|
36
|
+
initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
|
|
37
|
+
max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
|
|
38
|
+
)
|
|
39
|
+
@aws_region = aws_region
|
|
40
|
+
|
|
41
|
+
base_url ||= "https://bedrock-runtime.#{aws_region}.amazonaws.com"
|
|
42
|
+
|
|
43
|
+
super(
|
|
44
|
+
auth_token: bearer_token,
|
|
45
|
+
api_key: nil,
|
|
46
|
+
base_url: base_url,
|
|
47
|
+
max_retries: max_retries,
|
|
48
|
+
timeout: timeout,
|
|
49
|
+
initial_retry_delay: initial_retry_delay,
|
|
50
|
+
max_retry_delay: max_retry_delay
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@messages = ::Anthropic::Resources::Messages.new(client: self)
|
|
54
|
+
@completions = ::Anthropic::Resources::Completions.new(client: self)
|
|
55
|
+
@beta = ::Anthropic::Resources::Beta.new(client: self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Intercepts request building to apply Bedrock-specific transformations
|
|
61
|
+
# before the parent class processes the request.
|
|
62
|
+
def build_request(req, opts)
|
|
63
|
+
fit_req_to_bedrock_specs!(req)
|
|
64
|
+
req = super
|
|
65
|
+
body = req.fetch(:body)
|
|
66
|
+
req[:body] = StringIO.new(body.to_a.join) if body.is_a?(Enumerator)
|
|
67
|
+
req
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Rewrites Anthropic API paths to Bedrock endpoint paths and injects
|
|
71
|
+
# the Bedrock anthropic_version field.
|
|
72
|
+
#
|
|
73
|
+
# Adapted from Anthropic::Helpers::Bedrock::Client#fit_req_to_bedrock_specs!
|
|
74
|
+
def fit_req_to_bedrock_specs!(request_components)
|
|
75
|
+
if (body = request_components[:body]).is_a?(Hash)
|
|
76
|
+
body[:anthropic_version] ||= BEDROCK_VERSION
|
|
77
|
+
body.transform_keys!("anthropic-beta": :anthropic_beta)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
case request_components[:path]
|
|
81
|
+
in %r{^v1/messages/batches}
|
|
82
|
+
raise NotImplementedError, "The Batch API is not supported in Bedrock yet"
|
|
83
|
+
in %r{v1/messages/count_tokens}
|
|
84
|
+
raise NotImplementedError, "Token counting is not supported in Bedrock yet"
|
|
85
|
+
in %r{v1/models\?beta=true}
|
|
86
|
+
raise NotImplementedError,
|
|
87
|
+
"Please instead use https://docs.anthropic.com/en/api/claude-on-amazon-bedrock#list-available-models " \
|
|
88
|
+
"to list available models on Bedrock."
|
|
89
|
+
else
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if %w[
|
|
93
|
+
v1/complete
|
|
94
|
+
v1/messages
|
|
95
|
+
v1/messages?beta=true
|
|
96
|
+
].include?(request_components[:path]) && request_components[:method] == :post && body.is_a?(Hash)
|
|
97
|
+
model = body.delete(:model)
|
|
98
|
+
model = URI.encode_www_form_component(model.to_s)
|
|
99
|
+
stream = body.delete(:stream) || false
|
|
100
|
+
request_components[:path] =
|
|
101
|
+
stream ? "model/#{model}/invoke-with-response-stream" : "model/#{model}/invoke"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
request_components
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_agent/providers/common/model"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Bedrock
|
|
8
|
+
# Configuration for AWS Bedrock provider.
|
|
9
|
+
#
|
|
10
|
+
# AWS credentials are resolved in order:
|
|
11
|
+
# 1. Explicit options (aws_access_key, aws_secret_key)
|
|
12
|
+
# 2. Environment variables (AWS_REGION, AWS_ACCESS_KEY_ID, etc.)
|
|
13
|
+
# 3. AWS SDK default chain (profiles, IAM roles, instance metadata)
|
|
14
|
+
#
|
|
15
|
+
# Unlike the Anthropic provider, no API key is needed — authentication
|
|
16
|
+
# is handled entirely through AWS credentials.
|
|
17
|
+
#
|
|
18
|
+
# @example Minimal config (uses SDK default chain)
|
|
19
|
+
# Bedrock::Options.new(aws_region: "eu-west-2")
|
|
20
|
+
#
|
|
21
|
+
# @example Explicit credentials
|
|
22
|
+
# Bedrock::Options.new(
|
|
23
|
+
# aws_region: "eu-west-2",
|
|
24
|
+
# aws_access_key: "AKIA...",
|
|
25
|
+
# aws_secret_key: "..."
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# @example With profile
|
|
29
|
+
# Bedrock::Options.new(
|
|
30
|
+
# aws_region: "eu-west-2",
|
|
31
|
+
# aws_profile: "my-profile"
|
|
32
|
+
# )
|
|
33
|
+
class Options < Common::BaseModel
|
|
34
|
+
attribute :aws_region, :string
|
|
35
|
+
attribute :aws_access_key, :string
|
|
36
|
+
attribute :aws_secret_key, :string
|
|
37
|
+
attribute :aws_session_token, :string
|
|
38
|
+
attribute :aws_profile, :string
|
|
39
|
+
attribute :aws_bearer_token, :string
|
|
40
|
+
attribute :base_url, :string
|
|
41
|
+
attribute :anthropic_beta, :string
|
|
42
|
+
|
|
43
|
+
attribute :max_retries, :integer, default: ::Anthropic::Client::DEFAULT_MAX_RETRIES
|
|
44
|
+
attribute :timeout, :float, default: ::Anthropic::Client::DEFAULT_TIMEOUT_IN_SECONDS
|
|
45
|
+
attribute :initial_retry_delay, :float, default: ::Anthropic::Client::DEFAULT_INITIAL_RETRY_DELAY
|
|
46
|
+
attribute :max_retry_delay, :float, default: ::Anthropic::Client::DEFAULT_MAX_RETRY_DELAY
|
|
47
|
+
|
|
48
|
+
def initialize(kwargs = {})
|
|
49
|
+
kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
|
|
50
|
+
|
|
51
|
+
super(**deep_compact(kwargs.except(:default_url_options).merge(
|
|
52
|
+
aws_region: kwargs[:aws_region] || ENV["AWS_REGION"] || ENV["AWS_DEFAULT_REGION"],
|
|
53
|
+
aws_access_key: kwargs[:aws_access_key] || ENV["AWS_ACCESS_KEY_ID"],
|
|
54
|
+
aws_secret_key: kwargs[:aws_secret_key] || ENV["AWS_SECRET_ACCESS_KEY"],
|
|
55
|
+
aws_session_token: kwargs[:aws_session_token] || ENV["AWS_SESSION_TOKEN"],
|
|
56
|
+
aws_profile: kwargs[:aws_profile] || ENV["AWS_PROFILE"],
|
|
57
|
+
aws_bearer_token: kwargs[:aws_bearer_token] || ENV["AWS_BEARER_TOKEN_BEDROCK"]
|
|
58
|
+
)))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Bedrock handles authentication at the client level (SigV4 or bearer token),
|
|
62
|
+
# so no extra headers are needed in request options.
|
|
63
|
+
def extra_headers
|
|
64
|
+
{}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Excludes sensitive AWS credentials from serialized output.
|
|
68
|
+
# The provider's client() method reads credentials directly from options attributes.
|
|
69
|
+
def serialize
|
|
70
|
+
attributes.symbolize_keys.except(
|
|
71
|
+
:aws_access_key, :aws_secret_key, :aws_session_token, :aws_profile, :aws_bearer_token
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "anthropic_provider"
|
|
4
|
+
require_relative "bedrock/_types"
|
|
5
|
+
|
|
6
|
+
module ActiveAgent
|
|
7
|
+
module Providers
|
|
8
|
+
# Provider for Anthropic models hosted on AWS Bedrock.
|
|
9
|
+
#
|
|
10
|
+
# Inherits all functionality from AnthropicProvider (streaming, tool use,
|
|
11
|
+
# multimodal, JSON format emulation) and overrides only the client
|
|
12
|
+
# construction to use Anthropic::BedrockClient for AWS authentication.
|
|
13
|
+
#
|
|
14
|
+
# @example Configuration in active_agent.yml
|
|
15
|
+
# bedrock:
|
|
16
|
+
# service: "Bedrock"
|
|
17
|
+
# aws_region: "eu-west-2"
|
|
18
|
+
# model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
|
19
|
+
#
|
|
20
|
+
# @example Agent usage
|
|
21
|
+
# class SummaryAgent < ApplicationAgent
|
|
22
|
+
# generate_with :bedrock, model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
|
23
|
+
#
|
|
24
|
+
# def summarize
|
|
25
|
+
# prompt(message: params[:message])
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @see AnthropicProvider
|
|
30
|
+
class BedrockProvider < AnthropicProvider
|
|
31
|
+
# @return [String]
|
|
32
|
+
def self.service_name
|
|
33
|
+
"Bedrock"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Class]
|
|
37
|
+
def self.options_klass
|
|
38
|
+
Bedrock::Options
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [ActiveModel::Type::Value]
|
|
42
|
+
def self.prompt_request_type
|
|
43
|
+
Anthropic::RequestType.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns a configured Bedrock client.
|
|
47
|
+
#
|
|
48
|
+
# When a bearer token is available (via +aws_bearer_token+ option or
|
|
49
|
+
# +AWS_BEARER_TOKEN_BEDROCK+ env var), uses {Bedrock::BearerClient}
|
|
50
|
+
# which sends an +Authorization: Bearer+ header.
|
|
51
|
+
#
|
|
52
|
+
# Otherwise, falls back to {Anthropic::BedrockClient} which handles
|
|
53
|
+
# SigV4 signing, credential resolution, and Bedrock URL path rewriting.
|
|
54
|
+
#
|
|
55
|
+
# @return [Bedrock::BearerClient, Anthropic::Helpers::Bedrock::Client]
|
|
56
|
+
def client
|
|
57
|
+
@client ||= if options.aws_bearer_token.present?
|
|
58
|
+
Bedrock::BearerClient.new(
|
|
59
|
+
aws_region: options.aws_region,
|
|
60
|
+
bearer_token: options.aws_bearer_token,
|
|
61
|
+
base_url: options.base_url.presence,
|
|
62
|
+
max_retries: options.max_retries,
|
|
63
|
+
timeout: options.timeout,
|
|
64
|
+
initial_retry_delay: options.initial_retry_delay,
|
|
65
|
+
max_retry_delay: options.max_retry_delay
|
|
66
|
+
)
|
|
67
|
+
else
|
|
68
|
+
::Anthropic::BedrockClient.new(
|
|
69
|
+
aws_region: options.aws_region,
|
|
70
|
+
aws_access_key: options.aws_access_key,
|
|
71
|
+
aws_secret_key: options.aws_secret_key,
|
|
72
|
+
aws_session_token: options.aws_session_token,
|
|
73
|
+
aws_profile: options.aws_profile,
|
|
74
|
+
base_url: options.base_url.presence,
|
|
75
|
+
max_retries: options.max_retries,
|
|
76
|
+
timeout: options.timeout,
|
|
77
|
+
initial_retry_delay: options.initial_retry_delay,
|
|
78
|
+
max_retry_delay: options.max_retry_delay
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -37,7 +37,7 @@ module ActiveAgent
|
|
|
37
37
|
role = hash[:role]&.to_s
|
|
38
38
|
|
|
39
39
|
case role
|
|
40
|
-
when "system"
|
|
40
|
+
when "system", "developer"
|
|
41
41
|
nil # System messages are dropped in common format, replaced by Instructions
|
|
42
42
|
when "user", nil
|
|
43
43
|
# Handle both standard format and format with `text` key
|
|
@@ -51,12 +51,6 @@ module ActiveAgent
|
|
|
51
51
|
when "assistant"
|
|
52
52
|
# Filter to only known attributes for Assistant
|
|
53
53
|
filtered_hash = hash.slice(:role, :content, :name)
|
|
54
|
-
|
|
55
|
-
# Compress content array to string if needed (Anthropic format)
|
|
56
|
-
if filtered_hash[:content].is_a?(Array)
|
|
57
|
-
filtered_hash[:content] = compress_content_array(filtered_hash[:content])
|
|
58
|
-
end
|
|
59
|
-
|
|
60
54
|
Common::Messages::Assistant.new(**filtered_hash)
|
|
61
55
|
when "tool"
|
|
62
56
|
# Filter to only known attributes for Tool
|
|
@@ -94,29 +88,6 @@ module ActiveAgent
|
|
|
94
88
|
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
95
89
|
end
|
|
96
90
|
end
|
|
97
|
-
|
|
98
|
-
# Compresses Anthropic-style content array into a string.
|
|
99
|
-
#
|
|
100
|
-
# Anthropic messages can have content as an array of blocks like:
|
|
101
|
-
# [{type: "text", text: "..."}, {type: "tool_use", ...}]
|
|
102
|
-
# This extracts and joins text blocks into a single string.
|
|
103
|
-
#
|
|
104
|
-
# @param content_array [Array<Hash>]
|
|
105
|
-
# @return [String]
|
|
106
|
-
def compress_content_array(content_array)
|
|
107
|
-
content_array.map do |block|
|
|
108
|
-
case block[:type]&.to_s
|
|
109
|
-
when "text"
|
|
110
|
-
block[:text]
|
|
111
|
-
when "tool_use"
|
|
112
|
-
# Tool use blocks don't have readable text content
|
|
113
|
-
nil
|
|
114
|
-
else
|
|
115
|
-
# Unknown block type, try to extract text if present
|
|
116
|
-
block[:text]
|
|
117
|
-
end
|
|
118
|
-
end.compact.join("\n")
|
|
119
|
-
end
|
|
120
91
|
end
|
|
121
92
|
|
|
122
93
|
# Type for Messages array
|
|
@@ -124,7 +95,9 @@ module ActiveAgent
|
|
|
124
95
|
def cast(value)
|
|
125
96
|
case value
|
|
126
97
|
when Array
|
|
127
|
-
value.map { |v| message_type.cast(v) }.compact
|
|
98
|
+
messages = value.map { |v| message_type.cast(v) }.compact
|
|
99
|
+
# Split messages with array content into separate messages
|
|
100
|
+
messages.flat_map { |msg| split_content_blocks(msg) }
|
|
128
101
|
when nil
|
|
129
102
|
[]
|
|
130
103
|
else
|
|
@@ -152,6 +125,44 @@ module ActiveAgent
|
|
|
152
125
|
def message_type
|
|
153
126
|
@message_type ||= MessageType.new
|
|
154
127
|
end
|
|
128
|
+
|
|
129
|
+
# Splits an assistant message with array content into separate messages
|
|
130
|
+
# for each content block.
|
|
131
|
+
#
|
|
132
|
+
# @param message [Common::Messages::Base]
|
|
133
|
+
# @return [Array<Common::Messages::Base>]
|
|
134
|
+
def split_content_blocks(message)
|
|
135
|
+
# Only split assistant messages with array content
|
|
136
|
+
return [ message ] unless message.is_a?(Common::Messages::Assistant) && message.content.is_a?(Array)
|
|
137
|
+
|
|
138
|
+
message.content.map do |block|
|
|
139
|
+
case block[:type]&.to_s
|
|
140
|
+
when "text"
|
|
141
|
+
# Create a message for text blocks
|
|
142
|
+
Common::Messages::Assistant.new(role: "assistant", content: block[:text], name: message.name)
|
|
143
|
+
when "tool_use"
|
|
144
|
+
# Create a message with tool use info as string representation
|
|
145
|
+
input = block[:input]
|
|
146
|
+
input_str = input.nil? || input.empty? ? "{}" : JSON.pretty_generate(input)
|
|
147
|
+
tool_info = "[Tool Use: #{block[:name]}]\nID: #{block[:id]}\nInput: #{input_str}"
|
|
148
|
+
Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
|
|
149
|
+
when "mcp_tool_use"
|
|
150
|
+
# Create a message with MCP tool use info
|
|
151
|
+
input = block[:input]
|
|
152
|
+
input_str = input.nil? || input.empty? ? "{}" : JSON.pretty_generate(input)
|
|
153
|
+
tool_info = "[MCP Tool Use: #{block[:name]}]\nID: #{block[:id]}\nServer: #{block[:server_name]}\nInput: #{input_str}"
|
|
154
|
+
Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
|
|
155
|
+
when "mcp_tool_result"
|
|
156
|
+
# Create a message with MCP tool result
|
|
157
|
+
result_info = "[MCP Tool Result]\n#{block[:content]}"
|
|
158
|
+
Common::Messages::Assistant.new(role: "assistant", content: result_info, name: message.name)
|
|
159
|
+
else
|
|
160
|
+
# For unknown block types, try to extract text
|
|
161
|
+
content = block[:text] || block.to_s
|
|
162
|
+
Common::Messages::Assistant.new(role: "assistant", content:, name: message.name)
|
|
163
|
+
end
|
|
164
|
+
end.compact
|
|
165
|
+
end
|
|
155
166
|
end
|
|
156
167
|
end
|
|
157
168
|
end
|
|
@@ -9,7 +9,7 @@ module ActiveAgent
|
|
|
9
9
|
# Represents messages sent by the AI assistant in a conversation.
|
|
10
10
|
class Assistant < Base
|
|
11
11
|
attribute :role, :string, as: "assistant"
|
|
12
|
-
attribute :content
|
|
12
|
+
attribute :content # Accept both string and array (provider-native formats)
|
|
13
13
|
attribute :name, :string
|
|
14
14
|
|
|
15
15
|
validates :content, presence: true
|
|
@@ -24,9 +24,16 @@ module ActiveAgent
|
|
|
24
24
|
# @param normalize_names [Symbol, nil] key normalization method (e.g., :underscore)
|
|
25
25
|
# @return [Hash, Array, nil] parsed JSON structure or nil if parsing fails
|
|
26
26
|
def parsed_json(symbolize_names: true, normalize_names: :underscore)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
# Handle array content (from content blocks) by searching through each block
|
|
28
|
+
content_str = if content.is_a?(Array)
|
|
29
|
+
content.map { |block| block.is_a?(Hash) ? block[:text] : block.to_s }.join("\n")
|
|
30
|
+
else
|
|
31
|
+
content.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
start_char = [ content_str.index("{"), content_str.index("[") ].compact.min
|
|
35
|
+
end_char = [ content_str.rindex("}"), content_str.rindex("]") ].compact.max
|
|
36
|
+
content_stripped = content_str[start_char..end_char] if start_char && end_char
|
|
30
37
|
return unless content_stripped
|
|
31
38
|
|
|
32
39
|
content_parsed = JSON.parse(content_stripped)
|
|
@@ -48,6 +55,15 @@ module ActiveAgent
|
|
|
48
55
|
nil
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
# Returns content as a string, handling both string and array formats
|
|
59
|
+
def text
|
|
60
|
+
if content.is_a?(Array)
|
|
61
|
+
content.map { |block| block.is_a?(Hash) ? block[:text] : block.to_s }.join("\n")
|
|
62
|
+
else
|
|
63
|
+
content.to_s
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
51
67
|
alias_method :json_object, :parsed_json
|
|
52
68
|
alias_method :parse_json, :parsed_json
|
|
53
69
|
end
|
|
@@ -86,7 +86,13 @@ module ActiveAgent
|
|
|
86
86
|
"### Message #{index} (#{role.capitalize})\n#{content}"
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
# Renders
|
|
89
|
+
# Renders tools section for preview.
|
|
90
|
+
#
|
|
91
|
+
# Handles multiple tool formats:
|
|
92
|
+
# - Common format: {name: "...", description: "...", parameters: {...}}
|
|
93
|
+
# - Anthropic format: {name: "...", description: "...", input_schema: {...}}
|
|
94
|
+
# - Chat API format: {type: "function", function: {name: "...", description: "...", parameters: {...}}}
|
|
95
|
+
# - Responses API format: {type: "function", name: "...", description: "...", parameters: {...}}
|
|
90
96
|
#
|
|
91
97
|
# @param tools [Array<Hash>]
|
|
92
98
|
# @return [String]
|
|
@@ -96,17 +102,45 @@ module ActiveAgent
|
|
|
96
102
|
content = +"## Tools\n\n"
|
|
97
103
|
|
|
98
104
|
tools.each_with_index do |tool, index|
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
# Extract name and description from different formats
|
|
106
|
+
tool_name, tool_description, tool_params = extract_tool_details(tool)
|
|
107
|
+
|
|
108
|
+
content << "### #{tool_name || "Tool #{index + 1}"}\n"
|
|
109
|
+
content << "**Description:** #{tool_description || 'No description'}\n\n"
|
|
101
110
|
|
|
102
|
-
if
|
|
103
|
-
content << "**Parameters:**\n```json\n#{JSON.pretty_generate(
|
|
111
|
+
if tool_params
|
|
112
|
+
content << "**Parameters:**\n```json\n#{JSON.pretty_generate(tool_params)}\n```\n\n"
|
|
104
113
|
end
|
|
105
114
|
end
|
|
106
115
|
|
|
107
116
|
content.chomp
|
|
108
117
|
end
|
|
109
118
|
|
|
119
|
+
# Extracts tool details from different formats.
|
|
120
|
+
#
|
|
121
|
+
# @param tool [Hash]
|
|
122
|
+
# @return [Array<String, String, Hash>] [name, description, parameters]
|
|
123
|
+
def extract_tool_details(tool)
|
|
124
|
+
tool_hash = tool.is_a?(Hash) ? tool : {}
|
|
125
|
+
|
|
126
|
+
# Chat API nested format: {type: "function", function: {...}}
|
|
127
|
+
if tool_hash[:type] == "function" && tool_hash[:function]
|
|
128
|
+
func = tool_hash[:function]
|
|
129
|
+
return [
|
|
130
|
+
func[:name],
|
|
131
|
+
func[:description],
|
|
132
|
+
func[:parameters] || func[:input_schema]
|
|
133
|
+
]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Flat formats (common, Anthropic, Responses)
|
|
137
|
+
[
|
|
138
|
+
tool_hash[:name],
|
|
139
|
+
tool_hash[:description],
|
|
140
|
+
tool_hash[:parameters] || tool_hash[:input_schema]
|
|
141
|
+
]
|
|
142
|
+
end
|
|
143
|
+
|
|
110
144
|
# Extracts text content from various message formats.
|
|
111
145
|
#
|
|
112
146
|
# Handles string messages, hash messages with :content key, and
|