robot_lab 0.2.1 → 0.2.7
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/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +185 -64
- data/Rakefile +28 -12
- data/docs/api/core/index.md +41 -14
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +285 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +636 -60
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +152 -14
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +83 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +118 -30
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +153 -38
- data/docs/architecture/robot-execution.md +173 -91
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +299 -93
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +296 -50
- data/docs/guides/creating-networks.md +220 -31
- data/docs/guides/hooks.md +1084 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +37 -6
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +166 -51
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +300 -11
- data/docs/index.md +37 -5
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +23 -9
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +266 -0
- data/examples/README.md +203 -42
- data/examples/common.rb +79 -7
- data/examples/xyzzy.rb +97 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +14 -9
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/connection_poller.rb +2 -2
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +90 -18
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +18 -5
- data/lib/robot_lab/robot.rb +233 -89
- data/lib/robot_lab/run_config.rb +24 -5
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +70 -15
- data/mkdocs.yml +7 -1
- metadata +41 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
# In a bus-based SOG, writers receive many messages and each
|
|
21
21
|
# triggers a run() call. When the LLM responds with only tool
|
|
22
22
|
# calls (no text), RubyLLM appends an empty text content block
|
|
23
|
-
# to the chat history.
|
|
24
|
-
#
|
|
23
|
+
# to the chat history. Several providers reject that on the next
|
|
24
|
+
# call, permanently killing the writer.
|
|
25
25
|
#
|
|
26
|
-
# Fix: reset the
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
26
|
+
# Fix: reset the conversation before each message via the public
|
|
27
|
+
# Robot#clear_messages(keep_system: true). The writer doesn't need
|
|
28
|
+
# persistent chat history — shared memory is the single source of
|
|
29
|
+
# truth. Rebuilding @chat by hand instead would drop the tool
|
|
30
|
+
# callbacks and chat params that Robot#initialize installed, so
|
|
31
|
+
# clear_messages is both shorter and safer.
|
|
30
32
|
#
|
|
31
33
|
class Writer < RobotLab::Robot
|
|
32
34
|
attr_accessor :shared_memory, :display, :room
|
|
@@ -40,6 +42,7 @@ class Writer < RobotLab::Robot
|
|
|
40
42
|
|
|
41
43
|
super(
|
|
42
44
|
name: name,
|
|
45
|
+
**llm_opts,
|
|
43
46
|
template: room.mode[:template],
|
|
44
47
|
context: { writer_name: name },
|
|
45
48
|
bus: bus,
|
|
@@ -69,24 +72,18 @@ class Writer < RobotLab::Robot
|
|
|
69
72
|
]
|
|
70
73
|
end
|
|
71
74
|
|
|
75
|
+
# Room deliveries go to this robot's BusPoller, which serializes them
|
|
76
|
+
# behind any run() already in flight before invoking on_message.
|
|
72
77
|
def setup_room_subscription
|
|
73
78
|
@bus.subscribe(:room) do |delivery|
|
|
74
|
-
|
|
79
|
+
enqueue_delivery(delivery)
|
|
75
80
|
end
|
|
76
81
|
end
|
|
77
82
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
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?
|
|
83
|
+
# Drop the turn history, keep the rendered system prompt. Model,
|
|
84
|
+
# temperature, tools and the tool callbacks all survive untouched.
|
|
85
|
+
def reset_conversation!
|
|
86
|
+
clear_messages(keep_system: true)
|
|
90
87
|
end
|
|
91
88
|
|
|
92
89
|
def setup_message_handler
|
|
@@ -98,8 +95,8 @@ class Writer < RobotLab::Robot
|
|
|
98
95
|
log&.info("#{name} <- [#{message.from}] msg ##{@messages_processed} (#{message.content.to_s[0..80]}...)")
|
|
99
96
|
@display&.incoming(name, message.from, message.content)
|
|
100
97
|
|
|
101
|
-
# Fresh
|
|
102
|
-
|
|
98
|
+
# Fresh conversation for each message — shared memory is our persistence
|
|
99
|
+
reset_conversation!
|
|
103
100
|
|
|
104
101
|
# Build prompt with current memory context
|
|
105
102
|
memory_keys = shared_memory.keys
|
|
@@ -109,7 +106,10 @@ class Writer < RobotLab::Robot
|
|
|
109
106
|
log&.info("#{name} -> run() starting (prompt: #{prompt.length} chars)")
|
|
110
107
|
|
|
111
108
|
begin
|
|
112
|
-
|
|
109
|
+
# tools: :inherit — run() defaults to :none. Without it the writer
|
|
110
|
+
# would have all seven tools attached and none of them offered to the
|
|
111
|
+
# model, so nothing would ever be written to shared memory.
|
|
112
|
+
result = run(prompt, tools: :inherit)
|
|
113
113
|
reply_text = result.respond_to?(:reply) ? result.reply.to_s[0..120] : result.to_s[0..120]
|
|
114
114
|
log&.info("#{name} <- run() finished (reply: #{reply_text}...)")
|
|
115
115
|
rescue => e
|
|
@@ -125,6 +125,8 @@ FileUtils.mkdir_p(OUTPUT_DIR)
|
|
|
125
125
|
|
|
126
126
|
display = Display.new(log_path: log_path)
|
|
127
127
|
|
|
128
|
+
# RunConfig has no `provider` field, so the writers get provider+model via
|
|
129
|
+
# **llm_opts and inherit the rest of their operational defaults from here.
|
|
128
130
|
shared_config = RobotLab::RunConfig.new(
|
|
129
131
|
model: LLM[:default].model,
|
|
130
132
|
temperature: 0.7
|
data/examples/17_skills.rb
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
# 6. incident_responder — "you are the on-call analyst for..."
|
|
30
30
|
#
|
|
31
31
|
# Usage:
|
|
32
|
-
#
|
|
32
|
+
# ruby examples/17_skills.rb
|
|
33
33
|
#
|
|
34
34
|
# Templates:
|
|
35
35
|
# examples/prompts/
|
|
@@ -136,7 +136,7 @@ puts
|
|
|
136
136
|
# - sre_compliance: Recursive skill — auto-expands to include pii_redactor
|
|
137
137
|
# and audit_trail before its own compliance instructions
|
|
138
138
|
responder = RobotLab.build(
|
|
139
|
-
|
|
139
|
+
**llm_opts,
|
|
140
140
|
name: "incident_responder",
|
|
141
141
|
template: :incident_responder,
|
|
142
142
|
skills: [:runbook_protocol, :structured_output, :sre_compliance],
|
|
@@ -147,17 +147,20 @@ responder = RobotLab.build(
|
|
|
147
147
|
}
|
|
148
148
|
)
|
|
149
149
|
|
|
150
|
-
# Show what the robot got
|
|
150
|
+
# Show what the robot got.
|
|
151
|
+
#
|
|
152
|
+
# `skills` is the DECLARED list. The expanded list (with sre_compliance's
|
|
153
|
+
# nested skills spliced in ahead of it) has no public reader — but you don't
|
|
154
|
+
# need one, because the expansion is visible in the assembled system prompt
|
|
155
|
+
# printed below, which is the thing that actually reaches the model.
|
|
151
156
|
puts "Robot: #{responder.name}"
|
|
152
|
-
puts "
|
|
153
|
-
puts "Expanded skills: #{responder.instance_variable_get(:@expanded_skills).inspect}"
|
|
157
|
+
puts "Declared skills: #{responder.skills.inspect}"
|
|
154
158
|
puts "Model: #{responder.model}"
|
|
155
159
|
puts
|
|
156
160
|
|
|
157
|
-
# Display the assembled system prompt
|
|
158
|
-
chat
|
|
159
|
-
|
|
160
|
-
system_msg = messages.find { |m| m.role.to_s == "system" }
|
|
161
|
+
# Display the assembled system prompt.
|
|
162
|
+
# Robot#messages is a public reader over the chat's message list.
|
|
163
|
+
system_msg = responder.messages.find { |m| m.role.to_s == "system" }
|
|
161
164
|
if system_msg
|
|
162
165
|
prompt = system_msg.content
|
|
163
166
|
hr
|
|
@@ -174,7 +177,7 @@ end
|
|
|
174
177
|
# =============================================================================
|
|
175
178
|
|
|
176
179
|
plain_responder = RobotLab.build(
|
|
177
|
-
|
|
180
|
+
**llm_opts,
|
|
178
181
|
name: "plain_responder",
|
|
179
182
|
template: :incident_responder,
|
|
180
183
|
context: {
|
|
@@ -184,9 +187,7 @@ plain_responder = RobotLab.build(
|
|
|
184
187
|
}
|
|
185
188
|
)
|
|
186
189
|
|
|
187
|
-
|
|
188
|
-
plain_messages = plain_chat.instance_variable_get(:@messages)
|
|
189
|
-
plain_msg = plain_messages.find { |m| m.role.to_s == "system" }
|
|
190
|
+
plain_msg = plain_responder.messages.find { |m| m.role.to_s == "system" }
|
|
190
191
|
if plain_msg
|
|
191
192
|
puts
|
|
192
193
|
puts "For comparison — plain responder (no skills): #{plain_msg.content.length} chars"
|
data/examples/18_rails/README.md
CHANGED
|
@@ -11,7 +11,15 @@ Minimal Rails 8 app that demonstrates RobotLab's full Rails integration:
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
13
13
|
- Ruby 3.2+
|
|
14
|
-
-
|
|
14
|
+
- [Ollama](https://ollama.com) running locally with the demo model pulled:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
ollama serve
|
|
18
|
+
ollama pull qwen3.6
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
No API keys. Override the model with `ROBOT_LAB_PROVIDER` / `ROBOT_LAB_MODEL`,
|
|
22
|
+
and the endpoint with `OLLAMA_API_BASE`.
|
|
15
23
|
|
|
16
24
|
## Setup
|
|
17
25
|
|
|
@@ -33,6 +41,17 @@ bin/dev
|
|
|
33
41
|
2. Refresh the page — conversation history is preserved
|
|
34
42
|
3. Check `db/development.sqlite3` to see persisted threads and results
|
|
35
43
|
|
|
44
|
+
## Two things worth copying
|
|
45
|
+
|
|
46
|
+
**`RobotLab::RailsIntegration::Job` is the real superclass.** There is no
|
|
47
|
+
`RobotLab::Job` alias, so `class RobotRunJob < RobotLab::Job` raises `NameError`
|
|
48
|
+
at boot.
|
|
49
|
+
|
|
50
|
+
**`tools: :inherit` has to reach `run`.** `Robot#run` defaults to `tools: :none`,
|
|
51
|
+
which hands the provider an empty tool list. The job splats any extra keywords
|
|
52
|
+
into `robot.run`, so the controller passes `tools: :inherit` at enqueue time —
|
|
53
|
+
without it TimeTool is never offered and the model just guesses the time.
|
|
54
|
+
|
|
36
55
|
## Architecture
|
|
37
56
|
|
|
38
57
|
```
|
|
@@ -24,10 +24,14 @@ class ChatController < ApplicationController
|
|
|
24
24
|
checksum: Digest::SHA256.hexdigest(message)
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
# tools: :inherit is forwarded by the job straight through to robot.run.
|
|
28
|
+
# Without it run() defaults to tools: :none and TimeTool is never offered
|
|
29
|
+
# to the model, so "What time is it?" gets a guess instead of a tool call.
|
|
27
30
|
RobotRunJob.perform_later(
|
|
28
31
|
robot_class: "ChatRobot",
|
|
29
32
|
message: message,
|
|
30
|
-
thread_id: thread_id
|
|
33
|
+
thread_id: thread_id,
|
|
34
|
+
tools: :inherit
|
|
31
35
|
)
|
|
32
36
|
|
|
33
37
|
respond_to do |format|
|
|
@@ -2,18 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
# Generic background job for executing any robot asynchronously.
|
|
4
4
|
#
|
|
5
|
-
# Inherits from RobotLab::Job — Turbo Stream wiring, thread
|
|
6
|
-
# and completion/error broadcasting are all handled by the base
|
|
5
|
+
# Inherits from RobotLab::RailsIntegration::Job — Turbo Stream wiring, thread
|
|
6
|
+
# persistence, and completion/error broadcasting are all handled by the base
|
|
7
|
+
# class. (Note the full namespace: robot_lab-rails defines the class as
|
|
8
|
+
# RobotLab::RailsIntegration::Job and registers no RobotLab::Job alias.)
|
|
7
9
|
#
|
|
8
|
-
# Pass robot_class: at enqueue time to select which robot to run.
|
|
10
|
+
# Pass robot_class: at enqueue time to select which robot to run. Any extra
|
|
11
|
+
# keywords are forwarded straight to robot.run — which is how `tools:` gets
|
|
12
|
+
# through, since run defaults to :none and would otherwise send the model an
|
|
13
|
+
# empty tool list.
|
|
9
14
|
#
|
|
10
15
|
# @example Enqueue from a controller
|
|
11
16
|
# RobotRunJob.perform_later(
|
|
12
17
|
# robot_class: "ChatRobot",
|
|
13
18
|
# message: params[:message],
|
|
14
|
-
# thread_id: session_id
|
|
19
|
+
# thread_id: session_id,
|
|
20
|
+
# tools: :inherit
|
|
15
21
|
# )
|
|
16
22
|
#
|
|
17
|
-
class RobotRunJob < RobotLab::Job
|
|
23
|
+
class RobotRunJob < RobotLab::RailsIntegration::Job
|
|
18
24
|
queue_as :default
|
|
19
25
|
end
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Factory for the chat robot. RobotLab::RailsIntegration::Job calls
|
|
4
|
+
# ChatRobot.build(on_content:, on_tool_call:) to wire Turbo streaming.
|
|
5
|
+
#
|
|
6
|
+
# The model is served locally by Ollama. Ollama models are absent from
|
|
7
|
+
# RubyLLM's registry, so provider: must accompany model: — that is what makes
|
|
8
|
+
# RubyLLM skip the registry lookup.
|
|
3
9
|
class ChatRobot
|
|
4
10
|
SYSTEM_PROMPT = "You are a friendly assistant. Be concise."
|
|
5
11
|
|
|
12
|
+
PROVIDER = ENV.fetch("ROBOT_LAB_PROVIDER", "ollama")
|
|
13
|
+
MODEL = ENV.fetch("ROBOT_LAB_MODEL", "qwen3.6:latest")
|
|
14
|
+
|
|
6
15
|
def self.build(**options)
|
|
7
16
|
RobotLab.build(
|
|
8
17
|
name: "chat",
|
|
18
|
+
provider: PROVIDER,
|
|
19
|
+
model: MODEL,
|
|
9
20
|
system_prompt: SYSTEM_PROMPT,
|
|
10
21
|
local_tools: [TimeTool],
|
|
11
22
|
**options
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
RobotLab.config.logger = Rails.logger
|
|
4
|
+
|
|
5
|
+
# Runs against a local Ollama server — no API keys required.
|
|
6
|
+
# ollama serve
|
|
7
|
+
# ollama pull qwen3.6
|
|
8
|
+
RubyLLM.configure do |c|
|
|
9
|
+
c.logger = Rails.logger
|
|
10
|
+
c.ollama_api_base = ENV.fetch("OLLAMA_API_BASE", "http://localhost:11434/v1")
|
|
11
|
+
end
|
|
@@ -14,24 +14,30 @@
|
|
|
14
14
|
# naturally increase as conversation context accumulates.
|
|
15
15
|
# Use a fresh robot.build when you need a genuinely fresh context.
|
|
16
16
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# Ollama reports usage counts like the hosted providers do, so the tracking
|
|
18
|
+
# API behaves identically — only the price per token differs. Token counts are
|
|
19
|
+
# zero for providers that don't report usage data.
|
|
19
20
|
#
|
|
20
21
|
# Usage:
|
|
21
|
-
#
|
|
22
|
+
# ruby examples/19_token_tracking.rb
|
|
22
23
|
|
|
23
24
|
require_relative "common"
|
|
24
25
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# Cost model. A local Ollama model bills nothing, so the interesting number is
|
|
27
|
+
# what the same traffic WOULD have cost on a hosted model — set RATE_INPUT_CPM
|
|
28
|
+
# and RATE_OUTPUT_CPM to your provider's $-per-1M-token rates to see it.
|
|
29
|
+
# Defaults are zero: local inference is free.
|
|
30
|
+
RATE_INPUT_CPM = ENV.fetch("RATE_INPUT_CPM", "0").to_f
|
|
31
|
+
RATE_OUTPUT_CPM = ENV.fetch("RATE_OUTPUT_CPM", "0").to_f
|
|
28
32
|
|
|
29
33
|
def token_summary(input, output)
|
|
30
34
|
"in=#{input} out=#{output} total=#{input + output}"
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
def run_cost(input, output)
|
|
34
|
-
|
|
38
|
+
return "$0.00000 (local)" if RATE_INPUT_CPM.zero? && RATE_OUTPUT_CPM.zero?
|
|
39
|
+
|
|
40
|
+
dollars = (input * RATE_INPUT_CPM + output * RATE_OUTPUT_CPM) / 1_000_000.0
|
|
35
41
|
"$#{"%.5f" % dollars}"
|
|
36
42
|
end
|
|
37
43
|
|
|
@@ -41,8 +47,18 @@ end
|
|
|
41
47
|
|
|
42
48
|
banner "Per-Robot Token & Cost Tracking"
|
|
43
49
|
|
|
50
|
+
puts "Model: #{LLM[:default].provider}/#{LLM[:default].model}"
|
|
51
|
+
if RATE_INPUT_CPM.zero? && RATE_OUTPUT_CPM.zero?
|
|
52
|
+
puts "Rates: none set — local inference is free."
|
|
53
|
+
puts " Set RATE_INPUT_CPM / RATE_OUTPUT_CPM to price the same traffic"
|
|
54
|
+
puts " against a hosted provider."
|
|
55
|
+
else
|
|
56
|
+
puts "Rates: $#{RATE_INPUT_CPM}/1M in, $#{RATE_OUTPUT_CPM}/1M out"
|
|
57
|
+
end
|
|
58
|
+
puts
|
|
59
|
+
|
|
44
60
|
robot = RobotLab.build(
|
|
45
|
-
|
|
61
|
+
**llm_opts,
|
|
46
62
|
name: "analyst",
|
|
47
63
|
system_prompt: "You are a concise technical analyst. Keep every reply under 40 words."
|
|
48
64
|
)
|
|
@@ -106,7 +122,7 @@ puts "Fresh robot — genuinely zero context:"
|
|
|
106
122
|
puts
|
|
107
123
|
|
|
108
124
|
fresh = RobotLab.build(
|
|
109
|
-
|
|
125
|
+
**llm_opts,
|
|
110
126
|
name: "analyst2",
|
|
111
127
|
system_prompt: "You are a concise technical analyst. Keep every reply under 40 words."
|
|
112
128
|
)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
#
|
|
13
13
|
# Key behaviour when the breaker fires:
|
|
14
14
|
# The chat history contains a dangling tool_use with no tool_result.
|
|
15
|
-
#
|
|
15
|
+
# Most providers reject any subsequent request with that
|
|
16
16
|
# broken history. You MUST call robot.clear_messages before reusing the
|
|
17
17
|
# same robot instance — or simply build a fresh robot.
|
|
18
18
|
#
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
# - Contrast: robot without a circuit breaker on a task that terminates
|
|
24
24
|
#
|
|
25
25
|
# Usage:
|
|
26
|
-
#
|
|
26
|
+
# ruby examples/20_circuit_breaker.rb
|
|
27
27
|
|
|
28
28
|
require_relative "common"
|
|
29
29
|
|
|
@@ -69,14 +69,14 @@ TASK = "Run the batch process from step 1 using the MultiStepProcessor tool. " \
|
|
|
69
69
|
# Part 1: Circuit breaker fires — rescue ToolLoopError, then recover
|
|
70
70
|
#
|
|
71
71
|
# After ToolLoopError the chat contains a dangling tool_use block with no
|
|
72
|
-
# matching tool_result.
|
|
72
|
+
# matching tool_result. Providers reject any follow-up request with that
|
|
73
73
|
# history. Call clear_messages to flush the broken context before reuse.
|
|
74
74
|
# -------------------------------------------------------------------------
|
|
75
75
|
|
|
76
76
|
section "Part 1: Circuit Breaker Fires (max_tool_rounds: 5)"
|
|
77
77
|
|
|
78
78
|
robot = RobotLab.build(
|
|
79
|
-
|
|
79
|
+
**llm_opts,
|
|
80
80
|
name: "process_runner",
|
|
81
81
|
system_prompt: "You are a process runner. Execute tasks exactly as instructed.",
|
|
82
82
|
local_tools: [MultiStepProcessor],
|
|
@@ -84,7 +84,10 @@ robot = RobotLab.build(
|
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
begin
|
|
87
|
-
|
|
87
|
+
# tools: :inherit — run() defaults to :none, which sends the provider an
|
|
88
|
+
# empty tool list. With no tool to loop on there would be nothing for the
|
|
89
|
+
# breaker to trip.
|
|
90
|
+
robot.run(TASK, tools: :inherit)
|
|
88
91
|
puts "Process completed (unexpected for this demo)."
|
|
89
92
|
rescue RobotLab::ToolLoopError => e
|
|
90
93
|
puts "Circuit breaker fired!"
|
|
@@ -130,13 +133,13 @@ class SingleStep < RubyLLM::Tool
|
|
|
130
133
|
end
|
|
131
134
|
|
|
132
135
|
unguarded = RobotLab.build(
|
|
133
|
-
|
|
136
|
+
**llm_opts,
|
|
134
137
|
name: "calculator",
|
|
135
138
|
system_prompt: "Use the provided tool to answer questions.",
|
|
136
139
|
local_tools: [SingleStep]
|
|
137
140
|
)
|
|
138
141
|
|
|
139
|
-
result = unguarded.run("Double the number 21 using the tool.")
|
|
142
|
+
result = unguarded.run("Double the number 21 using the tool.", tools: :inherit)
|
|
140
143
|
puts "Result: #{result.reply&.strip}"
|
|
141
144
|
puts
|
|
142
145
|
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
# - robot.learnings — read the accumulated list
|
|
15
15
|
# - Learnings injected as "LEARNINGS FROM PREVIOUS RUNS:" prefix
|
|
16
16
|
# - Superset dedup: a broader learning replaces narrower earlier ones
|
|
17
|
-
# -
|
|
17
|
+
# - robot.memory[:learnings] — where the list is actually stored
|
|
18
|
+
# - Scope: learnings are per-robot and in-process; see robot_lab-durable
|
|
19
|
+
# (and example 33) for cross-session persistence
|
|
18
20
|
#
|
|
19
21
|
# Usage:
|
|
20
|
-
#
|
|
22
|
+
# ruby examples/21_learning_loop.rb
|
|
21
23
|
|
|
22
24
|
require_relative "common"
|
|
23
25
|
|
|
@@ -67,7 +69,7 @@ SNIPPETS = [
|
|
|
67
69
|
banner "Learning Accumulation Loop"
|
|
68
70
|
|
|
69
71
|
robot = RobotLab.build(
|
|
70
|
-
|
|
72
|
+
**llm_opts,
|
|
71
73
|
name: "code_reviewer",
|
|
72
74
|
system_prompt: <<~PROMPT
|
|
73
75
|
You are a concise Ruby code reviewer. For each snippet:
|
|
@@ -120,7 +122,7 @@ puts
|
|
|
120
122
|
# Demonstrate superset dedup: a broader learning replaces narrower ones
|
|
121
123
|
# ---------------------------------------------------------------
|
|
122
124
|
section "Deduplication Demo"
|
|
123
|
-
robot2 = RobotLab.build(name: "reviewer2", system_prompt: "You review code.")
|
|
125
|
+
robot2 = RobotLab.build(**llm_opts, name: "reviewer2", system_prompt: "You review code.")
|
|
124
126
|
|
|
125
127
|
robot2.learn("avoid using puts")
|
|
126
128
|
robot2.learn("avoid using puts and p in production code") # covers the first
|
|
@@ -130,26 +132,50 @@ robot2.learnings.each_with_index { |l, i| puts " #{i + 1}. #{l}" }
|
|
|
130
132
|
puts
|
|
131
133
|
|
|
132
134
|
# ---------------------------------------------------------------
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
+
# Where learnings actually live
|
|
136
|
+
#
|
|
137
|
+
# learn() writes through to the robot's own Memory under :learnings.
|
|
138
|
+
# Memory is exposed by the public `memory` reader, so you can inspect
|
|
139
|
+
# (or serialize) the list without touching instance variables.
|
|
135
140
|
# ---------------------------------------------------------------
|
|
136
|
-
section "
|
|
141
|
+
section "Learnings Are Backed by Robot Memory"
|
|
142
|
+
|
|
143
|
+
puts "robot.memory.get(:learnings):"
|
|
144
|
+
Array(robot.memory.get(:learnings)).each_with_index { |l, i| puts " #{i + 1}. #{l}" }
|
|
145
|
+
puts
|
|
146
|
+
puts "Same list as robot.learnings? #{robot.memory.get(:learnings) == robot.learnings}"
|
|
147
|
+
puts
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------
|
|
150
|
+
# Scope: learnings do NOT survive a rebuild
|
|
151
|
+
#
|
|
152
|
+
# Robot#initialize always constructs a fresh Memory and there is no
|
|
153
|
+
# `memory:` constructor parameter, so a new robot — even with the same
|
|
154
|
+
# name — starts with an empty learning list. learn() is a within-process
|
|
155
|
+
# accumulator, not a persistence layer.
|
|
156
|
+
# ---------------------------------------------------------------
|
|
157
|
+
section "Scope: In-Process Only"
|
|
137
158
|
|
|
138
|
-
shared_memory = robot.instance_variable_get(:@memory)
|
|
139
159
|
rebuilt = RobotLab.build(
|
|
140
|
-
|
|
160
|
+
**llm_opts,
|
|
141
161
|
name: "code_reviewer",
|
|
142
162
|
system_prompt: "You review code."
|
|
143
163
|
)
|
|
144
|
-
rebuilt.instance_variable_set(:@memory, shared_memory)
|
|
145
|
-
|
|
146
|
-
# Trigger the memory restore path
|
|
147
|
-
persisted = shared_memory.get(:learnings)
|
|
148
|
-
rebuilt.instance_variable_set(:@learnings, Array(persisted))
|
|
149
164
|
|
|
150
|
-
puts "Learnings on
|
|
151
|
-
|
|
165
|
+
puts "Learnings on a freshly built robot of the same name: #{rebuilt.learnings.size}"
|
|
166
|
+
puts "(Robot#initialize builds a new Memory every time — nothing carries over.)"
|
|
152
167
|
puts
|
|
168
|
+
puts <<~PERSIST
|
|
169
|
+
To carry knowledge across processes, use the robot_lab-durable gem:
|
|
170
|
+
|
|
171
|
+
require "robot_lab/durable"
|
|
172
|
+
robot.setup_durable_learning(domain: "ruby code review")
|
|
173
|
+
# ... run the robot ...
|
|
174
|
+
robot.run_reflector # promotes learnings to ~/.robot_lab/durable/<domain>.yml
|
|
175
|
+
|
|
176
|
+
On the next boot setup_durable_learning seeds robot.learnings from that
|
|
177
|
+
YAML store. See examples/33_stock_predictor.rb for a working loop.
|
|
178
|
+
PERSIST
|
|
153
179
|
|
|
154
180
|
hr
|
|
155
181
|
puts "Learning loop demo complete."
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
|
|
4
4
|
# Example 22: Context Window Compression
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# Drives RobotLab::HistoryCompressor directly over a synthetic transcript, so
|
|
7
|
+
# the keep/drop/summarize decisions are visible without spending any tokens.
|
|
8
|
+
# Old turns are scored against the recent context using stemmed
|
|
9
|
+
# term-frequency cosine similarity. High-relevance turns are kept verbatim;
|
|
10
|
+
# irrelevant turns are dropped; medium-relevance turns can be summarized.
|
|
11
|
+
#
|
|
12
|
+
# Robot#compress_history is the same algorithm applied in place to a live
|
|
13
|
+
# robot's chat — it constructs a HistoryCompressor over robot.messages and
|
|
14
|
+
# replaces them with the result. The final section shows that call.
|
|
11
15
|
#
|
|
12
16
|
# Demonstrates:
|
|
13
|
-
# -
|
|
17
|
+
# - HistoryCompressor.new(messages:, ...).call — pure, testable core
|
|
14
18
|
# - recent_turns: N — last N user+assistant pairs always protected
|
|
15
19
|
# - keep_threshold: / drop_threshold: — tunable relevance bands
|
|
16
20
|
# - summarizer: — optional lambda(text) -> String for medium-relevance
|
|
@@ -20,7 +24,7 @@
|
|
|
20
24
|
# gem 'classifier', '~> 2.3' # add to your Gemfile
|
|
21
25
|
#
|
|
22
26
|
# Usage:
|
|
23
|
-
#
|
|
27
|
+
# ruby examples/22_context_compression.rb # no LLM calls
|
|
24
28
|
|
|
25
29
|
require_relative "common"
|
|
26
30
|
|
|
@@ -47,23 +51,15 @@ end
|
|
|
47
51
|
|
|
48
52
|
banner "Context Window Compression"
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
# Build a robot and simulate a long conversation on two topics
|
|
52
|
-
# ---------------------------------------------------------------------------
|
|
53
|
-
bot = RobotLab.build(
|
|
54
|
-
model: LLM[:default].model,
|
|
55
|
-
name: "assistant",
|
|
56
|
-
system_prompt: "You are a concise Ruby expert. Reply in 2-3 sentences."
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
puts "Simulating a long conversation (no real LLM calls)..."
|
|
54
|
+
puts "Scoring a synthetic two-topic conversation (no LLM calls)..."
|
|
60
55
|
puts
|
|
61
56
|
|
|
62
|
-
#
|
|
57
|
+
# A conversation history with two distinct topics:
|
|
63
58
|
# older turns: Ruby metaprogramming (will become irrelevant)
|
|
64
59
|
# recent turns: Rails routing (current topic)
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
#
|
|
61
|
+
# HistoryCompressor only reads role/content and the message predicates, so a
|
|
62
|
+
# small Struct stands in for RubyLLM::Message and keeps the fixture readable.
|
|
67
63
|
|
|
68
64
|
FakeMsg = Struct.new(:role, :content, :tool_calls, :stop_reason) do
|
|
69
65
|
def text? = true
|
|
@@ -151,13 +147,17 @@ puts " Kept roles: #{result_b.map(&:role).join(', ')}"
|
|
|
151
147
|
puts
|
|
152
148
|
|
|
153
149
|
# ---------------------------------------------------------------------------
|
|
154
|
-
#
|
|
150
|
+
# The in-place form on a live robot (not executed here — it would spend tokens
|
|
151
|
+
# summarizing, and the interesting scoring behaviour is already shown above)
|
|
155
152
|
# ---------------------------------------------------------------------------
|
|
156
|
-
section "
|
|
153
|
+
section "In-Place Form: robot.compress_history"
|
|
154
|
+
puts "Same algorithm, applied to a live robot's own chat history. A second,"
|
|
155
|
+
puts "cheaper robot does the summarizing for the medium-relevance band."
|
|
157
156
|
show_code <<~RUBY
|
|
158
157
|
summarizer_bot = RobotLab.build(
|
|
159
|
-
model: "gpt-5.4",
|
|
160
158
|
name: "summarizer",
|
|
159
|
+
provider: "ollama",
|
|
160
|
+
model: "qwen2.5:7b",
|
|
161
161
|
system_prompt: "Summarize the following text in one sentence."
|
|
162
162
|
)
|
|
163
163
|
|
data/examples/23_convergence.rb
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
# - Convergence.similarity(a, b) — 0.0..1.0 cosine similarity score
|
|
12
12
|
# - Convergence.detected?(a, b) — boolean above default threshold (0.85)
|
|
13
13
|
# - Convergence.detected?(a, b, threshold: 0.6) — custom threshold
|
|
14
|
-
# -
|
|
14
|
+
# - Fast-path pattern: skip the reconciler when verifiers agree, using
|
|
15
|
+
# SimpleFlow optional-task activation
|
|
15
16
|
#
|
|
16
17
|
# Requires:
|
|
17
18
|
# gem 'classifier', '~> 2.3' # add to your Gemfile
|
|
18
19
|
#
|
|
19
20
|
# Usage:
|
|
20
|
-
#
|
|
21
|
+
# ruby examples/23_convergence.rb # no LLM calls — pure similarity scoring
|
|
21
22
|
|
|
22
23
|
require_relative "common"
|
|
23
24
|
|
|
@@ -68,27 +69,33 @@ end
|
|
|
68
69
|
# ---------------------------------------------------------------------------
|
|
69
70
|
# Router fast-path pattern
|
|
70
71
|
# ---------------------------------------------------------------------------
|
|
71
|
-
section "
|
|
72
|
+
section "Reconciler Fast-Path Pattern"
|
|
73
|
+
puts "Networks route with SimpleFlow's optional-task activation, not a router"
|
|
74
|
+
puts "lambda: a task declared `depends_on: :optional` stays dormant until some"
|
|
75
|
+
puts "earlier task activates it. A gate robot compares the two verifier replies"
|
|
76
|
+
puts "and only activates the expensive reconciler when they disagree."
|
|
72
77
|
show_code <<~RUBY
|
|
73
|
-
# Two
|
|
74
|
-
#
|
|
78
|
+
# Two verifiers run in parallel; a gate compares their replies and decides
|
|
79
|
+
# whether the reconciler is worth an LLM call.
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
class ConvergenceGate < RobotLab::Robot
|
|
82
|
+
def call(result)
|
|
83
|
+
a = result.context[:verifier_a]&.reply.to_s
|
|
84
|
+
b = result.context[:verifier_b]&.reply.to_s
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
["reconciler"]
|
|
86
|
+
return result if RobotLab::Convergence.detected?(a, b) # agree — stop here
|
|
87
|
+
|
|
88
|
+
result.activate(:reconciler) # disagree — escalate
|
|
84
89
|
end
|
|
85
90
|
end
|
|
86
91
|
|
|
87
|
-
network = RobotLab.create_network(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
network = RobotLab.create_network(name: "fact_check") do
|
|
93
|
+
task :verifier_a, verifier_a, depends_on: :none
|
|
94
|
+
task :verifier_b, verifier_b, depends_on: :none
|
|
95
|
+
task :gate, ConvergenceGate.new(name: "gate", system_prompt: "gate"),
|
|
96
|
+
depends_on: %i[verifier_a verifier_b]
|
|
97
|
+
task :reconciler, reconciler, depends_on: :optional
|
|
98
|
+
end
|
|
92
99
|
|
|
93
100
|
result = network.run(message: "Is this claim accurate?")
|
|
94
101
|
RUBY
|