activeagent 1.0.1 → 1.0.3
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/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/concerns/view.rb +1 -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 +2 -1
- data/lib/active_agent/providers/anthropic/request.rb +3 -1
- data/lib/active_agent/providers/anthropic/transforms.rb +88 -0
- data/lib/active_agent/providers/anthropic_provider.rb +14 -4
- 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 +6 -2
- data/lib/active_agent/providers/concerns/exception_handler.rb +1 -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/mock/request.rb +1 -4
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
- data/lib/active_agent/providers/open_ai/chat_provider.rb +5 -3
- data/lib/active_agent/providers/requesty/_types.rb +16 -0
- data/lib/active_agent/providers/requesty/options.rb +39 -0
- data/lib/active_agent/providers/requesty_provider.rb +63 -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 +120 -17
|
@@ -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.
|
|
@@ -71,7 +71,9 @@ module ActiveAgent
|
|
|
71
71
|
# @raise [ArgumentError] when gem model validation fails
|
|
72
72
|
def initialize(**params)
|
|
73
73
|
# Step 1: Extract custom fields that gem doesn't support
|
|
74
|
-
|
|
74
|
+
# Read response_format without deleting - normalize_params will delete and convert it
|
|
75
|
+
# to output_config for json_schema, or drop it for other types.
|
|
76
|
+
@response_format = params[:response_format]
|
|
75
77
|
@stream = params.delete(:stream)
|
|
76
78
|
anthropic_beta = params.delete(:anthropic_beta)
|
|
77
79
|
|
|
@@ -31,6 +31,12 @@ module ActiveAgent
|
|
|
31
31
|
params[:tools] = normalize_tools(params[:tools]) if params[:tools]
|
|
32
32
|
params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice]
|
|
33
33
|
|
|
34
|
+
# Normalize json_schema response_format → output_config (Anthropic's native structured output field)
|
|
35
|
+
if params[:response_format]
|
|
36
|
+
output_config = normalize_response_format(params.delete(:response_format))
|
|
37
|
+
params[:output_config] = output_config if output_config
|
|
38
|
+
end
|
|
39
|
+
|
|
34
40
|
# Handle mcps parameter (common format) -> transforms to mcp_servers (provider format)
|
|
35
41
|
if params[:mcps]
|
|
36
42
|
params[:mcp_servers] = normalize_mcp_servers(params.delete(:mcps))
|
|
@@ -159,6 +165,87 @@ module ActiveAgent
|
|
|
159
165
|
end
|
|
160
166
|
end
|
|
161
167
|
|
|
168
|
+
# Normalizes response_format to Anthropic output_config structure.
|
|
169
|
+
#
|
|
170
|
+
# Supported (ActiveAgent common format):
|
|
171
|
+
# { type: "json_schema", json_schema: { name: "...", schema: {...}, strict: true } }
|
|
172
|
+
# → { format: { type: "json_schema", schema: {...} } }
|
|
173
|
+
#
|
|
174
|
+
# Notes:
|
|
175
|
+
# - Anthropic requires `additionalProperties: false` on all object schemas.
|
|
176
|
+
# This is auto-injected into any object schemas that don't have it set.
|
|
177
|
+
# - Anthropic does not use OpenAI's `name` or `strict` fields in output_config.format.
|
|
178
|
+
# - json_object is not handled here; it remains prompt-emulated.
|
|
179
|
+
# - text is not handled here; Anthropic returns plain text by default.
|
|
180
|
+
#
|
|
181
|
+
# @param format [Hash, Symbol, String] ActiveAgent common response_format
|
|
182
|
+
# @return [Hash] Anthropic output_config hash, or nil if not applicable
|
|
183
|
+
def normalize_response_format(format)
|
|
184
|
+
case format
|
|
185
|
+
when Hash
|
|
186
|
+
format_hash = format.deep_symbolize_keys
|
|
187
|
+
|
|
188
|
+
if format_hash[:type].to_s == "json_schema"
|
|
189
|
+
schema = format_hash[:json_schema]&.dig(:schema)
|
|
190
|
+
if schema
|
|
191
|
+
schema = inject_additional_properties(schema.deep_dup)
|
|
192
|
+
end
|
|
193
|
+
{
|
|
194
|
+
format: {
|
|
195
|
+
type: "json_schema",
|
|
196
|
+
schema: schema
|
|
197
|
+
}.compact
|
|
198
|
+
}
|
|
199
|
+
elsif format_hash[:type].to_s == "json_object"
|
|
200
|
+
# json_object is not handled here; it remains prompt-emulated.
|
|
201
|
+
nil
|
|
202
|
+
elsif format_hash[:type].to_s == "text"
|
|
203
|
+
# text is not handled here; it remains prompt-emulated.
|
|
204
|
+
nil
|
|
205
|
+
else
|
|
206
|
+
# Pass through (already properly structured or Anthropic native format)
|
|
207
|
+
format_hash
|
|
208
|
+
end
|
|
209
|
+
when Symbol, String
|
|
210
|
+
# Bare :json_schema without a schema cannot use native output_config.
|
|
211
|
+
# Anthropic requires a valid JSON Schema — return nil to fall back.
|
|
212
|
+
nil
|
|
213
|
+
else
|
|
214
|
+
format
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Recursively injects `additionalProperties: false` into all object schemas.
|
|
219
|
+
#
|
|
220
|
+
# Anthropic's structured output API requires this field on all object types.
|
|
221
|
+
# Only inserts when not already explicitly set by the user.
|
|
222
|
+
#
|
|
223
|
+
# @param schema [Hash] JSON Schema hash
|
|
224
|
+
# @return [Hash] schema with additionalProperties injected
|
|
225
|
+
def inject_additional_properties(schema)
|
|
226
|
+
return schema unless schema.is_a?(Hash)
|
|
227
|
+
|
|
228
|
+
if schema[:type] == "object" && !schema.key?(:additionalProperties)
|
|
229
|
+
schema[:additionalProperties] = false
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Recurse into nested schemas
|
|
233
|
+
schema[:properties]&.each_value { |prop| inject_additional_properties(prop) }
|
|
234
|
+
schema[:items]&.then { |items| inject_additional_properties(items) }
|
|
235
|
+
schema[:anyOf]&.each { |s| inject_additional_properties(s) }
|
|
236
|
+
schema[:oneOf]&.each { |s| inject_additional_properties(s) }
|
|
237
|
+
schema[:allOf]&.each { |s| inject_additional_properties(s) }
|
|
238
|
+
|
|
239
|
+
if schema[:definitions]
|
|
240
|
+
schema[:definitions].each_value { |defn| inject_additional_properties(defn) }
|
|
241
|
+
end
|
|
242
|
+
if schema[:"$defs"]
|
|
243
|
+
schema[:"$defs"].each_value { |defn| inject_additional_properties(defn) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
schema
|
|
247
|
+
end
|
|
248
|
+
|
|
162
249
|
# Merges consecutive same-role messages into single messages with multiple content blocks.
|
|
163
250
|
#
|
|
164
251
|
# Required by Anthropic API - consecutive messages with the same role must be combined.
|
|
@@ -437,6 +524,7 @@ module ActiveAgent
|
|
|
437
524
|
msg.delete(:model)
|
|
438
525
|
msg.delete(:stop_reason)
|
|
439
526
|
msg.delete(:stop_sequence)
|
|
527
|
+
msg.delete(:stop_details)
|
|
440
528
|
msg.delete(:type)
|
|
441
529
|
msg.delete(:usage)
|
|
442
530
|
end
|
|
@@ -189,9 +189,14 @@ module ActiveAgent
|
|
|
189
189
|
when :ping
|
|
190
190
|
# No-Op Keep Awake
|
|
191
191
|
when :overloaded_error
|
|
192
|
-
|
|
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
|
|
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]
|
|
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,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,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
|