action_prompter 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 +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +586 -0
- data/lib/action_prompt/adapters/base.rb +62 -0
- data/lib/action_prompt/adapters/null.rb +21 -0
- data/lib/action_prompt/adapters/test.rb +59 -0
- data/lib/action_prompt/base.rb +117 -0
- data/lib/action_prompt/configuration.rb +38 -0
- data/lib/action_prompt/message.rb +81 -0
- data/lib/action_prompt/railtie.rb +36 -0
- data/lib/action_prompt/renderer.rb +78 -0
- data/lib/action_prompt/version.rb +5 -0
- data/lib/action_prompt.rb +54 -0
- data/lib/generators/action_prompt/action_prompt_generator.rb +60 -0
- data/lib/generators/action_prompt/templates/prompt.rb.tt +18 -0
- data/lib/generators/action_prompt/templates/prompt.text.erb.tt +13 -0
- metadata +109 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionPrompt
|
|
4
|
+
module Adapters
|
|
5
|
+
# A no-op adapter that silently discards all prompts and returns +nil+.
|
|
6
|
+
#
|
|
7
|
+
# Useful for disabling LLM calls in environments where you want to
|
|
8
|
+
# suppress all outbound requests (e.g., a staging environment without
|
|
9
|
+
# API credentials).
|
|
10
|
+
#
|
|
11
|
+
# ActionPrompt.configure do |config|
|
|
12
|
+
# config.adapter = ActionPrompt::Adapters::Null.new
|
|
13
|
+
# end
|
|
14
|
+
class Null < Base
|
|
15
|
+
# @return [nil] always returns nil without making any external calls
|
|
16
|
+
def complete(prompt_text, options = {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionPrompt
|
|
4
|
+
module Adapters
|
|
5
|
+
# A test adapter that captures deliveries in memory without making real
|
|
6
|
+
# API calls. Use this in your test suite to assert on prompt content.
|
|
7
|
+
#
|
|
8
|
+
# == Setup (spec/support/action_prompt.rb)
|
|
9
|
+
#
|
|
10
|
+
# RSpec.configure do |config|
|
|
11
|
+
# config.before(:each) do
|
|
12
|
+
# ActionPrompt.reset_configuration!
|
|
13
|
+
# ActionPrompt::Adapters::Test.clear_deliveries!
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# == Asserting on deliveries
|
|
18
|
+
#
|
|
19
|
+
# it "sends the article body in the prompt" do
|
|
20
|
+
# ArticleSummarizerPrompt.summarize(article).deliver_now
|
|
21
|
+
#
|
|
22
|
+
# delivery = ActionPrompt::Adapters::Test.deliveries.last
|
|
23
|
+
# expect(delivery[:prompt]).to include(article.body)
|
|
24
|
+
# expect(delivery[:options][:model]).to eq("gpt-4o-mini")
|
|
25
|
+
# end
|
|
26
|
+
class Test < Base
|
|
27
|
+
class << self
|
|
28
|
+
# All prompts delivered since the last {clear_deliveries!} call.
|
|
29
|
+
#
|
|
30
|
+
# Each entry is a Hash with keys:
|
|
31
|
+
# - +:prompt+ — the rendered prompt string
|
|
32
|
+
# - +:options+ — the merged LLM options hash
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<Hash>]
|
|
35
|
+
def deliveries
|
|
36
|
+
@deliveries ||= []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Clears all recorded deliveries. Call this in a +before(:each)+ block.
|
|
40
|
+
# @return [void]
|
|
41
|
+
def clear_deliveries!
|
|
42
|
+
@deliveries = []
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Records the delivery and returns a predictable canned response string.
|
|
47
|
+
#
|
|
48
|
+
# @param prompt_text [String]
|
|
49
|
+
# @param options [Hash]
|
|
50
|
+
# @return [String]
|
|
51
|
+
def complete(prompt_text, options = {})
|
|
52
|
+
delivery = { prompt: prompt_text, options: options }
|
|
53
|
+
self.class.deliveries << delivery
|
|
54
|
+
"[ActionPrompt::Test] Response for #{options[:model] || "unknown-model"} " \
|
|
55
|
+
"(#{prompt_text.length} chars)"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionPrompt
|
|
4
|
+
# Base class for all prompt classes. Subclass this to define your prompts.
|
|
5
|
+
#
|
|
6
|
+
# Each public instance method in a subclass is a "prompt action". Calling
|
|
7
|
+
# the method at the class level returns an {ActionPrompt::Message} that can
|
|
8
|
+
# be delivered synchronously via {ActionPrompt::Message#deliver_now}.
|
|
9
|
+
#
|
|
10
|
+
# == Defining a prompt
|
|
11
|
+
#
|
|
12
|
+
# class ArticleSummarizerPrompt < ActionPrompt::Base
|
|
13
|
+
# # Class-level defaults override the global config for this class only.
|
|
14
|
+
# default model: "gpt-4o-mini", temperature: 0.5
|
|
15
|
+
#
|
|
16
|
+
# def summarize(article)
|
|
17
|
+
# @article = article # available in the ERB template as @article
|
|
18
|
+
# @word_limit = 150
|
|
19
|
+
#
|
|
20
|
+
# prompt(model: "gpt-4o") # optional per-action override
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# == Delivering a prompt
|
|
25
|
+
#
|
|
26
|
+
# response = ArticleSummarizerPrompt.summarize(article).deliver_now
|
|
27
|
+
#
|
|
28
|
+
# == Template location
|
|
29
|
+
#
|
|
30
|
+
# app/views/action_prompts/article_summarizer_prompt/summarize.text.erb
|
|
31
|
+
#
|
|
32
|
+
class Base
|
|
33
|
+
# Inheritable hash of default LLM options for the class.
|
|
34
|
+
# Set via the {.default} class macro.
|
|
35
|
+
class_attribute :default_options, default: {}, instance_accessor: false
|
|
36
|
+
|
|
37
|
+
# ActionView path set populated by the Railtie (or set manually in tests).
|
|
38
|
+
class_attribute :view_paths, default: ActionView::PathSet.new, instance_accessor: false
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
# Sets default LLM options for all actions within this class.
|
|
42
|
+
# Options merge with the global config, and can be further overridden
|
|
43
|
+
# per-action by passing options to {#prompt}.
|
|
44
|
+
#
|
|
45
|
+
# @param options [Hash] e.g. +model: "gpt-4o", temperature: 0.7+
|
|
46
|
+
# @return [void]
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# class MyPrompt < ActionPrompt::Base
|
|
50
|
+
# default model: "gpt-4o", temperature: 0.3
|
|
51
|
+
# end
|
|
52
|
+
def default(**options)
|
|
53
|
+
self.default_options = default_options.merge(options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Intercepts calls to public instance methods (prompt actions) and
|
|
57
|
+
# returns an {ActionPrompt::Message}. This is the primary entry point
|
|
58
|
+
# for building a prompt from outside the class.
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# ArticleSummarizerPrompt.summarize(article) # => ActionPrompt::Message
|
|
62
|
+
#
|
|
63
|
+
# @return [ActionPrompt::Message]
|
|
64
|
+
def method_missing(method_name, *args, &block)
|
|
65
|
+
if public_method_defined?(method_name)
|
|
66
|
+
instance = new
|
|
67
|
+
instance.send(:process, method_name, *args, &block)
|
|
68
|
+
instance.message
|
|
69
|
+
else
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
75
|
+
public_method_defined?(method_name) || super
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The {ActionPrompt::Message} built by the most recent call to {#prompt}.
|
|
80
|
+
# @return [ActionPrompt::Message, nil]
|
|
81
|
+
attr_reader :message
|
|
82
|
+
|
|
83
|
+
# Declares per-action LLM options and finalises the {Message} for this
|
|
84
|
+
# action. Call this at the end of every action method.
|
|
85
|
+
#
|
|
86
|
+
# Options are merged in increasing priority order:
|
|
87
|
+
# global config defaults < class-level defaults < per-action options
|
|
88
|
+
#
|
|
89
|
+
# @param options [Hash] per-action overrides (e.g. +model:+, +temperature:+)
|
|
90
|
+
# @return [ActionPrompt::Message]
|
|
91
|
+
def prompt(options = {})
|
|
92
|
+
merged_options = ActionPrompt.configuration.default_options
|
|
93
|
+
.merge(self.class.default_options)
|
|
94
|
+
.merge(options)
|
|
95
|
+
@message = Message.new(self, @_action_name, merged_options)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Runs an action method by name and ensures a {Message} is available.
|
|
101
|
+
#
|
|
102
|
+
# @param method_name [Symbol, String]
|
|
103
|
+
# @param args [Array]
|
|
104
|
+
# @return [ActionPrompt::Message]
|
|
105
|
+
def process(method_name, *args)
|
|
106
|
+
@_action_name = method_name.to_s
|
|
107
|
+
@message = nil
|
|
108
|
+
public_send(method_name, *args)
|
|
109
|
+
|
|
110
|
+
# Guard: if the developer forgot to call `prompt` in their action,
|
|
111
|
+
# build a message with default options so deliver_now still works.
|
|
112
|
+
@message ||= Message.new(self, @_action_name,
|
|
113
|
+
ActionPrompt.configuration.default_options
|
|
114
|
+
.merge(self.class.default_options))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionPrompt
|
|
4
|
+
# Holds global configuration for the ActionPrompt framework.
|
|
5
|
+
#
|
|
6
|
+
# Configure via an initializer:
|
|
7
|
+
#
|
|
8
|
+
# # config/initializers/action_prompt.rb
|
|
9
|
+
# ActionPrompt.configure do |config|
|
|
10
|
+
# config.adapter = MyOpenAIAdapter.new(api_key: ENV.fetch("OPENAI_API_KEY"))
|
|
11
|
+
# config.default_options = { model: "gpt-4o", temperature: 0.7 }
|
|
12
|
+
# end
|
|
13
|
+
class Configuration
|
|
14
|
+
# The active delivery adapter. Defaults to the Test adapter.
|
|
15
|
+
# @return [ActionPrompt::Adapters::Base]
|
|
16
|
+
attr_accessor :adapter
|
|
17
|
+
|
|
18
|
+
# Global default options passed to every adapter call unless overridden.
|
|
19
|
+
# @return [Hash]
|
|
20
|
+
attr_accessor :default_options
|
|
21
|
+
|
|
22
|
+
# Logger instance. Defaults to Rails.logger inside a Rails app.
|
|
23
|
+
# @return [Logger]
|
|
24
|
+
attr_accessor :logger
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@adapter = ActionPrompt::Adapters::Test.new
|
|
28
|
+
@default_options = {}
|
|
29
|
+
@logger = default_logger
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def default_logger
|
|
35
|
+
defined?(Rails) ? Rails.logger : Logger.new($stdout, level: Logger::INFO)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionPrompt
|
|
4
|
+
# Represents a fully prepared prompt ready for delivery to an LLM adapter.
|
|
5
|
+
# Analogous to +Mail::Message+ in ActionMailer.
|
|
6
|
+
#
|
|
7
|
+
# Instances are returned by calling a class-level action method:
|
|
8
|
+
#
|
|
9
|
+
# message = ArticleSummarizerPrompt.summarize(article)
|
|
10
|
+
# message.body # => rendered ERB string (lazily evaluated)
|
|
11
|
+
# message.deliver_now # => LLM response string
|
|
12
|
+
class Message
|
|
13
|
+
# The prompt instance that built this message.
|
|
14
|
+
# @return [ActionPrompt::Base]
|
|
15
|
+
attr_reader :prompt_instance
|
|
16
|
+
|
|
17
|
+
# The name of the action that built this message.
|
|
18
|
+
# @return [String]
|
|
19
|
+
attr_reader :action_name
|
|
20
|
+
|
|
21
|
+
# Merged LLM options (global defaults < class defaults < per-action options).
|
|
22
|
+
# @return [Hash]
|
|
23
|
+
attr_reader :options
|
|
24
|
+
|
|
25
|
+
# @param prompt_instance [ActionPrompt::Base]
|
|
26
|
+
# @param action_name [String]
|
|
27
|
+
# @param options [Hash]
|
|
28
|
+
def initialize(prompt_instance, action_name, options = {})
|
|
29
|
+
@prompt_instance = prompt_instance
|
|
30
|
+
@action_name = action_name
|
|
31
|
+
@options = options
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The rendered prompt body. Evaluated lazily on first access.
|
|
35
|
+
#
|
|
36
|
+
# @return [String]
|
|
37
|
+
def body
|
|
38
|
+
@body ||= Renderer.new(prompt_instance, action_name).render
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Delivers the prompt to the configured adapter synchronously and returns
|
|
42
|
+
# the LLM response.
|
|
43
|
+
#
|
|
44
|
+
# @return [String, nil] the LLM's response (type depends on the adapter)
|
|
45
|
+
def deliver_now
|
|
46
|
+
log_delivery
|
|
47
|
+
ActionPrompt.configuration.adapter.complete(body, options)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Enqueues the prompt for asynchronous delivery via ActiveJob.
|
|
51
|
+
#
|
|
52
|
+
# @note Full ActiveJob integration is planned for a future release.
|
|
53
|
+
# This currently raises +NotImplementedError+.
|
|
54
|
+
# @raise [NotImplementedError]
|
|
55
|
+
def deliver_later(**_kwargs)
|
|
56
|
+
raise NotImplementedError,
|
|
57
|
+
"deliver_later is not yet implemented. Use deliver_now. " \
|
|
58
|
+
"ActiveJob integration is planned for a future release."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [String]
|
|
62
|
+
def inspect
|
|
63
|
+
"#<ActionPrompt::Message action=#{action_name.inspect} options=#{options.inspect}>"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
alias to_s inspect
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def log_delivery
|
|
71
|
+
logger = ActionPrompt.configuration.logger
|
|
72
|
+
return unless logger
|
|
73
|
+
|
|
74
|
+
logger.debug do
|
|
75
|
+
"[ActionPrompt] Delivering #{prompt_instance.class.name}##{action_name} " \
|
|
76
|
+
"via #{ActionPrompt.configuration.adapter.class.name} " \
|
|
77
|
+
"with options #{options.inspect}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module ActionPrompt
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
# Ensure app/prompts is part of the autoload paths so prompt classes
|
|
8
|
+
# are loaded by Zeitwerk without any extra configuration.
|
|
9
|
+
initializer "action_prompt.add_autoload_paths" do |app|
|
|
10
|
+
prompts_path = app.root.join("app", "prompts").to_s
|
|
11
|
+
|
|
12
|
+
app.config.autoload_paths << prompts_path
|
|
13
|
+
app.config.eager_load_paths << prompts_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Point ActionPrompt::Base's view paths at the application's view directory
|
|
17
|
+
# so ERB templates resolve correctly from app/views/action_prompts/.
|
|
18
|
+
initializer "action_prompt.configure_view_paths" do |app|
|
|
19
|
+
ActiveSupport.on_load(:action_prompt) do
|
|
20
|
+
ActionPrompt::Base.view_paths = ActionView::PathSet.new(
|
|
21
|
+
[app.root.join("app", "views").to_s]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Use Rails.logger once the app is fully initialized.
|
|
27
|
+
initializer "action_prompt.set_logger" do
|
|
28
|
+
ActiveSupport.on_load(:action_prompt) do
|
|
29
|
+
ActionPrompt.configuration.logger = Rails.logger
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Allow prompt classes to call run_load_hooks in the future.
|
|
34
|
+
ActiveSupport.run_load_hooks(:action_prompt, Base)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionPrompt
|
|
4
|
+
# Handles ERB template rendering for prompt actions via ActionView.
|
|
5
|
+
#
|
|
6
|
+
# Templates are resolved from the view paths configured on the prompt class
|
|
7
|
+
# (defaults to +app/views/+ in a Rails app). The lookup path convention is:
|
|
8
|
+
#
|
|
9
|
+
# app/views/action_prompts/<prompt_class_name>/<action_name>.text.erb
|
|
10
|
+
#
|
|
11
|
+
# == Examples
|
|
12
|
+
#
|
|
13
|
+
# ArticleSummarizerPrompt -> action_prompts/article_summarizer_prompt/summarize.text.erb
|
|
14
|
+
# Admin::ReportPrompt -> action_prompts/admin/report_prompt/generate.text.erb
|
|
15
|
+
class Renderer
|
|
16
|
+
# @param prompt_instance [ActionPrompt::Base] owns the instance variables for the template
|
|
17
|
+
# @param action_name [String] the prompt action method name
|
|
18
|
+
def initialize(prompt_instance, action_name)
|
|
19
|
+
@prompt_instance = prompt_instance
|
|
20
|
+
@action_name = action_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders the ERB template and returns the resulting string.
|
|
24
|
+
#
|
|
25
|
+
# @return [String] the rendered prompt body
|
|
26
|
+
# @raise [ActionView::MissingTemplate] if the template file cannot be found
|
|
27
|
+
def render
|
|
28
|
+
lookup_context = ActionView::LookupContext.new(resolved_view_paths)
|
|
29
|
+
view = ActionView::Base.with_empty_template_cache.new(lookup_context, assigns, nil)
|
|
30
|
+
view.render(template: template_path, formats: [:text], handlers: [:erb])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Derives the template path from the prompt class name and action.
|
|
36
|
+
#
|
|
37
|
+
# @return [String] e.g. "action_prompts/article_summarizer_prompt/summarize"
|
|
38
|
+
def template_path
|
|
39
|
+
class_segment = @prompt_instance.class.name
|
|
40
|
+
.gsub("::", "/")
|
|
41
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
42
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
43
|
+
.downcase
|
|
44
|
+
|
|
45
|
+
"action_prompts/#{class_segment}/#{@action_name}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the view paths configured on the prompt class, falling back to
|
|
49
|
+
# the Rails app root or the current working directory.
|
|
50
|
+
#
|
|
51
|
+
# @return [Array<String>]
|
|
52
|
+
def resolved_view_paths
|
|
53
|
+
class_paths = @prompt_instance.class.view_paths.to_a.map(&:to_s)
|
|
54
|
+
return class_paths unless class_paths.empty?
|
|
55
|
+
|
|
56
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
57
|
+
[Rails.root.join("app", "views").to_s]
|
|
58
|
+
else
|
|
59
|
+
[File.expand_path("app/views", Dir.pwd)]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Collects public instance variables from the prompt, exposing them
|
|
64
|
+
# to the ERB template as top-level assigns.
|
|
65
|
+
#
|
|
66
|
+
# Variables prefixed with +_+ or named +message+ are excluded.
|
|
67
|
+
#
|
|
68
|
+
# @return [Hash{String => Object}]
|
|
69
|
+
def assigns
|
|
70
|
+
@prompt_instance.instance_variables.each_with_object({}) do |ivar, hash|
|
|
71
|
+
name = ivar.to_s.delete_prefix("@")
|
|
72
|
+
next if name.start_with?("_") || name == "message"
|
|
73
|
+
|
|
74
|
+
hash[name] = @prompt_instance.instance_variable_get(ivar)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/core_ext/class/attribute"
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
require "action_view"
|
|
7
|
+
|
|
8
|
+
require_relative "action_prompt/version"
|
|
9
|
+
require_relative "action_prompt/adapters/base"
|
|
10
|
+
require_relative "action_prompt/adapters/null"
|
|
11
|
+
require_relative "action_prompt/adapters/test"
|
|
12
|
+
require_relative "action_prompt/configuration"
|
|
13
|
+
require_relative "action_prompt/renderer"
|
|
14
|
+
require_relative "action_prompt/message"
|
|
15
|
+
require_relative "action_prompt/base"
|
|
16
|
+
|
|
17
|
+
# ActionPrompt is an ActionMailer-inspired framework for defining, rendering,
|
|
18
|
+
# and delivering LLM prompts in Ruby on Rails applications.
|
|
19
|
+
#
|
|
20
|
+
# == Configuration
|
|
21
|
+
#
|
|
22
|
+
# ActionPrompt.configure do |config|
|
|
23
|
+
# config.adapter = MyAdapter.new(api_key: ENV.fetch("MY_API_KEY"))
|
|
24
|
+
# config.default_options = { model: "gpt-4o", temperature: 0.7 }
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# == Delivery
|
|
28
|
+
#
|
|
29
|
+
# response = ArticleSummarizerPrompt.summarize(article).deliver_now
|
|
30
|
+
#
|
|
31
|
+
module ActionPrompt
|
|
32
|
+
class << self
|
|
33
|
+
# Returns the global configuration object.
|
|
34
|
+
# @return [ActionPrompt::Configuration]
|
|
35
|
+
def configuration
|
|
36
|
+
@configuration ||= Configuration.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Yields the {Configuration} for block-style setup.
|
|
40
|
+
# @yieldparam config [ActionPrompt::Configuration]
|
|
41
|
+
# @return [void]
|
|
42
|
+
def configure
|
|
43
|
+
yield configuration
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resets configuration to defaults. Primarily useful in test suites.
|
|
47
|
+
# @return [void]
|
|
48
|
+
def reset_configuration!
|
|
49
|
+
@configuration = Configuration.new
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
require_relative "action_prompt/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/named_base"
|
|
5
|
+
|
|
6
|
+
module ActionPrompt
|
|
7
|
+
# Rails generator that creates a prompt class and its ERB view templates.
|
|
8
|
+
#
|
|
9
|
+
# == Usage
|
|
10
|
+
#
|
|
11
|
+
# rails generate action_prompt NAME [action action ...]
|
|
12
|
+
#
|
|
13
|
+
# == Examples
|
|
14
|
+
#
|
|
15
|
+
# rails generate action_prompt ArticleSummarizer summarize
|
|
16
|
+
# rails generate action_prompt Moderation classify flag
|
|
17
|
+
# rails generate action_prompt Admin::Report generate
|
|
18
|
+
#
|
|
19
|
+
# Each invocation creates:
|
|
20
|
+
# * app/prompts/<name>_prompt.rb
|
|
21
|
+
# * app/views/action_prompts/<name>_prompt/<action>.text.erb (one per action)
|
|
22
|
+
class ActionPromptGenerator < Rails::Generators::NamedBase
|
|
23
|
+
source_root File.expand_path("templates", __dir__)
|
|
24
|
+
|
|
25
|
+
argument :actions,
|
|
26
|
+
type: :array,
|
|
27
|
+
default: [],
|
|
28
|
+
banner: "action action ..."
|
|
29
|
+
|
|
30
|
+
desc "Creates an ActionPrompt prompt class and ERB view template(s)."
|
|
31
|
+
|
|
32
|
+
# ------------------------------------------------------------------ #
|
|
33
|
+
# Steps
|
|
34
|
+
# ------------------------------------------------------------------ #
|
|
35
|
+
|
|
36
|
+
def create_prompt_file
|
|
37
|
+
template "prompt.rb.tt",
|
|
38
|
+
File.join("app/prompts", class_path, "#{file_name}_prompt.rb")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create_view_files
|
|
42
|
+
actions.each do |action|
|
|
43
|
+
@current_action = action
|
|
44
|
+
template "prompt.text.erb.tt",
|
|
45
|
+
File.join("app/views/action_prompts", class_path,
|
|
46
|
+
"#{file_name}_prompt", "#{action}.text.erb")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Full CamelCase class name including the "Prompt" suffix.
|
|
53
|
+
# @return [String] e.g. "ArticleSummarizerPrompt"
|
|
54
|
+
def prompt_class_name
|
|
55
|
+
"#{class_name}Prompt"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
attr_reader :current_action
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= prompt_class_name %> < ActionPrompt::Base
|
|
4
|
+
# Set class-level defaults. These override the global config for this class.
|
|
5
|
+
# default model: "gpt-4o-mini", temperature: 0.7
|
|
6
|
+
<% actions.each do |action| -%>
|
|
7
|
+
|
|
8
|
+
def <%= action %>
|
|
9
|
+
# Assign instance variables here; they will be available in the ERB template.
|
|
10
|
+
# e.g. @record = some_argument
|
|
11
|
+
|
|
12
|
+
prompt(
|
|
13
|
+
# model: "gpt-4o-mini",
|
|
14
|
+
# temperature: 0.7
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
<% end -%>
|
|
18
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%# app/views/action_prompts/<%= file_name %>_prompt/<%= current_action %>.text.erb %>
|
|
2
|
+
<%# Instance variables set in <%= prompt_class_name %>#<%= current_action %> are available here. %>
|
|
3
|
+
|
|
4
|
+
You are a helpful assistant.
|
|
5
|
+
|
|
6
|
+
<%# TODO: Write your prompt below, referencing instance variables as needed. %>
|
|
7
|
+
<%# Example:
|
|
8
|
+
Please summarize the following content in three bullet points:
|
|
9
|
+
|
|
10
|
+
Title: <%= @record.title %>
|
|
11
|
+
|
|
12
|
+
<%= @record.body %>
|
|
13
|
+
%>
|