activeagent 0.6.0rc2 → 0.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9496edf8536e904969a006a14d65bd69c3257f4db22d80e859a977646a02513
4
- data.tar.gz: 0b35eb065fe146ee027d72789826ea3af793e57140d8aefead79d73586548e6e
3
+ metadata.gz: 56c61e9a6a6173d572792952121413f72d49d9b9bcee932a350666d845236465
4
+ data.tar.gz: 86267a64dea6c8eef7bf890fe10fcb1ef1a2b2d09ddeab3f0a9d1d25bb6465fd
5
5
  SHA512:
6
- metadata.gz: 65ce7639ce9106b99e827600b7582776413cdeb69b7de28cf7ed80e0e496ebc1be6ea613ab9f67e944bb7837344458d3709a2065eb2725fc9b7263c08146f08d
7
- data.tar.gz: c702c8618b701979dee4871ab200e7fd60d5c09fc2670926a9f77e1c0975ab14e2a5e9b30df2eccce9276440f5e6baf396d16320aabff67a1fbbd332a88b5379
6
+ metadata.gz: c173eac89c75335d474c533a41f3137ef2279c5aef1a90d60c93270383dd77c1e15b557c1e006e41c6f15f47f0fce12502c924fd939d58cac85978b97d749d2f
7
+ data.tar.gz: 6bd6d686a0a1a2613206f8b6d50d093a343e7913bd0dbdaac669abb95895a0306f114cc77e2cea01280981b5b5fff403d0edc6de1b1912cfc822ecc2935de961
data/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
  <source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/2bad263a-c09f-40b6-94ba-fff8e346d65d">
3
3
  <img alt="activeagents_banner" src="https://github.com/user-attachments/assets/0ebbaa2f-c6bf-4d40-bb77-931015a14be3">
4
4
  </picture>
5
- *Build AI in Rails*
6
5
 
7
6
 
7
+ > *Build AI in Rails*
8
8
  >
9
9
  > *Now Agents are Controllers*
10
10
  >
@@ -2,6 +2,8 @@ require "active_agent/collector"
2
2
  require "active_support/core_ext/string/inflections"
3
3
  require "active_support/core_ext/hash/except"
4
4
  require "active_support/core_ext/module/anonymous"
5
+ require "active_agent/action_prompt/message"
6
+ require "active_agent/action_prompt/action"
5
7
 
6
8
  # require "active_agent/log_subscriber"
7
9
  require "active_agent/rescuable"
@@ -10,6 +12,7 @@ module ActiveAgent
10
12
  class Base < AbstractController::Base
11
13
  include Callbacks
12
14
  include GenerationProvider
15
+ include Streaming
13
16
  include QueuedGeneration
14
17
  include Rescuable
15
18
  include Parameterized
@@ -217,7 +220,8 @@ module ActiveAgent
217
220
  def handle_response(response)
218
221
  return response unless response.message.requested_actions.present?
219
222
 
220
- # Perform the requested actions
223
+ # The assistant message with tool_calls is already added by update_context in the provider
224
+ # Now perform the requested actions which will add tool response messages
221
225
  perform_actions(requested_actions: response.message.requested_actions)
222
226
 
223
227
  # Continue generation with updated context
@@ -241,9 +245,9 @@ module ActiveAgent
241
245
  end
242
246
 
243
247
  def perform_action(action)
244
- current_context = context.clone
245
- # Merge action params with original params to preserve context
246
- original_params = current_context.params || {}
248
+ # Save the current messages to preserve conversation history
249
+ original_messages = context.messages.dup
250
+ original_params = context.params || {}
247
251
 
248
252
  if action.params.is_a?(Hash)
249
253
  self.params = original_params.merge(action.params)
@@ -251,20 +255,32 @@ module ActiveAgent
251
255
  self.params = original_params
252
256
  end
253
257
 
258
+ # Save the current prompt_was_called state and reset it so the action can render
259
+ original_prompt_was_called = @_prompt_was_called
260
+ @_prompt_was_called = false
261
+
262
+ # Process the action, which will render the view and populate context
254
263
  process(action.name)
255
- context.message.role = :tool
256
- context.message.action_id = action.id
257
- context.message.action_name = action.name
258
- context.message.generation_id = action.id
259
- current_context.message = context.message
260
- current_context.messages << context.message
261
- self.context = current_context
264
+
265
+ # The action should have called prompt which populates context.message
266
+ # Create a tool message from the rendered response
267
+ tool_message = context.message.dup
268
+ tool_message.role = :tool
269
+ tool_message.action_id = action.id
270
+ tool_message.action_name = action.name
271
+ tool_message.generation_id = action.id
272
+
273
+ # Restore the messages with the new tool message
274
+ context.messages = original_messages + [ tool_message ]
275
+
276
+ # Restore the prompt_was_called state
277
+ @_prompt_was_called = original_prompt_was_called
262
278
  end
263
279
 
264
280
  def initialize # :nodoc:
265
281
  super
266
282
  @_prompt_was_called = false
267
- @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {}, agent_instance: self)
283
+ @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options&.deep_dup || {}, agent_instance: self)
268
284
  end
269
285
 
270
286
  def process(method_name, *args) # :nodoc:
@@ -425,7 +441,13 @@ module ActiveAgent
425
441
  # Extract runtime options from prompt_options (exclude instructions as it has special template logic)
426
442
  runtime_options = prompt_options.slice(
427
443
  :model, :temperature, :max_tokens, :stream, :top_p, :frequency_penalty,
428
- :presence_penalty, :response_format, :seed, :stop, :tools_choice, :data_collection, :plugins
444
+ :presence_penalty, :response_format, :seed, :stop, :tools_choice, :plugins,
445
+
446
+ # OpenRouter Provider Settings
447
+ :data_collection, :require_parameters, :only, :ignore, :quantizations, :sort, :max_price,
448
+
449
+ # Built-in Tools Support (OpenAI Responses API)
450
+ :tools
429
451
  )
430
452
  # Handle explicit options parameter
431
453
  explicit_options = prompt_options[:options] || {}
@@ -30,7 +30,12 @@ module ActiveAgent
30
30
  @action_name = attributes.fetch(:action_name, nil)
31
31
  @mcp_servers = attributes.fetch(:mcp_servers, [])
32
32
  set_message if attributes[:message].is_a?(String) || @body.is_a?(String) && @message&.content
33
- set_messages if @instructions.present?
33
+ # Ensure we have a system message with instructions at the start
34
+ if @messages.empty? || @messages[0].role != :system
35
+ @messages.unshift(instructions_message)
36
+ elsif @instructions.present?
37
+ @messages[0] = instructions_message
38
+ end
34
39
  end
35
40
 
36
41
  def multimodal?
@@ -39,17 +44,22 @@ module ActiveAgent
39
44
 
40
45
  def messages=(messages)
41
46
  @messages = messages
42
- set_messages
47
+ # Only add system message if we have instructions and don't already have a system message
48
+ if @instructions.present? && (@messages.empty? || @messages.first&.role != :system)
49
+ set_messages
50
+ end
43
51
  end
44
52
 
45
53
  def instructions=(instructions)
46
- return if instructions.blank?
54
+ # Store the instructions even if blank (will use empty string)
55
+ @instructions = instructions || ""
47
56
 
48
- @instructions = instructions
57
+ # Update or add the system message
49
58
  if @messages[0].present? && @messages[0].role == :system
50
59
  @messages[0] = instructions_message
51
- else
52
- set_messages
60
+ elsif @messages.empty? || @messages[0].role != :system
61
+ # Only add system message if we don't have one at the start
62
+ @messages.unshift(instructions_message)
53
63
  end
54
64
  end
55
65
 
@@ -7,7 +7,7 @@ module ActiveAgent
7
7
  included do
8
8
  include ActiveSupport::Callbacks
9
9
  define_callbacks :generation, skip_after_callbacks_if_terminated: true
10
- define_callbacks :stream, skip_after_callbacks_if_terminated: true
10
+ define_callbacks :embedding, skip_after_callbacks_if_terminated: true
11
11
  end
12
12
 
13
13
  module ClassMethods
@@ -19,21 +19,15 @@ module ActiveAgent
19
19
  set_callback(:generation, callback, name, options)
20
20
  end
21
21
  end
22
- end
23
22
 
24
- # Defines a callback for handling streaming responses during generation
25
- def on_stream(*names, &blk)
26
- _insert_callbacks(names, blk) do |name, options|
27
- set_callback(:stream, :before, name, options)
23
+ # # Defines a callback that will get called right before/after/around the
24
+ # # embedding provider method.
25
+ define_method "#{callback}_embedding" do |*names, &blk|
26
+ _insert_callbacks(names, blk) do |name, options|
27
+ set_callback(:embedding, callback, name, options)
28
+ end
28
29
  end
29
30
  end
30
31
  end
31
-
32
- # Helper method to run stream callbacks
33
- def run_stream_callbacks(message, delta = nil, stop = false)
34
- run_callbacks(:stream) do
35
- yield(message, delta, stop) if block_given?
36
- end
37
- end
38
32
  end
39
33
  end
@@ -50,6 +50,18 @@ module ActiveAgent
50
50
  end
51
51
  end
52
52
 
53
+ def embed_now
54
+ processed_agent.handle_exceptions do
55
+ processed_agent.run_callbacks(:embedding) do
56
+ processed_agent.embed
57
+ end
58
+ end
59
+ end
60
+
61
+ def embed_later(options = {})
62
+ enqueue_generation :embed_now, options
63
+ end
64
+
53
65
  private
54
66
 
55
67
  def processed_agent
@@ -4,7 +4,7 @@ begin
4
4
  gem "ruby-anthropic", "~> 0.4.2"
5
5
  require "anthropic"
6
6
  rescue LoadError
7
- raise LoadError, "The 'ruby-anthropic' gem is required for AnthropicProvider. Please add it to your Gemfile and run `bundle install`."
7
+ raise LoadError, "The 'ruby-anthropic ~> 0.4.2' gem is required for AnthropicProvider. Please add it to your Gemfile and run `bundle install`."
8
8
  end
9
9
 
10
10
  require "active_agent/action_prompt/action"
@@ -36,9 +36,12 @@ module ActiveAgent
36
36
  end
37
37
 
38
38
  def chat_prompt(parameters: prompt_parameters)
39
- parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
39
+ if prompt.options[:stream] || config["stream"]
40
+ parameters[:stream] = provider_stream
41
+ @streaming_request_params = parameters
42
+ end
40
43
 
41
- chat_response(@client.messages(parameters: parameters))
44
+ chat_response(@client.messages(parameters: parameters), parameters)
42
45
  end
43
46
 
44
47
  protected
@@ -120,7 +123,7 @@ module ActiveAgent
120
123
  end
121
124
  end
122
125
 
123
- def chat_response(response)
126
+ def chat_response(response, request_params = nil)
124
127
  return @response if prompt.options[:stream]
125
128
 
126
129
  content = response["content"].first["text"]
@@ -137,7 +140,8 @@ module ActiveAgent
137
140
  @response = ActiveAgent::GenerationProvider::Response.new(
138
141
  prompt: prompt,
139
142
  message: message,
140
- raw_response: response
143
+ raw_response: response,
144
+ raw_request: request_params
141
145
  )
142
146
  end
143
147
 
@@ -60,7 +60,7 @@ module ActiveAgent
60
60
  end
61
61
 
62
62
  def format_error_message(error)
63
- message = if error.respond_to?(:response)
63
+ message = if error.respond_to?(:response) && error.response
64
64
  error.response[:body]
65
65
  elsif error.respond_to?(:message)
66
66
  error.message
@@ -94,12 +94,12 @@ module ActiveAgent
94
94
  def format_single_tool_call(action)
95
95
  # Default tool call format (OpenAI style)
96
96
  {
97
+ id: action.id,
97
98
  type: "function",
98
99
  function: {
99
100
  name: action.name,
100
101
  arguments: action.params.is_a?(String) ? action.params : action.params.to_json
101
- },
102
- id: action.id
102
+ }
103
103
  }
104
104
  end
105
105
  end
@@ -9,7 +9,56 @@ module ActiveAgent
9
9
  @access_token ||= config["api_key"] || config["access_token"] || ENV["OLLAMA_API_KEY"] || ENV["OLLAMA_ACCESS_TOKEN"]
10
10
  @model_name = config["model"]
11
11
  @host = config["host"] || "http://localhost:11434"
12
- @client = OpenAI::Client.new(uri_base: @host, access_token: @access_token, log_errors: true)
12
+ @api_version = config["api_version"] || "v1"
13
+ @client = OpenAI::Client.new(uri_base: @host, access_token: @access_token, log_errors: Rails.env.development?, api_version: @api_version)
14
+ end
15
+
16
+ protected
17
+
18
+ def format_error_message(error)
19
+ # Check for various connection-related errors
20
+ connection_errors = [
21
+ Errno::ECONNREFUSED,
22
+ Errno::EADDRNOTAVAIL,
23
+ Errno::EHOSTUNREACH,
24
+ Net::OpenTimeout,
25
+ Net::ReadTimeout,
26
+ SocketError,
27
+ Faraday::ConnectionFailed
28
+ ]
29
+
30
+ if connection_errors.any? { |klass| error.is_a?(klass) } ||
31
+ (error.message&.include?("Failed to open TCP connection") ||
32
+ error.message&.include?("Connection refused"))
33
+ "Unable to connect to Ollama at #{@host}. Please ensure Ollama is running on the configured host and port.\n" \
34
+ "You can start Ollama with: `ollama serve`\n" \
35
+ "Or update your configuration to point to the correct host."
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ def embeddings_parameters(input: prompt.message.content, model: "nomic-embed-text")
42
+ {
43
+ model: @config["embedding_model"] || model,
44
+ input: input
45
+ }
46
+ end
47
+
48
+ def embeddings_response(response, request_params = nil)
49
+ # Ollama can return either format:
50
+ # 1. OpenAI-compatible: { "data": [{ "embedding": [...] }] }
51
+ # 2. Native Ollama: { "embedding": [...] }
52
+ embedding = response.dig("data", 0, "embedding") || response.dig("embedding")
53
+
54
+ message = ActiveAgent::ActionPrompt::Message.new(content: embedding, role: "assistant")
55
+
56
+ @response = ActiveAgent::GenerationProvider::Response.new(
57
+ prompt: prompt,
58
+ message: message,
59
+ raw_response: response,
60
+ raw_request: request_params
61
+ )
13
62
  end
14
63
  end
15
64
  end
@@ -1,8 +1,8 @@
1
1
  begin
2
- gem "ruby-openai", "~> 8.2.0"
2
+ gem "ruby-openai", ">= 8.1.0"
3
3
  require "openai"
4
4
  rescue LoadError
5
- raise LoadError, "The 'ruby-openai' gem is required for OpenAIProvider. Please add it to your Gemfile and run `bundle install`."
5
+ raise LoadError, "The 'ruby-openai >= 8.1.0' gem is required for OpenAIProvider. Please add it to your Gemfile and run `bundle install`."
6
6
  end
7
7
 
8
8
  require "active_agent/action_prompt/action"
@@ -31,7 +31,7 @@ module ActiveAgent
31
31
  organization_id: @organization_id,
32
32
  admin_token: @admin_token,
33
33
  log_errors: Rails.env.development?
34
- )
34
+ )
35
35
 
36
36
  @model_name = config["model"] || "gpt-4o-mini"
37
37
  end
@@ -69,7 +69,12 @@ module ActiveAgent
69
69
  elsif chunk.dig("choices", 0, "delta", "tool_calls") && chunk.dig("choices", 0, "delta", "role")
70
70
  message = handle_message(chunk.dig("choices", 0, "delta"))
71
71
  prompt.messages << message
72
- @response = ActiveAgent::GenerationProvider::Response.new(prompt:, message:)
72
+ @response = ActiveAgent::GenerationProvider::Response.new(
73
+ prompt:,
74
+ message:,
75
+ raw_response: chunk,
76
+ raw_request: @streaming_request_params
77
+ )
73
78
  end
74
79
 
75
80
  if chunk.dig("choices", 0, "finish_reason")
@@ -87,12 +92,41 @@ module ActiveAgent
87
92
 
88
93
  private
89
94
 
90
- # Now using modules, but we can override build_provider_parameters for OpenAI-specific needs
91
- # The prompt_parameters method comes from ParameterBuilder module
92
- # The format_tools method comes from ToolManagement module
93
- # The provider_messages method comes from MessageFormatting module
95
+ # Override from ParameterBuilder to add web_search_options for Chat API
96
+ def build_provider_parameters
97
+ params = {}
98
+
99
+ # Check if we're using a model that supports web_search_options in Chat API
100
+ if chat_api_web_search_model? && @prompt.options[:web_search]
101
+ params[:web_search_options] = build_web_search_options(@prompt.options[:web_search])
102
+ end
103
+
104
+ params
105
+ end
106
+
107
+ def chat_api_web_search_model?
108
+ model = @prompt.options[:model] || @model_name
109
+ [ "gpt-4o-search-preview", "gpt-4o-mini-search-preview" ].include?(model)
110
+ end
111
+
112
+ def build_web_search_options(web_search_config)
113
+ options = {}
114
+
115
+ if web_search_config.is_a?(Hash)
116
+ options[:search_context_size] = web_search_config[:search_context_size] if web_search_config[:search_context_size]
117
+
118
+ if web_search_config[:user_location]
119
+ options[:user_location] = {
120
+ type: "approximate",
121
+ approximate: web_search_config[:user_location]
122
+ }
123
+ end
124
+ end
125
+
126
+ options
127
+ end
94
128
 
95
- def chat_response(response)
129
+ def chat_response(response, request_params = nil)
96
130
  return @response if prompt.options[:stream]
97
131
  message_json = response.dig("choices", 0, "message")
98
132
  message_json["id"] = response.dig("id") if message_json["id"].blank?
@@ -100,10 +134,15 @@ module ActiveAgent
100
134
 
101
135
  update_context(prompt: prompt, message: message, response: response)
102
136
 
103
- @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message: message, raw_response: response)
137
+ @response = ActiveAgent::GenerationProvider::Response.new(
138
+ prompt: prompt,
139
+ message: message,
140
+ raw_response: response,
141
+ raw_request: request_params
142
+ )
104
143
  end
105
144
 
106
- def responses_response(response)
145
+ def responses_response(response, request_params = nil)
107
146
  message_json = response["output"].find { |output_item| output_item["type"] == "message" }
108
147
  message_json["id"] = response.dig("id") if message_json["id"].blank?
109
148
 
@@ -113,10 +152,15 @@ module ActiveAgent
113
152
  role: message_json["role"].intern,
114
153
  action_requested: message_json["finish_reason"] == "tool_calls",
115
154
  raw_actions: message_json["tool_calls"] || [],
116
- content_type: prompt.output_schema.present? ? "application/json" : "text/plain",
155
+ content_type: prompt.output_schema.present? ? "application/json" : "text/plain"
117
156
  )
118
157
 
119
- @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message: message, raw_response: response)
158
+ @response = ActiveAgent::GenerationProvider::Response.new(
159
+ prompt: prompt,
160
+ message: message,
161
+ raw_response: response,
162
+ raw_request: request_params
163
+ )
120
164
  end
121
165
 
122
166
  def handle_message(message_json)
@@ -133,24 +177,78 @@ module ActiveAgent
133
177
  # handle_actions is now provided by ToolManagement module
134
178
 
135
179
  def chat_prompt(parameters: prompt_parameters)
136
- parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
137
- chat_response(@client.chat(parameters: parameters))
180
+ if prompt.options[:stream] || config["stream"]
181
+ parameters[:stream] = provider_stream
182
+ @streaming_request_params = parameters
183
+ end
184
+ chat_response(@client.chat(parameters: parameters), parameters)
138
185
  end
139
186
 
140
187
  def responses_prompt(parameters: responses_parameters)
141
188
  # parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
142
- responses_response(@client.responses.create(parameters: parameters))
189
+ responses_response(@client.responses.create(parameters: parameters), parameters)
143
190
  end
144
191
 
145
192
  def responses_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @prompt.options[:temperature] || @config["temperature"] || 0.7, tools: @prompt.actions, structured_output: @prompt.output_schema)
193
+ # Build tools array, combining action tools with built-in tools
194
+ tools_array = build_tools_for_responses(tools)
195
+
146
196
  {
147
197
  model: model,
148
198
  input: ActiveAgent::GenerationProvider::ResponsesAdapter.new(@prompt).input,
149
- tools: tools.presence,
199
+ tools: tools_array.presence,
150
200
  text: structured_output
151
201
  }.compact
152
202
  end
153
203
 
204
+ def build_tools_for_responses(action_tools)
205
+ tools = []
206
+
207
+ # Start with action tools (user-defined functions) if any
208
+ tools.concat(action_tools) if action_tools.present?
209
+
210
+ # Add built-in tools if specified in options[:tools]
211
+ if @prompt.options[:tools].present?
212
+ built_in_tools = @prompt.options[:tools]
213
+ built_in_tools = [ built_in_tools ] unless built_in_tools.is_a?(Array)
214
+
215
+ built_in_tools.each do |tool|
216
+ next unless tool.is_a?(Hash)
217
+
218
+ case tool[:type]
219
+ when "web_search_preview", "web_search"
220
+ web_search_tool = { type: "web_search_preview" }
221
+ web_search_tool[:search_context_size] = tool[:search_context_size] if tool[:search_context_size]
222
+ web_search_tool[:user_location] = tool[:user_location] if tool[:user_location]
223
+ tools << web_search_tool
224
+
225
+ when "image_generation"
226
+ image_gen_tool = { type: "image_generation" }
227
+ image_gen_tool[:size] = tool[:size] if tool[:size]
228
+ image_gen_tool[:quality] = tool[:quality] if tool[:quality]
229
+ image_gen_tool[:format] = tool[:format] if tool[:format]
230
+ image_gen_tool[:compression] = tool[:compression] if tool[:compression]
231
+ image_gen_tool[:background] = tool[:background] if tool[:background]
232
+ image_gen_tool[:partial_images] = tool[:partial_images] if tool[:partial_images]
233
+ tools << image_gen_tool
234
+
235
+ when "mcp"
236
+ mcp_tool = { type: "mcp" }
237
+ mcp_tool[:server_label] = tool[:server_label] if tool[:server_label]
238
+ mcp_tool[:server_description] = tool[:server_description] if tool[:server_description]
239
+ mcp_tool[:server_url] = tool[:server_url] if tool[:server_url]
240
+ mcp_tool[:connector_id] = tool[:connector_id] if tool[:connector_id]
241
+ mcp_tool[:authorization] = tool[:authorization] if tool[:authorization]
242
+ mcp_tool[:require_approval] = tool[:require_approval] if tool[:require_approval]
243
+ mcp_tool[:allowed_tools] = tool[:allowed_tools] if tool[:allowed_tools]
244
+ tools << mcp_tool
245
+ end
246
+ end
247
+ end
248
+
249
+ tools
250
+ end
251
+
154
252
  def embeddings_parameters(input: prompt.message.content, model: "text-embedding-3-large")
155
253
  {
156
254
  model: model,
@@ -158,14 +256,20 @@ module ActiveAgent
158
256
  }
159
257
  end
160
258
 
161
- def embeddings_response(response)
259
+ def embeddings_response(response, request_params = nil)
162
260
  message = ActiveAgent::ActionPrompt::Message.new(content: response.dig("data", 0, "embedding"), role: "assistant")
163
261
 
164
- @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message: message, raw_response: response)
262
+ @response = ActiveAgent::GenerationProvider::Response.new(
263
+ prompt: prompt,
264
+ message: message,
265
+ raw_response: response,
266
+ raw_request: request_params
267
+ )
165
268
  end
166
269
 
167
270
  def embeddings_prompt(parameters:)
168
- embeddings_response(@client.embeddings(parameters: embeddings_parameters))
271
+ params = embeddings_parameters
272
+ embeddings_response(@client.embeddings(parameters: params), params)
169
273
  end
170
274
  end
171
275
  end
@@ -23,12 +23,22 @@ module ActiveAgent
23
23
  # Data collection preference (allow, deny, or specific provider list)
24
24
  @data_collection = config["data_collection"] || @provider_preferences["data_collection"] || "allow"
25
25
 
26
+ # Require parameters preference (defaults to false)
27
+ @require_parameters = config["require_parameters"] || @provider_preferences["require_parameters"] || false
28
+
29
+ # Additional OpenRouter provider routing options
30
+ @only_providers = config["only"] || @provider_preferences["only"]
31
+ @ignore_providers = config["ignore"] || @provider_preferences["ignore"]
32
+ @quantizations = config["quantizations"] || @provider_preferences["quantizations"]
33
+ @sort_preference = config["sort"] || @provider_preferences["sort"]
34
+ @max_price = config["max_price"] || @provider_preferences["max_price"]
35
+
26
36
  # Initialize OpenAI client with OpenRouter base URL
27
37
  @client = OpenAI::Client.new(
28
38
  uri_base: "https://openrouter.ai/api/v1",
29
39
  access_token: @access_token,
30
40
  log_errors: Rails.env.development?,
31
- default_headers: openrouter_headers
41
+ extra_headers: openrouter_headers
32
42
  )
33
43
  end
34
44
 
@@ -144,9 +154,15 @@ module ActiveAgent
144
154
  parameters[:transforms] = @transforms if @transforms.present?
145
155
 
146
156
  # Add provider preferences (always include if we have data_collection or other settings)
147
- # Check both configured and runtime data_collection values
157
+ # Check both configured and runtime data_collection/require_parameters values
148
158
  runtime_data_collection = prompt&.options&.key?(:data_collection)
149
- if @provider_preferences.present? || @data_collection != "allow" || runtime_data_collection
159
+ runtime_require_parameters = prompt&.options&.key?(:require_parameters)
160
+ runtime_provider_options = prompt&.options&.keys&.any? { |k| [ :only, :ignore, :quantizations, :sort, :max_price ].include?(k) }
161
+
162
+ if @provider_preferences.present? || @data_collection != "allow" || @require_parameters != false ||
163
+ @only_providers.present? || @ignore_providers.present? || @quantizations.present? ||
164
+ @sort_preference.present? || @max_price.present? ||
165
+ runtime_data_collection || runtime_require_parameters || runtime_provider_options
150
166
  parameters[:provider] = build_provider_preferences
151
167
  end
152
168
 
@@ -160,7 +176,12 @@ module ActiveAgent
160
176
  def build_provider_preferences
161
177
  prefs = {}
162
178
  prefs[:order] = @provider_preferences["order"] if @provider_preferences["order"]
163
- prefs[:require_parameters] = @provider_preferences["require_parameters"] if @provider_preferences.key?("require_parameters")
179
+
180
+ # Require parameters can be overridden at runtime
181
+ require_parameters = prompt.options[:require_parameters] if prompt&.options&.key?(:require_parameters)
182
+ require_parameters = @require_parameters if require_parameters.nil?
183
+ prefs[:require_parameters] = require_parameters if require_parameters != false
184
+
164
185
  prefs[:allow_fallbacks] = @enable_fallbacks
165
186
 
166
187
  # Data collection can be:
@@ -172,6 +193,27 @@ module ActiveAgent
172
193
  data_collection ||= @data_collection
173
194
  prefs[:data_collection] = data_collection
174
195
 
196
+ # Additional OpenRouter provider routing options - check runtime overrides first
197
+ only_providers = prompt.options[:only] if prompt&.options&.key?(:only)
198
+ only_providers ||= @only_providers
199
+ prefs[:only] = only_providers if only_providers.present?
200
+
201
+ ignore_providers = prompt.options[:ignore] if prompt&.options&.key?(:ignore)
202
+ ignore_providers ||= @ignore_providers
203
+ prefs[:ignore] = ignore_providers if ignore_providers.present?
204
+
205
+ quantizations = prompt.options[:quantizations] if prompt&.options&.key?(:quantizations)
206
+ quantizations ||= @quantizations
207
+ prefs[:quantizations] = quantizations if quantizations.present?
208
+
209
+ sort_preference = prompt.options[:sort] if prompt&.options&.key?(:sort)
210
+ sort_preference ||= @sort_preference
211
+ prefs[:sort] = sort_preference if sort_preference.present?
212
+
213
+ max_price = prompt.options[:max_price] if prompt&.options&.key?(:max_price)
214
+ max_price ||= @max_price
215
+ prefs[:max_price] = max_price if max_price.present?
216
+
175
217
  prefs.compact
176
218
  end
177
219
 
@@ -186,9 +228,15 @@ module ActiveAgent
186
228
  params[:transforms] = @transforms if @transforms.present?
187
229
 
188
230
  # Add provider configuration (always include if we have data_collection or other settings)
189
- # Check both configured and runtime data_collection values
231
+ # Check both configured and runtime data_collection/require_parameters values
190
232
  runtime_data_collection = prompt&.options&.key?(:data_collection)
191
- if @provider_preferences.present? || @data_collection != "allow" || runtime_data_collection
233
+ runtime_require_parameters = prompt&.options&.key?(:require_parameters)
234
+ runtime_provider_options = prompt&.options&.keys&.any? { |k| [ :only, :ignore, :quantizations, :sort, :max_price ].include?(k) }
235
+
236
+ if @provider_preferences.present? || @data_collection != "allow" || @require_parameters != false ||
237
+ @only_providers.present? || @ignore_providers.present? || @quantizations.present? ||
238
+ @sort_preference.present? || @max_price.present? ||
239
+ runtime_data_collection || runtime_require_parameters || runtime_provider_options
192
240
  params[:provider] = build_provider_preferences
193
241
  end
194
242
 
@@ -3,13 +3,14 @@
3
3
  module ActiveAgent
4
4
  module GenerationProvider
5
5
  class Response
6
- attr_reader :message, :prompt, :raw_response
6
+ attr_reader :message, :prompt, :raw_response, :raw_request
7
7
  attr_accessor :metadata
8
8
 
9
- def initialize(prompt:, message: nil, raw_response: nil, metadata: nil)
9
+ def initialize(prompt:, message: nil, raw_response: nil, raw_request: nil, metadata: nil)
10
10
  @prompt = prompt
11
11
  @message = message || prompt.message
12
12
  @raw_response = raw_response
13
+ @raw_request = sanitize_request(raw_request)
13
14
  @metadata = metadata || {}
14
15
  end
15
16
 
@@ -17,14 +18,9 @@ module ActiveAgent
17
18
  def usage
18
19
  return nil unless @raw_response
19
20
 
20
- # OpenAI/OpenRouter format
21
+ # Most providers store usage in the same format
21
22
  if @raw_response.is_a?(Hash) && @raw_response["usage"]
22
23
  @raw_response["usage"]
23
- # Anthropic format
24
- elsif @raw_response.is_a?(Hash) && @raw_response["usage"]
25
- @raw_response["usage"]
26
- else
27
- nil
28
24
  end
29
25
  end
30
26
 
@@ -40,6 +36,40 @@ module ActiveAgent
40
36
  def total_tokens
41
37
  usage&.dig("total_tokens")
42
38
  end
39
+
40
+ private
41
+
42
+ def sanitize_request(request)
43
+ return nil if request.nil?
44
+ return request unless request.is_a?(Hash)
45
+
46
+ # Deep clone the request to avoid modifying the original
47
+ sanitized = request.deep_dup
48
+
49
+ # Sanitize any string values in the request
50
+ sanitize_hash_values(sanitized)
51
+ end
52
+
53
+ def sanitize_hash_values(hash)
54
+ hash.each do |key, value|
55
+ case value
56
+ when String
57
+ # Use ActiveAgent's sanitize_credentials to replace sensitive data
58
+ hash[key] = ActiveAgent.sanitize_credentials(value)
59
+ when Hash
60
+ sanitize_hash_values(value)
61
+ when Array
62
+ value.each_with_index do |item, index|
63
+ if item.is_a?(String)
64
+ value[index] = ActiveAgent.sanitize_credentials(item)
65
+ elsif item.is_a?(Hash)
66
+ sanitize_hash_values(item)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ hash
72
+ end
43
73
  end
44
74
  end
45
75
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Streaming
5
+ extend ActiveSupport::Concern
6
+
7
+ class StreamChunk < Data.define(:delta, :stop)
8
+ end
9
+
10
+ attr_accessor :stream_chunk
11
+
12
+ included do
13
+ include ActiveSupport::Callbacks
14
+ define_callbacks :stream, skip_after_callbacks_if_terminated: true
15
+ end
16
+
17
+ module ClassMethods
18
+ # Defines a callback for handling streaming responses during generation
19
+ def on_stream(*names, &blk)
20
+ _insert_callbacks(names, blk) do |name, options|
21
+ set_callback(:stream, :before, name, options)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Helper method to run stream callbacks
27
+ def run_stream_callbacks(message, delta = nil, stop = false)
28
+ @stream_chunk = StreamChunk.new(delta, stop)
29
+ run_callbacks(:stream) do
30
+ yield(message, delta, stop) if block_given?
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveAgent
2
- VERSION = "0.6.0rc2"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/active_agent.rb CHANGED
@@ -23,6 +23,7 @@ module ActiveAgent
23
23
 
24
24
  autoload :Base
25
25
  autoload :Callbacks
26
+ autoload :Streaming
26
27
  autoload :InlinePreviewInterceptor
27
28
  autoload :PromptHelper
28
29
  autoload :Generation
@@ -9,7 +9,6 @@ module Erb # :nodoc:
9
9
  def create_agent_layouts
10
10
  if behavior == :invoke
11
11
  formats.each do |format|
12
- puts format
13
12
  layout_path = File.join("app/views/layouts", filename_with_extensions("agent", format))
14
13
  template filename_with_extensions(:layout, format), layout_path unless File.exist?(layout_path)
15
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0rc2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
@@ -141,16 +141,16 @@ dependencies:
141
141
  name: ruby-openai
142
142
  requirement: !ruby/object:Gem::Requirement
143
143
  requirements:
144
- - - "~>"
144
+ - - ">="
145
145
  - !ruby/object:Gem::Version
146
- version: 8.2.0
146
+ version: 8.1.0
147
147
  type: :development
148
148
  prerelease: false
149
149
  version_requirements: !ruby/object:Gem::Requirement
150
150
  requirements:
151
- - - "~>"
151
+ - - ">="
152
152
  - !ruby/object:Gem::Version
153
- version: 8.2.0
153
+ version: 8.1.0
154
154
  - !ruby/object:Gem::Dependency
155
155
  name: ruby-anthropic
156
156
  requirement: !ruby/object:Gem::Requirement
@@ -249,6 +249,34 @@ dependencies:
249
249
  - - ">="
250
250
  - !ruby/object:Gem::Version
251
251
  version: '0'
252
+ - !ruby/object:Gem::Dependency
253
+ name: cuprite
254
+ requirement: !ruby/object:Gem::Requirement
255
+ requirements:
256
+ - - "~>"
257
+ - !ruby/object:Gem::Version
258
+ version: '0.15'
259
+ type: :development
260
+ prerelease: false
261
+ version_requirements: !ruby/object:Gem::Requirement
262
+ requirements:
263
+ - - "~>"
264
+ - !ruby/object:Gem::Version
265
+ version: '0.15'
266
+ - !ruby/object:Gem::Dependency
267
+ name: capybara
268
+ requirement: !ruby/object:Gem::Requirement
269
+ requirements:
270
+ - - "~>"
271
+ - !ruby/object:Gem::Version
272
+ version: '3.40'
273
+ type: :development
274
+ prerelease: false
275
+ version_requirements: !ruby/object:Gem::Requirement
276
+ requirements:
277
+ - - "~>"
278
+ - !ruby/object:Gem::Version
279
+ version: '3.40'
252
280
  description: The only agent-oriented AI framework designed for Rails, where Agents
253
281
  are Controllers. Build AI features with less complexity using the MVC conventions
254
282
  you love.
@@ -298,6 +326,7 @@ files:
298
326
  - lib/active_agent/rescuable.rb
299
327
  - lib/active_agent/sanitizers.rb
300
328
  - lib/active_agent/service.rb
329
+ - lib/active_agent/streaming.rb
301
330
  - lib/active_agent/test_case.rb
302
331
  - lib/active_agent/version.rb
303
332
  - lib/activeagent.rb