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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +9 -9
- data/.irbrc +6 -0
- data/CHANGELOG.md +140 -0
- data/README.md +263 -48
- data/Rakefile +71 -1
- data/docs/api/core/index.md +53 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +490 -130
- data/docs/api/core/state.md +55 -73
- data/docs/api/core/tool.md +205 -209
- data/docs/api/index.md +7 -28
- data/docs/api/mcp/client.md +119 -48
- data/docs/api/mcp/index.md +75 -60
- data/docs/api/mcp/server.md +120 -136
- data/docs/api/mcp/transports.md +172 -184
- data/docs/api/messages/index.md +35 -20
- data/docs/api/messages/text-message.md +67 -21
- data/docs/api/messages/tool-call-message.md +80 -41
- data/docs/api/messages/tool-result-message.md +119 -50
- data/docs/api/messages/user-message.md +48 -24
- data/docs/api/streaming/context.md +157 -74
- data/docs/api/streaming/events.md +114 -166
- data/docs/api/streaming/index.md +74 -72
- data/docs/architecture/core-concepts.md +360 -116
- data/docs/architecture/index.md +97 -59
- data/docs/architecture/message-flow.md +138 -129
- data/docs/architecture/network-orchestration.md +197 -50
- data/docs/architecture/robot-execution.md +199 -146
- data/docs/architecture/state-management.md +255 -187
- data/docs/concepts.md +311 -49
- data/docs/examples/basic-chat.md +89 -77
- data/docs/examples/index.md +222 -47
- data/docs/examples/mcp-server.md +207 -203
- data/docs/examples/multi-robot-network.md +129 -35
- data/docs/examples/rails-application.md +159 -160
- data/docs/examples/tool-usage.md +295 -204
- data/docs/getting-started/configuration.md +347 -154
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/installation.md +22 -13
- data/docs/getting-started/quick-start.md +166 -121
- data/docs/guides/building-robots.md +418 -212
- data/docs/guides/creating-networks.md +143 -24
- data/docs/guides/index.md +0 -5
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +137 -187
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +46 -41
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +19 -17
- data/examples/04_mcp.rb +5 -8
- data/examples/05_streaming.rb +5 -8
- data/examples/06_prompt_templates.rb +42 -37
- data/examples/07_network_memory.rb +13 -14
- data/examples/08_llm_config.rb +169 -0
- data/examples/09_chaining.rb +262 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +253 -0
- data/examples/12_message_bus.rb +74 -0
- data/examples/13_spawn.rb +90 -0
- data/examples/14_rusty_circuit/comic.rb +143 -0
- data/examples/14_rusty_circuit/display.rb +203 -0
- data/examples/14_rusty_circuit/heckler.rb +63 -0
- data/examples/14_rusty_circuit/open_mic.rb +123 -0
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
- data/examples/14_rusty_circuit/scout.rb +156 -0
- data/examples/14_rusty_circuit/scout_notes.md +89 -0
- data/examples/14_rusty_circuit/show.log +234 -0
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
- data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
- data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
- data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
- data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
- data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
- data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
- data/examples/15_memory_network_and_bus/output/memory.json +13 -0
- data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
- data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
- data/examples/16_writers_room/display.rb +158 -0
- data/examples/16_writers_room/output/.gitignore +2 -0
- data/examples/16_writers_room/output/opus_001.md +263 -0
- data/examples/16_writers_room/output/opus_001_notes.log +470 -0
- data/examples/16_writers_room/prompts/writer.md +37 -0
- data/examples/16_writers_room/room.rb +150 -0
- data/examples/16_writers_room/tools.rb +162 -0
- data/examples/16_writers_room/writer.rb +121 -0
- data/examples/16_writers_room/writers_room.rb +162 -0
- data/examples/README.md +197 -0
- data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
- data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
- data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
- data/examples/prompts/comedian.md +6 -0
- data/examples/prompts/comedy_critic.md +10 -0
- data/examples/prompts/configurable.md +9 -0
- data/examples/prompts/dispatcher.md +12 -0
- data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
- data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
- data/examples/prompts/frontmatter_mcp_test.md +9 -0
- data/examples/prompts/frontmatter_named_test.md +5 -0
- data/examples/prompts/frontmatter_tools_test.md +6 -0
- data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
- data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
- data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
- data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
- data/examples/prompts/llm_config_demo.md +20 -0
- data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
- data/examples/prompts/os_advocate.md +13 -0
- data/examples/prompts/os_chief.md +13 -0
- data/examples/prompts/os_editor.md +13 -0
- data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
- data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
- data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
- data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
- data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
- data/lib/robot_lab/ask_user.rb +75 -0
- data/lib/robot_lab/config/defaults.yml +121 -0
- data/lib/robot_lab/config.rb +183 -0
- data/lib/robot_lab/error.rb +6 -0
- data/lib/robot_lab/mcp/client.rb +1 -1
- data/lib/robot_lab/memory.rb +10 -34
- data/lib/robot_lab/network.rb +13 -20
- data/lib/robot_lab/robot/bus_messaging.rb +239 -0
- data/lib/robot_lab/robot/mcp_management.rb +88 -0
- data/lib/robot_lab/robot/template_rendering.rb +130 -0
- data/lib/robot_lab/robot.rb +240 -330
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/task.rb +8 -1
- data/lib/robot_lab/tool.rb +108 -172
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +2 -18
- data/lib/robot_lab/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +89 -57
- data/mkdocs.yml +0 -11
- metadata +121 -135
- data/docs/api/adapters/anthropic.md +0 -121
- data/docs/api/adapters/gemini.md +0 -133
- data/docs/api/adapters/index.md +0 -104
- data/docs/api/adapters/openai.md +0 -134
- data/docs/api/history/active-record-adapter.md +0 -195
- data/docs/api/history/config.md +0 -191
- data/docs/api/history/index.md +0 -132
- data/docs/api/history/thread-manager.md +0 -144
- data/docs/guides/history.md +0 -359
- data/examples/prompts/assistant/user.txt.erb +0 -1
- data/examples/prompts/billing/user.txt.erb +0 -1
- data/examples/prompts/classifier/user.txt.erb +0 -1
- data/examples/prompts/entity_extractor/user.txt.erb +0 -3
- data/examples/prompts/escalation/user.txt.erb +0 -34
- data/examples/prompts/general/user.txt.erb +0 -1
- data/examples/prompts/github_assistant/user.txt.erb +0 -1
- data/examples/prompts/helper/user.txt.erb +0 -1
- data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
- data/examples/prompts/order_support/user.txt.erb +0 -22
- data/examples/prompts/product_support/user.txt.erb +0 -32
- data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
- data/examples/prompts/synthesizer/user.txt.erb +0 -15
- data/examples/prompts/technical/user.txt.erb +0 -1
- data/examples/prompts/triage/user.txt.erb +0 -17
- data/lib/robot_lab/adapters/anthropic.rb +0 -163
- data/lib/robot_lab/adapters/base.rb +0 -85
- data/lib/robot_lab/adapters/gemini.rb +0 -193
- data/lib/robot_lab/adapters/openai.rb +0 -159
- data/lib/robot_lab/adapters/registry.rb +0 -81
- data/lib/robot_lab/configuration.rb +0 -143
- data/lib/robot_lab/errors.rb +0 -70
- data/lib/robot_lab/history/active_record_adapter.rb +0 -146
- data/lib/robot_lab/history/config.rb +0 -115
- data/lib/robot_lab/history/thread_manager.rb +0 -93
- 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}"
|