activeagent 0.4.3 → 0.5.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 +4 -4
- data/LICENSE +21 -0
- data/README.md +12 -0
- data/lib/active_agent/action_prompt/base.rb +103 -28
- data/lib/active_agent/action_prompt/message.rb +26 -1
- data/lib/active_agent/action_prompt/prompt.rb +24 -1
- data/lib/active_agent/action_prompt.rb +0 -108
- data/lib/active_agent/base.rb +11 -2
- data/lib/active_agent/generation_provider/anthropic_provider.rb +4 -3
- data/lib/active_agent/generation_provider/base_adapter.rb +19 -0
- data/lib/active_agent/generation_provider/open_ai_provider.rb +50 -6
- data/lib/active_agent/generation_provider/responses_adapter.rb +44 -0
- data/lib/active_agent/generation_provider.rb +0 -2
- data/lib/active_agent/parameterized.rb +9 -1
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +5 -0
- metadata +10 -13
- data/lib/active_agent/generation_methods.rb +0 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d2af71e1d2c2bc28b9a7d2301efe45ec7252f7f749ec3e3e0385f813becff1f
|
4
|
+
data.tar.gz: d5bc0672d10326144407d3f1c8554768c84fc61206541dde8f15e366cb21ae2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2f176f61abb5ab67321d70b56e858bd0ffb435636ebdad77c310c8656a43ae2086bc27a7d4b721b67061d5ac1530a7672ee23d7a3cfc104504c7a7666bbb998
|
7
|
+
data.tar.gz: 1398927382c0e9c73c17eb0c275e1696e49ea835537877f927f9f6ccc31cc8a7e7fdfb93ef4478b617e02a99221419b6e4a03bda2973e2b0efdf1293c4ed119f
|
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
|
+

|
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
|
-
|
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,7 +245,7 @@ module ActiveAgent
|
|
239
245
|
def initialize
|
240
246
|
super
|
241
247
|
@_prompt_was_called = false
|
242
|
-
@_context = ActiveAgent::ActionPrompt::Prompt.new(options: options)
|
248
|
+
@_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {})
|
243
249
|
end
|
244
250
|
|
245
251
|
def process(method_name, *args) # :nodoc:
|
@@ -294,25 +300,26 @@ module ActiveAgent
|
|
294
300
|
def prompt(headers = {}, &block)
|
295
301
|
return context if @_prompt_was_called && headers.blank? && !block
|
296
302
|
|
303
|
+
# Apply option hierarchy: prompt options > agent options > config options
|
304
|
+
merged_options = merge_options(headers)
|
297
305
|
raw_instructions = headers.has_key?(:instructions) ? headers[:instructions] : context.options[:instructions]
|
306
|
+
|
298
307
|
context.instructions = prepare_instructions(raw_instructions)
|
299
|
-
|
308
|
+
|
309
|
+
context.options.merge!(merged_options)
|
310
|
+
|
300
311
|
content_type = headers[:content_type]
|
312
|
+
|
301
313
|
headers = apply_defaults(headers)
|
302
314
|
context.messages = headers[:messages] || []
|
303
315
|
context.context_id = headers[:context_id]
|
304
316
|
context.params = params
|
305
317
|
|
306
|
-
context.
|
318
|
+
context.output_schema = load_schema(headers[:output_schema], set_prefixes(headers[:output_schema], lookup_context.prefixes))
|
307
319
|
|
308
|
-
|
309
|
-
headers[:body] = headers[:message].content
|
310
|
-
headers[:role] = headers[:message].role
|
311
|
-
elsif headers[:message].present? && headers[:message].is_a?(String)
|
312
|
-
headers[:body] = headers[:message]
|
313
|
-
headers[:role] = :user
|
314
|
-
end
|
320
|
+
context.charset = charset = headers[:charset]
|
315
321
|
|
322
|
+
headers = prepare_message(headers)
|
316
323
|
# wrap_generation_behavior!(headers[:generation_method], headers[:generation_method_options])
|
317
324
|
# assign_headers_to_context(context, headers)
|
318
325
|
responses = collect_responses(headers, &block)
|
@@ -322,37 +329,108 @@ module ActiveAgent
|
|
322
329
|
create_parts_from_responses(context, responses)
|
323
330
|
|
324
331
|
context.content_type = set_content_type(context, content_type, headers[:content_type])
|
332
|
+
|
325
333
|
context.charset = charset
|
326
334
|
context.actions = headers[:actions] || action_schemas
|
327
335
|
|
328
|
-
if (action_methods - ActiveAgent::Base.descendants.first.action_methods).include? action_name
|
329
|
-
context.actions = (action_methods - [ action_name ])
|
330
|
-
end
|
331
|
-
|
332
336
|
context
|
333
337
|
end
|
334
338
|
|
335
339
|
def action_methods
|
336
|
-
super - ActiveAgent::Base.public_instance_methods(false).map(&:to_s)
|
340
|
+
super - ActiveAgent::Base.public_instance_methods(false).map(&:to_s) - [ action_name ]
|
337
341
|
end
|
338
342
|
|
339
343
|
def action_schemas
|
340
|
-
prefixes = lookup_context.prefixes
|
344
|
+
prefixes = set_prefixes(action_name, lookup_context.prefixes)
|
341
345
|
|
342
346
|
action_methods.map do |action|
|
343
|
-
|
344
|
-
|
345
|
-
JSON.parse render_to_string(locals: { action_name: action }, action: action, formats: :json)
|
347
|
+
load_schema(action, prefixes)
|
346
348
|
end.compact
|
347
349
|
end
|
348
350
|
|
349
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
|
350
390
|
|
351
|
-
def
|
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:
|
352
428
|
if user_content_type.present?
|
353
429
|
user_content_type
|
354
|
-
|
355
|
-
|
430
|
+
elsif context.multimodal?
|
431
|
+
"multipart/mixed"
|
432
|
+
elsif prompt_context.body.is_a?(Array)
|
433
|
+
prompt_context.content_type || class_default
|
356
434
|
end
|
357
435
|
end
|
358
436
|
|
@@ -438,10 +516,7 @@ module ActiveAgent
|
|
438
516
|
|
439
517
|
def create_parts_from_responses(context, responses)
|
440
518
|
if responses.size > 1
|
441
|
-
# prompt_container = ActiveAgent::ActionPrompt::Prompt.new
|
442
|
-
# prompt_container.content_type = "multipart/alternative"
|
443
519
|
responses.each { |r| insert_part(context, r, context.charset) }
|
444
|
-
# context.add_part(prompt_container)
|
445
520
|
else
|
446
521
|
responses.each { |r| insert_part(context, r, context.charset) }
|
447
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,11 +24,16 @@ 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
|
@@ -74,6 +80,20 @@ module ActiveAgent
|
|
74
80
|
}
|
75
81
|
end
|
76
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
|
+
|
77
97
|
def headers(headers = {})
|
78
98
|
@headers.merge!(headers)
|
79
99
|
end
|
@@ -86,6 +106,9 @@ module ActiveAgent
|
|
86
106
|
|
87
107
|
def set_messages
|
88
108
|
@messages = [ instructions_message ] + @messages
|
109
|
+
# if @message.nil? || @message.content.blank?
|
110
|
+
# @message = @messages.last
|
111
|
+
# end
|
89
112
|
end
|
90
113
|
|
91
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
|
data/lib/active_agent/base.rb
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/active_agent/version.rb
CHANGED
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
|
|
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
|
+
version: 0.5.0rc2
|
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:
|
113
|
+
name: jbuilder
|
114
114
|
requirement: !ruby/object:Gem::Requirement
|
115
115
|
requirements:
|
116
116
|
- - ">="
|
117
117
|
- !ruby/object:Gem::Version
|
118
|
-
version: '
|
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: '
|
129
|
-
- - "<"
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '9.0'
|
125
|
+
version: '0'
|
132
126
|
- !ruby/object:Gem::Dependency
|
133
|
-
name:
|
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
|
@@ -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
|