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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55ee620ab3faf1e3c2d2ff44db3f28fe255a7379431c360f86b6f8dd87a8a752
4
- data.tar.gz: ddf1db22fc260b4007e374944833b4ffe08fd5bcab020a033a28956612a26bbc
3
+ metadata.gz: acd140832db9c4f719f094548791c956ec3cb0f7380cf1c213e9923be1070fe2
4
+ data.tar.gz: abdba3ecdc1c1d4a03f012944c5ebc08d906c02266872eb9e3fb54483add5f55
5
5
  SHA512:
6
- metadata.gz: 0f343de865b5a2bef48f3a86cd131c40b8173be6183267fbd22242b228b668891ea191c7dba2597db373a7814bb30e05b8b8e4004cf345defef1d2e84c7b9b92
7
- data.tar.gz: 3e872512ba87aa8f387d851d75d378e7b9cba91a20bbc0c316598af80166a71d691751e3a00a1aefda20a132f88e5a9bb58414ba312760dbc02dacec590bc11e
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
@@ -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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Promptly
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.13"
5
5
  end
data/lib/promptly.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "promptly/version"
4
4
  require_relative "promptly/renderer"
5
5
  require_relative "promptly/locator"
6
6
  require_relative "promptly/cache"
7
+ require_relative "promptly/helper"
7
8
 
8
9
  module Promptly
9
10
  class Error < StandardError; end
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.7
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-16 00:00:00.000000000 Z
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.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