promptly 0.1.7 → 0.1.13
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 +98 -33
- data/lib/promptly/helper.rb +19 -0
- data/lib/promptly/railtie.rb +15 -0
- data/lib/promptly/renderer.rb +6 -0
- data/lib/promptly/tasks/ai_prompts.rake +118 -0
- data/lib/promptly/version.rb +1 -1
- data/lib/promptly.rb +1 -0
- metadata +4 -4
- data/promptly.gemspec +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: acd140832db9c4f719f094548791c956ec3cb0f7380cf1c213e9923be1070fe2
|
4
|
+
data.tar.gz: abdba3ecdc1c1d4a03f012944c5ebc08d906c02266872eb9e3fb54483add5f55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b714d8ec43e3491248fc5a229cbbc2018227a28b32b68d84e8961fab5fe5185b860c55de425f3c573eb9b776f1922b5e56b331a6cf2be95cf7d8f33b303f8d1a
|
7
|
+
data.tar.gz: 69afcef9ff51ba00326a5e1efa1226777aecc97cfc9f9d85998a960353758a05b1fa967cef716dd441618f68afe67194333c0f4c1253a1b773d3f2818930dd12
|
data/README.md
CHANGED
@@ -1,37 +1,4 @@
|
|
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
1
|
|
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
2
|
# Promptly
|
36
3
|
|
37
4
|
Opinionated Rails integration for reusable AI prompt templates. Build maintainable, localized, and testable AI prompts using ERB or Liquid templates with Rails conventions.
|
@@ -182,6 +149,32 @@ rails ai_prompts:render[user_onboarding/welcome_email,es]
|
|
182
149
|
rails ai_prompts:render[user_onboarding/welcome_email]
|
183
150
|
```
|
184
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
|
+
|
185
178
|
## Rails App Integration
|
186
179
|
|
187
180
|
### Service Object Pattern
|
@@ -474,6 +467,78 @@ template = "Hello {{ name }}, welcome to {{ app }}!"
|
|
474
467
|
output = Promptly.render_template(template, locals: {name: "John", app: "MyApp"}, engine: :liquid)
|
475
468
|
```
|
476
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
|
+
|
477
542
|
## API Reference
|
478
543
|
|
479
544
|
### `Promptly.render(identifier, locale: nil, locals: {}, cache: true, ttl: nil)`
|
@@ -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
|
data/lib/promptly/railtie.rb
CHANGED
@@ -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
|
data/lib/promptly/renderer.rb
CHANGED
@@ -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
|
|
@@ -22,4 +22,122 @@ namespace :ai_prompts do
|
|
22
22
|
exit 1
|
23
23
|
end
|
24
24
|
end
|
25
|
+
|
26
|
+
desc "Lint prompt templates. Usage: rake ai_prompts:lint[identifier] LOCALES=en,es REQUIRED=name,app_name PROMPTS_PATH=..."
|
27
|
+
task :lint, [:identifier] => :environment do |_, args|
|
28
|
+
require "erb"
|
29
|
+
|
30
|
+
prompts_path = ENV["PROMPTS_PATH"] || Promptly.prompts_path
|
31
|
+
identifier_filter = args[:identifier]
|
32
|
+
|
33
|
+
locales = if ENV["LOCALES"]
|
34
|
+
ENV["LOCALES"].split(",").map(&:strip).reject(&:empty?)
|
35
|
+
elsif defined?(I18n) && I18n.respond_to?(:available_locales)
|
36
|
+
I18n.available_locales.map(&:to_s)
|
37
|
+
else
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
|
41
|
+
required_keys = (ENV["REQUIRED"] || "").split(",").map(&:strip).reject(&:empty?)
|
42
|
+
|
43
|
+
unless File.directory?(prompts_path)
|
44
|
+
warn "[lint] prompts_path not found: #{prompts_path}"
|
45
|
+
exit 1
|
46
|
+
end
|
47
|
+
|
48
|
+
exts = Promptly::Locator::SUPPORTED_EXTS
|
49
|
+
|
50
|
+
files = Dir.glob(File.join(prompts_path, "**", "*{#{exts.join(",")}}"))
|
51
|
+
if identifier_filter
|
52
|
+
files.select! do |f|
|
53
|
+
# match by identifier path without locale/ext
|
54
|
+
rel = f.sub(/^#{Regexp.escape(prompts_path)}\//, "")
|
55
|
+
base = rel.sub(/\.(?:[a-z]{2})?(?:#{exts.map { |e| Regexp.escape(e) }.join("|")})\z/, "")
|
56
|
+
base == identifier_filter
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if files.empty?
|
61
|
+
warn "[lint] No templates found under #{prompts_path}#{identifier_filter ? " for '#{identifier_filter}'" : ""}"
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
|
65
|
+
status = 0
|
66
|
+
|
67
|
+
# Group by identifier (path without locale/ext)
|
68
|
+
grouped = files.group_by do |f|
|
69
|
+
rel = f.sub(/^#{Regexp.escape(prompts_path)}\//, "")
|
70
|
+
rel.sub(/\.(?:[a-z]{2})?(?:#{exts.map { |e| Regexp.escape(e) }.join("|")})\z/, "")
|
71
|
+
end
|
72
|
+
|
73
|
+
grouped.each do |identifier, paths|
|
74
|
+
puts "[lint] Identifier: #{identifier}"
|
75
|
+
|
76
|
+
# 1) Syntax check and placeholder scan per file
|
77
|
+
paths.each do |path|
|
78
|
+
engine = Promptly::Locator.engine_for(path)
|
79
|
+
content = File.read(path)
|
80
|
+
|
81
|
+
begin
|
82
|
+
case engine
|
83
|
+
when :erb
|
84
|
+
# Compile ERB to Ruby, don't execute
|
85
|
+
ERB.new(content)
|
86
|
+
when :liquid
|
87
|
+
if defined?(::Liquid)
|
88
|
+
::Liquid::Template.parse(content)
|
89
|
+
else
|
90
|
+
warn " - WARN: Liquid not available; skipping syntax parse for #{File.basename(path)}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
rescue => e
|
94
|
+
warn " - ERROR: Syntax error in #{File.basename(path)}: #{e.class}: #{e.message}"
|
95
|
+
status = 1
|
96
|
+
end
|
97
|
+
|
98
|
+
# Required placeholder presence (best-effort scan)
|
99
|
+
if required_keys.any?
|
100
|
+
missing = []
|
101
|
+
required_keys.each do |key|
|
102
|
+
present = false
|
103
|
+
case engine
|
104
|
+
when :erb
|
105
|
+
# naive checks: @key or key inside ERB output tags
|
106
|
+
present ||= content.match?(/<%[=\-].*?@#{Regexp.escape(key)}[\W]/m)
|
107
|
+
present ||= content.match?(/<%[=\-].*?\b#{Regexp.escape(key)}\b/m)
|
108
|
+
when :liquid
|
109
|
+
present ||= content.match?(/\{\{\s*#{Regexp.escape(key)}[\s\|\}]/)
|
110
|
+
end
|
111
|
+
missing << key unless present
|
112
|
+
end
|
113
|
+
if missing.any?
|
114
|
+
warn " - ERROR: Missing required placeholders in #{File.basename(path)}: #{missing.join(", ")}"
|
115
|
+
status = 1
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# 2) Missing locale files (if locales provided)
|
121
|
+
if locales.any?
|
122
|
+
found_locales = paths.map do |p|
|
123
|
+
# extract locale between name and extension: name.<locale>.ext
|
124
|
+
File.basename(p)[/\.([a-z]{2})\.(?:erb|liquid)\z/, 1]
|
125
|
+
end.compact.uniq
|
126
|
+
|
127
|
+
missing_locales = locales - found_locales
|
128
|
+
if missing_locales.any?
|
129
|
+
warn " - WARN: Missing locale templates for #{identifier}: #{missing_locales.join(", ")}"
|
130
|
+
else
|
131
|
+
puts " - OK: Locale coverage satisfied"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
if status.zero?
|
137
|
+
puts "[lint] OK"
|
138
|
+
else
|
139
|
+
warn "[lint] FAIL"
|
140
|
+
end
|
141
|
+
exit status
|
142
|
+
end
|
25
143
|
end
|
data/lib/promptly/version.rb
CHANGED
data/lib/promptly.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: promptly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.13
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilbur Suero
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionview
|
@@ -97,12 +97,12 @@ files:
|
|
97
97
|
- lib/generators/promptly/prompt_generator.rb
|
98
98
|
- lib/promptly.rb
|
99
99
|
- lib/promptly/cache.rb
|
100
|
+
- lib/promptly/helper.rb
|
100
101
|
- lib/promptly/locator.rb
|
101
102
|
- lib/promptly/railtie.rb
|
102
103
|
- lib/promptly/renderer.rb
|
103
104
|
- lib/promptly/tasks/ai_prompts.rake
|
104
105
|
- lib/promptly/version.rb
|
105
|
-
- promptly.gemspec
|
106
106
|
homepage: https://github.com/wilburhimself/promptly
|
107
107
|
licenses:
|
108
108
|
- MIT
|
@@ -120,7 +120,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
120
120
|
requirements:
|
121
121
|
- - ">="
|
122
122
|
- !ruby/object:Gem::Version
|
123
|
-
version: '3.
|
123
|
+
version: '3.2'
|
124
124
|
- - "<"
|
125
125
|
- !ruby/object:Gem::Version
|
126
126
|
version: '3.4'
|
data/promptly.gemspec
DELETED
@@ -1,46 +0,0 @@
|
|
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/wilburhimself/promptly"
|
14
|
-
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = ">= 3.3", "< 3.4"
|
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/wilburhimself/promptly"
|
21
|
-
spec.metadata["changelog_uri"] = "https://github.com/wilburhimself/promptly/blob/main/CHANGELOG.md"
|
22
|
-
spec.metadata["documentation_uri"] = "https://github.com/wilburhimself/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 (single target: Rails 7.2.x)
|
38
|
-
spec.add_dependency "actionview", "~> 7.2"
|
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
|
-
# Development dependencies
|
45
|
-
spec.add_development_dependency "railties", "~> 7.2"
|
46
|
-
end
|