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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +116 -0
- data/LICENSE +21 -0
- data/README.md +324 -0
- data/examples/document/README.md +150 -0
- data/examples/document/document_assistant.rb +535 -0
- data/examples/document/document_rules.rb +60 -0
- data/examples/document/document_token.rb +83 -0
- data/examples/document/document_workflow.rb +114 -0
- data/examples/document/mock_executor.rb +80 -0
- data/lib/circuit_breaker/executors/README.md +664 -0
- data/lib/circuit_breaker/executors/agent_executor.rb +187 -0
- data/lib/circuit_breaker/executors/assistant_executor.rb +245 -0
- data/lib/circuit_breaker/executors/base_executor.rb +56 -0
- data/lib/circuit_breaker/executors/docker_executor.rb +56 -0
- data/lib/circuit_breaker/executors/dsl.rb +97 -0
- data/lib/circuit_breaker/executors/llm/memory.rb +82 -0
- data/lib/circuit_breaker/executors/llm/tools.rb +94 -0
- data/lib/circuit_breaker/executors/nats_executor.rb +230 -0
- data/lib/circuit_breaker/executors/serverless_executor.rb +25 -0
- data/lib/circuit_breaker/executors/step_executor.rb +47 -0
- data/lib/circuit_breaker/history.rb +81 -0
- data/lib/circuit_breaker/rules.rb +251 -0
- data/lib/circuit_breaker/templates/mermaid.html.erb +51 -0
- data/lib/circuit_breaker/templates/plantuml.html.erb +55 -0
- data/lib/circuit_breaker/token.rb +486 -0
- data/lib/circuit_breaker/visualizer.rb +173 -0
- data/lib/circuit_breaker/workflow_dsl.rb +359 -0
- data/lib/circuit_breaker.rb +236 -0
- data/workflow-editor/.gitignore +24 -0
- data/workflow-editor/README.md +106 -0
- data/workflow-editor/eslint.config.js +28 -0
- data/workflow-editor/index.html +13 -0
- data/workflow-editor/package-lock.json +6864 -0
- data/workflow-editor/package.json +50 -0
- data/workflow-editor/postcss.config.js +6 -0
- data/workflow-editor/public/vite.svg +1 -0
- data/workflow-editor/src/App.css +42 -0
- data/workflow-editor/src/App.tsx +365 -0
- data/workflow-editor/src/assets/react.svg +1 -0
- data/workflow-editor/src/components/AddNodeButton.tsx +68 -0
- data/workflow-editor/src/components/EdgeDetails.tsx +175 -0
- data/workflow-editor/src/components/NodeDetails.tsx +177 -0
- data/workflow-editor/src/components/ResizablePanel.tsx +74 -0
- data/workflow-editor/src/components/SaveButton.tsx +45 -0
- data/workflow-editor/src/config/change_workflow.yaml +59 -0
- data/workflow-editor/src/config/constants.ts +11 -0
- data/workflow-editor/src/config/flowConfig.ts +189 -0
- data/workflow-editor/src/config/uiConfig.ts +77 -0
- data/workflow-editor/src/config/workflow.yaml +58 -0
- data/workflow-editor/src/hooks/useKeyPress.ts +29 -0
- data/workflow-editor/src/index.css +34 -0
- data/workflow-editor/src/main.tsx +10 -0
- data/workflow-editor/src/server/saveWorkflow.ts +81 -0
- data/workflow-editor/src/utils/saveWorkflow.ts +92 -0
- data/workflow-editor/src/utils/workflowLoader.ts +26 -0
- data/workflow-editor/src/utils/workflowTransformer.ts +91 -0
- data/workflow-editor/src/vite-env.d.ts +1 -0
- data/workflow-editor/src/yaml.d.ts +4 -0
- data/workflow-editor/tailwind.config.js +15 -0
- data/workflow-editor/tsconfig.app.json +26 -0
- data/workflow-editor/tsconfig.json +7 -0
- data/workflow-editor/tsconfig.node.json +24 -0
- data/workflow-editor/vite.config.ts +8 -0
- 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>
|