boxcars 0.8.3 → 0.8.4
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/Gemfile.lock +9 -9
- data/README.md +2 -1
- data/USER_CONTEXT_GUIDE.md +435 -0
- data/lib/boxcars/engine/anthropic.rb +4 -2
- data/lib/boxcars/engine/cohere.rb +4 -2
- data/lib/boxcars/engine/gemini_ai.rb +7 -5
- data/lib/boxcars/engine/gpt4all_eng.rb +5 -5
- data/lib/boxcars/engine/groq.rb +10 -8
- data/lib/boxcars/engine/intelligence_base.rb +3 -13
- data/lib/boxcars/engine/ollama.rb +4 -2
- data/lib/boxcars/engine/openai.rb +3 -1
- data/lib/boxcars/engine/perplexityai.rb +3 -1
- data/lib/boxcars/engine/unified_observability.rb +2 -1
- data/lib/boxcars/engine.rb +17 -10
- data/lib/boxcars/observability.rb +44 -2
- data/lib/boxcars/observability_backends/posthog_backend.rb +10 -15
- data/lib/boxcars/observation.rb +40 -0
- data/lib/boxcars/version.rb +1 -1
- data/lib/boxcars.rb +1 -0
- metadata +2 -2
- data/POSTHOG_TEST_README.md +0 -118
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40c67f759e97f22570fba131b967c73b871b1f96cfbc97df78b1c0438bdf4291
|
4
|
+
data.tar.gz: 55f5e64fa79b8035e1bceb95912cbc5a8c587c29fa3b09d89a65ab83fe4375a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba4b4434d263c9b4a875f5ab2db607d11a91795592f39c608143c058a38b773e6349ece90946f7a2137a6edd56d375ebd5992195921660ceac2fa5b4262e6ef5
|
7
|
+
data.tar.gz: c8d58cdfeb28d9a2d6bcba655478d7dd3572db4c2fbd72a566cca6a71c37941d0b7592c02c75f3430e8e7821dcf959942de2d960335864d70da1976e9f7ae63a
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
boxcars (0.8.
|
4
|
+
boxcars (0.8.4)
|
5
5
|
faraday-retry (~> 2.0)
|
6
6
|
google_search_results (~> 2.2)
|
7
7
|
gpt4all (~> 0.0.5)
|
@@ -243,14 +243,14 @@ GEM
|
|
243
243
|
addressable (>= 2.3.5)
|
244
244
|
faraday (>= 0.17.3, < 3)
|
245
245
|
securerandom (0.4.1)
|
246
|
-
sqlite3 (2.
|
247
|
-
sqlite3 (2.
|
248
|
-
sqlite3 (2.
|
249
|
-
sqlite3 (2.
|
250
|
-
sqlite3 (2.
|
251
|
-
sqlite3 (2.
|
252
|
-
sqlite3 (2.
|
253
|
-
sqlite3 (2.
|
246
|
+
sqlite3 (2.7.0-aarch64-linux-gnu)
|
247
|
+
sqlite3 (2.7.0-aarch64-linux-musl)
|
248
|
+
sqlite3 (2.7.0-arm-linux-gnu)
|
249
|
+
sqlite3 (2.7.0-arm-linux-musl)
|
250
|
+
sqlite3 (2.7.0-arm64-darwin)
|
251
|
+
sqlite3 (2.7.0-x86_64-darwin)
|
252
|
+
sqlite3 (2.7.0-x86_64-linux-gnu)
|
253
|
+
sqlite3 (2.7.0-x86_64-linux-musl)
|
254
254
|
stringio (3.1.7)
|
255
255
|
strings-ansi (0.2.0)
|
256
256
|
timeout (0.4.3)
|
data/README.md
CHANGED
@@ -388,7 +388,7 @@ Boxcars automatically tracks LLM calls with detailed metrics:
|
|
388
388
|
|
389
389
|
```ruby
|
390
390
|
# This automatically generates observability events
|
391
|
-
engine = Boxcars::Openai.new
|
391
|
+
engine = Boxcars::Openai.new(user_id: USER_ID) # optional user_id. All engines take this.
|
392
392
|
calc = Boxcars::Calculator.new(engine: engine)
|
393
393
|
result = calc.run "what is 2 + 2?"
|
394
394
|
```
|
@@ -404,6 +404,7 @@ result = calc.run "what is 2 + 2?"
|
|
404
404
|
- `error_message`: Error details if the call failed
|
405
405
|
- `response_raw_body`: Raw API response
|
406
406
|
- `api_call_parameters`: Parameters sent to the API
|
407
|
+
- `distinct_id`: If you specify a user_id to your engine, it will be passed up.
|
407
408
|
|
408
409
|
#### Manual Tracking
|
409
410
|
|
@@ -0,0 +1,435 @@
|
|
1
|
+
# User Context in Boxcars Observations
|
2
|
+
|
3
|
+
This guide explains how to use the new user context feature in Boxcars observations to track which user performed specific actions.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The user context feature allows you to associate user information with Boxcars observations, enabling better tracking, debugging, and analytics. This is particularly useful when using Boxcars in web applications where you want to tie AI operations to specific users.
|
8
|
+
|
9
|
+
## Key Features
|
10
|
+
|
11
|
+
- **Non-intrusive**: Uses the existing `added_context` mechanism
|
12
|
+
- **Backward compatible**: Existing code continues to work unchanged
|
13
|
+
- **Flexible**: You control what user data is included
|
14
|
+
- **Analytics-ready**: Automatically formats user data for analytics systems like PostHog
|
15
|
+
- **Privacy-conscious**: Only includes data you explicitly provide
|
16
|
+
|
17
|
+
## Enhanced Observation Class
|
18
|
+
|
19
|
+
### New Methods
|
20
|
+
|
21
|
+
#### `Observation.with_user(note, user_context:, status: :ok, **additional_context)`
|
22
|
+
|
23
|
+
Creates an observation with user context.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
user_context = {
|
27
|
+
id: current_user.id,
|
28
|
+
email: current_user.email,
|
29
|
+
role: current_user.role
|
30
|
+
}
|
31
|
+
|
32
|
+
observation = Boxcars::Observation.with_user(
|
33
|
+
"User performed search",
|
34
|
+
user_context: user_context,
|
35
|
+
query: "machine learning",
|
36
|
+
results_count: 42
|
37
|
+
)
|
38
|
+
```
|
39
|
+
|
40
|
+
#### `Observation.ok_with_user(note, user_context:, **additional_context)`
|
41
|
+
|
42
|
+
Creates a successful observation with user context.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
observation = Boxcars::Observation.ok_with_user(
|
46
|
+
"Search completed successfully",
|
47
|
+
user_context: user_context,
|
48
|
+
processing_time_ms: 150
|
49
|
+
)
|
50
|
+
```
|
51
|
+
|
52
|
+
#### `Observation.err_with_user(note, user_context:, **additional_context)`
|
53
|
+
|
54
|
+
Creates an error observation with user context.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
observation = Boxcars::Observation.err_with_user(
|
58
|
+
"Search failed due to timeout",
|
59
|
+
user_context: user_context,
|
60
|
+
error_code: "TIMEOUT"
|
61
|
+
)
|
62
|
+
```
|
63
|
+
|
64
|
+
### New Instance Methods
|
65
|
+
|
66
|
+
#### `#user_context`
|
67
|
+
|
68
|
+
Returns the user context hash if present, `nil` otherwise.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
observation.user_context
|
72
|
+
# => {id: 123, email: "user@example.com", role: "admin"}
|
73
|
+
```
|
74
|
+
|
75
|
+
#### `#user_context?`
|
76
|
+
|
77
|
+
Returns `true` if the observation has user context, `false` otherwise.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
observation.user_context?
|
81
|
+
# => true
|
82
|
+
```
|
83
|
+
|
84
|
+
## Enhanced Observability System
|
85
|
+
|
86
|
+
### Updated Track Method
|
87
|
+
|
88
|
+
The `Boxcars::Observability.track` method now accepts an optional `observation` parameter:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
Boxcars::Observability.track(
|
92
|
+
event: 'user_action',
|
93
|
+
properties: { action_type: 'search' },
|
94
|
+
observation: observation_with_user_context
|
95
|
+
)
|
96
|
+
```
|
97
|
+
|
98
|
+
When an observation with user context is provided, the user data is automatically merged into the tracking properties with `$user_` prefixes (PostHog compatible).
|
99
|
+
|
100
|
+
### New Track Observation Method
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
Boxcars::Observability.track_observation(
|
104
|
+
observation,
|
105
|
+
event: 'custom_event_name', # optional, defaults to 'boxcar_observation'
|
106
|
+
additional_property: 'value'
|
107
|
+
)
|
108
|
+
```
|
109
|
+
|
110
|
+
This method automatically:
|
111
|
+
- Extracts observation details (note, status, timestamp)
|
112
|
+
- Includes all additional context from the observation
|
113
|
+
- Merges user context with `$user_` prefixes
|
114
|
+
- Sends everything to your configured observability backend
|
115
|
+
|
116
|
+
## Rails Integration Pattern
|
117
|
+
|
118
|
+
### Controller Helper Methods
|
119
|
+
|
120
|
+
Create helper methods in your `ApplicationController`:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
class ApplicationController < ActionController::Base
|
124
|
+
private
|
125
|
+
|
126
|
+
# Helper method to create observations with user context
|
127
|
+
def create_observation_with_user(note, status: :ok, **additional_context)
|
128
|
+
if current_user
|
129
|
+
Boxcars::Observation.with_user(
|
130
|
+
note,
|
131
|
+
user_context: current_user.to_user_context,
|
132
|
+
status: status,
|
133
|
+
**additional_context
|
134
|
+
)
|
135
|
+
else
|
136
|
+
Boxcars::Observation.new(note: note, status: status, **additional_context)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Helper method for successful operations
|
141
|
+
def success_observation_with_user(note, **additional_context)
|
142
|
+
create_observation_with_user(note, status: :ok, **additional_context)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Helper method for error operations
|
146
|
+
def error_observation_with_user(note, **additional_context)
|
147
|
+
create_observation_with_user(note, status: :error, **additional_context)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
### User Model Extension
|
153
|
+
|
154
|
+
Add a method to your User model to convert user data to context:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
class User < ApplicationRecord
|
158
|
+
def to_user_context
|
159
|
+
{
|
160
|
+
id: id,
|
161
|
+
email: email,
|
162
|
+
role: role,
|
163
|
+
# Add other relevant fields, but be mindful of privacy
|
164
|
+
# Don't include sensitive data like passwords, tokens, etc.
|
165
|
+
}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
### Usage in Controllers
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
class SearchController < ApplicationController
|
174
|
+
def search
|
175
|
+
query = params[:query]
|
176
|
+
|
177
|
+
begin
|
178
|
+
# Perform search with Boxcars
|
179
|
+
results = perform_boxcar_search(query)
|
180
|
+
|
181
|
+
# Track successful search
|
182
|
+
observation = success_observation_with_user(
|
183
|
+
"Search completed successfully",
|
184
|
+
query: query,
|
185
|
+
results_count: results.count,
|
186
|
+
processing_time_ms: 250
|
187
|
+
)
|
188
|
+
|
189
|
+
Boxcars::Observability.track_observation(
|
190
|
+
observation,
|
191
|
+
event: 'search_completed',
|
192
|
+
controller: 'SearchController',
|
193
|
+
action: 'search'
|
194
|
+
)
|
195
|
+
|
196
|
+
render json: { results: results }
|
197
|
+
|
198
|
+
rescue StandardError => e
|
199
|
+
# Track search error
|
200
|
+
error_observation = error_observation_with_user(
|
201
|
+
"Search failed: #{e.message}",
|
202
|
+
query: query,
|
203
|
+
error_class: e.class.name
|
204
|
+
)
|
205
|
+
|
206
|
+
Boxcars::Observability.track_observation(
|
207
|
+
error_observation,
|
208
|
+
event: 'search_failed',
|
209
|
+
controller: 'SearchController',
|
210
|
+
action: 'search'
|
211
|
+
)
|
212
|
+
|
213
|
+
render json: { error: "Search failed" }, status: 500
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
## Analytics Integration
|
220
|
+
|
221
|
+
### PostHog Integration
|
222
|
+
|
223
|
+
When using PostHog as your observability backend, user context is automatically formatted with `$user_` prefixes:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
# This user context:
|
227
|
+
user_context = {
|
228
|
+
id: 123,
|
229
|
+
email: "user@example.com",
|
230
|
+
role: "admin"
|
231
|
+
}
|
232
|
+
|
233
|
+
# Becomes these PostHog properties:
|
234
|
+
{
|
235
|
+
"$user_id" => 123,
|
236
|
+
"$user_email" => "user@example.com",
|
237
|
+
"$user_role" => "admin"
|
238
|
+
}
|
239
|
+
```
|
240
|
+
|
241
|
+
This allows PostHog to automatically associate events with users and enables user-based analytics and segmentation.
|
242
|
+
|
243
|
+
### Custom Analytics Systems
|
244
|
+
|
245
|
+
For other analytics systems, you can access the user context directly:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
observation = Boxcars::Observation.ok_with_user("Action completed", user_context: user_data)
|
249
|
+
|
250
|
+
# Access user context
|
251
|
+
user_info = observation.user_context
|
252
|
+
# => {id: 123, email: "user@example.com", role: "admin"}
|
253
|
+
|
254
|
+
# Include in your custom tracking
|
255
|
+
your_analytics.track(
|
256
|
+
event: 'boxcar_action',
|
257
|
+
user_id: user_info[:id],
|
258
|
+
user_email: user_info[:email],
|
259
|
+
properties: observation.to_h
|
260
|
+
)
|
261
|
+
```
|
262
|
+
|
263
|
+
## Privacy Considerations
|
264
|
+
|
265
|
+
### What to Include
|
266
|
+
|
267
|
+
**Safe to include:**
|
268
|
+
- User ID (for tracking purposes)
|
269
|
+
- Email (if needed for support)
|
270
|
+
- Role/permissions level
|
271
|
+
- Account type (free, premium, etc.)
|
272
|
+
- Tenant/organization ID
|
273
|
+
|
274
|
+
**Avoid including:**
|
275
|
+
- Passwords or password hashes
|
276
|
+
- API keys or tokens
|
277
|
+
- Personal identification numbers
|
278
|
+
- Credit card information
|
279
|
+
- Any other sensitive personal data
|
280
|
+
|
281
|
+
### Example Safe User Context
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
def to_user_context
|
285
|
+
{
|
286
|
+
id: id,
|
287
|
+
email: email,
|
288
|
+
role: role,
|
289
|
+
account_type: subscription&.plan_name,
|
290
|
+
organization_id: organization_id,
|
291
|
+
created_at: created_at.iso8601,
|
292
|
+
last_login: last_sign_in_at&.iso8601
|
293
|
+
}
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
## Testing
|
298
|
+
|
299
|
+
### RSpec Examples
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
RSpec.describe "User Context in Observations" do
|
303
|
+
let(:user_context) do
|
304
|
+
{
|
305
|
+
id: 123,
|
306
|
+
email: "test@example.com",
|
307
|
+
role: "admin"
|
308
|
+
}
|
309
|
+
end
|
310
|
+
|
311
|
+
it "creates observation with user context" do
|
312
|
+
observation = Boxcars::Observation.ok_with_user(
|
313
|
+
"Test action",
|
314
|
+
user_context: user_context
|
315
|
+
)
|
316
|
+
|
317
|
+
expect(observation.user_context?).to be true
|
318
|
+
expect(observation.user_context[:id]).to eq(123)
|
319
|
+
expect(observation.user_context[:email]).to eq("test@example.com")
|
320
|
+
end
|
321
|
+
|
322
|
+
it "tracks observation with user context" do
|
323
|
+
observation = Boxcars::Observation.ok_with_user(
|
324
|
+
"Test action",
|
325
|
+
user_context: user_context
|
326
|
+
)
|
327
|
+
|
328
|
+
expect(Boxcars::Observability).to receive(:track).with(
|
329
|
+
event: 'test_event',
|
330
|
+
properties: hash_including(
|
331
|
+
"$user_id" => 123,
|
332
|
+
"$user_email" => "test@example.com"
|
333
|
+
)
|
334
|
+
)
|
335
|
+
|
336
|
+
Boxcars::Observability.track(
|
337
|
+
event: 'test_event',
|
338
|
+
properties: {},
|
339
|
+
observation: observation
|
340
|
+
)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
```
|
344
|
+
|
345
|
+
## Migration Guide
|
346
|
+
|
347
|
+
### Existing Code
|
348
|
+
|
349
|
+
Your existing observation code continues to work unchanged:
|
350
|
+
|
351
|
+
```ruby
|
352
|
+
# This still works exactly as before
|
353
|
+
observation = Boxcars::Observation.ok("Action completed", extra: "data")
|
354
|
+
Boxcars::Observability.track(event: 'action', properties: { type: 'test' })
|
355
|
+
```
|
356
|
+
|
357
|
+
### Adding User Context
|
358
|
+
|
359
|
+
To add user context to existing observations, simply replace:
|
360
|
+
|
361
|
+
```ruby
|
362
|
+
# Before
|
363
|
+
observation = Boxcars::Observation.ok("Action completed")
|
364
|
+
|
365
|
+
# After
|
366
|
+
observation = Boxcars::Observation.ok_with_user(
|
367
|
+
"Action completed",
|
368
|
+
user_context: current_user.to_user_context
|
369
|
+
)
|
370
|
+
```
|
371
|
+
|
372
|
+
### Updating Tracking Calls
|
373
|
+
|
374
|
+
To include user context in tracking:
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
# Before
|
378
|
+
Boxcars::Observability.track(
|
379
|
+
event: 'action_completed',
|
380
|
+
properties: { action_type: 'search' }
|
381
|
+
)
|
382
|
+
|
383
|
+
# After
|
384
|
+
Boxcars::Observability.track_observation(
|
385
|
+
observation_with_user_context,
|
386
|
+
event: 'action_completed',
|
387
|
+
action_type: 'search'
|
388
|
+
)
|
389
|
+
```
|
390
|
+
|
391
|
+
## Benefits
|
392
|
+
|
393
|
+
1. **Better Debugging**: Quickly identify which user encountered an issue
|
394
|
+
2. **User Analytics**: Segment usage patterns by user type, role, or other attributes
|
395
|
+
3. **Compliance**: Track user actions for audit trails
|
396
|
+
4. **Support**: Faster issue resolution with user context
|
397
|
+
5. **Product Insights**: Understand how different user segments use AI features
|
398
|
+
|
399
|
+
## Best Practices
|
400
|
+
|
401
|
+
1. **Consistent Context**: Use the same user context structure across your application
|
402
|
+
2. **Helper Methods**: Create controller helpers to reduce code duplication
|
403
|
+
3. **Privacy First**: Only include necessary user data
|
404
|
+
4. **Error Handling**: Always handle cases where `current_user` might be nil
|
405
|
+
5. **Testing**: Write tests for both user and anonymous scenarios
|
406
|
+
6. **Documentation**: Document what user data you're tracking and why
|
407
|
+
|
408
|
+
## Troubleshooting
|
409
|
+
|
410
|
+
### Common Issues
|
411
|
+
|
412
|
+
**User context not appearing in analytics:**
|
413
|
+
- Ensure your observability backend is configured
|
414
|
+
- Check that you're using `track_observation` or passing the `observation` parameter to `track`
|
415
|
+
- Verify the observation actually has user context with `user_context?`
|
416
|
+
|
417
|
+
**Anonymous users causing errors:**
|
418
|
+
- Always check for `current_user` presence before creating user context
|
419
|
+
- Use helper methods that handle nil users gracefully
|
420
|
+
|
421
|
+
**Missing user data:**
|
422
|
+
- Ensure your `to_user_context` method returns a hash
|
423
|
+
- Check that the user object has the expected attributes
|
424
|
+
|
425
|
+
### Debug Example
|
426
|
+
|
427
|
+
```ruby
|
428
|
+
# Debug user context
|
429
|
+
observation = Boxcars::Observation.ok_with_user("Test", user_context: user_data)
|
430
|
+
puts "Has user context: #{observation.user_context?}"
|
431
|
+
puts "User context: #{observation.user_context.inspect}"
|
432
|
+
puts "Full observation: #{observation.to_h.inspect}"
|
433
|
+
```
|
434
|
+
|
435
|
+
This comprehensive user context system provides a clean, privacy-conscious way to associate user information with Boxcars operations, enabling better tracking, debugging, and analytics while maintaining backward compatibility.
|
@@ -28,10 +28,11 @@ module Boxcars
|
|
28
28
|
# useful for when you need to use AI to answer questions. You should ask targeted questions".
|
29
29
|
# @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
|
30
30
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], **kwargs)
|
31
|
+
user_id = kwargs.delete(:user_id)
|
31
32
|
@llm_params = DEFAULT_PARAMS.merge(kwargs)
|
32
33
|
@prompts = prompts
|
33
34
|
@batch_size = 20
|
34
|
-
super(description:, name:)
|
35
|
+
super(description:, name:, user_id:)
|
35
36
|
end
|
36
37
|
|
37
38
|
def conversation_model?(_model)
|
@@ -278,7 +279,8 @@ module Boxcars
|
|
278
279
|
request_context = {
|
279
280
|
prompt: call_context[:prompt_object],
|
280
281
|
inputs: call_context[:inputs],
|
281
|
-
conversation_for_api: call_context[:api_request_params]
|
282
|
+
conversation_for_api: call_context[:api_request_params],
|
283
|
+
user_id:
|
282
284
|
}
|
283
285
|
|
284
286
|
track_ai_generation(
|
@@ -27,10 +27,11 @@ module Boxcars
|
|
27
27
|
# useful for when you need to use AI to answer questions. You should ask targeted questions".
|
28
28
|
# @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
|
29
29
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], **kwargs)
|
30
|
+
user_id = kwargs.delete(:user_id)
|
30
31
|
@llm_params = DEFAULT_PARAMS.merge(kwargs)
|
31
32
|
@prompts = prompts
|
32
33
|
@batch_size = 20
|
33
|
-
super(description:, name:)
|
34
|
+
super(description:, name:, user_id:)
|
34
35
|
end
|
35
36
|
|
36
37
|
def conversation_model?(_model)
|
@@ -211,7 +212,8 @@ module Boxcars
|
|
211
212
|
request_context = {
|
212
213
|
prompt: call_context[:prompt_object],
|
213
214
|
inputs: call_context[:inputs],
|
214
|
-
conversation_for_api: call_context[:api_request_params]
|
215
|
+
conversation_for_api: call_context[:api_request_params],
|
216
|
+
user_id:
|
215
217
|
}
|
216
218
|
|
217
219
|
track_ai_generation(
|
@@ -19,10 +19,11 @@ module Boxcars
|
|
19
19
|
"You should ask targeted questions"
|
20
20
|
|
21
21
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
|
22
|
+
user_id = kwargs.delete(:user_id)
|
22
23
|
@llm_params = DEFAULT_PARAMS.merge(kwargs) # Corrected typo here
|
23
24
|
@prompts = prompts
|
24
25
|
@batch_size = batch_size
|
25
|
-
super(description:, name:)
|
26
|
+
super(description:, name:, user_id:)
|
26
27
|
end
|
27
28
|
|
28
29
|
# Renamed from open_ai_client to gemini_client for clarity
|
@@ -68,7 +69,8 @@ module Boxcars
|
|
68
69
|
request_context = {
|
69
70
|
prompt: current_prompt_object,
|
70
71
|
inputs:,
|
71
|
-
conversation_for_api: api_request_params&.dig(:messages) || []
|
72
|
+
conversation_for_api: api_request_params&.dig(:messages) || [],
|
73
|
+
user_id:
|
72
74
|
}
|
73
75
|
track_ai_generation(
|
74
76
|
duration_ms:,
|
@@ -82,13 +84,13 @@ module Boxcars
|
|
82
84
|
# If there's an error, raise it to maintain backward compatibility with existing tests
|
83
85
|
raise response_data[:error] if response_data[:error]
|
84
86
|
|
85
|
-
response_data
|
87
|
+
response_data[:parsed_json]
|
86
88
|
end
|
87
89
|
|
88
90
|
def run(question, **)
|
89
91
|
prompt = Prompt.new(template: question)
|
90
|
-
|
91
|
-
answer =
|
92
|
+
response = client(prompt:, inputs: {}, **)
|
93
|
+
answer = _extract_content_from_gemini_response(response)
|
92
94
|
Boxcars.debug("Answer: #{answer}", :cyan)
|
93
95
|
answer
|
94
96
|
end
|
@@ -19,10 +19,11 @@ module Boxcars
|
|
19
19
|
}.freeze
|
20
20
|
|
21
21
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 2, **kwargs)
|
22
|
+
user_id = kwargs.delete(:user_id)
|
22
23
|
@gpt4all_params = DEFAULT_PARAMS.merge(kwargs) # Store merged params
|
23
24
|
@prompts = prompts
|
24
25
|
@batch_size = batch_size # Retain if used by other methods
|
25
|
-
super(description:, name:)
|
26
|
+
super(description:, name:, user_id:)
|
26
27
|
end
|
27
28
|
|
28
29
|
def client(prompt:, inputs: {}, **kwargs)
|
@@ -31,10 +32,8 @@ module Boxcars
|
|
31
32
|
# current_params are the effective parameters for this call, including defaults and overrides
|
32
33
|
current_params = @gpt4all_params.merge(kwargs)
|
33
34
|
# api_request_params for GPT4All is just the input text.
|
34
|
-
api_request_params = nil
|
35
|
+
api_request_params, gpt4all_instance = nil
|
35
36
|
current_prompt_object = prompt.is_a?(Array) ? prompt.first : prompt
|
36
|
-
gpt4all_instance = nil # To ensure it's in scope for ensure block
|
37
|
-
|
38
37
|
begin
|
39
38
|
gpt4all_instance = Gpt4all::ConversationalAI.new
|
40
39
|
# prepare_resources might download models, could take time.
|
@@ -68,7 +67,8 @@ module Boxcars
|
|
68
67
|
request_context = {
|
69
68
|
prompt: current_prompt_object,
|
70
69
|
inputs:,
|
71
|
-
conversation_for_api: api_request_params&.dig(:prompt)
|
70
|
+
conversation_for_api: api_request_params&.dig(:prompt),
|
71
|
+
user_id:
|
72
72
|
}
|
73
73
|
|
74
74
|
track_ai_generation(
|
data/lib/boxcars/engine/groq.rb
CHANGED
@@ -19,10 +19,11 @@ module Boxcars
|
|
19
19
|
"You should ask targeted questions"
|
20
20
|
|
21
21
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
|
22
|
+
user_id = kwargs.delete(:user_id)
|
22
23
|
@groq_params = DEFAULT_PARAMS.merge(kwargs) # Corrected typo here
|
23
24
|
@prompts = prompts
|
24
25
|
@batch_size = batch_size
|
25
|
-
super(description:, name:)
|
26
|
+
super(description:, name:, user_id:)
|
26
27
|
end
|
27
28
|
|
28
29
|
# Renamed from open_ai_client to groq_client for clarity
|
@@ -60,7 +61,8 @@ module Boxcars
|
|
60
61
|
request_context = {
|
61
62
|
prompt: current_prompt_object,
|
62
63
|
inputs:,
|
63
|
-
conversation_for_api: api_request_params&.dig(:messages)
|
64
|
+
conversation_for_api: api_request_params&.dig(:messages),
|
65
|
+
user_id:
|
64
66
|
}
|
65
67
|
track_ai_generation(
|
66
68
|
duration_ms:,
|
@@ -74,23 +76,23 @@ module Boxcars
|
|
74
76
|
# If there's an error, raise it to maintain backward compatibility with existing tests
|
75
77
|
raise response_data[:error] if response_data[:error]
|
76
78
|
|
77
|
-
response_data
|
79
|
+
response_data[:parsed_json]
|
78
80
|
end
|
79
81
|
|
80
82
|
def run(question, **)
|
81
83
|
prompt = Prompt.new(template: question)
|
82
|
-
|
83
|
-
answer =
|
84
|
+
response = client(prompt:, inputs: {}, **)
|
85
|
+
answer = extract_answer(response)
|
84
86
|
Boxcars.debug("Answer: #{answer}", :cyan)
|
85
87
|
answer
|
86
88
|
end
|
87
89
|
|
90
|
+
private
|
91
|
+
|
88
92
|
def default_params
|
89
|
-
@groq_params
|
93
|
+
@groq_params
|
90
94
|
end
|
91
95
|
|
92
|
-
private
|
93
|
-
|
94
96
|
# Helper methods for the client method
|
95
97
|
def _prepare_groq_request_params(prompt_object, inputs, current_params)
|
96
98
|
messages_hash_from_prompt = prompt_object.as_messages(inputs)
|
@@ -17,10 +17,11 @@ module Boxcars
|
|
17
17
|
# @param batch_size [Integer] The number of prompts to send to the Engine at a time.
|
18
18
|
# @param kwargs [Hash] Additional parameters to pass to the Engine.
|
19
19
|
def initialize(provider:, description:, name:, prompts: [], batch_size: 20, **kwargs)
|
20
|
+
user_id = kwargs.delete(:user_id)
|
20
21
|
@provider = provider
|
21
22
|
# Start with defaults, merge other kwargs, then explicitly set model if provided in initialize
|
22
23
|
@all_params = default_model_params.merge(kwargs)
|
23
|
-
super(description:, name:, prompts:, batch_size:)
|
24
|
+
super(description:, name:, prompts:, batch_size:, user_id:)
|
24
25
|
end
|
25
26
|
|
26
27
|
# can be overridden by provider subclass
|
@@ -68,7 +69,7 @@ module Boxcars
|
|
68
69
|
|
69
70
|
adapter = adapter(api_key:, params:)
|
70
71
|
convo = prompt.as_intelligence_conversation(inputs:)
|
71
|
-
request_context = { prompt: prompt&.as_prompt(inputs:)&.[](:prompt), inputs:, conversation_for_api: convo.to_h }
|
72
|
+
request_context = { user_id:, prompt: prompt&.as_prompt(inputs:)&.[](:prompt), inputs:, conversation_for_api: convo.to_h }
|
72
73
|
request = Intelligence::ChatRequest.new(adapter:)
|
73
74
|
|
74
75
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -118,17 +119,6 @@ module Boxcars
|
|
118
119
|
|
119
120
|
private
|
120
121
|
|
121
|
-
def extract_answer(response)
|
122
|
-
# Handle different response formats
|
123
|
-
if response["choices"]
|
124
|
-
response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
|
125
|
-
elsif response["candidates"]
|
126
|
-
response["candidates"].map { |c| c.dig("content", "parts", 0, "text") }.join("\n").strip
|
127
|
-
else
|
128
|
-
response["output"] || response.to_s
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
122
|
def check_response(response)
|
133
123
|
return if response.is_a?(Hash) && response.key?("choices")
|
134
124
|
|
@@ -19,10 +19,11 @@ module Boxcars
|
|
19
19
|
"You should ask targeted questions"
|
20
20
|
|
21
21
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 2, **kwargs)
|
22
|
+
user_id = kwargs.delete(:user_id)
|
22
23
|
@ollama_params = DEFAULT_PARAMS.merge(kwargs)
|
23
24
|
@prompts = prompts
|
24
25
|
@batch_size = batch_size # Retain if used by other methods
|
25
|
-
super(description:, name:)
|
26
|
+
super(description:, name:, user_id:)
|
26
27
|
end
|
27
28
|
|
28
29
|
# Renamed from open_ai_client to ollama_client for clarity
|
@@ -63,7 +64,8 @@ module Boxcars
|
|
63
64
|
request_context = {
|
64
65
|
prompt: current_prompt_object,
|
65
66
|
inputs:,
|
66
|
-
conversation_for_api: api_request_params&.dig(:messages)
|
67
|
+
conversation_for_api: api_request_params&.dig(:messages),
|
68
|
+
user_id:
|
67
69
|
}
|
68
70
|
track_ai_generation(
|
69
71
|
duration_ms:,
|
@@ -20,6 +20,7 @@ module Boxcars
|
|
20
20
|
"You should ask targeted questions"
|
21
21
|
|
22
22
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
|
23
|
+
user_id = kwargs.delete(:user_id)
|
23
24
|
@open_ai_params = DEFAULT_PARAMS.merge(kwargs)
|
24
25
|
# Special handling for o1-mini model (deprecated?)
|
25
26
|
if @open_ai_params[:model] =~ /^o/ && @open_ai_params[:max_tokens]
|
@@ -29,7 +30,7 @@ module Boxcars
|
|
29
30
|
|
30
31
|
@prompts = prompts
|
31
32
|
@batch_size = batch_size
|
32
|
-
super(description:, name:)
|
33
|
+
super(description:, name:, user_id:)
|
33
34
|
end
|
34
35
|
|
35
36
|
def self.open_ai_client(openai_access_token: nil)
|
@@ -231,6 +232,7 @@ module Boxcars
|
|
231
232
|
request_context = {
|
232
233
|
prompt: call_context[:prompt_object],
|
233
234
|
inputs: call_context[:inputs],
|
235
|
+
user_id:,
|
234
236
|
conversation_for_api: is_chat_model ? api_request_params[:messages] : api_request_params[:prompt]
|
235
237
|
}
|
236
238
|
|
@@ -20,10 +20,11 @@ module Boxcars
|
|
20
20
|
"You should ask targeted questions"
|
21
21
|
|
22
22
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
|
23
|
+
user_id = kwargs.delete(:user_id)
|
23
24
|
@perplexity_params = DEFAULT_PARAMS.merge(kwargs)
|
24
25
|
@prompts = prompts
|
25
26
|
@batch_size = batch_size # Retain if used by generate
|
26
|
-
super(description:, name:)
|
27
|
+
super(description:, name:, user_id:)
|
27
28
|
end
|
28
29
|
|
29
30
|
# Perplexity models are conversational.
|
@@ -96,6 +97,7 @@ module Boxcars
|
|
96
97
|
request_context = {
|
97
98
|
prompt: current_prompt_object,
|
98
99
|
inputs:,
|
100
|
+
user_id:,
|
99
101
|
conversation_for_api: api_request_params&.dig(:messages)
|
100
102
|
}
|
101
103
|
track_ai_generation(
|
@@ -52,7 +52,8 @@ module Boxcars
|
|
52
52
|
'$ai_latency': duration_seconds,
|
53
53
|
'$ai_http_status': extract_status_code(response_data) || (response_data[:success] ? 200 : 500),
|
54
54
|
'$ai_base_url': get_base_url_for_provider(provider),
|
55
|
-
'$ai_is_error': !response_data[:success]
|
55
|
+
'$ai_is_error': !response_data[:success],
|
56
|
+
user_id:
|
56
57
|
}
|
57
58
|
|
58
59
|
# Add error details if present
|
data/lib/boxcars/engine.rb
CHANGED
@@ -3,18 +3,20 @@
|
|
3
3
|
module Boxcars
|
4
4
|
# @abstract
|
5
5
|
class Engine
|
6
|
-
attr_reader :prompts, :batch_size
|
6
|
+
attr_reader :prompts, :batch_size, :user_id
|
7
7
|
|
8
8
|
# An Engine is used by Boxcars to generate output from prompts
|
9
9
|
# @param name [String] The name of the Engine. Defaults to classname.
|
10
10
|
# @param description [String] A description of the Engine.
|
11
11
|
# @param prompts [Array<Prompt>] The prompts to use for the Engine.
|
12
12
|
# @param batch_size [Integer] The number of prompts to send to the Engine at a time.
|
13
|
-
|
13
|
+
# @param user_id [String, Integer] The ID of the user using this Engine (optional for observability).
|
14
|
+
def initialize(description: 'Engine', name: nil, prompts: [], batch_size: 20, user_id: nil)
|
14
15
|
@name = name || self.class.name
|
15
16
|
@description = description
|
16
17
|
@prompts = prompts
|
17
18
|
@batch_size = batch_size
|
19
|
+
@user_id = user_id
|
18
20
|
end
|
19
21
|
|
20
22
|
# Get an answer from the Engine.
|
@@ -60,14 +62,8 @@ module Boxcars
|
|
60
62
|
sub_prompts.each do |sprompt, inputs|
|
61
63
|
client_response = client(prompt: sprompt, inputs:, **params)
|
62
64
|
|
63
|
-
#
|
64
|
-
|
65
|
-
# - Legacy format: direct API response hash (OpenAI, others)
|
66
|
-
api_response_hash = if client_response.is_a?(Hash) && client_response.key?(:parsed_json)
|
67
|
-
client_response[:parsed_json]
|
68
|
-
else
|
69
|
-
client_response
|
70
|
-
end
|
65
|
+
# All engines now return the parsed API response hash directly
|
66
|
+
api_response_hash = client_response
|
71
67
|
|
72
68
|
# Ensure we have a hash to work with
|
73
69
|
unless api_response_hash.is_a?(Hash)
|
@@ -101,6 +97,17 @@ module Boxcars
|
|
101
97
|
end
|
102
98
|
EngineResult.new(generations:, engine_output: { token_usage: })
|
103
99
|
end
|
100
|
+
|
101
|
+
def extract_answer(response)
|
102
|
+
# Handle different response formats
|
103
|
+
if response["choices"]
|
104
|
+
response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
|
105
|
+
elsif response["candidates"]
|
106
|
+
response["candidates"].map { |c| c.dig("content", "parts", 0, "text") }.join("\n").strip
|
107
|
+
else
|
108
|
+
response["output"] || response.to_s
|
109
|
+
end
|
110
|
+
end
|
104
111
|
end
|
105
112
|
end
|
106
113
|
|
@@ -20,16 +20,58 @@ module Boxcars
|
|
20
20
|
#
|
21
21
|
# @param event [String, Symbol] The name of the event to track.
|
22
22
|
# @param properties [Hash] A hash of properties associated with the event.
|
23
|
-
|
23
|
+
# @param observation [Boxcars::Observation, nil] Optional observation object to extract user context from.
|
24
|
+
def track(event:, properties:, observation: nil)
|
24
25
|
return unless backend
|
25
26
|
|
26
|
-
|
27
|
+
# Merge user context from observation if present
|
28
|
+
final_properties = properties.dup
|
29
|
+
final_properties = merge_user_context(final_properties, observation.user_context) if observation&.user_context?
|
30
|
+
|
31
|
+
backend.track(event:, properties: final_properties)
|
27
32
|
rescue StandardError
|
28
33
|
# Fail silently as requested.
|
29
34
|
# Optionally, if Boxcars had a central logger:
|
30
35
|
# Boxcars.logger.warn "Boxcars::Observability: Backend error during track: #{e.message} (#{e.class.name})"
|
31
36
|
end
|
32
37
|
|
38
|
+
# Tracks an observation event, automatically extracting user context if present
|
39
|
+
# @param observation [Boxcars::Observation] The observation to track
|
40
|
+
# @param event [String, Symbol] The event name (defaults to 'boxcar_observation')
|
41
|
+
# @param additional_properties [Hash] Additional properties to include
|
42
|
+
def track_observation(observation, event: 'boxcar_observation', **additional_properties)
|
43
|
+
properties = {
|
44
|
+
observation_note: observation.note,
|
45
|
+
observation_status: observation.status,
|
46
|
+
timestamp: Time.now.iso8601
|
47
|
+
}.merge(additional_properties)
|
48
|
+
|
49
|
+
# Add all observation context (including user_context) to properties
|
50
|
+
properties.merge!(observation.added_context) if observation.added_context
|
51
|
+
|
52
|
+
track(event:, properties:, observation:)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Merge user context into properties with proper namespacing
|
58
|
+
# @param properties [Hash] The existing properties
|
59
|
+
# @param user_context [Hash] The user context to merge
|
60
|
+
# @return [Hash] The merged properties
|
61
|
+
def merge_user_context(properties, user_context)
|
62
|
+
return properties unless user_context.is_a?(Hash)
|
63
|
+
|
64
|
+
# Add user context with proper prefixing for analytics systems
|
65
|
+
user_properties = {}
|
66
|
+
user_context.each do |key, value|
|
67
|
+
# Use $user_ prefix for PostHog compatibility
|
68
|
+
user_key = key.to_s.start_with?('$user_') ? key : "$user_#{key}"
|
69
|
+
user_properties[user_key] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
properties.merge(user_properties)
|
73
|
+
end
|
74
|
+
|
33
75
|
# Flushes any pending events if the backend supports it.
|
34
76
|
# This is useful for testing or when you need to ensure events are sent before the process exits.
|
35
77
|
def flush
|
@@ -52,21 +52,9 @@ module Boxcars
|
|
52
52
|
# @param properties [Hash] A hash of properties for the event.
|
53
53
|
# It's recommended to include a `:user_id` for user-specific tracking.
|
54
54
|
def track(event:, properties:)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
distinct_id = tracking_properties.delete(:user_id) || tracking_properties.delete('user_id') || "anonymous_user"
|
59
|
-
|
60
|
-
# The PostHog gem's capture method handles distinct_id and properties.
|
61
|
-
# It's important that distinct_id is a string.
|
62
|
-
@posthog_client.capture(
|
63
|
-
distinct_id: distinct_id.to_s, # Ensure distinct_id is a string
|
64
|
-
event: event.to_s, # Ensure event name is a string
|
65
|
-
properties: tracking_properties
|
66
|
-
)
|
67
|
-
# The posthog-ruby client handles flushing events asynchronously.
|
68
|
-
# If immediate flushing is needed for testing or specific scenarios:
|
69
|
-
# @posthog_client.flush
|
55
|
+
properties = {} unless properties.is_a?(Hash)
|
56
|
+
distinct_id = properties.delete(:user_id) || current_user_id || "anonymous_user"
|
57
|
+
@posthog_client.capture(distinct_id:, event:, properties:)
|
70
58
|
end
|
71
59
|
|
72
60
|
# Flushes any pending events to PostHog immediately.
|
@@ -74,5 +62,12 @@ module Boxcars
|
|
74
62
|
def flush
|
75
63
|
@posthog_client.flush if @posthog_client.respond_to?(:flush)
|
76
64
|
end
|
65
|
+
|
66
|
+
# in Rails, this is a way to find the current user id
|
67
|
+
def current_user_id
|
68
|
+
return unless defined?(::Current) && ::Current.respond_to?(:user)
|
69
|
+
|
70
|
+
::Current.user&.id
|
71
|
+
end
|
77
72
|
end
|
78
73
|
end
|
data/lib/boxcars/observation.rb
CHANGED
@@ -52,5 +52,45 @@ module Boxcars
|
|
52
52
|
def self.err(note, **)
|
53
53
|
new(note:, status: :error, **)
|
54
54
|
end
|
55
|
+
|
56
|
+
# create a new Observation with user context
|
57
|
+
# @param note [String] The text to use for the observation
|
58
|
+
# @param user_context [Hash] User information (e.g., { id: 123, email: "user@example.com", role: "admin" })
|
59
|
+
# @param status [Symbol] :ok or :error
|
60
|
+
# @param added_context [Hash] Any additional context to add to the result
|
61
|
+
# @return [Boxcars::Observation] The observation
|
62
|
+
def self.with_user(note, user_context:, status: :ok, **)
|
63
|
+
new(note:, status:, user_context:, **)
|
64
|
+
end
|
65
|
+
|
66
|
+
# create a new Observation with user context and status :ok
|
67
|
+
# @param note [String] The text to use for the observation
|
68
|
+
# @param user_context [Hash] User information (e.g., { id: 123, email: "user@example.com", role: "admin" })
|
69
|
+
# @param added_context [Hash] Any additional context to add to the result
|
70
|
+
# @return [Boxcars::Observation] The observation
|
71
|
+
def self.ok_with_user(note, user_context:, **)
|
72
|
+
with_user(note, user_context:, status: :ok, **)
|
73
|
+
end
|
74
|
+
|
75
|
+
# create a new Observation with user context and status :error
|
76
|
+
# @param note [String] The text to use for the observation
|
77
|
+
# @param user_context [Hash] User information (e.g., { id: 123, email: "user@example.com", role: "admin" })
|
78
|
+
# @param added_context [Hash] Any additional context to add to the result
|
79
|
+
# @return [Boxcars::Observation] The observation
|
80
|
+
def self.err_with_user(note, user_context:, **)
|
81
|
+
with_user(note, user_context:, status: :error, **)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Extract user context from the observation
|
85
|
+
# @return [Hash, nil] The user context if present
|
86
|
+
def user_context
|
87
|
+
added_context[:user_context]
|
88
|
+
end
|
89
|
+
|
90
|
+
# Check if this observation has user context
|
91
|
+
# @return [Boolean] true if user context is present
|
92
|
+
def user_context?
|
93
|
+
!user_context.nil?
|
94
|
+
end
|
55
95
|
end
|
56
96
|
end
|
data/lib/boxcars/version.rb
CHANGED
data/lib/boxcars.rb
CHANGED
@@ -218,6 +218,7 @@ module Boxcars
|
|
218
218
|
end
|
219
219
|
|
220
220
|
require_relative "boxcars/version"
|
221
|
+
require_relative "boxcars/observation"
|
221
222
|
require_relative "boxcars/observability_backend"
|
222
223
|
require_relative "boxcars/observability"
|
223
224
|
# If users want it, they can require 'boxcars/observability_backends/multi_backend'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: boxcars
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Francis Sullivan
|
@@ -139,9 +139,9 @@ files:
|
|
139
139
|
- Gemfile
|
140
140
|
- Gemfile.lock
|
141
141
|
- LICENSE.txt
|
142
|
-
- POSTHOG_TEST_README.md
|
143
142
|
- README.md
|
144
143
|
- Rakefile
|
144
|
+
- USER_CONTEXT_GUIDE.md
|
145
145
|
- bin/console
|
146
146
|
- bin/setup
|
147
147
|
- boxcars.gemspec
|
data/POSTHOG_TEST_README.md
DELETED
@@ -1,118 +0,0 @@
|
|
1
|
-
# Boxcars Engines PostHog Observability Test
|
2
|
-
|
3
|
-
This test program demonstrates how to use the Boxcars library with PostHog observability backend to track AI engine usage.
|
4
|
-
|
5
|
-
## Prerequisites
|
6
|
-
|
7
|
-
1. **PostHog Ruby Gem**: Install the required gem
|
8
|
-
```bash
|
9
|
-
gem install posthog-ruby
|
10
|
-
```
|
11
|
-
|
12
|
-
2. **Environment Variables**: Ensure your `.env` file contains:
|
13
|
-
```
|
14
|
-
POSTHOG_API_KEY=your_posthog_project_api_key
|
15
|
-
POSTHOG_HOST=https://app.posthog.com # or your self-hosted instance
|
16
|
-
```
|
17
|
-
|
18
|
-
3. **AI Provider API Keys**: For testing different engines, you'll need:
|
19
|
-
```
|
20
|
-
openai_access_token=your_openai_token
|
21
|
-
GOOGLE_API_KEY=your_google_api_key
|
22
|
-
ANTHROPIC_API_KEY=your_anthropic_key
|
23
|
-
GROQ_API_KEY=your_groq_key
|
24
|
-
```
|
25
|
-
|
26
|
-
## Usage
|
27
|
-
|
28
|
-
### Method 1: IRB Interactive Session (Recommended)
|
29
|
-
|
30
|
-
Start IRB with the required dependencies:
|
31
|
-
|
32
|
-
```bash
|
33
|
-
irb -r dotenv/load -r boxcars -r debug -r boxcars/observability_backends/posthog_backend
|
34
|
-
```
|
35
|
-
|
36
|
-
Then in the IRB session:
|
37
|
-
|
38
|
-
```ruby
|
39
|
-
# Load and run the test
|
40
|
-
load 'test_engines_with_posthog.rb'
|
41
|
-
|
42
|
-
# Or set up PostHog backend manually:
|
43
|
-
Boxcars::Observability.backend = Boxcars::PosthogBackend.new(
|
44
|
-
api_key: ENV['POSTHOG_API_KEY'],
|
45
|
-
host: ENV['POSTHOG_HOST']
|
46
|
-
)
|
47
|
-
|
48
|
-
# Run manual tests
|
49
|
-
manual_test(model: 'gpt-4o', prompt: 'What is machine learning?')
|
50
|
-
manual_test(model: 'flash', prompt: 'Explain Ruby in one sentence')
|
51
|
-
manual_test(model: 'sonnet', prompt: 'Write a short poem about coding')
|
52
|
-
```
|
53
|
-
|
54
|
-
### Method 2: Direct Ruby Execution
|
55
|
-
|
56
|
-
```bash
|
57
|
-
ruby test_engines_with_posthog.rb
|
58
|
-
```
|
59
|
-
|
60
|
-
## What the Test Does
|
61
|
-
|
62
|
-
1. **Initializes PostHog Backend**: Sets up the PostHog observability backend with your API credentials
|
63
|
-
2. **Tests Multiple Engines**: Runs tests against various AI engines:
|
64
|
-
- Gemini Flash (Default)
|
65
|
-
- GPT-4o (OpenAI)
|
66
|
-
- Claude Sonnet (Anthropic)
|
67
|
-
- Groq Llama
|
68
|
-
3. **Tracks Observability Events**: Each API call generates PostHog events with AI-specific properties
|
69
|
-
4. **Provides Manual Testing**: Includes a `manual_test` function for interactive testing
|
70
|
-
|
71
|
-
## PostHog Events
|
72
|
-
|
73
|
-
The test will generate events in PostHog with properties like:
|
74
|
-
|
75
|
-
- `$ai_model`: The AI model used (e.g., "gpt-4o", "gemini-2.5-flash")
|
76
|
-
- `$ai_provider`: The provider (e.g., "openai", "google", "anthropic")
|
77
|
-
- `$ai_input_tokens`: Number of input tokens
|
78
|
-
- `$ai_output_tokens`: Number of output tokens
|
79
|
-
- `$ai_latency`: Response time in seconds
|
80
|
-
- `$ai_http_status`: HTTP status code
|
81
|
-
- `$ai_trace_id`: Unique trace identifier
|
82
|
-
- `$ai_is_error`: Boolean indicating if there was an error
|
83
|
-
|
84
|
-
## Viewing Results
|
85
|
-
|
86
|
-
After running the test:
|
87
|
-
|
88
|
-
1. Go to your PostHog dashboard
|
89
|
-
2. Navigate to Events or Live Events
|
90
|
-
3. Look for events with AI-related properties
|
91
|
-
4. You can create insights and dashboards to analyze AI usage patterns
|
92
|
-
|
93
|
-
## Troubleshooting
|
94
|
-
|
95
|
-
- **Missing PostHog gem**: Install with `gem install posthog-ruby`
|
96
|
-
- **Missing API keys**: Check your `.env` file has the required keys
|
97
|
-
- **Engine errors**: Some engines may fail if you don't have valid API keys for those providers
|
98
|
-
- **No events in PostHog**: Check your PostHog API key and host configuration
|
99
|
-
|
100
|
-
## Available Models
|
101
|
-
|
102
|
-
You can test with these model aliases:
|
103
|
-
|
104
|
-
- `flash` - Gemini 2.5 Flash
|
105
|
-
- `gpt-4o` - OpenAI GPT-4o
|
106
|
-
- `sonnet` - Claude Sonnet
|
107
|
-
- `groq` - Groq Llama
|
108
|
-
- `online` - Perplexity Sonar
|
109
|
-
- And many more (see `lib/boxcars/engines.rb`)
|
110
|
-
|
111
|
-
## Example Manual Tests
|
112
|
-
|
113
|
-
```ruby
|
114
|
-
# Test different models
|
115
|
-
manual_test(model: 'flash', prompt: 'Explain quantum computing')
|
116
|
-
manual_test(model: 'gpt-4o', prompt: 'Write a Python function to sort a list')
|
117
|
-
manual_test(model: 'sonnet', prompt: 'What are the benefits of functional programming?')
|
118
|
-
manual_test(model: 'groq', prompt: 'Describe the difference between AI and ML')
|