activeagent 1.0.1 → 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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -4
  3. data/lib/active_agent/base.rb +3 -2
  4. data/lib/active_agent/concerns/provider.rb +6 -2
  5. data/lib/active_agent/concerns/rescue.rb +39 -0
  6. data/lib/active_agent/concerns/streaming.rb +2 -1
  7. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
  10. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
  11. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
  12. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
  13. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
  14. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
  15. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
  16. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
  17. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
  18. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
  19. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
  20. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
  21. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
  22. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
  23. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
  24. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
  25. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
  26. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
  27. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
  28. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
  29. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
  30. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
  31. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
  32. data/lib/active_agent/dashboard/config/routes.rb +78 -0
  33. data/lib/active_agent/dashboard/engine.rb +39 -0
  34. data/lib/active_agent/dashboard.rb +151 -0
  35. data/lib/active_agent/providers/_base_provider.rb +2 -1
  36. data/lib/active_agent/providers/anthropic_provider.rb +14 -4
  37. data/lib/active_agent/providers/azure/_types.rb +5 -0
  38. data/lib/active_agent/providers/azure/options.rb +111 -0
  39. data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
  40. data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
  41. data/lib/active_agent/providers/azure_provider.rb +133 -0
  42. data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
  43. data/lib/active_agent/providers/bedrock/_types.rb +8 -0
  44. data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
  45. data/lib/active_agent/providers/bedrock/options.rb +77 -0
  46. data/lib/active_agent/providers/bedrock_provider.rb +84 -0
  47. data/lib/active_agent/providers/common/messages/_types.rb +6 -2
  48. data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
  49. data/lib/active_agent/providers/gemini/_types.rb +19 -0
  50. data/lib/active_agent/providers/gemini/options.rb +41 -0
  51. data/lib/active_agent/providers/gemini_provider.rb +94 -0
  52. data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
  53. data/lib/active_agent/providers/open_ai/chat_provider.rb +2 -0
  54. data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
  55. data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
  56. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
  57. data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
  58. data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
  59. data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
  60. data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
  61. data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
  62. data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
  63. data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
  64. data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
  65. data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
  66. data/lib/active_agent/railtie.rb +32 -1
  67. data/lib/active_agent/telemetry/configuration.rb +213 -0
  68. data/lib/active_agent/telemetry/instrumentation.rb +155 -0
  69. data/lib/active_agent/telemetry/reporter.rb +176 -0
  70. data/lib/active_agent/telemetry/span.rb +267 -0
  71. data/lib/active_agent/telemetry/tracer.rb +184 -0
  72. data/lib/active_agent/telemetry.rb +162 -0
  73. data/lib/active_agent/version.rb +1 -1
  74. data/lib/active_agent.rb +2 -0
  75. data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
  76. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
  77. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
  78. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
  79. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
  80. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
  81. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
  82. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
  83. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
  84. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
  85. data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
  86. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
  87. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
  88. metadata +99 -13
@@ -9,7 +9,8 @@ require_relative "concerns/tool_choice_clearing"
9
9
  # @private
10
10
  GEM_LOADERS = {
11
11
  anthropic: [ "anthropic", "~> 1.12", "anthropic" ],
12
- openai: [ "openai", "~> 0.34", "openai" ]
12
+ openai: [ "openai", "~> 0.34", "openai" ],
13
+ ruby_llm: [ "ruby_llm", ">= 1.0", "ruby_llm" ]
13
14
  }
14
15
 
15
16
  # Requires a provider's gem dependency.
@@ -189,9 +189,14 @@ module ActiveAgent
189
189
  when :ping
190
190
  # No-Op Keep Awake
191
191
  when :overloaded_error
192
- # TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
192
+ # TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
193
+
194
+ # Higher-level convenience events from anthropic gem's MessageStream
195
+ when :text, :input_json, :citation, :thinking, :signature
196
+ # No-Op; Already handled via :content_block_delta
197
+
193
198
  else
194
- # No-Op: Looks like internal tracking from gem wrapper
199
+ # No-Op; Internal tracking from gem wrapper
195
200
  return if api_response_chunk.respond_to?(:snapshot)
196
201
  raise "Unexpected chunk type: #{api_response_chunk.type}"
197
202
  end
@@ -296,11 +301,16 @@ module ActiveAgent
296
301
  def process_prompt_finished_extract_function_calls
297
302
  message_stack.pluck(:content).flatten.select { _1 in { type: "tool_use" } }.map do |api_function_call|
298
303
  json_buf = api_function_call.delete(:json_buf)
299
- api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf
304
+ api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf.present?
300
305
 
301
306
  # Handle case where :input is still a JSON string (gem >= 1.14.0)
307
+ # For tools with no parameters, input may be an empty string
302
308
  if api_function_call[:input].is_a?(String)
303
- api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
309
+ if api_function_call[:input].present?
310
+ api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
311
+ else
312
+ api_function_call[:input] = {}
313
+ end
304
314
  end
305
315
 
306
316
  api_function_call
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "options"
4
+ require_relative "../open_ai/chat/_types"
5
+ require_relative "../open_ai/embedding/_types"
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../open_ai/options"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Azure
8
+ # Configuration options for Azure OpenAI Service.
9
+ #
10
+ # Azure OpenAI uses a different authentication and endpoint structure than standard OpenAI:
11
+ # - Endpoint: https://{resource}.openai.azure.com/openai/deployments/{deployment}/
12
+ # - Authentication: api-key header instead of Authorization: Bearer
13
+ # - API Version: Required query parameter
14
+ #
15
+ # You can configure Azure OpenAI in two ways:
16
+ #
17
+ # 1. Using azure_resource and deployment_id (standard Azure OpenAI):
18
+ # @example
19
+ # options = Azure::Options.new(
20
+ # api_key: ENV["AZURE_OPENAI_API_KEY"],
21
+ # azure_resource: "mycompany",
22
+ # deployment_id: "gpt-4-deployment",
23
+ # api_version: "2024-10-21"
24
+ # )
25
+ #
26
+ # 2. Using a direct host/base_url (for custom domains or Azure AI Foundry):
27
+ # @example
28
+ # options = Azure::Options.new(
29
+ # api_key: ENV["AZURE_OPENAI_API_KEY"],
30
+ # host: "https://mycompany.cognitiveservices.azure.com/openai/deployments/gpt-4",
31
+ # api_version: "2024-10-21"
32
+ # )
33
+ class Options < ActiveAgent::Providers::OpenAI::Options
34
+ DEFAULT_API_VERSION = "2024-10-21"
35
+
36
+ attribute :azure_resource, :string
37
+ attribute :deployment_id, :string
38
+ attribute :api_version, :string, fallback: DEFAULT_API_VERSION
39
+
40
+ validates :azure_resource, presence: true, unless: :explicit_host_provided?
41
+ validates :deployment_id, presence: true, unless: :explicit_host_provided?
42
+
43
+ def initialize(kwargs = {})
44
+ kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
45
+ kwargs[:api_version] ||= resolve_api_version(kwargs)
46
+ # Store explicit host before super processes kwargs
47
+ # host is aliased to base_url in parent, so check both
48
+ @explicit_host = kwargs[:host] || kwargs[:base_url]
49
+ super(kwargs)
50
+ end
51
+
52
+ # Returns Azure-specific headers for authentication.
53
+ #
54
+ # Azure uses api-key header instead of Authorization: Bearer.
55
+ #
56
+ # @return [Hash] headers including api-key
57
+ def extra_headers
58
+ { "api-key" => api_key }
59
+ end
60
+
61
+ # Returns Azure-specific query parameters.
62
+ #
63
+ # Azure requires api-version as a query parameter.
64
+ #
65
+ # @return [Hash] query parameters including api-version
66
+ def extra_query
67
+ { "api-version" => api_version }
68
+ end
69
+
70
+ # Builds the base URL for Azure OpenAI API requests.
71
+ #
72
+ # If a direct host/base_url is provided, uses that directly.
73
+ # Otherwise, constructs the URL from azure_resource and deployment_id.
74
+ #
75
+ # @return [String] the Azure OpenAI endpoint URL
76
+ def base_url
77
+ if @explicit_host.present?
78
+ @explicit_host
79
+ elsif azure_resource.present? && deployment_id.present?
80
+ "https://#{azure_resource}.openai.azure.com/openai/deployments/#{deployment_id}"
81
+ else
82
+ raise ArgumentError, "Either host or azure_resource + deployment_id must be provided"
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def explicit_host_provided?
89
+ @explicit_host.present?
90
+ end
91
+
92
+ def resolve_api_key(kwargs)
93
+ kwargs[:api_key] ||
94
+ kwargs[:access_token] ||
95
+ ENV["AZURE_OPENAI_API_KEY"] ||
96
+ ENV["AZURE_OPENAI_ACCESS_TOKEN"]
97
+ end
98
+
99
+ def resolve_api_version(kwargs)
100
+ kwargs[:api_version] ||
101
+ ENV["AZURE_OPENAI_API_VERSION"] ||
102
+ DEFAULT_API_VERSION
103
+ end
104
+
105
+ # Not used as part of Azure OpenAI
106
+ def resolve_organization_id(_settings) = nil
107
+ def resolve_project_id(_settings) = nil
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,2 @@
1
+ # Azure OpenAI, alias for AzureOpenAI service name resolution
2
+ require_relative "azure_provider"
@@ -0,0 +1,2 @@
1
+ # Azure OpenAI, alias for :azure_openai provider reference
2
+ require_relative "azure_provider"
@@ -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,2 @@
1
+ # Azure OpenAI, alternative naming for consistency with other provider aliases
2
+ require_relative "azure_provider"
@@ -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
@@ -142,11 +142,15 @@ module ActiveAgent
142
142
  Common::Messages::Assistant.new(role: "assistant", content: block[:text], name: message.name)
143
143
  when "tool_use"
144
144
  # Create a message with tool use info as string representation
145
- tool_info = "[Tool Use: #{block[:name]}]\nID: #{block[:id]}\nInput: #{JSON.pretty_generate(block[:input])}"
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}"
146
148
  Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
147
149
  when "mcp_tool_use"
148
150
  # Create a message with MCP tool use info
149
- tool_info = "[MCP Tool Use: #{block[:name]}]\nID: #{block[:id]}\nServer: #{block[:server_name]}\nInput: #{JSON.pretty_generate(block[:input] || {})}"
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}"
150
154
  Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
151
155
  when "mcp_tool_result"
152
156
  # Create a message with MCP tool result
@@ -55,6 +55,7 @@ module ActiveAgent
55
55
  yield
56
56
  rescue => exception
57
57
  rescue_with_handler(exception) || raise
58
+ nil # Discard handler return value to prevent polluting raw_response
58
59
  end
59
60
 
60
61
  # Bubbles up exceptions to the Agent's rescue_from if a handler is defined.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "options"
4
+ require_relative "../open_ai/chat/_types"
5
+ require_relative "../open_ai/embedding/_types"
6
+
7
+ module ActiveAgent
8
+ module Providers
9
+ module Gemini
10
+ # Reuse OpenAI Chat request type (same API format)
11
+ RequestType = OpenAI::Chat::RequestType
12
+
13
+ # Reuse OpenAI Embedding types (same API format)
14
+ module Embedding
15
+ RequestType = OpenAI::Embedding::RequestType
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../open_ai/options"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Gemini
8
+ # Configuration options for Gemini provider
9
+ #
10
+ # Extends OpenAI::Options with Gemini-specific settings including
11
+ # the default base URL for Gemini's OpenAI-compatible API endpoint.
12
+ #
13
+ # @example Basic configuration
14
+ # options = Options.new(api_key: 'your-api-key')
15
+ #
16
+ # @example With environment variable
17
+ # # Set GEMINI_API_KEY or GOOGLE_API_KEY
18
+ # options = Options.new({})
19
+ #
20
+ # @see https://ai.google.dev/gemini-api/docs/openai
21
+ class Options < ActiveAgent::Providers::OpenAI::Options
22
+ GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
23
+
24
+ attribute :base_url, :string, fallback: GEMINI_BASE_URL
25
+
26
+ private
27
+
28
+ def resolve_api_key(kwargs)
29
+ kwargs[:api_key] ||
30
+ kwargs[:access_token] ||
31
+ ENV["GEMINI_API_KEY"] ||
32
+ ENV["GOOGLE_API_KEY"]
33
+ end
34
+
35
+ # Not used as part of Gemini
36
+ def resolve_organization_id(kwargs) = nil
37
+ def resolve_project_id(kwargs) = nil
38
+ end
39
+ end
40
+ end
41
+ end