promptly 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a2623e7a763b98955af565cb861e43980b214fb22d41dd88ab0b62f5055d0cb5
4
+ data.tar.gz: ccbfe0e62c1f64a64fee853adcdd65696d94b54405ef11a3785df8e30ab70657
5
+ SHA512:
6
+ metadata.gz: 816567532c493a580a3271722b0dba4ad950dca6096a8213dfe6312e032e4d86efd8119eff1cc8a96a0b715eddfcafe2b5d346cb171f726f95a1c76e1df5dfdd
7
+ data.tar.gz: 0abd4d2331934ff0d9e0684a11eac7f5b15458f18eed11a56c91e364ca6cad08d530b3a233d8673562888a504608896197f98dd546ca5baa624a2fe84c7f339e
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ Parallel: true
2
+ Fix: true
3
+ Format: progress
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,447 @@
1
+ # Promptly
2
+
3
+ Opinionated Rails integration for reusable AI prompt templates. Build maintainable, localized, and testable AI prompts using ERB or Liquid templates with Rails conventions.
4
+
5
+ ## Features
6
+
7
+ - **Template rendering**: ERB (via ActionView) and optional Liquid support
8
+ - **I18n integration**: Automatic locale fallback (`welcome.es.erb` → `welcome.en.erb` → `welcome.erb`)
9
+ - **Rails conventions**: Store prompts in `app/prompts/` with organized subdirectories
10
+ - **Preview & CLI**: Test prompts in Rails console or via rake tasks
11
+ - **Minimal setup**: Auto-loads via Railtie, zero configuration required
12
+
13
+ ## Install
14
+
15
+ Add to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "promptly"
19
+ ```
20
+
21
+ For Liquid template support, also add:
22
+
23
+ ```ruby
24
+ gem "liquid"
25
+ ```
26
+
27
+ Then run:
28
+
29
+ ```bash
30
+ bundle install
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ### 1. Create prompt templates
36
+
37
+ Create `app/prompts/user_onboarding/welcome_email.en.erb`:
38
+
39
+ ```erb
40
+ You are a friendly customer success manager writing a personalized welcome email.
41
+
42
+ Context:
43
+ - User name: <%= name %>
44
+ - App name: <%= app_name %>
45
+ - User's role: <%= user_role %>
46
+ - Available features for this user: <%= features.join(", ") %>
47
+ - User signed up <%= days_since_signup %> days ago
48
+
49
+ Task: Write a warm, personalized welcome email that:
50
+ 1. Addresses the user by name
51
+ 2. Explains the key benefits specific to their role
52
+ 3. Highlights 2-3 most relevant features they should try first
53
+ 4. Includes a clear call-to-action to get started
54
+ 5. Maintains a professional but friendly tone
55
+
56
+ Keep the email concise (under 200 words) and actionable.
57
+ ```
58
+
59
+ Create `app/prompts/user_onboarding/welcome_email.es.erb`:
60
+
61
+ ```erb
62
+ Eres un gerente de éxito del cliente amigable escribiendo un email de bienvenida personalizado.
63
+
64
+ Contexto:
65
+ - Nombre del usuario: <%= name %>
66
+ - Nombre de la app: <%= app_name %>
67
+ - Rol del usuario: <%= user_role %>
68
+ - Funciones disponibles para este usuario: <%= features.join(", ") %>
69
+ - El usuario se registró hace <%= days_since_signup %> días
70
+
71
+ Tarea: Escribe un email de bienvenida cálido y personalizado que:
72
+ 1. Se dirija al usuario por su nombre
73
+ 2. Explique los beneficios clave específicos para su rol
74
+ 3. Destaque 2-3 funciones más relevantes que debería probar primero
75
+ 4. Incluya una llamada a la acción clara para comenzar
76
+ 5. Mantenga un tono profesional pero amigable
77
+
78
+ Mantén el email conciso (menos de 200 palabras) y orientado a la acción.
79
+ ```
80
+
81
+ ### 2. Render in your Rails app
82
+
83
+ ```ruby
84
+ # In a controller, service, or anywhere in Rails
85
+ prompt = Promptly.preview(
86
+ "user_onboarding/welcome_email",
87
+ locale: :es,
88
+ locals: {
89
+ name: "María García",
90
+ app_name: "ProjectHub",
91
+ user_role: "Team Lead",
92
+ features: ["Create projects", "Invite team members", "Track progress", "Generate reports"],
93
+ days_since_signup: 2
94
+ }
95
+ )
96
+
97
+ # Send to your AI service (OpenAI, Anthropic, etc.)
98
+ ai_response = openai_client.completions(
99
+ model: "gpt-4",
100
+ messages: [{role: "user", content: prompt}]
101
+ )
102
+
103
+ puts ai_response.dig("choices", 0, "message", "content")
104
+ # => AI-generated personalized welcome email in Spanish
105
+ ```
106
+
107
+ ### 3. Test via Rails console
108
+
109
+ ```ruby
110
+ rails console
111
+
112
+ # Preview the prompt before sending to AI
113
+ prompt = Promptly.preview(
114
+ "user_onboarding/welcome_email",
115
+ locale: :en,
116
+ locals: {
117
+ name: "John Smith",
118
+ app_name: "ProjectHub",
119
+ user_role: "Developer",
120
+ features: ["API access", "Code reviews", "Deployment tools"],
121
+ days_since_signup: 1
122
+ }
123
+ )
124
+ puts prompt
125
+
126
+ # Uses I18n.locale by default
127
+ I18n.locale = :es
128
+ prompt = Promptly.preview(
129
+ "user_onboarding/welcome_email",
130
+ locals: {
131
+ name: "María García",
132
+ app_name: "ProjectHub",
133
+ user_role: "Team Lead",
134
+ features: ["Crear proyectos", "Invitar miembros", "Seguimiento"],
135
+ days_since_signup: 3
136
+ }
137
+ )
138
+ ```
139
+
140
+ ### 4. CLI preview
141
+
142
+ ```bash
143
+ # Preview specific locale (shows the prompt, not AI output)
144
+ rails ai_prompts:preview[user_onboarding/welcome_email,es]
145
+
146
+ # Uses default locale
147
+ rails ai_prompts:preview[user_onboarding/welcome_email]
148
+ ```
149
+
150
+ ## Rails App Integration
151
+
152
+ ### Service Object Pattern
153
+
154
+ ```ruby
155
+ # app/services/ai_prompt_service.rb
156
+ class AiPromptService
157
+ def self.generate_welcome_email(user, locale: I18n.locale)
158
+ prompt = Promptly.preview(
159
+ "user_onboarding/welcome_email",
160
+ locale: locale,
161
+ locals: {
162
+ name: user.full_name,
163
+ app_name: Rails.application.class.module_parent_name,
164
+ user_role: user.role.humanize,
165
+ features: available_features_for(user),
166
+ days_since_signup: (Date.current - user.created_at.to_date).to_i
167
+ }
168
+ )
169
+
170
+ # Send to AI service and return generated content
171
+ openai_client.chat(
172
+ model: "gpt-4",
173
+ messages: [{role: "user", content: prompt}]
174
+ ).dig("choices", 0, "message", "content")
175
+ end
176
+
177
+ private
178
+
179
+ def self.available_features_for(user)
180
+ # Return features based on user's plan, role, etc.
181
+ case user.plan
182
+ when "basic"
183
+ ["Create projects", "Basic reporting"]
184
+ when "pro"
185
+ ["Create projects", "Team collaboration", "Advanced analytics", "API access"]
186
+ else
187
+ ["Create projects"]
188
+ end
189
+ end
190
+
191
+ def self.openai_client
192
+ @openai_client ||= OpenAI::Client.new(access_token: Rails.application.credentials.openai_api_key)
193
+ end
194
+ end
195
+ ```
196
+
197
+ ### Mailer Integration
198
+
199
+ ```ruby
200
+ # app/mailers/user_mailer.rb
201
+ class UserMailer < ApplicationMailer
202
+ def welcome_email(user)
203
+ @user = user
204
+ @ai_content = AiPromptService.generate_welcome_email(user, locale: user.locale)
205
+
206
+ mail(
207
+ to: user.email,
208
+ subject: t('mailer.welcome.subject')
209
+ )
210
+ end
211
+ end
212
+ ```
213
+
214
+ ### Background Job Usage
215
+
216
+ ```ruby
217
+ # app/jobs/generate_ai_content_job.rb
218
+ class GenerateAiContentJob < ApplicationJob
219
+ def perform(user_id, prompt_identifier, locals = {})
220
+ user = User.find(user_id)
221
+
222
+ prompt = Promptly.preview(
223
+ prompt_identifier,
224
+ locale: user.locale,
225
+ locals: locals.merge(
226
+ user_name: user.full_name,
227
+ user_role: user.role,
228
+ account_type: user.account_type
229
+ )
230
+ )
231
+
232
+ # Generate AI content
233
+ ai_response = openai_client.chat(
234
+ model: "gpt-4",
235
+ messages: [{role: "user", content: prompt}]
236
+ )
237
+
238
+ generated_content = ai_response.dig("choices", 0, "message", "content")
239
+
240
+ # Store or send the generated content
241
+ user.notifications.create!(
242
+ title: "AI Generated Content Ready",
243
+ content: generated_content,
244
+ notification_type: prompt_identifier.split('/').last
245
+ )
246
+ end
247
+
248
+ private
249
+
250
+ def openai_client
251
+ @openai_client ||= OpenAI::Client.new(access_token: Rails.application.credentials.openai_api_key)
252
+ end
253
+ end
254
+
255
+ # Usage
256
+ GenerateAiContentJob.perform_later(
257
+ user.id,
258
+ "coaching/goal_review",
259
+ {
260
+ current_goals: user.goals.active.pluck(:title),
261
+ progress_summary: "Made good progress on fitness goals",
262
+ challenges: ["Time management", "Consistency"]
263
+ }
264
+ )
265
+ ```
266
+
267
+ ## I18n Prompts Usage
268
+
269
+ ### Directory Structure
270
+
271
+ ```
272
+ app/prompts/
273
+ ├── user_onboarding/
274
+ │ ├── welcome_email.en.erb # English AI prompt
275
+ │ ├── welcome_email.es.erb # Spanish AI prompt
276
+ │ └── onboarding_checklist.erb # Fallback (any locale)
277
+ ├── content_generation/
278
+ │ ├── blog_post_outline.en.erb
279
+ │ ├── social_media_post.es.erb
280
+ │ └── product_description.erb
281
+ └── ai_coaching/
282
+ ├── goal_review.en.liquid # Liquid AI prompt
283
+ └── goal_review.es.liquid
284
+ ```
285
+
286
+ ### Locale Resolution
287
+
288
+ Promptly follows this resolution order:
289
+
290
+ 1. **Requested locale**: `welcome.es.erb` (if `locale: :es` specified)
291
+ 2. **Default locale**: `welcome.en.erb` (if `I18n.default_locale == :en`)
292
+ 3. **Fallback**: `welcome.erb` (no locale suffix)
293
+
294
+ ```ruby
295
+ # Configure I18n in your Rails app
296
+ # config/application.rb
297
+ config.i18n.default_locale = :en
298
+ config.i18n.available_locales = [:en, :es, :fr]
299
+
300
+ # Usage examples
301
+ I18n.locale = :es
302
+ I18n.default_locale = :en
303
+
304
+ # Will try: welcome_email.es.erb → welcome_email.en.erb → welcome_email.erb
305
+ prompt = Promptly.preview(
306
+ "user_onboarding/welcome_email",
307
+ locals: {
308
+ name: "María García",
309
+ app_name: "ProjectHub",
310
+ user_role: "Manager",
311
+ features: ["Team management", "Analytics", "Reporting"],
312
+ days_since_signup: 1
313
+ }
314
+ )
315
+
316
+ # Force specific locale for AI prompt generation
317
+ prompt = Promptly.preview(
318
+ "content_generation/blog_post_outline",
319
+ locale: :fr,
320
+ locals: {
321
+ topic: "Intelligence Artificielle",
322
+ target_audience: "Développeurs",
323
+ word_count: 1500
324
+ }
325
+ )
326
+ ```
327
+
328
+ ### Liquid Templates
329
+
330
+ For more complex templating needs, use Liquid:
331
+
332
+ ```liquid
333
+ <!-- app/prompts/ai_coaching/goal_review.en.liquid -->
334
+ You are an experienced life coach conducting a goal review session.
335
+
336
+ Context:
337
+ - Client name: {{ user_name }}
338
+ - Goals being reviewed: {% for goal in current_goals %}{{ goal }}{% unless forloop.last %}, {% endunless %}{% endfor %}
339
+ - Recent progress: {{ progress_summary }}
340
+ - Current challenges: {% for challenge in challenges %}{{ challenge }}{% unless forloop.last %}, {% endunless %}{% endfor %}
341
+ - Review period: {{ review_period | default: "monthly" }}
342
+
343
+ Task: Provide a personalized goal review that:
344
+ 1. Acknowledges their progress and celebrates wins
345
+ 2. Addresses each challenge with specific, actionable advice
346
+ 3. Suggests 2-3 concrete next steps for the coming {{ review_period }}
347
+ 4. Asks 1-2 thoughtful questions to help them reflect
348
+ 5. Maintains an encouraging but realistic tone
349
+
350
+ {% if current_goals.size > 5 %}
351
+ Note: The client has many goals. Help them prioritize the most important ones.
352
+ {% endif %}
353
+
354
+ Format your response as a conversational coaching session, not a formal report.
355
+ ```
356
+
357
+ ```ruby
358
+ # Generate AI coaching content with Liquid template
359
+ prompt = Promptly.preview(
360
+ "ai_coaching/goal_review",
361
+ locale: :en,
362
+ locals: {
363
+ user_name: "Alex",
364
+ current_goals: ["Run 5K under 25min", "Gym 3x/week", "Read 12 books/year"],
365
+ progress_summary: "Consistent with gym, behind on running pace, ahead on reading",
366
+ challenges: ["Time management", "Motivation on rainy days"],
367
+ review_period: "monthly"
368
+ }
369
+ )
370
+
371
+ # Send to AI service for personalized coaching
372
+ ai_coaching_session = openai_client.chat(
373
+ model: "gpt-4",
374
+ messages: [{role: "user", content: prompt}]
375
+ ).dig("choices", 0, "message", "content")
376
+ ```
377
+
378
+ ## Configuration
379
+
380
+ ### Custom Prompts Path
381
+
382
+ ```ruby
383
+ # config/initializers/rails_ai_prompts.rb
384
+ Promptly.prompts_path = Rails.root.join("lib", "ai_prompts")
385
+ ```
386
+
387
+ ### Direct Template Rendering
388
+
389
+ ```ruby
390
+ # Render ERB directly (without file lookup)
391
+ template = "Hello <%= name %>, welcome to <%= app %>!"
392
+ output = Promptly.render(template, locals: {name: "John", app: "MyApp"})
393
+
394
+ # Render Liquid directly
395
+ template = "Hello {{ name }}, welcome to {{ app }}!"
396
+ output = Promptly.render(template, locals: {name: "John", app: "MyApp"}, engine: :liquid)
397
+ ```
398
+
399
+ ## API Reference
400
+
401
+ ### `Promptly.preview(identifier, locale: nil, locals: {})`
402
+
403
+ Renders a template by identifier with locale fallback.
404
+
405
+ - **identifier**: Template path like `"user_onboarding/welcome"`
406
+ - **locale**: Specific locale (defaults to `I18n.locale`)
407
+ - **locals**: Hash of variables for template
408
+
409
+ ### `Promptly.render(template, locals: {}, engine: :erb)`
410
+
411
+ Renders template string directly.
412
+
413
+ - **template**: Template string
414
+ - **locals**: Hash of variables
415
+ - **engine**: `:erb` or `:liquid`
416
+
417
+ ### `Promptly.prompts_path`
418
+
419
+ Get/set the root directory for prompt templates (defaults to `Rails.root/app/prompts`).
420
+
421
+ ## Development
422
+
423
+ ```bash
424
+ # Install dependencies
425
+ bundle install
426
+
427
+ # Run tests
428
+ bundle exec rspec
429
+
430
+ # Run linter
431
+ bundle exec standardrb
432
+
433
+ # Build gem
434
+ rake build
435
+ ```
436
+
437
+ ## Contributing
438
+
439
+ 1. Fork it
440
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
441
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
442
+ 4. Push to the branch (`git push origin my-new-feature`)
443
+ 5. Create new Pull Request
444
+
445
+ ## License
446
+
447
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ desc "Run standardrb"
6
+ task :standard do
7
+ exec "bundle exec standardrb"
8
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Promptly
4
+ class Locator
5
+ SUPPORTED_EXTS = [".erb", ".liquid"].freeze
6
+
7
+ def self.prompts_path
8
+ Promptly.prompts_path
9
+ end
10
+
11
+ # identifier: e.g. "user_onboarding/welcome"
12
+ # locale: e.g. :en, :es
13
+ # returns absolute path to template file or nil
14
+ def self.resolve(identifier, locale: nil)
15
+ base = File.join(prompts_path, identifier)
16
+ requested_locale = (locale || (defined?(I18n) ? I18n.locale : nil))&.to_s
17
+ default_locale = (defined?(I18n) ? I18n.default_locale : nil)&.to_s
18
+
19
+ candidates = []
20
+ [requested_locale, default_locale, nil].compact.uniq.each do |loc|
21
+ if loc
22
+ SUPPORTED_EXTS.each do |ext|
23
+ candidates << "#{base}.#{loc}#{ext}"
24
+ end
25
+ end
26
+ end
27
+ SUPPORTED_EXTS.each do |ext|
28
+ candidates << "#{base}#{ext}"
29
+ end
30
+
31
+ candidates.find { |p| File.file?(p) }
32
+ end
33
+
34
+ # Choose engine based on file extension
35
+ def self.engine_for(path)
36
+ case File.extname(path)
37
+ when ".erb" then :erb
38
+ when ".liquid" then :liquid
39
+ else
40
+ :erb
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Promptly
4
+ class Railtie < Rails::Railtie
5
+ initializer "promptly.configure" do
6
+ # Intentionally minimal per DHH style; conventions over configuration
7
+ # Hook points will be added as features land.
8
+ Rails.logger.info("[promptly] loaded") if defined?(Rails.logger)
9
+ end
10
+
11
+ rake_tasks do
12
+ load "promptly/tasks/ai_prompts.rake"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+
5
+ module Promptly
6
+ class Renderer
7
+ def self.render(template, locals: {}, engine: :erb)
8
+ case engine.to_sym
9
+ when :erb
10
+ render_erb(template, locals)
11
+ when :liquid
12
+ render_liquid(template, locals)
13
+ else
14
+ raise ArgumentError, "Unsupported engine: #{engine} (use :erb or :liquid)"
15
+ end
16
+ end
17
+
18
+ def self.render_erb(template, locals)
19
+ view_class = if ActionView::Base.respond_to?(:with_empty_template_cache)
20
+ ActionView::Base.with_empty_template_cache
21
+ else
22
+ Class.new(ActionView::Base)
23
+ end
24
+
25
+ lookup = ActionView::LookupContext.new(ActionView::PathSet.new([]))
26
+ av = view_class.new(lookup, {}, nil)
27
+
28
+ av.render(inline: template, type: :erb, locals: locals || {})
29
+ end
30
+
31
+ def self.render_liquid(template, locals)
32
+ unless defined?(::Liquid)
33
+ raise LoadError, "Liquid is not available. Add `gem 'liquid'` to your Gemfile to use :liquid engine."
34
+ end
35
+
36
+ stringified = (locals || {}).each_with_object({}) do |(k, v), h|
37
+ h[k.to_s] = v
38
+ end
39
+
40
+ ::Liquid::Template.parse(template).render(stringified)
41
+ end
42
+
43
+ private_class_method :render_erb, :render_liquid
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ai_prompts do
4
+ desc "Preview a prompt: rake ai_prompts:preview[identifier,locale]"
5
+ task :preview, [:identifier, :locale] => :environment do |_, args|
6
+ identifier = args[:identifier]
7
+ locale = args[:locale]
8
+ prompts_path = ENV["PROMPTS_PATH"]
9
+
10
+ unless identifier
11
+ warn "Usage: rake ai_prompts:preview[identifier,locale]"
12
+ exit 1
13
+ end
14
+
15
+ begin
16
+ Promptly.prompts_path = prompts_path if prompts_path
17
+
18
+ output = Promptly.preview(identifier, locale: locale)
19
+ puts output
20
+ rescue Promptly::Error => e
21
+ warn "Error: #{e.class}: #{e.message}"
22
+ exit 1
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Promptly
4
+ VERSION = "0.1.0"
5
+ end
data/lib/promptly.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "promptly/version"
4
+ require_relative "promptly/renderer"
5
+ require_relative "promptly/locator"
6
+
7
+ module Promptly
8
+ class Error < StandardError; end
9
+
10
+ def self.render(template, locals: {}, engine: :erb)
11
+ Renderer.render(template, locals: locals, engine: engine)
12
+ end
13
+
14
+ # Configurable prompts root (defaults to Rails.root/app/prompts when Rails is present)
15
+ def self.prompts_path
16
+ @prompts_path || default_prompts_path
17
+ end
18
+
19
+ def self.prompts_path=(path)
20
+ @prompts_path = path
21
+ end
22
+
23
+ # Preview a template by identifier using locator rules
24
+ # identifier: "user_onboarding/welcome"
25
+ # locale: defaults to I18n.locale when available
26
+ def self.preview(identifier, locale: nil, locals: {})
27
+ path = Locator.resolve(identifier, locale: locale)
28
+ raise Error, "Template not found for '#{identifier}' (locale: #{locale.inspect}) under #{prompts_path}" unless path
29
+
30
+ engine = Locator.engine_for(path)
31
+ template = File.read(path)
32
+ Renderer.render(template, locals: locals, engine: engine)
33
+ end
34
+
35
+ def self.default_prompts_path
36
+ if defined?(::Rails) && Rails.respond_to?(:root) && Rails.root
37
+ File.join(Rails.root.to_s, "app", "prompts")
38
+ else
39
+ File.expand_path("app/prompts", Dir.pwd)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Auto-load Railtie when inside Rails
45
+ begin
46
+ require "rails/railtie"
47
+ require_relative "promptly/railtie"
48
+ rescue LoadError
49
+ # Rails not available; noop
50
+ end
data/promptly.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/promptly/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "promptly"
7
+ spec.version = Promptly::VERSION
8
+ spec.authors = ["Wilbur Suero"]
9
+ spec.email = ["wilbur@example.com"]
10
+
11
+ spec.summary = "Opinionated Rails integration for reusable AI prompt templates"
12
+ spec.description = "Build maintainable, localized, and testable AI prompts using ERB or Liquid templates with Rails conventions"
13
+ spec.homepage = "https://github.com/wilbursuero/promptly"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/wilbursuero/promptly"
21
+ spec.metadata["changelog_uri"] = "https://github.com/wilbursuero/promptly/blob/main/CHANGELOG.md"
22
+ spec.metadata["documentation_uri"] = "https://github.com/wilbursuero/promptly/blob/main/README.md"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ gemfiles = Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
29
+ end
30
+ end
31
+ spec.files = gemfiles
32
+
33
+ spec.bindir = "exe"
34
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ # Runtime dependencies
38
+ spec.add_dependency "actionview", "~> 7.0"
39
+
40
+ # Development dependencies
41
+ spec.add_development_dependency "rspec", "~> 3.12"
42
+ spec.add_development_dependency "standard", "~> 1.37"
43
+ spec.add_development_dependency "liquid", "~> 5.5"
44
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: promptly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wilbur Suero
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-08-16 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionview
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.12'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.12'
40
+ - !ruby/object:Gem::Dependency
41
+ name: standard
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.37'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.37'
54
+ - !ruby/object:Gem::Dependency
55
+ name: liquid
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.5'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5.5'
68
+ description: Build maintainable, localized, and testable AI prompts using ERB or Liquid
69
+ templates with Rails conventions
70
+ email:
71
+ - wilbur@example.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".rspec"
77
+ - ".standard.yml"
78
+ - LICENSE
79
+ - README.md
80
+ - Rakefile
81
+ - lib/promptly.rb
82
+ - lib/promptly/locator.rb
83
+ - lib/promptly/railtie.rb
84
+ - lib/promptly/renderer.rb
85
+ - lib/promptly/tasks/ai_prompts.rake
86
+ - lib/promptly/version.rb
87
+ - promptly.gemspec
88
+ homepage: https://github.com/wilbursuero/promptly
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ allowed_push_host: https://rubygems.org
93
+ homepage_uri: https://github.com/wilbursuero/promptly
94
+ source_code_uri: https://github.com/wilbursuero/promptly
95
+ changelog_uri: https://github.com/wilbursuero/promptly/blob/main/CHANGELOG.md
96
+ documentation_uri: https://github.com/wilbursuero/promptly/blob/main/README.md
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 3.0.0
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.6.2
112
+ specification_version: 4
113
+ summary: Opinionated Rails integration for reusable AI prompt templates
114
+ test_files: []