activeagent 0.3.3 → 0.4.1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_agent/action_prompt/base.rb +470 -0
  3. data/lib/active_agent/action_prompt/message.rb +22 -2
  4. data/lib/active_agent/action_prompt/prompt.rb +2 -2
  5. data/lib/active_agent/action_prompt.rb +84 -0
  6. data/lib/active_agent/base.rb +9 -448
  7. data/lib/active_agent/callbacks.rb +9 -9
  8. data/lib/active_agent/generation.rb +3 -3
  9. data/lib/active_agent/generation_job.rb +8 -6
  10. data/lib/active_agent/generation_provider/open_ai_provider.rb +20 -8
  11. data/lib/active_agent/parameterized.rb +1 -1
  12. data/lib/active_agent/version.rb +1 -1
  13. data/lib/active_agent.rb +1 -2
  14. data/lib/generators/active_agent/agent_generator.rb +5 -3
  15. data/lib/generators/active_agent/install_generator.rb +2 -9
  16. data/lib/generators/active_agent/templates/agent.rb.tt +1 -1
  17. data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -4
  18. data/lib/generators/erb/agent_generator.rb +43 -0
  19. data/lib/generators/erb/install_generator.rb +16 -0
  20. data/lib/generators/erb/templates/application_agent.rb.tt +7 -0
  21. data/lib/generators/erb/templates/layout.html.erb.tt +1 -0
  22. data/lib/generators/erb/templates/layout.text.erb.tt +1 -0
  23. data/lib/generators/erb/templates/view.html.erb.tt +5 -0
  24. data/lib/generators/erb/templates/view.text.erb.tt +3 -0
  25. data/lib/generators/test_unit/agent_generator.rb +30 -0
  26. data/lib/generators/test_unit/install_generator.rb +13 -0
  27. data/lib/generators/test_unit/templates/functional_test.rb.tt +20 -0
  28. data/lib/generators/test_unit/templates/preview.rb.tt +14 -0
  29. metadata +29 -15
  30. data/lib/active_agent/README.md +0 -21
  31. data/lib/active_agent/action_prompt/README.md +0 -0
  32. data/lib/active_agent/generation_provider/README.md +0 -0
  33. data/lib/generators/active_agent/templates/action.html.erb.tt +0 -0
  34. data/lib/generators/active_agent/templates/action.json.jbuilder.tt +0 -14
  35. data/lib/generators/active_agent/templates/agent.html.erb +0 -1
  36. data/lib/generators/active_agent/templates/agent.text.erb +0 -1
  37. data/lib/generators/active_agent/templates/agent_spec.rb.tt +0 -0
  38. data/lib/generators/active_agent/templates/agent_test.rb.tt +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 065b41c8228d87f1c49d6099813e4d67fc8c853f2985bcdff01e8af23d0561d3
4
- data.tar.gz: 820580bb35c3596adfdaee4edb9dc7f54044328f32d92a767d6134a3460875f2
3
+ metadata.gz: c93bac60dcbdb44bfc6461435f84e79499682db83a93ea07526a5b546c2a1b63
4
+ data.tar.gz: c3a7a995862ef152661b1ede6c012e17747c190853cdc501e0a0f99445c7fe40
5
5
  SHA512:
6
- metadata.gz: 46752438d8e2ec5772e6c6a134906410de7363c366c187ce771bc6f13c7c748d23975ea7716e98ff22a76e8bfa46be43513cc6ce97cf504788ea612ac68caaeb
7
- data.tar.gz: 1e8f480da2ac8107114ee9503aa9db582cbd7719e1ceceb809a741a8b0a81b67a038c46ecb75bff31e5ef9b77ce3197c6cc8bfbcb19c9c3779e097d8b3d53411
6
+ metadata.gz: 5e2836e13ca5d353d221a33643c7c101320e2615982dceccf4dfdf25c9e9fe9bbdeb48503aa70d1a21ed1dd8cfdab0ec76348342dbbd29dc5cfb59f9306266f9
7
+ data.tar.gz: edaddc4339c00242935f0ba70fdc95f98547c12ccd6d4b4da9cd4081dd051e3521c6b4bdb5745fe6e7a382e462024b0dc0aec12eacd730b2ebc5f12a81315d35
@@ -0,0 +1,470 @@
1
+ require "active_agent/collector"
2
+ require "active_support/core_ext/string/inflections"
3
+ require "active_support/core_ext/hash/except"
4
+ require "active_support/core_ext/module/anonymous"
5
+
6
+ # require "active_agent/log_subscriber"
7
+ require "active_agent/rescuable"
8
+ module ActiveAgent
9
+ module ActionPrompt
10
+ class Base < AbstractController::Base
11
+ include Callbacks
12
+ include GenerationMethods
13
+ include GenerationProvider
14
+ include QueuedGeneration
15
+ include Rescuable
16
+ include Parameterized
17
+ include Previews
18
+ # include FormBuilder
19
+
20
+ abstract!
21
+
22
+ include AbstractController::Rendering
23
+
24
+ include AbstractController::Logger
25
+ include AbstractController::Helpers
26
+ include AbstractController::Translation
27
+ include AbstractController::AssetPaths
28
+ include AbstractController::Callbacks
29
+ include AbstractController::Caching
30
+
31
+ include ActionView::Layouts
32
+
33
+ PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ :@_action_has_layout ]
34
+
35
+ helper ActiveAgent::PromptHelper
36
+
37
+ class_attribute :options
38
+
39
+ class_attribute :default_params, default: {
40
+ mime_version: "1.0",
41
+ charset: "UTF-8",
42
+ content_type: "text/plain",
43
+ parts_order: [ "text/plain", "text/enriched", "text/html" ]
44
+ }.freeze
45
+
46
+ class << self
47
+ # Register one or more Observers which will be notified when prompt is generated.
48
+ def register_observers(*observers)
49
+ observers.flatten.compact.each { |observer| register_observer(observer) }
50
+ end
51
+
52
+ # Unregister one or more previously registered Observers.
53
+ def unregister_observers(*observers)
54
+ observers.flatten.compact.each { |observer| unregister_observer(observer) }
55
+ end
56
+
57
+ # Register one or more Interceptors which will be called before prompt is sent.
58
+ def register_interceptors(*interceptors)
59
+ interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
60
+ end
61
+
62
+ # Unregister one or more previously registered Interceptors.
63
+ def unregister_interceptors(*interceptors)
64
+ interceptors.flatten.compact.each { |interceptor| unregister_interceptor(interceptor) }
65
+ end
66
+
67
+ # Register an Observer which will be notified when prompt is generated.
68
+ # Either a class, string, or symbol can be passed in as the Observer.
69
+ # If a string or symbol is passed in it will be camelized and constantized.
70
+ def register_observer(observer)
71
+ Prompt.register_observer(observer_class_for(observer))
72
+ end
73
+
74
+ # Unregister a previously registered Observer.
75
+ # Either a class, string, or symbol can be passed in as the Observer.
76
+ # If a string or symbol is passed in it will be camelized and constantized.
77
+ def unregister_observer(observer)
78
+ Prompt.unregister_observer(observer_class_for(observer))
79
+ end
80
+
81
+ # Register an Interceptor which will be called before prompt is sent.
82
+ # Either a class, string, or symbol can be passed in as the Interceptor.
83
+ # If a string or symbol is passed in it will be camelized and constantized.
84
+ def register_interceptor(interceptor)
85
+ Prompt.register_interceptor(observer_class_for(interceptor))
86
+ end
87
+
88
+ # Unregister a previously registered Interceptor.
89
+ # Either a class, string, or symbol can be passed in as the Interceptor.
90
+ # If a string or symbol is passed in it will be camelized and constantized.
91
+ def unregister_interceptor(interceptor)
92
+ Prompt.unregister_interceptor(observer_class_for(interceptor))
93
+ end
94
+
95
+ def observer_class_for(value) # :nodoc:
96
+ case value
97
+ when String, Symbol
98
+ value.to_s.camelize.constantize
99
+ else
100
+ value
101
+ end
102
+ end
103
+ private :observer_class_for
104
+
105
+ # Define how the agent should generate content
106
+ def generate_with(provider, **options)
107
+ self.generation_provider = provider
108
+ self.options = (options || {}).merge(options)
109
+ self.options[:stream] = new.agent_stream if self.options[:stream]
110
+ generation_provider.config.merge!(self.options)
111
+ end
112
+
113
+ def stream_with(&stream)
114
+ self.options = (options || {}).merge(stream: stream)
115
+ end
116
+
117
+ # Returns the name of the current agent. This method is also being used as a path for a view lookup.
118
+ # If this is an anonymous agent, this method will return +anonymous+ instead.
119
+ def agent_name
120
+ @agent_name ||= anonymous? ? "anonymous" : name.underscore
121
+ end
122
+ # Allows to set the name of current agent.
123
+ attr_writer :agent_name
124
+ alias_method :controller_path, :agent_name
125
+
126
+ # Sets the defaults through app configuration:
127
+ #
128
+ # config.action_agent.default(from: "no-reply@example.org")
129
+ #
130
+ # Aliased by ::default_options=
131
+ def default(value = nil)
132
+ self.default_params = default_params.merge(value).freeze if value
133
+ default_params
134
+ end
135
+ # Allows to set defaults through app configuration:
136
+ #
137
+ # config.action_agent.default_options = { from: "no-reply@example.org" }
138
+ alias_method :default_options=, :default
139
+
140
+ # Wraps a prompt generation inside of ActiveSupport::Notifications instrumentation.
141
+ #
142
+ # This method is actually called by the +ActionPrompt::Prompt+ object itself
143
+ # through a callback when you call <tt>:generate_prompt</tt> on the +ActionPrompt::Prompt+,
144
+ # calling +generate_prompt+ directly and passing an +ActionPrompt::Prompt+ will do
145
+ # nothing except tell the logger you generated the prompt.
146
+ def generate_prompt(prompt) # :nodoc:
147
+ ActiveSupport::Notifications.instrument("deliver.active_agent") do |payload|
148
+ set_payload_for_prompt(payload, prompt)
149
+ yield # Let Prompt do the generation actions
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ def set_payload_for_prompt(payload, prompt)
156
+ payload[:prompt] = prompt.encoded
157
+ payload[:agent] = agent_name
158
+ payload[:message_id] = prompt.message_id
159
+ payload[:date] = prompt.date
160
+ payload[:perform_generations] = prompt.perform_generations
161
+ end
162
+
163
+ def method_missing(method_name, ...)
164
+ if action_methods.include?(method_name.name)
165
+ Generation.new(self, method_name, ...)
166
+ else
167
+ super
168
+ end
169
+ end
170
+
171
+ def respond_to_missing?(method, include_all = false)
172
+ action_methods.include?(method.name) || super
173
+ end
174
+ end
175
+
176
+ attr_internal :context
177
+
178
+ def agent_stream
179
+ proc do |message, delta, stop|
180
+ run_stream_callbacks(message, delta, stop) do |message, delta, stop|
181
+ yield message, delta, stop if block_given?
182
+ end
183
+ end
184
+ end
185
+
186
+ def embed
187
+ context.options.merge(options)
188
+ generation_provider.embed(context) if context && generation_provider
189
+ handle_response(generation_provider.response)
190
+ end
191
+
192
+ # Add embedding capability to Message class
193
+ ActiveAgent::ActionPrompt::Message.class_eval do
194
+ def embed
195
+ agent_class = ActiveAgent::Base.descendants.first
196
+ agent = agent_class.new
197
+ agent.context = ActiveAgent::ActionPrompt::Prompt.new(message: self)
198
+ agent.embed
199
+ self
200
+ end
201
+ end
202
+
203
+ # Make context accessible for chaining
204
+ # attr_accessor :context
205
+
206
+ def perform_generation
207
+ context.options.merge(options)
208
+ if (action_methods - ActiveAgent::Base.descendants.first.action_methods).include? action_name
209
+ context.message = context.messages.last
210
+ context.actions = []
211
+ end
212
+ generation_provider.generate(context) if context && generation_provider
213
+ handle_response(generation_provider.response)
214
+ end
215
+
216
+ def handle_response(response)
217
+ return response unless response.message.requested_actions.present?
218
+ perform_actions(requested_actions: response.message.requested_actions)
219
+ update_context(response)
220
+ end
221
+
222
+ def update_context(response)
223
+ context.message = context.messages.last
224
+ ActiveAgent::GenerationProvider::Response.new(prompt: context)
225
+ end
226
+
227
+ def perform_actions(requested_actions:)
228
+ requested_actions.each do |action|
229
+ perform_action(action)
230
+ end
231
+ end
232
+
233
+ def perform_action(action)
234
+ current_context = context.clone
235
+ process(action.name, *action.params)
236
+ context.messages.last.role = :tool
237
+ context.messages.last.action_id = action.id
238
+ context.messages.last.action_name = action.name
239
+ context.messages.last.generation_id = action.id
240
+ current_context.messages << context.messages.last
241
+ self.context = current_context
242
+ end
243
+
244
+ def initialize
245
+ super
246
+ @_prompt_was_called = false
247
+ @_context = ActiveAgent::ActionPrompt::Prompt.new(instructions: options[:instructions], options: options)
248
+ end
249
+
250
+ def process(method_name, *args) # :nodoc:
251
+ payload = {
252
+ agent: self.class.name,
253
+ action: method_name,
254
+ args: args
255
+ }
256
+
257
+ ActiveSupport::Notifications.instrument("process.active_agent", payload) do
258
+ super
259
+ @_context = ActiveAgent::ActionPrompt::Prompt.new unless @_prompt_was_called
260
+ end
261
+ end
262
+ ruby2_keywords(:process)
263
+
264
+ class NullPrompt # :nodoc:
265
+ def message
266
+ ""
267
+ end
268
+
269
+ def header
270
+ {}
271
+ end
272
+
273
+ def respond_to?(string, include_all = false)
274
+ true
275
+ end
276
+
277
+ def method_missing(...)
278
+ nil
279
+ end
280
+ end
281
+
282
+ # Returns the name of the agent object.
283
+ def agent_name
284
+ self.class.agent_name
285
+ end
286
+
287
+ def headers(args = nil)
288
+ if args
289
+ @_context.headers(args)
290
+ else
291
+ @_context
292
+ end
293
+ end
294
+
295
+ def prompt_with(*)
296
+ context.update_context(*)
297
+ end
298
+
299
+ def prompt(headers = {}, &block)
300
+ return context if @_prompt_was_called && headers.blank? && !block
301
+ content_type = headers[:content_type]
302
+ headers = apply_defaults(headers)
303
+ context.messages = headers[:messages] || []
304
+ context.context_id = headers[:context_id]
305
+
306
+ context.charset = charset = headers[:charset]
307
+
308
+ if headers[:message].present? && headers[:message].is_a?(ActiveAgent::ActionPrompt::Message)
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
315
+
316
+ # wrap_generation_behavior!(headers[:generation_method], headers[:generation_method_options])
317
+ # assign_headers_to_context(context, headers)
318
+ responses = collect_responses(headers, &block)
319
+
320
+ @_prompt_was_called = true
321
+
322
+ create_parts_from_responses(context, responses)
323
+
324
+ context.content_type = set_content_type(context, content_type, headers[:content_type])
325
+ context.charset = charset
326
+ context.actions = headers[:actions] || action_schemas
327
+
328
+ context
329
+ end
330
+
331
+ def action_methods
332
+ super - ActiveAgent::Base.public_instance_methods(false).map(&:to_s)
333
+ end
334
+
335
+ def action_schemas
336
+ action_methods.map do |action|
337
+ JSON.parse render_to_string(locals: { action_name: action }, action: action, formats: :json)
338
+ end.compact
339
+ end
340
+
341
+ private
342
+
343
+ def set_content_type(m, user_content_type, class_default) # :doc:
344
+ if user_content_type.present?
345
+ user_content_type
346
+ else
347
+ context.content_type || class_default
348
+ end
349
+ end
350
+
351
+ # Translates the +subject+ using \Rails I18n class under <tt>[agent_scope, action_name]</tt> scope.
352
+ # If it does not find a translation for the +subject+ under the specified scope it will default to a
353
+ # humanized version of the <tt>action_name</tt>.
354
+ # If the subject has interpolations, you can pass them through the +interpolations+ parameter.
355
+ def default_i18n_subject(interpolations = {}) # :doc:
356
+ agent_scope = self.class.agent_name.tr("/", ".")
357
+ I18n.t(:subject, **interpolations.merge(scope: [ agent_scope, action_name ], default: action_name.humanize))
358
+ end
359
+
360
+ def apply_defaults(headers)
361
+ default_values = self.class.default.except(*headers.keys).transform_values do |value|
362
+ compute_default(value)
363
+ end
364
+
365
+ headers.reverse_merge(default_values)
366
+ end
367
+
368
+ def compute_default(value)
369
+ return value unless value.is_a?(Proc)
370
+
371
+ if value.arity == 1
372
+ instance_exec(self, &value)
373
+ else
374
+ instance_exec(&value)
375
+ end
376
+ end
377
+
378
+ def assign_headers_to_context(context, headers)
379
+ assignable = headers.except(:parts_order, :content_type, :body, :role, :template_name,
380
+ :template_path, :delivery_method, :delivery_method_options)
381
+
382
+ assignable.each { |k, v| context.send(k, v) if context.respond_to?(k) }
383
+ end
384
+
385
+ def collect_responses(headers, &)
386
+ if block_given?
387
+ collect_responses_from_block(headers, &)
388
+ elsif headers[:body]
389
+ collect_responses_from_text(headers)
390
+ else
391
+ collect_responses_from_templates(headers)
392
+ end
393
+ end
394
+
395
+ def collect_responses_from_block(headers)
396
+ templates_name = headers[:template_name] || action_name
397
+ collector = ActiveAgent::Collector.new(lookup_context) { render(templates_name) }
398
+ yield(collector)
399
+ collector.responses
400
+ end
401
+
402
+ def collect_responses_from_text(headers)
403
+ [ {
404
+ body: headers.delete(:body),
405
+ content_type: headers[:content_type] || "text/plain"
406
+ } ]
407
+ end
408
+
409
+ def collect_responses_from_templates(headers)
410
+ templates_path = headers[:template_path] || self.class.agent_name
411
+ templates_name = headers[:template_name] || action_name
412
+
413
+ each_template(Array(templates_path), templates_name).map do |template|
414
+ format = template.format || formats.first
415
+ {
416
+ body: render(template: template, formats: [ format ]),
417
+ content_type: Mime[format].to_s
418
+ }
419
+ end.compact
420
+ end
421
+
422
+ def each_template(paths, name, &)
423
+ templates = lookup_context.find_all(name, paths)
424
+ if templates.empty?
425
+ raise ActionView::MissingTemplate.new(paths, name, paths, false, "agent")
426
+ else
427
+ templates.uniq(&:format).each(&)
428
+ end
429
+ end
430
+
431
+ def create_parts_from_responses(context, responses)
432
+ if responses.size > 1
433
+ # prompt_container = ActiveAgent::ActionPrompt::Prompt.new
434
+ # prompt_container.content_type = "multipart/alternative"
435
+ responses.each { |r| insert_part(context, r, context.charset) }
436
+ # context.add_part(prompt_container)
437
+ else
438
+ responses.each { |r| insert_part(context, r, context.charset) }
439
+ end
440
+ end
441
+
442
+ def insert_part(context, response, charset)
443
+ message = ActiveAgent::ActionPrompt::Message.new(
444
+ content: response[:body],
445
+ content_type: response[:content_type],
446
+ charset: charset
447
+ )
448
+ context.add_part(message)
449
+ end
450
+
451
+ # This and #instrument_name is for caching instrument
452
+ def instrument_payload(key)
453
+ {
454
+ agent: agent_name,
455
+ key: key
456
+ }
457
+ end
458
+
459
+ def instrument_name
460
+ "active_agent"
461
+ end
462
+
463
+ def _protected_ivars
464
+ PROTECTED_IVARS
465
+ end
466
+
467
+ ActiveSupport.run_load_hooks(:active_agent, self)
468
+ end
469
+ end
470
+ end
@@ -1,16 +1,34 @@
1
1
  module ActiveAgent
2
2
  module ActionPrompt
3
3
  class Message
4
- VALID_ROLES = %w[system assistant user tool function].freeze
4
+ class << self
5
+ def from_messages(messages)
6
+ return messages if messages.empty? || messages.first.is_a?(Message)
5
7
 
6
- attr_accessor :action_id, :content, :role, :action_requested, :requested_actions, :content_type, :charset
8
+ messages.map do |message|
9
+ if message.is_a?(Hash)
10
+ new(message)
11
+ elsif message.is_a?(Message)
12
+ message
13
+ else
14
+ raise ArgumentError, "Messages must be Hash or Message objects"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ VALID_ROLES = %w[system assistant user tool].freeze
20
+
21
+ attr_accessor :action_id, :action_name, :raw_actions, :generation_id, :content, :role, :action_requested, :requested_actions, :content_type, :charset
7
22
 
8
23
  def initialize(attributes = {})
9
24
  @action_id = attributes[:action_id]
25
+ @action_name = attributes[:action_name]
26
+ @generation_id = attributes[:generation_id]
10
27
  @charset = attributes[:charset] || "UTF-8"
11
28
  @content = attributes[:content] || ""
12
29
  @content_type = attributes[:content_type] || "text/plain"
13
30
  @role = attributes[:role] || :user
31
+ @raw_actions = attributes[:raw_actions]
14
32
  @requested_actions = attributes[:requested_actions] || []
15
33
  @action_requested = @requested_actions.any?
16
34
  validate_role
@@ -24,6 +42,8 @@ module ActiveAgent
24
42
  hash = {
25
43
  role: role,
26
44
  action_id: action_id,
45
+ name: action_name,
46
+ generation_id: generation_id,
27
47
  content: content,
28
48
  type: content_type,
29
49
  charset: charset
@@ -23,7 +23,7 @@ module ActiveAgent
23
23
  @context_id = attributes.fetch(:context_id, nil)
24
24
  @headers = attributes.fetch(:headers, {})
25
25
  @parts = attributes.fetch(:parts, [])
26
-
26
+ @messages = Message.from_messages(@messages)
27
27
  set_message if attributes[:message].is_a?(String) || @body.is_a?(String) && @message&.content
28
28
  set_messages
29
29
  end
@@ -68,7 +68,7 @@ module ActiveAgent
68
68
  private
69
69
 
70
70
  def set_messages
71
- @messages = [ Message.new(content: @instructions, role: :system) ] + @messages if @instructions.present?
71
+ @messages = [ Message.new(content: @instructions, role: :system) ] + Message.from_messages(@messages) if @instructions.present?
72
72
  end
73
73
 
74
74
  def set_message
@@ -34,6 +34,90 @@ module ActiveAgent
34
34
  # }.freeze
35
35
  end
36
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
+
37
121
  class TestAgent
38
122
  class << self
39
123
  attr_accessor :generations