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,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Registry
|
|
7
|
+
module Adapters
|
|
8
|
+
# MongoDB-backed prompt repository.
|
|
9
|
+
class MongoDB < Base
|
|
10
|
+
def initialize(collection: nil, database: nil, collection_name: "ruby_llm_registry_prompts")
|
|
11
|
+
@collection = collection || resolve_collection(database, collection_name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(path, version: nil, label: nil)
|
|
15
|
+
docs = collection.find(path: path).to_a
|
|
16
|
+
raise PromptNotFoundError, "Prompt path not found: #{path}" if docs.empty?
|
|
17
|
+
|
|
18
|
+
doc = if version
|
|
19
|
+
docs.find { |record| record["version"] == Version.parse(version).to_s }
|
|
20
|
+
elsif label
|
|
21
|
+
label = label.to_sym
|
|
22
|
+
docs.find { |record| labels_for(record).include?(label) } || docs.find { |record| record["version"] == label.to_s }
|
|
23
|
+
else
|
|
24
|
+
docs.max_by { |record| Version.parse(record["version"]) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
raise PromptNotFoundError, "Prompt not found: #{path}" unless doc
|
|
28
|
+
|
|
29
|
+
prompt_from_document(doc)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def available_versions(path)
|
|
33
|
+
collection.find(path: path).map { |doc| Version.parse(doc["version"]) }.sort
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def store(prompt, overwrite: false)
|
|
37
|
+
existing = collection.find(path: prompt.path, version: prompt.version.to_s).first
|
|
38
|
+
if existing && !overwrite
|
|
39
|
+
raise Error, "Prompt #{prompt.path}@#{prompt.version} already exists"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
doc = serialize_prompt(prompt)
|
|
43
|
+
existing ? collection.replace_one({ path: prompt.path, version: prompt.version.to_s }, doc) : collection.insert_one(doc)
|
|
44
|
+
prompt
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :collection
|
|
50
|
+
|
|
51
|
+
def resolve_collection(database, collection_name)
|
|
52
|
+
raise ArgumentError, "Provide a collection or database" unless database
|
|
53
|
+
|
|
54
|
+
database[collection_name]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def serialize_prompt(prompt)
|
|
58
|
+
{
|
|
59
|
+
"path" => prompt.path,
|
|
60
|
+
"namespace" => prompt.namespace,
|
|
61
|
+
"name" => prompt.name,
|
|
62
|
+
"version" => prompt.version.to_s,
|
|
63
|
+
"body" => prompt.body,
|
|
64
|
+
"labels" => prompt.labels.map(&:to_s),
|
|
65
|
+
"metadata" => prompt.metadata,
|
|
66
|
+
"required_vars" => prompt.required_vars.map(&:to_s),
|
|
67
|
+
"source_path" => prompt.source_path
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def prompt_from_document(doc)
|
|
72
|
+
Prompt.new(
|
|
73
|
+
name: doc["name"],
|
|
74
|
+
namespace: doc["namespace"],
|
|
75
|
+
version: doc["version"],
|
|
76
|
+
body: doc["body"],
|
|
77
|
+
source_path: doc["source_path"],
|
|
78
|
+
labels: doc["labels"] || [],
|
|
79
|
+
metadata: doc["metadata"] || {},
|
|
80
|
+
required_vars: doc["required_vars"] || []
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def labels_for(doc)
|
|
85
|
+
Array(doc["labels"]).map(&:to_sym)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Registry
|
|
7
|
+
module Adapters
|
|
8
|
+
# S3-backed prompt repository.
|
|
9
|
+
class S3 < Base
|
|
10
|
+
def initialize(client:, bucket:, prefix: "prompts")
|
|
11
|
+
@client = client
|
|
12
|
+
@bucket = bucket
|
|
13
|
+
@prefix = prefix.to_s.sub(%r{\A/+|/+$}, "")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(path, version: nil, label: nil)
|
|
17
|
+
key = object_key(path, version: version, label: label)
|
|
18
|
+
raise PromptNotFoundError, "Prompt not found: #{path}" unless key
|
|
19
|
+
|
|
20
|
+
payload = client.get_object(bucket: bucket, key: key).body.read
|
|
21
|
+
Importer.new(payload, format: :json, path: path).to_prompt
|
|
22
|
+
rescue NoMethodError
|
|
23
|
+
raise PromptNotFoundError, "Prompt not found: #{path}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def available_versions(path)
|
|
27
|
+
prefix_key = object_prefix(path)
|
|
28
|
+
response = client.list_objects_v2(bucket: bucket, prefix: prefix_key)
|
|
29
|
+
Array(response.contents).map do |object|
|
|
30
|
+
version_from_key(object.key)
|
|
31
|
+
end.compact.map { |value| Version.parse(value) }.sort
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def store(prompt, overwrite: false)
|
|
35
|
+
key = object_key(prompt.path, version: prompt.version.to_s)
|
|
36
|
+
if !overwrite && object_exists?(key)
|
|
37
|
+
raise Error, "Prompt #{prompt.path}@#{prompt.version} already exists"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
client.put_object(
|
|
41
|
+
bucket: bucket,
|
|
42
|
+
key: key,
|
|
43
|
+
body: Exporter.new(prompt).to_json,
|
|
44
|
+
content_type: "application/json"
|
|
45
|
+
)
|
|
46
|
+
prompt
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :client, :bucket, :prefix
|
|
52
|
+
|
|
53
|
+
def object_key(path, version: nil, label: nil)
|
|
54
|
+
if version
|
|
55
|
+
"#{object_prefix(path)}/v#{Version.parse(version)}.json"
|
|
56
|
+
elsif label
|
|
57
|
+
label = label.to_sym
|
|
58
|
+
manifest_key = "#{object_prefix(path)}/#{label}.json"
|
|
59
|
+
object_exists?(manifest_key) ? manifest_key : nil
|
|
60
|
+
else
|
|
61
|
+
latest_key(path)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def latest_key(path)
|
|
66
|
+
keys = list_keys(path)
|
|
67
|
+
versioned = keys.map { |key| [version_from_key(key), key] }.compact
|
|
68
|
+
versioned.max_by { |version, _key| Version.parse(version) }&.last
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def list_keys(path)
|
|
72
|
+
response = client.list_objects_v2(bucket: bucket, prefix: object_prefix(path))
|
|
73
|
+
Array(response.contents).map(&:key)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def object_exists?(key)
|
|
77
|
+
client.head_object(bucket: bucket, key: key)
|
|
78
|
+
true
|
|
79
|
+
rescue StandardError
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def object_prefix(path)
|
|
84
|
+
[prefix, path].reject(&:empty?).join("/")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def version_from_key(key)
|
|
88
|
+
basename = key.split("/").last
|
|
89
|
+
basename.sub(/\.(json|yaml|yml)\z/, "").sub(/\Av/, "")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Registry
|
|
8
|
+
module Adapters
|
|
9
|
+
# SQLite-backed prompt repository.
|
|
10
|
+
class SQLite < Base
|
|
11
|
+
def initialize(path:, table_name: "ruby_llm_registry_prompts")
|
|
12
|
+
require_sqlite3!
|
|
13
|
+
@database = SQLite3::Database.new(path)
|
|
14
|
+
@database.results_as_hash = true
|
|
15
|
+
@table_name = table_name
|
|
16
|
+
ensure_schema!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get(path, version: nil, label: nil)
|
|
20
|
+
rows = rows_for_path(path)
|
|
21
|
+
raise PromptNotFoundError, "Prompt path not found: #{path}" if rows.empty?
|
|
22
|
+
|
|
23
|
+
row = if version
|
|
24
|
+
rows.find { |record| record["version"] == Version.parse(version).to_s }
|
|
25
|
+
elsif label
|
|
26
|
+
label = label.to_sym
|
|
27
|
+
rows.find { |record| labels_for(record).include?(label) } || rows.find { |record| record["version"] == label.to_s }
|
|
28
|
+
else
|
|
29
|
+
rows.max_by { |record| Version.parse(record["version"]) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
raise PromptNotFoundError, "Prompt not found: #{path}" unless row
|
|
33
|
+
|
|
34
|
+
prompt_from_row(row)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def available_versions(path)
|
|
38
|
+
rows_for_path(path).map { |row| Version.parse(row["version"]) }.sort
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def store(prompt, overwrite: false)
|
|
42
|
+
existing = rows_for_path(prompt.path).find { |row| row["version"] == prompt.version.to_s }
|
|
43
|
+
if existing && !overwrite
|
|
44
|
+
raise Error, "Prompt #{prompt.path}@#{prompt.version} already exists"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
data = serialize_prompt(prompt)
|
|
48
|
+
if existing
|
|
49
|
+
update_row(data)
|
|
50
|
+
else
|
|
51
|
+
insert_row(data)
|
|
52
|
+
end
|
|
53
|
+
prompt
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader :database, :table_name
|
|
59
|
+
|
|
60
|
+
def require_sqlite3!
|
|
61
|
+
require "sqlite3"
|
|
62
|
+
rescue LoadError => e
|
|
63
|
+
raise LoadError, "sqlite3 gem is required for the SQLite adapter"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ensure_schema!
|
|
67
|
+
database.execute <<~SQL
|
|
68
|
+
CREATE TABLE IF NOT EXISTS #{table_name} (
|
|
69
|
+
path TEXT NOT NULL,
|
|
70
|
+
namespace TEXT NOT NULL,
|
|
71
|
+
name TEXT NOT NULL,
|
|
72
|
+
version TEXT NOT NULL,
|
|
73
|
+
body TEXT NOT NULL,
|
|
74
|
+
labels_json TEXT NOT NULL,
|
|
75
|
+
metadata_json TEXT NOT NULL,
|
|
76
|
+
required_vars_json TEXT NOT NULL,
|
|
77
|
+
source_path TEXT,
|
|
78
|
+
created_at TEXT NOT NULL,
|
|
79
|
+
updated_at TEXT NOT NULL,
|
|
80
|
+
UNIQUE(path, version)
|
|
81
|
+
)
|
|
82
|
+
SQL
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def rows_for_path(path)
|
|
86
|
+
database.execute("SELECT * FROM #{table_name} WHERE path = ?", [path])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def serialize_prompt(prompt)
|
|
90
|
+
{
|
|
91
|
+
path: prompt.path,
|
|
92
|
+
namespace: prompt.namespace,
|
|
93
|
+
name: prompt.name,
|
|
94
|
+
version: prompt.version.to_s,
|
|
95
|
+
body: prompt.body,
|
|
96
|
+
labels_json: JSON.generate(prompt.labels),
|
|
97
|
+
metadata_json: JSON.generate(prompt.metadata),
|
|
98
|
+
required_vars_json: JSON.generate(prompt.required_vars),
|
|
99
|
+
source_path: prompt.source_path,
|
|
100
|
+
created_at: Time.now.utc.iso8601,
|
|
101
|
+
updated_at: Time.now.utc.iso8601
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def insert_row(data)
|
|
106
|
+
database.execute(
|
|
107
|
+
<<~SQL,
|
|
108
|
+
INSERT INTO #{table_name}
|
|
109
|
+
(path, namespace, name, version, body, labels_json, metadata_json, required_vars_json, source_path, created_at, updated_at)
|
|
110
|
+
VALUES
|
|
111
|
+
(:path, :namespace, :name, :version, :body, :labels_json, :metadata_json, :required_vars_json, :source_path, :created_at, :updated_at)
|
|
112
|
+
SQL
|
|
113
|
+
data
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def update_row(data)
|
|
118
|
+
database.execute(
|
|
119
|
+
<<~SQL,
|
|
120
|
+
UPDATE #{table_name}
|
|
121
|
+
SET namespace = :namespace,
|
|
122
|
+
name = :name,
|
|
123
|
+
body = :body,
|
|
124
|
+
labels_json = :labels_json,
|
|
125
|
+
metadata_json = :metadata_json,
|
|
126
|
+
required_vars_json = :required_vars_json,
|
|
127
|
+
source_path = :source_path,
|
|
128
|
+
updated_at = :updated_at
|
|
129
|
+
WHERE path = :path AND version = :version
|
|
130
|
+
SQL
|
|
131
|
+
data
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def prompt_from_row(row)
|
|
136
|
+
Prompt.new(
|
|
137
|
+
name: row["name"],
|
|
138
|
+
namespace: row["namespace"],
|
|
139
|
+
version: row["version"],
|
|
140
|
+
body: row["body"],
|
|
141
|
+
source_path: row["source_path"],
|
|
142
|
+
labels: JSON.parse(row["labels_json"] || "[]"),
|
|
143
|
+
metadata: JSON.parse(row["metadata_json"] || "{}"),
|
|
144
|
+
required_vars: JSON.parse(row["required_vars_json"] || "[]")
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def labels_for(row)
|
|
149
|
+
JSON.parse(row["labels_json"] || "[]").map(&:to_sym)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "adapters/base"
|
|
4
|
+
require_relative "adapters/sqlite"
|
|
5
|
+
require_relative "adapters/active_record"
|
|
6
|
+
require_relative "adapters/mongo"
|
|
7
|
+
require_relative "adapters/s3"
|
|
8
|
+
|
|
9
|
+
module RubyLLM
|
|
10
|
+
module Registry
|
|
11
|
+
module Adapters
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def build(type = :filesystem, **options)
|
|
15
|
+
case type.to_sym
|
|
16
|
+
when :filesystem then FilesystemBackend.new(**options)
|
|
17
|
+
when :sqlite then SQLite.new(**options)
|
|
18
|
+
when :active_record, :ar then ActiveRecord.new(**options)
|
|
19
|
+
when :mongo, :mongodb then MongoDB.new(**options)
|
|
20
|
+
when :s3 then S3.new(**options)
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "Unknown adapter type: #{type.inspect}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Registry
|
|
5
|
+
# Represents a comparison between two prompt revisions.
|
|
6
|
+
class Comparison
|
|
7
|
+
attr_reader :left, :right, :changes
|
|
8
|
+
|
|
9
|
+
def initialize(left, right)
|
|
10
|
+
@left = left
|
|
11
|
+
@right = right
|
|
12
|
+
@changes = build_changes
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def changed?
|
|
16
|
+
changes.any?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def changed_fields
|
|
20
|
+
changes.keys
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def body_diff
|
|
24
|
+
DiffLines.new(left.body.to_s, right.body.to_s).to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
{
|
|
29
|
+
left: snapshot(left),
|
|
30
|
+
right: snapshot(right),
|
|
31
|
+
changes: changes,
|
|
32
|
+
body_diff: body_diff
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
return "No changes" unless changed?
|
|
38
|
+
|
|
39
|
+
parts = changes.map do |field, change|
|
|
40
|
+
"#{field}: #{change[:from].inspect} -> #{change[:to].inspect}"
|
|
41
|
+
end
|
|
42
|
+
"#{parts.join('; ')}\n#{body_diff}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_changes
|
|
48
|
+
fields = %i[version labels metadata required_vars body]
|
|
49
|
+
fields.each_with_object({}) do |field, hash|
|
|
50
|
+
left_value = normalize_field(left.public_send(field))
|
|
51
|
+
right_value = normalize_field(right.public_send(field))
|
|
52
|
+
hash[field] = { from: left_value, to: right_value } unless left_value == right_value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def normalize_field(value)
|
|
57
|
+
case value
|
|
58
|
+
when Version
|
|
59
|
+
value.to_s
|
|
60
|
+
when Array
|
|
61
|
+
value.map { |item| normalize_field(item) }
|
|
62
|
+
when Hash
|
|
63
|
+
value.each_with_object({}) do |(key, item), hash|
|
|
64
|
+
hash[key] = normalize_field(item)
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def snapshot(prompt)
|
|
72
|
+
prompt.respond_to?(:to_h) ? prompt.to_h : prompt
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Minimal line-oriented diff representation for prompt bodies.
|
|
77
|
+
class DiffLines
|
|
78
|
+
def initialize(left, right)
|
|
79
|
+
@left = left.to_s.split("\n")
|
|
80
|
+
@right = right.to_s.split("\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def to_s
|
|
84
|
+
build.join("\n")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
attr_reader :left, :right
|
|
90
|
+
|
|
91
|
+
def build
|
|
92
|
+
output = []
|
|
93
|
+
max = [left.length, right.length].max
|
|
94
|
+
|
|
95
|
+
max.times do |index|
|
|
96
|
+
l = left[index]
|
|
97
|
+
r = right[index]
|
|
98
|
+
|
|
99
|
+
if l == r
|
|
100
|
+
output << " #{l}" if l
|
|
101
|
+
else
|
|
102
|
+
output << "-#{l}" if l
|
|
103
|
+
output << "+#{r}" if r
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
output
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Registry
|
|
5
|
+
# Context object used while rendering ERB prompts.
|
|
6
|
+
class RenderContext
|
|
7
|
+
def initialize(values = {})
|
|
8
|
+
@values = normalize(values)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def method_missing(name, *args)
|
|
12
|
+
raise NoMethodError, "undefined method `#{name}` for #{self}" unless args.empty?
|
|
13
|
+
|
|
14
|
+
@values[name]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def respond_to_missing?(name, _include_private = false)
|
|
18
|
+
@values.key?(name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def current_date
|
|
22
|
+
Date.today
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def current_time
|
|
26
|
+
Time.now
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
alias today current_date
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def normalize(values)
|
|
34
|
+
values.each_with_object({}) do |(key, value), hash|
|
|
35
|
+
hash[key.to_sym] = value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Registry
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class PromptNotFoundError < Error; end
|
|
7
|
+
class InvalidVersionError < Error; end
|
|
8
|
+
class MissingVariableError < Error; end
|
|
9
|
+
class UnknownLabelError < Error; end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Registry
|
|
8
|
+
# Serializes prompt objects into portable formats.
|
|
9
|
+
class Exporter
|
|
10
|
+
def initialize(prompt)
|
|
11
|
+
@prompt = prompt
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
prompt.to_h
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_yaml
|
|
19
|
+
YAML.dump(to_h)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_json(*args)
|
|
23
|
+
JSON.pretty_generate(to_h, *args)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_markdown
|
|
27
|
+
<<~MARKDOWN.chomp
|
|
28
|
+
---
|
|
29
|
+
#{YAML.dump(front_matter).sub(/\A---\s*\n/, "").sub(/\n\z/, "")}
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
#{prompt.body}
|
|
33
|
+
MARKDOWN
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :prompt
|
|
39
|
+
|
|
40
|
+
def front_matter
|
|
41
|
+
{
|
|
42
|
+
version: prompt.version.to_s,
|
|
43
|
+
labels: prompt.labels,
|
|
44
|
+
required_vars: prompt.required_vars,
|
|
45
|
+
metadata: prompt.metadata,
|
|
46
|
+
name: prompt.name,
|
|
47
|
+
namespace: prompt.namespace,
|
|
48
|
+
source_path: prompt.source_path
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|