robot_lab 0.0.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.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.github/workflows/deploy-yard-docs.yml +52 -0
  5. data/CHANGELOG.md +55 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +332 -0
  9. data/Rakefile +67 -0
  10. data/docs/api/adapters/anthropic.md +121 -0
  11. data/docs/api/adapters/gemini.md +133 -0
  12. data/docs/api/adapters/index.md +104 -0
  13. data/docs/api/adapters/openai.md +134 -0
  14. data/docs/api/core/index.md +113 -0
  15. data/docs/api/core/memory.md +314 -0
  16. data/docs/api/core/network.md +291 -0
  17. data/docs/api/core/robot.md +273 -0
  18. data/docs/api/core/state.md +273 -0
  19. data/docs/api/core/tool.md +353 -0
  20. data/docs/api/history/active-record-adapter.md +195 -0
  21. data/docs/api/history/config.md +191 -0
  22. data/docs/api/history/index.md +132 -0
  23. data/docs/api/history/thread-manager.md +144 -0
  24. data/docs/api/index.md +82 -0
  25. data/docs/api/mcp/client.md +221 -0
  26. data/docs/api/mcp/index.md +111 -0
  27. data/docs/api/mcp/server.md +225 -0
  28. data/docs/api/mcp/transports.md +264 -0
  29. data/docs/api/messages/index.md +67 -0
  30. data/docs/api/messages/text-message.md +102 -0
  31. data/docs/api/messages/tool-call-message.md +144 -0
  32. data/docs/api/messages/tool-result-message.md +154 -0
  33. data/docs/api/messages/user-message.md +171 -0
  34. data/docs/api/streaming/context.md +174 -0
  35. data/docs/api/streaming/events.md +237 -0
  36. data/docs/api/streaming/index.md +108 -0
  37. data/docs/architecture/core-concepts.md +243 -0
  38. data/docs/architecture/index.md +138 -0
  39. data/docs/architecture/message-flow.md +320 -0
  40. data/docs/architecture/network-orchestration.md +216 -0
  41. data/docs/architecture/robot-execution.md +243 -0
  42. data/docs/architecture/state-management.md +323 -0
  43. data/docs/assets/css/custom.css +56 -0
  44. data/docs/assets/images/robot_lab.jpg +0 -0
  45. data/docs/concepts.md +216 -0
  46. data/docs/examples/basic-chat.md +193 -0
  47. data/docs/examples/index.md +129 -0
  48. data/docs/examples/mcp-server.md +290 -0
  49. data/docs/examples/multi-robot-network.md +312 -0
  50. data/docs/examples/rails-application.md +420 -0
  51. data/docs/examples/tool-usage.md +310 -0
  52. data/docs/getting-started/configuration.md +230 -0
  53. data/docs/getting-started/index.md +56 -0
  54. data/docs/getting-started/installation.md +179 -0
  55. data/docs/getting-started/quick-start.md +203 -0
  56. data/docs/guides/building-robots.md +376 -0
  57. data/docs/guides/creating-networks.md +366 -0
  58. data/docs/guides/history.md +359 -0
  59. data/docs/guides/index.md +68 -0
  60. data/docs/guides/mcp-integration.md +356 -0
  61. data/docs/guides/memory.md +309 -0
  62. data/docs/guides/rails-integration.md +432 -0
  63. data/docs/guides/streaming.md +314 -0
  64. data/docs/guides/using-tools.md +394 -0
  65. data/docs/index.md +160 -0
  66. data/examples/01_simple_robot.rb +38 -0
  67. data/examples/02_tools.rb +106 -0
  68. data/examples/03_network.rb +103 -0
  69. data/examples/04_mcp.rb +219 -0
  70. data/examples/05_streaming.rb +124 -0
  71. data/examples/06_prompt_templates.rb +324 -0
  72. data/examples/07_network_memory.rb +329 -0
  73. data/examples/prompts/assistant/system.txt.erb +2 -0
  74. data/examples/prompts/assistant/user.txt.erb +1 -0
  75. data/examples/prompts/billing/system.txt.erb +7 -0
  76. data/examples/prompts/billing/user.txt.erb +1 -0
  77. data/examples/prompts/classifier/system.txt.erb +4 -0
  78. data/examples/prompts/classifier/user.txt.erb +1 -0
  79. data/examples/prompts/entity_extractor/system.txt.erb +11 -0
  80. data/examples/prompts/entity_extractor/user.txt.erb +3 -0
  81. data/examples/prompts/escalation/system.txt.erb +35 -0
  82. data/examples/prompts/escalation/user.txt.erb +34 -0
  83. data/examples/prompts/general/system.txt.erb +4 -0
  84. data/examples/prompts/general/user.txt.erb +1 -0
  85. data/examples/prompts/github_assistant/system.txt.erb +6 -0
  86. data/examples/prompts/github_assistant/user.txt.erb +1 -0
  87. data/examples/prompts/helper/system.txt.erb +1 -0
  88. data/examples/prompts/helper/user.txt.erb +1 -0
  89. data/examples/prompts/keyword_extractor/system.txt.erb +8 -0
  90. data/examples/prompts/keyword_extractor/user.txt.erb +3 -0
  91. data/examples/prompts/order_support/system.txt.erb +27 -0
  92. data/examples/prompts/order_support/user.txt.erb +22 -0
  93. data/examples/prompts/product_support/system.txt.erb +30 -0
  94. data/examples/prompts/product_support/user.txt.erb +32 -0
  95. data/examples/prompts/sentiment_analyzer/system.txt.erb +9 -0
  96. data/examples/prompts/sentiment_analyzer/user.txt.erb +3 -0
  97. data/examples/prompts/synthesizer/system.txt.erb +14 -0
  98. data/examples/prompts/synthesizer/user.txt.erb +15 -0
  99. data/examples/prompts/technical/system.txt.erb +7 -0
  100. data/examples/prompts/technical/user.txt.erb +1 -0
  101. data/examples/prompts/triage/system.txt.erb +16 -0
  102. data/examples/prompts/triage/user.txt.erb +17 -0
  103. data/lib/generators/robot_lab/install_generator.rb +78 -0
  104. data/lib/generators/robot_lab/robot_generator.rb +55 -0
  105. data/lib/generators/robot_lab/templates/initializer.rb.tt +41 -0
  106. data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
  107. data/lib/generators/robot_lab/templates/result_model.rb.tt +52 -0
  108. data/lib/generators/robot_lab/templates/robot.rb.tt +46 -0
  109. data/lib/generators/robot_lab/templates/robot_test.rb.tt +32 -0
  110. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +53 -0
  111. data/lib/generators/robot_lab/templates/thread_model.rb.tt +40 -0
  112. data/lib/robot_lab/adapters/anthropic.rb +163 -0
  113. data/lib/robot_lab/adapters/base.rb +85 -0
  114. data/lib/robot_lab/adapters/gemini.rb +193 -0
  115. data/lib/robot_lab/adapters/openai.rb +159 -0
  116. data/lib/robot_lab/adapters/registry.rb +81 -0
  117. data/lib/robot_lab/configuration.rb +143 -0
  118. data/lib/robot_lab/error.rb +32 -0
  119. data/lib/robot_lab/errors.rb +70 -0
  120. data/lib/robot_lab/history/active_record_adapter.rb +146 -0
  121. data/lib/robot_lab/history/config.rb +115 -0
  122. data/lib/robot_lab/history/thread_manager.rb +93 -0
  123. data/lib/robot_lab/mcp/client.rb +210 -0
  124. data/lib/robot_lab/mcp/server.rb +84 -0
  125. data/lib/robot_lab/mcp/transports/base.rb +56 -0
  126. data/lib/robot_lab/mcp/transports/sse.rb +117 -0
  127. data/lib/robot_lab/mcp/transports/stdio.rb +133 -0
  128. data/lib/robot_lab/mcp/transports/streamable_http.rb +139 -0
  129. data/lib/robot_lab/mcp/transports/websocket.rb +108 -0
  130. data/lib/robot_lab/memory.rb +882 -0
  131. data/lib/robot_lab/memory_change.rb +123 -0
  132. data/lib/robot_lab/message.rb +357 -0
  133. data/lib/robot_lab/network.rb +350 -0
  134. data/lib/robot_lab/rails/engine.rb +29 -0
  135. data/lib/robot_lab/rails/railtie.rb +42 -0
  136. data/lib/robot_lab/robot.rb +560 -0
  137. data/lib/robot_lab/robot_result.rb +205 -0
  138. data/lib/robot_lab/robotic_model.rb +324 -0
  139. data/lib/robot_lab/state_proxy.rb +188 -0
  140. data/lib/robot_lab/streaming/context.rb +144 -0
  141. data/lib/robot_lab/streaming/events.rb +95 -0
  142. data/lib/robot_lab/streaming/sequence_counter.rb +48 -0
  143. data/lib/robot_lab/task.rb +117 -0
  144. data/lib/robot_lab/tool.rb +223 -0
  145. data/lib/robot_lab/tool_config.rb +112 -0
  146. data/lib/robot_lab/tool_manifest.rb +234 -0
  147. data/lib/robot_lab/user_message.rb +118 -0
  148. data/lib/robot_lab/version.rb +5 -0
  149. data/lib/robot_lab/waiter.rb +73 -0
  150. data/lib/robot_lab.rb +195 -0
  151. data/mkdocs.yml +214 -0
  152. data/sig/robot_lab.rbs +4 -0
  153. metadata +442 -0
@@ -0,0 +1,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simple_flow"
4
+
5
+ module RobotLab
6
+ # Orchestrates multiple robots in a pipeline workflow
7
+ #
8
+ # Network is a thin wrapper around SimpleFlow::Pipeline that provides
9
+ # a clean DSL for defining robot workflows with sequential, parallel,
10
+ # and conditional execution.
11
+ #
12
+ # == Shared Memory
13
+ #
14
+ # Networks provide a shared reactive memory that all robots can read and write.
15
+ # Robots can subscribe to memory keys and be notified when values change,
16
+ # or use blocking reads to wait for values from other robots.
17
+ #
18
+ # == Broadcast Messages
19
+ #
20
+ # Networks support a broadcast channel for network-wide announcements.
21
+ # Use `broadcast` to send messages to all robots, and `on_broadcast` to
22
+ # register handlers for incoming broadcasts.
23
+ #
24
+ # @example Sequential execution
25
+ # network = RobotLab.create_network(name: "pipeline") do
26
+ # task :analyst, analyst_robot, depends_on: :none
27
+ # task :writer, writer_robot, depends_on: [:analyst]
28
+ # end
29
+ #
30
+ # @example With per-task context
31
+ # network = RobotLab.create_network(name: "support") do
32
+ # task :classifier, classifier_robot, depends_on: :none
33
+ # task :billing, billing_robot,
34
+ # context: { department: "billing" },
35
+ # tools: [RefundTool],
36
+ # depends_on: :optional
37
+ # end
38
+ #
39
+ # @example Parallel execution with shared memory
40
+ # network = RobotLab.create_network(name: "analysis") do
41
+ # task :fetch, fetcher_robot, depends_on: :none
42
+ # task :sentiment, sentiment_robot, depends_on: [:fetch]
43
+ # task :entities, entity_robot, depends_on: [:fetch]
44
+ # task :summarize, summary_robot, depends_on: [:sentiment, :entities]
45
+ # end
46
+ #
47
+ # # In sentiment_robot:
48
+ # memory.set(:sentiment, analyze_sentiment(text))
49
+ #
50
+ # # In summarize_robot:
51
+ # results = memory.get(:sentiment, :entities, wait: 60)
52
+ #
53
+ # @example Broadcasting
54
+ # network.on_broadcast do |message|
55
+ # puts "Received: #{message[:event]}"
56
+ # end
57
+ #
58
+ # network.broadcast(event: :pause, reason: "rate limit")
59
+ #
60
+ class Network
61
+ # Reserved key for broadcast messages in memory
62
+ BROADCAST_KEY = :_network_broadcast
63
+
64
+ # @!attribute [r] name
65
+ # @return [String] unique identifier for the network
66
+ # @!attribute [r] pipeline
67
+ # @return [SimpleFlow::Pipeline] the underlying pipeline
68
+ # @!attribute [r] robots
69
+ # @return [Hash<String, Robot>] robots in this network, keyed by name
70
+ # @!attribute [r] memory
71
+ # @return [Memory] shared memory for all robots in the network
72
+ attr_reader :name, :pipeline, :robots, :memory
73
+
74
+ # Creates a new Network instance.
75
+ #
76
+ # @param name [String] unique identifier for the network
77
+ # @param concurrency [Symbol] concurrency model (:auto, :threads, :async)
78
+ # @param memory [Memory, nil] optional pre-configured memory instance
79
+ # @yield Block for defining pipeline tasks
80
+ #
81
+ # @example
82
+ # network = Network.new(name: "support") do
83
+ # task :classifier, classifier, depends_on: :none
84
+ # task :billing, billing_robot, context: { dept: "billing" }, depends_on: :optional
85
+ # end
86
+ #
87
+ def initialize(name:, concurrency: :auto, memory: nil, &block)
88
+ @name = name.to_s
89
+ @robots = {}
90
+ @tasks = {}
91
+ @pipeline = SimpleFlow::Pipeline.new(concurrency: concurrency)
92
+ @memory = memory || Memory.new(network_name: @name)
93
+ @broadcast_handlers = []
94
+
95
+ instance_eval(&block) if block_given?
96
+ end
97
+
98
+ # Add a robot as a pipeline task with optional per-task configuration
99
+ #
100
+ # @param name [Symbol] task name
101
+ # @param robot [Robot] the robot instance
102
+ # @param context [Hash] task-specific context (deep-merged with run params)
103
+ # @param mcp [Symbol, Array] MCP server config (:none, :inherit, or array)
104
+ # @param tools [Symbol, Array] tools config (:none, :inherit, or array)
105
+ # @param memory [Memory, Hash, nil] task-specific memory
106
+ # @param depends_on [Symbol, Array<Symbol>] dependencies (:none, :optional, or task names)
107
+ # @return [self]
108
+ #
109
+ # @example Entry point task
110
+ # task :classifier, classifier_robot, depends_on: :none
111
+ #
112
+ # @example Task with context and tools
113
+ # task :billing, billing_robot,
114
+ # context: { department: "billing", escalation: 2 },
115
+ # tools: [RefundTool, InvoiceTool],
116
+ # depends_on: :optional
117
+ #
118
+ # @example Task with dependencies
119
+ # task :writer, writer_robot, depends_on: [:analyst]
120
+ #
121
+ def task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, depends_on: :none)
122
+ task_wrapper = Task.new(
123
+ name: name,
124
+ robot: robot,
125
+ context: context,
126
+ mcp: mcp,
127
+ tools: tools,
128
+ memory: memory
129
+ )
130
+
131
+ @robots[name.to_s] = robot
132
+ @tasks[name.to_s] = task_wrapper
133
+ @pipeline.step(name, task_wrapper, depends_on: depends_on)
134
+ self
135
+ end
136
+
137
+ # Define a parallel execution block
138
+ #
139
+ # @param name [Symbol, nil] optional name for the parallel group
140
+ # @param depends_on [Symbol, Array] dependencies for this group
141
+ # @yield Block containing task definitions
142
+ # @return [self]
143
+ #
144
+ # @example Named parallel group
145
+ # parallel :fetch_data, depends_on: :validate do
146
+ # task :fetch_orders, orders_robot
147
+ # task :fetch_products, products_robot
148
+ # end
149
+ # task :process, processor, depends_on: :fetch_data
150
+ #
151
+ def parallel(name = nil, depends_on: :none, &block)
152
+ @pipeline.parallel(name, depends_on: depends_on, &block)
153
+ self
154
+ end
155
+
156
+ # Run the network with the given context
157
+ #
158
+ # All robots share the network's memory during execution. The memory
159
+ # is passed to each robot and can be used for inter-robot communication.
160
+ #
161
+ # @param run_context [Hash] context passed to all robots (message:, user_id:, etc.)
162
+ # @return [SimpleFlow::Result] final pipeline result
163
+ #
164
+ # @example
165
+ # result = network.run(message: "I need help with billing", user_id: 123)
166
+ # result.value # => RobotResult from last robot
167
+ # result.context[:classifier] # => RobotResult from classifier
168
+ #
169
+ def run(**run_context)
170
+ # Include shared memory in run params so robots can access it
171
+ run_context[:network_memory] = @memory
172
+
173
+ initial_result = SimpleFlow::Result.new(
174
+ run_context,
175
+ context: { run_params: run_context }
176
+ )
177
+
178
+ @pipeline.call_parallel(initial_result)
179
+ end
180
+
181
+ # Broadcast a message to all robots in the network.
182
+ #
183
+ # This sends a network-wide message that all robots subscribed via
184
+ # `on_broadcast` will receive asynchronously.
185
+ #
186
+ # @param payload [Hash] the message payload
187
+ # @return [self]
188
+ #
189
+ # @example Pause all robots
190
+ # network.broadcast(event: :pause, reason: "rate limit hit")
191
+ #
192
+ # @example Signal completion
193
+ # network.broadcast(event: :phase_complete, phase: "analysis")
194
+ #
195
+ def broadcast(payload)
196
+ message = {
197
+ payload: payload,
198
+ network: @name,
199
+ timestamp: Time.now
200
+ }
201
+
202
+ # Notify handlers asynchronously
203
+ @broadcast_handlers.each do |handler|
204
+ dispatch_async { handler.call(message) }
205
+ end
206
+
207
+ # Also set in memory so robots can subscribe via memory.subscribe
208
+ @memory.set(BROADCAST_KEY, message)
209
+
210
+ self
211
+ end
212
+
213
+ # Register a handler for broadcast messages.
214
+ #
215
+ # The handler is called asynchronously whenever `broadcast` is called.
216
+ #
217
+ # @yield [Hash] the broadcast message with :payload, :network, :timestamp
218
+ # @return [self]
219
+ #
220
+ # @example
221
+ # network.on_broadcast do |message|
222
+ # case message[:payload][:event]
223
+ # when :pause
224
+ # pause_current_work
225
+ # when :resume
226
+ # resume_work
227
+ # end
228
+ # end
229
+ #
230
+ def on_broadcast(&block)
231
+ raise ArgumentError, "Block required for on_broadcast" unless block_given?
232
+
233
+ @broadcast_handlers << block
234
+ self
235
+ end
236
+
237
+ # Reset the shared memory.
238
+ #
239
+ # Clears all values in the network's shared memory. This is useful
240
+ # between runs if you want to start with a fresh memory state.
241
+ #
242
+ # @return [self]
243
+ #
244
+ def reset_memory
245
+ @memory.reset
246
+ self
247
+ end
248
+
249
+ # Get a robot by name
250
+ #
251
+ # @param name [String, Symbol]
252
+ # @return [Robot, nil]
253
+ #
254
+ def robot(name)
255
+ @robots[name.to_s]
256
+ end
257
+
258
+ # @!method [](name)
259
+ # Alias for {#robot}.
260
+ # @param name [String, Symbol] the robot name
261
+ # @return [Robot, nil]
262
+ alias [] robot
263
+
264
+ # Get all robots in the network
265
+ #
266
+ # @return [Array<Robot>]
267
+ #
268
+ def available_robots
269
+ @robots.values
270
+ end
271
+
272
+ # Add a robot to the network without adding it as a task
273
+ #
274
+ # Useful for dynamically adding robots that will be referenced later.
275
+ #
276
+ # @param robot [Robot] the robot instance to add
277
+ # @return [self]
278
+ # @raise [ArgumentError] if a robot with the same name already exists
279
+ #
280
+ def add_robot(robot)
281
+ if @robots.key?(robot.name)
282
+ raise ArgumentError, "Robot '#{robot.name}' already exists in network '#{@name}'"
283
+ end
284
+
285
+ @robots[robot.name] = robot
286
+ self
287
+ end
288
+
289
+ # Visualize the pipeline as ASCII
290
+ #
291
+ # @return [String, nil]
292
+ #
293
+ def visualize
294
+ @pipeline.visualize_ascii
295
+ end
296
+
297
+ # Export pipeline to Mermaid format
298
+ #
299
+ # @return [String, nil]
300
+ #
301
+ def to_mermaid
302
+ @pipeline.visualize_mermaid
303
+ end
304
+
305
+ # Export pipeline to DOT format (Graphviz)
306
+ #
307
+ # @return [String, nil]
308
+ #
309
+ def to_dot
310
+ @pipeline.visualize_dot
311
+ end
312
+
313
+ # Get the execution plan
314
+ #
315
+ # @return [String, nil]
316
+ #
317
+ def execution_plan
318
+ @pipeline.execution_plan
319
+ end
320
+
321
+ # Converts the network to a hash representation
322
+ #
323
+ # @return [Hash]
324
+ #
325
+ def to_h
326
+ {
327
+ name: name,
328
+ robots: @robots.keys,
329
+ tasks: @tasks.keys,
330
+ optional_tasks: @pipeline.optional_steps.to_a
331
+ }.compact
332
+ end
333
+
334
+ private
335
+
336
+ def dispatch_async(&block)
337
+ # Use Async if available (preferred for fiber-based concurrency)
338
+ if defined?(Async) && Async::Task.current?
339
+ Async { block.call }
340
+ else
341
+ # Fall back to Thread for basic async dispatch
342
+ Thread.new do
343
+ block.call
344
+ rescue StandardError => e
345
+ warn "Network broadcast handler error: #{e.message}"
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ module Rails
5
+ # Rails Engine for RobotLab integration
6
+ #
7
+ # Provides automatic loading of RobotLab components and
8
+ # integration with Rails applications.
9
+ #
10
+ class Engine < ::Rails::Engine
11
+ isolate_namespace RobotLab
12
+
13
+ initializer "robot_lab.configure" do |app|
14
+ # Load configuration from Rails config
15
+ app.config.robot_lab ||= ActiveSupport::OrderedOptions.new
16
+ end
17
+
18
+ initializer "robot_lab.add_autoload_paths", before: :set_autoload_paths do |app|
19
+ app.config.autoload_paths << root.join("app", "robots")
20
+ app.config.autoload_paths << root.join("app", "tools")
21
+ end
22
+
23
+ config.generators do |g|
24
+ g.test_framework :minitest, fixture: false
25
+ g.fixture_replacement nil
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ module Rails
5
+ # Railtie for RobotLab Rails integration
6
+ #
7
+ # Provides configuration hooks and initialization for
8
+ # Rails applications using RobotLab.
9
+ #
10
+ class Railtie < ::Rails::Railtie
11
+ config.robot_lab = ActiveSupport::OrderedOptions.new
12
+
13
+ initializer "robot_lab.configuration" do |app|
14
+ RobotLab.configure do |config|
15
+ # Apply Rails-specific configuration
16
+ rails_config = app.config.robot_lab
17
+
18
+ config.default_model = rails_config.default_model if rails_config.default_model
19
+ config.default_provider = rails_config.default_provider if rails_config.default_provider
20
+ config.logger = ::Rails.logger
21
+ end
22
+ end
23
+
24
+ initializer "robot_lab.active_record" do
25
+ ActiveSupport.on_load(:active_record) do
26
+ # Extend ActiveRecord with RobotLab concerns if needed
27
+ end
28
+ end
29
+
30
+ rake_tasks do
31
+ # Load RobotLab rake tasks
32
+ path = File.expand_path("../tasks", __dir__)
33
+ Dir.glob("#{path}/**/*.rake").each { |f| load f }
34
+ end
35
+
36
+ generators do
37
+ require "generators/robot_lab/install_generator"
38
+ require "generators/robot_lab/robot_generator"
39
+ end
40
+ end
41
+ end
42
+ end