The Prompt Generator allows for:
rails g prompt AnExamplePrompt
rails g prompt Resources::Verbs::CommonClassifierPrompt
…to get:
- Prompt class:
app/prompts/.../an_example_prompt.rb - Schema class:
app/schemas/.../an_example_schema.rb - Value object:
app/values/.../an_example_value.rb - Prompt YAML:
config/prompts/.../an_example_prompt.yml
All wired to BasePrompt and Schematist::Schema.
1. Generator class
Create lib/generators/prompt/prompt_generator.rb:
# lib/generators/prompt/prompt_generator.rb
# frozen_string_literal: true
require "rails/generators"
class PromptGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
def create_prompt_files
template "prompt.rb.tt", prompt_class_path
template "schema.rb.tt", schema_class_path
template "value.rb.tt", value_class_path
template "prompt.yml.tt", prompt_yaml_path
end
private
# "Resources::Verbs::CommonClassifierPrompt" -> "CommonClassifier"
# "AnExamplePrompt" -> "AnExample"
def base_name
@base_name ||= class_name.split("::").last.sub(/Prompt\z/, "")
end
# "CommonClassifier"
def base_class_name
base_name
end
# "common_classifier"
def base_file_name
base_name.underscore
end
# app/prompts/resources/verbs/common_classifier_prompt.rb
def prompt_class_path
File.join("app/prompts", class_path, "#{file_name}.rb")
end
# app/schemas/resources/verbs/common_classifier_schema.rb
def schema_class_path
File.join("app/schemas", class_path, "#{base_file_name}_schema.rb")
end
# app/values/resources/verbs/common_classifier_value.rb
def value_class_path
File.join("app/values", class_path, "#{base_file_name}_value.rb")
end
# config/prompts/resources/verbs/common_classifier_prompt.yml
def prompt_yaml_path
File.join("config/prompts", class_path, "#{base_file_name}_prompt.yml")
end
# ["config", "prompts", "resources", "verbs", "common_classifier_prompt.yml"]
def prompt_config_components
["config", "prompts"] + class_path + ["#{base_file_name}_prompt.yml"]
end
# ["resources", "verbs", "common_classifier"]
def trace_name_parts
class_path + [base_file_name]
end
end
Rails will autoload generators from lib/generators automatically in dev; if not, you can add config.autoload_paths << Rails.root.join("lib") but you probably already have that.
2. Templates
Create a templates folder:
mkdir -p lib/generators/prompt/templates
2.1 Prompt class template
lib/generators/prompt/templates/prompt.rb.tt
# frozen_string_literal: true
<% module_namespacing do -%>
class <%= class_name %> < BasePrompt
def prompt_file_path
Rails.root.join(
<%= prompt_config_components.map { |part| "\"#{part}\"" }.join(", ") %>
)
end
def schema_class
<%= (class_path.map(&:camelize) + ["#{base_class_name}Schema"]).join("::") %>
end
def value_class
<%= (class_path.map(&:camelize) + ["#{base_class_name}Value"]).join("::") %>
end
def model_name
ENV.fetch("AI_DEFAULT_MODEL", "gpt-4o-mini")
end
end
<% end -%>
This:
- Inherits from
BasePrompt - Points to
config/prompts/.../<base>_prompt.yml - Hooks up
SchemaandValueclasses in the same namespace - Sets a reasonable default model
2.2 Schema template
lib/generators/prompt/templates/schema.rb.tt
# frozen_string_literal: true
require "schematist"
<% module_namespacing do -%>
class <%= base_class_name %>Schema < Schematist::Schema
# TODO: define your schema fields, for example:
#
# boolean :ok, description: "Whether the operation succeeded"
# string :message, required: false
#
# See Schematist::Schema docs for the DSL.
end
<% end -%>
This generates Namespace::ExampleSchema < Schematist::Schema.
2.3 Value object template
lib/generators/prompt/templates/value.rb.tt
# frozen_string_literal: true
<% module_namespacing do -%>
class <%= base_class_name %>Value
def initialize(payload)
@payload = payload.is_a?(Hash) ? payload : {}
end
# TODO: add typed accessors, for example:
#
# def ok
# @payload["ok"]
# end
#
# def message
# @payload["message"]
# end
end
<% end -%>
This generates Namespace::ExampleValue.
You can later refactor to use a small BaseValue if you like, but this keeps the generator simple and flexible.
2.4 Prompt YAML template
lib/generators/prompt/templates/prompt.yml.tt
system: |-
You are <%= base_class_name %> prompt. Describe what this prompt does here.
Explain the task, the output JSON shape, and any rules the model should follow.
user: |-
# Add user-facing instructions here.
# Reference variables with .
#
# Example:
# Resource ID:
# Language:
This creates:
config/prompts/.../example_prompt.yml
You’ll usually customise this right after generating.
3. Example usage
3.1 Simple name
rails g prompt AnExamplePrompt
Generates:
-
app/prompts/an_example_prompt.rb- class
AnExamplePrompt < BasePrompt
- class
-
app/schemas/an_example_schema.rb- class
AnExampleSchema < Schematist::Schema
- class
-
app/values/an_example_value.rb- class
AnExampleValue
- class
-
config/prompts/an_example_prompt.yml
3.2 Namespaced name
rails g prompt Resources::Verbs::CommonClassifierPrompt
Generates:
-
app/prompts/resources/verbs/common_classifier_prompt.rbResources::Verbs::CommonClassifierPrompt < BasePrompt
-
app/schemas/resources/verbs/common_classifier_schema.rbResources::Verbs::CommonClassifierSchema < Schematist::Schema
-
app/values/resources/verbs/common_classifier_value.rbResources::Verbs::CommonClassifierValue
-
config/prompts/resources/verbs/common_classifier_prompt.yml
Which fits exactly with the structure we designed earlier.
4. Quick sanity check
Just to tie back to your design:
- Prompt:
Namespace::ExamplePrompt(inherits fromBasePrompt) - Schema:
Namespace::ExampleSchema < Schematist::Schema - Value:
Namespace::ExampleValue - Prompt text:
config/prompts/namespace/example_prompt.yml
And for each new LLM behaviour, you now just:
rails g prompt Resources::Verbs::SomeNewPrompt