activeagent 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/active_agent/action_prompt/base.rb +468 -0
- data/lib/active_agent/action_prompt/message.rb +22 -2
- data/lib/active_agent/action_prompt/prompt.rb +2 -2
- data/lib/active_agent/action_prompt.rb +84 -0
- data/lib/active_agent/base.rb +9 -448
- data/lib/active_agent/callbacks.rb +9 -9
- data/lib/active_agent/generation.rb +3 -3
- data/lib/active_agent/generation_job.rb +8 -6
- data/lib/active_agent/generation_provider/open_ai_provider.rb +20 -8
- data/lib/active_agent/parameterized.rb +1 -1
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +1 -2
- data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -4
- metadata +18 -9
- data/lib/active_agent/README.md +0 -21
- data/lib/active_agent/action_prompt/README.md +0 -0
- data/lib/active_agent/generation_provider/README.md +0 -0
data/lib/active_agent/base.rb
CHANGED
@@ -1,14 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_agent/prompt_helper"
|
4
|
-
require "active_agent/action_prompt/
|
5
|
-
require "active_agent/collector"
|
6
|
-
require "active_support/core_ext/string/inflections"
|
7
|
-
require "active_support/core_ext/hash/except"
|
8
|
-
require "active_support/core_ext/module/anonymous"
|
9
|
-
|
10
|
-
# require "active_agent/log_subscriber"
|
11
|
-
require "active_agent/rescuable"
|
4
|
+
require "active_agent/action_prompt/base"
|
12
5
|
|
13
6
|
# The ActiveAgent module provides a framework for creating agents that can generate content
|
14
7
|
# and handle various actions. The Base class within this module extends AbstractController::Base
|
@@ -30,446 +23,14 @@ require "active_agent/rescuable"
|
|
30
23
|
# The class also includes several protected instance variables and defines hooks for loading
|
31
24
|
# additional functionality.
|
32
25
|
module ActiveAgent
|
33
|
-
class Base <
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
include
|
39
|
-
|
40
|
-
|
41
|
-
# include FormBuilder
|
42
|
-
|
43
|
-
abstract!
|
44
|
-
|
45
|
-
include AbstractController::Rendering
|
46
|
-
|
47
|
-
include AbstractController::Logger
|
48
|
-
include AbstractController::Helpers
|
49
|
-
include AbstractController::Translation
|
50
|
-
include AbstractController::AssetPaths
|
51
|
-
include AbstractController::Callbacks
|
52
|
-
include AbstractController::Caching
|
53
|
-
|
54
|
-
include ActionView::Layouts
|
55
|
-
|
56
|
-
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ :@_action_has_layout ]
|
57
|
-
|
58
|
-
helper ActiveAgent::PromptHelper
|
59
|
-
|
60
|
-
class_attribute :options
|
61
|
-
|
62
|
-
class_attribute :default_params, default: {
|
63
|
-
mime_version: "1.0",
|
64
|
-
charset: "UTF-8",
|
65
|
-
content_type: "text/plain",
|
66
|
-
parts_order: [ "text/plain", "text/enriched", "text/html" ]
|
67
|
-
}.freeze
|
68
|
-
|
69
|
-
class << self
|
70
|
-
# Register one or more Observers which will be notified when prompt is generated.
|
71
|
-
def register_observers(*observers)
|
72
|
-
observers.flatten.compact.each { |observer| register_observer(observer) }
|
73
|
-
end
|
74
|
-
|
75
|
-
# Unregister one or more previously registered Observers.
|
76
|
-
def unregister_observers(*observers)
|
77
|
-
observers.flatten.compact.each { |observer| unregister_observer(observer) }
|
78
|
-
end
|
79
|
-
|
80
|
-
# Register one or more Interceptors which will be called before prompt is sent.
|
81
|
-
def register_interceptors(*interceptors)
|
82
|
-
interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
|
83
|
-
end
|
84
|
-
|
85
|
-
# Unregister one or more previously registered Interceptors.
|
86
|
-
def unregister_interceptors(*interceptors)
|
87
|
-
interceptors.flatten.compact.each { |interceptor| unregister_interceptor(interceptor) }
|
88
|
-
end
|
89
|
-
|
90
|
-
# Register an Observer which will be notified when prompt is generated.
|
91
|
-
# Either a class, string, or symbol can be passed in as the Observer.
|
92
|
-
# If a string or symbol is passed in it will be camelized and constantized.
|
93
|
-
def register_observer(observer)
|
94
|
-
Prompt.register_observer(observer_class_for(observer))
|
95
|
-
end
|
96
|
-
|
97
|
-
# Unregister a previously registered Observer.
|
98
|
-
# Either a class, string, or symbol can be passed in as the Observer.
|
99
|
-
# If a string or symbol is passed in it will be camelized and constantized.
|
100
|
-
def unregister_observer(observer)
|
101
|
-
Prompt.unregister_observer(observer_class_for(observer))
|
102
|
-
end
|
103
|
-
|
104
|
-
# Register an Interceptor which will be called before prompt is sent.
|
105
|
-
# Either a class, string, or symbol can be passed in as the Interceptor.
|
106
|
-
# If a string or symbol is passed in it will be camelized and constantized.
|
107
|
-
def register_interceptor(interceptor)
|
108
|
-
Prompt.register_interceptor(observer_class_for(interceptor))
|
109
|
-
end
|
110
|
-
|
111
|
-
# Unregister a previously registered Interceptor.
|
112
|
-
# Either a class, string, or symbol can be passed in as the Interceptor.
|
113
|
-
# If a string or symbol is passed in it will be camelized and constantized.
|
114
|
-
def unregister_interceptor(interceptor)
|
115
|
-
Prompt.unregister_interceptor(observer_class_for(interceptor))
|
116
|
-
end
|
117
|
-
|
118
|
-
def observer_class_for(value) # :nodoc:
|
119
|
-
case value
|
120
|
-
when String, Symbol
|
121
|
-
value.to_s.camelize.constantize
|
122
|
-
else
|
123
|
-
value
|
124
|
-
end
|
125
|
-
end
|
126
|
-
private :observer_class_for
|
127
|
-
|
128
|
-
# Define how the agent should generate content
|
129
|
-
def generate_with(provider, **options)
|
130
|
-
self.generation_provider = provider
|
131
|
-
self.options = (options || {}).merge(options)
|
132
|
-
self.options[:stream] = new.agent_stream if self.options[:stream]
|
133
|
-
generation_provider.config.merge!(self.options)
|
134
|
-
end
|
135
|
-
|
136
|
-
def stream_with(&stream)
|
137
|
-
self.options = (options || {}).merge(stream: stream)
|
138
|
-
end
|
139
|
-
|
140
|
-
# Returns the name of the current agent. This method is also being used as a path for a view lookup.
|
141
|
-
# If this is an anonymous agent, this method will return +anonymous+ instead.
|
142
|
-
def agent_name
|
143
|
-
@agent_name ||= anonymous? ? "anonymous" : name.underscore
|
144
|
-
end
|
145
|
-
# Allows to set the name of current agent.
|
146
|
-
attr_writer :agent_name
|
147
|
-
alias_method :controller_path, :agent_name
|
148
|
-
|
149
|
-
# Sets the defaults through app configuration:
|
150
|
-
#
|
151
|
-
# config.action_agent.default(from: "no-reply@example.org")
|
152
|
-
#
|
153
|
-
# Aliased by ::default_options=
|
154
|
-
def default(value = nil)
|
155
|
-
self.default_params = default_params.merge(value).freeze if value
|
156
|
-
default_params
|
157
|
-
end
|
158
|
-
# Allows to set defaults through app configuration:
|
159
|
-
#
|
160
|
-
# config.action_agent.default_options = { from: "no-reply@example.org" }
|
161
|
-
alias_method :default_options=, :default
|
162
|
-
|
163
|
-
# Wraps a prompt generation inside of ActiveSupport::Notifications instrumentation.
|
164
|
-
#
|
165
|
-
# This method is actually called by the +ActionPrompt::Prompt+ object itself
|
166
|
-
# through a callback when you call <tt>:generate_prompt</tt> on the +ActionPrompt::Prompt+,
|
167
|
-
# calling +generate_prompt+ directly and passing an +ActionPrompt::Prompt+ will do
|
168
|
-
# nothing except tell the logger you generated the prompt.
|
169
|
-
def generate_prompt(prompt) # :nodoc:
|
170
|
-
ActiveSupport::Notifications.instrument("deliver.active_agent") do |payload|
|
171
|
-
set_payload_for_prompt(payload, prompt)
|
172
|
-
yield # Let Prompt do the generation actions
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
private
|
177
|
-
|
178
|
-
def set_payload_for_prompt(payload, prompt)
|
179
|
-
payload[:prompt] = prompt.encoded
|
180
|
-
payload[:agent] = agent_name
|
181
|
-
payload[:message_id] = prompt.message_id
|
182
|
-
payload[:date] = prompt.date
|
183
|
-
payload[:perform_generations] = prompt.perform_generations
|
184
|
-
end
|
185
|
-
|
186
|
-
def method_missing(method_name, ...)
|
187
|
-
if action_methods.include?(method_name.name)
|
188
|
-
Generation.new(self, method_name, ...)
|
189
|
-
else
|
190
|
-
super
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
def respond_to_missing?(method, include_all = false)
|
195
|
-
action_methods.include?(method.name) || super
|
196
|
-
end
|
26
|
+
class Base < ActiveAgent::ActionPrompt::Base
|
27
|
+
# This class is the base class for agents in the ActiveAgent framework.
|
28
|
+
# It is built on top of ActionPrompt which provides methods for generating content, handling actions, and managing prompts.
|
29
|
+
# ActiveAgent::Base is designed to be extended by specific agent implementations.
|
30
|
+
# It provides a common set of agent actions for self-contained agents that can determine their own actions using all available actions.
|
31
|
+
# Base actions include: text_prompt, continue, reasoning, reiterate, and conclude
|
32
|
+
def text_prompt
|
33
|
+
prompt(stream: params[:stream], messages: params[:messages], message: params[:message], context_id: params[:context_id])
|
197
34
|
end
|
198
|
-
|
199
|
-
attr_internal :prompt_context
|
200
|
-
|
201
|
-
def agent_stream
|
202
|
-
proc do |message, delta, stop|
|
203
|
-
run_stream_callbacks(message, delta, stop) do |message, delta, stop|
|
204
|
-
yield message, delta, stop if block_given?
|
205
|
-
end
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
def embed
|
210
|
-
prompt_context.options.merge(options)
|
211
|
-
generation_provider.embed(prompt_context) if prompt_context && generation_provider
|
212
|
-
handle_response(generation_provider.response)
|
213
|
-
end
|
214
|
-
|
215
|
-
# Add embedding capability to Message class
|
216
|
-
ActiveAgent::ActionPrompt::Message.class_eval do
|
217
|
-
def embed
|
218
|
-
agent_class = ActiveAgent::Base.descendants.first
|
219
|
-
agent = agent_class.new
|
220
|
-
agent.prompt_context = ActiveAgent::ActionPrompt::Prompt.new(message: self)
|
221
|
-
agent.embed
|
222
|
-
self
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
|
-
# Make prompt_context accessible for chaining
|
227
|
-
# attr_accessor :prompt_context
|
228
|
-
|
229
|
-
def perform_generation
|
230
|
-
prompt_context.options.merge(options)
|
231
|
-
generation_provider.generate(prompt_context) if prompt_context && generation_provider
|
232
|
-
handle_response(generation_provider.response)
|
233
|
-
end
|
234
|
-
|
235
|
-
def handle_response(response)
|
236
|
-
return response unless response.message.requested_actions.present?
|
237
|
-
perform_actions(requested_actions: response.message.requested_actions)
|
238
|
-
update_prompt_context(response)
|
239
|
-
end
|
240
|
-
|
241
|
-
def update_prompt_context(response)
|
242
|
-
prompt_context.message = prompt_context.messages.last
|
243
|
-
ActiveAgent::GenerationProvider::Response.new(prompt: prompt_context)
|
244
|
-
end
|
245
|
-
|
246
|
-
def perform_actions(requested_actions:)
|
247
|
-
requested_actions.each do |action|
|
248
|
-
perform_action(action)
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
def perform_action(action)
|
253
|
-
current_context = prompt_context.clone
|
254
|
-
process(action.name, *action.params)
|
255
|
-
prompt_context.messages.last.role = :tool
|
256
|
-
prompt_context.messages.last.action_id = action.id
|
257
|
-
current_context.messages << prompt_context.messages.last
|
258
|
-
self.prompt_context = current_context
|
259
|
-
end
|
260
|
-
|
261
|
-
def initialize
|
262
|
-
super
|
263
|
-
@_prompt_was_called = false
|
264
|
-
@_prompt_context = ActiveAgent::ActionPrompt::Prompt.new(instructions: options[:instructions], options: options)
|
265
|
-
end
|
266
|
-
|
267
|
-
def process(method_name, *args) # :nodoc:
|
268
|
-
payload = {
|
269
|
-
agent: self.class.name,
|
270
|
-
action: method_name,
|
271
|
-
args: args
|
272
|
-
}
|
273
|
-
|
274
|
-
ActiveSupport::Notifications.instrument("process.active_agent", payload) do
|
275
|
-
super
|
276
|
-
@_prompt_context = ActiveAgent::ActionPrompt::Prompt.new unless @_prompt_was_called
|
277
|
-
end
|
278
|
-
end
|
279
|
-
ruby2_keywords(:process)
|
280
|
-
|
281
|
-
class NullPrompt # :nodoc:
|
282
|
-
def message
|
283
|
-
""
|
284
|
-
end
|
285
|
-
|
286
|
-
def header
|
287
|
-
{}
|
288
|
-
end
|
289
|
-
|
290
|
-
def respond_to?(string, include_all = false)
|
291
|
-
true
|
292
|
-
end
|
293
|
-
|
294
|
-
def method_missing(...)
|
295
|
-
nil
|
296
|
-
end
|
297
|
-
end
|
298
|
-
|
299
|
-
# Returns the name of the agent object.
|
300
|
-
def agent_name
|
301
|
-
self.class.agent_name
|
302
|
-
end
|
303
|
-
|
304
|
-
def headers(args = nil)
|
305
|
-
if args
|
306
|
-
@_prompt_context.headers(args)
|
307
|
-
else
|
308
|
-
@_prompt_context
|
309
|
-
end
|
310
|
-
end
|
311
|
-
|
312
|
-
def prompt_with(*)
|
313
|
-
prompt_context.update_prompt_context(*)
|
314
|
-
end
|
315
|
-
|
316
|
-
def prompt(headers = {}, &block)
|
317
|
-
return prompt_context if @_prompt_was_called && headers.blank? && !block
|
318
|
-
content_type = headers[:content_type]
|
319
|
-
headers = apply_defaults(headers)
|
320
|
-
prompt_context.messages = headers[:messages] || []
|
321
|
-
prompt_context.context_id = headers[:context_id]
|
322
|
-
|
323
|
-
prompt_context.charset = charset = headers[:charset]
|
324
|
-
|
325
|
-
responses = collect_responses(headers, &block)
|
326
|
-
|
327
|
-
@_prompt_was_called = true
|
328
|
-
|
329
|
-
create_parts_from_responses(prompt_context, responses)
|
330
|
-
|
331
|
-
prompt_context.content_type = set_content_type(prompt_context, content_type, headers[:content_type])
|
332
|
-
prompt_context.charset = charset
|
333
|
-
prompt_context.actions = headers[:actions] || action_schemas
|
334
|
-
|
335
|
-
prompt_context
|
336
|
-
end
|
337
|
-
|
338
|
-
def action_schemas
|
339
|
-
action_methods.map do |action|
|
340
|
-
if action != "text_prompt"
|
341
|
-
JSON.parse render_to_string(locals: { action_name: action }, action: action, formats: :json)
|
342
|
-
end
|
343
|
-
end.compact
|
344
|
-
end
|
345
|
-
|
346
|
-
private
|
347
|
-
|
348
|
-
def set_content_type(m, user_content_type, class_default) # :doc:
|
349
|
-
if user_content_type.present?
|
350
|
-
user_content_type
|
351
|
-
else
|
352
|
-
prompt_context.content_type || class_default
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
# Translates the +subject+ using \Rails I18n class under <tt>[agent_scope, action_name]</tt> scope.
|
357
|
-
# If it does not find a translation for the +subject+ under the specified scope it will default to a
|
358
|
-
# humanized version of the <tt>action_name</tt>.
|
359
|
-
# If the subject has interpolations, you can pass them through the +interpolations+ parameter.
|
360
|
-
def default_i18n_subject(interpolations = {}) # :doc:
|
361
|
-
agent_scope = self.class.agent_name.tr("/", ".")
|
362
|
-
I18n.t(:subject, **interpolations.merge(scope: [ agent_scope, action_name ], default: action_name.humanize))
|
363
|
-
end
|
364
|
-
|
365
|
-
def apply_defaults(headers)
|
366
|
-
default_values = self.class.default.except(*headers.keys).transform_values do |value|
|
367
|
-
compute_default(value)
|
368
|
-
end
|
369
|
-
|
370
|
-
headers.reverse_merge(default_values)
|
371
|
-
end
|
372
|
-
|
373
|
-
def compute_default(value)
|
374
|
-
return value unless value.is_a?(Proc)
|
375
|
-
|
376
|
-
if value.arity == 1
|
377
|
-
instance_exec(self, &value)
|
378
|
-
else
|
379
|
-
instance_exec(&value)
|
380
|
-
end
|
381
|
-
end
|
382
|
-
|
383
|
-
def assign_headers_to_prompt_context(prompt_context, headers)
|
384
|
-
assignable = headers.except(:parts_order, :content_type, :body, :template_name,
|
385
|
-
:template_path, :delivery_method, :delivery_method_options)
|
386
|
-
assignable.each { |k, v| prompt_context[k] = v }
|
387
|
-
end
|
388
|
-
|
389
|
-
def collect_responses(headers, &)
|
390
|
-
if block_given?
|
391
|
-
collect_responses_from_block(headers, &)
|
392
|
-
elsif headers[:body]
|
393
|
-
collect_responses_from_text(headers)
|
394
|
-
else
|
395
|
-
collect_responses_from_templates(headers)
|
396
|
-
end
|
397
|
-
end
|
398
|
-
|
399
|
-
def collect_responses_from_block(headers)
|
400
|
-
templates_name = headers[:template_name] || action_name
|
401
|
-
collector = Collector.new(lookup_context) { render(templates_name) }
|
402
|
-
yield(collector)
|
403
|
-
collector.responses
|
404
|
-
end
|
405
|
-
|
406
|
-
def collect_responses_from_text(headers)
|
407
|
-
[ {
|
408
|
-
body: headers.delete(:body),
|
409
|
-
content_type: headers[:content_type] || "text/plain"
|
410
|
-
} ]
|
411
|
-
end
|
412
|
-
|
413
|
-
def collect_responses_from_templates(headers)
|
414
|
-
templates_path = headers[:template_path] || self.class.agent_name
|
415
|
-
templates_name = headers[:template_name] || action_name
|
416
|
-
|
417
|
-
each_template(Array(templates_path), templates_name).map do |template|
|
418
|
-
next if template.format == :json
|
419
|
-
|
420
|
-
format = template.format || formats.first
|
421
|
-
{
|
422
|
-
body: render(template: template, formats: [ format ]),
|
423
|
-
content_type: Mime[format].to_s
|
424
|
-
}
|
425
|
-
end.compact
|
426
|
-
end
|
427
|
-
|
428
|
-
def each_template(paths, name, &)
|
429
|
-
templates = lookup_context.find_all(name, paths)
|
430
|
-
if templates.empty?
|
431
|
-
raise ActionView::MissingTemplate.new(paths, name, paths, false, "agent")
|
432
|
-
else
|
433
|
-
templates.uniq(&:format).each(&)
|
434
|
-
end
|
435
|
-
end
|
436
|
-
|
437
|
-
def create_parts_from_responses(prompt_context, responses)
|
438
|
-
if responses.size > 1
|
439
|
-
# prompt_container = ActiveAgent::ActionPrompt::Prompt.new
|
440
|
-
# prompt_container.content_type = "multipart/alternative"
|
441
|
-
responses.each { |r| insert_part(prompt_context, r, prompt_context.charset) }
|
442
|
-
# prompt_context.add_part(prompt_container)
|
443
|
-
else
|
444
|
-
responses.each { |r| insert_part(prompt_context, r, prompt_context.charset) }
|
445
|
-
end
|
446
|
-
end
|
447
|
-
|
448
|
-
def insert_part(prompt_context, response, charset)
|
449
|
-
message = ActiveAgent::ActionPrompt::Message.new(
|
450
|
-
content: response[:body],
|
451
|
-
content_type: response[:content_type],
|
452
|
-
charset: charset
|
453
|
-
)
|
454
|
-
prompt_context.add_part(message)
|
455
|
-
end
|
456
|
-
|
457
|
-
# This and #instrument_name is for caching instrument
|
458
|
-
def instrument_payload(key)
|
459
|
-
{
|
460
|
-
agent: agent_name,
|
461
|
-
key: key
|
462
|
-
}
|
463
|
-
end
|
464
|
-
|
465
|
-
def instrument_name
|
466
|
-
"active_agent"
|
467
|
-
end
|
468
|
-
|
469
|
-
def _protected_ivars
|
470
|
-
PROTECTED_IVARS
|
471
|
-
end
|
472
|
-
|
473
|
-
ActiveSupport.run_load_hooks(:active_agent, self)
|
474
35
|
end
|
475
36
|
end
|
@@ -6,31 +6,31 @@ module ActiveAgent
|
|
6
6
|
|
7
7
|
included do
|
8
8
|
include ActiveSupport::Callbacks
|
9
|
-
define_callbacks :
|
9
|
+
define_callbacks :generation, skip_after_callbacks_if_terminated: true
|
10
10
|
define_callbacks :stream, skip_after_callbacks_if_terminated: true
|
11
11
|
end
|
12
12
|
|
13
13
|
module ClassMethods
|
14
14
|
# Defines a callback that will get called right before the
|
15
15
|
# prompt is sent to the generation provider method.
|
16
|
-
def
|
17
|
-
set_callback(:
|
16
|
+
def before_generation(*filters, &blk)
|
17
|
+
set_callback(:generation, :before, *filters, &blk)
|
18
18
|
end
|
19
19
|
|
20
20
|
# Defines a callback that will get called right after the
|
21
21
|
# prompt's generation method is finished.
|
22
|
-
def
|
23
|
-
set_callback(:
|
22
|
+
def after_generation(*filters, &blk)
|
23
|
+
set_callback(:generation, :after, *filters, &blk)
|
24
24
|
end
|
25
25
|
|
26
26
|
# Defines a callback that will get called around the prompt's generation method.
|
27
|
-
def
|
28
|
-
set_callback(:
|
27
|
+
def around_generation(*filters, &blk)
|
28
|
+
set_callback(:generation, :around, *filters, &blk)
|
29
29
|
end
|
30
30
|
|
31
31
|
# Defines a callback for handling streaming responses during generation
|
32
|
-
def on_stream(*filters, &)
|
33
|
-
set_callback(:stream, :before, *filters, &)
|
32
|
+
def on_stream(*filters, &blk)
|
33
|
+
set_callback(:stream, :before, *filters, &blk)
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
@@ -36,7 +36,7 @@ module ActiveAgent
|
|
36
36
|
|
37
37
|
def generate_now!
|
38
38
|
processed_agent.handle_exceptions do
|
39
|
-
processed_agent.run_callbacks(:
|
39
|
+
processed_agent.run_callbacks(:generation) do
|
40
40
|
processed_agent.perform_generation!
|
41
41
|
end
|
42
42
|
end
|
@@ -44,7 +44,7 @@ module ActiveAgent
|
|
44
44
|
|
45
45
|
def generate_now
|
46
46
|
processed_agent.handle_exceptions do
|
47
|
-
processed_agent.run_callbacks(:
|
47
|
+
processed_agent.run_callbacks(:generation) do
|
48
48
|
processed_agent.perform_generation
|
49
49
|
end
|
50
50
|
end
|
@@ -70,7 +70,7 @@ module ActiveAgent
|
|
70
70
|
"method*, or 3. use a custom Active Job instead of #generate_later."
|
71
71
|
else
|
72
72
|
@agent_class.generation_job.set(options).perform_later(
|
73
|
-
@agent_class.name, @action.to_s, args: @args
|
73
|
+
@agent_class.name, @action.to_s, generation_method.to_s, args: @args
|
74
74
|
)
|
75
75
|
end
|
76
76
|
end
|
@@ -18,12 +18,14 @@ module ActiveAgent
|
|
18
18
|
|
19
19
|
rescue_from StandardError, with: :handle_exception_with_agent_class
|
20
20
|
|
21
|
-
def perform(
|
22
|
-
agent_class =
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
def perform(agent, agent_method, generation_method, args:, kwargs: nil, params: nil)
|
22
|
+
agent_class = params ? agent.constantize.with(params) : agent.constantize
|
23
|
+
prompt = if kwargs
|
24
|
+
agent_class.public_send(agent_method, *args, **kwargs)
|
25
|
+
else
|
26
|
+
agent_class.public_send(agent_method, *args)
|
27
|
+
end
|
28
|
+
prompt.send(generation_method)
|
27
29
|
end
|
28
30
|
|
29
31
|
private
|
@@ -10,7 +10,12 @@ module ActiveAgent
|
|
10
10
|
super
|
11
11
|
@api_key = config["api_key"]
|
12
12
|
@model_name = config["model"] || "gpt-4o-mini"
|
13
|
-
|
13
|
+
|
14
|
+
@client = if (@host = config["host"])
|
15
|
+
OpenAI::Client.new(uri_base: @host, access_token: @api_key)
|
16
|
+
else
|
17
|
+
OpenAI::Client.new(access_token: @api_key)
|
18
|
+
end
|
14
19
|
end
|
15
20
|
|
16
21
|
def generate(prompt)
|
@@ -33,18 +38,20 @@ module ActiveAgent
|
|
33
38
|
|
34
39
|
def provider_stream
|
35
40
|
agent_stream = prompt.options[:stream]
|
41
|
+
|
36
42
|
message = ActiveAgent::ActionPrompt::Message.new(content: "", role: :assistant)
|
37
43
|
|
38
44
|
@response = ActiveAgent::GenerationProvider::Response.new(prompt:, message:)
|
39
45
|
proc do |chunk, bytesize|
|
40
46
|
new_content = chunk.dig("choices", 0, "delta", "content")
|
41
|
-
if new_content && !new_content.blank
|
47
|
+
if new_content && !new_content.blank?
|
48
|
+
message.generation_id = chunk.dig("id")
|
42
49
|
message.content += new_content
|
43
50
|
|
44
51
|
agent_stream.call(message, new_content, false) do |message, new_content|
|
45
52
|
yield message, new_content if block_given?
|
46
53
|
end
|
47
|
-
elsif chunk.dig("choices", 0, "delta", "tool_calls") &&
|
54
|
+
elsif chunk.dig("choices", 0, "delta", "tool_calls") && chunk.dig("choices", 0, "delta", "role")
|
48
55
|
message = handle_message(chunk.dig("choices", 0, "delta"))
|
49
56
|
prompt.messages << message
|
50
57
|
@response = ActiveAgent::GenerationProvider::Response.new(prompt:, message:)
|
@@ -70,13 +77,17 @@ module ActiveAgent
|
|
70
77
|
provider_message = {
|
71
78
|
role: message.role,
|
72
79
|
tool_call_id: message.action_id.presence,
|
80
|
+
name: message.action_name.presence,
|
81
|
+
tool_calls: message.raw_actions.present? ? message.raw_actions[:tool_calls] : (message.requested_actions.map { |action| { type: "function", name: action.name, arguments: action.params.to_json } } if message.action_requested),
|
82
|
+
generation_id: message.generation_id,
|
73
83
|
content: message.content,
|
74
84
|
type: message.content_type,
|
75
85
|
charset: message.charset
|
76
86
|
}.compact
|
77
87
|
|
78
|
-
if message.content_type == "image_url"
|
79
|
-
provider_message[:
|
88
|
+
if message.content_type == "image_url" || message.content[0..4] == "data:"
|
89
|
+
provider_message[:type] = "image_url"
|
90
|
+
provider_message[:image_url] = { url: message.content }
|
80
91
|
end
|
81
92
|
provider_message
|
82
93
|
end
|
@@ -84,9 +95,8 @@ module ActiveAgent
|
|
84
95
|
|
85
96
|
def chat_response(response)
|
86
97
|
return @response if prompt.options[:stream]
|
87
|
-
|
88
98
|
message_json = response.dig("choices", 0, "message")
|
89
|
-
|
99
|
+
message_json["id"] = response.dig("id") if message_json["id"].blank?
|
90
100
|
message = handle_message(message_json)
|
91
101
|
|
92
102
|
update_context(prompt: prompt, message: message, response: response)
|
@@ -96,9 +106,11 @@ module ActiveAgent
|
|
96
106
|
|
97
107
|
def handle_message(message_json)
|
98
108
|
ActiveAgent::ActionPrompt::Message.new(
|
109
|
+
generation_id: message_json["id"],
|
99
110
|
content: message_json["content"],
|
100
111
|
role: message_json["role"].intern,
|
101
112
|
action_requested: message_json["finish_reason"] == "tool_calls",
|
113
|
+
raw_actions: message_json["tool_calls"] || [],
|
102
114
|
requested_actions: handle_actions(message_json["tool_calls"])
|
103
115
|
)
|
104
116
|
end
|
@@ -108,7 +120,7 @@ module ActiveAgent
|
|
108
120
|
|
109
121
|
tool_calls.map do |tool_call|
|
110
122
|
next if tool_call["function"].nil? || tool_call["function"]["name"].blank?
|
111
|
-
args = tool_call["function"]["arguments"].blank? ? nil : JSON.parse(tool_call["function"]["arguments"], {symbolize_names: true})
|
123
|
+
args = tool_call["function"]["arguments"].blank? ? nil : JSON.parse(tool_call["function"]["arguments"], { symbolize_names: true })
|
112
124
|
|
113
125
|
ActiveAgent::ActionPrompt::Action.new(
|
114
126
|
id: tool_call["id"],
|
@@ -57,7 +57,7 @@ module ActiveAgent
|
|
57
57
|
super
|
58
58
|
else
|
59
59
|
@agent_class.generation_job.set(options).perform_later(
|
60
|
-
@agent_class.name, @action.to_s, params: @params, args: @args
|
60
|
+
@agent_class.name, @action.to_s, generation_method.to_s, params: @params, args: @args
|
61
61
|
)
|
62
62
|
end
|
63
63
|
end
|
data/lib/active_agent/version.rb
CHANGED
data/lib/active_agent.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require "yaml"
|
2
2
|
require "abstract_controller"
|
3
|
-
require "active_agent/action_prompt"
|
4
3
|
require "active_agent/generation_provider"
|
5
4
|
require "active_agent/version"
|
6
5
|
require "active_agent/deprecator"
|
@@ -49,7 +48,7 @@ module ActiveAgent
|
|
49
48
|
|
50
49
|
def load_configuration(file)
|
51
50
|
if File.exist?(file)
|
52
|
-
config_file = YAML.load(ERB.new(File.read(file)).result)
|
51
|
+
config_file = YAML.load(ERB.new(File.read(file)).result, aliases: true)
|
53
52
|
env = ENV["RAILS_ENV"] || ENV["ENV"] || "development"
|
54
53
|
@config = config_file[env] || config_file
|
55
54
|
end
|
@@ -3,9 +3,5 @@ class ApplicationAgent < ActiveAgent::Base
|
|
3
3
|
layout 'agent'
|
4
4
|
|
5
5
|
generate_with :openai, model: "gpt-4o-mini", instructions: "You are a helpful assistant."
|
6
|
-
|
7
|
-
def text_prompt
|
8
|
-
prompt { |format| format.text { render plain: params[:message] } }
|
9
|
-
end
|
10
6
|
end
|
11
7
|
<% end %>
|