robot_lab 0.0.12 → 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 +108 -0
- data/CLAUDE.md +139 -0
- data/README.md +91 -45
- data/Rakefile +109 -3
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/api/messages/index.md +21 -0
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +20 -7
- data/docs/guides/creating-networks.md +23 -0
- 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/18_rails/app/jobs/robot_run_job.rb +15 -75
- 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 +235 -0
- 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 +10 -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 +5 -5
- data/lib/robot_lab/network.rb +12 -7
- 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 -48
- 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 +155 -90
- data/.github/workflows/deploy-yard-docs.yml +0 -52
- data/docs/examples/rails-application.md +0 -419
- data/docs/guides/ractor-parallelism.md +0 -364
- data/docs/guides/rails-integration.md +0 -642
- 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/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 -92
- 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_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/railtie.rb +0 -42
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
|
@@ -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
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 33: XYZZY Stock Price Predictor
|
|
5
|
+
#
|
|
6
|
+
# Consumes fake streaming prices for ticker XYZZY from a Redis channel,
|
|
7
|
+
# predicts the high and low over the next price window using an SMA + EMA
|
|
8
|
+
# ensemble, and uses a RobotLab learning robot to tune predictor parameters
|
|
9
|
+
# after each window closes.
|
|
10
|
+
#
|
|
11
|
+
# Run alongside:
|
|
12
|
+
# ruby examples/33_stock_generator.rb (in a separate terminal)
|
|
13
|
+
#
|
|
14
|
+
# Prerequisites:
|
|
15
|
+
# gem install redis
|
|
16
|
+
# Redis server running on localhost:6379
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# ruby examples/33_stock_predictor.rb
|
|
20
|
+
|
|
21
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
22
|
+
require "robot_lab"
|
|
23
|
+
require "robot_lab/durable"
|
|
24
|
+
require "redis"
|
|
25
|
+
require "json"
|
|
26
|
+
|
|
27
|
+
CHANNEL = "stock:xyzzy"
|
|
28
|
+
WINDOW_SIZE = 12 # ticks per prediction window
|
|
29
|
+
|
|
30
|
+
# ── Mutable predictor parameters ──────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
module PredictorConfig
|
|
33
|
+
@sma_window = 10
|
|
34
|
+
@sma_std_multiplier = 1.5
|
|
35
|
+
@ema_alpha = 0.2
|
|
36
|
+
@ema_vol_multiplier = 2.0
|
|
37
|
+
@sma_weight = 0.5
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
attr_accessor :sma_window, :sma_std_multiplier, :ema_alpha,
|
|
41
|
+
:ema_vol_multiplier, :sma_weight
|
|
42
|
+
|
|
43
|
+
def summary
|
|
44
|
+
format(
|
|
45
|
+
"sma_window=%d sma_std=%.2f ema_alpha=%.2f ema_vol=%.2f sma_weight=%.2f",
|
|
46
|
+
sma_window, sma_std_multiplier, ema_alpha, ema_vol_multiplier, sma_weight
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ── SMA predictor ──────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
module SMAPredictor
|
|
55
|
+
def self.predict(prices)
|
|
56
|
+
window = prices.last(PredictorConfig.sma_window)
|
|
57
|
+
mean = window.sum / window.size.to_f
|
|
58
|
+
var = window.sum { |p| (p - mean)**2 } / window.size.to_f
|
|
59
|
+
std = Math.sqrt(var)
|
|
60
|
+
mult = PredictorConfig.sma_std_multiplier
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
high: (mean + mult * std).round(2),
|
|
64
|
+
low: [mean - mult * std, 1.0].max.round(2)
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ── EMA predictor (stateful — updated every tick) ──────────────────────────────
|
|
70
|
+
|
|
71
|
+
module EMAPredictor
|
|
72
|
+
@ema = nil
|
|
73
|
+
@var_ema = nil
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
def update(price)
|
|
77
|
+
alpha = PredictorConfig.ema_alpha
|
|
78
|
+
if @ema.nil?
|
|
79
|
+
@ema = price
|
|
80
|
+
@var_ema = 0.0
|
|
81
|
+
else
|
|
82
|
+
delta = price - @ema
|
|
83
|
+
@ema = alpha * price + (1 - alpha) * @ema
|
|
84
|
+
@var_ema = alpha * delta**2 + (1 - alpha) * @var_ema
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def predict
|
|
89
|
+
return nil if @ema.nil?
|
|
90
|
+
|
|
91
|
+
vol = Math.sqrt(@var_ema) * PredictorConfig.ema_vol_multiplier
|
|
92
|
+
{
|
|
93
|
+
high: (@ema + vol).round(2),
|
|
94
|
+
low: [@ema - vol, 1.0].max.round(2)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ── Ensemble predictor ─────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
module EnsemblePredictor
|
|
103
|
+
def self.predict(prices)
|
|
104
|
+
sma = SMAPredictor.predict(prices)
|
|
105
|
+
ema = EMAPredictor.predict
|
|
106
|
+
return sma unless ema
|
|
107
|
+
|
|
108
|
+
w = PredictorConfig.sma_weight
|
|
109
|
+
{
|
|
110
|
+
high: (w * sma[:high] + (1 - w) * ema[:high]).round(2),
|
|
111
|
+
low: (w * sma[:low] + (1 - w) * ema[:low]).round(2)
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# ── AdjustParameters tool ──────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
class AdjustParameters < RobotLab::Tool
|
|
119
|
+
description "Adjust one predictor parameter to improve future prediction accuracy. " \
|
|
120
|
+
"Make at most one or two targeted changes per window."
|
|
121
|
+
|
|
122
|
+
param :parameter, type: "string",
|
|
123
|
+
desc: "Parameter to adjust: sma_window, sma_std_multiplier, ema_alpha, ema_vol_multiplier, sma_weight"
|
|
124
|
+
param :value, type: "number",
|
|
125
|
+
desc: "New value (sma_window: 3-30 int; std/vol multipliers: 0.5-4.0; ema_alpha: 0.05-0.5; sma_weight: 0.0-1.0)"
|
|
126
|
+
param :reasoning, type: "string",
|
|
127
|
+
desc: "Why this change should reduce prediction error"
|
|
128
|
+
|
|
129
|
+
LIMITS = {
|
|
130
|
+
"sma_window" => { min: 3, max: 30, integer: true },
|
|
131
|
+
"sma_std_multiplier" => { min: 0.5, max: 4.0, integer: false },
|
|
132
|
+
"ema_alpha" => { min: 0.05, max: 0.5, integer: false },
|
|
133
|
+
"ema_vol_multiplier" => { min: 0.5, max: 4.0, integer: false },
|
|
134
|
+
"sma_weight" => { min: 0.0, max: 1.0, integer: false }
|
|
135
|
+
}.freeze
|
|
136
|
+
|
|
137
|
+
def execute(parameter:, value:, reasoning:)
|
|
138
|
+
spec = LIMITS[parameter]
|
|
139
|
+
return "Unknown parameter '#{parameter}'. Valid: #{LIMITS.keys.join(", ")}" unless spec
|
|
140
|
+
|
|
141
|
+
clamped = value.to_f.clamp(spec[:min], spec[:max])
|
|
142
|
+
clamped = clamped.round if spec[:integer]
|
|
143
|
+
|
|
144
|
+
PredictorConfig.send(:"#{parameter}=", clamped)
|
|
145
|
+
|
|
146
|
+
"Set #{parameter} = #{clamped}. #{reasoning}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ── Error metrics ──────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
WindowResult = Data.define(
|
|
153
|
+
:window_num,
|
|
154
|
+
:predicted_high, :predicted_low,
|
|
155
|
+
:actual_high, :actual_low,
|
|
156
|
+
:high_err, :low_err, :mean_err
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def evaluate_window(window_num, predicted, actuals)
|
|
160
|
+
actual_high = actuals.max.round(2)
|
|
161
|
+
actual_low = actuals.min.round(2)
|
|
162
|
+
high_err = (predicted[:high] - actual_high).abs.round(2)
|
|
163
|
+
low_err = (predicted[:low] - actual_low).abs.round(2)
|
|
164
|
+
mean_err = ((high_err + low_err) / 2.0).round(2)
|
|
165
|
+
|
|
166
|
+
WindowResult.new(
|
|
167
|
+
window_num:,
|
|
168
|
+
predicted_high: predicted[:high], predicted_low: predicted[:low],
|
|
169
|
+
actual_high:, actual_low:,
|
|
170
|
+
high_err:, low_err:, mean_err:
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def tuner_prompt(result)
|
|
175
|
+
<<~PROMPT
|
|
176
|
+
Window #{result.window_num} just closed.
|
|
177
|
+
|
|
178
|
+
Prediction vs Actual:
|
|
179
|
+
Predicted: high=$#{result.predicted_high} low=$#{result.predicted_low}
|
|
180
|
+
Actual: high=$#{result.actual_high} low=$#{result.actual_low}
|
|
181
|
+
Error: high_err=$#{result.high_err} low_err=$#{result.low_err} mean_err=$#{result.mean_err}
|
|
182
|
+
|
|
183
|
+
Current parameters:
|
|
184
|
+
#{PredictorConfig.summary}
|
|
185
|
+
|
|
186
|
+
Window size: #{WINDOW_SIZE} ticks.
|
|
187
|
+
|
|
188
|
+
First call RecallKnowledge to check what has worked before.
|
|
189
|
+
Then decide whether to adjust a parameter via AdjustParameters.
|
|
190
|
+
If the error is acceptable or you are uncertain, do nothing.
|
|
191
|
+
If you notice a clear pattern worth preserving, call RecordKnowledge.
|
|
192
|
+
PROMPT
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
puts "=" * 60
|
|
198
|
+
puts "XYZZY Stock Predictor"
|
|
199
|
+
puts "=" * 60
|
|
200
|
+
puts "Channel : #{CHANNEL}"
|
|
201
|
+
puts "Window : #{WINDOW_SIZE} ticks"
|
|
202
|
+
puts "Model : SMA + EMA Ensemble with Durable Learning"
|
|
203
|
+
puts "Warmup : #{PredictorConfig.sma_window} ticks"
|
|
204
|
+
puts "Press Ctrl-C to stop."
|
|
205
|
+
puts "-" * 60
|
|
206
|
+
|
|
207
|
+
redis = Redis.new
|
|
208
|
+
prices = []
|
|
209
|
+
robot = RobotLab.build(
|
|
210
|
+
model: "gpt-5.4",
|
|
211
|
+
name: "predictor_tuner",
|
|
212
|
+
system_prompt: <<~PROMPT,
|
|
213
|
+
You are a quantitative analyst tuning an ensemble stock price range
|
|
214
|
+
predictor for ticker XYZZY. Each prediction covers the high and low
|
|
215
|
+
price over the next #{WINDOW_SIZE} ticks.
|
|
216
|
+
|
|
217
|
+
The ensemble combines a Simple Moving Average (SMA) band and an
|
|
218
|
+
Exponential Moving Average (EMA) band. Adjustable parameters:
|
|
219
|
+
|
|
220
|
+
sma_window (3-30 int) — lookback period for SMA
|
|
221
|
+
sma_std_multiplier (0.5-4.0) — band width relative to SMA stddev
|
|
222
|
+
ema_alpha (0.05-0.5) — EMA smoothing (higher = more reactive)
|
|
223
|
+
ema_vol_multiplier (0.5-4.0) — band width relative to EMA volatility
|
|
224
|
+
sma_weight (0.0-1.0) — SMA share in ensemble (EMA = 1 - weight)
|
|
225
|
+
|
|
226
|
+
Workflow per window:
|
|
227
|
+
1. Call RecallKnowledge to check past findings before acting.
|
|
228
|
+
2. If the error is clearly too high/low in one direction, adjust the
|
|
229
|
+
relevant band multiplier via AdjustParameters.
|
|
230
|
+
3. Make at most two adjustments per window to isolate cause and effect.
|
|
231
|
+
4. If you observe a reliable pattern, call RecordKnowledge to preserve it.
|
|
232
|
+
5. When uncertain, do nothing rather than guess.
|
|
233
|
+
PROMPT
|
|
234
|
+
local_tools: [AdjustParameters],
|
|
235
|
+
learn: true,
|
|
236
|
+
learn_domain: "xyzzy stock prediction"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
warmed_up = false
|
|
240
|
+
pending_pred = nil # { prediction: {high:, low:}, window_prices: [] }
|
|
241
|
+
window_num = 0
|
|
242
|
+
|
|
243
|
+
trap("INT") { puts "\nPredictor stopped."; exit }
|
|
244
|
+
|
|
245
|
+
puts "Connecting to Redis and subscribing to #{CHANNEL}..."
|
|
246
|
+
|
|
247
|
+
redis.subscribe(CHANNEL) do |on|
|
|
248
|
+
on.message do |_channel, payload|
|
|
249
|
+
data = JSON.parse(payload, symbolize_names: true)
|
|
250
|
+
tick = data[:tick]
|
|
251
|
+
price = data[:price].to_f
|
|
252
|
+
|
|
253
|
+
EMAPredictor.update(price)
|
|
254
|
+
prices << price
|
|
255
|
+
|
|
256
|
+
# ── Warmup phase ──────────────────────────────────────────────
|
|
257
|
+
unless warmed_up
|
|
258
|
+
if prices.size < PredictorConfig.sma_window
|
|
259
|
+
puts "Tick %5d $%8.2f [warming up %d/%d]" % [tick, price, prices.size, PredictorConfig.sma_window]
|
|
260
|
+
next
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
warmed_up = true
|
|
264
|
+
pred = EnsemblePredictor.predict(prices)
|
|
265
|
+
pending_pred = { prediction: pred, window_prices: [] }
|
|
266
|
+
|
|
267
|
+
puts "Tick %5d $%8.2f [warmup done]" % [tick, price]
|
|
268
|
+
puts " First prediction → high=$#{pred[:high]} low=$#{pred[:low]}"
|
|
269
|
+
next
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# ── Accumulate current window ──────────────────────────────────
|
|
273
|
+
pending_pred[:window_prices] << price
|
|
274
|
+
progress = pending_pred[:window_prices].size
|
|
275
|
+
pred = pending_pred[:prediction]
|
|
276
|
+
|
|
277
|
+
puts "Tick %5d $%8.2f [%2d/#{WINDOW_SIZE}] (pred high=$#{pred[:high]} low=$#{pred[:low]})" %
|
|
278
|
+
[tick, price, progress]
|
|
279
|
+
|
|
280
|
+
next unless progress >= WINDOW_SIZE
|
|
281
|
+
|
|
282
|
+
# ── Window closed — evaluate ───────────────────────────────────
|
|
283
|
+
window_num += 1
|
|
284
|
+
result = evaluate_window(window_num, pred, pending_pred[:window_prices])
|
|
285
|
+
|
|
286
|
+
puts "\n#{"─" * 60}"
|
|
287
|
+
puts " Window #{result.window_num} result:"
|
|
288
|
+
puts " Predicted high=$%-8.2f low=$%-.2f" % [result.predicted_high, result.predicted_low]
|
|
289
|
+
puts " Actual high=$%-8.2f low=$%-.2f" % [result.actual_high, result.actual_low]
|
|
290
|
+
puts " Error high=%-8.2f low=%-8.2f mean=%.2f" % [result.high_err, result.low_err, result.mean_err]
|
|
291
|
+
puts "#{"─" * 60}"
|
|
292
|
+
|
|
293
|
+
print " [tuner] analyzing window #{window_num}..."
|
|
294
|
+
tuner_response = robot.run(tuner_prompt(result))
|
|
295
|
+
tuner_line = tuner_response.reply.lines.first&.chomp || "(no response)"
|
|
296
|
+
puts "\r [tuner] #{tuner_line}#{" " * 20}"
|
|
297
|
+
puts " Params: #{PredictorConfig.summary}"
|
|
298
|
+
puts
|
|
299
|
+
|
|
300
|
+
# ── Start next window ──────────────────────────────────────────
|
|
301
|
+
new_pred = EnsemblePredictor.predict(prices)
|
|
302
|
+
pending_pred = { prediction: new_pred, window_prices: [] }
|
|
303
|
+
puts " Next prediction → high=$#{new_pred[:high]} low=$#{new_pred[:low]}"
|
|
304
|
+
puts
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 34: AgentSkills.io Integration
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates the unified skills: param detecting AgentSkills folder format.
|
|
7
|
+
# Skills in ~/.prompts/skills/ are matched at runtime via embedding similarity
|
|
8
|
+
# before each run() call — only relevant skills are injected.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# mkdir -p ~/.prompts/skills/code_reviewer
|
|
12
|
+
# # (create SKILL.md as shown in the example header)
|
|
13
|
+
# ANTHROPIC_API_KEY=your_key ruby examples/34_agentskills.rb
|
|
14
|
+
|
|
15
|
+
require_relative "common"
|
|
16
|
+
|
|
17
|
+
require "logger"
|
|
18
|
+
log_file = File.join(__dir__, "34.log")
|
|
19
|
+
RobotLab.config.logger = Logger.new(log_file)
|
|
20
|
+
RubyLLM.configure { |c| c.logger = Logger.new(log_file) }
|
|
21
|
+
|
|
22
|
+
banner "RobotLab — AgentSkills.io Integration Demo"
|
|
23
|
+
|
|
24
|
+
# Check if the skill is installed
|
|
25
|
+
skill_path = File.expand_path("~/.prompts/skills/code_reviewer/SKILL.md")
|
|
26
|
+
unless File.exist?(skill_path)
|
|
27
|
+
puts "Demo skill not found at #{skill_path}"
|
|
28
|
+
puts "Create it with:"
|
|
29
|
+
puts " mkdir -p ~/.prompts/skills/code_reviewer"
|
|
30
|
+
puts " # Then add SKILL.md with name: code_reviewer"
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Build a robot that lists code_reviewer as a candidate skill.
|
|
35
|
+
# At runtime, if the user message is semantically similar to
|
|
36
|
+
# "Review Ruby code for quality, style, and potential bugs",
|
|
37
|
+
# the skill's instructions are injected into the system prompt.
|
|
38
|
+
robot = RobotLab.build(
|
|
39
|
+
model: LLM[:default].model,
|
|
40
|
+
name: "assistant",
|
|
41
|
+
system_prompt: "You are a helpful Ruby programming assistant.",
|
|
42
|
+
skills: [:code_reviewer]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
puts "Pending AgentSkills: #{robot.instance_variable_get(:@pending_agent_skills).map(&:name).inspect}"
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
# Message semantically related to code review — skill should activate
|
|
49
|
+
code_question = <<~MSG
|
|
50
|
+
Please review this Ruby method for quality issues:
|
|
51
|
+
|
|
52
|
+
def process(data)
|
|
53
|
+
begin
|
|
54
|
+
result = data.map { |item| transform(item) }
|
|
55
|
+
save(result)
|
|
56
|
+
rescue => e
|
|
57
|
+
puts e.message
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
MSG
|
|
61
|
+
|
|
62
|
+
puts "Query: code review (skill should activate)"
|
|
63
|
+
result = robot.run(code_question)
|
|
64
|
+
puts result.reply
|
|
65
|
+
puts
|
|
66
|
+
hr
|
|
67
|
+
|
|
68
|
+
# Message unrelated to code review — skill should NOT activate
|
|
69
|
+
puts "Query: general question (skill should NOT activate)"
|
|
70
|
+
result = robot.run("What is the capital of France?")
|
|
71
|
+
puts result.reply
|
|
72
|
+
puts
|
data/examples/README.md
CHANGED
|
@@ -57,6 +57,7 @@ examples/
|
|
|
57
57
|
26_document_store.rb # Embedding-based document store (RAG) via fastembed
|
|
58
58
|
29_ractor_tools.rb # Ractor-safe tools: worker pool, freeze_deep, parallel batch
|
|
59
59
|
30_ractor_network.rb # Ractor network scheduler: dependency waves, parallel_mode
|
|
60
|
+
31_launch_assessment.rb # 6 parallel analysts, max_concurrent_robots: 4 semaphore cap
|
|
60
61
|
18_rails/ # Minimal Rails 8 demo app (full integration)
|
|
61
62
|
app/robots/chat_robot.rb # Robot factory with system prompt + TimeTool
|
|
62
63
|
app/tools/time_tool.rb # Custom RobotLab::Tool subclass
|
|
@@ -225,7 +226,7 @@ Demonstrates `robot.search_history(query, limit:)` — semantic search over accu
|
|
|
225
226
|
|
|
226
227
|
Demonstrates `memory.store_document(key, text)` and `memory.search_documents(query, limit:)` — a lightweight RAG store using `fastembed` (BAAI/bge-small-en-v1.5). Documents are embedded once; queries are compared by cosine similarity at search time. Includes the `RobotLab::DocumentStore` standalone API and a RAG pattern sketch showing how to pass retrieved context to a robot.
|
|
227
228
|
|
|
228
|
-
**Requires:** `
|
|
229
|
+
**Requires:** `robot_lab-document_store` gem (`gem "robot_lab-document_store"` in your Gemfile); downloads the ~23 MB ONNX model on first run (cached in `~/.cache/fastembed/`)
|
|
229
230
|
|
|
230
231
|
### 27 — Production Incident War Room
|
|
231
232
|
|
|
@@ -297,6 +298,14 @@ and the `pipeline.step_dependencies` dependency graph inspection.
|
|
|
297
298
|
|
|
298
299
|
**Requires:** None for Parts 1 & 2. LLM API key for Part 3.
|
|
299
300
|
|
|
301
|
+
### 31 — Product Launch Assessment (Concurrency Cap)
|
|
302
|
+
|
|
303
|
+
Six specialist robots evaluate a product launch simultaneously: market, competitive, technical, risk, financial, and legal analysts. `RunConfig.new(max_concurrent_robots: 4)` caps the `Async::Semaphore` at 4 in-flight LLM calls — robots 5 and 6 queue until a slot opens. A `LaunchDirector` reads all six findings from shared reactive memory and issues a GO / NO-GO recommendation. Start timestamps in the output make the semaphore behavior visible.
|
|
304
|
+
|
|
305
|
+
Demonstrates: `max_concurrent_robots:` on `RunConfig`, `Async::Semaphore` back-pressure via `simple_flow`, six parallel `depends_on: :none` tasks, shared memory writes and blocking reads.
|
|
306
|
+
|
|
307
|
+
**Requires:** LLM API key
|
|
308
|
+
|
|
300
309
|
### 18 — Rails Integration Demo
|
|
301
310
|
|
|
302
311
|
A minimal, hand-built Rails 8 app that exercises every piece of RobotLab's Rails integration end-to-end. No `rails new` — every file is hand-crafted for minimum size.
|