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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/README.md +10 -4
  4. data/lib/active_agent/base.rb +3 -2
  5. data/lib/active_agent/concerns/provider.rb +6 -2
  6. data/lib/active_agent/concerns/rescue.rb +39 -0
  7. data/lib/active_agent/concerns/streaming.rb +2 -1
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
  10. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
  11. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
  12. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
  13. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
  14. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
  15. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
  16. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
  17. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
  18. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
  19. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
  20. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
  21. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
  22. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
  23. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
  24. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
  25. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
  26. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
  27. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
  28. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
  29. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
  30. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
  31. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
  32. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
  33. data/lib/active_agent/dashboard/config/routes.rb +78 -0
  34. data/lib/active_agent/dashboard/engine.rb +39 -0
  35. data/lib/active_agent/dashboard.rb +151 -0
  36. data/lib/active_agent/providers/_base_provider.rb +4 -1
  37. data/lib/active_agent/providers/anthropic/options.rb +4 -6
  38. data/lib/active_agent/providers/anthropic/request.rb +28 -3
  39. data/lib/active_agent/providers/anthropic/transforms.rb +131 -2
  40. data/lib/active_agent/providers/anthropic_provider.rb +97 -30
  41. data/lib/active_agent/providers/azure/_types.rb +5 -0
  42. data/lib/active_agent/providers/azure/options.rb +111 -0
  43. data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
  44. data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
  45. data/lib/active_agent/providers/azure_provider.rb +133 -0
  46. data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
  47. data/lib/active_agent/providers/bedrock/_types.rb +8 -0
  48. data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
  49. data/lib/active_agent/providers/bedrock/options.rb +77 -0
  50. data/lib/active_agent/providers/bedrock_provider.rb +84 -0
  51. data/lib/active_agent/providers/common/messages/_types.rb +42 -31
  52. data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
  53. data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
  54. data/lib/active_agent/providers/concerns/previewable.rb +39 -5
  55. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
  56. data/lib/active_agent/providers/gemini/_types.rb +19 -0
  57. data/lib/active_agent/providers/gemini/options.rb +41 -0
  58. data/lib/active_agent/providers/gemini_provider.rb +94 -0
  59. data/lib/active_agent/providers/open_ai/chat/transforms.rb +120 -4
  60. data/lib/active_agent/providers/open_ai/chat_provider.rb +40 -0
  61. data/lib/active_agent/providers/open_ai/responses/request.rb +17 -2
  62. data/lib/active_agent/providers/open_ai/responses/transforms.rb +135 -0
  63. data/lib/active_agent/providers/open_ai/responses_provider.rb +38 -0
  64. data/lib/active_agent/providers/open_router/request.rb +20 -0
  65. data/lib/active_agent/providers/open_router/transforms.rb +30 -0
  66. data/lib/active_agent/providers/open_router_provider.rb +14 -0
  67. data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
  68. data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
  69. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
  70. data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
  71. data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
  72. data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
  73. data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
  74. data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
  75. data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
  76. data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
  77. data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
  78. data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
  79. data/lib/active_agent/railtie.rb +32 -1
  80. data/lib/active_agent/telemetry/configuration.rb +213 -0
  81. data/lib/active_agent/telemetry/instrumentation.rb +155 -0
  82. data/lib/active_agent/telemetry/reporter.rb +176 -0
  83. data/lib/active_agent/telemetry/span.rb +267 -0
  84. data/lib/active_agent/telemetry/tracer.rb +184 -0
  85. data/lib/active_agent/telemetry.rb +162 -0
  86. data/lib/active_agent/version.rb +1 -1
  87. data/lib/active_agent.rb +2 -0
  88. data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
  89. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
  90. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
  91. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
  92. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
  93. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
  94. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
  95. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
  96. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
  97. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
  98. data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
  99. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
  100. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
  101. metadata +101 -14
@@ -148,6 +148,26 @@ module ActiveAgent
148
148
  self.messages = instructions_messages + current_messages
149
149
  end
150
150
 
151
+ # Gets tool_choice bypassing gem validation
152
+ #
153
+ # OpenRouter supports "any" which isn't valid in OpenAI gem types.
154
+ #
155
+ # @return [String, Hash, nil]
156
+ def tool_choice
157
+ __getobj__.instance_variable_get(:@data)[:tool_choice]
158
+ end
159
+
160
+ # Sets tool_choice bypassing gem validation
161
+ #
162
+ # OpenRouter supports "any" which isn't valid in OpenAI gem types,
163
+ # so we bypass the gem's type validation by setting @data directly.
164
+ #
165
+ # @param value [String, Hash, nil]
166
+ # @return [void]
167
+ def tool_choice=(value)
168
+ __getobj__.instance_variable_get(:@data)[:tool_choice] = value
169
+ end
170
+
151
171
  # Accessor for OpenRouter-specific provider preferences
152
172
  #
153
173
  # @return [Hash, nil]
@@ -62,9 +62,39 @@ module ActiveAgent
62
62
  # Use OpenAI transforms for the base parameters
63
63
  openai_params = OpenAI::Chat::Transforms.normalize_params(params)
64
64
 
65
+ # Override tool_choice normalization for OpenRouter's "any" vs "required" difference
66
+ if openai_params[:tool_choice]
67
+ openai_params[:tool_choice] = normalize_tool_choice(openai_params[:tool_choice])
68
+ end
69
+
65
70
  [ openai_params, openrouter_params ]
66
71
  end
67
72
 
73
+ # Normalizes tools using OpenAI transforms
74
+ #
75
+ # @param tools [Array<Hash>]
76
+ # @return [Array<Hash>]
77
+ def normalize_tools(tools)
78
+ OpenAI::Chat::Transforms.normalize_tools(tools)
79
+ end
80
+
81
+ # Normalizes tool_choice for OpenRouter API differences
82
+ #
83
+ # OpenRouter uses "any" instead of OpenAI's "required" for forcing tool use.
84
+ # Converts common format to OpenRouter-specific format:
85
+ # - "required" (common) → "any" (OpenRouter)
86
+ # - Everything else delegates to OpenAI transforms
87
+ #
88
+ # @param tool_choice [String, Hash, Symbol]
89
+ # @return [String, Hash, Symbol]
90
+ def normalize_tool_choice(tool_choice)
91
+ # Convert "required" to OpenRouter's "any"
92
+ return "any" if tool_choice.to_s == "required"
93
+
94
+ # For everything else, use OpenAI transforms
95
+ OpenAI::Chat::Transforms.normalize_tool_choice(tool_choice)
96
+ end
97
+
68
98
  # Normalizes messages using OpenAI transforms
69
99
  #
70
100
  # @param messages [Array, String, Hash, nil]
@@ -33,6 +33,20 @@ module ActiveAgent
33
33
 
34
34
  protected
35
35
 
36
+ # @see BaseProvider#prepare_prompt_request
37
+ # @return [Request]
38
+ def prepare_prompt_request
39
+ prepare_prompt_request_tools
40
+ super
41
+ end
42
+
43
+ # Returns true if tool_choice == "any" (OpenRouter's equivalent of "required").
44
+ #
45
+ # @return [Boolean]
46
+ def tool_choice_forces_required?
47
+ request.tool_choice == "any"
48
+ end
49
+
36
50
  # Merges streaming delta into the message with role cleanup.
37
51
  #
38
52
  # Overrides parent to handle OpenRouter's role copying behavior which duplicates
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "options"
4
+ require_relative "request"
5
+ require_relative "embedding_request"
6
+
7
+ module ActiveAgent
8
+ module Providers
9
+ module RubyLLM
10
+ # Type for Request model
11
+ class RequestType < ActiveModel::Type::Value
12
+ def cast(value)
13
+ case value
14
+ when Request
15
+ value
16
+ when Hash
17
+ Request.new(**value.deep_symbolize_keys)
18
+ when nil
19
+ nil
20
+ else
21
+ raise ArgumentError, "Cannot cast #{value.class} to Request"
22
+ end
23
+ end
24
+
25
+ def serialize(value)
26
+ case value
27
+ when Request
28
+ value.serialize
29
+ when Hash
30
+ value
31
+ when nil
32
+ nil
33
+ else
34
+ raise ArgumentError, "Cannot serialize #{value.class}"
35
+ end
36
+ end
37
+
38
+ def deserialize(value)
39
+ cast(value)
40
+ end
41
+ end
42
+
43
+ # Type for embedding requests
44
+ class EmbeddingRequestType < ActiveModel::Type::Value
45
+ def cast(value)
46
+ case value
47
+ when RubyLLM::EmbeddingRequest
48
+ value
49
+ when Hash
50
+ RubyLLM::EmbeddingRequest.new(**value.deep_symbolize_keys)
51
+ when nil
52
+ nil
53
+ else
54
+ raise ArgumentError, "Cannot cast #{value.class} to EmbeddingRequest"
55
+ end
56
+ end
57
+
58
+ def serialize(value)
59
+ case value
60
+ when RubyLLM::EmbeddingRequest
61
+ value.serialize
62
+ when Hash
63
+ value
64
+ when nil
65
+ nil
66
+ else
67
+ raise ArgumentError, "Cannot serialize #{value.class}"
68
+ end
69
+ end
70
+
71
+ def deserialize(value)
72
+ cast(value)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/common/model"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module RubyLLM
8
+ # Embedding request model for RubyLLM provider.
9
+ class EmbeddingRequest < Common::BaseModel
10
+ attribute :model, :string
11
+ attribute :input
12
+ attribute :dimensions, :integer
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load all message classes
4
+ require_relative "base"
5
+ require_relative "system"
6
+ require_relative "user"
7
+ require_relative "assistant"
8
+ require_relative "tool"
9
+
10
+ module ActiveAgent
11
+ module Providers
12
+ module RubyLLM
13
+ module Messages
14
+ # Type for Messages array
15
+ class MessagesType < ActiveModel::Type::Value
16
+ def initialize
17
+ super
18
+ @message_type = MessageType.new
19
+ end
20
+
21
+ def cast(value)
22
+ case value
23
+ when Array
24
+ value.map { |v| @message_type.cast(v) }
25
+ when nil
26
+ nil
27
+ else
28
+ raise ArgumentError, "Cannot cast #{value.class} to Messages array"
29
+ end
30
+ end
31
+
32
+ def serialize(value)
33
+ case value
34
+ when Array
35
+ grouped = []
36
+
37
+ value.each do |message|
38
+ if grouped.empty? || grouped.last.role != message.role
39
+ grouped << message.deep_dup
40
+ else
41
+ grouped.last.content += message.content.deep_dup
42
+ end
43
+ end
44
+
45
+ grouped.map { |v| @message_type.serialize(v) }
46
+ when nil
47
+ nil
48
+ else
49
+ raise ArgumentError, "Cannot serialize #{value.class}"
50
+ end
51
+ end
52
+
53
+ def deserialize(value)
54
+ cast(value)
55
+ end
56
+ end
57
+
58
+ # Type for individual Message
59
+ class MessageType < ActiveModel::Type::Value
60
+ def cast(value)
61
+ case value
62
+ when Base
63
+ value
64
+ when String
65
+ User.new(content: value)
66
+ when Hash
67
+ hash = value.deep_symbolize_keys
68
+ role = hash[:role]&.to_sym
69
+
70
+ case role
71
+ when :user, nil
72
+ User.new(**hash)
73
+ when :assistant
74
+ Assistant.new(**hash)
75
+ when :system
76
+ System.new(**hash)
77
+ when :tool
78
+ Tool.new(**hash)
79
+ else
80
+ raise ArgumentError, "Unknown message role: #{role}"
81
+ end
82
+ when nil
83
+ nil
84
+ else
85
+ raise ArgumentError, "Cannot cast #{value.class} to Message"
86
+ end
87
+ end
88
+
89
+ def serialize(value)
90
+ case value
91
+ when Base
92
+ value.serialize
93
+ when Hash
94
+ value
95
+ when nil
96
+ nil
97
+ else
98
+ raise ArgumentError, "Cannot serialize #{value.class}"
99
+ end
100
+ end
101
+
102
+ def deserialize(value)
103
+ cast(value)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module RubyLLM
8
+ module Messages
9
+ # Assistant message for RubyLLM provider.
10
+ #
11
+ # Drops extra fields that are part of the API response but not
12
+ # part of the message structure.
13
+ class Assistant < Base
14
+ attribute :role, :string, as: "assistant"
15
+ attribute :content
16
+ attribute :tool_calls
17
+
18
+ validates :content, presence: true, unless: :tool_calls
19
+
20
+ # Drop API response fields that aren't part of the message
21
+ drop_attributes :usage, :id, :model, :stop_reason, :stop_sequence, :type,
22
+ :input_tokens, :output_tokens
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/common/model"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module RubyLLM
8
+ module Messages
9
+ # Base class for RubyLLM messages.
10
+ class Base < Common::BaseModel
11
+ attribute :role, :string
12
+ attribute :content
13
+
14
+ validates :role, presence: true
15
+
16
+ # Converts to common format.
17
+ #
18
+ # @return [Hash] message in canonical format with role and text content
19
+ def to_common
20
+ {
21
+ role: role,
22
+ content: extract_text_content,
23
+ name: nil
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ # Extracts text content from the content structure.
30
+ #
31
+ # @return [String] extracted text content
32
+ def extract_text_content
33
+ case content
34
+ when String
35
+ content
36
+ when Array
37
+ content.select { |block| block.is_a?(Hash) && block[:type] == "text" }
38
+ .map { |block| block[:text] }
39
+ .join("\n")
40
+ else
41
+ content.to_s
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module RubyLLM
8
+ module Messages
9
+ # System message for RubyLLM provider.
10
+ class System < Base
11
+ attribute :role, :string, as: "system"
12
+
13
+ validates :content, presence: true
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module RubyLLM
8
+ module Messages
9
+ # Tool result message for RubyLLM provider.
10
+ class Tool < Base
11
+ attribute :role, :string, as: "tool"
12
+ attribute :content
13
+ attribute :tool_call_id, :string
14
+
15
+ def to_common
16
+ common = super
17
+ common[:tool_call_id] = tool_call_id if tool_call_id
18
+ common
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module RubyLLM
8
+ module Messages
9
+ # User message for RubyLLM provider.
10
+ class User < Base
11
+ attribute :role, :string, as: "user"
12
+
13
+ validates :content, presence: true
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/common/model"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module RubyLLM
8
+ # Configuration options for the RubyLLM provider.
9
+ #
10
+ # RubyLLM manages its own API keys via RubyLLM.configure, so no
11
+ # provider-specific API key attributes are needed here.
12
+ class Options < Common::BaseModel
13
+ attribute :model, :string
14
+ attribute :temperature, :float
15
+ attribute :max_tokens, :integer
16
+
17
+ def initialize(kwargs = {})
18
+ kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
19
+ super(**deep_compact(kwargs))
20
+ end
21
+
22
+ def extra_headers
23
+ {}
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/common/model"
4
+
5
+ require_relative "messages/_types"
6
+
7
+ module ActiveAgent
8
+ module Providers
9
+ module RubyLLM
10
+ # Request model for RubyLLM provider.
11
+ class Request < Common::BaseModel
12
+ attribute :model, :string
13
+ attribute :messages, Messages::MessagesType.new
14
+ attribute :instructions
15
+ attribute :tools
16
+ attribute :tool_choice
17
+ attribute :temperature, :float
18
+ attribute :max_tokens, :integer
19
+ attribute :stream, :boolean, default: false
20
+ attribute :response_format
21
+
22
+ # Common Format Compatibility
23
+ def message=(value)
24
+ self.messages ||= []
25
+ self.messages << value
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Providers
5
+ module RubyLLM
6
+ # Bridges ActiveAgent tool definitions to RubyLLM's expected tool interface.
7
+ # RubyLLM expects tools as { "name" => tool } where each tool responds to
8
+ # #name, #description, #parameters, #params_schema, and #provider_params.
9
+ class ToolProxy
10
+ attr_reader :name, :description, :parameters
11
+
12
+ def initialize(name:, description:, parameters:)
13
+ @name = name
14
+ @description = description
15
+ @parameters = parameters
16
+ end
17
+
18
+ # RubyLLM checks this first; returns the JSON Schema directly so
19
+ # RubyLLM doesn't try to interpret our parameters as Parameter objects.
20
+ # Deep-stringifies keys to match RubyLLM's internal schema format.
21
+ def params_schema
22
+ deep_stringify(@parameters) if @parameters.is_a?(Hash) && @parameters.any?
23
+ end
24
+
25
+ # RubyLLM merges this into the tool definition
26
+ def provider_params
27
+ {}
28
+ end
29
+
30
+ private
31
+
32
+ def deep_stringify(obj)
33
+ case obj
34
+ when Hash
35
+ obj.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify(v) }
36
+ when Array
37
+ obj.map { |v| deep_stringify(v) }
38
+ else
39
+ obj
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end