roast-ai 0.2.2 → 0.3.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 +4 -4
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +3 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +15 -10
- data/README.md +189 -20
- data/examples/bash_prototyping/README.md +53 -0
- data/examples/bash_prototyping/analyze_network/prompt.md +13 -0
- data/examples/bash_prototyping/analyze_system/prompt.md +11 -0
- data/examples/bash_prototyping/api_testing.yml +14 -0
- data/examples/bash_prototyping/check_processes/prompt.md +11 -0
- data/examples/bash_prototyping/generate_report/prompt.md +16 -0
- data/examples/bash_prototyping/process_json_response/prompt.md +24 -0
- data/examples/bash_prototyping/system_analysis.yml +14 -0
- data/examples/bash_prototyping/test_public_api/prompt.md +22 -0
- data/examples/cmd/README.md +99 -0
- data/examples/cmd/analyze_project/prompt.md +57 -0
- data/examples/cmd/basic_demo/prompt.md +48 -0
- data/examples/cmd/basic_workflow.yml +17 -0
- data/examples/cmd/check_repository/prompt.md +57 -0
- data/examples/cmd/create_and_verify/prompt.md +56 -0
- data/examples/cmd/dev_workflow.yml +26 -0
- data/examples/cmd/explore_project/prompt.md +67 -0
- data/examples/cmd/explorer_workflow.yml +21 -0
- data/examples/cmd/smart_tool_selection/prompt.md +99 -0
- data/examples/grading/read_dependencies/prompt.md +4 -2
- data/examples/grading/run_coverage.rb +9 -0
- data/examples/grading/workflow.yml +0 -2
- data/examples/mcp/README.md +223 -0
- data/examples/mcp/analyze_changes/prompt.md +8 -0
- data/examples/mcp/analyze_issues/prompt.md +4 -0
- data/examples/mcp/analyze_schema/prompt.md +4 -0
- data/examples/mcp/check_data_quality/prompt.md +5 -0
- data/examples/mcp/check_documentation/prompt.md +4 -0
- data/examples/mcp/create_recommendations/prompt.md +5 -0
- data/examples/mcp/database_workflow.yml +29 -0
- data/examples/mcp/env_demo/workflow.yml +34 -0
- data/examples/mcp/fetch_pr_context/prompt.md +4 -0
- data/examples/mcp/filesystem_demo/create_test_file/prompt.md +2 -0
- data/examples/mcp/filesystem_demo/list_files/prompt.md +6 -0
- data/examples/mcp/filesystem_demo/read_with_mcp/prompt.md +7 -0
- data/examples/mcp/filesystem_demo/workflow.yml +38 -0
- data/examples/mcp/generate_insights/prompt.md +4 -0
- data/examples/mcp/generate_report/prompt.md +6 -0
- data/examples/mcp/generate_review/prompt.md +16 -0
- data/examples/mcp/github_workflow.yml +32 -0
- data/examples/mcp/multi_mcp_workflow.yml +58 -0
- data/examples/mcp/post_review/prompt.md +3 -0
- data/examples/mcp/save_report/prompt.md +6 -0
- data/examples/mcp/search_issues/prompt.md +2 -0
- data/examples/mcp/summarize/prompt.md +1 -0
- data/examples/mcp/test_filesystem/prompt.md +6 -0
- data/examples/mcp/test_github/prompt.md +8 -0
- data/examples/mcp/test_read/prompt.md +1 -0
- data/examples/mcp/workflow.yml +35 -0
- data/examples/shared_config/README.md +52 -0
- data/examples/shared_config/example_with_shared_config/workflow.yml +6 -0
- data/examples/shared_config/shared.yml +7 -0
- data/examples/step_configuration/README.md +0 -3
- data/examples/step_configuration/workflow.yml +0 -3
- data/examples/tool_config_example/README.md +109 -0
- data/examples/tool_config_example/example_step/prompt.md +42 -0
- data/examples/tool_config_example/workflow.yml +17 -0
- data/examples/workflow_generator/workflow.yml +0 -1
- data/lib/roast/helpers/function_caching_interceptor.rb +0 -4
- data/lib/roast/helpers/prompt_loader.rb +0 -1
- data/lib/roast/tools/bash.rb +62 -0
- data/lib/roast/tools/cmd.rb +121 -34
- data/lib/roast/tools/coding_agent.rb +86 -7
- data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +87 -0
- data/lib/roast/tools/search_file.rb +13 -1
- data/lib/roast/tools.rb +5 -5
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_iteration_step.rb +5 -4
- data/lib/roast/workflow/base_step.rb +30 -21
- data/lib/roast/workflow/base_workflow.rb +8 -10
- data/lib/roast/workflow/configuration.rb +12 -3
- data/lib/roast/workflow/configuration_loader.rb +63 -4
- data/lib/roast/workflow/configuration_parser.rb +0 -3
- data/lib/roast/workflow/error_handler.rb +0 -1
- data/lib/roast/workflow/file_state_repository.rb +0 -1
- data/lib/roast/workflow/iteration_executor.rb +4 -2
- data/lib/roast/workflow/output_manager.rb +0 -1
- data/lib/roast/workflow/step_executor_coordinator.rb +5 -3
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +1 -1
- data/lib/roast/workflow/step_loader.rb +35 -8
- data/lib/roast/workflow/step_orchestrator.rb +4 -2
- data/lib/roast/workflow/workflow_execution_context.rb +0 -2
- data/lib/roast/workflow/workflow_executor.rb +2 -4
- data/lib/roast/workflow/workflow_initializer.rb +66 -2
- data/lib/roast/workflow/workflow_runner.rb +1 -2
- data/lib/roast.rb +8 -0
- data/package-lock.json +6 -0
- data/roast.gemspec +2 -1
- metadata +72 -3
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Tools
|
8
|
+
module CodingAgent
|
9
|
+
module CodingAgentMessageFormatter
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def format_messages(json)
|
13
|
+
messages = json.dig("message", "content")
|
14
|
+
messages.map(&method(:format_message))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def format_message(message)
|
20
|
+
case message["type"]
|
21
|
+
when "text"
|
22
|
+
format_text(message["text"])
|
23
|
+
when "tool_use"
|
24
|
+
name = message["name"]
|
25
|
+
input = message["input"].except("description", "old_string", "new_string")
|
26
|
+
case name
|
27
|
+
when "Task"
|
28
|
+
"→ #{name}#{format_task_input(input)}"
|
29
|
+
when "TodoWrite"
|
30
|
+
"→ #{name}#{format_todo_write_input(input)}"
|
31
|
+
when "Bash", "Read", "Edit"
|
32
|
+
"→ #{name}(#{format_arguments(input)})"
|
33
|
+
else
|
34
|
+
"→ #{name} #{format_text(input.to_yaml)}"
|
35
|
+
end
|
36
|
+
when "tool_result"
|
37
|
+
# Ignore these message types
|
38
|
+
else
|
39
|
+
message.except("id").to_yaml
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_text(text)
|
44
|
+
text.lines.map do |line|
|
45
|
+
"\t#{line}"
|
46
|
+
end.join.lstrip
|
47
|
+
end
|
48
|
+
|
49
|
+
def format_task_input(input)
|
50
|
+
prompt = input["prompt"].lines.filter { |line| !line.blank? }.map { |line| "\t#{line}" }.join
|
51
|
+
args = format_arguments(input.except("prompt"))
|
52
|
+
"(#{args})\n#{prompt}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def format_todo_write_input(input)
|
56
|
+
todos = input["todos"].map(&method(:format_todo_write_input_item)).join("\n")
|
57
|
+
args = format_arguments(input.except("todos"))
|
58
|
+
"(#{args})\n#{todos}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def format_todo_write_input_item(item)
|
62
|
+
id = item["id"]
|
63
|
+
content = item["content"]
|
64
|
+
status = case item["status"]
|
65
|
+
when "pending"
|
66
|
+
"[ ]"
|
67
|
+
when "in_progress"
|
68
|
+
"[-]"
|
69
|
+
when "completed"
|
70
|
+
"[x]"
|
71
|
+
end
|
72
|
+
"\t#{id}. #{status} #{content}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def format_arguments(arguments)
|
76
|
+
if arguments.length == 1
|
77
|
+
arguments.first[1].to_json
|
78
|
+
else
|
79
|
+
arguments.map do |key, value|
|
80
|
+
"#{key}: #{value.to_json}"
|
81
|
+
end.join(", ")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -15,7 +15,7 @@ module Roast
|
|
15
15
|
:search_for_file,
|
16
16
|
"Search for a file in the project using a glob pattern.",
|
17
17
|
glob_pattern: { type: "string", description: "A glob pattern to search for. Example: 'test/**/*_test.rb'" },
|
18
|
-
path: { type: "string", description: "path to search from" },
|
18
|
+
path: { type: "string", description: "path to search from", default: "." },
|
19
19
|
) do |params|
|
20
20
|
Roast::Tools::SearchFile.call(params[:glob_pattern], params[:path]).tap do |result|
|
21
21
|
Roast::Helpers::Logger.debug(result) if ENV["DEBUG"]
|
@@ -26,6 +26,18 @@ module Roast
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def call(glob_pattern, path = ".")
|
29
|
+
raise ArgumentError, "glob_pattern is required" if glob_pattern.nil?
|
30
|
+
|
31
|
+
path ||= "."
|
32
|
+
|
33
|
+
unless File.exist?(path)
|
34
|
+
Roast::Helpers::Logger.error("Path does not exist: #{path}")
|
35
|
+
return "Path does not exist: #{path}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# prefix **/ to the glob pattern if it doesn't already have it
|
39
|
+
glob_pattern = "**/#{glob_pattern}" unless glob_pattern.start_with?("**")
|
40
|
+
|
29
41
|
Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{File.expand_path(path)}'\n")
|
30
42
|
search_for(glob_pattern, path).then do |results|
|
31
43
|
return "No results found for #{glob_pattern} in #{path}" if results.empty?
|
data/lib/roast/tools.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/cache"
|
4
3
|
require "English"
|
5
4
|
require "fileutils"
|
6
5
|
|
6
|
+
require "roast/tools/ask_user"
|
7
|
+
require "roast/tools/bash"
|
8
|
+
require "roast/tools/cmd"
|
9
|
+
require "roast/tools/coding_agent"
|
7
10
|
require "roast/tools/grep"
|
8
11
|
require "roast/tools/read_file"
|
9
12
|
require "roast/tools/search_file"
|
10
|
-
require "roast/tools/write_file"
|
11
13
|
require "roast/tools/update_files"
|
12
|
-
require "roast/tools/
|
13
|
-
require "roast/tools/coding_agent"
|
14
|
-
require "roast/tools/ask_user"
|
14
|
+
require "roast/tools/write_file"
|
15
15
|
|
16
16
|
module Roast
|
17
17
|
module Tools
|
data/lib/roast/version.rb
CHANGED
@@ -12,11 +12,12 @@ module Roast
|
|
12
12
|
|
13
13
|
DEFAULT_MAX_ITERATIONS = 100
|
14
14
|
|
15
|
-
attr_reader :steps
|
15
|
+
attr_reader :steps, :config_hash
|
16
16
|
|
17
|
-
def initialize(workflow, steps:, **kwargs)
|
17
|
+
def initialize(workflow, steps:, config_hash: {}, **kwargs)
|
18
18
|
super(workflow, **kwargs)
|
19
19
|
@steps = steps
|
20
|
+
@config_hash = config_hash
|
20
21
|
# Don't initialize cmd_tool here - we'll do it lazily when needed
|
21
22
|
end
|
22
23
|
|
@@ -65,7 +66,7 @@ module Roast
|
|
65
66
|
|
66
67
|
# Execute nested steps
|
67
68
|
def execute_nested_steps(steps, context, executor = nil)
|
68
|
-
executor ||= WorkflowExecutor.new(context,
|
69
|
+
executor ||= WorkflowExecutor.new(context, config_hash, context_path)
|
69
70
|
results = []
|
70
71
|
|
71
72
|
steps.each do |step|
|
@@ -139,7 +140,7 @@ module Roast
|
|
139
140
|
# Execute a step by name and return its result
|
140
141
|
def execute_step_by_name(step_name, context)
|
141
142
|
# Reuse existing step execution logic
|
142
|
-
executor = WorkflowExecutor.new(context,
|
143
|
+
executor = WorkflowExecutor.new(context, config_hash, context_path)
|
143
144
|
executor.execute_step(step_name)
|
144
145
|
end
|
145
146
|
|
@@ -9,20 +9,20 @@ module Roast
|
|
9
9
|
class BaseStep
|
10
10
|
extend Forwardable
|
11
11
|
|
12
|
-
attr_accessor :model, :print_response, :
|
12
|
+
attr_accessor :model, :print_response, :json, :params, :resource, :coerce_to
|
13
13
|
attr_reader :workflow, :name, :context_path
|
14
14
|
|
15
15
|
def_delegator :workflow, :append_to_final_output
|
16
16
|
def_delegator :workflow, :chat_completion
|
17
17
|
def_delegator :workflow, :transcript
|
18
18
|
|
19
|
-
|
19
|
+
# TODO: is this really the model we want to default to, and is this the right place to set it?
|
20
|
+
def initialize(workflow, model: "anthropic:claude-opus-4", name: nil, context_path: nil)
|
20
21
|
@workflow = workflow
|
21
22
|
@model = model
|
22
23
|
@name = name || self.class.name.underscore.split("/").last
|
23
24
|
@context_path = context_path || ContextPathResolver.resolve(self.class)
|
24
25
|
@print_response = false
|
25
|
-
@auto_loop = auto_loop
|
26
26
|
@json = false
|
27
27
|
@params = {}
|
28
28
|
@coerce_to = nil
|
@@ -31,7 +31,7 @@ module Roast
|
|
31
31
|
|
32
32
|
def call
|
33
33
|
prompt(read_sidecar_prompt)
|
34
|
-
result = chat_completion(print_response:,
|
34
|
+
result = chat_completion(print_response:, json:, params:)
|
35
35
|
|
36
36
|
# Apply coercion if configured
|
37
37
|
apply_coercion(result)
|
@@ -39,25 +39,24 @@ module Roast
|
|
39
39
|
|
40
40
|
protected
|
41
41
|
|
42
|
-
def chat_completion(print_response: nil,
|
42
|
+
def chat_completion(print_response: nil, json: nil, params: nil)
|
43
43
|
# Use instance variables as defaults if parameters are not provided
|
44
44
|
print_response = @print_response if print_response.nil?
|
45
|
-
auto_loop = @auto_loop if auto_loop.nil?
|
46
45
|
json = @json if json.nil?
|
47
46
|
params = @params if params.nil?
|
48
47
|
|
49
|
-
workflow.chat_completion(openai: workflow.openai? && model,
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
48
|
+
workflow.chat_completion(openai: workflow.openai? && model, model: model, json:, params:).tap do |result|
|
49
|
+
process_output(result, print_response:)
|
50
|
+
|
51
|
+
begin
|
52
|
+
if json
|
53
|
+
return nil if result.strip.empty? # Explicitly handle empty string
|
54
|
+
|
55
|
+
return JSON.parse(result)
|
56
|
+
end
|
57
|
+
rescue JSON::ParserError
|
58
|
+
# If JSON parsing fails, leave it as a string
|
58
59
|
end
|
59
|
-
end.tap do |response|
|
60
|
-
process_output(response, print_response:)
|
61
60
|
end
|
62
61
|
end
|
63
62
|
|
@@ -90,11 +89,11 @@ module Roast
|
|
90
89
|
private
|
91
90
|
|
92
91
|
def apply_coercion(result)
|
93
|
-
return result unless @coerce_to
|
94
|
-
|
95
92
|
case @coerce_to
|
96
93
|
when :boolean
|
97
|
-
# Simple boolean coercion
|
94
|
+
# Simple boolean coercion - empty string is false
|
95
|
+
return false if result.nil? || result == ""
|
96
|
+
|
98
97
|
!!result
|
99
98
|
when :llm_boolean
|
100
99
|
# Use LLM boolean coercer for natural language responses
|
@@ -104,9 +103,19 @@ module Roast
|
|
104
103
|
# Ensure result is iterable
|
105
104
|
return result if result.respond_to?(:each)
|
106
105
|
|
106
|
+
# Try to parse as JSON array first
|
107
|
+
if result.is_a?(String) && result.strip.start_with?("[")
|
108
|
+
begin
|
109
|
+
parsed = JSON.parse(result)
|
110
|
+
return parsed if parsed.is_a?(Array)
|
111
|
+
rescue JSON::ParserError
|
112
|
+
# Fall through to split by newlines
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
107
116
|
result.to_s.split("\n")
|
108
117
|
else
|
109
|
-
# Unknown coercion type, return as-is
|
118
|
+
# Unknown or nil coercion type, return as-is
|
110
119
|
result
|
111
120
|
end
|
112
121
|
end
|
@@ -2,12 +2,10 @@
|
|
2
2
|
|
3
3
|
require "raix/chat_completion"
|
4
4
|
require "raix/function_dispatch"
|
5
|
-
|
6
|
-
require "active_support/isolated_execution_state"
|
7
|
-
require "active_support/notifications"
|
8
|
-
require "active_support/core_ext/hash/indifferent_access"
|
9
|
-
require "roast/workflow/output_manager"
|
5
|
+
|
10
6
|
require "roast/workflow/context_path_resolver"
|
7
|
+
require "roast/workflow/dot_access_hash"
|
8
|
+
require "roast/workflow/output_manager"
|
11
9
|
|
12
10
|
module Roast
|
13
11
|
module Workflow
|
@@ -24,22 +22,22 @@ module Roast
|
|
24
22
|
:resource,
|
25
23
|
:session_name,
|
26
24
|
:session_timestamp,
|
27
|
-
:
|
28
|
-
:
|
25
|
+
:model,
|
26
|
+
:workflow_configuration
|
29
27
|
|
30
28
|
attr_reader :pre_processing_data
|
31
29
|
|
32
|
-
delegate :api_provider, :openai?, to: :
|
30
|
+
delegate :api_provider, :openai?, to: :workflow_configuration, allow_nil: true
|
33
31
|
delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
|
34
32
|
|
35
|
-
def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil,
|
33
|
+
def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, workflow_configuration: nil, pre_processing_data: nil)
|
36
34
|
@file = file
|
37
35
|
@name = name || self.class.name.underscore.split("/").last
|
38
36
|
@context_path = context_path || ContextPathResolver.resolve(self.class)
|
39
37
|
@resource = resource || Roast::Resources.for(file)
|
40
38
|
@session_name = session_name || @name
|
41
39
|
@session_timestamp = nil
|
42
|
-
@
|
40
|
+
@workflow_configuration = workflow_configuration
|
43
41
|
@pre_processing_data = pre_processing_data ? DotAccessHash.new(pre_processing_data).freeze : nil
|
44
42
|
|
45
43
|
# Initialize managers
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/core_ext/module/delegation"
|
4
3
|
require "roast/workflow/api_configuration"
|
5
4
|
require "roast/workflow/configuration_loader"
|
6
5
|
require "roast/workflow/resource_resolver"
|
@@ -11,7 +10,9 @@ module Roast
|
|
11
10
|
# Encapsulates workflow configuration data and provides structured access
|
12
11
|
# to the configuration settings
|
13
12
|
class Configuration
|
14
|
-
|
13
|
+
MCPTool = Struct.new(:name, :config, :only, :except, keyword_init: true)
|
14
|
+
|
15
|
+
attr_reader :config_hash, :workflow_path, :name, :steps, :pre_processing, :post_processing, :tools, :tool_configs, :mcp_tools, :function_configs, :model, :resource
|
15
16
|
attr_accessor :target
|
16
17
|
|
17
18
|
delegate :api_provider, :openrouter?, :openai?, :uri_base, to: :api_configuration
|
@@ -32,7 +33,8 @@ module Roast
|
|
32
33
|
@steps = ConfigurationLoader.extract_steps(@config_hash)
|
33
34
|
@pre_processing = ConfigurationLoader.extract_pre_processing(@config_hash)
|
34
35
|
@post_processing = ConfigurationLoader.extract_post_processing(@config_hash)
|
35
|
-
@tools = ConfigurationLoader.extract_tools(@config_hash)
|
36
|
+
@tools, @tool_configs = ConfigurationLoader.extract_tools(@config_hash)
|
37
|
+
@mcp_tools = ConfigurationLoader.extract_mcp_tools(@config_hash)
|
36
38
|
@function_configs = ConfigurationLoader.extract_functions(@config_hash)
|
37
39
|
@model = ConfigurationLoader.extract_model(@config_hash)
|
38
40
|
|
@@ -82,6 +84,13 @@ module Roast
|
|
82
84
|
@function_configs[function_name.to_s] || {}
|
83
85
|
end
|
84
86
|
|
87
|
+
# Get configuration for a specific tool
|
88
|
+
# @param tool_name [String] The name of the tool (e.g., 'Roast::Tools::Cmd')
|
89
|
+
# @return [Hash] The configuration for the tool or empty hash if not found
|
90
|
+
def tool_config(tool_name)
|
91
|
+
@tool_configs[tool_name.to_s] || {}
|
92
|
+
end
|
93
|
+
|
85
94
|
private
|
86
95
|
|
87
96
|
attr_reader :api_configuration
|
@@ -12,7 +12,21 @@ module Roast
|
|
12
12
|
# @return [Hash] The parsed configuration hash
|
13
13
|
def load(workflow_path)
|
14
14
|
validate_path!(workflow_path)
|
15
|
-
|
15
|
+
|
16
|
+
# Load shared.yml if it exists one level above
|
17
|
+
parent_dir = File.dirname(workflow_path)
|
18
|
+
shared_path = File.join(parent_dir, "..", "shared.yml")
|
19
|
+
|
20
|
+
yaml_content = ""
|
21
|
+
|
22
|
+
if File.exist?(shared_path)
|
23
|
+
yaml_content += File.read(shared_path)
|
24
|
+
yaml_content += "\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
yaml_content += File.read(workflow_path)
|
28
|
+
config_hash = YAML.load(yaml_content, aliases: true)
|
29
|
+
|
16
30
|
validate_config!(config_hash)
|
17
31
|
config_hash
|
18
32
|
end
|
@@ -46,11 +60,56 @@ module Roast
|
|
46
60
|
config_hash["post_processing"] || []
|
47
61
|
end
|
48
62
|
|
49
|
-
# Extract tools from the configuration
|
63
|
+
# Extract tools and tool configurations from the configuration
|
50
64
|
# @param config_hash [Hash] The configuration hash
|
51
|
-
# @return [Array] The tools array or empty array
|
65
|
+
# @return [Array, Hash] The tools array or empty array
|
52
66
|
def extract_tools(config_hash)
|
53
|
-
config_hash["tools"] || []
|
67
|
+
tools_config = config_hash["tools"] || []
|
68
|
+
tools = []
|
69
|
+
tool_configs = {}
|
70
|
+
|
71
|
+
tools_config.each do |tool_entry|
|
72
|
+
case tool_entry
|
73
|
+
when String
|
74
|
+
tools << tool_entry
|
75
|
+
when Hash
|
76
|
+
tool_entry.each do |tool_name, config|
|
77
|
+
# Skip MCP tool configurations (those with url or command)
|
78
|
+
if config.is_a?(Hash) && (config["url"] || config["command"])
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
tools << tool_name
|
83
|
+
tool_configs[tool_name] = config || {}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
[tools, tool_configs]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Extract MCP tools from the configuration
|
92
|
+
# @param config_hash [Hash] The configuration hash
|
93
|
+
# @return [Array] The MCP tools array or empty array
|
94
|
+
def extract_mcp_tools(config_hash)
|
95
|
+
tools = config_hash["tools"]&.select { |tool| tool.is_a?(Hash) } || []
|
96
|
+
return [] if tools.none?
|
97
|
+
|
98
|
+
mcp_tools = []
|
99
|
+
tools.each do |tool|
|
100
|
+
tool.each do |tool_name, config|
|
101
|
+
next unless config.is_a?(Hash) && (config["url"] || config["command"])
|
102
|
+
|
103
|
+
mcp_tools << Configuration::MCPTool.new(
|
104
|
+
name: tool_name,
|
105
|
+
config: config,
|
106
|
+
only: config["only"],
|
107
|
+
except: config["except"],
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
mcp_tools
|
54
113
|
end
|
55
114
|
|
56
115
|
# Extract function configurations
|
@@ -3,9 +3,6 @@
|
|
3
3
|
require "roast/workflow/configuration"
|
4
4
|
require "roast/workflow/workflow_initializer"
|
5
5
|
require "roast/workflow/workflow_runner"
|
6
|
-
require "active_support"
|
7
|
-
require "active_support/isolated_execution_state"
|
8
|
-
require "active_support/notifications"
|
9
6
|
|
10
7
|
module Roast
|
11
8
|
module Workflow
|
@@ -79,7 +79,6 @@ module Roast
|
|
79
79
|
)
|
80
80
|
output_file = File.join(session_dir, "final_output.txt")
|
81
81
|
File.write(output_file, output_content)
|
82
|
-
$stderr.puts "Final output saved to: #{output_file}"
|
83
82
|
output_file
|
84
83
|
rescue => e
|
85
84
|
$stderr.puts "Failed to save final output: #{e.message}"
|
@@ -4,10 +4,11 @@ module Roast
|
|
4
4
|
module Workflow
|
5
5
|
# Handles execution of iteration steps (repeat and each)
|
6
6
|
class IterationExecutor
|
7
|
-
def initialize(workflow, context_path, state_manager)
|
7
|
+
def initialize(workflow, context_path, state_manager, config_hash = {})
|
8
8
|
@workflow = workflow
|
9
9
|
@context_path = context_path
|
10
10
|
@state_manager = state_manager
|
11
|
+
@config_hash = config_hash
|
11
12
|
end
|
12
13
|
|
13
14
|
def execute_repeat(repeat_config)
|
@@ -31,6 +32,7 @@ module Roast
|
|
31
32
|
max_iterations: max_iterations,
|
32
33
|
name: "repeat_#{@workflow.output.size}",
|
33
34
|
context_path: @context_path,
|
35
|
+
config_hash: @config_hash,
|
34
36
|
)
|
35
37
|
|
36
38
|
# Apply configuration if provided
|
@@ -70,6 +72,7 @@ module Roast
|
|
70
72
|
steps: steps,
|
71
73
|
name: "each_#{variable_name}",
|
72
74
|
context_path: @context_path,
|
75
|
+
config_hash: @config_hash,
|
73
76
|
)
|
74
77
|
|
75
78
|
# Apply configuration if provided
|
@@ -92,7 +95,6 @@ module Roast
|
|
92
95
|
# Apply configuration settings to a step
|
93
96
|
def apply_step_configuration(step, step_config)
|
94
97
|
step.print_response = step_config["print_response"] if step_config.key?("print_response")
|
95
|
-
step.auto_loop = step_config["loop"] if step_config.key?("loop")
|
96
98
|
step.json = step_config["json"] if step_config.key?("json")
|
97
99
|
step.params = step_config["params"] if step_config.key?("params")
|
98
100
|
step.model = step_config["model"] if step_config.key?("model")
|
@@ -207,7 +207,8 @@ module Roast
|
|
207
207
|
exit_on_error = context.exit_on_error?(interpolated_name)
|
208
208
|
|
209
209
|
# Execute the command directly using the appropriate executor
|
210
|
-
|
210
|
+
# Pass the original key name for configuration lookup
|
211
|
+
result = execute(interpolated_command, { exit_on_error: exit_on_error, step_key: interpolated_name })
|
211
212
|
context.workflow.output[interpolated_name] = result
|
212
213
|
result
|
213
214
|
end
|
@@ -227,13 +228,14 @@ module Roast
|
|
227
228
|
execute_command_step(interpolated_step, { exit_on_error: exit_on_error })
|
228
229
|
else
|
229
230
|
exit_on_error = options.fetch(:exit_on_error, context.exit_on_error?(step))
|
230
|
-
execute_standard_step(interpolated_step,
|
231
|
+
execute_standard_step(interpolated_step, options.merge(exit_on_error: exit_on_error))
|
231
232
|
end
|
232
233
|
end
|
233
234
|
|
234
235
|
def execute_standard_step(step, options)
|
235
236
|
exit_on_error = options.fetch(:exit_on_error, true)
|
236
|
-
|
237
|
+
step_key = options[:step_key]
|
238
|
+
step_orchestrator.execute_step(step, exit_on_error: exit_on_error, step_key: step_key)
|
237
239
|
end
|
238
240
|
|
239
241
|
def validate_each_step!(step)
|
@@ -24,7 +24,7 @@ module Roast
|
|
24
24
|
step_config = config_hash[interpolated_name]
|
25
25
|
exit_on_error = step_config.is_a?(Hash) ? step_config.fetch("exit_on_error", true) : true
|
26
26
|
|
27
|
-
workflow.output[interpolated_name] = step_runner.execute_step(interpolated_command, exit_on_error: exit_on_error)
|
27
|
+
workflow.output[interpolated_name] = step_runner.execute_step(interpolated_command, exit_on_error: exit_on_error, step_key: interpolated_name)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -46,25 +46,32 @@ module Roast
|
|
46
46
|
# Finds and loads a step by name
|
47
47
|
#
|
48
48
|
# @param step_name [String, StepName] The name of the step to load
|
49
|
+
# @param step_key [String] The configuration key for the step (optional)
|
49
50
|
# @return [BaseStep] The loaded step instance
|
50
|
-
def load(step_name)
|
51
|
+
def load(step_name, step_key: nil)
|
51
52
|
name = step_name.is_a?(Roast::ValueObjects::StepName) ? step_name : Roast::ValueObjects::StepName.new(step_name)
|
52
53
|
|
54
|
+
# Get step config for per-step path
|
55
|
+
step_config = config_hash[name.to_s] || {}
|
56
|
+
per_step_path = step_config["path"]
|
57
|
+
|
53
58
|
# First check for a prompt step (contains spaces)
|
54
59
|
if name.plain_text?
|
55
|
-
step = Roast::Workflow::PromptStep.new(workflow, name: name.to_s
|
56
|
-
|
60
|
+
step = Roast::Workflow::PromptStep.new(workflow, name: name.to_s)
|
61
|
+
# Use step_key for configuration if provided, otherwise use name
|
62
|
+
config_key = step_key || name.to_s
|
63
|
+
configure_step(step, config_key)
|
57
64
|
return step
|
58
65
|
end
|
59
66
|
|
60
67
|
# Look for Ruby file in various locations
|
61
|
-
step_file_path = find_step_file(name.to_s)
|
68
|
+
step_file_path = find_step_file(name.to_s, per_step_path)
|
62
69
|
if step_file_path
|
63
70
|
return load_ruby_step(step_file_path, name.to_s)
|
64
71
|
end
|
65
72
|
|
66
73
|
# Look for step directory
|
67
|
-
step_directory = find_step_directory(name.to_s)
|
74
|
+
step_directory = find_step_directory(name.to_s, per_step_path)
|
68
75
|
unless step_directory
|
69
76
|
raise StepNotFoundError.new("Step directory or file not found: #{name}", step_name: name.to_s)
|
70
77
|
end
|
@@ -74,8 +81,22 @@ module Roast
|
|
74
81
|
|
75
82
|
private
|
76
83
|
|
84
|
+
def resolve_path(path)
|
85
|
+
return unless path
|
86
|
+
return path if Pathname.new(path).absolute?
|
87
|
+
|
88
|
+
File.expand_path(path, context_path)
|
89
|
+
end
|
90
|
+
|
77
91
|
# Find a Ruby step file in various locations
|
78
|
-
def find_step_file(step_name)
|
92
|
+
def find_step_file(step_name, per_step_path = nil)
|
93
|
+
# Check in per-step path first
|
94
|
+
if per_step_path
|
95
|
+
resolved_per_step_path = resolve_path(per_step_path)
|
96
|
+
custom_rb_path = File.join(resolved_per_step_path, "#{step_name}.rb")
|
97
|
+
return custom_rb_path if File.file?(custom_rb_path)
|
98
|
+
end
|
99
|
+
|
79
100
|
# Check in phase-specific directory first
|
80
101
|
if phase != :steps
|
81
102
|
phase_rb_path = File.join(context_path, phase.to_s, "#{step_name}.rb")
|
@@ -94,7 +115,14 @@ module Roast
|
|
94
115
|
end
|
95
116
|
|
96
117
|
# Find a step directory
|
97
|
-
def find_step_directory(step_name)
|
118
|
+
def find_step_directory(step_name, per_step_path = nil)
|
119
|
+
# Check in per-step path first
|
120
|
+
if per_step_path
|
121
|
+
resolved_per_step_path = resolve_path(per_step_path)
|
122
|
+
custom_step_path = File.join(resolved_per_step_path, step_name)
|
123
|
+
return custom_step_path if File.directory?(custom_step_path)
|
124
|
+
end
|
125
|
+
|
98
126
|
# Check in phase-specific directory first
|
99
127
|
if phase != :steps
|
100
128
|
phase_step_path = File.join(context_path, phase.to_s, step_name)
|
@@ -158,7 +186,6 @@ module Roast
|
|
158
186
|
# Apply configuration settings to a step
|
159
187
|
def apply_step_configuration(step, step_config)
|
160
188
|
step.print_response = step_config["print_response"] if step_config.key?("print_response")
|
161
|
-
step.auto_loop = step_config["loop"] if step_config.key?("loop")
|
162
189
|
step.json = step_config["json"] if step_config.key?("json")
|
163
190
|
step.params = step_config["params"] if step_config.key?("params")
|
164
191
|
step.coerce_to = step_config["coerce_to"].to_sym if step_config.key?("coerce_to")
|
@@ -22,13 +22,15 @@ module Roast
|
|
22
22
|
@workflow_executor = workflow_executor
|
23
23
|
end
|
24
24
|
|
25
|
-
def execute_step(name, exit_on_error: true)
|
25
|
+
def execute_step(name, exit_on_error: true, step_key: nil)
|
26
26
|
resource_type = @workflow.respond_to?(:resource) ? @workflow.resource&.type : nil
|
27
27
|
|
28
28
|
@error_handler.with_error_handling(name, resource_type: resource_type) do
|
29
29
|
$stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
|
30
30
|
|
31
|
-
|
31
|
+
# Use step_key for loading if provided, otherwise use name
|
32
|
+
load_key = step_key || name
|
33
|
+
step_object = @step_loader.load(name, step_key: load_key)
|
32
34
|
step_result = step_object.call
|
33
35
|
|
34
36
|
# Store result in workflow output
|