activeagent 0.0.0 → 0.1.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/README.md +153 -0
- data/Rakefile +3 -0
- data/lib/active_agent/README.md +21 -0
- data/lib/active_agent/action_prompt/README.md +44 -0
- data/lib/active_agent/action_prompt/base.rb +0 -0
- data/lib/active_agent/action_prompt/collector.rb +34 -0
- data/lib/active_agent/action_prompt/message.rb +44 -0
- data/lib/active_agent/action_prompt/prompt.rb +79 -0
- data/lib/active_agent/action_prompt.rb +127 -0
- data/lib/active_agent/base.rb +439 -0
- data/lib/active_agent/callbacks.rb +31 -0
- data/lib/active_agent/deprecator.rb +7 -0
- data/lib/active_agent/engine.rb +14 -0
- data/lib/active_agent/generation.rb +78 -0
- data/lib/active_agent/generation_job.rb +47 -0
- data/lib/active_agent/generation_methods.rb +60 -0
- data/lib/active_agent/generation_provider/README.md +17 -0
- data/lib/active_agent/generation_provider/base.rb +36 -0
- data/lib/active_agent/generation_provider/open_ai_provider.rb +68 -0
- data/lib/active_agent/generation_provider/response.rb +15 -0
- data/lib/active_agent/generation_provider.rb +63 -0
- data/lib/active_agent/inline_preview_interceptor.rb +60 -0
- data/lib/active_agent/log_subscriber.rb +44 -0
- data/lib/active_agent/operation.rb +13 -0
- data/lib/active_agent/parameterized.rb +66 -0
- data/lib/active_agent/preview.rb +133 -0
- data/lib/active_agent/prompt_helper.rb +19 -0
- data/lib/active_agent/queued_generation.rb +12 -0
- data/lib/active_agent/railtie.rb +89 -0
- data/lib/active_agent/rescuable.rb +34 -0
- data/lib/active_agent/service.rb +25 -0
- data/lib/active_agent/test_case.rb +125 -0
- data/lib/active_agent/version.rb +3 -0
- data/lib/active_agent.rb +63 -0
- data/lib/generators/active_agent/USAGE +18 -0
- data/lib/generators/active_agent/agent_generator.rb +63 -0
- data/lib/generators/active_agent/templates/action.html.erb.tt +0 -0
- data/lib/generators/active_agent/templates/action.json.jbuilder.tt +33 -0
- data/lib/generators/active_agent/templates/agent.rb.tt +14 -0
- data/lib/generators/active_agent/templates/agent_spec.rb.tt +0 -0
- data/lib/generators/active_agent/templates/agent_test.rb.tt +0 -0
- data/lib/generators/active_agent/templates/application_agent.rb.tt +4 -0
- metadata +129 -4
@@ -0,0 +1,439 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_agent/prompt_helper"
|
4
|
+
require "active_agent/action_prompt/prompt"
|
5
|
+
require "active_agent/action_prompt/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"
|
12
|
+
|
13
|
+
# The ActiveAgent module provides a framework for creating agents that can generate content
|
14
|
+
# and handle various actions. The Base class within this module extends AbstractController::Base
|
15
|
+
# and includes several modules to provide additional functionality such as callbacks, generation
|
16
|
+
# methods, and rescuable actions.
|
17
|
+
#
|
18
|
+
# The Base class defines several class methods for registering and unregistering observers and
|
19
|
+
# interceptors, as well as methods for generating content with a specified provider and streaming
|
20
|
+
# content. It also provides methods for setting default parameters and handling prompts.
|
21
|
+
#
|
22
|
+
# The instance methods in the Base class include methods for performing generation, processing
|
23
|
+
# actions, and handling headers and attachments. The class also defines a NullPrompt class for
|
24
|
+
# handling cases where no prompt is provided.
|
25
|
+
#
|
26
|
+
# The Base class uses ActiveSupport::Notifications for instrumentation and provides several
|
27
|
+
# private methods for setting payloads, applying defaults, and collecting responses from blocks,
|
28
|
+
# text, or templates.
|
29
|
+
#
|
30
|
+
# The class also includes several protected instance variables and defines hooks for loading
|
31
|
+
# additional functionality.
|
32
|
+
module ActiveAgent
|
33
|
+
class Base < AbstractController::Base
|
34
|
+
include Callbacks
|
35
|
+
include GenerationMethods
|
36
|
+
include GenerationProvider
|
37
|
+
include QueuedGeneration
|
38
|
+
include Rescuable
|
39
|
+
include Parameterized
|
40
|
+
include Previews
|
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
|
+
def prompt(...)
|
71
|
+
new.prompt(...)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Register one or more Observers which will be notified when mail is delivered.
|
75
|
+
def register_observers(*observers)
|
76
|
+
observers.flatten.compact.each { |observer| register_observer(observer) }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Unregister one or more previously registered Observers.
|
80
|
+
def unregister_observers(*observers)
|
81
|
+
observers.flatten.compact.each { |observer| unregister_observer(observer) }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Register one or more Interceptors which will be called before mail is sent.
|
85
|
+
def register_interceptors(*interceptors)
|
86
|
+
interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Unregister one or more previously registered Interceptors.
|
90
|
+
def unregister_interceptors(*interceptors)
|
91
|
+
interceptors.flatten.compact.each { |interceptor| unregister_interceptor(interceptor) }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Register an Observer which will be notified when mail is delivered.
|
95
|
+
# Either a class, string, or symbol can be passed in as the Observer.
|
96
|
+
# If a string or symbol is passed in it will be camelized and constantized.
|
97
|
+
def register_observer(observer)
|
98
|
+
Mail.register_observer(observer_class_for(observer))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Unregister a previously registered Observer.
|
102
|
+
# Either a class, string, or symbol can be passed in as the Observer.
|
103
|
+
# If a string or symbol is passed in it will be camelized and constantized.
|
104
|
+
def unregister_observer(observer)
|
105
|
+
Mail.unregister_observer(observer_class_for(observer))
|
106
|
+
end
|
107
|
+
|
108
|
+
# Register an Interceptor which will be called before mail is sent.
|
109
|
+
# Either a class, string, or symbol can be passed in as the Interceptor.
|
110
|
+
# If a string or symbol is passed in it will be camelized and constantized.
|
111
|
+
def register_interceptor(interceptor)
|
112
|
+
Mail.register_interceptor(observer_class_for(interceptor))
|
113
|
+
end
|
114
|
+
|
115
|
+
# Unregister a previously registered Interceptor.
|
116
|
+
# Either a class, string, or symbol can be passed in as the Interceptor.
|
117
|
+
# If a string or symbol is passed in it will be camelized and constantized.
|
118
|
+
def unregister_interceptor(interceptor)
|
119
|
+
Mail.unregister_interceptor(observer_class_for(interceptor))
|
120
|
+
end
|
121
|
+
|
122
|
+
def observer_class_for(value) # :nodoc:
|
123
|
+
case value
|
124
|
+
when String, Symbol
|
125
|
+
value.to_s.camelize.constantize
|
126
|
+
else
|
127
|
+
value
|
128
|
+
end
|
129
|
+
end
|
130
|
+
private :observer_class_for
|
131
|
+
|
132
|
+
# Define how the agent should generate content
|
133
|
+
def generate_with(provider, **options)
|
134
|
+
self.generation_provider = provider
|
135
|
+
self.options = (options || {}).merge(options)
|
136
|
+
end
|
137
|
+
|
138
|
+
def stream_with(&stream)
|
139
|
+
self.options = (options || {}).merge(stream: stream)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns the name of the current agent. This method is also being used as a path for a view lookup.
|
143
|
+
# If this is an anonymous agent, this method will return +anonymous+ instead.
|
144
|
+
def agent_name
|
145
|
+
@agent_name ||= anonymous? ? "anonymous" : name.underscore
|
146
|
+
end
|
147
|
+
# Allows to set the name of current agent.
|
148
|
+
attr_writer :agent_name
|
149
|
+
alias_method :controller_path, :agent_name
|
150
|
+
|
151
|
+
# Sets the defaults through app configuration:
|
152
|
+
#
|
153
|
+
# config.action_agent.default(from: "no-reply@example.org")
|
154
|
+
#
|
155
|
+
# Aliased by ::default_options=
|
156
|
+
def default(value = nil)
|
157
|
+
self.default_params = default_params.merge(value).freeze if value
|
158
|
+
default_params
|
159
|
+
end
|
160
|
+
# Allows to set defaults through app configuration:
|
161
|
+
#
|
162
|
+
# config.action_agent.default_options = { from: "no-reply@example.org" }
|
163
|
+
alias_method :default_options=, :default
|
164
|
+
|
165
|
+
# Wraps a prompt generation inside of ActiveSupport::Notifications instrumentation.
|
166
|
+
#
|
167
|
+
# This method is actually called by the +ActionPrompt::Prompt+ object itself
|
168
|
+
# through a callback when you call <tt>:generate_prompt</tt> on the +ActionPrompt::Prompt+,
|
169
|
+
# calling +generate_prompt+ directly and passing an +ActionPrompt::Prompt+ will do
|
170
|
+
# nothing except tell the logger you generated the prompt.
|
171
|
+
def generate_prompt(prompt) # :nodoc:
|
172
|
+
ActiveSupport::Notifications.instrument("deliver.active_agent") do |payload|
|
173
|
+
set_payload_for_prompt(payload, prompt)
|
174
|
+
yield # Let Prompt do the generation actions
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def set_payload_for_prompt(payload, prompt)
|
181
|
+
payload[:prompt] = prompt.encoded
|
182
|
+
payload[:agent] = name
|
183
|
+
payload[:message_id] = prompt.message_id
|
184
|
+
payload[:subject] = prompt.subject
|
185
|
+
payload[:to] = prompt.to
|
186
|
+
payload[:from] = prompt.from
|
187
|
+
payload[:bcc] = prompt.bcc if prompt.bcc.present?
|
188
|
+
payload[:cc] = prompt.cc if prompt.cc.present?
|
189
|
+
payload[:date] = prompt.date
|
190
|
+
payload[:perform_generations] = prompt.perform_generations
|
191
|
+
end
|
192
|
+
|
193
|
+
def method_missing(method_name, ...)
|
194
|
+
if action_methods.include?(method_name.name)
|
195
|
+
Generation.new(self, method_name, ...)
|
196
|
+
else
|
197
|
+
super
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def respond_to_missing?(method, include_all = false)
|
202
|
+
action_methods.include?(method.name) || super
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
attr_internal :context
|
207
|
+
|
208
|
+
def perform_generation
|
209
|
+
context.options.merge(options)
|
210
|
+
generation_provider.generate(context) if context && generation_provider
|
211
|
+
end
|
212
|
+
|
213
|
+
def initialize
|
214
|
+
super
|
215
|
+
@_prompt_was_called = false
|
216
|
+
@_context = ActiveAgent::ActionPrompt::Prompt.new(instructions: options[:instructions])
|
217
|
+
end
|
218
|
+
|
219
|
+
def process(method_name, *args) # :nodoc:
|
220
|
+
payload = {
|
221
|
+
agent: self.class.name,
|
222
|
+
action: method_name,
|
223
|
+
args: args
|
224
|
+
}
|
225
|
+
|
226
|
+
ActiveSupport::Notifications.instrument("process.active_agent", payload) do
|
227
|
+
super
|
228
|
+
@_context = ActiveAgent::ActionPrompt::Prompt.new unless @_prompt_was_called
|
229
|
+
end
|
230
|
+
end
|
231
|
+
ruby2_keywords(:process)
|
232
|
+
|
233
|
+
class NullPrompt # :nodoc:
|
234
|
+
def message
|
235
|
+
""
|
236
|
+
end
|
237
|
+
|
238
|
+
def header
|
239
|
+
{}
|
240
|
+
end
|
241
|
+
|
242
|
+
def respond_to?(string, include_all = false)
|
243
|
+
true
|
244
|
+
end
|
245
|
+
|
246
|
+
def method_missing(...)
|
247
|
+
nil
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Returns the name of the agent object.
|
252
|
+
def agent_name
|
253
|
+
self.class.agent_name
|
254
|
+
end
|
255
|
+
|
256
|
+
def headers(args = nil)
|
257
|
+
if args
|
258
|
+
@_context.headers(args)
|
259
|
+
else
|
260
|
+
@_context
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# def attachments
|
265
|
+
# if @_prompt_was_called
|
266
|
+
# LateAttachmentsProxy.new(@_context.attachments)
|
267
|
+
# else
|
268
|
+
# @_context.attachments
|
269
|
+
# end
|
270
|
+
# end
|
271
|
+
|
272
|
+
class LateAttachmentsProxy < SimpleDelegator
|
273
|
+
def inline
|
274
|
+
self
|
275
|
+
end
|
276
|
+
|
277
|
+
def []=(_name, _content)
|
278
|
+
_raise_error
|
279
|
+
end
|
280
|
+
|
281
|
+
private
|
282
|
+
|
283
|
+
def _raise_error
|
284
|
+
raise "Can't add attachments after `prompt` was called.\n" \
|
285
|
+
"Make sure to use `attachments[]=` before calling `prompt`."
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def prompt(headers = {}, &block)
|
290
|
+
return context if @_prompt_was_called && headers.blank? && !block
|
291
|
+
|
292
|
+
content_type = headers[:content_type]
|
293
|
+
|
294
|
+
headers = apply_defaults(headers)
|
295
|
+
|
296
|
+
context.charset = charset = headers[:charset]
|
297
|
+
|
298
|
+
responses = collect_responses(headers, &block)
|
299
|
+
@_prompt_was_called = true
|
300
|
+
|
301
|
+
create_parts_from_responses(context, responses)
|
302
|
+
|
303
|
+
context.content_type = set_content_type(context, content_type, headers[:content_type])
|
304
|
+
context.charset = charset
|
305
|
+
context.actions = headers[:actions] || action_schemas
|
306
|
+
binding.irb
|
307
|
+
context
|
308
|
+
end
|
309
|
+
|
310
|
+
def action_schemas
|
311
|
+
action_methods.map do |action|
|
312
|
+
JSON.parse render_to_string(locals: {action_name: action}, action: action, formats: :json)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
private
|
317
|
+
|
318
|
+
def set_content_type(m, user_content_type, class_default) # :doc:
|
319
|
+
if user_content_type.present?
|
320
|
+
user_content_type
|
321
|
+
else
|
322
|
+
context.content_type || class_default
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Translates the +subject+ using \Rails I18n class under <tt>[agent_scope, action_name]</tt> scope.
|
327
|
+
# If it does not find a translation for the +subject+ under the specified scope it will default to a
|
328
|
+
# humanized version of the <tt>action_name</tt>.
|
329
|
+
# If the subject has interpolations, you can pass them through the +interpolations+ parameter.
|
330
|
+
def default_i18n_subject(interpolations = {}) # :doc:
|
331
|
+
agent_scope = self.class.agent_name.tr("/", ".")
|
332
|
+
I18n.t(:subject, **interpolations.merge(scope: [agent_scope, action_name], default: action_name.humanize))
|
333
|
+
end
|
334
|
+
|
335
|
+
def apply_defaults(headers)
|
336
|
+
default_values = self.class.default.except(*headers.keys).transform_values do |value|
|
337
|
+
compute_default(value)
|
338
|
+
end
|
339
|
+
|
340
|
+
headers.reverse_merge(default_values)
|
341
|
+
end
|
342
|
+
|
343
|
+
def compute_default(value)
|
344
|
+
return value unless value.is_a?(Proc)
|
345
|
+
|
346
|
+
if value.arity == 1
|
347
|
+
instance_exec(self, &value)
|
348
|
+
else
|
349
|
+
instance_exec(&value)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def assign_headers_to_context(context, headers)
|
354
|
+
assignable = headers.except(:parts_order, :content_type, :body, :template_name,
|
355
|
+
:template_path, :delivery_method, :delivery_method_options)
|
356
|
+
assignable.each { |k, v| context[k] = v }
|
357
|
+
end
|
358
|
+
|
359
|
+
def collect_responses(headers, &)
|
360
|
+
if block_given?
|
361
|
+
collect_responses_from_block(headers, &)
|
362
|
+
elsif headers[:body]
|
363
|
+
collect_responses_from_text(headers)
|
364
|
+
else
|
365
|
+
collect_responses_from_templates(headers)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def collect_responses_from_block(headers)
|
370
|
+
templates_name = headers[:template_name] || action_name
|
371
|
+
collector = ActiveAgent::ActionPrompt::Collector.new(lookup_context) { render(templates_name) }
|
372
|
+
yield(collector)
|
373
|
+
collector.responses
|
374
|
+
end
|
375
|
+
|
376
|
+
def collect_responses_from_text(headers)
|
377
|
+
[{
|
378
|
+
body: headers.delete(:body),
|
379
|
+
content_type: headers[:content_type] || "text/plain"
|
380
|
+
}]
|
381
|
+
end
|
382
|
+
|
383
|
+
def collect_responses_from_templates(headers)
|
384
|
+
templates_path = headers[:template_path] || self.class.agent_name
|
385
|
+
templates_name = headers[:template_name] || action_name
|
386
|
+
|
387
|
+
each_template(Array(templates_path), templates_name).map do |template|
|
388
|
+
format = template.format || formats.first
|
389
|
+
{
|
390
|
+
body: render(template: template, formats: [format]),
|
391
|
+
content_type: Mime[format].to_s
|
392
|
+
}
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def each_template(paths, name, &)
|
397
|
+
templates = lookup_context.find_all(name, paths)
|
398
|
+
if templates.empty?
|
399
|
+
raise ActionView::MissingTemplate.new(paths, name, paths, false, "agent")
|
400
|
+
else
|
401
|
+
templates.uniq(&:format).each(&)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def create_parts_from_responses(context, responses)
|
406
|
+
if responses.size > 1 && false
|
407
|
+
prompt_container = ActiveAgent::ActionPrompt::Prompt.new
|
408
|
+
prompt_container.content_type = "multipart/alternative"
|
409
|
+
responses.each { |r| insert_part(context, r, context.charset) }
|
410
|
+
context.add_part(prompt_container)
|
411
|
+
else
|
412
|
+
responses.each { |r| insert_part(context, r, context.charset) }
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def insert_part(container, response, charset)
|
417
|
+
response[:charset] ||= charset
|
418
|
+
container.add_part(response)
|
419
|
+
end
|
420
|
+
|
421
|
+
# This and #instrument_name is for caching instrument
|
422
|
+
def instrument_payload(key)
|
423
|
+
{
|
424
|
+
agent: agent_name,
|
425
|
+
key: key
|
426
|
+
}
|
427
|
+
end
|
428
|
+
|
429
|
+
def instrument_name
|
430
|
+
"active_agent"
|
431
|
+
end
|
432
|
+
|
433
|
+
def _protected_ivars
|
434
|
+
PROTECTED_IVARS
|
435
|
+
end
|
436
|
+
|
437
|
+
ActiveSupport.run_load_hooks(:active_agent, self)
|
438
|
+
end
|
439
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAgent
|
4
|
+
module Callbacks
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include ActiveSupport::Callbacks
|
9
|
+
define_callbacks :generate, skip_after_callbacks_if_terminated: true
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Defines a callback that will get called right before the
|
14
|
+
# prompt is sent to the generation provider method.
|
15
|
+
def before_generate(*filters, &)
|
16
|
+
set_callback(:generate, :before, *filters, &)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Defines a callback that will get called right after the
|
20
|
+
# prompt's generation method is finished.
|
21
|
+
def after_generate(*filters, &)
|
22
|
+
set_callback(:generate, :after, *filters, &)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Defines a callback that will get called around the prompt's generation method.
|
26
|
+
def around_generate(*filters, &)
|
27
|
+
set_callback(:generate, :around, *filters, &)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# require "rails/engine"
|
2
|
+
|
3
|
+
# module ActiveAgent
|
4
|
+
# class Engine < ::Rails::Engine
|
5
|
+
# isolate_namespace ActiveAgent
|
6
|
+
|
7
|
+
# initializer "active_agent.view_paths" do |app|
|
8
|
+
# # Adding your gem's view path to Rails lookup paths
|
9
|
+
# ActiveSupport.on_load(:action_controller) do
|
10
|
+
# append_view_path Engine.root.join("app/views")
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# lib/active_agent/generation.rb
|
2
|
+
require "delegate"
|
3
|
+
|
4
|
+
module ActiveAgent
|
5
|
+
class Generation < Delegator
|
6
|
+
def initialize(agent_class, action, *args)
|
7
|
+
@agent_class, @action, @args = agent_class, action, args
|
8
|
+
@processed_agent = nil
|
9
|
+
@prompt_context = nil
|
10
|
+
end
|
11
|
+
ruby2_keywords(:initialize)
|
12
|
+
|
13
|
+
def __getobj__
|
14
|
+
@prompt_context ||= processed_agent.context
|
15
|
+
end
|
16
|
+
|
17
|
+
def __setobj__(prompt_context)
|
18
|
+
@prompt_context = prompt_context
|
19
|
+
end
|
20
|
+
|
21
|
+
def context
|
22
|
+
__getobj__
|
23
|
+
end
|
24
|
+
|
25
|
+
def processed?
|
26
|
+
@processed_agent || @prompt_context
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_later!(options = {})
|
30
|
+
enqueue_generation :generate_now!, options
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_later(options = {})
|
34
|
+
enqueue_generation :generate_now, options
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_now!
|
38
|
+
processed_agent.handle_exceptions do
|
39
|
+
processed_agent.run_callbacks(:generate) do
|
40
|
+
processed_agent..perform_generation
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_now
|
46
|
+
processed_agent.handle_exceptions do
|
47
|
+
processed_agent.run_callbacks(:generate) do
|
48
|
+
processed_agent.perform_generation
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def processed_agent
|
56
|
+
@processed_agent ||= @agent_class.new.tap do |agent|
|
57
|
+
agent.process(@action, *@args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def enqueue_generation(generation_method, options = {})
|
62
|
+
if processed?
|
63
|
+
::Kernel.raise "You've accessed the context before asking to " \
|
64
|
+
"generate it later, so you may have made local changes that would " \
|
65
|
+
"be silently lost if we enqueued a job to generate it. Why? Only " \
|
66
|
+
"the agent method *arguments* are passed with the generation job! " \
|
67
|
+
"Do not access the context in any way if you mean to generate it " \
|
68
|
+
"later. Workarounds: 1. don't touch the context before calling " \
|
69
|
+
"#generate_later, 2. only touch the context *within your agent " \
|
70
|
+
"method*, or 3. use a custom Active Job instead of #generate_later."
|
71
|
+
else
|
72
|
+
@agent_class.generation_job.set(options).perform_later(
|
73
|
+
@agent_class.name, @action.to_s, args: @args
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_job"
|
4
|
+
|
5
|
+
module ActiveAgent
|
6
|
+
# = Active Agent \GenerationJob
|
7
|
+
#
|
8
|
+
# The +ActiveAgent::GenerationJob+ class is used when you
|
9
|
+
# want to generate content outside of the request-response cycle. It supports
|
10
|
+
# sending messages with parameters.
|
11
|
+
#
|
12
|
+
# Exceptions are rescued and handled by the agent class.
|
13
|
+
class GenerationJob < ActiveJob::Base # :nodoc:
|
14
|
+
queue_as do
|
15
|
+
agent_class = arguments.first.constantize
|
16
|
+
agent_class.generate_later_queue_name
|
17
|
+
end
|
18
|
+
|
19
|
+
rescue_from StandardError, with: :handle_exception_with_agent_class
|
20
|
+
|
21
|
+
def perform(agent_class_name, action_name, args:, params: nil)
|
22
|
+
agent_class = agent_class_name.constantize
|
23
|
+
agent = agent_class.new
|
24
|
+
agent.params = params if params
|
25
|
+
agent.process(action_name, *args)
|
26
|
+
agent.generate
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# "Deserialize" the agent class name by hand in case another argument
|
32
|
+
# (like a Global ID reference) raised DeserializationError.
|
33
|
+
def agent_class
|
34
|
+
if agent = Array(@serialized_arguments).first || Array(arguments).first
|
35
|
+
agent.constantize
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_exception_with_agent_class(exception)
|
40
|
+
if klass = agent_class
|
41
|
+
klass.handle_exception exception
|
42
|
+
else
|
43
|
+
raise exception
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,60 @@
|
|
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
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Active Agent: Generation Provider
|
2
|
+
|
3
|
+
This README provides information about the base generation provider class and the generation provider module interfaces in the ActiveAgent library.
|
4
|
+
|
5
|
+
## Main Components
|
6
|
+
|
7
|
+
Base class - for creating and configuring generation providers.
|
8
|
+
Module - for including in your agent classes to provide generation provider functionality.
|
9
|
+
|
10
|
+
### ActiveAgent::GenerationProvider::Base
|
11
|
+
|
12
|
+
The main class for creating and configuring generation providers. It provides the core functionality for creating and managing generation providers.
|
13
|
+
|
14
|
+
#### Core Methods
|
15
|
+
|
16
|
+
##### initialize(options = {})
|
17
|
+
Creates a new generation provider object with the given options.
|