roast-ai 0.2.3 → 0.3.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/CHANGELOG.md +31 -0
- data/CLAUDE.md +3 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +15 -10
- data/README.md +214 -20
- data/bin/roast +1 -1
- 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/README.md +71 -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_step.rb +29 -21
- data/lib/roast/workflow/base_workflow.rb +8 -10
- data/lib/roast/workflow/configuration.rb +68 -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 +0 -1
- data/lib/roast/workflow/output_manager.rb +0 -1
- data/lib/roast/workflow/prompt_step.rb +1 -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 +1 -3
- 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 +73 -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
@@ -9,7 +9,7 @@ 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
|
@@ -17,13 +17,12 @@ module Roast
|
|
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
|
+
def initialize(workflow, model: "anthropic:claude-opus-4", name: nil, context_path: nil)
|
21
21
|
@workflow = workflow
|
22
22
|
@model = model
|
23
23
|
@name = name || self.class.name.underscore.split("/").last
|
24
24
|
@context_path = context_path || ContextPathResolver.resolve(self.class)
|
25
25
|
@print_response = false
|
26
|
-
@auto_loop = auto_loop
|
27
26
|
@json = false
|
28
27
|
@params = {}
|
29
28
|
@coerce_to = nil
|
@@ -32,7 +31,7 @@ module Roast
|
|
32
31
|
|
33
32
|
def call
|
34
33
|
prompt(read_sidecar_prompt)
|
35
|
-
result = chat_completion(print_response:,
|
34
|
+
result = chat_completion(print_response:, json:, params:)
|
36
35
|
|
37
36
|
# Apply coercion if configured
|
38
37
|
apply_coercion(result)
|
@@ -40,25 +39,24 @@ module Roast
|
|
40
39
|
|
41
40
|
protected
|
42
41
|
|
43
|
-
def chat_completion(print_response: nil,
|
42
|
+
def chat_completion(print_response: nil, json: nil, params: nil)
|
44
43
|
# Use instance variables as defaults if parameters are not provided
|
45
44
|
print_response = @print_response if print_response.nil?
|
46
|
-
auto_loop = @auto_loop if auto_loop.nil?
|
47
45
|
json = @json if json.nil?
|
48
46
|
params = @params if params.nil?
|
49
47
|
|
50
|
-
workflow.chat_completion(openai: workflow.openai? && model,
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
59
59
|
end
|
60
|
-
end.tap do |response|
|
61
|
-
process_output(response, print_response:)
|
62
60
|
end
|
63
61
|
end
|
64
62
|
|
@@ -91,11 +89,11 @@ module Roast
|
|
91
89
|
private
|
92
90
|
|
93
91
|
def apply_coercion(result)
|
94
|
-
return result unless @coerce_to
|
95
|
-
|
96
92
|
case @coerce_to
|
97
93
|
when :boolean
|
98
|
-
# Simple boolean coercion
|
94
|
+
# Simple boolean coercion - empty string is false
|
95
|
+
return false if result.nil? || result == ""
|
96
|
+
|
99
97
|
!!result
|
100
98
|
when :llm_boolean
|
101
99
|
# Use LLM boolean coercer for natural language responses
|
@@ -105,9 +103,19 @@ module Roast
|
|
105
103
|
# Ensure result is iterable
|
106
104
|
return result if result.respond_to?(:each)
|
107
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
|
+
|
108
116
|
result.to_s.split("\n")
|
109
117
|
else
|
110
|
-
# Unknown coercion type, return as-is
|
118
|
+
# Unknown or nil coercion type, return as-is
|
111
119
|
result
|
112
120
|
end
|
113
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
|
|
@@ -43,6 +45,8 @@ module Roast
|
|
43
45
|
# Process target and resource
|
44
46
|
@target = ConfigurationLoader.extract_target(@config_hash, options)
|
45
47
|
process_resource
|
48
|
+
|
49
|
+
mark_last_step_for_output
|
46
50
|
end
|
47
51
|
|
48
52
|
def context_path
|
@@ -82,6 +86,13 @@ module Roast
|
|
82
86
|
@function_configs[function_name.to_s] || {}
|
83
87
|
end
|
84
88
|
|
89
|
+
# Get configuration for a specific tool
|
90
|
+
# @param tool_name [String] The name of the tool (e.g., 'Roast::Tools::Cmd')
|
91
|
+
# @return [Hash] The configuration for the tool or empty hash if not found
|
92
|
+
def tool_config(tool_name)
|
93
|
+
@tool_configs[tool_name.to_s] || {}
|
94
|
+
end
|
95
|
+
|
85
96
|
private
|
86
97
|
|
87
98
|
attr_reader :api_configuration
|
@@ -93,6 +104,60 @@ module Roast
|
|
93
104
|
@target = @resource.value if has_target?
|
94
105
|
end
|
95
106
|
end
|
107
|
+
|
108
|
+
def mark_last_step_for_output
|
109
|
+
return if @steps.empty?
|
110
|
+
|
111
|
+
last_step = find_last_executable_step(@steps.last)
|
112
|
+
return unless last_step
|
113
|
+
|
114
|
+
# Get the step name/key
|
115
|
+
step_key = extract_step_key(last_step)
|
116
|
+
return unless step_key
|
117
|
+
|
118
|
+
# Ensure config exists for this step
|
119
|
+
@config_hash[step_key] ||= {}
|
120
|
+
|
121
|
+
# Only set print_response if not already explicitly configured
|
122
|
+
@config_hash[step_key]["print_response"] = true unless @config_hash[step_key].key?("print_response")
|
123
|
+
end
|
124
|
+
|
125
|
+
def find_last_executable_step(step)
|
126
|
+
case step
|
127
|
+
when String
|
128
|
+
step
|
129
|
+
when Hash
|
130
|
+
# Check if it's a special step type (if, unless, each, repeat, case)
|
131
|
+
if step.key?("if") || step.key?("unless")
|
132
|
+
# For conditional steps, try to find the last step in the "then" branch
|
133
|
+
then_steps = step["then"] || step["steps"]
|
134
|
+
find_last_executable_step(then_steps.last) if then_steps&.any?
|
135
|
+
elsif step.key?("each") || step.key?("repeat")
|
136
|
+
# For iteration steps, we can't reliably determine the last step
|
137
|
+
nil
|
138
|
+
elsif step.key?("case")
|
139
|
+
# For case steps, we can't reliably determine the last step
|
140
|
+
nil
|
141
|
+
elsif step.size == 1
|
142
|
+
# Regular hash step with variable assignment
|
143
|
+
step
|
144
|
+
end
|
145
|
+
when Array
|
146
|
+
# For parallel steps, we can't determine a single "last" step
|
147
|
+
nil
|
148
|
+
else
|
149
|
+
step
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def extract_step_key(step)
|
154
|
+
case step
|
155
|
+
when String
|
156
|
+
step
|
157
|
+
when Hash
|
158
|
+
step.keys.first
|
159
|
+
end
|
160
|
+
end
|
96
161
|
end
|
97
162
|
end
|
98
163
|
end
|
@@ -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}"
|
@@ -95,7 +95,6 @@ module Roast
|
|
95
95
|
# Apply configuration settings to a step
|
96
96
|
def apply_step_configuration(step, step_config)
|
97
97
|
step.print_response = step_config["print_response"] if step_config.key?("print_response")
|
98
|
-
step.auto_loop = step_config["loop"] if step_config.key?("loop")
|
99
98
|
step.json = step_config["json"] if step_config.key?("json")
|
100
99
|
step.params = step_config["params"] if step_config.key?("params")
|
101
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")
|