activeagent 0.6.0rc1 → 0.6.0rc4

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: 01b5ef5633bece3cdce9d9dba58e499b48c57d80c3a461ceda58adbff738e36a
4
- data.tar.gz: 9cc9065b10e9bbf0f8fdeba420fe7b9518b7e97f2d32e3bf11af55babd697009
3
+ metadata.gz: 805b4d2cce3173788f3bb281e40973d8782891937b5220654b7dad53a9f701ec
4
+ data.tar.gz: 968f534f94d4e32541f1d87e21854add0f4bf8c9e851ba75f7599291a0b384b9
5
5
  SHA512:
6
- metadata.gz: 6849b7f94803261d8049ed092aaa8176003b85d5353202aa1a96160860bb07dd98fc9171308c5ed4ea3c9644b6aa68cf09c9c5c827ec7a45300eb14b44e9f1e0
7
- data.tar.gz: db67a92dd9f9344fd50234caae1641bb70d57ca61200bbea98a177a175c791de886ee3ac42bc807e0e824d95c94adb6e7765908fec3f50cfc4f73aeb66e21c4f
6
+ metadata.gz: ec13c2dd2d8deb4fd4adb1a6f302de6a7bb5057fad1cd45f27fdecc6b8bea7b5496f3078e7a1f40168de9b4efaa8e4a2abf1cf1bd2865fa929912c58adce561c
7
+ data.tar.gz: a564772159ac558c8350d770977fc9b7b509c814435a748cac61af89bd87dc196614b45cc9f33db7bce8951568f659730bfe4cf95fd274f145ae00bb78510af6
@@ -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
@@ -104,7 +107,6 @@ module ActiveAgent
104
107
  # Define how the agent should generate content
105
108
  def generate_with(provider, **options)
106
109
  self.generation_provider = provider
107
-
108
110
  if options.has_key?(:instructions) || (self.options || {}).empty?
109
111
  # Either instructions explicitly provided, or no inherited options exist
110
112
  self.options = (self.options || {}).merge(options)
@@ -218,7 +220,8 @@ module ActiveAgent
218
220
  def handle_response(response)
219
221
  return response unless response.message.requested_actions.present?
220
222
 
221
- # 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
222
225
  perform_actions(requested_actions: response.message.requested_actions)
223
226
 
224
227
  # Continue generation with updated context
@@ -242,22 +245,36 @@ module ActiveAgent
242
245
  end
243
246
 
244
247
  def perform_action(action)
245
- current_context = context.clone
246
- # Merge action params with original params to preserve context
247
- 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 || {}
251
+
248
252
  if action.params.is_a?(Hash)
249
253
  self.params = original_params.merge(action.params)
250
254
  else
251
255
  self.params = original_params
252
256
  end
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
253
263
  process(action.name)
254
- context.message.role = :tool
255
- context.message.action_id = action.id
256
- context.message.action_name = action.name
257
- context.message.generation_id = action.id
258
- current_context.message = context.message
259
- current_context.messages << context.message
260
- 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
261
278
  end
262
279
 
263
280
  def initialize # :nodoc:
@@ -373,6 +390,10 @@ module ActiveAgent
373
390
  if headers[:message].present? && headers[:message].is_a?(ActiveAgent::ActionPrompt::Message)
374
391
  headers[:body] = headers[:message].content
375
392
  headers[:role] = headers[:message].role
393
+ elsif headers[:message].present? && headers[:message].is_a?(Array)
394
+ # Handle array of multipart content like [{type: "text", text: "..."}, {type: "file", file: {...}}]
395
+ headers[:body] = headers[:message]
396
+ headers[:role] = :user
376
397
  elsif headers[:message].present? && headers[:message].is_a?(String)
377
398
  headers[:body] = headers[:message]
378
399
  headers[:role] = :user
@@ -394,7 +415,6 @@ module ActiveAgent
394
415
  ActiveAgent::ActionPrompt::Message.new(content: headers[:body], content_type: "input_text")
395
416
  ]
396
417
  end
397
-
398
418
  headers
399
419
  end
400
420
 
@@ -421,7 +441,10 @@ module ActiveAgent
421
441
  # Extract runtime options from prompt_options (exclude instructions as it has special template logic)
422
442
  runtime_options = prompt_options.slice(
423
443
  :model, :temperature, :max_tokens, :stream, :top_p, :frequency_penalty,
424
- :presence_penalty, :response_format, :seed, :stop, :tools_choice, :data_collection
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
425
448
  )
426
449
  # Handle explicit options parameter
427
450
  explicit_options = prompt_options[:options] || {}
@@ -27,7 +27,7 @@ module ActiveAgent
27
27
  @metadata = attributes[:metadata] || {}
28
28
  @charset = attributes[:charset] || "UTF-8"
29
29
  @content = attributes[:content] || ""
30
- @content_type = attributes[:content_type] || "text/plain"
30
+ @content_type = detect_content_type(attributes)
31
31
  @role = attributes[:role] || :user
32
32
  @raw_actions = attributes[:raw_actions]
33
33
  @requested_actions = attributes[:requested_actions] || []
@@ -85,6 +85,22 @@ module ActiveAgent
85
85
 
86
86
  private
87
87
 
88
+ def detect_content_type(attributes)
89
+ # If content_type is explicitly provided, use it
90
+ return attributes[:content_type] if attributes[:content_type]
91
+
92
+ # If content is an array with multipart/mixed content, set appropriate type
93
+ if attributes[:content].is_a?(Array)
94
+ # Check if it contains multimodal content (text, image_url, file, etc.)
95
+ has_multimodal = attributes[:content].any? do |item|
96
+ item.is_a?(Hash) && (item[:type] || item["type"])
97
+ end
98
+ has_multimodal ? "multipart/mixed" : "array"
99
+ else
100
+ "text/plain"
101
+ end
102
+ end
103
+
88
104
  def validate_role
89
105
  unless VALID_ROLES.include?(role.to_s)
90
106
  raise ArgumentError, "Invalid role: #{role}. Valid roles are: #{VALID_ROLES.join(", ")}"
@@ -30,26 +30,36 @@ 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?
37
- @multimodal ||= @message&.content.is_a?(Array) || @messages.any? { |m| m.content.is_a?(Array) }
42
+ @multimodal ||= @message&.content.is_a?(Array) || @messages.any? { |m| m&.content.is_a?(Array) }
38
43
  end
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,6 @@ 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
11
10
  end
12
11
 
13
12
  module ClassMethods
@@ -20,20 +19,6 @@ module ActiveAgent
20
19
  end
21
20
  end
22
21
  end
23
-
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)
28
- end
29
- end
30
- 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
22
  end
38
23
  end
39
24
  end
@@ -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
 
@@ -10,6 +10,7 @@ module ActiveAgent
10
10
  include ParameterBuilder
11
11
 
12
12
  class GenerationProviderError < StandardError; end
13
+
13
14
  attr_reader :client, :config, :prompt, :response, :access_token, :model_name
14
15
 
15
16
  def initialize(config)
@@ -46,7 +46,6 @@ module ActiveAgent
46
46
 
47
47
  def handle_generation_error(error)
48
48
  error_message = format_error_message(error)
49
-
50
49
  # Create new error with original backtrace preserved
51
50
  new_error = ActiveAgent::GenerationProvider::Base::GenerationProviderError.new(error_message)
52
51
  new_error.set_backtrace(error.backtrace) if error.respond_to?(:backtrace)
@@ -61,7 +60,9 @@ module ActiveAgent
61
60
  end
62
61
 
63
62
  def format_error_message(error)
64
- message = if error.respond_to?(:message)
63
+ message = if error.respond_to?(:response)
64
+ error.response[:body]
65
+ elsif error.respond_to?(:message)
65
66
  error.message
66
67
  elsif error.respond_to?(:to_s)
67
68
  error.to_s
@@ -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
@@ -1,5 +1,5 @@
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
5
  raise LoadError, "The 'ruby-openai' gem is required for OpenAIProvider. Please add it to your Gemfile and run `bundle install`."
@@ -25,7 +25,13 @@ module ActiveAgent
25
25
  @access_token ||= config["api_key"] || config["access_token"] || OpenAI.configuration.access_token || ENV["OPENAI_ACCESS_TOKEN"]
26
26
  @organization_id = config["organization_id"] || OpenAI.configuration.organization_id || ENV["OPENAI_ORGANIZATION_ID"]
27
27
  @admin_token = config["admin_token"] || OpenAI.configuration.admin_token || ENV["OPENAI_ADMIN_TOKEN"]
28
- @client = OpenAI::Client.new(access_token: @access_token, uri_base: @host, organization_id: @organization_id)
28
+ @client = OpenAI::Client.new(
29
+ access_token: @access_token,
30
+ uri_base: @host,
31
+ organization_id: @organization_id,
32
+ admin_token: @admin_token,
33
+ log_errors: Rails.env.development?
34
+ )
29
35
 
30
36
  @model_name = config["model"] || "gpt-4o-mini"
31
37
  end
@@ -63,7 +69,12 @@ module ActiveAgent
63
69
  elsif chunk.dig("choices", 0, "delta", "tool_calls") && chunk.dig("choices", 0, "delta", "role")
64
70
  message = handle_message(chunk.dig("choices", 0, "delta"))
65
71
  prompt.messages << message
66
- @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
+ )
67
78
  end
68
79
 
69
80
  if chunk.dig("choices", 0, "finish_reason")
@@ -86,7 +97,7 @@ module ActiveAgent
86
97
  # The format_tools method comes from ToolManagement module
87
98
  # The provider_messages method comes from MessageFormatting module
88
99
 
89
- def chat_response(response)
100
+ def chat_response(response, request_params = nil)
90
101
  return @response if prompt.options[:stream]
91
102
  message_json = response.dig("choices", 0, "message")
92
103
  message_json["id"] = response.dig("id") if message_json["id"].blank?
@@ -94,10 +105,15 @@ module ActiveAgent
94
105
 
95
106
  update_context(prompt: prompt, message: message, response: response)
96
107
 
97
- @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message: message, raw_response: response)
108
+ @response = ActiveAgent::GenerationProvider::Response.new(
109
+ prompt: prompt,
110
+ message: message,
111
+ raw_response: response,
112
+ raw_request: request_params
113
+ )
98
114
  end
99
115
 
100
- def responses_response(response)
116
+ def responses_response(response, request_params = nil)
101
117
  message_json = response["output"].find { |output_item| output_item["type"] == "message" }
102
118
  message_json["id"] = response.dig("id") if message_json["id"].blank?
103
119
 
@@ -110,7 +126,12 @@ module ActiveAgent
110
126
  content_type: prompt.output_schema.present? ? "application/json" : "text/plain",
111
127
  )
112
128
 
113
- @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message: message, raw_response: response)
129
+ @response = ActiveAgent::GenerationProvider::Response.new(
130
+ prompt: prompt,
131
+ message: message,
132
+ raw_response: response,
133
+ raw_request: request_params
134
+ )
114
135
  end
115
136
 
116
137
  def handle_message(message_json)
@@ -127,13 +148,16 @@ module ActiveAgent
127
148
  # handle_actions is now provided by ToolManagement module
128
149
 
129
150
  def chat_prompt(parameters: prompt_parameters)
130
- parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
131
- chat_response(@client.chat(parameters: parameters))
151
+ if prompt.options[:stream] || config["stream"]
152
+ parameters[:stream] = provider_stream
153
+ @streaming_request_params = parameters
154
+ end
155
+ chat_response(@client.chat(parameters: parameters), parameters)
132
156
  end
133
157
 
134
158
  def responses_prompt(parameters: responses_parameters)
135
159
  # parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
136
- responses_response(@client.responses.create(parameters: parameters))
160
+ responses_response(@client.responses.create(parameters: parameters), parameters)
137
161
  end
138
162
 
139
163
  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)
@@ -152,14 +176,20 @@ module ActiveAgent
152
176
  }
153
177
  end
154
178
 
155
- def embeddings_response(response)
179
+ def embeddings_response(response, request_params = nil)
156
180
  message = ActiveAgent::ActionPrompt::Message.new(content: response.dig("data", 0, "embedding"), role: "assistant")
157
181
 
158
- @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message: message, raw_response: response)
182
+ @response = ActiveAgent::GenerationProvider::Response.new(
183
+ prompt: prompt,
184
+ message: message,
185
+ raw_response: response,
186
+ raw_request: request_params
187
+ )
159
188
  end
160
189
 
161
190
  def embeddings_prompt(parameters:)
162
- embeddings_response(@client.embeddings(parameters: embeddings_parameters))
191
+ params = embeddings_parameters
192
+ embeddings_response(@client.embeddings(parameters: params), params)
163
193
  end
164
194
  end
165
195
  end
@@ -4,31 +4,6 @@ require_relative "open_ai_provider"
4
4
  module ActiveAgent
5
5
  module GenerationProvider
6
6
  class OpenRouterProvider < OpenAIProvider
7
- # Vision-capable models on OpenRouter
8
- VISION_MODELS = [
9
- "openai/gpt-4-vision-preview",
10
- "openai/gpt-4o",
11
- "openai/gpt-4o-mini",
12
- "anthropic/claude-3-5-sonnet",
13
- "anthropic/claude-3-opus",
14
- "anthropic/claude-3-sonnet",
15
- "anthropic/claude-3-haiku",
16
- "google/gemini-pro-1.5",
17
- "google/gemini-pro-vision"
18
- ].freeze
19
-
20
- # Models that support structured output
21
- STRUCTURED_OUTPUT_MODELS = [
22
- "openai/gpt-4o",
23
- "openai/gpt-4o-2024-08-06",
24
- "openai/gpt-4o-mini",
25
- "openai/gpt-4o-mini-2024-07-18",
26
- "openai/gpt-4-turbo",
27
- "openai/gpt-4-turbo-2024-04-09",
28
- "openai/gpt-3.5-turbo-0125",
29
- "openai/gpt-3.5-turbo-1106"
30
- ].freeze
31
-
32
7
  def initialize(config)
33
8
  @config = config
34
9
  @access_token = config["api_key"] || config["access_token"] ||
@@ -48,12 +23,22 @@ module ActiveAgent
48
23
  # Data collection preference (allow, deny, or specific provider list)
49
24
  @data_collection = config["data_collection"] || @provider_preferences["data_collection"] || "allow"
50
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
+
51
36
  # Initialize OpenAI client with OpenRouter base URL
52
37
  @client = OpenAI::Client.new(
53
38
  uri_base: "https://openrouter.ai/api/v1",
54
39
  access_token: @access_token,
55
- log_errors: true,
56
- default_headers: openrouter_headers
40
+ log_errors: Rails.env.development?,
41
+ extra_headers: openrouter_headers
57
42
  )
58
43
  end
59
44
 
@@ -69,15 +54,6 @@ module ActiveAgent
69
54
  handle_openrouter_error(e)
70
55
  end
71
56
 
72
- # Helper methods for checking model capabilities
73
- def supports_vision?(model = @model_name)
74
- VISION_MODELS.include?(model)
75
- end
76
-
77
- def supports_structured_output?(model = @model_name)
78
- STRUCTURED_OUTPUT_MODELS.include?(model)
79
- end
80
-
81
57
  protected
82
58
 
83
59
  def build_provider_parameters
@@ -88,6 +64,32 @@ module ActiveAgent
88
64
  add_openrouter_params(params)
89
65
  end
90
66
 
67
+ def format_content_item(item)
68
+ # Handle OpenRouter-specific content formats
69
+ if item.is_a?(Hash)
70
+ case item[:type] || item["type"]
71
+ when "file"
72
+ # Convert file type to image_url for OpenRouter PDF support
73
+ file_data = item.dig(:file, :file_data) || item.dig("file", "file_data")
74
+ if file_data
75
+ {
76
+ type: "image_url",
77
+ image_url: {
78
+ url: file_data
79
+ }
80
+ }
81
+ else
82
+ item
83
+ end
84
+ else
85
+ # Use default formatting for other types
86
+ super
87
+ end
88
+ else
89
+ super
90
+ end
91
+ end
92
+
91
93
  private
92
94
 
93
95
  def default_app_name
@@ -152,19 +154,34 @@ module ActiveAgent
152
154
  parameters[:transforms] = @transforms if @transforms.present?
153
155
 
154
156
  # Add provider preferences (always include if we have data_collection or other settings)
155
- # Check both configured and runtime data_collection values
157
+ # Check both configured and runtime data_collection/require_parameters values
156
158
  runtime_data_collection = prompt&.options&.key?(:data_collection)
157
- 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
158
166
  parameters[:provider] = build_provider_preferences
159
167
  end
160
168
 
169
+ # Add plugins (e.g., for PDF processing)
170
+
171
+ parameters[:plugins] = prompt.options[:plugins] if prompt.options[:plugins].present?
172
+ parameters[:models] = prompt.options[:fallback_models] if prompt.options[:enable_fallbacks] && prompt.options[:fallback_models].present?
161
173
  parameters
162
174
  end
163
175
 
164
176
  def build_provider_preferences
165
177
  prefs = {}
166
178
  prefs[:order] = @provider_preferences["order"] if @provider_preferences["order"]
167
- 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
+
168
185
  prefs[:allow_fallbacks] = @enable_fallbacks
169
186
 
170
187
  # Data collection can be:
@@ -176,6 +193,27 @@ module ActiveAgent
176
193
  data_collection ||= @data_collection
177
194
  prefs[:data_collection] = data_collection
178
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
+
179
217
  prefs.compact
180
218
  end
181
219
 
@@ -190,9 +228,15 @@ module ActiveAgent
190
228
  params[:transforms] = @transforms if @transforms.present?
191
229
 
192
230
  # Add provider configuration (always include if we have data_collection or other settings)
193
- # Check both configured and runtime data_collection values
231
+ # Check both configured and runtime data_collection/require_parameters values
194
232
  runtime_data_collection = prompt&.options&.key?(:data_collection)
195
- 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
196
240
  params[:provider] = build_provider_preferences
197
241
  end
198
242
 
@@ -208,7 +252,6 @@ module ActiveAgent
208
252
  parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
209
253
 
210
254
  response = @client.chat(parameters: parameters)
211
-
212
255
  # Log if fallback was used
213
256
  if response.respond_to?(:headers) && response.headers["x-model"] != @model_name
214
257
  Rails.logger.info "[OpenRouter] Fallback model used: #{response.headers['x-model']}" if defined?(Rails)
@@ -229,7 +272,6 @@ module ActiveAgent
229
272
  message = handle_message(message_json) if message_json
230
273
 
231
274
  update_context(prompt: prompt, message: message, response: response) if message
232
-
233
275
  # Create response with OpenRouter metadata
234
276
  @response = ActiveAgent::GenerationProvider::Response.new(
235
277
  prompt: prompt,
@@ -315,7 +357,7 @@ module ActiveAgent
315
357
  handle_timeout_error(error)
316
358
  else
317
359
  # Fall back to parent error handling
318
- super(error) if defined?(super)
360
+ raise GenerationProviderError, error, error.backtrace
319
361
  end
320
362
  end
321
363
 
@@ -46,7 +46,7 @@ module ActiveAgent
46
46
  options = {}
47
47
 
48
48
  # Common options that map directly
49
- [ :stream, :top_p, :frequency_penalty, :presence_penalty, :seed, :stop, :user ].each do |key|
49
+ [ :stream, :top_p, :frequency_penalty, :presence_penalty, :seed, :stop, :user, :plugins ].each do |key|
50
50
  options[key] = @prompt.options[key] if @prompt.options.key?(key)
51
51
  end
52
52
 
@@ -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
@@ -68,7 +68,13 @@ module ActiveAgent
68
68
 
69
69
  initializer "active_agent.compile_config_methods" do
70
70
  ActiveSupport.on_load(:active_agent) do
71
- config.compile_methods! if config.respond_to?(:compile_methods!)
71
+ config.compile_methods! if config.class.respond_to?(:compile_methods!)
72
+ end
73
+ end
74
+
75
+ initializer "active_agent.inflections" do
76
+ ActiveSupport::Inflector.inflections do |inflect|
77
+ inflect.acronym "AI"
72
78
  end
73
79
  end
74
80
 
@@ -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.0rc1"
2
+ VERSION = "0.6.0rc4"
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.0rc1
4
+ version: 0.6.0rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
@@ -18,7 +18,7 @@ dependencies:
18
18
  version: '7.2'
19
19
  - - "<="
20
20
  - !ruby/object:Gem::Version
21
- version: 8.0.2.1
21
+ version: '9.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,7 +28,7 @@ dependencies:
28
28
  version: '7.2'
29
29
  - - "<="
30
30
  - !ruby/object:Gem::Version
31
- version: 8.0.2.1
31
+ version: '9.0'
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: actionview
34
34
  requirement: !ruby/object:Gem::Requirement
@@ -38,7 +38,7 @@ dependencies:
38
38
  version: '7.2'
39
39
  - - "<="
40
40
  - !ruby/object:Gem::Version
41
- version: 8.0.2.1
41
+ version: '9.0'
42
42
  type: :runtime
43
43
  prerelease: false
44
44
  version_requirements: !ruby/object:Gem::Requirement
@@ -48,7 +48,7 @@ dependencies:
48
48
  version: '7.2'
49
49
  - - "<="
50
50
  - !ruby/object:Gem::Version
51
- version: 8.0.2.1
51
+ version: '9.0'
52
52
  - !ruby/object:Gem::Dependency
53
53
  name: activesupport
54
54
  requirement: !ruby/object:Gem::Requirement
@@ -58,7 +58,7 @@ dependencies:
58
58
  version: '7.2'
59
59
  - - "<="
60
60
  - !ruby/object:Gem::Version
61
- version: 8.0.2.1
61
+ version: '9.0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
@@ -68,7 +68,7 @@ dependencies:
68
68
  version: '7.2'
69
69
  - - "<="
70
70
  - !ruby/object:Gem::Version
71
- version: 8.0.2.1
71
+ version: '9.0'
72
72
  - !ruby/object:Gem::Dependency
73
73
  name: activemodel
74
74
  requirement: !ruby/object:Gem::Requirement
@@ -78,7 +78,7 @@ dependencies:
78
78
  version: '7.2'
79
79
  - - "<="
80
80
  - !ruby/object:Gem::Version
81
- version: 8.0.2.1
81
+ version: '9.0'
82
82
  type: :runtime
83
83
  prerelease: false
84
84
  version_requirements: !ruby/object:Gem::Requirement
@@ -88,7 +88,7 @@ dependencies:
88
88
  version: '7.2'
89
89
  - - "<="
90
90
  - !ruby/object:Gem::Version
91
- version: 8.0.2.1
91
+ version: '9.0'
92
92
  - !ruby/object:Gem::Dependency
93
93
  name: activejob
94
94
  requirement: !ruby/object:Gem::Requirement
@@ -98,7 +98,7 @@ dependencies:
98
98
  version: '7.2'
99
99
  - - "<="
100
100
  - !ruby/object:Gem::Version
101
- version: 8.0.2.1
101
+ version: '9.0'
102
102
  type: :runtime
103
103
  prerelease: false
104
104
  version_requirements: !ruby/object:Gem::Requirement
@@ -108,35 +108,147 @@ dependencies:
108
108
  version: '7.2'
109
109
  - - "<="
110
110
  - !ruby/object:Gem::Version
111
- version: 8.0.2.1
111
+ version: '9.0'
112
112
  - !ruby/object:Gem::Dependency
113
113
  name: jbuilder
114
114
  requirement: !ruby/object:Gem::Requirement
115
115
  requirements:
116
116
  - - "~>"
117
117
  - !ruby/object:Gem::Version
118
- version: 2.14.1
118
+ version: '2.14'
119
119
  type: :development
120
120
  prerelease: false
121
121
  version_requirements: !ruby/object:Gem::Requirement
122
122
  requirements:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
- version: 2.14.1
125
+ version: '2.14'
126
126
  - !ruby/object:Gem::Dependency
127
127
  name: rails
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: ruby-openai
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: 8.1.0
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: 8.1.0
154
+ - !ruby/object:Gem::Dependency
155
+ name: ruby-anthropic
128
156
  requirement: !ruby/object:Gem::Requirement
129
157
  requirements:
130
158
  - - "~>"
131
159
  - !ruby/object:Gem::Version
132
- version: 8.0.2.1
160
+ version: 0.4.2
133
161
  type: :development
134
162
  prerelease: false
135
163
  version_requirements: !ruby/object:Gem::Requirement
136
164
  requirements:
137
165
  - - "~>"
138
166
  - !ruby/object:Gem::Version
139
- version: 8.0.2.1
167
+ version: 0.4.2
168
+ - !ruby/object:Gem::Dependency
169
+ name: standard
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ - !ruby/object:Gem::Dependency
183
+ name: rubocop-rails-omakase
184
+ requirement: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ type: :development
190
+ prerelease: false
191
+ version_requirements: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ - !ruby/object:Gem::Dependency
197
+ name: puma
198
+ requirement: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ type: :development
204
+ prerelease: false
205
+ version_requirements: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: '0'
210
+ - !ruby/object:Gem::Dependency
211
+ name: sqlite3
212
+ requirement: !ruby/object:Gem::Requirement
213
+ requirements:
214
+ - - ">="
215
+ - !ruby/object:Gem::Version
216
+ version: '0'
217
+ type: :development
218
+ prerelease: false
219
+ version_requirements: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ - !ruby/object:Gem::Dependency
225
+ name: vcr
226
+ requirement: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ type: :development
232
+ prerelease: false
233
+ version_requirements: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - ">="
236
+ - !ruby/object:Gem::Version
237
+ version: '0'
238
+ - !ruby/object:Gem::Dependency
239
+ name: webmock
240
+ requirement: !ruby/object:Gem::Requirement
241
+ requirements:
242
+ - - ">="
243
+ - !ruby/object:Gem::Version
244
+ version: '0'
245
+ type: :development
246
+ prerelease: false
247
+ version_requirements: !ruby/object:Gem::Requirement
248
+ requirements:
249
+ - - ">="
250
+ - !ruby/object:Gem::Version
251
+ version: '0'
140
252
  description: The only agent-oriented AI framework designed for Rails, where Agents
141
253
  are Controllers. Build AI features with less complexity using the MVC conventions
142
254
  you love.
@@ -186,6 +298,7 @@ files:
186
298
  - lib/active_agent/rescuable.rb
187
299
  - lib/active_agent/sanitizers.rb
188
300
  - lib/active_agent/service.rb
301
+ - lib/active_agent/streaming.rb
189
302
  - lib/active_agent/test_case.rb
190
303
  - lib/active_agent/version.rb
191
304
  - lib/activeagent.rb