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 Schema and Value classes 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
  • app/schemas/an_example_schema.rb

    • class AnExampleSchema < Schematist::Schema
  • app/values/an_example_value.rb

    • class AnExampleValue
  • config/prompts/an_example_prompt.yml

3.2 Namespaced name

rails g prompt Resources::Verbs::CommonClassifierPrompt

Generates:

  • app/prompts/resources/verbs/common_classifier_prompt.rb

    • Resources::Verbs::CommonClassifierPrompt < BasePrompt
  • app/schemas/resources/verbs/common_classifier_schema.rb

    • Resources::Verbs::CommonClassifierSchema < Schematist::Schema
  • app/values/resources/verbs/common_classifier_value.rb

    • Resources::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 from BasePrompt)
  • 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

This site uses Just the Docs, a documentation theme for Jekyll.