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,359 @@
1
+ module CircuitBreaker
2
+ module WorkflowBuilder
3
+ module DSL
4
+ class ::Symbol
5
+ def >>(other)
6
+ StateTransition.new(self, other)
7
+ end
8
+ end
9
+
10
+ class StateTransition
11
+ attr_reader :from, :to
12
+
13
+ def initialize(from, to)
14
+ @from = from
15
+ @to = to
16
+ end
17
+
18
+ def >>(other)
19
+ StateTransition.new(from, other)
20
+ end
21
+
22
+ def to_s
23
+ "#{from} -> #{to}"
24
+ end
25
+ end
26
+
27
+ class ActionContext
28
+ attr_reader :results
29
+
30
+ def initialize
31
+ @results = {}
32
+ end
33
+
34
+ def add_result(name, result)
35
+ @results[name] = result if name
36
+ end
37
+
38
+ def get_result(name)
39
+ @results[name]
40
+ end
41
+
42
+ def method_missing(name, *args)
43
+ @results[name]
44
+ end
45
+
46
+ def respond_to_missing?(name, include_private = false)
47
+ @results.key?(name) || super
48
+ end
49
+ end
50
+
51
+ class ActionBuilder
52
+ attr_reader :context, :actions
53
+
54
+ def initialize(workflow_builder)
55
+ @workflow_builder = workflow_builder
56
+ @context = ActionContext.new
57
+ @actions = []
58
+ end
59
+
60
+ def execute(executor, method, result_name = nil, **params)
61
+ @actions << [executor, method, result_name, params]
62
+ end
63
+
64
+ def execute_actions(token)
65
+ puts "Executing actions for token #{token.id}..."
66
+ @actions.each do |executor, method, result_name, params|
67
+ puts " Executing #{method} -> #{result_name}"
68
+ result = executor.send(method, token, **params)
69
+ puts " Result: #{result.inspect}"
70
+ @context.add_result(result_name, result) if result_name
71
+ end
72
+ end
73
+ end
74
+
75
+ class StateTransitionBuilder
76
+ def initialize(workflow_builder, state_transition)
77
+ @workflow_builder = workflow_builder
78
+ @from_state = state_transition.from
79
+ @to_state = state_transition.to
80
+ end
81
+
82
+ def transition(name)
83
+ @workflow_builder.transition(name, from: @from_state, to: @to_state)
84
+ end
85
+
86
+ def policy(rules)
87
+ @workflow_builder.policy(rules)
88
+ end
89
+
90
+ def actions(&block)
91
+ builder = ActionBuilder.new(@workflow_builder)
92
+ builder.instance_eval(&block)
93
+ @workflow_builder.set_action_context(builder.context)
94
+ end
95
+ end
96
+
97
+ class WorkflowBuilder
98
+ attr_reader :states, :transitions, :before_flows, :rules, :current_token
99
+
100
+ def initialize(rules = nil)
101
+ @states = []
102
+ @transitions = []
103
+ @before_flows = []
104
+ @rules = rules || Rules::DSL.define
105
+ @current_token = nil
106
+ end
107
+
108
+ def states(*states)
109
+ @states = states
110
+ end
111
+
112
+ def flow(transition_spec, name = nil, &block)
113
+ from_state, to_state = parse_transition_spec(transition_spec)
114
+ raise "Invalid transition spec" unless from_state && to_state
115
+
116
+ transition = Transition.new(name || "#{from_state}_to_#{to_state}", from_state, to_state, self)
117
+ transition.instance_eval(&block) if block_given?
118
+ @transitions << transition
119
+ transition
120
+ end
121
+
122
+ def before_flow(&block)
123
+ @before_flows << block
124
+ end
125
+
126
+ def find_transition(name, from_state = nil)
127
+ @transitions.find do |t|
128
+ t.name == name && (from_state.nil? || t.from_state == from_state)
129
+ end
130
+ end
131
+
132
+ def transition!(token, transition_name)
133
+ transition = @transitions.find { |t| t.name == transition_name }
134
+ raise "Invalid transition '#{transition_name}'" unless transition
135
+ raise "Invalid from state '#{token.state}' (expected #{transition.from_state})" unless token.state.to_s == transition.from_state.to_s
136
+
137
+ old_state = token.state
138
+ @current_token = token # Set current token before executing actions
139
+
140
+ begin
141
+ # Execute actions first
142
+ transition.execute_actions(token)
143
+
144
+ # Then validate rules
145
+ transition.validate_rules(token, @rules)
146
+
147
+ # Finally update state
148
+ token.state = transition.to_state.to_sym
149
+ token.record_transition(transition_name, old_state, token.state)
150
+
151
+ token
152
+ ensure
153
+ @current_token = nil # Clear current token after execution
154
+ end
155
+ end
156
+
157
+ def build
158
+ self # Return self since we're already a workflow
159
+ end
160
+
161
+ def pretty_print
162
+ puts "States:"
163
+ puts " #{@states.join(' -> ')}\n\n"
164
+
165
+ puts "Transitions:"
166
+ @transitions.each do |t|
167
+ puts " #{t.name}: #{t.from_state} -> #{t.to_state}"
168
+ puts " Rules:"
169
+ if t.rules.is_a?(Hash)
170
+ if t.rules[:all]
171
+ puts " All of:"
172
+ t.rules[:all].each { |r| puts " - #{r}" }
173
+ end
174
+ if t.rules[:any]
175
+ puts " Any of:"
176
+ t.rules[:any].each { |r| puts " - #{r}" }
177
+ end
178
+ else
179
+ t.rules.each { |r| puts " - #{r}" }
180
+ end
181
+ puts " Actions:"
182
+ if t.action_builder&.actions
183
+ t.action_builder.actions.each do |executor, method, result_name, params|
184
+ result = executor.send(method, @current_token, **params) if @current_token
185
+ puts " #{result_name}: #{result.inspect}"
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ private
192
+
193
+ def parse_transition_spec(spec)
194
+ case spec
195
+ when String
196
+ spec.split(">>").map(&:strip)
197
+ when Symbol
198
+ [spec.to_s]
199
+ when StateTransition
200
+ [spec.from.to_s, spec.to.to_s]
201
+ else
202
+ raise "Invalid transition spec: #{spec}"
203
+ end
204
+ end
205
+ end
206
+
207
+ class TransitionBuilder
208
+ attr_accessor :name, :from_state, :to_state, :rules, :action_context
209
+
210
+ def initialize(name, from_state, to_state)
211
+ @name = name
212
+ @from_state = from_state
213
+ @to_state = to_state
214
+ @rules = []
215
+ @action_context = nil
216
+ end
217
+
218
+ def build
219
+ Transition.new(
220
+ name: @name,
221
+ from_state: @from_state,
222
+ to_state: @to_state,
223
+ rules: @rules,
224
+ action_context: @action_context
225
+ )
226
+ end
227
+ end
228
+
229
+ class Transition
230
+ attr_reader :name, :from_state, :to_state, :rules, :workflow_builder
231
+ attr_accessor :action_context, :action_builder
232
+
233
+ def initialize(name, from_state, to_state, workflow_builder)
234
+ @name = name
235
+ @from_state = from_state
236
+ @to_state = to_state
237
+ @rules = []
238
+ @action_context = nil
239
+ @action_builder = nil
240
+ @workflow_builder = workflow_builder
241
+ end
242
+
243
+ def actions(&block)
244
+ @action_builder = ActionBuilder.new(@workflow_builder)
245
+ @action_builder.instance_eval(&block) if block_given?
246
+ @action_context = @action_builder.context
247
+ end
248
+
249
+ def policy(rules)
250
+ @rules = rules
251
+ end
252
+
253
+ def execute_actions(token)
254
+ @action_builder&.execute_actions(token)
255
+ end
256
+
257
+ def validate_rules(token, rules_dsl)
258
+ return unless @rules
259
+
260
+ rules_dsl.with_context(@action_context) do
261
+ # Handle :all rules
262
+ if @rules[:all]
263
+ @rules[:all].each do |rule|
264
+ raise "Rule '#{rule}' failed for transition '#{name}'" unless rules_dsl.evaluate(rule, token)
265
+ end
266
+ end
267
+
268
+ # Handle :any rules
269
+ if @rules[:any]
270
+ any_passed = @rules[:any].any? { |rule| rules_dsl.evaluate(rule, token) }
271
+ raise "None of the rules #{@rules[:any]} passed for transition '#{name}'" unless any_passed
272
+ end
273
+ end
274
+ end
275
+ end
276
+
277
+ module PrettyPrint
278
+ def pretty_print
279
+ puts "States:"
280
+ puts " #{@states.join(' -> ')}"
281
+ puts "\nTransitions:"
282
+ @transitions.each do |transition|
283
+ puts " #{transition.name}: #{transition.from_state} -> #{transition.to_state}"
284
+ if transition.rules && !transition.rules.empty?
285
+ puts " Rules:"
286
+ if transition.rules[:all]
287
+ puts " All of:"
288
+ transition.rules[:all].each { |rule| puts " - #{rule}" }
289
+ end
290
+ if transition.rules[:any]
291
+ puts " Any of:"
292
+ transition.rules[:any].each { |rule| puts " - #{rule}" }
293
+ end
294
+ end
295
+ if transition.action_context && !transition.action_context.results.empty?
296
+ puts " Actions:"
297
+ transition.action_context.results.each do |name, result|
298
+ puts " #{name}: #{result}"
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ class Workflow
306
+ include PrettyPrint
307
+
308
+ attr_reader :states, :transitions, :before_flows, :rules, :tokens
309
+
310
+ def initialize(states:, transitions:, before_flows:, rules:)
311
+ @states = states
312
+ @transitions = transitions
313
+ @before_flows = before_flows
314
+ @rules = rules
315
+ @tokens = []
316
+ end
317
+
318
+ def add_token(token)
319
+ token.state = @states.first if token.state.nil?
320
+ @tokens << token
321
+ end
322
+
323
+ def transition!(token, transition_name)
324
+ transition = @transitions.find { |t| t.name == transition_name }
325
+ raise "Invalid transition '#{transition_name}'" unless transition
326
+ raise "Invalid from state '#{token.state}'" unless token.state == transition.from_state
327
+
328
+ old_state = token.state
329
+ @current_token = token # Set current token
330
+
331
+ # Then validate rules
332
+ transition.validate_rules(token, @rules)
333
+
334
+ # Execute actions first
335
+ transition.execute_actions(token)
336
+
337
+ # Finally update state
338
+ token.state = transition.to_state
339
+ token.record_transition(transition_name, old_state, token.state)
340
+
341
+ @current_token = nil # Clear current token
342
+ token
343
+ end
344
+
345
+ private
346
+
347
+ def find_transition(name, current_state)
348
+ @transitions.find { |t| t.name == name && t.from_state == current_state }
349
+ end
350
+ end
351
+
352
+ def self.define(rules: nil, &block)
353
+ builder = WorkflowBuilder.new(rules)
354
+ builder.instance_eval(&block)
355
+ builder.build
356
+ end
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,236 @@
1
+ require 'concurrent'
2
+ require 'json'
3
+ require 'securerandom'
4
+ require 'set'
5
+ require_relative 'circuit_breaker/version'
6
+ require_relative 'circuit_breaker/token'
7
+ require_relative 'circuit_breaker/workflow_dsl'
8
+ require_relative 'circuit_breaker/rules'
9
+
10
+ # Module for implementing a Circuit Breaker pattern using a Petri net
11
+ module CircuitBreaker
12
+ # Represents a place in the Petri net workflow
13
+ # Places can hold tokens and connect to transitions via arcs
14
+ class Place
15
+ # @!attribute [r] name
16
+ # @return [Symbol] Name of the place
17
+ attr_reader :name
18
+
19
+ # @!attribute [r] tokens
20
+ # @return [Array<Token>] Tokens currently in this place
21
+ attr_reader :tokens
22
+
23
+ # @!attribute [r] input_arcs
24
+ # @return [Array<Arc>] Input arcs connected to this place
25
+ attr_reader :input_arcs
26
+
27
+ # @!attribute [r] output_arcs
28
+ # @return [Array<Arc>] Output arcs connected to this place
29
+ attr_reader :output_arcs
30
+
31
+ # Initialize a new place with a given name
32
+ # @param name [Symbol] Name of the place
33
+ def initialize(name)
34
+ @name = name
35
+ @tokens = []
36
+ @input_arcs = []
37
+ @output_arcs = []
38
+ @mutex = Mutex.new
39
+ end
40
+
41
+ # Add a token to this place
42
+ # @param token [Token] Token to add
43
+ def add_token(token)
44
+ @mutex.synchronize { @tokens << token }
45
+ end
46
+
47
+ # Remove and return the most recently added token
48
+ # @return [Token, nil] The removed token or nil if no tokens
49
+ def remove_token
50
+ @mutex.synchronize { @tokens.pop }
51
+ end
52
+
53
+ # Get the current number of tokens in this place
54
+ # @return [Integer] Number of tokens
55
+ def token_count
56
+ @tokens.size
57
+ end
58
+
59
+ # Add an input arc from a source to this place
60
+ # @param source [Transition] Source transition
61
+ # @param weight [Integer] Weight of the arc (default: 1)
62
+ # @return [Arc] The created arc
63
+ def add_input_arc(source, weight = 1)
64
+ arc = Arc.new(source, self, weight)
65
+ @input_arcs << arc
66
+ arc
67
+ end
68
+
69
+ # Add an output arc from this place to a target
70
+ # @param target [Transition] Target transition
71
+ # @param weight [Integer] Weight of the arc (default: 1)
72
+ # @return [Arc] The created arc
73
+ def add_output_arc(target, weight = 1)
74
+ arc = Arc.new(self, target, weight)
75
+ @output_arcs << arc
76
+ arc
77
+ end
78
+ end
79
+
80
+ # Represents an arc connecting places and transitions in the Petri net
81
+ # Arcs can have weights to specify how many tokens are required/produced
82
+ class Arc
83
+ # @!attribute [r] weight
84
+ # @return [Integer] Weight of the arc
85
+ attr_reader :weight
86
+
87
+ # @!attribute [r] source
88
+ # @return [Place, Transition] Source node
89
+ attr_reader :source
90
+
91
+ # @!attribute [r] target
92
+ # @return [Place, Transition] Target node
93
+ attr_reader :target
94
+
95
+ # Initialize a new arc
96
+ # @param source [Place, Transition] Source node
97
+ # @param target [Place, Transition] Target node
98
+ # @param weight [Integer] Weight of the arc (default: 1)
99
+ def initialize(source, target, weight = 1)
100
+ @source = source
101
+ @target = target
102
+ @weight = weight
103
+ end
104
+
105
+ # Check if this arc is enabled (can fire)
106
+ # @return [Boolean] True if the arc can fire
107
+ def enabled?
108
+ case source
109
+ when Place
110
+ source.token_count >= weight
111
+ when Transition
112
+ true # Transitions can always produce tokens
113
+ else
114
+ false
115
+ end
116
+ end
117
+ end
118
+
119
+ # Represents a transition in the Petri net workflow
120
+ # Transitions connect places and can have guard conditions
121
+ class Transition
122
+ attr_reader :name, :from_state, :to_state, :input_arcs, :output_arcs, :guard
123
+
124
+ def initialize(name:, from:, to:)
125
+ @name = name
126
+ @from_state = from
127
+ @to_state = to
128
+ @input_arcs = []
129
+ @output_arcs = []
130
+ @guard = nil
131
+ end
132
+
133
+ def add_input_arc(place, weight = 1)
134
+ arc = Arc.new(place, self, weight)
135
+ @input_arcs << arc
136
+ arc
137
+ end
138
+
139
+ def add_output_arc(place, weight = 1)
140
+ arc = Arc.new(self, place, weight)
141
+ @output_arcs << arc
142
+ arc
143
+ end
144
+
145
+ def set_guard(&block)
146
+ @guard = block
147
+ end
148
+
149
+ def enabled?
150
+ return false unless @guard.nil? || @guard.call
151
+ @input_arcs.all?(&:enabled?) && @output_arcs.all?(&:enabled?)
152
+ end
153
+
154
+ def can_fire?(token)
155
+ return true unless @guard
156
+ @guard.call(token)
157
+ end
158
+ end
159
+
160
+ # Main workflow class that manages the Petri net
161
+ # Can be created from a configuration or built programmatically
162
+ class Workflow
163
+ attr_reader :places, :transitions, :tokens, :rules
164
+
165
+ def initialize(states: [], transitions: {}, before_flows: [], rules: [])
166
+ @places = {}
167
+ @transitions = {}
168
+ @tokens = Set.new
169
+ @rules = rules
170
+ @before_flows = before_flows
171
+
172
+ self.states = states
173
+ transitions.each do |transition, data|
174
+ add_transition(transition, data[:from], data[:to])
175
+ @transitions[transition].set_guard do |token|
176
+ data[:rules]&.each { |rule| return false unless @rules.evaluate(rule, token) }
177
+ true
178
+ end
179
+ end
180
+ end
181
+
182
+ def states=(state_list)
183
+ state_list.each do |state|
184
+ @places[state] = Place.new(state)
185
+ end
186
+ end
187
+
188
+ def add_token(token)
189
+ token.state = @places.keys.first if token.state.nil?
190
+ @tokens.add(token)
191
+ @places[token.state].add_token(token)
192
+ end
193
+
194
+ def fire_transition(transition_name, token)
195
+ transition = @transitions[transition_name]
196
+ raise "Invalid transition: #{transition_name}" unless transition
197
+ raise "Invalid state: #{token.state}" unless @places[token.state]
198
+ raise "Token not in workflow" unless @tokens.include?(token)
199
+
200
+ # Run any before_flow blocks
201
+ @before_flows.each { |block| block.call(token) }
202
+
203
+ # Check if transition is valid
204
+ unless transition.from_state == token.state
205
+ raise "Cannot fire transition '#{transition_name}' from state '#{token.state}'"
206
+ end
207
+
208
+ # Try to fire the transition
209
+ unless transition.can_fire?(token)
210
+ raise "Transition '#{transition_name}' cannot fire"
211
+ end
212
+
213
+ # Move token to new state
214
+ @places[token.state].remove_token
215
+ old_state = token.state
216
+ token.state = transition.to_state
217
+ @places[token.state].add_token(token)
218
+
219
+ # Record the transition
220
+ token.record_transition(transition_name, old_state, token.state)
221
+
222
+ token
223
+ end
224
+
225
+ private
226
+
227
+ def add_transition(name, from, to)
228
+ transition = Transition.new(name: name, from: from, to: to)
229
+ @transitions[name] = transition
230
+
231
+ # Add arcs
232
+ @places[from].add_output_arc(transition)
233
+ transition.add_output_arc(@places[to])
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,24 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?