decision_agent 0.1.7 → 0.3.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -1
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/dmn/adapter.rb +135 -0
  5. data/lib/decision_agent/dmn/cache.rb +306 -0
  6. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  7. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  8. data/lib/decision_agent/dmn/errors.rb +30 -0
  9. data/lib/decision_agent/dmn/exporter.rb +217 -0
  10. data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
  11. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  12. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  13. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  14. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  15. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  16. data/lib/decision_agent/dmn/importer.rb +77 -0
  17. data/lib/decision_agent/dmn/model.rb +197 -0
  18. data/lib/decision_agent/dmn/parser.rb +191 -0
  19. data/lib/decision_agent/dmn/testing.rb +333 -0
  20. data/lib/decision_agent/dmn/validator.rb +315 -0
  21. data/lib/decision_agent/dmn/versioning.rb +229 -0
  22. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  23. data/lib/decision_agent/dsl/condition_evaluator.rb +1132 -12
  24. data/lib/decision_agent/dsl/schema_validator.rb +12 -1
  25. data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
  26. data/lib/decision_agent/version.rb +1 -1
  27. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  28. data/lib/decision_agent/web/public/app.js +119 -1
  29. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  30. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  31. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  32. data/lib/decision_agent/web/public/index.html +71 -0
  33. data/lib/decision_agent/web/public/styles.css +21 -0
  34. data/lib/decision_agent/web/server.rb +465 -0
  35. data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
  36. data/spec/advanced_operators_spec.rb +2147 -0
  37. data/spec/auth/rbac_adapter_spec.rb +228 -0
  38. data/spec/dmn/decision_graph_spec.rb +282 -0
  39. data/spec/dmn/decision_tree_spec.rb +203 -0
  40. data/spec/dmn/feel/errors_spec.rb +18 -0
  41. data/spec/dmn/feel/functions_spec.rb +400 -0
  42. data/spec/dmn/feel/simple_parser_spec.rb +274 -0
  43. data/spec/dmn/feel/types_spec.rb +176 -0
  44. data/spec/dmn/feel_parser_spec.rb +489 -0
  45. data/spec/dmn/hit_policy_spec.rb +202 -0
  46. data/spec/dmn/integration_spec.rb +226 -0
  47. data/spec/examples.txt +1909 -0
  48. data/spec/fixtures/dmn/complex_decision.dmn +81 -0
  49. data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
  50. data/spec/fixtures/dmn/simple_decision.dmn +40 -0
  51. data/spec/monitoring/metrics_collector_spec.rb +37 -35
  52. data/spec/monitoring/monitored_agent_spec.rb +14 -11
  53. data/spec/performance_optimizations_spec.rb +10 -3
  54. data/spec/thread_safety_spec.rb +10 -2
  55. data/spec/web_ui_rack_spec.rb +294 -0
  56. metadata +66 -1
@@ -0,0 +1,420 @@
1
+ require_relative "../errors"
2
+ require_relative "types"
3
+
4
+ module DecisionAgent
5
+ module Dmn
6
+ module Feel
7
+ # Built-in FEEL functions registry
8
+ # Functions either map to ConditionEvaluator operators or provide custom evaluation
9
+ module Functions
10
+ # Function registry
11
+ # rubocop:disable Style/MutableConstant
12
+ REGISTRY = {}
13
+ # rubocop:enable Style/MutableConstant
14
+
15
+ # Base class for all functions
16
+ class Base
17
+ class << self
18
+ # Register function with one or more names
19
+ def register(*names)
20
+ names.each { |name| REGISTRY[name.to_s] = self }
21
+ end
22
+
23
+ # Execute function with arguments and context
24
+ # Returns either a value or a ConditionEvaluator condition structure
25
+ def call(args, context = {})
26
+ raise NotImplementedError, "Subclasses must implement call"
27
+ end
28
+
29
+ # Validate argument count
30
+ def validate_arg_count(args, expected)
31
+ actual = args.length
32
+ valid = if expected.is_a?(Range)
33
+ expected.cover?(actual)
34
+ else
35
+ actual == expected
36
+ end
37
+
38
+ return if valid
39
+
40
+ raise FeelFunctionError, "Wrong number of arguments for #{name} (got #{actual}, expected #{expected})"
41
+ end
42
+ end
43
+ end
44
+
45
+ #
46
+ # STRING FUNCTIONS
47
+ #
48
+
49
+ class Substring < Base
50
+ register "substring", "substr"
51
+
52
+ def self.call(args, _context = {})
53
+ validate_arg_count(args, 2..3)
54
+ str = args[0].to_s
55
+ start_pos = args[1].to_i - 1 # FEEL is 1-indexed
56
+ length = args[2]&.to_i
57
+
58
+ return str[start_pos..] if length.nil?
59
+
60
+ str[start_pos, length] || ""
61
+ end
62
+ end
63
+
64
+ class StringLength < Base
65
+ register "string length", "length"
66
+
67
+ def self.call(args, _context = {})
68
+ validate_arg_count(args, 1)
69
+ args[0].to_s.length
70
+ end
71
+ end
72
+
73
+ class UpperCase < Base
74
+ register "upper case", "upper"
75
+
76
+ def self.call(args, _context = {})
77
+ validate_arg_count(args, 1)
78
+ args[0].to_s.upcase
79
+ end
80
+ end
81
+
82
+ class LowerCase < Base
83
+ register "lower case", "lower"
84
+
85
+ def self.call(args, _context = {})
86
+ validate_arg_count(args, 1)
87
+ args[0].to_s.downcase
88
+ end
89
+ end
90
+
91
+ class Contains < Base
92
+ register "contains"
93
+
94
+ def self.call(args, _context = {})
95
+ validate_arg_count(args, 2)
96
+ args[0].to_s.include?(args[1].to_s)
97
+ end
98
+ end
99
+
100
+ class StartsWith < Base
101
+ register "starts with"
102
+
103
+ def self.call(args, _context = {})
104
+ validate_arg_count(args, 2)
105
+ args[0].to_s.start_with?(args[1].to_s)
106
+ end
107
+ end
108
+
109
+ class EndsWith < Base
110
+ register "ends with"
111
+
112
+ def self.call(args, _context = {})
113
+ validate_arg_count(args, 2)
114
+ args[0].to_s.end_with?(args[1].to_s)
115
+ end
116
+ end
117
+
118
+ class SubstringBefore < Base
119
+ register "substring before"
120
+
121
+ def self.call(args, _context = {})
122
+ validate_arg_count(args, 2)
123
+ str = args[0].to_s
124
+ match = args[1].to_s
125
+ idx = str.index(match)
126
+ idx ? str[0...idx] : ""
127
+ end
128
+ end
129
+
130
+ class SubstringAfter < Base
131
+ register "substring after"
132
+
133
+ def self.call(args, _context = {})
134
+ validate_arg_count(args, 2)
135
+ str = args[0].to_s
136
+ match = args[1].to_s
137
+ idx = str.index(match)
138
+ idx ? str[(idx + match.length)..] : ""
139
+ end
140
+ end
141
+
142
+ class Replace < Base
143
+ register "replace"
144
+
145
+ def self.call(args, _context = {})
146
+ validate_arg_count(args, 3)
147
+ args[0].to_s.gsub(args[1].to_s, args[2].to_s)
148
+ end
149
+ end
150
+
151
+ #
152
+ # NUMERIC FUNCTIONS
153
+ #
154
+
155
+ class Abs < Base
156
+ register "abs", "absolute"
157
+
158
+ def self.call(args, _context = {})
159
+ validate_arg_count(args, 1)
160
+ args[0].to_f.abs
161
+ end
162
+ end
163
+
164
+ class Floor < Base
165
+ register "floor"
166
+
167
+ def self.call(args, _context = {})
168
+ validate_arg_count(args, 1)
169
+ args[0].to_f.floor
170
+ end
171
+ end
172
+
173
+ class Ceiling < Base
174
+ register "ceiling", "ceil"
175
+
176
+ def self.call(args, _context = {})
177
+ validate_arg_count(args, 1)
178
+ args[0].to_f.ceil
179
+ end
180
+ end
181
+
182
+ class Round < Base
183
+ register "round"
184
+
185
+ def self.call(args, _context = {})
186
+ validate_arg_count(args, 1..2)
187
+ value = args[0].to_f
188
+ precision = args[1].to_i
189
+ value.round(precision)
190
+ end
191
+ end
192
+
193
+ class Sqrt < Base
194
+ register "sqrt", "square root"
195
+
196
+ def self.call(args, _context = {})
197
+ validate_arg_count(args, 1)
198
+ Math.sqrt(args[0].to_f)
199
+ end
200
+ end
201
+
202
+ class Modulo < Base
203
+ register "modulo", "mod"
204
+
205
+ def self.call(args, _context = {})
206
+ validate_arg_count(args, 2)
207
+ args[0].to_f % args[1].to_f
208
+ end
209
+ end
210
+
211
+ class Odd < Base
212
+ register "odd"
213
+
214
+ def self.call(args, _context = {})
215
+ validate_arg_count(args, 1)
216
+ args[0].to_i.odd?
217
+ end
218
+ end
219
+
220
+ class Even < Base
221
+ register "even"
222
+
223
+ def self.call(args, _context = {})
224
+ validate_arg_count(args, 1)
225
+ args[0].to_i.even?
226
+ end
227
+ end
228
+
229
+ #
230
+ # LIST FUNCTIONS
231
+ #
232
+
233
+ class Count < Base
234
+ register "count"
235
+
236
+ def self.call(args, _context = {})
237
+ validate_arg_count(args, 1)
238
+ list = args[0]
239
+ return 0 unless list.is_a?(Array) || list.is_a?(Types::List)
240
+
241
+ list.length
242
+ end
243
+ end
244
+
245
+ class Sum < Base
246
+ register "sum"
247
+
248
+ def self.call(args, _context = {})
249
+ validate_arg_count(args, 1)
250
+ list = args[0]
251
+ return 0 unless list.is_a?(Array) || list.is_a?(Types::List)
252
+
253
+ list.map(&:to_f).sum
254
+ end
255
+ end
256
+
257
+ class Mean < Base
258
+ register "mean", "average"
259
+
260
+ def self.call(args, _context = {})
261
+ validate_arg_count(args, 1)
262
+ list = args[0]
263
+ return 0 unless list.is_a?(Array) || list.is_a?(Types::List)
264
+ return 0 if list.empty?
265
+
266
+ list.map(&:to_f).sum / list.length.to_f
267
+ end
268
+ end
269
+
270
+ class Min < Base
271
+ register "min", "minimum"
272
+
273
+ def self.call(args, _context = {})
274
+ # Can accept multiple args or a single list
275
+ values = args.length == 1 && (args[0].is_a?(Array) || args[0].is_a?(Types::List)) ? args[0] : args
276
+ return nil if values.empty?
277
+
278
+ values.map(&:to_f).min
279
+ end
280
+ end
281
+
282
+ class Max < Base
283
+ register "max", "maximum"
284
+
285
+ def self.call(args, _context = {})
286
+ # Can accept multiple args or a single list
287
+ values = args.length == 1 && (args[0].is_a?(Array) || args[0].is_a?(Types::List)) ? args[0] : args
288
+ return nil if values.empty?
289
+
290
+ values.map(&:to_f).max
291
+ end
292
+ end
293
+
294
+ class Append < Base
295
+ register "append"
296
+
297
+ def self.call(args, _context = {})
298
+ validate_arg_count(args, 2..)
299
+ list = Array(args[0])
300
+ items = args[1..]
301
+ list + items
302
+ end
303
+ end
304
+
305
+ class Reverse < Base
306
+ register "reverse"
307
+
308
+ def self.call(args, _context = {})
309
+ validate_arg_count(args, 1)
310
+ Array(args[0]).reverse
311
+ end
312
+ end
313
+
314
+ class IndexOf < Base
315
+ register "index of"
316
+
317
+ def self.call(args, _context = {})
318
+ validate_arg_count(args, 2)
319
+ list = Array(args[0])
320
+ match = args[1]
321
+ # FEEL is 1-indexed, Ruby is 0-indexed
322
+ idx = list.index(match)
323
+ idx ? idx + 1 : -1
324
+ end
325
+ end
326
+
327
+ class DistinctValues < Base
328
+ register "distinct values", "unique"
329
+
330
+ def self.call(args, _context = {})
331
+ validate_arg_count(args, 1)
332
+ Array(args[0]).uniq
333
+ end
334
+ end
335
+
336
+ #
337
+ # BOOLEAN FUNCTIONS
338
+ #
339
+
340
+ class Not < Base
341
+ register "not"
342
+
343
+ def self.call(args, _context = {})
344
+ validate_arg_count(args, 1)
345
+ !args[0]
346
+ end
347
+ end
348
+
349
+ class All < Base
350
+ register "all"
351
+
352
+ def self.call(args, _context = {})
353
+ validate_arg_count(args, 1)
354
+ list = Array(args[0])
355
+ list.all? { |item| [true, "true"].include?(item) }
356
+ end
357
+ end
358
+
359
+ class Any < Base
360
+ register "any"
361
+
362
+ def self.call(args, _context = {})
363
+ validate_arg_count(args, 1)
364
+ list = Array(args[0])
365
+ list.any? { |item| [true, "true"].include?(item) }
366
+ end
367
+ end
368
+
369
+ #
370
+ # DATE/TIME FUNCTIONS (Basic implementations)
371
+ #
372
+
373
+ class DateFunction < Base
374
+ register "date"
375
+
376
+ def self.call(args, _context = {})
377
+ validate_arg_count(args, 1)
378
+ Types::Date.new(args[0])
379
+ end
380
+ end
381
+
382
+ class TimeFunction < Base
383
+ register "time"
384
+
385
+ def self.call(args, _context = {})
386
+ validate_arg_count(args, 1)
387
+ Types::Time.new(args[0])
388
+ end
389
+ end
390
+
391
+ class DurationFunction < Base
392
+ register "duration"
393
+
394
+ def self.call(args, _context = {})
395
+ validate_arg_count(args, 1)
396
+ Types::Duration.parse(args[0])
397
+ end
398
+ end
399
+
400
+ # Lookup function by name
401
+ def self.get(name)
402
+ REGISTRY[name.to_s]
403
+ end
404
+
405
+ # Execute a function
406
+ def self.execute(name, args, context = {})
407
+ func = get(name)
408
+ raise FeelFunctionError, "Unknown function: #{name}" unless func
409
+
410
+ func.call(args, context)
411
+ end
412
+
413
+ # List all registered functions
414
+ def self.list
415
+ REGISTRY.keys.sort
416
+ end
417
+ end
418
+ end
419
+ end
420
+ end