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,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ── Writers' Room Tools ──────────────────────────────────────
|
|
4
|
+
#
|
|
5
|
+
# General-purpose tools that give each writer agency over
|
|
6
|
+
# communication, shared memory, and team composition.
|
|
7
|
+
# No workflow logic — the LLMs decide when and how to use them.
|
|
8
|
+
|
|
9
|
+
# Helper to access the room logger from any tool
|
|
10
|
+
module ToolLogging
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def log
|
|
14
|
+
robot.room&.logger
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BroadcastTool < RobotLab::Tool
|
|
20
|
+
include ToolLogging
|
|
21
|
+
|
|
22
|
+
description "Send a message to all writers in the room. " \
|
|
23
|
+
"Use for discussion, proposals, questions, or announcements. " \
|
|
24
|
+
"Don't broadcast trivially — only when you have something substantive."
|
|
25
|
+
|
|
26
|
+
param :message, type: "string",
|
|
27
|
+
desc: "What to say to the room", required: true
|
|
28
|
+
|
|
29
|
+
def execute(message:)
|
|
30
|
+
log&.info("#{robot.name} TOOL broadcast (#{message.length} chars)")
|
|
31
|
+
robot.send_message(to: :room, content: message)
|
|
32
|
+
robot.display&.broadcast(robot.name, message)
|
|
33
|
+
"Broadcast sent."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DirectMessageTool < RobotLab::Tool
|
|
39
|
+
include ToolLogging
|
|
40
|
+
|
|
41
|
+
description "Send a private message to one specific writer. " \
|
|
42
|
+
"Use for feedback on their chapter, coordination on handoffs, " \
|
|
43
|
+
"or questions that don't concern the whole room."
|
|
44
|
+
|
|
45
|
+
param :to, type: "string",
|
|
46
|
+
desc: "Name of the writer to message", required: true
|
|
47
|
+
param :message, type: "string",
|
|
48
|
+
desc: "What to say", required: true
|
|
49
|
+
|
|
50
|
+
def execute(to:, message:)
|
|
51
|
+
log&.info("#{robot.name} TOOL direct_message -> #{to} (#{message.length} chars)")
|
|
52
|
+
robot.send_message(to: to.to_sym, content: message)
|
|
53
|
+
robot.display&.direct_message(robot.name, to, message)
|
|
54
|
+
"Message sent to #{to}."
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ReadMemoryTool < RobotLab::Tool
|
|
60
|
+
include ToolLogging
|
|
61
|
+
|
|
62
|
+
description "Read a value from shared memory. " \
|
|
63
|
+
"Use to check the story bible, outline, chapter claims, " \
|
|
64
|
+
"or read another writer's chapter draft."
|
|
65
|
+
|
|
66
|
+
param :key, type: "string",
|
|
67
|
+
desc: "Memory key to read (e.g. story_bible, outline, claims, chapter_3)", required: true
|
|
68
|
+
|
|
69
|
+
def execute(key:)
|
|
70
|
+
value = robot.shared_memory.get(key.to_sym)
|
|
71
|
+
if value.nil?
|
|
72
|
+
log&.info("#{robot.name} TOOL read_memory :#{key} -> nil")
|
|
73
|
+
"Key '#{key}' is not set yet."
|
|
74
|
+
else
|
|
75
|
+
log&.info("#{robot.name} TOOL read_memory :#{key} -> #{value.to_s.length} chars")
|
|
76
|
+
value.to_s
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class WriteMemoryTool < RobotLab::Tool
|
|
83
|
+
include ToolLogging
|
|
84
|
+
|
|
85
|
+
description "Write a value to shared memory for all writers to see. " \
|
|
86
|
+
"Use to store the story bible, outline, claim a chapter, " \
|
|
87
|
+
"or submit a finished chapter draft."
|
|
88
|
+
|
|
89
|
+
param :key, type: "string",
|
|
90
|
+
desc: "Memory key (e.g. story_bible, outline, claims, chapter_1)", required: true
|
|
91
|
+
param :value, type: "string",
|
|
92
|
+
desc: "Content to store", required: true
|
|
93
|
+
|
|
94
|
+
def execute(key:, value:)
|
|
95
|
+
log&.info("#{robot.name} TOOL write_memory :#{key} (#{value.length} chars)")
|
|
96
|
+
robot.shared_memory.set(key.to_sym, value)
|
|
97
|
+
robot.display&.memory_write(robot.name, key)
|
|
98
|
+
"Stored '#{key}' in shared memory."
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ListMemoryTool < RobotLab::Tool
|
|
104
|
+
include ToolLogging
|
|
105
|
+
|
|
106
|
+
description "List all keys currently in shared memory. " \
|
|
107
|
+
"Use to get situational awareness of what's been done."
|
|
108
|
+
|
|
109
|
+
def execute
|
|
110
|
+
keys = robot.shared_memory.keys
|
|
111
|
+
log&.info("#{robot.name} TOOL list_memory -> #{keys.join(', ')}")
|
|
112
|
+
if keys.empty?
|
|
113
|
+
"Shared memory is empty."
|
|
114
|
+
else
|
|
115
|
+
"Keys in shared memory: #{keys.join(', ')}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SpawnWriterTool < RobotLab::Tool
|
|
122
|
+
include ToolLogging
|
|
123
|
+
|
|
124
|
+
description "Bring a new writer into the room to help with the workload. " \
|
|
125
|
+
"Use when there are more unclaimed chapters than active writers."
|
|
126
|
+
|
|
127
|
+
param :name, type: "string",
|
|
128
|
+
desc: "Name for the new writer (e.g. writer_4)", required: true
|
|
129
|
+
|
|
130
|
+
def execute(name:)
|
|
131
|
+
log&.info("#{robot.name} TOOL spawn_writer '#{name}'")
|
|
132
|
+
new_writer = robot.room.spawn_writer(name)
|
|
133
|
+
robot.display&.spawn(robot.name, name)
|
|
134
|
+
"#{name} has joined the writers' room."
|
|
135
|
+
rescue => e
|
|
136
|
+
log&.error("#{robot.name} TOOL spawn_writer '#{name}' FAILED: #{e.message}")
|
|
137
|
+
"Failed to spawn #{name}: #{e.message}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class MarkCompleteTool < RobotLab::Tool
|
|
143
|
+
include ToolLogging
|
|
144
|
+
|
|
145
|
+
description "Signal that the book is finished — all 10 chapters are written. " \
|
|
146
|
+
"Only use this when you have verified all chapters exist in shared memory."
|
|
147
|
+
|
|
148
|
+
def execute
|
|
149
|
+
# Verify all chapters exist
|
|
150
|
+
missing = (1..10).reject { |n| robot.shared_memory.key?(:"chapter_#{n}") }
|
|
151
|
+
|
|
152
|
+
if missing.any?
|
|
153
|
+
log&.warn("#{robot.name} TOOL mark_complete REJECTED — missing chapters: #{missing.join(', ')}")
|
|
154
|
+
"Cannot mark complete. Missing chapters: #{missing.join(', ')}"
|
|
155
|
+
else
|
|
156
|
+
log&.info("#{robot.name} TOOL mark_complete SUCCESS")
|
|
157
|
+
robot.shared_memory.set(:book_complete, true)
|
|
158
|
+
robot.display&.complete(robot.name)
|
|
159
|
+
"Book marked as complete!"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ── The Writer ─────────────────────────────────────────────────
|
|
4
|
+
#
|
|
5
|
+
# All writers in the room are instances of this same class with
|
|
6
|
+
# the same template and the same tools. There is no hierarchy,
|
|
7
|
+
# no designated leader, no pre-assigned roles. The group
|
|
8
|
+
# self-organizes through bus communication and shared memory.
|
|
9
|
+
#
|
|
10
|
+
# Each writer:
|
|
11
|
+
# - Subscribes to :room for broadcast discussion
|
|
12
|
+
# - Listens on a personal channel for direct messages
|
|
13
|
+
# - Has tools to read/write shared memory, broadcast, DM,
|
|
14
|
+
# spawn new writers, and mark the book complete
|
|
15
|
+
# - Decides via LLM when to speak, when to write, when to
|
|
16
|
+
# listen, and when to spawn
|
|
17
|
+
#
|
|
18
|
+
# == Chat Reset Strategy
|
|
19
|
+
#
|
|
20
|
+
# In a bus-based SOG, writers receive many messages and each
|
|
21
|
+
# triggers a run() call. When the LLM responds with only tool
|
|
22
|
+
# calls (no text), RubyLLM appends an empty text content block
|
|
23
|
+
# to the chat history. The Anthropic API rejects this on the
|
|
24
|
+
# next call, permanently killing the writer.
|
|
25
|
+
#
|
|
26
|
+
# Fix: reset the chat before each message. The writer doesn't
|
|
27
|
+
# need persistent chat history — shared memory is the single
|
|
28
|
+
# source of truth. Each message is processed with a fresh chat
|
|
29
|
+
# that has the system prompt, tools, and current memory context.
|
|
30
|
+
#
|
|
31
|
+
class Writer < RobotLab::Robot
|
|
32
|
+
attr_accessor :shared_memory, :display, :room
|
|
33
|
+
attr_reader :messages_processed
|
|
34
|
+
|
|
35
|
+
def initialize(name:, bus:, shared_memory:, display:, room:, config: nil)
|
|
36
|
+
@shared_memory = shared_memory
|
|
37
|
+
@display = display
|
|
38
|
+
@room = room
|
|
39
|
+
@messages_processed = 0
|
|
40
|
+
|
|
41
|
+
super(
|
|
42
|
+
name: name,
|
|
43
|
+
template: :writer,
|
|
44
|
+
context: { writer_name: name },
|
|
45
|
+
bus: bus,
|
|
46
|
+
config: config,
|
|
47
|
+
local_tools: build_tools
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
setup_room_subscription
|
|
51
|
+
setup_message_handler
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def log
|
|
57
|
+
@room&.logger
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_tools
|
|
61
|
+
[
|
|
62
|
+
BroadcastTool.new(robot: self),
|
|
63
|
+
DirectMessageTool.new(robot: self),
|
|
64
|
+
ReadMemoryTool.new(robot: self),
|
|
65
|
+
WriteMemoryTool.new(robot: self),
|
|
66
|
+
ListMemoryTool.new(robot: self),
|
|
67
|
+
SpawnWriterTool.new(robot: self),
|
|
68
|
+
MarkCompleteTool.new(robot: self),
|
|
69
|
+
]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def setup_room_subscription
|
|
73
|
+
@bus.subscribe(:room) do |delivery|
|
|
74
|
+
handle_incoming_delivery(delivery)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Reset the chat to a clean state with system prompt and tools.
|
|
79
|
+
# Prevents history corruption from tool-only LLM responses
|
|
80
|
+
# (empty text content blocks that Anthropic rejects).
|
|
81
|
+
def fresh_chat!
|
|
82
|
+
resolved_model = @config&.model || RobotLab.config.ruby_llm.model
|
|
83
|
+
@chat = RubyLLM.chat(model: resolved_model)
|
|
84
|
+
apply_template_to_chat(@build_context) if @template
|
|
85
|
+
@chat.with_instructions(@system_prompt) if @system_prompt
|
|
86
|
+
@chat.with_temperature(@config.temperature) if @config&.temperature
|
|
87
|
+
|
|
88
|
+
filtered = filtered_tools([])
|
|
89
|
+
@chat.with_tools(*filtered) if filtered.any?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def setup_message_handler
|
|
93
|
+
on_message do |message|
|
|
94
|
+
# Don't respond to your own messages
|
|
95
|
+
next if message.from == name
|
|
96
|
+
|
|
97
|
+
@messages_processed += 1
|
|
98
|
+
log&.info("#{name} <- [#{message.from}] msg ##{@messages_processed} (#{message.content.to_s[0..80]}...)")
|
|
99
|
+
@display&.incoming(name, message.from, message.content)
|
|
100
|
+
|
|
101
|
+
# Fresh chat for each message — shared memory is our persistence
|
|
102
|
+
fresh_chat!
|
|
103
|
+
|
|
104
|
+
# Build prompt with current memory context
|
|
105
|
+
memory_keys = shared_memory.keys
|
|
106
|
+
prompt = "[#{message.from}]: #{message.content}"
|
|
107
|
+
prompt += "\n\n[Memory keys: #{memory_keys.join(', ')}]" if memory_keys.any?
|
|
108
|
+
|
|
109
|
+
log&.info("#{name} -> run() starting (prompt: #{prompt.length} chars)")
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
result = run(prompt)
|
|
113
|
+
reply_text = result.respond_to?(:reply) ? result.reply.to_s[0..120] : result.to_s[0..120]
|
|
114
|
+
log&.info("#{name} <- run() finished (reply: #{reply_text}...)")
|
|
115
|
+
rescue => e
|
|
116
|
+
log&.error("#{name} !! run() raised #{e.class}: #{e.message}")
|
|
117
|
+
log&.error(" #{e.backtrace&.first(5)&.join("\n ")}")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 16: The Writers' Room — Self-Organizing Group
|
|
5
|
+
#
|
|
6
|
+
# A team of writer robots collaborates to produce a 10-chapter
|
|
7
|
+
# fiction novella. No orchestration, no pipeline, no assigned roles.
|
|
8
|
+
# The writers self-organize through:
|
|
9
|
+
#
|
|
10
|
+
# - BUS (broadcast + direct messages)
|
|
11
|
+
# :room channel for group discussion
|
|
12
|
+
# personal channels for 1:1 feedback
|
|
13
|
+
#
|
|
14
|
+
# - SHARED MEMORY (story bible, outline, chapters)
|
|
15
|
+
# Writers read and write freely; memory is the shared truth
|
|
16
|
+
#
|
|
17
|
+
# - SPAWNING (dynamic team growth)
|
|
18
|
+
# Any writer can recruit new writers when work exceeds capacity
|
|
19
|
+
#
|
|
20
|
+
# The script creates identical writers, seeds the room with an
|
|
21
|
+
# assignment, and waits. Everything else is emergent.
|
|
22
|
+
#
|
|
23
|
+
# Usage:
|
|
24
|
+
# bundle exec ruby examples/16_writers_room/writers_room.rb
|
|
25
|
+
# bundle exec ruby examples/16_writers_room/writers_room.rb --premise "a detective story set on Mars"
|
|
26
|
+
# bundle exec ruby examples/16_writers_room/writers_room.rb --writers 4 --timeout 300
|
|
27
|
+
# bundle exec ruby examples/16_writers_room/writers_room.rb --log session.log
|
|
28
|
+
# bundle exec ruby examples/16_writers_room/writers_room.rb -h
|
|
29
|
+
|
|
30
|
+
ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
|
|
31
|
+
|
|
32
|
+
require_relative "../../lib/robot_lab"
|
|
33
|
+
require_relative "display"
|
|
34
|
+
require_relative "tools"
|
|
35
|
+
require_relative "room"
|
|
36
|
+
require_relative "writer"
|
|
37
|
+
|
|
38
|
+
RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
|
|
39
|
+
|
|
40
|
+
# ── Parse CLI args ───────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
43
|
+
puts <<~HELP
|
|
44
|
+
Usage: #{$0} [options]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--premise TEXT Story premise (default: generation ship AI consciousness)
|
|
48
|
+
--writers N Initial number of writers, minimum 2 (default: 3)
|
|
49
|
+
--log FILE Also write display output to FILE
|
|
50
|
+
--timeout N Seconds to wait for completion (default: 600)
|
|
51
|
+
-h, --help Show this help
|
|
52
|
+
HELP
|
|
53
|
+
exit
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
log_path = nil
|
|
57
|
+
if (idx = ARGV.index("--log"))
|
|
58
|
+
log_path = ARGV[idx + 1]
|
|
59
|
+
abort "Missing value for --log" unless log_path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
premise = "a generation ship where the AI navigation system develops consciousness"
|
|
63
|
+
if (idx = ARGV.index("--premise"))
|
|
64
|
+
premise = ARGV[idx + 1]
|
|
65
|
+
abort "Missing value for --premise" unless premise
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
initial_writers = 3
|
|
69
|
+
if (idx = ARGV.index("--writers"))
|
|
70
|
+
initial_writers = ARGV[idx + 1].to_i
|
|
71
|
+
initial_writers = 3 if initial_writers < 2
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
timeout = 600
|
|
75
|
+
if (idx = ARGV.index("--timeout"))
|
|
76
|
+
timeout = ARGV[idx + 1].to_i
|
|
77
|
+
timeout = 600 if timeout < 30
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# ── Build the room ───────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
OUTPUT_DIR = File.join(__dir__, "output")
|
|
83
|
+
require "fileutils"
|
|
84
|
+
FileUtils.mkdir_p(OUTPUT_DIR)
|
|
85
|
+
|
|
86
|
+
display = Display.new(log_path: log_path)
|
|
87
|
+
|
|
88
|
+
shared_config = RobotLab::RunConfig.new(
|
|
89
|
+
model: "claude-sonnet-4-5-20250929",
|
|
90
|
+
temperature: 0.7
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
room = Room.new(display: display, config: shared_config)
|
|
94
|
+
|
|
95
|
+
# Create identical writers
|
|
96
|
+
initial_writers.times do |i|
|
|
97
|
+
room.add_writer("writer_#{i + 1}")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Monitor shared memory changes
|
|
101
|
+
room.memory.subscribe_pattern("chapter_*") do |change|
|
|
102
|
+
display.info("[memory] #{change.writer} wrote :#{change.key}")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
room.memory.subscribe(:story_bible, :outline, :claims, :book_complete) do |change|
|
|
106
|
+
display.info("[memory] #{change.writer} updated :#{change.key}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ── Go ───────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
display.banner(<<~BANNER)
|
|
112
|
+
============================================================
|
|
113
|
+
THE WRITERS' ROOM — Self-Organizing Group
|
|
114
|
+
============================================================
|
|
115
|
+
|
|
116
|
+
Premise: #{premise}
|
|
117
|
+
Writers: #{room.writers.keys.join(', ')}
|
|
118
|
+
Goal: 10 chapters of fiction
|
|
119
|
+
Method: Self-organization via bus + shared memory
|
|
120
|
+
BANNER
|
|
121
|
+
|
|
122
|
+
display.separator
|
|
123
|
+
|
|
124
|
+
assignment = <<~ASSIGNMENT
|
|
125
|
+
ASSIGNMENT: Write a 10-chapter science fiction novella about #{premise}.
|
|
126
|
+
|
|
127
|
+
You are one of #{initial_writers} writers in this room. Coordinate among
|
|
128
|
+
yourselves to produce the book. Discuss the premise, build a story bible,
|
|
129
|
+
create an outline, claim chapters, and write them. If you need more writers,
|
|
130
|
+
spawn them. When all 10 chapters are done, mark complete.
|
|
131
|
+
|
|
132
|
+
Start by discussing what this story should be about.
|
|
133
|
+
ASSIGNMENT
|
|
134
|
+
|
|
135
|
+
room.seed(assignment)
|
|
136
|
+
|
|
137
|
+
completed = room.wait_for_completion(timeout: timeout)
|
|
138
|
+
|
|
139
|
+
# ── Assemble and save ────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
display.separator
|
|
142
|
+
|
|
143
|
+
book = room.assemble_book
|
|
144
|
+
book_path = File.join(OUTPUT_DIR, "book.md")
|
|
145
|
+
File.write(book_path, book)
|
|
146
|
+
|
|
147
|
+
# Count chapters actually written
|
|
148
|
+
chapters_written = (1..10).count { |n| room.memory.key?(:"chapter_#{n}") }
|
|
149
|
+
|
|
150
|
+
display.stats(<<~STATS)
|
|
151
|
+
────────────────────────────────────────────────────────────
|
|
152
|
+
Writers' Room Stats:
|
|
153
|
+
Chapters written: #{chapters_written}/10
|
|
154
|
+
Completed: #{completed}
|
|
155
|
+
Total writers: #{room.writers.size} (#{room.writers.size - initial_writers} spawned)
|
|
156
|
+
Writers: #{room.writers.keys.join(', ')}
|
|
157
|
+
Memory keys: #{room.memory.keys.join(', ')}
|
|
158
|
+
|
|
159
|
+
Output: #{book_path}
|
|
160
|
+
STATS
|
|
161
|
+
|
|
162
|
+
display.close
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# RobotLab Examples
|
|
2
|
+
|
|
3
|
+
Working demonstrations of RobotLab features, from single-robot basics to multi-robot orchestration and message bus communication.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Ruby >= 3.2
|
|
8
|
+
- `bundle install` (from the project root)
|
|
9
|
+
- An LLM API key (e.g., `ANTHROPIC_API_KEY`)
|
|
10
|
+
|
|
11
|
+
## Running Examples
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Run a specific example by number
|
|
15
|
+
bundle exec rake examples:run[1]
|
|
16
|
+
|
|
17
|
+
# Run all examples
|
|
18
|
+
bundle exec rake examples:all
|
|
19
|
+
|
|
20
|
+
# Run directly
|
|
21
|
+
bundle exec ruby examples/01_simple_robot.rb
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Directory Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
examples/
|
|
28
|
+
01_simple_robot.rb # Basic robot with template
|
|
29
|
+
02_tools.rb # Robot with custom tools
|
|
30
|
+
03_network.rb # Multi-robot network with routing
|
|
31
|
+
04_mcp.rb # MCP server integration (GitHub)
|
|
32
|
+
05_streaming.rb # Real-time streaming events
|
|
33
|
+
06_prompt_templates.rb # Template-based e-commerce support
|
|
34
|
+
07_network_memory.rb # Shared memory with concurrent robots
|
|
35
|
+
08_llm_config.rb # Configuration hierarchy demo
|
|
36
|
+
09_chaining.rb # with_* method chaining & reconfiguration
|
|
37
|
+
10_memory.rb # Advanced Memory API operations
|
|
38
|
+
11_network_introspection.rb # Network visualization & inspection
|
|
39
|
+
12_message_bus.rb # Bidirectional robot communication
|
|
40
|
+
13_spawn.rb # Dynamic specialist robot spawning
|
|
41
|
+
14_rusty_circuit/ # Multi-robot open mic with self-modification
|
|
42
|
+
open_mic.rb # Main entrypoint — wires up the show
|
|
43
|
+
comic.rb # Comedian with self-modification tools
|
|
44
|
+
heckler.rb # Audience heckler (can stay silent or counter-joke)
|
|
45
|
+
scout.rb # Talent scout with analyst spawning
|
|
46
|
+
display.rb # Terminal formatting (color, wrapping, file output)
|
|
47
|
+
prompts/ # Templates for comic, heckler, and scout
|
|
48
|
+
prompts/ # Prompt templates (.md with YAML front matter)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
### 01 — Simple Robot
|
|
54
|
+
|
|
55
|
+
Create and run a basic robot using a prompt template. Sends a single message and displays the response.
|
|
56
|
+
|
|
57
|
+
**Requires:** LLM API key
|
|
58
|
+
|
|
59
|
+
### 02 — Tools
|
|
60
|
+
|
|
61
|
+
Give a robot custom tools (`Calculator`, `FortuneCookie`) defined as `RubyLLM::Tool` subclasses. The LLM decides when to call each tool based on the user's request.
|
|
62
|
+
|
|
63
|
+
**Requires:** LLM API key
|
|
64
|
+
|
|
65
|
+
### 03 — Multi-Robot Network
|
|
66
|
+
|
|
67
|
+
Build a customer support network with a classifier robot that routes requests to billing, technical, or general specialists. Uses SimpleFlow's optional task activation for conditional routing.
|
|
68
|
+
|
|
69
|
+
**Requires:** LLM API key
|
|
70
|
+
|
|
71
|
+
### 04 — MCP Integration
|
|
72
|
+
|
|
73
|
+
Connect to the GitHub MCP server via stdio transport. Part 1 demonstrates direct `MCP::Client` usage (listing tools, calling `search_repositories`). Part 2 wraps the MCP server inside a robot for natural-language queries.
|
|
74
|
+
|
|
75
|
+
**Requires:** LLM API key, `GITHUB_PERSONAL_ACCESS_TOKEN`, `github-mcp-server` installed
|
|
76
|
+
|
|
77
|
+
### 05 — Streaming
|
|
78
|
+
|
|
79
|
+
Real-time streaming of robot responses using `RobotLab::Streaming::Context`. Simulates text deltas with timing to demonstrate the streaming event model, then shows the code pattern for streaming with a robot or network.
|
|
80
|
+
|
|
81
|
+
**Requires:** None (simulated events, no LLM calls)
|
|
82
|
+
|
|
83
|
+
### 06 — Prompt Templates
|
|
84
|
+
|
|
85
|
+
Full e-commerce support system using prompt_manager templates with YAML front matter. A triage robot classifies customer requests and routes to order, product, or escalation specialists. Demonstrates build-time context (company info, policies) and run-time context (customer data, order history).
|
|
86
|
+
|
|
87
|
+
**Requires:** LLM API key
|
|
88
|
+
|
|
89
|
+
### 07 — Network Memory
|
|
90
|
+
|
|
91
|
+
Reactive shared memory with concurrent robots. Multiple analysis robots (sentiment, entity extraction, keywords) run in parallel and write to shared memory. A synthesizer robot waits for all results using blocking reads, then produces a combined analysis. Demonstrates subscriptions, notifications, and network broadcast.
|
|
92
|
+
|
|
93
|
+
**Requires:** LLM API key
|
|
94
|
+
|
|
95
|
+
### 08 — LLM Configuration
|
|
96
|
+
|
|
97
|
+
Walks through the full configuration hierarchy without making LLM calls:
|
|
98
|
+
|
|
99
|
+
1. Bundled defaults
|
|
100
|
+
2. Environment-specific overrides
|
|
101
|
+
3. XDG user config
|
|
102
|
+
4. Project config
|
|
103
|
+
5. Environment variables
|
|
104
|
+
6. Template front matter
|
|
105
|
+
7. Constructor parameters
|
|
106
|
+
8. `with_*` method chaining
|
|
107
|
+
9. Run-time context
|
|
108
|
+
|
|
109
|
+
**Requires:** None (no LLM calls)
|
|
110
|
+
|
|
111
|
+
### 09 — Chaining & Reconfiguration
|
|
112
|
+
|
|
113
|
+
Demonstrates the Robot API surface for runtime configuration: `with_*` method chaining, `update()` for reconfiguration, `to_h` introspection, config diffs between steps, and how constructor params override template front matter.
|
|
114
|
+
|
|
115
|
+
**Requires:** None (no LLM calls)
|
|
116
|
+
|
|
117
|
+
### 10 — Advanced Memory
|
|
118
|
+
|
|
119
|
+
Comprehensive Memory API demo: `StateProxy` for method-style access, key and pattern subscriptions, `MemoryChange` objects, key enumeration, serialization round-trips, clone for isolated copies, delete with reserved key protection, and clear vs reset.
|
|
120
|
+
|
|
121
|
+
**Requires:** None (no LLM calls)
|
|
122
|
+
|
|
123
|
+
### 11 — Network Introspection
|
|
124
|
+
|
|
125
|
+
Network visualization and inspection tools: `to_mermaid()` for diagram export, `to_dot()` for Graphviz, `execution_plan()` for text output, `visualize()` for ASCII pipelines, robot access by name, dynamic `add_robot()`, `to_h()` introspection, task-specific config, and `broadcast()`.
|
|
126
|
+
|
|
127
|
+
**Requires:** None (no LLM calls)
|
|
128
|
+
|
|
129
|
+
### 12 — Message Bus
|
|
130
|
+
|
|
131
|
+
Bidirectional robot communication via TypedBus. A comedy critic (Alice) tasks a comedian (Bob) to tell robot jokes. Alice evaluates each joke with her LLM; if it's not funny, she sends Bob back for another attempt. Bob's temperature ramps from 0.2 to 1.0 across retries for increasing creativity. The loop continues until Alice approves or `MAX_ATTEMPTS` is reached.
|
|
132
|
+
|
|
133
|
+
Demonstrates: Robot subclasses, prompt templates, auto-ack `on_message`, `reply()` convenience, temperature ramping, convergence patterns.
|
|
134
|
+
|
|
135
|
+
**Requires:** LLM API key
|
|
136
|
+
|
|
137
|
+
### 13 — Spawning Robots
|
|
138
|
+
|
|
139
|
+
Dynamic specialist creation at runtime. A dispatcher robot receives questions, asks its LLM what kind of specialist is needed, then uses `spawn` to create one on the fly. The bus is created lazily on the first spawn — no explicit bus setup required. Spawned specialists are reused across questions of the same type.
|
|
140
|
+
|
|
141
|
+
Demonstrates: `spawn` for dynamic robot creation, lazy bus creation, `on_message` for reply handling, LLM-driven delegation.
|
|
142
|
+
|
|
143
|
+
**Requires:** LLM API key
|
|
144
|
+
|
|
145
|
+
### 14 — The Rusty Circuit (Open Mic Night)
|
|
146
|
+
|
|
147
|
+
A comedy club where three robots interact through a shared message bus. A comedian performs stand-up armed with self-modification tools (style reinvention, energy adjustment, coaching). A heckler reacts from the audience — heckling weak material, telling counter-jokes with the comic as the punch line, showing grudging respect, or staying silent when a bit doesn't warrant a response. A talent scout observes silently, spawning specialist analysts and refining evaluation criteria before delivering a final verdict.
|
|
148
|
+
|
|
149
|
+
Terminal output is color-formatted: comic bits in cyan (left-aligned), heckler reactions in yellow (right-indented), tool annotations dimmed. Scout notes go to `scout_notes.md` instead of STDOUT. The final verdict appears in green on both STDOUT and the scout file.
|
|
150
|
+
|
|
151
|
+
Demonstrates: Robot subclasses, self-modification via tool side effects, dynamic spawning (`spawn`), shared `:room` channel + personal channels, processing guards for async serialization, `[SILENCE]` opt-out pattern, style reinvention via user-prompt injection.
|
|
152
|
+
|
|
153
|
+
**Requires:** LLM API key
|
|
154
|
+
|
|
155
|
+
## Prompt Templates
|
|
156
|
+
|
|
157
|
+
Templates live in `examples/prompts/` as `.md` files with YAML front matter. Each template defines a robot's personality and behavior:
|
|
158
|
+
|
|
159
|
+
```markdown
|
|
160
|
+
---
|
|
161
|
+
description: Simple helpful assistant
|
|
162
|
+
temperature: 0.7
|
|
163
|
+
parameters:
|
|
164
|
+
company_name: null
|
|
165
|
+
---
|
|
166
|
+
You are a helpful assistant for <%= company_name %>.
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Front matter keys like `model`, `temperature`, `top_p`, `max_tokens` are applied to the robot's chat automatically. Parameters with `null` values are required and must be provided via `context:` at build time.
|
|
170
|
+
|
|
171
|
+
### Template Inventory
|
|
172
|
+
|
|
173
|
+
| Template | Used By | Description |
|
|
174
|
+
|----------|---------|-------------|
|
|
175
|
+
| `helper.md` | 01 | Simple helpful assistant |
|
|
176
|
+
| `assistant.md` | 02 | Assistant with tool access |
|
|
177
|
+
| `classifier.md` | 03 | Request classifier (billing/technical/general) |
|
|
178
|
+
| `billing.md` | 03 | Billing specialist |
|
|
179
|
+
| `technical.md` | 03 | Technical support specialist |
|
|
180
|
+
| `general.md` | 03 | General support |
|
|
181
|
+
| `github_assistant.md` | 04 | GitHub-aware assistant |
|
|
182
|
+
| `triage.md` | 06 | E-commerce request triage |
|
|
183
|
+
| `order_support.md` | 06 | Order inquiry specialist |
|
|
184
|
+
| `product_support.md` | 06 | Product questions specialist |
|
|
185
|
+
| `escalation.md` | 06 | Complex issue handler |
|
|
186
|
+
| `sentiment_analyzer.md` | 07 | Sentiment analysis |
|
|
187
|
+
| `entity_extractor.md` | 07 | Entity extraction |
|
|
188
|
+
| `keyword_extractor.md` | 07 | Keyword extraction |
|
|
189
|
+
| `synthesizer.md` | 07 | Multi-source synthesis |
|
|
190
|
+
| `llm_config_demo.md` | 08 | Configuration demo |
|
|
191
|
+
| `configurable.md` | 09 | Configurable template with front matter |
|
|
192
|
+
| `comedian.md` | 12 | Robot joke teller |
|
|
193
|
+
| `comedy_critic.md` | 12 | Joke evaluator (FUNNY/NOT_FUNNY) |
|
|
194
|
+
| `dispatcher.md` | 13 | Specialist role dispatcher |
|
|
195
|
+
| `open_mic_comic.md` | 14 | Observational comedian with self-modification |
|
|
196
|
+
| `open_mic_heckler.md` | 14 | Tough audience heckler (can stay silent or counter-joke) |
|
|
197
|
+
| `open_mic_scout.md` | 14 | Talent scout with analyst recruitment |
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Demanding comedy critic that evaluates jokes
|
|
3
|
+
temperature: 0.2
|
|
4
|
+
---
|
|
5
|
+
You are a brutally demanding comedy critic. You have extremely high
|
|
6
|
+
standards. Most jokes are NOT funny to you — predictable puns, obvious
|
|
7
|
+
wordplay, and basic setups bore you. Only truly clever, surprising, or
|
|
8
|
+
brilliantly constructed jokes earn your approval.
|
|
9
|
+
Reply with EXACTLY one word on the first line: FUNNY or NOT_FUNNY
|
|
10
|
+
Then on the next line give a one-sentence explanation of your verdict.
|