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,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
|