Class: ActiveAgent::ActionPrompt::Base

Inherits:
AbstractController::Base
  • Object
show all
Includes:
AbstractController::AssetPaths, AbstractController::Caching, AbstractController::Callbacks, AbstractController::Helpers, AbstractController::Logger, AbstractController::Rendering, AbstractController::Translation, ActionView::Layouts, Callbacks, GenerationProvider, Parameterized, Previews, QueuedGeneration, Rescuable, Streaming
Defined in:
lib/active_agent/action_prompt/base.rb

Direct Known Subclasses

Base

Defined Under Namespace

Classes: NullPrompt

Constant Summary collapse

PROTECTED_IVARS =
AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ :@_action_has_layout ]

Class Attribute Summary collapse

Attributes included from Streaming

#stream_chunk

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Rescuable

#handle_exceptions

Methods included from Streaming

#run_stream_callbacks

Methods included from GenerationProvider

#generation_provider

Constructor Details

#initializeBase

:nodoc:



283
284
285
286
287
# File 'lib/active_agent/action_prompt/base.rb', line 283

def initialize # :nodoc:
  super
  @_prompt_was_called = false
  @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options&.deep_dup || {}, agent_instance: self)
end

Class Attribute Details

.agent_nameObject

Returns the name of the current agent. This method is also being used as a path for a view lookup. If this is an anonymous agent, this method will return anonymous instead.



129
130
131
# File 'lib/active_agent/action_prompt/base.rb', line 129

def agent_name
  @agent_name ||= anonymous? ? "anonymous" : name.underscore
end

Class Method Details

.controller_pathObject

Returns the name of the current agent. This method is also being used as a path for a view lookup. If this is an anonymous agent, this method will return anonymous instead.



134
135
136
# File 'lib/active_agent/action_prompt/base.rb', line 134

def agent_name
  @agent_name ||= anonymous? ? "anonymous" : name.underscore
end

.default(value = nil) ⇒ Object Also known as: default_options=

Sets the defaults through app configuration:

config.action_agent.default(from: "[email protected]")

Aliased by ::default_options=



141
142
143
144
# File 'lib/active_agent/action_prompt/base.rb', line 141

def default(value = nil)
  self.default_params = default_params.merge(value).freeze if value
  default_params
end

.generate_prompt(prompt) ⇒ Object

Wraps a prompt generation inside of ActiveSupport::Notifications instrumentation.

This method is actually called by the ActionPrompt::Prompt object itself through a callback when you call :generate_prompt on the ActionPrompt::Prompt, calling generate_prompt directly and passing an ActionPrompt::Prompt will do nothing except tell the logger you generated the prompt.



156
157
158
159
160
161
# File 'lib/active_agent/action_prompt/base.rb', line 156

def generate_prompt(prompt) # :nodoc:
  ActiveSupport::Notifications.instrument("deliver.active_agent") do |payload|
    set_payload_for_prompt(payload, prompt)
    yield # Let Prompt do the generation actions
  end
end

.generate_with(provider, **options) ⇒ Object

Define how the agent should generate content



111
112
113
114
115
116
117
118
119
120
121
# File 'lib/active_agent/action_prompt/base.rb', line 111

def generate_with(provider, **options)
  self.generation_provider = provider
  if options.has_key?(:instructions) || (self.options || {}).empty?
    # Either instructions explicitly provided, or no inherited options exist
    self.options = (self.options || {}).merge(options)
  else
    # Don't inherit instructions from parent if not explicitly set
    inherited_options = (self.options || {}).except(:instructions)
    self.options = inherited_options.merge(options)
  end
end

.observer_class_for(value) ⇒ Object

:nodoc:



100
101
102
103
104
105
106
107
# File 'lib/active_agent/action_prompt/base.rb', line 100

def observer_class_for(value) # :nodoc:
  case value
  when String, Symbol
    value.to_s.camelize.constantize
  else
    value
  end
end

.register_interceptor(interceptor) ⇒ Object

Register an Interceptor which will be called before prompt is sent. Either a class, string, or symbol can be passed in as the Interceptor. If a string or symbol is passed in it will be camelized and constantized.



89
90
91
# File 'lib/active_agent/action_prompt/base.rb', line 89

def register_interceptor(interceptor)
  Prompt.register_interceptor(observer_class_for(interceptor))
end

.register_interceptors(*interceptors) ⇒ Object

Register one or more Interceptors which will be called before prompt is sent.



63
64
65
# File 'lib/active_agent/action_prompt/base.rb', line 63

def register_interceptors(*interceptors)
  interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
end

.register_observer(observer) ⇒ Object

Register an Observer which will be notified when prompt is generated. Either a class, string, or symbol can be passed in as the Observer. If a string or symbol is passed in it will be camelized and constantized.



75
76
77
# File 'lib/active_agent/action_prompt/base.rb', line 75

def register_observer(observer)
  Prompt.register_observer(observer_class_for(observer))
end

.register_observers(*observers) ⇒ Object

Register one or more Observers which will be notified when prompt is generated.



53
54
55
# File 'lib/active_agent/action_prompt/base.rb', line 53

def register_observers(*observers)
  observers.flatten.compact.each { |observer| register_observer(observer) }
end

.stream_with(&stream) ⇒ Object



123
124
125
# File 'lib/active_agent/action_prompt/base.rb', line 123

def stream_with(&stream)
  self.options = (options || {}).merge(stream: stream)
end

.unregister_interceptor(interceptor) ⇒ Object

Unregister a previously registered Interceptor. Either a class, string, or symbol can be passed in as the Interceptor. If a string or symbol is passed in it will be camelized and constantized.



96
97
98
# File 'lib/active_agent/action_prompt/base.rb', line 96

def unregister_interceptor(interceptor)
  Prompt.unregister_interceptor(observer_class_for(interceptor))
end

.unregister_interceptors(*interceptors) ⇒ Object

Unregister one or more previously registered Interceptors.



68
69
70
# File 'lib/active_agent/action_prompt/base.rb', line 68

def unregister_interceptors(*interceptors)
  interceptors.flatten.compact.each { |interceptor| unregister_interceptor(interceptor) }
end

.unregister_observer(observer) ⇒ Object

Unregister a previously registered Observer. Either a class, string, or symbol can be passed in as the Observer. If a string or symbol is passed in it will be camelized and constantized.



82
83
84
# File 'lib/active_agent/action_prompt/base.rb', line 82

def unregister_observer(observer)
  Prompt.unregister_observer(observer_class_for(observer))
end

.unregister_observers(*observers) ⇒ Object

Unregister one or more previously registered Observers.



58
59
60
# File 'lib/active_agent/action_prompt/base.rb', line 58

def unregister_observers(*observers)
  observers.flatten.compact.each { |observer| unregister_observer(observer) }
end

Instance Method Details

#action_methodsObject



379
380
381
# File 'lib/active_agent/action_prompt/base.rb', line 379

def action_methods
  super - ActiveAgent::Base.public_instance_methods(false).map(&:to_s) - [ action_name ]
end

#action_schemasObject



383
384
385
386
387
388
389
# File 'lib/active_agent/action_prompt/base.rb', line 383

def action_schemas
  prefixes = set_prefixes(action_name, lookup_context.prefixes)

  action_methods.map do |action|
    render_schema(action, prefixes)
  end.compact
end

#agent_nameObject

Returns the name of the agent object.



322
323
324
# File 'lib/active_agent/action_prompt/base.rb', line 322

def agent_name
  self.class.agent_name
end

#agent_streamObject



188
189
190
191
192
193
194
195
196
# File 'lib/active_agent/action_prompt/base.rb', line 188

def agent_stream
  proc do |message, delta, stop, action_name|
    @_action_name = action_name

    run_stream_callbacks(message, delta, stop) do |message, delta, stop|
      yield message, delta, stop if block_given?
    end
  end
end

#continue_generationObject



234
235
236
237
238
# File 'lib/active_agent/action_prompt/base.rb', line 234

def continue_generation
  # Continue generating with the updated context that includes tool results
  generation_provider.generate(context) if context && generation_provider
  handle_response(generation_provider.response)
end

#embedObject



198
199
200
201
202
# File 'lib/active_agent/action_prompt/base.rb', line 198

def embed
  context.options.merge(options)
  generation_provider.embed(context) if context && generation_provider
  handle_response(generation_provider.response)
end

#handle_response(response) ⇒ Object



223
224
225
226
227
228
229
230
231
232
# File 'lib/active_agent/action_prompt/base.rb', line 223

def handle_response(response)
  return response unless response.message.requested_actions.present?

  # The assistant message with tool_calls is already added by update_context in the provider
  # Now perform the requested actions which will add tool response messages
  perform_actions(requested_actions: response.message.requested_actions)

  # Continue generation with updated context
  continue_generation
end

#headers(args = nil) ⇒ Object



326
327
328
329
330
331
332
# File 'lib/active_agent/action_prompt/base.rb', line 326

def headers(args = nil)
  if args
    @_context.headers(args)
  else
    @_context
  end
end

#perform_action(action) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/active_agent/action_prompt/base.rb', line 250

def perform_action(action)
  # Save the current messages to preserve conversation history
  original_messages = context.messages.dup
  original_params = context.params || {}

  if action.params.is_a?(Hash)
    self.params = original_params.merge(action.params)
  else
    self.params = original_params
  end

  # Save the current prompt_was_called state and reset it so the action can render
  original_prompt_was_called = @_prompt_was_called
  @_prompt_was_called = false

  # Process the action, which will render the view and populate context
  process(action.name)

  # The action should have called prompt which populates context.message
  # Create a tool message from the rendered response
  tool_message = context.message.dup
  tool_message.role = :tool
  tool_message.action_id = action.id
  tool_message.action_name = action.name
  tool_message.generation_id = action.id

  # Restore the messages with the new tool message
  context.messages = original_messages + [ tool_message ]

  # Restore the prompt_was_called state
  @_prompt_was_called = original_prompt_was_called
end

#perform_actions(requested_actions:) ⇒ Object



244
245
246
247
248
# File 'lib/active_agent/action_prompt/base.rb', line 244

def perform_actions(requested_actions:)
  requested_actions.each do |action|
    perform_action(action)
  end
end

#perform_generationObject

Make context accessible for chaining attr_accessor :context



218
219
220
221
# File 'lib/active_agent/action_prompt/base.rb', line 218

def perform_generation
  generation_provider.generate(context) if context && generation_provider
  handle_response(generation_provider.response)
end

#process(method_name, *args) ⇒ Object

:nodoc:



289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/active_agent/action_prompt/base.rb', line 289

def process(method_name, *args) # :nodoc:
  payload = {
    agent: self.class.name,
    action: method_name,
    args: args
  }

  ActiveSupport::Notifications.instrument("process.active_agent", payload) do
    super
    @_context = ActiveAgent::ActionPrompt::Prompt.new(agent_instance: self) unless @_prompt_was_called
  end
end

#prompt(headers = {}, &block) ⇒ Object



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/active_agent/action_prompt/base.rb', line 338

def prompt(headers = {}, &block)
  return context if @_prompt_was_called && headers.blank? && !block
  # Apply option hierarchy: prompt options > agent options > config options
  merged_options = merge_options(headers[:options] || {})

  raw_instructions = headers.has_key?(:instructions) ? headers[:instructions] : context.options[:instructions]

  context.instructions = prepare_instructions(raw_instructions)

  context.options.merge!(merged_options)
  context.options[:stream] = agent_stream if context.options[:stream]
  content_type = headers[:content_type]

  headers = apply_defaults(headers)
  context.messages = headers[:messages] || []
  context.mcp_servers = headers[:mcp_servers] || []
  context.context_id = headers[:context_id]
  context.params = params
  context.action_name = action_name

  context.output_schema = render_schema(headers[:output_schema], set_prefixes(headers[:output_schema], lookup_context.prefixes))

  context.charset = charset = headers[:charset]

  headers = prepare_message(headers)
  # wrap_generation_behavior!(headers[:generation_method], headers[:generation_method_options])
  # assign_headers_to_context(context, headers)
  responses = collect_responses(headers, &block)

  @_prompt_was_called = true

  create_parts_from_responses(context, responses)

  context.content_type = set_content_type(context, content_type, headers[:content_type])

  context.charset = charset
  context.actions = headers[:actions] || action_schemas

  context
end

#prompt_withObject



334
335
336
# File 'lib/active_agent/action_prompt/base.rb', line 334

def prompt_with(*)
  context.update_context(*)
end

#update_context(response) ⇒ Object



240
241
242
# File 'lib/active_agent/action_prompt/base.rb', line 240

def update_context(response)
  ActiveAgent::GenerationProvider::Response.new(prompt: context)
end