robot_lab 0.0.1 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +140 -0
  5. data/README.md +263 -48
  6. data/Rakefile +71 -1
  7. data/docs/api/core/index.md +53 -46
  8. data/docs/api/core/memory.md +200 -154
  9. data/docs/api/core/network.md +13 -3
  10. data/docs/api/core/robot.md +490 -130
  11. data/docs/api/core/state.md +55 -73
  12. data/docs/api/core/tool.md +205 -209
  13. data/docs/api/index.md +7 -28
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/messages/index.md +35 -20
  19. data/docs/api/messages/text-message.md +67 -21
  20. data/docs/api/messages/tool-call-message.md +80 -41
  21. data/docs/api/messages/tool-result-message.md +119 -50
  22. data/docs/api/messages/user-message.md +48 -24
  23. data/docs/api/streaming/context.md +157 -74
  24. data/docs/api/streaming/events.md +114 -166
  25. data/docs/api/streaming/index.md +74 -72
  26. data/docs/architecture/core-concepts.md +360 -116
  27. data/docs/architecture/index.md +97 -59
  28. data/docs/architecture/message-flow.md +138 -129
  29. data/docs/architecture/network-orchestration.md +197 -50
  30. data/docs/architecture/robot-execution.md +199 -146
  31. data/docs/architecture/state-management.md +255 -187
  32. data/docs/concepts.md +311 -49
  33. data/docs/examples/basic-chat.md +89 -77
  34. data/docs/examples/index.md +222 -47
  35. data/docs/examples/mcp-server.md +207 -203
  36. data/docs/examples/multi-robot-network.md +129 -35
  37. data/docs/examples/rails-application.md +159 -160
  38. data/docs/examples/tool-usage.md +295 -204
  39. data/docs/getting-started/configuration.md +347 -154
  40. data/docs/getting-started/index.md +1 -1
  41. data/docs/getting-started/installation.md +22 -13
  42. data/docs/getting-started/quick-start.md +166 -121
  43. data/docs/guides/building-robots.md +418 -212
  44. data/docs/guides/creating-networks.md +143 -24
  45. data/docs/guides/index.md +0 -5
  46. data/docs/guides/mcp-integration.md +152 -113
  47. data/docs/guides/memory.md +220 -164
  48. data/docs/guides/rails-integration.md +244 -162
  49. data/docs/guides/streaming.md +137 -187
  50. data/docs/guides/using-tools.md +259 -212
  51. data/docs/index.md +46 -41
  52. data/examples/01_simple_robot.rb +6 -9
  53. data/examples/02_tools.rb +6 -9
  54. data/examples/03_network.rb +19 -17
  55. data/examples/04_mcp.rb +5 -8
  56. data/examples/05_streaming.rb +5 -8
  57. data/examples/06_prompt_templates.rb +42 -37
  58. data/examples/07_network_memory.rb +13 -14
  59. data/examples/08_llm_config.rb +169 -0
  60. data/examples/09_chaining.rb +262 -0
  61. data/examples/10_memory.rb +331 -0
  62. data/examples/11_network_introspection.rb +253 -0
  63. data/examples/12_message_bus.rb +74 -0
  64. data/examples/13_spawn.rb +90 -0
  65. data/examples/14_rusty_circuit/comic.rb +143 -0
  66. data/examples/14_rusty_circuit/display.rb +203 -0
  67. data/examples/14_rusty_circuit/heckler.rb +63 -0
  68. data/examples/14_rusty_circuit/open_mic.rb +123 -0
  69. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  70. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  71. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  72. data/examples/14_rusty_circuit/scout.rb +156 -0
  73. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  74. data/examples/14_rusty_circuit/show.log +234 -0
  75. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  78. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  79. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  80. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  81. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  82. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  83. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  84. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  85. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  86. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  87. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  88. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  89. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  90. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  91. data/examples/16_writers_room/display.rb +158 -0
  92. data/examples/16_writers_room/output/.gitignore +2 -0
  93. data/examples/16_writers_room/output/opus_001.md +263 -0
  94. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  95. data/examples/16_writers_room/prompts/writer.md +37 -0
  96. data/examples/16_writers_room/room.rb +150 -0
  97. data/examples/16_writers_room/tools.rb +162 -0
  98. data/examples/16_writers_room/writer.rb +121 -0
  99. data/examples/16_writers_room/writers_room.rb +162 -0
  100. data/examples/README.md +197 -0
  101. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  102. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  103. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  104. data/examples/prompts/comedian.md +6 -0
  105. data/examples/prompts/comedy_critic.md +10 -0
  106. data/examples/prompts/configurable.md +9 -0
  107. data/examples/prompts/dispatcher.md +12 -0
  108. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  109. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  110. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  111. data/examples/prompts/frontmatter_named_test.md +5 -0
  112. data/examples/prompts/frontmatter_tools_test.md +6 -0
  113. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  114. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  115. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  116. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  117. data/examples/prompts/llm_config_demo.md +20 -0
  118. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  119. data/examples/prompts/os_advocate.md +13 -0
  120. data/examples/prompts/os_chief.md +13 -0
  121. data/examples/prompts/os_editor.md +13 -0
  122. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  123. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  124. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  125. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  126. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  127. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  128. data/lib/robot_lab/ask_user.rb +75 -0
  129. data/lib/robot_lab/config/defaults.yml +121 -0
  130. data/lib/robot_lab/config.rb +183 -0
  131. data/lib/robot_lab/error.rb +6 -0
  132. data/lib/robot_lab/mcp/client.rb +1 -1
  133. data/lib/robot_lab/memory.rb +10 -34
  134. data/lib/robot_lab/network.rb +13 -20
  135. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  136. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  137. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  138. data/lib/robot_lab/robot.rb +240 -330
  139. data/lib/robot_lab/robot_message.rb +44 -0
  140. data/lib/robot_lab/robot_result.rb +1 -0
  141. data/lib/robot_lab/run_config.rb +184 -0
  142. data/lib/robot_lab/state_proxy.rb +2 -12
  143. data/lib/robot_lab/streaming/context.rb +1 -1
  144. data/lib/robot_lab/task.rb +8 -1
  145. data/lib/robot_lab/tool.rb +108 -172
  146. data/lib/robot_lab/tool_config.rb +1 -1
  147. data/lib/robot_lab/tool_manifest.rb +2 -18
  148. data/lib/robot_lab/utils.rb +39 -0
  149. data/lib/robot_lab/version.rb +1 -1
  150. data/lib/robot_lab.rb +89 -57
  151. data/mkdocs.yml +0 -11
  152. metadata +121 -135
  153. data/docs/api/adapters/anthropic.md +0 -121
  154. data/docs/api/adapters/gemini.md +0 -133
  155. data/docs/api/adapters/index.md +0 -104
  156. data/docs/api/adapters/openai.md +0 -134
  157. data/docs/api/history/active-record-adapter.md +0 -195
  158. data/docs/api/history/config.md +0 -191
  159. data/docs/api/history/index.md +0 -132
  160. data/docs/api/history/thread-manager.md +0 -144
  161. data/docs/guides/history.md +0 -359
  162. data/examples/prompts/assistant/user.txt.erb +0 -1
  163. data/examples/prompts/billing/user.txt.erb +0 -1
  164. data/examples/prompts/classifier/user.txt.erb +0 -1
  165. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  166. data/examples/prompts/escalation/user.txt.erb +0 -34
  167. data/examples/prompts/general/user.txt.erb +0 -1
  168. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  169. data/examples/prompts/helper/user.txt.erb +0 -1
  170. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  171. data/examples/prompts/order_support/user.txt.erb +0 -22
  172. data/examples/prompts/product_support/user.txt.erb +0 -32
  173. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  174. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  175. data/examples/prompts/technical/user.txt.erb +0 -1
  176. data/examples/prompts/triage/user.txt.erb +0 -17
  177. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  178. data/lib/robot_lab/adapters/base.rb +0 -85
  179. data/lib/robot_lab/adapters/gemini.rb +0 -193
  180. data/lib/robot_lab/adapters/openai.rb +0 -159
  181. data/lib/robot_lab/adapters/registry.rb +0 -81
  182. data/lib/robot_lab/configuration.rb +0 -143
  183. data/lib/robot_lab/errors.rb +0 -70
  184. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  185. data/lib/robot_lab/history/config.rb +0 -115
  186. data/lib/robot_lab/history/thread_manager.rb +0 -93
  187. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 10: Advanced Memory Operations
5
+ #
6
+ # Demonstrates the Memory API without making any LLM calls. Covers:
7
+ # - StateProxy for method-style data access
8
+ # - Key subscriptions with MemoryChange objects
9
+ # - Pattern subscriptions
10
+ # - Unsubscribe
11
+ # - Key enumeration (keys, all_keys, key?)
12
+ # - Serialization (to_h, from_hash, to_json) with amazing_print
13
+ # - Serialization round-trip verification with hashdiff
14
+ # - Clone for isolated copies
15
+ # - Delete with reserved key protection
16
+ # - Clear vs reset
17
+ #
18
+ # Usage:
19
+ # bundle exec ruby examples/10_memory.rb
20
+
21
+ # Configure template path before loading
22
+ ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
23
+
24
+ require_relative "../lib/robot_lab"
25
+ require "json"
26
+ require "amazing_print"
27
+ require "hashdiff"
28
+
29
+ puts "=" * 70
30
+ puts "Example 10: Advanced Memory Operations"
31
+ puts "=" * 70
32
+ puts
33
+
34
+ # =============================================================================
35
+ # Section 1: StateProxy for method-style access
36
+ # =============================================================================
37
+
38
+ puts "--- Section 1: StateProxy for Method-Style Access ---"
39
+ puts
40
+
41
+ memory = RobotLab.create_memory(
42
+ data: { category: nil, priority: "low" },
43
+ enable_cache: false
44
+ )
45
+
46
+ puts "Initial data:"
47
+ ap memory.data.to_h
48
+ puts
49
+
50
+ # StateProxy allows method-style access to the :data hash
51
+ memory.data.category = "billing"
52
+
53
+ puts "After memory.data.category = 'billing':"
54
+ ap memory.data.to_h
55
+ puts
56
+
57
+ # Bracket-style also works
58
+ memory.data[:priority] = "high"
59
+ puts "After memory.data[:priority] = 'high':"
60
+ ap memory.data.to_h
61
+ puts
62
+
63
+ # =============================================================================
64
+ # Section 2: Subscriptions and MemoryChange
65
+ # =============================================================================
66
+
67
+ puts "--- Section 2: Subscriptions and MemoryChange ---"
68
+ puts
69
+
70
+ changes = []
71
+
72
+ sub_id = memory.subscribe(:status) do |change|
73
+ changes << change
74
+ end
75
+
76
+ puts "Subscribed to :status (sub_id: #{sub_id[0..7]}...)"
77
+
78
+ # Set current_writer so the MemoryChange knows who wrote
79
+ memory.current_writer = "classifier"
80
+ memory.set(:status, "processing")
81
+
82
+ # Give the async callback time to fire
83
+ sleep 0.1
84
+
85
+ if changes.any?
86
+ change = changes.first
87
+ puts "Change received:"
88
+ ap change.to_h
89
+ puts " created?: #{change.created?}"
90
+ puts " updated?: #{change.updated?}"
91
+ end
92
+
93
+ # Update the same key to see an 'updated' change
94
+ memory.set(:status, "complete")
95
+ sleep 0.1
96
+
97
+ if changes.size > 1
98
+ puts
99
+ puts "Second change (update):"
100
+ ap changes.last.to_h
101
+ puts " created?: #{changes.last.created?}"
102
+ puts " updated?: #{changes.last.updated?}"
103
+ end
104
+ puts
105
+
106
+ # =============================================================================
107
+ # Section 3: Pattern subscriptions
108
+ # =============================================================================
109
+
110
+ puts "--- Section 3: Pattern Subscriptions ---"
111
+ puts
112
+
113
+ pattern_changes = []
114
+
115
+ pattern_sub_id = memory.subscribe_pattern("analysis:*") do |change|
116
+ pattern_changes << change
117
+ end
118
+
119
+ puts "Subscribed to pattern 'analysis:*'"
120
+
121
+ memory.current_writer = "analyst"
122
+ memory.set(:"analysis:sentiment", { score: 0.8 })
123
+ memory.set(:"analysis:entities", ["Ruby", "LLM"])
124
+ memory.set(:unrelated_key, "ignored")
125
+
126
+ sleep 0.1
127
+
128
+ puts "Pattern matched #{pattern_changes.size} changes (expected 2):"
129
+ pattern_changes.each do |change|
130
+ ap({ change.key => change.value })
131
+ end
132
+ puts
133
+
134
+ # =============================================================================
135
+ # Section 4: Unsubscribe
136
+ # =============================================================================
137
+
138
+ puts "--- Section 4: Unsubscribe ---"
139
+ puts
140
+
141
+ count_before = changes.size
142
+ memory.unsubscribe(sub_id)
143
+ puts "Unsubscribed from :status"
144
+
145
+ memory.set(:status, "archived")
146
+ sleep 0.1
147
+
148
+ puts "Changes after unsubscribe: #{changes.size} (was #{count_before}, should be same)"
149
+ puts
150
+
151
+ # =============================================================================
152
+ # Section 5: Key management
153
+ # =============================================================================
154
+
155
+ puts "--- Section 5: Key Management ---"
156
+ puts
157
+
158
+ puts "memory.keys (non-reserved):"
159
+ ap memory.keys
160
+ puts
161
+
162
+ puts "memory.all_keys (includes reserved: data, results, messages, session_id, cache):"
163
+ ap memory.all_keys
164
+ puts
165
+
166
+ puts "memory.key?(:status) = #{memory.key?(:status)}"
167
+ puts "memory.key?(:nope) = #{memory.key?(:nope)}"
168
+ puts
169
+
170
+ # =============================================================================
171
+ # Section 6: Serialization round-trip
172
+ # =============================================================================
173
+
174
+ puts "--- Section 6: Serialization Round-Trip ---"
175
+ puts
176
+
177
+ hash = memory.to_h
178
+ puts "memory.to_h:"
179
+ ap hash
180
+ puts
181
+
182
+ json_str = memory.to_json
183
+ puts "memory.to_json length: #{json_str.length} chars"
184
+ puts
185
+
186
+ # Round-trip via from_hash
187
+ restored = RobotLab::Memory.from_hash(hash)
188
+
189
+ puts "Restored memory from hash:"
190
+ ap restored.to_h
191
+ puts
192
+
193
+ # Use hashdiff to verify the round-trip preserved everything
194
+ diff = Hashdiff.diff(hash, restored.to_h)
195
+ if diff.empty?
196
+ puts "Round-trip verification: PERFECT (no differences)"
197
+ else
198
+ puts "Round-trip differences:"
199
+ diff.each do |change|
200
+ op, path, *values = change
201
+ case op
202
+ when "+"
203
+ puts " + #{path}: #{values.first.inspect}"
204
+ when "-"
205
+ puts " - #{path}: #{values.first.inspect}"
206
+ when "~"
207
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
208
+ end
209
+ end
210
+ end
211
+ puts
212
+
213
+ # =============================================================================
214
+ # Section 7: Clone for isolation
215
+ # =============================================================================
216
+
217
+ puts "--- Section 7: Clone for Isolation ---"
218
+ puts
219
+
220
+ cloned = memory.clone
221
+ cloned.set(:isolated_key, "only in clone")
222
+
223
+ puts "cloned.key?(:isolated_key) = #{cloned.key?(:isolated_key)}"
224
+ puts "memory.key?(:isolated_key) = #{memory.key?(:isolated_key)} (isolated!)"
225
+ puts
226
+
227
+ # Show exactly what differs between clone and original
228
+ diff = Hashdiff.diff(memory.to_h, cloned.to_h)
229
+ puts "Diff (original vs clone):"
230
+ diff.each do |change|
231
+ op, path, *values = change
232
+ case op
233
+ when "+"
234
+ puts " + #{path}: #{values.first.inspect}"
235
+ when "-"
236
+ puts " - #{path}: #{values.first.inspect}"
237
+ when "~"
238
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
239
+ end
240
+ end
241
+ puts
242
+
243
+ # =============================================================================
244
+ # Section 8: Delete and reserved key protection
245
+ # =============================================================================
246
+
247
+ puts "--- Section 8: Delete and Reserved Key Protection ---"
248
+ puts
249
+
250
+ before_delete = memory.to_h
251
+ memory.delete(:status)
252
+
253
+ puts "After memory.delete(:status):"
254
+ diff = Hashdiff.diff(before_delete, memory.to_h)
255
+ diff.each do |change|
256
+ op, path, *values = change
257
+ case op
258
+ when "-"
259
+ puts " - #{path}: #{values.first.inspect}"
260
+ end
261
+ end
262
+ puts
263
+
264
+ begin
265
+ memory.delete(:data)
266
+ rescue ArgumentError => e
267
+ puts "memory.delete(:data) raises: #{e.message}"
268
+ end
269
+ puts
270
+
271
+ # =============================================================================
272
+ # Section 9: Clear vs Reset
273
+ # =============================================================================
274
+
275
+ puts "--- Section 9: Clear vs Reset ---"
276
+ puts
277
+
278
+ memory.set(:temp1, "value1")
279
+ memory.set(:temp2, "value2")
280
+
281
+ puts "Before clear:"
282
+ before_clear = memory.to_h
283
+ ap before_clear
284
+ puts
285
+
286
+ memory.clear
287
+
288
+ puts "After clear (non-reserved keys removed):"
289
+ after_clear = memory.to_h
290
+ ap after_clear
291
+ puts
292
+
293
+ puts "Diff (before clear vs after clear):"
294
+ Hashdiff.diff(before_clear, after_clear).each do |change|
295
+ op, path, *values = change
296
+ case op
297
+ when "+"
298
+ puts " + #{path}: #{values.first.inspect}"
299
+ when "-"
300
+ puts " - #{path}: #{values.first.inspect}"
301
+ when "~"
302
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
303
+ end
304
+ end
305
+ puts
306
+
307
+ before_reset = memory.to_h
308
+ memory.reset
309
+
310
+ puts "After reset (full reset to initial state):"
311
+ after_reset = memory.to_h
312
+ ap after_reset
313
+ puts
314
+
315
+ puts "Diff (before reset vs after reset):"
316
+ Hashdiff.diff(before_reset, after_reset).each do |change|
317
+ op, path, *values = change
318
+ case op
319
+ when "+"
320
+ puts " + #{path}: #{values.first.inspect}"
321
+ when "-"
322
+ puts " - #{path}: #{values.first.inspect}"
323
+ when "~"
324
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
325
+ end
326
+ end
327
+ puts
328
+
329
+ puts "=" * 70
330
+ puts "All sections completed without any LLM calls."
331
+ puts "=" * 70
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 11: Network Visualization & Introspection
5
+ #
6
+ # Demonstrates network inspection and visualization without making any
7
+ # LLM calls. Covers:
8
+ # - to_mermaid() — Mermaid diagram export
9
+ # - to_dot() — Graphviz DOT export
10
+ # - execution_plan() — text execution order
11
+ # - visualize() — ASCII pipeline visualization
12
+ # - robot(name) / [name] — access individual robots
13
+ # - available_robots() — list all robots
14
+ # - add_robot() — dynamically add a robot
15
+ # - to_h() — network introspection hash (via amazing_print)
16
+ # - Task-specific config (context:, depends_on:)
17
+ # - broadcast() and on_broadcast
18
+ #
19
+ # Usage:
20
+ # bundle exec ruby examples/11_network_introspection.rb
21
+
22
+ # Configure template path before loading
23
+ ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
24
+
25
+ require_relative "../lib/robot_lab"
26
+ require "amazing_print"
27
+ require "tempfile"
28
+
29
+ # On macOS + iTerm2, render DOT as a PNG and display inline via imgcat.
30
+ # Returns true if the image was displayed, false otherwise.
31
+ def render_dot_image(dot_source)
32
+ return false unless RUBY_PLATFORM.include?("darwin")
33
+
34
+ dot_cmd = `which dot 2>/dev/null`.chomp
35
+ imgcat = File.expand_path("~/.iterm2/imgcat")
36
+ imgcat = `which imgcat 2>/dev/null`.chomp unless File.executable?(imgcat)
37
+
38
+ return false unless File.executable?(dot_cmd) && File.executable?(imgcat)
39
+ return false unless ENV["TERM_PROGRAM"] == "iTerm.app"
40
+
41
+ Tempfile.create(["pipeline", ".png"]) do |png|
42
+ IO.popen([dot_cmd, "-Tpng", "-o", png.path], "w") { |io| io.write(dot_source) }
43
+
44
+ if $?.success? && File.size(png.path) > 0
45
+ system(imgcat, png.path)
46
+ true
47
+ else
48
+ false
49
+ end
50
+ end
51
+ end
52
+
53
+ puts "=" * 70
54
+ puts "Example 11: Network Visualization & Introspection"
55
+ puts "=" * 70
56
+ puts
57
+
58
+ # Shared RunConfig for all robots in this network
59
+ shared_config = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
60
+
61
+ # Per-task RunConfig override for the writer (higher creativity)
62
+ creative_config = RobotLab::RunConfig.new(temperature: 0.9)
63
+
64
+ # Build robots (no LLM calls, just instances)
65
+ classifier = RobotLab.build(name: "classifier", system_prompt: "Classify input")
66
+ analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data")
67
+ writer = RobotLab.build(name: "writer", system_prompt: "Write summary")
68
+
69
+ # Build network with RunConfig, dependencies, and per-task config
70
+ network = RobotLab.create_network(name: "demo_pipeline", config: shared_config) do
71
+ task :classify, classifier, depends_on: :none
72
+ task :analyze, analyst, context: { depth: "deep" }, depends_on: [:classify]
73
+ task :write, writer, config: creative_config, depends_on: [:analyze]
74
+ end
75
+
76
+ # =============================================================================
77
+ # Section 1: Visualization outputs
78
+ # =============================================================================
79
+
80
+ puts "--- Section 1: Visualization ---"
81
+ puts
82
+
83
+ mermaid = network.to_mermaid
84
+ if mermaid
85
+ puts "Mermaid diagram:"
86
+ puts mermaid
87
+ puts
88
+ end
89
+
90
+ dot = network.to_dot
91
+ if dot
92
+ puts "Graphviz DOT:"
93
+ puts dot
94
+ puts
95
+
96
+ if render_dot_image(dot)
97
+ puts "(Rendered pipeline graph above via Graphviz + imgcat)"
98
+ puts
99
+ end
100
+ end
101
+
102
+ plan = network.execution_plan
103
+ if plan
104
+ puts "Execution plan:"
105
+ puts plan
106
+ puts
107
+ end
108
+
109
+ ascii = network.visualize
110
+ if ascii
111
+ puts "ASCII visualization:"
112
+ puts ascii
113
+ puts
114
+ end
115
+
116
+ # If none of the visualization methods returned output, note it
117
+ unless mermaid || dot || plan || ascii
118
+ puts "(Visualization methods returned nil — depends on simple_flow version)"
119
+ puts
120
+ end
121
+
122
+ # =============================================================================
123
+ # Section 2: Robot access
124
+ # =============================================================================
125
+
126
+ puts "--- Section 2: Robot Access ---"
127
+ puts
128
+
129
+ # Access by task name with robot() method
130
+ # Note: robots are keyed by task name (the first arg to task()), not robot.name
131
+ puts "network.robot('classify').name = #{network.robot('classify').name.inspect}"
132
+
133
+ # Access with [] shorthand
134
+ puts "network['analyze'].name = #{network['analyze'].name.inspect}"
135
+
136
+ # Also works with symbols
137
+ puts "network[:write].name = #{network[:write].name.inspect}"
138
+ puts
139
+
140
+ # List all robots
141
+ puts "available_robots:"
142
+ ap network.available_robots.map(&:name)
143
+ puts
144
+
145
+ # =============================================================================
146
+ # Section 3: Dynamic robot addition
147
+ # =============================================================================
148
+
149
+ puts "--- Section 3: Dynamic Robot Addition ---"
150
+ puts
151
+
152
+ reviewer = RobotLab.build(name: "reviewer", system_prompt: "Review output")
153
+ network.add_robot(reviewer)
154
+
155
+ puts "After add_robot(reviewer):"
156
+ ap network.available_robots.map(&:name)
157
+ puts
158
+
159
+ # Attempting to add a duplicate raises an error
160
+ begin
161
+ network.add_robot(reviewer)
162
+ rescue ArgumentError => e
163
+ puts "Duplicate add_robot raises: #{e.message}"
164
+ end
165
+ puts
166
+
167
+ # =============================================================================
168
+ # Section 4: Network introspection
169
+ # =============================================================================
170
+
171
+ puts "--- Section 4: Network Introspection ---"
172
+ puts
173
+
174
+ puts "network.to_h:"
175
+ ap network.to_h
176
+ puts
177
+
178
+ # Individual robot introspection
179
+ puts "network['classify'].to_h:"
180
+ ap network["classify"].to_h
181
+ puts
182
+
183
+ puts "network['analyze'].to_h:"
184
+ ap network["analyze"].to_h
185
+ puts
186
+
187
+ # =============================================================================
188
+ # Section 5: RunConfig Introspection
189
+ # =============================================================================
190
+
191
+ puts "--- Section 5: RunConfig Introspection ---"
192
+ puts
193
+
194
+ puts "Network RunConfig (shared defaults):"
195
+ ap network.config.to_h
196
+ puts
197
+
198
+ puts "Merged effective config for :write task (network + task override):"
199
+ effective = shared_config.merge(creative_config)
200
+ ap effective.to_h
201
+ puts " model inherited from network, temperature overridden by task"
202
+ puts
203
+
204
+ # =============================================================================
205
+ # Section 6: Broadcast
206
+ # =============================================================================
207
+
208
+ puts "--- Section 6: Broadcast ---"
209
+ puts
210
+
211
+ broadcast_messages = []
212
+
213
+ network.on_broadcast do |msg|
214
+ broadcast_messages << msg
215
+ end
216
+
217
+ puts "Registered broadcast handler"
218
+
219
+ network.broadcast(event: :demo, message: "Hello from network!")
220
+
221
+ # Give async handler time to fire
222
+ sleep 0.1
223
+
224
+ if broadcast_messages.any?
225
+ puts "Broadcast received:"
226
+ ap broadcast_messages.first
227
+ else
228
+ puts "(No broadcast received — handler may be async)"
229
+ end
230
+ puts
231
+
232
+ # =============================================================================
233
+ # Section 7: Shared memory access
234
+ # =============================================================================
235
+
236
+ puts "--- Section 7: Shared Network Memory ---"
237
+ puts
238
+
239
+ puts "network.memory is a #{network.memory.class}"
240
+ puts "network.memory.network_name = #{network.memory.network_name.inspect}"
241
+
242
+ # Robots in a network share this memory during run()
243
+ network.memory.set(:demo_key, "shared value")
244
+ puts "network.memory.get(:demo_key) = #{network.memory.get(:demo_key).inspect}"
245
+ puts
246
+
247
+ puts "network.memory.to_h:"
248
+ ap network.memory.to_h
249
+ puts
250
+
251
+ puts "=" * 70
252
+ puts "All sections completed without any LLM calls."
253
+ puts "=" * 70
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 12: Message Bus — Converging on a Funny Robot Joke
5
+ #
6
+ # Alice tasks Bob to tell a robot joke. Alice uses her LLM to
7
+ # evaluate each joke. If she's not impressed she asks Bob to try
8
+ # again. The loop continues until Alice approves or MAX_ATTEMPTS.
9
+ #
10
+ # Usage:
11
+ # bundle exec ruby examples/12_message_bus.rb
12
+
13
+ ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
14
+
15
+ require_relative "../lib/robot_lab"
16
+
17
+ RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
18
+
19
+ MAX_ATTEMPTS = 5
20
+
21
+ class Comedian < RobotLab::Robot
22
+ TEMP_START = 0.2
23
+ TEMP_STEP = 0.2
24
+
25
+ def initialize(bus:)
26
+ super(name: "bob", template: :comedian, bus: bus, temperature: TEMP_START)
27
+ @attempts = 0
28
+ on_message do |message|
29
+ @attempts += 1
30
+ temp = [TEMP_START + TEMP_STEP * (@attempts - 1), 1.0].min
31
+ with_temperature(temp)
32
+ joke = run(message.content.to_s).reply.strip
33
+ puts " Bob [##{@attempts}, t=#{"%.1f" % temp}]: #{joke}"
34
+ send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
35
+ end
36
+ end
37
+
38
+ attr_reader :attempts
39
+ end
40
+
41
+ class ComedyCritic < RobotLab::Robot
42
+ def initialize(bus:)
43
+ super(name: "alice", template: :comedy_critic, bus: bus)
44
+ @accepted = false
45
+ @rounds = 0
46
+ on_message do |message|
47
+ @rounds += 1
48
+ verdict = run("Evaluate this joke:\n\n#{message.content}").reply.strip
49
+ puts " Alice: #{verdict}"
50
+ puts
51
+ @accepted = verdict.start_with?("FUNNY")
52
+ send_message(to: :bob, content: "Not funny enough. Try again.") unless @accepted || @rounds >= MAX_ATTEMPTS
53
+ end
54
+ end
55
+
56
+ attr_reader :accepted
57
+ end
58
+
59
+ bus = TypedBus::MessageBus.new
60
+ bob = Comedian.new(bus: bus)
61
+ alice = ComedyCritic.new(bus: bus)
62
+
63
+ puts "=" * 60
64
+ puts "Example 12: Tell Me a Funny Robot Joke"
65
+ puts "=" * 60
66
+ puts
67
+
68
+ puts "Alice: Tell me a funny robot joke."
69
+ puts
70
+ alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
71
+
72
+ puts "-" * 60
73
+ puts "Attempts: #{bob.attempts} / #{MAX_ATTEMPTS}"
74
+ puts "Accepted: #{alice.accepted}"