prompt_warden 0.1.0 → 0.1.1
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 +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +33 -2
- data/Gemfile +4 -4
- data/Gemfile.lock +21 -1
- data/README.md +217 -19
- data/Rakefile +19 -2
- data/bin/console +3 -3
- data/bin/pw_tail +8 -0
- data/examples/policy.yml +22 -0
- data/lib/prompt_warden/adapter.rb +59 -0
- data/lib/prompt_warden/buffer.rb +60 -0
- data/lib/prompt_warden/cli.rb +199 -0
- data/lib/prompt_warden/configuration.rb +39 -0
- data/lib/prompt_warden/cost_calculator.rb +105 -0
- data/lib/prompt_warden/event.rb +18 -0
- data/lib/prompt_warden/instrumentation/anthropic.rb +85 -0
- data/lib/prompt_warden/instrumentation/langchain.rb +76 -0
- data/lib/prompt_warden/instrumentation/openai.rb +79 -0
- data/lib/prompt_warden/policy.rb +73 -0
- data/lib/prompt_warden/railtie.rb +15 -0
- data/lib/prompt_warden/uploader.rb +93 -0
- data/lib/prompt_warden/version.rb +1 -1
- data/lib/prompt_warden.rb +32 -3
- data/prompt_warden.gemspec +33 -25
- data/spec/adapter_auto_detect_spec.rb +65 -0
- data/spec/anthropic_adapter_spec.rb +137 -0
- data/spec/buffer_spec.rb +44 -0
- data/spec/cli_spec.rb +255 -0
- data/spec/configuration_spec.rb +30 -0
- data/spec/cost_calculator_spec.rb +216 -0
- data/spec/event_spec.rb +30 -0
- data/spec/langchain_adapter_spec.rb +139 -0
- data/spec/openai_adapter_spec.rb +153 -0
- data/spec/policy_spec.rb +170 -0
- data/spec/prompt_warden_spec.rb +2 -2
- data/spec/spec_helper.rb +7 -8
- data/spec/uploader_spec.rb +79 -0
- metadata +98 -15
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../lib/prompt_warden/instrumentation/openai'
|
4
|
+
|
5
|
+
RSpec.describe 'PromptWarden OpenAI adapter' do
|
6
|
+
before do
|
7
|
+
require 'prompt_warden'
|
8
|
+
PromptWarden.configure { |c| c.project_token = 'tok' }
|
9
|
+
allow(PromptWarden).to receive(:record)
|
10
|
+
allow(PromptWarden::Policy.instance).to receive(:check_alerts).and_return([])
|
11
|
+
allow(PromptWarden).to receive(:calculate_cost).and_return(0.005)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'records prompt usage via instrumentation for chat method' do
|
15
|
+
client = Object.new
|
16
|
+
client.extend(PromptWarden::Instrumentation::OpenAI)
|
17
|
+
# Call the instrumentation method directly
|
18
|
+
client.send(:_pw_wrap, :chat, {
|
19
|
+
messages: [{ role: 'user', content: 'hi' }],
|
20
|
+
model: 'gpt-4o',
|
21
|
+
max_tokens: 1000
|
22
|
+
}) do
|
23
|
+
{ 'choices' => [{ 'message' => { 'content' => 'openai reply' } }] }
|
24
|
+
end
|
25
|
+
expect(PromptWarden).to have_received(:record).with(hash_including(
|
26
|
+
prompt: 'hi',
|
27
|
+
model: 'gpt-4o',
|
28
|
+
cost_usd: 0.005,
|
29
|
+
status: 'ok',
|
30
|
+
alerts: []
|
31
|
+
))
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'records prompt usage via instrumentation for completions method' do
|
35
|
+
client = Object.new
|
36
|
+
client.extend(PromptWarden::Instrumentation::OpenAI)
|
37
|
+
# Call the instrumentation method directly
|
38
|
+
client.send(:_pw_wrap, :completions, {
|
39
|
+
prompt: 'hello',
|
40
|
+
model: 'gpt-3.5-turbo',
|
41
|
+
max_tokens: 500
|
42
|
+
}) do
|
43
|
+
{ 'choices' => [{ 'message' => { 'content' => 'openai completion' } }] }
|
44
|
+
end
|
45
|
+
expect(PromptWarden).to have_received(:record).with(hash_including(
|
46
|
+
prompt: 'hello',
|
47
|
+
model: 'gpt-3.5-turbo',
|
48
|
+
cost_usd: 0.005,
|
49
|
+
status: 'ok',
|
50
|
+
alerts: []
|
51
|
+
))
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'includes alerts when policy alerts are detected' do
|
55
|
+
alerts = [{ type: 'regex', rule: '/confidential/i', level: 'warn' }]
|
56
|
+
allow(PromptWarden::Policy.instance).to receive(:check_alerts).and_return(alerts)
|
57
|
+
client = Object.new
|
58
|
+
client.extend(PromptWarden::Instrumentation::OpenAI)
|
59
|
+
# Call the instrumentation method directly
|
60
|
+
client.send(:_pw_wrap, :chat, {
|
61
|
+
messages: [{ role: 'user', content: 'This is confidential information' }],
|
62
|
+
model: 'gpt-4o'
|
63
|
+
}) do
|
64
|
+
{ 'choices' => [{ 'message' => { 'content' => 'openai reply' } }] }
|
65
|
+
end
|
66
|
+
expect(PromptWarden).to have_received(:record).with(hash_including(
|
67
|
+
prompt: 'This is confidential information',
|
68
|
+
model: 'gpt-4o',
|
69
|
+
status: 'ok',
|
70
|
+
alerts: alerts
|
71
|
+
))
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'records failed status on error with alerts' do
|
75
|
+
alerts = [{ type: 'regex', rule: '/ETA/i', level: 'warn' }]
|
76
|
+
allow(PromptWarden::Policy.instance).to receive(:check_alerts).and_return(alerts)
|
77
|
+
client = Object.new
|
78
|
+
client.extend(PromptWarden::Instrumentation::OpenAI)
|
79
|
+
expect {
|
80
|
+
client.send(:_pw_wrap, :chat, {
|
81
|
+
messages: [{ role: 'user', content: 'What is the ETA?' }],
|
82
|
+
model: 'gpt-4o'
|
83
|
+
}) do
|
84
|
+
raise 'test error'
|
85
|
+
end
|
86
|
+
}.to raise_error('test error')
|
87
|
+
expect(PromptWarden).to have_received(:record).with(hash_including(
|
88
|
+
prompt: 'What is the ETA?',
|
89
|
+
model: 'gpt-4o',
|
90
|
+
status: 'failed',
|
91
|
+
alerts: alerts
|
92
|
+
))
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'extracts prompt from messages array' do
|
96
|
+
client = Object.new
|
97
|
+
client.extend(PromptWarden::Instrumentation::OpenAI)
|
98
|
+
# Call the instrumentation method directly
|
99
|
+
client.send(:_pw_wrap, :chat, {
|
100
|
+
messages: [
|
101
|
+
{ role: 'user', content: 'Hello' },
|
102
|
+
{ role: 'assistant', content: 'Hi there' },
|
103
|
+
{ role: 'user', content: 'How are you?' }
|
104
|
+
],
|
105
|
+
model: 'gpt-4o'
|
106
|
+
}) do
|
107
|
+
{ 'choices' => [{ 'message' => { 'content' => 'openai reply' } }] }
|
108
|
+
end
|
109
|
+
expect(PromptWarden).to have_received(:record).with(hash_including(
|
110
|
+
prompt: "Hello\nHi there\nHow are you?",
|
111
|
+
model: 'gpt-4o',
|
112
|
+
status: 'ok',
|
113
|
+
alerts: []
|
114
|
+
))
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'uses enhanced cost calculation for estimates and actual costs' do
|
118
|
+
client = Object.new
|
119
|
+
client.extend(PromptWarden::Instrumentation::OpenAI)
|
120
|
+
|
121
|
+
# Mock cost calculation calls
|
122
|
+
allow(PromptWarden).to receive(:calculate_cost).with(
|
123
|
+
prompt: 'hi',
|
124
|
+
model: 'gpt-4o'
|
125
|
+
).and_return(0.003) # Estimate
|
126
|
+
|
127
|
+
allow(PromptWarden).to receive(:calculate_cost).with(
|
128
|
+
prompt: 'hi',
|
129
|
+
model: 'gpt-4o',
|
130
|
+
response_tokens: 50
|
131
|
+
).and_return(0.005) # Actual cost
|
132
|
+
|
133
|
+
client.send(:_pw_wrap, :chat, {
|
134
|
+
messages: [{ role: 'user', content: 'hi' }],
|
135
|
+
model: 'gpt-4o'
|
136
|
+
}) do
|
137
|
+
{
|
138
|
+
'choices' => [{ 'message' => { 'content' => 'openai reply' } }],
|
139
|
+
'usage' => { 'completion_tokens' => 50 }
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
expect(PromptWarden).to have_received(:calculate_cost).with(
|
144
|
+
prompt: 'hi',
|
145
|
+
model: 'gpt-4o'
|
146
|
+
)
|
147
|
+
expect(PromptWarden).to have_received(:calculate_cost).with(
|
148
|
+
prompt: 'hi',
|
149
|
+
model: 'gpt-4o',
|
150
|
+
response_tokens: 50
|
151
|
+
)
|
152
|
+
end
|
153
|
+
end
|
data/spec/policy_spec.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
RSpec.describe PromptWarden::Policy do
|
7
|
+
let(:policy_path) { File.join(Dir.mktmpdir, 'policy.yml') }
|
8
|
+
|
9
|
+
def write_rules(rules)
|
10
|
+
File.write(policy_path, rules.to_yaml)
|
11
|
+
ENV['PROMPT_WARDEN_POLICY'] = policy_path
|
12
|
+
PromptWarden::Policy.instance.reload!
|
13
|
+
end
|
14
|
+
|
15
|
+
after { ENV.delete('PROMPT_WARDEN_POLICY') }
|
16
|
+
|
17
|
+
# -----------------------------------------------------------
|
18
|
+
it 'blocks when cost exceeds max_cost_usd' do
|
19
|
+
write_rules(max_cost_usd: 0.01)
|
20
|
+
|
21
|
+
policy = described_class.instance
|
22
|
+
expect do
|
23
|
+
policy.check!(prompt: 'hi', cost_estimate: 0.02)
|
24
|
+
end.to raise_error(PromptWarden::PolicyError, /exceeds/)
|
25
|
+
end
|
26
|
+
|
27
|
+
# -----------------------------------------------------------
|
28
|
+
it 'blocks when prompt matches reject regex' do
|
29
|
+
write_rules(reject_if_regex: ['/password/i'])
|
30
|
+
|
31
|
+
policy = described_class.instance
|
32
|
+
expect do
|
33
|
+
policy.check!(prompt: 'My password is 123', cost_estimate: 0)
|
34
|
+
end.to raise_error(PromptWarden::PolicyError, /reject regex/)
|
35
|
+
end
|
36
|
+
|
37
|
+
# -----------------------------------------------------------
|
38
|
+
it 'returns :ok for compliant prompt' do
|
39
|
+
write_rules(max_cost_usd: 1, reject_if_regex: [])
|
40
|
+
|
41
|
+
policy = described_class.instance
|
42
|
+
result = policy.check!(prompt: 'hello', cost_estimate: 0.5)
|
43
|
+
expect(result).to eq :ok
|
44
|
+
end
|
45
|
+
|
46
|
+
# -----------------------------------------------------------
|
47
|
+
describe '#check_alerts' do
|
48
|
+
it 'returns empty array when no alert rules configured' do
|
49
|
+
write_rules({})
|
50
|
+
|
51
|
+
policy = described_class.instance
|
52
|
+
alerts = policy.check_alerts(prompt: 'any text', cost_estimate: 0.1)
|
53
|
+
expect(alerts).to eq []
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns empty array when prompt does not match warn regex' do
|
57
|
+
write_rules(warn_if_regex: ['/password/i'])
|
58
|
+
|
59
|
+
policy = described_class.instance
|
60
|
+
alerts = policy.check_alerts(prompt: 'hello world', cost_estimate: 0.1)
|
61
|
+
expect(alerts).to eq []
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'returns alert when prompt matches warn regex' do
|
65
|
+
write_rules(warn_if_regex: ['/password/i'])
|
66
|
+
|
67
|
+
policy = described_class.instance
|
68
|
+
alerts = policy.check_alerts(prompt: 'My password is 123', cost_estimate: 0.1)
|
69
|
+
expect(alerts).to eq [{ type: 'regex', rule: '/password/i', level: 'warn' }]
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'returns multiple alerts when prompt matches multiple warn regexes' do
|
73
|
+
write_rules(warn_if_regex: ['/password/i', '/ssn/i'])
|
74
|
+
|
75
|
+
policy = described_class.instance
|
76
|
+
alerts = policy.check_alerts(prompt: 'My password is 123 and SSN is 456', cost_estimate: 0.1)
|
77
|
+
expect(alerts).to contain_exactly(
|
78
|
+
{ type: 'regex', rule: '/password/i', level: 'warn' },
|
79
|
+
{ type: 'regex', rule: '/ssn/i', level: 'warn' }
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'returns cost alert when cost exceeds limit' do
|
84
|
+
write_rules(max_cost_usd: 0.05)
|
85
|
+
|
86
|
+
policy = described_class.instance
|
87
|
+
alerts = policy.check_alerts(prompt: 'hello', cost_estimate: 0.1)
|
88
|
+
expect(alerts).to eq [{ type: 'cost', limit: 0.05, level: 'block' }]
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'returns both regex and cost alerts when both conditions are met' do
|
92
|
+
write_rules(max_cost_usd: 0.05, warn_if_regex: ['/password/i'])
|
93
|
+
|
94
|
+
policy = described_class.instance
|
95
|
+
alerts = policy.check_alerts(prompt: 'My password is 123', cost_estimate: 0.1)
|
96
|
+
expect(alerts).to contain_exactly(
|
97
|
+
{ type: 'regex', rule: '/password/i', level: 'warn' },
|
98
|
+
{ type: 'cost', limit: 0.05, level: 'block' }
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'handles case insensitive regex flags' do
|
103
|
+
write_rules(warn_if_regex: ['/ETA/i'])
|
104
|
+
|
105
|
+
policy = described_class.instance
|
106
|
+
alerts = policy.check_alerts(prompt: 'The eta is 5 minutes', cost_estimate: 0.1)
|
107
|
+
expect(alerts).to eq [{ type: 'regex', rule: '/ETA/i', level: 'warn' }]
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'handles case sensitive regex when no flags' do
|
111
|
+
write_rules(warn_if_regex: ['/ETA'])
|
112
|
+
|
113
|
+
policy = described_class.instance
|
114
|
+
alerts = policy.check_alerts(prompt: 'The eta is 5 minutes', cost_estimate: 0.1)
|
115
|
+
expect(alerts).to eq []
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'handles regex with multiple flags' do
|
119
|
+
write_rules(warn_if_regex: ['/eta/im'])
|
120
|
+
|
121
|
+
policy = described_class.instance
|
122
|
+
alerts = policy.check_alerts(prompt: "ETA\nis 5 minutes", cost_estimate: 0.1)
|
123
|
+
expect(alerts).to eq [{ type: 'regex', rule: '/eta/im', level: 'warn' }]
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'handles plain string patterns' do
|
127
|
+
write_rules(warn_if_regex: ['password'])
|
128
|
+
|
129
|
+
policy = described_class.instance
|
130
|
+
alerts = policy.check_alerts(prompt: 'My password is 123', cost_estimate: 0.1)
|
131
|
+
expect(alerts).to eq [{ type: 'regex', rule: 'password', level: 'warn' }]
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'handles empty warn_if_regex array' do
|
135
|
+
write_rules(warn_if_regex: [])
|
136
|
+
|
137
|
+
policy = described_class.instance
|
138
|
+
alerts = policy.check_alerts(prompt: 'any text', cost_estimate: 0.1)
|
139
|
+
expect(alerts).to eq []
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# -----------------------------------------------------------
|
144
|
+
describe 'integration with check!' do
|
145
|
+
it 'allows check! to pass even when alerts are present' do
|
146
|
+
write_rules(
|
147
|
+
max_cost_usd: 1,
|
148
|
+
reject_if_regex: [],
|
149
|
+
warn_if_regex: ['/password/i']
|
150
|
+
)
|
151
|
+
|
152
|
+
policy = described_class.instance
|
153
|
+
result = policy.check!(prompt: 'My password is 123', cost_estimate: 0.5)
|
154
|
+
expect(result).to eq :ok
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'still blocks on reject regex even when alerts are present' do
|
158
|
+
write_rules(
|
159
|
+
max_cost_usd: 1,
|
160
|
+
reject_if_regex: ['/ssn/i'],
|
161
|
+
warn_if_regex: ['/password/i']
|
162
|
+
)
|
163
|
+
|
164
|
+
policy = described_class.instance
|
165
|
+
expect do
|
166
|
+
policy.check!(prompt: 'My password is 123 and SSN is 456', cost_estimate: 0.5)
|
167
|
+
end.to raise_error(PromptWarden::PolicyError, /reject regex/)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
data/spec/prompt_warden_spec.rb
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
RSpec.describe PromptWarden do
|
4
4
|
subject(:mod) { described_class }
|
5
5
|
|
6
|
-
it
|
6
|
+
it 'has a semantic version number' do
|
7
7
|
expect(PromptWarden::VERSION).to match(/\A\d+\.\d+\.\d+\z/)
|
8
8
|
end
|
9
9
|
|
10
|
-
it
|
10
|
+
it 'is defined as a module' do
|
11
11
|
expect(mod).to be_a(Module)
|
12
12
|
end
|
13
13
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative '../lib/prompt_warden'
|
4
|
+
require 'webmock/rspec'
|
5
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
4
6
|
|
5
7
|
RSpec.configure do |config|
|
6
|
-
|
7
|
-
config.example_status_persistence_file_path = ".rspec_status"
|
8
|
-
|
9
|
-
# Disable RSpec exposing methods globally on `Module` and `main`
|
8
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
10
9
|
config.disable_monkey_patching!
|
10
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
end
|
12
|
+
# fresh configuration between examples
|
13
|
+
config.before { PromptWarden.reset! if defined?(PromptWarden) }
|
15
14
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'webmock/rspec'
|
4
|
+
|
5
|
+
RSpec.describe PromptWarden::Uploader do
|
6
|
+
let(:api_url) { 'https://example.com/ingest' }
|
7
|
+
let(:config) do
|
8
|
+
PromptWarden::Configuration.new.tap do |c|
|
9
|
+
c.project_token = 'tok_123'
|
10
|
+
c.api_url = api_url
|
11
|
+
c.max_retries = 2
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
before do
|
16
|
+
PromptWarden.reset!
|
17
|
+
PromptWarden.configure do |c|
|
18
|
+
c.project_token = config.project_token
|
19
|
+
c.api_url = config.api_url
|
20
|
+
c.max_retries = config.max_retries
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
after do
|
25
|
+
# Clean up any failed files in tmpdir
|
26
|
+
Dir.glob(File.join(Dir.tmpdir, 'pw_failed_*.json.gz')).each { |f| File.delete(f) }
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'POSTs gzip payload with auth header' do
|
30
|
+
stub = stub_request(:post, api_url)
|
31
|
+
.with(headers: { 'Authorization' => 'Bearer tok_123',
|
32
|
+
'Content-Encoding' => 'gzip' })
|
33
|
+
.to_return(status: 200)
|
34
|
+
|
35
|
+
uploader = described_class.instance
|
36
|
+
uploader.enqueue('compressed bytes')
|
37
|
+
|
38
|
+
# give worker thread a tick
|
39
|
+
sleep 0.05
|
40
|
+
|
41
|
+
expect(stub).to have_been_requested
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'retries once on 500 then succeeds' do
|
45
|
+
stub_request(:post, api_url)
|
46
|
+
.to_return({ status: 500 }, { status: 200 })
|
47
|
+
|
48
|
+
uploader = described_class.instance
|
49
|
+
uploader.enqueue('data')
|
50
|
+
|
51
|
+
# wait long enough for retry thread (interval=0.05, backoff 2x)
|
52
|
+
sleep 0.2
|
53
|
+
|
54
|
+
expect(a_request(:post, api_url)).to have_been_made.times(2)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'writes failed batch to disk on network error' do
|
58
|
+
# Simulate network error
|
59
|
+
stub_request(:post, api_url).to_raise(Faraday::ConnectionFailed.new('fail'))
|
60
|
+
uploader = described_class.instance
|
61
|
+
uploader.enqueue('diskfail')
|
62
|
+
sleep 0.05
|
63
|
+
failed_files = Dir.glob(File.join(Dir.tmpdir, 'pw_failed_*.json.gz'))
|
64
|
+
expect(failed_files.size).to be >= 1
|
65
|
+
expect(File.binread(failed_files.first)).to eq 'diskfail'
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'pw:retry_failed task uploads and deletes failed files' do
|
69
|
+
# Write a failed file manually
|
70
|
+
path = File.join(Dir.tmpdir, 'pw_failed_test.json.gz')
|
71
|
+
File.binwrite(path, 'retryme')
|
72
|
+
stub = stub_request(:post, api_url).to_return(status: 200)
|
73
|
+
# Run the rake task logic directly
|
74
|
+
PromptWarden::Uploader.retry_failed!
|
75
|
+
sleep 0.05
|
76
|
+
expect(stub).to have_been_requested
|
77
|
+
expect(File.exist?(path)).to be false
|
78
|
+
end
|
79
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prompt_warden
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tyler Hammett
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday-retry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: yaml
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: activesupport
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +66,34 @@ dependencies:
|
|
38
66
|
- - ">="
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '6.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.3.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.3.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '13.2'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '13.2'
|
41
97
|
- !ruby/object:Gem::Dependency
|
42
98
|
name: rspec
|
43
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,39 +137,41 @@ dependencies:
|
|
81
137
|
- !ruby/object:Gem::Version
|
82
138
|
version: '2.26'
|
83
139
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
140
|
+
name: timecop
|
85
141
|
requirement: !ruby/object:Gem::Requirement
|
86
142
|
requirements:
|
87
|
-
- - "
|
143
|
+
- - "~>"
|
88
144
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
145
|
+
version: '0.9'
|
90
146
|
type: :development
|
91
147
|
prerelease: false
|
92
148
|
version_requirements: !ruby/object:Gem::Requirement
|
93
149
|
requirements:
|
94
|
-
- - "
|
150
|
+
- - "~>"
|
95
151
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
152
|
+
version: '0.9'
|
97
153
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
154
|
+
name: webmock
|
99
155
|
requirement: !ruby/object:Gem::Requirement
|
100
156
|
requirements:
|
101
157
|
- - "~>"
|
102
158
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
159
|
+
version: '3.20'
|
104
160
|
type: :development
|
105
161
|
prerelease: false
|
106
162
|
version_requirements: !ruby/object:Gem::Requirement
|
107
163
|
requirements:
|
108
164
|
- - "~>"
|
109
165
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
166
|
+
version: '3.20'
|
111
167
|
description: |
|
112
|
-
PromptWarden
|
113
|
-
|
168
|
+
PromptWarden provides automatic instrumentation for OpenAI, Anthropic, and Langchain SDKs.
|
169
|
+
Features include policy enforcement, cost calculation, real-time monitoring, and alert recording.
|
170
|
+
Includes CLI tool for live event streaming and comprehensive test coverage.
|
114
171
|
email:
|
115
172
|
- hello@promptwarden.io
|
116
|
-
executables:
|
173
|
+
executables:
|
174
|
+
- pw_tail
|
117
175
|
extensions: []
|
118
176
|
extra_rdoc_files: []
|
119
177
|
files:
|
@@ -128,13 +186,38 @@ files:
|
|
128
186
|
- README.md
|
129
187
|
- Rakefile
|
130
188
|
- bin/console
|
189
|
+
- bin/pw_tail
|
131
190
|
- bin/setup
|
191
|
+
- examples/policy.yml
|
132
192
|
- lib/prompt_warden.rb
|
193
|
+
- lib/prompt_warden/adapter.rb
|
194
|
+
- lib/prompt_warden/buffer.rb
|
195
|
+
- lib/prompt_warden/cli.rb
|
196
|
+
- lib/prompt_warden/configuration.rb
|
197
|
+
- lib/prompt_warden/cost_calculator.rb
|
198
|
+
- lib/prompt_warden/event.rb
|
199
|
+
- lib/prompt_warden/instrumentation/anthropic.rb
|
200
|
+
- lib/prompt_warden/instrumentation/langchain.rb
|
201
|
+
- lib/prompt_warden/instrumentation/openai.rb
|
202
|
+
- lib/prompt_warden/policy.rb
|
203
|
+
- lib/prompt_warden/railtie.rb
|
204
|
+
- lib/prompt_warden/uploader.rb
|
133
205
|
- lib/prompt_warden/version.rb
|
134
206
|
- prompt_warden.gemspec
|
135
207
|
- sig/prompt_warden.rbs
|
208
|
+
- spec/adapter_auto_detect_spec.rb
|
209
|
+
- spec/anthropic_adapter_spec.rb
|
210
|
+
- spec/buffer_spec.rb
|
211
|
+
- spec/cli_spec.rb
|
212
|
+
- spec/configuration_spec.rb
|
213
|
+
- spec/cost_calculator_spec.rb
|
214
|
+
- spec/event_spec.rb
|
215
|
+
- spec/langchain_adapter_spec.rb
|
216
|
+
- spec/openai_adapter_spec.rb
|
217
|
+
- spec/policy_spec.rb
|
136
218
|
- spec/prompt_warden_spec.rb
|
137
219
|
- spec/spec_helper.rb
|
220
|
+
- spec/uploader_spec.rb
|
138
221
|
homepage: https://promptwarden.io
|
139
222
|
licenses:
|
140
223
|
- MIT
|
@@ -157,5 +240,5 @@ requirements: []
|
|
157
240
|
rubygems_version: 3.4.19
|
158
241
|
signing_key:
|
159
242
|
specification_version: 4
|
160
|
-
summary:
|
243
|
+
summary: Record, audit, and guard AI prompt usage with automatic SDK instrumentation.
|
161
244
|
test_files: []
|