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,82 @@
1
+ module CircuitBreaker
2
+ module Executors
3
+ module LLM
4
+ class Memory
5
+ attr_reader :messages, :metadata
6
+
7
+ def initialize
8
+ @messages = []
9
+ @metadata = {}
10
+ end
11
+
12
+ def add_message(role:, content:, metadata: {})
13
+ message = {
14
+ role: role,
15
+ content: content,
16
+ timestamp: Time.now.utc,
17
+ metadata: metadata
18
+ }
19
+ @messages << message
20
+ message
21
+ end
22
+
23
+ def get_context(window_size: nil)
24
+ messages_to_return = window_size ? @messages.last(window_size) : @messages
25
+ messages_to_return.map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n")
26
+ end
27
+
28
+ def clear
29
+ @messages.clear
30
+ @metadata.clear
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ messages: @messages,
36
+ metadata: @metadata
37
+ }
38
+ end
39
+ end
40
+
41
+ class ConversationMemory < Memory
42
+ def initialize(system_prompt: nil)
43
+ super()
44
+ add_message(role: 'system', content: system_prompt) if system_prompt
45
+ end
46
+
47
+ def add_user_message(content, metadata: {})
48
+ add_message(role: 'user', content: content, metadata: metadata)
49
+ end
50
+
51
+ def add_assistant_message(content, metadata: {})
52
+ add_message(role: 'assistant', content: content, metadata: metadata)
53
+ end
54
+ end
55
+
56
+ class ChainMemory < Memory
57
+ def initialize
58
+ super
59
+ @intermediate_steps = []
60
+ end
61
+
62
+ def add_step_result(step_name:, input:, output:, metadata: {})
63
+ @intermediate_steps << {
64
+ step_name: step_name,
65
+ input: input,
66
+ output: output,
67
+ timestamp: Time.now.utc,
68
+ metadata: metadata
69
+ }
70
+ end
71
+
72
+ def get_step_history
73
+ @intermediate_steps
74
+ end
75
+
76
+ def to_h
77
+ super.merge(intermediate_steps: @intermediate_steps)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,94 @@
1
+ require 'json'
2
+
3
+ module CircuitBreaker
4
+ module Executors
5
+ module LLM
6
+ class Tool
7
+ attr_reader :name, :description, :parameters
8
+
9
+ def initialize(name:, description:, parameters: {})
10
+ @name = name
11
+ @description = description
12
+ @parameters = parameters
13
+ end
14
+
15
+ def execute(**args)
16
+ raise NotImplementedError, "#{self.class} must implement #execute"
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ name: @name,
22
+ description: @description,
23
+ parameters: @parameters
24
+ }
25
+ end
26
+ end
27
+
28
+ class ToolKit
29
+ def initialize
30
+ @tools = {}
31
+ end
32
+
33
+ def add_tool(tool)
34
+ @tools[tool.name] = tool
35
+ end
36
+
37
+ def get_tool(name)
38
+ @tools[name]
39
+ end
40
+
41
+ def available_tools
42
+ @tools.values
43
+ end
44
+
45
+ def tool_descriptions
46
+ @tools.values.map(&:to_h)
47
+ end
48
+
49
+ def execute_tool(name, **args)
50
+ tool = get_tool(name)
51
+ raise "Tool '#{name}' not found" unless tool
52
+ tool.execute(**args)
53
+ end
54
+ end
55
+
56
+ # Example built-in tools
57
+ class SearchTool < Tool
58
+ def initialize
59
+ super(
60
+ name: 'search',
61
+ description: 'Search for information on a given topic',
62
+ parameters: {
63
+ query: { type: 'string', description: 'The search query' }
64
+ }
65
+ )
66
+ end
67
+
68
+ def execute(query:)
69
+ # Implement actual search logic here
70
+ { results: "Search results for: #{query}" }
71
+ end
72
+ end
73
+
74
+ class CalculatorTool < Tool
75
+ def initialize
76
+ super(
77
+ name: 'calculator',
78
+ description: 'Perform mathematical calculations',
79
+ parameters: {
80
+ expression: { type: 'string', description: 'The mathematical expression to evaluate' }
81
+ }
82
+ )
83
+ end
84
+
85
+ def execute(expression:)
86
+ # Implement safe evaluation logic here
87
+ { result: eval(expression).to_s }
88
+ rescue => e
89
+ { error: e.message }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,230 @@
1
+ require 'nats/client'
2
+ require 'securerandom'
3
+ require 'json'
4
+ require_relative 'base_executor'
5
+
6
+ module CircuitBreaker
7
+ module Executors
8
+ # Executor class that uses NATS for workflow event distribution
9
+ # Handles workflow state management and event processing
10
+ class NatsExecutor < BaseExecutor
11
+ # Reader for workflow ID
12
+ attr_reader :workflow_id
13
+ # Reader for NATS client
14
+ attr_reader :nats
15
+
16
+ # Initialize a new NATS executor
17
+ # @param context [Hash] Initialization context
18
+ # @option context [String] :nats_url URL of the NATS server (default: 'nats://localhost:4222')
19
+ def initialize(context = {})
20
+ super
21
+ @nats_url = context[:nats_url] || 'nats://localhost:4222'
22
+ @nats = NATS.connect(@nats_url)
23
+ setup_jetstream
24
+ end
25
+
26
+ def execute
27
+ return unless @context[:petri_net]
28
+
29
+ workflow_id = create_workflow(
30
+ @context[:petri_net],
31
+ workflow_id: @context[:workflow_id]
32
+ )
33
+
34
+ @result = {
35
+ workflow_id: workflow_id,
36
+ status: 'completed'
37
+ }
38
+ end
39
+
40
+ private
41
+
42
+ # Set up JetStream streams for workflow data
43
+ # Creates streams for workflow state, events, and current states
44
+ def setup_jetstream
45
+ @js = @nats.jetstream
46
+
47
+ # Create streams for workflow state and events
48
+ @js.add_stream(name: 'WORKFLOWS', subjects: ['workflow.*'])
49
+ @js.add_stream(name: 'WORKFLOW_EVENTS', subjects: ['event.*'])
50
+ @js.add_stream(name: 'WORKFLOW_STATES', subjects: ['state.*'])
51
+ end
52
+
53
+ # Create a new workflow instance
54
+ # @param petri_net [Workflow] Workflow definition
55
+ # @param workflow_id [String, nil] Optional workflow ID
56
+ # @return [String] Workflow ID
57
+ def create_workflow(petri_net, workflow_id: nil)
58
+ @workflow_id = workflow_id || SecureRandom.uuid
59
+ puts "\n[Workflow] Creating new workflow with ID: #{@workflow_id}"
60
+
61
+ # Store initial workflow state
62
+ @js.publish("workflow.#{@workflow_id}",
63
+ petri_net.to_json,
64
+ headers: { 'Nats-Msg-Type' => 'workflow.create' }
65
+ )
66
+
67
+ # Subscribe to workflow events
68
+ @nats.subscribe("event.#{@workflow_id}") do |msg, reply, subject|
69
+ handle_event(msg)
70
+ end
71
+
72
+ @workflow_id
73
+ end
74
+
75
+ # Handle incoming workflow events
76
+ # @param msg [String] JSON-encoded event message
77
+ def handle_event(msg)
78
+ event = JSON.parse(msg)
79
+ puts "\n[Event] Received event: #{event['type']}"
80
+
81
+ case event['type']
82
+ when 'token_added'
83
+ handle_token_added(event)
84
+ when 'transition_fired'
85
+ handle_transition_fired(event)
86
+ when 'workflow_completed'
87
+ handle_workflow_completed(event)
88
+ end
89
+ end
90
+
91
+ # Handle token added event
92
+ # @param event [Hash] Event data
93
+ def handle_token_added(event)
94
+ puts "\n[Handler] Token added to place: #{event['place']} with data: #{event['data'].inspect}"
95
+ # Publish state change
96
+ @js.publish("state.#{@workflow_id}",
97
+ event.to_json,
98
+ headers: { 'Nats-Msg-Type' => 'state.token_added' }
99
+ )
100
+
101
+ # Check if any serverless functions need to be triggered
102
+ check_and_trigger_functions(event['place'])
103
+ end
104
+
105
+ # Handle transition fired event
106
+ # @param event [Hash] Event data
107
+ def handle_transition_fired(event)
108
+ puts "\n[Handler] Transition fired: #{event['transition']}"
109
+ @js.publish("state.#{@workflow_id}",
110
+ event.to_json,
111
+ headers: { 'Nats-Msg-Type' => 'state.transition_fired' }
112
+ )
113
+ end
114
+
115
+ # Handle workflow completed event
116
+ # @param event [Hash] Event data
117
+ def handle_workflow_completed(event)
118
+ puts "\n[Handler] Workflow completed#{event['next_workflow'] ? ' with next workflow configured' : ''}"
119
+ @js.publish("state.#{@workflow_id}",
120
+ event.to_json,
121
+ headers: { 'Nats-Msg-Type' => 'state.completed' }
122
+ )
123
+
124
+ # If there's a next workflow to trigger
125
+ if event['next_workflow']
126
+ trigger_next_workflow(event['next_workflow'])
127
+ end
128
+ end
129
+
130
+ # Check if any serverless functions need to be triggered
131
+ # @param place [String] Place in the workflow
132
+ def check_and_trigger_functions(place)
133
+ # Get function configuration for this place
134
+ function_config = get_function_config(place)
135
+ return unless function_config
136
+
137
+ # Publish event to trigger serverless function
138
+ @js.publish("function.#{function_config['name']}",
139
+ {
140
+ workflow_id: @workflow_id,
141
+ place: place,
142
+ config: function_config
143
+ }.to_json,
144
+ headers: { 'Nats-Msg-Type' => 'function.trigger' }
145
+ )
146
+ end
147
+
148
+ # Get function configuration for a given place
149
+ # @param place [String] Place in the workflow
150
+ # @return [Hash] Function configuration
151
+ def get_function_config(place)
152
+ # This would be loaded from a configuration store
153
+ # For now, returning a mock config
154
+ {
155
+ 'name' => "function_#{place}",
156
+ 'runtime' => 'ruby',
157
+ 'handler' => 'handle',
158
+ 'environment' => {}
159
+ }
160
+ end
161
+
162
+ # Trigger a new workflow instance
163
+ # @param next_workflow_config [Hash] Configuration for the next workflow
164
+ def trigger_next_workflow(next_workflow_config)
165
+ new_workflow_id = SecureRandom.uuid
166
+
167
+ # Create the new workflow
168
+ create_workflow(
169
+ next_workflow_config['petri_net'],
170
+ workflow_id: new_workflow_id
171
+ )
172
+
173
+ # Link the workflows
174
+ @js.publish("workflow.#{@workflow_id}.next",
175
+ {
176
+ previous_workflow_id: @workflow_id,
177
+ next_workflow_id: new_workflow_id
178
+ }.to_json,
179
+ headers: { 'Nats-Msg-Type' => 'workflow.link' }
180
+ )
181
+ end
182
+
183
+ # Add a token to a place in the workflow
184
+ # @param place [String] Place in the workflow
185
+ # @param data [Hash, nil] Optional data for the token
186
+ def add_token(place, data = nil)
187
+ puts "\n[Token] Adding token to place: #{place} with data: #{data.inspect}"
188
+ @js.publish("event.#{@workflow_id}",
189
+ {
190
+ type: 'token_added',
191
+ place: place,
192
+ data: data,
193
+ timestamp: Time.now.utc.iso8601
194
+ }.to_json,
195
+ headers: { 'Nats-Msg-Type' => 'event.token_added' }
196
+ )
197
+ end
198
+
199
+ # Fire a transition in the workflow
200
+ # @param transition_name [String] Name of the transition
201
+ def fire_transition(transition_name)
202
+ puts "\n[Transition] Firing transition: #{transition_name}"
203
+ @js.publish("event.#{@workflow_id}",
204
+ {
205
+ type: 'transition_fired',
206
+ transition: transition_name,
207
+ timestamp: Time.now.utc.iso8601
208
+ }.to_json,
209
+ headers: { 'Nats-Msg-Type' => 'event.transition_fired' }
210
+ )
211
+ end
212
+
213
+ # Complete the workflow
214
+ # @param next_workflow [Hash, nil] Optional configuration for the next workflow
215
+ def complete_workflow(next_workflow = nil)
216
+ puts "\n[Workflow] Completing workflow#{next_workflow ? ' and triggering next workflow' : ''}"
217
+ event_data = {
218
+ type: 'workflow_completed',
219
+ next_workflow: next_workflow,
220
+ timestamp: Time.now.utc.iso8601
221
+ }
222
+
223
+ @js.publish("event.#{@workflow_id}",
224
+ event_data.to_json,
225
+ headers: { 'Nats-Msg-Type' => 'event.workflow_completed' }
226
+ )
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'base_executor'
2
+
3
+ module CircuitBreaker
4
+ module Executors
5
+ class ServerlessExecutor < BaseExecutor
6
+ def initialize(context = {})
7
+ super
8
+ @function_name = context[:function_name]
9
+ @runtime = context[:runtime]
10
+ @payload = context[:payload]
11
+ end
12
+
13
+ def execute
14
+ # Implementation for serverless function execution would go here
15
+ # This would typically involve invoking a serverless function
16
+ @result = {
17
+ function_name: @function_name,
18
+ runtime: @runtime,
19
+ payload: @payload,
20
+ status: 'completed'
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ require_relative 'base_executor'
2
+
3
+ module CircuitBreaker
4
+ module Executors
5
+ class StepExecutor < BaseExecutor
6
+ def initialize(context = {})
7
+ super
8
+ @steps = context[:steps] || []
9
+ @parallel = context[:parallel] || false
10
+ end
11
+
12
+ def execute
13
+ results = if @parallel
14
+ execute_parallel
15
+ else
16
+ execute_sequential
17
+ end
18
+
19
+ @result = {
20
+ steps: results,
21
+ status: 'completed'
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ def execute_sequential
28
+ @steps.map do |step|
29
+ executor = step[:executor].new(step[:context])
30
+ executor.execute
31
+ executor.to_h
32
+ end
33
+ end
34
+
35
+ def execute_parallel
36
+ threads = @steps.map do |step|
37
+ Thread.new do
38
+ executor = step[:executor].new(step[:context])
39
+ executor.execute
40
+ executor.to_h
41
+ end
42
+ end
43
+ threads.map(&:value)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,81 @@
1
+ module CircuitBreaker
2
+ module History
3
+ class Entry
4
+ attr_reader :timestamp, :type, :details, :actor_id
5
+
6
+ def initialize(type:, details:, actor_id: nil)
7
+ @timestamp = Time.now
8
+ @type = type
9
+ @details = details
10
+ @actor_id = actor_id
11
+ end
12
+
13
+ def to_h
14
+ {
15
+ timestamp: timestamp,
16
+ type: type,
17
+ details: details,
18
+ actor_id: actor_id
19
+ }
20
+ end
21
+ end
22
+
23
+ def self.included(base)
24
+ base.class_eval do
25
+ attr_reader :history
26
+
27
+ # Add history initialization to existing initialize method
28
+ original_initialize = instance_method(:initialize)
29
+ define_method(:initialize) do |*args, **kwargs|
30
+ original_initialize.bind(self).call(*args, **kwargs)
31
+ @history = []
32
+ record_event(:created, "Object created")
33
+ end
34
+ end
35
+ end
36
+
37
+ def record_event(type, details, actor_id: nil)
38
+ entry = Entry.new(type: type, details: details, actor_id: actor_id)
39
+ @history << entry
40
+ trigger(:history_updated, entry: entry)
41
+ entry
42
+ end
43
+
44
+ def history_since(timestamp)
45
+ history.select { |entry| entry.timestamp >= timestamp }
46
+ end
47
+
48
+ def history_by_type(type)
49
+ history.select { |entry| entry.type == type }
50
+ end
51
+
52
+ def history_by_actor(actor_id)
53
+ history.select { |entry| entry.actor_id == actor_id }
54
+ end
55
+
56
+ def export_history(format = :json)
57
+ history_data = history.map(&:to_h)
58
+ case format
59
+ when :json
60
+ JSON.pretty_generate(history_data)
61
+ when :yaml
62
+ history_data.to_yaml
63
+ when :csv
64
+ require 'csv'
65
+ CSV.generate do |csv|
66
+ csv << ['Timestamp', 'Type', 'Details', 'Actor']
67
+ history_data.each do |entry|
68
+ csv << [
69
+ entry[:timestamp].iso8601,
70
+ entry[:type],
71
+ entry[:details],
72
+ entry[:actor_id]
73
+ ]
74
+ end
75
+ end
76
+ else
77
+ raise ArgumentError, "Unsupported format: #{format}"
78
+ end
79
+ end
80
+ end
81
+ end