roast-ai 0.3.1 → 0.4.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 +2 -2
- data/.gitignore +1 -0
- data/CHANGELOG.md +85 -0
- data/CLAUDE.md +106 -9
- data/Gemfile +4 -1
- data/Gemfile.lock +70 -16
- data/README.md +159 -8
- data/bin/console +1 -0
- data/bin/roast +1 -1
- data/claude-swarm.yml +210 -0
- data/docs/AGENT_STEPS.md +288 -0
- data/docs/VALIDATION.md +178 -0
- data/examples/agent_continue/add_documentation/prompt.md +5 -0
- data/examples/agent_continue/add_error_handling/prompt.md +5 -0
- data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
- data/examples/agent_continue/combined_workflow.yml +24 -0
- data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
- data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
- data/examples/agent_continue/document_with_context/prompt.md +5 -0
- data/examples/agent_continue/explore_api/prompt.md +6 -0
- data/examples/agent_continue/implement_client/prompt.md +6 -0
- data/examples/agent_continue/inline_workflow.yml +20 -0
- data/examples/agent_continue/refactor_code/prompt.md +2 -0
- data/examples/agent_continue/verify_changes/prompt.md +6 -0
- data/examples/agent_continue/workflow.yml +27 -0
- data/examples/agent_workflow/README.md +75 -0
- data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
- data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
- data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
- data/examples/agent_workflow/workflow.png +0 -0
- data/examples/agent_workflow/workflow.yml +16 -0
- data/examples/api_workflow/workflow.png +0 -0
- data/examples/apply_diff_demo/README.md +58 -0
- data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
- data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
- data/examples/apply_diff_demo/workflow.yml +24 -0
- data/examples/available_tools_demo/README.md +42 -0
- data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
- data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
- data/examples/available_tools_demo/workflow.png +0 -0
- data/examples/available_tools_demo/workflow.yml +32 -0
- data/examples/available_tools_demo/write_summary/prompt.md +6 -0
- data/examples/bash_prototyping/api_testing.png +0 -0
- data/examples/bash_prototyping/system_analysis.png +0 -0
- data/examples/case_when/detect_language/prompt.md +2 -2
- data/examples/case_when/workflow.png +0 -0
- data/examples/cmd/basic_workflow.png +0 -0
- data/examples/cmd/dev_workflow.png +0 -0
- data/examples/cmd/explorer_workflow.png +0 -0
- data/examples/conditional/simple_workflow.png +0 -0
- data/examples/conditional/workflow.png +0 -0
- data/examples/context_management_demo/README.md +43 -0
- data/examples/context_management_demo/workflow.yml +42 -0
- data/examples/direct_coerce_syntax/workflow.png +0 -0
- data/examples/dot_notation/workflow.png +0 -0
- data/examples/exit_on_error/workflow.png +0 -0
- data/examples/grading/run_coverage.rb +0 -2
- data/examples/grading/workflow.png +0 -0
- data/examples/interpolation/workflow.png +0 -0
- data/examples/interpolation/workflow.yml +1 -1
- data/examples/iteration/analyze_complexity/prompt.md +2 -2
- data/examples/iteration/generate_recommendations/prompt.md +2 -2
- data/examples/iteration/implement_fix/prompt.md +2 -2
- data/examples/iteration/prioritize_issues/prompt.md +1 -1
- data/examples/iteration/prompts/analyze_file.md +2 -2
- data/examples/iteration/prompts/generate_summary.md +1 -1
- data/examples/iteration/prompts/update_report.md +3 -3
- data/examples/iteration/prompts/write_report.md +3 -3
- data/examples/iteration/read_file/prompt.md +2 -2
- data/examples/iteration/select_next_issue/prompt.md +2 -2
- data/examples/iteration/update_fix_count/prompt.md +4 -4
- data/examples/iteration/verify_fix/prompt.md +3 -3
- data/examples/iteration/workflow.png +0 -0
- data/examples/json_handling/workflow.png +0 -0
- data/examples/mcp/README.md +3 -3
- data/examples/mcp/analyze_changes/prompt.md +1 -1
- data/examples/mcp/database_workflow.png +0 -0
- data/examples/mcp/database_workflow.yml +1 -1
- data/examples/mcp/env_demo/workflow.png +0 -0
- data/examples/mcp/fetch_pr_context/prompt.md +1 -1
- data/examples/mcp/filesystem_demo/workflow.png +0 -0
- data/examples/mcp/github_workflow.png +0 -0
- data/examples/mcp/github_workflow.yml +1 -1
- data/examples/mcp/multi_mcp_workflow.png +0 -0
- data/examples/mcp/post_review/prompt.md +1 -1
- data/examples/mcp/workflow.png +0 -0
- data/examples/no_model_fallback/README.md +17 -0
- data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
- data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
- data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
- data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
- data/examples/no_model_fallback/sample.rb +42 -0
- data/examples/no_model_fallback/workflow.yml +19 -0
- data/examples/openrouter_example/workflow.png +0 -0
- data/examples/pre_post_processing/analyze_test_file/prompt.md +1 -1
- data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
- data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
- data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
- data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
- data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
- data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
- data/examples/pre_post_processing/workflow.png +0 -0
- data/examples/rspec_to_minitest/workflow.png +0 -0
- data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
- data/examples/shared_config/shared.png +0 -0
- data/examples/single_target_prepost/workflow.png +0 -0
- data/examples/smart_coercion_defaults/workflow.png +0 -0
- data/examples/step_configuration/workflow.png +0 -0
- data/examples/swarm_example.yml +25 -0
- data/examples/tool_config_example/workflow.png +0 -0
- data/examples/user_input/README.md +90 -0
- data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
- data/examples/user_input/funny_name/workflow.png +0 -0
- data/examples/user_input/funny_name/workflow.yml +26 -0
- data/examples/user_input/generate_summary/prompt.md +11 -0
- data/examples/user_input/simple_input_demo/workflow.png +0 -0
- data/examples/user_input/simple_input_demo/workflow.yml +35 -0
- data/examples/user_input/survey_workflow.png +0 -0
- data/examples/user_input/survey_workflow.yml +71 -0
- data/examples/user_input/welcome_message/prompt.md +3 -0
- data/examples/user_input/workflow.png +0 -0
- data/examples/user_input/workflow.yml +73 -0
- data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
- data/examples/workflow_generator/workflow.png +0 -0
- data/lib/roast/errors.rb +6 -4
- data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
- data/lib/roast/helpers/logger.rb +12 -35
- data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
- data/lib/roast/helpers/prompt_loader.rb +0 -2
- data/lib/roast/helpers/timeout_handler.rb +91 -0
- data/lib/roast/resources/api_resource.rb +0 -4
- data/lib/roast/resources/url_resource.rb +0 -3
- data/lib/roast/resources.rb +0 -8
- data/lib/roast/services/context_threshold_checker.rb +42 -0
- data/lib/roast/services/token_counting_service.rb +44 -0
- data/lib/roast/tools/apply_diff.rb +128 -0
- data/lib/roast/tools/ask_user.rb +0 -2
- data/lib/roast/tools/bash.rb +12 -9
- data/lib/roast/tools/cmd.rb +29 -12
- data/lib/roast/tools/coding_agent.rb +65 -17
- data/lib/roast/tools/context_summarizer.rb +108 -0
- data/lib/roast/tools/grep.rb +0 -3
- data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
- data/lib/roast/tools/read_file.rb +0 -2
- data/lib/roast/tools/search_file.rb +0 -2
- data/lib/roast/tools/swarm.rb +124 -0
- data/lib/roast/tools/update_files.rb +0 -4
- data/lib/roast/tools/write_file.rb +0 -3
- data/lib/roast/tools.rb +0 -13
- data/lib/roast/value_objects/step_name.rb +14 -3
- data/lib/roast/value_objects/workflow_path.rb +0 -2
- data/lib/roast/value_objects.rb +4 -4
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/agent_step.rb +33 -0
- data/lib/roast/workflow/api_configuration.rb +0 -4
- data/lib/roast/workflow/base_iteration_step.rb +3 -6
- data/lib/roast/workflow/base_step.rb +54 -28
- data/lib/roast/workflow/base_workflow.rb +43 -23
- data/lib/roast/workflow/case_executor.rb +0 -1
- data/lib/roast/workflow/case_step.rb +0 -4
- data/lib/roast/workflow/command_executor.rb +0 -2
- data/lib/roast/workflow/conditional_executor.rb +0 -1
- data/lib/roast/workflow/conditional_step.rb +0 -4
- data/lib/roast/workflow/configuration.rb +5 -67
- data/lib/roast/workflow/configuration_loader.rb +63 -3
- data/lib/roast/workflow/configuration_parser.rb +1 -7
- data/lib/roast/workflow/context_manager.rb +89 -0
- data/lib/roast/workflow/dot_access_hash.rb +16 -1
- data/lib/roast/workflow/each_step.rb +1 -1
- data/lib/roast/workflow/error_handler.rb +0 -3
- data/lib/roast/workflow/expression_evaluator.rb +0 -3
- data/lib/roast/workflow/file_state_repository.rb +0 -5
- data/lib/roast/workflow/input_executor.rb +41 -0
- data/lib/roast/workflow/input_step.rb +163 -0
- data/lib/roast/workflow/iteration_executor.rb +0 -2
- data/lib/roast/workflow/output_handler.rb +1 -3
- data/lib/roast/workflow/output_manager.rb +0 -2
- data/lib/roast/workflow/repeat_step.rb +1 -1
- data/lib/roast/workflow/replay_handler.rb +1 -4
- data/lib/roast/workflow/resource_resolver.rb +0 -3
- data/lib/roast/workflow/session_manager.rb +0 -3
- data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
- data/lib/roast/workflow/state_manager.rb +2 -4
- data/lib/roast/workflow/state_repository_factory.rb +36 -0
- data/lib/roast/workflow/step_completion_reporter.rb +27 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +48 -24
- data/lib/roast/workflow/step_executor_factory.rb +0 -5
- data/lib/roast/workflow/step_executor_registry.rb +1 -4
- data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
- data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
- data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
- data/lib/roast/workflow/step_factory.rb +56 -0
- data/lib/roast/workflow/step_loader.rb +31 -17
- data/lib/roast/workflow/step_name_extractor.rb +84 -0
- data/lib/roast/workflow/step_orchestrator.rb +3 -2
- data/lib/roast/workflow/step_type_resolver.rb +28 -1
- data/lib/roast/workflow/validation_command.rb +197 -0
- data/lib/roast/workflow/validator.rb +0 -4
- data/lib/roast/workflow/validators/base_validator.rb +44 -0
- data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
- data/lib/roast/workflow/validators/linting_validator.rb +113 -0
- data/lib/roast/workflow/validators/schema_validator.rb +90 -0
- data/lib/roast/workflow/validators/step_collector.rb +57 -0
- data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
- data/lib/roast/workflow/workflow_executor.rb +11 -20
- data/lib/roast/workflow/workflow_initializer.rb +1 -8
- data/lib/roast/workflow/workflow_runner.rb +6 -7
- data/lib/roast/workflow.rb +0 -15
- data/lib/roast/workflow_diagram_generator.rb +298 -0
- data/lib/roast.rb +212 -10
- data/roast.gemspec +4 -2
- data/schema/workflow.json +123 -1
- metadata +143 -6
- data/lib/roast/helpers.rb +0 -12
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
module Validators
|
6
|
+
# Validates workflow configuration against JSON schema
|
7
|
+
class SchemaValidator < BaseValidator
|
8
|
+
attr_reader :parsed_yaml
|
9
|
+
|
10
|
+
def initialize(yaml_content, workflow_path = nil) # rubocop:disable Lint/MissingSuper
|
11
|
+
@yaml_content = yaml_content&.strip || ""
|
12
|
+
@workflow_path = workflow_path
|
13
|
+
@errors = []
|
14
|
+
@warnings = []
|
15
|
+
|
16
|
+
begin
|
17
|
+
@parsed_yaml = @yaml_content.empty? ? {} : YAML.safe_load(@yaml_content)
|
18
|
+
rescue Psych::SyntaxError => e
|
19
|
+
@errors << format_yaml_error(e)
|
20
|
+
@parsed_yaml = {}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate
|
25
|
+
if @parsed_yaml.empty?
|
26
|
+
@errors << {
|
27
|
+
type: :empty_configuration,
|
28
|
+
message: "Workflow configuration is empty",
|
29
|
+
suggestion: "Provide a valid workflow configuration with required fields: name, tools, and steps",
|
30
|
+
}
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
validator = Validator.new(@yaml_content)
|
35
|
+
unless validator.valid?
|
36
|
+
validator.errors.each do |error|
|
37
|
+
@errors << format_schema_error(error)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def format_yaml_error(error)
|
45
|
+
{
|
46
|
+
type: :yaml_syntax,
|
47
|
+
message: "YAML syntax error: #{error.message}",
|
48
|
+
line: error.line,
|
49
|
+
column: error.column,
|
50
|
+
suggestion: "Check YAML syntax at line #{error.line}, column #{error.column}",
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_schema_error(error)
|
55
|
+
# Parse JSON Schema error and make it more user-friendly
|
56
|
+
if error.include?("did not contain a required property")
|
57
|
+
# Extract property name from error
|
58
|
+
match = error.match(/required property of '([^']+)'/)
|
59
|
+
if match
|
60
|
+
property = match[1]
|
61
|
+
{
|
62
|
+
type: :schema,
|
63
|
+
message: "Missing required field: '#{property}'",
|
64
|
+
suggestion: "Add '#{property}' to your workflow configuration",
|
65
|
+
}
|
66
|
+
else
|
67
|
+
{
|
68
|
+
type: :schema,
|
69
|
+
message: error,
|
70
|
+
suggestion: "Check the required fields in your workflow configuration",
|
71
|
+
}
|
72
|
+
end
|
73
|
+
elsif error.include?("does not match")
|
74
|
+
{
|
75
|
+
type: :schema,
|
76
|
+
message: error,
|
77
|
+
suggestion: "Check the workflow schema documentation for valid values",
|
78
|
+
}
|
79
|
+
else
|
80
|
+
{
|
81
|
+
type: :schema,
|
82
|
+
message: error,
|
83
|
+
suggestion: "Refer to the workflow schema for correct configuration structure",
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
module Validators
|
6
|
+
# Collects and caches all steps from a workflow configuration
|
7
|
+
class StepCollector
|
8
|
+
def initialize(parsed_yaml)
|
9
|
+
@parsed_yaml = parsed_yaml
|
10
|
+
@all_steps = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def all_steps
|
14
|
+
@all_steps ||= collect_all_steps(@parsed_yaml)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def collect_all_steps(config, steps = [])
|
20
|
+
# Recursively collect all steps from the configuration
|
21
|
+
["steps", "pre_processing", "post_processing"].each do |key|
|
22
|
+
if config[key]
|
23
|
+
steps.concat(extract_steps_from_array(config[key]))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
steps
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_steps_from_array(steps_array, collected = [])
|
30
|
+
steps_array.each do |step|
|
31
|
+
case step
|
32
|
+
when String
|
33
|
+
collected << step
|
34
|
+
when Hash
|
35
|
+
if step["steps"]
|
36
|
+
collected.concat(extract_steps_from_array(step["steps"]))
|
37
|
+
end
|
38
|
+
# Handle conditional steps
|
39
|
+
["then", "else", "true", "false"].each do |branch|
|
40
|
+
if step[branch]
|
41
|
+
collected.concat(extract_steps_from_array(step[branch]))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
# Handle case/when steps
|
45
|
+
step["when"]&.each_value do |when_steps|
|
46
|
+
collected.concat(extract_steps_from_array(when_steps))
|
47
|
+
end
|
48
|
+
when Array
|
49
|
+
collected.concat(extract_steps_from_array(step))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
collected
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
module Validators
|
6
|
+
# Orchestrates all validators and aggregates results
|
7
|
+
class ValidationOrchestrator
|
8
|
+
attr_reader :errors, :warnings
|
9
|
+
|
10
|
+
def initialize(yaml_content, workflow_path = nil)
|
11
|
+
@yaml_content = yaml_content
|
12
|
+
@workflow_path = workflow_path
|
13
|
+
@errors = []
|
14
|
+
@warnings = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?
|
18
|
+
# First run schema validation
|
19
|
+
schema_validator = SchemaValidator.new(@yaml_content, @workflow_path)
|
20
|
+
|
21
|
+
unless schema_validator.valid?
|
22
|
+
@errors = schema_validator.errors
|
23
|
+
@warnings = schema_validator.warnings
|
24
|
+
return false
|
25
|
+
end
|
26
|
+
|
27
|
+
parsed_yaml = schema_validator.parsed_yaml
|
28
|
+
|
29
|
+
# If schema is valid, run other validators
|
30
|
+
if @errors.empty?
|
31
|
+
step_collector = StepCollector.new(parsed_yaml)
|
32
|
+
|
33
|
+
# Run dependency validation
|
34
|
+
dependency_validator = DependencyValidator.new(parsed_yaml, @workflow_path, step_collector: step_collector)
|
35
|
+
dependency_validator.validate
|
36
|
+
@errors.concat(dependency_validator.errors)
|
37
|
+
@warnings.concat(dependency_validator.warnings)
|
38
|
+
|
39
|
+
# Run linting only if no errors
|
40
|
+
if @errors.empty?
|
41
|
+
linting_validator = LintingValidator.new(parsed_yaml, @workflow_path, step_collector: step_collector)
|
42
|
+
linting_validator.validate
|
43
|
+
@warnings.concat(linting_validator.warnings)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
@errors.empty?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,21 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "English"
|
4
|
-
|
5
|
-
require "roast/workflow/command_executor"
|
6
|
-
require "roast/workflow/conditional_executor"
|
7
|
-
require "roast/workflow/error_handler"
|
8
|
-
require "roast/workflow/interpolator"
|
9
|
-
require "roast/workflow/iteration_executor"
|
10
|
-
require "roast/workflow/parallel_executor"
|
11
|
-
require "roast/workflow/state_manager"
|
12
|
-
require "roast/workflow/step_executor_factory"
|
13
|
-
require "roast/workflow/step_executor_coordinator"
|
14
|
-
require "roast/workflow/step_loader"
|
15
|
-
require "roast/workflow/step_orchestrator"
|
16
|
-
require "roast/workflow/step_type_resolver"
|
17
|
-
require "roast/workflow/workflow_context"
|
18
|
-
|
19
3
|
module Roast
|
20
4
|
module Workflow
|
21
5
|
# Handles the execution of workflow steps, including orchestration and threading
|
@@ -82,13 +66,13 @@ module Roast
|
|
82
66
|
@step_loader = step_loader || StepLoader.new(workflow, config_hash, context_path, phase: phase)
|
83
67
|
@command_executor = command_executor || CommandExecutor.new(logger: @error_handler)
|
84
68
|
@interpolator = interpolator || Interpolator.new(workflow, logger: @error_handler)
|
85
|
-
@state_manager = state_manager || StateManager.new(workflow, logger: @error_handler)
|
69
|
+
@state_manager = state_manager || StateManager.new(workflow, logger: @error_handler, storage_type: workflow.storage_type)
|
86
70
|
@iteration_executor = iteration_executor || IterationExecutor.new(workflow, context_path, @state_manager, config_hash)
|
87
71
|
@conditional_executor = conditional_executor || ConditionalExecutor.new(workflow, context_path, @state_manager, self)
|
88
72
|
@step_orchestrator = step_orchestrator || StepOrchestrator.new(workflow, @step_loader, @state_manager, @error_handler, self)
|
89
73
|
|
90
74
|
# Initialize coordinator with dependencies
|
91
|
-
|
75
|
+
base_coordinator = step_executor_coordinator || StepExecutorCoordinator.new(
|
92
76
|
context: @context,
|
93
77
|
dependencies: {
|
94
78
|
workflow_executor: self,
|
@@ -100,6 +84,13 @@ module Roast
|
|
100
84
|
error_handler: @error_handler,
|
101
85
|
},
|
102
86
|
)
|
87
|
+
|
88
|
+
# Only wrap with reporting decorator if workflow has token tracking enabled
|
89
|
+
@step_executor_coordinator = if workflow.respond_to?(:context_manager) && workflow.context_manager
|
90
|
+
StepExecutorWithReporting.new(base_coordinator, @context)
|
91
|
+
else
|
92
|
+
base_coordinator
|
93
|
+
end
|
103
94
|
end
|
104
95
|
|
105
96
|
# Logger interface methods for backward compatibility
|
@@ -127,8 +118,8 @@ module Roast
|
|
127
118
|
@interpolator.interpolate(text)
|
128
119
|
end
|
129
120
|
|
130
|
-
def execute_step(name, exit_on_error: true)
|
131
|
-
@step_executor_coordinator.execute(name, exit_on_error:
|
121
|
+
def execute_step(name, exit_on_error: true, is_last_step: nil)
|
122
|
+
@step_executor_coordinator.execute(name, exit_on_error:, is_last_step:)
|
132
123
|
rescue StepLoader::StepNotFoundError => e
|
133
124
|
raise StepNotFoundError.new(e.message, step_name: e.step_name, original_error: e.original_error)
|
134
125
|
rescue StepLoader::StepExecutionError => e
|
@@ -1,12 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "raix"
|
4
|
-
require "roast/initializers"
|
5
|
-
require "roast/helpers/function_caching_interceptor"
|
6
|
-
require "roast/helpers/logger"
|
7
|
-
require "roast/workflow/base_workflow"
|
8
|
-
require "roast/workflow/interpolator"
|
9
|
-
|
10
3
|
module Roast
|
11
4
|
module Workflow
|
12
5
|
# Handles initialization of workflow dependencies: initializers, tools, and API clients
|
@@ -103,7 +96,7 @@ module Roast
|
|
103
96
|
# Validate the client configuration by making a test API call
|
104
97
|
validate_api_client(client) if client
|
105
98
|
rescue OpenRouter::ConfigurationError, Faraday::UnauthorizedError => e
|
106
|
-
error = Roast::AuthenticationError.new("API authentication failed: No API token provided or token is invalid")
|
99
|
+
error = Roast::Errors::AuthenticationError.new("API authentication failed: No API token provided or token is invalid")
|
107
100
|
error.set_backtrace(e.backtrace)
|
108
101
|
|
109
102
|
ActiveSupport::Notifications.instrument("roast.workflow.start.error", {
|
@@ -1,12 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "erb"
|
4
|
-
require "roast/workflow/replay_handler"
|
5
|
-
require "roast/workflow/workflow_executor"
|
6
|
-
require "roast/workflow/output_handler"
|
7
|
-
require "roast/workflow/base_workflow"
|
8
|
-
require "roast/workflow/dot_access_hash"
|
9
|
-
|
10
3
|
module Roast
|
11
4
|
module Workflow
|
12
5
|
# Handles running workflows for files/targets and orchestrating execution
|
@@ -174,6 +167,12 @@ module Roast
|
|
174
167
|
workflow.verbose = @options[:verbose] if @options[:verbose].present?
|
175
168
|
workflow.concise = @options[:concise] if @options[:concise].present?
|
176
169
|
workflow.pause_step_name = @options[:pause] if @options[:pause].present?
|
170
|
+
# Set storage type based on CLI option (default is SQLite unless --file-storage is used)
|
171
|
+
workflow.storage_type = @options[:file_storage] ? "file" : nil
|
172
|
+
# Set model from configuration with fallback to default
|
173
|
+
workflow.model = @configuration.model || StepLoader::DEFAULT_MODEL
|
174
|
+
# Set context management configuration
|
175
|
+
workflow.context_management_config = @configuration.context_management
|
177
176
|
end
|
178
177
|
end
|
179
178
|
|
data/lib/roast/workflow.rb
CHANGED
@@ -1,20 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/workflow/base_step"
|
4
|
-
require "roast/workflow/prompt_step"
|
5
|
-
require "roast/workflow/base_iteration_step"
|
6
|
-
require "roast/workflow/repeat_step"
|
7
|
-
require "roast/workflow/each_step"
|
8
|
-
require "roast/workflow/base_workflow"
|
9
|
-
require "roast/workflow/configuration"
|
10
|
-
require "roast/workflow/workflow_execution_context"
|
11
|
-
require "roast/workflow/workflow_executor"
|
12
|
-
require "roast/workflow/configuration_parser"
|
13
|
-
require "roast/workflow/validator"
|
14
|
-
require "roast/workflow/state_repository"
|
15
|
-
require "roast/workflow/session_manager"
|
16
|
-
require "roast/workflow/file_state_repository"
|
17
|
-
|
18
3
|
module Roast
|
19
4
|
module Workflow
|
20
5
|
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
class WorkflowDiagramGenerator
|
5
|
+
def initialize(workflow_config, workflow_file_path = nil)
|
6
|
+
@workflow_config = workflow_config
|
7
|
+
@workflow_file_path = workflow_file_path
|
8
|
+
@graph = GraphViz.new(:G, type: :digraph)
|
9
|
+
@node_counter = 0
|
10
|
+
@nodes = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate(custom_output_path = nil)
|
14
|
+
configure_graph
|
15
|
+
build_graph(@workflow_config.steps)
|
16
|
+
|
17
|
+
output_path = custom_output_path || generate_output_filename
|
18
|
+
@graph.output(png: output_path)
|
19
|
+
output_path
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def configure_graph
|
25
|
+
@graph[:rankdir] = "TB"
|
26
|
+
@graph[:fontname] = "Helvetica"
|
27
|
+
@graph[:fontsize] = "12"
|
28
|
+
@graph[:bgcolor] = "white"
|
29
|
+
@graph[:pad] = "0.5"
|
30
|
+
@graph[:nodesep] = "0.7"
|
31
|
+
@graph[:ranksep] = "0.8"
|
32
|
+
@graph[:splines] = "spline"
|
33
|
+
|
34
|
+
# Default node styling
|
35
|
+
@graph.node[:shape] = "box"
|
36
|
+
@graph.node[:style] = "rounded,filled"
|
37
|
+
@graph.node[:fillcolor] = "#E8F4FD"
|
38
|
+
@graph.node[:color] = "#2563EB"
|
39
|
+
@graph.node[:fontname] = "Helvetica"
|
40
|
+
@graph.node[:fontsize] = "11"
|
41
|
+
@graph.node[:fontcolor] = "#1E293B"
|
42
|
+
@graph.node[:penwidth] = "1.5"
|
43
|
+
@graph.node[:height] = "0.6"
|
44
|
+
@graph.node[:margin] = "0.15"
|
45
|
+
|
46
|
+
# Edge styling
|
47
|
+
@graph.edge[:fontname] = "Helvetica"
|
48
|
+
@graph.edge[:fontsize] = "10"
|
49
|
+
@graph.edge[:color] = "#64748B"
|
50
|
+
@graph.edge[:penwidth] = "1.5"
|
51
|
+
@graph.edge[:arrowsize] = "0.8"
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_graph(steps, parent_node = nil)
|
55
|
+
previous_node = parent_node
|
56
|
+
|
57
|
+
steps.each do |step|
|
58
|
+
current_node = process_step(step)
|
59
|
+
|
60
|
+
if previous_node && current_node
|
61
|
+
@graph.add_edges(previous_node, current_node)
|
62
|
+
end
|
63
|
+
|
64
|
+
previous_node = current_node unless current_node.nil?
|
65
|
+
end
|
66
|
+
|
67
|
+
previous_node
|
68
|
+
end
|
69
|
+
|
70
|
+
def process_step(step)
|
71
|
+
case step
|
72
|
+
when String
|
73
|
+
create_step_node(step)
|
74
|
+
when Hash
|
75
|
+
process_control_flow(step)
|
76
|
+
else
|
77
|
+
::CLI::Kit.logger.warn("Unexpected step type in workflow diagram: #{step.class} - #{step.inspect}")
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_step_node(step_name)
|
83
|
+
node_id = next_node_id
|
84
|
+
label = step_name
|
85
|
+
|
86
|
+
# Check if it's an inline prompt
|
87
|
+
@nodes[node_id] = if step_name.start_with?("prompt:")
|
88
|
+
@graph.add_nodes(
|
89
|
+
node_id,
|
90
|
+
label: truncate_label(step_name[7..].strip),
|
91
|
+
fillcolor: "#FEF3C7",
|
92
|
+
color: "#F59E0B",
|
93
|
+
shape: "note",
|
94
|
+
fontsize: "10",
|
95
|
+
)
|
96
|
+
else
|
97
|
+
@graph.add_nodes(node_id, label: label)
|
98
|
+
end
|
99
|
+
|
100
|
+
@nodes[node_id]
|
101
|
+
end
|
102
|
+
|
103
|
+
def process_control_flow(control_flow)
|
104
|
+
if control_flow.key?("if") || control_flow.key?("unless")
|
105
|
+
process_conditional(control_flow)
|
106
|
+
elsif control_flow.key?("each") || control_flow.key?("repeat")
|
107
|
+
process_loop(control_flow)
|
108
|
+
elsif control_flow.key?("input")
|
109
|
+
process_input(control_flow)
|
110
|
+
elsif control_flow.key?("proceed?")
|
111
|
+
process_proceed(control_flow)
|
112
|
+
elsif control_flow.key?("case")
|
113
|
+
process_case(control_flow)
|
114
|
+
else
|
115
|
+
::CLI::Kit.logger.warn("Unexpected control flow structure in workflow diagram: #{control_flow.keys.join(", ")}")
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def process_conditional(conditional)
|
121
|
+
condition_type = conditional.key?("if") ? "if" : "unless"
|
122
|
+
condition = conditional[condition_type]
|
123
|
+
|
124
|
+
# Create diamond decision node
|
125
|
+
decision_id = next_node_id
|
126
|
+
decision_node = @graph.add_nodes(
|
127
|
+
decision_id,
|
128
|
+
label: "#{condition_type}: #{condition}",
|
129
|
+
shape: "diamond",
|
130
|
+
fillcolor: "#FEE2E2",
|
131
|
+
color: "#DC2626",
|
132
|
+
fontsize: "10",
|
133
|
+
height: "0.8",
|
134
|
+
width: "1.2",
|
135
|
+
)
|
136
|
+
|
137
|
+
# Process then branch
|
138
|
+
if conditional["then"]
|
139
|
+
then_steps = Array(conditional["then"])
|
140
|
+
if then_steps.any?
|
141
|
+
build_graph(then_steps, decision_node)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Process else branch
|
146
|
+
if conditional["else"]
|
147
|
+
else_steps = Array(conditional["else"])
|
148
|
+
if else_steps.any?
|
149
|
+
build_graph(else_steps, decision_node)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
decision_node
|
154
|
+
end
|
155
|
+
|
156
|
+
def process_loop(loop_control)
|
157
|
+
loop_type = loop_control.key?("each") ? "each" : "repeat"
|
158
|
+
loop_value = loop_control[loop_type]
|
159
|
+
|
160
|
+
# Create loop node
|
161
|
+
loop_id = next_node_id
|
162
|
+
loop_label = loop_type == "each" ? "each: #{loop_value}" : "repeat: #{loop_value}"
|
163
|
+
loop_node = @graph.add_nodes(
|
164
|
+
loop_id,
|
165
|
+
label: loop_label,
|
166
|
+
shape: "box3d",
|
167
|
+
fillcolor: "#D1FAE5",
|
168
|
+
color: "#10B981",
|
169
|
+
fontsize: "10",
|
170
|
+
penwidth: "2",
|
171
|
+
)
|
172
|
+
|
173
|
+
# Process loop body
|
174
|
+
if loop_control["do"]
|
175
|
+
loop_steps = Array(loop_control["do"])
|
176
|
+
if loop_steps.any?
|
177
|
+
last_loop_node = build_graph(loop_steps, loop_node)
|
178
|
+
# Add back edge to show loop
|
179
|
+
@graph.add_edges(
|
180
|
+
last_loop_node,
|
181
|
+
loop_node,
|
182
|
+
style: "dashed",
|
183
|
+
label: "loop",
|
184
|
+
color: "#10B981",
|
185
|
+
fontcolor: "#10B981",
|
186
|
+
arrowhead: "empty",
|
187
|
+
)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
loop_node
|
192
|
+
end
|
193
|
+
|
194
|
+
def process_input(input_control)
|
195
|
+
input_id = next_node_id
|
196
|
+
label = input_control["input"]
|
197
|
+
input_node = @graph.add_nodes(
|
198
|
+
input_id,
|
199
|
+
label: "input: #{label}",
|
200
|
+
shape: "parallelogram",
|
201
|
+
fillcolor: "#F3F4F6",
|
202
|
+
color: "#6B7280",
|
203
|
+
fontsize: "10",
|
204
|
+
)
|
205
|
+
input_node
|
206
|
+
end
|
207
|
+
|
208
|
+
def process_proceed(proceed_control)
|
209
|
+
proceed_id = next_node_id
|
210
|
+
proceed_node = @graph.add_nodes(
|
211
|
+
proceed_id,
|
212
|
+
label: "proceed?",
|
213
|
+
shape: "diamond",
|
214
|
+
fillcolor: "#FED7AA",
|
215
|
+
color: "#EA580C",
|
216
|
+
fontsize: "10",
|
217
|
+
height: "0.8",
|
218
|
+
)
|
219
|
+
|
220
|
+
# Process do branch if present
|
221
|
+
if proceed_control["do"]
|
222
|
+
proceed_steps = Array(proceed_control["do"])
|
223
|
+
if proceed_steps.any?
|
224
|
+
build_graph(proceed_steps, proceed_node)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
proceed_node
|
229
|
+
end
|
230
|
+
|
231
|
+
def process_case(case_control)
|
232
|
+
case_id = next_node_id
|
233
|
+
case_node = @graph.add_nodes(
|
234
|
+
case_id,
|
235
|
+
label: "case: #{case_control["case"]}",
|
236
|
+
shape: "diamond",
|
237
|
+
fillcolor: "#E9D5FF",
|
238
|
+
color: "#9333EA",
|
239
|
+
fontsize: "10",
|
240
|
+
height: "0.8",
|
241
|
+
width: "1.5",
|
242
|
+
)
|
243
|
+
|
244
|
+
# Process when branches
|
245
|
+
case_control["when"].each do |condition, steps|
|
246
|
+
when_steps = Array(steps)
|
247
|
+
next if when_steps.none?
|
248
|
+
|
249
|
+
first_when_node = process_step(when_steps.first)
|
250
|
+
@graph.add_edges(
|
251
|
+
case_node,
|
252
|
+
first_when_node,
|
253
|
+
label: condition.to_s,
|
254
|
+
fontcolor: "#9333EA",
|
255
|
+
)
|
256
|
+
|
257
|
+
if when_steps.length > 1
|
258
|
+
build_graph(when_steps[1..], first_when_node)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
case_node
|
263
|
+
end
|
264
|
+
|
265
|
+
def next_node_id
|
266
|
+
@node_counter += 1
|
267
|
+
"node_#{@node_counter}"
|
268
|
+
end
|
269
|
+
|
270
|
+
def truncate_label(text, max_length = 50)
|
271
|
+
return text if text.length <= max_length
|
272
|
+
|
273
|
+
"#{text[0...max_length]}..."
|
274
|
+
end
|
275
|
+
|
276
|
+
def generate_output_filename
|
277
|
+
if @workflow_file_path
|
278
|
+
# Get the directory and base name of the workflow file
|
279
|
+
dir = File.dirname(@workflow_file_path)
|
280
|
+
base = File.basename(@workflow_file_path, ".yml")
|
281
|
+
|
282
|
+
# Create the diagram filename in the same directory
|
283
|
+
File.join(dir, "#{base}.png")
|
284
|
+
else
|
285
|
+
# Fallback to workflow name if no file path provided
|
286
|
+
workflow_name = @workflow_config.name
|
287
|
+
sanitized_name = workflow_name
|
288
|
+
.downcase
|
289
|
+
.gsub(/[^a-z0-9]+/, "_")
|
290
|
+
.gsub(/^_|_$/, "")
|
291
|
+
.gsub(/_+/, "_")
|
292
|
+
|
293
|
+
sanitized_name = "workflow" if sanitized_name.empty?
|
294
|
+
"#{sanitized_name}_diagram.png"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|