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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +1 -1
  3. data/CHANGELOG.md +49 -1
  4. data/CLAUDE.md +20 -0
  5. data/CLAUDE_NOTES.md +68 -0
  6. data/Gemfile +1 -0
  7. data/Gemfile.lock +9 -6
  8. data/README.md +159 -26
  9. data/bin/roast +27 -0
  10. data/docs/ITERATION_SYNTAX.md +147 -0
  11. data/examples/case_when/README.md +58 -0
  12. data/examples/case_when/detect_language/prompt.md +16 -0
  13. data/examples/case_when/workflow.yml +58 -0
  14. data/examples/conditional/README.md +161 -0
  15. data/examples/conditional/check_condition/prompt.md +1 -0
  16. data/examples/conditional/simple_workflow.yml +15 -0
  17. data/examples/conditional/workflow.yml +23 -0
  18. data/examples/direct_coerce_syntax/README.md +32 -0
  19. data/examples/direct_coerce_syntax/workflow.yml +36 -0
  20. data/examples/dot_notation/README.md +37 -0
  21. data/examples/dot_notation/workflow.yml +44 -0
  22. data/examples/exit_on_error/README.md +50 -0
  23. data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
  24. data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
  25. data/examples/exit_on_error/workflow.yml +19 -0
  26. data/examples/grading/workflow.yml +10 -4
  27. data/examples/iteration/IMPLEMENTATION.md +88 -0
  28. data/examples/iteration/README.md +68 -0
  29. data/examples/iteration/analyze_complexity/prompt.md +22 -0
  30. data/examples/iteration/generate_recommendations/prompt.md +21 -0
  31. data/examples/iteration/generate_report/prompt.md +129 -0
  32. data/examples/iteration/implement_fix/prompt.md +25 -0
  33. data/examples/iteration/prioritize_issues/prompt.md +24 -0
  34. data/examples/iteration/prompts/analyze_file.md +28 -0
  35. data/examples/iteration/prompts/generate_summary.md +24 -0
  36. data/examples/iteration/prompts/update_report.md +29 -0
  37. data/examples/iteration/prompts/write_report.md +22 -0
  38. data/examples/iteration/read_file/prompt.md +9 -0
  39. data/examples/iteration/select_next_issue/prompt.md +25 -0
  40. data/examples/iteration/simple_workflow.md +39 -0
  41. data/examples/iteration/simple_workflow.yml +58 -0
  42. data/examples/iteration/update_fix_count/prompt.md +26 -0
  43. data/examples/iteration/verify_fix/prompt.md +29 -0
  44. data/examples/iteration/workflow.yml +42 -0
  45. data/examples/json_handling/README.md +32 -0
  46. data/examples/json_handling/workflow.yml +52 -0
  47. data/examples/openrouter_example/workflow.yml +2 -2
  48. data/examples/smart_coercion_defaults/README.md +65 -0
  49. data/examples/smart_coercion_defaults/workflow.yml +44 -0
  50. data/examples/step_configuration/README.md +87 -0
  51. data/examples/step_configuration/workflow.yml +60 -0
  52. data/examples/workflow_generator/README.md +27 -0
  53. data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
  54. data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
  55. data/examples/workflow_generator/get_user_input/prompt.md +14 -0
  56. data/examples/workflow_generator/info_from_roast.rb +22 -0
  57. data/examples/workflow_generator/workflow.yml +35 -0
  58. data/lib/roast/errors.rb +9 -0
  59. data/lib/roast/factories/api_provider_factory.rb +61 -0
  60. data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
  61. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  62. data/lib/roast/helpers/prompt_loader.rb +50 -1
  63. data/lib/roast/resources/base_resource.rb +7 -0
  64. data/lib/roast/resources.rb +6 -6
  65. data/lib/roast/tools/ask_user.rb +40 -0
  66. data/lib/roast/tools/cmd.rb +1 -1
  67. data/lib/roast/tools/search_file.rb +1 -1
  68. data/lib/roast/tools.rb +11 -1
  69. data/lib/roast/value_objects/api_token.rb +49 -0
  70. data/lib/roast/value_objects/step_name.rb +39 -0
  71. data/lib/roast/value_objects/workflow_path.rb +77 -0
  72. data/lib/roast/value_objects.rb +5 -0
  73. data/lib/roast/version.rb +1 -1
  74. data/lib/roast/workflow/api_configuration.rb +61 -0
  75. data/lib/roast/workflow/base_iteration_step.rb +184 -0
  76. data/lib/roast/workflow/base_step.rb +44 -27
  77. data/lib/roast/workflow/base_workflow.rb +76 -73
  78. data/lib/roast/workflow/case_executor.rb +49 -0
  79. data/lib/roast/workflow/case_step.rb +82 -0
  80. data/lib/roast/workflow/command_executor.rb +88 -0
  81. data/lib/roast/workflow/conditional_executor.rb +50 -0
  82. data/lib/roast/workflow/conditional_step.rb +59 -0
  83. data/lib/roast/workflow/configuration.rb +35 -158
  84. data/lib/roast/workflow/configuration_loader.rb +78 -0
  85. data/lib/roast/workflow/configuration_parser.rb +13 -248
  86. data/lib/roast/workflow/context_path_resolver.rb +43 -0
  87. data/lib/roast/workflow/dot_access_hash.rb +198 -0
  88. data/lib/roast/workflow/each_step.rb +86 -0
  89. data/lib/roast/workflow/error_handler.rb +97 -0
  90. data/lib/roast/workflow/expression_evaluator.rb +78 -0
  91. data/lib/roast/workflow/expression_utils.rb +36 -0
  92. data/lib/roast/workflow/file_state_repository.rb +3 -2
  93. data/lib/roast/workflow/interpolator.rb +34 -0
  94. data/lib/roast/workflow/iteration_executor.rb +103 -0
  95. data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
  96. data/lib/roast/workflow/output_handler.rb +35 -0
  97. data/lib/roast/workflow/output_manager.rb +77 -0
  98. data/lib/roast/workflow/parallel_executor.rb +49 -0
  99. data/lib/roast/workflow/prompt_step.rb +4 -1
  100. data/lib/roast/workflow/repeat_step.rb +75 -0
  101. data/lib/roast/workflow/replay_handler.rb +123 -0
  102. data/lib/roast/workflow/resource_resolver.rb +77 -0
  103. data/lib/roast/workflow/session_manager.rb +6 -2
  104. data/lib/roast/workflow/state_manager.rb +97 -0
  105. data/lib/roast/workflow/step_executor_coordinator.rb +221 -0
  106. data/lib/roast/workflow/step_executor_factory.rb +47 -0
  107. data/lib/roast/workflow/step_executor_registry.rb +79 -0
  108. data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
  109. data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
  110. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
  111. data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
  112. data/lib/roast/workflow/step_finder.rb +97 -0
  113. data/lib/roast/workflow/step_loader.rb +155 -0
  114. data/lib/roast/workflow/step_orchestrator.rb +45 -0
  115. data/lib/roast/workflow/step_runner.rb +23 -0
  116. data/lib/roast/workflow/step_type_resolver.rb +133 -0
  117. data/lib/roast/workflow/workflow_context.rb +60 -0
  118. data/lib/roast/workflow/workflow_executor.rb +90 -209
  119. data/lib/roast/workflow/workflow_initializer.rb +112 -0
  120. data/lib/roast/workflow/workflow_runner.rb +87 -0
  121. data/lib/roast/workflow.rb +3 -0
  122. data/lib/roast.rb +96 -3
  123. data/roast.gemspec +2 -1
  124. data/schema/workflow.json +112 -0
  125. metadata +112 -4
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+
5
+ module Roast
6
+ module Workflow
7
+ class CommandExecutor
8
+ class CommandExecutionError < StandardError
9
+ attr_reader :command, :exit_status, :original_error
10
+
11
+ def initialize(message, command:, exit_status: nil, original_error: nil)
12
+ @command = command
13
+ @exit_status = exit_status
14
+ @original_error = original_error
15
+ super(message)
16
+ end
17
+ end
18
+
19
+ def initialize(logger: nil)
20
+ @logger = logger || NullLogger.new
21
+ end
22
+
23
+ def execute(command_string, exit_on_error: true)
24
+ command = extract_command(command_string)
25
+
26
+ output = %x(#{command})
27
+ exit_status = $CHILD_STATUS.exitstatus
28
+
29
+ handle_execution_result(
30
+ command: command,
31
+ output: output,
32
+ exit_status: exit_status,
33
+ success: $CHILD_STATUS.success?,
34
+ exit_on_error: exit_on_error,
35
+ )
36
+ rescue ArgumentError, CommandExecutionError
37
+ raise
38
+ rescue => e
39
+ handle_execution_error(
40
+ command: command,
41
+ error: e,
42
+ exit_on_error: exit_on_error,
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ def extract_command(command_string)
49
+ match = command_string.strip.match(/^\$\((.*)\)$/)
50
+ raise ArgumentError, "Invalid command format. Expected $(command), got: #{command_string}" unless match
51
+
52
+ match[1]
53
+ end
54
+
55
+ def handle_execution_result(command:, output:, exit_status:, success:, exit_on_error:)
56
+ return output if success
57
+
58
+ if exit_on_error
59
+ raise CommandExecutionError.new(
60
+ "Command exited with non-zero status (#{exit_status})",
61
+ command: command,
62
+ exit_status: exit_status,
63
+ )
64
+ else
65
+ @logger.warn("Command '#{command}' exited with non-zero status (#{exit_status}), continuing execution")
66
+ output + "\n[Exit status: #{exit_status}]"
67
+ end
68
+ end
69
+
70
+ def handle_execution_error(command:, error:, exit_on_error:)
71
+ if exit_on_error
72
+ raise CommandExecutionError.new(
73
+ "Failed to execute command '#{command}': #{error.message}",
74
+ command: command,
75
+ original_error: error,
76
+ )
77
+ else
78
+ @logger.warn("Command '#{command}' failed with error: #{error.message}, continuing execution")
79
+ "Error executing command: #{error.message}\n[Exit status: error]"
80
+ end
81
+ end
82
+
83
+ class NullLogger
84
+ def warn(_message); end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Handles execution of conditional steps (if and unless)
6
+ class ConditionalExecutor
7
+ def initialize(workflow, context_path, state_manager, workflow_executor = nil)
8
+ @workflow = workflow
9
+ @context_path = context_path
10
+ @state_manager = state_manager
11
+ @workflow_executor = workflow_executor
12
+ end
13
+
14
+ def execute_conditional(conditional_config)
15
+ $stderr.puts "Executing conditional step: #{conditional_config.inspect}"
16
+
17
+ # Determine if this is an 'if' or 'unless' condition
18
+ condition_expr = conditional_config["if"] || conditional_config["unless"]
19
+ is_unless = conditional_config.key?("unless")
20
+ then_steps = conditional_config["then"]
21
+
22
+ # Verify required parameters
23
+ raise WorkflowExecutor::ConfigurationError, "Missing condition in conditional configuration" unless condition_expr
24
+ raise WorkflowExecutor::ConfigurationError, "Missing 'then' steps in conditional configuration" unless then_steps
25
+
26
+ # Create and execute a ConditionalStep
27
+ require "roast/workflow/conditional_step" unless defined?(Roast::Workflow::ConditionalStep)
28
+ conditional_step = ConditionalStep.new(
29
+ @workflow,
30
+ config: conditional_config,
31
+ name: "conditional_#{condition_expr.gsub(/[^a-zA-Z0-9_]/, "_")[0..20]}",
32
+ context_path: @context_path,
33
+ workflow_executor: @workflow_executor,
34
+ )
35
+
36
+ result = conditional_step.call
37
+
38
+ # Store a marker in workflow output to indicate which branch was taken
39
+ condition_key = is_unless ? "unless" : "if"
40
+ step_name = "#{condition_key}_#{condition_expr.gsub(/[^a-zA-Z0-9_]/, "_")[0..30]}"
41
+ @workflow.output[step_name] = result
42
+
43
+ # Save state
44
+ @state_manager.save_state(step_name, @workflow.output[step_name])
45
+
46
+ result
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/workflow/base_step"
4
+ require "roast/workflow/expression_evaluator"
5
+ require "roast/workflow/interpolator"
6
+
7
+ module Roast
8
+ module Workflow
9
+ class ConditionalStep < BaseStep
10
+ include ExpressionEvaluator
11
+
12
+ def initialize(workflow, config:, name:, context_path:, workflow_executor:, **kwargs)
13
+ super(workflow, name: name, context_path: context_path, **kwargs)
14
+
15
+ @config = config
16
+ @condition = config["if"] || config["unless"]
17
+ @is_unless = config.key?("unless")
18
+ @then_steps = config["then"] || []
19
+ @else_steps = config["else"] || []
20
+ @workflow_executor = workflow_executor
21
+ end
22
+
23
+ def call
24
+ # Evaluate the condition
25
+ condition_result = evaluate_condition(@condition)
26
+
27
+ # Invert the result if this is an 'unless' condition
28
+ condition_result = !condition_result if @is_unless
29
+
30
+ # Select which steps to execute based on the condition
31
+ steps_to_execute = condition_result ? @then_steps : @else_steps
32
+
33
+ # Execute the selected steps
34
+ unless steps_to_execute.empty?
35
+ @workflow_executor.execute_steps(steps_to_execute)
36
+ end
37
+
38
+ # Return a result indicating which branch was taken
39
+ { condition_result: condition_result, branch_executed: condition_result ? "then" : "else" }
40
+ end
41
+
42
+ private
43
+
44
+ def evaluate_condition(condition)
45
+ return false unless condition.is_a?(String)
46
+
47
+ if ruby_expression?(condition)
48
+ # For conditionals, coerce result to boolean
49
+ !!evaluate_ruby_expression(condition)
50
+ elsif bash_command?(condition)
51
+ evaluate_bash_command(condition, for_condition: true)
52
+ else
53
+ # Treat as a step name or direct boolean
54
+ evaluate_step_or_value(condition, for_condition: true)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,55 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "yaml"
3
+ require "active_support/core_ext/module/delegation"
4
+ require "roast/workflow/api_configuration"
5
+ require "roast/workflow/configuration_loader"
6
+ require "roast/workflow/resource_resolver"
7
+ require "roast/workflow/step_finder"
5
8
 
6
9
  module Roast
7
10
  module Workflow
8
11
  # Encapsulates workflow configuration data and provides structured access
9
12
  # to the configuration settings
10
13
  class Configuration
11
- attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :api_token, :api_provider, :model, :resource
14
+ attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :model, :resource
12
15
  attr_accessor :target
13
16
 
14
- def initialize(workflow_path, options = {})
15
- @workflow_path = workflow_path
16
- @config_hash = YAML.load_file(workflow_path)
17
-
18
- # Extract key configuration values
19
- @name = @config_hash["name"] || File.basename(workflow_path, ".yml")
20
- @steps = @config_hash["steps"] || []
17
+ delegate :api_provider, :openrouter?, :openai?, to: :api_configuration
21
18
 
22
- # Process tools configuration
23
- parse_tools
24
-
25
- # Process function-specific configurations
26
- parse_functions
27
-
28
- # Read the target parameter
29
- @target = options[:target] || @config_hash["target"]
19
+ # Delegate api_token to effective_token for backward compatibility
20
+ def api_token
21
+ @api_configuration.effective_token
22
+ end
30
23
 
31
- # Process the target command if it's a shell command
32
- @target = process_target(@target) if has_target?
24
+ def initialize(workflow_path, options = {})
25
+ @workflow_path = workflow_path
33
26
 
34
- # Create the appropriate resource object for the target
35
- if defined?(Roast::Resources)
36
- @resource = if has_target?
37
- Roast::Resources.for(@target)
38
- else
39
- Roast::Resources::NoneResource.new(nil)
40
- end
41
- end
27
+ # Load configuration using ConfigurationLoader
28
+ @config_hash = ConfigurationLoader.load(workflow_path)
42
29
 
43
- # Process API token if provided
44
- if @config_hash["api_token"]
45
- @api_token = process_shell_command(@config_hash["api_token"])
46
- end
30
+ # Extract basic configuration values
31
+ @name = ConfigurationLoader.extract_name(@config_hash, workflow_path)
32
+ @steps = ConfigurationLoader.extract_steps(@config_hash)
33
+ @tools = ConfigurationLoader.extract_tools(@config_hash)
34
+ @function_configs = ConfigurationLoader.extract_functions(@config_hash)
35
+ @model = ConfigurationLoader.extract_model(@config_hash)
47
36
 
48
- # Determine API provider (defaults to OpenAI if not specified)
49
- @api_provider = determine_api_provider
37
+ # Initialize components
38
+ @api_configuration = ApiConfiguration.new(@config_hash)
39
+ @step_finder = StepFinder.new(@steps)
50
40
 
51
- # Extract default model if provided
52
- @model = @config_hash["model"]
41
+ # Process target and resource
42
+ @target = ConfigurationLoader.extract_target(@config_hash, options)
43
+ process_resource
53
44
  end
54
45
 
55
46
  def context_path
@@ -76,62 +67,10 @@ module Roast
76
67
  # Handle different call patterns for backward compatibility
77
68
  if steps_array.is_a?(String) && target_step.nil?
78
69
  target_step = steps_array
79
- steps_array = steps
80
- elsif steps_array.is_a?(Array) && target_step.is_a?(String)
81
- # This is the normal case - steps_array and target_step are provided
82
- else
83
- # Default to self.steps if just the target_step is provided
84
- steps_array = steps
70
+ steps_array = nil
85
71
  end
86
72
 
87
- # First, try using the new more detailed search
88
- steps_array.each_with_index do |step, index|
89
- case step
90
- when Hash
91
- # Could be {name: command} or {name: {substeps}}
92
- step_key = step.keys.first
93
- return index if step_key == target_step
94
- when Array
95
- # This is a parallel step container, search inside it
96
- found = step.any? do |substep|
97
- case substep
98
- when Hash
99
- substep.keys.first == target_step
100
- when String
101
- substep == target_step
102
- else
103
- false
104
- end
105
- end
106
- return index if found
107
- when String
108
- return index if step == target_step
109
- end
110
- end
111
-
112
- # Fall back to the original method using extract_step_name
113
- steps_array.each_with_index do |step, index|
114
- step_name = extract_step_name(step)
115
- if step_name.is_a?(Array)
116
- # For arrays (parallel steps), check if target is in the array
117
- return index if step_name.flatten.include?(target_step)
118
- elsif step_name == target_step
119
- return index
120
- end
121
- end
122
-
123
- nil
124
- end
125
-
126
- # Returns an array of all tool class names
127
- def parse_tools
128
- # Only support array format: ["Roast::Tools::Grep", "Roast::Tools::ReadFile"]
129
- @tools = @config_hash["tools"] || []
130
- end
131
-
132
- # Parse function-specific configurations
133
- def parse_functions
134
- @function_configs = @config_hash["functions"] || {}
73
+ @step_finder.find_index(target_step, steps_array)
135
74
  end
136
75
 
137
76
  # Get configuration for a specific function
@@ -141,77 +80,15 @@ module Roast
141
80
  @function_configs[function_name.to_s] || {}
142
81
  end
143
82
 
144
- def openrouter?
145
- @api_provider == :openrouter
146
- end
147
-
148
- def openai?
149
- @api_provider == :openai
150
- end
151
-
152
83
  private
153
84
 
154
- def determine_api_provider
155
- return :openai unless @config_hash["api_provider"]
156
-
157
- provider = @config_hash["api_provider"].to_s.downcase
158
-
159
- case provider
160
- when "openai"
161
- :openai
162
- when "openrouter"
163
- :openrouter
164
- else
165
- Roast::Helpers::Logger.warn("Unknown API provider '#{provider}', defaulting to OpenAI")
166
- :openai
167
- end
168
- end
169
-
170
- def process_shell_command(command)
171
- # If it's a bash command with the $(command) syntax
172
- if command =~ /^\$\((.*)\)$/
173
- return Open3.capture2e({}, ::Regexp.last_match(1)).first.strip
174
- end
175
-
176
- # Legacy % prefix for backward compatibility
177
- if command.start_with?("% ")
178
- return Open3.capture2e({}, *command.split(" ")[1..-1]).first.strip
179
- end
180
-
181
- # Not a shell command, return as is
182
- command
183
- end
184
-
185
- def process_target(command)
186
- # Process shell command first
187
- processed = process_shell_command(command)
188
-
189
- # If it's a glob pattern, return the full paths of the files it matches
190
- if processed.include?("*")
191
- matched_files = Dir.glob(processed)
192
- # If no files match, return the pattern itself
193
- return processed if matched_files.empty?
194
-
195
- return matched_files.map { |file| File.expand_path(file) }.join("\n")
196
- end
197
-
198
- # For tests, if the command was already processed as a shell command and is simple,
199
- # don't expand the path to avoid breaking existing tests
200
- return processed if command != processed && !processed.include?("/")
85
+ attr_reader :api_configuration
201
86
 
202
- # assumed to be a direct file path(s)
203
- File.expand_path(processed)
204
- end
205
-
206
- def extract_step_name(step)
207
- case step
208
- when String
209
- step
210
- when Hash
211
- step.keys.first
212
- when Array
213
- # For arrays, we'll need special handling as they contain multiple steps
214
- step.map { |s| extract_step_name(s) }
87
+ def process_resource
88
+ if defined?(Roast::Resources)
89
+ @resource = ResourceResolver.resolve(@target, context_path)
90
+ # Update target with processed value for backward compatibility
91
+ @target = @resource.value if has_target?
215
92
  end
216
93
  end
217
94
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Roast
6
+ module Workflow
7
+ # Handles loading and parsing of workflow configuration files
8
+ class ConfigurationLoader
9
+ class << self
10
+ # Load configuration from a YAML file
11
+ # @param workflow_path [String] Path to the workflow YAML file
12
+ # @return [Hash] The parsed configuration hash
13
+ def load(workflow_path)
14
+ validate_path!(workflow_path)
15
+ config_hash = YAML.load_file(workflow_path)
16
+ validate_config!(config_hash)
17
+ config_hash
18
+ end
19
+
20
+ # Extract the workflow name from config or path
21
+ # @param config_hash [Hash] The configuration hash
22
+ # @param workflow_path [String] Path to the workflow file
23
+ # @return [String] The workflow name
24
+ def extract_name(config_hash, workflow_path)
25
+ config_hash["name"] || File.basename(workflow_path, ".yml")
26
+ end
27
+
28
+ # Extract steps from the configuration
29
+ # @param config_hash [Hash] The configuration hash
30
+ # @return [Array] The steps array or empty array
31
+ def extract_steps(config_hash)
32
+ config_hash["steps"] || []
33
+ end
34
+
35
+ # Extract tools from the configuration
36
+ # @param config_hash [Hash] The configuration hash
37
+ # @return [Array] The tools array or empty array
38
+ def extract_tools(config_hash)
39
+ config_hash["tools"] || []
40
+ end
41
+
42
+ # Extract function configurations
43
+ # @param config_hash [Hash] The configuration hash
44
+ # @return [Hash] The functions configuration or empty hash
45
+ def extract_functions(config_hash)
46
+ config_hash["functions"] || {}
47
+ end
48
+
49
+ # Extract model from the configuration
50
+ # @param config_hash [Hash] The configuration hash
51
+ # @return [String, nil] The model name if specified
52
+ def extract_model(config_hash)
53
+ config_hash["model"]
54
+ end
55
+
56
+ # Extract target from config or options
57
+ # @param config_hash [Hash] The configuration hash
58
+ # @param options [Hash] Runtime options
59
+ # @return [String, nil] The target if specified
60
+ def extract_target(config_hash, options = {})
61
+ options[:target] || config_hash["target"]
62
+ end
63
+
64
+ private
65
+
66
+ def validate_path!(workflow_path)
67
+ raise ArgumentError, "Workflow path cannot be nil" if workflow_path.nil?
68
+ raise ArgumentError, "Workflow file not found: #{workflow_path}" unless File.exist?(workflow_path)
69
+ raise ArgumentError, "Workflow path must be a YAML file" unless workflow_path.end_with?(".yml", ".yaml")
70
+ end
71
+
72
+ def validate_config!(config_hash)
73
+ raise ArgumentError, "Invalid workflow configuration" unless config_hash.is_a?(Hash)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end