circuit_breaker-wf 0.1.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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +52 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +116 -0
  6. data/LICENSE +21 -0
  7. data/README.md +324 -0
  8. data/examples/document/README.md +150 -0
  9. data/examples/document/document_assistant.rb +535 -0
  10. data/examples/document/document_rules.rb +60 -0
  11. data/examples/document/document_token.rb +83 -0
  12. data/examples/document/document_workflow.rb +114 -0
  13. data/examples/document/mock_executor.rb +80 -0
  14. data/lib/circuit_breaker/executors/README.md +664 -0
  15. data/lib/circuit_breaker/executors/agent_executor.rb +187 -0
  16. data/lib/circuit_breaker/executors/assistant_executor.rb +245 -0
  17. data/lib/circuit_breaker/executors/base_executor.rb +56 -0
  18. data/lib/circuit_breaker/executors/docker_executor.rb +56 -0
  19. data/lib/circuit_breaker/executors/dsl.rb +97 -0
  20. data/lib/circuit_breaker/executors/llm/memory.rb +82 -0
  21. data/lib/circuit_breaker/executors/llm/tools.rb +94 -0
  22. data/lib/circuit_breaker/executors/nats_executor.rb +230 -0
  23. data/lib/circuit_breaker/executors/serverless_executor.rb +25 -0
  24. data/lib/circuit_breaker/executors/step_executor.rb +47 -0
  25. data/lib/circuit_breaker/history.rb +81 -0
  26. data/lib/circuit_breaker/rules.rb +251 -0
  27. data/lib/circuit_breaker/templates/mermaid.html.erb +51 -0
  28. data/lib/circuit_breaker/templates/plantuml.html.erb +55 -0
  29. data/lib/circuit_breaker/token.rb +486 -0
  30. data/lib/circuit_breaker/visualizer.rb +173 -0
  31. data/lib/circuit_breaker/workflow_dsl.rb +359 -0
  32. data/lib/circuit_breaker.rb +236 -0
  33. data/workflow-editor/.gitignore +24 -0
  34. data/workflow-editor/README.md +106 -0
  35. data/workflow-editor/eslint.config.js +28 -0
  36. data/workflow-editor/index.html +13 -0
  37. data/workflow-editor/package-lock.json +6864 -0
  38. data/workflow-editor/package.json +50 -0
  39. data/workflow-editor/postcss.config.js +6 -0
  40. data/workflow-editor/public/vite.svg +1 -0
  41. data/workflow-editor/src/App.css +42 -0
  42. data/workflow-editor/src/App.tsx +365 -0
  43. data/workflow-editor/src/assets/react.svg +1 -0
  44. data/workflow-editor/src/components/AddNodeButton.tsx +68 -0
  45. data/workflow-editor/src/components/EdgeDetails.tsx +175 -0
  46. data/workflow-editor/src/components/NodeDetails.tsx +177 -0
  47. data/workflow-editor/src/components/ResizablePanel.tsx +74 -0
  48. data/workflow-editor/src/components/SaveButton.tsx +45 -0
  49. data/workflow-editor/src/config/change_workflow.yaml +59 -0
  50. data/workflow-editor/src/config/constants.ts +11 -0
  51. data/workflow-editor/src/config/flowConfig.ts +189 -0
  52. data/workflow-editor/src/config/uiConfig.ts +77 -0
  53. data/workflow-editor/src/config/workflow.yaml +58 -0
  54. data/workflow-editor/src/hooks/useKeyPress.ts +29 -0
  55. data/workflow-editor/src/index.css +34 -0
  56. data/workflow-editor/src/main.tsx +10 -0
  57. data/workflow-editor/src/server/saveWorkflow.ts +81 -0
  58. data/workflow-editor/src/utils/saveWorkflow.ts +92 -0
  59. data/workflow-editor/src/utils/workflowLoader.ts +26 -0
  60. data/workflow-editor/src/utils/workflowTransformer.ts +91 -0
  61. data/workflow-editor/src/vite-env.d.ts +1 -0
  62. data/workflow-editor/src/yaml.d.ts +4 -0
  63. data/workflow-editor/tailwind.config.js +15 -0
  64. data/workflow-editor/tsconfig.app.json +26 -0
  65. data/workflow-editor/tsconfig.json +7 -0
  66. data/workflow-editor/tsconfig.node.json +24 -0
  67. data/workflow-editor/vite.config.ts +8 -0
  68. metadata +267 -0
@@ -0,0 +1,251 @@
1
+ require 'date'
2
+ require 'uri'
3
+ require 'json-schema'
4
+
5
+ module CircuitBreaker
6
+ module Rules
7
+ class RuleError < StandardError; end
8
+ class RuleResult
9
+ attr_reader :valid, :errors
10
+
11
+ def initialize(valid, errors = [])
12
+ @valid = valid
13
+ @errors = Array(errors)
14
+ end
15
+
16
+ def valid?
17
+ @valid
18
+ end
19
+
20
+ def to_s
21
+ valid? ? "valid" : "invalid: #{errors.join(', ')}"
22
+ end
23
+
24
+ def &(other)
25
+ RuleResult.new(
26
+ valid? && other.valid?,
27
+ errors + other.errors
28
+ )
29
+ end
30
+
31
+ def |(other)
32
+ RuleResult.new(
33
+ valid? || other.valid?,
34
+ valid? || other.valid? ? [] : errors + other.errors
35
+ )
36
+ end
37
+ end
38
+
39
+ class DSL
40
+ def initialize
41
+ @rules = {}
42
+ @descriptions = {}
43
+ @context = nil
44
+ end
45
+
46
+ def self.define(&block)
47
+ dsl = new
48
+ dsl.instance_eval(&block) if block_given?
49
+ dsl
50
+ end
51
+
52
+ def rule(name, desc = nil, &block)
53
+ @rules[name] = block
54
+ @descriptions[name] = desc if desc
55
+ end
56
+
57
+ def evaluate(rule_name, token)
58
+ raise RuleError, "Unknown rule: #{rule_name}" unless @rules.key?(rule_name)
59
+
60
+ puts "Evaluating rule '#{rule_name}' for token #{token.id}"
61
+ puts " Context: #{@context.inspect}"
62
+ result = @rules[rule_name].call(token)
63
+ puts " Result: #{result.inspect}"
64
+ case result
65
+ when RuleResult
66
+ result.valid?
67
+ when true, false
68
+ result
69
+ else
70
+ !!result
71
+ end
72
+ rescue StandardError => e
73
+ raise RuleError, "Rule '#{rule_name}' failed for token #{token.id}: #{e.message}"
74
+ end
75
+
76
+ def chain(token)
77
+ RuleChain.new(self, token)
78
+ end
79
+
80
+ def rules
81
+ @rules.keys
82
+ end
83
+
84
+ def description(name)
85
+ @descriptions[name]
86
+ end
87
+
88
+ def with_context(context)
89
+ old_context = @context
90
+ @context = context
91
+ yield
92
+ ensure
93
+ @context = old_context
94
+ end
95
+
96
+ def context
97
+ @context
98
+ end
99
+
100
+ # Helper methods for common rule conditions
101
+ def presence(field)
102
+ ->(token) {
103
+ val = token.send(field)
104
+ result = !val.nil? && (!val.respond_to?(:empty?) || !val.empty?)
105
+ RuleResult.new(result, result ? [] : ["#{field} must be present"])
106
+ }
107
+ end
108
+
109
+ def different_values(field1, field2)
110
+ ->(token) {
111
+ val1, val2 = token.send(field1), token.send(field2)
112
+ result = presence(field1).call(token).valid? &&
113
+ presence(field2).call(token).valid? &&
114
+ val1 != val2
115
+ RuleResult.new(result, result ? [] : ["#{field1} must be different from #{field2}"])
116
+ }
117
+ end
118
+
119
+ def matches(field, pattern, message = nil)
120
+ ->(token) {
121
+ result = token.send(field).to_s.match?(pattern)
122
+ RuleResult.new(result, result ? [] : [message || "#{field} does not match pattern"])
123
+ }
124
+ end
125
+
126
+ def one_of(field, values)
127
+ ->(token) {
128
+ result = values.include?(token.send(field))
129
+ RuleResult.new(result, result ? [] : ["#{field} must be one of: #{values.join(', ')}"])
130
+ }
131
+ end
132
+
133
+ def length(field, options = {})
134
+ ->(token) {
135
+ val = token.send(field).to_s
136
+ min_valid = !options[:min] || val.length >= options[:min]
137
+ max_valid = !options[:max] || val.length <= options[:max]
138
+ result = min_valid && max_valid
139
+
140
+ errors = []
141
+ errors << "#{field} must be at least #{options[:min]} characters" if !min_valid
142
+ errors << "#{field} must be at most #{options[:max]} characters" if !max_valid
143
+
144
+ RuleResult.new(result, errors)
145
+ }
146
+ end
147
+
148
+ def json_schema(field, schema)
149
+ ->(token) {
150
+ begin
151
+ JSON::Validator.validate!(schema, token.send(field))
152
+ RuleResult.new(true)
153
+ rescue JSON::Schema::ValidationError => e
154
+ RuleResult.new(false, [e.message])
155
+ end
156
+ }
157
+ end
158
+
159
+ def numericality(field, options = {})
160
+ ->(token) {
161
+ val = token.send(field)
162
+ return RuleResult.new(false, ["#{field} must be a number"]) unless val.is_a?(Numeric)
163
+
164
+ errors = []
165
+ errors << "must be greater than #{options[:greater_than]}" if options[:greater_than] && !(val > options[:greater_than])
166
+ errors << "must be greater than or equal to #{options[:greater_than_or_equal_to]}" if options[:greater_than_or_equal_to] && !(val >= options[:greater_than_or_equal_to])
167
+ errors << "must be less than #{options[:less_than]}" if options[:less_than] && !(val < options[:less_than])
168
+ errors << "must be less than or equal to #{options[:less_than_or_equal_to]}" if options[:less_than_or_equal_to] && !(val <= options[:less_than_or_equal_to])
169
+ errors << "must be equal to #{options[:equal_to]}" if options[:equal_to] && val != options[:equal_to]
170
+ errors << "must not be equal to #{options[:other_than]}" if options[:other_than] && val == options[:other_than]
171
+
172
+ RuleResult.new(errors.empty?, errors.map { |e| "#{field} #{e}" })
173
+ }
174
+ end
175
+
176
+ def all(*rules)
177
+ ->(token) {
178
+ results = rules.map { |rule| rule.call(token) }
179
+ results.reduce(RuleResult.new(true)) { |acc, result| acc & (result.is_a?(RuleResult) ? result : RuleResult.new(result)) }
180
+ }
181
+ end
182
+
183
+ def any(*rules)
184
+ ->(token) {
185
+ results = rules.map { |rule| rule.call(token) }
186
+ results.reduce(RuleResult.new(false)) { |acc, result| acc | (result.is_a?(RuleResult) ? result : RuleResult.new(result)) }
187
+ }
188
+ end
189
+
190
+ def none(*rules)
191
+ ->(token) {
192
+ results = rules.map { |rule| rule.call(token) }
193
+ result = results.none? { |r| r.is_a?(RuleResult) ? r.valid? : r }
194
+ RuleResult.new(result, result ? [] : ["none of the rules should pass"])
195
+ }
196
+ end
197
+
198
+ def custom(field = nil, message = nil, &block)
199
+ if field
200
+ ->(token) {
201
+ result = block.call(token.send(field))
202
+ result.is_a?(RuleResult) ? result : RuleResult.new(result, message ? [message] : [])
203
+ }
204
+ else
205
+ ->(token) {
206
+ result = block.call(token)
207
+ result.is_a?(RuleResult) ? result : RuleResult.new(result, message ? [message] : [])
208
+ }
209
+ end
210
+ end
211
+
212
+ def depends_on(field, other_field, &block)
213
+ ->(token) {
214
+ other_value = token.send(other_field)
215
+ return RuleResult.new(true) if other_value.nil?
216
+
217
+ result = block.call(token.send(field), other_value)
218
+ result.is_a?(RuleResult) ? result : RuleResult.new(result, ["#{field} dependency on #{other_field} failed"])
219
+ }
220
+ end
221
+ end
222
+
223
+ class RuleChain
224
+ def initialize(dsl, token)
225
+ @dsl = dsl
226
+ @token = token
227
+ @result = RuleResult.new(true)
228
+ end
229
+
230
+ def requires(*rule_names)
231
+ results = rule_names.map { |rule| RuleResult.new(@dsl.evaluate(rule, @token)) }
232
+ @result = results.reduce(@result) { |acc, result| acc & result }
233
+ self
234
+ end
235
+
236
+ def requires_any(*rule_names)
237
+ results = rule_names.map { |rule| RuleResult.new(@dsl.evaluate(rule, @token)) }
238
+ @result = @result & results.reduce(RuleResult.new(false)) { |acc, result| acc | result }
239
+ self
240
+ end
241
+
242
+ def valid?
243
+ @result.valid?
244
+ end
245
+
246
+ def errors
247
+ @result.errors
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,51 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Workflow Visualization - Mermaid</title>
5
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
6
+ <style>
7
+ body {
8
+ font-family: Arial, sans-serif;
9
+ margin: 20px;
10
+ background-color: #f5f5f5;
11
+ }
12
+ .container {
13
+ max-width: 1200px;
14
+ margin: 0 auto;
15
+ background-color: white;
16
+ padding: 20px;
17
+ border-radius: 8px;
18
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
19
+ }
20
+ h1 {
21
+ color: #333;
22
+ text-align: center;
23
+ }
24
+ .diagram-container {
25
+ margin-top: 20px;
26
+ padding: 20px;
27
+ background-color: white;
28
+ border-radius: 4px;
29
+ }
30
+ </style>
31
+ <script>
32
+ mermaid.initialize({
33
+ startOnLoad: true,
34
+ theme: 'default',
35
+ flowchart: {
36
+ curve: 'basis'
37
+ }
38
+ });
39
+ </script>
40
+ </head>
41
+ <body>
42
+ <div class="container">
43
+ <h1>Workflow State Machine</h1>
44
+ <div class="diagram-container">
45
+ <div class="mermaid">
46
+ <%= diagram %>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </body>
51
+ </html>
@@ -0,0 +1,55 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Workflow Visualization - PlantUML</title>
5
+ <style>
6
+ body {
7
+ font-family: Arial, sans-serif;
8
+ margin: 20px;
9
+ background-color: #f5f5f5;
10
+ }
11
+ .container {
12
+ max-width: 1200px;
13
+ margin: 0 auto;
14
+ background-color: white;
15
+ padding: 20px;
16
+ border-radius: 8px;
17
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
18
+ }
19
+ h1 {
20
+ color: #333;
21
+ text-align: center;
22
+ }
23
+ .diagram-container {
24
+ margin-top: 20px;
25
+ padding: 20px;
26
+ background-color: white;
27
+ border-radius: 4px;
28
+ text-align: center;
29
+ }
30
+ pre {
31
+ text-align: left;
32
+ background-color: #f8f8f8;
33
+ padding: 15px;
34
+ border-radius: 4px;
35
+ overflow-x: auto;
36
+ }
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div class="container">
41
+ <h1>Workflow State Machine</h1>
42
+ <div class="diagram-container">
43
+ <pre><code><%= diagram %></code></pre>
44
+ <p>
45
+ <em>Note: To view this diagram, copy the PlantUML code above and paste it into a PlantUML editor or use the PlantUML server.</em>
46
+ </p>
47
+ <p>
48
+ <a href="http://www.plantuml.com/plantuml/uml/<%= [diagram].pack('m0') %>" target="_blank">
49
+ View on PlantUML Server
50
+ </a>
51
+ </p>
52
+ </div>
53
+ </div>
54
+ </body>
55
+ </html>