brainzlab 0.1.1 → 0.1.3

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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +6 -21
  3. data/README.md +24 -2
  4. data/lib/brainzlab/beacon/client.rb +207 -0
  5. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  6. data/lib/brainzlab/beacon.rb +215 -0
  7. data/lib/brainzlab/configuration.rb +372 -32
  8. data/lib/brainzlab/context.rb +2 -3
  9. data/lib/brainzlab/cortex/cache.rb +59 -0
  10. data/lib/brainzlab/cortex/client.rb +139 -0
  11. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  12. data/lib/brainzlab/cortex.rb +223 -0
  13. data/lib/brainzlab/dendrite/client.rb +230 -0
  14. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  15. data/lib/brainzlab/dendrite.rb +195 -0
  16. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  17. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  18. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  19. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  20. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  21. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  22. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  23. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  24. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  25. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  26. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  27. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  28. data/lib/brainzlab/devtools.rb +75 -0
  29. data/lib/brainzlab/flux/buffer.rb +96 -0
  30. data/lib/brainzlab/flux/client.rb +68 -0
  31. data/lib/brainzlab/flux/provisioner.rb +57 -0
  32. data/lib/brainzlab/flux.rb +174 -0
  33. data/lib/brainzlab/instrumentation/action_mailer.rb +14 -13
  34. data/lib/brainzlab/instrumentation/active_record.rb +28 -13
  35. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  36. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  37. data/lib/brainzlab/instrumentation/delayed_job.rb +27 -29
  38. data/lib/brainzlab/instrumentation/elasticsearch.rb +23 -24
  39. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  40. data/lib/brainzlab/instrumentation/faraday.rb +3 -4
  41. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  42. data/lib/brainzlab/instrumentation/grape.rb +24 -24
  43. data/lib/brainzlab/instrumentation/graphql.rb +24 -23
  44. data/lib/brainzlab/instrumentation/httparty.rb +13 -14
  45. data/lib/brainzlab/instrumentation/mongodb.rb +7 -7
  46. data/lib/brainzlab/instrumentation/net_http.rb +6 -6
  47. data/lib/brainzlab/instrumentation/redis.rb +14 -21
  48. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  49. data/lib/brainzlab/instrumentation/sidekiq.rb +29 -28
  50. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  51. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  52. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  53. data/lib/brainzlab/instrumentation.rb +84 -12
  54. data/lib/brainzlab/nerve/client.rb +215 -0
  55. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  56. data/lib/brainzlab/nerve.rb +219 -0
  57. data/lib/brainzlab/pulse/client.rb +15 -11
  58. data/lib/brainzlab/pulse/instrumentation.rb +90 -53
  59. data/lib/brainzlab/pulse/propagation.rb +29 -29
  60. data/lib/brainzlab/pulse/provisioner.rb +12 -12
  61. data/lib/brainzlab/pulse/tracer.rb +4 -4
  62. data/lib/brainzlab/pulse.rb +14 -14
  63. data/lib/brainzlab/rails/log_formatter.rb +127 -121
  64. data/lib/brainzlab/rails/log_subscriber.rb +70 -77
  65. data/lib/brainzlab/rails/railtie.rb +96 -86
  66. data/lib/brainzlab/recall/buffer.rb +1 -1
  67. data/lib/brainzlab/recall/client.rb +14 -10
  68. data/lib/brainzlab/recall/logger.rb +16 -18
  69. data/lib/brainzlab/recall/provisioner.rb +29 -12
  70. data/lib/brainzlab/recall.rb +14 -11
  71. data/lib/brainzlab/reflex/breadcrumbs.rb +2 -2
  72. data/lib/brainzlab/reflex/client.rb +14 -10
  73. data/lib/brainzlab/reflex/provisioner.rb +12 -12
  74. data/lib/brainzlab/reflex.rb +31 -31
  75. data/lib/brainzlab/sentinel/client.rb +216 -0
  76. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  77. data/lib/brainzlab/sentinel.rb +165 -0
  78. data/lib/brainzlab/signal/client.rb +60 -0
  79. data/lib/brainzlab/signal/provisioner.rb +55 -0
  80. data/lib/brainzlab/signal.rb +136 -0
  81. data/lib/brainzlab/synapse/client.rb +288 -0
  82. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  83. data/lib/brainzlab/synapse.rb +270 -0
  84. data/lib/brainzlab/utilities/circuit_breaker.rb +261 -0
  85. data/lib/brainzlab/utilities/health_check.rb +294 -0
  86. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  87. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  88. data/lib/brainzlab/utilities.rb +17 -0
  89. data/lib/brainzlab/vault/cache.rb +80 -0
  90. data/lib/brainzlab/vault/client.rb +196 -0
  91. data/lib/brainzlab/vault/provisioner.rb +49 -0
  92. data/lib/brainzlab/vault.rb +262 -0
  93. data/lib/brainzlab/version.rb +1 -1
  94. data/lib/brainzlab/vision/client.rb +128 -0
  95. data/lib/brainzlab/vision/provisioner.rb +136 -0
  96. data/lib/brainzlab/vision.rb +155 -0
  97. data/lib/brainzlab-sdk.rb +1 -1
  98. data/lib/brainzlab.rb +112 -13
  99. data/lib/generators/brainzlab/install/install_generator.rb +29 -27
  100. metadata +60 -1
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module BrainzLab
8
+ module Vault
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ @base_url = config.vault_url || 'https://vault.brainzlab.ai'
13
+ end
14
+
15
+ def get(key, environment:)
16
+ response = request(
17
+ :get,
18
+ "/api/v1/secrets/#{CGI.escape(key)}",
19
+ headers: { 'X-Vault-Environment' => environment }
20
+ )
21
+
22
+ return nil unless response.is_a?(Net::HTTPSuccess)
23
+
24
+ data = JSON.parse(response.body, symbolize_names: true)
25
+ data[:value]
26
+ rescue StandardError => e
27
+ log_error('get', e)
28
+ nil
29
+ end
30
+
31
+ def set(key, value, environment:, description: nil, note: nil)
32
+ body = {
33
+ key: key,
34
+ value: value,
35
+ description: description,
36
+ note: note
37
+ }.compact
38
+
39
+ response = request(
40
+ :post,
41
+ '/api/v1/secrets',
42
+ headers: { 'X-Vault-Environment' => environment },
43
+ body: body
44
+ )
45
+
46
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
47
+ rescue StandardError => e
48
+ log_error('set', e)
49
+ false
50
+ end
51
+
52
+ def list(environment:)
53
+ response = request(
54
+ :get,
55
+ '/api/v1/secrets',
56
+ headers: { 'X-Vault-Environment' => environment }
57
+ )
58
+
59
+ return [] unless response.is_a?(Net::HTTPSuccess)
60
+
61
+ data = JSON.parse(response.body, symbolize_names: true)
62
+ data[:secrets] || []
63
+ rescue StandardError => e
64
+ log_error('list', e)
65
+ []
66
+ end
67
+
68
+ def delete(key)
69
+ response = request(:delete, "/api/v1/secrets/#{CGI.escape(key)}")
70
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNoContent)
71
+ rescue StandardError => e
72
+ log_error('delete', e)
73
+ false
74
+ end
75
+
76
+ def export(environment:, format:)
77
+ params = { format: format }
78
+ response = request(
79
+ :get,
80
+ '/api/v1/sync/export',
81
+ headers: { 'X-Vault-Environment' => environment },
82
+ params: params
83
+ )
84
+
85
+ return {} unless response.is_a?(Net::HTTPSuccess)
86
+
87
+ case format
88
+ when :json
89
+ data = JSON.parse(response.body, symbolize_names: true)
90
+ data[:secrets] || {}
91
+ else
92
+ response.body
93
+ end
94
+ rescue StandardError => e
95
+ log_error('export', e)
96
+ format == :json ? {} : ''
97
+ end
98
+
99
+ def provision(project_id:, app_name:)
100
+ response = request(
101
+ :post,
102
+ '/api/v1/projects/provision',
103
+ body: { project_id: project_id, app_name: app_name },
104
+ use_service_key: true
105
+ )
106
+
107
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
108
+ rescue StandardError => e
109
+ log_error('provision', e)
110
+ false
111
+ end
112
+
113
+ # Get all provider keys for the current project
114
+ # Returns a hash of provider => decrypted_key
115
+ def get_provider_keys
116
+ response = request(:get, '/api/v1/provider_keys/bulk')
117
+
118
+ return {} unless response.is_a?(Net::HTTPSuccess)
119
+
120
+ data = JSON.parse(response.body, symbolize_names: true)
121
+ # Convert to simple hash: { openai: "sk-...", anthropic: "sk-..." }
122
+ keys = {}
123
+ (data[:keys] || []).each do |key_data|
124
+ keys[key_data[:provider].to_sym] = key_data[:key]
125
+ end
126
+ keys
127
+ rescue StandardError => e
128
+ log_error('get_provider_keys', e)
129
+ {}
130
+ end
131
+
132
+ # Get a specific provider key
133
+ def get_provider_key(provider:, model_type: 'llm')
134
+ response = request(
135
+ :get,
136
+ '/api/v1/provider_keys/resolve',
137
+ params: { provider: provider, model_type: model_type }
138
+ )
139
+
140
+ return nil unless response.is_a?(Net::HTTPSuccess)
141
+
142
+ data = JSON.parse(response.body, symbolize_names: true)
143
+ data[:key]
144
+ rescue StandardError => e
145
+ log_error('get_provider_key', e)
146
+ nil
147
+ end
148
+
149
+ private
150
+
151
+ def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
152
+ uri = URI.parse("#{@base_url}#{path}")
153
+
154
+ uri.query = URI.encode_www_form(params) if params
155
+
156
+ http = Net::HTTP.new(uri.host, uri.port)
157
+ http.use_ssl = uri.scheme == 'https'
158
+ http.open_timeout = 10
159
+ http.read_timeout = 30
160
+
161
+ request = case method
162
+ when :get
163
+ Net::HTTP::Get.new(uri)
164
+ when :post
165
+ Net::HTTP::Post.new(uri)
166
+ when :put
167
+ Net::HTTP::Put.new(uri)
168
+ when :delete
169
+ Net::HTTP::Delete.new(uri)
170
+ end
171
+
172
+ # Set headers
173
+ request['Content-Type'] = 'application/json'
174
+ request['Accept'] = 'application/json'
175
+
176
+ if use_service_key
177
+ request['X-Service-Key'] = @config.vault_master_key || @config.secret_key
178
+ else
179
+ auth_key = @config.vault_api_key || @config.secret_key
180
+ request['Authorization'] = "Bearer #{auth_key}" if auth_key
181
+ end
182
+
183
+ headers.each { |k, v| request[k] = v }
184
+
185
+ # Set body
186
+ request.body = body.to_json if body
187
+
188
+ http.request(request)
189
+ end
190
+
191
+ def log_error(operation, error)
192
+ BrainzLab.debug_log("[Vault::Client] #{operation} failed: #{error.message}")
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Vault
5
+ class Provisioner
6
+ def initialize(config)
7
+ @config = config
8
+ @provisioned = false
9
+ end
10
+
11
+ def ensure_project!
12
+ return if @provisioned
13
+ return unless @config.vault_auto_provision
14
+ return unless valid_auth?
15
+
16
+ @provisioned = true
17
+
18
+ # Try to provision with Platform project ID
19
+ project_id = detect_project_id
20
+ return unless project_id
21
+
22
+ client = Client.new(@config)
23
+ client.provision(
24
+ project_id: project_id,
25
+ app_name: @config.app_name || @config.service
26
+ )
27
+
28
+ BrainzLab.debug_log("[Vault::Provisioner] Project provisioned: #{project_id}")
29
+ rescue StandardError => e
30
+ BrainzLab.debug_log("[Vault::Provisioner] Provisioning failed: #{e.message}")
31
+ end
32
+
33
+ private
34
+
35
+ def valid_auth?
36
+ key = @config.vault_api_key || @config.vault_master_key || @config.secret_key
37
+ !key.nil? && !key.empty?
38
+ end
39
+
40
+ def detect_project_id
41
+ # Try environment variable first
42
+ return ENV['BRAINZLAB_PROJECT_ID'] if ENV['BRAINZLAB_PROJECT_ID']
43
+
44
+ # Could also detect from Platform API if we have a secret key
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'vault/client'
4
+ require_relative 'vault/cache'
5
+ require_relative 'vault/provisioner'
6
+
7
+ module BrainzLab
8
+ module Vault
9
+ class << self
10
+ # Load all secrets into ENV like dotenv
11
+ # This is the main method to use at app startup
12
+ #
13
+ # @param environment [String, Symbol] Environment to load (defaults to current)
14
+ # @param overwrite [Boolean] Whether to overwrite existing ENV vars (default: false)
15
+ # @param provider_keys [Boolean] Also load provider keys like OPENAI_API_KEY (default: true)
16
+ # @return [Hash] The secrets that were loaded
17
+ #
18
+ # @example
19
+ # # In config/application.rb or an initializer
20
+ # BrainzLab::Vault.load!
21
+ #
22
+ # # Load with options
23
+ # BrainzLab::Vault.load!(environment: :production, overwrite: true)
24
+ #
25
+ def load!(environment: nil, overwrite: false, provider_keys: true)
26
+ return {} unless enabled?
27
+
28
+ ensure_provisioned!
29
+ return {} unless BrainzLab.configuration.vault_valid?
30
+
31
+ env = environment&.to_s || BrainzLab.configuration.environment
32
+ loaded = {}
33
+
34
+ # Load regular secrets
35
+ secrets = export(environment: env, format: :json)
36
+ secrets.each do |key, value|
37
+ key_str = key.to_s
38
+ next unless overwrite || !ENV.key?(key_str)
39
+
40
+ ENV[key_str] = value.to_s
41
+ loaded[key_str] = value
42
+ BrainzLab.debug_log("[Vault] Loaded #{key_str}")
43
+ end
44
+
45
+ # Load provider keys (OpenAI, Anthropic, etc.)
46
+ if provider_keys
47
+ provider_secrets = load_provider_keys!(overwrite: overwrite)
48
+ loaded.merge!(provider_secrets)
49
+ end
50
+
51
+ BrainzLab.debug_log("[Vault] Loaded #{loaded.size} secrets into ENV")
52
+ loaded
53
+ rescue StandardError => e
54
+ BrainzLab.debug_log("[Vault] Failed to load secrets: #{e.message}")
55
+ {}
56
+ end
57
+
58
+ # Load provider keys (API keys for LLMs, etc.) into ENV
59
+ #
60
+ # @param overwrite [Boolean] Whether to overwrite existing ENV vars
61
+ # @return [Hash] Provider keys that were loaded
62
+ def load_provider_keys!(overwrite: false)
63
+ return {} unless enabled? && BrainzLab.configuration.vault_valid?
64
+
65
+ loaded = {}
66
+ provider_keys = client.get_provider_keys
67
+
68
+ provider_keys.each do |provider, key|
69
+ env_var = "#{provider.to_s.upcase}_API_KEY"
70
+ next unless overwrite || !ENV.key?(env_var)
71
+
72
+ ENV[env_var] = key
73
+ loaded[env_var] = key
74
+ BrainzLab.debug_log("[Vault] Loaded provider key: #{env_var}")
75
+ end
76
+
77
+ loaded
78
+ rescue StandardError => e
79
+ BrainzLab.debug_log("[Vault] Failed to load provider keys: #{e.message}")
80
+ {}
81
+ end
82
+
83
+ # Get a specific provider key
84
+ # @param provider [String, Symbol] Provider name (openai, anthropic, etc.)
85
+ # @param model_type [String] Model type (llm, embedding, etc.)
86
+ # @return [String, nil] The API key
87
+ def provider_key(provider, model_type: 'llm')
88
+ return nil unless enabled?
89
+
90
+ ensure_provisioned!
91
+ return nil unless BrainzLab.configuration.vault_valid?
92
+
93
+ # Check ENV first
94
+ env_var = "#{provider.to_s.upcase}_API_KEY"
95
+ return ENV[env_var] if ENV[env_var] && !ENV[env_var].empty?
96
+
97
+ # Fetch from Vault
98
+ client.get_provider_key(provider: provider.to_s, model_type: model_type)
99
+ end
100
+
101
+ # Get a secret value
102
+ # @param key [String] The secret key
103
+ # @param environment [String, Symbol] Optional environment (defaults to current environment)
104
+ # @param default [Object] Default value if secret not found
105
+ # @return [String, nil] The secret value
106
+ def get(key, environment: nil, default: nil)
107
+ return default unless enabled?
108
+
109
+ ensure_provisioned!
110
+ return default unless BrainzLab.configuration.vault_valid?
111
+
112
+ env = environment&.to_s || BrainzLab.configuration.environment
113
+ cache_key = "#{env}:#{key}"
114
+
115
+ # Check cache first
116
+ return cache.get(cache_key) if BrainzLab.configuration.vault_cache_enabled && cache.has?(cache_key)
117
+
118
+ value = client.get(key, environment: env)
119
+
120
+ if value.nil?
121
+ default
122
+ else
123
+ cache.set(cache_key, value) if BrainzLab.configuration.vault_cache_enabled
124
+ value
125
+ end
126
+ end
127
+
128
+ # Set a secret value
129
+ # @param key [String] The secret key
130
+ # @param value [String] The secret value
131
+ # @param environment [String, Symbol] Optional environment (defaults to current environment)
132
+ # @param description [String] Optional description
133
+ # @param note [String] Optional version note
134
+ # @return [Boolean] True if successful
135
+ def set(key, value, environment: nil, description: nil, note: nil)
136
+ return false unless enabled?
137
+
138
+ ensure_provisioned!
139
+ return false unless BrainzLab.configuration.vault_valid?
140
+
141
+ env = environment&.to_s || BrainzLab.configuration.environment
142
+ result = client.set(key, value, environment: env, description: description, note: note)
143
+
144
+ # Invalidate cache
145
+ cache.delete("#{env}:#{key}") if result && BrainzLab.configuration.vault_cache_enabled
146
+
147
+ result
148
+ end
149
+
150
+ # List all secret keys
151
+ # @param environment [String, Symbol] Optional environment
152
+ # @return [Array<Hash>] List of secret metadata
153
+ def list(environment: nil)
154
+ return [] unless enabled?
155
+
156
+ ensure_provisioned!
157
+ return [] unless BrainzLab.configuration.vault_valid?
158
+
159
+ env = environment&.to_s || BrainzLab.configuration.environment
160
+ client.list(environment: env)
161
+ end
162
+
163
+ # Delete (archive) a secret
164
+ # @param key [String] The secret key
165
+ # @return [Boolean] True if successful
166
+ def delete(key)
167
+ return false unless enabled?
168
+
169
+ ensure_provisioned!
170
+ return false unless BrainzLab.configuration.vault_valid?
171
+
172
+ result = client.delete(key)
173
+
174
+ # Invalidate all environment caches for this key
175
+ cache.delete_pattern("*:#{key}") if result && BrainzLab.configuration.vault_cache_enabled
176
+
177
+ result
178
+ end
179
+
180
+ # Export all secrets for an environment
181
+ # @param environment [String, Symbol] Environment to export
182
+ # @param format [Symbol] Output format (:json, :dotenv, :shell)
183
+ # @return [Hash, String] Exported secrets
184
+ def export(environment: nil, format: :json)
185
+ return {} unless enabled?
186
+
187
+ ensure_provisioned!
188
+ return {} unless BrainzLab.configuration.vault_valid?
189
+
190
+ env = environment&.to_s || BrainzLab.configuration.environment
191
+ client.export(environment: env, format: format)
192
+ end
193
+
194
+ # Fetch a secret with automatic fallback
195
+ # @param key [String] The secret key
196
+ # @param env_var [String] Environment variable to fall back to
197
+ # @return [String, nil] The secret value
198
+ def fetch(key, env_var: nil)
199
+ value = get(key)
200
+ return value if value && !value.to_s.empty?
201
+
202
+ # Fall back to environment variable
203
+ if env_var
204
+ ENV.fetch(env_var, nil)
205
+ else
206
+ ENV.fetch(key, nil)
207
+ end
208
+ end
209
+
210
+ # Clear the secret cache
211
+ def clear_cache!
212
+ cache.clear!
213
+ end
214
+
215
+ # Warm the cache with all secrets
216
+ def warm_cache!(environment: nil)
217
+ return unless enabled? && BrainzLab.configuration.vault_cache_enabled
218
+
219
+ env = environment&.to_s || BrainzLab.configuration.environment
220
+ secrets = export(environment: env, format: :json)
221
+
222
+ secrets.each do |key, value|
223
+ cache.set("#{env}:#{key}", value)
224
+ end
225
+ end
226
+
227
+ # === INTERNAL ===
228
+
229
+ def ensure_provisioned!
230
+ return if @provisioned
231
+
232
+ @provisioned = true
233
+ provisioner.ensure_project!
234
+ end
235
+
236
+ def provisioner
237
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
238
+ end
239
+
240
+ def client
241
+ @client ||= Client.new(BrainzLab.configuration)
242
+ end
243
+
244
+ def cache
245
+ @cache ||= Cache.new(BrainzLab.configuration.vault_cache_ttl)
246
+ end
247
+
248
+ def reset!
249
+ @client = nil
250
+ @provisioner = nil
251
+ @cache = nil
252
+ @provisioned = false
253
+ end
254
+
255
+ private
256
+
257
+ def enabled?
258
+ BrainzLab.configuration.vault_enabled
259
+ end
260
+ end
261
+ end
262
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BrainzLab
4
- VERSION = "0.1.1"
4
+ VERSION = '0.1.3'
5
5
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module BrainzLab
8
+ module Vision
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ # Execute an autonomous AI task
15
+ def execute_task(instruction:, start_url:, model: nil, browser_provider: nil, max_steps: 50, timeout: 300)
16
+ payload = {
17
+ instruction: instruction,
18
+ start_url: start_url,
19
+ max_steps: max_steps,
20
+ timeout: timeout
21
+ }
22
+ payload[:model] = model if model
23
+ payload[:browser_provider] = browser_provider if browser_provider
24
+
25
+ post('/mcp/tools/vision_task', payload)
26
+ end
27
+
28
+ # Create a browser session
29
+ def create_session(url: nil, viewport: nil, browser_provider: nil)
30
+ payload = {}
31
+ payload[:url] = url if url
32
+ payload[:viewport] = viewport if viewport
33
+ payload[:browser_provider] = browser_provider if browser_provider
34
+
35
+ post('/mcp/tools/vision_session_create', payload)
36
+ end
37
+
38
+ # Perform an AI-powered action
39
+ def ai_action(session_id:, instruction:, model: nil)
40
+ payload = {
41
+ session_id: session_id,
42
+ instruction: instruction
43
+ }
44
+ payload[:model] = model if model
45
+
46
+ post('/mcp/tools/vision_ai_action', payload)
47
+ end
48
+
49
+ # Perform a direct browser action
50
+ def perform(session_id:, action:, selector: nil, value: nil)
51
+ payload = {
52
+ session_id: session_id,
53
+ action: action.to_s
54
+ }
55
+ payload[:selector] = selector if selector
56
+ payload[:value] = value if value
57
+
58
+ post('/mcp/tools/vision_perform', payload)
59
+ end
60
+
61
+ # Extract structured data
62
+ def extract(session_id:, schema:, instruction: nil)
63
+ payload = {
64
+ session_id: session_id,
65
+ schema: schema
66
+ }
67
+ payload[:instruction] = instruction if instruction
68
+
69
+ post('/mcp/tools/vision_extract', payload)
70
+ end
71
+
72
+ # Close a session
73
+ def close_session(session_id:)
74
+ post('/mcp/tools/vision_session_close', { session_id: session_id })
75
+ end
76
+
77
+ # Take a screenshot
78
+ def screenshot(session_id:, full_page: true)
79
+ post('/mcp/tools/vision_screenshot', {
80
+ session_id: session_id,
81
+ full_page: full_page
82
+ })
83
+ end
84
+
85
+ private
86
+
87
+ def post(path, payload)
88
+ uri = URI.parse("#{@config.vision_url}#{path}")
89
+ request = Net::HTTP::Post.new(uri)
90
+ request['Content-Type'] = 'application/json'
91
+ request['Authorization'] = "Bearer #{auth_key}"
92
+ request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
93
+ request.body = JSON.generate(payload)
94
+
95
+ response = execute(uri, request)
96
+
97
+ case response
98
+ when Net::HTTPSuccess
99
+ JSON.parse(response.body, symbolize_names: true)
100
+ when Net::HTTPUnauthorized
101
+ { error: 'Unauthorized: Invalid API key' }
102
+ when Net::HTTPForbidden
103
+ { error: 'Forbidden: Vision is not enabled for this project' }
104
+ when Net::HTTPNotFound
105
+ { error: "Not found: #{path}" }
106
+ else
107
+ { error: "HTTP #{response.code}: #{response.message}" }
108
+ end
109
+ rescue JSON::ParserError => e
110
+ { error: "Invalid JSON response: #{e.message}" }
111
+ rescue StandardError => e
112
+ { error: "Request failed: #{e.message}" }
113
+ end
114
+
115
+ def auth_key
116
+ @config.vision_ingest_key || @config.vision_api_key || @config.secret_key
117
+ end
118
+
119
+ def execute(uri, request)
120
+ http = Net::HTTP.new(uri.host, uri.port)
121
+ http.use_ssl = uri.scheme == 'https'
122
+ http.open_timeout = 10
123
+ http.read_timeout = 300 # Long timeout for AI tasks
124
+ http.request(request)
125
+ end
126
+ end
127
+ end
128
+ end