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.
@@ -0,0 +1,283 @@
1
+ # AgentBill Orders Resource - CRUD operations with line items
2
+ #
3
+ # IMPORTANT: At least one agent must be associated with an order via
4
+ # agent_ids or agent_external_ids. Orders cannot be created without agents.
5
+ #
6
+ # Example:
7
+ # ab = AgentBill::Client.init(api_key: 'agb_...')
8
+ #
9
+ # # List orders
10
+ # result = ab.orders.list(limit: 10)
11
+ #
12
+ # # Create order (agent_ids or agent_external_ids REQUIRED)
13
+ # order = ab.orders.create(
14
+ # account_id: 'acct-123',
15
+ # name: 'Enterprise Q1',
16
+ # agent_ids: ['agent-uuid-1'] # REQUIRED
17
+ # )
18
+ #
19
+ # # Add line item
20
+ # ab.orders.add_line_item(
21
+ # order_id: order['id'],
22
+ # description: 'AI Usage',
23
+ # quantity: 1000,
24
+ # unit_price: 0.05,
25
+ # item_type: 'usage'
26
+ # )
27
+ #
28
+ # # Get order by ID
29
+ # order = ab.orders.get('order-123')
30
+ #
31
+ # # Update order
32
+ # order = ab.orders.update('order-123', status: 'active')
33
+ #
34
+ # # Delete order
35
+ # ab.orders.delete('order-123')
36
+
37
+ require 'net/http'
38
+ require 'json'
39
+
40
+ module AgentBill
41
+ # Valid item_type values for add_line_item:
42
+ # - usage: Variable consumption billing (API calls, tokens, requests)
43
+ # - platform_fee: Platform access and service charges
44
+ # - setup_fee: One-time onboarding and implementation costs
45
+ # - base_fee: Fixed recurring monthly/annual charges
46
+ # - overage: Usage exceeding included plan amounts
47
+ # - custom: Flexible billing for unique scenarios
48
+ # - flat_rate: Fixed-price service packages or retainers
49
+ # - product: Physical or digital product sales
50
+ # - service: Professional services and consulting
51
+ # - subscription: Recurring subscription billing
52
+ VALID_ITEM_TYPES = %w[usage platform_fee setup_fee base_fee overage custom product service subscription flat_rate].freeze
53
+
54
+ class OrdersResource
55
+ def initialize(config)
56
+ @config = config
57
+ @base_url = config[:base_url] || 'https://api.agentbill.io'
58
+ @api_key = config[:api_key]
59
+ @debug = config[:debug] || false
60
+ end
61
+
62
+ # List orders with pagination and filtering
63
+ #
64
+ # @param account_id [String, nil] Filter by account ID
65
+ # @param status [String, nil] Filter by status (draft, active, completed, cancelled)
66
+ # @param limit [Integer] Number of orders to return (default: 50)
67
+ # @param offset [Integer] Offset for pagination (default: 0)
68
+ # @param search [String, nil] Search term to filter by name or external_id
69
+ # @return [Hash] Hash with 'data' (list of orders) and 'pagination' info
70
+ def list(account_id: nil, status: nil, limit: 50, offset: 0, search: nil)
71
+ uri = URI("#{@base_url}/functions/v1/api-orders")
72
+ params = { limit: limit.to_s, offset: offset.to_s }
73
+ params[:search] = search if search
74
+ params[:account_id] = account_id if account_id
75
+ params[:status] = status if status
76
+ uri.query = URI.encode_www_form(params)
77
+
78
+ response = make_request(:get, uri)
79
+ handle_response(response, 'list orders')
80
+ end
81
+
82
+ # Create a new order
83
+ #
84
+ # IMPORTANT: At least one agent must be specified via agent_ids or agent_external_ids.
85
+ # Orders cannot be created without agent association for proper billing attribution.
86
+ #
87
+ # @param account_id [String] Account (customer) UUID (required unless account_external_id provided)
88
+ # @param name [String] Order name (required)
89
+ # @param agent_ids [Array<String>, nil] List of agent UUIDs (required if agent_external_ids not provided)
90
+ # @param agent_external_ids [Array<String>, nil] List of agent external IDs (required if agent_ids not provided)
91
+ # @param external_id [String, nil] Your external ID for this order
92
+ # @param account_external_id [String, nil] External ID of the account
93
+ # @param billing_contact_id [String, nil] Contact UUID for billing
94
+ # @param billing_contact_external_id [String, nil] External ID of billing contact
95
+ # @param start_date [String, nil] Order start date (ISO format)
96
+ # @param end_date [String, nil] Order end date (ISO format)
97
+ # @param status [String, nil] Order status (draft, active, completed, cancelled)
98
+ # @param currency [String, nil] Currency code (default: USD)
99
+ # @param metadata [Hash, nil] Additional metadata
100
+ # @return [Hash] Created order object
101
+ # @raise [ArgumentError] If neither agent_ids nor agent_external_ids provided
102
+ def create(account_id:, name:, agent_ids: nil, agent_external_ids: nil, external_id: nil,
103
+ account_external_id: nil, billing_contact_id: nil, billing_contact_external_id: nil,
104
+ start_date: nil, end_date: nil, status: nil, currency: nil, metadata: nil)
105
+ # Validate agent requirement
106
+ if (agent_ids.nil? || agent_ids.empty?) && (agent_external_ids.nil? || agent_external_ids.empty?)
107
+ raise ArgumentError, 'At least one agent must be specified via agent_ids or agent_external_ids. ' \
108
+ 'Orders cannot be created without agent association for billing attribution.'
109
+ end
110
+
111
+ uri = URI("#{@base_url}/functions/v1/api-orders")
112
+
113
+ payload = { account_id: account_id, name: name }
114
+ payload[:agent_ids] = agent_ids if agent_ids&.any?
115
+ payload[:agent_external_ids] = agent_external_ids if agent_external_ids&.any?
116
+ payload[:external_id] = external_id if external_id
117
+ payload[:account_external_id] = account_external_id if account_external_id
118
+ payload[:billing_contact_id] = billing_contact_id if billing_contact_id
119
+ payload[:billing_contact_external_id] = billing_contact_external_id if billing_contact_external_id
120
+ payload[:start_date] = start_date if start_date
121
+ payload[:end_date] = end_date if end_date
122
+ payload[:status] = status if status
123
+ payload[:currency] = currency if currency
124
+ payload[:metadata] = metadata if metadata
125
+
126
+ response = make_request(:post, uri, payload)
127
+ handle_response(response, 'create order')
128
+ end
129
+
130
+ # Get an order by ID or external_id (includes line items)
131
+ #
132
+ # @param order_id [String] Order UUID or external_id
133
+ # @param by_external_id [Boolean] If true, treat order_id as external_id
134
+ # @return [Hash] Order object with order_line_items
135
+ def get(order_id, by_external_id: false)
136
+ uri = URI("#{@base_url}/functions/v1/api-orders")
137
+ params = by_external_id ? { external_id: order_id } : { id: order_id }
138
+ uri.query = URI.encode_www_form(params)
139
+
140
+ response = make_request(:get, uri)
141
+ handle_response(response, 'get order')
142
+ end
143
+
144
+ # Update an order
145
+ #
146
+ # @param order_id [String] Order UUID
147
+ # @param name [String, nil] New name
148
+ # @param billing_contact_id [String, nil] New billing contact ID
149
+ # @param start_date [String, nil] New start date
150
+ # @param end_date [String, nil] New end date
151
+ # @param status [String, nil] New status
152
+ # @param currency [String, nil] New currency
153
+ # @param metadata [Hash, nil] New metadata (replaces existing)
154
+ # @return [Hash] Updated order object
155
+ def update(order_id, name: nil, billing_contact_id: nil, start_date: nil,
156
+ end_date: nil, status: nil, currency: nil, metadata: nil)
157
+ uri = URI("#{@base_url}/functions/v1/api-orders")
158
+
159
+ payload = { id: order_id }
160
+ payload[:name] = name if name
161
+ payload[:billing_contact_id] = billing_contact_id if billing_contact_id
162
+ payload[:start_date] = start_date if start_date
163
+ payload[:end_date] = end_date if end_date
164
+ payload[:status] = status if status
165
+ payload[:currency] = currency if currency
166
+ payload[:metadata] = metadata if metadata
167
+
168
+ response = make_request(:patch, uri, payload)
169
+ handle_response(response, 'update order')
170
+ end
171
+
172
+ # Delete an order and its line items
173
+ #
174
+ # @param order_id [String] Order UUID to delete
175
+ def delete(order_id)
176
+ uri = URI("#{@base_url}/functions/v1/api-orders")
177
+ payload = { id: order_id }
178
+
179
+ response = make_request(:delete, uri, payload)
180
+ handle_response(response, 'delete order')
181
+ end
182
+
183
+ # Add a line item to an order
184
+ #
185
+ # @param order_id [String] Order UUID (required)
186
+ # @param description [String] Line item description (required)
187
+ # @param quantity [Numeric] Quantity (required)
188
+ # @param unit_price [Numeric] Price per unit (required)
189
+ # @param product_id [String, nil] Product reference ID
190
+ # @param product_name [String, nil] Product name
191
+ # @param product_sku [String, nil] Product SKU
192
+ # @param usage_metric [String, nil] Usage metric name
193
+ # @param usage_quantity [Numeric, nil] Usage quantity
194
+ # @param discount_type [String, nil] "percentage" or "fixed"
195
+ # @param discount_value [Numeric, nil] Discount amount/percentage
196
+ # @param tax_rate [Numeric, nil] Tax rate percentage
197
+ # @param notes [String, nil] Additional notes
198
+ # @param sort_order [Integer, nil] Display order
199
+ # @param agent_id [String, nil] Associated agent ID
200
+ # @param item_type [String, nil] Type: usage, platform_fee, setup_fee, base_fee, overage, custom, product, service, subscription
201
+ # @param metadata [Hash, nil] Additional metadata
202
+ # @return [Hash] Created line item object
203
+ def add_line_item(order_id:, description:, quantity:, unit_price:, product_id: nil,
204
+ product_name: nil, product_sku: nil, usage_metric: nil, usage_quantity: nil,
205
+ discount_type: nil, discount_value: nil, tax_rate: nil, notes: nil,
206
+ sort_order: nil, agent_id: nil, item_type: nil, metadata: nil)
207
+ uri = URI("#{@base_url}/functions/v1/api-orders?action=line-items")
208
+
209
+ payload = {
210
+ order_id: order_id,
211
+ description: description,
212
+ quantity: quantity,
213
+ unit_price: unit_price
214
+ }
215
+ payload[:product_id] = product_id if product_id
216
+ payload[:product_name] = product_name if product_name
217
+ payload[:product_sku] = product_sku if product_sku
218
+ payload[:usage_metric] = usage_metric if usage_metric
219
+ payload[:usage_quantity] = usage_quantity if usage_quantity
220
+ payload[:discount_type] = discount_type if discount_type
221
+ payload[:discount_value] = discount_value if discount_value
222
+ payload[:tax_rate] = tax_rate if tax_rate
223
+ payload[:notes] = notes if notes
224
+ payload[:sort_order] = sort_order if sort_order
225
+ payload[:agent_id] = agent_id if agent_id
226
+ payload[:item_type] = item_type if item_type
227
+ payload[:metadata] = metadata if metadata
228
+
229
+ response = make_request(:post, uri, payload)
230
+ handle_response(response, 'add line item')
231
+ end
232
+
233
+ # Remove a line item from an order
234
+ #
235
+ # @param line_item_id [String] Line item UUID to delete
236
+ def remove_line_item(line_item_id)
237
+ uri = URI("#{@base_url}/functions/v1/api-orders?action=line-items")
238
+ payload = { id: line_item_id }
239
+
240
+ response = make_request(:delete, uri, payload)
241
+ handle_response(response, 'remove line item')
242
+ end
243
+
244
+ private
245
+
246
+ def make_request(method, uri, payload = nil)
247
+ http = Net::HTTP.new(uri.host, uri.port)
248
+ http.use_ssl = true
249
+ http.read_timeout = 30
250
+
251
+ case method
252
+ when :get
253
+ request = Net::HTTP::Get.new(uri)
254
+ when :post
255
+ request = Net::HTTP::Post.new(uri.request_uri)
256
+ request.body = payload.to_json if payload
257
+ when :patch
258
+ request = Net::HTTP::Patch.new(uri.path)
259
+ request.body = payload.to_json if payload
260
+ when :delete
261
+ request = Net::HTTP::Delete.new(uri.request_uri)
262
+ request.body = payload.to_json if payload
263
+ end
264
+
265
+ request['Content-Type'] = 'application/json'
266
+ request['X-API-Key'] = @api_key
267
+
268
+ http.request(request)
269
+ end
270
+
271
+ def handle_response(response, operation)
272
+ body = response.body ? JSON.parse(response.body) : {}
273
+
274
+ unless %w[200 201 204].include?(response.code)
275
+ error_msg = body['error'] || "Failed to #{operation}"
276
+ raise error_msg
277
+ end
278
+
279
+ puts "[AgentBill] #{operation} successful" if @debug
280
+ body
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,101 @@
1
+ # Perplexity AI Wrapper for AgentBill Ruby SDK
2
+ require_relative 'pricing'
3
+
4
+ module AgentBill
5
+ class PerplexityWrapper
6
+ def initialize(client, config, tracer)
7
+ @client = client
8
+ @config = config
9
+ @tracer = tracer
10
+ end
11
+
12
+ def wrap
13
+ original_chat_create = @client.method(:chat_create)
14
+ config = @config
15
+ tracer = @tracer
16
+
17
+ @client.define_singleton_method(:chat_create) do |params|
18
+ model = params[:model] || 'llama-3.1-sonar-small-128k-online'
19
+ messages = params[:messages] || []
20
+
21
+ start_time = Time.now
22
+ span = tracer.start_span('perplexity.chat.completions.create', {
23
+ # ✅ OTEL GenAI compliant attributes
24
+ 'gen_ai.system' => 'perplexity',
25
+ 'gen_ai.request.model' => model,
26
+ 'gen_ai.operation.name' => 'chat',
27
+ # ⚠️ Backward compatibility
28
+ 'model' => model,
29
+ 'provider' => 'perplexity'
30
+ })
31
+
32
+ begin
33
+ response = original_chat_create.call(params)
34
+ latency = ((Time.now - start_time) * 1000).round
35
+
36
+ input_tokens = response.dig(:usage, :prompt_tokens) || 0
37
+ output_tokens = response.dig(:usage, :completion_tokens) || 0
38
+ cost = Pricing.calculate_cost(model, input_tokens, output_tokens, 'perplexity')
39
+
40
+ track_usage(model, 'perplexity', input_tokens, output_tokens, latency, cost, config)
41
+
42
+ span.set_attributes({
43
+ # ✅ OTEL GenAI compliant attributes
44
+ 'gen_ai.usage.input_tokens' => input_tokens,
45
+ 'gen_ai.usage.output_tokens' => output_tokens,
46
+ 'gen_ai.response.id' => response.dig(:id),
47
+ # ⚠️ Backward compatibility
48
+ 'response.prompt_tokens' => input_tokens,
49
+ 'response.completion_tokens' => output_tokens,
50
+ 'latency_ms' => latency,
51
+ 'cost' => cost
52
+ })
53
+ span.set_status(0)
54
+
55
+ puts "[AgentBill] ✓ Perplexity chat tracked: $#{format('%.6f', cost)}" if config[:debug]
56
+ response
57
+ rescue => e
58
+ span.set_status(1, e.message)
59
+ raise
60
+ ensure
61
+ span.finish
62
+ end
63
+ end
64
+
65
+ @client
66
+ end
67
+
68
+ private
69
+
70
+ def track_usage(model, provider, input_tokens, output_tokens, latency, cost, config)
71
+ uri = URI("#{config[:base_url] || 'https://api.agentbill.io'}/functions/v1/track-ai-usage")
72
+
73
+ payload = {
74
+ api_key: config[:api_key],
75
+ customer_id: config[:customer_id],
76
+ agent_id: config[:agent_id],
77
+ event_name: 'ai_request',
78
+ model: model,
79
+ provider: provider,
80
+ prompt_tokens: input_tokens,
81
+ completion_tokens: output_tokens,
82
+ latency_ms: latency,
83
+ cost: cost
84
+ }
85
+
86
+ begin
87
+ http = Net::HTTP.new(uri.host, uri.port)
88
+ http.use_ssl = true
89
+ http.read_timeout = 10
90
+
91
+ request = Net::HTTP::Post.new(uri.path)
92
+ request['Content-Type'] = 'application/json'
93
+ request.body = payload.to_json
94
+
95
+ http.request(request)
96
+ rescue => e
97
+ puts "[AgentBill] Tracking failed: #{e.message}" if config[:debug]
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,52 @@
1
+ # AgentBill SDK Pricing - LOCAL ESTIMATION ONLY
2
+ #
3
+ # IMPORTANT: Cost calculation is now 100% SERVER-SIDE
4
+ # The server uses the model_pricing database table (synced by sync-model-pricing).
5
+ # This module is kept ONLY for local estimation/display purposes.
6
+ # The AUTHORITATIVE cost is always calculated server-side.
7
+
8
+ module AgentBill
9
+ module Pricing
10
+ # Default fallback rates for local estimation only
11
+ DEFAULT_INPUT_PER_1M = 1.0
12
+ DEFAULT_OUTPUT_PER_1M = 2.0
13
+
14
+ class << self
15
+ # LOCAL ESTIMATION ONLY - for display purposes.
16
+ # Actual cost is calculated server-side using model_pricing database.
17
+ def calculate_cost(model, input_tokens, output_tokens, provider = 'openai')
18
+ # Ollama is free
19
+ return 0.0 if provider == 'ollama' || model.to_s.start_with?('ollama/')
20
+
21
+ # Simple default estimate - server calculates actual cost
22
+ input_cost = (input_tokens.to_f / 1_000_000) * DEFAULT_INPUT_PER_1M
23
+ output_cost = (output_tokens.to_f / 1_000_000) * DEFAULT_OUTPUT_PER_1M
24
+ input_cost + output_cost
25
+ end
26
+
27
+ # LOCAL ESTIMATION ONLY for image generation
28
+ def calculate_image_cost(model, size, quality = 'standard')
29
+ if model.include?('dall-e-3')
30
+ quality == 'hd' ? 0.08 : 0.04
31
+ else
32
+ 0.02
33
+ end
34
+ end
35
+
36
+ # LOCAL ESTIMATION ONLY for audio
37
+ def calculate_audio_cost(model, duration_seconds: 0, chars: 0)
38
+ if model == 'whisper-1'
39
+ (duration_seconds.to_f / 60.0) * 0.006
40
+ elsif model.start_with?('tts-')
41
+ (chars.to_f / 1_000_000) * 15.0
42
+ else
43
+ 0.0
44
+ end
45
+ end
46
+
47
+ def embedding_model?(model)
48
+ model.downcase.include?('embedding') || model.downcase.include?('embed')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module AgentBill
8
+ # Signal Types Resource - CRUD operations for signal type management
9
+ # v7.15.0: New resource for programmatic signal type management
10
+ #
11
+ # @example
12
+ # client = AgentBill::Client.new(api_key: 'agb_...')
13
+ #
14
+ # # Create signal type
15
+ # st = client.signal_types.create(name: 'ai_completion', category: 'activity')
16
+ #
17
+ # # List all
18
+ # signal_types = client.signal_types.list
19
+ #
20
+ # # Get by ID
21
+ # st = client.signal_types.get(signal_type_id)
22
+ #
23
+ # # Update
24
+ # client.signal_types.update(signal_type_id, description: 'Updated')
25
+ #
26
+ # # Delete
27
+ # client.signal_types.delete(signal_type_id)
28
+ #
29
+ class SignalTypesResource
30
+ def initialize(config)
31
+ @base_url = config[:base_url] || 'https://api.agentbill.io'
32
+ @api_key = config[:api_key]
33
+ @debug = config[:debug] || false
34
+ end
35
+
36
+ # List signal types with optional filters
37
+ #
38
+ # @param category [String, nil] Filter by category ('activity' or 'outcome')
39
+ # @param include_global [Boolean] Include global signal types (default: true)
40
+ # @return [Hash] Response with :data array and :summary
41
+ def list(category: nil, include_global: true)
42
+ uri = URI("#{@base_url}/functions/v1/api-signal-types")
43
+ params = {}
44
+ params[:category] = category if category
45
+ params[:include_global] = 'false' unless include_global
46
+ uri.query = URI.encode_www_form(params) unless params.empty?
47
+
48
+ response = make_request(:get, uri)
49
+ handle_response(response, 'list signal types')
50
+ end
51
+
52
+ # Create a new signal type
53
+ #
54
+ # @param name [String] Signal type name (required)
55
+ # @param category [String] Category - 'activity' or 'outcome' (default: 'activity')
56
+ # @param description [String, nil] Description
57
+ # @param unit [String, nil] Unit of measurement
58
+ # @param default_value [Float, nil] Default value
59
+ # @param external_id [String, nil] Your external ID
60
+ # @param expected_metrics [Hash, nil] Expected metrics configuration
61
+ # @param revenue_formula [String, nil] Revenue formula
62
+ # @param validation_rules [Hash, nil] Validation rules
63
+ # @return [Hash] Created signal type
64
+ def create(name:, category: 'activity', description: nil, unit: nil, default_value: nil,
65
+ external_id: nil, expected_metrics: nil, revenue_formula: nil, validation_rules: nil)
66
+ payload = { name: name, category: category }
67
+ payload[:description] = description if description
68
+ payload[:unit] = unit if unit
69
+ payload[:default_value] = default_value if default_value
70
+ payload[:external_id] = external_id if external_id
71
+ payload[:expected_metrics] = expected_metrics if expected_metrics
72
+ payload[:revenue_formula] = revenue_formula if revenue_formula
73
+ payload[:validation_rules] = validation_rules if validation_rules
74
+
75
+ uri = URI("#{@base_url}/functions/v1/api-signal-types")
76
+ response = make_request(:post, uri, payload)
77
+ handle_response(response, 'create signal type')
78
+ end
79
+
80
+ # Get a signal type by ID or external_id
81
+ #
82
+ # @param signal_type_id [String] Signal type UUID or external_id
83
+ # @param by_external_id [Boolean] If true, treat signal_type_id as external_id
84
+ # @return [Hash] Signal type object
85
+ def get(signal_type_id, by_external_id: false)
86
+ uri = URI("#{@base_url}/functions/v1/api-signal-types")
87
+ param = by_external_id ? :external_id : :id
88
+ uri.query = URI.encode_www_form({ param => signal_type_id })
89
+
90
+ response = make_request(:get, uri)
91
+ handle_response(response, 'get signal type')
92
+ end
93
+
94
+ # Update a signal type
95
+ #
96
+ # @param signal_type_id [String] Signal type UUID
97
+ # @param name [String, nil] New name
98
+ # @param description [String, nil] New description
99
+ # @param category [String, nil] New category
100
+ # @param unit [String, nil] New unit
101
+ # @param default_value [Float, nil] New default value
102
+ # @param external_id [String, nil] New external ID
103
+ # @param expected_metrics [Hash, nil] New expected metrics
104
+ # @param revenue_formula [String, nil] New revenue formula
105
+ # @param validation_rules [Hash, nil] New validation rules
106
+ # @param is_active [Boolean, nil] Active status
107
+ # @return [Hash] Updated signal type
108
+ def update(signal_type_id, name: nil, description: nil, category: nil, unit: nil,
109
+ default_value: nil, external_id: nil, expected_metrics: nil,
110
+ revenue_formula: nil, validation_rules: nil, is_active: nil)
111
+ payload = { id: signal_type_id }
112
+ payload[:name] = name if name
113
+ payload[:description] = description if description
114
+ payload[:category] = category if category
115
+ payload[:unit] = unit if unit
116
+ payload[:default_value] = default_value if default_value
117
+ payload[:external_id] = external_id if external_id
118
+ payload[:expected_metrics] = expected_metrics if expected_metrics
119
+ payload[:revenue_formula] = revenue_formula if revenue_formula
120
+ payload[:validation_rules] = validation_rules if validation_rules
121
+ payload[:is_active] = is_active unless is_active.nil?
122
+
123
+ uri = URI("#{@base_url}/functions/v1/api-signal-types")
124
+ response = make_request(:patch, uri, payload)
125
+ handle_response(response, 'update signal type')
126
+ end
127
+
128
+ # Delete a signal type
129
+ #
130
+ # @param signal_type_id [String] Signal type UUID to delete
131
+ # @return [Hash] Deletion result
132
+ def delete(signal_type_id)
133
+ uri = URI("#{@base_url}/functions/v1/api-signal-types")
134
+ response = make_request(:delete, uri, { id: signal_type_id })
135
+ handle_response(response, 'delete signal type')
136
+ end
137
+
138
+ private
139
+
140
+ def make_request(method, uri, payload = nil)
141
+ http = Net::HTTP.new(uri.host, uri.port)
142
+ http.use_ssl = true
143
+
144
+ request = case method
145
+ when :get
146
+ Net::HTTP::Get.new(uri)
147
+ when :post
148
+ Net::HTTP::Post.new(uri)
149
+ when :patch
150
+ Net::HTTP::Patch.new(uri)
151
+ when :delete
152
+ Net::HTTP::Delete.new(uri)
153
+ end
154
+
155
+ request['Content-Type'] = 'application/json'
156
+ request['X-API-Key'] = @api_key
157
+ request.body = payload.to_json if payload
158
+
159
+ puts "[AgentBill] #{method.upcase} #{uri}" if @debug
160
+
161
+ http.request(request)
162
+ end
163
+
164
+ def handle_response(response, operation)
165
+ body = response.body ? JSON.parse(response.body) : {}
166
+
167
+ case response.code.to_i
168
+ when 200, 201, 204
169
+ body
170
+ when 404
171
+ raise "Signal type not found"
172
+ when 409
173
+ raise "Signal type already exists: #{body['error']}"
174
+ else
175
+ raise "Failed to #{operation}: #{body['error'] || response.body}"
176
+ end
177
+ end
178
+ end
179
+ end