robot_lab 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.architecture/AGENTS.md +32 -0
- data/.architecture/config.yml +8 -0
- data/.architecture/members.yml +60 -0
- data/.architecture/reviews/feature-free-will.md +490 -0
- data/.architecture/reviews/overall-codebase.md +427 -0
- data/.claude/settings.local.json +57 -0
- data/.codex/config.toml +2 -0
- data/.irbrc +2 -2
- data/.rubocop.yml +172 -0
- data/CHANGELOG.md +72 -0
- data/CLAUDE.md +139 -0
- data/README.md +91 -95
- data/Rakefile +109 -3
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +20 -7
- data/docs/guides/index.md +16 -16
- data/docs/guides/knowledge.md +7 -1
- data/docs/guides/observability.md +132 -0
- data/docs/index.md +30 -3
- data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
- data/examples/.envrc +1 -0
- data/examples/01_simple_robot.rb +5 -9
- data/examples/02_tools.rb +5 -9
- data/examples/03_network.rb +8 -9
- data/examples/04_mcp.rb +21 -29
- data/examples/05_streaming.rb +12 -18
- data/examples/06_prompt_templates.rb +11 -19
- data/examples/07_network_memory.rb +16 -31
- data/examples/08_llm_config.rb +10 -22
- data/examples/09_chaining.rb +16 -27
- data/examples/10_memory.rb +12 -28
- data/examples/11_network_introspection.rb +15 -29
- data/examples/12_message_bus.rb +5 -12
- data/examples/13_spawn.rb +5 -10
- data/examples/14_rusty_circuit/.envrc +1 -0
- data/examples/14_rusty_circuit/comic.rb +2 -0
- data/examples/14_rusty_circuit/heckler.rb +1 -1
- data/examples/14_rusty_circuit/open_mic.rb +1 -3
- data/examples/14_rusty_circuit/scout.rb +2 -0
- data/examples/15_memory_network_and_bus/.envrc +1 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
- data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
- data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
- data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
- data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/.envrc +1 -0
- data/examples/16_writers_room/writers_room.rb +2 -4
- data/examples/17_skills.rb +8 -17
- data/examples/18_rails/Gemfile +1 -0
- data/examples/19_token_tracking.rb +9 -15
- data/examples/20_circuit_breaker.rb +10 -19
- data/examples/21_learning_loop.rb +11 -20
- data/examples/22_context_compression.rb +6 -13
- data/examples/23_convergence.rb +6 -17
- data/examples/24_structured_delegation.rb +11 -15
- data/examples/25_history_search.rb +5 -12
- data/examples/26_document_store.rb +6 -13
- data/examples/27_incident_response/incident_response.rb +4 -5
- data/examples/28_mcp_discovery.rb +8 -11
- data/examples/29_ractor_tools.rb +4 -9
- data/examples/30_ractor_network.rb +10 -19
- data/examples/31_launch_assessment.rb +10 -23
- data/examples/32_newsletter_reader.rb +188 -0
- data/examples/33_stock_generator.rb +80 -0
- data/examples/33_stock_predictor.rb +306 -0
- data/examples/34_agentskills.rb +72 -0
- data/examples/README.md +1 -1
- data/examples/common.rb +76 -0
- data/examples/ruboruby.md +423 -0
- data/examples/temp.md +51 -0
- data/lib/robot_lab/agent_skill.rb +63 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/config.rb +1 -12
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- data/lib/robot_lab/mcp/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +3 -3
- data/lib/robot_lab/mcp/server.rb +1 -1
- data/lib/robot_lab/mcp/server_discovery.rb +0 -2
- data/lib/robot_lab/memory.rb +32 -27
- data/lib/robot_lab/memory_change.rb +2 -2
- data/lib/robot_lab/message.rb +4 -4
- data/lib/robot_lab/network.rb +11 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/bus_messaging.rb +9 -27
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/mcp_management.rb +5 -11
- data/lib/robot_lab/robot/template_rendering.rb +60 -40
- data/lib/robot_lab/robot.rb +323 -206
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +5 -11
- data/lib/robot_lab/script_tool.rb +76 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/tool.rb +3 -3
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +5 -7
- data/lib/robot_lab/user_message.rb +2 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +1 -1
- data/lib/robot_lab.rb +41 -52
- data/logfile +8 -0
- data/mkdocs.yml +2 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/site/404.html +2300 -0
- data/site/api/core/index.html +2706 -0
- data/site/api/core/memory/index.html +3793 -0
- data/site/api/core/network/index.html +3500 -0
- data/site/api/core/robot/index.html +4566 -0
- data/site/api/core/state/index.html +3390 -0
- data/site/api/core/tool/index.html +3843 -0
- data/site/api/index.html +2635 -0
- data/site/api/mcp/client/index.html +3435 -0
- data/site/api/mcp/index.html +2783 -0
- data/site/api/mcp/server/index.html +3252 -0
- data/site/api/mcp/transports/index.html +3352 -0
- data/site/api/messages/index.html +2641 -0
- data/site/api/messages/text-message/index.html +3087 -0
- data/site/api/messages/tool-call-message/index.html +3159 -0
- data/site/api/messages/tool-result-message/index.html +3252 -0
- data/site/api/messages/user-message/index.html +3212 -0
- data/site/api/streaming/context/index.html +3282 -0
- data/site/api/streaming/events/index.html +3347 -0
- data/site/api/streaming/index.html +2738 -0
- data/site/architecture/core-concepts/index.html +3757 -0
- data/site/architecture/index.html +2797 -0
- data/site/architecture/message-flow/index.html +3238 -0
- data/site/architecture/network-orchestration/index.html +3433 -0
- data/site/architecture/robot-execution/index.html +3140 -0
- data/site/architecture/state-management/index.html +3498 -0
- data/site/assets/css/custom.css +56 -0
- 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 +16 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- data/site/assets/javascripts/lunr/tinyseg.js +206 -0
- data/site/assets/javascripts/lunr/wordcut.js +6708 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
- data/site/concepts/index.html +3455 -0
- data/site/examples/basic-chat/index.html +2880 -0
- data/site/examples/index.html +2907 -0
- data/site/examples/mcp-server/index.html +3018 -0
- data/site/examples/multi-robot-network/index.html +3131 -0
- data/site/examples/rails-application/index.html +3329 -0
- data/site/examples/tool-usage/index.html +3085 -0
- data/site/getting-started/configuration/index.html +3745 -0
- data/site/getting-started/index.html +2572 -0
- data/site/getting-started/installation/index.html +2981 -0
- data/site/getting-started/quick-start/index.html +2942 -0
- data/site/guides/building-robots/index.html +4290 -0
- data/site/guides/creating-networks/index.html +3858 -0
- data/site/guides/index.html +2586 -0
- data/site/guides/mcp-integration/index.html +3581 -0
- data/site/guides/memory/index.html +3586 -0
- data/site/guides/rails-integration/index.html +4019 -0
- data/site/guides/streaming/index.html +3157 -0
- data/site/guides/using-tools/index.html +3802 -0
- data/site/index.html +2671 -0
- data/site/search/search_index.json +1 -0
- data/site/sitemap.xml +183 -0
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +1 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +154 -92
- data/docs/examples/rails-application.md +0 -419
- data/docs/guides/ractor-parallelism.md +0 -364
- data/docs/guides/rails-integration.md +0 -681
- data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
- data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
- data/lib/generators/robot_lab/install_generator.rb +0 -90
- data/lib/generators/robot_lab/job_generator.rb +0 -40
- data/lib/generators/robot_lab/robot_generator.rb +0 -55
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
- data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
- data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
- data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
- data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
- data/lib/robot_lab/document_store.rb +0 -155
- data/lib/robot_lab/ractor_boundary.rb +0 -42
- data/lib/robot_lab/ractor_job.rb +0 -37
- data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
- data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
- data/lib/robot_lab/ractor_worker_pool.rb +0 -117
- data/lib/robot_lab/rails_integration/engine.rb +0 -29
- data/lib/robot_lab/rails_integration/job.rb +0 -158
- data/lib/robot_lab/rails_integration/railtie.rb +0 -51
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
|
@@ -38,11 +38,9 @@
|
|
|
38
38
|
|
|
39
39
|
ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "../prompts")
|
|
40
40
|
|
|
41
|
-
require_relative "
|
|
41
|
+
require_relative "../common"
|
|
42
42
|
require "fileutils"
|
|
43
43
|
|
|
44
|
-
RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
|
|
45
|
-
|
|
46
44
|
OUTPUT_DIR = File.join(__dir__, "output")
|
|
47
45
|
FileUtils.mkdir_p(OUTPUT_DIR)
|
|
48
46
|
|
|
@@ -62,6 +60,7 @@ class SREScout < RobotLab::Robot
|
|
|
62
60
|
def initialize(name:, subsystem:, memory_key:, bus: nil)
|
|
63
61
|
super(
|
|
64
62
|
name: name,
|
|
63
|
+
model: LLM[:default].model,
|
|
65
64
|
system_prompt: "You are a senior SRE responding to a production outage. " \
|
|
66
65
|
"Diagnose one infrastructure layer in 2 sentences: " \
|
|
67
66
|
"first sentence is root cause, second is customer impact.",
|
|
@@ -112,7 +111,7 @@ class WarRoom < RobotLab::Robot
|
|
|
112
111
|
attr_reader :updates
|
|
113
112
|
|
|
114
113
|
def initialize(bus:)
|
|
115
|
-
super(name: "war_room", system_prompt: "SRE war-room coordinator.", bus: bus)
|
|
114
|
+
super(name: "war_room", model: LLM[:default].model, system_prompt: "SRE war-room coordinator.", bus: bus)
|
|
116
115
|
@updates = []
|
|
117
116
|
@delivery_mutex = Mutex.new # only for reading @updates outside Async
|
|
118
117
|
|
|
@@ -178,7 +177,7 @@ db_scout = SREScout.new(name: "db_scout", subsystem: "database", memory_key
|
|
|
178
177
|
net_scout = SREScout.new(name: "net_scout", subsystem: "network", memory_key: :net_finding, bus: bus)
|
|
179
178
|
app_scout = SREScout.new(name: "app_scout", subsystem: "application", memory_key: :app_finding, bus: bus)
|
|
180
179
|
war_room = WarRoom.new(bus: bus)
|
|
181
|
-
commander = IncidentCommander.new(name: "commander", system_prompt: "SRE incident commander.")
|
|
180
|
+
commander = IncidentCommander.new(name: "commander", model: LLM[:default].model, system_prompt: "SRE incident commander.")
|
|
182
181
|
|
|
183
182
|
# Build the investigation network
|
|
184
183
|
# poller_group: labels are registered on the network's shared BusPoller.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# == Key config
|
|
13
13
|
#
|
|
14
14
|
# robot = RobotLab.build(
|
|
15
|
+
# model: "gpt-5.4",
|
|
15
16
|
# mcp_discovery: true, # ← enables semantic filtering
|
|
16
17
|
# mcp: [ ... ] # ← candidate servers, each with :description
|
|
17
18
|
# )
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
# Usage:
|
|
30
31
|
# bundle exec ruby examples/28_mcp_discovery.rb
|
|
31
32
|
|
|
32
|
-
require_relative "
|
|
33
|
+
require_relative "common"
|
|
33
34
|
|
|
34
35
|
# Three representative MCP server configurations
|
|
35
36
|
SERVERS = [
|
|
@@ -59,10 +60,8 @@ def show_query(label, query)
|
|
|
59
60
|
puts
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
puts "Example 28: MCP Server Discovery"
|
|
63
|
+
banner "MCP Server Discovery"
|
|
64
64
|
puts " Semantic server selection via TF cosine similarity"
|
|
65
|
-
puts "=" * 60
|
|
66
65
|
puts
|
|
67
66
|
puts "Candidate servers:"
|
|
68
67
|
SERVERS.each do |s|
|
|
@@ -70,14 +69,12 @@ SERVERS.each do |s|
|
|
|
70
69
|
end
|
|
71
70
|
puts
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
puts "-" * 60
|
|
72
|
+
section "Discovery Queries"
|
|
75
73
|
show_query("File ops", "read my config file")
|
|
76
74
|
show_query("Package mgmt", "install imagemagick via homebrew")
|
|
77
75
|
show_query("Code review", "list open pull requests on my repo")
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
puts "-" * 60
|
|
77
|
+
section "Fallback Cases"
|
|
81
78
|
|
|
82
79
|
# No description → all servers returned
|
|
83
80
|
no_desc_servers = SERVERS.map { |s| s.except(:description) }
|
|
@@ -93,10 +90,10 @@ result = RobotLab::MCP::ServerDiscovery.select("install imagemagick", from: SERV
|
|
|
93
90
|
puts " High threshold : returns all (#{result.size} servers) — no match above 1.0"
|
|
94
91
|
|
|
95
92
|
puts
|
|
96
|
-
|
|
97
|
-
puts "-" * 60
|
|
93
|
+
section "mcp_discovery: true on a Robot"
|
|
98
94
|
puts <<~NOTE
|
|
99
95
|
RobotLab.build(
|
|
96
|
+
model: "gpt-5.4",
|
|
100
97
|
name: "assistant",
|
|
101
98
|
mcp_discovery: true,
|
|
102
99
|
mcp: [
|
|
@@ -109,4 +106,4 @@ puts <<~NOTE
|
|
|
109
106
|
# Only the :brew server is connected for this message:
|
|
110
107
|
robot.run("install imagemagick")
|
|
111
108
|
NOTE
|
|
112
|
-
|
|
109
|
+
hr
|
data/examples/29_ractor_tools.rb
CHANGED
|
@@ -26,9 +26,8 @@
|
|
|
26
26
|
# SHA-256 rounds (~320 ms on modern hardware) so the 4-6× speedup is
|
|
27
27
|
# clearly visible on a 6-core machine.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
require_relative "../lib/robot_lab"
|
|
29
|
+
require_relative "common"
|
|
30
|
+
require "robot_lab/ractor"
|
|
32
31
|
require "digest"
|
|
33
32
|
|
|
34
33
|
# Always shut down the pool when the process exits.
|
|
@@ -115,10 +114,7 @@ end
|
|
|
115
114
|
# Demo
|
|
116
115
|
# =============================================================================
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
puts "Example 29: Ractor-Safe CPU Tools"
|
|
120
|
-
puts "=" * 62
|
|
121
|
-
puts
|
|
117
|
+
banner "Ractor-Safe CPU Tools"
|
|
122
118
|
|
|
123
119
|
DIVIDER = ("─" * 54).freeze
|
|
124
120
|
|
|
@@ -238,6 +234,5 @@ puts " #{DIVIDER}"
|
|
|
238
234
|
RobotLab.shutdown_ractor_pool
|
|
239
235
|
puts " Pool shut down cleanly (poison-pill × #{pool.size} workers)."
|
|
240
236
|
puts
|
|
241
|
-
|
|
237
|
+
hr
|
|
242
238
|
puts "Example 29 complete."
|
|
243
|
-
puts "=" * 62
|
|
@@ -30,14 +30,10 @@
|
|
|
30
30
|
# bundle exec ruby examples/30_ractor_network.rb # Parts 1 & 2
|
|
31
31
|
# ANTHROPIC_API_KEY=key ruby examples/30_ractor_network.rb # Parts 1, 2 & 3
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
require_relative "common"
|
|
34
|
+
require "robot_lab/ractor"
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
puts "=" * 62
|
|
38
|
-
puts "Example 30: Ractor Network Scheduler"
|
|
39
|
-
puts "=" * 62
|
|
40
|
-
puts
|
|
36
|
+
banner "Ractor Network Scheduler"
|
|
41
37
|
|
|
42
38
|
DIVIDER = ("─" * 54).freeze
|
|
43
39
|
|
|
@@ -73,8 +69,7 @@ LATENCIES = {
|
|
|
73
69
|
#
|
|
74
70
|
# Speedup ≈ 1.7×
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
puts
|
|
72
|
+
section "Part 1: Simulated Parallel Run (no API key)"
|
|
78
73
|
|
|
79
74
|
# SimulatedScheduler overrides execute_spec so that instead of
|
|
80
75
|
# constructing a real Robot and calling the LLM, it just sleeps for
|
|
@@ -140,14 +135,13 @@ puts
|
|
|
140
135
|
# Part 2: Network.new(parallel_mode: :ractor) API
|
|
141
136
|
# =============================================================================
|
|
142
137
|
|
|
143
|
-
|
|
144
|
-
puts
|
|
138
|
+
section "Part 2: Network.new(parallel_mode: :ractor)"
|
|
145
139
|
|
|
146
140
|
# When parallel_mode: :ractor is set on a Network, network.run(message:)
|
|
147
141
|
# routes through RactorNetworkScheduler instead of SimpleFlow::Pipeline.
|
|
148
142
|
# The default mode is :async (unchanged SimpleFlow behavior).
|
|
149
143
|
|
|
150
|
-
model =
|
|
144
|
+
model = LLM[:default].model
|
|
151
145
|
|
|
152
146
|
network = RobotLab::Network.new(name: "research_pipeline", parallel_mode: :ractor) do
|
|
153
147
|
task :headline_finder, RobotLab.build(name: "headline_finder",
|
|
@@ -197,19 +191,17 @@ puts
|
|
|
197
191
|
# =============================================================================
|
|
198
192
|
|
|
199
193
|
unless ENV["ANTHROPIC_API_KEY"]
|
|
200
|
-
|
|
194
|
+
section "Part 3: Live LLM Run"
|
|
201
195
|
puts " Set ANTHROPIC_API_KEY to run the real pipeline."
|
|
202
196
|
puts " Expected behavior: headline_finder, background_brief, and"
|
|
203
197
|
puts " fact_checker run in parallel; report_writer follows."
|
|
204
198
|
puts
|
|
205
|
-
|
|
199
|
+
hr
|
|
206
200
|
puts "Example 30 complete."
|
|
207
|
-
puts "=" * 62
|
|
208
201
|
exit 0
|
|
209
202
|
end
|
|
210
203
|
|
|
211
|
-
|
|
212
|
-
puts
|
|
204
|
+
section "Part 3: Live LLM Run (ANTHROPIC_API_KEY detected)"
|
|
213
205
|
|
|
214
206
|
puts " Running 4-robot research pipeline on:"
|
|
215
207
|
puts " \"#{topic}\""
|
|
@@ -251,6 +243,5 @@ rescue RobotLab::Error => e
|
|
|
251
243
|
end
|
|
252
244
|
|
|
253
245
|
puts
|
|
254
|
-
|
|
246
|
+
hr
|
|
255
247
|
puts "Example 30 complete."
|
|
256
|
-
puts "=" * 62
|
|
@@ -47,11 +47,7 @@
|
|
|
47
47
|
# Usage:
|
|
48
48
|
# ANTHROPIC_API_KEY=your_key ruby examples/31_launch_assessment.rb
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
require_relative "../lib/robot_lab"
|
|
53
|
-
|
|
54
|
-
RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
|
|
50
|
+
require_relative "common"
|
|
55
51
|
|
|
56
52
|
# ── AnalystRobot ────────────────────────────────────────────────────────────────
|
|
57
53
|
#
|
|
@@ -65,6 +61,7 @@ class AnalystRobot < RobotLab::Robot
|
|
|
65
61
|
def initialize(name:, memory_key:, role:)
|
|
66
62
|
super(
|
|
67
63
|
name: name,
|
|
64
|
+
model: LLM[:default].model,
|
|
68
65
|
system_prompt: "You are a #{role}. " \
|
|
69
66
|
"Review the product brief in 2-3 crisp sentences from your area of expertise. " \
|
|
70
67
|
"Close with a one-word verdict: READY or NOT-READY."
|
|
@@ -172,6 +169,7 @@ end
|
|
|
172
169
|
|
|
173
170
|
director = LaunchDirector.new(
|
|
174
171
|
name: "launch_director",
|
|
172
|
+
model: LLM[:default].model,
|
|
175
173
|
system_prompt: "You are the VP of Product making the final launch call."
|
|
176
174
|
)
|
|
177
175
|
|
|
@@ -202,10 +200,8 @@ end
|
|
|
202
200
|
|
|
203
201
|
# ── Run ─────────────────────────────────────────────────────────────────────────
|
|
204
202
|
|
|
205
|
-
|
|
206
|
-
puts "Example 31: Product Launch Assessment"
|
|
203
|
+
banner "Product Launch Assessment"
|
|
207
204
|
puts " 6 specialist analysts in parallel, max_concurrent_robots: 4"
|
|
208
|
-
puts "=" * 68
|
|
209
205
|
puts
|
|
210
206
|
puts "Pipeline:"
|
|
211
207
|
puts network.visualize
|
|
@@ -215,30 +211,21 @@ puts
|
|
|
215
211
|
puts "Product brief:"
|
|
216
212
|
puts PRODUCT_BRIEF.strip.gsub(/^/, " ")
|
|
217
213
|
puts
|
|
218
|
-
|
|
219
|
-
puts "Running — analysts 5 and 6 queue until a semaphore slot opens..."
|
|
220
|
-
puts "-" * 68
|
|
221
|
-
puts
|
|
214
|
+
section "Running — Analysts 5 and 6 Queue Until a Semaphore Slot Opens"
|
|
222
215
|
|
|
223
216
|
$run_start = Time.now
|
|
224
217
|
result = network.run(message: PRODUCT_BRIEF)
|
|
225
218
|
elapsed = "%.1f" % (Time.now - $run_start)
|
|
226
219
|
|
|
227
220
|
puts
|
|
228
|
-
|
|
221
|
+
hr
|
|
229
222
|
puts "All analysts complete. Total wall time: #{elapsed}s"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
puts "LAUNCH DIRECTOR RECOMMENDATION"
|
|
234
|
-
puts "=" * 68
|
|
235
|
-
puts
|
|
223
|
+
hr
|
|
224
|
+
|
|
225
|
+
section "Launch Director Recommendation"
|
|
236
226
|
puts network.memory[:recommendation]
|
|
237
227
|
puts
|
|
238
|
-
|
|
239
|
-
puts "INDIVIDUAL ANALYST VERDICTS"
|
|
240
|
-
puts "=" * 68
|
|
241
|
-
puts
|
|
228
|
+
section "Individual Analyst Verdicts"
|
|
242
229
|
analyst_robots.each do |robot|
|
|
243
230
|
label = robot.name.gsub("_", " ").upcase
|
|
244
231
|
finding = network.memory.get(robot.memory_key).to_s
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 32: Newsletter Issue Retriever
|
|
5
|
+
#
|
|
6
|
+
# Fetches all unprocessed issues from multiple Ruby newsletters via RSS
|
|
7
|
+
# and saves each as a Markdown file in the Obsidian Clippings folder.
|
|
8
|
+
#
|
|
9
|
+
# - Processes oldest unprocessed issue first (across all feeds)
|
|
10
|
+
# - Filename: <newsletter-name>_YYYYMMDD.md
|
|
11
|
+
# - Includes YAML frontmatter with source URL (compatible with Clippings workflow)
|
|
12
|
+
# - Tracks processed issue URLs in ~/.robot_lab/newsletter_processed.yaml
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# ruby examples/32_newsletter_reader.rb
|
|
16
|
+
|
|
17
|
+
require "net/http"
|
|
18
|
+
require "open3"
|
|
19
|
+
require "time"
|
|
20
|
+
require "uri"
|
|
21
|
+
require "yaml"
|
|
22
|
+
require "fileutils"
|
|
23
|
+
require "rexml/document"
|
|
24
|
+
require "rexml/xpath"
|
|
25
|
+
|
|
26
|
+
NEWSLETTER_RSS_URLS = [
|
|
27
|
+
"https://rss.beehiiv.com/feeds/MTJunJRFxo.xml", # Ruby AI News
|
|
28
|
+
"https://cprss.s3.amazonaws.com/rubyweekly.com.xml" # Ruby Weekly
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
CLIPPINGS_DIR = File.expand_path("/Users/dewayne/Documents/obsidian_order_intelligence/PKM/Clippings")
|
|
32
|
+
PROCESSED_STATE_FILE = File.join(Dir.home, ".robot_lab", "newsletter_processed.yaml")
|
|
33
|
+
|
|
34
|
+
# Tracks which newsletter issue URLs have been saved.
|
|
35
|
+
class ProcessedIssues
|
|
36
|
+
def initialize(path: PROCESSED_STATE_FILE)
|
|
37
|
+
@path = path
|
|
38
|
+
@urls = load_urls
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def processed?(url)
|
|
42
|
+
@urls.include?(url)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mark_processed(url)
|
|
46
|
+
@urls << url
|
|
47
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
48
|
+
File.write(@path, YAML.dump(@urls.uniq))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def count = @urls.size
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def load_urls
|
|
56
|
+
return [] unless File.exist?(@path)
|
|
57
|
+
Array(YAML.safe_load(File.read(@path)) || [])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Fetches a single RSS feed URL and returns its items (unsorted).
|
|
62
|
+
# Each item: { title:, url:, pub_date:, published_at:, html:, channel_name: }
|
|
63
|
+
def fetch_rss_items(url)
|
|
64
|
+
uri = URI(url)
|
|
65
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.get(uri.request_uri) }
|
|
66
|
+
raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
67
|
+
|
|
68
|
+
doc = REXML::Document.new(response.body)
|
|
69
|
+
ns = { "content" => "http://purl.org/rss/1.0/modules/content/" }
|
|
70
|
+
|
|
71
|
+
channel_title = REXML::XPath.first(doc, "//channel/title")&.text&.strip || "newsletter"
|
|
72
|
+
|
|
73
|
+
REXML::XPath.match(doc, "//channel/item").filter_map do |item|
|
|
74
|
+
item_url = REXML::XPath.first(item, "link")&.text&.strip
|
|
75
|
+
title = REXML::XPath.first(item, "title")&.text&.strip
|
|
76
|
+
pub_date = REXML::XPath.first(item, "pubDate")&.text&.strip
|
|
77
|
+
html = REXML::XPath.first(item, "content:encoded", ns)&.text ||
|
|
78
|
+
REXML::XPath.first(item, "description")&.text || ""
|
|
79
|
+
|
|
80
|
+
next unless item_url && title && pub_date
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
title: title,
|
|
84
|
+
url: item_url,
|
|
85
|
+
pub_date: pub_date,
|
|
86
|
+
published_at: Time.parse(pub_date),
|
|
87
|
+
html: html,
|
|
88
|
+
channel_name: channel_title
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Fetches all configured RSS feeds and returns items sorted oldest-first.
|
|
94
|
+
def fetch_all_rss_items
|
|
95
|
+
NEWSLETTER_RSS_URLS.flat_map do |feed_url|
|
|
96
|
+
print " #{feed_url}... "
|
|
97
|
+
items = fetch_rss_items(feed_url)
|
|
98
|
+
puts "#{items.size} issues"
|
|
99
|
+
items
|
|
100
|
+
end.sort_by { |item| item[:published_at] }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Converts HTML to Markdown via html2markdown CLI, stripping UTM params.
|
|
104
|
+
def html_to_markdown(html)
|
|
105
|
+
md, = Open3.capture3(
|
|
106
|
+
"html2markdown",
|
|
107
|
+
stdin_data: html
|
|
108
|
+
)
|
|
109
|
+
md.gsub(/\]\(([^)]+)\)/) do
|
|
110
|
+
url = $1
|
|
111
|
+
if url.include?("?")
|
|
112
|
+
base, query = url.split("?", 2)
|
|
113
|
+
kept = query.split("&").reject { |p| p.start_with?("utm_") }
|
|
114
|
+
"](#{kept.empty? ? base : "#{base}?#{kept.join("&")}"})"
|
|
115
|
+
else
|
|
116
|
+
"](#{url})"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Builds the output filename: <newsletter-name>_YYYYMMDD.md
|
|
122
|
+
def output_filename(channel_name, published_at)
|
|
123
|
+
safe_name = channel_name.downcase.gsub(/[^a-z0-9]+/, "_").delete_suffix("_")
|
|
124
|
+
date_str = published_at.strftime("%Y%m%d")
|
|
125
|
+
"#{safe_name}_#{date_str}.md"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Wraps markdown content in YAML frontmatter compatible with the Clippings workflow.
|
|
129
|
+
def wrap_with_frontmatter(title:, url:, pub_date:, body:)
|
|
130
|
+
date = Time.parse(pub_date).strftime("%Y-%m-%d")
|
|
131
|
+
<<~MD
|
|
132
|
+
---
|
|
133
|
+
source: #{url}
|
|
134
|
+
title: "#{title.gsub('"', '\\"')}"
|
|
135
|
+
date: #{date}
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
# #{title}
|
|
139
|
+
|
|
140
|
+
#{body.strip}
|
|
141
|
+
MD
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
puts "=" * 60
|
|
147
|
+
puts "Example 32: Newsletter Issue Retriever"
|
|
148
|
+
puts "=" * 60
|
|
149
|
+
puts
|
|
150
|
+
|
|
151
|
+
FileUtils.mkdir_p(CLIPPINGS_DIR)
|
|
152
|
+
state = ProcessedIssues.new
|
|
153
|
+
|
|
154
|
+
puts "Fetching #{NEWSLETTER_RSS_URLS.size} RSS feeds..."
|
|
155
|
+
all_items = fetch_all_rss_items
|
|
156
|
+
pending = all_items.reject { |item| state.processed?(item[:url]) }
|
|
157
|
+
puts "#{all_items.size} total issues found across all feeds."
|
|
158
|
+
|
|
159
|
+
if pending.empty?
|
|
160
|
+
puts "All issues already saved. Nothing to do."
|
|
161
|
+
exit
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
puts "#{pending.size} unprocessed (#{state.count} already done). Saving oldest-first."
|
|
165
|
+
puts
|
|
166
|
+
|
|
167
|
+
pending.each_with_index do |item, idx|
|
|
168
|
+
filename = output_filename(item[:channel_name], item[:published_at])
|
|
169
|
+
filepath = File.join(CLIPPINGS_DIR, filename)
|
|
170
|
+
|
|
171
|
+
print "[#{idx + 1}/#{pending.size}] #{item[:title]} (#{item[:published_at].strftime("%Y-%m-%d")})... "
|
|
172
|
+
|
|
173
|
+
body = html_to_markdown(item[:html])
|
|
174
|
+
content = wrap_with_frontmatter(
|
|
175
|
+
title: item[:title],
|
|
176
|
+
url: item[:url],
|
|
177
|
+
pub_date: item[:pub_date],
|
|
178
|
+
body: body
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
File.write(filepath, content)
|
|
182
|
+
state.mark_processed(item[:url])
|
|
183
|
+
|
|
184
|
+
puts "saved → #{filename}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
puts
|
|
188
|
+
puts "Done. #{pending.size} issue(s) saved to #{CLIPPINGS_DIR}"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 33: XYZZY Stock Price Generator
|
|
5
|
+
#
|
|
6
|
+
# Publishes fake streaming prices for ticker XYZZY to a Redis channel
|
|
7
|
+
# using Geometric Brownian Motion with occasional volatility regime shifts.
|
|
8
|
+
#
|
|
9
|
+
# Prerequisites:
|
|
10
|
+
# gem install redis
|
|
11
|
+
# Redis server running on localhost:6379
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# ruby examples/33_stock_generator.rb
|
|
15
|
+
|
|
16
|
+
require "redis"
|
|
17
|
+
require "json"
|
|
18
|
+
require "time"
|
|
19
|
+
|
|
20
|
+
CHANNEL = "stock:xyzzy"
|
|
21
|
+
START_PRICE = 100.0
|
|
22
|
+
BASE_VOL = 0.008 # baseline volatility per tick (~0.8%)
|
|
23
|
+
DRIFT = 0.0001 # slight upward drift per tick
|
|
24
|
+
|
|
25
|
+
# Box-Muller transform — standard normal sample
|
|
26
|
+
def randn
|
|
27
|
+
Math.sqrt(-2.0 * Math.log(rand)) * Math.cos(2.0 * Math::PI * rand)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Occasionally shift the volatility regime to create interesting price dynamics
|
|
31
|
+
def current_volatility(tick)
|
|
32
|
+
case tick % 60
|
|
33
|
+
when 0..10 then BASE_VOL * 2.0 # high volatility burst
|
|
34
|
+
when 30..35 then BASE_VOL * 0.4 # low volatility squeeze
|
|
35
|
+
else BASE_VOL
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
redis = Redis.new
|
|
42
|
+
price = START_PRICE
|
|
43
|
+
tick = 0
|
|
44
|
+
|
|
45
|
+
trap("INT") { puts "\nGenerator stopped."; exit }
|
|
46
|
+
|
|
47
|
+
puts "=" * 50
|
|
48
|
+
puts "XYZZY Stock Generator"
|
|
49
|
+
puts "=" * 50
|
|
50
|
+
puts "Channel : #{CHANNEL}"
|
|
51
|
+
puts "Interval: 5 seconds per tick"
|
|
52
|
+
puts "Model : Geometric Brownian Motion"
|
|
53
|
+
puts "Press Ctrl-C to stop."
|
|
54
|
+
puts "-" * 50
|
|
55
|
+
|
|
56
|
+
loop do
|
|
57
|
+
tick += 1
|
|
58
|
+
|
|
59
|
+
vol = current_volatility(tick)
|
|
60
|
+
price = (price * Math.exp((DRIFT - 0.5 * vol**2) + vol * randn)).round(2)
|
|
61
|
+
price = [price, 1.0].max # floor at $1.00
|
|
62
|
+
|
|
63
|
+
regime = case vol
|
|
64
|
+
when BASE_VOL * 2.0 then " [HIGH VOL]"
|
|
65
|
+
when BASE_VOL * 0.4 then " [low vol]"
|
|
66
|
+
else ""
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
payload = JSON.generate(
|
|
70
|
+
ticker: "XYZZY",
|
|
71
|
+
price: price,
|
|
72
|
+
tick: tick,
|
|
73
|
+
timestamp: Time.now.iso8601
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
redis.publish(CHANNEL, payload)
|
|
77
|
+
puts "Tick %5d $%8.2f vol=%.3f%s" % [tick, price, vol, regime]
|
|
78
|
+
|
|
79
|
+
sleep 5
|
|
80
|
+
end
|