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 +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +63 -0
- data/LICENSE.txt +21 -0
- data/README.md +272 -0
- data/Rakefile +9 -0
- data/examples/basic_usage.rb +147 -0
- data/lib/generators/ruby_llm_template/install_generator.rb +117 -0
- data/lib/ruby_llm/template/chat_extension.rb +44 -0
- data/lib/ruby_llm/template/configuration.rb +27 -0
- data/lib/ruby_llm/template/loader.rb +146 -0
- data/lib/ruby_llm/template/railtie.rb +14 -0
- data/lib/ruby_llm/template/version.rb +7 -0
- data/lib/ruby_llm/template.rb +63 -0
- data/sig/ruby_llm/template.rbs +6 -0
- metadata +107 -0
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
data/.standard.yml
ADDED
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,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,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
|
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: []
|