prompt_canary 0.3.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/CHANGELOG.md +86 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +338 -0
- data/Rakefile +12 -0
- data/app/controllers/prompt_canary/application_controller.rb +6 -0
- data/app/controllers/prompt_canary/dashboard/prompts_controller.rb +69 -0
- data/app/views/layouts/prompt_canary/application.html.erb +42 -0
- data/app/views/prompt_canary/dashboard/prompts/index.html.erb +50 -0
- data/app/views/prompt_canary/dashboard/prompts/show.html.erb +114 -0
- data/config/routes.rb +12 -0
- data/examples/auto_rollback.rb +105 -0
- data/examples/demo.rb +83 -0
- data/exe/prompt_canary +6 -0
- data/lib/generators/prompt_canary/install_generator.rb +39 -0
- data/lib/generators/prompt_canary/templates/create_prompt_canary_calls.rb +62 -0
- data/lib/prompt_canary/adapter_factory.rb +16 -0
- data/lib/prompt_canary/adapters/anthropic.rb +39 -0
- data/lib/prompt_canary/adapters/base.rb +11 -0
- data/lib/prompt_canary/cli/commands/history.rb +63 -0
- data/lib/prompt_canary/cli/commands/status.rb +55 -0
- data/lib/prompt_canary/cli.rb +69 -0
- data/lib/prompt_canary/configuration.rb +31 -0
- data/lib/prompt_canary/deployment.rb +186 -0
- data/lib/prompt_canary/engine.rb +7 -0
- data/lib/prompt_canary/monitor.rb +30 -0
- data/lib/prompt_canary/monitor_job.rb +13 -0
- data/lib/prompt_canary/prompt.rb +13 -0
- data/lib/prompt_canary/prompt_executor.rb +27 -0
- data/lib/prompt_canary/promptable.rb +50 -0
- data/lib/prompt_canary/railtie.rb +28 -0
- data/lib/prompt_canary/recorder.rb +55 -0
- data/lib/prompt_canary/result.rb +18 -0
- data/lib/prompt_canary/rollback_rule.rb +22 -0
- data/lib/prompt_canary/router.rb +61 -0
- data/lib/prompt_canary/storage/active_record_adapter.rb +58 -0
- data/lib/prompt_canary/storage/memory.rb +21 -0
- data/lib/prompt_canary/storage/sqlite.rb +64 -0
- data/lib/prompt_canary/storage_factory.rb +24 -0
- data/lib/prompt_canary/version.rb +5 -0
- data/lib/prompt_canary/version_builder.rb +52 -0
- data/lib/prompt_canary/version_object.rb +63 -0
- data/lib/prompt_canary.rb +101 -0
- data/sig/prompt_canary.rbs +4 -0
- metadata +95 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptCanary
|
|
4
|
+
class Router
|
|
5
|
+
def self.choose(prompt_class, context)
|
|
6
|
+
primary = db_primary(prompt_class) || prompt_class.primary_version
|
|
7
|
+
partial = prompt_class.versions.find { |v| v.partial_rollout? || v.predicate? }
|
|
8
|
+
return primary unless partial
|
|
9
|
+
return primary if demoted?(prompt_class.name, partial.name)
|
|
10
|
+
|
|
11
|
+
route_partial(partial, primary, context, prompt_class.name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.route_partial(partial, primary, context, prompt_name)
|
|
15
|
+
return partial if partial.matches_predicate?(context)
|
|
16
|
+
|
|
17
|
+
call_id = context[:call_id]
|
|
18
|
+
return primary unless call_id
|
|
19
|
+
|
|
20
|
+
percent = canary_percent(prompt_name, partial.name, partial.rollout.fetch(:percent, 0))
|
|
21
|
+
Zlib.crc32(call_id.to_s) % 100 < percent ? partial : primary
|
|
22
|
+
end
|
|
23
|
+
private_class_method :route_partial
|
|
24
|
+
|
|
25
|
+
def self.canary_percent(prompt_name, version_name, default)
|
|
26
|
+
return default unless defined?(PromptCanary::RolloutOverride)
|
|
27
|
+
|
|
28
|
+
override = PromptCanary::RolloutOverride
|
|
29
|
+
.where(prompt: prompt_name, version: version_name)
|
|
30
|
+
.where("rollout_override > 0")
|
|
31
|
+
.first
|
|
32
|
+
override ? override.rollout_override : default
|
|
33
|
+
rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::StatementInvalid
|
|
34
|
+
default
|
|
35
|
+
end
|
|
36
|
+
private_class_method :canary_percent
|
|
37
|
+
|
|
38
|
+
def self.db_primary(prompt_class)
|
|
39
|
+
return nil unless defined?(PromptCanary::PrimaryOverride)
|
|
40
|
+
|
|
41
|
+
override = PromptCanary::PrimaryOverride.find_by(prompt: prompt_class.name)
|
|
42
|
+
return nil unless override
|
|
43
|
+
|
|
44
|
+
prompt_class.versions.find { |v| v.name == override.version }
|
|
45
|
+
rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::StatementInvalid
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
private_class_method :db_primary
|
|
49
|
+
|
|
50
|
+
def self.demoted?(prompt_name, version_name)
|
|
51
|
+
return false unless defined?(PromptCanary::RolloutOverride)
|
|
52
|
+
|
|
53
|
+
PromptCanary::RolloutOverride
|
|
54
|
+
.where(prompt: prompt_name, version: version_name, rollout_override: 0)
|
|
55
|
+
.exists?
|
|
56
|
+
rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::StatementInvalid
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
private_class_method :demoted?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module PromptCanary
|
|
7
|
+
class Call < ::ActiveRecord::Base
|
|
8
|
+
self.table_name = "prompt_canary_calls"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class RolloutOverride < ::ActiveRecord::Base
|
|
12
|
+
self.table_name = "prompt_canary_rollout_overrides"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class PrimaryOverride < ::ActiveRecord::Base
|
|
16
|
+
self.table_name = "prompt_canary_primary_overrides"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class PromptEvent < ::ActiveRecord::Base
|
|
20
|
+
self.table_name = "prompt_canary_events"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Storage
|
|
24
|
+
class ActiveRecord
|
|
25
|
+
def write(record)
|
|
26
|
+
Call.create!(
|
|
27
|
+
prompt: record[:prompt],
|
|
28
|
+
version: record[:version],
|
|
29
|
+
latency_ms: record[:latency_ms],
|
|
30
|
+
tokens: record[:tokens]&.to_json,
|
|
31
|
+
error: record[:error]&.message,
|
|
32
|
+
recorded_at: record[:recorded_at]
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read_recent(prompt:, version:, limit:)
|
|
37
|
+
Call.where(prompt: prompt, version: version)
|
|
38
|
+
.order(recorded_at: :desc)
|
|
39
|
+
.limit(limit)
|
|
40
|
+
.reverse
|
|
41
|
+
.map { |row| deserialize(row) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def deserialize(row)
|
|
47
|
+
{
|
|
48
|
+
prompt: row.prompt,
|
|
49
|
+
version: row.version,
|
|
50
|
+
latency_ms: row.latency_ms,
|
|
51
|
+
tokens: row.tokens ? JSON.parse(row.tokens, symbolize_names: true) : nil,
|
|
52
|
+
error: row.error ? StandardError.new(row.error) : nil,
|
|
53
|
+
recorded_at: row.recorded_at
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptCanary
|
|
4
|
+
module Storage
|
|
5
|
+
class Memory
|
|
6
|
+
def initialize
|
|
7
|
+
@records = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def write(record)
|
|
11
|
+
@records << record
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def read_recent(prompt:, version:, limit:)
|
|
15
|
+
@records
|
|
16
|
+
.select { |r| r[:prompt] == prompt && r[:version] == version }
|
|
17
|
+
.last(limit)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sqlite3"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module PromptCanary
|
|
7
|
+
module Storage
|
|
8
|
+
class SQLite
|
|
9
|
+
def initialize(path: "prompt_canary.db")
|
|
10
|
+
@db = ::SQLite3::Database.new(path)
|
|
11
|
+
@db.results_as_hash = true
|
|
12
|
+
create_table
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def write(record)
|
|
16
|
+
@db.execute(
|
|
17
|
+
"INSERT INTO calls (prompt, version, latency_ms, tokens, error, recorded_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
18
|
+
[
|
|
19
|
+
record[:prompt],
|
|
20
|
+
record[:version],
|
|
21
|
+
record[:latency_ms],
|
|
22
|
+
JSON.dump(record[:tokens]),
|
|
23
|
+
record[:error]&.message,
|
|
24
|
+
record[:recorded_at].iso8601
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read_recent(prompt:, version:, limit:)
|
|
30
|
+
rows = @db.execute(
|
|
31
|
+
"SELECT * FROM calls WHERE prompt = ? AND version = ? ORDER BY recorded_at DESC LIMIT ?",
|
|
32
|
+
[prompt, version, limit]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
rows.reverse.map do |row|
|
|
36
|
+
{
|
|
37
|
+
prompt: row["prompt"],
|
|
38
|
+
version: row["version"],
|
|
39
|
+
latency_ms: row["latency_ms"],
|
|
40
|
+
tokens: row["tokens"] ? JSON.parse(row["tokens"], symbolize_names: true) : nil,
|
|
41
|
+
error: row["error"] ? StandardError.new(row["error"]) : nil,
|
|
42
|
+
recorded_at: Time.parse(row["recorded_at"])
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def create_table
|
|
50
|
+
@db.execute(<<~SQL)
|
|
51
|
+
CREATE TABLE IF NOT EXISTS calls (
|
|
52
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
prompt TEXT NOT NULL,
|
|
54
|
+
version TEXT NOT NULL,
|
|
55
|
+
latency_ms INTEGER,
|
|
56
|
+
tokens TEXT,
|
|
57
|
+
error TEXT,
|
|
58
|
+
recorded_at TEXT NOT NULL
|
|
59
|
+
)
|
|
60
|
+
SQL
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptCanary
|
|
4
|
+
class StorageFactory
|
|
5
|
+
REGISTRY = {
|
|
6
|
+
memory: -> { Storage::Memory.new },
|
|
7
|
+
sqlite: lambda {
|
|
8
|
+
require "prompt_canary/storage/sqlite"
|
|
9
|
+
Storage::SQLite.new
|
|
10
|
+
},
|
|
11
|
+
active_record: lambda {
|
|
12
|
+
require "prompt_canary/storage/active_record_adapter"
|
|
13
|
+
Storage::ActiveRecord.new
|
|
14
|
+
}
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def self.build(storage_name)
|
|
18
|
+
builder = REGISTRY[storage_name]
|
|
19
|
+
raise ConfigurationError, "Unknown storage: #{storage_name.inspect}" unless builder
|
|
20
|
+
|
|
21
|
+
builder.call
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptCanary
|
|
4
|
+
class VersionBuilder
|
|
5
|
+
attr_reader :_name, :_model, :_system, :_rollout
|
|
6
|
+
|
|
7
|
+
def initialize(name)
|
|
8
|
+
@_name = name
|
|
9
|
+
@_rollout = {}
|
|
10
|
+
@_rollback_rules = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def rollout(value)
|
|
14
|
+
@_rollout = value
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def rollout_to(&block)
|
|
18
|
+
@_predicate = block
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def rollback_if(metric, over:, greater_than: nil, less_than: nil)
|
|
22
|
+
comparator = if greater_than
|
|
23
|
+
:greater_than
|
|
24
|
+
elsif less_than
|
|
25
|
+
:less_than
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "rollback_if requires greater_than: or less_than:"
|
|
28
|
+
end
|
|
29
|
+
threshold = greater_than || less_than
|
|
30
|
+
@_rollback_rules << RollbackRule.new(metric: metric, threshold: threshold, comparator: comparator, window: over)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def model(value)
|
|
34
|
+
@_model = value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def system(value = nil, &block)
|
|
38
|
+
@_system = block || value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build
|
|
42
|
+
Version.new(
|
|
43
|
+
name: _name,
|
|
44
|
+
model: _model,
|
|
45
|
+
system: _system,
|
|
46
|
+
rollout: _rollout,
|
|
47
|
+
predicate: @_predicate,
|
|
48
|
+
rollback_rules: @_rollback_rules
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module PromptCanary
|
|
6
|
+
class Version
|
|
7
|
+
attr_reader :name, :model, :system, :rollout, :rollback_rules
|
|
8
|
+
|
|
9
|
+
def initialize(name:, model:, system:, rollout:, predicate: nil, rollback_rules: [])
|
|
10
|
+
@name = name
|
|
11
|
+
@model = model
|
|
12
|
+
@system = system
|
|
13
|
+
@rollout = rollout
|
|
14
|
+
@predicate = predicate
|
|
15
|
+
@rollback_rules = rollback_rules
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def system_for(args = {})
|
|
19
|
+
@system.respond_to?(:call) ? @system.call(args) : @system
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def predicate?
|
|
23
|
+
!@predicate.nil?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def matches_predicate?(context)
|
|
27
|
+
return false unless @predicate
|
|
28
|
+
|
|
29
|
+
@predicate.call(context)
|
|
30
|
+
rescue StandardError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def partial_rollout?
|
|
35
|
+
rollout.fetch(:percent, 0).positive?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def routes?(key)
|
|
39
|
+
roll = Zlib.crc32(key.to_s) % 100
|
|
40
|
+
roll < rollout.fetch(:percent, 0)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def set_rollout!(percent)
|
|
44
|
+
@rollout = { percent: percent }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def demoted?
|
|
48
|
+
@demoted || false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def demote!
|
|
52
|
+
@demoted = true
|
|
53
|
+
@previous_rollout = @rollout.fetch(:percent, 0)
|
|
54
|
+
@rollout = { percent: 0 }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def restore!
|
|
58
|
+
@demoted = false
|
|
59
|
+
@rollout = { percent: @previous_rollout || 0 }
|
|
60
|
+
@previous_rollout = nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptCanary
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class ConfigurationError < Error; end
|
|
6
|
+
class DuplicateVersionError < Error; end
|
|
7
|
+
class NoPrimaryVersionError < Error; end
|
|
8
|
+
class UnknownVersionError < Error; end
|
|
9
|
+
class CannotDemotePrimaryError < Error; end
|
|
10
|
+
class DemotedVersionError < Error; end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require_relative "prompt_canary/deployment"
|
|
14
|
+
|
|
15
|
+
module PromptCanary
|
|
16
|
+
class << self
|
|
17
|
+
include Deployment
|
|
18
|
+
|
|
19
|
+
def configure
|
|
20
|
+
yield configuration
|
|
21
|
+
configuration.validate!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def register_prompt(klass)
|
|
29
|
+
registered_prompts << klass
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def registered_prompts
|
|
33
|
+
@registered_prompts ||= []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_configuration!
|
|
37
|
+
@configuration = nil
|
|
38
|
+
@registered_prompts = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def load_prompt_classes(path, loader: method(:require))
|
|
42
|
+
return unless File.directory?(path)
|
|
43
|
+
|
|
44
|
+
Dir[File.join(path, "**", "*.rb")].sort.each { |f| loader.call(f) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_storage_config!(logger)
|
|
48
|
+
return unless configuration.storage == :sqlite
|
|
49
|
+
|
|
50
|
+
logger.warn(
|
|
51
|
+
"[PromptCanary] storage: :sqlite is not recommended for multi-process Rails deployments. " \
|
|
52
|
+
"Run `rails generate prompt_canary:install && rails db:migrate` " \
|
|
53
|
+
"and set `storage: :active_record`."
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stats(prompt_class, version_name, over: 100)
|
|
58
|
+
recorder = Recorder.new(storage: StorageFactory.build(configuration.storage))
|
|
59
|
+
recorder.stats(prompt: prompt_class.name, version: version_name, over: over)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def subscribe(event, &block)
|
|
63
|
+
subscribers[event] << block
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reset_subscribers!
|
|
67
|
+
@subscribers = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def publish(event, payload = {})
|
|
73
|
+
subscribers[event].each { |sub| sub.call(payload) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def subscribers
|
|
77
|
+
@subscribers ||= Hash.new { |h, k| h[k] = [] }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
require_relative "prompt_canary/version"
|
|
83
|
+
require_relative "prompt_canary/configuration"
|
|
84
|
+
require_relative "prompt_canary/rollback_rule"
|
|
85
|
+
require_relative "prompt_canary/result"
|
|
86
|
+
require_relative "prompt_canary/version_object"
|
|
87
|
+
require_relative "prompt_canary/version_builder"
|
|
88
|
+
require_relative "prompt_canary/promptable"
|
|
89
|
+
require_relative "prompt_canary/prompt"
|
|
90
|
+
require_relative "prompt_canary/router"
|
|
91
|
+
require_relative "prompt_canary/adapters/base"
|
|
92
|
+
require_relative "prompt_canary/adapters/anthropic"
|
|
93
|
+
require_relative "prompt_canary/adapter_factory"
|
|
94
|
+
require_relative "prompt_canary/storage/memory"
|
|
95
|
+
require_relative "prompt_canary/storage_factory"
|
|
96
|
+
require_relative "prompt_canary/prompt_executor"
|
|
97
|
+
require_relative "prompt_canary/recorder"
|
|
98
|
+
require_relative "prompt_canary/monitor"
|
|
99
|
+
require_relative "prompt_canary/cli"
|
|
100
|
+
require_relative "prompt_canary/railtie" if defined?(Rails::Railtie)
|
|
101
|
+
require_relative "prompt_canary/engine" if defined?(Rails::Engine)
|
metadata
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: prompt_canary
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rockwell Windsor Rice
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Declare prompts as Ruby classes with versioned configurations, route
|
|
14
|
+
traffic by percentage or predicate, record telemetry, and automatically roll back
|
|
15
|
+
misbehaving versions.
|
|
16
|
+
email:
|
|
17
|
+
- rockwellwindsor@gmail.com
|
|
18
|
+
executables:
|
|
19
|
+
- prompt_canary
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- CHANGELOG.md
|
|
24
|
+
- CODE_OF_CONDUCT.md
|
|
25
|
+
- CONTRIBUTING.md
|
|
26
|
+
- LICENSE.txt
|
|
27
|
+
- README.md
|
|
28
|
+
- Rakefile
|
|
29
|
+
- app/controllers/prompt_canary/application_controller.rb
|
|
30
|
+
- app/controllers/prompt_canary/dashboard/prompts_controller.rb
|
|
31
|
+
- app/views/layouts/prompt_canary/application.html.erb
|
|
32
|
+
- app/views/prompt_canary/dashboard/prompts/index.html.erb
|
|
33
|
+
- app/views/prompt_canary/dashboard/prompts/show.html.erb
|
|
34
|
+
- config/routes.rb
|
|
35
|
+
- examples/auto_rollback.rb
|
|
36
|
+
- examples/demo.rb
|
|
37
|
+
- exe/prompt_canary
|
|
38
|
+
- lib/generators/prompt_canary/install_generator.rb
|
|
39
|
+
- lib/generators/prompt_canary/templates/create_prompt_canary_calls.rb
|
|
40
|
+
- lib/prompt_canary.rb
|
|
41
|
+
- lib/prompt_canary/adapter_factory.rb
|
|
42
|
+
- lib/prompt_canary/adapters/anthropic.rb
|
|
43
|
+
- lib/prompt_canary/adapters/base.rb
|
|
44
|
+
- lib/prompt_canary/cli.rb
|
|
45
|
+
- lib/prompt_canary/cli/commands/history.rb
|
|
46
|
+
- lib/prompt_canary/cli/commands/status.rb
|
|
47
|
+
- lib/prompt_canary/configuration.rb
|
|
48
|
+
- lib/prompt_canary/deployment.rb
|
|
49
|
+
- lib/prompt_canary/engine.rb
|
|
50
|
+
- lib/prompt_canary/monitor.rb
|
|
51
|
+
- lib/prompt_canary/monitor_job.rb
|
|
52
|
+
- lib/prompt_canary/prompt.rb
|
|
53
|
+
- lib/prompt_canary/prompt_executor.rb
|
|
54
|
+
- lib/prompt_canary/promptable.rb
|
|
55
|
+
- lib/prompt_canary/railtie.rb
|
|
56
|
+
- lib/prompt_canary/recorder.rb
|
|
57
|
+
- lib/prompt_canary/result.rb
|
|
58
|
+
- lib/prompt_canary/rollback_rule.rb
|
|
59
|
+
- lib/prompt_canary/router.rb
|
|
60
|
+
- lib/prompt_canary/storage/active_record_adapter.rb
|
|
61
|
+
- lib/prompt_canary/storage/memory.rb
|
|
62
|
+
- lib/prompt_canary/storage/sqlite.rb
|
|
63
|
+
- lib/prompt_canary/storage_factory.rb
|
|
64
|
+
- lib/prompt_canary/version.rb
|
|
65
|
+
- lib/prompt_canary/version_builder.rb
|
|
66
|
+
- lib/prompt_canary/version_object.rb
|
|
67
|
+
- sig/prompt_canary.rbs
|
|
68
|
+
homepage: https://github.com/rockwellwindsor/prompt_canary
|
|
69
|
+
licenses:
|
|
70
|
+
- MIT
|
|
71
|
+
metadata:
|
|
72
|
+
allowed_push_host: https://rubygems.org
|
|
73
|
+
homepage_uri: https://github.com/rockwellwindsor/prompt_canary
|
|
74
|
+
source_code_uri: https://github.com/rockwellwindsor/prompt_canary
|
|
75
|
+
changelog_uri: https://github.com/rockwellwindsor/prompt_canary/blob/main/CHANGELOG.md
|
|
76
|
+
post_install_message:
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: 3.2.0
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 3.4.1
|
|
92
|
+
signing_key:
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Canary deploys and automatic rollback for LLM prompts in Ruby.
|
|
95
|
+
test_files: []
|