Skip to content

Commit

Permalink
Nested Template Support in Prompts
Browse files Browse the repository at this point in the history
  • Loading branch information
kladaFOX committed Oct 10, 2024
1 parent 0f6983d commit aa68a08
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 45 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,12 @@ This version enhances the flexibility and robustness of the Completions class, e

* **Prompt Path Detection:**
* Prompt paths now use the configured `root` to locate the template files. This ensures that Spectre works correctly in various environments where template paths may vary.



* **Nested Template Support in Prompts**
* You can now organize your prompt files in nested directories and render them using the `Spectre::Prompt.render` method.
* **Example**: To render a template from a nested folder:
```ruby
Spectre::Prompt.render(template: 'nested/folder/nested', locals: { query: 'What is AI?' })
```
* This feature allows for better organization and scalability when dealing with multiple prompt categories and complex scenarios.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,33 @@ Spectre::Prompt.render(
- **`template`:** The path to the prompt template file (e.g., `rag/system`).
- **`locals`:** A hash of variables to be used inside the ERB template.

**Using Nested Templates for Prompts**

Spectre's `Prompt` class now supports rendering templates from nested directories. This allows you to better organize your prompt files in a structured folder hierarchy.

You can organize your prompt templates in subfolders. For instance, you can have the following structure:

```
app/
spectre/
prompts/
rag/
system.yml.erb
user.yml.erb
nested/
folder/
nested.yml.erb
```

To render a prompt from a nested folder, simply pass the full path to the `template` argument:

```ruby
# Rendering from a nested folder
Spectre::Prompt.render(template: 'nested/folder/nested', locals: { query: 'What is AI?' })
```

This allows for more flexibility when organizing your prompt files, particularly when dealing with complex scenarios or multiple prompt categories.

**Combining Completions with Prompts**

You can also combine completions and prompts like so:
Expand Down
23 changes: 12 additions & 11 deletions lib/spectre/prompt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ def prompts_path

# Render a prompt by reading and rendering the YAML template
#
# @param template [String] The path to the template file, formatted as 'type/prompt' (e.g., 'rag/system')
# @param template [String] The path to the template file, formatted as 'folder1/folder2/prompt'
# @param locals [Hash] Variables to be passed to the template for rendering
#
# @return [String] Rendered prompt
def render(template:, locals: {})
type, prompt = split_template(template)
file_path = prompt_file_path(type, prompt)
path, prompt = split_template(template)
file_path = prompt_file_path(path, prompt)

raise "Prompt file not found: #{file_path}" unless File.exist?(file_path)

Expand Down Expand Up @@ -57,22 +57,23 @@ def detect_prompts_path
end
end

# Split the template parameter into type and prompt
# Split the template parameter into path and prompt
#
# @param template [String] Template path in the format 'type/prompt' (e.g., 'rag/system')
# @return [Array<String, String>] An array containing the type and prompt
# @param template [String] Template path in the format 'folder1/folder2/prompt'
# @return [Array<String, String>] An array containing the folder path and the prompt name
def split_template(template)
template.split('/')
*path_parts, prompt = template.split('/')
[File.join(path_parts), prompt]
end

# Build the path to the desired prompt file
#
# @param type [String] Name of the prompt folder
# @param prompt [String] Type of prompt (e.g., 'system', 'user')
# @param path [String] Path to the prompt folder(s)
# @param prompt [String] Name of the prompt file (e.g., 'system', 'user')
#
# @return [String] Full path to the template file
def prompt_file_path(type, prompt)
File.join(prompts_path, type, "#{prompt}.yml.erb")
def prompt_file_path(path, prompt)
File.join(prompts_path, path, "#{prompt}.yml.erb")
end

# Preprocess locals recursively to escape special characters in strings
Expand Down
78 changes: 45 additions & 33 deletions spec/spectre/prompt_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,35 @@
ERB
end

let(:nested_prompt_content) do
<<~ERB
nested: |
Nested context for <%= @query %>
ERB
end

let(:invalid_yaml_content) do
<<~ERB
system: |
You are a helpful assistant.
This line should cause a YAML error because it is missing indentation or a key
ERB
end

before do
# Create a temporary directory to hold the prompts
@tmpdir = Dir.mktmpdir

# Create the necessary folders and files for the test
prompts_folder = File.join(@tmpdir, 'rag')
nested_folder = File.join(@tmpdir, 'nested/folder')
FileUtils.mkdir_p(prompts_folder)
FileUtils.mkdir_p(nested_folder)

# Write the mock system.yml.erb and user.yml.erb files
# Write the mock system.yml.erb, user.yml.erb, and nested.yml.erb files
File.write(File.join(prompts_folder, 'system.yml.erb'), system_prompt_content)
File.write(File.join(prompts_folder, 'user.yml.erb'), user_prompt_content)
File.write(File.join(nested_folder, 'nested.yml.erb'), nested_prompt_content)

# Temporarily set the prompts_path to the tmp directory
allow(Spectre::Prompt).to receive(:prompts_path).and_return(@tmpdir)
Expand All @@ -42,68 +60,62 @@
end

describe '.render' do
subject { described_class.render(template: template, locals: locals) }

let(:locals) { {} }
let(:template) { 'rag/system' }

context 'when generating the system prompt' do
it 'returns the rendered system prompt' do
result = described_class.render(template: 'rag/system')
expect(result).to eq("You are a helpful assistant.\n")
expect(subject).to eq("You are a helpful assistant.\n")
end
end

context 'when generating the user prompt with locals' do
let(:query) { 'What is AI?' }
let(:objects) { ['AI is cool', 'AI is the future'] }
let(:template) { 'rag/user' }
let(:locals) { { query: 'What is AI?', objects: ['AI is cool', 'AI is the future'] } }

it 'returns the rendered user prompt with local variables' do
result = described_class.render(
template: 'rag/user',
locals: { query: query, objects: objects }
)

expected_result = "User's query: What is AI?\nContext: AI is cool, AI is the future\n"
expect(result).to eq(expected_result)
expect(subject).to eq(expected_result)
end
end

context 'when locals contain special characters' do
let(:query) { 'What is <AI> & why is it important?' }
let(:objects) { ['AI & ML', 'Future of AI'] }
let(:template) { 'rag/user' }
let(:locals) { { query: 'What is <AI> & why is it important?', objects: ['AI & ML', 'Future of AI'] } }

it 'escapes and restores special characters in the user prompt' do
result = described_class.render(
template: 'rag/user',
locals: { query: query, objects: objects }
)

expected_result = "User's query: What is <AI> & why is it important?\nContext: AI & ML, Future of AI\n"
expect(result).to eq(expected_result)
expect(subject).to eq(expected_result)
end
end

context 'when the prompt file is not found' do
let(:template) { 'nonexistent_template' }

it 'raises an error indicating the file is missing' do
expect {
described_class.render(template: 'nonexistent_template')
}.to raise_error(RuntimeError, /Prompt file not found/)
expect { subject }.to raise_error(RuntimeError, /Prompt file not found/)
end
end

context 'when there is a YAML syntax error in the prompt file' do
let(:invalid_yaml_content) do
<<~ERB
system: |
You are a helpful assistant.
This line should cause a YAML error because it is missing indentation or a key
ERB
end

before do
File.write(File.join(@tmpdir, 'rag/system.yml.erb'), invalid_yaml_content)
end

it 'raises a YAML syntax error' do
expect {
described_class.render(template: 'rag/system')
}.to raise_error(RuntimeError, /YAML Syntax Error/)
expect { subject }.to raise_error(RuntimeError, /YAML Syntax Error/)
end
end

context 'when generating a nested prompt' do
let(:template) { 'nested/folder/nested' }
let(:locals) { { query: 'What is AI?' } }

it 'returns the rendered nested prompt' do
expected_result = "Nested context for What is AI?\n"
expect(subject).to eq(expected_result)
end
end
end
Expand Down

0 comments on commit aa68a08

Please sign in to comment.