Module: ActiveAgent::Providers::Anthropic::Transforms
- Defined in:
- lib/active_agent/providers/anthropic/transforms.rb
Overview
Transforms between convenient input formats and Anthropic API structures.
Provides bidirectional transformations:
-
Expand: shortcuts → API format (string → content blocks, consecutive messages → grouped)
-
Compress: API format → shortcuts (single content blocks → strings for efficiency)
Class Method Summary collapse
-
.cleanup_serialized_request(hash, defaults, gem_object = nil) ⇒ Hash
Cleans up serialized request for API submission.
-
.compress_content(hash) ⇒ Hash
Converts single-element content arrays back to string shorthand.
-
.compress_message_content!(msg) ⇒ void
Converts single text block arrays to string shorthand.
-
.gem_to_hash(gem_object) ⇒ Hash
Converts gem model object to hash via JSON round-trip.
-
.normalize_content(content) ⇒ Array<Hash>
Expands content shortcuts into structured content block arrays.
-
.normalize_content_item(item) ⇒ Hash
Infers content block type from hash keys or converts string to text block.
-
.normalize_mcp_servers(mcp_servers) ⇒ Array<Hash>
Normalizes MCP servers from common format to Anthropic format.
-
.normalize_messages(messages) ⇒ Array<Hash>
Merges consecutive same-role messages into single messages with multiple content blocks.
- .normalize_params(params) ⇒ Hash
-
.normalize_source(source) ⇒ Hash
Converts image/document source shortcuts to API structure.
-
.normalize_system(system) ⇒ String, Array
Converts system content shortcuts to API format.
- .normalize_system_block(block) ⇒ Hash
-
.normalize_tool_choice(tool_choice) ⇒ Object
Normalizes tool_choice from common format to Anthropic gem model objects.
-
.normalize_tools(tools) ⇒ Array<Hash>
Normalizes tools from common format to Anthropic format.
-
.parse_data_uri(data_uri) ⇒ Hash
Extracts media type and data from data URI.
Class Method Details
.cleanup_serialized_request(hash, defaults, gem_object = nil) ⇒ Hash
Cleans up serialized request for API submission.
Removes response-only fields, applies content compression, removes provider-internal fields, and removes default values. Note: max_tokens is kept even if it matches default as Anthropic API requires it.
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 432 def cleanup_serialized_request(hash, defaults, gem_object = nil) # Remove response-only fields from messages if hash[:messages] hash[:messages].each do |msg| msg.delete(:id) msg.delete(:model) msg.delete(:stop_reason) msg.delete(:stop_sequence) msg.delete(:type) msg.delete(:usage) end end # Apply content compression for API efficiency compress_content(hash) # Remove provider-internal fields and empty arrays hash.delete(:stop_sequences) if hash[:stop_sequences] == [] hash.delete(:mcp_servers) if hash[:mcp_servers] == [] hash.delete(:tool_choice) if hash[:tool_choice].nil? # Don't send null tool_choice # Remove default values (except max_tokens which is required by API) defaults.each do |key, value| next if key == :max_tokens # Anthropic API requires max_tokens hash.delete(key) if hash[key] == value end hash end |
.compress_content(hash) ⇒ Hash
Converts single-element content arrays back to string shorthand.
Reduces payload size by reversing the expansion done by normalize methods.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 390 def compress_content(hash) return hash unless hash.is_a?(Hash) # Compress message content hash[:messages]&.each do |msg| (msg) end # Compress system content if hash[:system].is_a?(Array) hash[:system] = compress_system_content(hash[:system]) end hash end |
.compress_message_content!(msg) ⇒ void
This method returns an undefined value.
Converts single text block arrays to string shorthand.
‘[“text”, text: “hello”]` → `“hello”`
412 413 414 415 416 417 418 419 420 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 412 def (msg) content = msg[:content] return unless content.is_a?(Array) # Single text block → string shorthand if content.one? && content.first.is_a?(Hash) && content.first[:type] == "text" msg[:content] = content.first[:text] end end |
.gem_to_hash(gem_object) ⇒ Hash
Converts gem model object to hash via JSON round-trip.
This ensures proper nested serialization and symbolic keys.
21 22 23 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 21 def gem_to_hash(gem_object) JSON.parse(gem_object.to_json, symbolize_names: true) end |
.normalize_content(content) ⇒ Array<Hash>
Expands content shortcuts into structured content block arrays.
Handles multiple input formats:
-
String → ‘[“text”, text: “…”]`
-
Hash with multiple keys → separate blocks per content type
-
Array → normalized items
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 254 def normalize_content(content) case content when String # String → array with single text block [ { type: "text", text: content } ] when Array # Normalize each item in the array content.flat_map { |item| normalize_content_item(item) } when Hash # Check if hash has multiple content keys (text, image, document) # If so, expand into separate content blocks hash = content.deep_symbolize_keys content_keys = [ :text, :image, :document ] found_keys = content_keys & hash.keys if found_keys.size > 1 # Multiple content types - expand into array found_keys.flat_map { |key| normalize_content_item({ key => hash[key] }) } else # Single content item [ normalize_content_item(content) ] end when nil [] else # Pass through other types (might be gem objects already) [ content ] end end |
.normalize_content_item(item) ⇒ Hash
Infers content block type from hash keys or converts string to text block.
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 288 def normalize_content_item(item) case item when String { type: "text", text: item } when Hash hash = item.deep_symbolize_keys # If type is specified, return as-is return hash if hash[:type] # Type inference based on keys if hash[:text] { type: "text" }.merge(hash) elsif hash[:image] # Normalize image source format source = normalize_source(hash[:image]) { type: "image", source: source }.merge(hash.except(:image)) elsif hash[:document] # Normalize document source format source = normalize_source(hash[:document]) { type: "document", source: source }.merge(hash.except(:document)) elsif hash[:tool_use_id] # Tool result content { type: "tool_result" }.merge(hash) elsif hash[:id] && hash[:name] && hash[:input] # Tool use content { type: "tool_use" }.merge(hash) else # Unknown format - return as-is and let gem validate hash end else # Pass through (might be gem object) item end end |
.normalize_mcp_servers(mcp_servers) ⇒ Array<Hash>
Normalizes MCP servers from common format to Anthropic format.
Common format:
{name: "stripe", url: "https://...", authorization: "token"}
Anthropic format:
{type: "url", name: "stripe", url: "https://...", authorization_token: "token"}
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 78 def normalize_mcp_servers(mcp_servers) return mcp_servers unless mcp_servers.is_a?(Array) mcp_servers.map do |server| server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server # If already in Anthropic native format (has type: "url"), return as-is # Check for absence of common format 'authorization' field OR presence of native 'authorization_token' if server_hash[:type] == "url" && (server_hash[:authorization_token] || !server_hash[:authorization]) next server_hash end # Convert common format to Anthropic format result = { type: "url", name: server_hash[:name], url: server_hash[:url] } # Map 'authorization' to 'authorization_token' if server_hash[:authorization] result[:authorization_token] = server_hash[:authorization] elsif server_hash[:authorization_token] result[:authorization_token] = server_hash[:authorization_token] end result.compact end end |
.normalize_messages(messages) ⇒ Array<Hash>
Merges consecutive same-role messages into single messages with multiple content blocks.
Required by Anthropic API - consecutive messages with the same role must be combined.
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 168 def () return unless .is_a?(Array) grouped = [] .each do |msg| msg_hash = msg.is_a?(Hash) ? msg.deep_symbolize_keys : { role: :user, content: msg } # Extract role role = msg_hash[:role]&.to_sym || :user # Determine content if msg_hash.key?(:content) # Has explicit content key content = normalize_content(msg_hash[:content]) elsif msg_hash.key?(:role) && msg_hash.keys.size > 1 # Has role + other keys (e.g., {role: "assistant", text: "..."}) # Treat everything except :role as content content = normalize_content(msg_hash.except(:role)) elsif !msg_hash.key?(:role) # No role or content - treat entire hash as content content = normalize_content(msg_hash) else # Only has role, no content content = [] end if grouped.empty? || grouped.last[:role] != role grouped << { role: role, content: content } else # Merge content from consecutive same-role messages grouped.last[:content] += content end end grouped end |
.normalize_params(params) ⇒ Hash
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 27 def normalize_params(params) params = params.dup params[:messages] = (params[:messages]) if params[:messages] params[:system] = normalize_system(params[:system]) if params[:system] params[:tools] = normalize_tools(params[:tools]) if params[:tools] params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] # Handle mcps parameter (common format) -> transforms to mcp_servers (provider format) if params[:mcps] params[:mcp_servers] = normalize_mcp_servers(params.delete(:mcps)) elsif params[:mcp_servers] params[:mcp_servers] = normalize_mcp_servers(params[:mcp_servers]) end params end |
.normalize_source(source) ⇒ Hash
Converts image/document source shortcuts to API structure.
Handles multiple formats:
-
Regular URL → ‘“url”, url: “…”`
-
Data URI → ‘“base64”, media_type: “…”, data: “…”`
-
Hash with base64 → ‘“base64”, media_type: “…”, data: “…”`
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 334 def normalize_source(source) case source when String # Check if it's a data URI (e.g., "data:image/png;base64,...") if source.start_with?("data:") parse_data_uri(source) else # Regular URL → wrap in url source type { type: "url", url: source } end when Hash hash = source.deep_symbolize_keys # Already has type → return as-is return hash if hash[:type] # Has base64 data → add type if hash[:data] && hash[:media_type] { type: "base64" }.merge(hash) else # Unknown format → return as-is hash end else source end end |
.normalize_system(system) ⇒ String, Array
Converts system content shortcuts to API format.
Handles string, hash, or array inputs. Strings pass through unchanged since Anthropic accepts both string and structured formats.
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 213 def normalize_system(system) case system when String # Keep strings as-is - Anthropic accepts both string and array system when Array # Normalize array of system blocks system.map { |block| normalize_system_block(block) } when Hash # Single hash becomes array with one block [ normalize_system_block(system) ] else system end end |
.normalize_system_block(block) ⇒ Hash
231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 231 def normalize_system_block(block) case block when String { type: "text", text: block } when Hash hash = block.deep_symbolize_keys # Add type if missing and can be inferred hash[:type] ||= "text" if hash[:text] hash else block end end |
.normalize_tool_choice(tool_choice) ⇒ Object
Normalizes tool_choice from common format to Anthropic gem model objects.
The Anthropic gem expects tool_choice to be a model object (ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool, etc.), not a plain hash.
Maps:
-
“required” → ToolChoiceAny (force tool use)
-
“auto” → ToolChoiceAuto (let model decide)
-
{ name: “…” } → ToolChoiceTool with name
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 120 def normalize_tool_choice(tool_choice) # If already a gem model object, return as-is return tool_choice if tool_choice.is_a?(::Anthropic::Models::ToolChoiceAuto) || tool_choice.is_a?(::Anthropic::Models::ToolChoiceAny) || tool_choice.is_a?(::Anthropic::Models::ToolChoiceTool) || tool_choice.is_a?(::Anthropic::Models::ToolChoiceNone) case tool_choice when "required" # Create ToolChoiceAny model for forcing tool use ::Anthropic::Models::ToolChoiceAny.new(type: :any) when "auto" # Create ToolChoiceAuto model for letting model decide ::Anthropic::Models::ToolChoiceAuto.new(type: :auto) when Hash choice_hash = tool_choice.deep_symbolize_keys # If has type field, create appropriate model if choice_hash[:type] case choice_hash[:type].to_sym when :any ::Anthropic::Models::ToolChoiceAny.new(**choice_hash) when :auto ::Anthropic::Models::ToolChoiceAuto.new(**choice_hash) when :tool ::Anthropic::Models::ToolChoiceTool.new(**choice_hash) when :none ::Anthropic::Models::ToolChoiceNone.new(**choice_hash) else choice_hash end # Convert { name: "..." } to ToolChoiceTool elsif choice_hash[:name] ::Anthropic::Models::ToolChoiceTool.new(type: :tool, name: choice_hash[:name]) else choice_hash end else tool_choice end end |
.normalize_tools(tools) ⇒ Array<Hash>
Normalizes tools from common format to Anthropic format.
Accepts both ‘parameters` and `input_schema` keys, converting to Anthropic’s ‘input_schema`.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 50 def normalize_tools(tools) return tools unless tools.is_a?(Array) tools.map do |tool| tool_hash = tool.is_a?(Hash) ? tool.deep_symbolize_keys : tool # If already in Anthropic format (has input_schema), return as-is next tool_hash if tool_hash[:input_schema] # Convert common format with 'parameters' to Anthropic format with 'input_schema' if tool_hash[:parameters] tool_hash = tool_hash.dup tool_hash[:input_schema] = tool_hash.delete(:parameters) end tool_hash end end |
.parse_data_uri(data_uri) ⇒ Hash
Extracts media type and data from data URI.
Expected format: ‘data:[<media type>],<data>`
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 367 def parse_data_uri(data_uri) # Extract media type and data from data URI # Format: data:[<media type>][;base64],<data> match = data_uri.match(%r{\Adata:([^;,]+)(?:;base64)?,(.+)\z}) if match { type: "base64", media_type: match[1], data: match[2] } else # Invalid data URI - return as URL fallback { type: "url", url: data_uri } end end |