promptly 0.1.7 → 0.1.17

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55ee620ab3faf1e3c2d2ff44db3f28fe255a7379431c360f86b6f8dd87a8a752
4
- data.tar.gz: ddf1db22fc260b4007e374944833b4ffe08fd5bcab020a033a28956612a26bbc
3
+ metadata.gz: 8a23a4bcc5d7efe0f5c83ec8cd21d0badc3d6a09b6ab6be98b0b89178ee1f9ac
4
+ data.tar.gz: 5136d03f17b3c1372abc31146da3771bf719ea0a71869a1233685421f22ea9b1
5
5
  SHA512:
6
- metadata.gz: 0f343de865b5a2bef48f3a86cd131c40b8173be6183267fbd22242b228b668891ea191c7dba2597db373a7814bb30e05b8b8e4004cf345defef1d2e84c7b9b92
7
- data.tar.gz: 3e872512ba87aa8f387d851d75d378e7b9cba91a20bbc0c316598af80166a71d691751e3a00a1aefda20a132f88e5a9bb58414ba312760dbc02dacec590bc11e
6
+ metadata.gz: 42f82b2467f10f8851aada866267cd047a95dd99f592b50bbd0189ccddfd58ff619f0973f981f3f88be803bbc213ac3a2b645f2d9ed59a8bc0967c715ceb8030
7
+ data.tar.gz: 368c7a84f7215db1415a3d2165e6061917977ac924127c937ebf9e48db641d35f373dbb8df3f857145bb9e00881faa6b15048872ded0fc968a4922defad35337
data/README.md CHANGED
@@ -1,37 +1,3 @@
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.
33
-
34
- ## API Reference
35
1
  # Promptly
36
2
 
37
3
  Opinionated Rails integration for reusable AI prompt templates. Build maintainable, localized, and testable AI prompts using ERB or Liquid templates with Rails conventions.
@@ -44,6 +10,11 @@ Opinionated Rails integration for reusable AI prompt templates. Build maintainab
44
10
  - **Render & CLI**: Test prompts in Rails console or via rake tasks
45
11
  - **Minimal setup**: Auto-loads via Railtie, zero configuration required
46
12
  - **Prompt caching**: Configurable cache store, TTL, and cache-bypass options
13
+ - **Schema Validation**: Ensure all locals passed to templates match a defined schema.
14
+
15
+ ## Documentation
16
+
17
+ For detailed documentation, please visit the [Promptly Wiki](https://github.com/wilburhimself/promptly/wiki).
47
18
 
48
19
  ## Install
49
20
 
@@ -67,54 +38,6 @@ bundle install
67
38
 
68
39
  ## Quick Start
69
40
 
70
- ### 1. Create prompt templates
71
-
72
- Create `app/prompts/user_onboarding/welcome_email.en.erb`:
73
-
74
- ```erb
75
- You are a friendly customer success manager writing a personalized welcome email.
76
-
77
- Context:
78
- - User name: <%= name %>
79
- - App name: <%= app_name %>
80
- - User's role: <%= user_role %>
81
- - Available features for this user: <%= features.join(", ") %>
82
- - User signed up <%= days_since_signup %> days ago
83
-
84
- Task: Write a warm, personalized welcome email that:
85
- 1. Addresses the user by name
86
- 2. Explains the key benefits specific to their role
87
- 3. Highlights 2-3 most relevant features they should try first
88
- 4. Includes a clear call-to-action to get started
89
- 5. Maintains a professional but friendly tone
90
-
91
- Keep the email concise (under 200 words) and actionable.
92
- ```
93
-
94
- Create `app/prompts/user_onboarding/welcome_email.es.erb`:
95
-
96
- ```erb
97
- Eres un gerente de éxito del cliente amigable escribiendo un email de bienvenida personalizado.
98
-
99
- Contexto:
100
- - Nombre del usuario: <%= name %>
101
- - Nombre de la app: <%= app_name %>
102
- - Rol del usuario: <%= user_role %>
103
- - Funciones disponibles para este usuario: <%= features.join(", ") %>
104
- - El usuario se registró hace <%= days_since_signup %> días
105
-
106
- Tarea: Escribe un email de bienvenida cálido y personalizado que:
107
- 1. Se dirija al usuario por su nombre
108
- 2. Explique los beneficios clave específicos para su rol
109
- 3. Destaque 2-3 funciones más relevantes que debería probar primero
110
- 4. Incluya una llamada a la acción clara para comenzar
111
- 5. Mantenga un tono profesional pero amigable
112
-
113
- Mantén el email conciso (menos de 200 palabras) y orientado a la acción.
114
- ```
115
-
116
- ### 2. Render in your Rails app
117
-
118
41
  ```ruby
119
42
  # In a controller, service, or anywhere in Rails
120
43
  prompt = Promptly.render(
@@ -139,365 +62,38 @@ puts ai_response.dig("choices", 0, "message", "content")
139
62
  # => AI-generated personalized welcome email in Spanish
140
63
  ```
141
64
 
142
- ### 3. Test via Rails console
143
-
144
- ```ruby
145
- rails console
146
-
147
- # Render the prompt before sending to AI
148
- prompt = Promptly.render(
149
- "user_onboarding/welcome_email",
150
- locale: :en,
151
- locals: {
152
- name: "John Smith",
153
- app_name: "ProjectHub",
154
- user_role: "Developer",
155
- features: ["API access", "Code reviews", "Deployment tools"],
156
- days_since_signup: 1
157
- }
158
- )
159
- puts prompt
160
-
161
- # Uses I18n.locale by default
162
- I18n.locale = :es
163
- prompt = Promptly.render(
164
- "user_onboarding/welcome_email",
165
- locals: {
166
- name: "María García",
167
- app_name: "ProjectHub",
168
- user_role: "Team Lead",
169
- features: ["Crear proyectos", "Invitar miembros", "Seguimiento"],
170
- days_since_signup: 3
171
- }
172
- )
173
- ```
174
-
175
- ### 4. CLI rendering
176
-
177
- ```bash
178
- # Render specific locale (shows the prompt, not AI output)
179
- rails ai_prompts:render[user_onboarding/welcome_email,es]
180
-
181
- # Uses default locale
182
- rails ai_prompts:render[user_onboarding/welcome_email]
183
- ```
184
-
185
- ## Rails App Integration
186
-
187
- ### Service Object Pattern
188
-
189
- ```ruby
190
- # app/services/ai_prompt_service.rb
191
- class AiPromptService
192
- def self.generate_welcome_email(user, locale: I18n.locale)
193
- prompt = Promptly.render(
194
- "user_onboarding/welcome_email",
195
- locale: locale,
196
- locals: {
197
- name: user.full_name,
198
- app_name: Rails.application.class.module_parent_name,
199
- user_role: user.role.humanize,
200
- features: available_features_for(user),
201
- days_since_signup: (Date.current - user.created_at.to_date).to_i
202
- }
203
- )
204
-
205
- # Send to AI service and return generated content
206
- openai_client.chat(
207
- model: "gpt-4",
208
- messages: [{role: "user", content: prompt}]
209
- ).dig("choices", 0, "message", "content")
210
- end
211
-
212
- private
213
-
214
- def self.available_features_for(user)
215
- # Return features based on user's plan, role, etc.
216
- case user.plan
217
- when "basic"
218
- ["Create projects", "Basic reporting"]
219
- when "pro"
220
- ["Create projects", "Team collaboration", "Advanced analytics", "API access"]
221
- else
222
- ["Create projects"]
223
- end
224
- end
225
-
226
- def self.openai_client
227
- @openai_client ||= OpenAI::Client.new(access_token: Rails.application.credentials.openai_api_key)
228
- end
229
- end
230
- ```
231
-
232
- ### Mailer Integration
233
-
234
- ```ruby
235
- # app/mailers/user_mailer.rb
236
- class UserMailer < ApplicationMailer
237
- def welcome_email(user)
238
- @user = user
239
- @ai_content = AiPromptService.generate_welcome_email(user, locale: user.locale)
240
-
241
- mail(
242
- to: user.email,
243
- subject: t('mailer.welcome.subject')
244
- )
245
- end
246
- end
247
- ```
248
-
249
- ### Background Job Usage
250
-
251
- ```ruby
252
- # app/jobs/generate_ai_content_job.rb
253
- class GenerateAiContentJob < ApplicationJob
254
- def perform(user_id, prompt_identifier, locals = {})
255
- user = User.find(user_id)
256
-
257
- prompt = Promptly.render(
258
- prompt_identifier,
259
- locale: user.locale,
260
- locals: locals.merge(
261
- user_name: user.full_name,
262
- user_role: user.role,
263
- account_type: user.account_type
264
- )
265
- )
266
-
267
- # Generate AI content
268
- ai_response = openai_client.chat(
269
- model: "gpt-4",
270
- messages: [{role: "user", content: prompt}]
271
- )
272
-
273
- generated_content = ai_response.dig("choices", 0, "message", "content")
274
-
275
- # Store or send the generated content
276
- user.notifications.create!(
277
- title: "AI Generated Content Ready",
278
- content: generated_content,
279
- notification_type: prompt_identifier.split('/').last
280
- )
281
- end
282
-
283
- private
284
-
285
- def openai_client
286
- @openai_client ||= OpenAI::Client.new(access_token: Rails.application.credentials.openai_api_key)
287
- end
288
- end
289
-
290
- # Usage
291
- GenerateAiContentJob.perform_later(
292
- user.id,
293
- "coaching/goal_review",
294
- {
295
- current_goals: user.goals.active.pluck(:title),
296
- progress_summary: "Made good progress on fitness goals",
297
- challenges: ["Time management", "Consistency"]
298
- }
299
- )
300
- ```
301
-
302
- ## I18n Prompts Usage
303
-
304
- ### Directory Structure
65
+ ## Structured Outputs (Response Schema Validation)
305
66
 
306
- ```
307
- app/prompts/
308
- ├── user_onboarding/
309
- │ ├── welcome_email.en.erb # English AI prompt
310
- │ ├── welcome_email.es.erb # Spanish AI prompt
311
- │ └── onboarding_checklist.erb # Fallback (any locale)
312
- ├── content_generation/
313
- │ ├── blog_post_outline.en.erb
314
- │ ├── social_media_post.es.erb
315
- │ └── product_description.erb
316
- └── ai_coaching/
317
- ├── goal_review.en.liquid # Liquid AI prompt
318
- └── goal_review.es.liquid
319
- ```
67
+ Promptly supports OpenAI's structured outputs (`guided_json` style) by defining `.response.json` files alongside your templates.
320
68
 
321
- ### Locale Resolution
322
-
323
- Promptly follows this resolution order:
324
-
325
- 1. **Requested locale**: `welcome.es.erb` (if `locale: :es` specified)
326
- 2. **Default locale**: `welcome.en.erb` (if `I18n.default_locale == :en`)
327
- 3. **Fallback**: `welcome.erb` (no locale suffix)
69
+ For example, given an output schema `app/prompts/user_onboarding/welcome.response.json`, you can pass it directly to an AI service:
328
70
 
329
71
  ```ruby
330
- # Configure I18n in your Rails app
331
- # config/application.rb
332
- config.i18n.default_locale = :en
333
- config.i18n.available_locales = [:en, :es, :fr]
334
-
335
- # Usage examples
336
- I18n.locale = :es
337
- I18n.default_locale = :en
338
-
339
- # Will try: welcome_email.es.erb → welcome_email.en.erb → welcome_email.erb
340
- prompt = Promptly.render(
341
- "user_onboarding/welcome_email",
342
- locals: {
343
- name: "María García",
344
- app_name: "ProjectHub",
345
- user_role: "Manager",
346
- features: ["Team management", "Analytics", "Reporting"],
347
- days_since_signup: 1
348
- }
349
- )
350
-
351
- # Force specific locale for AI prompt generation
352
- prompt = Promptly.render(
353
- "content_generation/blog_post_outline",
354
- locale: :fr,
355
- locals: {
356
- topic: "Intelligence Artificielle",
357
- target_audience: "Développeurs",
358
- word_count: 1500
72
+ # Returns the schema wrapped in expected OpenAI format
73
+ response_format = Promptly.response_format("user_onboarding/welcome", strict: true)
74
+
75
+ ai_response = openai_client.chat(
76
+ parameters: {
77
+ model: "gpt-4o",
78
+ messages: [{role: "user", content: prompt}],
79
+ response_format: response_format
359
80
  }
360
81
  )
361
82
  ```
362
83
 
363
- ### Liquid Templates
364
-
365
- For more complex templating needs, use Liquid:
366
-
367
- ```liquid
368
- <!-- app/prompts/ai_coaching/goal_review.en.liquid -->
369
- You are an experienced life coach conducting a goal review session.
370
-
371
- Context:
372
- - Client name: {{ user_name }}
373
- - Goals being reviewed: {% for goal in current_goals %}{{ goal }}{% unless forloop.last %}, {% endunless %}{% endfor %}
374
- - Recent progress: {{ progress_summary }}
375
- - Current challenges: {% for challenge in challenges %}{{ challenge }}{% unless forloop.last %}, {% endunless %}{% endfor %}
376
- - Review period: {{ review_period | default: "monthly" }}
377
-
378
- Task: Provide a personalized goal review that:
379
- 1. Acknowledges their progress and celebrates wins
380
- 2. Addresses each challenge with specific, actionable advice
381
- 3. Suggests 2-3 concrete next steps for the coming {{ review_period }}
382
- 4. Asks 1-2 thoughtful questions to help them reflect
383
- 5. Maintains an encouraging but realistic tone
384
-
385
- {% if current_goals.size > 5 %}
386
- Note: The client has many goals. Help them prioritize the most important ones.
387
- {% endif %}
388
-
389
- Format your response as a conversational coaching session, not a formal report.
390
- ```
84
+ You can also natively validate the returned JSON string in Ruby to ensure it conforms exactly to the schema:
391
85
 
392
86
  ```ruby
393
- # Generate AI coaching content with Liquid template
394
- prompt = Promptly.render(
395
- "ai_coaching/goal_review",
396
- locale: :en,
397
- locals: {
398
- user_name: "Alex",
399
- current_goals: ["Run 5K under 25min", "Gym 3x/week", "Read 12 books/year"],
400
- progress_summary: "Consistent with gym, behind on running pace, ahead on reading",
401
- challenges: ["Time management", "Motivation on rainy days"],
402
- review_period: "monthly"
403
- }
404
- )
405
-
406
- # Send to AI service for personalized coaching
407
- ai_coaching_session = openai_client.chat(
408
- model: "gpt-4",
409
- messages: [{role: "user", content: prompt}]
410
- ).dig("choices", 0, "message", "content")
411
- ```
412
-
413
- ## Configuration
87
+ raw_json = ai_response.dig("choices", 0, "message", "content")
414
88
 
415
- ### Custom Prompts Path
416
-
417
- ```ruby
418
- # config/initializers/rails_ai_prompts.rb
419
- Promptly.prompts_path = Rails.root.join("lib", "ai_prompts")
420
- ```
421
-
422
- ### Caching
423
-
424
- Promptly supports optional caching for rendered prompts.
425
-
426
- - Default: enabled, TTL = 3600 seconds (1 hour).
427
- - In Rails, the Railtie auto-uses `Rails.cache` if present.
428
-
429
- Configure globally:
430
-
431
- ```ruby
432
- # config/initializers/promptly.rb
433
- Promptly::Cache.configure do |c|
434
- c.store = Rails.cache # or any ActiveSupport::Cache store
435
- c.ttl = 3600 # default TTL in seconds
436
- c.enabled = true # globally enable/disable caching
89
+ begin
90
+ # Validates and parses the JSON, or raises Promptly::ValidationError
91
+ parsed_output = Promptly.validate_response!("user_onboarding/welcome", raw_json)
92
+ rescue Promptly::ValidationError => e
93
+ # Handle invalid response
437
94
  end
438
95
  ```
439
96
 
440
- Per-call options:
441
-
442
- ```ruby
443
- # Bypass cache for this render only
444
- Promptly.render("user_onboarding/welcome_email", locals: {...}, cache: false)
445
-
446
- # Custom TTL for this render only
447
- Promptly.render("user_onboarding/welcome_email", locals: {...}, ttl: 5.minutes)
448
- ```
449
-
450
- Invalidation:
451
-
452
- ```ruby
453
- # Clear entire cache store (if supported by the store)
454
- Promptly::Cache.clear
455
-
456
- # Delete a specific cached entry
457
- Promptly::Cache.delete(
458
- identifier: "user_onboarding/welcome_email",
459
- locale: :en,
460
- locals: {name: "John"},
461
- prompts_path: Promptly.prompts_path
462
- )
463
- ```
464
-
465
- ### Direct Template Rendering
466
-
467
- ```ruby
468
- # Render ERB directly (without file lookup)
469
- template = "Hello <%= name %>, welcome to <%= app %>!"
470
- output = Promptly.render_template(template, locals: {name: "John", app: "MyApp"})
471
-
472
- # Render Liquid directly
473
- template = "Hello {{ name }}, welcome to {{ app }}!"
474
- output = Promptly.render_template(template, locals: {name: "John", app: "MyApp"}, engine: :liquid)
475
- ```
476
-
477
- ## API Reference
478
-
479
- ### `Promptly.render(identifier, locale: nil, locals: {}, cache: true, ttl: nil)`
480
-
481
- Renders a template by identifier with locale fallback and optional caching.
482
-
483
- - **identifier**: Template path like `"user_onboarding/welcome"`
484
- - **locale**: Specific locale (defaults to `I18n.locale`)
485
- - **locals**: Hash of variables for template
486
- - **cache**: Enable/disable caching for this call (defaults to `true`)
487
- - **ttl**: Time-to-live in seconds for cache entry (overrides default TTL)
488
-
489
- ### `Promptly.render_template(template, locals: {}, engine: :erb)`
490
-
491
- Renders template string directly.
492
-
493
- - **template**: Template string
494
- - **locals**: Hash of variables
495
- - **engine**: `:erb` or `:liquid`
496
-
497
- ### `Promptly.prompts_path`
498
-
499
- Get/set the root directory for prompt templates (defaults to `Rails.root/app/prompts`).
500
-
501
97
  ## Development
502
98
 
503
99
  ```bash
@@ -524,4 +120,4 @@ rake build
524
120
 
525
121
  ## License
526
122
 
527
- MIT
123
+ MIT
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ load "promptly/tasks/ai_prompts.rake"
4
5
 
5
6
  desc "Run standardrb"
6
7
  task :standard do
@@ -0,0 +1 @@
1
+ This is a test prompt.
@@ -0,0 +1,6 @@
1
+ ---
2
+ version: 1.0
3
+ author: Test Author
4
+ change_notes: Initial version
5
+ ---
6
+ This is a test prompt with metadata.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Promptly
4
+ module Helper
5
+ # Render a prompt template by identifier.
6
+ #
7
+ # Example:
8
+ # # app/prompts/welcome_email.erb
9
+ # # Hello <%= @user.name %>, welcome!
10
+ #
11
+ # # In a mailer, job, or service including this module:
12
+ # # render_prompt("welcome_email", user: @user)
13
+ #
14
+ # Supports locale-aware lookup and caching.
15
+ def render_prompt(identifier, locale: nil, cache: true, ttl: nil, **locals)
16
+ Promptly.render(identifier, locale: locale, locals: locals, cache: cache, ttl: ttl)
17
+ end
18
+ end
19
+ end
@@ -11,6 +11,21 @@ module Promptly
11
11
  if defined?(Rails.cache)
12
12
  Promptly::Cache.store = Rails.cache
13
13
  end
14
+
15
+ # Make render_prompt available in mailers, jobs, and controllers
16
+ if defined?(ActiveSupport)
17
+ ActiveSupport.on_load(:action_mailer) do
18
+ include Promptly::Helper
19
+ end
20
+
21
+ ActiveSupport.on_load(:active_job) do
22
+ include Promptly::Helper
23
+ end
24
+
25
+ ActiveSupport.on_load(:action_controller) do
26
+ include Promptly::Helper
27
+ end
28
+ end
14
29
  end
15
30
 
16
31
  rake_tasks do
@@ -26,6 +26,12 @@ module Promptly
26
26
  lookup = ActionView::LookupContext.new(ActionView::PathSet.new([]))
27
27
  av = view_class.new(lookup, {}, nil)
28
28
 
29
+ # Make locals available both as locals and instance variables (e.g., @user)
30
+ (locals || {}).each do |k, v|
31
+ ivar = "@#{k}"
32
+ av.instance_variable_set(ivar, v)
33
+ end
34
+
29
35
  av.render(inline: template, type: :erb, locals: locals || {})
30
36
  end
31
37