robot_lab 0.3.0 → 0.5.0

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.
Files changed (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +29 -19
  4. data/lib/robot_lab/errors.rb +4 -0
  5. data/lib/robot_lab/hook_context.rb +9 -2
  6. data/lib/robot_lab/mcp/registry.rb +99 -0
  7. data/lib/robot_lab/mcp/server.rb +47 -83
  8. data/lib/robot_lab/mcp/server_discovery.rb +22 -9
  9. data/lib/robot_lab/mcp.rb +259 -0
  10. data/lib/robot_lab/robot/mcp_management.rb +159 -105
  11. data/lib/robot_lab/robot/tool_call_gating.rb +144 -0
  12. data/lib/robot_lab/robot.rb +72 -99
  13. data/lib/robot_lab/run_config.rb +3 -2
  14. data/lib/robot_lab/tool.rb +8 -23
  15. data/lib/robot_lab/version.rb +1 -1
  16. data/lib/robot_lab.rb +1 -4
  17. metadata +16 -292
  18. data/.architecture/AGENTS.md +0 -32
  19. data/.architecture/config.yml +0 -8
  20. data/.architecture/members.yml +0 -60
  21. data/.architecture/reviews/feature-free-will.md +0 -490
  22. data/.architecture/reviews/overall-codebase.md +0 -427
  23. data/.claude/settings.local.json +0 -57
  24. data/.codex/config.toml +0 -2
  25. data/.envrc +0 -6
  26. data/.github/workflows/deploy-github-pages.yml +0 -52
  27. data/.irbrc +0 -6
  28. data/.loki +0 -65
  29. data/.quality/reek_baseline.txt +0 -43
  30. data/.rubocop.yml +0 -10
  31. data/Archspec.rb +0 -44
  32. data/CLAUDE.md +0 -141
  33. data/COMMITS.md +0 -196
  34. data/Rakefile +0 -193
  35. data/_typos.toml +0 -21
  36. data/agent2agent_review.md +0 -192
  37. data/agentf_improvements.md +0 -253
  38. data/agents.md +0 -14
  39. data/docs/api/core/index.md +0 -147
  40. data/docs/api/core/memory.md +0 -578
  41. data/docs/api/core/network.md +0 -553
  42. data/docs/api/core/result.md +0 -211
  43. data/docs/api/core/robot.md +0 -1667
  44. data/docs/api/core/state.md +0 -145
  45. data/docs/api/core/tool.md +0 -520
  46. data/docs/api/errors.md +0 -323
  47. data/docs/api/hooks.md +0 -469
  48. data/docs/api/index.md +0 -137
  49. data/docs/api/mcp/client.md +0 -387
  50. data/docs/api/mcp/index.md +0 -267
  51. data/docs/api/mcp/server.md +0 -252
  52. data/docs/api/mcp/transports.md +0 -339
  53. data/docs/api/messages/index.md +0 -126
  54. data/docs/api/messages/text-message.md +0 -170
  55. data/docs/api/messages/tool-call-message.md +0 -206
  56. data/docs/api/messages/tool-result-message.md +0 -242
  57. data/docs/api/messages/user-message.md +0 -232
  58. data/docs/api/skills.md +0 -423
  59. data/docs/api/streaming/context.md +0 -280
  60. data/docs/api/streaming/events.md +0 -185
  61. data/docs/api/streaming/index.md +0 -156
  62. data/docs/api/support.md +0 -846
  63. data/docs/architecture/core-concepts.md +0 -575
  64. data/docs/architecture/index.md +0 -252
  65. data/docs/architecture/message-flow.md +0 -366
  66. data/docs/architecture/network-orchestration.md +0 -478
  67. data/docs/architecture/robot-execution.md +0 -396
  68. data/docs/architecture/state-management.md +0 -410
  69. data/docs/assets/css/custom.css +0 -56
  70. data/docs/assets/images/robot_lab.jpg +0 -0
  71. data/docs/concepts.md +0 -641
  72. data/docs/examples/basic-chat.md +0 -258
  73. data/docs/examples/index.md +0 -425
  74. data/docs/examples/mcp-server.md +0 -403
  75. data/docs/examples/multi-robot-network.md +0 -476
  76. data/docs/examples/tool-usage.md +0 -468
  77. data/docs/getting-started/configuration.md +0 -645
  78. data/docs/getting-started/index.md +0 -56
  79. data/docs/getting-started/installation.md +0 -197
  80. data/docs/getting-started/quick-start.md +0 -293
  81. data/docs/guides/building-robots.md +0 -1187
  82. data/docs/guides/creating-networks.md +0 -736
  83. data/docs/guides/hooks.md +0 -1206
  84. data/docs/guides/index.md +0 -78
  85. data/docs/guides/knowledge.md +0 -219
  86. data/docs/guides/mcp-integration.md +0 -740
  87. data/docs/guides/memory.md +0 -458
  88. data/docs/guides/observability.md +0 -733
  89. data/docs/guides/streaming.md +0 -323
  90. data/docs/guides/using-tools.md +0 -780
  91. data/docs/index.md +0 -280
  92. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  93. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  94. data/examples/.envrc +0 -3
  95. data/examples/01_simple_robot.rb +0 -31
  96. data/examples/02_tools.rb +0 -109
  97. data/examples/03_network.rb +0 -118
  98. data/examples/04_mcp.rb +0 -217
  99. data/examples/05_streaming.rb +0 -137
  100. data/examples/06_prompt_templates.rb +0 -325
  101. data/examples/07_network_memory.rb +0 -313
  102. data/examples/08_llm_config.rb +0 -162
  103. data/examples/09_chaining.rb +0 -251
  104. data/examples/10_memory.rb +0 -315
  105. data/examples/11_network_introspection.rb +0 -239
  106. data/examples/12_message_bus.rb +0 -67
  107. data/examples/13_spawn.rb +0 -85
  108. data/examples/14_rusty_circuit/.envrc +0 -1
  109. data/examples/14_rusty_circuit/.gitignore +0 -1
  110. data/examples/14_rusty_circuit/README.md +0 -1
  111. data/examples/14_rusty_circuit/comic.rb +0 -149
  112. data/examples/14_rusty_circuit/display.rb +0 -217
  113. data/examples/14_rusty_circuit/heckler.rb +0 -65
  114. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  115. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  116. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  117. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  118. data/examples/14_rusty_circuit/scout.rb +0 -165
  119. data/examples/14_rusty_circuit/show.log +0 -234
  120. data/examples/15_memory_network_and_bus/.envrc +0 -1
  121. data/examples/15_memory_network_and_bus/README.md +0 -66
  122. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  123. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  124. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  125. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  126. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  127. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  128. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  129. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  130. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  131. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  132. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  133. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  134. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  135. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  136. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  137. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  138. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  139. data/examples/16_writers_room/.envrc +0 -1
  140. data/examples/16_writers_room/display.rb +0 -158
  141. data/examples/16_writers_room/output/.gitignore +0 -4
  142. data/examples/16_writers_room/output/README.md +0 -69
  143. data/examples/16_writers_room/output/opus_001.md +0 -263
  144. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  145. data/examples/16_writers_room/output/opus_002.md +0 -245
  146. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  147. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  148. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  149. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  150. data/examples/16_writers_room/prompts/writer.md +0 -37
  151. data/examples/16_writers_room/room.rb +0 -186
  152. data/examples/16_writers_room/tools.rb +0 -173
  153. data/examples/16_writers_room/writer.rb +0 -121
  154. data/examples/16_writers_room/writers_room.rb +0 -256
  155. data/examples/17_skills.rb +0 -234
  156. data/examples/18_rails/.envrc +0 -3
  157. data/examples/18_rails/.gitignore +0 -5
  158. data/examples/18_rails/Gemfile +0 -11
  159. data/examples/18_rails/README.md +0 -67
  160. data/examples/18_rails/Rakefile +0 -4
  161. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  162. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  163. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  164. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  165. data/examples/18_rails/app/models/application_record.rb +0 -5
  166. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  167. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  168. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  169. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  170. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  171. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  172. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  173. data/examples/18_rails/bin/dev +0 -7
  174. data/examples/18_rails/bin/rails +0 -6
  175. data/examples/18_rails/bin/setup +0 -15
  176. data/examples/18_rails/config/application.rb +0 -33
  177. data/examples/18_rails/config/cable.yml +0 -2
  178. data/examples/18_rails/config/database.yml +0 -5
  179. data/examples/18_rails/config/environment.rb +0 -4
  180. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  181. data/examples/18_rails/config/routes.rb +0 -6
  182. data/examples/18_rails/config.ru +0 -4
  183. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  184. data/examples/19_token_tracking.rb +0 -138
  185. data/examples/20_circuit_breaker.rb +0 -147
  186. data/examples/21_learning_loop.rb +0 -181
  187. data/examples/22_context_compression.rb +0 -172
  188. data/examples/23_convergence.rb +0 -133
  189. data/examples/24_structured_delegation.rb +0 -151
  190. data/examples/25_history_search/conversation.jsonl +0 -30
  191. data/examples/25_history_search.rb +0 -133
  192. data/examples/26_document_store/api_versioning_adr.md +0 -52
  193. data/examples/26_document_store/incident_postmortem.md +0 -46
  194. data/examples/26_document_store/postgres_runbook.md +0 -49
  195. data/examples/26_document_store/redis_caching_guide.md +0 -48
  196. data/examples/26_document_store/sidekiq_guide.md +0 -51
  197. data/examples/26_document_store.rb +0 -140
  198. data/examples/27_incident_response/README.md +0 -65
  199. data/examples/27_incident_response/incident_response.rb +0 -261
  200. data/examples/28_mcp_discovery.rb +0 -113
  201. data/examples/29_ractor_tools.rb +0 -240
  202. data/examples/30_ractor_network.rb +0 -252
  203. data/examples/31_launch_assessment.rb +0 -246
  204. data/examples/32_newsletter_reader.rb +0 -193
  205. data/examples/33_stock_generator.rb +0 -80
  206. data/examples/33_stock_predictor.rb +0 -327
  207. data/examples/34_agentskills.rb +0 -76
  208. data/examples/35_hooks.rb +0 -266
  209. data/examples/README.md +0 -550
  210. data/examples/common.rb +0 -180
  211. data/examples/prompts/assistant.md +0 -5
  212. data/examples/prompts/audit_trail.md +0 -8
  213. data/examples/prompts/billing.md +0 -10
  214. data/examples/prompts/classifier.md +0 -7
  215. data/examples/prompts/comedian.md +0 -6
  216. data/examples/prompts/comedy_critic.md +0 -10
  217. data/examples/prompts/configurable.md +0 -9
  218. data/examples/prompts/dispatcher.md +0 -12
  219. data/examples/prompts/entity_extractor.md +0 -14
  220. data/examples/prompts/escalation.md +0 -42
  221. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  222. data/examples/prompts/frontmatter_named_test.md +0 -5
  223. data/examples/prompts/frontmatter_tools_test.md +0 -6
  224. data/examples/prompts/general.md +0 -7
  225. data/examples/prompts/github_assistant.md +0 -14
  226. data/examples/prompts/helper.md +0 -4
  227. data/examples/prompts/incident_responder.md +0 -18
  228. data/examples/prompts/keyword_extractor.md +0 -11
  229. data/examples/prompts/llm_config_demo.md +0 -20
  230. data/examples/prompts/order_support.md +0 -35
  231. data/examples/prompts/os_advocate.md +0 -13
  232. data/examples/prompts/os_chief.md +0 -13
  233. data/examples/prompts/os_editor.md +0 -13
  234. data/examples/prompts/parameterized_main_test.md +0 -6
  235. data/examples/prompts/pii_redactor.md +0 -7
  236. data/examples/prompts/product_support.md +0 -37
  237. data/examples/prompts/runbook_protocol.md +0 -12
  238. data/examples/prompts/sentiment_analyzer.md +0 -12
  239. data/examples/prompts/skill_a_test.md +0 -4
  240. data/examples/prompts/skill_b_test.md +0 -4
  241. data/examples/prompts/skill_config_test.md +0 -5
  242. data/examples/prompts/skill_cycle_a_test.md +0 -6
  243. data/examples/prompts/skill_cycle_b_test.md +0 -6
  244. data/examples/prompts/skill_description_test.md +0 -4
  245. data/examples/prompts/skill_leaf_test.md +0 -4
  246. data/examples/prompts/skill_nested_test.md +0 -6
  247. data/examples/prompts/skill_refs_main_test.md +0 -6
  248. data/examples/prompts/skill_self_ref_test.md +0 -6
  249. data/examples/prompts/skill_with_mcp_test.md +0 -9
  250. data/examples/prompts/skill_with_params_test.md +0 -6
  251. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  252. data/examples/prompts/skill_with_tools_test.md +0 -6
  253. data/examples/prompts/sre_compliance.md +0 -10
  254. data/examples/prompts/structured_output.md +0 -7
  255. data/examples/prompts/synthesizer.md +0 -17
  256. data/examples/prompts/technical.md +0 -10
  257. data/examples/prompts/template_with_skills_test.md +0 -6
  258. data/examples/prompts/triage.md +0 -22
  259. data/examples/ruboruby.md +0 -423
  260. data/examples/run_all.rb +0 -60
  261. data/examples/xyzzy.rb +0 -97
  262. data/lib/robot_lab/mcp/client.rb +0 -223
  263. data/lib/robot_lab/mcp/connection_poller.rb +0 -195
  264. data/lib/robot_lab/mcp/transports/base.rb +0 -64
  265. data/lib/robot_lab/mcp/transports/sse.rb +0 -120
  266. data/lib/robot_lab/mcp/transports/stdio.rb +0 -170
  267. data/lib/robot_lab/mcp/transports/streamable_http.rb +0 -141
  268. data/lib/robot_lab/mcp/transports/websocket.rb +0 -111
  269. data/logfile +0 -8
  270. data/mkdocs.yml +0 -209
  271. data/robot_concurrency.md +0 -38
  272. data/simple_acp_review.md +0 -298
  273. data/temp.md +0 -6
  274. data/tool_manifest_plan.md +0 -155
@@ -1,258 +0,0 @@
1
- # Basic Chat
2
-
3
- A simple conversational robot example.
4
-
5
- ## Overview
6
-
7
- This example demonstrates the minimal setup for a conversational robot that can respond to user messages using `robot.run("message")`.
8
-
9
- The closest runnable file in this repository is
10
- [`examples/01_simple_robot.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/01_simple_robot.rb).
11
- The snippets below wrap that same API in a small REPL.
12
-
13
- ## Complete Example
14
-
15
- ```ruby
16
- #!/usr/bin/env ruby
17
-
18
- require "bundler/setup"
19
- require "robot_lab"
20
-
21
- # Build a simple assistant
22
- assistant = RobotLab.build(
23
- name: "assistant",
24
- description: "A helpful conversational assistant",
25
- system_prompt: <<~PROMPT,
26
- You are a helpful, friendly assistant. You provide clear,
27
- concise answers to questions. Be conversational but informative.
28
- PROMPT
29
- model: "claude-sonnet-4"
30
- )
31
-
32
- # Simple REPL
33
- puts "Chat with the assistant (type 'quit' to exit)"
34
- puts "-" * 50
35
-
36
- loop do
37
- print "\nYou: "
38
- input = gets&.chomp
39
-
40
- break if input.nil? || input.downcase == "quit"
41
- next if input.empty?
42
-
43
- # Run the robot with the user's message
44
- result = assistant.run(input)
45
-
46
- # Display response
47
- puts "\nAssistant: #{result.last_text_content}"
48
- end
49
-
50
- puts "\nGoodbye!"
51
- ```
52
-
53
- ## With Streaming
54
-
55
- Pass a block to `run` to receive each `RubyLLM::Chunk` as it arrives.
56
-
57
- > [!WARNING]
58
- > The yielded object is a `RubyLLM::Chunk`. Read its text with `chunk.content`.
59
- > There is no `chunk.text` method — a guard like `if chunk.respond_to?(:text)`
60
- > silently prints nothing.
61
-
62
- ```ruby
63
- #!/usr/bin/env ruby
64
-
65
- require "bundler/setup"
66
- require "robot_lab"
67
-
68
- assistant = RobotLab.build(
69
- name: "assistant",
70
- system_prompt: "You are a helpful assistant.",
71
- model: "claude-sonnet-4"
72
- )
73
-
74
- puts "Chat with streaming (type 'quit' to exit)"
75
- puts "-" * 50
76
-
77
- loop do
78
- print "\nYou: "
79
- input = gets&.chomp
80
-
81
- break if input.nil? || input.downcase == "quit"
82
- next if input.empty?
83
-
84
- print "\nAssistant: "
85
- assistant.run(input) { |chunk| print chunk.content }
86
- puts
87
- end
88
-
89
- puts "\nGoodbye!"
90
- ```
91
-
92
- You can also wire streaming once at build time with the `on_content:` callback,
93
- which fires on every `run`:
94
-
95
- ```ruby
96
- assistant = RobotLab.build(
97
- name: "assistant",
98
- system_prompt: "You are a helpful assistant.",
99
- on_content: ->(chunk) { print chunk.content }
100
- )
101
-
102
- assistant.run("Tell me a one-sentence fact about Ruby.")
103
- ```
104
-
105
- When both are supplied, the stored `on_content` callback fires first, then the
106
- block. See [`examples/05_streaming.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/05_streaming.rb)
107
- for all four variations (stored callback, per-call block, both, and via `RunConfig`).
108
-
109
- ## With Template
110
-
111
- Templates are `.md` files with YAML front matter, resolved from the configured
112
- prompts directory (`ROBOT_LAB_TEMPLATE_PATH`). Parameters declared `null` in the
113
- front matter are required and are supplied via `context:`.
114
-
115
- ```ruby
116
- #!/usr/bin/env ruby
117
-
118
- require "bundler/setup"
119
- require "robot_lab"
120
-
121
- # Template file: prompts/support.md
122
- # ---
123
- # description: Support assistant
124
- # parameters:
125
- # company_name: null
126
- # tone: friendly
127
- # ---
128
- # You are a <%= tone %> support assistant for <%= company_name %>.
129
-
130
- assistant = RobotLab.build(
131
- name: "assistant",
132
- template: :support,
133
- context: { company_name: "Acme Corp", tone: "friendly" },
134
- model: "claude-sonnet-4"
135
- )
136
-
137
- puts "Chat with template-based assistant (type 'quit' to exit)"
138
- puts "-" * 50
139
-
140
- loop do
141
- print "\nYou: "
142
- input = gets&.chomp
143
-
144
- break if input.nil? || input.downcase == "quit"
145
- next if input.empty?
146
-
147
- result = assistant.run(input)
148
- puts "\nAssistant: #{result.last_text_content}"
149
- end
150
-
151
- puts "\nGoodbye!"
152
- ```
153
-
154
- A full template-driven network lives in
155
- [`examples/06_prompt_templates.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/06_prompt_templates.rb).
156
-
157
- ## With Memory
158
-
159
- ```ruby
160
- #!/usr/bin/env ruby
161
-
162
- require "bundler/setup"
163
- require "robot_lab"
164
-
165
- assistant = RobotLab.build(
166
- name: "assistant",
167
- system_prompt: "You are a helpful assistant. Use the user's name when you know it.",
168
- model: "claude-sonnet-4"
169
- )
170
-
171
- puts "Chat with memory (type 'quit' to exit)"
172
- puts "-" * 50
173
-
174
- # Store user info in the robot's inherent memory
175
- assistant.memory[:user_name] = "Alice"
176
-
177
- loop do
178
- print "\nYou: "
179
- input = gets&.chomp
180
-
181
- break if input.nil? || input.downcase == "quit"
182
- next if input.empty?
183
-
184
- # The robot's persistent @chat maintains conversation history automatically
185
- result = assistant.run(input)
186
- puts "\nAssistant: #{result.last_text_content}"
187
- end
188
-
189
- puts "\nGoodbye!"
190
- ```
191
-
192
- The full Memory API — subscriptions, `StateProxy`, blocking reads, clone and
193
- reset — is demonstrated in
194
- [`examples/10_memory.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/10_memory.rb).
195
-
196
- ## Bare Robot with Chaining
197
-
198
- ```ruby
199
- #!/usr/bin/env ruby
200
-
201
- require "bundler/setup"
202
- require "robot_lab"
203
-
204
- # Build a bare robot with no template or prompt
205
- robot = RobotLab.build(name: "bot")
206
-
207
- # Configure via chaining
208
- result = robot
209
- .with_model("claude-sonnet-4")
210
- .with_temperature(0.7)
211
- .with_instructions("You are a pirate. Respond in pirate speak.")
212
- .run("What is the weather like today?")
213
-
214
- puts result.last_text_content
215
- ```
216
-
217
- > [!NOTE]
218
- > Only the `with_*` methods that `RubyLLM::Chat` exposes are delegated:
219
- > `with_context`, `with_headers`, `with_instructions`, `with_model`, `with_params`,
220
- > `with_schema`, `with_temperature`, `with_thinking`, `with_tool`, `with_tools`
221
- > (plus RobotLab's own `with_template` and `with_bus`).
222
- > There is no `with_max_tokens` / `with_top_p` / `with_top_k` — use a constructor
223
- > kwarg (`max_tokens: 2000`) or `with_params(max_tokens: 2000, top_p: 0.3)`.
224
-
225
- `examples/09_chaining.rb` walks through chaining and reconfiguration without
226
- making any LLM calls.
227
-
228
- ## Running
229
-
230
- ```bash
231
- # Set API key
232
- export ANTHROPIC_API_KEY="your-key"
233
-
234
- # Simplest runnable robot
235
- ruby examples/01_simple_robot.rb
236
-
237
- # Streaming
238
- ruby examples/05_streaming.rb
239
-
240
- # with_* chaining and reconfiguration (no LLM calls)
241
- ruby examples/09_chaining.rb
242
- ```
243
-
244
- ## Key Concepts
245
-
246
- 1. **Robot Building**: Use `RobotLab.build(name:, system_prompt:)` or `RobotLab.build(name:, template:)` to create a robot
247
- 2. **Execution**: Call `robot.run("message")` to send a message and get a response
248
- 3. **Response**: Access the text via `result.last_text_content` (aliased as `result.reply`)
249
- 4. **Streaming**: Pass a block to `robot.run("message") { |chunk| print chunk.content }`, or set `on_content:` at build time
250
- 5. **Memory**: Access inherent memory via `robot.memory[:key]`
251
- 6. **Chaining**: Configure with the delegated `with_*` methods, which return `self`
252
- 7. **Conversation History**: The persistent `@chat` maintains history across multiple `run` calls
253
-
254
- ## See Also
255
-
256
- - [Building Robots Guide](../guides/building-robots.md)
257
- - [Streaming Guide](../guides/streaming.md)
258
- - [Robot API Reference](../api/core/robot.md)
@@ -1,425 +0,0 @@
1
- # Examples
2
-
3
- Complete working examples demonstrating RobotLab features.
4
-
5
- ## Overview
6
-
7
- These examples show how to use RobotLab for common scenarios, from simple chatbots to complex multi-robot systems.
8
-
9
- ## Examples
10
-
11
- | Example | Description |
12
- |---------|-------------|
13
- | [Basic Chat](basic-chat.md) | Simple conversational robot |
14
- | [Multi-Robot Network](multi-robot-network.md) | Customer service with routing |
15
- | [Tool Usage](tool-usage.md) | External API integration |
16
- | [MCP Server](mcp-server.md) | Connecting a robot to external MCP servers |
17
- | [Message Bus](#message-bus) | Bidirectional robot communication with convergence |
18
- | [Spawning Robots](#spawning-robots) | Dynamic specialist creation at runtime |
19
-
20
- > **Rails example** — see [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails/blob/main/docs/examples/rails-application.md) for a full Rails integration example.
21
-
22
- ## Quick Links
23
-
24
- ### Simple Examples
25
-
26
- - [Hello World Robot](#hello-world)
27
- - [Robot with Tools](#robot-with-tools)
28
- - [Network with Routing](#network-with-routing)
29
-
30
- ### Advanced Examples
31
-
32
- - [Streaming Responses](basic-chat.md#with-streaming)
33
- - [Persistent Conversations](basic-chat.md#with-memory)
34
- - [MCP Integration](mcp-server.md)
35
- - [Message Bus Communication](#message-bus)
36
- - [Spawning Robots](#spawning-robots)
37
-
38
- ## Hello World
39
-
40
- ```ruby
41
- require "robot_lab"
42
-
43
- # Configuration is handled automatically via MywayConfig.
44
- # Set API keys via environment variables:
45
- # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
46
- # Or via config files (~/.config/robot_lab/robot_lab.yml — the filename
47
- # repeats the app name; ~/.config/robot_lab/config.yml is never read)
48
-
49
- robot = RobotLab.build(
50
- name: "greeter",
51
- system_prompt: "You are a friendly greeter. Say hello warmly."
52
- )
53
-
54
- result = robot.run("Hi there!")
55
-
56
- puts result.last_text_content
57
- ```
58
-
59
- ## Robot with Tools
60
-
61
- Give the LLM a fixed set of operations rather than an expression evaluator —
62
- never `eval` a string the model produced.
63
-
64
- ```ruby
65
- class CalculatorTool < RubyLLM::Tool
66
- description "Performs basic arithmetic operations"
67
-
68
- param :operation, type: "string", desc: "add, subtract, multiply, or divide"
69
- param :a, type: "number", desc: "First operand"
70
- param :b, type: "number", desc: "Second operand"
71
-
72
- def execute(operation:, a:, b:)
73
- case operation
74
- when "add" then a + b
75
- when "subtract" then a - b
76
- when "multiply" then a * b
77
- when "divide" then a.to_f / b
78
- else "Unknown operation: #{operation}"
79
- end
80
- end
81
- end
82
-
83
- robot = RobotLab.build(
84
- name: "calculator",
85
- system_prompt: "You help with calculations.",
86
- local_tools: [CalculatorTool]
87
- )
88
-
89
- # tools: :inherit is REQUIRED — run() defaults to tools: :none
90
- result = robot.run("What's 25 * 4?", tools: :inherit)
91
- puts result.last_text_content
92
- ```
93
-
94
- > [!WARNING]
95
- > `Robot#run` defaults to `tools: :none` and `mcp: :none`. A plain
96
- > `robot.run("...")` sends the LLM **no tools at all**, even when `local_tools:`
97
- > were attached at build time. Pass `tools: :inherit` on the call that should be
98
- > able to use them.
99
- >
100
- > For a **standalone** robot like this one, do not pass `tools: :inherit` at
101
- > *build* time — there the parent level is the global default (`:none`), so it
102
- > produces an allowlist that matches nothing. Leave `tools:` unset in the
103
- > constructor. (Inside a network the opposite holds: build-time `:inherit` is how a
104
- > robot opts into the allowlist on the network's `config:`. See
105
- > [Configuration](../getting-started/configuration.md#hierarchical-mcp-and-tools).)
106
-
107
- See [`examples/02_tools.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/02_tools.rb)
108
- for a second tool definition (`FortuneCookie`) alongside the calculator.
109
-
110
- ## Network with Routing
111
-
112
- Routing is not configured declaratively — a robot performs it. Subclass
113
- `RobotLab::Robot`, override `#call`, and activate one of the `depends_on: :optional`
114
- tasks based on what the LLM returned.
115
-
116
- ```ruby
117
- class ClassifierRobot < RobotLab::Robot
118
- def call(result)
119
- run_context = extract_run_context(result)
120
- message = run_context.delete(:message)
121
- robot_result = run(message, **run_context)
122
-
123
- new_result = result
124
- .with_context(@name.to_sym, robot_result)
125
- .continue(robot_result)
126
-
127
- case robot_result.reply.to_s.strip.downcase
128
- when /billing/ then new_result.activate(:billing)
129
- when /technical/ then new_result.activate(:technical)
130
- else new_result.activate(:general)
131
- end
132
- end
133
- end
134
-
135
- classifier = ClassifierRobot.new(
136
- name: "classifier",
137
- system_prompt: "Classify the request as BILLING, TECHNICAL, or GENERAL. Respond with only the category."
138
- )
139
-
140
- billing = RobotLab.build(name: "billing", system_prompt: "You handle billing questions.")
141
- tech = RobotLab.build(name: "technical", system_prompt: "You handle technical issues.")
142
- general = RobotLab.build(name: "general", system_prompt: "You handle everything else.")
143
-
144
- network = RobotLab.create_network(name: "support") do
145
- task :classifier, classifier, depends_on: :none
146
- task :billing, billing, depends_on: :optional
147
- task :technical, tech, depends_on: :optional
148
- task :general, general, depends_on: :optional
149
- end
150
-
151
- result = network.run(message: "I was charged twice for my subscription")
152
-
153
- # Access individual robot results via context
154
- puts result.context[:classifier].last_text_content
155
- puts result.value.last_text_content # the specialist that was activated
156
- ```
157
-
158
- > [!WARNING]
159
- > `result.context` is keyed by the **robot's** name (`with_context(@name.to_sym, ...)`),
160
- > not the task name, and `activate(:name)` takes a **task** name. Keep the two
161
- > identical — activating a task name that was never declared **raises
162
- > `ArgumentError` and aborts the run** (`Step :classifier attempted to activate
163
- > unknown step :billing`). The same happens for a task that was declared without
164
- > `depends_on: :optional`.
165
-
166
- Full version: [`examples/03_network.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/03_network.rb).
167
-
168
- ## Chaining Configuration
169
-
170
- Robots support `with_*` methods that return `self` for chaining:
171
-
172
- ```ruby
173
- robot = RobotLab.build(name: "assistant")
174
- .with_instructions("You are a helpful coding assistant.")
175
- .with_temperature(0.3)
176
- .with_model("gpt-4o")
177
-
178
- result = robot.run("Explain Ruby blocks.")
179
- puts result.last_text_content
180
- ```
181
-
182
- > [!NOTE]
183
- > The chainable set is exactly what `RubyLLM::Chat` exposes — `with_context`,
184
- > `with_headers`, `with_instructions`, `with_model`, `with_params`, `with_schema`,
185
- > `with_temperature`, `with_thinking`, `with_tool`, `with_tools` — plus RobotLab's
186
- > `with_template` and `with_bus`. There is no `with_max_tokens`, `with_top_p`, or
187
- > `with_top_k`; use a constructor kwarg or `with_params(max_tokens: 2000)`.
188
-
189
- ## Using Templates
190
-
191
- Templates are `.md` files with optional YAML front matter, managed by prompt_manager.
192
- The body is rendered with **ERB** — interpolate with `<%= var %>`.
193
-
194
- ```ruby
195
- # Template file: prompts/support.md
196
- # ---
197
- # model: claude-sonnet-4
198
- # temperature: 0.5
199
- # parameters:
200
- # company_name: null
201
- # ---
202
- # You are a support assistant for <%= company_name %>.
203
-
204
- robot = RobotLab.build(
205
- name: "support",
206
- template: :support,
207
- context: { company_name: "Acme Corp" }
208
- )
209
-
210
- result = robot.run("How do I reset my password?")
211
- puts result.last_text_content
212
- ```
213
-
214
- > [!NOTE]
215
- > `{{ var }}` is **not** interpolated — it passes through to the LLM verbatim.
216
- > Of the LLM keys accepted in front matter, only `model:` and `temperature:` are
217
- > actually applied to the chat. `top_p`, `top_k`, `max_tokens`, `presence_penalty`,
218
- > `frequency_penalty`, and `stop` are parsed and then silently dropped; supply
219
- > those as constructor kwargs or via a `RunConfig` instead.
220
-
221
- ## Running Examples
222
-
223
- 1. Install dependencies:
224
- ```bash
225
- bundle install
226
- ```
227
-
228
- 2. Set API key:
229
- ```bash
230
- export ANTHROPIC_API_KEY="your-key"
231
- ```
232
-
233
- 3. Run example:
234
- ```bash
235
- ruby examples/01_simple_robot.rb
236
- ```
237
-
238
- Or use the provided rake tasks:
239
-
240
- ```bash
241
- bundle exec rake examples:all # Run all examples
242
- bundle exec rake examples:run[1] # Run specific example by number
243
- ```
244
-
245
- ## Shared Example Setup (`examples/common.rb`)
246
-
247
- Most numbered examples (`01_*.rb` through `35_*.rb`) pull in a shared setup file:
248
-
249
- ```ruby
250
- require_relative "common"
251
- ```
252
-
253
- The line sits below each example's header comment rather than at the very top of
254
- the file, and three examples do without it entirely —
255
- `32_newsletter_reader.rb`, `33_stock_generator.rb`, and `33_stock_predictor.rb`.
256
-
257
- `common.rb` handles the shared boilerplate so individual examples stay focused:
258
-
259
- - **`LLM` hash** — frozen lookup of provider/model pairs accessible as `LLM[:default]`, `LLM[:local]`, `LLM[:anthropic]`. Each entry is a `LlmConfig = Data.define(:provider, :model)` value, so you access the model string as `LLM[:default].model`.
260
- - **`RubyLLM.configure`** — sets a null logger and `LLM[:default].model` as the `default_model`.
261
- - **`RobotLab.configure`** — sets a null logger.
262
- - **Output helpers** — `banner(title)`, `section(title)`, `hr`, and `show_code(ruby_string, label:)` (Rouge-highlighted) for consistent terminal formatting.
263
-
264
- ## Template Path via direnv
265
-
266
- Examples that bundle their own `prompts/` directory ship with a `.envrc` file:
267
-
268
- ```
269
- examples/.envrc
270
- examples/14_rusty_circuit/.envrc
271
- examples/15_memory_network_and_bus/.envrc
272
- examples/16_writers_room/.envrc
273
- ```
274
-
275
- Each sets `ROBOT_LAB_TEMPLATE_PATH` to the local `prompts/` directory when [direnv](https://direnv.net/) is active. `common.rb` also sets this variable as a fallback if `direnv` has not loaded the `.envrc`:
276
-
277
- ```ruby
278
- ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
279
- ```
280
-
281
- This means examples work correctly whether you run them from the project root with rake tasks or directly from inside the example's own directory.
282
-
283
- ## Message Bus
284
-
285
- Robots can communicate bidirectionally via a message bus, enabling convergence loops and negotiation patterns. This example demonstrates a comedy critic tasking a comedian to generate jokes until one passes:
286
-
287
- ```ruby
288
- ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
289
- require "robot_lab"
290
-
291
- MAX_ATTEMPTS = 5
292
-
293
- class Comedian < RobotLab::Robot
294
- TEMP_START = 0.2
295
- TEMP_STEP = 0.2
296
-
297
- def initialize(bus:)
298
- super(name: "bob", template: :comedian, bus: bus, temperature: TEMP_START)
299
- @attempts = 0
300
- on_message do |message|
301
- @attempts += 1
302
- temp = [TEMP_START + TEMP_STEP * (@attempts - 1), 1.0].min
303
- with_temperature(temp)
304
- joke = run(message.content.to_s).reply.strip
305
- send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
306
- end
307
- end
308
-
309
- attr_reader :attempts
310
- end
311
-
312
- class ComedyCritic < RobotLab::Robot
313
- def initialize(bus:)
314
- super(name: "alice", template: :comedy_critic, bus: bus)
315
- @accepted = false
316
- @rounds = 0
317
- on_message do |message|
318
- @rounds += 1
319
- verdict = run("Evaluate this joke:\n\n#{message.content}").reply.strip
320
- @accepted = verdict.start_with?("FUNNY")
321
- # The @rounds guard is what terminates the loop — without it the critic
322
- # keeps sending Bob back forever.
323
- send_message(to: :bob, content: "Not funny enough. Try again.") unless @accepted || @rounds >= MAX_ATTEMPTS
324
- end
325
- end
326
-
327
- attr_reader :accepted
328
- end
329
-
330
- bus = TypedBus::MessageBus.new
331
- bob = Comedian.new(bus: bus)
332
- alice = ComedyCritic.new(bus: bus)
333
-
334
- alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
335
- puts "Attempts: #{bob.attempts} / #{MAX_ATTEMPTS}"
336
- puts "Accepted: #{alice.accepted}"
337
- ```
338
-
339
- Key patterns demonstrated:
340
-
341
- - **Robot subclasses** with templates for prompt management
342
- - **Auto-ack** via 1-arg `on_message` blocks
343
- - **`send_reply(to:, content:, in_reply_to:)`** for correlated responses
344
- - **Temperature ramping** (0.2 &rarr; 1.0) for increasing creativity
345
- - **Convergence loop** that terminates when the critic approves *or* `MAX_ATTEMPTS` is reached
346
-
347
- > [!WARNING]
348
- > `MAX_ATTEMPTS` only bounds the loop because the critic checks it before sending
349
- > Bob back. Declaring the constant without testing it leaves the two robots
350
- > messaging each other indefinitely.
351
-
352
- Run: `bundle exec ruby examples/12_message_bus.rb`
353
-
354
- ## Spawning Robots
355
-
356
- Robots can create new specialist robots at runtime using `spawn`. A dispatcher receives questions, decides what kind of specialist is needed, and spawns one on the fly. The bus is created lazily — no explicit setup required:
357
-
358
- ```ruby
359
- ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
360
- require "robot_lab"
361
-
362
- QUESTIONS = [
363
- "Why did the Roman Empire fall?",
364
- "Write a haiku about recursion.",
365
- "What is the square root of 144?",
366
- ].freeze
367
-
368
- class Dispatcher < RobotLab::Robot
369
- attr_reader :spawned
370
-
371
- def initialize(bus: nil)
372
- super(name: "dispatcher", template: :dispatcher, bus: bus)
373
- @spawned = {}
374
- @pending = {}
375
-
376
- on_message do |message|
377
- puts " Dispatcher <- :#{message.from} replied"
378
- puts " | #{message.content.to_s.lines.first&.strip}"
379
- @pending.delete(message.from)
380
- end
381
- end
382
-
383
- def dispatch(question)
384
- plan = run(question).last_text_content.strip
385
- role, instruction = plan.split("\n", 2)
386
- role = role.strip.downcase.gsub(/\s+/, "_")
387
- instruction = instruction&.strip || "You are a helpful #{role}."
388
-
389
- specialist = @spawned[role] ||= spawn(
390
- name: role,
391
- system_prompt: instruction
392
- )
393
-
394
- @pending[role] = question
395
-
396
- specialist.send_message(to: :dispatcher, content:
397
- specialist.run(question).last_text_content.strip
398
- )
399
- end
400
- end
401
-
402
- dispatcher = Dispatcher.new
403
-
404
- QUESTIONS.each_with_index do |question, i|
405
- puts "\nQuestion #{i + 1}: #{question}"
406
- dispatcher.dispatch(question)
407
- end
408
-
409
- puts "\nSpecialists spawned: #{dispatcher.spawned.keys.join(', ')}"
410
- ```
411
-
412
- Key patterns demonstrated:
413
-
414
- - **`spawn`** for dynamic robot creation (bus created lazily)
415
- - **`on_message`** for reply handling
416
- - **LLM-driven delegation** — the dispatcher asks its LLM what specialist to create
417
- - **Specialist reuse** — spawned robots are cached and reused across questions
418
-
419
- Run: `bundle exec ruby examples/13_spawn.rb`
420
-
421
- ## See Also
422
-
423
- - [Getting Started](../getting-started/index.md)
424
- - [Guides](../guides/index.md)
425
- - [API Reference](../api/index.md)