roast-ai 0.1.7 → 0.2.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/ci.yaml +1 -1
- data/CHANGELOG.md +49 -1
- data/CLAUDE.md +20 -0
- data/CLAUDE_NOTES.md +68 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +9 -6
- data/README.md +159 -26
- data/bin/roast +27 -0
- data/docs/ITERATION_SYNTAX.md +147 -0
- data/examples/case_when/README.md +58 -0
- data/examples/case_when/detect_language/prompt.md +16 -0
- data/examples/case_when/workflow.yml +58 -0
- data/examples/conditional/README.md +161 -0
- data/examples/conditional/check_condition/prompt.md +1 -0
- data/examples/conditional/simple_workflow.yml +15 -0
- data/examples/conditional/workflow.yml +23 -0
- data/examples/direct_coerce_syntax/README.md +32 -0
- data/examples/direct_coerce_syntax/workflow.yml +36 -0
- data/examples/dot_notation/README.md +37 -0
- data/examples/dot_notation/workflow.yml +44 -0
- data/examples/exit_on_error/README.md +50 -0
- data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
- data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
- data/examples/exit_on_error/workflow.yml +19 -0
- data/examples/grading/workflow.yml +10 -4
- data/examples/iteration/IMPLEMENTATION.md +88 -0
- data/examples/iteration/README.md +68 -0
- data/examples/iteration/analyze_complexity/prompt.md +22 -0
- data/examples/iteration/generate_recommendations/prompt.md +21 -0
- data/examples/iteration/generate_report/prompt.md +129 -0
- data/examples/iteration/implement_fix/prompt.md +25 -0
- data/examples/iteration/prioritize_issues/prompt.md +24 -0
- data/examples/iteration/prompts/analyze_file.md +28 -0
- data/examples/iteration/prompts/generate_summary.md +24 -0
- data/examples/iteration/prompts/update_report.md +29 -0
- data/examples/iteration/prompts/write_report.md +22 -0
- data/examples/iteration/read_file/prompt.md +9 -0
- data/examples/iteration/select_next_issue/prompt.md +25 -0
- data/examples/iteration/simple_workflow.md +39 -0
- data/examples/iteration/simple_workflow.yml +58 -0
- data/examples/iteration/update_fix_count/prompt.md +26 -0
- data/examples/iteration/verify_fix/prompt.md +29 -0
- data/examples/iteration/workflow.yml +42 -0
- data/examples/json_handling/README.md +32 -0
- data/examples/json_handling/workflow.yml +52 -0
- data/examples/openrouter_example/workflow.yml +2 -2
- data/examples/smart_coercion_defaults/README.md +65 -0
- data/examples/smart_coercion_defaults/workflow.yml +44 -0
- data/examples/step_configuration/README.md +87 -0
- data/examples/step_configuration/workflow.yml +60 -0
- data/examples/workflow_generator/README.md +27 -0
- data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
- data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
- data/examples/workflow_generator/get_user_input/prompt.md +14 -0
- data/examples/workflow_generator/info_from_roast.rb +22 -0
- data/examples/workflow_generator/workflow.yml +35 -0
- data/lib/roast/errors.rb +9 -0
- data/lib/roast/factories/api_provider_factory.rb +61 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
- data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
- data/lib/roast/helpers/prompt_loader.rb +50 -1
- data/lib/roast/resources/base_resource.rb +7 -0
- data/lib/roast/resources.rb +6 -6
- data/lib/roast/tools/ask_user.rb +40 -0
- data/lib/roast/tools/cmd.rb +1 -1
- data/lib/roast/tools/search_file.rb +1 -1
- data/lib/roast/tools.rb +11 -1
- data/lib/roast/value_objects/api_token.rb +49 -0
- data/lib/roast/value_objects/step_name.rb +39 -0
- data/lib/roast/value_objects/workflow_path.rb +77 -0
- data/lib/roast/value_objects.rb +5 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/api_configuration.rb +61 -0
- data/lib/roast/workflow/base_iteration_step.rb +184 -0
- data/lib/roast/workflow/base_step.rb +44 -27
- data/lib/roast/workflow/base_workflow.rb +76 -73
- data/lib/roast/workflow/case_executor.rb +49 -0
- data/lib/roast/workflow/case_step.rb +82 -0
- data/lib/roast/workflow/command_executor.rb +88 -0
- data/lib/roast/workflow/conditional_executor.rb +50 -0
- data/lib/roast/workflow/conditional_step.rb +59 -0
- data/lib/roast/workflow/configuration.rb +35 -158
- data/lib/roast/workflow/configuration_loader.rb +78 -0
- data/lib/roast/workflow/configuration_parser.rb +13 -248
- data/lib/roast/workflow/context_path_resolver.rb +43 -0
- data/lib/roast/workflow/dot_access_hash.rb +198 -0
- data/lib/roast/workflow/each_step.rb +86 -0
- data/lib/roast/workflow/error_handler.rb +97 -0
- data/lib/roast/workflow/expression_evaluator.rb +78 -0
- data/lib/roast/workflow/expression_utils.rb +36 -0
- data/lib/roast/workflow/file_state_repository.rb +3 -2
- data/lib/roast/workflow/interpolator.rb +34 -0
- data/lib/roast/workflow/iteration_executor.rb +103 -0
- data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
- data/lib/roast/workflow/output_handler.rb +35 -0
- data/lib/roast/workflow/output_manager.rb +77 -0
- data/lib/roast/workflow/parallel_executor.rb +49 -0
- data/lib/roast/workflow/prompt_step.rb +4 -1
- data/lib/roast/workflow/repeat_step.rb +75 -0
- data/lib/roast/workflow/replay_handler.rb +123 -0
- data/lib/roast/workflow/resource_resolver.rb +77 -0
- data/lib/roast/workflow/session_manager.rb +6 -2
- data/lib/roast/workflow/state_manager.rb +97 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +221 -0
- data/lib/roast/workflow/step_executor_factory.rb +47 -0
- data/lib/roast/workflow/step_executor_registry.rb +79 -0
- data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
- data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
- data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
- data/lib/roast/workflow/step_finder.rb +97 -0
- data/lib/roast/workflow/step_loader.rb +155 -0
- data/lib/roast/workflow/step_orchestrator.rb +45 -0
- data/lib/roast/workflow/step_runner.rb +23 -0
- data/lib/roast/workflow/step_type_resolver.rb +133 -0
- data/lib/roast/workflow/workflow_context.rb +60 -0
- data/lib/roast/workflow/workflow_executor.rb +90 -209
- data/lib/roast/workflow/workflow_initializer.rb +112 -0
- data/lib/roast/workflow/workflow_runner.rb +87 -0
- data/lib/roast/workflow.rb +3 -0
- data/lib/roast.rb +96 -3
- data/roast.gemspec +2 -1
- data/schema/workflow.json +112 -0
- metadata +112 -4
@@ -0,0 +1,35 @@
|
|
1
|
+
# Workflow Generator
|
2
|
+
#
|
3
|
+
# This workflow generates new Roast workflows based on user descriptions.
|
4
|
+
# It gets user input, analyzes the request, generates an appropriate workflow structure,
|
5
|
+
# and creates all necessary files in a new directory.
|
6
|
+
|
7
|
+
name: Workflow Generator
|
8
|
+
model: gpt-4o-mini
|
9
|
+
|
10
|
+
tools:
|
11
|
+
- Roast::Tools::WriteFile
|
12
|
+
- Roast::Tools::ReadFile
|
13
|
+
- Roast::Tools::Cmd
|
14
|
+
- Roast::Tools::AskUser
|
15
|
+
|
16
|
+
steps:
|
17
|
+
- get_user_input
|
18
|
+
- info_from_roast
|
19
|
+
- analyze_user_request
|
20
|
+
- create_workflow_files
|
21
|
+
|
22
|
+
# Step configurations
|
23
|
+
get_user_input:
|
24
|
+
print_response: false
|
25
|
+
json: true
|
26
|
+
auto_loop: false
|
27
|
+
|
28
|
+
analyze_user_request:
|
29
|
+
print_response: true
|
30
|
+
|
31
|
+
generate_workflow_structure:
|
32
|
+
print_response: true
|
33
|
+
|
34
|
+
create_workflow_files:
|
35
|
+
print_response: false
|
data/lib/roast/errors.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
# Custom error for API resource not found (404) responses
|
5
|
+
class ResourceNotFoundError < StandardError; end
|
6
|
+
|
7
|
+
# Custom error for when API authentication fails
|
8
|
+
class AuthenticationError < StandardError; end
|
9
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Factories
|
5
|
+
# Factory for determining and creating API provider configurations
|
6
|
+
class ApiProviderFactory
|
7
|
+
SUPPORTED_PROVIDERS = {
|
8
|
+
"openai" => :openai,
|
9
|
+
"openrouter" => :openrouter,
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
DEFAULT_PROVIDER = :openai
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# Determines the API provider from configuration
|
16
|
+
# @param config [Hash] The configuration hash
|
17
|
+
# @return [Symbol] The API provider symbol (:openai or :openrouter)
|
18
|
+
def from_config(config)
|
19
|
+
return DEFAULT_PROVIDER unless config["api_provider"]
|
20
|
+
|
21
|
+
provider_name = config["api_provider"].to_s.downcase
|
22
|
+
provider = SUPPORTED_PROVIDERS[provider_name]
|
23
|
+
|
24
|
+
unless provider
|
25
|
+
Roast::Helpers::Logger.warn("Unknown API provider '#{provider_name}', defaulting to #{DEFAULT_PROVIDER}")
|
26
|
+
return DEFAULT_PROVIDER
|
27
|
+
end
|
28
|
+
|
29
|
+
provider
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns true if the provider is OpenRouter
|
33
|
+
# @param provider [Symbol] The provider symbol
|
34
|
+
# @return [Boolean]
|
35
|
+
def openrouter?(provider)
|
36
|
+
provider == :openrouter
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns true if the provider is OpenAI
|
40
|
+
# @param provider [Symbol] The provider symbol
|
41
|
+
# @return [Boolean]
|
42
|
+
def openai?(provider)
|
43
|
+
provider == :openai
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the list of supported provider names
|
47
|
+
# @return [Array<String>]
|
48
|
+
def supported_provider_names
|
49
|
+
SUPPORTED_PROVIDERS.keys
|
50
|
+
end
|
51
|
+
|
52
|
+
# Validates a provider symbol
|
53
|
+
# @param provider [Symbol] The provider to validate
|
54
|
+
# @return [Boolean]
|
55
|
+
def valid_provider?(provider)
|
56
|
+
SUPPORTED_PROVIDERS.values.include?(provider)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -89,11 +89,60 @@ module Roast
|
|
89
89
|
|
90
90
|
def process_erb_if_needed(content)
|
91
91
|
if content.include?("<%")
|
92
|
-
|
92
|
+
begin
|
93
|
+
ERB.new(content, trim_mode: "-").result(context.instance_eval { binding })
|
94
|
+
rescue TypeError => e
|
95
|
+
if e.message.include?("no implicit conversion of nil into String")
|
96
|
+
# Try to find which variable is causing the issue
|
97
|
+
variable_hint = detect_nil_variable(content)
|
98
|
+
|
99
|
+
error_message = <<~ERROR
|
100
|
+
This workflow requires a file or target to be specified.
|
101
|
+
#{variable_hint}
|
102
|
+
|
103
|
+
Usage: roast execute <workflow.yml> <file_or_pattern>
|
104
|
+
|
105
|
+
Examples:
|
106
|
+
roast execute #{context.respond_to?(:configuration) && context.configuration&.workflow_path || "workflow.yml"} test/my_test.rb
|
107
|
+
roast execute #{context.respond_to?(:configuration) && context.configuration&.workflow_path || "workflow.yml"} "test/**/*_test.rb"
|
108
|
+
ERROR
|
109
|
+
raise error_message
|
110
|
+
else
|
111
|
+
raise e
|
112
|
+
end
|
113
|
+
rescue NoMethodError => e
|
114
|
+
if e.message.include?("undefined method") && e.message.include?("for nil")
|
115
|
+
variable_hint = detect_nil_variable(content)
|
116
|
+
|
117
|
+
error_message = <<~ERROR
|
118
|
+
Error processing prompt template: #{e.message}
|
119
|
+
#{variable_hint}
|
120
|
+
|
121
|
+
This may indicate that the workflow requires a file or target to be specified.
|
122
|
+
|
123
|
+
Usage: roast execute <workflow.yml> <file_or_pattern>
|
124
|
+
ERROR
|
125
|
+
raise error_message
|
126
|
+
else
|
127
|
+
raise e
|
128
|
+
end
|
129
|
+
end
|
93
130
|
else
|
94
131
|
content
|
95
132
|
end
|
96
133
|
end
|
134
|
+
|
135
|
+
def detect_nil_variable(content)
|
136
|
+
if content.include?("workflow.file")
|
137
|
+
"The prompt template references 'workflow.file' but no file was provided."
|
138
|
+
elsif content.include?("<%= file %>")
|
139
|
+
"The prompt template references 'file' but no file was provided."
|
140
|
+
elsif content.match(/<%= .*?\.(\w+) %>/)
|
141
|
+
"The prompt template is trying to access a property that doesn't exist."
|
142
|
+
else
|
143
|
+
"The prompt template contains an ERB expression that references a nil value."
|
144
|
+
end
|
145
|
+
end
|
97
146
|
end
|
98
147
|
end
|
99
148
|
end
|
@@ -19,6 +19,13 @@ module Roast
|
|
19
19
|
target
|
20
20
|
end
|
21
21
|
|
22
|
+
# Get the value of the resource (alias for target)
|
23
|
+
# Used for backward compatibility
|
24
|
+
# @return [String] The resource target value
|
25
|
+
def value
|
26
|
+
target
|
27
|
+
end
|
28
|
+
|
22
29
|
# Check if the resource exists
|
23
30
|
# @return [Boolean] true if the resource exists
|
24
31
|
def exists?
|
data/lib/roast/resources.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
require "roast/resources/base_resource"
|
4
|
+
require "roast/resources/file_resource"
|
5
|
+
require "roast/resources/directory_resource"
|
6
|
+
require "roast/resources/url_resource"
|
7
|
+
require "roast/resources/api_resource"
|
8
|
+
require "roast/resources/none_resource"
|
9
9
|
require "uri"
|
10
10
|
|
11
11
|
module Roast
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/helpers/logger"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Tools
|
7
|
+
module AskUser
|
8
|
+
extend self
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Add this method to be included in other classes
|
12
|
+
def included(base)
|
13
|
+
base.class_eval do
|
14
|
+
function(
|
15
|
+
:ask_user,
|
16
|
+
"Ask the user for input with a specific prompt. Returns the user's response.",
|
17
|
+
prompt: { type: "string", description: "The prompt to show the user" },
|
18
|
+
) do |params|
|
19
|
+
Roast::Tools::AskUser.call(params[:prompt])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(prompt)
|
26
|
+
Roast::Helpers::Logger.info("💬 Asking user: #{prompt}\n")
|
27
|
+
|
28
|
+
response = ::CLI::UI::Prompt.ask(prompt)
|
29
|
+
|
30
|
+
Roast::Helpers::Logger.info("User responded: #{response}\n")
|
31
|
+
response
|
32
|
+
rescue StandardError => e
|
33
|
+
"Error getting user input: #{e.message}".tap do |error_message|
|
34
|
+
Roast::Helpers::Logger.error(error_message + "\n")
|
35
|
+
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/roast/tools/cmd.rb
CHANGED
@@ -28,7 +28,7 @@ module Roast
|
|
28
28
|
Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
|
29
29
|
|
30
30
|
# Validate the command starts with one of the allowed prefixes
|
31
|
-
allowed_prefixes = ["pwd", "find", "ls", "rake", "ruby", "dev"]
|
31
|
+
allowed_prefixes = ["pwd", "find", "ls", "rake", "ruby", "dev", "mkdir"]
|
32
32
|
command_prefix = command.split(" ").first
|
33
33
|
|
34
34
|
err = "Error: Command not allowed. Only commands starting with #{allowed_prefixes.join(", ")} are permitted."
|
@@ -26,7 +26,7 @@ module Roast
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def call(glob_pattern, path = ".")
|
29
|
-
Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{path}'\n")
|
29
|
+
Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{File.expand_path(path)}'\n")
|
30
30
|
search_for(glob_pattern, path).then do |results|
|
31
31
|
return "No results found for #{glob_pattern} in #{path}" if results.empty?
|
32
32
|
return read_contents(results.first) if results.size == 1
|
data/lib/roast/tools.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "active_support/cache"
|
4
4
|
require "English"
|
5
|
+
require "fileutils"
|
5
6
|
|
6
7
|
require "roast/tools/grep"
|
7
8
|
require "roast/tools/read_file"
|
@@ -10,12 +11,21 @@ require "roast/tools/write_file"
|
|
10
11
|
require "roast/tools/update_files"
|
11
12
|
require "roast/tools/cmd"
|
12
13
|
require "roast/tools/coding_agent"
|
14
|
+
require "roast/tools/ask_user"
|
13
15
|
|
14
16
|
module Roast
|
15
17
|
module Tools
|
16
18
|
extend self
|
17
19
|
|
18
|
-
|
20
|
+
# Initialize cache and ensure .gitignore exists
|
21
|
+
cache_dir = File.join(Dir.pwd, ".roast", "cache")
|
22
|
+
FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir)
|
23
|
+
|
24
|
+
# Add .gitignore to cache directory
|
25
|
+
gitignore_path = File.join(cache_dir, ".gitignore")
|
26
|
+
File.write(gitignore_path, "*") unless File.exist?(gitignore_path)
|
27
|
+
|
28
|
+
CACHE = ActiveSupport::Cache::FileStore.new(cache_dir)
|
19
29
|
|
20
30
|
def file_to_prompt(file)
|
21
31
|
<<~PROMPT
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module ValueObjects
|
5
|
+
# Value object representing an API token with validation
|
6
|
+
class ApiToken
|
7
|
+
class InvalidTokenError < StandardError; end
|
8
|
+
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
def initialize(value)
|
12
|
+
@value = value&.to_s
|
13
|
+
validate!
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def present?
|
18
|
+
!blank?
|
19
|
+
end
|
20
|
+
|
21
|
+
def blank?
|
22
|
+
@value.nil? || @value.strip.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
@value
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
return false unless other.is_a?(ApiToken)
|
31
|
+
|
32
|
+
value == other.value
|
33
|
+
end
|
34
|
+
alias_method :eql?, :==
|
35
|
+
|
36
|
+
def hash
|
37
|
+
[self.class, @value].hash
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def validate!
|
43
|
+
return if @value.nil? # Allow nil tokens, just not empty strings
|
44
|
+
|
45
|
+
raise InvalidTokenError, "API token cannot be an empty string" if @value.strip.empty?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module ValueObjects
|
5
|
+
# Value object representing a step name, which can be either a plain text prompt
|
6
|
+
# or a reference to a step file
|
7
|
+
class StepName
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@value = name.to_s.strip
|
12
|
+
freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def plain_text?
|
16
|
+
@value.include?(" ")
|
17
|
+
end
|
18
|
+
|
19
|
+
def file_reference?
|
20
|
+
!plain_text?
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
@value
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
return false unless other.is_a?(StepName)
|
29
|
+
|
30
|
+
value == other.value
|
31
|
+
end
|
32
|
+
alias_method :eql?, :==
|
33
|
+
|
34
|
+
def hash
|
35
|
+
[self.class, @value].hash
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module ValueObjects
|
7
|
+
# Value object representing a workflow file path with validation and resolution
|
8
|
+
class WorkflowPath
|
9
|
+
class InvalidPathError < StandardError; end
|
10
|
+
|
11
|
+
attr_reader :value
|
12
|
+
|
13
|
+
def initialize(path)
|
14
|
+
@value = normalize_path(path)
|
15
|
+
@pathname = Pathname.new(@value)
|
16
|
+
validate!
|
17
|
+
freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def exist?
|
21
|
+
pathname.exist?
|
22
|
+
end
|
23
|
+
|
24
|
+
def absolute?
|
25
|
+
pathname.absolute?
|
26
|
+
end
|
27
|
+
|
28
|
+
def relative?
|
29
|
+
pathname.relative?
|
30
|
+
end
|
31
|
+
|
32
|
+
def dirname
|
33
|
+
pathname.dirname.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def basename
|
37
|
+
pathname.basename.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@value
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_path
|
45
|
+
@value
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
return false unless other.is_a?(WorkflowPath)
|
50
|
+
|
51
|
+
value == other.value
|
52
|
+
end
|
53
|
+
alias_method :eql?, :==
|
54
|
+
|
55
|
+
def hash
|
56
|
+
[self.class, @value].hash
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :pathname
|
62
|
+
|
63
|
+
def normalize_path(path)
|
64
|
+
path.to_s.strip
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate!
|
68
|
+
raise InvalidPathError, "Workflow path cannot be empty" if @value.empty?
|
69
|
+
raise InvalidPathError, "Workflow path must have .yml or .yaml extension" unless valid_extension?
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid_extension?
|
73
|
+
@value.end_with?(".yml") || @value.end_with?(".yaml")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/roast/version.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/factories/api_provider_factory"
|
4
|
+
require "roast/workflow/resource_resolver"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Workflow
|
8
|
+
# Handles API-related configuration including tokens and providers
|
9
|
+
class ApiConfiguration
|
10
|
+
attr_reader :api_token, :api_provider
|
11
|
+
|
12
|
+
def initialize(config_hash)
|
13
|
+
@config_hash = config_hash
|
14
|
+
process_api_configuration
|
15
|
+
end
|
16
|
+
|
17
|
+
# Check if using OpenRouter
|
18
|
+
# @return [Boolean] true if using OpenRouter
|
19
|
+
def openrouter?
|
20
|
+
Roast::Factories::ApiProviderFactory.openrouter?(@api_provider)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if using OpenAI
|
24
|
+
# @return [Boolean] true if using OpenAI
|
25
|
+
def openai?
|
26
|
+
Roast::Factories::ApiProviderFactory.openai?(@api_provider)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the effective API token including environment variables
|
30
|
+
# @return [String, nil] The API token
|
31
|
+
def effective_token
|
32
|
+
@api_token || environment_token
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def process_api_configuration
|
38
|
+
extract_api_token
|
39
|
+
extract_api_provider
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_api_token
|
43
|
+
if @config_hash["api_token"]
|
44
|
+
@api_token = ResourceResolver.process_shell_command(@config_hash["api_token"])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def extract_api_provider
|
49
|
+
@api_provider = Roast::Factories::ApiProviderFactory.from_config(@config_hash)
|
50
|
+
end
|
51
|
+
|
52
|
+
def environment_token
|
53
|
+
if openai?
|
54
|
+
ENV["OPENAI_API_KEY"]
|
55
|
+
elsif openrouter?
|
56
|
+
ENV["OPENROUTER_API_KEY"]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|