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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acd140832db9c4f719f094548791c956ec3cb0f7380cf1c213e9923be1070fe2
4
- data.tar.gz: abdba3ecdc1c1d4a03f012944c5ebc08d906c02266872eb9e3fb54483add5f55
3
+ metadata.gz: 8a23a4bcc5d7efe0f5c83ec8cd21d0badc3d6a09b6ab6be98b0b89178ee1f9ac
4
+ data.tar.gz: 5136d03f17b3c1372abc31146da3771bf719ea0a71869a1233685421f22ea9b1
5
5
  SHA512:
6
- metadata.gz: b714d8ec43e3491248fc5a229cbbc2018227a28b32b68d84e8961fab5fe5185b860c55de425f3c573eb9b776f1922b5e56b331a6cf2be95cf7d8f33b303f8d1a
7
- data.tar.gz: 69afcef9ff51ba00326a5e1efa1226777aecc97cfc9f9d85998a960353758a05b1fa967cef716dd441618f68afe67194333c0f4c1253a1b773d3f2818930dd12
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
- ### 3. Test via Rails console
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
- ```ruby
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
- ### Background Job Usage
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
- # app/jobs/generate_ai_content_job.rb
246
- class GenerateAiContentJob < ApplicationJob
247
- def perform(user_id, prompt_identifier, locals = {})
248
- user = User.find(user_id)
249
-
250
- prompt = Promptly.render(
251
- prompt_identifier,
252
- locale: user.locale,
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
- ## I18n Prompts Usage
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
- # Configure I18n in your Rails app
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
- # Force specific locale for AI prompt generation
345
- prompt = Promptly.render(
346
- "content_generation/blog_post_outline",
347
- locale: :fr,
348
- locals: {
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
@@ -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.
@@ -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}#{identifier_filter ? " for '#{identifier_filter}'" : ""}"
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?(/<%[=\-].*?@#{Regexp.escape(key)}[\W]/m)
107
- present ||= content.match?(/<%[=\-].*?\b#{Regexp.escape(key)}\b/m)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Promptly
4
- VERSION = "0.1.13"
4
+ VERSION = "0.1.17"
5
5
  end