roast-ai 0.1.0 → 0.1.1
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 +4 -4
- data/.github/workflows/cla.yml +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +3 -1
- data/Gemfile +0 -1
- data/Gemfile.lock +3 -4
- data/README.md +418 -4
- data/Rakefile +1 -6
- data/docs/INSTRUMENTATION.md +202 -0
- data/examples/api_workflow/README.md +85 -0
- data/examples/api_workflow/fetch_api_data/prompt.md +10 -0
- data/examples/api_workflow/generate_report/prompt.md +10 -0
- data/examples/api_workflow/prompt.md +10 -0
- data/examples/api_workflow/transform_data/prompt.md +10 -0
- data/examples/api_workflow/workflow.yml +30 -0
- data/examples/grading/workflow.yml +2 -2
- data/examples/instrumentation.rb +76 -0
- data/examples/rspec_to_minitest/README.md +68 -0
- data/examples/rspec_to_minitest/analyze_spec/prompt.md +30 -0
- data/examples/rspec_to_minitest/create_minitest/prompt.md +33 -0
- data/examples/rspec_to_minitest/run_and_improve/prompt.md +35 -0
- data/examples/rspec_to_minitest/workflow.md +10 -0
- data/examples/rspec_to_minitest/workflow.yml +40 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +72 -8
- data/lib/roast/helpers/prompt_loader.rb +2 -0
- data/lib/roast/resources/api_resource.rb +137 -0
- data/lib/roast/resources/base_resource.rb +47 -0
- data/lib/roast/resources/directory_resource.rb +40 -0
- data/lib/roast/resources/file_resource.rb +33 -0
- data/lib/roast/resources/none_resource.rb +29 -0
- data/lib/roast/resources/url_resource.rb +63 -0
- data/lib/roast/resources.rb +100 -0
- data/lib/roast/tools/coding_agent.rb +69 -0
- data/lib/roast/tools.rb +1 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_step.rb +21 -17
- data/lib/roast/workflow/base_workflow.rb +49 -16
- data/lib/roast/workflow/configuration.rb +83 -8
- data/lib/roast/workflow/configuration_parser.rb +171 -3
- data/lib/roast/workflow/file_state_repository.rb +126 -0
- data/lib/roast/workflow/prompt_step.rb +16 -0
- data/lib/roast/workflow/session_manager.rb +82 -0
- data/lib/roast/workflow/state_repository.rb +21 -0
- data/lib/roast/workflow/workflow_executor.rb +99 -9
- data/lib/roast/workflow.rb +4 -0
- data/lib/roast.rb +2 -5
- data/roast.gemspec +1 -1
- data/schema/workflow.json +12 -0
- metadata +31 -6
- data/.rspec +0 -1
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
class PromptStep < BaseStep
|
6
|
+
def initialize(workflow, **kwargs)
|
7
|
+
super(workflow, **kwargs)
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
prompt(name)
|
12
|
+
chat_completion(auto_loop: false, print_response: true)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "digest"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Workflow
|
8
|
+
# Manages session creation, timestamping, and directory management
|
9
|
+
class SessionManager
|
10
|
+
def initialize
|
11
|
+
@session_mutex = Mutex.new
|
12
|
+
@session_timestamps = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get or create a session directory for the workflow
|
16
|
+
def ensure_session_directory(workflow_id, session_name, file_path, timestamp: nil)
|
17
|
+
@session_mutex.synchronize do
|
18
|
+
# Create or get the workflow directory
|
19
|
+
workflow_dir = workflow_directory(session_name, file_path)
|
20
|
+
FileUtils.mkdir_p(workflow_dir)
|
21
|
+
|
22
|
+
# Ensure .gitignore exists
|
23
|
+
gitignore_path = File.join(workflow_dir, ".gitignore")
|
24
|
+
File.write(gitignore_path, "*") unless File.exist?(gitignore_path)
|
25
|
+
|
26
|
+
# Get or create session timestamp
|
27
|
+
session_timestamp = timestamp || @session_timestamps[workflow_id] || create_new_session(workflow_id)
|
28
|
+
|
29
|
+
# Create session directory
|
30
|
+
session_dir = File.join(workflow_dir, session_timestamp)
|
31
|
+
FileUtils.mkdir_p(session_dir)
|
32
|
+
session_dir
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Find a session directory for the workflow
|
37
|
+
def find_session_directory(session_name, file_path, timestamp = nil)
|
38
|
+
workflow_dir = workflow_directory(session_name, file_path)
|
39
|
+
return unless File.directory?(workflow_dir)
|
40
|
+
|
41
|
+
if timestamp
|
42
|
+
session_dir = File.join(workflow_dir, timestamp)
|
43
|
+
File.directory?(session_dir) ? session_dir : nil
|
44
|
+
else
|
45
|
+
find_latest_session_directory(workflow_dir)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get the session timestamp for a workflow
|
50
|
+
def session_timestamp(workflow_id)
|
51
|
+
@session_timestamps[workflow_id]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Set the session timestamp for a workflow
|
55
|
+
def set_session_timestamp(workflow_id, timestamp)
|
56
|
+
@session_timestamps[workflow_id] = timestamp
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create a new session for a workflow
|
60
|
+
def create_new_session(workflow_id)
|
61
|
+
timestamp = Time.now.utc.strftime("%Y%m%d_%H%M%S_%L")
|
62
|
+
@session_timestamps[workflow_id] = timestamp
|
63
|
+
timestamp
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def workflow_directory(session_name, file_path)
|
69
|
+
workflow_dir_name = session_name.parameterize.underscore
|
70
|
+
file_id = Digest::MD5.hexdigest(file_path)
|
71
|
+
file_basename = File.basename(file_path).parameterize.underscore
|
72
|
+
human_readable_id = "#{file_basename}_#{file_id[0..7]}"
|
73
|
+
File.join(Dir.pwd, ".roast", "sessions", workflow_dir_name, human_readable_id)
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_latest_session_directory(workflow_dir)
|
77
|
+
sessions = Dir.children(workflow_dir).sort.reverse
|
78
|
+
sessions.empty? ? nil : File.join(workflow_dir, sessions.first)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Interface for state persistence operations
|
6
|
+
# Handles saving and loading workflow state in a thread-safe manner
|
7
|
+
class StateRepository
|
8
|
+
def save_state(workflow, step_name, state_data)
|
9
|
+
raise NotImplementedError, "#{self.class} must implement save_state"
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_state_before_step(workflow, step_name, timestamp: nil)
|
13
|
+
raise NotImplementedError, "#{self.class} must implement load_state_before_step"
|
14
|
+
end
|
15
|
+
|
16
|
+
def save_final_output(workflow, output_content)
|
17
|
+
raise NotImplementedError, "#{self.class} must implement save_final_output"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/isolated_execution_state"
|
5
|
+
require "active_support/notifications"
|
6
|
+
|
3
7
|
module Roast
|
4
8
|
module Workflow
|
5
9
|
# Handles the execution of workflow steps, including orchestration and threading
|
@@ -30,16 +34,60 @@ module Roast
|
|
30
34
|
end
|
31
35
|
|
32
36
|
def execute_step(name)
|
33
|
-
|
34
|
-
|
37
|
+
start_time = Time.now
|
38
|
+
# For tests, make sure that we handle this gracefully
|
39
|
+
resource_type = workflow.respond_to?(:resource) ? workflow.resource&.type : nil
|
40
|
+
|
41
|
+
ActiveSupport::Notifications.instrument("roast.step.start", {
|
42
|
+
step_name: name,
|
43
|
+
resource_type: resource_type,
|
44
|
+
})
|
45
|
+
|
46
|
+
$stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
|
47
|
+
|
48
|
+
result = if name.starts_with?("$(")
|
49
|
+
strip_and_execute(name).tap do |output|
|
50
|
+
# Add the command and output to the transcript for reference in following steps
|
51
|
+
workflow.transcript << { user: "I just executed the following command: ```\n#{name}\n```\n\nHere is the output:\n\n```\n#{output}\n```" }
|
52
|
+
workflow.transcript << { assistant: "Noted, thank you." }
|
53
|
+
end
|
54
|
+
elsif name.include?("*") && (!workflow.respond_to?(:resource) || !workflow.resource)
|
55
|
+
# Only use the glob method if we don't have a resource object yet
|
56
|
+
# This is for backward compatibility
|
57
|
+
glob(name)
|
58
|
+
else
|
59
|
+
step_object = find_and_load_step(name)
|
60
|
+
step_result = step_object.call
|
61
|
+
workflow.output[name] = step_result
|
62
|
+
|
63
|
+
# Save state after each step if the workflow supports it
|
64
|
+
save_state(name, step_result) if workflow.respond_to?(:session_name) && workflow.session_name
|
35
65
|
|
36
|
-
|
66
|
+
step_result
|
67
|
+
end
|
68
|
+
|
69
|
+
execution_time = Time.now - start_time
|
37
70
|
|
38
|
-
|
39
|
-
|
71
|
+
ActiveSupport::Notifications.instrument("roast.step.complete", {
|
72
|
+
step_name: name,
|
73
|
+
resource_type: resource_type,
|
74
|
+
success: true,
|
75
|
+
execution_time: execution_time,
|
76
|
+
result_size: result.to_s.length,
|
77
|
+
})
|
40
78
|
|
41
|
-
workflow.output[name] = result
|
42
79
|
result
|
80
|
+
rescue => e
|
81
|
+
execution_time = Time.now - start_time
|
82
|
+
|
83
|
+
ActiveSupport::Notifications.instrument("roast.step.error", {
|
84
|
+
step_name: name,
|
85
|
+
resource_type: resource_type,
|
86
|
+
error: e.class.name,
|
87
|
+
message: e.message,
|
88
|
+
execution_time: execution_time,
|
89
|
+
})
|
90
|
+
raise
|
43
91
|
end
|
44
92
|
|
45
93
|
private
|
@@ -66,6 +114,11 @@ module Roast
|
|
66
114
|
end
|
67
115
|
|
68
116
|
def find_and_load_step(step_name)
|
117
|
+
# First check for a prompt step
|
118
|
+
if step_name.strip.include?(" ")
|
119
|
+
return Roast::Workflow::PromptStep.new(workflow, name: step_name, auto_loop: false)
|
120
|
+
end
|
121
|
+
|
69
122
|
# First check for a ruby file with the step name
|
70
123
|
rb_file_path = File.join(context_path, "#{step_name}.rb")
|
71
124
|
if File.file?(rb_file_path)
|
@@ -100,8 +153,15 @@ module Roast
|
|
100
153
|
def setup_step(step_class, step_name, context_path)
|
101
154
|
step_class.new(workflow, name: step_name, context_path: context_path).tap do |step|
|
102
155
|
step_config = config_hash[step_name]
|
156
|
+
|
157
|
+
# Always set the model, even if there's no step_config
|
158
|
+
# Use step-specific model if defined, otherwise use workflow default model, or fallback to DEFAULT_MODEL
|
159
|
+
step.model = step_config&.dig("model") || config_hash["model"] || DEFAULT_MODEL
|
160
|
+
|
161
|
+
# Pass resource to step if supported
|
162
|
+
step.resource = workflow.resource if step.respond_to?(:resource=)
|
163
|
+
|
103
164
|
if step_config.present?
|
104
|
-
step.model = step_config["model"] || DEFAULT_MODEL
|
105
165
|
step.print_response = step_config["print_response"] if step_config["print_response"].present?
|
106
166
|
step.loop = step_config["loop"] if step_config["loop"].present?
|
107
167
|
step.json = step_config["json"] if step_config["json"].present?
|
@@ -111,8 +171,38 @@ module Roast
|
|
111
171
|
end
|
112
172
|
|
113
173
|
def strip_and_execute(step)
|
114
|
-
|
115
|
-
|
174
|
+
if step.match?(/^\$\((.*)\)$/)
|
175
|
+
command = step.strip.match(/^\$\((.*)\)$/)[1]
|
176
|
+
%x(#{command})
|
177
|
+
else
|
178
|
+
raise "Missing closing parentheses: #{step}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def save_state(step_name, step_result)
|
183
|
+
state_repository = FileStateRepository.new
|
184
|
+
|
185
|
+
# Gather necessary data for state
|
186
|
+
static_data = workflow.respond_to?(:transcript) ? workflow.transcript.map(&:itself) : []
|
187
|
+
|
188
|
+
# Get output and final_output if available
|
189
|
+
output = workflow.respond_to?(:output) ? workflow.output.clone : {}
|
190
|
+
final_output = workflow.respond_to?(:final_output) ? workflow.final_output.clone : []
|
191
|
+
|
192
|
+
state_data = {
|
193
|
+
step_name: step_name,
|
194
|
+
order: output.keys.index(step_name) || output.size,
|
195
|
+
transcript: static_data,
|
196
|
+
output: output,
|
197
|
+
final_output: final_output,
|
198
|
+
execution_order: output.keys,
|
199
|
+
}
|
200
|
+
|
201
|
+
# Save the state
|
202
|
+
state_repository.save_state(workflow, step_name, state_data)
|
203
|
+
rescue => e
|
204
|
+
# Don't fail the workflow if state saving fails
|
205
|
+
$stderr.puts "Warning: Failed to save workflow state: #{e.message}"
|
116
206
|
end
|
117
207
|
end
|
118
208
|
end
|
data/lib/roast/workflow.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "roast/workflow/base_step"
|
4
|
+
require "roast/workflow/prompt_step"
|
4
5
|
require "roast/workflow/base_workflow"
|
5
6
|
require "roast/workflow/configuration"
|
6
7
|
require "roast/workflow/workflow_executor"
|
7
8
|
require "roast/workflow/configuration_parser"
|
8
9
|
require "roast/workflow/validator"
|
10
|
+
require "roast/workflow/state_repository"
|
11
|
+
require "roast/workflow/session_manager"
|
12
|
+
require "roast/workflow/file_state_repository"
|
9
13
|
|
10
14
|
module Roast
|
11
15
|
module Workflow
|
data/lib/roast.rb
CHANGED
@@ -5,6 +5,7 @@ require "thor"
|
|
5
5
|
require "roast/version"
|
6
6
|
require "roast/tools"
|
7
7
|
require "roast/helpers"
|
8
|
+
require "roast/resources"
|
8
9
|
require "roast/workflow"
|
9
10
|
|
10
11
|
module Roast
|
@@ -16,7 +17,7 @@ module Roast
|
|
16
17
|
option :output, type: :string, aliases: "-o", desc: "Save results to a file"
|
17
18
|
option :verbose, type: :boolean, aliases: "-v", desc: "Show output from all steps as they are executed"
|
18
19
|
option :target, type: :string, aliases: "-t", desc: "Override target files. Can be file path, glob pattern, or $(shell command)"
|
19
|
-
option :
|
20
|
+
option :replay, type: :string, aliases: "-r", desc: "Resume workflow from a specific step. Format: step_name or session_timestamp:step_name"
|
20
21
|
def execute(*paths)
|
21
22
|
raise Thor::Error, "Workflow configuration file is required" if paths.empty?
|
22
23
|
|
@@ -24,10 +25,6 @@ module Roast
|
|
24
25
|
expanded_workflow_path = File.expand_path(workflow_path)
|
25
26
|
raise Thor::Error, "Expected a Roast workflow configuration file, got directory: #{expanded_workflow_path}" if File.directory?(expanded_workflow_path)
|
26
27
|
|
27
|
-
if options[:subject] && !File.exist?(options[:subject])
|
28
|
-
raise Thor::Error, "Subject file does not exist: #{options[:subject]}"
|
29
|
-
end
|
30
|
-
|
31
28
|
Roast::Workflow::ConfigurationParser.new(expanded_workflow_path, files, options.transform_keys(&:to_sym)).begin!
|
32
29
|
end
|
33
30
|
|
data/roast.gemspec
CHANGED
@@ -39,6 +39,6 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_dependency("activesupport", "~> 8.0")
|
40
40
|
spec.add_dependency("faraday-retry")
|
41
41
|
spec.add_dependency("json-schema")
|
42
|
-
spec.add_dependency("raix", "0.8.
|
42
|
+
spec.add_dependency("raix", "~> 0.8.4")
|
43
43
|
spec.add_dependency("thor", "~> 1.3")
|
44
44
|
end
|
data/schema/workflow.json
CHANGED
@@ -11,6 +11,18 @@
|
|
11
11
|
"type": "string"
|
12
12
|
}
|
13
13
|
},
|
14
|
+
"target": {
|
15
|
+
"type": "string",
|
16
|
+
"description": "Optional target file, glob pattern, or shell command for the workflow to operate on"
|
17
|
+
},
|
18
|
+
"api_token": {
|
19
|
+
"type": "string",
|
20
|
+
"description": "Shell command to fetch an API token dynamically, e.g. $(cat ~/.my-token)"
|
21
|
+
},
|
22
|
+
"model": {
|
23
|
+
"type": "string",
|
24
|
+
"description": "Default AI model to use for all steps in the workflow"
|
25
|
+
},
|
14
26
|
"inputs": {
|
15
27
|
"type": "array",
|
16
28
|
"items": {
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roast-ai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
@@ -55,16 +55,16 @@ dependencies:
|
|
55
55
|
name: raix
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
|
-
- -
|
58
|
+
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version: 0.8.
|
60
|
+
version: 0.8.4
|
61
61
|
type: :runtime
|
62
62
|
prerelease: false
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
|
-
- -
|
65
|
+
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: 0.8.
|
67
|
+
version: 0.8.4
|
68
68
|
- !ruby/object:Gem::Dependency
|
69
69
|
name: thor
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -92,7 +92,6 @@ files:
|
|
92
92
|
- ".github/workflows/ci.yaml"
|
93
93
|
- ".github/workflows/cla.yml"
|
94
94
|
- ".gitignore"
|
95
|
-
- ".rspec"
|
96
95
|
- ".rubocop.yml"
|
97
96
|
- ".ruby-version"
|
98
97
|
- CHANGELOG.md
|
@@ -105,6 +104,13 @@ files:
|
|
105
104
|
- README.md
|
106
105
|
- Rakefile
|
107
106
|
- bin/console
|
107
|
+
- docs/INSTRUMENTATION.md
|
108
|
+
- examples/api_workflow/README.md
|
109
|
+
- examples/api_workflow/fetch_api_data/prompt.md
|
110
|
+
- examples/api_workflow/generate_report/prompt.md
|
111
|
+
- examples/api_workflow/prompt.md
|
112
|
+
- examples/api_workflow/transform_data/prompt.md
|
113
|
+
- examples/api_workflow/workflow.yml
|
108
114
|
- examples/grading/analyze_coverage/prompt.md
|
109
115
|
- examples/grading/calculate_final_grade.rb
|
110
116
|
- examples/grading/format_result.rb
|
@@ -118,6 +124,13 @@ files:
|
|
118
124
|
- examples/grading/workflow.rb.md
|
119
125
|
- examples/grading/workflow.ts+tsx.md
|
120
126
|
- examples/grading/workflow.yml
|
127
|
+
- examples/instrumentation.rb
|
128
|
+
- examples/rspec_to_minitest/README.md
|
129
|
+
- examples/rspec_to_minitest/analyze_spec/prompt.md
|
130
|
+
- examples/rspec_to_minitest/create_minitest/prompt.md
|
131
|
+
- examples/rspec_to_minitest/run_and_improve/prompt.md
|
132
|
+
- examples/rspec_to_minitest/workflow.md
|
133
|
+
- examples/rspec_to_minitest/workflow.yml
|
121
134
|
- exe/roast
|
122
135
|
- lib/roast.rb
|
123
136
|
- lib/roast/helpers.rb
|
@@ -126,8 +139,16 @@ files:
|
|
126
139
|
- lib/roast/helpers/minitest_coverage_runner.rb
|
127
140
|
- lib/roast/helpers/path_resolver.rb
|
128
141
|
- lib/roast/helpers/prompt_loader.rb
|
142
|
+
- lib/roast/resources.rb
|
143
|
+
- lib/roast/resources/api_resource.rb
|
144
|
+
- lib/roast/resources/base_resource.rb
|
145
|
+
- lib/roast/resources/directory_resource.rb
|
146
|
+
- lib/roast/resources/file_resource.rb
|
147
|
+
- lib/roast/resources/none_resource.rb
|
148
|
+
- lib/roast/resources/url_resource.rb
|
129
149
|
- lib/roast/tools.rb
|
130
150
|
- lib/roast/tools/cmd.rb
|
151
|
+
- lib/roast/tools/coding_agent.rb
|
131
152
|
- lib/roast/tools/grep.rb
|
132
153
|
- lib/roast/tools/read_file.rb
|
133
154
|
- lib/roast/tools/search_file.rb
|
@@ -138,6 +159,10 @@ files:
|
|
138
159
|
- lib/roast/workflow/base_workflow.rb
|
139
160
|
- lib/roast/workflow/configuration.rb
|
140
161
|
- lib/roast/workflow/configuration_parser.rb
|
162
|
+
- lib/roast/workflow/file_state_repository.rb
|
163
|
+
- lib/roast/workflow/prompt_step.rb
|
164
|
+
- lib/roast/workflow/session_manager.rb
|
165
|
+
- lib/roast/workflow/state_repository.rb
|
141
166
|
- lib/roast/workflow/validator.rb
|
142
167
|
- lib/roast/workflow/workflow_executor.rb
|
143
168
|
- roast.gemspec
|
data/.rspec
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
--require spec_helper
|