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,312 @@
1
+ # Multi-Robot Network
2
+
3
+ Customer service system with intelligent routing using SimpleFlow pipelines.
4
+
5
+ ## Overview
6
+
7
+ This example demonstrates a multi-robot network where a classifier routes customer inquiries to specialized support robots using optional task activation.
8
+
9
+ ## Complete Example
10
+
11
+ ```ruby
12
+ #!/usr/bin/env ruby
13
+ # examples/customer_service.rb
14
+
15
+ require "bundler/setup"
16
+ require "robot_lab"
17
+
18
+ RobotLab.configure do |config|
19
+ config.default_model = "claude-sonnet-4"
20
+ end
21
+
22
+ # Custom classifier that routes to specialists
23
+ class ClassifierRobot < RobotLab::Robot
24
+ def call(result)
25
+ robot_result = run(**extract_run_context(result))
26
+
27
+ new_result = result
28
+ .with_context(@name.to_sym, robot_result)
29
+ .continue(robot_result)
30
+
31
+ # Route based on classification
32
+ category = robot_result.last_text_content.to_s.strip.downcase
33
+
34
+ case category
35
+ when /billing/ then new_result.activate(:billing_agent)
36
+ when /technical/ then new_result.activate(:tech_agent)
37
+ when /account/ then new_result.activate(:account_agent)
38
+ else new_result.activate(:general_agent)
39
+ end
40
+ end
41
+ end
42
+
43
+ # Classifier robot
44
+ classifier = ClassifierRobot.new(
45
+ name: "classifier",
46
+ description: "Classifies customer inquiries",
47
+ system_prompt: <<~PROMPT
48
+ You are a customer inquiry classifier. Analyze the customer's message
49
+ and respond with exactly ONE of these categories:
50
+
51
+ - BILLING (payment issues, invoices, refunds, subscriptions)
52
+ - TECHNICAL (bugs, errors, how-to questions, feature requests)
53
+ - ACCOUNT (login issues, profile changes, security concerns)
54
+ - GENERAL (everything else)
55
+
56
+ Respond with ONLY the category name, nothing else.
57
+ PROMPT
58
+ )
59
+
60
+ # Billing specialist
61
+ billing_agent = RobotLab.build(
62
+ name: "billing_agent",
63
+ description: "Handles billing inquiries",
64
+ system_prompt: <<~PROMPT
65
+ You are a billing support specialist. You help customers with:
66
+ - Payment issues and refunds
67
+ - Invoice questions
68
+ - Subscription management
69
+ - Pricing inquiries
70
+
71
+ Be helpful, empathetic, and provide clear next steps.
72
+ PROMPT
73
+ )
74
+
75
+ # Technical support
76
+ tech_agent = RobotLab.build(
77
+ name: "tech_agent",
78
+ description: "Handles technical issues",
79
+ system_prompt: <<~PROMPT
80
+ You are a technical support specialist. You help customers with:
81
+ - Bug reports and troubleshooting
82
+ - Feature explanations
83
+ - Integration questions
84
+ - Best practices
85
+
86
+ Ask clarifying questions when needed. Provide step-by-step solutions.
87
+ PROMPT
88
+ )
89
+
90
+ # Account specialist
91
+ account_agent = RobotLab.build(
92
+ name: "account_agent",
93
+ description: "Handles account issues",
94
+ system_prompt: <<~PROMPT
95
+ You are an account support specialist. You help customers with:
96
+ - Login and authentication issues
97
+ - Profile and settings changes
98
+ - Security concerns
99
+ - Account recovery
100
+
101
+ Prioritize security while being helpful.
102
+ PROMPT
103
+ )
104
+
105
+ # General support
106
+ general_agent = RobotLab.build(
107
+ name: "general_agent",
108
+ description: "Handles general inquiries",
109
+ system_prompt: <<~PROMPT
110
+ You are a general support agent. You help customers with:
111
+ - Product information
112
+ - General questions
113
+ - Feedback collection
114
+ - Routing to appropriate departments
115
+
116
+ Be friendly and informative.
117
+ PROMPT
118
+ )
119
+
120
+ # Create the network with optional task routing
121
+ network = RobotLab.create_network(name: "customer_service") do
122
+ task :classifier, classifier, depends_on: :none
123
+ task :billing_agent, billing_agent, depends_on: :optional
124
+ task :tech_agent, tech_agent, depends_on: :optional
125
+ task :account_agent, account_agent, depends_on: :optional
126
+ task :general_agent, general_agent, depends_on: :optional
127
+ end
128
+
129
+ # Run the support system
130
+ puts "Customer Service System"
131
+ puts "=" * 50
132
+ puts
133
+
134
+ test_inquiries = [
135
+ "I was charged twice for my subscription last month",
136
+ "How do I reset my password?",
137
+ "The app crashes when I try to upload photos",
138
+ "What features are included in the pro plan?"
139
+ ]
140
+
141
+ test_inquiries.each do |inquiry|
142
+ puts "Customer: #{inquiry}"
143
+ puts "-" * 50
144
+
145
+ result = network.run(message: inquiry)
146
+
147
+ # Show classification
148
+ if result.context[:classifier]
149
+ puts "Classification: #{result.context[:classifier].last_text_content}"
150
+ end
151
+
152
+ # Show specialist response
153
+ if result.value.is_a?(RobotLab::RobotResult)
154
+ puts "Handled by: #{result.value.robot_name}"
155
+ puts "Response: #{result.value.last_text_content[0..200]}..."
156
+ end
157
+
158
+ puts
159
+ puts "=" * 50
160
+ puts
161
+ end
162
+ ```
163
+
164
+ ## With Context Passing
165
+
166
+ ```ruby
167
+ # Enhanced version with additional context
168
+
169
+ class ContextAwareClassifier < RobotLab::Robot
170
+ def call(result)
171
+ robot_result = run(**extract_run_context(result))
172
+
173
+ # Store classification in context for specialist
174
+ new_result = result
175
+ .with_context(@name.to_sym, robot_result)
176
+ .with_context(:classification, robot_result.last_text_content.strip)
177
+ .with_context(:original_message, result.context[:run_params][:message])
178
+ .continue(robot_result)
179
+
180
+ category = robot_result.last_text_content.to_s.downcase
181
+ case category
182
+ when /billing/ then new_result.activate(:billing_agent)
183
+ when /technical/ then new_result.activate(:tech_agent)
184
+ else new_result.activate(:general_agent)
185
+ end
186
+ end
187
+ end
188
+
189
+ # Specialist can access shared context
190
+ class BillingAgent < RobotLab::Robot
191
+ def call(result)
192
+ # Access context from classifier
193
+ classification = result.context[:classification]
194
+ original_message = result.context[:original_message]
195
+
196
+ robot_result = run(
197
+ **extract_run_context(result),
198
+ classification: classification,
199
+ customer_message: original_message
200
+ )
201
+
202
+ result.with_context(@name.to_sym, robot_result).continue(robot_result)
203
+ end
204
+ end
205
+ ```
206
+
207
+ ## Per-Task Configuration
208
+
209
+ ```ruby
210
+ # Tasks with individual context and tools
211
+ network = RobotLab.create_network(name: "support") do
212
+ task :classifier, classifier, depends_on: :none
213
+ task :billing_agent, billing_agent,
214
+ context: { department: "billing", escalation_level: 2 },
215
+ tools: [RefundTool, InvoiceTool],
216
+ depends_on: :optional
217
+ task :tech_agent, tech_agent,
218
+ context: { department: "technical" },
219
+ mcp: [FilesystemServer],
220
+ depends_on: :optional
221
+ end
222
+ ```
223
+
224
+ ## Pipeline Pattern
225
+
226
+ ```ruby
227
+ # Sequential processing pipeline
228
+ network = RobotLab.create_network(name: "document_processor") do
229
+ task :extract, extractor, depends_on: :none
230
+ task :analyze, analyzer, depends_on: [:extract]
231
+ task :format, formatter, depends_on: [:analyze]
232
+ end
233
+
234
+ result = network.run(message: "Process this document")
235
+ puts result.value.last_text_content
236
+ ```
237
+
238
+ ## Parallel Analysis Pattern
239
+
240
+ ```ruby
241
+ # Fan-out / fan-in pattern
242
+ network = RobotLab.create_network(name: "multi_analysis", concurrency: :threads) do
243
+ task :prepare, preparer, depends_on: :none
244
+
245
+ # These run in parallel
246
+ task :sentiment, sentiment_analyzer, depends_on: [:prepare]
247
+ task :entities, entity_extractor, depends_on: [:prepare]
248
+ task :keywords, keyword_extractor, depends_on: [:prepare]
249
+
250
+ # Waits for all three
251
+ task :summarize, summarizer, depends_on: [:sentiment, :entities, :keywords]
252
+ end
253
+
254
+ result = network.run(message: "Analyze this text")
255
+
256
+ # Access parallel results
257
+ puts "Sentiment: #{result.context[:sentiment].last_text_content}"
258
+ puts "Entities: #{result.context[:entities].last_text_content}"
259
+ puts "Keywords: #{result.context[:keywords].last_text_content}"
260
+ puts "Summary: #{result.value.last_text_content}"
261
+ ```
262
+
263
+ ## Conditional Halting
264
+
265
+ ```ruby
266
+ class ValidatorRobot < RobotLab::Robot
267
+ def call(result)
268
+ robot_result = run(**extract_run_context(result))
269
+
270
+ if robot_result.last_text_content.include?("INVALID")
271
+ # Halt the pipeline early
272
+ result.halt(robot_result)
273
+ else
274
+ result.with_context(@name.to_sym, robot_result).continue(robot_result)
275
+ end
276
+ end
277
+ end
278
+
279
+ network = RobotLab.create_network(name: "validated_pipeline") do
280
+ task :validate, validator, depends_on: :none
281
+ task :process, processor, depends_on: [:validate] # Only runs if not halted
282
+ end
283
+
284
+ result = network.run(message: "Process this")
285
+ if result.halted?
286
+ puts "Validation failed: #{result.value.last_text_content}"
287
+ else
288
+ puts "Processing complete: #{result.value.last_text_content}"
289
+ end
290
+ ```
291
+
292
+ ## Running
293
+
294
+ ```bash
295
+ export ANTHROPIC_API_KEY="your-key"
296
+ ruby examples/customer_service.rb
297
+ ```
298
+
299
+ ## Key Concepts
300
+
301
+ 1. **SimpleFlow Pipeline**: DAG-based execution with dependency management
302
+ 2. **Optional Tasks**: Activated dynamically based on classification
303
+ 3. **Robot#call**: Custom routing logic in classifier robots
304
+ 4. **Context Flow**: Data passed through `result.context`
305
+ 5. **Parallel Execution**: Tasks with same dependencies run concurrently
306
+ 6. **Per-Task Configuration**: Each task can have its own context, tools, and MCP servers
307
+
308
+ ## See Also
309
+
310
+ - [Creating Networks Guide](../guides/creating-networks.md)
311
+ - [Network Orchestration](../architecture/network-orchestration.md)
312
+ - [API Reference: Network](../api/core/network.md)
@@ -0,0 +1,420 @@
1
+ # Rails Application
2
+
3
+ Full Rails integration with Action Cable and background jobs.
4
+
5
+ ## Overview
6
+
7
+ This example demonstrates integrating RobotLab into a Rails application with real-time streaming via Action Cable, background job processing, and persistent conversation history.
8
+
9
+ ## Setup
10
+
11
+ ### 1. Add to Gemfile
12
+
13
+ ```ruby
14
+ # Gemfile
15
+ gem "robot_lab"
16
+ ```
17
+
18
+ ### 2. Run Generator
19
+
20
+ ```bash
21
+ rails generate robot_lab:install
22
+ ```
23
+
24
+ This creates:
25
+
26
+ - `config/initializers/robot_lab.rb`
27
+ - `app/robots/` directory
28
+ - Database migrations for history
29
+
30
+ ### 3. Run Migrations
31
+
32
+ ```bash
33
+ rails db:migrate
34
+ ```
35
+
36
+ ## Configuration
37
+
38
+ ```ruby
39
+ # config/initializers/robot_lab.rb
40
+
41
+ RobotLab.configure do |config|
42
+ config.default_model = ENV.fetch("LLM_MODEL", "claude-sonnet-4")
43
+
44
+ # Enable logging in development
45
+ config.logger = Rails.logger if Rails.env.development?
46
+ end
47
+ ```
48
+
49
+ ## Models
50
+
51
+ ```ruby
52
+ # app/models/conversation_thread.rb
53
+ class ConversationThread < ApplicationRecord
54
+ belongs_to :user
55
+ has_many :messages, class_name: "ConversationMessage", dependent: :destroy
56
+
57
+ validates :external_id, presence: true, uniqueness: true
58
+
59
+ def self.find_or_create_for(user:, external_id: nil)
60
+ external_id ||= SecureRandom.uuid
61
+ find_or_create_by!(user: user, external_id: external_id)
62
+ end
63
+ end
64
+
65
+ # app/models/conversation_message.rb
66
+ class ConversationMessage < ApplicationRecord
67
+ belongs_to :thread, class_name: "ConversationThread"
68
+
69
+ validates :role, presence: true
70
+ validates :content, presence: true
71
+
72
+ scope :ordered, -> { order(:position) }
73
+
74
+ def to_robot_result
75
+ RobotLab::RobotResult.from_hash(
76
+ robot_name: robot_name,
77
+ input: input,
78
+ output: output
79
+ )
80
+ end
81
+ end
82
+ ```
83
+
84
+ ## Robot Definitions
85
+
86
+ ```ruby
87
+ # app/robots/support_robot.rb
88
+ class SupportRobot
89
+ def self.build
90
+ RobotLab.build do
91
+ name "support"
92
+ description "Customer support assistant"
93
+
94
+ template <<~PROMPT
95
+ You are a helpful customer support assistant for our company.
96
+ Be friendly, professional, and thorough in your responses.
97
+ If you need to look up information, use the available tools.
98
+ PROMPT
99
+
100
+ tool :get_user_info do
101
+ description "Get information about the current user"
102
+
103
+ handler do |state:, **_|
104
+ user_id = state.data[:user_id]
105
+ user = User.find(user_id)
106
+
107
+ {
108
+ name: user.name,
109
+ email: user.email,
110
+ plan: user.subscription&.plan || "free",
111
+ member_since: user.created_at.to_date.to_s
112
+ }
113
+ rescue ActiveRecord::RecordNotFound
114
+ { error: "User not found" }
115
+ end
116
+ end
117
+
118
+ tool :get_orders do
119
+ description "Get user's recent orders"
120
+ parameter :limit, type: :integer, default: 5
121
+
122
+ handler do |limit:, state:, **_|
123
+ user_id = state.data[:user_id]
124
+ orders = Order.where(user_id: user_id)
125
+ .order(created_at: :desc)
126
+ .limit(limit)
127
+
128
+ orders.map do |order|
129
+ {
130
+ id: order.external_id,
131
+ status: order.status,
132
+ total: order.total.to_f,
133
+ created_at: order.created_at.iso8601
134
+ }
135
+ end
136
+ end
137
+ end
138
+
139
+ tool :create_ticket do
140
+ description "Create a support ticket"
141
+ parameter :subject, type: :string, required: true
142
+ parameter :description, type: :string, required: true
143
+ parameter :priority, type: :string, enum: %w[low medium high], default: "medium"
144
+
145
+ handler do |subject:, description:, priority:, state:, **_|
146
+ ticket = SupportTicket.create!(
147
+ user_id: state.data[:user_id],
148
+ subject: subject,
149
+ description: description,
150
+ priority: priority
151
+ )
152
+
153
+ {
154
+ success: true,
155
+ ticket_id: ticket.external_id,
156
+ message: "Ticket created successfully"
157
+ }
158
+ rescue => e
159
+ { success: false, error: e.message }
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ ```
166
+
167
+ ## Network Configuration
168
+
169
+ ```ruby
170
+ # app/robots/support_network.rb
171
+ class SupportNetwork
172
+ def self.build
173
+ RobotLab.create_network do
174
+ name "support_network"
175
+ default_model "claude-sonnet-4"
176
+
177
+ history RobotLab::History::ActiveRecordAdapter.new(
178
+ thread_model: ConversationThread,
179
+ result_model: ConversationMessage
180
+ ).to_config
181
+
182
+ add_robot SupportRobot.build
183
+ end
184
+ end
185
+ end
186
+ ```
187
+
188
+ ## Service Object
189
+
190
+ ```ruby
191
+ # app/services/chat_service.rb
192
+ class ChatService
193
+ def initialize(user:, thread_id: nil)
194
+ @user = user
195
+ @thread_id = thread_id
196
+ @network = SupportNetwork.build
197
+ end
198
+
199
+ def call(message:, &streaming_callback)
200
+ user_message = build_message(message)
201
+ state = build_state(user_message)
202
+
203
+ result = @network.run(state: state, user_id: @user.id) do |event|
204
+ streaming_callback&.call(event)
205
+ end
206
+
207
+ {
208
+ thread_id: result.state.thread_id,
209
+ response: extract_response(result),
210
+ messages: result.new_results
211
+ }
212
+ end
213
+
214
+ private
215
+
216
+ def build_message(content)
217
+ if @thread_id
218
+ RobotLab::UserMessage.new(content, thread_id: @thread_id)
219
+ else
220
+ content
221
+ end
222
+ end
223
+
224
+ def build_state(message)
225
+ RobotLab.create_state(
226
+ message: message,
227
+ data: { user_id: @user.id }
228
+ )
229
+ end
230
+
231
+ def extract_response(result)
232
+ result.last_result&.output&.find { |m| m.is_a?(RobotLab::TextMessage) }&.content
233
+ end
234
+ end
235
+ ```
236
+
237
+ ## Controller
238
+
239
+ ```ruby
240
+ # app/controllers/api/chats_controller.rb
241
+ module Api
242
+ class ChatsController < ApplicationController
243
+ before_action :authenticate_user!
244
+
245
+ def create
246
+ service = ChatService.new(
247
+ user: current_user,
248
+ thread_id: params[:thread_id]
249
+ )
250
+
251
+ result = service.call(message: params[:message])
252
+
253
+ render json: {
254
+ thread_id: result[:thread_id],
255
+ response: result[:response]
256
+ }
257
+ end
258
+ end
259
+ end
260
+ ```
261
+
262
+ ## Action Cable Integration
263
+
264
+ ```ruby
265
+ # app/channels/chat_channel.rb
266
+ class ChatChannel < ApplicationCable::Channel
267
+ def subscribed
268
+ stream_for current_user
269
+ end
270
+
271
+ def receive(data)
272
+ ChatJob.perform_later(
273
+ user_id: current_user.id,
274
+ thread_id: data["thread_id"],
275
+ message: data["message"]
276
+ )
277
+ end
278
+ end
279
+
280
+ # app/jobs/chat_job.rb
281
+ class ChatJob < ApplicationJob
282
+ queue_as :default
283
+
284
+ def perform(user_id:, thread_id:, message:)
285
+ user = User.find(user_id)
286
+
287
+ service = ChatService.new(user: user, thread_id: thread_id)
288
+
289
+ service.call(message: message) do |event|
290
+ case event.type
291
+ when :text_delta
292
+ broadcast_to_user(user, type: "text", content: event.text)
293
+ when :tool_call
294
+ broadcast_to_user(user, type: "tool", name: event.name)
295
+ when :complete
296
+ broadcast_to_user(user, type: "complete")
297
+ end
298
+ end
299
+ end
300
+
301
+ private
302
+
303
+ def broadcast_to_user(user, data)
304
+ ChatChannel.broadcast_to(user, data)
305
+ end
306
+ end
307
+ ```
308
+
309
+ ## Frontend (Stimulus)
310
+
311
+ ```javascript
312
+ // app/javascript/controllers/chat_controller.js
313
+ import { Controller } from "@hotwired/stimulus"
314
+ import { createConsumer } from "@rails/actioncable"
315
+
316
+ export default class extends Controller {
317
+ static targets = ["messages", "input", "response"]
318
+
319
+ connect() {
320
+ this.consumer = createConsumer()
321
+ this.channel = this.consumer.subscriptions.create("ChatChannel", {
322
+ received: (data) => this.handleMessage(data)
323
+ })
324
+ }
325
+
326
+ disconnect() {
327
+ this.channel?.unsubscribe()
328
+ }
329
+
330
+ send() {
331
+ const message = this.inputTarget.value.trim()
332
+ if (!message) return
333
+
334
+ this.appendMessage("user", message)
335
+ this.inputTarget.value = ""
336
+
337
+ // Create response container
338
+ this.currentResponse = document.createElement("div")
339
+ this.currentResponse.className = "message assistant"
340
+ this.messagesTarget.appendChild(this.currentResponse)
341
+
342
+ this.channel.send({
343
+ message: message,
344
+ thread_id: this.threadId
345
+ })
346
+ }
347
+
348
+ handleMessage(data) {
349
+ switch (data.type) {
350
+ case "text":
351
+ this.currentResponse.textContent += data.content
352
+ break
353
+ case "tool":
354
+ // Show tool indicator
355
+ break
356
+ case "complete":
357
+ this.threadId = data.thread_id
358
+ break
359
+ }
360
+ }
361
+
362
+ appendMessage(role, content) {
363
+ const div = document.createElement("div")
364
+ div.className = `message ${role}`
365
+ div.textContent = content
366
+ this.messagesTarget.appendChild(div)
367
+ }
368
+ }
369
+ ```
370
+
371
+ ## View
372
+
373
+ ```erb
374
+ <!-- app/views/chats/show.html.erb -->
375
+ <div data-controller="chat">
376
+ <div class="messages" data-chat-target="messages">
377
+ <!-- Messages appear here -->
378
+ </div>
379
+
380
+ <form data-action="submit->chat#send">
381
+ <input type="text"
382
+ data-chat-target="input"
383
+ placeholder="Type a message..."
384
+ autocomplete="off">
385
+ <button type="submit">Send</button>
386
+ </form>
387
+ </div>
388
+ ```
389
+
390
+ ## Running
391
+
392
+ ```bash
393
+ # Install dependencies
394
+ bundle install
395
+ yarn install
396
+
397
+ # Setup database
398
+ rails db:migrate
399
+
400
+ # Set API key
401
+ export ANTHROPIC_API_KEY="your-key"
402
+
403
+ # Start server
404
+ bin/dev
405
+ ```
406
+
407
+ ## Key Concepts
408
+
409
+ 1. **Robot Classes**: Encapsulate robot definitions
410
+ 2. **Network Classes**: Configure multi-robot networks
411
+ 3. **Service Objects**: Handle business logic
412
+ 4. **Action Cable**: Real-time streaming to browser
413
+ 5. **Background Jobs**: Non-blocking processing
414
+ 6. **History Persistence**: ActiveRecord integration
415
+
416
+ ## See Also
417
+
418
+ - [Rails Integration Guide](../guides/rails-integration.md)
419
+ - [Streaming Guide](../guides/streaming.md)
420
+ - [History Guide](../guides/history.md)