ruby_llm-registry 0.1.0
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/.idea/.gitignore +5 -0
- data/.idea/inspectionProfiles/Project_Default.xml +5 -0
- data/.idea/jsLibraryMappings.xml +6 -0
- data/.idea/misc.xml +17 -0
- data/.idea/modules.xml +8 -0
- data/.idea/ruby_llm-registry.iml +79 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +65 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/Rakefile +12 -0
- data/build_release.sh +13 -0
- data/lib/ruby_llm/registry/adapters/active_record.rb +115 -0
- data/lib/ruby_llm/registry/adapters/base.rb +47 -0
- data/lib/ruby_llm/registry/adapters/mongo.rb +91 -0
- data/lib/ruby_llm/registry/adapters/s3.rb +95 -0
- data/lib/ruby_llm/registry/adapters/sqlite.rb +155 -0
- data/lib/ruby_llm/registry/adapters.rb +28 -0
- data/lib/ruby_llm/registry/comparison.rb +112 -0
- data/lib/ruby_llm/registry/context.rb +40 -0
- data/lib/ruby_llm/registry/errors.rb +11 -0
- data/lib/ruby_llm/registry/exporter.rb +54 -0
- data/lib/ruby_llm/registry/filesystem_backend.rb +160 -0
- data/lib/ruby_llm/registry/front_matter.rb +28 -0
- data/lib/ruby_llm/registry/importer.rb +107 -0
- data/lib/ruby_llm/registry/prompt.rb +98 -0
- data/lib/ruby_llm/registry/semver.rb +55 -0
- data/lib/ruby_llm/registry/version.rb +7 -0
- data/lib/ruby_llm/registry.rb +107 -0
- data/sig/ruby_llm/registry.rbs +6 -0
- metadata +77 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Registry
|
|
8
|
+
# Filesystem-backed registry adapter.
|
|
9
|
+
class FilesystemBackend
|
|
10
|
+
SUPPORTED_EXTENSIONS = %w[.md .prompt].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(root:, manifest_path: nil)
|
|
13
|
+
@root = Pathname.new(root)
|
|
14
|
+
@manifest_path = manifest_path && Pathname.new(manifest_path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def get(path, version: nil, label: nil)
|
|
18
|
+
prompt_dir = root.join(path)
|
|
19
|
+
raise PromptNotFoundError, "Prompt path not found: #{path}" unless prompt_dir.directory?
|
|
20
|
+
|
|
21
|
+
manifest = load_manifest
|
|
22
|
+
file_path = resolve_file_path(prompt_dir, path, version: version, label: label, manifest: manifest)
|
|
23
|
+
if file_path.nil?
|
|
24
|
+
raise PromptNotFoundError,
|
|
25
|
+
"Prompt not found: #{path}#{lookup_suffix(version: version, label: label)}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
parse_prompt(path, file_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def available_versions(path)
|
|
32
|
+
prompt_dir = root.join(path)
|
|
33
|
+
return [] unless prompt_dir.directory?
|
|
34
|
+
|
|
35
|
+
prompt_dir.children.select { |entry| entry.file? && versioned_prompt_file?(entry) }.map do |entry|
|
|
36
|
+
Version.parse(version_from_filename(entry.basename.to_s))
|
|
37
|
+
end.sort
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def store(prompt, overwrite: false, extension: ".md")
|
|
41
|
+
prompt_dir = root.join(prompt.path)
|
|
42
|
+
prompt_dir.mkpath
|
|
43
|
+
target = prompt_dir.join("v#{prompt.version}#{extension}")
|
|
44
|
+
raise Error, "Prompt already exists: #{target}" if target.exist? && !overwrite
|
|
45
|
+
|
|
46
|
+
target.write(Exporter.new(prompt).to_markdown)
|
|
47
|
+
prompt
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :root, :manifest_path
|
|
53
|
+
|
|
54
|
+
def load_manifest
|
|
55
|
+
manifest_file = manifest_path || root.join("manifest.yml")
|
|
56
|
+
return {} unless manifest_file.file?
|
|
57
|
+
|
|
58
|
+
YAML.safe_load(manifest_file.read, aliases: true) || {}
|
|
59
|
+
rescue Psych::SyntaxError
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolve_file_path(prompt_dir, prompt_key, version:, label:, manifest:)
|
|
64
|
+
return resolve_by_version(prompt_dir, version) if version
|
|
65
|
+
return resolve_by_label(prompt_dir, prompt_key, label, manifest) if label
|
|
66
|
+
|
|
67
|
+
resolve_latest(prompt_dir)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolve_by_version(prompt_dir, version)
|
|
71
|
+
normalized = Version.parse(version).to_s
|
|
72
|
+
candidates = candidate_files(prompt_dir).select do |entry|
|
|
73
|
+
version_from_filename(entry.basename.to_s) == normalized
|
|
74
|
+
end
|
|
75
|
+
candidates.first
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def resolve_by_label(prompt_dir, prompt_key, label, manifest)
|
|
79
|
+
label = label.to_sym
|
|
80
|
+
|
|
81
|
+
if (mapped_version = manifest_version_for(prompt_key, label, manifest))
|
|
82
|
+
return resolve_by_version(prompt_dir, mapped_version)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
direct = candidate_files(prompt_dir).find do |entry|
|
|
86
|
+
entry.basename.to_s.sub(entry.extname, "") == label.to_s || entry.basename.to_s == label.to_s
|
|
87
|
+
end
|
|
88
|
+
return direct if direct
|
|
89
|
+
|
|
90
|
+
label == :latest ? resolve_latest(prompt_dir) : nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def manifest_version_for(prompt_key, label, manifest)
|
|
94
|
+
prompt_manifest = manifest.dig("prompts", prompt_key) || manifest.dig(:prompts, prompt_key)
|
|
95
|
+
return nil unless prompt_manifest.is_a?(Hash)
|
|
96
|
+
|
|
97
|
+
aliases = prompt_manifest["aliases"] || prompt_manifest[:aliases] || {}
|
|
98
|
+
aliases[label.to_s] || aliases[label]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def resolve_latest(prompt_dir)
|
|
102
|
+
candidate_files(prompt_dir).max_by do |entry|
|
|
103
|
+
Version.parse(version_from_filename(entry.basename.to_s))
|
|
104
|
+
rescue InvalidVersionError
|
|
105
|
+
Version.parse("0.0.0")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def candidate_files(prompt_dir)
|
|
110
|
+
prompt_dir.children.select { |entry| entry.file? && versioned_prompt_file?(entry) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def versioned_prompt_file?(entry)
|
|
114
|
+
SUPPORTED_EXTENSIONS.include?(entry.extname) && entry.basename.to_s.match?(version_file_pattern(entry.extname))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def version_from_filename(filename)
|
|
118
|
+
filename.sub(/\.(md|prompt)\z/, "").sub(/\Av/, "")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_prompt(prompt_key, file_path)
|
|
122
|
+
content = file_path.read
|
|
123
|
+
metadata, body = FrontMatter.parse(content)
|
|
124
|
+
file_version = Version.parse(metadata[:version] || version_from_filename(file_path.basename.to_s))
|
|
125
|
+
labels = symbol_list(metadata[:labels] || metadata["labels"])
|
|
126
|
+
required_vars = symbol_list(metadata[:required_vars] || metadata["required_vars"])
|
|
127
|
+
prompt_metadata = metadata[:metadata] || metadata["metadata"] || {}
|
|
128
|
+
|
|
129
|
+
Prompt.new(
|
|
130
|
+
name: prompt_key.split("/").last,
|
|
131
|
+
namespace: prompt_key.split("/")[0...-1].join("/"),
|
|
132
|
+
version: file_version,
|
|
133
|
+
body: body,
|
|
134
|
+
source_path: file_path.to_s,
|
|
135
|
+
labels: labels,
|
|
136
|
+
metadata: prompt_metadata,
|
|
137
|
+
required_vars: required_vars
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def symbol_list(values)
|
|
142
|
+
Array(values).map(&:to_sym)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def version_file_pattern(extension)
|
|
146
|
+
/\Av?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?#{Regexp.escape(extension)}\z/
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def lookup_suffix(version:, label:)
|
|
150
|
+
if version
|
|
151
|
+
" version #{version}"
|
|
152
|
+
elsif label
|
|
153
|
+
" label #{label}"
|
|
154
|
+
else
|
|
155
|
+
""
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Registry
|
|
8
|
+
# Simple YAML front matter parser for prompt files.
|
|
9
|
+
module FrontMatter
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def parse(content)
|
|
13
|
+
return [{}, content] unless content.start_with?("---\n")
|
|
14
|
+
|
|
15
|
+
parts = content.split("\n---\n", 2)
|
|
16
|
+
return [{}, content] if parts.length != 2
|
|
17
|
+
|
|
18
|
+
header = parts.first.sub(/\A---\n/, "")
|
|
19
|
+
body = parts.last
|
|
20
|
+
metadata = YAML.safe_load(header, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
21
|
+
metadata = metadata.each_with_object({}) { |(key, value), hash| hash[key.to_sym] = value }
|
|
22
|
+
[metadata, body]
|
|
23
|
+
rescue Psych::SyntaxError
|
|
24
|
+
[{}, content]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "json"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module Registry
|
|
9
|
+
# Deserializes prompt exports into Prompt objects.
|
|
10
|
+
class Importer
|
|
11
|
+
def initialize(payload, format: :auto, path: nil)
|
|
12
|
+
@payload = payload
|
|
13
|
+
@format = format
|
|
14
|
+
@path = path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_prompt
|
|
18
|
+
data, body = case normalized_format
|
|
19
|
+
when :hash
|
|
20
|
+
extract_hash(payload)
|
|
21
|
+
when :json
|
|
22
|
+
extract_json(payload)
|
|
23
|
+
when :yaml
|
|
24
|
+
extract_yaml(payload)
|
|
25
|
+
when :markdown
|
|
26
|
+
extract_markdown(payload)
|
|
27
|
+
else
|
|
28
|
+
infer_payload
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
build_prompt(data, body)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :payload, :format, :path
|
|
37
|
+
|
|
38
|
+
def normalized_format
|
|
39
|
+
format == :auto ? infer_format : format.to_sym
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def infer_format
|
|
43
|
+
return :hash if payload.is_a?(Hash)
|
|
44
|
+
return :markdown if payload.to_s.start_with?("---\n")
|
|
45
|
+
return :json if payload.to_s.lstrip.start_with?("{")
|
|
46
|
+
|
|
47
|
+
:yaml
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def infer_payload
|
|
51
|
+
case infer_format
|
|
52
|
+
when :hash then extract_hash(payload)
|
|
53
|
+
when :markdown then extract_markdown(payload)
|
|
54
|
+
when :json then extract_json(payload)
|
|
55
|
+
else extract_yaml(payload)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def extract_hash(value)
|
|
60
|
+
data = value.transform_keys(&:to_sym)
|
|
61
|
+
[data, data[:body] || ""]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extract_json(value)
|
|
65
|
+
data = JSON.parse(value.to_s)
|
|
66
|
+
extract_hash(data)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_yaml(value)
|
|
70
|
+
data = YAML.safe_load(value.to_s, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
71
|
+
extract_hash(data)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def extract_markdown(value)
|
|
75
|
+
data, body = FrontMatter.parse(value.to_s)
|
|
76
|
+
[data, body]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_prompt(data, body)
|
|
80
|
+
prompt_path = path || data[:path] || data["path"]
|
|
81
|
+
namespace, name = split_path(prompt_path, data)
|
|
82
|
+
Prompt.new(
|
|
83
|
+
name: name,
|
|
84
|
+
namespace: namespace,
|
|
85
|
+
version: data[:version] || data["version"] || "0.0.0",
|
|
86
|
+
body: body,
|
|
87
|
+
source_path: data[:source_path] || data["source_path"],
|
|
88
|
+
labels: data[:labels] || data["labels"] || [],
|
|
89
|
+
metadata: data[:metadata] || data["metadata"] || {},
|
|
90
|
+
required_vars: data[:required_vars] || data["required_vars"] || []
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def split_path(prompt_path, data)
|
|
95
|
+
if prompt_path && prompt_path.include?("/")
|
|
96
|
+
pieces = prompt_path.split("/")
|
|
97
|
+
[pieces[0...-1].join("/"), pieces.last]
|
|
98
|
+
else
|
|
99
|
+
namespace = data[:namespace] || data["namespace"] || ""
|
|
100
|
+
name = data[:name] || data["name"] || prompt_path || "prompt"
|
|
101
|
+
[namespace, name]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Registry
|
|
7
|
+
# Represents a loaded prompt template plus its metadata.
|
|
8
|
+
class Prompt
|
|
9
|
+
attr_reader :name, :namespace, :version, :labels, :metadata, :required_vars, :body, :source_path
|
|
10
|
+
|
|
11
|
+
def initialize(name:, namespace:, version:, body:, source_path:, labels: [], metadata: {}, required_vars: [])
|
|
12
|
+
@name = name.to_s
|
|
13
|
+
@namespace = namespace.to_s
|
|
14
|
+
@version = version.is_a?(Version) ? version : Version.parse(version)
|
|
15
|
+
@body = body.to_s
|
|
16
|
+
@source_path = source_path
|
|
17
|
+
@labels = Array(labels).compact.map(&:to_sym)
|
|
18
|
+
@metadata = symbolize_keys(metadata)
|
|
19
|
+
@required_vars = Array(required_vars).compact.map(&:to_sym)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def path
|
|
23
|
+
[namespace, name].reject(&:empty?).join("/")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render(context = nil, **kwargs)
|
|
27
|
+
values = normalize_context(context, kwargs)
|
|
28
|
+
validate_required_vars!(values)
|
|
29
|
+
ERB.new(body, trim_mode: "-").result(RenderContext.new(values).instance_eval { binding })
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_message(role: :system, context: nil, **)
|
|
33
|
+
{ role: role, content: render(context, **) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def export(format: :markdown)
|
|
37
|
+
Exporter.new(self).public_send(exporter_method(format))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def diff(other)
|
|
41
|
+
other_prompt = other.is_a?(Prompt) ? other : Importer.new(other, format: :auto).to_prompt
|
|
42
|
+
Comparison.new(self, other_prompt)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
path: path,
|
|
48
|
+
namespace: namespace,
|
|
49
|
+
name: name,
|
|
50
|
+
version: version.to_s,
|
|
51
|
+
labels: labels,
|
|
52
|
+
metadata: metadata,
|
|
53
|
+
required_vars: required_vars,
|
|
54
|
+
source_path: source_path,
|
|
55
|
+
body: body
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def normalize_context(context, kwargs)
|
|
62
|
+
if context.is_a?(Hash)
|
|
63
|
+
context.merge(kwargs)
|
|
64
|
+
elsif context.nil?
|
|
65
|
+
kwargs
|
|
66
|
+
elsif kwargs.empty?
|
|
67
|
+
context.respond_to?(:to_h) ? context.to_h : { context: context }
|
|
68
|
+
else
|
|
69
|
+
raise ArgumentError, "Pass either a context hash or keyword arguments, not both"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_required_vars!(values)
|
|
74
|
+
missing = required_vars.reject { |key| values.key?(key) || values.key?(key.to_s) }
|
|
75
|
+
return if missing.empty?
|
|
76
|
+
|
|
77
|
+
raise MissingVariableError, "Missing required prompt variables: #{missing.join(", ")}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def symbolize_keys(hash)
|
|
81
|
+
hash.each_with_object({}) do |(key, value), memo|
|
|
82
|
+
memo[key.to_sym] = value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def exporter_method(format)
|
|
87
|
+
case format.to_sym
|
|
88
|
+
when :yaml then :to_yaml
|
|
89
|
+
when :json then :to_json
|
|
90
|
+
when :markdown, :md then :to_markdown
|
|
91
|
+
when :hash then :to_h
|
|
92
|
+
else
|
|
93
|
+
raise ArgumentError, "Unsupported export format: #{format.inspect}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Registry
|
|
5
|
+
class Version
|
|
6
|
+
include Comparable
|
|
7
|
+
|
|
8
|
+
VERSION_PATTERN = /\Av?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?\z/
|
|
9
|
+
|
|
10
|
+
attr_reader :major, :minor, :patch, :prerelease
|
|
11
|
+
|
|
12
|
+
def self.parse(value)
|
|
13
|
+
match = VERSION_PATTERN.match(value.to_s)
|
|
14
|
+
raise InvalidVersionError, "Invalid semantic version: #{value.inspect}" unless match
|
|
15
|
+
|
|
16
|
+
new(
|
|
17
|
+
major: match[1].to_i,
|
|
18
|
+
minor: match[2].to_i,
|
|
19
|
+
patch: match[3].to_i,
|
|
20
|
+
prerelease: match[4]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(major:, minor:, patch:, prerelease: nil)
|
|
25
|
+
@major = major
|
|
26
|
+
@minor = minor
|
|
27
|
+
@patch = patch
|
|
28
|
+
@prerelease = prerelease
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def <=>(other)
|
|
32
|
+
other = self.class.parse(other) unless other.is_a?(self.class)
|
|
33
|
+
|
|
34
|
+
[major, minor, patch, release_rank, prerelease.to_s] <=> [other.major, other.minor, other.patch, other.release_rank, other.prerelease.to_s]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def release_rank
|
|
38
|
+
prerelease.nil? ? 1 : 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stable?
|
|
42
|
+
prerelease.nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_s
|
|
46
|
+
base = "#{major}.#{minor}.#{patch}"
|
|
47
|
+
prerelease.nil? ? base : "#{base}-#{prerelease}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect
|
|
51
|
+
"#<#{self.class.name} #{self}>"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "registry/version"
|
|
4
|
+
require_relative "registry/errors"
|
|
5
|
+
require_relative "registry/semver"
|
|
6
|
+
require_relative "registry/front_matter"
|
|
7
|
+
require_relative "registry/context"
|
|
8
|
+
require_relative "registry/prompt"
|
|
9
|
+
require_relative "registry/comparison"
|
|
10
|
+
require_relative "registry/exporter"
|
|
11
|
+
require_relative "registry/importer"
|
|
12
|
+
require_relative "registry/adapters"
|
|
13
|
+
require_relative "registry/filesystem_backend"
|
|
14
|
+
|
|
15
|
+
module RubyLLM
|
|
16
|
+
module Registry
|
|
17
|
+
class Configuration
|
|
18
|
+
attr_accessor :root, :manifest_path, :default_adapter, :default_database_adapter
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@root = nil
|
|
22
|
+
@manifest_path = nil
|
|
23
|
+
@default_adapter = :filesystem
|
|
24
|
+
@default_database_adapter = :sqlite
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
def configuration
|
|
30
|
+
@configuration ||= Configuration.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def configure
|
|
34
|
+
yield(configuration)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset!
|
|
38
|
+
@configuration = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get(path, version: nil, label: nil, root: nil, manifest_path: nil)
|
|
42
|
+
backend(root: root, manifest_path: manifest_path).get(path, version: version, label: label)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def backend(type = nil, **options)
|
|
46
|
+
type = (type || configuration.default_adapter).to_sym
|
|
47
|
+
|
|
48
|
+
case type
|
|
49
|
+
when :filesystem
|
|
50
|
+
FilesystemBackend.new(
|
|
51
|
+
root: options[:root] || configuration.root || default_root,
|
|
52
|
+
manifest_path: options[:manifest_path] || configuration.manifest_path
|
|
53
|
+
)
|
|
54
|
+
when :sqlite, :active_record, :ar, :mongo, :mongodb, :s3
|
|
55
|
+
Adapters.build(type, **options)
|
|
56
|
+
else
|
|
57
|
+
raise ArgumentError, "Unknown backend type: #{type.inspect}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def database_backend(type = nil, **options)
|
|
62
|
+
backend(type || configuration.default_database_adapter, **options)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def export(prompt_or_path, format: :markdown, backend: nil, version: nil, label: nil, **options)
|
|
66
|
+
prompt = if prompt_or_path.is_a?(Prompt)
|
|
67
|
+
prompt_or_path
|
|
68
|
+
elsif backend
|
|
69
|
+
backend.get(prompt_or_path, version: version, label: label)
|
|
70
|
+
else
|
|
71
|
+
get(prompt_or_path, version: version, label: label, **options)
|
|
72
|
+
end
|
|
73
|
+
Exporter.new(prompt).public_send(exporter_method(format))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def import(payload, format: :auto, backend: nil, **options)
|
|
77
|
+
prompt = Importer.new(payload, format: format, **options).to_prompt
|
|
78
|
+
backend&.store(prompt)
|
|
79
|
+
prompt
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def diff(left, right)
|
|
83
|
+
left_prompt = left.respond_to?(:body) ? left : Importer.new(left, format: :auto).to_prompt
|
|
84
|
+
right_prompt = right.respond_to?(:body) ? right : Importer.new(right, format: :auto).to_prompt
|
|
85
|
+
Comparison.new(left_prompt, right_prompt)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def default_root
|
|
89
|
+
local_candidates = %w[prompts app/prompts].map { |relative| File.join(Dir.pwd, relative) }
|
|
90
|
+
local_candidates.find { |candidate| Dir.exist?(candidate) } || File.join(Dir.pwd, "prompts")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def exporter_method(format)
|
|
96
|
+
case format.to_sym
|
|
97
|
+
when :yaml then :to_yaml
|
|
98
|
+
when :json then :to_json
|
|
99
|
+
when :markdown, :md then :to_markdown
|
|
100
|
+
when :hash then :to_h
|
|
101
|
+
else
|
|
102
|
+
raise ArgumentError, "Unsupported export format: #{format.inspect}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_llm-registry
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sal Scotto
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Local-first, versioned prompt storage and rendering for RubyLLM applications.
|
|
13
|
+
email:
|
|
14
|
+
- sal.scotto@gmail.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- ".idea/.gitignore"
|
|
20
|
+
- ".idea/inspectionProfiles/Project_Default.xml"
|
|
21
|
+
- ".idea/jsLibraryMappings.xml"
|
|
22
|
+
- ".idea/misc.xml"
|
|
23
|
+
- ".idea/modules.xml"
|
|
24
|
+
- ".idea/ruby_llm-registry.iml"
|
|
25
|
+
- ".idea/vcs.xml"
|
|
26
|
+
- ".rspec"
|
|
27
|
+
- ".rubocop.yml"
|
|
28
|
+
- CHANGELOG.md
|
|
29
|
+
- CODE_OF_CONDUCT.md
|
|
30
|
+
- LICENSE.txt
|
|
31
|
+
- README.md
|
|
32
|
+
- Rakefile
|
|
33
|
+
- build_release.sh
|
|
34
|
+
- lib/ruby_llm/registry.rb
|
|
35
|
+
- lib/ruby_llm/registry/adapters.rb
|
|
36
|
+
- lib/ruby_llm/registry/adapters/active_record.rb
|
|
37
|
+
- lib/ruby_llm/registry/adapters/base.rb
|
|
38
|
+
- lib/ruby_llm/registry/adapters/mongo.rb
|
|
39
|
+
- lib/ruby_llm/registry/adapters/s3.rb
|
|
40
|
+
- lib/ruby_llm/registry/adapters/sqlite.rb
|
|
41
|
+
- lib/ruby_llm/registry/comparison.rb
|
|
42
|
+
- lib/ruby_llm/registry/context.rb
|
|
43
|
+
- lib/ruby_llm/registry/errors.rb
|
|
44
|
+
- lib/ruby_llm/registry/exporter.rb
|
|
45
|
+
- lib/ruby_llm/registry/filesystem_backend.rb
|
|
46
|
+
- lib/ruby_llm/registry/front_matter.rb
|
|
47
|
+
- lib/ruby_llm/registry/importer.rb
|
|
48
|
+
- lib/ruby_llm/registry/prompt.rb
|
|
49
|
+
- lib/ruby_llm/registry/semver.rb
|
|
50
|
+
- lib/ruby_llm/registry/version.rb
|
|
51
|
+
- sig/ruby_llm/registry.rbs
|
|
52
|
+
homepage: https://github.com/washu/ruby_llm-registry
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://github.com/washu/ruby_llm-registry
|
|
57
|
+
source_code_uri: https://github.com/washu/ruby_llm-registry
|
|
58
|
+
changelog_uri: https://github.com/washu/ruby_llm-registry/blob/main/CHANGELOG.md
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 3.3.0
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 3.6.9
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: Production-grade prompt lifecycle management for RubyLLM
|
|
77
|
+
test_files: []
|