roast-ai 0.1.6 → 0.2.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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +1 -1
  3. data/CHANGELOG.md +48 -0
  4. data/CLAUDE.md +20 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +11 -6
  7. data/README.md +225 -13
  8. data/bin/roast +27 -0
  9. data/docs/INSTRUMENTATION.md +42 -1
  10. data/docs/ITERATION_SYNTAX.md +119 -0
  11. data/examples/conditional/README.md +161 -0
  12. data/examples/conditional/check_condition/prompt.md +1 -0
  13. data/examples/conditional/simple_workflow.yml +15 -0
  14. data/examples/conditional/workflow.yml +23 -0
  15. data/examples/dot_notation/README.md +37 -0
  16. data/examples/dot_notation/workflow.yml +44 -0
  17. data/examples/exit_on_error/README.md +50 -0
  18. data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
  19. data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
  20. data/examples/exit_on_error/workflow.yml +19 -0
  21. data/examples/grading/workflow.yml +5 -1
  22. data/examples/iteration/IMPLEMENTATION.md +88 -0
  23. data/examples/iteration/README.md +68 -0
  24. data/examples/iteration/analyze_complexity/prompt.md +22 -0
  25. data/examples/iteration/generate_recommendations/prompt.md +21 -0
  26. data/examples/iteration/generate_report/prompt.md +129 -0
  27. data/examples/iteration/implement_fix/prompt.md +25 -0
  28. data/examples/iteration/prioritize_issues/prompt.md +24 -0
  29. data/examples/iteration/prompts/analyze_file.md +28 -0
  30. data/examples/iteration/prompts/generate_summary.md +24 -0
  31. data/examples/iteration/prompts/update_report.md +29 -0
  32. data/examples/iteration/prompts/write_report.md +22 -0
  33. data/examples/iteration/read_file/prompt.md +9 -0
  34. data/examples/iteration/select_next_issue/prompt.md +25 -0
  35. data/examples/iteration/simple_workflow.md +39 -0
  36. data/examples/iteration/simple_workflow.yml +58 -0
  37. data/examples/iteration/update_fix_count/prompt.md +26 -0
  38. data/examples/iteration/verify_fix/prompt.md +29 -0
  39. data/examples/iteration/workflow.yml +42 -0
  40. data/examples/openrouter_example/workflow.yml +2 -2
  41. data/examples/workflow_generator/README.md +27 -0
  42. data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
  43. data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
  44. data/examples/workflow_generator/get_user_input/prompt.md +14 -0
  45. data/examples/workflow_generator/info_from_roast.rb +22 -0
  46. data/examples/workflow_generator/workflow.yml +35 -0
  47. data/lib/roast/errors.rb +9 -0
  48. data/lib/roast/factories/api_provider_factory.rb +61 -0
  49. data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
  50. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  51. data/lib/roast/helpers/prompt_loader.rb +50 -1
  52. data/lib/roast/resources/base_resource.rb +7 -0
  53. data/lib/roast/resources.rb +6 -6
  54. data/lib/roast/tools/ask_user.rb +40 -0
  55. data/lib/roast/tools/cmd.rb +1 -1
  56. data/lib/roast/tools/search_file.rb +1 -1
  57. data/lib/roast/tools/update_files.rb +413 -0
  58. data/lib/roast/tools.rb +12 -1
  59. data/lib/roast/value_objects/api_token.rb +49 -0
  60. data/lib/roast/value_objects/step_name.rb +39 -0
  61. data/lib/roast/value_objects/workflow_path.rb +77 -0
  62. data/lib/roast/value_objects.rb +5 -0
  63. data/lib/roast/version.rb +1 -1
  64. data/lib/roast/workflow/api_configuration.rb +61 -0
  65. data/lib/roast/workflow/base_iteration_step.rb +165 -0
  66. data/lib/roast/workflow/base_step.rb +4 -24
  67. data/lib/roast/workflow/base_workflow.rb +76 -73
  68. data/lib/roast/workflow/command_executor.rb +88 -0
  69. data/lib/roast/workflow/conditional_executor.rb +50 -0
  70. data/lib/roast/workflow/conditional_step.rb +96 -0
  71. data/lib/roast/workflow/configuration.rb +35 -158
  72. data/lib/roast/workflow/configuration_loader.rb +78 -0
  73. data/lib/roast/workflow/configuration_parser.rb +13 -248
  74. data/lib/roast/workflow/context_path_resolver.rb +43 -0
  75. data/lib/roast/workflow/dot_access_hash.rb +198 -0
  76. data/lib/roast/workflow/each_step.rb +86 -0
  77. data/lib/roast/workflow/error_handler.rb +97 -0
  78. data/lib/roast/workflow/expression_utils.rb +36 -0
  79. data/lib/roast/workflow/file_state_repository.rb +3 -2
  80. data/lib/roast/workflow/interpolator.rb +34 -0
  81. data/lib/roast/workflow/iteration_executor.rb +85 -0
  82. data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
  83. data/lib/roast/workflow/output_handler.rb +35 -0
  84. data/lib/roast/workflow/output_manager.rb +77 -0
  85. data/lib/roast/workflow/parallel_executor.rb +49 -0
  86. data/lib/roast/workflow/repeat_step.rb +75 -0
  87. data/lib/roast/workflow/replay_handler.rb +123 -0
  88. data/lib/roast/workflow/resource_resolver.rb +77 -0
  89. data/lib/roast/workflow/session_manager.rb +6 -2
  90. data/lib/roast/workflow/state_manager.rb +97 -0
  91. data/lib/roast/workflow/step_executor_coordinator.rb +205 -0
  92. data/lib/roast/workflow/step_executor_factory.rb +47 -0
  93. data/lib/roast/workflow/step_executor_registry.rb +79 -0
  94. data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
  95. data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
  96. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
  97. data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
  98. data/lib/roast/workflow/step_finder.rb +97 -0
  99. data/lib/roast/workflow/step_loader.rb +154 -0
  100. data/lib/roast/workflow/step_orchestrator.rb +45 -0
  101. data/lib/roast/workflow/step_runner.rb +23 -0
  102. data/lib/roast/workflow/step_type_resolver.rb +117 -0
  103. data/lib/roast/workflow/workflow_context.rb +60 -0
  104. data/lib/roast/workflow/workflow_executor.rb +90 -209
  105. data/lib/roast/workflow/workflow_initializer.rb +112 -0
  106. data/lib/roast/workflow/workflow_runner.rb +87 -0
  107. data/lib/roast/workflow.rb +3 -0
  108. data/lib/roast.rb +96 -3
  109. data/roast.gemspec +3 -1
  110. data/schema/workflow.json +85 -0
  111. metadata +112 -4
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InfoFromRoast < Roast::Workflow::BaseStep
4
+ def call
5
+ examples_path = File.join(Roast::ROOT, "examples")
6
+ tools_path = File.join(Roast::ROOT, "lib", "roast", "tools")
7
+
8
+ # Get list of available tools
9
+ available_tools = Dir.entries(tools_path)
10
+ .select { |file| file.end_with?(".rb") }
11
+ .map { |file| file.gsub(".rb", "") }
12
+ .reject { |tool| tool == "." || tool == ".." }
13
+ .sort
14
+
15
+ {
16
+ examples_directory: examples_path,
17
+ tools_directory: tools_path,
18
+ available_tools: available_tools,
19
+ message: "Examples directory and available tools provided for analysis step",
20
+ }
21
+ end
22
+ end
@@ -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
@@ -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
@@ -4,7 +4,7 @@ require "active_support"
4
4
  require "active_support/isolated_execution_state"
5
5
  require "active_support/cache"
6
6
  require "active_support/notifications"
7
- require_relative "logger"
7
+ require "roast/helpers/logger"
8
8
 
9
9
  module Roast
10
10
  module Helpers
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "coverage"
4
4
  require "minitest"
5
- require_relative "logger"
5
+ require "roast/helpers/logger"
6
6
 
7
7
  # Disable the built-in `at_exit` hook for Minitest before anything else
8
8
  module Minitest
@@ -89,11 +89,60 @@ module Roast
89
89
 
90
90
  def process_erb_if_needed(content)
91
91
  if content.include?("<%")
92
- ERB.new(content, trim_mode: "-").result(context.instance_eval { binding })
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?
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "resources/base_resource"
4
- require_relative "resources/file_resource"
5
- require_relative "resources/directory_resource"
6
- require_relative "resources/url_resource"
7
- require_relative "resources/api_resource"
8
- require_relative "resources/none_resource"
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
@@ -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