promptly 0.1.13 → 0.1.21

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.
data/lib/promptly.rb CHANGED
@@ -5,14 +5,68 @@ require_relative "promptly/renderer"
5
5
  require_relative "promptly/locator"
6
6
  require_relative "promptly/cache"
7
7
  require_relative "promptly/helper"
8
+ require_relative "promptly/validator"
9
+ require "yaml"
8
10
 
9
11
  module Promptly
10
12
  class Error < StandardError; end
13
+ class ValidationError < Error; end
14
+
15
+ class Prompt
16
+ attr_reader :content, :version, :author, :change_notes
17
+
18
+ def initialize(content:, version: nil, author: nil, change_notes: nil)
19
+ @content = content
20
+ @version = version
21
+ @author = author
22
+ @change_notes = change_notes
23
+ end
24
+
25
+ def to_s
26
+ content
27
+ end
28
+ end
11
29
 
12
30
  def self.render_template(template, locals: {}, engine: :erb)
13
31
  Renderer.render(template, locals: locals, engine: engine)
14
32
  end
15
33
 
34
+ def self.response_format(identifier, strict: true)
35
+ schema_path = File.join(prompts_path, "#{identifier}.response.json")
36
+ raise Error, "Schema file not found for '#{identifier}' at #{schema_path}" unless File.exist?(schema_path)
37
+
38
+ require "json"
39
+ schema_content = JSON.parse(File.read(schema_path))
40
+
41
+ {
42
+ type: "json_schema",
43
+ json_schema: {
44
+ name: identifier.gsub(/[^a-zA-Z0-9_-]/, "_"),
45
+ strict: strict,
46
+ schema: schema_content
47
+ }
48
+ }
49
+ end
50
+
51
+ def self.validate_response!(identifier, json_string)
52
+ schema_path = File.join(prompts_path, "#{identifier}.response.json")
53
+ raise Error, "Schema file not found for '#{identifier}' at #{schema_path}" unless File.exist?(schema_path)
54
+
55
+ require "json"
56
+ require "json_schemer"
57
+
58
+ schema_content = JSON.parse(File.read(schema_path))
59
+ parsed_json = JSON.parse(json_string)
60
+
61
+ schemer = JSONSchemer.schema(schema_content)
62
+ unless schemer.valid?(parsed_json)
63
+ errors = schemer.validate(parsed_json).to_a
64
+ raise ValidationError, "Response does not match schema: #{errors.inspect}"
65
+ end
66
+
67
+ parsed_json
68
+ end
69
+
16
70
  # Configurable prompts root (defaults to Rails.root/app/prompts when Rails is present)
17
71
  def self.prompts_path
18
72
  @prompts_path || default_prompts_path
@@ -43,12 +97,34 @@ module Promptly
43
97
  end
44
98
 
45
99
  private_class_method def self.render_without_cache(identifier, locale: nil, locals: {})
100
+ schema_path = File.join(prompts_path, "#{identifier}.schema.yml")
101
+ Validator.validate!(locals, schema_path)
102
+
46
103
  path = Locator.resolve(identifier, locale: locale)
47
104
  raise Error, "Template not found for '#{identifier}' (locale: #{locale.inspect}) under #{prompts_path}" unless path
48
105
 
49
106
  engine = Locator.engine_for(path)
50
- template = File.read(path)
51
- Renderer.render(template, locals: locals, engine: engine)
107
+ file_content = File.read(path)
108
+
109
+ # Extract YAML front matter
110
+ match = file_content.match(/\A---\n(.*)
111
+ ---\s*\n/m)
112
+ if match
113
+ metadata = YAML.safe_load(match[1])
114
+ template = match.post_match
115
+ else
116
+ metadata = {}
117
+ template = file_content
118
+ end
119
+
120
+ content = Renderer.render(template, locals: locals, engine: engine)
121
+
122
+ Prompt.new(
123
+ content: content,
124
+ version: metadata["version"],
125
+ author: metadata["author"],
126
+ change_notes: metadata["change_notes"]
127
+ )
52
128
  end
53
129
 
54
130
  def self.default_prompts_path
@@ -0,0 +1,63 @@
1
+ ## Configuration
2
+
3
+ ### Custom Prompts Path
4
+
5
+ ```ruby
6
+ # config/initializers/rails_ai_prompts.rb
7
+ Promptly.prompts_path = Rails.root.join("lib", "ai_prompts")
8
+ ```
9
+
10
+ ### Caching
11
+
12
+ Promptly supports optional caching for rendered prompts.
13
+
14
+ - Default: enabled, TTL = 3600 seconds (1 hour).
15
+ - In Rails, the Railtie auto-uses `Rails.cache` if present.
16
+
17
+ Configure globally:
18
+
19
+ ```ruby
20
+ # config/initializers/promptly.rb
21
+ Promptly::Cache.configure do |c|
22
+ c.store = Rails.cache # or any ActiveSupport::Cache store
23
+ c.ttl = 3600 # default TTL in seconds
24
+ c.enabled = true # globally enable/disable caching
25
+ end
26
+ ```
27
+
28
+ Per-call options:
29
+
30
+ ```ruby
31
+ # Bypass cache for this render only
32
+ Promptly.render("user_onboarding/welcome_email", locals: {...}, cache: false)
33
+
34
+ # Custom TTL for this render only
35
+ Promptly.render("user_onboarding/welcome_email", locals: {...}, ttl: 5.minutes)
36
+ ```
37
+
38
+ Invalidation:
39
+
40
+ ```ruby
41
+ # Clear entire cache store (if supported by the store)
42
+ Promptly::Cache.clear
43
+
44
+ # Delete a specific cached entry
45
+ Promptly::Cache.delete(
46
+ identifier: "user_onboarding/welcome_email",
47
+ locale: :en,
48
+ locals: {name: "John"},
49
+ prompts_path: Promptly.prompts_path
50
+ )
51
+ ```
52
+
53
+ ### Direct Template Rendering
54
+
55
+ ```ruby
56
+ # Render ERB directly (without file lookup)
57
+ template = "Hello <%= name %>, welcome to <%= app %>!"
58
+ output = Promptly.render_template(template, locals: {name: "John", app: "MyApp"})
59
+
60
+ # Render Liquid directly
61
+ template = "Hello {{ name }}, welcome to {{ app }}!"
62
+ output = Promptly.render_template(template, locals: {name: "John", app: "MyApp"}, engine: :liquid)
63
+ ```
@@ -0,0 +1,36 @@
1
+ ## Functional Prompt Tests
2
+
3
+ Promptly provides an RSpec helper to write functional tests for your prompts. This allows you to verify the rendered output of your prompts, ensuring that they are correctly formatted and that all variables are properly interpolated.
4
+
5
+ ### RSpec Helper
6
+
7
+ The `expect_prompt_render` helper is available in your RSpec tests. It takes the prompt identifier and a hash of locals as arguments.
8
+
9
+ **Example:**
10
+
11
+ ```ruby
12
+ # spec/prompts/user_onboarding/welcome_email_spec.rb
13
+ require "spec_helper"
14
+
15
+ RSpec.describe "user_onboarding/welcome_email" do
16
+ it "renders the welcome email correctly" do
17
+ locals = {
18
+ name: "John Doe",
19
+ app_name: "My App",
20
+ user_role: "Admin",
21
+ features: ["Feature 1", "Feature 2"],
22
+ days_since_signup: 5
23
+ }
24
+
25
+ expect(Promptly.render("user_onboarding/welcome_email", locals: locals)).to include("Hello John Doe")
26
+ end
27
+ end
28
+ ```
29
+
30
+ ### Rake Task
31
+
32
+ You can run all your prompt tests using the `ai_prompts:test_prompts` Rake task.
33
+
34
+ ```bash
35
+ rake ai_prompts:test_prompts
36
+ ```
@@ -0,0 +1,32 @@
1
+ ## Generators
2
+
3
+ Create prompt templates following conventions.
4
+
5
+ ```bash
6
+ # ERB with multiple locales
7
+ rails g promptly:prompt user_onboarding/welcome_email --locales en es --engine erb
8
+
9
+ # Liquid with a single locale
10
+ rails g promptly:prompt ai_coaching/goal_review --locales en --engine liquid
11
+
12
+ # Fallback-only (no locale suffix)
13
+ rails g promptly:prompt content_generation/outline --no-locale
14
+ ```
15
+
16
+ Options:
17
+
18
+ - `--engine` erb|liquid (default: erb)
19
+ - `--locales` space-separated list (default: I18n.available_locales if available, else `en`)
20
+ - `--no-locale` create only fallback file (e.g., `welcome_email.erb`)
21
+ - `--force` overwrite existing files
22
+
23
+ Generated files are placed under `app/prompts/` and directories are created as needed.
24
+
25
+ Examples:
26
+
27
+ - `app/prompts/user_onboarding/welcome_email.en.erb`
28
+ - `app/prompts/user_onboarding/welcome_email.es.erb`
29
+ - `app/prompts/ai_coaching/goal_review.en.liquid`
30
+ - `app/prompts/content_generation/outline.erb` (fallback-only)
31
+
32
+ The generator seeds a minimal, intention-revealing scaffold you can edit immediately.
@@ -0,0 +1,25 @@
1
+ ## Helper: render_prompt
2
+
3
+ Use a concise helper anywhere in Rails to render prompts with locals that are also exposed as instance variables inside ERB templates.
4
+
5
+ * **Auto-included**: Controllers, Mailers, and Jobs via Railtie.
6
+ * **Services/Plain Ruby**: `include Promptly::Helper`.
7
+
8
+ Example template and usage:
9
+
10
+ ```erb
11
+ # app/prompts/welcome_email.erb
12
+ Hello <%= @user.name %>, welcome to our service!
13
+ We're excited to have you join.
14
+ ```
15
+
16
+ ```ruby
17
+ # In a mailer, job, controller, or a service that includes Promptly::Helper
18
+ rendered = render_prompt("welcome_email", user: @user)
19
+ ```
20
+
21
+ Notes:
22
+
23
+ - **Locals become @instance variables** in ERB. Passing `user: @user` makes `@user` available in the template.
24
+ - **Localization**: `render_prompt("welcome_email", locale: :es, user: user)` resolves `welcome_email.es.erb` with fallback per `Promptly::Locator`.
25
+ - **Caching**: Controlled per call (`cache:`, `ttl:`) and globally via `Promptly::Cache`.
data/wiki/Home.md ADDED
@@ -0,0 +1,29 @@
1
+ # Welcome to the Promptly Wiki!
2
+
3
+ Promptly is an opinionated Rails integration for reusable AI prompt templates. It helps you build maintainable, localized, and testable AI prompts using ERB or Liquid templates with Rails conventions.
4
+
5
+ This wiki provides detailed documentation for Promptly. If you are new to Promptly, we recommend starting with the [Quick Start](https://github.com/wilburhimself/promptly/wiki/Quick-Start) guide.
6
+
7
+ ## Features
8
+
9
+ - **Template rendering**: ERB (via ActionView) and optional Liquid support
10
+ - **I18n integration**: Automatic locale fallback (`welcome.es.erb` → `welcome.en.erb` → `welcome.erb`)
11
+ - **Rails conventions**: Store prompts in `app/prompts/` with organized subdirectories
12
+ - **Render & CLI**: Test prompts in Rails console or via rake tasks
13
+ - **Minimal setup**: Auto-loads via Railtie, zero configuration required
14
+ - **Prompt caching**: Configurable cache store, TTL, and cache-bypass options
15
+ - **Schema Validation**: Ensure all locals passed to templates match a defined schema.
16
+ - **Functional Prompt Tests**: Write functional tests for your prompts using RSpec.
17
+
18
+ ## Documentation
19
+
20
+ - [Quick Start](https://github.com/wilburhimself/promptly/wiki/Quick-Start)
21
+ - [Schema Validation](https://github.com/wilburhimself/promptly/wiki/Schema-Validation)
22
+ - [Helper: render_prompt](https://github.com/wilburhimself/promptly/wiki/Helper-render_prompt)
23
+ - [Rails App Integration](https://github.com/wilburhimself/promptly/wiki/Rails-App-Integration)
24
+ - [I18n Prompts Usage](https://github.com/wilburhimself/promptly/wiki/I18n-Prompts-Usage)
25
+ - [Liquid Templates](https://github.com/wilburhimself/promptly/wiki/Liquid-Templates)
26
+ - [Configuration](https://github.com/wilburhimself/promptly/wiki/Configuration)
27
+ - [Generators](https://github.com/wilburhimself/promptly/wiki/Generators)
28
+ - [Linting Templates](https://github.com/wilburhimself/promptly/wiki/Linting-Templates)
29
+ - [Functional Prompt Tests](https://github.com/wilburhimself/promptly/wiki/Functional-Prompt-Tests)
@@ -0,0 +1,60 @@
1
+ ## I18n Prompts Usage
2
+
3
+ ### Directory Structure
4
+
5
+ ```
6
+ app/prompts/
7
+ ├── user_onboarding/
8
+ │ ├── welcome_email.en.erb # English AI prompt
9
+ │ ├── welcome_email.es.erb # Spanish AI prompt
10
+ │ └── onboarding_checklist.erb # Fallback (any locale)
11
+ ├── content_generation/
12
+ │ ├── blog_post_outline.en.erb
13
+ │ ├── social_media_post.es.erb
14
+ │ └── product_description.erb
15
+ └── ai_coaching/
16
+ ├── goal_review.en.liquid # Liquid AI prompt
17
+ └── goal_review.es.liquid
18
+ ```
19
+
20
+ ### Locale Resolution
21
+
22
+ Promptly follows this resolution order:
23
+
24
+ 1. **Requested locale**: `welcome.es.erb` (if `locale: :es` specified)
25
+ 2. **Default locale**: `welcome.en.erb` (if `I18n.default_locale == :en`)
26
+ 3. **Fallback**: `welcome.erb` (no locale suffix)
27
+
28
+ ```ruby
29
+ # Configure I18n in your Rails app
30
+ # config/application.rb
31
+ config.i18n.default_locale = :en
32
+ config.i18n.available_locales = [:en, :es, :fr]
33
+
34
+ # Usage examples
35
+ I18n.locale = :es
36
+ I18n.default_locale = :en
37
+
38
+ # Will try: welcome_email.es.erb → welcome_email.en.erb → welcome_email.erb
39
+ prompt = Promptly.render(
40
+ "user_onboarding/welcome_email",
41
+ locals: {
42
+ name: "María García",
43
+ app_name: "ProjectHub",
44
+ user_role: "Manager",
45
+ features: ["Team management", "Analytics", "Reporting"],
46
+ days_since_signup: 1
47
+ }
48
+ )
49
+
50
+ # Force specific locale for AI prompt generation
51
+ prompt = Promptly.render(
52
+ "content_generation/blog_post_outline",
53
+ locale: :fr,
54
+ locals: {
55
+ topic: "Intelligence Artificielle",
56
+ target_audience: "Développeurs",
57
+ word_count: 1500
58
+ }
59
+ )
60
+ ```
@@ -0,0 +1,38 @@
1
+ ## Linting Templates
2
+
3
+ Validate your prompt templates from the CLI.
4
+
5
+ ```bash
6
+ # Lint all templates under the prompts path
7
+ rake ai_prompts:lint
8
+
9
+ # Lint a specific identifier (path without locale/ext)
10
+ rake ai_prompts:lint[user_onboarding/welcome_email]
11
+
12
+ # Specify locales to check for coverage
13
+ LOCALES=en,es rake ai_prompts:lint
14
+
15
+ # Require placeholders to exist in templates
16
+ REQUIRED=name,app_name rake ai_prompts:lint[user_onboarding/welcome_email]
17
+
18
+ # Point to a custom prompts directory
19
+ PROMPTS_PATH=lib/ai_prompts rake ai_prompts:lint
20
+ ```
21
+
22
+ What it checks:
23
+
24
+ - **Syntax errors**
25
+ - ERB: compiles with `ERB.new` (no execution)
26
+ - Liquid: parses with `Liquid::Template.parse` (if `liquid` gem present)
27
+ - **Missing locale files**
28
+ - For each identifier, warns when required locales are missing
29
+ - Locales source: `LOCALES` env or `I18n.available_locales`
30
+ - **Required placeholders**
31
+ - Best-effort scan for required keys from `REQUIRED` env
32
+ - ERB: looks for `<%= ... @key ... %>` or `<%= ... key ... %>` usage
33
+ - Liquid: looks for `{{ key }}` usage
34
+
35
+ Exit codes:
36
+
37
+ - `0` when all checks pass
38
+ - `1` when errors are found (syntax or missing required placeholders)
@@ -0,0 +1,49 @@
1
+ ### Liquid Templates
2
+
3
+ For more complex templating needs, use Liquid:
4
+
5
+ ```liquid
6
+ <!-- app/prompts/ai_coaching/goal_review.en.liquid -->
7
+ You are an experienced life coach conducting a goal review session.
8
+
9
+ Context:
10
+ - Client name: {{ user_name }}
11
+ - Goals being reviewed: {% for goal in current_goals %}{{ goal }}{% unless forloop.last %}, {% endunless %}{% endfor %}
12
+ - Recent progress: {{ progress_summary }}
13
+ - Current challenges: {% for challenge in challenges %}{{ challenge }}{% unless forloop.last %}, {% endunless %}{% endfor %}
14
+ - Review period: {{ review_period | default: "monthly" }}
15
+
16
+ Task: Provide a personalized goal review that:
17
+ 1. Acknowledges their progress and celebrates wins
18
+ 2. Addresses each challenge with specific, actionable advice
19
+ 3. Suggests 2-3 concrete next steps for the coming {{ review_period }}
20
+ 4. Asks 1-2 thoughtful questions to help them reflect
21
+ 5. Maintains an encouraging but realistic tone
22
+
23
+ {% if current_goals.size > 5 %}
24
+ Note: The client has many goals. Help them prioritize the most important ones.
25
+ {% endif %}
26
+
27
+ Format your response as a conversational coaching session, not a formal report.
28
+ ```
29
+
30
+ ```ruby
31
+ # Generate AI coaching content with Liquid template
32
+ prompt = Promptly.render(
33
+ "ai_coaching/goal_review",
34
+ locale: :en,
35
+ locals: {
36
+ user_name: "Alex",
37
+ current_goals: ["Run 5K under 25min", "Gym 3x/week", "Read 12 books/year"],
38
+ progress_summary: "Consistent with gym, behind on running pace, ahead on reading",
39
+ challenges: ["Time management", "Motivation on rainy days"],
40
+ review_period: "monthly"
41
+ }
42
+ )
43
+
44
+ # Send to AI service for personalized coaching
45
+ ai_coaching_session = openai_client.chat(
46
+ model: "gpt-4",
47
+ messages: [{role: "user", content: prompt}]
48
+ ).dig("choices", 0, "message", "content")
49
+ ```
@@ -0,0 +1,34 @@
1
+ ## Prompt Version Metadata
2
+
3
+ Promptly allows you to add optional metadata fields to your prompt templates, such as `version`, `author`, and `change_notes`. This metadata can be useful for tracking changes to your prompts and for documentation purposes.
4
+
5
+ ### Adding Metadata to Prompts
6
+
7
+ To add metadata to a prompt, include a YAML front matter block at the beginning of your template file. The front matter block must start and end with `---`.
8
+
9
+ **Example:**
10
+
11
+ ```erb
12
+ ---
13
+ version: 1.0
14
+ author: John Doe
15
+ change_notes: Initial version of the welcome email.
16
+ ---
17
+ You are a friendly customer success manager writing a personalized welcome email.
18
+ ...
19
+ ```
20
+
21
+ ### Accessing Metadata
22
+
23
+ When you render a prompt, the `Promptly.render` method returns a `Prompt` object. This object contains the rendered content of the prompt, as well as the metadata fields.
24
+
25
+ **Example:**
26
+
27
+ ```ruby
28
+ prompt = Promptly.render("user_onboarding/welcome_email")
29
+
30
+ puts prompt.content # The rendered content of the email
31
+ puts prompt.version # 1.0
32
+ puts prompt.author # John Doe
33
+ puts prompt.change_notes # Initial version of the welcome email.
34
+ ```
@@ -0,0 +1,116 @@
1
+ ## Quick Start
2
+
3
+ ### 1. Create prompt templates
4
+
5
+ Create `app/prompts/user_onboarding/welcome_email.en.erb`:
6
+
7
+ ```erb
8
+ You are a friendly customer success manager writing a personalized welcome email.
9
+
10
+ Context:
11
+ - User name: <%= name %>
12
+ - App name: <%= app_name %>
13
+ - User's role: <%= user_role %>
14
+ - Available features for this user: <%= features.join(", ") %>
15
+ - User signed up <%= days_since_signup %> days ago
16
+
17
+ Task: Write a warm, personalized welcome email that:
18
+ 1. Addresses the user by name
19
+ 2. Explains the key benefits specific to their role
20
+ 3. Highlights 2-3 most relevant features they should try first
21
+ 4. Includes a clear call-to-action to get started
22
+ 5. Maintains a professional but friendly tone
23
+
24
+ Keep the email concise (under 200 words) and actionable.
25
+ ```
26
+
27
+ Create `app/prompts/user_onboarding/welcome_email.es.erb`:
28
+
29
+ ```erb
30
+ Eres un gerente de éxito del cliente amigable escribiendo un email de bienvenida personalizado.
31
+
32
+ Contexto:
33
+ - Nombre del usuario: <%= name %>
34
+ - Nombre de la app: <%= app_name %>
35
+ - Rol del usuario: <%= user_role %>
36
+ - Funciones disponibles para este usuario: <%= features.join(", ") %>
37
+ - El usuario se registró hace <%= days_since_signup %> días
38
+
39
+ Tarea: Escribe un email de bienvenida cálido y personalizado que:
40
+ 1. Se dirija al usuario por su nombre
41
+ 2. Explique los beneficios clave específicos para su rol
42
+ 3. Destaque 2-3 funciones más relevantes que debería probar primero
43
+ 4. Incluya una llamada a la acción clara para comenzar
44
+ 5. Mantenga un tono profesional pero amigable
45
+
46
+ Mantén el email conciso (menos de 200 palabras) y orientado a la acción.
47
+ ```
48
+
49
+ ### 2. Render in your Rails app
50
+
51
+ ```ruby
52
+ # In a controller, service, or anywhere in Rails
53
+ prompt = Promptly.render(
54
+ "user_onboarding/welcome_email",
55
+ locale: :es,
56
+ locals: {
57
+ name: "María García",
58
+ app_name: "ProjectHub",
59
+ user_role: "Team Lead",
60
+ features: ["Create projects", "Invite team members", "Track progress", "Generate reports"],
61
+ days_since_signup: 2
62
+ }
63
+ )
64
+
65
+ # Send to your AI service (OpenAI, Anthropic, etc.)
66
+ ai_response = openai_client.completions(
67
+ model: "gpt-4",
68
+ messages: [{role: "user", content: prompt}]
69
+ )
70
+
71
+ puts ai_response.dig("choices", 0, "message", "content")
72
+ # => AI-generated personalized welcome email in Spanish
73
+ ```
74
+
75
+ ### 3. Test via Rails console
76
+
77
+ ```ruby
78
+ rails console
79
+
80
+ # Render the prompt before sending to AI
81
+ prompt = Promptly.render(
82
+ "user_onboarding/welcome_email",
83
+ locale: :en,
84
+ locals: {
85
+ name: "John Smith",
86
+ app_name: "ProjectHub",
87
+ user_role: "Developer",
88
+ features: ["API access", "Code reviews", "Deployment tools"],
89
+ days_since_signup: 1
90
+ }
91
+ )
92
+ puts prompt
93
+
94
+ # Uses I18n.locale by default
95
+ I18n.locale = :es
96
+ prompt = Promptly.render(
97
+ "user_onboarding/welcome_email",
98
+ locals: {
99
+ name: "María García",
100
+ app_name: "ProjectHub",
101
+ user_role: "Team Lead",
102
+ features: ["Crear proyectos", "Invitar miembros", "Seguimiento"],
103
+ days_since_signup: 3
104
+ }
105
+ )
106
+ ```
107
+
108
+ ### 4. CLI rendering
109
+
110
+ ```bash
111
+ # Render specific locale (shows the prompt, not AI output)
112
+ rails ai_prompts:render[user_onboarding/welcome_email,es]
113
+
114
+ # Uses default locale
115
+ rails ai_prompts:render[user_onboarding/welcome_email]
116
+ ```