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.
- checksums.yaml +4 -4
- data/lib/active_agent/action_prompt/base.rb +470 -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/agent_generator.rb +5 -3
- data/lib/generators/active_agent/install_generator.rb +2 -9
- data/lib/generators/active_agent/templates/agent.rb.tt +1 -1
- data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -4
- data/lib/generators/erb/agent_generator.rb +43 -0
- data/lib/generators/erb/install_generator.rb +16 -0
- data/lib/generators/erb/templates/application_agent.rb.tt +7 -0
- data/lib/generators/erb/templates/layout.html.erb.tt +1 -0
- data/lib/generators/erb/templates/layout.text.erb.tt +1 -0
- data/lib/generators/erb/templates/view.html.erb.tt +5 -0
- data/lib/generators/erb/templates/view.text.erb.tt +3 -0
- data/lib/generators/test_unit/agent_generator.rb +30 -0
- data/lib/generators/test_unit/install_generator.rb +13 -0
- data/lib/generators/test_unit/templates/functional_test.rb.tt +20 -0
- data/lib/generators/test_unit/templates/preview.rb.tt +14 -0
- metadata +29 -15
- 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/generators/active_agent/templates/action.html.erb.tt +0 -0
- data/lib/generators/active_agent/templates/action.json.jbuilder.tt +0 -14
- data/lib/generators/active_agent/templates/agent.html.erb +0 -1
- data/lib/generators/active_agent/templates/agent.text.erb +0 -1
- data/lib/generators/active_agent/templates/agent_spec.rb.tt +0 -0
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c93bac60dcbdb44bfc6461435f84e79499682db83a93ea07526a5b546c2a1b63
|
4
|
+
data.tar.gz: c3a7a995862ef152661b1ede6c012e17747c190853cdc501e0a0f99445c7fe40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
4
|
+
class << self
|
5
|
+
def from_messages(messages)
|
6
|
+
return messages if messages.empty? || messages.first.is_a?(Message)
|
5
7
|
|
6
|
-
|
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
|