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,68 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::DSL::Workflow
|
|
5
|
+
|
|
6
|
+
# This workflow demonstrates basic configuration of models and providers.
|
|
7
|
+
# It shows how to set global defaults and override them for specific steps.
|
|
8
|
+
|
|
9
|
+
config do
|
|
10
|
+
# Set default configuration for all chat cogs
|
|
11
|
+
chat do
|
|
12
|
+
model "gpt-4o-mini"
|
|
13
|
+
provider :openai
|
|
14
|
+
show_prompt! # Show prompts for all chat cogs
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Override for a specific cog - use more capable model
|
|
18
|
+
chat(:analyze_trends) do
|
|
19
|
+
model "gpt-5"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Override for a specific cog - hide response
|
|
23
|
+
chat(:format_report) do
|
|
24
|
+
no_show_response!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
execute do
|
|
29
|
+
# This cog uses the global config (gpt-4o-mini)
|
|
30
|
+
chat(:extract_info) do
|
|
31
|
+
<<~PROMPT
|
|
32
|
+
Extract the key information from this text and format it as a bullet list:
|
|
33
|
+
|
|
34
|
+
"Our Q4 results show revenue of $2.5M, up 15% from Q3. Customer count increased to 1,200
|
|
35
|
+
(from 950), and average deal size grew from $2,000 to $2,300. However, churn rate rose
|
|
36
|
+
slightly to 8% from 7%."
|
|
37
|
+
PROMPT
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# This cog uses the specific override configured above (gpt-5)
|
|
41
|
+
chat(:analyze_trends) do
|
|
42
|
+
<<~PROMPT
|
|
43
|
+
Analyze these metrics and identify the most important trends:
|
|
44
|
+
|
|
45
|
+
#{chat!(:extract_info).text}
|
|
46
|
+
|
|
47
|
+
Focus on: growth patterns, concerning signals, and strategic implications.
|
|
48
|
+
PROMPT
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# This cog uses the specific configuration above (hidden response)
|
|
52
|
+
chat(:format_report) do
|
|
53
|
+
<<~PROMPT
|
|
54
|
+
Format this analysis as a structured report with clear sections:
|
|
55
|
+
|
|
56
|
+
#{chat!(:analyze_trends).text}
|
|
57
|
+
PROMPT
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Final step: display the formatted report
|
|
61
|
+
ruby(:display) do
|
|
62
|
+
puts "\n" + "=" * 70
|
|
63
|
+
puts "QUARTERLY ANALYSIS REPORT"
|
|
64
|
+
puts "=" * 70
|
|
65
|
+
puts chat!(:format_report).response
|
|
66
|
+
puts "=" * 70 + "\n"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Chapter 5: Control Flow
|
|
2
|
+
|
|
3
|
+
In previous chapters, you learned how to chain cogs together and configure them. Now you'll learn how to create
|
|
4
|
+
dynamic workflows that adapt based on conditions: skipping steps when needed, handling failures gracefully, and
|
|
5
|
+
checking whether steps actually ran.
|
|
6
|
+
|
|
7
|
+
## What You'll Learn
|
|
8
|
+
|
|
9
|
+
- How to conditionally skip cogs with `skip!`
|
|
10
|
+
- How to handle cog failures with `fail!` and `no_abort_on_failure!`
|
|
11
|
+
- How command failures work and how to control them
|
|
12
|
+
- How to check if a cog ran successfully with three different accessors
|
|
13
|
+
- The difference between `!`, `?`, and non-bang accessors
|
|
14
|
+
|
|
15
|
+
## Conditional Execution with skip!
|
|
16
|
+
|
|
17
|
+
Use `skip!` inside a cog's input block to conditionally skip that cog:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
execute do
|
|
21
|
+
cmd(:check_status) { "curl https://api.example.com/status" }
|
|
22
|
+
|
|
23
|
+
cmd(:notify) do
|
|
24
|
+
status = cmd!(:check_status).out
|
|
25
|
+
skip! if status.include?("healthy")
|
|
26
|
+
"echo 'Service needs attention!'"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When `skip!` is called, the cog immediately stops executing and is marked as skipped. Skipped cogs won't have
|
|
32
|
+
output and can be detected using the `?` accessor.
|
|
33
|
+
|
|
34
|
+
## Checking if Cogs Ran
|
|
35
|
+
|
|
36
|
+
You have three ways to access cog outputs, each with different behavior:
|
|
37
|
+
|
|
38
|
+
- `cog!(:name)` - Returns output, raises error if cog didn't run yet, was skipped, or failed
|
|
39
|
+
- `cog?(:name)` - Returns `true` if cog ran and completed successfully, `false` otherwise
|
|
40
|
+
- `cog(:name)` - Returns output or `nil` if cog didn't run yet, was skipped, or failed
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
execute do
|
|
44
|
+
chat(:analyze) do
|
|
45
|
+
data = cmd(:fetch_data).out # Returns nil if fetch_data was skipped
|
|
46
|
+
skip! unless data
|
|
47
|
+
"Analyze this: #{data}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
ruby do
|
|
51
|
+
if chat?(:analyze)
|
|
52
|
+
puts "Analysis completed: #{chat!(:analyze).response}"
|
|
53
|
+
else
|
|
54
|
+
puts "Analysis was skipped"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The `?` accessor is particularly useful for checking whether optional steps ran.
|
|
61
|
+
|
|
62
|
+
## Handling Failures
|
|
63
|
+
|
|
64
|
+
Cogs can fail in two ways: by explicitly calling `fail!`, or by encountering errors during execution (like a command
|
|
65
|
+
returning a non-zero exit code). By default, any cog failure will also abort the entire workflow.
|
|
66
|
+
|
|
67
|
+
### Explicit Failure with fail!
|
|
68
|
+
|
|
69
|
+
Use `fail!` to terminate a cog when conditions prevent successful execution:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
execute do
|
|
73
|
+
agent(:process) do |my|
|
|
74
|
+
file = Pathname.new("my/data/file.json")
|
|
75
|
+
fail! unless file.exist?
|
|
76
|
+
my.prompt = "Process this file: #{file.realpath}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Command Failures
|
|
82
|
+
|
|
83
|
+
By default, the `cmd` cog automatically fails when a command returns a non-zero exit status.
|
|
84
|
+
And also by default, when a cog fails the entire workflow is aborted.
|
|
85
|
+
You can control both aspects of this behavior:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
config do
|
|
89
|
+
cmd(:risky) do
|
|
90
|
+
# This command might fail, but continue the workflow anyway if it does
|
|
91
|
+
no_abort_on_failure!
|
|
92
|
+
end
|
|
93
|
+
cmd(:grep) do
|
|
94
|
+
# This command might exit with a non-zero status code, but that doesn't represent a failure for it
|
|
95
|
+
no_fail_on_error!
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
execute do
|
|
100
|
+
cmd(:risky) { "[ $RANDOM -gt 30000 ] && echo 'it worked!'" } # This command will probably fail
|
|
101
|
+
|
|
102
|
+
# We expect a non-zero exit code here as part of normal operations.
|
|
103
|
+
# `grep` matching no lines does not represent a failure condition for our workflow.
|
|
104
|
+
cmd(:grep) { "grep 'pattern' file.txt" }
|
|
105
|
+
|
|
106
|
+
ruby do
|
|
107
|
+
puts cmd?(:risky) ? "Risky command succeeded" : "Risky command failed"
|
|
108
|
+
puts "Grep matched #{cmd(:grep).lines.length} lines"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Use `no_abort_on_failure!` to let the workflow continue even when a cog fails. The non-bang and `?` accessors let you
|
|
114
|
+
check the result and handle failures gracefully.
|
|
115
|
+
|
|
116
|
+
Use `no_fail_on_error!` with the `cmd` cog specifically to indicate that a non-zero status code should not
|
|
117
|
+
be considered a failure.
|
|
118
|
+
|
|
119
|
+
## Running the Workflows
|
|
120
|
+
|
|
121
|
+
To run the examples in this chapter:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Conditional execution example
|
|
125
|
+
bin/roast execute --executor=dsl dsl/tutorial/05_control_flow/conditional_execution.rb
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Handling failures example
|
|
130
|
+
bin/roast execute --executor=dsl dsl/tutorial/05_control_flow/handling_failures.rb
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Try It Yourself
|
|
134
|
+
|
|
135
|
+
1. **Add conditions** - Use `skip!` to create optional workflow steps
|
|
136
|
+
2. **Check outcomes** - Use the `?` accessor to branch based on whether steps ran
|
|
137
|
+
3. **Handle failures** - Use `fail!` for validation and the non-bang accessor for recovery
|
|
138
|
+
4. **Combine approaches** - Mix conditional execution with the techniques from previous chapters
|
|
139
|
+
|
|
140
|
+
## Key Takeaways
|
|
141
|
+
|
|
142
|
+
- Use `skip!` to conditionally skip cogs based on runtime conditions
|
|
143
|
+
- Use `fail!` to explicitly terminate cogs that can't complete successfully
|
|
144
|
+
- Commands automatically fail on non-zero exit status (configurable with `no_fail_on_error!`)
|
|
145
|
+
- Use `no_abort_on_failure!` to continue workflows even when cogs fail
|
|
146
|
+
- Use `cog?(:name)` to check if a cog ran successfully
|
|
147
|
+
- Use `cog(:name)` (non-bang) to safely access outputs that might not exist
|
|
148
|
+
- Use `cog!(:name)` when you expect the cog to have run successfully
|
|
149
|
+
- Control flow makes workflows dynamic and adaptive
|
|
150
|
+
|
|
151
|
+
## What's Next?
|
|
152
|
+
|
|
153
|
+
In the next chapter, you'll learn about processing collections: using `map` to apply operations across multiple items
|
|
154
|
+
and building workflows that handle batches of data.
|
|
155
|
+
|
|
156
|
+
But first, experiment with control flow to create workflows that adapt to different conditions!
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::DSL::Workflow
|
|
5
|
+
|
|
6
|
+
# This workflow demonstrates conditional execution using skip! and the ? accessor.
|
|
7
|
+
# It checks a random number and runs different steps based on whether it's even or odd.
|
|
8
|
+
|
|
9
|
+
config do
|
|
10
|
+
cmd do
|
|
11
|
+
display!
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
execute do
|
|
16
|
+
# Generate a random number
|
|
17
|
+
cmd(:random_number) { "echo $RANDOM" }
|
|
18
|
+
|
|
19
|
+
# This step only runs if the number is even
|
|
20
|
+
cmd(:process_even) do
|
|
21
|
+
number = cmd!(:random_number).text.to_i
|
|
22
|
+
skip! if number.odd?
|
|
23
|
+
"echo 'Processing even number: #{number}'"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# This step only runs if the number is odd
|
|
27
|
+
cmd(:process_odd) do
|
|
28
|
+
number = cmd!(:random_number).text.to_i
|
|
29
|
+
skip! if number.even?
|
|
30
|
+
"echo 'Processing odd number: #{number}'"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# This step always runs and reports which path was taken
|
|
34
|
+
cmd do |my|
|
|
35
|
+
my.command = "echo"
|
|
36
|
+
|
|
37
|
+
# Use the ? accessor to check which steps ran
|
|
38
|
+
my.args << if cmd?(:process_even)
|
|
39
|
+
"Even path executed"
|
|
40
|
+
elsif cmd?(:process_odd)
|
|
41
|
+
"Odd path executed"
|
|
42
|
+
else
|
|
43
|
+
"Neither path executed (unexpected!)"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Demonstrate using non-bang accessor
|
|
48
|
+
ruby do
|
|
49
|
+
puts "\n" + "=" * 70
|
|
50
|
+
puts "CONDITIONAL EXECUTION RESULTS"
|
|
51
|
+
puts "=" * 70
|
|
52
|
+
|
|
53
|
+
number = cmd!(:random_number).out.to_i
|
|
54
|
+
puts "Random number was: #{number}"
|
|
55
|
+
|
|
56
|
+
# Non-bang accessor returns nil if cog didn't run
|
|
57
|
+
puts "Even result: #{cmd(:process_even) || "n/a"}"
|
|
58
|
+
puts "Odd result: #{cmd(:process_odd) || "n/a"}"
|
|
59
|
+
|
|
60
|
+
puts "=" * 70 + "\n"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::DSL::Workflow
|
|
5
|
+
|
|
6
|
+
# This workflow demonstrates failure handling with both no_abort_on_failure! and no_fail_on_error!
|
|
7
|
+
# It shows how workflows can continue even when individual cogs fail.
|
|
8
|
+
|
|
9
|
+
config do
|
|
10
|
+
# This chat cog might fail, but the workflow should continue
|
|
11
|
+
chat(:followup) do
|
|
12
|
+
no_abort_on_failure!
|
|
13
|
+
no_display!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# This command might return non-zero, but that's not a failure
|
|
17
|
+
cmd(:grep) do
|
|
18
|
+
no_fail_on_error!
|
|
19
|
+
no_display!
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
execute do
|
|
24
|
+
# Step 1: Ask LLM to make up some data and return it as JSON
|
|
25
|
+
chat(:generate_data) do
|
|
26
|
+
<<~PROMPT
|
|
27
|
+
Make up a simple shopping list with 3-5 items as a JSON object
|
|
28
|
+
`{ items: [ name: ..., quantity: ... ] }`.
|
|
29
|
+
PROMPT
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Step 2: Ask a follow-up question, but fail if the original response didn't include "milk"
|
|
33
|
+
chat(:followup) do
|
|
34
|
+
shopping_list_items = chat!(:generate_data).json![:items].map { |it| it[:name].downcase }
|
|
35
|
+
fail! unless shopping_list_items.include?("milk")
|
|
36
|
+
|
|
37
|
+
<<~PROMPT
|
|
38
|
+
Based on this shopping list:
|
|
39
|
+
#{chat!(:generate_data).text}
|
|
40
|
+
|
|
41
|
+
Suggest a recipe that uses milk from the list.
|
|
42
|
+
PROMPT
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Step 3: Run a command that might fail (grep returns non-zero when no matches)
|
|
46
|
+
cmd(:grep) do |my|
|
|
47
|
+
my.command = "grep -i eggs"
|
|
48
|
+
my.stdin = chat!(:generate_data).text
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Step 4: Print summary results
|
|
52
|
+
ruby do
|
|
53
|
+
puts "\n" + "=" * 70
|
|
54
|
+
puts "WORKFLOW RESULTS"
|
|
55
|
+
puts "=" * 70
|
|
56
|
+
|
|
57
|
+
shopping_list = chat!(:generate_data).json!
|
|
58
|
+
puts "\nGenerated shopping list:"
|
|
59
|
+
shopping_list[:items].each { |it| puts "- #{it[:name]}: #{it[:quantity]}" }
|
|
60
|
+
|
|
61
|
+
if chat?(:followup)
|
|
62
|
+
puts "\n✓ Follow-up succeeded (milk was in the list):"
|
|
63
|
+
puts chat!(:followup).text
|
|
64
|
+
else
|
|
65
|
+
puts "\n✗ Follow-up failed (milk was not in the list)"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if cmd!(:grep).status.exitstatus == 0
|
|
69
|
+
puts "\n✓ Grep found matches:"
|
|
70
|
+
puts cmd!(:grep).text
|
|
71
|
+
else
|
|
72
|
+
puts "\n✗ Grep found no matches"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts "\n" + "=" * 70
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Chapter 6: Reusable Scopes
|
|
2
|
+
|
|
3
|
+
In previous chapters, all your workflows used a single `execute` block. Now you'll learn how to create reusable scopes
|
|
4
|
+
that can be called multiple times with different inputs, making your workflows more modular and maintainable.
|
|
5
|
+
|
|
6
|
+
## What You'll Learn
|
|
7
|
+
|
|
8
|
+
- How to define named execute scopes
|
|
9
|
+
- How to call scopes with the `call` cog
|
|
10
|
+
- How to pass values to scopes
|
|
11
|
+
- How to return values from scopes with `outputs!`
|
|
12
|
+
- How to extract outputs using `from()`
|
|
13
|
+
|
|
14
|
+
## Named Execute Scopes
|
|
15
|
+
|
|
16
|
+
Define a named execute scope by providing a name to `execute`. Just like the top-level execute scope, you can define
|
|
17
|
+
however many steps you want, and they'll run in order, top-to-bottom. Cogs can access the outputs of previous
|
|
18
|
+
cog's in their own scope, but cannot access the output of cogs defined in other scopes.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
execute(:process_file) do
|
|
22
|
+
cmd(:word_count) { "wc -w #{filename}" }
|
|
23
|
+
ruby { puts "Words: #{cmd!(:word_count).text}" }
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
You define all execute scopes at the top level of your workflow file. But, you can define them in any order.
|
|
27
|
+
Named scopes don't run automatically. They must be called explicitly using a cog like the `call` cog.
|
|
28
|
+
|
|
29
|
+
## The call Cog
|
|
30
|
+
|
|
31
|
+
Use the `call` cog to invoke a named scope:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
execute(:greet) do
|
|
35
|
+
chat { "say 'Hello, World!'" }
|
|
36
|
+
chat { "Tell me a funny joke" }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
execute do
|
|
40
|
+
call(run: :greet) # Invokes the :greet scope
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can call the same scope multiple times:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
execute do
|
|
48
|
+
call(run: :greet)
|
|
49
|
+
call(run: :greet)
|
|
50
|
+
call(run: :greet)
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Passing Values to Scopes
|
|
55
|
+
|
|
56
|
+
Pass values to a called scope, and access them as the second block parameter in any cog's input block:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
execute(:greet_person) do
|
|
60
|
+
chat do |my, name|
|
|
61
|
+
my.prompt = "Say hello to #{name}!"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
execute do
|
|
66
|
+
call(run: :greet_person) { "Alice" }
|
|
67
|
+
call(run: :greet_person) { "Bob" }
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The value from the `call` cog becomes available as the second parameter to any cog in the called scope.
|
|
72
|
+
|
|
73
|
+
## Returning Values with outputs!
|
|
74
|
+
|
|
75
|
+
Use `outputs!` to specify what a scope returns:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
execute(:double_number) do
|
|
79
|
+
ruby(:calculate) { |_, number| number * 2 }
|
|
80
|
+
|
|
81
|
+
outputs! { ruby!(:calculate).value }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
execute do
|
|
85
|
+
call(:result, run: :double_number) { 21 }
|
|
86
|
+
|
|
87
|
+
ruby do
|
|
88
|
+
answer = from(call!(:result))
|
|
89
|
+
puts "The answer is: #{answer}" # => 42
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The `from()` helper extracts the final output from a called scope.
|
|
95
|
+
|
|
96
|
+
## Extracting output values from specific cogs
|
|
97
|
+
|
|
98
|
+
The `outputs!` block is optional. In its absence, the return value will be the output value of the final cog
|
|
99
|
+
in the scope. You can also pass a block to `from` and access the output of any cog(s) you want from the scope.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
execute(:number_math) do
|
|
103
|
+
ruby(:add_two) { |_, number| number + 2 }
|
|
104
|
+
ruby(:subtract_two) { |_, number| number - 2 }
|
|
105
|
+
ruby(:multiply_by_two) { |_, number| number * 2 }
|
|
106
|
+
ruby(:divide_by_two) { |_, number| number / 2 }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
execute do
|
|
110
|
+
# the return value of the block given to `call` will be the value passed to the scope being run,
|
|
111
|
+
# but you can also set the value explicitly
|
|
112
|
+
call(:result, run: :number_math) { |my| my.value = 28 }
|
|
113
|
+
|
|
114
|
+
ruby do
|
|
115
|
+
answer = from(call!(:result)).value # this yields the output of the final cog in :number_math: ruby!(:divide_by_two)
|
|
116
|
+
puts "The final answer is: #{answer}" # => 14
|
|
117
|
+
|
|
118
|
+
# pass a block to `from` to access the output of other cogs from the scope that was run
|
|
119
|
+
# this block runs in the context of the other scope and can access any cogs defined in it
|
|
120
|
+
subtraction, multiplication = from(call!(:result)) do
|
|
121
|
+
[
|
|
122
|
+
ruby!(:subtract_two).value,
|
|
123
|
+
ruby!(:multiply_by_two).value
|
|
124
|
+
]
|
|
125
|
+
end
|
|
126
|
+
puts "Some intermediate answers are: #{subtraction} (subtraction) and #{multiplication} (multiplication)"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Running the Workflows
|
|
132
|
+
|
|
133
|
+
To run the examples in this chapter:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Basic scope example
|
|
137
|
+
bin/roast execute --executor=dsl dsl/tutorial/06_reusable_scopes/basic_scope.rb
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Parameterized scope example
|
|
142
|
+
bin/roast execute --executor=dsl dsl/tutorial/06_reusable_scopes/parameterized_scope.rb
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Accessing specific scope outputs
|
|
147
|
+
bin/roast execute --executor=dsl dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Try It Yourself
|
|
151
|
+
|
|
152
|
+
1. **Create reusable logic** - Extract common operations into named scopes
|
|
153
|
+
2. **Parameterize scopes** - Pass different values to the same scope
|
|
154
|
+
3. **Return values** - Use `outputs!` to capture 'default' result values from scopes
|
|
155
|
+
4. **Chain scopes** - Call scopes from within other scopes
|
|
156
|
+
|
|
157
|
+
## Key Takeaways
|
|
158
|
+
|
|
159
|
+
- Use `execute(:name) do ... end` to define reusable scopes
|
|
160
|
+
- Use `call(run: :scope_name)` to invoke named scopes
|
|
161
|
+
- Pass values to scopes using the block parameter of `call`
|
|
162
|
+
- Access passed values as the second parameter in cog input blocks
|
|
163
|
+
- Use `outputs!` to specify what a scope returns
|
|
164
|
+
- Use `from(call!(:name))` to extract the return value
|
|
165
|
+
- Named scopes don't run unless explicitly called
|
|
166
|
+
|
|
167
|
+
## What's Next?
|
|
168
|
+
|
|
169
|
+
In the next chapter, you'll learn about `map`—a powerful way to apply a scope to every item in a collection, enabling
|
|
170
|
+
batch processing and parallel execution.
|
|
171
|
+
|
|
172
|
+
But first, experiment with reusable scopes to make your workflows more modular!
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::DSL::Workflow
|
|
5
|
+
|
|
6
|
+
# This workflow demonstrates accessing specific cog outputs from a called scope.
|
|
7
|
+
# It shows how to use from() with a block to extract outputs from multiple cogs,
|
|
8
|
+
# and how the default return value is the final cog's output when no outputs! block is provided.
|
|
9
|
+
|
|
10
|
+
config do
|
|
11
|
+
chat do
|
|
12
|
+
model "gpt-4o-mini"
|
|
13
|
+
no_show_response!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
chat(:final_summary) do
|
|
17
|
+
show_response!
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Define a scope that processes text through multiple transformations
|
|
22
|
+
# Note: No outputs! block, so the default return is the last cog's output
|
|
23
|
+
execute(:process_text) do
|
|
24
|
+
chat(:extract_keywords) do |_, text|
|
|
25
|
+
<<~PROMPT
|
|
26
|
+
Extract 3-5 key words or phrases from this text, in JSON form: `{ keywords: [ ... ] }`
|
|
27
|
+
#{text}
|
|
28
|
+
PROMPT
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
chat(:determine_sentiment) do |_, text|
|
|
32
|
+
<<~PROMPT
|
|
33
|
+
What is the sentiment of this text? Answer with just one word: positive, negative, or neutral.
|
|
34
|
+
#{text}
|
|
35
|
+
PROMPT
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
chat(:generate_title) do |_, text|
|
|
39
|
+
<<~PROMPT
|
|
40
|
+
Generate a short, catchy title (5-7 words) for this text:
|
|
41
|
+
#{text}
|
|
42
|
+
PROMPT
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
chat(:word_count) do |_, text|
|
|
46
|
+
"Count the approximate number of words in this text and respond with just the number: #{text}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Main workflow
|
|
51
|
+
execute do
|
|
52
|
+
ruby do
|
|
53
|
+
puts "=" * 70
|
|
54
|
+
puts "ACCESSING SCOPE OUTPUTS EXAMPLE"
|
|
55
|
+
puts "=" * 70
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Process a sample text
|
|
59
|
+
call(:result, run: :process_text) do
|
|
60
|
+
<<~TEXT
|
|
61
|
+
Artificial intelligence is revolutionizing software development.
|
|
62
|
+
Machine learning models can now write code, detect bugs, and
|
|
63
|
+
suggest improvements. This technology empowers developers to
|
|
64
|
+
build better software faster than ever before.
|
|
65
|
+
TEXT
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The default return value (without outputs!) is the last cog's output
|
|
69
|
+
ruby do
|
|
70
|
+
puts "\nDEFAULT RETURN VALUE (last cog in scope):"
|
|
71
|
+
word_count = from(call!(:result)).text
|
|
72
|
+
puts "Word count: #{word_count}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Use from() with a block to access specific cogs from the scope
|
|
76
|
+
ruby do
|
|
77
|
+
puts "\nACCESSING SPECIFIC COGS:"
|
|
78
|
+
|
|
79
|
+
# The block runs in the context of the called scope
|
|
80
|
+
# You can access any cogs defined in that scope
|
|
81
|
+
keywords, sentiment, title = from(call!(:result)) do
|
|
82
|
+
[
|
|
83
|
+
chat!(:extract_keywords).json![:keywords],
|
|
84
|
+
chat!(:determine_sentiment).text,
|
|
85
|
+
chat!(:generate_title).text,
|
|
86
|
+
]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
puts "Keywords: #{keywords.join(", ")}"
|
|
90
|
+
puts "Sentiment: #{sentiment}"
|
|
91
|
+
puts "Title: #{title}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Create a final summary combining the extracted information
|
|
95
|
+
chat(:final_summary) do
|
|
96
|
+
keywords, sentiment, title, word_count = from(call!(:result)) do
|
|
97
|
+
[
|
|
98
|
+
chat!(:extract_keywords).json![:keywords],
|
|
99
|
+
chat!(:determine_sentiment).text,
|
|
100
|
+
chat!(:generate_title).text,
|
|
101
|
+
chat!(:word_count).text,
|
|
102
|
+
]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
<<~PROMPT
|
|
106
|
+
Create a brief meta-summary (2-3 sentences) using this analysis:
|
|
107
|
+
|
|
108
|
+
Title: #{title}
|
|
109
|
+
Keywords: #{keywords.join(", ")}
|
|
110
|
+
Sentiment: #{sentiment}
|
|
111
|
+
Word Count: #{word_count}
|
|
112
|
+
PROMPT
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
ruby do
|
|
116
|
+
puts "\n" + "=" * 70
|
|
117
|
+
puts <<~NOTE
|
|
118
|
+
KEY POINTS:
|
|
119
|
+
- Without outputs!, the default return is the last cog's output
|
|
120
|
+
- Use from(call!(:name)) to access the default return value
|
|
121
|
+
- Use from(call!(:name)) { ... } with a block to access specific cogs
|
|
122
|
+
- The block runs in the context of the called scope
|
|
123
|
+
- You can extract outputs from multiple cogs in one call
|
|
124
|
+
NOTE
|
|
125
|
+
end
|
|
126
|
+
end
|