decision_agent 0.2.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache_adapter"
4
+ require "monitor"
5
+
6
+ module DecisionAgent
7
+ module DataEnrichment
8
+ module Cache
9
+ # In-memory cache adapter (default, no dependencies)
10
+ class MemoryAdapter < CacheAdapter
11
+ include MonitorMixin
12
+
13
+ def initialize
14
+ super
15
+ @cache = {}
16
+ end
17
+
18
+ # Get cached value
19
+ #
20
+ # @param key [String] Cache key
21
+ # @return [Hash, nil] Cached data or nil if not found/expired
22
+ def get(key)
23
+ synchronize do
24
+ entry = @cache[key]
25
+ return nil unless entry
26
+
27
+ # Check if expired
28
+ if entry[:expires_at] < Time.now
29
+ @cache.delete(key)
30
+ return nil
31
+ end
32
+
33
+ entry[:value]
34
+ end
35
+ end
36
+
37
+ # Set cached value
38
+ #
39
+ # @param key [String] Cache key
40
+ # @param value [Hash] Data to cache
41
+ # @param ttl [Integer] Time to live in seconds
42
+ def set(key, value, ttl)
43
+ synchronize do
44
+ @cache[key] = {
45
+ value: value,
46
+ expires_at: Time.now + ttl
47
+ }
48
+ end
49
+ end
50
+
51
+ # Delete cached value
52
+ #
53
+ # @param key [String] Cache key
54
+ def delete(key)
55
+ synchronize do
56
+ @cache.delete(key)
57
+ end
58
+ end
59
+
60
+ # Clear all cached values
61
+ def clear
62
+ synchronize do
63
+ @cache.clear
64
+ end
65
+ end
66
+
67
+ # Get cache statistics
68
+ #
69
+ # @return [Hash] Cache statistics
70
+ def stats
71
+ synchronize do
72
+ now = Time.now
73
+ valid_entries = @cache.count { |_k, v| v[:expires_at] >= now }
74
+ expired_entries = @cache.size - valid_entries
75
+
76
+ {
77
+ size: @cache.size,
78
+ valid: valid_entries,
79
+ expired: expired_entries
80
+ }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module DataEnrichment
5
+ # Base class for cache adapters
6
+ class CacheAdapter
7
+ # Get cached value
8
+ #
9
+ # @param key [String] Cache key
10
+ # @return [Hash, nil] Cached data or nil if not found/expired
11
+ def get(key)
12
+ raise NotImplementedError, "#{self.class} must implement #get"
13
+ end
14
+
15
+ # Set cached value
16
+ #
17
+ # @param key [String] Cache key
18
+ # @param value [Hash] Data to cache
19
+ # @param ttl [Integer] Time to live in seconds
20
+ def set(key, value, ttl)
21
+ raise NotImplementedError, "#{self.class} must implement #set"
22
+ end
23
+
24
+ # Delete cached value
25
+ #
26
+ # @param key [String] Cache key
27
+ def delete(key)
28
+ raise NotImplementedError, "#{self.class} must implement #delete"
29
+ end
30
+
31
+ # Clear all cached values
32
+ def clear
33
+ raise NotImplementedError, "#{self.class} must implement #clear"
34
+ end
35
+
36
+ # Generate cache key from request parameters
37
+ #
38
+ # @param endpoint_name [Symbol] Endpoint identifier
39
+ # @param params [Hash] Request parameters
40
+ # @return [String] Cache key
41
+ def cache_key(endpoint_name, params)
42
+ # Sort params for consistent key generation
43
+ sorted_params = params.sort.to_h
44
+ param_string = sorted_params.map { |k, v| "#{k}=#{v}" }.join("&")
45
+ "#{endpoint_name}:#{Digest::SHA256.hexdigest(param_string)}"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module DecisionAgent
6
+ module DataEnrichment
7
+ # Circuit breaker pattern for resilience
8
+ #
9
+ # Prevents cascading failures by opening the circuit after N failures
10
+ class CircuitBreaker
11
+ include MonitorMixin
12
+
13
+ # Circuit states
14
+ CLOSED = :closed # Normal operation
15
+ OPEN = :open # Circuit is open, failing fast
16
+ HALF_OPEN = :half_open # Testing if service recovered
17
+
18
+ def initialize(failure_threshold: 5, timeout: 60, success_threshold: 2)
19
+ super()
20
+ @failure_threshold = failure_threshold
21
+ @timeout = timeout
22
+ @success_threshold = success_threshold
23
+ @state = CLOSED
24
+ @failure_count = 0
25
+ @success_count = 0
26
+ @last_failure_time = nil
27
+ end
28
+
29
+ # Execute block with circuit breaker protection
30
+ #
31
+ # @yield Block to execute
32
+ # @return [Object] Result of block execution
33
+ # @raise [CircuitOpenError] If circuit is open
34
+ def call
35
+ synchronize do
36
+ check_state
37
+ raise CircuitOpenError, "Circuit is open" if @state == OPEN
38
+ end
39
+
40
+ begin
41
+ result = yield
42
+ record_success
43
+ result
44
+ rescue StandardError => e
45
+ record_failure
46
+ raise e
47
+ end
48
+ end
49
+
50
+ # Check if circuit is open
51
+ #
52
+ # @return [Boolean]
53
+ def open?
54
+ synchronize do
55
+ check_state
56
+ @state == OPEN
57
+ end
58
+ end
59
+
60
+ # Reset circuit breaker to closed state
61
+ def reset
62
+ synchronize do
63
+ @state = CLOSED
64
+ @failure_count = 0
65
+ @success_count = 0
66
+ @last_failure_time = nil
67
+ end
68
+ end
69
+
70
+ # Get current state
71
+ #
72
+ # @return [Symbol] Current state
73
+ def state
74
+ synchronize do
75
+ check_state
76
+ @state
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def check_state
83
+ case @state
84
+ when OPEN
85
+ # Check if timeout has elapsed, move to half-open
86
+ if @last_failure_time && (Time.now - @last_failure_time) >= @timeout
87
+ @state = HALF_OPEN
88
+ @success_count = 0
89
+ end
90
+ when HALF_OPEN
91
+ # State remains half-open until success threshold is met
92
+ when CLOSED
93
+ # State remains closed until failure threshold is met
94
+ end
95
+ end
96
+
97
+ def record_success
98
+ synchronize do
99
+ case @state
100
+ when HALF_OPEN
101
+ @success_count += 1
102
+ if @success_count >= @success_threshold
103
+ @state = CLOSED
104
+ @failure_count = 0
105
+ @success_count = 0
106
+ end
107
+ when CLOSED
108
+ @failure_count = 0 # Reset failure count on success
109
+ end
110
+ end
111
+ end
112
+
113
+ def record_failure
114
+ synchronize do
115
+ @failure_count += 1
116
+ @last_failure_time = Time.now
117
+
118
+ case @state
119
+ when HALF_OPEN
120
+ # Failures in half-open state immediately open the circuit
121
+ @state = OPEN
122
+ @success_count = 0
123
+ when CLOSED
124
+ # Open circuit if failure threshold is met
125
+ @state = OPEN if @failure_count >= @failure_threshold
126
+ end
127
+ end
128
+ end
129
+
130
+ # Error raised when circuit is open
131
+ class CircuitOpenError < StandardError
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "base64"
7
+ require_relative "cache_adapter"
8
+ require_relative "cache/memory_adapter"
9
+ require_relative "circuit_breaker"
10
+ require_relative "errors"
11
+
12
+ module DecisionAgent
13
+ module DataEnrichment
14
+ # HTTP client for data enrichment requests
15
+ class Client
16
+ # Error classes
17
+ class RequestError < StandardError
18
+ attr_reader :status_code, :response_body
19
+
20
+ def initialize(message, status_code: nil, response_body: nil)
21
+ super(message)
22
+ @status_code = status_code
23
+ @response_body = response_body
24
+ end
25
+ end
26
+
27
+ class TimeoutError < RequestError
28
+ end
29
+
30
+ class NetworkError < RequestError
31
+ end
32
+
33
+ attr_reader :config, :cache_adapter, :circuit_breaker
34
+
35
+ def initialize(config:, cache_adapter: nil, circuit_breaker: nil)
36
+ @config = config
37
+ @cache_adapter = cache_adapter || Cache::MemoryAdapter.new
38
+ @circuit_breakers = {}
39
+ @circuit_breaker_default = circuit_breaker || CircuitBreaker.new
40
+ @circuit_breaker = @circuit_breaker_default
41
+ end
42
+
43
+ # Fetch data from configured endpoint
44
+ #
45
+ # @param endpoint_name [Symbol] Endpoint identifier
46
+ # @param params [Hash] Request parameters
47
+ # @param use_cache [Boolean] Whether to use cache (default: true)
48
+ # @return [Hash] Response data
49
+ # @raise [RequestError] If request fails
50
+ def fetch(endpoint_name, params: {}, use_cache: true)
51
+ endpoint_config = @config.endpoint(endpoint_name)
52
+ raise ArgumentError, "Unknown endpoint: #{endpoint_name}" unless endpoint_config
53
+
54
+ # Generate cache key
55
+ cache_key = @cache_adapter.cache_key(endpoint_name, params)
56
+
57
+ # Check cache first
58
+ if use_cache
59
+ cached = @cache_adapter.get(cache_key)
60
+ return cached if cached
61
+ end
62
+
63
+ # Execute request with circuit breaker
64
+ circuit_breaker = get_circuit_breaker(endpoint_name)
65
+ begin
66
+ response_data = circuit_breaker.call do
67
+ execute_request(endpoint_name, endpoint_config, params)
68
+ end
69
+ rescue CircuitBreaker::CircuitOpenError => e
70
+ # Try to return cached data on circuit open
71
+ cached = @cache_adapter.get(cache_key) if use_cache
72
+ return cached if cached
73
+
74
+ raise RequestError, "Circuit breaker is open for #{endpoint_name}: #{e.message}"
75
+ end
76
+
77
+ # Cache response
78
+ @cache_adapter.set(cache_key, response_data, endpoint_config[:cache][:ttl]) if use_cache && endpoint_config[:cache][:ttl].positive?
79
+
80
+ response_data
81
+ end
82
+
83
+ # Clear cache for endpoint
84
+ #
85
+ # @param endpoint_name [Symbol, nil] Endpoint identifier, or nil to clear all
86
+ def clear_cache(endpoint_name = nil)
87
+ if endpoint_name
88
+ # Clear cache entries for this endpoint (requires cache adapter support)
89
+ # For now, just clear all if endpoint-specific clearing isn't supported
90
+ end
91
+ @cache_adapter.clear
92
+ end
93
+
94
+ private
95
+
96
+ def execute_request(endpoint_name, endpoint_config, params)
97
+ uri = URI(endpoint_config[:url])
98
+ method = endpoint_config[:method]
99
+ timeout = endpoint_config[:timeout]
100
+
101
+ http = Net::HTTP.new(uri.host, uri.port)
102
+ http.use_ssl = (uri.scheme == "https")
103
+ http.read_timeout = timeout
104
+ http.open_timeout = timeout
105
+
106
+ request = build_request(uri, method, endpoint_config, params)
107
+
108
+ response = http.request(request)
109
+
110
+ handle_response(response, endpoint_name)
111
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
112
+ raise TimeoutError, "Request timeout for #{endpoint_name}: #{e.message}"
113
+ rescue StandardError => e
114
+ raise NetworkError, "Network error for #{endpoint_name}: #{e.message}"
115
+ end
116
+
117
+ def build_request(uri, method, endpoint_config, params)
118
+ headers = endpoint_config[:headers].dup || {}
119
+ apply_auth(headers, endpoint_config[:auth])
120
+
121
+ case method
122
+ when :get
123
+ # Add params to query string for GET requests
124
+ if params.any?
125
+ query_string = URI.encode_www_form(params)
126
+ uri_with_query = URI("#{uri}?#{query_string}")
127
+ request = Net::HTTP::Get.new(uri_with_query)
128
+ else
129
+ request = Net::HTTP::Get.new(uri)
130
+ end
131
+ when :post
132
+ request = Net::HTTP::Post.new(uri)
133
+ request.body = params.to_json
134
+ headers["Content-Type"] ||= "application/json"
135
+ when :put
136
+ request = Net::HTTP::Put.new(uri)
137
+ request.body = params.to_json
138
+ headers["Content-Type"] ||= "application/json"
139
+ when :delete
140
+ request = Net::HTTP::Delete.new(uri)
141
+ else
142
+ raise ArgumentError, "Unsupported HTTP method: #{method}"
143
+ end
144
+
145
+ headers.each { |k, v| request[k] = v }
146
+ request
147
+ end
148
+
149
+ def apply_auth(headers, auth_config)
150
+ return unless auth_config
151
+
152
+ case auth_config[:type]
153
+ when :api_key
154
+ header_name = auth_config[:header] || "X-API-Key"
155
+ api_key = get_secret(auth_config[:secret_key] || "API_KEY")
156
+ headers[header_name] = api_key
157
+ when :basic
158
+ username = get_secret(auth_config[:username_key] || "USERNAME")
159
+ password = get_secret(auth_config[:password_key] || "PASSWORD")
160
+ credentials = Base64.strict_encode64("#{username}:#{password}")
161
+ headers["Authorization"] = "Basic #{credentials}"
162
+ when :bearer
163
+ token = get_secret(auth_config[:token_key] || "TOKEN")
164
+ headers["Authorization"] = "Bearer #{token}"
165
+ end
166
+ end
167
+
168
+ def get_secret(key)
169
+ # Try environment variable first
170
+ return ENV[key] if ENV.key?(key)
171
+ return ENV[key.upcase] if ENV.key?(key.upcase)
172
+
173
+ # Could extend to support vault integration here
174
+ raise ArgumentError, "Secret not found: #{key}. Set environment variable #{key.upcase}"
175
+ end
176
+
177
+ def handle_response(response, endpoint_name)
178
+ case response.code.to_i
179
+ when 200..299
180
+ parse_response(response)
181
+ when 400..499
182
+ raise RequestError.new(
183
+ "Client error for #{endpoint_name}: HTTP #{response.code}",
184
+ status_code: response.code.to_i,
185
+ response_body: response.body
186
+ )
187
+ when 500..599
188
+ raise RequestError.new(
189
+ "Server error for #{endpoint_name}: HTTP #{response.code}",
190
+ status_code: response.code.to_i,
191
+ response_body: response.body
192
+ )
193
+ else
194
+ raise RequestError.new(
195
+ "Unexpected response for #{endpoint_name}: HTTP #{response.code}",
196
+ status_code: response.code.to_i,
197
+ response_body: response.body
198
+ )
199
+ end
200
+ end
201
+
202
+ def parse_response(response)
203
+ content_type = response["Content-Type"] || ""
204
+ body = response.body
205
+
206
+ return {} if body.nil? || body.empty?
207
+
208
+ if content_type.include?("application/json")
209
+ JSON.parse(body, symbolize_names: true)
210
+ else
211
+ { body: body, content_type: content_type }
212
+ end
213
+ end
214
+
215
+ def get_circuit_breaker(endpoint_name)
216
+ @circuit_breakers[endpoint_name] ||= @circuit_breaker_default.dup
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module DataEnrichment
5
+ # Configuration for data enrichment endpoints
6
+ class Config
7
+ attr_accessor :endpoints, :default_timeout, :default_retry, :default_cache
8
+
9
+ def initialize
10
+ @endpoints = {}
11
+ @default_timeout = 5 # seconds
12
+ @default_retry = { max_attempts: 3, backoff: :exponential }
13
+ @default_cache = { ttl: 3600, adapter: :memory }
14
+ end
15
+
16
+ # Add or update an endpoint configuration
17
+ #
18
+ # @param name [Symbol] Endpoint identifier
19
+ # @param url [String] Base URL for the endpoint
20
+ # @param method [Symbol] HTTP method (:get, :post, :put, :delete)
21
+ # @param auth [Hash] Authentication configuration
22
+ # @param cache [Hash] Cache configuration
23
+ # @param retry_config [Hash] Retry configuration
24
+ # @param timeout [Integer] Request timeout in seconds
25
+ # @param headers [Hash] Default headers
26
+ # @param rate_limit [Hash] Rate limiting configuration
27
+ #
28
+ # @example
29
+ # config.add_endpoint(:credit_bureau,
30
+ # url: "https://api.creditbureau.com/v1/score",
31
+ # method: :post,
32
+ # auth: { type: :api_key, header: "X-API-Key" },
33
+ # cache: { ttl: 3600, adapter: :redis },
34
+ # retry_config: { max_attempts: 3, backoff: :exponential }
35
+ # )
36
+ def add_endpoint(name, url:, method: :get, auth: nil, cache: nil, retry_config: nil, timeout: nil, headers: {}, rate_limit: nil) # rubocop:disable Metrics/ParameterLists
37
+ @endpoints[name.to_sym] = {
38
+ url: url,
39
+ method: method.to_sym,
40
+ auth: auth,
41
+ cache: cache || @default_cache.dup,
42
+ retry: retry_config || @default_retry.dup,
43
+ timeout: timeout || @default_timeout,
44
+ headers: headers,
45
+ rate_limit: rate_limit
46
+ }
47
+ end
48
+
49
+ # Get endpoint configuration
50
+ #
51
+ # @param name [Symbol] Endpoint identifier
52
+ # @return [Hash, nil] Endpoint configuration or nil if not found
53
+ def endpoint(name)
54
+ @endpoints[name.to_sym]
55
+ end
56
+
57
+ # Check if endpoint exists
58
+ #
59
+ # @param name [Symbol] Endpoint identifier
60
+ # @return [Boolean]
61
+ def endpoint?(name)
62
+ @endpoints.key?(name.to_sym)
63
+ end
64
+
65
+ # Remove endpoint configuration
66
+ #
67
+ # @param name [Symbol] Endpoint identifier
68
+ def remove_endpoint(name)
69
+ @endpoints.delete(name.to_sym)
70
+ end
71
+
72
+ # Clear all endpoint configurations
73
+ def clear
74
+ @endpoints.clear
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module DataEnrichment
5
+ # Base error class for data enrichment
6
+ class Error < StandardError
7
+ end
8
+
9
+ # Error raised when endpoint is not configured
10
+ class EndpointNotFoundError < Error
11
+ end
12
+
13
+ # Error raised when request fails
14
+ class RequestError < Error
15
+ attr_reader :status_code, :response_body
16
+
17
+ def initialize(message, status_code: nil, response_body: nil)
18
+ super(message)
19
+ @status_code = status_code
20
+ @response_body = response_body
21
+ end
22
+ end
23
+
24
+ # Error raised when request times out
25
+ class TimeoutError < RequestError
26
+ end
27
+
28
+ # Error raised when network error occurs
29
+ class NetworkError < RequestError
30
+ end
31
+
32
+ # Error raised when circuit breaker is open
33
+ class CircuitOpenError < Error
34
+ end
35
+ end
36
+ end