activeagent 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -4
  3. data/lib/active_agent/base.rb +3 -2
  4. data/lib/active_agent/concerns/provider.rb +6 -2
  5. data/lib/active_agent/concerns/rescue.rb +39 -0
  6. data/lib/active_agent/concerns/streaming.rb +2 -1
  7. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
  10. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
  11. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
  12. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
  13. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
  14. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
  15. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
  16. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
  17. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
  18. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
  19. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
  20. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
  21. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
  22. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
  23. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
  24. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
  25. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
  26. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
  27. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
  28. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
  29. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
  30. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
  31. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
  32. data/lib/active_agent/dashboard/config/routes.rb +78 -0
  33. data/lib/active_agent/dashboard/engine.rb +39 -0
  34. data/lib/active_agent/dashboard.rb +151 -0
  35. data/lib/active_agent/providers/_base_provider.rb +2 -1
  36. data/lib/active_agent/providers/anthropic_provider.rb +14 -4
  37. data/lib/active_agent/providers/azure/_types.rb +5 -0
  38. data/lib/active_agent/providers/azure/options.rb +111 -0
  39. data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
  40. data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
  41. data/lib/active_agent/providers/azure_provider.rb +133 -0
  42. data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
  43. data/lib/active_agent/providers/bedrock/_types.rb +8 -0
  44. data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
  45. data/lib/active_agent/providers/bedrock/options.rb +77 -0
  46. data/lib/active_agent/providers/bedrock_provider.rb +84 -0
  47. data/lib/active_agent/providers/common/messages/_types.rb +6 -2
  48. data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
  49. data/lib/active_agent/providers/gemini/_types.rb +19 -0
  50. data/lib/active_agent/providers/gemini/options.rb +41 -0
  51. data/lib/active_agent/providers/gemini_provider.rb +94 -0
  52. data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
  53. data/lib/active_agent/providers/open_ai/chat_provider.rb +2 -0
  54. data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
  55. data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
  56. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
  57. data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
  58. data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
  59. data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
  60. data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
  61. data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
  62. data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
  63. data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
  64. data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
  65. data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
  66. data/lib/active_agent/railtie.rb +32 -1
  67. data/lib/active_agent/telemetry/configuration.rb +213 -0
  68. data/lib/active_agent/telemetry/instrumentation.rb +155 -0
  69. data/lib/active_agent/telemetry/reporter.rb +176 -0
  70. data/lib/active_agent/telemetry/span.rb +267 -0
  71. data/lib/active_agent/telemetry/tracer.rb +184 -0
  72. data/lib/active_agent/telemetry.rb +162 -0
  73. data/lib/active_agent/version.rb +1 -1
  74. data/lib/active_agent.rb +2 -0
  75. data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
  76. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
  77. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
  78. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
  79. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
  80. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
  81. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
  82. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
  83. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
  84. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
  85. data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
  86. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
  87. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
  88. metadata +99 -13
@@ -0,0 +1,94 @@
1
+ require_relative "_base_provider"
2
+
3
+ require_gem!(:openai, __FILE__)
4
+
5
+ require_relative "open_ai_provider"
6
+ require_relative "gemini/_types"
7
+
8
+ module ActiveAgent
9
+ module Providers
10
+ # Provides access to Google's Gemini API via OpenAI-compatible endpoint.
11
+ #
12
+ # Extends OpenAI provider to work with Gemini's OpenAI-compatible API,
13
+ # enabling access to Gemini models through a familiar interface.
14
+ #
15
+ # @see OpenAI::ChatProvider
16
+ # @see https://ai.google.dev/gemini-api/docs/openai
17
+ class GeminiProvider < OpenAI::ChatProvider
18
+ # @return [String]
19
+ def self.service_name
20
+ "Gemini"
21
+ end
22
+
23
+ # @return [Class]
24
+ def self.options_klass
25
+ namespace::Options
26
+ end
27
+
28
+ # @return [ActiveModel::Type::Value]
29
+ def self.prompt_request_type
30
+ namespace::RequestType.new
31
+ end
32
+
33
+ # @return [ActiveModel::Type::Value]
34
+ def self.embed_request_type
35
+ namespace::Embedding::RequestType.new
36
+ end
37
+
38
+ protected
39
+
40
+ # Executes chat completion request with Gemini-specific error handling.
41
+ #
42
+ # @see OpenAI::ChatProvider#api_prompt_execute
43
+ # @param parameters [Hash]
44
+ # @return [Object, nil] response object or nil for streaming
45
+ # @raise [OpenAI::Errors::APIConnectionError] when Gemini API unreachable
46
+ def api_prompt_execute(parameters)
47
+ super
48
+
49
+ rescue ::OpenAI::Errors::APIConnectionError => exception
50
+ log_connection_error(exception)
51
+ raise exception
52
+ end
53
+
54
+ # Executes embedding request with Gemini-specific error handling.
55
+ #
56
+ # @param parameters [Hash]
57
+ # @return [Hash] symbolized API response
58
+ # @raise [OpenAI::Errors::APIConnectionError] when Gemini API unreachable
59
+ def api_embed_execute(parameters)
60
+ client.embeddings.create(**parameters).as_json.deep_symbolize_keys
61
+ rescue ::OpenAI::Errors::APIConnectionError => exception
62
+ log_connection_error(exception)
63
+ raise exception
64
+ end
65
+
66
+ # Merges streaming delta into the message with role cleanup.
67
+ #
68
+ # Overrides parent to handle Gemini's role copying behavior which duplicates
69
+ # the role field in every streaming chunk, requiring manual cleanup to prevent
70
+ # message corruption.
71
+ #
72
+ # @see OpenAI::ChatProvider#message_merge_delta
73
+ # @param message [Hash]
74
+ # @param delta [Hash]
75
+ # @return [Hash]
76
+ def message_merge_delta(message, delta)
77
+ message[:role] = delta.delete(:role) if delta[:role]
78
+
79
+ hash_merge_delta(message, delta)
80
+ end
81
+
82
+ # Logs connection failures with Gemini API details for debugging.
83
+ #
84
+ # @param error [Exception]
85
+ # @return [void]
86
+ def log_connection_error(error)
87
+ instrument("connection_error.provider.active_agent",
88
+ uri_base: options.base_url,
89
+ exception: error.class,
90
+ message: error.message)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/hash/keys"
4
+ require "uri"
4
5
 
5
6
  module ActiveAgent
6
7
  module Providers
@@ -124,10 +125,19 @@ module ActiveAgent
124
125
  { type: "text", text: msg_hash[:text] },
125
126
  { type: "image_url", image_url: { url: msg_hash[:image] } }
126
127
  ]
128
+ elsif msg_hash.key?(:text) && msg_hash.key?(:document)
129
+ # Shorthand with both text and document: { text: "...", document: "url" }
130
+ [
131
+ { type: "text", text: msg_hash[:text] },
132
+ build_file_content(msg_hash[:document])
133
+ ]
127
134
  elsif msg_hash.key?(:image)
128
135
  # Shorthand with only image: { image: "url" }
129
136
  # Text comes from adjacent prompt arguments
130
137
  [ { type: "image_url", image_url: { url: msg_hash[:image] } } ]
138
+ elsif msg_hash.key?(:document)
139
+ # Shorthand with only document: { document: "url" }
140
+ [ build_file_content(msg_hash[:document]) ]
131
141
  elsif msg_hash.key?(:text)
132
142
  # Shorthand: { text: "..." } or { role: "...", text: "..." }
133
143
  msg_hash[:text]
@@ -137,13 +147,39 @@ module ActiveAgent
137
147
  end
138
148
 
139
149
  # Create appropriate message param based on role and content
140
- extra_params = msg_hash.except(:role, :content, :text, :image)
150
+ extra_params = msg_hash.except(:role, :content, :text, :image, :document)
141
151
  create_message_param(role, content, extra_params)
142
152
  else
143
153
  raise ArgumentError, "Cannot normalize #{message.class} to message"
144
154
  end
145
155
  end
146
156
 
157
+ # Builds a file content block for the Chat API file type
158
+ #
159
+ # Handles both URL and data URI formats:
160
+ # - URL: "http://example.com/document.pdf" → { type: "file", file: { filename: "...", url: "..." } }
161
+ # - Data URI: "data:application/pdf;base64,..." → { type: "file", file: { filename: "...", file_data: "..." } }
162
+ #
163
+ # @param document [String] URL or data URI
164
+ # @return [Hash] file content block
165
+ def build_file_content(document)
166
+ if document.start_with?("data:")
167
+ # Data URI - extract or infer filename from media type
168
+ media_type = document.match(%r{\Adata:([^;,]+)})&.[](1) || "application/octet-stream"
169
+ extension = media_type.split("/").last&.gsub(/[^a-zA-Z0-9]/, "_") || "bin"
170
+ filename = "document.#{extension}"
171
+ { type: "file", file: { filename: filename, file_data: document } }
172
+ else
173
+ # Regular URL
174
+ filename = File.basename(URI.parse(document).path.to_s)
175
+ filename = "document.pdf" if filename.empty?
176
+ { type: "file", file: { filename: filename, url: document } }
177
+ end
178
+ rescue URI::Error
179
+ # Fallback for invalid URLs
180
+ { type: "file", file: { filename: "document.pdf", url: document } }
181
+ end
182
+
147
183
  # Creates the appropriate gem message param class for the given role
148
184
  #
149
185
  # @param role [String] message role (developer, system, user, assistant, tool, function)
@@ -95,6 +95,8 @@ module ActiveAgent
95
95
  def process_stream_chunk(api_response_event)
96
96
  instrument("stream_chunk.active_agent")
97
97
 
98
+ broadcast_stream_open
99
+
98
100
  # Called Multiple Times: [Chunk<T>, T]<Content, ToolsCall>
99
101
  case api_response_event.type
100
102
  when :chunk
@@ -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