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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 664b869ad3c4946de2726eb00ba6530e4eabf8e5f77232275bc3af91bd265199
|
4
|
+
data.tar.gz: b56c3c7e33b6478ea8b9021c348c185e2dd410c7860c89309216a17959c15ecc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3a8fce56fd182a7cbdf64c88e9895166eb86fbad211d8616d1a034b4b2b81bbafe835cb228553d60773508866948331d42bdc48005304ae05e8697df51c1ac41
|
7
|
+
data.tar.gz: 3ded34fe6807cb4e1d88e295e8551ff664ece1c3736ecdee8c0d299b38d101f074d381ab54becf86ef1f22dc2278a86c2c7a0c292144c0eadade74602c7747db
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,36 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [0.1.
|
3
|
+
## [0.1.1] - 2025-07-23
|
4
4
|
|
5
|
-
|
5
|
+
### Added
|
6
|
+
- CLI executable (`pw_tail`) for real-time event monitoring
|
7
|
+
- Enhanced cost calculation with accurate token counting and model-specific pricing
|
8
|
+
- Comprehensive test suite with 106 passing specs
|
9
|
+
- Support for major AI models with current pricing (GPT-4o, Claude-3, etc.)
|
10
|
+
- Rake task for retrying failed uploads (`rake pw:retry_failed`)
|
11
|
+
|
12
|
+
### Features
|
13
|
+
- **Cost Calculation**: Accurate token counting with tiktoken integration and fallback
|
14
|
+
- **CLI Monitoring**: Real-time event streaming with JSON and human-readable formats
|
15
|
+
- **Reliability**: Disk-retry mechanism for failed uploads
|
16
|
+
- **Extensibility**: Adapter system for custom SDK integration
|
17
|
+
|
18
|
+
### Supported Models
|
19
|
+
- OpenAI: gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo
|
20
|
+
- Anthropic: claude-3-opus, claude-3-sonnet, claude-3-haiku
|
21
|
+
- Default fallback pricing for unknown models
|
22
|
+
|
23
|
+
## [0.1.0] - 2025-07-23
|
24
|
+
|
25
|
+
### Added
|
26
|
+
- Automatic SDK instrumentation for OpenAI, Anthropic, and Langchain
|
27
|
+
- YAML-based policy configuration with cost limits and regex patterns
|
28
|
+
- Policy alert system with non-blocking warnings and blocking rejections
|
29
|
+
- Automatic alert recording in events with structured alert data
|
30
|
+
- Asynchronous event uploads with gzip compression and disk-retry fallback
|
31
|
+
|
32
|
+
### Features
|
33
|
+
- **Policy Enforcement**: Cost limits, regex pattern matching, alert generation
|
34
|
+
- **Event Capture**: Complete AI interaction data with metadata and alerts
|
35
|
+
- **Reliability**: Disk-retry mechanism for failed uploads
|
36
|
+
- **Extensibility**: Adapter system for custom SDK integration
|
data/Gemfile
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
source
|
3
|
+
source 'https://rubygems.org'
|
4
4
|
|
5
5
|
# Specify your gem's dependencies in prompt_warden.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
-
gem
|
9
|
-
gem
|
8
|
+
gem 'irb'
|
9
|
+
gem 'rake', '~> 13.0'
|
10
10
|
|
11
|
-
gem
|
11
|
+
gem 'rspec', '~> 3.0'
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
prompt_warden (0.1.
|
4
|
+
prompt_warden (0.1.1)
|
5
5
|
activesupport (>= 6.1)
|
6
6
|
faraday (~> 2.9)
|
7
|
+
faraday-retry (~> 2.2)
|
8
|
+
yaml
|
7
9
|
|
8
10
|
GEM
|
9
11
|
remote: https://rubygems.org/
|
@@ -21,12 +23,17 @@ GEM
|
|
21
23
|
securerandom (>= 0.3)
|
22
24
|
tzinfo (~> 2.0, >= 2.0.5)
|
23
25
|
uri (>= 0.13.1)
|
26
|
+
addressable (2.8.7)
|
27
|
+
public_suffix (>= 2.0.2, < 7.0)
|
24
28
|
ast (2.4.3)
|
25
29
|
base64 (0.3.0)
|
26
30
|
benchmark (0.4.1)
|
27
31
|
bigdecimal (3.2.2)
|
28
32
|
concurrent-ruby (1.3.5)
|
29
33
|
connection_pool (2.5.3)
|
34
|
+
crack (1.0.0)
|
35
|
+
bigdecimal
|
36
|
+
rexml
|
30
37
|
date (3.4.1)
|
31
38
|
diff-lcs (1.6.2)
|
32
39
|
drb (2.2.3)
|
@@ -37,6 +44,9 @@ GEM
|
|
37
44
|
logger
|
38
45
|
faraday-net_http (3.4.1)
|
39
46
|
net-http (>= 0.5.0)
|
47
|
+
faraday-retry (2.3.2)
|
48
|
+
faraday (~> 2.0)
|
49
|
+
hashdiff (1.2.0)
|
40
50
|
i18n (1.14.7)
|
41
51
|
concurrent-ruby (~> 1.0)
|
42
52
|
io-console (0.8.1)
|
@@ -62,6 +72,7 @@ GEM
|
|
62
72
|
psych (5.2.6)
|
63
73
|
date
|
64
74
|
stringio
|
75
|
+
public_suffix (6.0.2)
|
65
76
|
racc (1.8.1)
|
66
77
|
rainbow (3.1.1)
|
67
78
|
rake (13.3.0)
|
@@ -71,6 +82,7 @@ GEM
|
|
71
82
|
regexp_parser (2.10.0)
|
72
83
|
reline (0.6.2)
|
73
84
|
io-console (~> 0.5)
|
85
|
+
rexml (3.4.1)
|
74
86
|
rspec (3.13.1)
|
75
87
|
rspec-core (~> 3.13.0)
|
76
88
|
rspec-expectations (~> 3.13.0)
|
@@ -114,12 +126,18 @@ GEM
|
|
114
126
|
ruby-progressbar (1.13.0)
|
115
127
|
securerandom (0.4.1)
|
116
128
|
stringio (3.1.7)
|
129
|
+
timecop (0.9.10)
|
117
130
|
tzinfo (2.0.6)
|
118
131
|
concurrent-ruby (~> 1.0)
|
119
132
|
unicode-display_width (3.1.4)
|
120
133
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
121
134
|
unicode-emoji (4.0.4)
|
122
135
|
uri (1.0.3)
|
136
|
+
webmock (3.25.1)
|
137
|
+
addressable (>= 2.8.0)
|
138
|
+
crack (>= 0.3.2)
|
139
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
140
|
+
yaml (0.2.1)
|
123
141
|
|
124
142
|
PLATFORMS
|
125
143
|
arm64-darwin-23
|
@@ -133,6 +151,8 @@ DEPENDENCIES
|
|
133
151
|
rspec (~> 3.12, ~> 3.0)
|
134
152
|
rubocop (~> 1.60)
|
135
153
|
rubocop-rspec (~> 2.26)
|
154
|
+
timecop (~> 0.9)
|
155
|
+
webmock (~> 3.20)
|
136
156
|
|
137
157
|
BUNDLED WITH
|
138
158
|
2.7.0
|
data/README.md
CHANGED
@@ -1,43 +1,241 @@
|
|
1
1
|
# PromptWarden
|
2
2
|
|
3
|
-
|
3
|
+
**Record, audit, and guard AI prompt usage** with automatic SDK instrumentation, policy enforcement, and real-time monitoring.
|
4
4
|
|
5
|
-
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Automatic SDK Capture**: Zero-code integration with OpenAI, Anthropic, and Langchain
|
8
|
+
- **Policy Guardrails**: YAML-based rules for cost limits, regex patterns, and alerts
|
9
|
+
- **Enhanced Cost Calculation**: Accurate token counting and model-specific pricing
|
10
|
+
- **Real-time Monitoring**: CLI tool for live event streaming and filtering
|
11
|
+
- **Alert System**: Non-blocking warnings and blocking rejections based on patterns
|
12
|
+
- **Automatic Alert Recording**: Alerts included in events and uploaded to SaaS
|
13
|
+
- **Asynchronous Uploads**: Batched, gzipped events with disk-retry fallback
|
6
14
|
|
7
15
|
## Installation
|
8
16
|
|
9
|
-
|
17
|
+
```bash
|
18
|
+
gem install prompt_warden
|
19
|
+
```
|
20
|
+
|
21
|
+
Or add to your Gemfile:
|
10
22
|
|
11
|
-
|
23
|
+
```ruby
|
24
|
+
gem 'prompt_warden'
|
25
|
+
```
|
12
26
|
|
13
|
-
|
14
|
-
|
27
|
+
## Quick Start
|
28
|
+
|
29
|
+
1. **Configure** (in your app's initializer):
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
PromptWarden.configure do |config|
|
33
|
+
config.project_token = 'your-project-token'
|
34
|
+
config.api_url = 'https://your-saas.com/api/v1/ingest'
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
2. **Create Policy** (`config/promptwarden.yml`):
|
39
|
+
|
40
|
+
```yaml
|
41
|
+
max_cost_usd: 0.50 # Block if projected call cost > $0.50
|
42
|
+
reject_if_regex:
|
43
|
+
- /password/i
|
44
|
+
- /(ssn|social\s*security)/i
|
45
|
+
warn_if_regex:
|
46
|
+
- /\bETA\b/i
|
15
47
|
```
|
16
48
|
|
17
|
-
|
49
|
+
3. **Use AI SDKs** (automatically instrumented):
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# OpenAI
|
53
|
+
client = OpenAI::Client.new
|
54
|
+
response = client.chat(parameters: {
|
55
|
+
model: "gpt-4o",
|
56
|
+
messages: [{ role: "user", content: "What is the ETA?" }]
|
57
|
+
})
|
58
|
+
|
59
|
+
# Anthropic
|
60
|
+
client = Anthropic::Client.new
|
61
|
+
response = client.messages(
|
62
|
+
model: "claude-3-opus-20240229",
|
63
|
+
max_tokens: 1000,
|
64
|
+
messages: [{ role: "user", content: "What is the ETA?" }]
|
65
|
+
)
|
66
|
+
```
|
67
|
+
|
68
|
+
## CLI Tool
|
69
|
+
|
70
|
+
Monitor events in real-time with the `pw_tail` command:
|
18
71
|
|
19
72
|
```bash
|
20
|
-
|
73
|
+
# Follow all events
|
74
|
+
./bin/pw_tail
|
75
|
+
|
76
|
+
# Show only events with alerts
|
77
|
+
./bin/pw_tail --alerts
|
78
|
+
|
79
|
+
# Filter by model
|
80
|
+
./bin/pw_tail --model gpt-4o
|
81
|
+
|
82
|
+
# Show events above cost threshold
|
83
|
+
./bin/pw_tail --cost 0.01
|
84
|
+
|
85
|
+
# Filter by status
|
86
|
+
./bin/pw_tail --status failed
|
87
|
+
|
88
|
+
# Limit number of events
|
89
|
+
./bin/pw_tail --limit 10
|
90
|
+
|
91
|
+
# Output in JSON format
|
92
|
+
./bin/pw_tail --json
|
93
|
+
|
94
|
+
# Show recent events without following
|
95
|
+
./bin/pw_tail --no-follow
|
21
96
|
```
|
22
97
|
|
23
|
-
|
98
|
+
### CLI Output Format
|
24
99
|
|
25
|
-
|
100
|
+
```
|
101
|
+
10:30:00 gpt-4o $0.005 ok [⚠️ /ETA/i] | What is the ETA for this project?
|
102
|
+
10:31:15 claude-3 $0.75 ok [💰 >$0.5] | How much does this cost?
|
103
|
+
10:32:30 gpt-4o $0.001 ok | Simple question without alerts
|
104
|
+
```
|
26
105
|
|
27
|
-
##
|
106
|
+
## Policy Features
|
28
107
|
|
29
|
-
|
108
|
+
### Cost Limits
|
109
|
+
```yaml
|
110
|
+
max_cost_usd: 0.50 # Block requests exceeding $0.50
|
111
|
+
```
|
30
112
|
|
31
|
-
|
113
|
+
### Regex Patterns
|
114
|
+
```yaml
|
115
|
+
reject_if_regex: # Block requests matching patterns
|
116
|
+
- /password/i
|
117
|
+
- /(ssn|social\s*security)/i
|
32
118
|
|
33
|
-
|
119
|
+
warn_if_regex: # Log warnings for patterns
|
120
|
+
- /\bETA\b/i
|
121
|
+
- /urgent/i
|
122
|
+
```
|
34
123
|
|
35
|
-
|
124
|
+
### Programmatic Checks
|
125
|
+
```ruby
|
126
|
+
# Check for alerts (non-blocking)
|
127
|
+
alerts = PromptWarden::Policy.instance.check_alerts(
|
128
|
+
prompt: "What is the ETA?",
|
129
|
+
cost_estimate: 0.005
|
130
|
+
)
|
131
|
+
|
132
|
+
# Check for blocks (raises PolicyError)
|
133
|
+
PromptWarden::Policy.instance.check!(
|
134
|
+
prompt: "What is the password?",
|
135
|
+
cost_estimate: 0.001
|
136
|
+
)
|
137
|
+
```
|
36
138
|
|
37
|
-
##
|
139
|
+
## Enhanced Cost Calculation
|
38
140
|
|
39
|
-
|
141
|
+
PromptWarden provides accurate cost calculation with:
|
40
142
|
|
41
|
-
|
143
|
+
- **Model-specific pricing** for OpenAI and Anthropic models
|
144
|
+
- **Token counting** with tiktoken integration for OpenAI models
|
145
|
+
- **Response token integration** for accurate post-request costs
|
146
|
+
- **Fallback estimation** for unknown models
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# Calculate cost for a prompt
|
150
|
+
cost = PromptWarden.calculate_cost(
|
151
|
+
prompt: "Explain quantum computing",
|
152
|
+
model: "gpt-4o"
|
153
|
+
)
|
154
|
+
|
155
|
+
# Calculate cost with actual response tokens
|
156
|
+
actual_cost = PromptWarden.calculate_cost(
|
157
|
+
prompt: "Explain quantum computing",
|
158
|
+
model: "gpt-4o",
|
159
|
+
response_tokens: 150
|
160
|
+
)
|
161
|
+
```
|
162
|
+
|
163
|
+
### Supported Models
|
164
|
+
|
165
|
+
**OpenAI Models:**
|
166
|
+
- `gpt-4o` ($0.0025/1K input, $0.01/1K output)
|
167
|
+
- `gpt-4o-mini` ($0.00015/1K input, $0.0006/1K output)
|
168
|
+
- `gpt-4-turbo` ($0.01/1K input, $0.03/1K output)
|
169
|
+
- `gpt-3.5-turbo` ($0.0005/1K input, $0.0015/1K output)
|
170
|
+
|
171
|
+
**Anthropic Models:**
|
172
|
+
- `claude-3-opus-20240229` ($0.015/1K input, $0.075/1K output)
|
173
|
+
- `claude-3-sonnet-20240229` ($0.003/1K input, $0.015/1K output)
|
174
|
+
- `claude-3-haiku-20240307` ($0.00025/1K input, $0.00125/1K output)
|
175
|
+
|
176
|
+
## Supported SDKs
|
177
|
+
|
178
|
+
- **OpenAI**: `openai` gem
|
179
|
+
- **Anthropic**: `anthropic` gem
|
180
|
+
- **Langchain**: `langchain` gem
|
181
|
+
|
182
|
+
## Gem vs SaaS
|
183
|
+
|
184
|
+
**PromptWarden Gem** (this repository):
|
185
|
+
- Local policy enforcement
|
186
|
+
- Event capture and buffering
|
187
|
+
- Enhanced cost calculation
|
188
|
+
- Asynchronous uploads to SaaS
|
189
|
+
- CLI monitoring tool
|
190
|
+
- Disk-retry for failed uploads
|
191
|
+
|
192
|
+
**PromptWarden SaaS** (separate application):
|
193
|
+
- Data storage and retention
|
194
|
+
- Analytics dashboards
|
195
|
+
- Advanced alerting (Slack, email)
|
196
|
+
- User and project management
|
197
|
+
- Cost tracking and reporting
|
198
|
+
|
199
|
+
## Event Structure
|
200
|
+
|
201
|
+
Events are automatically captured and include:
|
202
|
+
|
203
|
+
```json
|
204
|
+
{
|
205
|
+
"id": "uuid",
|
206
|
+
"prompt": "What is the ETA?",
|
207
|
+
"response": "The ETA is 2 weeks",
|
208
|
+
"model": "gpt-4o",
|
209
|
+
"latency_ms": 1250,
|
210
|
+
"cost_usd": 0.005,
|
211
|
+
"status": "ok",
|
212
|
+
"timestamp": "2024-01-15T10:30:00Z",
|
213
|
+
"alerts": [
|
214
|
+
{
|
215
|
+
"type": "regex",
|
216
|
+
"rule": "/ETA/i",
|
217
|
+
"level": "warn"
|
218
|
+
}
|
219
|
+
]
|
220
|
+
}
|
221
|
+
```
|
222
|
+
|
223
|
+
## Development
|
224
|
+
|
225
|
+
```bash
|
226
|
+
# Install dependencies
|
227
|
+
bundle install
|
228
|
+
|
229
|
+
# Run tests
|
230
|
+
bundle exec rspec
|
231
|
+
|
232
|
+
# Run CLI tests
|
233
|
+
bundle exec rspec spec/cli_spec.rb
|
234
|
+
|
235
|
+
# Test cost calculation
|
236
|
+
ruby test_cost_calculation.rb
|
237
|
+
```
|
238
|
+
|
239
|
+
## License
|
42
240
|
|
43
|
-
|
241
|
+
MIT License - see LICENSE file for details.
|
data/Rakefile
CHANGED
@@ -1,8 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
5
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
7
7
|
|
8
8
|
task default: :spec
|
9
|
+
|
10
|
+
desc 'Retry failed PromptWarden uploads from disk'
|
11
|
+
namespace :pw do
|
12
|
+
task :retry_failed do
|
13
|
+
require_relative './lib/prompt_warden'
|
14
|
+
|
15
|
+
# Configure with default values if not already configured
|
16
|
+
unless PromptWarden.configuration.project_token
|
17
|
+
PromptWarden.configure do |c|
|
18
|
+
c.project_token = ENV['PROMPT_WARDEN_TOKEN'] || 'default_token'
|
19
|
+
c.api_url = ENV['PROMPT_WARDEN_API'] || 'https://httpbin.org/post'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
PromptWarden::Uploader.retry_failed!
|
24
|
+
end
|
25
|
+
end
|
data/bin/console
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'prompt_warden'
|
6
6
|
|
7
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
8
8
|
# with your gem easier. You can also use a different console, if you like.
|
9
9
|
|
10
|
-
require
|
10
|
+
require 'irb'
|
11
11
|
IRB.start(__FILE__)
|
data/bin/pw_tail
ADDED
data/examples/policy.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# PromptWarden Policy Configuration
|
2
|
+
# This file configures guard-rails for AI prompt usage
|
3
|
+
|
4
|
+
# Cost limits - block if projected call cost exceeds this amount
|
5
|
+
max_cost_usd: 0.50
|
6
|
+
|
7
|
+
# Reject patterns - block execution if prompt matches any of these regexes
|
8
|
+
reject_if_regex:
|
9
|
+
- /password/i
|
10
|
+
- /(ssn|social\s*security)/i
|
11
|
+
- /credit\s*card/i
|
12
|
+
- /api\s*key/i
|
13
|
+
|
14
|
+
# Alert patterns - log alerts if prompt matches any of these regexes
|
15
|
+
# (does not block execution, but creates alerts in events)
|
16
|
+
warn_if_regex:
|
17
|
+
- /\bETA\b/i
|
18
|
+
- /deadline/i
|
19
|
+
- /urgent/i
|
20
|
+
- /asap/i
|
21
|
+
- /confidential/i
|
22
|
+
- /internal\s*use\s*only/i
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PromptWarden
|
4
|
+
module Adapter
|
5
|
+
ENTRY = Struct.new(:gem_name, :const_path, :block, :loaded)
|
6
|
+
REGISTRY = []
|
7
|
+
|
8
|
+
# public API ----------------------------------------------------------
|
9
|
+
def self.map(gem_name, const_path, &block)
|
10
|
+
REGISTRY << ENTRY.new(gem_name, const_path, block, false)
|
11
|
+
try_load(gem_name) # run now if gem already active
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.auto_load_all!
|
15
|
+
Gem.loaded_specs.keys.each { |name| try_load(name) }
|
16
|
+
PromptWarden.configuration.run_pending_adapters
|
17
|
+
end
|
18
|
+
|
19
|
+
# internal ------------------------------------------------------------
|
20
|
+
def self.try_load(gem_name)
|
21
|
+
entry = REGISTRY.find { |e| e.gem_name == gem_name }
|
22
|
+
return unless entry && !entry.loaded
|
23
|
+
|
24
|
+
return unless Gem.loaded_specs.key?(gem_name) || const_path_defined?(entry.const_path)
|
25
|
+
|
26
|
+
entry.block.call
|
27
|
+
entry.loaded = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.const_path_defined?(path)
|
31
|
+
names = path.split('::')
|
32
|
+
mod = Object
|
33
|
+
names.each do |name|
|
34
|
+
return false unless mod.const_defined?(name, false)
|
35
|
+
|
36
|
+
mod = mod.const_get(name)
|
37
|
+
end
|
38
|
+
true
|
39
|
+
end
|
40
|
+
private_class_method :const_path_defined?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# built‑in mappings -------------------------------------------------------
|
45
|
+
PromptWarden::Adapter.map('openai', 'OpenAI::Client') do
|
46
|
+
require_relative 'instrumentation/openai'
|
47
|
+
rescue Exception
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
PromptWarden::Adapter.map('anthropic', 'Anthropic::Client') do
|
51
|
+
require_relative 'instrumentation/anthropic'
|
52
|
+
rescue Exception
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
PromptWarden::Adapter.map('langchain', 'Langchain::LLM::Base') do
|
56
|
+
require_relative 'instrumentation/langchain'
|
57
|
+
rescue Exception
|
58
|
+
raise
|
59
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'zlib'
|
5
|
+
|
6
|
+
module PromptWarden
|
7
|
+
class Buffer
|
8
|
+
def initialize(cfg = PromptWarden.configuration)
|
9
|
+
@cfg = cfg
|
10
|
+
@events = []
|
11
|
+
@bytes = 0
|
12
|
+
@mutex = Mutex.new
|
13
|
+
start_timer
|
14
|
+
end
|
15
|
+
|
16
|
+
# -- Public ----------------------------------------------------------
|
17
|
+
|
18
|
+
# Enqueue an Event (or Hash). Flushes automatically when batch_bytes hit.
|
19
|
+
def push(event)
|
20
|
+
json = JSON.generate(event.to_h)
|
21
|
+
should_flush = false
|
22
|
+
|
23
|
+
@mutex.synchronize do
|
24
|
+
@events << json
|
25
|
+
@bytes += json.bytesize
|
26
|
+
should_flush = @bytes >= @cfg.batch_bytes
|
27
|
+
end
|
28
|
+
|
29
|
+
flush! if should_flush
|
30
|
+
end
|
31
|
+
|
32
|
+
# Manual flush
|
33
|
+
def flush!
|
34
|
+
batch = nil
|
35
|
+
|
36
|
+
@mutex.synchronize do
|
37
|
+
return if @events.empty?
|
38
|
+
|
39
|
+
batch = @events.join("\n")
|
40
|
+
@events.clear
|
41
|
+
@bytes = 0
|
42
|
+
end
|
43
|
+
|
44
|
+
compressed = Zlib.gzip(batch)
|
45
|
+
Uploader.instance.enqueue(compressed)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Flushes buffer every flush_interval seconds
|
51
|
+
def start_timer
|
52
|
+
Thread.new do
|
53
|
+
loop do
|
54
|
+
sleep @cfg.flush_interval
|
55
|
+
flush!
|
56
|
+
end
|
57
|
+
end.tap { |t| t.name = 'PromptWarden::BufferFlusher' }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|