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
@@ -1,37 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "erb"
|
4
|
-
require "forwardable"
|
5
|
-
require "roast/workflow/context_path_resolver"
|
6
|
-
|
7
3
|
module Roast
|
8
4
|
module Workflow
|
9
5
|
class BaseStep
|
10
|
-
|
11
|
-
|
12
|
-
attr_accessor :model, :print_response, :json, :params, :resource, :coerce_to
|
6
|
+
attr_accessor :model, :print_response, :json, :params, :resource, :coerce_to, :available_tools
|
13
7
|
attr_reader :workflow, :name, :context_path
|
14
8
|
|
15
|
-
|
16
|
-
|
17
|
-
def_delegator :workflow, :transcript
|
9
|
+
delegate :append_to_final_output, :transcript, to: :workflow
|
10
|
+
delegate_missing_to :workflow
|
18
11
|
|
19
12
|
# TODO: is this really the model we want to default to, and is this the right place to set it?
|
20
13
|
def initialize(workflow, model: "anthropic:claude-opus-4", name: nil, context_path: nil)
|
21
14
|
@workflow = workflow
|
22
15
|
@model = model
|
23
|
-
@name = name
|
16
|
+
@name = normalize_name(name)
|
24
17
|
@context_path = context_path || ContextPathResolver.resolve(self.class)
|
25
18
|
@print_response = false
|
26
19
|
@json = false
|
27
20
|
@params = {}
|
28
21
|
@coerce_to = nil
|
22
|
+
@available_tools = nil
|
29
23
|
@resource = workflow.resource if workflow.respond_to?(:resource)
|
30
24
|
end
|
31
25
|
|
32
26
|
def call
|
33
27
|
prompt(read_sidecar_prompt)
|
34
|
-
result = chat_completion(print_response:, json:, params:)
|
28
|
+
result = chat_completion(print_response:, json:, params:, available_tools:)
|
35
29
|
|
36
30
|
# Apply coercion if configured
|
37
31
|
apply_coercion(result)
|
@@ -39,25 +33,17 @@ module Roast
|
|
39
33
|
|
40
34
|
protected
|
41
35
|
|
42
|
-
def chat_completion(print_response: nil, json: nil, params: nil)
|
36
|
+
def chat_completion(print_response: nil, json: nil, params: nil, available_tools: nil)
|
43
37
|
# Use instance variables as defaults if parameters are not provided
|
44
38
|
print_response = @print_response if print_response.nil?
|
45
39
|
json = @json if json.nil?
|
46
40
|
params = @params if params.nil?
|
41
|
+
available_tools = @available_tools if available_tools.nil?
|
47
42
|
|
48
|
-
workflow.chat_completion(openai: workflow.openai? && model, model: model, json:, params:)
|
49
|
-
|
50
|
-
|
51
|
-
begin
|
52
|
-
if json
|
53
|
-
return nil if result.strip.empty? # Explicitly handle empty string
|
43
|
+
result = workflow.chat_completion(openai: workflow.openai? && model, model: model, json:, params:, available_tools:)
|
44
|
+
process_output(result, print_response:)
|
54
45
|
|
55
|
-
|
56
|
-
end
|
57
|
-
rescue JSON::ParserError
|
58
|
-
# If JSON parsing fails, leave it as a string
|
59
|
-
end
|
60
|
-
end
|
46
|
+
result
|
61
47
|
end
|
62
48
|
|
63
49
|
def prompt(text)
|
@@ -79,8 +65,20 @@ module Roast
|
|
79
65
|
def process_output(response, print_response:)
|
80
66
|
output_path = File.join(context_path, "output.txt")
|
81
67
|
if File.exist?(output_path) && print_response
|
82
|
-
#
|
83
|
-
|
68
|
+
# Deep wrap the response for template access
|
69
|
+
template_response = deep_wrap_for_templates(response)
|
70
|
+
|
71
|
+
# Debug output
|
72
|
+
if template_response.is_a?(DotAccessHash) && template_response.recommendations&.is_a?(Array)
|
73
|
+
$stderr.puts "DEBUG: recommendations array has #{template_response.recommendations.size} items"
|
74
|
+
$stderr.puts "DEBUG: first item class: #{template_response.recommendations.first.class}" if template_response.recommendations.first
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create a binding that includes the wrapped response
|
78
|
+
template_binding = binding
|
79
|
+
template_binding.local_variable_set(:response, template_response)
|
80
|
+
|
81
|
+
append_to_final_output(ERB.new(File.read(output_path), trim_mode: "-").result(template_binding))
|
84
82
|
elsif print_response
|
85
83
|
append_to_final_output(response)
|
86
84
|
end
|
@@ -88,6 +86,35 @@ module Roast
|
|
88
86
|
|
89
87
|
private
|
90
88
|
|
89
|
+
def normalize_name(name)
|
90
|
+
return name if name.is_a?(Roast::ValueObjects::StepName)
|
91
|
+
|
92
|
+
name_value = name || self.class.name.underscore.split("/").last
|
93
|
+
Roast::ValueObjects::StepName.new(name_value)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Deep wrap response for ERB templates
|
97
|
+
# This creates a new structure where:
|
98
|
+
# - Hashes are wrapped in DotAccessHash
|
99
|
+
# - Arrays are cloned with their Hash elements wrapped
|
100
|
+
def deep_wrap_for_templates(obj)
|
101
|
+
case obj
|
102
|
+
when Hash
|
103
|
+
# Convert the hash to a new hash with wrapped values
|
104
|
+
wrapped_hash = {}
|
105
|
+
obj.each do |key, value|
|
106
|
+
wrapped_hash[key] = deep_wrap_for_templates(value)
|
107
|
+
end
|
108
|
+
DotAccessHash.new(wrapped_hash)
|
109
|
+
when Array
|
110
|
+
# Create a new array with wrapped elements
|
111
|
+
# This allows the template to use dot notation on array elements
|
112
|
+
obj.map { |item| deep_wrap_for_templates(item) }
|
113
|
+
else
|
114
|
+
obj
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
91
118
|
def apply_coercion(result)
|
92
119
|
case @coerce_to
|
93
120
|
when :boolean
|
@@ -97,7 +124,6 @@ module Roast
|
|
97
124
|
!!result
|
98
125
|
when :llm_boolean
|
99
126
|
# Use LLM boolean coercer for natural language responses
|
100
|
-
require "roast/workflow/llm_boolean_coercer"
|
101
127
|
LlmBooleanCoercer.coerce(result)
|
102
128
|
when :iterable
|
103
129
|
# Ensure result is iterable
|
@@ -1,12 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "raix/chat_completion"
|
4
|
-
require "raix/function_dispatch"
|
5
|
-
|
6
|
-
require "roast/workflow/context_path_resolver"
|
7
|
-
require "roast/workflow/dot_access_hash"
|
8
|
-
require "roast/workflow/output_manager"
|
9
|
-
|
10
3
|
module Roast
|
11
4
|
module Workflow
|
12
5
|
class BaseWorkflow
|
@@ -23,12 +16,15 @@ module Roast
|
|
23
16
|
:session_name,
|
24
17
|
:session_timestamp,
|
25
18
|
:model,
|
26
|
-
:workflow_configuration
|
19
|
+
:workflow_configuration,
|
20
|
+
:storage_type,
|
21
|
+
:context_management_config
|
27
22
|
|
28
|
-
attr_reader :pre_processing_data
|
23
|
+
attr_reader :pre_processing_data, :context_manager
|
29
24
|
|
30
25
|
delegate :api_provider, :openai?, to: :workflow_configuration, allow_nil: true
|
31
26
|
delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
|
27
|
+
delegate_missing_to :output
|
32
28
|
|
33
29
|
def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, workflow_configuration: nil, pre_processing_data: nil)
|
34
30
|
@file = file
|
@@ -42,6 +38,8 @@ module Roast
|
|
42
38
|
|
43
39
|
# Initialize managers
|
44
40
|
@output_manager = OutputManager.new
|
41
|
+
@context_manager = ContextManager.new
|
42
|
+
@context_management_config = {}
|
45
43
|
|
46
44
|
# Setup prompt and handlers
|
47
45
|
read_sidecar_prompt.then do |prompt|
|
@@ -59,29 +57,55 @@ module Roast
|
|
59
57
|
step_model = kwargs[:model]
|
60
58
|
|
61
59
|
with_model(step_model) do
|
60
|
+
# Configure context manager if needed
|
61
|
+
if @context_management_config.any?
|
62
|
+
@context_manager.configure(@context_management_config)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Track token usage before API call
|
66
|
+
messages = kwargs[:messages] || transcript.flatten.compact
|
67
|
+
if @context_management_config[:enabled]
|
68
|
+
@context_manager.track_usage(messages)
|
69
|
+
@context_manager.check_warnings
|
70
|
+
end
|
71
|
+
|
62
72
|
ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
|
63
73
|
model: model,
|
64
74
|
parameters: kwargs.except(:openai, :model),
|
65
75
|
})
|
66
76
|
|
77
|
+
# Clear any previous response
|
78
|
+
Thread.current[:chat_completion_response] = nil
|
79
|
+
|
67
80
|
# Call the parent module's chat_completion
|
68
81
|
# skip model because it is read directly from the model method
|
69
82
|
result = super(**kwargs.except(:model))
|
70
83
|
execution_time = Time.now - start_time
|
71
84
|
|
85
|
+
# Extract token usage from the raw response stored by Raix
|
86
|
+
raw_response = Thread.current[:chat_completion_response]
|
87
|
+
token_usage = extract_token_usage(raw_response) if raw_response
|
88
|
+
|
89
|
+
# Update context manager with actual token usage if available
|
90
|
+
if token_usage && @context_management_config[:enabled]
|
91
|
+
actual_total = token_usage.dig("total_tokens") || token_usage.dig(:total_tokens)
|
92
|
+
@context_manager.update_with_actual_usage(actual_total) if actual_total
|
93
|
+
end
|
94
|
+
|
72
95
|
ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
|
73
96
|
success: true,
|
74
97
|
model: model,
|
75
98
|
parameters: kwargs.except(:openai, :model),
|
76
99
|
execution_time: execution_time,
|
77
100
|
response_size: result.to_s.length,
|
101
|
+
token_usage: token_usage,
|
78
102
|
})
|
79
103
|
result
|
80
104
|
end
|
81
105
|
rescue Faraday::ResourceNotFound => e
|
82
106
|
execution_time = Time.now - start_time
|
83
107
|
message = e.response.dig(:body, "error", "message") || e.message
|
84
|
-
error = Roast::ResourceNotFoundError.new(message)
|
108
|
+
error = Roast::Errors::ResourceNotFoundError.new(message)
|
85
109
|
error.set_backtrace(e.backtrace)
|
86
110
|
log_and_raise_error(error, message, step_model || model, kwargs, execution_time)
|
87
111
|
rescue => e
|
@@ -104,19 +128,6 @@ module Roast
|
|
104
128
|
# Expose output manager for state management
|
105
129
|
attr_reader :output_manager
|
106
130
|
|
107
|
-
# Allow direct access to output values without 'output.' prefix
|
108
|
-
def method_missing(method_name, *args, &block)
|
109
|
-
if output.respond_to?(method_name)
|
110
|
-
output.send(method_name, *args, &block)
|
111
|
-
else
|
112
|
-
super
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def respond_to_missing?(method_name, include_private = false)
|
117
|
-
output.respond_to?(method_name) || super
|
118
|
-
end
|
119
|
-
|
120
131
|
private
|
121
132
|
|
122
133
|
def log_and_raise_error(error, message, model, params, execution_time)
|
@@ -134,6 +145,15 @@ module Roast
|
|
134
145
|
def read_sidecar_prompt
|
135
146
|
Roast::Helpers::PromptLoader.load_prompt(self, file)
|
136
147
|
end
|
148
|
+
|
149
|
+
def extract_token_usage(result)
|
150
|
+
# Token usage is typically in the response metadata
|
151
|
+
# This depends on the API provider's response format
|
152
|
+
return unless result.is_a?(Hash) || result.respond_to?(:to_h)
|
153
|
+
|
154
|
+
result_hash = result.is_a?(Hash) ? result : result.to_h
|
155
|
+
result_hash.dig("usage") || result_hash.dig(:usage)
|
156
|
+
end
|
137
157
|
end
|
138
158
|
end
|
139
159
|
end
|
@@ -24,7 +24,6 @@ module Roast
|
|
24
24
|
raise WorkflowExecutor::ConfigurationError, "Missing 'when' clauses in case configuration" unless when_clauses
|
25
25
|
|
26
26
|
# Create and execute a CaseStep
|
27
|
-
require "roast/workflow/case_step" unless defined?(Roast::Workflow::CaseStep)
|
28
27
|
case_step = CaseStep.new(
|
29
28
|
@workflow,
|
30
29
|
config: case_config,
|
@@ -24,7 +24,6 @@ module Roast
|
|
24
24
|
raise WorkflowExecutor::ConfigurationError, "Missing 'then' steps in conditional configuration" unless then_steps
|
25
25
|
|
26
26
|
# Create and execute a ConditionalStep
|
27
|
-
require "roast/workflow/conditional_step" unless defined?(Roast::Workflow::ConditionalStep)
|
28
27
|
conditional_step = ConditionalStep.new(
|
29
28
|
@workflow,
|
30
29
|
config: conditional_config,
|
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/workflow/api_configuration"
|
4
|
-
require "roast/workflow/configuration_loader"
|
5
|
-
require "roast/workflow/resource_resolver"
|
6
|
-
require "roast/workflow/step_finder"
|
7
|
-
|
8
3
|
module Roast
|
9
4
|
module Workflow
|
10
5
|
# Encapsulates workflow configuration data and provides structured access
|
@@ -12,7 +7,7 @@ module Roast
|
|
12
7
|
class Configuration
|
13
8
|
MCPTool = Struct.new(:name, :config, :only, :except, keyword_init: true)
|
14
9
|
|
15
|
-
attr_reader :config_hash, :workflow_path, :name, :steps, :pre_processing, :post_processing, :tools, :tool_configs, :mcp_tools, :function_configs, :model, :resource
|
10
|
+
attr_reader :config_hash, :workflow_path, :name, :steps, :pre_processing, :post_processing, :tools, :tool_configs, :mcp_tools, :function_configs, :model, :resource, :context_management
|
16
11
|
attr_accessor :target
|
17
12
|
|
18
13
|
delegate :api_provider, :openrouter?, :openai?, :uri_base, to: :api_configuration
|
@@ -37,6 +32,7 @@ module Roast
|
|
37
32
|
@mcp_tools = ConfigurationLoader.extract_mcp_tools(@config_hash)
|
38
33
|
@function_configs = ConfigurationLoader.extract_functions(@config_hash)
|
39
34
|
@model = ConfigurationLoader.extract_model(@config_hash)
|
35
|
+
@context_management = ConfigurationLoader.extract_context_management(@config_hash)
|
40
36
|
|
41
37
|
# Initialize components
|
42
38
|
@api_configuration = ApiConfiguration.new(@config_hash)
|
@@ -45,8 +41,6 @@ module Roast
|
|
45
41
|
# Process target and resource
|
46
42
|
@target = ConfigurationLoader.extract_target(@config_hash, options)
|
47
43
|
process_resource
|
48
|
-
|
49
|
-
mark_last_step_for_output
|
50
44
|
end
|
51
45
|
|
52
46
|
def context_path
|
@@ -98,65 +92,9 @@ module Roast
|
|
98
92
|
attr_reader :api_configuration
|
99
93
|
|
100
94
|
def process_resource
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
@target = @resource.value if has_target?
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def mark_last_step_for_output
|
109
|
-
return if @steps.empty?
|
110
|
-
|
111
|
-
last_step = find_last_executable_step(@steps.last)
|
112
|
-
return unless last_step
|
113
|
-
|
114
|
-
# Get the step name/key
|
115
|
-
step_key = extract_step_key(last_step)
|
116
|
-
return unless step_key
|
117
|
-
|
118
|
-
# Ensure config exists for this step
|
119
|
-
@config_hash[step_key] ||= {}
|
120
|
-
|
121
|
-
# Only set print_response if not already explicitly configured
|
122
|
-
@config_hash[step_key]["print_response"] = true unless @config_hash[step_key].key?("print_response")
|
123
|
-
end
|
124
|
-
|
125
|
-
def find_last_executable_step(step)
|
126
|
-
case step
|
127
|
-
when String
|
128
|
-
step
|
129
|
-
when Hash
|
130
|
-
# Check if it's a special step type (if, unless, each, repeat, case)
|
131
|
-
if step.key?("if") || step.key?("unless")
|
132
|
-
# For conditional steps, try to find the last step in the "then" branch
|
133
|
-
then_steps = step["then"] || step["steps"]
|
134
|
-
find_last_executable_step(then_steps.last) if then_steps&.any?
|
135
|
-
elsif step.key?("each") || step.key?("repeat")
|
136
|
-
# For iteration steps, we can't reliably determine the last step
|
137
|
-
nil
|
138
|
-
elsif step.key?("case")
|
139
|
-
# For case steps, we can't reliably determine the last step
|
140
|
-
nil
|
141
|
-
elsif step.size == 1
|
142
|
-
# Regular hash step with variable assignment
|
143
|
-
step
|
144
|
-
end
|
145
|
-
when Array
|
146
|
-
# For parallel steps, we can't determine a single "last" step
|
147
|
-
nil
|
148
|
-
else
|
149
|
-
step
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def extract_step_key(step)
|
154
|
-
case step
|
155
|
-
when String
|
156
|
-
step
|
157
|
-
when Hash
|
158
|
-
step.keys.first
|
159
|
-
end
|
95
|
+
@resource = ResourceResolver.resolve(@target, context_path)
|
96
|
+
# Update target with processed value for backward compatibility
|
97
|
+
@target = @resource.value if has_target?
|
160
98
|
end
|
161
99
|
end
|
162
100
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "yaml"
|
4
|
-
|
5
3
|
module Roast
|
6
4
|
module Workflow
|
7
5
|
# Handles loading and parsing of workflow configuration files
|
@@ -10,7 +8,7 @@ module Roast
|
|
10
8
|
# Load configuration from a YAML file
|
11
9
|
# @param workflow_path [String] Path to the workflow YAML file
|
12
10
|
# @return [Hash] The parsed configuration hash
|
13
|
-
def load(workflow_path)
|
11
|
+
def load(workflow_path, options = {})
|
14
12
|
validate_path!(workflow_path)
|
15
13
|
|
16
14
|
# Load shared.yml if it exists one level above
|
@@ -25,6 +23,18 @@ module Roast
|
|
25
23
|
end
|
26
24
|
|
27
25
|
yaml_content += File.read(workflow_path)
|
26
|
+
|
27
|
+
# Use comprehensive validation if requested
|
28
|
+
if options[:comprehensive_validation]
|
29
|
+
validator = Validators::ValidationOrchestrator.new(yaml_content, workflow_path)
|
30
|
+
unless validator.valid?
|
31
|
+
raise_validation_errors(validator)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Show warnings if any
|
35
|
+
display_warnings(validator.warnings) if validator.warnings.any?
|
36
|
+
end
|
37
|
+
|
28
38
|
config_hash = YAML.load(yaml_content, aliases: true)
|
29
39
|
|
30
40
|
validate_config!(config_hash)
|
@@ -134,6 +144,30 @@ module Roast
|
|
134
144
|
options[:target] || config_hash["target"]
|
135
145
|
end
|
136
146
|
|
147
|
+
# Extract context management configuration
|
148
|
+
# @param config_hash [Hash] The configuration hash
|
149
|
+
# @return [Hash] The context management configuration with defaults
|
150
|
+
def extract_context_management(config_hash)
|
151
|
+
default_config = {
|
152
|
+
enabled: true,
|
153
|
+
strategy: "auto",
|
154
|
+
threshold: 0.8,
|
155
|
+
max_tokens: nil,
|
156
|
+
retain_steps: [],
|
157
|
+
}
|
158
|
+
|
159
|
+
return default_config unless config_hash["context_management"].is_a?(Hash)
|
160
|
+
|
161
|
+
config = config_hash["context_management"]
|
162
|
+
{
|
163
|
+
enabled: config.fetch("enabled", default_config[:enabled]),
|
164
|
+
strategy: config.fetch("strategy", default_config[:strategy]),
|
165
|
+
threshold: config.fetch("threshold", default_config[:threshold]),
|
166
|
+
max_tokens: config["max_tokens"],
|
167
|
+
retain_steps: config.fetch("retain_steps", default_config[:retain_steps]),
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
137
171
|
private
|
138
172
|
|
139
173
|
def validate_path!(workflow_path)
|
@@ -145,6 +179,32 @@ module Roast
|
|
145
179
|
def validate_config!(config_hash)
|
146
180
|
raise ArgumentError, "Invalid workflow configuration" unless config_hash.is_a?(Hash)
|
147
181
|
end
|
182
|
+
|
183
|
+
def raise_validation_errors(validator)
|
184
|
+
error_messages = validator.errors.map do |error|
|
185
|
+
message = "• #{error[:message]}"
|
186
|
+
message += " (#{error[:suggestion]})" if error[:suggestion]
|
187
|
+
message
|
188
|
+
end.join("\n")
|
189
|
+
|
190
|
+
raise CLI::Kit::Abort, <<~ERROR
|
191
|
+
Workflow validation failed with #{validator.errors.size} error(s):
|
192
|
+
|
193
|
+
#{error_messages}
|
194
|
+
ERROR
|
195
|
+
end
|
196
|
+
|
197
|
+
def display_warnings(warnings)
|
198
|
+
return if warnings.empty?
|
199
|
+
|
200
|
+
::CLI::UI::Frame.open("Validation Warnings", color: :yellow) do
|
201
|
+
warnings.each do |warning|
|
202
|
+
puts ::CLI::UI.fmt("{{yellow:#{warning[:message]}}}")
|
203
|
+
puts ::CLI::UI.fmt(" {{gray:→ #{warning[:suggestion]}}}") if warning[:suggestion]
|
204
|
+
puts
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
148
208
|
end
|
149
209
|
end
|
150
210
|
end
|
@@ -1,17 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/workflow/configuration"
|
4
|
-
require "roast/workflow/workflow_initializer"
|
5
|
-
require "roast/workflow/workflow_runner"
|
6
|
-
|
7
3
|
module Roast
|
8
4
|
module Workflow
|
9
5
|
class ConfigurationParser
|
10
|
-
extend Forwardable
|
11
|
-
|
12
6
|
attr_reader :configuration, :options, :files, :current_workflow
|
13
7
|
|
14
|
-
|
8
|
+
delegate :output, to: :current_workflow
|
15
9
|
|
16
10
|
def initialize(workflow_path, files = [], options = {})
|
17
11
|
@configuration = Configuration.new(workflow_path, options)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
class ContextManager
|
6
|
+
attr_reader :total_tokens
|
7
|
+
|
8
|
+
def initialize(token_counter: nil, threshold_checker: nil)
|
9
|
+
@token_counter = token_counter || Services::TokenCountingService.new
|
10
|
+
@threshold_checker = threshold_checker || Services::ContextThresholdChecker.new
|
11
|
+
@total_tokens = 0
|
12
|
+
@message_count = 0
|
13
|
+
@config = default_config
|
14
|
+
@last_actual_update = nil
|
15
|
+
@estimated_tokens_since_update = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure(config)
|
19
|
+
@config = default_config.merge(config)
|
20
|
+
end
|
21
|
+
|
22
|
+
def track_usage(messages)
|
23
|
+
current_tokens = @token_counter.count_messages(messages)
|
24
|
+
@total_tokens += current_tokens
|
25
|
+
@message_count += messages.size
|
26
|
+
|
27
|
+
{
|
28
|
+
current_tokens: current_tokens,
|
29
|
+
total_tokens: @total_tokens,
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def should_compact?(token_count = @total_tokens)
|
34
|
+
return false unless @config[:enabled]
|
35
|
+
|
36
|
+
@threshold_checker.should_compact?(
|
37
|
+
token_count,
|
38
|
+
@config[:threshold],
|
39
|
+
@config[:max_tokens],
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_warnings(token_count = @total_tokens)
|
44
|
+
return unless @config[:enabled]
|
45
|
+
|
46
|
+
warning = @threshold_checker.check_warning_threshold(
|
47
|
+
token_count,
|
48
|
+
@config[:threshold],
|
49
|
+
@config[:max_tokens],
|
50
|
+
)
|
51
|
+
|
52
|
+
if warning
|
53
|
+
ActiveSupport::Notifications.instrument("roast.context_warning", warning)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def reset
|
58
|
+
@total_tokens = 0
|
59
|
+
@message_count = 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def statistics
|
63
|
+
{
|
64
|
+
total_tokens: @total_tokens,
|
65
|
+
message_count: @message_count,
|
66
|
+
average_tokens_per_message: @message_count > 0 ? @total_tokens / @message_count : 0,
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def update_with_actual_usage(actual_total)
|
71
|
+
return unless actual_total && actual_total > 0
|
72
|
+
|
73
|
+
@total_tokens = actual_total
|
74
|
+
@last_actual_update = Time.now
|
75
|
+
@estimated_tokens_since_update = 0
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def default_config
|
81
|
+
{
|
82
|
+
enabled: true,
|
83
|
+
threshold: 0.8,
|
84
|
+
max_tokens: nil, # Will use default from threshold checker
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -9,7 +9,7 @@ module Roast
|
|
9
9
|
|
10
10
|
def [](key)
|
11
11
|
value = @hash[key.to_sym] || @hash[key.to_s]
|
12
|
-
|
12
|
+
wrap_value(value)
|
13
13
|
end
|
14
14
|
|
15
15
|
def []=(key, value)
|
@@ -193,6 +193,21 @@ module Roast
|
|
193
193
|
end
|
194
194
|
|
195
195
|
alias_method :member?, :has_key?
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def wrap_value(value)
|
200
|
+
case value
|
201
|
+
when Hash
|
202
|
+
DotAccessHash.new(value)
|
203
|
+
when Array
|
204
|
+
# Don't create a new array - return the original array
|
205
|
+
# Only wrap Hash elements within the array when needed
|
206
|
+
value
|
207
|
+
else
|
208
|
+
value
|
209
|
+
end
|
210
|
+
end
|
196
211
|
end
|
197
212
|
end
|
198
213
|
end
|