roast-ai 0.4.10 → 0.5.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/.claude/commands/docs/write-comments.md +36 -0
- data/.github/CODEOWNERS +1 -1
- data/.github/workflows/ci.yaml +10 -6
- data/.gitignore +0 -1
- data/.rubocop.yml +7 -1
- data/.ruby-version +1 -1
- data/CLAUDE.md +2 -2
- data/CONTRIBUTING.md +2 -0
- data/Gemfile +19 -18
- data/Gemfile.lock +35 -58
- data/README.md +118 -1432
- data/README_LEGACY.md +1464 -0
- data/Rakefile +39 -4
- data/dev.yml +29 -0
- data/dsl/agent_sessions.rb +20 -0
- data/dsl/async_cogs.rb +49 -0
- data/dsl/async_cogs_complex.rb +67 -0
- data/dsl/call.rb +44 -0
- data/dsl/collect_from.rb +72 -0
- data/dsl/json_output.rb +28 -0
- data/dsl/map.rb +55 -0
- data/dsl/map_reduce.rb +37 -0
- data/dsl/map_with_index.rb +49 -0
- data/dsl/next_break.rb +45 -0
- data/dsl/next_break_parallel.rb +44 -0
- data/dsl/outputs.rb +39 -0
- data/dsl/outputs_bang.rb +36 -0
- data/dsl/parallel_map.rb +37 -0
- data/dsl/prompts/simple_prompt.md.erb +3 -0
- data/dsl/prototype.rb +5 -7
- data/dsl/repeat_loop_results.rb +53 -0
- data/dsl/ruby_cog.rb +72 -0
- data/dsl/simple_agent.rb +18 -0
- data/dsl/simple_chat.rb +15 -1
- data/dsl/simple_repeat.rb +29 -0
- data/dsl/skip.rb +36 -0
- data/dsl/step_communication.rb +2 -3
- data/dsl/targets_and_params.rb +57 -0
- data/dsl/temperature.rb +17 -0
- data/dsl/temporary_directory.rb +22 -0
- data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
- data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
- data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
- data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
- data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
- data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
- data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
- data/dsl/tutorial/03_targets_and_params/README.md +230 -0
- data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
- data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
- data/dsl/tutorial/04_configuration_options/README.md +209 -0
- data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
- data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
- data/dsl/tutorial/05_control_flow/README.md +156 -0
- data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
- data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
- data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
- data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
- data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
- data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
- data/dsl/tutorial/07_processing_collections/README.md +152 -0
- data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
- data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
- data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
- data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
- data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
- data/dsl/tutorial/09_async_cogs/README.md +197 -0
- data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
- data/dsl/tutorial/README.md +222 -0
- data/dsl/working_directory.rb +16 -0
- data/exe/roast +1 -1
- data/internal/documentation/architectural-notes.md +115 -0
- data/internal/documentation/doc-comments-external.md +686 -0
- data/internal/documentation/doc-comments-internal.md +342 -0
- data/internal/documentation/doc-comments.md +211 -0
- data/lib/roast/dsl/cog/config.rb +274 -3
- data/lib/roast/dsl/cog/input.rb +53 -10
- data/lib/roast/dsl/cog/output.rb +297 -8
- data/lib/roast/dsl/cog/registry.rb +35 -3
- data/lib/roast/dsl/cog/stack.rb +1 -1
- data/lib/roast/dsl/cog/store.rb +5 -5
- data/lib/roast/dsl/cog.rb +70 -14
- data/lib/roast/dsl/cog_input_context.rb +36 -1
- data/lib/roast/dsl/cog_input_manager.rb +116 -7
- data/lib/roast/dsl/cogs/agent/config.rb +465 -0
- data/lib/roast/dsl/cogs/agent/input.rb +81 -0
- data/lib/roast/dsl/cogs/agent/output.rb +59 -0
- data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
- data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
- data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
- data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
- data/lib/roast/dsl/cogs/agent.rb +75 -0
- data/lib/roast/dsl/cogs/chat/config.rb +453 -0
- data/lib/roast/dsl/cogs/chat/input.rb +92 -0
- data/lib/roast/dsl/cogs/chat/output.rb +64 -0
- data/lib/roast/dsl/cogs/chat/session.rb +68 -0
- data/lib/roast/dsl/cogs/chat.rb +59 -56
- data/lib/roast/dsl/cogs/cmd.rb +251 -61
- data/lib/roast/dsl/cogs/ruby.rb +171 -0
- data/lib/roast/dsl/command_runner.rb +191 -0
- data/lib/roast/dsl/config_manager.rb +58 -11
- data/lib/roast/dsl/control_flow.rb +41 -0
- data/lib/roast/dsl/execution_manager.rb +162 -32
- data/lib/roast/dsl/nil_assertions.rb +23 -0
- data/lib/roast/dsl/system_cog/params.rb +32 -0
- data/lib/roast/dsl/system_cog.rb +36 -0
- data/lib/roast/dsl/system_cogs/call.rb +163 -0
- data/lib/roast/dsl/system_cogs/map.rb +454 -0
- data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
- data/lib/roast/dsl/workflow.rb +26 -16
- data/lib/roast/dsl/workflow_context.rb +20 -0
- data/lib/roast/dsl/workflow_params.rb +24 -0
- data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
- data/lib/roast/sorbet_runtime_stub.rb +154 -0
- data/lib/roast/tools/apply_diff.rb +1 -3
- data/lib/roast/tools/cmd.rb +4 -3
- data/lib/roast/tools/read_file.rb +1 -1
- data/lib/roast/tools/update_files.rb +1 -1
- data/lib/roast/tools/write_file.rb +1 -1
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_workflow.rb +4 -0
- data/lib/roast/workflow/step_loader.rb +14 -2
- data/lib/roast-ai.rb +4 -0
- data/lib/roast.rb +58 -21
- data/{roast.gemspec → roast-ai.gemspec} +9 -13
- data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
- data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
- data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
- data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
- data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
- data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
- data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
- data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
- data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
- data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
- data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1185 -5
- data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +311 -5
- data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +486 -5
- data/sorbet/tapioca/config.yml +6 -0
- data/sorbet/tapioca/require.rb +2 -0
- metadata +157 -30
- data/dsl/less_simple.rb +0 -112
- data/dsl/scoped_executors.rb +0 -28
- data/dsl/simple.rb +0 -8
- data/lib/roast/dsl/cogs/execute.rb +0 -46
- data/lib/roast/dsl/cogs/graph.rb +0 -53
- data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
- data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
- data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
- data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
- data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
- data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
- data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
- data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
- data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
- data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
- data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
- data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
- data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
- data/sorbet/rbi/gems/rack@2.2.19.rbi +0 -5676
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
- data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::DSL::Workflow
|
|
5
|
+
|
|
6
|
+
# This workflow demonstrates basic named execute scopes and the call cog.
|
|
7
|
+
# It shows how to define reusable logic and invoke it multiple times.
|
|
8
|
+
|
|
9
|
+
config {}
|
|
10
|
+
|
|
11
|
+
# Define a reusable scope that displays a separator
|
|
12
|
+
execute(:print_separator) do
|
|
13
|
+
ruby { puts "=" * 70 }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Define a scope that generates a random word
|
|
17
|
+
execute(:random_word) do
|
|
18
|
+
cmd(:word) { "shuf /usr/share/dict/words -n 1" }
|
|
19
|
+
|
|
20
|
+
outputs! { cmd!(:word).text }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Main workflow
|
|
24
|
+
execute do
|
|
25
|
+
call(run: :print_separator)
|
|
26
|
+
|
|
27
|
+
ruby { puts "REUSABLE SCOPES EXAMPLE" }
|
|
28
|
+
|
|
29
|
+
call(run: :print_separator)
|
|
30
|
+
|
|
31
|
+
# Call the random_word scope three times
|
|
32
|
+
ruby { puts "\nGenerating three random words:" }
|
|
33
|
+
|
|
34
|
+
call(:word1, run: :random_word)
|
|
35
|
+
call(:word2, run: :random_word)
|
|
36
|
+
call(:word3, run: :random_word)
|
|
37
|
+
|
|
38
|
+
# Extract and display the results
|
|
39
|
+
ruby do
|
|
40
|
+
word1 = from(call!(:word1))
|
|
41
|
+
word2 = from(call!(:word2))
|
|
42
|
+
word3 = from(call!(:word3))
|
|
43
|
+
|
|
44
|
+
puts " 1. #{word1}"
|
|
45
|
+
puts " 2. #{word2}"
|
|
46
|
+
puts " 3. #{word3}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
call(run: :print_separator)
|
|
50
|
+
|
|
51
|
+
ruby do
|
|
52
|
+
puts <<~NOTE
|
|
53
|
+
KEY POINTS:
|
|
54
|
+
- Named scopes don't run automatically - they must be called
|
|
55
|
+
- The same scope can be called multiple times
|
|
56
|
+
- Each call is independent with its own execution context
|
|
57
|
+
- Use outputs! to return values from a scope
|
|
58
|
+
- Use from() to extract the returned value
|
|
59
|
+
NOTE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
call(run: :print_separator)
|
|
63
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::DSL::Workflow
|
|
5
|
+
|
|
6
|
+
# This workflow demonstrates parameterized scopes.
|
|
7
|
+
# It shows how to pass values to scopes and use them within the scope's cogs.
|
|
8
|
+
|
|
9
|
+
config do
|
|
10
|
+
chat do
|
|
11
|
+
model "gpt-4o-mini"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Define a scope that analyzes text
|
|
16
|
+
execute(:analyze_text) do
|
|
17
|
+
chat(:analysis) do |_, text|
|
|
18
|
+
# The text parameter comes from the call cog's input block
|
|
19
|
+
<<~PROMPT
|
|
20
|
+
Analyze this text and provide:
|
|
21
|
+
1. Word count (approximate)
|
|
22
|
+
2. Main topic
|
|
23
|
+
3. Sentiment (positive/negative/neutral)
|
|
24
|
+
|
|
25
|
+
Text: #{text}
|
|
26
|
+
|
|
27
|
+
Return your analysis as a brief summary.
|
|
28
|
+
PROMPT
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
outputs! { chat!(:analysis).response }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Main workflow
|
|
35
|
+
execute do
|
|
36
|
+
ruby { puts "=" * 70 }
|
|
37
|
+
ruby { puts "PARAMETERIZED SCOPES EXAMPLE" }
|
|
38
|
+
ruby { puts "=" * 70 }
|
|
39
|
+
|
|
40
|
+
# Analyze different texts using the same scope
|
|
41
|
+
call(:analysis1, run: :analyze_text) do
|
|
42
|
+
"The quick brown fox jumps over the lazy dog."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
call(:analysis2, run: :analyze_text) do
|
|
46
|
+
"Artificial intelligence is transforming how we build software."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
call(:analysis3, run: :analyze_text) do
|
|
50
|
+
"I'm disappointed with the results of this experiment."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Display all results
|
|
54
|
+
ruby do
|
|
55
|
+
puts "\nANALYSIS 1:"
|
|
56
|
+
puts from(call!(:analysis1))
|
|
57
|
+
|
|
58
|
+
puts "\nANALYSIS 2:"
|
|
59
|
+
puts from(call!(:analysis2))
|
|
60
|
+
|
|
61
|
+
puts "\nANALYSIS 3:"
|
|
62
|
+
puts from(call!(:analysis3))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
ruby { puts "\n" + "=" * 70 }
|
|
66
|
+
|
|
67
|
+
ruby do
|
|
68
|
+
puts <<~NOTE
|
|
69
|
+
KEY POINTS:
|
|
70
|
+
- Pass values to scopes using call's input block
|
|
71
|
+
- Access the value as the second parameter in cog blocks
|
|
72
|
+
- The same scope can process different inputs
|
|
73
|
+
- This makes scopes reusable and composable
|
|
74
|
+
NOTE
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
ruby { puts "=" * 70 }
|
|
78
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Chapter 7: Processing Collections
|
|
2
|
+
|
|
3
|
+
Learn how to process arrays and other collections with the `map` cog, which applies a named execute scope to each item
|
|
4
|
+
in a collection.
|
|
5
|
+
|
|
6
|
+
## The `map` Cog
|
|
7
|
+
|
|
8
|
+
The `map` cog executes a named execute scope (defined with `execute(:name)`) for each item in a collection:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
execute(:process_item) do
|
|
12
|
+
chat(:analyze) do |_, item|
|
|
13
|
+
"Analyze this item: #{item}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
execute do
|
|
18
|
+
map(run: :process_item) { ["item1", "item2", "item3"] }
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The scope receives each item as its value parameter, just like with the `call` cog.
|
|
23
|
+
|
|
24
|
+
## Collecting Results
|
|
25
|
+
|
|
26
|
+
Use `collect` to gather outputs from all iterations into an array:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Get all final outputs
|
|
30
|
+
results = collect(map!(:process_items))
|
|
31
|
+
|
|
32
|
+
# Transform outputs with a block, with access to the original item and its index in the source collection
|
|
33
|
+
results = collect(map!(:process_items)) do |output, item, index|
|
|
34
|
+
{ original_item: item, result_text: output.text, index: }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Access other cog outputs from within each iteration
|
|
38
|
+
sessions = collect(map!(:process_items)) do
|
|
39
|
+
chat!(:analyze).session
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Reducing Results
|
|
44
|
+
|
|
45
|
+
Use `reduce` to combine outputs into a single value:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# Sum all outputs
|
|
49
|
+
# The second argument to `reduce` is the initial value for the accumulator. It is required.
|
|
50
|
+
total = reduce(map!(:calculate_scores), 0) do |sum, output|
|
|
51
|
+
# the block given to `reduce` should return the new value of the accumulator at each step
|
|
52
|
+
sum + output
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Build a hash
|
|
56
|
+
results = reduce(map!(:process_items), {}) do |hash, output, item, index|
|
|
57
|
+
# returning nil will skip this item; it will not reset the accumulator to nil
|
|
58
|
+
hash.merge(item => output) unless output.text.include?("failure")
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Parallel Execution
|
|
63
|
+
|
|
64
|
+
By default, `map` runs iterations serially. Configure parallel execution for the `map` cog in the `config` block.
|
|
65
|
+
Roast uses fibers for efficient asynchronous operation.
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
config do
|
|
69
|
+
map do
|
|
70
|
+
parallel(3) # Run up to 3 iterations concurrently for all `map` cogs
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
map(:unlimited) do
|
|
74
|
+
parallel! # Run all iterations in parallel for a specific `map` cog
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Results from `collect` and `reduce` are always returned in the original order, regardless of completion order.
|
|
80
|
+
|
|
81
|
+
## Accessing Specific Iterations
|
|
82
|
+
|
|
83
|
+
Access the output from a specific iteration using `.iteration(index)`.
|
|
84
|
+
This is convenient shorthand to avoid having to `collect` all the outputs when you only want one
|
|
85
|
+
in a particular situation.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Get output from third iteration (index 2)
|
|
89
|
+
result = from(map!(:process_items).iteration(2))
|
|
90
|
+
|
|
91
|
+
# Access first and last iterations
|
|
92
|
+
first_result = from(map!(:process_items).first)
|
|
93
|
+
last_result = from(map!(:process_items).last)
|
|
94
|
+
|
|
95
|
+
# Use with a block to access specific cogs
|
|
96
|
+
result = from(map!(:process_items).iteration(2)) do
|
|
97
|
+
chat!(:analyze).response
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Working with Indices
|
|
102
|
+
|
|
103
|
+
Scopes called by `map` receive the iteration index as a third parameter:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
execute(:process_with_index) do
|
|
107
|
+
chat(:numbered) do |_, item, index|
|
|
108
|
+
"Process item #{index}: #{item}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
execute do
|
|
113
|
+
# Default: indices start at 0
|
|
114
|
+
map(run: :process_with_index) { ["a", "b", "c"] }
|
|
115
|
+
|
|
116
|
+
# Custom starting index
|
|
117
|
+
map(run: :process_with_index) do |my|
|
|
118
|
+
my.items = ["a", "b", "c"]
|
|
119
|
+
my.initial_index = 1 # Start counting from 1
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Technically, because you can call a scope with `call` just as well as `map`, the third `index` argument
|
|
125
|
+
will always be present. When you invoke a scope with `call`, the index will be 0 by default. You can also override
|
|
126
|
+
the index value for an individual call invocation, to simulate processing one item from the middle of a collection.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
call(run: :some_scope) do |my|
|
|
130
|
+
my.value = "some data"
|
|
131
|
+
my.index = 3
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Running the Workflows
|
|
136
|
+
|
|
137
|
+
Try these examples to see `map` in action:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Basic map with collect, reduce, and accessing specific iterations
|
|
141
|
+
bin/roast execute --executor=dsl dsl/tutorial/07_processing_collections/basic_map.rb
|
|
142
|
+
|
|
143
|
+
# Parallel execution
|
|
144
|
+
bin/roast execute --executor=dsl dsl/tutorial/07_processing_collections/parallel_map.rb
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## What's Next?
|
|
148
|
+
|
|
149
|
+
In the next chapter, you'll learn about iterative workflows with the `repeat` cog: an easy way to execute a set of
|
|
150
|
+
steps repeatedly until a condition is met.
|
|
151
|
+
|
|
152
|
+
But first, experiment with `map` to process collections and try different parallel execution strategies!
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::DSL::Workflow
|
|
5
|
+
|
|
6
|
+
config do
|
|
7
|
+
chat do
|
|
8
|
+
model "gpt-4o-mini"
|
|
9
|
+
provider :openai
|
|
10
|
+
no_display!
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
execute(:analyze_city) do
|
|
15
|
+
chat(:extract_info) do |_, city|
|
|
16
|
+
<<~PROMPT
|
|
17
|
+
For the city #{city}, provide a one-sentence fact about its population or geography.
|
|
18
|
+
Keep it brief and factual.
|
|
19
|
+
PROMPT
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
execute do
|
|
24
|
+
cities = ["Tokyo", "Paris", "Cairo", "Sydney"]
|
|
25
|
+
|
|
26
|
+
# Apply the :analyze_city scope to each city
|
|
27
|
+
map(:city_analysis, run: :analyze_city) { cities }
|
|
28
|
+
|
|
29
|
+
# Collect all the responses
|
|
30
|
+
ruby do
|
|
31
|
+
puts "\n=== City Facts ==="
|
|
32
|
+
facts = collect(map!(:city_analysis)) do
|
|
33
|
+
chat!(:extract_info).response.strip
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
cities.each_with_index do |city, i|
|
|
37
|
+
puts "#{city}: #{facts[i]}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Use reduce to combine results
|
|
42
|
+
ruby do
|
|
43
|
+
summary = reduce(map!(:city_analysis), "Summary of cities:") do |acc, _, city, index|
|
|
44
|
+
"#{acc}\n- #{city} (position #{index})"
|
|
45
|
+
end
|
|
46
|
+
puts "\n#{summary}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Access a specific iteration
|
|
50
|
+
ruby do
|
|
51
|
+
puts "\n=== Accessing Specific Iterations ==="
|
|
52
|
+
|
|
53
|
+
# Get the second city's analysis (Paris, index 1)
|
|
54
|
+
paris_fact = from(map!(:city_analysis).iteration(1)) do
|
|
55
|
+
chat!(:extract_info).response.strip
|
|
56
|
+
end
|
|
57
|
+
puts "Paris (index 1): #{paris_fact}"
|
|
58
|
+
|
|
59
|
+
# Access first and last
|
|
60
|
+
first_city = from(map!(:city_analysis).first) do
|
|
61
|
+
chat!(:extract_info).response.strip
|
|
62
|
+
end
|
|
63
|
+
last_city = from(map!(:city_analysis).last) do
|
|
64
|
+
chat!(:extract_info).response.strip
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
puts "First: #{first_city}"
|
|
68
|
+
puts "Last: #{last_city}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::DSL::Workflow
|
|
5
|
+
|
|
6
|
+
config do
|
|
7
|
+
chat do
|
|
8
|
+
model "gpt-4o-mini"
|
|
9
|
+
provider :openai
|
|
10
|
+
no_display!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
map(:serial) do
|
|
14
|
+
no_parallel! # Explicitly serial (this is the default)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
map(:limited_parallel) do
|
|
18
|
+
parallel(2) # Process up to 2 items concurrently
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
map(:unlimited_parallel) do
|
|
22
|
+
parallel! # Process all items concurrently
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
execute(:generate_fact) do
|
|
27
|
+
chat(:fact) do |_, topic, index|
|
|
28
|
+
<<~PROMPT
|
|
29
|
+
Generate a brief, interesting fact (one sentence) about: #{topic}
|
|
30
|
+
Label it as "Fact #{index}:"
|
|
31
|
+
PROMPT
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
execute do
|
|
36
|
+
topics = ["Ruby", "Python", "JavaScript", "Go", "Rust"]
|
|
37
|
+
|
|
38
|
+
# Serial execution (one at a time)
|
|
39
|
+
map(:serial, run: :generate_fact) do |my|
|
|
40
|
+
my.items = topics
|
|
41
|
+
my.initial_index = 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
ruby do
|
|
45
|
+
puts "\n=== Serial Execution ==="
|
|
46
|
+
facts = collect(map!(:serial)) { chat!(:fact).response.strip }
|
|
47
|
+
facts.each { |fact| puts fact }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Limited parallelism (up to 2 concurrent)
|
|
51
|
+
map(:limited_parallel, run: :generate_fact) do |my|
|
|
52
|
+
my.items = topics
|
|
53
|
+
my.initial_index = 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
ruby do
|
|
57
|
+
puts "\n=== Limited Parallel Execution (max 2 concurrent) ==="
|
|
58
|
+
facts = collect(map!(:limited_parallel)) { chat!(:fact).response.strip }
|
|
59
|
+
facts.each { |fact| puts fact }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Unlimited parallelism (all at once)
|
|
63
|
+
map(:unlimited_parallel, run: :generate_fact) do |my|
|
|
64
|
+
my.items = topics
|
|
65
|
+
my.initial_index = 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
ruby do
|
|
69
|
+
puts "\n=== Unlimited Parallel Execution ==="
|
|
70
|
+
facts = collect(map!(:unlimited_parallel)) { chat!(:fact).response.strip }
|
|
71
|
+
facts.each { |fact| puts fact }
|
|
72
|
+
puts "\nNote: Results are always returned in original order, regardless of completion order."
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Chapter 8: Iterative Workflows
|
|
2
|
+
|
|
3
|
+
Learn how to create iterative workflows with the `repeat` cog, which executes a named scope repeatedly until a condition
|
|
4
|
+
is met. Unlike `map` which processes a collection holding a fix number of items, `repeat` continues indefinitely until
|
|
5
|
+
it hits a specified maximum number of iterations or you explicitly stop it with `break!`.
|
|
6
|
+
|
|
7
|
+
## The `repeat` Cog
|
|
8
|
+
|
|
9
|
+
The `repeat` cog executes a named execute scope repeatedly, where the output of each iteration becomes the input to the
|
|
10
|
+
next:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
execute(:process) do
|
|
14
|
+
ruby(:step_increment) do |_, value, index|
|
|
15
|
+
new_value = value + index
|
|
16
|
+
puts "Current value: #{new_value}"
|
|
17
|
+
new_value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
ruby { break! if ruby!(:step_increment).value >= 12 }
|
|
21
|
+
|
|
22
|
+
outputs { ruby!(:step_increment).value }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
execute do
|
|
26
|
+
repeat(:loop, run: :process) { 0 } # Start with initial value 0
|
|
27
|
+
|
|
28
|
+
ruby do
|
|
29
|
+
puts "Final value: #{repeat!(:loop).value}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Each iteration receives two parameters:
|
|
35
|
+
|
|
36
|
+
1. The **value** from the previous iteration (or the initial value for the first iteration)
|
|
37
|
+
2. The **index** starting at 0 (or a custom `initial_index`)
|
|
38
|
+
|
|
39
|
+
Key differences from `map`:
|
|
40
|
+
|
|
41
|
+
- `repeat` continues until you call `break!` (or hit the specified maximum number of iterations)
|
|
42
|
+
- Each iteration's output becomes the next iteration's input (iterative transformation)
|
|
43
|
+
- `map` processes independent items; `repeat` builds up a result iteratively
|
|
44
|
+
|
|
45
|
+
## Maximum Iteration Limit
|
|
46
|
+
|
|
47
|
+
You can specify the optional `max_iterations` attribute on the input value of the `repeat` cog to set a limit
|
|
48
|
+
on the number of loop iterations that can run. By default, the value of this attribute is `nil`, and the loop
|
|
49
|
+
will be allowed to run forever, until explicitly terminated from within.
|
|
50
|
+
|
|
51
|
+
## Breaking Out of Loops
|
|
52
|
+
|
|
53
|
+
Use `break!` to terminate the loop. Once `break!` is called, no more iterations will run.
|
|
54
|
+
Also, the cog in which you call `break!` will not run, and no subsequent cogs in that iteration will run either.
|
|
55
|
+
It is often cleanest to call `break!` in a `ruby` cog at the end of the scope, that only performs a check for the
|
|
56
|
+
termination condition(s), but you can call it anywhere that makes sense.
|
|
57
|
+
|
|
58
|
+
NOTE: the `outputs` block will __always__ run, even if you call `break!` earlier in the scope.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
execute(:find_threshold) do
|
|
62
|
+
chat(:analyze) do |_, number|
|
|
63
|
+
"Is #{number} greater than 50? Answer yes or no."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
ruby do
|
|
67
|
+
response = chat!(:analyze).response
|
|
68
|
+
break! if response.downcase.include?("yes")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
outputs { |_, number| number + 10 }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
execute do
|
|
75
|
+
repeat(:search, run: :find_threshold) { 0 }
|
|
76
|
+
|
|
77
|
+
ruby do
|
|
78
|
+
puts "Stopped at: #{repeat!(:search).value}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Skipping to the Next Iteration
|
|
84
|
+
|
|
85
|
+
Use `next!` to skip the rest of the current iteration and immediately start the next one:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
execute(:process_numbers) do
|
|
89
|
+
ruby(:check) do |_, _, index|
|
|
90
|
+
# Skip processing for multiples of 3
|
|
91
|
+
if index % 3 == 0
|
|
92
|
+
puts " Skipping every third 3 iteration"
|
|
93
|
+
next!
|
|
94
|
+
end
|
|
95
|
+
puts "Processing #{index}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
ruby(:double) do |_, value|
|
|
99
|
+
result = value * 2
|
|
100
|
+
puts " Doubled to #{result}"
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
ruby { |_, _, index| break! if index >= 10 }
|
|
105
|
+
|
|
106
|
+
outputs { ruby!(:double).value }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
execute do
|
|
110
|
+
repeat(run: :process_numbers) { 1 }
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
When `next!` is called, the remaining cogs in that iteration are skipped, but the `outputs` block will still run, to
|
|
115
|
+
generate the input value for the next iteration.
|
|
116
|
+
|
|
117
|
+
## Accessing Iteration Results
|
|
118
|
+
|
|
119
|
+
Access specific iterations or the final result:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
execute do
|
|
123
|
+
repeat(:loop, run: :process) { 0 }
|
|
124
|
+
|
|
125
|
+
ruby do
|
|
126
|
+
# Get value directly from the final iteration's output block
|
|
127
|
+
final = repeat!(:loop).value
|
|
128
|
+
puts "Final result: #{final}"
|
|
129
|
+
|
|
130
|
+
# Access specific iterations
|
|
131
|
+
first = from(repeat!(:loop).first)
|
|
132
|
+
last = from(repeat!(:loop).last)
|
|
133
|
+
third = from(repeat!(:loop).iteration(2))
|
|
134
|
+
|
|
135
|
+
puts "First iteration: #{first}"
|
|
136
|
+
puts "Last iteration: #{last}"
|
|
137
|
+
puts "Third iteration: #{third}"
|
|
138
|
+
|
|
139
|
+
# Access specific cogs in specific iterations
|
|
140
|
+
answer = from(repeat!(:loop).iteration(-2)) { chat!(:question).text }
|
|
141
|
+
puts "Second-to-last answer: #{answer}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Processing All Iterations
|
|
147
|
+
|
|
148
|
+
Use `.results` with `collect` or `reduce` to process all iteration outputs:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
execute do
|
|
152
|
+
repeat(:loop, run: :calculate) { 1 }
|
|
153
|
+
|
|
154
|
+
ruby do
|
|
155
|
+
# Collect all intermediate values
|
|
156
|
+
all_values = collect(repeat!(:loop).results) do
|
|
157
|
+
ruby!(:calculate).value
|
|
158
|
+
end
|
|
159
|
+
puts "All values: #{all_values.inspect}"
|
|
160
|
+
|
|
161
|
+
# Sum all iterations
|
|
162
|
+
total = reduce(repeat!(:loop).results, 0) do |sum|
|
|
163
|
+
sum + ruby!(:calculate).value
|
|
164
|
+
end
|
|
165
|
+
puts "Sum of all iterations: #{total}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## The `outputs` Block
|
|
171
|
+
|
|
172
|
+
The `outputs` block determines what value gets passed to the next iteration. It always runs, even on an iteration where
|
|
173
|
+
`break!` or `next!` is called:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
execute(:accumulate) do
|
|
177
|
+
ruby(:add) do |_, sum, index|
|
|
178
|
+
sum + index
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
ruby { |_, _, index| break! if index >= 5 }
|
|
182
|
+
|
|
183
|
+
# This runs even on the final iteration (when break! is called)
|
|
184
|
+
outputs { ruby!(:add).value }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
execute do
|
|
188
|
+
repeat(run: :accumulate) { 0 }
|
|
189
|
+
|
|
190
|
+
ruby do
|
|
191
|
+
# The final value includes the computation from the iteration where break! was called
|
|
192
|
+
puts "Final sum: #{repeat!(:accumulate).value}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Custom Starting Index
|
|
198
|
+
|
|
199
|
+
Customize where iteration indices start:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
execute do
|
|
203
|
+
repeat(run: :process) do |my|
|
|
204
|
+
my.value = 100
|
|
205
|
+
my.index = 10 # Start counting from 10
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
ruby do
|
|
209
|
+
puts "Final: #{repeat!.value}"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Running the Workflows
|
|
215
|
+
|
|
216
|
+
Try these examples to see `repeat` in action:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Basic iterative transformation
|
|
220
|
+
bin/roast execute --executor=dsl dsl/tutorial/08_iterative_workflows/basic_repeat.rb
|
|
221
|
+
|
|
222
|
+
# Using break! to terminate based on conditions
|
|
223
|
+
bin/roast execute --executor=dsl dsl/tutorial/08_iterative_workflows/conditional_break.rb
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## What's Next?
|
|
227
|
+
|
|
228
|
+
In the final chapter, you'll learn about asynchronous cogs: how to run multiple independent tasks concurrently to improve
|
|
229
|
+
workflow performance without waiting for each task to complete before starting the next.
|
|
230
|
+
|
|
231
|
+
But first, experiment with `repeat` to create iterative transformations and see how values flow from one iteration to the next!
|