robot_lab 0.0.1 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +140 -0
  5. data/README.md +263 -48
  6. data/Rakefile +71 -1
  7. data/docs/api/core/index.md +53 -46
  8. data/docs/api/core/memory.md +200 -154
  9. data/docs/api/core/network.md +13 -3
  10. data/docs/api/core/robot.md +490 -130
  11. data/docs/api/core/state.md +55 -73
  12. data/docs/api/core/tool.md +205 -209
  13. data/docs/api/index.md +7 -28
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/messages/index.md +35 -20
  19. data/docs/api/messages/text-message.md +67 -21
  20. data/docs/api/messages/tool-call-message.md +80 -41
  21. data/docs/api/messages/tool-result-message.md +119 -50
  22. data/docs/api/messages/user-message.md +48 -24
  23. data/docs/api/streaming/context.md +157 -74
  24. data/docs/api/streaming/events.md +114 -166
  25. data/docs/api/streaming/index.md +74 -72
  26. data/docs/architecture/core-concepts.md +360 -116
  27. data/docs/architecture/index.md +97 -59
  28. data/docs/architecture/message-flow.md +138 -129
  29. data/docs/architecture/network-orchestration.md +197 -50
  30. data/docs/architecture/robot-execution.md +199 -146
  31. data/docs/architecture/state-management.md +255 -187
  32. data/docs/concepts.md +311 -49
  33. data/docs/examples/basic-chat.md +89 -77
  34. data/docs/examples/index.md +222 -47
  35. data/docs/examples/mcp-server.md +207 -203
  36. data/docs/examples/multi-robot-network.md +129 -35
  37. data/docs/examples/rails-application.md +159 -160
  38. data/docs/examples/tool-usage.md +295 -204
  39. data/docs/getting-started/configuration.md +347 -154
  40. data/docs/getting-started/index.md +1 -1
  41. data/docs/getting-started/installation.md +22 -13
  42. data/docs/getting-started/quick-start.md +166 -121
  43. data/docs/guides/building-robots.md +418 -212
  44. data/docs/guides/creating-networks.md +143 -24
  45. data/docs/guides/index.md +0 -5
  46. data/docs/guides/mcp-integration.md +152 -113
  47. data/docs/guides/memory.md +220 -164
  48. data/docs/guides/rails-integration.md +244 -162
  49. data/docs/guides/streaming.md +137 -187
  50. data/docs/guides/using-tools.md +259 -212
  51. data/docs/index.md +46 -41
  52. data/examples/01_simple_robot.rb +6 -9
  53. data/examples/02_tools.rb +6 -9
  54. data/examples/03_network.rb +19 -17
  55. data/examples/04_mcp.rb +5 -8
  56. data/examples/05_streaming.rb +5 -8
  57. data/examples/06_prompt_templates.rb +42 -37
  58. data/examples/07_network_memory.rb +13 -14
  59. data/examples/08_llm_config.rb +169 -0
  60. data/examples/09_chaining.rb +262 -0
  61. data/examples/10_memory.rb +331 -0
  62. data/examples/11_network_introspection.rb +253 -0
  63. data/examples/12_message_bus.rb +74 -0
  64. data/examples/13_spawn.rb +90 -0
  65. data/examples/14_rusty_circuit/comic.rb +143 -0
  66. data/examples/14_rusty_circuit/display.rb +203 -0
  67. data/examples/14_rusty_circuit/heckler.rb +63 -0
  68. data/examples/14_rusty_circuit/open_mic.rb +123 -0
  69. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  70. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  71. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  72. data/examples/14_rusty_circuit/scout.rb +156 -0
  73. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  74. data/examples/14_rusty_circuit/show.log +234 -0
  75. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  78. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  79. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  80. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  81. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  82. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  83. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  84. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  85. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  86. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  87. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  88. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  89. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  90. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  91. data/examples/16_writers_room/display.rb +158 -0
  92. data/examples/16_writers_room/output/.gitignore +2 -0
  93. data/examples/16_writers_room/output/opus_001.md +263 -0
  94. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  95. data/examples/16_writers_room/prompts/writer.md +37 -0
  96. data/examples/16_writers_room/room.rb +150 -0
  97. data/examples/16_writers_room/tools.rb +162 -0
  98. data/examples/16_writers_room/writer.rb +121 -0
  99. data/examples/16_writers_room/writers_room.rb +162 -0
  100. data/examples/README.md +197 -0
  101. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  102. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  103. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  104. data/examples/prompts/comedian.md +6 -0
  105. data/examples/prompts/comedy_critic.md +10 -0
  106. data/examples/prompts/configurable.md +9 -0
  107. data/examples/prompts/dispatcher.md +12 -0
  108. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  109. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  110. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  111. data/examples/prompts/frontmatter_named_test.md +5 -0
  112. data/examples/prompts/frontmatter_tools_test.md +6 -0
  113. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  114. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  115. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  116. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  117. data/examples/prompts/llm_config_demo.md +20 -0
  118. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  119. data/examples/prompts/os_advocate.md +13 -0
  120. data/examples/prompts/os_chief.md +13 -0
  121. data/examples/prompts/os_editor.md +13 -0
  122. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  123. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  124. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  125. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  126. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  127. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  128. data/lib/robot_lab/ask_user.rb +75 -0
  129. data/lib/robot_lab/config/defaults.yml +121 -0
  130. data/lib/robot_lab/config.rb +183 -0
  131. data/lib/robot_lab/error.rb +6 -0
  132. data/lib/robot_lab/mcp/client.rb +1 -1
  133. data/lib/robot_lab/memory.rb +10 -34
  134. data/lib/robot_lab/network.rb +13 -20
  135. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  136. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  137. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  138. data/lib/robot_lab/robot.rb +240 -330
  139. data/lib/robot_lab/robot_message.rb +44 -0
  140. data/lib/robot_lab/robot_result.rb +1 -0
  141. data/lib/robot_lab/run_config.rb +184 -0
  142. data/lib/robot_lab/state_proxy.rb +2 -12
  143. data/lib/robot_lab/streaming/context.rb +1 -1
  144. data/lib/robot_lab/task.rb +8 -1
  145. data/lib/robot_lab/tool.rb +108 -172
  146. data/lib/robot_lab/tool_config.rb +1 -1
  147. data/lib/robot_lab/tool_manifest.rb +2 -18
  148. data/lib/robot_lab/utils.rb +39 -0
  149. data/lib/robot_lab/version.rb +1 -1
  150. data/lib/robot_lab.rb +89 -57
  151. data/mkdocs.yml +0 -11
  152. metadata +121 -135
  153. data/docs/api/adapters/anthropic.md +0 -121
  154. data/docs/api/adapters/gemini.md +0 -133
  155. data/docs/api/adapters/index.md +0 -104
  156. data/docs/api/adapters/openai.md +0 -134
  157. data/docs/api/history/active-record-adapter.md +0 -195
  158. data/docs/api/history/config.md +0 -191
  159. data/docs/api/history/index.md +0 -132
  160. data/docs/api/history/thread-manager.md +0 -144
  161. data/docs/guides/history.md +0 -359
  162. data/examples/prompts/assistant/user.txt.erb +0 -1
  163. data/examples/prompts/billing/user.txt.erb +0 -1
  164. data/examples/prompts/classifier/user.txt.erb +0 -1
  165. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  166. data/examples/prompts/escalation/user.txt.erb +0 -34
  167. data/examples/prompts/general/user.txt.erb +0 -1
  168. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  169. data/examples/prompts/helper/user.txt.erb +0 -1
  170. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  171. data/examples/prompts/order_support/user.txt.erb +0 -22
  172. data/examples/prompts/product_support/user.txt.erb +0 -32
  173. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  174. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  175. data/examples/prompts/technical/user.txt.erb +0 -1
  176. data/examples/prompts/triage/user.txt.erb +0 -17
  177. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  178. data/lib/robot_lab/adapters/base.rb +0 -85
  179. data/lib/robot_lab/adapters/gemini.rb +0 -193
  180. data/lib/robot_lab/adapters/openai.rb +0 -159
  181. data/lib/robot_lab/adapters/registry.rb +0 -81
  182. data/lib/robot_lab/configuration.rb +0 -143
  183. data/lib/robot_lab/errors.rb +0 -70
  184. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  185. data/lib/robot_lab/history/config.rb +0 -115
  186. data/lib/robot_lab/history/thread_manager.rb +0 -93
  187. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -1,324 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- # Thread-local storage for capturing tool executions during RubyLLM auto-execution.
5
- #
6
- # Stores tool execution records in thread-local storage so they can be
7
- # retrieved after an LLM inference call completes.
8
- #
9
- class ToolExecutionCapture
10
- # Returns the captured tool executions for the current thread.
11
- #
12
- # @return [Array<Hash>] array of execution records
13
- def self.captured
14
- Thread.current[:robot_lab_tool_executions] ||= []
15
- end
16
-
17
- # Clears the captured tool executions for the current thread.
18
- #
19
- # @return [Array] empty array
20
- def self.clear!
21
- Thread.current[:robot_lab_tool_executions] = []
22
- end
23
-
24
- # Records a tool execution.
25
- #
26
- # @param tool_name [String] name of the executed tool
27
- # @param tool_id [String] unique identifier for this execution
28
- # @param input [Hash] input parameters passed to the tool
29
- # @param output [Object] the tool's return value
30
- # @return [Array<Hash>] the updated captured array
31
- def self.record(tool_name:, tool_id:, input:, output:)
32
- captured << {
33
- tool_name: tool_name,
34
- tool_id: tool_id,
35
- input: input,
36
- output: output
37
- }
38
- end
39
- end
40
-
41
- # Wrapper around ruby_llm for LLM inference
42
- #
43
- # RoboticModel provides a unified interface for LLM calls, handling:
44
- # - Message format conversion via adapters
45
- # - Tool registration and execution
46
- # - Streaming support
47
- #
48
- # @example Basic usage
49
- # model = RoboticModel.new("claude-sonnet-4", provider: :anthropic)
50
- # messages = [TextMessage.new(role: :user, content: "Hello!")]
51
- # response = model.infer(messages, [])
52
- #
53
- # @example With tools
54
- # model.infer(messages, [weather_tool], tool_choice: "auto")
55
- #
56
- class RoboticModel
57
- # @!attribute [r] model_id
58
- # @return [String] the LLM model identifier
59
- # @!attribute [r] provider
60
- # @return [Symbol] the LLM provider (:anthropic, :openai, :gemini, etc.)
61
- # @!attribute [r] adapter
62
- # @return [Adapters::Base] the adapter for message conversion
63
- attr_reader :model_id, :provider, :adapter
64
-
65
- # Creates a new RoboticModel instance.
66
- #
67
- # @param model_id [String] the model identifier
68
- # @param provider [Symbol, nil] the provider (auto-detected if not specified)
69
- def initialize(model_id, provider: nil)
70
- @model_id = model_id
71
- @provider = provider || detect_provider(model_id)
72
- @adapter = Adapters::Registry.for(@provider)
73
- end
74
-
75
- # Perform inference with messages and optional tools
76
- #
77
- # @param messages [Array<Message>] Conversation messages
78
- # @param tools [Array<Tool>] Available tools
79
- # @param tool_choice [String, Symbol] Tool selection mode
80
- # @param streaming [Proc, nil] Streaming callback
81
- # @return [InferenceResponse]
82
- #
83
- def infer(messages, tools = [], tool_choice: "auto", streaming: nil, &block)
84
- chat = create_chat
85
-
86
- # Register tools if any
87
- if tools.any?
88
- ruby_llm_tools = create_ruby_llm_tools(tools)
89
- chat = chat.with_tools(*ruby_llm_tools)
90
- end
91
-
92
- # Add system message if present
93
- system_content = @adapter.extract_system_message(messages)
94
- chat = chat.with_instructions(system_content) if system_content
95
-
96
- # Build conversation (excluding the last user message since ask() will add it)
97
- conversation = @adapter.conversation_messages(messages)
98
- conversation[0...-1].each do |msg|
99
- add_message_to_chat(chat, msg)
100
- end
101
-
102
- # Make the request (ask adds the user message)
103
- user_content = conversation.last&.content || ""
104
-
105
- # Clear tool execution capture before making the request
106
- ToolExecutionCapture.clear!
107
-
108
- response = if block_given? || streaming
109
- chat.ask(user_content, &(block || streaming))
110
- else
111
- chat.ask(user_content)
112
- end
113
-
114
- # Parse response
115
- output = @adapter.parse_response(response)
116
-
117
- # Build captured tool results from auto-executed tools
118
- captured_tool_results = build_captured_tool_results(tools)
119
-
120
- InferenceResponse.new(
121
- output: output,
122
- raw: response,
123
- model: model_id,
124
- provider: provider,
125
- captured_tool_results: captured_tool_results
126
- )
127
- end
128
-
129
- # Quick ask without full message array
130
- #
131
- # @param prompt [String] User prompt
132
- # @param system [String, nil] System prompt
133
- # @param tools [Array<Tool>] Available tools
134
- # @return [InferenceResponse]
135
- #
136
- def ask(prompt, system: nil, tools: [], &block)
137
- messages = []
138
- messages << TextMessage.new(role: "system", content: system) if system
139
- messages << TextMessage.new(role: "user", content: prompt)
140
-
141
- infer(messages, tools, &block)
142
- end
143
-
144
- private
145
-
146
- def create_chat
147
- RubyLLM.chat(model: model_id, provider: provider)
148
- end
149
-
150
- def create_ruby_llm_tools(tools)
151
- tools.map do |tool|
152
- # Create a dynamic RubyLLM::Tool subclass
153
- create_tool_class(tool)
154
- end
155
- end
156
-
157
- def create_tool_class(tool)
158
- # Build a RubyLLM::Tool subclass dynamically
159
- tool_definition = tool
160
- tool_name = tool.name
161
-
162
- klass = Class.new(RubyLLM::Tool) do
163
- description tool_definition.description || ""
164
-
165
- # Add parameters from schema
166
- schema = tool_definition.to_json_schema
167
- if schema[:parameters] && schema[:parameters][:properties]
168
- schema[:parameters][:properties].each do |prop_name, prop_def|
169
- required = schema[:parameters][:required]&.include?(prop_name.to_s)
170
- param prop_name.to_sym,
171
- type: prop_def[:type] || "string",
172
- desc: prop_def[:description],
173
- required: required
174
- end
175
- end
176
-
177
- define_method(:execute) do |**kwargs|
178
- # This is called by ruby_llm when the tool is invoked
179
- # Call the handler directly (bypassing Tool#call which requires context)
180
- # Handlers should use **_context pattern to accept but ignore context
181
- output = tool_definition.handler.call(kwargs, robot: nil, network: nil, step: nil)
182
-
183
- # Record the execution for later retrieval
184
- ToolExecutionCapture.record(
185
- tool_name: tool_name,
186
- tool_id: SecureRandom.uuid,
187
- input: kwargs,
188
- output: output
189
- )
190
-
191
- output
192
- end
193
- end
194
-
195
- # Set the class name so RubyLLM can identify the tool
196
- # RubyLLM converts class names to snake_case for tool identification
197
- class_name = tool_name.split("_").map(&:capitalize).join
198
- klass.define_singleton_method(:name) { class_name }
199
-
200
- # Also define instance method for name (used by some RubyLLM code paths)
201
- klass.define_method(:name) { tool_name }
202
-
203
- # Store reference to our tool for later execution
204
- klass.define_singleton_method(:robot_lab_tool) { tool_definition }
205
- klass
206
- end
207
-
208
- def build_captured_tool_results(tools)
209
- ToolExecutionCapture.captured.map do |capture|
210
- _tool = tools.find { |t| t.name == capture[:tool_name] }
211
- tool_message = ToolMessage.new(
212
- id: capture[:tool_id],
213
- name: capture[:tool_name],
214
- input: capture[:input]
215
- )
216
- ToolResultMessage.new(
217
- tool: tool_message,
218
- content: { data: capture[:output] }
219
- )
220
- end
221
- end
222
-
223
- def add_message_to_chat(chat, msg)
224
- case msg
225
- when TextMessage
226
- if msg.user?
227
- chat.add_message(role: :user, content: msg.content)
228
- elsif msg.assistant?
229
- chat.add_message(role: :assistant, content: msg.content)
230
- end
231
- when ToolResultMessage
232
- # Tool results are handled by ruby_llm internally
233
- end
234
- end
235
-
236
- def detect_provider(model_id)
237
- case model_id.to_s.downcase
238
- when /^claude/, /^anthropic/
239
- :anthropic
240
- when /^gpt/, /^o1/, /^o3/, /^chatgpt/
241
- :openai
242
- when /^gemini/
243
- :gemini
244
- when /^llama/, /^mistral/, /^mixtral/
245
- :ollama
246
- else
247
- RobotLab.configuration.default_provider
248
- end
249
- end
250
- end
251
-
252
- # Response from LLM inference.
253
- #
254
- # Contains the parsed output, raw response, and any captured tool results.
255
- #
256
- class InferenceResponse
257
- # @!attribute [r] output
258
- # @return [Array<Message>] parsed output messages
259
- # @!attribute [r] raw
260
- # @return [Object] the raw response from RubyLLM
261
- # @!attribute [r] model
262
- # @return [String] the model that generated the response
263
- # @!attribute [r] provider
264
- # @return [Symbol] the provider that handled the request
265
- # @!attribute [r] captured_tool_results
266
- # @return [Array<ToolResultMessage>] tool executions that were auto-executed
267
- attr_reader :output, :raw, :model, :provider, :captured_tool_results
268
-
269
- # Creates a new InferenceResponse instance.
270
- #
271
- # @param output [Array<Message>] parsed output messages
272
- # @param raw [Object] raw response from RubyLLM
273
- # @param model [String] model identifier
274
- # @param provider [Symbol] provider identifier
275
- # @param captured_tool_results [Array<ToolResultMessage>] captured results
276
- def initialize(output:, raw:, model:, provider:, captured_tool_results: [])
277
- @output = output
278
- @raw = raw
279
- @model = model
280
- @provider = provider
281
- @captured_tool_results = captured_tool_results
282
- end
283
-
284
- # Get the stop reason from the last output message
285
- #
286
- # @return [String, nil]
287
- #
288
- def stop_reason
289
- output.last&.stop_reason
290
- end
291
-
292
- # Check if inference stopped naturally
293
- #
294
- # @return [Boolean]
295
- #
296
- def stopped?
297
- stop_reason == "stop"
298
- end
299
-
300
- # Check if inference wants to call tools
301
- #
302
- # @return [Boolean]
303
- #
304
- def wants_tools?
305
- stop_reason == "tool" || output.any?(&:tool_call?)
306
- end
307
-
308
- # Get all tool calls from the response
309
- #
310
- # @return [Array<ToolMessage>]
311
- #
312
- def tool_calls
313
- output.select(&:tool_call?).flat_map(&:tools)
314
- end
315
-
316
- # Get the text content
317
- #
318
- # @return [String, nil]
319
- #
320
- def text_content
321
- output.select(&:text?).map(&:content).join
322
- end
323
- end
324
- end