promptly 0.1.13 → 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 +4 -4
- data/README.md +24 -493
- data/Rakefile +1 -0
- data/app/prompts/test/prompt.en.erb +1 -0
- data/app/prompts/test/prompt_with_metadata.en.erb +6 -0
- data/lib/promptly/tasks/ai_prompts.rake +9 -4
- data/lib/promptly/validator.rb +24 -0
- data/lib/promptly/version.rb +1 -1
- data/lib/promptly.rb +78 -2
- data/wiki/Configuration.md +63 -0
- data/wiki/Functional-Prompt-Tests.md +36 -0
- data/wiki/Generators.md +32 -0
- data/wiki/Helper-render_prompt.md +25 -0
- data/wiki/Home.md +29 -0
- data/wiki/I18n-Prompts-Usage.md +60 -0
- data/wiki/Linting-Templates.md +38 -0
- data/wiki/Liquid-Templates.md +49 -0
- data/wiki/Prompt-Version-Metadata.md +34 -0
- data/wiki/Quick-Start.md +116 -0
- data/wiki/Rails-App-Integration.md +116 -0
- data/wiki/Schema-Validation.md +27 -0
- data/wiki/_Sidebar.md +11 -0
- metadata +33 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a23a4bcc5d7efe0f5c83ec8cd21d0badc3d6a09b6ab6be98b0b89178ee1f9ac
|
|
4
|
+
data.tar.gz: 5136d03f17b3c1372abc31146da3771bf719ea0a71869a1233685421f22ea9b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 42f82b2467f10f8851aada866267cd047a95dd99f592b50bbd0189ccddfd58ff619f0973f981f3f88be803bbc213ac3a2b645f2d9ed59a8bc0967c715ceb8030
|
|
7
|
+
data.tar.gz: 368c7a84f7215db1415a3d2165e6061917977ac924127c937ebf9e48db641d35f373dbb8df3f857145bb9e00881faa6b15048872ded0fc968a4922defad35337
|
data/README.md
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# Promptly
|
|
3
2
|
|
|
4
3
|
Opinionated Rails integration for reusable AI prompt templates. Build maintainable, localized, and testable AI prompts using ERB or Liquid templates with Rails conventions.
|
|
@@ -11,6 +10,11 @@ Opinionated Rails integration for reusable AI prompt templates. Build maintainab
|
|
|
11
10
|
- **Render & CLI**: Test prompts in Rails console or via rake tasks
|
|
12
11
|
- **Minimal setup**: Auto-loads via Railtie, zero configuration required
|
|
13
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).
|
|
14
18
|
|
|
15
19
|
## Install
|
|
16
20
|
|
|
@@ -34,54 +38,6 @@ bundle install
|
|
|
34
38
|
|
|
35
39
|
## Quick Start
|
|
36
40
|
|
|
37
|
-
### 1. Create prompt templates
|
|
38
|
-
|
|
39
|
-
Create `app/prompts/user_onboarding/welcome_email.en.erb`:
|
|
40
|
-
|
|
41
|
-
```erb
|
|
42
|
-
You are a friendly customer success manager writing a personalized welcome email.
|
|
43
|
-
|
|
44
|
-
Context:
|
|
45
|
-
- User name: <%= name %>
|
|
46
|
-
- App name: <%= app_name %>
|
|
47
|
-
- User's role: <%= user_role %>
|
|
48
|
-
- Available features for this user: <%= features.join(", ") %>
|
|
49
|
-
- User signed up <%= days_since_signup %> days ago
|
|
50
|
-
|
|
51
|
-
Task: Write a warm, personalized welcome email that:
|
|
52
|
-
1. Addresses the user by name
|
|
53
|
-
2. Explains the key benefits specific to their role
|
|
54
|
-
3. Highlights 2-3 most relevant features they should try first
|
|
55
|
-
4. Includes a clear call-to-action to get started
|
|
56
|
-
5. Maintains a professional but friendly tone
|
|
57
|
-
|
|
58
|
-
Keep the email concise (under 200 words) and actionable.
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Create `app/prompts/user_onboarding/welcome_email.es.erb`:
|
|
62
|
-
|
|
63
|
-
```erb
|
|
64
|
-
Eres un gerente de éxito del cliente amigable escribiendo un email de bienvenida personalizado.
|
|
65
|
-
|
|
66
|
-
Contexto:
|
|
67
|
-
- Nombre del usuario: <%= name %>
|
|
68
|
-
- Nombre de la app: <%= app_name %>
|
|
69
|
-
- Rol del usuario: <%= user_role %>
|
|
70
|
-
- Funciones disponibles para este usuario: <%= features.join(", ") %>
|
|
71
|
-
- El usuario se registró hace <%= days_since_signup %> días
|
|
72
|
-
|
|
73
|
-
Tarea: Escribe un email de bienvenida cálido y personalizado que:
|
|
74
|
-
1. Se dirija al usuario por su nombre
|
|
75
|
-
2. Explique los beneficios clave específicos para su rol
|
|
76
|
-
3. Destaque 2-3 funciones más relevantes que debería probar primero
|
|
77
|
-
4. Incluya una llamada a la acción clara para comenzar
|
|
78
|
-
5. Mantenga un tono profesional pero amigable
|
|
79
|
-
|
|
80
|
-
Mantén el email conciso (menos de 200 palabras) y orientado a la acción.
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### 2. Render in your Rails app
|
|
84
|
-
|
|
85
41
|
```ruby
|
|
86
42
|
# In a controller, service, or anywhere in Rails
|
|
87
43
|
prompt = Promptly.render(
|
|
@@ -106,463 +62,38 @@ puts ai_response.dig("choices", 0, "message", "content")
|
|
|
106
62
|
# => AI-generated personalized welcome email in Spanish
|
|
107
63
|
```
|
|
108
64
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```ruby
|
|
112
|
-
rails console
|
|
113
|
-
|
|
114
|
-
# Render the prompt before sending to AI
|
|
115
|
-
prompt = Promptly.render(
|
|
116
|
-
"user_onboarding/welcome_email",
|
|
117
|
-
locale: :en,
|
|
118
|
-
locals: {
|
|
119
|
-
name: "John Smith",
|
|
120
|
-
app_name: "ProjectHub",
|
|
121
|
-
user_role: "Developer",
|
|
122
|
-
features: ["API access", "Code reviews", "Deployment tools"],
|
|
123
|
-
days_since_signup: 1
|
|
124
|
-
}
|
|
125
|
-
)
|
|
126
|
-
puts prompt
|
|
127
|
-
|
|
128
|
-
# Uses I18n.locale by default
|
|
129
|
-
I18n.locale = :es
|
|
130
|
-
prompt = Promptly.render(
|
|
131
|
-
"user_onboarding/welcome_email",
|
|
132
|
-
locals: {
|
|
133
|
-
name: "María García",
|
|
134
|
-
app_name: "ProjectHub",
|
|
135
|
-
user_role: "Team Lead",
|
|
136
|
-
features: ["Crear proyectos", "Invitar miembros", "Seguimiento"],
|
|
137
|
-
days_since_signup: 3
|
|
138
|
-
}
|
|
139
|
-
)
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### 4. CLI rendering
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
# Render specific locale (shows the prompt, not AI output)
|
|
146
|
-
rails ai_prompts:render[user_onboarding/welcome_email,es]
|
|
147
|
-
|
|
148
|
-
# Uses default locale
|
|
149
|
-
rails ai_prompts:render[user_onboarding/welcome_email]
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Helper: render_prompt
|
|
153
|
-
|
|
154
|
-
Use a concise helper anywhere in Rails to render prompts with locals that are also exposed as instance variables inside ERB templates.
|
|
155
|
-
|
|
156
|
-
* **Auto-included**: Controllers, Mailers, and Jobs via Railtie.
|
|
157
|
-
* **Services/Plain Ruby**: `include Promptly::Helper`.
|
|
158
|
-
|
|
159
|
-
Example template and usage:
|
|
160
|
-
|
|
161
|
-
```erb
|
|
162
|
-
# app/prompts/welcome_email.erb
|
|
163
|
-
Hello <%= @user.name %>, welcome to our service!
|
|
164
|
-
We're excited to have you join.
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
168
|
-
# In a mailer, job, controller, or a service that includes Promptly::Helper
|
|
169
|
-
rendered = render_prompt("welcome_email", user: @user)
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Notes:
|
|
173
|
-
|
|
174
|
-
- **Locals become @instance variables** in ERB. Passing `user: @user` makes `@user` available in the template.
|
|
175
|
-
- **Localization**: `render_prompt("welcome_email", locale: :es, user: user)` resolves `welcome_email.es.erb` with fallback per `Promptly::Locator`.
|
|
176
|
-
- **Caching**: Controlled per call (`cache:`, `ttl:`) and globally via `Promptly::Cache`.
|
|
177
|
-
|
|
178
|
-
## Rails App Integration
|
|
179
|
-
|
|
180
|
-
### Service Object Pattern
|
|
181
|
-
|
|
182
|
-
```ruby
|
|
183
|
-
# app/services/ai_prompt_service.rb
|
|
184
|
-
class AiPromptService
|
|
185
|
-
def self.generate_welcome_email(user, locale: I18n.locale)
|
|
186
|
-
prompt = Promptly.render(
|
|
187
|
-
"user_onboarding/welcome_email",
|
|
188
|
-
locale: locale,
|
|
189
|
-
locals: {
|
|
190
|
-
name: user.full_name,
|
|
191
|
-
app_name: Rails.application.class.module_parent_name,
|
|
192
|
-
user_role: user.role.humanize,
|
|
193
|
-
features: available_features_for(user),
|
|
194
|
-
days_since_signup: (Date.current - user.created_at.to_date).to_i
|
|
195
|
-
}
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
# Send to AI service and return generated content
|
|
199
|
-
openai_client.chat(
|
|
200
|
-
model: "gpt-4",
|
|
201
|
-
messages: [{role: "user", content: prompt}]
|
|
202
|
-
).dig("choices", 0, "message", "content")
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
private
|
|
206
|
-
|
|
207
|
-
def self.available_features_for(user)
|
|
208
|
-
# Return features based on user's plan, role, etc.
|
|
209
|
-
case user.plan
|
|
210
|
-
when "basic"
|
|
211
|
-
["Create projects", "Basic reporting"]
|
|
212
|
-
when "pro"
|
|
213
|
-
["Create projects", "Team collaboration", "Advanced analytics", "API access"]
|
|
214
|
-
else
|
|
215
|
-
["Create projects"]
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def self.openai_client
|
|
220
|
-
@openai_client ||= OpenAI::Client.new(access_token: Rails.application.credentials.openai_api_key)
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Mailer Integration
|
|
65
|
+
## Structured Outputs (Response Schema Validation)
|
|
226
66
|
|
|
227
|
-
|
|
228
|
-
# app/mailers/user_mailer.rb
|
|
229
|
-
class UserMailer < ApplicationMailer
|
|
230
|
-
def welcome_email(user)
|
|
231
|
-
@user = user
|
|
232
|
-
@ai_content = AiPromptService.generate_welcome_email(user, locale: user.locale)
|
|
233
|
-
|
|
234
|
-
mail(
|
|
235
|
-
to: user.email,
|
|
236
|
-
subject: t('mailer.welcome.subject')
|
|
237
|
-
)
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
```
|
|
67
|
+
Promptly supports OpenAI's structured outputs (`guided_json` style) by defining `.response.json` files alongside your templates.
|
|
241
68
|
|
|
242
|
-
|
|
69
|
+
For example, given an output schema `app/prompts/user_onboarding/welcome.response.json`, you can pass it directly to an AI service:
|
|
243
70
|
|
|
244
71
|
```ruby
|
|
245
|
-
#
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
locals: locals.merge(
|
|
254
|
-
user_name: user.full_name,
|
|
255
|
-
user_role: user.role,
|
|
256
|
-
account_type: user.account_type
|
|
257
|
-
)
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
# Generate AI content
|
|
261
|
-
ai_response = openai_client.chat(
|
|
262
|
-
model: "gpt-4",
|
|
263
|
-
messages: [{role: "user", content: prompt}]
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
generated_content = ai_response.dig("choices", 0, "message", "content")
|
|
267
|
-
|
|
268
|
-
# Store or send the generated content
|
|
269
|
-
user.notifications.create!(
|
|
270
|
-
title: "AI Generated Content Ready",
|
|
271
|
-
content: generated_content,
|
|
272
|
-
notification_type: prompt_identifier.split('/').last
|
|
273
|
-
)
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
private
|
|
277
|
-
|
|
278
|
-
def openai_client
|
|
279
|
-
@openai_client ||= OpenAI::Client.new(access_token: Rails.application.credentials.openai_api_key)
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
# Usage
|
|
284
|
-
GenerateAiContentJob.perform_later(
|
|
285
|
-
user.id,
|
|
286
|
-
"coaching/goal_review",
|
|
287
|
-
{
|
|
288
|
-
current_goals: user.goals.active.pluck(:title),
|
|
289
|
-
progress_summary: "Made good progress on fitness goals",
|
|
290
|
-
challenges: ["Time management", "Consistency"]
|
|
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
|
|
291
80
|
}
|
|
292
81
|
)
|
|
293
82
|
```
|
|
294
83
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
### Directory Structure
|
|
298
|
-
|
|
299
|
-
```
|
|
300
|
-
app/prompts/
|
|
301
|
-
├── user_onboarding/
|
|
302
|
-
│ ├── welcome_email.en.erb # English AI prompt
|
|
303
|
-
│ ├── welcome_email.es.erb # Spanish AI prompt
|
|
304
|
-
│ └── onboarding_checklist.erb # Fallback (any locale)
|
|
305
|
-
├── content_generation/
|
|
306
|
-
│ ├── blog_post_outline.en.erb
|
|
307
|
-
│ ├── social_media_post.es.erb
|
|
308
|
-
│ └── product_description.erb
|
|
309
|
-
└── ai_coaching/
|
|
310
|
-
├── goal_review.en.liquid # Liquid AI prompt
|
|
311
|
-
└── goal_review.es.liquid
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Locale Resolution
|
|
315
|
-
|
|
316
|
-
Promptly follows this resolution order:
|
|
317
|
-
|
|
318
|
-
1. **Requested locale**: `welcome.es.erb` (if `locale: :es` specified)
|
|
319
|
-
2. **Default locale**: `welcome.en.erb` (if `I18n.default_locale == :en`)
|
|
320
|
-
3. **Fallback**: `welcome.erb` (no locale suffix)
|
|
84
|
+
You can also natively validate the returned JSON string in Ruby to ensure it conforms exactly to the schema:
|
|
321
85
|
|
|
322
86
|
```ruby
|
|
323
|
-
|
|
324
|
-
# config/application.rb
|
|
325
|
-
config.i18n.default_locale = :en
|
|
326
|
-
config.i18n.available_locales = [:en, :es, :fr]
|
|
327
|
-
|
|
328
|
-
# Usage examples
|
|
329
|
-
I18n.locale = :es
|
|
330
|
-
I18n.default_locale = :en
|
|
331
|
-
|
|
332
|
-
# Will try: welcome_email.es.erb → welcome_email.en.erb → welcome_email.erb
|
|
333
|
-
prompt = Promptly.render(
|
|
334
|
-
"user_onboarding/welcome_email",
|
|
335
|
-
locals: {
|
|
336
|
-
name: "María García",
|
|
337
|
-
app_name: "ProjectHub",
|
|
338
|
-
user_role: "Manager",
|
|
339
|
-
features: ["Team management", "Analytics", "Reporting"],
|
|
340
|
-
days_since_signup: 1
|
|
341
|
-
}
|
|
342
|
-
)
|
|
87
|
+
raw_json = ai_response.dig("choices", 0, "message", "content")
|
|
343
88
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
"
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
topic: "Intelligence Artificielle",
|
|
350
|
-
target_audience: "Développeurs",
|
|
351
|
-
word_count: 1500
|
|
352
|
-
}
|
|
353
|
-
)
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### Liquid Templates
|
|
357
|
-
|
|
358
|
-
For more complex templating needs, use Liquid:
|
|
359
|
-
|
|
360
|
-
```liquid
|
|
361
|
-
<!-- app/prompts/ai_coaching/goal_review.en.liquid -->
|
|
362
|
-
You are an experienced life coach conducting a goal review session.
|
|
363
|
-
|
|
364
|
-
Context:
|
|
365
|
-
- Client name: {{ user_name }}
|
|
366
|
-
- Goals being reviewed: {% for goal in current_goals %}{{ goal }}{% unless forloop.last %}, {% endunless %}{% endfor %}
|
|
367
|
-
- Recent progress: {{ progress_summary }}
|
|
368
|
-
- Current challenges: {% for challenge in challenges %}{{ challenge }}{% unless forloop.last %}, {% endunless %}{% endfor %}
|
|
369
|
-
- Review period: {{ review_period | default: "monthly" }}
|
|
370
|
-
|
|
371
|
-
Task: Provide a personalized goal review that:
|
|
372
|
-
1. Acknowledges their progress and celebrates wins
|
|
373
|
-
2. Addresses each challenge with specific, actionable advice
|
|
374
|
-
3. Suggests 2-3 concrete next steps for the coming {{ review_period }}
|
|
375
|
-
4. Asks 1-2 thoughtful questions to help them reflect
|
|
376
|
-
5. Maintains an encouraging but realistic tone
|
|
377
|
-
|
|
378
|
-
{% if current_goals.size > 5 %}
|
|
379
|
-
Note: The client has many goals. Help them prioritize the most important ones.
|
|
380
|
-
{% endif %}
|
|
381
|
-
|
|
382
|
-
Format your response as a conversational coaching session, not a formal report.
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
```ruby
|
|
386
|
-
# Generate AI coaching content with Liquid template
|
|
387
|
-
prompt = Promptly.render(
|
|
388
|
-
"ai_coaching/goal_review",
|
|
389
|
-
locale: :en,
|
|
390
|
-
locals: {
|
|
391
|
-
user_name: "Alex",
|
|
392
|
-
current_goals: ["Run 5K under 25min", "Gym 3x/week", "Read 12 books/year"],
|
|
393
|
-
progress_summary: "Consistent with gym, behind on running pace, ahead on reading",
|
|
394
|
-
challenges: ["Time management", "Motivation on rainy days"],
|
|
395
|
-
review_period: "monthly"
|
|
396
|
-
}
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
# Send to AI service for personalized coaching
|
|
400
|
-
ai_coaching_session = openai_client.chat(
|
|
401
|
-
model: "gpt-4",
|
|
402
|
-
messages: [{role: "user", content: prompt}]
|
|
403
|
-
).dig("choices", 0, "message", "content")
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
## Configuration
|
|
407
|
-
|
|
408
|
-
### Custom Prompts Path
|
|
409
|
-
|
|
410
|
-
```ruby
|
|
411
|
-
# config/initializers/rails_ai_prompts.rb
|
|
412
|
-
Promptly.prompts_path = Rails.root.join("lib", "ai_prompts")
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### Caching
|
|
416
|
-
|
|
417
|
-
Promptly supports optional caching for rendered prompts.
|
|
418
|
-
|
|
419
|
-
- Default: enabled, TTL = 3600 seconds (1 hour).
|
|
420
|
-
- In Rails, the Railtie auto-uses `Rails.cache` if present.
|
|
421
|
-
|
|
422
|
-
Configure globally:
|
|
423
|
-
|
|
424
|
-
```ruby
|
|
425
|
-
# config/initializers/promptly.rb
|
|
426
|
-
Promptly::Cache.configure do |c|
|
|
427
|
-
c.store = Rails.cache # or any ActiveSupport::Cache store
|
|
428
|
-
c.ttl = 3600 # default TTL in seconds
|
|
429
|
-
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
|
|
430
94
|
end
|
|
431
95
|
```
|
|
432
96
|
|
|
433
|
-
Per-call options:
|
|
434
|
-
|
|
435
|
-
```ruby
|
|
436
|
-
# Bypass cache for this render only
|
|
437
|
-
Promptly.render("user_onboarding/welcome_email", locals: {...}, cache: false)
|
|
438
|
-
|
|
439
|
-
# Custom TTL for this render only
|
|
440
|
-
Promptly.render("user_onboarding/welcome_email", locals: {...}, ttl: 5.minutes)
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
Invalidation:
|
|
444
|
-
|
|
445
|
-
```ruby
|
|
446
|
-
# Clear entire cache store (if supported by the store)
|
|
447
|
-
Promptly::Cache.clear
|
|
448
|
-
|
|
449
|
-
# Delete a specific cached entry
|
|
450
|
-
Promptly::Cache.delete(
|
|
451
|
-
identifier: "user_onboarding/welcome_email",
|
|
452
|
-
locale: :en,
|
|
453
|
-
locals: {name: "John"},
|
|
454
|
-
prompts_path: Promptly.prompts_path
|
|
455
|
-
)
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
### Direct Template Rendering
|
|
459
|
-
|
|
460
|
-
```ruby
|
|
461
|
-
# Render ERB directly (without file lookup)
|
|
462
|
-
template = "Hello <%= name %>, welcome to <%= app %>!"
|
|
463
|
-
output = Promptly.render_template(template, locals: {name: "John", app: "MyApp"})
|
|
464
|
-
|
|
465
|
-
# Render Liquid directly
|
|
466
|
-
template = "Hello {{ name }}, welcome to {{ app }}!"
|
|
467
|
-
output = Promptly.render_template(template, locals: {name: "John", app: "MyApp"}, engine: :liquid)
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
## Generators
|
|
471
|
-
|
|
472
|
-
Create prompt templates following conventions.
|
|
473
|
-
|
|
474
|
-
```bash
|
|
475
|
-
# ERB with multiple locales
|
|
476
|
-
rails g promptly:prompt user_onboarding/welcome_email --locales en es --engine erb
|
|
477
|
-
|
|
478
|
-
# Liquid with a single locale
|
|
479
|
-
rails g promptly:prompt ai_coaching/goal_review --locales en --engine liquid
|
|
480
|
-
|
|
481
|
-
# Fallback-only (no locale suffix)
|
|
482
|
-
rails g promptly:prompt content_generation/outline --no-locale
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
Options:
|
|
486
|
-
|
|
487
|
-
- `--engine` erb|liquid (default: erb)
|
|
488
|
-
- `--locales` space-separated list (default: I18n.available_locales if available, else `en`)
|
|
489
|
-
- `--no-locale` create only fallback file (e.g., `welcome_email.erb`)
|
|
490
|
-
- `--force` overwrite existing files
|
|
491
|
-
|
|
492
|
-
Generated files are placed under `app/prompts/` and directories are created as needed.
|
|
493
|
-
|
|
494
|
-
Examples:
|
|
495
|
-
|
|
496
|
-
- `app/prompts/user_onboarding/welcome_email.en.erb`
|
|
497
|
-
- `app/prompts/user_onboarding/welcome_email.es.erb`
|
|
498
|
-
- `app/prompts/ai_coaching/goal_review.en.liquid`
|
|
499
|
-
- `app/prompts/content_generation/outline.erb` (fallback-only)
|
|
500
|
-
|
|
501
|
-
The generator seeds a minimal, intention-revealing scaffold you can edit immediately.
|
|
502
|
-
|
|
503
|
-
## Linting Templates
|
|
504
|
-
|
|
505
|
-
Validate your prompt templates from the CLI.
|
|
506
|
-
|
|
507
|
-
```bash
|
|
508
|
-
# Lint all templates under the prompts path
|
|
509
|
-
rake ai_prompts:lint
|
|
510
|
-
|
|
511
|
-
# Lint a specific identifier (path without locale/ext)
|
|
512
|
-
rake ai_prompts:lint[user_onboarding/welcome_email]
|
|
513
|
-
|
|
514
|
-
# Specify locales to check for coverage
|
|
515
|
-
LOCALES=en,es rake ai_prompts:lint
|
|
516
|
-
|
|
517
|
-
# Require placeholders to exist in templates
|
|
518
|
-
REQUIRED=name,app_name rake ai_prompts:lint[user_onboarding/welcome_email]
|
|
519
|
-
|
|
520
|
-
# Point to a custom prompts directory
|
|
521
|
-
PROMPTS_PATH=lib/ai_prompts rake ai_prompts:lint
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
What it checks:
|
|
525
|
-
|
|
526
|
-
- **Syntax errors**
|
|
527
|
-
- ERB: compiles with `ERB.new` (no execution)
|
|
528
|
-
- Liquid: parses with `Liquid::Template.parse` (if `liquid` gem present)
|
|
529
|
-
- **Missing locale files**
|
|
530
|
-
- For each identifier, warns when required locales are missing
|
|
531
|
-
- Locales source: `LOCALES` env or `I18n.available_locales`
|
|
532
|
-
- **Required placeholders**
|
|
533
|
-
- Best-effort scan for required keys from `REQUIRED` env
|
|
534
|
-
- ERB: looks for `<%= ... @key ... %>` or `<%= ... key ... %>` usage
|
|
535
|
-
- Liquid: looks for `{{ key }}` usage
|
|
536
|
-
|
|
537
|
-
Exit codes:
|
|
538
|
-
|
|
539
|
-
- `0` when all checks pass
|
|
540
|
-
- `1` when errors are found (syntax or missing required placeholders)
|
|
541
|
-
|
|
542
|
-
## API Reference
|
|
543
|
-
|
|
544
|
-
### `Promptly.render(identifier, locale: nil, locals: {}, cache: true, ttl: nil)`
|
|
545
|
-
|
|
546
|
-
Renders a template by identifier with locale fallback and optional caching.
|
|
547
|
-
|
|
548
|
-
- **identifier**: Template path like `"user_onboarding/welcome"`
|
|
549
|
-
- **locale**: Specific locale (defaults to `I18n.locale`)
|
|
550
|
-
- **locals**: Hash of variables for template
|
|
551
|
-
- **cache**: Enable/disable caching for this call (defaults to `true`)
|
|
552
|
-
- **ttl**: Time-to-live in seconds for cache entry (overrides default TTL)
|
|
553
|
-
|
|
554
|
-
### `Promptly.render_template(template, locals: {}, engine: :erb)`
|
|
555
|
-
|
|
556
|
-
Renders template string directly.
|
|
557
|
-
|
|
558
|
-
- **template**: Template string
|
|
559
|
-
- **locals**: Hash of variables
|
|
560
|
-
- **engine**: `:erb` or `:liquid`
|
|
561
|
-
|
|
562
|
-
### `Promptly.prompts_path`
|
|
563
|
-
|
|
564
|
-
Get/set the root directory for prompt templates (defaults to `Rails.root/app/prompts`).
|
|
565
|
-
|
|
566
97
|
## Development
|
|
567
98
|
|
|
568
99
|
```bash
|
|
@@ -589,4 +120,4 @@ rake build
|
|
|
589
120
|
|
|
590
121
|
## License
|
|
591
122
|
|
|
592
|
-
MIT
|
|
123
|
+
MIT
|
data/Rakefile
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This is a test prompt.
|
|
@@ -58,7 +58,7 @@ namespace :ai_prompts do
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
if files.empty?
|
|
61
|
-
warn "[lint] No templates found under #{prompts_path}#{
|
|
61
|
+
warn "[lint] No templates found under #{prompts_path}#{" for '#{identifier_filter}'" if identifier_filter}"
|
|
62
62
|
exit 1
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -103,10 +103,10 @@ namespace :ai_prompts do
|
|
|
103
103
|
case engine
|
|
104
104
|
when :erb
|
|
105
105
|
# naive checks: @key or key inside ERB output tags
|
|
106
|
-
present ||= content.match?(/<%[
|
|
107
|
-
present ||= content.match?(/<%[
|
|
106
|
+
present ||= content.match?(/<%[=-].*?@#{Regexp.escape(key)}\W/m)
|
|
107
|
+
present ||= content.match?(/<%[=-].*?\b#{Regexp.escape(key)}\b/m)
|
|
108
108
|
when :liquid
|
|
109
|
-
present ||= content.match?(/\{\{\s*#{Regexp.escape(key)}[\s
|
|
109
|
+
present ||= content.match?(/\{\{\s*#{Regexp.escape(key)}[\s|}]/)
|
|
110
110
|
end
|
|
111
111
|
missing << key unless present
|
|
112
112
|
end
|
|
@@ -140,4 +140,9 @@ namespace :ai_prompts do
|
|
|
140
140
|
end
|
|
141
141
|
exit status
|
|
142
142
|
end
|
|
143
|
+
|
|
144
|
+
desc "Run functional tests for prompts"
|
|
145
|
+
task :test_prompts do
|
|
146
|
+
exec "bundle exec rspec spec/prompts"
|
|
147
|
+
end
|
|
143
148
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Promptly
|
|
2
|
+
class Validator
|
|
3
|
+
def self.validate!(locals, schema_path)
|
|
4
|
+
return unless File.exist?(schema_path)
|
|
5
|
+
|
|
6
|
+
schema = YAML.load_file(schema_path)
|
|
7
|
+
missing_keys = schema.keys - locals.keys.map(&:to_s)
|
|
8
|
+
unless missing_keys.empty?
|
|
9
|
+
raise ArgumentError, "Missing required locals: #{missing_keys.join(", ")}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Optional: type checking
|
|
13
|
+
schema.each do |key, type|
|
|
14
|
+
next unless locals.key?(key.to_sym)
|
|
15
|
+
value = locals[key.to_sym]
|
|
16
|
+
case type
|
|
17
|
+
when "string" then raise TypeError unless value.is_a?(String)
|
|
18
|
+
when "integer" then raise TypeError unless value.is_a?(Integer)
|
|
19
|
+
when "array" then raise TypeError unless value.is_a?(Array)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/promptly/version.rb
CHANGED