activeagent 0.4.2 → 0.5.0rc1

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: 47bdf9e37e7297013da9b708a3dde24829cff913ef1ec74abc273768af85f4b9
4
- data.tar.gz: ad71eb99a02c3d527b9c43b9728be6f861a4f7b230eb3258709135d6268b88ce
3
+ metadata.gz: 84fb3a9276bfde64ad37b2c77498bb4516a7ba73902fd2dde9e145582b651681
4
+ data.tar.gz: cc8b182729b625c8d0fc6d34addb082602874fc22452e48a38bb9780145d956b
5
5
  SHA512:
6
- metadata.gz: b796e122648f92d27f8bb7b8ae2f7933cddc47bf7d4af682086375b6b775b54d50e8d05d94e5e70bb5dbd28b15cd17a24639d2ecde1d69dd2ef57a6bf1f29c23
7
- data.tar.gz: 58e8fddc983ca61e142e7710ff6c7315bff8215ecc007dee903ab499602ba323958fe84ec756f7932290cab2dcac214664e760d37588aada9368aacbe7492720
6
+ metadata.gz: 040e898a392c9da21a304c966f947db67698431f66c2972d27e1572a6763adf3368bd6a2456ee3826b2a5055df521c6ab2503566357178bea77c55aa0f9d18a6
7
+ data.tar.gz: 5d331eab9c61a1a1a008f71469b7d1714e0ddcbe2ed60f5a140390ab009d06839c4ef485808fb125518ab5b02fe91ecb5db22b656296331ec883a080fffed490
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Active Agents AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,12 @@
1
+ ![Active Agent Logo](https://framerusercontent.com/images/oEx786EYW2ZVL4Xf9hparOVLjHI.png)
2
+ > *Build AI in Rails*
3
+ >
4
+ > *Now Agents are Controllers*
5
+ >
6
+ > *Makes code [TonsOfFun](https://tonsoffun.github.io)!*
7
+
8
+ # Active Agent
9
+ Active Agent provides that missing AI layer in the Rails framework, offering a structured approach to building AI-powered applications through Agent Oriented Programming. **Now Agents are Controllers!** Designing applications using agents allows developers to create modular, reusable components that can be easily integrated into existing systems. This approach promotes code reusability, maintainability, and scalability, making it easier to build complex AI-driven applications with the Object Oriented Ruby code you already use today.
10
+
11
+ ## Documentation
12
+ [docs.activeagents.ai](https://docs.activeagents.ai) - The official documentation site for Active Agent.
@@ -9,7 +9,6 @@ module ActiveAgent
9
9
  module ActionPrompt
10
10
  class Base < AbstractController::Base
11
11
  include Callbacks
12
- include GenerationMethods
13
12
  include GenerationProvider
14
13
  include QueuedGeneration
15
14
  include Rescuable
@@ -105,9 +104,16 @@ module ActiveAgent
105
104
  # Define how the agent should generate content
106
105
  def generate_with(provider, **options)
107
106
  self.generation_provider = provider
108
- self.options = (options || {}).merge(options)
107
+
108
+ if options.has_key?(:instructions) || (self.options || {}).empty?
109
+ # Either instructions explicitly provided, or no inherited options exist
110
+ self.options = (self.options || {}).merge(options)
111
+ else
112
+ # Don't inherit instructions from parent if not explicitly set
113
+ inherited_options = (self.options || {}).except(:instructions)
114
+ self.options = inherited_options.merge(options)
115
+ end
109
116
  self.options[:stream] = new.agent_stream if self.options[:stream]
110
- generation_provider.config.merge!(self.options)
111
117
  end
112
118
 
113
119
  def stream_with(&stream)
@@ -239,9 +245,7 @@ module ActiveAgent
239
245
  def initialize
240
246
  super
241
247
  @_prompt_was_called = false
242
- @_context = ActiveAgent::ActionPrompt::Prompt.new(
243
- instructions: prepare_instructions(options[:instructions]), options: options
244
- )
248
+ @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {})
245
249
  end
246
250
 
247
251
  def process(method_name, *args) # :nodoc:
@@ -296,24 +300,26 @@ module ActiveAgent
296
300
  def prompt(headers = {}, &block)
297
301
  return context if @_prompt_was_called && headers.blank? && !block
298
302
 
299
- context.instructions = prepare_instructions(headers[:instructions]) if headers.has_key?(:instructions)
300
- context.options.merge!(options)
303
+ # Apply option hierarchy: prompt options > agent options > config options
304
+ merged_options = merge_options(headers)
305
+ raw_instructions = headers.has_key?(:instructions) ? headers[:instructions] : context.options[:instructions]
306
+
307
+ context.instructions = prepare_instructions(raw_instructions)
308
+
309
+ context.options.merge!(merged_options)
310
+
301
311
  content_type = headers[:content_type]
312
+
302
313
  headers = apply_defaults(headers)
303
314
  context.messages = headers[:messages] || []
304
315
  context.context_id = headers[:context_id]
305
316
  context.params = params
306
317
 
307
- context.charset = charset = headers[:charset]
318
+ context.output_schema = load_schema(headers[:output_schema], set_prefixes(headers[:output_schema], lookup_context.prefixes))
308
319
 
309
- if headers[:message].present? && headers[:message].is_a?(ActiveAgent::ActionPrompt::Message)
310
- headers[:body] = headers[:message].content
311
- headers[:role] = headers[:message].role
312
- elsif headers[:message].present? && headers[:message].is_a?(String)
313
- headers[:body] = headers[:message]
314
- headers[:role] = :user
315
- end
320
+ context.charset = charset = headers[:charset]
316
321
 
322
+ headers = prepare_message(headers)
317
323
  # wrap_generation_behavior!(headers[:generation_method], headers[:generation_method_options])
318
324
  # assign_headers_to_context(context, headers)
319
325
  responses = collect_responses(headers, &block)
@@ -323,33 +329,108 @@ module ActiveAgent
323
329
  create_parts_from_responses(context, responses)
324
330
 
325
331
  context.content_type = set_content_type(context, content_type, headers[:content_type])
332
+
326
333
  context.charset = charset
327
334
  context.actions = headers[:actions] || action_schemas
328
335
 
329
- if (action_methods - ActiveAgent::Base.descendants.first.action_methods).include? action_name
330
- context.actions = (action_methods - [ action_name ])
331
- end
332
-
333
336
  context
334
337
  end
335
338
 
336
339
  def action_methods
337
- super - ActiveAgent::Base.public_instance_methods(false).map(&:to_s)
340
+ super - ActiveAgent::Base.public_instance_methods(false).map(&:to_s) - [ action_name ]
338
341
  end
339
342
 
340
343
  def action_schemas
344
+ prefixes = set_prefixes(action_name, lookup_context.prefixes)
345
+
341
346
  action_methods.map do |action|
342
- JSON.parse render_to_string(locals: { action_name: action }, action: action, formats: :json)
347
+ load_schema(action, prefixes)
343
348
  end.compact
344
349
  end
345
350
 
346
351
  private
352
+ def prepare_message(headers)
353
+ if headers[:message].present? && headers[:message].is_a?(ActiveAgent::ActionPrompt::Message)
354
+ headers[:body] = headers[:message].content
355
+ headers[:role] = headers[:message].role
356
+ elsif headers[:message].present? && headers[:message].is_a?(String)
357
+ headers[:body] = headers[:message]
358
+ headers[:role] = :user
359
+ end
360
+ load_input_data(headers)
361
+
362
+ headers
363
+ end
364
+
365
+ def load_input_data(headers)
366
+ if headers[:image_data].present?
367
+ headers[:body] = [
368
+ ActiveAgent::ActionPrompt::Message.new(content: headers[:image_data], content_type: "image_data"),
369
+ ActiveAgent::ActionPrompt::Message.new(content: headers[:body], content_type: "input_text")
370
+ ]
371
+ elsif headers[:file_data].present?
372
+ headers[:body] = [
373
+ ActiveAgent::ActionPrompt::Message.new(content: headers[:file_data], metadata: { filename: "resume.pdf" }, content_type: "file_data"),
374
+ ActiveAgent::ActionPrompt::Message.new(content: headers[:body], content_type: "input_text")
375
+ ]
376
+ end
377
+
378
+ headers
379
+ end
380
+
381
+ def set_prefixes(action, prefixes)
382
+ prefixes = lookup_context.prefixes | [ self.class.agent_name ]
383
+ end
384
+
385
+ def load_schema(action_name, prefixes)
386
+ return unless lookup_context.template_exists?(action_name, prefixes, false, formats: [ :json ])
387
+
388
+ JSON.parse render_to_string(locals: { action_name: action_name }, action: action_name, formats: :json)
389
+ end
347
390
 
348
- def set_content_type(m, user_content_type, class_default) # :doc:
391
+ def merge_options(prompt_options)
392
+ config_options = generation_provider&.config || {}
393
+ agent_options = (self.class.options || {}).deep_dup # Defensive copy to prevent mutation
394
+
395
+ parent_options = self.class.superclass.respond_to?(:options) ? (self.class.superclass.options || {}) : {}
396
+
397
+ # Extract runtime options from prompt_options (exclude instructions as it has special template logic)
398
+ runtime_options = prompt_options.slice(
399
+ :model, :temperature, :max_tokens, :stream, :top_p, :frequency_penalty,
400
+ :presence_penalty, :response_format, :seed, :stop, :tools_choice, :user
401
+ )
402
+
403
+ # Handle explicit options parameter
404
+ explicit_options = prompt_options[:options] || {}
405
+
406
+ # Merge with proper precedence: config < agent < explicit_options
407
+ # Don't include instructions in automatic merging as it has special template fallback logic
408
+ config_options_filtered = config_options.except(:instructions)
409
+ agent_options_filtered = agent_options.except(:instructions)
410
+ explicit_options_filtered = explicit_options.except(:instructions)
411
+
412
+ merged = config_options_filtered.merge(agent_options_filtered).merge(explicit_options_filtered)
413
+
414
+ # Only merge runtime options that are actually present (not nil)
415
+ runtime_options.each do |key, value|
416
+ next if value.nil?
417
+ # Special handling for stream option: preserve agent_stream proc if it exists
418
+ if key == :stream && agent_options[:stream].is_a?(Proc) && !value.is_a?(Proc)
419
+ next
420
+ end
421
+ merged[key] = value
422
+ end
423
+
424
+ merged
425
+ end
426
+
427
+ def set_content_type(prompt_context, user_content_type, class_default) # :doc:
349
428
  if user_content_type.present?
350
429
  user_content_type
351
- else
352
- context.content_type || class_default
430
+ elsif context.multimodal?
431
+ "multipart/mixed"
432
+ elsif prompt_context.body.is_a?(Array)
433
+ prompt_context.content_type || class_default
353
434
  end
354
435
  end
355
436
 
@@ -435,10 +516,7 @@ module ActiveAgent
435
516
 
436
517
  def create_parts_from_responses(context, responses)
437
518
  if responses.size > 1
438
- # prompt_container = ActiveAgent::ActionPrompt::Prompt.new
439
- # prompt_container.content_type = "multipart/alternative"
440
519
  responses.each { |r| insert_part(context, r, context.charset) }
441
- # context.add_part(prompt_container)
442
520
  else
443
521
  responses.each { |r| insert_part(context, r, context.charset) }
444
522
  end
@@ -18,12 +18,13 @@ module ActiveAgent
18
18
  end
19
19
  VALID_ROLES = %w[system assistant user tool].freeze
20
20
 
21
- attr_accessor :action_id, :action_name, :raw_actions, :generation_id, :content, :role, :action_requested, :requested_actions, :content_type, :charset
21
+ attr_accessor :action_id, :action_name, :raw_actions, :generation_id, :content, :role, :action_requested, :requested_actions, :content_type, :charset, :metadata
22
22
 
23
23
  def initialize(attributes = {})
24
24
  @action_id = attributes[:action_id]
25
25
  @action_name = attributes[:action_name]
26
26
  @generation_id = attributes[:generation_id]
27
+ @metadata = attributes[:metadata] || {}
27
28
  @charset = attributes[:charset] || "UTF-8"
28
29
  @content = attributes[:content] || ""
29
30
  @content_type = attributes[:content_type] || "text/plain"
@@ -58,6 +59,30 @@ module ActiveAgent
58
59
  @agent_class.embed(@content)
59
60
  end
60
61
 
62
+ def inspect
63
+ truncated_content = if @content.is_a?(String) && @content.length > 256
64
+ @content[0, 256] + "..."
65
+ elsif @content.is_a?(Array)
66
+ @content.map do |item|
67
+ if item.is_a?(String) && item.length > 256
68
+ item[0, 256] + "..."
69
+ else
70
+ item
71
+ end
72
+ end
73
+ else
74
+ @content
75
+ end
76
+
77
+ "#<#{self.class}:0x#{object_id.to_s(16)}\n" +
78
+ " @action_id=#{@action_id.inspect},\n" +
79
+ " @action_name=#{@action_name.inspect},\n" +
80
+ " @action_requested=#{@action_requested.inspect},\n" +
81
+ " @charset=#{@charset.inspect},\n" +
82
+ " @content=#{truncated_content.inspect},\n" +
83
+ " @role=#{@role.inspect}>"
84
+ end
85
+
61
86
  private
62
87
 
63
88
  def validate_role
@@ -4,10 +4,11 @@ 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
7
+ attr_accessor :actions, :body, :content_type, :context_id, :message, :options, :mime_version, :charset, :context, :parts, :params, :action_choice, :agent_class, :output_schema
8
8
 
9
9
  def initialize(attributes = {})
10
10
  @options = attributes.fetch(:options, {})
11
+ @multimodal = attributes.fetch(:multimodal, false)
11
12
  @agent_class = attributes.fetch(:agent_class, ApplicationAgent)
12
13
  @actions = attributes.fetch(:actions, [])
13
14
  @action_choice = attributes.fetch(:action_choice, "")
@@ -23,17 +24,24 @@ module ActiveAgent
23
24
  @context_id = attributes.fetch(:context_id, nil)
24
25
  @headers = attributes.fetch(:headers, {})
25
26
  @parts = attributes.fetch(:parts, [])
27
+ @output_schema = attributes.fetch(:output_schema, nil)
26
28
  @messages = Message.from_messages(@messages)
27
29
  set_message if attributes[:message].is_a?(String) || @body.is_a?(String) && @message&.content
28
30
  set_messages if @instructions.present?
29
31
  end
30
32
 
33
+ def multimodal?
34
+ @multimodal ||= @message&.content.is_a?(Array) || @messages.any? { |m| m.content.is_a?(Array) }
35
+ end
36
+
31
37
  def messages=(messages)
32
38
  @messages = messages
33
39
  set_messages
34
40
  end
35
41
 
36
42
  def instructions=(instructions)
43
+ return if instructions.blank?
44
+
37
45
  @instructions = instructions
38
46
  if @messages[0].present? && @messages[0].role == :system
39
47
  @messages[0] = instructions_message
@@ -72,6 +80,20 @@ module ActiveAgent
72
80
  }
73
81
  end
74
82
 
83
+ def inspect
84
+ "#<#{self.class}:0x#{object_id.to_s(16)}\n" +
85
+ " @options=#{@options.inspect.gsub(Rails.application.credentials.dig(:openai, :api_key), '<OPENAI_API_KEY>')}\n" +
86
+ " @actions=#{@actions.inspect}\n" +
87
+ " @action_choice=#{@action_choice.inspect}\n" +
88
+ " @instructions=#{@instructions.inspect}\n" +
89
+ " @message=#{@message.inspect}\n" +
90
+ " @output_schema=#{@output_schema}\n" +
91
+ " @headers=#{@headers.inspect}\n" +
92
+ " @context=#{@context.inspect}\n" +
93
+ " @messages=#{@messages.inspect}\n" +
94
+ ">"
95
+ end
96
+
75
97
  def headers(headers = {})
76
98
  @headers.merge!(headers)
77
99
  end
@@ -84,6 +106,9 @@ module ActiveAgent
84
106
 
85
107
  def set_messages
86
108
  @messages = [ instructions_message ] + @messages
109
+ # if @message.nil? || @message.content.blank?
110
+ # @message = @messages.last
111
+ # end
87
112
  end
88
113
 
89
114
  def set_message
@@ -15,113 +15,5 @@ module ActiveAgent
15
15
  autoload :Base
16
16
 
17
17
  extend ActiveSupport::Concern
18
-
19
- included do
20
- include AbstractController::Rendering
21
- include AbstractController::Layouts
22
- include AbstractController::Helpers
23
- include AbstractController::Translation
24
- include AbstractController::AssetPaths
25
- include AbstractController::Callbacks
26
- include AbstractController::Caching
27
-
28
- include ActionView::Layouts
29
-
30
- helper ActiveAgent::PromptHelper
31
- # class_attribute :default_params, default: {
32
- # content_type: "text/plain",
33
- # parts_order: ["text/plain", "text/html", "application/json"]
34
- # }.freeze
35
- end
36
-
37
- # # def self.prompt(headers = {}, &)
38
- # # new.prompt(headers, &)
39
- # # end
40
-
41
- # def prompt(headers = {}, &block)
42
- # return @_message if @_prompt_was_called && headers.blank? && !block
43
-
44
- # headers = apply_defaults(headers)
45
-
46
- # @_message = ActiveAgent::ActionPrompt::Prompt.new
47
-
48
- # assign_headers_to_message(@_message, headers)
49
-
50
- # responses = collect_responses(headers, &block)
51
-
52
- # @_prompt_was_called = true
53
-
54
- # create_parts_from_responses(@_message, responses)
55
-
56
- # @_message
57
- # end
58
-
59
- private
60
-
61
- def apply_defaults(headers)
62
- headers.reverse_merge(self.class.default_params)
63
- end
64
-
65
- def assign_headers_to_message(message, headers)
66
- assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path)
67
- assignable.each { |k, v| message.send(:"#{k}=", v) }
68
- end
69
-
70
- def collect_responses(headers, &block)
71
- if block
72
- collect_responses_from_block(headers, &block)
73
- elsif headers[:body]
74
- collect_responses_from_text(headers)
75
- else
76
- collect_responses_from_templates(headers)
77
- end
78
- end
79
-
80
- def collect_responses_from_block(headers, &block)
81
- templates_name = headers[:template_name] || action_name
82
- collector = ActiveAgent::ActionPrompt::Collector.new(lookup_context) { render(templates_name) }
83
- yield(collector)
84
- collector.responses
85
- end
86
-
87
- def collect_responses_from_text(headers)
88
- [ {
89
- body: headers.delete(:body),
90
- content_type: headers[:content_type] || "text/plain"
91
- } ]
92
- end
93
-
94
- def collect_responses_from_templates(headers)
95
- templates_path = headers[:template_path] || self.class.name.sub(/Agent$/, "").underscore
96
- templates_name = headers[:template_name] || action_name
97
- each_template(Array(templates_path), templates_name).map do |template|
98
- format = template.format || formats.first
99
- {
100
- body: render(template: template, formats: [ format ]),
101
- content_type: Mime[format].to_s
102
- }
103
- end
104
- end
105
-
106
- def each_template(paths, name, &)
107
- templates = lookup_context.find_all(name, paths)
108
- if templates.empty?
109
- raise ActionView::MissingTemplate.new(paths, name, paths, false, "prompt")
110
- else
111
- templates.uniq(&:format).each(&)
112
- end
113
- end
114
-
115
- def create_parts_from_responses(message, responses)
116
- responses.each do |response|
117
- message.add_part(response[:body], content_type: response[:content_type])
118
- end
119
- end
120
-
121
- class TestAgent
122
- class << self
123
- attr_accessor :generations
124
- end
125
- end
126
18
  end
127
19
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_agent/action_prompt"
3
4
  require "active_agent/prompt_helper"
4
5
  require "active_agent/action_prompt/base"
5
6
 
@@ -29,8 +30,16 @@ module ActiveAgent
29
30
  # ActiveAgent::Base is designed to be extended by specific agent implementations.
30
31
  # It provides a common set of agent actions for self-contained agents that can determine their own actions using all available actions.
31
32
  # Base actions include: prompt_context, continue, reasoning, reiterate, and conclude
32
- def prompt_context
33
- prompt(stream: params[:stream], messages: params[:messages], message: params[:message], context_id: params[:context_id])
33
+ def prompt_context(additional_options = {})
34
+ prompt(
35
+ {
36
+ stream: params[:stream],
37
+ messages: params[:messages],
38
+ message: params[:message],
39
+ context_id: params[:context_id],
40
+ options: params[:options]
41
+ }.merge(additional_options)
42
+ )
34
43
  end
35
44
  end
36
45
  end
@@ -20,7 +20,8 @@ module ActiveAgent
20
20
 
21
21
  chat_prompt(parameters: prompt_parameters)
22
22
  rescue => e
23
- raise GenerationProviderError, e.message
23
+ error_message = e.respond_to?(:message) ? e.message : e.to_s
24
+ raise GenerationProviderError, error_message
24
25
  end
25
26
 
26
27
  def chat_prompt(parameters: prompt_parameters)
@@ -44,12 +45,12 @@ module ActiveAgent
44
45
  end
45
46
  end
46
47
 
47
- def prompt_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @config["temperature"] || 0.7, tools: @prompt.actions)
48
+ def prompt_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @prompt.options[:temperature] || @config["temperature"] || 0.7, tools: @prompt.actions)
48
49
  params = {
49
50
  model: model,
50
51
  messages: provider_messages(messages),
51
52
  temperature: temperature,
52
- max_tokens: 4096
53
+ max_tokens: @prompt.options[:max_tokens] || @config["max_tokens"] || 4096
53
54
  }
54
55
 
55
56
  if tools&.present?
@@ -0,0 +1,19 @@
1
+ module ActiveAgent
2
+ module GenerationProvider
3
+ class BaseAdapter
4
+ attr_reader :prompt
5
+
6
+ def initialize(prompt)
7
+ @prompt = prompt
8
+ end
9
+
10
+ def input
11
+ raise NotImplementedError, "Subclasses must implement the 'input' method"
12
+ end
13
+
14
+ def response
15
+ raise NotImplementedError, "Subclasses must implement the 'response' method"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,7 +1,14 @@
1
- require "openai"
1
+ begin
2
+ gem "ruby-openai", "~> 8.1.0"
3
+ require "openai"
4
+ rescue LoadError
5
+ raise LoadError, "The 'ruby-openai' gem is required for OpenAIProvider. Please add it to your Gemfile and run `bundle install`."
6
+ end
7
+
2
8
  require "active_agent/action_prompt/action"
3
9
  require_relative "base"
4
10
  require_relative "response"
11
+ require_relative "responses_adapter"
5
12
 
6
13
  module ActiveAgent
7
14
  module GenerationProvider
@@ -21,9 +28,14 @@ module ActiveAgent
21
28
  def generate(prompt)
22
29
  @prompt = prompt
23
30
 
24
- chat_prompt(parameters: prompt_parameters)
31
+ if @prompt.multimodal? || @prompt.content_type == "multipart/mixed"
32
+ responses_prompt(parameters: responses_parameters)
33
+ else
34
+ chat_prompt(parameters: prompt_parameters)
35
+ end
25
36
  rescue => e
26
- raise GenerationProviderError, e.message
37
+ error_message = e.respond_to?(:message) ? e.message : e.to_s
38
+ raise GenerationProviderError, error_message
27
39
  end
28
40
 
29
41
  def embed(prompt)
@@ -31,7 +43,8 @@ module ActiveAgent
31
43
 
32
44
  embeddings_prompt(parameters: embeddings_parameters)
33
45
  rescue => e
34
- raise GenerationProviderError, e.message
46
+ error_message = e.respond_to?(:message) ? e.message : e.to_s
47
+ raise GenerationProviderError, error_message
35
48
  end
36
49
 
37
50
  private
@@ -63,13 +76,14 @@ module ActiveAgent
63
76
  end
64
77
  end
65
78
 
66
- def prompt_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @config["temperature"] || 0.7, tools: @prompt.actions)
79
+ def prompt_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @prompt.options[:temperature] || @config["temperature"] || 0.7, tools: @prompt.actions)
67
80
  {
68
81
  model: model,
69
82
  messages: provider_messages(messages),
70
83
  temperature: temperature,
84
+ max_tokens: @prompt.options[:max_tokens] || @config["max_tokens"],
71
85
  tools: tools.presence
72
- }
86
+ }.compact
73
87
  end
74
88
 
75
89
  def provider_messages(messages)
@@ -104,6 +118,22 @@ module ActiveAgent
104
118
  @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message: message, raw_response: response)
105
119
  end
106
120
 
121
+ def responses_response(response)
122
+ message_json = response.dig("output", 0)
123
+ message_json["id"] = response.dig("id") if message_json["id"].blank?
124
+
125
+ message = ActiveAgent::ActionPrompt::Message.new(
126
+ generate_id: message_json["id"],
127
+ content: message_json["content"].first["text"],
128
+ role: message_json["role"].intern,
129
+ action_requested: message_json["finish_reason"] == "tool_calls",
130
+ raw_actions: message_json["tool_calls"] || [],
131
+ content_type: prompt.output_schema.present? ? "application/json" : "text/plain",
132
+ )
133
+
134
+ @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message: message, raw_response: response)
135
+ end
136
+
107
137
  def handle_message(message_json)
108
138
  ActiveAgent::ActionPrompt::Message.new(
109
139
  generation_id: message_json["id"],
@@ -135,6 +165,20 @@ module ActiveAgent
135
165
  chat_response(@client.chat(parameters: parameters))
136
166
  end
137
167
 
168
+ def responses_prompt(parameters: responses_parameters)
169
+ # parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
170
+ responses_response(@client.responses.create(parameters: parameters))
171
+ end
172
+
173
+ 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)
174
+ {
175
+ model: model,
176
+ input: ActiveAgent::GenerationProvider::ResponsesAdapter.new(@prompt).input,
177
+ tools: tools.presence,
178
+ text: structured_output
179
+ }.compact
180
+ end
181
+
138
182
  def embeddings_parameters(input: prompt.message.content, model: "text-embedding-3-large")
139
183
  {
140
184
  model: model,
@@ -0,0 +1,44 @@
1
+ require_relative "base_adapter"
2
+
3
+ module ActiveAgent
4
+ module GenerationProvider
5
+ class ResponsesAdapter < BaseAdapter
6
+ def initialize(prompt)
7
+ super(prompt)
8
+ @prompt = prompt
9
+ end
10
+
11
+ def input
12
+ messages.map do |message|
13
+ if message.content.is_a?(Array)
14
+ {
15
+ role: message.role,
16
+ content: message.content.map do |content_part|
17
+ if content_part.is_a?(String)
18
+ { type: "input_text", text: content_part }
19
+ elsif content_part.is_a?(ActiveAgent::ActionPrompt::Message) && content_part.content_type == "input_text"
20
+ { type: "input_text", text: content_part.content }
21
+ elsif content_part.is_a?(ActiveAgent::ActionPrompt::Message) && content_part.content_type == "image_data"
22
+ { type: "input_image", image_url: content_part.content }
23
+ elsif content_part.is_a?(ActiveAgent::ActionPrompt::Message) && content_part.content_type == "file_data"
24
+ { type: "input_file", filename: content_part.metadata[:filename], file_data: content_part.content }
25
+ else
26
+ raise ArgumentError, "Unsupported content type in message"
27
+ end
28
+ end.compact
29
+ }
30
+ else
31
+ {
32
+ role: message.role,
33
+ content: message.content
34
+ }
35
+ end
36
+ end.compact
37
+ end
38
+
39
+ def messages
40
+ prompt.messages
41
+ end
42
+ end
43
+ end
44
+ end
@@ -23,8 +23,6 @@ module ActiveAgent
23
23
  def configure_provider(config)
24
24
  require "active_agent/generation_provider/#{config["service"].underscore}_provider"
25
25
  ActiveAgent::GenerationProvider.const_get("#{config["service"].camelize}Provider").new(config)
26
- rescue LoadError
27
- raise "Missing generation provider configuration for #{config["service"].inspect}"
28
26
  end
29
27
 
30
28
  def generation_provider
@@ -13,7 +13,15 @@ module ActiveAgent
13
13
  end
14
14
 
15
15
  module ClassMethods
16
- def with(params)
16
+ def with(params = {})
17
+ # Separate runtime options from regular params
18
+ runtime_options = params.extract!(:model, :temperature, :max_tokens, :stream, :top_p,
19
+ :frequency_penalty, :presence_penalty, :response_format,
20
+ :seed, :stop, :tools_choice, :user)
21
+
22
+ # Pass runtime options as :options parameter
23
+ params[:options] = runtime_options if runtime_options.any?
24
+
17
25
  ActiveAgent::Parameterized::Agent.new(self, params)
18
26
  end
19
27
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveAgent
2
- VERSION = "0.4.2"
2
+ VERSION = "0.5.0rc1"
3
3
  end
data/lib/active_agent.rb CHANGED
@@ -34,6 +34,11 @@ module ActiveAgent
34
34
  class << self
35
35
  attr_accessor :config
36
36
 
37
+ def filter_credential_keys(example)
38
+ example.gsub(Rails.application.credentials.dig(:openai, :api_key), "<OPENAI_API_KEY>")
39
+ .gsub(Rails.application.credentials.dig(:open_router, :api_key), "<OPEN_ROUTER_API_KEY>")
40
+ end
41
+
37
42
  def eager_load!
38
43
  super
39
44
 
@@ -8,7 +8,6 @@ module Erb # :nodoc:
8
8
  source_root File.expand_path("templates", __dir__)
9
9
 
10
10
  def create_agent_layouts
11
- template "layout.html.erb.tt", "app/views/layouts/agent.html.erb"
12
11
  template "layout.text.erb.tt", "app/views/layouts/agent.text.erb"
13
12
  template "layout.json.erb.tt", "app/views/layouts/agent.json.erb"
14
13
  end
@@ -4,7 +4,7 @@ require "rails/generators/test_unit"
4
4
 
5
5
  module TestUnit # :nodoc:
6
6
  module Generators # :nodoc:
7
- class InstallGenerator < Base # :nodoc:
7
+ class InstallGenerator < ::Rails::Generators::Base # :nodoc:
8
8
  # TestUnit install generator for ActiveAgent
9
9
  # This can be used to create additional test-specific files during installation
10
10
  # Currently no additional files are needed for TestUnit setup
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.4.2
4
+ version: 0.5.0rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
@@ -110,27 +110,21 @@ dependencies:
110
110
  - !ruby/object:Gem::Version
111
111
  version: '9.0'
112
112
  - !ruby/object:Gem::Dependency
113
- name: rails
113
+ name: jbuilder
114
114
  requirement: !ruby/object:Gem::Requirement
115
115
  requirements:
116
116
  - - ">="
117
117
  - !ruby/object:Gem::Version
118
- version: '7.2'
119
- - - "<"
120
- - !ruby/object:Gem::Version
121
- version: '9.0'
122
- type: :runtime
118
+ version: '0'
119
+ type: :development
123
120
  prerelease: false
124
121
  version_requirements: !ruby/object:Gem::Requirement
125
122
  requirements:
126
123
  - - ">="
127
124
  - !ruby/object:Gem::Version
128
- version: '7.2'
129
- - - "<"
130
- - !ruby/object:Gem::Version
131
- version: '9.0'
125
+ version: '0'
132
126
  - !ruby/object:Gem::Dependency
133
- name: jbuilder
127
+ name: rails
134
128
  requirement: !ruby/object:Gem::Requirement
135
129
  requirements:
136
130
  - - ">="
@@ -152,6 +146,8 @@ extensions: []
152
146
  extra_rdoc_files: []
153
147
  files:
154
148
  - CHANGELOG.md
149
+ - LICENSE
150
+ - README.md
155
151
  - lib/active_agent.rb
156
152
  - lib/active_agent/action_prompt.rb
157
153
  - lib/active_agent/action_prompt/action.rb
@@ -164,14 +160,15 @@ files:
164
160
  - lib/active_agent/deprecator.rb
165
161
  - lib/active_agent/generation.rb
166
162
  - lib/active_agent/generation_job.rb
167
- - lib/active_agent/generation_methods.rb
168
163
  - lib/active_agent/generation_provider.rb
169
164
  - lib/active_agent/generation_provider/anthropic_provider.rb
170
165
  - lib/active_agent/generation_provider/base.rb
166
+ - lib/active_agent/generation_provider/base_adapter.rb
171
167
  - lib/active_agent/generation_provider/ollama_provider.rb
172
168
  - lib/active_agent/generation_provider/open_ai_provider.rb
173
169
  - lib/active_agent/generation_provider/open_router_provider.rb
174
170
  - lib/active_agent/generation_provider/response.rb
171
+ - lib/active_agent/generation_provider/responses_adapter.rb
175
172
  - lib/active_agent/inline_preview_interceptor.rb
176
173
  - lib/active_agent/log_subscriber.rb
177
174
  - lib/active_agent/parameterized.rb
@@ -227,7 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
227
224
  - !ruby/object:Gem::Version
228
225
  version: '0'
229
226
  requirements: []
230
- rubygems_version: 3.6.9
227
+ rubygems_version: 3.7.0
231
228
  specification_version: 4
232
229
  summary: Rails AI Agents Framework
233
230
  test_files: []
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tmpdir"
4
- require_relative "action_prompt"
5
-
6
- module ActiveAgent
7
- # = Active Agent \GenerationM74ethods
8
- #
9
- # This module handles everything related to prompt generation, from registering
10
- # new generation methods to configuring the prompt object to be sent.
11
- module GenerationMethods
12
- extend ActiveSupport::Concern
13
-
14
- included do
15
- # Do not make this inheritable, because we always want it to propagate
16
- cattr_accessor :raise_generation_errors, default: true
17
- cattr_accessor :perform_generations, default: true
18
-
19
- class_attribute :generation_methods, default: {}.freeze
20
- class_attribute :generation_method, default: :smtp
21
-
22
- add_generation_method :test, ActiveAgent::ActionPrompt::TestAgent
23
- end
24
-
25
- module ClassMethods
26
- delegate :generations, :generations=, to: ActiveAgent::ActionPrompt::TestAgent
27
-
28
- def add_generation_method(symbol, klass, default_options = {})
29
- class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
30
- public_send(:"#{symbol}_settings=", default_options)
31
- self.generation_methods = generation_methods.merge(symbol.to_sym => klass).freeze
32
- end
33
-
34
- def wrap_generation_behavior(prompt, method = nil, options = nil) # :nodoc:
35
- method ||= generation_method
36
- prompt.generation_handler = self
37
-
38
- case method
39
- when NilClass
40
- raise "Generation method cannot be nil"
41
- when Symbol
42
- if klass = generation_methods[method]
43
- prompt.generation_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))
44
- else
45
- raise "Invalid generation method #{method.inspect}"
46
- end
47
- else
48
- prompt.generation_method(method)
49
- end
50
-
51
- prompt.perform_generations = perform_generations
52
- prompt.raise_generation_errors = raise_generation_errors
53
- end
54
- end
55
-
56
- def wrap_generation_behavior!(*) # :nodoc:
57
- self.class.wrap_generation_behavior(prompt, *)
58
- end
59
- end
60
- end