ruby_llm-template 0.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 75ab8a9d634742e2ce6cfd5b8af64bf28891e7ee8d7d6e49283ecc40cbe3e922
4
+ data.tar.gz: bf7ca2149ada527fa50bc51828c623a38d3f517648b8b548896d92b665920217
5
+ SHA512:
6
+ metadata.gz: 9ca1c964c2629d14461bc13965227141ffcbcde5104fd01a988452787d6fbf52a69cea8a65d7133a38e4d8858e6f57cef206fd157f20886cb6b0c277e024e691
7
+ data.tar.gz: 2badb609535562c1e274ff141708f3d14cb9f8cb565f8dc00e9346e8d601f03bd0071d131eaa2b26fd1c2f67756b4b5385967b73463085e6dd3cb2b695222175
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-01-28
9
+
10
+ ### Added
11
+ - Initial release of RubyLLM::Template
12
+ - Template management system for RubyLLM with ERB support
13
+ - Configuration system for template directories
14
+ - Support for system, user, assistant, and schema message templates
15
+ - **RubyLLM::Schema Integration**: Support for `schema.rb` files using the RubyLLM::Schema DSL
16
+ - Rails integration with automatic configuration and generator
17
+ - Comprehensive test suite with 37 test cases
18
+ - Error handling with descriptive messages
19
+ - Documentation and examples
20
+
21
+ ### Features
22
+ - **Template Organization**: Structure prompts in folders with separate ERB files for each message role
23
+ - **ERB Templating**: Full ERB support with context variables and Ruby logic
24
+ - **Schema Definition**: Use `schema.rb` files with RubyLLM::Schema DSL for type-safe, dynamic schemas
25
+ - **Rails Integration**: Seamless Rails integration with generators and automatic configuration
26
+ - **Configurable**: Set custom template directories per environment
27
+ - **Schema Support**: Automatic schema loading and application with fallback to JSON
28
+ - **Error Handling**: Clear error messages for common issues
29
+ - **Smart Dependencies**: Optional RubyLLM::Schema dependency with graceful fallbacks
30
+
31
+ ### Schema Features
32
+ - **Ruby DSL**: Use RubyLLM::Schema for clean, type-safe schema definitions
33
+ - **Context Variables**: Access template context variables within schema.rb files
34
+ - **Dynamic Schemas**: Generate schemas based on runtime conditions
35
+ - **Schema-Only Approach**: Exclusively supports schema.rb files with clear error messages
36
+ - **No JSON Fallback**: Eliminates error-prone JSON string manipulation
37
+
38
+ ### Usage
39
+ ```ruby
40
+ # Basic usage with schema.rb
41
+ RubyLLM.chat.with_template(:extract_metadata, document: @document).complete
42
+
43
+ # Context variables available in both ERB and schema.rb
44
+ RubyLLM.chat.with_template(:extract_metadata,
45
+ document: @document,
46
+ categories: ["finance", "technology"],
47
+ max_items: 10
48
+ ).complete
49
+ ```
50
+
51
+ ### Template Structure
52
+ ```
53
+ prompts/extract_metadata/
54
+ ├── system.txt.erb # System message
55
+ ├── user.txt.erb # User prompt with ERB
56
+ ├── assistant.txt.erb # Optional assistant message
57
+ └── schema.rb # RubyLLM::Schema definition
58
+ ```
59
+
60
+ ### Rails Setup
61
+ ```bash
62
+ rails generate ruby_llm_template:install
63
+ ```
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Daniel Friis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,272 @@
1
+ # RubyLLM::Template
2
+
3
+ A flexible template management system for [RubyLLM](https://github.com/crmne/ruby_llm) that allows you to organize and reuse ERB templates.
4
+
5
+ ```ruby
6
+ RubyLLM.chat.with_template(:extract_metadata, document: @document).complete
7
+
8
+ # ----------------------------------
9
+ # Retrieves the following files:
10
+ # ----------------------------------
11
+ # prompts/
12
+ # extract_metadata/
13
+ # ├── system.txt.erb # System message
14
+ # ├── user.txt.erb # User prompt
15
+ # ├── assistant.txt.erb # Assistant message (optional)
16
+ # └── schema.rb # RubyLLM::Schema definition (optional)
17
+
18
+ ```
19
+
20
+ ## Features
21
+
22
+ - 🎯 **Organized Templates**: Structure your prompts in folders with separate files for system, user, assistant, and schema messages
23
+ - 🔄 **ERB Templating**: Use full ERB power with context variables and Ruby logic
24
+ - ⚙️ **Configurable**: Set custom template directories per environment
25
+ - 🚀 **Rails Integration**: Seamless Rails integration with generators and automatic configuration
26
+ - 🧪 **Well Tested**: Comprehensive test suite ensuring reliability
27
+ - 📦 **Minimal Dependencies**: Only depends on RubyLLM and standard Ruby libraries
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's Gemfile:
32
+
33
+ ```ruby
34
+ gem 'ruby_llm'
35
+ gem 'ruby_llm-template'
36
+ gem 'ruby_llm-schema' # Optional for schema.rb support
37
+ ```
38
+
39
+ And then execute:
40
+
41
+ ```bash
42
+ bundle install
43
+ ```
44
+
45
+ ### Rails Setup
46
+
47
+ If you're using Rails, run the generator to set up the template system:
48
+
49
+ ```bash
50
+ rails generate ruby_llm_template:install
51
+ ```
52
+
53
+ This will:
54
+ - Create `config/initializers/ruby_llm_template.rb`
55
+ - Create `app/prompts/` directory
56
+ - Generate an example template at `app/prompts/extract_metadata/`
57
+
58
+ ## Quick Start
59
+
60
+ ### 1. Create a Template
61
+
62
+ Create a directory structure like this:
63
+
64
+ ```
65
+ prompts/
66
+ extract_metadata/
67
+ ├── system.txt.erb # System message
68
+ ├── user.txt.erb # User prompt
69
+ ├── assistant.txt.erb # Assistant message (optional)
70
+ └── schema.rb # RubyLLM::Schema definition (optional)
71
+ ```
72
+
73
+ ### 2. Write Your Templates
74
+
75
+ **`prompts/extract_metadata/system.txt.erb`**:
76
+ ```erb
77
+ You are an expert document analyzer. Extract metadata from documents in a structured format.
78
+ ```
79
+
80
+ **`prompts/extract_metadata/user.txt.erb`**:
81
+ ```erb
82
+ Please analyze this document: <%= document %>
83
+
84
+ <% if additional_context %>
85
+ Additional context: <%= additional_context %>
86
+ <% end %>
87
+ ```
88
+
89
+ **`prompts/extract_metadata/schema.rb`**:
90
+ ```ruby
91
+ # Using RubyLLM::Schema DSL for clean, type-safe schemas
92
+ RubyLLM::Schema.create do
93
+ string :title, description: "Document title"
94
+ array :topics, description: "Main topics" do
95
+ string
96
+ end
97
+ string :summary, description: "Brief summary"
98
+
99
+ # Optional fields with validation
100
+ number :confidence, required: false, minimum: 0, maximum: 1
101
+
102
+ # Nested objects
103
+ object :metadata, required: false do
104
+ string :author
105
+ string :date, format: "date"
106
+ end
107
+ end
108
+ ```
109
+
110
+ ### 3. Use the Template
111
+
112
+ ```ruby
113
+ # Basic usage
114
+ RubyLLM.chat.with_template(:extract_metadata, document: @document).complete
115
+
116
+ # With context variables
117
+ RubyLLM.chat.with_template(:extract_metadata,
118
+ document: @document,
119
+ additional_context: "Focus on technical details"
120
+ ).complete
121
+
122
+ # Chaining with other RubyLLM methods
123
+ RubyLLM.chat
124
+ .with_template(:extract_metadata, document: @document)
125
+ .with_model("gpt-4")
126
+ .complete
127
+ ```
128
+
129
+ ## Configuration
130
+
131
+ ### Non-Rails Applications
132
+
133
+ ```ruby
134
+ RubyLlm::Template.configure do |config|
135
+ config.template_directory = "/path/to/your/prompts"
136
+ end
137
+ ```
138
+
139
+ ### Rails Applications
140
+
141
+ The gem automatically configures itself to use `Rails.root.join("app", "prompts")`, but you can override this in `config/initializers/ruby_llm_template.rb`:
142
+
143
+ ```ruby
144
+ RubyLlm::Template.configure do |config|
145
+ config.template_directory = Rails.root.join("app", "ai_prompts")
146
+ end
147
+ ```
148
+
149
+ ## Template Structure
150
+
151
+ Each template is a directory containing ERB files for different message roles:
152
+
153
+ - **`system.txt.erb`** - System message that sets the AI's behavior
154
+ - **`user.txt.erb`** - User message/prompt
155
+ - **`assistant.txt.erb`** - Pre-filled assistant message (optional)
156
+ - **`schema.rb`** - RubyLLM::Schema definition for structured output (optional)
157
+
158
+ Templates are processed in order: system → user → assistant → schema
159
+
160
+ ## ERB Context
161
+
162
+ All context variables passed to `with_template` are available in your ERB templates:
163
+
164
+ ```erb
165
+ Hello <%= name %>!
166
+
167
+ <% if urgent %>
168
+ 🚨 URGENT: <%= message %>
169
+ <% else %>
170
+ 📋 Regular: <%= message %>
171
+ <% end %>
172
+
173
+ Processing <%= documents.length %> documents:
174
+ <% documents.each_with_index do |doc, i| %>
175
+ <%= i + 1 %>. <%= doc.title %>
176
+ <% end %>
177
+ ```
178
+
179
+ ## Advanced Usage
180
+
181
+ ### Complex Templates
182
+
183
+ ```ruby
184
+ # Template with conditional logic and loops
185
+ RubyLLM.chat.with_template(:analyze_reports,
186
+ reports: @reports,
187
+ priority: "high",
188
+ include_charts: true,
189
+ deadline: 1.week.from_now
190
+ ).complete
191
+ ```
192
+
193
+ ### Multiple Template Calls
194
+
195
+ ```ruby
196
+ chat = RubyLLM.chat
197
+ .with_template(:initialize_session, user: current_user)
198
+ .with_template(:load_context, project: @project)
199
+
200
+ # Add more messages dynamically
201
+ chat.ask("What should we focus on first?")
202
+ ```
203
+
204
+ ### Schema Definition with RubyLLM::Schema
205
+
206
+ The gem integrates with [RubyLLM::Schema](https://github.com/danielfriis/ruby_llm-schema) to provide a clean Ruby DSL for defining JSON schemas. Use `schema.rb` files instead of JSON:
207
+
208
+ ```ruby
209
+ # prompts/analyze_results/schema.rb
210
+ RubyLLM::Schema.create do
211
+ number :confidence, minimum: 0, maximum: 1, description: "Analysis confidence"
212
+
213
+ array :results, description: "Analysis results" do
214
+ object do
215
+ string :item, description: "Result item"
216
+ number :score, minimum: 0, maximum: 100, description: "Item score"
217
+
218
+ # Context variables are available
219
+ string :category, enum: categories if defined?(categories)
220
+ end
221
+ end
222
+
223
+ # Optional nested structures
224
+ object :metadata, required: false do
225
+ string :model_version
226
+ string :timestamp, format: "date-time"
227
+ end
228
+ end
229
+ ```
230
+
231
+ **Benefits of `schema.rb`:**
232
+ - 🎯 **Type-safe**: Ruby DSL with built-in validation
233
+ - 🔄 **Dynamic**: Access template context variables for conditional schemas
234
+ - 📝 **Readable**: Clean, self-documenting Ruby syntax vs JSON
235
+ - 🔧 **Flexible**: Generate schemas based on runtime conditions
236
+ - 🚀 **No JSON**: Eliminate error-prone JSON string manipulation
237
+
238
+ **Schema-Only Approach**: The gem exclusively supports `schema.rb` files with RubyLLM::Schema. If you have a `schema.rb` file but the gem isn't installed, you'll get a clear error message.
239
+
240
+ ## Error Handling
241
+
242
+ The gem provides clear error messages for common issues:
243
+
244
+ ```ruby
245
+ begin
246
+ RubyLLM.chat.with_template(:extract_metadata).complete
247
+ rescue RubyLlm::Template::Error => e
248
+ puts e.message
249
+ # "Template 'extract_metadata' not found in /path/to/prompts"
250
+ # "Schema file 'extract_metadata/schema.rb' found but RubyLLM::Schema gem is not installed"
251
+ end
252
+ ```
253
+
254
+ ## Development
255
+
256
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
257
+
258
+ To run the test suite:
259
+
260
+ ```bash
261
+ bundle exec rspec
262
+ ```
263
+
264
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
265
+
266
+ ## Contributing
267
+
268
+ Bug reports and pull requests are welcome on GitHub at https://github.com/danielfriis/ruby_llm-template.
269
+
270
+ ## License
271
+
272
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "standard/rake"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: :spec
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example of using RubyLLM::Template
5
+ # This file demonstrates basic usage without actually calling RubyLLM APIs
6
+
7
+ require_relative "../lib/ruby_llm/template"
8
+
9
+ # Configure template directory
10
+ RubyLlm::Template.configure do |config|
11
+ config.template_directory = File.join(__dir__, "prompts")
12
+ end
13
+
14
+ # Create example prompts directory
15
+ prompts_dir = File.join(__dir__, "prompts", "extract_metadata")
16
+ FileUtils.mkdir_p(prompts_dir)
17
+
18
+ # Create example template files
19
+ File.write(File.join(prompts_dir, "system.txt.erb"), <<~ERB)
20
+ You are an expert document analyzer. Your task is to extract metadata from the provided document.
21
+
22
+ Please analyze the document carefully and extract relevant information such as:
23
+ - Document type
24
+ - Key topics
25
+ - Important dates
26
+ - Main entities mentioned
27
+
28
+ Provide your analysis in a structured format.
29
+ ERB
30
+
31
+ File.write(File.join(prompts_dir, "user.txt.erb"), <<~ERB)
32
+ Please analyze the following document and extract its metadata:
33
+
34
+ Document: <%= document %>
35
+
36
+ <% if additional_context %>
37
+ Additional context: <%= additional_context %>
38
+ <% end %>
39
+
40
+ Focus areas: <%= focus_areas.join(", ") if defined?(focus_areas) && focus_areas.any? %>
41
+ ERB
42
+
43
+ # Create schema.rb file using RubyLLM::Schema DSL
44
+ File.write(File.join(prompts_dir, "schema.rb"), <<~RUBY)
45
+ # Mock RubyLLM::Schema for this example
46
+ module RubyLLM
47
+ class Schema
48
+ def self.create(&block)
49
+ instance = new
50
+ instance.instance_eval(&block)
51
+ instance
52
+ end
53
+
54
+ def initialize
55
+ @schema = {type: "object", properties: {}, required: []}
56
+ end
57
+
58
+ def string(name, **options)
59
+ @schema[:properties][name] = {type: "string"}.merge(options.except(:required))
60
+ @schema[:required] << name unless options[:required] == false
61
+ end
62
+
63
+ def array(name, **options, &block)
64
+ prop = {type: "array"}.merge(options.except(:required))
65
+ prop[:items] = {type: "string"} if !block_given?
66
+ @schema[:properties][name] = prop
67
+ @schema[:required] << name unless options[:required] == false
68
+ end
69
+
70
+ def to_json_schema
71
+ {name: "ExtractMetadataSchema", schema: @schema}
72
+ end
73
+ end
74
+ end
75
+
76
+ # The actual schema definition
77
+ RubyLLM::Schema.create do
78
+ string :document_type, description: "The type of document (e.g., report, article, email)"
79
+
80
+ array :key_topics, description: "Main topics discussed in the document"
81
+
82
+ array :important_dates, required: false, description: "Significant dates mentioned"
83
+
84
+ # Context variables are available in schema.rb files
85
+ focus_count = defined?(focus_areas) ? focus_areas&.length || 3 : 3
86
+ end
87
+ RUBY
88
+
89
+ puts "🎯 RubyLLM::Template Example"
90
+ puts "=" * 40
91
+
92
+ # Mock chat object that demonstrates the extension
93
+ class MockChat
94
+ include RubyLlm::Template::ChatExtension
95
+
96
+ def initialize
97
+ @messages = []
98
+ @schema = nil
99
+ end
100
+
101
+ def add_message(role:, content:)
102
+ @messages << {role: role, content: content}
103
+ puts "📝 Added #{role} message: #{content[0..100]}#{"..." if content.length > 100}"
104
+ end
105
+
106
+ def with_schema(schema)
107
+ @schema = schema
108
+ if schema.is_a?(Hash) && schema[:schema]
109
+ puts "📋 Schema applied: #{schema[:name]} with #{schema[:schema][:properties]&.keys&.length || 0} properties"
110
+ else
111
+ puts "📋 Schema applied with #{schema.keys.length} properties"
112
+ end
113
+ self
114
+ end
115
+
116
+ def complete
117
+ puts "\n🤖 Chat would now be sent to AI with:"
118
+ puts " - #{@messages.length} messages"
119
+ puts " - Schema: #{@schema ? "Yes" : "No"}"
120
+ puts "\n💬 Messages:"
121
+ @messages.each_with_index do |msg, i|
122
+ puts " #{i + 1}. [#{msg[:role].upcase}] #{msg[:content][0..80]}#{"..." if msg[:content].length > 80}"
123
+ end
124
+ self
125
+ end
126
+ end
127
+
128
+ # Simulate the usage
129
+ begin
130
+ chat = MockChat.new
131
+
132
+ # This demonstrates the desired API:
133
+ # RubyLLM.chat.with_template(:extract_metadata, context).complete
134
+ chat.with_template(:extract_metadata,
135
+ document: "Q3 Financial Report: Revenue increased 15% to $2.3M. Key challenges include supply chain delays affecting Q4 projections.",
136
+ additional_context: "Focus on financial metrics and future outlook",
137
+ focus_areas: ["revenue", "challenges", "projections"]).complete
138
+ rescue RubyLlm::Template::Error => e
139
+ puts "❌ Error: #{e.message}"
140
+ end
141
+
142
+ puts "\n✅ Example completed successfully!"
143
+ puts "\nTo use with real RubyLLM:"
144
+ puts " RubyLLM.chat.with_template(:extract_metadata, document: @document).complete"
145
+
146
+ # Clean up example files
147
+ FileUtils.rm_rf(File.join(__dir__, "prompts")) if Dir.exist?(File.join(__dir__, "prompts"))
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RubyLlmTemplate
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ desc "Install RubyLLM Template system"
9
+
10
+ def self.source_root
11
+ @source_root ||= File.expand_path("templates", __dir__)
12
+ end
13
+
14
+ def create_initializer
15
+ create_file "config/initializers/ruby_llm_template.rb", <<~RUBY
16
+ # frozen_string_literal: true
17
+
18
+ RubyLlm::Template.configure do |config|
19
+ # Set the directory where your prompts are stored
20
+ # Default: Rails.root.join("app", "prompts")
21
+ # config.template_directory = Rails.root.join("app", "prompts")
22
+ end
23
+ RUBY
24
+ end
25
+
26
+ def create_template_directory
27
+ empty_directory "app/prompts"
28
+
29
+ create_file "app/prompts/.keep", ""
30
+
31
+ # Create an example template
32
+ create_example_template
33
+ end
34
+
35
+ def show_readme
36
+ say <<~MESSAGE
37
+
38
+ RubyLLM Template has been installed!
39
+
40
+ Prompts directory: app/prompts/
41
+ Configuration: config/initializers/ruby_llm_template.rb
42
+
43
+ Example usage:
44
+ RubyLLM.chat.with_template(:extract_metadata, document: @document).complete
45
+
46
+ Template structure:
47
+ app/prompts/extract_metadata/
48
+ ├── system.txt.erb # System message
49
+ ├── user.txt.erb # User prompt
50
+ ├── assistant.txt.erb # Assistant message (optional)
51
+ └── schema.rb # RubyLLM::Schema definition (optional)
52
+
53
+ Get started by creating your first template!
54
+ MESSAGE
55
+ end
56
+
57
+ private
58
+
59
+ def create_example_template
60
+ example_dir = "app/prompts/extract_metadata"
61
+ empty_directory example_dir
62
+
63
+ create_file "#{example_dir}/system.txt.erb", <<~ERB
64
+ You are an expert document analyzer. Your task is to extract metadata from the provided document.
65
+
66
+ Please analyze the document carefully and extract relevant information such as:
67
+ - Document type
68
+ - Key topics
69
+ - Important dates
70
+ - Main entities mentioned
71
+
72
+ Provide your analysis in a structured format.
73
+ ERB
74
+
75
+ create_file "#{example_dir}/user.txt.erb", <<~ERB
76
+ Please analyze the following document and extract its metadata:
77
+
78
+ <% if defined?(document) && document %>
79
+ Document: <%= document %>
80
+ <% else %>
81
+ [Document content will be provided here]
82
+ <% end %>
83
+
84
+ <% if defined?(additional_context) && additional_context %>
85
+ Additional context: <%= additional_context %>
86
+ <% end %>
87
+ ERB
88
+
89
+ create_file "#{example_dir}/schema.rb", <<~RUBY
90
+ # frozen_string_literal: true
91
+
92
+ # Schema definition using RubyLLM::Schema DSL
93
+ # See: https://github.com/danielfriis/ruby_llm-schema
94
+
95
+ RubyLLM::Schema.create do
96
+ string :document_type, description: "The type of document (e.g., report, article, email)"
97
+
98
+ array :key_topics, description: "Main topics discussed in the document" do
99
+ string
100
+ end
101
+
102
+ array :important_dates, required: false, description: "Significant dates mentioned in the document" do
103
+ string format: "date"
104
+ end
105
+
106
+ array :entities, required: false, description: "Named entities found in the document" do
107
+ object do
108
+ string :name
109
+ string :type, enum: ["person", "organization", "location", "other"]
110
+ end
111
+ end
112
+ end
113
+ RUBY
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlm
4
+ module Template
5
+ module ChatExtension
6
+ def with_template(template_name, context = {})
7
+ loader = RubyLlm::Template::Loader.new(template_name)
8
+
9
+ unless loader.template_exists?
10
+ raise RubyLlm::Template::Error, "Template '#{template_name}' not found in #{RubyLlm::Template.configuration.template_directory}"
11
+ end
12
+
13
+ # Apply templates in a specific order to maintain conversation flow
14
+ template_order = ["system", "user", "assistant"]
15
+
16
+ template_order.each do |role|
17
+ next unless loader.available_roles.include?(role)
18
+
19
+ content = loader.render_template(role, context)
20
+ next unless content && !content.strip.empty?
21
+
22
+ add_message(role: role, content: content.strip)
23
+ end
24
+
25
+ # Handle schema separately if it exists
26
+ if loader.available_roles.include?("schema")
27
+ schema_result = loader.render_template("schema", context)
28
+
29
+ if schema_result
30
+ if schema_result.respond_to?(:to_json_schema)
31
+ # It's a RubyLLM::Schema instance
32
+ with_schema(schema_result.to_json_schema)
33
+ else
34
+ # It's a schema class
35
+ with_schema(schema_result)
36
+ end
37
+ end
38
+ end
39
+
40
+ self
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlm
4
+ module Template
5
+ class Configuration
6
+ attr_writer :template_directory
7
+
8
+ def initialize
9
+ @template_directory = nil
10
+ end
11
+
12
+ def template_directory
13
+ @template_directory || default_template_directory
14
+ end
15
+
16
+ private
17
+
18
+ def default_template_directory
19
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
20
+ Rails.root.join("app", "prompts")
21
+ else
22
+ File.join(Dir.pwd, "prompts")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "pathname"
5
+
6
+ begin
7
+ require "ruby_llm/schema"
8
+ rescue LoadError
9
+ # RubyLLM::Schema not available, schema.rb files won't work
10
+ end
11
+
12
+ module RubyLlm
13
+ module Template
14
+ class Loader
15
+ SUPPORTED_ROLES = %w[system user assistant schema].freeze
16
+
17
+ def initialize(template_name, template_directory: nil)
18
+ @template_name = template_name.to_s
19
+ @template_directory = Pathname.new(template_directory || RubyLlm::Template.configuration.template_directory)
20
+ @template_path = @template_directory.join(@template_name)
21
+ end
22
+
23
+ def render_template(role, context = {})
24
+ return nil unless SUPPORTED_ROLES.include?(role.to_s)
25
+
26
+ # Handle schema role specially - only support .rb files
27
+ if role.to_s == "schema"
28
+ return render_schema_template(context)
29
+ end
30
+
31
+ # Handle regular ERB template
32
+ file_name = "#{role}.txt.erb"
33
+ template_file = @template_path.join(file_name)
34
+
35
+ return nil unless File.exist?(template_file)
36
+
37
+ template_content = File.read(template_file)
38
+ erb = ERB.new(template_content)
39
+
40
+ # Create a binding with the context variables
41
+ binding_context = create_binding_context(context)
42
+ erb.result(binding_context)
43
+ rescue => e
44
+ raise Error, "Failed to render template '#{@template_name}/#{file_name}': #{e.message}"
45
+ end
46
+
47
+ def available_roles
48
+ return [] unless Dir.exist?(@template_path)
49
+
50
+ roles = []
51
+
52
+ # Check for ERB templates (excluding schema.txt.erb)
53
+ Dir.glob("*.txt.erb", base: @template_path).each do |file|
54
+ role = File.basename(file, ".txt.erb")
55
+ next if role == "schema" # Skip schema.txt.erb files
56
+ roles << role if SUPPORTED_ROLES.include?(role)
57
+ end
58
+
59
+ # Check for schema.rb file
60
+ if File.exist?(@template_path.join("schema.rb"))
61
+ roles << "schema" unless roles.include?("schema")
62
+ end
63
+
64
+ roles.uniq
65
+ end
66
+
67
+ def template_exists?
68
+ Dir.exist?(@template_path) && !available_roles.empty?
69
+ end
70
+
71
+ def load_schema_class(context = {})
72
+ schema_file = @template_path.join("schema.rb")
73
+ return nil unless File.exist?(schema_file)
74
+ return nil unless defined?(RubyLLM::Schema)
75
+
76
+ schema_content = File.read(schema_file)
77
+ schema_context = create_schema_context(context)
78
+
79
+ # Evaluate the schema file
80
+ result = schema_context.instance_eval(schema_content, schema_file.to_s)
81
+
82
+ # Handle different patterns:
83
+ # 1. RubyLLM::Schema.create { } pattern - returns instance
84
+ if result.is_a?(RubyLLM::Schema) || result.respond_to?(:to_json_schema)
85
+ return result
86
+ end
87
+
88
+ # 2. Class definition pattern - look for TemplateClass::Schema
89
+ template_class_name = @template_name.to_s.split("_").map(&:capitalize).join
90
+ schema_class_name = "#{template_class_name}::Schema"
91
+
92
+ schema_class = constantize_safe(schema_class_name)
93
+ return schema_class if schema_class
94
+
95
+ raise Error, "Schema file must return a RubyLLM::Schema instance or define class '#{schema_class_name}'"
96
+ rescue => e
97
+ raise Error, "Failed to load schema from '#{@template_name}/schema.rb': #{e.message}"
98
+ end
99
+
100
+ private
101
+
102
+ def render_schema_template(context = {})
103
+ # Only support schema.rb files with RubyLLM::Schema
104
+ schema_instance = load_schema_class(context)
105
+ return schema_instance if schema_instance
106
+
107
+ # If there's a schema.rb file but RubyLLM::Schema isn't available, error
108
+ schema_file = @template_path.join("schema.rb")
109
+ if File.exist?(schema_file) && !defined?(RubyLLM::Schema)
110
+ raise Error, "Schema file '#{@template_name}/schema.rb' found but RubyLLM::Schema gem is not installed. Add 'gem \"ruby_llm-schema\"' to your Gemfile."
111
+ end
112
+
113
+ nil
114
+ end
115
+
116
+ def create_binding_context(context)
117
+ # Create a new binding with the context variables available
118
+ context.each do |key, value|
119
+ define_singleton_method(key) { value }
120
+ end
121
+
122
+ binding
123
+ end
124
+
125
+ def create_schema_context(context)
126
+ schema_context = Object.new
127
+ context.each do |key, value|
128
+ schema_context.instance_variable_set("@#{key}", value)
129
+ schema_context.define_singleton_method(key) { value }
130
+ end
131
+ schema_context
132
+ end
133
+
134
+ def constantize_safe(class_name)
135
+ if defined?(Rails)
136
+ class_name.constantize
137
+ else
138
+ # Simple constantize for non-Rails environments
139
+ Object.const_get(class_name)
140
+ end
141
+ rescue NameError
142
+ nil
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlm
4
+ module Template
5
+ class Railtie < Rails::Railtie
6
+ initializer "ruby_llm_template.configure" do |app|
7
+ # Set default template directory for Rails applications
8
+ RubyLlm::Template.configure do |config|
9
+ config.template_directory ||= app.root.join("app", "prompts")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlm
4
+ module Template
5
+ VERSION = "0.1.2"
6
+ end
7
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "template/version"
4
+ require_relative "template/configuration"
5
+ require_relative "template/loader"
6
+ require_relative "template/chat_extension"
7
+
8
+ # Load Rails integration if Rails is available
9
+ begin
10
+ require "rails"
11
+ require_relative "template/railtie"
12
+ rescue LoadError
13
+ # Rails not available
14
+ end
15
+
16
+ module RubyLlm
17
+ module Template
18
+ class Error < StandardError; end
19
+
20
+ def self.configuration
21
+ @configuration ||= Configuration.new
22
+ end
23
+
24
+ def self.configure
25
+ yield(configuration)
26
+ end
27
+
28
+ def self.reset_configuration!
29
+ @configuration = nil
30
+ end
31
+ end
32
+ end
33
+
34
+ # Extend RubyLLM's Chat class if it's available
35
+ begin
36
+ require "ruby_llm"
37
+
38
+ if defined?(RubyLLM) && RubyLLM.respond_to?(:chat)
39
+ # We need to extend the actual chat class returned by RubyLLM.chat
40
+ # This is a monkey patch approach, but necessary for the API we want
41
+
42
+ module RubyLLMChatTemplateExtension
43
+ def self.extended(base)
44
+ base.extend(RubyLlm::Template::ChatExtension)
45
+ end
46
+ end
47
+
48
+ # Hook into RubyLLM.chat to extend the returned object
49
+ module RubyLLMTemplateHook
50
+ def chat(*args, **kwargs)
51
+ chat_instance = super
52
+ chat_instance.extend(RubyLlm::Template::ChatExtension)
53
+ chat_instance
54
+ end
55
+ end
56
+
57
+ if defined?(RubyLLM)
58
+ RubyLLM.singleton_class.prepend(RubyLLMTemplateHook)
59
+ end
60
+ end
61
+ rescue LoadError
62
+ # RubyLLM not available, extension will be loaded when it becomes available
63
+ end
@@ -0,0 +1,6 @@
1
+ module RubyLlm
2
+ module Template
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_llm-template
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Friis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-09-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby_llm
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby_llm-schema
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.12'
55
+ description: RubyLLM::Template provides a flexible template system for RubyLLM, allowing
56
+ you to organize chat prompts, system messages, and schemas in ERB template files
57
+ for easy reuse and maintenance.
58
+ email:
59
+ - d@friis.me
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".rspec"
65
+ - ".standard.yml"
66
+ - CHANGELOG.md
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - examples/basic_usage.rb
71
+ - lib/generators/ruby_llm_template/install_generator.rb
72
+ - lib/ruby_llm/template.rb
73
+ - lib/ruby_llm/template/chat_extension.rb
74
+ - lib/ruby_llm/template/configuration.rb
75
+ - lib/ruby_llm/template/loader.rb
76
+ - lib/ruby_llm/template/railtie.rb
77
+ - lib/ruby_llm/template/version.rb
78
+ - sig/ruby_llm/template.rbs
79
+ homepage: https://github.com/danielfriis/ruby_llm-template
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ allowed_push_host: https://rubygems.org
84
+ homepage_uri: https://github.com/danielfriis/ruby_llm-template
85
+ source_code_uri: https://github.com/danielfriis/ruby_llm-template
86
+ changelog_uri: https://github.com/danielfriis/ruby_llm-template/blob/main/CHANGELOG.md
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 3.1.0
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubygems_version: 3.4.19
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: Template management system for RubyLLM - organize and reuse ERB templates
106
+ for AI chat interactions
107
+ test_files: []