activeagent 0.5.0 → 0.6.0rc2

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: a16cb39db946881080e40bdf640106d6e3251cc7a80b8e77a036b7f815b9e9e3
4
- data.tar.gz: e41d8b9560fc8dabe3caf5c79c67a094fe5d2196a810e30a5ed58c9428b9e158
3
+ metadata.gz: a9496edf8536e904969a006a14d65bd69c3257f4db22d80e859a977646a02513
4
+ data.tar.gz: 0b35eb065fe146ee027d72789826ea3af793e57140d8aefead79d73586548e6e
5
5
  SHA512:
6
- metadata.gz: dc64e5b15aff61f1504a995c9cd79a9e62203b219c833fac56164ee2e127e6688363a3262b20786edf6ef70a34eff11f3fa9c89986e9428452a8a13ed2cbad28
7
- data.tar.gz: 297263095bf29d165bc298edccf20bccd606bf5c8f64107b9bceca91f82eb53b2c31b3707b844830e8aea004402210f903707d106443b4e49b4222eb728a6ffe
6
+ metadata.gz: 65ce7639ce9106b99e827600b7582776413cdeb69b7de28cf7ed80e0e496ebc1be6ea613ab9f67e944bb7837344458d3709a2065eb2725fc9b7263c08146f08d
7
+ data.tar.gz: c702c8618b701979dee4871ab200e7fd60d5c09fc2670926a9f77e1c0975ab14e2a5e9b30df2eccce9276440f5e6baf396d16320aabff67a1fbbd332a88b5379
@@ -104,7 +104,6 @@ module ActiveAgent
104
104
  # Define how the agent should generate content
105
105
  def generate_with(provider, **options)
106
106
  self.generation_provider = provider
107
-
108
107
  if options.has_key?(:instructions) || (self.options || {}).empty?
109
108
  # Either instructions explicitly provided, or no inherited options exist
110
109
  self.options = (self.options || {}).merge(options)
@@ -199,9 +198,9 @@ module ActiveAgent
199
198
  # Add embedding capability to Message class
200
199
  ActiveAgent::ActionPrompt::Message.class_eval do
201
200
  def embed
202
- agent_class = ActiveAgent::Base.descendants.first
201
+ agent_class = ApplicationAgent
203
202
  agent = agent_class.new
204
- agent.context = ActiveAgent::ActionPrompt::Prompt.new(message: self)
203
+ agent.context = ActiveAgent::ActionPrompt::Prompt.new(message: self, agent_instance: agent)
205
204
  agent.embed
206
205
  self
207
206
  end
@@ -217,8 +216,18 @@ module ActiveAgent
217
216
 
218
217
  def handle_response(response)
219
218
  return response unless response.message.requested_actions.present?
219
+
220
+ # Perform the requested actions
220
221
  perform_actions(requested_actions: response.message.requested_actions)
221
- update_context(response)
222
+
223
+ # Continue generation with updated context
224
+ continue_generation
225
+ end
226
+
227
+ def continue_generation
228
+ # Continue generating with the updated context that includes tool results
229
+ generation_provider.generate(context) if context && generation_provider
230
+ handle_response(generation_provider.response)
222
231
  end
223
232
 
224
233
  def update_context(response)
@@ -233,10 +242,15 @@ module ActiveAgent
233
242
 
234
243
  def perform_action(action)
235
244
  current_context = context.clone
236
- # Set params from the action for controller access
245
+ # Merge action params with original params to preserve context
246
+ original_params = current_context.params || {}
247
+
237
248
  if action.params.is_a?(Hash)
238
- self.params = action.params
249
+ self.params = original_params.merge(action.params)
250
+ else
251
+ self.params = original_params
239
252
  end
253
+
240
254
  process(action.name)
241
255
  context.message.role = :tool
242
256
  context.message.action_id = action.id
@@ -250,7 +264,7 @@ module ActiveAgent
250
264
  def initialize # :nodoc:
251
265
  super
252
266
  @_prompt_was_called = false
253
- @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {})
267
+ @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {}, agent_instance: self)
254
268
  end
255
269
 
256
270
  def process(method_name, *args) # :nodoc:
@@ -262,7 +276,7 @@ module ActiveAgent
262
276
 
263
277
  ActiveSupport::Notifications.instrument("process.active_agent", payload) do
264
278
  super
265
- @_context = ActiveAgent::ActionPrompt::Prompt.new unless @_prompt_was_called
279
+ @_context = ActiveAgent::ActionPrompt::Prompt.new(agent_instance: self) unless @_prompt_was_called
266
280
  end
267
281
  end
268
282
  ruby2_keywords(:process)
@@ -317,11 +331,12 @@ module ActiveAgent
317
331
 
318
332
  headers = apply_defaults(headers)
319
333
  context.messages = headers[:messages] || []
334
+ context.mcp_servers = headers[:mcp_servers] || []
320
335
  context.context_id = headers[:context_id]
321
336
  context.params = params
322
337
  context.action_name = action_name
323
338
 
324
- context.output_schema = load_schema(headers[:output_schema], set_prefixes(headers[:output_schema], lookup_context.prefixes))
339
+ context.output_schema = render_schema(headers[:output_schema], set_prefixes(headers[:output_schema], lookup_context.prefixes))
325
340
 
326
341
  context.charset = charset = headers[:charset]
327
342
 
@@ -350,7 +365,7 @@ module ActiveAgent
350
365
  prefixes = set_prefixes(action_name, lookup_context.prefixes)
351
366
 
352
367
  action_methods.map do |action|
353
- load_schema(action, prefixes)
368
+ render_schema(action, prefixes)
354
369
  end.compact
355
370
  end
356
371
 
@@ -359,6 +374,10 @@ module ActiveAgent
359
374
  if headers[:message].present? && headers[:message].is_a?(ActiveAgent::ActionPrompt::Message)
360
375
  headers[:body] = headers[:message].content
361
376
  headers[:role] = headers[:message].role
377
+ elsif headers[:message].present? && headers[:message].is_a?(Array)
378
+ # Handle array of multipart content like [{type: "text", text: "..."}, {type: "file", file: {...}}]
379
+ headers[:body] = headers[:message]
380
+ headers[:role] = :user
362
381
  elsif headers[:message].present? && headers[:message].is_a?(String)
363
382
  headers[:body] = headers[:message]
364
383
  headers[:role] = :user
@@ -380,7 +399,6 @@ module ActiveAgent
380
399
  ActiveAgent::ActionPrompt::Message.new(content: headers[:body], content_type: "input_text")
381
400
  ]
382
401
  end
383
-
384
402
  headers
385
403
  end
386
404
 
@@ -388,10 +406,14 @@ module ActiveAgent
388
406
  prefixes = lookup_context.prefixes | [ self.class.agent_name ]
389
407
  end
390
408
 
391
- def load_schema(action_name, prefixes)
392
- return unless lookup_context.template_exists?(action_name, prefixes, false, formats: [ :json ])
409
+ def render_schema(schema_or_action, prefixes)
410
+ # If it's already a hash (direct schema), return it
411
+ return schema_or_action if schema_or_action.is_a?(Hash)
412
+
413
+ # Otherwise try to load from template
414
+ return unless lookup_context.template_exists?(schema_or_action, prefixes, false, formats: [ :json ])
393
415
 
394
- JSON.parse render_to_string(locals: { action_name: action_name }, action: action_name, formats: :json)
416
+ JSON.parse render_to_string(locals: { action_name: schema_or_action }, action: schema_or_action, formats: :json)
395
417
  end
396
418
 
397
419
  def merge_options(prompt_options)
@@ -403,7 +425,7 @@ module ActiveAgent
403
425
  # Extract runtime options from prompt_options (exclude instructions as it has special template logic)
404
426
  runtime_options = prompt_options.slice(
405
427
  :model, :temperature, :max_tokens, :stream, :top_p, :frequency_penalty,
406
- :presence_penalty, :response_format, :seed, :stop, :tools_choice
428
+ :presence_penalty, :response_format, :seed, :stop, :tools_choice, :data_collection, :plugins
407
429
  )
408
430
  # Handle explicit options parameter
409
431
  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(", ")}"
@@ -4,12 +4,13 @@ module ActiveAgent
4
4
  module ActionPrompt
5
5
  class Prompt
6
6
  attr_reader :messages, :instructions
7
- attr_accessor :actions, :body, :content_type, :context_id, :message, :options, :mime_version, :charset, :context, :parts, :params, :action_choice, :agent_class, :output_schema, :action_name
7
+ attr_accessor :actions, :body, :content_type, :context_id, :message, :options, :mime_version, :charset, :context, :parts, :params, :action_choice, :agent_class, :output_schema, :action_name, :agent_instance, :mcp_servers
8
8
 
9
9
  def initialize(attributes = {})
10
10
  @options = attributes.fetch(:options, {})
11
11
  @multimodal = attributes.fetch(:multimodal, false)
12
12
  @agent_class = attributes.fetch(:agent_class, ApplicationAgent)
13
+ @agent_instance = attributes.fetch(:agent_instance, nil)
13
14
  @actions = attributes.fetch(:actions, [])
14
15
  @action_choice = attributes.fetch(:action_choice, "")
15
16
  @instructions = attributes.fetch(:instructions, "")
@@ -27,12 +28,13 @@ module ActiveAgent
27
28
  @output_schema = attributes.fetch(:output_schema, nil)
28
29
  @messages = Message.from_messages(@messages)
29
30
  @action_name = attributes.fetch(:action_name, nil)
31
+ @mcp_servers = attributes.fetch(:mcp_servers, [])
30
32
  set_message if attributes[:message].is_a?(String) || @body.is_a?(String) && @message&.content
31
33
  set_messages if @instructions.present?
32
34
  end
33
35
 
34
36
  def multimodal?
35
- @multimodal ||= @message&.content.is_a?(Array) || @messages.any? { |m| m.content.is_a?(Array) }
37
+ @multimodal ||= @message&.content.is_a?(Array) || @messages.any? { |m| m&.content.is_a?(Array) }
36
38
  end
37
39
 
38
40
  def messages=(messages)
@@ -81,7 +83,7 @@ module ActiveAgent
81
83
 
82
84
  def inspect
83
85
  "#<#{self.class}:0x#{object_id.to_s(16)}\n" +
84
- " @options=#{@options.inspect.gsub(Rails.application.credentials.dig(:openai, :api_key), '<OPENAI_API_KEY>')}\n" +
86
+ " @options=#{ActiveAgent.sanitize_credentials(@options.inspect)}\n" +
85
87
  " @actions=#{@actions.inspect}\n" +
86
88
  " @action_choice=#{@action_choice.inspect}\n" +
87
89
  " @instructions=#{@instructions.inspect}\n" +
@@ -37,7 +37,8 @@ module ActiveAgent
37
37
  messages: params[:messages],
38
38
  message: params[:message],
39
39
  context_id: params[:context_id],
40
- options: params[:options]
40
+ options: params[:options],
41
+ mcp_servers: params[:mcp_servers]
41
42
  }.merge(additional_options)
42
43
  )
43
44
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ class Configuration
5
+ attr_accessor :verbose_generation_errors
6
+ attr_accessor :generation_retry_errors
7
+ attr_accessor :generation_max_retries
8
+ attr_accessor :generation_provider_logger
9
+
10
+ def initialize
11
+ @verbose_generation_errors = false
12
+ @generation_retry_errors = []
13
+ @generation_max_retries = 3
14
+ @generation_provider_logger = nil
15
+ end
16
+
17
+ def verbose_generation_errors?
18
+ @verbose_generation_errors
19
+ end
20
+ end
21
+
22
+ class << self
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def configure
28
+ yield configuration if block_given?
29
+ configuration
30
+ end
31
+
32
+ def reset_configuration!
33
+ @configuration = Configuration.new
34
+ end
35
+ end
36
+ end
@@ -10,23 +10,29 @@ end
10
10
  require "active_agent/action_prompt/action"
11
11
  require_relative "base"
12
12
  require_relative "response"
13
+ require_relative "stream_processing"
14
+ require_relative "message_formatting"
15
+ require_relative "tool_management"
13
16
 
14
17
  module ActiveAgent
15
18
  module GenerationProvider
16
19
  class AnthropicProvider < Base
20
+ include StreamProcessing
21
+ include MessageFormatting
22
+ include ToolManagement
17
23
  def initialize(config)
18
24
  super
19
25
  @access_token ||= config["api_key"] || config["access_token"] || Anthropic.configuration.access_token || ENV["ANTHROPIC_ACCESS_TOKEN"]
20
- @client = Anthropic::Client.new(access_token: @access_token)
26
+ @extra_headers = config["extra_headers"] || {}
27
+ @client = Anthropic::Client.new(access_token: @access_token, extra_headers: @extra_headers)
21
28
  end
22
29
 
23
30
  def generate(prompt)
24
31
  @prompt = prompt
25
32
 
26
- chat_prompt(parameters: prompt_parameters)
27
- rescue => e
28
- error_message = e.respond_to?(:message) ? e.message : e.to_s
29
- raise GenerationProviderError, error_message
33
+ with_error_handling do
34
+ chat_prompt(parameters: prompt_parameters)
35
+ end
30
36
  end
31
37
 
32
38
  def chat_prompt(parameters: prompt_parameters)
@@ -35,75 +41,75 @@ module ActiveAgent
35
41
  chat_response(@client.messages(parameters: parameters))
36
42
  end
37
43
 
38
- private
44
+ protected
39
45
 
40
- def provider_stream
41
- agent_stream = prompt.options[:stream]
42
- message = ActiveAgent::ActionPrompt::Message.new(content: "", role: :assistant)
43
- @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message:)
46
+ # Override from StreamProcessing module for Anthropic-specific streaming
47
+ def process_stream_chunk(chunk, message, agent_stream)
48
+ if new_content = chunk.dig(:delta, :text)
49
+ message.content += new_content
50
+ agent_stream&.call(message, new_content, false, prompt.action_name)
51
+ end
44
52
 
45
- proc do |chunk|
46
- if new_content = chunk.dig(:delta, :text)
47
- message.content += new_content
48
- agent_stream.call(message, nil, false, prompt.action_name) if agent_stream.respond_to?(:call)
49
- end
53
+ if chunk[:type] == "message_stop"
54
+ finalize_stream(message, agent_stream)
50
55
  end
51
56
  end
52
57
 
53
- def prompt_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @prompt.options[:temperature] || @config["temperature"] || 0.7, tools: @prompt.actions)
54
- # fix for new Anthropic API that requires messages to be in a specific format without system role
55
- messages = messages.reject { |m| m.role == :system }
58
+ # Override from ParameterBuilder to handle Anthropic-specific requirements
59
+ def build_provider_parameters
60
+ # Anthropic requires system message separately and no system role in messages
61
+ filtered_messages = @prompt.messages.reject { |m| m.role == :system }
62
+ system_message = @prompt.messages.find { |m| m.role == :system }
63
+
56
64
  params = {
57
- model: model,
58
- system: @prompt.options[:instructions],
59
- messages: provider_messages(messages),
60
- temperature: temperature,
61
- max_tokens: @prompt.options[:max_tokens] || @config["max_tokens"] || 4096
65
+ system: system_message&.content || @prompt.options[:instructions]
62
66
  }
63
67
 
64
- if tools&.present?
65
- params[:tools] = format_tools(tools)
66
- end
68
+ # Override messages to use filtered version
69
+ @filtered_messages = filtered_messages
67
70
 
68
71
  params
69
72
  end
70
73
 
71
- def format_tools(tools)
72
- tools.map do |tool|
73
- {
74
- name: tool["name"] || tool["function"]["name"],
75
- description: tool["description"] || tool["function"]["description"],
76
- input_schema: tool["parameters"] || tool["function"]["parameters"]
77
- }
74
+ def build_base_parameters
75
+ super.tap do |params|
76
+ # Use filtered messages if available (set by build_provider_parameters)
77
+ params[:messages] = provider_messages(@filtered_messages || @prompt.messages)
78
+ # Anthropic requires max_tokens
79
+ params[:max_tokens] ||= 4096
78
80
  end
79
81
  end
80
82
 
81
- def provider_messages(messages)
82
- messages.map do |message|
83
- provider_message = {
84
- role: convert_role(message.role),
85
- content: []
86
- }
83
+ # Override from ToolManagement for Anthropic-specific tool format
84
+ def format_single_tool(tool)
85
+ {
86
+ name: tool["name"] || tool.dig("function", "name") || tool[:name] || tool.dig(:function, :name),
87
+ description: tool["description"] || tool.dig("function", "description") || tool[:description] || tool.dig(:function, :description),
88
+ input_schema: tool["parameters"] || tool.dig("function", "parameters") || tool[:parameters] || tool.dig(:function, :parameters)
89
+ }
90
+ end
87
91
 
88
- provider_message[:content] << if message.content_type == "image_url"
89
- {
90
- type: "image",
91
- source: {
92
- type: "url",
93
- url: message.content
94
- }
95
- }
96
- else
97
- {
98
- type: "text",
99
- text: message.content
100
- }
101
- end
102
-
103
- provider_message
92
+ # Override from MessageFormatting for Anthropic-specific message format
93
+ def format_content(message)
94
+ # Anthropic requires content as an array
95
+ if message.content_type == "image_url"
96
+ [ format_image_content(message).first ]
97
+ else
98
+ [ { type: "text", text: message.content } ]
104
99
  end
105
100
  end
106
101
 
102
+ def format_image_content(message)
103
+ [ {
104
+ type: "image",
105
+ source: {
106
+ type: "url",
107
+ url: message.content
108
+ }
109
+ } ]
110
+ end
111
+
112
+ # Override from MessageFormatting for Anthropic role mapping
107
113
  def convert_role(role)
108
114
  case role.to_s
109
115
  when "system" then "system"
@@ -123,7 +129,7 @@ module ActiveAgent
123
129
  content: content,
124
130
  role: "assistant",
125
131
  action_requested: response["stop_reason"] == "tool_use",
126
- requested_actions: handle_actions(response["tool_use"])
132
+ requested_actions: handle_actions(response["content"].map { |c| c if c["type"] == "tool_use" }.reject { |m| m.blank? }.to_a),
127
133
  )
128
134
 
129
135
  update_context(prompt: prompt, message: message, response: response)
@@ -135,17 +141,18 @@ module ActiveAgent
135
141
  )
136
142
  end
137
143
 
138
- def handle_actions(tool_uses)
139
- return unless tool_uses&.present?
144
+ # Override from ToolManagement for Anthropic-specific tool parsing
145
+ def parse_tool_call(tool_use)
146
+ return nil unless tool_use
140
147
 
141
- tool_uses.map do |tool_use|
142
- ActiveAgent::ActionPrompt::Action.new(
143
- id: tool_use[:id],
144
- name: tool_use[:name],
145
- params: tool_use[:input]
146
- )
147
- end
148
+ ActiveAgent::ActionPrompt::Action.new(
149
+ id: tool_use[:id],
150
+ name: tool_use[:name],
151
+ params: tool_use[:input]
152
+ )
148
153
  end
154
+
155
+ private
149
156
  end
150
157
  end
151
158
  end
@@ -1,21 +1,34 @@
1
1
  # lib/active_agent/generation_provider/base.rb
2
2
 
3
+ require_relative "error_handling"
4
+ require_relative "parameter_builder"
5
+
3
6
  module ActiveAgent
4
7
  module GenerationProvider
5
8
  class Base
9
+ include ErrorHandling
10
+ include ParameterBuilder
11
+
6
12
  class GenerationProviderError < StandardError; end
13
+
7
14
  attr_reader :client, :config, :prompt, :response, :access_token, :model_name
8
15
 
9
16
  def initialize(config)
10
17
  @config = config
11
18
  @prompt = nil
12
19
  @response = nil
20
+ @model_name = config["model"] if config
13
21
  end
14
22
 
15
23
  def generate(prompt)
16
24
  raise NotImplementedError, "Subclasses must implement the 'generate' method"
17
25
  end
18
26
 
27
+ def embed(prompt)
28
+ # Optional embedding support - override in providers that support it
29
+ raise NotImplementedError, "#{self.class.name} does not support embeddings"
30
+ end
31
+
19
32
  private
20
33
 
21
34
  def handle_response(response)
@@ -30,11 +43,12 @@ module ActiveAgent
30
43
 
31
44
  protected
32
45
 
33
- def prompt_parameters
34
- {
35
- messages: @prompt.messages,
36
- temperature: @config["temperature"] || 0.7
37
- }
46
+ # This method is now provided by ParameterBuilder module
47
+ # but can still be overridden if needed
48
+ def build_provider_parameters
49
+ # Base implementation returns empty hash
50
+ # Providers override this to add their specific parameters
51
+ {}
38
52
  end
39
53
  end
40
54
  end