agentbill-sdk 2.0.1 → 7.17.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 +4 -4
- data/.github/workflows/ci.yml +5 -4
- data/CHANGELOG.md +67 -13
- data/README.md +4 -3
- data/SECURITY.md +2 -2
- data/examples/ollama_basic.rb +81 -0
- data/examples/openai_custom_event.rb +50 -0
- data/examples/perplexity_basic.rb +66 -0
- data/lib/agentbill/agents.rb +226 -0
- data/lib/agentbill/customers.rb +164 -0
- data/lib/agentbill/distributed.rb +109 -0
- data/lib/agentbill/exceptions.rb +84 -0
- data/lib/agentbill/ollama_wrapper.rb +153 -0
- data/lib/agentbill/orders.rb +283 -0
- data/lib/agentbill/perplexity_wrapper.rb +101 -0
- data/lib/agentbill/pricing.rb +52 -0
- data/lib/agentbill/signal_types.rb +179 -0
- data/lib/agentbill/signals.rb +199 -0
- data/lib/agentbill/tracer.rb +69 -12
- data/lib/agentbill/tracing.rb +343 -0
- data/lib/agentbill/version.rb +1 -1
- data/lib/agentbill/wrappers.rb +384 -0
- data/lib/agentbill.rb +288 -52
- metadata +17 -2
data/lib/agentbill.rb
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
require 'net/http'
|
|
2
2
|
require 'json'
|
|
3
3
|
require 'securerandom'
|
|
4
|
+
require 'digest'
|
|
4
5
|
require_relative 'agentbill/version'
|
|
5
6
|
require_relative 'agentbill/tracer'
|
|
7
|
+
require_relative 'agentbill/pricing'
|
|
8
|
+
require_relative 'agentbill/customers'
|
|
9
|
+
require_relative 'agentbill/agents'
|
|
10
|
+
require_relative 'agentbill/orders'
|
|
6
11
|
|
|
7
12
|
module AgentBill
|
|
8
13
|
class Client
|
|
9
|
-
attr_reader :config, :tracer
|
|
14
|
+
attr_reader :config, :tracer, :customers, :agents, :orders
|
|
10
15
|
|
|
11
16
|
def initialize(config)
|
|
12
17
|
@config = config
|
|
13
18
|
@tracer = Tracer.new(config)
|
|
19
|
+
@customers = CustomersResource.new(config)
|
|
20
|
+
@agents = AgentsResource.new(config)
|
|
21
|
+
@orders = OrdersResource.new(config)
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def self.init(config)
|
|
@@ -24,33 +32,23 @@ module AgentBill
|
|
|
24
32
|
[1, text.to_s.length / 4].max
|
|
25
33
|
end
|
|
26
34
|
|
|
27
|
-
def estimate_cost(model, input_tokens, output_tokens)
|
|
28
|
-
|
|
29
|
-
pricing = {
|
|
30
|
-
'gpt-4' => { input: 0.03, output: 0.06 },
|
|
31
|
-
'gpt-4o' => { input: 0.005, output: 0.015 },
|
|
32
|
-
'gpt-4o-mini' => { input: 0.00015, output: 0.0006 },
|
|
33
|
-
'claude-sonnet-4-5' => { input: 0.003, output: 0.015 },
|
|
34
|
-
'claude-opus-4-1-20250805' => { input: 0.015, output: 0.075 },
|
|
35
|
-
'claude-3-5-sonnet-20241022' => { input: 0.003, output: 0.015 },
|
|
36
|
-
'mistral-large-latest' => { input: 0.004, output: 0.012 },
|
|
37
|
-
'gemini-pro' => { input: 0.00025, output: 0.0005 }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
model_price = pricing[model] || { input: 0.001, output: 0.002 }
|
|
41
|
-
(input_tokens / 1000.0 * model_price[:input]) + (output_tokens / 1000.0 * model_price[:output])
|
|
35
|
+
def estimate_cost(model, input_tokens, output_tokens, provider = 'openai')
|
|
36
|
+
Pricing.calculate_cost(model, input_tokens, output_tokens, provider)
|
|
42
37
|
end
|
|
43
38
|
|
|
44
39
|
def validate_request(model, messages, estimated_output_tokens = 1000)
|
|
45
|
-
|
|
40
|
+
# Always validate when customer_id is present - backend will check DB policies
|
|
41
|
+
return { 'allowed' => true } unless @config[:customer_id]
|
|
46
42
|
|
|
47
|
-
uri = URI("#{@config[:base_url] || 'https://
|
|
43
|
+
uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/ai-cost-guard-router")
|
|
48
44
|
|
|
49
45
|
payload = {
|
|
50
46
|
api_key: @config[:api_key],
|
|
51
47
|
customer_id: @config[:customer_id],
|
|
52
48
|
model: model,
|
|
53
|
-
messages: messages
|
|
49
|
+
messages: messages,
|
|
50
|
+
daily_budget_override: @config[:daily_budget],
|
|
51
|
+
monthly_budget_override: @config[:monthly_budget]
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
begin
|
|
@@ -74,18 +72,42 @@ module AgentBill
|
|
|
74
72
|
end
|
|
75
73
|
end
|
|
76
74
|
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
# NOTE: track_usage is deprecated in v7.0.2 - signals now go through OTEL collector
|
|
76
|
+
def track_usage(model, provider, input_tokens, output_tokens, latency_ms, cost, event_name = 'ai_request', trace_id = nil, span_id = nil)
|
|
77
|
+
# v7.0.2: Route signals through otel-collector with proper span format
|
|
78
|
+
uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/otel-collector")
|
|
79
|
+
|
|
80
|
+
trace_id ||= SecureRandom.hex(16)
|
|
81
|
+
span_id ||= SecureRandom.hex(8)
|
|
82
|
+
now_ns = (Time.now.to_f * 1_000_000_000).to_i.to_s
|
|
79
83
|
|
|
80
84
|
payload = {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
resourceSpans: [{
|
|
86
|
+
resource: {
|
|
87
|
+
attributes: [
|
|
88
|
+
{ key: 'service.name', value: { stringValue: 'agentbill-ruby-sdk' } },
|
|
89
|
+
{ key: 'agentbill.customer_id', value: { stringValue: @config[:customer_id] || '' } }
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
scopeSpans: [{
|
|
93
|
+
scope: { name: 'agentbill.signals', version: '7.16.1' },
|
|
94
|
+
spans: [{
|
|
95
|
+
traceId: trace_id,
|
|
96
|
+
spanId: span_id,
|
|
97
|
+
name: 'agentbill.trace.signal',
|
|
98
|
+
kind: 1,
|
|
99
|
+
startTimeUnixNano: now_ns,
|
|
100
|
+
endTimeUnixNano: now_ns,
|
|
101
|
+
attributes: [
|
|
102
|
+
{ key: 'agentbill.event_name', value: { stringValue: event_name } },
|
|
103
|
+
{ key: 'agentbill.is_business_event', value: { boolValue: true } },
|
|
104
|
+
{ key: 'gen_ai.request.model', value: { stringValue: model } },
|
|
105
|
+
{ key: 'gen_ai.system', value: { stringValue: provider } }
|
|
106
|
+
],
|
|
107
|
+
status: { code: 1 }
|
|
108
|
+
}]
|
|
109
|
+
}]
|
|
110
|
+
}]
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
begin
|
|
@@ -95,12 +117,13 @@ module AgentBill
|
|
|
95
117
|
|
|
96
118
|
request = Net::HTTP::Post.new(uri.path)
|
|
97
119
|
request['Content-Type'] = 'application/json'
|
|
120
|
+
request['x-api-key'] = @config[:api_key]
|
|
98
121
|
request.body = payload.to_json
|
|
99
122
|
|
|
100
123
|
http.request(request)
|
|
101
|
-
puts "[AgentBill
|
|
124
|
+
puts "[AgentBill] Usage tracked via OTEL: $#{format('%.4f', cost)}" if @config[:debug]
|
|
102
125
|
rescue => e
|
|
103
|
-
puts "[AgentBill
|
|
126
|
+
puts "[AgentBill] Tracking failed: #{e.message}" if @config[:debug]
|
|
104
127
|
end
|
|
105
128
|
end
|
|
106
129
|
|
|
@@ -116,6 +139,10 @@ module AgentBill
|
|
|
116
139
|
messages = params[:messages] || []
|
|
117
140
|
max_tokens = params[:max_tokens] || params[:max_completion_tokens] || 1000
|
|
118
141
|
|
|
142
|
+
# Extract event_name from agentbill_options if provided (don't pass to OpenAI)
|
|
143
|
+
agentbill_options = params.delete(:agentbill_options) || {}
|
|
144
|
+
event_name = agentbill_options[:event_name] || 'ai_request'
|
|
145
|
+
|
|
119
146
|
# Phase 1: Validate budget BEFORE API call
|
|
120
147
|
validation = config[:_client].send(:validate_request, model, messages, max_tokens)
|
|
121
148
|
unless validation['allowed']
|
|
@@ -126,9 +153,59 @@ module AgentBill
|
|
|
126
153
|
raise error
|
|
127
154
|
end
|
|
128
155
|
|
|
156
|
+
# v7.6.10: Check for cached response from semantic cache
|
|
157
|
+
# CRITICAL FIX: Router sends cost_saved/tokens_saved at top level
|
|
158
|
+
if validation['cached'] && validation['response_data']
|
|
159
|
+
puts "[AgentBill] ✓ Cache hit - returning cached response" if config[:debug]
|
|
160
|
+
|
|
161
|
+
# v7.6.10 FIX: Check both nested 'cache'/'cache_info' AND top-level fields
|
|
162
|
+
cache_info = validation['cache'] || validation['cache_info'] || {}
|
|
163
|
+
cached_response = validation['response_data']
|
|
164
|
+
usage = cached_response['usage'] || {}
|
|
165
|
+
|
|
166
|
+
# v7.6.10 FIX: Extract from cache_info OR top-level validation
|
|
167
|
+
tokens_saved = cache_info['tokens_saved'] || validation['tokens_saved'] || usage['total_tokens'] || 0
|
|
168
|
+
cost_saved = cache_info['cost_saved'] || validation['cost_saved'] || 0
|
|
169
|
+
cached_input_tokens = usage['prompt_tokens'] || (tokens_saved * 0.2).to_i
|
|
170
|
+
cached_output_tokens = usage['completion_tokens'] || (tokens_saved - cached_input_tokens)
|
|
171
|
+
cached_total_tokens = tokens_saved > 0 ? tokens_saved : (cached_input_tokens + cached_output_tokens)
|
|
172
|
+
|
|
173
|
+
# v7.6.10 CRITICAL FIX: Set gen_ai.usage.* to 0 for cache hits
|
|
174
|
+
# This prevents OTEL ingestion from calculating cost for cached responses
|
|
175
|
+
span = tracer.start_span('openai.chat.completion', {
|
|
176
|
+
'gen_ai.system' => 'openai',
|
|
177
|
+
'gen_ai.request.model' => model,
|
|
178
|
+
'gen_ai.operation.name' => 'chat',
|
|
179
|
+
'model' => model,
|
|
180
|
+
'provider' => 'openai',
|
|
181
|
+
'agentbill.from_cache' => true,
|
|
182
|
+
'agentbill.cache_hit' => true,
|
|
183
|
+
# OTEL cost calculation attributes - set to 0 for cache hits
|
|
184
|
+
'gen_ai.usage.input_tokens' => 0,
|
|
185
|
+
'gen_ai.usage.output_tokens' => 0,
|
|
186
|
+
'gen_ai.usage.total_tokens' => 0,
|
|
187
|
+
# Informational attributes - actual cached token counts
|
|
188
|
+
'agentbill.cached_input_tokens' => cached_input_tokens,
|
|
189
|
+
'agentbill.cached_output_tokens' => cached_output_tokens,
|
|
190
|
+
'agentbill.cached_total_tokens' => cached_total_tokens,
|
|
191
|
+
'agentbill.tokens_saved' => tokens_saved,
|
|
192
|
+
'agentbill.cost_saved' => cost_saved
|
|
193
|
+
})
|
|
194
|
+
span.set_status(0)
|
|
195
|
+
span.finish
|
|
196
|
+
tracer.flush
|
|
197
|
+
|
|
198
|
+
return cached_response
|
|
199
|
+
end
|
|
200
|
+
|
|
129
201
|
# Phase 2: Execute AI call
|
|
130
202
|
start_time = Time.now
|
|
131
203
|
span = tracer.start_span('openai.chat.completion', {
|
|
204
|
+
# ✅ OTEL GenAI compliant attributes
|
|
205
|
+
'gen_ai.system' => 'openai',
|
|
206
|
+
'gen_ai.request.model' => model,
|
|
207
|
+
'gen_ai.operation.name' => 'chat',
|
|
208
|
+
# ⚠️ Backward compatibility
|
|
132
209
|
'model' => model,
|
|
133
210
|
'provider' => 'openai'
|
|
134
211
|
})
|
|
@@ -137,14 +214,19 @@ module AgentBill
|
|
|
137
214
|
response = original_method.call(params)
|
|
138
215
|
latency = ((Time.now - start_time) * 1000).round
|
|
139
216
|
|
|
140
|
-
#
|
|
217
|
+
# NOTE: wrap() only creates OTEL spans, NOT signals
|
|
218
|
+
# Signals should be created explicitly via track_signal()
|
|
141
219
|
input_tokens = response.dig(:usage, :prompt_tokens) || 0
|
|
142
220
|
output_tokens = response.dig(:usage, :completion_tokens) || 0
|
|
143
221
|
cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens)
|
|
144
222
|
|
|
145
|
-
config[:_client].send(:track_usage, model, 'openai', input_tokens, output_tokens, latency, cost)
|
|
146
|
-
|
|
147
223
|
span.set_attributes({
|
|
224
|
+
# ✅ OTEL GenAI compliant attributes
|
|
225
|
+
'gen_ai.usage.input_tokens' => input_tokens,
|
|
226
|
+
'gen_ai.usage.output_tokens' => output_tokens,
|
|
227
|
+
'gen_ai.usage.total_tokens' => response.dig(:usage, :total_tokens),
|
|
228
|
+
'gen_ai.response.id' => response.dig(:id),
|
|
229
|
+
# ⚠️ Backward compatibility
|
|
148
230
|
'response.prompt_tokens' => input_tokens,
|
|
149
231
|
'response.completion_tokens' => output_tokens,
|
|
150
232
|
'response.total_tokens' => response.dig(:usage, :total_tokens),
|
|
@@ -152,16 +234,24 @@ module AgentBill
|
|
|
152
234
|
})
|
|
153
235
|
span.set_status(0)
|
|
154
236
|
|
|
155
|
-
puts "[AgentBill
|
|
237
|
+
puts "[AgentBill] ✓ OTEL span created: $#{format('%.4f', cost)} (use track_signal() for signals)" if config[:debug]
|
|
238
|
+
|
|
239
|
+
# v7.5.0: Cache AI response for semantic caching
|
|
240
|
+
response_content = response.dig(:choices, 0, :message, :content) || ''
|
|
241
|
+
prompt_text = messages.map { |m| m[:content] }.join(' ')
|
|
242
|
+
prompt_hash = Digest::SHA256.hexdigest(prompt_text)[0..15]
|
|
243
|
+
config[:_client].send(:cache_response, model, prompt_hash, response_content, input_tokens + output_tokens, cost)
|
|
244
|
+
|
|
156
245
|
response
|
|
157
246
|
rescue => e
|
|
158
247
|
span.set_status(1, e.message)
|
|
159
248
|
raise
|
|
160
249
|
ensure
|
|
161
250
|
span.finish
|
|
251
|
+
# Auto-flush spans to prevent data loss
|
|
252
|
+
tracer.flush
|
|
162
253
|
end
|
|
163
254
|
end
|
|
164
|
-
|
|
165
255
|
# Store reference to self for helper methods
|
|
166
256
|
@config[:_client] = self
|
|
167
257
|
client
|
|
@@ -190,6 +280,11 @@ module AgentBill
|
|
|
190
280
|
# Phase 2: Execute AI call
|
|
191
281
|
start_time = Time.now
|
|
192
282
|
span = tracer.start_span('anthropic.message', {
|
|
283
|
+
# ✅ OTEL GenAI compliant attributes
|
|
284
|
+
'gen_ai.system' => 'anthropic',
|
|
285
|
+
'gen_ai.request.model' => model,
|
|
286
|
+
'gen_ai.operation.name' => 'chat',
|
|
287
|
+
# ⚠️ Backward compatibility
|
|
193
288
|
'model' => model,
|
|
194
289
|
'provider' => 'anthropic'
|
|
195
290
|
})
|
|
@@ -198,39 +293,45 @@ module AgentBill
|
|
|
198
293
|
response = original_method.call(params)
|
|
199
294
|
latency = ((Time.now - start_time) * 1000).round
|
|
200
295
|
|
|
201
|
-
#
|
|
296
|
+
# NOTE: wrap() only creates OTEL spans, NOT signals
|
|
202
297
|
input_tokens = response.dig(:usage, :input_tokens) || 0
|
|
203
298
|
output_tokens = response.dig(:usage, :output_tokens) || 0
|
|
204
|
-
cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens)
|
|
205
|
-
|
|
206
|
-
config[:_client].send(:track_usage, model, 'anthropic', input_tokens, output_tokens, latency, cost)
|
|
299
|
+
cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens, 'anthropic')
|
|
207
300
|
|
|
208
301
|
span.set_attributes({
|
|
302
|
+
# ✅ OTEL GenAI compliant attributes
|
|
303
|
+
'gen_ai.usage.input_tokens' => input_tokens,
|
|
304
|
+
'gen_ai.usage.output_tokens' => output_tokens,
|
|
305
|
+
'gen_ai.response.id' => response.dig(:id),
|
|
306
|
+
# ⚠️ Backward compatibility
|
|
209
307
|
'response.input_tokens' => input_tokens,
|
|
210
308
|
'response.output_tokens' => output_tokens,
|
|
211
309
|
'latency_ms' => latency
|
|
212
310
|
})
|
|
213
311
|
span.set_status(0)
|
|
214
312
|
|
|
215
|
-
puts "[AgentBill
|
|
313
|
+
puts "[AgentBill] ✓ OTEL span created: $#{format('%.4f', cost)} (use track_signal() for signals)" if config[:debug]
|
|
216
314
|
response
|
|
217
315
|
rescue => e
|
|
218
316
|
span.set_status(1, e.message)
|
|
219
317
|
raise
|
|
220
318
|
ensure
|
|
221
319
|
span.finish
|
|
320
|
+
# Auto-flush spans
|
|
321
|
+
tracer.flush
|
|
222
322
|
end
|
|
223
323
|
end
|
|
224
|
-
|
|
225
324
|
# Store reference to self for helper methods
|
|
226
325
|
@config[:_client] = self
|
|
227
326
|
client
|
|
228
327
|
end
|
|
229
328
|
|
|
230
|
-
# Track a comprehensive signal with all 68 parameters
|
|
329
|
+
# Track a comprehensive signal with all 68 parameters including trace_id and span_id
|
|
231
330
|
#
|
|
232
|
-
# Supports
|
|
331
|
+
# Supports optional trace_id and span_id for OTEL correlation:
|
|
233
332
|
# - event_name (required)
|
|
333
|
+
# - trace_id (optional) - For correlating with OTEL spans for cost reconciliation
|
|
334
|
+
# - span_id (optional) - For correlating with OTEL spans for cost reconciliation
|
|
234
335
|
# - data_source, timestamp
|
|
235
336
|
# - agent_external_id, customer_external_id, account_external_id, user_external_id,
|
|
236
337
|
# order_external_id, session_id, conversation_id, thread_id
|
|
@@ -243,26 +344,46 @@ module AgentBill
|
|
|
243
344
|
# - feedback_score, user_satisfaction, error_type, error_message, retry_count, success_rate
|
|
244
345
|
# - tags, category, priority, severity, compliance_flag, data_classification
|
|
245
346
|
# - product_id, feature_flag, environment, deployment_version, region, tenant_id
|
|
246
|
-
# - parent_span_id
|
|
347
|
+
# - parent_span_id
|
|
247
348
|
# - custom_dimensions, metadata, data
|
|
248
349
|
#
|
|
249
350
|
# Example:
|
|
351
|
+
# # Basic tracking
|
|
250
352
|
# agentbill.track_signal(
|
|
251
353
|
# event_name: "user_conversion",
|
|
252
354
|
# revenue: 99.99,
|
|
253
|
-
# customer_external_id: "cust_123"
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
355
|
+
# customer_external_id: "cust_123"
|
|
356
|
+
# )
|
|
357
|
+
#
|
|
358
|
+
# # With OTEL correlation
|
|
359
|
+
# span = agentbill.tracer.start_span('ai_completion')
|
|
360
|
+
# # ... make AI call ...
|
|
361
|
+
# agentbill.track_signal(
|
|
362
|
+
# event_name: "ai_request",
|
|
363
|
+
# revenue: 5.00,
|
|
364
|
+
# trace_id: span.trace_id, # Optional
|
|
365
|
+
# span_id: span.span_id # Optional
|
|
257
366
|
# )
|
|
258
367
|
def track_signal(**params)
|
|
259
368
|
raise ArgumentError, "event_name is required" unless params[:event_name]
|
|
260
369
|
|
|
261
|
-
uri = URI("#{@config[:base_url] || 'https://
|
|
370
|
+
uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/record-signals")
|
|
262
371
|
|
|
263
372
|
# Add timestamp if not provided
|
|
264
373
|
params[:timestamp] ||= Time.now.to_f
|
|
265
374
|
|
|
375
|
+
# Auto-fill customer_id or customer_external_id from config if not provided
|
|
376
|
+
if !params[:customer_id] && !params[:customer_external_id] && @config[:customer_id]
|
|
377
|
+
# Check if it's a UUID format - send as customer_id, else customer_external_id
|
|
378
|
+
uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
379
|
+
is_uuid = @config[:customer_id].match?(uuid_regex)
|
|
380
|
+
if is_uuid
|
|
381
|
+
params[:customer_id] = @config[:customer_id]
|
|
382
|
+
else
|
|
383
|
+
params[:customer_external_id] = @config[:customer_id]
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
266
387
|
# Remove nil values
|
|
267
388
|
payload = params.reject { |_, v| v.nil? }
|
|
268
389
|
|
|
@@ -271,14 +392,15 @@ module AgentBill
|
|
|
271
392
|
http.use_ssl = true
|
|
272
393
|
|
|
273
394
|
request = Net::HTTP::Post.new(uri.path)
|
|
274
|
-
request['
|
|
395
|
+
request['X-API-Key'] = @config[:api_key]
|
|
275
396
|
request['Content-Type'] = 'application/json'
|
|
276
397
|
request.body = payload.to_json
|
|
277
398
|
|
|
278
399
|
response = http.request(request)
|
|
279
400
|
|
|
280
401
|
if @config[:debug]
|
|
281
|
-
|
|
402
|
+
trace_info = params[:trace_id] ? " (trace: #{params[:trace_id]})" : ""
|
|
403
|
+
puts "[AgentBill] Signal tracked: #{params[:event_name]}#{trace_info}"
|
|
282
404
|
end
|
|
283
405
|
|
|
284
406
|
response.code == '200'
|
|
@@ -290,8 +412,122 @@ module AgentBill
|
|
|
290
412
|
end
|
|
291
413
|
end
|
|
292
414
|
|
|
415
|
+
# Track a conversion event for prompt profitability analysis
|
|
416
|
+
# Links conversions to AI prompts to calculate ROI per prompt
|
|
417
|
+
#
|
|
418
|
+
# @param event_type [String] Type of conversion (e.g., 'purchase', 'signup', 'trial_start')
|
|
419
|
+
# @param event_value [Float] Revenue amount from the conversion
|
|
420
|
+
# @param signal_id [String, nil] Optional UUID linking to specific AI signal/prompt
|
|
421
|
+
# @param session_id [String, nil] Optional session identifier
|
|
422
|
+
# @param attribution_window_hours [Integer] Time window for attribution (default: 24 hours)
|
|
423
|
+
# @param currency [String] Currency code (default: 'USD')
|
|
424
|
+
# @param metadata [Hash, nil] Optional additional data
|
|
425
|
+
# @return [Hash] Response with success status and conversion_id
|
|
426
|
+
#
|
|
427
|
+
# Example:
|
|
428
|
+
# # Track a purchase conversion
|
|
429
|
+
# result = agentbill.track_conversion(
|
|
430
|
+
# event_type: 'purchase',
|
|
431
|
+
# event_value: 99.99,
|
|
432
|
+
# currency: 'USD'
|
|
433
|
+
# )
|
|
434
|
+
#
|
|
435
|
+
# # Link conversion to specific AI prompt
|
|
436
|
+
# result = agentbill.track_conversion(
|
|
437
|
+
# event_type: 'trial_signup',
|
|
438
|
+
# event_value: 29.99,
|
|
439
|
+
# signal_id: 'signal-uuid-from-prompt',
|
|
440
|
+
# session_id: 'session-123'
|
|
441
|
+
# )
|
|
442
|
+
def track_conversion(event_type:, event_value:, signal_id: nil, session_id: nil, attribution_window_hours: 24, currency: 'USD', metadata: nil)
|
|
443
|
+
uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/track-conversion")
|
|
444
|
+
|
|
445
|
+
payload = {
|
|
446
|
+
api_key: @config[:api_key],
|
|
447
|
+
customer_id: @config[:customer_id],
|
|
448
|
+
event_type: event_type,
|
|
449
|
+
event_value: event_value,
|
|
450
|
+
signal_id: signal_id,
|
|
451
|
+
session_id: session_id,
|
|
452
|
+
attribution_window_hours: attribution_window_hours,
|
|
453
|
+
currency: currency,
|
|
454
|
+
metadata: metadata
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
begin
|
|
458
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
459
|
+
http.use_ssl = true
|
|
460
|
+
|
|
461
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
462
|
+
request['Content-Type'] = 'application/json'
|
|
463
|
+
request.body = payload.to_json
|
|
464
|
+
|
|
465
|
+
response = http.request(request)
|
|
466
|
+
data = JSON.parse(response.body)
|
|
467
|
+
|
|
468
|
+
if response.code != '200'
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
error: data['error'] || "HTTP #{response.code}"
|
|
472
|
+
}
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
if @config[:debug]
|
|
476
|
+
puts "[AgentBill] Conversion tracked: #{event_type} = $#{event_value} (ID: #{data['conversion_id']})"
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
{
|
|
480
|
+
success: true,
|
|
481
|
+
conversion_id: data['conversion_id']
|
|
482
|
+
}
|
|
483
|
+
rescue => e
|
|
484
|
+
if @config[:debug]
|
|
485
|
+
puts "[AgentBill] Failed to track conversion: #{e.message}"
|
|
486
|
+
end
|
|
487
|
+
{
|
|
488
|
+
success: false,
|
|
489
|
+
error: e.message
|
|
490
|
+
}
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
293
494
|
def flush
|
|
294
495
|
@tracer.flush
|
|
295
496
|
end
|
|
497
|
+
|
|
498
|
+
private
|
|
499
|
+
|
|
500
|
+
# Cache AI response for semantic caching (v7.5.0)
|
|
501
|
+
def cache_response(model, prompt_hash, response_content, tokens_used = 0, cost = 0.0)
|
|
502
|
+
return if response_content.nil? || response_content.empty?
|
|
503
|
+
|
|
504
|
+
uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/cache-ai-response")
|
|
505
|
+
|
|
506
|
+
payload = {
|
|
507
|
+
api_key: @config[:api_key],
|
|
508
|
+
prompt_hash: prompt_hash,
|
|
509
|
+
response_content: response_content,
|
|
510
|
+
model: model,
|
|
511
|
+
tokens_used: tokens_used,
|
|
512
|
+
cost: cost,
|
|
513
|
+
customer_id: @config[:customer_id],
|
|
514
|
+
agent_id: @config[:agent_id]
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
begin
|
|
518
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
519
|
+
http.use_ssl = true
|
|
520
|
+
http.read_timeout = 5
|
|
521
|
+
|
|
522
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
523
|
+
request['Content-Type'] = 'application/json'
|
|
524
|
+
request.body = payload.to_json
|
|
525
|
+
|
|
526
|
+
http.request(request)
|
|
527
|
+
puts "[AgentBill] Response cached for semantic caching" if @config[:debug]
|
|
528
|
+
rescue => e
|
|
529
|
+
puts "[AgentBill] Cache response failed: #{e.message}" if @config[:debug]
|
|
530
|
+
end
|
|
531
|
+
end
|
|
296
532
|
end
|
|
297
533
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agentbill-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 7.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AgentBill
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -65,11 +65,26 @@ files:
|
|
|
65
65
|
- agentbill.gemspec
|
|
66
66
|
- examples/anthropic_basic.rb
|
|
67
67
|
- examples/manual_otel_tracking.rb
|
|
68
|
+
- examples/ollama_basic.rb
|
|
68
69
|
- examples/openai_basic.rb
|
|
70
|
+
- examples/openai_custom_event.rb
|
|
71
|
+
- examples/perplexity_basic.rb
|
|
69
72
|
- examples/zero_config.rb
|
|
70
73
|
- lib/agentbill.rb
|
|
74
|
+
- lib/agentbill/agents.rb
|
|
75
|
+
- lib/agentbill/customers.rb
|
|
76
|
+
- lib/agentbill/distributed.rb
|
|
77
|
+
- lib/agentbill/exceptions.rb
|
|
78
|
+
- lib/agentbill/ollama_wrapper.rb
|
|
79
|
+
- lib/agentbill/orders.rb
|
|
80
|
+
- lib/agentbill/perplexity_wrapper.rb
|
|
81
|
+
- lib/agentbill/pricing.rb
|
|
82
|
+
- lib/agentbill/signal_types.rb
|
|
83
|
+
- lib/agentbill/signals.rb
|
|
71
84
|
- lib/agentbill/tracer.rb
|
|
85
|
+
- lib/agentbill/tracing.rb
|
|
72
86
|
- lib/agentbill/version.rb
|
|
87
|
+
- lib/agentbill/wrappers.rb
|
|
73
88
|
homepage: https://github.com/Agent-Bill/Ruby
|
|
74
89
|
licenses:
|
|
75
90
|
- MIT
|