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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 065b41c8228d87f1c49d6099813e4d67fc8c853f2985bcdff01e8af23d0561d3
4
- data.tar.gz: 820580bb35c3596adfdaee4edb9dc7f54044328f32d92a767d6134a3460875f2
3
+ metadata.gz: 8567895dba91f6a677d35693b104e3a85f138cfcba9d6e5a2e0b50d536d435c9
4
+ data.tar.gz: 5debb8cd50b84a5d4859df01fefcd4b9161c30aae75a0dd1a53e08054331b554
5
5
  SHA512:
6
- metadata.gz: 46752438d8e2ec5772e6c6a134906410de7363c366c187ce771bc6f13c7c748d23975ea7716e98ff22a76e8bfa46be43513cc6ce97cf504788ea612ac68caaeb
7
- data.tar.gz: 1e8f480da2ac8107114ee9503aa9db582cbd7719e1ceceb809a741a8b0a81b67a038c46ecb75bff31e5ef9b77ce3197c6cc8bfbcb19c9c3779e097d8b3d53411
6
+ metadata.gz: d1aabd80bf2ee0e66141de25f799beb042aa662006a8c2bc96d083f86dfd02fd3518732da58dedc8ad09347ee7262c16c1b0244b433bbbe556d980e8ed6027d6
7
+ data.tar.gz: ab612e4af865564a94fed7cb8bf0626e696a5a54b3302b14505f36f58a4d55e3716340175b815a020430f903e2e8fe8f61f2d50ef4ca6ac4d027780952964cd0
@@ -0,0 +1,468 @@
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 :prompt_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
+ prompt_context.options.merge(options)
188
+ generation_provider.embed(prompt_context) if prompt_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.prompt_context = ActiveAgent::ActionPrompt::Prompt.new(message: self)
198
+ agent.embed
199
+ self
200
+ end
201
+ end
202
+
203
+ # Make prompt_context accessible for chaining
204
+ # attr_accessor :prompt_context
205
+
206
+ def perform_generation
207
+ prompt_context.options.merge(options)
208
+ if (action_methods - ActiveAgent::Base.descendants.first.action_methods).include? action_name
209
+ prompt_context.message = prompt_context.messages.last
210
+ prompt_context.actions = []
211
+ end
212
+ generation_provider.generate(prompt_context) if prompt_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_prompt_context(response)
220
+ end
221
+
222
+ def update_prompt_context(response)
223
+ prompt_context.message = prompt_context.messages.last
224
+ ActiveAgent::GenerationProvider::Response.new(prompt: 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 = prompt_context.clone
235
+ process(action.name, *action.params)
236
+ prompt_context.messages.last.role = :tool
237
+ prompt_context.messages.last.action_id = action.id
238
+ prompt_context.messages.last.action_name = action.name
239
+ prompt_context.messages.last.generation_id = action.id
240
+ current_context.messages << prompt_context.messages.last
241
+ self.prompt_context = current_context
242
+ end
243
+
244
+ def initialize
245
+ super
246
+ @_prompt_was_called = false
247
+ @_prompt_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
+ @_prompt_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
+ @_prompt_context.headers(args)
290
+ else
291
+ @_prompt_context
292
+ end
293
+ end
294
+
295
+ def prompt_with(*)
296
+ prompt_context.update_prompt_context(*)
297
+ end
298
+
299
+ def prompt(headers = {}, &block)
300
+ return prompt_context if @_prompt_was_called && headers.blank? && !block
301
+ content_type = headers[:content_type]
302
+ headers = apply_defaults(headers)
303
+ prompt_context.messages = headers[:messages] || []
304
+ prompt_context.context_id = headers[:context_id]
305
+
306
+ prompt_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_prompt_context(prompt_context, headers)
318
+ responses = collect_responses(headers, &block)
319
+
320
+ @_prompt_was_called = true
321
+
322
+ create_parts_from_responses(prompt_context, responses)
323
+
324
+ prompt_context.content_type = set_content_type(prompt_context, content_type, headers[:content_type])
325
+ prompt_context.charset = charset
326
+ prompt_context.actions = headers[:actions] || action_schemas
327
+
328
+ prompt_context
329
+ end
330
+
331
+ def action_schemas
332
+ action_methods.map do |action|
333
+ if action != "text_prompt"
334
+ JSON.parse render_to_string(locals: { action_name: action }, action: action, formats: :json)
335
+ end
336
+ end.compact
337
+ end
338
+
339
+ private
340
+
341
+ def set_content_type(m, user_content_type, class_default) # :doc:
342
+ if user_content_type.present?
343
+ user_content_type
344
+ else
345
+ prompt_context.content_type || class_default
346
+ end
347
+ end
348
+
349
+ # Translates the +subject+ using \Rails I18n class under <tt>[agent_scope, action_name]</tt> scope.
350
+ # If it does not find a translation for the +subject+ under the specified scope it will default to a
351
+ # humanized version of the <tt>action_name</tt>.
352
+ # If the subject has interpolations, you can pass them through the +interpolations+ parameter.
353
+ def default_i18n_subject(interpolations = {}) # :doc:
354
+ agent_scope = self.class.agent_name.tr("/", ".")
355
+ I18n.t(:subject, **interpolations.merge(scope: [ agent_scope, action_name ], default: action_name.humanize))
356
+ end
357
+
358
+ def apply_defaults(headers)
359
+ default_values = self.class.default.except(*headers.keys).transform_values do |value|
360
+ compute_default(value)
361
+ end
362
+
363
+ headers.reverse_merge(default_values)
364
+ end
365
+
366
+ def compute_default(value)
367
+ return value unless value.is_a?(Proc)
368
+
369
+ if value.arity == 1
370
+ instance_exec(self, &value)
371
+ else
372
+ instance_exec(&value)
373
+ end
374
+ end
375
+
376
+ def assign_headers_to_prompt_context(prompt_context, headers)
377
+ assignable = headers.except(:parts_order, :content_type, :body, :role, :template_name,
378
+ :template_path, :delivery_method, :delivery_method_options)
379
+
380
+ assignable.each { |k, v| prompt_context.send(k, v) if prompt_context.respond_to?(k) }
381
+ end
382
+
383
+ def collect_responses(headers, &)
384
+ if block_given?
385
+ collect_responses_from_block(headers, &)
386
+ elsif headers[:body]
387
+ collect_responses_from_text(headers)
388
+ else
389
+ collect_responses_from_templates(headers)
390
+ end
391
+ end
392
+
393
+ def collect_responses_from_block(headers)
394
+ templates_name = headers[:template_name] || action_name
395
+ collector = ActiveAgent::Collector.new(lookup_context) { render(templates_name) }
396
+ yield(collector)
397
+ collector.responses
398
+ end
399
+
400
+ def collect_responses_from_text(headers)
401
+ [ {
402
+ body: headers.delete(:body),
403
+ content_type: headers[:content_type] || "text/plain"
404
+ } ]
405
+ end
406
+
407
+ def collect_responses_from_templates(headers)
408
+ templates_path = headers[:template_path] || self.class.agent_name
409
+ templates_name = headers[:template_name] || action_name
410
+
411
+ each_template(Array(templates_path), templates_name).map do |template|
412
+ format = template.format || formats.first
413
+ {
414
+ body: render(template: template, formats: [ format ]),
415
+ content_type: Mime[format].to_s
416
+ }
417
+ end.compact
418
+ end
419
+
420
+ def each_template(paths, name, &)
421
+ templates = lookup_context.find_all(name, paths)
422
+ if templates.empty?
423
+ raise ActionView::MissingTemplate.new(paths, name, paths, false, "agent")
424
+ else
425
+ templates.uniq(&:format).each(&)
426
+ end
427
+ end
428
+
429
+ def create_parts_from_responses(prompt_context, responses)
430
+ if responses.size > 1
431
+ # prompt_container = ActiveAgent::ActionPrompt::Prompt.new
432
+ # prompt_container.content_type = "multipart/alternative"
433
+ responses.each { |r| insert_part(prompt_context, r, prompt_context.charset) }
434
+ # prompt_context.add_part(prompt_container)
435
+ else
436
+ responses.each { |r| insert_part(prompt_context, r, prompt_context.charset) }
437
+ end
438
+ end
439
+
440
+ def insert_part(prompt_context, response, charset)
441
+ message = ActiveAgent::ActionPrompt::Message.new(
442
+ content: response[:body],
443
+ content_type: response[:content_type],
444
+ charset: charset
445
+ )
446
+ prompt_context.add_part(message)
447
+ end
448
+
449
+ # This and #instrument_name is for caching instrument
450
+ def instrument_payload(key)
451
+ {
452
+ agent: agent_name,
453
+ key: key
454
+ }
455
+ end
456
+
457
+ def instrument_name
458
+ "active_agent"
459
+ end
460
+
461
+ def _protected_ivars
462
+ PROTECTED_IVARS
463
+ end
464
+
465
+ ActiveSupport.run_load_hooks(:active_agent, self)
466
+ end
467
+ end
468
+ 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