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
|
@@ -4,11 +4,13 @@ require_relative "common/response"
|
|
|
4
4
|
require_relative "concerns/exception_handler"
|
|
5
5
|
require_relative "concerns/instrumentation"
|
|
6
6
|
require_relative "concerns/previewable"
|
|
7
|
+
require_relative "concerns/tool_choice_clearing"
|
|
7
8
|
|
|
8
9
|
# @private
|
|
9
10
|
GEM_LOADERS = {
|
|
10
11
|
anthropic: [ "anthropic", "~> 1.12", "anthropic" ],
|
|
11
|
-
openai: [ "openai", "~> 0.34", "openai" ]
|
|
12
|
+
openai: [ "openai", "~> 0.34", "openai" ],
|
|
13
|
+
ruby_llm: [ "ruby_llm", ">= 1.0", "ruby_llm" ]
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
# Requires a provider's gem dependency.
|
|
@@ -45,6 +47,7 @@ module ActiveAgent
|
|
|
45
47
|
include ExceptionHandler
|
|
46
48
|
include Instrumentation
|
|
47
49
|
include Previewable
|
|
50
|
+
include ToolChoiceClearing
|
|
48
51
|
|
|
49
52
|
class ProvidersError < StandardError; end
|
|
50
53
|
|
|
@@ -28,15 +28,13 @@ module ActiveAgent
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def serialize
|
|
31
|
-
super.except(:anthropic_beta)
|
|
32
|
-
hash[:extra_headers] = extra_headers unless extra_headers.blank?
|
|
33
|
-
end
|
|
31
|
+
super.except(:anthropic_beta)
|
|
34
32
|
end
|
|
35
33
|
|
|
34
|
+
# Anthropic gem handles beta headers differently via client.beta
|
|
35
|
+
# rather than via extra_headers in request_options
|
|
36
36
|
def extra_headers
|
|
37
|
-
|
|
38
|
-
"anthropic-beta" => anthropic_beta.presence,
|
|
39
|
-
)
|
|
37
|
+
{}
|
|
40
38
|
end
|
|
41
39
|
|
|
42
40
|
private
|
|
@@ -67,11 +67,13 @@ module ActiveAgent
|
|
|
67
67
|
# @option params [Array<Hash>] :messages required
|
|
68
68
|
# @option params [Integer] :max_tokens (4096)
|
|
69
69
|
# @option params [Hash] :response_format custom field for JSON mode simulation
|
|
70
|
+
# @option params [String] :anthropic_beta beta version for features like MCP
|
|
70
71
|
# @raise [ArgumentError] when gem model validation fails
|
|
71
72
|
def initialize(**params)
|
|
72
73
|
# Step 1: Extract custom fields that gem doesn't support
|
|
73
74
|
@response_format = params.delete(:response_format)
|
|
74
75
|
@stream = params.delete(:stream)
|
|
76
|
+
anthropic_beta = params.delete(:anthropic_beta)
|
|
75
77
|
|
|
76
78
|
# Step 2: Map common format 'instructions' to Anthropic's 'system'
|
|
77
79
|
if params.key?(:instructions)
|
|
@@ -84,10 +86,24 @@ module ActiveAgent
|
|
|
84
86
|
# Step 4: Transform params for gem compatibility
|
|
85
87
|
transformed = Transforms.normalize_params(params)
|
|
86
88
|
|
|
87
|
-
# Step 5:
|
|
88
|
-
|
|
89
|
+
# Step 5: Determine if we need beta params (for MCP or other beta features)
|
|
90
|
+
use_beta = anthropic_beta.present? || transformed[:mcp_servers]&.any?
|
|
89
91
|
|
|
90
|
-
# Step 6:
|
|
92
|
+
# Step 6: Add betas parameter if using beta API
|
|
93
|
+
if use_beta
|
|
94
|
+
# Default to MCP beta version if not specified
|
|
95
|
+
beta_version = anthropic_beta || "mcp-client-2025-04-04"
|
|
96
|
+
transformed[:betas] = [ beta_version ]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Step 7: Create gem model - use Beta version if needed
|
|
100
|
+
gem_model = if use_beta
|
|
101
|
+
::Anthropic::Models::Beta::MessageCreateParams.new(**transformed)
|
|
102
|
+
else
|
|
103
|
+
::Anthropic::Models::MessageCreateParams.new(**transformed)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Step 8: Delegate all method calls to gem model
|
|
91
107
|
super(gem_model)
|
|
92
108
|
rescue ArgumentError => e
|
|
93
109
|
# Re-raise with more context
|
|
@@ -134,6 +150,15 @@ module ActiveAgent
|
|
|
134
150
|
self.system = value
|
|
135
151
|
end
|
|
136
152
|
|
|
153
|
+
# Accessor for MCP servers.
|
|
154
|
+
#
|
|
155
|
+
# Safely returns MCP servers array, defaulting to empty array if not set.
|
|
156
|
+
#
|
|
157
|
+
# @return [Array]
|
|
158
|
+
def mcp_servers
|
|
159
|
+
__getobj__.instance_variable_get(:@data)[:mcp_servers] || []
|
|
160
|
+
end
|
|
161
|
+
|
|
137
162
|
# Removes the last message from the messages array.
|
|
138
163
|
#
|
|
139
164
|
# Used for JSON format simulation to remove the lead-in assistant message.
|
|
@@ -28,9 +28,137 @@ module ActiveAgent
|
|
|
28
28
|
params = params.dup
|
|
29
29
|
params[:messages] = normalize_messages(params[:messages]) if params[:messages]
|
|
30
30
|
params[:system] = normalize_system(params[:system]) if params[:system]
|
|
31
|
+
params[:tools] = normalize_tools(params[:tools]) if params[:tools]
|
|
32
|
+
params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice]
|
|
33
|
+
|
|
34
|
+
# Handle mcps parameter (common format) -> transforms to mcp_servers (provider format)
|
|
35
|
+
if params[:mcps]
|
|
36
|
+
params[:mcp_servers] = normalize_mcp_servers(params.delete(:mcps))
|
|
37
|
+
elsif params[:mcp_servers]
|
|
38
|
+
params[:mcp_servers] = normalize_mcp_servers(params[:mcp_servers])
|
|
39
|
+
end
|
|
40
|
+
|
|
31
41
|
params
|
|
32
42
|
end
|
|
33
43
|
|
|
44
|
+
# Normalizes tools from common format to Anthropic format.
|
|
45
|
+
#
|
|
46
|
+
# Accepts both `parameters` and `input_schema` keys, converting to Anthropic's `input_schema`.
|
|
47
|
+
#
|
|
48
|
+
# @param tools [Array<Hash>]
|
|
49
|
+
# @return [Array<Hash>]
|
|
50
|
+
def normalize_tools(tools)
|
|
51
|
+
return tools unless tools.is_a?(Array)
|
|
52
|
+
|
|
53
|
+
tools.map do |tool|
|
|
54
|
+
tool_hash = tool.is_a?(Hash) ? tool.deep_symbolize_keys : tool
|
|
55
|
+
|
|
56
|
+
# If already in Anthropic format (has input_schema), return as-is
|
|
57
|
+
next tool_hash if tool_hash[:input_schema]
|
|
58
|
+
|
|
59
|
+
# Convert common format with 'parameters' to Anthropic format with 'input_schema'
|
|
60
|
+
if tool_hash[:parameters]
|
|
61
|
+
tool_hash = tool_hash.dup
|
|
62
|
+
tool_hash[:input_schema] = tool_hash.delete(:parameters)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
tool_hash
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Normalizes MCP servers from common format to Anthropic format.
|
|
70
|
+
#
|
|
71
|
+
# Common format:
|
|
72
|
+
# {name: "stripe", url: "https://...", authorization: "token"}
|
|
73
|
+
# Anthropic format:
|
|
74
|
+
# {type: "url", name: "stripe", url: "https://...", authorization_token: "token"}
|
|
75
|
+
#
|
|
76
|
+
# @param mcp_servers [Array<Hash>]
|
|
77
|
+
# @return [Array<Hash>]
|
|
78
|
+
def normalize_mcp_servers(mcp_servers)
|
|
79
|
+
return mcp_servers unless mcp_servers.is_a?(Array)
|
|
80
|
+
|
|
81
|
+
mcp_servers.map do |server|
|
|
82
|
+
server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server
|
|
83
|
+
|
|
84
|
+
# If already in Anthropic native format (has type: "url"), return as-is
|
|
85
|
+
# Check for absence of common format 'authorization' field OR presence of native 'authorization_token'
|
|
86
|
+
if server_hash[:type] == "url" && (server_hash[:authorization_token] || !server_hash[:authorization])
|
|
87
|
+
next server_hash
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Convert common format to Anthropic format
|
|
91
|
+
result = {
|
|
92
|
+
type: "url",
|
|
93
|
+
name: server_hash[:name],
|
|
94
|
+
url: server_hash[:url]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Map 'authorization' to 'authorization_token'
|
|
98
|
+
if server_hash[:authorization]
|
|
99
|
+
result[:authorization_token] = server_hash[:authorization]
|
|
100
|
+
elsif server_hash[:authorization_token]
|
|
101
|
+
result[:authorization_token] = server_hash[:authorization_token]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
result.compact
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Normalizes tool_choice from common format to Anthropic gem model objects.
|
|
109
|
+
#
|
|
110
|
+
# The Anthropic gem expects tool_choice to be a model object (ToolChoiceAuto,
|
|
111
|
+
# ToolChoiceAny, ToolChoiceTool, etc.), not a plain hash.
|
|
112
|
+
#
|
|
113
|
+
# Maps:
|
|
114
|
+
# - "required" → ToolChoiceAny (force tool use)
|
|
115
|
+
# - "auto" → ToolChoiceAuto (let model decide)
|
|
116
|
+
# - { name: "..." } → ToolChoiceTool with name
|
|
117
|
+
#
|
|
118
|
+
# @param tool_choice [String, Hash, Object]
|
|
119
|
+
# @return [Object] Anthropic gem model object
|
|
120
|
+
def normalize_tool_choice(tool_choice)
|
|
121
|
+
# If already a gem model object, return as-is
|
|
122
|
+
return tool_choice if tool_choice.is_a?(::Anthropic::Models::ToolChoiceAuto) ||
|
|
123
|
+
tool_choice.is_a?(::Anthropic::Models::ToolChoiceAny) ||
|
|
124
|
+
tool_choice.is_a?(::Anthropic::Models::ToolChoiceTool) ||
|
|
125
|
+
tool_choice.is_a?(::Anthropic::Models::ToolChoiceNone)
|
|
126
|
+
|
|
127
|
+
case tool_choice
|
|
128
|
+
when "required"
|
|
129
|
+
# Create ToolChoiceAny model for forcing tool use
|
|
130
|
+
::Anthropic::Models::ToolChoiceAny.new(type: :any)
|
|
131
|
+
when "auto"
|
|
132
|
+
# Create ToolChoiceAuto model for letting model decide
|
|
133
|
+
::Anthropic::Models::ToolChoiceAuto.new(type: :auto)
|
|
134
|
+
when Hash
|
|
135
|
+
choice_hash = tool_choice.deep_symbolize_keys
|
|
136
|
+
|
|
137
|
+
# If has type field, create appropriate model
|
|
138
|
+
if choice_hash[:type]
|
|
139
|
+
case choice_hash[:type].to_sym
|
|
140
|
+
when :any
|
|
141
|
+
::Anthropic::Models::ToolChoiceAny.new(**choice_hash)
|
|
142
|
+
when :auto
|
|
143
|
+
::Anthropic::Models::ToolChoiceAuto.new(**choice_hash)
|
|
144
|
+
when :tool
|
|
145
|
+
::Anthropic::Models::ToolChoiceTool.new(**choice_hash)
|
|
146
|
+
when :none
|
|
147
|
+
::Anthropic::Models::ToolChoiceNone.new(**choice_hash)
|
|
148
|
+
else
|
|
149
|
+
choice_hash
|
|
150
|
+
end
|
|
151
|
+
# Convert { name: "..." } to ToolChoiceTool
|
|
152
|
+
elsif choice_hash[:name]
|
|
153
|
+
::Anthropic::Models::ToolChoiceTool.new(type: :tool, name: choice_hash[:name])
|
|
154
|
+
else
|
|
155
|
+
choice_hash
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
tool_choice
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
34
162
|
# Merges consecutive same-role messages into single messages with multiple content blocks.
|
|
35
163
|
#
|
|
36
164
|
# Required by Anthropic API - consecutive messages with the same role must be combined.
|
|
@@ -317,9 +445,10 @@ module ActiveAgent
|
|
|
317
445
|
# Apply content compression for API efficiency
|
|
318
446
|
compress_content(hash)
|
|
319
447
|
|
|
320
|
-
# Remove provider-internal fields
|
|
321
|
-
hash.delete(:mcp_servers) # Provider-level config, not API param
|
|
448
|
+
# Remove provider-internal fields and empty arrays
|
|
322
449
|
hash.delete(:stop_sequences) if hash[:stop_sequences] == []
|
|
450
|
+
hash.delete(:mcp_servers) if hash[:mcp_servers] == []
|
|
451
|
+
hash.delete(:tool_choice) if hash[:tool_choice].nil? # Don't send null tool_choice
|
|
323
452
|
|
|
324
453
|
# Remove default values (except max_tokens which is required by API)
|
|
325
454
|
defaults.each do |key, value|
|
|
@@ -17,6 +17,17 @@ module ActiveAgent
|
|
|
17
17
|
#
|
|
18
18
|
# @see BaseProvider
|
|
19
19
|
class AnthropicProvider < BaseProvider
|
|
20
|
+
# Lead-in message for JSON response format emulation
|
|
21
|
+
JSON_RESPONSE_FORMAT_LEAD_IN = "Here is the JSON requested:\n{"
|
|
22
|
+
|
|
23
|
+
attr_internal :json_format_retry_count
|
|
24
|
+
|
|
25
|
+
def initialize(kwargs = {})
|
|
26
|
+
super
|
|
27
|
+
|
|
28
|
+
self.json_format_retry_count = kwargs[:max_retries] || ::Anthropic::Client::DEFAULT_MAX_RETRIES
|
|
29
|
+
end
|
|
30
|
+
|
|
20
31
|
# @todo Add support for Anthropic::BedrockClient and Anthropic::VertexClient
|
|
21
32
|
# @return [Anthropic::Client]
|
|
22
33
|
def client
|
|
@@ -39,22 +50,31 @@ module ActiveAgent
|
|
|
39
50
|
super
|
|
40
51
|
end
|
|
41
52
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
# Extracts function names from Anthropic's tool_use content blocks.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array<String>]
|
|
56
|
+
def extract_used_function_names
|
|
57
|
+
message_stack.pluck(:content).flatten.select { _1[:type] == "tool_use" }.pluck(:name)
|
|
58
|
+
end
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
# Checks if tool_choice requires the model to call any tool.
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean] true if tool_choice type is :any
|
|
63
|
+
def tool_choice_forces_required?
|
|
64
|
+
return false unless request.tool_choice.respond_to?(:type)
|
|
48
65
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
tool_choice_name = request.tool_choice.respond_to?(:name) ? request.tool_choice.name : nil
|
|
66
|
+
request.tool_choice.type == :any
|
|
67
|
+
end
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
# Checks if tool_choice requires a specific tool to be called.
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<Boolean, String|nil>] [true, tool_name] if forcing a specific tool, [false, nil] otherwise
|
|
72
|
+
def tool_choice_forces_specific?
|
|
73
|
+
return [ false, nil ] unless request.tool_choice.respond_to?(:type)
|
|
74
|
+
return [ false, nil ] unless request.tool_choice.type == :tool
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
tool_name = request.tool_choice.respond_to?(:name) ? request.tool_choice.name : nil
|
|
77
|
+
[ true, tool_name ]
|
|
58
78
|
end
|
|
59
79
|
|
|
60
80
|
# @api private
|
|
@@ -63,19 +83,30 @@ module ActiveAgent
|
|
|
63
83
|
|
|
64
84
|
self.message_stack.push({
|
|
65
85
|
role: "assistant",
|
|
66
|
-
content:
|
|
86
|
+
content: JSON_RESPONSE_FORMAT_LEAD_IN
|
|
67
87
|
})
|
|
68
88
|
end
|
|
69
89
|
|
|
90
|
+
# Selects between Anthropic's stable and beta message APIs.
|
|
91
|
+
#
|
|
92
|
+
# Uses beta API when explicitly requested via anthropic_beta option or when
|
|
93
|
+
# using MCP servers, which require beta features. Falls back to stable API
|
|
94
|
+
# for standard message creation.
|
|
95
|
+
#
|
|
70
96
|
# @see BaseProvider#api_prompt_executer
|
|
71
|
-
# @return [Anthropic::Messages]
|
|
97
|
+
# @return [Anthropic::Messages, Anthropic::Resources::Beta::Messages]
|
|
72
98
|
def api_prompt_executer
|
|
73
|
-
|
|
99
|
+
# Use beta API when anthropic_beta option is set or when using MCP servers
|
|
100
|
+
if options.anthropic_beta.present? || request.mcp_servers&.any?
|
|
101
|
+
client.beta.messages
|
|
102
|
+
else
|
|
103
|
+
client.messages
|
|
104
|
+
end
|
|
74
105
|
end
|
|
75
106
|
|
|
76
107
|
# @see BaseProvider#api_response_normalize
|
|
77
108
|
# @param api_response [Anthropic::Models::Message]
|
|
78
|
-
# @return [Hash]
|
|
109
|
+
# @return [Hash]
|
|
79
110
|
def api_response_normalize(api_response)
|
|
80
111
|
return api_response unless api_response
|
|
81
112
|
|
|
@@ -158,9 +189,14 @@ module ActiveAgent
|
|
|
158
189
|
when :ping
|
|
159
190
|
# No-Op Keep Awake
|
|
160
191
|
when :overloaded_error
|
|
161
|
-
|
|
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
|
+
|
|
162
198
|
else
|
|
163
|
-
# No-Op
|
|
199
|
+
# No-Op; Internal tracking from gem wrapper
|
|
164
200
|
return if api_response_chunk.respond_to?(:snapshot)
|
|
165
201
|
raise "Unexpected chunk type: #{api_response_chunk.type}"
|
|
166
202
|
end
|
|
@@ -200,31 +236,57 @@ module ActiveAgent
|
|
|
200
236
|
end
|
|
201
237
|
end
|
|
202
238
|
|
|
203
|
-
#
|
|
204
|
-
#
|
|
239
|
+
# Processes completed API response and handles JSON format retries.
|
|
240
|
+
#
|
|
241
|
+
# When response_format is json_object and the response fails JSON validation,
|
|
242
|
+
# recursively retries the request to obtain well-formed JSON.
|
|
205
243
|
#
|
|
244
|
+
# @see BaseProvider#process_prompt_finished
|
|
206
245
|
# @param api_response [Anthropic::Models::Message]
|
|
207
246
|
# @return [Common::PromptResponse, nil]
|
|
208
247
|
def process_prompt_finished(api_response = nil)
|
|
209
248
|
# Convert gem object to hash so that raw_response[:usage] works
|
|
210
249
|
api_response_hash = api_response ? Anthropic::Transforms.gem_to_hash(api_response) : nil
|
|
211
|
-
|
|
250
|
+
|
|
251
|
+
common_response = super(api_response_hash)
|
|
252
|
+
|
|
253
|
+
# If we failed to get the expected well formed JSON Object Response, recursively try again
|
|
254
|
+
if request.response_format&.dig(:type) == "json_object" && common_response.message.parsed_json.nil? && json_format_retry_count > 0
|
|
255
|
+
self.json_format_retry_count -= 1
|
|
256
|
+
|
|
257
|
+
resolve_prompt
|
|
258
|
+
else
|
|
259
|
+
common_response
|
|
260
|
+
end
|
|
212
261
|
end
|
|
213
262
|
|
|
263
|
+
# Reconstructs JSON responses that were split due to Anthropic format constraints.
|
|
214
264
|
#
|
|
215
|
-
#
|
|
216
|
-
#
|
|
265
|
+
# Anthropic's API doesn't natively support json_object response format, so we
|
|
266
|
+
# simulate it by having the assistant echo a JSON lead-in ("Here is the JSON requested:\n{"),
|
|
267
|
+
# then send the response back for completion. This method detects and reverses
|
|
268
|
+
# that workaround by stripping the lead-in message and prepending "{" to the response.
|
|
217
269
|
#
|
|
218
270
|
# @see BaseProvider#process_prompt_finished_extract_messages
|
|
219
|
-
# @param api_response [Hash]
|
|
271
|
+
# @param api_response [Hash] API response with content blocks
|
|
220
272
|
# @return [Array<Hash>, nil]
|
|
221
273
|
def process_prompt_finished_extract_messages(api_response)
|
|
222
274
|
return unless api_response
|
|
223
275
|
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
276
|
+
# Get the last message (may be either Hash or gem object)
|
|
277
|
+
last_message = request.messages.last
|
|
278
|
+
last_role = last_message.is_a?(Hash) ? last_message[:role] : last_message&.role
|
|
279
|
+
last_content = last_message.is_a?(Hash) ? last_message[:content] : last_message&.content
|
|
280
|
+
|
|
281
|
+
# Check if the last message in request is the JSON lead-in prompt
|
|
282
|
+
if last_role.to_sym == :assistant && last_content == JSON_RESPONSE_FORMAT_LEAD_IN
|
|
283
|
+
# Remove the lead-in message from the request
|
|
284
|
+
request.messages.pop
|
|
285
|
+
|
|
286
|
+
# Prepend "{" to the response's first content text
|
|
287
|
+
if api_response[:content]&.first&.dig(:text)
|
|
288
|
+
api_response[:content][0][:text] = "{#{api_response[:content][0][:text]}"
|
|
289
|
+
end
|
|
228
290
|
end
|
|
229
291
|
|
|
230
292
|
[ api_response ]
|
|
@@ -239,11 +301,16 @@ module ActiveAgent
|
|
|
239
301
|
def process_prompt_finished_extract_function_calls
|
|
240
302
|
message_stack.pluck(:content).flatten.select { _1 in { type: "tool_use" } }.map do |api_function_call|
|
|
241
303
|
json_buf = api_function_call.delete(:json_buf)
|
|
242
|
-
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?
|
|
243
305
|
|
|
244
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
|
|
245
308
|
if api_function_call[:input].is_a?(String)
|
|
246
|
-
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
|
|
247
314
|
end
|
|
248
315
|
|
|
249
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
|